new feature: Emblem Cards (#10498)

* new feature: Emblem Cards

Allows match/tournament creator to specify cards to give each player
emblem versions of (or just the starting player for symmetric effects).

Technical details:
- new UI for specifying emblem cards (.dck files)
  - available for all match/tournament types
- new class `EmblemOfCard`
- new method `copyWithZone` on `AbilityImpl` (used to make abilities
  work from command zone)
- new fields on `GameOptions` and `MatchOptions` for emblem cards
- emblems are granted after mulligans, before first turn (technically
  after Planechase starting plane creation)

* fixes

* defaults for emblem cards in match options (fixes quick game buttons)

* minor fixes

* use DeckCardInfo instead of Card for emblem cards options

* restore accessible parent properties

* fix images for card emblems

* look up cards in a way that preserves which art

* fix typos; make Emblem.sourceObject protected

* add descriptions to planechase and emblem cards

* fixes

* add some unit tests for known working cards

* fix author name

* add explanation comment

* fix up tests

* copyWithZone: no longer modifies zone for singleton abilities

* directly check for MageSingleton
This commit is contained in:
Artemis Kearney 2023-09-26 21:47:13 -05:00 committed by GitHub
parent 04dba063aa
commit 41874b0b4b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 774 additions and 39 deletions

View file

@ -22,11 +22,26 @@
<Layout>
<DimensionLayout dim="0">
<Group type="103" groupAlignment="0" attributes="0">
<Group type="102" alignment="1" attributes="0">
<Group type="102" attributes="0">
<EmptySpace max="-2" attributes="0"/>
<Group type="103" groupAlignment="1" attributes="0">
<Component id="jSeparator2" alignment="1" max="32767" attributes="0"/>
<Group type="103" groupAlignment="0" attributes="0">
<Component id="jSeparator4" alignment="1" max="32767" attributes="0"/>
<Component id="jSeparator2" alignment="1" max="32767" attributes="0"/>
<Group type="102" alignment="1" attributes="0">
<EmptySpace min="0" pref="0" max="32767" attributes="0"/>
<Component id="btnOK" min="-2" max="-2" attributes="0"/>
</Group>
<Component id="jSeparator3" alignment="0" max="32767" attributes="0"/>
<Group type="102" alignment="0" attributes="0">
<Component id="txtEmblemCardsPerPlayer" max="32767" attributes="2"/>
<EmptySpace min="-2" max="-2" attributes="0"/>
<Component id="btnEmblemCardsPerPlayer" min="-2" pref="24" max="-2" attributes="0"/>
</Group>
<Group type="102" alignment="0" attributes="0">
<Component id="txtEmblemCardsStartingPlayer" max="32767" attributes="2"/>
<EmptySpace min="-2" max="-2" attributes="0"/>
<Component id="btnEmblemCardsStartingPlayer" min="-2" pref="24" max="-2" attributes="0"/>
</Group>
<Group type="102" alignment="0" attributes="0">
<Component id="lblMulliganType" min="-2" max="-2" attributes="0"/>
<EmptySpace max="-2" attributes="0"/>
@ -35,20 +50,21 @@
<Group type="102" alignment="0" attributes="0">
<Component id="lblFreeMulligans" min="-2" max="-2" attributes="0"/>
<EmptySpace max="-2" attributes="0"/>
<Component id="spnFreeMulligans" pref="126" max="32767" attributes="0"/>
<Component id="spnFreeMulligans" max="32767" attributes="0"/>
</Group>
<Group type="102" alignment="0" attributes="0">
<Group type="102" attributes="0">
<Group type="103" groupAlignment="0" attributes="0">
<Component id="chkPlaneChase" min="-2" max="-2" attributes="0"/>
<Component id="lblGeneralOptions" alignment="0" min="-2" max="-2" attributes="0"/>
<Component id="lblVariantOptions" alignment="0" min="-2" max="-2" attributes="0"/>
<Component id="chkPlaneChase" min="-2" max="-2" attributes="0"/>
<Component id="chkEmblemCards" alignment="0" min="-2" max="-2" attributes="0"/>
<Component id="lblEmblemCardsPerPlayer" alignment="0" min="-2" max="-2" attributes="0"/>
<Component id="lblEmblemCardsStartingPlayer" alignment="0" min="-2" max="-2" attributes="0"/>
<Component id="lblGeneralOptions" alignment="0" min="-2" max="-2" attributes="0"/>
</Group>
<EmptySpace min="0" pref="125" max="32767" attributes="0"/>
</Group>
<Group type="102" alignment="1" attributes="0">
<EmptySpace min="0" pref="0" max="32767" attributes="0"/>
<Component id="btnOK" min="-2" max="-2" attributes="0"/>
</Group>
<Component id="planechaseDescriptionLabel" alignment="0" max="32767" attributes="0"/>
<Component id="emblemCardsDescriptionLabel" alignment="0" max="32767" attributes="0"/>
</Group>
<EmptySpace max="-2" attributes="0"/>
</Group>
@ -76,6 +92,28 @@
<EmptySpace max="-2" attributes="0"/>
<Component id="chkPlaneChase" min="-2" max="-2" attributes="0"/>
<EmptySpace max="-2" attributes="0"/>
<Component id="planechaseDescriptionLabel" min="-2" max="-2" attributes="0"/>
<EmptySpace max="-2" attributes="0"/>
<Component id="jSeparator3" min="-2" max="-2" attributes="0"/>
<EmptySpace max="-2" attributes="0"/>
<Component id="chkEmblemCards" min="-2" max="-2" attributes="0"/>
<EmptySpace max="-2" attributes="0"/>
<Component id="emblemCardsDescriptionLabel" min="-2" max="-2" attributes="0"/>
<EmptySpace max="-2" attributes="0"/>
<Component id="lblEmblemCardsPerPlayer" min="-2" max="-2" attributes="0"/>
<EmptySpace max="-2" attributes="0"/>
<Group type="103" groupAlignment="3" attributes="0">
<Component id="txtEmblemCardsPerPlayer" alignment="3" min="-2" max="-2" attributes="0"/>
<Component id="btnEmblemCardsPerPlayer" alignment="3" min="-2" pref="21" max="-2" attributes="0"/>
</Group>
<EmptySpace max="-2" attributes="0"/>
<Component id="lblEmblemCardsStartingPlayer" min="-2" max="-2" attributes="0"/>
<EmptySpace max="-2" attributes="0"/>
<Group type="103" groupAlignment="3" attributes="0">
<Component id="txtEmblemCardsStartingPlayer" alignment="3" min="-2" max="-2" attributes="0"/>
<Component id="btnEmblemCardsStartingPlayer" alignment="3" min="-2" pref="21" max="-2" attributes="0"/>
</Group>
<EmptySpace max="32767" attributes="0"/>
<Component id="jSeparator4" min="-2" max="-2" attributes="0"/>
<EmptySpace min="0" pref="0" max="-2" attributes="0"/>
<Component id="btnOK" min="-2" pref="30" max="-2" attributes="0"/>
@ -173,7 +211,7 @@
</Component>
<Component class="javax.swing.JCheckBox" name="chkPlaneChase">
<Properties>
<Property name="text" type="java.lang.String" value="PlaneChase"/>
<Property name="text" type="java.lang.String" value="Planechase"/>
<Property name="toolTipText" type="java.lang.String" value="Use the PlaneChase variant for your game."/>
</Properties>
<AccessibilityProperties>
@ -185,6 +223,19 @@
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="chkPlaneChaseActionPerformed"/>
</Events>
</Component>
<Component class="javax.swing.JLabel" name="planechaseDescriptionLabel">
<Properties>
<Property name="text" type="java.lang.String" value="&lt;html&gt;Shared planar deck of all implemented planes.&lt;br&gt;Uses a 9-sided planar die with 2 planeswalk sides and 2 chaos sides.&lt;br&gt;Some ability text may be incorrect.&lt;br&gt;Some rules details (such as who controls plane abilities) may be incorrect."/>
<Property name="verticalAlignment" type="int" value="1"/>
</Properties>
<AccessibilityProperties>
<Property name="AccessibleContext.accessibleName" type="java.lang.String" value="Planechase Description"/>
<Property name="AccessibleContext.accessibleDescription" type="java.lang.String" value="Shared planar deck of all implemented planes.&#xa;Uses a 9-sided planar die with 2 planeswalk sides and 2 chaos sides.&#xa;Some ability text may be incorrect.&#xa;Some rules details (such as who controls plane abilities) may be incorrect."/>
<Property name="AccessibleContext.accessibleParent" type="javax.accessibility.Accessible" editor="org.netbeans.modules.form.ComponentChooserEditor">
<ComponentRef name="chkPlaneChase"/>
</Property>
</AccessibilityProperties>
</Component>
<Component class="javax.swing.JSeparator" name="jSeparator4">
</Component>
<Component class="javax.swing.JButton" name="btnOK">
@ -195,5 +246,80 @@
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="btnOKActionPerformed"/>
</Events>
</Component>
<Component class="javax.swing.JSeparator" name="jSeparator3">
</Component>
<Component class="javax.swing.JCheckBox" name="chkEmblemCards">
<Properties>
<Property name="font" type="java.awt.Font" editor="org.netbeans.beaninfo.editors.FontEditor">
<Font name="Tahoma" size="11" style="1"/>
</Property>
<Property name="text" type="java.lang.String" value="Emblem Cards (Experimental)"/>
<Property name="toolTipText" type="java.lang.String" value="If enabled, select cards to give players emblem copies of"/>
</Properties>
<Events>
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="chkEmblemCardsActionPerformed"/>
</Events>
</Component>
<Component class="javax.swing.JButton" name="btnEmblemCardsPerPlayer">
<Properties>
<Property name="text" type="java.lang.String" value="..."/>
</Properties>
<Events>
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="btnEmblemCardsPerPlayerActionPerformed"/>
</Events>
</Component>
<Component class="javax.swing.JTextField" name="txtEmblemCardsPerPlayer">
<Events>
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="txtEmblemCardsPerPlayerActionPerformed"/>
</Events>
</Component>
<Component class="javax.swing.JLabel" name="lblEmblemCardsPerPlayer">
<Properties>
<Property name="text" type="java.lang.String" value="Per-Player File"/>
<Property name="toolTipText" type="java.lang.String" value="An emblem of each card in this file is given to each player"/>
</Properties>
<AccessibilityProperties>
<Property name="AccessibleContext.accessibleParent" type="javax.accessibility.Accessible" editor="org.netbeans.modules.form.ComponentChooserEditor">
<ComponentRef name="chkEmblemCards"/>
</Property>
</AccessibilityProperties>
</Component>
<Component class="javax.swing.JButton" name="btnEmblemCardsStartingPlayer">
<Properties>
<Property name="text" type="java.lang.String" value="..."/>
</Properties>
<Events>
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="btnEmblemCardsStartingPlayerActionPerformed"/>
</Events>
</Component>
<Component class="javax.swing.JTextField" name="txtEmblemCardsStartingPlayer">
<AccessibilityProperties>
<Property name="AccessibleContext.accessibleDescription" type="java.lang.String" value=""/>
</AccessibilityProperties>
<Events>
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="txtEmblemCardsStartingPlayerActionPerformed"/>
</Events>
</Component>
<Component class="javax.swing.JLabel" name="lblEmblemCardsStartingPlayer">
<Properties>
<Property name="text" type="java.lang.String" value="Starting Player File"/>
<Property name="toolTipText" type="java.lang.String" value="An emblem of every card in this file is given to the starting player (useful for symmetric effects)"/>
</Properties>
<AccessibilityProperties>
<Property name="AccessibleContext.accessibleParent" type="javax.accessibility.Accessible" editor="org.netbeans.modules.form.ComponentChooserEditor">
<ComponentRef name="chkEmblemCards"/>
</Property>
</AccessibilityProperties>
</Component>
<Component class="javax.swing.JLabel" name="emblemCardsDescriptionLabel">
<Properties>
<Property name="text" type="java.lang.String" value="&lt;html&gt;Give players emblems with the abilities of cards.&lt;br&gt;Note that some abilities may not function correctly from the command zone.&lt;br&gt;If anything breaks, please report it on GitHub."/>
<Property name="verticalAlignment" type="int" value="1"/>
</Properties>
<AccessibilityProperties>
<Property name="AccessibleContext.accessibleName" type="java.lang.String" value="Emblem Cards description"/>
<Property name="AccessibleContext.accessibleDescription" type="java.lang.String" value="Give players emblems with the abilities of cards.&#xa;Note that some abilities may not function correctly from the command zone.&#xa;If anything breaks, please report it on GitHub."/>
</AccessibilityProperties>
</Component>
</SubComponents>
</Form>

View file

@ -1,12 +1,21 @@
package mage.client.dialog;
import mage.cards.decks.Deck;
import mage.cards.decks.DeckFileFilter;
import mage.cards.decks.importer.DeckImporter;
import mage.client.MageFrame;
import mage.constants.MultiplayerAttackOption;
import mage.constants.RangeOfInfluence;
import mage.game.GameException;
import mage.game.match.MatchOptions;
import mage.game.mulligan.MulliganType;
import org.apache.log4j.Logger;
import javax.swing.*;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
/**
* App GUI: custom options for match/tournament
@ -19,28 +28,49 @@ public class CustomOptionsDialog extends MageDialog {
TABLE(
PreferencesDialog.KEY_NEW_TABLE_NUMBER_OF_FREE_MULLIGANS,
PreferencesDialog.KEY_NEW_TABLE_MULLIGAN_TYPE,
PreferencesDialog.KEY_NEW_TABLE_PLANECHASE
PreferencesDialog.KEY_NEW_TABLE_PLANECHASE,
PreferencesDialog.KEY_NEW_TABLE_EMBLEM_CARDS_ENABLED,
PreferencesDialog.KEY_NEW_TABLE_EMBLEM_CARDS_PER_PLAYER_FILE,
PreferencesDialog.KEY_NEW_TABLE_EMBLEM_CARDS_STARTING_PLAYER_FILE
),
TOURNEY(
PreferencesDialog.KEY_NEW_TOURNAMENT_NUMBER_OF_FREE_MULLIGANS,
PreferencesDialog.KEY_NEW_TOURNAMENT_MULLIGUN_TYPE,
PreferencesDialog.KEY_NEW_TOURNAMENT_PLANE_CHASE
PreferencesDialog.KEY_NEW_TOURNAMENT_PLANE_CHASE,
PreferencesDialog.KEY_NEW_TOURNAMENT_EMBLEM_CARDS_ENABLED,
PreferencesDialog.KEY_NEW_TOURNAMENT_EMBLEM_CARDS_PER_PLAYER_FILE,
PreferencesDialog.KEY_NEW_TOURNAMENT_EMBLEM_CARDS_STARTING_PLAYER_FILE
);
public final String NUMBER_OF_FREE_MULLIGANS;
public final String MULLIGAN_TYPE;
public final String PLANECHASE;
public final String EMBLEM_CARDS_ENABLED;
public final String EMBLEM_CARDS_PER_PLAYER_FILE;
public final String EMBLEM_CARDS_STARTING_PLAYER_FILE;
SaveLoadKeys(String numberOfFreeMulligans, String mulliganType, String planechase) {
SaveLoadKeys(
String numberOfFreeMulligans,
String mulliganType,
String planechase,
String emblemCardsEnabled,
String emblemCardsPerPlayerFile,
String emblemCardsStartingPlayerFile
) {
NUMBER_OF_FREE_MULLIGANS = numberOfFreeMulligans;
MULLIGAN_TYPE = mulliganType;
PLANECHASE = planechase;
EMBLEM_CARDS_ENABLED = emblemCardsEnabled;
EMBLEM_CARDS_PER_PLAYER_FILE = emblemCardsPerPlayerFile;
EMBLEM_CARDS_STARTING_PLAYER_FILE = emblemCardsStartingPlayerFile;
}
}
private static final Logger logger = Logger.getLogger(CustomOptionsDialog.class);
private final SaveLoadKeys saveLoadKeys;
private final JButton openButton;
private final JFileChooser fcSelectEmblemCardsPerPlayer;
private final JFileChooser fcSelectEmblemCardsStartingPlayer;
/**
* Creates new form NewTableDialog
@ -52,6 +82,12 @@ public class CustomOptionsDialog extends MageDialog {
this.spnFreeMulligans.setModel(new SpinnerNumberModel(0, 0, 5, 1));
cbMulliganType.setModel(new DefaultComboBoxModel(MulliganType.values()));
this.setModal(true);
fcSelectEmblemCardsPerPlayer = new JFileChooser();
fcSelectEmblemCardsPerPlayer.setAcceptAllFileFilterUsed(false);
fcSelectEmblemCardsPerPlayer.addChoosableFileFilter(new DeckFileFilter("dck", "XMage's deck files (*.dck)"));
fcSelectEmblemCardsStartingPlayer = new JFileChooser();
fcSelectEmblemCardsStartingPlayer.setAcceptAllFileFilterUsed(false);
fcSelectEmblemCardsStartingPlayer.addChoosableFileFilter(new DeckFileFilter("dck", "XMage's deck files (*.dck)"));
}
/**
@ -71,8 +107,18 @@ public class CustomOptionsDialog extends MageDialog {
jSeparator2 = new javax.swing.JSeparator();
lblVariantOptions = new javax.swing.JLabel();
chkPlaneChase = new javax.swing.JCheckBox();
planechaseDescriptionLabel = new javax.swing.JLabel();
jSeparator4 = new javax.swing.JSeparator();
btnOK = new javax.swing.JButton();
jSeparator3 = new javax.swing.JSeparator();
chkEmblemCards = new javax.swing.JCheckBox();
btnEmblemCardsPerPlayer = new javax.swing.JButton();
txtEmblemCardsPerPlayer = new javax.swing.JTextField();
lblEmblemCardsPerPlayer = new javax.swing.JLabel();
btnEmblemCardsStartingPlayer = new javax.swing.JButton();
txtEmblemCardsStartingPlayer = new javax.swing.JTextField();
lblEmblemCardsStartingPlayer = new javax.swing.JLabel();
emblemCardsDescriptionLabel = new javax.swing.JLabel();
setTitle("Custom Options");
@ -103,7 +149,7 @@ public class CustomOptionsDialog extends MageDialog {
lblVariantOptions.setFont(new java.awt.Font("Tahoma", 1, 11)); // NOI18N
lblVariantOptions.setText("Variant Options");
chkPlaneChase.setText("PlaneChase");
chkPlaneChase.setText("Planechase");
chkPlaneChase.setToolTipText("Use the PlaneChase variant for your game.");
chkPlaneChase.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
@ -111,6 +157,9 @@ public class CustomOptionsDialog extends MageDialog {
}
});
planechaseDescriptionLabel.setText("<html>Shared planar deck of all implemented planes.<br>Uses a 9-sided planar die with 2 planeswalk sides and 2 chaos sides.<br>Some ability text may be incorrect.<br>Some rules details (such as who controls plane abilities) may be incorrect.");
planechaseDescriptionLabel.setVerticalAlignment(javax.swing.SwingConstants.TOP);
btnOK.setText("OK");
btnOK.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
@ -118,32 +167,90 @@ public class CustomOptionsDialog extends MageDialog {
}
});
chkEmblemCards.setFont(new java.awt.Font("Tahoma", 1, 11)); // NOI18N
chkEmblemCards.setText("Emblem Cards (Experimental)");
chkEmblemCards.setToolTipText("If enabled, select cards to give players emblem copies of");
chkEmblemCards.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
chkEmblemCardsActionPerformed(evt);
}
});
btnEmblemCardsPerPlayer.setText("...");
btnEmblemCardsPerPlayer.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
btnEmblemCardsPerPlayerActionPerformed(evt);
}
});
txtEmblemCardsPerPlayer.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
txtEmblemCardsPerPlayerActionPerformed(evt);
}
});
lblEmblemCardsPerPlayer.setText("Per-Player File");
lblEmblemCardsPerPlayer.setToolTipText("An emblem of each card in this file is given to each player");
btnEmblemCardsStartingPlayer.setText("...");
btnEmblemCardsStartingPlayer.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
btnEmblemCardsStartingPlayerActionPerformed(evt);
}
});
txtEmblemCardsStartingPlayer.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
txtEmblemCardsStartingPlayerActionPerformed(evt);
}
});
lblEmblemCardsStartingPlayer.setText("Starting Player File");
lblEmblemCardsStartingPlayer.setToolTipText("An emblem of every card in this file is given to the starting player (useful for symmetric effects)");
emblemCardsDescriptionLabel.setText("<html>Give players emblems with the abilities of cards.<br>Note that some abilities may not function correctly from the command zone.<br>If anything breaks, please report it on GitHub.");
emblemCardsDescriptionLabel.setVerticalAlignment(javax.swing.SwingConstants.TOP);
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()
.addGroup(layout.createSequentialGroup()
.addContainerGap()
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING)
.addComponent(jSeparator2)
.addComponent(jSeparator4)
.addGroup(javax.swing.GroupLayout.Alignment.LEADING, layout.createSequentialGroup()
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addComponent(jSeparator4, javax.swing.GroupLayout.Alignment.TRAILING)
.addComponent(jSeparator2, javax.swing.GroupLayout.Alignment.TRAILING)
.addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
.addGap(0, 0, Short.MAX_VALUE)
.addComponent(btnOK))
.addComponent(jSeparator3)
.addGroup(layout.createSequentialGroup()
.addComponent(txtEmblemCardsPerPlayer)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(btnEmblemCardsPerPlayer, javax.swing.GroupLayout.PREFERRED_SIZE, 24, javax.swing.GroupLayout.PREFERRED_SIZE))
.addGroup(layout.createSequentialGroup()
.addComponent(txtEmblemCardsStartingPlayer)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(btnEmblemCardsStartingPlayer, javax.swing.GroupLayout.PREFERRED_SIZE, 24, javax.swing.GroupLayout.PREFERRED_SIZE))
.addGroup(layout.createSequentialGroup()
.addComponent(lblMulliganType)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(cbMulliganType, 0, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
.addGroup(javax.swing.GroupLayout.Alignment.LEADING, layout.createSequentialGroup()
.addGroup(layout.createSequentialGroup()
.addComponent(lblFreeMulligans)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(spnFreeMulligans, javax.swing.GroupLayout.DEFAULT_SIZE, 126, Short.MAX_VALUE))
.addGroup(javax.swing.GroupLayout.Alignment.LEADING, layout.createSequentialGroup()
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addComponent(chkPlaneChase)
.addComponent(lblGeneralOptions)
.addComponent(lblVariantOptions))
.addGap(0, 125, Short.MAX_VALUE))
.addComponent(spnFreeMulligans))
.addGroup(layout.createSequentialGroup()
.addGap(0, 0, Short.MAX_VALUE)
.addComponent(btnOK)))
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addComponent(lblVariantOptions)
.addComponent(chkPlaneChase)
.addComponent(chkEmblemCards)
.addComponent(lblEmblemCardsPerPlayer)
.addComponent(lblEmblemCardsStartingPlayer)
.addComponent(lblGeneralOptions))
.addGap(0, 0, Short.MAX_VALUE))
.addComponent(planechaseDescriptionLabel)
.addComponent(emblemCardsDescriptionLabel))
.addContainerGap())
);
layout.setVerticalGroup(
@ -166,6 +273,26 @@ public class CustomOptionsDialog extends MageDialog {
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(chkPlaneChase)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(planechaseDescriptionLabel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(jSeparator3, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(chkEmblemCards)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(emblemCardsDescriptionLabel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(lblEmblemCardsPerPlayer)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
.addComponent(txtEmblemCardsPerPlayer, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
.addComponent(btnEmblemCardsPerPlayer, javax.swing.GroupLayout.PREFERRED_SIZE, 21, javax.swing.GroupLayout.PREFERRED_SIZE))
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(lblEmblemCardsStartingPlayer)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
.addComponent(txtEmblemCardsStartingPlayer, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
.addComponent(btnEmblemCardsStartingPlayer, javax.swing.GroupLayout.PREFERRED_SIZE, 21, javax.swing.GroupLayout.PREFERRED_SIZE))
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
.addComponent(jSeparator4, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
.addGap(0, 0, 0)
.addComponent(btnOK, javax.swing.GroupLayout.PREFERRED_SIZE, 30, javax.swing.GroupLayout.PREFERRED_SIZE)
@ -182,6 +309,14 @@ public class CustomOptionsDialog extends MageDialog {
spnFreeMulligans.getAccessibleContext().setAccessibleDescription("Select the number of free mulligans");
spnFreeMulligans.getAccessibleContext().setAccessibleParent(lblFreeMulligans);
chkPlaneChase.getAccessibleContext().setAccessibleParent(lblVariantOptions);
planechaseDescriptionLabel.getAccessibleContext().setAccessibleName("Planechase Description");
planechaseDescriptionLabel.getAccessibleContext().setAccessibleDescription("Shared planar deck of all implemented planes.\nUses a 9-sided planar die with 2 planeswalk sides and 2 chaos sides.\nSome ability text may be incorrect.\nSome rules details (such as who controls plane abilities) may be incorrect.");
planechaseDescriptionLabel.getAccessibleContext().setAccessibleParent(chkPlaneChase);
lblEmblemCardsPerPlayer.getAccessibleContext().setAccessibleParent(chkEmblemCards);
txtEmblemCardsStartingPlayer.getAccessibleContext().setAccessibleDescription("");
lblEmblemCardsStartingPlayer.getAccessibleContext().setAccessibleParent(chkEmblemCards);
emblemCardsDescriptionLabel.getAccessibleContext().setAccessibleName("Emblem Cards description");
emblemCardsDescriptionLabel.getAccessibleContext().setAccessibleDescription("Give players emblems with the abilities of cards.\nNote that some abilities may not function correctly from the command zone.\nIf anything breaks, please report it on GitHub.");
pack();
}// </editor-fold>//GEN-END:initComponents
@ -205,10 +340,50 @@ public class CustomOptionsDialog extends MageDialog {
updateActiveCount();
}//GEN-LAST:event_chkPlaneChaseActionPerformed
private void chkEmblemCardsActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_chkEmblemCardsActionPerformed
updateActiveCount();
}//GEN-LAST:event_chkEmblemCardsActionPerformed
private void btnEmblemCardsPerPlayerActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnEmblemCardsPerPlayerActionPerformed
loadEmblemCardFile(false);
}//GEN-LAST:event_btnEmblemCardsPerPlayerActionPerformed
private void txtEmblemCardsPerPlayerActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_txtEmblemCardsPerPlayerActionPerformed
}//GEN-LAST:event_txtEmblemCardsPerPlayerActionPerformed
private void btnEmblemCardsStartingPlayerActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_btnEmblemCardsStartingPlayerActionPerformed
loadEmblemCardFile(true);
}//GEN-LAST:event_btnEmblemCardsStartingPlayerActionPerformed
private void txtEmblemCardsStartingPlayerActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_txtEmblemCardsStartingPlayerActionPerformed
}//GEN-LAST:event_txtEmblemCardsStartingPlayerActionPerformed
public void showDialog() {
this.setLocation(150, 100);
this.setVisible(true);
}
private void loadEmblemCardFile(boolean isStartingPlayer) {
JFileChooser fileChooser = isStartingPlayer ? fcSelectEmblemCardsStartingPlayer : fcSelectEmblemCardsPerPlayer;
JTextField textField = isStartingPlayer ? txtEmblemCardsStartingPlayer : txtEmblemCardsPerPlayer;
String prefKey = isStartingPlayer ? "lastStartingPlayerEmblemCardsFolder" : "lastPerPlayerEmblemCardsFolder";
String lastFolder = MageFrame.getPreferences().get(prefKey, "");
if (!lastFolder.isEmpty()) {
fileChooser.setCurrentDirectory(new File(lastFolder));
}
int ret = fileChooser.showDialog(this, "Select Emblem Cards");
if (ret == JFileChooser.APPROVE_OPTION) {
File file = fileChooser.getSelectedFile();
textField.setText(file.getPath());
try {
MageFrame.getPreferences().put(prefKey, file.getCanonicalPath());
} catch (IOException ex) {
}
}
fileChooser.setSelectedFile(null);
}
public void onLoadSettings(int version) {
@ -231,6 +406,9 @@ public class CustomOptionsDialog extends MageDialog {
this.chkPlaneChase.setSelected(PreferencesDialog.getCachedValue(saveLoadKeys.PLANECHASE + versionStr, "No").equals("Yes"));
this.spnFreeMulligans.setValue(Integer.parseInt(PreferencesDialog.getCachedValue(saveLoadKeys.NUMBER_OF_FREE_MULLIGANS + versionStr, "0")));
this.cbMulliganType.setSelectedItem(MulliganType.valueByName(PreferencesDialog.getCachedValue(saveLoadKeys.MULLIGAN_TYPE + versionStr, MulliganType.GAME_DEFAULT.toString())));
this.chkEmblemCards.setSelected(PreferencesDialog.getCachedValue(saveLoadKeys.EMBLEM_CARDS_ENABLED + versionStr, "No").equals("Yes"));
this.txtEmblemCardsPerPlayer.setText(PreferencesDialog.getCachedValue(saveLoadKeys.EMBLEM_CARDS_PER_PLAYER_FILE, ""));
this.txtEmblemCardsStartingPlayer.setText(PreferencesDialog.getCachedValue(saveLoadKeys.EMBLEM_CARDS_STARTING_PLAYER_FILE, ""));
updateActiveCount();
}
@ -250,6 +428,10 @@ public class CustomOptionsDialog extends MageDialog {
PreferencesDialog.saveValue(saveLoadKeys.NUMBER_OF_FREE_MULLIGANS + versionStr, Integer.toString(options.getFreeMulligans()));
PreferencesDialog.saveValue(saveLoadKeys.MULLIGAN_TYPE + versionStr, options.getMulliganType().toString());
PreferencesDialog.saveValue(saveLoadKeys.PLANECHASE + versionStr, options.isPlaneChase() ? "Yes" : "No");
PreferencesDialog.saveValue(saveLoadKeys.EMBLEM_CARDS_ENABLED + versionStr,
!(options.getGlobalEmblemCards().isEmpty() && options.getPerPlayerEmblemCards().isEmpty()) ? "Yes" : "No");
PreferencesDialog.saveValue(saveLoadKeys.EMBLEM_CARDS_PER_PLAYER_FILE + versionStr, txtEmblemCardsPerPlayer.getText());
PreferencesDialog.saveValue(saveLoadKeys.EMBLEM_CARDS_STARTING_PLAYER_FILE + versionStr, txtEmblemCardsStartingPlayer.getText());
}
/**
@ -259,6 +441,42 @@ public class CustomOptionsDialog extends MageDialog {
options.setFreeMulligans((Integer) spnFreeMulligans.getValue());
options.setMullgianType((MulliganType) cbMulliganType.getSelectedItem());
options.setPlaneChase(chkPlaneChase.isSelected());
if (chkEmblemCards.isSelected()) {
if (!txtEmblemCardsPerPlayer.getText().isEmpty()) {
Deck perPlayerEmblemDeck = null;
try {
perPlayerEmblemDeck = Deck.load(DeckImporter.importDeckFromFile(txtEmblemCardsPerPlayer.getText(), true), true, true);
} catch (GameException e1) {
JOptionPane.showMessageDialog(MageFrame.getDesktop(), e1.getMessage(), "Error loading deck", JOptionPane.ERROR_MESSAGE);
}
if (perPlayerEmblemDeck != null) {
perPlayerEmblemDeck.clearLayouts();
options.setPerPlayerEmblemCards(perPlayerEmblemDeck.getDeckCardLists().getCards());
}
else {
options.setPerPlayerEmblemCards(Collections.emptySet());
}
}
if (!txtEmblemCardsStartingPlayer.getText().isEmpty()) {
Deck startingPlayerEmblemDeck = null;
try {
startingPlayerEmblemDeck = Deck.load(DeckImporter.importDeckFromFile(txtEmblemCardsStartingPlayer.getText(), true), true, true);
} catch (GameException e1) {
JOptionPane.showMessageDialog(MageFrame.getDesktop(), e1.getMessage(), "Error loading deck", JOptionPane.ERROR_MESSAGE);
}
if (startingPlayerEmblemDeck != null) {
startingPlayerEmblemDeck.clearLayouts();
options.setGlobalEmblemCards(startingPlayerEmblemDeck.getDeckCardLists().getCards());
}
else {
options.setGlobalEmblemCards(Collections.emptySet());
}
}
}
else {
options.setPerPlayerEmblemCards(Collections.emptySet());
options.setGlobalEmblemCards(Collections.emptySet());
}
}
public void updateActiveCount() {
@ -266,6 +484,7 @@ public class CustomOptionsDialog extends MageDialog {
if ((Integer)spnFreeMulligans.getValue() > 0) activeCount++;
if (!cbMulliganType.getSelectedItem().toString().equals(MulliganType.GAME_DEFAULT.toString())) activeCount++;
if (chkPlaneChase.isSelected()) activeCount++;
if (chkEmblemCards.isSelected()) activeCount++;
if (activeCount == 0) {
openButton.setText("Custom Options...");
}
@ -275,15 +494,25 @@ public class CustomOptionsDialog extends MageDialog {
}
// Variables declaration - do not modify//GEN-BEGIN:variables
private javax.swing.JButton btnEmblemCardsPerPlayer;
private javax.swing.JButton btnEmblemCardsStartingPlayer;
private javax.swing.JButton btnOK;
private javax.swing.JComboBox<String> cbMulliganType;
private javax.swing.JCheckBox chkEmblemCards;
private javax.swing.JCheckBox chkPlaneChase;
private javax.swing.JLabel emblemCardsDescriptionLabel;
private javax.swing.JSeparator jSeparator2;
private javax.swing.JSeparator jSeparator3;
private javax.swing.JSeparator jSeparator4;
private javax.swing.JLabel lblEmblemCardsPerPlayer;
private javax.swing.JLabel lblEmblemCardsStartingPlayer;
private javax.swing.JLabel lblFreeMulligans;
private javax.swing.JLabel lblGeneralOptions;
private javax.swing.JLabel lblMulliganType;
private javax.swing.JLabel lblVariantOptions;
private javax.swing.JLabel planechaseDescriptionLabel;
private javax.swing.JSpinner spnFreeMulligans;
private javax.swing.JTextField txtEmblemCardsPerPlayer;
private javax.swing.JTextField txtEmblemCardsStartingPlayer;
// End of variables declaration//GEN-END:variables
}

View file

@ -213,6 +213,9 @@ public class PreferencesDialog extends javax.swing.JDialog {
public static final String KEY_NEW_TABLE_MINIMUM_RATING = "newTableMinimumRating";
public static final String KEY_NEW_TABLE_RATED = "newTableRated";
public static final String KEY_NEW_TABLE_EDH_POWER_LEVEL = "newTableEdhPowerLevel";
public static final String KEY_NEW_TABLE_EMBLEM_CARDS_ENABLED = "newTableEmblemCardsEnabled";
public static final String KEY_NEW_TABLE_EMBLEM_CARDS_PER_PLAYER_FILE = "newTableEmblemCardsPerPlayerFile";
public static final String KEY_NEW_TABLE_EMBLEM_CARDS_STARTING_PLAYER_FILE = "newTableEmblemCardsStartingPlayerFile";
// pref setting for new tournament dialog
public static final String KEY_NEW_TOURNAMENT_NAME = "newTournamentName";
@ -237,6 +240,9 @@ public class PreferencesDialog extends javax.swing.JDialog {
public static final String KEY_NEW_TOURNAMENT_QUIT_RATIO = "newTournamentQuitRatio";
public static final String KEY_NEW_TOURNAMENT_MINIMUM_RATING = "newTournamentMinimumRating";
public static final String KEY_NEW_TOURNAMENT_RATED = "newTournamentRated";
public static final String KEY_NEW_TOURNAMENT_EMBLEM_CARDS_ENABLED = "newTournamentEmblemCardsEnabled";
public static final String KEY_NEW_TOURNAMENT_EMBLEM_CARDS_PER_PLAYER_FILE = "newTournamentEmblemCardsPerPlayerFile";
public static final String KEY_NEW_TOURNAMENT_EMBLEM_CARDS_STARTING_PLAYER_FILE = "newTournamentEmblemCardsStartingPlayerFile";
// Settings for auto-choosing targets
public static final String KEY_AUTO_TARGET_LEVEL = "autoTargetLevel";

View file

@ -99,7 +99,8 @@ public class TablesPanel extends javax.swing.JPanel {
+ "<br>FM: = Numbers of freee mulligans"
+ "<br>Constr.: = Construction time for limited tournament formats"
+ "<br>RB = Rollback allowed"
+ "<br>PC = Planechase active"
+ "<br>PC = Planechase active"
+ "<br>EC = One or more emblem cards in use"
+ "<br>SP = Spectators allowed"
+ "<br>Rng: Range of visibility for multiplayer matches"
)

View file

@ -716,8 +716,9 @@ public class CardView extends SimpleCardView {
// emblem images are always with common (black) symbol
this.frameStyle = FrameStyle.M15_NORMAL;
this.expansionSetCode = emblem.getExpansionSetCode();
this.cardNumber = "";
this.cardNumber = emblem.getCardNumber();
this.imageNumber = emblem.getImageNumber();
this.usesVariousArt = emblem.getUsesVariousArt();
this.rarity = Rarity.COMMON;
this.playableStats = emblem.playableStats.copy();

View file

@ -1,7 +1,7 @@
package mage.view;
import mage.cards.Card;
import mage.game.command.Emblem;
import mage.game.command.emblems.EmblemOfCard;
import mage.players.PlayableObjectStats;
import java.io.Serializable;
@ -15,7 +15,9 @@ public class EmblemView implements CommandObjectView, Serializable {
protected UUID id;
protected String name;
protected String cardNumber = "";
protected int imageNum;
protected boolean usesVariousArt = false;
protected String expansionSetCode;
protected List<String> rules;
protected PlayableObjectStats playableStats = new PlayableObjectStats();
@ -26,6 +28,10 @@ public class EmblemView implements CommandObjectView, Serializable {
this.imageNum = emblem.getImageNumber();
this.expansionSetCode = emblem.getExpansionSetCode();
this.rules = emblem.getAbilities().getRules(emblem.getName());
if (emblem instanceof EmblemOfCard) {
cardNumber = emblem.getCardNumber();
usesVariousArt = ((EmblemOfCard) emblem).getUsesVariousArt();
}
}
@Override
@ -43,10 +49,17 @@ public class EmblemView implements CommandObjectView, Serializable {
return id;
}
public String getCardNumber() {
return cardNumber;
}
@Override
public int getImageNumber() {
return imageNum;
}
public boolean getUsesVariousArt() {
return this.usesVariousArt;
}
@Override
public List<String> getRules() {

View file

@ -117,6 +117,10 @@ public class TableView implements Serializable {
if (table.getMatch().getOptions().isPlaneChase()) {
addInfo.append(" PC");
}
if (!(table.getMatch().getOptions().getPerPlayerEmblemCards().isEmpty())
|| !(table.getMatch().getOptions().getGlobalEmblemCards().isEmpty())) {
addInfo.append(" EC");
}
if (table.getMatch().getOptions().isSpectatorsAllowed()) {
addInfo.append(" SP");
}
@ -177,6 +181,10 @@ public class TableView implements Serializable {
if (tourneyMatchOptions.isPlaneChase()) {
infoText.append(" PC");
}
if (!(table.getTournament().getOptions().getMatchOptions().getPerPlayerEmblemCards().isEmpty())
|| !(table.getTournament().getOptions().getMatchOptions().getGlobalEmblemCards().isEmpty())) {
infoText.append(" EC");
}
if (table.getTournament().getOptions().isWatchingAllowed()) {
infoText.append(" SP");
}

View file

@ -628,6 +628,8 @@ public class TableController {
gameOptions.rollbackTurnsAllowed = match.getOptions().isRollbackTurnsAllowed();
gameOptions.bannedUsers = match.getOptions().getBannedUsers();
gameOptions.planeChase = match.getOptions().isPlaneChase();
gameOptions.perPlayerEmblemCards = match.getOptions().getPerPlayerEmblemCards();
gameOptions.globalEmblemCards = match.getOptions().getGlobalEmblemCards();
match.getGame().setGameOptions(gameOptions);
managerFactory.gameManager().createGameSession(match.getGame(), userPlayerMap, table.getId(), choosingPlayerId, gameOptions);
String creator = null;

View file

@ -0,0 +1,184 @@
package org.mage.test.cards.emblems;
import mage.cards.repository.CardRepository;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import mage.counters.CounterType;
import mage.game.command.emblems.EmblemOfCard;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
* @author artemiswkearney
*/
public class EmblemOfCardTest extends CardTestPlayerBase {
@Test
public void testEmblemOfGriselbrand() {
// Flying, lifelink
// Pay 7 life: Draw seven cards.
addEmblem(playerA, new EmblemOfCard(
CardRepository.instance.findCard("Griselbrand", true).getMockCard()
));
setLife(playerA, 20);
assertHandCount(playerA, 0);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Pay 7 life: Draw");
setStopAt(1, PhaseStep.END_TURN);
execute();
assertHandCount(playerA, 7);
assertLife(playerA, 13);
assertEmblemCount(playerA, 1);
}
@Test
public void testEmblemOfYurlok() {
// Vigilance
// A player losing unspent mana causes that player to lose that much life.
// {1}, {T}: Each player adds {B}{R}{G}.
addEmblem(playerA, new EmblemOfCard(
CardRepository.instance.findCard("Yurlok of Scorch Thrash", true).getMockCard()
));
setLife(playerA, 20);
// {T}: Add {R}.
addCard(Zone.BATTLEFIELD, playerA, "Mountain");
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {R}");
checkManaPool("after tapping Mountain", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "R", 1);
checkPlayableAbility("can't tap emblem", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "{1}, {T}:", false);
// wait for mana burn
setStopAt(1, PhaseStep.BEGIN_COMBAT);
checkLife("takes 1 point of mana burn", 1, PhaseStep.BEGIN_COMBAT, playerA, 19);
execute();
assertEmblemCount(playerA, 1);
}
@Test
public void testEmblemOfOmniscience() {
// You may cast spells from your hand without paying their mana costs.
addEmblem(playerA, new EmblemOfCard(
CardRepository.instance.findCard("Omniscience", true).getMockCard()
));
// Colossal Dreadmaw {4}{G}{G}
// Creature - Dinosaur 6/6
// Trample
addCard(Zone.HAND, playerA, "Colossal Dreadmaw");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Colossal Dreadmaw");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
setStopAt(1, PhaseStep.PRECOMBAT_MAIN);
execute();
assertPermanentCount(playerA, "Colossal Dreadmaw", 1);
assertEmblemCount(playerA, 1);
}
@Test
public void testEmblemOfParadoxEngine() {
// Whenever you cast a spell, untap all nonland permanents you control.
addEmblem(playerA, new EmblemOfCard(
CardRepository.instance.findCard("Paradox Engine", true).getMockCard()
));
// {T}: Add {G}.
addCard(Zone.BATTLEFIELD, playerA, "Mox Emerald");
// Sol Ring {1}
// Artifact
// {T}: Add {C}{C}.
addCard(Zone.HAND, playerA, "Sol Ring");
// Basalt Monolith {3}
// Artifact
// Basalt Monolith doesnt untap during your untap step.
// {T}: Add {C}{C}{C}.
// {3}: Untap Basalt Monolith.
addCard(Zone.HAND, playerA, "Basalt Monolith");
// Book of Rass {6}
// Artifact
// {2}, Pay 2 life: Draw a card.
// (just a dummy artifact to cast and spend the mana with)
addCard(Zone.HAND, playerA, "Book of Rass");
setLife(playerA, 20);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Sol Ring");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Basalt Monolith");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Book of Rass");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}, Pay");
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}, Pay");
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}, Pay");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertLife(playerA, 14);
assertEmblemCount(playerA, 1);
}
@Test
public void testEmblemOfDoublingSeason() {
// If an effect would create one or more tokens under your control, it
// creates twice that many of those tokens instead.
// If an effect would put one or more counters on a permanent you
// control, it puts twice that many of those counters on that permanent instead.
addEmblem(playerA, new EmblemOfCard(
CardRepository.instance.findCard("Doubling Season", true).getMockCard()
));
// {T}: Add {W}.
addCard(Zone.BATTLEFIELD, playerA, "Plains", 6);
// Elspeth, Sun's Champion {4}{W}{W}
// Legendary Planeswalker Elspeth
// +1: Create three 1/1 white Soldier creature tokens.
// 3: Destroy all creatures with power 4 or greater.
// 7: You get an emblem with Creatures you control get +2/+2 and have flying.
// Loyalty: 4
addCard(Zone.HAND, playerA, "Elspeth, Sun's Champion");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Elspeth, Sun's Champion");
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
checkPermanentCounters(
"Elspeth's loyalty is doubled",
1,
PhaseStep.PRECOMBAT_MAIN,
playerA,
"Elspeth, Sun's Champion",
CounterType.LOYALTY,
8
);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "+1: Create");
checkPlayableAbility("can't still activate Griselbrand", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Pay 7 life:", false);
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
checkPermanentCounters(
"+1 is not doubled",
1,
PhaseStep.PRECOMBAT_MAIN,
playerA,
"Elspeth, Sun's Champion",
CounterType.LOYALTY,
9
);
checkPermanentCount(
"Soldier tokens doubled",
1,
PhaseStep.PRECOMBAT_MAIN,
playerA,
"Soldier Token",
6
);
execute();
assertEmblemCount(playerA, 1);
}
}

View file

@ -1481,6 +1481,13 @@ public abstract class AbilityImpl implements Ability {
}
public AbilityImpl copyWithZone(Zone zone) {
if (this instanceof MageSingleton) {
// not safe to change zone for singletons
// in theory there could be some sort of wrapper to effectively change
// the zone here, but currently no use of copyWithZone actually needs
// to change the zone of any existing singleton abilities
return this;
}
AbilityImpl copy = ((AbilityImpl)this.copy());
copy.zone = zone;
copy.newId();

View file

@ -22,6 +22,7 @@ import mage.abilities.mana.TriggeredManaAbility;
import mage.actions.impl.MageAction;
import mage.cards.*;
import mage.cards.decks.Deck;
import mage.cards.decks.DeckCardInfo;
import mage.choices.Choice;
import mage.constants.*;
import mage.counters.CounterType;
@ -41,6 +42,7 @@ import mage.game.combat.Combat;
import mage.game.combat.CombatGroup;
import mage.game.command.*;
import mage.game.command.dungeons.UndercityDungeon;
import mage.game.command.emblems.EmblemOfCard;
import mage.game.command.emblems.TheRingEmblem;
import mage.game.events.*;
import mage.game.events.TableEvent.EventType;
@ -1317,6 +1319,22 @@ public abstract class GameImpl implements Game {
addPlane(plane, startingPlayerId);
state.setPlaneChase(this, gameOptions.planeChase);
}
if (!gameOptions.perPlayerEmblemCards.isEmpty()) {
for (UUID playerId : state.getPlayerList(startingPlayerId)) {
for (DeckCardInfo info : gameOptions.perPlayerEmblemCards) {
Card card = EmblemOfCard.cardFromDeckInfo(info);
addEmblem(new EmblemOfCard(card), card, playerId);
}
}
}
if (!gameOptions.globalEmblemCards.isEmpty()) {
for (DeckCardInfo info : gameOptions.globalEmblemCards) {
Card card = EmblemOfCard.cardFromDeckInfo(info);
addEmblem(new EmblemOfCard(card), card, startingPlayerId);
}
}
}
public void initGameDefaultWatchers() {

View file

@ -1,10 +1,13 @@
package mage.game;
import mage.cards.decks.DeckCardInfo;
import mage.constants.PhaseStep;
import mage.util.Copyable;
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
/**
@ -52,6 +55,15 @@ public class GameOptions implements Serializable, Copyable<GameOptions> {
*/
public Set<String> bannedUsers = Collections.emptySet();
/**
* Cards to be given to each player as emblems
*/
public Collection<DeckCardInfo> perPlayerEmblemCards = Collections.emptySet();
/**
* Cards to be given to the starting player as emblems
*/
public Collection<DeckCardInfo> globalEmblemCards = Collections.emptySet();
// PLANECHASE game mode
public boolean planeChase = false;
@ -73,6 +85,8 @@ public class GameOptions implements Serializable, Copyable<GameOptions> {
this.rollbackTurnsAllowed = options.rollbackTurnsAllowed;
this.bannedUsers.addAll(options.bannedUsers);
this.planeChase = options.planeChase;
this.perPlayerEmblemCards = new HashSet<>(options.perPlayerEmblemCards);
this.globalEmblemCards = new HashSet<>(options.globalEmblemCards);
}
@Override

View file

@ -33,7 +33,7 @@ public abstract class Emblem extends CommandObjectImpl {
private static final ManaCosts emptyCost = new ManaCostsImpl<>();
private UUID controllerId;
private MageObject sourceObject;
protected MageObject sourceObject;
private boolean copy;
private MageObject copyFrom; // copied card INFO (used to call original adjusters)
private FrameStyle frameStyle;

View file

@ -0,0 +1,108 @@
package mage.game.command.emblems;
import mage.MageObject;
import mage.abilities.AbilityImpl;
import mage.cards.Card;
import mage.cards.decks.DeckCardInfo;
import mage.cards.mock.MockCard;
import mage.cards.repository.CardCriteria;
import mage.cards.repository.CardInfo;
import mage.cards.repository.CardRepository;
import mage.constants.Zone;
import mage.game.command.Emblem;
import mage.util.CardUtil;
import org.apache.log4j.Logger;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author artemiswkearney
* Emblem with all the abilities of an existing card.
* Can be used for custom gamemodes like Omniscience Draft (as seen on Arena),
* mana burn with Yurlok of Scorch Thrash, and anything else players might think of.
*/
public final class EmblemOfCard extends Emblem {
private final boolean usesVariousArt;
private static final Logger logger = Logger.getLogger(EmblemOfCard.class);
public static Card lookupCard(
String cardName,
String cardNumber,
String setCode,
String infoTypeForError
) {
int cardNumberInt = CardUtil.parseCardNumberAsInt(cardNumber);
List<CardInfo> found = CardRepository.instance.findCards(new CardCriteria()
.name(cardName)
.minCardNumber(cardNumberInt)
.maxCardNumber(cardNumberInt)
.setCodes(setCode));
return found.stream()
.filter(ci -> ci.getCardNumber().equals(cardNumber))
.findFirst()
.orElseGet(() -> found.stream()
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("No real card for " + infoTypeForError + " " + cardName)))
.getCard();
}
public static Card cardFromDeckInfo(DeckCardInfo info) {
return lookupCard(
info.getCardName(),
info.getCardNum(),
info.getSetCode(),
"DeckCardInfo"
);
}
public EmblemOfCard(Card card, Zone zone) {
super(card.getName());
if (card instanceof MockCard) {
card = lookupCard(
card.getName(),
card.getCardNumber(),
card.getExpansionSetCode(),
"MockCard"
);
}
this.getAbilities().addAll(card.getAbilities().stream().filter(
ability -> zone.match(ability.getZone())
).map(ability -> {
if (ability instanceof AbilityImpl && ability.getZone() == zone) {
return ((AbilityImpl)ability).copyWithZone(Zone.COMMAND);
}
return ability;
}).collect(Collectors.toList()));
this.getAbilities().setSourceId(this.getId());
this.setExpansionSetCode(card.getExpansionSetCode());
this.setCardNumber(card.getCardNumber());
this.setImageNumber(card.getImageNumber());
this.usesVariousArt = card.getUsesVariousArt();
}
public EmblemOfCard(Card card) {
this(card, Zone.BATTLEFIELD);
}
private EmblemOfCard(EmblemOfCard eoc) {
super(eoc);
this.usesVariousArt = eoc.usesVariousArt;
}
@Override
public EmblemOfCard copy() {
return new EmblemOfCard(this);
}
@Override
public void setSourceObject(MageObject sourceObject) {
this.sourceObject = sourceObject;
// super method would try and fail to find the emblem image here
// (not sure why that would be setSoureObject's job; we get our image during construction)
}
public boolean getUsesVariousArt() {
return usesVariousArt;
}
}

View file

@ -1,6 +1,7 @@
package mage.game.match;
import mage.cards.decks.DeckCardInfo;
import mage.constants.MatchBufferTime;
import mage.constants.MatchTimeLimit;
import mage.constants.MultiplayerAttackOption;
@ -11,10 +12,7 @@ import mage.game.result.ResultProtos;
import mage.players.PlayerType;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.*;
/**
*
@ -52,6 +50,9 @@ public class MatchOptions implements Serializable {
protected MatchBufferTime matchBufferTime; // Amount of time each player gets before their normal time limit counts down. Refreshes each time the normal timer is invoked.
protected MulliganType mulliganType;
protected Collection<DeckCardInfo> perPlayerEmblemCards;
protected Collection<DeckCardInfo> globalEmblemCards;
/*public MatchOptions(String name, String gameType) {
this.name = name;
this.gameType = gameType;
@ -65,6 +66,8 @@ public class MatchOptions implements Serializable {
this.password = "";
this.multiPlayer = multiPlayer;
this.numSeats = numSeats;
this.perPlayerEmblemCards = Collections.emptySet();
this.globalEmblemCards = Collections.emptySet();
}
public void setNumSeats (int numSeats) {
@ -288,4 +291,19 @@ public class MatchOptions implements Serializable {
return mulliganType;
}
public Collection<DeckCardInfo> getPerPlayerEmblemCards() {
return perPlayerEmblemCards;
}
public void setPerPlayerEmblemCards(Collection<DeckCardInfo> perPlayerEmblemCards) {
this.perPlayerEmblemCards = perPlayerEmblemCards;
}
public Collection<DeckCardInfo> getGlobalEmblemCards() {
return globalEmblemCards;
}
public void setGlobalEmblemCards(Collection<DeckCardInfo> globalEmblemCards) {
this.globalEmblemCards = globalEmblemCards;
}
}