Implement missing cards with dividable shields for damage replacement effects

This commit is contained in:
Lyu Zong-Hong
2021-06-05 18:18:34 +09:00
parent f95fb7d08b
commit ab31c8fa9d
25 changed files with 1077 additions and 173 deletions

View File

@@ -760,7 +760,7 @@ public final class CMatchUI
}
}
};
if (FThreads.isGuiThread()) { // run this now whether in EDT or not so that it doesn't clobber later stuff
FThreads.invokeInEdtNowOrLater(focusRoutine);
} else {
@@ -1028,6 +1028,24 @@ public final class CMatchUI
return result.get();
}
@Override
public Map<GameEntityView, Integer> assignGenericAmount(final CardView effectSource, final Map<GameEntityView, Integer> target,
final int amount, final boolean atLeastOne, final String amountLabel) {
if (amount <= 0) {
return Collections.emptyMap();
}
final AtomicReference<Map<GameEntityView, Integer>> result = new AtomicReference<>();
FThreads.invokeInEdtAndWait(new Runnable() {
@Override
public void run() {
final VAssignGenericAmount v = new VAssignGenericAmount(CMatchUI.this, effectSource, target, amount, atLeastOne, amountLabel);
result.set(v.getAssignedMap());
}});
return result.get();
}
@Override
public void openView(final TrackableCollection<PlayerView> myPlayers) {
final GameView gameView = getGameView();
@@ -1273,28 +1291,28 @@ public final class CMatchUI
String stackNotificationPolicy = FModel.getPreferences().getPref(FPref.UI_STACK_EFFECT_NOTIFICATION_POLICY);
boolean isAi = sa.getActivatingPlayer().isAI();
boolean isTrigger = sa.isTrigger();
int stackIndex = event.stackIndex;
if(stackIndex == nextNotifiableStackIndex) {
int stackIndex = event.stackIndex;
if(stackIndex == nextNotifiableStackIndex) {
if(ForgeConstants.STACK_EFFECT_NOTIFICATION_ALWAYS.equals(stackNotificationPolicy) || (ForgeConstants.STACK_EFFECT_NOTIFICATION_AI_AND_TRIGGERED.equals(stackNotificationPolicy) && (isAi || isTrigger))) {
// We can go and show the modal
SpellAbilityStackInstance si = event.si;
MigLayout migLayout = new MigLayout("insets 15, left, gap 30, fill");
JPanel mainPanel = new JPanel(migLayout);
final Dimension parentSize = JOptionPane.getRootFrame().getSize();
Dimension maxSize = new Dimension(1400, parentSize.height - 100);
mainPanel.setMaximumSize(maxSize);
mainPanel.setOpaque(false);
mainPanel.setOpaque(false);
// Big Image
addBigImageToStackModalPanel(mainPanel, si);
// Text
addTextToStackModalPanel(mainPanel,sa,si);
// Small images
int numSmallImages = 0;
// If current effect is a triggered/activated ability of an enchantment card, I want to show the enchanted card
GameEntityView enchantedEntityView = null;
Card hostCard = sa.getHostCard();
@@ -1308,45 +1326,45 @@ public final class CMatchUI
&& !sa.getRootAbility().getPaidList("Sacrificed").isEmpty()) {
// If the player activated its ability by sacrificing the enchantment, the enchantment has not anything attached anymore and the ex-enchanted card has to be searched in other ways.. for example, the green enchantment "Carapace"
enchantedEntity = sa.getRootAbility().getPaidList("Sacrificed").get(0).getEnchantingCard();
if(enchantedEntity != null) {
enchantedEntityView = enchantedEntity.getView();
if(enchantedEntity != null) {
enchantedEntityView = enchantedEntity.getView();
numSmallImages++;
}
}
}
// If current effect is a triggered ability, I want to show the triggering card if present
SpellAbility sourceSA = (SpellAbility) si.getTriggeringObject(AbilityKey.SourceSA);
CardView sourceCardView = null;
if(sourceSA != null) {
sourceCardView = sourceSA.getHostCard().getView();
numSmallImages++;
}
}
// I also want to show each type of targets (both cards and players)
List<GameEntityView> targets = getTargets(si,new ArrayList<GameEntityView>());
numSmallImages = numSmallImages + targets.size();
// Now I know how many small images - on to render them
if(enchantedEntityView != null) {
addSmallImageToStackModalPanel(enchantedEntityView,mainPanel,numSmallImages);
addSmallImageToStackModalPanel(enchantedEntityView,mainPanel,numSmallImages);
}
if(sourceCardView != null) {
addSmallImageToStackModalPanel(sourceCardView,mainPanel,numSmallImages);
addSmallImageToStackModalPanel(sourceCardView,mainPanel,numSmallImages);
}
for(GameEntityView gev : targets) {
addSmallImageToStackModalPanel(gev, mainPanel, numSmallImages);
}
FOptionPane.showOptionDialog(null, "Forge", null, mainPanel, ImmutableList.of(Localizer.getInstance().getMessage("lblOK")));
}
FOptionPane.showOptionDialog(null, "Forge", null, mainPanel, ImmutableList.of(Localizer.getInstance().getMessage("lblOK")));
// here the user closed the modal - time to update the next notifiable stack index
}
// In any case, I have to increase the counter
nextNotifiableStackIndex++;
} else {
// Not yet time to show the modal - schedule the method again, and try again later
Runnable tryAgainThread = new Runnable() {
@Override
@@ -1355,8 +1373,8 @@ public final class CMatchUI
}
};
GuiBase.getInterface().invokeInEdtLater(tryAgainThread);
}
}
}
private List<GameEntityView> getTargets(SpellAbilityStackInstance si, List<GameEntityView> result){
@@ -1380,22 +1398,22 @@ public final class CMatchUI
return getTargets(si.getSubInstance(),result);
}
private void addBigImageToStackModalPanel(JPanel mainPanel, SpellAbilityStackInstance si) {
StackItemView siv = si.getView();
int rotation = getRotation(si.getCardView());
FImagePanel imagePanel = new FImagePanel();
BufferedImage bufferedImage = FImageUtil.getImage(siv.getSourceCard().getCurrentState());
FImagePanel imagePanel = new FImagePanel();
BufferedImage bufferedImage = FImageUtil.getImage(siv.getSourceCard().getCurrentState());
imagePanel.setImage(bufferedImage, rotation, AutoSizeImageMode.SOURCE);
int imageWidth = 433;
int imageHeight = 600;
Dimension imagePanelDimension = new Dimension(imageWidth,imageHeight);
imagePanel.setMinimumSize(imagePanelDimension);
mainPanel.add(imagePanel, "cell 0 0, spany 3");
mainPanel.add(imagePanel, "cell 0 0, spany 3");
}
private void addTextToStackModalPanel(JPanel mainPanel, SpellAbility sa, SpellAbilityStackInstance si) {
String who = sa.getActivatingPlayer().getName();
String action = sa.isSpell() ? " cast " : sa.isTrigger() ? " triggered " : " activated ";
@@ -1409,45 +1427,45 @@ public final class CMatchUI
TargetChoices targets = si.getTargetChoices();
sb.append(targets);
}
sb.append(".");
sb.append(".");
String message1 = sb.toString();
String message2 = si.getStackDescription();
String message2 = si.getStackDescription();
String messageTotal = message1 + "\n\n" + message2;
final FTextArea prompt1 = new FTextArea(messageTotal);
prompt1.setFont(FSkin.getFont(21));
prompt1.setAutoSize(true);
prompt1.setMinimumSize(new Dimension(475,200));
mainPanel.add(prompt1, "cell 1 0, aligny top");
mainPanel.add(prompt1, "cell 1 0, aligny top");
}
private void addSmallImageToStackModalPanel(GameEntityView gameEntityView, JPanel mainPanel, int numTarget) {
if(gameEntityView instanceof CardView) {
CardView cardView = (CardView) gameEntityView;
int currRotation = getRotation(cardView);
int currRotation = getRotation(cardView);
FImagePanel targetPanel = new FImagePanel();
BufferedImage bufferedImage = FImageUtil.getImage(cardView.getCurrentState());
BufferedImage bufferedImage = FImageUtil.getImage(cardView.getCurrentState());
targetPanel.setImage(bufferedImage, currRotation, AutoSizeImageMode.SOURCE);
int imageWidth = 217;
int imageHeight = 300;
Dimension targetPanelDimension = new Dimension(imageWidth,imageHeight);
targetPanel.setMinimumSize(targetPanelDimension);
mainPanel.add(targetPanel, "cell 1 1, split " + numTarget+ ", aligny bottom");
mainPanel.add(targetPanel, "cell 1 1, split " + numTarget+ ", aligny bottom");
} else if(gameEntityView instanceof PlayerView) {
PlayerView playerView = (PlayerView) gameEntityView;
SkinImage playerAvatar = getPlayerAvatar(playerView, 0);
final FLabel lblIcon = new FLabel.Builder().icon(playerAvatar).build();
Dimension dimension = playerAvatar.getSizeForPaint(JOptionPane.getRootFrame().getGraphics());
mainPanel.add(lblIcon, "cell 1 1, split " + numTarget+ ", w " + dimension.getWidth() + ", h " + dimension.getHeight() + ", aligny bottom");
mainPanel.add(lblIcon, "cell 1 1, split " + numTarget+ ", w " + dimension.getWidth() + ", h " + dimension.getHeight() + ", aligny bottom");
}
}
}
private int getRotation(CardView cardView) {
final int rotation;
if (cardView.isSplitCard()) {
String cardName = cardView.getName();
if (cardName.isEmpty()) { cardName = cardView.getAlternateState().getName(); }
PaperCard pc = StaticData.instance().getCommonCards().getCard(cardName);
boolean hasKeywordAftermath = pc != null && Card.getCardForUi(pc).hasKeyword(Keyword.AFTERMATH);
@@ -1459,7 +1477,7 @@ public final class CMatchUI
return rotation;
}
@Override
public void notifyStackRemoval(GameEventSpellRemovedFromStack event) {
// I always decrease the counter
@@ -1474,49 +1492,49 @@ public final class CMatchUI
createLandPopupPanel(land);
}
};
GuiBase.getInterface().invokeInEdtAndWait(createPopupThread);
GuiBase.getInterface().invokeInEdtAndWait(createPopupThread);
}
private void createLandPopupPanel(Card land) {
String landPlayedNotificationPolicy = FModel.getPreferences().getPref(FPref.UI_LAND_PLAYED_NOTIFICATION_POLICY);
Player cardController = land.getController();
boolean isAi = cardController.isAI();
if(ForgeConstants.LAND_PLAYED_NOTIFICATION_ALWAYS.equals(landPlayedNotificationPolicy)
Player cardController = land.getController();
boolean isAi = cardController.isAI();
if(ForgeConstants.LAND_PLAYED_NOTIFICATION_ALWAYS.equals(landPlayedNotificationPolicy)
|| (ForgeConstants.LAND_PLAYED_NOTIFICATION_AI.equals(landPlayedNotificationPolicy) && (isAi))
|| (ForgeConstants.LAND_PLAYED_NOTIFICATION_ALWAYS_FOR_NONBASIC_LANDS.equals(landPlayedNotificationPolicy) && !land.isBasicLand())
|| (ForgeConstants.LAND_PLAYED_NOTIFICATION_AI_FOR_NONBASIC_LANDS.equals(landPlayedNotificationPolicy) && !land.isBasicLand()) && (isAi)) {
String title = "Forge";
String title = "Forge";
List<String> options = ImmutableList.of(Localizer.getInstance().getMessage("lblOK"));
MigLayout migLayout = new MigLayout("insets 15, left, gap 30, fill");
JPanel mainPanel = new JPanel(migLayout);
final Dimension parentSize = JOptionPane.getRootFrame().getSize();
Dimension maxSize = new Dimension(1400, parentSize.height - 100);
mainPanel.setMaximumSize(maxSize);
mainPanel.setOpaque(false);
mainPanel.setOpaque(false);
int rotation = getRotation(land.getView());
FImagePanel imagePanel = new FImagePanel();
BufferedImage bufferedImage = FImageUtil.getImage(land.getCurrentState().getView());
FImagePanel imagePanel = new FImagePanel();
BufferedImage bufferedImage = FImageUtil.getImage(land.getCurrentState().getView());
imagePanel.setImage(bufferedImage, rotation, AutoSizeImageMode.SOURCE);
int imageWidth = 433;
int imageHeight = 600;
Dimension imagePanelDimension = new Dimension(imageWidth,imageHeight);
imagePanel.setMinimumSize(imagePanelDimension);
mainPanel.add(imagePanel, "cell 0 0, spany 3");
String msg = cardController.toString() + " puts " + land.toString() + " into play into " + ZoneType.Battlefield.toString() + ".";
String msg = cardController.toString() + " puts " + land.toString() + " into play into " + ZoneType.Battlefield.toString() + ".";
final FTextArea prompt1 = new FTextArea(msg);
prompt1.setFont(FSkin.getFont(21));
prompt1.setAutoSize(true);
prompt1.setMinimumSize(new Dimension(475,200));
mainPanel.add(prompt1, "cell 1 0, aligny top");
FOptionPane.showOptionDialog(null, title, null, mainPanel, options);
}
mainPanel.add(prompt1, "cell 1 0, aligny top");
FOptionPane.showOptionDialog(null, title, null, mainPanel, options);
}
}
}

View File

@@ -0,0 +1,314 @@
/*
* Forge: Play Magic: the Gathering.
* Copyright (C) 2021 Forge Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package forge.screens.match;
import java.awt.Dialog.ModalityType;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.border.Border;
import forge.game.GameEntityView;
import forge.game.card.CardView;
import forge.game.player.PlayerView;
import forge.gui.SOverlayUtils;
import forge.toolbox.FButton;
import forge.toolbox.FLabel;
import forge.toolbox.FScrollPane;
import forge.toolbox.FSkin;
import forge.toolbox.FSkin.SkinnedPanel;
import forge.util.Localizer;
import forge.util.TextUtil;
import forge.view.FDialog;
import forge.view.arcane.CardPanel;
import net.miginfocom.swing.MigLayout;
/**
* Assembles Swing components of assign damage dialog.
*
* This needs a JDialog to maintain a modal state.
* Without the modal state, the PhaseHandler automatically
* moves forward to phase Main2 without assigning damage.
*
* <br><br><i>(V at beginning of class name denotes a view class.)</i>
*/
public class VAssignGenericAmount {
final Localizer localizer = Localizer.getInstance();
private final CMatchUI matchUI;
// Width and height of assign dialog
private final int wDlg = 700;
private final int hDlg = 500;
private final FDialog dlg = new FDialog();
// Amount storage
private final int totalAmountToAssign;
private final String lblAmount;
private final JLabel lblTotalAmount;
// Label Buttons
private final FButton btnOK = new FButton(localizer.getMessage("lblOk"));
private final FButton btnReset = new FButton(localizer.getMessage("lblReset"));
private static class AssignTarget {
public final GameEntityView entity;
public final JLabel label;
public final int max;
public int amount;
public AssignTarget(final GameEntityView e, final JLabel lbl, int max0) {
entity = e;
label = lbl;
max = max0;
amount = 0;
}
}
private final List<AssignTarget> targetsList = new ArrayList<>();
private final Map<GameEntityView, AssignTarget> targetsMap = new HashMap<>();
// Mouse actions
private final MouseAdapter mad = new MouseAdapter() {
@Override
public void mouseEntered(final MouseEvent evt) {
((CardPanel) evt.getSource()).setBorder(new FSkin.LineSkinBorder(FSkin.getColor(FSkin.Colors.CLR_ACTIVE), 2));
}
@Override
public void mouseExited(final MouseEvent evt) {
((CardPanel) evt.getSource()).setBorder((Border)null);
}
@Override
public void mousePressed(final MouseEvent evt) {
CardView source = ((CardPanel) evt.getSource()).getCard(); // will be NULL for player
boolean meta = evt.isControlDown();
boolean isLMB = SwingUtilities.isLeftMouseButton(evt);
boolean isRMB = SwingUtilities.isRightMouseButton(evt);
if ( isLMB || isRMB)
assignAmountTo(source, meta, isLMB);
}
};
public VAssignGenericAmount(final CMatchUI matchUI, final CardView effectSource, final Map<GameEntityView, Integer> targets, final int amount, final boolean atLeastOne, final String amountLabel) {
this.matchUI = matchUI;
dlg.setTitle(localizer.getMessage("lbLAssignAmountForEffect", amountLabel, effectSource.toString()));
totalAmountToAssign = amount;
lblAmount = amountLabel;
lblTotalAmount = new FLabel.Builder().text(localizer.getMessage("lblTotalAmountText", lblAmount)).build();
// Top-level UI stuff
final JPanel overlay = SOverlayUtils.genericOverlay();
final SkinnedPanel pnlMain = new SkinnedPanel();
pnlMain.setBackground(FSkin.getColor(FSkin.Colors.CLR_THEME2));
// Effect Source area
final CardPanel pnlSource = new CardPanel(matchUI, effectSource);
pnlSource.setOpaque(false);
pnlSource.setCardBounds(0, 0, 105, 150);
final JPanel pnlInfo = new JPanel(new MigLayout("insets 0, gap 0, wrap"));
pnlInfo.setOpaque(false);
pnlInfo.add(lblTotalAmount, "gap 0 0 20px 5px");
pnlInfo.add(new FLabel.Builder().text(localizer.getMessage("lblLClickAmountMessage", lblAmount)).build(), "gap 0 0 0 5px");
pnlInfo.add(new FLabel.Builder().text(localizer.getMessage("lblRClickAmountMessage", lblAmount)).build(), "gap 0 0 0 5px");
// Targets area
final JPanel pnlTargets = new JPanel();
pnlTargets.setOpaque(false);
int cols = targets.size();
final String wrap = "wrap " + cols;
pnlTargets.setLayout(new MigLayout("insets 0, gap 0, ax center, " + wrap));
final FScrollPane scrTargets = new FScrollPane(pnlTargets, false);
// Top row of cards...
for (final Map.Entry<GameEntityView, Integer> e : targets.entrySet()) {
int maxAmount = e.getValue() != null ? e.getValue() : amount;
final AssignTarget at = new AssignTarget(e.getKey(), new FLabel.Builder().text("0").fontSize(18).fontAlign(SwingConstants.CENTER).build(), maxAmount);
addPanelForTarget(pnlTargets, at);
}
// ... bottom row of labels.
for (final AssignTarget l : targetsList) {
pnlTargets.add(l.label, "w 145px!, h 30px!, gap 5px 5px 0 5px");
}
btnOK.addActionListener(new ActionListener() {
@Override public void actionPerformed(ActionEvent arg0) { finish(); } });
btnReset.addActionListener(new ActionListener() {
@Override public void actionPerformed(ActionEvent arg0) { resetAssignedAmount(); initialAssignAmount(atLeastOne); } });
// Final UI layout
pnlMain.setLayout(new MigLayout("insets 0, gap 0, wrap 2, ax center"));
pnlMain.add(pnlSource, "w 125px!, h 160px!, gap 50px 0 0 15px");
pnlMain.add(pnlInfo, "gap 20px 0 0 15px");
pnlMain.add(scrTargets, "w 96%!, gap 2% 0 0 0, pushy, growy, ax center, span 2");
final JPanel pnlButtons = new JPanel(new MigLayout("insets 0, gap 0, ax center"));
pnlButtons.setOpaque(false);
pnlButtons.add(btnOK, "w 110px!, h 30px!, gap 0 10px 0 0");
pnlButtons.add(btnReset, "w 110px!, h 30px!");
pnlMain.add(pnlButtons, "ax center, w 350px!, gap 10px 10px 10px 10px, span 2");
overlay.add(pnlMain);
pnlMain.getRootPane().setDefaultButton(btnOK);
initialAssignAmount(atLeastOne);
SOverlayUtils.showOverlay();
dlg.setUndecorated(true);
dlg.setContentPane(pnlMain);
dlg.setSize(new Dimension(wDlg, hDlg));
dlg.setLocation((overlay.getWidth() - wDlg) / 2, (overlay.getHeight() - hDlg) / 2);
dlg.setModalityType(ModalityType.APPLICATION_MODAL);
dlg.setVisible(true);
}
private void addPanelForTarget(final JPanel pnlTargets, final AssignTarget at) {
CardView cv = null;
if (at.entity instanceof CardView) {
cv = (CardView)at.entity;
} else if (at.entity instanceof PlayerView) {
final PlayerView p = (PlayerView)at.entity;
cv = new CardView(-1, null, at.entity.toString(), p, matchUI.getAvatarImage(p.getLobbyPlayerName()));
} else {
return;
}
final CardPanel cp = new CardPanel(matchUI, cv);
cp.setCardBounds(0, 0, 105, 150);
cp.setOpaque(true);
pnlTargets.add(cp, "w 145px!, h 170px!, gap 5px 5px 3px 3px, ax center");
cp.addMouseListener(mad);
targetsMap.put(cv, at);
targetsList.add(at);
}
private void assignAmountTo(CardView source, final boolean meta, final boolean isAdding) {
AssignTarget at = targetsMap.get(source);
int assigned = at.amount;
int leftToAssign = Math.max(0, at.max - assigned);
int amountToAdd = isAdding ? 1 : -1;
int remainingAmount = Math.min(getRemainingAmount(), leftToAssign);
// Left click adds, right click substracts.
// Hold Ctrl to assign to maximum amount
if (meta) {
if (isAdding) {
amountToAdd = leftToAssign > 0 ? leftToAssign : 0;
} else {
amountToAdd = -assigned;
}
}
if (amountToAdd > remainingAmount) {
amountToAdd = remainingAmount;
}
if (0 == amountToAdd || amountToAdd + assigned < 0) {
return;
}
addAssignedAmount(at, amountToAdd);
updateLabels();
}
private void initialAssignAmount(boolean atLeastOne) {
if (!atLeastOne) {
updateLabels();
return;
}
for(AssignTarget at : targetsList) {
addAssignedAmount(at, 1);
}
updateLabels();
}
private void resetAssignedAmount() {
for(AssignTarget at : targetsList)
at.amount = 0;
}
private void addAssignedAmount(final AssignTarget at, int addedAmount) {
// If we don't have enough left or we're trying to unassign too much return
final int canAssign = getRemainingAmount();
if (canAssign < addedAmount) {
addedAmount = canAssign;
}
at.amount = Math.max(0, addedAmount + at.amount);
}
private int getRemainingAmount() {
int spent = 0;
for(AssignTarget at : targetsList) {
spent += at.amount;
}
return totalAmountToAssign - spent;
}
/** Updates labels and other UI elements.*/
private void updateLabels() {
int amountLeft = totalAmountToAssign;
for ( AssignTarget at : targetsList )
{
amountLeft -= at.amount;
StringBuilder sb = new StringBuilder();
sb.append(at.amount);
if (at.max - at.amount == 0) {
sb.append(" (").append(localizer.getMessage("lblMax")).append(")");
}
at.label.setText(sb.toString());
}
lblTotalAmount.setText(TextUtil.concatNoSpace(localizer.getMessage("lblAvailableAmount", lblAmount), ": " , String.valueOf(amountLeft), " (of ", String.valueOf(totalAmountToAssign), ")"));
btnOK.setEnabled(amountLeft == 0);
}
private void finish() {
if ( getRemainingAmount() > 0 )
return;
dlg.dispose();
SOverlayUtils.hideOverlay();
}
public Map<GameEntityView, Integer> getAssignedMap() {
Map<GameEntityView, Integer> result = new HashMap<>(targetsList.size());
for (AssignTarget at : targetsList)
result.put(at.entity, at.amount);
return result;
}
}

View File

@@ -146,6 +146,12 @@ public class PlayerControllerForTests extends PlayerController {
throw new IllegalStateException("Erring on the side of caution here...");
}
@Override
public Map<GameEntity, Integer> divideShield(Card effectSource, Map<GameEntity, Integer> affected, int shieldAmount) {
throw new IllegalStateException("Erring on the side of caution here...");
}
@Override
public Integer announceRequirements(SpellAbility ability, String announce) {
throw new IllegalStateException("Erring on the side of caution here...");