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 <Jetz722@gmail.com>
This commit is contained in:
Jetz72
2025-02-09 23:43:48 -05:00
committed by GitHub
parent 04172eead0
commit 493a8f351b
10 changed files with 128 additions and 60 deletions

View File

@@ -340,6 +340,8 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
protected boolean renderForUi = true; protected boolean renderForUi = true;
private final CardView view; private final CardView view;
private String overlayText = null;
private SpellAbility[] basicLandAbilities = new SpellAbility[MagicColor.WUBRG.length]; private SpellAbility[] basicLandAbilities = new SpellAbility[MagicColor.WUBRG.length];
private int planeswalkerAbilityActivated; private int planeswalkerAbilityActivated;
@@ -6826,6 +6828,17 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
return getType().hasStringType("Class"); 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() { public final void animateBestow() {
animateBestow(true); animateBestow(true);
} }

View File

@@ -491,6 +491,7 @@ public class CardView extends GameEntityView {
} }
void updateCurrentRoom(Card c) { void updateCurrentRoom(Card c) {
set(TrackableProperty.CurrentRoom, c.getCurrentRoom()); set(TrackableProperty.CurrentRoom, c.getCurrentRoom());
updateMarkerText(c);
} }
public int getIntensity() { public int getIntensity() {
@@ -514,6 +515,7 @@ public class CardView extends GameEntityView {
} }
void updateClassLevel(Card c) { void updateClassLevel(Card c) {
set(TrackableProperty.ClassLevel, c.getClassLevel()); set(TrackableProperty.ClassLevel, c.getClassLevel());
updateMarkerText(c);
} }
public int getRingLevel() { public int getRingLevel() {
@@ -525,6 +527,41 @@ public class CardView extends GameEntityView {
set(TrackableProperty.RingLevel, p.getNumRingTemptedYou()); set(TrackableProperty.RingLevel, p.getNumRingTemptedYou());
} }
public String getOverlayText() {
return get(TrackableProperty.OverlayText);
}
public List<String> getMarkerText() {
return get(TrackableProperty.MarkerText);
}
void updateMarkerText(Card c) {
List<String> 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() { private String getRemembered() {
return get(TrackableProperty.Remembered); return get(TrackableProperty.Remembered);
} }
@@ -974,6 +1011,7 @@ public class CardView extends GameEntityView {
updateDamage(c); updateDamage(c);
updateSpecialize(c); updateSpecialize(c);
updateRingLevel(c); updateRingLevel(c);
updateMarkerText(c);
if (c.getIntensity(false) > 0) { if (c.getIntensity(false) > 0) {
updateIntensity(c); updateIntensity(c);

View File

@@ -95,8 +95,6 @@ public enum CounterEnumType {
CORRUPTION("CRPTN", 210, 121, 210), CORRUPTION("CRPTN", 210, 121, 210),
CRANK("CRANK!", 181, 148, 15),
CROAK("CROAK", 155, 255, 5), CROAK("CROAK", 155, 255, 5),
CREDIT("CRDIT", 188, 197, 234), CREDIT("CRDIT", 188, 197, 234),

View File

@@ -189,7 +189,6 @@ public class Player extends GameEntity implements Comparable<Player> {
private final Map<Card, Integer> commanderCast = Maps.newHashMap(); private final Map<Card, Integer> commanderCast = Maps.newHashMap();
private final Map<Card, Integer> commanderDamage = Maps.newHashMap(); private final Map<Card, Integer> commanderDamage = Maps.newHashMap();
private DetachedCardEffect commanderEffect = null; private DetachedCardEffect commanderEffect = null;
private DetachedCardEffect speedEffect;
private Card monarchEffect; private Card monarchEffect;
private Card initiativeEffect; private Card initiativeEffect;
@@ -197,6 +196,7 @@ public class Player extends GameEntity implements Comparable<Player> {
private Card contraptionSprocketEffect; private Card contraptionSprocketEffect;
private Card radiationEffect; private Card radiationEffect;
private Card keywordEffect; private Card keywordEffect;
private Card speedEffect;
private Map<Long, Integer> additionalVotes = Maps.newHashMap(); private Map<Long, Integer> additionalVotes = Maps.newHashMap();
private Map<Long, Integer> additionalOptionalVotes = Maps.newHashMap(); private Map<Long, Integer> additionalOptionalVotes = Maps.newHashMap();
@@ -1971,20 +1971,19 @@ public class Player extends GameEntity implements Comparable<Player> {
return speed; return speed;
} }
public final void increaseSpeed() { public final void increaseSpeed() {
if (speedEffect == null) createSpeedEffect();
if (!maxSpeed()) { // can't increase past 4 if (!maxSpeed()) { // can't increase past 4
int old = speed; int old = speed;
speed++; speed++;
view.updateSpeed(this);
getGame().fireEvent(new GameEventSpeedChanged(this, old, speed)); //play sound effect getGame().fireEvent(new GameEventSpeedChanged(this, old, speed)); //play sound effect
updateSpeedEffect();
} }
} }
public final void decreaseSpeed() { public final void decreaseSpeed() {
if (speed > 1) { // can't decrease speed below 1 if (speed > 1) { // can't decrease speed below 1
int old = speed; int old = speed;
speed--; speed--;
view.updateSpeed(this);
game.fireEvent(new GameEventSpeedChanged(this, old, speed)); game.fireEvent(new GameEventSpeedChanged(this, old, speed));
updateSpeedEffect();
} }
} }
public final boolean noSpeed() { public final boolean noSpeed() {
@@ -1995,22 +1994,62 @@ public class Player extends GameEntity implements Comparable<Player> {
} }
public final void setSpeed(int i) { //just used for copy/save public final void setSpeed(int i) { //just used for copy/save
speed = i; speed = i;
if (speed > 0) view.updateSpeed(this); if(this.speedEffect != null)
updateSpeedEffect();
} }
public final void createSpeedEffect() { public final void createSpeedEffect() {
final PlayerZone com = getZone(ZoneType.Command); if(this.speedEffect != null || this.noSpeed())
DetachedCardEffect eff = new DetachedCardEffect(this, "Speed Effect"); 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. // 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.” // 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 | " + String trigger = "Mode$ LifeLostAll | ValidPlayer$ Opponent | TriggerZones$ Command | ActivationLimit$ 1 | " +
"PlayerTurn$ True | CheckSVar$ Count$YourSpeed | SVarCompare$ LT4 | " "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."; + "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"; String speedUp = "DB$ ChangeSpeed";
Trigger lifeLostTrigger = TriggerHandler.parseTrigger(trigger, eff, true); Trigger lifeLostTrigger = TriggerHandler.parseTrigger(trigger, speedEffect, true);
lifeLostTrigger.setOverridingAbility(AbilityFactory.getAbility(speedUp, eff)); lifeLostTrigger.setOverridingAbility(AbilityFactory.getAbility(speedUp, speedEffect));
eff.addTrigger(lifeLostTrigger); speedFront.addTrigger(lifeLostTrigger);
this.speedEffect = eff;
com.add(eff); 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<Card> getPlaneswalkedToThisTurn() { public final List<Card> getPlaneswalkedToThisTurn() {
@@ -3990,8 +4029,8 @@ public class Player extends GameEntity implements Comparable<Player> {
public void setCrankCounter(int counters) { public void setCrankCounter(int counters) {
this.crankCounter = counters; this.crankCounter = counters;
if (this.contraptionSprocketEffect != null) { if (this.contraptionSprocketEffect != null) {
Map<CounterType, Integer> counterMap = Map.of(CounterType.get(CounterEnumType.CRANK), this.crankCounter); String label = Localizer.getInstance().getMessage("lblCrank", this.crankCounter);
contraptionSprocketEffect.setCounters(counterMap); contraptionSprocketEffect.setOverlayText(label);
} }
else if (this.getCardsIn(ZoneType.Battlefield).anyMatch(CardPredicates.CONTRAPTIONS)) { else if (this.getCardsIn(ZoneType.Battlefield).anyMatch(CardPredicates.CONTRAPTIONS)) {
this.createContraptionSprockets(); this.createContraptionSprockets();
@@ -4017,12 +4056,8 @@ public class Player extends GameEntity implements Comparable<Player> {
contraptionSprocketEffect.setName("Contraption Sprockets"); contraptionSprocketEffect.setName("Contraption Sprockets");
contraptionSprocketEffect.setGamePieceType(GamePieceType.EFFECT); contraptionSprocketEffect.setGamePieceType(GamePieceType.EFFECT);
//Add "counters" on the effect to represent the current CRANK counter position. String label = Localizer.getInstance().getMessage("lblCrank", this.crankCounter);
//This and some other un-cards could benefit from a distinct system for positional counters or markers, contraptionSprocketEffect.setOverlayText(label);
//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<CounterType, Integer> counterMap = Map.of(CounterType.get(CounterEnumType.CRANK), this.crankCounter);
contraptionSprocketEffect.setCounters(counterMap);
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.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(); contraptionSprocketEffect.updateStateForView();

View File

@@ -329,13 +329,6 @@ public class PlayerView extends GameEntityView {
set(TrackableProperty.ControlVotes, val); set(TrackableProperty.ControlVotes, val);
} }
public int getSpeed() {
return get(TrackableProperty.Speed);
}
void updateSpeed(Player p) {
set(TrackableProperty.Speed, p.getSpeed());
}
public int getAdditionalVillainousChoices() { public int getAdditionalVillainousChoices() {
return get(TrackableProperty.AdditionalVillainousChoices); return get(TrackableProperty.AdditionalVillainousChoices);
} }
@@ -604,10 +597,6 @@ public class PlayerView extends GameEntityView {
} }
details.add(Localizer.getInstance().getMessage("lblExtraTurnCountHas", String.valueOf(getExtraTurnCount()))); 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()); final String keywords = Lang.joinHomogenous(getDisplayableKeywords());
if (!keywords.isEmpty()) { if (!keywords.isEmpty()) {
details.add(keywords); details.add(keywords);

View File

@@ -85,6 +85,8 @@ public enum TrackableProperty {
RingLevel(TrackableTypes.IntegerType), RingLevel(TrackableTypes.IntegerType),
CurrentRoom(TrackableTypes.StringType), CurrentRoom(TrackableTypes.StringType),
Intensity(TrackableTypes.IntegerType), Intensity(TrackableTypes.IntegerType),
OverlayText(TrackableTypes.StringType),
MarkerText(TrackableTypes.StringListType),
Remembered(TrackableTypes.StringType), Remembered(TrackableTypes.StringType),
NamedCard(TrackableTypes.StringListType), NamedCard(TrackableTypes.StringListType),
PlayerMayLook(TrackableTypes.PlayerViewCollectionType, FreezeMode.IgnoresFreeze), PlayerMayLook(TrackableTypes.PlayerViewCollectionType, FreezeMode.IgnoresFreeze),
@@ -217,7 +219,6 @@ public enum TrackableProperty {
CommanderCast(TrackableTypes.IntegerMapType), CommanderCast(TrackableTypes.IntegerMapType),
CommanderDamage(TrackableTypes.IntegerMapType), CommanderDamage(TrackableTypes.IntegerMapType),
MindSlaveMaster(TrackableTypes.PlayerViewType), MindSlaveMaster(TrackableTypes.PlayerViewType),
Speed(TrackableTypes.IntegerType),
Ante(TrackableTypes.CardViewCollectionType, FreezeMode.IgnoresFreeze), 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 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

View File

@@ -533,11 +533,8 @@ public class CardPanel extends SkinnedPanel implements CardContainer, IDisposabl
} }
if (card.getCurrentRoom() != null && !card.getCurrentRoom().isEmpty()) { if(card.getMarkerText() != null) {
List<String> markers = new ArrayList<>(); drawMarkersTabs(g, card.getMarkerText());
markers.add("In Room:");
markers.add(card.getCurrentRoom());
drawMarkersTabs(g, markers);
} }
final int combatXSymbols = (cardXOffset + (cardWidth / 4)) - 16; final int combatXSymbols = (cardXOffset + (cardWidth / 4)) - 16;

View File

@@ -781,22 +781,11 @@ public class CardRenderer {
} }
if (card.getCurrentRoom() != null && !card.getCurrentRoom().isEmpty()) { if(card.getMarkerText() != null) {
List<String> markers = new ArrayList<>(); List<String> markers = card.getMarkerText();
markers.add("In Room:"); if(markers.size() > 1) //Use smaller text for multi-line strings.
markers.add(card.getCurrentRoom());
drawMarkersTabs(markers, g, x, y, w, h, false); drawMarkersTabs(markers, g, x, y, w, h, false);
} else
//Class level
if (card.getCurrentState().getType().hasStringType("Class") && ZoneType.Battlefield.equals(card.getZone())) {
List<String> markers = new ArrayList<>();
markers.add("CL:" + card.getClassLevel());
drawMarkersTabs(markers, g, x, y - markersHeight, w, h, true);
}
//Ring level
if (card.getRingLevel() > 0) {
List<String> markers = new ArrayList<>();
markers.add("RL:" + card.getRingLevel());
drawMarkersTabs(markers, g, x, y - markersHeight, w, h, true); drawMarkersTabs(markers, g, x, y - markersHeight, w, h, true);
} }
@@ -1528,7 +1517,7 @@ public class CardRenderer {
int pageSize = 128; int pageSize = 128;
//only generate images for characters that could be used by Forge //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 PixmapPacker packer = new PixmapPacker(pageSize, pageSize, Pixmap.Format.RGBA8888, 2, false);
final FreeTypeFontParameter parameter = new FreeTypeFontParameter(); final FreeTypeFontParameter parameter = new FreeTypeFontParameter();

View File

@@ -2347,7 +2347,6 @@ lblAntedHas=Ante''d: {0}
lblAdditionalVotes=You get {0} additional votes. lblAdditionalVotes=You get {0} additional votes.
lblOptionalAdditionalVotes=You may vote {0} additional times. lblOptionalAdditionalVotes=You may vote {0} additional times.
lblControlsVote=You choose how each player votes. lblControlsVote=You choose how each player votes.
lblSpeed=Speed: {0}
#VStack.java #VStack.java
lblAlwaysYes=Always Yes lblAlwaysYes=Always Yes
lblAlwaysNo=Always No lblAlwaysNo=Always No
@@ -3007,6 +3006,9 @@ lblChooseACompanion=Choose a companion
lblChooseAColorFor=Choose a color for {0} lblChooseAColorFor=Choose a color for {0}
lblRevealFaceDownCards=Revealing face-down cards from lblRevealFaceDownCards=Revealing face-down cards from
lblLearnALesson=Learn a Lesson lblLearnALesson=Learn a Lesson
lblSpeed=SPEED: {0}
lblMaxSpeed=SPEED: MAX!
lblCrank=CRANK! — {0}
#QuestPreferences.java #QuestPreferences.java
lblWildOpponentNumberError=Wild Opponents can only be 0 to 3 lblWildOpponentNumberError=Wild Opponents can only be 0 to 3
#GauntletWinLose.java #GauntletWinLose.java

View File

@@ -520,13 +520,19 @@ public class CardDetailUtil {
// class level // class level
if (card.getId() >= 0 && card.getCurrentState().getType().hasStringType("Class") && card.getZone() == ZoneType.Battlefield) { if (card.getId() >= 0 && card.getCurrentState().getType().hasStringType("Class") && card.getZone() == ZoneType.Battlefield) {
area.append("\n\n"); area.append("\n\n");
area.append("(Class Level:").append(card.getClassLevel()).append(")"); area.append("(Class Level: ").append(card.getClassLevel()).append(")");
} }
//ring level //ring level
if (card.getRingLevel() > 0 && card.getZone() == ZoneType.Command) { if (card.getRingLevel() > 0 && card.getZone() == ZoneType.Command) {
area.append("\n\n"); 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 // sector