From bb591dd038614459d1e4fdd50f9759a2491c3e99 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Tue, 29 Jun 2021 06:57:43 -0400 Subject: [PATCH] [AFR] Implementing dungeon mechanic (ready for review) (#7937) * added dungeon and dungeon room class * [AFR] Implemented Tomb of Annihilation * [AFR] Implemented Shortcut Seeker * [AFR] Implemented Gloom Stalker * [AFR] Implemented Nadaar, Selfless Paladin * added room triggers * added more venturing code, currently untested * fixed error * moved venture into dungeon from player class to game class * removed unnecessary sourceobject from dungeon * fixed npe error * added dungeon completion * fixed concurrent modification exception * added logging * added proper copy methods * added views * updated room text generation * added some missing code * finished implementing CompletedDungeonCondition * [AFR] Implemented Ellywick Tumblestrum * [AFR] Implemented Lost Mine of Phandelver * added choice dialog for dungeons * [AFR] Implemented Dungeon of the Mad Mage * small text fix * added initial dungeon test * [AFR] Implemented Cloister Gargoyle * [AFR] Implemented Dungeon Crawler * small text change for dungeon rooms * added more tests * some simplification to dungeon props * updated testing helper functions * added currently failing test for venturing on separate steps and turns * added tests for dungeon completion * fixed missing trigger visual and dungeons not persisting through turns * some text updates * added rollback test * added a test for multiple dungeons at once * added one more condition test --- .../collection/viewer/MageBook.java | 18 +- .../client/dialog/TestCardRenderDialog.java | 175 ++++---- .../java/mage/client/util/CardsViewUtil.java | 16 +- .../src/main/java/mage/view/CardView.java | 21 + .../src/main/java/mage/view/CardsView.java | 4 + .../src/main/java/mage/view/DungeonView.java | 84 ++++ .../src/main/java/mage/view/GameView.java | 7 + .../src/main/java/mage/view/PlayerView.java | 10 +- .../src/mage/cards/c/CloisterGargoyle.java | 56 +++ .../src/mage/cards/d/DungeonCrawler.java | 44 ++ .../src/mage/cards/e/EllywickTumblestrum.java | 93 +++++ Mage.Sets/src/mage/cards/g/GloomStalker.java | 48 +++ .../mage/cards/n/NadaarSelflessPaladin.java | 58 +++ .../src/mage/cards/s/ShortcutSeeker.java | 40 ++ .../sets/AdventuresInTheForgottenRealms.java | 6 + .../mage/test/cards/dungeons/DungeonTest.java | 387 ++++++++++++++++++ .../main/java/mage/abilities/AbilityImpl.java | 12 +- .../mage/abilities/ActivatedAbilityImpl.java | 12 +- .../CompletedDungeonTriggeredAbility.java | 49 +++ .../common/CompletedDungeonCondition.java | 30 ++ .../common/combat/CantAttackTargetEffect.java | 4 +- .../keyword/VentureIntoTheDungeonEffect.java | 32 ++ .../main/java/mage/constants/CardType.java | 1 + .../java/mage/constants/MageObjectType.java | 1 + .../src/main/java/mage/constants/SubType.java | 1 + Mage/src/main/java/mage/game/Game.java | 13 +- Mage/src/main/java/mage/game/GameImpl.java | 95 ++++- .../main/java/mage/game/command/Command.java | 16 +- .../main/java/mage/game/command/Dungeon.java | 371 +++++++++++++++++ .../java/mage/game/command/DungeonRoom.java | 167 ++++++++ .../command/dungeons/DungeonOfTheMadMage.java | 189 +++++++++ .../dungeons/LostMineOfPhandelver.java | 86 ++++ .../command/dungeons/TombOfAnnihilation.java | 266 ++++++++++++ .../emblems/EllywickTumblestrumEmblem.java | 81 ++++ .../main/java/mage/game/events/GameEvent.java | 7 + .../game/permanent/token/SkeletonToken.java | 2 +- .../game/permanent/token/SkeletonToken2.java | 28 ++ .../game/permanent/token/TheAtropalToken.java | 33 ++ Mage/src/main/java/mage/players/Player.java | 15 +- .../main/java/mage/players/PlayerImpl.java | 1 - .../common/CompletedDungeonWatcher.java | 44 ++ Utils/mtg-cards-data.txt | 2 + 42 files changed, 2481 insertions(+), 144 deletions(-) create mode 100644 Mage.Common/src/main/java/mage/view/DungeonView.java create mode 100644 Mage.Sets/src/mage/cards/c/CloisterGargoyle.java create mode 100644 Mage.Sets/src/mage/cards/d/DungeonCrawler.java create mode 100644 Mage.Sets/src/mage/cards/e/EllywickTumblestrum.java create mode 100644 Mage.Sets/src/mage/cards/g/GloomStalker.java create mode 100644 Mage.Sets/src/mage/cards/n/NadaarSelflessPaladin.java create mode 100644 Mage.Sets/src/mage/cards/s/ShortcutSeeker.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/dungeons/DungeonTest.java create mode 100644 Mage/src/main/java/mage/abilities/common/CompletedDungeonTriggeredAbility.java create mode 100644 Mage/src/main/java/mage/abilities/condition/common/CompletedDungeonCondition.java create mode 100644 Mage/src/main/java/mage/abilities/effects/keyword/VentureIntoTheDungeonEffect.java create mode 100644 Mage/src/main/java/mage/game/command/Dungeon.java create mode 100644 Mage/src/main/java/mage/game/command/DungeonRoom.java create mode 100644 Mage/src/main/java/mage/game/command/dungeons/DungeonOfTheMadMage.java create mode 100644 Mage/src/main/java/mage/game/command/dungeons/LostMineOfPhandelver.java create mode 100644 Mage/src/main/java/mage/game/command/dungeons/TombOfAnnihilation.java create mode 100644 Mage/src/main/java/mage/game/command/emblems/EllywickTumblestrumEmblem.java create mode 100644 Mage/src/main/java/mage/game/permanent/token/SkeletonToken2.java create mode 100644 Mage/src/main/java/mage/game/permanent/token/TheAtropalToken.java create mode 100644 Mage/src/main/java/mage/watchers/common/CompletedDungeonWatcher.java diff --git a/Mage.Client/src/main/java/mage/client/deckeditor/collection/viewer/MageBook.java b/Mage.Client/src/main/java/mage/client/deckeditor/collection/viewer/MageBook.java index 8491c7418f..8cfdba4c96 100644 --- a/Mage.Client/src/main/java/mage/client/deckeditor/collection/viewer/MageBook.java +++ b/Mage.Client/src/main/java/mage/client/deckeditor/collection/viewer/MageBook.java @@ -1,7 +1,10 @@ package mage.client.deckeditor.collection.viewer; import mage.abilities.icon.CardIconRenderSettings; -import mage.cards.*; +import mage.cards.CardDimensions; +import mage.cards.ExpansionSet; +import mage.cards.MageCard; +import mage.cards.Sets; import mage.cards.repository.CardCriteria; import mage.cards.repository.CardInfo; import mage.cards.repository.CardRepository; @@ -18,15 +21,13 @@ import mage.client.util.audio.AudioManager; import mage.client.util.sets.ConstructedFormats; import mage.components.ImagePanel; import mage.components.ImagePanelStyle; +import mage.game.command.Dungeon; import mage.game.command.Emblem; import mage.game.command.Plane; import mage.game.draft.RateCard; import mage.game.permanent.PermanentToken; import mage.game.permanent.token.Token; -import mage.view.CardView; -import mage.view.EmblemView; -import mage.view.PermanentView; -import mage.view.PlaneView; +import mage.view.*; import org.apache.log4j.Logger; import org.mage.card.arcane.ManaSymbols; import org.mage.plugins.card.images.CardDownloadData; @@ -365,6 +366,8 @@ public class MageBook extends JComponent { addToken((Token) item, bigCard, null, position); } else if (item instanceof Emblem) { addEmblem((Emblem) item, bigCard, null, position); + } else if (item instanceof Dungeon) { + addDungeon((Dungeon) item, bigCard, null, position); } else if (item instanceof Plane) { addPlane((Plane) item, bigCard, null, position); } else { @@ -430,6 +433,11 @@ public class MageBook extends JComponent { addCard(cardView, bigCard, gameId, rectangle, false); } + private void addDungeon(Dungeon dungeon, BigCard bigCard, UUID gameId, Rectangle rectangle) { + CardView cardView = new CardView(new DungeonView(dungeon)); + addCard(cardView, bigCard, gameId, rectangle, false); + } + private void addPlane(Plane plane, BigCard bigCard, UUID gameId, Rectangle rectangle) { CardView cardView = new CardView(new PlaneView(plane)); addCard(cardView, bigCard, gameId, rectangle, false); diff --git a/Mage.Client/src/main/java/mage/client/dialog/TestCardRenderDialog.java b/Mage.Client/src/main/java/mage/client/dialog/TestCardRenderDialog.java index f79e91956c..c4b48fd4a6 100644 --- a/Mage.Client/src/main/java/mage/client/dialog/TestCardRenderDialog.java +++ b/Mage.Client/src/main/java/mage/client/dialog/TestCardRenderDialog.java @@ -3,9 +3,9 @@ package mage.client.dialog; import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.icon.CardIconImpl; -import mage.abilities.icon.CardIconType; import mage.abilities.icon.CardIconOrder; import mage.abilities.icon.CardIconPosition; +import mage.abilities.icon.CardIconType; import mage.cards.*; import mage.cards.decks.Deck; import mage.cards.repository.CardInfo; @@ -15,13 +15,16 @@ import mage.cards.repository.ExpansionRepository; import mage.client.MageFrame; import mage.client.cards.BigCard; import mage.client.themes.ThemeType; -import mage.client.util.*; +import mage.client.util.ClientEventType; import mage.client.util.Event; +import mage.client.util.GUISizeHelper; +import mage.client.util.Listener; import mage.constants.MultiplayerAttackOption; import mage.constants.RangeOfInfluence; import mage.constants.Zone; import mage.game.Game; import mage.game.GameImpl; +import mage.game.command.Dungeon; import mage.game.command.Emblem; import mage.game.command.Plane; import mage.game.match.MatchType; @@ -40,8 +43,8 @@ import org.mage.card.arcane.CardPanel; import javax.swing.*; import java.awt.*; import java.awt.event.KeyEvent; -import java.util.*; import java.util.List; +import java.util.*; /** * App GUI: debug only, testing card renders and manipulations @@ -180,6 +183,12 @@ public class TestCardRenderDialog extends MageDialog { return emblemView; } + private AbilityView createDungeon(Dungeon dungeon) { + AbilityView emblemView = new AbilityView(dungeon.getAbilities().get(0), dungeon.getName(), new CardView(new DungeonView(dungeon))); + emblemView.setName(dungeon.getName()); + return emblemView; + } + private AbilityView createPlane(Plane plane) { AbilityView planeView = new AbilityView(plane.getAbilities().get(0), plane.getName(), new CardView(new PlaneView(plane))); planeView.setName(plane.getName()); @@ -224,7 +233,7 @@ public class TestCardRenderDialog extends MageDialog { // init card listener for clicks, menu and other events if (this.cardListener == null) { this.cardListener = event -> { - switch(event.getEventType()) { + switch (event.getEventType()) { case CARD_CLICK: case CARD_DOUBLE_CLICK: handleCardClick(event); @@ -408,7 +417,7 @@ public class TestCardRenderDialog extends MageDialog { labelRenderMode.setText("Render mode:"); - comboRenderMode.setModel(new javax.swing.DefaultComboBoxModel<>(new String[] { "MTGO", "Image" })); + comboRenderMode.setModel(new javax.swing.DefaultComboBoxModel<>(new String[]{"MTGO", "Image"})); comboRenderMode.addItemListener(new java.awt.event.ItemListener() { public void itemStateChanged(java.awt.event.ItemEvent evt) { comboRenderModeItemStateChanged(evt); @@ -432,7 +441,7 @@ public class TestCardRenderDialog extends MageDialog { labelCardIconsPosition.setText("Card icons position:"); - comboCardIconsPosition.setModel(new javax.swing.DefaultComboBoxModel<>(new String[] { "TOP", "LEFT", "RIGHT", "BOTTOM", "CORNER_TOP_LEFT", "CORNER_TOP_RIGHT", "CORNER_BOTTOM_LEFT", "CORNER_BOTTOM_RIGHT" })); + comboCardIconsPosition.setModel(new javax.swing.DefaultComboBoxModel<>(new String[]{"TOP", "LEFT", "RIGHT", "BOTTOM", "CORNER_TOP_LEFT", "CORNER_TOP_RIGHT", "CORNER_BOTTOM_LEFT", "CORNER_BOTTOM_RIGHT"})); comboCardIconsPosition.setSelectedIndex(1); comboCardIconsPosition.addItemListener(new java.awt.event.ItemListener() { public void itemStateChanged(java.awt.event.ItemEvent evt) { @@ -460,7 +469,7 @@ public class TestCardRenderDialog extends MageDialog { labelCardIconsOrder.setText("Order:"); - comboCardIconsOrder.setModel(new javax.swing.DefaultComboBoxModel<>(new String[] { "START", "CENTER", "END" })); + comboCardIconsOrder.setModel(new javax.swing.DefaultComboBoxModel<>(new String[]{"START", "CENTER", "END"})); comboCardIconsOrder.setSelectedIndex(2); comboCardIconsOrder.addItemListener(new java.awt.event.ItemListener() { public void itemStateChanged(java.awt.event.ItemEvent evt) { @@ -471,41 +480,41 @@ public class TestCardRenderDialog extends MageDialog { javax.swing.GroupLayout panelCardIconsLayout = new javax.swing.GroupLayout(panelCardIcons); panelCardIcons.setLayout(panelCardIconsLayout); panelCardIconsLayout.setHorizontalGroup( - panelCardIconsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(panelCardIconsLayout.createSequentialGroup() - .addComponent(labelCardIconsPosition) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(comboCardIconsPosition, javax.swing.GroupLayout.PREFERRED_SIZE, 80, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(labelCardIconsOrder) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(comboCardIconsOrder, javax.swing.GroupLayout.PREFERRED_SIZE, 80, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(labelCardIconsMaxVisible) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(spinnerCardIconsMaxVisible, javax.swing.GroupLayout.PREFERRED_SIZE, 50, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(labelCardIconsAdditionalAmount) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(spinnerCardIconsAdditionalAmount, javax.swing.GroupLayout.PREFERRED_SIZE, 50, javax.swing.GroupLayout.PREFERRED_SIZE) - .addContainerGap(188, Short.MAX_VALUE)) + panelCardIconsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(panelCardIconsLayout.createSequentialGroup() + .addComponent(labelCardIconsPosition) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(comboCardIconsPosition, javax.swing.GroupLayout.PREFERRED_SIZE, 80, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(labelCardIconsOrder) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(comboCardIconsOrder, javax.swing.GroupLayout.PREFERRED_SIZE, 80, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(labelCardIconsMaxVisible) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(spinnerCardIconsMaxVisible, javax.swing.GroupLayout.PREFERRED_SIZE, 50, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(labelCardIconsAdditionalAmount) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(spinnerCardIconsAdditionalAmount, javax.swing.GroupLayout.PREFERRED_SIZE, 50, javax.swing.GroupLayout.PREFERRED_SIZE) + .addContainerGap(188, Short.MAX_VALUE)) ); panelCardIconsLayout.setVerticalGroup( - panelCardIconsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(panelCardIconsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(comboCardIconsPosition, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(labelCardIconsPosition) - .addComponent(labelCardIconsMaxVisible) - .addComponent(spinnerCardIconsMaxVisible, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(labelCardIconsAdditionalAmount) - .addComponent(spinnerCardIconsAdditionalAmount, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(labelCardIconsOrder) - .addComponent(comboCardIconsOrder, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + panelCardIconsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(panelCardIconsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(comboCardIconsPosition, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(labelCardIconsPosition) + .addComponent(labelCardIconsMaxVisible) + .addComponent(spinnerCardIconsMaxVisible, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(labelCardIconsAdditionalAmount) + .addComponent(spinnerCardIconsAdditionalAmount, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(labelCardIconsOrder) + .addComponent(comboCardIconsOrder, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) ); labelTheme.setText("Theme:"); - comboTheme.setModel(new javax.swing.DefaultComboBoxModel<>(new String[] { "loading..." })); + comboTheme.setModel(new javax.swing.DefaultComboBoxModel<>(new String[]{"loading..."})); comboTheme.setToolTipText("WARNING, selected theme will be applied to full app, not render dialog only"); comboTheme.addItemListener(new java.awt.event.ItemListener() { public void itemStateChanged(java.awt.event.ItemEvent evt) { @@ -516,55 +525,55 @@ public class TestCardRenderDialog extends MageDialog { javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane()); getContentPane().setLayout(layout); layout.setHorizontalGroup( - layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() - .addContainerGap() - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(panelCardIcons, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(cardsPanel, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() - .addGap(0, 0, Short.MAX_VALUE) - .addComponent(buttonCancel, javax.swing.GroupLayout.PREFERRED_SIZE, 100, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addGroup(layout.createSequentialGroup() - .addComponent(buttonReloadCards) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(labelRenderMode) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(comboRenderMode, javax.swing.GroupLayout.PREFERRED_SIZE, 131, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(labelTheme) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(comboTheme, javax.swing.GroupLayout.PREFERRED_SIZE, 80, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(labelSize) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(sliderSize, javax.swing.GroupLayout.PREFERRED_SIZE, 100, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(checkBoxGenerateManyCards) - .addGap(0, 0, Short.MAX_VALUE))) - .addContainerGap()) + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(panelCardIcons, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(cardsPanel, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() + .addGap(0, 0, Short.MAX_VALUE) + .addComponent(buttonCancel, javax.swing.GroupLayout.PREFERRED_SIZE, 100, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addGroup(layout.createSequentialGroup() + .addComponent(buttonReloadCards) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(labelRenderMode) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(comboRenderMode, javax.swing.GroupLayout.PREFERRED_SIZE, 131, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(labelTheme) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(comboTheme, javax.swing.GroupLayout.PREFERRED_SIZE, 80, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(labelSize) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(sliderSize, javax.swing.GroupLayout.PREFERRED_SIZE, 100, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(checkBoxGenerateManyCards) + .addGap(0, 0, Short.MAX_VALUE))) + .addContainerGap()) ); layout.setVerticalGroup( - layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() - .addContainerGap() - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(buttonReloadCards) - .addComponent(labelRenderMode) - .addComponent(comboRenderMode, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(labelSize) - .addComponent(comboTheme, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(labelTheme)) - .addComponent(sliderSize, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(checkBoxGenerateManyCards)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(panelCardIcons, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(cardsPanel, javax.swing.GroupLayout.DEFAULT_SIZE, 401, Short.MAX_VALUE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(buttonCancel, javax.swing.GroupLayout.PREFERRED_SIZE, 30, javax.swing.GroupLayout.PREFERRED_SIZE) - .addContainerGap()) + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(buttonReloadCards) + .addComponent(labelRenderMode) + .addComponent(comboRenderMode, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(labelSize) + .addComponent(comboTheme, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(labelTheme)) + .addComponent(sliderSize, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(checkBoxGenerateManyCards)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(panelCardIcons, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(cardsPanel, javax.swing.GroupLayout.DEFAULT_SIZE, 401, Short.MAX_VALUE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(buttonCancel, javax.swing.GroupLayout.PREFERRED_SIZE, 30, javax.swing.GroupLayout.PREFERRED_SIZE) + .addContainerGap()) ); pack(); diff --git a/Mage.Client/src/main/java/mage/client/util/CardsViewUtil.java b/Mage.Client/src/main/java/mage/client/util/CardsViewUtil.java index 941756d7bc..24a25a7ac2 100644 --- a/Mage.Client/src/main/java/mage/client/util/CardsViewUtil.java +++ b/Mage.Client/src/main/java/mage/client/util/CardsViewUtil.java @@ -48,19 +48,21 @@ public final class CardsViewUtil { public static CardsView convertCommandObject(List view) { CardsView cards = new CardsView(); - for (CommandObjectView commandObject : view) { + CardView cardView; if (commandObject instanceof EmblemView) { - CardView cardView = new CardView((EmblemView) commandObject); - cards.put(commandObject.getId(), cardView); + cardView = new CardView((EmblemView) commandObject); + } else if (commandObject instanceof DungeonView) { + cardView = new CardView((DungeonView) commandObject); } else if (commandObject instanceof PlaneView) { - CardView cardView = new CardView((PlaneView) commandObject); - cards.put(commandObject.getId(), cardView); + cardView = new CardView((PlaneView) commandObject); } else if (commandObject instanceof CommanderView) { - cards.put(commandObject.getId(), (CommanderView) commandObject); + cardView = (CommanderView) commandObject; + } else { + continue; } + cards.put(commandObject.getId(), cardView); } - return cards; } } diff --git a/Mage.Common/src/main/java/mage/view/CardView.java b/Mage.Common/src/main/java/mage/view/CardView.java index 3e2646a596..0345dd999e 100644 --- a/Mage.Common/src/main/java/mage/view/CardView.java +++ b/Mage.Common/src/main/java/mage/view/CardView.java @@ -20,6 +20,7 @@ import mage.counters.CounterType; import mage.designations.Designation; import mage.filter.FilterMana; import mage.game.Game; +import mage.game.command.Dungeon; import mage.game.command.Emblem; import mage.game.command.Plane; import mage.game.permanent.Permanent; @@ -579,6 +580,11 @@ public class CardView extends SimpleCardView { Emblem emblem = (Emblem) object; this.rarity = Rarity.SPECIAL; this.rules = emblem.getAbilities().getRules(emblem.getName()); + } else if (object instanceof Dungeon) { + this.mageObjectType = MageObjectType.DUNGEON; + Dungeon dungeon = (Dungeon) object; + this.rarity = Rarity.SPECIAL; + this.rules = dungeon.getRules(); } else if (object instanceof Plane) { this.mageObjectType = MageObjectType.PLANE; Plane plane = (Plane) object; @@ -631,6 +637,21 @@ public class CardView extends SimpleCardView { this.rarity = Rarity.COMMON; } + public CardView(DungeonView dungeon) { + this(true); + this.gameObject = true; + this.id = dungeon.getId(); + this.mageObjectType = MageObjectType.DUNGEON; + this.name = dungeon.getName(); + this.displayName = name; + this.displayFullName = name; + this.rules = dungeon.getRules(); + // emblem images are always with common (black) symbol + this.frameStyle = FrameStyle.M15_NORMAL; + this.expansionSetCode = dungeon.getExpansionSetCode(); + this.rarity = Rarity.COMMON; + } + public CardView(PlaneView plane) { this(true); this.gameObject = true; diff --git a/Mage.Common/src/main/java/mage/view/CardsView.java b/Mage.Common/src/main/java/mage/view/CardsView.java index 084cd0895a..cd0b2832a9 100644 --- a/Mage.Common/src/main/java/mage/view/CardsView.java +++ b/Mage.Common/src/main/java/mage/view/CardsView.java @@ -6,6 +6,7 @@ import mage.abilities.effects.Effect; import mage.cards.Card; import mage.constants.Zone; import mage.game.Game; +import mage.game.command.Dungeon; import mage.game.command.Emblem; import mage.game.command.Plane; import mage.game.permanent.Permanent; @@ -110,6 +111,9 @@ public class CardsView extends LinkedHashMap { abilityView = new AbilityView(ability, sourceObject.getName(), new CardView(new EmblemView((Emblem) sourceObject))); abilityView.setName(sourceObject.getName()); // abilityView.setExpansionSetCode(sourceCard.getExpansionSetCode()); + } else if (sourceObject instanceof Dungeon) { + abilityView = new AbilityView(ability, sourceObject.getName(), new CardView(new DungeonView((Dungeon) sourceObject))); + abilityView.setName(sourceObject.getName()); } else if (sourceObject instanceof Plane) { abilityView = new AbilityView(ability, sourceObject.getName(), new CardView(new PlaneView((Plane) sourceObject))); abilityView.setName(sourceObject.getName()); diff --git a/Mage.Common/src/main/java/mage/view/DungeonView.java b/Mage.Common/src/main/java/mage/view/DungeonView.java new file mode 100644 index 0000000000..9f5af24312 --- /dev/null +++ b/Mage.Common/src/main/java/mage/view/DungeonView.java @@ -0,0 +1,84 @@ +package mage.view; + +import mage.game.command.Dungeon; +import mage.players.PlayableObjectStats; + +import java.io.Serializable; +import java.util.List; +import java.util.UUID; + +/** + * @author TheElk801 + */ +public class DungeonView implements CommandObjectView, Serializable { + + protected UUID id; + protected String name; + protected String expansionSetCode; + protected List rules; + protected PlayableObjectStats playableStats = new PlayableObjectStats(); + + public DungeonView(Dungeon dungeon) { + this.id = dungeon.getId(); + this.name = dungeon.getName(); + this.expansionSetCode = dungeon.getExpansionSetCodeForImage(); + this.rules = dungeon.getRules(); + } + + @Override + public String getExpansionSetCode() { + return expansionSetCode; + } + + @Override + public String getName() { + return name; + } + + @Override + public UUID getId() { + return id; + } + + @Override + public List getRules() { + return rules; + } + + @Override + public boolean isPlayable() { + return this.playableStats.getPlayableAmount() > 0; + } + + @Override + public void setPlayableStats(PlayableObjectStats playableStats) { + this.playableStats = playableStats; + } + + @Override + public PlayableObjectStats getPlayableStats() { + return this.playableStats; + } + + @Override + public boolean isChoosable() { + // unsupported + return false; + } + + @Override + public void setChoosable(boolean isChoosable) { + // unsupported + } + + @Override + public boolean isSelected() { + // unsupported + return false; + } + + @Override + public void setSelected(boolean isSelected) { + // unsupported + } +} diff --git a/Mage.Common/src/main/java/mage/view/GameView.java b/Mage.Common/src/main/java/mage/view/GameView.java index 3fb7f7e80b..f52d49163f 100644 --- a/Mage.Common/src/main/java/mage/view/GameView.java +++ b/Mage.Common/src/main/java/mage/view/GameView.java @@ -13,6 +13,7 @@ import mage.game.ExileZone; import mage.game.Game; import mage.game.GameState; import mage.game.combat.CombatGroup; +import mage.game.command.Dungeon; import mage.game.command.Emblem; import mage.game.command.Plane; import mage.game.permanent.Permanent; @@ -114,6 +115,12 @@ public class GameView implements Serializable { stack.put(stackObject.getId(), new StackAbilityView(game, (StackAbility) stackObject, object.getName(), cardView)); checkPaid(stackObject.getId(), ((StackAbility) stackObject)); + } else if (object instanceof Dungeon) { + CardView cardView = new CardView(new DungeonView((Dungeon) object)); + stackObject.setName(object.getName()); + stack.put(stackObject.getId(), + new StackAbilityView(game, (StackAbility) stackObject, object.getName(), cardView)); + checkPaid(stackObject.getId(), ((StackAbility) stackObject)); } else if (object instanceof Plane) { CardView cardView = new CardView(new PlaneView((Plane) object)); stackObject.setName(object.getName()); diff --git a/Mage.Common/src/main/java/mage/view/PlayerView.java b/Mage.Common/src/main/java/mage/view/PlayerView.java index fd31146a6c..0d0f433605 100644 --- a/Mage.Common/src/main/java/mage/view/PlayerView.java +++ b/Mage.Common/src/main/java/mage/view/PlayerView.java @@ -6,10 +6,7 @@ import mage.designations.Designation; import mage.game.ExileZone; import mage.game.Game; import mage.game.GameState; -import mage.game.command.CommandObject; -import mage.game.command.Commander; -import mage.game.command.Emblem; -import mage.game.command.Plane; +import mage.game.command.*; import mage.game.permanent.Permanent; import mage.players.Player; import mage.players.net.UserData; @@ -113,6 +110,11 @@ public class PlayerView implements Serializable { if (emblem.getControllerId().equals(this.playerId)) { commandList.add(new EmblemView(emblem)); } + } else if (commandObject instanceof Dungeon) { + Dungeon dungeon = (Dungeon) commandObject; + if (dungeon.getControllerId().equals(this.playerId)) { + commandList.add(new DungeonView(dungeon)); + } } else if (commandObject instanceof Plane) { Plane plane = (Plane) commandObject; // Planes are universal and all players can see them. diff --git a/Mage.Sets/src/mage/cards/c/CloisterGargoyle.java b/Mage.Sets/src/mage/cards/c/CloisterGargoyle.java new file mode 100644 index 0000000000..f524a0d514 --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/CloisterGargoyle.java @@ -0,0 +1,56 @@ +package mage.cards.c; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.condition.common.CompletedDungeonCondition; +import mage.abilities.decorator.ConditionalContinuousEffect; +import mage.abilities.effects.common.continuous.BoostSourceEffect; +import mage.abilities.effects.common.continuous.GainAbilitySourceEffect; +import mage.abilities.effects.keyword.VentureIntoTheDungeonEffect; +import mage.abilities.keyword.FlyingAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubType; +import mage.watchers.common.CompletedDungeonWatcher; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class CloisterGargoyle extends CardImpl { + + public CloisterGargoyle(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ARTIFACT, CardType.CREATURE}, "{2}{W}"); + + this.subtype.add(SubType.GARGOYLE); + this.power = new MageInt(0); + this.toughness = new MageInt(4); + + // When Cloister Gargoyle enters the battlefield, venture into the dungeon. + this.addAbility(new EntersBattlefieldTriggeredAbility(new VentureIntoTheDungeonEffect())); + + // As long as you've completed a dungeon, Cloister Gargoyle gets +3/+0 and has flying. + Ability ability = new SimpleStaticAbility(new ConditionalContinuousEffect( + new BoostSourceEffect(3, 0, Duration.WhileOnBattlefield), + CompletedDungeonCondition.instance, "as long as you've completed a dungeon, {this} gets +3/+0" + )); + ability.addEffect(new ConditionalContinuousEffect(new GainAbilitySourceEffect( + FlyingAbility.getInstance(), Duration.WhileOnBattlefield + ), CompletedDungeonCondition.instance, "and has flying")); + this.addAbility(ability.addHint(CompletedDungeonCondition.getHint()), new CompletedDungeonWatcher()); + } + + private CloisterGargoyle(final CloisterGargoyle card) { + super(card); + } + + @Override + public CloisterGargoyle copy() { + return new CloisterGargoyle(this); + } +} diff --git a/Mage.Sets/src/mage/cards/d/DungeonCrawler.java b/Mage.Sets/src/mage/cards/d/DungeonCrawler.java new file mode 100644 index 0000000000..6c606b068e --- /dev/null +++ b/Mage.Sets/src/mage/cards/d/DungeonCrawler.java @@ -0,0 +1,44 @@ +package mage.cards.d; + +import mage.MageInt; +import mage.abilities.common.CompletedDungeonTriggeredAbility; +import mage.abilities.common.EntersBattlefieldTappedAbility; +import mage.abilities.effects.common.ReturnSourceFromGraveyardToHandEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.Zone; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class DungeonCrawler extends CardImpl { + + public DungeonCrawler(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{B}"); + + this.subtype.add(SubType.ZOMBIE); + this.power = new MageInt(2); + this.toughness = new MageInt(1); + + // Dungeon Crawler enters the battlefield tapped. + this.addAbility(new EntersBattlefieldTappedAbility()); + + // Whenever you complete a dungeon, you may return Dungeon Crawler from your graveyard to your hand. + this.addAbility(new CompletedDungeonTriggeredAbility( + Zone.GRAVEYARD, new ReturnSourceFromGraveyardToHandEffect(), true + )); + } + + private DungeonCrawler(final DungeonCrawler card) { + super(card); + } + + @Override + public DungeonCrawler copy() { + return new DungeonCrawler(this); + } +} diff --git a/Mage.Sets/src/mage/cards/e/EllywickTumblestrum.java b/Mage.Sets/src/mage/cards/e/EllywickTumblestrum.java new file mode 100644 index 0000000000..e7634ce174 --- /dev/null +++ b/Mage.Sets/src/mage/cards/e/EllywickTumblestrum.java @@ -0,0 +1,93 @@ +package mage.cards.e; + +import mage.abilities.Ability; +import mage.abilities.LoyaltyAbility; +import mage.abilities.common.PlaneswalkerEntersWithLoyaltyCountersAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.GetEmblemEffect; +import mage.abilities.effects.keyword.VentureIntoTheDungeonEffect; +import mage.cards.*; +import mage.constants.*; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.game.command.emblems.EllywickTumblestrumEmblem; +import mage.players.Player; +import mage.target.common.TargetCardInLibrary; +import mage.watchers.common.CompletedDungeonWatcher; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class EllywickTumblestrum extends CardImpl { + + public EllywickTumblestrum(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.PLANESWALKER}, "{2}{G}{G}"); + + this.addSuperType(SuperType.LEGENDARY); + this.subtype.add(SubType.ELLYWICK); + this.addAbility(new PlaneswalkerEntersWithLoyaltyCountersAbility(4)); + + // +1: Venture into the dungeon. + this.addAbility(new LoyaltyAbility(new VentureIntoTheDungeonEffect(), 1)); + + // −2: Look at the top six cards of your library. You may reveal a creature card from among them and put it into your hand. If it's legendary, you gain 3 life. Put the rest on the bottom of your library in a random order. + this.addAbility(new LoyaltyAbility(new EllywickTumblestrumEffect(), -2)); + + // −7: You get an emblem with "Creatures you control have trample and haste and get +2/+2 for each differently named dungeon you've completed." + this.addAbility(new LoyaltyAbility( + new GetEmblemEffect(new EllywickTumblestrumEmblem()), -7 + ), new CompletedDungeonWatcher()); + } + + private EllywickTumblestrum(final EllywickTumblestrum card) { + super(card); + } + + @Override + public EllywickTumblestrum copy() { + return new EllywickTumblestrum(this); + } +} + +class EllywickTumblestrumEffect extends OneShotEffect { + + EllywickTumblestrumEffect() { + super(Outcome.Benefit); + staticText = "look at the top six cards of your library. You may reveal a creature card " + + "from among them and put it into your hand. If it's legendary, you gain 3 life. " + + "Put the rest on the bottom of your library in a random order"; + } + + private EllywickTumblestrumEffect(final EllywickTumblestrumEffect effect) { + super(effect); + } + + @Override + public EllywickTumblestrumEffect copy() { + return new EllywickTumblestrumEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null) { + return false; + } + Cards cards = new CardsImpl(player.getLibrary().getTopCards(game, 6)); + TargetCardInLibrary target = new TargetCardInLibrary(0, 1, StaticFilters.FILTER_CARD_CREATURE); + player.choose(outcome, cards, target, game); + Card card = cards.get(target.getFirstTarget(), game); + if (card != null) { + player.revealCards(source, new CardsImpl(card), game); + player.moveCards(card, Zone.HAND, source, game); + cards.remove(card); + if (card.isLegendary()) { + player.gainLife(3, game, source); + } + } + player.putCardsOnBottomOfLibrary(cards, game, source, false); + return true; + } +} diff --git a/Mage.Sets/src/mage/cards/g/GloomStalker.java b/Mage.Sets/src/mage/cards/g/GloomStalker.java new file mode 100644 index 0000000000..64c3222d98 --- /dev/null +++ b/Mage.Sets/src/mage/cards/g/GloomStalker.java @@ -0,0 +1,48 @@ +package mage.cards.g; + +import mage.MageInt; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.condition.common.CompletedDungeonCondition; +import mage.abilities.decorator.ConditionalContinuousEffect; +import mage.abilities.effects.common.continuous.GainAbilitySourceEffect; +import mage.abilities.keyword.DoubleStrikeAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubType; +import mage.watchers.common.CompletedDungeonWatcher; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class GloomStalker extends CardImpl { + + public GloomStalker(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{W}"); + + this.subtype.add(SubType.DWARF); + this.subtype.add(SubType.RANGER); + this.power = new MageInt(2); + this.toughness = new MageInt(3); + + // As long as you've completed a dungeon, Gloom Stalker has double strike. + this.addAbility(new SimpleStaticAbility(new ConditionalContinuousEffect( + new GainAbilitySourceEffect( + DoubleStrikeAbility.getInstance(), Duration.WhileOnBattlefield + ), CompletedDungeonCondition.instance, + "as long as you've completed a dungeon, {this} has double strike" + )).addHint(CompletedDungeonCondition.getHint()), new CompletedDungeonWatcher()); + } + + private GloomStalker(final GloomStalker card) { + super(card); + } + + @Override + public GloomStalker copy() { + return new GloomStalker(this); + } +} diff --git a/Mage.Sets/src/mage/cards/n/NadaarSelflessPaladin.java b/Mage.Sets/src/mage/cards/n/NadaarSelflessPaladin.java new file mode 100644 index 0000000000..4f7e401982 --- /dev/null +++ b/Mage.Sets/src/mage/cards/n/NadaarSelflessPaladin.java @@ -0,0 +1,58 @@ +package mage.cards.n; + +import mage.MageInt; +import mage.abilities.common.EntersBattlefieldOrAttacksSourceTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.condition.common.CompletedDungeonCondition; +import mage.abilities.decorator.ConditionalContinuousEffect; +import mage.abilities.effects.common.continuous.BoostControlledEffect; +import mage.abilities.effects.keyword.VentureIntoTheDungeonEffect; +import mage.abilities.keyword.VigilanceAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.watchers.common.CompletedDungeonWatcher; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class NadaarSelflessPaladin extends CardImpl { + + public NadaarSelflessPaladin(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{W}"); + + this.addSuperType(SuperType.LEGENDARY); + this.subtype.add(SubType.DRAGON); + this.subtype.add(SubType.KNIGHT); + this.power = new MageInt(3); + this.toughness = new MageInt(3); + + // Vigilance + this.addAbility(VigilanceAbility.getInstance()); + + // Whenever Nadaar, Selfless Paladin enters the battlefield or attacks, venture into the dungeon. + this.addAbility(new EntersBattlefieldOrAttacksSourceTriggeredAbility(new VentureIntoTheDungeonEffect())); + + // Other creatures you control get +1/+1 as long as you've completed a dungeon. + this.addAbility(new SimpleStaticAbility(new ConditionalContinuousEffect( + new BoostControlledEffect( + 1, 1, Duration.WhileOnBattlefield, true + ), CompletedDungeonCondition.instance, + "other creatures you control get +1/+1 as long as you've completed a dungeon" + )).addHint(CompletedDungeonCondition.getHint()), new CompletedDungeonWatcher()); + } + + private NadaarSelflessPaladin(final NadaarSelflessPaladin card) { + super(card); + } + + @Override + public NadaarSelflessPaladin copy() { + return new NadaarSelflessPaladin(this); + } +} diff --git a/Mage.Sets/src/mage/cards/s/ShortcutSeeker.java b/Mage.Sets/src/mage/cards/s/ShortcutSeeker.java new file mode 100644 index 0000000000..755321924c --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/ShortcutSeeker.java @@ -0,0 +1,40 @@ +package mage.cards.s; + +import mage.MageInt; +import mage.abilities.common.DealsCombatDamageToAPlayerTriggeredAbility; +import mage.abilities.effects.keyword.VentureIntoTheDungeonEffect; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class ShortcutSeeker extends CardImpl { + + public ShortcutSeeker(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{3}{U}"); + + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.ROGUE); + this.power = new MageInt(2); + this.toughness = new MageInt(5); + + // Whenever Shortcut Seeker deals combat damage to a player, venture into the dungeon. + this.addAbility(new DealsCombatDamageToAPlayerTriggeredAbility( + new VentureIntoTheDungeonEffect(), false + )); + } + + private ShortcutSeeker(final ShortcutSeeker card) { + super(card); + } + + @Override + public ShortcutSeeker copy() { + return new ShortcutSeeker(this); + } +} diff --git a/Mage.Sets/src/mage/sets/AdventuresInTheForgottenRealms.java b/Mage.Sets/src/mage/sets/AdventuresInTheForgottenRealms.java index 4a738597fd..3726e4303f 100644 --- a/Mage.Sets/src/mage/sets/AdventuresInTheForgottenRealms.java +++ b/Mage.Sets/src/mage/sets/AdventuresInTheForgottenRealms.java @@ -26,17 +26,23 @@ public final class AdventuresInTheForgottenRealms extends ExpansionSet { this.maxCardNumberInBooster = 275; cards.add(new SetCardInfo("Bruenor Battlehammer", 219, Rarity.UNCOMMON, mage.cards.b.BruenorBattlehammer.class)); + cards.add(new SetCardInfo("Cloister Gargoyle", 7, Rarity.UNCOMMON, mage.cards.c.CloisterGargoyle.class)); cards.add(new SetCardInfo("Drizzt Do'Urden", 220, Rarity.RARE, mage.cards.d.DrizztDoUrden.class)); + cards.add(new SetCardInfo("Dungeon Crawler", 99, Rarity.UNCOMMON, mage.cards.d.DungeonCrawler.class)); + cards.add(new SetCardInfo("Ellywick Tumblestrum", 181, Rarity.MYTHIC, mage.cards.e.EllywickTumblestrum.class)); cards.add(new SetCardInfo("Evolving Wilds", 353, Rarity.COMMON, mage.cards.e.EvolvingWilds.class)); cards.add(new SetCardInfo("Flumph", 15, Rarity.RARE, mage.cards.f.Flumph.class)); cards.add(new SetCardInfo("Forest", 278, Rarity.LAND, mage.cards.basiclands.Forest.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Gloom Stalker", 16, Rarity.COMMON, mage.cards.g.GloomStalker.class)); cards.add(new SetCardInfo("Island", 266, Rarity.LAND, mage.cards.basiclands.Island.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Lolth, Spider Queen", 112, Rarity.MYTHIC, mage.cards.l.LolthSpiderQueen.class)); cards.add(new SetCardInfo("Mountain", 274, Rarity.LAND, mage.cards.basiclands.Mountain.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Nadaar, Selfless Paladin", 27, Rarity.RARE, mage.cards.n.NadaarSelflessPaladin.class)); cards.add(new SetCardInfo("Plains", 262, Rarity.LAND, mage.cards.basiclands.Plains.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Portable Hole", 33, Rarity.UNCOMMON, mage.cards.p.PortableHole.class)); cards.add(new SetCardInfo("Power Word Kill", 114, Rarity.UNCOMMON, mage.cards.p.PowerWordKill.class)); cards.add(new SetCardInfo("Prosperous Innkeeper", 200, Rarity.UNCOMMON, mage.cards.p.ProsperousInnkeeper.class)); + cards.add(new SetCardInfo("Shortcut Seeker", 73, Rarity.COMMON, mage.cards.s.ShortcutSeeker.class)); cards.add(new SetCardInfo("Swamp", 270, Rarity.LAND, mage.cards.basiclands.Swamp.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Tasha's Hideous Laughter", 78, Rarity.RARE, mage.cards.t.TashasHideousLaughter.class)); cards.add(new SetCardInfo("Tiamat", 235, Rarity.MYTHIC, mage.cards.t.Tiamat.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/dungeons/DungeonTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/dungeons/DungeonTest.java new file mode 100644 index 0000000000..d6415d5b63 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/dungeons/DungeonTest.java @@ -0,0 +1,387 @@ +package org.mage.test.cards.dungeons; + +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.mana.GenericManaCost; +import mage.abilities.effects.keyword.VentureIntoTheDungeonEffect; +import mage.abilities.keyword.DoubleStrikeAbility; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import mage.game.command.Dungeon; +import org.junit.Assert; +import org.junit.Test; +import org.mage.test.player.TestPlayer; +import org.mage.test.serverside.base.CardTestPlayerBase; + +import java.util.stream.Stream; + +/** + * @author TheElk801 + */ +public class DungeonTest extends CardTestPlayerBase { + + private static final String TOMB_OF_ANNIHILATION = "Tomb of Annihilation"; + private static final String LOST_MINE_OF_PHANDELVER = "Lost Mine of Phandelver"; + private static final String DUNGEON_OF_THE_MAD_MAGE = "Dungeon of the Mad Mage"; + private static final String FLAMESPEAKER_ADEPT = "Flamespeaker Adept"; + private static final String GLOOM_STALKER = "Gloom Stalker"; + private static final String DUNGEON_CRAWLER = "Dungeon Crawler"; + private static final String SILVERCOAT_LION = "Silvercoat Lion"; + + private void makeTester() { + makeTester(playerA); + } + + private void makeTester(TestPlayer player) { + addCustomCardWithAbility( + "tester", player, + new SimpleActivatedAbility(new VentureIntoTheDungeonEffect(), new GenericManaCost(0)) + ); + } + + private Stream makeStream(TestPlayer player) { + return currentGame + .getState() + .getCommand() + .stream() + .filter(Dungeon.class::isInstance) + .map(Dungeon.class::cast) + .filter(dungeon -> dungeon.isControlledBy(player.getId())); + } + + private Dungeon getCurrentDungeon(TestPlayer player) { + Assert.assertTrue( + "Players should not control more than one dungeon", + makeStream(player).count() < 2 + ); + return makeStream(player).findFirst().orElse(null); + } + + private void assertDungeonRoom(String dungeonName, String roomName) { + assertDungeonRoom(playerA, dungeonName, roomName); + } + + private void assertDungeonRoom(TestPlayer player, String dungeonName, String roomName) { + Dungeon dungeon = getCurrentDungeon(player); + if (dungeonName == null) { + Assert.assertNull("There should be no dungeon", dungeon); + return; + } + Assert.assertNotNull("Dungeon should not be null", dungeon); + Assert.assertEquals("Dungeon should be " + dungeonName, dungeonName, dungeon.getName()); + Assert.assertEquals( + "Current room is " + roomName, + roomName, dungeon.getCurrentRoom().getName() + ); + } + + @Test + public void test__LostMineOfPhandelver_room1() { + makeTester(); + addCard(Zone.BATTLEFIELD, playerA, FLAMESPEAKER_ADEPT); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{0}:"); + setChoice(playerA, LOST_MINE_OF_PHANDELVER); + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + assertAllCommandsUsed(); + + assertPowerToughness(playerA, FLAMESPEAKER_ADEPT, 4, 3); + assertPermanentCount(playerA, "Goblin", 0); + assertDungeonRoom(LOST_MINE_OF_PHANDELVER, "Cave Entrance"); + assertLife(playerA, 20); + assertLife(playerB, 20); + assertHandCount(playerA, 0); + } + + @Test + public void test__LostMineOfPhandelver_room2() { + makeTester(); + addCard(Zone.BATTLEFIELD, playerA, FLAMESPEAKER_ADEPT); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{0}:"); + setChoice(playerA, LOST_MINE_OF_PHANDELVER); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{0}:"); + setChoice(playerA, "Yes"); // Goblin Lair + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + assertAllCommandsUsed(); + + assertPowerToughness(playerA, FLAMESPEAKER_ADEPT, 4, 3); + assertPermanentCount(playerA, "Goblin", 1); + assertDungeonRoom(LOST_MINE_OF_PHANDELVER, "Goblin Lair"); + assertLife(playerA, 20); + assertLife(playerB, 20); + assertHandCount(playerA, 0); + } + + @Test + public void test__LostMineOfPhandelver_room3() { + makeTester(); + addCard(Zone.BATTLEFIELD, playerA, FLAMESPEAKER_ADEPT); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{0}:"); + setChoice(playerA, LOST_MINE_OF_PHANDELVER); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{0}:"); + setChoice(playerA, "Yes"); // Goblin Lair + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{0}:"); + setChoice(playerA, "No"); // Dark Pool + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + assertAllCommandsUsed(); + + assertPowerToughness(playerA, FLAMESPEAKER_ADEPT, 4, 3); + assertPermanentCount(playerA, "Goblin", 1); + assertDungeonRoom(LOST_MINE_OF_PHANDELVER, "Dark Pool"); + assertLife(playerA, 20 + 1); + assertLife(playerB, 20 - 1); + assertHandCount(playerA, 0); + } + + @Test + public void test__LostMineOfPhandelver_room4() { + makeTester(); + addCard(Zone.BATTLEFIELD, playerA, FLAMESPEAKER_ADEPT); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{0}:"); + setChoice(playerA, LOST_MINE_OF_PHANDELVER); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{0}:"); + setChoice(playerA, "Yes"); // Goblin Lair + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{0}:"); + setChoice(playerA, "No"); // Dark Pool + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{0}:"); + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + assertAllCommandsUsed(); + + assertPowerToughness(playerA, FLAMESPEAKER_ADEPT, 4, 3); + assertPermanentCount(playerA, "Goblin", 1); + assertDungeonRoom(null, null); + assertLife(playerA, 20 + 1); + assertLife(playerB, 20 - 1); + assertHandCount(playerA, 1); + } + + @Test + public void test__LostMineOfPhandelver_multipleTurns() { + makeTester(); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{0}:"); + setChoice(playerA, LOST_MINE_OF_PHANDELVER); + activateAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "{0}:"); + setChoice(playerA, "Yes"); // Goblin Lair + activateAbility(2, PhaseStep.PRECOMBAT_MAIN, playerA, "{0}:"); + setChoice(playerA, "No"); // Dark Pool + activateAbility(2, PhaseStep.POSTCOMBAT_MAIN, playerA, "{0}:"); + + setStopAt(2, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Goblin", 1); + assertDungeonRoom(null, null); + assertLife(playerA, 20 + 1); + assertLife(playerB, 20 - 1); + assertHandCount(playerA, 1); + } + + @Test + public void test__LostMineOfPhandelver_rollback() { + makeTester(); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{0}:"); + setChoice(playerA, LOST_MINE_OF_PHANDELVER); + activateAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "{0}:"); + setChoice(playerA, "Yes"); // Goblin Lair + activateAbility(2, PhaseStep.PRECOMBAT_MAIN, playerA, "{0}:"); + setChoice(playerA, "No"); // Dark Pool + activateAbility(2, PhaseStep.POSTCOMBAT_MAIN, playerA, "{0}:"); + + rollbackTurns(2, PhaseStep.END_TURN, playerA, 0); + setStopAt(2, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Goblin", 1); + assertDungeonRoom(LOST_MINE_OF_PHANDELVER, "Goblin Lair"); + assertLife(playerA, 20); + assertLife(playerB, 20); + assertHandCount(playerA, 0); + } + + @Test + public void test__LostMineOfPhandelver_rollbackDifferentChoice() { + makeTester(); + addCard(Zone.BATTLEFIELD, playerA, SILVERCOAT_LION); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{0}:"); + setChoice(playerA, LOST_MINE_OF_PHANDELVER); + activateAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "{0}:"); + setChoice(playerA, "Yes"); // Goblin Lair + activateAbility(2, PhaseStep.PRECOMBAT_MAIN, playerA, "{0}:"); + setChoice(playerA, "No"); // Dark Pool + activateAbility(2, PhaseStep.POSTCOMBAT_MAIN, playerA, "{0}:"); + + rollbackTurns(2, PhaseStep.END_TURN, playerA, 0); + + rollbackAfterActionsStart(); + activateAbility(2, PhaseStep.PRECOMBAT_MAIN, playerA, "{0}:"); + setChoice(playerA, "Yes"); // Storeroom + addTarget(playerA, SILVERCOAT_LION); + rollbackAfterActionsEnd(); + + setStopAt(2, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertPowerToughness(playerA, SILVERCOAT_LION, 3, 3); + assertCounterCount(playerA, SILVERCOAT_LION, CounterType.P1P1, 1); + assertPermanentCount(playerA, "Goblin", 1); + assertDungeonRoom(LOST_MINE_OF_PHANDELVER, "Storeroom"); + assertLife(playerA, 20); + assertLife(playerB, 20); + assertHandCount(playerA, 0); + } + + @Test + public void test__Dungeon_multiplePlayers() { + makeTester(playerA); + addCard(Zone.BATTLEFIELD, playerA, FLAMESPEAKER_ADEPT); + makeTester(playerB); + addCard(Zone.BATTLEFIELD, playerB, FLAMESPEAKER_ADEPT); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{0}:"); + setChoice(playerA, LOST_MINE_OF_PHANDELVER); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{0}:"); + setChoice(playerA, "Yes"); // Goblin Lair + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{0}:"); + setChoice(playerA, "No"); // Dark Pool + + activateAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerB, "{0}:"); + setChoice(playerB, DUNGEON_OF_THE_MAD_MAGE); + waitStackResolved(1, PhaseStep.POSTCOMBAT_MAIN); + activateAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerB, "{0}:"); + waitStackResolved(1, PhaseStep.POSTCOMBAT_MAIN); + activateAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerB, "{0}:"); + setChoice(playerB, "Yes"); // Goblin Bazaar + waitStackResolved(1, PhaseStep.POSTCOMBAT_MAIN); + activateAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerB, "{0}:"); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertPowerToughness(playerA, FLAMESPEAKER_ADEPT, 4, 3); + assertPowerToughness(playerB, FLAMESPEAKER_ADEPT, 6, 3); + assertPermanentCount(playerA, "Goblin", 1); + assertPermanentCount(playerB, "Treasure", 1); + assertDungeonRoom(playerA, LOST_MINE_OF_PHANDELVER, "Dark Pool"); + assertDungeonRoom(playerB, DUNGEON_OF_THE_MAD_MAGE, "Lost Level"); + assertLife(playerA, 20 + 1); + assertLife(playerB, 20 - 1 + 1); + assertHandCount(playerA, 0); + assertHandCount(playerB, 0); + } + + @Test + public void test__CompletedDungeonCondition_true() { + makeTester(); + addCard(Zone.BATTLEFIELD, playerA, GLOOM_STALKER); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{0}:"); + setChoice(playerA, LOST_MINE_OF_PHANDELVER); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{0}:"); + setChoice(playerA, "Yes"); // Goblin Lair + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{0}:"); + setChoice(playerA, "No"); // Dark Pool + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{0}:"); + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + assertAllCommandsUsed(); + + assertAbility(playerA, GLOOM_STALKER, DoubleStrikeAbility.getInstance(), true); + assertDungeonRoom(null, null); + } + + @Test + public void test__CompletedDungeonCondition_false() { + addCard(Zone.BATTLEFIELD, playerA, GLOOM_STALKER); + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + assertAllCommandsUsed(); + + assertAbility(playerA, GLOOM_STALKER, DoubleStrikeAbility.getInstance(), false); + assertDungeonRoom(null, null); + } + + @Test + public void test__CompletedDungeonCondition_falseThenTrue() { + makeTester(); + addCard(Zone.BATTLEFIELD, playerA, GLOOM_STALKER); + + setStopAt(1, PhaseStep.UPKEEP); + execute(); + assertAbility(playerA, GLOOM_STALKER, DoubleStrikeAbility.getInstance(), false); + assertDungeonRoom(null, null); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{0}:"); + setChoice(playerA, LOST_MINE_OF_PHANDELVER); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{0}:"); + setChoice(playerA, "Yes"); // Goblin Lair + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{0}:"); + setChoice(playerA, "No"); // Dark Pool + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{0}:"); + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + assertAllCommandsUsed(); + + assertAbility(playerA, GLOOM_STALKER, DoubleStrikeAbility.getInstance(), true); + assertDungeonRoom(null, null); + } + + @Test + public void test__CompletedDungeonTriggeredAbility() { + makeTester(); + addCard(Zone.GRAVEYARD, playerA, DUNGEON_CRAWLER); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{0}:"); + setChoice(playerA, LOST_MINE_OF_PHANDELVER); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{0}:"); + setChoice(playerA, "Yes"); // Goblin Lair + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{0}:"); + setChoice(playerA, "No"); // Dark Pool + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{0}:"); + setChoice(playerA, "Yes"); // return Dungeon Crawler + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + execute(); + assertAllCommandsUsed(); + + assertHandCount(playerA, DUNGEON_CRAWLER, 1); + assertDungeonRoom(null, null); + } +} diff --git a/Mage/src/main/java/mage/abilities/AbilityImpl.java b/Mage/src/main/java/mage/abilities/AbilityImpl.java index 14376a487c..9c273bae43 100644 --- a/Mage/src/main/java/mage/abilities/AbilityImpl.java +++ b/Mage/src/main/java/mage/abilities/AbilityImpl.java @@ -1,9 +1,5 @@ package mage.abilities; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.UUID; import mage.MageIdentifier; import mage.MageObject; import mage.abilities.costs.*; @@ -21,6 +17,7 @@ import mage.cards.Card; import mage.cards.SplitCard; import mage.constants.*; import mage.game.Game; +import mage.game.command.Dungeon; import mage.game.command.Emblem; import mage.game.command.Plane; import mage.game.events.GameEvent; @@ -37,6 +34,11 @@ import mage.util.ThreadLocalStringBuilder; import mage.watchers.Watcher; import org.apache.log4j.Logger; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.UUID; + /** * @author BetaSteward_at_googlemail.com */ @@ -963,7 +965,7 @@ public abstract class AbilityImpl implements Ability { } MageObject object = game.getObject(this.getSourceId()); // emblem/planes are always actual - if (object instanceof Emblem || object instanceof Plane) { + if (object instanceof Emblem || object instanceof Dungeon || object instanceof Plane) { return true; } } diff --git a/Mage/src/main/java/mage/abilities/ActivatedAbilityImpl.java b/Mage/src/main/java/mage/abilities/ActivatedAbilityImpl.java index ecf20a1ca6..fac50900b3 100644 --- a/Mage/src/main/java/mage/abilities/ActivatedAbilityImpl.java +++ b/Mage/src/main/java/mage/abilities/ActivatedAbilityImpl.java @@ -13,9 +13,7 @@ import mage.abilities.mana.ManaOptions; import mage.cards.Card; import mage.constants.*; import mage.game.Game; -import mage.game.command.Commander; -import mage.game.command.Emblem; -import mage.game.command.Plane; +import mage.game.command.CommandObject; import mage.game.permanent.Permanent; import mage.util.CardUtil; @@ -212,12 +210,8 @@ public abstract class ActivatedAbilityImpl extends AbilityImpl implements Activa return true; } MageObject mageObject = game.getObject(this.sourceId); - if (mageObject instanceof Emblem) { - return ((Emblem) mageObject).isControlledBy(playerId); - } else if (mageObject instanceof Plane) { - return ((Plane) mageObject).isControlledBy(playerId); - } else if (mageObject instanceof Commander) { - return ((Commander) mageObject).isControlledBy(playerId); + if (mageObject instanceof CommandObject) { + return ((CommandObject) mageObject).isControlledBy(playerId); } else if (game.getState().getZone(this.sourceId) != Zone.BATTLEFIELD) { return ((Card) mageObject).isOwnedBy(playerId); } diff --git a/Mage/src/main/java/mage/abilities/common/CompletedDungeonTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/CompletedDungeonTriggeredAbility.java new file mode 100644 index 0000000000..4b7f768b66 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/common/CompletedDungeonTriggeredAbility.java @@ -0,0 +1,49 @@ +package mage.abilities.common; + +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.effects.Effect; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.events.GameEvent; + +/** + * @author TheElk801 + */ +public class CompletedDungeonTriggeredAbility extends TriggeredAbilityImpl { + + public CompletedDungeonTriggeredAbility(Effect effect) { + this(effect, false); + } + + public CompletedDungeonTriggeredAbility(Effect effect, boolean optional) { + this(Zone.BATTLEFIELD, effect, optional); + } + + public CompletedDungeonTriggeredAbility(Zone zone, Effect effect, boolean optional) { + super(zone, effect, optional); + } + + private CompletedDungeonTriggeredAbility(final CompletedDungeonTriggeredAbility ability) { + super(ability); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.DUNGEON_COMPLETED; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + return isControlledBy(event.getPlayerId()); + } + + @Override + public CompletedDungeonTriggeredAbility copy() { + return new CompletedDungeonTriggeredAbility(this); + } + + @Override + public String getRule() { + return "Whenever you complete a dungeon, " + super.getRule(); + } +} diff --git a/Mage/src/main/java/mage/abilities/condition/common/CompletedDungeonCondition.java b/Mage/src/main/java/mage/abilities/condition/common/CompletedDungeonCondition.java new file mode 100644 index 0000000000..6adf95c599 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/condition/common/CompletedDungeonCondition.java @@ -0,0 +1,30 @@ +package mage.abilities.condition.common; + +import mage.abilities.Ability; +import mage.abilities.condition.Condition; +import mage.abilities.hint.ConditionHint; +import mage.abilities.hint.Hint; +import mage.game.Game; +import mage.watchers.common.CompletedDungeonWatcher; + +/** + * @author TheElk801 + */ +public enum CompletedDungeonCondition implements Condition { + instance; + private static final Hint hint = new ConditionHint(instance, "You've completed a dungeon"); + + @Override + public boolean apply(Game game, Ability source) { + return CompletedDungeonWatcher.checkPlayer(source.getControllerId(), game); + } + + @Override + public String toString() { + return "you've completed a dungeon"; + } + + public static Hint getHint() { + return hint; + } +} diff --git a/Mage/src/main/java/mage/abilities/effects/common/combat/CantAttackTargetEffect.java b/Mage/src/main/java/mage/abilities/effects/common/combat/CantAttackTargetEffect.java index c0148d71d5..ffee7967c2 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/combat/CantAttackTargetEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/combat/CantAttackTargetEffect.java @@ -42,7 +42,9 @@ public class CantAttackTargetEffect extends RestrictionEffect { } String text = "target " + mode.getTargets().get(0).getTargetName() + " can't attack"; if (this.duration == Duration.EndOfTurn) { - text += " this turn"; + return text + " this turn"; + } else if (this.duration == Duration.UntilYourNextTurn) { + return text + " until your next turn"; } return text; } diff --git a/Mage/src/main/java/mage/abilities/effects/keyword/VentureIntoTheDungeonEffect.java b/Mage/src/main/java/mage/abilities/effects/keyword/VentureIntoTheDungeonEffect.java new file mode 100644 index 0000000000..9e4a9c682b --- /dev/null +++ b/Mage/src/main/java/mage/abilities/effects/keyword/VentureIntoTheDungeonEffect.java @@ -0,0 +1,32 @@ +package mage.abilities.effects.keyword; + +import mage.abilities.Ability; +import mage.abilities.effects.OneShotEffect; +import mage.constants.Outcome; +import mage.game.Game; + +/** + * @author TheElk801 + */ +public class VentureIntoTheDungeonEffect extends OneShotEffect { + + public VentureIntoTheDungeonEffect() { + super(Outcome.Benefit); + staticText = "venture into the dungeon. (Enter the first room or advance to the next room.)"; + } + + private VentureIntoTheDungeonEffect(final VentureIntoTheDungeonEffect effect) { + super(effect); + } + + @Override + public VentureIntoTheDungeonEffect copy() { + return new VentureIntoTheDungeonEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + game.ventureIntoDungeon(source.getControllerId()); + return true; + } +} diff --git a/Mage/src/main/java/mage/constants/CardType.java b/Mage/src/main/java/mage/constants/CardType.java index 15dc72b6b7..62b8cd986c 100644 --- a/Mage/src/main/java/mage/constants/CardType.java +++ b/Mage/src/main/java/mage/constants/CardType.java @@ -14,6 +14,7 @@ public enum CardType { ARTIFACT("Artifact", true, true), CONSPIRACY("Conspiracy", false, false), CREATURE("Creature", true, true), + DUNGEON("Dungeon", false, false), ENCHANTMENT("Enchantment", true, true), INSTANT("Instant", false, true), LAND("Land", true, true), diff --git a/Mage/src/main/java/mage/constants/MageObjectType.java b/Mage/src/main/java/mage/constants/MageObjectType.java index 8d08765e64..17bf39f3f6 100644 --- a/Mage/src/main/java/mage/constants/MageObjectType.java +++ b/Mage/src/main/java/mage/constants/MageObjectType.java @@ -53,6 +53,7 @@ public enum MageObjectType { TOKEN("Token", true, true), SPELL("Spell", false, true), PERMANENT("Permanent", true, true), + DUNGEON("Dungeon", false, false), EMBLEM("Emblem", false, false), COMMANDER("Commander", false, false), DESIGNATION("Designation", false, false), diff --git a/Mage/src/main/java/mage/constants/SubType.java b/Mage/src/main/java/mage/constants/SubType.java index 5049bc7cc4..39c1ee75a7 100644 --- a/Mage/src/main/java/mage/constants/SubType.java +++ b/Mage/src/main/java/mage/constants/SubType.java @@ -409,6 +409,7 @@ public enum SubType { DOMRI("Domri", SubTypeSet.PlaneswalkerType), DOOKU("Dooku", SubTypeSet.PlaneswalkerType, true), // Star Wars DOVIN("Dovin", SubTypeSet.PlaneswalkerType), + ELLYWICK("Ellywick", SubTypeSet.PlaneswalkerType), ELSPETH("Elspeth", SubTypeSet.PlaneswalkerType), ESTRID("Estrid", SubTypeSet.PlaneswalkerType), FREYALISE("Freyalise", SubTypeSet.PlaneswalkerType), diff --git a/Mage/src/main/java/mage/game/Game.java b/Mage/src/main/java/mage/game/Game.java index af4ad622b3..d4d83971fc 100644 --- a/Mage/src/main/java/mage/game/Game.java +++ b/Mage/src/main/java/mage/game/Game.java @@ -19,10 +19,7 @@ import mage.choices.Choice; import mage.constants.*; import mage.counters.Counters; import mage.game.combat.Combat; -import mage.game.command.CommandObject; -import mage.game.command.Commander; -import mage.game.command.Emblem; -import mage.game.command.Plane; +import mage.game.command.*; import mage.game.events.GameEvent; import mage.game.events.Listener; import mage.game.events.PlayerQueryEvent; @@ -80,6 +77,10 @@ public interface Game extends MageItem, Serializable { MageObject getEmblem(UUID objectId); + Dungeon getDungeon(UUID objectId); + + Dungeon getPlayerDungeon(UUID objectId); + UUID getControllerId(UUID objectId); UUID getOwnerId(UUID objectId); @@ -394,6 +395,10 @@ public interface Game extends MageItem, Serializable { void addCommander(Commander commander); + Dungeon addDungeon(Dungeon dungeon, UUID playerId); + + void ventureIntoDungeon(UUID playerId); + /** * Adds a permanent to the battlefield * diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java index 81939b25d4..c4da068ba5 100644 --- a/Mage/src/main/java/mage/game/GameImpl.java +++ b/Mage/src/main/java/mage/game/GameImpl.java @@ -38,10 +38,7 @@ import mage.filter.common.FilterCreaturePermanent; import mage.filter.predicate.mageobject.NamePredicate; import mage.filter.predicate.permanent.ControllerIdPredicate; import mage.game.combat.Combat; -import mage.game.command.CommandObject; -import mage.game.command.Commander; -import mage.game.command.Emblem; -import mage.game.command.Plane; +import mage.game.command.*; import mage.game.events.*; import mage.game.events.TableEvent.EventType; import mage.game.mulligan.Mulligan; @@ -355,7 +352,7 @@ public abstract class GameImpl implements Game, Serializable { if (item.getId().equals(objectId)) { return item; } - if (item.getSourceId().equals(objectId) && item instanceof Spell) { + if (item instanceof Spell && item.getSourceId().equals(objectId)) { return item; } } @@ -431,6 +428,59 @@ public abstract class GameImpl implements Game, Serializable { return null; } + @Override + public Dungeon getDungeon(UUID objectId) { + return state + .getCommand() + .stream() + .filter(commandObject -> commandObject.getId().equals(objectId)) + .filter(Dungeon.class::isInstance) + .map(Dungeon.class::cast) + .findFirst() + .orElse(null); + } + + @Override + public Dungeon getPlayerDungeon(UUID playerId) { + return state + .getCommand() + .stream() + .filter(commandObject -> commandObject.isControlledBy(playerId)) + .filter(Dungeon.class::isInstance) + .map(Dungeon.class::cast) + .findFirst() + .orElse(null); + } + + private void removeDungeon(Dungeon dungeon) { + if (dungeon == null) { + return; + } + Player player = getPlayer(dungeon.getControllerId()); + if (player != null) { + informPlayers(player.getLogName() + " has completed " + dungeon.getLogName()); + } + state.getCommand().remove(dungeon); + fireEvent(GameEvent.getEvent( + GameEvent.EventType.DUNGEON_COMPLETED, dungeon.getId(), null, + dungeon.getControllerId(), dungeon.getName(), 0 + )); + } + + private Dungeon getOrCreateDungeon(UUID playerId) { + Dungeon dungeon = this.getPlayerDungeon(playerId); + if (dungeon != null && dungeon.hasNextRoom()) { + return dungeon; + } + removeDungeon(dungeon); + return this.addDungeon(Dungeon.selectDungeon(playerId, this), playerId); + } + + @Override + public void ventureIntoDungeon(UUID playerId) { + this.getOrCreateDungeon(playerId).moveToNextRoom(playerId, this); + } + @Override public UUID getOwnerId(UUID objectId) { return getOwnerId(getObject(objectId)); @@ -1658,6 +1708,13 @@ public abstract class GameImpl implements Game, Serializable { state.addCommandObject(commander); } + @Override + public Dungeon addDungeon(Dungeon dungeon, UUID playerId) { + dungeon.setControllerId(playerId); + state.addCommandObject(dungeon); + return dungeon; + } + @Override public void addPermanent(Permanent permanent, int createOrder) { if (createOrder == 0) { @@ -1902,6 +1959,34 @@ public abstract class GameImpl implements Game, Serializable { } } + // If a Dungeon is on its last room and is not the source of any triggered abilities, it is removed + Set dungeonsToRemove = new HashSet<>(); + for (CommandObject commandObject : state.getCommand()) { + if (!(commandObject instanceof Dungeon)) { + continue; + } + Dungeon dungeon = (Dungeon) commandObject; + boolean removeDungeon = !dungeon.hasNextRoom() + && this.getStack() + .stream() + .filter(DungeonRoom::isRoomTrigger) + .map(StackObject::getSourceId) + .noneMatch(dungeon.getId()::equals) + && this.state + .getTriggered(dungeon.getControllerId()) + .stream() + .filter(DungeonRoom::isRoomTrigger) + .map(Ability::getSourceId) + .noneMatch(dungeon.getId()::equals); + if (removeDungeon) { + dungeonsToRemove.add(dungeon); + } + } + for (Dungeon dungeon : dungeonsToRemove) { + this.removeDungeon(dungeon); + somethingHappened = true; + } + // If a commander is in a graveyard or in exile and that card was put into that zone // since the last time state-based actions were checked, its owner may put it into the command zone. // signature spells goes to command zone all the time diff --git a/Mage/src/main/java/mage/game/command/Command.java b/Mage/src/main/java/mage/game/command/Command.java index 10a28122fc..b1b4803f3d 100644 --- a/Mage/src/main/java/mage/game/command/Command.java +++ b/Mage/src/main/java/mage/game/command/Command.java @@ -1,26 +1,20 @@ - - package mage.game.command; import java.util.ArrayList; /** - * * @author Viserion */ public class Command extends ArrayList { - public Command () {} - - public Command(final Command command) { - addAll(command); + public Command() { } - /*public void checkTriggers(GameEvent event, Game game) { - for (CommandObject commandObject: this) { - commandObject.checkTriggers(event, game); + private Command(final Command command) { + for (CommandObject commandObject : command) { + add(commandObject.copy()); } - }*/ + } public Command copy() { return new Command(this); diff --git a/Mage/src/main/java/mage/game/command/Dungeon.java b/Mage/src/main/java/mage/game/command/Dungeon.java new file mode 100644 index 0000000000..fd32eca9a2 --- /dev/null +++ b/Mage/src/main/java/mage/game/command/Dungeon.java @@ -0,0 +1,371 @@ +package mage.game.command; + +import mage.MageInt; +import mage.MageObject; +import mage.ObjectColor; +import mage.abilities.Abilities; +import mage.abilities.AbilitiesImpl; +import mage.abilities.Ability; +import mage.abilities.costs.mana.ManaCost; +import mage.abilities.costs.mana.ManaCosts; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.ContinuousEffect; +import mage.abilities.effects.Effect; +import mage.abilities.text.TextPart; +import mage.cards.FrameStyle; +import mage.choices.Choice; +import mage.choices.ChoiceImpl; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.game.Game; +import mage.game.command.dungeons.DungeonOfTheMadMage; +import mage.game.command.dungeons.LostMineOfPhandelver; +import mage.game.command.dungeons.TombOfAnnihilation; +import mage.game.events.GameEvent; +import mage.game.events.ZoneChangeEvent; +import mage.players.Player; +import mage.util.GameLog; +import mage.util.SubTypes; + +import java.util.*; + +/** + * @author TheElk801 + */ +public class Dungeon implements CommandObject { + + private static final Set dungeonNames = new HashSet<>(); + + static { + dungeonNames.add("Tomb of Annihilation"); + dungeonNames.add("Lost Mine of Phandelver"); + dungeonNames.add("Dungeon of the Mad Mage"); + } + + private static final ArrayList emptySet = new ArrayList<>(Arrays.asList(CardType.DUNGEON)); + private static final ObjectColor emptyColor = new ObjectColor(); + private static final ManaCosts emptyCost = new ManaCostsImpl<>(); + + private final String name; + private UUID id; + private UUID controllerId; + private boolean copy; + private MageObject copyFrom; // copied card INFO (used to call original adjusters) + private FrameStyle frameStyle; + private final Abilities abilites = new AbilitiesImpl<>(); + private final String expansionSetCodeForImage; + private final List dungeonRooms = new ArrayList<>(); + private DungeonRoom currentRoom = null; + + public Dungeon(String name, String expansionSetCodeForImage) { + this.id = UUID.randomUUID(); + this.name = name; + this.expansionSetCodeForImage = expansionSetCodeForImage; + } + + public Dungeon(final Dungeon dungeon) { + this.id = dungeon.id; + this.name = dungeon.name; + this.frameStyle = dungeon.frameStyle; + this.controllerId = dungeon.controllerId; + this.copy = dungeon.copy; + this.copyFrom = (dungeon.copyFrom != null ? dungeon.copyFrom : null); + this.expansionSetCodeForImage = dungeon.expansionSetCodeForImage; + this.copyRooms(dungeon); + } + + private void copyRooms(Dungeon dungeon) { + Map copyMap = new HashMap<>(); + for (DungeonRoom dungeonRoom : dungeon.dungeonRooms) { + DungeonRoom copiedRoom = copyMap.computeIfAbsent(dungeonRoom.getName(), (s) -> dungeonRoom.copy()); + for (DungeonRoom nextRoom : dungeonRoom.getNextRooms()) { + copiedRoom.addNextRoom(copyMap.computeIfAbsent(nextRoom.getName(), (s) -> nextRoom.copy())); + } + this.addRoom(copiedRoom); + } + this.currentRoom = copyMap.computeIfAbsent(dungeon.currentRoom.getName(), (s) -> dungeon.currentRoom.copy()); + } + + public void addRoom(DungeonRoom room) { + this.dungeonRooms.add(room); + room.getRoomTriggeredAbility().setSourceId(id); + this.abilites.add(room.getRoomTriggeredAbility()); + } + + public void moveToNextRoom(UUID playerId, Game game) { + if (currentRoom == null) { + currentRoom = dungeonRooms.get(0); + } else { + currentRoom = currentRoom.chooseNextRoom(playerId, game); + } + Player player = game.getPlayer(getControllerId()); + if (player != null) { + game.informPlayers(player.getLogName() + " has entered " + currentRoom.getName()); + } + game.fireEvent(GameEvent.getEvent( + GameEvent.EventType.ROOM_ENTERED, currentRoom.getId(), null, playerId + )); + } + + public DungeonRoom getCurrentRoom() { + return currentRoom; + } + + public boolean hasNextRoom() { + return currentRoom != null && currentRoom.hasNextRoom(); + } + + public List getRules() { + List rules = new ArrayList<>(); + rules.add("(" + ( + currentRoom != null ? + "Currently in " + currentRoom.getName() : + "Not currently in a room" + ) + ")"); + dungeonRooms.stream().map(DungeonRoom::toString).forEach(rules::add); + return rules; + } + + public static Dungeon selectDungeon(UUID playerId, Game game) { + Player player = game.getPlayer(playerId); + Choice choice = new ChoiceImpl(true); + choice.setMessage("Choose a dungeon to venture into"); + choice.setChoices(dungeonNames); + player.choose(Outcome.Neutral, choice, game); + switch (choice.getChoice()) { + case "Tomb of Annihilation": + return new TombOfAnnihilation(); + case "Lost Mine of Phandelver": + return new LostMineOfPhandelver(); + case "Dungeon of the Mad Mage": + return new DungeonOfTheMadMage(); + default: + throw new UnsupportedOperationException("A dungeon should have been chosen"); + } + } + + @Override + public FrameStyle getFrameStyle() { + return frameStyle; + } + + @Override + public void assignNewId() { + this.id = UUID.randomUUID(); + } + + @Override + public MageObject getSourceObject() { + return null; + } + + @Override + public UUID getSourceId() { + return null; + } + + @Override + public UUID getControllerId() { + return this.controllerId; + } + + public void setControllerId(UUID controllerId) { + this.controllerId = controllerId; + this.abilites.setControllerId(controllerId); + } + + @Override + public void setCopy(boolean isCopy, MageObject copyFrom) { + this.copy = isCopy; + this.copyFrom = (copyFrom != null ? copyFrom.copy() : null); + } + + @Override + public boolean isCopy() { + return this.copy; + } + + @Override + public MageObject getCopyFrom() { + return this.copyFrom; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getIdName() { + return getName() + " [" + getId().toString().substring(0, 3) + ']'; + } + + @Override + public String getLogName() { + return GameLog.getColoredObjectIdName(this); + } + + @Override + public String getImageName() { + return this.name; + } + + @Override + public void setName(String name) { + } + + @Override + public ArrayList getCardType() { + return emptySet; + } + + @Override + public SubTypes getSubtype() { + return new SubTypes(); + } + + @Override + public SubTypes getSubtype(Game game) { + return new SubTypes(); + } + + @Override + public boolean hasSubtype(SubType subtype, Game game) { + return false; + } + + @Override + public EnumSet getSuperType() { + return EnumSet.noneOf(SuperType.class); + } + + @Override + public Abilities getAbilities() { + return abilites; + } + + @Override + public boolean hasAbility(Ability ability, Game game) { + return getAbilities().contains(ability); + } + + @Override + public ObjectColor getColor() { + return emptyColor; + } + + @Override + public ObjectColor getColor(Game game) { + return emptyColor; + } + + @Override + public ObjectColor getFrameColor(Game game) { + return emptyColor; + } + + @Override + public ManaCosts getManaCost() { + return emptyCost; + } + + @Override + public int getManaValue() { + return 0; + } + + @Override + public MageInt getPower() { + return MageInt.EmptyMageInt; + } + + @Override + public MageInt getToughness() { + return MageInt.EmptyMageInt; + } + + @Override + public int getStartingLoyalty() { + return 0; + } + + @Override + public void setStartingLoyalty(int startingLoyalty) { + } + + @Override + public void adjustCosts(Ability ability, Game game) { + } + + @Override + public void adjustTargets(Ability ability, Game game) { + } + + @Override + public UUID getId() { + return this.id; + } + + @Override + public Dungeon copy() { + return new Dungeon(this); + } + + public String getExpansionSetCodeForImage() { + return expansionSetCodeForImage; + } + + @Override + public int getZoneChangeCounter(Game game) { + return 1; + } + + @Override + public void updateZoneChangeCounter(Game game, ZoneChangeEvent event) { + throw new UnsupportedOperationException("Unsupported operation"); + } + + @Override + public void setZoneChangeCounter(int value, Game game) { + throw new UnsupportedOperationException("Unsupported operation"); + } + + @Override + public boolean isAllCreatureTypes(Game game) { + return false; + } + + @Override + public void setIsAllCreatureTypes(boolean value) { + } + + @Override + public void setIsAllCreatureTypes(Game game, boolean value) { + } + + public void discardEffects() { + for (Ability ability : abilites) { + for (Effect effect : ability.getEffects()) { + if (effect instanceof ContinuousEffect) { + ((ContinuousEffect) effect).discard(); + } + } + } + } + + @Override + public List getTextParts() { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + public TextPart addTextPart(TextPart textPart) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + public void removePTCDA() { + } +} diff --git a/Mage/src/main/java/mage/game/command/DungeonRoom.java b/Mage/src/main/java/mage/game/command/DungeonRoom.java new file mode 100644 index 0000000000..bb1da3917b --- /dev/null +++ b/Mage/src/main/java/mage/game/command/DungeonRoom.java @@ -0,0 +1,167 @@ +package mage.game.command; + +import mage.abilities.TriggeredAbility; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.effects.Effect; +import mage.constants.Outcome; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.stack.StackAbility; +import mage.game.stack.StackObject; +import mage.players.Player; +import mage.target.Target; +import mage.util.CardUtil; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +/** + * @author TheElk801 + */ +public class DungeonRoom { + + private final UUID id; + private final String name; + private final List nextRooms = new ArrayList<>(); + private final RoomTriggeredAbility roomTriggeredAbility; + + public DungeonRoom(String name, Effect... effects) { + this.id = UUID.randomUUID(); + this.name = name; + roomTriggeredAbility = new RoomTriggeredAbility(this, effects); + } + + private DungeonRoom(final DungeonRoom room) { + this.id = room.id; + this.name = room.name; + this.roomTriggeredAbility = new RoomTriggeredAbility(this, room.roomTriggeredAbility); + } + + public DungeonRoom copy() { + return new DungeonRoom(this); + } + + public void addTarget(Target target) { + roomTriggeredAbility.addTarget(target); + } + + public void addNextRoom(DungeonRoom room) { + nextRooms.add(room); + } + + public RoomTriggeredAbility getRoomTriggeredAbility() { + return roomTriggeredAbility; + } + + public UUID getId() { + return id; + } + + @Override + public String toString() { + return roomTriggeredAbility.getText(); + } + + public String getName() { + return name; + } + + public boolean hasNextRoom() { + return !nextRooms.isEmpty(); + } + + List getNextRooms() { + return nextRooms; + } + + public DungeonRoom chooseNextRoom(UUID playerId, Game game) { + switch (nextRooms.size()) { + case 0: + return null; + case 1: + return nextRooms.get(0); + case 2: + DungeonRoom room1 = nextRooms.get(0); + DungeonRoom room2 = nextRooms.get(1); + Player player = game.getPlayer(playerId); + if (player == null) { + return null; + } + return player.chooseUse( + Outcome.Neutral, "Choose which room to go to", + null, room1.name, room2.name, null, game + ) ? room1 : room2; + default: + throw new UnsupportedOperationException("there shouldn't be more than two rooms to go to"); + } + } + + String generateDestinationText() { + if (nextRooms.isEmpty()) { + return ""; + } + return " (Leads to " + + nextRooms + .stream() + .map(DungeonRoom::getName) + .reduce((a, b) -> a + " or " + b) + .orElse("") + + ")"; + } + + public static boolean isRoomTrigger(StackObject stackObject) { + return stackObject instanceof StackAbility + && stackObject.getStackAbility() instanceof RoomTriggeredAbility; + } + + public static boolean isRoomTrigger(TriggeredAbility ability) { + return ability instanceof RoomTriggeredAbility; + } +} + +class RoomTriggeredAbility extends TriggeredAbilityImpl { + + private final DungeonRoom room; + + RoomTriggeredAbility(DungeonRoom room, Effect... effects) { + super(Zone.COMMAND, null, false); + this.room = room; + for (Effect effect : effects) { + this.addEffect(effect); + } + this.setRuleVisible(false); + } + + RoomTriggeredAbility(DungeonRoom room, final RoomTriggeredAbility ability) { + super(ability); + this.room = room; + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.ROOM_ENTERED; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + return event.getTargetId().equals(room.getId()); + } + + @Override + public RoomTriggeredAbility copy() { + return new RoomTriggeredAbility(this.room, this); + } + + public String getText() { + return room.getName() + " — " + + CardUtil.getTextWithFirstCharUpperCase(super.getRule()) + + room.generateDestinationText(); + } + + @Override + public String getRule() { + return "When you enter this room, " + super.getRule() + " (" + room.getName() + ")"; + } +} diff --git a/Mage/src/main/java/mage/game/command/dungeons/DungeonOfTheMadMage.java b/Mage/src/main/java/mage/game/command/dungeons/DungeonOfTheMadMage.java new file mode 100644 index 0000000000..e5eec10293 --- /dev/null +++ b/Mage/src/main/java/mage/game/command/dungeons/DungeonOfTheMadMage.java @@ -0,0 +1,189 @@ +package mage.game.command.dungeons; + +import mage.ApprovingObject; +import mage.abilities.Ability; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.common.GainLifeEffect; +import mage.abilities.effects.common.combat.CantAttackTargetEffect; +import mage.abilities.effects.keyword.ScryEffect; +import mage.cards.Card; +import mage.cards.Cards; +import mage.cards.CardsImpl; +import mage.constants.Duration; +import mage.constants.Outcome; +import mage.constants.Zone; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.game.command.Dungeon; +import mage.game.command.DungeonRoom; +import mage.game.permanent.token.SkeletonToken2; +import mage.game.permanent.token.TreasureToken; +import mage.players.Player; +import mage.target.common.TargetCardInHand; +import mage.target.common.TargetCreaturePermanent; + +/** + * @author TheElk801 + */ +public class DungeonOfTheMadMage extends Dungeon { + + public DungeonOfTheMadMage() { + super("Dungeon of the Mad Mage", "AFR"); + // (1) Yawning Portal — You gain 1 life. (→ 2) + DungeonRoom yawningPortal = new DungeonRoom("Yawning Portal", new GainLifeEffect(1)); + + // (2) Dungeon Level — Scry 1. (→ 3a or 3b) + DungeonRoom dungeonLevel = new DungeonRoom( + "Dungeon Level", new ScryEffect(1, false) + ); + + // (3a) Goblin Bazaar — Create a Treasure token. (→ 4) + DungeonRoom goblinBazaar = new DungeonRoom("Goblin Bazaar", new CreateTokenEffect(new TreasureToken())); + + // (3b) Twisted Caverns — Target creature can't attack until your next turn. (→ 4) + DungeonRoom twistedCaverns = new DungeonRoom( + "Twisted Caverns", new CantAttackTargetEffect(Duration.UntilYourNextTurn) + ); + twistedCaverns.addTarget(new TargetCreaturePermanent()); + + // (4) Lost Level — Scry 2. (→ 5a or 5b) + DungeonRoom lostLevel = new DungeonRoom("Lost Level", new ScryEffect(2, false)); + + // (5a) Runestone Caverns — Exile the top two cards of your library. You may play them. (→ 6) + DungeonRoom runestoneCaverns = new DungeonRoom("Runestone Caverns", new RunestoneCavernsEffect()); + + // (5b) Muiral's Graveyard — Create two 1/1 black Skeleton creature tokens. (→ 6) + DungeonRoom muiralsGraveyard = new DungeonRoom( + "Muiral's Graveyard", new CreateTokenEffect(new SkeletonToken2(), 2) + ); + + // (6) Deep Mines — Scry 3. (→ 7) + DungeonRoom deepMines = new DungeonRoom("Deep Mines", new ScryEffect(3, false)); + + // (7) Mad Wizard's Lair — Draw three cards and reveal them. You may cast one of them without paying its mana cost. + DungeonRoom madWizardsLair = new DungeonRoom("Mad Wizard's Lair", new MadWizardsLairEffect()); + + yawningPortal.addNextRoom(dungeonLevel); + dungeonLevel.addNextRoom(goblinBazaar); + dungeonLevel.addNextRoom(twistedCaverns); + goblinBazaar.addNextRoom(lostLevel); + twistedCaverns.addNextRoom(lostLevel); + lostLevel.addNextRoom(runestoneCaverns); + lostLevel.addNextRoom(muiralsGraveyard); + runestoneCaverns.addNextRoom(deepMines); + muiralsGraveyard.addNextRoom(deepMines); + deepMines.addNextRoom(madWizardsLair); + + this.addRoom(yawningPortal); + this.addRoom(dungeonLevel); + this.addRoom(goblinBazaar); + this.addRoom(twistedCaverns); + this.addRoom(lostLevel); + this.addRoom(runestoneCaverns); + this.addRoom(muiralsGraveyard); + this.addRoom(deepMines); + this.addRoom(madWizardsLair); + } + + private DungeonOfTheMadMage(final DungeonOfTheMadMage dungeon) { + super(dungeon); + } + + public DungeonOfTheMadMage copy() { + return new DungeonOfTheMadMage(this); + } +} + +class RunestoneCavernsEffect extends OneShotEffect { + + RunestoneCavernsEffect() { + super(Outcome.Benefit); + staticText = "exile the top two cards of your library. You may play them"; + } + + private RunestoneCavernsEffect(final RunestoneCavernsEffect effect) { + super(effect); + } + + @Override + public RunestoneCavernsEffect copy() { + return new RunestoneCavernsEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null) { + return false; + } + Cards cards = new CardsImpl(player.getLibrary().getTopCards(game, 2)); + if (cards.isEmpty()) { + return false; + } + player.moveCards(cards, Zone.EXILED, source, game); + while (!cards.isEmpty()) { + for (Card card : cards.getCards(game)) { + if (!player.chooseUse(Outcome.PlayForFree, "Play " + card.getName() + "?", source, game)) { + continue; + } + game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), Boolean.TRUE); + player.cast( + player.chooseAbilityForCast(card, game, false), + game, false, new ApprovingObject(source, game) + ); + game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), null); + } + cards.retainZone(Zone.EXILED, game); + if (cards.isEmpty() || !player.chooseUse( + Outcome.PlayForFree, "Continue playing the exiled cards?", source, game + )) { + break; + } + } + return true; + } +} + +class MadWizardsLairEffect extends OneShotEffect { + + MadWizardsLairEffect() { + super(Outcome.Benefit); + staticText = "draw three cards and reveal them. You may cast one of them without paying its mana cost"; + } + + private MadWizardsLairEffect(final MadWizardsLairEffect effect) { + super(effect); + } + + @Override + public MadWizardsLairEffect copy() { + return new MadWizardsLairEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null) { + return false; + } + Cards cards = new CardsImpl(player.getLibrary().getTopCards(game, 3)); + if (player.drawCards(3, source, game) != cards.size()) { + return true; + } + player.revealCards(source, cards, game); + TargetCardInHand target = new TargetCardInHand(0, 1, StaticFilters.FILTER_CARD_NON_LAND); + player.choose(Outcome.PlayForFree, cards, target, game); + Card card = player.getHand().get(target.getFirstTarget(), game); + if (card == null) { + return true; + } + game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), Boolean.TRUE); + player.cast( + player.chooseAbilityForCast(card, game, true), + game, true, new ApprovingObject(source, game) + ); + game.getState().setValue("PlayFromNotOwnHandZone" + card.getId(), null); + return true; + } +} diff --git a/Mage/src/main/java/mage/game/command/dungeons/LostMineOfPhandelver.java b/Mage/src/main/java/mage/game/command/dungeons/LostMineOfPhandelver.java new file mode 100644 index 0000000000..e3fc369b35 --- /dev/null +++ b/Mage/src/main/java/mage/game/command/dungeons/LostMineOfPhandelver.java @@ -0,0 +1,86 @@ +package mage.game.command.dungeons; + +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.GainLifeEffect; +import mage.abilities.effects.common.LoseLifeOpponentsEffect; +import mage.abilities.effects.common.continuous.BoostTargetEffect; +import mage.abilities.effects.common.counter.AddCountersTargetEffect; +import mage.abilities.effects.keyword.ScryEffect; +import mage.constants.Duration; +import mage.counters.CounterType; +import mage.game.command.Dungeon; +import mage.game.command.DungeonRoom; +import mage.game.permanent.token.GoblinToken; +import mage.game.permanent.token.TreasureToken; +import mage.target.common.TargetCreaturePermanent; + +/** + * @author TheElk801 + */ +public class LostMineOfPhandelver extends Dungeon { + + public LostMineOfPhandelver() { + super("Lost Mine of Phandelver", "AFR"); + // (1) Cave Entrance — Scry 1. (→ 2a or 2b) + DungeonRoom caveEntrance = new DungeonRoom( + "Cave Entrance", new ScryEffect(1, false) + ); + + // (2a) Goblin Lair — Create a 1/1 red Goblin creature token. (→ 3a or 3b) + DungeonRoom goblinLair = new DungeonRoom("Goblin Lair", new CreateTokenEffect(new GoblinToken())); + + // (2b) Mine Tunnels — Create a Treasure token. (→ 3b or 3c) + DungeonRoom mineTunnels = new DungeonRoom("Mine Tunnels", new CreateTokenEffect(new TreasureToken())); + + // (3a) Storeroom — Put a +1/+1 counter on target creature. (→ 4) + DungeonRoom storeroom = new DungeonRoom( + "Storeroom", new AddCountersTargetEffect(CounterType.P1P1.createInstance()) + ); + storeroom.addTarget(new TargetCreaturePermanent()); + + // (3b) Dark Pool — Each opponent loses 1 life and you gain 1 life. (→ 4) + DungeonRoom darkPool = new DungeonRoom( + "Dark Pool", new LoseLifeOpponentsEffect(1), + new GainLifeEffect(1).concatBy("and") + ); + + // (3c) Fungi Cavern — Target creature gets -4/-0 until your next turn. (→ 4) + DungeonRoom fungiCavern = new DungeonRoom( + "Fungi Cavern", new BoostTargetEffect(-4, 0, Duration.UntilYourNextTurn) + ); + fungiCavern.addTarget(new TargetCreaturePermanent()); + + // (4) Temple of Dumathoin — Draw a card. + DungeonRoom templeOfDumathoin = new DungeonRoom( + "Temple of Dumathoin", new DrawCardSourceControllerEffect(1) + ); + + caveEntrance.addNextRoom(goblinLair); + caveEntrance.addNextRoom(mineTunnels); + goblinLair.addNextRoom(storeroom); + goblinLair.addNextRoom(darkPool); + mineTunnels.addNextRoom(darkPool); + mineTunnels.addNextRoom(fungiCavern); + storeroom.addNextRoom(templeOfDumathoin); + darkPool.addNextRoom(templeOfDumathoin); + fungiCavern.addNextRoom(templeOfDumathoin); + + this.addRoom(caveEntrance); + this.addRoom(goblinLair); + this.addRoom(mineTunnels); + this.addRoom(storeroom); + this.addRoom(darkPool); + this.addRoom(fungiCavern); + this.addRoom(templeOfDumathoin); + } + + private LostMineOfPhandelver(final LostMineOfPhandelver dungeon) { + super(dungeon); + } + + @Override + public LostMineOfPhandelver copy() { + return new LostMineOfPhandelver(this); + } +} diff --git a/Mage/src/main/java/mage/game/command/dungeons/TombOfAnnihilation.java b/Mage/src/main/java/mage/game/command/dungeons/TombOfAnnihilation.java new file mode 100644 index 0000000000..78ce42d16a --- /dev/null +++ b/Mage/src/main/java/mage/game/command/dungeons/TombOfAnnihilation.java @@ -0,0 +1,266 @@ +package mage.game.command.dungeons; + +import mage.abilities.Ability; +import mage.abilities.dynamicvalue.common.CardTypeAssignment; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.common.CreateTokenEffect; +import mage.abilities.effects.common.LoseLifeAllPlayersEffect; +import mage.cards.Card; +import mage.cards.Cards; +import mage.cards.CardsImpl; +import mage.constants.CardType; +import mage.constants.Outcome; +import mage.filter.StaticFilters; +import mage.filter.common.FilterControlledPermanent; +import mage.filter.predicate.Predicates; +import mage.game.Game; +import mage.game.command.Dungeon; +import mage.game.command.DungeonRoom; +import mage.game.permanent.Permanent; +import mage.game.permanent.token.TheAtropalToken; +import mage.players.Player; +import mage.target.TargetPermanent; +import mage.target.common.TargetControlledPermanent; +import mage.target.common.TargetDiscard; + +import java.util.*; + +/** + * @author TheElk801 + */ +public final class TombOfAnnihilation extends Dungeon { + + static final FilterControlledPermanent filter + = new FilterControlledPermanent("an artifact, a creature, or a land"); + + static { + filter.add(Predicates.or( + CardType.ARTIFACT.getPredicate(), + CardType.CREATURE.getPredicate(), + CardType.LAND.getPredicate() + )); + } + + public TombOfAnnihilation() { + super("Tomb of Annihilation", "AFR"); + // (1) Trapped Entry — Each player loses 1 life. (→ 2a or 2b) + DungeonRoom trappedEntry = new DungeonRoom("Trapped Entry", new LoseLifeAllPlayersEffect(1)); + + // (2a) Veils of Fear — Each player loses 2 life unless they discard a card. (→ 3) + DungeonRoom veilsOfFear = new DungeonRoom("Veils of Fear", new VeilsOfFearEffect()); + + // (2b) Oubliette — Discard a card and sacrifice an artifact, a creature, and a land. (→ 4) + DungeonRoom oubliette = new DungeonRoom("Oubliette", new OublietteEffect()); + + // (3) Sandfall Cell — Each player loses 2 life unless they sacrifice an artifact, a creature, or a land. (→ 4) + DungeonRoom sandfallCell = new DungeonRoom("Sandfall Cell", new SandfallCellEffect()); + + // (4) Cradle of the Death God — Create The Atropal, a legendary 4/4 black God Horror creature token with deathtouch. + DungeonRoom cradleOfTheDeathGod = new DungeonRoom("Cradle of the Death God", new CreateTokenEffect(new TheAtropalToken())); + + trappedEntry.addNextRoom(veilsOfFear); + trappedEntry.addNextRoom(oubliette); + veilsOfFear.addNextRoom(sandfallCell); + oubliette.addNextRoom(sandfallCell); + sandfallCell.addNextRoom(cradleOfTheDeathGod); + + this.addRoom(trappedEntry); + this.addRoom(veilsOfFear); + this.addRoom(oubliette); + this.addRoom(sandfallCell); + this.addRoom(cradleOfTheDeathGod); + } + + private TombOfAnnihilation(final TombOfAnnihilation dungeon) { + super(dungeon); + } + + public TombOfAnnihilation copy() { + return new TombOfAnnihilation(this); + } +} + +class VeilsOfFearEffect extends OneShotEffect { + + VeilsOfFearEffect() { + super(Outcome.Neutral); + staticText = "each player loses 2 life unless they discard a card"; + } + + private VeilsOfFearEffect(final VeilsOfFearEffect effect) { + super(effect); + } + + @Override + public VeilsOfFearEffect copy() { + return new VeilsOfFearEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Map map = new HashMap<>(); + for (UUID playerId : game.getState().getPlayersInRange(source.getControllerId(), game)) { + Player player = game.getPlayer(playerId); + if (player == null) { + continue; + } + TargetDiscard target = new TargetDiscard(0, 1, StaticFilters.FILTER_CARD, playerId); + player.choose(Outcome.PreventDamage, target, source.getSourceId(), game); + map.put(playerId, game.getCard(target.getFirstTarget())); + } + for (Map.Entry entry : map.entrySet()) { + Player player = game.getPlayer(entry.getKey()); + if (player == null) { + continue; + } + if (entry.getValue() != null) { + player.discard(entry.getValue(), false, source, game); + } else { + player.loseLife(2, game, source, false); + } + } + return true; + } +} + +class OublietteEffect extends OneShotEffect { + + OublietteEffect() { + super(Outcome.Sacrifice); + staticText = "discard a card and sacrifice an artifact, a creature, and a land"; + } + + private OublietteEffect(final OublietteEffect effect) { + super(effect); + } + + @Override + public OublietteEffect copy() { + return new OublietteEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + if (player == null) { + return false; + } + player.discard(1, false, false, source, game); + int saccable = OublietteTarget.checkTargetCount(source, game); + if (saccable < 1) { + return true; + } + OublietteTarget target = new OublietteTarget(Math.min(saccable, 3)); + player.choose(Outcome.Sacrifice, target, source.getSourceId(), game); + for (UUID targetId : target.getTargets()) { + Permanent permanent = game.getPermanent(targetId); + if (permanent != null) { + permanent.sacrifice(source, game); + } + } + return true; + } +} + +class OublietteTarget extends TargetControlledPermanent { + + private static final CardTypeAssignment cardTypeAssigner = new CardTypeAssignment( + CardType.ARTIFACT, + CardType.CREATURE, + CardType.LAND + ); + private static final FilterControlledPermanent filter = TombOfAnnihilation.filter.copy(); + + static { + filter.setMessage("an artifact, a creature, and a land"); + } + + OublietteTarget(int numTargets) { + super(numTargets, numTargets, filter, true); + } + + private OublietteTarget(final OublietteTarget target) { + super(target); + } + + @Override + public OublietteTarget copy() { + return new OublietteTarget(this); + } + + @Override + public boolean canTarget(UUID playerId, UUID id, Ability ability, Game game) { + if (!super.canTarget(playerId, id, ability, game)) { + return false; + } + Permanent permanent = game.getPermanent(id); + if (permanent == null) { + return false; + } + if (this.getTargets().isEmpty()) { + return true; + } + Cards cards = new CardsImpl(this.getTargets()); + cards.add(permanent); + return cardTypeAssigner.getRoleCount(cards, game) >= cards.size(); + } + + + @Override + public Set possibleTargets(UUID sourceId, UUID sourceControllerId, Game game) { + Set possibleTargets = super.possibleTargets(sourceId, sourceControllerId, game); + possibleTargets.removeIf(uuid -> !this.canTarget(sourceControllerId, uuid, null, game)); + return possibleTargets; + } + + static int checkTargetCount(Ability source, Game game) { + List permanents = game + .getBattlefield() + .getActivePermanents(filter, source.getControllerId(), source.getSourceId(), game); + return cardTypeAssigner.getRoleCount(new CardsImpl(permanents), game); + } +} + +class SandfallCellEffect extends OneShotEffect { + + SandfallCellEffect() { + super(Outcome.Neutral); + staticText = "each player loses 2 life unless they sacrifice an artifact, a creature, or a land"; + } + + private SandfallCellEffect(final SandfallCellEffect effect) { + super(effect); + } + + @Override + public SandfallCellEffect copy() { + return new SandfallCellEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Map map = new HashMap<>(); + for (UUID playerId : game.getState().getPlayersInRange(source.getControllerId(), game)) { + Player player = game.getPlayer(playerId); + if (player == null) { + continue; + } + TargetPermanent target = new TargetPermanent(0, 1, TombOfAnnihilation.filter, true); + player.choose(Outcome.PreventDamage, target, source.getSourceId(), game); + map.put(playerId, game.getPermanent(target.getFirstTarget())); + } + for (Map.Entry entry : map.entrySet()) { + Player player = game.getPlayer(entry.getKey()); + if (player == null) { + continue; + } + if (entry.getValue() != null) { + entry.getValue().sacrifice(source, game); + } else { + player.loseLife(2, game, source, false); + } + } + return true; + } +} + diff --git a/Mage/src/main/java/mage/game/command/emblems/EllywickTumblestrumEmblem.java b/Mage/src/main/java/mage/game/command/emblems/EllywickTumblestrumEmblem.java new file mode 100644 index 0000000000..f2a2e19112 --- /dev/null +++ b/Mage/src/main/java/mage/game/command/emblems/EllywickTumblestrumEmblem.java @@ -0,0 +1,81 @@ +package mage.game.command.emblems; + +import mage.abilities.Ability; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.dynamicvalue.DynamicValue; +import mage.abilities.effects.Effect; +import mage.abilities.effects.common.continuous.BoostControlledEffect; +import mage.abilities.effects.common.continuous.GainAbilityControlledEffect; +import mage.abilities.hint.Hint; +import mage.abilities.keyword.HasteAbility; +import mage.abilities.keyword.TrampleAbility; +import mage.constants.Duration; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.game.command.Emblem; +import mage.watchers.common.CompletedDungeonWatcher; + +import java.util.Set; + +/** + * @author TheElk801 + */ +public final class EllywickTumblestrumEmblem extends Emblem { + + // −7: You get an emblem with "Creatures you control have trample and haste and get +2/+2 for each differently named dungeon you've completed." + public EllywickTumblestrumEmblem() { + this.setName("Emblem Ellywick"); + this.setExpansionSetCodeForImage("AFR"); + Ability ability = new SimpleStaticAbility(new GainAbilityControlledEffect( + TrampleAbility.getInstance(), Duration.EndOfGame, + StaticFilters.FILTER_PERMANENT_CREATURES + )); + ability.addEffect(new GainAbilityControlledEffect( + HasteAbility.getInstance(), Duration.EndOfGame, + StaticFilters.FILTER_PERMANENT_CREATURES + ).setText("and haste")); + ability.addEffect(new BoostControlledEffect( + EllywickTumblestrumEmblemValue.instance, + EllywickTumblestrumEmblemValue.instance, + Duration.EndOfGame + ).setText("and get +2/+2 for each differently named dungeon you've completed")); + this.getAbilities().add(ability.addHint(EllywickTumblestrumEmblemHint.instance)); + } +} + +enum EllywickTumblestrumEmblemValue implements DynamicValue { + instance; + + @Override + public int calculate(Game game, Ability sourceAbility, Effect effect) { + return 2 * CompletedDungeonWatcher.getCompletedNames(sourceAbility.getControllerId(), game).size(); + } + + @Override + public EllywickTumblestrumEmblemValue copy() { + return this; + } + + @Override + public String getMessage() { + return ""; + } +} + +enum EllywickTumblestrumEmblemHint implements Hint { + instance; + + @Override + public String getText(Game game, Ability ability) { + Set names = CompletedDungeonWatcher.getCompletedNames(ability.getControllerId(), game); + if (names.isEmpty()) { + return "No dungeons completed"; + } + return "Completed dungeons: " + String.join(", ", names); + } + + @Override + public EllywickTumblestrumEmblemHint copy() { + return this; + } +} diff --git a/Mage/src/main/java/mage/game/events/GameEvent.java b/Mage/src/main/java/mage/game/events/GameEvent.java index ad1f6d481c..51985cef0e 100644 --- a/Mage/src/main/java/mage/game/events/GameEvent.java +++ b/Mage/src/main/java/mage/game/events/GameEvent.java @@ -448,6 +448,13 @@ public class GameEvent implements Serializable { flag not used for this event */ VOTE, VOTED, + /* dungeons + targetId id of the room + sourceId sourceId of the ability causing player to venture + playerId player in the dungeon + */ + ROOM_ENTERED, + DUNGEON_COMPLETED, //custom events CUSTOM_EVENT } diff --git a/Mage/src/main/java/mage/game/permanent/token/SkeletonToken.java b/Mage/src/main/java/mage/game/permanent/token/SkeletonToken.java index 4f267bdf72..734c3a99dd 100644 --- a/Mage/src/main/java/mage/game/permanent/token/SkeletonToken.java +++ b/Mage/src/main/java/mage/game/permanent/token/SkeletonToken.java @@ -12,7 +12,7 @@ import mage.constants.Zone; public final class SkeletonToken extends TokenImpl { public SkeletonToken() { - super("Skeleton", "1/1 black Skeleton creature with \"{B}: Regenerate this creature\""); + super("Skeleton", "1/1 black Skeleton creature token with \"{B}: Regenerate this creature\""); cardType.add(CardType.CREATURE); this.subtype.add(SubType.SKELETON); color.setBlack(true); diff --git a/Mage/src/main/java/mage/game/permanent/token/SkeletonToken2.java b/Mage/src/main/java/mage/game/permanent/token/SkeletonToken2.java new file mode 100644 index 0000000000..d1f324204d --- /dev/null +++ b/Mage/src/main/java/mage/game/permanent/token/SkeletonToken2.java @@ -0,0 +1,28 @@ +package mage.game.permanent.token; + +import mage.MageInt; +import mage.constants.CardType; +import mage.constants.SubType; + +/** + * @author TheElk801 + */ +public final class SkeletonToken2 extends TokenImpl { + + public SkeletonToken2() { + super("Skeleton", "1/1 black Skeleton creature token"); + cardType.add(CardType.CREATURE); + this.subtype.add(SubType.SKELETON); + color.setBlack(true); + power = new MageInt(1); + toughness = new MageInt(1); + } + + public SkeletonToken2(final SkeletonToken2 token) { + super(token); + } + + public SkeletonToken2 copy() { + return new SkeletonToken2(this); + } +} diff --git a/Mage/src/main/java/mage/game/permanent/token/TheAtropalToken.java b/Mage/src/main/java/mage/game/permanent/token/TheAtropalToken.java new file mode 100644 index 0000000000..93bd1d8957 --- /dev/null +++ b/Mage/src/main/java/mage/game/permanent/token/TheAtropalToken.java @@ -0,0 +1,33 @@ +package mage.game.permanent.token; + +import mage.MageInt; +import mage.abilities.keyword.DeathtouchAbility; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.SuperType; + +/** + * @author TheElk801 + */ +public final class TheAtropalToken extends TokenImpl { + + public TheAtropalToken() { + super("The Atropal", "The Atropal, a legendary 4/4 black God Horror creature token with deathtouch"); + supertype.add(SuperType.LEGENDARY); + cardType.add(CardType.CREATURE); + color.setBlack(true); + subtype.add(SubType.GOD); + subtype.add(SubType.HORROR); + power = new MageInt(4); + toughness = new MageInt(4); + addAbility(DeathtouchAbility.getInstance()); + } + + public TheAtropalToken(final TheAtropalToken token) { + super(token); + } + + public TheAtropalToken copy() { + return new TheAtropalToken(this); + } +} diff --git a/Mage/src/main/java/mage/players/Player.java b/Mage/src/main/java/mage/players/Player.java index 3d9b815069..d7e5e6c25e 100644 --- a/Mage/src/main/java/mage/players/Player.java +++ b/Mage/src/main/java/mage/players/Player.java @@ -711,13 +711,13 @@ public interface Player extends MageItem, Copyable { /** * Player distributes amount among multiple options * - * @param outcome AI hint - * @param messages List of options to distribute amount among - * @param min Minimum value per option - * @param max Total amount to be distributed - * @param type MultiAmountType enum to set dialog options such as title and header - * @param game Game - * @return List of integers with size equal to messages.size(). The sum of the integers is equal to max. + * @param outcome AI hint + * @param messages List of options to distribute amount among + * @param min Minimum value per option + * @param max Total amount to be distributed + * @param type MultiAmountType enum to set dialog options such as title and header + * @param game Game + * @return List of integers with size equal to messages.size(). The sum of the integers is equal to max. */ List getMultiAmount(Outcome outcome, List messages, int min, int max, MultiAmountType type, Game game); @@ -1044,5 +1044,4 @@ public interface Player extends MageItem, Copyable { * @return */ FilterMana getPhyrexianColors(); - } diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index 389b9d9c2e..9ac7974a24 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -4726,5 +4726,4 @@ public abstract class PlayerImpl implements Player, Serializable { public String toString() { return getName() + " (" + super.getClass().getSimpleName() + ")"; } - } diff --git a/Mage/src/main/java/mage/watchers/common/CompletedDungeonWatcher.java b/Mage/src/main/java/mage/watchers/common/CompletedDungeonWatcher.java new file mode 100644 index 0000000000..a7407102df --- /dev/null +++ b/Mage/src/main/java/mage/watchers/common/CompletedDungeonWatcher.java @@ -0,0 +1,44 @@ +package mage.watchers.common; + +import mage.constants.WatcherScope; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.watchers.Watcher; + +import java.util.*; + +/** + * @author TheElk801 + */ +public class CompletedDungeonWatcher extends Watcher { + + private final Map> playerMap = new HashMap<>(); + private static final Set emptySet = new HashSet<>(); + + public CompletedDungeonWatcher() { + super(WatcherScope.GAME); + } + + @Override + public void watch(GameEvent event, Game game) { + switch (event.getType()) { + case DUNGEON_COMPLETED: + playerMap.computeIfAbsent(event.getPlayerId(), u -> new HashSet<>()).add(event.getData()); + return; + case BEGINNING_PHASE_PRE: + if (game.getTurnNum() == 1) { + playerMap.clear(); + } + } + } + + public static boolean checkPlayer(UUID playerId, Game game) { + CompletedDungeonWatcher watcher = game.getState().getWatcher(CompletedDungeonWatcher.class); + return watcher != null && !watcher.playerMap.getOrDefault(playerId, emptySet).isEmpty(); + } + + public static Set getCompletedNames(UUID playerId, Game game) { + CompletedDungeonWatcher watcher = game.getState().getWatcher(CompletedDungeonWatcher.class); + return watcher != null ? watcher.playerMap.getOrDefault(playerId, emptySet) : emptySet; + } +} diff --git a/Utils/mtg-cards-data.txt b/Utils/mtg-cards-data.txt index 59ee113ee7..a6444384ca 100644 --- a/Utils/mtg-cards-data.txt +++ b/Utils/mtg-cards-data.txt @@ -41320,12 +41320,14 @@ Temple of the False God|Commander 2021|326|U||Land|||{T}: Add {C}{C}. Activate o Temple of Triumph|Commander 2021|327|R||Land|||Temple of Triumph enters the battlefield tapped.$When Temple of Triumph enters the battlefield, scry 1.${T}: Add {R} or {W}.| Tranquil Thicket|Commander 2021|408|C||Land|||Tranquil Thicket enters the battlefield tapped.${T}: Add {G}.$Cycling {G}| Yavimaya Coast|Commander 2021|409|R||Land|||{T}: Add {C}.${T}: Add {G} or {U}. Yavimaya Coast deals 1 damage to you.| +Cloister Gargoyle|Adventures in the Forgotten Realms|7|U|{2}{W}|Artifact Creature - Gargoyle|0|4|When Cloister Gargoyle enters the battlefield, venture into the dungeon.$As long as you've completed a dungeon, Cloister Gargoyle gets +3/+0 and has flying.| Flumph|Adventures in the Forgotten Realms|15|R|{1}{W}|Creature - Jellyfish|0|4|Defender, flying$Whenever Flumph is dealt damage, you and target opponent each draw a card.| Gloom Stalker|Adventures in the Forgotten Realms|16|C|{2}{W}|Creature - Dwarf Ranger|2|3|As long as you've completed a dungeon, Gloom Stalker has double strike.| Nadaar, Selfless Paladin|Adventures in the Forgotten Realms|27|R|{2}{W}|Legendary Creature - Dragon Knight|3|3|Vigilance$Whenever Nadaar, Selfless Paladin enters the battlefield or attacks, venture into the dungeon.$Other creatures you control get +1/+1 as long as you've completed a dungeon.| Portable Hole|Adventures in the Forgotten Realms|33|U|{W}|Artifact|||When Portable Hole enters the battlefield, exile target nonland permanent an opponent controls with mana value 2 or less until Portable Hole leaves the battlefield.| Shortcut Seeker|Adventures in the Forgotten Realms|73|C|{3}{U}|Creature - Human Rogue|2|5|Whenever Shortcut Seeker deals combat damage to a player, venture into the dungeon.| Tasha's Hideous Laughter|Adventures in the Forgotten Realms|78|R|{1}{U}{U}|Sorcery|||Each opponent exiles cards from the top of their library until that player has exiled cards with total mana value 20 or more.| +Dungeon Crawler|Adventures in the Forgotten Realms|99|U|{B}|Creature - Zombie|2|1|Dungeon Crawler enters the battlefield tapped.$Whenever you complete a dungeon, you may return Dungeon Crawler from your graveyard to your hand.| Lolth, Spider Queen|Adventures in the Forgotten Realms|112|M|{3}{B}{B}|Legendary Planeswalker - Lolth|4|Whenever a creature you control dies, put a loyalty counter on Lolth, Spider Queen.$0: You draw a card and you lose 1 life.$−3: Create two 2/1 black Spider creature tokens with menace and reach.$−8: You get an emblem with "Whenever an opponent is dealt combat damage by one or more creatures you control, if that player lost less than 8 life this turn, they lose life equal to the difference."| Power Word Kill|Adventures in the Forgotten Realms|114|U|{1}{B}|Instant|||Destroy target non-Angel, non-Demon, non-Devil, non-Dragon creature.| Vorpal Sword|Adventures in the Forgotten Realms|124|R|{B}|Artifact - Equipment|||Equipped creature gets +2/+0 and has deathtouch.${5}{B}{B}{B}: Until end of turn, Vorpal Sword gains "Whenever equipped creature deals combat damage to a player, that player loses the game."$Equip {B}{B}|