From c8a9a1a9db22836222f67aaee6a6764963661715 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Fri, 1 Mar 2024 02:08:25 +0400 Subject: [PATCH] game: improved game logs for faced-down spells and exiled cards - now it support popup hint to view card/permanent (part of #11884, related to #11881, #8781) --- .../mage/cards/a/AshiokWickedManipulator.java | 2 +- .../effects/common/ExileTargetEffect.java | 3 +- .../main/java/mage/constants/EmptyNames.java | 9 ++++- Mage/src/main/java/mage/game/stack/Spell.java | 17 ++++++---- Mage/src/main/java/mage/players/Player.java | 2 +- .../main/java/mage/players/PlayerImpl.java | 34 +++++++++++++++---- Mage/src/main/java/mage/util/CardUtil.java | 4 ++- Mage/src/main/java/mage/util/GameLog.java | 32 ++++++++++++----- 8 files changed, 77 insertions(+), 26 deletions(-) diff --git a/Mage.Sets/src/mage/cards/a/AshiokWickedManipulator.java b/Mage.Sets/src/mage/cards/a/AshiokWickedManipulator.java index b8af22339c3..34476d6c6df 100644 --- a/Mage.Sets/src/mage/cards/a/AshiokWickedManipulator.java +++ b/Mage.Sets/src/mage/cards/a/AshiokWickedManipulator.java @@ -97,7 +97,7 @@ class AshiokWickedManipulatorReplacementEffect extends ReplacementEffectImpl { } Set cards = player.getLibrary().getTopCards(game, event.getAmount()); - player.moveCardsToExile(cards, source, game, false, null, ""); + player.moveCardsToExile(cards, source, game, true, null, ""); return true; } diff --git a/Mage/src/main/java/mage/abilities/effects/common/ExileTargetEffect.java b/Mage/src/main/java/mage/abilities/effects/common/ExileTargetEffect.java index ef0ddbbe66c..8c404a46482 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/ExileTargetEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/ExileTargetEffect.java @@ -30,7 +30,7 @@ public class ExileTargetEffect extends OneShotEffect { private String exileZone = null; private UUID exileId = null; private boolean toSourceExileZone = false; // exile the targets to a source object specific exile zone (takes care of zone change counter) - private boolean withName = true; + private boolean withName = true; // for face down - allows to hide card name in game logs before real face down apply public ExileTargetEffect(String effectText) { this(); @@ -75,6 +75,7 @@ public class ExileTargetEffect extends OneShotEffect { return this; } + // TODO: replace to withHiddenName and instructions to use public void setWithName(boolean withName) { this.withName = withName; } diff --git a/Mage/src/main/java/mage/constants/EmptyNames.java b/Mage/src/main/java/mage/constants/EmptyNames.java index 5617f739dba..96696c533c5 100644 --- a/Mage/src/main/java/mage/constants/EmptyNames.java +++ b/Mage/src/main/java/mage/constants/EmptyNames.java @@ -9,7 +9,8 @@ public enum EmptyNames { // TODO: make names for that cards and enable Assert.assertNotEquals("", permanentName); for assertXXX tests // TODO: replace all getName().equals to haveSameNames and haveEmptyName FACE_DOWN_CREATURE(""), // "Face down creature" - FACE_DOWN_TOKEN(""); // "Face down token" + FACE_DOWN_TOKEN(""), // "Face down token" + FACE_DOWN_CARD(""); // "Face down card" public static final String EMPTY_NAME_IN_LOGS = "face down object"; @@ -23,4 +24,10 @@ public enum EmptyNames { public String toString() { return cardName; } + + public static boolean isEmptyName(String objectName) { + return objectName.equals(FACE_DOWN_CREATURE.toString()) + || objectName.equals(FACE_DOWN_TOKEN.toString()) + || objectName.equals(FACE_DOWN_CARD.toString()); + } } diff --git a/Mage/src/main/java/mage/game/stack/Spell.java b/Mage/src/main/java/mage/game/stack/Spell.java index b3fcca29d38..9b6959ebbe5 100644 --- a/Mage/src/main/java/mage/game/stack/Spell.java +++ b/Mage/src/main/java/mage/game/stack/Spell.java @@ -6,7 +6,6 @@ import mage.abilities.costs.mana.ActivationManaAbilityStep; import mage.abilities.costs.mana.ManaCost; import mage.abilities.costs.mana.ManaCosts; import mage.abilities.keyword.BestowAbility; -import mage.abilities.keyword.MorphAbility; import mage.abilities.keyword.PrototypeAbility; import mage.abilities.keyword.TransformAbility; import mage.cards.*; @@ -83,8 +82,8 @@ public class Spell extends StackObjectImpl implements Card { // simulate another side as new card (another code part in continues effect from disturb ability) affectedCard = TransformAbility.transformCardSpellStatic(card, card.getSecondCardFace(), game); } - if (ability instanceof PrototypeAbility){ - affectedCard = ((PrototypeAbility)ability).prototypeCardSpell(card); + if (ability instanceof PrototypeAbility) { + affectedCard = ((PrototypeAbility) ability).prototypeCardSpell(card); this.prototyped = true; } @@ -100,7 +99,7 @@ public class Spell extends StackObjectImpl implements Card { this.ability = ability; this.ability.setControllerId(controllerId); - if (ability.getSpellAbilityCastMode().isFaceDown()){ + if (ability.getSpellAbilityCastMode().isFaceDown()) { // TODO: need research: // - why it use game param for color and subtype (possible bug?) // - is it possible to use BecomesFaceDownCreatureEffect.makeFaceDownObject or like that? @@ -202,8 +201,10 @@ public class Spell extends StackObjectImpl implements Card { } public String getSpellCastText(Game game) { - if (this.getSpellAbility() instanceof MorphAbility) { - return "a card face down"; + if (this.getSpellAbility().getSpellAbilityCastMode().isFaceDown()) { + // add face down name with object link, so user can look at it from logs + return "a " + GameLog.getColoredObjectIdName(this.getSpellAbility().getCharacteristics(game)) + + " using " + this.getSpellAbility().getSpellAbilityCastMode(); } if (card instanceof AdventureCardSpell) { @@ -677,7 +678,9 @@ public class Spell extends StackObjectImpl implements Card { } @Override - public void setManaCost(ManaCosts costs) { this.manaCost = costs.copy(); } + public void setManaCost(ManaCosts costs) { + this.manaCost = costs.copy(); + } /** * 202.3b When calculating the converted mana cost of an object with an {X} diff --git a/Mage/src/main/java/mage/players/Player.java b/Mage/src/main/java/mage/players/Player.java index 51ef25d4480..5fedee1b40b 100644 --- a/Mage/src/main/java/mage/players/Player.java +++ b/Mage/src/main/java/mage/players/Player.java @@ -988,7 +988,7 @@ public interface Player extends MageItem, Copyable { * @param source * @param game * @param fromZone - * @param withName + * @param withName for face down: used to hide card name in game logs before real face down status apply * @return */ @Deprecated diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index ef49c21917f..b1698c4f49f 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -1849,7 +1849,7 @@ public abstract class PlayerImpl implements Player, Serializable { int last = cards.size(); for (Card card : cards.getCards(game)) { current++; - sb.append(GameLog.getColoredObjectName(card)); + sb.append(GameLog.getColoredObjectName(card)); // TODO: see same usage in OfferingAbility for hide card's id (is it needs for reveal too?!) if (current < last) { sb.append(", "); } @@ -4699,7 +4699,7 @@ public abstract class PlayerImpl implements Player, Serializable { Player eventPlayer = game.getPlayer(info.event.getPlayerId()); if (eventPlayer != null && fromZone != null) { game.informPlayers(eventPlayer.getLogName() + " puts " - + (info.faceDown ? "a card face down " : permanent.getLogName()) + " from " + + GameLog.getColoredObjectIdName(permanent) + " from " + fromZone.toString().toLowerCase(Locale.ENGLISH) + " onto the Battlefield" + CardUtil.getSourceLogName(game, source, permanent.getId())); } @@ -4725,10 +4725,23 @@ public abstract class PlayerImpl implements Player, Serializable { break; case EXILED: for (Card card : cards) { + // 708.9. + // If a face-down permanent or a face-down component of a merged permanent moves from the + // battlefield to any other zone, its owner must reveal it to all players as they move it. + // If a face-down spell moves from the stack to any zone other than the battlefield, + // its owner must reveal it to all players as they move it. If a player leaves the game, + // all face-down permanents, face-down components of merged permanents, and face-down spells + // owned by that player must be revealed to all players. At the end of each game, all + // face-down permanents, face-down components of merged permanents, and face-down spells must + // be revealed to all players. + + // force to show face down name in logs + // TODO: replace it to real reveal code somehow? fromZone = game.getState().getZone(card.getId()); boolean withName = (fromZone == Zone.BATTLEFIELD || fromZone == Zone.STACK) || !card.isFaceDown(game); + if (moveCardToExileWithInfo(card, null, "", source, game, fromZone, withName)) { successfulMovedCards.add(card); } @@ -5006,10 +5019,19 @@ public abstract class PlayerImpl implements Player, Serializable { } } if (Zone.EXILED.equals(game.getState().getZone(card.getId()))) { // only if target zone was not replaced - game.informPlayers(this.getLogName() + " moves " + (withName ? card.getLogName() - + (card.isCopy() ? " (Copy)" : "") : "a card face down") + ' ' - + (fromZone != null ? "from " + fromZone.toString().toLowerCase(Locale.ENGLISH) - + ' ' : "") + "to the exile zone" + CardUtil.getSourceLogName(game, source, card.getId())); + String visibleName; + if (withName) { + // warning, withName param used to forced name show of the face down card (see 708.9.) + if (card.getName().isEmpty()) { + throw new IllegalStateException("Wrong code usage: method must find real card name, but found nothing", new Throwable()); + } + visibleName = card.getLogName() + (card.isCopy() ? " (Copy)" : ""); + } else { + visibleName = "a " + GameLog.getNeutralObjectIdName(EmptyNames.FACE_DOWN_CARD.toString(), card.getId()); + } + game.informPlayers(this.getLogName() + " moves " + visibleName + + (fromZone != null ? " from " + fromZone.toString().toLowerCase(Locale.ENGLISH) : "") + + " to the exile zone" + CardUtil.getSourceLogName(game, source, card.getId())); } } diff --git a/Mage/src/main/java/mage/util/CardUtil.java b/Mage/src/main/java/mage/util/CardUtil.java index 216dceadbfb..1b2aa5ddc78 100644 --- a/Mage/src/main/java/mage/util/CardUtil.java +++ b/Mage/src/main/java/mage/util/CardUtil.java @@ -757,7 +757,9 @@ public final class CardUtil { } public static boolean haveEmptyName(String name) { - return name == null || name.isEmpty() || name.equals(EmptyNames.FACE_DOWN_CREATURE.toString()) || name.equals(EmptyNames.FACE_DOWN_TOKEN.toString()); + return name == null + || name.isEmpty() + || EmptyNames.isEmptyName(name); } public static boolean haveEmptyName(MageObject object) { diff --git a/Mage/src/main/java/mage/util/GameLog.java b/Mage/src/main/java/mage/util/GameLog.java index 87a5b99e3c4..4e12e9fe55d 100644 --- a/Mage/src/main/java/mage/util/GameLog.java +++ b/Mage/src/main/java/mage/util/GameLog.java @@ -33,12 +33,15 @@ public final class GameLog { static final String LOG_TT_COLOR_COLORLESS = "#94A4BA"; static final String LOG_COLOR_NEUTRAL = "#F0F8FF"; // AliceBlue - private static String getNameForLogs(MageObject mageObject) { - if (mageObject.getName().equals(EmptyNames.FACE_DOWN_CREATURE.toString()) - || mageObject.getName().equals(EmptyNames.FACE_DOWN_TOKEN.toString())) { + private static String getNameForLogs(MageObject object) { + return getNameForLogs(object.getName()); + } + + private static String getNameForLogs(String objectName) { + if (EmptyNames.isEmptyName(objectName)) { return EmptyNames.EMPTY_NAME_IN_LOGS; } else { - return mageObject.getName(); + return objectName; } } @@ -74,14 +77,27 @@ public final class GameLog { ); } + /** + * Create object "link" in game logs + */ + public static String getNeutralObjectIdName(String objectName, UUID objectId) { + return getColoredObjectIdName( + new ObjectColor(), + objectId, + getNameForLogs(objectName), + String.format("[%s]", objectId.toString().substring(0, 3)), + null + ); + } + /** * Prepare html text with additional object info (can be used for card popup in GUI) * - * @param color text color of the colored part - * @param objectID object id - * @param visibleColorPart colored part, popup will be work on it + * @param color text color of the colored part + * @param objectID object id + * @param visibleColorPart colored part, popup will be work on it * @param visibleNormalPart additional part with default color - * @param alternativeName alternative name, popup will use it on unknown object id or name + * @param alternativeName alternative name, popup will use it on unknown object id or name * @return */ public static String getColoredObjectIdName(ObjectColor color,