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;
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<Card>, 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);
}

View File

@@ -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<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() {
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);

View File

@@ -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),

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> 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<Player> {
private Card contraptionSprocketEffect;
private Card radiationEffect;
private Card keywordEffect;
private Card speedEffect;
private Map<Long, Integer> additionalVotes = Maps.newHashMap();
private Map<Long, Integer> additionalOptionalVotes = Maps.newHashMap();
@@ -1971,20 +1971,19 @@ public class Player extends GameEntity implements Comparable<Player> {
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<Player> {
}
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<Card> getPlaneswalkedToThisTurn() {
@@ -3990,8 +4029,8 @@ public class Player extends GameEntity implements Comparable<Player> {
public void setCrankCounter(int counters) {
this.crankCounter = counters;
if (this.contraptionSprocketEffect != null) {
Map<CounterType, Integer> 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<Player> {
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<CounterType, Integer> 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();

View File

@@ -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);

View File

@@ -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

View File

@@ -533,11 +533,8 @@ public class CardPanel extends SkinnedPanel implements CardContainer, IDisposabl
}
if (card.getCurrentRoom() != null && !card.getCurrentRoom().isEmpty()) {
List<String> 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;

View File

@@ -781,23 +781,12 @@ public class CardRenderer {
}
if (card.getCurrentRoom() != null && !card.getCurrentRoom().isEmpty()) {
List<String> 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<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);
if(card.getMarkerText() != null) {
List<String> 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();

View File

@@ -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

View File

@@ -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