From 493a8f351b9d09c1b2dcd52155a31e5e29e22b2a Mon Sep 17 00:00:00 2001 From: Jetz72 Date: Sun, 9 Feb 2025 23:43:48 -0500 Subject: [PATCH] Add Speed Tracker to Command Zone (#6982) * Add command zone effect displaying speed * Remove enum counter type for speed. * Make Start Your Engines an SBA. * LifeLost -> LifeLostAll per speed rules. * Use same game event for all speed changes. * Fix keyword not appearing in detail text * Cleanup extra createSpeedEffect. * Add support for arbitrary overlay text. Remove fake counters. * Text styling. * Remove extra SBA check. * Remove speed from PlayerView; localization support. --------- Co-authored-by: Jetz --- .../src/main/java/forge/game/card/Card.java | 13 ++++ .../main/java/forge/game/card/CardView.java | 38 ++++++++++ .../java/forge/game/card/CounterEnumType.java | 2 - .../main/java/forge/game/player/Player.java | 75 ++++++++++++++----- .../java/forge/game/player/PlayerView.java | 11 --- .../forge/trackable/TrackableProperty.java | 3 +- .../java/forge/view/arcane/CardPanel.java | 7 +- .../src/forge/card/CardRenderer.java | 25 ++----- forge-gui/res/languages/en-US.properties | 4 +- .../java/forge/gui/card/CardDetailUtil.java | 10 ++- 10 files changed, 128 insertions(+), 60 deletions(-) diff --git a/forge-game/src/main/java/forge/game/card/Card.java b/forge-game/src/main/java/forge/game/card/Card.java index fa9527cced0..ab40f0e8c09 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -340,6 +340,8 @@ public class Card extends GameEntity implements Comparable, IHasSVars, ITr protected boolean renderForUi = true; private final CardView view; + private String overlayText = null; + private SpellAbility[] basicLandAbilities = new SpellAbility[MagicColor.WUBRG.length]; private int planeswalkerAbilityActivated; @@ -6826,6 +6828,17 @@ public class Card extends GameEntity implements Comparable, IHasSVars, ITr return getType().hasStringType("Class"); } + /** + * Displays a string as an overlay on top of this card (similar to the way counter text is shown). + */ + public void setOverlayText(String overlayText) { + this.overlayText = overlayText; + view.updateMarkerText(this); + } + public String getOverlayText() { + return this.overlayText; + } + public final void animateBestow() { animateBestow(true); } diff --git a/forge-game/src/main/java/forge/game/card/CardView.java b/forge-game/src/main/java/forge/game/card/CardView.java index dbaafb0606f..3cad75778d6 100644 --- a/forge-game/src/main/java/forge/game/card/CardView.java +++ b/forge-game/src/main/java/forge/game/card/CardView.java @@ -491,6 +491,7 @@ public class CardView extends GameEntityView { } void updateCurrentRoom(Card c) { set(TrackableProperty.CurrentRoom, c.getCurrentRoom()); + updateMarkerText(c); } public int getIntensity() { @@ -514,6 +515,7 @@ public class CardView extends GameEntityView { } void updateClassLevel(Card c) { set(TrackableProperty.ClassLevel, c.getClassLevel()); + updateMarkerText(c); } public int getRingLevel() { @@ -525,6 +527,41 @@ public class CardView extends GameEntityView { set(TrackableProperty.RingLevel, p.getNumRingTemptedYou()); } + public String getOverlayText() { + return get(TrackableProperty.OverlayText); + } + public List getMarkerText() { + return get(TrackableProperty.MarkerText); + } + void updateMarkerText(Card c) { + List markerItems = new ArrayList<>(); + if(c.getCurrentRoom() != null && !c.getCurrentRoom().isEmpty()) { + markerItems.add("In Room:"); + markerItems.add(c.getCurrentRoom()); + } + if(c.isClassCard() && c.isInZone(ZoneType.Battlefield)) { + markerItems.add("CL:" + c.getClassLevel()); + } + if(getRingLevel() > 0) { + markerItems.add("RL:" + getRingLevel()); + } + + if(StringUtils.isNotEmpty(c.getOverlayText())) { + set(TrackableProperty.OverlayText, c.getOverlayText()); + markerItems.add(c.getOverlayText()); + } + else { + //Overlay text is any custom string. It gets mixed in with the other marker lines, but it also needs its + //own property so that it can display in the card detail text. + set(TrackableProperty.OverlayText, null); + } + + if(markerItems.isEmpty()) + set(TrackableProperty.MarkerText, null); + else + set(TrackableProperty.MarkerText, markerItems); + } + private String getRemembered() { return get(TrackableProperty.Remembered); } @@ -974,6 +1011,7 @@ public class CardView extends GameEntityView { updateDamage(c); updateSpecialize(c); updateRingLevel(c); + updateMarkerText(c); if (c.getIntensity(false) > 0) { updateIntensity(c); diff --git a/forge-game/src/main/java/forge/game/card/CounterEnumType.java b/forge-game/src/main/java/forge/game/card/CounterEnumType.java index 6fa250d000b..a9565c05af1 100644 --- a/forge-game/src/main/java/forge/game/card/CounterEnumType.java +++ b/forge-game/src/main/java/forge/game/card/CounterEnumType.java @@ -95,8 +95,6 @@ public enum CounterEnumType { CORRUPTION("CRPTN", 210, 121, 210), - CRANK("CRANK!", 181, 148, 15), - CROAK("CROAK", 155, 255, 5), CREDIT("CRDIT", 188, 197, 234), diff --git a/forge-game/src/main/java/forge/game/player/Player.java b/forge-game/src/main/java/forge/game/player/Player.java index 2db3db2e60f..68c6db6345a 100644 --- a/forge-game/src/main/java/forge/game/player/Player.java +++ b/forge-game/src/main/java/forge/game/player/Player.java @@ -189,7 +189,6 @@ public class Player extends GameEntity implements Comparable { private final Map commanderCast = Maps.newHashMap(); private final Map commanderDamage = Maps.newHashMap(); private DetachedCardEffect commanderEffect = null; - private DetachedCardEffect speedEffect; private Card monarchEffect; private Card initiativeEffect; @@ -197,6 +196,7 @@ public class Player extends GameEntity implements Comparable { private Card contraptionSprocketEffect; private Card radiationEffect; private Card keywordEffect; + private Card speedEffect; private Map additionalVotes = Maps.newHashMap(); private Map additionalOptionalVotes = Maps.newHashMap(); @@ -1971,20 +1971,19 @@ public class Player extends GameEntity implements Comparable { return speed; } public final void increaseSpeed() { - if (speedEffect == null) createSpeedEffect(); if (!maxSpeed()) { // can't increase past 4 int old = speed; speed++; - view.updateSpeed(this); getGame().fireEvent(new GameEventSpeedChanged(this, old, speed)); //play sound effect + updateSpeedEffect(); } } public final void decreaseSpeed() { if (speed > 1) { // can't decrease speed below 1 int old = speed; speed--; - view.updateSpeed(this); game.fireEvent(new GameEventSpeedChanged(this, old, speed)); + updateSpeedEffect(); } } public final boolean noSpeed() { @@ -1995,22 +1994,62 @@ public class Player extends GameEntity implements Comparable { } public final void setSpeed(int i) { //just used for copy/save speed = i; - if (speed > 0) view.updateSpeed(this); + if(this.speedEffect != null) + updateSpeedEffect(); } public final void createSpeedEffect() { - final PlayerZone com = getZone(ZoneType.Command); - DetachedCardEffect eff = new DetachedCardEffect(this, "Speed Effect"); + if(this.speedEffect != null || this.noSpeed()) + return; + + speedEffect = new Card(game.nextCardId(), null, game); + speedEffect.setOwner(this); + speedEffect.setGamePieceType(GamePieceType.EFFECT); + + speedEffect.addAlternateState(CardStateName.Flipped, false); + CardState speedFront = speedEffect.getState(CardStateName.Original); + CardState speedBack = speedEffect.getState(CardStateName.Flipped); + + speedFront.setImageKey("t:speed"); + speedFront.setName("Start Your Engines!"); + + speedBack.setImageKey("t:max_speed"); + speedBack.setName("Max Speed!"); + + String label = Localizer.getInstance().getMessage("lblSpeed", this.speed); + speedEffect.setOverlayText(label); + // 702.179d There is an inherent triggered ability associated with a player having 1 or more speed. This ability has no source and is controlled by that player. // That ability is “Whenever one or more opponents lose life during your turn, if your speed is less than 4, your speed increases by 1. This ability triggers only once each turn.” String trigger = "Mode$ LifeLostAll | ValidPlayer$ Opponent | TriggerZones$ Command | ActivationLimit$ 1 | " + "PlayerTurn$ True | CheckSVar$ Count$YourSpeed | SVarCompare$ LT4 | " + "TriggerDescription$ Whenever one or more opponents lose life during your turn, if your speed is less than 4, your speed increases by 1. This ability triggers only once each turn."; String speedUp = "DB$ ChangeSpeed"; - Trigger lifeLostTrigger = TriggerHandler.parseTrigger(trigger, eff, true); - lifeLostTrigger.setOverridingAbility(AbilityFactory.getAbility(speedUp, eff)); - eff.addTrigger(lifeLostTrigger); - this.speedEffect = eff; - com.add(eff); + Trigger lifeLostTrigger = TriggerHandler.parseTrigger(trigger, speedEffect, true); + lifeLostTrigger.setOverridingAbility(AbilityFactory.getAbility(speedUp, speedEffect)); + speedFront.addTrigger(lifeLostTrigger); + + speedEffect.updateStateForView(); + + if(this.maxSpeed()) + speedEffect.setState(CardStateName.Flipped, true); + + final PlayerZone com = getZone(ZoneType.Command); + com.add(speedEffect); + this.updateZoneForView(com); + } + protected void updateSpeedEffect() { + if(this.speedEffect == null) { + if(this.noSpeed()) + return; + createSpeedEffect(); + } + Localizer localizer = Localizer.getInstance(); + String label = this.maxSpeed() ? localizer.getMessage("lblMaxSpeed") : localizer.getMessage("lblSpeed", this.speed); + speedEffect.setOverlayText(label); + if(maxSpeed() && speedEffect.getCurrentStateName() == CardStateName.Original) + speedEffect.setState(CardStateName.Flipped, true); + else if(!maxSpeed() && speedEffect.getCurrentStateName() == CardStateName.Flipped) + speedEffect.setState(CardStateName.Original, true); } public final List getPlaneswalkedToThisTurn() { @@ -3990,8 +4029,8 @@ public class Player extends GameEntity implements Comparable { public void setCrankCounter(int counters) { this.crankCounter = counters; if (this.contraptionSprocketEffect != null) { - Map counterMap = Map.of(CounterType.get(CounterEnumType.CRANK), this.crankCounter); - contraptionSprocketEffect.setCounters(counterMap); + String label = Localizer.getInstance().getMessage("lblCrank", this.crankCounter); + contraptionSprocketEffect.setOverlayText(label); } else if (this.getCardsIn(ZoneType.Battlefield).anyMatch(CardPredicates.CONTRAPTIONS)) { this.createContraptionSprockets(); @@ -4017,12 +4056,8 @@ public class Player extends GameEntity implements Comparable { contraptionSprocketEffect.setName("Contraption Sprockets"); contraptionSprocketEffect.setGamePieceType(GamePieceType.EFFECT); - //Add "counters" on the effect to represent the current CRANK counter position. - //This and some other un-cards could benefit from a distinct system for positional counters or markers, - //see for instance Baron von Count or B-I-N-G-O. For now this is sufficient to display the current sprocket - //on the counter overlay, and I don't think any existing effect will notice it. - Map counterMap = Map.of(CounterType.get(CounterEnumType.CRANK), this.crankCounter); - contraptionSprocketEffect.setCounters(counterMap); + String label = Localizer.getInstance().getMessage("lblCrank", this.crankCounter); + contraptionSprocketEffect.setOverlayText(label); contraptionSprocketEffect.setText("At the beginning of your upkeep, if you control a Contraption, move the CRANK! counter to the next sprocket and crank any number of that sprocket's Contraptions."); contraptionSprocketEffect.updateStateForView(); diff --git a/forge-game/src/main/java/forge/game/player/PlayerView.java b/forge-game/src/main/java/forge/game/player/PlayerView.java index a5934479cff..d7130dab99a 100644 --- a/forge-game/src/main/java/forge/game/player/PlayerView.java +++ b/forge-game/src/main/java/forge/game/player/PlayerView.java @@ -329,13 +329,6 @@ public class PlayerView extends GameEntityView { set(TrackableProperty.ControlVotes, val); } - public int getSpeed() { - return get(TrackableProperty.Speed); - } - void updateSpeed(Player p) { - set(TrackableProperty.Speed, p.getSpeed()); - } - public int getAdditionalVillainousChoices() { return get(TrackableProperty.AdditionalVillainousChoices); } @@ -604,10 +597,6 @@ public class PlayerView extends GameEntityView { } details.add(Localizer.getInstance().getMessage("lblExtraTurnCountHas", String.valueOf(getExtraTurnCount()))); - if (getSpeed() > 0) { - details.add(Localizer.getInstance().getMessage("lblSpeed", String.valueOf(getSpeed()))); - } - final String keywords = Lang.joinHomogenous(getDisplayableKeywords()); if (!keywords.isEmpty()) { details.add(keywords); diff --git a/forge-game/src/main/java/forge/trackable/TrackableProperty.java b/forge-game/src/main/java/forge/trackable/TrackableProperty.java index 6faafb56411..f8a46ab428e 100644 --- a/forge-game/src/main/java/forge/trackable/TrackableProperty.java +++ b/forge-game/src/main/java/forge/trackable/TrackableProperty.java @@ -85,6 +85,8 @@ public enum TrackableProperty { RingLevel(TrackableTypes.IntegerType), CurrentRoom(TrackableTypes.StringType), Intensity(TrackableTypes.IntegerType), + OverlayText(TrackableTypes.StringType), + MarkerText(TrackableTypes.StringListType), Remembered(TrackableTypes.StringType), NamedCard(TrackableTypes.StringListType), PlayerMayLook(TrackableTypes.PlayerViewCollectionType, FreezeMode.IgnoresFreeze), @@ -217,7 +219,6 @@ public enum TrackableProperty { CommanderCast(TrackableTypes.IntegerMapType), CommanderDamage(TrackableTypes.IntegerMapType), MindSlaveMaster(TrackableTypes.PlayerViewType), - Speed(TrackableTypes.IntegerType), Ante(TrackableTypes.CardViewCollectionType, FreezeMode.IgnoresFreeze), Battlefield(TrackableTypes.CardViewCollectionType, FreezeMode.IgnoresFreeze), //zones can't respect freeze, otherwise cards that die from state based effects won't have that reflected in the UI diff --git a/forge-gui-desktop/src/main/java/forge/view/arcane/CardPanel.java b/forge-gui-desktop/src/main/java/forge/view/arcane/CardPanel.java index 20b10e34fbf..73b334f2490 100644 --- a/forge-gui-desktop/src/main/java/forge/view/arcane/CardPanel.java +++ b/forge-gui-desktop/src/main/java/forge/view/arcane/CardPanel.java @@ -533,11 +533,8 @@ public class CardPanel extends SkinnedPanel implements CardContainer, IDisposabl } - if (card.getCurrentRoom() != null && !card.getCurrentRoom().isEmpty()) { - List markers = new ArrayList<>(); - markers.add("In Room:"); - markers.add(card.getCurrentRoom()); - drawMarkersTabs(g, markers); + if(card.getMarkerText() != null) { + drawMarkersTabs(g, card.getMarkerText()); } final int combatXSymbols = (cardXOffset + (cardWidth / 4)) - 16; diff --git a/forge-gui-mobile/src/forge/card/CardRenderer.java b/forge-gui-mobile/src/forge/card/CardRenderer.java index 193807f2c77..ca9c0b79552 100644 --- a/forge-gui-mobile/src/forge/card/CardRenderer.java +++ b/forge-gui-mobile/src/forge/card/CardRenderer.java @@ -781,23 +781,12 @@ public class CardRenderer { } - if (card.getCurrentRoom() != null && !card.getCurrentRoom().isEmpty()) { - List markers = new ArrayList<>(); - markers.add("In Room:"); - markers.add(card.getCurrentRoom()); - drawMarkersTabs(markers, g, x, y, w, h, false); - } - //Class level - if (card.getCurrentState().getType().hasStringType("Class") && ZoneType.Battlefield.equals(card.getZone())) { - List markers = new ArrayList<>(); - markers.add("CL:" + card.getClassLevel()); - drawMarkersTabs(markers, g, x, y - markersHeight, w, h, true); - } - //Ring level - if (card.getRingLevel() > 0) { - List markers = new ArrayList<>(); - markers.add("RL:" + card.getRingLevel()); - drawMarkersTabs(markers, g, x, y - markersHeight, w, h, true); + if(card.getMarkerText() != null) { + List markers = card.getMarkerText(); + if(markers.size() > 1) //Use smaller text for multi-line strings. + drawMarkersTabs(markers, g, x, y, w, h, false); + else + drawMarkersTabs(markers, g, x, y - markersHeight, w, h, true); } float otherSymbolsSize = w / 4f; @@ -1528,7 +1517,7 @@ public class CardRenderer { int pageSize = 128; //only generate images for characters that could be used by Forge - String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890/-+:'"; + String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890/-+:'!—"; final PixmapPacker packer = new PixmapPacker(pageSize, pageSize, Pixmap.Format.RGBA8888, 2, false); final FreeTypeFontParameter parameter = new FreeTypeFontParameter(); diff --git a/forge-gui/res/languages/en-US.properties b/forge-gui/res/languages/en-US.properties index f6396408b0e..6d51e4a8d10 100644 --- a/forge-gui/res/languages/en-US.properties +++ b/forge-gui/res/languages/en-US.properties @@ -2347,7 +2347,6 @@ lblAntedHas=Ante''d: {0} lblAdditionalVotes=You get {0} additional votes. lblOptionalAdditionalVotes=You may vote {0} additional times. lblControlsVote=You choose how each player votes. -lblSpeed=Speed: {0} #VStack.java lblAlwaysYes=Always Yes lblAlwaysNo=Always No @@ -3007,6 +3006,9 @@ lblChooseACompanion=Choose a companion lblChooseAColorFor=Choose a color for {0} lblRevealFaceDownCards=Revealing face-down cards from lblLearnALesson=Learn a Lesson +lblSpeed=SPEED: {0} +lblMaxSpeed=SPEED: MAX! +lblCrank=CRANK! — {0} #QuestPreferences.java lblWildOpponentNumberError=Wild Opponents can only be 0 to 3 #GauntletWinLose.java diff --git a/forge-gui/src/main/java/forge/gui/card/CardDetailUtil.java b/forge-gui/src/main/java/forge/gui/card/CardDetailUtil.java index 11ca00bda78..c0c56041405 100644 --- a/forge-gui/src/main/java/forge/gui/card/CardDetailUtil.java +++ b/forge-gui/src/main/java/forge/gui/card/CardDetailUtil.java @@ -520,13 +520,19 @@ public class CardDetailUtil { // class level if (card.getId() >= 0 && card.getCurrentState().getType().hasStringType("Class") && card.getZone() == ZoneType.Battlefield) { area.append("\n\n"); - area.append("(Class Level:").append(card.getClassLevel()).append(")"); + area.append("(Class Level: ").append(card.getClassLevel()).append(")"); } //ring level if (card.getRingLevel() > 0 && card.getZone() == ZoneType.Command) { area.append("\n\n"); - area.append("(Ring Level:").append(card.getRingLevel()).append(")"); + area.append("(Ring Level: ").append(card.getRingLevel()).append(")"); + } + + // Text on gameplay trackers (e.g. Speed) + if (StringUtils.isNotEmpty(card.getOverlayText())) { + area.append("\n\n"); + area.append(String.format("(%s)", card.getOverlayText())); } // sector