Compare commits

...

332 Commits

Author SHA1 Message Date
Blacksmith
eb3fb9e1ce [maven-release-plugin] prepare release forge-1.6.20 2019-02-02 02:00:28 +00:00
Blacksmith
b27f49c4ff Update README.txt for release 2019-02-02 01:58:00 +00:00
Sol
1c33e3b039 Merge branch 'master' into 'master'
Update rakdos_the_showstopper.txt using DestroyAll

See merge request core-developers/forge!1344
2019-02-02 01:35:24 +00:00
T.J. Tillman
eda0a49097 Update token image parameter for Domri, Chaos Bringer 2019-02-02 00:14:16 +00:00
T.J. Tillman
ea8c6630e0 Update cry_of_the_carnarium.txt 2019-02-01 23:31:09 +00:00
T.J. Tillman
0d4310382e Update rakdos_the_showstopper.txt using DestroyAll 2019-02-01 20:10:21 +00:00
Sol
4ae1547384 Merge branch 'patch-3' into 'master'
Update carnival_carnage.txt

See merge request core-developers/forge!1343
2019-02-01 19:11:38 +00:00
Sol
4192195579 Update carnival_carnage.txt 2019-02-01 19:11:15 +00:00
Sol
0d11e28c11 Merge branch 'master' into 'master'
Update Captive Audience to Functional plus a few others

See merge request core-developers/forge!1342
2019-02-01 19:10:41 +00:00
T.J. Tillman
9cf78d0eb4 Update deputy_of_detention.txt 2019-02-01 18:18:04 +00:00
T.J. Tillman
7843004c40 Fixed Ferocious condition 2019-02-01 18:12:25 +00:00
T.J. Tillman
4c7c2ee1b5 User reported Locust God's return trigger wasn't working on Resolution, but that it does work for Scarab God. Changed one word in Locust God's trigger to match Scarab God's 2019-02-01 17:20:23 +00:00
T.J. Tillman
05538bfba2 Update captive_audience.txt 2019-02-01 17:10:37 +00:00
Michael Kamensky
6fe6d6ecfd Merge branch 'master' into 'master'
A few more RNA card script updates

See merge request core-developers/forge!1339
2019-02-01 16:56:41 +00:00
Michael Kamensky
34a8184cc7 Merge branch 'patch-3' into 'master'
Light up the stage: Fix script with Commune with Lava

See merge request core-developers/forge!1341
2019-02-01 16:55:39 +00:00
Hans Mackowiak
5d9f86bf20 Light up the stage: Fix script with Commune with Lava 2019-02-01 16:34:43 +00:00
Michael Kamensky
661555551d Merge branch 'rnabuild' into 'master'
RNA LDA deck generation data

See merge request core-developers/forge!1340
2019-02-01 14:35:40 +00:00
austinio7116
02e118b2e2 RNA deckgen data
(cherry picked from commit eb7a6da)

(cherry picked from commit 32e02e7)
2019-02-01 08:02:02 +00:00
maustin
b59fbe43f0 Merge branch 'coremaster' into rnabuild 2019-02-01 08:01:34 +00:00
T.J. Tillman
ac51d4170f Update rumbling_ruin.txt 2019-02-01 07:37:01 +00:00
Sol
dc91df0e19 Merge branch 'patch-2' into 'master'
Update plaza_of_harmony.txt

See merge request core-developers/forge!1338
2019-02-01 04:20:07 +00:00
Sol
5bce464268 Update plaza_of_harmony.txt 2019-02-01 04:19:56 +00:00
T.J. Tillman
60d5afe9a2 Update eyes_everywhere.txt 2019-02-01 04:02:15 +00:00
T.J. Tillman
ef6e09d4c4 Update fireblade_artist.txt 2019-02-01 03:12:45 +00:00
Sol
76324bc6db Merge branch 'master' into 'master'
Fixed Rakdos, the Showstopper

See merge request core-developers/forge!1336
2019-02-01 01:08:25 +00:00
T.J. Tillman
bee8348b67 Update cosmotronic_wave.txt to reflect ruling that this is a continuous effect. 2019-01-31 22:12:14 +00:00
T.J. Tillman
51fb900eb5 Revert "Update rumbling_ruin.txt"
This reverts commit 8b902671d6
2019-01-31 22:06:31 +00:00
T.J. Tillman
8b902671d6 Update rumbling_ruin.txt 2019-01-31 22:02:53 +00:00
T.J. Tillman
7dbbf0a554 Update rakdos_the_showstopper.txt 2019-01-31 19:22:12 +00:00
T.J. Tillman
e75b15b7cb Update rakdos_the_showstopper.txt 2019-01-31 19:20:09 +00:00
Michael Kamensky
43cca9b635 Merge branch 'master' into 'master'
Check for alternative additional costs in the AI routines.

Closes #813 and #820

See merge request core-developers/forge!1334
2019-01-31 13:43:48 +00:00
Agetian
b29f390b62 - Fix the AI activating player setup for additional costs.
- Fix Thrilling Encore.
2019-01-31 16:39:49 +03:00
Michael Kamensky
f83a97bedd Merge branch '819-spectra-ward-broken' into 'master'
Resolve "Spectra Ward broken"

Closes #819

See merge request core-developers/forge!1335
2019-01-31 13:24:19 +00:00
Agetian
c58690dd96 Merge branch 'master' of git.cardforge.org:core-developers/forge into agetian-master 2019-01-31 09:34:27 +03:00
Hans Mackowiak
3402cddd1d GameAction: fix Spectral Ward #819 2019-01-31 06:25:06 +00:00
Agetian
3956a5b558 - Check for alternative additional costs in the AI routines (fixes AI not paying those costs for Final Payment and other cards). 2019-01-31 08:45:53 +03:00
Sol
72b60c91f5 Merge branch 'master' into 'master'
Fix Deputy of Detention

See merge request core-developers/forge!1333
2019-01-31 04:28:00 +00:00
T.J. Tillman
d063e98e33 Update deputy_of_detention.txt 2019-01-31 04:11:09 +00:00
Sol
8743cba5d7 Merge branch 'rna-rankings' into 'master'
Add RNA rankings

See merge request core-developers/forge!1332
2019-01-31 01:25:37 +00:00
Chris H
1d2bbe4923 Add RNA rankings 2019-01-30 20:24:30 -05:00
Michael Kamensky
58311e5265 Merge branch 'fix-gurzigost' into 'master'
Fix Gurzigost.

Closes #817

See merge request core-developers/forge!1331
2019-01-30 16:04:16 +00:00
Agetian
8c020c7c8c - Generic check. 2019-01-30 18:54:15 +03:00
Agetian
ba25696ea7 - Fix Gurzigost. 2019-01-30 18:30:07 +03:00
Michael Kamensky
68359097df Merge branch 'patch-2' into 'master'
CardState: this catch the Exception in case it crash in Keyword.initialize

See merge request core-developers/forge!1330
2019-01-30 15:17:47 +00:00
Hans Mackowiak
4b882ad215 CardState: this catch the Exception in case it crash in Keyword.initialize 2019-01-30 10:22:43 +00:00
Michael Kamensky
ee6806b36a Merge branch 'patch-1' into 'master'
Send SpellAbility into getDefinedPlayers of greatestPower

See merge request core-developers/forge!1329
2019-01-30 04:48:01 +00:00
Sol
343723e529 Send SpellAbility into getDefinedPlayers of greatestPower 2019-01-30 01:34:05 +00:00
Sol
15bf426386 Merge branch 'master' into 'master'
Couple RNA fixes

See merge request core-developers/forge!1328
2019-01-30 01:19:19 +00:00
T.J. Tillman
77184dc930 Update consecrate_consume.txt 2019-01-30 00:13:01 +00:00
T.J. Tillman
5f11897d11 Update consecrate_consume.txt 2019-01-29 23:43:22 +00:00
T.J. Tillman
40764691fd Update code_of_constraint.txt - Doesn't untap only true for Addendum case 2019-01-29 23:38:51 +00:00
T.J. Tillman
210cc6164d Update consecrate_consume.txt 2019-01-29 22:45:38 +00:00
T.J. Tillman
11f597bb23 Fix Consume so that target player will sacrifice its greatest power creature. Not very clean, but used a workaround mimicking Crackling Doom to get it to work. 2019-01-29 22:43:12 +00:00
T.J. Tillman
015623a122 Fix Dovin, Grand Arbiter's +1 ability 2019-01-29 19:57:56 +00:00
T.J. Tillman
b04e16d1ca Update mirror_march.txt 2019-01-29 17:50:02 +00:00
Michael Kamensky
e3f24dccd4 Merge branch 'master' into 'master'
Added RNA achievements (by Marek14).

See merge request core-developers/forge!1327
2019-01-29 14:32:42 +00:00
Agetian
2e0812dd6a - Added RNA achievements (by Marek14). 2019-01-29 17:31:50 +03:00
T.J. Tillman
9deeec7b14 Merge branch 'fixDeputyOfDetention' into 'master'
Fix Deputy of Detention

See merge request tjtillmancoag/forge!3
2019-01-29 08:57:28 +00:00
T.J. Tillman
b58b2b93e2 Fix Deputy of Detention (now exiles other cards of the same name, and also now returns those cards to the BF when it leaves the BF) 2019-01-29 07:37:06 +00:00
Michael Kamensky
2697f5f404 Merge branch 'master' into 'master'
Fixed Sentinel's Mark Lifelink trigger

See merge request core-developers/forge!1325
2019-01-29 05:46:50 +00:00
T.J. Tillman
78db487e8d Merge branch 'sentinelsMarkTrigger' into 'master'
Fix lifelink trigger on Sentinel's Mark

See merge request tjtillmancoag/forge!2
2019-01-29 05:32:53 +00:00
T.J. Tillman
1ecd1a8340 Fix lifelink trigger on Sentinel's Mark 2019-01-29 05:31:58 +00:00
Michael Kamensky
c7539de77d Merge branch 'master' into 'master'
RNA Planeswalker Deck Precons

See merge request core-developers/forge!1324
2019-01-29 05:12:28 +00:00
Agetian
b0ee2cedff - Added RNA planeswalker decks to quest mode precons. 2019-01-29 08:11:50 +03:00
Sol
7a0453390a Merge branch 'migrate-upcoming' into 'master'
Migrate upcoming

See merge request core-developers/forge!1323
2019-01-29 04:13:50 +00:00
Chris H
1b47a2df87 Migrate RNA to alpha folders 2019-01-28 22:58:33 -05:00
Chris H
fd54606277 Merge branch 'master' of https://git.cardforge.org/core-developers/forge 2019-01-28 22:44:44 -05:00
Chris H
25b02141bd Merge branch 'master' of https://git.cardforge.org/core-developers/forge 2019-01-28 22:43:07 -05:00
Sol
bcc4158956 Merge branch 'someRNAFixes' into 'master'
Fix Spectacle cost for Blade Juggler

See merge request core-developers/forge!1322
2019-01-29 03:42:37 +00:00
T.J. Tillman
fc5b76fbb4 Fix cast cost of Colossus 2019-01-29 03:41:59 +00:00
T.J. Tillman
38ddca0acd Fix Spectacle cost 2019-01-29 03:37:10 +00:00
Sol
7ed935c43c Merge branch 'someRNAFixes' into 'master'
Fix Sphinx of Foresight opening scry timing

See merge request core-developers/forge!1321
2019-01-29 03:32:52 +00:00
T.J. Tillman
14629715a2 Fix cost of spirit-making ability on Ethereal Absolution 2019-01-29 03:22:44 +00:00
T.J. Tillman
e71ff26b81 Update forge-gui/res/cardsfolder/upcoming/sphinx_of_foresight.txt 2019-01-29 00:35:26 +00:00
Sol
7d6dec773a Merge branch 'someRNAFixes' into 'master'
New Cards: RNA Domri planeswalkers

See merge request core-developers/forge!1320
2019-01-28 23:35:33 +00:00
T.J. Tillman
e52bcc6cdc Update forge-gui/res/cardsfolder/upcoming/dovins_automaton.txt 2019-01-28 22:26:02 +00:00
T.J. Tillman
b3e1d96aff Upload New File 2019-01-28 22:06:12 +00:00
T.J. Tillman
e13880c87e Domri, City Smasher 2019-01-28 20:18:07 +00:00
Chris H
0b14efebb8 Merge branch 'master' of https://git.cardforge.org/core-developers/forge 2019-01-28 15:15:06 -05:00
Sol
18a3bfe0b8 Merge branch 'someRNAFixes' into 'master'
A Few RNA card script fixes

See merge request core-developers/forge!1316
2019-01-28 20:08:56 +00:00
T.J. Tillman
d8da15d8ae Update forge-gui/res/cardsfolder/upcoming/dovin_architect_of_law.txt 2019-01-28 19:58:23 +00:00
T.J. Tillman
5bd9f324e0 Update forge-gui/res/cardsfolder/upcoming/dovins_dismissal.txt 2019-01-28 18:09:35 +00:00
T.J. Tillman
e0b463c6ea Update forge-gui/res/cardsfolder/upcoming/cindervines.txt 2019-01-28 18:03:00 +00:00
Michael Kamensky
01742f06f6 Merge branch 'highlight' into 'master'
Darken non-selectable cards in Desktop GUI

See merge request core-developers/forge!1317
2019-01-28 17:55:36 +00:00
T.J. Tillman
dbe212065d Update forge-gui/res/cardsfolder/r/raid_bombardment.txt 2019-01-28 17:44:21 +00:00
T.J. Tillman
4b8584376d Update forge-gui/res/cardsfolder/upcoming/cavalcade_of_calamity.txt 2019-01-28 17:27:48 +00:00
Michael Kamensky
6cf7d3ae80 Merge branch 'master' into 'master'
Added puzzle PS_RNA0a (RNA Prerelease Puzzle)

See merge request core-developers/forge!1319
2019-01-28 12:22:42 +00:00
Agetian
104aaa1255 - Added puzzle PS_RNA0a (RNA Prerelease Puzzle) 2019-01-28 15:22:06 +03:00
Peter F. Patel-Schneider
1ba1226661 refresh hand floating zone when hand is updated 2019-01-28 05:53:38 -05:00
T.J. Tillman
f8bf8c7f28 Update forge-gui/res/cardsfolder/upcoming/cindervines.txt 2019-01-28 08:41:26 +00:00
T.J. Tillman
763cb3d08e Update forge-gui/res/cardsfolder/upcoming/shimmer_of_possibility.txt 2019-01-28 08:30:23 +00:00
T.J. Tillman
5e94c6c8d3 Update forge-gui/res/cardsfolder/upcoming/forbidding_spirit.txt 2019-01-28 08:27:47 +00:00
T.J. Tillman
b08ce49057 Update forge-gui/res/cardsfolder/upcoming/senate_griffin.txt, forge-gui/res/cardsfolder/upcoming/haazda_officer.txt, forge-gui/res/cardsfolder/upcoming/forbidding_spirit.txt, forge-gui/res/cardsfolder/upcoming/domris_nodorog.txt files 2019-01-28 08:07:43 +00:00
T.J. Tillman
7c98ed75be Update forge-gui/res/cardsfolder/upcoming/domris_nodorog.txt, forge-gui/res/cardsfolder/upcoming/forbidding_spirit.txt, forge-gui/res/cardsfolder/upcoming/haazda_officer.txt, forge-gui/res/cardsfolder/upcoming/senate_griffin.txt files 2019-01-28 08:02:07 +00:00
T.J. Tillman
1c9a515bb3 Update forge-gui/res/cardsfolder/upcoming/domris_nodorog.txt 2019-01-28 07:47:55 +00:00
T.J. Tillman
a462d34249 Update forge-gui/res/cardsfolder/upcoming/charging_war_boar.txt, forge-gui/res/cardsfolder/upcoming/cindervines.txt files 2019-01-28 07:38:32 +00:00
T.J. Tillman
e1ceed159f Update forge-gui/res/cardsfolder/upcoming/elite_arrester.txt, forge-gui/res/cardsfolder/upcoming/clamor_shaman.txt files 2019-01-28 07:08:05 +00:00
T.J. Tillman
89757b9677 Update forge-gui/res/cardsfolder/upcoming/footlight_fiend.txt 2019-01-28 07:04:13 +00:00
T.J. Tillman
01c52667ad Update forge-gui/res/cardsfolder/upcoming/cavalcade_of_calamity.txt, forge-gui/res/cardsfolder/r/raid_bombardment.txt, forge-gui/res/cardsfolder/upcoming/forbidding_spirit.txt, forge-gui/res/cardsfolder/upcoming/get_the_point.txt, forge-gui/res/cardsfolder/upcoming/haazda_officer.txt, forge-gui/res/cardsfolder/upcoming/lawmages_binding.txt, forge-gui/res/cardsfolder/upcoming/rubble_slinger.txt, forge-gui/res/cardsfolder/upcoming/rubblebelt_runner.txt, forge-gui/res/cardsfolder/upcoming/screaming_shield.txt, forge-gui/res/cardsfolder/upcoming/scuttlegator.txt, forge-gui/res/cardsfolder/upcoming/senate_griffin.txt, forge-gui/res/cardsfolder/upcoming/summary_judgment.txt, forge-gui/res/cardsfolder/upcoming/vizkopa_vampire.txt files 2019-01-28 07:01:43 +00:00
Peter F. Patel-Schneider
25003aa74e better way to schedule visual updates to non-selectable cards 2019-01-27 21:09:52 -05:00
T.J. Tillman
766277673d Update simic_locket.txt 2019-01-27 23:45:21 +00:00
T.J. Tillman
a1ad2208c8 Update rix_maadi_reveler.txt 2019-01-27 23:01:06 +00:00
T.J. Tillman
0bfdfb78d3 Update ill_gotten_inheritance.txt 2019-01-27 22:43:34 +00:00
T.J. Tillman
b23425ea9a Update bloodmist_infiltrator.txt 2019-01-27 22:37:18 +00:00
T.J. Tillman
8b7ace6d1c Added TrigChangeZone to bounce it back, and CheckSVar to check if instant cast in Main Phase 2019-01-27 21:45:52 +00:00
T.J. Tillman
1dc8ef5244 Fixed problem with self-exiling after trigger resolution (Hanmac) 2019-01-27 20:34:35 +00:00
T.J. Tillman
625585f005 Fixed Scry 3 2019-01-27 20:15:05 +00:00
T.J. Tillman
acdee9ff0c Update fireblade_artist.txt 2019-01-27 19:43:56 +00:00
T.J. Tillman
54c5d3f344 Fix Fireblade Artist to reflect "when you" sacrifice trigger (similar to Heart-Piercer Manticore) 2019-01-27 19:25:20 +00:00
T.J. Tillman
4fa8fa3993 Update sentinels_mark.txt 2019-01-27 18:04:06 +00:00
T.J. Tillman
3b21b2a610 Fix fireblade_artist dealing damage on sacrifice 2019-01-27 17:58:28 +00:00
T.J. Tillman
07324f63d9 Update spawn_of_mayhem.txt 2019-01-27 17:56:47 +00:00
T.J. Tillman
79c3570bcc Update gutterbones.txt 2019-01-27 17:55:48 +00:00
T.J. Tillman
d9400fe069 Update rix_maadi_reveler.txt 2019-01-27 17:55:04 +00:00
Peter F. Patel-Schneider
70f0f1108b darken non-selectable cards during paintChildren 2019-01-27 08:22:50 -05:00
Peter F. Patel-Schneider
62316abf38 darken non-selectable cards when selecting (needs work) 2019-01-27 07:16:54 -05:00
Peter F. Patel-Schneider
e2ddcdda4d stronger highlighting for selectable cards 2019-01-27 07:16:54 -05:00
T.J. Tillman
e604d9d0cb Update consecrate_consume.txt to add Cleanup Remembered 2019-01-27 05:18:49 +00:00
tjtillmancoag
ed218b6e4c Fixed Amplifire (properly changes Power & Toughness),
Consume (of Consecrate // Consume, works now),
Final Payment (allows sacrifice of enchantment/creature as payment),
Incongruity (fixed A:AB$ to A:SP$),
Sentinel's Mark (gives Lifelink on Main phase trigger)
Silhana Wayfinder (triggers on ETB)
Smelt Ward Ignus (loses control of creature at EoT)
Undercity's Embrace (no life amount in gainlife ability was causing crash on cast)
2019-01-26 19:39:04 -08:00
Michael Kamensky
866c959036 Merge branch 'spellControllerFix' into 'master'
Spell: fix casting opponent spells

Closes #811

See merge request core-developers/forge!1315
2019-01-26 14:31:12 +00:00
Hanmac
b2616860c0 Spell: fix casting opponent spells 2019-01-26 13:28:55 +01:00
Hans Mackowiak
0ceb882ab0 Merge branch 'charmRework' into 'master'
Charm rework

Closes #806

See merge request core-developers/forge!1312
2019-01-26 09:24:25 +00:00
Hans Mackowiak
560dc572c2 Charm rework 2019-01-26 09:24:25 +00:00
Michael Kamensky
27bae4a72b Merge branch 'fix-rna-sealed' into 'master'
Fix up the RNA printsheets a little bit (wrong card names, wrong set IDs)

See merge request core-developers/forge!1313
2019-01-25 15:02:16 +00:00
Agetian
6686937742 - Fix up the RNA printsheets a little bit (wrong card names, wrong set IDs) 2019-01-25 18:01:24 +03:00
Michael Kamensky
d6a466df11 Merge branch 'fix-rna-sealed' into 'master'
Fix AI sealed booster generation for Ravnica Allegiance

Closes #801

See merge request core-developers/forge!1311
2019-01-25 14:27:08 +00:00
Agetian
7eedd04aa1 - Add basic lands definitions to Ravnica Allegiance.
- Revert blocks.txt to use RNA for the land set.
2019-01-25 17:21:21 +03:00
Agetian
e3c9a40a17 - Use M19 lands as the default basic lands for GRN and RNA sealed/draft, since neither set has basic lands of its own in actual booster packs. 2019-01-25 17:17:40 +03:00
Agetian
76ebfa9460 - Fix AI sealed booster generation for Ravnica Allegiance (use GRN basic lands). 2019-01-25 17:04:09 +03:00
Michael Kamensky
e0fd812590 Merge branch 'patch-teysa' into 'master'
TriggerHandler: Only real die can look back

Closes #797

See merge request core-developers/forge!1298
2019-01-25 04:55:39 +00:00
Michael Kamensky
2c2104dfe6 Merge branch 'deckimport' into 'master'
DeckRecognizer: Recognize split card names with single slash

See merge request core-developers/forge!1304
2019-01-25 04:53:36 +00:00
Evan Murawski
4661a19982 DeckRecognizer: Recognize split card names with single slash 2019-01-25 04:53:36 +00:00
Hans Mackowiak
9778f9e3a0 Teysa and Gitrog fixes 2019-01-24 21:26:17 +01:00
Hanmac
b6019c6308 GameSimulatorTest: second test for Teysa 2019-01-24 07:45:36 +01:00
Hans Mackowiak
7006c548a5 GameSimulatorTest: Teysa Karlov and Xathrid Necromancer 2019-01-24 07:45:36 +01:00
Hans Mackowiak
2eae26cefb TriggerHandler: Only real die can look back
Rework Panharmonicon
2019-01-24 07:43:14 +01:00
Michael Kamensky
ccd1117762 Merge branch 'riotDouble' into 'master'
Riot: Make Riot Enchantment look into the future

See merge request core-developers/forge!1303
2019-01-24 06:42:11 +00:00
Hanmac
1917871b98 GameSimulatorTest: assert check for DoubleRiot 2019-01-24 07:28:14 +01:00
Hanmac
96e6111400 Riot: Make Riot Enchantment look into the future 2019-01-24 07:27:22 +01:00
Michael Kamensky
f8d360f25f Merge branch 'master' into 'master'
Restore AI SVars for a couple cards.

See merge request core-developers/forge!1310
2019-01-24 05:38:38 +00:00
Agetian
5901c6b66b - Restore AI SVars. 2019-01-24 08:38:11 +03:00
Michael Kamensky
b9782f9790 Merge branch 'master' into 'master'
Integrate Schnautzr's token updates and fix merge conflicts.

See merge request core-developers/forge!1309
2019-01-24 05:37:17 +00:00
Agetian
74c69f0d37 - Integrate Schnautzr's token updates and fix merge conflicts. 2019-01-24 08:36:35 +03:00
Michael Kamensky
5a81cdc40d Merge branch 'master' into 'master'
Added more AI SVars for RNA, fixed Dross Scorpion trigger description. Labeled some buggy RNA scripts.

See merge request core-developers/forge!1308
2019-01-24 05:28:34 +00:00
Agetian
7ebe97f802 - Added TODO labels. 2019-01-24 08:27:54 +03:00
Agetian
956ab8e96e - Added more AI SVars for RNA.
- Added some comments for the needed script fixes.
- Fixed Dross Scorpion trigger description.
2019-01-24 08:08:52 +03:00
Rob Schnautz
b821ec7af5 Add token scripts and download URLs. 2019-01-24 04:57:54 +00:00
Chris H
6a92f68602 Merge branch 'master' of https://git.cardforge.org/core-developers/forge 2019-01-23 22:23:47 -05:00
Sol
d0c5bcf053 Merge branch 'patch-2' into 'master'
Update Ravnica Allegiance.txt

See merge request core-developers/forge!1306
2019-01-24 02:55:00 +00:00
Sol
4069b603c2 Update Ravnica Allegiance.txt 2019-01-24 02:54:49 +00:00
Sol
0813f9bc66 Merge branch 'more-rna' into 'master'
More rna

See merge request core-developers/forge!1305
2019-01-24 02:52:52 +00:00
Sol
0687fc2a31 More rna 2019-01-24 02:52:52 +00:00
Chris H
3f52a8c4f5 Merge remote-tracking branch 'origin/master' 2019-01-23 21:10:33 -05:00
Sol
82e276c661 Merge branch 'more-rna' into 'master'
RNA Templates

See merge request core-developers/forge!1291
2019-01-24 01:48:54 +00:00
Michael Kamensky
195fe58dcb Merge branch 'master' into 'master'
fix bug in min number of cards to select in digs

See merge request core-developers/forge!1302
2019-01-23 09:38:00 +00:00
Peter F. Patel-Schneider
afd1070850 fix bug in min number of cards to select in digs 2019-01-23 03:42:45 -05:00
Chris H
8580d585df Fix Mirror March 2019-01-22 21:42:46 -05:00
maustin
090d6ad9fb Merge branch 'more-rna' of https://git.cardforge.org/friarsol/forge into rnabuild 2019-01-22 06:47:12 +00:00
Michael Kamensky
8ed1ad4afc Merge branch 'master' into 'master'
remember old location and size of pop-up card list window

See merge request core-developers/forge!1300
2019-01-22 04:18:38 +00:00
Chris H
bc854f4d42 RNA Blockdata 2019-01-21 22:23:45 -05:00
Chris H
d8585fd925 Kaya 2019-01-21 22:23:15 -05:00
Chris H
5ae5fef43e Fix more splits 2019-01-21 21:54:51 -05:00
Chris H
48a0ac6039 missing split for Depose//Deploy 2019-01-21 21:53:26 -05:00
Chris H
57a3462623 Cleanup weird Light up the Stage 2019-01-21 21:18:22 -05:00
Chris H
929b5fbcfe Fix some more broken cards 2019-01-21 21:08:36 -05:00
Chris H
56fda56807 Fixes 2019-01-21 21:08:35 -05:00
Chris H
872defd992 Fix Bedazzle 2019-01-21 21:08:30 -05:00
Chris H
e78202e432 Incubation // Incongruity 2019-01-21 21:08:28 -05:00
Chris H
657d85530a Repudiate//replicate 2019-01-21 21:08:27 -05:00
Chris H
a6e0f9b472 Warrant/Warden 2019-01-21 21:08:25 -05:00
Chris H
86abb0d1d5 Fixing more broken RNA cards 2019-01-21 21:08:24 -05:00
Chris H
28a5bd109e Move broken to upcoming 2019-01-21 21:08:22 -05:00
Chris H
b357096da2 Depost deploy 2019-01-21 21:08:21 -05:00
Chris H
d1d1df27bc Consecrate Consume 2019-01-21 21:08:20 -05:00
Chris H
d7ebafe883 Collision Colossus 2019-01-21 21:08:19 -05:00
Chris H
f09130eb86 Carnival Carnage 2019-01-21 21:08:18 -05:00
Chris H
50e6a06478 Fix split cards 2019-01-21 21:08:17 -05:00
Chris H
65fc4a4371 RNA Templates 2019-01-21 21:08:16 -05:00
Peter F. Patel-Schneider
67ca319674 remove debugging print in manipulateCardList 2019-01-21 18:10:30 -05:00
Peter F. Patel-Schneider
1f4142e368 remember old location and size of pop-up card list window 2019-01-21 18:05:19 -05:00
Michael Kamensky
b8147a2e58 Merge branch 'master' into 'master'
use multi-select for dig

Closes #799

See merge request core-developers/forge!1296
2019-01-21 14:47:08 +00:00
Michael Kamensky
2b8a756932 Merge branch 'schnautzr-master-patch-51486' into 'master'
Collector Number Sort update for RNA

See merge request core-developers/forge!1297
2019-01-21 05:06:20 +00:00
Rob Schnautz
f78bfce802 RNA looks at artifact casting cost only 2019-01-21 03:31:16 +00:00
Peter F. Patel-Schneider
ebcb4e28de Add desktop GUI and Dig changes to CHANGES.txt 2019-01-20 21:23:16 -05:00
Peter F. Patel-Schneider
f0c45cf814 Add visual chooser for two lists and use it for and/or dig 2019-01-20 21:23:16 -05:00
Peter F. Patel-Schneider
aff8d5ce01 Use chooseEntitiesForEffect for dig (except and/or dig) 2019-01-20 21:23:16 -05:00
Peter F. Patel-Schneider
94064a2a13 Fix bug in chooseEntitiesforEffect in Mobile GUI 2019-01-20 21:16:34 -05:00
Sol
6359021370 Merge branch 'master' into 'master'
Fix for Invalid Deck or Proxy File Names

Closes #560

See merge request core-developers/forge!1279
2019-01-21 00:55:35 +00:00
Evan Murawski
6090bc8117 Fix for Invalid Deck or Proxy File Names 2019-01-21 00:55:35 +00:00
Peter F. Patel-Schneider
264744645e Merge branch 'highlighting' 2019-01-20 16:41:50 -05:00
Michael Kamensky
ab1e1d2386 Merge branch 'master' into 'master'
put a cyan border on valid cards when selecting targets and costs (desktop UI only)

See merge request core-developers/forge!1281
2019-01-20 19:27:11 +00:00
Hans Mackowiak
87ad257c46 Merge branch 'damageMapClear' into 'master'
DamageMap: add clear after being triggered

See merge request core-developers/forge!1290
2019-01-20 09:12:04 +00:00
Sol
bc5e7f07ae Merge branch 'gift-pack-edition' into 'master'
M19 Gift Pack edition file

See merge request core-developers/forge!1293
2019-01-20 02:59:39 +00:00
Chris H
f18651be7a M19 Gift Pack edition file 2019-01-19 21:02:24 -05:00
Chris H
9921f58155 M19 Gift Pack edition file 2019-01-19 20:58:35 -05:00
Sol
3ade967f85 Merge branch 'patch-1' into 'master'
Update skitter_eel.txt

See merge request core-developers/forge!1292
2019-01-20 01:44:01 +00:00
Sol
243c90ced9 Update skitter_eel.txt 2019-01-20 01:43:46 +00:00
Hanmac
c1c421fff0 DamageMap: add clear after being triggered 2019-01-19 18:11:55 +01:00
Michael Kamensky
bf6f8048d6 Merge branch 'spellCostReduceTarget' into 'master'
CostAdjustment: use Relative for Amount that depends on the SpellAbility

See merge request core-developers/forge!1289
2019-01-19 12:52:12 +00:00
Hanmac
f26935f37f CostAdjustment: use Relative for Amount that depends on the SpellAbility, only works for Self 2019-01-19 13:27:19 +01:00
Michael Kamensky
f3f7700ba9 Merge branch 'patch-1' into 'master'
Update sharktocrab.txt

See merge request core-developers/forge!1288
2019-01-19 10:41:41 +00:00
Hans Mackowiak
2817c4ef61 Update sharktocrab.txt 2019-01-19 10:28:33 +00:00
Michael Kamensky
166700b573 Merge branch 'master' into 'master'
Added puzzle PS_RNA0 (Ravnica Allegiance Card Preview Puzzle).

See merge request core-developers/forge!1287
2019-01-19 10:14:20 +00:00
Agetian
6edb5ad591 - Added puzzle PS_RNA0 (Ravnica Allegiance Card Preview Puzzle). 2019-01-19 13:13:41 +03:00
Hans Mackowiak
66f2ab4e91 Merge branch 'patch-1' into 'master'
Update sharktocrab

See merge request core-developers/forge!1286
2019-01-19 10:11:43 +00:00
Hans Mackowiak
1f211e6ed5 Update sharktocrab 2019-01-19 10:11:15 +00:00
Michael Kamensky
41093c2f7f Merge branch 'ai-hints-rna' into 'master'
First pass of AI hints for RNA.

See merge request core-developers/forge!1284
2019-01-19 10:02:13 +00:00
Agetian
f4df89dca8 Merge remote-tracking branch 'origin/ai-hints-rna' into ai-hints-rna
# Conflicts:
#	forge-gui/res/cardsfolder/upcoming/ethereal_absolution.txt
#	forge-gui/res/cardsfolder/upcoming/watchful_giant.txt
2019-01-19 10:27:27 +03:00
Agetian
ffabf14192 - AI hints for RNA. 2019-01-19 10:25:43 +03:00
Michael Kamensky
5ce9db0b08 Merge branch 'upcomingFix20190119' into 'master'
Fix for upcoming cards

Closes #798

See merge request core-developers/forge!1285
2019-01-19 07:23:14 +00:00
Hanmac
5165c16233 Fix for upcoming cards 2019-01-19 07:57:08 +01:00
Agetian
1e34fd27fe - AI hints for RNA. 2019-01-19 09:34:58 +03:00
Sol
12b464dbcb Merge branch 'Austin/forge-ravnicaallegiance' into 'master'
Austin/forge ravnicaallegiance

See merge request core-developers/forge!1275
2019-01-19 02:53:56 +00:00
Chris H
76e3f0305b Do Type changes affect Svar keys? That sounds bad 2019-01-18 21:29:24 -05:00
Chris H
bb57003518 Biogenic Ooze -> TokenScripts 2019-01-18 21:26:47 -05:00
Chris H
dfbfcf0a11 Hero of Precinct One -> TokenScripts 2019-01-18 21:24:29 -05:00
Chris H
c4c061d669 Fix Rampage of the Clans 2019-01-18 21:23:05 -05:00
Chris H
e76db8e7ff Fix Gruul Spellbreaker affected 2019-01-18 21:15:23 -05:00
Chris H
9c4fd8b570 Fix Spectacle 2019-01-18 21:02:36 -05:00
Chris H
5d2e7a16d3 Fix Afterlife crashes 2019-01-18 21:02:36 -05:00
Chris H
cf3a28b120 Remove bogus card 2019-01-18 21:02:35 -05:00
austinio7116
10f8616e37 First pass of remaining spoilers, commiting and fixing up those that forgescribe got close 2019-01-18 21:02:32 -05:00
maustin
1bade895e1 Complete Ravnica Allegiance editions file 2019-01-18 21:00:53 -05:00
austinio7116
1940bca932 More straightforward RNA cards 2019-01-18 21:00:46 -05:00
austinio7116
620c27a117 Attempts to fix comments in MR - correct RemAI, fix combine guildname move counters and add X Svar to electrodominance 2019-01-18 21:00:43 -05:00
austinio7116
3071f1f74b First batch of easy forgescribe cleanups with editions file 2019-01-18 21:00:42 -05:00
Michael Kamensky
0f44b644c1 Merge branch 'bugfix' into 'master'
fix crash when selecting players

See merge request core-developers/forge!1282
2019-01-18 05:08:51 +00:00
Peter F. Patel-Schneider
427b2973b8 Merge branch 'bugfix' 2019-01-17 16:09:50 -05:00
Peter F. Patel-Schneider
25d1d2bb20 Merge branch 'bugfix' into highlighting
Bring bug fix into highlight code
2019-01-17 16:07:49 -05:00
Peter F. Patel-Schneider
8f2123f183 add fix to crash when selecting targets 2019-01-17 16:02:22 -05:00
Peter F. Patel-Schneider
595fbae34f Fix crash when selecting a player 2019-01-17 15:37:59 -05:00
Peter F. Patel-Schneider
3fd842c1e4 Merge branch 'master' of https://git.cardforge.org/core-developers/forge
Bactrack to upstream master to fix bug
2019-01-17 15:30:09 -05:00
Peter F. Patel-Schneider
11f9520c1e Highlight sa targets in Desktop GUI 2019-01-17 09:35:52 -05:00
Michael Kamensky
40c64a943a Merge branch 'patch-1' into 'master'
FIX Protean Raider

See merge request core-developers/forge!1280
2019-01-17 06:01:13 +00:00
Hans Mackowiak
4f953bf2e1 FIX Protean Raider 2019-01-17 05:42:10 +00:00
Peter F. Patel-Schneider
c7d9646f7f adjust highlighting for new manipulation interface 2019-01-16 13:10:56 -05:00
Peter F. Patel-Schneider
d4b4dc5ba0 Remove debugging print 2019-01-16 12:09:13 -05:00
Peter F. Patel-Schneider
2cd86bfe1e put yellow border on selectable cards 2019-01-16 12:09:13 -05:00
Peter F. Patel-Schneider
2f9fb96d29 use Iterators instead of Lists where possible in manipulateCardList 2019-01-16 12:06:21 -05:00
Michael Kamensky
39204513fb Merge branch 'master' into 'master'
Added puzzle PS_CFB (Possibility Storm: How Much Fireball Can You Channel).

See merge request core-developers/forge!1278
2019-01-16 07:17:02 +00:00
Agetian
ebaa96004f - Added puzzle PS_CFB (How Much Fireball Can You Channel). 2019-01-16 10:16:20 +03:00
Michael Kamensky
d381251472 Merge branch 'master' into 'master'
code cleanup for moveable card displays; add movement by clicking to display-based scry

See merge request core-developers/forge!1276
2019-01-16 07:07:16 +00:00
Michael Kamensky
f00e759a93 Merge branch 'removeCountersAnyNumber' into 'master'
CountersRemoveEffect: add ValidSource and CounterNum$ Any

See merge request core-developers/forge!1277
2019-01-16 07:05:06 +00:00
Hanmac
aab63e2029 CountersRemoveEffect: add ValidSource and CounterNum$ Any 2019-01-16 07:30:24 +01:00
Peter F. Patel-Schneider
3749f2a5de fix bug in computing manipulable card views 2019-01-15 22:10:05 -05:00
Peter F. Patel-Schneider
aa7ad578d5 change manipulateCardList to CardView 2019-01-15 06:24:11 -05:00
Peter F. Patel-Schneider
fb80dece04 ListCardArea augment clicking 2019-01-15 06:24:11 -05:00
Peter F. Patel-Schneider
482ab4b87c Add left-click to top and right-click to bottom for ListCardArea 2019-01-15 06:24:11 -05:00
Peter F. Patel-Schneider
f3f9a915d5 Base FloatingZone on FloatingCardArea 2019-01-15 06:24:11 -05:00
Peter F. Patel-Schneider
52184e24ce Add intermediate FloatingCardArea to hold commonalities between FloatingZone and ListCardArea 2019-01-15 06:24:11 -05:00
Michael Kamensky
b6dbfcee96 Merge branch 'master' into 'master'
fixes for multi-player scry; fix bug when human player scrying entire library

See merge request core-developers/forge!1271
2019-01-15 10:59:30 +00:00
Peter F. Patel-Schneider
e07be68786 Remove unused import in GameAction 2019-01-14 07:40:50 -05:00
Peter Patel-Schneider
9c3ae840b8 Merge branch 'pfps_master' into 'master'
GameAction: add PlayerController:confirmMulliganScry

See merge request pfps/forge!1
2019-01-14 01:43:38 +00:00
Hanmac
20eba0fbd7 GameAction: add PlayerController:confirmMulliganScry 2019-01-13 22:01:22 +01:00
Peter F. Patel-Schneider
735516e6d5 Fix bug in scry when scrying entire library 2019-01-13 10:25:35 -05:00
Peter F. Patel-Schneider
087495f5a0 Force AI to accept mulligan scry (otherwise there is a crash) 2019-01-13 10:24:40 -05:00
Peter F. Patel-Schneider
b94ec24948 Changes to do parts of multi-player scry in correct order 2019-01-13 10:08:29 -05:00
Michael Kamensky
e3257e025d Merge branch 'sunburst' into 'master'
Sunburst: rewrite using ETB counter

See merge request core-developers/forge!1269
2019-01-13 14:10:44 +00:00
Hans Mackowiak
2640a509f9 Sunburst: rewrite using ETB counter 2019-01-13 14:10:44 +00:00
Michael Kamensky
e2eb957af9 Merge branch 'sentry2019Breadcrumbs' into 'master'
Sentry: add more Breadcumbs

See merge request core-developers/forge!1274
2019-01-13 09:44:15 +00:00
Hans Mackowiak
81f07cb8a3 Sentry: add more Breadcumbs 2019-01-13 09:44:15 +00:00
Michael Kamensky
1e3cb2e66c Merge branch 'multiplayer-mulligan' into 'master'
Remove partial paris mulligan

See merge request core-developers/forge!1273
2019-01-13 09:17:00 +00:00
Michael Kamensky
76da5b0cac Merge branch 'token_asterisk_replacement' into 'master'
Replace asterisks with x's for token filenames

See merge request core-developers/forge!1272
2019-01-13 09:15:53 +00:00
Chris H
fdc1c32287 Remove partial paris mulligan 2019-01-12 23:49:09 -05:00
Chris H
3b38547fc9 Replace asterisks with x's for token filenames 2019-01-12 22:55:23 -05:00
Michael Kamensky
53d1716255 Merge branch 'deckAnyNumber' into 'master'
DeckFormat: A deck can have any number of cards named CARDNAME.

See merge request core-developers/forge!1270
2019-01-12 16:13:00 +00:00
Hans Mackowiak
0606a00942 DeckFormat: A deck can have any number of cards named CARDNAME. 2019-01-12 16:13:00 +00:00
swordshine
af3c645521 Merge branch 'adaptReduceCost' into 'master'
Adapt: add ReduceCost for Pteramander

See merge request core-developers/forge!1267
2019-01-12 07:49:50 +00:00
swordshine
e5093c6d2f Merge branch 'patch-2' into 'master'
Fix vicious_rumors.txt not discarding

See merge request core-developers/forge!1268
2019-01-12 07:48:49 +00:00
Sol
59102b0e08 Update vicious_rumors.txt 2019-01-12 01:36:29 +00:00
Hanmac
17cff99c2b Adapt: add ReduceCost for Pteramander 2019-01-11 21:15:27 +01:00
Michael Kamensky
7d0dbff8bc Merge branch 'master' into 'master'
Select cards from any Zone that can be shown and do arrangeForScry by popping up library and moving cards (in desktop GUI); Pass min/max card selections through to GUI

See merge request core-developers/forge!1260
2019-01-11 17:08:50 +00:00
Sol
6974e2de27 Merge branch 'patch-2' into 'master'
CostPayLife: fix unused import

See merge request core-developers/forge!1266
2019-01-11 13:27:37 +00:00
Hans Mackowiak
770ed4524a CostPayLife: fix unused import 2019-01-11 13:24:33 +00:00
Hans Mackowiak
7f4dc85554 Merge branch '793-new-feature-paylife-trigger' into 'master'
New Feature: PayLife Trigger

Closes #793

See merge request core-developers/forge!1265
2019-01-11 13:01:08 +00:00
Hans Mackowiak
005fa3d732 Font of Agonies: use new PayLife trigger 2019-01-11 10:28:27 +00:00
Hans Mackowiak
bbe8d79400 Player: add PayLife Trigger 2019-01-11 10:16:29 +00:00
Hans Mackowiak
edaab9d7de TriggerType: add PayLife 2019-01-11 09:38:02 +00:00
Hans Mackowiak
842c6e681d TriggerPayLife: add new Trigger 2019-01-11 09:33:56 +00:00
Hans Mackowiak
05be8406cc Update TriggerLifeGained: better format 2019-01-11 09:31:53 +00:00
Hans Mackowiak
7aa879fb8e Update CostDraw: fix Description 2019-01-11 09:19:12 +00:00
Hans Mackowiak
1e6a9b8c26 Update CostDamage, fix Description 2019-01-11 09:17:55 +00:00
Hans Mackowiak
b2e9c88f62 CostPayLife: refund never called and doesn't work
negative amount can't be paid in the function
2019-01-11 09:16:50 +00:00
Peter F. Patel-Schneider
3609ff9eff only use new input methods if in desktop GUI; remove debugging prints 2019-01-11 03:48:42 -05:00
Peter F. Patel-Schneider
09fc3ae60c add new GUI interface allowing cards to be moved around in a list and use for arrangeForScry if preference UI_SELECT_FROM_CARD_DISPLAYS is set 2019-01-09 21:13:09 -05:00
Michael Kamensky
948c13dd15 Merge branch 'deckicons' into 'master'
Add icons to duels and challenges in LEB and ARN worlds

See merge request core-developers/forge!1262
2019-01-09 06:04:07 +00:00
Rob Schnautz
b8f7d08fae Remove extraneous line 2019-01-09 05:50:07 +00:00
Rob Schnautz
576b7e2dd3 fix kane 2019-01-09 04:35:52 +00:00
Rob Schnautz
024b2e1a01 Add new quest opponent avatars to the downloader. 2019-01-09 04:23:24 +00:00
Rob Schnautz
daf4b9d974 ARN icons and some flavor text. 2019-01-08 03:25:43 +00:00
Rob Schnautz
36b3431975 rest of the LEB world icons 2019-01-08 02:54:36 +00:00
Rob Schnautz
3ded3926f4 Remove placeholder icons for shiny new icons. 2019-01-08 02:50:58 +00:00
Peter F. Patel-Schneider
69502dd97b Pass min/max through choice code so that user can't choose too many cards 2019-01-06 12:07:19 -05:00
Peter F. Patel-Schneider
234304f9ec Add option to select cards from any Zone that can be shown (implemented in desktop GUI only for now) 2019-01-06 10:47:20 -05:00
Michael Kamensky
193a1cc255 Merge branch 'teysaKarlov' into 'master'
Teysa Karlov: add Dieharmonicon to TriggerHandler

See merge request core-developers/forge!1259
2019-01-06 11:12:38 +00:00
Hanmac
d0b569a07f Teysa Karlov: add Dieharmonicon to TriggerHandler 2019-01-06 11:38:58 +01:00
Michael Kamensky
b17edd31ac Merge branch 'tapAttacker' into 'master'
Taps Trigger now flag for being used as Attacker

See merge request core-developers/forge!1258
2019-01-06 05:34:09 +00:00
Hanmac
891f61701c Taps Trigger now flag for being used as Attacker 2019-01-05 20:52:40 +01:00
Hans Mackowiak
e35f5098b9 Merge branch 'charmNone' into 'master'
CharmEffect: now allows zero choices

See merge request core-developers/forge!1257
2019-01-05 12:01:30 +00:00
Hans Mackowiak
55da7435f8 CharmEffect: now allows zero choices 2019-01-05 12:01:30 +00:00
Michael Kamensky
f0a561b1ee Merge branch 'adaptBio' into 'master'
Adapt changes for Biomancer's Familiar

See merge request core-developers/forge!1256
2019-01-04 19:38:37 +00:00
Hans Mackowiak
bbd48d1033 Adapt changes for Biomancer's Familiar 2019-01-04 19:38:37 +00:00
Hans Mackowiak
847013c6c7 Merge branch 'morphSimFix' into 'master'
fixes for game sim

See merge request core-developers/forge!1255
2019-01-04 09:23:47 +00:00
Hanmac
61aa144631 fixes for game sim 2019-01-03 21:44:18 +01:00
Michael Kamensky
dd87f74eb4 Merge branch 'atEOTSvar' into 'master'
At eot svar

See merge request core-developers/forge!1254
2019-01-03 17:47:16 +00:00
Hanmac
95dbc5333e updated card scripts, remove EndOfTurnLeavePlay, add tokenscripts 2019-01-03 16:02:04 +01:00
Hanmac
0d2114416c extend AtEOT, fix resetTurnActivations 2019-01-03 15:54:53 +01:00
Michael Kamensky
215808b011 Merge branch 'facedown-reveal-fix' into 'master'
Fixed the reveal for face-down cards leaving battlefield/stack not working anymore.

See merge request core-developers/forge!1253
2018-12-30 10:43:37 +00:00
Agetian
1ced8bba7d - Style fix. Added a comment. 2018-12-30 08:26:34 +03:00
Agetian
468ff7fc50 - Fixed the reveal for face-down cards leaving battlefield/stack not working anymore. 2018-12-30 08:19:29 +03:00
Hans Mackowiak
0eed2c67d1 Merge branch 'morphRework' into 'master'
Morph and Manifest are not to facedown anymore

See merge request core-developers/forge!1252
2018-12-29 19:18:35 +00:00
Hanmac
4168ef0b0d Morph and Manifest are not to facedown anymore 2018-12-29 15:45:20 +01:00
Michael Kamensky
bf59d932f4 Merge branch 'master' into 'master'
XMage cube ports update

See merge request core-developers/forge!1251
2018-12-27 18:21:14 +00:00
Agetian
765b0e4625 - XMage cube ports update (2 new cubes). 2018-12-27 21:19:45 +03:00
Michael Kamensky
16058bbea2 Merge branch 'tweak-target-overlay' into 'master'
Tweak target overlay

See merge request core-developers/forge!1248
2018-12-27 18:09:24 +00:00
Michael Kamensky
f47f95cd2e Merge branch 'seanceCopyFix' into 'master'
Seance: use AddTypes and AtEOT instead of extra Clones defined

See merge request core-developers/forge!1249
2018-12-27 18:09:00 +00:00
Hanmac
7f0f62abb6 Seance: use AddTypes and AtEOT instead of extra Clones defined 2018-12-27 17:31:34 +01:00
Hans Mackowiak
22627b57f0 Merge branch 'patch-2' into 'master'
Update growth_spiral.txt

Closes #792

See merge request core-developers/forge!1250
2018-12-27 16:23:02 +00:00
Hans Mackowiak
5a81cace02 Update growth_spiral.txt 2018-12-27 16:21:28 +00:00
Tim Scott
3f172e6872 Merge remote-tracking branch 'remotes/upstream-forge/master' into tweak-target-overlay 2018-12-27 08:08:00 -06:00
Michael Kamensky
52dcdaa47d Merge branch 'master' into 'master'
AI damage chaining: Eliminated a redundant call to canPlay (already tested at this point).

See merge request core-developers/forge!1247
2018-12-27 10:18:41 +00:00
Agetian
d4476cbd2b - Eliminated a redundant call to canPlay (already tested above). 2018-12-27 13:17:49 +03:00
Michael Kamensky
d2c4ab28c2 Merge branch 'master' into 'master'
Fixed the AI misplaying with Chamber Sentry.

Closes #791

See merge request core-developers/forge!1246
2018-12-27 05:23:31 +00:00
Agetian
23d67ab0cf - Fixed the AI misplaying with Chamber Sentry. 2018-12-27 08:21:28 +03:00
Tim Scott
3f55b9f503 Flesh out README. 2018-12-26 10:18:40 -06:00
Tim Scott
e222444809 Update readme with build info. 2018-12-26 09:32:22 -06:00
Tim Scott
83ed648966 Merge remote-tracking branch 'remotes/upstream-forge/master' into tweak-target-overlay 2018-12-26 08:44:28 -06:00
Hans Mackowiak
a5c1d88460 Merge branch 'putLandOptionalFix' into 'master'
Fix 'You may put' 'land card from your hand onto the battlefield' cards

See merge request core-developers/forge!1245
2018-12-26 10:28:58 +00:00
Hanmac
fee83e0db4 Fix 'You may put' 'land card from your hand onto the battlefield' cards 2018-12-26 11:19:23 +01:00
Tim Scott
dadf1b12d3 Add README file. 2018-12-25 10:55:00 -06:00
Tim Scott
5ff8298b62 Fix issue with missing foe blocking arcs for a foe that is targeted by friendly enchantment and arcs are set to "ON" (vs MOUSEOVER). 2018-12-25 10:04:19 -06:00
Tim Scott
44e181981d Update targeting overlay to draw target arrow for stack top when "On" (no initial mouse over required).
Prevent identical target arrows from being added to the arc lists which was causing a minor graphics glitch.
2018-12-24 16:44:01 -06:00
611 changed files with 9282 additions and 3135 deletions

119
README.md Normal file
View File

@@ -0,0 +1,119 @@
# Forge
Gitlab repo is found [here](https://git.cardforge.org/core-developers/forge).
Dev instructions here: [Getting Started](https://www.slightlymagic.net/wiki/Forge:How_to_Get_Started_Developing_Forge) (Somewhat outdated)
Discord channel [here](https://discordapp.com/channels/267367946135928833/267742313390931968)
# Requirements / Tools
- Java IDE such as IntelliJ or Eclipse
- Git
- Maven
- Gitlab account
- Libgdx (optional: familiarity with this library is helpful for mobile platform development)
- Android SDK (optional: for Android releases)
- RoboVM (optional: for iOS releases) (TBD: Current status of support by libgdx)
# Project Quick Setup
- Log in to gitlab with your user account and fork the project.
- Clone your forked project to your local machine
- Go to the project location on your machine. Run Maven to download all dependencies and build a snapshot. Example for Windows & Linux: `mvn -U -B clean -P windows-linux install`
# Eclipse
## Project Setup
- Follow the instructions for cloning from Gitlab. You'll need a Gitlab account setup and an SSH key defined. If you are on a
Windows machine you can use Putty with TortoiseGit. Run puttygen.exe to generate the key -- save the private key and export
the OpenSSH public key. If you just leave the dialog open, you can copy and paste the key from it to your Gitlab profile under
"SSH keys".
Run pageant.exe and add the private key generated earlier. TortoiseGit will use this for accessing Gitlab.
- Fork the Forge git repo to your Gitlab account.
- Clone your forked repo to your local machine.
- Make sure the Java SDK is installed -- not just the JRE. Java 8 or newer required. At the time of this writing, JDK 11 works as expected.
- You need maven to load in dependencies and build. Obtain that [from here](https://maven.apache.org/download.cgi). Execute the following from the root repo dir to download dependencies, etc:
`mvn -U -B clean -P windows-linux install`
For the desktop, this will create a populated directory at `forge-gui-desktop/target/forge-gui-desktop-<release-name>` containing typical release files such as the jar, Windows executable, resource files, etc.
- Install Eclipse for Java. Launch it. At the time of this writing, Eclipse 2018-12 works as expected. YMMV for other versions.
- Create a workspace. Go to the workbench. Right-click inside of Package Explorer > Import... > General > Existing Projects into Workspace > Navigate to local forge repo >
Check "Search for nested projects" > Uncheck 'forge', check the rest > Finish.
- Let Eclipse run through building the project.
## Project Launch
### Desktop
- Right-click on forge-gui-desktop > Run As... > Java Application > "Main - forge.view" > Proceed
### Mobile (Desktop dev)
- Right-click on forge-gui-mobile-dev > Run As... > Java Application > "Main - forge.app" > Proceed
# IntelliJ
TBD
# General Notes
## Project Hierarchy
Forge is divided into 4 primary projects with additional projects that target specific platform releases. The primary projects are:
- forge-ai
- forge-core
- forge-game
- forge-gui
The platform-specific projects are:
- forge-gui-android
- forge-gui-desktop
- forge-gui-ios
- forge-gui-mobile
- forge-gui-mobile-dev
### forge-ai
### forge-core
### forge-game
### forge-gui
### forge-gui-android
Libgdx-based backend targeting Android. Requires Android SDK and relies on forge-gui-mobile for GUI logic.
### forge-gui-desktop
Java Swing based GUI targeting desktop machines.
Screen layout and game logic revolving around the GUI is found here. For example, the overlay arrows (when enabled) that indicate attackers and blockers, or the targets of the stack are defined and drawn by this.
### forge-gui-ios
Libgdx-based backend targeting iOS. Relies on forge-gui-mobile for GUI logic.
### forge-gui-mobile
Mobile GUI game logic utilizing [libgdx](https://libgdx.badlogicgames.com/) library. Screen layout and game logic revolving around the GUI for the mobile platforms is found here.
### forge-gui-mobile-dev
Libgdx backend for desktop development for mobile backends. Utilizes LWJGL. Relies on forge-gui-mobile for GUI logic.

View File

@@ -6,7 +6,7 @@
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>1.6.20-SNAPSHOT</version>
<version>1.6.20</version>
</parent>
<artifactId>forge-ai</artifactId>

View File

@@ -1211,7 +1211,7 @@ public class AiController {
public boolean confirmAction(SpellAbility sa, PlayerActionConfirmMode mode, String message) {
ApiType api = sa.getApi();
// Abilities without api may also use this routine, However they should provide a unique mode value
// Abilities without api may also use this routine, However they should provide a unique mode value ?? How could this work?
if (api == null) {
String exMsg = String.format("AI confirmAction does not know what to decide about %s mode (api is null).",
mode);

View File

@@ -821,6 +821,8 @@ public class AiCostDecision extends CostDecisionMakerBase {
}
}
}
} else if (sVar.equals("Count$xPaid")) {
c = AbilityUtils.calculateAmount(source, "PayX", null);
} else {
c = AbilityUtils.calculateAmount(source, amount, ability);
}

View File

@@ -2847,7 +2847,7 @@ public class ComputerUtil {
repParams.put("Source", source);
List<ReplacementEffect> list = player.getGame().getReplacementHandler().getReplacementList(repParams,
ReplacementLayer.None);
ReplacementLayer.Other);
if (Iterables.any(list, CardTraitPredicates.hasParam("AiLogic", "NoLife"))) {
return false;
@@ -2878,7 +2878,7 @@ public class ComputerUtil {
repParams.put("Source", source);
List<ReplacementEffect> list = player.getGame().getReplacementHandler().getReplacementList(repParams,
ReplacementLayer.None);
ReplacementLayer.Other);
if (Iterables.any(list, CardTraitPredicates.hasParam("AiLogic", "NoLife"))) {
// no life gain is not negative

View File

@@ -95,9 +95,15 @@ public class ComputerUtilAbility {
public static List<SpellAbility> getOriginalAndAltCostAbilities(final List<SpellAbility> originList, final Player player) {
final List<SpellAbility> newAbilities = Lists.newArrayList();
for (SpellAbility sa : originList) {
sa.setActivatingPlayer(player);
List<SpellAbility> originListWithAddCosts = Lists.newArrayList();
for (SpellAbility sa : originList) {
// If this spell has alternative additional costs, add them instead of the unmodified SA itself
sa.setActivatingPlayer(player);
originListWithAddCosts.addAll(GameActionUtil.getAdditionalCostSpell(sa));
}
for (SpellAbility sa : originListWithAddCosts) {
// determine which alternative costs are cheaper than the original and prioritize them
List<SpellAbility> saAltCosts = GameActionUtil.getAlternativeCosts(sa, player);
List<SpellAbility> priorityAltSa = Lists.newArrayList();

View File

@@ -2590,7 +2590,7 @@ public class ComputerUtilCombat {
// repParams.put("PreventMap", preventMap);
List<ReplacementEffect> list = game.getReplacementHandler().getReplacementList(repParams,
ReplacementLayer.None);
ReplacementLayer.Other);
return !list.isEmpty();
}

View File

@@ -162,10 +162,36 @@ public class PlayerControllerAi extends PlayerController {
@Override
public <T extends GameEntity> List<T> chooseEntitiesForEffect(
FCollectionView<T> optionList, DelayedReveal delayedReveal, SpellAbility sa, String title,
FCollectionView<T> optionList, int min, int max, DelayedReveal delayedReveal, SpellAbility sa, String title,
Player targetedPlayer) {
// this isn't used
return null;
if (delayedReveal != null) {
reveal(delayedReveal.getCards(), delayedReveal.getZone(), delayedReveal.getOwner(), delayedReveal.getMessagePrefix());
}
FCollection<T> remaining = new FCollection<T>(optionList);
List<T> selecteds = new ArrayList<T>();
T selected;
do {
selected = chooseSingleEntityForEffect(remaining, null, sa, title, selecteds.size()>=min, targetedPlayer);
if ( selected != null ) {
remaining.remove(selected);
selecteds.add(selected);
}
} while ( (selected != null ) && (selecteds.size() < max) );
return selecteds;
}
@Override
public <T extends GameEntity> List<T> chooseFromTwoListsForEffect(FCollectionView<T> optionList1, FCollectionView<T> optionList2,
boolean optional, DelayedReveal delayedReveal, SpellAbility sa, String title, Player targetedPlayer) {
if (delayedReveal != null) {
reveal(delayedReveal.getCards(), delayedReveal.getZone(), delayedReveal.getOwner(), delayedReveal.getMessagePrefix());
}
T selected1 = chooseSingleEntityForEffect(optionList1, null, sa, title, optional, targetedPlayer);
T selected2 = chooseSingleEntityForEffect(optionList2, null, sa, title, optional || selected1!=null, targetedPlayer);
List<T> selecteds = new ArrayList<T>();
if ( selected1 != null ) { selecteds.add(selected1); }
if ( selected2 != null ) { selecteds.add(selected2); }
return selecteds;
}
@Override
@@ -1090,7 +1116,7 @@ public class PlayerControllerAi extends PlayerController {
@Override
public List<Card> chooseCardsForZoneChange(
ZoneType destination, List<ZoneType> origin, SpellAbility sa, CardCollection fetchList,
ZoneType destination, List<ZoneType> origin, SpellAbility sa, CardCollection fetchList, int min, int max,
DelayedReveal delayedReveal, String selectPrompt, Player decider) {
// this isn't used
return null;
@@ -1164,4 +1190,10 @@ public class PlayerControllerAi extends PlayerController {
return chosenOptCosts;
}
@Override
public boolean confirmMulliganScry(Player p) {
// Always true?
return true;
}
}

View File

@@ -11,6 +11,7 @@ import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.*;
import forge.game.cost.Cost;
import forge.game.cost.CostPart;
import forge.game.cost.CostPartMana;
import forge.game.cost.CostRemoveCounter;
import forge.game.keyword.Keyword;
@@ -286,6 +287,18 @@ public class DamageDealAi extends DamageAiBase {
}
}
if ("XCountersDamage".equals(logic) && sa.getPayCosts() != null) {
// Check to ensure that we have enough counters to remove per the defined PayX
for (CostPart part : sa.getPayCosts().getCostParts()) {
if (part instanceof CostRemoveCounter) {
if (source.getCounters(((CostRemoveCounter) part).counter) < Integer.valueOf(source.getSVar("PayX"))) {
return false;
}
break;
}
}
}
return true;
}
@@ -1080,7 +1093,7 @@ public class DamageDealAi extends DamageAiBase {
continue; // not a toughness debuff
}
}
if (StringUtils.isNumeric(dmgDef) && ab.canPlay()) { // currently doesn't work for X and other dependent costs
if (StringUtils.isNumeric(dmgDef)) { // currently doesn't work for X and other dependent costs
if (sa.usesTargeting() && ab.usesTargeting()) {
// Ensure that the chained spell can target at least the same things (or more) as the current one
TargetRestrictions tgtSa = sa.getTargetRestrictions();

View File

@@ -113,7 +113,7 @@ public class ManifestAi extends SpellAbilityAi {
repParams.put("Origin", ZoneType.Library);
repParams.put("Destination", ZoneType.Battlefield);
repParams.put("Source", sa.getHostCard());
List<ReplacementEffect> list = game.getReplacementHandler().getReplacementList(repParams, ReplacementLayer.None);
List<ReplacementEffect> list = game.getReplacementHandler().getReplacementList(repParams, ReplacementLayer.Other);
if (!list.isEmpty()) {
return false;
}

View File

@@ -310,7 +310,8 @@ public class GameCopier {
newCard.setManifested(true);
// TODO: Should be able to copy other abilities...
if (isCreature && hasManaCost) {
newCard.addSpellAbility(CardFactoryUtil.abilityManifestFaceUp(newCard, newCard.getManaCost()));
newCard.getState(CardStateName.Original).addSpellAbility(
CardFactoryUtil.abilityManifestFaceUp(newCard, newCard.getManaCost()));
}
}
}

View File

@@ -6,7 +6,7 @@
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>1.6.20-SNAPSHOT</version>
<version>1.6.20</version>
</parent>
<artifactId>forge-core</artifactId>

View File

@@ -20,6 +20,8 @@ package forge.deck;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import forge.StaticData;
import forge.card.CardRules;
import forge.card.CardRulesPredicates;
@@ -47,7 +49,7 @@ public enum DeckFormat {
QuestDeck ( Range.between(40, Integer.MAX_VALUE), Range.between(0, 15), 4),
Limited ( Range.between(40, Integer.MAX_VALUE), null, Integer.MAX_VALUE),
Commander ( Range.is(99), Range.between(0, 10), 1, new Predicate<CardRules>() {
private final Set<String> bannedCards = new HashSet<String>(Arrays.asList(
private final Set<String> bannedCards = ImmutableSet.of(
"Adriana's Valor", "Advantageous Proclamation", "Amulet of Quoz", "Ancestral Recall", "Assemble the Rank and Vile",
"Backup Plan", "Balance", "Biorhythm", "Black Lotus", "Brago's Favor", "Braids, Cabal Minion", "Bronze Tablet",
"Channel", "Chaos Orb", "Coalition Victory", "Contract from Below", "Darkpact", "Demonic Attorney", "Double Stroke",
@@ -59,7 +61,7 @@ public enum DeckFormat {
"Rebirth", "Recurring Nightmare", "Rofellos, Llanowar Emissary", "Secret Summoning", "Secrets of Paradise",
"Sentinel Dispatch", "Shahrazad", "Sovereign's Realm", "Summoner's Bond", "Sundering Titan", "Sway of the Stars",
"Sylvan Primordial", "Tempest Efreet", "Time Vault", "Time Walk", "Timmerian Fiends", "Tinker", "Tolarian Academy",
"Trade Secrets", "Unexpected Potential", "Upheaval", "Weight Advantage", "Worldfire", "Worldknit", "Yawgmoth's Bargain"));
"Trade Secrets", "Unexpected Potential", "Upheaval", "Weight Advantage", "Worldfire", "Worldknit", "Yawgmoth's Bargain");
@Override
public boolean apply(CardRules rules) {
if (bannedCards.contains(rules.getName())) {
@@ -70,8 +72,8 @@ public enum DeckFormat {
}),
Pauper ( Range.is(60), Range.between(0, 10), 1),
Brawl ( Range.is(59), Range.between(0, 15), 1, null, new Predicate<PaperCard>() {
private final Set<String> bannedCards = new HashSet<String>(Arrays.asList(
"Baral, Chief of Compliance","Smuggler's Copter","Sorcerous Spyglass"));
private final Set<String> bannedCards = ImmutableSet.of(
"Baral, Chief of Compliance","Smuggler's Copter","Sorcerous Spyglass");
@Override
public boolean apply(PaperCard card) {
//why do we need to hard code the bannings here - they are defined in the GameFormat predicate used below
@@ -81,7 +83,7 @@ public enum DeckFormat {
return StaticData.instance() == null ? false : StaticData.instance().getBrawlPredicate().apply(card);
}
}) {
private final ImmutableSet<String> bannedCommanders = ImmutableSet.of("Baral, Chief of Compliance");
private final Set<String> bannedCommanders = ImmutableSet.of("Baral, Chief of Compliance");
@Override
public boolean isLegalCommander(CardRules rules) {
@@ -89,11 +91,11 @@ public enum DeckFormat {
}
},
TinyLeaders ( Range.is(49), Range.between(0, 10), 1, new Predicate<CardRules>() {
private final Set<String> bannedCards = new HashSet<String>(Arrays.asList(
private final Set<String> bannedCards = ImmutableSet.of(
"Ancestral Recall", "Balance", "Black Lotus", "Black Vise", "Channel", "Chaos Orb", "Contract From Below", "Counterbalance", "Darkpact", "Demonic Attorney", "Demonic Tutor", "Earthcraft", "Edric, Spymaster of Trest", "Falling Star",
"Fastbond", "Flash", "Goblin Recruiter", "Grindstone", "Hermit Druid", "Imperial Seal", "Jeweled Bird", "Karakas", "Library of Alexandria", "Mana Crypt", "Mana Drain", "Mana Vault", "Metalworker", "Mind Twist", "Mishra's Workshop",
"Mox Emerald", "Mox Jet", "Mox Pearl", "Mox Ruby", "Mox Sapphire", "Necropotence", "Shahrazad", "Skullclamp", "Sol Ring", "Strip Mine", "Survival of the Fittest", "Sword of Body and Mind", "Time Vault", "Time Walk", "Timetwister",
"Timmerian Fiends", "Tolarian Academy", "Umezawa's Jitte", "Vampiric Tutor", "Wheel of Fortune", "Yawgmoth's Will"));
"Timmerian Fiends", "Tolarian Academy", "Umezawa's Jitte", "Vampiric Tutor", "Wheel of Fortune", "Yawgmoth's Will");
@Override
public boolean apply(CardRules rules) {
@@ -112,7 +114,7 @@ public enum DeckFormat {
return true;
}
}) {
private final ImmutableSet<String> bannedCommanders = ImmutableSet.of("Derevi, Empyrial Tactician", "Erayo, Soratami Ascendant", "Rofellos, Llanowar Emissary");
private final Set<String> bannedCommanders = ImmutableSet.of("Derevi, Empyrial Tactician", "Erayo, Soratami Ascendant", "Rofellos, Llanowar Emissary");
@Override
public boolean isLegalCommander(CardRules rules) {
@@ -141,13 +143,6 @@ public enum DeckFormat {
private final static String ADVPROCLAMATION = "Advantageous Proclamation";
private final static String SOVREALM = "Sovereign's Realm";
private static final List<String> limitExceptions = Arrays.asList(
new String[]{"Relentless Rats", "Shadowborn Apostle", "Rat Colony"});
public static List<String> getLimitExceptions(){
return limitExceptions;
}
private DeckFormat(Range<Integer> mainRange0, Range<Integer> sideRange0, int maxCardCopies0, Predicate<CardRules> cardPoolFilter0, Predicate<PaperCard> paperCardPoolFilter0) {
mainRange = mainRange0;
sideRange = sideRange0;
@@ -342,7 +337,6 @@ public enum DeckFormat {
//basic lands, Shadowborn Apostle, Relentless Rats and Rat Colony
final CardPool allCards = deck.getAllCardsInASinglePool(hasCommander());
final ImmutableSet<String> limitExceptions = ImmutableSet.of("Relentless Rats", "Shadowborn Apostle", "Rat Colony");
// should group all cards by name, so that different editions of same card are really counted as the same card
for (final Entry<String, Integer> cp : Aggregates.groupSumBy(allCards, PaperCard.FN_GET_NAME)) {
@@ -351,8 +345,7 @@ public enum DeckFormat {
return TextUtil.concatWithSpace("contains the nonexisting card", cp.getKey());
}
final boolean canHaveMultiple = simpleCard.getRules().getType().isBasicLand() || limitExceptions.contains(cp.getKey());
if (!canHaveMultiple && cp.getValue() > maxCopies) {
if (!canHaveAnyNumberOf(simpleCard) && cp.getValue() > maxCopies) {
return TextUtil.concatWithSpace("must not contain more than", String.valueOf(maxCopies), "copies of the card", cp.getKey());
}
}
@@ -370,6 +363,12 @@ public enum DeckFormat {
return null;
}
public static boolean canHaveAnyNumberOf(final IPaperCard icard) {
return icard.getRules().getType().isBasicLand()
|| Iterables.contains(icard.getRules().getMainPart().getKeywords(),
"A deck can have any number of cards named CARDNAME.");
}
public static String getPlaneSectionConformanceProblem(final CardPool planes) {
//Must contain at least 10 planes/phenomenons, but max 2 phenomenons. Singleton.
if (planes == null || planes.countAll() < 10) {

View File

@@ -102,6 +102,7 @@ public class DeckRecognizer {
// Pattern.compile("(.*)[^A-Za-wyz]*\\s+([\\d]{1,2})");
private static final Pattern SEARCH_NUMBERS_IN_FRONT = Pattern.compile("([\\d]{1,2})[^A-Za-wyz]*\\s+(.*)");
//private static final Pattern READ_SEPARATED_EDITION = Pattern.compile("[[\\(\\{]([a-zA-Z0-9]){1,3})[]*\\s+(.*)");
private static final Pattern SEARCH_SINGLE_SLASH = Pattern.compile("(?<=[^/])\\s*/\\s*(?=[^/])");
private final SetPreference useLastSet;
private final ICardDatabase db;
@@ -125,7 +126,10 @@ public class DeckRecognizer {
return new Token(TokenType.Comment, 0, rawLine);
}
final char smartQuote = (char) 8217;
final String line = rawLine.trim().replace(smartQuote, '\'');
String line = rawLine.trim().replace(smartQuote, '\'');
// Some websites export split card names with a single slash. Replace with double slash.
line = SEARCH_SINGLE_SLASH.matcher(line).replaceFirst(" // ");
Token result = null;
final Matcher foundNumbersInFront = DeckRecognizer.SEARCH_NUMBERS_IN_FRONT.matcher(line);

View File

@@ -100,8 +100,7 @@ public class PaperToken implements InventoryItemFromSet, IPaperCard {
build.add(edition.getCode());
}
// Should future image file names be all lower case? Instead of Up case sets?
return StringUtils.join(build, "_").toLowerCase();
return StringUtils.join(build, "_").replace('*', 'x').toLowerCase();
}
public PaperToken(final CardRules c) { this(c, null, null); }

View File

@@ -86,7 +86,7 @@ public class Localizer {
resourceBundle = ResourceBundle.getBundle(languageRegionID, new Locale(splitLocale[0], splitLocale[1]), loader);
} catch (NullPointerException | MissingResourceException e) {
//If the language can't be loaded, default to US English
resourceBundle = ResourceBundle.getBundle("en-GB", new Locale("en", "GB"), loader);
resourceBundle = ResourceBundle.getBundle("en-US", new Locale("en", "US"), loader);
e.printStackTrace();
}

View File

@@ -6,7 +6,7 @@
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>1.6.20-SNAPSHOT</version>
<version>1.6.20</version>
</parent>
<artifactId>forge-game</artifactId>

View File

@@ -29,6 +29,7 @@ import forge.game.ability.effects.AttachEffect;
import forge.game.card.*;
import forge.game.event.*;
import forge.game.keyword.KeywordInterface;
import forge.game.keyword.KeywordsChange;
import forge.game.player.GameLossReason;
import forge.game.player.Player;
import forge.game.replacement.ReplacementEffect;
@@ -50,9 +51,9 @@ import forge.util.collect.FCollection;
import forge.util.collect.FCollectionView;
import forge.util.maps.HashMapOfLists;
import forge.util.maps.MapOfLists;
import org.apache.commons.lang3.tuple.ImmutablePair;
import java.util.*;
import java.util.Map.Entry;
/**
* Methods for common actions performed during a game.
@@ -70,10 +71,8 @@ public class GameAction {
}
public final void resetActivationsPerTurn() {
final CardCollectionView all = game.getCardsInGame();
// Reset Activations per Turn
for (final Card card : all) {
for (final Card card : game.getCardsInGame()) {
for (final SpellAbility sa : card.getAllSpellAbilities()) {
sa.getRestrictions().resetTurnActivations();
}
@@ -104,6 +103,7 @@ public class GameAction {
boolean toBattlefield = zoneTo.is(ZoneType.Battlefield);
boolean fromBattlefield = zoneFrom != null && zoneFrom.is(ZoneType.Battlefield);
boolean toHand = zoneTo.is(ZoneType.Hand);
boolean wasFacedown = c.isFaceDown();
//Rule 110.5g: A token that has left the battlefield can't move to another zone
if (c.isToken() && zoneFrom != null && !fromBattlefield && !zoneFrom.is(ZoneType.Command)) {
@@ -150,13 +150,15 @@ public class GameAction {
// Cards returned from exile face-down must be reset to their original state, otherwise
// all sort of funky shenanigans may happen later (e.g. their ETB replacement effects are set
// up on the wrong card state etc.).
if (c.isFaceDown() && (fromBattlefield || (toHand && zoneFrom.is(ZoneType.Exile)))) {
if (wasFacedown && (fromBattlefield || (toHand && zoneFrom.is(ZoneType.Exile)))) {
c.setState(CardStateName.Original, true);
c.runFaceupCommands();
}
// Clean up the temporary Dash SVar when the Dashed card leaves the battlefield
if (fromBattlefield && c.getSVar("EndOfTurnLeavePlay").equals("Dash")) {
// Clean up the temporary AtEOT SVar
String endofTurn = c.getSVar("EndOfTurnLeavePlay");
if (fromBattlefield && (endofTurn.equals("Dash") || endofTurn.equals("AtEOT"))) {
c.removeSVar("EndOfTurnLeavePlay");
}
@@ -291,6 +293,33 @@ public class GameAction {
copied.getOwner().addInboundToken(copied);
}
if (toBattlefield) {
// HACK for making the RIOT enchantment look into the Future
// need to check the Keywords what it would have on the Battlefield
Card riotLKI = CardUtil.getLKICopy(copied);
riotLKI.setLastKnownZone(zoneTo);
CardCollection preList = new CardCollection(riotLKI);
checkStaticAbilities(false, Sets.newHashSet(riotLKI), preList);
List<Long> changedTimeStamps = Lists.newArrayList();
for(Map.Entry<Long, KeywordsChange> e : riotLKI.getChangedCardKeywords().entrySet()) {
if (!copied.hasChangedCardKeywords(e.getKey())) {
KeywordsChange o = e.getValue();
o.setHostCard(copied);
for (KeywordInterface k : o.getKeywords()) {
for (ReplacementEffect re : k.getReplacements()) {
// this param need to be set, otherwise in ReplaceMoved it fails
re.getMapParams().put("BypassEtbCheck", "True");
}
}
copied.addChangedCardKeywordsInternal(o, e.getKey());
changedTimeStamps.add(e.getKey());
}
}
checkStaticAbilities(false);
}
Map<String, Object> repParams = Maps.newHashMap();
repParams.put("Event", "Moved");
repParams.put("Affected", copied);
@@ -435,10 +464,14 @@ public class GameAction {
}
// rule 504.6: reveal a face-down card leaving the stack
if (zoneFrom != null && zoneTo != null && zoneFrom.is(ZoneType.Stack) && !zoneTo.is(ZoneType.Battlefield) && c.isFaceDown()) {
if (zoneFrom != null && zoneTo != null && zoneFrom.is(ZoneType.Stack) && !zoneTo.is(ZoneType.Battlefield) && wasFacedown) {
// FIXME: tracker freeze-unfreeze is needed here to avoid a bug with the card staying face down in the View for the reveal
boolean trackerFrozen = game.getTracker().isFrozen();
game.getTracker().unfreeze();
c.setState(CardStateName.Original, true);
reveal(new CardCollection(c), c.getOwner(), true, "Face-down card moves from the stack: ");
c.setState(CardStateName.FaceDown, true);
if (trackerFrozen) { game.getTracker().freeze(); }
}
if (fromBattlefield) {
@@ -466,19 +499,19 @@ public class GameAction {
changeZone(null, zoneTo, unmeld, position, cause, params);
}
// Reveal if face-down
if (c.isFaceDown()) {
if (wasFacedown) {
// FIXME: tracker freeze-unfreeze is needed here to avoid a bug with the card staying face down in the View for the reveal
boolean trackerFrozen = game.getTracker().isFrozen();
game.getTracker().unfreeze();
c.setState(CardStateName.Original, true);
reveal(new CardCollection(c), c.getOwner(), true, "Face-down card leaves the battlefield: ");
c.setState(CardStateName.FaceDown, true);
if (trackerFrozen) { game.getTracker().freeze(); }
copied.setState(CardStateName.Original, true);
}
unattachCardLeavingBattlefield(copied);
// Remove all changed keywords
copied.removeAllChangedText(game.getNextTimestamp());
// reset activations
for (SpellAbility ab : copied.getSpellAbilities()) {
ab.getRestrictions().resetTurnActivations();
}
} else if (toBattlefield) {
// reset timestamp in changezone effects so they have same timestamp if ETB simutaneously
copied.setTimestamp(game.getNextTimestamp());
@@ -1094,7 +1127,7 @@ public class GameAction {
if (c.isAttachedToEntity()) {
final GameEntity ge = c.getEntityAttachedTo();
if (!ge.canBeAttached(c)) {
if (!ge.canBeAttached(c, true)) {
c.unattachFromEntity(ge);
checkAgain = true;
}
@@ -1669,12 +1702,8 @@ public class GameAction {
// rule 103.4b
boolean isMultiPlayer = game.getPlayers().size() > 2;
int mulliganDelta = isMultiPlayer ? 0 : 1;
// https://magic.wizards.com/en/articles/archive/feature/checking-brawl-2018-07-09
if (game.getRules().hasAppliedVariant(GameType.Brawl) && !isMultiPlayer){
mulliganDelta = 0;
}
int mulliganDelta = isMultiPlayer || game.getRules().hasAppliedVariant(GameType.Brawl) ? 0 : 1;
boolean allKept;
do {
@@ -1690,32 +1719,17 @@ public class GameAction {
}
if (toMulligan != null && !toMulligan.isEmpty()) {
if (!isCommander) {
toMulligan = new CardCollection(p.getCardsIn(ZoneType.Hand));
for (final Card c : toMulligan) {
moveToLibrary(c, null, null);
}
try {
Thread.sleep(100); //delay for a tiny bit to give UI a chance catch up
} catch (InterruptedException e) {
e.printStackTrace();
}
p.shuffle(null);
p.drawCards(handSize[i] - mulliganDelta);
} else {
List<Card> toExile = Lists.newArrayList(toMulligan);
for (Card c : toExile) {
exile(c, null, null);
}
exiledDuringMulligans.addAll(p, toExile);
try {
Thread.sleep(100); //delay for a tiny bit to give UI a chance catch up
} catch (InterruptedException e) {
e.printStackTrace();
}
p.drawCards(toExile.size() - 1);
toMulligan = new CardCollection(p.getCardsIn(ZoneType.Hand));
for (final Card c : toMulligan) {
moveToLibrary(c, null, null);
}
try {
Thread.sleep(100); //delay for a tiny bit to give UI a chance catch up
} catch (InterruptedException e) {
e.printStackTrace();
}
p.shuffle(null);
p.drawCards(handSize[i] - mulliganDelta);
p.onMulliganned();
allKept = false;
} else {
@@ -1726,21 +1740,17 @@ public class GameAction {
mulliganDelta++;
} while (!allKept);
if (isCommander) {
for (Entry<Player, Collection<Card>> kv : exiledDuringMulligans.entrySet()) {
Player p = kv.getKey();
Collection<Card> cc = kv.getValue();
for (Card c : cc) {
moveToLibrary(c, null, null);
}
p.shuffle(null);
//Vancouver Mulligan as a scry with the decisions inside
List<Player> scryers = Lists.newArrayList();
for(Player p : whoCanMulligan) {
if (p.getStartingHandSize() > p.getZone(ZoneType.Hand).size()) {
scryers.add(p);
}
}
//Vancouver Mulligan
for(Player p : whoCanMulligan) {
if (p.getStartingHandSize() > p.getZone(ZoneType.Hand).size()) {
p.scry(1, null);
for(Player p : scryers) {
if (p.getController().confirmMulliganScry(p)) {
scry(ImmutableList.of(p), 1, null);
}
}
}
@@ -1838,4 +1848,68 @@ public class GameAction {
runParams.put("Player", p);
game.getTriggerHandler().runTrigger(TriggerType.BecomeMonarch, runParams, false);
}
// Make scry an action function so that it can be used for mulligans (with a null cause)
// Assumes that the list of players is in APNAP order, which should be the case
// Optional here as well to handle the way that mulligans do the choice
// 701.17. Scry
// 701.17a To “scry N” means to look at the top N cards of your library, then put any number of them
// on the bottom of your library in any order and the rest on top of your library in any order.
// 701.17b If a player is instructed to scry 0, no scry event occurs. Abilities that trigger whenever a
// player scries wont trigger.
// 701.17c If multiple players scry at once, each of those players looks at the top cards of their library
// at the same time. Those players decide in APNAP order (see rule 101.4) where to put those
// cards, then those cards move at the same time.
public void scry(List<Player> players, int numScry, SpellAbility cause) {
if (numScry == 0) {
return;
}
// reveal the top N library cards to the player (only)
// no real need to separate out the look if
// there is only one player scrying
if (players.size() > 1) {
for (final Player p : players) {
final CardCollection topN = new CardCollection(p.getCardsIn(ZoneType.Library, numScry));
revealTo(topN, p);
}
}
// make the decisions
List<ImmutablePair<CardCollection, CardCollection>> decisions = Lists.newArrayList();
for (final Player p : players) {
final CardCollection topN = new CardCollection(p.getCardsIn(ZoneType.Library, numScry));
ImmutablePair<CardCollection, CardCollection> decision = p.getController().arrangeForScry(topN);
decisions.add(decision);
int numToTop = decision.getLeft() == null ? 0 : decision.getLeft().size();
int numToBottom = decision.getRight() == null ? 0 : decision.getRight().size();
// publicize the decision
game.fireEvent(new GameEventScry(p, numToTop, numToBottom));
}
// do the moves after all the decisions (maybe not necesssary, but let's
// do it the official way)
for (int i = 0; i < players.size(); i++) {
// no good iterate simultaneously in Java
final Player p = players.get(i);
final CardCollection toTop = decisions.get(i).getLeft();
final CardCollection toBottom = decisions.get(i).getRight();
if (toTop != null) {
Collections.reverse(toTop); // reverse to get the correct order
for (Card c : toTop) {
moveToLibrary(c, cause, null);
}
}
if (toBottom != null) {
for (Card c : toBottom) {
moveToBottomOfLibrary(c, cause, null);
}
}
if (cause != null) {
// set up triggers (but not actually do them until later)
final Map<String, Object> runParams = Maps.newHashMap();
runParams.put("Player", p);
game.getTriggerHandler().runTrigger(TriggerType.Scry, runParams, false);
}
}
}
}

View File

@@ -28,6 +28,8 @@ import forge.game.cost.Cost;
import forge.game.spellability.*;
import forge.game.zone.ZoneType;
import forge.util.FileSection;
import io.sentry.Sentry;
import io.sentry.event.BreadcrumbBuilder;
import java.util.List;
import java.util.Map;
@@ -130,7 +132,16 @@ public final class AbilityFactory {
String source = state.getName().isEmpty() ? abString : state.getName();
throw new RuntimeException("AbilityFactory : getAbility -- no API in " + source + ": " + abString);
}
return getAbility(mapParams, type, state, parent);
try {
return getAbility(mapParams, type, state, parent);
} catch (Error | Exception ex) {
String msg = "AbilityFactory:getAbility: crash when trying to create ability ";
Sentry.getContext().recordBreadcrumb(
new BreadcrumbBuilder().setMessage(msg)
.withData("Card", state.getName()).withData("Ability", abString).build()
);
throw new RuntimeException(msg + " of card: " + state.getName(), ex);
}
}
public static final SpellAbility getAbility(final Card hostCard, final String svar) {

View File

@@ -225,10 +225,6 @@ public class AbilityUtils {
if (o != null && o instanceof Card) {
cards.add(game.getCardState((Card) o));
}
} else if (defined.equals("Clones")) {
for (final Card clone : hostCard.getClones()) {
cards.add(game.getCardState(clone));
}
} else if (defined.equals("Imprinted")) {
for (final Card imprint : hostCard.getImprintedCards()) {
cards.add(game.getCardState(imprint));

View File

@@ -230,8 +230,17 @@ public abstract class SpellAbilityEffect {
if (desc.isEmpty()) {
StringBuilder sb = new StringBuilder();
sb.append(location).append(" ");
if (location.equals("Hand")) {
sb.append("Return ");
} else if (location.equals("SacrificeCtrl")) {
sb.append("Its controller sacrifices ");
} else {
sb.append(location).append(" ");
}
sb.append(Lang.joinHomogenous(crds));
if (location.equals("Hand")) {
sb.append("to your hand").append(" ");
}
sb.append(" at the ");
if (combat) {
sb.append("end of combat.");
@@ -255,9 +264,18 @@ public abstract class SpellAbilityEffect {
final Trigger trig = TriggerHandler.parseTrigger(delTrig.toString(), sa.getHostCard(), intrinsic);
for (final Card c : crds) {
trig.addRemembered(c);
// Svar for AI
if (!c.hasSVar("EndOfTurnLeavePlay")) {
c.setSVar("EndOfTurnLeavePlay", "AtEOT");
}
}
String trigSA = "";
if (location.equals("Sacrifice")) {
if (location.equals("Hand")) {
trigSA = "DB$ ChangeZone | Defined$ DelayTriggerRemembered | Origin$ Battlefield | Destination$ Hand";
} else if (location.equals("SacrificeCtrl")) {
trigSA = "DB$ SacrificeAll | Defined$ DelayTriggerRemembered";
} else if (location.equals("Sacrifice")) {
trigSA = "DB$ SacrificeAll | Defined$ DelayTriggerRemembered | Controller$ You";
} else if (location.equals("Exile")) {
trigSA = "DB$ ChangeZone | Defined$ DelayTriggerRemembered | Origin$ Battlefield | Destination$ Exile";
@@ -289,6 +307,11 @@ public abstract class SpellAbilityEffect {
}
trig.setOverridingAbility(AbilityFactory.getAbility(trigSA, card));
card.addTrigger(trig);
// Svar for AI
if (!card.hasSVar("EndOfTurnLeavePlay")) {
card.setSVar("EndOfTurnLeavePlay", "AtEOT");
}
}
protected static void addForgetOnMovedTrigger(final Card card, final String zone) {

View File

@@ -852,7 +852,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
}
// ensure that selection is within maximum allowed changeNum
do {
selectedCards = decider.getController().chooseCardsForZoneChange(destination, origin, sa, fetchList, delayedReveal, selectPrompt, decider);
selectedCards = decider.getController().chooseCardsForZoneChange(destination, origin, sa, fetchList, 0, changeNum, delayedReveal, selectPrompt, decider);
} while (selectedCards != null && selectedCards.size() > changeNum);
if (selectedCards != null) {
for (Card card : selectedCards) {

View File

@@ -67,6 +67,8 @@ public class CharmEffect extends SpellAbilityEffect {
if (num == min) {
sb.append(Lang.getNumeral(num));
} else if (min == 0) {
sb.append("up to ").append(Lang.getNumeral(num));
} else {
sb.append(Lang.getNumeral(min)).append(" or ").append(list.size() == 2 ? "both" : "more");
}
@@ -101,6 +103,8 @@ public class CharmEffect extends SpellAbilityEffect {
if (num == min) {
sb.append(Lang.getNumeral(num));
} else if (min == 0) {
sb.append("up to ").append(Lang.getNumeral(num));
} else {
sb.append(Lang.getNumeral(min)).append(" or ").append(list.size() == 2 ? "both" : "more");
}
@@ -146,19 +150,19 @@ public class CharmEffect extends SpellAbilityEffect {
return "";
}
public static void makeChoices(SpellAbility sa) {
public static boolean makeChoices(SpellAbility sa) {
//this resets all previous choices
sa.setSubAbility(null);
// Entwine does use all Choices
if (sa.isEntwine()) {
chainAbilities(sa, makePossibleOptions(sa));
return;
return true;
}
final int num = sa.hasParam("CharmNumOnResolve") ?
AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("CharmNumOnResolve"), sa)
: Integer.parseInt(sa.hasParam("CharmNum") ? sa.getParam("CharmNum") : "1");
: Integer.parseInt(sa.getParamOrDefault("CharmNum", "1"));
final int min = sa.hasParam("MinCharmNum") ? Integer.parseInt(sa.getParam("MinCharmNum")) : num;
Card source = sa.getHostCard();
@@ -177,6 +181,7 @@ public class CharmEffect extends SpellAbilityEffect {
List<AbilitySub> chosen = chooser.getController().chooseModeForAbility(sa, min, num, sa.hasParam("CanRepeatModes"));
chainAbilities(sa, chosen);
return chosen != null && !chosen.isEmpty();
}
private static void chainAbilities(SpellAbility sa, List<AbilitySub> chosen) {

View File

@@ -164,7 +164,6 @@ public class CopyPermanentEffect extends SpellAbilityEffect {
} else {
tgtCards = getTargetCards(sa);
}
host.clearClones();
for (final Card c : tgtCards) {
if (!sa.usesTargeting() || c.canBeTargetedBy(sa)) {
@@ -184,7 +183,6 @@ public class CopyPermanentEffect extends SpellAbilityEffect {
//copyInPlay.setSetCode(c.getSetCode());
copyInPlay.setCloneOrigin(host);
sa.getHostCard().addClone(copyInPlay);
if (!pumpKeywords.isEmpty()) {
copyInPlay.addChangedCardKeywords(pumpKeywords, Lists.<String>newArrayList(), false, false, timestamp);
}
@@ -266,7 +264,6 @@ public class CopyPermanentEffect extends SpellAbilityEffect {
final List<String> svars = Lists.newArrayList();
final List<String> triggers = Lists.newArrayList();
boolean asNonLegendary = false;
boolean resetActivations = false;
if (sa.hasParam("Keywords")) {
keywords.addAll(Arrays.asList(sa.getParam("Keywords").split(" & ")));
@@ -277,9 +274,6 @@ public class CopyPermanentEffect extends SpellAbilityEffect {
if (sa.hasParam("NonLegendary")) {
asNonLegendary = true;
}
if (sa.hasParam("ResetAbilityActivations")) {
resetActivations = true;
}
if (sa.hasParam("AddSVars")) {
svars.addAll(Arrays.asList(sa.getParam("AddSVars").split(" & ")));
}
@@ -411,11 +405,6 @@ public class CopyPermanentEffect extends SpellAbilityEffect {
copy.removeIntrinsicKeyword("Devoid");
}
if (resetActivations) {
for (SpellAbility ab : copy.getSpellAbilities()) {
ab.getRestrictions().resetTurnActivations();
}
}
copy.updateStateForView();
return copy;
}

View File

@@ -199,6 +199,14 @@ public class CountersPutEffect extends SpellAbilityEffect {
counterAmount = pc.chooseNumber(sa, "How many counters?", 0, counterAmount, params);
}
// Adapt need extra logic
if (sa.hasParam("Adapt")) {
if (!(tgtCard.getCounters(CounterType.P1P1) == 0
|| tgtCard.hasKeyword("CARDNAME adapts as though it had no +1/+1 counters"))) {
continue;
}
}
if (sa.hasParam("Tribute")) {
// make a copy to check if it would be on the battlefield
Card noTributeLKI = CardUtil.getLKICopy(tgtCard);
@@ -266,6 +274,13 @@ public class CountersPutEffect extends SpellAbilityEffect {
runParams.put("Card", tgtCard);
game.getTriggerHandler().runTrigger(TriggerType.BecomeRenowned, runParams, false);
}
if (sa.hasParam("Adapt")) {
// need to remove special keyword
tgtCard.removeHiddenExtrinsicKeyword("CARDNAME adapts as though it had no +1/+1 counters");
final Map<String, Object> runParams = Maps.newHashMap();
runParams.put("Card", tgtCard);
game.getTriggerHandler().runTrigger(TriggerType.Adapt, runParams, false);
}
} else {
// adding counters to something like re-suspend cards
// etbcounter should apply multiplier

View File

@@ -4,6 +4,8 @@ import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CounterType;
import forge.game.player.Player;
import forge.game.player.PlayerController;
@@ -69,11 +71,14 @@ public class CountersRemoveEffect extends SpellAbilityEffect {
final Card card = sa.getHostCard();
final Game game = card.getGame();
final Player player = sa.getActivatingPlayer();
PlayerController pc = player.getController();
final String type = sa.getParam("CounterType");
final String num = sa.getParam("CounterNum");
int cntToRemove = 0;
if (!num.equals("All") && !num.equals("Remembered")) {
if (!num.equals("All") && !num.equals("Any") && !num.equals("Remembered")) {
cntToRemove = AbilityUtils.calculateAmount(sa.getHostCard(), num, sa);
}
@@ -96,6 +101,7 @@ public class CountersRemoveEffect extends SpellAbilityEffect {
}
boolean rememberRemoved = sa.hasParam("RememberRemoved");
boolean rememberAmount = sa.hasParam("RememberAmount");
for (final Player tgtPlayer : getTargetPlayers(sa)) {
// Removing energy
@@ -107,7 +113,23 @@ public class CountersRemoveEffect extends SpellAbilityEffect {
}
}
for (final Card tgtCard : getTargetCards(sa)) {
CardCollectionView srcCards = null;
if (sa.hasParam("ValidSource")) {
srcCards = game.getCardsIn(ZoneType.Battlefield);
srcCards = CardLists.getValidCards(srcCards, sa.getParam("ValidSource"), player, card, sa);
if (num.equals("Any")) {
StringBuilder sb = new StringBuilder();
sb.append("Choose cards to take ").append(counterType.getName()).append(" counters from");
srcCards = player.getController().chooseCardsForEffect(srcCards, sa, sb.toString(), 0, srcCards.size(), true);
}
} else {
srcCards = getTargetCards(sa);
}
int totalRemoved = 0;
for (final Card tgtCard : srcCards) {
Card gameCard = game.getCardState(tgtCard, null);
// gameCard is LKI in that case, the card is not in game anymore
// or the timestamp did change
@@ -123,14 +145,12 @@ public class CountersRemoveEffect extends SpellAbilityEffect {
}
game.updateLastStateForCard(gameCard);
continue;
} else if (num.equals("All")) {
} else if (num.equals("All") || num.equals("Any")) {
cntToRemove = gameCard.getCounters(counterType);
} else if (sa.getParam("CounterNum").equals("Remembered")) {
} else if (num.equals("Remembered")) {
cntToRemove = gameCard.getCountersAddedBy(card, counterType);
}
PlayerController pc = sa.getActivatingPlayer().getController();
if (type.equals("Any")) {
while (cntToRemove > 0 && gameCard.hasCounters()) {
final Map<CounterType, Integer> tgtCounters = gameCard.getCounters();
@@ -162,7 +182,7 @@ public class CountersRemoveEffect extends SpellAbilityEffect {
cntToRemove = Math.min(cntToRemove, gameCard.getCounters(counterType));
if (zone.is(ZoneType.Battlefield) || zone.is(ZoneType.Exile)) {
if (sa.hasParam("UpTo")) {
if (sa.hasParam("UpTo") || num.equals("Any")) {
Map<String, Object> params = Maps.newHashMap();
params.put("Target", gameCard);
params.put("CounterType", type);
@@ -179,10 +199,17 @@ public class CountersRemoveEffect extends SpellAbilityEffect {
}
}
game.updateLastStateForCard(gameCard);
totalRemoved += cntToRemove;
}
}
}
}
if (totalRemoved > 0 && rememberAmount) {
// TODO use SpellAbility Remember later
card.addRemembered(Integer.valueOf(totalRemoved));
}
}
}

View File

@@ -121,6 +121,9 @@ public class DamageAllEffect extends DamageBaseEffect {
if (!usedDamageMap) {
preventMap.triggerPreventDamage(false);
damageMap.triggerDamageDoneOnce(false, sa);
preventMap.clear();
damageMap.clear();
}
replaceDying(sa);

View File

@@ -30,7 +30,7 @@ public class DamageDealEffect extends DamageBaseEffect {
final int dmg = AbilityUtils.calculateAmount(sa.getHostCard(), damage, sa);
List<GameObject> tgts = getTargets(sa);
if (tgts.isEmpty())
if (tgts.isEmpty())
return "";
final List<Card> definedSources = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("DamageSource"), sa);
@@ -131,15 +131,15 @@ public class DamageDealEffect extends DamageBaseEffect {
sa.setPreventMap(preventMap);
usedDamageMap = true;
}
final List<Card> definedSources = AbilityUtils.getDefinedCards(hostCard, sa.getParam("DamageSource"), sa);
if (definedSources == null || definedSources.isEmpty()) {
return;
}
for (Card source : definedSources) {
final Card sourceLKI = hostCard.getGame().getChangeZoneLKIInfo(source);
if (divideOnResolution) {
// Dividing Damage up to multiple targets using combat damage box
// Currently only used for Master of the Wild Hunt
@@ -147,7 +147,7 @@ public class DamageDealEffect extends DamageBaseEffect {
if (players.isEmpty()) {
return;
}
CardCollection assigneeCards = new CardCollection();
// Do we have a way of doing this in a better fashion?
for (GameObject obj : tgts) {
@@ -155,7 +155,7 @@ public class DamageDealEffect extends DamageBaseEffect {
assigneeCards.add((Card)obj);
}
}
Player assigningPlayer = players.get(0);
Map<Card, Integer> map = assigningPlayer.getController().assignCombatDamage(sourceLKI, assigneeCards, dmg, null, true);
for (Entry<Card, Integer> dt : map.entrySet()) {
@@ -166,6 +166,9 @@ public class DamageDealEffect extends DamageBaseEffect {
preventMap.triggerPreventDamage(false);
// non combat damage cause lifegain there
damageMap.triggerDamageDoneOnce(false, sa);
preventMap.clear();
damageMap.clear();
}
replaceDying(sa);
return;
@@ -201,7 +204,7 @@ public class DamageDealEffect extends DamageBaseEffect {
}
}
}
if (remember) {
source.addRemembered(damageMap.row(sourceLKI).keySet());
}
@@ -210,6 +213,9 @@ public class DamageDealEffect extends DamageBaseEffect {
preventMap.triggerPreventDamage(false);
// non combat damage cause lifegain there
damageMap.triggerDamageDoneOnce(false, sa);
preventMap.clear();
damageMap.clear();
}
replaceDying(sa);
}

View File

@@ -132,6 +132,9 @@ public class DamageEachEffect extends DamageBaseEffect {
if (!usedDamageMap) {
preventMap.triggerPreventDamage(false);
damageMap.triggerDamageDoneOnce(false, sa);
preventMap.clear();
damageMap.clear();
}
replaceDying(sa);

View File

@@ -21,10 +21,12 @@ public class DamageResolveEffect extends SpellAbilityEffect {
if (preventMap != null) {
preventMap.triggerPreventDamage(false);
preventMap.clear();
}
// non combat damage cause lifegain there
if (damageMap != null) {
damageMap.triggerDamageDoneOnce(false, sa);
damageMap.clear();
}
}

View File

@@ -187,14 +187,14 @@ public class DigEffect extends SpellAbilityEffect {
if (!andOrValid.equals("")) {
andOrCards = CardLists.getValidCards(top, andOrValid.split(","), host.getController(), host, sa);
andOrCards.removeAll((Collection<?>)valid);
valid.addAll(andOrCards);
valid.addAll(andOrCards); //pfps need to add andOr cards to valid to have set of all valid cards set up
}
else {
andOrCards = new CardCollection();
}
}
else {
// If all the cards are valid choices, no need for a separate reveal dialog to the chooser.
// If all the cards are valid choices, no need for a separate reveal dialog to the chooser. pfps??
if (p == chooser && destZone1ChangeNum > 1) {
delayedReveal = null;
}
@@ -238,55 +238,41 @@ public class DigEffect extends SpellAbilityEffect {
if (sa.hasParam("RandomOrder")) {
CardLists.shuffle(movedCards);
}
}
else {
} else {
String prompt;
if (sa.hasParam("PrimaryPrompt")) {
prompt = sa.getParam("PrimaryPrompt");
} else {
prompt = "Choose card(s) to put into " + destZone1.name();
if (destZone1.equals(ZoneType.Library)) {
if (libraryPosition == -1) {
prompt = "Choose card(s) to put on the bottom of {player's} library";
} else if (libraryPosition == 0) {
prompt = "Choose card(s) to put on top of {player's} library";
}
}
}
if (sa.hasParam("PrimaryPrompt")) {
prompt = sa.getParam("PrimaryPrompt");
} else {
prompt = "Choose a card to put into " + destZone1.name();
if (destZone1.equals(ZoneType.Library)) {
if (libraryPosition == -1) {
prompt = "Choose a card to put on the bottom of {player's} library";
}
else if (libraryPosition == 0) {
prompt = "Choose a card to put on top of {player's} library";
}
}
}
movedCards = new CardCollection();
for (int i = 0; i < destZone1ChangeNum || (anyNumber && i < numToDig); i++) {
// let user get choice
Card chosen = null;
if (!valid.isEmpty()) {
// If we're choosing multiple cards, only need to show the reveal dialog the first time through.
boolean shouldReveal = (i == 0);
chosen = chooser.getController().chooseSingleEntityForEffect(valid, shouldReveal ? delayedReveal : null, sa, prompt, anyNumber || optional, p);
}
else {
if (i == 0) {
chooser.getController().notifyOfValue(sa, null, "No valid cards");
}
}
if (chosen == null) {
break;
}
movedCards.add(chosen);
valid.remove(chosen);
if (!andOrValid.equals("")) {
andOrCards.remove(chosen);
if (!chosen.isValid(andOrValid.split(","), host.getController(), host, sa)) {
valid = new CardCollection(andOrCards);
}
else if (!chosen.isValid(changeValid.split(","), host.getController(), host, sa)) {
valid.removeAll((Collection<?>)andOrCards);
}
}
}
movedCards = new CardCollection();
if (valid.isEmpty()) {
chooser.getController().notifyOfValue(sa, null, "No valid cards");
} else {
if ( p == chooser ) { // the digger can still see all the dug cards when choosing
chooser.getController().tempShowCards(top);
}
List<Card> chosen;
if (!andOrValid.equals("")) {
valid.removeAll(andOrCards); //pfps remove andOr cards to get two two choices set up correctly
chosen = chooser.getController().chooseFromTwoListsForEffect(valid, andOrCards, optional, delayedReveal, sa, prompt, p);
} else {
int max = anyNumber ? valid.size() : Math.min(valid.size(),destZone1ChangeNum);
int min = (anyNumber || optional) ? 0 : max;
chosen = chooser.getController().chooseEntitiesForEffect(valid, min, max, delayedReveal, sa, prompt, p);
}
chooser.getController().endTempShowCards();
movedCards.addAll(chosen);
}
if (!changeValid.isEmpty() && !sa.hasParam("ExileFaceDown") && !sa.hasParam("NoReveal")) {
game.getAction().reveal(movedCards, chooser, true,

View File

@@ -153,6 +153,9 @@ public class FightEffect extends DamageBaseEffect {
if (!usedDamageMap) {
preventMap.triggerPreventDamage(false);
damageMap.triggerDamageDoneOnce(false, sa);
preventMap.clear();
damageMap.clear();
}
replaceDying(sa);

View File

@@ -4,17 +4,17 @@ import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import java.util.List;
import com.google.common.collect.Lists;
public class ScryEffect extends SpellAbilityEffect {
@Override
protected String getStackDescription(SpellAbility sa) {
final StringBuilder sb = new StringBuilder();
final List<Player> tgtPlayers = getTargetPlayers(sa);
for (final Player p : tgtPlayers) {
for (final Player p : getTargetPlayers(sa)) {
sb.append(p.toString()).append(" ");
}
@@ -36,19 +36,16 @@ public class ScryEffect extends SpellAbilityEffect {
boolean isOptional = sa.hasParam("Optional");
final TargetRestrictions tgt = sa.getTargetRestrictions();
final List<Player> tgtPlayers = getTargetPlayers(sa);
final List<Player> players = Lists.newArrayList(); // players really affected
for (final Player p : tgtPlayers) {
if ((tgt == null) || p.canBeTargetedBy(sa)) {
if (isOptional && !p.getController().confirmAction(sa, null, "Do you want to scry?")) {
continue;
}
p.scry(num, sa);
}
}
// Optional here for spells that have optional multi-player scrying
for (final Player p : getTargetPlayers(sa)) {
if ( (!sa.usesTargeting() || p.canBeTargetedBy(sa)) &&
(!isOptional || p.getController().confirmAction(sa, null, "Do you want to scry?")) ) {
players.add(p);
}
}
sa.getActivatingPlayer().getGame().getAction().scry(players, num, sa);
}
}

View File

@@ -101,7 +101,7 @@ public class Card extends GameEntity implements Comparable<Card> {
// cards attached or otherwise linked to this card
private CardCollection hauntedBy, devouredCards, delvedCards, convokedCards, imprintedCards, encodedCards;
private CardCollection mustBlockCards, clones, gainControlTargets, chosenCards, blockedThisTurn, blockedByThisTurn;
private CardCollection mustBlockCards, gainControlTargets, chosenCards, blockedThisTurn, blockedByThisTurn;
// if this card is attached or linked to something, what card is it currently attached to
private Card encoding, cloneOrigin, haunting, effectSource, pairedWith, meldedWith;
@@ -528,30 +528,30 @@ public class Card extends GameEntity implements Comparable<Card> {
public Card manifest(Player p, SpellAbility sa) {
// Turn Face Down (even if it's DFC).
CardState originalCard = this.getState(CardStateName.Original);
ManaCost cost = originalCard.getManaCost();
ManaCost cost = getState(CardStateName.Original).getManaCost();
boolean isCreature = this.isCreature();
boolean isCreature = isCreature();
// Sometimes cards are manifested while already being face down
if (!turnFaceDown(true) && currentStateName != CardStateName.FaceDown) {
return null;
}
// Sometimes cards are manifested while already being face down
if (!turnFaceDown(true) && !isFaceDown()) {
return null;
}
// Move to p's battlefield
Game game = p.getGame();
// Just in case you aren't the controller, now you are!
this.setController(p, game.getNextTimestamp());
// Just in case you aren't the controller, now you are!
setController(p, game.getNextTimestamp());
// Mark this card as "manifested"
this.setPreFaceDownState(CardStateName.Original);
this.setManifested(true);
setPreFaceDownState(CardStateName.Original);
setManifested(true);
Card c = game.getAction().moveToPlay(this, p, sa);
// Add manifest demorph static ability for creatures
if (isCreature && !cost.isNoCost()) {
c.addSpellAbility(CardFactoryUtil.abilityManifestFaceUp(c, cost));
// Add Manifest to original State
c.getState(CardStateName.Original).addSpellAbility(CardFactoryUtil.abilityManifestFaceUp(c, cost));
c.updateStateForView();
}
@@ -1031,22 +1031,6 @@ public class Card extends GameEntity implements Comparable<Card> {
public final GameEntity getMustAttackEntityThisTurn() { return mustAttackEntityThisTurn; }
public final void setMustAttackEntityThisTurn(GameEntity entThisTurn) { mustAttackEntityThisTurn = entThisTurn; }
public final CardCollectionView getClones() {
return CardCollection.getView(clones);
}
public final void setClones(final Iterable<Card> clones0) {
clones = clones0 == null ? null : new CardCollection(clones0);
}
public final void addClone(final Card c) {
if (clones == null) {
clones = new CardCollection();
}
clones.add(c);
}
public final void clearClones() {
clones = null;
}
public final Card getCloneOrigin() {
return cloneOrigin;
}
@@ -1067,7 +1051,8 @@ public class Card extends GameEntity implements Comparable<Card> {
}
public final boolean hasConverge() {
return "Count$Converge".equals(getSVar("X")) || "Count$Converge".equals(getSVar("Y")) || hasKeyword("Sunburst");
return "Count$Converge".equals(getSVar("X")) || "Count$Converge".equals(getSVar("Y")) ||
hasKeyword(Keyword.SUNBURST) || hasKeyword("Modular:Sunburst");
}
@Override
@@ -1463,250 +1448,261 @@ public class Card extends GameEntity implements Comparable<Card> {
int i = 0;
for (KeywordInterface inst : keywords) {
String keyword = inst.getOriginal();
if (keyword.startsWith("SpellCantTarget")) {
continue;
}
// format text changes
if (CardUtil.isKeywordModifiable(keyword)
&& keywordsGrantedByTextChanges.contains(inst)) {
for (final Entry<String, String> e : textChanges) {
final String value = e.getValue();
if (keyword.contains(value)) {
keyword = TextUtil.fastReplace(keyword, value,
TextUtil.concatNoSpace("<strike>", e.getKey(), "</strike> ", value));
// assume (for now) max one change per keyword
break;
try {
if (keyword.startsWith("SpellCantTarget")) {
continue;
}
// format text changes
if (CardUtil.isKeywordModifiable(keyword)
&& keywordsGrantedByTextChanges.contains(inst)) {
for (final Entry<String, String> e : textChanges) {
final String value = e.getValue();
if (keyword.contains(value)) {
keyword = TextUtil.fastReplace(keyword, value,
TextUtil.concatNoSpace("<strike>", e.getKey(), "</strike> ", value));
// assume (for now) max one change per keyword
break;
}
}
}
}
if (keyword.startsWith("CantBeCounteredBy")) {
final String[] p = keyword.split(":");
sbLong.append(p[2]).append("\r\n");
} else if (keyword.startsWith("etbCounter")) {
final String[] p = keyword.split(":");
final StringBuilder s = new StringBuilder();
if (p.length > 4) {
if (!"no desc".equals(p[4])) {
s.append(p[4]);
if (keyword.startsWith("CantBeCounteredBy") || keyword.startsWith("Panharmonicon")
|| keyword.startsWith("Dieharmonicon")) {
final String[] p = keyword.split(":");
sbLong.append(p[2]).append("\r\n");
} else if (keyword.startsWith("etbCounter")) {
final String[] p = keyword.split(":");
final StringBuilder s = new StringBuilder();
if (p.length > 4) {
if (!"no desc".equals(p[4])) {
s.append(p[4]);
}
} else {
s.append(getName());
s.append(" enters the battlefield with ");
s.append(Lang.nounWithNumeral(p[2], CounterType.valueOf(p[1]).getName() + " counter"));
s.append(" on it.");
}
} else {
s.append(getName());
s.append(" enters the battlefield with ");
s.append(Lang.nounWithNumeral(p[2], CounterType.valueOf(p[1]).getName() + " counter"));
s.append(" on it.");
}
sbLong.append(s).append("\r\n");
} else if (keyword.startsWith("Protection:")) {
final String[] k = keyword.split(":");
sbLong.append(k[2]).append("\r\n");
} else if (keyword.startsWith("Creatures can't attack unless their controller pays")) {
final String[] k = keyword.split(":");
if (!k[3].equals("no text")) {
sbLong.append(k[3]).append("\r\n");
}
} else if (keyword.startsWith("Enchant")) {
String k = keyword;
k = TextUtil.fastReplace(k, "Curse", "");
sbLong.append(k).append("\r\n");
} else if (keyword.startsWith("Ripple")) {
sbLong.append(TextUtil.fastReplace(keyword, ":", " ")).append("\r\n");
} else if (keyword.startsWith("Madness")) {
String[] parts = keyword.split(":");
// If no colon exists in Madness keyword, it must have been granted and assumed the cost from host
if (parts.length < 2) {
sbLong.append(parts[0]).append(" ").append(this.getManaCost()).append("\r\n");
} else {
sbLong.append(parts[0]).append(" ").append(ManaCostParser.parse(parts[1])).append("\r\n");
}
} else if (keyword.startsWith("Morph") || keyword.startsWith("Megamorph")) {
String[] k = keyword.split(":");
sbLong.append(k[0]);
if (k.length > 1) {
final Cost mCost = new Cost(k[1], true);
if (!mCost.isOnlyManaCost()) {
sbLong.append("");
sbLong.append(s).append("\r\n");
} else if (keyword.startsWith("Protection:")) {
final String[] k = keyword.split(":");
sbLong.append(k[2]).append("\r\n");
} else if (keyword.startsWith("Creatures can't attack unless their controller pays")) {
final String[] k = keyword.split(":");
if (!k[3].equals("no text")) {
sbLong.append(k[3]).append("\r\n");
}
if (mCost.isOnlyManaCost()) {
sbLong.append(" ");
} else if (keyword.startsWith("Enchant")) {
String k = keyword;
k = TextUtil.fastReplace(k, "Curse", "");
sbLong.append(k).append("\r\n");
} else if (keyword.startsWith("Ripple")) {
sbLong.append(TextUtil.fastReplace(keyword, ":", " ")).append("\r\n");
} else if (keyword.startsWith("Madness")) {
String[] parts = keyword.split(":");
// If no colon exists in Madness keyword, it must have been granted and assumed the cost from host
if (parts.length < 2) {
sbLong.append(parts[0]).append(" ").append(this.getManaCost()).append("\r\n");
} else {
sbLong.append(parts[0]).append(" ").append(ManaCostParser.parse(parts[1])).append("\r\n");
}
sbLong.append(mCost.toString()).delete(sbLong.length() - 2, sbLong.length());
if (!mCost.isOnlyManaCost()) {
sbLong.append(".");
} else if (keyword.startsWith("Morph") || keyword.startsWith("Megamorph")) {
String[] k = keyword.split(":");
sbLong.append(k[0]);
if (k.length > 1) {
final Cost mCost = new Cost(k[1], true);
if (!mCost.isOnlyManaCost()) {
sbLong.append("");
}
if (mCost.isOnlyManaCost()) {
sbLong.append(" ");
}
sbLong.append(mCost.toString()).delete(sbLong.length() - 2, sbLong.length());
if (!mCost.isOnlyManaCost()) {
sbLong.append(".");
}
sbLong.append(" (" + inst.getReminderText() + ")");
sbLong.append("\r\n");
}
} else if (keyword.startsWith("Emerge")) {
final String[] k = keyword.split(":");
sbLong.append(k[0]).append(" ").append(ManaCostParser.parse(k[1]));
sbLong.append(" (" + inst.getReminderText() + ")");
sbLong.append("\r\n");
}
} else if (keyword.startsWith("Emerge")) {
final String[] k = keyword.split(":");
sbLong.append(k[0]).append(" ").append(ManaCostParser.parse(k[1]));
sbLong.append(" (" + inst.getReminderText() + ")");
sbLong.append("\r\n");
} else if (keyword.startsWith("Echo")) {
sbLong.append("Echo ");
final String[] upkeepCostParams = keyword.split(":");
sbLong.append(upkeepCostParams.length > 2 ? "- " + upkeepCostParams[2] : ManaCostParser.parse(upkeepCostParams[1]));
sbLong.append(" (At the beginning of your upkeep, if CARDNAME came under your control since the beginning of your last upkeep, sacrifice it unless you pay its echo cost.)");
sbLong.append("\r\n");
} else if (keyword.startsWith("Cumulative upkeep")) {
sbLong.append("Cumulative upkeep ");
final String[] upkeepCostParams = keyword.split(":");
sbLong.append(upkeepCostParams.length > 2 ? "- " + upkeepCostParams[2] : ManaCostParser.parse(upkeepCostParams[1]));
sbLong.append("\r\n");
} else if (keyword.startsWith("Alternative Cost")) {
sbLong.append("Has alternative cost.");
} else if (keyword.startsWith("AlternateAdditionalCost")) {
final String costString1 = keyword.split(":")[1];
final String costString2 = keyword.split(":")[2];
final Cost cost1 = new Cost(costString1, false);
final Cost cost2 = new Cost(costString2, false);
sbLong.append("As an additional cost to cast ")
.append(getName()).append(", ")
.append(cost1.toSimpleString())
.append(" or pay ")
.append(cost2.toSimpleString())
.append(".\r\n");
} else if (keyword.startsWith("Multikicker")) {
if (!keyword.endsWith("Generic")) {
final String[] n = keyword.split(":");
final Cost cost = new Cost(n[1], false);
sbLong.append("Multikicker ").append(cost.toSimpleString());
sbLong.append(" (" + inst.getReminderText() + ")").append("\r\n");
}
} else if (keyword.startsWith("Kicker")) {
if (!keyword.endsWith("Generic")) {
final StringBuilder sbx = new StringBuilder();
final String[] n = keyword.split(":");
sbx.append("Kicker ");
final Cost cost = new Cost(n[1], false);
sbx.append(cost.toSimpleString());
if (Lists.newArrayList(n).size() > 2) {
sbx.append(" and/or ");
final Cost cost2 = new Cost(n[2], false);
sbx.append(cost2.toSimpleString());
} else if (keyword.startsWith("Echo")) {
sbLong.append("Echo ");
final String[] upkeepCostParams = keyword.split(":");
sbLong.append(upkeepCostParams.length > 2 ? "- " + upkeepCostParams[2] : ManaCostParser.parse(upkeepCostParams[1]));
sbLong.append(" (At the beginning of your upkeep, if CARDNAME came under your control since the beginning of your last upkeep, sacrifice it unless you pay its echo cost.)");
sbLong.append("\r\n");
} else if (keyword.startsWith("Cumulative upkeep")) {
sbLong.append("Cumulative upkeep ");
final String[] upkeepCostParams = keyword.split(":");
sbLong.append(upkeepCostParams.length > 2 ? "- " + upkeepCostParams[2] : ManaCostParser.parse(upkeepCostParams[1]));
sbLong.append("\r\n");
} else if (keyword.startsWith("Alternative Cost")) {
sbLong.append("Has alternative cost.");
} else if (keyword.startsWith("AlternateAdditionalCost")) {
final String costString1 = keyword.split(":")[1];
final String costString2 = keyword.split(":")[2];
final Cost cost1 = new Cost(costString1, false);
final Cost cost2 = new Cost(costString2, false);
sbLong.append("As an additional cost to cast ")
.append(getName()).append(", ")
.append(cost1.toSimpleString())
.append(" or pay ")
.append(cost2.toSimpleString())
.append(".\r\n");
} else if (keyword.startsWith("Multikicker")) {
if (!keyword.endsWith("Generic")) {
final String[] n = keyword.split(":");
final Cost cost = new Cost(n[1], false);
sbLong.append("Multikicker ").append(cost.toSimpleString());
sbLong.append(" (" + inst.getReminderText() + ")").append("\r\n");
}
sbx.append(" (" + inst.getReminderText() + ")");
sbLong.append(sbx).append("\r\n");
}
} else if (keyword.startsWith("Hexproof:")) {
final String k[] = keyword.split(":");
sbLong.append("Hexproof from ").append(k[2])
.append(" (").append(inst.getReminderText()).append(")").append("\r\n");
} else if (keyword.endsWith(".") && !keyword.startsWith("Haunt")) {
sbLong.append(keyword).append("\r\n");
} else if (keyword.startsWith("Presence") || keyword.startsWith("MayFlash")) {
// Pseudo keywords, only print Reminder
sbLong.append(inst.getReminderText());
} else if (keyword.contains("At the beginning of your upkeep, ")
&& keyword.contains(" unless you pay")) {
sbLong.append(keyword).append("\r\n");
} else if (keyword.startsWith("Strive") || keyword.startsWith("Escalate")
|| keyword.startsWith("ETBReplacement")
|| keyword.startsWith("CantBeBlockedBy ")
|| keyword.startsWith("Affinity")
|| keyword.equals("CARDNAME enters the battlefield tapped.")
|| keyword.startsWith("UpkeepCost")) {
} else if (keyword.equals("Provoke") || keyword.equals("Ingest") || keyword.equals("Unleash")
|| keyword.equals("Soulbond") || keyword.equals("Partner") || keyword.equals("Retrace")
|| keyword.equals("Living Weapon") || keyword.equals("Myriad") || keyword.equals("Exploit")
|| keyword.equals("Changeling") || keyword.equals("Delve")
|| keyword.equals("Split second")
|| keyword.equals("Suspend") // for the ones without amounnt
|| keyword.equals("Hideaway") || keyword.equals("Ascend")
|| keyword.equals("Totem armor") || keyword.equals("Battle cry")
|| keyword.equals("Devoid") || keyword.equals("Riot")){
sbLong.append(keyword + " (" + inst.getReminderText() + ")");
} else if (keyword.startsWith("Partner:")) {
final String[] k = keyword.split(":");
sbLong.append("Partner with " + k[1] + " (" + inst.getReminderText() + ")");
} else if (keyword.startsWith("Modular") || keyword.startsWith("Bloodthirst") || keyword.startsWith("Dredge")
|| keyword.startsWith("Fabricate") || keyword.startsWith("Soulshift") || keyword.startsWith("Bushido")
|| keyword.startsWith("Crew") || keyword.startsWith("Tribute") || keyword.startsWith("Absorb")
|| keyword.startsWith("Graft") || keyword.startsWith("Fading") || keyword.startsWith("Vanishing")
|| keyword.startsWith("Afterlife")
|| keyword.startsWith("Afflict") || keyword.startsWith ("Poisonous") || keyword.startsWith("Rampage")
|| keyword.startsWith("Renown") || keyword.startsWith("Annihilator") || keyword.startsWith("Devour")) {
final String[] k = keyword.split(":");
sbLong.append(k[0] + " " + k[1] + " (" + inst.getReminderText() + ")");
} else if (keyword.contains("Haunt")) {
sb.append("\r\nHaunt (");
if (isCreature()) {
sb.append("When this creature dies, exile it haunting target creature.");
} else {
sb.append("When this spell card is put into a graveyard after resolving, ");
sb.append("exile it haunting target creature.");
}
sb.append(")");
} else if (keyword.equals("Convoke") || keyword.equals("Dethrone")|| keyword.equals("Fear")
|| keyword.equals("Melee") || keyword.equals("Improvise")|| keyword.equals("Shroud")
|| keyword.equals("Banding") || keyword.equals("Intimidate")|| keyword.equals("Evolve")
|| keyword.equals("Exalted") || keyword.equals("Extort")|| keyword.equals("Flanking")
|| keyword.equals("Horsemanship") || keyword.equals("Infect")|| keyword.equals("Persist")
|| keyword.equals("Phasing") || keyword.equals("Shadow")|| keyword.equals("Skulk")
|| keyword.equals("Undying") || keyword.equals("Wither") || keyword.equals("Cascade")
|| keyword.equals("Mentor")) {
if (sb.length() != 0) {
sb.append("\r\n");
}
sb.append(keyword + " (" + inst.getReminderText() + ")");
} else if (keyword.endsWith(" offering")) {
String offeringType = keyword.split(" ")[0];
if (sb.length() != 0) {
sb.append("\r\n");
}
sbLong.append(keyword);
sbLong.append(" (" + Keyword.getInstance("Offering:"+ offeringType).getReminderText() + ")");
} else if (keyword.startsWith("Equip") || keyword.startsWith("Fortify") || keyword.startsWith("Outlast")
|| keyword.startsWith("Unearth") || keyword.startsWith("Scavenge") || keyword.startsWith("Spectacle")
|| keyword.startsWith("Evoke") || keyword.startsWith("Bestow") || keyword.startsWith("Dash")
|| keyword.startsWith("Surge") || keyword.startsWith("Transmute") || keyword.startsWith("Suspend")
|| keyword.equals("Undaunted") || keyword.startsWith("Monstrosity") || keyword.startsWith("Embalm")
|| keyword.startsWith("Level up") || keyword.equals("Prowess") || keyword.startsWith("Eternalize")
|| keyword.startsWith("Reinforce") || keyword.startsWith("Champion") || keyword.startsWith("Prowl")
|| keyword.startsWith("Amplify") || keyword.startsWith("Ninjutsu") || keyword.startsWith("Adapt")
|| keyword.startsWith("Cycling") || keyword.startsWith("TypeCycling")) {
// keyword parsing takes care of adding a proper description
} else if (keyword.startsWith("CantBeBlockedByAmount")) {
sbLong.append(getName()).append(" can't be blocked ");
sbLong.append(getTextForKwCantBeBlockedByAmount(keyword));
} else if (keyword.startsWith("CantBlock")) {
sbLong.append(getName()).append(" can't block ");
if (keyword.contains("CardUID")) {
sbLong.append("CardID (").append(Integer.valueOf(keyword.split("CantBlockCardUID_")[1])).append(")");
} else {
} else if (keyword.startsWith("Kicker")) {
if (!keyword.endsWith("Generic")) {
final StringBuilder sbx = new StringBuilder();
final String[] n = keyword.split(":");
sbx.append("Kicker ");
final Cost cost = new Cost(n[1], false);
sbx.append(cost.toSimpleString());
if (Lists.newArrayList(n).size() > 2) {
sbx.append(" and/or ");
final Cost cost2 = new Cost(n[2], false);
sbx.append(cost2.toSimpleString());
}
sbx.append(" (" + inst.getReminderText() + ")");
sbLong.append(sbx).append("\r\n");
}
} else if (keyword.startsWith("Hexproof:")) {
final String k[] = keyword.split(":");
sbLong.append("Hexproof from ").append(k[2])
.append(" (").append(inst.getReminderText()).append(")").append("\r\n");
} else if (keyword.endsWith(".") && !keyword.startsWith("Haunt")) {
sbLong.append(keyword).append("\r\n");
} else if (keyword.startsWith("Presence") || keyword.startsWith("MayFlash")) {
// Pseudo keywords, only print Reminder
sbLong.append(inst.getReminderText());
} else if (keyword.contains("At the beginning of your upkeep, ")
&& keyword.contains(" unless you pay")) {
sbLong.append(keyword).append("\r\n");
} else if (keyword.startsWith("Strive") || keyword.startsWith("Escalate")
|| keyword.startsWith("ETBReplacement")
|| keyword.startsWith("CantBeBlockedBy ")
|| keyword.startsWith("Affinity")
|| keyword.equals("CARDNAME enters the battlefield tapped.")
|| keyword.startsWith("UpkeepCost")) {
} else if (keyword.equals("Provoke") || keyword.equals("Ingest") || keyword.equals("Unleash")
|| keyword.equals("Soulbond") || keyword.equals("Partner") || keyword.equals("Retrace")
|| keyword.equals("Living Weapon") || keyword.equals("Myriad") || keyword.equals("Exploit")
|| keyword.equals("Changeling") || keyword.equals("Delve")
|| keyword.equals("Split second") || keyword.equals("Sunburst")
|| keyword.equals("Suspend") // for the ones without amounnt
|| keyword.equals("Hideaway") || keyword.equals("Ascend")
|| keyword.equals("Totem armor") || keyword.equals("Battle cry")
|| keyword.equals("Devoid") || keyword.equals("Riot")){
sbLong.append(keyword + " (" + inst.getReminderText() + ")");
} else if (keyword.startsWith("Partner:")) {
final String[] k = keyword.split(":");
sbLong.append(k.length > 1 ? k[1] + ".\r\n" : "");
sbLong.append("Partner with " + k[1] + " (" + inst.getReminderText() + ")");
} else if (keyword.startsWith("Modular") || keyword.startsWith("Bloodthirst") || keyword.startsWith("Dredge")
|| keyword.startsWith("Fabricate") || keyword.startsWith("Soulshift") || keyword.startsWith("Bushido")
|| keyword.startsWith("Crew") || keyword.startsWith("Tribute") || keyword.startsWith("Absorb")
|| keyword.startsWith("Graft") || keyword.startsWith("Fading") || keyword.startsWith("Vanishing")
|| keyword.startsWith("Afterlife")
|| keyword.startsWith("Afflict") || keyword.startsWith ("Poisonous") || keyword.startsWith("Rampage")
|| keyword.startsWith("Renown") || keyword.startsWith("Annihilator") || keyword.startsWith("Devour")) {
final String[] k = keyword.split(":");
sbLong.append(k[0] + " " + k[1] + " (" + inst.getReminderText() + ")");
} else if (keyword.contains("Haunt")) {
sb.append("\r\nHaunt (");
if (isCreature()) {
sb.append("When this creature dies, exile it haunting target creature.");
} else {
sb.append("When this spell card is put into a graveyard after resolving, ");
sb.append("exile it haunting target creature.");
}
sb.append(")");
} else if (keyword.equals("Convoke") || keyword.equals("Dethrone")|| keyword.equals("Fear")
|| keyword.equals("Melee") || keyword.equals("Improvise")|| keyword.equals("Shroud")
|| keyword.equals("Banding") || keyword.equals("Intimidate")|| keyword.equals("Evolve")
|| keyword.equals("Exalted") || keyword.equals("Extort")|| keyword.equals("Flanking")
|| keyword.equals("Horsemanship") || keyword.equals("Infect")|| keyword.equals("Persist")
|| keyword.equals("Phasing") || keyword.equals("Shadow")|| keyword.equals("Skulk")
|| keyword.equals("Undying") || keyword.equals("Wither") || keyword.equals("Cascade")
|| keyword.equals("Mentor")) {
if (sb.length() != 0) {
sb.append("\r\n");
}
sb.append(keyword + " (" + inst.getReminderText() + ")");
} else if (keyword.endsWith(" offering")) {
String offeringType = keyword.split(" ")[0];
if (sb.length() != 0) {
sb.append("\r\n");
}
sbLong.append(keyword);
sbLong.append(" (" + Keyword.getInstance("Offering:"+ offeringType).getReminderText() + ")");
} else if (keyword.startsWith("Equip") || keyword.startsWith("Fortify") || keyword.startsWith("Outlast")
|| keyword.startsWith("Unearth") || keyword.startsWith("Scavenge") || keyword.startsWith("Spectacle")
|| keyword.startsWith("Evoke") || keyword.startsWith("Bestow") || keyword.startsWith("Dash")
|| keyword.startsWith("Surge") || keyword.startsWith("Transmute") || keyword.startsWith("Suspend")
|| keyword.equals("Undaunted") || keyword.startsWith("Monstrosity") || keyword.startsWith("Embalm")
|| keyword.startsWith("Level up") || keyword.equals("Prowess") || keyword.startsWith("Eternalize")
|| keyword.startsWith("Reinforce") || keyword.startsWith("Champion") || keyword.startsWith("Prowl")
|| keyword.startsWith("Amplify") || keyword.startsWith("Ninjutsu") || keyword.startsWith("Adapt")
|| keyword.startsWith("Cycling") || keyword.startsWith("TypeCycling")) {
// keyword parsing takes care of adding a proper description
} else if (keyword.startsWith("CantBeBlockedByAmount")) {
sbLong.append(getName()).append(" can't be blocked ");
sbLong.append(getTextForKwCantBeBlockedByAmount(keyword));
} else if (keyword.startsWith("CantBlock")) {
sbLong.append(getName()).append(" can't block ");
if (keyword.contains("CardUID")) {
sbLong.append("CardID (").append(Integer.valueOf(keyword.split("CantBlockCardUID_")[1])).append(")");
} else {
final String[] k = keyword.split(":");
sbLong.append(k.length > 1 ? k[1] + ".\r\n" : "");
}
} else if (keyword.equals("Unblockable")) {
sbLong.append(getName()).append(" can't be blocked.\r\n");
} else if (keyword.equals("AllNonLegendaryCreatureNames")) {
sbLong.append(getName()).append(" has all names of nonlegendary creature cards.\r\n");
} else if (keyword.startsWith("IfReach")) {
String k[] = keyword.split(":");
sbLong.append(getName()).append(" can block ")
.append(CardType.getPluralType(k[1]))
.append(" as though it had reach.\r\n");
} else if (keyword.startsWith("MayEffectFromOpeningHand")) {
final String[] k = keyword.split(":");
// need to get SpellDescription from Svar
String desc = AbilityFactory.getMapParams(getSVar(k[1])).get("SpellDescription");
sbLong.append(desc);
} else if (keyword.startsWith("Saga")) {
String k[] = keyword.split(":");
String desc = "(As this Saga enters and after your draw step, "
+ " add a lore counter. Sacrifice after " + Strings.repeat("I", Integer.valueOf(k[1])) + ".)";
sbLong.append(desc);
}
} else if (keyword.equals("Unblockable")) {
sbLong.append(getName()).append(" can't be blocked.\r\n");
} else if (keyword.equals("AllNonLegendaryCreatureNames")) {
sbLong.append(getName()).append(" has all names of nonlegendary creature cards.\r\n");
} else if (keyword.startsWith("IfReach")) {
String k[] = keyword.split(":");
sbLong.append(getName()).append(" can block ")
.append(CardType.getPluralType(k[1]))
.append(" as though it had reach.\r\n");
} else if (keyword.startsWith("MayEffectFromOpeningHand")) {
final String[] k = keyword.split(":");
// need to get SpellDescription from Svar
String desc = AbilityFactory.getMapParams(getSVar(k[1])).get("SpellDescription");
sbLong.append(desc);
} else if (keyword.startsWith("Saga")) {
String k[] = keyword.split(":");
String desc = "(As this Saga enters and after your draw step, "
+ " add a lore counter. Sacrifice after " + Strings.repeat("I", Integer.valueOf(k[1])) + ".)";
sbLong.append(desc);
}
else {
if ((i != 0) && (sb.length() != 0)) {
sb.append(", ");
else {
if ((i != 0) && (sb.length() != 0)) {
sb.append(", ");
}
sb.append(keyword);
}
if (sbLong.length() > 0) {
sbLong.append("\r\n");
}
sb.append(keyword);
}
if (sbLong.length() > 0) {
sbLong.append("\r\n");
}
i++;
i++;
} catch (Exception e) {
String msg = "Card:keywordToText: crash in Keyword parsing";
Sentry.getContext().recordBreadcrumb(
new BreadcrumbBuilder().setMessage(msg)
.withData("Card", this.getName()).withData("Keyword", keyword).build()
);
throw new RuntimeException("Error in Card " + this.getName() + " with Keyword " + keyword, e);
}
}
if (sb.length() > 0) {
sb.append("\r\n");
@@ -2249,6 +2245,16 @@ public class Card extends GameEntity implements Comparable<Card> {
updateBasicLandAbilities(list, state);
}
// add Facedown abilities from Original state but only if this state is face down
// need CardStateView#getState or might crash in StackOverflow
if ((mana == null || mana == false) && isFaceDown() && state.getView().getState() == CardStateName.FaceDown) {
for (SpellAbility sa : getState(CardStateName.Original).getNonManaAbilities()) {
if (sa.isManifestUp() || sa.isMorphUp()) {
list.add(sa);
}
}
}
for (KeywordInterface kw : getUnhiddenKeywords(state)) {
for (SpellAbility sa : kw.getAbilities()) {
if (mana == null || mana == sa.isManaAbility()) {
@@ -3232,11 +3238,15 @@ public class Card extends GameEntity implements Comparable<Card> {
}
public final void tap() {
tap(false);
}
public final void tap(boolean attacker) {
if (tapped) { return; }
// Run triggers
final Map<String, Object> runParams = Maps.newTreeMap();
runParams.put("Card", this);
runParams.put("Attacker", attacker);
getGame().getTriggerHandler().runTrigger(TriggerType.Taps, runParams, false);
setTapped(true);
@@ -3395,6 +3405,15 @@ public class Card extends GameEntity implements Comparable<Card> {
return change;
}
public final boolean hasChangedCardKeywords(final long timestamp) {
return changedCardKeywords.containsKey(timestamp);
}
public final void addChangedCardKeywordsInternal(final KeywordsChange change, final long timestamp) {
changedCardKeywords.put(timestamp, change);
updateKeywordsCache(currentState);
}
// Hidden keywords will be left out
public final Collection<KeywordInterface> getUnhiddenKeywords() {
return getUnhiddenKeywords(currentState);
@@ -3643,10 +3662,8 @@ public class Card extends GameEntity implements Comparable<Card> {
if (s.startsWith("HIDDEN")) {
removeHiddenExtrinsicKeyword(s);
}
else {
if (extrinsicKeyword.remove(s)) {
currentState.getView().updateKeywords(this, currentState);
}
else if (extrinsicKeyword.remove(s)) {
currentState.getView().updateKeywords(this, currentState);
}
}
@@ -4812,6 +4829,15 @@ public class Card extends GameEntity implements Comparable<Card> {
// Note: This should only be called after state has been set to CardStateName.FaceDown,
// so the below call should be valid since the state should have been created already.
getState(CardStateName.FaceDown).setImageKey(ImageKeys.getTokenKey(image));
if (!manifested) {
// remove Manifest Up abilities from Original State
CardState original = getState(CardStateName.Original);
for (SpellAbility sa : original.getNonManaAbilities()) {
if (sa.isManifestUp()) {
original.removeSpellAbility(sa);
}
}
}
}
public final void animateBestow() {
@@ -5715,7 +5741,7 @@ public class Card extends GameEntity implements Comparable<Card> {
public void setChangedCardKeywords(Map<Long, KeywordsChange> changedCardKeywords) {
this.changedCardKeywords.clear();
for (Entry<Long, KeywordsChange> entry : changedCardKeywords.entrySet()) {
this.changedCardKeywords.put(entry.getKey(), entry.getValue());
this.changedCardKeywords.put(entry.getKey(), entry.getValue().copy(this, true));
}
}

View File

@@ -19,6 +19,13 @@ import forge.game.trigger.TriggerType;
public class CardDamageMap extends ForwardingTable<Card, GameEntity, Integer> {
private Table<Card, GameEntity, Integer> dataMap = HashBasedTable.create();
public CardDamageMap(Table<Card, GameEntity, Integer> damageMap) {
this.putAll(damageMap);
}
public CardDamageMap() {
}
public void triggerPreventDamage(boolean isCombat) {
for (Map.Entry<GameEntity, Map<Card, Integer>> e : this.columnMap().entrySet()) {
int sum = 0;

View File

@@ -25,8 +25,6 @@ import forge.card.mana.ManaCost;
import forge.game.Game;
import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.ability.effects.CharmEffect;
import forge.game.cost.Cost;
import forge.game.player.Player;
import forge.game.replacement.ReplacementHandler;
@@ -95,7 +93,6 @@ public class CardFactory {
out.setAttachedCards(in.getAttachedCards());
out.setEntityAttachedTo(in.getEntityAttachedTo());
out.setClones(in.getClones());
out.setCastSA(in.getCastSA());
for (final Object o : in.getRemembered()) {
out.addRemembered(o);
@@ -431,6 +428,9 @@ public class CardFactory {
private static void readCardFace(Card c, ICardFace face) {
// Name first so Senty has the Card name
c.setName(face.getName());
for (String r : face.getReplacements()) c.addReplacementEffect(ReplacementHandler.parseReplacement(r, c, true));
for (String s : face.getStaticAbilities()) c.addStaticAbility(s);
for (String t : face.getTriggers()) c.addTrigger(TriggerHandler.parseTrigger(t, c, true));
@@ -440,7 +440,6 @@ public class CardFactory {
// keywords not before variables
c.addIntrinsicKeywords(face.getKeywords(), false);
c.setName(face.getName());
c.setManaCost(face.getManaCost());
c.setText(face.getNonAbilityText());
@@ -618,6 +617,9 @@ public class CardFactory {
}
if (from.getRestrictions() != null) {
to.setRestrictions((SpellAbilityRestriction) from.getRestrictions().copy());
if (!lki) {
to.getRestrictions().resetTurnActivations();
}
}
if (from.getConditions() != null) {
to.setConditions((SpellAbilityCondition) from.getConditions().copy());
@@ -676,9 +678,6 @@ public class CardFactory {
}
trig.setStackDescription(trig.toString());
if (trig.getApi() == ApiType.Charm && !trig.isWrapper()) {
CharmEffect.makeChoices(trig);
}
WrappedAbility wrapperAbility = new WrappedAbility(t, trig, ((WrappedAbility) sa).getDecider());
wrapperAbility.setTrigger(true);

View File

@@ -25,7 +25,6 @@ import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import forge.GameCommand;
import forge.card.*;
import forge.card.mana.ManaAtom;
import forge.card.mana.ManaCost;
@@ -136,27 +135,27 @@ public class CardFactoryUtil {
public static SpellAbility abilityMorphUp(final Card sourceCard, final String costStr, final boolean mega) {
Cost cost = new Cost(costStr, true);
String costDesc = cost.toString();
// get rid of the ": " at the end
costDesc = costDesc.substring(0, costDesc.length() - 2);
StringBuilder sbCost = new StringBuilder(mega ? "Megamorph" : "Morph");
sbCost.append(" ");
if (!cost.isOnlyManaCost()) {
costDesc = "" + costDesc;
sbCost.append(" ");
}
// get rid of the ": " at the end
sbCost.append(costDesc.substring(0, costDesc.length() - 2));
String ab = "ST$ SetState | Cost$ " + costStr + " | CostDesc$ Morph" + costDesc
+ " | MorphUp$ True"
+ " | ConditionDefined$ Self | ConditionPresent$ Card.faceDown"
+ " | Mode$ TurnFace | SpellDescription$ (Turn this face up any time for its morph cost.)";
StringBuilder sb = new StringBuilder();
sb.append("ST$ SetState | Cost$ ").append(costStr).append(" | CostDesc$ ").append(sbCost);
sb.append(" | MorphUp$ True | Secondary$ True | IsPresent$ Card.Self+faceDown");
if (mega) {
ab += " | Mega$ True";
sb.append(" | Mega$ True");
}
sb.append(" | Mode$ TurnFace | SpellDescription$ (Turn this face up any time for its morph cost.)");
final SpellAbility morphUp = AbilityFactory.getAbility(ab, sourceCard);
final SpellAbility morphUp = AbilityFactory.getAbility(sb.toString(), sourceCard);
final StringBuilder sbStack = new StringBuilder();
sbStack.append(sourceCard.getName()).append(" - turn this card face up.");
morphUp.setStackDescription(sbStack.toString());
morphUp.setIsMorphUp(true);
return morphUp;
}
@@ -166,18 +165,17 @@ public class CardFactoryUtil {
String costDesc = manaCost.toString();
// Cost need to be set later
String ab = "ST$ SetState | Cost$ 0 | CostDesc$ Unmanifest " + costDesc
+ " | ManifestUp$ True"
+ " | ConditionDefined$ Self | ConditionPresent$ Card.faceDown+manifested"
+ " | Mode$ TurnFace | SpellDescription$ (Turn this face up any time for its mana cost.)";
StringBuilder sb = new StringBuilder();
sb.append("ST$ SetState | Cost$ 0 | CostDesc$ Unmanifest ").append(costDesc);
sb.append(" | ManifestUp$ True | Secondary$ True | IsPresent$ Card.Self+faceDown+manifested");
sb.append(" | Mode$ TurnFace | SpellDescription$ (Turn this face up any time for its mana cost.)");
final SpellAbility manifestUp = AbilityFactory.getAbility(ab, sourceCard);
final SpellAbility manifestUp = AbilityFactory.getAbility(sb.toString(), sourceCard);
manifestUp.setPayCosts(new Cost(manaCost, true));
final StringBuilder sbStack = new StringBuilder();
sbStack.append(sourceCard.getName()).append(" - turn this card face up.");
manifestUp.setStackDescription(sbStack.toString());
manifestUp.setIsManifestUp(true);
return manifestUp;
}
@@ -2100,7 +2098,7 @@ public class CardFactoryUtil {
String abStr = "DB$ PutCounter | Defined$ Self | CounterType$ " + splitkw[1]
+ " | ETB$ True | CounterNum$ " + amount;
if (!StringUtils.isNumeric(amount)) {
if (!StringUtils.isNumeric(amount) && card.hasSVar(amount)) {
abStr += " | References$ " + amount;
}
@@ -3453,12 +3451,10 @@ public class CardFactoryUtil {
sb.append(m);
sb.append(" (").append(inst.getReminderText()).append(")");
if ("Sunburst".equals(m)) {
card.setSVar(m, "Count$Converge");
}
final ReplacementEffect re = makeEtbCounter(sb.toString(), card, intrinsic);
if ("Sunburst".equals(m)) {
re.getOverridingAbility().setSVar("Sunburst", "Count$Converge");
}
inst.addReplacement(re);
} else if (keyword.equals("Rebound")) {
String repeffstr = "Event$ Moved | ValidCard$ Card.Self+wasCastFromHand+YouOwn+YouCtrl "
@@ -3513,6 +3509,19 @@ public class CardFactoryUtil {
String sb = "etbCounter:LORE:1:no Condition:no desc";
final ReplacementEffect re = makeEtbCounter(sb, card, intrinsic);
inst.addReplacement(re);
} else if (keyword.equals("Sunburst")) {
// Rule 702.43a If this object is entering the battlefield as a creature,
// ignoring any type-changing effects that would affect it
CounterType t = card.isCreature() ? CounterType.P1P1 : CounterType.CHARGE;
StringBuilder sb = new StringBuilder("etbCounter:");
sb.append(t).append(":Sunburst:no Condition:");
sb.append("Sunburst (").append(inst.getReminderText()).append(")");
final ReplacementEffect re = makeEtbCounter(sb.toString(), card, intrinsic);
re.getOverridingAbility().setSVar("Sunburst", "Count$Converge");
inst.addReplacement(re);
} else if (keyword.equals("Totem armor")) {
String repeffstr = "Event$ Destroy | ActiveZones$ Battlefield | ValidCard$ Card.EnchantedBy"
@@ -3704,13 +3713,24 @@ public class CardFactoryUtil {
final String[] k = keyword.split(":");
final String magnitude = k[1];
final String manacost = k[2];
final String reduceCost = k.length > 3 ? k[3] : null;
Set<String> references = Sets.newHashSet();
String desc = "Adapt " + magnitude;
String effect = "AB$ PutCounter | Cost$ " + manacost + " | ConditionPresent$ "
+ "Card.Self+counters_EQ0_P1P1 | Adapt$ True | CounterNum$ " + magnitude
String effect = "AB$ PutCounter | Cost$ " + manacost + " | Adapt$ True | CounterNum$ " + magnitude
+ " | CounterType$ P1P1 | StackDescription$ SpellDescription";
if (reduceCost != null) {
effect += "| ReduceCost$ " + reduceCost;
references.add(reduceCost);
desc += ". This ability costs {1} less to activate for each instant and sorcery card in your graveyard.";
}
if (!references.isEmpty()) {
effect += "| References$ " + TextUtil.join(references, ",");
}
effect += "| SpellDescription$ " + desc + " (" + inst.getReminderText() + ")";
final SpellAbility sa = AbilityFactory.getAbility(effect, card);
@@ -4037,22 +4057,12 @@ public class CardFactoryUtil {
final String[] k = keyword.split(":");
inst.addSpellAbility(abilityMorphDown(card));
CardState state = card.getState(CardStateName.FaceDown);
state.setSVars(card.getSVars());
KeywordInterface facedownKeyword = Keyword.getInstance("");
facedownKeyword.addSpellAbility(abilityMorphUp(card, k[1], false));
state.addIntrinsicKeywords(Lists.newArrayList(facedownKeyword));
inst.addSpellAbility(abilityMorphUp(card, k[1], false));
} else if (keyword.startsWith("Megamorph")){
final String[] k = keyword.split(":");
inst.addSpellAbility(abilityMorphDown(card));
CardState state = card.getState(CardStateName.FaceDown);
state.setSVars(card.getSVars());
KeywordInterface facedownKeyword = Keyword.getInstance("");
facedownKeyword.addSpellAbility(abilityMorphUp(card, k[1], true));
state.addIntrinsicKeywords(Lists.newArrayList(facedownKeyword));
inst.addSpellAbility(abilityMorphUp(card, k[1], true));
} else if (keyword.startsWith("Multikicker")) {
final String[] n = keyword.split(":");
final SpellAbility sa = card.getFirstSpellAbility();
@@ -4182,18 +4192,17 @@ public class CardFactoryUtil {
String effect = "AB$ PutCounter | Cost$ " + manacost + " ExileFromGrave<1/CARDNAME> " +
"| ActivationZone$ Graveyard | ValidTgts$ Creature | CounterType$ P1P1 " +
"| CounterNum$ ScavengeX | SorcerySpeed$ True | References$ ScavengeX " +
"| CounterNum$ ScavengeX | SorcerySpeed$ True " +
"| PrecostDesc$ Scavenge | CostDesc$ " + ManaCostParser.parse(manacost) +
"| SpellDescription$ (" + inst.getReminderText() + ")";
card.setSVar("ScavengeX", "Count$CardPower");
final SpellAbility sa = AbilityFactory.getAbility(effect, card);
sa.setSVar("ScavengeX", "Count$CardPower");
sa.setIntrinsic(intrinsic);
sa.setTemporary(!intrinsic);
inst.addSpellAbility(sa);
} else if (keyword.startsWith("Spectacle")) {
final String[] k = keyword.split(":");
final Cost cost = new Cost(k[1], false);
@@ -4211,28 +4220,6 @@ public class CardFactoryUtil {
newSA.setTemporary(!intrinsic);
inst.addSpellAbility(newSA);
} else if (keyword.equals("Sunburst") && intrinsic) {
final GameCommand sunburstCIP = new GameCommand() {
private static final long serialVersionUID = 1489845860231758299L;
@Override
public void run() {
CounterType t = card.isCreature() ? CounterType.P1P1 : CounterType.CHARGE;
card.addCounter(t, card.getSunburstValue(), card.getController(), true);
}
};
final GameCommand sunburstLP = new GameCommand() {
private static final long serialVersionUID = -7564420917490677427L;
@Override
public void run() {
card.setSunburstValue(0);
}
};
card.addComesIntoPlayCommand(sunburstCIP);
card.addLeavesPlayCommand(sunburstLP);
} else if (keyword.startsWith("Surge")) {
final String[] k = keyword.split(":");
final Cost surgeCost = new Cost(k[1], false);

View File

@@ -1239,7 +1239,7 @@ public class CardProperty {
} else if (property.startsWith("greatestPower")) {
CardCollectionView cards = CardLists.filter(game.getCardsIn(ZoneType.Battlefield), Presets.CREATURES);
if (property.contains("ControlledBy")) {
FCollectionView<Player> p = AbilityUtils.getDefinedPlayers(source, property.split("ControlledBy")[1], null);
FCollectionView<Player> p = AbilityUtils.getDefinedPlayers(source, property.split("ControlledBy")[1], spellAbility);
cards = CardLists.filterControlledBy(cards, p);
if (!cards.contains(card)) {
return false;

View File

@@ -43,6 +43,9 @@ import java.util.Collection;
import java.util.List;
import java.util.Map;
import io.sentry.Sentry;
import io.sentry.event.BreadcrumbBuilder;
public class CardState extends GameObject {
private String name = "";
private CardType type = new CardType();
@@ -69,6 +72,10 @@ public class CardState extends GameObject {
private final CardStateView view;
private final Card card;
public CardState(Card card, CardStateName name) {
this(card.getView().createAlternateState(name), card);
}
public CardState(CardStateView view0, Card card0) {
view = view0;
card = card0;
@@ -209,7 +216,19 @@ public class CardState extends GameObject {
if (s.trim().length() == 0) {
return null;
}
KeywordInterface inst = intrinsicKeywords.add(s);
KeywordInterface inst = null;
try {
inst = intrinsicKeywords.add(s);
} catch (Exception e) {
String msg = "CardState:addIntrinsicKeyword: failed to parse Keyword";
Sentry.getContext().recordBreadcrumb(
new BreadcrumbBuilder().setMessage(msg)
.withData("Card", card.getName()).withData("Keyword", s).build()
);
//rethrow
throw new RuntimeException("Error in Keyword " + s + " for card " + card.getName(), e);
}
if (inst != null && initTraits) {
inst.createTraits(card, true);
}

View File

@@ -269,7 +269,6 @@ public final class CardUtil {
newCopy.setAttachedCards(in.getAttachedCards());
newCopy.setEntityAttachedTo(in.getEntityAttachedTo());
newCopy.setClones(in.getClones());
newCopy.setHaunting(in.getHaunting());
newCopy.setCopiedPermanent(in.getCopiedPermanent());
for (final Card haunter : in.getHauntedBy()) {

View File

@@ -11,6 +11,7 @@ import com.google.common.collect.Maps;
import com.google.common.collect.Table;
import forge.game.Game;
import forge.game.spellability.SpellAbility;
import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType;
@@ -45,4 +46,39 @@ public class CardZoneTable extends ForwardingTable<ZoneType, ZoneType, CardColle
game.getTriggerHandler().runTrigger(TriggerType.ChangesZoneAll, runParams, false);
}
}
public CardCollection filterCards(Iterable<ZoneType> origin, ZoneType destination, String valid, Card host, SpellAbility sa) {
CardCollection allCards = new CardCollection();
if (destination != null) {
if (!containsColumn(destination)) {
return allCards;
}
}
if (origin != null) {
for (ZoneType z : origin) {
if (containsRow(z)) {
if (destination != null) {
allCards.addAll(row(z).get(destination));
} else {
for (CardCollection c : row(z).values()) {
allCards.addAll(c);
}
}
}
}
} else if (destination != null) {
for (CardCollection c : column(destination).values()) {
allCards.addAll(c);
}
} else {
for (CardCollection c : values()) {
allCards.addAll(c);
}
}
if (valid != null) {
allCards = CardLists.getValidCards(allCards, valid.split(","), host.getController(), host, sa);
}
return allCards;
}
}

View File

@@ -816,6 +816,7 @@ public class Combat {
}
preventMap.triggerPreventDamage(true);
preventMap.clear();
// This was deeper before, but that resulted in the stack entry acting like before.
// Run the trigger to deal combat damage once

View File

@@ -366,10 +366,9 @@ public class CostAdjustment {
if (manaCost.toString().equals("{0}")) {
return 0;
}
final Map<String, String> params = staticAbility.getMapParams();
final Card hostCard = staticAbility.getHostCard();
final Card card = sa.getHostCard();
final String amount = params.get("Amount");
final String amount = staticAbility.getParam("Amount");
if (!checkRequirement(sa, staticAbility)) {
return 0;
@@ -380,14 +379,16 @@ public class CostAdjustment {
value = CardFactoryUtil.xCount(card, hostCard.getSVar(amount));
} else if ("Undaunted".equals(amount)) {
value = card.getController().getOpponents().size();
} else if (staticAbility.hasParam("Relative")) {
value = AbilityUtils.calculateAmount(hostCard, amount, sa);
} else {
value = AbilityUtils.calculateAmount(hostCard, amount, staticAbility);
}
if (!params.containsKey("Cost") && ! params.containsKey("Color")) {
if (!staticAbility.hasParam("Cost") && ! staticAbility.hasParam("Color")) {
int minMana = 0;
if (params.containsKey("MinMana")) {
minMana = Integer.valueOf(params.get("MinMana"));
if (staticAbility.hasParam("MinMana")) {
minMana = Integer.valueOf(staticAbility.getParam("MinMana"));
}
final int maxReduction = Math.max(0, manaCost.getConvertedManaCost() - minMana);
@@ -395,7 +396,7 @@ public class CostAdjustment {
return Math.min(value, maxReduction);
}
} else {
final String color = params.containsKey("Cost") ? params.get("Cost") : params.get("Color");
final String color = staticAbility.getParamOrDefault("Cost", staticAbility.getParam("Color"));
int sumGeneric = 0;
// might be problematic for wierd hybrid combinations
for (final String cost : color.split(" ")) {

View File

@@ -23,7 +23,7 @@ import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
/**
* The Class CostPayLife.
* The Class CostDamage.
*/
public class CostDamage extends CostPart {
@@ -74,6 +74,8 @@ public class CostDamage extends CostPart {
preventMap.triggerPreventDamage(false);
damageMap.triggerDamageDoneOnce(false, sa);
preventMap.clear();
damageMap.clear();
return decision.c > 0;
}

View File

@@ -25,7 +25,7 @@ import java.util.ArrayList;
import java.util.List;
/**
* The Class CostPayLife.
* The Class CostDraw.
*/
public class CostDraw extends CostPart {
/**

View File

@@ -18,7 +18,6 @@
package forge.game.cost;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
@@ -57,17 +56,6 @@ public class CostPayLife extends CostPart {
return sb.toString();
}
/*
* (non-Javadoc)
*
* @see forge.card.cost.CostPart#refund(forge.Card)
*/
@Override
public final void refund(final Card source) {
// Really should be activating player
source.getController().payLife(this.paidAmount * -1, null);
}
/*
* (non-Javadoc)
*

View File

@@ -8,6 +8,8 @@ import java.util.Iterator;
import com.google.common.collect.Multimap;
import com.google.common.collect.MultimapBuilder;
import forge.game.card.Card;
public class KeywordCollection implements Iterable<String>, Serializable {
private static final long serialVersionUID = -2882986558147844702L;
@@ -151,6 +153,12 @@ public class KeywordCollection implements Iterable<String>, Serializable {
return map.get(keyword);
}
public void setHostCard(final Card host) {
for (KeywordInterface k : map.values()) {
k.setHostCard(host);
}
}
@Override
public Iterator<String> iterator() {
return new Iterator<String>() {

View File

@@ -202,7 +202,7 @@ public abstract class KeywordInstance<T extends KeywordInstance<?>> implements K
public Collection<StaticAbility> getStaticAbilities() {
return staticAbilities;
}
/*
* (non-Javadoc)
* @see forge.game.keyword.KeywordInterface#copy()
@@ -233,7 +233,7 @@ public abstract class KeywordInstance<T extends KeywordInstance<?>> implements K
return result;
} catch (final Exception ex) {
throw new RuntimeException("KeywordInstance : clone() error, " + ex);
throw new RuntimeException("KeywordInstance : clone() error", ex);
}
}
@@ -252,4 +252,26 @@ public abstract class KeywordInstance<T extends KeywordInstance<?>> implements K
public boolean redundant(Collection<KeywordInterface> list) {
return !list.isEmpty() && keyword.isMultipleRedundant;
}
/* (non-Javadoc)
* @see forge.game.keyword.KeywordInterface#setHostCard(forge.game.card.Card)
*/
@Override
public void setHostCard(Card host) {
for (SpellAbility sa : this.abilities) {
sa.setHostCard(host);
}
for (Trigger tr : this.triggers) {
tr.setHostCard(host);
}
for (ReplacementEffect re : this.replacements) {
re.setHostCard(host);
}
for (StaticAbility sa : this.staticAbilities) {
sa.setHostCard(host);
}
}
}

View File

@@ -31,6 +31,7 @@ public interface KeywordInterface extends Cloneable {
public void addSpellAbility(final SpellAbility s);
public void addStaticAbility(final StaticAbility st);
public void setHostCard(final Card host);
/**
* @return the triggers

View File

@@ -30,12 +30,11 @@ import forge.game.card.Card;
* </p>
*
* @author Forge
* @version $Id: KeywordsChange.java 27095 2014-08-17 07:32:24Z elcnesh $
*/
public class KeywordsChange {
private final KeywordCollection keywords = new KeywordCollection();
private final List<KeywordInterface> removeKeywordInterfaces = Lists.newArrayList();
private final List<String> removeKeywords = Lists.newArrayList();
public class KeywordsChange implements Cloneable {
private KeywordCollection keywords = new KeywordCollection();
private List<KeywordInterface> removeKeywordInterfaces = Lists.newArrayList();
private List<String> removeKeywords = Lists.newArrayList();
private boolean removeAllKeywords;
private boolean removeIntrinsicKeywords;
@@ -63,7 +62,7 @@ public class KeywordsChange {
this.removeAllKeywords = removeAll;
this.removeIntrinsicKeywords = removeIntrinsic;
}
public KeywordsChange(
final Collection<KeywordInterface> keywordList,
final Collection<KeywordInterface> removeKeywordInterfaces,
@@ -172,4 +171,49 @@ public class KeywordsChange {
removeIntrinsicKeywords = true;
}
}
public void setHostCard(final Card host) {
keywords.setHostCard(host);
for (KeywordInterface k : removeKeywordInterfaces) {
k.setHostCard(host);
}
}
public KeywordsChange copy(final Card host, final boolean lki) {
try {
KeywordsChange result = (KeywordsChange)super.clone();
result.keywords = new KeywordCollection();
for (KeywordInterface ki : this.keywords.getValues()) {
result.keywords.insert(ki.copy(host, lki));
}
result.removeKeywords = Lists.newArrayList(removeKeywords);
result.removeKeywordInterfaces = Lists.newArrayList();
for (KeywordInterface ki : this.removeKeywordInterfaces) {
removeKeywordInterfaces.add(ki.copy(host, lki));
}
return result;
} catch (final Exception ex) {
throw new RuntimeException("KeywordsChange : clone() error", ex);
}
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("<+");
sb.append(this.keywords);
sb.append("|-");
sb.append(this.removeKeywordInterfaces);
sb.append("|-");
sb.append(this.removeKeywords);
sb.append(">");
return sb.toString();
}
}

View File

@@ -526,7 +526,7 @@ public class PhaseHandler implements java.io.Serializable {
if (canAttack) {
if (shouldTapForAttack) {
attacker.tap();
attacker.tap(true);
}
} else {
combat.removeFromCombat(attacker);

View File

@@ -515,14 +515,15 @@ public class Player extends GameEntity implements Comparable<Player> {
return false;
}
if (lifePayment <= 0)
return true;
loseLife(lifePayment);
// rule 118.8
if (life >= lifePayment) {
return (loseLife(lifePayment) > 0);
}
return false;
// Run triggers
final Map<String, Object> runParams = Maps.newHashMap();
runParams.put("Player", this);
runParams.put("LifeAmount", lifePayment);
game.getTriggerHandler().runTrigger(TriggerType.PayLife, runParams, false);
return true;
}
public final boolean canPayEnergy(final int energyPayment) {
@@ -1261,42 +1262,6 @@ public class Player extends GameEntity implements Comparable<Player> {
return drawCards(1);
}
public void scry(final int numScry, SpellAbility cause) {
final CardCollection topN = new CardCollection(this.getCardsIn(ZoneType.Library, numScry));
if (topN.isEmpty()) {
return;
}
final ImmutablePair<CardCollection, CardCollection> lists = getController().arrangeForScry(topN);
final CardCollection toTop = lists.getLeft();
final CardCollection toBottom = lists.getRight();
int numToBottom = 0;
int numToTop = 0;
if (toBottom != null) {
for(Card c : toBottom) {
getGame().getAction().moveToBottomOfLibrary(c, cause, null);
numToBottom++;
}
}
if (toTop != null) {
Collections.reverse(toTop); // the last card in list will become topmost in library, have to revert thus.
for(Card c : toTop) {
getGame().getAction().moveToLibrary(c, cause, null);
numToTop++;
}
}
getGame().fireEvent(new GameEventScry(this, numToTop, numToBottom));
final Map<String, Object> runParams = Maps.newHashMap();
runParams.put("Player", this);
getGame().getTriggerHandler().runTrigger(TriggerType.Scry, runParams, false);
}
public void surveil(int num, SpellAbility cause) {
final Map<String, Object> repParams = Maps.newHashMap();

View File

@@ -13,8 +13,8 @@ public enum PlayerActionConfirmMode {
ChangeZoneGeneral,
BidLife,
OptionalChoose,
Tribute;
Tribute,
// Ripple;
;
}
}

View File

@@ -81,6 +81,9 @@ public abstract class PlayerController {
public Player getPlayer() { return player; }
public LobbyPlayer getLobbyPlayer() { return lobbyPlayer; }
public void tempShowCards(final Iterable<Card> cards) { } // show cards in UI until ended
public void endTempShowCards() { }
public final SpellAbility getAbilityToPlay(final Card hostCard, final List<SpellAbility> abilities) { return getAbilityToPlay(hostCard, abilities, null); }
public abstract SpellAbility getAbilityToPlay(Card hostCard, List<SpellAbility> abilities, ITriggerEvent triggerEvent);
@@ -111,7 +114,8 @@ public abstract class PlayerController {
public abstract SpellAbility chooseSingleSpellForEffect(List<SpellAbility> spells, SpellAbility sa, String title,
Map<String, Object> params);
public abstract <T extends GameEntity> List<T> chooseEntitiesForEffect(FCollectionView<T> optionList, DelayedReveal delayedReveal, SpellAbility sa, String title, Player relatedPlayer);
public abstract <T extends GameEntity> List<T> chooseEntitiesForEffect(FCollectionView<T> optionList, int min, int max, DelayedReveal delayedReveal, SpellAbility sa, String title, Player relatedPlayer);
public abstract <T extends GameEntity> List<T> chooseFromTwoListsForEffect(FCollectionView<T> optionList1, FCollectionView<T> optionList2, boolean optional, DelayedReveal delayedReveal, SpellAbility sa, String title, Player relatedPlayer);
public abstract boolean confirmAction(SpellAbility sa, PlayerActionConfirmMode mode, String message);
public abstract boolean confirmBidAction(SpellAbility sa, PlayerActionConfirmMode bidlife, String string, int bid, Player winner);
@@ -239,7 +243,7 @@ public abstract class PlayerController {
// better to have this odd method than those if playerType comparison in ChangeZone
public abstract Card chooseSingleCardForZoneChange(ZoneType destination, List<ZoneType> origin, SpellAbility sa, CardCollection fetchList, DelayedReveal delayedReveal, String selectPrompt, boolean isOptional, Player decider);
public abstract List<Card> chooseCardsForZoneChange(ZoneType destination, List<ZoneType> origin, SpellAbility sa, CardCollection fetchList, DelayedReveal delayedReveal, String selectPrompt, Player decider);
public abstract List<Card> chooseCardsForZoneChange(ZoneType destination, List<ZoneType> origin, SpellAbility sa, CardCollection fetchList, int min, int max, DelayedReveal delayedReveal, String selectPrompt, Player decider);
public abstract void autoPassCancel();
@@ -260,4 +264,6 @@ public abstract class PlayerController {
}
public abstract List<OptionalCostValue> chooseOptionalCosts(SpellAbility choosen, List<OptionalCostValue> optionalCostValues);
public abstract boolean confirmMulliganScry(final Player p);
}

View File

@@ -76,7 +76,7 @@ public class ReplaceMoved extends ReplacementEffect {
return false;
}
if (zt.equals(ZoneType.Battlefield) && getHostCard().equals(affected)) {
if (zt.equals(ZoneType.Battlefield) && getHostCard().equals(affected) && !hasParam("BypassEtbCheck")) {
// would be an etb replacement effect that enters the battlefield
Card lki = CardUtil.getLKICopy(affected);
lki.setLastKnownZone(lki.getController().getZone(zt));

View File

@@ -39,7 +39,7 @@ public abstract class ReplacementEffect extends TriggerReplacementBase {
/** The ID. */
private int id;
private ReplacementLayer layer = ReplacementLayer.None;
private ReplacementLayer layer = ReplacementLayer.Other;
/** The has run. */
private boolean hasRun = false;

View File

@@ -8,8 +8,7 @@ package forge.game.replacement;
public enum ReplacementLayer {
Control,
Copy,
Other,
None;
Other;
/**
* TODO: Write javadoc for this method.

View File

@@ -144,6 +144,8 @@ public abstract class Spell extends SpellAbility implements java.io.Serializable
if (lkicheck) {
game.getAction().checkStaticAbilities(false);
game.getTracker().unfreeze();
// reset owner for lki
card.setController(null, 0);
}
if (!(isInstant || activator.canCastSorcery() || flash || getRestrictions().isInstantSpeed()

View File

@@ -107,8 +107,6 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
private boolean spectacle = false;
private boolean offering = false;
private boolean emerge = false;
private boolean morphup = false;
private boolean manifestUp = false;
private boolean cumulativeupkeep = false;
private boolean outlast = false;
private boolean blessing = false;
@@ -372,22 +370,15 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
public boolean isAbility() { return true; }
public boolean isMorphUp() {
return morphup;
return this.hasParam("MorphUp");
}
public boolean isCastFaceDown() {
return false;
}
public final void setIsMorphUp(final boolean b) {
morphup = b;
}
public boolean isManifestUp() {
return manifestUp;
}
public final void setIsManifestUp(final boolean b) {
manifestUp = b;
return hasParam("ManifestUp");
}
public boolean isCycling() {
@@ -884,6 +875,14 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
clone.manaPart = new AbilityManaPart(host, mapParams);
}
// need to copy the damage tables
if (damageMap != null) {
clone.damageMap = new CardDamageMap(damageMap);
}
if (preventMap != null) {
clone.preventMap = new CardDamageMap(preventMap);
}
// clear maps for copy, the values will be added later
clone.additionalAbilities = Maps.newHashMap();
clone.additionalAbilityLists = Maps.newHashMap();

View File

@@ -336,9 +336,10 @@ public final class StaticAbilityContinuous {
final String colors = params.get("AddColor");
if (colors.equals("ChosenColor")) {
addColors = CardUtil.getShortColorsString(hostCard.getChosenColors());
} else if (colors.equals("All")) {
addColors = "W U B R G";
} else {
addColors = CardUtil.getShortColorsString(new ArrayList<String>(Arrays.asList(colors.split(
" & "))));
addColors = CardUtil.getShortColorsString(Arrays.asList(colors.split(" & ")));
}
}
@@ -346,9 +347,10 @@ public final class StaticAbilityContinuous {
final String colors = params.get("SetColor");
if (colors.equals("ChosenColor")) {
addColors = CardUtil.getShortColorsString(hostCard.getChosenColors());
} else if (colors.equals("All")) {
addColors = "W U B R G";
} else {
addColors = CardUtil.getShortColorsString(new ArrayList<String>(Arrays.asList(
colors.split(" & "))));
addColors = CardUtil.getShortColorsString(Arrays.asList(colors.split(" & ")));
}
se.setOverwriteColors(true);
}

View File

@@ -206,15 +206,20 @@ public abstract class Trigger extends TriggerReplacementBase {
} else {
saDesc = sa.getDescription();
}
// string might have leading whitespace
saDesc = saDesc.trim();
if (!saDesc.isEmpty()) {
// string might have leading whitespace
saDesc = saDesc.trim();
saDesc = saDesc.substring(0, 1).toLowerCase() + saDesc.substring(1);
// in case sa starts with CARDNAME, dont lowercase it
if (!saDesc.startsWith(sa.getHostCard().getName())) {
saDesc = saDesc.substring(0, 1).toLowerCase() + saDesc.substring(1);
}
if (saDesc.contains("ORIGINALHOST") && sa.getOriginalHost() != null) {
saDesc = TextUtil.fastReplace(saDesc, "ORIGINALHOST", sa.getOriginalHost().getName());
}
result = TextUtil.fastReplace(result, "ABILITY", saDesc);
} else {
saDesc = "<take no action>"; // printed in case nothing is chosen for the ability (e.g. Charm with Up to X)
}
result = TextUtil.fastReplace(result, "ABILITY", saDesc);
}
return result;

View File

@@ -0,0 +1,75 @@
/*
* Forge: Play Magic: the Gathering.
* Copyright (C) 2011 Forge Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package forge.game.trigger;
import java.util.Map;
import forge.game.card.Card;
import forge.game.spellability.SpellAbility;
/**
* <p>
* Trigger_Evolved class.
* </p>
*
* @author Forge
*/
public class TriggerAdapt extends Trigger {
/**
* <p>
* Constructor for Trigger_Evolved.
* </p>
*
* @param params
* a {@link java.util.HashMap} object.
* @param host
* a {@link forge.game.card.Card} object.
* @param intrinsic
* the intrinsic
*/
public TriggerAdapt(final Map<String, String> params, final Card host, final boolean intrinsic) {
super(params, host, intrinsic);
}
/** {@inheritDoc} */
@Override
public final boolean performTest(final Map<String, Object> runParams2) {
final Card sac = (Card) runParams2.get("Card");
if (hasParam("ValidCard")) {
if (!sac.isValid(getParam("ValidCard").split(","), getHostCard().getController(),
getHostCard(), null)) {
return false;
}
}
return true;
}
/** {@inheritDoc} */
@Override
public final void setTriggeringObjects(final SpellAbility sa) {
sa.setTriggeringObject("Card", getRunParams().get("Card"));
}
@Override
public String getImportantStackObjects(SpellAbility sa) {
StringBuilder sb = new StringBuilder();
sb.append("Adapt: ").append(sa.getTriggeringObject("Card"));
return sb.toString();
}
}

View File

@@ -42,48 +42,19 @@ public class TriggerChangesZoneAll extends Trigger {
}
private CardCollection filterCards(CardZoneTable table) {
CardCollection allCards = new CardCollection();
ZoneType destination = null;
List<ZoneType> origin = null;
if (hasParam("Destination")) {
if (!getParam("Destination").equals("Any")) {
destination = ZoneType.valueOf(getParam("Destination"));
if (!table.containsColumn(destination)) {
return allCards;
}
}
if (hasParam("Destination") && !getParam("Destination").equals("Any")) {
destination = ZoneType.valueOf(getParam("Destination"));
}
if (hasParam("Origin") && !getParam("Origin").equals("Any")) {
if (getParam("Origin") == null) {
return allCards;
}
final List<ZoneType> origin = ZoneType.listValueOf(getParam("Origin"));
for (ZoneType z : origin) {
if (table.containsRow(z)) {
if (destination != null) {
allCards.addAll(table.row(z).get(destination));
} else {
for (CardCollection c : table.row(z).values()) {
allCards.addAll(c);
}
}
}
}
} else if (destination != null) {
for (CardCollection c : table.column(destination).values()) {
allCards.addAll(c);
}
} else {
for (CardCollection c : table.values()) {
allCards.addAll(c);
}
origin = ZoneType.listValueOf(getParam("Origin"));
}
if (hasParam("ValidCards")) {
allCards = CardLists.getValidCards(allCards, getParam("ValidCards").split(","),
getHostCard().getController(), getHostCard(), null);
}
return allCards;
final String valid = this.getParamOrDefault("ValidCards", null);
return table.filterCards(origin, destination, valid, getHostCard(), null);
}
}

View File

@@ -17,6 +17,8 @@
*/
package forge.game.trigger;
import java.util.Map;
import forge.game.card.Card;
import forge.game.spellability.SpellAbility;
@@ -26,7 +28,6 @@ import forge.game.spellability.SpellAbility;
* </p>
*
* @author Forge
* @version $Id: TriggerEvolved.java 17802 2012-10-31 08:05:14Z Max mtg $
*/
public class TriggerEvolved extends Trigger {
@@ -42,17 +43,17 @@ public class TriggerEvolved extends Trigger {
* @param intrinsic
* the intrinsic
*/
public TriggerEvolved(final java.util.Map<String, String> params, final Card host, final boolean intrinsic) {
public TriggerEvolved(final Map<String, String> params, final Card host, final boolean intrinsic) {
super(params, host, intrinsic);
}
/** {@inheritDoc} */
@Override
public final boolean performTest(final java.util.Map<String, Object> runParams2) {
public final boolean performTest(final Map<String, Object> runParams2) {
final Card sac = (Card) runParams2.get("Card");
if (this.mapParams.containsKey("ValidCard")) {
if (!sac.isValid(this.mapParams.get("ValidCard").split(","), this.getHostCard().getController(),
this.getHostCard(), null)) {
if (hasParam("ValidCard")) {
if (!sac.isValid(getParam("ValidCard").split(","), getHostCard().getController(),
getHostCard(), null)) {
return false;
}
}
@@ -62,7 +63,7 @@ public class TriggerEvolved extends Trigger {
/** {@inheritDoc} */
@Override
public final void setTriggeringObjects(final SpellAbility sa) {
sa.setTriggeringObject("Card", this.getRunParams().get("Card"));
sa.setTriggeringObject("Card", getRunParams().get("Card"));
}
@Override

View File

@@ -25,7 +25,10 @@ import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.ability.effects.CharmEffect;
import forge.game.card.Card;
import forge.game.card.CardLists;
import forge.game.card.CardUtil;
import forge.game.card.CardZoneTable;
import forge.game.keyword.KeywordInterface;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.Ability;
@@ -42,6 +45,7 @@ import io.sentry.event.BreadcrumbBuilder;
import java.util.*;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
@@ -376,11 +380,7 @@ public class TriggerHandler {
// Static triggers
for (final Trigger t : Lists.newArrayList(activeTriggers)) {
if (t.isStatic() && canRunTrigger(t, mode, runParams)) {
int x = 1 + handlePanharmonicon(t, runParams);
for (int i = 0; i < x; ++i) {
runSingleTrigger(t, runParams);
}
runSingleTrigger(t, runParams);
checkStatics = true;
}
@@ -448,7 +448,7 @@ public class TriggerHandler {
}
}
int x = 1 + handlePanharmonicon(t, runParams);;
int x = 1 + handlePanharmonicon(t, runParams, player);
for (int i = 0; i < x; ++i) {
runSingleTrigger(t, runParams);
@@ -636,7 +636,10 @@ public class TriggerHandler {
sa.setStackDescription(sa.toString());
if (sa.getApi() == ApiType.Charm && !sa.isWrapper()) {
CharmEffect.makeChoices(sa);
if (!CharmEffect.makeChoices(sa)) {
// 603.3c If no mode is chosen, the ability is removed from the stack.
return;
}
}
Player decider = null;
@@ -692,29 +695,80 @@ public class TriggerHandler {
}
}
private int handlePanharmonicon(final Trigger t, final Map<String, Object> runParams) {
final Card host = t.getHostCard();
final Player p = host.getController();
private int handlePanharmonicon(final Trigger t, final Map<String, Object> runParams, final Player p) {
Card host = t.getHostCard();
// not a changesZone trigger
if (t.getMode() != TriggerType.ChangesZone) {
// not a changesZone trigger or changesZoneAll
if (t.getMode() != TriggerType.ChangesZone && t.getMode() != TriggerType.ChangesZoneAll) {
return 0;
}
// leave battlefield trigger, might be dying
// only real changeszone look back for this
if (t.getMode() == TriggerType.ChangesZone && "Battlefield".equals(t.getParam("Origin"))) {
// Need to get the last info from the trigger host
host = game.getChangeZoneLKIInfo(host);
}
// not a Permanent you control
if (!host.isPermanent() || !host.isInZone(ZoneType.Battlefield)) {
return 0;
}
int n = 0;
for (final String kw : p.getKeywords()) {
if (kw.startsWith("Panharmonicon")) {
if (runParams.get("Destination") instanceof String) {
final String dest = (String) runParams.get("Destination");
if (dest.equals("Battlefield") && runParams.get("Card") instanceof Card) {
final Card card = (Card) runParams.get("Card");
if (t.getMode() == TriggerType.ChangesZone) {
// iterate over all cards
final List<Card> lastCards = CardLists.filterControlledBy(p.getGame().getLastStateBattlefield(), p);
for (final Card ck : lastCards) {
for (final KeywordInterface ki : ck.getKeywords()) {
final String kw = ki.getOriginal();
if (kw.startsWith("Panharmonicon")) {
// Enter the Battlefield Trigger
if (runParams.get("Destination") instanceof String) {
final String dest = (String) runParams.get("Destination");
if ("Battlefield".equals(dest) && runParams.get("Card") instanceof Card) {
final Card card = (Card) runParams.get("Card");
final String valid = kw.split(":")[1];
if (card.isValid(valid.split(","), p, ck, null)) {
n++;
}
}
}
} else if (kw.startsWith("Dieharmonicon")) {
// 700.4. The term dies means “is put into a graveyard from the battlefield.”
if (runParams.get("Origin") instanceof String) {
final String origin = (String) runParams.get("Origin");
if ("Battlefield".equals(origin) && runParams.get("Destination") instanceof String) {
final String dest = (String) runParams.get("Destination");
if ("Graveyard".equals(dest) && runParams.get("Card") instanceof Card) {
final Card card = (Card) runParams.get("Card");
final String valid = kw.split(":")[1];
if (card.isValid(valid.split(","), p, ck, null)) {
n++;
}
}
}
}
}
}
}
} else if (t.getMode() == TriggerType.ChangesZoneAll) {
final CardZoneTable table = (CardZoneTable) runParams.get("Cards");
// iterate over all cards
for (final Card ck : p.getCardsIn(ZoneType.Battlefield)) {
for (final KeywordInterface ki : ck.getKeywords()) {
final String kw = ki.getOriginal();
if (kw.startsWith("Panharmonicon")) {
// currently there is no ChangesZoneAll that would trigger on etb
final String valid = kw.split(":")[1];
if (card.isValid(valid.split(","), p, host, null)) {
if (!table.filterCards(null, ZoneType.Battlefield, valid, ck, null).isEmpty()) {
n++;
}
} else if (kw.startsWith("Dieharmonicon")) {
// 700.4. The term dies means “is put into a graveyard from the battlefield.”
final String valid = kw.split(":")[1];
if (!table.filterCards(ImmutableList.of(ZoneType.Battlefield), ZoneType.Graveyard,
valid, ck, null).isEmpty()) {
n++;
}
}

View File

@@ -20,13 +20,14 @@ package forge.game.trigger;
import forge.game.card.Card;
import forge.game.spellability.SpellAbility;
import java.util.Map;
/**
* <p>
* Trigger_LifeGained class.
* </p>
*
* @author Forge
* @version $Id$
*/
public class TriggerLifeGained extends Trigger {
@@ -36,32 +37,30 @@ public class TriggerLifeGained extends Trigger {
* </p>
*
* @param params
* a {@link java.util.HashMap} object.
* a {@link java.util.Map} object.
* @param host
* a {@link forge.game.card.Card} object.
* @param intrinsic
* the intrinsic
*/
public TriggerLifeGained(final java.util.Map<String, String> params, final Card host, final boolean intrinsic) {
public TriggerLifeGained(final Map<String, String> params, final Card host, final boolean intrinsic) {
super(params, host, intrinsic);
}
/** {@inheritDoc} */
@Override
public final boolean performTest(final java.util.Map<String, Object> runParams2) {
if (this.mapParams.containsKey("ValidPlayer")) {
if (!matchesValid(runParams2.get("Player"), this.mapParams.get("ValidPlayer").split(","),
this.getHostCard())) {
public final boolean performTest(final Map<String, Object> runParams2) {
if (hasParam("ValidPlayer")) {
if (!matchesValid(runParams2.get("Player"), getParam("ValidPlayer").split(","), getHostCard())) {
return false;
}
}
if (this.mapParams.containsKey("ValidSource")) {
if (!matchesValid(runParams2.get("Source"), this.mapParams.get("ValidSource").split(","),
this.getHostCard())) {
if (hasParam("ValidSource")) {
if (!matchesValid(runParams2.get("Source"), getParam("ValidSource").split(","), getHostCard())) {
return false;
}
}
if (this.mapParams.containsKey("Spell")) {
if (hasParam("Spell")) {
final SpellAbility spellAbility = (SpellAbility) runParams2.get("SourceSA");
if (spellAbility == null || !spellAbility.getRootAbility().isSpell()) {
return false;
@@ -71,12 +70,11 @@ public class TriggerLifeGained extends Trigger {
return true;
}
/** {@inheritDoc} */
@Override
public final void setTriggeringObjects(final SpellAbility sa) {
sa.setTriggeringObject("LifeAmount", this.getRunParams().get("LifeAmount"));
sa.setTriggeringObject("Player", this.getRunParams().get("Player"));
sa.setTriggeringObject("LifeAmount", getRunParams().get("LifeAmount"));
sa.setTriggeringObject("Player", getRunParams().get("Player"));
}
@Override

View File

@@ -0,0 +1,76 @@
/*
* Forge: Play Magic: the Gathering.
* Copyright (C) 2011 Forge Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package forge.game.trigger;
import forge.game.card.Card;
import forge.game.spellability.SpellAbility;
import java.util.Map;
/**
* <p>
* Trigger_LifeGained class.
* </p>
*
* @author Forge
*/
public class TriggerPayLife extends Trigger {
/**
* <p>
* Constructor for TriggerPayLife.
* </p>
*
* @param params
* a {@link java.util.Map} object.
* @param host
* a {@link forge.game.card.Card} object.
* @param intrinsic
* the intrinsic
*/
public TriggerPayLife(final Map<String, String> params, final Card host, final boolean intrinsic) {
super(params, host, intrinsic);
}
/** {@inheritDoc} */
@Override
public final boolean performTest(final Map<String, Object> runParams2) {
if (hasParam("ValidPlayer")) {
if (!matchesValid(runParams2.get("Player"), getParam("ValidPlayer").split(","), getHostCard())) {
return false;
}
}
return true;
}
/** {@inheritDoc} */
@Override
public final void setTriggeringObjects(final SpellAbility sa) {
sa.setTriggeringObject("LifeAmount", getRunParams().get("LifeAmount"));
sa.setTriggeringObject("Player", getRunParams().get("Player"));
}
@Override
public String getImportantStackObjects(SpellAbility sa) {
StringBuilder sb = new StringBuilder();
sb.append("Player: ").append(sa.getTriggeringObject("Player")).append(", ");
sb.append("paid Amount: ").append(sa.getTriggeringObject("LifeAmount"));
return sb.toString();
}
}

View File

@@ -28,7 +28,6 @@ import java.util.Map;
* </p>
*
* @author Forge
* @version $Id$
*/
public class TriggerTaps extends Trigger {
@@ -50,15 +49,26 @@ public class TriggerTaps extends Trigger {
/** {@inheritDoc} */
@Override
public final boolean performTest(final java.util.Map<String, Object> runParams2) {
public final boolean performTest(final Map<String, Object> runParams2) {
final Card tapper = (Card) runParams2.get("Card");
if (this.mapParams.containsKey("ValidCard")) {
if (!tapper.isValid(this.mapParams.get("ValidCard").split(","), this.getHostCard().getController(),
this.getHostCard(), null)) {
if (hasParam("ValidCard")) {
if (!tapper.isValid(getParam("ValidCard").split(","), getHostCard().getController(),
getHostCard(), null)) {
return false;
}
}
if (hasParam("Attacker")) {
if ("True".equalsIgnoreCase(getParam("Attacker"))) {
if (!(Boolean)runParams2.get("Attacker")) {
return false;
}
} else if ("False".equalsIgnoreCase(getParam("Attacker"))) {
if ((Boolean)runParams2.get("Attacker")) {
return false;
}
}
}
return true;
}
@@ -67,7 +77,7 @@ public class TriggerTaps extends Trigger {
/** {@inheritDoc} */
@Override
public final void setTriggeringObjects(final SpellAbility sa) {
sa.setTriggeringObject("Card", this.getRunParams().get("Card"));
sa.setTriggeringObject("Card", getRunParams().get("Card"));
}
@Override

View File

@@ -14,6 +14,7 @@ import java.util.Map;
public enum TriggerType {
Abandoned(TriggerAbandoned.class),
AbilityCast(TriggerSpellAbilityCast.class),
Adapt(TriggerAdapt.class),
Always(TriggerAlways.class),
Attached(TriggerAttached.class),
AttackerBlocked(TriggerAttackerBlocked.class),
@@ -63,6 +64,7 @@ public enum TriggerType {
NewGame(TriggerNewGame.class),
PayCumulativeUpkeep(TriggerPayCumulativeUpkeep.class),
PayEcho(TriggerPayEcho.class),
PayLife(TriggerPayLife.class),
Phase(TriggerPhase.class),
PhaseIn(TriggerPhaseIn.class),
PhaseOut(TriggerPhaseOut.class),
@@ -79,7 +81,6 @@ public enum TriggerType {
SpellAbilityCast(TriggerSpellAbilityCast.class),
SpellCast(TriggerSpellAbilityCast.class),
Surveil(TriggerSurveil.class),
Tapped(TriggerTaps.class),
Taps(TriggerTaps.class),
TapsForMana(TriggerTapsForMana.class),
Transformed(TriggerTransformed.class),

View File

@@ -13,7 +13,7 @@ public class TriggerWaiting {
private Map<String, Object> params;
private List<Trigger> triggers = null;
public TriggerWaiting(TriggerType m, Map<String, Object> p) {
public TriggerWaiting(TriggerType m, Map<String, Object> p) {
mode = m;
params = p;
}
@@ -25,7 +25,6 @@ public class TriggerWaiting {
public Map<String, Object> getParams() {
return params;
}
public List<Trigger> getTriggers() {
return triggers;
@@ -35,7 +34,7 @@ public class TriggerWaiting {
this.triggers = triggers;
}
@Override
@Override
public String toString() {
return TextUtil.concatWithSpace("Waiting trigger:", mode.toString(),"with", params.toString());
}

View File

@@ -569,13 +569,6 @@ public class MagicStack /* extends MyObservable */ implements Iterable<SpellAbil
// verified by System.identityHashCode(card);
final Card tmp = sa.getHostCard();
if (!(sa instanceof WrappedAbility && sa.isTrigger())) { tmp.setCanCounter(true); } // reset mana pumped counter magic flag
if (tmp.getClones().size() > 0) {
for (final Card c : game.getCardsIn(ZoneType.Battlefield)) {
if (c.equals(tmp)) {
c.setClones(tmp.getClones());
}
}
}
// xManaCostPaid will reset when cast the spell, comment out to fix Venarian Gold
// sa.getHostCard().setXManaCostPaid(0);
}

View File

@@ -19,7 +19,7 @@
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>1.6.20-SNAPSHOT</version>
<version>1.6.20</version>
</parent>
<artifactId>forge-gui-android</artifactId>

View File

@@ -4,7 +4,7 @@
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>1.6.20-SNAPSHOT</version>
<version>1.6.20</version>
</parent>
<artifactId>forge-gui-desktop</artifactId>

View File

@@ -29,7 +29,7 @@ import forge.item.PaperCard;
import forge.model.FModel;
import forge.screens.match.CMatchUI;
import forge.toolbox.FOptionPane;
import forge.view.arcane.ListCardArea;
public class GuiChoose {
@@ -285,5 +285,30 @@ public class GuiChoose {
return null;
}
public static List<CardView> manipulateCardList(final CMatchUI gui, final String title, final Iterable<CardView> cards, final Iterable<CardView> manipulable,
final boolean toTop, final boolean toBottom, final boolean toAnywhere) {
gui.setSelectables(manipulable);
final Callable<List<CardView>> callable = new Callable<List<CardView>>() {
@Override
public List<CardView> call() throws Exception {
ListCardArea tempArea = ListCardArea.show(gui,title,cards,manipulable,toTop,toBottom,toAnywhere);
// tempArea.pack();
tempArea.show();
final List<CardView> cardList = tempArea.getCards();
return cardList;
}
};
final FutureTask<List<CardView>> ft = new FutureTask<List<CardView>>(callable);
FThreads.invokeInEdtAndWait(ft);
gui.clearSelectables();
try {
List<CardView> result = ft.get();
return result;
} catch (final Exception e) { // we have waited enough
e.printStackTrace();
}
return null;
}
}

View File

@@ -201,8 +201,7 @@ public abstract class ACEditorBase<TItem extends InventoryItem, TModel extends D
int qty = itemEntry.getValue();
int max;
if (deck == null || card == null || card.getRules().getType().isBasic() ||
limit == CardLimit.None || DeckFormat.getLimitExceptions().contains(card.getName())) {
if (deck == null || card == null || limit == CardLimit.None || DeckFormat.canHaveAnyNumberOf(card)) {
max = Integer.MAX_VALUE;
}
else {

View File

@@ -4,6 +4,7 @@ import java.awt.Dialog.ModalityType;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.io.File;
import java.util.regex.Pattern;
import javax.swing.JFileChooser;
import javax.swing.SwingUtilities;
@@ -22,6 +23,7 @@ import forge.screens.deckeditor.CDeckEditorUI;
import forge.screens.deckeditor.DeckImport;
import forge.screens.deckeditor.SEditorIO;
import forge.screens.deckeditor.views.VCurrentDeck;
import forge.toolbox.FOptionPane;
/**
* Controls the "current deck" panel in the deck editor UI.
@@ -244,8 +246,17 @@ public enum CCurrentDeck implements ICDoc {
if (fileChooser.showSaveDialog(null) == JFileChooser.APPROVE_OPTION) {
final File file = fileChooser.getSelectedFile();
final String check = file.getAbsolutePath();
previousDirectory = file.getParentFile();
if (!previousDirectory.exists()) {
FOptionPane.showErrorDialog("Cannot save deck to " + check);
return null;
}
if(isFileNameInvalid(file)) {
FOptionPane.showErrorDialog("Cannot save deck to " + check + "\nDeck name may not include any of the characters / \\ : * ? \" < > |");
return null;
}
return check.endsWith(".dck") ? file : new File(check + ".dck");
}
@@ -261,14 +272,29 @@ public enum CCurrentDeck implements ICDoc {
if (save.showSaveDialog(null) == JFileChooser.APPROVE_OPTION) {
final File file = save.getSelectedFile();
final String check = file.getAbsolutePath();
previousDirectory = file.getParentFile();
if (!previousDirectory.exists()) {
FOptionPane.showErrorDialog("Cannot save proxies to " + check);
return null;
}
if(isFileNameInvalid(file)) {
FOptionPane.showErrorDialog("Cannot save proxies to " + check + "\nFile name may not include any of the characters / \\ : * ? \" < > |");
return null;
}
return check.endsWith(".html") ? file : new File(check + ".html");
}
return null;
}
/** Checks if the file name includes any of the invalid characters / \ : * ? " < > : */
private static boolean isFileNameInvalid(File file) {
final Pattern pattern = Pattern.compile("[/\\\\:*?\\\"<>|]");
return pattern.matcher(file.getName()).find();
}
/** The Constant HTML_FILTER. */
public static final FileFilter HTML_FILTER = new FileFilter() {
@Override
@@ -281,4 +307,5 @@ public enum CCurrentDeck implements ICDoc {
return "Proxy File .html";
}
};
}

View File

@@ -100,7 +100,8 @@ import forge.util.ITriggerEvent;
import forge.util.gui.SOptionPane;
import forge.view.FView;
import forge.view.arcane.CardPanel;
import forge.view.arcane.FloatingCardArea;
import forge.view.arcane.FloatingZone;
import forge.match.input.*;
/**
* Constructs instance of match UI controller, used as a single point of
@@ -395,10 +396,12 @@ public final class CMatchUI
break;
case Hand:
updateHand = true;
//$FALL-THROUGH$
updateZones = true;
FloatingZone.refresh(owner, zone);
break;
default:
updateZones = true;
FloatingCardArea.refresh(owner, zone);
FloatingZone.refresh(owner, zone);
break;
}
}
@@ -424,6 +427,57 @@ public final class CMatchUI
}
}
@Override
public Iterable<PlayerZoneUpdate> tempShowZones(final PlayerView controller, final Iterable<PlayerZoneUpdate> zonesToUpdate) {
for (final PlayerZoneUpdate update : zonesToUpdate) {
final PlayerView player = update.getPlayer();
for (final ZoneType zone : update.getZones()) {
switch (zone) {
case Battlefield: // always shown
break;
case Hand: // controller hand always shown
if (controller != player) {
FloatingZone.show(this,player,zone);
}
break;
case Library:
case Graveyard:
case Exile:
case Flashback:
case Command:
FloatingZone.show(this,player,zone);
break;
default:
break;
}
}
}
return zonesToUpdate; //pfps should return only the newly shown zones
}
@Override
public void hideZones(final PlayerView controller, final Iterable<PlayerZoneUpdate> zonesToUpdate) {
for (final PlayerZoneUpdate update : zonesToUpdate) {
final PlayerView player = update.getPlayer();
for (final ZoneType zone : update.getZones()) {
switch (zone) {
case Battlefield: // always shown
break;
case Hand: // the controller's hand should never be temporarily shown, but ...
case Library:
case Graveyard:
case Exile:
case Flashback:
case Command:
FloatingZone.hide(this,player,zone);
break;
default:
break;
}
}
}
}
// Player's mana pool changes
@Override
public void updateManaPool(final Iterable<PlayerView> manaPoolUpdate) {
@@ -465,11 +519,51 @@ public final class CMatchUI
}
break;
default:
FloatingZone.refresh(c.getController(),zone); // in case the card is visible in the zone
break;
}
}
}
@Override
public void setSelectables(final Iterable<CardView> cards) {
super.setSelectables(cards);
// update zones on tabletop and floating zones - non-selectable cards may be rendered differently
FThreads.invokeInEdtNowOrLater(new Runnable() {
@Override public final void run() {
for (final PlayerView p : getGameView().getPlayers()) {
if ( p.getCards(ZoneType.Battlefield) != null ) {
updateCards(p.getCards(ZoneType.Battlefield));
}
if ( p.getCards(ZoneType.Hand) != null ) {
updateCards(p.getCards(ZoneType.Hand));
}
}
FloatingZone.refreshAll();
}
});
}
@Override
public void clearSelectables() {
super.clearSelectables();
// update zones on tabletop and floating zones - non-selectable cards may be rendered differently
FThreads.invokeInEdtNowOrLater(new Runnable() {
@Override public final void run() {
for (final PlayerView p : getGameView().getPlayers()) {
if ( p.getCards(ZoneType.Battlefield) != null ) {
updateCards(p.getCards(ZoneType.Battlefield));
}
if ( p.getCards(ZoneType.Hand) != null ) {
updateCards(p.getCards(ZoneType.Hand));
}
}
FloatingZone.refreshAll();
}
});
}
@Override
public List<JMenu> getMenus() {
return menus.getMenus();
@@ -504,7 +598,7 @@ public final class CMatchUI
layoutControl.initialize();
layoutControl.update();
}
FloatingCardArea.closeAll();
FloatingZone.closeAll();
}
@Override
@@ -562,7 +656,7 @@ public final class CMatchUI
case Exile:
case Graveyard:
case Library:
return FloatingCardArea.getCardPanel(this, card);
return FloatingZone.getCardPanel(this, card);
default:
break;
}
@@ -673,7 +767,7 @@ public final class CMatchUI
@Override
public void finishGame() {
FloatingCardArea.closeAll(); //ensure floating card areas cleared and closed after the game
FloatingZone.closeAll(); //ensure floating card areas cleared and closed after the game
final GameView gameView = getGameView();
if (hasLocalPlayers() || gameView.isMatchOver()) {
new ViewWinLose(gameView, this).show();
@@ -787,7 +881,7 @@ public final class CMatchUI
} else {
final ZoneType zone = hostCard.getZone();
if (ImmutableList.of(ZoneType.Command, ZoneType.Exile, ZoneType.Graveyard, ZoneType.Library).contains(zone)) {
FloatingCardArea.show(this, hostCard.getController(), zone);
FloatingZone.show(this, hostCard.getController(), zone);
}
menuParent = panel.getParent();
x = triggerEvent.getX();
@@ -942,11 +1036,16 @@ public final class CMatchUI
}
@Override
public List<GameEntityView> chooseEntitiesForEffect(final String title, final List<? extends GameEntityView> optionList, final DelayedReveal delayedReveal) {
public List<GameEntityView> chooseEntitiesForEffect(final String title, final List<? extends GameEntityView> optionList, final int min, final int max, final DelayedReveal delayedReveal) {
if (delayedReveal != null) {
reveal(delayedReveal.getMessagePrefix(), delayedReveal.getCards()); //TODO: Merge this into search dialog
}
return (List<GameEntityView>) order(title,"Selected", 0, optionList.size(), optionList, null, null, false);
return (List<GameEntityView>) order(title,"Selected", min, max, optionList, null, null, false);
}
@Override
public List<CardView> manipulateCardList(final String title, final Iterable<CardView> cards, final Iterable<CardView> manipulable, final boolean toTop, final boolean toBottom, final boolean toAnywhere) {
return GuiChoose.manipulateCardList(this, title, cards, manipulable, toTop, toBottom, toAnywhere);
}
@Override

View File

@@ -79,6 +79,13 @@ public class TargetingOverlay {
x2 = end.x;
y2 = end.y;
}
@Override
public boolean equals(Object obj)
{
Arc arc = (Arc)obj;
return ((arc.x1 == x1) && (arc.x2 == x2) && (arc.y1 == y1) && (arc.y2 == y2));
}
}
private final Set<CardView> cardsVisualized = new HashSet<CardView>();
@@ -331,13 +338,24 @@ public class TargetingOverlay {
activeStackItem.addMouseListener(new MouseAdapter() {
@Override
public void mouseEntered(final MouseEvent e) {
assembleStackArrows();
FView.SINGLETON_INSTANCE.getFrame().repaint();
if (matchUI.getCDock().getArcState() == ArcState.MOUSEOVER) {
assembleStackArrows();
FView.SINGLETON_INSTANCE.getFrame().repaint();
}
}
@Override
public void mouseExited(final MouseEvent e) {
assembleStackArrows();
FView.SINGLETON_INSTANCE.getFrame().repaint();
if (matchUI.getCDock().getArcState() == ArcState.MOUSEOVER) {
assembleStackArrows();
FView.SINGLETON_INSTANCE.getFrame().repaint();
}
}
@Override
public void mouseClicked(final MouseEvent e) {
if (matchUI.getCDock().getArcState() == ArcState.ON) {
assembleStackArrows();
FView.SINGLETON_INSTANCE.getFrame().repaint();
}
}
});
}
@@ -384,18 +402,27 @@ public class TargetingOverlay {
if (start == null || end == null) {
return;
}
Arc newArc = new Arc(end,start);
switch (connects) {
case Friends:
case FriendsStackTargeting:
arcsFriend.add(new Arc(end, start));
if (!arcsFriend.contains(newArc)) {
arcsFriend.add(newArc);
}
break;
case FoesAttacking:
arcsFoeAtk.add(new Arc(end, start));
if (!arcsFoeAtk.contains(newArc)) {
arcsFoeAtk.add(newArc);
}
break;
case FoesBlocking:
case FoesStackTargeting:
arcsFoeDef.add(new Arc(end, start));
if (!arcsFoeDef.contains(newArc)) {
arcsFoeDef.add(newArc);
}
break;
}
}
@@ -443,8 +470,8 @@ public class TargetingOverlay {
if (!attackingCard.equals(c) && !blockingCard.equals(c)) { continue; }
addArc(endpoints.get(attackingCard.getId()), endpoints.get(blockingCard.getId()), ArcConnection.FoesBlocking);
cardsVisualized.add(blockingCard);
cardsVisualized.add(attackingCard);
}
cardsVisualized.add(attackingCard);
}
}
}

View File

@@ -2,7 +2,7 @@ package forge.screens.match;
import forge.game.player.PlayerView;
import forge.game.zone.ZoneType;
import forge.view.arcane.FloatingCardArea;
import forge.view.arcane.FloatingZone;
/**
* Receives click and programmatic requests for viewing data stacks in the
@@ -27,6 +27,6 @@ public final class ZoneAction implements Runnable {
@Override
public void run() {
FloatingCardArea.showOrHide(matchUI, player, zone);
FloatingZone.showOrHide(matchUI, player, zone);
}
}
}

View File

@@ -47,6 +47,7 @@ import forge.gui.framework.DragTab;
import forge.gui.framework.EDocID;
import forge.gui.framework.IVDoc;
import forge.screens.match.controllers.CStack;
import forge.screens.match.controllers.CDock.ArcState;
import forge.toolbox.FMouseAdapter;
import forge.toolbox.FScrollPanel;
import forge.toolbox.FSkin;
@@ -194,19 +195,45 @@ public class VStack implements IVDoc<CStack> {
setFont(FSkin.getFont());
setWrapStyleWord(true);
setMinimumSize(new Dimension(CARD_WIDTH + 2 * PADDING, CARD_HEIGHT + 2 * PADDING));
// if the top of the stack is not assigned yet...
if (hoveredItem == null)
{
// set things up to draw an arc from it...
hoveredItem = StackInstanceTextArea.this;
controller.getMatchUI().setCard(item.getSourceCard());
}
addMouseListener(new MouseAdapter() {
@Override
public void mouseEntered(final MouseEvent e) {
hoveredItem = StackInstanceTextArea.this;
controller.getMatchUI().setCard(item.getSourceCard());
if (controller.getMatchUI().getCDock().getArcState() == ArcState.MOUSEOVER) {
hoveredItem = StackInstanceTextArea.this;
controller.getMatchUI().setCard(item.getSourceCard());
}
}
@Override
public void mouseExited(final MouseEvent e) {
if (hoveredItem == StackInstanceTextArea.this) {
hoveredItem = null;
}
if (controller.getMatchUI().getCDock().getArcState() == ArcState.MOUSEOVER) {
if (hoveredItem == StackInstanceTextArea.this) {
hoveredItem = null;
}
}
}
@Override
public void mouseClicked(final MouseEvent e) {
if (controller.getMatchUI().getCDock().getArcState() == ArcState.ON) {
if (hoveredItem == StackInstanceTextArea.this) {
hoveredItem = null;
}
else
{
hoveredItem = StackInstanceTextArea.this;
controller.getMatchUI().setCard(item.getSourceCard());
}
}
}
});

View File

@@ -246,8 +246,8 @@ public class CardArea extends CardPanelContainer implements CardPanelMouseListen
dragPanel.setDisplayEnabled(false);
CardPanel.setDragAnimationPanel(new CardPanel(dragPanel.getMatchUI(), dragPanel.getCard()));
final JFrame frame = (JFrame) SwingUtilities.windowForComponent(this);
final JLayeredPane layeredPane = frame.getLayeredPane();
final RootPaneContainer frame = (RootPaneContainer) SwingUtilities.windowForComponent(this);
final JLayeredPane layeredPane = frame.getLayeredPane();
layeredPane.add(CardPanel.getDragAnimationPanel());
layeredPane.moveToFront(CardPanel.getDragAnimationPanel());
final Point p = SwingUtilities.convertPoint(this, this.mouseDragStartX, this.mouseDragStartY, layeredPane);

View File

@@ -253,7 +253,7 @@ public class CardPanel extends SkinnedPanel implements CardContainer, IDisposabl
g2d.rotate(getTappedAngle(), cardXOffset + edgeOffset, (cardYOffset + cardHeight)
- edgeOffset);
}
super.paint(g2d);
super.paint(g2d);
}
@Override
@@ -268,12 +268,12 @@ public class CardPanel extends SkinnedPanel implements CardContainer, IDisposabl
final int cornerSize = noBorderPref && !cardImgHasAlpha ? 0 : Math.max(4, Math.round(cardWidth * CardPanel.ROUNDED_CORNER_SIZE));
final int offset = isTapped() && (!noBorderPref || cardImgHasAlpha) ? 1 : 0;
// Magenta outline for when card was chosen to pay
// Magenta outline for when card is chosen
if (matchUI.isUsedToPay(getCard())) {
g2d.setColor(Color.magenta);
final int n2 = Math.max(1, Math.round(2 * cardWidth * CardPanel.SELECTED_BORDER_SIZE));
g2d.fillRoundRect(cardXOffset - n2, (cardYOffset - n2) + offset, cardWidth + (n2 * 2), cardHeight + (n2 * 2), cornerSize + n2, cornerSize + n2);
}
}
// Green outline for hover
if (isSelected) {
@@ -282,7 +282,7 @@ public class CardPanel extends SkinnedPanel implements CardContainer, IDisposabl
g2d.fillRoundRect(cardXOffset - n, (cardYOffset - n) + offset, cardWidth + (n * 2), cardHeight + (n * 2), cornerSize + n , cornerSize + n);
}
// Black fill - (will become outline for white bordered cards)
// Black fill - (will become an outline for white bordered cards)
g2d.setColor(Color.black);
g2d.fillRoundRect(cardXOffset, cardYOffset + offset, cardWidth, cardHeight, cornerSize, cornerSize);
@@ -305,6 +305,12 @@ public class CardPanel extends SkinnedPanel implements CardContainer, IDisposabl
g2d.fillRoundRect(cardXOffset + ins, cardYOffset + ins, cardWidth - ins*2, cardHeight - ins*2, cornerSize-ins, cornerSize-ins);
}
}
if (matchUI.isSelectable(getCard())) { // White border for selectable cards to further highlight them
g2d.setColor(Color.WHITE);
final int ins = 1;
g2d.fillRoundRect(cardXOffset+ins, cardYOffset+ins, cardWidth-ins*2, cardHeight-ins*2, cornerSize-ins, cornerSize-ins);
}
}
private void drawManaCost(final Graphics g, final ManaCost cost, final int deltaY) {
@@ -328,6 +334,17 @@ public class CardPanel extends SkinnedPanel implements CardContainer, IDisposabl
drawFoilEffect(g, card, cardXOffset, cardYOffset,
cardWidth, cardHeight, Math.round(cardWidth * BLACK_BORDER_SIZE));
}
boolean nonselectable = matchUI.isSelecting() && !matchUI.isSelectable(getCard());
// if selecting, darken non-selectable cards
if ( nonselectable ) {
boolean noBorderPref = !isPreferenceEnabled(FPref.UI_RENDER_BLACK_BORDERS);
boolean cardImgHasAlpha = imagePanel != null && imagePanel.getSrcImage() != null && imagePanel.getSrcImage().getColorModel().hasAlpha();
final int cornerSize = noBorderPref && !cardImgHasAlpha ? 0 : Math.max(4, Math.round(cardWidth * CardPanel.ROUNDED_CORNER_SIZE));
final int offset = isTapped() && (!noBorderPref || cardImgHasAlpha) ? 1 : 0;
g.setColor(new Color(0.0f,0.0f,0.0f,0.6f));
g.fillRoundRect(cardXOffset, cardYOffset + offset, cardWidth, cardHeight, cornerSize, cornerSize);
}
}
public static void drawFoilEffect(final Graphics g, final CardView card2, final int x, final int y, final int width, final int height, final int borderSize) {
@@ -779,6 +796,7 @@ public class CardPanel extends SkinnedPanel implements CardContainer, IDisposabl
return FModel.getPreferences().getPrefBoolean(preferenceName);
}
// don't show overlays on non-selectable cards when selecting
private boolean isShowingOverlays() {
return isPreferenceEnabled(FPref.UI_SHOW_CARD_OVERLAYS) && card != null;
}

View File

@@ -182,6 +182,10 @@ public abstract class CardPanelContainer extends SkinnedPanel {
});
}
protected boolean cardPanelDraggable(final CardPanel panel) {
return true;
}
private MouseMotionListener setupMotionMouseListener() {
final MouseMotionListener mml = new MouseMotionListener() {
@Override
@@ -207,20 +211,23 @@ public abstract class CardPanelContainer extends SkinnedPanel {
if (panel != mouseDownPanel) {
return;
}
if (intialMouseDragX == -1) {
intialMouseDragX = x;
intialMouseDragY = y;
return;
}
if ((Math.abs(x - intialMouseDragX) < CardPanelContainer.DRAG_SMUDGE)
if (cardPanelDraggable(panel)) { // allow for non-draggable cards
if (intialMouseDragX == -1) {
intialMouseDragX = x;
intialMouseDragY = y;
return;
}
if ((Math.abs(x - intialMouseDragX) < CardPanelContainer.DRAG_SMUDGE)
&& (Math.abs(y - intialMouseDragY) < CardPanelContainer.DRAG_SMUDGE)) {
return;
}
mouseDownPanel = null;
setMouseDragPanel(panel);
mouseDragOffsetX = panel.getX() - intialMouseDragX;
mouseDragOffsetY = panel.getY() - intialMouseDragY;
mouseDragStart(getMouseDragPanel(), evt);
return;
}
mouseDownPanel = null;
setMouseDragPanel(panel);
mouseDragOffsetX = panel.getX() - intialMouseDragX;
mouseDragOffsetY = panel.getY() - intialMouseDragY;
mouseDragStart(getMouseDragPanel(), evt);
}
}
@Override
@@ -453,4 +460,4 @@ public abstract class CardPanelContainer extends SkinnedPanel {
this.layoutListeners.add(listener);
}
}
}

View File

@@ -17,25 +17,19 @@
*/
package forge.view.arcane;
import java.awt.Component;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.swing.ScrollPaneConstants;
import javax.swing.Timer;
import forge.Singletons;
import forge.assets.FSkinProp;
import forge.game.card.CardView;
import forge.game.player.PlayerView;
import forge.game.zone.ZoneType;
import forge.gui.framework.SDisplayUtil;
import forge.model.FModel;
import forge.properties.ForgePreferences;
@@ -43,185 +37,58 @@ import forge.properties.ForgePreferences.FPref;
import forge.screens.match.CMatchUI;
import forge.toolbox.FMouseAdapter;
import forge.toolbox.FScrollPane;
import forge.toolbox.FSkin;
import forge.toolbox.MouseTriggerEvent;
import forge.util.collect.FCollectionView;
import forge.util.Lang;
//import forge.util.collect.FCollectionView;
import forge.view.FDialog;
import forge.view.FFrame;
public class FloatingCardArea extends CardArea {
private static final long serialVersionUID = 1927906492186378596L;
// show some cards in a new window
public abstract class FloatingCardArea extends CardArea {
private static final String COORD_DELIM = ",";
protected static final String COORD_DELIM = ",";
protected static final ForgePreferences prefs = FModel.getPreferences();
private static final ForgePreferences prefs = FModel.getPreferences();
private static final Map<Integer, FloatingCardArea> floatingAreas = new HashMap<Integer, FloatingCardArea>();
protected String title;
protected FPref locPref;
protected boolean hasBeenShown, locLoaded;
private static int getKey(final PlayerView player, final ZoneType zone) {
return 40 * player.getId() + zone.hashCode();
}
public static void showOrHide(final CMatchUI matchUI, final PlayerView player, final ZoneType zone) {
final FloatingCardArea cardArea = _init(matchUI, player, zone);
cardArea.showOrHideWindow();
}
public static void show(final CMatchUI matchUI, final PlayerView player, final ZoneType zone) {
final FloatingCardArea cardArea = _init(matchUI, player, zone);
cardArea.showWindow();
}
private static FloatingCardArea _init(final CMatchUI matchUI, final PlayerView player, final ZoneType zone) {
final int key = getKey(player, zone);
FloatingCardArea cardArea = floatingAreas.get(key);
if (cardArea == null || cardArea.getMatchUI() != matchUI) {
cardArea = new FloatingCardArea(matchUI, player, zone);
floatingAreas.put(key, cardArea);
} else {
cardArea.setPlayer(player); //ensure player is updated if needed
}
return cardArea;
}
public static CardPanel getCardPanel(final CMatchUI matchUI, final CardView card) {
final FloatingCardArea window = _init(matchUI, card.getController(), card.getZone());
return window.getCardPanel(card.getId());
}
public static void refresh(final PlayerView player, final ZoneType zone) {
FloatingCardArea cardArea = floatingAreas.get(getKey(player, zone));
if (cardArea != null) {
cardArea.setPlayer(player); //ensure player is updated if needed
cardArea.refresh();
}
protected abstract FDialog getWindow();
protected abstract Iterable<CardView> getCards();
//refresh flashback zone when graveyard, library, or exile zones updated
switch (zone) {
case Graveyard:
case Library:
case Exile:
refresh(player, ZoneType.Flashback);
break;
default:
break;
}
protected FloatingCardArea(final CMatchUI matchUI) {
this(matchUI, new FScrollPane(false, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER));
}
public static void closeAll() {
for (final FloatingCardArea cardArea : floatingAreas.values()) {
cardArea.window.setVisible(false);
}
floatingAreas.clear();
protected FloatingCardArea(final CMatchUI matchUI, final FScrollPane scrollPane) {
super(matchUI, scrollPane);
}
private final ZoneType zone;
private PlayerView player;
private String title;
private FPref locPref;
private boolean hasBeenShown, locLoaded;
@SuppressWarnings("serial")
private final FDialog window = new FDialog(false, true, "0") {
@Override
public void setLocationRelativeTo(Component c) {
//don't change location this way if dialog has already been shown or location was loaded from preferences
if (hasBeenShown || locLoaded) { return; }
super.setLocationRelativeTo(c);
}
@Override
public void setVisible(boolean b0) {
if (isVisible() == b0) { return; }
if (!b0 && hasBeenShown && locPref != null) {
//update preference before hiding window, as otherwise its location will be 0,0
prefs.setPref(locPref,
getX() + COORD_DELIM + getY() + COORD_DELIM +
getWidth() + COORD_DELIM + getHeight());
//don't call prefs.save(), instead allowing them to be saved when match ends
}
super.setVisible(b0);
if (b0) {
refresh();
hasBeenShown = true;
}
}
};
private FloatingCardArea(final CMatchUI matchUI, final PlayerView player0, final ZoneType zone0) {
super(matchUI, new FScrollPane(false, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER));
window.add(getScrollPane(), "grow, push");
getScrollPane().setViewportView(this);
setOpaque(false);
switch (zone0) {
case Exile:
window.setIconImage(FSkin.getImage(FSkinProp.IMG_ZONE_EXILE));
break;
case Graveyard:
window.setIconImage(FSkin.getImage(FSkinProp.IMG_ZONE_GRAVEYARD));
break;
case Hand:
window.setIconImage(FSkin.getImage(FSkinProp.IMG_ZONE_HAND));
break;
case Library:
window.setIconImage(FSkin.getImage(FSkinProp.IMG_ZONE_LIBRARY));
break;
case Flashback:
window.setIconImage(FSkin.getImage(FSkinProp.IMG_ZONE_FLASHBACK));
break;
default:
locPref = null;
break;
}
zone = zone0;
setPlayer(player0);
setVertical(true);
}
private void setPlayer(PlayerView player0) {
if (player == player0) { return; }
player = player0;
title = Lang.getPossessedObject(player0.getName(), zone.name()) + " (%d)";
boolean isAi = player0.isAI();
switch (zone) {
case Exile:
locPref = isAi ? FPref.ZONE_LOC_AI_EXILE : FPref.ZONE_LOC_HUMAN_EXILE;
break;
case Graveyard:
locPref = isAi ? FPref.ZONE_LOC_AI_GRAVEYARD : FPref.ZONE_LOC_HUMAN_GRAVEYARD;
break;
case Hand:
locPref = isAi ? FPref.ZONE_LOC_AI_HAND : FPref.ZONE_LOC_HUMAN_HAND;
break;
case Library:
locPref = isAi ? FPref.ZONE_LOC_AI_LIBRARY : FPref.ZONE_LOC_HUMAN_LIBRARY;
break;
case Flashback:
locPref = isAi ? FPref.ZONE_LOC_AI_FLASHBACK : FPref.ZONE_LOC_HUMAN_FLASHBACK;
break;
default:
locPref = null;
break;
}
}
private void showWindow() {
protected void showWindow() {
onShow();
window.setFocusableWindowState(false); // should probably do this earlier
window.setVisible(true);
getWindow().setFocusableWindowState(false); // should probably do this earlier
getWindow().setVisible(true);
}
private void showOrHideWindow() {
protected void hideWindow() {
onShow();
window.setFocusableWindowState(false); // should probably do this earlier
window.setVisible(!window.isVisible());
getWindow().setFocusableWindowState(false); // should probably do this earlier
getWindow().setVisible(false);
}
private void onShow() {
protected void showOrHideWindow() {
onShow();
getWindow().setFocusableWindowState(false); // should probably do this earlier
getWindow().setVisible(!getWindow().isVisible());
}
protected void onShow() {
if (!hasBeenShown) {
loadLocation();
window.getTitleBar().addMouseListener(new FMouseAdapter() {
getWindow().getTitleBar().addMouseListener(new FMouseAdapter() {
@Override public final void onLeftDoubleClick(final MouseEvent e) {
window.setVisible(false); //hide window if titlebar double-clicked
getWindow().setVisible(false); //hide window if titlebar double-clicked
}
});
}
}
private void loadLocation() {
protected void loadLocation() {
if (locPref != null) {
String value = prefs.getPref(locPref);
if (value.length() > 0) {
@@ -255,7 +122,7 @@ public class FloatingCardArea extends CardArea {
y = screenBounds.y;
}
}
window.setBounds(x, y, w, h);
getWindow().setBounds(x, y, w, h);
locLoaded = true;
return;
}
@@ -269,14 +136,17 @@ public class FloatingCardArea extends CardArea {
}
//fallback default size
FFrame mainFrame = Singletons.getView().getFrame();
window.setSize(mainFrame.getWidth() / 5, mainFrame.getHeight() / 2);
getWindow().setSize(mainFrame.getWidth() / 5, mainFrame.getHeight() / 2);
}
private void refresh() {
if (!window.isVisible()) { return; } //don't refresh while window hidden
protected void refresh() {
if (!getWindow().isVisible()) { return; } //don't refresh while window hidden
doRefresh();
}
protected void doRefresh() {
List<CardPanel> cardPanels = new ArrayList<CardPanel>();
FCollectionView<CardView> cards = player.getCards(zone);
Iterable<CardView> cards = getCards();
if (cards != null) {
for (final CardView card : cards) {
CardPanel cardPanel = getCardPanel(card.getId());
@@ -293,27 +163,26 @@ public class FloatingCardArea extends CardArea {
boolean hadCardPanels = getCardPanels().size() > 0;
setCardPanels(cardPanels);
window.setTitle(String.format(title, cardPanels.size()));
getWindow().setTitle(String.format(title, cardPanels.size()));
//if window had cards and now doesn't, hide window
//(e.g. cast final card from Flashback zone)
if (hadCardPanels && cardPanels.size() == 0) {
window.setVisible(false);
getWindow().setVisible(false);
}
}
@Override
public void doLayout() {
if (window.isResizing()) {
if (getWindow().isResizing()) {
//delay layout slightly to reduce flicker during window resize
layoutTimer.restart();
}
else {
} else {
finishDoLayout();
}
}
private final Timer layoutTimer = new Timer(250, new ActionListener() {
protected final Timer layoutTimer = new Timer(250, new ActionListener() {
@Override
public void actionPerformed(ActionEvent arg0) {
layoutTimer.stop();
@@ -321,7 +190,7 @@ public class FloatingCardArea extends CardArea {
}
});
private void finishDoLayout() {
protected void finishDoLayout() {
super.doLayout();
}
@@ -331,12 +200,12 @@ public class FloatingCardArea extends CardArea {
super.mouseOver(panel, evt);
}
@Override
public final void mouseLeftClicked(final CardPanel panel, final MouseEvent evt) {
public void mouseLeftClicked(final CardPanel panel, final MouseEvent evt) {
getMatchUI().getGameController().selectCard(panel.getCard(), null, new MouseTriggerEvent(evt));
super.mouseLeftClicked(panel, evt);
}
@Override
public final void mouseRightClicked(final CardPanel panel, final MouseEvent evt) {
public void mouseRightClicked(final CardPanel panel, final MouseEvent evt) {
getMatchUI().getGameController().selectCard(panel.getCard(), null, new MouseTriggerEvent(evt));
super.mouseRightClicked(panel, evt);
}

View File

@@ -0,0 +1,198 @@
/*
* Forge: Play Magic: the Gathering.
* Copyright (C) 2011 Nate
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package forge.view.arcane;
import java.awt.Component;
import java.util.HashMap;
import java.util.Map;
import javax.swing.ScrollPaneConstants;
import forge.assets.FSkinProp;
import forge.game.card.CardView;
import forge.game.player.PlayerView;
import forge.game.zone.ZoneType;
import forge.properties.ForgePreferences.FPref;
import forge.screens.match.CMatchUI;
import forge.toolbox.FScrollPane;
import forge.toolbox.FSkin;
//import forge.util.collect.FCollectionView;
import forge.util.Lang;
import forge.view.FDialog;
public class FloatingZone extends FloatingCardArea {
private static final long serialVersionUID = 1927906492186378596L;
private static final Map<Integer, FloatingZone> floatingAreas = new HashMap<Integer, FloatingZone>();
private static int getKey(final PlayerView player, final ZoneType zone) {
return 40 * player.getId() + zone.hashCode();
}
public static void showOrHide(final CMatchUI matchUI, final PlayerView player, final ZoneType zone) {
final FloatingZone cardArea = _init(matchUI, player, zone);
cardArea.showOrHideWindow();
}
public static void show(final CMatchUI matchUI, final PlayerView player, final ZoneType zone) {
final FloatingZone cardArea = _init(matchUI, player, zone);
cardArea.showWindow();
}
public static void hide(final CMatchUI matchUI, final PlayerView player, final ZoneType zone) {
final FloatingZone cardArea = _init(matchUI, player, zone);
cardArea.hideWindow();
}
private static FloatingZone _init(final CMatchUI matchUI, final PlayerView player, final ZoneType zone) {
final int key = getKey(player, zone);
FloatingZone cardArea = floatingAreas.get(key);
if (cardArea == null || cardArea.getMatchUI() != matchUI) {
cardArea = new FloatingZone(matchUI, player, zone);
floatingAreas.put(key, cardArea);
} else {
cardArea.setPlayer(player); //ensure player is updated if needed
}
return cardArea;
}
public static CardPanel getCardPanel(final CMatchUI matchUI, final CardView card) {
final FloatingZone window = _init(matchUI, card.getController(), card.getZone());
return window.getCardPanel(card.getId());
}
public static void refresh(final PlayerView player, final ZoneType zone) {
FloatingZone cardArea = floatingAreas.get(getKey(player, zone));
if (cardArea != null) {
cardArea.setPlayer(player); //ensure player is updated if needed
cardArea.refresh();
}
//refresh flashback zone when graveyard, library, or exile zones updated
switch (zone) {
case Graveyard:
case Library:
case Exile:
refresh(player, ZoneType.Flashback);
break;
default:
break;
}
}
public static void closeAll() {
for (final FloatingZone cardArea : floatingAreas.values()) {
cardArea.window.setVisible(false);
}
floatingAreas.clear();
}
public static void refreshAll() {
for (final FloatingZone cardArea : floatingAreas.values()) {
cardArea.refresh();
}
}
private final ZoneType zone;
private PlayerView player;
@SuppressWarnings("serial")
private final FDialog window = new FDialog(false, true, "0") {
@Override
public void setLocationRelativeTo(Component c) {
//don't change location this way if dialog has already been shown or location was loaded from preferences
if (hasBeenShown || locLoaded) { return; }
super.setLocationRelativeTo(c);
}
@Override
public void setVisible(boolean b0) {
if (isVisible() == b0) { return; }
if (!b0 && hasBeenShown && locPref != null) {
//update preference before hiding window, as otherwise its location will be 0,0
prefs.setPref(locPref,
getX() + COORD_DELIM + getY() + COORD_DELIM +
getWidth() + COORD_DELIM + getHeight());
//don't call prefs.save(), instead allowing them to be saved when match ends
}
super.setVisible(b0);
if (b0) {
refresh();
hasBeenShown = true;
}
}
};
protected FDialog getWindow() {
return window;
}
protected Iterable<CardView> getCards() {
return player.getCards(zone);
}
private FloatingZone(final CMatchUI matchUI, final PlayerView player0, final ZoneType zone0) {
super(matchUI, new FScrollPane(false, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER));
window.add(getScrollPane(), "grow, push");
getScrollPane().setViewportView(this);
setOpaque(false);
switch (zone0) {
case Exile:
window.setIconImage(FSkin.getImage(FSkinProp.IMG_ZONE_EXILE));
break;
case Graveyard:
window.setIconImage(FSkin.getImage(FSkinProp.IMG_ZONE_GRAVEYARD));
break;
case Hand:
window.setIconImage(FSkin.getImage(FSkinProp.IMG_ZONE_HAND));
break;
case Library:
window.setIconImage(FSkin.getImage(FSkinProp.IMG_ZONE_LIBRARY));
break;
case Flashback:
window.setIconImage(FSkin.getImage(FSkinProp.IMG_ZONE_FLASHBACK));
break;
default:
locPref = null;
break;
}
zone = zone0;
setPlayer(player0);
setVertical(true);
}
private void setPlayer(PlayerView player0) {
if (player == player0) { return; }
player = player0;
title = Lang.getPossessedObject(player0.getName(), zone.name()) + " (%d)";
boolean isAi = player0.isAI();
switch (zone) {
case Exile:
locPref = isAi ? FPref.ZONE_LOC_AI_EXILE : FPref.ZONE_LOC_HUMAN_EXILE;
break;
case Graveyard:
locPref = isAi ? FPref.ZONE_LOC_AI_GRAVEYARD : FPref.ZONE_LOC_HUMAN_GRAVEYARD;
break;
case Hand:
locPref = isAi ? FPref.ZONE_LOC_AI_HAND : FPref.ZONE_LOC_HUMAN_HAND;
break;
case Library:
locPref = isAi ? FPref.ZONE_LOC_AI_LIBRARY : FPref.ZONE_LOC_HUMAN_LIBRARY;
break;
case Flashback:
locPref = isAi ? FPref.ZONE_LOC_AI_FLASHBACK : FPref.ZONE_LOC_HUMAN_FLASHBACK;
break;
default:
locPref = null;
break;
}
}
}

View File

@@ -0,0 +1,273 @@
/*
* Forge: Play Magic: the Gathering.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package forge.view.arcane;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.List;
import forge.game.card.CardView;
import forge.screens.match.CMatchUI;
import forge.view.arcane.util.CardPanelMouseAdapter;
import forge.view.FDialog;
import forge.toolbox.FButton;
// Show a list of cards in a new window, containing the moveable cards
// Allow moves of the moveable cards to top, to bottom, or anywhere
// Return a list of cards with the results of the moves
// Really should have a difference between visible cards and moveable cards,
// but that would require consirable changes to card panels and elsewhere
public class ListCardArea extends FloatingCardArea {
private static ArrayList<CardView> cardList;
private static ArrayList<CardView> moveableCards;
private static ListCardArea storedArea;
private static FButton doneButton;
private boolean toTop, toBottom, toAnywhere;
private ListCardArea(final CMatchUI matchUI) {
super(matchUI);
window.add(getScrollPane(),"grow, push");
getScrollPane().setViewportView(this);
doneButton = new FButton("Done");
doneButton.addActionListener(new ActionListener() {
@Override public void actionPerformed(ActionEvent e) { window.setVisible(false); }
});
window.add(doneButton,BorderLayout.SOUTH);
setOpaque(false);
}
public static ListCardArea show(final CMatchUI matchUI, final String title0, final Iterable<CardView> cardList0, final Iterable<CardView> moveableCards0, final boolean toTop0, final boolean toBottom0, final boolean toAnywhere0) {
if (storedArea==null) {
storedArea = new ListCardArea(matchUI);
}
cardList = new ArrayList<CardView>();
for ( CardView cv : cardList0 ) { cardList.add(cv) ; }
moveableCards = new ArrayList<CardView>(); // make sure moveable cards are in cardlist
for ( CardView card : moveableCards0 ) {
if ( cardList.contains(card) ) {
moveableCards.add(card);
}
}
storedArea.title = title0;
storedArea.toTop = toTop0;
storedArea.toBottom = toBottom0;
storedArea.toAnywhere = toAnywhere0;
storedArea.setDragEnabled(true);
storedArea.setVertical(true);
storedArea.doRefresh();
storedArea.showWindow();
return storedArea;
}
public ListCardArea(final CMatchUI matchUI, final String title0, final List<CardView> cardList0, final List<CardView> moveableCards0, final boolean toTop0, final boolean toBottom0, final boolean toAnywhere0) {
super(matchUI);
window.add(getScrollPane(),"grow, push");
getScrollPane().setViewportView(this);
setOpaque(false);
doneButton = new FButton("Done");
doneButton.addActionListener(new ActionListener() {
@Override public void actionPerformed(ActionEvent e) { window.setVisible(false); }
});
window.add(doneButton,BorderLayout.SOUTH);
cardList = new ArrayList<CardView>(cardList0); // this is modified - pfps - is there a better way?
moveableCards = new ArrayList<CardView>(moveableCards0);
title = title0;
toTop = toTop0;
toBottom = toBottom0;
toAnywhere = toAnywhere0;
this.setDragEnabled(true);
this.setVertical(true);
storedArea = this;
}
public List<CardView> getCards() {
return cardList;
}
@SuppressWarnings("serial")
protected final FDialog window = new FDialog(true, true, "0") {
@Override
public void setLocationRelativeTo(Component c) {
if (hasBeenShown || locLoaded) { return; }
super.setLocationRelativeTo(c);
}
@Override
public void setVisible(boolean b0) {
if (isVisible() == b0) { return; }
if (!b0 && hasBeenShown && locPref != null) {
//update preference before hiding window, as otherwise its location will be 0,0
prefs.setPref(locPref,
getX() + COORD_DELIM + getY() + COORD_DELIM +
getWidth() + COORD_DELIM + getHeight());
//don't call prefs.save(), instead allowing them to be saved when match ends
}
super.setVisible(b0);
if (b0) {
refresh();
hasBeenShown = true;
}
}
};
@Override
protected FDialog getWindow() {
return window;
}
@Override
protected void showWindow() {
onShow();
getWindow().setFocusableWindowState(true);
getWindow().setVisible(true);
}
@Override
protected void onShow() {
if (!hasBeenShown) {
loadLocation();
this.addCardPanelMouseListener(new CardPanelMouseAdapter() {
@Override
public void mouseDragEnd(final CardPanel dragPanel, final MouseEvent evt) {
dragEnd(dragPanel);
}
});
this.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(final KeyEvent e) {
switch (e.getKeyCode()) {
case KeyEvent.VK_ENTER:
doneButton.doClick();
break;
default:
break;
}
}
});
}
}
// is this a valid place to move the card?
private boolean validIndex(final CardView card, final int index) {
if (toAnywhere) { return true; }
int oldIndex = cardList.indexOf(card);
boolean topMove = true;
for(int i=0; i<index+(oldIndex<index?1:0); i++) {
if (!moveableCards.contains(cardList.get(i))) { topMove=false; break; }
}
if (toTop && topMove) { return true; }
boolean bottomMove = true;
for(int i=index+1-(oldIndex>index?1:0); i<cardList.size(); i++) {
if (!moveableCards.contains(cardList.get(i))) { bottomMove=false; break; }
}
if (toBottom && bottomMove) { return true; }
return false;
}
@Override
protected boolean cardPanelDraggable(final CardPanel panel) {
return moveableCards.contains(panel.getCard());
}
private void dragEnd(final CardPanel dragPanel) {
// if drag is not allowed, don't move anything
final CardView dragCard = dragPanel.getCard();
if (moveableCards.contains(dragCard)) {
//update index of dragged card in hand zone to match new index within hand area
final int index = getCardPanels().indexOf(dragPanel);
if (validIndex(dragCard,index)) {
synchronized (cardList) {
cardList.remove(dragCard);
cardList.add(index, dragCard);
}
}
}
refresh();
}
// @Override
// protected void refresh() {
// doRefresh();
// }
@Override
public void doLayout() {
// if (window.isResizing()) {
// //delay layout slightly to reduce flicker during window resize
// layoutTimer.restart();
// }
//else {
finishDoLayout();
//}
}
// move to beginning of list if allowable else to beginning of bottom if allowable
@Override
public final void mouseLeftClicked(final CardPanel panel, final MouseEvent evt) {
final CardView clickCard = panel.getCard();
if ( moveableCards.contains(clickCard) ) {
if ( toTop || toBottom ) {
synchronized (cardList) {
cardList.remove(clickCard);
int position;
if ( toTop ) {
position = 0 ;
} else { // to beginning of bottom: warning, untested
for ( position = cardList.size() ;
position>0 && moveableCards.contains(cardList.get(position-1)) ;
position-- );
}
cardList.add(position,clickCard);
}
refresh();
}
}
super.mouseLeftClicked(panel, evt);
}
@Override
public final void mouseRightClicked(final CardPanel panel, final MouseEvent evt) {
final CardView clickCard = panel.getCard();
if (moveableCards.contains(clickCard)) {
if ( toTop || toBottom ) {
synchronized (cardList) {
cardList.remove(clickCard);
int position;
if ( toBottom ) {
position = cardList.size() ;
} else { // to end of top
for ( position = 0 ;
position<cardList.size() && moveableCards.contains(cardList.get(position)) ;
position++ );
}
cardList.add(position,clickCard);
}
refresh();
}
}
super.mouseRightClicked(panel, evt);
}
}

View File

@@ -184,7 +184,7 @@ public class GameSimulatorTest extends SimulationTestCase {
GameSimulator sim = createSimulator(game, p);
Game simGame = sim.getSimulatedGameState();
SpellAbility unmorphSA = findSAWithPrefix(ripper, "MorphReveal a black card");
SpellAbility unmorphSA = findSAWithPrefix(ripper, "MorphReveal a black card");
assertNotNull(unmorphSA);
sim.simulateSpellAbility(unmorphSA);
assertEquals(18, simGame.getPlayers().get(0).getLife());
@@ -1503,4 +1503,197 @@ public class GameSimulatorTest extends SimulationTestCase {
}
public void testRiotEnchantment() {
Game game = initAndCreateGame();
Player p = game.getPlayers().get(0);
game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p);
final String goblinName = "Zhur-Taa Goblin";
addCard("Rhythm of the Wild", p);
Card goblin = addCardToZone(goblinName, p, ZoneType.Hand);
addCard("Mountain", p);
addCard("Forest", p);
SpellAbility goblinSA = goblin.getFirstSpellAbility();
assertNotNull(goblinSA);
GameSimulator sim = createSimulator(game, p);
int score = sim.simulateSpellAbility(goblinSA).value;
assertTrue(score > 0);
Game simGame = sim.getSimulatedGameState();
Card simGoblin = findCardWithName(simGame, goblinName);
assertNotNull(simGoblin);
int effects = simGoblin.getCounters(CounterType.P1P1) + simGoblin.getKeywordMagnitude(Keyword.HASTE);
assertTrue(effects == 2);
}
public void testTeysaKarlovXathridNecromancer() {
// Teysa Karlov and Xathrid Necromancer dying at the same time makes 4 token
Game game = initAndCreateGame();
Player p = game.getPlayers().get(0);
game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p);
addCard("Teysa Karlov", p);
addCard("Xathrid Necromancer", p);
for (int i = 0; i < 4; i++) {
addCardToZone("Plains", p, ZoneType.Battlefield);
}
Card wrathOfGod = addCardToZone("Wrath of God", p, ZoneType.Hand);
game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p);
SpellAbility wrathSA = wrathOfGod.getFirstSpellAbility();
assertNotNull(wrathSA);
GameSimulator sim = createSimulator(game, p);
int score = sim.simulateSpellAbility(wrathSA).value;
assertTrue(score > 0);
Game simGame = sim.getSimulatedGameState();
int numZombies = countCardsWithName(simGame, "Zombie");
assertTrue(numZombies == 4);
}
public void testDoubleTeysaKarlovXathridNecromancer() {
// Teysa Karlov dieing because of Legendary rule will make Xathrid Necromancer trigger 3 times
Game game = initAndCreateGame();
Player p = game.getPlayers().get(0);
game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p);
addCard("Teysa Karlov", p);
addCard("Xathrid Necromancer", p);
for (int i = 0; i < 3; i++) {
addCard("Plains", p);
}
addCard("Swamp", p);
Card second = addCardToZone("Teysa Karlov", p, ZoneType.Hand);
SpellAbility secondSA = second.getFirstSpellAbility();
GameSimulator sim = createSimulator(game, p);
int score = sim.simulateSpellAbility(secondSA).value;
assertTrue(score > 0);
Game simGame = sim.getSimulatedGameState();
int numZombies = countCardsWithName(simGame, "Zombie");
assertTrue(numZombies == 3);
}
public void testTeysaKarlovGitrogMonster() {
Game game = initAndCreateGame();
Player p = game.getPlayers().get(0);
game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p);
addCard("Teysa Karlov", p);
addCard("The Gitrog Monster", p);
addCard("Dryad Arbor", p);
for (int i = 0; i < 4; i++) {
addCard("Plains", p);
addCardToZone("Plains", p, ZoneType.Library);
}
Card armageddon = addCardToZone("Armageddon", p, ZoneType.Hand);
game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p);
SpellAbility armageddonSA = armageddon.getFirstSpellAbility();
GameSimulator sim = createSimulator(game, p);
int score = sim.simulateSpellAbility(armageddonSA).value;
assertTrue(score > 0);
Game simGame = sim.getSimulatedGameState();
// Two cards drawn
assertTrue(simGame.getPlayers().get(0).getZone(ZoneType.Hand).size() == 2);
}
public void testTeysaKarlovGitrogMonsterGitrogDies() {
Game game = initAndCreateGame();
Player p = game.getPlayers().get(0);
game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p);
Card teysa = addCard("Teysa Karlov", p);
addCard("The Gitrog Monster", p);
addCard("Dryad Arbor", p);
String indestructibilityName = "Indestructibility";
Card indestructibility = addCard(indestructibilityName, p);
indestructibility.attachToEntity(teysa);
// update Indestructible state
game.getAction().checkStateEffects(true);
for (int i = 0; i < 4; i++) {
addCard("Plains", p);
addCardToZone("Plains", p, ZoneType.Library);
}
Card armageddon = addCardToZone("Wrath of God", p, ZoneType.Hand);
game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p);
SpellAbility armageddonSA = armageddon.getFirstSpellAbility();
GameSimulator sim = createSimulator(game, p);
int score = sim.simulateSpellAbility(armageddonSA).value;
assertTrue(score > 0);
Game simGame = sim.getSimulatedGameState();
// One cards drawn
assertTrue(simGame.getPlayers().get(0).getZone(ZoneType.Hand).size() == 1);
}
public void testTeysaKarlovGitrogMonsterTeysaDies() {
Game game = initAndCreateGame();
Player p = game.getPlayers().get(0);
game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p);
addCard("Teysa Karlov", p);
Card gitrog = addCard("The Gitrog Monster", p);
addCard("Dryad Arbor", p);
String indestructibilityName = "Indestructibility";
Card indestructibility = addCard(indestructibilityName, p);
indestructibility.attachToEntity(gitrog);
// update Indestructible state
game.getAction().checkStateEffects(true);
for (int i = 0; i < 4; i++) {
addCard("Plains", p);
addCardToZone("Plains", p, ZoneType.Library);
}
Card armageddon = addCardToZone("Wrath of God", p, ZoneType.Hand);
game.getPhaseHandler().devModeSet(PhaseType.MAIN2, p);
SpellAbility armageddonSA = armageddon.getFirstSpellAbility();
GameSimulator sim = createSimulator(game, p);
int score = sim.simulateSpellAbility(armageddonSA).value;
assertTrue(score > 0);
Game simGame = sim.getSimulatedGameState();
// One cards drawn
assertTrue(simGame.getPlayers().get(0).getZone(ZoneType.Hand).size() == 1);
}
}

View File

@@ -174,7 +174,13 @@ public class PlayerControllerForTests extends PlayerController {
}
@Override
public <T extends GameEntity> List<T> chooseEntitiesForEffect(FCollectionView<T> optionList, DelayedReveal delayedReveal, SpellAbility sa, String title, Player relatedPlayer) {
public <T extends GameEntity> List<T> chooseEntitiesForEffect(FCollectionView<T> optionList, int min, int max, DelayedReveal delayedReveal, SpellAbility sa, String title, Player relatedPlayer) {
// this isn't used
return null;
}
@Override
public <T extends GameEntity> List<T> chooseFromTwoListsForEffect(FCollectionView<T> optionList1, FCollectionView<T> optionList2, boolean optional, DelayedReveal delayedReveal, SpellAbility sa, String title, Player targetedPlayer) {
// this isn't used
return null;
}
@@ -624,7 +630,7 @@ public class PlayerControllerForTests extends PlayerController {
}
@Override
public List<Card> chooseCardsForZoneChange(ZoneType destination, List<ZoneType> origin, SpellAbility sa, CardCollection fetchList, DelayedReveal delayedReveal, String selectPrompt, Player decider) {
public List<Card> chooseCardsForZoneChange(ZoneType destination, List<ZoneType> origin, SpellAbility sa, CardCollection fetchList, int min, int max, DelayedReveal delayedReveal, String selectPrompt, Player decider) {
// this isn't used
return null;
}
@@ -678,4 +684,10 @@ public class PlayerControllerForTests extends PlayerController {
return null;
}
@Override
public boolean confirmMulliganScry(Player p) {
// TODO Auto-generated method stub
return false;
}
}

View File

@@ -12,7 +12,7 @@
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>1.6.20-SNAPSHOT</version>
<version>1.6.20</version>
</parent>
<artifactId>forge-gui-ios</artifactId>

View File

@@ -4,7 +4,7 @@
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>1.6.20-SNAPSHOT</version>
<version>1.6.20</version>
</parent>
<artifactId>forge-gui-mobile-dev</artifactId>

View File

@@ -4,7 +4,7 @@
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>1.6.20-SNAPSHOT</version>
<version>1.6.20</version>
</parent>
<artifactId>forge-gui-mobile</artifactId>

View File

@@ -730,7 +730,7 @@ public class FDeckEditor extends TabPageScreen<FDeckEditor> {
if (deck == null || card == null) {
max = Integer.MAX_VALUE;
}
else if (limit == CardLimit.None || card.getRules().getType().isBasic() || DeckFormat.getLimitExceptions().contains(card.getName())) {
else if (limit == CardLimit.None || DeckFormat.canHaveAnyNumberOf(card)) {
max = Integer.MAX_VALUE;
if (parentScreen.isLimitedEditor() && !isAddSource) {
//prevent adding more than is in other pool when editing limited decks

View File

@@ -343,6 +343,16 @@ public class MatchController extends AbstractGuiGame {
view.updateZones(zonesToUpdate);
}
@Override
public Iterable<PlayerZoneUpdate> tempShowZones(final PlayerView controller, final Iterable<PlayerZoneUpdate> zonesToUpdate) {
return view.tempShowZones(controller, zonesToUpdate);
}
@Override
public void hideZones(final PlayerView controller, final Iterable<PlayerZoneUpdate> zonesToUpdate) {
view.hideZones(controller, zonesToUpdate);
}
@Override
public void updateCards(final Iterable<CardView> cards) {
for (final CardView card : cards) {
@@ -506,8 +516,16 @@ public class MatchController extends AbstractGuiGame {
}
@Override
public List<GameEntityView> chooseEntitiesForEffect(String title, List<? extends GameEntityView> optionList, DelayedReveal delayedReveal) {
return SGuiChoose.order(title, "Selected", 0, -1, (List<GameEntityView>) optionList, null);
public List<GameEntityView> chooseEntitiesForEffect(String title, List<? extends GameEntityView> optionList, int min, int max, DelayedReveal delayedReveal) {
final int m1 = max >= 0 ? optionList.size() - max : -1;
final int m2 = min >= 0 ? optionList.size() - min : -1;
return SGuiChoose.order(title, "Selected", m1, m2, (List<GameEntityView>) optionList, null);
}
@Override
public List<CardView> manipulateCardList(final String title, final Iterable<CardView> cards, final Iterable<CardView> manipulable, final boolean toTop, final boolean toBottom, final boolean toAnywhere) {
System.err.println("Not implemented yet - should never be called");
return null;
}
@Override

View File

@@ -468,6 +468,15 @@ public class MatchScreen extends FScreen {
}
}
public Iterable<PlayerZoneUpdate> tempShowZones(final PlayerView controller, final Iterable<PlayerZoneUpdate> zonesToUpdate) {
// pfps needs to actually do something
return zonesToUpdate; // pfps should return only those zones newly shown
}
public void hideZones(final PlayerView controller, final Iterable<PlayerZoneUpdate> zonesToUpdate) {
// pfps needs to actually do something
}
public void updateSingleCard(final CardView card) {
final CardAreaPanel pnl = CardAreaPanel.get(card);
if (pnl == null) { return; }

View File

@@ -244,8 +244,8 @@ public class GuiChoose {
}
public static <T> void many(final String title, final String topCaption, int min, int max, final List<T> sourceChoices, CardView referenceCard, final Callback<List<T>> callback) {
int m2 = min >= 0 ? sourceChoices.size() - min : -1;
int m1 = max >= 0 ? sourceChoices.size() - max : -1;
int m2 = min >= 0 ? sourceChoices.size() - min : -1;
order(title, topCaption, m1, m2, sourceChoices, null, referenceCard, callback);
}

View File

@@ -1 +1,92 @@
This file is automatically updated by our release bot on Discord, Blacksmith. It is created from the files present in the 'release-files' directory. Please do not hand-edit this file if using the bot to perform a release, as your changes will be overwritten.
Forge: 02/02/2019 ver 1.6.20
18457 cards in total.
--------------
Release Notes:
--------------
- New Cards -
Vizkopa Vampire; Vengeant Vampire; Vindictive Vampire; Verity Circle; Zegana, Utopian Speaker; Zhur-Taa Goblin; Kaya, Orzhov Usurper; Knight of the Last Breath; Knight of Sorrows; Kaya's Wrath; Thought Collapse; Tenth District Veteran; Territorial Boar; Teysa Karlov; Theater of Horrors; Trollbred Guardian; Tithe Taker; Thirsting Shade; Tome of the Guildpact; Thrash; Twilight Panther; Tin Street Dodger; The Haunt of Hightower; Titanic Brawl; Cindervines; Combine Guildmage; Carrion Imp; Consecrate; Collision; Code of Constraint; Clear the Stage; Captive Audience; Clamor Shaman; Consign to the Pit; Cavalcade of Calamity; Clear the Mind; Civic Stalwart; Carnival; Catacomb Crocodile; Charging War Boar; Cry of the Carnarium; Coral Commando; Clan Guildmage; Cult Guildmage; Chillbringer; Growth Spiral; Galloping Lizrog; Gruul Locket; Get the Point; Gruul Spellbreaker; Gravel-Hide Goblin; Growth-Chamber Guardian; Gyre Engineer; Glass of the Guildpact; Guardian Project; Grotesque Demise; Gateway Sneak; Ghor-Clan Wrecker; Gates Ablaze; Gutterbones; Goblin Goliath; Grasping Thrull; Goblin Gathering; Gatebreaker Ram; Gruul Beastmaster; Gate Colossus; Windstorm Drake; Wrecking Beast; Watchful Giant; Warrant; Wilderness Reclamation; Wall of Lost Thoughts; Impassioned Orator; Incubation Druid; Inspired Sphinx; Immortal Phoenix; Immolation Shaman; Incubation; Imperious Oligarch; Ill-Gotten Inheritance; Dovin's Automaton; Dovin's Dismissal; Debtors' Transport; Deface; Dovin's Acuity; Drill Bit; Dagger Caster; Dovin, Grand Arbiter; Deputy of Detention; Domri, City Smasher; Dead Revels; Domri's Nodorog; Domri, Chaos Bringer; Dovin, Architect of Law; Depose; Justiciar's Portal; Judith, the Scourge Diva; Angelic Exaltation; Applied Biomancy; Axebane Beast; Aeromunculus; Awaken the Erstwhile; Avatar of Growth; Angler Turtle; Angel of Grace; Azorius Locket; Azorius Skyguard; Arrester's Admonition; Arrester's Zeal; Azorius Knight-Arbiter; Amplifire; Angelic Guardian; Archway Angel; Prying Eyes; Prime Speaker Vannifar; Pteramander; Plaza of Harmony; Pestilent Spirit; Prowling Caracal; Persistent Petitioners; Pitiless Pontiff; Priest of Forgotten Gods; Plague Wight; Precognitive Perception; Rumbling Ruin; Ragefire; Rot Hulk; Rakdos Locket; Repudiate; Ravager Wurm; Rix Maadi Reveler; Rally to Battle; Rakdos Firewheeler; Rakdos Roustabout; Rhythm of the Wild; Rubblebelt Recluse; Rafter Demon; Rubble Reading; Rampage of the Clans; Rampaging Rendhorn; Resolute Watchdog; Rampaging Brontodon; Rubblebelt Runner; Rubble Slinger; Rakdos Trumpeter; Rakdos, the Showstopper; Regenesis; Revival; Lavinia, Azorius Renegade; Lumbering Battlement; Light Up the Stage; Lawmage's Binding; Hero of Precinct One; Haazda Officer; Hydroid Krasis; Humongulus; High Alert; Hackrobat; Noxious Groodion; Nikya of the Old Ways; Seraph of the Scales; Sharktocrab; Swirling Torrent; Summary Judgment; Skitter Eel; Stony Strength; Sphinx of Foresight; Spawn of Mayhem; Sphinx of the Guildpact; Spire Mangler; Skatewing Spy; Sphinx's Insight; Senate Griffin; Spirit of the Spires; Silhana Wayfinder; Spear Spewer; Smelt-Ward Ignus; Senate Guildmage; Skewer the Critics; Syndicate Guildmage; Slimebind; Savage Smash; Sky Tether; Spikewheel Acrobat; Sentinel's Mark; Sagittars' Volley; Saruli Caretaker; Shimmer of Possibility; Scuttlegator; Senate Courier; Sage's Row Savant; Sunder Shaman; Screaming Shield; Skarrgan Hellkite; Scorchmark; Storm Strike; Smothering Tithe; Steeple Creeper; Syndicate Messenger; Sylvan Brushstrider; Simic Locket; Sphinx of New Prahv; Simic Ascendancy; Sauroform Hybrid; Bloodmist Infiltrator; Basilica Bell-Haunt; Burning-Tree Vandal; Biogenic Upgrade; Bankrupt in Blood; Bolrac-Clan Crusher; Benthic Biomancer; Biomancer's Familiar; Bring to Trial; Bedevil; Bedeck; Blade Juggler; Burn Bright; Bladebrand; Biogenic Ooze; Emergency Powers; Electrodominance; Eyes Everywhere; Essence Capture; Elite Arrester; Ethereal Absolution; End-Raze Forerunners; Expose to Daylight; Enraged Ceratok; Undercity Scavenger; Undercity's Embrace; Unbreakable Formation; Orzhov Locket; Open the Gates; Orzhov Racketeers; Orzhov Enforcer; Frenzied Arynx; Faerie Duelist; Footlight Fiend; Fireblade Artist; Frilled Mystic; Flames of the Raze-Boar; Final Payment; Feral Maaka; Font of Agonies; Forbidding Spirit; Ministrant of Obligation; Mirror March; Mesmerizing Benthid; Macabre Mockery; Militant Angel; Mass Manipulation; Quench
- Desktop GUI -
The Desktop GUI can pop up zones (Library, Graveyard, etc.) allow players to select cards from them when the option UI_SELECT_FROM_CARD_DISPLAYS is set.
The Desktop GUI outlines the selectable cards in many situations. This is not done when playing mana costs.
- Digging -
Multi-card digging (e.g., for Genesis Wave) is done as a single multiple-card selection instead of a sequence of single-card selections.
- Game Night -
Support was added for the Game Night box set, including all 10 exclusive cards.
- AI improvements -
More AI improvements were implemented, hopefully making the game a little more interesting and challenging to play.
- Bug fixes -
As always, this release of Forge features an assortment of bug fixes and improvements based on user feedback during the previous release run.
-------------
Known Issues:
-------------
Known issues are here: https://git.cardforge.org/core-developers/forge/issues
Feel free to report your own there if you have any.
-------------
Installation:
-------------
The Forge archive includes a MANUAL.txt file and we ask that you spend a few minutes reading this file as it contains some information that may prove useful. We do tend to update this file at times and you should quickly read this file and look for new information for each and every new release. Thank you.
The archive format used for the Forge distribution is ".tar.bz2". There are utilities for Windows, Mac OS and the various *nix's that can be used to extract/decompress these ".tar.bz2" archives. We recommend that you extract/decompress the Forge archive into a new and unused folder.
Some people use the Windows application 7zip. This utility can be found at http://www.7-zip.org/download.html. Mac users can double click on the archive and the application Archive Utility will launch and extract the archive. Mac users do not need to download a separate utility.
Once the Forge archive has been decompressed you should then be able to launch Forge by using the included launcher. Launching Forge by double clicking on the forge jar file in the past caused a java heap space error. Forge's memory requirements have increased over time and the launchers increase the java heap space available to Forge. Currently you can launch Forge by double clicking on the forge jar file without a java heap space error but this is likely to change as we add in more sounds, icons, etc.
- The Mac OS application version -
We haven't been able to distribute the OS X Application version of Forge in sometime. We've recently automated our release tools, and will continue to look in the viability of creating this file now that things are autoamted.
- Online Multiplayer -
For local network play you should only need two systems running Forge. One to host and one to join and play. For remote (over the Internet) play you will need to ensure that the port used (36743 by default) is forwarded to the hosting machine.
--------------------
Active Contributors:
--------------------
Agetian
Austinio7116
Churrufli
DrDev
excessum
Gos
Hanmac
Indigo Dragon
Jamin Collins
KrazyTheFox
Luke
Marek14
mcrawford620
Meerkov
Myrd
nefigah
OgreBattlecruiser
pfps
Seravy
Sirspud
Sloth
slyfox7777777
Sol
Swordshine
tjtillman
tojammot
torridus
Xyx
Zuchinni
(Quest icons used created by Teekatas, from his Legendora set http://raindropmemory.deviantart.com)
(Thanks to the XMage team for permission to use their targeting arrows.)
(Thanks to http://www.freesound.org/browse/ for providing some sound files.)

View File

@@ -4,7 +4,7 @@
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>1.6.20-SNAPSHOT</version>
<version>1.6.20</version>
</parent>
<artifactId>forge-gui</artifactId>

View File

@@ -1,3 +1,10 @@
- Desktop GUI -
The Desktop GUI can pop up zones (Library, Graveyard, etc.) allow players to select cards from them when the option UI_SELECT_FROM_CARD_DISPLAYS is set.
The Desktop GUI outlines the selectable cards in many situations. This is not done when playing mana costs.
- Digging -
Multi-card digging (e.g., for Genesis Wave) is done as a single multiple-card selection instead of a sequence of single-card selections.
- Game Night -
Support was added for the Game Night box set, including all 10 exclusive cards.

View File

@@ -78,3 +78,4 @@ Dominaria, 3/6/DOM, DOM
Core Set 2019, 3/6/M19, M19
Guilds of Ravnica, 3/6/GRN, GRN
Ultimate Masters, 3/6/M19, UMA
Ravnica Allegiance, 3/6/RNA, RNA

View File

@@ -105,3 +105,4 @@ BBD: 36 Boosters
M19: 36 Boosters
GRN: 36 Boosters
UMA: 24 Boosters
RNA: 36 Boosters

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