Compare commits

...

897 Commits

Author SHA1 Message Date
Jetz72
01da2df6f0 Merge pull request #9080 from Jetz72/fixes20251104
Apply display/flavor names in more places
2025-11-04 09:53:20 -06:00
Fulgur14
ec84a3f137 TLA cards, 4th November (#9079) 2025-11-04 15:42:21 +01:00
Jetz
02e2e713ed Cleanup - use Card.getTranslatedName method 2025-11-04 09:27:12 -05:00
Jetz
a21666040c Apply Card.getDisplayName to user-facing text 2025-11-04 09:17:52 -05:00
Jetz
fe2bd016b6 Add card face display name for card name lists. 2025-11-04 08:41:42 -05:00
Fulgur14
0bb67ec8d0 TLA cards, 3rd November (#9072)
Firebender Ascension and support
2025-11-04 14:20:26 +01:00
Paul Hammerton
6d188c09ca Merge pull request #9077 from paulsnoops/edition-updates
Edition updates: PSPL, PURL, PW25, SLC, SLD, TLA, TLE
2025-11-04 09:29:50 +00:00
Paul Hammerton
f25d8c58d9 Edition updates: PSPL, PURL, PW25, SLC, SLD, TLA, TLE 2025-11-04 09:25:28 +00:00
Fulgur14
f9c5fc7317 Iroh's Demonstration (TLA) (#9076) 2025-11-04 10:20:02 +01:00
Fulgur14
02031e159d TLA/TLE cards 3rd November - Aftermath (#9075) 2025-11-04 10:03:56 +01:00
tool4ever
dd35f97035 Cleanup scripting API first pass (#9070) 2025-11-04 09:23:21 +01:00
tool4ever
ee045d854d Wiki cleanup: remove dead files (#9065) 2025-11-03 08:28:06 +01:00
Eradev
65e4fa7fd7 Minor card fixes (#9069)
* Update DDR

* Update BLB

* Update Realmbreaker, the Invasion Tree (Warning because SVar starts with X)

* Update LTR
2025-11-03 06:56:02 +01:00
tool4ever
be77cc1147 Add workflow_dispatch trigger to sync-wiki.yml 2025-11-02 18:01:03 +00:00
tool4ever
53683f2496 Change wiki sync branch from 'wiki' to 'master' 2025-11-02 17:55:14 +00:00
tool4ever
f0b937a5f0 Sync wiki from main repo (#8320) 2025-11-02 17:51:51 +00:00
Hans Mackowiak
def3fa5d23 build ValidTgtsDesc via Lang (#9063)
* build ValidTgtsDesc via Lang

* use buildValidDesc for AuraSpells

* Add ValidTgtsDesc to GUI message
2025-11-02 17:44:54 +01:00
Fulgur14
e33ddee5bf Agna Qel's (TLA leak) (#9062)
* Create mai_jaded_edge.txt

* Create iroh_tea_master.txt

* Update zedruu_the_greathearted.txt

* Update zedruus_lantern.txt

* Modify Agna Qel'a card attributes

Updated the land card description and abilities.
2025-11-02 17:59:16 +03:00
tool4ever
15fca4243d Fix reflected only checking activated mana abilities (#9058) 2025-11-02 10:50:16 +00:00
tool4ever
6bf288af38 Update trigger zones for Noctis card text 2025-11-01 20:39:58 +00:00
tool4ever
586e30af09 Update the_rise_of_sozin_fire_lord_sozin.txt
Closes #9059
2025-11-01 17:46:42 +00:00
Agetian
ab3c66fd0a Add puzzle PS_SPM3. (#9060)
* - Added puzzle PS_SPM3.

* - Minor mistype fix.
2025-11-01 20:26:38 +03:00
Janek Gröhl
ab4c91dd2d Implemented Commander variant of Adventure Mode (#8970)
* Added UI elements for commander mode in NewGame screen.
- still missing random pool of commander candidates sharing a color with each selected color identity.

* Added rudimentary commander choice with preview image.
For now, it is 5 suggestions per color identity with the requirement that the chosen color is part of the commander's identity.

* Commander card is now added to the player inventory and cannot be sold.
The save game adds a flag whether the game is in commander mode, which will be important for the future changes.

* Commander is now auto-added to the initial deck. Commander cannot be removed. Commander is properly saved and loaded.

* Duels now feature the command zone and all rules

* The chosen commander is now set to every deck if the game mode is commander.

* Only cards can be added to the deck that fulfill the color identity.

* heuristically add a pile of singleton cards to function as the starter for the commander's journey.

* Fix land color identity bug in commander pile

* Items are put on autosell if they do not match commander identity.

* Autosell is now properly set with the card flip timing rules. When visiting a shop, the deck conformaty message does not show anymore.

* Remove strict necessity for a fixed commander. This way, a player may choose to switch to a different legendary creature they find.

* 15 Streamlined commander choices without having to swap colors individually.

* Clean up to remove now redundant i18n label. More focus on creatures for starter commander decks.

* Refactor adventure commander deck generation into the DeckgenUtil class

* Refactorings as suggested by @Jetz72.

* Remove custom deck generator

* handle DeckFormat logic through AdventureDeckEditor.
Fix bugs, where partner commanders would be overwritten when using the stock DeckgenUtil.

* Remove random new line

* Code cleanup

* Fix unused import error

* Fix unused import error

* Revert "organise import" changes and random space linebreak changes.

* Removed another import package.* statement

* Implement fixed commander deck to start with. Removed UI edits and random commander choice.

* Add commander-specific cards and a two white overrun effects

* Revert to prior string formatting.

* Remove import *

* Formatting nitpicking to change as little original code as possible.

---------

Co-authored-by: Agetian <stavdev@mail.ru>
2025-11-01 19:56:55 +03:00
tool4ever
54b8094211 Some cleanup (#9056) 2025-11-01 14:44:47 +00:00
Fulgur14
1445fc2621 The Spirit Oasis (TLA) (#9054) 2025-10-31 20:52:32 +00:00
Fulgur14
d53d9a4ded TLA cards, 31st October (#9053) 2025-10-31 20:50:10 +00:00
Fulgur14
650bf09720 June, Bounty Hunter (TLA) (#9052) 2025-10-31 20:43:22 +00:00
Chris H
759c592b00 Fix trigger mana variable from SVar to DB (#9051) 2025-10-31 14:18:41 +00:00
Fulgur14
7d53fa88cf TLA cards, 30th October, Batch 2 (#9048) 2025-10-30 21:49:55 +00:00
Fulgur14
0de568ce71 TLA cards, 30th October (#9045) 2025-10-30 18:38:44 +00:00
Paul Hammerton
9f3120e2a9 Merge pull request #9046 from paulsnoops/edition-updates
Edition updates: TLA, TLE
2025-10-30 18:02:14 +00:00
Paul Hammerton
fc89ca7093 Edition updates: TLA, TLE 2025-10-30 18:01:05 +00:00
Eradev
4c44b6b337 Proper land selection when sacrificing 2025-10-30 13:50:59 +01:00
Fulgur14
3a1c3bf632 TLA cards, 28th October, Batch 3 (#9025) 2025-10-30 11:36:40 +01:00
Paul Hammerton
e5483f03c5 Add flavorName to editions (#9019) 2025-10-30 08:52:37 +01:00
Fulgur14
45e9bee6e5 Update gran_gran.txt (#9043) 2025-10-30 08:01:48 +01:00
Fulgur14
107aa63c07 TLA/TLE cards, 28th October, Batch 4 (#9031) 2025-10-29 21:58:27 +00:00
Fulgur14
106afcc200 TLA cards, 28th October, Batch 2 (#9024) 2025-10-29 21:58:06 +00:00
Fulgur14
2fc1c120f3 TLA cards, 28th October, Batch 1 (#9023) 2025-10-29 21:57:53 +00:00
Fulgur14
8a7f28858f TLA/TLE cards, 29th October (#9039) 2025-10-29 21:57:39 +00:00
Jetz72
149c14eea1 Fix CardRequest for unflavored name not preferring unflavored prints (#9040) 2025-10-29 21:55:11 +00:00
Paul Hammerton
f6c29f54b3 Merge pull request #9036 from paulsnoops/edition-updates-tla
Edition updates: SLD, TLA, TLE
2025-10-29 19:07:02 +00:00
Paul Hammerton
61f197b93c Edition updates: SLD, TLA, TLE 2025-10-29 19:06:01 +00:00
Eradev
124c595af5 Heist fix (#9032) 2025-10-29 10:53:41 +00:00
tool4EvEr
1cf6633b13 Fix corner case with Perplexing Chimera 2025-10-29 11:16:36 +01:00
Fulgur14
c8fd36682f Energybending (TLA) 2025-10-29 09:50:02 +00:00
Fulgur14
4673ec23bd Two TLA cards (#9020) 2025-10-28 16:22:55 +01:00
Paul Hammerton
b1ba531c64 Merge pull request #9018 from paulsnoops/edition-updates
Edition updates: LMAR, PF25, PMEI, PSPL, PW25, SLC, SLD, TLA, TLE
2025-10-28 10:26:24 +00:00
Paul Hammerton
2fa753d583 Edition updates: LMAR, PF25, PMEI, PSPL, PW25, SLC, SLD, TLA, TLE 2025-10-28 10:22:47 +00:00
Paul Hammerton
50c55a1893 Merge pull request #9017 from paulsnoops/formats-update
Archived formats update
2025-10-28 09:50:56 +00:00
Paul Hammerton
fdc65ffd85 Archived formats update 2025-10-28 09:45:15 +00:00
tool4ever
954031cdb2 Refactor AiProfile handling (#9012)
Co-authored-by: tool4EvEr <tool4EvEr@>
2025-10-28 07:26:18 +03:00
tool4ever
364845ab4e Fix Rock Hydra crash (#9011) 2025-10-27 19:25:41 +00:00
tool4ever
5fc1d4214b Restore check to treat empty room as dynamic state (#9009) 2025-10-27 17:03:15 +00:00
tool4ever
2ee356461c Update fire_lord_ozai.txt
Closes #9010
2025-10-27 16:38:09 +01:00
tool4ever
bfb0c9d41f Fix logic (#9008) 2025-10-27 10:11:46 +01:00
tool4ever
b272612c41 Clean up checks (#9006) 2025-10-26 22:02:08 +00:00
tool4ever
a6e8ac5ccf Subtlety fix (#9005) 2025-10-26 21:52:53 +00:00
Eradev
136940a16c Tokens cleanup (Sets T-Z) (#8159)
* Update TPR

* Update TMP

* Update 10E

* Update BRC

* Update BRO

* Update LTR

* Update ELD

* Update TSB

* Update TSP

* Update TOR

* Update PZ2

* Add r_1_1_soldier

* Update UNF

* Update UGL

* Update UNH

* Update 2ED

* Update UND

* Update UDS

* Update ULG

* Update USG

* Update VIS

* Update VMA

* Update 40K

* Update WAR

* Update WTH

* Update WWK

* Update PZEN

* Update ZEN

* Update ZNR

* Update ZNC

* Update WOE WOC

* Add mention needing to flip tokens

* Rename w_2_2_cat flying.txt to w_2_2_cat_flying.txt

fix name

* Fix squad token names

---------

Co-authored-by: Hans Mackowiak <hanmac@gmx.de>
2025-10-26 22:48:54 +01:00
Hans Mackowiak
76b9da7e22 Update PhaseHandler.java
Add missing break
2025-10-26 17:17:04 +01:00
Hans Mackowiak
f291e9ea59 Fix Multiple Replacement Effects not working correctly 2025-10-25 16:03:19 +02:00
Fulgur14
42a14cfad0 Sozin's Comet (TLA) (#8993) 2025-10-25 10:52:29 +02:00
Jetz72
ed3bc665e2 Update grolnoks_skin.txt
Fix a couple card names in the spellbook.

Closes #8989
2025-10-24 17:22:35 +02:00
Hans Mackowiak
0eb3062c64 CardCloneStates: EmptyRoom should be affected by CloneStates 2025-10-24 09:35:32 +02:00
Hans Mackowiak
4063ac55e6 AbilityUtils: refactor Party types (#8967) 2025-10-23 23:04:22 +02:00
Hans Mackowiak
4cb581152e Update fearsome_whelp.txt
Closes #8982
2025-10-23 11:27:43 +02:00
Cees Timmerman
43525ce657 Fix AI helps opponent create an army (#8901) (#8902) 2025-10-22 20:53:58 +03:00
Hans Mackowiak
8c2c32a3d8 CardRules: fix toUnmodifiableList for Android 2025-10-22 19:27:33 +02:00
Jetz72
7ea1c51990 Escape closed bracket character (#8978)
I *think* this might be what's breaking Android? Digging up docs for old versions is a pain.
2025-10-22 17:07:06 +02:00
Jetz72
b0ee850d0f Merge pull request #8976 from Jetz72/fixes20251022
Revert to numbered capturing groups in CardEdition regexes
2025-10-22 09:29:41 -04:00
Jetz
21a293a458 Revert non-capturing groups too 2025-10-22 09:21:48 -04:00
Jetz
e67365e0d6 Removed one last named group.
Change name group to require at least one character
2025-10-22 08:57:25 -04:00
Jetz
8f12828617 Revert to numbered capturing groups in CardEdition regexes 2025-10-22 08:31:59 -04:00
JoeMoCode
1cb873a1da Fixing issue 8925 - AI has trouble casting spells that create X-toughness creatures. (#8972) 2025-10-22 10:26:46 +03:00
Jetz72
f9b6652c2a Flavor Names (#8849)
* Make getAllFaces return nonnull list

* Optimize Predicates

* CardDB and script syntax changes

* Apply syntax changes

* In-game support for flavor names

* Add display names to PaperCards

* Support searching by flavor names

* Remove some WIP stuff

* Update PaperCard translation key.

* Update capitalization

* Auto-map to variants when edition entry uses a flavor name

* Consolidate display name logic.

* Added syntax for generating flavor named variants in edition files.

* Some examples of new syntax.

* Ignore flavored oracle text when searching rules text

* add hasFlavorName

* Add image key

* Get correct variant from card requests with flavor names.
2025-10-21 15:00:59 +02:00
tool4ever
1188b6889a Fix triggered Iterable missing support for LKI (#8968) 2025-10-21 08:58:53 +00:00
Paul Hammerton
ea590f8574 Merge pull request #8966 from Darthseid/patch-1
Updated to match MTG Arena changes.
2025-10-20 21:11:49 +01:00
Darthseid
6cc2dcf3f0 Updated to match MTG Arena changes. 2025-10-20 22:02:40 +02:00
Darthseid
42357d5d6f Updated to match MTG Arena changes. 2025-10-20 22:02:40 +02:00
Darthseid
21977e7dcd Updated to match MTG Arena changes.
https://www.reddit.com/r/MagicArena/comments/16b18t8/a_bunch_of_cards_were_unnerfed_for_rotation_to/
2025-10-20 22:02:40 +02:00
Darthseid
608a1c11ed Updated Official MTG Arena Change. 2025-10-20 22:02:40 +02:00
Darthseid
79d1f6f1da Updated MTG Arena Balance Change. 2025-10-20 22:02:40 +02:00
Darthseid
2fc6935563 Corrected Traumatic Prank's mana cost. (#8962)
It was previously 3R. You can see the actual cost and effects here: https://scryfall.com/card/ysnc/12/traumatic-prank
2025-10-20 18:20:57 +02:00
Hans Mackowiak
5efd7e6e7c ColorSet: add combine function (#8960) 2025-10-20 09:45:57 +02:00
tool4ever
a8d494d2d8 Update ComputerUtilCombat.java 2025-10-20 07:58:03 +02:00
tool4ever
7367e93140 Fix not being able to freely divide combat damage to attackers (#8961) 2025-10-19 20:50:57 +00:00
tool4ever
6f38479190 Fix AI not targeting triggered Random Charm (#8955) 2025-10-19 18:57:11 +00:00
Eradev
80a4a42575 Card typo and rewards fixes (#8953) 2025-10-19 08:17:21 +00:00
tool4ever
24ef895cb7 Fix Leonardo (#8949) 2025-10-19 08:15:36 +00:00
Fulgur14
472f94af66 Leonardo, Sewer Samurai (TMT) (#8948) 2025-10-18 19:13:59 +00:00
Hans Mackowiak
a99c899eec GameAction: cleanup Static giving Abilities to the Stack (#8587)
* Add test that fails on master engine (before KeywordCollection sanity fix)
2025-10-18 12:24:29 +00:00
Cees Timmerman
d94fe1e958 Fix Elturel Survivors not played (#8920) (#8921) 2025-10-18 07:29:39 +00:00
Hans Mackowiak
27a638a32b TMT: Add Sneak (#8915)
* TMT: Add Sneak

* Sneak on Creature
2025-10-18 09:27:38 +02:00
Hans Mackowiak
a0a3546691 GameObject: turn into an interface 2025-10-17 21:01:37 +02:00
Hans Mackowiak
bbf5319815 KeywordCollection: Use linkedHashSetValues to prevent duplicates (#8941) 2025-10-17 15:55:46 +00:00
tool4ever
5588268c74 Fix complex targeting restriction in mysterious_stranger (#8940) 2025-10-17 13:59:55 +02:00
tool4ever
b9008b7f12 Update heroes_in_a_half_shell.txt 2025-10-17 10:50:01 +02:00
Fulgur14
32fb982d0d Update the_lost_and_the_damned.txt (#8931) 2025-10-16 14:29:08 +00:00
Jetz72
efbba093e0 Merge pull request #8500 from Jetz72/setEventCommand
Refactor event generation, add `set event` command.
2025-10-16 10:24:20 -04:00
Jetz
4a616c8ac6 Move round calculation to after participant amount is set. 2025-10-16 09:09:08 -04:00
tool4ever
f64701d039 Raphael, Tag Team Tough and support (#8936) 2025-10-16 09:04:40 +00:00
tool4ever
77dae2418f Clean up Ruin Grinder (#8930) 2025-10-16 08:05:41 +00:00
Jetz
7e47a6ee2c Move EventStyle specification closer to similar parameters. 2025-10-15 09:53:36 -04:00
Jetz
1093984bec Merge branch 'master' into setEventCommand
# Conflicts:
#	forge-gui-mobile/src/forge/adventure/data/AdventureEventData.java
#	forge-gui-mobile/src/forge/adventure/stage/MapStage.java
#	forge-gui-mobile/src/forge/adventure/util/AdventureEventController.java
2025-10-15 09:19:44 -04:00
tool4ever
8ae1bcd2c5 Update mazes_end.txt
Closes #8933
2025-10-15 13:22:00 +02:00
tool4ever
f9150bea34 Fix Bitterbloom Bearer (#8932) 2025-10-15 11:30:41 +02:00
tool4ever
04186de397 Update bitterbloom_bearer.txt 2025-10-15 11:19:33 +02:00
Fulgur14
4827e10bd4 Create turtles_forever.txt (#8927) 2025-10-15 08:10:15 +02:00
tool4ever
da92a93d1d Fix The Lost and the Damned not triggering for tokens (#8928)
Co-authored-by: tool4EvEr <tool4EvEr@>
2025-10-15 08:12:50 +03:00
Hans Mackowiak
6b17b65084 Update shriek_treblemaker.txt
Closes #8929
2025-10-15 04:55:59 +02:00
tool4ever
fac5b6c253 Support Turtles Forever (#8926) 2025-10-14 17:16:21 +00:00
Hans Mackowiak
6617e2627c Conquest: make ColorButton instead of ColorFilter 2025-10-14 11:51:14 +02:00
Hans Mackowiak
17d361d62d ~ lf 2025-10-14 08:53:07 +00:00
tool4ever
05a81d00fd Update rally_maneuver.txt
Closes #8924
2025-10-14 06:58:59 +00:00
tool4ever
808ecde0c0 Update the_death_of_gwen_stacy.txt 2025-10-14 08:02:17 +02:00
Fulgur14
7fb7c62fbe 15 TMT/TMC cards (#8895) 2025-10-13 17:50:20 +00:00
tool4ever
6ddb3124ac Fix Pump / Draw AI half broken (#8918) 2025-10-13 17:49:05 +00:00
Hans Mackowiak
c454253d48 ManaCostShard: build imageKey
Remove "/" dynamic for `imageKey`
2025-10-12 22:23:12 +02:00
Hans Mackowiak
fdbdffd874 Update DeckRecognizer.java
Remove extra manaSymbolsMap
2025-10-12 22:22:41 +02:00
Renato Filipe Vidal Santos
010f917550 Update consuming_ferocity.txt 2025-10-12 15:58:27 +02:00
Renato Filipe Vidal Santos
43cefed048 Update lumbering_laundry.txt 2025-10-12 15:23:59 +02:00
Renato Filipe Vidal Santos
3d15f743c3 Update the_darkness_crystal.txt 2025-10-12 15:23:59 +02:00
Paul Hammerton
f05170c1a4 Merge pull request #8907 from Eradev/FixMissingTokenMED
Update MPS_RNA
2025-10-12 11:25:19 +01:00
Eradev
9147836f1a Update MPS_RNA 2025-10-11 18:49:48 -04:00
Paul Hammerton
098c98570d Merge pull request #8899 from paulsnoops/edition-updates
Edition updates: PMEI, PZA, SLD, TMC, TMT
2025-10-11 10:27:22 +01:00
Paul Hammerton
8bb15f6bbc Edition updates: PMEI, PZA, SLD, TMC, TMT 2025-10-11 10:25:52 +01:00
Eradev
63002aba97 Fix Ambitious Dragonborn (#8898)
* Update IKO

* Update Ambitious Dragonborn
2025-10-11 09:05:20 +03:00
Eradev
52adb2dc2b Ensure you get the valid card (#8878) 2025-10-11 09:05:08 +03:00
Renato Filipe Vidal Santos
8478d3ce47 Adventure cleanup: 2025-10-10 (#8896)
* Update skep_outer.tmx

* Update skep_outer.tmx

* Update skep_outer.tmx

* Update quests.json

* Update quests.json

* Update quests.json

* Update quests.json
2025-10-11 09:03:43 +03:00
Hans Mackowiak
02c865765b Update DeckRecognizer.java
remove call to getLocalisedMagicColorName
2025-10-10 11:26:40 +02:00
Hans Mackowiak
327d60ed84 Update doctor_octopus_master_planner.txt
Closes #8879
2025-10-09 17:39:26 +02:00
Hans Mackowiak
f8d883d91f ColorSet: turn Set into an Enum (#8757) 2025-10-09 15:03:45 +02:00
Eradev
1962a8d28b Fix typo 2025-10-08 21:10:14 -04:00
Eradev
f3ecd51253 Minor cleanup 2025-10-08 21:10:14 -04:00
Eradev
bceb3c4503 Check every edition of a card instead of just the first one 2025-10-08 21:10:14 -04:00
Eradev
87977e836d Update PLS 2025-10-08 21:10:14 -04:00
tool4ever
7b8f69a022 Update ComputerUtilCombat.java 2025-10-08 21:07:40 -04:00
Agetian
a0442e7c3e - Tweak the parameter names. 2025-10-08 21:07:40 -04:00
Agetian
845969bc34 Update forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java
Co-authored-by: Chris H <zenchristo@gmail.com>
2025-10-08 21:07:40 -04:00
Agetian
4256d9ae1f - Add a TODO. 2025-10-08 21:07:40 -04:00
Agetian
3a956c4e20 - Take 4. Avoid creating an extra CardCollection unless needed. 2025-10-08 21:07:40 -04:00
Agetian
590abbe8e6 - Assign the result of the orderBlockers call. 2025-10-08 21:07:40 -04:00
Agetian
29fe1266d7 - Reuse AiBlockController.orderBlockers 2025-10-08 21:07:40 -04:00
Agetian
6152933766 - Only call the sort command in case legacy ordering is disabled, assume sorted elsewhere otherwise. 2025-10-08 21:07:40 -04:00
Agetian
af947c9af5 - Fix comment placement 2025-10-08 21:07:40 -04:00
Agetian
7deb7b1ecc - Fix AI not ordering combatants for damage in non-legacy mode - take two (inside distributeAIDamage) 2025-10-08 21:07:40 -04:00
Agetian
5a19c9ed7e Revert "- Fix AI not ordering combatants for damage in non-legacy mode."
This reverts commit ff784183dc.
2025-10-08 21:07:40 -04:00
Agetian
bbd9fb7c37 - Fix AI not ordering combatants for damage in non-legacy mode. 2025-10-08 21:07:40 -04:00
Eradev
4e17b4c9d2 Missing import 2025-10-08 21:06:33 -04:00
Eradev
caf8b0535f CardUtil File format 2025-10-08 21:06:33 -04:00
Eradev
0ee3dc4e67 Check for Snow lands and Wastes when generating random decks 2025-10-08 21:06:33 -04:00
kevlahnota
3847080221 Merge pull request #8875 from kevlahnota/master4
prevent NPE
2025-10-09 06:10:12 +08:00
Anthony Calosa
f7c965de02 prevent NPE 2025-10-09 06:06:59 +08:00
tool4ever
b844181c0b Benevolent Blessing: fix exception ignoring opponent auras (#8870) 2025-10-08 18:58:12 +00:00
Agetian
7f02c73090 Further amendment to AI MoveCounters (#8869) 2025-10-08 17:55:55 +00:00
Agetian
f0f1f5a1d4 Slight cleanup amendment to the MoveCounters AI (#8866)
* - Fix MoveCounters with beneficial counters.

* - Slight cleanup.
2025-10-08 14:23:11 +03:00
Agetian
5a59113d78 - Fix MoveCounters with beneficial counters. (#8865) 2025-10-08 14:08:24 +03:00
Eradev
6935f962d3 Update AKH (#8863) 2025-10-08 11:39:43 +02:00
Jetz72
34002a1ed7 Merge pull request #8858 from Jetz72/templateFixes
Add issue types to github templates
2025-10-07 14:59:33 -04:00
Jetz72
a22f28ef50 Update feature_request.md 2025-10-07 14:53:15 -04:00
Jetz72
f6d33ed483 Update bug_report.md
Add issue type to template.
2025-10-07 14:51:51 -04:00
Renato Filipe Vidal Santos
5fae32e048 Add files via upload (#8846)
Co-authored-by: Agetian <stavdev@mail.ru>
2025-10-07 18:58:21 +03:00
Eradev
ab1d423e28 Update the_soul_stone.txt (#8854) 2025-10-07 06:31:55 +02:00
Eradev
1b8c87e578 Fix event init 2025-10-07 06:30:10 +02:00
Hans Mackowiak
bbf7a25979 Refactor UnlockedDoors using PlayerProperty 2025-10-07 06:29:20 +02:00
kevlahnota
31c2686307 Merge pull request #8852 from kevlahnota/master4
prevent NPE
2025-10-07 09:48:10 +08:00
Anthony Calosa
04f454ae83 prevent NPE 2025-10-07 05:32:41 +08:00
tool4ever
5b2a56bd4d Update the_rise_of_sozin_fire_lord_sozin.txt 2025-10-06 17:52:18 +00:00
Eradev
5bbc86c1a1 Fix crash in Quest rewards screen 2025-10-06 07:41:52 -04:00
Eradev
a5765905d8 Fix Quest start with boosters and unsactionned pool 2025-10-06 07:41:52 -04:00
tool4ever
c4a3b5c545 Update avatar_aang_aang_master_of_elements.txt 2025-10-06 13:01:37 +02:00
tool4ever
81e1306cd7 Update garland_royal_kidnapper.txt 2025-10-05 18:31:42 +00:00
Hans Mackowiak
39f6377f33 remove leftover variable 2025-10-05 19:12:38 +02:00
Hans Mackowiak
0af4be42c4 DungeonsCompleted with handlePaid (#8842) 2025-10-05 18:31:12 +02:00
Agetian
da8b77acf7 Add AtOppEOT logic to a couple lands that the AI misuses (#8839)
* - AtOppEOT is automatic now (also tweak the FlipACoinAi template to make use of it)

* - AILogic$ Never is now generic.
2025-10-05 16:21:15 +00:00
Hans Mackowiak
4cef049a1d AbilityUtils: move SumPower to handlePaid (#8838)
* AbilityUtils: move SumPower to handlePaid

* Replace SumPower with CardPower

* Replace SumToughness with CardToughness
2025-10-05 11:27:21 +02:00
tool4ever
8cd386b654 Update ChangeZoneAi.java 2025-10-05 08:39:51 +00:00
tool4ever
c1e87334cd Some cleanup (#8833) 2025-10-04 20:31:47 +00:00
Jetz72
3ae3e1b218 Merge pull request #8828 from Eradev/FixDivZero
Fix Div by zero error for collection percentage.
2025-10-04 09:37:24 -04:00
Hans Mackowiak
7acfe6c240 Different names rework (#8823)
* turn CardChangedName into record

* Add CardLists.getDifferentNamesCount

* CostDiscard: rename to +WithDifferentNames

* CostSacrifice: use getDifferentNamesCount

* Refactor DifferentCardNames_
2025-10-04 15:36:17 +02:00
tool4ever
66a8e1f04f Fix Henzie + Gorex (#8829) 2025-10-04 11:30:21 +00:00
Eradev
091c61829b Fix Div by zero error 2025-10-04 03:56:40 -04:00
kevlahnota
a2b324c9ce prevent NPE VAvatar.java 2025-10-04 07:50:11 +08:00
Paul Hammerton
4ab8c1414c Merge pull request #8827 from paulsnoops/fix-sld
Edition updates: SLD
2025-10-03 21:29:03 +01:00
Paul Hammerton
46df4cdb0d Edition updates: SLD 2025-10-03 21:28:30 +01:00
tool4ever
183100fbdc Update soul_collector.txt 2025-10-03 20:16:57 +00:00
squee1968
50c7188e04 Update chocobo_camp.txt 2025-10-03 21:17:34 +02:00
Chris H
d46d7300b4 Reduce cost of Bird creature token spell 2025-10-02 23:40:28 -04:00
tool4ever
27a0a8f791 Update the_spot_living_portal.txt
Closes #8819
2025-10-02 19:48:54 +00:00
Paul Hammerton
5a63250735 Merge pull request #8817 from paulsnoops/edition-updates
Edition updates: ECL, FIC, FIN, SLD, SLP
2025-10-01 18:13:23 +01:00
Paul Hammerton
04fbe9fac3 Edition updates: ECL, FIC, FIN, SLD, SLP 2025-10-01 18:12:16 +01:00
tool4ever
31f8da5687 Some fixes (#8816) 2025-10-01 14:54:46 +00:00
Hans Mackowiak
c0fe93ee30 Update the_destined_thief.txt
fix DBDraw SVars
2025-10-01 11:39:31 +02:00
Simisays
a1042e0a1d Update jin_sakai_ghost_of_tsushima.txt (#8815) 2025-09-30 19:24:51 +00:00
Hans Mackowiak
cdbf10f1f7 Change type desc (#8809)
* add ChangeTypeDesc for basic land

* Search N is up to

* add some more ChangeTypeDesc

* add some more ChangeTypeDesc

* add some more ChangeTypeDesc

* Update panorama

* update landscape

* Update Clan Monuments

* Small Monument to Perfection change

* update capenna fetchers

* remove ChangeNum from basic Land searchers

* better desc for fetch lands

* remove uneeded ChangeTypeDesc
2025-09-30 20:43:42 +02:00
Simisays
bc6eed5c7d Update joel_resolute_survivor.txt (#8814) 2025-09-30 17:32:21 +00:00
Simisays
b90c14660c FIC 12 Panorama/Holiday box cards (#8803) 2025-09-30 14:22:24 +00:00
Renato Filipe Vidal Santos
eeac0272ab YEOE: 7 cards (#8476) 2025-09-30 06:42:50 +00:00
Renato Filipe Vidal Santos
dcdf2b6e7a YEOE: 4 cards, batch 3 (#8499) 2025-09-30 06:35:08 +00:00
Renato Filipe Vidal Santos
dab01c9b86 YEOE: 4 cards, batch 2 (#8451) 2025-09-30 05:48:49 +00:00
Fulgur14
74dc09b2e5 12 FIC Scene cards + Formidable Speaker (#8808) 2025-09-29 18:07:43 +00:00
Hans Mackowiak
8dc9a18c4f Card: addColor use internal Record (#8793) 2025-09-29 16:31:21 +02:00
Hans Mackowiak
a390284b8e Property cleanup (#8804)
* remove NotTriggeredCard

* remove nonManaAbility

* remove notattacking and notblocking

* remove notnamed
2025-09-28 21:55:25 +02:00
Hans Mackowiak
1d97a4ec7e Update eirdu_carrier_of_dawn_isilu_carrier_of_twilight.txt 2025-09-28 21:20:22 +02:00
Simisays
a21a61cb05 SLD 11 cards + support for new partner variants (#8797) 2025-09-28 19:07:54 +00:00
Simisays
cd84663bed [ECL] 10 cards (#8794) 2025-09-28 13:55:52 +00:00
Hans Mackowiak
4d2b634e4f cantBeEnchantedByMsg and cantAttach StaticAbility (#8772)
* cantBeEnchantedByMsg and cantAttach StaticAbility

* finish cantBeAttachedMsg
2025-09-28 15:35:19 +02:00
tool4ever
4eccfa8177 Restore fixes (#8800) 2025-09-28 07:09:47 +00:00
Paul Hammerton
90ea109a17 Merge pull request #8795 from paulsnoops/edition-updates
Edition updates: ECL, OLGC, OVNT, SLD
2025-09-27 10:42:39 +01:00
Paul Hammerton
a9c42fb340 Edition updates: ECL, OLGC, OVNT, SLD 2025-09-27 10:40:42 +01:00
kevlahnota
f1a095fb56 Merge pull request #8792 from kevlahnota/master4
add breadcrumb to GameAction moveTo
2025-09-27 08:16:02 +08:00
Anthony Calosa
4ed06fae87 add breadcrumb to GameAction moveTo 2025-09-27 08:08:53 +08:00
tool4ever
0d020070a2 Fix tapped cost parts blocking real payment (#8789) 2025-09-26 23:20:04 +00:00
Paul Hammerton
5d39efd9fc Merge pull request #8787 from paulsnoops/edition-updates
Edition updates: AA3, AA4, ECL, MAR, PMEI, SLD, TLE
2025-09-26 17:47:55 +01:00
Paul Hammerton
fd5555fdd6 Edition updates: AA3, AA4, ECL, MAR, PMEI, SLD, TLE 2025-09-26 17:46:48 +01:00
Hans Mackowiak
6dd10be044 Update wild_pack_squad.txt
Closes #8786
2025-09-26 17:00:33 +02:00
Agetian
1b419a13e8 - Fix isWebSligned in Card.
- Add puzzleds PS_SPM1 and PS_TLA1.
2025-09-26 16:07:20 +02:00
Chris H
c0f5615885 Manually fixing version touches 2025-09-26 09:08:49 -04:00
tool4ever
c10b5706f1 Some fixes (#8779) 2025-09-26 08:48:22 +02:00
kevlahnota
9599d01d2c Merge pull request #8780 from kevlahnota/master4
fix Vertical layout for VManaPool
2025-09-26 08:32:51 +08:00
Anthony Calosa
f11c2ad8a4 fix Vertical layout for VManaPool 2025-09-26 08:29:07 +08:00
kevlahnota
20479518da Merge pull request #8777 from kevlahnota/master4
fix removing item by name
2025-09-25 06:57:09 +08:00
Anthony Calosa
1ded8fe5da fix removing item by name 2025-09-25 06:51:02 +08:00
Hans Mackowiak
2d1fe88d32 FSkinProp: unify Enum to SkinProp places (#8764)
* FSkinProp: unify Enum to SkinProp places

* FSkinProp: add iconFromDeckSection
2025-09-24 19:52:58 +02:00
Paul Hammerton
7ca0c2b87b Merge pull request #8776 from paulsnoops/archived-formats-update
Format updates: AA3, AA4
2025-09-24 17:53:50 +01:00
Paul Hammerton
e4a83de6cf Format updates: AA3, AA4 2025-09-24 17:52:33 +01:00
Paul Hammerton
7321aac496 Merge pull request #8774 from paulsnoops/edition-updates
Edition updates: AA3, AA4, ECL, SLD
2025-09-24 09:24:00 +01:00
Paul Hammerton
28dcdf6ddf Update Secret Lair Drop Series.txt 2025-09-24 09:21:44 +01:00
Paul Hammerton
5999909d32 Create Arena Anthology 4.txt 2025-09-24 09:19:15 +01:00
Paul Hammerton
0036c27470 Create Arena Anthology 3.txt 2025-09-24 08:50:46 +01:00
Paul Hammerton
7325443da5 Create Lorwyn Eclipsed.txt 2025-09-24 08:29:33 +01:00
kevlahnota
c4c1388f8b Merge pull request #8773 from kevlahnota/master4
fix NoSuchMethodError on older Android
2025-09-24 13:57:46 +08:00
Anthony Calosa
f4e90e1d38 fix NoSuchMethodError on older Android
- closes #8771
- closes #8770
2025-09-24 13:47:12 +08:00
Renato Filipe Vidal Santos
ab3b00cb65 Cleanup 2025-09-23 2025-09-23 16:26:25 +00:00
kevlahnota
a6cbbeef84 Merge pull request #8767 from kevlahnota/master4
update Sentry, update oshi, update android-all, npe prevention
2025-09-23 19:17:46 +08:00
Anthony Calosa
40629a72a9 update Sentry, update oshi, update android-all, npe prevention 2025-09-23 19:09:15 +08:00
squee1968
f6bc6d6742 Fix Lady Octopus, Inspired Inventor (#8766)
* Fix Lady Octopus, Inspired Inventor
2025-09-23 10:00:33 +02:00
Hans Mackowiak
da52bc17e8 GameEventAttachment: flip oldTarget and newTarget param (#8761)
* GameEventAttachment: flip oldTarget and newTarget param

---------

Co-authored-by: tool4EvEr <tool4EvEr@>
2025-09-22 16:16:57 +02:00
Hans Mackowiak
50b7543933 ColorEnum: use ITranslatable interface 2025-09-22 16:14:21 +02:00
Chris H
a4378a20e5 Manually fixing version touches 2025-09-21 20:58:46 -04:00
GitHub Actions
a6395373f7 Restore POM files for preparation of next release 2025-09-21 20:58:46 -04:00
GitHub Actions
54f41a28cd [maven-release-plugin] prepare for next development iteration 2025-09-21 20:58:46 -04:00
GitHub Actions
a2c798ba86 [maven-release-plugin] prepare release forge-2.0.06 2025-09-21 20:58:46 -04:00
Chris H
8f7a77354a Update apt-get so latest libxml2-utils is available 2025-09-21 20:58:46 -04:00
kevlahnota
b50cdea5df Update drop_tower.txt 2025-09-22 06:37:26 +08:00
Renato Filipe Vidal Santos
a7568b0845 Cleanup 2025-09-21 2025-09-21 07:30:43 +00:00
kevlahnota
e158b5966e Merge pull request #8755 from kevlahnota/master4
use Supplier for FModel for thread safety
2025-09-21 09:01:18 +08:00
kevlahnota
bea5ffb0a8 Update FModel.java
unused var
2025-09-21 08:51:30 +08:00
Anthony Calosa
ac492fac35 use Supplier for FModel for thread safety 2025-09-21 08:47:28 +08:00
Renato Filipe Vidal Santos
35e6268a95 Add files via upload (#8754) 2025-09-20 19:58:28 +03:00
Hans Mackowiak
90bd0c73d0 Transform: all DFC can transform now 2025-09-20 18:11:39 +02:00
kevlahnota
ea293a46b1 Merge pull request #8750 from kevlahnota/master4
update FModel, add Icon Overlay for Horizontal tabs
2025-09-19 22:58:47 +08:00
Anthony Calosa
6ec6d64cb2 update FModel, add Icon Overlay for Horizontal tabs 2025-09-19 22:06:38 +08:00
Chris H
9bc6ab747d Update Venom, Eddie Brock's power and toughness 2025-09-19 09:22:54 -04:00
kevlahnota
a72df6ad16 updateRarityFilterOdds (#8746) 2025-09-19 07:19:50 +02:00
Renato Filipe Vidal Santos
1105b3fcbd Add files via upload 2025-09-19 07:14:55 +02:00
kevlahnota
08239def86 Merge pull request #8745 from kevlahnota/master4
fix gap and visibility on horizontal layout
2025-09-19 10:39:52 +08:00
Anthony Calosa
cb7fc3df4e fix gap and visibility on horizontal layout 2025-09-19 10:32:18 +08:00
tool4EvEr
9b8020bb30 Fix copied SA using old replacingObjects 2025-09-18 21:20:21 +02:00
Eradev
398917d010 Support named abilities (#8464)
* Support named abilities

* Check if spell was cast from a named ability
2025-09-18 20:15:07 +03:00
tool4ever
10a68c27d5 Update omarthis_ghostfire_initiate.txt 2025-09-18 16:36:25 +00:00
kevlahnota
ba4ee98c3b Merge pull request #8741 from kevlahnota/master4
remove duplicated code and minor format
2025-09-18 19:44:29 +08:00
Anthony Calosa
c2c3add52b update switch 2025-09-18 19:35:23 +08:00
Anthony Calosa
1c7bc1c0d3 remove duplicated code and minor format 2025-09-18 19:27:55 +08:00
tool4EvEr
14f146f8d0 Remove TargetsFromSingleZone 2025-09-18 08:02:15 +02:00
kevlahnota
0cab03b96c Update VManaPool.java 2025-09-18 06:37:45 +08:00
tool4ever
8f5f2059a2 Fix Self-Destruct (#8730) 2025-09-17 18:58:40 +00:00
Hans Mackowiak
34323107c9 AetherFilter: use Predicate for Caption (#8724)
* AetherFilter: use Predicate for Caption
2025-09-17 18:00:08 +02:00
kevlahnota
dbf9568866 Merge pull request #8726 from kevlahnota/master4
update setlookup files
2025-09-17 14:13:38 +08:00
Anthony Calosa
7933893dcb update setlookup files
- closes #8721
2025-09-17 14:08:54 +08:00
kevlahnota
415e2c12e1 Merge pull request #8725 from kevlahnota/master4
fix tab display area size on Horizontal layout
2025-09-17 12:28:25 +08:00
Anthony Calosa
930019db40 fix tab display area size on Horizontal layout 2025-09-17 12:24:35 +08:00
tool4ever
72139b523c Improve AI timeout detection (#8723) 2025-09-16 18:04:00 +00:00
Hans Mackowiak
6cf2f20cdc ColorSet: store EnumSet and toString as Final (#8720) 2025-09-16 16:32:16 +02:00
kevlahnota
cebddb7f4b Update damage_control_crew.txt 2025-09-16 15:38:33 +08:00
kevlahnota
4a6275d282 Merge pull request #8710 from Card-Forge/enhance-prize-visiblity
Only show event cost before you sign up for the event
2025-09-16 15:15:58 +08:00
kevlahnota
bdeaf8cef1 Merge pull request #8719 from kevlahnota/master4
update AltZoneLayout VCardDisplayArea for Landscape
2025-09-16 12:11:22 +08:00
Anthony Calosa
40f6a5b472 update 2025-09-16 12:03:18 +08:00
Anthony Calosa
f4bff30680 update AltZoneLayout VCardDisplayArea for Landscape
- Off (Default Layout)
- Vertical (Alt Layout with old Vertical Scroll Card Display Area)
- Horizontal (Alt Layout with Horizontal Scroll Card Display Area)
- fix mouse scroll to scroll horizontall while ShiftKey is pressed
2025-09-16 11:50:15 +08:00
marthinwurer
5f32c23dc5 moved stopwatch to a class field 2025-09-15 14:57:16 -04:00
marthinwurer
ae7159297b made simulation test extend ai test 2025-09-15 14:57:16 -04:00
marthinwurer
cedfa68c16 removed unused imports 2025-09-15 14:57:16 -04:00
marthinwurer
1f235c5e71 remove todos 2025-09-15 14:57:16 -04:00
marthinwurer
9f8bb8ae4d added integration test 2025-09-15 14:57:16 -04:00
marthinwurer
cd3a26e434 break out main loop step 2025-09-15 14:57:16 -04:00
marthinwurer
d2d6b7bc53 break out main game loop function 2025-09-15 14:57:16 -04:00
marthinwurer
052e72d8ea break out first turn setup 2025-09-15 14:57:16 -04:00
marthinwurer
b902d516a7 Added todos for known problems 2025-09-15 14:57:16 -04:00
tool4ever
79fd4a3f8d Fix crash after removing target when copying divided spell (#8714)
Co-authored-by: tool4EvEr <tool4EvEr@>
2025-09-15 14:27:21 +02:00
Hans Mackowiak
10d359e7d7 Update MagicColor.Color enum (#8715)
Adds ShortName and Symbol
2025-09-15 14:12:46 +02:00
kevlahnota
1cb9e78b21 Merge pull request #8712 from kevlahnota/master4
Prevent NPE
2025-09-15 10:30:31 +08:00
Anthony Calosa
d999c5e213 Prevent NPE 2025-09-15 09:48:45 +08:00
tool4ever
1f7862c970 Update superior_foes_of_spider_man.txt
Closes #8711
2025-09-14 19:26:44 +00:00
Chris H
cdc696c506 Only show event cost before you sign up for the event 2025-09-14 11:13:29 -04:00
Chris H
64897f1ab4 Simplify logic 2025-09-14 10:42:19 -04:00
Chris H
905cf52c5e Disable 4 player drafts for Quest and Adventure until the UIs can handle it 2025-09-14 10:42:19 -04:00
kevlahnota
3139d593a3 Merge pull request #8708 from kevlahnota/master4
throw RuntimeException message for BoosterGenerator makesheet
2025-09-14 08:49:04 +08:00
Anthony Calosa
8f71a5b06e throw RuntimeException message for BoosterGenerator makesheet 2025-09-14 08:44:10 +08:00
kevlahnota
e9742a2292 Merge pull request #8707 from kevlahnota/master4
use player or geneticAI deck for missing decks
2025-09-14 08:17:11 +08:00
Anthony Calosa
a6c893693d remove duplicated code 2025-09-14 08:14:28 +08:00
Anthony Calosa
327ee6853f use player or geneticAI deck for missing decks 2025-09-14 08:04:22 +08:00
kevlahnota
3e7f98db6a Merge pull request #8706 from kevlahnota/master4
Different effect card for Emrakul Castle difficulty
2025-09-13 21:19:19 +08:00
Anthony Calosa
e49021ffdd Different effect card for Emrakul Castle difficulty 2025-09-13 21:13:26 +08:00
Chris H
0c5ff17e94 Fix typo for SPM draft 2025-09-12 23:36:36 -04:00
kevlahnota
2925e94cec Merge pull request #8703 from kevlahnota/master4
fix crash Android 8-13 NoSuchMethodError, prevent crash for missing card
2025-09-13 09:14:09 +08:00
Anthony Calosa
02b7e408dc fix crash Android 8-13 NoSuchMethodError, prevent crash for missing card 2025-09-13 09:09:04 +08:00
Anthony Calosa
b61044abb5 prevent crash for missing card, fix crash Android 8-13 NoSuchMethodError 2025-09-13 08:57:29 +08:00
kevlahnota
f1114eeada Merge pull request #8700 from kevlahnota/master4
Android crash fix
2025-09-12 10:15:15 +08:00
Anthony Calosa
b131e4eb04 Android crash fix 2025-09-12 10:11:34 +08:00
kevlahnota
8ba2ae4a24 Merge pull request #8699 from kevlahnota/master4
remove duplicated code for keyword search
2025-09-12 09:55:22 +08:00
Anthony Calosa
ac93233e19 remove duplicated code for keyword search 2025-09-12 09:38:40 +08:00
kevlahnota
6b29e18a58 Merge pull request #8698 from kevlahnota/master4
update Keyword get method
2025-09-12 05:17:38 +08:00
Anthony Calosa
fd2d3c7bb9 update Keyword get method 2025-09-12 05:09:55 +08:00
Paul Hammerton
a1d67cf844 Merge pull request #8697 from paulsnoops/edition-updates
Edition updates: OMB
2025-09-11 17:44:02 +01:00
Paul Hammerton
2b64f82f98 Edition updates: OMB 2025-09-11 17:42:20 +01:00
Jetz72
43a1570601 Deck Importer support for Adventure, Quest, and Planar Conquest (#8681)
* Some cleanup.

* Expanded/fixed basic land set functions for quest and adventure.

* Get land sets from unlocked planes in conquest mode.

* Add importer for Adventure, Quest, and Conquest.

* Remove unused import

* Remove redundant override

* Deprecate hasBasicLands predicate.

* Delete getManaNameAndSymbol

---------

Co-authored-by: Jetz <Jetz722@gmail.com>
2025-09-11 19:22:17 +03:00
Agetian
db8e084332 - Add puzzle PS_EOE3. (#8696) 2025-09-11 19:21:51 +03:00
kevlahnota
c2fc877201 minor changes to Emrakul Boss Deck
replaced some cards that AI can't use properly
2025-09-11 19:45:39 +08:00
kevlahnota
50f34442f6 Merge pull request #8694 from kevlahnota/master4
fix Keyword Advanced Search
2025-09-11 19:04:51 +08:00
Anthony Calosa
1dbd358c19 fix to include main face 2025-09-11 19:01:59 +08:00
Anthony Calosa
bc3faa88c4 fix Keyword Advanced Search 2025-09-11 18:55:44 +08:00
kevlahnota
10618d4f31 Merge pull request #8693 from kevlahnota/master4
update Emrakul Deck, losing to boss fight breaks random equipment
2025-09-11 16:10:10 +08:00
Anthony Calosa
47e873b4e9 update Emrakul Deck, losing to boss fight breaks random equipment 2025-09-11 16:03:20 +08:00
Chris H
acc9ead5b2 Spm new set (#8687)
* Add draft info for Spider-man
2025-09-11 06:02:40 +02:00
Chris H
6125022c9e Migrate SPM (and some other early spoilers) 2025-09-10 19:08:22 -04:00
Hans Mackowiak
a0be5e25cb CardEdition: fix tokenCN with ☇ 2025-09-10 22:10:00 +02:00
Hans Mackowiak
f2e3cdc111 fix c_1_1_myr to c_1_1_a_myr 2025-09-10 22:02:38 +02:00
Hans Mackowiak
7384aada40 Update pictures_of_spider_man.txt 2025-09-10 06:34:11 +02:00
Eradev
1a8a4f63ae Update Recruit Instructor 2025-09-10 06:31:01 +02:00
Eradev
530d1efcd8 Fix tokens and some images ID (#8684)
* Fig hags ID

* Double sided tokens should now be supported

* Fix zombie ID
2025-09-10 06:30:15 +02:00
tool4ever
d8b1d76f42 Update silk_web_weaver.txt 2025-09-09 21:40:24 +00:00
kevlahnota
830b145e88 Merge pull request #8680 from kevlahnota/master4
update android hwinfo
2025-09-09 22:52:21 +08:00
Anthony Calosa
51ed06d60a update android hwinfo 2025-09-09 22:36:09 +08:00
Eradev
ac9b73935f Tokens cleanup (F-G sets) (#8049)
* Update FEM

* Update PIP

* Update FRF

* Update 5DN

* Update 5ED

* Update FDN

* Update 4ED

* Update F12

* Update V15

* Update V14

* Update DRB

* Update V11

* Update V16

* Update V12

* Update V10

* Update V17

* Update FUT

* Update F17

* Update GDY

* Update GNT

* Update GN2

* Update GTC

* Update GS1

* Update PGPX

* Update GPT

* Update GK1

* Update GRN

* Update GK1

* Added old numbers back for now.

* Rename Fallout set
2025-09-09 13:35:19 +02:00
Eradev
fe2f49fbcc Tokens cleanup (D-E sets) (#8046)
* Update DKA

* Update DST

* Update DIS

* Update WHO

* Update DMR

* Update DMU

* Update DOM

* Update 2X2

* Update 2XM

* Update DGM

* Update DTK

* Update DS0

* Update DDQ

* Update DDC

* Update DDO

* Update DDU

* Update DDT

* Update DDS

* Update TD2

* Update DDN

* Update DDP

* Update DPA

* Update AFR

* Update DSC

* Update P8ED

* Update 8ED

* Update EMN

* Update EMA

* Update EVE

* Update EXO

* Update E02

* Fixed emblem name

* Add u_2_1_a_myr back

* Add b_0_0_germ back

* Update NPH
2025-09-09 13:24:40 +02:00
Eradev
3d31494a1b Tokens cleanup (N-P sets) (#8056)
* Update NMS

* Update NPH

* Update 9ED

* Update OGW

* Update ODY

* Update ONS

* Update PHEL

* Update OTJ

* Update ONE

* Update PLC

* Update PC2

* Re-add planes

* Update HOP

* Update PLS

* Update PCA

* Update PWCS

* Update PCY

* Update PD3
2025-09-09 13:15:57 +02:00
Eradev
ff42e730a2 Tokens cleanup (R-S sets) (#8138)
* Update SOK

* Update SCG

* Update ALA

* Update SOM

* Update SHM

* Update SOI

* Update SCD

* Update SNC

* Update STX

* Update STH

* Update PSVC

* Update RNA

* Update RAV

* Update RTR

* Update REV

* Update ROE

* Update PRIX

* Update RIX

* Fix RNA
2025-09-09 13:00:12 +02:00
Paul Hammerton
a66fb8791c Merge pull request #8678 from paulsnoops/sld-update
Edition updates: OM1, SLD
2025-09-09 10:49:43 +01:00
Paul Hammerton
2ad5d20e83 Edition updates: OM1, SLD 2025-09-09 10:45:32 +01:00
Hans Mackowiak
4790394698 Update miles_morales_ultimate_spider_man.txt
Fix typo
2025-09-09 08:42:16 +02:00
Eradev
8b1c427809 Fix crash when equipping 2025-09-09 05:42:46 +02:00
tool4ever
c0b667c373 Some clean up (#8673) 2025-09-08 20:40:03 +00:00
kevlahnota
ca61627f5b Merge pull request #8671 from kevlahnota/master4
restore backup save for errors
2025-09-08 19:28:52 +08:00
Anthony Calosa
c3e2a5b5ea update 2025-09-08 19:23:58 +08:00
Anthony Calosa
0d26b499d3 restore backup save for errors 2025-09-08 19:17:51 +08:00
kevlahnota
bbbc5e0ee6 Merge pull request #8669 from kevlahnota/master4
return extra info for save errors
2025-09-08 15:46:43 +08:00
Anthony Calosa
714d9442f1 return extra info for save errors 2025-09-08 15:41:16 +08:00
tool4ever
d199765e6d Update coal_hill_school.txt 2025-09-08 08:47:27 +02:00
kevlahnota
e5156d8999 Merge pull request #8668 from kevlahnota/master4
revert save read
2025-09-08 14:34:46 +08:00
Anthony Calosa
c0d5397541 revert save read
- todo find a better way to inform player with save error
2025-09-08 14:34:10 +08:00
kevlahnota
444897d0f9 Update lizard_connorss_curse.txt 2025-09-08 14:20:43 +08:00
kevlahnota
f2cb7956d3 Merge pull request #8667 from kevlahnota/master4
announce savefile error
2025-09-08 12:10:10 +08:00
Anthony Calosa
e0c2a49c6b don't write data with errors 2025-09-08 12:06:23 +08:00
Anthony Calosa
d64e5ebc13 announce savefile error 2025-09-08 12:01:32 +08:00
xanxer6rB
9bea1bc717 Update the_reaver_cleaver.txt (#8661) 2025-09-07 12:43:19 +00:00
Hans Mackowiak
bdf9573467 CantTarget: fix AffectedZone not for Players 2025-09-07 11:25:35 +02:00
Hans Mackowiak
042eb4bf79 CantTarget: return the StaticAbility 2025-09-07 10:35:05 +02:00
Hans Mackowiak
4b0337a836 remove unneeded Protection flag 2025-09-07 09:44:22 +02:00
kevlahnota
9284775921 update ImageView group, piles
should fix concurrentmodification and npe prevention
2025-09-07 13:54:52 +08:00
tool4ever
1671a3298e Fix AI sometimes tapping mana sources for 0 (#8659) 2025-09-06 14:18:36 +00:00
kevlahnota
64e3193a80 Merge pull request #8660 from kevlahnota/master4
update VSubmenuSealed panel
2025-09-06 21:35:57 +08:00
Anthony Calosa
3e6cd92f14 update VSubmenuSealed panel
- closes #8658
2025-09-06 21:32:17 +08:00
Paul Hammerton
b7d4e0129e Merge pull request #8657 from paulsnoops/edition-updates
Edition updates: MAR, OMB, PSPM, SPM
2025-09-06 10:48:18 +01:00
Paul Hammerton
f76127dcea Edition updates: MAR, OMB, PSPM, SPM 2025-09-06 10:33:25 +01:00
Paul Hammerton
c7c0938936 Merge pull request #8656 from paulsnoops/spm-formats
SPM Format updates (+ MAR, OM1, OMB, SPE)
2025-09-06 10:20:18 +01:00
Paul Hammerton
9788a815a7 SPM Format updates (+ MAR, OM1, OMB, SPE) 2025-09-06 10:17:33 +01:00
Matija Huremović
cce25a37c4 Added the -c (clock) flag to determine the maximum time in seconds before the sim execution calls the match a draw. (#8202) 2025-09-06 09:04:59 +00:00
Hans Mackowiak
85812d819f Mayhem: add Reminder for Land with no Cost (#8654)
* Mayhem: add Reminder for Land with no Cost
2025-09-06 11:02:15 +02:00
Hans Mackowiak
a974352908 CountersPutAi: prevent AI from going overflow 2025-09-06 09:18:42 +02:00
kevlahnota
45e04cfef7 Update CONTRIBUTORS.txt
reference:
https://api.github.com/repos/Card-Forge/forge/contributors
[?per_page=100&&page=1] | [?per_page=100&&page=2]
2025-09-06 12:00:22 +08:00
kevlahnota
6a83cb89f7 Merge pull request #8653 from kevlahnota/master4
update mobile BugReporter savetofile
2025-09-06 10:57:34 +08:00
Anthony Calosa
494bd75572 update mobile BugReporter savetofile 2025-09-06 10:53:34 +08:00
Paul Hammerton
b065e468b0 Merge pull request #8652 from paulsnoops/fix-spm
Edition updates: SPM
2025-09-05 18:49:52 +01:00
Paul Hammerton
86aff59e40 Edition updates: SPM 2025-09-05 18:49:16 +01:00
Paul Hammerton
748c4e1dd7 Merge pull request #8651 from paulsnoops/edition-updates
Edition updates: MAR, OM1, PSPM, SLD, SPM
2025-09-05 18:34:51 +01:00
Fulgur14
dfb776f526 Final SPM batch (#8648) 2025-09-05 17:33:46 +00:00
Paul Hammerton
ca5d2bb109 Undo Add OM1 AltNames 2025-09-05 18:33:40 +01:00
Paul Hammerton
b42050a40f Add OM1 AltNames 2025-09-05 18:30:37 +01:00
Paul Hammerton
14220ae37f Edition updates: PSPM & SPM tokens 2025-09-05 18:26:56 +01:00
Paul Hammerton
46900c3f49 Edition updates: MAR, OM1, SLD, SPM 2025-09-05 18:19:16 +01:00
tool4ever
cb2bf996d0 Fix Profane Transfusion (#8650)
* Param for Mister Negative
2025-09-05 17:08:46 +00:00
tool4ever
edbf6692f2 Remove fake KW (#8649) 2025-09-05 16:04:07 +00:00
tool4ever
b5f16b83a0 Fix AI checking cost before less expensive logic (#8643) 2025-09-05 09:54:53 +02:00
Fulgur14
ee4902a5ae Spider-Girl and friends (SPM) (#8644) 2025-09-05 09:54:05 +02:00
Fulgur14
0835817d8a Morbius and friends (SPM) (#8640) 2025-09-05 09:48:21 +02:00
Hans Mackowiak
15d4e1ac20 Update spider_suit.txt 2025-09-05 09:23:07 +02:00
kevlahnota
ecdce50f12 Merge pull request #8645 from kevlahnota/master4
NPE prevention
2025-09-05 10:50:45 +08:00
Anthony Calosa
247fd7810e NPE prevention 2025-09-05 06:43:48 +08:00
tool4ever
055cd40fb1 Fix special actions going through frozen stack (#8642) 2025-09-04 19:16:46 +00:00
Renato Filipe Vidal Santos
f1d448509e YEOE: 6 cards 2025-09-04 15:50:36 +00:00
Paul Hammerton
f2581855a4 Merge pull request #8639 from paulsnoops/master
Edition updates: MAR, SPM
2025-09-04 16:03:09 +01:00
Paul Hammerton
1e4c7d4b00 Update Marvel's Spider-Man.txt 2025-09-04 16:02:16 +01:00
Paul Hammerton
2d80e44f70 Update Marvel's Spider-Man.txt 2025-09-04 16:01:06 +01:00
Paul Hammerton
a207bdbb51 Edition updates 2025-09-04 15:54:26 +01:00
Jetz72
c09ec2f410 Merge pull request #8619 from Jetz72/fixes20250902
Fix Arachnus Spinner not choosing a target for the Web
2025-09-04 08:20:21 -05:00
kevlahnota
c579df4c82 Merge pull request #8584 from SapphiCat/master
Fix crash copying adventure decks with full slots
2025-09-04 18:35:09 +08:00
Fulgur14
d39ab505df Sandman's Quicksand (SPM) 2025-09-04 11:38:12 +02:00
Fulgur14
7756dfa5be Jackal and friends (SPM) (#8634) 2025-09-04 11:13:36 +02:00
kevlahnota
c1b6f3126e Merge pull request #8633 from Eradev/FixCollectorNoRegex
Fix CollNo pattern after #7921
2025-09-04 15:47:54 +08:00
kevlahnota
11d45e0fa3 Merge pull request #8636 from kevlahnota/master4
Prevent building some card vars for view purposes only
2025-09-04 14:09:21 +08:00
Anthony Calosa
0504b41dbd Prevent building some card vars for view purposes only
buggy cards will still crash ingame when invoked
2025-09-04 14:04:56 +08:00
kevlahnota
b670cc3736 Update spider_verse.txt 2025-09-04 13:19:43 +08:00
kevlahnota
885300ba49 Merge pull request #8635 from kevlahnota/master4
prevent Concurrent Modificaton onRefresh ItemListView
2025-09-04 13:07:38 +08:00
Anthony Calosa
ff8fd51bf1 prevent NPE, update The Clone Saga trigger 2025-09-04 12:58:11 +08:00
Anthony Calosa
80475c6b62 prevent Concurrent Modificaton onRefresh ItemListView 2025-09-04 12:47:06 +08:00
kevlahnota
88ea36523b Merge pull request #8632 from Eradev/RemoveSetLookupForJ21
Delete SetLookup for J21
2025-09-04 10:17:31 +08:00
Eradev
3386d79cb1 Remove the Alchemy cards that were removed. 2025-09-03 22:07:36 -04:00
Eradev
f97429d1d4 Fix CollNo pattern after #7921 2025-09-03 21:53:49 -04:00
kevlahnota
3628d4e22a Merge pull request #8630 from kevlahnota/master4
remove unsupported cards on game matches
2025-09-04 09:38:26 +08:00
Eradev
fffc9a4282 Delete SetLookup for J21 2025-09-03 21:09:57 -04:00
Anthony Calosa
7cb32b0b25 update deck check 2025-09-04 07:33:21 +08:00
Anthony Calosa
cb13682737 remove unsupported cards on game matches
- closes #8616
2025-09-04 06:50:04 +08:00
Fulgur14
9656a9fb4c Lizard, Connors's Curse (SPM) (#8629) 2025-09-03 20:55:26 +00:00
tool4ever
2fd9d3ae14 CardDb: Remove hardcoded workaround in favor of reusing newer filter (#8628) 2025-09-03 20:49:33 +00:00
Fulgur14
1ea90f6655 Black Cat and gang (SPM) (#8626) 2025-09-03 17:53:50 +00:00
Fulgur14
37342910fa Swarm, Being of Bees and friends (SPM) (#8627) 2025-09-03 17:46:08 +00:00
autumnmyst
3a00727602 Add new duplicate cards directory to deal with duplicate test cards and implement correct Fast // Furious card in UNK (#8520)
* Added Steamflogger Boss to eternal cards in UST
2025-09-03 17:39:53 +00:00
Fulgur14
589d592997 Rhino's Rampage (SPM) (#8624) 2025-09-03 16:12:49 +02:00
Jetz
83101f7f41 Fix incorrect conformity in adventure events. 2025-09-03 09:11:09 -04:00
Fulgur14
c7d24f1c01 4 SPM cards (#8623) 2025-09-03 13:06:42 +02:00
Fulgur14
2a678aa7e9 Spider-UK et al. (SPM) (#8613) 2025-09-03 13:04:46 +02:00
tool4ever
6a758ffc0a Mayhem for Lands (#8615) 2025-09-03 13:03:52 +02:00
tool4ever
83421bac56 Optional fixes (#8622) 2025-09-03 13:00:15 +02:00
Fulgur14
5234365cc1 Heroes' Hangout (SPM) (#8621) 2025-09-03 09:39:06 +02:00
Fulgur14
76558a841c Create the_spot_living_portal.txt (#8617) 2025-09-03 09:37:47 +02:00
Chris H
1338735000 Spiderman is only pick 2 if pod size is 4 2025-09-02 23:12:32 -04:00
Chris H
44c33e2955 Apply pod size to Desktop UI draft view 2025-09-02 23:12:32 -04:00
Chris H
b8a5668db6 Add ability to draft with less than 8 players 2025-09-02 23:12:32 -04:00
Chris H
6a84a7a6f0 Update token entries in Fate Reforged.txt 2025-09-02 23:12:14 -04:00
Jetz
4811c9d726 end of file line break 2025-09-02 20:08:09 -04:00
Jetz
7cea6fce1e Fix Arachnus Spinner not choosing a target for the Web 2025-09-02 20:06:07 -04:00
tool4ever
46ba289329 Update sculpted_sunburst.txt
Closes #8618
2025-09-02 21:52:24 +00:00
Fulgur14
2c9e5ee814 Superior Spider-Man and other nonsense (SPM) (#8614) 2025-09-02 19:09:54 +00:00
Eradev
230eba687d Add unique suffix for reversible cards (#7921) 2025-09-02 18:05:19 +00:00
Fulgur14
5faf317aaf 3 SPM cards (2nd September) (#8612) 2025-09-02 16:59:23 +02:00
Fulgur14
388448c94f Cosmic Spider-Man (SPM) (#8610) 2025-09-02 16:24:11 +02:00
Fulgur14
230282b525 Parker Luck (SPM) (#8609) 2025-09-02 15:34:22 +02:00
Fulgur14
0f92bc18d4 Chameleon, Master of Disguise & Spinneret and Spiderling (#8608) 2025-09-02 13:16:27 +02:00
Paul Hammerton
dc4cee62dc Merge pull request #8607 from paulsnoops/edition-updates
Edition updates: MAR, SPM
2025-09-02 10:44:36 +01:00
Paul Hammerton
ea4c8fbc72 Edition updates: MAR, SPM 2025-09-02 10:42:42 +01:00
Fulgur14
068ea955d9 Impostor Syndrome (SPM) (#8600) 2025-09-02 09:19:18 +02:00
xanxer6rB
f898263bac Update the_soul_stone.txt
Added missing T: Add {B}
2025-09-02 06:34:47 +02:00
Hans Mackowiak
a3147f5e26 ~ fix spiders_man_heroic_horde TrigGainLife 2025-09-02 00:43:53 +02:00
tool4ever
a309eaee02 Update ultimate_green_goblin.txt 2025-09-01 19:25:29 +00:00
Fulgur14
94b058ec07 Agent Venom (SPM) (#8599) 2025-09-01 18:02:11 +00:00
Fulgur14
46bf3348be Flash Thompson and Peter Parker's Camera (SPM) (#8598) 2025-09-01 17:27:24 +00:00
Fulgur14
7c4c820e92 Ezekiel Sims and friends (SPM) (#8596) 2025-09-01 16:35:03 +00:00
Fulgur14
ed3ea99e1a Spiders-Man, Heroic Horde (SPM) (#8593) 2025-09-01 16:24:07 +02:00
tool4ever
ec21a0c789 The Soul Stone and support (#8579) 2025-09-01 13:45:01 +02:00
Fulgur14
bfe0a78765 Radioactive Spider (SPM) (#8592) 2025-09-01 13:40:06 +02:00
Fulgur14
0abc224e1b Steel Wrecking Ball (SPM) (#8588) 2025-08-31 18:06:16 +00:00
SapphiCat
fa3ce1c7f7 Merge branch 'Card-Forge:master' into master 2025-08-31 07:35:08 -07:00
tool4ever
48a5046d93 Fix Urza's Miter (#8586) 2025-08-31 10:39:59 +00:00
Fulgur14
9f9e68f34d Create pictures_of_spider_man.txt (#8583) 2025-08-31 07:29:35 +00:00
kit
d7c7aaa51e Fix crash copying adventure decks with full slots 2025-08-30 14:02:02 -07:00
tool4ever
ffe883e7d9 Behold the Sinister Six! and support (#8578) 2025-08-30 10:57:13 +00:00
Hans Mackowiak
b07a9944aa GameActionUtil: fix getFirstSpellAbility using castSA 2025-08-30 12:55:28 +02:00
Fulgur14
997d489a7e SPM cards from the debut (#8576) 2025-08-30 10:19:58 +00:00
Paul Hammerton
d181746679 Merge pull request #8581 from paulsnoops/master
Edition updates: MAR, PF25, SLD, SPM
2025-08-30 11:16:07 +01:00
Paul Hammerton
693d6cd9e3 Edition updates: MAR, PF25, SLD, SPM 2025-08-30 11:14:15 +01:00
kevlahnota
01d5bf3c21 Merge pull request #8521 from antoniomartinelli/fix/lastimportedcubeid
fix: wrong cube id saved as LAST_IMPORTED_CUBE_ID
2025-08-30 06:55:02 +08:00
kevlahnota
29a630386f Merge pull request #8410 from rappazzo/stop-tracking-changelog
fix: move CHANGES.txt generation to target directory
2025-08-30 06:31:10 +08:00
Hans Mackowiak
75c7938f1e CounterType: turn wrapper into an interface (#8572)
* CounterType: turn wrapper into an interface

* remove CounterType.get(CounterEnumType) helper
2025-08-29 20:44:08 +02:00
Fulgur14
110e885c67 Create with_great_power.txt (#8573) 2025-08-29 18:40:39 +00:00
Hans Mackowiak
548b448f0d Update Game.java
`getNonactivePlayers` not needed anymore
2025-08-29 20:38:55 +02:00
Hans Mackowiak
a2e80ac0f3 TriggerHandler: no need for APNAP (#8567)
* TriggerHandler: no need for APNAP

The order of the Trigger will be handled later in MagicStack

---------

Co-authored-by: tool4EvEr <tool4EvEr@>
2025-08-29 20:37:26 +02:00
Eradev
238d426202 Alchemy update 20250818 (#8492) 2025-08-29 17:55:18 +00:00
Hans Mackowiak
d6f585a80c Event: use Record 2025-08-29 18:05:05 +02:00
tool4ever
223aeb1bff Update displaced_dinosaurs.txt 2025-08-29 12:02:27 +02:00
Hans Mackowiak
4bf02ea61f MagicStack: remove getNonactivePlayers for SpellCastSinceBegOfYourLastTurn 2025-08-29 11:20:38 +02:00
tool4ever
ea186ae4af Update faunsbane_troll.txt 2025-08-28 19:26:09 +00:00
kevlahnota
3e4cd9e189 Update LoadDraftScreen.java
prevent IllegalStateException since the option pane show method needs to run in EDT
2025-08-28 20:13:01 +08:00
Eradev
e35c193f92 Fix content passed 2025-08-27 22:36:22 -04:00
Eradev
e5443fc394 Typo 2025-08-27 22:36:22 -04:00
Eradev
9998092c70 Create helper class to not duplicate the code 2025-08-27 22:36:22 -04:00
Eradev
9f81e0cd34 Move custom types into edition files 2025-08-27 22:36:22 -04:00
Eradev
5569f18053 Remove param 2025-08-27 22:36:22 -04:00
Eradev
40190b5442 Remove TreeSet 2025-08-27 22:36:22 -04:00
Eradev
a33905241d Revert "Ensure subtype not used"
This reverts commit 6797f0c1b0.
2025-08-27 22:36:22 -04:00
Eradev
3a40b560f6 Removed import 2025-08-27 22:36:22 -04:00
Eradev
d79fb96770 Ensure subtype not used 2025-08-27 22:36:22 -04:00
Eradev
f50fe2f89c Now read from a directory 2025-08-27 22:36:22 -04:00
Eradev
a36deab334 Allow custom types 2025-08-27 22:36:22 -04:00
kevlahnota
28feff99d7 Merge pull request #8565 from kevlahnota/master4
prevent NPE, ConcurrentModification on ImageView onRefresh
2025-08-28 10:04:57 +08:00
Anthony Calosa
bab8791012 prevent NPE, ConcurrentModification on ImageView onRefresh
use Record for ImageRecord
2025-08-28 06:28:34 +08:00
Fulgur14
db20b27c9d Update soul_seizer_ghastly_haunting.txt (#8431) 2025-08-27 15:20:24 +00:00
Renato Filipe Vidal Santos
087db3f757 YEOE: 5 cards (#8484) 2025-08-27 15:16:24 +00:00
kevlahnota
db194fab97 Merge pull request #8561 from kevlahnota/master4
Remove unsupported cards from AdventurePlayer inventory
2025-08-27 21:52:52 +08:00
Anthony Calosa
0585ece2c1 Remove unsupported cards from AdventurePlayer inventory
- closes #8545
2025-08-27 21:34:07 +08:00
Eradev
1611559909 Add new card filters (#8557)
* Add is:vanilla

* Add is:custom
2025-08-27 06:10:49 +00:00
Cees Timmerman
d6dee9575b More human-readable events (#8502) 2025-08-27 07:23:14 +02:00
Cees Timmerman
6dd9a731fa Fix event.oldEntiy typo 2025-08-27 07:23:14 +02:00
Leandro Doctors
cf5a8508d6 Remove empty test class
Dead, unmaintained code eventually leads to problems.
2025-08-27 06:56:12 +02:00
tool4ever
0fa7df090e Fix Muraganda Petroglyphs vs. Saga with no abilities (#8551)
---------

Co-authored-by: TRT <>
Co-authored-by: tool4EvEr <tool4EvEr@>
2025-08-27 06:51:15 +02:00
kevlahnota
86838d94f7 Merge pull request #8544 from Eradev/RemoveUnsupportedCard
Option to remove unsupported card from collection
2025-08-27 12:21:53 +08:00
kevlahnota
83c4db07ac Merge pull request #8559 from Eradev/RemoveAdvCardsRewards
Remove custom adventure cards from rewards.
2025-08-27 12:19:58 +08:00
Eradev
b544f2dd00 Remove custom adventure cards from rewards. 2025-08-26 20:54:26 -04:00
kevlahnota
079e6f04ac update canBeOathbreaker check (#8556) 2025-08-27 05:22:00 +08:00
Eradev
d04c541578 Fix edition mapping 2025-08-26 10:34:41 -04:00
Hans Mackowiak
7cc2cf530d AnimateBase: PerpetualManaCost 2025-08-26 15:57:50 +02:00
Hans Mackowiak
61a2c7cadb Update DamageAllAi.java (#8550)
Remove `getNonactivePlayers`
2025-08-26 15:51:39 +02:00
tool4ever
efbf2e1a9c Saga ETB counters aren't intrinsic (#8549) 2025-08-26 15:47:17 +02:00
Anthony Calosa
9b8441d45b update Console Textfield navigation 2025-08-26 21:04:06 +08:00
Hans Mackowiak
f8b7a0fb9a MagicStack: check for StackEntries in addAllTriggeredAbilities 2025-08-26 14:56:24 +02:00
shenshinoman
ab49e97797 Expanding my Innistrad further. new enemies. New maps. Second biome is partially populated. Everything is contained to innistrad plane, so should have no impact on the main game outside that plane. 2025-08-26 08:40:30 -04:00
shenshinoman
c27a9d136f Expanding my Innistrad further. new enemies. New maps. Second biome is partially populated. Everything is contained to innistrad plane, so should have no impact on the main game outside that plane. 2025-08-26 08:40:30 -04:00
Leandro Doctors
91241ee53c clean up: remove obsolete GitLab templates
The project moved to GitHub almost a year ago.

Having duplicated, unmaintained templates will generate problems in the
future. If needed, they can always be retrieved from the commit history.
2025-08-26 13:32:30 +02:00
Eradev
31e537f42b Remove changes to FContainer 2025-08-26 06:33:30 -04:00
Eradev
e550c307c2 Option to remove unsupported card from collection 2025-08-26 06:29:49 -04:00
Paul Hammerton
b4f01b7ebb Merge pull request #8542 from paulsnoops/treetop-recluse
Add treetop_recluse.txt
2025-08-26 09:10:59 +01:00
Eradev
7744474f39 Fix token numbers 2025-08-26 09:58:01 +02:00
Paul Hammerton
1365b82968 Add treetop_recluse.txt 2025-08-26 08:43:01 +01:00
Hans Mackowiak
5bb532bf9d Game: only reverse NonactivePlayers if more than one
Reversing the Turn Order should have no Effect there if only 2 players
2025-08-26 07:35:39 +02:00
tool4ever
258b8c18a9 Fix Thief of Blood simultaneous ETB (#8535)
Co-authored-by: TRT <>
2025-08-26 08:26:32 +03:00
Eradev
a66349d8a1 Missing token (#8538)
* Add g_1_1_forest_dryad_squirrel

* Update curiosity
2025-08-26 08:26:25 +03:00
kevlahnota
1933cf1863 Merge pull request #8537 from kevlahnota/master3
migrate Callback to Consumer Interface
2025-08-26 12:59:53 +08:00
kevlahnota
0d342c778f fix NG+ 2025-08-26 09:11:31 +08:00
kevlahnota
f452b94cb8 update missing migration 2025-08-26 07:16:43 +08:00
kevlahnota
9325794e2f Update FFileChooser.java 2025-08-26 07:03:24 +08:00
kevlahnota
192a64bbc3 Update Forge.java 2025-08-26 06:58:09 +08:00
kevlahnota
deb8369f11 Update FDeckChooser.java 2025-08-26 06:53:59 +08:00
Anthony Calosa
b24f536190 use Java Consumer 2025-08-26 06:34:59 +08:00
Leandro Doctors
28ec24069c CI: add support for JDK 21
Depends on https://github.com/Card-Forge/forge/pull/8533
2025-08-25 17:24:19 -04:00
Anthony Calosa
f27472d9bd migrate Callback to Interface
- closes #5717
2025-08-26 05:14:20 +08:00
Leandro Doctors
780cc8ddbf Fix build on JDK 21+
This bumps `izpack-maven-plugin`.

Tested with JDK 25.

Similar fix for another project:
https://github.com/lsc-project/lsc/pull/385

Original Error message:
`[ERROR] Failed to execute goal
org.codehaus.izpack:izpack-maven-plugin:5.2.3:izpack
(standard-installer) on project forge-installer: Execution
standard-installer
of goal org.codehaus.izpack:izpack-maven-plugin:5.2.3:izpack failed:
java.lang.ArrayIndexOutOfBoundsException: Index 70131 out of bounds for
length 22674 -> [Help 1]`
2025-08-25 13:50:57 -04:00
Jetz
4abc96c045 Fix rebase issue 2025-08-25 08:34:43 -04:00
Jetz
bee208f1d5 Add toNameLookup to print sheet, remove containsCardName. 2025-08-25 08:31:19 -04:00
Jetz
999e76e598 Refactor event generation, add set event command. 2025-08-25 08:30:56 -04:00
kevlahnota
23555b9564 migrate function to method reference 2025-08-25 19:54:42 +08:00
kevlahnota
431827be35 Update Keyword.java for Double Team
- closes #8519
2025-08-25 11:49:49 +08:00
kevlahnota
b0dba74c6c Merge pull request #8528 from kevlahnota/master3
update ConsoleCommandInterpreter
2025-08-25 06:56:58 +08:00
Anthony Calosa
ad0a690764 minor typo 2025-08-25 06:56:27 +08:00
Anthony Calosa
977f2c75b9 update comment 2025-08-25 06:53:28 +08:00
Anthony Calosa
8313414f78 update ConsoleCommandInterpreter 2025-08-25 06:49:59 +08:00
kevlahnota
89955bf201 Merge pull request #8527 from kevlahnota/master3
capture adventure SaveFileData exception to sentry
2025-08-25 05:38:14 +08:00
Anthony Calosa
99d191901a capture adventure SaveFileData exception to sentry 2025-08-25 05:33:45 +08:00
kevlahnota
1a306b3da3 Merge pull request #8525 from kevlahnota/master3
prevent saving error on inventory
2025-08-25 04:55:51 +08:00
Anthony Calosa
9ff03cbd41 prevent saving error on inventory 2025-08-25 04:50:58 +08:00
tool4ever
07a1dbc099 Fix scripts (#8524) 2025-08-24 22:10:56 +02:00
Hans Mackowiak
f24b3ea3b3 Update enchantment_alteration.txt
closes #8522
2025-08-24 22:02:36 +02:00
kevlahnota
770cc72fcd Merge pull request #8523 from kevlahnota/master3
update migration message
2025-08-25 03:36:27 +08:00
Anthony Calosa
8426a74900 update migration message 2025-08-25 03:34:43 +08:00
antoniomartinelli
8d98eda18d fix: wrong cube id saved as LAST_IMPORTED_CUBE_ID 2025-08-24 18:17:54 +02:00
Paul Hammerton
6e6509eaff Merge pull request #8518 from paulsnoops/edition-updates
Edition updates: PSPM
2025-08-24 11:13:05 +01:00
Paul Hammerton
c56ddee47d Edition updates: PSPM 2025-08-24 11:11:41 +01:00
Eradev
07985ac487 Allow custom cards as reward (#8512) 2025-08-23 21:55:28 -04:00
kevlahnota
6c93491d7f Don't select sideboard first when creating new deck 2025-08-24 05:13:36 +08:00
Eradev
c9b012c88a Fix Bloomburrow lands (#8489)
* Fix Bloomburrow lands

* Proper fix

* Revert "Proper fix"

This reverts commit f3a46e40d9.

* Use collector number
2025-08-23 14:14:57 -04:00
kevlahnota
9b53144976 Merge pull request #8509 from kevlahnota/master3
remove Normalizer from clipboard and PaperCard artist.
2025-08-24 00:53:22 +08:00
Anthony Calosa
31020296d8 unused import 2025-08-24 00:46:02 +08:00
Anthony Calosa
65fb3414d8 remove Normalizer from clipboard and PaperCard artist. 2025-08-24 00:42:40 +08:00
kevlahnota
98fee0d86b Merge pull request #8497 from kevlahnota/master3
update InventoryScene, add repair cracked item for gold
2025-08-23 23:17:08 +08:00
Anthony Calosa
becdadb279 NPE prevention for rewards 2025-08-23 23:08:11 +08:00
Hans Mackowiak
26da0ab0d4 Create rw_1_1_soldier.txt
Fix missing TokenScript
2025-08-23 16:46:16 +02:00
Anthony Calosa
e1f4d755e0 minor fix for refreshing overlay 2025-08-23 21:38:52 +08:00
Anthony Calosa
efe7d67d9f add unusable indicator 2025-08-23 21:34:57 +08:00
Anthony Calosa
3a5e11504a add migration message, fix cloning of itemData 2025-08-23 20:07:28 +08:00
Fulgur14
6657602b80 J. Jonah Jameson (SPM) (#8506) 2025-08-23 11:00:45 +02:00
Eradev
eeca33855d Fix minor card render problems (#8478)
* Missing space

* Unescape line breaks in card descriptions
2025-08-23 06:56:21 +03:00
Matthew Scott Krafczyk
cdc63f35bb Add python specific entries to .gitignore (#8390) 2025-08-23 06:55:28 +03:00
Jetz72
5a9ea8d260 Merge pull request #8344 from verifiedtm/deck-editor-sections
Add other deck sections for other formats
2025-08-22 07:56:55 -05:00
Fulgur14
5a363922bd Oracle updates - EOE (#8496) 2025-08-22 12:57:01 +02:00
Anthony Calosa
1ff16ca509 prevent NPE 2025-08-22 16:43:17 +08:00
TRT
c878401197 Script cleanup 2025-08-22 10:03:27 +02:00
Eradev
8f518b7b1f Fix IsDoctorCheck 2025-08-22 06:33:29 +02:00
Anthony Calosa
fb624458f0 update InventoryScene, add repair cracked item for gold 2025-08-22 12:32:12 +08:00
Paul Hammerton
ee3220f33b Merge pull request #8487 from paulsnoops/fix-yeoe
YEOE edition and format updates for check land cycle
2025-08-21 19:31:16 +01:00
Paul Hammerton
7bc591fa78 YEOE mana-fixing lands edtion and format update 2025-08-21 19:25:47 +01:00
kevlahnota
c02d942fc3 Merge pull request #8485 from kevlahnota/master3
request access if needed to use backup and restore
2025-08-21 22:00:02 +08:00
Anthony Calosa
3b9ded8270 request access if needed to use backup and restore 2025-08-21 21:49:45 +08:00
tool4ever
fdf0c13a7f Remove outdated Briar Shield code path (#8477) 2025-08-21 07:15:21 +01:00
Hans Mackowiak
80b1cac394 lf 2025-08-21 07:05:35 +02:00
Hans Mackowiak
8c80c32113 lf 2025-08-21 07:01:46 +02:00
Agetian
a74b033c26 Revert "Ambrosia Whiteheart AI slight improvement (#8473)" (#8483)
This reverts commit ba8a30ebdd.
2025-08-21 07:48:20 +03:00
kevlahnota
16fcbc0ebf Merge pull request #8481 from kevlahnota/master3
fix osFamily name for sentry, update android-all
2025-08-21 09:54:47 +08:00
Anthony Calosa
62ffada6fe update cpu name 2025-08-21 09:46:30 +08:00
Anthony Calosa
e5a0d335af fix osFamily name for sentry, update android-all 2025-08-21 08:00:29 +08:00
Agetian
ba8a30ebdd Ambrosia Whiteheart AI slight improvement (#8473)
* - Add achievement for EOE/EOC by Marek14.

* - Slight logic improvement for Ambrosia Whiteheart
2025-08-20 18:17:35 +03:00
Agetian
e25dbe5196 Add puzzles PS_EOE1 and PS_EOE2. (#8475)
* - Add achievement for EOE/EOC by Marek14.

* - Add puzzles PS_EOE1 and PS_EOE2.
2025-08-20 18:17:11 +03:00
Renato Filipe Vidal Santos
3533ad8b74 Update darigaaz_shivan_champion.txt (#8474) 2025-08-20 18:16:58 +03:00
kevlahnota
2d20ce07e3 Merge pull request #8470 from kevlahnota/master3
move hwinfo output on create method
2025-08-20 21:59:21 +08:00
Anthony Calosa
e5e0a9240d move hwinfo output on create method 2025-08-20 21:55:22 +08:00
kevlahnota
5e83fff859 Merge pull request #8469 from kevlahnota/master3
update device info
2025-08-20 20:58:00 +08:00
Anthony Calosa
2d0acc734e indention 2025-08-20 20:56:22 +08:00
Anthony Calosa
b6bc3a6e96 update device info
should fill up sentry basic device and os info for mobile builds
2025-08-20 20:41:46 +08:00
Eradev
7ec7a65f33 Partially revert 38b6283a by restoring deleted rebalanced card files 2025-08-20 12:55:47 +02:00
Eradev
2a97b67f72 Remove unused import 2025-08-20 12:55:47 +02:00
Eradev
06be01d81c Remove WIP 2025-08-20 12:55:47 +02:00
Eradev
386f6aaac6 Try to get non-Alchemy version 2025-08-20 12:55:47 +02:00
Eradev
82d11bfb45 Update coll.no. for Zendikar Rising 2025-08-20 12:55:47 +02:00
Eradev
f39d900fbf Huge Alchemy Cleanup 2025-08-20 12:55:47 +02:00
Eradev
335edec357 Add info 2025-08-20 12:53:44 +02:00
Eradev
e58f035b19 Update JUD 2025-08-20 12:53:44 +02:00
Eradev
f85d610393 Update J13 2025-08-20 12:53:44 +02:00
Eradev
fce0095af1 Update J12 2025-08-20 12:53:44 +02:00
Eradev
82bea111df Update G11 2025-08-20 12:53:44 +02:00
Eradev
0aded8f1f4 Add borders to H17 2025-08-20 12:53:44 +02:00
Eradev
84130f0586 Update XLN 2025-08-20 12:53:44 +02:00
Eradev
ed4d1059dd Update INV 2025-08-20 12:53:44 +02:00
Eradev
c73be86211 Update ISD 2025-08-20 12:53:44 +02:00
Eradev
6cb2e7a91a Update MID 2025-08-20 12:53:44 +02:00
Eradev
58713e65ed Update VOW 2025-08-20 12:53:44 +02:00
Eradev
676c26fae7 Update IKO 2025-08-20 12:53:44 +02:00
Eradev
f2e4e67021 Update ICE 2025-08-20 12:53:44 +02:00
Eradev
18ba476fc9 Fix H17 2025-08-20 12:53:44 +02:00
Eradev
d9ed82972f Add H17 2025-08-20 12:53:44 +02:00
Eradev
c9affaa1a1 Update HOU 2025-08-20 12:53:44 +02:00
Eradev
8ef89adaef Update HML 2025-08-20 12:53:44 +02:00
Eradev
f25db898ae Remove Code2 MD1 2025-08-20 12:52:52 +02:00
Eradev
cddc41b353 Update MOR 2025-08-20 12:52:52 +02:00
Eradev
d4e918660b Update MMA 2025-08-20 12:52:52 +02:00
Eradev
3f16a3e27f Update MM3 2025-08-20 12:52:52 +02:00
Eradev
53d27f1437 Update MM2 2025-08-20 12:52:52 +02:00
Eradev
408dd310d3 Update MH1 2025-08-20 12:52:52 +02:00
Eradev
0050446ff0 Update MH3 2025-08-20 12:52:52 +02:00
Eradev
efb47d949f Update MH2 2025-08-20 12:52:52 +02:00
Eradev
a67303866e Update MD1 2025-08-20 12:52:52 +02:00
Eradev
756e80595c Update MRD 2025-08-20 12:52:52 +02:00
Eradev
089a021d02 Update PMBS 2025-08-20 12:52:52 +02:00
Eradev
d89a94dcd2 Update MBS 2025-08-20 12:52:52 +02:00
Eradev
b298f66348 Update MIR 2025-08-20 12:52:52 +02:00
Eradev
fa277c7b7c Update MMQ 2025-08-20 12:52:52 +02:00
Eradev
76ec449d33 Update ME1 2025-08-20 12:52:52 +02:00
Eradev
d481ade524 Update ME4 2025-08-20 12:52:52 +02:00
Eradev
1b468efff1 Update ME3 2025-08-20 12:52:52 +02:00
Eradev
9be9a4795b Update ME2 2025-08-20 12:52:52 +02:00
Eradev
49bcf7ded4 Update A25 2025-08-20 12:52:52 +02:00
Eradev
28989b9c49 Update P04 2025-08-20 12:52:52 +02:00
Eradev
9e40b1f6cf Update MPR 2025-08-20 12:52:52 +02:00
Eradev
a9e95caa42 Update ORI 2025-08-20 12:52:52 +02:00
Eradev
eb39c97661 Update TD0 2025-08-20 12:52:52 +02:00
Eradev
52f19272d0 Update M15 2025-08-20 12:52:52 +02:00
Eradev
4cf37379c7 Update PM15 2025-08-20 12:52:52 +02:00
Eradev
746d0476c9 Update PM10 2025-08-20 12:52:52 +02:00
Eradev
cf93d61d0c Update M14 2025-08-20 12:52:52 +02:00
Eradev
5d9197446b Update PM14 2025-08-20 12:52:52 +02:00
Eradev
753d5560d6 Update M13 2025-08-20 12:52:52 +02:00
Eradev
1b68d30ff1 Update PM10 2025-08-20 12:52:52 +02:00
Eradev
278eed7af9 Update PPC1 2025-08-20 12:52:52 +02:00
Hans Mackowiak
6eb4e32225 Update Murders at Karlov Manor.txt 2025-08-20 12:48:52 +02:00
tool4ever
e160a7d517 Tighten scripts (#8467) 2025-08-20 12:34:56 +02:00
Eradev
476cdda0c2 Use KTK goblin as r_1_1_goblin_haste since it has unique art 2025-08-20 12:30:18 +02:00
Eradev
efdabb8bf6 Update PLG25 2025-08-20 12:30:18 +02:00
Eradev
ca648df852 Update LRW 2025-08-20 12:30:18 +02:00
Eradev
04afb61351 Update LEB 2025-08-20 12:30:18 +02:00
Eradev
869632361c Update LEA 2025-08-20 12:30:18 +02:00
Eradev
d994abb559 Update LGN 2025-08-20 12:30:18 +02:00
Eradev
d082c2c250 Update LEG 2025-08-20 12:30:18 +02:00
Eradev
586a474168 Update PZ1 2025-08-20 12:30:18 +02:00
Eradev
b7268dd6fd Update NEO 2025-08-20 12:30:18 +02:00
Eradev
c3e4ea228a Update KTK 2025-08-20 12:30:18 +02:00
Eradev
237946f569 Update KHM 2025-08-20 12:30:18 +02:00
Eradev
cae32e5b2a Update KLD 2025-08-20 12:30:18 +02:00
tool4ever
715ba9803f Fix running out of memory from too many waiting triggers (#8466) 2025-08-20 11:42:04 +02:00
Renato Filipe Vidal Santos
283198f0a4 Tidying trigger-granting Slivers (#8465) 2025-08-20 11:40:54 +02:00
Fulgur14
bab889c406 Firebending cards (TLA) (#8445) 2025-08-20 11:40:22 +02:00
tool4ever
92c6f5369d Fix missing trigger (#8462) 2025-08-20 10:30:55 +02:00
Renato Filipe Vidal Santos
6042948aed Update freyalise_skyshroud_partisan.txt 2025-08-20 06:39:07 +02:00
Fulgur14
136577fec0 Avatar Aang (TLA) (#8459) 2025-08-19 21:30:13 +02:00
tool4ever
180fda53df Update krumar_initiate.txt 2025-08-19 20:03:36 +02:00
Antonio Martinelli
ca6b175fb8 Import from CubeCobra for Draft and Sealed (#8375)
* Import from CubeCobra feature for Draft and Sealed

* adjust comment for cube Import

* CubeImporter refactoring and generalisation

* Allow hyphens in Cube ID validation

* remove unused imports

* make parseFromURL private

* add LAST_IMPORTED_CUBE_ID to ForgePreferences

---------

Co-authored-by: Antonio <mart@gmail.com>
2025-08-19 11:25:50 -04:00
Antonio Martinelli
dd25dc4b06 update rankings_cubecobra.txt (#8458) 2025-08-19 18:13:08 +03:00
Hans Mackowiak
9893fc3cf9 Player: extra Flag for Element Bend 2025-08-19 16:41:31 +02:00
Michael Rappazzo
67b6c2c03f fix: move CHANGES.txt generation to target directory
- Generate CHANGES.txt in forge-gui-desktop/target/ instead of source tree
- Update installer to copy from target directory for all build profiles
- Add CHANGES.txt to .gitignore since it's generated
- Remove hardcoded fromRef to use latest tag automatically
- Remove maven-release-plugin exclusion for untracked file
2025-08-19 06:49:42 -04:00
Agetian
bb073b8682 Tweak AI logic for Krumar Initiate (#8453)
* - Add achievement for EOE/EOC by Marek14.

* - Add AI logic for Krumar Initiate

* - Tweak AI logic for Krumar Initiate

* Update EndureAi.java

only when doing X

---------

Co-authored-by: Hans Mackowiak <hanmac@gmx.de>
2025-08-19 13:36:11 +03:00
Agetian
9bbfad5be3 AI logic for Krumar Initiate (#8450)
* - Add achievement for EOE/EOC by Marek14.

* - Add AI logic for Krumar Initiate
2025-08-19 13:11:41 +03:00
Paul Hammerton
2c7e7d5b63 Merge pull request #8448 from paulsnoops/edition-updates
Edition updates: AA1, AA2
2025-08-19 10:21:44 +01:00
Paul Hammerton
94e7258a03 Edition updates: AA1, AA2 2025-08-19 10:18:58 +01:00
Renato Filipe Vidal Santos
95eb90eac2 Update claws_out.txt 2025-08-19 07:08:24 +02:00
Eradev
0f0a4c54e7 Fix Hanweir Garrison CollNo 2025-08-19 07:07:27 +02:00
Eradev
399923bfa4 Remove dupe + fix SIR 2025-08-19 07:07:27 +02:00
Eradev
39f0ab9eae Fix closing bracket 2025-08-19 07:07:27 +02:00
Eradev
8b1cd54417 Search split types 2025-08-19 07:07:27 +02:00
Eradev
ff2192fa7a Fix Meld key 2025-08-19 07:07:27 +02:00
Eradev
08443a307c Fix Domri Rade's emblem 2025-08-19 06:35:36 +02:00
Eradev
bd90e1bccc Check for emblems in other instead of tokens 2025-08-19 06:35:36 +02:00
Hans Mackowiak
9cb0bd301e TLA: Firebending 2025-08-19 06:13:00 +02:00
kevlahnota
0923960215 Merge pull request #8442 from Jetz72/fixes20250818
Fix auto-sell cards reappearing in collection
2025-08-19 08:36:26 +08:00
Jetz
0f68dc1ab6 Make give item command case-insensitive 2025-08-18 19:06:52 -04:00
Jetz
09b88b8575 Fix auto-sell not being removed from adventure collection 2025-08-18 19:05:02 -04:00
kevlahnota
fae2f25b69 Merge pull request #8441 from kevlahnota/master3
update Itemmanager portrait layout, update Sentry scope
2025-08-19 06:29:03 +08:00
Anthony Calosa
d3e0696ecc update Itemmanager portrait layout, update Sentry scope 2025-08-19 06:19:44 +08:00
Paul Hammerton
6a7723eba9 Merge pull request #8440 from paulsnoops/fix-scripts
Fix Earth Rumble and Roku's Mastery
2025-08-18 23:05:34 +01:00
Paul Hammerton
10d6fd157e Fix Earth Rumble and Roku's Mastery 2025-08-18 23:01:10 +01:00
Paul Hammerton
048133df30 Merge pull request #8439 from paulsnoops/aa2-edition-fixes
AA2 edition spelling
2025-08-18 22:56:04 +01:00
Paul Hammerton
2fe7fec14f AA2 edition spelling 2025-08-18 22:53:36 +01:00
Paul Hammerton
73e7b27c09 Merge pull request #8438 from paulsnoops/yeoe-formats
Format updates: YEOE
2025-08-18 22:35:33 +01:00
Paul Hammerton
2fdbd5a85c Add YEOE to formats 2025-08-18 22:32:06 +01:00
Paul Hammerton
78e27e0073 Merge pull request #8437 from paulsnoops/edition-updates
Edition updates: AA1, AA2, SLD, TLA, TLE, YEOE
2025-08-18 22:25:38 +01:00
Paul Hammerton
bad585c8c9 Edition updates: AA1, AA2, SLD, TLA, TLE, YEOE 2025-08-18 22:23:11 +01:00
Eradev
11d10a8129 Fix Meld cards fetch (#7981)
* BOM

* ANB Update

* J25 Update

* SIR update

* Mend

* Find the right back for meld cards

* ANB Update

* J25 Update

* SIR update

* Mend

* Find the right back for meld cards

* Update PW25

* Update SPG

* Typo

* Update ACR

* Update CMM

* Add PFDN and Invasion alternate arts

* Update MKM

* Update NPH

* J25 -> J22 for non-existing cards

* Update WOC

* Update HBG

* Use printsheet

* Use helper functions
2025-08-18 14:31:33 +02:00
Agetian
0b89e9d137 - Add achievement for EOE/EOC by Marek14. (#8433) 2025-08-18 15:21:09 +03:00
kevlahnota
70df1ff0aa Merge pull request #8428 from Jetz72/fixes20250817
Handful of Deck Editor updates
2025-08-18 16:52:30 +08:00
Jetz
24d3169592 Enable variant replacement in Quest and Planar Conquest 2025-08-17 21:37:36 -04:00
Jetz
b8db0fea5e Disable custom cards conformity check in Adventure mode. 2025-08-17 21:11:51 -04:00
Jetz
514519b45b Merge branch 'master' into fixes20250817
# Conflicts:
#	forge-gui-mobile/src/forge/itemmanager/ItemManager.java
2025-08-17 21:01:42 -04:00
Jetz
70f6bcb63c Fix missing or crushed group and pile-by buttons in narrow layouts 2025-08-17 20:52:21 -04:00
Jetz
5a3e55f704 Fix crushed filter buttons in portrait mode 2025-08-17 20:51:20 -04:00
Jetz
4452e07443 Remove unused format filter visibility controls 2025-08-17 19:58:20 -04:00
Jetz
a125e6c0ea Support command line args width= and height= for easier testing of portrait layouts. 2025-08-17 19:57:21 -04:00
kevlahnota
fa4688e113 Merge pull request #8426 from kevlahnota/master3
fix Itemmanager portrait layout
2025-08-18 07:46:12 +08:00
Anthony Calosa
0236609558 fix Itemmanager portrait layout 2025-08-18 07:40:52 +08:00
kevlahnota
3f15fb1b98 Merge pull request #8424 from shenshinoman/Crystal_Expansions
Fixing bugs that were preventing proper world gen in Crystal Kingdoms…
2025-08-18 06:01:41 +08:00
kevlahnota
e22718c72a Merge pull request #8425 from kevlahnota/master3
fix RandomDeckGenerator AI Deck Selection by preference
2025-08-18 05:59:47 +08:00
Anthony Calosa
e94985576c refactor check 2025-08-18 05:56:54 +08:00
Anthony Calosa
71e132a9e0 fix RandomDeckGenerator AI Deck Selection by preference 2025-08-18 05:52:24 +08:00
Jetz
8d5d56bed8 Add dev cheat to add cards to inventory and draft packs 2025-08-17 17:18:17 -04:00
shenshinoman
386c9799c2 Fixing bugs that were preventing proper world gen in Crystal Kingdoms. Very basic fix, and a slight notice in the quests to inform the player that they have not been udpated yet. 2025-08-17 16:08:03 -04:00
Fulgur14
a1be657460 Airbending cards (#8422) 2025-08-17 19:36:15 +01:00
Fulgur14
d4994e8fa0 Fire Lord Ozai (TLE) (#8361)
* Support CombatMana
2025-08-17 17:03:26 +01:00
Renato Filipe Vidal Santos
fb054d9f64 Tidying DBCleanup (#8420) 2025-08-17 13:34:23 +01:00
kevlahnota
8790843b3b Merge pull request #8421 from kevlahnota/master3
revert fileprovider, add sentryscope
2025-08-17 20:32:51 +08:00
Anthony Calosa
38383dff02 revert fileprovider, add sentryscope 2025-08-17 20:20:33 +08:00
Hans Mackowiak
a0f9923c21 TLA: Airbend (#8419) 2025-08-17 13:24:11 +02:00
kevlahnota
812c0ac5b1 Merge pull request #8418 from kevlahnota/master3
update SettingsScene, Settings page indent
2025-08-17 11:12:43 +08:00
Anthony Calosa
2e09146f44 remove unused options 2025-08-17 11:02:48 +08:00
Anthony Calosa
25080411d5 update SettingsScene, Settings page indent
limit hasprintedpt to vehicles and spacecraft to fix modal card renders with non creature backside
- closes #8413
2025-08-17 10:52:17 +08:00
kevlahnota
55e20e96f9 Merge pull request #8414 from Jetz72/fixes20250816
Attempt to fix another NPE in deserializing decks
2025-08-17 06:14:31 +08:00
kevlahnota
981ab1c6ed prevent NPE 2025-08-17 06:13:51 +08:00
Hans Mackowiak
a1089f8073 Update the_cabbage_merchant.txt 2025-08-16 23:56:03 +02:00
tool4ever
fe747a3908 Update the_cabbage_merchant.txt 2025-08-16 21:50:39 +02:00
Paul Hammerton
a16c9480e7 Merge pull request #8412 from Eradev/FixEOE
Fix EOE def
2025-08-16 16:21:41 +01:00
Jetz72
b40e9fc817 Merge branch 'Card-Forge:master' into fixes20250816 2025-08-16 10:15:33 -05:00
Jetz
6eec0e8988 Fix another potential NPE deserializing old decks... 2025-08-16 11:13:42 -04:00
Hans Mackowiak
d6320caadf CardEdition: getPrintSheetsBySection use collectorNumber instead of index 2025-08-16 16:53:43 +02:00
Eradev
c4f125525a Fix EOE def 2025-08-16 10:38:43 -04:00
kevlahnota
56209261d0 Merge pull request #8411 from Jetz72/fixes20250816
Fix NPE deserializing old decks
2025-08-16 22:37:44 +08:00
Jetz
5df4d64345 Fix a potential NPE deserializing old decks.
Fix `Deck.cloneFieldsTo` to copy tags in the right direction.
2025-08-16 10:05:41 -04:00
kevlahnota
d6d6065104 Merge pull request #8402 from Jetz72/fixes20250815c
A few auto-sell section fixes
2025-08-16 14:32:04 +08:00
kevlahnota
333aea0641 Merge pull request #8405 from kevlahnota/master3
fix booster draft menu not showing on first try
2025-08-16 14:29:57 +08:00
Anthony Calosa
c7bb3d49a6 fix booster draft menu not showing on first try 2025-08-16 14:14:33 +08:00
Jetz
7c982498dd Avoid trying to generate a move option for cards that don't have a valid destination 2025-08-15 23:51:19 -04:00
Jetz
0e2c95afed Fix ClassCastException 2025-08-15 23:48:50 -04:00
Jetz
df6eb23341 Always show quantity of auto-sell section. 2025-08-15 23:48:31 -04:00
kevlahnota
1204de13c3 Merge pull request #8400 from Jetz72/fixes20250815b
Fix negative gold payouts from selling cards.
2025-08-16 09:55:23 +08:00
Jetz
9cb21de11d Fix negative gold payouts from selling cards. 2025-08-15 21:37:58 -04:00
kevlahnota
2f16b9e110 Merge pull request #8399 from Jetz72/fixes20250815
Fix some crashes from the new editor
2025-08-16 07:22:57 +08:00
Jetz
cf28139b02 Fix a second crash for the same reason... 2025-08-15 19:19:28 -04:00
Jetz
87e8e7d5e5 Fix crash entering a game without extra sections 2025-08-15 19:12:40 -04:00
Jetz
dcc3a681d9 Whoops committed the wrong file.
This reverts commit cc7f30da88.
2025-08-15 19:12:39 -04:00
Jetz
cc7f30da88 Fix crash entering a game without extra sections 2025-08-15 19:05:59 -04:00
Jetz
ed9eabed38 Fix Jumpstart deck preview 2025-08-15 18:31:45 -04:00
Paul Hammerton
99ac95bfca Merge pull request #8398 from paulsnoops/edition-updates
Edition updates: PMEI
2025-08-15 21:24:05 +01:00
Paul Hammerton
eb178fc9d1 Edition updates: PMEI 2025-08-15 21:15:07 +01:00
Paul Hammerton
7567e29cf1 Merge pull request #8397 from paulsnoops/edition-updates
Edition updates: TLA, TLE, YEOE
2025-08-15 19:18:48 +01:00
Paul Hammerton
b2a456140c YEOE 2025-08-15 19:11:49 +01:00
Paul Hammerton
9c3ff6b570 Edition updates: TLA, TLE 2025-08-15 18:40:44 +01:00
Hans Mackowiak
e4d238ba6c ~lf 2025-08-15 19:27:29 +02:00
Hans Mackowiak
42363204dd add PrintSheet & ItemPool helper 2025-08-15 19:27:09 +02:00
Jetz72
de89d557dd Mobile Deck Editor Reorganization and Feature Update (#7519)
Mobile Deck Editor Reorg
2025-08-15 11:48:25 -04:00
kevlahnota
c5a53b21e7 Merge pull request #8396 from kevlahnota/master3
fix flip cards redownloading same image with different name
2025-08-15 20:43:07 +08:00
Anthony Calosa
02f43b1ef0 update comment 2025-08-15 20:37:58 +08:00
Anthony Calosa
d7cb0b7ac1 fix flip cards redownloading same image with different name
also fix sync for sentry native ndk since this is declared as system scope
2025-08-15 20:34:26 +08:00
kevlahnota
2efeb573e6 Merge pull request #8395 from Eradev/DisplayFlipCards
Display flip cards correctly
2025-08-15 20:10:24 +08:00
kevlahnota
370ff638ab Merge pull request #8394 from dracontes/rv-fix_odin
Update summon_primal_odin.txt
2025-08-15 20:10:10 +08:00
kevlahnota
108564efe3 Merge pull request #8392 from Eradev/UpdatePLST
Update PLST
2025-08-15 20:09:34 +08:00
Eradev
43f96657e0 Rename vars 2025-08-15 06:57:06 -04:00
Eradev
cb004bfba2 Fix imports 2025-08-15 06:53:17 -04:00
Eradev
f276597a82 Flip in Reward screen 2025-08-15 06:49:18 -04:00
Renato Filipe Vidal Santos
4fd9855daa Update summon_primal_odin.txt 2025-08-15 11:27:31 +01:00
Eradev
ebdce74f92 Fix display in inventory 2025-08-15 06:20:20 -04:00
Eradev
98e9b8f28b Display flipped cards correctly 2025-08-15 05:54:31 -04:00
Hans Mackowiak
a31c0358a4 Update TriggerType.java
Add the other Bending trigger
2025-08-15 11:21:22 +02:00
Fulgur14
8640522fff TLA/TLE cards (14th August), Batch 4 (#8387) 2025-08-15 09:59:17 +02:00
Fulgur14
a5fa350e7e TLA/TLE cards (14th August), Batch 3 (#8385) 2025-08-15 09:57:34 +02:00
Fulgur14
c4a0cff1ca TLA/TLE cards (14th August), Batch 2 (#8383) 2025-08-15 09:56:22 +02:00
Fulgur14
30d02ebbb6 TLA/TLE cards (14th August), Batch 1 (#8382) 2025-08-15 09:48:11 +02:00
Hans Mackowiak
9a90359283 Update EarthbendEffect.java
Fix apostrophe
2025-08-15 09:44:55 +02:00
Fulgur14
edbcab544e TLA/TLE Earthbending cards (#8393) 2025-08-15 09:42:48 +02:00
Hans Mackowiak
f55bf4691d TLA: Earthbend (#8386) 2025-08-15 09:24:45 +02:00
Eradev
16e44309a6 Update PLST 2025-08-15 02:43:27 -04:00
tool4ever
c4d58e3dba Update wormfang_drake.txt 2025-08-15 08:33:17 +02:00
Ellios77
16155d3670 Punctuation fixes for card scripts (#8389) 2025-08-15 08:21:49 +02:00
kevlahnota
bd929bcc72 Merge pull request #8388 from kevlahnota/master3
add Preload Custom Drafts
2025-08-15 08:52:49 +08:00
Anthony Calosa
e2cc52fd02 add Preload Custom Drafts
Should fix longer startup time on mobile version instead of forcing to load custom drafts file at startup.
2025-08-15 08:45:21 +08:00
Paul Hammerton
be5c7cfd04 Merge pull request #8381 from paulsnoops/unf-update
Edition updates: UNF
2025-08-14 18:27:57 +01:00
Paul Hammerton
9d14949138 Edition updates: UNF 2025-08-14 18:25:28 +01:00
tool4ever
6fe5dbad3c Update InputAttack.java 2025-08-14 16:53:18 +00:00
Matthew Scott Krafczyk
f4f8ee9cb6 android-dev-build (#8352)
* improve resource finding procedure and add android-dev-build

Added the `android-dev-build` profile to trigger building a developer
apk.

Developer apk is at application id forge.app.dev, and has name
'Forge (dev)'. It is installable in parallel with the official release
app allowing developers to test the android build on their phones
without interfering with their existing installation.

Updated how android resources are found to accomodate changes in
application id at runtime. This method doesn't rely on the 'R' package.

* Use all arguments of getIdentifier

Use all arguments of getIdentifier instead of using a fully qualified
resource name
2025-08-14 19:06:54 +03:00
Antonio Martinelli
0767eb03e0 Add LSVCube and Name-Sticker Goblin (card) (#8348)
* Add LSVCube and draft

* Add "Name Sticker" Goblin card (MTGO Version)

---------

Co-authored-by: Antonio <mart@gmail.com>
Co-authored-by: Agetian <stavdev@mail.ru>
2025-08-14 19:06:46 +03:00
kevlahnota
feb6062f2f Update MakeCardEffect.java
- closes #8373
2025-08-14 22:44:23 +08:00
Eradev
113dc4d5e8 Don't try to attack itself. (#8376) 2025-08-14 16:11:00 +02:00
Hans Mackowiak
ca20d98a2c Update Tarkir Dragonstorm.txt 2025-08-14 14:15:00 +02:00
Hans Mackowiak
599a629068 Update Dungeons & Dragons Adventures in the Forgotten Realms.txt
Also add Dungeons to "other" section
2025-08-14 09:54:59 +02:00
Hans Mackowiak
4c05ad655d Update Dungeons & Dragons Adventures in the Forgotten Realms.txt
Fix Dungeon CN
2025-08-14 09:22:13 +02:00
Ellios77
203233f0d2 Fixed typo in script (#8372) 2025-08-14 09:18:31 +02:00
Hans Mackowiak
ac8f7357db Update corrupted_shapeshifter.txt 2025-08-14 06:29:33 +02:00
kevlahnota
4c396065f5 Merge pull request #8370 from kevlahnota/master3
update Sentry
2025-08-14 06:07:54 +08:00
Anthony Calosa
cc4dac38fe update Sentry 2025-08-14 05:59:47 +08:00
Eradev
394818f533 Filter by foil status (#8358) 2025-08-13 19:45:53 +03:00
Eradev
bfd4a68e23 Try to attack mustAttackEntities first. (#8355) 2025-08-13 19:45:42 +03:00
Paul Hammerton
9d54e82214 Merge pull request #8368 from paulsnoops/edition-updates
Edition updates: TLA, TLE
2025-08-13 17:41:30 +01:00
Paul Hammerton
18c2278066 Edition updates: TLA, TLE 2025-08-13 17:40:00 +01:00
tool4ever
b3d549a712 Update mirror_shield_hoplite.txt 2025-08-13 16:22:58 +00:00
tool4ever
0887e78e90 Update agrus_kos_eternal_soldier.txt 2025-08-13 16:22:26 +00:00
Hans Mackowiak
df3f86dd73 CardState: Fix Card copies itself though ReplacementEffect (#8356)
Co-authored-by: tool4EvEr <tool4EvEr@>
2025-08-13 17:51:14 +02:00
Hans Mackowiak
89af1d0cf1 Update Card.java 2025-08-13 17:07:32 +02:00
Eradev
10a68d63a1 Fix exact search (#8357) 2025-08-13 08:46:47 +00:00
Hans Mackowiak
8ccebbc13c Update The List.txt
Closes #8363
2025-08-13 10:37:22 +02:00
Fulgur14
4c663cd943 TLA/TLE cards that don't require bending (#8359) 2025-08-13 08:36:41 +00:00
Hans Mackowiak
ca16bf74f5 Update Card.hasNoAbilities
For Adventure and Omen
2025-08-13 10:09:01 +02:00
tool4ever
a1968ef9fd Update depthshaker_titan.txt 2025-08-13 07:58:25 +02:00
kevlahnota
de1a999611 Merge pull request #8353 from kevlahnota/master3
exclude unused xcf files in releases
2025-08-12 20:56:41 +08:00
Anthony Calosa
d0e8bc5de0 exclude unused xcf files in releases
- closes #8322
2025-08-12 20:49:49 +08:00
Eradev
38fb647cd7 Migration to PLST (#7972)
* Fallback to PLST

* Fix audit for Funny cards.

* Remove FMB1, update PLST

* Fallback to PLST

* Convert cards to PLST

* BOM

* Minor fix

* LF

* Remove conversion. Already handled.
2025-08-12 08:17:11 +03:00
kevlahnota
90bb83c5d4 Merge pull request #8351 from kevlahnota/master3
add backup/restore classic mode data
2025-08-11 22:00:07 +08:00
Anthony Calosa
467fff3651 add backup/restore classic mode data 2025-08-11 21:53:19 +08:00
Hans Mackowiak
3ab50a5fb5 Update samis_curiosity.txt
Add StackDesc for Lander Token
2025-08-11 09:35:03 +02:00
Tim Miller
e4d58a0a88 Add other deck sections for other formats 2025-08-10 17:03:30 -04:00
kevlahnota
10e592c741 Merge pull request #8343 from kevlahnota/master3
request external storage access
2025-08-10 20:04:33 +08:00
Anthony Calosa
b3241c85eb request external storage access
- closes #7405
2025-08-10 19:56:29 +08:00
Cees Timmerman
abd1702c96 Fix NullPointerException in forge.game.zone.Zone.toString (#8326) (#8327) 2025-08-10 08:03:56 +02:00
Hans Mackowiak
d91e86c608 Update GameEventManaPool.java (#8340)
Closes #8328, #8329
2025-08-09 16:13:49 +02:00
Hans Mackowiak
f0ebac019d Perpetual: use Record for different types (#8337)
* Perpetual: use Record for different types
2025-08-09 13:04:14 +02:00
tool4ever
c18995915a Fix scripts (#8339) 2025-08-09 10:50:27 +00:00
Paul Hammerton
55d2c23aa0 Merge pull request #8338 from paulsnoops/edition-updates
Edition updates: PSPM, PW25, SLD, SPM
2025-08-09 10:57:58 +01:00
Paul Hammerton
28c9f719c5 Edition updates: PSPM, PW25, SLD, SPM 2025-08-09 10:56:08 +01:00
Hans Mackowiak
bb5a276324 Update kessig.txt
Fix ActiveZones
2025-08-09 08:51:13 +02:00
tool4ever
e99df5b267 Update wellgabber_apothecary.txt
Closes #8334
2025-08-08 18:07:11 +00:00
kevlahnota
54e67b3d57 Merge pull request #8333 from kevlahnota/master3
update progressbar, update boosterdraft readfile
2025-08-08 22:01:42 +08:00
Anthony Calosa
b1d30ab9dd remove unused borderless list 2025-08-08 21:31:32 +08:00
Anthony Calosa
f2d65b2515 update check 2025-08-08 21:01:58 +08:00
Anthony Calosa
286fcd3fbe update progressbar, update boosterdraft readfile
also removed unneccesary settings since forge uses shaders for round corners for rendering borders
2025-08-08 20:46:12 +08:00
Hans Mackowiak
00332ff3fe Fix Phantom Train not updating PT (#8332)
* Fix Phantom Train not updating PT
2025-08-08 12:52:29 +02:00
tool4ever
000fa14c17 Fix voting crash (#8325) 2025-08-08 10:15:27 +02:00
tool4ever
c5b4be6c97 Update edgar_master_machinist.txt 2025-08-07 16:13:52 +00:00
kevlahnota
d9b2a8f4e0 Merge pull request #8321 from kevlahnota/master3
update sentry
2025-08-07 21:50:08 +08:00
Anthony Calosa
2898b00300 update sentry 2025-08-07 21:42:20 +08:00
Hans Mackowiak
a4671b62d4 CardStateName: combine Modal with Transformed (#8305)
* CardStateName: combine Modal with Transformed

* Make new TMDFC transformable

---------

Co-authored-by: tool4EvEr <tool4EvEr@>
2025-08-07 11:28:24 +02:00
kevlahnota
0c87ed7381 Merge pull request #8319 from kevlahnota/master3
fix VPrompt
2025-08-07 05:29:34 +08:00
Anthony Calosa
4104a0b2a8 fix VPrompt 2025-08-07 05:28:13 +08:00
tool4ever
4d62f436d5 ExiledWith check for keyworded (#8317) 2025-08-06 19:41:39 +00:00
kevlahnota
506f88bd6f Update friendly_neighborhood.txt
fix no API
2025-08-06 20:47:32 +08:00
Hans Mackowiak
a1ad355e25 Update CardEdition.java
Closes #8314 8314
2025-08-06 14:17:13 +02:00
kevlahnota
91d6571cff Merge pull request #8315 from kevlahnota/master3
Reversed prompt button toggle
2025-08-06 18:57:55 +08:00
Anthony Calosa
e43c5534b3 Reversed prompt button toggle
- closes #7366
2025-08-06 18:51:49 +08:00
Jetz
77e3453d35 Fix vote out card type 2025-08-05 21:50:39 -04:00
Jetz
2568df1b91 Reorder ReplaceMoved.canReplace to do some common easy checks first. 2025-08-05 21:50:39 -04:00
Jetz
dc830a8432 Include weird zones in forEachCardInGame 2025-08-05 21:50:39 -04:00
Chris H
a9cffbd313 Collect all of slots paper cards before adding it to the final result 2025-08-05 21:05:49 -04:00
tool4ever
65775f5c20 Update moonlit_meditation.txt
Closes #8310
2025-08-05 20:00:03 +00:00
Fulgur14
c6f9056e2c Gwen and Miles (Transformation to be added later) (#8306)
* Gwen and Miles (Transformation to be added later)

* Create miles_morales_ultimate_spider_man.txt

* Update miles_morales_ultimate_spider_man.txt

* Update miles_morales_ultimate_spider_man.txt
2025-08-05 19:06:01 +03:00
Hans Mackowiak
967807d441 Merge pull request #8307
Fixes Vote Protection
2025-08-05 11:17:15 +02:00
Hans Mackowiak
cc7b3505d7 Update ComputerUtilCard.java 2025-08-05 09:54:43 +02:00
Hans Mackowiak
3cd965a24f Update ComputerUtilCard.java 2025-08-05 09:49:11 +02:00
Hans Mackowiak
3502bf6de2 Update ComputerUtil.java
Fix
2025-08-05 09:45:31 +02:00
Hans Mackowiak
458e05635c Update CardFactoryUtil.java
Allow Iterable for getMostProminentColorsFromList
2025-08-05 09:42:47 +02:00
Hans Mackowiak
f4cd9e0f4f Update ComputerUtil.java
Fix Vote "Protection"
2025-08-05 09:40:12 +02:00
tool4EvEr
1f3871d5dc Thread.stop fallback 2025-08-04 22:15:23 +02:00
tool4ever
f4d304b2c1 Update abyssal_hunter.txt 2025-08-04 13:00:15 +00:00
tool4ever
24937fb32d Reduce potential for ConcurrentModificationException (#8302) 2025-08-04 14:30:10 +02:00
Hans Mackowiak
5d222577be Update AddTurnAi.java
Fix AddTurnAi
2025-08-04 14:15:46 +02:00
Hans Mackowiak
0e010cece2 Update ComputerUtil.java
Fix vote "StrengthOrNumbers"
2025-08-04 14:15:02 +02:00
Hans Mackowiak
21080d65f5 Create b_0_0_germ.txt
Closes #8301
2025-08-04 13:20:47 +02:00
Fulgur14
83475b9063 Carnage, Crimson Chaos (SPM) (#8299) 2025-08-04 07:40:39 +02:00
tool4ever
15a6494d9c Update space_time_anomaly.txt
Closes #8297
2025-08-03 16:26:40 +00:00
Hans Mackowiak
58ad1f3103 Keyword: Web-slinging 2025-08-03 16:24:52 +02:00
Hans Mackowiak
b2bff7762f Mayhem: add new Cast-from-Graveyard variant 2025-08-03 16:24:24 +02:00
kevlahnota
cd4a673862 Merge pull request #8295 from kevlahnota/master3
fix NPE and FChoiceList render display
2025-08-03 20:54:13 +08:00
Anthony Calosa
1f61c6bd00 fix NPE and FChoiceList render display 2025-08-03 20:51:02 +08:00
Cees Timmerman
cc2f46795f Fix infinite loop OOM crash (#8254) 2025-08-02 15:15:49 +02:00
Chris H
db5ea3ec9c Fix undo restore restoring to bottom of library 2025-08-02 15:03:27 +02:00
Adam Jones
21f2f6e2c3 Update forge.sh
Allow parameters to be passed in to the shell script
2025-08-02 15:03:11 +02:00
Hans Mackowiak
65b5f4fac3 VoteEffect: use SpellAbility Choices (#8286)
* VoteEffect: use SpellAbility Choices

* fixes

* fix VotePlayer

* subAbs only for Choices
2025-08-02 16:01:12 +03:00
tool4ever
2291cf9d0c Fix scripts (#8288) 2025-08-02 11:29:01 +00:00
Paul Hammerton
3f8b3fa2a7 Merge pull request #8287 from paulsnoops/edition-updates
Edition updates: PW25, SLD, SPM
2025-08-02 10:24:59 +01:00
Paul Hammerton
e6fa1268d9 Edition updates: PW25, SLD, SPM 2025-08-02 10:23:36 +01:00
Renato Filipe Vidal Santos
eda8205acd Fixing Ultima, Origin of Oblivion (#8278)
* Update ultima_origin_of_oblivion.txt

* Update ultima_origin_of_oblivion.txt

* Update ultima_origin_of_oblivion.txt

* Update ultima_origin_of_oblivion.txt

* Update ultima_origin_of_oblivion.txt
2025-08-02 08:20:50 +03:00
Renato Filipe Vidal Santos
019131447a Update icetill_explorer.txt 2025-08-02 06:59:59 +02:00
kevlahnota
41cd90bc2a Fix dual foil renders 2025-08-02 00:38:03 +08:00
kevlahnota
0fe1677ced Update Foil rotation check 2025-08-02 00:27:00 +08:00
tool4ever
b1a2a93f75 Update PumpAllAi.java 2025-08-01 12:43:56 +02:00
tool4ever
50a2c283f1 Update lightstall_inquisitor.txt 2025-08-01 08:34:25 +02:00
kevlahnota
83188daba4 Merge pull request #8275 from Card-Forge/kevlahnota-rotatefix
Extra check for rotation
2025-08-01 08:31:50 +08:00
kevlahnota
3312e409fd Extra check for rotation 2025-08-01 08:24:38 +08:00
tool4ever
5420a90f05 Update iron_spider_stark_upgrade.txt
Closes #8272
2025-07-31 20:16:28 +00:00
tool4ever
4d340d7bf3 Update wrathful_raptors.txt 2025-07-31 20:15:06 +00:00
tool4ever
5342175a97 Update wrathful_red_dragon.txt 2025-07-31 20:14:39 +00:00
tool4ever
e5a1c86b1f Align invalid Player property result with Card (for Lozhan) (#8264) 2025-07-31 18:56:55 +00:00
tool4ever
1aca32005b Fix Brave the Wilds (#8266)
Co-authored-by: tool4EvEr <tool4EvEr@>
2025-07-31 21:32:12 +03:00
tool4ever
6d7853428f Fix bad refactor (for real this time) (#8263)
Co-authored-by: tool4EvEr <tool4EvEr@>
2025-07-31 20:05:05 +02:00
kevlahnota
683d92399d FDeckEditor Foil Menu (#8260) 2025-07-31 21:58:05 +08:00
kevlahnota
9b9bf6cf53 update holofoil texture (#8259)
a bit modern but has more blended effect with most card images along with hue changes effect
2025-07-31 20:24:55 +08:00
kevlahnota
c9b826d9b4 Merge pull request #8237 from kevlahnota/master2
Refactor Foil Effect for Mobile
2025-07-31 05:48:36 +08:00
kevlahnota
81ff72f1f2 Merge pull request #8189 from kevlahnota/nyxDesktop
add nyx effect to desktop card renders
2025-07-31 05:45:46 +08:00
tool4ever
dfda04f7ba Fix bad refactor (#8252) 2025-07-30 20:43:20 +00:00
tool4ever
3bc0e0c386 More AI cleanup (#8251)
* Fix infinite loop
2025-07-30 19:59:20 +00:00
Hans Mackowiak
5829192446 Update valgavoths_onslaught.txt
fix SpellDescription
2025-07-30 21:50:57 +02:00
Chris H
cbae45be87 Update shaun_father_of_synths.txt (#8250) 2025-07-30 19:23:54 +00:00
Agetian
2b4ec73318 - Tweak marked damage on PS_FIN4. (#8249) 2025-07-30 18:21:59 +03:00
Agetian
7ff426e413 - Add puzzle PS_FIN4. (#8248) 2025-07-30 18:19:26 +03:00
shenshinoman
25e3ad8432 Resolving the land shop issue in Shandalar Old World. (#8231) 2025-07-30 17:52:25 +03:00
tool4ever
051edff18e Consolidate logic (#8247) 2025-07-30 09:50:59 +00:00
Anthony Calosa
100ae7b164 update dispose 2025-07-30 08:51:32 +08:00
Anthony Calosa
3b4c417549 Refactor Foil Effect for Mobile
Should fix rendering for Full, Crop and Art renders
2025-07-29 19:42:01 +08:00
kevlahnota
3fcb9e7a3c add stars.png 2025-07-24 19:57:11 +08:00
Anthony Calosa
f69ff10b7c add nyx effect to desktop card renders 2025-07-24 19:31:26 +08:00
2876 changed files with 63171 additions and 38139 deletions

View File

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

View File

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

View File

@@ -129,7 +129,9 @@ jobs:
makeLatest: true
- name: 🔧 Install XML tools
run: sudo apt-get install -y libxml2-utils
run: |
sudo apt-get update
sudo apt-get install -y libxml2-utils
- name: 🔼 Bump versionCode in root POM
id: bump_version

28
.github/workflows/sync-wiki.yml vendored Normal file
View File

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

View File

@@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
java: [ '17' ]
java: ['17', '21']
name: Test with Java ${{ matrix.Java }}
steps:
- uses: actions/checkout@v3

7
.gitignore vendored
View File

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

View File

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

View File

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

View File

@@ -15,7 +15,7 @@ public class Main {
public static void main(String[] args) {
GuiBase.setInterface(new GuiMobile(Files.exists(Paths.get("./res"))?"./":"../forge-gui/"));
GuiBase.setDeviceInfo("", "", 0, 0);
GuiBase.setDeviceInfo(null, 0, 0, System.getProperty("user.home") + "/Downloads/");
new EditorMainWindow(Config.instance());
}
}

68
docs/AI.md Normal file
View File

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

12
docs/Adventure-Mode.md Normal file
View File

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

109
docs/Card-Images.md Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

90
docs/Configure-Planes.md Normal file
View File

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

23
docs/Configure-Sets.md Normal file
View File

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

View File

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

107
docs/Create-Enemies.md Normal file
View File

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

178
docs/Create-Rewards.md Normal file
View File

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

42
docs/Create-new-Maps.md Normal file
View File

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

View File

@@ -0,0 +1,178 @@
# Guide - Creating a Custom Card
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.
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
In this tutorial we'll create a new card called "Goblin Card Guide".
1. Open Script Directory
Navigate to your Custom Card Scripts folder and open (or create) the subfolder for the starting letter of your new card.
In our example:
> C:/Users/<your username>/Application Data/Roaming/Forge/custom/cards/g
2. Add Card Script
Create a new .txt file using all lowercase letters. Spaces are represented with "_" and you can leave out special characters like commas or apostrophes.
> goblin_card_guide.txt
Now we need to define our card. Add the following to your Goblin_Card_Guide text file:
```
Name:Goblin Card Guide
ManaCost:1 R
Types:Creature Goblin
PT:2/2
K:Haste
Oracle:Haste
```
Let's break our card down:
- Name - The name as it appears on the card.
- ManaCost - The card's cost with colorless mana first and spaces between different mana symbols.
- Types - The card's type and then any subtypes seperated by spaces.
- PT - Power and Toughness, which is only used for Creatures (or some cards that turn into creatures like Vehicles)
- K - A Keyword that gives our creature an ability.
- 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.
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`
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.
In the "CDI Promos.txt" file we can see a list of the cards in the set. Add a new entry at the end of the list for our new card:
```
78 R Circle of Flame @James Paick
79 U Stormblood Berserker @Greg Staples
80 R Dungrove Elder @Scott Chou
81 R Goblin Card Guide @Forge Team
```
The first number is the collector number for the card in the set. This must be unique.
The next field is the card's rarity in the set - we've made our card a rare.
Next is the card's name and finally the name of the artist for the cards artwork.
Speaking of which - our card doesn't have any artwork. Let's fix that.
4. Add Card Image
Open your Card Images folder. Find the code for the set you added your new card to and open the corrosponding subfolder. For us this will be:
> C:/Users/<your username>/Application Data/Local/Forge/Cache/pics/cards/PDCI
Add your card image in the folder. Ther are various online tools to create custom cards. For the purposes of the guide you can use this image:
<img src="https://github.com/user-attachments/assets/55363e68-0232-42e2-a1f7-8971686119e6" width="250"/>
5. Add a Triggered Ability
So we've created a creature card and added it to the game. However our creature is a little... boring, so let's update it to have a triggered ability.
"Whenever Goblin Card Guide deals damage to an opponent, draw a card."
Open the txt file again:
> goblin_card_guide.txt
And update the contents to the following:
```
Name:Goblin Card Guide
ManaCost:1 R
Types:Creature Goblin
PT:2/2
K:Haste
T:Mode$ DamageDone | ValidSource$ Card.Self | ValidTarget$ Opponent | TriggerZones$ Battlefield | Execute$ TrigDraw | TriggerDescription$ Whenever CARDNAME deals damage to an opponent, draw a card.
SVar:TrigDraw:DB$ Draw | Defined$ You | NumCards$ 1
Oracle:Haste
\nWhenever Goblin Card Guide deals damage to an opponent, draw a card.
```
Let's take a look at our changes:
- T:Mode$ DamageDone - Trigger when Damage is Done.
- ValidSource$ Card.Self - The source of the trigger. "Self" means our our card must be the source of the damage for the trigger to occur.
- ValidTarget$ Opponent - The target of the trigger. "Oppenent" means our oppenent must receive damage for the trigger to occur.
- TriggerZones$ Battlefield - Means the card must be on the battlefield for the trigger to occur.
- Execute$ TrigDraw - What happens then the trigger occurs - in this case trigger a Draw effect.
- SVar:TrigDraw:DB$ Draw - A String Variable used by the script. This one takes the Triggered Draw (TrigDraw) and tells the script to draw a card. "DB$ Draw" means "Drawback" "Draw".*
- Defined$ You - Who is drawing the card(s), in this case the controller of the card.
- NumCards$ 1 - Draw 1 card. This could be changed to any number to trigger drawing that many card.
- TriggerDescription$ - The description of the trigger.
*"Drawback" is a connotation that has since been replaced with "SubAbility".
The original connotation differentiated between AB (Ability), SP (Spell) and DB (Drawback).
AP and SP require costs, whereas Drawback do not. The card scripts still use the "DB$" connotation.
6. Updating our Image
Finally since we createda new effect on our card, we need to update the image. Card images are simply .jpg files and don't update or read from any scripts or gamefiles.
I used the same online card creator to make the change to our card:
<img src="https://github.com/user-attachments/assets/9c1df095-7d94-4a1a-a30f-9d0f64f6da38" width="250"/>
Simply save and rename the image to "Goblin Card Guide.fullborder.jpg" then overwrite the previous file we used in:
> C:/Users/<your username>/Application Data/Roaming/Forge/custom/cards/g
7. Next Steps
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:
>./Forge/res/cardsfolder/cardsfolder.zip
Unzipping this file will sllow you to search for any card in the containing subfolders.

View File

@@ -0,0 +1,258 @@
# 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.
**Note:** This tutorial is currently for **Windows only**.
## Where do the files go?
### Non-image files
Everything but your cards images go into `%appdata%/Forge/custom`. You will need to put the files in the correct directories in order for the game to load them correctly.
The important folders are the following (you can create them if they don't exist):
* **cards**: Your card rules (logic) go inside this folder (.txt). I suggest you create subfolders inside it to keep everything clean.
* **editions**: Your editions (sets) definition files (.txt) go inside this folder.
* **tokens**: Your tokens definition files (.txt) go inside this folder.
### Image files
Your card and token images go into `%localappdata%/Forge/Cache/pics`.
The important folders are the following:
* **cards**: This is where you will put your card images. You'll want to create a folder with the Code of your edition. The images are named using this convention `Card Name.full.jpg`. If you have multiple art for the same card (something like basic lands, or maybe alternate art of the same card), then you can name them `Card Name1.full.jpg`, `Card Name2.full.jpg`, and so forth.
* **tokens**: Same as the cards folder, your tokens will go inside this folder. The naming convention is `token_script_name.jpg`. So if your token script name is `b_5_5_golem_trample`, then you can put your token image inside your edition folder named `b_5_5_golem_trample.jpg`. If there is a collector number, append it at the beginning, such as `1_b_5_5_golem_trample.jpg`
## Creating your edition definition file
As mentioned in the opening section, we'll be partially implementing the **MSEM Champions** set. Let's create a new text file (.txt) inside `%appdata%/Forge/custom/editions`. Let's name it `MSEM Champions.txt`.
> Note: The file's name don't matter, but it'll be easier to find it if you ever need to edit anything.
Let's paste the following inside it:
```
[metadata]
Code=MSEM_CHAMPIONS
Name=MSEM Champions
Date=2017-10-01
Type=Custom
[cards]
7 M Master Chef
33 M Golden Touch
34 M Avatar of Basat
35 M Exeunt
62 M Unearth
78 M Fox of the Orange Orchard
78★ S Fox of the Orange Orchard
107 M Inked Summoner
107★ S Inked Summoner
130 M Plains
131 M Island
132 M Swamp
133 M Mountain
134 M Forest
[tokens]
b_1_1_bird_flying
b_3_3_cat_deathtouch
b_5_5_golem_trample
[CreatureTypes]
Artist:Artists
```
Let's break it down.
```
[metadata]
Code=MSEM_CHAMPIONS
Name=MSEM Champions
Date=2017-10-01
Type=Custom
```
The **[metadata]** section contains the information about your edition.
* **Code** is an **unique** identifier for your edition.
* **Name** is the name of your edition.
* **Date** is the date the set was first released/created.
* **Type** should be `Custom` as Forge do things differently for them, including but not limited to skipping the automatic download of the images.
```
[cards]
7 M Master Chef
34 M Avatar of Basat
35 M Exeunt
62 M Unearth
78 M Fox of the Orange Orchard
78★ S Fox of the Orange Orchard
107 M Inked Summoner
107★ S Inked Summoner
130 L Plains
131 L Island
132 L Swamp
133 L Mountain
134 L Forest
```
The **[cards]** section contains the cards of your edition. Any card appearing under this section has a chance to appear as a reward and in shops.
Each line is as follow: `CollectorNumber Rarity CardName @ArtistName`.
* The collector number should be unique in a given set. You can see some numbers have a ★ next to their name. In this case it denotes an alternate art of a card, but it could also have been a different number altogether.
> Note: While it generally doesn't matter what collector number you use, avoid using the collector number F followed by only digits (ie. `F001`) as it represents a "Funny" card inside a normal edition. (Think of Funny cards as Un- sets cards)
* The rarity is a one letter representation. It can be L (Basic Land), C (Common), U (Uncommon), R (Rare), M (Mythic Rare), S (Special).
* The card name is self-explanatory. It should match the name of the corresponding card rule. (More of that later)
* The artist is optional, but it should be `@Artist Name` if present. We'll omit it in this tutorial.
> Note: You can put the cards in the list even if they aren't scripted yet. Forge will skip over them.
```
[tokens]
b_1_1_bird_flying
b_3_3_cat_deathtouch
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.
If you load the game with just file, you'll be able to see that Master Chef, Unearth and the basic lands can already be found in game. That's because they share a name with existing Magic the Gathering cards. **However**, [Master Chef](https://msem-instigator.herokuapp.com/card?q=Master+Chef) from MSEM and [Master Chef](https://scryfall.com/card/clb/241/master-chef) from MTG are two different cards! You must ensure that your custom cards do not have the same name as an existing one, unless you just want it to be another print, just like [Unearth](https://msem-instigator.herokuapp.com/card/CHAMPIONS/62/Unearth) and the basic lands in this example.
Let's comment out Master Chef to avoid a name conflict with an existing MTG card:
```
[cards]
#7 M Master Chef
33 M Golden Touch
...
```
Save your file, and let's move onto another step.
## Scripting your first cards
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.
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.
Let's create the following files:
avatar_of_basat.txt
```
Name:Avatar of Basat
ManaCost:R
Types:Creature Avatar
PT:2/1
S:Mode$ CantBlock | ValidCard$ Card.Self | Description$ CARDNAME can't block.
K:Menace
Oracle:Menace\nAvatar of Basat can't block.
```
exhunt.txt
```
Name:Exeunt
ManaCost:B
Types:Instant
A:SP$ Sacrifice | SacValid$ Creature | Defined$ Player | SpellDescription$ Each player sacrifices a creature.
AI:RemoveDeck:All
Oracle:Each player sacrifices a creature.
```
fox_of_the_orange_orchard.txt
```
Name:Fox of the Orange Orchard
ManaCost:1 W
Types:Creature Fox Spirit
PT:3/1
Oracle:
```
inked_summoner.txt
```
Name:Inked Summoner
ManaCost:1 B
Types:Creature Human Warlock Artist
PT:1/2
T:Mode$ Phase | Phase$ End of Turn | ValidPlayer$ You | TriggerZones$ Battlefield | Execute$ TrigBranch | CheckSVar$ X | SVarCompare$ GE2 | TriggerDescription$ At the beginning of your end step, if you lost 2 or more life this turn, create a 1/1 black Bird creature token with flying. If you lost 4 or more life this turn, instead create a 3/3 black Cat creature token with deathtouch. If you lost 6 or more life this turn, instead create a 5/5 black Golem creature token with trample.
SVar:TrigBranch:DB$ Branch | BranchConditionSVar$ Y | TrueSubAbility$ TrigBranch2 | FalseSubAbility$ DBBird
SVar:TrigBranch2:DB$ Branch | BranchConditionSVar$ Z | TrueSubAbility$ DBGolem | FalseSubAbility$ DBCat
SVar:DBBird:DB$ Token | TokenScript$ b_1_1_bird_flying | TokenAmount$ 1
SVar:DBCat:DB$ Token | TokenScript$ b_3_3_cat_deathtouch | TokenAmount$ 1
SVar:DBGolem:DB$ Token | TokenScript$ b_5_5_golem_trample | TokenAmount$ 1
SVar:X:PlayerCountPropertyYou$LifeLostThisTurn
SVar:Y:Count$Compare X GE4.1.0
SVar:Z:Count$Compare X GE6.1.0
Oracle:At the beginning of your end step, if you lost 2 or more life this turn, create a 1/1 black Bird creature token with flying.\nIf you lost 4 or more life this turn, instead create a 3/3 black Cat creature token with deathtouch.\nIf you lost 6 or more life this turn, instead create a 5/5 black Golem creature token with trample.
```
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.
```
[CreatureTypes]
Artist:Artists
```
Oh no! If you play with Inked Summoner now, you will crash when summoning a token. That's because they don't exist in MTG and we need to define them! Let's go onto the next step!
## 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!
> Just like for card scripting, this tutorial will not teach you about scripting them.
b_1_1_bird.flying.txt
```
Name:Bird Token
ManaCost:no cost
Colors:black
Types:Creature Bird
PT:1/1
K:Flying
Oracle:Flying
```
b_3_3_cat_deathtouch.txt
```
Name:Cat Token
ManaCost:no cost
Colors:black
Types:Creature Cat
PT:3/3
K:Deathtouch
Oracle:Deathtouch
```
b_5_5_golem_trample.txt
```
Name:Golem Token
ManaCost:no cost
Colors:black
Types:Creature Golem
PT:5/5
K:Trample
Oracle:Trample
```
Great! Now Inked Summoner no longer make the game crash! Now let's add some images to spice it all up.
## 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.
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.
![b_1_1_bird_flying](https://github.com/user-attachments/assets/531583c1-3985-4744-858a-3a49fd12740a)
![b_3_3_cat_deathtouch](https://github.com/user-attachments/assets/15a24e62-be43-4c0c-aeac-0ddb38fca97a)
![b_5_5_golem_trample](https://github.com/user-attachments/assets/7ff44dc7-0284-48bd-9233-785ac79106f3)
You can now start your game again, and see that the art loads correctly now.
## 🎉 Congratulations!
Youve 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.

34
docs/Credit-and-Thanks.md Normal file
View File

@@ -0,0 +1,34 @@
## Special Thanks
1. Quest icons created by Teekatas, from his Legendora set - [Link](http://raindropmemory.deviantart.com/)
1. Thanks to the XMage team for permission to use their targeting arrows.
1. Thanks to http://www.freesound.org/browse/ for providing some sound files.
1. Credit to Kevin Macleod for the royalty free music that is used.
1. Credit to Juhani Junkala for the royalty free music for boss battles - [Link](https://opengameart.org/content/boss-battle-music)
## Adventure Mode Sprites and tiles.
Some of the sprites and tiles that are used in Adventure mode come from external artists. These fall under the ownership of their respective artists and may be used only under the conditions laid out in their respective licence.
1. Credit to Reddit user [wampastompah](https://www.reddit.com/user/wampastompah) for the sprites of iconic MTG characters that are used in Adventure mode
1. Credit to [Aleksandr Makarov's](https://itch.io/profile/iknowkingrabbit) pixelart (tiles and sprites) that are used in Adventure mode.
1. Credit to [Elthen](https://linktr.ee/elthen) for his sprites that are used in adventure mode.
1. Credit to [DeepDiveGameStudio](https://deepdivegamestudio.itch.io) for some of the sprites that are used in adventure mode.
1. Credit to [Krishna Palacio](https://itch.io/profile/krishna-palacio) for some of the sprites and tiles that are used in adventure mode.
1. Credit to https://opengameart.org/ for some of the tiles. More in dept creditation of the tiles used can be found [Here](https://docs.google.com/spreadsheets/d/e/2PACX-1vTD0L8yDltZnn4025CZ9exhziCj0rbpd2aKE-0qeHikVBz51OKaAHLsgFuaDuIWrkrqIJicxzdn3SG1/pubhtml)
## Soundtrack
### Adventure
1. Battle = Rising Legion - Derek Fiechter//Assyrian Fortress - Derek Fiechter//Goblin Raid - Brandon Fiechter// As we Marched - Vindsvept // Mecha Dwarves - Derek Fiechter
1. Boss =
1. Inn= Wild Boar's Inn - Brandon Fiechter// Black Rock - Logan Epic Canto //The Land of Gnomes - Logan Epic Canto//Dance of the Red Lights- Logan Epic Canto // Poland - Brandon Fiechter // Tir Nan Og - Logan Epic Canto // Village Consort -Kevin Macleod
1. Dungeon = Swamp Witch - Derek Fiechter // Giant Mushrooms - Derek Fiechter// Cobweb Fairies - Derek Fiechter
1. Menus = Vindsvept - Wanderer // Vindsvept - Lake of Light
1. Waste biome = Vindsvept - Through the Woods we Ran // Vindsvept - Adventure
1. Swamp biome = Merky Swamp - Brandon Fiechter // Masquerade of the Ghosts - Brandon Fiechter
1. Island biome = Pirates of the Coast - Brandon Fiechter & Derek Fiechter//Buried Treasure - Brandon Fiechter & Derek Fiechter
1. Forest biome = Enchanted Forest - Logan Epic Canto // The Celtic King
1. Plains biome = Cobblestone Village - Brandon Fiechter // Camelot - Brandon Fiechter
1. Mountain biome = Goblin Tinker - Brandon Fiechter // Goblin King's Tavern - Brandon Fiechter
1. Castle = Horus - Brandon Fiechter & Derek Fiechter

46
docs/Currency.md Normal file
View File

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

35
docs/Custom-Music.md Normal file
View File

@@ -0,0 +1,35 @@
# Custom music for adventure mode
In the `res/adventure/common/music` folder resides all the music that is played in various parts of adventure mode:
* `black` - Tracks that are played in the black magic biome in the overworld
* `blue` - Tracks that are played in the blue magic biome in the overworld
* `boss` - Tracks that are played in a boss battle
* `castle` - Tracks that are played inside biome boss dungeons
* `cave` - Tracks that are played in any dungeon that is not a biome boss dungeon
* `colorless` - Tracks that are played in the colorless magic (wastes) biome in the overworld
* `green` - Tracks that are played in the green magic biome in the overworld
* `match` - Tracks that are played during a non-boss battle
* `menus` - Tracks that are played in the opening menu of adventure mode
* `red` - Tracks that are played in the red magic biome in the overworld
* `town` - Tracks that are played when you are in any town
* `white` - Tracks that are played in the white magic biome in the overworld
The game will play a track from any of the above folders at random. The tracks can be any
file name with a `.mp3`, `.wav` or `.m4a` extension.
NOTE: On Steam Deck, if you added Forge Adventure mode to Steam Deck gaming mode, the game may
have issues playing tracks that have non-ascii characters in its name. A current
workaround is to rename such tracks with accented characters to their ascii equivalents (eg. `é` to `e`)
and such tracks will play again. Reference: https://github.com/Card-Forge/forge/issues/8290
To customize the music in adventure mode, simply [add to -OR- replace] the existing music track files in the folders referenced above.
# Custom music for regular Forge
In the `res/music` folder resides all the music that is played in regular Forge if you have music enabled.
* `match` - Tracks that are played inside a game
* `menus` - Tracks that are played outside of any game (ie. menus)
All the same playback rules apply. The process for customizing the music here is the same as adventure mode.

View File

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

View File

@@ -0,0 +1,46 @@
In order to build and sign the android release, you will need the `forge.keystore` file (which is not present in the repository). This file will need to be placed in the `forge-gui-android` folder. This file should **never** be committed to the repository.
In preparation for the android release, update the version recorded in the following files:
```
forge-gui-android/pom.xml
forge-gui-ios/pom.xml
forge-gui-mobile/src/forge/Forge.java
```
In the first two, you're looking for `alpha-version` (~line 10 in both files) and setting the string accordingly.
In the last one, you're looking for the declaration of `CURRENT_VERSION` (~line 37) and setting it to the same value as the previous two files.
Commit the changes to these three files to the repository with an appropriately descriptive commit message.
A script such as the following will compile and sign the android build:
```
export ANDROID_HOME=/opt/android-sdk/
export _JAVA_OPTIONS="-Xmx2g"
mvn -U -B clean \\
-P android-release-build,android-release-sign \\
install \\
-Dsign.keystore=forge.keystore \\
-Dsign.alias=Forge \\
-Dsign.storepass=${FORGE_STOREPASS} \\
-Dsign.keypass=${FORGE_KEYPASS}
```
Once the above build has successfully completed and passed any desired testing, the following will build, sign, and publish the build:
```
export ANDROID_HOME=/opt/android-sdk/
export _JAVA_OPTIONS="-Xmx2g"
mvn -U -B clean \\
-P android-release-build,android-release-sign,android-release-upload \\
install \\
-Dsign.keystore=forge.keystore \\
-Dsign.alias=Forge \\
-Dsign.storepass=${FORGE_STOREPASS} \\
-Dsign.keypass=${FORGE_KEYPASS} \\
-Dcardforge.user=${FORGE_FTP_USER} \\
-Dcardforge.pass=${FORGE_FTP_PASS}
```
After this, the appropriate forum posts will need to be made announcing the new release.

View File

@@ -0,0 +1,139 @@
1. Setup IntelliJ with Android (plugin, SDKs etc).
2. Get the Android 13 image for the emulator (important: Android 13), then run the emulator. *Skip this step if you use a physical device.*
3. Make sure you can build the APK.
a. `mvn clean verify -P android-debug` using the working directory forge-gui-android/
4. Use `android-debug` profile for creating the debug version from below in the forge-gui-android/pom.xml
5. Use this maven command: `mvn android:deploy-apk android:run`
6. Look at the emulator (or device), it should say "Waiting for debugger to connect".
7. CRTL+SHIFT+A in IntelliJ and type in `attach android` and click on "Attach Debugger to Android Process".
8. Tick "Show all processes" and you should be able to pick `forge.app` from the list and click "Okay".
9. Ready to debug
Profile XML for debugging:
```
<profile>
<id>android-debug</id>
<properties>
<android.debug>true</android.debug>
<android.run.debug>true</android.run.debug>
<android.deployapk.filename>target\forge-android-${snapshot-version}-aligned-debugSigned.apk</android.deployapk.filename>
<packaging.type>apk</packaging.type>
</properties>
<build>
<plugins>
<plugin>
<artifactId>exec-maven-plugin</artifactId>
<version>3.4.1</version>
<groupId>org.codehaus.mojo</groupId>
<executions>
<execution>
<id>SignV2</id>
<phase>verify</phase>
<goals>
<goal>exec</goal>
</goals>
</execution>
</executions>
<configuration>
<workingDirectory>${pom.basedir}</workingDirectory>
<executable>java</executable>
<arguments>
<argument>-jar</argument>
<argument>${pom.basedir}/tools/uber-apk-signer.jar</argument>
<argument>-a</argument>
<argument>${pom.basedir}/target/</argument>
<argument>--debug</argument>
</arguments>
</configuration>
</plugin>
<plugin>
<groupId>com.github.cardforge.maven.plugins</groupId>
<artifactId>android-maven-plugin</artifactId>
<dependencies>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>2.3.4</version>
</dependency>
<dependency>
<groupId>sun</groupId>
<artifactId>misc</artifactId>
<version>1</version>
<scope>system</scope>
<systemPath>${pom.basedir}/libs/sun-misc.jar</systemPath>
</dependency>
</dependencies>
<version>4.6.2</version>
<extensions>true</extensions>
<configuration>
<sign>
<debug>false</debug>
</sign>
<sdk>
<platform>${androidPlatform}</platform>
<buildTools>${androidBuildTools}</buildTools>
</sdk>
<dexForceJumbo>true</dexForceJumbo>
<assetsDirectory>${project.basedir}/assets</assetsDirectory>
<resourceDirectory>${project.basedir}/res</resourceDirectory>
<nativeLibrariesDirectory>${project.basedir}/libs</nativeLibrariesDirectory>
<extractDuplicates>true</extractDuplicates>
<proguard>
<skip>false</skip>
<config>${project.basedir}/proguard.cfg</config>
</proguard>
<proguardProguardJarPath>${pom.basedir}/tools/proguard.jar</proguardProguardJarPath>
<release>false</release>
<dexCompiler>d8</dexCompiler>
<d8>
<minApi>26</minApi>
<jvmArguments>
<argument>${build.min.memory}</argument>
<argument>${build.max.memory}</argument>
</jvmArguments>
</d8>
<dex>
<multi-dex>true</multi-dex>
<jvmArguments>
<argument>${build.min.memory}</argument>
<argument>${build.max.memory}</argument>
</jvmArguments>
<dexArguments>--min-sdk-version=26</dexArguments>
</dex>
</configuration>
<executions>
<execution>
<id>update-manifest</id>
<goals>
<goal>manifest-merger</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>se.bjurr.gitchangelog</groupId>
<artifactId>git-changelog-maven-plugin</artifactId>
<version>2.2.0</version>
<inherited>false</inherited>
<executions>
<execution>
<id>GenerateGitChangelog</id>
<phase>generate-sources</phase>
<goals>
<goal>git-changelog</goal>
</goals>
<configuration>
<skip>true</skip>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
```

View File

@@ -0,0 +1,8 @@
This is based on Latent Dirichlet allocation:
1) use Agetian's getmtgdecks.sh scripts to download recent decks from mtgdecks.net and convert them to forge format and separate into Standard/Modern/Pioneer using his python tools (deck2forge_3.5.py and deckSorter_1.4a)
2) copy the new decks into forge-gui/res/deckgendecks/$format for each format and remove the $format.lda.dat and $format.raw.dat files from that folder
3) check out the Austin/ldastream branch and merge it with the latest
4) run the forge.deck.generate.LDAModelGenerator class in the forge-gui-desktop module
this will generate updated .raw.dat and .lda.dat files that can then be checked in
probably need to do step 3 before 2 actually

183
docs/Development/DevMode.md Normal file
View File

@@ -0,0 +1,183 @@
Developer Mode is a Mode that allows Developers to try out different
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
during the Retail build. Since Forge is in constant Beta, this Mode is
available during our Beta releases. On the NewGame Screen, just make
sure it's checked and you'll be able to edit the options at any point
during the game.
## Lose By Decking
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
There are a few View Zone menu item which help to make sure your Library
has what you think or the AI isn't being too overly foolish. They do
exactly what you might think. Click on the menu item, and all Cards in
that Zone are revealed to you.
## Generate Mana
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
colorless is generated and put into your Mana Pool.
## Setup Game State
The Setup Game State is a Command that lets you open an external file to
Add things into the Game State. Currently the command uses an external
text file that is opened through a file open dialog when you click on
the Setup Game State menu item. The file structure is simple. It must
contain any of the following lines defining what needs to be changed in
the current game state:
### HumanLife
Defines the value to set the human's current life to. Example:
HumanLife=6
### AILife
Defines the value to set the computer opponent's life to. Example:
AILife=14
### HumanCardsInPlay
Defines the cards that are to be placed under Human's control on the
battlefield. Can be just one card name or a list of card names separated
with semicolons. Example: HumanCardsInPlay=Swamp; Swamp; Drudge
Skeletons
### AICardsInPlay
Defines the cards that are to be placed under Computer's control on the
battlefield. Can be just one card name or a list of card names separated
with semicolons. Example: AICardsInPlay=Island
### HumanCardsInHand
Defines the cards that are to replace the Human's current hand. Old
cards in the Human's card will be removed. Can be just one card name or
a list of card names separated with semicolons, see HumanCardsInPlay or
AICardsInPlay above for examples.
### AICardsInHand
Defines the cards that are to replace the Computer's current hand. Old
cards in the Computer's card will be removed. Can be just one card name
or a list of card names separated with semicolons, see HumanCardsInPlay
or AICardsInPlay above for examples.
### HumanCardsInGraveyard
Defines the cards that are to replace the Human's current graveyard. Old
cards in the Human's graveyard will be removed. Can be just one card
name or a list of card names separated with semicolons, see
HumanCardsInPlay or AICardsInPlay above for examples.
### AICardsInGraveyard
Defines the cards that are to replace the Computer's current graveyard.
Old cards in the Computer's graveyard will be removed. Can be just one
card name or a list of card names separated with semicolons, see
HumanCardsInPlay or AICardsInPlay above for examples.
### HumanCardsInLibrary
Defines the cards that are to replace the Human's current library. Old
cards in the Human's library will be removed. Can be just one card name
or a list of card names separated with semicolons, see HumanCardsInPlay
or AICardsInPlay above for examples.
### AICardsInLibrary
Defines the cards that are to replace the Computer's current library.
Old cards in the Computer's library will be removed. Can be just one
card name or a list of card names separated with semicolons, see
HumanCardsInPlay or AICardsInPlay above for examples.
### HumanCardsInExile
Defines the cards that are to replace the Human's current exile area.
Old cards in the Human's exile area will be removed. Can be just one
card name or a list of card names separated with semicolons, see
HumanCardsInPlay or AICardsInPlay above for examples.
### AICardsInExile
Defines the cards that are to replace the Computer's current exile area.
Old cards in the Computer's exile area will be removed. Can be just one
card name or a list of card names separated with semicolons, see
HumanCardsInPlay or AICardsInPlay above for examples.
### ActivePlayer
Defines the active player that must be given priority when the game
state is set up. If this line is absent, the current player is not
changed. There are two valid values for this option: Human and AI. Note
that this option does not change the current phase, only the current
player. Example: ActivePlayer=AI
### ActivePhase
Changes the current phase of the game to the one specified. The valid
values are Untap, Upkeep, Draw, Main1, Declare Attackers, Declare
Blockers, Main2, End of Turn, and Cleanup. Note that the phase names are
case-sensitive.
### Comments
Game state files are allowed to have comments - any line that is
prefixed with a pound sign (\#) is not parsed and is considered to be a
text fragment. Also, empty lines are ignored. Example: <b>\# This is a
comment.</b>
### Specifying a Set for Spawned Cards
When specifying card names, you can optionally add the set code by
appending the pipe sign (|) and the three-letter set code to the card
name you are spawning. That will allow you to spawn cards from specific
sets. For example, <b>HumanCardsInPlay=Mountain; Mountain</b> will just
spawn two Mountains on the human battlefield, using the latest possible
set for them, while <b>HumanCardsInPlay=Mountain|4ED; Mountain|4ED</b>
will spawn two Mountains from the 4th Edition on the human battlefield.
### Samples
Here are two examples of valid state configuration files:
HumanLife=5
AILife=6
HumanCardsInPlay=Forest; Forest; Forest; Llanowar Elves
AICardsInPlay=Mountain; Mountain; Mountain
HumanCardsInHand=Island; Raging Goblin
AICardsInHand=Swamp; Swamp; Forest
Sets human's life to 5, AI life to 6, puts 3 Forests and Llanowar Elves
on the human battlefield, puts 3 Mountains on the computer battlefield,
replaces the human hand with Island and Raging Goblin, replaces the AI
hand with 2 Swamps and a Forest. Does not add any cards to either
graveyard and does not replace either library.
HumanCardsInPlay=Plains|10E
AICardsInPlay=Mountain; Raging Goblin
AICardsInGraveyard=Force of Nature; Raging Goblin; Amulet of Kroog
ActivePlayer=Human
ActivePhase=Main1
Does not change the human's life and the AI life, puts a Plains from the
10th Edition on the human battlefield, puts a Mountain and a Raging
Goblin on the opponent's battlefield, does not replace the human and the
AI hands and leaves them intact, and adds Force of Nature, Raging
Goblin, and Amulet of Kroog to the computer's graveyard. Does not
replace either library. Sets the current player to Human and the current
phase to Main 1.
It's important to note you won't get achievements in a game once you started cheating.

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

View File

@@ -0,0 +1,141 @@
## Part 1
### Install IntelliJ
IntelliJ comes in two versions:
- **Community Edition**: Free to download and sufficient for this project (it is not a trial version that expires).
- **Ultimate Edition**: Paid version that has support for various frameworks (which we don't need).
You can find versions for Windows, Linux, and macOS on the official website.
No additional plugins are required for this project.
* start IntelliJ
![01-01-initial-prompt](01-01-initial-prompt.png)
* select **Do not import settings**
* click **OK**
![01-02-ui-theme-selection](01-02-ui-theme-selection.png)
* select Theme
* click **Skip Remaining and Set Defaults**
![01-03-checkout-from-git](01-03-checkout-from-git.png)
* select **Check out from Version Control**
* select **Git**
![01-04-fill-in-the-form](01-04-fill-in-the-form.png)
* enter your repository/branch details
* click **Test**
* click **Clone**
* -- wait ---
You will receive a prompt indicating that your checkout from version control contained a project file. In my experience, attempting to open this file fails to do anything useful.
![01-05-do-not-open-it](01-05-do-not-open-it.png)
* click **No**
![01-06-click-open](01-06-click-open.png)
* click **Open**
![01-07-open-the-pom](01-07-open-the-pom.png)
Browse to the folder you specified for the parent and inside the folder you specified for the git checkout. You should see a *pom.xml* file.
* select the *pom.xml* file
* click **OK**
![01-08-open-as-project](01-08-open-as-project.png)
* select **Open as Project**
At this point IntelliJ should be open with the Forge project. However, it will take a while for IntelliJ to download all the project dependencies. This is reflected in the bottom status bar by default.
## Part 2
* select **File**
* select **Project Structure...**
![02-01-project-settings](02-01-project-settings.png)
* select **New**
* select **JDK**
![02-02-new-jdk](02-02-new-jdk.png)
![02-03-jdk-homedirectory](02-03-jdk-homedirectory.png)
If necessary browse to the JDK directory.
* click **OK**
![02-04-after-jdk-setup](02-04-after-jdk-setup.png)
* click **OK**
* select **Run** from the top menu
* select **Debug...** from the drop down
![02-05-edit-configurations](02-05-edit-configurations.png)
* select **Edit Configurations...**
* click the **+** in the upper left
![02-06-application](02-06-application.png)
* select "Application"
![02-07-debug-setup](02-07-debug-setup.png)
* set the **Name** to: Forge
* set the **Main class** to: forge.view.Main
* Latest IntelliJ Versions: click Modify options and check Add VM Options
* set the **VM options** to:
* **(JAVA 17 and above)**
> -Xms768m -XX:+UseParallelGC -Dsun.java2d.xrender=false --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.text=ALL-UNNAMED --add-opens java.desktop/java.awt.font=ALL-UNNAMED --add-opens java.base/jdk.internal.misc=ALL-UNNAMED --add-opens java.base/sun.nio.ch=ALL-UNNAMED --add-opens java.base/java.nio=ALL-UNNAMED --add-opens java.base/java.math=ALL-UNNAMED --add-opens java.base/java.util.concurrent=ALL-UNNAMED --add-opens java.desktop/java.awt=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED --add-opens java.desktop/javax.swing=ALL-UNNAMED --add-opens java.desktop/java.beans=ALL-UNNAMED --add-opens java.desktop/javax.swing.border=ALL-UNNAMED -Dio.netty.tryReflectionSetAccessible=true
* set the **Working directory** to %MODULE_WORKING_DIR%
* set **Use classpath of module** to: forge-gui-desktop
* click **Debug**
* -- wait --
If all goes well, you should eventually see the Forge splash screen followed by the main UI.
### Adventure Mode debugging on Desktop
Follow the same steps to create a Run Configuration, but use forge-gui-mobile-dev instead of forge-gui-desktop as the module and directory.
* select **Run** from the top menu
* select **Debug...** from the drop down
* select **Edit Configurations...**
* click the **+** in the upper left
* select "Application"
* set the **Name** to: Forge
* set the **Main class** to: forge.app.Main
* Latest IntelliJ Versions: click Modify options and check Add VM Options
* set the **VM options** to:
* **(JAVA 17 and above)**
> -Xms768m -XX:+UseParallelGC -Dsun.java2d.xrender=false --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/java.text=ALL-UNNAMED --add-opens java.desktop/java.awt.font=ALL-UNNAMED --add-opens java.base/jdk.internal.misc=ALL-UNNAMED --add-opens java.base/sun.nio.ch=ALL-UNNAMED --add-opens java.base/java.nio=ALL-UNNAMED --add-opens java.base/java.math=ALL-UNNAMED --add-opens java.base/java.util.concurrent=ALL-UNNAMED --add-opens java.desktop/java.awt=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED --add-opens java.desktop/javax.swing=ALL-UNNAMED --add-opens java.desktop/java.beans=ALL-UNNAMED --add-opens java.desktop/javax.swing.border=ALL-UNNAMED -Dio.netty.tryReflectionSetAccessible=true
* set the **Working directory** to %MODULE_WORKING_DIR%
* set **Use classpath of module** to: forge-gui-mobile-dev
* click **Debug**

View File

@@ -0,0 +1,44 @@
# 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**

View File

@@ -0,0 +1,119 @@
Spend as other mana: The easiest way would probably be to intercept mana costs before they are paid and replace them with different mana costs (so it only changes what you're asked to pay).
Effects are:
Celestial Dawn: You may spend white mana as though it were mana of any color. You may spend other mana only as though it were colorless mana.
False Dawn: Until end of turn, you may spend white mana as though it were mana of any color.
Mycosynth Lattice: Players may spend mana as though it were mana of any color.
North Star: For one spell this turn, you may spend mana as though it were mana of any color to pay that spell's mana cost.
Quicksilver Elemental: You may spend blue mana as though it were mana of any color to pay the activation costs of Quicksilver Elemental's abilities.
Sunglasses of Urza: You may spend white mana as though it were red mana.
Generic mana costs can be paid by any mana... so that's not relevant to any of the conversion matrices.
The left side of the matrix is Mana in your ManaPool and the top of the matrix is ManaCosts.
For Colored or Colorless specific costs the tables will look like:
Identity Matrix
| | W | U | B | R | G | C |
| - | - | - | - | - | - | - |
W| X | O | O | O | O | O |
U| O | X | O | O | O | O |
B| O | O | X | O | O | O |
R| O | O | O | X | O | O |
G| O | O | O | O | X | O |
C| O | O | O | O | O | X |
By default, there are no restrictions on what mana can pay for what. So this gives us a restriction MatriX | Of
| | W | U | B | R | G | C |
| - | - | - | - | - | - | - |
W| X | X | X | X | X | X |
U| X | X | X | X | X | X |
B| X | X | X | X | X | X |
R| X | X | X | X | X | X |
G| X | X | X | X | X | X |
C| X | X | X | X | X | X |
Since these are the default matrices, we take too mutable matrices. An additive matrix (that starts out as the identity and ORs any modifiers to itself) and a restriction matrix that starts as completely unrestricted and ANDs any restrictions to itself.
Once all of the modifiers have been assigned you take the additive matrix AND it to the restriction matrix and that's how you determine what Mana you have can pay for a Mana Cost.
Celestial Dawn modifies the table to look like this:
| | W | U | B | R | G | C |
| - | - | - | - | - | - | - |
W| X | X | X | X | X | O |
U| O | O | O | O | O | X |
B| O | O | O | O | O | X |
R| O | O | O | O | O | X |
G| O | O | O | O | O | X |
C| O | O | O | O | O | X |
The False Dawn table looks like this:
| | W | U | B | R | G | C |
| - | - | - | - | - | - | - |
W| X | X | X | X | X | O |
U| O | X | O | O | O | O |
B| O | O | X | O | O | O |
R| O | O | O | X | O | O |
G| O | O | O | O | X | O |
C| O | O | O | O | O | X |
Mycosynth Lattice
| | W | U | B | R | G | C |
| - | - | - | - | - | - | - |
W| X | X | X | X | X | O |
U| X | X | X | X | X | O |
B| X | X | X | X | X | O |
R| X | X | X | X | X | O |
G| X | X | X | X | X | O |
C| X | X | X | X | X | X |
North Star (although specific tO | One spell, so might be better in a different location)
| | W | U | B | R | G | C |
| - | - | - | - | - | - | - |
W| X | X | X | X | X | X |
U| X | X | X | X | X | X |
B| X | X | X | X | X | X |
R| X | X | X | X | X | X |
G| X | X | X | X | X | X |
C| X | X | X | X | X | X |
Quicksilver Elemental, the table is similar to False Dawn:
| | W | U | B | R | G | C |
| - | - | - | - | - | - | - |
W| X | O | O | O | O | O |
U| X | X | X | X | X | X |
B| O | O | X | O | O | O |
R| O | O | O | X | O | O |
G| O | O | O | O | X | O |
C| O | O | O | O | O | X |
Sunglasses of Urza
| | W | U | B | R | G | C |
| - | - | - | - | - | - | - |
W| X | O | O | X | O | O |
U| O | X | O | O | O | O |
B| O | O | X | O | O | O |
R| O | O | O | X | O | O |
G| O | O | O | O | X | O |
C| O | O | O | O | O | X |
RECAP:
1. Start with original matrix (each color/colorless being able to pay for itself)
2. OR all of the positive allowances to the matrix (which is all of them except for the restriction in Celestial Dawn)
3. AND that to the payment restriction (just second half of Celestial Dawn)
4. Apply the payment matrix when trying to pay mana.

View File

@@ -0,0 +1,64 @@
Everyone is free to work on whatever part they're interested in. This is just meant to be a rough overview for new contributors which areas are currently "understaffed" and could use some love.
The "Ancestors" column is basically there to list currently inactive developers that might still be around to provide implementation details on Discord. In that case please use the Search function and make a decent effort to understand the code first, unnecessary pinging for simple things might give you a bad reputation :P
## DevOps
| Concept | Owners | Ancestors | Example tasks |
| - | - | - | - |
| PC Releases | friarsol | Agetian | - update Maven dependencies |
| Android Releases | | kevlahnota | |
| Sentry | JaminCollins | | - watch for rare/unusual crashes |
## Ingame Engine
| Concept | Owners | Ancestors | Example tasks |
| - | - | - | - |
| Ability Effects | friarsol, Hanmac, Northmoc, TRT | | - support new mechanics<br>- debug complicated reports<br>- improve stackdescriptions<br>- research rulings<br>- compare with Arena |
| Adventure | TabletopGeneral | Grimm | |
## Magic Metadata
| Concept | Owners | Ancestors | Example tasks |
| - | - | - | - |
| Deck generation | | Austinio | |
| Netdecks | churrufli | | |
| Editions | Snoops | friarsol | |
| Images | | kevlahnota | |
## User Interface
| Concept | Owners | Ancestors | Example tasks |
| - | - | - | - |
| Desktop | | | |
| Android | | DrDev, kevlahnota | - test new libGDX versions |
| Localization | | Alumi | - update card translation files<br>- update engine text (native speaker not required) |
| Sound effects | | | |
## Forge Script DSL
| 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 |
| ForgeScribe | | Austinino | |
## Modes
| Concept | Owners | Ancestors | Example tasks |
| - | - | - | - |
| Quest | friarsol | | |
| [Network Play](network-play) | | JaminCollins | |
| Gauntlet | | | |
| Draft | | | |
| Planar Conquest | | DrDev | |
| Puzzle | Agetian | | |
| Headless | | | |
| Adventure (Content) | TabletopGeneral, Simisays | Grimm | - balancing |
## Artificial Intelligence
| Concept | Owners | Ancestors | Example tasks |
| - | - | - | - |
| Effect APIs | Hanmac, Agetian | | |
| Combat | Agetian, TRT | | |
| Limited | | | |
| Simulation | | Myrd | |
| DeckHints | | | |
## Miscellaneous
| Concept | Owners | Ancestors | Example tasks |
| - | - | - | - |
| Documentation | | | - update Wiki |

40
docs/Different-Planes.md Normal file
View File

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

97
docs/Docker-Setup.md Normal file
View File

@@ -0,0 +1,97 @@
Directions here to use a docker container to play Forge:
# FORGE-DESKTOP-SNAPSHOT
Pull docker image.
```
- docker pull xanxerdocker/forge-desktop-snapshot
```
Run container and remove container after exit:
```
- docker run --rm -it -p 3002:3000 forge-desktop-snapshot bash
```
Run container in detached mode. Will retain data after exit:
```
- docker run -d -it -p 3002:3000 forge-desktop-snapshot bash
```
Access container at localhost:3002 in your web browser.
Once inside the container. Run the setup script using sudo:
```
- bash setup_forge_desktop.sh
```
After the setup is complete. To play forge-desktop-SNAPSHOT or forge-adventure mode run:
```
- bash forge_game_selector.sh
```
# FORGE DEV ENVIRONMENT:
Dockerized apps for a forge development environment
- intellij-ce
- Magic Set Editor 2 - Advanced
- Tiled - Map Editor
Pull docker images.
```
- docker pull xanxerdocker/intellij-ce-ide
- docker pull xanxerdocker/magic-set-editor-2-advanced
- docker pull xanxerdocker/tiled-map-editor
```
# Intellij-ce IDE container
Run Intellij-ce container and remove container after exit:
```
- docker run --rm -it -p 3003:3000 xanxerdocker/intellij-cd-ide bash
```
Run detached Intellij-ce container. Will retain data after exit")
```
- docker run -d -it -p 3003:3000 xanxerdocker/intellij-ce-ide bash
```
Access container via localhost:3003 in web browser and to start application run:
```
- bash run_intellij-ce.sh
```
# Magic Set Editor 2 Advanced
Run Magic Set Editor 2 - Advanced container and remove container after exit.
```
- docker run --rm -it -p 3004:3000 xanxerdocker/magic-set-editor-2-advanced bash
```
Run detached: Magic Set Editor 2 - Advanced container. Will retain data after exit.
```
- docker run -d -it -p 3004:3000 xanxerdocker/magic-set-editor-2-advanced bash
```
Access container via localhost:3004 in web browser.
Once inside the container to compile the app and finish setup run using sudo:
```
- bash install-mse-adv-full.sh
```
After the app is compiled, to start app run:
```
- bash run_mse.sh
```
# Tiled - Map Editor
Run Tiled - Map Editor container and remove container after exit:
```
- docker run --rm -it -p 3005:3000 xanxerdocker/tiled-map-editor bash
```
Run detached Tiled - Map Editor container. Will retain data after exit.
```
- docker run -d -it -p 3005:3000 xanxerdocker/tiled-map-editor bash
```
Access container via localhost:3005 in web browser and to start application run:
```
- bash run_tiled_map_editor.sh
```

82
docs/Dungeons.md Normal file
View File

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

View File

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

29
docs/File-Formats.md Normal file
View File

@@ -0,0 +1,29 @@
# About
There are many different file formats used in forge, due to the longevity of forge and it's growth and change, older formats are used and new ones are being created. This is an attempt to capture the requirements of these files for general creation, and modification. Unfortunately these "formats" may change and this list, or the data in them may become obsolete as changes happen, especially in newer features like Adventure Mode.
# List
This is a quick hand jammed list of the formats that I have found looking through the /res/ folder. This is not complete, but is a quick reference and a start to identifying the files that can be "customized" or modified to make the engine work for you.
The list is (roughly identified) "format name", and the folder location and filename or just the extension if a specific filename is not required. Where "plane folder" is used, it is a subfolder of that game mode, `Adventure/Shandalar/` for example, or `Conquest/planes/Shandalar/` for conquests, and `quest/world/Shandalar/` for quests.
- Deck Files - .dck
- Card Files - .txt
- Editions - .txt
- Draft - .draft
- Blocks - blocks.txt
- Formats - type/name.txt
- Puzzle - .pzl
- Quest
- Theme - .thm
- Worlds - World folder/worlds.txt
- Adventure - Plane Folder
- Plane Configuration - config.json
- Generated Enemy - .json
- Generated Starter Deck - .json
- Conquest - Plane Folder
- Banned Cards - banned_cards.txt
- Cards - cards.txt
- Plane Cards - plane_cards.txt
- Regions - regions.txt
- Sets - sets.txt
- Events - set/_events.txt

View File

@@ -0,0 +1,115 @@
# An oral/written history of Forge
Here is a roughly written history of some notable points in Forge's history along with some Java 8 playable versions of the game linked for some of the older ones...
## 2007-04-26 - Forge introduced on https://mtgrares.blogspot.com/ by mtgrares.
365 cards available.
`The card set is the best of Magic, and includes such cards like Ancestral Recall, Juzam Djinn, Serendib Efreet, Flametongue Kavu, Man-o'-War, all the moxes, Kokusho, the Evening Star, Keiga, the Tide Star, as well as old favorites like Wrath of God, Psychic Blast, Serra Angel, and a few Planar Chaos cards like Pyrohemia, Serra Sphinx, Damnation.`
All of the blogposts are still available as of this writing (May 2023), but most of the links are defunct since they are pointing to since abandoned locations.
## 2008-07/08 - Oldest known historic archive of Forge.
850 cards available. Original white-gray Java Swing interface. **All** cards live in a cards.txt file. No card "scripting" exists yet.
## 2010-07 - Improved original interface
Still using cards.txt file. Beginnings of the "res" folder for resources to be used inside Forge.
## 2011-09 - Start transitioning from "vintage" Forge to "modern" Forge.
Mostly old-style interface with new age loading screen. Initial card scripting is being used along with the cardsfolder structure. Although the individual cards scripts, contain lots of vestiges of the past. (SetInfo, End tags, Picture tags, even the original scripting format)
## 2012-02 - Desktop interface transitions
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"
## 2014-05 - Android app is published via Maven
## 2015-04 - Network play is rudimentary but available
## 2018-04 - Focus on adding automation tooling
## 2021-12 - Adventure mode functional
## 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**)
Supports over 28,500 cards.
### Some significant events that need to be dated
* Quest mode
* Enable all phases
* AI can cast instants
* Creation of the Ability Factory/Scripting 2.0 system
* Creation of costing system
* Allowance of more than one player
# Legends
## Recounting of "Forge 2.0" by @Agetian
My favorite "Forge legend" is probably the legend about Forge 2.0 (that never came to be, at least thus far). Forge 2.0 was something that was brought up multiple times in Forge history, different targets were set for the hypothetical "future Forge" that would be worthy of increasing the major version number to 2, including, but not limited to, network play support, better UI, better AI (such as Minimax etc.), 100% support for standard cards, and more. Quite amusingly, the absolute majority of those targets have been met since, and even far surpassed, what with the addition of the mobile port, Planar Conquest, Adventure mode, and much more, but Forge is still using the 1.x version scheme it's been using since way back, hehe. Originally, Forge 2.0 was brought up my MtgRares (including a few times on his blog), but the original developer quit the active development before reaching his 2.0 targets, sadly. Thus, at the moment "Forge 2.0" represents a certain hypothetical, future version of Forge that's significantly and unconditionally better than what we currently have, whatever it may be.
The alternative variation of this historical legend is the legend about "Forge 2" - something that was also brought up a couple times in Forge history, mostly coming, unfortunately, from single developers. The hypothetical "Forge 2" was the complete, from-the-ground-up rewrite of Forge that would solve the many poor design choices that are difficult to solve through refactoring, and would thus pave the way for better UI/UX and AI implementations, make the game more streamlined and easier to code, among other things. Sadly, "Forge 2", just as well as "Forge 2.0", hasn't come to be, since the task to recreate all the game mechanics essentially from scratch, mostly as a single developer effort, has proven to be insurmountable.
## Original Adventure Mode
https://www.slightlymagic.net/forum/viewtopic.php?f=52&t=2907
For any of you curious about what the first attempt at making a Shandalar-like Adventure mode in Forge looked like (a development effort from 2010-2011), here are a few screenshots posted by the original developer who left circa 2011 or 2012. The project was abandoned, as far as I know. Pretty sure I had a test version somewhere in my backups (where you could walk around a mostly empty World Map and maybe start a battle or something, I don't remember the details), but I can't find it anymore.
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
## Abe Sergeant writes about Forge
2009-09-11 - Here is the article on star city.
https://articles.starcitygames.com/articles/the-kitchen-table-302-an-mtg-forge-quest/
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/
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.
Quest is the most fun Ive 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 Im 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. Im not holding anything back.
# 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.
## Sourceforge SVN (2007-2008)
Original location. Moved when the name changed.
## Google Code SVN (2008-2011)
Google code was shutting down "soon" so we used it as an excuse to find a new home
## Bitbucket/GIT (2011-2011)
This one lasted barely a few weeks. It was hated pretty universally, even by the people who suggested it.
## Slightly Magic SVN (2011-2017)
The nice folks at Slightly Magic hosted an SVN for us.
## Hosted Gitlab (2017?-2021)
One of the devs was comfortable in Git/Gitlab and moved everything over. Took a few attempts to get there but we made it.
## Github (2021-Present)
Conversion from Git to Git was a lot easier.
## Jendave's modularization (2011)
The initial modularization attempt pulled everything from living under forge-gui/ module and built out some of the Maven structure.
## 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.
## 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.

View File

@@ -0,0 +1,93 @@
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.
### Check the FAQ in Discord
https://discord.com/channels/267367946135928833/1095026912927154176
### Search the help posts in Discord
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
### 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)
* 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
### How do I extract Forge?
* 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.*
Bug reports from users are the lifeblood of Forge. Please keep in mind that "beta" releases *are* test releases. Forge is constantly evolving, so we do not yet have "stable" or "production" releases. Because of the pace at which new cards are added to the multiverse by external forces, this will be the norm for some time. We do not expect everything to work 100%. We have a small number of developers and a handful of slightly less technical people actively improving the game. We simply cannot devote the resources to test every single card, much less the nearly infinite ways the cards can interact.
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
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.
* 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 nothing, please try to reproduce the problem and take notes. If we can use your notes to reproduce the bug for ourselves, it is *much* easier to fix!
* 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?
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
### 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.
A development environment such as [IntelliJ](https://www.jetbrains.com/idea) is beneficial, as it helps writing, compiling and testing your changes.
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).
### 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.
# Gameplay
### 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.
### 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.
### 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.
# Quest Mode
### 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.
### Sealed Deck Mode
[HOW-TO: Customize your Sealed Deck games with fantasy blocks](https://www.slightlymagic.net/forum/viewtopic.php?f=26&t=8164)

35
docs/Gameplay-Guide.md Normal file
View File

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

57
docs/Home.md Normal file
View File

@@ -0,0 +1,57 @@
> [!CAUTION]
> - if you want to contribute to this Wiki please only make pull requests against the main repositories *docs* folder or your changes might get lost
> - due to GitHub limitations all filenames should be unique
# What is Forge?
Forge is a "Rules Engine" for the game Magic: the Gathering.
Forge is not related in any way with Wizards of the Coast.
Forge is open source software released under the GNU Public License.
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!
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) (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 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 is functional, but may still result in game play errors. See the Network Play section of this wiki.
# Adventure Mode
Forge now has an Adventure 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 is baked into the Android/Mobile release of Forge, and as a separate executable already provided in the Desktop release package.
# Download and Install
* Most users please see the [User Guide.](User-Guide)
* For SteamDeck or Bazzite Installation see [Steam Deck Install.](Steam-Deck-and-Bazzite-Install)
# Support
[Basic Troubleshooting](Troubleshooting-FAQ) - Check here first.
[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.
# Forge Developers
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
to their heart's content. He's taken a hands off approach for some time
now, but still talks about it on his blog linked above.
At this time, there are many developers who help in different stages of
the game. Some of us work on the infrastructure of the program, others
work on creating new abilities and cards that use them, but there is a
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
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)
> <span style="color: red;">Note:</span> if you're reading this locally here's the [Table of Contents](_sidebar.md)

View File

@@ -0,0 +1,48 @@
Setting up your cube in Forge is relatively straightforward. Forge needs to know two things: the contents of your cube and the contents of a cube pack
1) Define the contents of your cube
For this you will need to add your cube list to the 'cube' folder, which can be found under the 'res' folder in your Forge installation directory.
Cube lists are defined in .dck files. You can either create a new one with a text editor, or copy an existing one and change its file name and contents. Regardless of your approach, you should end up with a file that roughly looks like this:
```
[metadata]
Name=Two Card Cube
[Main]
1 Abbot of Keral Keep|ORI
1 Abrade|HOU
```
**Don't change the first and third line**. On the second line you can put in the name of your cube after 'Name=', in this example the name of our cube is Two Card Cube. Note this name, as you will later need to refer to it.
Starting on the fourth line, you can list each card in your cube. Always start with the number of times a given card is included in your cube (important for those breaking singleton), then the name of the card, then a pipe (the '|' character), and finally the three letter set code.
Tip: An easy way to find the three letter code for a card is to look up that card on www.scryfall.com and search up the correct version of the card. The set code will appear at the top of the prints overview behind the full set name.
Note that this example cube contains just two cards. Adding more is simply a matter of following the same pattern and adding a line for each new cards.
Once you're done with your file, save it in the 'cube' folder. Use the value of the Name field as the filename and give it the .dck file extension. In this example, we would save the file as 'Two Card Cube.dck'.
2) Define the contents of your boosters
The next step is to define the draft file that instructs Forge how your boosters look. For this you will need to create a .draft file in the 'draft' folder, which can again be found under the 'res' folder in your Forge installation directory.
Note that the options I will describe here are somewhat limited. Unfortunately seeding boosters is not easily achieved. Let's again look at an example:
```
Name:The Two Card Cube
DeckFile:Two Card Cube
Singleton:True
Booster: 15 Any
NumPacks:3
```
The first line contains the name of the cube as it will be presented in Forge.
The second line contains should match the name field of the .dck file you just created.
The third line defines whether this is a singleton draft format or a regular set format. Note that for cubes breaking singleton, you still want to set this value to True, since you define which cards you break singleton on in the .dck file.
The fifth line defines how many cards appear in a booster. You can change the number, but do not touch the 'Any', that part instructs Forge to pull cards from the entirety of your cube list.
Finally, the sixth line defines the number of packs used when drafting your cube.
Once you're done with this file, save it in the 'draft' folder. You can again use the value in the name field, this time in conjunction with the file extension 'draft'. In this example, we would save the file as 'The Two Card Cube.draft'.
3) Backup both files
The next step is to backup both your .dck file and your .draft file. You'll be happy you did when Forge needs to be reinstalled from scratch for some reason.
4) Draft!
You've done it! Fire up Forge, and select Booster Draft in the left side menu (it's one of the options under Sanctioned Formats). Click the New Booster Draft Game button, select the option Custom Cube, click OK, choose the cube you just created (remember, the name presented here is the value for the field Name you entered in the .draft file), and click OK again. That's it, you're in. Happy drafting!

View File

@@ -0,0 +1,11 @@
# 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)

13
docs/Mana-Shards.md Normal file
View File

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

View File

@@ -0,0 +1,77 @@
## Missing tournament legal cards
Total number of unique cards in Forge (as of the release of Final Fantasy): 30,331
**For the latest updates during spoiler season** check out the "Projects" tab on Github for information which cards have been added. It takes some time to implement cards, and WotC doesn't send us spoilers ahead of time, so we barely have any time to implement new cards. Please be patient, and don't ask when cards will be ready.
| Format | Total Cards | Missing Cards | % Completed |
| --------------- | --------------- | --------------- | --------------- |
| Standard | 4,088 | 0 | 100 % |
| Modern | 20,584 | 0 | 100 % |
| Historic (including Alchemy cards) | 12,921 | 0 | 100 % |
| Vintage/Legacy/Commander (including banned & restricted cards)| 29,185 | 46 | 99.8 % |
### Stickers [46 Cards]
Introduces an entirely new type of game piece to track, which comes in four different varieties - name, ability, art, and stats. They were not widely used and are currently banned in all official formats except commander.
1. [Baaallerina](https://scryfall.com/card/unf/35/baaallerina)
1. [Bioluminary](https://scryfall.com/card/unf/38/bioluminary)
1. [Croakid Amphibonaut](https://scryfall.com/card/unf/43/croakid-amphibonaut)
1. [Glitterflitter](https://scryfall.com/card/unf/48/glitterflitter)
1. [Make a _____ Splash](https://scryfall.com/card/unf/50/make-a-_____-splash)
1. [Prize Wall](https://scryfall.com/card/unf/57/prize-wall)
1. [_____ _____ _____ Trespasser](https://scryfall.com/card/unf/61/_____-_____-_____-trespasser)
1. [Unlawful Entry](https://scryfall.com/card/unf/62/unlawful-entry)
1. [Wizards of the _____](https://scryfall.com/card/unf/64/wizards-of-the-_____)
1. [Carnival Carnivore](https://scryfall.com/card/unf/68/carnival-carnivore)
1. [Last Voyage of the _____](https://scryfall.com/card/unf/78/last-voyage-of-the-_____)
1. [Scampire](https://scryfall.com/card/unf/89/scampire)
1. [Scared Stiff](https://scryfall.com/card/unf/90/scared-stiff)
1. [Wolf in _____ Clothing](https://scryfall.com/card/unf/95/wolf-in-_____-clothing)
1. [_____ Balls of Fire](https://scryfall.com/card/unf/100/_____-balls-of-fire)
1. [Big Winner](https://scryfall.com/card/unf/101/big-winner)
1. [_____ Goblin](https://scryfall.com/card/unf/107/_____-goblin)
1. [Goblin Airbrusher](https://scryfall.com/card/unf/108/goblin-airbrusher)
1. [Minotaur de Force](https://scryfall.com/card/unf/114/minotaur-de-force)
1. [_____ _____ Rocketship](https://scryfall.com/card/unf/191/_____-_____-rocketship)
1. [Ticketomaton](https://scryfall.com/card/unf/195/ticketomaton)
1. [Wicker Picker](https://scryfall.com/card/unf/196/wicker-picker)
1. [Tusk and Whiskers](https://scryfall.com/card/unf/182/tusk-and-whiskers)
1. [Aerialephant](https://scryfall.com/card/unf/2/aerialephant)
1. [Robo-Piñata](https://scryfall.com/card/unf/25/robo-pi%C3%B1ata)
1. [Sanguine Sipper](https://scryfall.com/card/unf/26/sanguine-sipper)
1. [_____ Bird Gets the Worm](https://scryfall.com/card/unf/5/_____-bird-gets-the-worm)
1. [Park Bleater](https://scryfall.com/card/unf/21/park-bleater)
1. [Pin Collection](https://scryfall.com/card/unf/23/pin-collection)
1. [Sword-Swallowing Seraph](https://scryfall.com/card/unf/30/sword-swallowing-seraph)
1. [Proficient Pyrodancer](https://scryfall.com/card/unf/120/proficient-pyrodancer)
1. [Wee Champion](https://scryfall.com/card/unf/127/wee-champion)
1. [Chicken Troupe](https://scryfall.com/card/unf/133/chicken-troupe)
1. [Clandestine Chameleon](https://scryfall.com/card/unf/134/clandestine-chameleon)
1. [Done for the Day](https://scryfall.com/card/unf/136/done-for-the-day)
1. [Fight the _____ Fight](https://scryfall.com/card/unf/138/fight-the-_____-fight)
1. [Finishing Move](https://scryfall.com/card/unf/139/finishing-move)
1. [Roxi, Publicist to the Stars](https://scryfall.com/card/unf/177/roxi-publicist-to-the-stars)
1. [Grabby Tabby](https://scryfall.com/card/unf/140/grabby-tabby)
1. [Lineprancers](https://scryfall.com/card/unf/146/lineprancers)
1. [_____-o-saurus](https://scryfall.com/card/unf/148/_____-o-saurus)
1. [Stiltstrider](https://scryfall.com/card/unf/157/stiltstrider)
1. [Costume Shop](https://scryfall.com/card/unf/206a/costume-shop)
1. [Ambassador Blorpityblorpboop](https://scryfall.com/card/unf/161/ambassador-blorpityblorpboop)
1. [A Good Day to Pie](https://scryfall.com/card/unf/12/a-good-day-to-pie)
1. [Command Performance](https://scryfall.com/card/unf/42/command-performance)
## Missing variant cards
### [Bounty](https://mtg.fandom.com/wiki/Outlaws_of_Thunder_Junction/Commander_decks#Bounty_cards) (Casual Variant)
The release of the Outlaws of Thunder Junction Commander Decks included a set of twelve "Bounty" cards and rules for a variant format that uses them. This format is not defined in the comprehensive rules, and is not currently implemented in Forge.
### [Hero's Path](https://mtg.fandom.com/wiki/Hero's_Path) (Promotional Variant)
During the Theros Block, a series of promotional events was held. These featured three PvE encounters with unique rules, along with nontraditional cards for use by both the player and enemies in these encounters. The PvE encounters could be reconstructed in Forge, but supporting the Heroes and Godslayer weapons would add many complications.
## Missing nonlegal and funny cards
Complete support for Un-cards and other non-legal cards is a non-goal of Forge, but individual cards may be implemented when they work within the scope of the project. For a complete list, see [Un-cards, Playtest Cards, and Other Funny Cards](https://github.com/Card-Forge/forge/wiki/Un%E2%80%90cards,-Playtest-Cards,-and-Other-Funny-Cards).

View File

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

34
docs/Network-FAQ.md Normal file
View File

@@ -0,0 +1,34 @@
# Frequently Asked Questions About Networking Games
### Can I play you?
* No. Thanks for the offer, but right now I don't have time. And the truth is I'm just not that good at magic really, the AI is a better opponent than me.
### Can Mac Players play against Windows?
* Yes! Forge is completely cross platform, as it's built in java the same software is running on every system, this allows cross platform compatibility; Android to Windows to MacOS/OSX... potentially anything that can run Java.
Note: Forge is not built for iOS, as iOS doesn't support or run java.
### Can I play random players?
* No. Network play is designed, currently, to play against someone you know. If you want to play against random people, you can make a request in the discord.
### Why can't my friend connect?
* There's three major reasons
1. You're not on the same network and you're trying to use the local IP address. Please read [this part of the guide](network-play#remote-network).
1. You're on the same network and your local firewall isn't configured correctly. Please read [this part of the guide](network-play#host-based-firewall).
1. You're not on the same network and your router ports aren't forwarded. Please read [this part of the guide](network-play#port-forwarding-or-nat).
* IF you're not on the same network and you can't access port forwards, you'll need this [Network Extra](Networking-Extras).
### Is it possible to play with people not on the same Wi-Fi?
* Yes. There's two primary ways to handle this:
1. On a home internet: Open and forward ports on your home router to your computer hosting the game.
* Additional information is here: [Remote Networking](network-play#remote-network)
2. On public Wi-Fi hotspots or cell networks: Use a VPN to create a private network.
* Additional information is here: [Network Extra](Networking-Extras)
* This may be more specific to getting you going: [VPN Providers](Networking-Extras#virtual-private-network-service-providers)

216
docs/Network-Play.md Normal file
View File

@@ -0,0 +1,216 @@
### Disclaimer:
While multiplayer over a network *does* currently work, and complete games *have* been played, it is still very much a "*work in progress*". This means;
* You **will** find bugs. When they appear;
* They will likely be mid-game.
* They will likely make it impossible to complete the current game/match.
* You **will** need to restart both the client's and host's games.
**You've been warned!** But please report bugs and issues, this will help make network play more stable.
***
### Support:
**We have a discord channel dedicated for network play related matters: https://discord.gg/nsAhGwD**
I've created a Networking FAQ, check the sidebar for it. I'll update it as I get **Q's** in discord that are **FA**.
Note: This guide has been written to be as comprehensive as possible; from a quick start, to troubleshooting, to network configuration and concepts. It's not possible to identify all network configurations or issues, but the typical and most basic are represented in this guide. I personally want network play to be easy to use, set up, and for it to be stable. The more people playing over network the more its play tested, and more bugs can be found.
***
### Requirements:
* **At least two devices**, all devices must be running the same version of Forge.
* Host: The device running forge as the "server."
* Client: The device connecting to the Forge server.
* **A Network**;
* Local: Wi-Fi in the same home, "Wi-Fi Direct" between two devices, or Ethernet for PC
* Remote: IPv4 Internet (Home Wi-Fi Router to the internet, Ethernet to Router to internet)
* **Firewall Exclusions and Port Forwards**;
* NOTE: Don't DISABLE your firewall, use exclusions to allow forge app or the port specifically.
* By default Forge (when running as a server) will listen on port **36743**.
* For clients to be able to connect to the server, this port will need to be allowed through the server host's local firewall (Windows Defender Firewall, Ubuntu/Linux UFW, etc.).
* Additionally, if the client is "remote" or outside your local network, the above port will need to be forwarded through the *host's router's firewall* (see below) **and** through the *host's local firewall*.
***
### Quick Start:
* Start Forge on both devices; please confirm versions numbers are the same before continuing.
* Mobile players: Choose "Classic Mode"
* Go to
* Mobile: "Play Online"
* Desktop: "Online Multiplayer" > "Lobby" > Click "Connect to Server"
* Decide who will "host" the game.
* That person must **not** put anything in the "Connect to Server" popup box, and click OK.
* Forge will attempt to show the IP Address of the Host's machine verify before providing to the other player:
* **For local play,** the IP address would probably start with 192.168... verify with your device's network settings. ```Forge may recommend to use the word "localhost", ignore this. I suspect this is a java issue with naming the network address.```
* **For remote play,** the IP address should be verified with https://canyouseeme.org/
* The person **not** hosting is a "client" and MUST to put the IP Address determined from the above step into the "Connect to Server" popup.
* You can then start a network match;
* The host can select the type of match, team configuration, number of matches per game, and other settings.
* All Players can select their decks, their sleeves (if mobile), and avatar.
* Toggle the ready switch to signal you're ready.
* When all players are ready to start, the host can start the match.
***
### Troubleshooting:
Folks in discord are there to help you get your game going!
#### Current Network-based Multiplayer Known Issues
* Some points where the game is waiting on an opponent's decision/action do not properly indicate this. #158
* On mobile, if you are in the same room without Wi-Fi access, try "personal hotspot"/"WiFi-Direct" options first, you don't need a port forward that way.
* If you are on the same Private Wi-Fi, you do not need to do this.
* If you are on a Public Wi-Fi or Hotspot, you will probably not be able to communicate, using a "personal hotspot" would be a better option.
* Due to the lack of traffic optimizations, playing over network feels choppy/laggy. A single game can transfer hundreds of megabytes between all clients! _Slow networks will be slow._
* If your ISP uses IPv6 but uses something called Dual Stack lite, network play won't work. Network play **requires IPv4 addressing** or IPv6 using (full) Dual Stack!
#### "Disconnected From Lobby"
* A common cause for this is that the client and server resource (**res**) folder content differs. This can be verified by checking the game log and looking for an IOException referring to a "Card ... not found". #175
***
## Network Configuration - Setup and Testing
_Please consider reading "Hosting a Server" section for the concepts behind the setup and testing, and terminology._
### Local Network:
#### **Host Based Firewall**
* Mobile Device: There shouldn't be much you need to do, start Forge, host a server. Anyone else on the same local network should be able to connect.
* Desktop Device: Depending on your OS, you may need to allow Forge through the host based firewall. Windows Defender Firewall, Ubuntu would have UFW.
* Either allow the app itself.
* or the default port of 36743.
#### **Validate**
* Android Device: You can test from your mobile device with PortDroid, to scan if the port is open:
* Setup
* Install [**PortDroid**](https://play.google.com/store/apps/details?id=com.stealthcopter.portdroid)
* Open **PortDroid**
* From the menu in the upper left, select *Port Scanner*
* Select the three dot menu in the upper right.
* Select *Port Scanner Settings*
* At the bottom under *Port Lists*, select *Add New Scan*
* Enter *Forge* for the *Scan Title*
* Enter *36743* for the ports to scan.
* Select *ADD SCAN*
* Test
* From the menu in the upper left, select *Port Scanner*
* In the first field on the left, enter the internal IP of the host system.
* From the middle drop down, select *Forge*
* Select *SCAN*
* If all goes well, you'll receive an indication that 1 port is open.
* Windows Device: You can run telnet to test if the port is open.
* Setup
* Install Telnet: As telnet is no longer available in Windows by default, [please follow the instructions found here](https://social.technet.microsoft.com/wiki/contents/articles/38433.windows-10-enabling-telnet-client.aspx) before continuing.
* Test
* Open a **command prompt**
* Type: *telnet INTERNALIP 36743* (where INTERNALIP is your forge host's internal IP address)
* Press *Enter*
* If all goes well, your command prompt title bar will change to *Telnet* followed by the host system's IP address. At this point you can close the window.
* If it doesn't you'll see something like the following: ```Could not open connection to the host, on port 36743: Connect failed```
* Mac OSX Device:
* I'll quote Apple "Telnet is insecure and you should move to more secure communications methods, like HTTPS."
* You can't be trusted to use this on your own.
* Maybe check the app store for a "port scanner" follow their instructions for scanning for port "36743" on "localhost".
* Linux Device: You probably don't need our help. Install telnet from your package distro and run telnet similar to Windows.
If you've passed the validation, then your configuration should be good and you can provide your partner the IP address of your device. Please confirm your IP address with your devices' network configuration settings.
### Remote Network:
A Remote Network setup **MUST** pass the local network setup above.
#### **Port Forwarding or "NAT"**
_Each router is different, so instead of specific step by step instructions here, this is general theory._
* Setup
* You'll need to find the admin interface for your router. On most consumer models, this is a web page that can be accessed at `http://_._._.1` where the first three parts of the address are the same as your system's internal IP address (as reported by Forge). For example, if your internal IP address is 192.168.0.54, the admin page would likely be at: `http://192.168.0.1/`.
* Once inside the interface, you'll need to look for "port forwarding". This is sometimes buried under different menu options, like "advanced". Once located, you'll want to add a new port forward. The interface will likely ask for several bits of information:
* remote or external IP
* external port
* internal host or IP
* internal port
* Both port values should be set to **36743**.
* The remote or external IP should be left **blank** or set to **any**.
* The internal host or IP would be the machine running Forge.
Help for your specific router may be here: https://portforward.com/router.htm
*Don't download software you don't know what it does, that website's software is NOT recommended to be used, just use the guides they provide.*
* Testing
* For the external testing use a site like: [CanYouSeeMe.org](http://canyouseeme.org)
* Your external IP should already be detected by the site.
* Simply enter **36743** for the port to check and click *Check Port*.
* If all goes well, you'll get a *Success* message back.
If you've passed both of the local and remote testing, then your configuration should be good and you can provide your partner the IP address of your network. Please confirm the IP address with [CanYouSeeMe.org](http://canyouseeme.org).
***
## Network Configuration - Hosting a Server
Welcome to hosting a server, this is a more descriptive view as to what's going on when you host a server. These concepts can be applied to most server set ups, but we will focus on Forge in three different configurations:
* **Local Networks (Over your local network)**
* Local Private Wi-Fi, "Wi-Fi Direct", or "Hotspot" from a mobile device, also wired networks for PC players.
* **Remote Networks (Over the Internet)**
* Networks you don't control. (Cell to Cell, or Public Hotspots.)
* Network you do control. (Home internet.)
#### Local Private Network:
A local network's IP addresses are typically self managed by your router, with what is called the DHCP server. These addresses are assigned to your devices automatically and are what you would use to connect to each other device. These devices will talk to each other over the Wi-Fi Access Point (which is typically your router.) In a wired environment they will talk across the wire through a switch (also typically your router.)
When you do a "Wi-Fi direct" in Android, this allows one device to be the Wi-Fi access point, other devices can connect to this network as a local private network.
#### Remote Network:
Remote networks are two networks separated by the internet, or by what is called VLANs. On public hotspots, two devices on that "same Wi-Fi" will be separated by these VLANs, and can not talk to each other directly. Similarly, two cell phones on the same provider will be separated by VLANs, or just by given an IPv6 address.
If you do not control your internet connection, like a public hotspot or a cell provided internet you'll need to do a software defined network (SDN), or a virtual private network (VPN) which is outside the scope of this wiki and any discord help. It is recommended to connect via "Wi-Fi Direct" if you are near each other instead.
If you control your internet connection's network, such as it's your home network and can access the router configuration, you can host a game by opening the ports on your network, and allowing the game through the router's firewall. This allows the remote player, on practically any other internet connected network to connect through your router to your server.
### Firewalls:
Firewalls are kind of archaic, but are still implemented, and are designed to block your connection intentionally.
A quick history; Computer OS's used to respond with a port being "closed" if no service was available on it, and "open" if a connection was able to be opened. An OS should not report a port as "closed", just ignore the request. (Reporting a port as "closed" is like hearing a knock on the door, opening the door and slamming it shut.) This wasn't a security concern at the time, so attackers were able to identify if a computer was "alive" if it responded with a "closed" port, and would moved on if it didn't get a response at all. Any response from a computer triggered a deeper attack to find which ports were actually "open." Thus firewalls were born...
Firewalls were initially implemented to prevent open ports from talking to just any random device, limiting access to only approved connection requests and limiting attackers from gaining access, however ports were still reporting as "closed" in the early days. Firewalls then implemented a function to prevent ports without services on them from reporting "closed" at all and just ignored the traffic instead, making it appear as there was no computer at that address.
In devices with firewalls you need to allow a port for a service through the Host Based Firewall. You'll also need to do this on your router for remote games as well for the Network Based Firewall. I believe, most Linux devices (including Android) no longer report "closed" for ports without services, and firewalls are typically not needed as the only ports that should be open are ones with services needing ports open. In Windows this is not the case, and is why Windows Defender Firewall is implemented and ports will need to be opened to allow Forge to accept a connection. You should however be able to connect to a mobile device without concerns of a firewall blocking you.
### Port Forwarding:
In routers, just after the firewall is a routing function called Network Address Translation. "NAT" allows a computer behind the router (from the internet perspective) to provide its service as if it is the router itself. In most routers, when you enable a NAT/Port Forward, it will also allow the Port through the Firewall.
```[Device](Internet) ---> 36743{Router} ---> 36743[Host]```
Once this initial communication is started, the two devices will talk over their designated and determined ports and have a TCP "conversation" (called a stream) until the connection is completed or closed. Once the TCP stream is closed, another device can connect.
If you've used PlayStation systems you might recognize the term "NAT"; level 1, 2, or 3. This naming is not part of the standard, and is bad and confusing; (I believe) "Level 1" meant no NAT'ing all traffic was redirected through PlayStation servers, "Level 2" meant Half Open or Static NAT (which is what we will do for Forge), and "Level 3" mean the use of UPnP/DLNA which was automatic NAT services and allows ALL software to control your firewall for you. (UPnP should be concerning from a security perspective.)
### Network Topology:
Here's how your typical network topology and network flow will look, when connecting and playing with Forge.
* Typical Wi-Fi Home Network with 2 devices
* A Figure of Local Game Connection through Wi-Fi with PC Host.
![image](https://user-images.githubusercontent.com/1243145/186308954-930e2db5-e428-401f-8117-d223141a5037.png)
* Mobile/Android would be similar, but without the firewall on the host. (Personally, I believe this may change because of how stupid android "security" is becoming, and you'll have a firewall you can't tune, or make exceptions.)
* Typical Remote Network Connection with 2 devices
* A Figure of Remote Game Connection through Firewall and Port Forward on Router.
![image](https://user-images.githubusercontent.com/1243145/186335351-c2c670bd-599e-497a-a210-ab866d7ebd95.png)

83
docs/Networking-Extras.md Normal file
View File

@@ -0,0 +1,83 @@
# Disclaimer
This page should be considered "experimental" and **at your own risk**.
Forge, it's developers, and supporters won't be able to help you with this. I'll try to provide some details about setting up the software and networks listed here, but please direct any support to the platform or provider's support and documentation. This should mostly be just a list of options of how to set up a remote game without access to port forwards on your network.
_Please don't use this guide to get around security of your work/school network on a work/school computer, only use it for accessible networks like hotspots or the like, with your personal devices._
_Attempting to subvert security measures may be considered a crime, in the least you'll get fired or expelled, I like you I don't want that on you._
# Networks
So you found out you don't have control over your network to open ports for a remote game connection, what can you do? **Well, hopefully this guide will get you on the right track.**
Some scenarios where this may be;
+ you're on a cell data connection
+ you're on a public Wi-Fi Hotspot
+ you're at school or work
+ dad is kind of a tightwad and doesn't want games to have open ports on the router.
+ sorry, my dad was actually cool about this, and let me manage the network router.
These connections don't allow you to forward ports to your device's IP address, and that's a problem with Forge for remote connections. You'll need to be able to communicate with a server or service outside of the network first to set up a line of communication, which then allows you to make direct connections to another device. The most common concept is a Virtual Private Network or VPN, allowing a virtual network to be instantiated, and connected to. Additionally newer concepts such as Software Defined Networks can provide similar functions, without the layers of security/encryption a VPN would.
## Virtual Private Network Software
VPNs were not designed to subvert security, but to provide a secure communication path on less secure networks. Allowing enterprise or work computers the ability to remotely connect to the work network.
Today, most VPN service providers provide a secure channel to the internet for "completely legitimate" reasons. However, going back to the original intent you can stand up a virtual private network that you and friends can access using VPN Software.
**Don't confuse a VPN Provider (PIA, Nord, Etc.) as a VPN Software or Service you run (OpenVPN).**
### Software:
If you know of other VPN software (NOT SERVICE PROVIDERS), please expand this list.
+ OpenVPN
- VPN Software you can run on a router or a server.
+ IPSec Tunnel (?)
- A part of VPN that you typically run on two routers?
### Notes:
VPN Software will still require control of **a network**, a place to install the VPN, and have remote access to it. So this is where complexity is introduced, you'll either need to set up the VPN on a home network you can control, or on a server that you can rent from a provider with a public IP address you can access. Once this is done, both players can install the VPN software on their devices, and connect to that server.
Once you've connected to a VPN Server, you can return to the Network Play guide and test your connections as a "Local Private Network".
## Virtual Private Network Service Providers
Some VPN Service Providers provide an actual VPN service, and not just a pipe for "completely legitimate internet access" purposes. Since these are rarer please only put these types of VPN providers here, **everyone can google** the others.
### Providers:
+ [RadminVPN](radmin-vpn.com)
- A free VPN provider for actual Private Networking Access.
+ [Hamachi by LogMeIn](https://www.vpn.net/)
- Free for up to 5 computers.
### Note:
Some "completely legitimate internet access" VPN Service providers may allow you to have port forwards through their networks. Check with the provider before signing up, however both players might need the same provider but might not. You can search google for those VPN providers, **we don't need to list them here.**
## Software Defined Networks
A software defined network is similar to a VPN, but without the encryption and stuff that can slow and prevent a connection out. A Software Defined Network by a provider means you don't need to have your own server, they provide the remote access. Therefore you only need to configure the basic network settings like the IP Schema, and then allow access.
Once you've configured the "virtual switch", users can connect with the provider's software, you can accept their access, then assign an IP address, and connect to each other.
### Providers:
Please expand this list if you know of others!
+ [ZeroTier One](https://www.zerotier.com/download/) - It's free for personal use.
### Notes:
Configuring a network can get complex, however a simple network should already be configured by the provider. You should be able to follow the instructions from the provider to stand up your first network, provide your friend the link to the software, the access code to the network, and accept their access request.
Once you've done this, you can return to the Network Play guide and test your connections as a "Local Private Network".
## Security
As a general note, if you use a provider for network traffic including a VPN provider; They have access to **everything** you do on their network! They may not see the data if you use HTTPS for encryption, but they can see where you go, and can collect that meta data.
Secondly, being on a private network (VPN or SDN) with anyone else does put your device at risk, especially if your device has security vulnerabilities that aren't patched. It is recommended to only provide access to people you TRUST on these private networks you stand up, and disconnect when you're not using them. **Don't create a network and provide access to hundreds of people and stay connected, you're ONLY asking for trouble.**

View File

@@ -0,0 +1,23 @@
**[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.

View File

@@ -0,0 +1,125 @@
**[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)

View File

@@ -0,0 +1,73 @@
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

5
docs/Retired/images.md Normal file
View File

@@ -0,0 +1,5 @@
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

View File

@@ -0,0 +1,46 @@
## Snapshots
Currently (as of Dec 2023), Desktop and Android snapshots have been automated via GithubActions.
You can see the workflows here.
[![Create Desktop/Android Snapshot](https://github.com/Card-Forge/forge/actions/workflows/snapshot-both-pc-android.yml/badge.svg)](https://github.com/Card-Forge/forge/actions/workflows/snapshot-both-pc-android.yml)
These run daily at around 2pm ET time for people looking for them. The upload scripts should take care of removing the previous files uploaded on the last run. If necessary, you can run these scripts manually. Occasionally these builds fail to upload due to unclear reasons. Retrying the snapshot usually seems to fix the issue. (As of 2/23/2025, the tag is tagging an old SHA. We'll try to resolve that, but know that the snapshot file time represents which code is utilized.)
The latest daily snapshots can be found [here](https://github.com/Card-Forge/forge/releases/tag/daily-snapshots)
## Releases
Currently (as of Dec 2023), Desktop releases run an automated script with a few manual pieces.
Currently (as of Dec 4, 2023), Android releases are not run. Although, hopefully with the Android snapshot now automated we'll be able to get back to getting a full release for Android as well.
### Desktop Release Process
It would be useful for other people to be comfortable with the release process. Currently, GitHub has the secrets required to add a snapshot or full release, so it's mostly just running through the steps below to kick off and validate a release.
1. Run Build
* Verify test suite is passing on the repo
[![Test build](https://github.com/Card-Forge/forge/actions/workflows/test-build.yaml/badge.svg?branch=master)](https://github.com/Card-Forge/forge/actions/workflows/test-build.yaml)
* Create a new branch from the default branch
* Run the https://github.com/Card-Forge/forge/actions/workflows/maven-publish.yml workflow on the new branch
2. Verify
* After it completes, download the finished package. Extract it and do a quick verification that it works.
3. For snapshot updates, a few files currently need manual updates. Check out this PR for an example: https://github.com/Card-Forge/forge/pull/4293/files
(Hopefully in the next few iterations, we'll have this automated during the release)
* forge-gui-desktop/pom.xml
* fromRef
* forge-gui-android/AndroidManifest.xml
* android::versionCode
* android::versionName
* forge-gui-android/pom.xml
* alpha-version
* forge-gui-mobile/src/forge/Forge.java
* CURRENT_VERSION
4. Merge
* 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
* Upload the package and its sha to the create new release page
4. Marketing
* Advertise in the #announcements channel in the Discord

View File

@@ -0,0 +1,69 @@
# SteamDeck and Bazzite Support
_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.
* 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)
Once you've installed Waydroid, you can follow the same steps you would in any Android device.
1. Open browser to https://github.com/Card-Forge/forge
1. Open Snapshots from the Link in the ReadMe Page
1. Download the Snapshot APK
1. Open the APK from the download notification in your Android OS.
1. Give permissions to allow the browser to run APK installers.
1. Once installed, Forge should request image storage and audio permissions.
1. Forge sends you to the Settings Page to provide permissions.
1. Run Forge from the Application Drawer (likely Forge is still running)
1. You can tap anywhere else on the screen and Forge will restart.
1. Once Forge restarts it will ask to update the Assets.
1. Congratulations you can play Forge on your SteamDeck.
### Running Forge Android in Steam GameMode
My understanding is that Waydroid running detached Android apps doesn't work at the moment, so the best way to run Forge is to start Waydroid itself from Steam Gamemode and then start the Forge App. To do this you will need to add Waydroid to Steam as a non-Steam Game, controller support should be natively supported this way, you should also enable touch screen pass-through in the Steam Controller configuration if you didn't already during install of Waydroid.
1. Add Waydroid as Non-steam game.
1. Configure/Enable controller support with touchscreen.
1. Start Waydroid from Steam GameMode.
1. Let the Waydroid Android OS boot.
1. Start Forge
## Forge Desktop (JAR or BZ2 File)
### Installing Forge Desktop Natively (Not Recommended)
Barring a flatpack (and packing Java with Forge), the correct way to install Forge Desktop natively (installer JAR or BZ2 archive) would be to install Java OpenJDK in the OS globally. **This is against Steam and Bazzite Dev recommendations**, however is doable;
* For Bazzite: simply use `rpm-ostree install java-21-openjdk`
> You can then install forge as you would in any Linux OS, following the [user guide](user-guide).
> Again, this is against Bazzite devs recommendations, and they will likely belittle you and tell you you can't ask for support, if you tell them you did this like a normal Linux user would.
* For SteamOS: I believe you need to unlock the OS, install java from the package repo, then lock the OS again.
> This Wiki will not provide instructions for this, if you feel you can do this you can probably look up guides to help you.
### Installing Forge in a Container/VM (Boxes or Other)
Another option is to install Forge in a Linux Container or VM, where you can download and install Java JDK into the container, also download and install Forge into the container then run Forge. This is not recommended as it can be confusing to a novice Linux user, adds system overhead, likely UI problems, and would be difficult to perform on a handheld device.
> This Wiki will not provide instructions for this, if you feel you can do this you can probably look up guides to help you.

3
docs/Themes.md Normal file
View File

@@ -0,0 +1,3 @@
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.

101
docs/Towns-&-Capitals.md Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

116
docs/User-Guide.md Normal file
View File

@@ -0,0 +1,116 @@
# 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 />
https://github.com/user-attachments/assets/7a0c7bb8-7cf9-4800-8091-bcc30ff2f4d8
* **Releases**;
* 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
**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;
- Download - [https://jdk.java.net/](https://jdk.java.net/)
- 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.
# Install and Run
Forge requires Java to run.
_**Download and unpack /install the package to their own new folder!**_
### 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.
* Browse to your preferred install directory (create a new directory for clean installation) and click next until installation starts.
![image](https://github.com/Card-Forge/forge/assets/9781539/b7575f49-f6b3-4933-a15f-726314547c4f)
* After the installation finishes, close the installer. Run the executable forge|forge-adventure (.exe/.sh/.cmd)
### 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 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
* 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.
* 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;
- check you're up to date with your version.
- check in the settings that the "Selector Mode" is set to `Default`
# System Requirements and Historic Details
Since Forge is written in Java, it is compatible on any Operating System
that can run the Java Runtime Environment. Forge requires
Java 17 (Forge is not backwards compatible with older versions of Java).
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
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
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**
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.) 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 doesnt work?
Sometimes double-clicking will open the jar file in a different program.
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
Linux systems (file permission related).

65
docs/_sidebar.md Normal file
View File

@@ -0,0 +1,65 @@
### [Forge Wiki Home](Home.md)
- [User Guide](User-Guide.md)
- [FAQ](Frequently-Asked-Questions.md)
- [SteamDeck/Bazzite](Steam-Deck-and-Bazzite-Install.md)
- [AI](ai.md)
- [Network Play](network-play.md)
- [Network FAQ](Network-FAQ.md)
- [Network Extra](Networking-Extras.md)
- [Adventure Mode](Adventure-Mode.md)
- Gameplay Guide
- [Getting Started](Gameplay-Guide.md)
- [Different Planes](Different-Planes.md)
- [Currency](Currency.md)
- [Deck Building Tips](Deck-Building-Tips.md)
- [Towns & Capitals](Towns-&-Capitals.md)
- [Dungeons](Dungeons.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
- [Tutorial 1, Create your first Plane](Tutorial-1-Create-your-First-Plane.md)
- [Tutorial 2, A New Look (creating your first map.)](Tutorial-2-A-New-Look.md)
- [Tutorial 3, Configuration (Configuring your Plane)](Tutorial-3-Configuration.md)
- [Card Scripting API](Card-scripting-API/Card-scripting-API.md)
- [Ability effects](Card-scripting-API/AbilityFactory.md)
- [Triggers](Card-scripting-API/Triggers.md)
- [Replacements](Card-scripting-API/Replacements.md)
- Statics
- [Costs](Card-scripting-API/Costs.md)
- [Affected / Targets](Card-scripting-API/Targeting.md)
- [Restrictions / Conditions](Card-scripting-API/Restrictions.md)
- [Guide: Creating a Custom Card](Card-scripting-API/Creating-a-Custom-Card.md)
- [Development]((SM-autoconverted)-how-to-get-started-developing-forge.md)
- [IntelliJ Setup](Development/IntelliJ-setup/IntelliJ-setup.md)
- [Snapshots and Releases](Snapshots-and-Releases.md)
- [Android Builds](Development/android-builds.md)
- [Ownership](Development/ownership.md)
- [Docker Container](docker-setup.md)
- [Customization and Themes.md](Themes.md)
- Skins
- Sounds
- [Music](Custom-Music.md)
- [Card Images](Card-Images.md)
- [File Formats](File-Formats.md)
- [Creating your first custom set](Creating-a-custom-set.md)
- [Missing Cards in Forge](Missing-Cards-in-Forge.md)
- [Uncards, Playtest Cards, and Other Funny Cards](Uncards,-Playtest-Cards,-and-Other-Funny-Cards.md)
- [Credit and Thanks](Credit-and-Thanks.md)

View File

@@ -275,27 +275,6 @@ public class AiAttackController {
return false;
}
// the attacker will die to a triggered ability (e.g. Sarkhan the Masterless)
for (Card c : ai.getOpponents().getCardsIn(ZoneType.Battlefield)) {
for (Trigger t : c.getTriggers()) {
if (t.getMode() == TriggerType.Attacks) {
SpellAbility sa = t.ensureAbility();
if (sa == null) {
continue;
}
if (sa.getApi() == ApiType.EachDamage && "TriggeredAttacker".equals(sa.getParam("Defined"))) {
List<Card> valid = CardLists.getValidCards(c.getController().getCreaturesInPlay(), sa.getParam("ValidCards"), c.getController(), c, sa);
// TODO: this assumes that 1 damage is dealt per creature. Improve this to check the parameter/X to determine
// how much damage is dealt by each of the creatures in the valid list.
if (attacker.getNetToughness() <= valid.size()) {
return false;
}
}
}
}
}
if ("TRUE".equals(attacker.getSVar("HasAttackEffect"))) {
return true;
}
@@ -414,7 +393,7 @@ public class AiAttackController {
if (ai.getController().isAI()) {
PlayerControllerAi aic = ((PlayerControllerAi) ai.getController());
pilotsNonAggroDeck = aic.pilotsNonAggroDeck();
playAggro = !pilotsNonAggroDeck || aic.getAi().getBooleanProperty(AiProps.PLAY_AGGRO);
playAggro = !pilotsNonAggroDeck || aic.getAi().getBoolProperty(AiProps.PLAY_AGGRO);
}
// TODO make switchable via AI property
int thresholdMod = 0;
@@ -635,13 +614,7 @@ public class AiAttackController {
// if true, the AI will attempt to identify which blockers will already be taken,
// thus attempting to predict how many creatures with evasion can actively block
boolean predictEvasion = false;
if (ai.getController().isAI()) {
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
if (aic.getBooleanProperty(AiProps.COMBAT_ASSAULT_ATTACK_EVASION_PREDICTION)) {
predictEvasion = true;
}
}
boolean predictEvasion = AiProfileUtil.getBoolProperty(ai, AiProps.COMBAT_ASSAULT_ATTACK_EVASION_PREDICTION);
CardCollection accountedBlockers = new CardCollection(this.blockers);
CardCollection categorizedAttackers = new CardCollection();
@@ -881,12 +854,12 @@ public class AiAttackController {
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
simAI = aic.usesSimulation();
if (!simAI) {
playAggro = aic.getBooleanProperty(AiProps.PLAY_AGGRO);
playAggro = aic.getBoolProperty(AiProps.PLAY_AGGRO);
chanceToAttackToTrade = aic.getIntProperty(AiProps.CHANCE_TO_ATTACK_INTO_TRADE);
tradeIfTappedOut = aic.getBooleanProperty(AiProps.ATTACK_INTO_TRADE_WHEN_TAPPED_OUT);
tradeIfTappedOut = aic.getBoolProperty(AiProps.ATTACK_INTO_TRADE_WHEN_TAPPED_OUT);
extraChanceIfOppHasMana = aic.getIntProperty(AiProps.CHANCE_TO_ATKTRADE_WHEN_OPP_HAS_MANA);
tradeIfLowerLifePressure = aic.getBooleanProperty(AiProps.RANDOMLY_ATKTRADE_ONLY_ON_LOWER_LIFE_PRESSURE);
predictEvasion = aic.getBooleanProperty(AiProps.COMBAT_ATTRITION_ATTACK_EVASION_PREDICTION);
tradeIfLowerLifePressure = aic.getBoolProperty(AiProps.RANDOMLY_ATKTRADE_ONLY_ON_LOWER_LIFE_PRESSURE);
predictEvasion = aic.getBoolProperty(AiProps.COMBAT_ATTRITION_ATTACK_EVASION_PREDICTION);
}
}
@@ -995,6 +968,16 @@ public class AiAttackController {
return aiAggression;
}
// Only do decisive attacks against token-generating players
if (!bAssault && defender instanceof Player) {
Player opponent = (Player)defender;
if (CardLists.count(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Rabble Rousing"))
- CardLists.count(opponent.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Darien, King of Kjeldor"))
- CardLists.count(opponent.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Kazuul, Tyrant of the Cliffs")) < 0) {
return aiAggression;
}
}
if (bAssault && defender == defendingOpponent) { // in case we are forced to attack someone else
if (LOG_AI_ATTACKS)
System.out.println("Assault");
@@ -1005,7 +988,7 @@ public class AiAttackController {
if (attackMax != -1 && combat.getAttackers().size() >= attackMax)
return aiAggression;
// TODO if lifeInDanger use chance to hold back some
// TODO if lifeInDanger use chance to hold back some (especially in multiplayer)
if (canAttackWrapper(attacker, defender) && isEffectiveAttacker(ai, attacker, combat, defender)) {
combat.addAttacker(attacker, defender);
}
@@ -1102,7 +1085,8 @@ public class AiAttackController {
for (final Card pCard : myList) {
// if the creature can attack then it's a potential attacker this
// turn, assume summoning sickness creatures will be able to
if (ComputerUtilCombat.canAttackNextTurn(pCard) && pCard.getNetCombatDamage() > 0) {
// TODO: Account for triggered power boosts.
if (ComputerUtilCombat.canAttackNextTurn(pCard) && (pCard.getNetCombatDamage() > 0 || "TRUE".equals(pCard.getSVar("HasAttackEffect")))) {
candidateAttackers.add(pCard);
candidateUnblockedDamage += ComputerUtilCombat.damageIfUnblocked(pCard, defendingOpponent, null, false);
computerForces++;
@@ -1433,7 +1417,7 @@ public class AiAttackController {
// Check if maybe we are too reckless in adding this attacker
if (canKillAllDangerous) {
boolean avoidAttackingIntoBlock = ai.getController().isAI()
&& ((PlayerControllerAi) ai.getController()).getAi().getBooleanProperty(AiProps.TRY_TO_AVOID_ATTACKING_INTO_CERTAIN_BLOCK);
&& ((PlayerControllerAi) ai.getController()).getAi().getBoolProperty(AiProps.TRY_TO_AVOID_ATTACKING_INTO_CERTAIN_BLOCK);
boolean attackerWillDie = defPower >= attacker.getNetToughness();
boolean uselessAttack = !hasCombatEffect && !hasAttackEffect;
boolean noContributionToAttack = attackers.size() <= defenders.size() || attacker.getNetPower() <= 0;

View File

@@ -861,10 +861,9 @@ public class AiBlockController {
return;
}
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
final int evalThresholdToken = aic.getIntProperty(AiProps.THRESHOLD_TOKEN_CHUMP_TO_SAVE_PLANESWALKER);
final int evalThresholdNonToken = aic.getIntProperty(AiProps.THRESHOLD_NONTOKEN_CHUMP_TO_SAVE_PLANESWALKER);
final boolean onlyIfLethal = aic.getBooleanProperty(AiProps.CHUMP_TO_SAVE_PLANESWALKER_ONLY_ON_LETHAL);
final int evalThresholdToken = AiProfileUtil.getIntProperty(ai, AiProps.THRESHOLD_TOKEN_CHUMP_TO_SAVE_PLANESWALKER);
final int evalThresholdNonToken = AiProfileUtil.getIntProperty(ai, AiProps.THRESHOLD_NONTOKEN_CHUMP_TO_SAVE_PLANESWALKER);
final boolean onlyIfLethal = AiProfileUtil.getBoolProperty(ai, AiProps.CHUMP_TO_SAVE_PLANESWALKER_ONLY_ON_LETHAL);
if (evalThresholdToken > 0 || evalThresholdNonToken > 0) {
// detect how much damage is threatened to each of the planeswalkers, see which ones would be
@@ -1047,7 +1046,7 @@ public class AiBlockController {
clearBlockers(combat, possibleBlockers);
diff = (ai.getLife() * 2) - 5; // This is the minimal gain for an unnecessary trade
if (ai.getController().isAI() && diff > 0 && ((PlayerControllerAi) ai.getController()).getAi().getBooleanProperty(AiProps.PLAY_AGGRO)) {
if (diff > 0 && AiProfileUtil.getBoolProperty(ai, AiProps.PLAY_AGGRO)) {
diff = 0;
}
@@ -1284,9 +1283,9 @@ public class AiBlockController {
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
// simulation must get same results or it may crash
if (!aic.usesSimulation()) {
enableRandomTrades = aic.getBooleanProperty(AiProps.ENABLE_RANDOM_FAVORABLE_TRADES_ON_BLOCK);
randomTradeIfBehindOnBoard = aic.getBooleanProperty(AiProps.RANDOMLY_TRADE_EVEN_WHEN_HAVE_LESS_CREATS);
randomTradeIfCreatInHand = aic.getBooleanProperty(AiProps.ALSO_TRADE_WHEN_HAVE_A_REPLACEMENT_CREAT);
enableRandomTrades = aic.getBoolProperty(AiProps.ENABLE_RANDOM_FAVORABLE_TRADES_ON_BLOCK);
randomTradeIfBehindOnBoard = aic.getBoolProperty(AiProps.RANDOMLY_TRADE_EVEN_WHEN_HAVE_LESS_CREATS);
randomTradeIfCreatInHand = aic.getBoolProperty(AiProps.ALSO_TRADE_WHEN_HAVE_A_REPLACEMENT_CREAT);
minRandomTradeChance = aic.getIntProperty(AiProps.MIN_CHANCE_TO_RANDOMLY_TRADE_ON_BLOCK);
maxRandomTradeChance = aic.getIntProperty(AiProps.MAX_CHANCE_TO_RANDOMLY_TRADE_ON_BLOCK);
chanceModForEmbalm = aic.getIntProperty(AiProps.CHANCE_DECREASE_TO_TRADE_VS_EMBALM);

View File

@@ -59,7 +59,6 @@ public class AiCardMemory {
ATTACHED_THIS_TURN, // These equipments were attached to something already this turn
ANIMATED_THIS_TURN, // These cards had their AF Animate effect activated this turn
BOUNCED_THIS_TURN, // These cards were bounced this turn
ACTIVATED_THIS_TURN, // These cards had their ability activated this turn
CHOSEN_FOG_EFFECT, // These cards are marked as the Fog-like effect the AI is planning to cast this turn
MARKED_TO_AVOID_REENTRY, // These cards may cause a stack smash when processed recursively, and are thus marked to avoid a crash
PAYS_TAP_COST, // These cards will be tapped as part of a cost and cannot be chosen in another part

View File

@@ -66,13 +66,11 @@ import io.sentry.Breadcrumb;
import io.sentry.Sentry;
import java.util.*;
import java.util.concurrent.FutureTask;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@@ -99,6 +97,7 @@ public class AiController {
private int lastAttackAggression;
private boolean useLivingEnd;
private List<SpellAbility> skipped;
private boolean timeoutReached;
public AiController(final Player computerPlayer, final Game game0) {
player = computerPlayer;
@@ -378,7 +377,7 @@ public class AiController {
if (card.isSaga()) {
for (final Trigger tr : card.getTriggers()) {
if (tr.getMode() != TriggerType.CounterAdded || !tr.isChapter()) {
if (!tr.isChapter()) {
continue;
}
@@ -394,6 +393,7 @@ public class AiController {
return false;
}
// usually later chapters make use of an earlier one
break;
}
}
@@ -483,7 +483,7 @@ public class AiController {
if (lands.size() >= Math.max(maxCmcInHand, 6)) {
// don't play MDFC land if other side is spell and enough lands are available
if (!c.isLand() || (c.isModal() && !c.getState(CardStateName.Modal).getType().isLand())) {
if (!c.isLand() || (c.isModal() && !c.getState(CardStateName.Backside).getType().isLand())) {
return false;
}
@@ -764,7 +764,7 @@ public class AiController {
return predictSpellToCastInMain2(exceptSA, true);
}
private SpellAbility predictSpellToCastInMain2(ApiType exceptSA, boolean handOnly) {
if (!getBooleanProperty(AiProps.PREDICT_SPELLS_FOR_MAIN2)) {
if (!getBoolProperty(AiProps.PREDICT_SPELLS_FOR_MAIN2)) {
return null;
}
@@ -888,27 +888,8 @@ public class AiController {
private AiPlayDecision canPlayAndPayForFace(final SpellAbility sa) {
final Card host = sa.getHostCard();
// Check a predefined condition
if (sa.hasParam("AICheckSVar")) {
final String svarToCheck = sa.getParam("AICheckSVar");
String comparator = "GE";
int compareTo = 1;
if (sa.hasParam("AISVarCompare")) {
final String fullCmp = sa.getParam("AISVarCompare");
comparator = fullCmp.substring(0, 2);
final String strCmpTo = fullCmp.substring(2);
try {
compareTo = Integer.parseInt(strCmpTo);
} catch (final Exception ignored) {
compareTo = AbilityUtils.calculateAmount(host, host.getSVar(strCmpTo), sa);
}
}
int left = AbilityUtils.calculateAmount(host, svarToCheck, sa);
if (!Expressions.compare(left, comparator, compareTo)) {
return AiPlayDecision.AnotherTime;
}
if (sa.hasParam("AICheckSVar") && !aiShouldRun(sa, sa, host, null)) {
return AiPlayDecision.AnotherTime;
}
// this is the "heaviest" check, which also sets up targets, defines X, etc.
@@ -926,7 +907,7 @@ public class AiController {
// check if enough left (pass memory indirectly because we don't want to include those)
Set<Card> tappedForMana = AiCardMemory.getMemorySet(player, MemorySet.PAYS_TAP_COST);
if (tappedForMana != null && tappedForMana.isEmpty() &&
if (tappedForMana != null && !tappedForMana.isEmpty() &&
!ComputerUtilCost.checkTapTypeCost(player, sa.getPayCosts(), host, sa, new CardCollection(tappedForMana))) {
return AiPlayDecision.CantAfford;
}
@@ -949,7 +930,7 @@ public class AiController {
final Card card = sa.getHostCard();
// Trying to play a card that has Buyback without a Buyback cost, look for possible additional considerations
if (getBooleanProperty(AiProps.TRY_TO_PRESERVE_BUYBACK_SPELLS)) {
if (getBoolProperty(AiProps.TRY_TO_PRESERVE_BUYBACK_SPELLS)) {
if (card.hasKeyword(Keyword.BUYBACK) && !sa.isBuyback() && !canPlaySpellWithoutBuyback(card, sa)) {
return AiPlayDecision.NeedsToPlayCriteriaNotMet;
}
@@ -1318,27 +1299,13 @@ public class AiController {
}
public String getProperty(AiProps propName) {
return AiProfileUtil.getAIProp(getPlayer().getLobbyPlayer(), propName);
return AiProfileUtil.getProperty(getPlayer(), propName);
}
public int getIntProperty(AiProps propName) {
String prop = AiProfileUtil.getAIProp(getPlayer().getLobbyPlayer(), propName);
if (prop == null || prop.isEmpty()) {
return Integer.parseInt(propName.getDefault());
}
return Integer.parseInt(prop);
return AiProfileUtil.getIntProperty(getPlayer(), propName);
}
public boolean getBooleanProperty(AiProps propName) {
String prop = AiProfileUtil.getAIProp(getPlayer().getLobbyPlayer(), propName);
if (prop == null || prop.isEmpty()) {
return Boolean.parseBoolean(propName.getDefault());
}
return Boolean.parseBoolean(prop);
public boolean getBoolProperty(AiProps propName) {
return AiProfileUtil.getBoolProperty(getPlayer(), propName);
}
public AiPlayDecision canPlayFromEffectAI(Spell spell, boolean mandatory, boolean withoutPayingManaCost) {
@@ -1349,7 +1316,7 @@ public class AiController {
final Card card = spell.getHostCard();
if (spell instanceof SpellApiBased) {
boolean chance = false;
boolean chance;
if (withoutPayingManaCost) {
chance = SpellApiToAi.Converter.get(spell).doTriggerNoCostWithSubs(player, spell, mandatory).willingToPlay();
} else {
@@ -1487,7 +1454,7 @@ public class AiController {
CardCollection inHand = CardLists.filter(player.getCardsIn(ZoneType.Hand), CardPredicates.NON_LANDS);
CardCollectionView otb = player.getCardsIn(ZoneType.Battlefield);
if (getBooleanProperty(AiProps.HOLD_LAND_DROP_ONLY_IF_HAVE_OTHER_PERMS)) {
if (getBoolProperty(AiProps.HOLD_LAND_DROP_ONLY_IF_HAVE_OTHER_PERMS)) {
if (!otb.anyMatch(CardPredicates.NON_LANDS)) {
return false;
}
@@ -1666,8 +1633,10 @@ public class AiController {
Sentry.captureMessage(ex.getMessage() + "\nAssertionError [verifyTransitivity]: " + assertex);
}
final ExecutorService executor = Executors.newSingleThreadExecutor();
Future<SpellAbility> future = executor.submit(() -> {
// in case of infinite loop reset below would not be reached
timeoutReached = false;
FutureTask<SpellAbility> future = new FutureTask<>(() -> {
//avoid ComputerUtil.aiLifeInDanger in loops as it slows down a lot.. call this outside loops will generally be fast...
boolean isLifeInDanger = useLivingEnd && ComputerUtil.aiLifeInDanger(player, true, 0);
for (final SpellAbility sa : ComputerUtilAbility.getOriginalAndAltCostAbilities(all, player)) {
@@ -1676,6 +1645,11 @@ public class AiController {
continue;
}
if (timeoutReached) {
timeoutReached = false;
break;
}
if (sa.getHostCard().hasKeyword(Keyword.STORM)
&& sa.getApi() != ApiType.Counter // AI would suck at trying to deliberately proc a Storm counterspell
&& player.getZone(ZoneType.Hand).contains(
@@ -1745,11 +1719,21 @@ public class AiController {
return null;
});
// instead of computing all available concurrently just add a simple timeout depending on the user prefs
Thread t = new Thread(future);
t.start();
try {
// instead of computing all available concurrently just add a simple timeout depending on the user prefs
return future.get(game.getAITimeout(), TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
future.cancel(true);
try {
t.stop();
} catch (UnsupportedOperationException ex) {
// Android and Java 20 dropped support to stop so sadly thread will keep running
timeoutReached = true;
future.cancel(true);
// TODO wait a few more seconds to try and exit at a safe point before letting the engine continue
// TODO mark some as skipped to increase chance to find something playable next priority
}
return null;
}
}
@@ -1801,14 +1785,9 @@ public class AiController {
* @param sa the sa
* @return true, if successful
*/
public final boolean aiShouldRun(final ReplacementEffect effect, final SpellAbility sa, GameEntity affected) {
Card hostCard = effect.getHostCard();
if (hostCard.hasAlternateState()) {
hostCard = game.getCardState(hostCard);
}
public final boolean aiShouldRun(final CardTraitBase effect, final SpellAbility sa, final Card host, final GameEntity affected) {
if (effect.hasParam("AILogic") && effect.getParam("AILogic").equalsIgnoreCase("ProtectFriendly")) {
final Player controller = hostCard.getController();
final Player controller = host.getController();
if (affected instanceof Player) {
return !((Player) affected).isOpponentOf(controller);
}
@@ -1817,7 +1796,6 @@ public class AiController {
}
}
if (effect.hasParam("AICheckSVar")) {
System.out.println("aiShouldRun?" + sa);
final String svarToCheck = effect.getParam("AICheckSVar");
String comparator = "GE";
int compareTo = 1;
@@ -1830,9 +1808,9 @@ public class AiController {
compareTo = Integer.parseInt(strCmpTo);
} catch (final Exception ignored) {
if (sa == null) {
compareTo = AbilityUtils.calculateAmount(hostCard, hostCard.getSVar(strCmpTo), effect);
compareTo = AbilityUtils.calculateAmount(host, host.getSVar(strCmpTo), effect);
} else {
compareTo = AbilityUtils.calculateAmount(hostCard, hostCard.getSVar(strCmpTo), sa);
compareTo = AbilityUtils.calculateAmount(host, host.getSVar(strCmpTo), sa);
}
}
}
@@ -1840,13 +1818,12 @@ public class AiController {
int left = 0;
if (sa == null) {
left = AbilityUtils.calculateAmount(hostCard, svarToCheck, effect);
left = AbilityUtils.calculateAmount(host, svarToCheck, effect);
} else {
left = AbilityUtils.calculateAmount(hostCard, svarToCheck, sa);
left = AbilityUtils.calculateAmount(host, svarToCheck, sa);
}
System.out.println("aiShouldRun?" + left + comparator + compareTo);
return Expressions.compare(left, comparator, compareTo);
} else if (effect.hasParam("AICheckDredge")) {
} else if (effect.isKeyword(Keyword.DREDGE)) {
return player.getCardsIn(ZoneType.Library).size() > 8 || player.isCardInPlay("Laboratory Maniac");
} else return sa != null && doTrigger(sa, false);
}

View File

@@ -29,12 +29,15 @@ public class AiCostDecision extends CostDecisionMakerBase {
private final CardCollection tapped;
public AiCostDecision(Player ai0, SpellAbility sa, final boolean effect) {
this(ai0, sa, effect, false);
}
public AiCostDecision(Player ai0, SpellAbility sa, final boolean effect, final boolean payMana) {
super(ai0, effect, sa, sa.getHostCard());
discarded = new CardCollection();
tapped = new CardCollection();
Set<Card> tappedForMana = AiCardMemory.getMemorySet(ai0, MemorySet.PAYS_TAP_COST);
if (tappedForMana != null) {
if (!payMana && tappedForMana != null) {
tapped.addAll(tappedForMana);
}
}
@@ -110,7 +113,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
randomSubset = ability.getActivatingPlayer().getController().orderMoveToZoneList(randomSubset, ZoneType.Graveyard, ability);
}
return PaymentDecision.card(randomSubset);
} else if (type.equals("DifferentNames")) {
} else if (type.contains("+WithDifferentNames")) {
CardCollection differentNames = new CardCollection();
CardCollection discardMe = CardLists.filter(hand, CardPredicates.hasSVar("DiscardMe"));
while (c > 0) {
@@ -563,7 +566,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
int thisRemove = Math.min(prefCard.getCounters(cType), stillToRemove);
if (thisRemove > 0) {
removed += thisRemove;
table.put(null, prefCard, CounterType.get(cType), thisRemove);
table.put(null, prefCard, cType, thisRemove);
}
}
}
@@ -573,7 +576,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
@Override
public PaymentDecision visit(CostRemoveAnyCounter cost) {
final int c = cost.getAbilityAmount(ability);
final Card originalHost = ObjectUtils.defaultIfNull(ability.getOriginalHost(), source);
final Card originalHost = ObjectUtils.getIfNull(ability.getOriginalHost(), source);
if (c <= 0) {
return null;
@@ -716,7 +719,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
int over = Math.min(crd.getCounters(CounterEnumType.QUEST) - e, c - toRemove);
if (over > 0) {
toRemove += over;
table.put(null, crd, CounterType.get(CounterEnumType.QUEST), over);
table.put(null, crd, CounterEnumType.QUEST, over);
}
}
}

View File

@@ -19,7 +19,7 @@ public enum AiPlayDecision {
StackNotEmpty,
AnotherTime,
// Don't play decision reasons,
// Don't play decision reasons
CantPlaySa,
CantPlayAi,
CantAfford,

View File

@@ -18,6 +18,7 @@
package forge.ai;
import forge.LobbyPlayer;
import forge.game.player.Player;
import forge.util.Aggregates;
import forge.util.FileUtil;
import forge.util.TextUtil;
@@ -113,6 +114,23 @@ public class AiProfileUtil {
return profileMap;
}
public static String getProperty(final Player p, final AiProps propName) {
String prop = AiProfileUtil.getAIProp(p.getLobbyPlayer(), propName);
if (prop == null || prop.isEmpty()) {
// TODO if p is human try to predict some values from previous plays or something
return propName.getDefault();
}
return prop;
}
public static int getIntProperty(final Player p, final AiProps propName) {
return Integer.parseInt(getProperty(p, propName));
}
public static boolean getBoolProperty(final Player p, final AiProps propName) {
return Boolean.parseBoolean(getProperty(p, propName));
}
/**
* Returns an AI property value for the current profile.
*

View File

@@ -345,27 +345,24 @@ public class ComputerUtil {
return sacMeList.getFirst();
} else {
// empty sacMeList, so get some viable average preference if the option is enabled
if (ai.getController().isAI()) {
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
boolean enableDefaultPref = aic.getBooleanProperty(AiProps.SACRIFICE_DEFAULT_PREF_ENABLE);
if (enableDefaultPref) {
int minCMC = aic.getIntProperty(AiProps.SACRIFICE_DEFAULT_PREF_MIN_CMC);
int maxCMC = aic.getIntProperty(AiProps.SACRIFICE_DEFAULT_PREF_MAX_CMC);
int maxCreatureEval = aic.getIntProperty(AiProps.SACRIFICE_DEFAULT_PREF_MAX_CREATURE_EVAL);
boolean allowTokens = aic.getBooleanProperty(AiProps.SACRIFICE_DEFAULT_PREF_ALLOW_TOKENS);
List<String> dontSac = Arrays.asList("Black Lotus", "Mox Pearl", "Mox Jet", "Mox Emerald", "Mox Ruby", "Mox Sapphire", "Lotus Petal");
CardCollection allowList = CardLists.filter(typeList, card -> {
if (card.isCreature() && ComputerUtilCard.evaluateCreature(card) > maxCreatureEval) {
return false;
}
return (allowTokens && card.isToken())
|| (card.getCMC() >= minCMC && card.getCMC() <= maxCMC && !dontSac.contains(card.getName()));
});
if (!allowList.isEmpty()) {
CardLists.sortByCmcDesc(allowList);
return allowList.getLast();
boolean enableDefaultPref = AiProfileUtil.getBoolProperty(ai, AiProps.SACRIFICE_DEFAULT_PREF_ENABLE);
if (enableDefaultPref) {
int minCMC = AiProfileUtil.getIntProperty(ai, AiProps.SACRIFICE_DEFAULT_PREF_MIN_CMC);
int maxCMC = AiProfileUtil.getIntProperty(ai, AiProps.SACRIFICE_DEFAULT_PREF_MAX_CMC);
int maxCreatureEval = AiProfileUtil.getIntProperty(ai, AiProps.SACRIFICE_DEFAULT_PREF_MAX_CREATURE_EVAL);
boolean allowTokens = AiProfileUtil.getBoolProperty(ai, AiProps.SACRIFICE_DEFAULT_PREF_ALLOW_TOKENS);
List<String> dontSac = Arrays.asList("Black Lotus", "Mox Pearl", "Mox Jet", "Mox Emerald", "Mox Ruby", "Mox Sapphire", "Lotus Petal");
CardCollection allowList = CardLists.filter(typeList, card -> {
if (card.isCreature() && ComputerUtilCard.evaluateCreature(card) > maxCreatureEval) {
return false;
}
return (allowTokens && card.isToken())
|| (card.getCMC() >= minCMC && card.getCMC() <= maxCMC && !dontSac.contains(card.getName()));
});
if (!allowList.isEmpty()) {
CardLists.sortByCmcDesc(allowList);
return allowList.getLast();
}
}
}
@@ -767,7 +764,7 @@ public class ComputerUtil {
public static CardCollection chooseUntapType(final Player ai, final String type, final Card activate, final boolean untap, final int amount, SpellAbility sa) {
CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), activate.getController(), activate, sa);
typeList = CardLists.filter(typeList, CardPredicates.TAPPED, c -> c.getCounters(CounterEnumType.STUN) == 0 || c.canRemoveCounters(CounterType.get(CounterEnumType.STUN)));
typeList = CardLists.filter(typeList, CardPredicates.TAPPED, c -> c.getCounters(CounterEnumType.STUN) == 0 || c.canRemoveCounters(CounterEnumType.STUN));
if (untap) {
typeList.remove(activate);
@@ -1074,6 +1071,80 @@ public class ComputerUtil {
return prevented;
}
/**
* Is it OK to cast this for less than the Max Targets?
* @param source the source Card
* @return true if it's OK to cast this Card for less than the max targets
*/
public static boolean shouldCastLessThanMax(final Player ai, final Card source) {
if (source.getXManaCostPaid() > 0) {
// If TargetMax is MaxTgts (i.e., an "X" cost), this is fine because AI is limited by payment resources available.
return true;
}
if (aiLifeInDanger(ai, false, 0)) {
// Otherwise, if life is possibly in danger, then this is fine.
return true;
}
// do not play now.
return false;
}
/**
* Is this discard probably worse than a random draw?
* @param discard Card to discard
* @return boolean
*/
public static boolean isWorseThanDraw(final Player ai, Card discard) {
if (discard.hasSVar("DiscardMe")) {
return true;
}
final Game game = ai.getGame();
final CardCollection landsInPlay = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.LANDS_PRODUCING_MANA);
final CardCollection landsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.LANDS);
final CardCollection nonLandsInHand = CardLists.getNotType(ai.getCardsIn(ZoneType.Hand), "Land");
final int highestCMC = Math.max(6, Aggregates.max(nonLandsInHand, Card::getCMC));
final int discardCMC = discard.getCMC();
if (discard.isLand()) {
if (landsInPlay.size() >= highestCMC
|| (landsInPlay.size() + landsInHand.size() > 6 && landsInHand.size() > 1)
|| (landsInPlay.size() > 3 && nonLandsInHand.size() == 0)) {
// Don't need more land.
return true;
}
} else { //non-land
if (discardCMC > landsInPlay.size() + landsInHand.size() + 2) {
// not castable for some time.
return true;
} else if (!game.getPhaseHandler().isPlayerTurn(ai)
&& game.getPhaseHandler().getPhase().isAfter(PhaseType.MAIN2)
&& discardCMC > landsInPlay.size() + landsInHand.size()
&& discardCMC > landsInPlay.size() + 1
&& nonLandsInHand.size() > 1) {
// not castable for at least one other turn.
return true;
} else if (landsInPlay.size() > 5 && discard.getCMC() <= 1
&& !discard.hasProperty("hasXCost", ai, null, null)) {
// Probably don't need small stuff now.
return true;
}
}
return false;
}
// returns true if it's better to wait until blockers are declared
public static boolean waitForBlocking(final SpellAbility sa) {
final Game game = sa.getActivatingPlayer().getGame();
final PhaseHandler ph = game.getPhaseHandler();
return sa.getHostCard().isCreature()
&& sa.getPayCosts().hasTapCost()
&& (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)
&& !ph.getNextTurn().equals(sa.getActivatingPlayer()))
&& !sa.getHostCard().hasSVar("EndOfTurnLeavePlay")
&& !sa.hasParam("ActivationPhases");
}
public static boolean castPermanentInMain1(final Player ai, final SpellAbility sa) {
final Card card = sa.getHostCard();
final CardState cardState = card.isFaceDown() ? card.getState(CardStateName.Original) : card.getCurrentState();
@@ -1245,80 +1316,6 @@ public class ComputerUtil {
return false;
}
/**
* Is it OK to cast this for less than the Max Targets?
* @param source the source Card
* @return true if it's OK to cast this Card for less than the max targets
*/
public static boolean shouldCastLessThanMax(final Player ai, final Card source) {
if (source.getXManaCostPaid() > 0) {
// If TargetMax is MaxTgts (i.e., an "X" cost), this is fine because AI is limited by payment resources available.
return true;
}
if (aiLifeInDanger(ai, false, 0)) {
// Otherwise, if life is possibly in danger, then this is fine.
return true;
}
// do not play now.
return false;
}
/**
* Is this discard probably worse than a random draw?
* @param discard Card to discard
* @return boolean
*/
public static boolean isWorseThanDraw(final Player ai, Card discard) {
if (discard.hasSVar("DiscardMe")) {
return true;
}
final Game game = ai.getGame();
final CardCollection landsInPlay = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.LANDS_PRODUCING_MANA);
final CardCollection landsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.LANDS);
final CardCollection nonLandsInHand = CardLists.getNotType(ai.getCardsIn(ZoneType.Hand), "Land");
final int highestCMC = Math.max(6, Aggregates.max(nonLandsInHand, Card::getCMC));
final int discardCMC = discard.getCMC();
if (discard.isLand()) {
if (landsInPlay.size() >= highestCMC
|| (landsInPlay.size() + landsInHand.size() > 6 && landsInHand.size() > 1)
|| (landsInPlay.size() > 3 && nonLandsInHand.size() == 0)) {
// Don't need more land.
return true;
}
} else { //non-land
if (discardCMC > landsInPlay.size() + landsInHand.size() + 2) {
// not castable for some time.
return true;
} else if (!game.getPhaseHandler().isPlayerTurn(ai)
&& game.getPhaseHandler().getPhase().isAfter(PhaseType.MAIN2)
&& discardCMC > landsInPlay.size() + landsInHand.size()
&& discardCMC > landsInPlay.size() + 1
&& nonLandsInHand.size() > 1) {
// not castable for at least one other turn.
return true;
} else if (landsInPlay.size() > 5 && discard.getCMC() <= 1
&& !discard.hasProperty("hasXCost", ai, null, null)) {
// Probably don't need small stuff now.
return true;
}
}
return false;
}
// returns true if it's better to wait until blockers are declared
public static boolean waitForBlocking(final SpellAbility sa) {
final Game game = sa.getActivatingPlayer().getGame();
final PhaseHandler ph = game.getPhaseHandler();
return sa.getHostCard().isCreature()
&& sa.getPayCosts().hasTapCost()
&& (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)
&& !ph.getNextTurn().equals(sa.getActivatingPlayer()))
&& !sa.getHostCard().hasSVar("EndOfTurnLeavePlay")
&& !sa.hasParam("ActivationPhases");
}
public static boolean castSpellInMain1(final Player ai, final SpellAbility sa) {
final Card source = sa.getHostCard();
final SpellAbility sub = sa.getSubAbility();
@@ -1327,7 +1324,6 @@ public class ComputerUtil {
return true;
}
// Cipher spells
if (sub != null) {
final ApiType api = sub.getApi();
if (ApiType.Encode == api && !ai.getCreaturesInPlay().isEmpty()) {
@@ -1622,7 +1618,6 @@ public class ComputerUtil {
damage = dmg;
}
// Triggered abilities
if (c.isCreature() && c.isInPlay() && CombatUtil.canAttack(c)) {
for (final Trigger t : c.getTriggers()) {
if (TriggerType.Attacks.equals(t.getMode())) {
@@ -2001,8 +1996,7 @@ public class ComputerUtil {
else if ((threatApi == ApiType.Attach && (topStack.isCurse() || "Curse".equals(topStack.getParam("AILogic"))))
&& (saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll
|| saviourApi == ApiType.Protection || saviourApi == null)) {
AiController aic = aiPlayer.isAI() ? ((PlayerControllerAi)aiPlayer.getController()).getAi() : null;
boolean enableCurseAuraRemoval = aic != null ? aic.getBooleanProperty(AiProps.ACTIVELY_DESTROY_IMMEDIATELY_UNBLOCKABLE) : false;
boolean enableCurseAuraRemoval = AiProfileUtil.getBoolProperty(aiPlayer, AiProps.ACTIVELY_DESTROY_IMMEDIATELY_UNBLOCKABLE);
if (enableCurseAuraRemoval) {
for (final Object o : objects) {
if (o instanceof Card c) {
@@ -2043,17 +2037,14 @@ public class ComputerUtil {
// a creature will [hopefully] die from a spell on stack
boolean willDieFromSpell = false;
boolean noStackCheck = false;
if (ai.getController().isAI()) {
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
if (aic.getBooleanProperty(AiProps.DONT_EVAL_KILLSPELLS_ON_STACK_WITH_PERMISSION)) {
// See if permission is on stack and ignore this check if there is and the relevant AI flag is set
// TODO: improve this so that this flag is not needed and the AI can properly evaluate spells in presence of counterspells.
for (SpellAbilityStackInstance si : game.getStack()) {
SpellAbility sa = si.getSpellAbility();
if (sa.getApi() == ApiType.Counter) {
noStackCheck = true;
break;
}
if (AiProfileUtil.getBoolProperty(ai, AiProps.DONT_EVAL_KILLSPELLS_ON_STACK_WITH_PERMISSION)) {
// See if permission is on stack and ignore this check if there is and the relevant AI flag is set
// TODO: improve this so that this flag is not needed and the AI can properly evaluate spells in presence of counterspells.
for (SpellAbilityStackInstance si : game.getStack()) {
SpellAbility sa = si.getSpellAbility();
if (sa.getApi() == ApiType.Counter) {
noStackCheck = true;
break;
}
}
}
@@ -2082,8 +2073,7 @@ public class ComputerUtil {
* @return a filtered list with no dying creatures in it
*/
public static CardCollection filterCreaturesThatWillDieThisTurn(final Player ai, final CardCollection list, final SpellAbility excludeSa) {
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
if (aic.getBooleanProperty(AiProps.AVOID_TARGETING_CREATS_THAT_WILL_DIE)) {
if (AiProfileUtil.getBoolProperty(ai, AiProps.AVOID_TARGETING_CREATS_THAT_WILL_DIE)) {
// Try to avoid targeting creatures that are dead on board
List<Card> willBeKilled = CardLists.filter(list, card -> card.isCreature() && predictCreatureWillDieThisTurn(ai, card, excludeSa));
list.removeAll(willBeKilled);
@@ -2256,25 +2246,14 @@ public class ComputerUtil {
boolean bottom = false;
// AI profile-based toggles
int maxLandsToScryLandsToTop = 3;
int minLandsToScryLandsAway = 8;
int minCreatsToScryCreatsAway = 5;
int minCreatEvalThreshold = 160; // just a bit higher than a baseline 2/2 creature or a 1/1 mana dork
int lowCMCThreshold = 3;
int maxCreatsToScryLowCMCAway = 3;
boolean uncastablesToBottom = false;
int uncastableCMCThreshold = 1;
if (player.getController().isAI()) {
AiController aic = ((PlayerControllerAi)player.getController()).getAi();
maxLandsToScryLandsToTop = aic.getIntProperty(AiProps.SCRY_NUM_LANDS_TO_STILL_NEED_MORE);
minLandsToScryLandsAway = aic.getIntProperty(AiProps.SCRY_NUM_LANDS_TO_NOT_NEED_MORE);
minCreatsToScryCreatsAway = aic.getIntProperty(AiProps.SCRY_NUM_CREATURES_TO_NOT_NEED_SUBPAR_ONES);
minCreatEvalThreshold = aic.getIntProperty(AiProps.SCRY_EVALTHR_TO_SCRY_AWAY_LOWCMC_CREATURE);
lowCMCThreshold = aic.getIntProperty(AiProps.SCRY_EVALTHR_CMC_THRESHOLD);
maxCreatsToScryLowCMCAway = aic.getIntProperty(AiProps.SCRY_EVALTHR_CREATCOUNT_TO_SCRY_AWAY_LOWCMC);
uncastablesToBottom = aic.getBooleanProperty(AiProps.SCRY_IMMEDIATELY_UNCASTABLE_TO_BOTTOM);
uncastableCMCThreshold = aic.getIntProperty(AiProps.SCRY_IMMEDIATELY_UNCASTABLE_CMC_DIFF);
}
int maxLandsToScryLandsToTop = AiProfileUtil.getIntProperty(player, AiProps.SCRY_NUM_LANDS_TO_STILL_NEED_MORE);
int minLandsToScryLandsAway = AiProfileUtil.getIntProperty(player, AiProps.SCRY_NUM_LANDS_TO_NOT_NEED_MORE);
int minCreatsToScryCreatsAway = AiProfileUtil.getIntProperty(player, AiProps.SCRY_NUM_CREATURES_TO_NOT_NEED_SUBPAR_ONES);
int minCreatEvalThreshold = AiProfileUtil.getIntProperty(player, AiProps.SCRY_EVALTHR_TO_SCRY_AWAY_LOWCMC_CREATURE);
int lowCMCThreshold = AiProfileUtil.getIntProperty(player, AiProps.SCRY_EVALTHR_CMC_THRESHOLD);
int maxCreatsToScryLowCMCAway = AiProfileUtil.getIntProperty(player, AiProps.SCRY_EVALTHR_CREATCOUNT_TO_SCRY_AWAY_LOWCMC);
boolean uncastablesToBottom = AiProfileUtil.getBoolProperty(player, AiProps.SCRY_IMMEDIATELY_UNCASTABLE_TO_BOTTOM);
int uncastableCMCThreshold = AiProfileUtil.getIntProperty(player, AiProps.SCRY_IMMEDIATELY_UNCASTABLE_CMC_DIFF);
CardCollectionView allCards = player.getAllCards();
CardCollectionView cardsInHand = player.getCardsIn(ZoneType.Hand);
@@ -2544,7 +2523,7 @@ public class ComputerUtil {
boolean opponent = controller.isOpponentOf(ai);
final CounterType p1p1Type = CounterType.get(CounterEnumType.P1P1);
final CounterType p1p1Type = CounterEnumType.P1P1;
if (!sa.hasParam("AILogic")) {
return Aggregates.random(options);
@@ -2553,7 +2532,7 @@ public class ComputerUtil {
String logic = sa.getParam("AILogic");
switch (logic) {
case "Torture":
return "Torture";
return options.get(1);
case "GraceOrCondemnation":
List<ZoneType> graceZones = new ArrayList<ZoneType>();
graceZones.add(ZoneType.Battlefield);
@@ -2561,12 +2540,12 @@ public class ComputerUtil {
CardCollection graceCreatures = CardLists.getType(game.getCardsIn(graceZones), "Creature");
int humanGrace = CardLists.filterControlledBy(graceCreatures, ai.getOpponents()).size();
int aiGrace = CardLists.filterControlledBy(graceCreatures, ai).size();
return aiGrace > humanGrace ? "Grace" : "Condemnation";
return options.get(aiGrace > humanGrace ? 0 : 1);
case "CarnageOrHomage":
CardCollection cardsInPlay = CardLists.getNotType(game.getCardsIn(ZoneType.Battlefield), "Land");
CardCollection humanlist = CardLists.filterControlledBy(cardsInPlay, ai.getOpponents());
CardCollection computerlist = ai.getCreaturesInPlay();
return ComputerUtilCard.evaluatePermanentList(computerlist) + 3 < ComputerUtilCard.evaluatePermanentList(humanlist) ? "Carnage" : "Homage";
return options.get(ComputerUtilCard.evaluatePermanentList(computerlist) + 3 < ComputerUtilCard.evaluatePermanentList(humanlist) ? 0 : 1);
case "Judgment":
if (votes.isEmpty()) {
CardCollection list = new CardCollection();
@@ -2580,68 +2559,71 @@ public class ComputerUtil {
return Iterables.getFirst(votes.keySet(), null);
case "Protection":
if (votes.isEmpty()) {
List<String> restrictedToColors = Lists.newArrayList();
Map<String, SpellAbility> restrictedToColors = Maps.newHashMap();
for (Object o : options) {
if (o instanceof String) {
restrictedToColors.add((String) o);
}
if (o instanceof SpellAbility sp) { // TODO check for Color Word Changes
restrictedToColors.put(sp.getOriginalDescription(), sp);
}
}
CardCollection lists = CardLists.filterControlledBy(game.getCardsInGame(), ai.getOpponents());
return StringUtils.capitalize(ComputerUtilCard.getMostProminentColor(lists, restrictedToColors));
return restrictedToColors.get(StringUtils.capitalize(ComputerUtilCard.getMostProminentColor(lists, restrictedToColors.keySet())));
}
return Iterables.getFirst(votes.keySet(), null);
case "FeatherOrQuill":
SpellAbility feather = (SpellAbility)options.get(0);
SpellAbility quill = (SpellAbility)options.get(1);
// try to mill opponent with Quill vote
if (opponent && !controller.cantLoseCheck(GameLossReason.Milled)) {
int numQuill = votes.get("Quill").size();
int numQuill = votes.get(quill).size();
if (numQuill + 1 >= controller.getCardsIn(ZoneType.Library).size()) {
return controller.isCardInPlay("Laboratory Maniac") ? "Feather" : "Quill";
return controller.isCardInPlay("Laboratory Maniac") ? feather : quill;
}
}
// is it can't receive counters, choose +1/+1 ones
if (!source.canReceiveCounters(p1p1Type)) {
return opponent ? "Feather" : "Quill";
return opponent ? feather : quill;
}
// if source is not on the battlefield anymore, choose +1/+1 ones
if (!game.getCardState(source).isInPlay()) {
return opponent ? "Feather" : "Quill";
return opponent ? feather : quill;
}
// if no hand cards, try to mill opponent
if (controller.getCardsIn(ZoneType.Hand).isEmpty()) {
return opponent ? "Quill" : "Feather";
return opponent ? quill : feather;
}
// AI has something to discard
if (ai.equals(controller)) {
CardCollectionView aiCardsInHand = ai.getCardsIn(ZoneType.Hand);
if (CardLists.count(aiCardsInHand, CardPredicates.hasSVar("DiscardMe")) >= 1) {
return "Quill";
return quill;
}
}
// default card draw and discard are better than +1/+1 counter
return opponent ? "Feather" : "Quill";
return opponent ? feather : quill;
case "StrengthOrNumbers":
SpellAbility strength = (SpellAbility)options.get(0);
SpellAbility numbers = (SpellAbility)options.get(1);
// similar to fabricate choose +1/+1 or Token
final SpellAbility saToken = sa.findSubAbilityByType(ApiType.Token);
int numStrength = votes.get("Strength").size();
int numNumbers = votes.get("Numbers").size();
int numStrength = votes.get(strength).size();
int numNumbers = votes.get(numbers).size();
Card token = TokenAi.spawnToken(controller, saToken);
Card token = TokenAi.spawnToken(controller, numbers);
// is it can't receive counters, choose +1/+1 ones
if (!source.canReceiveCounters(p1p1Type)) {
return opponent ? "Strength" : "Numbers";
return opponent ? strength : numbers;
}
// if source is not on the battlefield anymore
if (!game.getCardState(source).isInPlay()) {
return opponent ? "Strength" : "Numbers";
return opponent ? strength : numbers;
}
// token would not survive
if (token == null || !token.isCreature() || token.getNetToughness() < 1) {
return opponent ? "Numbers" : "Strength";
return opponent ? numbers : strength;
}
// TODO check for ETB to +1/+1 counters or over another trigger like lifegain
@@ -2662,35 +2644,40 @@ public class ComputerUtil {
int scoreStrength = ComputerUtilCard.evaluateCreature(sourceStrength) + tokenScore * numNumbers;
int scoreNumbers = ComputerUtilCard.evaluateCreature(sourceNumbers) + tokenScore * (numNumbers + 1);
return (scoreNumbers >= scoreStrength) != opponent ? "Numbers" : "Strength";
return (scoreNumbers >= scoreStrength) != opponent ? numbers : strength;
case "SproutOrHarvest":
SpellAbility sprout = (SpellAbility)options.get(0);
SpellAbility harvest = (SpellAbility)options.get(1);
// lifegain would hurt or has no effect
if (opponent) {
if (lifegainNegative(controller, source)) {
return "Harvest";
return harvest;
}
} else {
if (lifegainNegative(controller, source)) {
return "Sprout";
return sprout;
}
}
// is it can't receive counters, choose +1/+1 ones
if (!source.canReceiveCounters(p1p1Type)) {
return opponent ? "Sprout" : "Harvest";
return opponent ? sprout : harvest;
}
// if source is not on the battlefield anymore
if (!game.getCardState(source).isInPlay()) {
return opponent ? "Sprout" : "Harvest";
return opponent ? sprout : harvest;
}
// TODO add Lifegain to +1/+1 counters trigger
// for now +1/+1 counters are better
return opponent ? "Harvest" : "Sprout";
return opponent ? harvest : sprout;
case "DeathOrTaxes":
int numDeath = votes.get("Death").size();
int numTaxes = votes.get("Taxes").size();
SpellAbility death = (SpellAbility)options.get(0);
SpellAbility taxes = (SpellAbility)options.get(1);
int numDeath = votes.get(death).size();
int numTaxes = votes.get(taxes).size();
if (opponent) {
CardCollection aiCreatures = ai.getCreaturesInPlay();
@@ -2698,29 +2685,29 @@ public class ComputerUtil {
// would need to sacrifice more creatures than AI has
// sacrifice even more
if (aiCreatures.size() <= numDeath) {
return "Death";
return death;
}
// would need to discard more cards than it has
if (aiCardsInHand.size() <= numTaxes) {
return "Taxes";
return taxes;
}
// has cards with SacMe or Token
if (CardLists.count(aiCreatures, CardPredicates.hasSVar("SacMe").or(CardPredicates.TOKEN)) >= numDeath) {
return "Death";
return death;
}
// has cards with DiscardMe
if (CardLists.count(aiCardsInHand, CardPredicates.hasSVar("DiscardMe")) >= numTaxes) {
return "Taxes";
return taxes;
}
// discard is probably less worse than sacrifice
return "Taxes";
return taxes;
} else {
// ai is first voter or ally of controller
// both are not affected, but if opponents control creatures, sacrifice is worse
return controller.getOpponents().getCreaturesInPlay().isEmpty() ? "Taxes" : "Death";
return controller.getOpponents().getCreaturesInPlay().isEmpty() ? taxes : death;
}
default:
return Iterables.getFirst(options, null);
@@ -3098,41 +3085,38 @@ public class ComputerUtil {
public static CardCollection filterAITgts(SpellAbility sa, Player ai, CardCollection srcList, boolean alwaysStrict) {
final Card source = sa.getHostCard();
if (source == null) { return srcList; }
if (sa.hasParam("AITgts")) {
CardCollection list;
String aiTgts = sa.getParam("AITgts");
if (aiTgts.startsWith("BetterThan")) {
int value = 0;
if (aiTgts.endsWith("Source")) {
value = ComputerUtilCard.evaluateCreature(source);
if (source.isEnchanted()) {
for (Card enc : source.getEnchantedBy()) {
if (enc.getController().equals(ai)) {
value += 100; // is 100 per AI's own aura enough?
}
}
}
} else if (aiTgts.contains("EvalRating.")) {
value = AbilityUtils.calculateAmount(source, aiTgts.substring(aiTgts.indexOf(".") + 1), sa);
} else {
System.err.println("Warning: Unspecified AI target evaluation rating for SA " + sa);
value = ComputerUtilCard.evaluateCreature(source);
}
final int totalValue = value;
list = CardLists.filter(srcList, c -> ComputerUtilCard.evaluateCreature(c) > totalValue + 30);
} else {
list = CardLists.getValidCards(srcList, sa.getParam("AITgts"), sa.getActivatingPlayer(), source, sa);
}
if (!list.isEmpty() || sa.hasParam("AITgtsStrict") || alwaysStrict) {
return list;
} else {
return srcList;
}
if (source == null || !sa.hasParam("AITgts")) {
return srcList;
}
CardCollection list;
String aiTgts = sa.getParam("AITgts");
if (aiTgts.startsWith("BetterThan")) {
int value = 0;
if (aiTgts.endsWith("Source")) {
value = ComputerUtilCard.evaluateCreature(source);
if (source.isEnchanted()) {
for (Card enc : source.getEnchantedBy()) {
if (enc.getController().equals(ai)) {
value += 100; // is 100 per AI's own aura enough?
}
}
}
} else if (aiTgts.contains("EvalRating.")) {
value = AbilityUtils.calculateAmount(source, aiTgts.substring(aiTgts.indexOf(".") + 1), sa);
} else {
System.err.println("Warning: Unspecified AI target evaluation rating for SA " + sa);
value = ComputerUtilCard.evaluateCreature(source);
}
final int totalValue = value;
list = CardLists.filter(srcList, c -> ComputerUtilCard.evaluateCreature(c) > totalValue + 30);
} else {
list = CardLists.getValidCards(srcList, sa.getParam("AITgts"), sa.getActivatingPlayer(), source, sa);
}
if (!list.isEmpty() || sa.hasParam("AITgtsStrict") || alwaysStrict) {
return list;
}
return srcList;
}

View File

@@ -500,7 +500,7 @@ public class ComputerUtilCard {
}
List<Card> lands = CardLists.filter(list, CardPredicates.LANDS);
if (lands.size() > 6) {
if (lands.size() > 6 || lands.size() == Iterables.size(list)) {
return getWorstLand(lands);
}
@@ -919,14 +919,14 @@ public class ComputerUtilCard {
return MagicColor.Constant.WHITE; // no difference, there was no prominent color
}
public static String getMostProminentColor(final CardCollectionView list, final List<String> restrictedToColors) {
public static String getMostProminentColor(final CardCollectionView list, final Iterable<String> restrictedToColors) {
byte colors = CardFactoryUtil.getMostProminentColorsFromList(list, restrictedToColors);
for (byte c : MagicColor.WUBRG) {
if ((colors & c) != 0) {
return MagicColor.toLongString(c);
}
}
return restrictedToColors.get(0); // no difference, there was no prominent color
return Iterables.get(restrictedToColors, 0); // no difference, there was no prominent color
}
public static List<String> getColorByProminence(final List<Card> list) {
@@ -1060,7 +1060,6 @@ public class ComputerUtilCard {
public static boolean useRemovalNow(final SpellAbility sa, final Card c, final int dmg, ZoneType destination) {
final Player ai = sa.getActivatingPlayer();
final AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
final Game game = ai.getGame();
final PhaseHandler ph = game.getPhaseHandler();
final PhaseType phaseType = ph.getPhase();
@@ -1207,7 +1206,7 @@ public class ComputerUtilCard {
}
} else if (c.isPlaneswalker()) {
threat = 1;
} else if (aic.getBooleanProperty(AiProps.ACTIVELY_DESTROY_ARTS_AND_NONAURA_ENCHS) && ((c.isArtifact() && !c.isCreature()) || (c.isEnchantment() && !c.isAura()))) {
} else if (AiProfileUtil.getBoolProperty(ai, AiProps.ACTIVELY_DESTROY_ARTS_AND_NONAURA_ENCHS) && ((c.isArtifact() && !c.isCreature()) || (c.isEnchantment() && !c.isAura()))) {
// non-creature artifacts and global enchantments with suspicious intrinsic abilities
boolean priority = false;
if (c.getOwner().isOpponentOf(ai) && c.getController().isOpponentOf(ai)) {
@@ -1311,7 +1310,7 @@ public class ComputerUtilCard {
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
simAI = aic.usesSimulation();
if (!simAI) {
holdCombatTricks = aic.getBooleanProperty(AiProps.TRY_TO_HOLD_COMBAT_TRICKS_UNTIL_BLOCK);
holdCombatTricks = aic.getBoolProperty(AiProps.TRY_TO_HOLD_COMBAT_TRICKS_UNTIL_BLOCK);
chanceToHoldCombatTricks = aic.getIntProperty(AiProps.CHANCE_TO_HOLD_COMBAT_TRICKS_UNTIL_BLOCK);
}
}
@@ -1640,7 +1639,7 @@ public class ComputerUtilCard {
// if we got here, Berserk will result in the pumped creature dying at EOT and the opponent will not lose
// (other similar cards with AILogic$ Berserk that do not die only when attacking are excluded from consideration)
if (ai.getController() instanceof PlayerControllerAi) {
boolean aggr = ((PlayerControllerAi) ai.getController()).getAi().getBooleanProperty(AiProps.USE_BERSERK_AGGRESSIVELY)
boolean aggr = ((PlayerControllerAi) ai.getController()).getAi().getBoolProperty(AiProps.USE_BERSERK_AGGRESSIVELY)
|| sa.hasParam("AtEOT");
if (!aggr) {
return false;
@@ -1907,17 +1906,10 @@ public class ComputerUtilCard {
return oppCards;
}
boolean enablePriorityRemoval = false;
boolean priorityRemovalOnlyInDanger = false;
int priorityRemovalThreshold = 0;
int lifeInDanger = 5;
if (ai.getController().isAI()) {
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
enablePriorityRemoval = aic.getBooleanProperty(AiProps.ACTIVELY_DESTROY_IMMEDIATELY_UNBLOCKABLE);
priorityRemovalThreshold = aic.getIntProperty(AiProps.DESTROY_IMMEDIATELY_UNBLOCKABLE_THRESHOLD);
priorityRemovalOnlyInDanger = aic.getBooleanProperty(AiProps.DESTROY_IMMEDIATELY_UNBLOCKABLE_ONLY_IN_DNGR);
lifeInDanger = aic.getIntProperty(AiProps.DESTROY_IMMEDIATELY_UNBLOCKABLE_LIFE_IN_DNGR);
}
boolean enablePriorityRemoval = AiProfileUtil.getBoolProperty(ai, AiProps.ACTIVELY_DESTROY_IMMEDIATELY_UNBLOCKABLE);
int priorityRemovalThreshold = AiProfileUtil.getIntProperty(ai, AiProps.DESTROY_IMMEDIATELY_UNBLOCKABLE_THRESHOLD);
boolean priorityRemovalOnlyInDanger = AiProfileUtil.getBoolProperty(ai, AiProps.DESTROY_IMMEDIATELY_UNBLOCKABLE_ONLY_IN_DNGR);
int lifeInDanger = AiProfileUtil.getIntProperty(ai, AiProps.DESTROY_IMMEDIATELY_UNBLOCKABLE_LIFE_IN_DNGR);
if (!enablePriorityRemoval) {
// Nothing to do here, the profile does not allow prioritizing

View File

@@ -177,16 +177,16 @@ public class ComputerUtilCombat {
public static int damageIfUnblocked(final Card attacker, final GameEntity attacked, final Combat combat, boolean withoutAbilities) {
int damage = attacker.getNetCombatDamage();
int sum = 0;
if (attacked instanceof Player player && !player.canLoseLife()) {
return 0;
}
// ask ReplacementDamage directly
if (isCombatDamagePrevented(attacker, attacked, damage)) {
if (attacked instanceof Player p && !p.canLoseLife()) {
return 0;
}
if (!attacker.hasKeyword(Keyword.INFECT)) {
// ask ReplacementDamage directly
if (isCombatDamagePrevented(attacker, attacked, damage)) {
return 0;
}
damage += predictPowerBonusOfAttacker(attacker, null, combat, withoutAbilities);
sum = predictDamageTo(attacked, damage, attacker, true);
if (attacker.hasDoubleStrike()) {
@@ -443,13 +443,12 @@ public class ComputerUtilCombat {
}
}
int threshold = 0;
int maxTreshold = 0;
if (ai.getController().isAI()) {
threshold = ((PlayerControllerAi) ai.getController()).getAi().getIntProperty(AiProps.AI_IN_DANGER_THRESHOLD);
maxTreshold = ((PlayerControllerAi) ai.getController()).getAi().getIntProperty(AiProps.AI_IN_DANGER_MAX_THRESHOLD) - threshold;
if (resultingPoison(ai, combat) > Math.max(7, ai.getPoisonCounters())) {
return true;
}
int threshold = AiProfileUtil.getIntProperty(ai, AiProps.AI_IN_DANGER_THRESHOLD);
int maxTreshold = AiProfileUtil.getIntProperty(ai, AiProps.AI_IN_DANGER_MAX_THRESHOLD) - threshold;
int chance = MyRandom.getRandom().nextInt(80) + 5;
while (maxTreshold > 0) {
if (MyRandom.getRandom().nextInt(100) < chance) {
@@ -458,10 +457,6 @@ public class ComputerUtilCombat {
maxTreshold--;
}
if (resultingPoison(ai, combat) > Math.max(7, ai.getPoisonCounters())) {
return true;
}
return !ai.cantLoseForZeroOrLessLife() && lifeThatWouldRemain(ai, combat) - payment < Math.min(threshold, ai.getLife());
}
@@ -974,17 +969,13 @@ public class ComputerUtilCombat {
continue;
}
int pBonus = 0;
if (ability.getApi() == ApiType.Pump) {
if (!ability.hasParam("NumAtt")) {
continue;
}
if (ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumAtt"), ability);
if (pBonus > 0) {
power += pBonus;
}
}
pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumAtt"), ability);
} else if (ability.getApi() == ApiType.PutCounter) {
if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) {
continue;
@@ -998,12 +989,11 @@ public class ComputerUtilCombat {
continue;
}
if (ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
if (pBonus > 0) {
power += pBonus;
}
}
pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
}
if (pBonus > 0 && ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
power += pBonus;
}
}
@@ -1107,17 +1097,13 @@ public class ComputerUtilCombat {
continue;
}
int tBonus = 0;
if (ability.getApi() == ApiType.Pump) {
if (!ability.hasParam("NumDef")) {
continue;
}
if (ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
int tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumDef"), ability);
if (tBonus > 0) {
toughness += tBonus;
}
}
tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumDef"), ability);
} else if (ability.getApi() == ApiType.PutCounter) {
if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) {
continue;
@@ -1131,12 +1117,11 @@ public class ComputerUtilCombat {
continue;
}
if (ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
int tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
if (tBonus > 0) {
toughness += tBonus;
}
}
tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
}
if (tBonus > 0 && ComputerUtilCost.canPayCost(ability, blocker.getController(), false)) {
toughness += tBonus;
}
}
return toughness;
@@ -1305,6 +1290,7 @@ public class ComputerUtilCombat {
continue;
}
int pBonus = 0;
if (ability.getApi() == ApiType.Pump) {
if (!ability.hasParam("NumAtt")) {
continue;
@@ -1314,11 +1300,8 @@ public class ComputerUtilCombat {
continue;
}
if (!ability.getPayCosts().hasTapCost() && ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) {
int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumAtt"), ability);
if (pBonus > 0) {
power += pBonus;
}
if (!ability.getPayCosts().hasTapCost()) {
pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumAtt"), ability);
}
} else if (ability.getApi() == ApiType.PutCounter) {
if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) {
@@ -1333,13 +1316,14 @@ public class ComputerUtilCombat {
continue;
}
if (!ability.getPayCosts().hasTapCost() && ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) {
int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
if (pBonus > 0) {
power += pBonus;
}
if (!ability.getPayCosts().hasTapCost()) {
pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
}
}
if (pBonus > 0 && ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) {
power += pBonus;
}
}
return power;
}
@@ -1444,7 +1428,11 @@ public class ComputerUtilCombat {
int damage = AbilityUtils.calculateAmount(source, sa.getParam("NumDmg"), sa);
toughness -= predictDamageTo(attacker, damage, source, false);
continue;
} else if (sa.getApi() == ApiType.EachDamage && "TriggeredAttackerLKICopy".equals(sa.getParam("Defined"))) {
List<Card> valid = CardLists.getValidCards(source.getController().getCreaturesInPlay(), sa.getParam("ValidCards"), source.getController(), source, sa);
// TODO: this assumes that 1 damage is dealt per creature. Improve this to check the parameter/X to determine
// how much damage is dealt by each of the creatures in the valid list.
toughness -= valid.size();
} else if (ApiType.Pump.equals(sa.getApi())) {
if (!sa.hasParam("NumDef")) {
continue;
@@ -1530,16 +1518,14 @@ public class ComputerUtilCombat {
if (ability.getPayCosts().hasTapCost() && !attacker.hasKeyword(Keyword.VIGILANCE)) {
continue;
}
if (!ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) {
continue;
}
int tBonus = 0;
if (ability.getApi() == ApiType.Pump) {
if (!ability.hasParam("NumDef")) {
continue;
}
toughness += AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumDef"), ability, true);
tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("NumDef"), ability, true);
} else if (ability.getApi() == ApiType.PutCounter) {
if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) {
continue;
@@ -1553,10 +1539,11 @@ public class ComputerUtilCombat {
continue;
}
int tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
if (tBonus > 0) {
toughness += tBonus;
}
tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParamOrDefault("CounterNum", "1"), ability);
}
if (tBonus > 0 && ComputerUtilCost.canPayCost(ability, attacker.getController(), false)) {
toughness += tBonus;
}
}
return toughness;
@@ -2020,35 +2007,35 @@ public class ComputerUtilCombat {
*
* @param self
* a {@link forge.game.player.Player} object.
* @param attacker
* @param combatant
* a {@link forge.game.card.Card} object.
* @param block
* @param opposedCombatants
* @param dmgCanDeal
* a int.
* @param defender
* @param overrideOrder overriding combatant order
*/
public static Map<Card, Integer> distributeAIDamage(final Player self, final Card attacker, final CardCollectionView block, final CardCollectionView remaining, int dmgCanDeal, GameEntity defender, boolean overrideOrder) {
public static Map<Card, Integer> distributeAIDamage(final Player self, final Card combatant, CardCollectionView opposedCombatants, final CardCollectionView remaining, int dmgCanDeal, GameEntity defender, boolean overrideOrder) {
Map<Card, Integer> damageMap = Maps.newHashMap();
Combat combat = attacker.getGame().getCombat();
Combat combat = combatant.getGame().getCombat();
boolean isAttacking = defender != null;
// Check for Banding, Defensive Formation
boolean isAttackingMe = isAttacking && combat.getDefenderPlayerByAttacker(attacker).equals(self);
boolean isBlockingMyBand = attacker.getController().isOpponentOf(self) && AttackingBand.isValidBand(block, true);
boolean isAttackingMe = isAttacking && combat.getDefenderPlayerByAttacker(combatant).equals(self);
boolean isBlockingMyBand = combatant.getController().isOpponentOf(self) && AttackingBand.isValidBand(opposedCombatants, true);
final boolean aiDistributesBandingDmg = isAttackingMe || isBlockingMyBand;
final boolean hasTrample = attacker.hasKeyword(Keyword.TRAMPLE);
final boolean hasTrample = combatant.hasKeyword(Keyword.TRAMPLE);
if (combat != null && remaining != null && hasTrample && attacker.isAttacking() && !aiDistributesBandingDmg) {
if (combat != null && remaining != null && hasTrample && combatant.isAttacking() && !aiDistributesBandingDmg) {
// if attacker has trample and some of its blockers are also blocking others it's generally a good idea
// to assign those without trample first so we can maximize the damage to the defender
for (final Card c : remaining) {
if (c == attacker || c.hasKeyword(Keyword.TRAMPLE)) {
if (c == combatant || c.hasKeyword(Keyword.TRAMPLE)) {
continue;
}
final CardCollection sharedBlockers = new CardCollection(block);
final CardCollection sharedBlockers = new CardCollection(opposedCombatants);
sharedBlockers.retainAll(combat.getBlockers(c));
if (!sharedBlockers.isEmpty()) {
// signal skip for now
@@ -2058,12 +2045,21 @@ public class ComputerUtilCombat {
// TODO sort remaining tramplers for DamageDone triggers
}
if (block.size() == 1) {
final Card blocker = block.getFirst();
// Order the combatants in preferred order in case legacy ordering is disabled
if (isAttacking && overrideOrder) {
if (combatant.isAttacking()) {
opposedCombatants = AiBlockController.orderBlockers(combatant, new CardCollection(opposedCombatants));
} else {
opposedCombatants = AiBlockController.orderAttackers(combatant, new CardCollection(opposedCombatants));
}
}
if (opposedCombatants.size() == 1) {
final Card blocker = opposedCombatants.getFirst();
int dmgToBlocker = dmgCanDeal;
if (hasTrample && isAttacking && !aiDistributesBandingDmg) { // otherwise no entity to deliver damage via trample
dmgToBlocker = getEnoughDamageToKill(blocker, dmgCanDeal, attacker, true);
dmgToBlocker = getEnoughDamageToKill(blocker, dmgCanDeal, combatant, true);
if (dmgCanDeal < dmgToBlocker) {
// can't kill so just put the lowest legal amount
@@ -2082,9 +2078,9 @@ public class ComputerUtilCombat {
// Does the attacker deal lethal damage to all blockers
//Blocking Order now determined after declare blockers
Card lastBlocker = null;
for (final Card b : block) {
for (final Card b : opposedCombatants) {
lastBlocker = b;
final int dmgToKill = getEnoughDamageToKill(b, dmgCanDeal, attacker, true);
final int dmgToKill = getEnoughDamageToKill(b, dmgCanDeal, combatant, true);
if (dmgToKill <= dmgCanDeal) {
damageMap.put(b, dmgToKill);
dmgCanDeal -= dmgToKill;
@@ -2109,15 +2105,15 @@ public class ComputerUtilCombat {
} else {
// In the event of Banding or Defensive Formation, assign max damage to the blocker who
// can tank all the damage or to the worst blocker to lose as little as possible
for (final Card b : block) {
final int dmgToKill = getEnoughDamageToKill(b, dmgCanDeal, attacker, true);
for (final Card b : opposedCombatants) {
final int dmgToKill = getEnoughDamageToKill(b, dmgCanDeal, combatant, true);
if (dmgToKill > dmgCanDeal) {
damageMap.put(b, dmgCanDeal);
break;
}
}
if (damageMap.isEmpty()) {
damageMap.put(ComputerUtilCard.getWorstCreatureAI(block), dmgCanDeal);
damageMap.put(ComputerUtilCard.getWorstCreatureAI(opposedCombatants), dmgCanDeal);
}
}
return damageMap;

View File

@@ -356,7 +356,7 @@ public class ComputerUtilCost {
final boolean beforeNextTurn = ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN) && ai.getGame().getPhaseHandler().getNextTurn().equals(ai) && ComputerUtilCard.evaluateCreature(source) <= 150;
final boolean creatureInDanger = ComputerUtil.predictCreatureWillDieThisTurn(ai, source, sourceAbility, false)
&& !ComputerUtilCombat.willOpposingCreatureDieInCombat(ai, source, combat);
final int lifeThreshold = ai.getController().isAI() ? (((PlayerControllerAi) ai.getController()).getAi().getIntProperty(AiProps.AI_IN_DANGER_THRESHOLD)) : 4;
final int lifeThreshold = AiProfileUtil.getIntProperty(ai, AiProps.AI_IN_DANGER_THRESHOLD);
final boolean aiInDanger = ai.getLife() <= lifeThreshold && ai.canLoseLife() && !ai.cantLoseForZeroOrLessLife();
if (creatureInDanger && !ComputerUtilCombat.isDangerousToSacInCombat(ai, source, combat)) {
return true;

View File

@@ -287,7 +287,9 @@ public class ComputerUtilMana {
continue;
}
if (!ComputerUtilCost.checkTapTypeCost(ai, ma.getPayCosts(), ma.getHostCard(), sa, AiCardMemory.getMemorySet(ai, MemorySet.PAYS_TAP_COST))) {
int amount = ma.hasParam("Amount") ? AbilityUtils.calculateAmount(ma.getHostCard(), ma.getParam("Amount"), ma) : 1;
if (amount <= 0) {
// wrong gamestate for variable amount
continue;
}
@@ -351,9 +353,14 @@ public class ComputerUtilMana {
continue;
}
// these should come last since they reserve the paying cards
// (this means if a mana ability has both parts it doesn't currently undo reservations if the second part fails)
if (!ComputerUtilCost.checkForManaSacrificeCost(ai, ma.getPayCosts(), ma, ma.isTrigger())) {
continue;
}
if (!ComputerUtilCost.checkTapTypeCost(ai, ma.getPayCosts(), ma.getHostCard(), sa, AiCardMemory.getMemorySet(ai, MemorySet.PAYS_TAP_COST))) {
continue;
}
return paymentChoice;
}
@@ -443,7 +450,6 @@ public class ComputerUtilMana {
manaProduced = manaProduced.replace(s, color);
}
} else if (saMana.hasParam("ReplaceColor")) {
// replace color
String color = saMana.getParam("ReplaceColor");
if ("Chosen".equals(color)) {
if (card.hasChosenColor()) {
@@ -481,14 +487,14 @@ public class ComputerUtilMana {
String originalProduced = manaProduced.toString();
if (originalProduced.isEmpty()) {
return manaProduced.toString();
return originalProduced;
}
// Run triggers like Nissa
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromCard(hostCard);
runParams.put(AbilityKey.Activator, ai); // assuming AI would only ever gives itself mana
runParams.put(AbilityKey.AbilityMana, saPayment);
runParams.put(AbilityKey.Produced, manaProduced.toString());
runParams.put(AbilityKey.Produced, originalProduced);
for (Trigger tr : ai.getGame().getTriggerHandler().getActiveTrigger(TriggerType.TapsForMana, runParams)) {
SpellAbility trSA = tr.ensureAbility();
if (trSA == null) {
@@ -704,9 +710,9 @@ public class ComputerUtilMana {
if (hasConverge &&
(toPay == ManaCostShard.GENERIC || toPay == ManaCostShard.X)) {
final int unpaidColors = cost.getUnpaidColors() + cost.getColorsPaid() ^ ManaCostShard.COLORS_SUPERPOSITION;
for (final byte b : ColorSet.fromMask(unpaidColors)) {
for (final MagicColor.Color b : ColorSet.fromMask(unpaidColors)) {
// try and pay other colors for converge
final ManaCostShard shard = ManaCostShard.valueOf(b);
final ManaCostShard shard = ManaCostShard.valueOf(b.getColorMask());
saList = sourcesForShards.get(shard);
if (saList != null && !saList.isEmpty()) {
toPay = shard;
@@ -735,7 +741,8 @@ public class ComputerUtilMana {
if (saPayment != null && ComputerUtilCost.isSacrificeSelfCost(saPayment.getPayCosts())) {
if (sa.getTargets() != null && sa.getTargets().contains(saPayment.getHostCard())) {
saExcludeList.add(saPayment); // not a good idea to sac a card that you're targeting with the SA you're paying for
// not a good idea to sac a card that you're targeting with the SA you're paying for
saExcludeList.add(saPayment);
continue;
}
}
@@ -809,11 +816,11 @@ public class ComputerUtilMana {
String manaProduced = predictManafromSpellAbility(saPayment, ai, toPay);
payMultipleMana(cost, manaProduced, ai);
// remove from available lists
// remove to prevent re-usage since resources don't get consumed
sourcesForShards.values().removeIf(CardTraitPredicates.isHostCard(saPayment.getHostCard()));
} else {
final CostPayment pay = new CostPayment(saPayment.getPayCosts(), saPayment);
if (!pay.payComputerCosts(new AiCostDecision(ai, saPayment, effect))) {
if (!pay.payComputerCosts(new AiCostDecision(ai, saPayment, effect, true))) {
saList.remove(saPayment);
continue;
}
@@ -822,8 +829,10 @@ public class ComputerUtilMana {
// subtract mana from mana pool
manapool.payManaFromAbility(sa, cost, saPayment);
// no need to remove abilities from resource map,
// once their costs are paid and consume resources, they can not be used again
// need to consider if another use is now prevented
if (!cost.isPaid() && saPayment.isActivatedAbility() && !saPayment.getRestrictions().canPlay(saPayment.getHostCard(), saPayment)) {
sourcesForShards.values().removeIf(s -> s == saPayment);
}
if (hasConverge) {
// hack to prevent converge re-using sources
@@ -887,7 +896,8 @@ public class ComputerUtilMana {
if (hasConverge) {
// add extra colors for paying converge
final int unpaidColors = cost.getUnpaidColors() + cost.getColorsPaid() ^ ManaCostShard.COLORS_SUPERPOSITION;
for (final byte b : ColorSet.fromMask(unpaidColors)) {
for (final MagicColor.Color color : ColorSet.fromMask(unpaidColors)) {
final byte b = color.getColorMask();
final ManaCostShard shard = ManaCostShard.valueOf(b);
if (!sourcesForShards.containsKey(shard)) {
if (ai.getManaPool().canPayForShardWithColor(shard, b)) {
@@ -914,7 +924,7 @@ public class ComputerUtilMana {
ColorSet shared = ColorSet.fromMask(toPay.getColorMask()).getSharedColors(ColorSet.fromNames(m.getComboColors(saPayment).split(" ")));
// but other effects might still lead to a more permissive payment
if (!shared.isColorless()) {
m.setExpressChoice(ColorSet.fromMask(shared.iterator().next()));
m.setExpressChoice(shared.iterator().next().getShortName());
}
getComboManaChoice(ai, saPayment, sa, cost);
}
@@ -1089,7 +1099,7 @@ public class ComputerUtilMana {
// * pay hybrids
// * pay phyrexian, keep mana for colorless
// * pay generic
return cost.getShardToPayByPriority(shardsToPay, ColorSet.ALL_COLORS.getColor());
return cost.getShardToPayByPriority(shardsToPay, ColorSet.WUBRG.getColor());
}
private static void adjustManaCostToAvoidNegEffects(ManaCostBeingPaid cost, final Card card, Player ai) {
@@ -1282,11 +1292,11 @@ public class ComputerUtilMana {
* @return ManaCost
*/
public static ManaCostBeingPaid calculateManaCost(final Cost cost, final SpellAbility sa, final boolean test, final int extraMana, final boolean effect) {
Card card = sa.getHostCard();
Card host = sa.getHostCard();
Zone castFromBackup = null;
if (test && sa.isSpell() && !card.isInZone(ZoneType.Stack)) {
castFromBackup = card.getCastFrom();
card.setCastFrom(card.getZone() != null ? card.getZone() : null);
if (test && sa.isSpell() && !host.isInZone(ZoneType.Stack)) {
castFromBackup = host.getCastFrom();
host.setCastFrom(host.getZone() != null ? host.getZone() : null);
}
Cost payCosts;
@@ -1309,7 +1319,7 @@ public class ComputerUtilMana {
final int multiplicator = Math.max(xCounter, 1);
manaToAdd = extraMana * multiplicator;
} else {
manaToAdd = AbilityUtils.calculateAmount(card, sa.getParamOrDefault("XAlternative", "X"), sa) * xCounter;
manaToAdd = AbilityUtils.calculateAmount(host, sa.getParamOrDefault("XAlternative", "X"), sa) * xCounter;
}
if (manaToAdd < 1 && payCosts != null && payCosts.getCostMana().getXMin() > 0) {
@@ -1321,7 +1331,7 @@ public class ComputerUtilMana {
if (xColor == null) {
xColor = "1";
}
if (card.hasKeyword("Spend only colored mana on X. No more than one mana of each color may be spent this way.")) {
if (host.hasKeyword("Spend only colored mana on X. No more than one mana of each color may be spent this way.")) {
xColor = "WUBRGX";
}
if (xCounter > 0) {
@@ -1346,14 +1356,14 @@ public class ComputerUtilMana {
mCost = ManaCost.combine(mCost, mkCost);
ManaCostBeingPaid mcbp = new ManaCostBeingPaid(mCost);
if (!canPayManaCost(mcbp, sa, sa.getActivatingPlayer(), true)) {
sa.getHostCard().setSVar("NumTimes", "Number$" + i);
host.setSVar("NumTimes", "Number$" + i);
break;
}
}
}
if (test && sa.isSpell()) {
sa.getHostCard().setCastFrom(castFromBackup);
if (test && sa.isSpell() && !host.isInZone(ZoneType.Stack)) {
host.setCastFrom(castFromBackup);
}
return manaCost;
@@ -1496,7 +1506,7 @@ public class ComputerUtilMana {
}
if (!cost.isReusuableResource()) {
for(CostPart part : cost.getCostParts()) {
for (CostPart part : cost.getCostParts()) {
if (part instanceof CostSacrifice && !part.payCostFromSource()) {
unpreferredCost = true;
}
@@ -1586,11 +1596,10 @@ public class ComputerUtilMana {
}
// don't use abilities with dangerous drawbacks
// TODO this has already been checked earlier
AbilitySub sub = m.getSubAbility();
if (sub != null) {
if (!SpellApiToAi.Converter.get(sub).chkDrawbackWithSubs(ai, sub).willingToPlay()) {
continue;
}
if (sub != null && !SpellApiToAi.Converter.get(sub).chkDrawbackWithSubs(ai, sub).willingToPlay()) {
continue;
}
manaMap.get(ManaAtom.GENERIC).add(m); // add to generic source list
@@ -1658,7 +1667,6 @@ public class ComputerUtilMana {
if (replaced.contains("C")) {
manaMap.put(ManaAtom.COLORLESS, m);
}
}
}
}

View File

@@ -264,12 +264,14 @@ public abstract class GameState {
}
if (c.hasMergedCard()) {
String suffix = c.getTopMergedCard().hasPaperFoil() ? "+" : "";
// we have to go by the current top card name here
newText.append(c.getTopMergedCard().getPaperCard().getName()).append("|Set:")
newText.append(c.getTopMergedCard().getPaperCard().getName()).append(suffix).append("|Set:")
.append(c.getTopMergedCard().getPaperCard().getEdition()).append("|Art:")
.append(c.getTopMergedCard().getPaperCard().getArtIndex());
} else {
newText.append(c.getPaperCard().getName()).append("|Set:").append(c.getPaperCard().getEdition())
String suffix = c.hasPaperFoil() ? "+" : "";
newText.append(c.getPaperCard().getName()).append(suffix).append("|Set:").append(c.getPaperCard().getEdition())
.append("|Art:").append(c.getPaperCard().getArtIndex());
}
}
@@ -319,18 +321,21 @@ public abstract class GameState {
newText.append(":Cloaked");
}
}
if (c.getCurrentStateName().equals(CardStateName.Transformed)) {
newText.append("|Transformed");
} else if (c.getCurrentStateName().equals(CardStateName.Flipped)) {
if (c.getCurrentStateName().equals(CardStateName.Flipped)) {
newText.append("|Flipped");
} else if (c.getCurrentStateName().equals(CardStateName.Meld)) {
newText.append("|Meld");
if (c.getMeldedWith() != null) {
String suffix = c.getMeldedWith().hasPaperFoil() ? "+" : "";
newText.append(":");
newText.append(c.getMeldedWith().getName());
newText.append(c.getMeldedWith().getName()).append(suffix);
}
} else if (c.getCurrentStateName().equals(CardStateName.Backside)) {
if (c.isModal()) {
newText.append("|Modal");
} else {
newText.append("|Transformed");
}
} else if (c.getCurrentStateName().equals(CardStateName.Modal)) {
newText.append("|Modal");
}
if (c.getPlayerAttachedTo() != null) {
@@ -1311,8 +1316,8 @@ public abstract class GameState {
if (info.endsWith("Cloaked")) {
c.setCloaked(new SpellAbility.EmptySa(ApiType.Cloak, c));
}
} else if (info.startsWith("Transformed")) {
c.setState(CardStateName.Transformed, true);
} else if (info.startsWith("Transformed") || info.startsWith("Modal")) {
c.setState(CardStateName.Backside, true);
c.setBackSide(true);
} else if (info.startsWith("Flipped")) {
c.setState(CardStateName.Flipped, true);
@@ -1330,9 +1335,6 @@ public abstract class GameState {
}
c.setState(CardStateName.Meld, true);
c.setBackSide(true);
} else if (info.startsWith("Modal")) {
c.setState(CardStateName.Modal, true);
c.setBackSide(true);
}
else if (info.startsWith("OnAdventure")) {
String abAdventure = "DB$ Effect | RememberObjects$ Self | StaticAbilities$ Play | ForgetOnMoved$ Exile | Duration$ Permanent | ConditionDefined$ Self | ConditionPresent$ Card.!copiedSpell";

View File

@@ -139,9 +139,9 @@ public class PlayerControllerAi extends PlayerController {
}
}
boolean sbLimitedFormats = getAi().getBooleanProperty(AiProps.SIDEBOARDING_IN_LIMITED_FORMATS);
boolean sbSharedTypesOnly = getAi().getBooleanProperty(AiProps.SIDEBOARDING_SHARED_TYPE_ONLY);
boolean sbPlaneswalkerException = getAi().getBooleanProperty(AiProps.SIDEBOARDING_PLANESWALKER_EQ_CREATURE);
boolean sbLimitedFormats = getAi().getBoolProperty(AiProps.SIDEBOARDING_IN_LIMITED_FORMATS);
boolean sbSharedTypesOnly = getAi().getBoolProperty(AiProps.SIDEBOARDING_SHARED_TYPE_ONLY);
boolean sbPlaneswalkerException = getAi().getBoolProperty(AiProps.SIDEBOARDING_PLANESWALKER_EQ_CREATURE);
int sbChanceOnWin = getAi().getIntProperty(AiProps.SIDEBOARDING_CHANCE_ON_WIN);
int sbChancePerCard = getAi().getIntProperty(AiProps.SIDEBOARDING_CHANCE_PER_CARD);
@@ -460,7 +460,11 @@ public class PlayerControllerAi extends PlayerController {
@Override
public boolean confirmReplacementEffect(ReplacementEffect replacementEffect, SpellAbility effectSA, GameEntity affected, String question) {
return brains.aiShouldRun(replacementEffect, effectSA, affected);
Card host = replacementEffect.getHostCard();
if (host.hasAlternateState()) {
host = host.getGame().getCardState(host);
}
return brains.aiShouldRun(replacementEffect, effectSA, host, affected);
}
@Override
@@ -1024,13 +1028,13 @@ public class PlayerControllerAi extends PlayerController {
if ((colors.getColor() & chosenColorMask) != 0) {
return chosenColorMask;
}
return Iterables.getFirst(colors, (byte)0);
return Iterables.getFirst(colors, MagicColor.Color.COLORLESS).getColorMask();
}
@Override
public byte chooseColor(String message, SpellAbility sa, ColorSet colors) {
if (colors.countColors() < 2) {
return Iterables.getFirst(colors, MagicColor.WHITE);
return Iterables.getFirst(colors, MagicColor.Color.WHITE).getColorMask();
}
// You may switch on sa.getApi() here and use sa.getParam("AILogic")
CardCollectionView hand = player.getCardsIn(ZoneType.Hand);
@@ -1043,7 +1047,7 @@ public class PlayerControllerAi extends PlayerController {
if ((colors.getColor() & chosenColorMask) != 0) {
return chosenColorMask;
}
return Iterables.getFirst(colors, MagicColor.WHITE);
return Iterables.getFirst(colors, MagicColor.Color.WHITE).getColorMask();
}
@Override
@@ -1270,9 +1274,15 @@ public class PlayerControllerAi extends PlayerController {
}
}
private boolean prepareSingleSa(final Card host, final SpellAbility sa, boolean isMandatory) {
private boolean prepareSingleSa(final Card host, SpellAbility sa, boolean isMandatory) {
if (sa.getApi() == ApiType.Charm) {
return CharmEffect.makeChoices(sa);
if (!CharmEffect.makeChoices(sa)) {
return false;
}
if (!sa.hasParam("Random")) {
return true;
}
sa = sa.getSubAbility();
}
if (sa.hasParam("TargetingPlayer")) {
Player targetingPlayer = AbilityUtils.getDefinedPlayers(host, sa.getParam("TargetingPlayer"), sa).get(0);
@@ -1347,6 +1357,11 @@ public class PlayerControllerAi extends PlayerController {
// Ai won't understand that anyway
}
@Override
public void revealUnsupported(Map<Player, List<PaperCard>> unsupported) {
// Ai won't understand that anyway
}
@Override
public Map<DeckSection, List<? extends PaperCard>> complainCardsCantPlayWell(Deck myDeck) {
// TODO check if profile detection set to Auto
@@ -1357,7 +1372,7 @@ public class PlayerControllerAi extends PlayerController {
@Override
public CardCollectionView cheatShuffle(CardCollectionView list) {
return brains.getBooleanProperty(AiProps.CHEAT_WITH_MANA_ON_SHUFFLE) ? brains.cheatShuffle(list) : list;
return brains.getBoolProperty(AiProps.CHEAT_WITH_MANA_ON_SHUFFLE) ? brains.cheatShuffle(list) : list;
}
@Override
@@ -1529,12 +1544,10 @@ public class PlayerControllerAi extends PlayerController {
}
// Don't choose Tomb of Annihilation when life in danger unless we can win right away or can't lose for 0 life
if (ai.getController().isAI()) { // FIXME: is this needed? Can simulation ever run this for a non-AI player?
int lifeInDanger = (((PlayerControllerAi) ai.getController()).getAi().getIntProperty(AiProps.AI_IN_DANGER_THRESHOLD));
if ((ai.getLife() <= lifeInDanger && !ai.cantLoseForZeroOrLessLife())
&& !(ai.getLife() > 1 && ai.getWeakestOpponent().getLife() == 1)) {
dungeonNames.remove("Tomb of Annihilation");
}
int lifeInDanger = AiProfileUtil.getIntProperty(player, AiProps.AI_IN_DANGER_THRESHOLD);
if ((ai.getLife() <= lifeInDanger && !ai.cantLoseForZeroOrLessLife())
&& !(ai.getLife() > 1 && ai.getWeakestOpponent().getLife() == 1)) {
dungeonNames.remove("Tomb of Annihilation");
}
try {

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