Rework Ring-bearer implementation. Add GUI + gamelogs. (#10596)

* Fix Ring-bearer choosing & add some GUI + logs

* use a ring svg in a separate gold panel

* use a fontawesome svg

* add a couple null checks, group icon with commander

* rework rinbearer logic according to review

* fix typo in game log

* small fixes
This commit is contained in:
Susucre 2023-07-13 01:40:09 +02:00 committed by GitHub
parent 4065e2e935
commit 14235b6320
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 177 additions and 86 deletions

View file

@ -19,7 +19,7 @@ public enum SourceIsRingBearerCondition implements Condition {
.ofNullable(source.getSourcePermanentIfItStillExists(game))
.filter(Objects::nonNull)
.filter(permanent -> permanent.isControlledBy(source.getControllerId()))
.map(permanent -> permanent.isRingBearer(game))
.map(permanent -> permanent.isRingBearer())
.orElse(false);
}

View file

@ -10,5 +10,5 @@ public enum CardIconCategory {
ABILITY, // example: flying (on left side)
PLAYABLE_COUNT, // on bottom left corner
SYSTEM, // example: too many icons combines in the one icon (on left side)
COMMANDER // example: commander (on top center icon)
COMMANDER, // example: commander (on top center icon)
}

View file

@ -16,7 +16,8 @@ public enum CardIconColor {
DEFAULT(),
YELLOW(new Color(231, 203, 18), new Color(76, 76, 76), new Color(0, 0, 0)),
RED(new Color(255, 31, 75), new Color(76, 76, 76), new Color(229, 228, 228));
RED(new Color(255, 31, 75), new Color(76, 76, 76), new Color(229, 228, 228)),
GOLD(new Color(186, 105, 19), new Color(76, 76, 76), new Color(229, 228, 228));
private final Color fillColor;
private final Color strokeColor;

View file

@ -14,6 +14,7 @@ public class CardIconImpl implements CardIcon, Serializable {
// Utility Icons
public static final CardIconImpl FACE_DOWN = new CardIconImpl(CardIconType.OTHER_FACEDOWN, "Card is face down");
public static final CardIconImpl COMMANDER = new CardIconImpl(CardIconType.COMMANDER, "Card is commander");
public static final CardIconImpl RINGBEARER = new CardIconImpl(CardIconType.RINGBEARER, "Ring-bearer");
// Ability Icons
public static final CardIconImpl ABILITY_CREW = new CardIconImpl(CardIconType.ABILITY_CREW,

View file

@ -35,6 +35,7 @@ public enum CardIconType {
OTHER_FACEDOWN("prepared/reply-fill.svg", CardIconCategory.ABILITY, 100),
OTHER_COST_X("prepared/square-fill.svg", CardIconCategory.ABILITY, 100),
//
RINGBEARER("prepared/ring.svg", CardIconCategory.COMMANDER, 100),
COMMANDER("prepared/crown.svg", CardIconCategory.COMMANDER, 100), // TODO: fix big size, see CardIconsPanel
//
SYSTEM_COMBINED("prepared/square-fill.svg", CardIconCategory.SYSTEM, 1000), // inner usage, must use last order

View file

@ -11,6 +11,7 @@ import mage.filter.predicate.Predicates;
import mage.filter.predicate.mageobject.*;
import mage.filter.predicate.other.AnotherTargetPredicate;
import mage.filter.predicate.permanent.AttachedOrShareCreatureTypePredicate;
import mage.filter.predicate.permanent.RingBearerPredicate;
import mage.filter.predicate.permanent.TappedPredicate;
import mage.filter.predicate.permanent.TokenPredicate;
@ -664,6 +665,13 @@ public final class StaticFilters {
FILTER_CONTROLLED_PERMANENT_NON_LAND.setLockedFilter(true);
}
public static final FilterControlledPermanent FILTER_CONTROLLED_RINGBEARER = new FilterControlledPermanent("the controlled Ring-bearer");
static {
FILTER_CONTROLLED_RINGBEARER.add(RingBearerPredicate.instance);
FILTER_CONTROLLED_RINGBEARER.setLockedFilter(true);
}
public static final FilterLandPermanent FILTER_LAND = new FilterLandPermanent();
static {

View file

@ -12,6 +12,6 @@ public enum RingBearerPredicate implements Predicate<Permanent> {
@Override
public boolean apply(Permanent input, Game game) {
return input.isRingBearer(game);
return input.isRingBearer();
}
}

View file

@ -589,7 +589,10 @@ public abstract class GameImpl implements Game {
}
player.chooseRingBearer(this);
getOrCreateTheRing(playerId).addNextAbility(this);
fireEvent(GameEvent.getEvent(GameEvent.EventType.TEMPTED_BY_RING, player.getRingBearerId(), null, playerId));
Permanent ringbearer = player.getRingBearer(this);
UUID ringbearerId = ringbearer == null ? null : ringbearer.getId();
fireEvent(GameEvent.getEvent(GameEvent.EventType.TEMPTED_BY_RING, ringbearerId, null, playerId));
}
@Override

View file

@ -20,6 +20,7 @@ import mage.game.Game;
import mage.game.command.Emblem;
import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.target.targetpointer.FixedTarget;
import mage.watchers.common.TemptedByTheRingWatcher;
@ -53,15 +54,16 @@ public final class TheRingEmblem extends Emblem {
}
public void addNextAbility(Game game) {
String logText;
Ability ability;
switch (TemptedByTheRingWatcher.getCount(this.getControllerId(), game)) {
case 0:
// Your Ring-bearer is legendary and can't be blocked by creatures with greater power.
logText = "Your Ring-bearer is legendary and can't be blocked by creatures with greater power.";
ability = new SimpleStaticAbility(Zone.COMMAND, new TheRingEmblemLegendaryEffect());
ability.addEffect(new TheRingEmblemEvasionEffect());
break;
case 1:
// Whenever your Ring-bearer attacks, draw a card, then discard a card.
logText = "Whenever your Ring-bearer attacks, draw a card, then discard a card.";
ability = new AttacksCreatureYouControlTriggeredAbility(
Zone.COMMAND,
new DrawDiscardControllerEffect(1, 1),
@ -69,11 +71,11 @@ public final class TheRingEmblem extends Emblem {
).setTriggerPhrase("Whenever your Ring-bearer attacks, ");
break;
case 2:
// Whenever your Ring-bearer becomes blocked by a creature, that creature's controller sacrifices it at end of combat.
logText ="Whenever your Ring-bearer becomes blocked by a creature, that creature's controller sacrifices it at end of combat.";
ability = new TheRingEmblemTriggeredAbility();
break;
case 3:
// Whenever your Ring-bearer deals combat damage to a player, each opponent loses 3 life.
logText = "Whenever your Ring-bearer deals combat damage to a player, each opponent loses 3 life.";
ability = new DealsDamageToAPlayerAllTriggeredAbility(
Zone.COMMAND, new LoseLifeOpponentsEffect(3), filter, false,
SetTargetPointer.NONE, true, false
@ -82,10 +84,20 @@ public final class TheRingEmblem extends Emblem {
default:
return;
}
UUID controllerId = this.getControllerId();
this.getAbilities().add(ability);
ability.setSourceId(this.getId());
ability.setControllerId(this.getControllerId());
ability.setControllerId(controllerId);
game.getState().addAbility(ability, this);
String name = "";
if(controllerId != null){
Player player = game.getPlayer(controllerId);
if(player != null){
name = player.getLogName();
}
}
game.informPlayers(name + " gains a new Ring ability: \"" + logText + "\"");
}
}
@ -94,7 +106,7 @@ enum TheRingEmblemPredicate implements Predicate<Permanent> {
@Override
public boolean apply(Permanent input, Game game) {
return input.isRingBearer(game);
return input.isRingBearer();
}
}
@ -148,7 +160,7 @@ class TheRingEmblemEvasionEffect extends RestrictionEffect {
@Override
public boolean applies(Permanent permanent, Ability source, Game game) {
return permanent.isControlledBy(source.getControllerId())
&& permanent.isRingBearer(game);
&& permanent.isRingBearer();
}
@Override
@ -184,7 +196,7 @@ class TheRingEmblemTriggeredAbility extends TriggeredAbilityImpl {
if (attacker == null
|| blocker == null
|| attacker.isControlledBy(getControllerId())
|| !attacker.isRingBearer(game)) {
|| !attacker.isRingBearer()) {
return false;
}
this.getEffects().setTargetPointer(new FixedTarget(blocker, game));

View file

@ -420,7 +420,9 @@ public interface Permanent extends Card, Controllable {
boolean isManifested();
boolean isRingBearer(Game game);
boolean isRingBearer();
void setRingBearer(Game game, boolean value);
@Override
Permanent copy();

View file

@ -72,6 +72,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
protected boolean renowned;
protected boolean manifested = false;
protected boolean morphed = false;
protected boolean ringBearerFlag = false;
protected int classLevel = 1;
protected final Set<UUID> goadingPlayers = new HashSet<>();
protected UUID originalControllerId;
@ -165,6 +166,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
this.transformed = permanent.transformed;
this.monstrous = permanent.monstrous;
this.renowned = permanent.renowned;
this.ringBearerFlag = permanent.ringBearerFlag;
this.classLevel = permanent.classLevel;
this.goadingPlayers.addAll(permanent.goadingPlayers);
this.pairedPermanent = permanent.pairedPermanent;
@ -801,11 +803,24 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
return true;
}
// Losing control of a ring bearer clear its status.
public void removeUncontrolledRingBearer(Game game){
if(isRingBearer()) {
UUID controllerId = beforeResetControllerId;
Player controller = controllerId == null ? null : game.getPlayer(controllerId);
String controllerName = controller == null ? "" : controller.getLogName();
game.informPlayers(controllerName + " has lost control of " + getLogName() + ". It is no longer a Ring-bearer.");
this.setRingBearer(game, false);
}
}
@Override
public boolean checkControlChanged(Game game) {
if (!controllerId.equals(beforeResetControllerId)) {
this.removeFromCombat(game);
this.controlledFromStartOfControllerTurn = false;
this.removeUncontrolledRingBearer(game);
this.getAbilities(game).setControllerId(controllerId);
game.getContinuousEffects().setController(objectId, controllerId);
@ -1656,6 +1671,41 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
this.renowned = value;
}
// Used as key for the ring bearer info.
private static final String ringbearerInfoKey = "IS_RINGBEARER";
@Override
public void setRingBearer(Game game, boolean value) {
if(value == this.ringBearerFlag){
return;
}
if(value) {
// The player may have another Ringbearer. We need to clear it if so.
//
// 701.52a Certain spells and abilities have the text the Ring tempts you.
// Each time the Ring tempts you, choose a creature you control.
// That creature becomes your Ring-bearer until another creature
// becomes your Ring-bearer or another player gains control of it.
Player player = game.getPlayer(getControllerId());
String playername = "";
if(player != null){
playername = player.getLogName();
Permanent existingRingbearer = player.getRingBearer(game);
if(existingRingbearer != null && existingRingbearer.getId() != this.getId()){
existingRingbearer.setRingBearer(game, false);
}
}
addInfo(ringbearerInfoKey, CardUtil.addToolTipMarkTags("Is " + playername + "'s Ring-bearer"), game);
}
else {
addInfo(ringbearerInfoKey, null, game);
}
this.ringBearerFlag = value;
}
@Override
public int getClassLevel() {
return classLevel;
@ -1811,10 +1861,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent {
}
@Override
public boolean isRingBearer(Game game) {
Player player = game.getPlayer(getControllerId());
return player != null && this.equals(player.getRingBearer(game));
}
public boolean isRingBearer() { return ringBearerFlag; }
@Override
public boolean fight(Permanent fightTarget, Ability source, Game game) {

View file

@ -1080,8 +1080,6 @@ public interface Player extends MageItem, Copyable<Player> {
*/
FilterMana getPhyrexianColors();
UUID getRingBearerId();
Permanent getRingBearer(Game game);
void chooseRingBearer(Game game);

View file

@ -177,8 +177,6 @@ public abstract class PlayerImpl implements Player, Serializable {
// mana colors the player can handle like Phyrexian mana
protected FilterMana phyrexianColors;
protected UUID ringBearerId = null;
// Used during available mana calculation to give back possible available net mana from triggered mana abilities (No need to copy)
protected final List<List<Mana>> availableTriggeredManaList = new ArrayList<>();
@ -288,7 +286,6 @@ public abstract class PlayerImpl implements Player, Serializable {
}
this.payManaMode = player.payManaMode;
this.phyrexianColors = player.getPhyrexianColors() != null ? player.phyrexianColors.copy() : null;
this.ringBearerId = player.ringBearerId;
for (Designation object : player.designations) {
this.designations.add(object.copy());
}
@ -376,8 +373,6 @@ public abstract class PlayerImpl implements Player, Serializable {
this.phyrexianColors = player.getPhyrexianColors() != null ? player.getPhyrexianColors().copy() : null;
this.ringBearerId = player.getRingBearerId();
this.designations.clear();
for (Designation object : player.getDesignations()) {
this.designations.add(object.copy());
@ -5140,59 +5135,87 @@ public abstract class PlayerImpl implements Player, Serializable {
return this.phyrexianColors;
}
@Override
public UUID getRingBearerId() {
return ringBearerId;
}
@Override
public Permanent getRingBearer(Game game) {
if (ringBearerId == null) {
return null;
}
Permanent bearer = game.getPermanent(ringBearerId);
if (bearer != null && bearer.isControlledBy(getId())) {
return bearer;
}
ringBearerId = null;
return null;
return game.getBattlefield()
.getActivePermanents(
StaticFilters.FILTER_CONTROLLED_RINGBEARER,
getId(),null, game)
.stream()
.filter(p -> p != null)
.findFirst()
.orElse(null);
}
// 701.52a Certain spells and abilities have the text the Ring tempts you. Each time the Ring tempts
// you, choose a creature you control. That creature becomes your Ring-bearer until another
// creature becomes your Ring-bearer or another player gains control of it.
@Override
public void chooseRingBearer(Game game) {
Permanent currentBearer = getRingBearer(game);
int creatureCount = game.getBattlefield().count(
StaticFilters.FILTER_CONTROLLED_CREATURE, getId(), null, game
);
boolean mustChoose;
if (currentBearer == null) {
if (creatureCount > 0) {
mustChoose = true;
} else {
return;
}
} else if (currentBearer.isCreature(game)) {
if (creatureCount > 1) {
mustChoose = false;
} else {
return;
}
} else if (creatureCount > 0) {
mustChoose = false;
UUID currentBearerId = currentBearer == null ? null : currentBearer.getId();
List<UUID> ids = game.getBattlefield()
.getActivePermanents(StaticFilters.FILTER_CONTROLLED_CREATURE, getId(), null, game)
.stream()
.filter(p -> p != null)
.map(p -> p.getId())
.collect(Collectors.toList());
if(ids.isEmpty()) {
game.informPlayers(getLogName() + " has no creature to be Ring-bearer.");
return;
}
// There should always be a creature at the end.
UUID newBearerId;
if(ids.size() == 1){
// Only one creature, it will be the Ring-bearer.
// The player does not have to make any choice.
newBearerId = ids.get(0);
} else {
return;
// Multiple possible Ring-bearer.
// Asking first if the player wants to change Ring-bearer.
boolean mustChoose = currentBearer == null || !(currentBearer.isCreature(game));
boolean choosing = mustChoose;
if (!mustChoose) {
choosing = chooseUse(Outcome.Neutral, "Choose a new Ring-bearer?", null, game);
}
if (choosing) {
TargetPermanent target = new TargetControlledCreaturePermanent();
target.setNotTarget(true);
target.withChooseHint("to be your Ring-bearer");
choose(Outcome.Neutral, target, null, game);
newBearerId = target.getFirstTarget();
}
else {
newBearerId = currentBearerId;
}
}
if (!mustChoose && !chooseUse(Outcome.Neutral, "Choose a new Ring-bearer?", null, game)) {
return;
}
TargetPermanent target = new TargetControlledCreaturePermanent();
target.setNotTarget(true);
target.withChooseHint("to be your Ring-bearer");
choose(Outcome.Neutral, target, null, game);
UUID newBearerId = target.getFirstTarget();
if (game.getPermanent(newBearerId) != null) {
game.fireEvent(GameEvent.getEvent(GameEvent.EventType.RING_BEARER_CHOSEN, newBearerId, null, getId()));
this.ringBearerId = newBearerId;
if(currentBearerId != null && currentBearerId == newBearerId) {
// Oracle Ruling for Call of the Ring
//
// If the creature you choose as your Ring-bearer was already your Ring-bearer,
// that still counts as choosing that creature as your Ring-bearer for the purpose
// f abilities that trigger "whenever you choose a creature as your Ring-bearer"
// or abilities that care about which creature was chosen as your Ring-bearer.
// (2023-06-16)
game.informPlayers(getLogName() + " did not choose a new Ring-bearer. " +
"It is still " + (currentBearer == null ? "" : currentBearer.getLogName()) + ".");
game.fireEvent(GameEvent.getEvent(GameEvent.EventType.RING_BEARER_CHOSEN, currentBearerId, null, getId()));
} else {
Permanent ringBearer = game.getPermanent(newBearerId);
if(ringBearer != null){
// The setRingBearer method is taking care of removing
// the status from the current ring bearer, if existing.
ringBearer.setRingBearer(game, true);
game.informPlayers(getLogName() + " has chosen " + ringBearer.getLogName() + " as Ring-bearer.");
game.fireEvent(GameEvent.getEvent(GameEvent.EventType.RING_BEARER_CHOSEN, newBearerId, null, getId()));
}
}
}