mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-14 09:48:02 +00:00
Compare commits
2 Commits
Hanmac-pat
...
forge-2.0.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b13b17bba | ||
|
|
b4b96240f9 |
139
.github/workflows/maven-publish.yml
vendored
139
.github/workflows/maven-publish.yml
vendored
@@ -2,21 +2,10 @@ 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
|
||||
@@ -43,132 +32,10 @@ jobs:
|
||||
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 }}
|
||||
- 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 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
|
||||
mvn -U -B clean -P windows-linux install release:clean release:prepare release:perform -T 1C -Dcardforge-repo.username=${{ secrets.FTP_USERNAME }} -Dcardforge-repo.password=${{ secrets.FTP_PASSWORD }}
|
||||
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 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 }}
|
||||
|
||||
32
.github/workflows/snapshot-both-pc-android.yml
vendored
32
.github/workflows/snapshot-both-pc-android.yml
vendored
@@ -8,9 +8,6 @@ on:
|
||||
description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)'
|
||||
required: false
|
||||
default: false
|
||||
schedule:
|
||||
# * is a special character in YAML so you have to quote this string
|
||||
- cron: '00 18 * * *'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -112,21 +109,16 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
|
||||
- name: Upload snapshot to GitHub Prerelease
|
||||
uses: ncipollo/release-action@v1
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: 📂 Sync files
|
||||
uses: SamKirkland/FTP-Deploy-Action@v4.3.4
|
||||
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 }}
|
||||
server: ftp.cardforge.org
|
||||
username: ${{ secrets.FTP_USERNAME }}
|
||||
password: ${{ secrets.FTP_PASSWORD }}
|
||||
local-dir: izpack/
|
||||
server-dir: downloads/dailysnapshots/
|
||||
state-name: .ftp-deploy-both-sync-state.json
|
||||
exclude: |
|
||||
*.pom
|
||||
*.repositories
|
||||
*.xml
|
||||
|
||||
4
.github/workflows/snapshots-android.yml
vendored
4
.github/workflows/snapshots-android.yml
vendored
@@ -13,6 +13,10 @@ on:
|
||||
# description: 'Upload the completed Android package'
|
||||
# required: false
|
||||
# default: true
|
||||
schedule:
|
||||
# * is a special character in YAML so you have to quote this string
|
||||
- cron: '00 19 * * *'
|
||||
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
3
.github/workflows/snapshots-pc.yml
vendored
3
.github/workflows/snapshots-pc.yml
vendored
@@ -8,6 +8,9 @@ on:
|
||||
description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)'
|
||||
required: false
|
||||
default: false
|
||||
schedule:
|
||||
# * is a special character in YAML so you have to quote this string
|
||||
- cron: '30 18 * * *'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -12,7 +12,6 @@
|
||||
.settings
|
||||
.classpath
|
||||
.project
|
||||
.checkstyle
|
||||
|
||||
# Ignore VS Code config files
|
||||
|
||||
@@ -25,8 +24,6 @@
|
||||
|
||||
nbactions.xml
|
||||
|
||||
# Ignore flattened pom
|
||||
.flattened-pom.xml
|
||||
|
||||
# Ignore binaries, temp files and test output, everywhere
|
||||
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
<!--
|
||||
Derived from: https://stackoverflow.com/a/67002852
|
||||
-->
|
||||
<settings xmlns="http://maven.apache.org/SETTINGS/1.2.0"
|
||||
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">
|
||||
<mirrors>
|
||||
<mirror>
|
||||
<id>4thline-repo-http-unblocker</id>
|
||||
<mirrorOf>4thline-repo</mirrorOf>
|
||||
<name></name>
|
||||
<url>http://4thline.org/m2</url>
|
||||
</mirror>
|
||||
</mirrors>
|
||||
|
||||
<servers>
|
||||
<server>
|
||||
<id>cardforge-repo</id>
|
||||
|
||||
@@ -6,7 +6,7 @@ Dev instructions here: [Getting Started](https://github.com/Card-Forge/forge/wik
|
||||
|
||||
## Requirements / Tools
|
||||
|
||||
- your favourite Java IDE (IntelliJ, Eclipse, VSCodium, Emacs, Vi...)
|
||||
- you favourite Java IDE (IntelliJ, Eclipse, VSCodium, Emacs, Vi...)
|
||||
- Java JDK 17 or later
|
||||
- Git
|
||||
- Git client (optional)
|
||||
@@ -28,6 +28,7 @@ Dev instructions here: [Getting Started](https://github.com/Card-Forge/forge/wik
|
||||
|
||||
IntelliJ is the recommended IDE for Forge development. Quick start guide for [setting up the Forge project within IntelliJ](https://github.com/Card-Forge/forge/wiki/IntelliJ-setup).
|
||||
|
||||
|
||||
## Eclipse
|
||||
|
||||
Eclipse includes Maven integration so a separate install is not necessary. For other IDEs, your mileage may vary.
|
||||
@@ -123,11 +124,10 @@ TBD
|
||||
|
||||
SNAPSHOT builds can be built via the Maven integration in Eclipse.
|
||||
|
||||
1. Create a Maven build for the forge top-level project. Right-click on the forge project. Run as.. > Maven build...
|
||||
|
||||
1) Create a Maven build for the forge top-level project. Right-click on the forge project. Run as.. > Maven build...
|
||||
- On the Main tab, set Goals: clean install, set Profiles: windows-linux
|
||||
|
||||
2. Run forge Maven build. If everything built, you should see "BUILD SUCCESS" in the Console View.
|
||||
2) Run forge Maven build. If everything built, you should see "BUILD SUCCESS" in the Console View.
|
||||
|
||||
The resulting snapshot will be found at: forge-gui-desktop/target/forge-gui-desktop-[version]-SNAPSHOT
|
||||
|
||||
@@ -158,19 +158,13 @@ The platform-specific projects are:
|
||||
|
||||
#### 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.
|
||||
The forge-gui project includes the scripting resource definitions in the res/ path.
|
||||
|
||||
#### forge-gui-android
|
||||
|
||||
|
||||
@@ -26,14 +26,13 @@ Join the **Forge community** on [Discord](https://discord.gg/HcPJNyD66a)!
|
||||
|
||||
### 📥 Desktop Installation
|
||||
1. **Latest Releases:** Download the latest version [here](https://github.com/Card-Forge/forge/releases/latest).
|
||||
2. **Snapshot Build:** For the latest development version, grab the `forge-gui-desktop` tarball from our [Snapshot Build](https://github.com/Card-Forge/forge/releases/tag/daily-snapshots).
|
||||
2. **Snapshot Build:** For the latest development version, grab the `forge-gui-desktop` tarball from our [Snapshot Build](https://downloads.cardforge.org/dailysnapshots/).
|
||||
- **Tip:** Extract to a new folder to prevent version conflicts.
|
||||
3. **User Data Management:** Previous players’ data is preserved during upgrades.
|
||||
4. **Java Requirement:** Ensure you have **Java 17 or later** installed.
|
||||
|
||||
### 📱 Android Installation
|
||||
- _(Note: **Android 11** is the minimum requirements with at least **6GB RAM** to run smoothly. You need to enable **"Install unknown apps"** for Forge to initialize and update itself)_
|
||||
- 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.
|
||||
- Download the **APK** from the [Snapshot Build](https://downloads.cardforge.org/dailysnapshots/). On the first launch, Forge will automatically download all necessary assets.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<artifactId>forge</artifactId>
|
||||
<groupId>forge</groupId>
|
||||
<version>${revision}</version>
|
||||
<version>2.0.01</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
|
||||
@@ -45,16 +45,17 @@ public class BiomeStructureDataMappingEditor extends JComponent {
|
||||
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))
|
||||
if(!(value instanceof BiomeStructureData.BiomeStructureDataMapping))
|
||||
return label;
|
||||
BiomeStructureData.BiomeStructureDataMapping data=(BiomeStructureData.BiomeStructureDataMapping) value;
|
||||
// Get the renderer component from parent class
|
||||
|
||||
label.setText(biomeData.name);
|
||||
label.setText(data.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));
|
||||
if(itemAtlas.has(data.name))
|
||||
label.setIcon(itemAtlas.get(data.name));
|
||||
else
|
||||
{
|
||||
ImageIcon img=itemAtlas.getAny();
|
||||
|
||||
@@ -25,8 +25,9 @@ public class DialogOptionEditor extends JComponent{
|
||||
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))
|
||||
if(!(value instanceof DialogData))
|
||||
return label;
|
||||
DialogData dialog=(DialogData) value;
|
||||
StringBuilder builder=new StringBuilder();
|
||||
if(dialog.name==null||dialog.name.isEmpty())
|
||||
builder.append("[[Blank Option]]");
|
||||
|
||||
@@ -27,16 +27,17 @@ public class ItemsEditor extends JComponent {
|
||||
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))
|
||||
if(!(value instanceof ItemData))
|
||||
return label;
|
||||
ItemData Item=(ItemData) value;
|
||||
// Get the renderer component from parent class
|
||||
|
||||
label.setText(item.name);
|
||||
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));
|
||||
if(itemAtlas.has(Item.iconName))
|
||||
label.setIcon(itemAtlas.get(Item.iconName));
|
||||
else
|
||||
{
|
||||
ImageIcon img=itemAtlas.getAny();
|
||||
|
||||
@@ -26,8 +26,9 @@ public class QuestEditor extends JComponent {
|
||||
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))
|
||||
if(!(value instanceof AdventureQuestData))
|
||||
return label;
|
||||
AdventureQuestData quest=(AdventureQuestData) value;
|
||||
// Get the renderer component from parent class
|
||||
|
||||
label.setText(quest.name);
|
||||
|
||||
@@ -26,8 +26,9 @@ public class QuestStageEditor extends JComponent{
|
||||
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))
|
||||
if(!(value instanceof AdventureQuestStage))
|
||||
return label;
|
||||
AdventureQuestStage stageData=(AdventureQuestStage) value;
|
||||
label.setText(stageData.name);
|
||||
//label.setIcon(new ImageIcon(Config.instance().getFilePath(stageData.sourcePath))); //Type icon eventually?
|
||||
return label;
|
||||
|
||||
@@ -43,8 +43,9 @@ public class WorldEditor extends JComponent {
|
||||
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))
|
||||
if(!(value instanceof BiomeData))
|
||||
return label;
|
||||
BiomeData biome=(BiomeData) value;
|
||||
// Get the renderer component from parent class
|
||||
|
||||
label.setText(biome.name);
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<artifactId>forge</artifactId>
|
||||
<groupId>forge</groupId>
|
||||
<version>${revision}</version>
|
||||
<version>2.0.01</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>forge-ai</artifactId>
|
||||
|
||||
@@ -37,7 +37,6 @@ import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.SpellAbilityPredicates;
|
||||
import forge.game.staticability.StaticAbility;
|
||||
import forge.game.staticability.StaticAbilityAssignCombatDamageAsUnblocked;
|
||||
import forge.game.staticability.StaticAbilityMode;
|
||||
import forge.game.trigger.Trigger;
|
||||
import forge.game.trigger.TriggerType;
|
||||
import forge.game.zone.ZoneType;
|
||||
@@ -116,8 +115,8 @@ public class AiAttackController {
|
||||
} // overloaded constructor to evaluate single specified attacker
|
||||
|
||||
private void refreshCombatants(GameEntity defender) {
|
||||
if (defender instanceof Card card && card.isBattle()) {
|
||||
this.oppList = getOpponentCreatures(card.getProtectingPlayer());
|
||||
if (defender instanceof Card && ((Card) defender).isBattle()) {
|
||||
this.oppList = getOpponentCreatures(((Card) defender).getProtectingPlayer());
|
||||
} else {
|
||||
this.oppList = getOpponentCreatures(defendingOpponent);
|
||||
}
|
||||
@@ -313,8 +312,7 @@ public class AiAttackController {
|
||||
}
|
||||
}
|
||||
// Poison opponent if unblocked
|
||||
if (defender instanceof Player player
|
||||
&& ComputerUtilCombat.poisonIfUnblocked(attacker, player) > 0) {
|
||||
if (defender instanceof Player && ComputerUtilCombat.poisonIfUnblocked(attacker, (Player) defender) > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -851,9 +849,10 @@ public class AiAttackController {
|
||||
// decided to attack another defender so related lists need to be updated
|
||||
// (though usually rather try to avoid this situation for performance reasons)
|
||||
if (defender != defendingOpponent) {
|
||||
if (defender instanceof Player p) {
|
||||
defendingOpponent = p;
|
||||
} else if (defender instanceof Card defCard) {
|
||||
if (defender instanceof Player) {
|
||||
defendingOpponent = (Player) defender;
|
||||
} else if (defender instanceof Card) {
|
||||
Card defCard = (Card) defender;
|
||||
if (defCard.isBattle()) {
|
||||
defendingOpponent = defCard.getProtectingPlayer();
|
||||
} else {
|
||||
@@ -947,8 +946,8 @@ public class AiAttackController {
|
||||
return 1;
|
||||
}
|
||||
// or weakest player
|
||||
if (r1.getKey() instanceof Player p1 && r2.getKey() instanceof Player p2) {
|
||||
return p1.getLife() - p2.getLife();
|
||||
if (r1.getKey() instanceof Player && r2.getKey() instanceof Player) {
|
||||
return ((Player) r1.getKey()).getLife() - ((Player) r2.getKey()).getLife();
|
||||
}
|
||||
}
|
||||
return r2.getValue() - r1.getValue();
|
||||
@@ -1315,7 +1314,7 @@ public class AiAttackController {
|
||||
attackersAssigned.add(attacker);
|
||||
|
||||
// check if attackers are enough to finish the attacked planeswalker
|
||||
if (i < left.size() - 1 && defender instanceof Card card) {
|
||||
if (i < left.size() - 1 && defender instanceof Card) {
|
||||
final int blockNum = this.blockers.size();
|
||||
int attackNum = 0;
|
||||
int damage = 0;
|
||||
@@ -1329,7 +1328,7 @@ public class AiAttackController {
|
||||
}
|
||||
}
|
||||
// if enough damage: switch to next planeswalker
|
||||
if (damage >= ComputerUtilCombat.getDamageToKill(card, true)) {
|
||||
if (damage >= ComputerUtilCombat.getDamageToKill((Card) defender, true)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1397,7 +1396,7 @@ public class AiAttackController {
|
||||
);
|
||||
|
||||
// total power of the defending creatures, used in predicting whether a gang block can kill the attacker
|
||||
defPower = CardLists.getTotalPower(validBlockers, null);
|
||||
defPower = CardLists.getTotalPower(validBlockers, true, false);
|
||||
|
||||
// look at the attacker in relation to the blockers to establish a
|
||||
// number of factors about the attacking context that will be relevant
|
||||
@@ -1588,7 +1587,7 @@ public class AiAttackController {
|
||||
// but there are no creatures it can target, no need to exert with it
|
||||
boolean missTarget = false;
|
||||
for (StaticAbility st : c.getStaticAbilities()) {
|
||||
if (!st.checkMode(StaticAbilityMode.OptionalAttackCost)) {
|
||||
if (!"OptionalAttackCost".equals(st.getParam("Mode"))) {
|
||||
continue;
|
||||
}
|
||||
SpellAbility sa = st.getPayingTrigSA();
|
||||
@@ -1755,12 +1754,10 @@ public class AiAttackController {
|
||||
private boolean doRevengeOfRavensAttackLogic(final GameEntity defender, final Queue<Card> attackersLeft, int numForcedAttackers, int maxAttack) {
|
||||
// TODO: detect Revenge of Ravens by the trigger instead of by name
|
||||
boolean revengeOfRavens = false;
|
||||
if (defender instanceof Player player) {
|
||||
revengeOfRavens = !CardLists.filter(player.getCardsIn(ZoneType.Battlefield),
|
||||
CardPredicates.nameEquals("Revenge of Ravens")).isEmpty();
|
||||
} else if (defender instanceof Card card) {
|
||||
revengeOfRavens = !CardLists.filter(card.getController().getCardsIn(ZoneType.Battlefield),
|
||||
CardPredicates.nameEquals("Revenge of Ravens")).isEmpty();
|
||||
if (defender instanceof Player) {
|
||||
revengeOfRavens = !CardLists.filter(((Player)defender).getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Revenge of Ravens")).isEmpty();
|
||||
} else if (defender instanceof Card) {
|
||||
revengeOfRavens = !CardLists.filter(((Card)defender).getController().getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Revenge of Ravens")).isEmpty();
|
||||
}
|
||||
|
||||
if (!revengeOfRavens) {
|
||||
|
||||
@@ -161,12 +161,12 @@ public class AiBlockController {
|
||||
// defend battles with fewer defense counters before battles with more defense counters,
|
||||
// if planeswalker/battle will be too difficult to defend don't even bother
|
||||
for (GameEntity defender : defenders) {
|
||||
if ((defender instanceof Card card1 && card1.getController().equals(ai))
|
||||
|| (defender instanceof Card card2 && card2.isBattle() && card2.getProtectingPlayer().equals(ai))) {
|
||||
final CardCollection ccAttackers = combat.getAttackersOf(defender);
|
||||
if ((defender instanceof Card && ((Card) defender).getController().equals(ai))
|
||||
|| (defender instanceof Card && ((Card) defender).isBattle() && ((Card) defender).getProtectingPlayer().equals(ai))) {
|
||||
final CardCollection attackers = combat.getAttackersOf(defender);
|
||||
// Begin with the attackers that pose the biggest threat
|
||||
CardLists.sortByPowerDesc(ccAttackers);
|
||||
sortedAttackers.addAll(ccAttackers);
|
||||
CardLists.sortByPowerDesc(attackers);
|
||||
sortedAttackers.addAll(attackers);
|
||||
} else if (defender instanceof Player && defender.equals(ai)) {
|
||||
firstAttacker = combat.getAttackersOf(defender);
|
||||
CardLists.sortByPowerDesc(firstAttacker);
|
||||
@@ -872,9 +872,9 @@ public class AiBlockController {
|
||||
CardCollection threatenedPWs = new CardCollection();
|
||||
for (final Card attacker : attackers) {
|
||||
GameEntity def = combat.getDefenderByAttacker(attacker);
|
||||
if (def instanceof Card card) {
|
||||
if (def instanceof Card) {
|
||||
if (!onlyIfLethal) {
|
||||
threatenedPWs.add(card);
|
||||
threatenedPWs.add((Card) def);
|
||||
} else {
|
||||
int damageToPW = 0;
|
||||
for (final Card pwatkr : combat.getAttackersOf(def)) {
|
||||
@@ -906,12 +906,12 @@ public class AiBlockController {
|
||||
continue;
|
||||
}
|
||||
GameEntity def = combat.getDefenderByAttacker(attacker);
|
||||
if (def instanceof Card card && threatenedPWs.contains(def)) {
|
||||
if (def instanceof Card && threatenedPWs.contains(def)) {
|
||||
Card blockerDecided = null;
|
||||
for (final Card blocker : chumpPWDefenders) {
|
||||
if (CombatUtil.canBlock(attacker, blocker, combat)) {
|
||||
combat.addBlocker(attacker, blocker);
|
||||
pwsWithChumpBlocks.add(card);
|
||||
pwsWithChumpBlocks.add((Card) def);
|
||||
chosenChumpBlockers.add(blocker);
|
||||
blockerDecided = blocker;
|
||||
blockersLeft.remove(blocker);
|
||||
@@ -1346,8 +1346,8 @@ public class AiBlockController {
|
||||
&& ai.getZone(ZoneType.Hand).contains(CardPredicates.CREATURES)
|
||||
&& aiCreatureCount + maxCreatDiffWithRepl >= oppCreatureCount;
|
||||
boolean wantToSavePlaneswalker = MyRandom.percentTrue(chanceToSavePW)
|
||||
&& combat.getDefenderByAttacker(attacker) instanceof Card card
|
||||
&& card.isPlaneswalker();
|
||||
&& combat.getDefenderByAttacker(attacker) instanceof Card
|
||||
&& ((Card) combat.getDefenderByAttacker(attacker)).isPlaneswalker();
|
||||
boolean wantToTradeDownToSavePW = chanceToTradeDownToSaveWalker > 0;
|
||||
|
||||
return ((evalBlk <= evalAtk + 1) || (wantToSavePlaneswalker && wantToTradeDownToSavePW)) // "1" accounts for tapped.
|
||||
|
||||
@@ -23,12 +23,10 @@ import com.google.common.collect.Lists;
|
||||
import forge.ai.AiCardMemory.MemorySet;
|
||||
import forge.ai.ability.ChangeZoneAi;
|
||||
import forge.ai.ability.LearnAi;
|
||||
import forge.ai.simulation.GameStateEvaluator;
|
||||
import forge.ai.simulation.SpellAbilityPicker;
|
||||
import forge.card.CardStateName;
|
||||
import forge.card.CardType;
|
||||
import forge.card.MagicColor;
|
||||
import forge.card.mana.ManaAtom;
|
||||
import forge.card.mana.ManaCost;
|
||||
import forge.deck.Deck;
|
||||
import forge.deck.DeckSection;
|
||||
@@ -54,7 +52,6 @@ import forge.game.replacement.ReplacementType;
|
||||
import forge.game.spellability.*;
|
||||
import forge.game.staticability.StaticAbility;
|
||||
import forge.game.staticability.StaticAbilityDisableTriggers;
|
||||
import forge.game.staticability.StaticAbilityMode;
|
||||
import forge.game.staticability.StaticAbilityMustTarget;
|
||||
import forge.game.trigger.Trigger;
|
||||
import forge.game.trigger.TriggerType;
|
||||
@@ -69,16 +66,11 @@ import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import static forge.ai.ComputerUtilMana.getAvailableManaEstimate;
|
||||
import static java.lang.Math.max;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* AiController class.
|
||||
@@ -295,7 +287,7 @@ public class AiController {
|
||||
}
|
||||
|
||||
// can't fetch partner isn't problematic
|
||||
if (tr.isKeyword(Keyword.PARTNER)) {
|
||||
if (tr.getKeyword() != null && tr.getKeyword().getOriginal().startsWith("Partner")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -502,8 +494,6 @@ public class AiController {
|
||||
return null;
|
||||
}
|
||||
|
||||
landList = ComputerUtilCard.dedupeCards(landList);
|
||||
|
||||
CardCollection nonLandsInHand = CardLists.filter(player.getCardsIn(ZoneType.Hand), CardPredicates.NON_LANDS);
|
||||
|
||||
// Some considerations for Momir/MoJhoSto
|
||||
@@ -544,7 +534,7 @@ public class AiController {
|
||||
landList = unreflectedLands;
|
||||
}
|
||||
|
||||
// try to skip lands that enter the battlefield tapped if we might want to play something this turn
|
||||
//try to skip lands that enter the battlefield tapped
|
||||
if (!nonLandsInHand.isEmpty()) {
|
||||
CardCollection nonTappedLands = new CardCollection();
|
||||
for (Card land : landList) {
|
||||
@@ -552,6 +542,7 @@ public class AiController {
|
||||
final Map<AbilityKey, Object> repParams = AbilityKey.mapFromAffected(land);
|
||||
repParams.put(AbilityKey.Origin, land.getZone().getZoneType());
|
||||
repParams.put(AbilityKey.Destination, ZoneType.Battlefield);
|
||||
repParams.put(AbilityKey.Source, land);
|
||||
|
||||
// add Params for AddCounter Replacements
|
||||
GameEntityCounterTable table = new GameEntityCounterTable();
|
||||
@@ -579,48 +570,10 @@ public class AiController {
|
||||
|
||||
nonTappedLands.add(land);
|
||||
}
|
||||
|
||||
// if we have the choice, see if we can play an untapped land
|
||||
if (!nonTappedLands.isEmpty()) {
|
||||
// If we have a lot of mana, prefer untapped lands.
|
||||
// We're either topdecking or have drawn enough the tempo no longer matters.
|
||||
int mana_available = getAvailableManaEstimate(player);
|
||||
if (mana_available > 6) {
|
||||
landList = nonTappedLands;
|
||||
} else {
|
||||
// get the costs of the nonland cards in hand and the mana we have available.
|
||||
// If adding one won't make something new castable, then pick a tapland.
|
||||
int max_inc = 0;
|
||||
for (Card c : nonTappedLands) {
|
||||
max_inc = max(max_inc, c.getMaxManaProduced());
|
||||
}
|
||||
// check for lands with no mana abilities
|
||||
if (max_inc > 0) {
|
||||
boolean found = false;
|
||||
for (Card c : nonLandsInHand) {
|
||||
// TODO make this work better with split cards and Monocolored Hybrid
|
||||
ManaCost cost = c.getManaCost();
|
||||
// check for incremental cmc
|
||||
// check for X cost spells
|
||||
if ((cost.getCMC() - mana_available) * (cost.getCMC() - mana_available - max_inc - 1) < 0 ||
|
||||
(cost.countX() > 0 && cost.getCMC() >= mana_available)) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (found) {
|
||||
landList = nonTappedLands;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Early out if we only have one card left
|
||||
if (landList.size() == 1) {
|
||||
return landList.get(0);
|
||||
}
|
||||
|
||||
// Choose first land to be able to play a one drop
|
||||
if (player.getLandsInPlay().isEmpty()) {
|
||||
@@ -642,85 +595,39 @@ public class AiController {
|
||||
}
|
||||
}
|
||||
|
||||
// play lands with a basic type and/or color that is needed the most
|
||||
//play lands with a basic type that is needed the most
|
||||
final CardCollectionView landsInBattlefield = player.getCardsIn(ZoneType.Battlefield);
|
||||
final List<String> basics = Lists.newArrayList();
|
||||
|
||||
// what colors are available?
|
||||
int[] counts = new int[6]; // in WUBRGC order
|
||||
|
||||
for (Card c : player.getCardsIn(ZoneType.Battlefield)) {
|
||||
for (SpellAbility m: c.getManaAbilities()) {
|
||||
m.setActivatingPlayer(c.getController());
|
||||
for (AbilityManaPart mp : m.getAllManaParts()) {
|
||||
for (String part : mp.mana(m).split(" ")) {
|
||||
// TODO handle any
|
||||
int index = ManaAtom.getIndexFromName(part);
|
||||
if (index != -1) {
|
||||
counts[index] += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// what types can I go get?
|
||||
int[] basic_counts = new int[5]; // in WUBRG order
|
||||
for (final String name : MagicColor.Constant.BASIC_LANDS) {
|
||||
if (!CardLists.getType(landList, name).isEmpty()) {
|
||||
basics.add(name);
|
||||
}
|
||||
}
|
||||
if (!basics.isEmpty()) {
|
||||
for (int i = 0; i < MagicColor.Constant.BASIC_LANDS.size(); i++) {
|
||||
String b = MagicColor.Constant.BASIC_LANDS.get(i);
|
||||
// Which basic land is least available
|
||||
int minSize = Integer.MAX_VALUE;
|
||||
String minType = null;
|
||||
|
||||
for (String b : basics) {
|
||||
final int num = CardLists.getType(landsInBattlefield, b).size();
|
||||
basic_counts[i] = num;
|
||||
}
|
||||
}
|
||||
// pick the land with the best score.
|
||||
// use the evaluation plus a modifier for each new color pip and basic type
|
||||
Card toReturn = Aggregates.itemWithMax(IterableUtil.filter(landList, Card::hasPlayableLandFace),
|
||||
(card -> {
|
||||
// base score is for the evaluation score
|
||||
int score = GameStateEvaluator.evaluateLand(card);
|
||||
// add for new basic type
|
||||
for (String cardType: card.getType()) {
|
||||
int index = MagicColor.Constant.BASIC_LANDS.indexOf(cardType);
|
||||
if (index != -1 && basic_counts[index] == 0) {
|
||||
score += 25;
|
||||
if (num < minSize) {
|
||||
minType = b;
|
||||
minSize = num;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO handle fetchlands and what they can fetch for
|
||||
// determine new color pips
|
||||
int[] card_counts = new int[6]; // in WUBRGC order
|
||||
for (SpellAbility m: card.getManaAbilities()) {
|
||||
m.setActivatingPlayer(card.getController());
|
||||
for (AbilityManaPart mp : m.getAllManaParts()) {
|
||||
for (String part : mp.mana(m).split(" ")) {
|
||||
// TODO handle any
|
||||
int index = ManaAtom.getIndexFromName(part);
|
||||
if (index != -1) {
|
||||
card_counts[index] += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (minType != null) {
|
||||
landList = CardLists.getType(landList, minType);
|
||||
}
|
||||
|
||||
// use 1 / x+1 for diminishing returns
|
||||
// TODO use max pips of each color in the deck from deck statistics to weight this
|
||||
for (int i = 0; i < card_counts.length; i++) {
|
||||
int diff = (card_counts[i] * 50) / (counts[i] + 1);
|
||||
score += diff;
|
||||
// pick dual lands if available
|
||||
if (landList.anyMatch(CardPredicates.NONBASIC_LANDS)) {
|
||||
landList = CardLists.filter(landList, CardPredicates.NONBASIC_LANDS);
|
||||
}
|
||||
|
||||
// TODO utility lands only if we have enough to pay their costs
|
||||
// TODO Tron lands and other lands that care about land counts
|
||||
|
||||
return score;
|
||||
}));
|
||||
return toReturn;
|
||||
}
|
||||
return ComputerUtilCard.getBestLandToPlayAI(landList);
|
||||
}
|
||||
|
||||
// if return true, go to next phase
|
||||
@@ -868,15 +775,9 @@ public class AiController {
|
||||
if (currentState != null) {
|
||||
host.setState(sa.getCardStateName(), false);
|
||||
}
|
||||
if (sa.isSpell()) {
|
||||
host.setCastSA(sa);
|
||||
}
|
||||
|
||||
AiPlayDecision decision = canPlayAndPayForFace(sa);
|
||||
|
||||
if (sa.isSpell()) {
|
||||
host.setCastSA(null);
|
||||
}
|
||||
if (currentState != null) {
|
||||
host.setState(currentState, false);
|
||||
}
|
||||
@@ -1017,7 +918,7 @@ public class AiController {
|
||||
Sentry.setExtra("Card", card.getName());
|
||||
Sentry.setExtra("SA", sa.toString());
|
||||
|
||||
boolean canPlay = SpellApiToAi.Converter.get(sa).canPlayAIWithSubs(player, sa);
|
||||
boolean canPlay = SpellApiToAi.Converter.get(sa.getApi()).canPlayAIWithSubs(player, sa);
|
||||
|
||||
// remove added extra
|
||||
Sentry.removeExtra("Card");
|
||||
@@ -1130,7 +1031,7 @@ public class AiController {
|
||||
// Memory Crystal-like effects need special handling
|
||||
for (Card c : game.getCardsIn(ZoneType.Battlefield)) {
|
||||
for (StaticAbility s : c.getStaticAbilities()) {
|
||||
if (s.checkMode(StaticAbilityMode.ReduceCost)
|
||||
if ("ReduceCost".equals(s.getParam("Mode"))
|
||||
&& "Spell.Buyback".equals(s.getParam("ValidSpell"))) {
|
||||
neededMana -= AbilityUtils.calculateAmount(c, s.getParam("Amount"), s);
|
||||
}
|
||||
@@ -1140,7 +1041,7 @@ public class AiController {
|
||||
neededMana = 0;
|
||||
}
|
||||
|
||||
int hasMana = getAvailableManaEstimate(player, false);
|
||||
int hasMana = ComputerUtilMana.getAvailableManaEstimate(player, false);
|
||||
if (hasMana < neededMana - 1) {
|
||||
return true;
|
||||
}
|
||||
@@ -1395,9 +1296,9 @@ public class AiController {
|
||||
if (spell instanceof SpellApiBased) {
|
||||
boolean chance = false;
|
||||
if (withoutPayingManaCost) {
|
||||
chance = SpellApiToAi.Converter.get(spell).doTriggerNoCostWithSubs(player, spell, mandatory);
|
||||
chance = SpellApiToAi.Converter.get(spell.getApi()).doTriggerNoCostWithSubs(player, spell, mandatory);
|
||||
} else {
|
||||
chance = SpellApiToAi.Converter.get(spell).doTriggerAI(player, spell, mandatory);
|
||||
chance = SpellApiToAi.Converter.get(spell.getApi()).doTriggerAI(player, spell, mandatory);
|
||||
}
|
||||
if (!chance) {
|
||||
return AiPlayDecision.TargetingFailed;
|
||||
@@ -1549,7 +1450,7 @@ public class AiController {
|
||||
int minCMCInHand = Aggregates.min(inHand, Card::getCMC);
|
||||
if (minCMCInHand == Integer.MAX_VALUE)
|
||||
minCMCInHand = 0;
|
||||
int predictedMana = getAvailableManaEstimate(player, true);
|
||||
int predictedMana = ComputerUtilMana.getAvailableManaEstimate(player, true);
|
||||
|
||||
boolean canCastWithLandDrop = (predictedMana + 1 >= minCMCInHand) && minCMCInHand > 0 && !isTapLand;
|
||||
boolean cantCastAnythingNow = predictedMana < minCMCInHand;
|
||||
@@ -1710,8 +1611,7 @@ public class AiController {
|
||||
Sentry.captureMessage(ex.getMessage() + "\nAssertionError [verifyTransitivity]: " + assertex);
|
||||
}
|
||||
|
||||
final ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
Future<SpellAbility> future = executor.submit(() -> {
|
||||
CompletableFuture<SpellAbility> future = CompletableFuture.supplyAsync(() -> {
|
||||
//avoid ComputerUtil.aiLifeInDanger in loops as it slows down a lot.. call this outside loops will generally be fast...
|
||||
boolean isLifeInDanger = useLivingEnd && ComputerUtil.aiLifeInDanger(player, true, 0);
|
||||
for (final SpellAbility sa : ComputerUtilAbility.getOriginalAndAltCostAbilities(all, player)) {
|
||||
@@ -1730,35 +1630,28 @@ public class AiController {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// living end AI decks
|
||||
// TODO: generalize the implementation so that superfluous logic-specific checks for life, library size, etc. aren't needed
|
||||
AiPlayDecision aiPlayDecision = AiPlayDecision.CantPlaySa;
|
||||
if (useLivingEnd) {
|
||||
if (sa.isCycling() && sa.canCastTiming(player)
|
||||
&& player.getCardsIn(ZoneType.Library).size() >= 10) {
|
||||
if (sa.isCycling() && sa.canCastTiming(player) && player.getCardsIn(ZoneType.Library).size() >= 10) {
|
||||
if (ComputerUtilCost.canPayCost(sa, player, sa.isTrigger())) {
|
||||
if (sa.getPayCosts() != null && sa.getPayCosts().hasSpecificCostType(CostPayLife.class)
|
||||
&& !player.cantLoseForZeroOrLessLife() && player.getLife() <= sa.getPayCosts()
|
||||
.getCostPartByType(CostPayLife.class).getAbilityAmount(sa) * 2) {
|
||||
&& !player.cantLoseForZeroOrLessLife()
|
||||
&& player.getLife() <= sa.getPayCosts().getCostPartByType(CostPayLife.class).getAbilityAmount(sa) * 2) {
|
||||
aiPlayDecision = AiPlayDecision.CantAfford;
|
||||
} else {
|
||||
aiPlayDecision = AiPlayDecision.WillPlay;
|
||||
}
|
||||
}
|
||||
} else if (sa.getHostCard().hasKeyword(Keyword.CASCADE)) {
|
||||
if (isLifeInDanger) { // needs more tune up for certain conditions
|
||||
aiPlayDecision = player.getCreaturesInPlay().size() >= 4 ? AiPlayDecision.CantPlaySa
|
||||
: AiPlayDecision.WillPlay;
|
||||
} else if (CardLists
|
||||
.filter(player.getZone(ZoneType.Graveyard).getCards(), CardPredicates.CREATURES)
|
||||
.size() > 4) {
|
||||
if (isLifeInDanger) { //needs more tune up for certain conditions
|
||||
aiPlayDecision = player.getCreaturesInPlay().size() >= 4 ? AiPlayDecision.CantPlaySa : AiPlayDecision.WillPlay;
|
||||
} else if (CardLists.filter(player.getZone(ZoneType.Graveyard).getCards(), CardPredicates.CREATURES).size() > 4) {
|
||||
if (player.getCreaturesInPlay().size() >= 4) // it's good minimum
|
||||
continue;
|
||||
else if (!sa.getHostCard().isPermanent() && sa.canCastTiming(player)
|
||||
&& ComputerUtilCost.canPayCost(sa, player, sa.isTrigger()))
|
||||
aiPlayDecision = AiPlayDecision.WillPlay;
|
||||
// needs tuneup for bad matchups like reanimator and other things to check on opponent graveyard
|
||||
else if (!sa.getHostCard().isPermanent() && sa.canCastTiming(player) && ComputerUtilCost.canPayCost(sa, player, sa.isTrigger()))
|
||||
aiPlayDecision = AiPlayDecision.WillPlay;// needs tuneup for bad matchups like reanimator and other things to check on opponent graveyard
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
@@ -1791,9 +1684,11 @@ public class AiController {
|
||||
|
||||
// instead of computing all available concurrently just add a simple timeout depending on the user prefs
|
||||
try {
|
||||
if (game.AI_CAN_USE_TIMEOUT)
|
||||
return future.completeOnTimeout(null, game.getAITimeout(), TimeUnit.SECONDS).get();
|
||||
else
|
||||
return future.get(game.getAITimeout(), TimeUnit.SECONDS);
|
||||
} catch (InterruptedException | ExecutionException | TimeoutException e) {
|
||||
future.cancel(true);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1831,7 +1726,7 @@ public class AiController {
|
||||
if (spell instanceof WrappedAbility)
|
||||
return doTrigger(((WrappedAbility) spell).getWrappedAbility(), mandatory);
|
||||
if (spell.getApi() != null)
|
||||
return SpellApiToAi.Converter.get(spell).doTriggerAI(player, spell, mandatory);
|
||||
return SpellApiToAi.Converter.get(spell.getApi()).doTriggerAI(player, spell, mandatory);
|
||||
if (spell.getPayCosts() == Cost.Zero && !spell.usesTargeting()) {
|
||||
// For non-converted triggers (such as Cumulative Upkeep) that don't have costs or targets to worry about
|
||||
return true;
|
||||
@@ -2071,7 +1966,7 @@ public class AiController {
|
||||
}
|
||||
|
||||
// AI has decided to help. Now let's figure out how much they can help
|
||||
int mana = getAvailableManaEstimate(player, true);
|
||||
int mana = ComputerUtilMana.getAvailableManaEstimate(player, true);
|
||||
|
||||
// TODO We should make a logical guess here, but for now just uh yknow randomly decide?
|
||||
// What do I want to play next? Can I still pay for that and have mana left over to help?
|
||||
@@ -2369,7 +2264,7 @@ public class AiController {
|
||||
|
||||
// TODO move to more common place
|
||||
public static <T extends TriggerReplacementBase> List<T> filterList(List<T> input, Function<SpellAbility, Object> pred, Object value) {
|
||||
return filterList(input, trb -> trb.ensureAbility() != null && pred.apply(trb.ensureAbility()) == value);
|
||||
return filterList(input, trb -> pred.apply(trb.ensureAbility()) == value);
|
||||
}
|
||||
|
||||
public static List<SpellAbility> filterListByApi(List<SpellAbility> input, ApiType type) {
|
||||
|
||||
@@ -46,14 +46,6 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
return PaymentDecision.number(c);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PaymentDecision visit(CostBehold cost) {
|
||||
final String type = cost.getType();
|
||||
CardCollectionView hand = player.getCardsIn(cost.getRevealFrom());
|
||||
hand = CardLists.getValidCards(hand, type.split(";"), player, source, ability);
|
||||
return hand.isEmpty() ? null : PaymentDecision.card(getBestCreatureAI(hand));
|
||||
}
|
||||
|
||||
@Override
|
||||
public PaymentDecision visit(CostChooseColor cost) {
|
||||
int c = cost.getAbilityAmount(ability);
|
||||
|
||||
@@ -120,6 +120,7 @@ public class AiDeckStatistics {
|
||||
}
|
||||
|
||||
return fromDeck(deck, player);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -48,7 +48,6 @@ import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.SpellAbilityStackInstance;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.staticability.StaticAbility;
|
||||
import forge.game.staticability.StaticAbilityMode;
|
||||
import forge.game.trigger.Trigger;
|
||||
import forge.game.trigger.TriggerType;
|
||||
import forge.game.trigger.WrappedAbility;
|
||||
@@ -157,6 +156,7 @@ public class ComputerUtil {
|
||||
}
|
||||
|
||||
public static int counterSpellRestriction(final Player ai, final SpellAbility sa) {
|
||||
// Move this to AF?
|
||||
// Restriction Level is Based off a handful of factors
|
||||
|
||||
int restrict = 0;
|
||||
@@ -214,6 +214,7 @@ public class ComputerUtil {
|
||||
return restrict;
|
||||
}
|
||||
|
||||
// this is used for AI's counterspells
|
||||
public static final boolean playStack(SpellAbility sa, final Player ai, final Game game) {
|
||||
sa.setActivatingPlayer(ai);
|
||||
if (!ComputerUtilCost.canPayCost(sa, ai, false))
|
||||
@@ -248,6 +249,47 @@ public class ComputerUtil {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static final boolean playSpellAbilityWithoutPayingManaCost(final Player ai, final SpellAbility sa, final Game game) {
|
||||
SpellAbility newSA = sa.copyWithNoManaCost();
|
||||
newSA.setActivatingPlayer(ai);
|
||||
|
||||
if (!CostPayment.canPayAdditionalCosts(newSA.getPayCosts(), newSA, false) || !ComputerUtilMana.canPayManaCost(newSA, ai, 0, false)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
newSA = GameActionUtil.addExtraKeywordCost(newSA);
|
||||
|
||||
final Card source = newSA.getHostCard();
|
||||
|
||||
Zone fromZone = game.getZoneOf(source);
|
||||
int zonePosition = 0;
|
||||
if (fromZone != null) {
|
||||
zonePosition = fromZone.getCards().indexOf(source);
|
||||
}
|
||||
|
||||
if (newSA.isSpell() && !source.isCopiedSpell()) {
|
||||
newSA.setHostCard(game.getAction().moveToStack(source, newSA));
|
||||
|
||||
if (newSA.getApi() == ApiType.Charm && !CharmEffect.makeChoices(newSA)) {
|
||||
// 603.3c If no mode is chosen, the ability is removed from the stack.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
final CostPayment pay = new CostPayment(newSA.getPayCosts(), newSA);
|
||||
|
||||
// do this after card got added to stack
|
||||
if (!newSA.checkRestrictions(ai)) {
|
||||
GameActionUtil.rollbackAbility(newSA, fromZone, zonePosition, pay, source);
|
||||
return false;
|
||||
}
|
||||
|
||||
pay.payComputerCosts(new AiCostDecision(ai, newSA, false));
|
||||
|
||||
game.getStack().add(newSA);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static final boolean playNoStack(final Player ai, SpellAbility sa, final Game game, final boolean effect) {
|
||||
sa.setActivatingPlayer(ai);
|
||||
// TODO: We should really restrict what doesn't use the Stack
|
||||
@@ -752,7 +794,7 @@ public class ComputerUtil {
|
||||
tapList.clear();
|
||||
}
|
||||
tapList.add(next);
|
||||
totalPower = CardLists.getTotalPower(tapList, sa);
|
||||
totalPower = CardLists.getTotalPower(tapList, true, sa.isCrew());
|
||||
if (totalPower >= amount) {
|
||||
break;
|
||||
}
|
||||
@@ -864,7 +906,7 @@ public class ComputerUtil {
|
||||
|
||||
// Run non-mandatory trigger.
|
||||
// These checks only work if the Executing SpellAbility is an Ability_Sub.
|
||||
if ((exSA instanceof AbilitySub) && !SpellApiToAi.Converter.get(exSA).doTriggerAI(ai, exSA, false)) {
|
||||
if ((exSA instanceof AbilitySub) && !SpellApiToAi.Converter.get(exSA.getApi()).doTriggerAI(ai, exSA, false)) {
|
||||
// AI would not run this trigger if given the chance
|
||||
return sacrificed;
|
||||
}
|
||||
@@ -1100,11 +1142,6 @@ public class ComputerUtil {
|
||||
}
|
||||
}
|
||||
|
||||
// if AI has no speed, play start your engines on Main1
|
||||
if (ai.noSpeed() && cardState.hasKeyword(Keyword.START_YOUR_ENGINES)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// cast Blitz in main 1 if the creature attacks
|
||||
if (sa.isBlitz() && ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, card)) {
|
||||
return true;
|
||||
@@ -1414,7 +1451,9 @@ public class ComputerUtil {
|
||||
}
|
||||
}
|
||||
for (final CostPart part : abCost.getCostParts()) {
|
||||
if (part instanceof CostSacrifice sac) {
|
||||
if (part instanceof CostSacrifice) {
|
||||
final CostSacrifice sac = (CostSacrifice) part;
|
||||
|
||||
final String type = sac.getType();
|
||||
|
||||
if (type.equals("CARDNAME")) {
|
||||
@@ -1459,14 +1498,15 @@ public class ComputerUtil {
|
||||
// check for Continuous abilities that grant Haste
|
||||
for (final Card c : all) {
|
||||
for (StaticAbility stAb : c.getStaticAbilities()) {
|
||||
if (stAb.checkMode(StaticAbilityMode.Continuous) && stAb.hasParam("AddKeyword")
|
||||
&& stAb.getParam("AddKeyword").contains("Haste")) {
|
||||
Map<String, String> params = stAb.getMapParams();
|
||||
if ("Continuous".equals(params.get("Mode")) && params.containsKey("AddKeyword")
|
||||
&& params.get("AddKeyword").contains("Haste")) {
|
||||
|
||||
if (c.isEquipment() && c.getEquipping() == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final String affected = stAb.getParam("Affected");
|
||||
final String affected = params.get("Affected");
|
||||
if (affected.contains("Creature.YouCtrl")
|
||||
|| affected.contains("Other+YouCtrl")) {
|
||||
return true;
|
||||
@@ -1519,10 +1559,11 @@ public class ComputerUtil {
|
||||
|
||||
for (final Card c : opp) {
|
||||
for (StaticAbility stAb : c.getStaticAbilities()) {
|
||||
if (stAb.checkMode(StaticAbilityMode.Continuous) && stAb.hasParam("AddKeyword")
|
||||
&& stAb.getParam("AddKeyword").contains("Haste")) {
|
||||
Map<String, String> params = stAb.getMapParams();
|
||||
if ("Continuous".equals(params.get("Mode")) && params.containsKey("AddKeyword")
|
||||
&& params.get("AddKeyword").contains("Haste")) {
|
||||
|
||||
final ArrayList<String> affected = Lists.newArrayList(stAb.getParam("Affected").split(","));
|
||||
final ArrayList<String> affected = Lists.newArrayList(params.get("Affected").split(","));
|
||||
if (affected.contains("Creature")) {
|
||||
return true;
|
||||
}
|
||||
@@ -1778,7 +1819,9 @@ public class ComputerUtil {
|
||||
noRegen = true;
|
||||
}
|
||||
for (final Object o : objects) {
|
||||
if (o instanceof Card c) {
|
||||
if (o instanceof Card) {
|
||||
final Card c = (Card) o;
|
||||
|
||||
// indestructible
|
||||
if (c.hasKeyword(Keyword.INDESTRUCTIBLE)) {
|
||||
continue;
|
||||
@@ -1842,7 +1885,9 @@ public class ComputerUtil {
|
||||
if (ComputerUtilCombat.predictDamageTo(c, dmg, source, false) >= ComputerUtilCombat.getDamageToKill(c, false)) {
|
||||
threatened.add(c);
|
||||
}
|
||||
} else if (o instanceof Player p) {
|
||||
} else if (o instanceof Player) {
|
||||
final Player p = (Player) o;
|
||||
|
||||
if (source.hasKeyword(Keyword.INFECT)) {
|
||||
if (p.canReceiveCounters(CounterEnumType.POISON) && ComputerUtilCombat.predictDamageTo(p, dmg, source, false) >= 10 - p.getPoisonCounters()) {
|
||||
threatened.add(p);
|
||||
@@ -1860,7 +1905,8 @@ public class ComputerUtil {
|
||||
|| saviourApi == null)) {
|
||||
final int dmg = -AbilityUtils.calculateAmount(source, topStack.getParam("NumDef"), topStack);
|
||||
for (final Object o : objects) {
|
||||
if (o instanceof Card c) {
|
||||
if (o instanceof Card) {
|
||||
final Card c = (Card) o;
|
||||
final boolean canRemove = (c.getNetToughness() <= dmg)
|
||||
|| (!c.hasKeyword(Keyword.INDESTRUCTIBLE) && c.getShieldCount() == 0 && dmg >= ComputerUtilCombat.getDamageToKill(c, false));
|
||||
if (!canRemove) {
|
||||
@@ -1906,7 +1952,9 @@ public class ComputerUtil {
|
||||
|| saviourApi == ApiType.Protection || saviourApi == null
|
||||
|| saviorWithSubsApi == ApiType.Pump || saviorWithSubsApi == ApiType.PumpAll)) {
|
||||
for (final Object o : objects) {
|
||||
if (o instanceof Card c) {
|
||||
if (o instanceof Card) {
|
||||
final Card c = (Card) o;
|
||||
// indestructible
|
||||
if (c.hasKeyword(Keyword.INDESTRUCTIBLE)) {
|
||||
continue;
|
||||
}
|
||||
@@ -1955,7 +2003,8 @@ public class ComputerUtil {
|
||||
&& topStack.hasParam("Destination")
|
||||
&& topStack.getParam("Destination").equals("Exile")) {
|
||||
for (final Object o : objects) {
|
||||
if (o instanceof Card c) {
|
||||
if (o instanceof Card) {
|
||||
final Card c = (Card) o;
|
||||
// give Shroud to targeted creatures
|
||||
if ((saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll) && (!topStack.usesTargeting() || !grantShroud)) {
|
||||
continue;
|
||||
@@ -1982,7 +2031,8 @@ public class ComputerUtil {
|
||||
&& (saviourApi == ApiType.ChangeZone || saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll
|
||||
|| saviourApi == ApiType.Protection || saviourApi == null)) {
|
||||
for (final Object o : objects) {
|
||||
if (o instanceof Card c) {
|
||||
if (o instanceof Card) {
|
||||
final Card c = (Card) o;
|
||||
// give Shroud to targeted creatures
|
||||
if ((saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll) && (!topStack.usesTargeting() || !grantShroud)) {
|
||||
continue;
|
||||
@@ -2004,7 +2054,8 @@ public class ComputerUtil {
|
||||
boolean enableCurseAuraRemoval = aic != null ? aic.getBooleanProperty(AiProps.ACTIVELY_DESTROY_IMMEDIATELY_UNBLOCKABLE) : false;
|
||||
if (enableCurseAuraRemoval) {
|
||||
for (final Object o : objects) {
|
||||
if (o instanceof Card c) {
|
||||
if (o instanceof Card) {
|
||||
final Card c = (Card) o;
|
||||
// give Shroud to targeted creatures
|
||||
if ((saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll) && (!topStack.usesTargeting() || !grantShroud)) {
|
||||
continue;
|
||||
@@ -2428,7 +2479,7 @@ public class ComputerUtil {
|
||||
// Are we picking a type to reduce costs for that type?
|
||||
boolean reducingCost = false;
|
||||
for (StaticAbility s : sa.getHostCard().getStaticAbilities()) {
|
||||
if (s.checkMode(StaticAbilityMode.ReduceCost) && "Card.ChosenType".equals(s.getParam("ValidCard"))) {
|
||||
if ("ReduceCost".equals(s.getParam("Mode")) && "Card.ChosenType".equals(s.getParam("ValidCard"))) {
|
||||
reducingCost = true;
|
||||
break;
|
||||
}
|
||||
@@ -2788,13 +2839,17 @@ public class ComputerUtil {
|
||||
if (!trigger.requirementsCheck(game)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!trigger.matchesValidParam("ValidCard", card)) {
|
||||
if (trigger.hasParam("ValidCard")) {
|
||||
if (!card.isValid(trigger.getParam("ValidCard").split(","), source.getController(), source, sa)) {
|
||||
continue;
|
||||
}
|
||||
if (!trigger.matchesValidParam("ValidActivatingPlayer", player)) {
|
||||
}
|
||||
|
||||
if (trigger.hasParam("ValidActivatingPlayer")) {
|
||||
if (!player.isValid(trigger.getParam("ValidActivatingPlayer"), source.getController(), source, sa)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// fall back for OverridingAbility
|
||||
SpellAbility trigSa = trigger.ensureAbility();
|
||||
@@ -2851,9 +2906,11 @@ public class ComputerUtil {
|
||||
&& AbilityUtils.getDefinedCards(permanent, source.getSVar(trigger.getParam("CheckOnTriggeredCard").split(" ")[0]), null).isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
if (!trigger.matchesValidParam("ValidCard", permanent)) {
|
||||
if (trigger.hasParam("ValidCard")) {
|
||||
if (!permanent.isValid(trigger.getParam("ValidCard"), source.getController(), source, null)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// fall back for OverridingAbility
|
||||
SpellAbility trigSa = trigger.ensureAbility();
|
||||
if (trigSa == null) {
|
||||
@@ -2890,7 +2947,7 @@ public class ComputerUtil {
|
||||
// Iceberg does use Ice as Storage
|
||||
|| (type.is(CounterEnumType.ICE) && !"Iceberg".equals(c.getName()))
|
||||
// some lands does use Depletion as Storage Counter
|
||||
|| (type.is(CounterEnumType.DEPLETION) && c.getReplacementEffects().anyMatch(r -> r.getMode().equals(ReplacementType.Untap) && r.getLayer().equals(ReplacementLayer.CantHappen)))
|
||||
|| (type.is(CounterEnumType.DEPLETION) && c.hasKeyword("CARDNAME doesn't untap during your untap step."))
|
||||
// treat Time Counters on suspended Cards as Bad,
|
||||
// and also on Chronozoa
|
||||
|| (type.is(CounterEnumType.TIME) && (!c.isInPlay() || "Chronozoa".equals(c.getName())))
|
||||
|
||||
@@ -21,7 +21,6 @@ import forge.game.cost.CostRemoveCounter;
|
||||
import forge.game.keyword.Keyword;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.OptionalCost;
|
||||
import forge.game.spellability.OptionalCostValue;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.SpellAbilityStackInstance;
|
||||
@@ -123,10 +122,6 @@ public class ComputerUtilAbility {
|
||||
boolean choseOptCost = false;
|
||||
List<OptionalCostValue> list = GameActionUtil.getOptionalCostValues(sa);
|
||||
if (!list.isEmpty()) {
|
||||
// still add base spell in case of Promise Gift
|
||||
if (list.stream().anyMatch(ocv -> ocv.getType().equals(OptionalCost.PromiseGift))) {
|
||||
result.add(sa);
|
||||
}
|
||||
list = player.getController().chooseOptionalCosts(sa, list);
|
||||
if (!list.isEmpty()) {
|
||||
choseOptCost = true;
|
||||
|
||||
@@ -48,7 +48,6 @@ import forge.game.replacement.ReplacementEffect;
|
||||
import forge.game.replacement.ReplacementLayer;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.staticability.StaticAbility;
|
||||
import forge.game.staticability.StaticAbilityMode;
|
||||
import forge.game.trigger.Trigger;
|
||||
import forge.game.zone.MagicStack;
|
||||
import forge.game.zone.ZoneType;
|
||||
@@ -692,8 +691,6 @@ public class ComputerUtilCard {
|
||||
public static boolean canBeBlockedProfitably(final Player ai, Card attacker, boolean checkingOther) {
|
||||
AiBlockController aiBlk = new AiBlockController(ai, checkingOther);
|
||||
Combat combat = new Combat(ai);
|
||||
// avoid removing original attacker
|
||||
attacker.setCombatLKI(null);
|
||||
combat.addAttacker(attacker, ai);
|
||||
final List<Card> attackers = Lists.newArrayList(attacker);
|
||||
aiBlk.assignBlockersGivenAttackers(combat, attackers);
|
||||
@@ -1214,7 +1211,8 @@ public class ComputerUtilCard {
|
||||
// if this thing is both owned and controlled by an opponent and it has a continuous ability,
|
||||
// assume it either benefits the player or disrupts the opponent
|
||||
for (final StaticAbility stAb : c.getStaticAbilities()) {
|
||||
if (stAb.checkMode(StaticAbilityMode.Continuous) && stAb.isIntrinsic()) {
|
||||
final Map<String, String> params = stAb.getMapParams();
|
||||
if (params.get("Mode").equals("Continuous") && stAb.isIntrinsic()) {
|
||||
priority = true;
|
||||
break;
|
||||
}
|
||||
@@ -1245,16 +1243,17 @@ public class ComputerUtilCard {
|
||||
}
|
||||
} else {
|
||||
for (final StaticAbility stAb : c.getStaticAbilities()) {
|
||||
final Map<String, String> params = stAb.getMapParams();
|
||||
//continuous buffs
|
||||
if (stAb.checkMode(StaticAbilityMode.Continuous) && "Creature.YouCtrl".equals(stAb.getParam("Affected"))) {
|
||||
if (params.get("Mode").equals("Continuous") && "Creature.YouCtrl".equals(params.get("Affected"))) {
|
||||
int bonusPT = 0;
|
||||
if (stAb.hasParam("AddPower")) {
|
||||
bonusPT += AbilityUtils.calculateAmount(c, stAb.getParam("AddPower"), stAb);
|
||||
if (params.containsKey("AddPower")) {
|
||||
bonusPT += AbilityUtils.calculateAmount(c, params.get("AddPower"), stAb);
|
||||
}
|
||||
if (stAb.hasParam("AddToughness")) {
|
||||
bonusPT += AbilityUtils.calculateAmount(c, stAb.getParam("AddPower"), stAb);
|
||||
if (params.containsKey("AddToughness")) {
|
||||
bonusPT += AbilityUtils.calculateAmount(c, params.get("AddPower"), stAb);
|
||||
}
|
||||
String kws = stAb.getParam("AddKeyword");
|
||||
String kws = params.get("AddKeyword");
|
||||
if (kws != null) {
|
||||
bonusPT += 4 * (1 + StringUtils.countMatches(kws, "&")); //treat each added keyword as a +2/+2 for now
|
||||
}
|
||||
@@ -1785,7 +1784,7 @@ public class ComputerUtilCard {
|
||||
// remove old boost that might be copied
|
||||
for (final StaticAbility stAb : c.getStaticAbilities()) {
|
||||
vCard.removePTBoost(c.getLayerTimestamp(), stAb.getId());
|
||||
if (!stAb.checkMode(StaticAbilityMode.Continuous)) {
|
||||
if (!stAb.checkMode("Continuous")) {
|
||||
continue;
|
||||
}
|
||||
if (!stAb.hasParam("Affected")) {
|
||||
@@ -1863,7 +1862,7 @@ public class ComputerUtilCard {
|
||||
if (!c.isCreature()) {
|
||||
return false;
|
||||
}
|
||||
if (c.hasKeyword("CARDNAME can't attack or block.") || (c.isTapped() && !c.canUntap(ai, true)) || (c.getOwner() == ai && ai.getOpponents().contains(c.getController()))) {
|
||||
if (c.hasKeyword("CARDNAME can't attack or block.") || (c.hasKeyword("CARDNAME doesn't untap during your untap step.") && c.isTapped()) || (c.getOwner() == ai && ai.getOpponents().contains(c.getController()))) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -2080,7 +2079,6 @@ public class ComputerUtilCard {
|
||||
return false;
|
||||
}
|
||||
|
||||
// use this function to skip expensive calculations on identical cards
|
||||
public static CardCollection dedupeCards(CardCollection cc) {
|
||||
if (cc.size() <= 1) {
|
||||
return cc;
|
||||
|
||||
@@ -31,7 +31,7 @@ import forge.game.combat.Combat;
|
||||
import forge.game.combat.CombatUtil;
|
||||
import forge.game.cost.CostPayment;
|
||||
import forge.game.keyword.Keyword;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.phase.Untap;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.replacement.ReplacementEffect;
|
||||
import forge.game.replacement.ReplacementLayer;
|
||||
@@ -39,7 +39,6 @@ import forge.game.replacement.ReplacementType;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.staticability.StaticAbility;
|
||||
import forge.game.staticability.StaticAbilityAssignCombatDamageAsUnblocked;
|
||||
import forge.game.staticability.StaticAbilityMode;
|
||||
import forge.game.staticability.StaticAbilityMustAttack;
|
||||
import forge.game.trigger.Trigger;
|
||||
import forge.game.trigger.TriggerType;
|
||||
@@ -102,7 +101,7 @@ public class ComputerUtilCombat {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (attacker.getGame().getReplacementHandler().wouldPhaseBeSkipped(attacker.getController(), PhaseType.COMBAT_BEGIN)) {
|
||||
if (attacker.getGame().getReplacementHandler().wouldPhaseBeSkipped(attacker.getController(), "BeginCombat")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -119,7 +118,7 @@ public class ComputerUtilCombat {
|
||||
// || (attacker.hasKeyword(Keyword.FADING) && attacker.getCounters(CounterEnumType.FADE) == 0)
|
||||
// || attacker.hasSVar("EndOfTurnLeavePlay"));
|
||||
// The creature won't untap next turn
|
||||
return !attacker.isTapped() || (attacker.getCounters(CounterEnumType.STUN) == 0 && attacker.canUntap(attacker.getController(), true));
|
||||
return !attacker.isTapped() || (attacker.getCounters(CounterEnumType.STUN) == 0 && Untap.canUntap(attacker));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -177,7 +176,7 @@ public class ComputerUtilCombat {
|
||||
public static int damageIfUnblocked(final Card attacker, final GameEntity attacked, final Combat combat, boolean withoutAbilities) {
|
||||
int damage = attacker.getNetCombatDamage();
|
||||
int sum = 0;
|
||||
if (attacked instanceof Player player && !player.canLoseLife()) {
|
||||
if (attacked instanceof Player && !((Player) attacked).canLoseLife()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -215,7 +214,7 @@ public class ComputerUtilCombat {
|
||||
int damage = attacker.getNetCombatDamage();
|
||||
int poison = 0;
|
||||
damage += predictPowerBonusOfAttacker(attacker, null, null, false);
|
||||
if (attacker.isInfectDamage(attacked)) {
|
||||
if (attacker.hasKeyword(Keyword.INFECT)) {
|
||||
int pd = predictDamageTo(attacked, damage, attacker, true);
|
||||
// opponent can always order it so that he gets 0
|
||||
if (pd == 1 && attacker.getController().getOpponents().getCardsIn(ZoneType.Battlefield).anyMatch(CardPredicates.nameEquals("Vorinclex, Monstrous Raider"))) {
|
||||
@@ -358,7 +357,7 @@ public class ComputerUtilCombat {
|
||||
} else if (attacker.hasKeyword(Keyword.TRAMPLE)) {
|
||||
int trampleDamage = getAttack(attacker) - totalShieldDamage(attacker, blockers);
|
||||
if (trampleDamage > 0) {
|
||||
if (attacker.isInfectDamage(ai)) {
|
||||
if (attacker.hasKeyword(Keyword.INFECT)) {
|
||||
poison += trampleDamage;
|
||||
}
|
||||
poison += predictExtraPoisonWithDamage(attacker, ai, trampleDamage);
|
||||
@@ -458,11 +457,11 @@ public class ComputerUtilCombat {
|
||||
maxTreshold--;
|
||||
}
|
||||
|
||||
if (resultingPoison(ai, combat) > Math.max(7, ai.getPoisonCounters())) {
|
||||
if (!ai.cantLoseForZeroOrLessLife() && lifeThatWouldRemain(ai, combat) - payment < Math.min(threshold, ai.getLife())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !ai.cantLoseForZeroOrLessLife() && lifeThatWouldRemain(ai, combat) - payment < Math.min(threshold, ai.getLife());
|
||||
return resultingPoison(ai, combat) > Math.max(7, ai.getPoisonCounters());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -501,11 +500,11 @@ public class ComputerUtilCombat {
|
||||
}
|
||||
}
|
||||
|
||||
if (resultingPoison(ai, combat) >= ai.getGame().getRules().getPoisonCountersToLose()) {
|
||||
if (!ai.cantLoseForZeroOrLessLife() && lifeThatWouldRemain(ai, combat) - payment < 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !ai.cantLoseForZeroOrLessLife() && lifeThatWouldRemain(ai, combat) - payment < 1;
|
||||
return resultingPoison(ai, combat) >= ai.getGame().getRules().getPoisonCountersToLose();
|
||||
}
|
||||
|
||||
// This calculates the amount of damage a blockgang can deal to the attacker
|
||||
@@ -901,7 +900,7 @@ public class ComputerUtilCombat {
|
||||
final CardCollectionView cardList = CardCollection.combine(game.getCardsIn(ZoneType.Battlefield), game.getCardsIn(ZoneType.Command));
|
||||
for (final Card card : cardList) {
|
||||
for (final StaticAbility stAb : card.getStaticAbilities()) {
|
||||
if (!stAb.checkMode(StaticAbilityMode.Continuous)) {
|
||||
if (!stAb.checkMode("Continuous")) {
|
||||
continue;
|
||||
}
|
||||
if (!stAb.hasParam("Affected") || !stAb.getParam("Affected").contains("blocking")) {
|
||||
@@ -1197,7 +1196,7 @@ public class ComputerUtilCombat {
|
||||
final CardCollectionView cardList = CardCollection.combine(game.getCardsIn(ZoneType.Battlefield), game.getCardsIn(ZoneType.Command));
|
||||
for (final Card card : cardList) {
|
||||
for (final StaticAbility stAb : card.getStaticAbilities()) {
|
||||
if (!stAb.checkMode(StaticAbilityMode.Continuous)) {
|
||||
if (!stAb.checkMode("Continuous")) {
|
||||
continue;
|
||||
}
|
||||
if (!stAb.hasParam("Affected") || !stAb.getParam("Affected").contains("attacking")) {
|
||||
@@ -1388,7 +1387,7 @@ public class ComputerUtilCombat {
|
||||
final CardCollectionView cardList = game.getCardsIn(ZoneType.Battlefield);
|
||||
for (final Card card : cardList) {
|
||||
for (final StaticAbility stAb : card.getStaticAbilities()) {
|
||||
if (!stAb.checkMode(StaticAbilityMode.Continuous)) {
|
||||
if (!"Continuous".equals(stAb.getParam("Mode"))) {
|
||||
continue;
|
||||
}
|
||||
if (!stAb.hasParam("Affected")) {
|
||||
@@ -1735,7 +1734,6 @@ public class ComputerUtilCombat {
|
||||
final int attackerLife = getDamageToKill(attacker, false)
|
||||
+ predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities);
|
||||
|
||||
// AI should be less worried about Deathtouch
|
||||
if (blocker.hasDoubleStrike()) {
|
||||
if (defenderDamage > 0 && (hasKeyword(blocker, "Deathtouch", withoutAbilities, combat) || attacker.hasSVar("DestroyWhenDamaged"))) {
|
||||
return true;
|
||||
@@ -1965,7 +1963,6 @@ public class ComputerUtilCombat {
|
||||
final int attackerLife = getDamageToKill(attacker, false)
|
||||
+ predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities);
|
||||
|
||||
// AI should be less worried about deathtouch
|
||||
if (attacker.hasDoubleStrike()) {
|
||||
if (attackerDamage >= defenderLife) {
|
||||
return true;
|
||||
@@ -2542,20 +2539,20 @@ public class ComputerUtilCombat {
|
||||
if (combat != null) {
|
||||
GameEntity def = combat.getDefenderByAttacker(sa.getHostCard());
|
||||
// 1. If the card that spawned the attacker was sent at a card, attack the same. Consider improving.
|
||||
if (def instanceof Card card && Iterables.contains(defenders, def)) {
|
||||
if (card.isPlaneswalker()) {
|
||||
if (def instanceof Card && Iterables.contains(defenders, def)) {
|
||||
if (((Card) def).isPlaneswalker()) {
|
||||
return def;
|
||||
}
|
||||
if (card.isBattle()) {
|
||||
if (((Card) def).isBattle()) {
|
||||
return def;
|
||||
}
|
||||
}
|
||||
// 2. Otherwise, go through the list of options one by one, choose the first one that can't be blocked profitably.
|
||||
for (GameEntity p : defenders) {
|
||||
if (p instanceof Player p1 && !ComputerUtilCard.canBeBlockedProfitably(p1, attacker, true)) {
|
||||
if (p instanceof Player && !ComputerUtilCard.canBeBlockedProfitably((Player)p, attacker, true)) {
|
||||
return p;
|
||||
}
|
||||
if (p instanceof Card card && !ComputerUtilCard.canBeBlockedProfitably(card.getController(), attacker, true)) {
|
||||
if (p instanceof Card && !ComputerUtilCard.canBeBlockedProfitably(((Card)p).getController(), attacker, true)) {
|
||||
return p;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,8 @@ public class ComputerUtilCost {
|
||||
return true;
|
||||
}
|
||||
for (final CostPart part : cost.getCostParts()) {
|
||||
if (part instanceof CostPutCounter addCounter) {
|
||||
if (part instanceof CostPutCounter) {
|
||||
final CostPutCounter addCounter = (CostPutCounter) part;
|
||||
final CounterType type = addCounter.getCounter();
|
||||
|
||||
if (type.is(CounterEnumType.M1M1)) {
|
||||
@@ -76,7 +77,9 @@ public class ComputerUtilCost {
|
||||
}
|
||||
final AiCostDecision decision = new AiCostDecision(sa.getActivatingPlayer(), sa, false);
|
||||
for (final CostPart part : cost.getCostParts()) {
|
||||
if (part instanceof CostRemoveCounter remCounter) {
|
||||
if (part instanceof CostRemoveCounter) {
|
||||
final CostRemoveCounter remCounter = (CostRemoveCounter) part;
|
||||
|
||||
final CounterType type = remCounter.counter;
|
||||
if (!part.payCostFromSource()) {
|
||||
if (type.is(CounterEnumType.P1P1)) {
|
||||
@@ -103,7 +106,9 @@ public class ComputerUtilCost {
|
||||
&& !source.hasKeyword(Keyword.UNDYING)) {
|
||||
return false;
|
||||
}
|
||||
} else if (part instanceof CostRemoveAnyCounter remCounter) {
|
||||
} else if (part instanceof CostRemoveAnyCounter) {
|
||||
final CostRemoveAnyCounter remCounter = (CostRemoveAnyCounter) part;
|
||||
|
||||
PaymentDecision pay = decision.visit(remCounter);
|
||||
return pay != null;
|
||||
}
|
||||
@@ -128,7 +133,9 @@ public class ComputerUtilCost {
|
||||
CardCollection hand = new CardCollection(ai.getCardsIn(ZoneType.Hand));
|
||||
|
||||
for (final CostPart part : cost.getCostParts()) {
|
||||
if (part instanceof CostDiscard disc) {
|
||||
if (part instanceof CostDiscard) {
|
||||
final CostDiscard disc = (CostDiscard) part;
|
||||
|
||||
final String type = disc.getType();
|
||||
final CardCollection typeList;
|
||||
int num;
|
||||
@@ -180,7 +187,8 @@ public class ComputerUtilCost {
|
||||
return true;
|
||||
}
|
||||
for (final CostPart part : cost.getCostParts()) {
|
||||
if (part instanceof CostDamage pay) {
|
||||
if (part instanceof CostDamage) {
|
||||
final CostDamage pay = (CostDamage) part;
|
||||
int realDamage = ComputerUtilCombat.predictDamageTo(ai, pay.getAbilityAmount(sa), source, false);
|
||||
if (ai.getLife() - realDamage < remainingLife
|
||||
&& realDamage > 0 && !ai.cantLoseForZeroOrLessLife()
|
||||
@@ -212,7 +220,9 @@ public class ComputerUtilCost {
|
||||
return true;
|
||||
}
|
||||
for (final CostPart part : cost.getCostParts()) {
|
||||
if (part instanceof CostPayLife payLife) {
|
||||
if (part instanceof CostPayLife) {
|
||||
final CostPayLife payLife = (CostPayLife) part;
|
||||
|
||||
int amount = payLife.getAbilityAmount(sourceAbility);
|
||||
|
||||
// check if there's override for the remainingLife threshold
|
||||
@@ -286,7 +296,8 @@ public class ComputerUtilCost {
|
||||
return true;
|
||||
}
|
||||
for (final CostPart part : cost.getCostParts()) {
|
||||
if (part instanceof CostSacrifice sac) {
|
||||
if (part instanceof CostSacrifice) {
|
||||
final CostSacrifice sac = (CostSacrifice) part;
|
||||
final int amount = AbilityUtils.calculateAmount(source, sac.getAmount(), sourceAbility);
|
||||
|
||||
if (sac.payCostFromSource() && source.isCreature()) {
|
||||
@@ -335,11 +346,12 @@ public class ComputerUtilCost {
|
||||
return true;
|
||||
}
|
||||
for (final CostPart part : cost.getCostParts()) {
|
||||
if (part instanceof CostSacrifice sac) {
|
||||
if (part instanceof CostSacrifice) {
|
||||
if (suppressRecursiveSacCostCheck) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final CostSacrifice sac = (CostSacrifice) part;
|
||||
final int amount = AbilityUtils.calculateAmount(source, sac.getAmount(), sourceAbility);
|
||||
|
||||
String type = sac.getType();
|
||||
@@ -608,7 +620,7 @@ public class ComputerUtilCost {
|
||||
}
|
||||
|
||||
return ComputerUtilMana.canPayManaCost(cost, sa, player, extraManaNeeded, effect)
|
||||
&& CostPayment.canPayAdditionalCosts(cost, sa, effect, player);
|
||||
&& CostPayment.canPayAdditionalCosts(cost, sa, effect);
|
||||
}
|
||||
|
||||
public static Set<String> getAvailableManaColors(Player ai, Card additionalLand) {
|
||||
|
||||
@@ -158,7 +158,7 @@ public class ComputerUtilMana {
|
||||
}
|
||||
|
||||
// Mana abilities on the same card
|
||||
String shardMana = shard.toShortString();
|
||||
String shardMana = shard.toString().replaceAll("\\{", "").replaceAll("\\}", "");
|
||||
|
||||
boolean payWithAb1 = ability1.getManaPart().mana(ability1).contains(shardMana);
|
||||
boolean payWithAb2 = ability2.getManaPart().mana(ability2).contains(shardMana);
|
||||
@@ -642,8 +642,7 @@ public class ComputerUtilMana {
|
||||
List<SpellAbility> paymentList = Lists.newArrayList();
|
||||
final ManaPool manapool = ai.getManaPool();
|
||||
|
||||
// Apply color/type conversion matrix if necessary (already done via autopay)
|
||||
if (ai.getControllingPlayer() == null) {
|
||||
// Apply the color/type conversion matrix if necessary
|
||||
manapool.restoreColorReplacements();
|
||||
CardPlayOption mayPlay = sa.getMayPlayOption();
|
||||
if (!effect) {
|
||||
@@ -657,13 +656,10 @@ public class ComputerUtilMana {
|
||||
AbilityUtils.applyManaColorConversion(manapool, sa.getParam("ManaConversion"));
|
||||
}
|
||||
StaticAbilityManaConvert.manaConvert(manapool, ai, sa.getHostCard(), effect && !sa.isCastFromPlayEffect() ? null : sa);
|
||||
}
|
||||
|
||||
// not worth checking if it makes sense to not spend floating first
|
||||
if (manapool.payManaCostFromPool(cost, sa, test, manaSpentToPay)) {
|
||||
CostPayment.handleOfferings(sa, test, cost.isPaid());
|
||||
// paid all from floating mana
|
||||
return true;
|
||||
return true; // paid all from floating mana
|
||||
}
|
||||
|
||||
boolean purePhyrexian = cost.containsOnlyPhyrexianMana();
|
||||
@@ -1284,13 +1280,7 @@ public class ComputerUtilMana {
|
||||
card.setCastFrom(card.getZone() != null ? card.getZone() : null);
|
||||
}
|
||||
|
||||
Cost payCosts;
|
||||
if (test) {
|
||||
payCosts = CostAdjustment.adjust(cost, sa, effect);
|
||||
} else {
|
||||
// when not testing CostPayment already handled raise
|
||||
payCosts = cost;
|
||||
}
|
||||
Cost payCosts = CostAdjustment.adjust(cost, sa, effect);
|
||||
CostPartMana manapart = payCosts != null ? payCosts.getCostMana() : null;
|
||||
final ManaCost mana = payCosts != null ? ( manapart == null ? ManaCost.ZERO : manapart.getManaCostFor(sa) ) : ManaCost.NO_COST;
|
||||
|
||||
@@ -1330,9 +1320,7 @@ public class ComputerUtilMana {
|
||||
}
|
||||
}
|
||||
|
||||
if (!effect) {
|
||||
CostAdjustment.adjust(manaCost, sa, null, test);
|
||||
}
|
||||
|
||||
if ("NumTimes".equals(sa.getParam("Announce"))) { // e.g. the Adversary cycle
|
||||
ManaCost mkCost = sa.getPayCosts().getTotalMana();
|
||||
@@ -1503,7 +1491,7 @@ public class ComputerUtilMana {
|
||||
AbilitySub sub = m.getSubAbility();
|
||||
// We really shouldn't be hardcoding names here. ChkDrawback should just return true for them
|
||||
if (sub != null && !card.getName().equals("Pristine Talisman") && !card.getName().equals("Zhur-Taa Druid")) {
|
||||
if (!SpellApiToAi.Converter.get(sub).chkDrawbackWithSubs(ai, sub)) {
|
||||
if (!SpellApiToAi.Converter.get(sub.getApi()).chkDrawbackWithSubs(ai, sub)) {
|
||||
continue;
|
||||
}
|
||||
needsLimitedResources = true; // TODO: check for good drawbacks (gainLife)
|
||||
@@ -1583,7 +1571,7 @@ public class ComputerUtilMana {
|
||||
// don't use abilities with dangerous drawbacks
|
||||
AbilitySub sub = m.getSubAbility();
|
||||
if (sub != null) {
|
||||
if (!SpellApiToAi.Converter.get(sub).chkDrawbackWithSubs(ai, sub)) {
|
||||
if (!SpellApiToAi.Converter.get(sub.getApi()).chkDrawbackWithSubs(ai, sub)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,6 +160,12 @@ public class CreatureEvaluator implements Function<Card, Integer> {
|
||||
value += addValue(20, "protection");
|
||||
}
|
||||
|
||||
for (final SpellAbility sa : c.getSpellAbilities()) {
|
||||
if (sa.isAbility()) {
|
||||
value += addValue(evaluateSpellAbility(sa), "sa: " + sa);
|
||||
}
|
||||
}
|
||||
|
||||
// paired creatures are more valuable because they grant a bonus to the other creature
|
||||
if (c.isPaired()) {
|
||||
value += addValue(14, "paired");
|
||||
@@ -207,7 +213,11 @@ public class CreatureEvaluator implements Function<Card, Integer> {
|
||||
value += addValue(1, "untapped");
|
||||
}
|
||||
|
||||
if (!c.canUntap(c.getController(), true)) {
|
||||
if (!c.getManaAbilities().isEmpty()) {
|
||||
value += addValue(10, "manadork");
|
||||
}
|
||||
|
||||
if (c.hasKeyword("CARDNAME doesn't untap during your untap step.")) {
|
||||
if (c.isTapped()) {
|
||||
value = addValue(50 + (c.getCMC() * 5), "tapped-useless"); // reset everything - useless
|
||||
} else {
|
||||
@@ -216,17 +226,6 @@ public class CreatureEvaluator implements Function<Card, Integer> {
|
||||
} else {
|
||||
value -= subValue(10 * c.getCounters(CounterEnumType.STUN), "stunned");
|
||||
}
|
||||
|
||||
for (final SpellAbility sa : c.getSpellAbilities()) {
|
||||
if (sa.isAbility()) {
|
||||
value += addValue(evaluateSpellAbility(sa), "sa: " + sa);
|
||||
}
|
||||
}
|
||||
|
||||
if (!c.getManaAbilities().isEmpty()) {
|
||||
value += addValue(10, "manadork");
|
||||
}
|
||||
|
||||
// use scaling because the creature is only available halfway
|
||||
if (c.hasKeyword(Keyword.PHASING)) {
|
||||
value -= subValue(Math.max(20, value / 2), "phasing");
|
||||
|
||||
@@ -13,7 +13,6 @@ import forge.card.mana.ManaAtom;
|
||||
import forge.game.Game;
|
||||
import forge.game.GameEntity;
|
||||
import forge.game.ability.AbilityFactory;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.ability.effects.DetachedCardEffect;
|
||||
import forge.game.card.*;
|
||||
import forge.game.card.token.TokenInfo;
|
||||
@@ -62,7 +61,6 @@ public abstract class GameState {
|
||||
private int landsPlayed = 0;
|
||||
private int landsPlayedLastTurn = 0;
|
||||
private int numRingTemptedYou = 0;
|
||||
private int speed = 0;
|
||||
private String precast = null;
|
||||
private String putOnStack = null;
|
||||
private final Map<ZoneType, String> cardTexts = new EnumMap<>(ZoneType.class);
|
||||
@@ -139,7 +137,6 @@ public abstract class GameState {
|
||||
sb.append(TextUtil.concatNoSpace(prefix + "landsplayed=", String.valueOf(p.landsPlayed), "\n"));
|
||||
sb.append(TextUtil.concatNoSpace(prefix + "landsplayedlastturn=", String.valueOf(p.landsPlayedLastTurn), "\n"));
|
||||
sb.append(TextUtil.concatNoSpace(prefix + "numringtemptedyou=", String.valueOf(p.numRingTemptedYou), "\n"));
|
||||
sb.append(TextUtil.concatNoSpace(prefix + "speed=", String.valueOf(p.speed), "\n"));
|
||||
if (!p.counters.isEmpty()) {
|
||||
sb.append(TextUtil.concatNoSpace(prefix + "counters=", p.counters, "\n"));
|
||||
}
|
||||
@@ -170,7 +167,6 @@ public abstract class GameState {
|
||||
p.counters = countersToString(player.getCounters());
|
||||
p.manaPool = processManaPool(player.getManaPool());
|
||||
p.numRingTemptedYou = player.getNumRingTemptedYou();
|
||||
p.speed = player.getSpeed();
|
||||
playerStates.add(p);
|
||||
}
|
||||
|
||||
@@ -229,7 +225,7 @@ public abstract class GameState {
|
||||
if (card instanceof DetachedCardEffect) {
|
||||
continue;
|
||||
}
|
||||
int playerIndex = game.getPlayers().indexOf(card.getZone().getPlayer());
|
||||
int playerIndex = game.getPlayers().indexOf(card.getController());
|
||||
addCard(zone, playerStates.get(playerIndex).cardTexts, card);
|
||||
}
|
||||
}
|
||||
@@ -546,8 +542,6 @@ public abstract class GameState {
|
||||
getPlayerState(categoryName).landsPlayedLastTurn = Integer.parseInt(categoryValue);
|
||||
} else if (categoryName.endsWith("numringtemptedyou")) {
|
||||
getPlayerState(categoryName).numRingTemptedYou = Integer.parseInt(categoryValue);
|
||||
} else if (categoryName.endsWith("speed")) {
|
||||
getPlayerState(categoryName).speed = Integer.parseInt(categoryValue);
|
||||
} else if (categoryName.endsWith("play") || categoryName.endsWith("battlefield")) {
|
||||
getPlayerState(categoryName).cardTexts.put(ZoneType.Battlefield, categoryValue);
|
||||
} else if (categoryName.endsWith("hand")) {
|
||||
@@ -1152,7 +1146,6 @@ public abstract class GameState {
|
||||
p.setLandsPlayedThisTurn(state.landsPlayed);
|
||||
p.setLandsPlayedLastTurn(state.landsPlayedLastTurn);
|
||||
p.setNumRingTemptedYou(state.numRingTemptedYou);
|
||||
p.setSpeed(state.speed);
|
||||
|
||||
p.clearPaidForSA();
|
||||
|
||||
@@ -1215,7 +1208,6 @@ public abstract class GameState {
|
||||
p.setRingLevel(i);
|
||||
}
|
||||
}
|
||||
if (state.speed > 0) p.createSpeedEffect();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1306,10 +1298,10 @@ public abstract class GameState {
|
||||
} else if (info.startsWith("FaceDown")) {
|
||||
c.turnFaceDown(true);
|
||||
if (info.endsWith("Manifested")) {
|
||||
c.setManifested(new SpellAbility.EmptySa(ApiType.Manifest, c));
|
||||
c.setManifested(true);
|
||||
}
|
||||
if (info.endsWith("Cloaked")) {
|
||||
c.setCloaked(new SpellAbility.EmptySa(ApiType.Cloak, c));
|
||||
c.setCloaked(true);
|
||||
}
|
||||
} else if (info.startsWith("Transformed")) {
|
||||
c.setState(CardStateName.Transformed, true);
|
||||
@@ -1409,7 +1401,7 @@ public abstract class GameState {
|
||||
} else if (info.equals("Foretold")) {
|
||||
c.setForetold(true);
|
||||
c.turnFaceDown(true);
|
||||
c.addMayLookFaceDownExile(c.getOwner());
|
||||
c.addMayLookTemp(c.getOwner());
|
||||
} else if (info.equals("ForetoldThisTurn")) {
|
||||
c.setTurnInZone(turn);
|
||||
} else if (info.equals("IsToken")) {
|
||||
|
||||
@@ -15,7 +15,6 @@ import forge.game.*;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.ability.effects.CharmEffect;
|
||||
import forge.game.ability.effects.RollDiceEffect;
|
||||
import forge.game.card.*;
|
||||
import forge.game.combat.Combat;
|
||||
import forge.game.cost.Cost;
|
||||
@@ -47,6 +46,7 @@ import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.tuple.ImmutablePair;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
|
||||
import java.security.InvalidParameterException;
|
||||
import java.util.*;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
@@ -352,7 +352,11 @@ public class PlayerControllerAi extends PlayerController {
|
||||
if (delayedReveal != null) {
|
||||
reveal(delayedReveal.getCards(), delayedReveal.getZone(), delayedReveal.getOwner(), delayedReveal.getMessagePrefix());
|
||||
}
|
||||
return SpellApiToAi.Converter.get(sa).chooseSingleEntity(player, sa, (FCollection<T>)optionList, isOptional, targetedPlayer, params);
|
||||
ApiType api = sa.getApi();
|
||||
if (null == api) {
|
||||
throw new InvalidParameterException("SA is not api-based, this is not supported yet");
|
||||
}
|
||||
return SpellApiToAi.Converter.get(api).chooseSingleEntity(player, sa, (FCollection<T>)optionList, isOptional, targetedPlayer, params);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -394,7 +398,11 @@ public class PlayerControllerAi extends PlayerController {
|
||||
@Override
|
||||
public SpellAbility chooseSingleSpellForEffect(List<SpellAbility> spells, SpellAbility sa, String title,
|
||||
Map<String, Object> params) {
|
||||
return SpellApiToAi.Converter.get(sa).chooseSingleSpellAbility(player, sa, spells, params);
|
||||
ApiType api = sa.getApi();
|
||||
if (null == api) {
|
||||
throw new InvalidParameterException("SA is not api-based, this is not supported yet");
|
||||
}
|
||||
return SpellApiToAi.Converter.get(api).chooseSingleSpellAbility(player, sa, spells, params);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -746,30 +754,6 @@ public class PlayerControllerAi extends PlayerController {
|
||||
return Aggregates.random(rolls);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Integer> chooseDiceToReroll(List<Integer> rolls) {
|
||||
//TODO create AI logic for this
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer chooseRollToModify(List<Integer> rolls) {
|
||||
//TODO create AI logic for this
|
||||
return Aggregates.random(rolls);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RollDiceEffect.DieRollResult chooseRollToSwap(List<RollDiceEffect.DieRollResult> rolls) {
|
||||
//TODO create AI logic for this
|
||||
return Aggregates.random(rolls);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chooseRollSwapValue(List<String> swapChoices, Integer currentResult, int power, int toughness) {
|
||||
//TODO create AI logic for this
|
||||
return Aggregates.random(swapChoices);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mulliganKeepHand(Player firstPlayer, int cardsToReturn) {
|
||||
return !ComputerUtil.wantMulligan(player, cardsToReturn);
|
||||
@@ -892,7 +876,11 @@ public class PlayerControllerAi extends PlayerController {
|
||||
|
||||
@Override
|
||||
public int chooseNumber(SpellAbility sa, String string, int min, int max, Map<String, Object> params) {
|
||||
return SpellApiToAi.Converter.get(sa).chooseNumber(player, sa, min, max, params);
|
||||
ApiType api = sa.getApi();
|
||||
if (null == api) {
|
||||
throw new InvalidParameterException("SA is not api-based, this is not supported yet");
|
||||
}
|
||||
return SpellApiToAi.Converter.get(api).chooseNumber(player, sa, min, max, params);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -994,7 +982,11 @@ public class PlayerControllerAi extends PlayerController {
|
||||
*/
|
||||
@Override
|
||||
public boolean chooseBinary(SpellAbility sa, String question, BinaryChoiceType kindOfChoice, Map<String, Object> params) {
|
||||
return SpellApiToAi.Converter.get(sa).chooseBinary(kindOfChoice, sa, params);
|
||||
ApiType api = sa.getApi();
|
||||
if (null == api) {
|
||||
throw new InvalidParameterException("SA is not api-based, this is not supported yet");
|
||||
}
|
||||
return SpellApiToAi.Converter.get(api).chooseBinary(kindOfChoice, sa, params);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1064,7 +1056,11 @@ public class PlayerControllerAi extends PlayerController {
|
||||
if (options.size() <= 1) {
|
||||
return Iterables.getFirst(options, null);
|
||||
}
|
||||
return SpellApiToAi.Converter.get(sa).chooseCounterType(options, sa, params);
|
||||
ApiType api = sa.getApi();
|
||||
if (null == api) {
|
||||
throw new InvalidParameterException("SA is not api-based, this is not supported yet");
|
||||
}
|
||||
return SpellApiToAi.Converter.get(api).chooseCounterType(options, sa, params);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1221,7 +1217,7 @@ public class PlayerControllerAi extends PlayerController {
|
||||
|
||||
@Override
|
||||
public boolean payCostToPreventEffect(Cost cost, SpellAbility sa, boolean alreadyPaid, FCollectionView<Player> allPayers) {
|
||||
if (SpellApiToAi.Converter.get(sa).willPayUnlessCost(sa, player, cost, alreadyPaid, allPayers)) {
|
||||
if (SpellApiToAi.Converter.get(sa.getApi()).willPayUnlessCost(sa, player, cost, alreadyPaid, allPayers)) {
|
||||
if (!ComputerUtilCost.canPayCost(cost, sa, player, true)) {
|
||||
return false;
|
||||
}
|
||||
@@ -1232,11 +1228,6 @@ public class PlayerControllerAi extends PlayerController {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean payCostDuringRoll(final Cost cost, final SpellAbility sa, final FCollectionView<Player> allPayers) {
|
||||
// TODO logic for AI to pay rerolls and modification costs
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void orderAndPlaySimultaneousSa(List<SpellAbility> activePlayerSAs) {
|
||||
for (final SpellAbility sa : getAi().orderPlaySa(activePlayerSAs)) {
|
||||
@@ -1293,11 +1284,15 @@ public class PlayerControllerAi extends PlayerController {
|
||||
|
||||
@Override
|
||||
public boolean playSaFromPlayEffect(SpellAbility tgtSA) {
|
||||
boolean optional = !tgtSA.getPayCosts().isMandatory();
|
||||
boolean optional = tgtSA.hasParam("Optional");
|
||||
boolean noManaCost = tgtSA.hasParam("WithoutManaCost");
|
||||
if (tgtSA instanceof Spell spell) { // Isn't it ALWAYS a spell?
|
||||
if (tgtSA instanceof Spell) { // Isn't it ALWAYS a spell?
|
||||
Spell spell = (Spell) tgtSA;
|
||||
// TODO if mandatory AI is only forced to use mana when it's already in the pool
|
||||
if (brains.canPlayFromEffectAI(spell, !optional, noManaCost) == AiPlayDecision.WillPlay || !optional) {
|
||||
if (noManaCost) {
|
||||
return ComputerUtil.playSpellAbilityWithoutPayingManaCost(player, tgtSA, getGame());
|
||||
}
|
||||
return ComputerUtil.playStack(tgtSA, player, getGame());
|
||||
}
|
||||
return false; // didn't play spell
|
||||
@@ -1402,7 +1397,11 @@ public class PlayerControllerAi extends PlayerController {
|
||||
|
||||
@Override
|
||||
public String chooseCardName(SpellAbility sa, List<ICardFace> faces, String message) {
|
||||
return SpellApiToAi.Converter.get(sa).chooseCardName(player, sa, faces);
|
||||
ApiType api = sa.getApi();
|
||||
if (null == api) {
|
||||
throw new InvalidParameterException("SA is not api-based, this is not supported yet");
|
||||
}
|
||||
return SpellApiToAi.Converter.get(api).chooseCardName(player, sa, faces);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1419,11 +1418,11 @@ public class PlayerControllerAi extends PlayerController {
|
||||
oppLibrary = CardLists.getValidCards(oppLibrary, valid, source.getController(), source, sa);
|
||||
}
|
||||
|
||||
if (source != null && source.getState(CardStateName.Original).hasKeyword(Keyword.HIDDEN_AGENDA)) {
|
||||
if (source != null && source.getState(CardStateName.Original).hasIntrinsicKeyword("Hidden agenda")) {
|
||||
// If any Conspiracies are present, try not to choose the same name twice
|
||||
// (otherwise the AI will spam the same name)
|
||||
for (Card consp : player.getCardsIn(ZoneType.Command)) {
|
||||
if (consp.getState(CardStateName.Original).hasKeyword(Keyword.HIDDEN_AGENDA)) {
|
||||
if (consp.getState(CardStateName.Original).hasIntrinsicKeyword("Hidden agenda")) {
|
||||
String chosenName = consp.getNamedCard();
|
||||
if (!chosenName.isEmpty()) {
|
||||
aiLibrary = CardLists.filter(aiLibrary, CardPredicates.nameNotEquals(chosenName));
|
||||
@@ -1507,7 +1506,11 @@ public class PlayerControllerAi extends PlayerController {
|
||||
|
||||
@Override
|
||||
public ICardFace chooseSingleCardFace(SpellAbility sa, List<ICardFace> faces, String message) {
|
||||
return SpellApiToAi.Converter.get(sa).chooseCardFace(player, sa, faces);
|
||||
ApiType api = sa.getApi();
|
||||
if (null == api) {
|
||||
throw new InvalidParameterException("SA is not api-based, this is not supported yet");
|
||||
}
|
||||
return SpellApiToAi.Converter.get(api).chooseCardFace(player, sa, faces);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1517,7 +1520,11 @@ public class PlayerControllerAi extends PlayerController {
|
||||
|
||||
@Override
|
||||
public CardState chooseSingleCardState(SpellAbility sa, List<CardState> states, String message, Map<String, Object> params) {
|
||||
return SpellApiToAi.Converter.get(sa).chooseCardState(player, sa, states, params);
|
||||
ApiType api = sa.getApi();
|
||||
if (null == api) {
|
||||
throw new InvalidParameterException("SA is not api-based, this is not supported yet");
|
||||
}
|
||||
return SpellApiToAi.Converter.get(api).chooseCardState(player, sa, states, params);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1569,7 +1576,32 @@ public class PlayerControllerAi extends PlayerController {
|
||||
|
||||
@Override
|
||||
public List<OptionalCostValue> chooseOptionalCosts(SpellAbility chosen, List<OptionalCostValue> optionalCostValues) {
|
||||
return SpellApiToAi.Converter.get(chosen).chooseOptionalCosts(chosen, player, optionalCostValues);
|
||||
List<OptionalCostValue> chosenOptCosts = Lists.newArrayList();
|
||||
Cost costSoFar = chosen.getPayCosts().copy();
|
||||
|
||||
for (OptionalCostValue opt : optionalCostValues) {
|
||||
// Choose the optional cost if it can be paid (to be improved later, check for playability and other conditions perhaps)
|
||||
Cost fullCost = opt.getCost().copy().add(costSoFar);
|
||||
SpellAbility fullCostSa = chosen.copyWithDefinedCost(fullCost);
|
||||
|
||||
// Playability check for Kicker
|
||||
if (opt.getType() == OptionalCost.Kicker1 || opt.getType() == OptionalCost.Kicker2) {
|
||||
SpellAbility kickedSaCopy = fullCostSa.copy();
|
||||
kickedSaCopy.addOptionalCost(opt.getType());
|
||||
Card copy = CardCopyService.getLKICopy(chosen.getHostCard());
|
||||
copy.setCastSA(kickedSaCopy);
|
||||
if (ComputerUtilCard.checkNeedsToPlayReqs(copy, kickedSaCopy) != AiPlayDecision.WillPlay) {
|
||||
continue; // don't choose kickers we don't want to play
|
||||
}
|
||||
}
|
||||
|
||||
if (ComputerUtilCost.canPayCost(fullCostSa, player, false)) {
|
||||
chosenOptCosts.add(opt);
|
||||
costSoFar.add(opt.getCost());
|
||||
}
|
||||
}
|
||||
|
||||
return chosenOptCosts;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1629,4 +1661,5 @@ public class PlayerControllerAi extends PlayerController {
|
||||
|
||||
return choices;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -359,28 +359,18 @@ public class SpecialCardAi {
|
||||
private static final int demonSacThreshold = Integer.MAX_VALUE; // if we're in dire conditions, sac everything from worst to best hoping to find an answer
|
||||
|
||||
public static boolean considerSacrificingCreature(final Player ai, final SpellAbility sa) {
|
||||
Card c = sa.getHostCard();
|
||||
|
||||
// Only check for sacrifice if it's the owner's turn, and it can attack.
|
||||
// TODO: Maybe check if sacrificing a creature allows AI to kill the opponent with the rest on their turn?
|
||||
if (!CombatUtil.canAttack(c) ||
|
||||
!ai.getGame().getPhaseHandler().isPlayerTurn(sa.getActivatingPlayer())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
CardCollection flyingCreatures = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield),
|
||||
CardPredicates.UNTAPPED.and(
|
||||
CardPredicates.hasKeyword(Keyword.FLYING).or(CardPredicates.hasKeyword(Keyword.REACH))));
|
||||
boolean hasUsefulBlocker = false;
|
||||
|
||||
for (Card fc : flyingCreatures) {
|
||||
if (!ComputerUtilCard.isUselessCreature(ai, fc)) {
|
||||
for (Card c : flyingCreatures) {
|
||||
if (!ComputerUtilCard.isUselessCreature(ai, c)) {
|
||||
hasUsefulBlocker = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return ai.getLife() <= c.getNetPower() && !hasUsefulBlocker;
|
||||
return ai.getLife() <= sa.getHostCard().getNetPower() && !hasUsefulBlocker;
|
||||
}
|
||||
|
||||
public static int getSacThreshold() {
|
||||
@@ -1479,7 +1469,6 @@ public class SpecialCardAi {
|
||||
if (best != null) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(best);
|
||||
sa.setXManaCostPaid(best.getCMC());
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import forge.card.mana.ManaCost;
|
||||
import forge.card.mana.ManaCostParser;
|
||||
import forge.game.GameEntity;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCopyService;
|
||||
import forge.game.card.CardState;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.cost.Cost;
|
||||
@@ -24,8 +23,6 @@ import forge.game.player.Player;
|
||||
import forge.game.player.PlayerActionConfirmMode;
|
||||
import forge.game.player.PlayerController.BinaryChoiceType;
|
||||
import forge.game.spellability.AbilitySub;
|
||||
import forge.game.spellability.OptionalCost;
|
||||
import forge.game.spellability.OptionalCostValue;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.SpellAbilityCondition;
|
||||
import forge.game.zone.ZoneType;
|
||||
@@ -86,9 +83,11 @@ public abstract class SpellAbilityAi {
|
||||
if (!alwaysOnDiscard && !checkPhaseRestrictions(ai, sa, ai.getGame().getPhaseHandler(), logic)) {
|
||||
return false;
|
||||
}
|
||||
} else if (!checkPhaseRestrictions(ai, sa, ai.getGame().getPhaseHandler())) {
|
||||
} else {
|
||||
if (!checkPhaseRestrictions(ai, sa, ai.getGame().getPhaseHandler())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!checkApiLogic(ai, sa)) {
|
||||
return false;
|
||||
@@ -258,7 +257,7 @@ public abstract class SpellAbilityAi {
|
||||
protected static boolean isSorcerySpeed(final SpellAbility sa, Player ai) {
|
||||
return (sa.getRootAbility().isSpell() && sa.getHostCard().isSorcery())
|
||||
|| (sa.getRootAbility().isActivatedAbility() && sa.getRootAbility().getRestrictions().isSorcerySpeed())
|
||||
|| (sa.getRootAbility().isAdventure() && sa.getHostCard().getState(CardStateName.Secondary).getType().isSorcery())
|
||||
|| (sa.getRootAbility().isAdventure() && sa.getHostCard().getState(CardStateName.Adventure).getType().isSorcery())
|
||||
|| (sa.isPwAbility() && !sa.withFlash(sa.getHostCard(), ai));
|
||||
}
|
||||
|
||||
@@ -306,7 +305,7 @@ public abstract class SpellAbilityAi {
|
||||
*/
|
||||
public boolean chkDrawbackWithSubs(Player aiPlayer, AbilitySub ab) {
|
||||
final AbilitySub subAb = ab.getSubAbility();
|
||||
return SpellApiToAi.Converter.get(ab).chkAIDrawback(ab, aiPlayer) && (subAb == null || chkDrawbackWithSubs(aiPlayer, subAb));
|
||||
return SpellApiToAi.Converter.get(ab.getApi()).chkAIDrawback(ab, aiPlayer) && (subAb == null || chkDrawbackWithSubs(aiPlayer, subAb));
|
||||
}
|
||||
|
||||
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
|
||||
@@ -342,9 +341,9 @@ public abstract class SpellAbilityAi {
|
||||
for (T ent : options) {
|
||||
if (ent instanceof Player) {
|
||||
hasPlayer = true;
|
||||
} else if (ent instanceof Card card) {
|
||||
} else if (ent instanceof Card) {
|
||||
hasCard = true;
|
||||
if (card.isPlaneswalker() || card.isBattle()) {
|
||||
if (((Card)ent).isPlaneswalker() || ((Card)ent).isBattle()) {
|
||||
hasAttackableCard = true;
|
||||
}
|
||||
}
|
||||
@@ -411,33 +410,4 @@ public abstract class SpellAbilityAi {
|
||||
public boolean chooseBinary(BinaryChoiceType kindOfChoice, SpellAbility sa, Map<String, Object> params) {
|
||||
return MyRandom.getRandom().nextBoolean();
|
||||
}
|
||||
|
||||
public List<OptionalCostValue> chooseOptionalCosts(SpellAbility chosen, Player player, List<OptionalCostValue> optionalCostValues) {
|
||||
List<OptionalCostValue> chosenOptCosts = Lists.newArrayList();
|
||||
Cost costSoFar = chosen.getPayCosts().copy();
|
||||
|
||||
for (OptionalCostValue opt : optionalCostValues) {
|
||||
// Choose the optional cost if it can be paid (to be improved later, check for playability and other conditions perhaps)
|
||||
Cost fullCost = opt.getCost().copy().add(costSoFar);
|
||||
SpellAbility fullCostSa = chosen.copyWithDefinedCost(fullCost);
|
||||
|
||||
// Playability check for Kicker
|
||||
if (opt.getType() == OptionalCost.Kicker1 || opt.getType() == OptionalCost.Kicker2) {
|
||||
SpellAbility kickedSaCopy = fullCostSa.copy();
|
||||
kickedSaCopy.addOptionalCost(opt.getType());
|
||||
Card copy = CardCopyService.getLKICopy(chosen.getHostCard());
|
||||
copy.setCastSA(kickedSaCopy);
|
||||
if (ComputerUtilCard.checkNeedsToPlayReqs(copy, kickedSaCopy) != AiPlayDecision.WillPlay) {
|
||||
continue; // don't choose kickers we don't want to play
|
||||
}
|
||||
}
|
||||
|
||||
if (ComputerUtilCost.canPayCost(fullCostSa, player, false)) {
|
||||
chosenOptCosts.add(opt);
|
||||
costSoFar.add(opt.getCost());
|
||||
}
|
||||
}
|
||||
|
||||
return chosenOptCosts;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,8 @@ import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.Maps;
|
||||
import forge.ai.ability.*;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.util.ReflectionUtil;
|
||||
|
||||
import java.security.InvalidParameterException;
|
||||
import java.util.Map;
|
||||
|
||||
public enum SpellApiToAi {
|
||||
@@ -41,7 +39,6 @@ public enum SpellApiToAi {
|
||||
.put(ApiType.Branch, BranchAi.class)
|
||||
.put(ApiType.Camouflage, ChooseCardAi.class)
|
||||
.put(ApiType.ChangeCombatants, ChangeCombatantsAi.class)
|
||||
.put(ApiType.ChangeSpeed, AlwaysPlayAi.class)
|
||||
.put(ApiType.ChangeTargets, ChangeTargetsAi.class)
|
||||
.put(ApiType.ChangeX, AlwaysPlayAi.class)
|
||||
.put(ApiType.ChangeZone, ChangeZoneAi.class)
|
||||
@@ -88,7 +85,6 @@ public enum SpellApiToAi {
|
||||
.put(ApiType.EachDamage, DamageEachAi.class)
|
||||
.put(ApiType.Effect, EffectAi.class)
|
||||
.put(ApiType.Encode, EncodeAi.class)
|
||||
.put(ApiType.Endure, EndureAi.class)
|
||||
.put(ApiType.EndCombatPhase, EndTurnAi.class)
|
||||
.put(ApiType.EndTurn, EndTurnAi.class)
|
||||
.put(ApiType.ExchangeLife, LifeExchangeAi.class)
|
||||
@@ -211,14 +207,6 @@ public enum SpellApiToAi {
|
||||
.put(ApiType.InternalRadiation, AlwaysPlayAi.class)
|
||||
.build());
|
||||
|
||||
public SpellAbilityAi get(final SpellAbility sa) {
|
||||
ApiType api = sa.getApi();
|
||||
if (null == api) {
|
||||
throw new InvalidParameterException("SA is not api-based, this is not supported yet");
|
||||
}
|
||||
return get(api);
|
||||
}
|
||||
|
||||
public SpellAbilityAi get(final ApiType api) {
|
||||
SpellAbilityAi result = apiToInstance.get(api);
|
||||
if (null == result) {
|
||||
|
||||
@@ -24,7 +24,6 @@ import forge.game.spellability.SpellAbility;
|
||||
import forge.game.staticability.StaticAbility;
|
||||
import forge.game.staticability.StaticAbilityContinuous;
|
||||
import forge.game.staticability.StaticAbilityLayer;
|
||||
import forge.game.staticability.StaticAbilityMode;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.FileSection;
|
||||
import forge.util.collect.FCollectionView;
|
||||
@@ -563,7 +562,7 @@ public class AnimateAi extends SpellAbilityAi {
|
||||
CardTraitChanges traits = card.getChangedCardTraits().get(timestamp, 0);
|
||||
if (traits != null) {
|
||||
for (StaticAbility stAb : traits.getStaticAbilities()) {
|
||||
if (stAb.checkMode(StaticAbilityMode.Continuous)) {
|
||||
if ("Continuous".equals(stAb.getParam("Mode"))) {
|
||||
for (final StaticAbilityLayer layer : stAb.getLayers()) {
|
||||
StaticAbilityContinuous.applyContinuousAbility(stAb, new CardCollection(card), layer);
|
||||
}
|
||||
|
||||
@@ -15,23 +15,20 @@ import forge.game.cost.Cost;
|
||||
import forge.game.cost.CostPart;
|
||||
import forge.game.cost.CostSacrifice;
|
||||
import forge.game.keyword.Keyword;
|
||||
import forge.game.keyword.KeywordInterface;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerActionConfirmMode;
|
||||
import forge.game.replacement.ReplacementLayer;
|
||||
import forge.game.replacement.ReplacementType;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.staticability.StaticAbility;
|
||||
import forge.game.staticability.StaticAbilityCantAttackBlock;
|
||||
import forge.game.staticability.StaticAbilityMode;
|
||||
import forge.game.trigger.Trigger;
|
||||
import forge.game.trigger.TriggerType;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.Aggregates;
|
||||
import forge.util.MyRandom;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
@@ -133,7 +130,7 @@ public class AttachAi extends SpellAbilityAi {
|
||||
int power = 0, toughness = 0;
|
||||
List<String> keywords = Lists.newArrayList();
|
||||
for (StaticAbility stAb : source.getStaticAbilities()) {
|
||||
if (stAb.checkMode(StaticAbilityMode.Continuous)) {
|
||||
if ("Continuous".equals(stAb.getParam("Mode"))) {
|
||||
if (stAb.hasParam("AddPower")) {
|
||||
power += AbilityUtils.calculateAmount(source, stAb.getParam("AddPower"), stAb);
|
||||
}
|
||||
@@ -310,8 +307,9 @@ public class AttachAi extends SpellAbilityAi {
|
||||
String type = "";
|
||||
|
||||
for (final StaticAbility stAb : attachSource.getStaticAbilities()) {
|
||||
if (stAb.checkMode(StaticAbilityMode.Continuous) && stAb.hasParam("AddType")) {
|
||||
type = stAb.getParam("AddType");
|
||||
final Map<String, String> stab = stAb.getMapParams();
|
||||
if (stab.get("Mode").equals("Continuous") && stab.containsKey("AddType")) {
|
||||
type = stab.get("AddType");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -373,39 +371,9 @@ public class AttachAi extends SpellAbilityAi {
|
||||
*/
|
||||
private static Card attachAIKeepTappedPreference(final SpellAbility sa, final List<Card> list, final boolean mandatory, final Card attachSource) {
|
||||
// AI For Cards like Paralyzing Grasp and Glimmerdust Nap
|
||||
|
||||
// check for ETB Trigger
|
||||
boolean tapETB = isAuraSpell(sa) && attachSource.getTriggers().anyMatch(t -> {
|
||||
if (t.getMode() != TriggerType.ChangesZone) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ZoneType.Battlefield.toString().equals(t.getParam("Destination"))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (t.hasParam("ValidCard") && !t.getParam("ValidCard").contains("Self")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SpellAbility tSa = t.ensureAbility();
|
||||
if (tSa == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ApiType.Tap.equals(tSa.getApi())) {
|
||||
return false;
|
||||
}
|
||||
if (!"Enchanted".equals(tSa.getParam("Defined"))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
final List<Card> prefList = CardLists.filter(list, c -> {
|
||||
// Don't do Untapped Vigilance cards
|
||||
if (!tapETB && c.isCreature() && c.hasKeyword(Keyword.VIGILANCE) && c.isUntapped()) {
|
||||
if (c.isCreature() && c.hasKeyword(Keyword.VIGILANCE) && c.isUntapped()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -420,10 +388,21 @@ public class AttachAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// already affected
|
||||
if (!c.canUntap(c.getController(), true)) {
|
||||
|
||||
if (!c.isEnchanted()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final Iterable<Card> auras = c.getEnchantedBy();
|
||||
for (Card aura : auras) {
|
||||
SpellAbility auraSA = aura.getSpells().get(0);
|
||||
if (auraSA.getApi() == ApiType.Attach) {
|
||||
if ("KeepTapped".equals(auraSA.getParam("AILogic"))) {
|
||||
// Don't attach multiple KeepTapped Auras to one card
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
@@ -570,46 +549,28 @@ public class AttachAi extends SpellAbilityAi {
|
||||
final Card attachSource) {
|
||||
// AI For choosing a Card to Animate.
|
||||
final Player ai = sa.getActivatingPlayer();
|
||||
Card attachSourceLki = null;
|
||||
for (Trigger t : attachSource.getTriggers()) {
|
||||
if (!t.getMode().equals(TriggerType.ChangesZone)) {
|
||||
continue;
|
||||
}
|
||||
if (!"Battlefield".equals(t.getParam("Destination"))) {
|
||||
continue;
|
||||
}
|
||||
if (!"Card.Self".equals(t.getParam("ValidCard"))) {
|
||||
continue;
|
||||
}
|
||||
SpellAbility trigSa = t.ensureAbility();
|
||||
SpellAbility animateSa = trigSa.findSubAbilityByType(ApiType.Animate);
|
||||
if (animateSa == null) {
|
||||
continue;
|
||||
}
|
||||
animateSa.setActivatingPlayer(sa.getActivatingPlayer());
|
||||
attachSourceLki = AnimateAi.becomeAnimated(attachSource, animateSa);
|
||||
}
|
||||
if (attachSourceLki == null) {
|
||||
return null;
|
||||
}
|
||||
final Card attachSourceLki = CardCopyService.getLKICopy(attachSource);
|
||||
attachSourceLki.setLastKnownZone(ai.getZone(ZoneType.Battlefield));
|
||||
final Card finalAttachSourceLki = attachSourceLki;
|
||||
// Suppress original attach Spell to replace it with another
|
||||
attachSourceLki.getFirstAttachSpell().setSuppressed(true);
|
||||
|
||||
//TODO for Reanimate Auras i need the new Attach Spell, in later versions it might be part of the Enchant Keyword
|
||||
attachSourceLki.addSpellAbility(AbilityFactory.getAbility(attachSourceLki, "NewAttach"));
|
||||
List<Card> betterList = CardLists.filter(list, c -> {
|
||||
final Card lki = CardCopyService.getLKICopy(c);
|
||||
// need to fake it as if lki would be on the battlefield
|
||||
lki.setLastKnownZone(ai.getZone(ZoneType.Battlefield));
|
||||
|
||||
// Reanimate Auras use "Enchant creature put onto the battlefield with CARDNAME" with Remembered
|
||||
finalAttachSourceLki.clearRemembered();
|
||||
finalAttachSourceLki.addRemembered(lki);
|
||||
attachSourceLki.clearRemembered();
|
||||
attachSourceLki.addRemembered(lki);
|
||||
|
||||
// need to check what the cards would be on the battlefield
|
||||
// do not attach yet, that would cause Events
|
||||
CardCollection preList = new CardCollection(lki);
|
||||
preList.add(finalAttachSourceLki);
|
||||
preList.add(attachSourceLki);
|
||||
c.getGame().getAction().checkStaticAbilities(false, Sets.newHashSet(preList), preList);
|
||||
boolean result = lki.canBeAttached(finalAttachSourceLki, null);
|
||||
boolean result = lki.canBeAttached(attachSourceLki, null);
|
||||
|
||||
//reset static abilities
|
||||
c.getGame().getAction().checkStaticAbilities(false);
|
||||
@@ -834,45 +795,27 @@ public class AttachAi extends SpellAbilityAi {
|
||||
int totPower = 0;
|
||||
final List<String> keywords = new ArrayList<>();
|
||||
|
||||
boolean cantAttack = false;
|
||||
boolean cantBlock = false;
|
||||
|
||||
for (final StaticAbility stAbility : attachSource.getStaticAbilities()) {
|
||||
if (stAbility.checkMode(StaticAbilityMode.CantAttack)) {
|
||||
String valid = stAbility.getParam("ValidCard");
|
||||
if (valid.contains(stCheck) || valid.contains("AttachedBy")) {
|
||||
cantAttack = true;
|
||||
}
|
||||
} else if (stAbility.checkMode(StaticAbilityMode.CantBlock)) {
|
||||
String valid = stAbility.getParam("ValidCard");
|
||||
if (valid.contains(stCheck) || valid.contains("AttachedBy")) {
|
||||
cantBlock = true;
|
||||
}
|
||||
} else if (stAbility.checkMode(StaticAbilityMode.CantBlockBy)) {
|
||||
String valid = stAbility.getParam("ValidBlocker");
|
||||
if (valid.contains(stCheck) || valid.contains("AttachedBy")) {
|
||||
cantBlock = true;
|
||||
}
|
||||
}
|
||||
final Map<String, String> stabMap = stAbility.getMapParams();
|
||||
|
||||
if (!stAbility.checkMode(StaticAbilityMode.Continuous)) {
|
||||
if (!stabMap.get("Mode").equals("Continuous")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final String affected = stAbility.getParam("Affected");
|
||||
final String affected = stabMap.get("Affected");
|
||||
|
||||
if (affected == null) {
|
||||
continue;
|
||||
}
|
||||
if ((affected.contains(stCheck) || affected.contains("AttachedBy"))) {
|
||||
totToughness += AbilityUtils.calculateAmount(attachSource, stAbility.getParam("AddToughness"), sa);
|
||||
totPower += AbilityUtils.calculateAmount(attachSource, stAbility.getParam("AddPower"), sa);
|
||||
totToughness += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddToughness"), sa);
|
||||
totPower += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddPower"), sa);
|
||||
|
||||
String kws = stAbility.getParam("AddKeyword");
|
||||
String kws = stabMap.get("AddKeyword");
|
||||
if (kws != null) {
|
||||
keywords.addAll(Arrays.asList(kws.split(" & ")));
|
||||
}
|
||||
kws = stAbility.getParam("AddHiddenKeyword");
|
||||
kws = stabMap.get("AddHiddenKeyword");
|
||||
if (kws != null) {
|
||||
keywords.addAll(Arrays.asList(kws.split(" & ")));
|
||||
}
|
||||
@@ -908,12 +851,6 @@ public class AttachAi extends SpellAbilityAi {
|
||||
prefList = CardLists.filter(prefList, c -> c.getNetPower() > 0 && ComputerUtilCombat.canAttackNextTurn(c));
|
||||
}
|
||||
|
||||
if (cantAttack) {
|
||||
prefList = CardLists.filter(prefList, c -> c.isCreature() && ComputerUtilCombat.canAttackNextTurn(c));
|
||||
} else if (cantBlock) { // TODO better can block filter?
|
||||
prefList = CardLists.filter(prefList, c -> c.isCreature() && !ComputerUtilCard.isUselessCreature(ai, c));
|
||||
}
|
||||
|
||||
//some auras aren't useful in multiples
|
||||
if (attachSource.hasSVar("NonStackingAttachEffect")) {
|
||||
prefList = CardLists.filter(prefList,
|
||||
@@ -988,10 +925,6 @@ public class AttachAi extends SpellAbilityAi {
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean isAuraSpell(final SpellAbility sa) {
|
||||
return sa.isSpell() && sa.getHostCard().isAura();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach preference.
|
||||
*
|
||||
@@ -1007,23 +940,7 @@ public class AttachAi extends SpellAbilityAi {
|
||||
*/
|
||||
private static boolean attachPreference(final SpellAbility sa, final TargetRestrictions tgt, final boolean mandatory) {
|
||||
GameObject o;
|
||||
boolean spellCanTargetPlayer = false;
|
||||
if (isAuraSpell(sa)) {
|
||||
Card source = sa.getHostCard();
|
||||
if (!source.hasKeyword(Keyword.ENCHANT)) {
|
||||
return false;
|
||||
}
|
||||
for (KeywordInterface ki : source.getKeywords(Keyword.ENCHANT)) {
|
||||
String ko = ki.getOriginal();
|
||||
String m[] = ko.split(":");
|
||||
String v = m[1];
|
||||
if (v.contains("Player") || v.contains("Opponent")) {
|
||||
spellCanTargetPlayer = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (tgt.canTgtPlayer() && (!isAuraSpell(sa) || spellCanTargetPlayer)) {
|
||||
if (tgt.canTgtPlayer()) {
|
||||
List<Player> targetable = new ArrayList<>();
|
||||
for (final Player player : sa.getHostCard().getGame().getPlayers()) {
|
||||
if (sa.canTarget(player)) {
|
||||
@@ -1088,8 +1005,9 @@ public class AttachAi extends SpellAbilityAi {
|
||||
CardCollection toRemove = new CardCollection();
|
||||
for (Trigger t : attachSource.getTriggers()) {
|
||||
if (t.getMode() == TriggerType.ChangesZone) {
|
||||
if ("Card.Self".equals(t.getParam("ValidCard"))
|
||||
&& "Battlefield".equals(t.getParam("Destination"))) {
|
||||
final Map<String, String> params = t.getMapParams();
|
||||
if ("Card.Self".equals(params.get("ValidCard"))
|
||||
&& "Battlefield".equals(params.get("Destination"))) {
|
||||
SpellAbility trigSa = t.ensureAbility();
|
||||
if (trigSa != null && trigSa.getApi() == ApiType.DealDamage && "Enchanted".equals(trigSa.getParam("Defined"))) {
|
||||
for (Card target : list) {
|
||||
@@ -1149,27 +1067,29 @@ public class AttachAi extends SpellAbilityAi {
|
||||
boolean grantingExtraBlock = false;
|
||||
|
||||
for (final StaticAbility stAbility : attachSource.getStaticAbilities()) {
|
||||
if (!stAbility.checkMode(StaticAbilityMode.Continuous)) {
|
||||
final Map<String, String> stabMap = stAbility.getMapParams();
|
||||
|
||||
if (!"Continuous".equals(stabMap.get("Mode"))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final String affected = stAbility.getParam("Affected");
|
||||
final String affected = stabMap.get("Affected");
|
||||
|
||||
if (affected == null) {
|
||||
continue;
|
||||
}
|
||||
if (affected.contains(stCheck) || affected.contains("AttachedBy")) {
|
||||
totToughness += AbilityUtils.calculateAmount(attachSource, stAbility.getParam("AddToughness"), stAbility);
|
||||
totPower += AbilityUtils.calculateAmount(attachSource, stAbility.getParam("AddPower"), stAbility);
|
||||
totToughness += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddToughness"), stAbility);
|
||||
totPower += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddPower"), stAbility);
|
||||
|
||||
grantingAbilities |= stAbility.hasParam("AddAbility");
|
||||
grantingExtraBlock |= stAbility.hasParam("CanBlockAmount") || stAbility.hasParam("CanBlockAny");
|
||||
grantingAbilities |= stabMap.containsKey("AddAbility");
|
||||
grantingExtraBlock |= stabMap.containsKey("CanBlockAmount") || stabMap.containsKey("CanBlockAny");
|
||||
|
||||
String kws = stAbility.getParam("AddKeyword");
|
||||
String kws = stabMap.get("AddKeyword");
|
||||
if (kws != null) {
|
||||
keywords.addAll(Arrays.asList(kws.split(" & ")));
|
||||
}
|
||||
kws = stAbility.getParam("AddHiddenKeyword");
|
||||
kws = stabMap.get("AddHiddenKeyword");
|
||||
if (kws != null) {
|
||||
keywords.addAll(Arrays.asList(kws.split(" & ")));
|
||||
}
|
||||
@@ -1238,17 +1158,12 @@ public class AttachAi extends SpellAbilityAi {
|
||||
// TODO Somehow test for definitive advantage (e.g. opponent low on health, AI is attacking)
|
||||
// to be able to deal the final blow with an enchanted vehicle like that
|
||||
boolean canOnlyTargetCreatures = true;
|
||||
if (attachSource.isAura()) {
|
||||
for (KeywordInterface ki : attachSource.getKeywords(Keyword.ENCHANT)) {
|
||||
String o = ki.getOriginal();
|
||||
String m[] = o.split(":");
|
||||
String v = m[1];
|
||||
if (!v.startsWith("Creature")) {
|
||||
for (String valid : ObjectUtils.firstNonNull(attachSource.getFirstAttachSpell(), sa).getTargetRestrictions().getValidTgts()) {
|
||||
if (!valid.startsWith("Creature")) {
|
||||
canOnlyTargetCreatures = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (canOnlyTargetCreatures && (attachSource.isAura() || attachSource.isEquipment())) {
|
||||
prefList = CardLists.filter(prefList, c -> c.getTimesCrewedThisTurn() == 0 || (attachSource.isEquipment() && attachSource.getGame().getPhaseHandler().is(PhaseType.MAIN1, ai)));
|
||||
}
|
||||
@@ -1472,6 +1387,8 @@ public class AttachAi extends SpellAbilityAi {
|
||||
c = attachAICuriosityPreference(sa, prefList, mandatory, attachSource);
|
||||
} else if ("ChangeType".equals(logic)) {
|
||||
c = attachAIChangeTypePreference(sa, prefList, mandatory, attachSource);
|
||||
} else if ("KeepTapped".equals(logic)) {
|
||||
c = attachAIKeepTappedPreference(sa, prefList, mandatory, attachSource);
|
||||
} else if ("Animate".equals(logic)) {
|
||||
c = attachAIAnimatePreference(sa, prefList, mandatory, attachSource);
|
||||
} else if ("Reanimate".equals(logic)) {
|
||||
@@ -1482,12 +1399,6 @@ public class AttachAi extends SpellAbilityAi {
|
||||
c = attachAIHighestEvaluationPreference(prefList);
|
||||
}
|
||||
|
||||
if (isAuraSpell(sa)) {
|
||||
if (attachSource.getReplacementEffects().anyMatch(re -> re.getMode().equals(ReplacementType.Untap) && re.getLayer().equals(ReplacementLayer.CantHappen))) {
|
||||
c = attachAIKeepTappedPreference(sa, prefList, mandatory, attachSource);
|
||||
}
|
||||
}
|
||||
|
||||
// Consider exceptional cases which break the normal evaluation rules
|
||||
if (!isUsefulAttachAction(ai, c, sa)) {
|
||||
return null;
|
||||
@@ -1640,6 +1551,8 @@ public class AttachAi extends SpellAbilityAi {
|
||||
} else if (keyword.endsWith("Prevent all combat damage that would be dealt to and dealt by CARDNAME.")
|
||||
|| keyword.endsWith("Prevent all damage that would be dealt to and dealt by CARDNAME.")) {
|
||||
return card.getNetCombatDamage() >= 2 && ComputerUtilCombat.canAttackNextTurn(card);
|
||||
} else if (keyword.endsWith("CARDNAME doesn't untap during your untap step.")) {
|
||||
return !card.isUntapped();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import forge.game.player.Player;
|
||||
import forge.game.player.PlayerActionConfirmMode;
|
||||
import forge.game.spellability.AbilitySub;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.SpellAbilityStackInstance;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.staticability.StaticAbilityMustTarget;
|
||||
import forge.game.zone.ZoneType;
|
||||
@@ -137,6 +138,8 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
if (aiLogic != null) {
|
||||
if (aiLogic.equals("Always")) {
|
||||
return true;
|
||||
} else if (aiLogic.startsWith("ExileSpell")) {
|
||||
return doExileSpellLogic(aiPlayer, sa);
|
||||
} else if (aiLogic.startsWith("SacAndUpgrade")) { // Birthing Pod, Natural Order, etc.
|
||||
return doSacAndUpgradeLogic(aiPlayer, sa);
|
||||
} else if (aiLogic.startsWith("SacAndRetFromGrave")) { // Recurring Nightmare, etc.
|
||||
@@ -452,7 +455,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
final AbilitySub subAb = sa.getSubAbility();
|
||||
return subAb == null || SpellApiToAi.Converter.get(subAb).chkDrawbackWithSubs(ai, subAb);
|
||||
return subAb == null || SpellApiToAi.Converter.get(subAb.getApi()).chkDrawbackWithSubs(ai, subAb);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -770,7 +773,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
final AbilitySub subAb = sa.getSubAbility();
|
||||
return subAb == null || SpellApiToAi.Converter.get(subAb).chkDrawbackWithSubs(ai, subAb);
|
||||
return subAb == null || SpellApiToAi.Converter.get(subAb.getApi()).chkDrawbackWithSubs(ai, subAb);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -818,7 +821,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
//don't unearth after attacking is possible
|
||||
if (sa.isKeyword(Keyword.UNEARTH) && ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
|
||||
if (sa.hasParam("Unearth") && ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -875,10 +878,6 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
origin.addAll(ZoneType.listValueOf(sa.getParam("TgtZone")));
|
||||
}
|
||||
|
||||
if (origin.contains(ZoneType.Stack) && doExileSpellLogic(ai, sa, mandatory)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final ZoneType destination = ZoneType.smartValueOf(sa.getParam("Destination"));
|
||||
final Game game = ai.getGame();
|
||||
|
||||
@@ -903,6 +902,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
}
|
||||
CardCollection list = CardLists.getTargetableCards(game.getCardsIn(origin), sa);
|
||||
|
||||
// Filter AI-specific targets if provided
|
||||
list = ComputerUtil.filterAITgts(sa, ai, list, true);
|
||||
if (sa.hasParam("AITgtsOnlyBetterThanSelf")) {
|
||||
list = CardLists.filter(list, card -> ComputerUtilCard.evaluateCreature(card) > ComputerUtilCard.evaluateCreature(source) + 30);
|
||||
@@ -914,9 +914,6 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
if (sa.isSpell()) {
|
||||
list.remove(source); // spells can't target their own source, because it's actually in the stack zone
|
||||
}
|
||||
|
||||
// list = CardLists.canSubsequentlyTarget(list, sa);
|
||||
|
||||
if (sa.hasParam("AttachedTo")) {
|
||||
list = CardLists.filter(list, c -> {
|
||||
for (Card card : game.getCardsIn(ZoneType.Battlefield)) {
|
||||
@@ -944,10 +941,6 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
|
||||
immediately = immediately || ComputerUtil.playImmediately(ai, sa);
|
||||
|
||||
if (list.isEmpty() && immediately && sa.getMaxTargets() == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Narrow down the list:
|
||||
if (origin.contains(ZoneType.Battlefield)) {
|
||||
if ("Polymorph".equals(sa.getParam("AILogic"))) {
|
||||
@@ -1285,10 +1278,8 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
list.remove(choice);
|
||||
if (sa.canTarget(choice)) {
|
||||
sa.getTargets().add(choice);
|
||||
}
|
||||
}
|
||||
|
||||
// Honor the Single Zone restriction. For now, simply remove targets that do not belong to the same zone as the first targeted card.
|
||||
// TODO: ideally the AI should consider at this point which targets exactly to pick (e.g. one card in the first player's graveyard
|
||||
@@ -1453,9 +1444,6 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
// AI Targeting
|
||||
Card choice = null;
|
||||
|
||||
// Filter out cards TargetsForEachPlayer
|
||||
list = CardLists.canSubsequentlyTarget(list, sa);
|
||||
|
||||
if (!list.isEmpty()) {
|
||||
Card mostExpensivePermanent = ComputerUtilCard.getMostExpensivePermanentAI(list);
|
||||
if (mostExpensivePermanent.isCreature()
|
||||
@@ -2069,26 +2057,33 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean doExileSpellLogic(final Player ai, final SpellAbility sa, final boolean mandatory) {
|
||||
List<ApiType> dangerousApi = null;
|
||||
CardCollection spells = new CardCollection(ai.getGame().getStackZone().getCards());
|
||||
Collections.reverse(spells);
|
||||
if (!mandatory && !spells.isEmpty()) {
|
||||
spells = spells.subList(0, 1);
|
||||
spells = ComputerUtil.filterAITgts(sa, ai, spells, true);
|
||||
dangerousApi = Arrays.asList(ApiType.DealDamage, ApiType.DamageAll, ApiType.Destroy, ApiType.DestroyAll, ApiType.Sacrifice, ApiType.SacrificeAll);
|
||||
private boolean doExileSpellLogic(final Player aiPlayer, final SpellAbility sa) {
|
||||
String aiLogic = sa.getParamOrDefault("AILogic", "");
|
||||
SpellAbilityStackInstance top = aiPlayer.getGame().getStack().peek();
|
||||
List<ApiType> dangerousApi = Arrays.asList(ApiType.DealDamage, ApiType.DamageAll, ApiType.Destroy, ApiType.DestroyAll, ApiType.Sacrifice, ApiType.SacrificeAll);
|
||||
int manaCost = 0;
|
||||
int minCost = 0;
|
||||
|
||||
if (aiLogic.contains(".")) {
|
||||
minCost = Integer.parseInt(aiLogic.substring(aiLogic.indexOf(".") + 1));
|
||||
}
|
||||
|
||||
for (Card c : spells) {
|
||||
SpellAbility topSA = ai.getGame().getStack().getSpellMatchingHost(c);
|
||||
if (topSA != null && (dangerousApi == null ||
|
||||
(dangerousApi.contains(topSA.getApi()) && topSA.getActivatingPlayer().isOpponentOf(ai)))
|
||||
&& sa.canTarget(topSA)) {
|
||||
if (top != null) {
|
||||
SpellAbility topSA = top.getSpellAbility();
|
||||
if (topSA != null) {
|
||||
if (topSA.getPayCosts().hasManaCost()) {
|
||||
manaCost = topSA.getPayCosts().getTotalMana().getCMC();
|
||||
}
|
||||
|
||||
if ((manaCost >= minCost || dangerousApi.contains(topSA.getApi()))
|
||||
&& topSA.getActivatingPlayer().isOpponentOf(aiPlayer)
|
||||
&& sa.canTargetSpellAbility(topSA)) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(topSA);
|
||||
return sa.isTargetNumberValid();
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -32,14 +32,13 @@ public class CharmAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
boolean timingRight = sa.isTrigger(); //is there a reason to play the charm now?
|
||||
boolean choiceForOpp = !ai.equals(sa.getActivatingPlayer());
|
||||
|
||||
// Reset the chosen list otherwise it will be locked in forever by earlier calls
|
||||
sa.setChosenList(null);
|
||||
sa.setSubAbility(null);
|
||||
List<AbilitySub> chosenList;
|
||||
|
||||
if (choiceForOpp) {
|
||||
if (!ai.equals(sa.getActivatingPlayer())) {
|
||||
// This branch is for "An Opponent chooses" Charm spells from Alliances
|
||||
// Current just choose the first available spell, which seem generally less disastrous for the AI.
|
||||
chosenList = choices.subList(1, choices.size());
|
||||
@@ -79,11 +78,6 @@ public class CharmAi extends SpellAbilityAi {
|
||||
|
||||
// store the choices so they'll get reused
|
||||
sa.setChosenList(chosenList);
|
||||
|
||||
if (choiceForOpp) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (sa.isSpell()) {
|
||||
// prebuild chain to improve cost calculation accuracy
|
||||
CharmEffect.chainAbilities(sa, chosenList);
|
||||
@@ -93,7 +87,8 @@ public class CharmAi extends SpellAbilityAi {
|
||||
return MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
||||
}
|
||||
|
||||
private List<AbilitySub> chooseOptionsAi(SpellAbility sa, List<AbilitySub> choices, final Player ai, boolean isTrigger, int num, int min) {
|
||||
private List<AbilitySub> chooseOptionsAi(SpellAbility sa, List<AbilitySub> choices, final Player ai, boolean isTrigger, int num,
|
||||
int min) {
|
||||
List<AbilitySub> chosenList = Lists.newArrayList();
|
||||
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
|
||||
boolean allowRepeat = sa.hasParam("CanRepeatModes"); // FIXME: unused for now, the AI doesn't know how to effectively handle repeated choices
|
||||
@@ -113,9 +108,10 @@ public class CharmAi extends SpellAbilityAi {
|
||||
int curPawprintAmount = AbilityUtils.calculateAmount(sub.getHostCard(), sub.getParamOrDefault("Pawprint", "0"), sub);
|
||||
if (pawprintAmount + curPawprintAmount > pawprintLimit) {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
pawprintAmount += curPawprintAmount;
|
||||
}
|
||||
}
|
||||
chosenList.add(sub);
|
||||
if (chosenList.size() == num) {
|
||||
return chosenList; // maximum choices reached
|
||||
|
||||
@@ -29,7 +29,7 @@ public class ChooseGenericAi extends SpellAbilityAi {
|
||||
return true;
|
||||
} else if ("Pump".equals(aiLogic) || "BestOption".equals(aiLogic)) {
|
||||
for (AbilitySub sb : sa.getAdditionalAbilityList("Choices")) {
|
||||
if (SpellApiToAi.Converter.get(sb).canPlayAIWithSubs(ai, sb)) {
|
||||
if (SpellApiToAi.Converter.get(sb.getApi()).canPlayAIWithSubs(ai, sb)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -93,7 +93,7 @@ public class ChooseGenericAi extends SpellAbilityAi {
|
||||
String unlessCost = sp.getParam("UnlessCost");
|
||||
sp.setActivatingPlayer(sa.getActivatingPlayer());
|
||||
Cost unless = new Cost(unlessCost, false);
|
||||
if (SpellApiToAi.Converter.get(sp).willPayUnlessCost(sp, player, unless, false, new FCollection<>(player))
|
||||
if (SpellApiToAi.Converter.get(sp.getApi()).willPayUnlessCost(sp, player, unless, false, new FCollection<>(player))
|
||||
&& ComputerUtilCost.canPayCost(unless, sp, player, true)) {
|
||||
return sp;
|
||||
}
|
||||
@@ -161,10 +161,10 @@ public class ChooseGenericAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
// FatespinnerSkipDraw,FatespinnerSkipMain,FatespinnerSkipCombat
|
||||
if (game.getReplacementHandler().wouldPhaseBeSkipped(player, PhaseType.DRAW)) {
|
||||
if (game.getReplacementHandler().wouldPhaseBeSkipped(player, "Draw")) {
|
||||
return skipDraw;
|
||||
}
|
||||
if (game.getReplacementHandler().wouldPhaseBeSkipped(player, PhaseType.COMBAT_BEGIN)) {
|
||||
if (game.getReplacementHandler().wouldPhaseBeSkipped(player, "BeginCombat")) {
|
||||
return skipCombat;
|
||||
}
|
||||
|
||||
@@ -262,7 +262,7 @@ public class ChooseGenericAi extends SpellAbilityAi {
|
||||
List<SpellAbility> filtered = Lists.newArrayList();
|
||||
// filter first for the spells which can be done
|
||||
for (SpellAbility sp : spells) {
|
||||
if (SpellApiToAi.Converter.get(sp).canPlayAIWithSubs(player, sp)) {
|
||||
if (SpellApiToAi.Converter.get(sp.getApi()).canPlayAIWithSubs(player, sp)) {
|
||||
filtered.add(sp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ public class ClassLevelUpAi extends SpellAbilityAi {
|
||||
continue;
|
||||
}
|
||||
SpellAbility effect = t.ensureAbility();
|
||||
if (!SpellApiToAi.Converter.get(effect).doTriggerAI(aiPlayer, effect, false)) {
|
||||
if (!SpellApiToAi.Converter.get(effect.getApi()).doTriggerAI(aiPlayer, effect, false)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ public class CloakAi extends ManifestBaseAi {
|
||||
// (e.g. Grafdigger's Cage)
|
||||
Card topCopy = CardCopyService.getLKICopy(card);
|
||||
topCopy.turnFaceDownNoUpdate();
|
||||
topCopy.setCloaked(sa);
|
||||
topCopy.setCloaked(true);
|
||||
|
||||
if (ComputerUtil.isETBprevented(topCopy)) {
|
||||
return false;
|
||||
|
||||
@@ -4,7 +4,6 @@ import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilMana;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardLists;
|
||||
@@ -19,13 +18,6 @@ public class ConniveAi extends SpellAbilityAi {
|
||||
return false; // can't draw anything
|
||||
}
|
||||
|
||||
Card host = sa.getHostCard();
|
||||
|
||||
final int num = AbilityUtils.calculateAmount(host, sa.getParamOrDefault("ConniveNum", "1"), sa);
|
||||
if (num == 0) {
|
||||
return false; // Won't do anything
|
||||
}
|
||||
|
||||
CardCollection list = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
|
||||
|
||||
// Filter AI-specific targets if provided
|
||||
|
||||
@@ -205,9 +205,6 @@ public class ControlGainAi extends SpellAbilityAi {
|
||||
while (t == null) {
|
||||
// filter by MustTarget requirement
|
||||
CardCollection originalList = new CardCollection(list);
|
||||
|
||||
list = CardLists.canSubsequentlyTarget(list, sa);
|
||||
|
||||
boolean mustTargetFiltered = StaticAbilityMustTarget.filterMustTargetCards(ai, list, sa);
|
||||
|
||||
if (planeswalkers > 0) {
|
||||
|
||||
@@ -152,8 +152,6 @@ public class CopyPermanentAi extends SpellAbilityAi {
|
||||
|
||||
// target loop
|
||||
while (sa.canAddMoreTarget()) {
|
||||
list = CardLists.canSubsequentlyTarget(list, sa);
|
||||
|
||||
if (list.isEmpty()) {
|
||||
if (!sa.isTargetNumberValid() || sa.getTargets().size() == 0) {
|
||||
sa.resetTargets();
|
||||
|
||||
@@ -255,9 +255,6 @@ public class CounterAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
sa.resetTargets();
|
||||
if (mandatory && !sa.canAddMoreTarget()) {
|
||||
return true;
|
||||
}
|
||||
Pair<SpellAbility, Boolean> pair = chooseTargetSpellAbility(game, sa, ai, mandatory);
|
||||
SpellAbility tgtSA = pair.getLeft();
|
||||
|
||||
@@ -381,7 +378,7 @@ public class CounterAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
// no reason to pay if we don't plan to confirm
|
||||
if (toBeCountered.isOptionalTrigger() && !SpellApiToAi.Converter.get(toBeCountered).doTriggerNoCostWithSubs(payer, toBeCountered, false)) {
|
||||
if (toBeCountered.isOptionalTrigger() && !SpellApiToAi.Converter.get(toBeCountered.getApi()).doTriggerNoCostWithSubs(payer, toBeCountered, false)) {
|
||||
return false;
|
||||
}
|
||||
// TODO check hasFizzled
|
||||
|
||||
@@ -95,7 +95,7 @@ public class DamageDealAi extends DamageAiBase {
|
||||
final String damage = sa.getParam("NumDmg");
|
||||
int dmg = calculateDamageAmount(sa, source, damage);
|
||||
|
||||
if (damage.equals("X") || (dmg == 0 && source.getSVar("X").equals("Count$xPaid"))) {
|
||||
if (damage.equals("X") || source.getSVar("X").equals("Count$xPaid")) {
|
||||
if (sa.getSVar("X").equals("Count$xPaid") || sa.getSVar(damage).equals("Count$xPaid")) {
|
||||
dmg = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ public class DelayedTriggerAi extends SpellAbilityAi {
|
||||
trigsa.setActivatingPlayer(ai);
|
||||
|
||||
if (trigsa instanceof AbilitySub) {
|
||||
return SpellApiToAi.Converter.get(trigsa).chkDrawbackWithSubs(ai, (AbilitySub)trigsa);
|
||||
return SpellApiToAi.Converter.get(trigsa.getApi()).chkDrawbackWithSubs(ai, (AbilitySub)trigsa);
|
||||
} else {
|
||||
return AiPlayDecision.WillPlay == ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(trigsa);
|
||||
}
|
||||
|
||||
@@ -216,8 +216,6 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
CardCollection originalList = new CardCollection(list);
|
||||
boolean mustTargetFiltered = StaticAbilityMustTarget.filterMustTargetCards(ai, list, sa);
|
||||
|
||||
list = CardLists.canSubsequentlyTarget(list, sa);
|
||||
|
||||
if (list.isEmpty()) {
|
||||
if (!sa.isMinTargetChosen() || sa.isZeroTargets()) {
|
||||
sa.resetTargets();
|
||||
@@ -277,7 +275,6 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
choice = aura;
|
||||
}
|
||||
}
|
||||
// TODO What about stolen permanents we're getting back at the end of the turn?
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,10 +284,8 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
list.remove(choice);
|
||||
if (sa.canTarget(choice)) {
|
||||
sa.getTargets().add(choice);
|
||||
}
|
||||
}
|
||||
} else if (sa.hasParam("Defined")) {
|
||||
list = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
|
||||
if ("WillSkipTurn".equals(logic) && (source.getController().equals(ai)
|
||||
@@ -366,10 +361,7 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
}
|
||||
} else {
|
||||
Card c = ComputerUtilCard.getBestAI(preferred);
|
||||
|
||||
if (sa.canTarget(c)) {
|
||||
sa.getTargets().add(c);
|
||||
}
|
||||
preferred.remove(c);
|
||||
}
|
||||
}
|
||||
@@ -390,9 +382,7 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
} else {
|
||||
c = ComputerUtilCard.getCheapestPermanentAI(list, sa, false);
|
||||
}
|
||||
if (sa.canTarget(c)) {
|
||||
sa.getTargets().add(c);
|
||||
}
|
||||
list.remove(c);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -515,17 +515,12 @@ public class DrawAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((computerHandSize + numCards > computerMaxHandSize)) {
|
||||
// Don't draw too many cards and then risk discarding cards at EOT
|
||||
if (game.getPhaseHandler().isPlayerTurn(ai)
|
||||
if ((computerHandSize + numCards > computerMaxHandSize)
|
||||
&& game.getPhaseHandler().isPlayerTurn(ai)
|
||||
&& !sa.isTrigger()
|
||||
&& !assumeSafeX
|
||||
&& !drawback) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (computerHandSize > computerMaxHandSize) {
|
||||
// Don't make my hand size get too big if already at max
|
||||
&& !assumeSafeX) {
|
||||
// Don't draw too many cards and then risk discarding cards at EOT
|
||||
if (!drawback) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,7 +287,7 @@ public class EffectAi extends SpellAbilityAi {
|
||||
} else if (logic.equals("Burn")) {
|
||||
// for DamageDeal sub-abilities (eg. Wild Slash, Skullcrack)
|
||||
SpellAbility burn = sa.getSubAbility();
|
||||
return SpellApiToAi.Converter.get(burn).canPlayAIWithSubs(ai, burn);
|
||||
return SpellApiToAi.Converter.get(burn.getApi()).canPlayAIWithSubs(ai, burn);
|
||||
} else if (logic.equals("YawgmothsWill")) {
|
||||
return SpecialCardAi.YawgmothsWill.consider(ai, sa);
|
||||
} else if (logic.startsWith("NeedCreatures")) {
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import com.google.common.collect.Sets;
|
||||
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.Game;
|
||||
import forge.game.card.*;
|
||||
import forge.game.card.token.TokenInfo;
|
||||
import forge.game.combat.Combat;
|
||||
import forge.game.combat.CombatUtil;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerActionConfirmMode;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.zone.ZoneType;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class EndureAi extends SpellAbilityAi {
|
||||
/* (non-Javadoc)
|
||||
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
|
||||
*/
|
||||
@Override
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
// Support for possible targeted Endure (e.g. target creature endures X)
|
||||
if (sa.usesTargeting()) {
|
||||
Card bestCreature = ComputerUtilCard.getBestCreatureAI(aiPlayer.getCardsIn(ZoneType.Battlefield));
|
||||
if (bestCreature == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(bestCreature);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean shouldPutCounters(Player ai, SpellAbility sa) {
|
||||
// TODO: adapted from Fabricate AI in TokenAi, maybe can be refactored to a single method
|
||||
final Card source = sa.getHostCard();
|
||||
final Game game = source.getGame();
|
||||
final String num = sa.getParamOrDefault("Num", "1");
|
||||
final int amount = AbilityUtils.calculateAmount(source, num, sa);
|
||||
|
||||
// if host would leave the play or if host is useless, create the token
|
||||
if (source.hasSVar("EndOfTurnLeavePlay") || ComputerUtilCard.isUselessCreature(ai, source)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// need a copy for one with extra +1/+1 counter boost,
|
||||
// without causing triggers to run
|
||||
final Card copy = CardCopyService.getLKICopy(source);
|
||||
copy.setCounters(CounterEnumType.P1P1, copy.getCounters(CounterEnumType.P1P1) + amount);
|
||||
copy.setZone(source.getZone());
|
||||
|
||||
// if host would put into the battlefield attacking
|
||||
Combat combat = source.getGame().getCombat();
|
||||
if (combat != null && combat.isAttacking(source)) {
|
||||
final Player defender = combat.getDefenderPlayerByAttacker(source);
|
||||
return defender.canLoseLife() && !ComputerUtilCard.canBeBlockedProfitably(defender, copy, true);
|
||||
}
|
||||
|
||||
// if the host has haste and can attack
|
||||
if (CombatUtil.canAttack(copy)) {
|
||||
for (final Player opp : ai.getOpponents()) {
|
||||
if (CombatUtil.canAttack(copy, opp) &&
|
||||
opp.canLoseLife() &&
|
||||
!ComputerUtilCard.canBeBlockedProfitably(opp, copy, true))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO check for trigger to turn token ETB into +1/+1 counter for host
|
||||
// TODO check for trigger to turn token ETB into damage or life loss for opponent
|
||||
// in these cases token might be preferred even if they would not survive
|
||||
|
||||
// evaluate creature with counters
|
||||
int evalCounter = ComputerUtilCard.evaluateCreature(copy);
|
||||
|
||||
// spawn the token so it's possible to evaluate it
|
||||
final Card token = TokenInfo.getProtoType("w_x_x_spirit", sa, ai, false);
|
||||
|
||||
token.setController(ai, 0);
|
||||
token.setLastKnownZone(ai.getZone(ZoneType.Battlefield));
|
||||
token.setTokenSpawningAbility(sa);
|
||||
|
||||
// evaluate the generated token
|
||||
token.setBasePowerString(num);
|
||||
token.setBasePower(amount);
|
||||
token.setBaseToughnessString(num);
|
||||
token.setBaseToughness(amount);
|
||||
|
||||
boolean result = true;
|
||||
|
||||
// need to check what the cards would be on the battlefield
|
||||
// do not attach yet, that would cause Events
|
||||
CardCollection preList = new CardCollection(token);
|
||||
game.getAction().checkStaticAbilities(false, Sets.newHashSet(token), preList);
|
||||
|
||||
// token would not survive
|
||||
if (!token.isCreature() || token.getNetToughness() < 1) {
|
||||
result = false;
|
||||
}
|
||||
|
||||
if (result) {
|
||||
int evalToken = ComputerUtilCard.evaluateCreature(token);
|
||||
result = evalToken < evalCounter;
|
||||
}
|
||||
|
||||
//reset static abilities
|
||||
game.getAction().checkStaticAbilities(false);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
|
||||
return shouldPutCounters(player, sa);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
// Support for possible targeted Endure (e.g. target creature endures X)
|
||||
if (sa.usesTargeting()) {
|
||||
CardCollection list = CardLists.getValidCards(aiPlayer.getGame().getCardsIn(ZoneType.Battlefield),
|
||||
sa.getTargetRestrictions().getValidTgts(), aiPlayer, sa.getHostCard(), sa);
|
||||
|
||||
if (!list.isEmpty()) {
|
||||
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(list));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return canPlayAI(aiPlayer, sa) || mandatory;
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ public class ImmediateTriggerAi extends SpellAbilityAi {
|
||||
trigsa.setActivatingPlayer(ai);
|
||||
|
||||
if (trigsa instanceof AbilitySub) {
|
||||
return SpellApiToAi.Converter.get(trigsa).chkDrawbackWithSubs(ai, (AbilitySub)trigsa);
|
||||
return SpellApiToAi.Converter.get(trigsa.getApi()).chkDrawbackWithSubs(ai, (AbilitySub)trigsa);
|
||||
} else {
|
||||
return AiPlayDecision.WillPlay == ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(trigsa);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ public class ManifestAi extends ManifestBaseAi {
|
||||
// (e.g. Grafdigger's Cage)
|
||||
Card topCopy = CardCopyService.getLKICopy(card);
|
||||
topCopy.turnFaceDownNoUpdate();
|
||||
topCopy.setManifested(sa);
|
||||
topCopy.setManifested(true);
|
||||
|
||||
if (ComputerUtil.isETBprevented(topCopy)) {
|
||||
return false;
|
||||
|
||||
@@ -93,7 +93,7 @@ public class PeekAndRevealAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
AbilitySub subAb = sa.getSubAbility();
|
||||
return subAb != null && SpellApiToAi.Converter.get(subAb).chkDrawbackWithSubs(player, subAb);
|
||||
return subAb != null && SpellApiToAi.Converter.get(subAb.getApi()).chkDrawbackWithSubs(player, subAb);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.staticability.StaticAbility;
|
||||
import forge.game.staticability.StaticAbilityMode;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.MyRandom;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
@@ -48,7 +47,7 @@ public class PermanentCreatureAi extends PermanentAi {
|
||||
if (sa.isDash()) {
|
||||
//only checks that the dashed creature will attack
|
||||
if (ph.isPlayerTurn(ai) && ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
|
||||
if (game.getReplacementHandler().wouldPhaseBeSkipped(ai, PhaseType.COMBAT_BEGIN))
|
||||
if (game.getReplacementHandler().wouldPhaseBeSkipped(ai, "BeginCombat"))
|
||||
return false;
|
||||
if (ComputerUtilCost.canPayCost(sa.getHostCard().getSpellPermanent(), ai, false)) {
|
||||
//do not dash if creature can be played normally
|
||||
@@ -71,7 +70,7 @@ public class PermanentCreatureAi extends PermanentAi {
|
||||
// after attacking
|
||||
if (card.hasSVar("EndOfTurnLeavePlay")
|
||||
&& (!ph.isPlayerTurn(ai) || ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
|
||||
|| game.getReplacementHandler().wouldPhaseBeSkipped(ai, PhaseType.COMBAT_BEGIN))) {
|
||||
|| game.getReplacementHandler().wouldPhaseBeSkipped(ai, "BeginCombat"))) {
|
||||
// AiPlayDecision.AnotherTime
|
||||
return false;
|
||||
}
|
||||
@@ -155,7 +154,7 @@ public class PermanentCreatureAi extends PermanentAi {
|
||||
boolean canCastAtOppTurn = true;
|
||||
for (Card c : ai.getGame().getCardsIn(ZoneType.Battlefield)) {
|
||||
for (StaticAbility s : c.getStaticAbilities()) {
|
||||
if (s.checkMode(StaticAbilityMode.CantBeCast) && StringUtils.contains(s.getParam("Activator"), "NonActive")
|
||||
if ("CantBeCast".equals(s.getParam("Mode")) && StringUtils.contains(s.getParam("Activator"), "NonActive")
|
||||
&& (!s.getParam("Activator").startsWith("You") || c.getController().equals(ai))) {
|
||||
canCastAtOppTurn = false;
|
||||
break;
|
||||
|
||||
@@ -542,8 +542,6 @@ public class PumpAi extends PumpAiBase {
|
||||
Card t = null;
|
||||
// boolean goodt = false;
|
||||
|
||||
list = CardLists.canSubsequentlyTarget(list, sa);
|
||||
|
||||
if (list.isEmpty()) {
|
||||
if (!sa.isMinTargetChosen() || sa.isZeroTargets()) {
|
||||
if (mandatory || ComputerUtil.activateForCost(sa, ai)) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import forge.game.combat.CombatUtil;
|
||||
import forge.game.keyword.Keyword;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.phase.Untap;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.zone.ZoneType;
|
||||
@@ -136,7 +137,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
|
||||
return CombatUtil.canBlockAtLeastOne(card, attackers);
|
||||
} else if (keyword.endsWith("This card doesn't untap during your next untap step.")) {
|
||||
return !ph.getPhase().isBefore(PhaseType.MAIN2) && !card.isUntapped() && ph.isPlayerTurn(ai)
|
||||
&& card.canUntap(card.getController(), true);
|
||||
&& Untap.canUntap(card);
|
||||
} else if (keyword.endsWith("Prevent all combat damage that would be dealt by CARDNAME.")
|
||||
|| keyword.endsWith("Prevent all damage that would be dealt by CARDNAME.")) {
|
||||
if (ph.isPlayerTurn(ai) && (!(CombatUtil.canBlock(card) || combat != null && combat.isBlocking(card))
|
||||
|
||||
@@ -6,7 +6,6 @@ import forge.ai.SpellAbilityAi;
|
||||
import forge.card.CardStateName;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.*;
|
||||
import forge.game.keyword.Keyword;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
@@ -143,7 +142,7 @@ public class SetStateAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
// hidden agenda
|
||||
if (card.getState(CardStateName.Original).hasKeyword(Keyword.HIDDEN_AGENDA)
|
||||
if (card.getState(CardStateName.Original).hasIntrinsicKeyword("Hidden agenda")
|
||||
&& card.isInZone(ZoneType.Command)) {
|
||||
String chosenName = card.getNamedCard();
|
||||
for (Card cast : ai.getGame().getStack().getSpellsCastThisTurn()) {
|
||||
|
||||
@@ -76,7 +76,7 @@ public class TokenAi extends SpellAbilityAi {
|
||||
final AbilitySub sub = sa.getSubAbility();
|
||||
// useful
|
||||
// no token created
|
||||
return pwPlus || (sub != null && SpellApiToAi.Converter.get(sub).chkAIDrawback(sub, ai)); // planeswalker plus ability or sub-ability is
|
||||
return pwPlus || (sub != null && SpellApiToAi.Converter.get(sub.getApi()).chkAIDrawback(sub, ai)); // planeswalker plus ability or sub-ability is
|
||||
}
|
||||
|
||||
String tokenPower = sa.getParamOrDefault("TokenPower", actualToken.getBasePowerString());
|
||||
@@ -206,8 +206,7 @@ public class TokenAi extends SpellAbilityAi {
|
||||
&& game.getPhaseHandler().getPlayerTurn().isOpponentOf(ai)
|
||||
&& game.getCombat() != null
|
||||
&& !game.getCombat().getAttackers().isEmpty()
|
||||
&& alwaysOnOppAttack
|
||||
&& actualToken.isCreature()) {
|
||||
&& alwaysOnOppAttack) {
|
||||
for (Card attacker : game.getCombat().getAttackers()) {
|
||||
if (CombatUtil.canBlock(attacker, actualToken)) {
|
||||
return true;
|
||||
@@ -367,7 +366,8 @@ public class TokenAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
private boolean tgtRoleAura(final Player ai, final SpellAbility sa, final Card tok, final boolean mandatory) {
|
||||
boolean isCurse = "Curse".equals(sa.getParam("AILogic")) || "Curse".equals(tok.getSVar("AttachAILogic"));
|
||||
boolean isCurse = "Curse".equals(sa.getParam("AILogic")) ||
|
||||
tok.getFirstAttachSpell().getParamOrDefault("AILogic", "").equals("Curse");
|
||||
List<Card> tgts = CardUtil.getValidCardsToTarget(sa);
|
||||
|
||||
// look for card without role from ai
|
||||
@@ -498,7 +498,7 @@ public class TokenAi extends SpellAbilityAi {
|
||||
if (!tokenCard.isCreature() || tokenCard.getNetToughness() < 1) {
|
||||
return false;
|
||||
}
|
||||
int evalActivator = ComputerUtilCard.evaluateCreature(tokenCard) + ComputerUtilCard.evaluateCreatureList(p.getCreaturesInPlay());
|
||||
int evalActivator = ComputerUtilCard.evaluateCreature(tokenCard) + ComputerUtilCard.evaluateCreatureList(p.getCreaturesInPlay());;
|
||||
int evalPayerCreatures = ComputerUtilCard.evaluateCreatureList(payer.getCreaturesInPlay());
|
||||
|
||||
if (evalActivator > evalPayerCreatures) {
|
||||
|
||||
@@ -16,6 +16,7 @@ import forge.game.cost.CostTap;
|
||||
import forge.game.mana.ManaCostBeingPaid;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.phase.Untap;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerCollection;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
@@ -318,14 +319,15 @@ public class UntapAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
public Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> list, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
|
||||
CardCollection filteredList = CardLists.filterControlledBy(list, ai.getYourTeam());
|
||||
if (!filteredList.isEmpty()) {
|
||||
return ComputerUtilCard.getBestAI(filteredList);
|
||||
}
|
||||
CardCollection pref = CardLists.filterControlledBy(list, ai.getYourTeam());
|
||||
if (pref.isEmpty()) {
|
||||
if (isOptional) {
|
||||
return null;
|
||||
}
|
||||
return ComputerUtilCard.getWorstAI(list);
|
||||
} else {
|
||||
list = pref;
|
||||
}
|
||||
return ComputerUtilCard.getBestAI(list);
|
||||
}
|
||||
|
||||
private static Card detectPriorityUntapTargets(final List<Card> untapList) {
|
||||
@@ -337,7 +339,7 @@ public class UntapAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
// See if there's anything to untap that is tapped and that doesn't untap during the next untap step by itself
|
||||
CardCollection noAutoUntap = CardLists.filter(untapList, c -> !c.canUntap(c.getController(), true));
|
||||
CardCollection noAutoUntap = CardLists.filter(untapList, Untap.CANUNTAP.negate());
|
||||
if (!noAutoUntap.isEmpty()) {
|
||||
return ComputerUtilCard.getBestAI(noAutoUntap);
|
||||
}
|
||||
|
||||
@@ -81,7 +81,6 @@ public class GameCopier {
|
||||
GameRules currentRules = origGame.getRules();
|
||||
Match newMatch = new Match(currentRules, newPlayers, origGame.getView().getTitle());
|
||||
Game newGame = new Game(newPlayers, currentRules, newMatch);
|
||||
newGame.dangerouslySetTimestamp(origGame.getTimestamp());
|
||||
|
||||
for (int i = 0; i < origGame.getPlayers().size(); i++) {
|
||||
Player origPlayer = origGame.getPlayers().get(i);
|
||||
@@ -95,8 +94,7 @@ public class GameCopier {
|
||||
newPlayer.setDamageReceivedThisTurn(origPlayer.getDamageReceivedThisTurn());
|
||||
newPlayer.setLandsPlayedThisTurn(origPlayer.getLandsPlayedThisTurn());
|
||||
newPlayer.setCounters(Maps.newHashMap(origPlayer.getCounters()));
|
||||
newPlayer.setSpeed(origPlayer.getSpeed());
|
||||
newPlayer.setBlessing(origPlayer.hasBlessing(), null);
|
||||
newPlayer.setBlessing(origPlayer.hasBlessing());
|
||||
newPlayer.setRevolt(origPlayer.hasRevolt());
|
||||
newPlayer.setDescended(origPlayer.getDescended());
|
||||
newPlayer.setLibrarySearched(origPlayer.getLibrarySearched());
|
||||
@@ -373,10 +371,10 @@ public class GameCopier {
|
||||
if (c.isFaceDown()) {
|
||||
newCard.turnFaceDown(true);
|
||||
if (c.isManifested()) {
|
||||
newCard.setManifested(c.getManifestedSA());
|
||||
newCard.setManifested(true);
|
||||
}
|
||||
if (c.isCloaked()) {
|
||||
newCard.setCloaked(c.getCloakedSA());
|
||||
newCard.setCloaked(true);
|
||||
}
|
||||
}
|
||||
if (c.isMonstrous()) {
|
||||
|
||||
@@ -177,6 +177,7 @@ public class GameStateEvaluator {
|
||||
// TODO should these be fixed quantities or should they be linear out of like 1000/(desired - total)?
|
||||
int value = 0;
|
||||
// get the colors of mana we can produce and the maximum number of pips
|
||||
int max_colored = 0;
|
||||
int max_total = 0;
|
||||
// this logic taken from ManaCost.getColorShardCounts()
|
||||
int[] counts = new int[6]; // in WUBRGC order
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<artifactId>forge</artifactId>
|
||||
<groupId>forge</groupId>
|
||||
<version>${revision}</version>
|
||||
<version>2.0.01</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>forge-core</artifactId>
|
||||
|
||||
@@ -23,14 +23,10 @@ public final class ImageKeys {
|
||||
|
||||
public static final String HIDDEN_CARD = "hidden";
|
||||
public static final String MORPH_IMAGE = "morph";
|
||||
public static final String DISGUISED_IMAGE = "disguised";
|
||||
public static final String MANIFEST_IMAGE = "manifest";
|
||||
public static final String CLOAKED_IMAGE = "cloaked";
|
||||
public static final String FORETELL_IMAGE = "foretell";
|
||||
public static final String BLESSING_IMAGE = "blessing";
|
||||
public static final String INITIATIVE_IMAGE = "initiative";
|
||||
public static final String MONARCH_IMAGE = "monarch";
|
||||
public static final String THE_RING_IMAGE = "the_ring";
|
||||
public static final String RADIATION_IMAGE = "radiation";
|
||||
|
||||
public static final String BACKFACE_POSTFIX = "$alt";
|
||||
public static final String SPECFACE_W = "$wspec";
|
||||
@@ -102,18 +98,8 @@ public final class ImageKeys {
|
||||
|
||||
final String dir;
|
||||
final String filename;
|
||||
String[] tempdata = null;
|
||||
if (key.startsWith(ImageKeys.TOKEN_PREFIX)) {
|
||||
tempdata = key.substring(ImageKeys.TOKEN_PREFIX.length()).split("\\|");
|
||||
String tokenname = tempdata[0];
|
||||
if (tempdata.length > 1) {
|
||||
tokenname += "_" + tempdata[1];
|
||||
}
|
||||
if (tempdata.length > 2) {
|
||||
tokenname += "_" + tempdata[2];
|
||||
}
|
||||
filename = tokenname;
|
||||
|
||||
filename = key.substring(ImageKeys.TOKEN_PREFIX.length());
|
||||
dir = CACHE_TOKEN_PICS_DIR;
|
||||
} else if (key.startsWith(ImageKeys.ICON_PREFIX)) {
|
||||
filename = key.substring(ImageKeys.ICON_PREFIX.length());
|
||||
@@ -154,30 +140,6 @@ public final class ImageKeys {
|
||||
cachedCards.put(filename, file);
|
||||
return file;
|
||||
}
|
||||
if (dir.equals(CACHE_TOKEN_PICS_DIR)) {
|
||||
String setlessFilename = tempdata[0];
|
||||
String setCode = tempdata.length > 1 ? tempdata[1] : "";
|
||||
String collectorNumber = tempdata.length > 2 ? tempdata[2] : "";
|
||||
if (!setCode.isEmpty()) {
|
||||
if (!collectorNumber.isEmpty()) {
|
||||
file = findFile(dir, setCode + "/" + collectorNumber + "_" + setlessFilename);
|
||||
if (file != null) {
|
||||
cachedCards.put(filename, file);
|
||||
return file;
|
||||
}
|
||||
}
|
||||
file = findFile(dir, setCode + "/" + setlessFilename);
|
||||
if (file != null) {
|
||||
cachedCards.put(filename, file);
|
||||
return file;
|
||||
}
|
||||
}
|
||||
file = findFile(dir, setlessFilename);
|
||||
if (file != null) {
|
||||
cachedCards.put(filename, file);
|
||||
return file;
|
||||
}
|
||||
}
|
||||
|
||||
// AE -> Ae and Ae -> AE for older cards with different file names
|
||||
// on case-sensitive file systems
|
||||
@@ -259,7 +221,39 @@ public final class ImageKeys {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
if (filename.contains("/")) {
|
||||
if (dir.equals(CACHE_TOKEN_PICS_DIR)) {
|
||||
int index = filename.lastIndexOf('_');
|
||||
if (index != -1) {
|
||||
String setlessFilename = filename.substring(0, index);
|
||||
String setCode = filename.substring(index + 1);
|
||||
// try with upper case set
|
||||
file = findFile(dir, setlessFilename + "_" + setCode.toUpperCase());
|
||||
if (file != null) {
|
||||
cachedCards.put(filename, file);
|
||||
return file;
|
||||
}
|
||||
// try with lower case set
|
||||
file = findFile(dir, setlessFilename + "_" + setCode.toLowerCase());
|
||||
if (file != null) {
|
||||
cachedCards.put(filename, file);
|
||||
return file;
|
||||
}
|
||||
// try without set name
|
||||
file = findFile(dir, setlessFilename);
|
||||
if (file != null) {
|
||||
cachedCards.put(filename, file);
|
||||
return file;
|
||||
}
|
||||
// if there's an art variant try without it
|
||||
if (setlessFilename.matches(".*[0-9]*$")) {
|
||||
file = findFile(dir, setlessFilename.replaceAll("[0-9]*$", ""));
|
||||
if (file != null) {
|
||||
cachedCards.put(filename, file);
|
||||
return file;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (filename.contains("/")) {
|
||||
String setlessFilename = filename.substring(filename.indexOf('/') + 1);
|
||||
file = findFile(dir, setlessFilename);
|
||||
if (file != null) {
|
||||
|
||||
@@ -95,12 +95,12 @@ public class StaticData {
|
||||
if (!loadNonLegalCards) {
|
||||
for (CardEdition e : editions) {
|
||||
if (e.getType() == CardEdition.Type.FUNNY || e.getBorderColor() == CardEdition.BorderColor.SILVER) {
|
||||
List<CardEdition.EditionEntry> eternalCards = e.getFunnyEternalCards();
|
||||
List<CardEdition.CardInSet> eternalCards = e.getFunnyEternalCards();
|
||||
|
||||
for (CardEdition.EditionEntry cis : e.getAllCardsInSet()) {
|
||||
for (CardEdition.CardInSet cis : e.getAllCardsInSet()) {
|
||||
if (eternalCards.contains(cis))
|
||||
continue;
|
||||
funnyCards.add(cis.name());
|
||||
funnyCards.add(cis.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -217,9 +217,6 @@ public class StaticData {
|
||||
}
|
||||
|
||||
public CardEdition getCardEdition(String setCode) {
|
||||
if (CardEdition.UNKNOWN_CODE.equals(setCode)) {
|
||||
return CardEdition.UNKNOWN;
|
||||
}
|
||||
CardEdition edition = this.editions.get(setCode);
|
||||
return edition;
|
||||
}
|
||||
@@ -251,15 +248,6 @@ public class StaticData {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a PaperCard by looking at all available card databases for any matching print.
|
||||
* @param cardName The name of the card
|
||||
* @return PaperCard instance found in one of the available CardDb databases, or <code>null</code> if not found.
|
||||
*/
|
||||
public PaperCard fetchCard(final String cardName) {
|
||||
return fetchCard(cardName, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a PaperCard by looking at all available card databases;
|
||||
* @param cardName The name of the card
|
||||
@@ -790,11 +778,11 @@ public class StaticData {
|
||||
|
||||
Map<String, Pair<Boolean, Integer>> cardCount = new HashMap<>();
|
||||
List<CompletableFuture<?>> futures = new ArrayList<>();
|
||||
for (CardEdition.EditionEntry c : e.getAllCardsInSet()) {
|
||||
if (cardCount.containsKey(c.name())) {
|
||||
cardCount.put(c.name(), Pair.of(c.collectorNumber() != null && c.collectorNumber().startsWith("F"), cardCount.get(c.name()).getRight() + 1));
|
||||
for (CardEdition.CardInSet c : e.getAllCardsInSet()) {
|
||||
if (cardCount.containsKey(c.name)) {
|
||||
cardCount.put(c.name, Pair.of(c.collectorNumber != null && c.collectorNumber.startsWith("F"), cardCount.get(c.name).getRight() + 1));
|
||||
} else {
|
||||
cardCount.put(c.name(), Pair.of(c.collectorNumber() != null && c.collectorNumber().startsWith("F"), 1));
|
||||
cardCount.put(c.name, Pair.of(c.collectorNumber != null && c.collectorNumber.startsWith("F"), 1));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -856,9 +844,9 @@ public class StaticData {
|
||||
futures.clear();
|
||||
|
||||
// TODO: Audit token images here...
|
||||
for(Map.Entry<String, Collection<CardEdition.EditionEntry>> tokenEntry : e.getTokens().asMap().entrySet()) {
|
||||
for(Map.Entry<String, Integer> tokenEntry : e.getTokens().entrySet()) {
|
||||
final String name = tokenEntry.getKey();
|
||||
final int artIndex = tokenEntry.getValue().size();
|
||||
final int artIndex = tokenEntry.getValue();
|
||||
try {
|
||||
PaperToken token = getAllTokens().getToken(name, e.getCode());
|
||||
if (token == null) {
|
||||
@@ -995,23 +983,4 @@ public class StaticData {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
public String getOtherImageKey(String name, String set) {
|
||||
if (this.editions.get(set) != null) {
|
||||
String realSetCode = this.editions.get(set).getOtherSet(name);
|
||||
if (realSetCode != null) {
|
||||
CardEdition.EditionEntry ee = this.editions.get(realSetCode).findOther(name);
|
||||
if (ee != null) { // TODO add collector Number and new ImageKey format
|
||||
return ImageKeys.getTokenKey(String.format("%s|%s|%s", name, realSetCode, ee.collectorNumber()));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (CardEdition e : this.editions) {
|
||||
CardEdition.EditionEntry ee = e.findOther(name);
|
||||
if (ee != null) { // TODO add collector Number and new ImageKey format
|
||||
return ImageKeys.getTokenKey(String.format("%s|%s|%s", name, e.getCode(), ee.collectorNumber()));
|
||||
}
|
||||
}
|
||||
// final fallback
|
||||
return ImageKeys.getTokenKey(name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Multimaps;
|
||||
import forge.StaticData;
|
||||
import forge.card.CardEdition.EditionEntry;
|
||||
import forge.card.CardEdition.CardInSet;
|
||||
import forge.card.CardEdition.Type;
|
||||
import forge.deck.generation.IDeckGenPool;
|
||||
import forge.item.IPaperCard;
|
||||
@@ -42,8 +42,7 @@ import java.util.stream.Stream;
|
||||
public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
public final static String foilSuffix = "+";
|
||||
public final static char NameSetSeparator = '|';
|
||||
public final static String FlagPrefix = "#";
|
||||
public static final String FlagSeparator = "\t";
|
||||
public final static String colorIDPrefix = "#";
|
||||
private final String exlcudedCardName = "Concentrate";
|
||||
private final String exlcudedCardSet = "DS0";
|
||||
|
||||
@@ -94,19 +93,19 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
public int artIndex;
|
||||
public boolean isFoil;
|
||||
public String collectorNumber;
|
||||
public Map<String, String> flags;
|
||||
public Set<String> colorID;
|
||||
|
||||
private CardRequest(String name, String edition, int artIndex, boolean isFoil, String collectorNumber) {
|
||||
this(name, edition, artIndex, isFoil, collectorNumber, null);
|
||||
}
|
||||
|
||||
private CardRequest(String name, String edition, int artIndex, boolean isFoil, String collectorNumber, Map<String, String> flags) {
|
||||
private CardRequest(String name, String edition, int artIndex, boolean isFoil, String collectorNumber, Set<String> colorID) {
|
||||
cardName = name;
|
||||
this.edition = edition;
|
||||
this.artIndex = artIndex;
|
||||
this.isFoil = isFoil;
|
||||
this.collectorNumber = collectorNumber;
|
||||
this.flags = flags;
|
||||
this.colorID = colorID;
|
||||
}
|
||||
|
||||
public static boolean isFoilCardName(final String cardName){
|
||||
@@ -121,8 +120,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
}
|
||||
|
||||
public static String compose(String cardName, String setCode) {
|
||||
if(setCode == null || StringUtils.isBlank(setCode) || setCode.equals(CardEdition.UNKNOWN_CODE))
|
||||
setCode = "";
|
||||
setCode = setCode != null ? setCode : "";
|
||||
cardName = cardName != null ? cardName : "";
|
||||
if (cardName.indexOf(NameSetSeparator) != -1)
|
||||
// If cardName is another RequestString, just get card name and forget about the rest.
|
||||
@@ -136,36 +134,16 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
return requestInfo + NameSetSeparator + artIndex;
|
||||
}
|
||||
|
||||
public static String compose(String cardName, String setCode, String collectorNumber) {
|
||||
String requestInfo = compose(cardName, setCode);
|
||||
// CollectorNumber will be wrapped in square brackets
|
||||
collectorNumber = preprocessCollectorNumber(collectorNumber);
|
||||
return requestInfo + NameSetSeparator + collectorNumber;
|
||||
}
|
||||
|
||||
public static String compose(String cardName, String setCode, int artIndex, Map<String, String> flags) {
|
||||
public static String compose(String cardName, String setCode, int artIndex, Set<String> colorID) {
|
||||
String requestInfo = compose(cardName, setCode);
|
||||
artIndex = Math.max(artIndex, IPaperCard.DEFAULT_ART_INDEX);
|
||||
if(flags == null)
|
||||
return requestInfo + NameSetSeparator + artIndex;
|
||||
return requestInfo + NameSetSeparator + artIndex + getFlagSegment(flags);
|
||||
String cid = colorID == null ? "" : NameSetSeparator +
|
||||
colorID.toString().replace("[", colorIDPrefix).replace(", ", colorIDPrefix).replace("]", "");
|
||||
return requestInfo + NameSetSeparator + artIndex + cid;
|
||||
}
|
||||
|
||||
public static String compose(String cardName, String setCode, String collectorNumber, Map<String, String> flags) {
|
||||
public static String compose(String cardName, String setCode, String collectorNumber) {
|
||||
String requestInfo = compose(cardName, setCode);
|
||||
collectorNumber = preprocessCollectorNumber(collectorNumber);
|
||||
if(flags == null || flags.isEmpty())
|
||||
return requestInfo + NameSetSeparator + collectorNumber;
|
||||
return requestInfo + NameSetSeparator + collectorNumber + getFlagSegment(flags);
|
||||
}
|
||||
|
||||
public static String compose(PaperCard card) {
|
||||
String name = compose(card.getName(), card.isFoil());
|
||||
return compose(name, card.getEdition(), card.getCollectorNumber(), card.getMarkedFlags().toMap());
|
||||
}
|
||||
|
||||
public static String compose(String cardName, String setCode, int artIndex, String collectorNumber) {
|
||||
String requestInfo = compose(cardName, setCode, artIndex);
|
||||
// CollectorNumber will be wrapped in square brackets
|
||||
collectorNumber = preprocessCollectorNumber(collectorNumber);
|
||||
return requestInfo + NameSetSeparator + collectorNumber;
|
||||
@@ -182,21 +160,19 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
return collectorNumber;
|
||||
}
|
||||
|
||||
private static String getFlagSegment(Map<String, String> flags) {
|
||||
if(flags == null)
|
||||
return "";
|
||||
String flagText = flags.entrySet().stream()
|
||||
.map(e -> e.getKey() + "=" + e.getValue())
|
||||
.collect(Collectors.joining(FlagSeparator));
|
||||
return NameSetSeparator + FlagPrefix + "{" + flagText + "}";
|
||||
public static String compose(String cardName, String setCode, int artIndex, String collectorNumber) {
|
||||
String requestInfo = compose(cardName, setCode, artIndex);
|
||||
// CollectorNumber will be wrapped in square brackets
|
||||
collectorNumber = preprocessCollectorNumber(collectorNumber);
|
||||
return requestInfo + NameSetSeparator + collectorNumber;
|
||||
}
|
||||
|
||||
private static boolean isCollectorNumber(String s) {
|
||||
return s.startsWith("[") && s.endsWith("]");
|
||||
}
|
||||
|
||||
private static boolean isFlagSegment(String s) {
|
||||
return s.startsWith(FlagPrefix);
|
||||
private static boolean isColorIDString(String s) {
|
||||
return s.startsWith(colorIDPrefix);
|
||||
}
|
||||
|
||||
private static boolean isArtIndex(String s) {
|
||||
@@ -225,36 +201,44 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
return null;
|
||||
|
||||
String[] info = TextUtil.split(reqInfo, NameSetSeparator);
|
||||
int index = 1;
|
||||
int setPos;
|
||||
int artPos;
|
||||
int cNrPos;
|
||||
int clrPos;
|
||||
if (info.length >= 4) { // name|set|artIndex|[collNr]
|
||||
setPos = isSetCode(info[1]) ? 1 : -1;
|
||||
artPos = isArtIndex(info[2]) ? 2 : -1;
|
||||
cNrPos = isCollectorNumber(info[3]) ? 3 : -1;
|
||||
int pos = cNrPos > 0 ? -1 : 3;
|
||||
clrPos = pos > 0 ? isColorIDString(info[pos]) ? pos : -1 : -1;
|
||||
} else if (info.length == 3) { // name|set|artIndex (or CollNr)
|
||||
setPos = isSetCode(info[1]) ? 1 : -1;
|
||||
artPos = isArtIndex(info[2]) ? 2 : -1;
|
||||
cNrPos = isCollectorNumber(info[2]) ? 2 : -1;
|
||||
int pos = cNrPos > 0 ? -1 : 2;
|
||||
clrPos = pos > 0 ? isColorIDString(info[pos]) ? pos : -1 : -1;
|
||||
} else if (info.length == 2) { // name|set (or artIndex, even if not possible via compose)
|
||||
setPos = isSetCode(info[1]) ? 1 : -1;
|
||||
artPos = isArtIndex(info[1]) ? 1 : -1;
|
||||
cNrPos = -1;
|
||||
clrPos = -1;
|
||||
} else {
|
||||
setPos = -1;
|
||||
artPos = -1;
|
||||
cNrPos = -1;
|
||||
clrPos = -1;
|
||||
}
|
||||
String cardName = info[0];
|
||||
boolean isFoil = false;
|
||||
int artIndex = IPaperCard.NO_ART_INDEX;
|
||||
String setCode = null;
|
||||
String collectorNumber = IPaperCard.NO_COLLECTOR_NUMBER;
|
||||
Map<String, String> flags = null;
|
||||
if (isFoilCardName(cardName)) {
|
||||
cardName = cardName.substring(0, cardName.length() - foilSuffix.length());
|
||||
isFoil = true;
|
||||
}
|
||||
|
||||
if(info.length > index && isSetCode(info[index])) {
|
||||
setCode = info[index];
|
||||
index++;
|
||||
}
|
||||
if(info.length > index && isArtIndex(info[index])) {
|
||||
artIndex = Integer.parseInt(info[index]);
|
||||
index++;
|
||||
}
|
||||
if(info.length > index && isCollectorNumber(info[index])) {
|
||||
collectorNumber = info[index].substring(1, info[index].length() - 1);
|
||||
index++;
|
||||
}
|
||||
if (info.length > index && isFlagSegment(info[index])) {
|
||||
String flagText = info[index].substring(FlagPrefix.length());
|
||||
flags = parseRequestFlags(flagText);
|
||||
}
|
||||
|
||||
if (CardEdition.UNKNOWN_CODE.equals(setCode)) { // ???
|
||||
int artIndex = artPos > 0 ? Integer.parseInt(info[artPos]) : IPaperCard.NO_ART_INDEX; // default: no art index
|
||||
String collectorNumber = cNrPos > 0 ? info[cNrPos].substring(1, info[cNrPos].length() - 1) : IPaperCard.NO_COLLECTOR_NUMBER;
|
||||
String setCode = setPos > 0 ? info[setPos] : null;
|
||||
Set<String> colorID = clrPos > 0 ? Arrays.stream(info[clrPos].substring(1).split(colorIDPrefix)).collect(Collectors.toSet()) : null;
|
||||
if (setCode != null && setCode.equals(CardEdition.UNKNOWN.getCode())) { // ???
|
||||
setCode = null;
|
||||
}
|
||||
if (setCode == null) {
|
||||
@@ -269,29 +253,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
// finally, check whether any between artIndex and CollectorNumber has been set
|
||||
if (collectorNumber.equals(IPaperCard.NO_COLLECTOR_NUMBER) && artIndex == IPaperCard.NO_ART_INDEX)
|
||||
artIndex = IPaperCard.DEFAULT_ART_INDEX;
|
||||
return new CardRequest(cardName, setCode, artIndex, isFoil, collectorNumber, flags);
|
||||
}
|
||||
|
||||
private static Map<String, String> parseRequestFlags(String flagText) {
|
||||
flagText = flagText.trim();
|
||||
if(flagText.isEmpty())
|
||||
return null;
|
||||
if(!flagText.startsWith("{")) {
|
||||
//Legacy form for marked colors. They'll be of the form "W#B#R"
|
||||
Map<String, String> flags = new HashMap<>();
|
||||
String normalizedColorString = ColorSet.fromNames(flagText.split(FlagPrefix)).toString();
|
||||
flags.put("markedColors", String.join("", normalizedColorString));
|
||||
return flags;
|
||||
}
|
||||
flagText = flagText.substring(1, flagText.length() - 1); //Trim the braces.
|
||||
//List of flags, a series of "key=value" text broken up by tabs.
|
||||
return Arrays.stream(flagText.split(FlagSeparator))
|
||||
.map(f -> f.split("=", 2))
|
||||
.filter(f -> f.length > 0)
|
||||
.collect(Collectors.toMap(
|
||||
entry -> entry[0],
|
||||
entry -> entry.length > 1 ? entry[1] : "true" //If there's no '=' in the entry, treat it as a boolean flag.
|
||||
));
|
||||
return new CardRequest(cardName, setCode, artIndex, isFoil, collectorNumber, colorID);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -332,27 +294,27 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
}
|
||||
}
|
||||
|
||||
private void addSetCard(CardEdition e, EditionEntry cis, CardRules cr) {
|
||||
private void addSetCard(CardEdition e, CardInSet cis, CardRules cr) {
|
||||
int artIdx = IPaperCard.DEFAULT_ART_INDEX;
|
||||
String key = e.getCode() + "/" + cis.name();
|
||||
String key = e.getCode() + "/" + cis.name;
|
||||
if (artIds.containsKey(key)) {
|
||||
artIdx = artIds.get(key) + 1;
|
||||
}
|
||||
|
||||
artIds.put(key, artIdx);
|
||||
addCard(new PaperCard(cr, e.getCode(), cis.rarity(), artIdx, false, cis.collectorNumber(), cis.artistName(), cis.functionalVariantName()));
|
||||
addCard(new PaperCard(cr, e.getCode(), cis.rarity, artIdx, false, cis.collectorNumber, cis.artistName, cis.functionalVariantName));
|
||||
}
|
||||
|
||||
private boolean addFromSetByName(String cardName, CardEdition ed, CardRules cr) {
|
||||
List<EditionEntry> cardsInSet = ed.getCardInSet(cardName); // empty collection if not present
|
||||
List<CardInSet> cardsInSet = ed.getCardInSet(cardName); // empty collection if not present
|
||||
if (cr.hasFunctionalVariants()) {
|
||||
cardsInSet = cardsInSet.stream().filter(c -> StringUtils.isEmpty(c.functionalVariantName())
|
||||
|| cr.getSupportedFunctionalVariants().contains(c.functionalVariantName())
|
||||
cardsInSet = cardsInSet.stream().filter(c -> StringUtils.isEmpty(c.functionalVariantName)
|
||||
|| cr.getSupportedFunctionalVariants().contains(c.functionalVariantName)
|
||||
).collect(Collectors.toList());
|
||||
}
|
||||
if (cardsInSet.isEmpty())
|
||||
return false;
|
||||
for (EditionEntry cis : cardsInSet) {
|
||||
for (CardInSet cis : cardsInSet) {
|
||||
addSetCard(ed, cis, cr);
|
||||
}
|
||||
return true;
|
||||
@@ -397,15 +359,15 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
upcomingSet = e;
|
||||
}
|
||||
|
||||
for (CardEdition.EditionEntry cis : e.getAllCardsInSet()) {
|
||||
CardRules cr = rulesByName.get(cis.name());
|
||||
for (CardEdition.CardInSet cis : e.getAllCardsInSet()) {
|
||||
CardRules cr = rulesByName.get(cis.name);
|
||||
if (cr == null) {
|
||||
missingCards.add(cis.name());
|
||||
missingCards.add(cis.name);
|
||||
continue;
|
||||
}
|
||||
if (cr.hasFunctionalVariants()) {
|
||||
if (StringUtils.isNotEmpty(cis.functionalVariantName())
|
||||
&& !cr.getSupportedFunctionalVariants().contains(cis.functionalVariantName())) {
|
||||
if (StringUtils.isNotEmpty(cis.functionalVariantName)
|
||||
&& !cr.getSupportedFunctionalVariants().contains(cis.functionalVariantName)) {
|
||||
//Supported card, unsupported variant.
|
||||
//Could note the card as missing but since these are often un-cards,
|
||||
//it's likely absent because it does something out of scope.
|
||||
@@ -444,7 +406,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
addCard(new PaperCard(cr, upcomingSet.getCode(), CardRarity.Unknown));
|
||||
} else if (enableUnknownCards && !this.filtered.contains(cr.getName())) {
|
||||
System.err.println("The card " + cr.getName() + " was not assigned to any set. Adding it to UNKNOWN set... to fix see res/editions/ folder. ");
|
||||
addCard(new PaperCard(cr, CardEdition.UNKNOWN_CODE, CardRarity.Special));
|
||||
addCard(new PaperCard(cr, CardEdition.UNKNOWN.getCode(), CardRarity.Special));
|
||||
}
|
||||
} else {
|
||||
System.err.println("The custom card " + cr.getName() + " was not assigned to any set. Adding it to custom USER set, and will try to load custom art from USER edition.");
|
||||
@@ -463,8 +425,8 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
lang = new LangEnglish();
|
||||
}
|
||||
// for now just check Universes Within
|
||||
for (EditionEntry cis : editions.get("SLX").getCards()) {
|
||||
String orgName = alternateName.get(cis.name());
|
||||
for (CardInSet cis : editions.get("SLX").getCards()) {
|
||||
String orgName = alternateName.get(cis.name);
|
||||
if (orgName != null) {
|
||||
// found original (beyond) print
|
||||
CardRules org = getRules(orgName);
|
||||
@@ -494,7 +456,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
CardRules within = new CardRules(new ICardFace[] { renamedMain, renamedOther, null, null, null, null, null }, org.getSplitType(), org.getAiHints());
|
||||
// so workshop can edit same script
|
||||
within.setNormalizedName(org.getNormalizedName());
|
||||
rulesByName.put(cis.name(), within);
|
||||
rulesByName.put(cis.name, within);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -630,15 +592,15 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
}
|
||||
|
||||
@Override
|
||||
public PaperCard getCard(final String cardName, String setCode, int artIndex, Map<String, String> flags) {
|
||||
String reqInfo = CardRequest.compose(cardName, setCode, artIndex, flags);
|
||||
public PaperCard getCard(final String cardName, String setCode, int artIndex, String collectorNumber) {
|
||||
String reqInfo = CardRequest.compose(cardName, setCode, artIndex, collectorNumber);
|
||||
CardRequest request = CardRequest.fromString(reqInfo);
|
||||
return tryGetCard(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PaperCard getCard(final String cardName, String setCode, String collectorNumber, Map<String, String> flags) {
|
||||
String reqInfo = CardRequest.compose(cardName, setCode, collectorNumber, flags);
|
||||
public PaperCard getCard(final String cardName, String setCode, int artIndex, Set<String> colorID) {
|
||||
String reqInfo = CardRequest.compose(cardName, setCode, artIndex, colorID);
|
||||
CardRequest request = CardRequest.fromString(reqInfo);
|
||||
return tryGetCard(request);
|
||||
}
|
||||
@@ -649,17 +611,14 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
return null;
|
||||
// 1. First off, try using all possible search parameters, to narrow down the actual cards looked for.
|
||||
String reqEditionCode = request.edition;
|
||||
if (reqEditionCode != null && !reqEditionCode.isEmpty()) {
|
||||
if (reqEditionCode != null && reqEditionCode.length() > 0) {
|
||||
// This get is robust even against expansion aliases (e.g. TE and TMP both valid for Tempest) -
|
||||
// MOST of the extensions have two short codes, 141 out of 221 (so far)
|
||||
// ALSO: Set Code are always UpperCase
|
||||
CardEdition edition = editions.get(reqEditionCode.toUpperCase());
|
||||
|
||||
PaperCard cardFromSet = this.getCardFromSet(request.cardName, edition, request.artIndex, request.collectorNumber, request.isFoil);
|
||||
if(cardFromSet != null && request.flags != null)
|
||||
cardFromSet = cardFromSet.copyWithFlags(request.flags);
|
||||
|
||||
return cardFromSet;
|
||||
return this.getCardFromSet(request.cardName, edition, request.artIndex,
|
||||
request.collectorNumber, request.isFoil, request.colorID);
|
||||
}
|
||||
|
||||
// 2. Card lookup in edition with specified filter didn't work.
|
||||
@@ -702,6 +661,11 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
|
||||
@Override
|
||||
public PaperCard getCardFromSet(String cardName, CardEdition edition, int artIndex, String collectorNumber, boolean isFoil) {
|
||||
return getCardFromSet(cardName, edition, artIndex, collectorNumber, isFoil, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PaperCard getCardFromSet(String cardName, CardEdition edition, int artIndex, String collectorNumber, boolean isFoil, Set<String> colorID) {
|
||||
if (edition == null || cardName == null) // preview cards
|
||||
return null; // No cards will be returned
|
||||
|
||||
@@ -710,18 +674,18 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
cardName = cardNameRequest.cardName;
|
||||
isFoil = isFoil || cardNameRequest.isFoil;
|
||||
|
||||
String code1 = edition.getCode(), code2 = edition.getCode2();
|
||||
|
||||
Predicate<PaperCard> filter = (c) -> {
|
||||
String ed = c.getEdition();
|
||||
return ed.equalsIgnoreCase(code1) || ed.equalsIgnoreCase(code2);
|
||||
};
|
||||
List<PaperCard> candidates = getAllCards(cardName, c -> {
|
||||
boolean artIndexFilter = true;
|
||||
boolean collectorNumberFilter = true;
|
||||
boolean setFilter = c.getEdition().equalsIgnoreCase(edition.getCode()) ||
|
||||
c.getEdition().equalsIgnoreCase(edition.getCode2());
|
||||
if (artIndex > 0)
|
||||
filter = filter.and((c) -> artIndex == c.getArtIndex());
|
||||
if (collectorNumber != null && !collectorNumber.isEmpty() && !collectorNumber.equals(IPaperCard.NO_COLLECTOR_NUMBER))
|
||||
filter = filter.and((c) -> collectorNumber.equals(c.getCollectorNumber()));
|
||||
|
||||
List<PaperCard> candidates = getAllCards(cardName, filter);
|
||||
artIndexFilter = (c.getArtIndex() == artIndex);
|
||||
if ((collectorNumber != null) && (collectorNumber.length() > 0)
|
||||
&& !(collectorNumber.equals(IPaperCard.NO_COLLECTOR_NUMBER)))
|
||||
collectorNumberFilter = (c.getCollectorNumber().equals(collectorNumber));
|
||||
return setFilter && artIndexFilter && collectorNumberFilter;
|
||||
});
|
||||
if (candidates.isEmpty())
|
||||
return null;
|
||||
|
||||
@@ -735,7 +699,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
while (!candidate.hasImage() && candidatesIterator.hasNext())
|
||||
candidate = candidatesIterator.next();
|
||||
candidate = candidate.hasImage() ? candidate : firstCandidate;
|
||||
return isFoil ? candidate.getFoiled() : candidate;
|
||||
return isFoil ? candidate.getFoiled().getColorIDVersion(colorID) : candidate.getColorIDVersion(colorID);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -778,6 +742,11 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
return this.tryToGetCardFromEditions(cardInfo, artPreference, artIndex, filter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PaperCard getCardFromEditions(final String cardInfo, final CardArtPreference artPreference, int artIndex, Set<String> colorID) {
|
||||
return this.tryToGetCardFromEditions(cardInfo, artPreference, artIndex, null, false, null, colorID);
|
||||
}
|
||||
|
||||
/*
|
||||
* ===============================================
|
||||
* 4. SPECIALISED CARD LOOKUP BASED ON
|
||||
@@ -851,7 +820,12 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
}
|
||||
|
||||
private PaperCard tryToGetCardFromEditions(String cardInfo, CardArtPreference artPreference, int artIndex,
|
||||
Date releaseDate, boolean releasedBeforeFlag, Predicate<PaperCard> filter) {
|
||||
Date releaseDate, boolean releasedBeforeFlag, Predicate<PaperCard> filter){
|
||||
return this.tryToGetCardFromEditions(cardInfo, artPreference, artIndex, releaseDate, releasedBeforeFlag, filter, null);
|
||||
}
|
||||
|
||||
private PaperCard tryToGetCardFromEditions(String cardInfo, CardArtPreference artPreference, int artIndex,
|
||||
Date releaseDate, boolean releasedBeforeFlag, Predicate<PaperCard> filter, Set<String> colorID){
|
||||
if (cardInfo == null)
|
||||
return null;
|
||||
final CardRequest cr = CardRequest.fromString(cardInfo);
|
||||
@@ -891,7 +865,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
for (PaperCard card : cards) {
|
||||
String setCode = card.getEdition();
|
||||
CardEdition ed;
|
||||
if (setCode.equals(CardEdition.UNKNOWN_CODE))
|
||||
if (setCode.equals(CardEdition.UNKNOWN.getCode()))
|
||||
ed = CardEdition.UNKNOWN;
|
||||
else
|
||||
ed = editions.get(card.getEdition());
|
||||
@@ -932,7 +906,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
}
|
||||
candidate = candidate.hasImage() ? candidate : firstCandidate;
|
||||
//If any, we're sure that at least one candidate is always returned despite it having any image
|
||||
return cr.isFoil ? candidate.getFoiled() : candidate;
|
||||
return cr.isFoil ? candidate.getFoiled().getColorIDVersion(colorID) : candidate.getColorIDVersion(colorID);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1043,7 +1017,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
public static final Predicate<PaperCard> EDITION_NON_PROMO = paperCard -> {
|
||||
String code = paperCard.getEdition();
|
||||
CardEdition edition = StaticData.instance().getCardEdition(code);
|
||||
if(edition == null && code.equals(CardEdition.UNKNOWN_CODE))
|
||||
if(edition == null && code.equals("???"))
|
||||
return true;
|
||||
return edition != null && edition.getType() != Type.PROMO;
|
||||
};
|
||||
@@ -1051,7 +1025,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
public static final Predicate<PaperCard> EDITION_NON_REPRINT = paperCard -> {
|
||||
String code = paperCard.getEdition();
|
||||
CardEdition edition = StaticData.instance().getCardEdition(code);
|
||||
if(edition == null && code.equals(CardEdition.UNKNOWN_CODE))
|
||||
if(edition == null && code.equals("???"))
|
||||
return true;
|
||||
return edition != null && Type.REPRINT_SET_TYPES.contains(edition.getType());
|
||||
};
|
||||
@@ -1107,8 +1081,8 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
public Collection<PaperCard> getAllCards(CardEdition edition) {
|
||||
List<PaperCard> cards = Lists.newArrayList();
|
||||
|
||||
for (EditionEntry cis : edition.getAllCardsInSet()) {
|
||||
PaperCard card = this.getCard(cis.name(), edition.getCode());
|
||||
for (CardInSet cis : edition.getAllCardsInSet()) {
|
||||
PaperCard card = this.getCard(cis.name, edition.getCode());
|
||||
if (card == null) {
|
||||
// Just in case the card is listed in the edition file but Forge doesn't support it
|
||||
continue;
|
||||
@@ -1152,6 +1126,29 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
.anyMatch(rarity::equals);
|
||||
}
|
||||
|
||||
public StringBuilder appendCardToStringBuilder(PaperCard card, StringBuilder sb) {
|
||||
final boolean hasBadSetInfo = card.getEdition().equals(CardEdition.UNKNOWN.getCode()) || StringUtils.isBlank(card.getEdition());
|
||||
sb.append(card.getName());
|
||||
if (card.isFoil()) {
|
||||
sb.append(CardDb.foilSuffix);
|
||||
}
|
||||
|
||||
if (!hasBadSetInfo) {
|
||||
int artCount = getArtCount(card.getName(), card.getEdition(), card.getFunctionalVariant());
|
||||
sb.append(CardDb.NameSetSeparator).append(card.getEdition());
|
||||
if (artCount >= IPaperCard.DEFAULT_ART_INDEX) {
|
||||
sb.append(CardDb.NameSetSeparator).append(card.getArtIndex()); // indexes start at 1 to match image file name conventions
|
||||
}
|
||||
if (card.getColorID() != null) {
|
||||
sb.append(CardDb.NameSetSeparator);
|
||||
for (String color : card.getColorID())
|
||||
sb.append(CardDb.colorIDPrefix).append(color);
|
||||
}
|
||||
}
|
||||
|
||||
return sb;
|
||||
}
|
||||
|
||||
public PaperCard createUnsupportedCard(String cardRequest) {
|
||||
CardRequest request = CardRequest.fromString(cardRequest);
|
||||
CardEdition cardEdition = CardEdition.UNKNOWN;
|
||||
@@ -1160,10 +1157,10 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
// May iterate over editions and find out if there is any card named 'cardRequest' but not implemented with Forge script.
|
||||
if (StringUtils.isBlank(request.edition)) {
|
||||
for (CardEdition edition : editions) {
|
||||
for (EditionEntry cardInSet : edition.getAllCardsInSet()) {
|
||||
if (cardInSet.name().equals(request.cardName)) {
|
||||
for (CardInSet cardInSet : edition.getAllCardsInSet()) {
|
||||
if (cardInSet.name.equals(request.cardName)) {
|
||||
cardEdition = edition;
|
||||
cardRarity = cardInSet.rarity();
|
||||
cardRarity = cardInSet.rarity;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1174,9 +1171,9 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
} else {
|
||||
cardEdition = editions.get(request.edition);
|
||||
if (cardEdition != null) {
|
||||
for (EditionEntry cardInSet : cardEdition.getAllCardsInSet()) {
|
||||
if (cardInSet.name().equals(request.cardName)) {
|
||||
cardRarity = cardInSet.rarity();
|
||||
for (CardInSet cardInSet : cardEdition.getAllCardsInSet()) {
|
||||
if (cardInSet.name.equals(request.cardName)) {
|
||||
cardRarity = cardInSet.rarity;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1227,9 +1224,9 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
// @leriomaggio: DONE! re-using here the same strategy implemented for lazy-loading!
|
||||
for (CardEdition e : editions.getOrderedEditions()) {
|
||||
int artIdx = IPaperCard.DEFAULT_ART_INDEX;
|
||||
for (EditionEntry cis : e.getCardInSet(cardName))
|
||||
paperCards.add(new PaperCard(rules, e.getCode(), cis.rarity(), artIdx++, false,
|
||||
cis.collectorNumber(), cis.artistName(), cis.functionalVariantName()));
|
||||
for (CardInSet cis : e.getCardInSet(cardName))
|
||||
paperCards.add(new PaperCard(rules, e.getCode(), cis.rarity, artIdx++, false,
|
||||
cis.collectorNumber, cis.artistName, cis.functionalVariantName));
|
||||
}
|
||||
} else {
|
||||
String lastEdition = null;
|
||||
@@ -1243,17 +1240,17 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
if (ed == null) {
|
||||
continue;
|
||||
}
|
||||
List<EditionEntry> cardsInSet = ed.getCardInSet(cardName);
|
||||
List<CardInSet> cardsInSet = ed.getCardInSet(cardName);
|
||||
if (cardsInSet.isEmpty())
|
||||
continue;
|
||||
int cardInSetIndex = Math.max(artIdx-1, 0); // make sure doesn't go below zero
|
||||
EditionEntry cds = cardsInSet.get(cardInSetIndex); // use ArtIndex to get the right Coll. Number
|
||||
CardInSet cds = cardsInSet.get(cardInSetIndex); // use ArtIndex to get the right Coll. Number
|
||||
paperCards.add(new PaperCard(rules, lastEdition, tuple.getValue(), artIdx++, false,
|
||||
cds.collectorNumber(), cds.artistName(), cds.functionalVariantName()));
|
||||
cds.collectorNumber, cds.artistName, cds.functionalVariantName));
|
||||
}
|
||||
}
|
||||
if (paperCards.isEmpty()) {
|
||||
paperCards.add(new PaperCard(rules, CardEdition.UNKNOWN_CODE, CardRarity.Special));
|
||||
paperCards.add(new PaperCard(rules, CardEdition.UNKNOWN.getCode(), CardRarity.Special));
|
||||
}
|
||||
// 2. add them to db
|
||||
for (PaperCard paperCard : paperCards) {
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
package forge.card;
|
||||
|
||||
import com.google.common.collect.*;
|
||||
|
||||
import forge.StaticData;
|
||||
import forge.card.CardDb.CardArtPreference;
|
||||
import forge.deck.CardPool;
|
||||
@@ -166,6 +165,43 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
}
|
||||
}
|
||||
|
||||
public static class CardInSet implements Comparable<CardInSet> {
|
||||
public final CardRarity rarity;
|
||||
public final String collectorNumber;
|
||||
public final String name;
|
||||
public final String artistName;
|
||||
public final String functionalVariantName;
|
||||
|
||||
public CardInSet(final String name, final String collectorNumber, final CardRarity rarity, final String artistName, final String functionalVariantName) {
|
||||
this.name = name;
|
||||
this.collectorNumber = collectorNumber;
|
||||
this.rarity = rarity;
|
||||
this.artistName = artistName;
|
||||
this.functionalVariantName = functionalVariantName;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (collectorNumber != null) {
|
||||
sb.append(collectorNumber);
|
||||
sb.append(' ');
|
||||
}
|
||||
if (rarity != CardRarity.Unknown) {
|
||||
sb.append(rarity);
|
||||
sb.append(' ');
|
||||
}
|
||||
sb.append(name);
|
||||
if (artistName != null) {
|
||||
sb.append(" @");
|
||||
sb.append(artistName);
|
||||
}
|
||||
if (functionalVariantName != null) {
|
||||
sb.append(" $");
|
||||
sb.append(functionalVariantName);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static final Map<String, String> sortableCollNumberLookup = new HashMap<>();
|
||||
/**
|
||||
* This method implements the main strategy to allow for natural ordering of collectorNumber
|
||||
@@ -208,32 +244,8 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
return sortableCollNr;
|
||||
}
|
||||
|
||||
public record EditionEntry(String name, String collectorNumber, CardRarity rarity, String artistName, String functionalVariantName) implements Comparable<EditionEntry> {
|
||||
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (collectorNumber != null) {
|
||||
sb.append(collectorNumber);
|
||||
sb.append(' ');
|
||||
}
|
||||
if (rarity != CardRarity.Unknown && rarity != CardRarity.Token) {
|
||||
sb.append(rarity);
|
||||
sb.append(' ');
|
||||
}
|
||||
sb.append(name);
|
||||
if (artistName != null) {
|
||||
sb.append(" @");
|
||||
sb.append(artistName);
|
||||
}
|
||||
if (functionalVariantName != null) {
|
||||
sb.append(" $");
|
||||
sb.append(functionalVariantName);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(EditionEntry o) {
|
||||
public int compareTo(CardInSet o) {
|
||||
final int nameCmp = name.compareToIgnoreCase(o.name);
|
||||
if (0 != nameCmp) {
|
||||
return nameCmp;
|
||||
@@ -250,17 +262,11 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
|
||||
private final static SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
|
||||
|
||||
/**
|
||||
* Equivalent to the set code of CardEdition.UNKNOWN
|
||||
*/
|
||||
public static final String UNKNOWN_CODE = "???";
|
||||
public static final CardEdition UNKNOWN = new CardEdition("1990-01-01", UNKNOWN_CODE, "??", Type.UNKNOWN, "Undefined", FoilType.NOT_SUPPORTED, new EditionEntry[]{});
|
||||
public static final CardEdition UNKNOWN = new CardEdition("1990-01-01", "???", "??", Type.UNKNOWN, "Undefined", FoilType.NOT_SUPPORTED, new CardInSet[]{});
|
||||
private Date date;
|
||||
private String code;
|
||||
private String code2;
|
||||
private String scryfallCode;
|
||||
private String tokensCode;
|
||||
private String tokenFallbackCode;
|
||||
private String cardsLanguage;
|
||||
private Type type;
|
||||
private String name;
|
||||
@@ -290,32 +296,31 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
private String doublePickDuringDraft = "";
|
||||
private String[] chaosDraftThemes = new String[0];
|
||||
|
||||
private final ListMultimap<String, EditionEntry> cardMap;
|
||||
private final List<EditionEntry> cardsInSet;
|
||||
private final ListMultimap<String, EditionEntry> tokenMap;
|
||||
private final ListMultimap<String, CardInSet> cardMap;
|
||||
private final List<CardInSet> cardsInSet;
|
||||
private final Map<String, Integer> tokenNormalized;
|
||||
// custom print sheets that will be loaded lazily
|
||||
private final Map<String, List<String>> customPrintSheetsToParse;
|
||||
private ListMultimap<String, EditionEntry> otherMap = ArrayListMultimap.create();
|
||||
|
||||
private int boosterArts = 1;
|
||||
private SealedTemplate boosterTpl = null;
|
||||
private final Map<String, SealedTemplate> boosterTemplates = new HashMap<>();
|
||||
|
||||
private CardEdition(ListMultimap<String, EditionEntry> cardMap, ListMultimap<String, EditionEntry> tokens, Map<String, List<String>> customPrintSheetsToParse) {
|
||||
private CardEdition(ListMultimap<String, CardInSet> cardMap, Map<String, Integer> tokens, Map<String, List<String>> customPrintSheetsToParse) {
|
||||
this.cardMap = cardMap;
|
||||
this.cardsInSet = new ArrayList<>(cardMap.values());
|
||||
Collections.sort(cardsInSet);
|
||||
this.tokenMap = tokens;
|
||||
this.tokenNormalized = tokens;
|
||||
this.customPrintSheetsToParse = customPrintSheetsToParse;
|
||||
}
|
||||
|
||||
private CardEdition(EditionEntry[] cards, ListMultimap<String, EditionEntry> tokens) {
|
||||
List<EditionEntry> cardsList = Arrays.asList(cards);
|
||||
private CardEdition(CardInSet[] cards, Map<String, Integer> tokens) {
|
||||
List<CardInSet> cardsList = Arrays.asList(cards);
|
||||
this.cardMap = ArrayListMultimap.create();
|
||||
this.cardMap.replaceValues("cards", cardsList);
|
||||
this.cardsInSet = new ArrayList<>(cardsList);
|
||||
Collections.sort(cardsInSet);
|
||||
this.tokenMap = tokens;
|
||||
this.tokenNormalized = tokens;
|
||||
this.customPrintSheetsToParse = new HashMap<>();
|
||||
}
|
||||
|
||||
@@ -332,8 +337,8 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
* @param name the name of the set
|
||||
* @param cards the cards in the set
|
||||
*/
|
||||
private CardEdition(String date, String code, String code2, Type type, String name, FoilType foil, EditionEntry[] cards) {
|
||||
this(cards, ArrayListMultimap.create());
|
||||
private CardEdition(String date, String code, String code2, Type type, String name, FoilType foil, CardInSet[] cards) {
|
||||
this(cards, new HashMap<>());
|
||||
this.code = code;
|
||||
this.code2 = code2;
|
||||
this.type = type;
|
||||
@@ -356,7 +361,6 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
public String getCode() { return code; }
|
||||
public String getCode2() { return code2; }
|
||||
public String getScryfallCode() { return scryfallCode.toLowerCase(); }
|
||||
public String getTokensCode() { return tokensCode.toLowerCase(); }
|
||||
public String getCardsLangCode() { return cardsLanguage.toLowerCase(); }
|
||||
public Type getType() { return type; }
|
||||
public String getName() { return name; }
|
||||
@@ -381,14 +385,14 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
public String getSheetReplaceCardFromSheet2() { return sheetReplaceCardFromSheet2; }
|
||||
public String[] getChaosDraftThemes() { return chaosDraftThemes; }
|
||||
|
||||
public List<EditionEntry> getCards() { return cardMap.get(EditionSectionWithCollectorNumbers.CARDS.getName()); }
|
||||
public List<EditionEntry> getRebalancedCards() { return cardMap.get(EditionSectionWithCollectorNumbers.REBALANCED.getName()); }
|
||||
public List<EditionEntry> getFunnyEternalCards() { return cardMap.get(EditionSectionWithCollectorNumbers.ETERNAL.getName()); }
|
||||
public List<EditionEntry> getAllCardsInSet() {
|
||||
public List<CardInSet> getCards() { return cardMap.get(EditionSectionWithCollectorNumbers.CARDS.getName()); }
|
||||
public List<CardInSet> getRebalancedCards() { return cardMap.get(EditionSectionWithCollectorNumbers.REBALANCED.getName()); }
|
||||
public List<CardInSet> getFunnyEternalCards() { return cardMap.get(EditionSectionWithCollectorNumbers.ETERNAL.getName()); }
|
||||
public List<CardInSet> getAllCardsInSet() {
|
||||
return cardsInSet;
|
||||
}
|
||||
|
||||
private ListMultimap<String, EditionEntry> cardsInSetLookupMap = null;
|
||||
private ListMultimap<String, CardInSet> cardsInSetLookupMap = null;
|
||||
|
||||
/**
|
||||
* Get all the CardInSet instances with the input card name.
|
||||
@@ -396,12 +400,12 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
* @return A List of all the CardInSet instances for a given name.
|
||||
* If not fount, an Empty sequence (view) will be returned instead!
|
||||
*/
|
||||
public List<EditionEntry> getCardInSet(String cardName){
|
||||
public List<CardInSet> getCardInSet(String cardName){
|
||||
if (cardsInSetLookupMap == null) {
|
||||
// initialise
|
||||
cardsInSetLookupMap = Multimaps.newListMultimap(new TreeMap<>(String.CASE_INSENSITIVE_ORDER), Lists::newArrayList);
|
||||
List<EditionEntry> cardsInSet = this.getAllCardsInSet();
|
||||
for (EditionEntry cis : cardsInSet){
|
||||
List<CardInSet> cardsInSet = this.getAllCardsInSet();
|
||||
for (CardInSet cis : cardsInSet){
|
||||
String key = cis.name;
|
||||
cardsInSetLookupMap.put(key, cis);
|
||||
}
|
||||
@@ -409,19 +413,8 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
return this.cardsInSetLookupMap.get(cardName);
|
||||
}
|
||||
|
||||
public EditionEntry getCardFromCollectorNumber(String collectorNumber) {
|
||||
if(collectorNumber == null || collectorNumber.isEmpty())
|
||||
return null;
|
||||
for(EditionEntry c : this.cardsInSet) {
|
||||
//Could build a map for this one too if it's used for more than one-offs.
|
||||
if (c.collectorNumber.equalsIgnoreCase(collectorNumber))
|
||||
return c;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean isRebalanced(String cardName) {
|
||||
for (EditionEntry cis : getRebalancedCards()) {
|
||||
for (CardInSet cis : getRebalancedCards()) {
|
||||
if (cis.name.equals(cardName)) {
|
||||
return true;
|
||||
}
|
||||
@@ -431,33 +424,7 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
|
||||
public boolean isModern() { return getDate().after(parseDate("2003-07-27")); } //8ED and above are modern except some promo cards and others
|
||||
|
||||
public Multimap<String, EditionEntry> getTokens() { return tokenMap; }
|
||||
|
||||
public String getTokenSet(String token) {
|
||||
if (tokenMap.containsKey(token)) {
|
||||
return this.getCode();
|
||||
}
|
||||
if (this.tokenFallbackCode != null) {
|
||||
return StaticData.instance().getCardEdition(this.tokenFallbackCode).getTokenSet(token);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public String getOtherSet(String token) {
|
||||
if (otherMap.containsKey(token)) {
|
||||
return this.getCode();
|
||||
}
|
||||
if (this.tokenFallbackCode != null) {
|
||||
return StaticData.instance().getCardEdition(this.tokenFallbackCode).getOtherSet(token);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public EditionEntry findOther(String name) {
|
||||
if (otherMap.containsKey(name)) {
|
||||
return Aggregates.random(otherMap.get(name));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public Map<String, Integer> getTokens() { return tokenNormalized; }
|
||||
|
||||
@Override
|
||||
public int compareTo(final CardEdition o) {
|
||||
@@ -541,8 +508,8 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
for (String sectionName : cardMap.keySet()) {
|
||||
PrintSheet sheet = new PrintSheet(String.format("%s %s", this.getCode(), sectionName));
|
||||
|
||||
List<EditionEntry> cards = cardMap.get(sectionName);
|
||||
for (EditionEntry card : cards) {
|
||||
List<CardInSet> cards = cardMap.get(sectionName);
|
||||
for (CardInSet card : cards) {
|
||||
int index = 1;
|
||||
if (cardToIndex.containsKey(card.name)) {
|
||||
index = cardToIndex.get(card.name) + 1;
|
||||
@@ -595,7 +562,6 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
it should also match the Un-set and older alternate art cards
|
||||
like Merseine from FEM.
|
||||
*/
|
||||
// Collector numbers now should allow hyphens for Planeswalker Championship Promos
|
||||
//"(^(?<cnum>[0-9]+.?) )?((?<rarity>[SCURML]) )?(?<name>.*)$"
|
||||
/* Ideally we'd use the named group above, but Android 6 and
|
||||
earlier don't appear to support named groups.
|
||||
@@ -609,20 +575,12 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
* functional variant name - grouping #9
|
||||
*/
|
||||
// "(^(.?[0-9A-Z]+.?))?(([SCURML]) )?(.*)$"
|
||||
"(^(.?[0-9A-Z-]+\\S?[A-Z]*)\\s)?(([SCURML])\\s)?([^@\\$]*)( @([^\\$]*))?( \\$(.+))?$"
|
||||
"(^(.?[0-9A-Z]+\\S?[A-Z]*)\\s)?(([SCURML])\\s)?([^@\\$]*)( @([^\\$]*))?( \\$(.+))?$"
|
||||
);
|
||||
|
||||
final Pattern tokenPattern = Pattern.compile(
|
||||
/*
|
||||
* cnum - grouping #2
|
||||
* name - grouping #3
|
||||
* artist name - grouping #5
|
||||
*/
|
||||
"(^(.?[0-9A-Z]+\\S?[A-Z]*)\\s)?([^@]*)( @(.*))?$"
|
||||
);
|
||||
|
||||
ListMultimap<String, EditionEntry> cardMap = ArrayListMultimap.create();
|
||||
ListMultimap<String, CardInSet> cardMap = ArrayListMultimap.create();
|
||||
List<BoosterSlot> boosterSlots = null;
|
||||
Map<String, Integer> tokenNormalized = new HashMap<>();
|
||||
Map<String, List<String>> customPrintSheetsToParse = new HashMap<>();
|
||||
List<String> editionSectionsWithCollectorNumbers = EditionSectionWithCollectorNumbers.getNames();
|
||||
|
||||
@@ -653,7 +611,7 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
String cardName = matcher.group(5);
|
||||
String artistName = matcher.group(7);
|
||||
String functionalVariantName = matcher.group(9);
|
||||
EditionEntry cis = new EditionEntry(cardName, collectorNumber, r, artistName, functionalVariantName);
|
||||
CardInSet cis = new CardInSet(cardName, collectorNumber, r, artistName, functionalVariantName);
|
||||
|
||||
cardMap.put(sectionName, cis);
|
||||
}
|
||||
@@ -667,58 +625,40 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
}
|
||||
}
|
||||
|
||||
ListMultimap<String, EditionEntry> tokenMap = ArrayListMultimap.create();
|
||||
ListMultimap<String, EditionEntry> otherMap = ArrayListMultimap.create();
|
||||
// parse tokens section
|
||||
if (contents.containsKey("tokens")) {
|
||||
for (String line : contents.get("tokens")) {
|
||||
if (StringUtils.isBlank(line))
|
||||
continue;
|
||||
Matcher matcher = tokenPattern.matcher(line);
|
||||
|
||||
if (!matcher.matches()) {
|
||||
continue;
|
||||
if (!tokenNormalized.containsKey(line)) {
|
||||
tokenNormalized.put(line, 1);
|
||||
} else {
|
||||
tokenNormalized.put(line, tokenNormalized.get(line) + 1);
|
||||
}
|
||||
|
||||
String collectorNumber = matcher.group(2);
|
||||
String cardName = matcher.group(3);
|
||||
String artistName = matcher.group(5);
|
||||
// rarity isn't used for this anyway
|
||||
EditionEntry tis = new EditionEntry(cardName, collectorNumber, CardRarity.Token, artistName, null);
|
||||
tokenMap.put(cardName, tis);
|
||||
}
|
||||
}
|
||||
if (contents.containsKey("other")) {
|
||||
for (String line : contents.get("other")) {
|
||||
if (StringUtils.isBlank(line))
|
||||
continue;
|
||||
Matcher matcher = tokenPattern.matcher(line);
|
||||
|
||||
if (!matcher.matches()) {
|
||||
continue;
|
||||
}
|
||||
String collectorNumber = matcher.group(2);
|
||||
String cardName = matcher.group(3);
|
||||
String artistName = matcher.group(5);
|
||||
EditionEntry tis = new EditionEntry(cardName, collectorNumber, CardRarity.Unknown, artistName, null);
|
||||
otherMap.put(cardName, tis);
|
||||
}
|
||||
}
|
||||
|
||||
CardEdition res = new CardEdition(cardMap, tokenMap, customPrintSheetsToParse);
|
||||
CardEdition res = new CardEdition(cardMap, tokenNormalized, customPrintSheetsToParse);
|
||||
res.boosterSlots = boosterSlots;
|
||||
// parse metadata section
|
||||
res.name = metadata.get("name");
|
||||
res.date = parseDate(metadata.get("date"));
|
||||
res.code = metadata.get("code");
|
||||
res.code2 = metadata.get("code2", res.code);
|
||||
res.scryfallCode = metadata.get("ScryfallCode", res.code);
|
||||
res.tokensCode = metadata.get("TokensCode", "T" + res.scryfallCode);
|
||||
res.tokenFallbackCode = metadata.get("TokenFallbackCode");
|
||||
res.cardsLanguage = metadata.get("CardLang", "en");
|
||||
res.boosterArts = metadata.getInt("BoosterCovers", 1);
|
||||
res.code2 = metadata.get("code2");
|
||||
if (res.code2 == null) {
|
||||
res.code2 = res.code;
|
||||
}
|
||||
res.scryfallCode = metadata.get("ScryfallCode");
|
||||
if (res.scryfallCode == null) {
|
||||
res.scryfallCode = res.code;
|
||||
}
|
||||
res.cardsLanguage = metadata.get("CardLang");
|
||||
if (res.cardsLanguage == null) {
|
||||
res.cardsLanguage = "en";
|
||||
}
|
||||
|
||||
res.otherMap = otherMap;
|
||||
res.boosterArts = metadata.getInt("BoosterCovers", 1);
|
||||
|
||||
String boosterDesc = metadata.get("Booster");
|
||||
|
||||
@@ -838,7 +778,7 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
initAliases(E); //Made a method in case the system changes, so it's consistent.
|
||||
}
|
||||
CardEdition customBucket = new CardEdition("2990-01-01", "USER", "USER",
|
||||
Type.CUSTOM_SET, "USER", FoilType.NOT_SUPPORTED, new EditionEntry[]{});
|
||||
Type.CUSTOM_SET, "USER", FoilType.NOT_SUPPORTED, new CardInSet[]{});
|
||||
this.add(customBucket);
|
||||
initAliases(customBucket);
|
||||
this.lock = true; //Consider it initialized and prevent from writing any more data.
|
||||
@@ -870,7 +810,7 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
|
||||
public CardEdition getEditionByCodeOrThrow(final String code) {
|
||||
final CardEdition set = this.get(code);
|
||||
if (null == set && code.equals(UNKNOWN_CODE)) //Hardcoded set ??? is not with the others, needs special check.
|
||||
if (null == set && code.equals("???")) //Hardcoded set ??? is not with the others, needs special check.
|
||||
return UNKNOWN;
|
||||
if (null == set) {
|
||||
throw new RuntimeException("Edition with code '" + code + "' not found");
|
||||
@@ -1001,12 +941,4 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public boolean hasBasicLands() {
|
||||
for(String landName : MagicColor.Constant.BASIC_LANDS) {
|
||||
if (null == StaticData.instance().getCommonCards().getCard(landName, this.getCode(), 0))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,7 +222,7 @@ final class CardFace implements ICardFace, Cloneable {
|
||||
else variant.replacements.addAll(0, this.replacements);
|
||||
|
||||
if(variant.variables == null) variant.variables = this.variables;
|
||||
else this.variables.forEach((k, v) -> variant.variables.putIfAbsent(k, v));
|
||||
else variant.variables.putAll(this.variables);
|
||||
|
||||
if(variant.nonAbilityText == null) variant.nonAbilityText = this.nonAbilityText;
|
||||
if(variant.draftActions == null) variant.draftActions = this.draftActions;
|
||||
|
||||
@@ -108,13 +108,9 @@ public final class CardRules implements ICardCharacteristics {
|
||||
// CR 903.4 colors defined by its characteristic-defining abilities
|
||||
for (String staticAbility : face.getStaticAbilities()) {
|
||||
if (staticAbility.contains("CharacteristicDefining$ True") && staticAbility.contains("SetColor$ All")) {
|
||||
return MagicColor.ALL_COLORS;
|
||||
res |= MagicColor.ALL_COLORS;
|
||||
}
|
||||
}
|
||||
// no need to check oracle if it is already all colors
|
||||
if (res == MagicColor.ALL_COLORS) {
|
||||
return res;
|
||||
}
|
||||
int len = oracleText.length();
|
||||
for (int i = 0; i < len; i++) {
|
||||
char c = oracleText.charAt(i); // This is to avoid needless allocations performed by toCharArray()
|
||||
@@ -285,10 +281,14 @@ public final class CardRules implements ICardCharacteristics {
|
||||
return true;
|
||||
}
|
||||
CardType type = mainPart.getType();
|
||||
if (!type.isLegendary()) {
|
||||
return false;
|
||||
boolean creature = type.isCreature();
|
||||
for (String staticAbility : mainPart.getStaticAbilities()) { // Check for Grist
|
||||
if (staticAbility.contains("CharacteristicDefining$ True") && staticAbility.contains("AddType$ Creature")) {
|
||||
creature = true;
|
||||
break;
|
||||
}
|
||||
if (canBeCreature() || type.isVehicle() || type.isSpacecraft()) {
|
||||
}
|
||||
if (type.isLegendary() && creature) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -356,9 +356,16 @@ public final class CardRules implements ICardCharacteristics {
|
||||
if (!type.isLegendary()) {
|
||||
return false;
|
||||
}
|
||||
if (canBeCreature() || type.isPlaneswalker()) {
|
||||
if (type.isCreature() || type.isPlaneswalker()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Grist is checked above, but new cards might work this way
|
||||
for (String staticAbility : mainPart.getStaticAbilities()) {
|
||||
if (staticAbility.contains("CharacteristicDefining$ True") && staticAbility.contains("AddType$ Creature")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -367,18 +374,12 @@ public final class CardRules implements ICardCharacteristics {
|
||||
if (!type.isLegendary()) {
|
||||
return false;
|
||||
}
|
||||
if (canBeCreature() || type.isPlaneswalker()) {
|
||||
if (type.isCreature() || type.isPlaneswalker()) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean canBeCreature() {
|
||||
CardType type = mainPart.getType();
|
||||
if (type.isCreature()) {
|
||||
return true;
|
||||
}
|
||||
for (String staticAbility : mainPart.getStaticAbilities()) { // Check for Grist
|
||||
// Grist is checked above, but new cards might work this way
|
||||
for (String staticAbility : mainPart.getStaticAbilities()) {
|
||||
if (staticAbility.contains("CharacteristicDefining$ True") && staticAbility.contains("AddType$ Creature")) {
|
||||
return true;
|
||||
}
|
||||
@@ -399,7 +400,6 @@ public final class CardRules implements ICardCharacteristics {
|
||||
}
|
||||
|
||||
public int getSetColorID() {
|
||||
//Could someday generalize this to support other kinds of markings.
|
||||
return setColorID;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,8 +11,7 @@ public enum CardSplitType
|
||||
Meld(FaceSelectionMethod.USE_ACTIVE_FACE, CardStateName.Meld),
|
||||
Split(FaceSelectionMethod.COMBINE, CardStateName.RightSplit),
|
||||
Flip(FaceSelectionMethod.USE_PRIMARY_FACE, CardStateName.Flipped),
|
||||
Adventure(FaceSelectionMethod.USE_PRIMARY_FACE, CardStateName.Secondary),
|
||||
Omen(FaceSelectionMethod.USE_PRIMARY_FACE, CardStateName.Secondary),
|
||||
Adventure(FaceSelectionMethod.USE_PRIMARY_FACE, CardStateName.Adventure),
|
||||
Modal(FaceSelectionMethod.USE_ACTIVE_FACE, CardStateName.Modal),
|
||||
Specialize(FaceSelectionMethod.USE_ACTIVE_FACE, null);
|
||||
|
||||
|
||||
@@ -5,11 +5,12 @@ public enum CardStateName {
|
||||
Original,
|
||||
FaceDown,
|
||||
Flipped,
|
||||
Converted,
|
||||
Transformed,
|
||||
Meld,
|
||||
LeftSplit,
|
||||
RightSplit,
|
||||
Secondary,
|
||||
Adventure,
|
||||
Modal,
|
||||
EmptyRoom,
|
||||
SpecializeW,
|
||||
|
||||
@@ -26,7 +26,6 @@ import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
@@ -316,12 +315,6 @@ public final class CardType implements Comparable<CardType>, CardTypeView {
|
||||
return landTypes;
|
||||
}
|
||||
|
||||
public Set<String> getBattleTypes() {
|
||||
if(!isBattle())
|
||||
return Set.of();
|
||||
return subtypes.stream().filter(CardType::isABattleType).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasStringType(String t) {
|
||||
if (t.isEmpty()) {
|
||||
@@ -529,9 +522,6 @@ public final class CardType implements Comparable<CardType>, CardTypeView {
|
||||
return hasSubtype("Contraption");
|
||||
}
|
||||
|
||||
public boolean isVehicle() { return hasSubtype("Vehicle"); }
|
||||
public boolean isSpacecraft() { return hasSubtype("Spacecraft"); }
|
||||
|
||||
@Override
|
||||
public boolean isSaga() {
|
||||
return hasSubtype("Saga");
|
||||
|
||||
@@ -16,7 +16,6 @@ public interface CardTypeView extends Iterable<String>, Serializable {
|
||||
|
||||
Set<String> getCreatureTypes();
|
||||
Set<String> getLandTypes();
|
||||
Set<String> getBattleTypes();
|
||||
|
||||
boolean hasStringType(String t);
|
||||
boolean hasType(CoreType type);
|
||||
|
||||
@@ -25,8 +25,6 @@ import forge.util.BinaryUtil;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* <p>CardColor class.</p>
|
||||
@@ -293,8 +291,14 @@ public final class ColorSet implements Comparable<ColorSet>, Iterable<Byte>, Ser
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
final ManaCostShard[] orderedShards = getOrderedShards();
|
||||
return Arrays.stream(orderedShards).map(ManaCostShard::toShortString).collect(Collectors.joining());
|
||||
if (this.orderWeight == -1) {
|
||||
return "n/a";
|
||||
}
|
||||
final String toReturn = MagicColor.toLongString(myColor);
|
||||
if (toReturn.equals(MagicColor.Constant.COLORLESS) && myColor != 0) {
|
||||
return "multi";
|
||||
}
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -372,10 +376,6 @@ public final class ColorSet implements Comparable<ColorSet>, Iterable<Byte>, Ser
|
||||
}
|
||||
}
|
||||
|
||||
public Stream<MagicColor.Color> stream() {
|
||||
return this.toEnumSet().stream();
|
||||
}
|
||||
|
||||
//Get array of mana cost shards for color set in the proper order
|
||||
public ManaCostShard[] getOrderedShards() {
|
||||
return shardOrderLookup[myColor];
|
||||
|
||||
@@ -5,42 +5,43 @@ import forge.item.PaperCard;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
public interface ICardDatabase extends Iterable<PaperCard> {
|
||||
/**
|
||||
* Magic Cards Database.
|
||||
* --------------------
|
||||
* This interface defines the general API for Database Access and Cards' Lookup.
|
||||
* <p>
|
||||
*
|
||||
* Methods for single Card's lookup currently support three alternative strategies:
|
||||
* 1. [getCard]: Card search based on a single card's attributes
|
||||
* (i.e. name, edition, art, collectorNumber)
|
||||
* <p>
|
||||
*
|
||||
* 2. [getCardFromSet]: Card Lookup from a single Expansion set.
|
||||
* Particularly useful in Deck Editors when a specific Set is specified.
|
||||
* <p>
|
||||
*
|
||||
* 3. [getCardFromEditions]: Card search considering a predefined `SetPreference` policy and/or a specified Date
|
||||
* when no expansion is specified for a card.
|
||||
* This method is particularly useful for Re-prints whenever no specific
|
||||
* Expansion is specified (e.g. in Deck Import) and a decision should be made
|
||||
* on which card to pick. This methods allows to adopt a SetPreference selection
|
||||
* policy to make this decision.
|
||||
* <p>
|
||||
*
|
||||
* The API also includes methods to fetch Collection of Card (i.e. PaperCard instances):
|
||||
* - all cards (no filter)
|
||||
* - all unique cards (by name)
|
||||
* - all prints of a single card
|
||||
* - all cards from a single Expansion Set
|
||||
* - all cards compliant with a filter condition (i.e. Predicate)
|
||||
* <p>
|
||||
*
|
||||
* Finally, various utility methods are supported:
|
||||
* - Get the foil version of a Card (if Any)
|
||||
* - Get the Order Number of a Card in an Expansion Set
|
||||
* - Get the number of Print/Arts for a card in a Set (useful for those exp. having multiple arts)
|
||||
* */
|
||||
public interface ICardDatabase extends Iterable<PaperCard> {
|
||||
|
||||
/* SINGLE CARD RETRIEVAL METHODS
|
||||
* ============================= */
|
||||
// 1. Card Lookup by attributes
|
||||
@@ -49,20 +50,22 @@ public interface ICardDatabase extends Iterable<PaperCard> {
|
||||
PaperCard getCard(String cardName, String edition, int artIndex);
|
||||
// [NEW Methods] Including the card CollectorNumber as criterion for DB lookup
|
||||
PaperCard getCard(String cardName, String edition, String collectorNumber);
|
||||
PaperCard getCard(String cardName, String edition, int artIndex, Map<String, String> flags);
|
||||
PaperCard getCard(String cardName, String edition, String collectorNumber, Map<String, String> flags);
|
||||
PaperCard getCard(String cardName, String edition, int artIndex, String collectorNumber);
|
||||
PaperCard getCard(String cardName, String edition, int artIndex, Set<String> colorID);
|
||||
|
||||
// 2. Card Lookup from a single Expansion Set
|
||||
PaperCard getCardFromSet(String cardName, CardEdition edition, boolean isFoil); // NOT yet used, included for API symmetry
|
||||
PaperCard getCardFromSet(String cardName, CardEdition edition, String collectorNumber, boolean isFoil);
|
||||
PaperCard getCardFromSet(String cardName, CardEdition edition, int artIndex, boolean isFoil);
|
||||
PaperCard getCardFromSet(String cardName, CardEdition edition, int artIndex, String collectorNumber, boolean isFoil);
|
||||
PaperCard getCardFromSet(String cardName, CardEdition edition, int artIndex, String collectorNumber, boolean isFoil, Set<String> colorID);
|
||||
|
||||
// 3. Card lookup based on CardArtPreference Selection Policy
|
||||
PaperCard getCardFromEditions(String cardName, CardArtPreference artPreference);
|
||||
PaperCard getCardFromEditions(String cardName, CardArtPreference artPreference, Predicate<PaperCard> filter);
|
||||
PaperCard getCardFromEditions(String cardName, CardArtPreference artPreference, int artIndex);
|
||||
PaperCard getCardFromEditions(String cardName, CardArtPreference artPreference, int artIndex, Predicate<PaperCard> filter);
|
||||
PaperCard getCardFromEditions(String cardName, CardArtPreference artPreference, int artIndex, Set<String> colorID);
|
||||
|
||||
// 4. Specialised Card Lookup on CardArtPreference Selection and Release Date
|
||||
PaperCard getCardFromEditionsReleasedBefore(String cardName, CardArtPreference artPreference, Date releaseDate);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package forge.card;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import forge.deck.DeckRecognizer;
|
||||
|
||||
/**
|
||||
* Holds byte values for each color magic has.
|
||||
@@ -188,12 +187,6 @@ public final class MagicColor {
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getLocalizedName() {
|
||||
//Should probably move some of this logic back here, or at least to a more general location.
|
||||
return DeckRecognizer.getLocalisedMagicColorName(getName());
|
||||
}
|
||||
|
||||
public byte getColormask() {
|
||||
return colormask;
|
||||
}
|
||||
|
||||
@@ -94,7 +94,6 @@ public enum ManaCostShard {
|
||||
/** The cmpc. */
|
||||
private final float cmpc;
|
||||
private final String stringValue;
|
||||
private final String shortStringValue;
|
||||
|
||||
/** The image key. */
|
||||
private final String imageKey;
|
||||
@@ -126,7 +125,6 @@ public enum ManaCostShard {
|
||||
this.cmc = this.getCMC();
|
||||
this.cmpc = this.getCmpCost();
|
||||
this.stringValue = "{" + sValue + "}";
|
||||
this.shortStringValue = sValue;
|
||||
this.imageKey = imgKey;
|
||||
}
|
||||
|
||||
@@ -234,21 +232,16 @@ public enum ManaCostShard {
|
||||
return ManaCostShard.valueOf(atoms);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the string representation of this shard - e.g. "{W}" "{2/U}" "{G/P}"
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see java.lang.Object#toString()
|
||||
*/
|
||||
@Override
|
||||
public final String toString() {
|
||||
return this.stringValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The string representation of this shard without brackets - e.g. "W" "2/U" "G/P"
|
||||
*/
|
||||
public final String toShortString() {
|
||||
return this.shortStringValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the cmc.
|
||||
*
|
||||
|
||||
@@ -52,10 +52,7 @@ public class CardPool extends ItemPool<PaperCard> {
|
||||
|
||||
public void add(final String cardRequest, final int amount) {
|
||||
CardDb.CardRequest request = CardDb.CardRequest.fromString(cardRequest);
|
||||
if(request.collectorNumber != null && !request.collectorNumber.equals(IPaperCard.NO_COLLECTOR_NUMBER))
|
||||
this.add(CardDb.CardRequest.compose(request.cardName, request.isFoil), request.edition, request.collectorNumber, amount, false, request.flags);
|
||||
else
|
||||
this.add(CardDb.CardRequest.compose(request.cardName, request.isFoil), request.edition, request.artIndex, amount, false, request.flags);
|
||||
this.add(CardDb.CardRequest.compose(request.cardName, request.isFoil), request.edition, request.artIndex, amount, false, request.colorID);
|
||||
}
|
||||
|
||||
public void add(final String cardName, final String setCode) {
|
||||
@@ -74,20 +71,7 @@ public class CardPool extends ItemPool<PaperCard> {
|
||||
public void add(String cardName, String setCode, int artIndex, final int amount) {
|
||||
this.add(cardName, setCode, artIndex, amount, false, null);
|
||||
}
|
||||
private void add(String cardName, String setCode, String collectorNumber, final int amount, boolean addAny, Map<String, String> flags) {
|
||||
Map<String, CardDb> dbs = StaticData.instance().getAvailableDatabases();
|
||||
for (Map.Entry<String, CardDb> entry: dbs.entrySet()){
|
||||
CardDb db = entry.getValue();
|
||||
PaperCard paperCard = db.getCard(cardName, setCode, collectorNumber, flags);
|
||||
if (paperCard != null) {
|
||||
this.add(paperCard, amount);
|
||||
return;
|
||||
}
|
||||
}
|
||||
//Failed to find it. Fall back accordingly?
|
||||
this.add(cardName, setCode, IPaperCard.NO_ART_INDEX, amount, addAny, flags);
|
||||
}
|
||||
private void add(String cardName, String setCode, int artIndex, final int amount, boolean addAny, Map<String, String> flags) {
|
||||
public void add(String cardName, String setCode, int artIndex, final int amount, boolean addAny, Set<String> colorID) {
|
||||
Map<String, CardDb> dbs = StaticData.instance().getAvailableDatabases();
|
||||
PaperCard paperCard = null;
|
||||
String selectedDbName = "";
|
||||
@@ -97,7 +81,7 @@ public class CardPool extends ItemPool<PaperCard> {
|
||||
for (Map.Entry<String, CardDb> entry: dbs.entrySet()){
|
||||
String dbName = entry.getKey();
|
||||
CardDb db = entry.getValue();
|
||||
paperCard = db.getCard(cardName, setCode, artIndex, flags);
|
||||
paperCard = db.getCard(cardName, setCode, artIndex, colorID);
|
||||
if (paperCard != null) {
|
||||
selectedDbName = dbName;
|
||||
break;
|
||||
@@ -139,7 +123,7 @@ public class CardPool extends ItemPool<PaperCard> {
|
||||
int cnt = artGroups[i - 1];
|
||||
if (cnt <= 0)
|
||||
continue;
|
||||
PaperCard randomCard = cardDb.getCard(cardName, setCode, i, flags);
|
||||
PaperCard randomCard = cardDb.getCard(cardName, setCode, i, colorID);
|
||||
this.add(randomCard, cnt);
|
||||
}
|
||||
}
|
||||
@@ -446,6 +430,7 @@ public class CardPool extends ItemPool<PaperCard> {
|
||||
public String toCardList(String separator) {
|
||||
List<Entry<PaperCard, Integer>> main2sort = Lists.newArrayList(this);
|
||||
main2sort.sort(ItemPoolSorter.BY_NAME_THEN_SET);
|
||||
final CardDb commonDb = StaticData.instance().getCommonCards();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
boolean isFirst = true;
|
||||
@@ -456,8 +441,10 @@ public class CardPool extends ItemPool<PaperCard> {
|
||||
else
|
||||
isFirst = false;
|
||||
|
||||
CardDb db = !e.getKey().getRules().isVariant() ? commonDb : StaticData.instance().getVariantCards();
|
||||
sb.append(e.getValue()).append(" ");
|
||||
sb.append(CardDb.CardRequest.compose(e.getKey()));
|
||||
db.appendCardToStringBuilder(e.getKey(), sb);
|
||||
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
@@ -476,4 +463,20 @@ public class CardPool extends ItemPool<PaperCard> {
|
||||
}
|
||||
return filteredPool;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a predicate to this CardPool's cards.
|
||||
* @param predicate the Predicate to apply to this CardPool
|
||||
* @return a new CardPool made from this CardPool with only the cards that agree with the provided Predicate
|
||||
*/
|
||||
public CardPool getFilteredPoolWithCardsCount(Predicate<PaperCard> predicate) {
|
||||
CardPool filteredPool = new CardPool();
|
||||
for (Entry<PaperCard, Integer> entry : this.items.entrySet()) {
|
||||
PaperCard pc = entry.getKey();
|
||||
int count = entry.getValue();
|
||||
if (predicate.test(pc))
|
||||
filteredPool.add(pc, count);
|
||||
}
|
||||
return filteredPool;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,7 +247,7 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
|
||||
|
||||
Map<String, List<String>> referenceDeckLoadingMap;
|
||||
if (deferredSections != null) {
|
||||
this.normalizeDeferredSections();
|
||||
this.validateDeferredSections();
|
||||
referenceDeckLoadingMap = new HashMap<>(this.deferredSections);
|
||||
} else
|
||||
referenceDeckLoadingMap = new HashMap<>(loadedSections);
|
||||
@@ -267,7 +267,7 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
|
||||
continue;
|
||||
final List<String> cardsInSection = s.getValue();
|
||||
ArrayList<String> cardNamesWithNoEdition = getAllCardNamesWithNoSpecifiedEdition(cardsInSection);
|
||||
if (!cardNamesWithNoEdition.isEmpty()) {
|
||||
if (cardNamesWithNoEdition.size() > 0) {
|
||||
includeCardsFromUnspecifiedSet = true;
|
||||
if (smartCardArtSelection)
|
||||
cardsWithNoEdition.put(sec, cardNamesWithNoEdition);
|
||||
@@ -281,10 +281,10 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
|
||||
optimiseCardArtSelectionInDeckSections(cardsWithNoEdition);
|
||||
}
|
||||
|
||||
private void normalizeDeferredSections() {
|
||||
private void validateDeferredSections() {
|
||||
/*
|
||||
Construct a temporary (DeckSection, CardPool) Maps, to be sanitised and finalised
|
||||
before copying into `this.parts`. This sanitization is applied because of the
|
||||
before copying into `this.parts`. This sanitisation is applied because of the
|
||||
validation schema introduced in DeckSections.
|
||||
*/
|
||||
Map<String, List<String>> validatedSections = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
|
||||
@@ -296,33 +296,61 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
|
||||
}
|
||||
|
||||
final List<String> cardsInSection = s.getValue();
|
||||
List<Pair<String, Integer>> originalCardRequests = CardPool.processCardList(cardsInSection);
|
||||
CardPool pool = CardPool.fromCardList(cardsInSection);
|
||||
if (pool.countDistinct() == 0)
|
||||
continue; // pool empty, no card has been found!
|
||||
|
||||
List<String> validatedSection = validatedSections.computeIfAbsent(s.getKey(), (k) -> new ArrayList<>());
|
||||
for (Entry<PaperCard, Integer> entry : pool) {
|
||||
PaperCard card = entry.getKey();
|
||||
String normalizedRequest = getPoolRequest(entry);
|
||||
if(deckSection.validate(card))
|
||||
validatedSection.add(normalizedRequest);
|
||||
else {
|
||||
// Card was in the wrong section. Move it to the right section.
|
||||
DeckSection cardSection = DeckSection.matchingSection(card);
|
||||
assert(cardSection.validate(card)); //Card doesn't fit in the matchingSection?
|
||||
List<String> sectionCardList = validatedSections.computeIfAbsent(cardSection.name(), (k) -> new ArrayList<>());
|
||||
sectionCardList.add(normalizedRequest);
|
||||
}
|
||||
// Filter pool by applying DeckSection Validation schema for Card Types (to avoid inconsistencies)
|
||||
CardPool filteredPool = pool.getFilteredPoolWithCardsCount(deckSection::validate);
|
||||
// Add all the cards from ValidPool anyway!
|
||||
List<String> whiteList = validatedSections.getOrDefault(s.getKey(), null);
|
||||
if (whiteList == null)
|
||||
whiteList = new ArrayList<>();
|
||||
for (Entry<PaperCard, Integer> entry : filteredPool) {
|
||||
String poolRequest = getPoolRequest(entry, originalCardRequests);
|
||||
whiteList.add(poolRequest);
|
||||
}
|
||||
validatedSections.put(s.getKey(), whiteList);
|
||||
|
||||
if (filteredPool.countDistinct() != pool.countDistinct()) {
|
||||
CardPool blackList = pool.getFilteredPoolWithCardsCount(input -> !(deckSection.validate(input)));
|
||||
|
||||
for (Entry<PaperCard, Integer> entry : blackList) {
|
||||
DeckSection cardSection = DeckSection.matchingSection(entry.getKey());
|
||||
String poolRequest = getPoolRequest(entry, originalCardRequests);
|
||||
List<String> sectionCardList = validatedSections.getOrDefault(cardSection.name(), null);
|
||||
if (sectionCardList == null)
|
||||
sectionCardList = new ArrayList<>();
|
||||
sectionCardList.add(poolRequest);
|
||||
validatedSections.put(cardSection.name(), sectionCardList);
|
||||
} // end for blacklist
|
||||
} // end if
|
||||
} // end main for on deferredSections
|
||||
|
||||
// Overwrite deferredSections
|
||||
this.deferredSections = validatedSections;
|
||||
}
|
||||
|
||||
private String getPoolRequest(Entry<PaperCard, Integer> entry) {
|
||||
private String getPoolRequest(Entry<PaperCard, Integer> entry, List<Pair<String, Integer>> originalCardRequests) {
|
||||
PaperCard card = entry.getKey();
|
||||
int amount = entry.getValue();
|
||||
String poolCardRequest = CardDb.CardRequest.compose(entry.getKey());
|
||||
String poolCardRequest = CardDb.CardRequest.compose(
|
||||
card.isFoil() ? CardDb.CardRequest.compose(card.getName(), true) : card.getName(),
|
||||
card.getEdition(), card.getArtIndex(), card.getColorID());
|
||||
String originalRequestCandidate = null;
|
||||
for (Pair<String, Integer> originalRequest : originalCardRequests){
|
||||
String cardRequest = originalRequest.getLeft();
|
||||
if (!StringUtils.startsWithIgnoreCase(poolCardRequest, cardRequest))
|
||||
continue;
|
||||
originalRequestCandidate = cardRequest;
|
||||
int cardAmount = originalRequest.getRight();
|
||||
if (amount == cardAmount)
|
||||
return String.format("%d %s", cardAmount, cardRequest);
|
||||
}
|
||||
// This is just in case, it should never happen as we're
|
||||
if (originalRequestCandidate != null)
|
||||
return String.format("%d %s", amount, originalRequestCandidate);
|
||||
return String.format("%d %s", amount, poolCardRequest);
|
||||
}
|
||||
|
||||
@@ -617,8 +645,9 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (o instanceof DeckBase deckBase) {
|
||||
boolean deckBaseEquals = super.equals(deckBase);
|
||||
if (o instanceof Deck) {
|
||||
final DeckBase dbase = (DeckBase) o;
|
||||
boolean deckBaseEquals = super.equals(dbase);
|
||||
if (!deckBaseEquals)
|
||||
return false;
|
||||
// ok so far we made sure they do have the same name. Now onto comparing parts
|
||||
|
||||
@@ -472,8 +472,7 @@ public class DeckRecognizer {
|
||||
"side", "sideboard", "sb",
|
||||
"main", "card", "mainboard",
|
||||
"avatar", "commander", "schemes",
|
||||
"conspiracy", "planes", "deck", "dungeon",
|
||||
"attractions", "contraptions"};
|
||||
"conspiracy", "planes", "deck", "dungeon"};
|
||||
|
||||
private static CharSequence[] allCardTypes(){
|
||||
List<String> cardTypesList = new ArrayList<>();
|
||||
@@ -672,8 +671,7 @@ public class DeckRecognizer {
|
||||
return checkAndSetCardToken(pc, edition, cardCount, deckSecFromCardLine,
|
||||
currentDeckSection, true);
|
||||
// UNKNOWN card as in the Counterspell|FEM case
|
||||
unknownCardToken = Token.UnknownCard(cardName, setCode, cardCount);
|
||||
continue;
|
||||
return Token.UnknownCard(cardName, setCode, cardCount);
|
||||
}
|
||||
// ok so we can simply ignore everything but card name - as set code does not exist
|
||||
// At this stage, we know the card name exists in the DB so a Card MUST be found
|
||||
@@ -987,7 +985,7 @@ public class DeckRecognizer {
|
||||
private static String getMagicColourLabel(MagicColor.Color magicColor) {
|
||||
if (magicColor == null) // Multicolour
|
||||
return String.format("%s {W}{U}{B}{R}{G}", getLocalisedMagicColorName("Multicolour"));
|
||||
return String.format("%s %s", magicColor.getLocalizedName(), magicColor.getSymbol());
|
||||
return String.format("%s %s", getLocalisedMagicColorName(magicColor.getName()), magicColor.getSymbol());
|
||||
}
|
||||
|
||||
private static final HashMap<Integer, String> manaSymbolsMap = new HashMap<Integer, String>() {{
|
||||
@@ -1006,8 +1004,8 @@ public class DeckRecognizer {
|
||||
if (magicColor2 == null || magicColor2 == MagicColor.Color.COLORLESS
|
||||
|| magicColor1 == MagicColor.Color.COLORLESS)
|
||||
return String.format("%s // %s", getMagicColourLabel(magicColor1), getMagicColourLabel(magicColor2));
|
||||
String localisedName1 = magicColor1.getLocalizedName();
|
||||
String localisedName2 = magicColor2.getLocalizedName();
|
||||
String localisedName1 = getLocalisedMagicColorName(magicColor1.getName());
|
||||
String localisedName2 = getLocalisedMagicColorName(magicColor2.getName());
|
||||
String comboManaSymbol = manaSymbolsMap.get(magicColor1.getColormask() | magicColor2.getColormask());
|
||||
return String.format("%s/%s {%s}", localisedName1, localisedName2, comboManaSymbol);
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ public enum DeckSection {
|
||||
CardType t = card.getRules().getType();
|
||||
// NOTE: Same rules applies to both Deck and Side, despite "Conspiracy cards" are allowed
|
||||
// in the SideBoard (see Rule 313.2)
|
||||
// Those will be matched later, in case (see `Deck::normalizeDeferredSections`)
|
||||
// Those will be matched later, in case (see `Deck::validateDeferredSections`)
|
||||
return !t.isConspiracy() && !t.isDungeon() && !t.isPhenomenon() && !t.isPlane() && !t.isScheme() && !t.isVanguard();
|
||||
};
|
||||
|
||||
|
||||
@@ -61,8 +61,6 @@ public class DeckSerializer {
|
||||
}
|
||||
|
||||
for(Entry<DeckSection, CardPool> s : d) {
|
||||
if(s.getValue().isEmpty())
|
||||
continue;
|
||||
out.add(TextUtil.enclosedBracket(s.getKey().toString()));
|
||||
out.add(s.getValue().toCardList(System.lineSeparator()));
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ package forge.item;
|
||||
|
||||
import forge.card.CardRarity;
|
||||
import forge.card.CardRules;
|
||||
import forge.card.ColorSet;
|
||||
import forge.card.ICardFace;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Set;
|
||||
|
||||
public interface IPaperCard extends InventoryItem, Serializable {
|
||||
|
||||
@@ -20,7 +20,7 @@ public interface IPaperCard extends InventoryItem, Serializable {
|
||||
String getEdition();
|
||||
String getCollectorNumber();
|
||||
String getFunctionalVariant();
|
||||
ColorSet getMarkedColors();
|
||||
Set<String> getColorID();
|
||||
int getArtIndex();
|
||||
boolean isFoil();
|
||||
boolean isToken();
|
||||
|
||||
@@ -26,9 +26,10 @@ import forge.util.Localizer;
|
||||
import forge.util.TextUtil;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* A lightweight version of a card that matches real-world cards, to use outside of games (eg. inventory, decks, trade).
|
||||
@@ -38,7 +39,6 @@ import java.util.stream.Collectors;
|
||||
* @author Forge
|
||||
*/
|
||||
public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet, IPaperCard {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 2942081982620691205L;
|
||||
|
||||
// Reference to rules
|
||||
@@ -55,15 +55,16 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
|
||||
private String artist;
|
||||
private final int artIndex;
|
||||
private final boolean foil;
|
||||
private final PaperCardFlags flags;
|
||||
private final String sortableName;
|
||||
private Boolean hasImage;
|
||||
private final boolean noSell;
|
||||
private Set<String> colorID;
|
||||
private String sortableName;
|
||||
private final String functionalVariant;
|
||||
|
||||
// Calculated fields are below:
|
||||
private transient CardRarity rarity; // rarity is given in ctor when set is assigned
|
||||
// Reference to a new instance of Self, but foiled!
|
||||
private transient PaperCard foiledVersion, noSellVersion, flaglessVersion;
|
||||
private transient Boolean hasImage;
|
||||
private transient PaperCard foiledVersion, noSellVersion;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
@@ -88,8 +89,8 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
|
||||
}
|
||||
|
||||
@Override
|
||||
public ColorSet getMarkedColors() {
|
||||
return this.flags.markedColors;
|
||||
public Set<String> getColorID() {
|
||||
return colorID;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -146,32 +147,32 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
|
||||
return unFoiledVersion;
|
||||
}
|
||||
public PaperCard getNoSellVersion() {
|
||||
if (this.flags.noSellValue)
|
||||
if (this.noSell)
|
||||
return this;
|
||||
|
||||
if (this.noSellVersion == null)
|
||||
this.noSellVersion = new PaperCard(this, this.flags.withNoSellValueFlag(true));
|
||||
if (this.noSellVersion == null) {
|
||||
this.noSellVersion = new PaperCard(this.rules, this.edition, this.rarity,
|
||||
this.artIndex, this.foil, String.valueOf(collectorNumber), this.artist, this.functionalVariant, true);
|
||||
}
|
||||
return this.noSellVersion;
|
||||
}
|
||||
|
||||
public PaperCard copyWithoutFlags() {
|
||||
if(this.flaglessVersion == null) {
|
||||
if(this.flags == PaperCardFlags.IDENTITY_FLAGS)
|
||||
this.flaglessVersion = this;
|
||||
else
|
||||
this.flaglessVersion = new PaperCard(this, null);
|
||||
}
|
||||
return flaglessVersion;
|
||||
}
|
||||
public PaperCard copyWithFlags(Map<String, String> flags) {
|
||||
if(flags == null || flags.isEmpty())
|
||||
return this.copyWithoutFlags();
|
||||
return new PaperCard(this, new PaperCardFlags(flags));
|
||||
}
|
||||
public PaperCard copyWithMarkedColors(ColorSet colors) {
|
||||
if(Objects.equals(colors, this.flags.markedColors))
|
||||
public PaperCard getSellable() {
|
||||
if (!this.noSell)
|
||||
return this;
|
||||
return new PaperCard(this, this.flags.withMarkedColors(colors));
|
||||
|
||||
PaperCard sellable = new PaperCard(this.rules, this.edition, this.rarity,
|
||||
this.artIndex, this.foil, String.valueOf(collectorNumber), this.artist, this.functionalVariant, false);
|
||||
return sellable;
|
||||
}
|
||||
public PaperCard getColorIDVersion(Set<String> colors) {
|
||||
if (colors == null && this.colorID == null)
|
||||
return this;
|
||||
if (this.colorID != null && this.colorID.equals(colors))
|
||||
return this;
|
||||
if (colors != null && colors.equals(this.colorID))
|
||||
return this;
|
||||
return new PaperCard(this.rules, this.edition, this.rarity,
|
||||
this.artIndex, this.foil, String.valueOf(collectorNumber), this.artist, this.functionalVariant, this.noSell, colors);
|
||||
}
|
||||
@Override
|
||||
public String getItemType() {
|
||||
@@ -179,12 +180,8 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
|
||||
return localizer.getMessage("lblCard");
|
||||
}
|
||||
|
||||
public PaperCardFlags getMarkedFlags() {
|
||||
return this.flags;
|
||||
}
|
||||
|
||||
public boolean hasNoSellValue() {
|
||||
return this.flags.noSellValue;
|
||||
public boolean isNoSell() {
|
||||
return noSell;
|
||||
}
|
||||
public boolean hasImage() {
|
||||
return hasImage(false);
|
||||
@@ -201,41 +198,38 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
|
||||
IPaperCard.NO_COLLECTOR_NUMBER, IPaperCard.NO_ARTIST_NAME, IPaperCard.NO_FUNCTIONAL_VARIANT);
|
||||
}
|
||||
|
||||
public PaperCard(final PaperCard copyFrom, final PaperCardFlags flags) {
|
||||
this(copyFrom.rules, copyFrom.edition, copyFrom.rarity, copyFrom.artIndex, copyFrom.foil, copyFrom.collectorNumber,
|
||||
copyFrom.artist, copyFrom.functionalVariant, flags);
|
||||
this.flaglessVersion = copyFrom.flaglessVersion;
|
||||
public PaperCard(final CardRules rules0, final String edition0, final CardRarity rarity0,
|
||||
final int artIndex0, final boolean foil0, final String collectorNumber0,
|
||||
final String artist0, final String functionalVariant) {
|
||||
this(rules0, edition0, rarity0, artIndex0, foil0, collectorNumber0, artist0, functionalVariant, false);
|
||||
}
|
||||
|
||||
public PaperCard(final CardRules rules0, final String edition0, final CardRarity rarity0,
|
||||
final int artIndex0, final boolean foil0, final String collectorNumber0,
|
||||
final String artist0, final String functionalVariant) {
|
||||
this(rules0, edition0, rarity0, artIndex0, foil0, collectorNumber0, artist0, functionalVariant, null);
|
||||
final String artist0, final String functionalVariant, final boolean noSell0) {
|
||||
this(rules0, edition0, rarity0, artIndex0, foil0, collectorNumber0, artist0, functionalVariant, noSell0, null);
|
||||
}
|
||||
|
||||
protected PaperCard(final CardRules rules, final String edition, final CardRarity rarity,
|
||||
final int artIndex, final boolean foil, final String collectorNumber,
|
||||
final String artist, final String functionalVariant, final PaperCardFlags flags) {
|
||||
if (rules == null || edition == null || rarity == null) {
|
||||
public PaperCard(final CardRules rules0, final String edition0, final CardRarity rarity0,
|
||||
final int artIndex0, final boolean foil0, final String collectorNumber0,
|
||||
final String artist0, final String functionalVariant, final boolean noSell0, final Set<String> colorID0) {
|
||||
if (rules0 == null || edition0 == null || rarity0 == null) {
|
||||
throw new IllegalArgumentException("Cannot create card without rules, edition or rarity");
|
||||
}
|
||||
this.rules = rules;
|
||||
name = rules.getName();
|
||||
this.edition = edition;
|
||||
this.artIndex = Math.max(artIndex, IPaperCard.DEFAULT_ART_INDEX);
|
||||
this.foil = foil;
|
||||
this.rarity = rarity;
|
||||
this.artist = TextUtil.normalizeText(artist);
|
||||
this.collectorNumber = (collectorNumber != null && !collectorNumber.isEmpty()) ? collectorNumber : IPaperCard.NO_COLLECTOR_NUMBER;
|
||||
rules = rules0;
|
||||
name = rules0.getName();
|
||||
edition = edition0;
|
||||
artIndex = Math.max(artIndex0, IPaperCard.DEFAULT_ART_INDEX);
|
||||
foil = foil0;
|
||||
rarity = rarity0;
|
||||
artist = TextUtil.normalizeText(artist0);
|
||||
collectorNumber = (collectorNumber0 != null) && (collectorNumber0.length() > 0) ? collectorNumber0 : IPaperCard.NO_COLLECTOR_NUMBER;
|
||||
// If the user changes the language this will make cards sort by the old language until they restart the game.
|
||||
// This is a good tradeoff
|
||||
sortableName = TextUtil.toSortableName(CardTranslation.getTranslatedName(rules.getName()));
|
||||
sortableName = TextUtil.toSortableName(CardTranslation.getTranslatedName(rules0.getName()));
|
||||
this.functionalVariant = functionalVariant != null ? functionalVariant : IPaperCard.NO_FUNCTIONAL_VARIANT;
|
||||
|
||||
if(flags == null || flags.equals(PaperCardFlags.IDENTITY_FLAGS))
|
||||
this.flags = PaperCardFlags.IDENTITY_FLAGS;
|
||||
else
|
||||
this.flags = flags;
|
||||
noSell = noSell0;
|
||||
colorID = colorID0;
|
||||
}
|
||||
|
||||
public static PaperCard FAKE_CARD = new PaperCard(CardRules.getUnsupportedCardNamed("Fake Card"), "Fake Edition", CardRarity.Common);
|
||||
@@ -262,7 +256,8 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
|
||||
}
|
||||
if (!getCollectorNumber().equals(other.getCollectorNumber()))
|
||||
return false;
|
||||
if (!Objects.equals(flags, other.flags))
|
||||
// colorID can be NULL
|
||||
if (getColorID() != other.getColorID())
|
||||
return false;
|
||||
return (other.foil == foil) && (other.artIndex == artIndex);
|
||||
}
|
||||
@@ -274,7 +269,13 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(name, edition, collectorNumber, artIndex, foil, flags);
|
||||
final int code = (name.hashCode() * 11) + (edition.hashCode() * 59) +
|
||||
(artIndex * 2) + (getCollectorNumber().hashCode() * 383);
|
||||
final int id = Optional.ofNullable(colorID).map(Set::hashCode).orElse(0);
|
||||
if (foil) {
|
||||
return code + id + 1;
|
||||
}
|
||||
return code + id;
|
||||
}
|
||||
|
||||
// FIXME: Check
|
||||
@@ -306,7 +307,7 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
|
||||
String collectorNumber = collectorNumber0;
|
||||
if (collectorNumber.equals(NO_COLLECTOR_NUMBER))
|
||||
collectorNumber = null;
|
||||
return CardEdition.getSortableCollectorNumber(collectorNumber);
|
||||
return CardEdition.CardInSet.getSortableCollectorNumber(collectorNumber);
|
||||
}
|
||||
|
||||
private String sortableCNKey = null;
|
||||
@@ -338,7 +339,6 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
|
||||
return Integer.compare(artIndex, o.getArtIndex());
|
||||
}
|
||||
|
||||
@Serial
|
||||
private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException {
|
||||
// default deserialization
|
||||
ois.defaultReadObject();
|
||||
@@ -347,46 +347,17 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
|
||||
if (pc == null) {
|
||||
pc = StaticData.instance().getVariantCards().getCard(name, edition, artIndex);
|
||||
if (pc == null) {
|
||||
System.out.println("PaperCard: " + name + " not found with set and index " + edition + ", " + artIndex);
|
||||
pc = readObjectAlternate(name, edition);
|
||||
if (pc == null) {
|
||||
throw new IOException(TextUtil.concatWithSpace("Card", name, "not found with set and index", edition, Integer.toString(artIndex)));
|
||||
}
|
||||
System.out.println("Alternate object found: " + pc.getName() + ", " + pc.getEdition() + ", " + pc.getArtIndex());
|
||||
throw new IOException(TextUtil.concatWithSpace("Card", name, "not found"));
|
||||
}
|
||||
}
|
||||
rules = pc.getRules();
|
||||
rarity = pc.getRarity();
|
||||
}
|
||||
|
||||
private IPaperCard readObjectAlternate(String name, String edition) throws ClassNotFoundException, IOException {
|
||||
IPaperCard pc = StaticData.instance().getCommonCards().getCard(name, edition);
|
||||
if (pc == null) {
|
||||
pc = StaticData.instance().getVariantCards().getCard(name, edition);
|
||||
}
|
||||
|
||||
if (pc == null) {
|
||||
pc = StaticData.instance().getCommonCards().getCard(name);
|
||||
if (pc == null) {
|
||||
pc = StaticData.instance().getVariantCards().getCard(name);
|
||||
}
|
||||
}
|
||||
|
||||
return pc;
|
||||
}
|
||||
|
||||
@Serial
|
||||
private Object readResolve() throws ObjectStreamException {
|
||||
//If we deserialize an old PaperCard with no flags, reinitialize as a fresh copy to set default flags.
|
||||
if(this.flags == null)
|
||||
return new PaperCard(this, null);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getImageKey(boolean altState) {
|
||||
String normalizedName = StringUtils.stripAccents(name);
|
||||
String imageKey = ImageKeys.CARD_PREFIX + normalizedName + CardDb.NameSetSeparator
|
||||
String noramlizedName = StringUtils.stripAccents(name);
|
||||
String imageKey = ImageKeys.CARD_PREFIX + noramlizedName + CardDb.NameSetSeparator
|
||||
+ edition + CardDb.NameSetSeparator + artIndex;
|
||||
if (altState) {
|
||||
imageKey += ImageKeys.BACKFACE_POSTFIX;
|
||||
@@ -522,88 +493,4 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
|
||||
public boolean isRebalanced() {
|
||||
return StaticData.instance().isRebalanced(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains properties of a card which distinguish it from an otherwise identical copy of the card with the same
|
||||
* name, edition, and collector number. Examples include permanent markings on the card, and flags for Adventure
|
||||
* mode.
|
||||
*/
|
||||
public static class PaperCardFlags implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = -3924720485840169336L;
|
||||
|
||||
/**
|
||||
* Chosen colors, for cards like Cryptic Spires.
|
||||
*/
|
||||
public final ColorSet markedColors;
|
||||
/**
|
||||
* Removes the sell value of the card in Adventure mode.
|
||||
*/
|
||||
public final boolean noSellValue;
|
||||
|
||||
//TODO: Could probably move foil here.
|
||||
|
||||
static final PaperCardFlags IDENTITY_FLAGS = new PaperCardFlags(Map.of());
|
||||
|
||||
protected PaperCardFlags(Map<String, String> flags) {
|
||||
if(flags.containsKey("markedColors"))
|
||||
markedColors = ColorSet.fromNames(flags.get("markedColors").split(""));
|
||||
else
|
||||
markedColors = null;
|
||||
noSellValue = flags.containsKey("noSellValue");
|
||||
}
|
||||
|
||||
//Copy constructor. There are some better ways to do this, and they should be explored once we have more than 4
|
||||
//or 5 fields here. Just need to ensure it's impossible to accidentally change a field while the PaperCardFlags
|
||||
//object is in use.
|
||||
private PaperCardFlags(PaperCardFlags copyFrom, ColorSet markedColors, Boolean noSellValue) {
|
||||
if(markedColors == null)
|
||||
markedColors = copyFrom.markedColors;
|
||||
else if(markedColors.isColorless())
|
||||
markedColors = null;
|
||||
this.markedColors = markedColors;
|
||||
this.noSellValue = noSellValue != null ? noSellValue : copyFrom.noSellValue;
|
||||
}
|
||||
|
||||
public PaperCardFlags withMarkedColors(ColorSet markedColors) {
|
||||
if(markedColors == null)
|
||||
markedColors = ColorSet.getNullColor();
|
||||
return new PaperCardFlags(this, markedColors, null);
|
||||
}
|
||||
|
||||
public PaperCardFlags withNoSellValueFlag(boolean noSellValue) {
|
||||
return new PaperCardFlags(this, null, noSellValue);
|
||||
}
|
||||
|
||||
private Map<String, String> asMap;
|
||||
public Map<String, String> toMap() {
|
||||
if(asMap != null)
|
||||
return asMap;
|
||||
Map<String, String> out = new HashMap<>();
|
||||
if(markedColors != null && !markedColors.isColorless())
|
||||
out.put("markedColors", markedColors.toString());
|
||||
if(noSellValue)
|
||||
out.put("noSellValue", "true");
|
||||
asMap = out;
|
||||
return out;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.toMap().entrySet().stream()
|
||||
.map((e) -> e.getKey() + "=" + e.getValue())
|
||||
.collect(Collectors.joining("\t"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof PaperCardFlags that)) return false;
|
||||
return noSellValue == that.noSellValue && Objects.equals(markedColors, that.markedColors);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(markedColors, noSellValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,11 @@ import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
public class PaperToken implements InventoryItemFromSet, IPaperCard {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private String name;
|
||||
private String collectorNumber;
|
||||
private String artist;
|
||||
private transient CardEdition edition;
|
||||
private ArrayList<String> imageFileName = new ArrayList<>();
|
||||
private transient CardRules cardRules;
|
||||
@@ -55,31 +54,75 @@ public class PaperToken implements InventoryItemFromSet, IPaperCard {
|
||||
return makeTokenFileName(fileName);
|
||||
}
|
||||
|
||||
public PaperToken(final CardRules c, CardEdition edition0, String imageFileName, String collectorNumber, String artist) {
|
||||
public static String makeTokenFileName(final CardRules rules, CardEdition edition) {
|
||||
ArrayList<String> build = new ArrayList<>();
|
||||
|
||||
String subtypes = StringUtils.join(rules.getType().getSubtypes(), " ");
|
||||
if (!rules.getName().equals(subtypes)) {
|
||||
return makeTokenFileName(rules.getName());
|
||||
}
|
||||
|
||||
ColorSet colors = rules.getColor();
|
||||
|
||||
if (colors.isColorless()) {
|
||||
build.add("C");
|
||||
} else {
|
||||
String color = "";
|
||||
if (colors.hasWhite()) color += "W";
|
||||
if (colors.hasBlue()) color += "U";
|
||||
if (colors.hasBlack()) color += "B";
|
||||
if (colors.hasRed()) color += "R";
|
||||
if (colors.hasGreen()) color += "G";
|
||||
|
||||
build.add(color);
|
||||
}
|
||||
|
||||
if (rules.getPower() != null && rules.getToughness() != null) {
|
||||
build.add(rules.getPower());
|
||||
build.add(rules.getToughness());
|
||||
}
|
||||
|
||||
String cardTypes = "";
|
||||
if (rules.getType().isArtifact()) cardTypes += "A";
|
||||
if (rules.getType().isEnchantment()) cardTypes += "E";
|
||||
|
||||
if (!cardTypes.isEmpty()) {
|
||||
build.add(cardTypes);
|
||||
}
|
||||
|
||||
build.add(subtypes);
|
||||
|
||||
// Are these keywords sorted?
|
||||
for (String keyword : rules.getMainPart().getKeywords()) {
|
||||
build.add(keyword);
|
||||
}
|
||||
|
||||
if (edition != null) {
|
||||
build.add(edition.getCode());
|
||||
}
|
||||
|
||||
return StringUtils.join(build, "_").replace('*', 'x').toLowerCase();
|
||||
}
|
||||
|
||||
public PaperToken(final CardRules c, CardEdition edition0, String imageFileName) {
|
||||
this.cardRules = c;
|
||||
this.name = c.getName();
|
||||
this.edition = edition0;
|
||||
this.collectorNumber = collectorNumber;
|
||||
this.artist = artist;
|
||||
|
||||
if (collectorNumber != null && !collectorNumber.isEmpty() && edition != null && edition.getTokens().containsKey(imageFileName)) {
|
||||
int idx = 0;
|
||||
// count the one with the same collectorNumber
|
||||
for (CardEdition.EditionEntry t : edition.getTokens().get(imageFileName)) {
|
||||
++idx;
|
||||
if (!t.collectorNumber().equals(collectorNumber)) {
|
||||
continue;
|
||||
if (edition != null && edition.getTokens().containsKey(imageFileName)) {
|
||||
this.artIndex = edition.getTokens().get(imageFileName);
|
||||
}
|
||||
// TODO make better image file names when collector number is known
|
||||
// for the right index, we need to count the ones with wrong collector number too
|
||||
this.imageFileName.add(String.format("%s|%s|%s|%d", imageFileName, edition.getCode(), collectorNumber, idx));
|
||||
}
|
||||
this.artIndex = this.imageFileName.size();
|
||||
} else if (null == edition || CardEdition.UNKNOWN == edition) {
|
||||
this.imageFileName.add(imageFileName);
|
||||
|
||||
if (imageFileName == null) {
|
||||
// This shouldn't really happen. We can just use the normalized name again for the base image name
|
||||
this.imageFileName.add(makeTokenFileName(c, edition0));
|
||||
} else {
|
||||
// Fallback if CollectorNumber is not used
|
||||
this.imageFileName.add(String.format("%s|%s", imageFileName, edition.getCode()));
|
||||
String formatEdition = null == edition || CardEdition.UNKNOWN == edition ? "" : "_" + edition.getCode().toLowerCase();
|
||||
|
||||
this.imageFileName.add(String.format("%s%s", imageFileName, formatEdition));
|
||||
for (int idx = 2; idx <= this.artIndex; idx++) {
|
||||
this.imageFileName.add(String.format("%s%d%s", imageFileName, idx, formatEdition));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,14 +138,12 @@ public class PaperToken implements InventoryItemFromSet, IPaperCard {
|
||||
|
||||
@Override
|
||||
public String getEdition() {
|
||||
return edition != null ? edition.getCode() : CardEdition.UNKNOWN_CODE;
|
||||
return edition != null ? edition.getCode() : "???";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCollectorNumber() {
|
||||
if (collectorNumber.isEmpty())
|
||||
return IPaperCard.NO_COLLECTOR_NUMBER;
|
||||
return collectorNumber;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -112,7 +153,7 @@ public class PaperToken implements InventoryItemFromSet, IPaperCard {
|
||||
}
|
||||
|
||||
@Override
|
||||
public ColorSet getMarkedColors() {
|
||||
public Set<String> getColorID() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -137,8 +178,13 @@ public class PaperToken implements InventoryItemFromSet, IPaperCard {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getArtist() {
|
||||
return artist;
|
||||
public String getArtist() { /*TODO*/
|
||||
return "";
|
||||
}
|
||||
|
||||
// Unfortunately this is a property of token, cannot move it outside of class
|
||||
public String getImageFilename() {
|
||||
return getImageFilename(1);
|
||||
}
|
||||
|
||||
public String getImageFilename(int idx) {
|
||||
@@ -213,21 +259,24 @@ public class PaperToken implements InventoryItemFromSet, IPaperCard {
|
||||
// InventoryItem
|
||||
@Override
|
||||
public String getImageKey(boolean altState) {
|
||||
String suffix = "";
|
||||
if (hasBackFace() && altState) {
|
||||
if (collectorNumber != null && !collectorNumber.isEmpty() && edition != null) {
|
||||
String name = cardRules.getOtherPart().getName().toLowerCase().replace(" token", "").replace(" ", "_");
|
||||
return ImageKeys.getTokenKey(String.format("%s|%s|%s%s", name, edition.getCode(), collectorNumber, ImageKeys.BACKFACE_POSTFIX));
|
||||
if (hasBackFace()) {
|
||||
String edCode = edition != null ? "_" + edition.getCode().toLowerCase() : "";
|
||||
if (altState) {
|
||||
String name = ImageKeys.TOKEN_PREFIX + cardRules.getOtherPart().getName().toLowerCase().replace(" token", "");
|
||||
name.replace(" ", "_");
|
||||
return name + edCode;
|
||||
} else {
|
||||
suffix = ImageKeys.BACKFACE_POSTFIX;
|
||||
String name = ImageKeys.TOKEN_PREFIX + cardRules.getMainPart().getName().toLowerCase().replace(" token", "");
|
||||
name.replace(" ", "_");
|
||||
return name + edCode;
|
||||
}
|
||||
}
|
||||
int idx = MyRandom.getRandom().nextInt(artIndex);
|
||||
return getImageKey(idx) + suffix;
|
||||
return getImageKey(idx);
|
||||
}
|
||||
|
||||
public String getImageKey(int artIndex) {
|
||||
return ImageKeys.getTokenKey(imageFileName.get(artIndex).replace(" ", "_"));
|
||||
return ImageKeys.TOKEN_PREFIX + imageFileName.get(artIndex).replace(" ", "_");
|
||||
}
|
||||
|
||||
public boolean isRebalanced() {
|
||||
|
||||
@@ -65,8 +65,8 @@ public class BoosterGenerator {
|
||||
}
|
||||
|
||||
public static List<PaperCard> getBoosterPack(SealedTemplate template) {
|
||||
if (template instanceof SealedTemplateWithSlots slots) {
|
||||
return BoosterGenerator.getBoosterPack(slots);
|
||||
if (template instanceof SealedTemplateWithSlots) {
|
||||
return BoosterGenerator.getBoosterPack((SealedTemplateWithSlots) template);
|
||||
}
|
||||
|
||||
List<PaperCard> result = new ArrayList<>();
|
||||
@@ -401,68 +401,46 @@ public class BoosterGenerator {
|
||||
System.out.println(numCards + " of type " + slotType);
|
||||
|
||||
// For cards that end in '+', attempt to convert this card to foil.
|
||||
boolean convertAllToFoil = slotType.endsWith("+");
|
||||
if (convertAllToFoil) {
|
||||
boolean convertCardFoil = slotType.endsWith("+");
|
||||
if (convertCardFoil) {
|
||||
slotType = slotType.substring(0, slotType.length() - 1);
|
||||
}
|
||||
|
||||
// Unpack Base
|
||||
BoosterSlot boosterSlot = boosterSlots.get(slotType);
|
||||
Map<String, Integer> slotReplacementCount = bulkSlotReplacement(boosterSlot, numCards);
|
||||
String determineSheet = boosterSlot.replaceSlot();
|
||||
|
||||
List<PaperCard> paperCards = Lists.newArrayList();
|
||||
for(Map.Entry<String, Integer> entry : slotReplacementCount.entrySet()) {
|
||||
String determineSheet = entry.getKey();
|
||||
int numCardsToGenerate = entry.getValue();
|
||||
|
||||
if (determineSheet == null || determineSheet.isEmpty() || numCardsToGenerate == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the sheet ends with a '+', convert all cards in replacement section to foil
|
||||
boolean convertThisToFoil = false;
|
||||
if (determineSheet.endsWith("+")) {
|
||||
determineSheet = determineSheet.substring(0, determineSheet.length() - 1);
|
||||
convertThisToFoil = true;
|
||||
convertCardFoil = true;
|
||||
}
|
||||
|
||||
String setCode = template.getEdition();
|
||||
|
||||
// Ok, so we have a sheet now. Most should be standard sheets, but some named edition sheets
|
||||
List<PaperCard> paperCards;
|
||||
PrintSheet ps;
|
||||
try {
|
||||
// Apply the edition to the sheet name by default. We'll try again if thats not a real sheet
|
||||
ps = getPrintSheet(determineSheet + " " + setCode);
|
||||
} catch (Exception e) {
|
||||
} catch(Exception e) {
|
||||
ps = getPrintSheet(determineSheet);
|
||||
}
|
||||
if (convertAllToFoil || convertThisToFoil) {
|
||||
for (PaperCard pc : ps.random(numCardsToGenerate, true)) {
|
||||
if (convertCardFoil) {
|
||||
paperCards = Lists.newArrayList();
|
||||
for(PaperCard pc : ps.random(numCards, true)) {
|
||||
paperCards.add(pc.getFoiled());
|
||||
}
|
||||
} else {
|
||||
paperCards.addAll(ps.random(numCardsToGenerate, true));
|
||||
paperCards = ps.random(numCards, true);
|
||||
}
|
||||
|
||||
result.addAll(paperCards);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static Map<String, Integer> bulkSlotReplacement(BoosterSlot boosterSlot, int numCards) {
|
||||
Map<String, Integer> slotReplacementCount = new HashMap<>();
|
||||
|
||||
for(int i = 0; i < numCards; i++) {
|
||||
String determineSheet = boosterSlot.replaceSlot();
|
||||
if (slotReplacementCount.containsKey(determineSheet)) {
|
||||
slotReplacementCount.put(determineSheet, slotReplacementCount.get(determineSheet) + 1);
|
||||
} else {
|
||||
slotReplacementCount.put(determineSheet, 1);
|
||||
}
|
||||
}
|
||||
|
||||
return slotReplacementCount;
|
||||
}
|
||||
|
||||
private static void ensureGuaranteedCardInBooster(List<PaperCard> result, SealedTemplate template, String boosterMustContain) {
|
||||
// First, see if there's already a card of the given type
|
||||
String[] types = TextUtil.split(boosterMustContain, ' ');
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
package forge.token;
|
||||
|
||||
import com.google.common.collect.HashMultimap;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Multimap;
|
||||
|
||||
import forge.card.CardDb;
|
||||
import forge.card.CardEdition;
|
||||
import forge.card.CardRules;
|
||||
import forge.item.IPaperCard;
|
||||
import forge.item.PaperToken;
|
||||
import forge.util.Aggregates;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Predicate;
|
||||
@@ -28,8 +23,8 @@ public class TokenDb implements ITokenDatabase {
|
||||
|
||||
// The image names should be the same as the script name + _set
|
||||
// If that isn't found, consider falling back to the original token
|
||||
private final Multimap<String, PaperToken> allTokenByName = HashMultimap.create();
|
||||
private final Map<String, PaperToken> extraTokensByName = Maps.newTreeMap(String.CASE_INSENSITIVE_ORDER);
|
||||
|
||||
private final Map<String, PaperToken> tokensByName = Maps.newTreeMap(String.CASE_INSENSITIVE_ORDER);
|
||||
|
||||
private final CardEdition.Collection editions;
|
||||
private final Map<String, CardRules> rulesByName;
|
||||
@@ -43,87 +38,38 @@ public class TokenDb implements ITokenDatabase {
|
||||
return this.rulesByName.containsKey(rule);
|
||||
|
||||
}
|
||||
@Override
|
||||
public PaperToken getToken(String tokenName) {
|
||||
return getToken(tokenName, CardEdition.UNKNOWN.getName());
|
||||
}
|
||||
|
||||
public void preloadTokens() {
|
||||
for (CardEdition edition : this.editions) {
|
||||
for (Map.Entry<String, Collection<CardEdition.EditionEntry>> inSet : edition.getTokens().asMap().entrySet()) {
|
||||
String name = inSet.getKey();
|
||||
String fullName = String.format("%s_%s", name, edition.getCode().toLowerCase());
|
||||
for (CardEdition.EditionEntry t : inSet.getValue()) {
|
||||
allTokenByName.put(fullName, addTokenInSet(edition, name, t));
|
||||
for (String name : edition.getTokens().keySet()) {
|
||||
try {
|
||||
getToken(name, edition.getCode());
|
||||
} catch(Exception e) {
|
||||
System.out.println(name + "_" + edition.getCode() + " defined in Edition file, but not defined as a token script.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean loadTokenFromSet(CardEdition edition, String name) {
|
||||
String fullName = String.format("%s_%s", name, edition.getCode().toLowerCase());
|
||||
if (allTokenByName.containsKey(fullName)) {
|
||||
return true;
|
||||
}
|
||||
if (!edition.getTokens().containsKey(name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (CardEdition.EditionEntry t : edition.getTokens().get(name)) {
|
||||
allTokenByName.put(fullName, addTokenInSet(edition, name, t));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected PaperToken addTokenInSet(CardEdition edition, String name, CardEdition.EditionEntry t) {
|
||||
CardRules rules;
|
||||
if (rulesByName.containsKey(name)) {
|
||||
rules = rulesByName.get(name);
|
||||
} else if ("w_2_2_spirit".equals(name) || "w_3_3_spirit".equals(name)) { // Hotfix for Endure Token
|
||||
rules = rulesByName.get("w_x_x_spirit");
|
||||
} else {
|
||||
throw new RuntimeException("wrong token name:" + name);
|
||||
}
|
||||
return new PaperToken(rules, edition, name, t.collectorNumber(), t.artistName());
|
||||
}
|
||||
|
||||
// try all editions to find token
|
||||
protected PaperToken fallbackToken(String name) {
|
||||
for (CardEdition edition : this.editions) {
|
||||
String fullName = String.format("%s_%s", name, edition.getCode().toLowerCase());
|
||||
if (loadTokenFromSet(edition, name)) {
|
||||
return Aggregates.random(allTokenByName.get(fullName));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PaperToken getToken(String tokenName) {
|
||||
return getToken(tokenName, CardEdition.UNKNOWN.getCode());
|
||||
}
|
||||
|
||||
@Override
|
||||
public PaperToken getToken(String tokenName, String edition) {
|
||||
CardEdition realEdition = editions.getEditionByCodeOrThrow(edition);
|
||||
String fullName = String.format("%s_%s", tokenName, realEdition.getCode().toLowerCase());
|
||||
String fullName = String.format("%s_%s", tokenName, edition.toLowerCase());
|
||||
|
||||
// token exist in Set, return one at random
|
||||
if (loadTokenFromSet(realEdition, tokenName)) {
|
||||
return Aggregates.random(allTokenByName.get(fullName));
|
||||
}
|
||||
PaperToken fallback = this.fallbackToken(tokenName);
|
||||
if (fallback != null) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
if (!extraTokensByName.containsKey(fullName)) {
|
||||
if (!tokensByName.containsKey(fullName)) {
|
||||
try {
|
||||
PaperToken pt = new PaperToken(rulesByName.get(tokenName), realEdition, tokenName, "", IPaperCard.NO_ARTIST_NAME);
|
||||
extraTokensByName.put(fullName, pt);
|
||||
PaperToken pt = new PaperToken(rulesByName.get(tokenName), editions.get(edition), tokenName);
|
||||
tokensByName.put(fullName, pt);
|
||||
return pt;
|
||||
} catch(Exception e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
return extraTokensByName.get(fullName);
|
||||
return tokensByName.get(fullName);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -173,7 +119,7 @@ public class TokenDb implements ITokenDatabase {
|
||||
|
||||
@Override
|
||||
public List<PaperToken> getAllTokens() {
|
||||
return new ArrayList<>(allTokenByName.values());
|
||||
return new ArrayList<>(tokensByName.values());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -193,6 +139,6 @@ public class TokenDb implements ITokenDatabase {
|
||||
|
||||
@Override
|
||||
public Iterator<PaperToken> iterator() {
|
||||
return allTokenByName.values().iterator();
|
||||
return tokensByName.values().iterator();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,6 @@ import forge.item.IPaperCard;
|
||||
import forge.item.PaperCard;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
|
||||
public class ImageUtil {
|
||||
public static float getNearestHQSize(float baseSize, float actualSize) {
|
||||
//get nearest power of actualSize to baseSize so that the image renders good
|
||||
@@ -102,7 +100,7 @@ public class ImageUtil {
|
||||
|
||||
if (includeSet) {
|
||||
String editionAliased = isDownloadUrl ? StaticData.instance().getEditions().getCode2ByCode(edition) : ImageKeys.getSetFolder(edition);
|
||||
if (editionAliased.isEmpty()) //FIXME: Custom Cards Workaround
|
||||
if (editionAliased == "") //FIXME: Custom Cards Workaround
|
||||
editionAliased = edition;
|
||||
return TextUtil.concatNoSpace(editionAliased, "/", fname);
|
||||
} else {
|
||||
@@ -166,7 +164,7 @@ public class ImageUtil {
|
||||
|
||||
public static String getScryfallDownloadUrl(PaperCard cp, String face, String setCode, String langCode, boolean useArtCrop, boolean hyphenateAlchemy){
|
||||
String editionCode;
|
||||
if (setCode != null && !setCode.isEmpty())
|
||||
if ((setCode != null) && (setCode.length() > 0))
|
||||
editionCode = setCode;
|
||||
else
|
||||
editionCode = cp.getEdition().toLowerCase();
|
||||
@@ -194,33 +192,8 @@ public class ImageUtil {
|
||||
String faceParam = "";
|
||||
if (cp.getRules().getOtherPart() != null) {
|
||||
faceParam = (face.equals("back") ? "&face=back" : "&face=front");
|
||||
} else if (cp.getRules().getSplitType() == CardSplitType.Meld
|
||||
&& !cardCollectorNumber.endsWith("a")
|
||||
&& !cardCollectorNumber.endsWith("b")) {
|
||||
// Only the bottom half of a meld card shares a collector number.
|
||||
// Hanweir Garrison EMN already has a appended.
|
||||
cardCollectorNumber += face.equals("back") ? "b" : "a";
|
||||
}
|
||||
|
||||
String cardCollectorNumberEncoded;
|
||||
try {
|
||||
cardCollectorNumberEncoded = URLEncoder.encode(cardCollectorNumber, "UTF-8");
|
||||
} catch (Exception e) {
|
||||
// Unlikely, for the possibility that "UTF-8" is not supported.
|
||||
System.err.println("UTF-8 encoding not supported on this device.");
|
||||
cardCollectorNumberEncoded = cardCollectorNumber;
|
||||
}
|
||||
|
||||
return String.format("%s/%s/%s?format=image&version=%s%s", editionCode, cardCollectorNumberEncoded,
|
||||
langCode, versionParam, faceParam);
|
||||
}
|
||||
|
||||
public static String getScryfallTokenDownloadUrl(String collectorNumber, String setCode, String langCode, String faceParam) {
|
||||
String versionParam = "normal";
|
||||
if (!faceParam.isEmpty()) {
|
||||
faceParam = (faceParam.equals("back") ? "&face=back" : "&face=front");
|
||||
}
|
||||
return String.format("%s/%s/%s?format=image&version=%s%s", setCode, collectorNumber,
|
||||
return String.format("%s/%s/%s?format=image&version=%s%s", editionCode, cardCollectorNumber,
|
||||
langCode, versionParam, faceParam);
|
||||
}
|
||||
|
||||
|
||||
@@ -269,20 +269,13 @@ public class ItemPool<T extends InventoryItem> implements Iterable<Entry<T, Inte
|
||||
// need not set out-of-sync: either remove did set, or nothing was removed
|
||||
}
|
||||
|
||||
public void removeIf(Predicate<T> test) {
|
||||
for (final T item : items.keySet()) {
|
||||
if (test.test(item))
|
||||
remove(item);
|
||||
}
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
items.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object obj) {
|
||||
return (obj instanceof ItemPool ip) &&
|
||||
(this.items.equals(ip.items));
|
||||
return (obj instanceof ItemPool) &&
|
||||
(this.items.equals(((ItemPool)obj).items));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<artifactId>forge</artifactId>
|
||||
<groupId>forge</groupId>
|
||||
<version>${revision}</version>
|
||||
<version>2.0.01</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>forge-game</artifactId>
|
||||
@@ -34,10 +34,5 @@
|
||||
<artifactId>sentry-logback</artifactId>
|
||||
<version>7.15.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jgrapht</groupId>
|
||||
<artifactId>jgrapht-core</artifactId>
|
||||
<version>1.5.2</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
@@ -2,14 +2,12 @@ package forge.game;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import forge.card.CardStateName;
|
||||
import forge.card.MagicColor;
|
||||
@@ -550,6 +548,11 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView,
|
||||
}
|
||||
}
|
||||
|
||||
if (params.containsKey("ActivateNoLoyaltyAbilitiesCondition")) {
|
||||
final Player active = game.getPhaseHandler().getPlayerTurn();
|
||||
return !active.getActivateLoyaltyAbilityThisTurn(this);
|
||||
}
|
||||
|
||||
if (params.containsKey("ClassLevel")) {
|
||||
final int level = getHostCard().getClassLevel();
|
||||
final int levelMin = Integer.parseInt(params.get("ClassLevel"));
|
||||
@@ -566,23 +569,13 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView,
|
||||
return CardView.get(hostCard);
|
||||
}
|
||||
|
||||
protected List<IHasSVars> getSVarFallback(final String name) {
|
||||
List<IHasSVars> result = Lists.newArrayList();
|
||||
|
||||
protected IHasSVars getSVarFallback() {
|
||||
if (this.getKeyword() != null && this.getKeyword().getStatic() != null) {
|
||||
// only do when the keyword has part of the SVar in ins original string
|
||||
if (name == null || this.getKeyword().getOriginal().contains(name)) {
|
||||
// TODO try to add the keyword instead if possible?
|
||||
result.add(this.getKeyword().getStatic());
|
||||
}
|
||||
return this.getKeyword().getStatic();
|
||||
}
|
||||
if (getCardState() != null)
|
||||
result.add(getCardState());
|
||||
result.add(getHostCard());
|
||||
return result;
|
||||
}
|
||||
protected Optional<IHasSVars> findSVar(final String name) {
|
||||
return getSVarFallback(name).stream().filter(f -> f.hasSVar(name)).findFirst();
|
||||
return getCardState();
|
||||
return getHostCard();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -590,12 +583,12 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView,
|
||||
if (sVars.containsKey(name)) {
|
||||
return sVars.get(name);
|
||||
}
|
||||
return findSVar(name).map(o -> o.getSVar(name)).orElse("");
|
||||
return getSVarFallback().getSVar(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasSVar(final String name) {
|
||||
return sVars.containsKey(name) || findSVar(name).isPresent();
|
||||
return sVars.containsKey(name) || getSVarFallback().hasSVar(name);
|
||||
}
|
||||
|
||||
public Integer getSVarInt(final String name) {
|
||||
@@ -610,21 +603,22 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView,
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSVar(final String name, final String value) {
|
||||
public final void setSVar(final String name, final String value) {
|
||||
sVars.put(name, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getSVars() {
|
||||
Map<String, String> res = Maps.newHashMap();
|
||||
// TODO reverse the order
|
||||
for (IHasSVars s : getSVarFallback(null)) {
|
||||
res.putAll(s.getSVars());
|
||||
}
|
||||
Map<String, String> res = Maps.newHashMap(getSVarFallback().getSVars());
|
||||
res.putAll(sVars);
|
||||
return res;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getDirectSVars() {
|
||||
return sVars;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSVars(Map<String, String> newSVars) {
|
||||
sVars = Maps.newTreeMap();
|
||||
|
||||
@@ -122,10 +122,23 @@ public class ForgeScript {
|
||||
}
|
||||
}
|
||||
return found;
|
||||
} else if (property.startsWith("hasAbility")) {
|
||||
String valid = property.substring(11);
|
||||
} else if (property.equals("hasActivatedAbilityWithTapCost")) {
|
||||
for (final SpellAbility sa : cardState.getSpellAbilities()) {
|
||||
if (sa.isValid(valid, sourceController, source, spellAbility)) {
|
||||
if (sa.isActivatedAbility() && sa.getPayCosts().hasTapCost()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} else if (property.equals("hasActivatedAbility")) {
|
||||
for (final SpellAbility sa : cardState.getSpellAbilities()) {
|
||||
if (sa.isActivatedAbility()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} else if (property.equals("hasOtherActivatedAbility")) {
|
||||
for (final SpellAbility sa : cardState.getSpellAbilities()) {
|
||||
if (sa.isActivatedAbility() && !sa.equals(spellAbility)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -191,11 +204,7 @@ public class ForgeScript {
|
||||
return sa.isCraft();
|
||||
} else if (property.equals("Crew")) {
|
||||
return sa.isCrew();
|
||||
} else if (property.equals("Saddle")) {
|
||||
return sa.isKeyword(Keyword.SADDLE);
|
||||
} else if (property.equals("Station")) {
|
||||
return sa.isKeyword(Keyword.STATION);
|
||||
}else if (property.equals("Cycling")) {
|
||||
} else if (property.equals("Cycling")) {
|
||||
return sa.isCycling();
|
||||
} else if (property.equals("Dash")) {
|
||||
return sa.isDash();
|
||||
@@ -207,8 +216,6 @@ public class ForgeScript {
|
||||
return sa.isEternalize();
|
||||
} else if (property.equals("Flashback")) {
|
||||
return sa.isFlashback();
|
||||
} else if (property.equals("Harmonize")) {
|
||||
return sa.isHarmonize();
|
||||
} else if (property.equals("Jumpstart")) {
|
||||
return sa.isJumpstart();
|
||||
} else if (property.equals("Kicked")) {
|
||||
@@ -227,16 +234,12 @@ public class ForgeScript {
|
||||
return sa.isTurnFaceUp();
|
||||
} else if (property.equals("isCastFaceDown")) {
|
||||
return sa.isCastFaceDown();
|
||||
} else if (property.equals("Unearth")) {
|
||||
return sa.isKeyword(Keyword.UNEARTH);
|
||||
} else if (property.equals("Modular")) {
|
||||
return sa.isKeyword(Keyword.MODULAR);
|
||||
} else if (property.equals("Equip")) {
|
||||
return sa.isEquip();
|
||||
} else if (property.equals("Boast")) {
|
||||
return sa.isBoast();
|
||||
} else if (property.equals("Exhaust")) {
|
||||
return sa.isExhaust();
|
||||
} else if (property.equals("Mutate")) {
|
||||
return sa.isMutate();
|
||||
} else if (property.equals("Ninjutsu")) {
|
||||
|
||||
@@ -22,7 +22,6 @@ import com.google.common.collect.HashBasedTable;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Multimap;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.common.collect.Table;
|
||||
import com.google.common.eventbus.EventBus;
|
||||
import forge.GameCommand;
|
||||
@@ -117,8 +116,8 @@ public class Game {
|
||||
private Map<Player, Card> topLibsCast = Maps.newHashMap();
|
||||
private Map<Card, Integer> facedownWhileCasting = Maps.newHashMap();
|
||||
|
||||
private Player initiative;
|
||||
private Player monarch;
|
||||
private Player initiative;
|
||||
private Player monarchBeginTurn;
|
||||
private Player startingPlayer;
|
||||
|
||||
@@ -262,6 +261,7 @@ public class Game {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public void addPlayer(int id, Player player) {
|
||||
playerCache.put(id, player);
|
||||
}
|
||||
@@ -523,7 +523,7 @@ public class Game {
|
||||
* The Direction in which the turn order of this Game currently proceeds.
|
||||
*/
|
||||
public final Direction getTurnOrder() {
|
||||
if (phaseHandler.getPlayerTurn() != null && phaseHandler.getPlayerTurn().isTurnOrderReversed()) {
|
||||
if (phaseHandler.getPlayerTurn() != null && phaseHandler.getPlayerTurn().getAmountOfKeyword("The turn order is reversed.") % 2 == 1) {
|
||||
return turnOrder.getOtherDirection();
|
||||
}
|
||||
return turnOrder;
|
||||
@@ -593,7 +593,7 @@ public class Game {
|
||||
}
|
||||
|
||||
public Zone getZoneOf(final Card card) {
|
||||
return card == null ? null : card.getLastKnownZone();
|
||||
return card.getLastKnownZone();
|
||||
}
|
||||
|
||||
public synchronized CardCollectionView getCardsIn(final ZoneType zone) {
|
||||
@@ -958,9 +958,9 @@ public class Game {
|
||||
// if the player who lost was the Monarch, someone else will be the monarch
|
||||
// TODO need to check rules if it should try the next player if able
|
||||
if (p.equals(getPhaseHandler().getPlayerTurn())) {
|
||||
getAction().becomeMonarch(getNextPlayerAfter(p), p.getMonarchSet());
|
||||
getAction().becomeMonarch(getNextPlayerAfter(p), null);
|
||||
} else {
|
||||
getAction().becomeMonarch(getPhaseHandler().getPlayerTurn(), p.getMonarchSet());
|
||||
getAction().becomeMonarch(getPhaseHandler().getPlayerTurn(), null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -970,22 +970,22 @@ public class Game {
|
||||
// If the player who has the initiative leaves the game on their own turn,
|
||||
// or the active player left the game at the same time, the next player in turn order takes the initiative.
|
||||
if (p.equals(getPhaseHandler().getPlayerTurn())) {
|
||||
getAction().takeInitiative(getNextPlayerAfter(p), p.getInitiativeSet());
|
||||
getAction().takeInitiative(getNextPlayerAfter(p), null);
|
||||
} else {
|
||||
getAction().takeInitiative(getPhaseHandler().getPlayerTurn(), p.getInitiativeSet());
|
||||
getAction().takeInitiative(getPhaseHandler().getPlayerTurn(), null);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove leftover items from
|
||||
getStack().removeInstancesControlledBy(p);
|
||||
|
||||
getTriggerHandler().onPlayerLost(p);
|
||||
|
||||
ingamePlayers.remove(p);
|
||||
lostPlayers.add(p);
|
||||
|
||||
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(p);
|
||||
getTriggerHandler().runTrigger(TriggerType.LosesGame, runParams, false);
|
||||
|
||||
getTriggerHandler().onPlayerLost(p);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1185,12 +1185,6 @@ public class Game {
|
||||
for (Player player : getRegisteredPlayers()) {
|
||||
player.onCleanupPhase();
|
||||
}
|
||||
for (final Card c : getCardsIncludePhasingIn(ZoneType.Battlefield)) {
|
||||
c.onCleanupPhase(getPhaseHandler().getPlayerTurn());
|
||||
}
|
||||
for (final Card card : getCardsInGame()) {
|
||||
card.resetActivationsPerTurn();
|
||||
}
|
||||
}
|
||||
|
||||
public void addCounterAddedThisTurn(Player putter, CounterType cType, Card card, Integer value) {
|
||||
@@ -1207,16 +1201,10 @@ public class Game {
|
||||
|
||||
public int getCounterAddedThisTurn(CounterType cType, String validPlayer, String validCard, Card source, Player sourceController, CardTraitBase ctb) {
|
||||
int result = 0;
|
||||
Set<CounterType> types = null;
|
||||
if (cType == null) {
|
||||
types = countersAddedThisTurn.rowKeySet();
|
||||
} else if (!countersAddedThisTurn.containsRow(cType)) {
|
||||
if (!countersAddedThisTurn.containsRow(cType)) {
|
||||
return result;
|
||||
} else {
|
||||
types = Sets.newHashSet(cType);
|
||||
}
|
||||
for (CounterType type : types) {
|
||||
for (Map.Entry<Player, List<Pair<Card, Integer>>> e : countersAddedThisTurn.row(type).entrySet()) {
|
||||
for (Map.Entry<Player, List<Pair<Card, Integer>>> e : countersAddedThisTurn.row(cType).entrySet()) {
|
||||
if (e.getKey().isValid(validPlayer.split(","), sourceController, source, ctb)) {
|
||||
for (Pair<Card, Integer> p : e.getValue()) {
|
||||
if (p.getKey().isValid(validCard.split(","), sourceController, source, ctb)) {
|
||||
@@ -1225,28 +1213,20 @@ public class Game {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
public int getCounterAddedThisTurn(CounterType cType, Card card) {
|
||||
int result = 0;
|
||||
Set<CounterType> types = null;
|
||||
if (cType == null) {
|
||||
types = countersAddedThisTurn.rowKeySet();
|
||||
} else if (!countersAddedThisTurn.containsRow(cType)) {
|
||||
if (!countersAddedThisTurn.containsRow(cType)) {
|
||||
return result;
|
||||
} else {
|
||||
types = Sets.newHashSet(cType);
|
||||
}
|
||||
for (CounterType type : types) {
|
||||
for (List<Pair<Card, Integer>> l : countersAddedThisTurn.row(type).values()) {
|
||||
for (List<Pair<Card, Integer>> l : countersAddedThisTurn.row(cType).values()) {
|
||||
for (Pair<Card, Integer> p : l) {
|
||||
if (p.getKey().equalsWithGameTimestamp(card)) {
|
||||
result += p.getValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -26,7 +26,6 @@ import forge.game.ability.AbilityFactory;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.ability.SpellAbilityEffect;
|
||||
import forge.game.ability.effects.DetachedCardEffect;
|
||||
import forge.game.card.*;
|
||||
import forge.game.card.CardPlayOption.PayManaCost;
|
||||
import forge.game.cost.Cost;
|
||||
@@ -35,7 +34,6 @@ import forge.game.keyword.KeywordInterface;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerCollection;
|
||||
import forge.game.player.PlayerController;
|
||||
import forge.game.player.PlayerController.FullControlFlag;
|
||||
import forge.game.replacement.ReplacementEffect;
|
||||
import forge.game.replacement.ReplacementHandler;
|
||||
import forge.game.replacement.ReplacementLayer;
|
||||
@@ -43,7 +41,6 @@ import forge.game.spellability.*;
|
||||
import forge.game.staticability.StaticAbility;
|
||||
import forge.game.staticability.StaticAbilityAlternativeCost;
|
||||
import forge.game.staticability.StaticAbilityLayer;
|
||||
import forge.game.staticability.StaticAbilityMode;
|
||||
import forge.game.zone.Zone;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.Aggregates;
|
||||
@@ -124,7 +121,7 @@ public final class GameActionUtil {
|
||||
|
||||
// need to be done there before static abilities does reset the card
|
||||
// These Keywords depend on the Mana Cost of for Split Cards
|
||||
if (sa.isBasicSpell() && !sa.isLandAbility()) {
|
||||
if (sa.isBasicSpell()) {
|
||||
for (final KeywordInterface inst : source.getKeywords()) {
|
||||
final String keyword = inst.getOriginal();
|
||||
|
||||
@@ -184,34 +181,6 @@ public final class GameActionUtil {
|
||||
flashback.setKeyword(inst);
|
||||
flashback.setIntrinsic(inst.isIntrinsic());
|
||||
alternatives.add(flashback);
|
||||
} else if (keyword.startsWith("Harmonize")) {
|
||||
if (!source.isInZone(ZoneType.Graveyard)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (keyword.equals("Harmonize") && source.getManaCost().isNoCost()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
SpellAbility harmonize = null;
|
||||
|
||||
if (keyword.contains(":")) {
|
||||
final String[] k = keyword.split(":");
|
||||
harmonize = sa.copyWithManaCostReplaced(activator, new Cost(k[1], false));
|
||||
String extraParams = k.length > 2 ? k[2] : "";
|
||||
if (!extraParams.isEmpty()) {
|
||||
for (Map.Entry<String, String> param : AbilityFactory.getMapParams(extraParams).entrySet()) {
|
||||
harmonize.putParam(param.getKey(), param.getValue());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
harmonize = sa.copy(activator);
|
||||
}
|
||||
harmonize.setAlternativeCost(AlternativeCost.Harmonize);
|
||||
harmonize.getRestrictions().setZone(ZoneType.Graveyard);
|
||||
harmonize.setKeyword(inst);
|
||||
harmonize.setIntrinsic(inst.isIntrinsic());
|
||||
alternatives.add(harmonize);
|
||||
} else if (keyword.startsWith("Foretell")) {
|
||||
// Foretell cast only from Exile
|
||||
if (!source.isInZone(ZoneType.Exile) || !source.isForetold() || source.enteredThisTurn() ||
|
||||
@@ -369,7 +338,7 @@ public final class GameActionUtil {
|
||||
newSA.setMayPlay(o);
|
||||
|
||||
final StringBuilder sb = new StringBuilder(sa.getDescription());
|
||||
if (!source.equals(host) && host.getRenderForUI()) {
|
||||
if (!source.equals(host) && host.getCardForUi() != null) {
|
||||
sb.append(" by ");
|
||||
if (host.isImmutable() && host.getEffectSource() != null) {
|
||||
sb.append(host.getEffectSource());
|
||||
@@ -419,7 +388,7 @@ public final class GameActionUtil {
|
||||
costSources.addAll(game.getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES));
|
||||
for (final Card ca : costSources) {
|
||||
for (final StaticAbility stAb : ca.getStaticAbilities()) {
|
||||
if (!stAb.checkConditions(StaticAbilityMode.OptionalCost)) {
|
||||
if (!stAb.checkConditions("OptionalCost")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -611,8 +580,9 @@ public final class GameActionUtil {
|
||||
" or greater>";
|
||||
final Cost cost = new Cost(casualtyCost, false);
|
||||
String str = "Pay for Casualty? " + cost.toSimpleString();
|
||||
boolean v = pc.addKeywordCost(sa, cost, ki, str);
|
||||
|
||||
if (pc.addKeywordCost(sa, cost, ki, str)) {
|
||||
if (v) {
|
||||
if (result == null) {
|
||||
result = sa.copy();
|
||||
}
|
||||
@@ -658,7 +628,9 @@ public final class GameActionUtil {
|
||||
final Cost cost = new Cost(k[1], false);
|
||||
String str = "Pay for Offspring? " + cost.toSimpleString();
|
||||
|
||||
if (pc.addKeywordCost(sa, cost, ki, str)) {
|
||||
boolean v = pc.addKeywordCost(sa, cost, ki, str);
|
||||
|
||||
if (v) {
|
||||
if (result == null) {
|
||||
result = sa.copy();
|
||||
}
|
||||
@@ -705,25 +677,6 @@ public final class GameActionUtil {
|
||||
}
|
||||
}
|
||||
|
||||
if (sa.isHarmonize()) {
|
||||
CardCollectionView creatures = activator.getCreaturesInPlay();
|
||||
if (!creatures.isEmpty()) {
|
||||
int max = Aggregates.max(creatures, Card::getNetPower);
|
||||
int n = pc.chooseNumber(sa, "Choose power of creature to tap", 0, max);
|
||||
final String harmonizeCost = "tapXType<1/Creature.powerEQ" + n + "/creature for Harmonize>";
|
||||
final Cost cost = new Cost(harmonizeCost, false);
|
||||
|
||||
if (pc.addKeywordCost(sa, cost, sa.getKeyword(), "Tap creature?")) {
|
||||
if (result == null) {
|
||||
result = sa.copy();
|
||||
}
|
||||
result.getPayCosts().add(cost);
|
||||
reset = true;
|
||||
result.setOptionalKeywordAmount(sa.getKeyword(), n);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (host.isCreature()) {
|
||||
String kw = "As an additional cost to cast creature spells," +
|
||||
" you may pay any amount of mana. If you do, that creature enters " +
|
||||
@@ -882,11 +835,9 @@ public final class GameActionUtil {
|
||||
}
|
||||
|
||||
public static CardCollectionView orderCardsByTheirOwners(Game game, CardCollectionView list, ZoneType dest, SpellAbility sa) {
|
||||
if (list.size() <= 1 &&
|
||||
(sa == null || !sa.getActivatingPlayer().getController().isFullControl(FullControlFlag.LayerTimestampOrder))) {
|
||||
if (list.size() <= 1) {
|
||||
return list;
|
||||
}
|
||||
Card eff = null;
|
||||
CardCollection completeList = new CardCollection();
|
||||
// CR 613.7m use APNAP
|
||||
PlayerCollection players = game.getPlayersInTurnOrder(game.getPhaseHandler().getPlayerTurn());
|
||||
@@ -902,28 +853,12 @@ public final class GameActionUtil {
|
||||
subList.add(c);
|
||||
}
|
||||
}
|
||||
if (sa != null && sa.getActivatingPlayer() == p && sa.hasParam("StaticEffect")) {
|
||||
// create helper card for ordering
|
||||
eff = new DetachedCardEffect(sa.getHostCard(), "Static Effect of " + sa.getHostCard());
|
||||
subList.add(eff);
|
||||
}
|
||||
CardCollectionView subListView = subList;
|
||||
if (subList.size() > 1) {
|
||||
subListView = p.getController().orderMoveToZoneList(subList, dest, sa);
|
||||
}
|
||||
completeList.addAll(subListView);
|
||||
}
|
||||
if (eff != null) {
|
||||
int idx = completeList.indexOf(eff);
|
||||
if (idx < completeList.size() - 1) {
|
||||
// effects with this param have the responsibility to realign it when later cards are reached
|
||||
sa.setSVar("StaticEffectUntilCardID", String.valueOf(completeList.get(idx + 1).getId()));
|
||||
// add generous offset to timestamp, to ensure it applies last compared to cards that were ordered to ETB before it
|
||||
idx += completeList.size() * 2;
|
||||
}
|
||||
sa.setSVar("StaticEffectTimestamp", String.valueOf(game.getNextTimestamp() + idx));
|
||||
completeList.remove(eff);
|
||||
}
|
||||
return completeList;
|
||||
}
|
||||
|
||||
@@ -947,16 +882,14 @@ public final class GameActionUtil {
|
||||
}
|
||||
|
||||
if (fromZone != null && !fromZone.is(ZoneType.None)) { // and not a copy
|
||||
// add back to where it came from, hopefully old state
|
||||
// skip GameAction
|
||||
oldCard.getZone().remove(oldCard);
|
||||
|
||||
// might have been an alternative lki host
|
||||
oldCard = ability.getCardState().getCard();
|
||||
|
||||
oldCard.setCastSA(null);
|
||||
oldCard.setCastFrom(null);
|
||||
|
||||
// add back to where it came from, hopefully old state
|
||||
// skip GameAction
|
||||
oldCard.getZone().remove(oldCard);
|
||||
// in some rare cases the old position no longer exists (Panglacial Wurm + Selvala)
|
||||
Integer newPosition = zonePosition >= 0 ? Math.min(zonePosition, fromZone.size()) : null;
|
||||
fromZone.add(oldCard, newPosition, null, true);
|
||||
|
||||
@@ -23,12 +23,15 @@ package forge.game;
|
||||
public enum GameEndReason {
|
||||
/** The All opponents lost. */
|
||||
AllOpponentsLost,
|
||||
// Noone won
|
||||
/** The Draw. */
|
||||
Draw, // Having little idea how they can reach a draw, so I didn't enumerate
|
||||
// possible reasons here
|
||||
// Special conditions, they force one player to win and thus end the game
|
||||
|
||||
/** Noone won */
|
||||
Draw,
|
||||
|
||||
/** Special conditions, they force one player to win and thus end the game */
|
||||
WinsGameSpellEffect,
|
||||
/** The Wins game spell effect. */
|
||||
WinsGameSpellEffect, // ones that could be both hardcoded (felidar) and
|
||||
// scripted ( such as Mayael's Aria )
|
||||
|
||||
/** Used to end multiplayer games where the all humans have lost or conceded while AIs cannot end match by themselves.*/
|
||||
AllHumansLost,
|
||||
|
||||
@@ -37,11 +37,11 @@ import forge.game.card.CounterEnumType;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.event.GameEventCardAttachment;
|
||||
import forge.game.keyword.Keyword;
|
||||
import forge.game.keyword.KeywordInterface;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.replacement.ReplacementEffect;
|
||||
import forge.game.replacement.ReplacementType;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.staticability.StaticAbilityCantAttach;
|
||||
import forge.game.zone.ZoneType;
|
||||
|
||||
@@ -267,18 +267,15 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
|
||||
}
|
||||
|
||||
protected boolean canBeEnchantedBy(final Card aura) {
|
||||
if (!aura.hasKeyword(Keyword.ENCHANT)) {
|
||||
return false;
|
||||
// TODO need to check for multiple Enchant Keywords
|
||||
|
||||
SpellAbility sa = aura.getFirstAttachSpell();
|
||||
TargetRestrictions tgt = null;
|
||||
if (sa != null) {
|
||||
tgt = sa.getTargetRestrictions();
|
||||
}
|
||||
for (KeywordInterface ki : aura.getKeywords(Keyword.ENCHANT)) {
|
||||
String k = ki.getOriginal();
|
||||
String m[] = k.split(":");
|
||||
String v = m[1];
|
||||
if (!isValid(v.split(","), aura.getController(), aura, null)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
return tgt != null && isValid(tgt.getValidTgts(), aura.getController(), aura, sa);
|
||||
}
|
||||
|
||||
public boolean hasCounters() {
|
||||
@@ -321,20 +318,11 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
|
||||
return canReceiveCounters(CounterType.get(type));
|
||||
}
|
||||
|
||||
public final void addCounter(final CounterType counterType, int n, final Player source, GameEntityCounterTable table) {
|
||||
public final void addCounter(final CounterType counterType, final int n, final Player source, GameEntityCounterTable table) {
|
||||
if (n <= 0 || !canReceiveCounters(counterType)) {
|
||||
// As per rule 107.1b
|
||||
return;
|
||||
}
|
||||
|
||||
Integer max = getCounterMax(counterType);
|
||||
if (max != null) {
|
||||
n = Math.min(n, max - getCounters(counterType));
|
||||
if (n <= 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// doesn't really add counters, but is just a helper to add them to the Table
|
||||
// so the Table can handle the Replacement Effect
|
||||
table.put(source, this, counterType, n);
|
||||
@@ -352,9 +340,6 @@ public abstract class GameEntity extends GameObject implements IIdentifiable {
|
||||
public void addCounterInternal(final CounterEnumType counterType, final int n, final Player source, final boolean fireEvents, GameEntityCounterTable table, Map<AbilityKey, Object> params) {
|
||||
addCounterInternal(CounterType.get(counterType), n, source, fireEvents, table, params);
|
||||
}
|
||||
public Integer getCounterMax(final CounterType counterType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public List<Pair<Integer, Boolean>> getDamageReceivedThisTurn() {
|
||||
return damageReceivedThisTurn;
|
||||
|
||||
@@ -159,17 +159,12 @@ public class GameEntityCounterTable extends ForwardingTable<Optional<Player>, Ga
|
||||
}
|
||||
|
||||
// Add ETB flag
|
||||
Map<AbilityKey, Object> runParams = AbilityKey.newMap();
|
||||
final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
|
||||
runParams.put(AbilityKey.Cause, cause);
|
||||
if (params != null) {
|
||||
runParams.putAll(params);
|
||||
}
|
||||
|
||||
boolean firstTime = false;
|
||||
if (gm.getKey() instanceof Card c) {
|
||||
firstTime = game.getCounterAddedThisTurn(null, c) == 0;
|
||||
}
|
||||
|
||||
// Apply counter after replacement effect
|
||||
for (Map.Entry<Optional<Player>, Map<CounterType, Integer>> e : values.entrySet()) {
|
||||
boolean remember = cause != null && cause.hasParam("RememberPut");
|
||||
@@ -187,13 +182,6 @@ public class GameEntityCounterTable extends ForwardingTable<Optional<Player>, Ga
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result.containsColumn(gm.getKey())) {
|
||||
runParams = AbilityKey.newMap();
|
||||
runParams.put(AbilityKey.Object, gm.getKey());
|
||||
runParams.put(AbilityKey.FirstTime, firstTime);
|
||||
game.getTriggerHandler().runTrigger(TriggerType.CounterTypeAddedAll, runParams, false);
|
||||
}
|
||||
}
|
||||
|
||||
int totalAdded = totalValues();
|
||||
|
||||
@@ -21,7 +21,7 @@ import com.google.common.collect.Lists;
|
||||
import forge.StaticData;
|
||||
import forge.card.CardDb;
|
||||
import forge.card.CardEdition;
|
||||
import forge.card.CardEdition.EditionEntry;
|
||||
import forge.card.CardEdition.CardInSet;
|
||||
import forge.card.CardRarity;
|
||||
import forge.deck.CardPool;
|
||||
import forge.deck.Deck;
|
||||
@@ -156,7 +156,7 @@ public class GameFormat implements Comparable<GameFormat> {
|
||||
for (CardRarity cr: this.getAllowedRarities()) {
|
||||
crp.add(StaticData.instance().getCommonCards().wasPrintedAtRarity(cr));
|
||||
}
|
||||
p = p.and(IterableUtil.<PaperCard>or(crp));
|
||||
p = p.and(IterableUtil.or(crp));
|
||||
}
|
||||
if (!this.getAdditionalCards().isEmpty()) {
|
||||
p = p.or(PaperCardPredicates.names(this.getAdditionalCards()));
|
||||
@@ -226,9 +226,9 @@ public class GameFormat implements Comparable<GameFormat> {
|
||||
for (String setCode : allowedSetCodes_ro) {
|
||||
CardEdition edition = StaticData.instance().getEditions().get(setCode);
|
||||
if (edition != null) {
|
||||
for (EditionEntry card : edition.getAllCardsInSet()) {
|
||||
if (!bannedCardNames_ro.contains(card.name())) {
|
||||
PaperCard pc = commonCards.getCard(card.name(), setCode, card.collectorNumber());
|
||||
for (CardInSet card : edition.getAllCardsInSet()) {
|
||||
if (!bannedCardNames_ro.contains(card.name)) {
|
||||
PaperCard pc = commonCards.getCard(card.name, setCode, card.collectorNumber);
|
||||
if (pc != null) {
|
||||
cards.add(pc);
|
||||
}
|
||||
|
||||
@@ -273,7 +273,8 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
|
||||
}
|
||||
|
||||
String controllerName;
|
||||
if (defender instanceof Card c) {
|
||||
if (defender instanceof Card) {
|
||||
Card c = ((Card)defender);
|
||||
controllerName = c.isBattle() ? c.getProtectingPlayer().getName() : c.getController().getName();
|
||||
} else {
|
||||
controllerName = defender.getName();
|
||||
@@ -304,7 +305,8 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
|
||||
|
||||
@Override
|
||||
public GameLogEntry visit(GameEventCardForetold ev) {
|
||||
return new GameLogEntry(GameLogEntryType.STACK_RESOLVE, ev.toString());
|
||||
String sb = TextUtil.concatWithSpace(ev.activatingPlayer.toString(), "has foretold.");
|
||||
return new GameLogEntry(GameLogEntryType.STACK_RESOLVE, sb);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -312,11 +314,6 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
|
||||
return new GameLogEntry(GameLogEntryType.STACK_RESOLVE, ev.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public GameLogEntry visit(GameEventDoorChanged ev) {
|
||||
return new GameLogEntry(GameLogEntryType.STACK_RESOLVE, ev.toString());
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void recieve(GameEvent ev) {
|
||||
GameLogEntry le = ev.visit(this);
|
||||
|
||||
@@ -33,6 +33,7 @@ public class GameRules {
|
||||
public boolean hasManaBurn() {
|
||||
return manaBurn;
|
||||
}
|
||||
|
||||
public void setManaBurn(final boolean manaBurn) {
|
||||
this.manaBurn = manaBurn;
|
||||
}
|
||||
@@ -40,6 +41,7 @@ public class GameRules {
|
||||
public boolean hasOrderCombatants() {
|
||||
return orderCombatants;
|
||||
}
|
||||
|
||||
public void setOrderCombatants(final boolean ordered) {
|
||||
this.orderCombatants = ordered;
|
||||
}
|
||||
@@ -47,6 +49,7 @@ public class GameRules {
|
||||
public int getPoisonCountersToLose() {
|
||||
return poisonCountersToLose;
|
||||
}
|
||||
|
||||
public void setPoisonCountersToLose(final int amount) {
|
||||
this.poisonCountersToLose = amount;
|
||||
}
|
||||
@@ -54,6 +57,7 @@ public class GameRules {
|
||||
public int getGamesPerMatch() {
|
||||
return gamesPerMatch;
|
||||
}
|
||||
|
||||
public void setGamesPerMatch(final int gamesPerMatch) {
|
||||
this.gamesPerMatch = gamesPerMatch;
|
||||
this.gamesToWinMatch = gamesPerMatch / 2 + 1;
|
||||
@@ -62,6 +66,7 @@ public class GameRules {
|
||||
public boolean useAnte() {
|
||||
return playForAnte;
|
||||
}
|
||||
|
||||
public void setPlayForAnte(final boolean useAnte) {
|
||||
this.playForAnte = useAnte;
|
||||
}
|
||||
@@ -69,6 +74,7 @@ public class GameRules {
|
||||
public boolean getMatchAnteRarity() {
|
||||
return matchAnteRarity;
|
||||
}
|
||||
|
||||
public void setMatchAnteRarity(final boolean matchRarity) {
|
||||
matchAnteRarity = matchRarity;
|
||||
}
|
||||
@@ -76,6 +82,7 @@ public class GameRules {
|
||||
public boolean getSideboardForAI() {
|
||||
return sideboardForAI;
|
||||
}
|
||||
|
||||
public void setSideboardForAI(final boolean sideboard) {
|
||||
sideboardForAI = sideboard;
|
||||
}
|
||||
@@ -83,6 +90,7 @@ public class GameRules {
|
||||
public boolean getAISideboardingEnabled() {
|
||||
return AISideboardingEnabled;
|
||||
}
|
||||
|
||||
public void setAISideboardingEnabled(final boolean aiSideboarding) {
|
||||
AISideboardingEnabled = aiSideboarding;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user