mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-19 20:28:00 +00:00
Compare commits
124 Commits
6d188c09ca
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1089fa002 | ||
|
|
ac331c3281 | ||
|
|
f37488429c | ||
|
|
8f184cc1fa | ||
|
|
60d50eac44 | ||
|
|
9493dd8305 | ||
|
|
b836ac0204 | ||
|
|
8173f9d93c | ||
|
|
d85c1467b6 | ||
|
|
fd1dcba0aa | ||
|
|
a3d19cb195 | ||
|
|
bd0d5d50bc | ||
|
|
5c3f737ae4 | ||
|
|
fda141fb64 | ||
|
|
d88d167c05 | ||
|
|
e1fcd4a599 | ||
|
|
1e409dd116 | ||
|
|
f19a177870 | ||
|
|
6ed977eadd | ||
|
|
316b8cb0f0 | ||
|
|
bf90e89feb | ||
|
|
41fc4e5041 | ||
|
|
d1f461196a | ||
|
|
2117bf6edf | ||
|
|
8129946fdf | ||
|
|
b48b0c16bd | ||
|
|
15946b2c28 | ||
|
|
bbcf49409b | ||
|
|
a7b26a7242 | ||
|
|
8fdafe3d66 | ||
|
|
e95673e0a2 | ||
|
|
41b39e74c3 | ||
|
|
b0d5fc5709 | ||
|
|
7b60e06551 | ||
|
|
c3869f4fba | ||
|
|
82305a3b1a | ||
|
|
edf021949e | ||
|
|
4ba2cb8737 | ||
|
|
9a17a9676c | ||
|
|
0a54cb0f11 | ||
|
|
24beee1d4c | ||
|
|
95fe9d3dd8 | ||
|
|
a24ae850b6 | ||
|
|
0e303c4cc7 | ||
|
|
d0d2baef24 | ||
|
|
2ac56916dd | ||
|
|
8f5276d10d | ||
|
|
c226ec5b1e | ||
|
|
89dfb252e8 | ||
|
|
940264c537 | ||
|
|
95dcd8984e | ||
|
|
37b5503e77 | ||
|
|
70b6ae1461 | ||
|
|
78f4b13744 | ||
|
|
dff971e9e8 | ||
|
|
c049d3c905 | ||
|
|
1dbdd49a99 | ||
|
|
b6c775eec5 | ||
|
|
164d1573e0 | ||
|
|
80ef4c2d19 | ||
|
|
e114819cc4 | ||
|
|
cdfe5ee18b | ||
|
|
f2feb5edf8 | ||
|
|
22a9b173a5 | ||
|
|
435af883c5 | ||
|
|
154b40b24d | ||
|
|
cd09193924 | ||
|
|
ca77564335 | ||
|
|
acf8220a10 | ||
|
|
32d732bbb1 | ||
|
|
e70aa429b6 | ||
|
|
355722516f | ||
|
|
f08d3f6447 | ||
|
|
d7a69b72e4 | ||
|
|
bce5466c62 | ||
|
|
c90ddd1bac | ||
|
|
f12fbb1774 | ||
|
|
c3c74efbe0 | ||
|
|
5fd44c4788 | ||
|
|
539bea6b11 | ||
|
|
40e9480c2c | ||
|
|
3db276f323 | ||
|
|
c9a5fe9135 | ||
|
|
219e3d6182 | ||
|
|
ee7670abf6 | ||
|
|
3736a0392d | ||
|
|
1be45d73dd | ||
|
|
33e7bfae13 | ||
|
|
61e04be781 | ||
|
|
417ec3043e | ||
|
|
30e9cc5254 | ||
|
|
4b89f60513 | ||
|
|
c5ba0f2c21 | ||
|
|
113c422478 | ||
|
|
85d856e332 | ||
|
|
9fe1328452 | ||
|
|
a29ead42b0 | ||
|
|
c765769466 | ||
|
|
d9e356e20c | ||
|
|
843f2de272 | ||
|
|
f27eb1042f | ||
|
|
48be3406c7 | ||
|
|
50a98238d1 | ||
|
|
ec814fc706 | ||
|
|
9248deebdb | ||
|
|
5d91f6a8fb | ||
|
|
02bc97b8d6 | ||
|
|
03c1a0e6f0 | ||
|
|
3399a455ba | ||
|
|
75d717cce9 | ||
|
|
39d35fd016 | ||
|
|
a8ae802474 | ||
|
|
561ad37f4f | ||
|
|
42cb1f55e1 | ||
|
|
01382fbcc4 | ||
|
|
227a1df06c | ||
|
|
01da2df6f0 | ||
|
|
ec84a3f137 | ||
|
|
02e2e713ed | ||
|
|
a21666040c | ||
|
|
fe2bd016b6 | ||
|
|
0bb67ec8d0 | ||
|
|
58cb309940 | ||
|
|
bd04a6f1b9 |
2
.github/workflows/sync-wiki.yml
vendored
2
.github/workflows/sync-wiki.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: fix md links for Gollum
|
- name: fix md links for Gollum
|
||||||
run: find ${{ github.workspace }}/docs/ -type f -name "*.md" -exec sed -i -E 's|(\[[^]]+]\()([^)]+\/)*([^).]+).md\)|\1\3)|g' '{}' \;
|
run: find ${{ github.workspace }}/docs/ -type f -name "*.md" -exec sed -i -E 's|(\[[^]]+]\()([^)]+\/)*([^).]+).md(#)*([[:alnum:]]*)\)|\1\3\4\5)|g' '{}' \;
|
||||||
- name: fix image links for Gollum
|
- name: fix image links for Gollum
|
||||||
run: find ${{ github.workspace }}/docs/ -type f -name "*.png" -exec mv '{}' ${{ github.workspace }}/docs/ \;
|
run: find ${{ github.workspace }}/docs/ -type f -name "*.png" -exec mv '{}' ${{ github.workspace }}/docs/ \;
|
||||||
- uses: Andrew-Chen-Wang/github-wiki-action@v5
|
- uses: Andrew-Chen-Wang/github-wiki-action@v5
|
||||||
|
|||||||
109
CONTRIBUTING.md
109
CONTRIBUTING.md
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
[Official repo](https://github.com/Card-Forge/forge.git).
|
[Official repo](https://github.com/Card-Forge/forge.git).
|
||||||
|
|
||||||
Dev instructions here: [Getting Started](https://github.com/Card-Forge/forge/wiki) (Somewhat outdated)
|
|
||||||
|
|
||||||
## Requirements / Tools
|
## Requirements / Tools
|
||||||
|
|
||||||
- your favourite Java IDE (IntelliJ, Eclipse, VSCodium, Emacs, Vi...)
|
- your favourite Java IDE (IntelliJ, Eclipse, VSCodium, Emacs, Vi...)
|
||||||
@@ -18,11 +16,10 @@ Dev instructions here: [Getting Started](https://github.com/Card-Forge/forge/wik
|
|||||||
|
|
||||||
## Project Quick Setup
|
## Project Quick Setup
|
||||||
|
|
||||||
- Login into GitHub with your user account and fork the project.
|
- Login into GitHub with your user account and fork the project
|
||||||
|
|
||||||
- Clone your forked project to your local machine
|
- Clone your forked project to your local machine
|
||||||
|
- Go to the project location on your machine. Run Maven to download all dependencies and build a snapshot.
|
||||||
- Go to the project location on your machine. Run Maven to download all dependencies and build a snapshot. Example for Windows & Linux: `mvn -U -B clean -P windows-linux install`
|
- Example for Windows & Linux: `mvn -U -B clean -P windows-linux install`
|
||||||
|
|
||||||
## IntelliJ
|
## IntelliJ
|
||||||
|
|
||||||
@@ -30,107 +27,31 @@ IntelliJ is the recommended IDE for Forge development. Quick start guide for [se
|
|||||||
|
|
||||||
## Eclipse
|
## Eclipse
|
||||||
|
|
||||||
Eclipse includes Maven integration so a separate install is not necessary. For other IDEs, your mileage may vary.
|
Eclipse includes Maven integration so a separate install is not necessary.
|
||||||
At this time, Eclipse is not the recommended IDE for Forge development.
|
Google no longer supports Android SDK releases for Eclipse.
|
||||||
|
|
||||||
### Project Setup
|
## Windows
|
||||||
|
|
||||||
- Follow the instructions for cloning from GitHub. You'll need to setup an account and your SSH key.
|
|
||||||
|
|
||||||
If you are on a Windows machine you can use Putty with TortoiseGit for SSH keys. Run puttygen.exe to generate the key -- save the private key and export
|
|
||||||
the OpenSSH public key. If you just leave the dialog open, you can copy and paste the key from it to your GitHub profile under
|
|
||||||
"SSH keys". Run pageant.exe and add the private key generated earlier. TortoiseGit will use this for accessing GitHub.
|
|
||||||
|
|
||||||
- Fork the Forge git repo to your GitHub account.
|
|
||||||
|
|
||||||
- Clone your forked repo to your local machine.
|
|
||||||
|
|
||||||
- Make sure the Java SDK is installed -- not just the JRE. Java 17 or newer required. If you execute `java -version` at the shell or command prompt, it should report version 17 or later.
|
|
||||||
|
|
||||||
- Install Eclipse 2021-12 or later for Java. Launch it.
|
|
||||||
|
|
||||||
- Create a workspace. Go to the workbench. Right-click inside of Package Explorer > Import... > Maven > Existing Maven Projects > Navigate to root path of the local forge repo and
|
|
||||||
ensure everything is checked > Finish.
|
|
||||||
|
|
||||||
- Let Eclipse run through building the project. You may be prompted for resolving any missing Maven plugins -- accept the ones offered. You may see errors appear in the "Problems" tab. These should
|
|
||||||
be automatically resolved as plug-ins are installed and Eclipse continues the build process. If this is the first time for some plug-in installs, Eclipse may prompt you to restart. Do so. Be patient
|
|
||||||
for this first time through.
|
|
||||||
|
|
||||||
- Once everything builds, all errors should disappear. You can now advance to Project launch.
|
|
||||||
|
|
||||||
### Project Launch
|
|
||||||
|
|
||||||
#### Desktop
|
|
||||||
|
|
||||||
This is the standard configuration used for releasing to Windows / Linux / MacOS.
|
|
||||||
|
|
||||||
- Right-click on forge-gui-desktop > Run As... > Java Application > "Main - forge.view" > Ok
|
|
||||||
|
|
||||||
- The familiar Forge splash screen, etc. should appear. Enjoy!
|
|
||||||
|
|
||||||
#### Mobile (Desktop dev)
|
|
||||||
|
|
||||||
This is the configuration used for doing mobile development using the Windows / Linux / MacOS front-end. Knowledge of libgdx is helpful here.
|
|
||||||
|
|
||||||
- Right-click on forge-gui-mobile-dev > Run As... > Java Application > "Main - forge.app" > Ok.
|
|
||||||
|
|
||||||
- A view similar to a mobile phone should appear. Enjoy!
|
|
||||||
|
|
||||||
### Eclipse / Android SDK Integration
|
|
||||||
|
|
||||||
Google no longer supports Android SDK releases for Eclipse. use IntelliJ.
|
|
||||||
|
|
||||||
#### Android SDK
|
|
||||||
|
|
||||||
TBD
|
TBD
|
||||||
|
|
||||||
##### Windows
|
## Linux / Mac OSX
|
||||||
|
|
||||||
TBD
|
TBD
|
||||||
|
|
||||||
##### Linux / Mac OSX
|
### Android Platform
|
||||||
|
|
||||||
TBD
|
In IntelliJ, if the SDK Manager is not already running, go to Tools > Android > Android SDK Manager. Install the following options / versions:
|
||||||
|
|
||||||
#### Android Plugin for Eclipse
|
|
||||||
|
|
||||||
TBD
|
|
||||||
|
|
||||||
#### Android Platform
|
|
||||||
|
|
||||||
In Intellij, if the SDK Manager is not already running, go to Tools > Android > Android SDK Manager. Install the following options / versions:
|
|
||||||
|
|
||||||
- Android SDK Build-tools 35.0.0
|
- Android SDK Build-tools 35.0.0
|
||||||
- Android 15 (API 35) SDK Platform
|
- Android 15 (API 35) SDK Platform
|
||||||
|
|
||||||
#### Proguard update
|
> [!CAUTION]
|
||||||
|
> Be careful about using unsupported api calls e.g. ``StringBuilder.isEmpty()``. Google's documentation for these is sometimes inaccurate.
|
||||||
|
|
||||||
|
### Proguard update
|
||||||
|
|
||||||
Standalone Proguard 7.6.0 is included with the project (proguard.jar) under forge-gui-android > tools and supports up to Java 23 (latest android uses Java 17).
|
Standalone Proguard 7.6.0 is included with the project (proguard.jar) under forge-gui-android > tools and supports up to Java 23 (latest android uses Java 17).
|
||||||
|
|
||||||
#### Android Build
|
|
||||||
|
|
||||||
TBD
|
|
||||||
|
|
||||||
#### Android Deploy
|
|
||||||
|
|
||||||
TBD
|
|
||||||
|
|
||||||
#### Android Debugging
|
|
||||||
|
|
||||||
TBD
|
|
||||||
|
|
||||||
### Windows / Linux SNAPSHOT build
|
|
||||||
|
|
||||||
SNAPSHOT builds can be built via the Maven integration in Eclipse.
|
|
||||||
|
|
||||||
1. Create a Maven build for the forge top-level project. Right-click on the forge project. Run as.. > Maven build...
|
|
||||||
|
|
||||||
- On the Main tab, set Goals: clean install, set Profiles: windows-linux
|
|
||||||
|
|
||||||
2. Run forge Maven build. If everything built, you should see "BUILD SUCCESS" in the Console View.
|
|
||||||
|
|
||||||
The resulting snapshot will be found at: forge-gui-desktop/target/forge-gui-desktop-[version]-SNAPSHOT
|
|
||||||
|
|
||||||
## Card Scripting
|
## Card Scripting
|
||||||
|
|
||||||
Visit [this page](https://github.com/Card-Forge/forge/wiki/Card-scripting-API) for information on scripting.
|
Visit [this page](https://github.com/Card-Forge/forge/wiki/Card-scripting-API) for information on scripting.
|
||||||
@@ -139,6 +60,8 @@ Card scripting resources are found in the forge-gui/res/ path.
|
|||||||
|
|
||||||
## General Notes
|
## General Notes
|
||||||
|
|
||||||
|
Art files need to be copyright-free and they should be in the public domain.
|
||||||
|
|
||||||
### Project Hierarchy
|
### Project Hierarchy
|
||||||
|
|
||||||
Forge is divided into 4 primary projects with additional projects that target specific platform releases. The primary projects are:
|
Forge is divided into 4 primary projects with additional projects that target specific platform releases. The primary projects are:
|
||||||
@@ -193,3 +116,5 @@ Mobile GUI game logic utilizing [libgdx](https://libgdx.badlogicgames.com/) libr
|
|||||||
#### forge-gui-mobile-dev
|
#### forge-gui-mobile-dev
|
||||||
|
|
||||||
Libgdx backend for desktop development for mobile backends. Utilizes LWJGL. Relies on forge-gui-mobile for GUI logic.
|
Libgdx backend for desktop development for mobile backends. Utilizes LWJGL. Relies on forge-gui-mobile for GUI logic.
|
||||||
|
|
||||||
|
#### forge-installer
|
||||||
@@ -32,7 +32,7 @@ Join the **Forge community** on [Discord](https://discord.gg/HcPJNyD66a)!
|
|||||||
4. **Java Requirement:** Ensure you have **Java 17 or later** installed.
|
4. **Java Requirement:** Ensure you have **Java 17 or later** installed.
|
||||||
|
|
||||||
### 📱 Android Installation
|
### 📱 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)_
|
- _(Note: **Android 11** is the minimum requirement with at least **6GB RAM** to run smoothly. You need to enable **"Install unknown apps"** for Forge to initialize and update itself)_
|
||||||
- Download the **APK** from the [Snapshot Build](https://github.com/Card-Forge/forge/releases/tag/daily-snapshots). On the first launch, Forge will automatically download all necessary assets.
|
- 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.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -61,7 +61,7 @@ Test your skills against AI in multiple formats:
|
|||||||
- **Commander**
|
- **Commander**
|
||||||
- **Cube**
|
- **Cube**
|
||||||
|
|
||||||
For comprehensive gameplay instructions, visit our [Gameplay Guide](https://github.com/Card-Forge/forge/wiki/Gameplay-Guide).
|
For comprehensive gameplay instructions, visit our [User Guide](https://github.com/Card-Forge/forge/wiki/User-Guide).
|
||||||
|
|
||||||
<img width="1282" height="752" alt="Sealed" src="https://github.com/user-attachments/assets/ae603dbd-4421-4753-a333-87cb0a28d772" />
|
<img width="1282" height="752" alt="Sealed" src="https://github.com/user-attachments/assets/ae603dbd-4421-4753-a333-87cb0a28d772" />
|
||||||
|
|
||||||
|
|||||||
11
docs/AI.md
11
docs/AI.md
@@ -1,11 +1,14 @@
|
|||||||
# About Forge's Artificial Intelligence
|
# About Forge's Artificial Intelligence
|
||||||
|
|
||||||
The AI is *not* "trained". It uses basic rules and can be easy to overcome knowing it's weaknesses.
|
The AI is *not* "trained". It uses basic rules and can be easy to overcome knowing its weaknesses.
|
||||||
|
|
||||||
The AI is:
|
The AI is:
|
||||||
* Best with Aggro and midrange decks
|
- Best with Aggro and midrange decks
|
||||||
* Poor to Ok in control decks
|
- Poor to Ok in control decks
|
||||||
* Pretty bad for most combo decks
|
- Pretty bad for most combo decks
|
||||||
|
|
||||||
|
The logic is mostly based on heuristics and split between effect APIs and all other ingame decisions. Sometimes there is hardcoded logic for single cards but that's usually not a healthy approach though it can be more justifiable for highly iconic cards.
|
||||||
|
Defining general concepts of smart play can help improve the win rate much easier, e.g. the AI will always attack with creatures that it has temporarily gained control of until end of turn in order not to miss the opportunity and thus waste the control effect.
|
||||||
|
|
||||||
If you want to train a model for the AI, please do. We would love to see something like that implemented in Forge.
|
If you want to train a model for the AI, please do. We would love to see something like that implemented in Forge.
|
||||||
|
|
||||||
|
|||||||
192
docs/Advanced-Search.md
Normal file
192
docs/Advanced-Search.md
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
# Advanced Search
|
||||||
|
|
||||||
|
Forge implements many ways to help you find the cards you want in your ever growing collection.
|
||||||
|
|
||||||
|
Pressing Ctrl+Enter in current search adds another editable search bar.
|
||||||
|
Here's how searching for all Goblins without Haste-related abilities might look:
|
||||||
|

|
||||||
|
|
||||||
|
Click the "X" in the upper right corner of each search widget to remove that filter from the filter stack.
|
||||||
|
|
||||||
|
Find-as-you-type is implemented for Deck Editor tables. Just start typing while the table has focus and the next card with a matching string in its name will be highlighted. If more than one card matches, hit Enter to select the next matching card. A popup panel will appear with the search string so you know what you are searching for. If no cards match the string, the string will be highlighted in red. Find-as-you-type mode is automatically exited after 5 seconds of inactivity, or hit Escape to exit find-as-you-type mode immediately.
|
||||||
|
|
||||||
|
## Additional information
|
||||||
|
|
||||||
|
Another way to filter is using [Scryfall-like syntax](https://scryfall.com/docs/syntax) in the collection search bar.
|
||||||
|
|
||||||
|
If no operators are passed between tokens, Forge will assume it is joined by `and`. For example, `t:cat t:warrior t:creature` will search for "creatures that are a cat **and** a warrior". Make sure to use `|` or `or` for your queries, as well as parentheses `( )` when needed.
|
||||||
|
|
||||||
|
Keywords can be negated by prefixing a minus sign `-`. For example, `t:creature -t:goblin` will search for "creatures that aren't goblins".
|
||||||
|
|
||||||
|
If no keywords are used, Forge will search in their name, type and oracle text for the passed values. For exemple, `(cat | warrior)` will search for cards that has `cat` or `warrior` anywhere in their name, type, or oracle text. Not that it is not bounrd, so it will also match on "catastrophe". This type of search can be negated too. For exemple, `lightning -bolt` will search for card with "lightning and not bolt in their name, types, or oracle text", or `(t:cat | t:warrior) -(orc | angel | phyrexian)` will search for "cat or warrior cards that don't have orc, angel, or phyrexian in their name, types, or oracle text.
|
||||||
|
|
||||||
|
## Implemented keywords
|
||||||
|
|
||||||
|
### Colors
|
||||||
|
|
||||||
|
#### Keyword(s): `color`, `c`
|
||||||
|
|
||||||
|
You can find cards that are a certain color using the `c:` or `color:` keyword. Both keywords accepts full color names like blue or the abbreviated color letters `w`, `u`, `r`, `b` and `g`.
|
||||||
|
|
||||||
|
You can use many nicknames for color sets: all guild names (e.g. `azorius`), all shard names (e.g. `bant`), all college names (e.g., `quandrix`), all wedge names (e.g. `abzan`), and the four-color nicknames `chaos`, `aggression`, `altruism`, `growth`, `artifice` are supported.
|
||||||
|
|
||||||
|
Use `c` or `colorless` to match colorless cards, and `m`, `multi`, or `multicolor` to match multicolor cards.
|
||||||
|
|
||||||
|
You can use comparison expressions (`>`, `<`, `>=`, `<=`, and `!=`) to check against ranges of colors.
|
||||||
|
|
||||||
|
*Exemples:*
|
||||||
|
`c:rg` - Cards that are at least red and green
|
||||||
|
`c!gruul` - Cards that exclusively red and green
|
||||||
|
`color>=uw -c:red` - Cards that are at least white and blue, but not red
|
||||||
|
|
||||||
|
### Card Types
|
||||||
|
|
||||||
|
#### Keyword(s): `type:`, `t:`
|
||||||
|
|
||||||
|
Find cards of a certain card type with the `t:` or `type:` keywords. You can search for any supertype, card type, or subtype.
|
||||||
|
|
||||||
|
Using only partial words is allowed.
|
||||||
|
|
||||||
|
*Exemples:*
|
||||||
|
`t:merfolk t:legend` - Legendary merfolk cards
|
||||||
|
`t:goblin -t:creature` - Goblin cards that aren't creatures
|
||||||
|
|
||||||
|
### Card Text
|
||||||
|
|
||||||
|
#### Keyword(s): `oracle:`, `o:`
|
||||||
|
|
||||||
|
Use the `o:` or `oracle:` keywords to find cards that have specific phrases in their text box.
|
||||||
|
|
||||||
|
You must put quotes `" "` around text with punctuation or spaces.
|
||||||
|
|
||||||
|
*Exemples:*
|
||||||
|
`o:"enters tapped"` - Cards that enter the battlefield tapped
|
||||||
|
|
||||||
|
#### Keyword(s): `keyword:`, `kw:`
|
||||||
|
|
||||||
|
You can use `keyword:` or `kw:` to search for cards with a specific keyword ability.
|
||||||
|
|
||||||
|
> Note: Known to be buggy. You can search by oracle text instead.
|
||||||
|
|
||||||
|
*Exemples:*
|
||||||
|
`kw:flying -t:creature` - Noncreatures that have the flying keyword
|
||||||
|
|
||||||
|
#### Keyword(s): `name:`
|
||||||
|
|
||||||
|
You can find cards with certain words in their name using `name`.
|
||||||
|
|
||||||
|
Supports `!` (exact search), `!=` (doesn't contain), and `:` or `=` (contains).
|
||||||
|
|
||||||
|
*Exemples:*
|
||||||
|
`name!Fire` - The card Fire
|
||||||
|
`name:Phyrexian`- Cards that contain Phyrexian in their name
|
||||||
|
|
||||||
|
#### Keyword(s): `is:vanilla`
|
||||||
|
|
||||||
|
Find vanilla cratures (Creatures with no abilities).
|
||||||
|
|
||||||
|
### Mana Costs
|
||||||
|
|
||||||
|
#### Keyword(s): `manavalue`, `mv`, `cmc`
|
||||||
|
|
||||||
|
You can find cards of a specific mana value with `manavalue`, `mv`, or `cmc`, comparing with a numeric expression (>, <, =, >=, <=, and !=).
|
||||||
|
|
||||||
|
*Exemples:*
|
||||||
|
`c:u mv=5` - Blue cards with mana value 5
|
||||||
|
|
||||||
|
### Power, Toughness, and Loyalty
|
||||||
|
|
||||||
|
#### Keyword(s): `power`, `pow`
|
||||||
|
|
||||||
|
You can use numeric expressions (>, <, =, >=, <=, and !=) to find cards with certain power using `power` or `pow`.
|
||||||
|
|
||||||
|
*Exemples:*
|
||||||
|
`pow>=8` - Cards with 8 or more power
|
||||||
|
`pow>tou c:w t:creature` - White creatures that are top-heavy
|
||||||
|
|
||||||
|
#### Keyword(s): `toughness`, `tou`
|
||||||
|
|
||||||
|
You can use numeric expressions (>, <, =, >=, <=, and !=) to find cards with certain toughness using `toughness` or `tou`.
|
||||||
|
|
||||||
|
*Exemples:*
|
||||||
|
`tou<=4` - Cards with 4 or less thoughness
|
||||||
|
|
||||||
|
#### Keyword(s): `loyalty`, `loy`
|
||||||
|
|
||||||
|
You can use numeric expressions (`>`, `<`, `=`, `>=`, `<=`, and `!=`) to find cards with certain starting loyalty using `loyalty` or `loy`.
|
||||||
|
|
||||||
|
*Exemples:*
|
||||||
|
`t:planeswalker loy=3` - Planeswalkers that start at 3 loyalty
|
||||||
|
|
||||||
|
### Sets (Editions)
|
||||||
|
|
||||||
|
#### Keyword(s): `set:`, `s:`, `edition:`, `e`
|
||||||
|
|
||||||
|
Use `s:`, `e:`, `set:`, or `edition:` to find cards using their Magic set code.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
`e:war` - Cards from War of the Spark
|
||||||
|
|
||||||
|
#### Keyword(s): `in:` (set)
|
||||||
|
|
||||||
|
The `in:` keyword finds cards that appeared in given set code.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
`in:lea` - Find cards that once appeared in Alpha.
|
||||||
|
|
||||||
|
### Rarity
|
||||||
|
|
||||||
|
#### Keyword(s): `rarity:`, `r:`
|
||||||
|
|
||||||
|
Use `r:` or `rarity:` to find cards by their print rarity. You can search for `land` (`l`) (usually only basic lands), `common` (`c`), `uncommon` (`u`), `rare` (`r`), `mythic` (`m`), and `special` (`s`). You can also use comparison operators like `<` and `>=.`
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
`r:common t:artifact` - Common artifacts
|
||||||
|
`r>=r`- Cards at rare rarity or above (rares and mythics)
|
||||||
|
|
||||||
|
#### Keyword(s): `in:` (rarity)
|
||||||
|
|
||||||
|
You can find cards that have been printed in a given rarity using `in:`
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
`in:rare` - Cards that have been printed at rare.
|
||||||
|
|
||||||
|
### Multi-faced cards
|
||||||
|
|
||||||
|
#### Keyword(s): `is:split`
|
||||||
|
|
||||||
|
Find split-faced cards.
|
||||||
|
|
||||||
|
#### Keyword(s): `is:flip`
|
||||||
|
|
||||||
|
Find flip cards.
|
||||||
|
|
||||||
|
#### Keyword(s): `is:transform`
|
||||||
|
|
||||||
|
Find cards that transform.
|
||||||
|
|
||||||
|
#### Keyword(s): `is:meld`
|
||||||
|
|
||||||
|
Find cards that meld.
|
||||||
|
|
||||||
|
#### Keyword(s) `is:leveler`
|
||||||
|
|
||||||
|
Find cards with Level Up.
|
||||||
|
|
||||||
|
### Others
|
||||||
|
|
||||||
|
#### Keyword(s): `is:modal`
|
||||||
|
|
||||||
|
Find modal cards.
|
||||||
|
|
||||||
|
#### Keyword(s): `is:custom`
|
||||||
|
|
||||||
|
Find cards from custom sets.
|
||||||
|
|
||||||
|
#### Keyword(s) `is:foil`
|
||||||
|
|
||||||
|
Find foil cards.
|
||||||
|
|
||||||
|
#### Keyword(s) `is:nonfoil`
|
||||||
|
|
||||||
|
Find nonfoil cards.
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
Forge provides an in-game console in adventure mode.
|
Forge provides an in-game console in adventure mode.
|
||||||
|
|
||||||
You can access (and close) the console while exploring by pressing F9 (or Fn-F9).
|
You can access (and close) the console while exploring by pressing F9 (or Fn-F9).
|
||||||
|
The equivalent method to access the console on mobile is to hold down the character image in the right top of the screen.
|
||||||
|
Holding the character image again will close the console (as will typing `exit`).
|
||||||
|
|
||||||
To scroll the console window, click and drag the text box.
|
To scroll the console window, click and drag the text box.
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
All Enemies are stored under `res/<AdventureName>/world/enemies.json`
|
All Enemies are stored under `res/<AdventureName>/world/enemies.json`
|
||||||
|
|
||||||
Enemies spawned on the overworld map or on map stages will use this exact template to define their base behavior. These values can be modified or added to with additional settings on an individual enemy basis, details of which can be found within [map instance](Create-new-Maps).
|
Enemies spawned on the overworld map or on map stages will use this exact template to define their base behavior. These values can be modified or added to with additional settings on an individual enemy basis, details of which can be found within [map instance](Create-new-Maps.md).
|
||||||
|
|
||||||
Some ideas for custom enemy cards:
|
Some ideas for custom enemy cards:
|
||||||
- basic (CR or at least Alchemy conform) effects for normal mobs to add flavor
|
- basic (CR or at least Alchemy conform) effects for normal mobs to add flavor
|
||||||
@@ -64,7 +64,7 @@ Supported directions are "Right","Left","Up","Down","RightDown","LeftDown","Left
|
|||||||
Array of strings containing paths to the decks used for this enemy (from `res/<AdventureName>`)
|
Array of strings containing paths to the decks used for this enemy (from `res/<AdventureName>`)
|
||||||
If no decks are defined then the enemy will act like a treasure chest and give the rewards without a fight.
|
If no decks are defined then the enemy will act like a treasure chest and give the rewards without a fight.
|
||||||
(only for enemies in dungeons)
|
(only for enemies in dungeons)
|
||||||
The format for the deck file can be the normal forge *.dck syntax or a json file that will behave like a collection of [rewards](Create-Rewards) to get a random generated deck.
|
The format for the deck file can be the normal forge *.dck syntax or a json file that will behave like a collection of [rewards](Create-Rewards.md) to get a random generated deck.
|
||||||
|
|
||||||
## **randomizeDeck**
|
## **randomizeDeck**
|
||||||
Boolean - if true then the enemy deck will be randomly selected from the deck array. If false, an algorithm will select a deck in sequential order based on the player's prior win/loss ratio against that opponent (discouraged and currently unused due to wild swings in ratio at low game count).
|
Boolean - if true then the enemy deck will be randomly selected from the deck array. If false, an algorithm will select a deck in sequential order based on the player's prior win/loss ratio against that opponent (discouraged and currently unused due to wild swings in ratio at low game count).
|
||||||
@@ -85,17 +85,17 @@ Decimal - Relative frequency with which this enemy will be picked to spawn in ap
|
|||||||
Decimal - Relative estimated difficulty associated with this enemy. Currently unused, but will likely be factored in as a part of filtering enemies into early/late game appropriate opponents. Existing values range from 0 to 1.0.
|
Decimal - Relative estimated difficulty associated with this enemy. Currently unused, but will likely be factored in as a part of filtering enemies into early/late game appropriate opponents. Existing values range from 0 to 1.0.
|
||||||
|
|
||||||
## **speed**
|
## **speed**
|
||||||
Integer - Movement speed of this enemy in overworld or on a [map instance](Create-new-Maps). For comparison, the player's base speed is set at a value of 32 (before any equipment / ability modifiers).
|
Integer - Movement speed of this enemy in overworld or on a [map instance](Create-new-Maps.md). For comparison, the player's base speed is set at a value of 32 (before any equipment / ability modifiers).
|
||||||
|
|
||||||
## **scale**
|
## **scale**
|
||||||
Decimal - Default 1.0. For enemies whose sprites are too large or small for their intended usage, this serves as multiplier for the enemy's visual dimensions & collision area. By default, we work with 16x16 pixel sprites for most entities - this can be replicated with a more detailed 32x32 sprite by setting a scale of 0.5 for the enemy entry.
|
Decimal - Default 1.0. For enemies whose sprites are too large or small for their intended usage, this serves as multiplier for the enemy's visual dimensions & collision area. By default, we work with 16x16 pixel sprites for most entities - this can be replicated with a more detailed 32x32 sprite by setting a scale of 0.5 for the enemy entry.
|
||||||
|
|
||||||
## **life**
|
## **life**
|
||||||
Integer - Base starting life total. This is modified universally by a value determined by the player's chosen difficulty, and can be adjusted further at the enemy object level on [map instances](Create-new-Maps).
|
Integer - Base starting life total. This is modified universally by a value determined by the player's chosen difficulty, and can be adjusted further at the enemy object level on [map instances](Create-new-Maps.md).
|
||||||
|
|
||||||
## **rewards**
|
## **rewards**
|
||||||
Array - A collection of the rewards to be granted for defeating the enemy.
|
Array - A collection of the rewards to be granted for defeating the enemy.
|
||||||
see [Create Rewards](Create-Rewards) for the syntax.
|
see [Create Rewards](Create-Rewards.md) for the syntax.
|
||||||
|
|
||||||
## **equipment**
|
## **equipment**
|
||||||
Array - A collection of strings representing [equipment items](adventure-items) normally intended for player use that this enemy will have. Not used widely, usually when an enemy will drop that [equipment](adventure-items) and it does not use [mana shards](mana-shards).
|
Array - A collection of strings representing [equipment items](adventure-items) normally intended for player use that this enemy will have. Not used widely, usually when an enemy will drop that [equipment](adventure-items) and it does not use [mana shards](mana-shards).
|
||||||
@@ -107,7 +107,7 @@ Valid options are:
|
|||||||
* `item` will give items to be added to the player's inventory.
|
* `item` will give items to be added to the player's inventory.
|
||||||
* `card` will create one or more cards matching a detailed set of filters to follow.
|
* `card` will create one or more cards matching a detailed set of filters to follow.
|
||||||
* `union` is a wrapper for multiple `card` instances that can have mutually exclusive filters.
|
* `union` is a wrapper for multiple `card` instances that can have mutually exclusive filters.
|
||||||
* `deckCard` is only used with rewards from [enemies](Create-Enemies), this functions as a `card` reward that is limited to cards found in that enemy's deck.
|
* `deckCard` is only used with rewards from [enemies](Create-Enemies.md), this functions as a `card` reward that is limited to cards found in that enemy's deck.
|
||||||
|
|
||||||
`{"type": "card", ...}`
|
`{"type": "card", ...}`
|
||||||
|
|
||||||
@@ -516,7 +516,7 @@ Opponent starting life +5
|
|||||||
1. **Cost**: 5000
|
1. **Cost**: 5000
|
||||||
|
|
||||||
#### Blue Staff
|
#### Blue Staff
|
||||||
1. **Effect**: Let's you fly for 5 shards
|
1. **Effect**: Lets you fly for 5 shards
|
||||||
1. **Location** : Capital cities
|
1. **Location** : Capital cities
|
||||||
1. **Cost**: 5000
|
1. **Cost**: 5000
|
||||||
|
|
||||||
@@ -559,4 +559,4 @@ Opponent starting life +5
|
|||||||
|
|
||||||
#### Blue Rune
|
#### Blue Rune
|
||||||
1. **Effect**: Teleports you to the center for 1 shard.
|
1. **Effect**: Teleports you to the center for 1 shard.
|
||||||
1. **Location** : Starting area
|
1. **Location** : Starting area
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
Basic Gamepad Support for Adventure Mode
|
Basic Gamepad Support for Adventure Mode
|
||||||
|
|
||||||
Tested using DS4 Cpntroller on Windows and Android.
|
Tested using DS4 Controller on Windows and Android.
|
||||||
|
|
||||||
If using on Windows OS and you have DS4Windows installed, you might experience dual input because of Emulated/Virtual Controller. To fix this you must use HidHide (better than exclusive mode). Refer to the guide here:
|
If using on Windows OS and you have DS4Windows installed, you might experience dual input because of Emulated/Virtual Controller. To fix this you must use HidHide (better than exclusive mode). Refer to the guide here:
|
||||||
https://vigem.org/projects/HidHide/Simple-Setup-Guide/
|
https://vigem.org/projects/HidHide/Simple-Setup-Guide/
|
||||||
@@ -8,6 +8,10 @@ Primarily there are two types of images you'll care about; cards, and tokens.
|
|||||||
|
|
||||||
**Tokens** - are the images for the cards replacing a generic "1/1 zombie" for example. These are less frequently updated, and are typically the bulk of what is missing when doing an audit. However, these are probably where the more true "custom" replacements are available, with either custom artwork, or modified of other existing.
|
**Tokens** - are the images for the cards replacing a generic "1/1 zombie" for example. These are less frequently updated, and are typically the bulk of what is missing when doing an audit. However, these are probably where the more true "custom" replacements are available, with either custom artwork, or modified of other existing.
|
||||||
|
|
||||||
|
A deck may explicitly define the edition and art variant of each card it includes. If a deck specifies those for no card, Forge uses a special algorithm to determine which card printings were the latest by the moment all of deck's had been printed. These very editions of cards are used when loading deck into memory to reflect the flavour of the season when the deck was built.
|
||||||
|
|
||||||
|
Card images are cleared from memory cache when switching screens and between games.
|
||||||
|
|
||||||
# Downloading
|
# Downloading
|
||||||
|
|
||||||
Due to charges in Forges hosting and scryfall terms you can no longer predownload card images. Turn on auto download in forge to download card images when first viewed.
|
Due to charges in Forges hosting and scryfall terms you can no longer predownload card images. Turn on auto download in forge to download card images when first viewed.
|
||||||
@@ -26,6 +30,8 @@ Due to charges in Forges hosting and scryfall terms you can no longer predownloa
|
|||||||
|
|
||||||
**(I'm not gatekeeping, please if you have a private location for bulk downloads or for alternate or custom arts, you can update this wiki too or let us know in the discord. I'll be happy to update the wiki page with additional sources.)**
|
**(I'm not gatekeeping, please if you have a private location for bulk downloads or for alternate or custom arts, you can update this wiki too or let us know in the discord. I'll be happy to update the wiki page with additional sources.)**
|
||||||
|
|
||||||
|
If you have an older Android device for increased performance or to save bandwidth it might be a good idea to use lower resolution images instead: https://www.slightlymagic.net/forum/viewtopic.php?f=15&t=29104
|
||||||
|
|
||||||
# Storage
|
# Storage
|
||||||
|
|
||||||
Card images are stored in `pics/cards`, and tokens in `pics/tokens`, in the Cache folder for forge:
|
Card images are stored in `pics/cards`, and tokens in `pics/tokens`, in the Cache folder for forge:
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
AbilityFactory parses differently from the Keyword parser. Your Ability line will look more like this:
|
AbilityFactory parses differently from the Keyword parser. Your Ability line will look more like this:
|
||||||
|
|
||||||
`A:{AB/SP/DB/ST}$ <AFSubclass> | {Necessary$ Parameters} | {Separated$ By} | {Pipes$ Here} | [Optional$ Values]`
|
`A:<AB/SP/DB/ST>$ <AFSubclass> | <Necessary$ Parameters> | (<Separated$ By> | <Pipes$ Here>) | [Optional$ {Values} [Nested$ Dependency]]`
|
||||||
|
|
||||||
In most cases, each AF subclass implements both the Spell and Ability.
|
The ability types are:
|
||||||
Much of the code is shared, so creating the data object will look very similar.
|
- **AB** for Activated Abilities
|
||||||
|
- **SP** for Spell
|
||||||
|
- **DB** for Drawback and many abilities that are subsidiary to other things, like replacements. They are only used to chain AFs together, and will never be the root AF
|
||||||
|
- **ST** for Static, this gets used in case the API should resolve without using the stack<br /> (e.g. the unique *Circling Vultures* special action is directly implemented in the script this way)
|
||||||
|
|
||||||
- **AB** is for Activated Abilities
|
Syntax definitions like the above will use different symbols to separate the variable parts from the plaintext:
|
||||||
- **SP** is for Spell
|
- angle brackets for mandatory parts
|
||||||
- **DB** is for Drawback and many abilities that are subsidiary to other things, like replacements. They are only used to chain AFs together, and will never be the root AF
|
- square brackets for optional parts
|
||||||
- **ST** is for Static, this gets used in case the API should resolve without using the stack<br> (e.g. the unique *Circling Vultures* special action is directly implemented in the script this way)
|
- round brackets for grouping parts that are exclusive to each other
|
||||||
|
- curly brackets to denote the type of a param
|
||||||
|
|
||||||
>*NOTE:*
|
>*NOTE:*
|
||||||
> - these factories are refactored from time to time (often to adapt to new sets), so while some entries could be slightly outdated, the base information should still be correct
|
> - these factories are refactored from time to time (often to adapt to new sets), so while some entries could be slightly outdated, the base information should still be correct
|
||||||
@@ -20,7 +24,7 @@ Much of the code is shared, so creating the data object will look very similar.
|
|||||||
|
|
||||||
## Cost / UnlessCost
|
## Cost / UnlessCost
|
||||||
|
|
||||||
`Cost$ <AbilityCost>` is the appropriate way to set the cost of the ability. Currently for spells, any additional costs including the original Mana cost need to appear in the Cost parameter in the AbilityFactory. For each card that uses it, the order in which the cost is paid will always be the same.
|
`Cost$ {AbilityCost}` is the appropriate way to set the cost of the ability. Currently for spells, any additional costs including the original Mana cost need to appear in the Cost parameter in the AbilityFactory. For each card that uses it, the order in which the cost is paid will always be the same.
|
||||||
|
|
||||||
Secondary abilities such as the DB executed by triggers or replacements (usually) don't need costs. (This is one reason to use DB over AB in these cases.)
|
Secondary abilities such as the DB executed by triggers or replacements (usually) don't need costs. (This is one reason to use DB over AB in these cases.)
|
||||||
|
|
||||||
@@ -52,9 +56,11 @@ The SpellDescription for secondary abilities (both AB and DB) is now displayed w
|
|||||||
|
|
||||||
## Remember*
|
## Remember*
|
||||||
|
|
||||||
Remembering is often needed when a card becomes a new object, which is then further affected by the ability. Typical example: [Flicker](https://github.com/Card-Forge/forge/blob/master/forge-gui/res/cardsfolder/f/flicker.txt)<br>
|
Remembering is often needed when a card becomes a new object, which is then further affected by the ability. Typical example: [Flicker](https://github.com/Card-Forge/forge/blob/master/forge-gui/res/cardsfolder/f/flicker.txt)<br />
|
||||||
Because cards keep their remembered parts when changing zones manual [cleanup](#Cleanup) is usually required.
|
Because cards keep their remembered parts when changing zones manual [cleanup](#Cleanup) is usually required.
|
||||||
|
|
||||||
|
## Duration
|
||||||
|
|
||||||
## AI params
|
## AI params
|
||||||
|
|
||||||
`IsCurse$ True` - For effects that are normally treated positive e.g. Pump
|
`IsCurse$ True` - For effects that are normally treated positive e.g. Pump
|
||||||
@@ -207,6 +213,8 @@ Attach separates the actually granting of abilities from the attaching to perman
|
|||||||
|
|
||||||
## BecomeMonarch
|
## BecomeMonarch
|
||||||
|
|
||||||
|
No own parameters.
|
||||||
|
|
||||||
## Bond
|
## Bond
|
||||||
|
|
||||||
Soulbonding two creatures. Only used internally by the engine.
|
Soulbonding two creatures. Only used internally by the engine.
|
||||||
@@ -235,7 +243,7 @@ Parameters
|
|||||||
|
|
||||||
## Choose*
|
## Choose*
|
||||||
|
|
||||||
These can be used to chain effects together. However for common cases many effects already support this directly, e.g. `PutCounter | Choices$``.<br>
|
These can be used to chain effects together. However for common cases many effects already support this directly, e.g. `PutCounter | Choices$``.<br />
|
||||||
Besides making the script shorter using such shortcuts usually also helps the AI making better use of the effect.
|
Besides making the script shorter using such shortcuts usually also helps the AI making better use of the effect.
|
||||||
|
|
||||||
### ChooseType
|
### ChooseType
|
||||||
@@ -295,6 +303,8 @@ Parameters:
|
|||||||
|
|
||||||
### ControlExchange
|
### ControlExchange
|
||||||
|
|
||||||
|
### ControlPlayer
|
||||||
|
|
||||||
### ControlSpell
|
### ControlSpell
|
||||||
|
|
||||||
## Copy*
|
## Copy*
|
||||||
@@ -304,7 +314,6 @@ Parameters:
|
|||||||
Copies a permanent on the battlefield.
|
Copies a permanent on the battlefield.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
|
|
||||||
- NumCopies - optional - the number of copies to put onto the
|
- NumCopies - optional - the number of copies to put onto the
|
||||||
battlefield. Supports SVar:X:????.
|
battlefield. Supports SVar:X:????.
|
||||||
- Keywords - optional - a list of keywords to add to the copies
|
- Keywords - optional - a list of keywords to add to the copies
|
||||||
@@ -315,7 +324,9 @@ Parameters:
|
|||||||
|
|
||||||
### CopySpellAbility
|
### CopySpellAbility
|
||||||
|
|
||||||
Copies a spell on the stack (Twincast, etc.).
|
Parameters:
|
||||||
|
- Num$ <Integer>
|
||||||
|
- Restrict$ <String>
|
||||||
|
|
||||||
## Counter
|
## Counter
|
||||||
|
|
||||||
@@ -411,8 +422,6 @@ Remove any type of counter from all valid cards.
|
|||||||
|
|
||||||
### Proliferate
|
### Proliferate
|
||||||
|
|
||||||
No own parameters.
|
|
||||||
|
|
||||||
### MoveCounters
|
### MoveCounters
|
||||||
|
|
||||||
Used for cards that Move Counters on Resolution, requiring the Host card
|
Used for cards that Move Counters on Resolution, requiring the Host card
|
||||||
@@ -510,8 +519,6 @@ Used in the script of *Karn Liberated*
|
|||||||
|
|
||||||
## Goad
|
## Goad
|
||||||
|
|
||||||
## Investigate
|
|
||||||
|
|
||||||
## Mana
|
## Mana
|
||||||
|
|
||||||
For lands or other permanent to produce mana.
|
For lands or other permanent to produce mana.
|
||||||
@@ -717,7 +724,13 @@ player chooses (eg: Burning of Xinye, or Imperial Edict).
|
|||||||
|
|
||||||
## StoreSVar
|
## StoreSVar
|
||||||
|
|
||||||
## Token
|
## Tokens
|
||||||
|
|
||||||
|
### Amass
|
||||||
|
|
||||||
|
### Investigate
|
||||||
|
|
||||||
|
### Token
|
||||||
|
|
||||||
Token simply lets you create tokens of any type.
|
Token simply lets you create tokens of any type.
|
||||||
|
|
||||||
@@ -748,12 +761,14 @@ As another example, here's Mitotic Slimes' use of TokenTriggers$:
|
|||||||
|
|
||||||
## Trigger
|
## Trigger
|
||||||
|
|
||||||
If possible split the SpellDescription$ of the the effect so the part for the trigger can become the StackDescription directly.
|
If possible split the SpellDescription of the effect so the part for the trigger can become the StackDescription directly.
|
||||||
|
|
||||||
### DelayedTrigger
|
### DelayedTrigger
|
||||||
|
|
||||||
### ImmediateTrigger
|
### ImmediateTrigger
|
||||||
|
|
||||||
|
TriggerAmount
|
||||||
|
|
||||||
## Turn structure
|
## Turn structure
|
||||||
|
|
||||||
### AddPhase
|
### AddPhase
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ A reference guide for scripting cards using the API parsed by the Forge engine.
|
|||||||
|
|
||||||
# Base Structure
|
# Base Structure
|
||||||
|
|
||||||
By opening any file in the /res/cardsfolder folder you can see the basic structure of how the data is created.<br>
|
By opening any file in the /res/cardsfolder folder you can see the basic structure of how the data is created.
|
||||||
Here's an example of a vanilla creature:
|
Here's an example of a vanilla creature:
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -13,11 +13,11 @@ PT:2/2
|
|||||||
Oracle:
|
Oracle:
|
||||||
```
|
```
|
||||||
|
|
||||||
The name of this card is Vanilla Creature.<br>
|
The name of this card is Vanilla Creature.
|
||||||
It's casting cost is {2}{G}.<br>
|
It's casting cost is {2}{G}.
|
||||||
It has the types Creature and Beast.<br>
|
It has the types Creature and Beast.
|
||||||
It has a Power-Toughness of 2/2.<br>
|
It has a Power-Toughness of 2/2.
|
||||||
It will not display any additional text in the card's template.<br>
|
It will not display any additional text in the card's template.
|
||||||
|
|
||||||
If a card has two faces, use AlternateMode:{CardStateName} in the front face and separate both by a new line with the text "ALTERNATE".
|
If a card has two faces, use AlternateMode:{CardStateName} in the front face and separate both by a new line with the text "ALTERNATE".
|
||||||
|
|
||||||
@@ -25,23 +25,23 @@ There are a few other properties that will appear in many cards. These are
|
|||||||
|
|
||||||
| Property | Description
|
| Property | Description
|
||||||
| - | -
|
| - | -
|
||||||
|`A`|[Ability effect](AbilityFactory)
|
|`A`|[Ability effect](Card-scripting-API/AbilityFactory.md)
|
||||||
|`AI`|RemoveDeck:<br />* `All`<br />This will prevent the card from appearing in random AI decks. It is applicable for cards the AI can't use at all like Dark Ritual and also for cards that the AI could use, but only ineffectively like Tortoise Formation. The AI won't draft these cards.<br />* `Random`<br /> This will prevent the card from appearing in random decks. It is only applicable for cards that are too narrow for random decks like Root Cage or Into the North. The AI won't draft these cards.<br />* `NonCommander`<br />
|
|`AI`|RemoveDeck:<br />* `All`<br />This will prevent the card from appearing in random AI decks. It is applicable for cards the AI can't use at all and also for cards that the AI could use, but only ineffectively. The AI won't draft these cards.<br />* `Random`<br /> This will prevent the card from appearing in random decks. It is only applicable for cards that are too narrow for random decks like *Root Cage* or *Into the North*. The AI won't draft these cards.<br />* `NonCommander`<br />
|
||||||
|`Colors`|Color(s) of the card<br /><br />When a card's color is determined by a color indicator rather than shards in a mana cost, this property must be defined. If no identifier is needed, this property should be omitted.<br /><br />* `Colors:red` - This is used on Kobolds of Kher Keep, which has a casting cost of {0} and requires a red indicator to make it red.<br /><br />* `Colors:red,green` - Since Arlinn, Embraced by the Moon has no casting cost (it's the back of a double-faced card), the red and green indicator must be included.
|
|`Colors`|Color(s) of the card<br /><br />When a card's color is determined by a color indicator rather than shards in a mana cost, this property must be defined. If no identifier is needed, this property should be omitted.<br /><br />Example:<br />`Colors:red,green` - Since *Arlinn, Embraced by the Moon* has no mana cost (it's the back of a double-faced card), the red and green indicator must be included.
|
||||||
|`DeckHints`|AI-related hints for a deck including this card<br /><br />To improve synergy this will increase the rank of of all other cards that share some of its DeckHints types. This helps with smoothing the selection so cards without these Entries won't be at an unfair disadvantage.<br /><br />The relevant code can be found in the [CardRanker](https://git.cardforge.org/core-developers/forge/-/blob/master/forge-gui/src/main/java/forge/gamemodes/limited/CardRanker.java) class.
|
|`DeckHints`|AI-related hints for a deck including this card<br /><br />To improve synergy this will increase the rank of of all other cards that share some of its DeckHints types. The following types are supported:<br />* Color<br />* Keyword<br />* Name<br />* Type<br /><br />This helps with smoothing the selection so cards without these Entries won't be at an unfair disadvantage.<br /><br />The relevant code can be found in the [CardRanker](https://github.com/Card-Forge/forge/blob/master/forge-gui/src/main/java/forge/gamemodes/limited/CardRanker.java) class.
|
||||||
|`DeckNeeds`|This can be considered a stronger variant when the AI should not put this card into its deck unless it has whatever other type is specified. The way this works is "inverted": it will directly decrease the rank of the card unless other cards are able to satisfy its types.<br />If a card demands more than one kind of type you can reuse it:<br />`DeckNeeds:Type$Human & Type$Warrior` will only find Human Warrior compared to `DeckNeeds:Type$Human\|Warrior` which is either
|
|`DeckNeeds`|This can be considered a stronger variant when the AI should not put this card into its deck unless it has whatever other type is specified. The way this works is "inverted": it will directly decrease the rank of the card unless other cards are able to satisfy its types.<br />If a card demands more than one kind of type you can reuse it:<br />`DeckNeeds:Type$Human & Type$Warrior` will only find Human Warrior compared to `DeckNeeds:Type$Human\|Warrior` which is either
|
||||||
|`DeckHas`|specifies that the deck now has a certain ability (like, token generation or counters) so that the drafting/deckbuilding AI knows that it now meets requirements for DeckHints/DeckNeeds. This is actually very useful since many of these (such as `Ability$Graveyard, Ability$Token, Ability$Counters`) are not deduced by parsing the abilities, so an explicit hint is necessary. Using the other types is also supported in case the implicit parsing wouldn't find it.<br />It doesn't require exact matching to have an effect but cards that care about multiple entries for a given type will be judged higher if a card seems to provide even "more" synergy for it.<br />Example:<br />Chishiro has two abilities so `DeckHas:Ability$Token & Ability$Counters` is used, therefore score for `DeckNeeds:Ability$Token\|Counters` is increased
|
|`DeckHas`|Specifies that the deck now has a certain ability (like, token generation) so that the drafting/deckbuilding AI knows that it now meets requirements for DeckHints/DeckNeeds. This is useful since many of these are not deduced by parsing the abilities, so an explicit hint is necessary. If you want to create a new archetype make sure to tag at least roughly 50 cards to start with and balacing the Has/Needs ratio. Currently used values are:<br />* Counters<br />* Graveyard<br />* Token<br /><br />Using the generic types is also supported in case the implicit parsing wouldn't find it (TokenScript$ is also included).<br />It doesn't require exact matching to have an effect but cards that care about multiple entries for a given type will be judged higher if a card seems to provide even "more" synergy for it.<br /><br />Example:<br />*Chishiro* has two abilities so `DeckHas:Ability$Token & Ability$Counters` is used, therefore score for `DeckNeeds:Ability$Token\|Counters` is increased
|
||||||
|`K`|Keyword (see below)
|
|`K`|Keyword (see below)
|
||||||
|`Loyalty`|Number of starting loyalty counters
|
|`Loyalty`|Number of starting loyalty counters
|
||||||
|`ManaCost`|Cost to cast the card shown in mana shards<br /><br />This property is required. It has a single parameter that is a mana cost.<br /><br />* `ManaCost:no cost` for cards that cannot be cast<br />* `ManaCost:1 W W` sets the casting cost to {1}{W}{W}
|
|`ManaCost`|Cost to cast the card shown in mana shards<br /><br />This property is required. It has a single parameter that is a mana cost.<br /><br />* `ManaCost:no cost` for cards that cannot be cast<br />* `ManaCost:1 W W` sets the casting cost to {1}{W}{W}
|
||||||
|`Name`|Name of the card<br /><br />A string of text that serves as the name of the card. Note that the registered trademark symbol cannot be included, and this property must have at least one character.<br /><br />Example:<br />* `Name:A Display of My Dark Power` sets the card's name to "A Display of My Dark Power"
|
|`Name`|Name of the card<br /><br />A string of text that serves as the name of the card. Note that the registered trademark symbol cannot be included, and this property must have at least one character.<br /><br />Example:<br />* `Name:A Display of My Dark Power` sets the card's name to "A Display of My Dark Power"
|
||||||
|`Oracle`|The current Oracle text used by the card.<br /><br />We actually have a Python Script that runs to be able to fill in this information, so don't worry about manually editing a lot of cards when Wizards decides to change the rules. <br /><br />This field is used by the Deck Editor to allow non-Legendary Creatures to be marked as potential commanders. Make sure "CARDNAME can be your commander." appears in the oracle text.
|
|`Oracle`|The current Oracle text used by the card.<br /><br />We actually have a Python Script that runs to be able to fill in this information, so don't worry about manually editing a lot of cards when Wizards decides to change the rules. <br /><br />This field is used by the Deck Editor to allow non-Legendary Creatures to be marked as potential commanders. Make sure "CARDNAME can be your commander." appears in the oracle text.
|
||||||
|`PT`|Power and toughness
|
|`PT`|Power and toughness
|
||||||
|`R`|[Replacement effect](Replacements)
|
|`R`|[Replacement effect](Card-scripting-API/Replacements.md)
|
||||||
|`S`|[Static ability](static-abilities)
|
|`S`|[Static ability](Card-scripting-API/Statics.md)
|
||||||
|`SVar`|String variable. Used throughout scripting in a handful of different ways.
|
|`SVar`|String variable. Used throughout scripting in a handful of different ways.
|
||||||
|`T`|[Triggered ability](Triggers)
|
|`T`|[Triggered ability](Card-scripting-API/Triggers.md)
|
||||||
|`Text`|Additional text that needs to be displayed on the CardDetailPanel that doesn't have any spell/ability that generates a description for it, for example "CARDNAME can be your commander." or "X can't be 0.".
|
|`Text`|Additional text that needs to be displayed on the CardDetailPanel that doesn't have any spell/ability that generates a description for it, e.g. "CARDNAME can be your commander." or "X can't be 0.".
|
||||||
|`Types`|Card types and subtypes<br /><br />Include all card types and subtypes, separated by spaces.<br /><br />Example:<br />* `Types:Enchantment Artifact Creature Golem` for a card that reads Enchantment Artifact Creature -- Golem
|
|`Types`|Card types and subtypes<br /><br />Include all card types and subtypes, separated by spaces.<br /><br />Example:<br />* `Types:Enchantment Artifact Creature Golem` for a card that reads Enchantment Artifact Creature -- Golem
|
||||||
|
|
||||||
Rarity and Set info are now defined in edition definition files. These can be found at /res/reditions path.
|
Rarity and Set info are now defined in edition definition files. These can be found at /res/reditions path.
|
||||||
@@ -50,6 +50,7 @@ Rarity and Set info are now defined in edition definition files. These can be fo
|
|||||||
- filename: all lowercase, skip special characters, underscore for spaces
|
- filename: all lowercase, skip special characters, underscore for spaces
|
||||||
- Unix(LF) line endings
|
- Unix(LF) line endings
|
||||||
- use empty lines only when separating multiple faces on a card
|
- use empty lines only when separating multiple faces on a card
|
||||||
|
- AI SVars right before the Oracle
|
||||||
- try to avoid writing default params to keep scripts concise
|
- try to avoid writing default params to keep scripts concise
|
||||||
- e.g. just `SP$ Draw` instead of `SP$ Draw | Defined$ You | NumCards$ 1`
|
- e.g. just `SP$ Draw` instead of `SP$ Draw | Defined$ You | NumCards$ 1`
|
||||||
|
|
||||||
@@ -59,67 +60,24 @@ All keywords need to be prepended with "K:" to be parsed correctly. Each keyword
|
|||||||
|
|
||||||
## Keywords without Parameters
|
## Keywords without Parameters
|
||||||
|
|
||||||
This section is for Keywords that require no additional parameters and are one or two words long. Most of these you would see exactly on cards in the game.<br>
|
This section is for Keywords that require no additional parameters and are one or two words long. Most of these you would see exactly on cards in the game.
|
||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
- Cascade
|
- Cascade
|
||||||
- Changeling
|
|
||||||
- Cipher
|
- Cipher
|
||||||
- Conspire
|
|
||||||
- Convoke
|
- Convoke
|
||||||
- Deathtouch
|
|
||||||
- Defender
|
|
||||||
- Delve
|
|
||||||
- Devoid
|
- Devoid
|
||||||
- Double Strike
|
|
||||||
- Epic
|
|
||||||
- Exalted
|
|
||||||
- Fear
|
|
||||||
- First Strike
|
- First Strike
|
||||||
- Flanking
|
|
||||||
- Flash
|
- Flash
|
||||||
- Flying
|
|
||||||
- Forestwalk
|
|
||||||
- Fuse
|
|
||||||
- Haste
|
- Haste
|
||||||
- Hideaway
|
|
||||||
- Horsemanship
|
|
||||||
- Indestructible
|
- Indestructible
|
||||||
- Infect
|
|
||||||
- Intimidate
|
|
||||||
- Islandwalk
|
|
||||||
- Landfall
|
|
||||||
- Legendary landwalk
|
|
||||||
- Lifelink
|
|
||||||
- Living Weapon
|
|
||||||
- Menace
|
|
||||||
- Mentor
|
- Mentor
|
||||||
- Mountainwalk
|
|
||||||
- Nonbasic landwalk
|
|
||||||
- Persist
|
|
||||||
- Plainswalk
|
|
||||||
- Prowess
|
|
||||||
- Provoke
|
- Provoke
|
||||||
- Reach
|
- Reach
|
||||||
- Rebound
|
|
||||||
- Retrace
|
|
||||||
- Riot
|
|
||||||
- Shadow
|
|
||||||
- Shroud
|
- Shroud
|
||||||
- Snow forestwalk
|
|
||||||
- Snow islandwalk
|
|
||||||
- Snow landwalk
|
|
||||||
- Snow mountainwalk
|
|
||||||
- Snow plainswalk
|
|
||||||
- Snow swamp walk
|
|
||||||
- Soulbond
|
|
||||||
- Split second
|
- Split second
|
||||||
- Storm
|
- Storm
|
||||||
- Sunburst
|
|
||||||
- Swampwalk
|
|
||||||
- Totem Armor
|
|
||||||
- Trample
|
- Trample
|
||||||
- Unblockable
|
|
||||||
- Undying
|
- Undying
|
||||||
- Vigilance
|
- Vigilance
|
||||||
- Wither
|
- Wither
|
||||||
@@ -127,7 +85,6 @@ Examples:
|
|||||||
## Keywords with parameters
|
## Keywords with parameters
|
||||||
|
|
||||||
- Adapt:{cost}
|
- Adapt:{cost}
|
||||||
- AdjustLandPlays:{params}
|
|
||||||
- Afterlife:{N}
|
- Afterlife:{N}
|
||||||
- AlternateAdditionalCost:{cost}
|
- AlternateAdditionalCost:{cost}
|
||||||
- Amplify:{cost}:{validType(comma separated)}
|
- Amplify:{cost}:{validType(comma separated)}
|
||||||
@@ -135,9 +92,7 @@ Examples:
|
|||||||
- Bloodthirst:{magnitude}
|
- Bloodthirst:{magnitude}
|
||||||
- Bestow:{cost}
|
- Bestow:{cost}
|
||||||
- Bushido:{magnitude}
|
- Bushido:{magnitude}
|
||||||
- CantBeBlockedByAmount {xMath}
|
|
||||||
- Champion:{validType}
|
- Champion:{validType}
|
||||||
- CostChange:{params}
|
|
||||||
- Crew:{cost}
|
- Crew:{cost}
|
||||||
- Cumulative upkeep:{cost}:{Description}
|
- Cumulative upkeep:{cost}:{Description}
|
||||||
- Cycling:{cost}
|
- Cycling:{cost}
|
||||||
@@ -146,12 +101,9 @@ Examples:
|
|||||||
- Dredge:{magnitude}
|
- Dredge:{magnitude}
|
||||||
- Echo:{cost}
|
- Echo:{cost}
|
||||||
- Emerge:{cost}
|
- Emerge:{cost}
|
||||||
- Enchant {params} \[Curse\]
|
- Enchant:{Type}
|
||||||
- Enchant {Type}
|
|
||||||
- Entwine:{cost}
|
- Entwine:{cost}
|
||||||
- Equip:{cost}
|
- Equip:{cost}
|
||||||
- etbCounter:{CounterType}:{CounterAmount}
|
|
||||||
- ETBReplacement:{Control/Copy/Other}:{AbilitySVar}\[:Optional\]
|
|
||||||
- Evoke:{cost}
|
- Evoke:{cost}
|
||||||
- Fabricate:{cost}
|
- Fabricate:{cost}
|
||||||
- Fading:{FadeCounters}
|
- Fading:{FadeCounters}
|
||||||
@@ -159,13 +111,11 @@ Examples:
|
|||||||
- Foretell:{cost}
|
- Foretell:{cost}
|
||||||
- Fortify:{cost}
|
- Fortify:{cost}
|
||||||
- Graft:{value}
|
- Graft:{value}
|
||||||
- Haunt:{ability}:{Description}
|
- Haunt:{ability}:<Ability>
|
||||||
- Hexproof:{ValidCards}:{Description}
|
- Hexproof:{ValidCards}:[Description]
|
||||||
- Kicker:{cost}
|
- Kicker:{cost}
|
||||||
- Level up:{cost}
|
- Level up:{cost}
|
||||||
- Madness:{cost}
|
- Madness:{cost}
|
||||||
- ManaConvert:
|
|
||||||
- maxLevel:{magnitude}
|
|
||||||
- MayEffectFromOpeningHand:{Effect}
|
- MayEffectFromOpeningHand:{Effect}
|
||||||
- Miracle:{cost}
|
- Miracle:{cost}
|
||||||
- Modular:{magnitude}
|
- Modular:{magnitude}
|
||||||
@@ -176,8 +126,6 @@ Examples:
|
|||||||
- Ninjutsu:{cost}
|
- Ninjutsu:{cost}
|
||||||
- Outlast:{cost}
|
- Outlast:{cost}
|
||||||
- Partner:{CardName}
|
- Partner:{CardName}
|
||||||
- Poisonous {magnitude}
|
|
||||||
- PreventAllDamageBy {ValidCards}
|
|
||||||
- Protection:{ValidCards}:{Description}
|
- Protection:{ValidCards}:{Description}
|
||||||
- Prowl:{cost}
|
- Prowl:{cost}
|
||||||
- Rampage:{magnitude}
|
- Rampage:{magnitude}
|
||||||
@@ -192,7 +140,6 @@ Examples:
|
|||||||
- Toxic:{poisonCounters}
|
- Toxic:{poisonCounters}
|
||||||
- TypeCycling:{Type}:{cost}
|
- TypeCycling:{Type}:{cost}
|
||||||
- Unearth:{cost}
|
- Unearth:{cost}
|
||||||
- UpkeepCost:{cost}
|
|
||||||
- Vanishing:{TimeCounters}
|
- Vanishing:{TimeCounters}
|
||||||
|
|
||||||
## Plaintext keywords
|
## Plaintext keywords
|
||||||
@@ -201,80 +148,12 @@ These are hardcoded but not truly keywords rules-wise and will eventually be tur
|
|||||||
Only listing the most common ones here so you can recognize them.
|
Only listing the most common ones here so you can recognize them.
|
||||||
CARDNAME is replaced by the card's name ingame.
|
CARDNAME is replaced by the card's name ingame.
|
||||||
|
|
||||||
- All creatures able to block CARDNAME do so.
|
|
||||||
- CARDNAME assigns no combat damage
|
|
||||||
- CARDNAME attacks each turn if able.
|
|
||||||
- CARDNAME attacks each combat if able.
|
|
||||||
- CARDNAME blocks each combat if able.
|
|
||||||
- CARDNAME blocks each turn if able.
|
|
||||||
- CARDNAME can attack as though it didn't have defender.
|
|
||||||
- CARDNAME can attack as though it had haste.
|
|
||||||
- CARDNAME can block as though it were untapped.
|
|
||||||
- CARDNAME can block creatures with shadow as though they didn't have shadow.
|
|
||||||
- CARDNAME can block creatures with landwalk abilities as though they didn't have those abilities.
|
|
||||||
- CARDNAME can block only creatures with flying.
|
|
||||||
- CARDNAME can only attack alone.
|
|
||||||
- CARDNAME can't attack.
|
|
||||||
- CARDNAME can't attack if you cast a spell this turn.
|
|
||||||
- CARDNAME can't attack if defending player controls an untapped creature with power {rest of text string}
|
|
||||||
- CARDNAME can't attack or block.
|
|
||||||
- CARDNAME can't attack or block alone.
|
- CARDNAME can't attack or block alone.
|
||||||
- CARDNAME can't be countered.
|
|
||||||
- CARDNAME can't be enchanted.
|
|
||||||
- CARDNAME can't be equipped.
|
|
||||||
- CARDNAME can't be regenerated.
|
|
||||||
- CARDNAME can't be the target of Aura spells.
|
|
||||||
- CARDNAME can't block.
|
|
||||||
- CARDNAME can't block creatures with power {rest of text string}
|
|
||||||
- CARDNAME can't block unless a creature with greater power also blocks.
|
- CARDNAME can't block unless a creature with greater power also blocks.
|
||||||
- CARDNAME can't block unless at least two other creatures block.
|
|
||||||
- CARDNAME can't transform
|
|
||||||
- CARDNAME doesn't untap during your untap step.
|
|
||||||
- CARDNAME enters the battlefield tapped.
|
|
||||||
- CARDNAME is {color}.
|
|
||||||
- CARDNAME must be blocked if able.
|
- CARDNAME must be blocked if able.
|
||||||
- CARDNAME must be blocked by exactly one creature if able.
|
|
||||||
- CARDNAME must be blocked by two or more creatures if able.
|
|
||||||
- CARDNAME can't be blocked unless all creatures defending player controls block it.
|
|
||||||
- CARDNAME's power and toughness are switched
|
|
||||||
- CARDNAME untaps during each other player's untap step.
|
|
||||||
- CARDNAME's activated abilities can't be activated.
|
|
||||||
- Creatures with power greater than CARDNAME's power can't block it.
|
|
||||||
- Creatures can't attack unless their controller pays:{params}
|
|
||||||
- Damage that would be dealt by CARDNAME can't be prevented.
|
|
||||||
- Damage that would reduce your life total to less than 1 reduces it to 1 instead.
|
|
||||||
- Enchant artifact
|
|
||||||
- Enchant creature
|
|
||||||
- Enchant creature with converted mana cost 2 or less
|
|
||||||
- Enchant creature without flying
|
|
||||||
- Enchant red or green creature
|
|
||||||
- Enchant land
|
|
||||||
- Enchant land you control
|
|
||||||
- Enchant tapped creature
|
|
||||||
- No more than one creature can attack each combat.
|
|
||||||
- No more than one creature can block each combat.
|
|
||||||
- No more than two creatures can attack you each combat.
|
|
||||||
- No more than two creatures can block each combat.
|
|
||||||
- Play with your hand revealed.
|
|
||||||
- Prevent all combat damage that would be dealt to and dealt by CARDNAME.
|
|
||||||
- Prevent all combat damage that would be dealt by CARDNAME.
|
|
||||||
- Prevent all combat damage that would be dealt to CARDNAME.
|
|
||||||
- Prevent all damage that would be dealt to and dealt by CARDNAME.
|
|
||||||
- Prevent all damage that would be dealt by CARDNAME.
|
|
||||||
- Prevent all damage that would be dealt to CARDNAME.
|
|
||||||
- Protection from {type}
|
|
||||||
- Remove CARDNAME from your deck before playing if you're not playing for ante.
|
- Remove CARDNAME from your deck before playing if you're not playing for ante.
|
||||||
- Spells and abilities your opponents control can't cause you to sacrifice permanents.
|
|
||||||
- You can't pay life to cast spells or activate abilities.
|
|
||||||
- You can't sacrifice creatures to cast spells or activate abilities.
|
|
||||||
- You can't draw cards.
|
|
||||||
- You can't gain life.
|
|
||||||
- You can't lose the game.
|
|
||||||
- You can't win the game.
|
|
||||||
- You don't lose the game for having 0 or less life.
|
|
||||||
- You may choose not to untap CARDNAME during your untap step.
|
- You may choose not to untap CARDNAME during your untap step.
|
||||||
- You may have CARDNAME assign its combat damage as though it weren't blocked.
|
- CantSearchLibrary
|
||||||
- Your life total can't change.
|
|
||||||
|
|
||||||
# General SVars
|
# General SVars
|
||||||
|
|
||||||
@@ -319,8 +198,6 @@ equipped already (Kor Duelist).
|
|||||||
|
|
||||||
`SVar:EndOfTurnLeavePlay:True`
|
`SVar:EndOfTurnLeavePlay:True`
|
||||||
|
|
||||||
`SVar:maxLevel:`
|
|
||||||
|
|
||||||
`SVar:HasCombatEffect:True`
|
`SVar:HasCombatEffect:True`
|
||||||
|
|
||||||
`SVar:HasAttackEffect:True`
|
`SVar:HasAttackEffect:True`
|
||||||
|
|||||||
23
docs/Card-scripting-API/Statics.md
Normal file
23
docs/Card-scripting-API/Statics.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
There are two major groups of static abilities:
|
||||||
|
|
||||||
|
# Statics for the main 7 layers
|
||||||
|
|
||||||
|
Syntax:
|
||||||
|
`S:Mode$ <Continuous> | <Affected$ {Valid Player/Card}> | <Layer-specific$ Params> | [Description$ {String}]`
|
||||||
|
|
||||||
|
Here's an example for layer 7c:
|
||||||
|
`Affected$ Creature.YouCtrl | AddPower$ 1 | AddToughness$ 1 | Description$ Creatures you control get +1/+1.`
|
||||||
|
|
||||||
|
See [StaticAbility.generateLayer()](https://github.com/Card-Forge/forge/blob/master/forge-game/src/main/java/forge/game/staticability/StaticAbility.java) for the full list of params on each Layer.
|
||||||
|
|
||||||
|
*Note:* Layer 1 is currently only implemented as a resolving effect instead.
|
||||||
|
|
||||||
|
# Statics for the concluding "game rules layer" ([CR 613.11](https://yawgatog.com/resources/magic-rules/#R61311))
|
||||||
|
|
||||||
|
The available effects are defined here: [StaticAbilityMode](https://github.com/Card-Forge/forge/blob/master/forge-game/src/main/java/forge/game/staticability/StaticAbilityMode.java).
|
||||||
|
|
||||||
|
*Note:* some rules-modifying parts are still coded via `Continuous` mode for now, e.g. `SetMaxHandSize$ {Integer}`.
|
||||||
|
|
||||||
|
## Combat
|
||||||
|
|
||||||
|
## Costs
|
||||||
@@ -1,54 +1,6 @@
|
|||||||
# Guide - Creating a Custom Card
|
Using the Forge API you can create your own custom cards and sets. This tutorial will walk you through the process of creating custom cards via the Force Custom Card Script.
|
||||||
|
|
||||||
Using the Forge API you can create your own custom cards and sets. This guide will walk you through the process of creating custom cards via the Force Custom Card Script.
|
If you are trying to script cards for a new set make sure you take advantage of the [Developer Mode](Development/DevMode.md) for testing to try and contribute without any obvious bugs.
|
||||||
|
|
||||||
The next tutorial will walk you through the process of adding your new cards to a custom set.
|
|
||||||
|
|
||||||
|
|
||||||
## File Locations
|
|
||||||
|
|
||||||
### Custom Card Scripts
|
|
||||||
|
|
||||||
Windows:
|
|
||||||
> C:/Users/<your username>/Application Data/Roaming/Forge/custom/cards
|
|
||||||
|
|
||||||
Linux:
|
|
||||||
> ~/.forge/custom/cards
|
|
||||||
|
|
||||||
Mac:
|
|
||||||
> TODO Add filepath
|
|
||||||
|
|
||||||
### Custom Tokens
|
|
||||||
|
|
||||||
Windows:
|
|
||||||
> C:/Users/<your username>/Application Data/Roaming/Forge/custom/tokens
|
|
||||||
|
|
||||||
Linux:
|
|
||||||
> ~/.forge/custom/tokens
|
|
||||||
|
|
||||||
Mac:
|
|
||||||
> TODO Add filepath
|
|
||||||
|
|
||||||
### Custom Editions
|
|
||||||
|
|
||||||
> C:/Users/<your username>/Application Data/Roaming/Forge/custom/editions
|
|
||||||
|
|
||||||
Linux:
|
|
||||||
> ~/.forge/custom/editions
|
|
||||||
|
|
||||||
Mac:
|
|
||||||
> TODO Add filepath
|
|
||||||
|
|
||||||
### Card Images
|
|
||||||
|
|
||||||
Windows:
|
|
||||||
> C:/Users/<your username>/Application Data/Local/Forge/Cache/pics/cards
|
|
||||||
|
|
||||||
Linux:
|
|
||||||
> ~/.cache/forge/pics/cards
|
|
||||||
|
|
||||||
Mac:
|
|
||||||
> TODO Add filepath
|
|
||||||
|
|
||||||
## Creating a New Card
|
## Creating a New Card
|
||||||
|
|
||||||
@@ -84,11 +36,11 @@ Let's break our card down:
|
|||||||
- K - A Keyword that gives our creature an ability.
|
- K - A Keyword that gives our creature an ability.
|
||||||
- Oracle - The actual text that appears on that card.
|
- Oracle - The actual text that appears on that card.
|
||||||
|
|
||||||
For a refrence of possible fields and their descriptions please see the [Card Scripting API](Card-scripting-API) document. Note that maintaining an up to date list of every ability of every Magic Card ever printed is not feasable. The API is meant to serve as a guide and includes most common used information.
|
For a reference of possible fields and their descriptions please see the [Card Scripting API](Card-scripting-API) document. Note that maintaining an up to date list of every ability of every Magic Card ever printed is not feasable. The API is meant to serve as a guide and includes most common used information.
|
||||||
|
|
||||||
3. Add Card to a Set
|
3. Add Card to a Set
|
||||||
|
|
||||||
Now that we have a new card we need to add it to a set so that it will be included in the game. For the purposes of this turorial we'll add out card to an existing set. If you wish to create your own set you can follow this guide: `Guide Coming Soon`
|
Now that we have a new card we need to add it to a set so that it will be included in the game. For the purposes of this tutorial we'll add our card to an existing set. If you wish to create your own set you can follow this [guide](Creating-a-custom-Set.md).
|
||||||
|
|
||||||
Navigate to your Custom Editions folder and open the text file for the set you would like to add your card to. Let's add our card to the "DCI Promos" set.
|
Navigate to your Custom Editions folder and open the text file for the set you would like to add your card to. Let's add our card to the "DCI Promos" set.
|
||||||
|
|
||||||
@@ -171,8 +123,62 @@ Simply save and rename the image to "Goblin Card Guide.fullborder.jpg" then over
|
|||||||
|
|
||||||
You can check the [Abilities](AbilityFactory) and [Triggers](Triggers) documentation for more information on this topic. These documents are meant as a guide and are unlikely to contain information about every ability in the game.
|
You can check the [Abilities](AbilityFactory) and [Triggers](Triggers) documentation for more information on this topic. These documents are meant as a guide and are unlikely to contain information about every ability in the game.
|
||||||
|
|
||||||
The sinplest method for creating an effect on your card is to find another card that does the same thing and copying the ability. These can be found in your Forge folder:
|
The sinmlest method for creating an effect on your card is to find another card that does the same thing and copying the ability. These can be found in your Forge folder:
|
||||||
|
|
||||||
>./Forge/res/cardsfolder/cardsfolder.zip
|
>./Forge/res/cardsfolder/cardsfolder.zip
|
||||||
|
|
||||||
Unzipping this file will sllow you to search for any card in the containing subfolders.
|
Unzipping this file will sllow you to search for any card in the containing subfolders.
|
||||||
|
|
||||||
|
# Custom mechanics
|
||||||
|
|
||||||
|
We don't accept new mechanics from outside of official Cards into the main repository. This restriction is needed to keep the engine healthy.
|
||||||
|
|
||||||
|
However there is some support to simulate them using named abilities:
|
||||||
|
|
||||||
|
This will support things like being able to target specific SA using a custom name. (Flash on Meditate abilities in this example)
|
||||||
|
|
||||||
|
```text
|
||||||
|
Name:Plo Koon
|
||||||
|
ManaCost:3 W W
|
||||||
|
Types:Legendary Creature KelDor Jedi
|
||||||
|
PT:4/4
|
||||||
|
|
||||||
|
S:Mode$ CastWithFlash | ValidSA$ Activated.NamedAbilityMeditate | Caster$ You | Description$ You may activate meditate abilities any time you could cast an instant.
|
||||||
|
|
||||||
|
A:AB$ ChangeZone | Named$ Meditate | Cost$ 1 W | ActivationZone$ Battlefield | SorcerySpeed$ True | Origin$ Battlefield | Destination$ Hand | Defined$ Self | SpellDescription$ Meditate (Return this creature to its owner's hand. Meditate only as a sorcery.)
|
||||||
|
|
||||||
|
Oracle:You may activate meditate abilities any time you could cast an instant.\nMeditate 1W (Return this creature to its owner's hand. Meditate only as a sorcery.)
|
||||||
|
```
|
||||||
|
|
||||||
|
Restrict trigger to only if was triggered by a specific type of SA (Only Scry when Meditating and not being bounced), and reduce cost for a specific type of ability.
|
||||||
|
|
||||||
|
```text
|
||||||
|
Name:Jedi Training
|
||||||
|
ManaCost:U
|
||||||
|
Types:Enchantment
|
||||||
|
|
||||||
|
S:Mode$ ReduceCost | ValidCard$ Card | ValidSpell$ Activated.NamedAbilityMeditate | Activator$ You | Amount$ 1 | Description$ Meditate abilities you activate cost {1} less to activate.
|
||||||
|
|
||||||
|
T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Hand | TriggerZones$ Battlefield | ValidCard$ Creature.Jedi+YouCtrl | Condition$ FromNamedAbilityMeditate | SubAbility$ DBScry | TriggerDescription$ Whenever a Jedi creature you control meditates, scry 1.
|
||||||
|
SVar:DBScry:DB$ Scry | ScryNum$ 1
|
||||||
|
|
||||||
|
Oracle:Meditate abilities you activate cost {1} less to activate.\nWhenever a Jedi creature you control meditates, scry 1.
|
||||||
|
```
|
||||||
|
|
||||||
|
It will also allow for cards to check if a card was cast using an certain ability using `Count$FromNamedAbility<name>.<true>.<false>`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Name:Chronic Traitor
|
||||||
|
ManaCost:2 B
|
||||||
|
Types:Creature Human Rogue
|
||||||
|
PT:2/1
|
||||||
|
|
||||||
|
T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | TriggerZones$ Battlefield | Execute$ TrigSacrifice | TriggerDescription$ When this creature enters, each player sacrifices a creature. If this creature's paranoia cost was paid, each player sacrifices two creatures instead.
|
||||||
|
SVar:TrigSacrifice:DB$ Sacrifice | Defined$ Player | SacValid$ Creature | Amount$ X
|
||||||
|
SVar:X:Count$FromNamedAbilityParanoia.2.1
|
||||||
|
|
||||||
|
T:Mode$ ChangesZone | TriggerZones$ Hand | ValidCard$ Permanent.YouCtrl | Origin$ Battlefield | Destination$ Any | Execute$ PayParanoia | TriggerDescription$ Paranoia {2}{B}{B} (You may cast this spell for its paranoia cost when a permanent you control leaves the battlefield.)
|
||||||
|
SVar:PayParanoia:DB$ Play | Named$ Paranoia | PlayCost$ 2 B B | ValidSA$ Spell.Self | Controller$ You | ValidZone$ Hand | Optional$ True
|
||||||
|
|
||||||
|
Oracle:When this creature enters, each player sacrifices a creature. If this creature's paranoia cost was paid, each player sacrifices two creatures instead.\nParanoia {2}{B}{B} (You may cast this spell for its paranoia cost when a permanent you control leaves the battlefield.)
|
||||||
|
```
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
# Creating a custom set
|
|
||||||
|
|
||||||
This is a tutorial to start creating your own custom set, or implementing an existing one. We'll take you step by step into implementing a few cards from [MSEM Champions](https://msem-instigator.herokuapp.com/set/MPS_MSE). This is a basic guide to help you get started.
|
This is a tutorial to start creating your own custom set, or implementing an existing one. We'll take you step by step into implementing a few cards from [MSEM Champions](https://msem-instigator.herokuapp.com/set/MPS_MSE). This is a basic guide to help you get started.
|
||||||
|
|
||||||
**Note:** This tutorial is currently for **Windows only**.
|
**Note:** This tutorial is currently for **Windows only**.
|
||||||
@@ -111,11 +109,11 @@ Each line is as follow: `CollectorNumber Rarity CardName @ArtistName`.
|
|||||||
|
|
||||||
> Note: You can put the cards in the list even if they aren't scripted yet. Forge will skip over them.
|
> Note: You can put the cards in the list even if they aren't scripted yet. Forge will skip over them.
|
||||||
|
|
||||||
```
|
```text
|
||||||
[tokens]
|
[tokens]
|
||||||
b_1_1_bird_flying
|
1 b_1_1_bird_flying
|
||||||
b_3_3_cat_deathtouch
|
2 b_3_3_cat_deathtouch
|
||||||
b_5_5_golem_trample
|
3 b_5_5_golem_trample
|
||||||
```
|
```
|
||||||
|
|
||||||
The **[tokens]** section is optional, and only needed if you want to use specific token images in this set. They should be named using the name of their token script. `b_1_1_bird_flying` means it is a black 1/1 bird with flying. More on that later.
|
The **[tokens]** section is optional, and only needed if you want to use specific token images in this set. They should be named using the name of their token script. `b_1_1_bird_flying` means it is a black 1/1 bird with flying. More on that later.
|
||||||
@@ -124,7 +122,7 @@ If you load the game with just file, you'll be able to see that Master Chef, Une
|
|||||||
|
|
||||||
Let's comment out Master Chef to avoid a name conflict with an existing MTG card:
|
Let's comment out Master Chef to avoid a name conflict with an existing MTG card:
|
||||||
|
|
||||||
```
|
```text
|
||||||
[cards]
|
[cards]
|
||||||
#7 M Master Chef
|
#7 M Master Chef
|
||||||
33 M Golden Touch
|
33 M Golden Touch
|
||||||
@@ -133,9 +131,9 @@ Let's comment out Master Chef to avoid a name conflict with an existing MTG card
|
|||||||
|
|
||||||
Save your file, and let's move onto another step.
|
Save your file, and let's move onto another step.
|
||||||
|
|
||||||
## Scripting your first cards
|
> If there is a conflict, you can add something in its name for differenciate it, such as a set tag (ie. `Master Chef (MSEM)`).
|
||||||
|
|
||||||
As mentioned earlier, your custom card rules need to be located inside `%appdata%/Forge/custom/cards`. I recommend creating subfolders for each starting letter (`Forge/custom/cards/a`, `Forge/custom/cards/b`, etc.) to quickly find if a card has a duplicate name.
|
## Scripting your first cards
|
||||||
|
|
||||||
Now, you might remember than Unearth was an existing MTG card so we do not need to create a custom card rule for it. Let's create a few others.
|
Now, you might remember than Unearth was an existing MTG card so we do not need to create a custom card rule for it. Let's create a few others.
|
||||||
> This tutorial will not teach you to script your cards, and they might not be perfect. Please check out [Creating a Custom Card](https://github.com/Card-Forge/forge/wiki/Creating-a-Custom-Card) if you want more info, or look at the existing cards to learn more complex scripting.
|
> This tutorial will not teach you to script your cards, and they might not be perfect. Please check out [Creating a Custom Card](https://github.com/Card-Forge/forge/wiki/Creating-a-Custom-Card) if you want more info, or look at the existing cards to learn more complex scripting.
|
||||||
@@ -143,7 +141,8 @@ Now, you might remember than Unearth was an existing MTG card so we do not need
|
|||||||
Let's create the following files:
|
Let's create the following files:
|
||||||
|
|
||||||
avatar_of_basat.txt
|
avatar_of_basat.txt
|
||||||
```
|
|
||||||
|
```text
|
||||||
Name:Avatar of Basat
|
Name:Avatar of Basat
|
||||||
ManaCost:R
|
ManaCost:R
|
||||||
Types:Creature Avatar
|
Types:Creature Avatar
|
||||||
@@ -154,7 +153,8 @@ Oracle:Menace\nAvatar of Basat can't block.
|
|||||||
```
|
```
|
||||||
|
|
||||||
exhunt.txt
|
exhunt.txt
|
||||||
```
|
|
||||||
|
```text
|
||||||
Name:Exeunt
|
Name:Exeunt
|
||||||
ManaCost:B
|
ManaCost:B
|
||||||
Types:Instant
|
Types:Instant
|
||||||
@@ -164,7 +164,8 @@ Oracle:Each player sacrifices a creature.
|
|||||||
```
|
```
|
||||||
|
|
||||||
fox_of_the_orange_orchard.txt
|
fox_of_the_orange_orchard.txt
|
||||||
```
|
|
||||||
|
```text
|
||||||
Name:Fox of the Orange Orchard
|
Name:Fox of the Orange Orchard
|
||||||
ManaCost:1 W
|
ManaCost:1 W
|
||||||
Types:Creature Fox Spirit
|
Types:Creature Fox Spirit
|
||||||
@@ -173,7 +174,8 @@ Oracle:
|
|||||||
```
|
```
|
||||||
|
|
||||||
inked_summoner.txt
|
inked_summoner.txt
|
||||||
```
|
|
||||||
|
```text
|
||||||
Name:Inked Summoner
|
Name:Inked Summoner
|
||||||
ManaCost:1 B
|
ManaCost:1 B
|
||||||
Types:Creature Human Warlock Artist
|
Types:Creature Human Warlock Artist
|
||||||
@@ -192,7 +194,7 @@ Oracle:At the beginning of your end step, if you lost 2 or more life this turn,
|
|||||||
|
|
||||||
If you load your game now, you should be able to find these cards you just scripted! You'll also notice that Inked Summoner is only listed as a Human Warlock, missing the Artist subtype. That's because Artist is not a real MTG subtype. You can add custom types directing inside the set definition file by following the sections found inside the `res/lists/TypeLists.txt` file. Duplicates will be ignored.
|
If you load your game now, you should be able to find these cards you just scripted! You'll also notice that Inked Summoner is only listed as a Human Warlock, missing the Artist subtype. That's because Artist is not a real MTG subtype. You can add custom types directing inside the set definition file by following the sections found inside the `res/lists/TypeLists.txt` file. Duplicates will be ignored.
|
||||||
|
|
||||||
```
|
```text
|
||||||
[CreatureTypes]
|
[CreatureTypes]
|
||||||
Artist:Artists
|
Artist:Artists
|
||||||
```
|
```
|
||||||
@@ -201,13 +203,12 @@ Oh no! If you play with Inked Summoner now, you will crash when summoning a toke
|
|||||||
|
|
||||||
## Scripting custom tokens
|
## Scripting custom tokens
|
||||||
|
|
||||||
The token scripts are located at `%appdata%/Forge/custom/tokens`.
|
|
||||||
|
|
||||||
Let's add the new tokens we need to make Inked Summoner work!
|
Let's add the new tokens we need to make Inked Summoner work!
|
||||||
> Just like for card scripting, this tutorial will not teach you about scripting them.
|
> Just like for card scripting, this tutorial will not teach you about scripting them.
|
||||||
|
|
||||||
b_1_1_bird.flying.txt
|
b_1_1_bird.flying.txt
|
||||||
```
|
|
||||||
|
```text
|
||||||
Name:Bird Token
|
Name:Bird Token
|
||||||
ManaCost:no cost
|
ManaCost:no cost
|
||||||
Colors:black
|
Colors:black
|
||||||
@@ -218,7 +219,8 @@ Oracle:Flying
|
|||||||
```
|
```
|
||||||
|
|
||||||
b_3_3_cat_deathtouch.txt
|
b_3_3_cat_deathtouch.txt
|
||||||
```
|
|
||||||
|
```text
|
||||||
Name:Cat Token
|
Name:Cat Token
|
||||||
ManaCost:no cost
|
ManaCost:no cost
|
||||||
Colors:black
|
Colors:black
|
||||||
@@ -229,7 +231,8 @@ Oracle:Deathtouch
|
|||||||
```
|
```
|
||||||
|
|
||||||
b_5_5_golem_trample.txt
|
b_5_5_golem_trample.txt
|
||||||
```
|
|
||||||
|
```text
|
||||||
Name:Golem Token
|
Name:Golem Token
|
||||||
ManaCost:no cost
|
ManaCost:no cost
|
||||||
Colors:black
|
Colors:black
|
||||||
@@ -243,9 +246,9 @@ Great! Now Inked Summoner no longer make the game crash! Now let's add some imag
|
|||||||
|
|
||||||
## Adding card and token images
|
## Adding card and token images
|
||||||
|
|
||||||
You can find the card images for the MSEM Champions edition [here](https://msem-instigator.herokuapp.com/set/CHAMPIONS). Find the ones you need and save them inside `%appdata%/../Local/Forge/Cache/pics/cards/MSEM_CHAMPIONS` Remember the filename format should be something like `Swamp.full.jpg` if you only have one variant in your edition. If you have multiples, then it should be something like `Fox of the Orange Orchard1.full.jpg`, `Fox of the Orange Orchard2.full.jpg`, etc. You can find the alternate images from [here](https://msem-instigator.herokuapp.com/set/MPS_MSE) if you want.
|
You can find the card images for the MSEM Champions edition [here](https://msem-instigator.herokuapp.com/set/CHAMPIONS). Find the ones you need and save them inside `%appdata%/../Local/Forge/Cache/pics/cards/MSEM_CHAMPIONS` Remember the filename format should be `{cardname}.fullborder.jpg` if you only have one variant in your edition. If you have multiples, then it should be `{cardname}{number}.fullborder.jpg` (ie. `Fox of the Orange Orchard1.fullborder.jpg`, `Fox of the Orange Orchard2.fullborder.jpg`, etc). You can find the alternate images from [here](https://msem-instigator.herokuapp.com/set/MPS_MSE) if you want.
|
||||||
|
|
||||||
For the tokens, we can deposit them inside `%localappdata%/Forge/Cache/pics/tokens/MSEM_CHAMPIONS`. They should be named the same as their token script so `b_1_1_bird_flying.jpg` and so forth.
|
For the tokens, we can deposit them inside `%localappdata%/Forge/Cache/pics/tokens/MSEM_CHAMPIONS`. They should be named the same as their number + token script so `1_b_1_1_bird_flying.jpg`, `2_b_3_3_cat_deathtouch.jpg`, and so forth.
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
@@ -253,6 +256,15 @@ For the tokens, we can deposit them inside `%localappdata%/Forge/Cache/pics/toke
|
|||||||
|
|
||||||
You can now start your game again, and see that the art loads correctly now.
|
You can now start your game again, and see that the art loads correctly now.
|
||||||
|
|
||||||
## 🎉 Congratulations!
|
## Excursion: Card variants
|
||||||
|
|
||||||
You’ve just added your first custom set in Forge! There's still much more to explore — scripting advanced abilities, custom mechanics, and set structures — but you now have a solid foundation to build from.
|
There are currently multiple ways to specify a flavor name:
|
||||||
|
* Manually, by writing `Variant:Foo:FlavorName:Loret Ipsum` in the card script, and adding `${"variant": "Foo"}` to the end of the edition entry. You'll also want to add `Variant:Foo:Oracle:When Loret Ipsum enters...` if the flavor name would appear in the Oracle text, or if it would otherwise be changed.
|
||||||
|
* By lookup, again by using `Variant:Foo:FlavorName:Loret Ipsum` in the card script, but simply using "Loret Ipsum" as the name in the edition file.
|
||||||
|
* Automatically, by putting `${"flavorName": "Loret Ipsum"}` at the end of the edition entry.
|
||||||
|
|
||||||
|
The third method is the easiest, but besides a simple find/replace for the card name, it won't be able to make any changes to flavor the Oracle text, such as for ability words. They all function the same under the hood once the CardDB is loaded; the latter two are just shortcuts for the first.
|
||||||
|
|
||||||
|
## 🎉 Congratulations
|
||||||
|
|
||||||
|
You’ve just added your first custom set in Forge! There's still much more to explore — scripting advanced abilities, custom mechanics, and set structures — but you now have a solid foundation to build from.
|
||||||
@@ -42,5 +42,3 @@ mvn -U -B clean \\
|
|||||||
-Dcardforge.user=${FORGE_FTP_USER} \\
|
-Dcardforge.user=${FORGE_FTP_USER} \\
|
||||||
-Dcardforge.pass=${FORGE_FTP_PASS}
|
-Dcardforge.pass=${FORGE_FTP_PASS}
|
||||||
```
|
```
|
||||||
|
|
||||||
After this, the appropriate forum posts will need to be made announcing the new release.
|
|
||||||
@@ -2,19 +2,10 @@ Developer Mode is a Mode that allows Developers to try out different
|
|||||||
things and gain different shortcuts during play. In a Normal program,
|
things and gain different shortcuts during play. In a Normal program,
|
||||||
this Mode would be available in a Debug build, but removed from the code
|
this Mode would be available in a Debug build, but removed from the code
|
||||||
during the Retail build. Since Forge is in constant Beta, this Mode is
|
during the Retail build. Since Forge is in constant Beta, this Mode is
|
||||||
available during our Beta releases. On the NewGame Screen, just make
|
available during our Beta releases. You can turn on or off this mode at the
|
||||||
sure it's checked and you'll be able to edit the options at any point
|
Home View -> Game Settings -> Preferences -> Gameplay Options section.
|
||||||
during the game.
|
|
||||||
|
|
||||||
## Lose By Decking
|
It's important to note you won't get achievements in a game once you started cheating.
|
||||||
|
|
||||||
This one is a simple Checkbox. If your Library is Empty and you try to
|
|
||||||
try a card, you normally lose the game. This checkbox should be on by
|
|
||||||
Default (meaning you lose) but if you are running a deck without many
|
|
||||||
cards, uncheck this box so you don't lose when failing to draw a card
|
|
||||||
when you need to.
|
|
||||||
|
|
||||||
\[THIS OPTION NOT READILY VISIBLE IN VERSIONS 1.6.23\]
|
|
||||||
|
|
||||||
## View Zone
|
## View Zone
|
||||||
|
|
||||||
@@ -26,8 +17,7 @@ that Zone are revealed to you.
|
|||||||
## Generate Mana
|
## Generate Mana
|
||||||
|
|
||||||
Useful for not needing Land in your Deck while you are just trying to
|
Useful for not needing Land in your Deck while you are just trying to
|
||||||
test something out, by selecting Generate Mana, 7 of each color and 7
|
test something out, by selecting Generate Mana, 7 of each type is generated and put into your Mana Pool.
|
||||||
colorless is generated and put into your Mana Pool.
|
|
||||||
|
|
||||||
## Setup Game State
|
## Setup Game State
|
||||||
|
|
||||||
@@ -179,5 +169,3 @@ AI hands and leaves them intact, and adds Force of Nature, Raging
|
|||||||
Goblin, and Amulet of Kroog to the computer's graveyard. Does not
|
Goblin, and Amulet of Kroog to the computer's graveyard. Does not
|
||||||
replace either library. Sets the current player to Human and the current
|
replace either library. Sets the current player to Human and the current
|
||||||
phase to Main 1.
|
phase to Main 1.
|
||||||
|
|
||||||
It's important to note you won't get achievements in a game once you started cheating.
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
# Why Rewrite Network Play?
|
|
||||||
|
|
||||||
The current implementation of **Network Play** relies on [Java serialization/deserialization](https://www.geeksforgeeks.org/serialization-in-java/) via [Netty](https://netty.io/). While this does work, it is inefficient, transferring large amounts of unnecessary (duplicate) data. The transferring of duplicate data has two negatives:
|
|
||||||
|
|
||||||
1. increased latency
|
|
||||||
2. increased bandwidth
|
|
||||||
|
|
||||||
The increased latency is very noticeable throughout a network game.
|
|
||||||
The increased bandwidth is a potential concern for mobile players, not everyone has an unlimited data plan.
|
|
||||||
|
|
||||||
Testing of the existing **Network Play** implementation has shown an individual **Game** transferring over 300MB of data.
|
|
||||||
|
|
||||||
# The Rewrite
|
|
||||||
|
|
||||||
The rewrite will utilize [protobuf](https://developers.google.com/protocol-buffers) and be approached in phases:
|
|
||||||
|
|
||||||
1. Lobby
|
|
||||||
2. Match
|
|
||||||
3. Game
|
|
||||||
|
|
||||||
## Lobby
|
|
||||||
|
|
||||||
The **Lobby** portion will handle:
|
|
||||||
|
|
||||||
* Handshake
|
|
||||||
* Player
|
|
||||||
* Name
|
|
||||||
* Avatar
|
|
||||||
* Game Rules Selection
|
|
||||||
* Deck Submission
|
|
||||||
|
|
||||||
The **Handshake** portion of the **Lobby** will be responsible for ensuring that it is a **Forge** client that is connecting **and** that the client is running a compatible **Network Play** implementation.
|
|
||||||
|
|
||||||
## Match
|
|
||||||
|
|
||||||
Number of **Games** that comprise a **Match**, normally first player to win 2 **Games**. This is important, because it is not *technically* best of 3. For example if either of the first two games of a **Match** are a *draw*, it is entirely possible to play a fourth **Game** in a **Match**. (Need a judge ruling reference on this)
|
|
||||||
|
|
||||||
## Game
|
|
||||||
|
|
||||||
This will be broken down in more detail, but the important bits to hand first are:
|
|
||||||
|
|
||||||
* Phases
|
|
||||||
* Passing of **priority**
|
|
||||||
* Notification upon receipt of **priority**
|
|
||||||
@@ -40,7 +40,5 @@ It would be useful for other people to be comfortable with the release process.
|
|||||||
* Create a PR from your branch and get it merged as quickly as you can (ideally before other PRs are merged).
|
* Create a PR from your branch and get it merged as quickly as you can (ideally before other PRs are merged).
|
||||||
* Create a new release from https://github.com/Card-Forge/forge/releases
|
* Create a new release from https://github.com/Card-Forge/forge/releases
|
||||||
* Upload the package and its sha to the create new release page
|
* Upload the package and its sha to the create new release page
|
||||||
|
5. Marketing
|
||||||
|
|
||||||
4. Marketing
|
|
||||||
* Advertise in the #announcements channel in the Discord
|
* Advertise in the #announcements channel in the Discord
|
||||||
BIN
docs/Development/github.png
Normal file
BIN
docs/Development/github.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 57 KiB |
@@ -34,7 +34,7 @@ The "Ancestors" column is basically there to list currently inactive developers
|
|||||||
## Forge Script DSL
|
## Forge Script DSL
|
||||||
| Concept | Owners | Ancestors | Example tasks |
|
| Concept | Owners | Ancestors | Example tasks |
|
||||||
| - | - | - | - |
|
| - | - | - | - |
|
||||||
| [Card Scripting](cardscripting) | TRT, Northmoc, Simisays, Fulgur14, Dracontes | a lot | - implement new Sets<br>- clean up outdated elements<br>- apply Oracle updates |
|
| [Card Scripting](cardscripting) | TRT, Northmoc, Simisays, Fulgur14, Dracontes | a lot | - implement new Sets<br>- review script PR<br>- clean up outdated elements<br>- apply Oracle updates |
|
||||||
| ForgeScribe | | Austinino | |
|
| ForgeScribe | | Austinino | |
|
||||||
|
|
||||||
## Modes
|
## Modes
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ Mostly old-style interface with new age loading screen. Initial card scripting i
|
|||||||
|
|
||||||
Supports over 10,000 cards. We start using a reconstructed UI that allows for better theming. And makes things feel less like an "Application" and more like a "Game"
|
Supports over 10,000 cards. We start using a reconstructed UI that allows for better theming. And makes things feel less like an "Application" and more like a "Game"
|
||||||
|
|
||||||
|
## 2013-10 - ver 1.5.1 is the first with Commander
|
||||||
|
|
||||||
## 2014-05 - Android app is published via Maven
|
## 2014-05 - Android app is published via Maven
|
||||||
|
|
||||||
## 2015-04 - Network play is rudimentary but available
|
## 2015-04 - Network play is rudimentary but available
|
||||||
@@ -36,8 +38,6 @@ Supports over 10,000 cards. We start using a reconstructed UI that allows for be
|
|||||||
|
|
||||||
## 2024-07 - _All non-Un set cards have been added to Forge_
|
## 2024-07 - _All non-Un set cards have been added to Forge_
|
||||||
|
|
||||||
## 2024-08 - Bloomburrow release
|
|
||||||
|
|
||||||
## 2024-09 - Duskmouth Release 1.6.65 (**Last release that supports Java 1.8**)
|
## 2024-09 - Duskmouth Release 1.6.65 (**Last release that supports Java 1.8**)
|
||||||
|
|
||||||
Supports over 28,500 cards.
|
Supports over 28,500 cards.
|
||||||
@@ -68,8 +68,6 @@ For any of you curious about what the first attempt at making a Shandalar-like A
|
|||||||
Interestingly, the above-mentioned thread refers to the proposed game mode as "Adventure Mode" a few times (closer to the end of the thread), something that we actually currently have and actively develop 👍
|
Interestingly, the above-mentioned thread refers to the proposed game mode as "Adventure Mode" a few times (closer to the end of the thread), something that we actually currently have and actively develop 👍
|
||||||
The interesting aspect of that development was the day/night cycle, I don't know why but I remember I thought that it looked quite atmospheric back in the day
|
The interesting aspect of that development was the day/night cycle, I don't know why but I remember I thought that it looked quite atmospheric back in the day
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Abe Sergeant writes about Forge
|
## Abe Sergeant writes about Forge
|
||||||
|
|
||||||
2009-09-11 - Here is the article on star city.
|
2009-09-11 - Here is the article on star city.
|
||||||
@@ -80,15 +78,12 @@ And here is it in archive.org in case that gets taken down
|
|||||||
|
|
||||||
https://web.archive.org/web/20210707215155/https://articles.starcitygames.com/articles/the-kitchen-table-302-an-mtg-forge-quest/
|
https://web.archive.org/web/20210707215155/https://articles.starcitygames.com/articles/the-kitchen-table-302-an-mtg-forge-quest/
|
||||||
|
|
||||||
MTG Forge comes with a Quest Mode. In Quest Mode, you begin with a random selection of cards, and have to build a 60-card deck. Then you play against decks by various computer opponents, and as you win, you get more cards, and the difficulty of your opponents increases.
|
> MTG Forge comes with a Quest Mode. In Quest Mode, you begin with a random selection of cards, and have to build a 60-card deck. Then you play against decks by various computer opponents, and as you win, you get more cards, and the difficulty of your opponents increases.<br />
|
||||||
|
> Quest is the most fun I’ve had playing Magic in a year.
|
||||||
Quest is the most fun I’ve had playing Magic in a year.
|
|
||||||
|
|
||||||
What I am going to do is show you a quick 10-game win in quest. You can play to 10, 20, 30, or 40 wins, but I’m just going to show you a 10 win in quest. I will show you where the game has bugs too, so you can see what I am talking about when I say WIP. I’m not holding anything back.
|
|
||||||
|
|
||||||
# Major disruptions
|
# Major disruptions
|
||||||
|
|
||||||
One interesting thing about Forge is the way it grew. Much of the first few years was solely on the back of an Amateur software engineer called "MTG Rares" soon enough "Dennis Bergkamp" came along and was doing a bunch more development. New Software Engineers joined the ranks off and on. From there on people would jump in, help for a handful of years and get too busy with life, or stop really playing magic or whatever. A handful of us have been around the longhaul, but not too many.
|
One interesting thing about Forge is the way it grew. Much of the first few years was solely on the back of an Amateur software engineer called "MTG Rares". Soon enough "Dennis Bergkamp" came along and was doing a bunch more development. New Software Engineers joined the ranks off and on. From there on people would jump in, help for a handful of years and get too busy with life, or stop really playing magic or whatever. A handful of us have been around the longhaul, but not too many.
|
||||||
|
|
||||||
## Sourceforge SVN (2007-2008)
|
## Sourceforge SVN (2007-2008)
|
||||||
Original location. Moved when the name changed.
|
Original location. Moved when the name changed.
|
||||||
@@ -103,13 +98,11 @@ One interesting thing about Forge is the way it grew. Much of the first few year
|
|||||||
## Github (2021-Present)
|
## Github (2021-Present)
|
||||||
Conversion from Git to Git was a lot easier.
|
Conversion from Git to Git was a lot easier.
|
||||||
|
|
||||||
|
|
||||||
## Jendave's modularization (2011)
|
## Jendave's modularization (2011)
|
||||||
The initial modularization attempt pulled everything from living under forge-gui/ module and built out some of the Maven structure.
|
The initial modularization attempt pulled everything from living under forge-gui/ module and built out some of the Maven structure.
|
||||||
|
|
||||||
## Maxmtg's modularization (2013?)
|
## Maxmtg's modularization (2013?)
|
||||||
Took this the next step further massively reorganizing the codebase. It caused major issues and took a few months to get resolved.
|
Took this the next step further massively reorganizing the codebase. It caused major issues and took a few months to get resolved.
|
||||||
|
|
||||||
## Hanmac's modularization (2017?
|
## Hanmac's modularization (2017?)
|
||||||
This was a smaller modularization mostly within certains areas that ultimately was positive, but led to some headaches during the process.
|
This was a smaller modularization mostly within certains areas that ultimately was positive, but led to some headaches during the process.
|
||||||
|
|
||||||
|
|||||||
@@ -1,35 +1,25 @@
|
|||||||
This is a list of basic troubleshooting questions that come up for most new players, before running to the discord about your issue, please review this FAQ for some of the more common issues.
|
This is a list of basic troubleshooting questions that come up for most new players, before running to the discord about your issue, please review this document for some of the more common issues.
|
||||||
|
|
||||||
### Check the FAQ in Discord
|
|
||||||
|
|
||||||
|
* Check the FAQ in Discord (probably more up-to-date if things break unexpectedly on us)
|
||||||
https://discord.com/channels/267367946135928833/1095026912927154176
|
https://discord.com/channels/267367946135928833/1095026912927154176
|
||||||
|
|
||||||
### Search the help posts in Discord
|
* Search the help posts in Discord
|
||||||
|
|
||||||
https://discord.com/channels/267367946135928833/1047001034788196452
|
https://discord.com/channels/267367946135928833/1047001034788196452
|
||||||
|
|
||||||
### Write a post in help section of Discord
|
|
||||||
|
|
||||||
https://discord.com/channels/267367946135928833/1047001034788196452
|
|
||||||
|
|
||||||
Note: For now, please also check [this](https://www.slightlymagic.net/forum/viewtopic.php?f=26&t=11825) forum topic for some additional information.
|
|
||||||
|
|
||||||
# General
|
# General
|
||||||
|
|
||||||
### How do I download content?
|
## How do I download content?
|
||||||
|
|
||||||
Forge has content downloaders within the app itself, you can use those tools to update the graphics assets. More information about card and token image assets can be found here. [Card Images, Downloading](Card-Images#downloading)
|
Forge has content downloaders within the app itself, you can use those tools to update the graphics assets. More information about card and token image assets can be found here. [Card Images, Downloading](Card-Images.md#downloading)
|
||||||
|
|
||||||
|
## My desktop match/deck view is all messed up?
|
||||||
|
The match and deck editor windows contain panels that can be moved and/or resized. The changes that you make are saved to files that are named "editor.xml" and "match.xml". These files can be found in your userDir/preferences/ directory.
|
||||||
|
|
||||||
|
Sometimes people will decide that they do not like the changes that they made and wish to go back to the original layout. To reset layouts to default, go to the Game Settings -> Preferences -> Troubleshooting section. You will find at this location two buttons that will reset the match layout and the deck editor layouts.
|
||||||
|
|
||||||
* If you have an older Android device for increased performance or to save bandwidth it might be a good idea to use lower resolution images instead: https://www.slightlymagic.net/forum/viewtopic.php?f=15&t=29104
|
Also use the mentioned measure if your match or deckeditor won't start - it would help in 90% of the cases.
|
||||||
|
|
||||||
### How do I extract Forge?
|
## I think I found a bug in Forge. What do I do?
|
||||||
|
|
||||||
* Forge uses a .tar.bz2 format for archiving. Depending on your operating system, different utilities can be used to untar the archive.
|
|
||||||
* If you use Windows, you may want to try 7-Zip (http://www.7-zip.org/download.html).
|
|
||||||
|
|
||||||
### I think I found a bug in Forge. What do I do?
|
|
||||||
|
|
||||||
*Most users, who are running beta versions of Forge, should continue to use these instructions. As for alpha testers, these instructions have yet to be made congruent with the latest automatic bug reporting from within Forge.*
|
*Most users, who are running beta versions of Forge, should continue to use these instructions. As for alpha testers, these instructions have yet to be made congruent with the latest automatic bug reporting from within Forge.*
|
||||||
|
|
||||||
@@ -37,10 +27,9 @@ Bug reports from users are the lifeblood of Forge. Please keep in mind that "bet
|
|||||||
|
|
||||||
For starters, please take note of (1) what you had in play, (2) what your opponent had in play and (3) what you were doing when the error occurred. If you get a Crash Report from inside Forge, please save the data to a file. This information is very important when reporting a problem. Don't worry if you didn't think of that right away, until your next start, the "Forge.log" in the game directory will also provide that information.
|
For starters, please take note of (1) what you had in play, (2) what your opponent had in play and (3) what you were doing when the error occurred. If you get a Crash Report from inside Forge, please save the data to a file. This information is very important when reporting a problem. Don't worry if you didn't think of that right away, until your next start, the "Forge.log" in the game directory will also provide that information.
|
||||||
|
|
||||||
If you did not get a Crash Report, but you have experienced a problem in how Forge handled one or more cards or game rules, *please read the cards (and the Oracle rulings) carefully* to make sure you understand how they work. You may be surprised to find that Forge is actually enforcing the rules
|
If you did not get a Crash Report, but you have experienced a problem in how Forge handled one or more cards or game rules, *please read the cards (and the Oracle rulings) carefully* to make sure you understand how they work. You may be surprised to find that Forge is actually enforcing the rules correctly.
|
||||||
correctly.
|
|
||||||
|
|
||||||
Because duplicate bug reports use up our limited resources, please research your bug with the **Search** box on Forge's [issue tracker](https://git.cardforge.org/core-developers/forge/-/issues) to see if your bug has already been reported there. For Crash Reports, use key words from the second paragraph of the Crash Report.
|
Because duplicate bug reports use up our limited resources, please research your bug with the **Search** box on Forge's [issue tracker](https://github.com/Card-Forge/forge/issues) to see if your bug has already been reported there. For Crash Reports, use key words from the second paragraph of the Crash Report.
|
||||||
|
|
||||||
* If you find a matching issue, examine it to see if you have anything new to contribute. For example, a different way of reproducing a problem can sometimes be helpful. If the issue was posted to the forum, you may post your additional information there.
|
* If you find a matching issue, examine it to see if you have anything new to contribute. For example, a different way of reproducing a problem can sometimes be helpful. If the issue was posted to the forum, you may post your additional information there.
|
||||||
|
|
||||||
@@ -48,13 +37,13 @@ Because duplicate bug reports use up our limited resources, please research your
|
|||||||
|
|
||||||
* If you're unsure, you can also post on one of the support channels of the discord. In case you do not get a timely response, please submit a new issue anyway to make sure it doesn't get lost.
|
* If you're unsure, you can also post on one of the support channels of the discord. In case you do not get a timely response, please submit a new issue anyway to make sure it doesn't get lost.
|
||||||
|
|
||||||
### I have an idea to make Forge better. What do I do?
|
## I have an idea to make Forge better. What do I do?
|
||||||
|
|
||||||
Follow the directions in [Bug Reports](Frequently-Asked-Questions#i-think-i-found-a-bug-in-forge-what-do-i-do), keeping in mind that you are not reporting a bug, but rather a **Feature Request**.
|
Follow the directions in [Bug Reports](Frequently-Asked-Questions#i-think-i-found-a-bug-in-forge-what-do-i-do), keeping in mind that you are not reporting a bug, but rather a **Feature Request**.
|
||||||
|
|
||||||
# Development
|
# Development
|
||||||
|
|
||||||
### I want to help develop Forge. How do I get started?
|
## I want to help develop Forge. How do I get started?
|
||||||
|
|
||||||
Forge is written in Java, so knowledge in that language (or similar Object Oriented languages like C++ or C\#) is very helpful. However, it is possible to learn the grammar for writing the data objects of cards without programming experience.
|
Forge is written in Java, so knowledge in that language (or similar Object Oriented languages like C++ or C\#) is very helpful. However, it is possible to learn the grammar for writing the data objects of cards without programming experience.
|
||||||
|
|
||||||
@@ -62,32 +51,38 @@ A development environment such as [IntelliJ](https://www.jetbrains.com/idea) is
|
|||||||
|
|
||||||
Thanks to the nature of how cards are implemented, you can also contribute these as small plain text files. This is especially helpful during a preview season, when there are a lot of new cards in the backlog. This is mostly coordinated in #card-scripting on the Discord (and the pins there).
|
Thanks to the nature of how cards are implemented, you can also contribute these as small plain text files. This is especially helpful during a preview season, when there are a lot of new cards in the backlog. This is mostly coordinated in #card-scripting on the Discord (and the pins there).
|
||||||
|
|
||||||
To obtain the source code of Forge, read our [Development Guide]((SM-autoconverted)--how-to-get-started-developing-forge).
|
For smaller first-time contributions using the GitHub web interface is also an alternative:
|
||||||
|

|
||||||
|
|
||||||
### My system is all setup to help. What now?
|
1. Register GitHub Account (if you don't already have one)
|
||||||
|
2. Make your own fork of Forge, press this button on the main project page (must only be done first time)
|
||||||
|
3. In your fork you can navigate to `forge-gui/res/cardsfolder/upcoming` and either Upload new files or open an existing one
|
||||||
|
4. When you're done at the bottom make sure to create a new branch, that makes it easier to keep your changes apart
|
||||||
|
5. On the next page make sure you choose the original project as merge target (see screens)
|
||||||
|
6. Please test your scripts and watch for review comments
|
||||||
|
|
||||||
|
To obtain the source code of Forge, read our [Development Guide](Development/IntelliJ-setup/IntelliJ-setup.md).
|
||||||
|
|
||||||
|
## My system is all setup to help. What now?
|
||||||
|
|
||||||
Take a look through the /res/cardsfolder folder. This is where all the card data lives. If you know of cards that are missing from Forge, see if there are similar cards that already exist.
|
Take a look through the /res/cardsfolder folder. This is where all the card data lives. If you know of cards that are missing from Forge, see if there are similar cards that already exist.
|
||||||
|
|
||||||
# Gameplay
|
# Gameplay
|
||||||
|
|
||||||
### Where do I use Flashback or a similar ability that is in an External area?
|
## Where do I use Flashback or a similar ability that is in an External area?
|
||||||
|
|
||||||
Click on the Lightning Bolt icon in the player panel. Since cards with External Activations aren't as clear to activate, we created this shortcut for this specific purpose.
|
Click on the Lightning Bolt icon in the player panel. Since cards with external Activations aren't as clear to activate, we created this shortcut for this specific purpose. After the last card is removed from a zone window, that window will automatically be hidden.
|
||||||
|
|
||||||
### How do I target a player?
|
## How do I target a player?
|
||||||
|
|
||||||
Just click on the player's Avatar in the Player Panel when prompted to select a Player as a target.
|
Just click on the player's Avatar in the Player Panel when prompted to select a Player as a target.
|
||||||
|
|
||||||
### Where did my mana go?
|
## Where did my mana go?
|
||||||
|
|
||||||
If you have an effect that generated you some mana, and you don't know where it is. Check out the Player Panel. There are 6 different mana subpools one for each color/colorless that should have it. If you accidentally tapped your mana before your Main Phase, your mana is gone. Sorry, we don't have a way at this time to revert these actions. In general, I'd say it's easier/better to start casting a spell first, then activate your mana so this doesn't happen.
|
If you have an effect that generated you some mana, and you don't know where it is. Check out the Player Panel. There are 6 different mana subpools one for each color/colorless that should have it. If you accidentally tapped your mana before your Main Phase, your mana is gone. Sorry, we don't have a way at this time to revert these actions. In general, I'd say it's easier/better to start casting a spell first, then activate your mana so this doesn't happen.
|
||||||
|
|
||||||
# Quest Mode
|
# Quest Mode
|
||||||
|
|
||||||
### What is the difference between Fantasy Quest and Normal Quest?
|
## What is the difference between Fantasy Quest and Normal Quest?
|
||||||
|
|
||||||
In Normal Quest, you start with 20 life and only have access to the Card Shop. In Fantasy Quest, you start at 15 life and gain additional access to the Bazaar which allows you to buy things like extra life points, Pets, Plants and more.
|
In Normal Quest, you start with 20 life and only have access to the Card Shop. In Fantasy Quest, you start at 15 life and gain additional access to the Bazaar which allows you to buy things like extra life points, Pets, Plants and more.
|
||||||
|
|
||||||
### Sealed Deck Mode
|
|
||||||
|
|
||||||
[HOW-TO: Customize your Sealed Deck games with fantasy blocks](https://www.slightlymagic.net/forum/viewtopic.php?f=26&t=8164)
|
|
||||||
|
|||||||
28
docs/Home.md
28
docs/Home.md
@@ -8,37 +8,35 @@ Forge is a "Rules Engine" for the game Magic: the Gathering.
|
|||||||
Forge is not related in any way with Wizards of the Coast.
|
Forge is not related in any way with Wizards of the Coast.
|
||||||
Forge is open source software released under the GNU Public License.
|
Forge is open source software released under the GNU Public License.
|
||||||
|
|
||||||
Up to 8 players are supported, with control of each assigned to human or AI control. Player decks can be imported, user-created with the Deck Editor, or automatically generated. Over 99% (and counting) of all cards in Magic's existence are available, with the missing ones mostly being pointless to implement in the context (e. g. the notorious Chaos Orb) or impossible. That's more than the official Magic Online!
|
Up to 8 players are supported, with each assigned to human or AI control. Player decks can be imported, user-created with the Deck Editor, or automatically generated. Over [99%](Missing-Cards-in-Forge.md) (and counting) of all cards in Magic's existence are available, with the missing ones mostly being pointless to implement in the context (e.g. the notorious Chaos Orb) or impossible. That's more than the official Magic Online!
|
||||||
|
|
||||||
For a complete list of unimplemented cards, either check the most recent release topic on the forums or use the "Audit Card and Image Data" check from "Content Downloaders" menu.
|
Forge creates a unique experience by combining this enormous card library with some RPG elements in [**Quest mode**](https://www.slightlymagic.net/forum/viewtopic.php?f=26&t=9258) on the desktop version, and **Planar Conquest** on the mobile version.
|
||||||
|
|
||||||
Forge creates a unique experience by combining this enormous card library with some RPG elements in [**Quest mode**](https://www.slightlymagic.net/forum/viewtopic.php?f=26&t=9258) (comparable to the 'Shandalar', the late 90's PC Game) on the desktop version, and **Planar Conquest** on the mobile version. Forge now includes a Graphical Map Based game mode called "Adventure Mode" which is more akin to 'Shandalar.'
|
Forge also features a wide variety of puzzles. For more details and features see the other pages in this wiki.
|
||||||
|
|
||||||
Forge also features a wide variety of puzzles. For more details and features see the MANUAL.txt in your game folder.
|
Currently, Forge functions best in Human vs. AI matches. Playing against another human player over the Internet with [Online Multiplayer mode](Network-FAQ.md) is functional, but may still result in game play errors.
|
||||||
|
|
||||||
Currently, Forge functions best in Human vs. AI matches. Playing against another human player over the Internet with Online Multiplayer mode is functional, but may still result in game play errors. See the Network Play section of this wiki.
|
|
||||||
|
|
||||||
# Adventure Mode
|
# Adventure Mode
|
||||||
Forge now has an Adventure Mode, along with the Classic deck building and match game modes with AI.
|
Forge now includes a Graphical Map Based game mode, along with the Classic deck building and match game modes with AI.
|
||||||
|
|
||||||
Adventure mode is a work-in-progress game mode where you explore the ever-changing landscape of Shandalar, and duel creatures to gain gold and new cards ~~to become the best and collect them all!~~ to battle the bosses in the castles for each color. You can visit towns to buy equipment and cards, crawl through dungeons to find artifacts and loot to help you on your journey. Adventure mode is an awesome reimagining of the original 'Shandalar' 90's PC Game in Forge.
|
Adventure mode is a work-in-progress game mode where you explore the ever-changing landscape of Shandalar, and duel creatures to gain gold and new cards ~~to become the best and collect them all!~~ to battle the bosses in the castles for each color. You can visit towns to buy equipment and cards, crawl through dungeons to find artifacts and loot to help you on your journey. Adventure mode is an awesome reimagining of the original 'Shandalar' late 90's PC Game in Forge.
|
||||||
|
|
||||||
Adventure is baked into the Android/Mobile release of Forge, and as a separate executable already provided in the Desktop release package.
|
Adventure is baked into the Android/Mobile release of Forge, and as a separate executable already provided in the Desktop release package.
|
||||||
|
|
||||||
# Download and Install
|
# Download and Install
|
||||||
|
|
||||||
* Most users please see the [User Guide.](User-Guide)
|
* Most users please see the [User Guide](User-Guide.md).
|
||||||
|
|
||||||
* For SteamDeck or Bazzite Installation see [Steam Deck Install.](Steam-Deck-and-Bazzite-Install)
|
* For SteamDeck or Bazzite devices see this [extra manual](Steam-Deck-and-Bazzite-Install.md).
|
||||||
|
|
||||||
# Support
|
# Support
|
||||||
[Basic Troubleshooting](Troubleshooting-FAQ) - Check here first.
|
[Basic Troubleshooting](#Troubleshooting.md) - Check here first.
|
||||||
|
|
||||||
[Join the Discord](https://discord.com/invite/3v9JCVr)! - We're happy to help you get going.
|
[Join the Discord](https://discord.com/invite/3v9JCVr)! - We're happy to help you get going.
|
||||||
|
|
||||||
[Other Frequently Asked Questions](Frequently-Asked-Questions) - For more advanced questions about forge mechanics and gameplay.
|
[Other Frequently Asked Questions](Frequently-Asked-Questions.md) - For more advanced explanations about Forge mechanics and gameplay.
|
||||||
|
|
||||||
# Forge Developers
|
# Developers
|
||||||
|
|
||||||
The original programmer can be found at http://mtgrares.blogspot.com. A while
|
The original programmer can be found at http://mtgrares.blogspot.com. A while
|
||||||
back he open sourced the project and let the other developers improve it
|
back he open sourced the project and let the other developers improve it
|
||||||
@@ -52,6 +50,8 @@ lot of behind the scenes action going on. For each release, it is common
|
|||||||
for the release developer to give a shout out for those that helped
|
for the release developer to give a shout out for those that helped
|
||||||
specifically for that version. Feel free to give kudos there.
|
specifically for that version. Feel free to give kudos there.
|
||||||
|
|
||||||
If you are curious about the written/oral history of Forge, we're working on recreating some of that [Forge Historical reference](Forge-historical-reference)
|
If you are curious about the written/oral history of Forge, we're working on recreating some of that [Forge Historical reference](Forge-historical-reference.md)
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
> <span style="color: red;">Note:</span> if you're reading this locally here's the [Table of Contents](_sidebar.md)
|
> <span style="color: red;">Note:</span> if you're reading this locally here's the [Table of Contents](_sidebar.md)
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
# Keyboard Shortcuts
|
|
||||||
|
|
||||||
The following shortcuts are available for the player during the match:
|
|
||||||
|
|
||||||
- Space: Confirm (most times, is the button shown on the left)
|
|
||||||
- Escape: Cancel (most times, is the button shown on the right)
|
|
||||||
- E: end turn
|
|
||||||
- Ctrl + Q: concede
|
|
||||||
- Ctrl + Z: undo
|
|
||||||
- Y: auto-yield to a trigger (always YES)
|
|
||||||
- N: auto-yield to a trigger (always NO)
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
**[WARNING!!!]**
|
|
||||||
|
|
||||||
Page imported from the old SlightlyMagic wiki. To be integrated into other wiki pages and/or README... or deleted.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Duplicate decks checking tool
|
|
||||||
|
|
||||||
This python script will check through your deck files and identify which
|
|
||||||
ones are similar to each other.
|
|
||||||
|
|
||||||
It has been tested with Python 2.6.
|
|
||||||
|
|
||||||
The source can be found on [it's bitbucket
|
|
||||||
page](https://bitbucket.org/asret/forge/src/tip/deckdupcheck.py), or
|
|
||||||
[downloaded
|
|
||||||
directly](https://bitbucket.org/asret/forge/raw/tip/deckdupcheck.py).
|
|
||||||
|
|
||||||
To run it, download it to your forge's "res" directory and invoke the
|
|
||||||
python interpretor on it.
|
|
||||||
|
|
||||||
It still needs work. At present it checks all decks against every other.
|
|
||||||
This means it will output each matching pair twice - once for each deck.
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
**[WARNING!!!]**
|
|
||||||
|
|
||||||
Page imported from the old SlightlyMagic wiki. To be integrated into other wiki pages and/or README... or deleted.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**WORK IN PROGRESS**
|
|
||||||
|
|
||||||
### Install Maven
|
|
||||||
|
|
||||||
<http://maven.apache.org/download.html#Installation>.
|
|
||||||
|
|
||||||
To test your installation you should execute the following command:
|
|
||||||
|
|
||||||
`mvn --version`
|
|
||||||
|
|
||||||
You should see something like like this:
|
|
||||||
|
|
||||||
`Apache Maven 3.0.3 (r1075438; 2011-02-28 09:31:09-0800)`
|
|
||||||
`Maven home: /opt/local/share/java/maven3`
|
|
||||||
`Java version: 1.6.0_24, vendor: Apple Inc.`
|
|
||||||
`Java home: /System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home`
|
|
||||||
`Default locale: en_US, platform encoding: MacRoman`
|
|
||||||
`OS name: "mac os x", version: "10.6.7", arch: "x86_64", family: "mac"`
|
|
||||||
|
|
||||||
#### Mac OSX
|
|
||||||
|
|
||||||
Maven should already be installed. To test installation, open the
|
|
||||||
Terminal application run the test command above.
|
|
||||||
|
|
||||||
#### Windows
|
|
||||||
|
|
||||||
1. Use the above link to download the zip file.
|
|
||||||
2. Unzip the file into a directory.
|
|
||||||
3. Add the directory to your "PATH" variable.
|
|
||||||
4. Open a command window
|
|
||||||
5. Execute the test command above.
|
|
||||||
|
|
||||||
#### Linux
|
|
||||||
|
|
||||||
1. Open your package manager e.g. Synaptic on Debian
|
|
||||||
2. Install the Maven2 package
|
|
||||||
3. Open a Terminal
|
|
||||||
4. Execute the test command above.
|
|
||||||
|
|
||||||
### Install SVN
|
|
||||||
|
|
||||||
<http://subversion.tigris.org/>
|
|
||||||
|
|
||||||
To test your installation you should execute the following command:
|
|
||||||
|
|
||||||
`svn --version`
|
|
||||||
|
|
||||||
You should see something like like this:
|
|
||||||
|
|
||||||
`svn, version 1.6.17 (r1128011)`
|
|
||||||
`compiled Jun 2 2011, 09:40:34`
|
|
||||||
|
|
||||||
#### Mac OSX
|
|
||||||
|
|
||||||
Maven should already be installed. To test installation, open the
|
|
||||||
Terminal application run the test command above.
|
|
||||||
|
|
||||||
#### Windows
|
|
||||||
|
|
||||||
1. Use the above link to download the zip file.
|
|
||||||
2. Unzip the file into a directory.
|
|
||||||
3. Add the directory to your "PATH" variable.
|
|
||||||
4. Open a command window
|
|
||||||
5. Execute the test command above.
|
|
||||||
|
|
||||||
#### Linux
|
|
||||||
|
|
||||||
1. Open your package manager e.g. Synaptic on Debian
|
|
||||||
2. Install the Subversion package
|
|
||||||
3. Open a Terminal
|
|
||||||
4. Execute the test command above.
|
|
||||||
|
|
||||||
### Build Forge
|
|
||||||
|
|
||||||
From a terminal window, go to the directory where forge was checked out
|
|
||||||
via GIT. Update to the latest version of the code
|
|
||||||
|
|
||||||
`mvn scm:update`
|
|
||||||
|
|
||||||
Use this command to perform a simple build of the jar file
|
|
||||||
|
|
||||||
`mvn -U -B clean install`
|
|
||||||
|
|
||||||
Use this command to do a snapshot package build
|
|
||||||
|
|
||||||
`mvn -U -B clean -P osx,windows-linux install`
|
|
||||||
|
|
||||||
Use this command to do a snapshot package build of the Windows/Linux
|
|
||||||
package only
|
|
||||||
|
|
||||||
`mvn -U -B clean -P windows-linux install`
|
|
||||||
|
|
||||||
Use this command to do a snapshot package build of the Mac OSX package
|
|
||||||
only
|
|
||||||
|
|
||||||
`mvn -U -B clean -P osx install`
|
|
||||||
|
|
||||||
Use this command to do a snapshot package build and site deployment
|
|
||||||
|
|
||||||
`mvn -U -B clean -P osx,windows-linux install site deploy site:deploy`
|
|
||||||
|
|
||||||
Use this command to do full package build and upload to GoogleCode
|
|
||||||
|
|
||||||
`mvn -U -B clean -P osx,windows-linux install site release:clean release:prepare release:perform -Dusername="`<user>`" -Dpassword="`<password>`"`
|
|
||||||
|
|
||||||
where <user> and <password> are your GoogleCode credentials (typically
|
|
||||||
something like "you@gmail.com" "w4e4sdg")
|
|
||||||
|
|
||||||
### Build System Utilities
|
|
||||||
|
|
||||||
These utilities are used in the build process. They are automatically
|
|
||||||
included in the build. The links are for reference only.
|
|
||||||
|
|
||||||
[Google Upload](http://code.google.com/p/maven-gcu-plugin/wiki/Usage)
|
|
||||||
|
|
||||||
[Jar Bundler](http://www.informagen.com/JarBundler/)
|
|
||||||
|
|
||||||
[Create DMG Script](http://www.yoursway.com/free/#createdmg)
|
|
||||||
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
What follows is a rough start of an API document for cardscripts.
|
|
||||||
|
|
||||||
| Property | Description
|
|
||||||
| - | -
|
|
||||||
|`A`|Ability
|
|
||||||
|`Colors`|Color(s) of the card<br /><br />When a card's color is determined by a color indicator rather than shards in a mana cost, this property must be defined. If no identifier is needed, this property should be omitted.<br /><br />*`Colors:red` - This is used on Kobolds of Kher Keep, which has a casting cost of {0} and requires a red indicator to make it red.<br /><br />*`Colors:red,green` - Since Arlinn, Embraced by the Moon has no casting cost (it's the back of a double-faced card), the red and green indicator must be included.
|
|
||||||
|`DeckHints`|AI-related hints for a deck including this card
|
|
||||||
|`K`|Keyword
|
|
||||||
|`Loyalty`|Number of starting loyalty counters
|
|
||||||
|`ManaCost`|Cost to cast the card shown in mana shards<br /><br />This property is required. It has a single parameter that is a mana cost.<br /><br />* `ManaCost:no cost` for cards that cannot be cast<br />* `ManaCost:1 W W` sets the casting cost to {1}{W}{W}
|
|
||||||
|`Name`|Name of the card<br /><br />A string of text that serves as the name of the card. Note that the registered trademark symbol cannot be included, and this property must have at least one character.<br /><br />Example:<br />* `Name:A Display of My Dark Power` sets the card's name to "A Display of My Dark Power"
|
|
||||||
|`Oracle`|Oracle text
|
|
||||||
|`PT`|Power and toughness
|
|
||||||
|`R`|Replacement effect
|
|
||||||
|`S`|Static ability
|
|
||||||
|`SVar`|String variable. Used throughout scripting in a handful of different ways.
|
|
||||||
|`T`|Triggered ability
|
|
||||||
|`Text`|Text on card
|
|
||||||
|`Types`|Card types and subtypes<br /><br />Include all card types and subtypes, separated by spaces.<br /><br />Example:<br />* `Types:Enchantment Artifact Creature Golem` for a card that reads Enchantment Artifact Creature -- Golem
|
|
||||||
|
|
||||||
** Parameters for abilities and variables and known accepted values **
|
|
||||||
|
|
||||||
(incomplete list):
|
|
||||||
|
|
||||||
* `AB$`: Ability
|
|
||||||
* `Ability$`: Ability
|
|
||||||
* `ActiveZones$`: Zone
|
|
||||||
* `Affected$`: Card
|
|
||||||
* `AffectedZone$`: Zone
|
|
||||||
* `ChangeNum$`: Integer
|
|
||||||
* `ChangeValid$`: CardType
|
|
||||||
* `ColorOrType$`: `Type`
|
|
||||||
* `Cost$`: Cost
|
|
||||||
* `Count$`: `xPaid`
|
|
||||||
* `DB$`: DB
|
|
||||||
* `Defined$`: Player, Card
|
|
||||||
* `Description$`: Text
|
|
||||||
* `Destination$`: Zone
|
|
||||||
* `DestinationZone$`: Zone
|
|
||||||
* `DigNum$`: Integer
|
|
||||||
* `Duration$`: UntilYourNextTurn
|
|
||||||
* `Event$`: Event
|
|
||||||
* `Execute$`: DB
|
|
||||||
* `Hidden$`: boolean
|
|
||||||
* `KW$`: Keyword
|
|
||||||
* `LifeAmount$`: Integer
|
|
||||||
* `MayPlay$: boolean
|
|
||||||
* `Mode$`: Mode
|
|
||||||
* `Name$`: Text
|
|
||||||
* `NotCause$`: Ability
|
|
||||||
* `NumAtt$`: `+1`
|
|
||||||
* `NumCards$`: Integer
|
|
||||||
* `Optional$`: boolean
|
|
||||||
* `Origin$`: Zone
|
|
||||||
* `Produced$`: ManaType
|
|
||||||
* `References$`: SVar
|
|
||||||
* `ReflectProperty$`: Property
|
|
||||||
* `ReplaceWith$`: Text
|
|
||||||
* `SP$`: Spell
|
|
||||||
* `SpellDescription$`: Text
|
|
||||||
* `StackDescription$`: Text
|
|
||||||
* `Static$`: boolean
|
|
||||||
* `SVars$`: SVar
|
|
||||||
* `TargetMax$`: Integer
|
|
||||||
* `TargetMin$`: Integer
|
|
||||||
* `TargetPrompt$`: Text
|
|
||||||
* `TriggerDescription$`: Text
|
|
||||||
* `TriggeredCard$`: Property
|
|
||||||
* `Triggers$`: Mode
|
|
||||||
* `TriggerZones$`: Zone
|
|
||||||
* `Valid$`: `Triggered`
|
|
||||||
* `ValidCard$`: Card
|
|
||||||
* `ValidTgts$`: Player, CardType
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
Forge can work with card images of your choice.
|
|
||||||
|
|
||||||
The naming convention is as follows:
|
|
||||||
|
|
||||||
If you have an older Android device for increased performance or to save bandwidth it might be a good idea to use lower resolution images instead: https://www.slightlymagic.net/forum/viewtopic.php?f=15&t=29104
|
|
||||||
4
docs/Skins.md
Normal file
4
docs/Skins.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
Download more skins here:
|
||||||
|
https://github.com/Card-Forge/forge-extras/releases/tag/themes
|
||||||
|
|
||||||
|
[Skin Template + Atlas file](https://github.com/user-attachments/files/23420566/forge_sprite_icons_template.zip)
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
# SteamDeck and Bazzite Support
|
|
||||||
|
|
||||||
_This instruction was written using Bazzite, however should be similar enough for SteamOS._
|
_This instruction was written using Bazzite, however should be similar enough for SteamOS._
|
||||||
|
|
||||||
In order to support the SteamDeck "natively" for full Forge Desktop mode, we would likely need to have a flatpack installer for the best user install experience, currently Forge has no intention to have a flatpack. The current **best** and recommended way to have Forge on your SteamDeck is to install and run the Android APK version in Waydroid Android Container.
|
In order to support the SteamDeck "natively" for full Forge Desktop mode, we would likely need to have a flatpack installer for the best user install experience, currently Forge has no intention to have a flatpack. The current **best** and recommended way to have Forge on your SteamDeck is to install and run the Android APK version in Waydroid Android Container.
|
||||||
|
|
||||||
* You will need to have installed Waydroid first, this reddit post may work for you; https://www.reddit.com/r/SteamDeck/comments/1ay7ev8/how_to_install_waydroid_android_on_your_steam_deck/
|
* You will need to have installed Waydroid first, this reddit post may work for you: https://www.reddit.com/r/SteamDeck/comments/1ay7ev8/how_to_install_waydroid_android_on_your_steam_deck/
|
||||||
|
|
||||||
## Installing Forge Android in Waydroid (Recommended Method)
|
## Installing Forge Android in Waydroid (Recommended Method)
|
||||||
Once you've installed Waydroid, you can follow the same steps you would in any Android device.
|
Once you've installed Waydroid, you can follow the same steps you would in any Android device.
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
This page is a placeholder for Theme creation information.
|
|
||||||
|
|
||||||
I'm working on creating an atlas data file for the skin sprite sheets. I'll separate this into 3 sections; Skins, Music and Sounds, and Card Images.
|
|
||||||
@@ -1,116 +1,196 @@
|
|||||||
# Downloads
|
# Downloads
|
||||||
* **Snapshots**;
|
|
||||||
* READ THESE NOTES BEFORE DOWNLOADING SNAPSHOTS:
|
|
||||||
* **Please use snapshots for Adventure Mode!**
|
|
||||||
* May contain more bugs, bug fixes, **definitely gets newest cards faster** and newer features.
|
|
||||||
* These are **NOW** automatically released daily.
|
|
||||||
* If the snapshot isn't in the location below, it's because its in the middle of uploading a new snapshot. Come back later to grab it.
|
|
||||||
* [_**CLICK HERE FOR DOWNLOAD LINKS - Forge SNAPSHOT Version (DESKTOP/ANDROID)**_](https://github.com/Card-Forge/forge/releases/tag/daily-snapshots)
|
|
||||||
* For desktop, grab the installer file that ends in .jar
|
|
||||||
* For android, grab the android file that ends in .apk
|
|
||||||
* **Android Installation Guide**
|
|
||||||
* Quick Guide for installing Android Snapshots: <br />
|
|
||||||
|
|
||||||
|
## Snapshots
|
||||||
|
|
||||||
https://github.com/user-attachments/assets/7a0c7bb8-7cf9-4800-8091-bcc30ff2f4d8
|
* Snapshots are automated daily builds of the source code.
|
||||||
|
* They contain the latest bug fixes, features and cards.
|
||||||
|
* If the snapshot isn't in the location below, it's because its in the middle of uploading a new snapshot - come back later to grab it.
|
||||||
|
|
||||||
|
[***CLICK HERE FOR DOWNLOAD LINKS - Forge SNAPSHOT Version (DESKTOP/ANDROID)***](https://github.com/Card-Forge/forge/releases/tag/daily-snapshots)
|
||||||
|
|
||||||
|
* For desktop, grab the installer file that ends in .jar
|
||||||
|
* For android, grab the android file that ends in .apk
|
||||||
|
‐ Watch the screen recording if one of following steps isn't clear for you
|
||||||
|
|
||||||
* **Releases**;
|
<https://github.com/user-attachments/assets/7a0c7bb8-7cf9-4800-8091-bcc30ff2f4d8>
|
||||||
* READ THESE NOTES BEFORE DOWNLOADING RELEASES:
|
|
||||||
- "Releases" are really intended where "99% cards implemented are working and stable."
|
|
||||||
- If you are looking for newly spoiled cards as soon as possible, grab the snapshot instead.
|
|
||||||
- The current release mechanism is failing unexpectedly for Android. So just stick with snapshots for Android users.
|
|
||||||
* [_**CLICK HERE FOR DOWNLOAD LINKS - RELEASE DESKTOP**_](https://github.com/Card-Forge/forge/releases/latest)
|
|
||||||
- Grab the installer file that ends in .jar
|
|
||||||
|
|
||||||
|
|
||||||
# Java Requirement
|
## Releases
|
||||||
|
|
||||||
|
* "Releases" are really intended where "99% cards implemented are working and stable".
|
||||||
|
* **They are NOT bug-free.** They are not updated after they're built, meaning you need to wait for the next release if you encounter a bug, or use the SNAPSHOT version instead.
|
||||||
|
* If you are looking for newly spoiled cards as soon as possible, grab the SNAPSHOT version instead.
|
||||||
|
* The current release mechanism is failing unexpectedly for Android. So just stick with snapshots for Android users.
|
||||||
|
|
||||||
|
[***CLICK HERE FOR DOWNLOAD LINKS - RELEASE DESKTOP***](https://github.com/Card-Forge/forge/releases/latest)
|
||||||
|
|
||||||
|
* Grab the installer file that ends in .jar
|
||||||
|
|
||||||
|
# System Requirements
|
||||||
|
|
||||||
**Forge Requires Java** to run, please make sure you have Java installed on your machine prior to attempting to run.
|
**Forge Requires Java** to run, please make sure you have Java installed on your machine prior to attempting to run.
|
||||||
|
|
||||||
* **Java 17** is required as minimum version and can be acquired through the Standard Edition Development Kit (JDK) or the OpenJDK. Continued development provides new features in those editions, therefore you need the Java Development Kit to have those newer editions;
|
* **Java 17** is required as minimum version and can be acquired through the Standard Edition Development Kit (JDK) or the OpenJDK. Continued development provides new features in those editions, therefore you need the Java Development Kit to have those newer editions:
|
||||||
- Download - [https://jdk.java.net/](https://jdk.java.net/)
|
* Download - [https://jdk.java.net/](https://jdk.java.net/)
|
||||||
- Source Code - [https://github.com/openjdk/jdk/](https://github.com/openjdk/jdk/)
|
* Source Code - [https://github.com/openjdk/jdk/](https://github.com/openjdk/jdk/)
|
||||||
|
|
||||||
Most people who have problems setting up Forge, do not have Java setup properly. If you are having trouble, open your terminal/command line and run `java --version`. That number should be 17 or higher.
|
Most people who have problems setting up Forge, do not have Java setup properly. If you are having trouble, open your terminal/command line and run `java --version`. That number should be 17 or higher.
|
||||||
|
|
||||||
|
The memory requirements for Forge have fluctuated over time. The default
|
||||||
|
setting on your computer for the Java heap space may not be enough to
|
||||||
|
prevent the above problems. If you launch Forge by double-clicking the
|
||||||
|
jar files directly you could eventually receive a **java heap space
|
||||||
|
error**.
|
||||||
|
|
||||||
|
We have created several scripts that will launch Forge with a greater
|
||||||
|
allotment of system resources. (We do this by passing `-Xmx1024m` as
|
||||||
|
an argument to the Java VM.)
|
||||||
|
|
||||||
|
If you plan to eventually download all card images make sure you have several gigabytes of free drive space.
|
||||||
|
|
||||||
# Install and Run
|
# Install and Run
|
||||||
|
|
||||||
Forge requires Java to run.
|
> Warning: Do **NOT** owerwrite an existing installation. Always unpack/install the package in a new folder to avoid problems!
|
||||||
|
|
||||||
_**Download and unpack /install the package to their own new folder!**_
|
## Install Wizard (jar)
|
||||||
|
|
||||||
### Install Wizard (jar)
|
|
||||||
* Run/Double click "**forge-installer**-VERSION.jar" where VERSION is the current release version and click next until the Target Path window appears. If double clicking the .jar file doesn't load the main interface you can run it via terminal/command line ```java -jar FILENAME.jar``` where FILENAME is the name of the installer.
|
* Run/Double click "**forge-installer**-VERSION.jar" where VERSION is the current release version and click next until the Target Path window appears. If double clicking the .jar file doesn't load the main interface you can run it via terminal/command line ```java -jar FILENAME.jar``` where FILENAME is the name of the installer.
|
||||||
|
|
||||||
* Browse to your preferred install directory (create a new directory for clean installation) and click next until installation starts.
|
* Browse to your preferred install directory and click next until installation starts.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
* After the installation finishes, close the installer. Run the executable forge|forge-adventure (.exe/.sh/.cmd)
|
* After the installation finishes, close the installer. Run the executable forge|forge-adventure (.exe/.sh/.cmd)
|
||||||
|
|
||||||
### Manual Extraction (tar.bz2)
|
### What if double-clicking doesn’t work?
|
||||||
|
|
||||||
* **Desktop Windows**:
|
Sometimes double-clicking will open the jar file in a different program.
|
||||||
* Unpack "forge...tar.**bz2**" with any unpacking/unzipping app (e.g. 7-zip, winrar, etc)
|
In Windows, you may need to right-click and open the properties to change the launching program to Java.
|
||||||
* You'll end up with "forge...**tar**".
|
This might be different in OSX or Linux systems (file permission related).
|
||||||
* Unpack that ".tar" file once more into its own folder.
|
|
||||||
* Run Forge app/exe
|
|
||||||
* **Desktop Linux/Mac**:
|
|
||||||
* Unpack "forge...**tar.bz2**" with any unpacking app. (Check your package repository, or app store.)
|
|
||||||
* You'll probably end up with just a folder, and fully extracted.
|
|
||||||
* If you do end up with a ".tar" file, unpack that file also into it's own folder.
|
|
||||||
* Run Forge script;
|
|
||||||
* Linux: Run the ".sh" file in a terminal (double clicking might work.)
|
|
||||||
* MacOS/OSX: Run the ".command" file by double clicking in Finder, or run from the terminal.
|
|
||||||
* If the command file doesn't appear to do anything, you'll need to [modify the permissions to be executable.](https://support.apple.com/guide/terminal/make-a-file-executable-apdd100908f-06b3-4e63-8a87-32e71241bab4/mac) (This is a temporary bug in the build process.)
|
|
||||||
* Additionally OSX needs to have a JRE AND a JDK installed because reasons.
|
|
||||||
* **Android**:
|
|
||||||
* Sideload/Install "forge...apk"
|
|
||||||
* Run Forge
|
|
||||||
|
|
||||||
## Play Adventure Mode on Desktop
|
## Manual Extraction (tar.bz2)
|
||||||
|
|
||||||
|
### Desktop Windows
|
||||||
|
|
||||||
|
* Unpack "forge...*tar.bz2*" with any unpacking/unzipping app (e.g. 7-zip, winrar, etc)
|
||||||
|
* You'll end up with "forge...*tar*".
|
||||||
|
* Unpack that ".tar" file once more into its own folder.
|
||||||
|
* Run Forge app/exe
|
||||||
|
|
||||||
|
### Desktop Linux/Mac
|
||||||
|
|
||||||
|
* Unpack "forge...*tar.bz2*" with any unpacking app. (Check your package repository, or app store.)
|
||||||
|
* You'll probably end up with just a folder, and fully extracted.
|
||||||
|
* If you do end up with a ".tar" file, unpack that file also into its own folder.
|
||||||
|
* Run Forge script:
|
||||||
|
* Linux: Run the ".sh" file in a terminal (double clicking might work.)
|
||||||
|
* MacOS/OSX: Run the ".command" file by double clicking in Finder, or run from the terminal.
|
||||||
|
* If the command file doesn't appear to do anything, you'll need to [modify the permissions to be executable.](https://support.apple.com/guide/terminal/make-a-file-executable-apdd100908f-06b3-4e63-8a87-32e71241bab4/mac) (This is a temporary bug in the build process.)
|
||||||
|
* Additionally OSX needs to have a JRE AND a JDK installed because reasons.
|
||||||
|
|
||||||
|
### Android
|
||||||
|
|
||||||
|
* Sideload/Install "forge...apk"
|
||||||
|
* Run Forge
|
||||||
|
|
||||||
|
# User data migration
|
||||||
|
There are three defined user data directories: userDir, cacheDir, and cardPicsDir, and their locations depend on the standard paths for your operating system:
|
||||||
|
|
||||||
|
Windows:
|
||||||
|
userDir=%APPDATA%/Forge/
|
||||||
|
cacheDir=%LOCALAPPDATA%/Forge/Cache/ (or %APPDATA%/Forge/Cache/ for Windows versions before the local/roaming directory split)
|
||||||
|
OSX:
|
||||||
|
userDir=$HOME/Library/Application Support/Forge/
|
||||||
|
cacheDir=$HOME/Library/Caches/Forge/
|
||||||
|
Linux:
|
||||||
|
userDir=$HOME/.forge/
|
||||||
|
cacheDir=$HOME/.cache/forge/
|
||||||
|
|
||||||
|
The appdata directory is hidden by default in Windows 7 and above versions. Open a Windows Explorer window (or double-click on My Computer) and in the address field type "%appdata%/forge/" (without the quotes).
|
||||||
|
|
||||||
|
cardPicsDir is defined as <cacheDir>/pics/cards/ by default. If you wish to use a non-default directory, please see the forge.profile.properties.example file located in the Forge installation directory root. You can use this file to, for example, share the card pics directory with another program, such as Magic Workstation.
|
||||||
|
|
||||||
|
If you are using the Mac OS X version of Forge then you will find the forge.profile.properties.example file by right clicking or control clicking on the Forge.app icon. Select "Show Package Contents" from the contextual menu. A Finder window will open and will display a folder named Contents. Navigate to the folder:
|
||||||
|
/Contents/Resources/Java/
|
||||||
|
and you will find the file.
|
||||||
|
|
||||||
|
## Import Data
|
||||||
|
If you have a directory full of deck files, you can use the Import Data dialog to copy or move them to the appropriate directory. The dialog gives you a full listing of all file copy/move operations, so you can see what will happen before you click 'Start Import'.
|
||||||
|
|
||||||
|
# Accessibility
|
||||||
|
We know some people are colorblind and may not be able to differentiate between colors of the default theme. Forge does have access to other [Skins](Skins.md), which use other color palettes that might be more suitable for you.
|
||||||
|
|
||||||
|
# Play Adventure Mode on Desktop
|
||||||
|
|
||||||
* Run the Adventure Mode EXE or Script in the Folder you extracted.
|
* Run the Adventure Mode EXE or Script in the Folder you extracted.
|
||||||
* The game will start with an option for Adventure or Classic Mobile UI.
|
* The game will start with an option for Adventure or Classic Mobile UI.
|
||||||
* Android/Mobile builds are built as the Adventure Mode or Mobile UI and nothing special is needed.
|
* Android/Mobile builds are built as the Adventure Mode or Mobile UI and nothing special is needed.
|
||||||
- If adventure mode option does not show up;
|
* If adventure mode option does not show up;
|
||||||
- check you're up to date with your version.
|
* check you're up to date with your version.
|
||||||
- check in the settings that the "Selector Mode" is set to `Default`
|
* check in the settings that the "Selector Mode" is set to `Default`
|
||||||
|
|
||||||
# System Requirements and Historic Details
|
# Gameplay
|
||||||
|
|
||||||
Since Forge is written in Java, it is compatible on any Operating System
|
## Targeting Arrows
|
||||||
that can run the Java Runtime Environment. Forge requires
|
When hovering over items on the stack, arrows will be displayed between that item and all of its targets (both cards and players).
|
||||||
Java 17 (Forge is not backwards compatible with older versions of Java).
|
The arrow will be red if the spell/ability's activator is an opponent of the target or its controller, and blue if targeting an ally of the target or its controller.
|
||||||
If you have difficulties with your System not working with Forge,
|
|
||||||
please come to the Discord so we can attempt to help.
|
|
||||||
This program works best with a screen resolution of **1280 by 720** or
|
|
||||||
better. Forge can now have it's window minimized to **800 by 600**
|
|
||||||
pixels but this may make the display area cramped and possibly limit
|
|
||||||
your ability to play. (This means Forge may not compatible with some
|
|
||||||
netbook computers.)
|
|
||||||
|
|
||||||
The memory requirements for Forge have fluctuated over time. The default
|
## Card Zoomer
|
||||||
setting on your computer for the java heap space may not be enough to
|
You can gaze at your HQ images in all their glory with just a flick of the mousewheel, holding the middle mouse button down, or holding the left and right mouse buttons down at the same time. This feature will also increase the size of low quality pics up to the size used for high quality pics, but the image will not be very clear.
|
||||||
prevent the above problems. If you launch Forge by double-clicking the
|
|
||||||
file **run-forge.jar** you will eventually receive a **java heap space
|
|
||||||
error**. The name of the forge jar file has changed as part of our new
|
|
||||||
Maven based build and release system. The name format now used is:
|
|
||||||
|
|
||||||
**forge-**{version number}**-jar-with-dependencies.jar**
|
Instructions:
|
||||||
|
- Works on any card image in the Deck Editor or Duel screen.
|
||||||
|
- Move your mouse over the card you want to zoom and mouse-wheel forward.
|
||||||
|
- Mouse-wheel back, mouse click or pressing ESC closes the zoomed image.
|
||||||
|
|
||||||
We have created several scripts that will launch Forge with a greater
|
Split cards (name contains "//") are rotated 90 degrees for easier viewing.
|
||||||
allotment of system resources. (We do this by passing **-Xmx1024m** as
|
|
||||||
an argument to the Java VM.) People using Windows OS should double click
|
|
||||||
the **forge.exe** file. People using Apple's Mac OS X should use the Mac
|
|
||||||
OS version and double click the **forge.command** file. People using one of the
|
|
||||||
other \*nix OS should double click the **forge.sh** file.
|
|
||||||
|
|
||||||
# What if double-clicking doesn’t work?
|
If a card is a flip or double-sided card then you can easily view the alternate image using flick wheel forward or tap CTRL key.
|
||||||
|
|
||||||
Sometimes double-clicking will open the jar file in a different program.
|
The standard flip graphic (the two rotated arrows) is displayed if the card can be flipped or transformed.
|
||||||
In Windows, you may need to right-click and open the properties to
|
|
||||||
change the launching program to Java. This might be different in OSX or
|
Forge supports showing XLHQ (extra large high quality) card pictures when zooming in on a card if these pictures are available. Forge will look for XLHQ card art in the "XLHQ" subfolder of the "pics/cards" folder in Forge cache. XLHQ pictures should have the ".xlhq.jpg" extension instead of the ".full.jpg" one (CCGHQ XLHQ releases comply with this naming scheme).
|
||||||
Linux systems (file permission related).
|
Please note that XLHQ versions of cards are *only* showed in the zoom view, regular card pictures are still used (LQ/HQ, depending on what you're using) on the battlefield and elsewhere in the game because XLHQ art is significantly more taxing in memory consumption (and in addition to that, XLHQ card borders are not cropped the way Forge expects them in order to show them properly on the battlefield anyway).
|
||||||
|
|
||||||
|
XLHQ tokens are also supported, but the naming scheme for them is a little different - they are looked up in "pics/tokens/XLHQ" and have their ordinary names.
|
||||||
|
|
||||||
|
## Easier creature type selection
|
||||||
|
When prompted to select a creature type for a card like *Obelisk of Urd*, creature types present in your deck will appear on top, sorted from most to least frequent, followed by all other creature types.
|
||||||
|
This should make it so, more often than not, you can just accept the dialog without searching.
|
||||||
|
|
||||||
|
## Auto-Target
|
||||||
|
When playing spells and abilities with the text "target opponent", if you only have one opponent, you will no longer be asked to choose the opponent to target.
|
||||||
|
When triggered abilities have only one valid target, that target will now be auto-selected.
|
||||||
|
|
||||||
|
## Auto-Pay
|
||||||
|
When paying mana costs, you can press Enter/Spacebar or click the Auto button in the Prompt to automatically pay the mana cost using available mana sources if possible.
|
||||||
|
- The button will be disabled if you cannot pay the mana cost at that time, in which case Enter/Spacebar will cancel the spell/ability instead.
|
||||||
|
- Uses the same logic the AI uses to pay mana costs. This means it will try to use mana in your pool before mana sources in play, using colorless mana to pay the colorless part of the cost if any is available.
|
||||||
|
- You can still manually pay the cost by clicking mana sources in play (e.g. lands) or clicking symbols in your mana pool, which might be a good idea if you want to save specific mana sources for a later play that turn.
|
||||||
|
- you'll still be prompted when paying Sunburst or cards that care what colors are spent to cast it (ex. Firespout).
|
||||||
|
|
||||||
|
## Auto-Yield
|
||||||
|
- When a spell or an ability appears on the stack and it says "(OPTIONAL)" you can right-click it to decide if you want to always accept or to decline it.
|
||||||
|
|
||||||
|
It is possible to specify the granularity level for auto-yields: the difference is that, for example, when choosing per ability if you auto-yield to Hellrider's triggered ability once, all triggers from other Hellrider cards will be automatically yielded to as well. When choosing per card, you will need to auto-yield to each Hellrider separately.
|
||||||
|
|
||||||
|
Note that in when auto-yielding per ability, yields will NOT be automatically cleared between games in a match, which should speed the game up. When auto-yielding per card, yields WILL be automatically cleared between games because they are dependent on card IDs which change from game to game, thus you will need to auto-yield to each card again in each game of the match.
|
||||||
|
|
||||||
|
- Pressing "End Turn" skips your attack phase and doesn't get cancelled automatically if a spell or ability is put on the stack. You'll still be given a chance to declare blockers if your opponent attacks, but after that the rest of your opponent's turn will then progress without you receiving priority.
|
||||||
|
|
||||||
|
To alleviate pressing this accidentally, as long as you're passing this way, you'll be able to press Escape or the Cancel button to be given the chance to act again. Phases with stops and spells/abilities resolving will be given a slight delay to allow you to see what's going on.
|
||||||
|
|
||||||
|
## Shift Key helper
|
||||||
|
* When you mouse over a flip, transform or Morph (controlled by you) card in battlefield, hold SHIFT to see other state of that card at the side panel that displays card picture and details.
|
||||||
|
* Hold SHIFT when clicking on mana pool icons to use as much of that mana type as possible towards the cost.
|
||||||
|
* Tap all lands in a stack using Shift+click on any in the stack.
|
||||||
|
* Attack with all creatures in a stack using Shift+click on any in the stack.
|
||||||
|
|
||||||
|
## Full Control
|
||||||
|
Right click/long tap on your player avatar:
|
||||||
|
This feature lets you skip different helpers that streamline gameplay by avoiding somewhat annoying GUI interactions and instead use AI logic to make reasonable decisions for beginners. Useful for certain corner cases or if you want to challenge yourself with the Comprehensive Rules:
|
||||||
|
e.g. the opposite cost order is needed if activating an animated "Halo Fountain" that's also tapped.
|
||||||
|
|
||||||
|
## Repeatable Sequences (Macros)
|
||||||
|
A feature for advanced users: during a match, you can use the default shortcut shift-R to specify a sequence of actions (mouse clicks, essentially, in the desktop paradigm). Type the IDs of cards/players you'd like to interact with, in order. Then the default shortcut @ (shift-2) will execute your sequence, one "click" at a time, repeating when it reaches the end. This is useful for executing repeated combos, such as sacrificing a recurring creature to Goblin Bombardment. You can see the IDs of cards by turning them on under "Card Overlays" in the "Game" menu.
|
||||||
|
|
||||||
|
The macro will dutifully execute your click sequence without regard to changes in game state (so if an opponent kills your specified creature mid-macro, and you continue to execute it, you will be essentially clicking on the creature in the graveyard, which may or may not be what you want).
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
### [Forge Wiki Home](Home.md)
|
### [Forge Wiki Home](Home.md)
|
||||||
|
|
||||||
- [User Guide](User-Guide.md)
|
- [User Guide](User-Guide.md)
|
||||||
- [FAQ](Frequently-Asked-Questions.md)
|
- [FAQ](Frequently-Asked-Questions.md)
|
||||||
- [SteamDeck/Bazzite](Steam-Deck-and-Bazzite-Install.md)
|
- [SteamDeck/Bazzite](Steam-Deck-and-Bazzite-Install.md)
|
||||||
@@ -6,60 +7,60 @@
|
|||||||
- [Network Play](network-play.md)
|
- [Network Play](network-play.md)
|
||||||
- [Network FAQ](Network-FAQ.md)
|
- [Network FAQ](Network-FAQ.md)
|
||||||
- [Network Extra](Networking-Extras.md)
|
- [Network Extra](Networking-Extras.md)
|
||||||
|
- [Advanced search](Advanced-Search.md)
|
||||||
|
|
||||||
- [Adventure Mode](Adventure-Mode.md)
|
- [Adventure Mode](Adventure/Adventure-Mode.md)
|
||||||
|
|
||||||
- Gameplay Guide
|
- Gameplay Guide
|
||||||
|
- [Getting Started](Adventure/Gameplay-Guide.md)
|
||||||
|
- [Different Planes](Adventure/Different-Planes.md)
|
||||||
|
- [Currency](Adventure/Currency.md)
|
||||||
|
- [Deck Building Tips](Adventure/Deck-Building-Tips.md)
|
||||||
|
- [Towns & Capitals](Adventure/Towns-&-Capitals.md)
|
||||||
|
- [Dungeons](Adventure/Dungeons.md)
|
||||||
|
- [Equipment & Items](Adventure/Equipments-and-Items.md)
|
||||||
|
- [Controller Support](Adventure/GAMEPAD.md)
|
||||||
|
- [Console & Cheats](Adventure/Console-and-Cheats.md)
|
||||||
|
|
||||||
- [Getting Started](Gameplay-Guide.md)
|
- [Modding & content creation](Adventure/Modding.md)
|
||||||
- [Different Planes](Different-Planes.md)
|
- [Create Enemies](Adventure/Create-Enemies.md)
|
||||||
- [Currency](Currency.md)
|
- [Create Rewards](Adventure/Create-Rewards.md)
|
||||||
- [Deck Building Tips](Deck-Building-Tips.md)
|
- [Create Maps](Adventure/Create-new-Maps.md)
|
||||||
- [Towns & Capitals](Towns-&-Capitals.md)
|
- [Configure Planes](Adventure/Configure-Planes.md)
|
||||||
- [Dungeons](Dungeons.md)
|
- [Configure Starting Sets](Adventure/Configure-Sets.md)
|
||||||
- [Equipments and Items](Equipments-and-Items.md)
|
|
||||||
- [Keyboard Shortcuts](Keyboard-Shortcuts.md)
|
|
||||||
- [Console and Cheats](Console-and-Cheats.md)
|
|
||||||
|
|
||||||
- [Modding and Development](Modding-and-Development.md)
|
|
||||||
|
|
||||||
- [Create Enemies](Create-Enemies.md)
|
|
||||||
- [Create Rewards](Create-Rewards.md)
|
|
||||||
- [Create Maps](Create-new-Maps.md)
|
|
||||||
- [Configure Planes](Configure-Planes.md)
|
|
||||||
- [Configure Starting Sets](Configure-Sets.md)
|
|
||||||
|
|
||||||
- Tutorials
|
- Tutorials
|
||||||
- [Tutorial 1, Create your first Plane](Tutorial-1-Create-your-First-Plane.md)
|
- [Tutorial 1, Create your first Plane](Adventure/Tutorial-1-Create-your-First-Plane.md)
|
||||||
- [Tutorial 2, A New Look (creating your first map.)](Tutorial-2-A-New-Look.md)
|
- [Tutorial 2, A New Look (creating your first map.)](Adventure/Tutorial-2-A-New-Look.md)
|
||||||
- [Tutorial 3, Configuration (Configuring your Plane)](Tutorial-3-Configuration.md)
|
- [Tutorial 3, Configuration (Configuring your Plane)](Adventure/Tutorial-3-Configuration.md)
|
||||||
|
|
||||||
|
|
||||||
- [Card Scripting API](Card-scripting-API/Card-scripting-API.md)
|
- [Card Scripting API](Card-scripting-API/Card-scripting-API.md)
|
||||||
- [Ability effects](Card-scripting-API/AbilityFactory.md)
|
- [Ability effects](Card-scripting-API/AbilityFactory.md)
|
||||||
- [Triggers](Card-scripting-API/Triggers.md)
|
- [Triggers](Card-scripting-API/Triggers.md)
|
||||||
- [Replacements](Card-scripting-API/Replacements.md)
|
- [Replacements](Card-scripting-API/Replacements.md)
|
||||||
- Statics
|
- [Statics](Card-scripting-API/Statics.md)
|
||||||
- [Costs](Card-scripting-API/Costs.md)
|
- [Costs](Card-scripting-API/Costs.md)
|
||||||
- [Affected / Targets](Card-scripting-API/Targeting.md)
|
- [Affected / Targets](Card-scripting-API/Targeting.md)
|
||||||
- [Restrictions / Conditions](Card-scripting-API/Restrictions.md)
|
- [Restrictions / Conditions](Card-scripting-API/Restrictions.md)
|
||||||
- [Guide: Creating a Custom Card](Card-scripting-API/Creating-a-Custom-Card.md)
|
- [Tutorial: creating a custom card](Card-scripting-API/Creating-a-Custom-Card.md)
|
||||||
|
|
||||||
- [Development]((SM-autoconverted)-how-to-get-started-developing-forge.md)
|
- Development
|
||||||
- [IntelliJ Setup](Development/IntelliJ-setup/IntelliJ-setup.md)
|
- [IntelliJ Setup](Development/IntelliJ-setup/IntelliJ-setup.md)
|
||||||
- [Snapshots and Releases](Snapshots-and-Releases.md)
|
- [Snapshots & Releases](Development/Snapshots-and-Releases.md)
|
||||||
- [Android Builds](Development/android-builds.md)
|
- [Android Builds](Development/Android-Builds.md)
|
||||||
|
- [Dev Mode](Development/DevMode.md)
|
||||||
- [Ownership](Development/ownership.md)
|
- [Ownership](Development/ownership.md)
|
||||||
- [Docker Container](docker-setup.md)
|
- [Docker Container](docker-setup.md)
|
||||||
|
|
||||||
- [Customization and Themes.md](Themes.md)
|
- Customization & Themes
|
||||||
- Skins
|
- [Skins](Skins.md)
|
||||||
- Sounds
|
- [Sounds & Music](Custom-Audio.md)
|
||||||
- [Music](Custom-Music.md)
|
|
||||||
- [Card Images](Card-Images.md)
|
- [Card Images](Card-Images.md)
|
||||||
- [File Formats](File-Formats.md)
|
- [File Formats](File-Formats.md)
|
||||||
- [Creating your first custom set](Creating-a-custom-set.md)
|
- [Tutorial: creating your first custom set](Creating-a-custom-Set.md)
|
||||||
|
- [Fantasy Blocks](fantasy-blocks.md)
|
||||||
|
|
||||||
- [Missing Cards in Forge](Missing-Cards-in-Forge.md)
|
- [Missing Cards in Forge](Missing-Cards-in-Forge.md)
|
||||||
- [Un‐cards, Playtest Cards, and Other Funny Cards](Un‐cards,-Playtest-Cards,-and-Other-Funny-Cards.md)
|
- [Un‐cards, Playtest Cards, and Other Funny Cards](Un‐cards,-Playtest-Cards,-and-Other-Funny-Cards.md)
|
||||||
- [Credit and Thanks](Credit-and-Thanks.md)
|
|
||||||
|
- [Credit & Thanks](Credit-and-Thanks.md)
|
||||||
83
docs/fantasy-blocks.md
Normal file
83
docs/fantasy-blocks.md
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
# How to customize your Forge Sealed Deck game with fantasy blocks
|
||||||
|
|
||||||
|
You can define your own dream block for sealed deck games by choosing the exact sets you want to include in the game - and even easily spice up your sealed deck experience with special booster packs that generate cards based on several different sets, the full cardpool or your customized cube.
|
||||||
|
|
||||||
|
To do this, you need to modify your "fantasyblocks.txt" file with your text editor of choice. You will find the file in the res/blockdata folder. Notice that there is also a "blocks.txt" file in this folder - it contains the predefined official MtG blocks (better leave it alone). The "fantasyblocks.txt" file currently includes some sample blocks we have built. You can delete them all, replace them with your own blocks, or just add new blocks after them. You can have as many fantasy blocks as you like - just remember to increment the index number every time you add a new one!
|
||||||
|
|
||||||
|
When you are finished, just save the fantasyblocks.txt file and relaunch Forge. If have made errors when editing the fantasyblocks.txt file, you may get error messages in your block now and Forge may fail to launch. (If this happens, check your logfile to see what went wrong.)
|
||||||
|
|
||||||
|
# What is a block file made of?
|
||||||
|
|
||||||
|
A block definition file (fantasyblocks.txt or blocks.txt) contains block definitions that are parsed when the game launches and then used internally when the game is running. One block definition per line. Let's look at a sample line:
|
||||||
|
|
||||||
|
`Index:0|Set0:LEA|Name:Alpha|DraftPacks:3|LandSetCode:LEA|SealedPacks:6`
|
||||||
|
|
||||||
|
This is the first line in the official blocks.txt file. It contains several parameters and values that are separated with |'s. The format for a parameter value/pair is "Parameter:value". Here's what the parameters mean:
|
||||||
|
|
||||||
|
Index: The internal index number of the block. This needs to be unique. When you add a new block (row), just check the last number and add 1.
|
||||||
|
Set0: This is a 3-letter code for MtG set. "LEA" value means that one of the sets included in this block is Alpha. A block can contain as many as 9 different sets (given as parameters Set0:xxx|Set1:yyy| etc. until Set8:zzz). Hint: If you don't know the 3-letter code for a set you want, you can look it up in the setdata.txt file in the same directory (res/blockdata).
|
||||||
|
Name: This is the name that is displayed in the block selection dialog box.
|
||||||
|
DraftPacks: How many boosters you get when Drafting this block.
|
||||||
|
LandSetCode: Which edition basic lands are used when playing Draft/Sealed Deck games with this block. Again, if necessary, you can look up the set code in the setdata.txt file. Note that if your block contains a core set, it is usually a good idea to use that edition's code here or may get basic lands from different editions and they will not show up properly in the deck editor.
|
||||||
|
SealedPacks: How many boosters you get when playing Sealed Deck games with this block. Note that the current version of Forge supports 4-9 booster pack SD games but if you give the players (for example) only 4 booster packs and your block contains more than four sets, you will be able to choose from the first four sets only. Thus, this number should always be at least as great as the number of different sets in the block if you want to be able to use them all. But it can, of course, be higher than number of sets in a sealed deck game. In this particular example, all players get 6 booster packs but, since the only set included in the block is Alpha, all 6 will be Alpha boosters.
|
||||||
|
|
||||||
|
Let's try another fairly straightforward example, this time from fantasyblocks.txt:
|
||||||
|
|
||||||
|
`Index:25|Set0:2ED|Set1:ATQ|Set2:ARN|Set3:DRK|Set4:LEG|Set5:FEM|Set6:ICE|Set7:HML|Set8:ALL|Name:(9) MtG Encyclopedia|DraftPacks:3|LandSetCode:2ED|SealedPacks:9`
|
||||||
|
|
||||||
|
Yes: this is fantasy block #25 (actually 26th block, since the first block is #0, not #1). It contains 9 different sets, basically everything from the birth of the game to Alliances. Hence, the name "MtG Encyclopedia" - theoretically, any card from the old printed book Magic the Gathering: Official Encyclopedia ("godbook") would be included here (except the promo cards). (The "(9)" in front of the name is simply a clue to give you an idea how many sets there are in the block.) Unlimited lands are used here. Draft players get only 3 different booster packs, so this block is poorly suited to Draft games (they would only get Unlimited, Arabian Nights and Antiquities boosters from this blocks). In Sealed Deck games, however, all you would get 9 different boosters if you select this block - and have the option of selecting starter packs instead of booster packs for Unlimited and/or Ice Age.
|
||||||
|
|
||||||
|
But the fun doesn't stop here...
|
||||||
|
|
||||||
|
# MetaSets: What are they, why should I care, and how do I use them?
|
||||||
|
|
||||||
|
Even 9 different sets may seem limiting sometimes. Or maybe you would have booster based your special, customized fantasy card list? MetaSets are have been added to allow you do exactly that: create a special booster pack that wouldn't be possible in real life.
|
||||||
|
|
||||||
|
Here is a MetaSet sample from fantasyblocks.txt:
|
||||||
|
|
||||||
|
`Index:26|Meta1:CUBE/ArabianExtended/ARAB|Meta2:META/ISD,DKA,AVR,M13/M13-ISD|Meta3:FULL/*/*|Name:(3) Metaset Sample|DraftPacks:3|LandSetCode:2ED|SealedPacks:6`
|
||||||
|
|
||||||
|
To define a MetaSet, you use the Meta0...Meta8 parameters, just like you would use the Set0...Set8 parameters. (Notice that, illogically, here I have accidentally started with Meta1 and not Meta0, shame on me! But the number doesn't really matter that much, it's the amount of different sets/MetaSets in a block that matters.)
|
||||||
|
|
||||||
|
The main difference lies in the values that a Meta(x) parameter can have. First we have:
|
||||||
|
|
||||||
|
`Meta1:CUBE/ArabianExtended/ARAB`
|
||||||
|
|
||||||
|
A MetaSet always needs 3 values, separated with slashes. The first value is the MetaSet type, in this case, a CUBE. The first value must be CUBE, FULL, or META. Any other value will cause an error.
|
||||||
|
The other two values are data and display name (they behave differently for the different MetaSet types). For a cube, the second value must be the cube name. If Forge cannot find this cube (defined in res/sealed/), it will give you an error when you try to choose this block. The last name is simply a visual name that is displayed when you choose the set distribution for you sealed deck game. For a cube, a "*C:" prefix is always automatically appended.
|
||||||
|
|
||||||
|
Next we have:
|
||||||
|
|
||||||
|
`Meta2:META/ISD,DKA,AVR,M13/M13-ISD`
|
||||||
|
|
||||||
|
This is a genuine 'meta' MetaSet - i.e., a set of sets, a block within a block. The first value is the type (META) and the last value is the display name (M13-ISD), just like in the cube example. For meta-type MetaSets, a "*B:" prefix is added in indicate that it is really a sub-block within the block.
|
||||||
|
The second value is the interesting part. It is a comma-separated list of sets that are combined to make the cardpool for this booster, in this case the whole Innistrad block and M13 core set. (Again, look up the codes in setdata.txt if you don't know them.) And here's the cool part: you can list any number of sets you like in the comma-separated list!
|
||||||
|
|
||||||
|
The final MetaSet in this sample block is:
|
||||||
|
|
||||||
|
`Meta3:FULL/*/*`
|
||||||
|
|
||||||
|
This one is pretty straightforward. It simply indicates that the boosters for this 'set' are based on the full cardpool available in Forge. While values 2 and 3 must be supplied (otherwise the line won't parse correctly), they are not important for the full cardpool MetaSet type. Value 2 is not used at all, and its display name will always be "*FULL".
|
||||||
|
|
||||||
|
So, effectively, the above "Metaset Sample" block can generate boosters based on the "Arabian Extended" cube, a sub-block consisting of the Innistrad block + M13, and/or full cardpool.
|
||||||
|
|
||||||
|
Finally, note that you can mix-and-match regular sets and MetaSets in a block - just be sure to count both when you set the SealedPacks parameter. For example, if your block contains regular sets Set0, Set1, and Set2 (3 sets), and MetaSets Meta1 and Meta2, the value will need to be at least 5. (The regular 6 would work nicely, too.)
|
||||||
|
|
||||||
|
# MetaSets: The Next Level
|
||||||
|
|
||||||
|
`Meta-Choose(S(RTR Prerelease Azorius Guild)Azorius guild;S(RTR Prerelease Selesnya Guild)Selesnya guild;S(RTR Prerelease Izzet Guild)Izzet guild;S(RTR Prerelease Rakdos Guild)Rakdos guild;S(RTR Prerelease Golgari Guild)Golgari guild)Guild`
|
||||||
|
|
||||||
|
Forge has these MetaSet types:
|
||||||
|
|
||||||
|
* Full("F") - All cards
|
||||||
|
* Cube("C") - Cube
|
||||||
|
* JoinedSet("J") - Joined set.. ex: J(ICE ALL HML CSP)Ice_Age_Block_Extended
|
||||||
|
* Choose("Select") - Choose from a list of nested metasets
|
||||||
|
* Random("Any") - Randomly select one of nested metasets
|
||||||
|
* Combo("All") - Combined booster means all nested sets will be selected
|
||||||
|
* Booster("B") - a common booster, associated with card edition... ex: B(DKA)
|
||||||
|
* SpecialBooster("S") - Special booster defined to support special events, that is not linked to any edition, see note below.
|
||||||
|
* Pack("T") - Tournament pack or Starter, valid only for editions where it was avaliable
|
||||||
|
|
||||||
|
* You may use either name or a shorter alias to denote a meta set. They are case insensitive now
|
||||||
|
* There is a new meta type "SpecialBooster" (added during 1.3.16 development), it's used to refer to special boosters declared in res\blockdata\boosters-special.txt . These boosters are used to hold RTR block sealed events, and may be used for MBS faction booster generation (if anyone would like to build a themed sealed game)
|
||||||
BIN
docs/search.png
Normal file
BIN
docs/search.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
@@ -810,13 +810,13 @@ public class AiController {
|
|||||||
return reserveManaSources(sa, phaseType, enemy, true, null);
|
return reserveManaSources(sa, phaseType, enemy, true, null);
|
||||||
}
|
}
|
||||||
public boolean reserveManaSources(SpellAbility sa, PhaseType phaseType, boolean enemy, boolean forNextSpell, SpellAbility exceptForThisSa) {
|
public boolean reserveManaSources(SpellAbility sa, PhaseType phaseType, boolean enemy, boolean forNextSpell, SpellAbility exceptForThisSa) {
|
||||||
ManaCostBeingPaid cost = ComputerUtilMana.calculateManaCost(sa.getPayCosts(), sa, true, 0, false);
|
ManaCostBeingPaid cost = ComputerUtilMana.calculateManaCost(sa.getPayCosts(), sa, player, true, 0, false);
|
||||||
CardCollection manaSources = ComputerUtilMana.getManaSourcesToPayCost(cost, sa, player);
|
CardCollection manaSources = ComputerUtilMana.getManaSourcesToPayCost(cost, sa, player);
|
||||||
|
|
||||||
// used for chained spells where two spells need to be cast in succession
|
// used for chained spells where two spells need to be cast in succession
|
||||||
if (exceptForThisSa != null) {
|
if (exceptForThisSa != null) {
|
||||||
manaSources.removeAll(ComputerUtilMana.getManaSourcesToPayCost(
|
manaSources.removeAll(ComputerUtilMana.getManaSourcesToPayCost(
|
||||||
ComputerUtilMana.calculateManaCost(exceptForThisSa.getPayCosts(), exceptForThisSa, true, 0, false),
|
ComputerUtilMana.calculateManaCost(exceptForThisSa.getPayCosts(), exceptForThisSa, player, true, 0, false),
|
||||||
exceptForThisSa, player));
|
exceptForThisSa, player));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1726,6 +1726,7 @@ public class AiController {
|
|||||||
return future.get(game.getAITimeout(), TimeUnit.SECONDS);
|
return future.get(game.getAITimeout(), TimeUnit.SECONDS);
|
||||||
} catch (InterruptedException | ExecutionException | TimeoutException e) {
|
} catch (InterruptedException | ExecutionException | TimeoutException e) {
|
||||||
try {
|
try {
|
||||||
|
e.printStackTrace();
|
||||||
t.stop();
|
t.stop();
|
||||||
} catch (UnsupportedOperationException ex) {
|
} catch (UnsupportedOperationException ex) {
|
||||||
// Android and Java 20 dropped support to stop so sadly thread will keep running
|
// Android and Java 20 dropped support to stop so sadly thread will keep running
|
||||||
|
|||||||
@@ -1285,20 +1285,6 @@ public class ComputerUtil {
|
|||||||
}
|
}
|
||||||
} // AntiBuffedBy
|
} // AntiBuffedBy
|
||||||
|
|
||||||
// Plane cards that give Haste (e.g. Sokenzan)
|
|
||||||
if (ai.getGame().getRules().hasAppliedVariant(GameType.Planechase)) {
|
|
||||||
for (Card c : ai.getGame().getActivePlanes()) {
|
|
||||||
for (StaticAbility s : c.getStaticAbilities()) {
|
|
||||||
if (s.hasParam("AddKeyword")
|
|
||||||
&& s.getParam("AddKeyword").contains("Haste")
|
|
||||||
&& "Creature".equals(s.getParam("Affected"))
|
|
||||||
&& card.isCreature()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final CardCollectionView vengevines = ai.getCardsIn(ZoneType.Graveyard, "Vengevine");
|
final CardCollectionView vengevines = ai.getCardsIn(ZoneType.Graveyard, "Vengevine");
|
||||||
if (!vengevines.isEmpty()) {
|
if (!vengevines.isEmpty()) {
|
||||||
final CardCollectionView creatures = ai.getCardsIn(ZoneType.Hand);
|
final CardCollectionView creatures = ai.getCardsIn(ZoneType.Hand);
|
||||||
@@ -1458,16 +1444,15 @@ public class ComputerUtil {
|
|||||||
for (StaticAbility stAb : c.getStaticAbilities()) {
|
for (StaticAbility stAb : c.getStaticAbilities()) {
|
||||||
if (stAb.checkMode(StaticAbilityMode.Continuous) && stAb.hasParam("AddKeyword")
|
if (stAb.checkMode(StaticAbilityMode.Continuous) && stAb.hasParam("AddKeyword")
|
||||||
&& stAb.getParam("AddKeyword").contains("Haste")) {
|
&& stAb.getParam("AddKeyword").contains("Haste")) {
|
||||||
|
|
||||||
if (c.isEquipment() && c.getEquipping() == null) {
|
if (c.isEquipment() && c.getEquipping() == null) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
final String affected = stAb.getParam("Affected");
|
final String affected = stAb.getParam("Affected");
|
||||||
if (affected.contains("Creature.YouCtrl")
|
if (affected.startsWith("Creature") && (affected.contains("YouCtrl") || !affected.contains("."))) {
|
||||||
|| affected.contains("Other+YouCtrl")) {
|
|
||||||
return true;
|
return true;
|
||||||
} else if (affected.contains("Creature.PairedWith") && !c.isPaired()) {
|
}
|
||||||
|
if (affected.contains("Creature.PairedWith") && !c.isPaired()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1482,8 +1467,7 @@ public class ComputerUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final String valid = params.get("ValidCard");
|
final String valid = params.get("ValidCard");
|
||||||
if (valid.contains("Creature.YouCtrl")
|
if (valid.contains("Creature.YouCtrl") || valid.contains("Other+YouCtrl") ) {
|
||||||
|| valid.contains("Other+YouCtrl") ) {
|
|
||||||
|
|
||||||
final SpellAbility sa = t.getOverridingAbility();
|
final SpellAbility sa = t.getOverridingAbility();
|
||||||
if (sa != null && sa.getApi() == ApiType.Pump && sa.hasParam("KW")
|
if (sa != null && sa.getApi() == ApiType.Pump && sa.hasParam("KW")
|
||||||
@@ -3083,12 +3067,16 @@ public class ComputerUtil {
|
|||||||
return numInHand > 0 || numInDeck >= 3;
|
return numInHand > 0 || numInDeck >= 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// this function should be called by most API to give scripters the option of helping AI
|
||||||
public static CardCollection filterAITgts(SpellAbility sa, Player ai, CardCollection srcList, boolean alwaysStrict) {
|
public static CardCollection filterAITgts(SpellAbility sa, Player ai, CardCollection srcList, boolean alwaysStrict) {
|
||||||
|
// TODO support players
|
||||||
final Card source = sa.getHostCard();
|
final Card source = sa.getHostCard();
|
||||||
if (source == null || !sa.hasParam("AITgts")) {
|
if (source == null || !sa.hasParam("AITgts")) {
|
||||||
return srcList;
|
return srcList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO randomize the order, just so human can't predict in advance which of two equal cards AI might pick
|
||||||
|
|
||||||
CardCollection list;
|
CardCollection list;
|
||||||
String aiTgts = sa.getParam("AITgts");
|
String aiTgts = sa.getParam("AITgts");
|
||||||
if (aiTgts.startsWith("BetterThan")) {
|
if (aiTgts.startsWith("BetterThan")) {
|
||||||
@@ -3111,10 +3099,23 @@ public class ComputerUtil {
|
|||||||
final int totalValue = value;
|
final int totalValue = value;
|
||||||
list = CardLists.filter(srcList, c -> ComputerUtilCard.evaluateCreature(c) > totalValue + 30);
|
list = CardLists.filter(srcList, c -> ComputerUtilCard.evaluateCreature(c) > totalValue + 30);
|
||||||
} else {
|
} else {
|
||||||
list = CardLists.getValidCards(srcList, sa.getParam("AITgts"), sa.getActivatingPlayer(), source, sa);
|
list = CardLists.getValidCards(srcList, aiTgts, sa.getActivatingPlayer(), source, sa);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!list.isEmpty() || sa.hasParam("AITgtsStrict") || alwaysStrict) {
|
if (sa.hasParam("AITgtsStrict") || alwaysStrict) {
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
if (!list.isEmpty()) {
|
||||||
|
// try to fill up with other regular targets to increase chance of playing
|
||||||
|
for (Card tgt : srcList) {
|
||||||
|
if (list.size() >= sa.getMinTargets()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (list.contains(tgt)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
list.add(tgt);
|
||||||
|
}
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
return srcList;
|
return srcList;
|
||||||
|
|||||||
@@ -509,16 +509,16 @@ public class ComputerUtilCost {
|
|||||||
*
|
*
|
||||||
* @param sa
|
* @param sa
|
||||||
* a {@link forge.game.spellability.SpellAbility} object.
|
* a {@link forge.game.spellability.SpellAbility} object.
|
||||||
* @param player
|
* @param payer
|
||||||
* a {@link forge.game.player.Player} object.
|
* a {@link forge.game.player.Player} object.
|
||||||
* @return a boolean.
|
* @return a boolean.
|
||||||
*/
|
*/
|
||||||
public static boolean canPayCost(final SpellAbility sa, final Player player, final boolean effect) {
|
public static boolean canPayCost(final SpellAbility sa, final Player payer, final boolean effect) {
|
||||||
return canPayCost(sa.getPayCosts(), sa, player, effect);
|
return canPayCost(sa.getPayCosts(), sa, payer, effect);
|
||||||
}
|
}
|
||||||
public static boolean canPayCost(final Cost cost, final SpellAbility sa, final Player player, final boolean effect) {
|
public static boolean canPayCost(final Cost cost, final SpellAbility sa, final Player payer, final boolean effect) {
|
||||||
if (sa.getActivatingPlayer() == null) {
|
if (sa.getActivatingPlayer() == null) {
|
||||||
sa.setActivatingPlayer(player); // complaints on NPE had came before this line was added.
|
sa.setActivatingPlayer(payer); // complaints on NPE had came before this line was added.
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for stuff like Nether Void
|
// Check for stuff like Nether Void
|
||||||
@@ -527,14 +527,14 @@ public class ComputerUtilCost {
|
|||||||
boolean cannotBeCountered = !sa.isCounterableBy(null);
|
boolean cannotBeCountered = !sa.isCounterableBy(null);
|
||||||
|
|
||||||
if (sa instanceof Spell) {
|
if (sa instanceof Spell) {
|
||||||
for (Card c : player.getGame().getCardsIn(ZoneType.Battlefield)) {
|
for (Card c : payer.getGame().getCardsIn(ZoneType.Battlefield)) {
|
||||||
final String snem = c.getSVar("AI_SpellsNeedExtraMana");
|
final String snem = c.getSVar("AI_SpellsNeedExtraMana");
|
||||||
if (!StringUtils.isBlank(snem)) {
|
if (!StringUtils.isBlank(snem)) {
|
||||||
if (cannotBeCountered && c.getName().equals("Nether Void")) {
|
if (cannotBeCountered && c.getName().equals("Nether Void")) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
String[] parts = TextUtil.split(snem, ' ');
|
String[] parts = TextUtil.split(snem, ' ');
|
||||||
boolean meetsRestriction = parts.length == 1 || player.isValid(parts[1], c.getController(), c, sa);
|
boolean meetsRestriction = parts.length == 1 || payer.isValid(parts[1], c.getController(), c, sa);
|
||||||
if(!meetsRestriction)
|
if(!meetsRestriction)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
@@ -545,7 +545,7 @@ public class ComputerUtilCost {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (Card c : player.getCardsIn(ZoneType.Command)) {
|
for (Card c : payer.getCardsIn(ZoneType.Command)) {
|
||||||
if (cannotBeCountered) {
|
if (cannotBeCountered) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -567,7 +567,7 @@ public class ComputerUtilCost {
|
|||||||
if (part.convertAmount() != null && part.convertAmount() == sa.getHostCard().getCurrentLoyalty()) {
|
if (part.convertAmount() != null && part.convertAmount() == sa.getHostCard().getCurrentLoyalty()) {
|
||||||
// refuse to pay if opponent has no creature threats or
|
// refuse to pay if opponent has no creature threats or
|
||||||
// 50% chance otherwise
|
// 50% chance otherwise
|
||||||
if (player.getOpponents().getCreaturesInPlay().isEmpty()
|
if (payer.getOpponents().getCreaturesInPlay().isEmpty()
|
||||||
|| MyRandom.getRandom().nextFloat() < .5f) {
|
|| MyRandom.getRandom().nextFloat() < .5f) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -591,7 +591,7 @@ public class ComputerUtilCost {
|
|||||||
Cost wardCost = ComputerUtilCard.getTotalWardCost(tgt);
|
Cost wardCost = ComputerUtilCard.getTotalWardCost(tgt);
|
||||||
// don't use API converter since it might have special part logic not meant for Ward cost
|
// don't use API converter since it might have special part logic not meant for Ward cost
|
||||||
SpellAbilityAi topAI = new SpellAbilityAi() {};
|
SpellAbilityAi topAI = new SpellAbilityAi() {};
|
||||||
if (!topAI.willPayCosts(player, sa, wardCost, sa.getHostCard())) {
|
if (!topAI.willPayCosts(payer, sa, wardCost, sa.getHostCard())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (wardCost.hasManaCost()) {
|
if (wardCost.hasManaCost()) {
|
||||||
@@ -606,7 +606,7 @@ public class ComputerUtilCost {
|
|||||||
if (sa.getHostCard().hasKeyword(Keyword.CASUALTY)) {
|
if (sa.getHostCard().hasKeyword(Keyword.CASUALTY)) {
|
||||||
for (final CostPart part : cost.getCostParts()) {
|
for (final CostPart part : cost.getCostParts()) {
|
||||||
if (part instanceof CostSacrifice) {
|
if (part instanceof CostSacrifice) {
|
||||||
CardCollection valid = CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield), part.getType().split(";"),
|
CardCollection valid = CardLists.getValidCards(payer.getCardsIn(ZoneType.Battlefield), part.getType().split(";"),
|
||||||
sa.getActivatingPlayer(), sa.getHostCard(), sa);
|
sa.getActivatingPlayer(), sa.getHostCard(), sa);
|
||||||
valid = CardLists.filter(valid, CardPredicates.hasSVar("AIDontSacToCasualty").negate());
|
valid = CardLists.filter(valid, CardPredicates.hasSVar("AIDontSacToCasualty").negate());
|
||||||
if (valid.isEmpty()) {
|
if (valid.isEmpty()) {
|
||||||
@@ -618,8 +618,8 @@ public class ComputerUtilCost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO both of these call CostAdjustment.adjust, try to reuse instead
|
// TODO both of these call CostAdjustment.adjust, try to reuse instead
|
||||||
return ComputerUtilMana.canPayManaCost(cost, sa, player, extraManaNeeded, effect)
|
return ComputerUtilMana.canPayManaCost(cost, sa, payer, extraManaNeeded, effect)
|
||||||
&& CostPayment.canPayAdditionalCosts(cost, sa, effect, player);
|
&& CostPayment.canPayAdditionalCosts(cost, sa, effect, payer);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Set<String> getAvailableManaColors(Player ai, Card additionalLand) {
|
public static Set<String> getAvailableManaColors(Player ai, Card additionalLand) {
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ public class ComputerUtilMana {
|
|||||||
return payManaCost(cost, sa, ai, false, 0, true, effect);
|
return payManaCost(cost, sa, ai, false, 0, true, effect);
|
||||||
}
|
}
|
||||||
private static boolean payManaCost(final Cost cost, final SpellAbility sa, final Player ai, final boolean test, final int extraMana, boolean checkPlayable, final boolean effect) {
|
private static boolean payManaCost(final Cost cost, final SpellAbility sa, final Player ai, final boolean test, final int extraMana, boolean checkPlayable, final boolean effect) {
|
||||||
ManaCostBeingPaid manaCost = calculateManaCost(cost, sa, test, extraMana, effect);
|
ManaCostBeingPaid manaCost = calculateManaCost(cost, sa, ai, test, extraMana, effect);
|
||||||
return payManaCost(manaCost, sa, ai, test, checkPlayable, effect);
|
return payManaCost(manaCost, sa, ai, test, checkPlayable, effect);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ public class ComputerUtilMana {
|
|||||||
* Return the number of colors used for payment for Converge
|
* Return the number of colors used for payment for Converge
|
||||||
*/
|
*/
|
||||||
public static int getConvergeCount(final SpellAbility sa, final Player ai) {
|
public static int getConvergeCount(final SpellAbility sa, final Player ai) {
|
||||||
ManaCostBeingPaid cost = calculateManaCost(sa.getPayCosts(), sa, true, 0, false);
|
ManaCostBeingPaid cost = calculateManaCost(sa.getPayCosts(), sa, ai, true, 0, false);
|
||||||
if (payManaCost(cost, sa, ai, true, true, false)) {
|
if (payManaCost(cost, sa, ai, true, true, false)) {
|
||||||
return cost.getSunburst();
|
return cost.getSunburst();
|
||||||
}
|
}
|
||||||
@@ -1291,7 +1291,7 @@ public class ComputerUtilMana {
|
|||||||
* @param extraMana extraMana
|
* @param extraMana extraMana
|
||||||
* @return ManaCost
|
* @return ManaCost
|
||||||
*/
|
*/
|
||||||
public static ManaCostBeingPaid calculateManaCost(final Cost cost, final SpellAbility sa, final boolean test, final int extraMana, final boolean effect) {
|
public static ManaCostBeingPaid calculateManaCost(final Cost cost, final SpellAbility sa, final Player payer, final boolean test, final int extraMana, final boolean effect) {
|
||||||
Card host = sa.getHostCard();
|
Card host = sa.getHostCard();
|
||||||
Zone castFromBackup = null;
|
Zone castFromBackup = null;
|
||||||
if (test && sa.isSpell() && !host.isInZone(ZoneType.Stack)) {
|
if (test && sa.isSpell() && !host.isInZone(ZoneType.Stack)) {
|
||||||
@@ -1302,6 +1302,10 @@ public class ComputerUtilMana {
|
|||||||
Cost payCosts;
|
Cost payCosts;
|
||||||
if (test) {
|
if (test) {
|
||||||
payCosts = CostAdjustment.adjust(cost, sa, effect);
|
payCosts = CostAdjustment.adjust(cost, sa, effect);
|
||||||
|
// prevent asking Human when only predicting
|
||||||
|
if (!payer.getController().isAI()) {
|
||||||
|
sa.setMaxWaterbend(null);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// when not testing CostPayment already handled raise
|
// when not testing CostPayment already handled raise
|
||||||
payCosts = cost;
|
payCosts = cost;
|
||||||
@@ -1345,9 +1349,7 @@ public class ComputerUtilMana {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!effect) {
|
CostAdjustment.adjust(manaCost, sa, payer, null, test, effect);
|
||||||
CostAdjustment.adjust(manaCost, sa, null, test);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("NumTimes".equals(sa.getParam("Announce"))) { // e.g. the Adversary cycle
|
if ("NumTimes".equals(sa.getParam("Announce"))) { // e.g. the Adversary cycle
|
||||||
ManaCost mkCost = sa.getPayCosts().getTotalMana();
|
ManaCost mkCost = sa.getPayCosts().getTotalMana();
|
||||||
@@ -1773,15 +1775,18 @@ public class ComputerUtilMana {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Matches list of creatures to shards in mana cost for convoking.
|
* Matches list of creatures to shards in mana cost for convoking.
|
||||||
* @param cost cost of convoked ability
|
*
|
||||||
* @param list creatures to be evaluated
|
* @param cost cost of convoked ability
|
||||||
* @param improvise
|
* @param list creatures to be evaluated
|
||||||
|
* @param artifacts
|
||||||
|
* @param creatures
|
||||||
* @return map between creatures and shards to convoke
|
* @return map between creatures and shards to convoke
|
||||||
*/
|
*/
|
||||||
public static Map<Card, ManaCostShard> getConvokeOrImproviseFromList(final ManaCost cost, List<Card> list, boolean improvise) {
|
public static Map<Card, ManaCostShard> getConvokeOrImproviseFromList(final ManaCost cost, List<Card> list, boolean artifacts, boolean creatures) {
|
||||||
final Map<Card, ManaCostShard> convoke = new HashMap<>();
|
final Map<Card, ManaCostShard> convoke = new HashMap<>();
|
||||||
Card convoked = null;
|
Card convoked = null;
|
||||||
if (!improvise) {
|
if (creatures && !artifacts) {
|
||||||
|
// Run for convoke but not improvise or waterbending
|
||||||
for (ManaCostShard toPay : cost) {
|
for (ManaCostShard toPay : cost) {
|
||||||
if (toPay.isSnow() || toPay.isColorless()) {
|
if (toPay.isSnow() || toPay.isColorless()) {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -963,6 +963,8 @@ public abstract class GameState {
|
|||||||
spellDef = spellDef.substring(0, spellDef.indexOf("->")).trim();
|
spellDef = spellDef.substring(0, spellDef.indexOf("->")).trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
spellDef = spellDef.replace("^", ":"); // alternate marker for when : is the name of the card
|
||||||
|
|
||||||
Card c = null;
|
Card c = null;
|
||||||
|
|
||||||
if (StringUtils.isNumeric(spellDef)) {
|
if (StringUtils.isNumeric(spellDef)) {
|
||||||
|
|||||||
@@ -1382,7 +1382,7 @@ public class PlayerControllerAi extends PlayerController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<Card, ManaCostShard> chooseCardsForConvokeOrImprovise(SpellAbility sa, ManaCost manaCost, CardCollectionView untappedCards, boolean improvise) {
|
public Map<Card, ManaCostShard> chooseCardsForConvokeOrImprovise(SpellAbility sa, ManaCost manaCost, CardCollectionView untappedCards, boolean artifacts, boolean creatures, Integer maxReduction) {
|
||||||
final Player ai = sa.getActivatingPlayer();
|
final Player ai = sa.getActivatingPlayer();
|
||||||
final PhaseHandler ph = ai.getGame().getPhaseHandler();
|
final PhaseHandler ph = ai.getGame().getPhaseHandler();
|
||||||
//Filter out mana sources that will interfere with payManaCost()
|
//Filter out mana sources that will interfere with payManaCost()
|
||||||
@@ -1390,9 +1390,10 @@ public class PlayerControllerAi extends PlayerController {
|
|||||||
|
|
||||||
// Filter out creatures if AI hasn't attacked yet
|
// Filter out creatures if AI hasn't attacked yet
|
||||||
if (ph.isPlayerTurn(ai) && ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
|
if (ph.isPlayerTurn(ai) && ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
|
||||||
if (improvise) {
|
if (!creatures) {
|
||||||
untapped = CardLists.filter(untapped, c -> !c.isCreature());
|
untapped = CardLists.filter(untapped, c -> !c.isCreature());
|
||||||
} else {
|
} else {
|
||||||
|
// TODO AI needs to learn how to use Convoke or Waterbend
|
||||||
return new HashMap<>();
|
return new HashMap<>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1406,13 +1407,16 @@ public class PlayerControllerAi extends PlayerController {
|
|||||||
if (!ai.getGame().getStack().isEmpty()) {
|
if (!ai.getGame().getStack().isEmpty()) {
|
||||||
final List<GameObject> objects = ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), null);
|
final List<GameObject> objects = ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), null);
|
||||||
for (Card c : blockers) {
|
for (Card c : blockers) {
|
||||||
if (objects.contains(c) && (!improvise || c.isArtifact())) {
|
if (objects.contains(c) && (creatures || c.isArtifact())) {
|
||||||
untapped.add(c);
|
untapped.add(c);
|
||||||
}
|
}
|
||||||
|
if (maxReduction != null && untapped.size() >= maxReduction) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ComputerUtilMana.getConvokeOrImproviseFromList(manaCost, untapped, improvise);
|
return ComputerUtilMana.getConvokeOrImproviseFromList(manaCost, untapped, artifacts, creatures);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ public enum SpellApiToAi {
|
|||||||
.put(ApiType.AddPhase, AddPhaseAi.class)
|
.put(ApiType.AddPhase, AddPhaseAi.class)
|
||||||
.put(ApiType.AddTurn, AddTurnAi.class)
|
.put(ApiType.AddTurn, AddTurnAi.class)
|
||||||
.put(ApiType.AdvanceCrank, AdvanceCrankAi.class)
|
.put(ApiType.AdvanceCrank, AdvanceCrankAi.class)
|
||||||
|
.put(ApiType.Airbend, AirbendAi.class)
|
||||||
.put(ApiType.AlterAttribute, AlterAttributeAi.class)
|
.put(ApiType.AlterAttribute, AlterAttributeAi.class)
|
||||||
.put(ApiType.Amass, AmassAi.class)
|
.put(ApiType.Amass, AmassAi.class)
|
||||||
.put(ApiType.Animate, AnimateAi.class)
|
.put(ApiType.Animate, AnimateAi.class)
|
||||||
@@ -86,6 +87,7 @@ public enum SpellApiToAi {
|
|||||||
.put(ApiType.DrainMana, DrainManaAi.class)
|
.put(ApiType.DrainMana, DrainManaAi.class)
|
||||||
.put(ApiType.Draw, DrawAi.class)
|
.put(ApiType.Draw, DrawAi.class)
|
||||||
.put(ApiType.EachDamage, DamageEachAi.class)
|
.put(ApiType.EachDamage, DamageEachAi.class)
|
||||||
|
.put(ApiType.Earthbend, EarthbendAi.class)
|
||||||
.put(ApiType.Effect, EffectAi.class)
|
.put(ApiType.Effect, EffectAi.class)
|
||||||
.put(ApiType.Encode, EncodeAi.class)
|
.put(ApiType.Encode, EncodeAi.class)
|
||||||
.put(ApiType.Endure, EndureAi.class)
|
.put(ApiType.Endure, EndureAi.class)
|
||||||
@@ -195,8 +197,7 @@ public enum SpellApiToAi {
|
|||||||
.put(ApiType.TimeTravel, TimeTravelAi.class)
|
.put(ApiType.TimeTravel, TimeTravelAi.class)
|
||||||
.put(ApiType.Token, TokenAi.class)
|
.put(ApiType.Token, TokenAi.class)
|
||||||
.put(ApiType.TwoPiles, TwoPilesAi.class)
|
.put(ApiType.TwoPiles, TwoPilesAi.class)
|
||||||
.put(ApiType.Unattach, CannotPlayAi.class)
|
.put(ApiType.Unattach, UnattachAi.class)
|
||||||
.put(ApiType.UnattachAll, UnattachAllAi.class)
|
|
||||||
.put(ApiType.UnlockDoor, AlwaysPlayAi.class)
|
.put(ApiType.UnlockDoor, AlwaysPlayAi.class)
|
||||||
.put(ApiType.Untap, UntapAi.class)
|
.put(ApiType.Untap, UntapAi.class)
|
||||||
.put(ApiType.UntapAll, UntapAllAi.class)
|
.put(ApiType.UntapAll, UntapAllAi.class)
|
||||||
|
|||||||
53
forge-ai/src/main/java/forge/ai/ability/AirbendAi.java
Normal file
53
forge-ai/src/main/java/forge/ai/ability/AirbendAi.java
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package forge.ai.ability;
|
||||||
|
|
||||||
|
import forge.ai.*;
|
||||||
|
import forge.game.card.Card;
|
||||||
|
import forge.game.card.CardCollection;
|
||||||
|
import forge.game.card.CardLists;
|
||||||
|
import forge.game.combat.Combat;
|
||||||
|
import forge.game.phase.PhaseHandler;
|
||||||
|
import forge.game.phase.PhaseType;
|
||||||
|
import forge.game.player.Player;
|
||||||
|
import forge.game.spellability.SpellAbility;
|
||||||
|
|
||||||
|
public class AirbendAi extends SpellAbilityAi {
|
||||||
|
@Override
|
||||||
|
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
|
||||||
|
// Check own cards that need saving, non-token, above CMC 2 so that it's hopefully worth saving this one
|
||||||
|
final Combat combat = aiPlayer.getGame().getCombat();
|
||||||
|
final CardCollection threatenedTgts = CardLists.filter(aiPlayer.getCreaturesInPlay(),
|
||||||
|
card -> !card.isToken() && card.getCMC() > 2 &&
|
||||||
|
(ComputerUtil.predictThreatenedObjects(aiPlayer, null, true).contains(card)
|
||||||
|
|| (combat.isAttacking(card) && combat.isBlocked(card) && ComputerUtilCombat.combatantWouldBeDestroyed(aiPlayer, card, combat))));
|
||||||
|
if (!threatenedTgts.isEmpty()) {
|
||||||
|
Card bestSaved = ComputerUtilCard.getBestAI(threatenedTgts);
|
||||||
|
sa.getTargets().add(bestSaved);
|
||||||
|
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check opponent's cards that need bouncing (only in the AI's own turn, main phase 1, or at the end of opponent's
|
||||||
|
// turn, to get rid of potential blockers)
|
||||||
|
PhaseHandler ph = aiPlayer.getGame().getPhaseHandler();
|
||||||
|
if (ph.is(PhaseType.MAIN1, aiPlayer) || (ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == aiPlayer)) {
|
||||||
|
final CardCollection opposingThreats = aiPlayer.getOpponents().getCreaturesInPlay();
|
||||||
|
if (!opposingThreats.isEmpty()) {
|
||||||
|
sa.getTargets().add(ComputerUtilCard.getBestAI(opposingThreats));
|
||||||
|
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: add logic to use it to remove threatening spells when the ability allows to target spells?
|
||||||
|
|
||||||
|
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||||
|
AiAbilityDecision decision = canPlay(aiPlayer, sa);
|
||||||
|
if (decision.willingToPlay() || mandatory) {
|
||||||
|
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||||
|
}
|
||||||
|
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import forge.ai.AiAbilityDecision;
|
|
||||||
import forge.ai.AiPlayDecision;
|
|
||||||
import forge.ai.*;
|
import forge.ai.*;
|
||||||
import forge.game.Game;
|
import forge.game.Game;
|
||||||
import forge.game.ability.AbilityUtils;
|
import forge.game.ability.AbilityUtils;
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
|
|||||||
} else if (top.getApi() == ApiType.Mana) {
|
} else if (top.getApi() == ApiType.Mana) {
|
||||||
// would lead to Stack Overflow by trying to play this again
|
// would lead to Stack Overflow by trying to play this again
|
||||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||||
} else if (top.getApi() == ApiType.DestroyAll || top.getApi() == ApiType.SacrificeAll || top.getApi() == ApiType.ChangeZoneAll || top.getApi() == ApiType.TapAll || top.getApi() == ApiType.UnattachAll) {
|
} else if (top.getApi() == ApiType.DestroyAll || top.getApi() == ApiType.SacrificeAll || top.getApi() == ApiType.ChangeZoneAll || top.getApi() == ApiType.TapAll) {
|
||||||
if (!top.usesTargeting() || top.getActivatingPlayer().equals(aiPlayer)) {
|
if (!top.usesTargeting() || top.getActivatingPlayer().equals(aiPlayer)) {
|
||||||
// If we activated a mass removal / mass tap / mass bounce / etc. spell, or if the opponent activated it but
|
// If we activated a mass removal / mass tap / mass bounce / etc. spell, or if the opponent activated it but
|
||||||
// it can't be retargeted, no reason to copy this spell since it'll probably do the same thing and is useless as a copy
|
// it can't be retargeted, no reason to copy this spell since it'll probably do the same thing and is useless as a copy
|
||||||
@@ -73,7 +73,7 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
|
|||||||
} else if (top.hasParam("ConditionManaSpent") || top.getHostCard().hasSVar("AINoCopy")) {
|
} else if (top.hasParam("ConditionManaSpent") || top.getHostCard().hasSVar("AINoCopy")) {
|
||||||
// Mana spent is not copied, so these spells generally do nothing when copied.
|
// Mana spent is not copied, so these spells generally do nothing when copied.
|
||||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||||
} else if (ComputerUtilCard.isCardRemAIDeck(top.getHostCard())) {
|
} else if (SpellApiToAi.Converter.get(top.getApi()) instanceof CannotPlayAi || ComputerUtilCard.isCardRemAIDeck(top.getHostCard())) {
|
||||||
// Don't try to copy anything you can't understand how to handle
|
// Don't try to copy anything you can't understand how to handle
|
||||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import forge.game.player.PlayerPredicates;
|
|||||||
import forge.game.spellability.AbilitySub;
|
import forge.game.spellability.AbilitySub;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
import forge.game.spellability.TargetRestrictions;
|
import forge.game.spellability.TargetRestrictions;
|
||||||
|
import forge.game.staticability.StaticAbility;
|
||||||
import forge.game.trigger.Trigger;
|
import forge.game.trigger.Trigger;
|
||||||
import forge.game.trigger.TriggerType;
|
import forge.game.trigger.TriggerType;
|
||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
@@ -105,7 +106,12 @@ public class CountersPutAi extends CountersAi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
int maxLevel = Integer.parseInt(sa.getParam("MaxLevel"));
|
int maxLevel = 0;
|
||||||
|
for (StaticAbility st : source.getStaticAbilities()) {
|
||||||
|
if (st.toString().startsWith("LEVEL ")) {
|
||||||
|
maxLevel = Math.max(maxLevel, Integer.parseInt(st.toString().substring(6, 7)));
|
||||||
|
}
|
||||||
|
}
|
||||||
return source.getCounters(CounterEnumType.LEVEL) < maxLevel;
|
return source.getCounters(CounterEnumType.LEVEL) < maxLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
61
forge-ai/src/main/java/forge/ai/ability/EarthbendAi.java
Normal file
61
forge-ai/src/main/java/forge/ai/ability/EarthbendAi.java
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
package forge.ai.ability;
|
||||||
|
|
||||||
|
import forge.ai.*;
|
||||||
|
import forge.game.card.Card;
|
||||||
|
import forge.game.card.CardCollection;
|
||||||
|
import forge.game.card.CardLists;
|
||||||
|
import forge.game.cost.Cost;
|
||||||
|
import forge.game.cost.CostPart;
|
||||||
|
import forge.game.cost.CostSacrifice;
|
||||||
|
import forge.game.player.Player;
|
||||||
|
import forge.game.spellability.SpellAbility;
|
||||||
|
|
||||||
|
public class EarthbendAi extends SpellAbilityAi {
|
||||||
|
@Override
|
||||||
|
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
|
||||||
|
CardCollection lands = aiPlayer.getLandsInPlay();
|
||||||
|
if (lands.isEmpty()) {
|
||||||
|
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
|
||||||
|
}
|
||||||
|
CardCollection fetchLands = CardLists.filter(lands, c -> {
|
||||||
|
for (final SpellAbility ability : c.getAllSpellAbilities()) {
|
||||||
|
if (ability.isActivatedAbility()) {
|
||||||
|
final Cost cost = ability.getPayCosts();
|
||||||
|
for (final CostPart part : cost.getCostParts()) {
|
||||||
|
if (!(part instanceof CostSacrifice)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
CostSacrifice sacCost = (CostSacrifice) part;
|
||||||
|
if (sacCost.payCostFromSource() && ComputerUtilCost.canPayCost(ability, c.getController(), false)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
Card tgtLand = null;
|
||||||
|
|
||||||
|
if (!fetchLands.isEmpty()) {
|
||||||
|
// Prioritize fetchlands as they can be reused later
|
||||||
|
tgtLand = ComputerUtilCard.getBestLandToAnimate(fetchLands);
|
||||||
|
} else {
|
||||||
|
tgtLand = ComputerUtilCard.getBestLandToAnimate(lands);
|
||||||
|
}
|
||||||
|
|
||||||
|
sa.getTargets().add(tgtLand);
|
||||||
|
|
||||||
|
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||||
|
AiAbilityDecision decision = canPlay(aiPlayer, sa);
|
||||||
|
if (decision.willingToPlay() || mandatory) {
|
||||||
|
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||||
|
}
|
||||||
|
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -124,13 +124,11 @@ public abstract class ManifestBaseAi extends SpellAbilityAi {
|
|||||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( MyRandom.getRandom().nextFloat() < .8) {
|
if (MyRandom.getRandom().nextFloat() < .8) {
|
||||||
// 80% chance to play a Manifest spell
|
// 80% chance to play a Manifest spell
|
||||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||||
} else {
|
|
||||||
// 20% chance to not play a Manifest spell
|
|
||||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
|
||||||
}
|
}
|
||||||
|
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ public class PermanentAi extends SpellAbilityAi {
|
|||||||
if ("SacToReduceCost".equals(sa.getParam("AILogic"))) {
|
if ("SacToReduceCost".equals(sa.getParam("AILogic"))) {
|
||||||
// reset X to better calculate
|
// reset X to better calculate
|
||||||
sa.setXManaCostPaid(0);
|
sa.setXManaCostPaid(0);
|
||||||
ManaCostBeingPaid paidCost = ComputerUtilMana.calculateManaCost(sa.getPayCosts(), sa, true, 0, false);
|
ManaCostBeingPaid paidCost = ComputerUtilMana.calculateManaCost(sa.getPayCosts(), sa, ai, true, 0, false);
|
||||||
|
|
||||||
int generic = paidCost.getGenericManaAmount();
|
int generic = paidCost.getGenericManaAmount();
|
||||||
// Set PayX here to maximum value.
|
// Set PayX here to maximum value.
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ public abstract class TapAiBase extends SpellAbilityAi {
|
|||||||
protected boolean tapPrefTargeting(final Player ai, final Card source, final SpellAbility sa, final boolean mandatory) {
|
protected boolean tapPrefTargeting(final Player ai, final Card source, final SpellAbility sa, final boolean mandatory) {
|
||||||
final Game game = ai.getGame();
|
final Game game = ai.getGame();
|
||||||
CardCollection tapList = CardLists.getTargetableCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), sa);
|
CardCollection tapList = CardLists.getTargetableCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), sa);
|
||||||
|
tapList = ComputerUtil.filterAITgts(sa, ai, tapList, false);
|
||||||
tapList = CardLists.filter(tapList, CardPredicates.CAN_TAP);
|
tapList = CardLists.filter(tapList, CardPredicates.CAN_TAP);
|
||||||
tapList = CardLists.filter(tapList, c -> {
|
tapList = CardLists.filter(tapList, c -> {
|
||||||
if (c.isCreature()) {
|
if (c.isCreature()) {
|
||||||
|
|||||||
49
forge-ai/src/main/java/forge/ai/ability/UnattachAi.java
Normal file
49
forge-ai/src/main/java/forge/ai/ability/UnattachAi.java
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package forge.ai.ability;
|
||||||
|
|
||||||
|
import forge.ai.*;
|
||||||
|
import forge.game.GameEntity;
|
||||||
|
import forge.game.ability.AbilityUtils;
|
||||||
|
import forge.game.card.Card;
|
||||||
|
import forge.game.player.Player;
|
||||||
|
import forge.game.spellability.SpellAbility;
|
||||||
|
import forge.util.collect.FCollection;
|
||||||
|
|
||||||
|
public class UnattachAi extends SpellAbilityAi {
|
||||||
|
|
||||||
|
/* (non-Javadoc)
|
||||||
|
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
||||||
|
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* (non-Javadoc)
|
||||||
|
* @see forge.card.abilityfactory.SpellAiLogic#doTriggerAINoCost(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility, boolean)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||||
|
final Card host = sa.getHostCard();
|
||||||
|
FCollection<GameEntity> targets = new FCollection<>();
|
||||||
|
if (!sa.usesTargeting()) {
|
||||||
|
targets = AbilityUtils.getDefinedEntities(host, sa.getParam("Defined"), sa);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mandatory && !targets.isEmpty()) {
|
||||||
|
Card attachment = (Card) targets.get(0);
|
||||||
|
if (attachment.isEquipment() && ai.getYourTeam().contains(attachment.getController())) {
|
||||||
|
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
||||||
|
}
|
||||||
|
|
||||||
|
// currently no card exists to get rid of curse aura this way
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
|
||||||
|
return doTriggerNoCost(ai, sa, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
package forge.ai.ability;
|
|
||||||
|
|
||||||
import forge.ai.*;
|
|
||||||
import forge.game.GameObject;
|
|
||||||
import forge.game.ability.AbilityUtils;
|
|
||||||
import forge.game.card.Card;
|
|
||||||
import forge.game.phase.PhaseType;
|
|
||||||
import forge.game.player.Player;
|
|
||||||
import forge.game.spellability.SpellAbility;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class UnattachAllAi extends SpellAbilityAi {
|
|
||||||
|
|
||||||
/* (non-Javadoc)
|
|
||||||
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
|
|
||||||
// Attach spells always have a target
|
|
||||||
if (sa.usesTargeting()) {
|
|
||||||
sa.resetTargets();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sa.getSVar("X").equals("Count$xPaid")) {
|
|
||||||
final int xPay = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
|
|
||||||
|
|
||||||
if (xPay == 0) {
|
|
||||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
|
||||||
}
|
|
||||||
|
|
||||||
sa.setXManaCostPaid(xPay);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ai.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_BLOCKERS)
|
|
||||||
&& !"Curse".equals(sa.getParam("AILogic"))) {
|
|
||||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* (non-Javadoc)
|
|
||||||
* @see forge.card.abilityfactory.SpellAiLogic#doTriggerAINoCost(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility, boolean)
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
|
||||||
final Card card = sa.getHostCard();
|
|
||||||
// Check if there are any valid targets
|
|
||||||
List<GameObject> targets = new ArrayList<>();
|
|
||||||
if (!sa.usesTargeting()) {
|
|
||||||
targets = AbilityUtils.getDefinedObjects(sa.getHostCard(), sa.getParam("Defined"), sa);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!mandatory && card.isEquipment() && !targets.isEmpty()) {
|
|
||||||
Card newTarget = (Card) targets.get(0);
|
|
||||||
//don't equip opponent creatures
|
|
||||||
if (!newTarget.getController().equals(ai)) {
|
|
||||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
|
||||||
}
|
|
||||||
|
|
||||||
//don't equip a worse creature
|
|
||||||
if (card.isEquipping()) {
|
|
||||||
Card oldTarget = card.getEquipping();
|
|
||||||
if (ComputerUtilCard.evaluateCreature(oldTarget) <= ComputerUtilCard.evaluateCreature(newTarget)) {
|
|
||||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
|
||||||
} else {
|
|
||||||
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
|
|
||||||
// AI should only activate this during Human's turn
|
|
||||||
return canPlay(ai, sa);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -66,10 +66,8 @@ public class UntapAi extends SpellAbilityAi {
|
|||||||
if (pDefined.isEmpty() || (pDefined.get(0).isTapped() && pDefined.get(0).getController() == ai)) {
|
if (pDefined.isEmpty() || (pDefined.get(0).isTapped() && pDefined.get(0).getController() == ai)) {
|
||||||
// If the defined card is tapped, or if there are no defined cards, we can play this ability
|
// If the defined card is tapped, or if there are no defined cards, we can play this ability
|
||||||
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
|
||||||
} else {
|
|
||||||
// Otherwise, we can't play this ability
|
|
||||||
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
|
||||||
}
|
}
|
||||||
|
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -776,7 +776,7 @@ public class StaticData {
|
|||||||
Queue<String> TOKEN_Q = new ConcurrentLinkedQueue<>();
|
Queue<String> TOKEN_Q = new ConcurrentLinkedQueue<>();
|
||||||
boolean nifHeader = false;
|
boolean nifHeader = false;
|
||||||
boolean cniHeader = false;
|
boolean cniHeader = false;
|
||||||
final Pattern funnyCardCollectorNumberPattern = Pattern.compile("^F\\d+");
|
final Pattern funnyCardCollectorNumberPattern = Pattern.compile("^F★?\\d+★?");
|
||||||
for (CardEdition e : editions) {
|
for (CardEdition e : editions) {
|
||||||
if (CardEdition.Type.FUNNY.equals(e.getType()))
|
if (CardEdition.Type.FUNNY.equals(e.getType()))
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -1066,10 +1066,8 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String getNormalizedName(final String cardName) {
|
public String getNormalizedName(final String cardName) {
|
||||||
String cardName1 = cardName;
|
|
||||||
// normalize Names first
|
// normalize Names first
|
||||||
cardName1 = normalizedNames.getOrDefault(cardName1, cardName1);
|
return normalizedNames.getOrDefault(cardName, cardName);
|
||||||
return cardName1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
package forge.card;
|
package forge.card;
|
||||||
|
|
||||||
import forge.card.mana.ManaCost;
|
import forge.card.mana.ManaCost;
|
||||||
|
import forge.util.Lang;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.Map.Entry;
|
import java.util.Map.Entry;
|
||||||
|
import java.util.regex.PatternSyntaxException;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -185,7 +187,25 @@ final class CardFace implements ICardFace, Cloneable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void assignMissingFieldsToVariant(CardFace variant) {
|
void assignMissingFieldsToVariant(CardFace variant) {
|
||||||
if(variant.oracleText == null) variant.oracleText = this.oracleText;
|
if(variant.oracleText == null) {
|
||||||
|
if(variant.flavorName != null && this.oracleText != null) {
|
||||||
|
try {
|
||||||
|
Lang lang = Lang.getInstance();
|
||||||
|
//Rudimentary name replacement. Can't do pronouns, ability words, or flavored keywords. Need to define variant text manually for that.
|
||||||
|
//Regex here checks for the name following either a word boundary or a literal "\n" string, since those haven't yet been converted to line breaks.
|
||||||
|
String flavoredText = this.oracleText.replaceAll("(?<=\\b|\\\\n)" + this.name + "\\b", variant.flavorName);
|
||||||
|
flavoredText = flavoredText.replaceAll("(?<=\\b|\\\\n)" + lang.getNickName(this.name) + "\\b", lang.getNickName(variant.flavorName));
|
||||||
|
variant.oracleText = flavoredText;
|
||||||
|
}
|
||||||
|
catch (PatternSyntaxException ignored) {
|
||||||
|
// Old versions of Android are weird about patterns sometimes. I don't *think* this is such a case but
|
||||||
|
// the documentation is unreliable. May be worth removing this once we're sure it's not a problem.
|
||||||
|
variant.oracleText = this.oracleText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
variant.oracleText = this.oracleText;
|
||||||
|
}
|
||||||
if(variant.manaCost == null) variant.manaCost = this.manaCost;
|
if(variant.manaCost == null) variant.manaCost = this.manaCost;
|
||||||
if(variant.color == null) variant.color = ColorSet.fromManaCost(variant.manaCost);
|
if(variant.color == null) variant.color = ColorSet.fromManaCost(variant.manaCost);
|
||||||
|
|
||||||
|
|||||||
@@ -475,11 +475,11 @@ public final class CardRules implements ICardCharacteristics {
|
|||||||
return getName();
|
return getName();
|
||||||
|
|
||||||
ICardFace mainFace = Objects.requireNonNullElse(mainPart.getFunctionalVariant(variantName), mainPart);
|
ICardFace mainFace = Objects.requireNonNullElse(mainPart.getFunctionalVariant(variantName), mainPart);
|
||||||
String mainPartName = Objects.requireNonNullElse(mainFace.getFlavorName(), mainFace.getName());
|
String mainPartName = mainFace.getDisplayName();
|
||||||
|
|
||||||
if(splitType.getAggregationMethod() == CardSplitType.FaceSelectionMethod.COMBINE) {
|
if(splitType.getAggregationMethod() == CardSplitType.FaceSelectionMethod.COMBINE) {
|
||||||
ICardFace otherFace = Objects.requireNonNullElse(otherPart.getFunctionalVariant(variantName), otherPart);
|
ICardFace otherFace = Objects.requireNonNullElse(otherPart.getFunctionalVariant(variantName), otherPart);
|
||||||
String otherPartName = Objects.requireNonNullElse(otherFace.getFlavorName(), otherFace.getName());
|
String otherPartName = otherFace.getDisplayName();
|
||||||
return mainPartName + " // " + otherPartName;
|
return mainPartName + " // " + otherPartName;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -504,16 +504,11 @@ public final class CardRules implements ICardCharacteristics {
|
|||||||
|
|
||||||
CardFace variantMain = ((CardFace) mainPart).getOrCreateFunctionalVariant(variantName);
|
CardFace variantMain = ((CardFace) mainPart).getOrCreateFunctionalVariant(variantName);
|
||||||
variantMain.setFlavorName(nameParts[0]);
|
variantMain.setFlavorName(nameParts[0]);
|
||||||
//Rudimentary name replacement. Can't do nicknames, pronouns, ability words, or flavored keywords. Need to define variants manually for that.
|
|
||||||
if(mainPart.getOracleText().contains(mainPart.getName()))
|
|
||||||
variantMain.setOracleText(mainPart.getOracleText().replace(mainPart.getName(), nameParts[0]));
|
|
||||||
((CardFace) mainPart).assignMissingFieldsToVariant(variantMain);
|
((CardFace) mainPart).assignMissingFieldsToVariant(variantMain);
|
||||||
|
|
||||||
if(otherPart != null) {
|
if(otherPart != null) {
|
||||||
CardFace variantOther = ((CardFace) otherPart).getOrCreateFunctionalVariant(variantName);
|
CardFace variantOther = ((CardFace) otherPart).getOrCreateFunctionalVariant(variantName);
|
||||||
variantOther.setFlavorName(nameParts[1]);
|
variantOther.setFlavorName(nameParts[1]);
|
||||||
if(otherPart.getOracleText().contains(otherPart.getName()))
|
|
||||||
variantMain.setOracleText(otherPart.getOracleText().replace(otherPart.getName(), nameParts[1]));
|
|
||||||
((CardFace) otherPart).assignMissingFieldsToVariant(variantOther);
|
((CardFace) otherPart).assignMissingFieldsToVariant(variantOther);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,15 @@ import java.util.Map;
|
|||||||
public interface ICardFace extends ICardCharacteristics, ICardRawAbilites, Comparable<ICardFace> {
|
public interface ICardFace extends ICardCharacteristics, ICardRawAbilites, Comparable<ICardFace> {
|
||||||
String getFlavorName();
|
String getFlavorName();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return this card's flavor name if it has one. Otherwise, the card's Oracle name.
|
||||||
|
*/
|
||||||
|
default String getDisplayName() {
|
||||||
|
if (this.getFlavorName() != null)
|
||||||
|
return this.getFlavorName();
|
||||||
|
return this.getName();
|
||||||
|
}
|
||||||
|
|
||||||
boolean hasFunctionalVariants();
|
boolean hasFunctionalVariants();
|
||||||
ICardFace getFunctionalVariant(String variant);
|
ICardFace getFunctionalVariant(String variant);
|
||||||
Map<String, ? extends ICardFace> getFunctionalVariants();
|
Map<String, ? extends ICardFace> getFunctionalVariants();
|
||||||
|
|||||||
@@ -75,11 +75,13 @@ public class Game {
|
|||||||
|
|
||||||
private List<Card> activePlanes = null;
|
private List<Card> activePlanes = null;
|
||||||
|
|
||||||
public final Phase cleanup;
|
|
||||||
public final Phase endOfCombat;
|
|
||||||
public final Phase endOfTurn;
|
|
||||||
public final Untap untap;
|
public final Untap untap;
|
||||||
public final Phase upkeep;
|
public final Phase upkeep;
|
||||||
|
public final Phase beginOfCombat;
|
||||||
|
public final Phase endOfCombat;
|
||||||
|
public final Phase endOfTurn;
|
||||||
|
public final Phase cleanup;
|
||||||
|
|
||||||
// to execute commands for "current" phase each time state based action is checked
|
// to execute commands for "current" phase each time state based action is checked
|
||||||
public final List<GameCommand> sbaCheckedCommandList;
|
public final List<GameCommand> sbaCheckedCommandList;
|
||||||
public final MagicStack stack;
|
public final MagicStack stack;
|
||||||
@@ -363,9 +365,10 @@ public class Game {
|
|||||||
|
|
||||||
untap = new Untap(this);
|
untap = new Untap(this);
|
||||||
upkeep = new Phase(PhaseType.UPKEEP);
|
upkeep = new Phase(PhaseType.UPKEEP);
|
||||||
cleanup = new Phase(PhaseType.CLEANUP);
|
beginOfCombat = new Phase(PhaseType.COMBAT_BEGIN);
|
||||||
endOfCombat = new Phase(PhaseType.COMBAT_END);
|
endOfCombat = new Phase(PhaseType.COMBAT_END);
|
||||||
endOfTurn = new Phase(PhaseType.END_OF_TURN);
|
endOfTurn = new Phase(PhaseType.END_OF_TURN);
|
||||||
|
cleanup = new Phase(PhaseType.CLEANUP);
|
||||||
|
|
||||||
sbaCheckedCommandList = new ArrayList<>();
|
sbaCheckedCommandList = new ArrayList<>();
|
||||||
|
|
||||||
@@ -428,6 +431,9 @@ public class Game {
|
|||||||
public final Phase getUpkeep() {
|
public final Phase getUpkeep() {
|
||||||
return upkeep;
|
return upkeep;
|
||||||
}
|
}
|
||||||
|
public final Phase getBeginOfCombat() {
|
||||||
|
return beginOfCombat;
|
||||||
|
}
|
||||||
public final Phase getEndOfCombat() {
|
public final Phase getEndOfCombat() {
|
||||||
return endOfCombat;
|
return endOfCombat;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ import org.jgrapht.graph.DefaultDirectedGraph;
|
|||||||
import org.jgrapht.graph.DefaultEdge;
|
import org.jgrapht.graph.DefaultEdge;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Methods for common actions performed during a game.
|
* Methods for common actions performed during a game.
|
||||||
@@ -78,7 +79,7 @@ public class GameAction {
|
|||||||
private boolean holdCheckingStaticAbilities = false;
|
private boolean holdCheckingStaticAbilities = false;
|
||||||
|
|
||||||
private final static Comparator<StaticAbility> effectOrder = Comparator.comparing(StaticAbility::isCharacteristicDefining).reversed()
|
private final static Comparator<StaticAbility> effectOrder = Comparator.comparing(StaticAbility::isCharacteristicDefining).reversed()
|
||||||
.thenComparing(s -> s.getHostCard().getLayerTimestamp());
|
.thenComparing(StaticAbility::getTimestamp);
|
||||||
|
|
||||||
public GameAction(Game game0) {
|
public GameAction(Game game0) {
|
||||||
game = game0;
|
game = game0;
|
||||||
@@ -702,7 +703,7 @@ public class GameAction {
|
|||||||
|
|
||||||
eff.addRemembered(copied);
|
eff.addRemembered(copied);
|
||||||
// refresh needed for canEnchant checks
|
// refresh needed for canEnchant checks
|
||||||
game.getAction().checkStaticAbilities(false, Sets.newHashSet(copied), new CardCollection(copied));
|
checkStaticAbilities(false, Sets.newHashSet(copied), new CardCollection(copied));
|
||||||
return eff;
|
return eff;
|
||||||
}
|
}
|
||||||
private void cleanStaticEffect(Card eff, Card copied) {
|
private void cleanStaticEffect(Card eff, Card copied) {
|
||||||
@@ -809,8 +810,7 @@ public class GameAction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move card in maingame if take card from subgame
|
// CR 720.4a Move card in maingame if take card from subgame
|
||||||
// 720.4a
|
|
||||||
if (zoneFrom != null && zoneFrom.is(ZoneType.Sideboard) && game.getMaingame() != null) {
|
if (zoneFrom != null && zoneFrom.is(ZoneType.Sideboard) && game.getMaingame() != null) {
|
||||||
Card maingameCard = c.getOwner().getMappingMaingameCard(c);
|
Card maingameCard = c.getOwner().getMappingMaingameCard(c);
|
||||||
if (maingameCard != null) {
|
if (maingameCard != null) {
|
||||||
@@ -913,7 +913,7 @@ public class GameAction {
|
|||||||
final PlayerZone removed = c.getOwner().getZone(ZoneType.Exile);
|
final PlayerZone removed = c.getOwner().getZone(ZoneType.Exile);
|
||||||
final Card copied = moveTo(removed, c, cause, params);
|
final Card copied = moveTo(removed, c, cause, params);
|
||||||
|
|
||||||
if (c.isImmutable()) {
|
if (c.isImmutable()) {
|
||||||
return copied;
|
return copied;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1103,11 +1103,16 @@ public class GameAction {
|
|||||||
if (stAb.checkMode(StaticAbilityMode.Continuous) && stAb.zonesCheck()) {
|
if (stAb.checkMode(StaticAbilityMode.Continuous) && stAb.zonesCheck()) {
|
||||||
staticAbilities.add(stAb);
|
staticAbilities.add(stAb);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!co.getStaticCommandList().isEmpty()) {
|
if (!co.getStaticCommandList().isEmpty()) {
|
||||||
staticList.add(co);
|
staticList.add(co);
|
||||||
}
|
}
|
||||||
return true;
|
for (StaticAbility stAb : co.getHiddenStaticAbilities()) {
|
||||||
|
if (stAb.checkMode(StaticAbilityMode.Continuous) && stAb.zonesCheck()) {
|
||||||
|
staticAbilities.add(stAb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
@@ -1138,7 +1143,7 @@ public class GameAction {
|
|||||||
affectedPerAbility.put(stAb, affectedHere);
|
affectedPerAbility.put(stAb, affectedHere);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 613.6 If an effect starts to apply in one layer and/or sublayer, it will continue to be applied
|
// CR 613.6 If an effect starts to apply in one layer and/or sublayer, it will continue to be applied
|
||||||
// to the same set of objects in each other applicable layer and/or sublayer,
|
// to the same set of objects in each other applicable layer and/or sublayer,
|
||||||
// even if the ability generating the effect is removed during this process.
|
// even if the ability generating the effect is removed during this process.
|
||||||
affectedHere = previouslyAffected;
|
affectedHere = previouslyAffected;
|
||||||
@@ -1154,7 +1159,7 @@ public class GameAction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 613.8c. After each effect is applied, the order of remaining effects is reevaluated
|
// CR 613.8c After each effect is applied, the order of remaining effects is reevaluated
|
||||||
// and may change if an effect that has not yet been applied becomes
|
// and may change if an effect that has not yet been applied becomes
|
||||||
// dependent on or independent of one or more other effects that have not yet been applied.
|
// dependent on or independent of one or more other effects that have not yet been applied.
|
||||||
}
|
}
|
||||||
@@ -1235,12 +1240,18 @@ public class GameAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private StaticAbility findStaticAbilityToApply(StaticAbilityLayer layer, List<StaticAbility> staticsForLayer, CardCollectionView preList, Map<StaticAbility, CardCollectionView> affectedPerAbility,
|
private StaticAbility findStaticAbilityToApply(StaticAbilityLayer layer, List<StaticAbility> staticsForLayer, CardCollectionView preList, Map<StaticAbility, CardCollectionView> affectedPerAbility,
|
||||||
Table<StaticAbility, StaticAbility, Set<StaticAbilityLayer>> dependencies) {
|
Table<StaticAbility, StaticAbility, Set<StaticAbilityLayer>> dependencies) {
|
||||||
|
StaticAbility first = staticsForLayer.get(0);
|
||||||
if (staticsForLayer.size() == 1) {
|
if (staticsForLayer.size() == 1) {
|
||||||
return staticsForLayer.get(0);
|
return first;
|
||||||
}
|
}
|
||||||
if (!StaticAbilityLayer.CONTINUOUS_LAYERS_WITH_DEPENDENCY.contains(layer)) {
|
if (!StaticAbilityLayer.CONTINUOUS_LAYERS_WITH_DEPENDENCY.contains(layer)) {
|
||||||
return staticsForLayer.get(0);
|
return first;
|
||||||
|
}
|
||||||
|
// CR 611.2c continuous effects from resolved abilities always affect the same objects the same way
|
||||||
|
Predicate<StaticAbility> isResolved = stAb -> stAb.getHostCard().isImmutable() && !stAb.getHostCard().isEmblem();
|
||||||
|
if (isResolved.test(first)) {
|
||||||
|
return first;
|
||||||
}
|
}
|
||||||
|
|
||||||
DefaultDirectedGraph<StaticAbility, DefaultEdge> dependencyGraph = new DefaultDirectedGraph<>(DefaultEdge.class);
|
DefaultDirectedGraph<StaticAbility, DefaultEdge> dependencyGraph = new DefaultDirectedGraph<>(DefaultEdge.class);
|
||||||
@@ -1248,6 +1259,10 @@ public class GameAction {
|
|||||||
for (StaticAbility stAb : staticsForLayer) {
|
for (StaticAbility stAb : staticsForLayer) {
|
||||||
dependencyGraph.addVertex(stAb);
|
dependencyGraph.addVertex(stAb);
|
||||||
|
|
||||||
|
if (isResolved.test(stAb)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
boolean exists = stAb.getHostCard().getStaticAbilities().contains(stAb);
|
boolean exists = stAb.getHostCard().getStaticAbilities().contains(stAb);
|
||||||
boolean compareAffected = false;
|
boolean compareAffected = false;
|
||||||
CardCollectionView affectedHere = affectedPerAbility.get(stAb);
|
CardCollectionView affectedHere = affectedPerAbility.get(stAb);
|
||||||
@@ -1255,7 +1270,7 @@ public class GameAction {
|
|||||||
affectedHere = StaticAbilityContinuous.getAffectedCards(stAb, preList);
|
affectedHere = StaticAbilityContinuous.getAffectedCards(stAb, preList);
|
||||||
compareAffected = true;
|
compareAffected = true;
|
||||||
}
|
}
|
||||||
List<Object> effectResults = generateStaticAbilityResult(layer, stAb);
|
Iterable<Object> effectResults = generateContinuousEffectChanges(layer, stAb);
|
||||||
|
|
||||||
for (StaticAbility otherStAb : staticsForLayer) {
|
for (StaticAbility otherStAb : staticsForLayer) {
|
||||||
if (stAb == otherStAb) {
|
if (stAb == otherStAb) {
|
||||||
@@ -1275,7 +1290,8 @@ public class GameAction {
|
|||||||
otherStAb.applyContinuousAbility(layer, affectedOther);
|
otherStAb.applyContinuousAbility(layer, affectedOther);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 613.8a. An effect is said to "depend on" another if
|
// CR 613.8a An effect is said to "depend on" another if
|
||||||
|
// * (a) + (c) already handled *
|
||||||
// (b) applying the other would change the text or the existence of the first effect...
|
// (b) applying the other would change the text or the existence of the first effect...
|
||||||
boolean dependency = exists != stAb.getHostCard().getStaticAbilities().contains(stAb);
|
boolean dependency = exists != stAb.getHostCard().getStaticAbilities().contains(stAb);
|
||||||
// ...what it applies to...
|
// ...what it applies to...
|
||||||
@@ -1285,7 +1301,7 @@ public class GameAction {
|
|||||||
}
|
}
|
||||||
// ...or what it does to any of the things it applies to
|
// ...or what it does to any of the things it applies to
|
||||||
if (!dependency) {
|
if (!dependency) {
|
||||||
List<Object> effectResultsAfterOther = generateStaticAbilityResult(layer, stAb);
|
Iterable<Object> effectResultsAfterOther = generateContinuousEffectChanges(layer, stAb);
|
||||||
dependency = !effectResults.equals(effectResultsAfterOther);
|
dependency = !effectResults.equals(effectResultsAfterOther);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1307,12 +1323,12 @@ public class GameAction {
|
|||||||
// when lucky the effect with the earliest timestamp has no dependency
|
// when lucky the effect with the earliest timestamp has no dependency
|
||||||
// then we can safely return it - otherwise we need to build the whole graph
|
// then we can safely return it - otherwise we need to build the whole graph
|
||||||
// because it might still be part of a loop
|
// because it might still be part of a loop
|
||||||
if (dependencyGraph.edgeSet().isEmpty() && stAb == staticsForLayer.get(0)) {
|
if (dependencyGraph.edgeSet().isEmpty() && stAb == first) {
|
||||||
return stAb;
|
return stAb;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 613.8b. If several dependent effects form a dependency loop, then this rule is ignored
|
// CR 613.8b If several dependent effects form a dependency loop, then this rule is ignored
|
||||||
List<List<StaticAbility>> cycles = new SzwarcfiterLauerSimpleCycles<>(dependencyGraph).findSimpleCycles();
|
List<List<StaticAbility>> cycles = new SzwarcfiterLauerSimpleCycles<>(dependencyGraph).findSimpleCycles();
|
||||||
for (List<StaticAbility> cyc : cycles) {
|
for (List<StaticAbility> cyc : cycles) {
|
||||||
for (int i = 0 ; i < cyc.size() - 1 ; i++) {
|
for (int i = 0 ; i < cyc.size() - 1 ; i++) {
|
||||||
@@ -1333,17 +1349,18 @@ public class GameAction {
|
|||||||
|
|
||||||
// now the earliest one left is the correct choice
|
// now the earliest one left is the correct choice
|
||||||
List<StaticAbility> statics = Lists.newArrayList(dependencyGraph.vertexSet());
|
List<StaticAbility> statics = Lists.newArrayList(dependencyGraph.vertexSet());
|
||||||
statics.sort(Comparator.comparing(s -> s.getHostCard().getLayerTimestamp()));
|
statics.sort(Comparator.comparing(StaticAbility::getTimestamp));
|
||||||
|
|
||||||
return statics.get(0);
|
return statics.get(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Object> generateStaticAbilityResult(StaticAbilityLayer layer, StaticAbility stAb) {
|
private Iterable<Object> generateContinuousEffectChanges(StaticAbilityLayer layer, StaticAbility stAb) {
|
||||||
List<Object> results = Lists.newArrayList();
|
List<Object> result = Collections.EMPTY_LIST;
|
||||||
if (layer == StaticAbilityLayer.CONTROL) {
|
if (layer == StaticAbilityLayer.CONTROL) {
|
||||||
results.addAll(AbilityUtils.getDefinedPlayers(stAb.getHostCard(), stAb.getParam("GainControl"), stAb));
|
result = Lists.newArrayList();
|
||||||
|
result.addAll(AbilityUtils.getDefinedPlayers(stAb.getHostCard(), stAb.getParam("GainControl"), stAb));
|
||||||
}
|
}
|
||||||
return results;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public final boolean checkStateEffects(final boolean runEvents) {
|
public final boolean checkStateEffects(final boolean runEvents) {
|
||||||
@@ -1776,7 +1793,7 @@ public class GameAction {
|
|||||||
if (sb.length() == 0) {
|
if (sb.length() == 0) {
|
||||||
sb.append(p).append(" ").append(Localizer.getInstance().getMessage("lblAssigns")).append("\n");
|
sb.append(p).append(" ").append(Localizer.getInstance().getMessage("lblAssigns")).append("\n");
|
||||||
}
|
}
|
||||||
String creature = CardTranslation.getTranslatedName(assignee.getName()) + " (" + assignee.getId() + ")";
|
String creature = assignee.getTranslatedName() + " (" + assignee.getId() + ")";
|
||||||
sb.append(creature).append(" ").append(sector).append("\n");
|
sb.append(creature).append(" ").append(sector).append("\n");
|
||||||
}
|
}
|
||||||
if (sb.length() > 0) {
|
if (sb.length() > 0) {
|
||||||
@@ -1793,7 +1810,7 @@ public class GameAction {
|
|||||||
c.getGame().getTracker().flush();
|
c.getGame().getTracker().flush();
|
||||||
|
|
||||||
c.setMoveToCommandZone(false);
|
c.setMoveToCommandZone(false);
|
||||||
if (c.getOwner().getController().confirmAction(c.getFirstSpellAbility(), PlayerActionConfirmMode.ChangeZoneToAltDestination, c.getName() + ": If a commander is in a graveyard or in exile and that card was put into that zone since the last time state-based actions were checked, its owner may put it into the command zone.", null)) {
|
if (c.getOwner().getController().confirmAction(c.getFirstSpellAbility(), PlayerActionConfirmMode.ChangeZoneToAltDestination, c.getDisplayName() + ": If a commander is in a graveyard or in exile and that card was put into that zone since the last time state-based actions were checked, its owner may put it into the command zone.", null)) {
|
||||||
moveTo(c.getOwner().getZone(ZoneType.Command), c, null, mapParams);
|
moveTo(c.getOwner().getZone(ZoneType.Command), c, null, mapParams);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -2075,7 +2092,7 @@ public class GameAction {
|
|||||||
}
|
}
|
||||||
if (showRevealDialog) {
|
if (showRevealDialog) {
|
||||||
final String message = Localizer.getInstance().getMessage("lblSacrifice");
|
final String message = Localizer.getInstance().getMessage("lblSacrifice");
|
||||||
game.getAction().reveal(result, ZoneType.Graveyard, c.getOwner(), false, message, false);
|
reveal(result, ZoneType.Graveyard, c.getOwner(), false, message, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (Map.Entry<Player, Collection<Card>> e : lki.asMap().entrySet()) {
|
for (Map.Entry<Player, Collection<Card>> e : lki.asMap().entrySet()) {
|
||||||
@@ -2217,7 +2234,7 @@ public class GameAction {
|
|||||||
/** Delivers a message to all players. (use reveal to show Cards) */
|
/** Delivers a message to all players. (use reveal to show Cards) */
|
||||||
public void notifyOfValue(SpellAbility saSource, GameObject relatedTarget, String value, Player playerExcept) {
|
public void notifyOfValue(SpellAbility saSource, GameObject relatedTarget, String value, Player playerExcept) {
|
||||||
if (saSource != null) {
|
if (saSource != null) {
|
||||||
String name = CardTranslation.getTranslatedName(saSource.getHostCard().getName());
|
String name = saSource.getHostCard().getTranslatedName();
|
||||||
value = TextUtil.fastReplace(value, "CARDNAME", name);
|
value = TextUtil.fastReplace(value, "CARDNAME", name);
|
||||||
value = TextUtil.fastReplace(value, "NICKNAME", Lang.getInstance().getNickName(name));
|
value = TextUtil.fastReplace(value, "NICKNAME", Lang.getInstance().getNickName(name));
|
||||||
}
|
}
|
||||||
@@ -2420,7 +2437,7 @@ public class GameAction {
|
|||||||
// it to either player or the papercard object so it feels like rule based for the player side..
|
// it to either player or the papercard object so it feels like rule based for the player side..
|
||||||
if (!c.hasMarkedColor()) {
|
if (!c.hasMarkedColor()) {
|
||||||
if (takesAction.isAI()) {
|
if (takesAction.isAI()) {
|
||||||
String prompt = CardTranslation.getTranslatedName(c.getName()) + ": " +
|
String prompt = c.getTranslatedName() + ": " +
|
||||||
Localizer.getInstance().getMessage("lblChooseNColors", Lang.getNumeral(2));
|
Localizer.getInstance().getMessage("lblChooseNColors", Lang.getNumeral(2));
|
||||||
SpellAbility sa = new SpellAbility.EmptySa(ApiType.ChooseColor, c, takesAction);
|
SpellAbility sa = new SpellAbility.EmptySa(ApiType.ChooseColor, c, takesAction);
|
||||||
sa.putParam("AILogic", "MostProminentInComputerDeck");
|
sa.putParam("AILogic", "MostProminentInComputerDeck");
|
||||||
@@ -2534,19 +2551,9 @@ public class GameAction {
|
|||||||
game.getTriggerHandler().runTrigger(TriggerType.TakesInitiative, runParams, false);
|
game.getTriggerHandler().runTrigger(TriggerType.TakesInitiative, runParams, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make scry an action function so that it can be used for mulligans (with a null cause)
|
|
||||||
// Assumes that the list of players is in APNAP order, which should be the case
|
|
||||||
// Optional here as well to handle the way that mulligans do the choice
|
|
||||||
// 701.17. Scry
|
|
||||||
// 701.17a To "scry N" means to look at the top N cards of your library, then put any number of them
|
|
||||||
// on the bottom of your library in any order and the rest on top of your library in any order.
|
|
||||||
// 701.17b If a player is instructed to scry 0, no scry event occurs. Abilities that trigger whenever a
|
|
||||||
// player scries won't trigger.
|
|
||||||
// 701.17c If multiple players scry at once, each of those players looks at the top cards of their library
|
|
||||||
// at the same time. Those players decide in APNAP order (see rule 101.4) where to put those
|
|
||||||
// cards, then those cards move at the same time.
|
|
||||||
public void scry(final List<Player> players, int numScry, SpellAbility cause) {
|
public void scry(final List<Player> players, int numScry, SpellAbility cause) {
|
||||||
if (numScry <= 0) {
|
if (numScry <= 0) {
|
||||||
|
// CR 701.22b If a player is instructed to scry 0, no scry event occurs.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2572,12 +2579,9 @@ public class GameAction {
|
|||||||
if (playerScry > 0) {
|
if (playerScry > 0) {
|
||||||
actualPlayers.put(p, playerScry);
|
actualPlayers.put(p, playerScry);
|
||||||
|
|
||||||
// reveal the top N library cards to the player (only)
|
// no real need to separate out the look if there is only one player scrying
|
||||||
// no real need to separate out the look if
|
|
||||||
// there is only one player scrying
|
|
||||||
if (players.size() > 1) {
|
if (players.size() > 1) {
|
||||||
final CardCollection topN = new CardCollection(p.getCardsIn(ZoneType.Library, playerScry));
|
revealTo(p.getCardsIn(ZoneType.Library, playerScry), p);
|
||||||
revealTo(topN, p);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2618,7 +2622,6 @@ public class GameAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (cause != null) {
|
if (cause != null) {
|
||||||
// set up triggers (but not actually do them until later)
|
|
||||||
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(p);
|
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(p);
|
||||||
runParams.put(AbilityKey.ScryNum, numLookedAt);
|
runParams.put(AbilityKey.ScryNum, numLookedAt);
|
||||||
runParams.put(AbilityKey.ScryBottom, toBottom == null ? 0 : toBottom.size());
|
runParams.put(AbilityKey.ScryBottom, toBottom == null ? 0 : toBottom.size());
|
||||||
@@ -2649,7 +2652,7 @@ public class GameAction {
|
|||||||
if (showRevealDialog) {
|
if (showRevealDialog) {
|
||||||
final String message = Localizer.getInstance().getMessage("lblMilledCards");
|
final String message = Localizer.getInstance().getMessage("lblMilledCards");
|
||||||
final boolean addSuffix = !toZoneStr.isEmpty();
|
final boolean addSuffix = !toZoneStr.isEmpty();
|
||||||
game.getAction().reveal(milledPlayer, destination, p, false, message, addSuffix);
|
reveal(milledPlayer, destination, p, false, message, addSuffix);
|
||||||
}
|
}
|
||||||
game.getGameLog().add(GameLogEntryType.ZONE_CHANGE, p + " milled " +
|
game.getGameLog().add(GameLogEntryType.ZONE_CHANGE, p + " milled " +
|
||||||
Lang.joinHomogenous(milledPlayer) + toZoneStr + ".");
|
Lang.joinHomogenous(milledPlayer) + toZoneStr + ".");
|
||||||
@@ -2666,7 +2669,7 @@ public class GameAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void dealDamage(final boolean isCombat, final CardDamageMap damageMap, final CardDamageMap preventMap,
|
public void dealDamage(final boolean isCombat, final CardDamageMap damageMap, final CardDamageMap preventMap,
|
||||||
final GameEntityCounterTable counterTable, final SpellAbility cause) {
|
final GameEntityCounterTable counterTable, final SpellAbility cause) {
|
||||||
// Clear assigned damage if is combat
|
// Clear assigned damage if is combat
|
||||||
if (isCombat) {
|
if (isCombat) {
|
||||||
for (Map.Entry<GameEntity, Map<Card, Integer>> et : damageMap.columnMap().entrySet()) {
|
for (Map.Entry<GameEntity, Map<Card, Integer>> et : damageMap.columnMap().entrySet()) {
|
||||||
@@ -2806,7 +2809,7 @@ public class GameAction {
|
|||||||
final FCollection<Player> players = game.getPlayers().filter(PlayerPredicates.canBeAttached(source, null));
|
final FCollection<Player> players = game.getPlayers().filter(PlayerPredicates.canBeAttached(source, null));
|
||||||
|
|
||||||
final Player pa = p.getController().chooseSingleEntityForEffect(players, aura,
|
final Player pa = p.getController().chooseSingleEntityForEffect(players, aura,
|
||||||
Localizer.getInstance().getMessage("lblSelectAPlayerAttachSourceTo", CardTranslation.getTranslatedName(source.getName())), null);
|
Localizer.getInstance().getMessage("lblSelectAPlayerAttachSourceTo", source.getTranslatedName()), null);
|
||||||
if (pa != null) {
|
if (pa != null) {
|
||||||
source.attachToEntity(pa, null, true);
|
source.attachToEntity(pa, null, true);
|
||||||
return true;
|
return true;
|
||||||
@@ -2831,7 +2834,7 @@ public class GameAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final Card o = p.getController().chooseSingleEntityForEffect(list, aura,
|
final Card o = p.getController().chooseSingleEntityForEffect(list, aura,
|
||||||
Localizer.getInstance().getMessage("lblSelectACardAttachSourceTo", CardTranslation.getTranslatedName(source.getName())), null);
|
Localizer.getInstance().getMessage("lblSelectACardAttachSourceTo", source.getTranslatedName()), null);
|
||||||
if (o != null) {
|
if (o != null) {
|
||||||
source.attachToEntity(game.getCardState(o), null, true);
|
source.attachToEntity(game.getCardState(o), null, true);
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ import java.util.EnumSet;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>
|
* <p>
|
||||||
* GameActionUtil class.
|
* GameActionUtil class.
|
||||||
@@ -633,7 +632,7 @@ public final class GameActionUtil {
|
|||||||
}
|
}
|
||||||
} else if (o.equals("Conspire")) {
|
} else if (o.equals("Conspire")) {
|
||||||
final String conspireCost = "tapXType<2/Creature.SharesColorWith/" +
|
final String conspireCost = "tapXType<2/Creature.SharesColorWith/" +
|
||||||
"creature that shares a color with " + host.getName() + ">";
|
"creature that shares a color with " + host.getDisplayName() + ">";
|
||||||
final Cost cost = new Cost(conspireCost, false);
|
final Cost cost = new Cost(conspireCost, false);
|
||||||
String str = "Pay for Conspire? " + cost.toSimpleString();
|
String str = "Pay for Conspire? " + cost.toSimpleString();
|
||||||
|
|
||||||
@@ -744,7 +743,7 @@ public final class GameActionUtil {
|
|||||||
for (KeywordInterface ki : c.getKeywords()) {
|
for (KeywordInterface ki : c.getKeywords()) {
|
||||||
if (kw.equals(ki.getOriginal())) {
|
if (kw.equals(ki.getOriginal())) {
|
||||||
final Cost cost = new Cost(ManaCost.ONE, false);
|
final Cost cost = new Cost(ManaCost.ONE, false);
|
||||||
String str = "Choose Amount for " + c.getName() + ": " + cost.toSimpleString();
|
String str = "Choose Amount for " + c.getDisplayName() + ": " + cost.toSimpleString();
|
||||||
|
|
||||||
int v = pc.chooseNumberForKeywordCost(sa, cost, ki, str, Integer.MAX_VALUE);
|
int v = pc.chooseNumberForKeywordCost(sa, cost, ki, str, Integer.MAX_VALUE);
|
||||||
|
|
||||||
@@ -859,8 +858,6 @@ public final class GameActionUtil {
|
|||||||
}
|
}
|
||||||
} else if (sa.getApi() == ApiType.ManaReflected) {
|
} else if (sa.getApi() == ApiType.ManaReflected) {
|
||||||
baseMana = abMana.getExpressChoice();
|
baseMana = abMana.getExpressChoice();
|
||||||
} else if (abMana.isSpecialMana()) {
|
|
||||||
baseMana = abMana.getExpressChoice();
|
|
||||||
} else {
|
} else {
|
||||||
baseMana = abMana.mana(sa);
|
baseMana = abMana.mana(sa);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -229,18 +229,18 @@ public abstract class GameEntity implements GameObject, IIdentifiable {
|
|||||||
}
|
}
|
||||||
public String cantBeAttachedMsg(final Card attach, SpellAbility sa, boolean checkSBA) {
|
public String cantBeAttachedMsg(final Card attach, SpellAbility sa, boolean checkSBA) {
|
||||||
if (!attach.isAttachment()) {
|
if (!attach.isAttachment()) {
|
||||||
return attach.getName() + " is not an attachment";
|
return attach.getDisplayName() + " is not an attachment";
|
||||||
}
|
}
|
||||||
if (equals(attach)) {
|
if (equals(attach)) {
|
||||||
return attach.getName() + " can't attach to itself";
|
return attach.getDisplayName() + " can't attach to itself";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attach.isCreature() && !attach.hasKeyword(Keyword.RECONFIGURE)) {
|
if (attach.isCreature() && !attach.hasKeyword(Keyword.RECONFIGURE)) {
|
||||||
return attach.getName() + " is a creature without reconfigure";
|
return attach.getDisplayName() + " is a creature without reconfigure";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attach.isPhasedOut()) {
|
if (attach.isPhasedOut()) {
|
||||||
return attach.getName() + " is phased out";
|
return attach.getDisplayName() + " is phased out";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attach.isAura()) {
|
if (attach.isAura()) {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import forge.game.*;
|
|||||||
import forge.game.ability.AbilityFactory.AbilityRecordType;
|
import forge.game.ability.AbilityFactory.AbilityRecordType;
|
||||||
import forge.game.card.*;
|
import forge.game.card.*;
|
||||||
import forge.game.cost.Cost;
|
import forge.game.cost.Cost;
|
||||||
|
import forge.game.cost.CostAdjustment;
|
||||||
import forge.game.cost.IndividualCostPaymentInstance;
|
import forge.game.cost.IndividualCostPaymentInstance;
|
||||||
import forge.game.keyword.Keyword;
|
import forge.game.keyword.Keyword;
|
||||||
import forge.game.keyword.KeywordInterface;
|
import forge.game.keyword.KeywordInterface;
|
||||||
@@ -1527,6 +1528,7 @@ public class AbilityUtils {
|
|||||||
else {
|
else {
|
||||||
cost = new Cost(unlessCost, true);
|
cost = new Cost(unlessCost, true);
|
||||||
}
|
}
|
||||||
|
cost = CostAdjustment.adjust(cost, sa, true);
|
||||||
return cost;
|
return cost;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -198,7 +198,6 @@ public enum ApiType {
|
|||||||
Token (TokenEffect.class),
|
Token (TokenEffect.class),
|
||||||
TwoPiles (TwoPilesEffect.class),
|
TwoPiles (TwoPilesEffect.class),
|
||||||
Unattach (UnattachEffect.class),
|
Unattach (UnattachEffect.class),
|
||||||
UnattachAll (UnattachAllEffect.class),
|
|
||||||
UnlockDoor (UnlockDoorEffect.class),
|
UnlockDoor (UnlockDoorEffect.class),
|
||||||
Untap (UntapEffect.class),
|
Untap (UntapEffect.class),
|
||||||
UntapAll (UntapAllEffect.class),
|
UntapAll (UntapAllEffect.class),
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ public abstract class SpellAbilityEffect {
|
|||||||
sb.append(TextUtil.enclosedParen(TextUtil.concatNoSpace("X","=",String.valueOf(amount))));
|
sb.append(TextUtil.enclosedParen(TextUtil.concatNoSpace("X","=",String.valueOf(amount))));
|
||||||
}
|
}
|
||||||
|
|
||||||
String currentName = CardTranslation.getTranslatedName(sa.getHostCard().getName());
|
String currentName = sa.getHostCard().getTranslatedName();
|
||||||
String substitutedDesc = TextUtil.fastReplace(sb.toString(), "CARDNAME", currentName);
|
String substitutedDesc = TextUtil.fastReplace(sb.toString(), "CARDNAME", currentName);
|
||||||
substitutedDesc = TextUtil.fastReplace(substitutedDesc, "NICKNAME", Lang.getInstance().getNickName(currentName));
|
substitutedDesc = TextUtil.fastReplace(substitutedDesc, "NICKNAME", Lang.getInstance().getNickName(currentName));
|
||||||
return substitutedDesc;
|
return substitutedDesc;
|
||||||
@@ -670,7 +670,7 @@ public abstract class SpellAbilityEffect {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// build an Effect with that information
|
// build an Effect with that information
|
||||||
String name = host.getName() + "'s Effect";
|
String name = host.getDisplayName() + "'s Effect";
|
||||||
|
|
||||||
final Card eff = createEffect(sa, controller, name, host.getImageKey());
|
final Card eff = createEffect(sa, controller, name, host.getImageKey());
|
||||||
if (cards != null) {
|
if (cards != null) {
|
||||||
@@ -735,7 +735,7 @@ public abstract class SpellAbilityEffect {
|
|||||||
Map<String, Object> params = Maps.newHashMap();
|
Map<String, Object> params = Maps.newHashMap();
|
||||||
params.put("Attacker", c);
|
params.put("Attacker", c);
|
||||||
defender = sa.getActivatingPlayer().getController().chooseSingleEntityForEffect(defs, sa,
|
defender = sa.getActivatingPlayer().getController().chooseSingleEntityForEffect(defs, sa,
|
||||||
Localizer.getInstance().getMessage("lblChooseDefenderToAttackWithCard", CardTranslation.getTranslatedName(c.getName())), false, params);
|
Localizer.getInstance().getMessage("lblChooseDefenderToAttackWithCard", c.getTranslatedName()), false, params);
|
||||||
|
|
||||||
if (defender != null && !combat.getAttackersOf(defender).contains(c)) {
|
if (defender != null && !combat.getAttackersOf(defender).contains(c)) {
|
||||||
// we might be reselecting
|
// we might be reselecting
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import forge.game.player.Player;
|
|||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
import forge.game.trigger.TriggerType;
|
import forge.game.trigger.TriggerType;
|
||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
import forge.util.CardTranslation;
|
|
||||||
import forge.util.Localizer;
|
import forge.util.Localizer;
|
||||||
|
|
||||||
public class AbandonEffect extends SpellAbilityEffect {
|
public class AbandonEffect extends SpellAbilityEffect {
|
||||||
@@ -24,7 +23,7 @@ public class AbandonEffect extends SpellAbilityEffect {
|
|||||||
Player controller = source.getController();
|
Player controller = source.getController();
|
||||||
|
|
||||||
boolean isOptional = sa.hasParam("Optional");
|
boolean isOptional = sa.hasParam("Optional");
|
||||||
if (isOptional && !controller.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblWouldYouLikeAbandonSource", CardTranslation.getTranslatedName(source.getName())), null)) {
|
if (isOptional && !controller.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblWouldYouLikeAbandonSource", source.getTranslatedName()), null)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ public class AddTurnEffect extends SpellAbilityEffect {
|
|||||||
public static void createCantSetSchemesInMotionEffect(SpellAbility sa) {
|
public static void createCantSetSchemesInMotionEffect(SpellAbility sa) {
|
||||||
final Card hostCard = sa.getHostCard();
|
final Card hostCard = sa.getHostCard();
|
||||||
final Game game = hostCard.getGame();
|
final Game game = hostCard.getGame();
|
||||||
final String name = hostCard.getName() + "'s Effect";
|
final String name = hostCard.getDisplayName() + "'s Effect";
|
||||||
final String image = hostCard.getImageKey();
|
final String image = hostCard.getImageKey();
|
||||||
|
|
||||||
final Card eff = createEffect(sa, sa.getActivatingPlayer(), name, image);
|
final Card eff = createEffect(sa, sa.getActivatingPlayer(), name, image);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import forge.game.card.Card;
|
|||||||
import forge.game.card.CardZoneTable;
|
import forge.game.card.CardZoneTable;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
|
import forge.game.spellability.SpellAbilityStackInstance;
|
||||||
import forge.game.trigger.TriggerType;
|
import forge.game.trigger.TriggerType;
|
||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
import forge.util.Lang;
|
import forge.util.Lang;
|
||||||
@@ -47,7 +48,7 @@ public class AirbendEffect extends SpellAbilityEffect {
|
|||||||
|
|
||||||
final CardZoneTable triggerList = CardZoneTable.getSimultaneousInstance(sa);
|
final CardZoneTable triggerList = CardZoneTable.getSimultaneousInstance(sa);
|
||||||
|
|
||||||
for (Card c : getTargetCards(sa)) {
|
for (Card c : getCardsfromTargets(sa)) {
|
||||||
final Card gameCard = game.getCardState(c, null);
|
final Card gameCard = game.getCardState(c, null);
|
||||||
// gameCard is LKI in that case, the card is not in game anymore
|
// gameCard is LKI in that case, the card is not in game anymore
|
||||||
// or the timestamp did change
|
// or the timestamp did change
|
||||||
@@ -55,21 +56,27 @@ public class AirbendEffect extends SpellAbilityEffect {
|
|||||||
if (gameCard == null || !c.equalsWithGameTimestamp(gameCard) || gameCard.isPhasedOut()) {
|
if (gameCard == null || !c.equalsWithGameTimestamp(gameCard) || gameCard.isPhasedOut()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!gameCard.canExiledBy(sa, true)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
handleExiledWith(gameCard, sa);
|
handleExiledWith(gameCard, sa);
|
||||||
|
|
||||||
Map<AbilityKey, Object> moveParams = AbilityKey.newMap();
|
Map<AbilityKey, Object> moveParams = AbilityKey.newMap();
|
||||||
AbilityKey.addCardZoneTableParams(moveParams, triggerList);
|
AbilityKey.addCardZoneTableParams(moveParams, triggerList);
|
||||||
|
|
||||||
Card movedCard = game.getAction().exile(gameCard, sa, moveParams);
|
SpellAbilityStackInstance si = null;
|
||||||
|
if (gameCard.isInZone(ZoneType.Stack)) {
|
||||||
|
SpellAbility stackSA = game.getStack().getSpellMatchingHost(gameCard);
|
||||||
|
si = game.getStack().getInstanceMatchingSpellAbilityID(stackSA);
|
||||||
|
}
|
||||||
|
|
||||||
|
Card movedCard = game.getAction().exile(gameCard, sa, moveParams);
|
||||||
if (movedCard == null || !movedCard.isInZone(ZoneType.Exile)) {
|
if (movedCard == null || !movedCard.isInZone(ZoneType.Exile)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (si != null) {
|
||||||
|
// GameAction.changeZone should really take care of cleaning up SASI when a card from the stack is removed.
|
||||||
|
game.getStack().remove(si);
|
||||||
|
}
|
||||||
|
|
||||||
// Effect to cast for 2 from exile
|
// Effect to cast for 2 from exile
|
||||||
Card eff = createEffect(sa, movedCard.getOwner(), "Airbend" + movedCard, hostCard.getImageKey());
|
Card eff = createEffect(sa, movedCard.getOwner(), "Airbend" + movedCard, hostCard.getImageKey());
|
||||||
eff.addRemembered(movedCard);
|
eff.addRemembered(movedCard);
|
||||||
@@ -84,10 +91,14 @@ public class AirbendEffect extends SpellAbilityEffect {
|
|||||||
|
|
||||||
game.getAction().moveToCommand(eff, sa);
|
game.getAction().moveToCommand(eff, sa);
|
||||||
}
|
}
|
||||||
|
|
||||||
triggerList.triggerChangesZoneAll(game, sa);
|
triggerList.triggerChangesZoneAll(game, sa);
|
||||||
handleExiledWith(triggerList.allCards(), sa);
|
handleExiledWith(triggerList.allCards(), sa);
|
||||||
|
|
||||||
pl.triggerElementalBend(TriggerType.Airbend);
|
// CR 701.65b
|
||||||
|
if (!triggerList.isEmpty()) {
|
||||||
|
pl.triggerElementalBend(TriggerType.Airbend);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import forge.game.card.CardPredicates;
|
|||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
import forge.util.CardTranslation;
|
|
||||||
import forge.util.Lang;
|
import forge.util.Lang;
|
||||||
import forge.util.Localizer;
|
import forge.util.Localizer;
|
||||||
import forge.util.collect.FCollection;
|
import forge.util.collect.FCollection;
|
||||||
@@ -124,7 +123,7 @@ public class AttachEffect extends SpellAbilityEffect {
|
|||||||
}
|
}
|
||||||
String attachToName;
|
String attachToName;
|
||||||
if (attachTo instanceof Card) {
|
if (attachTo instanceof Card) {
|
||||||
attachToName = CardTranslation.getTranslatedName(((Card)attachTo).getName());
|
attachToName = ((Card) attachTo).getTranslatedName();
|
||||||
} else {
|
} else {
|
||||||
attachToName = attachTo.toString();
|
attachToName = attachTo.toString();
|
||||||
}
|
}
|
||||||
@@ -141,7 +140,7 @@ public class AttachEffect extends SpellAbilityEffect {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
String message = Localizer.getInstance().getMessage("lblDoYouWantAttachSourceToTarget", CardTranslation.getTranslatedName(attachment.getName()), attachToName);
|
String message = Localizer.getInstance().getMessage("lblDoYouWantAttachSourceToTarget", attachment.getTranslatedName(), attachToName);
|
||||||
if (sa.hasParam("Optional") && !activator.getController().confirmAction(sa, null, message, null))
|
if (sa.hasParam("Optional") && !activator.getController().confirmAction(sa, null, message, null))
|
||||||
// TODO add params for message
|
// TODO add params for message
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import forge.game.event.GameEventCombatChanged;
|
|||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
import forge.game.spellability.SpellAbilityStackInstance;
|
import forge.game.spellability.SpellAbilityStackInstance;
|
||||||
import forge.util.CardTranslation;
|
|
||||||
import forge.util.Lang;
|
import forge.util.Lang;
|
||||||
import forge.util.Localizer;
|
import forge.util.Localizer;
|
||||||
|
|
||||||
@@ -35,7 +34,7 @@ public class ChangeCombatantsEffect extends SpellAbilityEffect {
|
|||||||
|
|
||||||
// TODO: may expand this effect for defined blocker (False Orders, General Jarkeld, Sorrow's Path, Ydwen Efreet)
|
// TODO: may expand this effect for defined blocker (False Orders, General Jarkeld, Sorrow's Path, Ydwen Efreet)
|
||||||
for (final Card c : getTargetCards(sa)) {
|
for (final Card c : getTargetCards(sa)) {
|
||||||
String cardString = CardTranslation.getTranslatedName(c.getName()) + " (" + c.getId() + ")";
|
String cardString = c.getTranslatedName() + " (" + c.getId() + ")";
|
||||||
if (isOptional && !activator.getController().confirmAction(sa, null,
|
if (isOptional && !activator.getController().confirmAction(sa, null,
|
||||||
Localizer.getInstance().getMessage("lblChangeCombatantOption", cardString), null)) {
|
Localizer.getInstance().getMessage("lblChangeCombatantOption", cardString), null)) {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -553,7 +553,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
|
|||||||
hostCard.addRemembered(CardCopyService.getLKICopy(gameCard));
|
hostCard.addRemembered(CardCopyService.getLKICopy(gameCard));
|
||||||
}
|
}
|
||||||
|
|
||||||
final String prompt = TextUtil.concatWithSpace(Localizer.getInstance().getMessage("lblDoYouWantMoveTargetFromOriToDest", CardTranslation.getTranslatedName(gameCard.getName()), Lang.joinHomogenous(origin, ZoneType::getTranslatedName), destination.getTranslatedName()));
|
final String prompt = TextUtil.concatWithSpace(Localizer.getInstance().getMessage("lblDoYouWantMoveTargetFromOriToDest", gameCard.getTranslatedName(), Lang.joinHomogenous(origin, ZoneType::getTranslatedName), destination.getTranslatedName()));
|
||||||
if (optional && !chooser.getController().confirmAction(sa, null, prompt, null)) {
|
if (optional && !chooser.getController().confirmAction(sa, null, prompt, null)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -710,7 +710,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
|
|||||||
list = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), sa.getParam("AttachAfter"), hostCard.getController(), hostCard, sa);
|
list = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), sa.getParam("AttachAfter"), hostCard.getController(), hostCard, sa);
|
||||||
}
|
}
|
||||||
if (!list.isEmpty()) {
|
if (!list.isEmpty()) {
|
||||||
String title = Localizer.getInstance().getMessage("lblSelectACardAttachSourceTo", CardTranslation.getTranslatedName(gameCard.getName()));
|
String title = Localizer.getInstance().getMessage("lblSelectACardAttachSourceTo", gameCard.getTranslatedName());
|
||||||
Map<String, Object> params = Maps.newHashMap();
|
Map<String, Object> params = Maps.newHashMap();
|
||||||
params.put("Attach", gameCard);
|
params.put("Attach", gameCard);
|
||||||
Card attachedTo = chooser.getController().chooseSingleEntityForEffect(list, sa, title, params);
|
Card attachedTo = chooser.getController().chooseSingleEntityForEffect(list, sa, title, params);
|
||||||
@@ -735,7 +735,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
|
|||||||
|
|
||||||
if (ZoneType.Hand.equals(destination) && ZoneType.Command.equals(originZone.getZoneType())) {
|
if (ZoneType.Hand.equals(destination) && ZoneType.Command.equals(originZone.getZoneType())) {
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
sb.append(movedCard.getName()).append(" has moved from Command Zone to ").append(activator).append("'s hand.");
|
sb.append(movedCard.getDisplayName()).append(" has moved from Command Zone to ").append(activator).append("'s hand.");
|
||||||
game.getGameLog().add(GameLogEntryType.ZONE_CHANGE, sb.toString());
|
game.getGameLog().add(GameLogEntryType.ZONE_CHANGE, sb.toString());
|
||||||
commandCards.add(movedCard); //add to list to reveal the commandzone cards
|
commandCards.add(movedCard); //add to list to reveal the commandzone cards
|
||||||
}
|
}
|
||||||
@@ -1043,10 +1043,10 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
|
|||||||
final int fetchNum = Math.min(player.getCardsIn(ZoneType.Library).size(), 4);
|
final int fetchNum = Math.min(player.getCardsIn(ZoneType.Library).size(), 4);
|
||||||
CardCollectionView shown = !decider.hasKeyword("LimitSearchLibrary") ? player.getCardsIn(ZoneType.Library) : player.getCardsIn(ZoneType.Library, fetchNum);
|
CardCollectionView shown = !decider.hasKeyword("LimitSearchLibrary") ? player.getCardsIn(ZoneType.Library) : player.getCardsIn(ZoneType.Library, fetchNum);
|
||||||
// Look at whole library before moving onto choosing a card
|
// Look at whole library before moving onto choosing a card
|
||||||
delayedReveal = new DelayedReveal(shown, ZoneType.Library, PlayerView.get(player), CardTranslation.getTranslatedName(source.getName()) + " - " + Localizer.getInstance().getMessage("lblLookingCardIn") + " ");
|
delayedReveal = new DelayedReveal(shown, ZoneType.Library, PlayerView.get(player), source.getTranslatedName() + " - " + Localizer.getInstance().getMessage("lblLookingCardIn") + " ");
|
||||||
}
|
}
|
||||||
else if (origin.contains(ZoneType.Hand) && player.isOpponentOf(decider)) {
|
else if (origin.contains(ZoneType.Hand) && player.isOpponentOf(decider)) {
|
||||||
delayedReveal = new DelayedReveal(player.getCardsIn(ZoneType.Hand), ZoneType.Hand, PlayerView.get(player), CardTranslation.getTranslatedName(source.getName()) + " - " + Localizer.getInstance().getMessage("lblLookingCardIn") + " ");
|
delayedReveal = new DelayedReveal(player.getCardsIn(ZoneType.Hand), ZoneType.Hand, PlayerView.get(player), source.getTranslatedName() + " - " + Localizer.getInstance().getMessage("lblLookingCardIn") + " ");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1346,7 +1346,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
|
|||||||
list = CardLists.filter(list, CardPredicates.canBeAttached(c, sa));
|
list = CardLists.filter(list, CardPredicates.canBeAttached(c, sa));
|
||||||
}
|
}
|
||||||
if (!list.isEmpty()) {
|
if (!list.isEmpty()) {
|
||||||
String title = Localizer.getInstance().getMessage("lblSelectACardAttachSourceTo", CardTranslation.getTranslatedName(c.getName()));
|
String title = Localizer.getInstance().getMessage("lblSelectACardAttachSourceTo", c.getTranslatedName());
|
||||||
Map<String, Object> params = Maps.newHashMap();
|
Map<String, Object> params = Maps.newHashMap();
|
||||||
params.put("Attach", c);
|
params.put("Attach", c);
|
||||||
Card attachedTo = decider.getController().chooseSingleEntityForEffect(list, sa, title, params);
|
Card attachedTo = decider.getController().chooseSingleEntityForEffect(list, sa, title, params);
|
||||||
@@ -1363,7 +1363,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
|
|||||||
if (sa.hasParam("AttachedToPlayer")) {
|
if (sa.hasParam("AttachedToPlayer")) {
|
||||||
FCollectionView<Player> list = AbilityUtils.getDefinedPlayers(source, sa.getParam("AttachedToPlayer"), sa);
|
FCollectionView<Player> list = AbilityUtils.getDefinedPlayers(source, sa.getParam("AttachedToPlayer"), sa);
|
||||||
if (!list.isEmpty()) {
|
if (!list.isEmpty()) {
|
||||||
String title = Localizer.getInstance().getMessage("lblSelectACardAttachSourceTo", CardTranslation.getTranslatedName(c.getName()));
|
String title = Localizer.getInstance().getMessage("lblSelectACardAttachSourceTo", c.getTranslatedName());
|
||||||
Map<String, Object> params = Maps.newHashMap();
|
Map<String, Object> params = Maps.newHashMap();
|
||||||
params.put("Attach", c);
|
params.put("Attach", c);
|
||||||
Player attachedTo = player.getController().chooseSingleEntityForEffect(list, sa, title, params);
|
Player attachedTo = player.getController().chooseSingleEntityForEffect(list, sa, title, params);
|
||||||
@@ -1391,7 +1391,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
|
|||||||
list = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), sa.getParam("AttachAfter"), c.getController(), c, sa);
|
list = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), sa.getParam("AttachAfter"), c.getController(), c, sa);
|
||||||
}
|
}
|
||||||
if (!list.isEmpty()) {
|
if (!list.isEmpty()) {
|
||||||
String title = Localizer.getInstance().getMessage("lblSelectACardAttachSourceTo", CardTranslation.getTranslatedName(c.getName()));
|
String title = Localizer.getInstance().getMessage("lblSelectACardAttachSourceTo", c.getTranslatedName());
|
||||||
Map<String, Object> params = Maps.newHashMap();
|
Map<String, Object> params = Maps.newHashMap();
|
||||||
params.put("Attach", movedCard);
|
params.put("Attach", movedCard);
|
||||||
Card attachedTo = decider.getController().chooseSingleEntityForEffect(list, sa, title, params);
|
Card attachedTo = decider.getController().chooseSingleEntityForEffect(list, sa, title, params);
|
||||||
@@ -1530,7 +1530,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
SpellAbility tgtSA = decider.getController().getAbilityToPlay(tgtCard, sas);
|
SpellAbility tgtSA = decider.getController().getAbilityToPlay(tgtCard, sas);
|
||||||
if (!decider.getController().confirmAction(tgtSA, null, Localizer.getInstance().getMessage("lblDoYouWantPlayCard", CardTranslation.getTranslatedName(tgtCard.getName())), null)) {
|
if (!decider.getController().confirmAction(tgtSA, null, Localizer.getInstance().getMessage("lblDoYouWantPlayCard", tgtCard.getTranslatedName()), null)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// if played, that card cannot be found
|
// if played, that card cannot be found
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import forge.game.player.Player;
|
|||||||
import forge.game.spellability.AbilitySub;
|
import forge.game.spellability.AbilitySub;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
import forge.util.Aggregates;
|
import forge.util.Aggregates;
|
||||||
import forge.util.CardTranslation;
|
|
||||||
import forge.util.Lang;
|
import forge.util.Lang;
|
||||||
import forge.util.Localizer;
|
import forge.util.Localizer;
|
||||||
import forge.util.collect.FCollection;
|
import forge.util.collect.FCollection;
|
||||||
@@ -235,7 +234,7 @@ public class CharmEffect extends SpellAbilityEffect {
|
|||||||
}
|
}
|
||||||
|
|
||||||
boolean isOptional = sa.hasParam("Optional");
|
boolean isOptional = sa.hasParam("Optional");
|
||||||
if (isOptional && !activator.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblWouldYouLikeCharm", CardTranslation.getTranslatedName(source.getName())), null)) {
|
if (isOptional && !activator.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblWouldYouLikeCharm", source.getTranslatedName()), null)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import com.google.common.collect.Lists;
|
|||||||
import forge.game.Direction;
|
import forge.game.Direction;
|
||||||
import forge.game.player.DelayedReveal;
|
import forge.game.player.DelayedReveal;
|
||||||
import forge.game.player.PlayerView;
|
import forge.game.player.PlayerView;
|
||||||
import forge.util.CardTranslation;
|
|
||||||
|
|
||||||
import forge.card.CardType;
|
import forge.card.CardType;
|
||||||
import forge.game.Game;
|
import forge.game.Game;
|
||||||
@@ -256,7 +255,7 @@ public class ChooseCardEffect extends SpellAbilityEffect {
|
|||||||
CardCollectionView shown = !p.hasKeyword("LimitSearchLibrary")
|
CardCollectionView shown = !p.hasKeyword("LimitSearchLibrary")
|
||||||
? searched.getCardsIn(ZoneType.Library) : searched.getCardsIn(ZoneType.Library, fetchNum);
|
? searched.getCardsIn(ZoneType.Library) : searched.getCardsIn(ZoneType.Library, fetchNum);
|
||||||
DelayedReveal delayedReveal = new DelayedReveal(shown, ZoneType.Library, PlayerView.get(searched),
|
DelayedReveal delayedReveal = new DelayedReveal(shown, ZoneType.Library, PlayerView.get(searched),
|
||||||
CardTranslation.getTranslatedName(host.getName()) + " - " +
|
host.getTranslatedName() + " - " +
|
||||||
Localizer.getInstance().getMessage("lblLookingCardIn") + " ");
|
Localizer.getInstance().getMessage("lblLookingCardIn") + " ");
|
||||||
Card choice = p.getController().chooseSingleEntityForEffect(pChoices, delayedReveal, sa, title,
|
Card choice = p.getController().chooseSingleEntityForEffect(pChoices, delayedReveal, sa, title,
|
||||||
!sa.hasParam("Mandatory"), p, null);
|
!sa.hasParam("Mandatory"), p, null);
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ public class ChooseSourceEffect extends SpellAbilityEffect {
|
|||||||
|
|
||||||
sb.append(Lang.joinHomogenous(getTargetPlayers(sa)));
|
sb.append(Lang.joinHomogenous(getTargetPlayers(sa)));
|
||||||
|
|
||||||
sb.append("chooses a source.");
|
sb.append(" chooses a source.");
|
||||||
|
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ public class ClashEffect extends SpellAbilityEffect {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
protected String getStackDescription(final SpellAbility sa) {
|
protected String getStackDescription(final SpellAbility sa) {
|
||||||
return sa.getHostCard().getName() + " - Clash with an opponent.";
|
return sa.getHostCard().getDisplayName() + " - Clash with an opponent.";
|
||||||
}
|
}
|
||||||
|
|
||||||
/* (non-Javadoc)
|
/* (non-Javadoc)
|
||||||
@@ -101,7 +101,7 @@ public class ClashEffect extends SpellAbilityEffect {
|
|||||||
pCMC = pCard.getCMC();
|
pCMC = pCard.getCMC();
|
||||||
toReveal.add(pCard);
|
toReveal.add(pCard);
|
||||||
|
|
||||||
reveal.append(player).append(" " + Localizer.getInstance().getMessage("lblReveals") + ": ").append(pCard.getName()).append(". " + Localizer.getInstance().getMessage("lblCMC") + "= ").append(pCMC);
|
reveal.append(player).append(" " + Localizer.getInstance().getMessage("lblReveals") + ": ").append(pCard.getDisplayName()).append(". " + Localizer.getInstance().getMessage("lblCMC") + "= ").append(pCMC);
|
||||||
reveal.append("\n");
|
reveal.append("\n");
|
||||||
}
|
}
|
||||||
if (!oLib.isEmpty()) {
|
if (!oLib.isEmpty()) {
|
||||||
@@ -109,7 +109,7 @@ public class ClashEffect extends SpellAbilityEffect {
|
|||||||
oCMC = oCard.getCMC();
|
oCMC = oCard.getCMC();
|
||||||
toReveal.add(oCard);
|
toReveal.add(oCard);
|
||||||
|
|
||||||
reveal.append(opponent).append(" " + Localizer.getInstance().getMessage("lblReveals") + ": ").append(oCard.getName()).append(". " + Localizer.getInstance().getMessage("lblCMC") + "= ").append(oCMC);
|
reveal.append(opponent).append(" " + Localizer.getInstance().getMessage("lblReveals") + ": ").append(oCard.getDisplayName()).append(". " + Localizer.getInstance().getMessage("lblCMC") + "= ").append(oCMC);
|
||||||
reveal.append("\n");
|
reveal.append("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,7 +138,7 @@ public class ClashEffect extends SpellAbilityEffect {
|
|||||||
final GameAction action = p.getGame().getAction();
|
final GameAction action = p.getGame().getAction();
|
||||||
final boolean putOnTop = p.getController().willPutCardOnTop(c);
|
final boolean putOnTop = p.getController().willPutCardOnTop(c);
|
||||||
final String location = putOnTop ? "top" : "bottom";
|
final String location = putOnTop ? "top" : "bottom";
|
||||||
final String clashOutcome = p.getName() + " clashed and put " + c.getName() + " to the " + location + " of library.";
|
final String clashOutcome = p.getName() + " clashed and put " + c.getDisplayName() + " to the " + location + " of library.";
|
||||||
|
|
||||||
if (putOnTop) {
|
if (putOnTop) {
|
||||||
action.moveToLibrary(c, sa);
|
action.moveToLibrary(c, sa);
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package forge.game.ability.effects;
|
package forge.game.ability.effects;
|
||||||
|
|
||||||
|
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
|
|
||||||
import forge.game.Game;
|
import forge.game.Game;
|
||||||
@@ -11,7 +10,6 @@ import forge.game.card.Card;
|
|||||||
import forge.game.event.GameEventRandomLog;
|
import forge.game.event.GameEventRandomLog;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
import forge.util.CardTranslation;
|
|
||||||
import forge.util.Localizer;
|
import forge.util.Localizer;
|
||||||
|
|
||||||
public class CleanUpEffect extends SpellAbilityEffect {
|
public class CleanUpEffect extends SpellAbilityEffect {
|
||||||
@@ -76,7 +74,7 @@ public class CleanUpEffect extends SpellAbilityEffect {
|
|||||||
|
|
||||||
protected String logOutput(SpellAbility sa, Card source) {
|
protected String logOutput(SpellAbility sa, Card source) {
|
||||||
final StringBuilder log = new StringBuilder();
|
final StringBuilder log = new StringBuilder();
|
||||||
final String name = CardTranslation.getTranslatedName(source.getName());
|
final String name = source.getTranslatedName();
|
||||||
String linebreak = "\r\n";
|
String linebreak = "\r\n";
|
||||||
|
|
||||||
if (sa.hasParam("ClearRemembered") && source.getRememberedCount() != 0) {
|
if (sa.hasParam("ClearRemembered") && source.getRememberedCount() != 0) {
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import forge.game.event.GameEventCardStatsChanged;
|
|||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
import forge.util.CardTranslation;
|
|
||||||
import forge.util.IterableUtil;
|
import forge.util.IterableUtil;
|
||||||
import forge.util.Localizer;
|
import forge.util.Localizer;
|
||||||
import forge.util.collect.FCollection;
|
import forge.util.collect.FCollection;
|
||||||
@@ -109,7 +108,7 @@ public class CloneEffect extends SpellAbilityEffect {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final boolean optional = sa.hasParam("Optional");
|
final boolean optional = sa.hasParam("Optional");
|
||||||
if (optional && !host.getController().getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantCopy", CardTranslation.getTranslatedName(cardToCopy.getName())), null)) {
|
if (optional && !host.getController().getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoYouWantCopy", cardToCopy.getTranslatedName()), null)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import forge.game.card.Card;
|
|||||||
import forge.game.card.CardCollectionView;
|
import forge.game.card.CardCollectionView;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
import forge.util.CardTranslation;
|
|
||||||
import forge.util.Localizer;
|
import forge.util.Localizer;
|
||||||
|
|
||||||
|
|
||||||
@@ -87,8 +86,8 @@ public class ControlExchangeEffect extends SpellAbilityEffect {
|
|||||||
|
|
||||||
if (sa.hasParam("Optional") && !sa.getActivatingPlayer().getController().confirmAction(sa, null,
|
if (sa.hasParam("Optional") && !sa.getActivatingPlayer().getController().confirmAction(sa, null,
|
||||||
Localizer.getInstance().getMessage("lblExchangeControl",
|
Localizer.getInstance().getMessage("lblExchangeControl",
|
||||||
CardTranslation.getTranslatedName(object1.getName()),
|
object1.getTranslatedName(),
|
||||||
CardTranslation.getTranslatedName(object2.getName())), null)) {
|
object2.getTranslatedName()), null)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import forge.game.player.Player;
|
|||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
import forge.game.trigger.TriggerType;
|
import forge.game.trigger.TriggerType;
|
||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
import forge.util.CardTranslation;
|
|
||||||
import forge.util.Localizer;
|
import forge.util.Localizer;
|
||||||
|
|
||||||
public class ControlGainEffect extends SpellAbilityEffect {
|
public class ControlGainEffect extends SpellAbilityEffect {
|
||||||
@@ -154,7 +153,7 @@ public class ControlGainEffect extends SpellAbilityEffect {
|
|||||||
|
|
||||||
if (sa.hasParam("Optional") && !activator.getController().confirmAction(sa, null,
|
if (sa.hasParam("Optional") && !activator.getController().confirmAction(sa, null,
|
||||||
Localizer.getInstance().getMessage("lblGainControlConfirm", newController,
|
Localizer.getInstance().getMessage("lblGainControlConfirm", newController,
|
||||||
CardTranslation.getTranslatedName(tgtC.getName())), null)) {
|
tgtC.getTranslatedName()), null)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package forge.game.ability.effects;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import forge.GameCommand;
|
|
||||||
import forge.game.Game;
|
import forge.game.Game;
|
||||||
import forge.game.ability.AbilityUtils;
|
import forge.game.ability.AbilityUtils;
|
||||||
import forge.game.ability.SpellAbilityEffect;
|
import forge.game.ability.SpellAbilityEffect;
|
||||||
@@ -28,10 +27,11 @@ public class ControlPlayerEffect extends SpellAbilityEffect {
|
|||||||
public void resolve(SpellAbility sa) {
|
public void resolve(SpellAbility sa) {
|
||||||
final Player controller = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("Controller"), sa).get(0);
|
final Player controller = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("Controller"), sa).get(0);
|
||||||
final Game game = controller.getGame();
|
final Game game = controller.getGame();
|
||||||
|
final boolean combat = sa.hasParam("Combat");
|
||||||
|
|
||||||
for (final Player pTarget: getTargetPlayers(sa)) {
|
for (final Player pTarget: getTargetPlayers(sa)) {
|
||||||
// before next untap gain control
|
// before next untap gain control
|
||||||
game.getCleanup().addUntil(pTarget, (GameCommand) () -> {
|
(combat ? game.getBeginOfCombat() : game.getCleanup()).addUntil(pTarget, () -> {
|
||||||
// CR 800.4b
|
// CR 800.4b
|
||||||
if (!controller.isInGame()) {
|
if (!controller.isInGame()) {
|
||||||
return;
|
return;
|
||||||
@@ -41,7 +41,7 @@ public class ControlPlayerEffect extends SpellAbilityEffect {
|
|||||||
pTarget.addController(ts, controller);
|
pTarget.addController(ts, controller);
|
||||||
|
|
||||||
// after following cleanup release control
|
// after following cleanup release control
|
||||||
game.getCleanup().addUntil((GameCommand) () -> pTarget.removeController(ts));
|
(combat ? game.getEndOfCombat() : game.getCleanup()).addUntil(() -> pTarget.removeController(ts));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ public class CopySpellAbilityEffect extends SpellAbilityEffect {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (SpellAbility chosenSA : copySpells) {
|
for (SpellAbility chosenSA : copySpells) {
|
||||||
if (isOptional && !controller.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoyouWantCopyTheSpell", CardTranslation.getTranslatedName(chosenSA.getHostCard().getName())), null)) {
|
if (isOptional && !controller.getController().confirmAction(sa, null, Localizer.getInstance().getMessage("lblDoyouWantCopyTheSpell", chosenSA.getHostCard().getTranslatedName()), null)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user