Compare commits

..

169 Commits

Author SHA1 Message Date
Hans Mackowiak
a21656521f Update Card.java
fix
2025-07-07 13:10:09 +02:00
Hans Mackowiak
bdec4095c0 Use ImmutableList.Builder
Use ImmutableList.Builder instead of ImmutableList.of(Iterables.concat())
2025-07-07 12:58:05 +02:00
Eradev
8475456c0b Add missing radiation token (#7948) 2025-07-07 08:51:41 +00:00
Fulgur14
651caccd71 Dawnsire, Sunstar Dreadnought (EOE) (#7958) 2025-07-07 06:18:15 +00:00
kevlahnota
8e9fb8570e Merge pull request #7957 from Eradev/FixEncodingAndroidCompatible
Encode collector number + fallback
2025-07-07 12:55:15 +08:00
Eradev
5a183a6042 Merge branch 'master' into FixEncodingAndroidCompatible 2025-07-07 00:14:02 -04:00
kevlahnota
67e6a7aa1a Merge pull request #7953 from kevlahnota/master2
fix W and S keys for TextField input
2025-07-07 12:09:28 +08:00
Anthony Calosa
b76c67f309 fix comment 2025-07-07 12:08:10 +08:00
Anthony Calosa
99c6b6d815 add comment 2025-07-07 12:06:35 +08:00
Eradev
cadc39699d Encode collector number + fallback 2025-07-07 00:05:37 -04:00
Chris H
8c06aab7b3 Revert "Encode UTF-8 symbols in collector number (#7923)"
This reverts commit fb69f245da.
2025-07-06 22:19:43 -04:00
Anthony Calosa
25c59cd5dd add KeyBinding ispressed condition 2025-07-07 09:37:03 +08:00
Anthony Calosa
9825239e43 fix W and S keys for TextField input 2025-07-07 06:00:04 +08:00
Eradev
fb69f245da Encode UTF-8 symbols in collector number (#7923)
* Fix black star in URL
2025-07-05 22:26:14 +02:00
Robin Woodby
7ef8dddc2a Remove unused import 2025-07-05 14:40:22 -04:00
Robin Woodby
2abcae84b1 Fix issue where card counts from additional printings were ignored 2025-07-05 14:40:22 -04:00
Robin Woodby
b4beb6c182 Remove unnecessary fully qualified names for java.util data structures 2025-07-05 14:40:22 -04:00
Robin Woodby
786d14663b Add copy collection to clipboard button to adventure mode deck editor
Includes translations
2025-07-05 14:40:22 -04:00
tool4ever
f8269e69c4 Fix Rest in Peace making merged cards disappear (#7946)
---------

Co-authored-by: tool4EvEr <tool4EvEr@>
2025-07-05 17:50:40 +02:00
tool4ever
29274d0acf StackInstance cleanup (#7947)
Co-authored-by: tool4EvEr <tool4EvEr@>
2025-07-05 18:15:55 +03:00
Hans Mackowiak
d6b925f171 move file 2025-07-05 17:01:18 +02:00
Hans Mackowiak
f1f18d1823 lf 2025-07-05 17:00:01 +02:00
tool4ever
03ff2147db True-Name Nemesis crashes on "$" Player name (#7944) 2025-07-05 08:54:14 +00:00
Fulgur14
c66a72806d 6 more leaked EOE cards (#7931) 2025-07-05 08:45:51 +00:00
Fulgur14
e89f2cbac0 Two cards from EOE uncommon leaks that I can actually read (#7895) 2025-07-05 08:34:04 +00:00
tool4ever
d831530c50 Tapestry Warden and support (#7943) 2025-07-05 08:31:38 +00:00
Fulgur14
9fe18c2af8 Evendo and Susur Secundi (#7942) 2025-07-05 08:25:45 +00:00
Fulgur14
fe4ff7ac0f 4 EOE leaks (#7941) 2025-07-05 07:26:04 +00:00
Cees Timmerman
a8ca3d8188 Fix 7937: Offspring token key malformed (#7938) 2025-07-05 07:00:06 +00:00
tool4ever
3b9867c537 Fix updateTarget (#7927)
* Fix meld exiling missing half
2025-07-05 06:58:26 +00:00
Fulgur14
01d22c26f4 Create terrasymbiosis.txt (#7936) 2025-07-04 17:03:28 +00:00
kevlahnota
e8f7fe5a95 Merge pull request #7933 from kevlahnota/master2
fix rounded borders for tokens since it gets the full bordered image …
2025-07-04 23:05:21 +08:00
Anthony Calosa
bd0b8fbc65 minor refactor 2025-07-04 22:45:50 +08:00
Anthony Calosa
05e20b4e92 Merge branch 'master' into master2 2025-07-04 22:39:20 +08:00
Anthony Calosa
716d58ad4b faster implementation 2025-07-04 22:28:51 +08:00
Anthony Calosa
ca12ad529a add comment 2025-07-04 21:58:20 +08:00
Anthony Calosa
6eea82706d better name 2025-07-04 21:52:56 +08:00
Anthony Calosa
d69d005ce0 update remaining checks 2025-07-04 21:27:25 +08:00
Anthony Calosa
b98b322dfe move check 2025-07-04 20:33:57 +08:00
Anthony Calosa
e73e72d150 update view check 2025-07-04 20:10:54 +08:00
Anthony Calosa
8cabc244f7 fix rounded borders for tokens since it gets the full bordered image on scryfall by default now 2025-07-04 19:32:50 +08:00
Fulgur14
4f04e5cc13 Infinite Guideline Station (EOE leak) (#7928) 2025-07-04 10:57:41 +02:00
Agetian
bcf9d2585d - Update LDA data for Standard and Modern (Final Fantasy). (#7926) 2025-07-03 21:42:02 +03:00
Fulgur14
e03b07f940 Create command_bridge.txt (#7924) 2025-07-03 16:45:45 +00:00
Eradev
a45c6aa37e Fix kavu deck (#7919)
* FIx Kavu Deck (C11 -> CMD)

* Whitespaces
2025-07-03 07:00:33 +00:00
Agetian
dd672945f2 - Update LDA data for Standard and Modern (Final Fantasy). (#7918) 2025-07-02 21:25:41 +03:00
Fulgur14
e634be4273 Embrace Oblivion (EOE) 2025-07-02 15:55:41 +00:00
Simisays
9acf5bee41 update (#7916) 2025-07-02 18:25:54 +03:00
tool4ever
1e3297ab64 Update spirit_sisters_call.txt
Closes #7912
2025-07-01 22:11:06 +00:00
Simisays
58df290c8e Adventure church map fix (#7911)
* update

* item name fix
2025-07-01 20:17:40 +03:00
tool4ever
e2075886b1 Update celes_rune_knight.txt 2025-07-01 07:42:42 +02:00
Chris H
ad53abc75a Update snapshot-both-pc-android.yml 2025-06-30 18:38:46 -04:00
Chris H
a599c318dd Update snapshot-both-pc-android.yml 2025-06-30 18:35:51 -04:00
Chris H
14e2a0c5e2 Update snapshot-both-pc-android.yml 2025-06-30 18:28:51 -04:00
Chris H
2ae3efc12e Fix The Brothers War landscape book 2025-06-30 16:48:09 -04:00
tool4ever
fb18134c12 Update merieke_ri_berit.txt
Fix missing trigger when destroyed
2025-06-30 21:13:47 +02:00
Paul Hammerton
2215a101a3 Merge pull request #7906 from paulsnoops/b-a-r-30-june-2025
Banned and Restricted Announcement for June 30, 2025
2025-06-30 18:19:22 +01:00
Paul Hammerton
4df2fe7ac5 Banned and Restricted Announcement for June 30, 2025 2025-06-30 18:08:25 +01:00
Chris H
c24369d1ec Update snapshot-both-pc-android.yml 2025-06-29 12:38:59 -07:00
kevlahnota
41eb86029a Update README.md
Add note to Android installation
2025-06-29 19:52:01 +08:00
Hans Mackowiak
357026ce66 ~ lf 2025-06-28 17:03:26 +02:00
Paul Hammerton
9b558cb069 Merge pull request #7901 from paulsnoops/edition-updates
Edition updates: EOE, EOS, PA1, SLD, TLA & Add Sonic, PA1 to formats
2025-06-28 09:15:11 +01:00
Paul Hammerton
303020ca75 Edition updates: EOE, EOS, PA1, SLD, TLA & Add Sonic, PA1 to formats 2025-06-28 09:10:28 +01:00
Fulgur14
b900abcc71 Sonic the Hedgehog and the rest of his Secret Lair (#7898) 2025-06-28 09:01:20 +02:00
tool4ever
c4ba6df6e9 Update rootwise_survivor.txt 2025-06-28 08:26:08 +02:00
tool4ever
cc4a507799 Update ghouls_night_out.txt
Closes #7900
2025-06-28 08:24:26 +02:00
tool4ever
d8de8ec696 Update professor_hojo.txt 2025-06-27 22:48:18 +02:00
tool4ever
e6563814e8 Amplify: fix only revealing hardcoded types (#7896) 2025-06-27 17:32:52 +02:00
Eradev
016d51669f Better DesecrationDemon sac check. (#7880)
* Better DesecrationDemon sac check.

* Fix var.

---------

Co-authored-by: Agetian <stavdev@mail.ru>
2025-06-27 06:58:49 +03:00
Jacob Arbib
f19828b2ec Add Duration handling to TextBoxExchangeEffect and added Exchange of Words (#7890) 2025-06-26 17:08:36 +00:00
Hans Mackowiak
36fc87c2a1 ~ lf 2025-06-26 07:03:38 +02:00
tool4ever
4322bddabf Fix NPE by defeating Invasion of Ikoria while controlling Henzie (#7893) 2025-06-25 19:59:27 +00:00
Fulgur14
c128a9d4ba Alpharael, Dreaming Acolyte (EOE) (#7891) 2025-06-25 17:53:25 +00:00
Chris H
2feeffc95c Update jenova_ancient_calamity.txt (#7892) 2025-06-25 17:42:20 +00:00
Hans Mackowiak
53de238a7e Update EnemySprite.java
Fix Android not having stream().toList()
2025-06-25 17:25:46 +02:00
Cees Timmerman
c207e74369 Fix Meld art download (#7858) (#7884)
* Fix Meld art download (#7858)

---------

Co-authored-by: tool4ever <therealtoolkit@hotmail.com>
Co-authored-by: Hans Mackowiak <hanmac@gmx.de>
Co-authored-by: Agetian <stavdev@mail.ru>
2025-06-25 10:18:43 +02:00
tool4ever
04ca02a77a Update fiery_gambit.txt
Fix collision
Closes #7886
2025-06-22 22:21:28 +02:00
Hans Mackowiak
6ff89c71b6 Add Spacecraft (#7885)
* Add Spacecraft

---------

Co-authored-by: tool4ever <therealtoolkit@hotmail.com>
2025-06-22 11:03:52 +02:00
Hans Mackowiak
d7c6a8e53e ~ lf 2025-06-22 10:09:00 +02:00
kevlahnota
150741a443 Merge pull request #7883 from Card-Forge/keybind-hud-android-fix
Fix Android Keybind for Adventure Mode Console toggle
2025-06-22 09:01:36 +08:00
kevlahnota
460f322c44 Update KeyBinding.java
remove duplicates, update back key binding
2025-06-21 22:49:58 +08:00
kevlahnota
ef5d35fe38 Update GameHUD.java
update console ZIndex
2025-06-21 22:46:25 +08:00
kevlahnota
16044556b5 Update Console.java
exit command for console
2025-06-21 22:43:40 +08:00
Paul Hammerton
81d5079468 Merge pull request #7882 from paulsnoops/editionupdates
Edition updates: EOE, PSPL
2025-06-21 11:04:48 +01:00
Paul Hammerton
edc4b22cbc Edition updates: EOE, PSPL 2025-06-21 11:01:04 +01:00
Cees Timmerman
c01dfd4740 Fix Meld art download (#7858) 2025-06-21 11:52:08 +02:00
Paul Hammerton
53a4a46d1f Merge pull request #7881 from paulsnoops/edition-updates
Edition updates: EOC, EOE, EOS, PF25, PJSC, PPRO, SLD, TLA
2025-06-21 10:03:39 +01:00
Paul Hammerton
d61e7e7102 PPRO 2025-06-21 09:59:24 +01:00
Paul Hammerton
17f4df9293 Edition updates: EOC, EOE, EOS, PF25, PJSC, SLD, TLA 2025-06-21 09:55:08 +01:00
Fulgur14
24071582bb 3 EOE cards + 2 EOC cards (#7875) 2025-06-21 07:24:52 +00:00
Eradev
5754466a0d Fix Triplicate Titans tokens ids 2025-06-21 08:27:56 +02:00
Fulgur14
a18eda92b7 Harmonious Grovestrider (EOE) (#7873) 2025-06-20 21:05:18 +00:00
Fulgur14
11d0a8f2fe Create tezzeret_cruel_captain.txt (#7870) 2025-06-20 21:05:10 +00:00
Cees Timmerman
2b8b386882 Fix ConcurrentModificationException (#7865) (#7867)
* Fix ConcurrentModificationException (#7865)

* Update CostPayment.java
2025-06-20 07:33:32 +02:00
Hans Mackowiak
45a5027451 Update kuja_genome_sorcerer_trance_kuja_fate_defied.txt
Closes #7869
2025-06-20 07:32:18 +02:00
Hans Mackowiak
2cb72f55f3 Update Modern Horizons 3.txt
Closes #7864
2025-06-19 10:12:05 +02:00
jpvorenk
fa27fbab46 fix null pointer on draft refresh (#7863) 2025-06-17 21:36:29 -04:00
EfourC
87821fe287 Adventure: Made tooltips instant and reduced size (#7787)
* Made adventure reward tooltips instant and adjusted their size on Desktop

* Moved RewardTooltipManager to be an internal class of RewardActor.
2025-06-16 20:40:08 -04:00
Seth Milliken
cee4bcd867 card: correct sundown pass mana order (#7855)
* card: correct sundown pass mana order
2025-06-15 10:28:23 +02:00
tool4ever
b9cc4c18a8 Update yuna_grand_summoner.txt 2025-06-14 21:56:29 +02:00
EfourC
4577e61940 Corrected height of buttons for disabled Auto and Quck save slots, and adjusted font color. 2025-06-14 13:59:24 -04:00
EfourC
9ac6147a98 Made the autoSell buttons for rewards semi-transparent unless selected and fixed transparency carrying over to other Actors in a batch. Also made small adjustment to card flipping speed to be a little more snappy and fit the sound effect better. 2025-06-14 13:54:14 -04:00
Chris H
74d4ea8a78 Replace each card in a slot individually instead of replacing the whole slot at once. (Fixes higher rates of replacements) (#7854) 2025-06-14 18:02:54 +03:00
Robin Woodby
e2f4c7f872 Autofocus the first dialog option on desktop with keyboard input
Previously, autofocus only worked with gamepads connected.
2025-06-14 10:33:41 -04:00
Robin Woodby
07db8e5a94 Allow ESC to dismiss console in addition to android back button 2025-06-14 10:33:41 -04:00
Robin Woodby
ed7fb03b9c Fix issue where eventTouchUp is firing before keyUp in GameHUD 2025-06-14 10:33:41 -04:00
Robin Woodby
f0ebc3e6a0 Remove redundant call to openMenu causing flickering 2025-06-14 10:33:41 -04:00
Robin Woodby
f2570b4cef Update adventure mode deck editor to support ESC key to go back. 2025-06-14 10:33:41 -04:00
Robin Woodby
97e1939021 Fix enter key not working to save/load in menus 2025-06-14 10:33:41 -04:00
Robin Woodby
bebb894f3e Fix setText call for done button on rewards page 2025-06-14 10:33:41 -04:00
Robin Woodby
288c533c91 Revert reward scene done button text to check mark except for shop
This way translation when loading from file works for "Done"
2025-06-14 10:33:41 -04:00
Robin Woodby
6891f9a5d5 Change back button to esc in shop and reward screens for consistency 2025-06-14 10:33:41 -04:00
Simisays
37a62eed6b Update crypt.tmx (#7853) 2025-06-14 16:40:25 +03:00
MD200210
3ee6b0e58d Details Labels and Zooming on Adventure World Map (#7840)
* Details Button on Adventure Map.
Zooming on Adventure Map

* no Wildcard Import
2025-06-13 07:12:28 +03:00
Robin Woodby
7e6f772345 Update forge-ai description to be more specific and less redundant 2025-06-11 21:05:20 -04:00
Robin Woodby
ef28173b65 Add missing module descriptions in contributing docs 2025-06-11 21:05:20 -04:00
Robin Woodby
a26614c6b6 Remove double spaces and fix typos 2025-06-11 21:05:20 -04:00
Chris H
ee00293e48 Update rydias_return.txt 2025-06-11 16:21:50 -04:00
MatthiasD
ce977387e6 Fix Update Symbols 2025-06-11 16:00:52 -04:00
MD200210
8cc45dbbd8 Fix Cid (#7842) 2025-06-11 17:47:29 +02:00
MatthiasD
851a4c104c Update and change FCA 2025-06-11 16:28:49 +02:00
45ddf3fded Fix: Update getCommander method to enable StorageImmediatelySerialized subfolders 2025-06-11 09:09:10 -04:00
tool4ever
cb538f4026 Update depth_defiler.txt (#7829) 2025-06-09 10:36:34 +02:00
EfourC
6bd6db3c16 Adventure: AccountingLabel animation update 2025-06-08 21:57:07 -04:00
Chris H
6af3940ffb Update queen_brahne.txt 2025-06-08 21:43:34 -04:00
Renato Filipe Vidal Santos
66c864c8d7 Cleanup: DBCleanup (#7822) 2025-06-08 21:32:18 +03:00
rwalters
15354dd8e8 Fix ff limited cards (#7819)
* Fixing the power and toughness of two cards relevant in limited

---------

Co-authored-by: Agetian <stavdev@mail.ru>
2025-06-08 08:37:56 +03:00
Renato Filipe Vidal Santos
215070fe3f Update blot_out.txt 2025-06-07 12:59:12 +02:00
Renato Filipe Vidal Santos
ed73883e80 Update overtaker.txt (#7815) 2025-06-07 12:59:25 +03:00
Renato Filipe Vidal Santos
df85ebc0aa Update ayula_queen_among_bears.txt (#7812)
* Update ayula_queen_among_bears.txt

* Update rampaging_yao_guai.txt

* Update ulvenwald_bear.txt

* Update rampaging_yao_guai.txt
2025-06-07 10:43:33 +03:00
Hans Mackowiak
885a6af943 Update shambling_cieth.txt
Closes #7810
2025-06-06 17:50:13 +02:00
Renato Filipe Vidal Santos
94820d4782 Update tranquil_frillback.txt 2025-06-06 17:05:11 +02:00
Renato Filipe Vidal Santos
7f044e5ac3 Update wicked_slumber.txt 2025-06-06 15:11:59 +02:00
Renato Filipe Vidal Santos
1284f23620 Update primeval_bounty.txt (#7807) 2025-06-06 12:34:37 +02:00
Agetian
bdcc8acdc4 Add Final Fantasy achievements by Marek14 (#7806)
* - Add FIN achievements by Marek14.

* - Minor tweak.
2025-06-06 10:21:44 +03:00
Chris H
73a9dfcf43 Automate patch version increment 2025-06-05 23:37:40 -04:00
GitHub Actions
7a277ce283 [maven-release-plugin] prepare for next development iteration 2025-06-05 23:37:40 -04:00
GitHub Actions
f8b3f9dd30 [maven-release-plugin] prepare release forge-2.0.04 2025-06-05 23:37:40 -04:00
Chris H
840f3ea96c Automate patch version increment 2025-06-05 22:45:57 -04:00
Chris H
51929434be Update flatten to work with full release 2025-06-05 22:45:57 -04:00
gsonnier333
3ec480afa4 fixed weapons_vendor.txt power/toughness 2025-06-05 21:21:27 -04:00
Chris H
3d19e9e444 Update summon_g_f_ifrit.txt 2025-06-05 21:21:14 -04:00
Renato Filipe Vidal Santos
6d987791f7 Fixing Animate Dead and similar effects) 2025-06-05 14:25:08 +02:00
Renato Filipe Vidal Santos
7b7da00c22 Incidental fixes: 2025-06-05 2025-06-05 08:54:38 +02:00
tool4ever
34791bd892 Update stangg.txt 2025-06-05 08:53:38 +02:00
Renato Filipe Vidal Santos
e6bcd1be72 Add files via upload 2025-06-04 20:33:46 -04:00
Paul Hammerton
4cac354983 Merge pull request #7794 from paulsnoops/fin-formats
Add Final Fantasy to formats
2025-06-04 18:11:13 +01:00
Paul Hammerton
042f7f77a9 Add Final Fantasy to formats 2025-06-04 18:06:21 +01:00
Hans Mackowiak
5fbb1dd0cd Update summon_leviathan.txt
Fix Draw
2025-06-04 08:55:00 +02:00
Hans Mackowiak
bb14dfc00e Update summon_leviathan.txt
Closes #7784
2025-06-04 07:20:24 +02:00
Renato Filipe Vidal Santos
f83cc4ccfa Update fin.rnk 2025-06-03 11:05:10 -04:00
tool4ever
d0d6835a5d Update zodiark_umbral_god.txt 2025-06-03 13:24:17 +00:00
tool4EvEr
104bc8fc55 SpellAbility: set CardState inside constructor 2025-06-03 13:26:25 +02:00
SprinkleMeTimbers
057dd867a8 Adventure: added support for dynamic deck count (#7669)
* Added: utility method for adding an integer selecting combo-box.

* Added: Adventure now supports dynamic amount of decks

* Tweaked: lowered max deck count to 20.

* Added: dynamic deck count can't ever be higher than the maximum in current version.
2025-06-03 13:17:30 +03:00
Chris H
9eed8f5095 Update some names in fin rankings (#7776) 2025-06-03 13:17:18 +03:00
Chris H
1f62be9773 Migrate FIN upcoming (#7777) 2025-06-03 10:13:01 +00:00
Hans Mackowiak
7a46f92059 Update CardState updateSpellAbilities only when not inPlay (#7780)
* Fix copy order
2025-06-03 10:12:35 +00:00
tool4EvEr
cfa79b9676 attachAuraOnIndirectETB: fix missing activator 2025-06-03 09:54:37 +02:00
tool4ever
c0a63fa15b Fix Dark Depths triggering vs. Blood Moon (#7768) 2025-06-03 08:07:10 +02:00
Renato Filipe Vidal Santos
c61537ae16 Add files via upload 2025-06-02 23:44:22 -04:00
Renato Filipe Vidal Santos
3871095b92 Add files via upload 2025-06-02 23:44:22 -04:00
Simisays
dee846da49 Update cave_bigzombie.tmx 2025-06-02 22:08:08 -04:00
Jason Vorenkamp
2477553d13 fix incorrect resource name 2025-06-02 21:46:37 -04:00
Renato Filipe Vidal Santos
c9df7d7f8e Cleaning upcoming: 2025-06-02 2025-06-02 16:02:17 +00:00
rwalters
53cb093f9e (Adventure) Fix draft scene transition (#7666)
* Fixing an issue in which touching the space the map occupies outside of the world map does not allow the player to move (very relevant on maps with content in the top left corner)

* Fixing a bug in which the transition screen's non-blocking of the start match button can be clicked multiple times, which results in a crash when the match ends
2025-06-02 10:24:50 -04:00
MD200210
28e86970dc Added Draft Info (#7764)
* FIN Draft Info

(cherry picked from commit ad0385617b26391efa2b866157eea674ea850e1e)

* Correction

* Correction

* Correction
2025-06-02 10:24:10 -04:00
Chris H
f847fc1669 Fix sketchbooks that are missing colons in the name 2025-06-02 07:22:26 -04:00
Hans Mackowiak
f3df55177a CardState: fix Android not having stream().toList(); Closes #7761 2025-06-02 07:10:26 +02:00
Chris H
be6f345127 Hotfix PaperCard edition issues 2025-06-01 22:50:58 -04:00
879 changed files with 5989 additions and 1720 deletions

View File

@@ -81,7 +81,7 @@ jobs:
export _JAVA_OPTIONS="-Xmx2g"
d=$(date +%m.%d)
# build only desktop and only try to move desktop files
mvn -U -B clean -P windows-linux install -e -T 1C release:clean release:prepare release:perform -DskipTests
mvn -U -B clean -P windows-linux install -DskipTests -Dskip.flatten=true -e -T 1C release:clean release:prepare release:perform
mkdir izpack
# move bz2 and jar from work dir to izpack dir
mv /home/runner/work/forge/forge/forge-installer/*/*.{bz2,jar} izpack/
@@ -128,6 +128,44 @@ jobs:
removeArtifacts: true
makeLatest: true
- name: 🔧 Install XML tools
run: sudo apt-get install -y libxml2-utils
- name: 🔼 Bump versionCode in root POM
id: bump_version
run: |
cd /home/runner/work/forge/forge/
current_version=$(xmllint --xpath "//*[local-name()='versionCode']/text()" pom.xml)
echo "Current versionCode: $current_version"
IFS='.' read -r major minor patch <<< "${current_version}"
new_patch=$(printf "%02d" $((10#$patch + 1)))
new_version="${major}.${minor}.${new_patch}"
sed -i -E "s|<versionCode>.*</versionCode>|<versionCode>${new_version}</versionCode>|" pom.xml
echo "version_code=${new_version}" >> $GITHUB_OUTPUT
- name: ♻️ Restore {revision} in child POMs
run: |
find . -name pom.xml ! -path "./pom.xml" | while read -r pom; do
sed -i -E 's|<version>2\.0+\.[0-9]+(-SNAPSHOT)?</version>|<version>${revision}</version>|' "$pom"
done
- name: 💾 Commit restored {revision}
run: |
# Add only pom.xml files
find . -name pom.xml -exec git add {} \;
# Commit if there are changes
if git diff --cached --quiet; then
echo "No pom.xml changes to commit."
else
git commit -m "Restore POM files for preparation of next release" || echo "No changes to commit"
git push
fi
- name: Send failure notification to Discord
if: failure() # This step runs only if the job fails
run: |

View File

@@ -6,7 +6,7 @@ Dev instructions here: [Getting Started](https://github.com/Card-Forge/forge/wik
## Requirements / Tools
- you favourite Java IDE (IntelliJ, Eclipse, VSCodium, Emacs, Vi...)
- your favourite Java IDE (IntelliJ, Eclipse, VSCodium, Emacs, Vi...)
- Java JDK 17 or later
- Git
- Git client (optional)
@@ -22,42 +22,41 @@ Dev instructions here: [Getting Started](https://github.com/Card-Forge/forge/wik
- Clone your forked project to your local machine
- Go to the project location on your machine. Run Maven to download all dependencies and build a snapshot. Example for Windows & Linux: `mvn -U -B clean -P windows-linux install`
- Go to the project location on your machine. Run Maven to download all dependencies and build a snapshot. Example for Windows & Linux: `mvn -U -B clean -P windows-linux install`
## IntelliJ
IntelliJ is the recommended IDE for Forge development. Quick start guide for [setting up the Forge project within IntelliJ](https://github.com/Card-Forge/forge/wiki/IntelliJ-setup).
## Eclipse
Eclipse includes Maven integration so a separate install is not necessary. For other IDEs, your mileage may vary.
Eclipse includes Maven integration so a separate install is not necessary. For other IDEs, your mileage may vary.
At this time, Eclipse is not the recommended IDE for Forge development.
### Project Setup
- Follow the instructions for cloning from GitHub. You'll need to setup an account and your SSH key.
- Follow the instructions for cloning from GitHub. You'll need to setup an account and your SSH key.
If you are on a Windows machine you can use Putty with TortoiseGit for SSH keys. Run puttygen.exe to generate the key -- save the private key and export
the OpenSSH public key. If you just leave the dialog open, you can copy and paste the key from it to your GitHub profile under
"SSH keys". Run pageant.exe and add the private key generated earlier. TortoiseGit will use this for accessing GitHub.
If you are on a Windows machine you can use Putty with TortoiseGit for SSH keys. Run puttygen.exe to generate the key -- save the private key and export
the OpenSSH public key. If you just leave the dialog open, you can copy and paste the key from it to your GitHub profile under
"SSH keys". Run pageant.exe and add the private key generated earlier. TortoiseGit will use this for accessing GitHub.
- Fork the Forge git repo to your GitHub account.
- Clone your forked repo to your local machine.
- Make sure the Java SDK is installed -- not just the JRE. Java 17 or newer required. If you execute `java -version` at the shell or command prompt, it should report version 17 or later.
- Make sure the Java SDK is installed -- not just the JRE. Java 17 or newer required. If you execute `java -version` at the shell or command prompt, it should report version 17 or later.
- Install Eclipse 2021-12 or later for Java. Launch it.
- Install Eclipse 2021-12 or later for Java. Launch it.
- Create a workspace. Go to the workbench. Right-click inside of Package Explorer > Import... > Maven > Existing Maven Projects > Navigate to root path of the local forge repo and
- Create a workspace. Go to the workbench. Right-click inside of Package Explorer > Import... > Maven > Existing Maven Projects > Navigate to root path of the local forge repo and
ensure everything is checked > Finish.
- Let Eclipse run through building the project. You may be prompted for resolving any missing Maven plugins -- accept the ones offered. You may see errors appear in the "Problems" tab. These should
be automatically resolved as plug-ins are installed and Eclipse continues the build process. If this is the first time for some plug-in installs, Eclipse may prompt you to restart. Do so. Be patient
- Let Eclipse run through building the project. You may be prompted for resolving any missing Maven plugins -- accept the ones offered. You may see errors appear in the "Problems" tab. These should
be automatically resolved as plug-ins are installed and Eclipse continues the build process. If this is the first time for some plug-in installs, Eclipse may prompt you to restart. Do so. Be patient
for this first time through.
- Once everything builds, all errors should disappear. You can now advance to Project launch.
- Once everything builds, all errors should disappear. You can now advance to Project launch.
### Project Launch
@@ -67,15 +66,15 @@ This is the standard configuration used for releasing to Windows / Linux / MacOS
- Right-click on forge-gui-desktop > Run As... > Java Application > "Main - forge.view" > Ok
- The familiar Forge splash screen, etc. should appear. Enjoy!
- The familiar Forge splash screen, etc. should appear. Enjoy!
#### Mobile (Desktop dev)
This is the configuration used for doing mobile development using the Windows / Linux / MacOS front-end. Knowledge of libgdx is helpful here.
This is the configuration used for doing mobile development using the Windows / Linux / MacOS front-end. Knowledge of libgdx is helpful here.
- Right-click on forge-gui-mobile-dev > Run As... > Java Application > "Main - forge.app" > Ok.
- A view similar to a mobile phone should appear. Enjoy!
- A view similar to a mobile phone should appear. Enjoy!
### Eclipse / Android SDK Integration
@@ -99,7 +98,7 @@ TBD
#### Android Platform
In Intellij, if the SDK Manager is not already running, go to Tools > Android > Android SDK Manager. Install the following options / versions:
In Intellij, if the SDK Manager is not already running, go to Tools > Android > Android SDK Manager. Install the following options / versions:
- Android SDK Build-tools 35.0.0
- Android 15 (API 35) SDK Platform
@@ -124,10 +123,11 @@ TBD
SNAPSHOT builds can be built via the Maven integration in Eclipse.
1) Create a Maven build for the forge top-level project. Right-click on the forge project. Run as.. > Maven build...
1. Create a Maven build for the forge top-level project. Right-click on the forge project. Run as.. > Maven build...
- On the Main tab, set Goals: clean install, set Profiles: windows-linux
2) Run forge Maven build. If everything built, you should see "BUILD SUCCESS" in the Console View.
2. Run forge Maven build. If everything built, you should see "BUILD SUCCESS" in the Console View.
The resulting snapshot will be found at: forge-gui-desktop/target/forge-gui-desktop-[version]-SNAPSHOT
@@ -141,7 +141,7 @@ Card scripting resources are found in the forge-gui/res/ path.
### Project Hierarchy
Forge is divided into 4 primary projects with additional projects that target specific platform releases. The primary projects are:
Forge is divided into 4 primary projects with additional projects that target specific platform releases. The primary projects are:
- forge-ai
- forge-core
@@ -158,32 +158,38 @@ The platform-specific projects are:
#### forge-ai
The forge-ai project contains the computer opponent logic for gameplay. It includes decision-making algorithms for specific abilities, cards and turn phases.
#### forge-core
The forge-core project contains the core game engine, card mechanics, rules engine, and fundamental game logic. It includes the implementation of Magic: The Gathering rules, card interactions, and the game state management system.
#### forge-game
The forge-game project handles the game session management, player interactions, and game flow control. It includes implementations for multiplayer support, game modes, matchmaking, and game state persistence. This module bridges the core game engine with the user interface and networking components.
#### forge-gui
The forge-gui project includes the scripting resource definitions in the res/ path.
The forge-gui project contains the user interface components and rendering logic for the game. It includes the main game window, card displays, player interactions, and the scripting resource definitions in the res/ path.
#### forge-gui-android
Libgdx-based backend targeting Android. Requires Android SDK and relies on forge-gui-mobile for GUI logic.
Libgdx-based backend targeting Android. Requires Android SDK and relies on forge-gui-mobile for GUI logic.
#### forge-gui-desktop
Java Swing based GUI targeting desktop machines.
Screen layout and game logic revolving around the GUI is found here. For example, the overlay arrows (when enabled) that indicate attackers and blockers, or the targets of the stack are defined and drawn by this.
Screen layout and game logic revolving around the GUI is found here. For example, the overlay arrows (when enabled) that indicate attackers and blockers, or the targets of the stack are defined and drawn by this.
#### forge-gui-ios
Libgdx-based backend targeting iOS. Relies on forge-gui-mobile for GUI logic.
Libgdx-based backend targeting iOS. Relies on forge-gui-mobile for GUI logic.
#### forge-gui-mobile
Mobile GUI game logic utilizing [libgdx](https://libgdx.badlogicgames.com/) library. Screen layout and game logic revolving around the GUI for the mobile platforms is found here.
Mobile GUI game logic utilizing [libgdx](https://libgdx.badlogicgames.com/) library. Screen layout and game logic revolving around the GUI for the mobile platforms is found here.
#### forge-gui-mobile-dev
Libgdx backend for desktop development for mobile backends. Utilizes LWJGL. Relies on forge-gui-mobile for GUI logic.
Libgdx backend for desktop development for mobile backends. Utilizes LWJGL. Relies on forge-gui-mobile for GUI logic.

View File

@@ -32,6 +32,7 @@ Join the **Forge community** on [Discord](https://discord.gg/HcPJNyD66a)!
4. **Java Requirement:** Ensure you have **Java 17 or later** installed.
### 📱 Android Installation
- _(Note: **Android 11** is the minimum requirements with at least **6GB RAM** to run smoothly. You need to enable **"Install unknown apps"** for Forge to initialize and update itself)_
- Download the **APK** from the [Snapshot Build](https://github.com/Card-Forge/forge/releases/tag/daily-snapshots). On the first launch, Forge will automatically download all necessary assets.
---

View File

@@ -359,18 +359,28 @@ public class SpecialCardAi {
private static final int demonSacThreshold = Integer.MAX_VALUE; // if we're in dire conditions, sac everything from worst to best hoping to find an answer
public static boolean considerSacrificingCreature(final Player ai, final SpellAbility sa) {
Card c = sa.getHostCard();
// Only check for sacrifice if it's the owner's turn, and it can attack.
// TODO: Maybe check if sacrificing a creature allows AI to kill the opponent with the rest on their turn?
if (!CombatUtil.canAttack(c) ||
!ai.getGame().getPhaseHandler().isPlayerTurn(sa.getActivatingPlayer())) {
return false;
}
CardCollection flyingCreatures = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield),
CardPredicates.UNTAPPED.and(
CardPredicates.hasKeyword(Keyword.FLYING).or(CardPredicates.hasKeyword(Keyword.REACH))));
CardPredicates.UNTAPPED.and(
CardPredicates.hasKeyword(Keyword.FLYING).or(CardPredicates.hasKeyword(Keyword.REACH))));
boolean hasUsefulBlocker = false;
for (Card c : flyingCreatures) {
if (!ComputerUtilCard.isUselessCreature(ai, c)) {
for (Card fc : flyingCreatures) {
if (!ComputerUtilCard.isUselessCreature(ai, fc)) {
hasUsefulBlocker = true;
break;
}
}
return ai.getLife() <= sa.getHostCard().getNetPower() && !hasUsefulBlocker;
return ai.getLife() <= c.getNetPower() && !hasUsefulBlocker;
}
public static int getSacThreshold() {

View File

@@ -33,6 +33,11 @@ public final class ImageKeys {
public static final String RADIATION_IMAGE = "radiation";
public static final String BACKFACE_POSTFIX = "$alt";
public static final String SPECFACE_W = "$wspec";
public static final String SPECFACE_U = "$uspec";
public static final String SPECFACE_B = "$bspec";
public static final String SPECFACE_R = "$rspec";
public static final String SPECFACE_G = "$gspec";
private static String CACHE_CARD_PICS_DIR, CACHE_TOKEN_PICS_DIR, CACHE_ICON_PICS_DIR, CACHE_BOOSTER_PICS_DIR,
CACHE_FATPACK_PICS_DIR, CACHE_BOOSTERBOX_PICS_DIR, CACHE_PRECON_PICS_DIR, CACHE_TOURNAMENTPACK_PICS_DIR;
@@ -92,28 +97,13 @@ public final class ImageKeys {
return cachedCards.get(key);
}
public static File getImageFile(String key) {
return getImageFile(key, false);
}
public static File getImageFile(String key, boolean artCrop) {
if (StringUtils.isEmpty(key))
return null;
final String dir;
final String filename;
String[] tempdata = null;
if (key.startsWith(ImageKeys.CARD_PREFIX)) {
tempdata = key.substring(ImageKeys.CARD_PREFIX.length()).split("\\|");
String tokenname = tempdata[0];
if (tempdata.length > 1) {
tokenname += "_" + tempdata[1];
}
if (tempdata.length > 2) {
tokenname += "_" + tempdata[2];
}
filename = tokenname ;
dir = CACHE_CARD_PICS_DIR;
} else if (key.startsWith(ImageKeys.TOKEN_PREFIX)) {
if (key.startsWith(ImageKeys.TOKEN_PREFIX)) {
tempdata = key.substring(ImageKeys.TOKEN_PREFIX.length()).split("\\|");
String tokenname = tempdata[0];
if (tempdata.length > 1) {
@@ -164,31 +154,7 @@ public final class ImageKeys {
cachedCards.put(filename, file);
return file;
}
if (tempdata != null && dir.equals(CACHE_CARD_PICS_DIR)) {
String setlessFilename = tempdata[0] + (artCrop ? ".artcrop" : ".fullborder");
String setCode = tempdata.length > 1 ? tempdata[1] : "";
String collectorNumber = tempdata.length > 2 ? tempdata[2] : "";
if (!setCode.isEmpty()) {
if (!collectorNumber.isEmpty()) {
file = findFile(dir, setCode + "/" + collectorNumber + "_" + setlessFilename);
if (file != null) {
cachedCards.put(filename, file);
return file;
}
}
file = findFile(dir, setCode + "/" + setlessFilename);
if (file != null) {
cachedCards.put(filename, file);
return file;
}
}
file = findFile(dir, setlessFilename);
if (file != null) {
cachedCards.put(filename, file);
return file;
}
}
if (tempdata != null && dir.equals(CACHE_TOKEN_PICS_DIR)) {
if (dir.equals(CACHE_TOKEN_PICS_DIR)) {
String setlessFilename = tempdata[0];
String setCode = tempdata.length > 1 ? tempdata[1] : "";
String collectorNumber = tempdata.length > 2 ? tempdata[2] : "";

View File

@@ -433,17 +433,6 @@ public final class CardEdition implements Comparable<CardEdition> {
public Multimap<String, EditionEntry> getTokens() { return tokenMap; }
public EditionEntry getTokenFromCollectorNumber(String collectorNumber) {
if(collectorNumber == null || collectorNumber.isEmpty())
return null;
for(EditionEntry c : this.tokenMap.values()) {
//Could build a map for this one too if it's used for more than one-offs.
if (c.collectorNumber.equalsIgnoreCase(collectorNumber))
return c;
}
return null;
}
public String getTokenSet(String token) {
if (tokenMap.containsKey(token)) {
return this.getCode();

View File

@@ -20,8 +20,6 @@ package forge.card;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import forge.StaticData;
import forge.card.mana.IParserManaCost;
import forge.card.mana.ManaCost;
import forge.card.mana.ManaCostShard;
@@ -151,10 +149,6 @@ public final class CardRules implements ICardCharacteristics {
return splitType;
}
public boolean hasBackSide() {
return CardSplitType.DUAL_FACED_CARDS.contains(splitType) || splitType == CardSplitType.Flip;
}
public ICardFace getMainPart() {
return mainPart;
}
@@ -171,32 +165,20 @@ public final class CardRules implements ICardCharacteristics {
return Iterables.concat(Arrays.asList(mainPart, otherPart), specializedParts.values());
}
public String getImageName(CardStateName state) {
if (splitType == CardSplitType.Split) {
return mainPart.getName() + otherPart.getName();
} else if (state.equals(splitType.getChangedStateName())) {
if (otherPart != null) {
return otherPart.getName();
} else if (this.hasBackSide()) {
if (!getMeldWith().isEmpty()) {
final CardDb db = StaticData.instance().getCommonCards();
return db.getRules(getMeldWith()).getOtherPart().getName();
}
return null;
}
}
switch (state) {
case SpecializeW:
case SpecializeU:
case SpecializeB:
case SpecializeR:
case SpecializeG:
ICardFace face = specializedParts.get(state);
return face != null ? face.getName() : null;
default:
return getName();
}
public ICardFace getWSpecialize() {
return specializedParts.get(CardStateName.SpecializeW);
}
public ICardFace getUSpecialize() {
return specializedParts.get(CardStateName.SpecializeU);
}
public ICardFace getBSpecialize() {
return specializedParts.get(CardStateName.SpecializeB);
}
public ICardFace getRSpecialize() {
return specializedParts.get(CardStateName.SpecializeR);
}
public ICardFace getGSpecialize() {
return specializedParts.get(CardStateName.SpecializeG);
}
public String getName() {
@@ -303,7 +285,10 @@ public final class CardRules implements ICardCharacteristics {
return true;
}
CardType type = mainPart.getType();
if (type.isLegendary() && canBeCreature()) {
if (!type.isLegendary()) {
return false;
}
if (canBeCreature() || type.isVehicle() || type.isSpacecraft()) {
return true;
}
return false;

View File

@@ -516,7 +516,7 @@ public final class CardType implements Comparable<CardType>, CardTypeView {
@Override
public boolean isAttachment() { return isAura() || isEquipment() || isFortification(); }
@Override
public boolean isAura() { return hasSubtype("Aura"); }
public boolean isAura() { return hasSubtype("Aura"); }
@Override
public boolean isEquipment() { return hasSubtype("Equipment"); }
@Override
@@ -529,6 +529,9 @@ public final class CardType implements Comparable<CardType>, CardTypeView {
return hasSubtype("Contraption");
}
public boolean isVehicle() { return hasSubtype("Vehicle"); }
public boolean isSpacecraft() { return hasSubtype("Spacecraft"); }
@Override
public boolean isSaga() {
return hasSubtype("Saga");

View File

@@ -24,6 +24,7 @@ import forge.util.CardTranslation;
import forge.util.ImageUtil;
import forge.util.Localizer;
import forge.util.TextUtil;
import org.apache.commons.lang3.StringUtils;
import java.io.*;
import java.util.*;
@@ -346,13 +347,34 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
if (pc == null) {
pc = StaticData.instance().getVariantCards().getCard(name, edition, artIndex);
if (pc == null) {
throw new IOException(TextUtil.concatWithSpace("Card", name, "not found"));
System.out.println("PaperCard: " + name + " not found with set and index " + edition + ", " + artIndex);
pc = readObjectAlternate(name, edition);
if (pc == null) {
throw new IOException(TextUtil.concatWithSpace("Card", name, "not found with set and index", edition, Integer.toString(artIndex)));
}
System.out.println("Alternate object found: " + pc.getName() + ", " + pc.getEdition() + ", " + pc.getArtIndex());
}
}
rules = pc.getRules();
rarity = pc.getRarity();
}
private IPaperCard readObjectAlternate(String name, String edition) throws ClassNotFoundException, IOException {
IPaperCard pc = StaticData.instance().getCommonCards().getCard(name, edition);
if (pc == null) {
pc = StaticData.instance().getVariantCards().getCard(name, edition);
}
if (pc == null) {
pc = StaticData.instance().getCommonCards().getCard(name);
if (pc == null) {
pc = StaticData.instance().getVariantCards().getCard(name);
}
}
return pc;
}
@Serial
private Object readResolve() throws ObjectStreamException {
//If we deserialize an old PaperCard with no flags, reinitialize as a fresh copy to set default flags.
@@ -363,14 +385,20 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
@Override
public String getImageKey(boolean altState) {
return altState ? this.getCardAltImageKey() : this.getCardImageKey();
String normalizedName = StringUtils.stripAccents(name);
String imageKey = ImageKeys.CARD_PREFIX + normalizedName + CardDb.NameSetSeparator
+ edition + CardDb.NameSetSeparator + artIndex;
if (altState) {
imageKey += ImageKeys.BACKFACE_POSTFIX;
}
return imageKey;
}
private String cardImageKey = null;
@Override
public String getCardImageKey() {
if (this.cardImageKey == null)
this.cardImageKey = ImageUtil.getImageKey(this, CardStateName.Original);
this.cardImageKey = ImageUtil.getImageKey(this, "", true);
return cardImageKey;
}
@@ -379,9 +407,9 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
public String getCardAltImageKey() {
if (this.cardAltImageKey == null){
if (this.hasBackFace())
this.cardAltImageKey = ImageUtil.getImageKey(this, this.getRules().getSplitType().getChangedStateName());
this.cardAltImageKey = ImageUtil.getImageKey(this, "back", true);
else // altImageKey will be the same as cardImageKey
this.cardAltImageKey = getCardImageKey();
this.cardAltImageKey = ImageUtil.getImageKey(this, "", true);
}
return cardAltImageKey;
}
@@ -391,9 +419,9 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
public String getCardWSpecImageKey() {
if (this.cardWSpecImageKey == null) {
if (this.rules.getSplitType() == CardSplitType.Specialize)
this.cardWSpecImageKey = ImageUtil.getImageKey(this, CardStateName.SpecializeW);
this.cardWSpecImageKey = ImageUtil.getImageKey(this, "white", true);
else // just use cardImageKey
this.cardWSpecImageKey = getCardImageKey();
this.cardWSpecImageKey = ImageUtil.getImageKey(this, "", true);
}
return cardWSpecImageKey;
}
@@ -403,9 +431,9 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
public String getCardUSpecImageKey() {
if (this.cardUSpecImageKey == null) {
if (this.rules.getSplitType() == CardSplitType.Specialize)
this.cardUSpecImageKey = ImageUtil.getImageKey(this, CardStateName.SpecializeU);
this.cardUSpecImageKey = ImageUtil.getImageKey(this, "blue", true);
else // just use cardImageKey
this.cardUSpecImageKey = getCardImageKey();
this.cardUSpecImageKey = ImageUtil.getImageKey(this, "", true);
}
return cardUSpecImageKey;
}
@@ -415,9 +443,9 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
public String getCardBSpecImageKey() {
if (this.cardBSpecImageKey == null) {
if (this.rules.getSplitType() == CardSplitType.Specialize)
this.cardBSpecImageKey = ImageUtil.getImageKey(this, CardStateName.SpecializeB);
this.cardBSpecImageKey = ImageUtil.getImageKey(this, "black", true);
else // just use cardImageKey
this.cardBSpecImageKey = getCardImageKey();
this.cardBSpecImageKey = ImageUtil.getImageKey(this, "", true);
}
return cardBSpecImageKey;
}
@@ -427,9 +455,9 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
public String getCardRSpecImageKey() {
if (this.cardRSpecImageKey == null) {
if (this.rules.getSplitType() == CardSplitType.Specialize)
this.cardRSpecImageKey = ImageUtil.getImageKey(this, CardStateName.SpecializeR);
this.cardRSpecImageKey = ImageUtil.getImageKey(this, "red", true);
else // just use cardImageKey
this.cardRSpecImageKey = getCardImageKey();
this.cardRSpecImageKey = ImageUtil.getImageKey(this, "", true);
}
return cardRSpecImageKey;
}
@@ -439,16 +467,18 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
public String getCardGSpecImageKey() {
if (this.cardGSpecImageKey == null) {
if (this.rules.getSplitType() == CardSplitType.Specialize)
this.cardGSpecImageKey = ImageUtil.getImageKey(this, CardStateName.SpecializeG);
this.cardGSpecImageKey = ImageUtil.getImageKey(this, "green", true);
else // just use cardImageKey
this.cardGSpecImageKey = getCardImageKey();
this.cardGSpecImageKey = ImageUtil.getImageKey(this, "", true);
}
return cardGSpecImageKey;
}
@Override
public boolean hasBackFace(){
return this.rules.hasBackSide();
CardSplitType cst = this.rules.getSplitType();
return cst == CardSplitType.Transform || cst == CardSplitType.Flip || cst == CardSplitType.Meld
|| cst == CardSplitType.Modal;
}
@Override

View File

@@ -401,46 +401,68 @@ public class BoosterGenerator {
System.out.println(numCards + " of type " + slotType);
// For cards that end in '+', attempt to convert this card to foil.
boolean convertCardFoil = slotType.endsWith("+");
if (convertCardFoil) {
boolean convertAllToFoil = slotType.endsWith("+");
if (convertAllToFoil) {
slotType = slotType.substring(0, slotType.length() - 1);
}
// Unpack Base
BoosterSlot boosterSlot = boosterSlots.get(slotType);
String determineSheet = boosterSlot.replaceSlot();
Map<String, Integer> slotReplacementCount = bulkSlotReplacement(boosterSlot, numCards);
if (determineSheet.endsWith("+")) {
determineSheet = determineSheet.substring(0, determineSheet.length() - 1);
convertCardFoil = true;
}
List<PaperCard> paperCards = Lists.newArrayList();
for(Map.Entry<String, Integer> entry : slotReplacementCount.entrySet()) {
String determineSheet = entry.getKey();
int numCardsToGenerate = entry.getValue();
String setCode = template.getEdition();
// Ok, so we have a sheet now. Most should be standard sheets, but some named edition sheets
List<PaperCard> paperCards;
PrintSheet ps;
try {
// Apply the edition to the sheet name by default. We'll try again if thats not a real sheet
ps = getPrintSheet(determineSheet + " " + setCode);
} catch(Exception e) {
ps = getPrintSheet(determineSheet);
}
if (convertCardFoil) {
paperCards = Lists.newArrayList();
for(PaperCard pc : ps.random(numCards, true)) {
paperCards.add(pc.getFoiled());
if (determineSheet == null || determineSheet.isEmpty() || numCardsToGenerate == 0) {
continue;
}
} else {
paperCards = ps.random(numCards, true);
}
result.addAll(paperCards);
// If the sheet ends with a '+', convert all cards in replacement section to foil
boolean convertThisToFoil = false;
if (determineSheet.endsWith("+")) {
determineSheet = determineSheet.substring(0, determineSheet.length() - 1);
convertThisToFoil = true;
}
String setCode = template.getEdition();
PrintSheet ps;
try {
// Apply the edition to the sheet name by default. We'll try again if thats not a real sheet
ps = getPrintSheet(determineSheet + " " + setCode);
} catch (Exception e) {
ps = getPrintSheet(determineSheet);
}
if (convertAllToFoil || convertThisToFoil) {
for (PaperCard pc : ps.random(numCardsToGenerate, true)) {
paperCards.add(pc.getFoiled());
}
} else {
paperCards.addAll(ps.random(numCardsToGenerate, true));
}
result.addAll(paperCards);
}
}
return result;
}
private static Map<String, Integer> bulkSlotReplacement(BoosterSlot boosterSlot, int numCards) {
Map<String, Integer> slotReplacementCount = new HashMap<>();
for(int i = 0; i < numCards; i++) {
String determineSheet = boosterSlot.replaceSlot();
if (slotReplacementCount.containsKey(determineSheet)) {
slotReplacementCount.put(determineSheet, slotReplacementCount.get(determineSheet) + 1);
} else {
slotReplacementCount.put(determineSheet, 1);
}
}
return slotReplacementCount;
}
private static void ensureGuaranteedCardInBooster(List<PaperCard> result, SealedTemplate template, String boosterMustContain) {
// First, see if there's already a card of the given type
String[] types = TextUtil.split(boosterMustContain, ' ');

View File

@@ -5,11 +5,12 @@ import forge.StaticData;
import forge.card.CardDb;
import forge.card.CardRules;
import forge.card.CardSplitType;
import forge.card.CardStateName;
import forge.item.IPaperCard;
import forge.item.PaperCard;
import org.apache.commons.lang3.StringUtils;
import java.net.URLEncoder;
public class ImageUtil {
public static float getNearestHQSize(float baseSize, float actualSize) {
//get nearest power of actualSize to baseSize so that the image renders good
@@ -25,17 +26,20 @@ public class ImageUtil {
key = imageKey.substring(ImageKeys.CARD_PREFIX.length());
else
return null;
if (key.endsWith(ImageKeys.BACKFACE_POSTFIX)) {
key = key.substring(0, key.length() - ImageKeys.BACKFACE_POSTFIX.length());
}
if (key.isEmpty())
return null;
String[] tempdata = key.split("\\|");
PaperCard cp = StaticData.instance().fetchCard(tempdata[0], tempdata[1], tempdata[2]);
CardDb db = StaticData.instance().getCommonCards();
PaperCard cp = null;
//db shouldn't be null
if (db != null) {
cp = db.getCard(key);
if (cp == null) {
db = StaticData.instance().getVariantCards();
if (db != null)
cp = db.getCard(key);
}
}
if (cp == null)
System.err.println("Can't find PaperCard from key: " + key);
// return cp regardless if it's null
@@ -52,21 +56,6 @@ public class ImageUtil {
return key;
}
public static String getImageRelativePath(String name, String set, String collectorNumber, boolean artChop) {
StringBuilder sb = new StringBuilder();
sb.append(set).append("/");
if (!collectorNumber.isEmpty() && !collectorNumber.equals(IPaperCard.NO_COLLECTOR_NUMBER)) {
sb.append(collectorNumber).append("_");
}
sb.append(StringUtils.stripAccents(name));
sb.append(artChop ? ".artcrop" : ".fullborder");
sb.append(".jpg");
return sb.toString();
}
public static String getImageRelativePath(PaperCard cp, String face, boolean includeSet, boolean isDownloadUrl) {
final String nameToUse = cp == null ? null : getNameToUse(cp, face);
if (nameToUse == null) {
@@ -113,7 +102,7 @@ public class ImageUtil {
if (includeSet) {
String editionAliased = isDownloadUrl ? StaticData.instance().getEditions().getCode2ByCode(edition) : ImageKeys.getSetFolder(edition);
if (editionAliased == "") //FIXME: Custom Cards Workaround
if (editionAliased.isEmpty()) //FIXME: Custom Cards Workaround
editionAliased = edition;
return TextUtil.concatNoSpace(editionAliased, "/", fname);
} else {
@@ -136,15 +125,25 @@ public class ImageUtil {
else
return null;
} else if (face.equals("white")) {
return card.getImageName(CardStateName.SpecializeW);
if (card.getWSpecialize() != null) {
return card.getWSpecialize().getName();
}
} else if (face.equals("blue")) {
return card.getImageName(CardStateName.SpecializeU);
if (card.getUSpecialize() != null) {
return card.getUSpecialize().getName();
}
} else if (face.equals("black")) {
return card.getImageName(CardStateName.SpecializeB);
if (card.getBSpecialize() != null) {
return card.getBSpecialize().getName();
}
} else if (face.equals("red")) {
return card.getImageName(CardStateName.SpecializeR);
if (card.getRSpecialize() != null) {
return card.getRSpecialize().getName();
}
} else if (face.equals("green")) {
return card.getImageName(CardStateName.SpecializeG);
if (card.getGSpecialize() != null) {
return card.getGSpecialize().getName();
}
} else if (CardSplitType.Split == cp.getRules().getSplitType()) {
return card.getMainPart().getName() + card.getOtherPart().getName();
} else if (!IPaperCard.NO_FUNCTIONAL_VARIANT.equals(cp.getFunctionalVariant())) {
@@ -153,86 +152,66 @@ public class ImageUtil {
return cp.getName();
}
public static String getNameToUse(PaperCard cp, CardStateName face) {
if (!IPaperCard.NO_FUNCTIONAL_VARIANT.equals(cp.getFunctionalVariant())) {
return cp.getFunctionalVariant();
}
final CardRules card = cp.getRules();
return card.getImageName(face);
}
public static String getImageKey(PaperCard cp, String face, boolean includeSet) {
return getImageRelativePath(cp, face, includeSet, false);
}
public static String getImageKey(PaperCard cp, CardStateName face) {
String name = getNameToUse(cp, face);
String number = cp.getCollectorNumber();
String suffix = "";
switch (face) {
case SpecializeB:
number += "b";
break;
case SpecializeG:
number += "g";
break;
case SpecializeR:
number += "r";
break;
case SpecializeU:
number += "u";
break;
case SpecializeW:
number += "w";
break;
case Meld:
case Modal:
case Secondary:
case Transformed:
suffix = ImageKeys.BACKFACE_POSTFIX;
break;
case Flipped:
break; // add info to rotate the image?
default:
break;
};
return ImageKeys.CARD_PREFIX + name + CardDb.NameSetSeparator + cp.getEdition()
+ CardDb.NameSetSeparator + number + CardDb.NameSetSeparator + cp.getArtIndex() + suffix;
}
public static String getDownloadUrl(PaperCard cp, String face) {
return getImageRelativePath(cp, face, true, true);
}
public static String getScryfallDownloadUrl(String collectorNumber, String setCode, String langCode, String faceParam, boolean useArtCrop){
return getScryfallDownloadUrl(collectorNumber, setCode, langCode, faceParam, useArtCrop, false);
public static String getScryfallDownloadUrl(PaperCard cp, String face, String setCode, String langCode, boolean useArtCrop){
return getScryfallDownloadUrl(cp, face, setCode, langCode, useArtCrop, false);
}
public static String getScryfallDownloadUrl(String collectorNumber, String setCode, String langCode, String faceParam, boolean useArtCrop, boolean hyphenateAlchemy){
public static String getScryfallDownloadUrl(PaperCard cp, String face, String setCode, String langCode, boolean useArtCrop, boolean hyphenateAlchemy){
String editionCode;
if (setCode != null && !setCode.isEmpty())
editionCode = setCode;
else
editionCode = cp.getEdition().toLowerCase();
String cardCollectorNumber = cp.getCollectorNumber();
// Hack to account for variations in Arabian Nights
collectorNumber = collectorNumber.replace("+", "");
cardCollectorNumber = cardCollectorNumber.replace("+", "");
// override old planechase sets from their modified id since scryfall move the planechase cards outside their original setcode
if (collectorNumber.startsWith("OHOP")) {
setCode = "ohop";
collectorNumber = collectorNumber.substring("OHOP".length());
} else if (collectorNumber.startsWith("OPCA")) {
setCode = "opca";
collectorNumber = collectorNumber.substring("OPCA".length());
} else if (collectorNumber.startsWith("OPC2")) {
setCode = "opc2";
collectorNumber = collectorNumber.substring("OPC2".length());
if (cardCollectorNumber.startsWith("OHOP")) {
editionCode = "ohop";
cardCollectorNumber = cardCollectorNumber.substring("OHOP".length());
} else if (cardCollectorNumber.startsWith("OPCA")) {
editionCode = "opca";
cardCollectorNumber = cardCollectorNumber.substring("OPCA".length());
} else if (cardCollectorNumber.startsWith("OPC2")) {
editionCode = "opc2";
cardCollectorNumber = cardCollectorNumber.substring("OPC2".length());
} else if (hyphenateAlchemy) {
if (!collectorNumber.startsWith("A")) {
if (!cardCollectorNumber.startsWith("A")) {
return null;
}
collectorNumber = collectorNumber.replace("A", "A-");
cardCollectorNumber = cardCollectorNumber.replace("A", "A-");
}
String versionParam = useArtCrop ? "art_crop" : "normal";
if (!faceParam.isEmpty()) {
faceParam = (faceParam.equals("back") ? "&face=back" : "&face=front");
String faceParam = "";
if (cp.getRules().getOtherPart() != null) {
faceParam = (face.equals("back") ? "&face=back" : "&face=front");
} else if (cp.getRules().getSplitType() == CardSplitType.Meld
&& !cardCollectorNumber.endsWith("a")
&& !cardCollectorNumber.endsWith("b")) {
// Only the bottom half of a meld card shares a collector number.
// Hanweir Garrison EMN already has a appended.
cardCollectorNumber += face.equals("back") ? "b" : "a";
}
return String.format("%s/%s/%s?format=image&version=%s%s", setCode, collectorNumber,
String cardCollectorNumberEncoded;
try {
cardCollectorNumberEncoded = URLEncoder.encode(cardCollectorNumber, "UTF-8");
} catch (Exception e) {
// Unlikely, for the possibility that "UTF-8" is not supported.
System.err.println("UTF-8 encoding not supported on this device.");
cardCollectorNumberEncoded = cardCollectorNumber;
}
return String.format("%s/%s/%s?format=image&version=%s%s", editionCode, cardCollectorNumberEncoded,
langCode, versionParam, faceParam);
}
@@ -258,4 +237,4 @@ public class ImageUtil {
}
return out.toString();
}
}
}

View File

@@ -193,7 +193,9 @@ public class ForgeScript {
return sa.isCrew();
} else if (property.equals("Saddle")) {
return sa.isKeyword(Keyword.SADDLE);
} else if (property.equals("Cycling")) {
} else if (property.equals("Station")) {
return sa.isKeyword(Keyword.STATION);
}else if (property.equals("Cycling")) {
return sa.isCycling();
} else if (property.equals("Dash")) {
return sa.isDash();

View File

@@ -519,6 +519,7 @@ public class GameAction {
}
card.setZone(zoneTo);
}
copied.clearMergedCards();
} else {
storeChangesZoneAll(copied, zoneFrom, zoneTo, params);
// "enter the battlefield as a copy" - apply code here
@@ -548,20 +549,20 @@ public class GameAction {
copied.updateStateForView();
// we don't want always trigger before counters are placed
game.getTriggerHandler().suppressMode(TriggerType.Always);
// Need to apply any static effects to produce correct triggers
checkStaticAbilities();
// needed for counters + ascend
if (!suppress && toBattlefield) {
game.getTriggerHandler().registerActiveTrigger(copied, false);
}
if (!table.isEmpty()) {
// we don't want always trigger before counters are placed
game.getTriggerHandler().suppressMode(TriggerType.Always);
// Need to apply any static effects to produce correct triggers
checkStaticAbilities();
// do ETB counters after zone add
table.replaceCounterEffect(game, null, true, true, params);
game.getTriggerHandler().clearSuppression(TriggerType.Always);
}
// do ETB counters after zone add
table.replaceCounterEffect(game, null, true, true, params);
game.getTriggerHandler().clearSuppression(TriggerType.Always);
// update static abilities after etb counters have been placed
checkStaticAbilities();
@@ -645,7 +646,8 @@ public class GameAction {
// Ask controller if it wants to be on top or bottom of other meld.
unmeldPosition++;
}
changeZone(null, zoneTo, unmeld, position, cause, params);
unmeld = changeZone(null, zoneTo, unmeld, position, cause, params);
storeChangesZoneAll(unmeld, zoneFrom, zoneTo, params);
}
} else if (toBattlefield) {
for (Player p : game.getPlayers()) {
@@ -2794,6 +2796,7 @@ public class GameAction {
if (aura == null) {
return false;
}
aura.setActivatingPlayer(source.getController());
Set<ZoneType> zones = EnumSet.noneOf(ZoneType.class);
boolean canTargetPlayer = false;

View File

@@ -130,7 +130,6 @@ public enum AbilityKey {
SourceSA("SourceSA"),
SpellAbility("SpellAbility"),
SpellAbilityTargets("SpellAbilityTargets"),
StackInstance("StackInstance"),
StackSa("StackSa"),
SurveilNum("SurveilNum"),
Target("Target"),
@@ -140,7 +139,7 @@ public enum AbilityKey {
Valiant("Valiant"),
Won("Won"),
// below used across different Replacements, don't reuse
// below shared across different Replacements, don't reuse
InternalTriggerTable("InternalTriggerTable"),
SimultaneousETB("SimultaneousETB"); // for CR 614.13c

View File

@@ -663,7 +663,7 @@ public class AbilityUtils {
Object o = root.getTriggeringObject(AbilityKey.fromString(calcX[0].substring(9)));
val = o instanceof Player ? playerXProperty((Player) o, calcX[1], card, ability) : 0;
}
else if (calcX[0].equals("TriggeredSpellAbility") || calcX[0].equals("TriggeredStackInstance") || calcX[0].equals("SpellTargeted")) {
else if (calcX[0].equals("TriggeredSpellAbility") || calcX[0].equals("SpellTargeted")) {
final SpellAbility sat = Iterables.getFirst(getDefinedSpellAbilities(card, calcX[0], sa), null);
val = sat == null ? 0 : xCount(sat.getHostCard(), calcX[1], sat);
}
@@ -1281,8 +1281,6 @@ public class AbilityUtils {
final Object o = root.getTriggeringObject(AbilityKey.fromString(triggeringType));
if (o instanceof SpellAbility) {
s = (SpellAbility) o;
} else if (o instanceof SpellAbilityStackInstance) {
s = ((SpellAbilityStackInstance) o).getSpellAbility();
}
} else if (defined.endsWith("Targeted") && sa instanceof SpellAbility) {
final List<TargetChoices> targets = defined.startsWith("This") ? Arrays.asList(((SpellAbility)sa).getTargets()) : ((SpellAbility)sa).getAllTargetChoices();
@@ -1685,11 +1683,11 @@ public class AbilityUtils {
return doXMath(x, expr, c, ctb);
} else if (TriggerType.SpellCast.equals(t.getMode())) {
// Cast Trigger like Hydroid Krasis
SpellAbilityStackInstance castSI = (SpellAbilityStackInstance) root.getTriggeringObject(AbilityKey.StackInstance);
if (castSI == null || castSI.getSpellAbility().getXManaCostPaid() == null) {
SpellAbility castSA = (SpellAbility) root.getTriggeringObject(AbilityKey.SpellAbility);
if (castSA == null || castSA.getXManaCostPaid() == null) {
return doXMath(0, expr, c, ctb);
}
return doXMath(castSI.getSpellAbility().getXManaCostPaid(), expr, c, ctb);
return doXMath(castSA.getXManaCostPaid(), expr, c, ctb);
} else if (TriggerType.Cycled.equals(t.getMode())) {
SpellAbility cycleSA = (SpellAbility) sa.getTriggeringObject(AbilityKey.Cause);
if (cycleSA == null || cycleSA.getXManaCostPaid() == null) {
@@ -3751,6 +3749,10 @@ public class AbilityUtils {
return Aggregates.max(paidList, Card::getNetToughness);
}
if (string.startsWith("TapPowerValue")) {
return CardLists.getTotalPower(paidList, ctb);
}
if (string.startsWith("SumToughness")) {
return Aggregates.sum(paidList, Card::getNetToughness);
}

View File

@@ -37,7 +37,7 @@ public class ChangeTargetsEffect extends SpellAbilityEffect {
final Player chooser = sa.hasParam("Chooser") ? getDefinedPlayersOrTargeted(sa, "Chooser").get(0) : activator;
final MagicStack stack = activator.getGame().getStack();
for (final SpellAbility tgtSA : sas) {
SpellAbilityStackInstance si = stack.getInstanceMatchingSpellAbilityID(tgtSA);
if (si == null) {
@@ -72,8 +72,8 @@ public class ChangeTargetsEffect extends SpellAbilityEffect {
// 2. prepare new target choices
SpellAbilityStackInstance replaceIn = chosenTarget.getKey();
GameObject oldTarget = chosenTarget.getValue();
TargetChoices oldTargetBlock = replaceIn.getTargetChoices();
TargetChoices newTargetBlock = oldTargetBlock.clone();
TargetChoices newTargetBlock = replaceIn.getTargetChoices();
TargetChoices oldTargetBlock = newTargetBlock.clone();
// gets the divided value from old target
Integer div = oldTargetBlock.getDividedValue(oldTarget);
// 3. test if updated choices would be correct.
@@ -87,7 +87,7 @@ public class ChangeTargetsEffect extends SpellAbilityEffect {
if (div != null) {
newTargetBlock.addDividedAllocation(newTarget, div);
}
replaceIn.updateTarget(newTargetBlock, sa.getHostCard());
replaceIn.updateTarget(oldTargetBlock, sa.getHostCard());
}
} else {
while (changingTgtSI != null) {
@@ -104,25 +104,26 @@ public class ChangeTargetsEffect extends SpellAbilityEffect {
if (candidates.isEmpty()) {
return;
}
changingTgtSA.resetTargets();
GameEntity choice = Aggregates.random(candidates);
TargetChoices oldTarget = changingTgtSA.getTargets();
changingTgtSA.resetTargets();
changingTgtSA.getTargets().add(choice);
if (changingTgtSA.isDividedAsYouChoose()) {
changingTgtSA.addDividedAllocation(choice, div);
}
changingTgtSI.updateTarget(changingTgtSA.getTargets(), sa.getHostCard());
changingTgtSI.updateTarget(oldTarget, sa.getHostCard());
}
else if (sa.hasParam("DefinedMagnet")) {
GameObject newTarget = Iterables.getFirst(getDefinedCardsOrTargeted(sa, "DefinedMagnet"), null);
if (newTarget != null && changingTgtSA.canTarget(newTarget)) {
int div = changingTgtSA.getTotalDividedValue();
TargetChoices oldTarget = changingTgtSA.getTargets();
changingTgtSA.resetTargets();
changingTgtSA.getTargets().add(newTarget);
changingTgtSI.updateTarget(changingTgtSA.getTargets(), sa.getHostCard());
if (changingTgtSA.isDividedAsYouChoose()) {
changingTgtSA.addDividedAllocation(newTarget, div);
}
changingTgtSI.updateTarget(oldTarget, sa.getHostCard());
}
} else {
// Update targets, with a potential new target
@@ -132,8 +133,9 @@ public class ChangeTargetsEffect extends SpellAbilityEffect {
source = changingTgtSA.getTargetCard();
}
Predicate<GameObject> filter = sa.hasParam("TargetRestriction") ? GameObjectPredicates.restriction(sa.getParam("TargetRestriction").split(","), activator, source, sa) : null;
TargetChoices newTarget = chooser.getController().chooseNewTargetsFor(changingTgtSA, filter, false);
changingTgtSI.updateTarget(newTarget, sa.getHostCard());
TargetChoices oldTarget = changingTgtSA.getTargets();
chooser.getController().chooseNewTargetsFor(changingTgtSA, filter, false);
changingTgtSI.updateTarget(oldTarget, sa.getHostCard());
}
}
changingTgtSI = changingTgtSI.getSubInstance();

View File

@@ -2,6 +2,7 @@ package forge.game.ability.effects;
import com.google.common.collect.Lists;
import forge.GameCommand;
import forge.game.Game;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
@@ -36,6 +37,10 @@ public class TextBoxExchangeEffect extends SpellAbilityEffect {
@Override
public void resolve(final SpellAbility sa) {
if (!checkValidDuration(sa.getParam("Duration"), sa)) {
return;
}
final List<Card> tgtCards = getTargetCards(sa);
if (tgtCards.size() < 2) {
return;
@@ -55,6 +60,37 @@ public class TextBoxExchangeEffect extends SpellAbilityEffect {
swapTextBox(c1, data2, ts);
swapTextBox(c2, data1, ts);
if (sa.hasParam("Duration")) {
final GameCommand revertTextExchange = new GameCommand() {
private static final long serialVersionUID = 5331255714437747836L;
@Override
public void run() {
// Check if the cards are still there
Card card1 = game.getCardState(c1, null);
Card card2 = game.getCardState(c2, null);
if (card1 != null && c1.equalsWithGameTimestamp(card1)) {
card1.removeChangedCardTraits(ts, 0);
card1.removeChangedCardKeywords(ts, 0, false);
card1.updateChangedText();
card1.updateStateForView();
game.fireEvent(new GameEventCardStatsChanged(card1));
}
if (card2 != null && c2.equalsWithGameTimestamp(card2)) {
card2.removeChangedCardTraits(ts, 0);
card2.removeChangedCardKeywords(ts, 0, false);
card2.updateChangedText();
card2.updateStateForView();
game.fireEvent(new GameEventCardStatsChanged(card2));
}
}
};
addUntilCommand(sa, revertTextExchange);
}
game.fireEvent(new GameEventCardStatsChanged(c1));
game.fireEvent(new GameEventCardStatsChanged(c2));
}

View File

@@ -1407,6 +1407,10 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
public final CardCollectionView getMergedCards() {
return CardCollection.getView(mergedCards);
}
public final void setMergedCards(Iterable<Card> mc) {
mergedCards = new CardCollection(mc);
}
public final Card getTopMergedCard() {
return mergedCards.get(0);
}
@@ -2739,7 +2743,8 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
|| keyword.startsWith("Class") || keyword.startsWith("Blitz")
|| keyword.startsWith("Specialize") || keyword.equals("Ravenous")
|| keyword.equals("For Mirrodin") || keyword.equals("Job select") || keyword.startsWith("Craft")
|| keyword.startsWith("Landwalk") || keyword.startsWith("Visit") || keyword.startsWith("Mobilize")) {
|| keyword.startsWith("Landwalk") || keyword.startsWith("Visit") || keyword.startsWith("Mobilize")
|| keyword.startsWith("Station")) {
// keyword parsing takes care of adding a proper description
} else if (keyword.equals("Read ahead")) {
sb.append(Localizer.getInstance().getMessage("lblReadAhead")).append(" (").append(Localizer.getInstance().getMessage("lblReadAheadDesc"));
@@ -4229,12 +4234,12 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
return ImmutableList.of();
}
Iterable<CardChangedType> byText = changedTypeByText == null ? ImmutableList.of() : ImmutableList.of(this.changedTypeByText);
return ImmutableList.copyOf(Iterables.concat(
changedCardTypesByText.values(), // Layer 3
byText, // Layer 3 by Word Changes,
changedCardTypesCharacterDefining.values(), // Layer 4
changedCardTypes.values() // Layer 6
));
return ImmutableList.<CardChangedType>builder()
.addAll(changedCardTypesByText.values()) // Layer 3
.addAll(byText) // Layer 3 by Word Changes,
.addAll(changedCardTypesCharacterDefining.values()) // Layer 4
.addAll(changedCardTypes.values()) // Layer 6
.build();
}
public boolean clearChangedCardTypes() {
@@ -6515,6 +6520,29 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
if(uiCard != null)
uiCard.currentState.setImageKey(iFN);
}
public final void setImageKey(final IPaperCard ipc, final CardStateName stateName) {
if (ipc == null)
return;
switch (stateName) {
case SpecializeB:
setImageKey(ipc.getCardBSpecImageKey());
break;
case SpecializeR:
setImageKey(ipc.getCardRSpecImageKey());
break;
case SpecializeG:
setImageKey(ipc.getCardGSpecImageKey());
break;
case SpecializeU:
setImageKey(ipc.getCardUSpecImageKey());
break;
case SpecializeW:
setImageKey(ipc.getCardWSpecImageKey());
break;
default:
break;
}
}
public String getImageKey(CardStateName state) {
if (!getRenderForUI()) {
@@ -6814,6 +6842,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
}
public final void setSpecialized(final boolean bool) {
specialized = bool;
setImageKey(getPaperCard(), getCurrentStateName());
}
public final boolean canSpecialize() {
return getRules() != null && getRules().getSplitType() == CardSplitType.Specialize;

View File

@@ -66,8 +66,15 @@ public class CardCopyService {
out.setCollectible(copyFrom.isCollectible());
// this's necessary for forge.game.GameAction.unattachCardLeavingBattlefield(Card)
out.setAttachedCards(copyFrom.getAttachedCards());
out.setEntityAttachedTo(copyFrom.getEntityAttachedTo());
if (copyFrom.hasCardAttachments()) {
out.setAttachedCards(copyFrom.getAttachedCards());
}
if (copyFrom.isAttachedToEntity()) {
out.setEntityAttachedTo(copyFrom.getEntityAttachedTo());
}
if (copyFrom.hasMergedCard()) {
out.setMergedCards(copyFrom.getMergedCards());
}
out.setLeavesPlayCommands(copyFrom.getLeavesPlayCommands());

View File

@@ -187,7 +187,7 @@ public class CardFactory {
c.setRarity(cp.getRarity());
// Would like to move this away from in-game entities
String originalPicture = cp.getCardImageKey();
String originalPicture = cp.getImageKey(false);
c.setImageKey(originalPicture);
if(cp.isToken())
@@ -198,11 +198,11 @@ public class CardFactory {
if (c.hasAlternateState()) {
if (c.isFlipCard()) {
c.setState(CardStateName.Flipped, false);
c.setImageKey(cp.getCardAltImageKey());
c.setImageKey(cp.getImageKey(true));
}
else if (c.isDoubleFaced() && cardRules != null) {
c.setState(cardRules.getSplitType().getChangedStateName(), false);
c.setImageKey(cp.getCardAltImageKey());
c.setImageKey(cp.getImageKey(true));
}
else if (c.isSplitCard()) {
c.setState(CardStateName.LeftSplit, false);
@@ -216,23 +216,23 @@ public class CardFactory {
c.setImageKey(originalPicture);
} else if (c.canSpecialize()) {
c.setState(CardStateName.SpecializeW, false);
c.setImageKey(cp.getCardWSpecImageKey());
c.setImageKey(cp.getImageKey(false) + ImageKeys.SPECFACE_W);
c.setSetCode(cp.getEdition());
c.setRarity(cp.getRarity());
c.setState(CardStateName.SpecializeU, false);
c.setImageKey(cp.getCardUSpecImageKey());
c.setImageKey(cp.getImageKey(false) + ImageKeys.SPECFACE_U);
c.setSetCode(cp.getEdition());
c.setRarity(cp.getRarity());
c.setState(CardStateName.SpecializeB, false);
c.setImageKey(cp.getCardBSpecImageKey());
c.setImageKey(cp.getImageKey(false) + ImageKeys.SPECFACE_B);
c.setSetCode(cp.getEdition());
c.setRarity(cp.getRarity());
c.setState(CardStateName.SpecializeR, false);
c.setImageKey(cp.getCardRSpecImageKey());
c.setImageKey(cp.getImageKey(false) + ImageKeys.SPECFACE_R);
c.setSetCode(cp.getEdition());
c.setRarity(cp.getRarity());
c.setState(CardStateName.SpecializeG, false);
c.setImageKey(cp.getCardGSpecImageKey());
c.setImageKey(cp.getImageKey(false) + ImageKeys.SPECFACE_G);
c.setSetCode(cp.getEdition());
c.setRarity(cp.getRarity());
}
@@ -742,7 +742,7 @@ public class CardFactory {
TextUtil.fastReplace(host.getName(), ",", ""),
" ", "_").toLowerCase();
String set = host.getSetCode().toLowerCase();
state.setImageKey(ImageKeys.getTokenKey("offspring_" + name + "_" + set));
state.setImageKey(ImageKeys.getTokenKey("offspring_" + name + "|" + set));
}

View File

@@ -2122,23 +2122,13 @@ public class CardFactoryUtil {
} else if (keyword.startsWith("Amplify")) {
final String[] ampString = keyword.split(":");
final String amplifyMagnitude = ampString[1];
final String ampTypes = ampString[2];
String[] refinedTypes = ampTypes.split(",");
final StringBuilder types = new StringBuilder();
for (int i = 0; i < refinedTypes.length; i++) {
types.append("Card.").append(refinedTypes[i]).append("+YouCtrl");
if (i + 1 != refinedTypes.length) {
types.append(",");
}
}
// Setup ETB replacement effects
final String actualRep = "Event$ Moved | Destination$ Battlefield | ValidCard$ Card.Self |"
+ " | ReplacementResult$ Updated | Description$ Amplify " + amplifyMagnitude + " ("
+ inst.getReminderText() + ")";
final String abString = "DB$ Reveal | AnyNumber$ True | RevealValid$ "
+ types.toString() + " | RememberRevealed$ True";
final String abString = "DB$ Reveal | AnyNumber$ True | RevealValid$ Card.YouOwn+sharesCreatureTypeWith+Other+NotDefinedReplacedSimultaneousETB | RememberRevealed$ True";
SpellAbility saReveal = AbilityFactory.getAbility(abString, card);
@@ -2792,7 +2782,7 @@ public class CardFactoryUtil {
final String[] k = keyword.split(":");
final Cost blitzCost = new Cost(k[1], false);
final SpellAbility newSA = card.getFirstSpellAbility().copyWithManaCostReplaced(host.getController(), blitzCost);
final SpellAbility newSA = card.getFirstSpellAbilityWithFallback().copyWithManaCostReplaced(host.getController(), blitzCost);
if (k.length > 2) {
newSA.getMapParams().put("ValidAfterStack", k[2]);
@@ -2905,7 +2895,7 @@ public class CardFactoryUtil {
}
desc += ")";
final SpellAbility sa = card.getFirstSpellAbility();
final SpellAbility sa = card.getFirstSpellAbilityWithFallback();
final SpellAbility newSA = sa.copyWithDefinedCost(new Cost(costStr, false));
newSA.getRestrictions().setIsPresent(validStr + ".YouCtrl+CanBeSacrificedBy");
@@ -3591,6 +3581,15 @@ public class CardFactoryUtil {
sa.setSVar("ScavengeX", "Exiled$CardPower");
sa.setIntrinsic(intrinsic);
inst.addSpellAbility(sa);
} else if (keyword.startsWith("Station")) {
String effect = "AB$ PutCounter | Cost$ tapXType<1/Creature.Other> | Defined$ Self " +
"| CounterType$ CHARGE | CounterNum$ StationX | SorcerySpeed$ True " +
"| CostDesc$ | SpellDescription$ Station (" + inst.getReminderText() + ")";
final SpellAbility sa = AbilityFactory.getAbility(effect, card);
sa.setSVar("StationX", "TappedCards$TapPowerValue");
sa.setIntrinsic(intrinsic);
inst.addSpellAbility(sa);
} else if (keyword.startsWith("Encore")) {
final String[] k = keyword.split(":");
final String manacost = k[1];

View File

@@ -421,13 +421,13 @@ public class CardLists {
* @param cardList the list of creature cards for which to sum the power
* @param crew for cards that crew with toughness rather than power
*/
public static int getTotalPower(Iterable<Card> cardList, SpellAbility sa) {
public static int getTotalPower(Iterable<Card> cardList, CardTraitBase ctb) {
int total = 0;
for (final Card crd : cardList) {
if (StaticAbilityTapPowerValue.withToughness(crd, sa)) {
if (StaticAbilityTapPowerValue.withToughness(crd, ctb)) {
total += Math.max(0, crd.getNetToughness());
} else {
int m = StaticAbilityTapPowerValue.getMod(crd, sa);
int m = StaticAbilityTapPowerValue.getMod(crd, ctb);
total += Math.max(0, crd.getNetPower() + m);
}
}

View File

@@ -52,6 +52,7 @@ import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class CardState extends GameObject implements IHasSVars, ITranslatable {
private String name = "";
@@ -366,14 +367,16 @@ public class CardState extends GameObject implements IHasSVars, ITranslatable {
public final FCollectionView<SpellAbility> getManaAbilities() {
FCollection<SpellAbility> newCol = new FCollection<>();
updateSpellAbilities(newCol, true);
newCol.addAll(abilities.stream().filter(SpellAbility::isManaAbility).toList());
// stream().toList() causes crash on Android, use Collectors.toList()
newCol.addAll(abilities.stream().filter(SpellAbility::isManaAbility).collect(Collectors.toList()));
card.updateSpellAbilities(newCol, this, true);
return newCol;
}
public final FCollectionView<SpellAbility> getNonManaAbilities() {
FCollection<SpellAbility> newCol = new FCollection<>();
updateSpellAbilities(newCol, false);
newCol.addAll(abilities.stream().filter(Predicate.not(SpellAbility::isManaAbility)).toList());
// stream().toList() causes crash on Android, use Collectors.toList()
newCol.addAll(abilities.stream().filter(Predicate.not(SpellAbility::isManaAbility)).collect(Collectors.toList()));
card.updateSpellAbilities(newCol, this, false);
return newCol;
}
@@ -385,7 +388,10 @@ public class CardState extends GameObject implements IHasSVars, ITranslatable {
CardState leftState = getCard().getState(CardStateName.LeftSplit);
Collection<SpellAbility> leftAbilities = leftState.abilities;
if (null != mana) {
leftAbilities = leftAbilities.stream().filter(mana ? SpellAbility::isManaAbility : Predicate.not(SpellAbility::isManaAbility)).toList();
leftAbilities = leftAbilities.stream()
.filter(mana ? SpellAbility::isManaAbility : Predicate.not(SpellAbility::isManaAbility))
// stream().toList() causes crash on Android, use Collectors.toList()
.collect(Collectors.toList());
}
newCol.addAll(leftAbilities);
leftState.updateSpellAbilities(newCol, mana);
@@ -394,7 +400,10 @@ public class CardState extends GameObject implements IHasSVars, ITranslatable {
CardState rightState = getCard().getState(CardStateName.RightSplit);
Collection<SpellAbility> rightAbilities = rightState.abilities;
if (null != mana) {
rightAbilities = rightAbilities.stream().filter(mana ? SpellAbility::isManaAbility : Predicate.not(SpellAbility::isManaAbility)).toList();
rightAbilities = rightAbilities.stream()
.filter(mana ? SpellAbility::isManaAbility : Predicate.not(SpellAbility::isManaAbility))
// stream().toList() causes crash on Android, use Collectors.toList()
.collect(Collectors.toList());
}
newCol.addAll(rightAbilities);
rightState.updateSpellAbilities(newCol, mana);
@@ -428,8 +437,7 @@ public class CardState extends GameObject implements IHasSVars, ITranslatable {
CardTypeView type = getTypeWithChanges();
if (type.isLand()) {
if (landAbility == null) {
landAbility = new LandAbility(card);
landAbility.setCardState(this);
landAbility = new LandAbility(card, this);
}
newCol.add(landAbility);
} else if (type.isAura()) {

View File

@@ -101,7 +101,6 @@ public class CardView extends GameEntityView {
set(TrackableProperty.Controller, ownerAndController);
set(TrackableProperty.ImageKey, imageKey);
}
public PlayerView getOwner() {
return get(TrackableProperty.Owner);
}
@@ -1061,6 +1060,8 @@ public class CardView extends GameEntityView {
mergedCollection.add(card);
}
}
} else {
set(TrackableProperty.MergedCards, null);
}
updateMergeCollections(mergedCollection);
@@ -1335,29 +1336,9 @@ public class CardView extends GameEntityView {
}
void updateImageKey(Card c) {
set(TrackableProperty.ImageKey, c.getImageKey());
// currently only works for Cards
if (!c.getGamePieceType().equals(GamePieceType.CARD)) {
return;
}
IPaperCard pc = c.getPaperCard();
if (pc != null) {
set(TrackableProperty.Artist, pc.getArtist());
}
}
void updateImageKey(CardState c) {
set(TrackableProperty.ImageKey, c.getImageKey());
// currently only works for Cards
if (!c.getCard().getGamePieceType().equals(GamePieceType.CARD)) {
return;
}
IPaperCard pc = c.getCard().getPaperCard();
if (pc != null) { // currently Artist is per Card
set(TrackableProperty.Artist, pc.getArtist());
}
}
public String getArtist() {
return get(TrackableProperty.Artist);
}
public CardTypeView getType() {

View File

@@ -282,7 +282,7 @@ public class CostPayment extends ManaConversionMatrix {
private static List<Pair<Mana, Integer>> selectManaToPayFor(final ManaPool manapool, final ManaCostShard shard,
final SpellAbility saBeingPaidFor, final byte colorsPaid, Map<String, Integer> xManaCostPaidByColor) {
final List<Pair<Mana, Integer>> weightedOptions = new ArrayList<>();
for (final Mana thisMana : manapool) {
for (final Mana thisMana : Lists.newArrayList(manapool)) {
if (shard == ManaCostShard.COLORED_X && !ManaCostBeingPaid.canColoredXShardBePaidByColor(MagicColor.toShortString(thisMana.getColor()), xManaCostPaidByColor)) {
continue;
}

View File

@@ -178,6 +178,7 @@ public enum Keyword {
SQUAD("Squad", KeywordWithCost.class, false, "As an additional cost to cast this spell, you may pay %s any number of times. When this creature enters, create that many tokens that are copies of it."),
START_YOUR_ENGINES("Start your engines", SimpleKeyword.class, true, "If you have no speed, it starts at 1. It increases once on each of your turns when an opponent loses life. Max speed is 4."),
STARTING_INTENSITY("Starting intensity", KeywordWithAmount.class, true, null),
STATION("Station", KeywordWithAmount.class, false, "Tap another creature you control: Put charge counters equal to its power on this Spacecraft. Station only as a sorcery. Its an artifact creature at %d+."),
STORM("Storm", SimpleKeyword.class, false, "When you cast this spell, copy it for each other spell that was cast before it this turn. You may choose new targets for the copies."),
STRIVE("Strive", KeywordWithCost.class, false, "CARDNAME costs %s more to cast for each target beyond the first."),
SUNBURST("Sunburst", SimpleKeyword.class, false, "This enters with either a +1/+1 or charge counter on it for each color of mana spent to cast it based on whether it's a creature."),

View File

@@ -904,10 +904,8 @@ public class Player extends GameEntity implements Comparable<Player> {
runParams.put(AbilityKey.CounterAmount, oldValue + i + 1);
getGame().getTriggerHandler().runTrigger(TriggerType.CounterAdded, AbilityKey.newMap(runParams), false);
}
if (addAmount > 0) {
runParams.put(AbilityKey.CounterAmount, addAmount);
getGame().getTriggerHandler().runTrigger(TriggerType.CounterAddedOnce, AbilityKey.newMap(runParams), false);
}
runParams.put(AbilityKey.CounterAmount, addAmount);
getGame().getTriggerHandler().runTrigger(TriggerType.CounterAddedOnce, AbilityKey.newMap(runParams), false);
if (table != null) {
table.put(source, this, counterType, addAmount);
}
@@ -3605,7 +3603,7 @@ public class Player extends GameEntity implements Comparable<Player> {
}
String trigStr = "Mode$ Phase | Phase$ Main1 | ValidPlayer$ You | TriggerZones$ Command | TriggerDescription$ " +
"At the beginning of your precombat main phase, if you have any rad counters, mill that many cards. For each nonland card milled this way, you lose 1 life and a rad counter.";
"At the beginning of your precombat main phase, if you have any rad counters, mill that many cards. For each nonland card milled this way, you lose 1 life and a rad counter.";
Trigger tr = TriggerHandler.parseTrigger(trigStr, radiationEffect, true);
SpellAbility sa = AbilityFactory.getAbility("DB$ InternalRadiation", radiationEffect);

View File

@@ -22,6 +22,7 @@ import com.esotericsoftware.minlog.Log;
import forge.card.mana.ManaCost;
import forge.game.Game;
import forge.game.card.Card;
import forge.game.card.CardState;
import forge.game.cost.Cost;
/**
@@ -37,6 +38,9 @@ public abstract class Ability extends SpellAbility {
protected Ability(final Card sourceCard, final ManaCost manaCost) {
this(sourceCard, new Cost(manaCost, true), null);
}
protected Ability(final Card sourceCard, final ManaCost manaCost, final CardState state) {
super(sourceCard, new Cost(manaCost, true), null, state);
}
protected Ability(final Card sourceCard, final ManaCost manaCost, SpellAbilityView view0) {
this(sourceCard, new Cost(manaCost, true), view0);
}

View File

@@ -19,6 +19,7 @@ package forge.game.spellability;
import forge.card.mana.ManaCost;
import forge.game.card.Card;
import forge.game.card.CardState;
import forge.game.cost.Cost;
/**
@@ -43,6 +44,9 @@ public abstract class AbilityStatic extends Ability implements Cloneable {
public AbilityStatic(final Card sourceCard, final ManaCost manaCost) {
super(sourceCard, manaCost);
}
public AbilityStatic(final Card sourceCard, final ManaCost manaCost, final CardState state) {
super(sourceCard, manaCost, state);
}
public AbilityStatic(final Card sourceCard, final Cost abCost, final TargetRestrictions tgt) {
super(sourceCard, abCost);

View File

@@ -21,6 +21,7 @@ import forge.card.CardStateName;
import forge.card.mana.ManaCost;
import forge.game.card.Card;
import forge.game.card.CardCopyService;
import forge.game.card.CardState;
import forge.game.player.Player;
import forge.game.staticability.StaticAbility;
import forge.game.zone.ZoneType;
@@ -31,8 +32,8 @@ import org.apache.commons.lang3.StringUtils;
public class LandAbility extends AbilityStatic {
public LandAbility(Card sourceCard) {
super(sourceCard, ManaCost.NO_COST);
public LandAbility(Card sourceCard, CardState state) {
super(sourceCard, ManaCost.NO_COST, state);
getRestrictions().setZone(ZoneType.Hand);
}

View File

@@ -195,12 +195,18 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
}
protected SpellAbility(final Card iSourceCard, final Cost toPay) {
this(iSourceCard, toPay, null);
this(iSourceCard, toPay, null, null);
}
protected SpellAbility(final Card iSourceCard, final Cost toPay, SpellAbilityView view0) {
this(iSourceCard, toPay, view0, null);
}
protected SpellAbility(final Card iSourceCard, final Cost toPay, SpellAbilityView view0, CardState cs) {
id = nextId();
hostCard = iSourceCard;
payCosts = toPay;
if (cs != null) {
cardState = cs;
}
if (view0 == null) {
view0 = new SpellAbilityView(this);
}
@@ -1222,12 +1228,14 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
try {
clone = (SpellAbility) clone();
clone.id = lki ? id : nextId();
clone.view = new SpellAbilityView(clone, lki || host.getGame() == null ? null : host.getGame().getTracker());
// don't use setHostCard to not trigger the not copied parts yet
copyHelper(clone, host, lki || keepTextChanges);
// need CardState before View
clone.view = new SpellAbilityView(clone, lki || host.getGame() == null ? null : host.getGame().getTracker());
// always set this to false, it is only set in CopyEffect
clone.mayChooseNewTargets = false;

View File

@@ -133,18 +133,16 @@ public class SpellAbilityStackInstance implements IIdentifiable, IHasCardView {
return ability.getTargets();
}
public void updateTarget(TargetChoices target, Card cause) {
if (target != null) {
TargetChoices oldTarget = ability.getTargets();
ability.setTargets(target);
public void updateTarget(TargetChoices oldTC, Card cause) {
if (oldTC != null) {
stackDescription = ability.getStackDescription();
view.updateTargetCards(this);
view.updateTargetPlayers(this);
view.updateText(this);
Set<GameObject> distinctObjects = Sets.newHashSet();
for (final GameObject tgt : target) {
if (oldTarget != null && oldTarget.contains(tgt)) {
for (final GameObject tgt : ability.getTargets()) {
if (oldTC.contains(tgt)) {
// it was an old target, so don't trigger becomes target
continue;
}

View File

@@ -43,6 +43,7 @@ import forge.util.TextUtil;
import org.apache.commons.lang3.StringUtils;
import java.util.*;
import java.util.regex.Matcher;
import java.util.stream.Collectors;
/**
@@ -273,7 +274,7 @@ public final class StaticAbilityContinuous {
if (hostCard.hasChosenPlayer()) {
Player cp = hostCard.getChosenPlayer();
input = input.replaceAll("ChosenPlayerUID", String.valueOf(cp.getId()));
input = input.replaceAll("ChosenPlayerName", cp.getName());
input = input.replaceAll("ChosenPlayerName", Matcher.quoteReplacement(cp.getName()));
}
if (hostCard.hasNamedCard()) {
final String chosenName = hostCard.getNamedCard().replace(",", ";");

View File

@@ -1,20 +1,20 @@
package forge.game.staticability;
import forge.game.CardTraitBase;
import forge.game.Game;
import forge.game.card.Card;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
public class StaticAbilityTapPowerValue {
public static boolean withToughness(final Card card, final SpellAbility sa) {
public static boolean withToughness(final Card card, final CardTraitBase ctb) {
final Game game = card.getGame();
for (final Card ca : game.getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) {
for (final StaticAbility stAb : ca.getStaticAbilities()) {
if (!stAb.checkConditions(StaticAbilityMode.TapPowerValue)) {
continue;
}
if (withToughness(stAb, card, sa)) {
if (withToughness(stAb, card, ctb)) {
return true;
}
}
@@ -22,20 +22,20 @@ public class StaticAbilityTapPowerValue {
return false;
}
public static boolean withToughness(final StaticAbility stAb, final Card card, final SpellAbility sa) {
public static boolean withToughness(final StaticAbility stAb, final Card card, final CardTraitBase ctb) {
if (!stAb.getParam("Value").equals("Toughness")) {
return false;
}
if (!stAb.matchesValidParam("ValidCard", card)) {
return false;
}
if (!stAb.matchesValidParam("ValidSA", sa)) {
if (!stAb.matchesValidParam("ValidSA", ctb)) {
return false;
}
return true;
}
public static int getMod(final Card card, SpellAbility sa) {
public static int getMod(final Card card, final CardTraitBase ctb) {
int i = 0;
final Game game = card.getGame();
for (final Card ca : game.getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) {
@@ -46,11 +46,10 @@ public class StaticAbilityTapPowerValue {
if (!stAb.matchesValidParam("ValidCard", card)) {
continue;
}
if (!stAb.matchesValidParam("ValidSA", sa)) {
if (!stAb.matchesValidParam("ValidSA", ctb)) {
continue;
}
int t = Integer.parseInt(stAb.getParam("Value"));
i = i + t;
i += Integer.parseInt(stAb.getParam("Value"));
}
}
return i;

View File

@@ -83,7 +83,6 @@ public class TriggerBecomesTarget extends Trigger {
public final void setTriggeringObjects(final SpellAbility sa, Map<AbilityKey, Object> runParams) {
sa.setTriggeringObject(AbilityKey.Source, ((SpellAbility) runParams.get(AbilityKey.SourceSA)).getHostCard());
sa.setTriggeringObjectsFrom(runParams, AbilityKey.SourceSA, AbilityKey.Target);
sa.setTriggeringObject(AbilityKey.StackInstance, sa.getHostCard().getGame().getStack().getInstanceMatchingSpellAbilityID((SpellAbility) runParams.get(AbilityKey.SourceSA)));
}
@Override

View File

@@ -24,7 +24,6 @@ import java.util.Set;
import com.google.common.collect.Sets;
import forge.card.ColorSet;
import forge.game.Game;
import forge.game.GameEntity;
import forge.game.GameObject;
import forge.game.ability.AbilityKey;
@@ -35,7 +34,6 @@ import forge.game.card.CardUtil;
import forge.game.mana.Mana;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityStackInstance;
import forge.game.spellability.TargetChoices;
import forge.util.Expressions;
import forge.util.Localizer;
@@ -78,22 +76,9 @@ public class TriggerSpellAbilityCastOrCopy extends Trigger {
return false;
}
final Card cast = spellAbility.getHostCard();
final Game game = cast.getGame();
final SpellAbilityStackInstance si = game.getStack().getInstanceMatchingSpellAbilityID(spellAbility);
if (!matchesValidParam("ValidPlayer", runParams.get(AbilityKey.Player))) {
return false;
}
if (hasParam("ValidActivatingPlayer")) {
Player activator;
if (spellAbility.isManaAbility()) {
activator = (Player) runParams.get(AbilityKey.Activator);
} else if (si == null) {
return false;
} else {
activator = si.getSpellAbility().getActivatingPlayer();
}
Player activator = (Player) runParams.get(AbilityKey.Activator);
if (!matchesValidParam("ValidActivatingPlayer", activator)) {
return false;
@@ -143,9 +128,6 @@ public class TriggerSpellAbilityCastOrCopy extends Trigger {
if (hasParam("TargetsValid")) {
SpellAbility sa = spellAbility;
if (si != null) {
sa = si.getSpellAbility();
}
boolean validTgtFound = false;
while (sa != null && !validTgtFound) {
@@ -249,13 +231,10 @@ public class TriggerSpellAbilityCastOrCopy extends Trigger {
/** {@inheritDoc} */
@Override
public final void setTriggeringObjects(final SpellAbility sa, Map<AbilityKey, Object> runParams) {
final SpellAbility castSA = (SpellAbility) runParams.get(AbilityKey.SpellAbility);
final SpellAbilityStackInstance si = sa.getHostCard().getGame().getStack().getInstanceMatchingSpellAbilityID(castSA);
final SpellAbility saForTargets = si != null ? si.getSpellAbility() : castSA;
sa.setTriggeringObject(AbilityKey.Card, castSA.getHostCard());
sa.setTriggeringObject(AbilityKey.SpellAbility, castSA);
sa.setTriggeringObject(AbilityKey.StackInstance, si);
final List<TargetChoices> allTgts = saForTargets.getAllTargetChoices();
final SpellAbility cause = (SpellAbility) runParams.get(AbilityKey.SpellAbility);
sa.setTriggeringObject(AbilityKey.Card, cause.getHostCard());
sa.setTriggeringObject(AbilityKey.SpellAbility, cause);
final List<TargetChoices> allTgts = cause.getAllTargetChoices();
if (!allTgts.isEmpty()) {
final FCollection<GameEntity> saTargets = new FCollection<>();
for (TargetChoices tc : allTgts) {
@@ -263,11 +242,10 @@ public class TriggerSpellAbilityCastOrCopy extends Trigger {
}
sa.setTriggeringObject(AbilityKey.SpellAbilityTargets, saTargets);
}
sa.setTriggeringObject(AbilityKey.LifeAmount, castSA.getAmountLifePaid());
sa.setTriggeringObject(AbilityKey.LifeAmount, cause.getAmountLifePaid());
sa.setTriggeringObjectsFrom(
runParams,
AbilityKey.CardLKI,
AbilityKey.Player,
AbilityKey.Activator,
AbilityKey.CurrentStormCount,
AbilityKey.CurrentCastSpells

View File

@@ -267,7 +267,7 @@ public class MagicStack /* extends MyObservable */ implements Iterable<SpellAbil
addAbilityActivatedThisTurn(sp, source);
}
Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(source.getController());
Map<AbilityKey, Object> runParams = AbilityKey.newMap();
runParams.put(AbilityKey.Activator, activator);
runParams.put(AbilityKey.SpellAbility, sp);
game.getTriggerHandler().runTrigger(TriggerType.SpellAbilityCast, runParams, true);
@@ -355,7 +355,7 @@ public class MagicStack /* extends MyObservable */ implements Iterable<SpellAbil
si = push(sp, si, id);
// Copied spells aren't cast per se so triggers shouldn't run for them.
Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(sp.getHostCard().getController());
Map<AbilityKey, Object> runParams = AbilityKey.newMap();
if (sp.isSpell() && !sp.isCopied()) {
final Card lki = CardCopyService.getLKICopy(sp.getHostCard());
@@ -394,7 +394,7 @@ public class MagicStack /* extends MyObservable */ implements Iterable<SpellAbil
}
runParams.put(AbilityKey.Activator, sp.getActivatingPlayer());
runParams.put(AbilityKey.SpellAbility, si.getSpellAbility());
runParams.put(AbilityKey.SpellAbility, sp);
runParams.put(AbilityKey.CurrentStormCount, thisTurnCast.size());
runParams.put(AbilityKey.CurrentCastSpells, Lists.newArrayList(thisTurnCast));

View File

@@ -118,7 +118,6 @@ public enum TrackableProperty {
//Card State
Name(TrackableTypes.StringType),
Artist(TrackableTypes.StringType),
Colors(TrackableTypes.ColorSetType),
OriginalColors(TrackableTypes.ColorSetType),
LeftSplitColors(TrackableTypes.ColorSetType),

View File

@@ -40,7 +40,7 @@ import com.google.common.cache.CacheLoader.InvalidCacheLoadException;
import com.google.common.cache.LoadingCache;
import com.mortennobel.imagescaling.ResampleOp;
import forge.card.CardEdition;
import forge.card.CardSplitType;
import forge.game.card.Card;
import forge.game.card.CardView;
import forge.game.player.PlayerView;
@@ -56,8 +56,8 @@ import forge.model.FModel;
import forge.toolbox.FSkin;
import forge.toolbox.FSkin.SkinIcon;
import forge.toolbox.imaging.FCardImageRenderer;
import forge.util.Aggregates;
import forge.util.ImageUtil;
import forge.util.TextUtil;
/**
* This class stores ALL card images in a cache with soft values. this means
@@ -171,41 +171,65 @@ public class ImageCache {
IPaperCard ipc = null;
boolean altState = imageKey.endsWith(ImageKeys.BACKFACE_POSTFIX);
boolean useArtCrop = "Crop".equals(FModel.getPreferences().getPref(ForgePreferences.FPref.UI_CARD_ART_FORMAT));
String fileName = imageKey;
String specColor = "";
if (imageKey.endsWith(ImageKeys.SPECFACE_W)) {
specColor = "white";
} else if (imageKey.endsWith(ImageKeys.SPECFACE_U)) {
specColor = "blue";
} else if (imageKey.endsWith(ImageKeys.SPECFACE_B)) {
specColor = "black";
} else if (imageKey.endsWith(ImageKeys.SPECFACE_R)) {
specColor = "red";
} else if (imageKey.endsWith(ImageKeys.SPECFACE_G)) {
specColor = "green";
}
if (altState)
imageKey = imageKey.substring(0, imageKey.length() - ImageKeys.BACKFACE_POSTFIX.length());
if (!specColor.isEmpty())
imageKey = imageKey.substring(0, imageKey.length() - ImageKeys.SPECFACE_W.length());
if (imageKey.startsWith(ImageKeys.CARD_PREFIX)) {
String[] tempdata = imageKey.substring(2).split("\\|"); //We want to check the edition first.
String name = tempdata[0];
String setCode = tempdata.length > 1 ? tempdata[1] : CardEdition.UNKNOWN_CODE;
String collectorNumber = tempdata.length > 3 ? tempdata[2] : IPaperCard.NO_COLLECTOR_NUMBER;
CardEdition edition = StaticData.instance().getEditions().get(setCode);
if (useArtCrop) {
CardEdition.EditionEntry ee;
if (!collectorNumber.isEmpty() && !collectorNumber.equals(IPaperCard.NO_COLLECTOR_NUMBER)) {
ee = edition.getCardFromCollectorNumber(collectorNumber);
if (ee != null) { // TODO handle Specialize Collector number
ee = edition.getCardFromCollectorNumber(collectorNumber.substring(0, collectorNumber.length() - 1));
ipc = ImageUtil.getPaperCardFromImageKey(imageKey);
if (ipc != null) {
if (altState) {
imageKey = ipc.getCardAltImageKey();
} else if (!specColor.isEmpty()) {
switch (specColor) {
case "white":
imageKey = ipc.getCardWSpecImageKey();
break;
case "blue":
imageKey = ipc.getCardUSpecImageKey();
break;
case "black":
imageKey = ipc.getCardBSpecImageKey();
break;
case "red":
imageKey = ipc.getCardRSpecImageKey();
break;
case "green":
imageKey = ipc.getCardGSpecImageKey();
break;
}
} else {
ee = Aggregates.random(edition.getCardInSet(name));
}
// Skip fetching if artist info is not available for art crop
if (ee != null && ee.artistName().isEmpty()) {
useArtCrop = false;
imageKey = ipc.getCardImageKey();
}
if (StringUtils.isBlank(imageKey))
return Pair.of(_defaultImage, true);
}
ipc = StaticData.instance().fetchCard(name, setCode, collectorNumber);
fileName = ImageUtil.getImageRelativePath(name, setCode, collectorNumber, useArtCrop);
} // TODO add artCrop for Token
}
// Replace .full to .artcrop if art crop is preferred
// Only allow use art if the artist info is available
boolean useArtCrop = "Crop".equals(FModel.getPreferences().getPref(ForgePreferences.FPref.UI_CARD_ART_FORMAT))
&& ipc != null && !ipc.getArtist().isEmpty();
String originalKey = imageKey;
if (useArtCrop) {
if (ipc != null && ipc.getRules().getSplitType() == CardSplitType.Flip) {
// Art crop will always use front face as image key for flip cards
imageKey = ipc.getCardImageKey();
}
imageKey = TextUtil.fastReplace(imageKey, ".full", ".artcrop");
}
// Load from file and add to cache if not found in cache initially.
BufferedImage original = getImage(imageKey);
@@ -215,11 +239,16 @@ public class ImageCache {
}
// if art crop is exist, check also if the full card image is also cached.
if (useArtCrop && original != null) {
BufferedImage cached = _CACHE.getIfPresent(originalKey);
if (cached != null)
return Pair.of(cached, false);
}
boolean noBorder = !useArtCrop && !isPreferenceEnabled(ForgePreferences.FPref.UI_RENDER_BLACK_BORDERS);
boolean fetcherEnabled = isPreferenceEnabled(ForgePreferences.FPref.UI_ENABLE_ONLINE_IMAGE_FETCHER);
boolean isPlaceholder = (original == null) && fetcherEnabled;
String setCode = fileName.split("/")[0].trim().toUpperCase();
String setCode = imageKey.split("/")[0].trim().toUpperCase();
// If the user has indicated that they prefer Forge NOT render a black border, round the image corners
// to account for JPEG images that don't have a transparency.
@@ -272,7 +301,7 @@ public class ImageCache {
CardView card = ipc != null ? Card.getCardForUi(ipc).getView() : cardView;
String legalString = null;
original = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
if (art != null && ipc != null) {
if (art != null) {
Calendar cal = Calendar.getInstance();
cal.setTime(StaticData.instance().getCardEdition(ipc.getEdition()).getDate());
int year = cal.get(Calendar.YEAR);

View File

@@ -18,8 +18,7 @@ final class ImageLoader extends CacheLoader<String, BufferedImage> {
if (FModel.getPreferences().getPrefBoolean(ForgePreferences.FPref.UI_DISABLE_CARD_IMAGES))
return null;
boolean useArtCrop = "Crop".equals(FModel.getPreferences().getPref(ForgePreferences.FPref.UI_CARD_ART_FORMAT));
File file = ImageKeys.getImageFile(key, useArtCrop);
File file = ImageKeys.getImageFile(key);
if (file != null) {
if (!file.exists()) {
return null;

View File

@@ -77,11 +77,41 @@ public final class FImageUtil {
}
boolean altState = key.endsWith(ImageKeys.BACKFACE_POSTFIX);
String specColor = "";
if (key.endsWith(ImageKeys.SPECFACE_W)) {
specColor = "white";
} else if (key.endsWith(ImageKeys.SPECFACE_U)) {
specColor = "blue";
} else if (key.endsWith(ImageKeys.SPECFACE_B)) {
specColor = "black";
} else if (key.endsWith(ImageKeys.SPECFACE_R)) {
specColor = "red";
} else if (key.endsWith(ImageKeys.SPECFACE_G)) {
specColor = "green";
}
String imageKey = key;
if (prefix.equals(ImageKeys.CARD_PREFIX)) {
PaperCard card = ImageUtil.getPaperCardFromImageKey(key);
if (altState) {
imageKey = card.getCardAltImageKey();
} else if (!specColor.isEmpty()) {
switch (specColor) {
case "white":
imageKey = card.getCardWSpecImageKey();
break;
case "blue":
imageKey = card.getCardUSpecImageKey();
break;
case "black":
imageKey = card.getCardBSpecImageKey();
break;
case "red":
imageKey = card.getCardRSpecImageKey();
break;
case "green":
imageKey = card.getCardGSpecImageKey();
break;
}
} else {
imageKey = card.getCardImageKey();
}
@@ -89,6 +119,9 @@ public final class FImageUtil {
if(altState) {
imageKey = imageKey.substring(0, imageKey.length() - ImageKeys.BACKFACE_POSTFIX.length());
imageKey += "full.jpg";
} else if (!specColor.isEmpty()) {
imageKey = imageKey.substring(0, imageKey.length() - ImageKeys.SPECFACE_W.length());
imageKey += "full.jpg";
}
File file = ImageKeys.getImageFile(imageKey);

View File

@@ -34,14 +34,10 @@ public class SwingImageFetcher extends ImageFetcher {
return false;
}
String newdespath = destPath;
if (!destPath.contains(".artcrop")) {
newdespath = urlToDownload.contains(".fullborder.jpg") || urlToDownload.startsWith(ForgeConstants.URL_PIC_SCRYFALL_DOWNLOAD) ?
TextUtil.fastReplace(destPath, ".full.jpg", ".fullborder.jpg") : destPath;
if (!newdespath.contains(".full") && urlToDownload.startsWith(ForgeConstants.URL_PIC_SCRYFALL_DOWNLOAD) && !destPath.startsWith(ForgeConstants.CACHE_TOKEN_PICS_DIR))
newdespath = newdespath.replace(".jpg", ".fullborder.jpg"); //fix planes/phenomenon for round border options
}
String newdespath = urlToDownload.contains(".fullborder.jpg") || urlToDownload.startsWith(ForgeConstants.URL_PIC_SCRYFALL_DOWNLOAD) ?
TextUtil.fastReplace(destPath, ".full.jpg", ".fullborder.jpg") : destPath;
if (!newdespath.contains(".full") && urlToDownload.startsWith(ForgeConstants.URL_PIC_SCRYFALL_DOWNLOAD) && !destPath.startsWith(ForgeConstants.CACHE_TOKEN_PICS_DIR))
newdespath = newdespath.replace(".jpg", ".fullborder.jpg"); //fix planes/phenomenon for round border options
URL url = new URL(urlToDownload);
System.out.println("Attempting to fetch: " + url);
BufferedImage image = ImageIO.read(url);

View File

@@ -274,6 +274,14 @@ public class Forge implements ApplicationListener {
return false;
}
public static boolean hasExternalInput() {
return hasGamepad() || hasKeyboard();
}
public static boolean hasKeyboard() {
return !GuiBase.isAndroid();
}
public static InputProcessor getInputProcessor() {
return inputProcessor;
}
@@ -1235,7 +1243,15 @@ public class Forge implements ApplicationListener {
if (keyInputAdapter != null) {
return keyInputAdapter.keyUp(keyCode);
}
return false;
// if no active key input adapter, give current screen or overlay a chance to handle key
FContainer container = FOverlay.getTopOverlay();
if (container == null) {
container = currentScreen;
if (container == null) {
return false;
}
}
return container.keyUp(keyCode);
}
@Override

View File

@@ -448,7 +448,7 @@ public class EnemySprite extends CharacterSprite implements Steerable<Vector2> {
if (data.copyPlayerDeck && Current.latestDeck() != null) {
List<PaperCard> paperCardList = Current.latestDeck().getMain().toFlatList().stream()
.filter(paperCard -> !paperCard.isVeryBasicLand())
.toList();
.collect(Collectors.toList());
if (paperCardList.size() < 6) {
// Player trying to cheese doppleganger and farm cards. Sorry, the fun police have arrived

View File

@@ -35,7 +35,9 @@ import java.util.*;
* Class that represents the player (not the player sprite)
*/
public class AdventurePlayer implements Serializable, SaveFileContent {
public static final int NUMBER_OF_DECKS = 10;
public static final int MIN_DECK_COUNT = 10;
// this is a purely arbitrary limit, could be higher or lower; just meant as some sort of reasonable limit for the user
public static final int MAX_DECK_COUNT = 20;
// Player profile data.
private String name;
private int heroRace;
@@ -45,7 +47,7 @@ public class AdventurePlayer implements Serializable, SaveFileContent {
// Deck data
private Deck deck;
private final Deck[] decks = new Deck[NUMBER_OF_DECKS];
private final ArrayList<Deck> decks = new ArrayList<Deck>(MIN_DECK_COUNT);
private int selectedDeckIndex = 0;
private final DifficultyData difficultyData = new DifficultyData();
@@ -91,9 +93,13 @@ public class AdventurePlayer implements Serializable, SaveFileContent {
return statistic;
}
public int getDeckCount() { return decks.size(); }
private void clearDecks() {
for (int i = 0; i < NUMBER_OF_DECKS; i++) decks[i] = new Deck(Forge.getLocalizer().getMessage("lblEmptyDeck"));
deck = decks[0];
decks.clear();
for (int i = 0; i < MIN_DECK_COUNT; i++)
decks.add(new Deck(Forge.getLocalizer().getMessage("lblEmptyDeck")));
deck = decks.get(0);
selectedDeckIndex = 0;
}
@@ -140,7 +146,7 @@ public class AdventurePlayer implements Serializable, SaveFileContent {
announceCustom = usingCustomDeck = isUsingCustomDeck;
deck = startingDeck;
decks[0] = deck;
decks.set(0, deck);
cards.addAllFlat(deck.getAllCardsInASinglePool().toFlatList());
@@ -173,9 +179,9 @@ public class AdventurePlayer implements Serializable, SaveFileContent {
}
public void setSelectedDeckSlot(int slot) {
if (slot >= 0 && slot < NUMBER_OF_DECKS) {
if (slot >= 0 && slot < getDeckCount()) {
selectedDeckIndex = slot;
deck = decks[selectedDeckIndex];
deck = decks.get(selectedDeckIndex);
setColorIdentity(DeckProxy.getColorIdentity(deck));
}
}
@@ -214,7 +220,7 @@ public class AdventurePlayer implements Serializable, SaveFileContent {
}
public Deck getDeck(int index) {
return decks[index];
return decks.get(index);
}
public CardPool getCards() {
@@ -448,17 +454,44 @@ public class AdventurePlayer implements Serializable, SaveFileContent {
}
}
for (int i = 0; i < NUMBER_OF_DECKS; i++) {
if (!data.containsKey("deck_name_" + i)) {
if (i == 0) decks[i] = deck;
else decks[i] = new Deck(Forge.getLocalizer().getMessage("lblEmptyDeck"));
continue;
// load decks
// check if this save has dynamic deck count, use set-count load if not
boolean hasDynamicDeckCount = data.containsKey("deckCount");
if (hasDynamicDeckCount) {
int dynamicDeckCount = data.readInt("deckCount");
// in case the save had previously saved more decks than the current version allows (in case of the max being lowered)
dynamicDeckCount = Math.min(MAX_DECK_COUNT, dynamicDeckCount);
for (int i = 0; i < dynamicDeckCount; i++){
// the first x elements are pre-created
if (i < MIN_DECK_COUNT) {
decks.set(i, new Deck(data.readString("deck_name_" + i)));
}
else {
decks.add(new Deck(data.readString("deck_name_" + i)));
}
decks.get(i).getMain().addAll(CardPool.fromCardList(Lists.newArrayList((String[]) data.readObject("deck_" + i))));
if (data.containsKey("sideBoardCards_" + i))
decks.get(i).getOrCreate(DeckSection.Sideboard).addAll(CardPool.fromCardList(Lists.newArrayList((String[]) data.readObject("sideBoardCards_" + i))));
}
// in case we allow removing decks from the deck selection GUI, populate up to the minimum
for (int i = dynamicDeckCount++; i < MIN_DECK_COUNT; i++) {
decks.set(i, new Deck(Forge.getLocalizer().getMessage("lblEmptyDeck")));
}
// legacy load
} else {
for (int i = 0; i < MIN_DECK_COUNT; i++) {
if (!data.containsKey("deck_name_" + i)) {
if (i == 0) decks.set(i, deck);
else decks.set(i, new Deck(Forge.getLocalizer().getMessage("lblEmptyDeck")));
continue;
}
decks.set(i, new Deck(data.readString("deck_name_" + i)));
decks.get(i).getMain().addAll(CardPool.fromCardList(Lists.newArrayList((String[]) data.readObject("deck_" + i))));
if (data.containsKey("sideBoardCards_" + i))
decks.get(i).getOrCreate(DeckSection.Sideboard).addAll(CardPool.fromCardList(Lists.newArrayList((String[]) data.readObject("sideBoardCards_" + i))));
}
decks[i] = new Deck(data.readString("deck_name_" + i));
decks[i].getMain().addAll(CardPool.fromCardList(Lists.newArrayList((String[]) data.readObject("deck_" + i))));
if (data.containsKey("sideBoardCards_" + i))
decks[i].getOrCreate(DeckSection.Sideboard).addAll(CardPool.fromCardList(Lists.newArrayList((String[]) data.readObject("sideBoardCards_" + i))));
}
setSelectedDeckSlot(data.readInt("selectedDeckIndex"));
cards.addAll(CardPool.fromCardList(Lists.newArrayList((String[]) data.readObject("cards"))));
@@ -602,11 +635,14 @@ public class AdventurePlayer implements Serializable, SaveFileContent {
data.storeObject("deckCards", deck.getMain().toCardList("\n").split("\n"));
if (deck.get(DeckSection.Sideboard) != null)
data.storeObject("sideBoardCards", deck.get(DeckSection.Sideboard).toCardList("\n").split("\n"));
for (int i = 0; i < NUMBER_OF_DECKS; i++) {
data.store("deck_name_" + i, decks[i].getName());
data.storeObject("deck_" + i, decks[i].getMain().toCardList("\n").split("\n"));
if (decks[i].get(DeckSection.Sideboard) != null)
data.storeObject("sideBoardCards_" + i, decks[i].get(DeckSection.Sideboard).toCardList("\n").split("\n"));
// save decks dynamically
data.store("deckCount", getDeckCount());
for (int i = 0; i < getDeckCount(); i++) {
data.store("deck_name_" + i, decks.get(i).getName());
data.storeObject("deck_" + i, decks.get(i).getMain().toCardList("\n").split("\n"));
if (decks.get(i).get(DeckSection.Sideboard) != null)
data.storeObject("sideBoardCards_" + i, decks.get(i).get(DeckSection.Sideboard).toCardList("\n").split("\n"));
}
data.store("selectedDeckIndex", selectedDeckIndex);
data.storeObject("cards", cards.toCardList("\n").split("\n"));
@@ -933,7 +969,7 @@ public class AdventurePlayer implements Serializable, SaveFileContent {
public void renameDeck(String text) {
deck = (Deck) deck.copyTo(text);
decks[selectedDeckIndex] = deck;
decks.set(selectedDeckIndex, deck);
}
public int cardSellPrice(PaperCard card) {
@@ -1182,10 +1218,23 @@ public class AdventurePlayer implements Serializable, SaveFileContent {
}
/**
* Deletes a deck by replacing the current selected deck with a new deck
* Clears a deck by replacing the current selected deck with a new deck
*/
public void deleteDeck() {
deck = decks[selectedDeckIndex] = new Deck(Forge.getLocalizer().getMessage("lblEmptyDeck"));
public void clearDeck() {
deck = decks.set(selectedDeckIndex, new Deck(Forge.getLocalizer().getMessage("lblEmptyDeck")));
}
/**
* Actually removes the deck from the list of decks.
*/
public void deleteDeck(){
int oldIndex = selectedDeckIndex;
this.setSelectedDeckSlot(0);
decks.remove(oldIndex);
}
public void addDeck(){
decks.add(new Deck(Forge.getLocalizer().getMessage("lblEmptyDeck")));
}
/**
@@ -1194,9 +1243,9 @@ public class AdventurePlayer implements Serializable, SaveFileContent {
* @return int - index of new copy slot, or -1 if no slot was available
*/
public int copyDeck() {
for (int i = 0; i < decks.length; i++) {
for (int i = 0; i < MAX_DECK_COUNT; i++) {
if (isEmptyDeck(i)) {
decks[i] = (Deck) deck.copyTo(deck.getName() + " (" + Forge.getLocalizer().getMessage("lblCopy") + ")");
decks.set(i, (Deck) deck.copyTo(deck.getName() + " (" + Forge.getLocalizer().getMessage("lblCopy") + ")"));
return i;
}
}
@@ -1205,7 +1254,7 @@ public class AdventurePlayer implements Serializable, SaveFileContent {
}
public boolean isEmptyDeck(int deckIndex) {
return decks[deckIndex].isEmpty() && decks[deckIndex].getName().equals(Forge.getLocalizer().getMessage("lblEmptyDeck"));
return decks.get(deckIndex).isEmpty() && decks.get(deckIndex).getName().equals(Forge.getLocalizer().getMessage("lblEmptyDeck"));
}
public void removeEvent(AdventureEventData completedEvent) {
@@ -1227,8 +1276,8 @@ public class AdventurePlayer implements Serializable, SaveFileContent {
// 2. Count max cards across all decks in excess of unsellable
Map<PaperCard, Integer> maxCardCounts = new HashMap<>();
for (int i = 0; i < NUMBER_OF_DECKS; i++) {
for (final Map.Entry<PaperCard, Integer> cp : decks[i].getAllCardsInASinglePool()) {
for (int i = 0; i < getDeckCount(); i++) {
for (final Map.Entry<PaperCard, Integer> cp : decks.get(i).getAllCardsInASinglePool()) {
int count = cp.getValue();
if (count > maxCardCounts.getOrDefault(cp.getKey(), 0)) {
maxCardCounts.put(cp.getKey(), cp.getValue());

View File

@@ -20,6 +20,7 @@ public class PointOfInterestChanges implements SaveFileContent {
//private final java.util.Map<Integer, Float> shopModifiers = new HashMap<>();
private final java.util.Map<Integer, Integer> reputation = new HashMap<>();
private Boolean isBookmarked;
private Boolean isVisited;
public static class Map extends HashMap<String,PointOfInterestChanges> implements SaveFileContent {
@Override
@@ -67,6 +68,7 @@ public class PointOfInterestChanges implements SaveFileContent {
reputation.putAll((java.util.Map<Integer, Integer>) data.readObject("reputation"));
}
isBookmarked = (Boolean) data.readObject("isBookmarked");
isVisited = (Boolean) data.readObject("isVisited");
}
@Override
@@ -78,6 +80,7 @@ public class PointOfInterestChanges implements SaveFileContent {
data.storeObject("shopSeeds", shopSeeds);
data.storeObject("reputation", reputation);
data.storeObject("isBookmarked", isBookmarked);
data.storeObject("isVisited", isVisited);
return data;
}
@@ -177,4 +180,12 @@ public class PointOfInterestChanges implements SaveFileContent {
// reset map when assigning as a quest target that needs enemies
deletedObjects.clear();
}
public boolean isVisited() {
if (isVisited ==null)
return false;
return isVisited;
}
public void visit() {
isVisited = true;
}
}

View File

@@ -389,7 +389,7 @@ public class AdventureDeckEditor extends TabPageScreen<AdventureDeckEditor> {
@Override
public void onActivate() {
decksUsingMyCards = new ItemPool<>(InventoryItem.class);
for (int i = 0; i < AdventurePlayer.NUMBER_OF_DECKS; i++) {
for (int i = 0; i < AdventurePlayer.current().getDeckCount(); i++) {
final Deck deck = AdventurePlayer.current().getDeck(i);
CardPool main = deck.getMain();
for (final Map.Entry<PaperCard, Integer> e : main) {
@@ -577,6 +577,9 @@ public class AdventureDeckEditor extends TabPageScreen<AdventureDeckEditor> {
@Override
protected void buildMenu() {
addItem(new FMenuItem(Forge.getLocalizer().getMessage("btnCopyToClipboard"), Forge.hdbuttons ? FSkinImage.HDEXPORT : FSkinImage.BLANK, e1 -> FDeckViewer.copyDeckToClipboard(getDeck())));
addItem(new FMenuItem(Forge.getLocalizer().getMessage("btnCopyCollectionToClipboard"), Forge.hdbuttons ? FSkinImage.HDEXPORT : FSkinImage.BLANK, e1 -> {
FDeckViewer.copyCollectionToClipboard(AdventurePlayer.current().getCards());
}));
if (allowsAddBasic()) {
FMenuItem addBasic = new FMenuItem(Forge.getLocalizer().getMessage("lblAddBasicLands"), FSkinImage.LANDLOGO, e1 -> launchBasicLandDialog());
addItem(addBasic);
@@ -649,6 +652,8 @@ public class AdventureDeckEditor extends TabPageScreen<AdventureDeckEditor> {
Map<String, CardEdition> editionsByName = new HashMap<>();
for (CardEdition e : FModel.getMagicDb().getEditions()) {
editionsByName.put(e.getName().toLowerCase(), e);
editionsByName.put(e.getName().replace(":", "").toLowerCase(), e);
editionsByName.put(e.getName().replace("'", "").toLowerCase(), e);
}
String sketchbookPrefix = "landscape sketchbook - ";
@@ -1611,5 +1616,13 @@ public class AdventureDeckEditor extends TabPageScreen<AdventureDeckEditor> {
}
return true;
}
@Override
public boolean keyUp(int keyCode) {
if (keyCode == Input.Keys.ESCAPE) {
return this.tabHeader.btnBack.trigger();
}
return super.keyUp(keyCode);
}
}

View File

@@ -16,11 +16,12 @@ import forge.adventure.util.Current;
public class DeckSelectScene extends UIScene {
private final IntMap<TextraButton> buttons = new IntMap<>();
private final IntMap<Label> labels = new IntMap<>();
Color defColor;
TextField textInput;
Table layout;
TextraLabel header;
TextraButton back, edit, rename;
TextraButton back, edit, rename, add;
int currentSlot = 0;
ScrollPane scrollPane;
Dialog renameDialog;
@@ -45,13 +46,13 @@ public class DeckSelectScene extends UIScene {
root.add(header).colspan(2);
root.row();
root.add(scrollPane).expand().width(window.getWidth() - 20);
for (int i = 0; i < AdventurePlayer.NUMBER_OF_DECKS; i++)
addDeckSlot(Forge.getLocalizer().getMessage("lblDeck") + ": " + (i + 1), i);
this.layoutDeckButtons();
textInput = Controls.newTextField("");
back = ui.findActor("return");
edit = ui.findActor("edit");
rename = ui.findActor("rename");
add = ui.findActor("add");
ui.onButtonPress("return", DeckSelectScene.this::back);
ui.onButtonPress("edit", DeckSelectScene.this::edit);
ui.onButtonPress("rename", () -> {
@@ -59,11 +60,44 @@ public class DeckSelectScene extends UIScene {
showRenameDialog();
});
ui.onButtonPress("copy", DeckSelectScene.this::copy);
ui.onButtonPress("delete", DeckSelectScene.this::maybeDelete);
ui.onButtonPress("delete", DeckSelectScene.this::promptDelete);
ui.onButtonPress("add", DeckSelectScene.this::addDeck);
defColor = ui.findActor("return").getColor();
window.add(root);
}
private void refreshDeckButtons(){
clearDeckButtons();
layoutDeckButtons();
}
private void clearDeckButtons(){
int count = AdventurePlayer.current().getDeckCount();
for (int i = count; i >= 0; i--){
clearDeckButton(i);
}
layout.clearChildren();
buttons.clear();
labels.clear();
}
private void layoutDeckButtons() {
for (int i = 0; i < AdventurePlayer.current().getDeckCount(); i++)
addDeckButton(Forge.getLocalizer().getMessage("lblDeck") + ": " + (i + 1), i);
}
private void addDeck(){
if (Current.player().getDeckCount() >= AdventurePlayer.MAX_DECK_COUNT){
showDialog(createGenericDialog(Forge.getLocalizer().getMessage("lblAddDeck"), Forge.getLocalizer().getMessage("lblMaxDeckCountReached"),
Forge.getLocalizer().getMessage("lblOK"), null, this::removeDialog, null));
return;
}
Current.player().addDeck();
refreshDeckButtons();
select(Current.player().getSelectedDeckIndex());
}
private void copy() {
if (Current.player().isEmptyDeck(currentSlot)) return;
int index = Current.player().copyDeck();
@@ -79,17 +113,32 @@ public class DeckSelectScene extends UIScene {
}
}
private void maybeDelete() {
if (Current.player().isEmptyDeck(currentSlot)) return;
private void promptDelete() {
Dialog deleteDialog = createGenericDialog(Forge.getLocalizer().getMessage("lblDelete"), Forge.getLocalizer().getMessage("lblAreYouSureProceedDelete"),
Forge.getLocalizer().getMessage("lblOK"),
Forge.getLocalizer().getMessage("lblAbort"), this::delete, this::removeDialog);
Forge.getLocalizer().getMessage("lblAbort"), this::clearOrDelete, this::removeDialog);
showDialog(deleteDialog);
}
private void delete() {
Current.player().deleteDeck();
/**
* Clears or deletes the currently selected deck.
*/
private void clearOrDelete(){
if (currentSlot >= AdventurePlayer.MIN_DECK_COUNT){
Current.player().deleteDeck();
}
else {
Current.player().clearDeck();
}
refreshDeckButtons();
select(0);
removeDialog();
}
private void clear() {
Current.player().clearDeck();
updateDeckButton(currentSlot);
removeDialog();
}
@@ -117,7 +166,18 @@ public class DeckSelectScene extends UIScene {
showDialog(renameDialog);
}
private TextraButton addDeckSlot(String name, int i) {
/**
* Not sure if this is strictly necessary in Java but wouldn't want to leak before clearing the layout table.
*/
private void clearDeckButton(int i){
if (buttons.containsKey(i)) {
TextraButton button = buttons.remove(i);
button.clearListeners();
button.clearActions();
}
}
private TextraButton addDeckButton(String name, int i) {
TextraButton button = Controls.newTextButton("-");
button.addListener(new ClickListener() {
@Override
@@ -131,9 +191,12 @@ public class DeckSelectScene extends UIScene {
}
});
button.setText(Current.player().getDeck(i).getName());
Label label = Controls.newLabel(name);
layout.add(Controls.newLabel(name)).pad(2);
layout.add(button).fill(true, false).expand(true, false).align(Align.left).expandX().pad(2);
buttons.put(i, button);
labels.put(i, label);
addToSelectable(new Selectable(button));
layout.row();
return button;
@@ -158,13 +221,7 @@ public class DeckSelectScene extends UIScene {
@Override
public void enter() {
for (int i = 0; i < AdventurePlayer.NUMBER_OF_DECKS; i++) {
if (buttons.containsKey(i)) {
buttons.get(i).setText(Current.player().getDeck(i).getName());
buttons.get(i).getTextraLabel().layout();
buttons.get(i).layout();
}
}
refreshDeckButtons();
GameHUD.getInstance().switchAudio();
select(Current.player().getSelectedDeckIndex());
performTouch(scrollPane); //can use mouse wheel if available to scroll after selection
@@ -175,9 +232,7 @@ public class DeckSelectScene extends UIScene {
private void rename() {
String text = textInput.getText();
Current.player().renameDeck(text);
buttons.get(currentSlot).setText(Current.player().getDeck(currentSlot).getName());
buttons.get(currentSlot).getTextraLabel().layout();
buttons.get(currentSlot).layout();
updateDeckButton(currentSlot);
}
private void edit() {

View File

@@ -511,13 +511,11 @@ public class EventScene extends MenuScene implements IAfterMatch {
if (match.p1 instanceof AdventureEventData.AdventureEventHuman) {
humanMatch = match;
continue;
} else if (match.p2 instanceof AdventureEventData.AdventureEventHuman) {
AdventureEventData.AdventureEventParticipant placeholder = match.p1;
match.p1 = match.p2;
match.p2 = placeholder;
humanMatch = match;
continue;
} else {
//Todo: Actually run match simulation here
if (MyRandom.percentTrue(50)) {
@@ -530,7 +528,6 @@ public class EventScene extends MenuScene implements IAfterMatch {
match.winner = match.p2;
}
}
}
if (humanMatch != null && humanMatch.round != currentEvent.currentRound)
@@ -539,14 +536,16 @@ public class EventScene extends MenuScene implements IAfterMatch {
DuelScene duelScene = DuelScene.instance();
EnemySprite enemy = humanMatch.p2.getSprite();
currentEvent.nextOpponent = humanMatch.p2;
advance.setDisabled(true);
FThreads.invokeInEdtNowOrLater(() -> Forge.setTransitionScreen(new TransitionScreen(() -> {
duelScene.initDuels(WorldStage.getInstance().getPlayerSprite(), enemy, false, currentEvent);
advance.setDisabled(false);
Forge.switchScene(duelScene);
}, Forge.takeScreenshot(), true, false, false, false, "", Current.player().avatar(), enemy.getAtlasPath(), Current.player().getName(), enemy.getName(), humanMatch.p1.getRecord(), humanMatch.p2.getRecord())));
} else {
finishRound();
advance.setDisabled(false);
}
advance.setDisabled(false);
}
AdventureEventData.AdventureEventMatch humanMatch = null;

View File

@@ -4,15 +4,20 @@ import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.Group;
import com.badlogic.gdx.scenes.scene2d.InputEvent;
import com.badlogic.gdx.scenes.scene2d.InputListener;
import com.badlogic.gdx.scenes.scene2d.ui.Image;
import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane;
import com.badlogic.gdx.scenes.scene2d.utils.TextureRegionDrawable;
import com.badlogic.gdx.utils.SnapshotArray;
import com.github.tommyettinger.textra.TextraButton;
import com.github.tommyettinger.textra.TypingLabel;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import forge.Forge;
import forge.adventure.data.AdventureEventData;
import forge.adventure.data.AdventureQuestData;
import forge.adventure.player.AdventurePlayer;
import forge.adventure.pointofintrest.PointOfInterest;
import forge.adventure.stage.GameHUD;
import forge.adventure.stage.WorldStage;
@@ -37,6 +42,9 @@ public class MapViewScene extends UIScene {
private int index = -1;
private float avatarX = 0, avatarY = 0;
private Set<Vector2> positions;
private final List<TypingLabel> details;
private final float maxZoom = 1.2f;
private final float minZoom = 0.25f;
private Set<PointOfInterest> bookmark;
public static MapViewScene instance() {
@@ -49,7 +57,25 @@ public class MapViewScene extends UIScene {
super(Forge.isLandscapeMode() ? "ui/map.json" : "ui/map_portrait.json");
ui.onButtonPress("done", this::done);
ui.onButtonPress("quest", this::scroll);
scroll = ui.findActor("map");
//TODO:Add Translations for buttons
ui.onButtonPress("details", this::details);
ui.onButtonPress("events", this::events);
ui.onButtonPress("reputation", this::reputation);
ui.onButtonPress("names", this::names);
ui.onButtonPress("zoomIn", this::zoomIn);
ui.onButtonPress("zoomOut", this::zoomOut);
scroll = new ScrollPane(null,Controls.getSkin()) {
@Override
public void addScrollListener() {
return;
}
};
scroll.setName("map");
scroll.setActor(Controls.newTextraLabel(""));
scroll.setWidth(ui.findActor("map").getWidth());
scroll.setHeight(ui.findActor("map").getHeight());
ui.addActor(scroll);
scroll.setZIndex(1);
labels = Lists.newArrayList();
positions = Sets.newHashSet();
bookmark = Sets.newHashSet();
@@ -60,16 +86,53 @@ public class MapViewScene extends UIScene {
img.setPosition(0, 0);
table.addActor(img);
table.addActor(miniMapPlayer);
miniMapPlayer.setZIndex(2);
details = Lists.newArrayList();
ui.addListener(new InputListener() {
public boolean scrolled(InputEvent event, float x, float y, float scrollAmountX, float scrollAmountY) {
event.cancel();
scroll.setScrollbarsVisible(true);
if (scrollAmountY > 0) {
zoomOut();
return true;
} else if (scrollAmountY < 0) {
zoomIn();
return true;
}
return false;
}
});
stage.setScrollFocus(ui);
}
public void test() {
img.setPosition((scroll.getScrollPercentX()*2334 +233)*0.1f + 0.9f*img.getX(),(2544-scroll.getScrollPercentY()*2544 +128)*0.1f + 0.9f*img.getY());
img.setScale(img.getScaleX()*0.9f);
miniMapPlayer.setPosition((scroll.getScrollPercentX()*2334 +233)*0.1f + 0.9f*miniMapPlayer.getX(),(2544-scroll.getScrollPercentY()*2544 +128)*0.1f + 0.9f*miniMapPlayer.getY());
miniMapPlayer.setScale(miniMapPlayer.getScaleX()*0.9f);
for(Actor actor : table.getChildren()) {
if (actor instanceof TypingLabel) {
actor.setPosition((scroll.getScrollPercentX() * 2334 + 233) * 0.1f + 0.9f * actor.getX(), (2544 - scroll.getScrollPercentY() * 2544 + 128) * 0.1f + 0.9f * actor.getY());
}
}
}
public boolean done() {
GameHUD.getInstance().getTouchpad().setVisible(false);
for (Actor a : table.getChildren()) {
if (a instanceof TypingLabel)
a.remove();
SnapshotArray<Actor> allActors = table.getChildren();
for (int i = 0; i < allActors.size; i++) {
if (allActors.get(i) instanceof TypingLabel) {
allActors.get(i).remove();
i--;
}
}
labels.clear();
positions.clear();
details.clear();
miniMapPlayer.setScale(1);
img.setScale(1);
img.setPosition(0,0);
index = -1;
Forge.switchToLast();
return true;
@@ -101,6 +164,134 @@ public class MapViewScene extends UIScene {
return true;
}
public void details() {
TextraButton detailsButton = ui.findActor("details");
if (detailsButton != null) {
detailsButton.setVisible(false);
detailsButton.setDisabled(true);
}
TextraButton eventButton = ui.findActor("events");
if (eventButton != null) {
eventButton.setVisible(true);
eventButton.setDisabled(false);
}
List<PointOfInterest> allPois = Current.world().getAllPointOfInterest();
for (PointOfInterest poi : allPois) {
for (AdventureEventData data : AdventurePlayer.current().getEvents()) {
if (data.sourceID.equals(poi.getID())) {
TypingLabel label = Controls.newTypingLabel("[%?BLACKEN] " + data.getCardBlock());
table.addActor(label);
details.add(label);
label.setPosition(img.getScaleX()*(getMapX(poi.getPosition().x) - label.getWidth() / 2) + img.getX(), img.getScaleY()*(getMapY(poi.getPosition().y) - label.getHeight() / 2) + img.getY());
label.skipToTheEnd();
}
}
}
}
public void events() {
TextraButton eventsButton = ui.findActor("events");
if (eventsButton != null) {
eventsButton.setVisible(false);
eventsButton.setDisabled(true);
}
TextraButton repButton = ui.findActor("reputation");
if (repButton != null) {
repButton.setVisible(true);
repButton.setDisabled(false);
}
for (TypingLabel detail : details) {
table.removeActor(detail);
}
List<PointOfInterest> allPois = Current.world().getAllPointOfInterest();
details.clear();
for (PointOfInterest poi : allPois) {
int rep = WorldSave.getCurrentSave().getPointOfInterestChanges(poi.getID()).getMapReputation();
if (rep != 0) {
TypingLabel label = Controls.newTypingLabel("[%?BLACKEN] " + rep);
table.addActor(label);
details.add(label);
label.setPosition(img.getScaleX()*(getMapX(poi.getPosition().x) - label.getWidth() / 2) + img.getX(), img.getScaleY()*(getMapY(poi.getPosition().y) - label.getHeight() / 2) + img.getY());
label.skipToTheEnd();
}
}
}
public void reputation() {
TextraButton repButton = ui.findActor("reputation");
if (repButton != null) {
repButton.setVisible(false);
repButton.setDisabled(true);
}
TextraButton namesButton = ui.findActor("names");
if (namesButton != null) {
namesButton.setVisible(true);
namesButton.setDisabled(false);
}
for (TypingLabel detail : details) {
table.removeActor(detail);
}
details.clear();
List<PointOfInterest> allPois = Current.world().getAllPointOfInterest();
for (PointOfInterest poi : allPois) {
if (WorldSave.getCurrentSave().getPointOfInterestChanges(poi.getID()).isVisited()) {
if ("cave".equalsIgnoreCase(poi.getData().type) || "dungeon".equalsIgnoreCase(poi.getData().type) || "castle".equalsIgnoreCase(poi.getData().type)) {
TypingLabel label = Controls.newTypingLabel("[%?BLACKEN] " + poi.getDisplayName());
table.addActor(label);
details.add(label);
label.setPosition(img.getScaleX()*(getMapX(poi.getPosition().x) - label.getWidth() / 2) + img.getX(), img.getScaleY()*(getMapY(poi.getPosition().y) - label.getHeight() / 2) + img.getY());
label.skipToTheEnd();
}
}
}
}
public void names() {
TextraButton namesButton = ui.findActor("names");
if (namesButton != null) {
namesButton.setVisible(false);
namesButton.setDisabled(true);
}
TextraButton detailsButton = ui.findActor("details");
if (detailsButton != null) {
detailsButton.setVisible(true);
detailsButton.setDisabled(false);
}
for (TypingLabel detail : details) {
table.removeActor(detail);
}
details.clear();
}
public void zoomOut() {
if (img.getScaleX()*0.9f > minZoom) {
img.setPosition((scroll.getScrollX() + scroll.getWidth()/2) * 0.1f + 0.9f * img.getX(), (scroll.getMaxY() - scroll.getScrollY() + scroll.getHeight()/2) * 0.1f + 0.9f * img.getY());
img.setScale(img.getScaleX() * 0.9f);
miniMapPlayer.setPosition((scroll.getScrollX() + scroll.getWidth()/2) * 0.1f + 0.9f * miniMapPlayer.getX(), (scroll.getMaxY() - scroll.getScrollY() + scroll.getHeight()/2) * 0.1f + 0.9f * miniMapPlayer.getY());
miniMapPlayer.setScale(miniMapPlayer.getScaleX() * 0.9f);
for (Actor actor : table.getChildren()) {
if (actor instanceof TypingLabel) {
actor.setPosition((scroll.getScrollX() + scroll.getWidth()/2) * 0.1f + 0.9f * actor.getX(), (scroll.getMaxY() - scroll.getScrollY() + scroll.getHeight()/2) * 0.1f + 0.9f * actor.getY());
}
}
}
}
public void zoomIn() {
if (img.getScaleX()*1.1f < maxZoom) {
img.setPosition(-(scroll.getScrollX() + scroll.getWidth()/2) * 0.1f + 1.1f * img.getX(), -(scroll.getMaxY() - scroll.getScrollY() + scroll.getHeight()/2) * 0.1f + 1.1f * img.getY());
img.setScale(img.getScaleX() * 1.1f);
miniMapPlayer.setPosition(-(scroll.getScrollX() + scroll.getWidth()/2) * 0.1f + 1.1f * miniMapPlayer.getX(), -(scroll.getMaxY() - scroll.getScrollY() + scroll.getHeight()/2) * 0.1f + 1.1f * miniMapPlayer.getY());
miniMapPlayer.setScale(miniMapPlayer.getScaleX() * 1.1f);
for (Actor actor : table.getChildren()) {
if (actor instanceof TypingLabel) {
actor.setPosition(-(scroll.getScrollX() + scroll.getWidth()/2) * 0.1f + 1.1f * actor.getX(), -(scroll.getMaxY() - scroll.getScrollY() + scroll.getHeight()/2) * 0.1f + 1.1f * actor.getY());
}
}
}
}
@Override
public void enter() {
if (miniMapTexture != null)
@@ -135,6 +326,37 @@ public class MapViewScene extends UIScene {
label.setPosition(getMapX(poi.getPosition().x) - label.getWidth() / 2, getMapY(poi.getPosition().y) - label.getHeight() / 2);
label.skipToTheEnd();
}
TextraButton detailsButton = ui.findActor("details");
if (detailsButton != null) {
detailsButton.setVisible(true);
detailsButton.setDisabled(false);
}
TextraButton eventButton = ui.findActor("events");
if (eventButton != null) {
eventButton.setVisible(false);
eventButton.setDisabled(true);
}
TextraButton repButton = ui.findActor("reputation");
if (repButton != null) {
repButton.setVisible(false);
repButton.setDisabled(true);
}
TextraButton namesButton = ui.findActor("names");
if (namesButton != null) {
namesButton.setVisible(false);
namesButton.setDisabled(true);
}
TextraButton zoomInButton = ui.findActor("zoomIn");
if (zoomInButton != null) {
zoomInButton.setVisible(true);
zoomInButton.setDisabled(false);
}
TextraButton zoomOutButton = ui.findActor("zoomOut");
if (zoomOutButton != null) {
zoomOutButton.setVisible(true);
zoomOutButton.setDisabled(false);
}
TextraButton questButton = ui.findActor("quest");
if (questButton != null) {
questButton.setDisabled(labels.isEmpty());

View File

@@ -308,7 +308,7 @@ public class MenuScene extends UIScene {
}
dialog.show(stage, Actions.show());
dialog.setPosition((stage.getWidth() - dialog.getWidth()) / 2, (stage.getHeight() - dialog.getHeight()) / 2);
if (Forge.hasGamepad() && !dialogButtonMap.isEmpty())
if (Forge.hasExternalInput() && !dialogButtonMap.isEmpty())
stage.setKeyboardFocus(dialogButtonMap.first());
}

View File

@@ -159,18 +159,15 @@ public class RewardScene extends UIScene {
boolean done(boolean skipShowLoot) {
GameHUD.getInstance().getTouchpad().setVisible(false);
if (!skipShowLoot) {
doneButton.setText("[+OK]");
showLootOrDone();
return true;
}
if (type != null) {
switch (type) {
case Shop:
doneButton.setText("[+OK]");
break;
case QuestReward:
case Loot:
doneButton.setText("[+OK]");
break;
}
}
@@ -217,7 +214,6 @@ public class RewardScene extends UIScene {
@Override
public void enter() {
autoSell = false;
doneButton.setText("[+OK]");
updateDetailButton();
super.enter();
}
@@ -252,7 +248,7 @@ public class RewardScene extends UIScene {
reward.flip();
}
}, delay);
delay += 0.15f;
delay += 0.12f;
}
}
} else {
@@ -342,6 +338,8 @@ public class RewardScene extends UIScene {
this.shopActor = shopActor;
this.changes = shopActor.getMapStage().getChanges();
addToSelectable(restockButton);
} else {
doneButton.setText("[+OK]");
}
for (Actor actor : new Array.ArrayIterator<>(generated)) {
actor.remove();
@@ -406,7 +404,6 @@ public class RewardScene extends UIScene {
switch (type) {
case Shop:
doneButton.setText("[+OK]");
String shopName = shopActor.getDescription();
if ((shopName != null && !shopName.isEmpty())) {
headerLabel.setVisible(true);
@@ -425,19 +422,16 @@ public class RewardScene extends UIScene {
headerLabel.setVisible(false);
headerLabel.setText("");
restockButton.setVisible(false);
doneButton.setText("[+OK]");
break;
case Loot:
headerLabel.skipToTheEnd();
headerLabel.setPosition(restockButton.getX(), restockButton.getY());
headerLabel.setVisible(true);
headerLabel.setText("[%?SHINY][;]\u2610 " + Forge.getLocalizer().getMessage("lblAll"));
headerLabel.skipToTheEnd();
restockButton.setVisible(false);
doneButton.setText("[+OK]");
break;
case RewardChoice:
restockButton.setVisible(false);
doneButton.setText("[+OK]");
headerLabel.setVisible(remainingSelections > 0);
headerLabel.setText(Forge.getLocalizer().getMessage("lblSelectRewards", remainingSelections));
doneButton.setDisabled(remainingSelections > 0);

View File

@@ -1,6 +1,7 @@
package forge.adventure.scene;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.controllers.Controller;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
@@ -242,6 +243,7 @@ public class UIScene extends Scene {
public Dialog createGenericDialog(String title, String label, String stringYes, String stringNo, Runnable runnableYes, Runnable runnableNo) {
return createGenericDialog(title, label, stringYes, stringNo, runnableYes, runnableNo, false, "");
}
public Dialog createGenericDialog(String title, String label, String stringYes, String stringNo, Runnable runnableYes, Runnable runnableNo, boolean cancelButton, String stringCancel) {
Dialog dialog = new Dialog(title == null ? "" : title, Controls.getSkin());
textboxOpen = true;
@@ -340,16 +342,15 @@ public class UIScene extends Scene {
}
public boolean keyPressed(int keycode) {
Selectable selection = getSelected();
ui.pressDown(keycode);
Selectable selection = getSelected();
if (KeyBinding.Use.isPressed(keycode)) {
if (selection != null) {
selection.onPressDown(this);
return true;
}
}
ui.pressDown(keycode);
if (stage.getKeyboardFocus() instanceof SelectBox) {
SelectBox box = (SelectBox) stage.getKeyboardFocus();
if (box.getScrollPane().hasParent()) {
@@ -379,17 +380,22 @@ public class UIScene extends Scene {
scroll.setScrollY(scroll.getScrollY() + 20);
}
}
if(!textboxOpen){
if (KeyBinding.Down.isPressed(keycode))
if (!textboxOpen) {
//Allow letter S for TextField since this is binded on down keys
if (KeyBinding.Down.isPressed(keycode, !(stage.getKeyboardFocus() instanceof TextField))
|| KeyBinding.Down.isPressed(keycode, Input.Keys.S != keycode))
selectNextDown();
if (KeyBinding.Up.isPressed(keycode))
//Allow letter W for TextField since this is binded on up keys
if (KeyBinding.Up.isPressed(keycode, !(stage.getKeyboardFocus() instanceof TextField))
|| KeyBinding.Up.isPressed(keycode, Input.Keys.W != keycode))
selectNextUp();
if (!(stage.getKeyboardFocus() instanceof Selector) && !(stage.getKeyboardFocus() instanceof TextField) && !(stage.getKeyboardFocus() instanceof Slider)) {
if (KeyBinding.Right.isPressed(keycode))
selectNextRight();
if (KeyBinding.Left.isPressed(keycode))
selectNextLeft();
}
// Allow Right & Left keybinds if not Selector, Slider or Textfield
if (KeyBinding.Right.isPressed(keycode, !(stage.getKeyboardFocus() instanceof Selector)
&& !(stage.getKeyboardFocus() instanceof TextField) && !(stage.getKeyboardFocus() instanceof Slider)))
selectNextRight();
if (KeyBinding.Left.isPressed(keycode, !(stage.getKeyboardFocus() instanceof Selector)
&& !(stage.getKeyboardFocus() instanceof TextField) && !(stage.getKeyboardFocus() instanceof Slider)))
selectNextLeft();
}
if (!dialogShowing()) {
Button pressedButton = ui.buttonPressed(keycode);

View File

@@ -75,6 +75,10 @@ public class Console extends Window {
}
public void command(String text) {
if (text.equalsIgnoreCase("exit")) {
toggle();
return;
}
Cell<Label> newLine=content.add(text);
newLine.getActor().setColor(1,1,1,1);
newLine.growX().align(Align.left|Align.bottom).row();

View File

@@ -786,6 +786,7 @@ public class GameHUD extends Stage {
console.toggle();
if (console.isVisible()) {
clearAbility();
console.setZIndex(ui.getChildren().size);
} else {
updateAbility();
}
@@ -794,6 +795,12 @@ public class GameHUD extends Stage {
@Override
public boolean keyUp(int keycode) {
ui.pressUp(keycode);
Button pressedButton = ui.buttonPressed(keycode);
if (pressedButton != null) {
pressedButton.fire(eventTouchUp);
}
return super.keyUp(keycode);
}
@@ -807,16 +814,17 @@ public class GameHUD extends Stage {
toggleConsole();
return true;
}
if (keycode == Input.Keys.BACK) {
if (KeyBinding.Back.isPressed(keycode)) {
if (console.isVisible()) {
toggleConsole();
return true;
}
}
if (console.isVisible())
return true;
Button pressedButton = ui.buttonPressed(keycode);
if (pressedButton != null) {
performTouch(pressedButton);
pressedButton.fire(eventTouchDown);
}
return super.keyDown(keycode);
}
@@ -877,7 +885,7 @@ public class GameHUD extends Stage {
dialogOnlyInput = true;
gameStage.hudIsShowingDialog(true);
MapStage.getInstance().hudIsShowingDialog(true);
if (Forge.hasGamepad() && !dialogButtonMap.isEmpty())
if (Forge.hasExternalInput() && !dialogButtonMap.isEmpty())
this.setKeyboardFocus(dialogButtonMap.first());
}

View File

@@ -110,7 +110,7 @@ public abstract class GameStage extends Stage {
dialog.setPosition((dialogStage.getWidth() - dialog.getWidth()) / 2, (dialogStage.getHeight() - dialog.getHeight()) / 2);
dialogOnlyInput = true;
if (Forge.hasGamepad() && !dialogButtonMap.isEmpty())
if (Forge.hasExternalInput() && !dialogButtonMap.isEmpty())
dialogStage.setKeyboardFocus(dialogButtonMap.first());
}
@@ -577,9 +577,6 @@ public abstract class GameStage extends Stage {
if (!player.isMoving())
stop();
}
if (KeyBinding.Menu.isPressed(keycode)) {
openMenu();
}
return false;
}

View File

@@ -1132,8 +1132,10 @@ public class MapStage extends GameStage {
dialog.show(dialogStage, Actions.show());
dialog.setPosition((dialogStage.getWidth() - dialog.getWidth()) / 2, (dialogStage.getHeight() - dialog.getHeight()) / 2);
dialogOnlyInput = true;
if (Forge.hasGamepad() && !dialogButtonMap.isEmpty())
if (Forge.hasExternalInput() && !dialogButtonMap.isEmpty()) {
dialogStage.setKeyboardFocus(dialogButtonMap.first());
}
}

View File

@@ -216,6 +216,7 @@ public class WorldStage extends GameStage implements SaveFileContent {
WorldSave.getCurrentSave().autoSave();
loadPOI(point.getPointOfInterest());
point.getMapSprite().checkOut();
WorldSave.getCurrentSave().getPointOfInterestChanges(point.getPointOfInterest().getID()).visit();
return true;
} else {
if (point == collidingPoint) {
@@ -365,7 +366,7 @@ public class WorldStage extends GameStage implements SaveFileContent {
super.draw();
if (WorldSave.getCurrentSave().getPlayer().hasAnnounceFantasy()) {
MapStage.getInstance().showDeckAwardDialog("{BLINK=WHITE;RED}Chaos Mode!{ENDBLINK}\n" +
"Enemy will use Preconstructed or Random Generated Decks. Genetic AI Decks will be available to some enemies on Hard difficulty.",
"Enemy will use Preconstructed or Random Generated Decks. Genetic AI Decks will be available to some enemies on Hard difficulty.",
WorldSave.getCurrentSave().getPlayer().getSelectedDeck());
WorldSave.getCurrentSave().getPlayer().clearAnnounceFantasy();
} else if (WorldSave.getCurrentSave().getPlayer().hasAnnounceCustom()) {

View File

@@ -48,6 +48,12 @@ public class Controls {
this.setWidth(this.layout.getWidth() + (this.style != null && this.style.background != null ? this.style.background.getLeftWidth() + this.style.background.getRightWidth() : 0.0F));
layout();
}
@Override
public void draw(Batch batch, float parentAlpha) {
batch.setColor(1f, 1f, 1f, 1f); // Set color before drawing each actor, per libGDX docs
super.draw(batch, parentAlpha);
}
}
static class TextButtonFix extends TextraButton {
@@ -249,6 +255,27 @@ public class Controls {
return ret;
}
static public SelectBox<Integer> newComboBox(Integer[] text, int item, Function<Object, Void> func) {
SelectBox<Integer> ret = newComboBox();
ret.getStyle().listStyle.selection.setTopHeight(4);
ret.setItems(text);
ret.addListener(new ChangeListener() {
@Override
public void changed(ChangeEvent event, Actor actor) {
try {
func.apply(((SelectBox) actor).getSelected());
} catch (Exception e) {
e.printStackTrace();
}
}
});
func.apply(item);
ret.getList().setAlignment(Align.center);
ret.setSelected(item);
ret.setAlignment(Align.right);
return ret;
}
static public TextField newTextField(String text) {
return new TextField(text, getSkin());
}
@@ -485,10 +512,11 @@ public class Controls {
private String currencyIcon;
private boolean isShards;
private int currencyAmount;
private float animationDelay = 2f; //seconds to wait before replacing intermediate label
private float intermediateDuration = 2f; // Seconds to wait before replacing intermediate label
private final String NEGDECOR = "[RED]-";
private final String POSDECOR = "[GREEN]+";
private final Timer t = new Timer();
private boolean isInitializing = true;
public AccountingLabel(TextraLabel target, boolean isShards) {
target.setVisible(false);
@@ -500,50 +528,72 @@ public class Controls {
if (isShards) {
currencyAmount = Current.player().getShards();
currencyIcon = "[+Shards]";
Current.player().onShardsChange(() -> update(AdventurePlayer.current().getShards(), true));
Current.player().onShardsChange(() -> {
if (!isInitializing) { // Avoid unwanted call to update() during scene initialization, triggering animation
update(AdventurePlayer.current().getShards(), true);
}
});
} else {
currencyAmount = Current.player().getGold();
currencyIcon = "[+Gold] "; //fix space since gold sprite is wider than a single glyph
Current.player().onGoldChange(() -> update(AdventurePlayer.current().getGold(), true));
Current.player().onGoldChange(() -> {
if (!isInitializing) { // Avoid unwanted call to update() during scene initialization, triggering animation
update(AdventurePlayer.current().getGold(), true);
}
});
}
label.setText(getLabelText(currencyAmount));
setName(label.getName());
replaceLabel(label);
isInitializing = false; // Initialization complete
}
public void setAnimationDelay(float animationDelay) {
this.animationDelay = animationDelay;
public void setIntermediateDuration(float intermediateDuration) {
this.intermediateDuration = intermediateDuration;
}
public float getAnimationDelay() {
return animationDelay;
public float getIntermediateDuration() {
return intermediateDuration;
}
public void update(int newAmount) {
update(newAmount, false);
}
public void update(int newAmount, boolean animate) {
public void update(int newAmount, boolean animateIntermediate) {
if (animate) {
if (animateIntermediate) {
TextraLabel temporaryLabel = getUpdateLabel(newAmount);
currencyAmount = newAmount;
replaceLabel(temporaryLabel);
t.schedule(new AccountingLabelUpdater(temporaryLabel), animationDelay);
// Add a quick 'bump' animation to the temporary label
SequenceAction sequence = new SequenceAction();
sequence.addAction(Actions.alpha(0.25f));
sequence.addAction(Actions.parallel(
Actions.alpha(1f, 0.05f, Interpolation.pow2Out),
Actions.moveBy(0f, 2f, 0.05f, Interpolation.pow2Out)
));
sequence.addAction(Actions.moveBy(0f, -2f, 0.05f, Interpolation.pow2Out));
temporaryLabel.addAction(sequence);
t.schedule(new AccountingLabelUpdater(temporaryLabel), intermediateDuration);
} else {
currencyAmount = newAmount;
drawFinalLabel(false);
drawFinalLabel(true); // Draw final label with animation since the intermediate label was not used.
}
}
private void drawFinalLabel(boolean fadeIn) {
private void drawFinalLabel(boolean animateFinal) {
TextraLabel finalLabel = getDefaultLabel();
if (fadeIn) {
if (animateFinal) {
// Add a quick fade-in animation to the final label
SequenceAction sequence = new SequenceAction();
sequence.addAction(Actions.alpha(0.5f));
sequence.addAction(Actions.alpha(1f, 2f, Interpolation.pow2Out));
sequence.addAction(Actions.alpha(0.25f));
sequence.addAction(Actions.alpha(1f, 0.1f, Interpolation.pow2Out));
finalLabel.addAction(sequence);
}
replaceLabel(finalLabel);
@@ -556,7 +606,7 @@ public class Controls {
private TextraLabel getUpdateLabel(int newAmount) {
int delta = newAmount - currencyAmount;
String updateText = delta == 0 ? "" : (delta < 0 ? NEGDECOR + delta * -1 : POSDECOR + delta);
return Controls.newTextraLabel(getLabelText(currencyAmount, updateText));
return Controls.newTextraLabel(getLabelText(newAmount, updateText));
}
private String getLabelText(int amount) {
@@ -579,13 +629,15 @@ public class Controls {
label.remove();
label = newLabel;
placeholder.getStage().addActor(label);
label.setZIndex(placeholder.getZIndex() - 1); // Ensure the new label is behind any tooltips that were present
}
private class AccountingLabelUpdater extends Timer.Task {
@Override
public void run() {
if (label.equals(target)) {
drawFinalLabel(true);
drawFinalLabel(false); // Passing false to avoid final animation since the intermediate label was animated
}
}

View File

@@ -8,82 +8,80 @@ import forge.gui.GuiBase;
// The standard button has index 0, controller binding is 1. Others can be added if needed.
public enum KeyBinding {
Left("Left", new int[]{Input.Keys.LEFT,Input.Keys.DPAD_LEFT, Input.Keys.A}),
Up("Up", new int[] {Input.Keys.UP,Input.Keys.DPAD_UP, Input.Keys.W}),
Right("Right", new int[] {Input.Keys.RIGHT,Input.Keys.DPAD_RIGHT, Input.Keys.D}),
Down("Down", new int[] {Input.Keys.DOWN,Input.Keys.DPAD_DOWN, Input.Keys.S}),
Menu("Menu", new int[] {Input.Keys.ESCAPE,Input.Keys.BUTTON_START}),
Inventory("Inventory", new int[] {Input.Keys.I,Input.Keys.BUTTON_X}),
Status("Status", new int[] {Input.Keys.Q,Input.Keys.BUTTON_Y}),
Deck("Deck", new int[] {Input.Keys.E,Input.Keys.BUTTON_A}),
Map("Map", new int[] {Input.Keys.M,Input.Keys.BUTTON_SELECT}),
Equip("Equip", new int[] {Input.Keys.E,Input.Keys.BUTTON_X}),
ExitToWorldMap("ExitToWorldMap",new int[] {Input.Keys.F4,Input.Keys.BUTTON_L2}),
Bookmark("Bookmark",new int[] {Input.Keys.B, Input.Keys.BUTTON_R2}),
Use("Use", new int[] {Input.Keys.ENTER,Input.Keys.BUTTON_A}),
Back("Back", new int[] {Input.Keys.ESCAPE,Input.Keys.BUTTON_B}),
ScrollUp("ScrollUp", new int[] {Input.Keys.PAGE_UP,Input.Keys.BUTTON_L1}),
ScrollDown("ScrollDown", new int[] {Input.Keys.PAGE_DOWN,Input.Keys.BUTTON_R1}),
Left("Left", new int[]{Input.Keys.LEFT, Input.Keys.DPAD_LEFT, Input.Keys.A}),
Up("Up", new int[]{Input.Keys.UP, Input.Keys.DPAD_UP, Input.Keys.W}),
Right("Right", new int[]{Input.Keys.RIGHT, Input.Keys.DPAD_RIGHT, Input.Keys.D}),
Down("Down", new int[]{Input.Keys.DOWN, Input.Keys.DPAD_DOWN, Input.Keys.S}),
Menu("Menu", new int[]{Input.Keys.ESCAPE, Input.Keys.BUTTON_START}),
Inventory("Inventory", new int[]{Input.Keys.I, Input.Keys.BUTTON_X}),
Status("Status", new int[]{Input.Keys.Q, Input.Keys.BUTTON_Y}),
Deck("Deck", new int[]{Input.Keys.E, Input.Keys.BUTTON_A}),
Map("Map", new int[]{Input.Keys.M, Input.Keys.BUTTON_SELECT}),
Equip("Equip", new int[]{Input.Keys.E, Input.Keys.BUTTON_X}),
ExitToWorldMap("ExitToWorldMap", new int[]{Input.Keys.F4, Input.Keys.BUTTON_L2}),
Bookmark("Bookmark", new int[]{Input.Keys.B, Input.Keys.BUTTON_R2}),
Use("Use", new int[]{Input.Keys.ENTER, Input.Keys.BUTTON_A}),
Back("Back", new int[]{Input.Keys.ESCAPE, Input.Keys.BUTTON_B, Input.Keys.BACK}),
ScrollUp("ScrollUp", new int[]{Input.Keys.PAGE_UP, Input.Keys.BUTTON_L1}),
ScrollDown("ScrollDown", new int[]{Input.Keys.PAGE_DOWN, Input.Keys.BUTTON_R1}),
;
String name;
int [] bindings;
final String name;
final int[] bindings;
KeyBinding(String name, int [] bindings)
{
this.name=name;
KeyBinding(String name, int[] bindings) {
this.name = name;
this.bindings = bindings;
}
public boolean isPressed(int key)
{
for(int i = 0; i < bindings.length; i++){
if(key == bindings[i]) {
return true;
public boolean isPressed(int key) {
return isPressed(key, null);
}
public boolean isPressed(int key, Boolean requiredCondition) {
if (requiredCondition == null || requiredCondition) {
for (int i = 0; i < bindings.length; i++) {
if (key == bindings[i]) {
return true;
}
}
}
return false;
}
// The controller binding always has index 1.
static String controllerPrefix="XBox_";
final static String controllerPrefix = "XBox_";
public String getLabelText(boolean pressed) {
if(Controllers.getCurrent()!=null)
{
return "[%120][+"+controllerPrefix+Input.Keys.toString(bindings[1]).replace(" Button","")+(pressed?"_pressed]":"]");
}
else
{
if(GuiBase.isAndroid())
if (Controllers.getCurrent() != null) {
return "[%120][+" + controllerPrefix + Input.Keys.toString(bindings[1]).replace(" Button", "") + (pressed ? "_pressed]" : "]");
} else {
if (GuiBase.isAndroid())
return "";
return "[%120][+"+Input.Keys.toString(bindings[0])+(pressed?"_pressed]":"]");
return "[%120][+" + Input.Keys.toString(bindings[0]) + (pressed ? "_pressed]" : "]");
}
}
public static int controllerButtonToKey(Controller controller,int key)
{
ControllerMapping map=controller.getMapping();
if(key==map.buttonA) return Input.Keys.BUTTON_A;
if(key==map.buttonB) return Input.Keys.BUTTON_B;
if(key==map.buttonX) return Input.Keys.BUTTON_X;
if(key==map.buttonY) return Input.Keys.BUTTON_Y;
if(key==map.buttonBack) return Input.Keys.BUTTON_SELECT;
if(key==map.buttonStart) return Input.Keys.BUTTON_START;
if(key==map.buttonL1) return Input.Keys.BUTTON_L1;
if(key==map.buttonL2) return Input.Keys.BUTTON_L2;
if(key==map.buttonR1) return Input.Keys.BUTTON_R1;
if(key==map.buttonR2) return Input.Keys.BUTTON_R2;
if(key==map.buttonDpadUp) return Input.Keys.DPAD_UP;
if(key==map.buttonDpadDown) return Input.Keys.DPAD_DOWN;
if(key==map.buttonDpadLeft) return Input.Keys.DPAD_LEFT;
if(key==map.buttonDpadRight) return Input.Keys.DPAD_RIGHT;
if(key==map.buttonLeftStick) return Input.Keys.BUTTON_THUMBL;
if(key==map.buttonRightStick) return Input.Keys.BUTTON_THUMBR;
public static int controllerButtonToKey(Controller controller, int key) {
ControllerMapping map = controller.getMapping();
if (key == map.buttonA) return Input.Keys.BUTTON_A;
if (key == map.buttonB) return Input.Keys.BUTTON_B;
if (key == map.buttonX) return Input.Keys.BUTTON_X;
if (key == map.buttonY) return Input.Keys.BUTTON_Y;
if (key == map.buttonBack) return Input.Keys.BUTTON_SELECT;
if (key == map.buttonStart) return Input.Keys.BUTTON_START;
if (key == map.buttonL1) return Input.Keys.BUTTON_L1;
if (key == map.buttonL2) return Input.Keys.BUTTON_L2;
if (key == map.buttonR1) return Input.Keys.BUTTON_R1;
if (key == map.buttonR2) return Input.Keys.BUTTON_R2;
if (key == map.buttonDpadUp) return Input.Keys.DPAD_UP;
if (key == map.buttonDpadDown) return Input.Keys.DPAD_DOWN;
if (key == map.buttonDpadLeft) return Input.Keys.DPAD_LEFT;
if (key == map.buttonDpadRight) return Input.Keys.DPAD_RIGHT;
if (key == map.buttonLeftStick) return Input.Keys.BUTTON_THUMBL;
if (key == map.buttonRightStick) return Input.Keys.BUTTON_THUMBR;
if(key==map.buttonDpadUp) return Input.Keys.DPAD_UP;
if(key==map.buttonDpadDown) return Input.Keys.DPAD_DOWN;
if(key==map.buttonDpadLeft) return Input.Keys.DPAD_LEFT;
if(key==map.buttonDpadRight) return Input.Keys.DPAD_RIGHT;
return 0;
}
}

View File

@@ -14,9 +14,12 @@ import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.Group;
import com.badlogic.gdx.scenes.scene2d.InputEvent;
import com.badlogic.gdx.scenes.scene2d.actions.Actions;
import com.badlogic.gdx.scenes.scene2d.InputListener;
import com.badlogic.gdx.scenes.scene2d.ui.Image;
import com.badlogic.gdx.scenes.scene2d.ui.Label;
import com.badlogic.gdx.scenes.scene2d.ui.Tooltip;
import com.badlogic.gdx.scenes.scene2d.ui.TooltipManager;
import com.badlogic.gdx.scenes.scene2d.utils.ActorGestureListener;
import com.badlogic.gdx.scenes.scene2d.utils.ClickListener;
import com.badlogic.gdx.scenes.scene2d.utils.TextureRegionDrawable;
@@ -120,10 +123,10 @@ public class RewardActor extends Actor implements Disposable, ImageFetcher.Callb
public void onImageFetched() {
ImageCache.getInstance().clear();
if(reward.type.equals(Reward.Type.Card)) {
imageKey = reward.getCard().getImageKey(false);
PaperCard card = ImageUtil.getPaperCardFromImageKey(imageKey);
imageKey = card.getCardImageKey();
int count = 0;
@@ -220,6 +223,7 @@ public class RewardActor extends Actor implements Disposable, ImageFetcher.Callb
reward.setAutoSell(!reward.isAutoSell());
String c = reward.isAutoSell() ? "[%85][GREEN]" : "[%85][GRAY]";
autoSell.setText(c + "\uFF04");
autoSell.getColor().a = reward.isAutoSell() ? 1f : 0.7f;
}
public RewardActor(Reward reward, boolean flippable, RewardScene.Type type, boolean showOverlay) {
@@ -235,6 +239,17 @@ public class RewardActor extends Actor implements Disposable, ImageFetcher.Callb
case Card: {
if (!reward.isNoSell) {
autoSell = Controls.newTextButton("[%85][GRAY]\uFF04");
autoSell.getColor().a = 0.7f; // semi-transparent by default
autoSell.addListener(new InputListener() {
@Override
public void enter(InputEvent event, float x, float y, int pointer, Actor fromActor) {
if (!reward.isAutoSell()) autoSell.getColor().a = 1f;
}
@Override
public void exit(InputEvent event, float x, float y, int pointer, Actor toActor) {
if (!reward.isAutoSell()) autoSell.getColor().a = 0.7f;
}
});
float scale = autoSell.getWidth();
autoSell.setSize(scale, scale * 1.2f);
autoSell.addListener(new ClickListener() {
@@ -246,7 +261,8 @@ public class RewardActor extends Actor implements Disposable, ImageFetcher.Callb
hasbackface = reward.getCard().hasBackFace();
if (ImageCache.getInstance().imageKeyFileExists(reward.getCard().getImageKey(false)) && !Forge.enableUIMask.equals("Art")) {
int count = 0;
File frontFace = ImageKeys.getImageFile(reward.getCard().getImageKey(false));
PaperCard card = ImageUtil.getPaperCardFromImageKey(reward.getCard().getImageKey(false));
File frontFace = ImageKeys.getImageFile(card.getCardImageKey());
if (frontFace != null) {
try {
Texture front = Forge.getAssets().manager().get(frontFace.getPath(), Texture.class, false);
@@ -272,7 +288,8 @@ public class RewardActor extends Actor implements Disposable, ImageFetcher.Callb
//preload card back for performance
if (hasbackface) {
if (ImageCache.getInstance().imageKeyFileExists(reward.getCard().getImageKey(true))) {
File backFace = ImageKeys.getImageFile(reward.getCard().getImageKey(true));
PaperCard cardBack = ImageUtil.getPaperCardFromImageKey(reward.getCard().getImageKey(true));
File backFace = ImageKeys.getImageFile(cardBack.getCardAltImageKey());
if (backFace != null) {
try {
Texture back = Forge.getAssets().manager().get(backFace.getPath(), Texture.class, false);
@@ -396,7 +413,8 @@ public class RewardActor extends Actor implements Disposable, ImageFetcher.Callb
if (imageKey != "") {
isBooster = true;
File file = new File(IMAGE_LIST_QUEST_BOOSTERS_FILE);
try (Scanner scanner = new Scanner(file)) {
try {
Scanner scanner = new Scanner(file);
String boosterPath = "";
while(scanner.hasNextLine())
{
@@ -564,8 +582,8 @@ public class RewardActor extends Actor implements Disposable, ImageFetcher.Callb
TextureRegionDrawable drawable = new TextureRegionDrawable(ImageCache.getInstance().croppedBorderImage(texture));
float origW = texture.getWidth();
float origH = texture.getHeight();
float boundW = Scene.getIntendedWidth() * 0.95f;
float boundH = Scene.getIntendedHeight() * 0.95f;
float boundW = GuiBase.isAndroid() ? Scene.getIntendedWidth() * 0.95f : Scene.getIntendedWidth() * 0.7f; // Use smaller size for Desktop
float boundH = GuiBase.isAndroid() ? Scene.getIntendedHeight() * 0.95f : Scene.getIntendedHeight() * 0.7f; // Use smaller size for Desktop
float newW = origW;
float newH = origH;
if (origW > boundW) {
@@ -843,7 +861,7 @@ public class RewardActor extends Actor implements Disposable, ImageFetcher.Callb
super.act(delta);
if (clicked) {
if (flipProcess < 1)
flipProcess += delta * 2.4;
flipProcess += delta * 4;
else
flipProcess = 1;
@@ -1109,11 +1127,17 @@ public class RewardActor extends Actor implements Disposable, ImageFetcher.Callb
public TextraLabel getStoredLabel() {
return cLabel;
}
@Override
public void draw(Batch batch, float parentAlpha) {
batch.setColor(1f, 1f, 1f, 1f); // Set color before drawing each actor, per libGDX docs
super.draw(batch, parentAlpha);
}
}
class ImageToolTip extends Tooltip<ComplexTooltip> {
public ImageToolTip(ComplexTooltip contents) {
super(contents);
super(contents, RewardTooltipManager.getInstance());
}
public Image getImage() {
@@ -1253,4 +1277,30 @@ public class RewardActor extends Actor implements Disposable, ImageFetcher.Callb
super.draw(batch, parentAlpha);
}
}
/**
* Extend and override TooltipManager to avoid the built-in default animations.
*/
static class RewardTooltipManager extends TooltipManager {
private static RewardTooltipManager instance;
public static RewardTooltipManager getInstance() {
if (instance == null) {
instance = new RewardTooltipManager();
}
return instance;
}
@Override
protected void showAction(Tooltip tooltip) {
// Overriding showAction for instant tooltip display
}
@Override
protected void hideAction(Tooltip tooltip) {
tooltip.getContainer().addAction(Actions.sequence(
Actions.removeActor() // Remove tooltip without animation
));
}
}
}

View File

@@ -247,6 +247,9 @@ public class ImageCache {
imageKey = imageKey.substring(0, imageKey.length() - ImageKeys.BACKFACE_POSTFIX.length());
}
if (imageKey.startsWith(ImageKeys.CARD_PREFIX)) {
PaperCard card = ImageUtil.getPaperCardFromImageKey(imageKey);
if (card != null)
imageKey = altState ? card.getCardAltImageKey() : card.getCardImageKey();
if (StringUtils.isBlank(imageKey)) {
if (useDefaultIfNotFound)
return getDefaultImage();
@@ -292,7 +295,7 @@ public class ImageCache {
image fetcher to update automatically after the card image/s are downloaded*/
imageLoaded = false;
if (image != null && imageRecord.get().get(image.toString()) == null)
imageRecord.get().put(image.toString(), new ImageRecord(Color.valueOf("#171717").toString(), false, getRadius(image))); //black border
imageRecord.get().put(image.toString(), new ImageRecord(Color.valueOf("#171717").toString(), false, getRadius(image), image.toString().contains(".fullborder.") || image.toString().contains("tokens"))); //black border
}
}
return image;
@@ -346,7 +349,7 @@ public class ImageCache {
radius = 22;
updateImageRecord(cardTexture.toString(),
borderless ? Color.valueOf("#171717").toString() : isCloserToWhite(getpixelColor(cardTexture)).getLeft(),
!borderless && isCloserToWhite(getpixelColor(cardTexture)).getRight(), radius);
!borderless && isCloserToWhite(getpixelColor(cardTexture)).getRight(), radius, cardTexture.toString().contains(".fullborder.") || cardTexture.toString().contains("tokens"));
}
return cardTexture;
}
@@ -441,8 +444,8 @@ public class ImageCache {
return 1;
return 0;
}
public void updateImageRecord(String textureString, String colorValue, Boolean isClosertoWhite, int radius) {
imageRecord.get().put(textureString, new ImageRecord(colorValue, isClosertoWhite, radius));
public void updateImageRecord(String textureString, String colorValue, Boolean isClosertoWhite, int radius, boolean fullborder) {
imageRecord.get().put(textureString, new ImageRecord(colorValue, isClosertoWhite, radius, fullborder));
}
public int getRadius(Texture t) {
@@ -457,6 +460,15 @@ public class ImageCache {
return i;
}
public boolean isFullBorder(Texture image) {
if (image == null)
return false;
ImageRecord record = imageRecord.get().get(image.toString());
if (record == null)
return false;
return record.isFullBorder;
}
public FImage getBorder(String textureString) {
ImageRecord record = imageRecord.get().get(textureString);
if (record == null)
@@ -541,11 +553,13 @@ public class ImageCache {
String colorValue;
Boolean isCloserToWhite;
Integer cardRadius;
boolean isFullBorder;
ImageRecord(String colorString, Boolean closetoWhite, int radius) {
ImageRecord(String colorString, Boolean closetoWhite, int radius, boolean fullborder) {
colorValue = colorString;
isCloserToWhite = closetoWhite;
cardRadius = radius;
isFullBorder = fullborder;
}
}
}

View File

@@ -34,21 +34,22 @@ public class CardImage implements FImage {
@Override
public void draw(Graphics g, float x, float y, float w, float h) {
CardView cv = CardView.getCardForUi(card);
if (image == null) { //attempt to retrieve card image if needed
image = ImageCache.getInstance().getImage(card);
if (image == null) {
if (!Forge.enableUIMask.equals("Off")) //render this if mask is still loading
CardImageRenderer.drawCardImage(g, CardView.getCardForUi(card), false, x, y, w, h, CardStackPosition.Top, Forge.enableUIMask.equals("Art"), true);
CardImageRenderer.drawCardImage(g, cv, false, x, y, w, h, CardStackPosition.Top, Forge.enableUIMask.equals("Art"), true);
return; //can't draw anything if can't be loaded yet
}
}
if (image == ImageCache.getInstance().getDefaultImage() || Forge.enableUIMask.equals("Art")) {
CardImageRenderer.drawCardImage(g, CardView.getCardForUi(card), false, x, y, w, h, CardStackPosition.Top, true, true);
CardImageRenderer.drawCardImage(g, cv, false, x, y, w, h, CardStackPosition.Top, true, true);
} else {
if (Forge.enableUIMask.equals("Full")) {
if (image.toString().contains(".fullborder."))
if (ImageCache.getInstance().isFullBorder(image))
g.drawCardRoundRect(image, null, x, y, w, h, false, false);
else {
float radius = (h - w) / 8;

View File

@@ -9,9 +9,9 @@ import java.util.List;
import forge.ImageKeys;
import forge.assets.*;
import forge.item.PaperCard;
import forge.util.ImageUtil;
import forge.util.TextBounds;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import com.badlogic.gdx.graphics.Color;
@@ -156,7 +156,13 @@ public class CardImageRenderer {
}
//space for artist
textBoxHeight -= 2 * PT_FONT.getCapHeight();
String artist = ObjectUtils.firstNonNull(state.getArtist(), "WOTC");
PaperCard paperCard = null;
try {
paperCard = ImageUtil.getPaperCardFromImageKey(state.getImageKey());
} catch (Exception e) {}
String artist = "WOTC";
if (paperCard != null && !paperCard.getArtist().isEmpty())
artist = paperCard.getArtist();
float minTextBoxHeight = 2 * headerHeight;
if (textBoxHeight < minTextBoxHeight) {
artHeight -= (minTextBoxHeight - textBoxHeight); //subtract from art height if text box not big enough otherwise
@@ -808,7 +814,7 @@ public class CardImageRenderer {
}
if (rotatePlane && (card.getCurrentState().isPhenomenon() || card.getCurrentState().isPlane() || (card.getCurrentState().isBattle() && !altState) || (card.getAlternateState() != null && card.getAlternateState().isBattle() && altState))) {
if (Forge.enableUIMask.equals("Full")) {
if (image.toString().contains(".fullborder."))
if (ImageCache.getInstance().isFullBorder(image))
g.drawCardRoundRect(image, new_x, new_y, new_w, new_h, new_x + new_w / 2, new_y + new_h / 2, -90);
else {
g.drawRotatedImage(FSkin.getBorders().get(0), new_x, new_y, new_w, new_h, new_x + new_w / 2, new_y + new_h / 2, -90);
@@ -821,7 +827,7 @@ public class CardImageRenderer {
} else if (rotateSplit && isCurrentCard && card.isSplitCard() && canshow && !card.isFaceDown()) {
boolean isAftermath = card.getText().contains("Aftermath") || card.getAlternateState().getOracleText().contains("Aftermath");
if (Forge.enableUIMask.equals("Full")) {
if (image.toString().contains(".fullborder."))
if (ImageCache.getInstance().isFullBorder(image))
g.drawCardRoundRect(image, new_x, new_y, new_w, new_h, new_x + new_w / 2, new_y + new_h / 2, isAftermath ? 90 : -90);
else {
g.drawRotatedImage(FSkin.getBorders().get(ImageCache.getInstance().getFSkinBorders(card)), new_x, new_y, new_w, new_h, new_x + new_w / 2, new_y + new_h / 2, isAftermath ? 90 : -90);
@@ -837,7 +843,7 @@ public class CardImageRenderer {
if (card.isSplitCard() && rotateSplit && isCurrentCard) {
boolean isAftermath = card.getText().contains("Aftermath") || card.getAlternateState().getOracleText().contains("Aftermath");
if (Forge.enableUIMask.equals("Full")) {
if (image.toString().contains(".fullborder."))
if (ImageCache.getInstance().isFullBorder(image))
g.drawCardRoundRect(image, new_x, new_y, new_w, new_h, new_x + new_w / 2, new_y + new_h / 2, isAftermath ? 90 : -90);
else {
g.drawRotatedImage(FSkin.getBorders().get(ImageCache.getInstance().getFSkinBorders(card)), new_x, new_y, new_w, new_h, new_x + new_w / 2, new_y + new_h / 2, isAftermath ? 90 : -90);
@@ -849,7 +855,7 @@ public class CardImageRenderer {
g.drawRotatedImage(image, new_x, new_y, new_w, new_h, new_x + new_w / 2, new_y + new_h / 2, isAftermath ? 90 : -90);
} else {
if (Forge.enableUIMask.equals("Full")) {
if (image.toString().contains(".fullborder."))
if (ImageCache.getInstance().isFullBorder(image))
g.drawCardRoundRect(image, null, x, y, w, h, false, false);
else {
g.drawImage(ImageCache.getInstance().getBorderImage(image.toString()), ImageCache.getInstance().borderColor(image), x, y, w, h);
@@ -866,7 +872,7 @@ public class CardImageRenderer {
g.drawImage(sleeves, x, y, w, h);
}
} else if (Forge.enableUIMask.equals("Full") && canshow) {
if (image.toString().contains(".fullborder."))
if (ImageCache.getInstance().isFullBorder(image))
g.drawCardRoundRect(image, null, x, y, w, h, false, false);
else {
g.drawImage(ImageCache.getInstance().getBorderImage(image.toString()), ImageCache.getInstance().borderColor(image), x, y, w, h);

View File

@@ -597,6 +597,7 @@ public class CardRenderer {
public static void drawCard(Graphics g, IPaperCard pc, float x, float y, float w, float h, CardStackPosition pos) {
Texture image = new RendererCachedCardImage(pc, false).getImage();
final CardView card = CardView.getCardForUi(pc);
float radius = (h - w) / 8;
float croppedArea = isModernFrame(pc) ? CROP_MULTIPLIER : 0.97f;
float minusxy = isModernFrame(pc) ? 0.0f : 0.13f * radius;
@@ -609,7 +610,7 @@ public class CardRenderer {
CardImageRenderer.drawCardImage(g, CardView.getCardForUi(pc), false, x, y, w, h, pos, true, true);
} else {
if (Forge.enableUIMask.equals("Full")) {
if (image.toString().contains(".fullborder."))
if (ImageCache.getInstance().isFullBorder(image))
g.drawCardRoundRect(image, null, x, y, w, h, false, false);
else {
//tint the border
@@ -622,7 +623,6 @@ public class CardRenderer {
g.drawImage(image, x, y, w, h);
}
if (pc.isFoil()) { //draw foil effect if needed
final CardView card = CardView.getCardForUi(pc);
if (card.getCurrentState().getFoilIndex() == 0) { //if foil finish not yet established, assign a random one
card.getCurrentState().setFoilIndexOverride(-1);
}
@@ -630,7 +630,7 @@ public class CardRenderer {
}
} else {
//if card has invalid or no texture due to sudden changes in ImageCache, draw CardImageRenderer instead and wait for it to refresh automatically
CardImageRenderer.drawCardImage(g, CardView.getCardForUi(pc), false, x, y, w, h, pos, true, true);
CardImageRenderer.drawCardImage(g, card, false, x, y, w, h, pos, true, true);
}
}
@@ -663,7 +663,7 @@ public class CardRenderer {
if (FModel.getPreferences().getPrefBoolean(ForgePreferences.FPref.UI_ROTATE_PLANE_OR_PHENOMENON)
&& (card.getCurrentState().isPhenomenon() || card.getCurrentState().isPlane() || (card.getCurrentState().isBattle() && !showAltState) || (card.getAlternateState() != null && card.getAlternateState().isBattle() && showAltState)) && rotate) {
if (Forge.enableUIMask.equals("Full")) {
if (image.toString().contains(".fullborder."))
if (ImageCache.getInstance().isFullBorder(image))
g.drawCardRoundRect(image, x, y, w, h, x + w / 2, y + h / 2, -90);
else {
g.drawRotatedImage(FSkin.getBorders().get(0), x, y, w, h, x + w / 2, y + h / 2, -90);
@@ -675,7 +675,7 @@ public class CardRenderer {
g.drawRotatedImage(image, x, y, w, h, x + w / 2, y + h / 2, -90);
} else {
if (Forge.enableUIMask.equals("Full") && canshow) {
if (image.toString().contains(".fullborder."))
if (ImageCache.getInstance().isFullBorder(image))
g.drawCardRoundRect(image, crack_overlay, x, y, w, h, drawGray(card), magnify ? false : card.getDamage() > 0);
else {
//boolean t = (card.getCurrentState().getOriginalColors() != card.getCurrentState().getColors()) || card.getCurrentState().hasChangeColors();

View File

@@ -103,22 +103,27 @@ public class CardZoom extends FOverlay {
if (pc != null) {
Card cardW = Card.fromPaperCard(pc, null);
cardW.setState(CardStateName.SpecializeW, true);
cardW.setImageKey(pc, CardStateName.SpecializeW);
list.add(cardW.getView());
Card cardU = Card.fromPaperCard(pc, null);
cardU.setState(CardStateName.SpecializeU, true);;
cardU.setState(CardStateName.SpecializeU, true);
cardU.setImageKey(pc, CardStateName.SpecializeU);
list.add(cardU.getView());
Card cardB = Card.fromPaperCard(pc, null);
cardB.setState(CardStateName.SpecializeB, true);
cardB.setImageKey(pc, CardStateName.SpecializeB);
list.add(cardB.getView());
Card cardR = Card.fromPaperCard(pc, null);
cardR.setState(CardStateName.SpecializeR, true);
cardR.setImageKey(pc, CardStateName.SpecializeR);
list.add(cardR.getView());
Card cardG = Card.fromPaperCard(pc, null);
cardG.setState(CardStateName.SpecializeG, true);
cardG.setImageKey(pc, CardStateName.SpecializeG);
list.add(cardG.getView());
}
if (!list.isEmpty())

View File

@@ -1997,7 +1997,9 @@ public class FDeckEditor extends TabPageScreen<FDeckEditor> {
@Override
public void refresh() {
BoosterDraft draft = parentScreen.getDraft();
if (draft == null) { return; }
if (draft == null || !draft.hasNextChoice()) {
return;
}
CardPool pool = draft.nextChoice();
int packNumber = draft.getCurrentBoosterIndex() + 1;

View File

@@ -72,6 +72,21 @@ public class FDeckViewer extends FScreen {
FOptionPane.showMessageDialog(Forge.getLocalizer().getMessage("lblDeckListCopiedClipboard", deck.getName()));
}
public static void copyCollectionToClipboard(CardPool pool) {
final String nl = System.lineSeparator();
final StringBuilder collectionList = new StringBuilder();
Set<String> accounted = new HashSet<>();
for (final Entry<PaperCard, Integer> entry : pool) {
String cardName = entry.getKey().getCardName();
if (!accounted.contains(cardName)) {
collectionList.append(pool.countByName(cardName)).append(" ").append(cardName).append(nl);
accounted.add(cardName);
}
}
Forge.getClipboard().setContents(collectionList.toString());
FOptionPane.showMessageDialog(Forge.getLocalizer().getMessage("lblCollectionCopiedClipboard"));
}
private final Deck deck;
private final CardManager cardManager;
private DeckSection currentSection;

View File

@@ -190,4 +190,8 @@ public abstract class FDisplayObject {
public boolean keyDown(int keyCode) {
return false;
}
public boolean keyUp(int keyCode) {
return false;
}
}

View File

@@ -20,7 +20,7 @@ Name=kavu
1 Kavu Primarch|TSR|1
4 Mountain|MH2|1
3 Mountain|MH2|2
2 Punishing Fire|C11|1
2 Punishing Fire|CMD|1
2 Raging Kavu|INV|1
2 Ram Through|IKO|1
2 Sacred Foundry|GRN|1

View File

@@ -32,7 +32,7 @@
<object id="53" template="../../obj/treasure.tx" x="194" y="125"/>
<object id="54" template="../../obj/enemy.tx" x="201.253" y="161.473">
<properties>
<property name="effect" value="{ &quot;startBattleWithCard&quot;: [ &quot;Dreadhorde Invasion&quot; }"/>
<property name="effect" value="{ &quot;startBattleWithCard&quot;: [&quot;Dreadhorde Invasion&quot;]}"/>
<property name="enemy" value="Big Zombie"/>
<property name="speedModifier" type="float" value="3"/>
<property name="threatRange" type="int" value="60"/>

View File

@@ -184,7 +184,7 @@
&quot;type&quot;: &quot;item&quot;,
&quot;count&quot;: 1,
&quot;probability&quot;: 1,
&quot;itemName&quot;: &quot;Lightbringers Boots'&quot;
&quot;itemName&quot;: &quot;Lightbringers Boots&quot;
}
]</property>
<property name="spawn.Easy" type="bool" value="true"/>

View File

@@ -74,7 +74,7 @@
</object>
<object id="58" template="../../obj/enemy.tx" x="153" y="516">
<properties>
<property name="effect" value="{ &quot;startBattleWithCard&quot;: [ &quot;Exquisite Blood&quot; }"/>
<property name="effect" value="{ &quot;startBattleWithCard&quot;: [ &quot;Exquisite Blood&quot; ] }"/>
<property name="enemy" value="Fallen Angel"/>
<property name="threatRange" type="int" value="60"/>
</properties>

View File

@@ -6,7 +6,7 @@
<tileset firstgid="1" source="../../tileset/main.tsx"/>
<tileset firstgid="10113" source="../../tileset/buildings.tsx"/>
<tileset firstgid="11905" source="../../tileset/dungeon.tsx"/>
<tileset firstgid="16001" source="../../tileset/main-nocollide.tsx"/>
<tileset firstgid="16385" source="../../tileset/main-nocollide.tsx"/>
<layer id="1" name="Background" width="35" height="25">
<data encoding="base64" compression="zlib">
eJybxsnAMG0AMDYwEO4YCm6hBxhIu9HBqFuwg8HmloFOsyMFqLIPtAsgAOSOUbdggsHijlEwCgYzmCM60C4YBaNg6AIAYHk9+Q==
@@ -24,12 +24,12 @@
</layer>
<layer id="5" name="AboveSprites" width="35" height="25">
<data encoding="base64" compression="zlib">
eJzNlktuwjAQhp22Ujelq8SqVJUdN2BTIagESH0seyV6jKqAaLsADsAtOEDLgoPwGAtHHqYe26Qh5Je+lfP4PBk7FqLYDBKesnj4iICzQH6kEL/AQv4d28Tue88LrE3f845hiV0iUrNTugyT/TFbHl/L44LzeSfEBQNNE/XzKNnNCffuB1kvDWmuaeh7H6Tb5a2+TwfoAt8wVrs0+ILr4qsh57KUBpWediraRWV+a+ih+nAunNupXK5grAJco77K4xt9JYa0V2wumDGMTYAp49Ik+3aoC+1dri6Yp6oQz8BL1e6CWcdhLm14Voehq99Dv1NIv2BC9passdXJ5rKO/XvKsUJdsq4lmsjRd+rs0JKG9Do1/2O43Ou6zm520IScm961I/5X/Od/7XNxudl6Nksfr2K3iyt5u6j6pv1waPJ24faKkPM+t/fis8Mh0DWRxaUoXNkCTvTB1g==
eJzNlk9OAjEUhztI4gZczTQmBHbcwI0hSCIslC1HgmMQlYAu1AN4A5ccAFlwENHX0Ekfz762jMMwv+Rbdf58ffPaqRDF5inhKYuHjwioBLKSQnwBa/l37Cd233tWYG0ePe+YldglIjU7pcss2R+z5XNUHhecRVOIKgNNF/XzPNnNCffuA1kvHWmu6eh7e9LtMrnapw8MgBcYa58bfMF18dWQc9lIg8pYOxXtorJsGMaoPpwL53YqlxqM1YEL1Fd5fKPnxJD2is0F8wpjb8A749Il+3aoC+1dri6Yu5YQ98CwZXfBbOMwl1t4Vp9hoN9Dv1NIv2BC9passdXJ5rKN/XvKsUJdsq4lmsjRd+rscCMN6XVq/sdwudZ1/bjcQRNybppqR/yv+M//2uficrP1bJY+/o7dLq7k7aLqm/bDocnbhdsrQs773N6Lzw6HQNdEFpeicOUXefTD2g==
</data>
</layer>
<layer id="6" name="Clutter" width="35" height="25">
<data encoding="base64" compression="zlib">
eJxjYBgF1Ab/RUgTpxSsEIVgEMgF2rEcyA6XhPAXimLXAxK/5s/AcB2Ib/hTzy0HJfDIhSDYK5HchcuN1HDLSjz+J0WcEjAHaCYTElYOQeXbiaFimLi9GKZZPcHUdx8yQHcLDHPI4tYD8xMhQIwaXKDCgoGh0oJ8/dQEO4Du2Eklt5QHUMccEBDGkff2KTAw7FdA0LiAaCDt3RKjyMAQq4igcYHNFLoFOb0QGy6q7PjNZCazbEBOL8SGCyG30KrMBAGQ3eiYEKBVuUSqOwiBRBFMTAmgpr+p5abBDiipB2gNqFkeUwqoWR5TCigtj0cB5QAAqOk3GA==
eJxjYBgF1Ab/RUgTpxSsEIVgEMgF2rEcyA6XhPAXimLXAxIPCwSqA+KIQOq55aAEbjnHMAR7JZK7cLmRGm5Zicf/pIhTAuYAzWRCwotDUfl2YqgYJm4vhmkWTyj13YcM0N0CwxyyuPXA/EQIEKMGF6iwYGCotCBfPzXBDqA7dlLJLd+pmPeEceS9fQoMDPsVEDQuMDWI9m6JUWRgiFVE0LiAcTBl9iOnF2LDRZUdv5nMZJYNyOmF2HAh5BZalZkgALIbHRMCtCqXSHUHIZAogokpAdT0N7XcNNgBJfUArQE1y2NKATXLY0oBpeXxKKAcAABkFTay
</data>
</layer>
<layer id="7" name="AboveAboveClutter" width="35" height="25">
@@ -52,7 +52,7 @@
<object id="49" template="../../obj/gold.tx" x="48.3333" y="126.333"/>
<object id="50" template="../../obj/enemy.tx" x="298.667" y="182">
<properties>
<property name="effect" value="{ &quot;startBattleWithCard&quot;: [ &quot;Black Market&quot; }"/>
<property name="effect" value="{ &quot;startBattleWithCard&quot;: [&quot;Black Market&quot;]}"/>
<property name="enemy" value="Cosmic Horror"/>
<property name="threatRange" type="int" value="40"/>
</properties>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.10.1" orientation="orthogonal" renderorder="right-down" width="30" height="17" tilewidth="16" tileheight="16" infinite="0" nextlayerid="8" nextobjectid="113">
<map version="1.10" tiledversion="1.10.1" orientation="orthogonal" renderorder="right-down" width="30" height="17" tilewidth="16" tileheight="16" infinite="0" nextlayerid="8" nextobjectid="138">
<editorsettings>
<export target="wastetown..tmx" format="tmx"/>
</editorsettings>
@@ -106,36 +106,37 @@
<property name="threatRange" type="int" value="30"/>
</properties>
</object>
<object id="87" template="../../obj/enemy.tx" x="278.667" y="203.667">
<object id="87" template="../../obj/enemy.tx" x="276.711" y="206.275">
<properties>
<property name="enemy" value="Monk"/>
<property name="threatRange" type="int" value="40"/>
</properties>
</object>
<object id="88" template="../../obj/exit.tx" x="162.521" y="212.637" width="24.2714" height="6.20943"/>
<object id="89" template="../../obj/exit.tx" x="163.046" y="180.468" width="24.2714" height="6.20943"/>
<object id="90" template="../../obj/exit.tx" x="114.792" y="180.818" width="24.2714" height="6.20943"/>
<object id="91" template="../../obj/exit.tx" x="114.792" y="116.48" width="24.2714" height="6.20943"/>
<object id="92" template="../../obj/exit.tx" x="115.142" y="148.299" width="24.2714" height="6.20943"/>
<object id="93" template="../../obj/exit.tx" x="162.696" y="117.179" width="24.2714" height="6.20943"/>
<object id="94" template="../../obj/exit.tx" x="162.696" y="85.36" width="24.2714" height="6.20943"/>
<object id="95" template="../../obj/exit.tx" x="114.792" y="85.36" width="24.2714" height="6.20943"/>
<object id="96" template="../../obj/exit.tx" x="162.696" y="149.348" width="24.2714" height="6.20943"/>
<object id="97" template="../../obj/exit.tx" x="227.034" y="148.299" width="24.2714" height="6.20943"/>
<object id="98" template="../../obj/exit.tx" x="255.357" y="51.7924" width="15.5298" height="6.20943"/>
<object id="99" template="../../obj/exit.tx" x="273.714" y="84.6607" width="24.621" height="6.20943"/>
<object id="100" template="../../obj/exit.tx" x="274.413" y="117.179" width="24.621" height="6.20943"/>
<object id="101" template="../../obj/exit.tx" x="226.16" y="212.288" width="24.621" height="6.20943"/>
<object id="102" template="../../obj/exit.tx" x="275.113" y="148.299" width="24.621" height="6.20943"/>
<object id="103" template="../../obj/exit.tx" x="227.558" y="180.468" width="24.621" height="6.20943"/>
<object id="104" template="../../obj/exit.tx" x="226.16" y="116.83" width="24.621" height="6.20943"/>
<object id="105" template="../../obj/exit.tx" x="225.81" y="84.6607" width="24.621" height="6.20943"/>
<object id="106" template="../../obj/exit.tx" x="193.291" y="31.5119" width="29.5163" height="35.5811"/>
<object id="107" template="../../obj/exit.tx" x="323.016" y="98.6472" width="8.18684" height="24.0422"/>
<object id="108" template="../../obj/exit.tx" x="323.191" y="145.677" width="8.18684" height="24.0422"/>
<object id="109" template="../../obj/exit.tx" x="98.3579" y="98.8221" width="8.18684" height="24.0422"/>
<object id="110" template="../../obj/exit.tx" x="98.0082" y="148.474" width="8.18684" height="24.0422"/>
<object id="111" template="../../obj/waypoint.tx" x="201.099" y="238.512"/>
<object id="112" template="../../obj/waypoint.tx" x="201.448" y="105.641"/>
<object id="113" template="../../obj/collision.tx" x="226.244" y="149.837" width="24.0078" height="5.4"/>
<object id="114" template="../../obj/collision.tx" x="226.196" y="181.7" width="24.0078" height="5.4"/>
<object id="115" template="../../obj/collision.tx" x="226.996" y="213.5" width="24.0078" height="5.4"/>
<object id="116" template="../../obj/collision.tx" x="275.396" y="181.9" width="24.0078" height="5.4"/>
<object id="117" template="../../obj/collision.tx" x="274.596" y="117.3" width="24.0078" height="5.4"/>
<object id="118" template="../../obj/collision.tx" x="274.596" y="148.9" width="24.0078" height="5.4"/>
<object id="119" template="../../obj/collision.tx" x="226.596" y="116.7" width="24.0078" height="5.4"/>
<object id="120" template="../../obj/collision.tx" x="227.196" y="85.7" width="24.0078" height="5.4"/>
<object id="121" template="../../obj/collision.tx" x="274.996" y="85.3" width="24.0078" height="5.4"/>
<object id="122" template="../../obj/collision.tx" x="324.196" y="99.5" width="7.0078" height="23"/>
<object id="123" template="../../obj/collision.tx" x="323.896" y="147.7" width="7.0078" height="23"/>
<object id="124" template="../../obj/collision.tx" x="99.0961" y="147.9" width="7.0078" height="23"/>
<object id="125" template="../../obj/collision.tx" x="98.4961" y="100.3" width="7.0078" height="23"/>
<object id="126" template="../../obj/collision.tx" x="115.896" y="116.9" width="22.6078" height="6.6"/>
<object id="127" template="../../obj/collision.tx" x="115.496" y="148.7" width="22.6078" height="6.6"/>
<object id="128" template="../../obj/collision.tx" x="115.696" y="180.9" width="22.6078" height="6.6"/>
<object id="129" template="../../obj/collision.tx" x="163.296" y="212.9" width="22.6078" height="6.6"/>
<object id="130" template="../../obj/collision.tx" x="163.296" y="180.5" width="22.6078" height="6.6"/>
<object id="131" template="../../obj/collision.tx" x="163.696" y="148.5" width="22.6078" height="6.6"/>
<object id="132" template="../../obj/collision.tx" x="163.496" y="116.3" width="22.6078" height="6.6"/>
<object id="133" template="../../obj/collision.tx" x="163.296" y="84.1" width="22.6078" height="6.6"/>
<object id="134" template="../../obj/collision.tx" x="115.696" y="84.1" width="22.6078" height="6.6"/>
<object id="135" template="../../obj/collision.tx" x="256.296" y="51.1" width="16.2078" height="8.4"/>
<object id="136" template="../../obj/collision.tx" x="192.496" y="31.9" width="31.2078" height="36.8"/>
</objectgroup>
</map>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.10.2" orientation="orthogonal" renderorder="right-down" width="60" height="50" tilewidth="16" tileheight="16" infinite="0" nextlayerid="13" nextobjectid="119">
<map version="1.10" tiledversion="1.10.1" orientation="orthogonal" renderorder="right-down" width="60" height="50" tilewidth="16" tileheight="16" infinite="0" nextlayerid="13" nextobjectid="119">
<editorsettings>
<export target="wastetown..tmx" format="tmx"/>
</editorsettings>
@@ -173,7 +173,7 @@
<property name="enemy" value="Greater Sandwurm"/>
<property name="hidden" type="bool" value="true"/>
<property name="speedModifier" type="float" value="40"/>
<property name="threatRange" type="int" value="30"/>
<property name="threatRange" type="int" value="80"/>
</properties>
</object>
<object id="115" template="../../obj/enemy.tx" x="855.333" y="361.333">
@@ -181,7 +181,7 @@
<property name="enemy" value="Greater Sandwurm"/>
<property name="hidden" type="bool" value="true"/>
<property name="speedModifier" type="float" value="40"/>
<property name="threatRange" type="int" value="30"/>
<property name="threatRange" type="int" value="70"/>
</properties>
</object>
<object id="116" template="../../obj/enemy.tx" x="294" y="367.333">
@@ -197,7 +197,7 @@
<property name="enemy" value="Scarab"/>
<property name="hidden" type="bool" value="true"/>
<property name="speedModifier" type="float" value="40"/>
<property name="threatRange" type="int" value="50"/>
<property name="threatRange" type="int" value="60"/>
</properties>
</object>
<object id="118" template="../../obj/booster.tx" x="4" y="641.333">

View File

@@ -83,7 +83,7 @@
]
}
]</property>
<property name="displayNameOverride" value="Tibalt's Pet Torturer"/>
<property name="displayNameOverride" value="Tibalt's Torturer"/>
<property name="effect">{ &quot;startBattleWithCard&quot;: [ &quot;Phyrexian Arena&quot;,&quot;Xander's Wake&quot;]
}</property>
<property name="enemy" value="Torturer"/>

View File

@@ -162,8 +162,8 @@
"minHeight": 16,
"rightWidth": 4,
"leftWidth": 4,
"bottomHeight": 7,
"topHeight": 4,
"bottomHeight": 8,
"topHeight": 5,
"offsetX": 0,
"offsetY": 0,
"offsetXspeed": 0,
@@ -566,7 +566,7 @@
"downFontColor": "RGBA_255_255_255_255",
"overFontColor": "RGBA_255_255_255_255",
"focusedFontColor": "RGBA_255_255_255_255",
"disabledFontColor": "RGBA_255_255_255_255",
"disabledFontColor": "RGBA_204_200_200_255",
"checkedFontColor": "RGBA_255_255_255_255",
"checkedDownFontColor": "RGBA_255_255_255_255",
"checkedOverFontColor": "RGBA_255_255_255_255",

View File

@@ -1,4 +1,4 @@
Deathknightidle.png
deathknightidle.png
size: 80,16
format: RGBA8888
filter: Nearest,Nearest

View File

@@ -34,7 +34,7 @@
"width": 100,
"height": 30,
"x": 365,
"y": 60
"y": 50
},
{
"type": "TextButton",
@@ -44,7 +44,7 @@
"width": 100,
"height": 30,
"x": 365,
"y": 110
"y": 90
},
{
"type": "TextButton",
@@ -53,7 +53,7 @@
"width": 100,
"height": 30,
"x": 365,
"y": 160
"y": 130
},
{
"type": "TextButton",
@@ -62,6 +62,15 @@
"width": 100,
"height": 30,
"x": 365,
"y": 170
},
{
"type": "TextButton",
"name": "add",
"text": "tr(lblAddDeck)",
"width": 100,
"height": 30,
"x": 365,
"y": 210
}
]

View File

@@ -15,33 +15,42 @@
"x": 4,
"y": 4,
"width": 262,
"height": 414
"height": 384
},
{
"type": "TextButton",
"name": "add",
"text": "tr(lblAddDeck)",
"width": 130,
"height": 30,
"x": 4,
"y": 388
},
{
"type": "TextButton",
"name": "delete",
"text": "tr(lblDelete)",
"width": 86,
"width": 130,
"height": 30,
"x": 136,
"y": 388
},
{
"type": "TextButton",
"name": "copy",
"text": "tr(lblCopy)",
"width": 130,
"height": 30,
"x": 4,
"y": 418
},
{
"type": "TextButton",
"name": "copy",
"text": "tr(lblCopy)",
"width": 86,
"height": 30,
"x": 92,
"y": 418
},
{
"type": "TextButton",
"name": "rename",
"text": "tr(lblRename)",
"width": 86,
"width": 130,
"height": 30,
"x": 180,
"x": 136,
"y": 418
},
{

View File

@@ -37,10 +37,10 @@
{
"type": "TextButton",
"name": "done",
"text": "[+OK]",
"text": "Back",
"width": 48,
"height": 30,
"binding": "Use",
"binding": "Back",
"x": 420,
"y": 120
} ,

View File

@@ -2,10 +2,10 @@
"width": 480,
"height": 270,
"yDown": true,
"elements": [
"elements": [
{
"type": "Scroll",
"name": "map",
"name": "map",
"width": 480,
"height": 270
},
@@ -28,6 +28,60 @@
"height": 20,
"x": 415,
"y": 245
}
},
{
"type": "TextButton",
"name": "details",
"text": "[%80] Details",
"width": 60,
"height": 20,
"x": 210,
"y": 245
},
{
"type": "TextButton",
"name": "events",
"text": "[%80] Events",
"width": 60,
"height": 20,
"x": 210,
"y": 245
},
{
"type": "TextButton",
"name": "reputation",
"text": "[%80] Reputation",
"width": 60,
"height": 20,
"x": 210,
"y": 245
},
{
"type": "TextButton",
"name": "names",
"text": "[%80] Names",
"width": 60,
"height": 20,
"x": 210,
"y": 245
},
{
"type": "TextButton",
"name": "zoomIn",
"text": "[%80]+",
"width": 20,
"height": 20,
"x": 5,
"y": 5
},
{
"type": "TextButton",
"name": "zoomOut",
"text": "[%80]-",
"width": 20,
"height": 20,
"x": 455,
"y": 5
}
]
}

View File

@@ -26,6 +26,60 @@
"height": 20,
"x": 205,
"y": 455
}
},
{
"type": "TextButton",
"name": "details",
"text": "[%80] Details",
"width": 60,
"height": 20,
"x": 105,
"y": 245
},
{
"type": "TextButton",
"name": "events",
"text": "[%80] Events",
"width": 60,
"height": 20,
"x": 105,
"y": 245
},
{
"type": "TextButton",
"name": "reputation",
"text": "[%80] Reputation",
"width": 60,
"height": 20,
"x": 105,
"y": 245
},
{
"type": "TextButton",
"name": "names",
"text": "[%80] Names",
"width": 60,
"height": 20,
"x": 105,
"y": 245
},
{
"type": "TextButton",
"name": "zoomIn",
"text": "[%80]+",
"width": 20,
"height": 20,
"x": 5,
"y": 5
},
{
"type": "TextButton",
"name": "zoomOut",
"text": "[%80]-",
"width": 20,
"height": 20,
"x": 245,
"y": 5
}
]
}

View File

@@ -141,3 +141,4 @@ Pioneer Masters, 3/6/PIO, PIO
Innistrad Remastered, 3/6/INR, INR
Aetherdrift, 3/6/DFT, DFT
Tarkir Dragonstorm, 3/6/TDM, TDM
Final Fantasy, 3/6/FIN, FIN

View File

@@ -5,8 +5,8 @@ PT:3/2
K:Flying
T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | TriggerZones$ Battlefield | Execute$ TrigMill | TriggerDescription$ At the beginning of your upkeep, mill a card. If an instant or sorcery card was milled this way, transform CARDNAME.
SVar:TrigMill:DB$ Mill | Defined$ You | RememberMilled$ True | SubAbility$ DBTransform
SVar:DBTransform:DB$ SetState | Defined$ Self | ConditionDefined$ Remembered | ConditionPresent$ Card.Instant,Card.Sorcery | SubAbility$ Cleanup | Mode$ Transform
SVar:Cleanup:DB$ Cleanup | ClearRemembered$ True
SVar:DBTransform:DB$ SetState | Defined$ Self | ConditionDefined$ Remembered | ConditionPresent$ Card.Instant,Card.Sorcery | SubAbility$ DBCleanup | Mode$ Transform
SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True
DeckHints:Ability$Delirium & Type$Instant|Sorcery
AlternateMode:DoubleFaced
Oracle:Flying\nAt the beginning of your upkeep, mill a card. If an instant or sorcery card was milled this way, transform Aberrant Researcher.

View File

@@ -5,4 +5,4 @@ A:SP$ Charm | Choices$ DBTokens,DBTap | CharmNum$ 1
SVar:DBTokens:DB$ Token | TokenScript$ c_1_1_hero | TokenOwner$ You | TokenAmount$ 3 | SpellDescription$ Take the Elevator — Create three 1/1 colorless Hero creature tokens.
SVar:DBTap:DB$ Tap | ValidTgts$ Creature | TargetMin$ 0 | TargetMax$ 3 | TgtPrompt$ Select up to three target creatures | SubAbility$ DBPutCounter | SpellDescription$ Take 59 Flights of Stairs — Tap up to three target creatures. Put a stun counter on one of them.
SVar:DBPutCounter:DB$ PutCounter | Choices$ Creature.targetedBy | CounterType$ Stun | CounterNum$ 1
Oracle:Choose one—\n• Take the Elevator — Create three 1/1 colorless Hero creature tokens.\n• Take 59 Flights of Stairs — Tap up to three target creatures. Put a stun counter on one of them. (If a permanent with a stun counter would become untapped, remove one from it instead.)
Oracle:Choose one —\n• Take the Elevator — Create three 1/1 colorless Hero creature tokens.\n• Take 59 Flights of Stairs — Tap up to three target creatures. Put a stun counter on one of them. (If a permanent with a stun counter would become untapped, remove one from it instead.)

View File

@@ -4,6 +4,6 @@ Types:Legendary Creature Spirit Soldier
PT:3/4
K:Vigilance
T:Mode$ BecomesTarget | ValidTarget$ Card.Self | ValidSource$ Ability.numTargets EQ1 | TriggerZones$ Battlefield | Execute$ TrigCopy | TriggerDescription$ Whenever CARDNAME becomes the target of an ability that targets only it, you may pay {1}{R/W}. If you do, copy that ability for each other creature you control that ability could target. Each copy targets a different one of those creatures. ({R/W} can be paid with either {R} or {W}.)
SVar:TrigCopy:AB$ CopySpellAbility | Cost$ 1 RW | Defined$ TriggeredStackInstance | CopyForEachCanTarget$ Creature.YouCtrl
SVar:TrigCopy:AB$ CopySpellAbility | Cost$ 1 RW | Defined$ TriggeredSpellAbility | CopyForEachCanTarget$ Creature.YouCtrl
AI:RemoveDeck:Random
Oracle:Vigilance\nWhenever Agrus Kos, Eternal Soldier becomes the target of an ability that targets only it, you may pay {1}{R/W}. If you do, copy that ability for each other creature you control that ability could target. Each copy targets a different one of those creatures. ({R/W} can be paid with either {R} or {W}.)

View File

@@ -9,5 +9,5 @@ SVar:Mana:AB$ Mana | Cost$ T Sac<1/CARDNAME/this artifact> | Produced$ Any | Amo
K:Class:3:4 R:AddTrigger$ TriggerExplosion
SVar:TriggerExplosion:Mode$ SpellCast | ValidCard$ Card | ValidActivatingPlayer$ You | TriggerZones$ Battlefield | ValidSA$ Spell.ManaFromTreasure | Execute$ TrigDamage | TriggerDescription$ Whenever you cast a spell, if mana from a Treasure was spent to cast it, this Class deals damage equal to that spell's mana value to each opponent.
SVar:TrigDamage:DB$ DealDamage | Defined$ Opponent | NumDmg$ X
SVar:X:TriggeredStackInstance$CardManaCostLKI
SVar:X:TriggeredSpellAbility$CardManaCostLKI
Oracle:(Gain the next level as a sorcery to add its ability.)\nWhen Alchemist's Talent enters, create two tapped Treasure tokens.\n{1}{R}: Level 2\nTreasures you control have "{T}, Sacrifice this artifact: Add two mana of any one color."\n{4}{R}: Level 3\nWhenever you cast a spell, if mana from a Treasure was spent to cast it, this Class deals damage equal to that spell's mana value to each opponent.

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