GUI: introduced default card hints:

* refactor: added helper emblems instead rad counter's inherent emblems (use initGameDefaultHelperEmblems to define new card hints or other fake objects);
* refactor: added card hints support for emblems, planes and other command objects;
* GUI: added storm counter as default card hint (use hints tool to see it, closes #12360);
This commit is contained in:
Oleg Agafonov 2024-07-27 09:40:41 +04:00
parent 83823acec7
commit 521a0f6e32
36 changed files with 234 additions and 144 deletions

View file

@ -152,7 +152,7 @@ public class MageEditorPane extends JEditorPane {
if (cardView == null) {
Plane plane = Plane.createPlaneByFullName(cardName);
if (plane != null) {
cardView = new CardView(new PlaneView(plane));
cardView = new CardView(new PlaneView(plane, null));
}
}

View file

@ -423,7 +423,7 @@ public class MageBook extends JComponent {
}
private void addEmblem(Emblem emblem, BigCard bigCard, UUID gameId, Rectangle rectangle) {
CardView cardView = new CardView(new EmblemView(emblem));
CardView cardView = new CardView(new EmblemView(emblem, null));
addCard(cardView, bigCard, gameId, rectangle, false);
}
@ -433,7 +433,7 @@ public class MageBook extends JComponent {
}
private void addPlane(Plane plane, BigCard bigCard, UUID gameId, Rectangle rectangle) {
CardView cardView = new CardView(new PlaneView(plane));
CardView cardView = new CardView(new PlaneView(plane, null));
addCard(cardView, bigCard, gameId, rectangle, false);
}

View file

@ -163,7 +163,7 @@ public class CardHintsHelperDialog extends MageDialog implements MageDesktopIcon
settings.add(this.currentGroup.toString());
// from search
if (this.currentSearch.length() > 0 && !this.currentSearch.equals(SEARCH_EMPTY_TEXT)) {
if (!this.currentSearch.isEmpty() && !this.currentSearch.equals(SEARCH_EMPTY_TEXT)) {
settings.add(this.currentSearch);
}
@ -235,6 +235,11 @@ public class CardHintsHelperDialog extends MageDialog implements MageDesktopIcon
this.lastHints.add(new CardHintInfo(currentPlayer, "hand", card));
});
// helper emblems for better UX
this.lastGameView.getMyHelperEmblems().values().forEach(card -> {
this.lastHints.add(new CardHintInfo(currentPlayer, "xmage", card));
});
// stack
this.lastGameView.getStack().values().forEach(card -> {
this.lastHints.add(new CardHintInfo(currentPlayer, "stack", card));

View file

@ -223,20 +223,20 @@ public class TestCardRenderDialog extends MageDialog {
return cardView;
}
private AbilityView createEmblem(Emblem emblem) {
AbilityView emblemView = new AbilityView(emblem.getAbilities().get(0), emblem.getName(), new CardView(new EmblemView(emblem)));
private AbilityView createEmblem(Game game, Emblem emblem) {
AbilityView emblemView = new AbilityView(emblem.getAbilities().get(0), emblem.getName(), new CardView(new EmblemView(emblem, game)));
emblemView.setName(emblem.getName());
return emblemView;
}
private AbilityView createDungeon(Dungeon dungeon) {
private AbilityView createDungeon(Game game, Dungeon dungeon) {
AbilityView emblemView = new AbilityView(dungeon.getAbilities().get(0), dungeon.getName(), new CardView(new DungeonView(dungeon)));
emblemView.setName(dungeon.getName());
return emblemView;
}
private AbilityView createPlane(Plane plane) {
AbilityView planeView = new AbilityView(plane.getAbilities().get(0), plane.getName(), new CardView(new PlaneView(plane)));
private AbilityView createPlane(Game game, Plane plane) {
AbilityView planeView = new AbilityView(plane.getAbilities().get(0), plane.getName(), new CardView(new PlaneView(plane, game)));
planeView.setName(plane.getName());
return planeView;
}

View file

@ -145,6 +145,7 @@ public final class GamePanel extends javax.swing.JPanel {
}
this.game.getMyHand().values().forEach(c -> this.allCardsIndex.put(c.getId(), c));
this.game.getMyHelperEmblems().values().forEach(c -> this.allCardsIndex.put(c.getId(), c));
this.game.getStack().values().forEach(c -> this.allCardsIndex.put(c.getId(), c));
this.game.getExile()
.stream()

View file

@ -821,7 +821,7 @@ public class CardView extends SimpleCardView {
this.mageObjectType = MageObjectType.EMBLEM;
Emblem emblem = (Emblem) object;
this.rarity = Rarity.SPECIAL;
this.rules = new ArrayList<>(emblem.getAbilities().getRules(emblem.getName()));
this.rules = new ArrayList<>(emblem.getAbilities().getRules(game, emblem));
} else if (object instanceof Dungeon) {
this.mageObjectType = MageObjectType.DUNGEON;
Dungeon dungeon = (Dungeon) object;
@ -834,14 +834,14 @@ public class CardView extends SimpleCardView {
this.frameStyle = FrameStyle.M15_NORMAL;
// Display in landscape/rotated/on its side
this.rotate = true;
this.rules = new ArrayList<>(plane.getAbilities().getRules(plane.getName()));
this.rules = new ArrayList<>(plane.getAbilities().getRules(game, plane));
} else if (object instanceof Designation) {
this.mageObjectType = MageObjectType.DESIGNATION;
Designation designation = (Designation) object;
this.rarity = Rarity.SPECIAL;
this.frameStyle = FrameStyle.M15_NORMAL;
// Display in landscape/rotated/on its side
this.rules = new ArrayList<>(designation.getAbilities().getRules(designation.getName()));
this.rules = new ArrayList<>(designation.getAbilities().getRules(game, designation));
}
if (this.rarity == null && object instanceof StackAbility) {
StackAbility stackAbility = (StackAbility) object;
@ -1106,7 +1106,7 @@ public class CardView extends SimpleCardView {
this.name = token.getName();
this.displayName = token.getName();
this.displayFullName = token.getName();
this.rules = new ArrayList<>(token.getAbilities().getRules(this.name));
this.rules = new ArrayList<>(token.getAbilities().getRules(game, token));
this.power = token.getPower().toString();
this.toughness = token.getToughness().toString();
this.loyalty = "";

View file

@ -107,26 +107,22 @@ public class CardsView extends LinkedHashMap<UUID, CardView> {
isCard = true;
}
if (sourceObject instanceof Emblem) {
// Emblems are not normally OUTSIDE, except the special Radiation Emblem from rad counters.
abilityView = new AbilityView(ability, sourceObject.getName(), new CardView(new EmblemView((Emblem) sourceObject)));
// emblems are not normally OUTSIDE, except the helper emblems like Radiation
abilityView = new AbilityView(ability, sourceObject.getName(), new CardView(new EmblemView((Emblem) sourceObject, game)));
abilityView.setName(sourceObject.getName());
}
break;
case COMMAND:
sourceObject = game.getObject(ability.getSourceId());
if (sourceObject instanceof Emblem) {
// Card sourceCard = (Card) ((Emblem) sourceObject).getSourceObject();
// if (sourceCard == null) {
// throw new IllegalArgumentException("Source card for emblem not found.");
// }
abilityView = new AbilityView(ability, sourceObject.getName(), new CardView(new EmblemView((Emblem) sourceObject)));
abilityView = new AbilityView(ability, sourceObject.getName(), new CardView(new EmblemView((Emblem) sourceObject, game)));
abilityView.setName(sourceObject.getName());
// abilityView.setExpansionSetCode(sourceCard.getExpansionSetCode());
} else if (sourceObject instanceof Dungeon) {
abilityView = new AbilityView(ability, sourceObject.getName(), new CardView(new DungeonView((Dungeon) sourceObject)));
abilityView.setName(sourceObject.getName());
} else if (sourceObject instanceof Plane) {
abilityView = new AbilityView(ability, sourceObject.getName(), new CardView(new PlaneView((Plane) sourceObject)));
abilityView = new AbilityView(ability, sourceObject.getName(), new CardView(new PlaneView((Plane) sourceObject, game)));
abilityView.setName(sourceObject.getName());
}
break;

View file

@ -1,5 +1,6 @@
package mage.view;
import mage.game.Game;
import mage.game.command.Emblem;
import mage.game.command.emblems.EmblemOfCard;
import mage.players.PlayableObjectStats;
@ -23,13 +24,13 @@ public class EmblemView implements CommandObjectView, Serializable {
protected List<String> rules;
protected PlayableObjectStats playableStats = new PlayableObjectStats();
public EmblemView(Emblem emblem) {
public EmblemView(Emblem emblem, Game game) {
this.id = emblem.getId();
this.name = emblem.getName();
this.imageFileName = emblem.getImageFileName();
this.imageNumber = emblem.getImageNumber();
this.expansionSetCode = emblem.getExpansionSetCode();
this.rules = emblem.getAbilities().getRules(emblem.getName());
this.rules = emblem.getAbilities().getRules(game, emblem);
if (emblem instanceof EmblemOfCard) {
cardNumber = emblem.getCardNumber();
usesVariousArt = ((EmblemOfCard) emblem).getUsesVariousArt();

View file

@ -44,6 +44,7 @@ public class GameView implements Serializable {
private final List<PlayerView> players = new ArrayList<>();
private UUID myPlayerId = null; // null for watcher
private final CardsView myHand = new CardsView();
private final CardsView myHelperEmblems = new CardsView();
private PlayableObjectsList canPlayObjects;
private final Map<String, SimpleCardsView> opponentHands = new HashMap<>();
private final Map<String, SimpleCardsView> watchedHands = new HashMap<>();
@ -75,6 +76,11 @@ public class GameView implements Serializable {
createdForPlayer = player;
this.myPlayerId = player.getId();
this.myHand.putAll(new CardsView(game, player.getHand().getCards(game), createdForPlayerId));
state.getHelperEmblems().stream()
.filter(emblem -> emblem.isControlledBy(player.getId()))
.forEach(emblem -> {
this.myHelperEmblems.put(emblem.getId(), new CardView(new EmblemView(emblem, game)));
});
}
}
for (StackObject stackObject : state.getStack()) {
@ -112,7 +118,7 @@ public class GameView implements Serializable {
stack.put(stackObject.getId(), new StackAbilityView(game, (StackAbility) stackObject, token.getName(), token, new CardView(token, game)));
checkPaid(stackObject.getId(), (StackAbility) stackObject);
} else if (object instanceof Emblem) {
CardView cardView = new CardView(new EmblemView((Emblem) object));
CardView cardView = new CardView(new EmblemView((Emblem) object, game));
// Card sourceCard = (Card) ((Emblem) object).getSourceObject();
stackObject.setName(object.getName());
// ((StackAbility) stackObject).setExpansionSetCode(sourceCard.getExpansionSetCode());
@ -126,7 +132,7 @@ public class GameView implements Serializable {
new StackAbilityView(game, (StackAbility) stackObject, object.getName(), object, cardView));
checkPaid(stackObject.getId(), ((StackAbility) stackObject));
} else if (object instanceof Plane) {
CardView cardView = new CardView(new PlaneView((Plane) object));
CardView cardView = new CardView(new PlaneView((Plane) object, game));
stackObject.setName(object.getName());
stack.put(stackObject.getId(),
new StackAbilityView(game, (StackAbility) stackObject, object.getName(), object, cardView));
@ -239,6 +245,10 @@ public class GameView implements Serializable {
return myHand;
}
public CardsView getMyHelperEmblems() {
return myHelperEmblems;
}
public PlayerView getMyPlayer() {
if (this.myPlayerId == null) {
return null;

View file

@ -1,5 +1,6 @@
package mage.view;
import mage.game.Game;
import mage.game.command.Plane;
import mage.players.PlayableObjectStats;
@ -20,13 +21,13 @@ public class PlaneView implements CommandObjectView, Serializable {
protected List<String> rules;
protected PlayableObjectStats playableStats = new PlayableObjectStats();
public PlaneView(Plane plane) {
public PlaneView(Plane plane, Game game) {
this.id = plane.getId();
this.name = plane.getName();
this.imageFileName = plane.getImageFileName();
this.imageNumber = plane.getImageNumber();
this.expansionSetCode = plane.getExpansionSetCode();
this.rules = plane.getAbilities().getRules(plane.getName());
this.rules = plane.getAbilities().getRules(game, plane);
}
@Override

View file

@ -40,6 +40,7 @@ public class PlayerView implements Serializable {
private final CardsView graveyard = new CardsView();
private final CardsView exile = new CardsView();
private final CardsView sideboard = new CardsView();
private final CardsView helperCards = new CardsView();
private final Map<UUID, PermanentView> battlefield = new LinkedHashMap<>();
private final CardView topCard;
private final UserData userData;
@ -120,7 +121,7 @@ public class PlayerView implements Serializable {
if (commandObject instanceof Emblem) {
Emblem emblem = (Emblem) commandObject;
if (emblem.getControllerId().equals(this.playerId)) {
commandList.add(new EmblemView(emblem));
commandList.add(new EmblemView(emblem, game));
}
} else if (commandObject instanceof Dungeon) {
Dungeon dungeon = (Dungeon) commandObject;
@ -130,7 +131,7 @@ public class PlayerView implements Serializable {
} else if (commandObject instanceof Plane) {
Plane plane = (Plane) commandObject;
// Planes are universal and all players can see them.
commandList.add(new PlaneView(plane));
commandList.add(new PlaneView(plane, game));
} else if (commandObject instanceof Commander) {
Commander commander = (Commander) commandObject;
if (commander.getControllerId().equals(this.playerId)) {

View file

@ -156,9 +156,7 @@ public class TappedForManaFromMultipleEffects extends CardTestPlayerBase {
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}, Sacrifice a Fo");
setChoice(playerA, "Forest"); // sacrifice
setChoice(playerA, "Green");
setChoice(playerA, "Green");
setChoice(playerA, "Green");
setChoiceAmount(playerA, 0, 3); // x0 red, x3 green
checkManaPool("must produce green", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "G", 3);
setStrictChooseMode(true);
@ -177,9 +175,7 @@ public class TappedForManaFromMultipleEffects extends CardTestPlayerBase {
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}, Sacrifice a Fo");
setChoice(playerA, "Forest"); // sacrifice
setChoice(playerA, "Green");
setChoice(playerA, "Green");
setChoice(playerA, "Green");
setChoiceAmount(playerA, 0, 3); // x0 red, x3 green
checkManaPool("must produce green", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "G", 3 * 2); // double by mana reflect
setStrictChooseMode(true);

View file

@ -2892,7 +2892,7 @@ public class TestPlayer implements Player {
break;
}
}
Assert.fail(String.format("Missing choice in multi amount: %s (pos %d - %s)", type.getHeader(), i + 1, messages.get(i)));
Assert.fail(String.format("Missing choice in multi amount: %s (pos %d - %s)", type.getHeader(), i, messages));
}
// extra check

View file

@ -2258,7 +2258,7 @@ public class VerifyCardDataTest {
System.out.println();
System.out.println(card.getName() + " " + card.getManaCost().getText());
if (card instanceof SplitCard || card instanceof ModalDoubleFacedCard) {
card.getAbilities().getRules(card.getName()).forEach(this::printAbilityText);
card.getAbilities().getRules().forEach(this::printAbilityText);
} else {
card.getRules().forEach(this::printAbilityText);
}

View file

@ -1,6 +1,7 @@
package mage.abilities;
import mage.MageObject;
import mage.abilities.keyword.ProtectionAbility;
import mage.abilities.mana.ActivatedManaAbilityImpl;
import mage.constants.Zone;
@ -18,7 +19,6 @@ import java.util.function.Predicate;
* interface for this.
*
* @param <T> The ability type this collection will hold.
*
* @see mage.abilities.AbilitiesImpl
* @see mage.abilities.DelayedTriggeredAbilities
* @see mage.abilities.SpecialActions
@ -27,25 +27,27 @@ import java.util.function.Predicate;
public interface Abilities<T extends Ability> extends List<T>, Serializable {
/**
* Retrieves a {@link List}&lt;{@link String}&gt; of ability texts for the
* given source.
*
* @param source The source to retrieve ability texts.
* @return the {@link List}&lt;{@link String}&gt; of ability texts.
*
* @see mage.cards.CardImpl#getRules()
* @see mage.abilities.keyword.LevelAbility#getRule()
* Return rules as part of another rules. Use it for text generation.
*/
List<String> getRules(String source);
List<String> getRules();
List<String> getRules(String source, boolean capitalize);
/**
* Return rules as part of another rules. Use it for text generation.
*/
List<String> getRules(boolean capitalize);
/**
* Return full rules with card hints. Use it for user's data like GameView
*
* @param game on null will ignore card hints
*/
List<String> getRules(Game game, MageObject object);
/**
* Retrieves all activated abilities for the given {@link Zone}.
*
* @param zone The {@link Zone} for which abilities should be retrieved.
* @return All abilities for the given {@link Zone}
*
* @see mage.cards.CardImpl#getSpellAbility()
*/
Abilities<ActivatedAbility> getActivatedAbilities(Zone zone);
@ -55,7 +57,6 @@ public interface Abilities<T extends Ability> extends List<T>, Serializable {
*
* @param zone The {@link Zone} for which abilities should be retrieved.
* @return All abilities for the given {@link Zone}
*
* @see mage.cards.CardImpl#getSpellAbility()
*/
Abilities<ActivatedAbility> getPlayableAbilities(Zone zone);
@ -68,7 +69,6 @@ public interface Abilities<T extends Ability> extends List<T>, Serializable {
* {@link ActivatedManaAbilityImpl mana abilities}.
* @return All {@link ActivatedManaAbilityImpl mana abilities} for the given
* {@link Zone}.
*
* @see mage.cards.CardImpl#getMana()
* @see mage.players.PlayerImpl#getManaAvailable(mage.game.Game)
* @see mage.players.PlayerImpl#getAvailableManaProducers(mage.game.Game)
@ -85,7 +85,6 @@ public interface Abilities<T extends Ability> extends List<T>, Serializable {
* @param playerId The id of the player to check availability for
* @return All {@link ActivatedManaAbilityImpl mana abilities} for the given
* {@link Zone} that can be used.
*
* @see mage.cards.CardImpl#getMana()
* @see mage.players.PlayerImpl#getManaAvailable(mage.game.Game)
* @see mage.players.PlayerImpl#getAvailableManaProducers(mage.game.Game)
@ -99,22 +98,16 @@ public interface Abilities<T extends Ability> extends List<T>, Serializable {
* @param zone The {@link Zone} to search for {@link StaticAbility}
* @return All {@link StaticAbility static abilities} in the given
* {@link Zone}
*
* @see
* mage.abilities.effects.ContinuousEffects#getLayeredEffects(mage.game.Game)
* @see
* mage.abilities.effects.ContinuousEffects#getApplicableRequirementEffects(mage.game.permanent.Permanent,
* @see mage.abilities.effects.ContinuousEffects#getLayeredEffects(mage.game.Game)
* @see mage.abilities.effects.ContinuousEffects#getApplicableRequirementEffects(mage.game.permanent.Permanent,
* mage.game.Game)
* @see
* mage.abilities.effects.ContinuousEffects#getApplicableRestrictionEffects(mage.game.permanent.Permanent,
* @see mage.abilities.effects.ContinuousEffects#getApplicableRestrictionEffects(mage.game.permanent.Permanent,
* mage.game.Game)
* @see
* mage.abilities.effects.ContinuousEffects#getApplicableReplacementEffects(mage.game.events.GameEvent,
* @see mage.abilities.effects.ContinuousEffects#getApplicableReplacementEffects(mage.game.events.GameEvent,
* mage.game.Game)
* @see mage.abilities.effects.ContinuousEffects#asThough(java.util.UUID,
* mage.constants.AsThoughEffectType, mage.game.Game)
* @see
* mage.abilities.effects.ContinuousEffects#costModification(mage.abilities.Ability,
* @see mage.abilities.effects.ContinuousEffects#costModification(mage.abilities.Ability,
* mage.game.Game)
*/
Abilities<StaticAbility> getStaticAbilities(Zone zone);
@ -133,14 +126,11 @@ public interface Abilities<T extends Ability> extends List<T>, Serializable {
* @param zone The {@link Zone} to search for
* {@link TriggeredAbility triggered abilities}
* @return All found {@link TriggeredAbility triggered abilities}.
*
* @see mage.cards.CardImpl#checkTriggers(mage.constants.Zone,
* mage.game.events.GameEvent, mage.game.Game)
* @see
* mage.game.permanent.PermanentImpl#checkTriggers(mage.game.events.GameEvent,
* @see mage.game.permanent.PermanentImpl#checkTriggers(mage.game.events.GameEvent,
* mage.game.Game)
* @see
* mage.game.permanent.PermanentCard#checkPermanentOnlyTriggers(mage.game.events.ZoneChangeEvent,
* @see mage.game.permanent.PermanentCard#checkPermanentOnlyTriggers(mage.game.events.ZoneChangeEvent,
* mage.game.Game)
*/
Abilities<TriggeredAbility> getTriggeredAbilities(Zone zone);
@ -149,16 +139,17 @@ public interface Abilities<T extends Ability> extends List<T>, Serializable {
* Retrieves all {@link ProtectionAbility protection abilities}.
*
* @return All found {@link ProtectionAbility protection abilities}.
*
* @see mage.game.permanent.PermanentImpl#hasProtectionFrom(mage.MageObject)
* @see mage.players.PlayerImpl#hasProtectionFrom(mage.MageObject)
* @see mage.players.PlayerImpl#canDamage(mage.MageObject)
*/
Abilities<ProtectionAbility> getProtectionAbilities();
Abilities<Ability> getAllAbilities();
/**
* TODO Method is unused, keep it around?
*
* <p>
* The only implementation seems to want to use this for totally a set of
* abilities by some arbitrary numeral value. Possibly a good method to be
* used by the AI's?
@ -172,7 +163,6 @@ public interface Abilities<T extends Ability> extends List<T>, Serializable {
* Sets the controller of this set of abilities.
*
* @param controllerId
*
* @see mage.cards.CardImpl#setControllerId(java.util.UUID)
* @see mage.cards.CardImpl#setOwnerId(java.util.UUID)
* @see mage.game.permanent.PermanentImpl#changeControllerId(java.util.UUID,
@ -185,7 +175,6 @@ public interface Abilities<T extends Ability> extends List<T>, Serializable {
* Sets the source of this set of abilities.
*
* @param sourceId
*
* @see mage.cards.CardImpl#assignNewId()
*/
void setSourceId(UUID sourceId);
@ -212,7 +201,7 @@ public interface Abilities<T extends Ability> extends List<T>, Serializable {
/**
* TODO Method is unused, keep it around?
*
* <p>
* Gets the ability represented by the given abilityId.
*
* @param abilityId
@ -224,7 +213,7 @@ public interface Abilities<T extends Ability> extends List<T>, Serializable {
* TODO The usage of this method seems redundant to that of
* {@link #containsKey(java.util.UUID)} minus the fact that it searches for
* exact instances instead of id's of singleton Abilities.
*
* <p>
* Searches for the exact instance of the passed in ability.
*
* @param ability
@ -261,6 +250,7 @@ public interface Abilities<T extends Ability> extends List<T>, Serializable {
/**
* Returns true if one or more of the abilities are activated mana abilities with the pollDependant flag set to true.
*
* @return
*/
boolean hasPoolDependantAbilities();
@ -275,12 +265,15 @@ public interface Abilities<T extends Ability> extends List<T>, Serializable {
String getValue();
@Deprecated // use permanent.removeAbility instead
@Deprecated
// use permanent.removeAbility instead
boolean remove(Object o);
@Deprecated // use permanent.removeAbility instead
@Deprecated
// use permanent.removeAbility instead
boolean removeAll(Collection<?> c);
@Deprecated // use permanent.removeAbility instead
@Deprecated
// use permanent.removeAbility instead
boolean removeIf(Predicate<? super T> filter);
}

View file

@ -1,5 +1,6 @@
package mage.abilities;
import mage.MageObject;
import mage.abilities.common.ZoneChangeTriggeredAbility;
import mage.abilities.costs.Cost;
import mage.abilities.keyword.ProtectionAbility;
@ -7,6 +8,7 @@ import mage.abilities.mana.ActivatedManaAbilityImpl;
import mage.abilities.mana.ManaAbility;
import mage.constants.Zone;
import mage.game.Game;
import mage.util.CardUtil;
import mage.util.ThreadLocalStringBuilder;
import org.apache.log4j.Logger;
@ -44,12 +46,12 @@ public class AbilitiesImpl<T extends Ability> extends ArrayList<T> implements Ab
}
@Override
public List<String> getRules(String source) {
return getRules(source, true);
public List<String> getRules() {
return getRules(true);
}
@Override
public List<String> getRules(String source, boolean capitalize) {
public List<String> getRules(boolean capitalize) {
List<String> rules = new ArrayList<>();
for (T ability : this) {
@ -102,6 +104,12 @@ public class AbilitiesImpl<T extends Ability> extends ArrayList<T> implements Ab
return rules;
}
@Override
public List<String> getRules(Game game, MageObject object) {
Abilities<Ability> sourceAbilities = this.getAllAbilities();
return CardUtil.getCardRulesWithAdditionalInfo(game, object, sourceAbilities, sourceAbilities);
}
/**
* Activated Ability in the engine are broader than in the rules.
* Notably SpellAbility & PlayLandAbility are ActivatedAbility,
@ -193,6 +201,11 @@ public class AbilitiesImpl<T extends Ability> extends ArrayList<T> implements Ab
.collect(Collectors.toCollection(AbilitiesImpl::new));
}
@Override
public Abilities<Ability> getAllAbilities() {
return stream().collect(Collectors.toCollection(AbilitiesImpl::new));
}
@Override
public void setControllerId(UUID controllerId) {
for (Ability ability : this) {

View file

@ -31,7 +31,7 @@ public class CompoundAbility extends AbilitiesImpl<Ability> {
}
StringBuilder sb = new StringBuilder();
List<String> rules = super.getRules(null, false);
List<String> rules = super.getRules(false);
for (int index = 0; index < rules.size(); index++) {
if (index > 0) {
if (index < rules.size() - 1) {

View file

@ -309,7 +309,7 @@ public class TriggeredAbilities extends LinkedHashMap<String, TriggeredAbility>
public void removeAbilitiesOfNonExistingSources(Game game) {
// e.g. Token that had triggered abilities
entrySet().removeIf(entry -> game.getObject(entry.getValue().getSourceId()) == null
&& game.getState().getInherentEmblems().stream().noneMatch(emblem -> emblem.getId().equals(entry.getValue().getSourceId()))
&& game.getState().getHelperEmblems().stream().noneMatch(emblem -> emblem.getId().equals(entry.getValue().getSourceId()))
&& game.getState().getDesignations().stream().noneMatch(designation -> designation.getId().equals(entry.getValue().getSourceId())));
}

View file

@ -1338,7 +1338,7 @@ public class ContinuousEffects implements Serializable {
effectsMap.put(key, object.getIdName() + ": " + ability.getRule(object.getName()));
objectsMap.put(key, object);
} else {
effectsMap.put(key, entry.getKey().getText(null));
effectsMap.put(key, entry.getKey().getText(ability.getModes().getMode()));
objectsMap.put(key, null);
}
}

View file

@ -46,7 +46,7 @@ public class GetEmblemEffect extends OneShotEffect {
public String getText() {
StringBuilder sb = new StringBuilder();
sb.append("you get an emblem with \"");
List<String> rules = emblem.getAbilities().getRules(null);
List<String> rules = emblem.getAbilities().getRules();
sb.append(rules.get(0));
if (rules.size() == 2) {
sb.append("\" and \"");

View file

@ -9,6 +9,7 @@ import mage.game.Game;
import mage.game.command.Emblem;
import mage.players.Player;
import java.util.List;
import java.util.stream.Collectors;
/**
@ -54,6 +55,6 @@ public class GetEmblemTargetPlayerEffect extends OneShotEffect {
return staticText;
}
return getTargetPointer().describeTargets(mode.getTargets(), "that player")
+ " gets an emblem with \"" + String.join("; ", emblem.getAbilities().getRules(null)) + "\"";
+ " gets an emblem with \"" + String.join("; ", emblem.getAbilities().getRules()) + "\"";
}
}

View file

@ -192,7 +192,7 @@ public class LevelerCardBuilder {
sb.append(power);
sb.append('/');
sb.append(toughness);
List<String> abilityText = abilities.getRules("{this}");
List<String> abilityText = abilities.getRules();
if (!abilityText.isEmpty()) {
sb.append("<br>");
sb.append(abilityText.stream().collect(Collectors.joining("<br>")));

View file

@ -134,7 +134,7 @@ public abstract class AdventureCard extends CardImpl {
public List<String> getSharedRules(Game game) {
// rules without spellcard
Abilities<Ability> sourceAbilities = this.getSharedAbilities(game);
return CardUtil.getCardRulesWithAdditionalInfo(game, this.getId(), this.getName(), sourceAbilities, sourceAbilities);
return CardUtil.getCardRulesWithAdditionalInfo(game, this, sourceAbilities, sourceAbilities);
}
@Override

View file

@ -230,13 +230,13 @@ public abstract class CardImpl extends MageObjectImpl implements Card {
@Override
public List<String> getRules() {
Abilities<Ability> sourceAbilities = this.getAbilities();
return CardUtil.getCardRulesWithAdditionalInfo(this.getId(), this.getName(), sourceAbilities, sourceAbilities);
return CardUtil.getCardRulesWithAdditionalInfo(this, sourceAbilities, sourceAbilities);
}
@Override
public List<String> getRules(Game game) {
Abilities<Ability> sourceAbilities = this.getAbilities(game);
return CardUtil.getCardRulesWithAdditionalInfo(game, this.getId(), this.getName(), sourceAbilities, sourceAbilities);
return CardUtil.getCardRulesWithAdditionalInfo(game, this, sourceAbilities, sourceAbilities);
}
/**

View file

@ -358,8 +358,7 @@ public abstract class ModalDoubleFacedCard extends CardImpl implements CardWithH
// rules must show only main side (another side visible by toggle/transform button in GUI)
// card hints from both sides
return CardUtil.getCardRulesWithAdditionalInfo(
this.getId(),
this.getName(),
this,
this.getInnerAbilities(true, false),
this.getInnerAbilities(true, true)
);
@ -371,8 +370,7 @@ public abstract class ModalDoubleFacedCard extends CardImpl implements CardWithH
// card hints from both sides
return CardUtil.getCardRulesWithAdditionalInfo(
game,
this.getId(),
this.getName(),
this,
this.getInnerAbilities(game, true, false),
this.getInnerAbilities(game, true, true)
);

View file

@ -189,8 +189,7 @@ public abstract class SplitCard extends CardImpl implements CardWithHalves {
public List<String> getRules() {
Abilities<Ability> sourceAbilities = this.getAbilities();
List<String> res = CardUtil.getCardRulesWithAdditionalInfo(
this.getId(),
this.getName(),
this,
sourceAbilities,
sourceAbilities
);
@ -205,8 +204,7 @@ public abstract class SplitCard extends CardImpl implements CardWithHalves {
Abilities<Ability> sourceAbilities = this.getAbilities(game);
List<String> res = CardUtil.getCardRulesWithAdditionalInfo(
game,
this.getId(),
this.getName(),
this,
sourceAbilities,
sourceAbilities
);

View file

@ -119,8 +119,7 @@ public class MockSplitCard extends SplitCard implements MockableCard {
// so a MockSplitCard must ignore it (duplicate fix)
Abilities<Ability> sourceAbilities = this.getAbilities();
return CardUtil.getCardRulesWithAdditionalInfo(
this.getId(),
this.getName(),
this,
sourceAbilities,
sourceAbilities
);

View file

@ -33,6 +33,7 @@ public enum TokenRepository {
public static final String XMAGE_IMAGE_NAME_NIGHT = "Night";
public static final String XMAGE_IMAGE_NAME_THE_MONARCH = "The Monarch";
public static final String XMAGE_IMAGE_NAME_RADIATION = "Radiation";
public static final String XMAGE_IMAGE_NAME_HELPER_EMBLEM = "Helper Emblem";
private static final Logger logger = Logger.getLogger(TokenRepository.class);
@ -305,6 +306,10 @@ public enum TokenRepository {
// Radiation (for trigger)
res.add(createXmageToken(XMAGE_IMAGE_NAME_RADIATION, 1, "https://api.scryfall.com/cards/tpip/22/en?format=image"));
// Helper emblem (for global card hints)
// use backface for it
res.add(createXmageToken(XMAGE_IMAGE_NAME_HELPER_EMBLEM, 1, "https://upload.wikimedia.org/wikipedia/en/a/aa/Magic_the_gathering-card_back.jpg"));
return res;
}

View file

@ -48,6 +48,7 @@ import mage.game.command.dungeons.UndercityDungeon;
import mage.game.command.emblems.EmblemOfCard;
import mage.game.command.emblems.RadiationEmblem;
import mage.game.command.emblems.TheRingEmblem;
import mage.game.command.emblems.XmageHelperEmblem;
import mage.game.events.*;
import mage.game.events.TableEvent.EventType;
import mage.game.mulligan.Mulligan;
@ -444,7 +445,7 @@ public abstract class GameImpl implements Game {
return designation;
}
}
for (Emblem emblem : state.getInherentEmblems()) {
for (Emblem emblem : state.getHelperEmblems()) {
if (emblem.getId().equals(objectId)) {
return emblem;
}
@ -1400,13 +1401,7 @@ public abstract class GameImpl implements Game {
}
}
// Rad counter mechanic for every player
for (UUID playerId : state.getPlayerList(startingPlayerId)) {
// This is not a real emblem. Just a fake source for the
// inherent trigger ability related to Rad counters
// Faking a source just to display something on the stack ability.
state.addInherentEmblem(new RadiationEmblem(), playerId);
}
initGameDefaultHelperEmblems();
}
public void initGameDefaultWatchers() {
@ -1447,6 +1442,22 @@ public abstract class GameImpl implements Game {
getState().addWatcher(bloodthirstWatcher);
}
public void initGameDefaultHelperEmblems() {
// Rad Counter's trigger source
for (UUID playerId : state.getPlayerList(startingPlayerId)) {
// This is not a real emblem. Just a fake source for the
// inherent trigger ability related to Rad counters
// Faking a source just to display something on the stack ability.
state.addHelperEmblem(new RadiationEmblem(), playerId);
}
// global card hints for better UX
for (UUID playerId : state.getPlayerList(startingPlayerId)) {
state.addHelperEmblem(new XmageHelperEmblem().withCardHint("storm counter", StormAbility.getHint()), playerId);
}
}
protected void sendStartMessage(Player choosingPlayer, Player startingPlayer) {
StringBuilder message = new StringBuilder();
if (choosingPlayer != null) {

View file

@ -85,7 +85,7 @@ public class GameState implements Serializable, Copyable<GameState> {
private boolean isPlaneChase;
private List<String> seenPlanes = new ArrayList<>();
private List<Designation> designations = new ArrayList<>();
private List<Emblem> inherentEmblems = new ArrayList<>();
private List<Emblem> helperEmblems = new ArrayList<>(); // fake emblems for inner usage like better UX
private Exile exile;
private Battlefield battlefield;
private int turnNum = 1;
@ -158,7 +158,7 @@ public class GameState implements Serializable, Copyable<GameState> {
this.isPlaneChase = state.isPlaneChase;
this.seenPlanes.addAll(state.seenPlanes);
this.designations.addAll(state.designations);
this.inherentEmblems = CardUtil.deepCopyObject(state.inherentEmblems);
this.helperEmblems = CardUtil.deepCopyObject(state.helperEmblems);
this.exile = state.exile.copy();
this.battlefield = state.battlefield.copy();
this.turnNum = state.turnNum;
@ -206,7 +206,7 @@ public class GameState implements Serializable, Copyable<GameState> {
exile.clear();
command.clear();
designations.clear();
inherentEmblems.clear();
helperEmblems.clear();
seenPlanes.clear();
isPlaneChase = false;
revealed.clear();
@ -248,7 +248,7 @@ public class GameState implements Serializable, Copyable<GameState> {
this.isPlaneChase = state.isPlaneChase;
this.seenPlanes = state.seenPlanes;
this.designations = state.designations;
this.inherentEmblems = state.inherentEmblems;
this.helperEmblems = state.helperEmblems;
this.exile = state.exile;
this.battlefield = state.battlefield;
this.turnNum = state.turnNum;
@ -519,12 +519,12 @@ public class GameState implements Serializable, Copyable<GameState> {
return designations;
}
public List<Emblem> getInherentEmblems() {
return inherentEmblems;
public List<Emblem> getHelperEmblems() {
return helperEmblems;
}
public Plane getCurrentPlane() {
if (command != null && command.size() > 0) {
if (command != null && !command.isEmpty()) {
for (CommandObject cobject : command) {
if (cobject instanceof Plane) {
return (Plane) cobject;
@ -1184,16 +1184,13 @@ public class GameState implements Serializable, Copyable<GameState> {
}
/**
* Inherent triggers (Rad counters) in the rules have no source.
* However to fit better with the engine, we make a fake emblem source,
* which is not displayed in any game zone. That allows the trigger to
* have a source, which helps with a bunch of situation like hosting,
* rather than having a trigger.
* Add fake/helper emblems for hidden source of inherent triggers like Rad counters,
* additional card hints like storm counter, etc. See GameImpl.initGameDefaultHelperEmblems
* <p>
* Should not be used except in very specific situations
* It allows game and GUI find and show full card object in stack's triggers, logs and hints popup.
*/
public void addInherentEmblem(Emblem emblem, UUID controllerId) {
getInherentEmblems().add(emblem);
public void addHelperEmblem(Emblem emblem, UUID controllerId) {
helperEmblems.add(emblem);
emblem.setControllerId(controllerId);
for (Ability ability : emblem.getInitAbilities()) {
ability.setControllerId(controllerId);

View file

@ -19,6 +19,7 @@ import mage.constants.SubType;
import mage.constants.SuperType;
import mage.game.Game;
import mage.game.events.ZoneChangeEvent;
import mage.util.CardUtil;
import mage.util.SubTypes;
import java.util.Collections;
@ -37,7 +38,7 @@ public abstract class Emblem extends CommandObjectImpl {
private boolean copy;
private MageObject copyFrom; // copied card INFO (used to call original adjusters)
protected FrameStyle frameStyle;
private Abilities<Ability> abilites = new AbilitiesImpl<>();
private Abilities<Ability> abilities = new AbilitiesImpl<>();
public Emblem(String name) {
super(name);
@ -50,7 +51,7 @@ public abstract class Emblem extends CommandObjectImpl {
this.sourceObject = emblem.sourceObject;
this.copy = emblem.copy;
this.copyFrom = emblem.copyFrom;
this.abilites = emblem.abilites.copy();
this.abilities = emblem.abilities.copy();
}
@Override
@ -73,7 +74,7 @@ public abstract class Emblem extends CommandObjectImpl {
this.setImageFileName(""); // use default
this.setImageNumber(foundInfo.getImageNumber());
} else {
// how-to fix: add emblem to the tokens-database
// how-to fix: add emblem to tokens-database.txt
throw new IllegalArgumentException("Wrong code usage: can't find token info for the emblem: " + this.getClass().getName());
}
}
@ -98,7 +99,7 @@ public abstract class Emblem extends CommandObjectImpl {
public void setControllerId(UUID controllerId) {
this.controllerId = controllerId;
this.abilites.setControllerId(controllerId);
this.abilities.setControllerId(controllerId);
}
@Override
@ -152,7 +153,7 @@ public abstract class Emblem extends CommandObjectImpl {
@Override
public Abilities<Ability> getAbilities() {
return abilites;
return abilities;
}
@Override
@ -260,7 +261,7 @@ public abstract class Emblem extends CommandObjectImpl {
}
public void discardEffects() {
for (Ability ability : abilites) {
for (Ability ability : abilities) {
for (Effect effect : ability.getEffects()) {
if (effect instanceof ContinuousEffect) {
((ContinuousEffect) effect).discard();

View file

@ -45,7 +45,7 @@ public class RadiationEmblem extends Emblem {
this.setImageFileName(""); // use default
this.setImageNumber(foundInfo.getImageNumber());
} else {
// how-to fix: add emblem to the tokens-database
// how-to fix: add emblem to the tokens-database TokenRepository->loadXmageTokens
throw new IllegalArgumentException("Wrong code usage: can't find xmage token info for: " + TokenRepository.XMAGE_IMAGE_NAME_RADIATION);
}
}

View file

@ -0,0 +1,52 @@
package mage.game.command.emblems;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.common.InfoEffect;
import mage.abilities.hint.Hint;
import mage.cards.FrameStyle;
import mage.cards.repository.TokenInfo;
import mage.cards.repository.TokenRepository;
import mage.constants.Zone;
import mage.game.command.Emblem;
/**
* GUI: inner xmage emblem to show additional info for players like global hints
*
* @author JayDi85
*/
public class XmageHelperEmblem extends Emblem {
public XmageHelperEmblem() {
super("Helper Emblem");
this.frameStyle = FrameStyle.M15_NORMAL;
TokenInfo foundInfo = TokenRepository.instance.findPreferredTokenInfoForXmage(TokenRepository.XMAGE_IMAGE_NAME_HELPER_EMBLEM, null);
if (foundInfo != null) {
this.setExpansionSetCode(foundInfo.getSetCode());
this.setUsesVariousArt(false);
this.setCardNumber("");
this.setImageFileName(""); // use default
this.setImageNumber(foundInfo.getImageNumber());
} else {
// how-to fix: add emblem to the tokens-database TokenRepository->loadXmageTokens
throw new IllegalArgumentException("Wrong code usage: can't find xmage token info for: " + TokenRepository.XMAGE_IMAGE_NAME_HELPER_EMBLEM);
}
}
private XmageHelperEmblem(final XmageHelperEmblem card) {
super(card);
}
@Override
public XmageHelperEmblem copy() {
return new XmageHelperEmblem(this);
}
public XmageHelperEmblem withCardHint(String name, Hint hint) {
this.getAbilities().add(new SimpleStaticAbility(
Zone.ALL,
new InfoEffect(name)).addHint(hint)
);
return this;
}
}

View file

@ -1207,9 +1207,9 @@ public final class CardUtil {
}
}
public static List<String> getCardRulesWithAdditionalInfo(UUID cardId, String cardName,
public static List<String> getCardRulesWithAdditionalInfo(MageObject object,
Abilities<Ability> rulesSource, Abilities<Ability> hintAbilities) {
return getCardRulesWithAdditionalInfo(null, cardId, cardName, rulesSource, hintAbilities);
return getCardRulesWithAdditionalInfo(null, object, rulesSource, hintAbilities);
}
/**
@ -1218,10 +1218,10 @@ public final class CardUtil {
* @param rulesSource abilities list to show as rules
* @param hintsSource abilities list to show as card hints only (you can add additional hints here; example: from second or transformed side)
*/
public static List<String> getCardRulesWithAdditionalInfo(Game game, UUID cardId, String cardName,
public static List<String> getCardRulesWithAdditionalInfo(Game game, MageObject object,
Abilities<Ability> rulesSource, Abilities<Ability> hintsSource) {
try {
List<String> rules = rulesSource.getRules(cardName);
List<String> rules = rulesSource.getRules();
if (game == null || game.getPhase() == null) {
// dynamic hints for started game only
@ -1229,7 +1229,10 @@ public final class CardUtil {
}
// additional effect's info from card.addInfo methods
rules.addAll(game.getState().getCardState(cardId).getInfo().values());
CardState cardState = game.getState().getCardState(object.getId());
if (cardState != null) {
rules.addAll(cardState.getInfo().values());
}
// ability hints
List<String> abilityHints = new ArrayList<>();
@ -1254,7 +1257,7 @@ public final class CardUtil {
return rules;
} catch (Exception e) {
logger.error("Exception in rules generation for card: " + cardName, e);
logger.error("Exception in rules generation for object: " + object.getName(), e);
}
return RULES_ERROR_INFO;
}

View file

@ -658,7 +658,7 @@ public final class ManaUtil {
return getColorIdentity(
token.getColor(),
String.join("", token.getManaCostSymbols()),
token.getAbilities().getRules(token.getName()),
token.getAbilities().getRules(),
token.getBackFace() == null ? null : token.getBackFace().getCopySourceCard()
);
}

View file

@ -2,8 +2,11 @@ package mage.util;
import java.io.Serializable;
// Author: alexander-novo
// A helper class for facilitating the multi-choose dialog
/**
* A helper class for facilitating the multi-choose dialog
*
* @author alexander-novo
*/
public class MultiAmountMessage implements Serializable {
public String message;
public int min;
@ -20,4 +23,9 @@ public class MultiAmountMessage implements Serializable {
this.max = max;
this.defaultValue = defaultValue;
}
@Override
public String toString() {
return String.format("%s - from %d to %d - default %d", message, min, max, defaultValue);
}
}