[CLB] Implemented White Plume Adventurer

This commit is contained in:
Evan Kranzler 2022-05-17 20:18:18 -04:00
parent 4777466b50
commit 86dad5e54f
13 changed files with 524 additions and 9 deletions

View file

@ -54,6 +54,7 @@ public class PlayerView implements Serializable {
private final boolean passedAllTurns; // F9
private final boolean passedUntilEndStepBeforeMyTurn; // F11
private final boolean monarch;
private final boolean initiative;
private final List<String> designationNames = new ArrayList<>();
public PlayerView(Player player, GameState state, Game game, UUID createdForPlayerId, UUID watcherUserId) {
@ -153,6 +154,7 @@ public class PlayerView implements Serializable {
this.passedUntilStackResolved = player.getPassedUntilStackResolved();
this.passedUntilEndStepBeforeMyTurn = player.getPassedUntilEndStepBeforeMyTurn();
this.monarch = player.getId().equals(game.getMonarchId());
this.initiative = player.getId().equals(game.getInitiativeId());
for (Designation designation : player.getDesignations()) {
this.designationNames.add(designation.getName());
}
@ -309,6 +311,10 @@ public class PlayerView implements Serializable {
return monarch;
}
public boolean isInitiative() {
return initiative;
}
public List<String> getDesignationNames() {
return designationNames;
}

View file

@ -0,0 +1,108 @@
package mage.cards.w;
import mage.MageInt;
import mage.abilities.Ability;
import mage.abilities.common.BeginningOfUpkeepTriggeredAbility;
import mage.abilities.common.EntersBattlefieldTriggeredAbility;
import mage.abilities.condition.common.CompletedDungeonCondition;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.TakeTheInitiativeEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Outcome;
import mage.constants.SubType;
import mage.constants.TargetController;
import mage.filter.FilterPermanent;
import mage.filter.StaticFilters;
import mage.filter.common.FilterControlledCreaturePermanent;
import mage.filter.predicate.permanent.TappedPredicate;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.target.TargetPermanent;
import mage.watchers.common.CompletedDungeonWatcher;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class WhitePlumeAdventurer extends CardImpl {
public WhitePlumeAdventurer(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{W}");
this.subtype.add(SubType.ORC);
this.subtype.add(SubType.CLERIC);
this.power = new MageInt(3);
this.toughness = new MageInt(3);
// When White Plume Adventurer enters battlefield, you take the initiative.
this.addAbility(new EntersBattlefieldTriggeredAbility(new TakeTheInitiativeEffect()));
// At the beginning of each opponent's upkeep, untap a creature you control. If you've completed a dungeon, untap all creatures you control instead.
this.addAbility(new BeginningOfUpkeepTriggeredAbility(
new WhitePlumeAdventurerEffect(), TargetController.OPPONENT, false
).addHint(CompletedDungeonCondition.getHint()), new CompletedDungeonWatcher());
}
private WhitePlumeAdventurer(final WhitePlumeAdventurer card) {
super(card);
}
@Override
public WhitePlumeAdventurer copy() {
return new WhitePlumeAdventurer(this);
}
}
class WhitePlumeAdventurerEffect extends OneShotEffect {
private static final FilterPermanent filter
= new FilterControlledCreaturePermanent("tapped creature you control");
static {
filter.add(TappedPredicate.TAPPED);
}
WhitePlumeAdventurerEffect() {
super(Outcome.Benefit);
staticText = "untap a creature you control. If you've completed a dungeon, " +
"untap all creatures you control instead";
}
private WhitePlumeAdventurerEffect(final WhitePlumeAdventurerEffect effect) {
super(effect);
}
@Override
public WhitePlumeAdventurerEffect copy() {
return new WhitePlumeAdventurerEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
if (!game.getBattlefield().contains(filter, source, game, 1)) {
return false;
}
if (CompletedDungeonWatcher.checkPlayer(source.getControllerId(), game)) {
for (Permanent permanent : game.getBattlefield().getActivePermanents(
StaticFilters.FILTER_CONTROLLED_CREATURE,
source.getControllerId(), source, game
)) {
permanent.untap(game);
}
return true;
}
Player player = game.getPlayer(source.getControllerId());
if (player == null) {
return false;
}
TargetPermanent target = new TargetPermanent(filter);
target.setNotTarget(true);
player.choose(outcome, target, source, game);
Permanent permanent = game.getPermanent(target.getFirstTarget());
return permanent != null && permanent.untap(game);
}
}

View file

@ -34,6 +34,7 @@ public final class CommanderLegendsBattleForBaldursGate extends ExpansionSet {
cards.add(new SetCardInfo("Sea of Clouds", 360, Rarity.RARE, mage.cards.s.SeaOfClouds.class));
cards.add(new SetCardInfo("Spire Garden", 361, Rarity.RARE, mage.cards.s.SpireGarden.class));
cards.add(new SetCardInfo("Wand of Wonder", 204, Rarity.RARE, mage.cards.w.WandOfWonder.class));
cards.add(new SetCardInfo("White Plume Adventurer", 49, Rarity.RARE, mage.cards.w.WhitePlumeAdventurer.class));
cards.add(new SetCardInfo("Zevlor, Elturel Exile", 296, Rarity.RARE, mage.cards.z.ZevlorElturelExile.class));
}
}

View file

@ -0,0 +1,32 @@
package mage.abilities.effects.common;
import mage.abilities.Ability;
import mage.abilities.effects.OneShotEffect;
import mage.constants.Outcome;
import mage.game.Game;
/**
* @author TheElk801
*/
public class TakeTheInitiativeEffect extends OneShotEffect {
public TakeTheInitiativeEffect() {
super(Outcome.Benefit);
staticText = "you take the initiative";
}
private TakeTheInitiativeEffect(final TakeTheInitiativeEffect effect) {
super(effect);
}
@Override
public TakeTheInitiativeEffect copy() {
return new TakeTheInitiativeEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
game.takeInitiative(source, source.getControllerId());
return true;
}
}

View file

@ -26,7 +26,7 @@ public class VentureIntoTheDungeonEffect extends OneShotEffect {
@Override
public boolean apply(Game game, Ability source) {
game.ventureIntoDungeon(source.getControllerId());
game.ventureIntoDungeon(source.getControllerId(), false);
return true;
}
}

View file

@ -1,13 +1,12 @@
package mage.designations;
/**
*
* @author LevelX2
*/
public enum DesignationType {
THE_MONARCH("The Monarch"),
CITYS_BLESSING("City's Blessing");
CITYS_BLESSING("City's Blessing"),
THE_INITIATIVE("The Initiative");
private final String text;

View file

@ -0,0 +1,179 @@
package mage.designations;
import mage.abilities.Ability;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.effects.OneShotEffect;
import mage.constants.Outcome;
import mage.constants.Zone;
import mage.game.Controllable;
import mage.game.Game;
import mage.game.events.DamagedPlayerBatchEvent;
import mage.game.events.GameEvent;
import mage.target.targetpointer.FixedTarget;
import java.util.Objects;
import java.util.UUID;
/**
* @author TheElk801
*/
public class Initiative extends Designation {
public Initiative() {
super(DesignationType.THE_INITIATIVE, "CLB");
// Whenever one or more creatures a player controls deals combat damage to you, that player takes the initiative.
this.addAbility(new InitiativeDamageTriggeredAbility());
// Whenever you take the initiative and at the beginning of your upkeep, venture into Undercity.
this.addAbility(new InitiativeVentureTriggeredAbility());
}
private Initiative(final Initiative card) {
super(card);
}
@Override
public Initiative copy() {
return new Initiative(this);
}
}
class InitiativeDamageTriggeredAbility extends TriggeredAbilityImpl {
InitiativeDamageTriggeredAbility() {
super(Zone.ALL, new InitiativeTakeEffect());
}
private InitiativeDamageTriggeredAbility(final InitiativeDamageTriggeredAbility ability) {
super(ability);
}
@Override
public InitiativeDamageTriggeredAbility copy() {
return new InitiativeDamageTriggeredAbility(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.DAMAGED_PLAYER_BATCH;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
if (!event.getTargetId().equals(game.getInitiativeId())) {
return false;
}
DamagedPlayerBatchEvent dEvent = (DamagedPlayerBatchEvent) event;
UUID playerId = dEvent
.getEvents()
.stream()
.map(GameEvent::getSourceId)
.map(game::getPermanent)
.filter(Objects::nonNull)
.map(Controllable::getControllerId)
.findFirst()
.orElse(null);
if (playerId == null) {
return false;
}
this.getEffects().setTargetPointer(new FixedTarget(playerId));
return true;
}
@Override
public String getRule() {
return "Whenever one or more creatures a player controls deals combat damage to you, that player takes the initiative.";
}
}
class InitiativeTakeEffect extends OneShotEffect {
InitiativeTakeEffect() {
super(Outcome.Benefit);
}
private InitiativeTakeEffect(final InitiativeTakeEffect effect) {
super(effect);
}
@Override
public InitiativeTakeEffect copy() {
return new InitiativeTakeEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
game.takeInitiative(source, getTargetPointer().getFirst(game, source));
return true;
}
}
class InitiativeVentureTriggeredAbility extends TriggeredAbilityImpl {
InitiativeVentureTriggeredAbility() {
super(Zone.ALL, new InitiativeUndercityEffect());
}
private InitiativeVentureTriggeredAbility(final InitiativeVentureTriggeredAbility ability) {
super(ability);
}
@Override
public InitiativeVentureTriggeredAbility copy() {
return new InitiativeVentureTriggeredAbility(this);
}
@Override
public boolean checkEventType(GameEvent event, Game game) {
return event.getType() == GameEvent.EventType.UPKEEP_STEP_PRE
|| event.getType() == GameEvent.EventType.TOOK_INITIATIVE;
}
@Override
public boolean checkTrigger(GameEvent event, Game game) {
UUID playerId;
switch (event.getType()) {
case UPKEEP_STEP_PRE:
if (!game.isActivePlayer(game.getInitiativeId())) {
return false;
}
playerId = game.getActivePlayerId();
break;
case TOOK_INITIATIVE:
playerId = event.getPlayerId();
break;
default:
return false;
}
this.getEffects().setTargetPointer(new FixedTarget(playerId));
return true;
}
@Override
public String getRule() {
return "Whenever you take the initiative and at the beginning of your upkeep, venture into Undercity.";
}
}
class InitiativeUndercityEffect extends OneShotEffect {
InitiativeUndercityEffect() {
super(Outcome.Benefit);
}
private InitiativeUndercityEffect(final InitiativeUndercityEffect effect) {
super(effect);
}
@Override
public InitiativeUndercityEffect copy() {
return new InitiativeUndercityEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
game.ventureIntoDungeon(getTargetPointer().getFirst(game, source), true);
return true;
}
}

View file

@ -386,7 +386,7 @@ public interface Game extends MageItem, Serializable, Copyable<Game> {
Dungeon addDungeon(Dungeon dungeon, UUID playerId);
void ventureIntoDungeon(UUID playerId);
void ventureIntoDungeon(UUID playerId, boolean undercity);
/**
* Tells whether the current game has day or night, defaults to false
@ -519,6 +519,10 @@ public interface Game extends MageItem, Serializable, Copyable<Game> {
void setMonarchId(Ability source, UUID monarchId);
UUID getInitiativeId();
void takeInitiative(Ability source, UUID initiativeId);
int damagePlayerOrPlaneswalker(UUID playerOrWalker, int damage, UUID attackerId, Ability source, Game game, boolean combatDamage, boolean preventable);
int damagePlayerOrPlaneswalker(UUID playerOrWalker, int damage, UUID attackerId, Ability source, Game game, boolean combatDamage, boolean preventable, List<UUID> appliedEffects);

View file

@ -26,6 +26,7 @@ import mage.constants.*;
import mage.counters.CounterType;
import mage.counters.Counters;
import mage.designations.Designation;
import mage.designations.Initiative;
import mage.designations.Monarch;
import mage.filter.Filter;
import mage.filter.FilterCard;
@ -36,6 +37,7 @@ import mage.filter.predicate.mageobject.NamePredicate;
import mage.filter.predicate.permanent.ControllerIdPredicate;
import mage.game.combat.Combat;
import mage.game.command.*;
import mage.game.command.dungeons.UndercityDungeon;
import mage.game.events.*;
import mage.game.events.TableEvent.EventType;
import mage.game.mulligan.Mulligan;
@ -536,21 +538,24 @@ public abstract class GameImpl implements Game {
));
}
private Dungeon getOrCreateDungeon(UUID playerId) {
private Dungeon getOrCreateDungeon(UUID playerId, boolean undercity) {
Dungeon dungeon = this.getPlayerDungeon(playerId);
if (dungeon != null && dungeon.hasNextRoom()) {
return dungeon;
}
removeDungeon(dungeon);
return this.addDungeon(Dungeon.selectDungeon(playerId, this), playerId);
return this.addDungeon(undercity ? new UndercityDungeon() : Dungeon.selectDungeon(playerId, this), playerId);
}
@Override
public void ventureIntoDungeon(UUID playerId) {
public void ventureIntoDungeon(UUID playerId, boolean undercity) {
if (playerId == null) {
return;
}
if (replaceEvent(GameEvent.getEvent(GameEvent.EventType.VENTURE, playerId, null, playerId))) {
return;
}
this.getOrCreateDungeon(playerId).moveToNextRoom(playerId, this);
this.getOrCreateDungeon(playerId, undercity).moveToNextRoom(playerId, this);
fireEvent(GameEvent.getEvent(GameEvent.EventType.VENTURED, playerId, null, playerId));
}
@ -3691,6 +3696,22 @@ public abstract class GameImpl implements Game {
}
}
@Override
public UUID getInitiativeId() {
return getState().getInitiativeId();
}
@Override
public void takeInitiative(Ability source, UUID initiativeId) {
if (getInitiativeId() == null) {
getState().addDesignation(new Initiative(), this, initiativeId);
} else if (!getInitiativeId().equals(initiativeId)) {
getState().setInitiativeId(initiativeId);
}
informPlayers(getPlayer(initiativeId).getLogName() + " takes the initiative");
fireEvent(new GameEvent(GameEvent.EventType.TOOK_INITIATIVE, initiativeId, source, initiativeId));
}
@Override
public int damagePlayerOrPlaneswalker(UUID playerOrWalker, int damage, UUID attackerId, Ability source, Game game, boolean combatDamage, boolean preventable) {
return damagePlayerOrPlaneswalker(playerOrWalker, damage, attackerId, source, game, combatDamage, preventable, null);

View file

@ -76,6 +76,7 @@ public class GameState implements Serializable, Copyable<GameState> {
private UUID priorityPlayerId; // player that has currently priority
private UUID playerByOrderId; // player that has currently priority
private UUID monarchId; // player that is the monarch
private UUID initiativeId; // player that has the initiative
private SpellStack stack;
private Command command;
private boolean isPlaneChase;
@ -144,6 +145,7 @@ public class GameState implements Serializable, Copyable<GameState> {
this.priorityPlayerId = state.priorityPlayerId;
this.playerByOrderId = state.playerByOrderId;
this.monarchId = state.monarchId;
this.initiativeId = state.initiativeId;
this.turn = state.turn.copy();
this.stack = state.stack.copy();
@ -249,6 +251,7 @@ public class GameState implements Serializable, Copyable<GameState> {
this.playerByOrderId = state.playerByOrderId;
this.priorityPlayerId = state.priorityPlayerId;
this.monarchId = state.monarchId;
this.initiativeId = state.initiativeId;
this.stack = state.stack;
this.command = state.command;
this.isPlaneChase = state.isPlaneChase;
@ -482,6 +485,14 @@ public class GameState implements Serializable, Copyable<GameState> {
this.monarchId = monarchId;
}
public UUID getInitiativeId() {
return initiativeId;
}
public void setInitiativeId(UUID initiativeId) {
this.initiativeId = initiativeId;
}
public UUID getChoosingPlayerId() {
return choosingPlayerId;
}

View file

@ -0,0 +1,152 @@
package mage.game.command.dungeons;
import mage.abilities.Ability;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.CreateTokenEffect;
import mage.abilities.effects.common.DrawCardSourceControllerEffect;
import mage.abilities.effects.common.LoseLifeTargetEffect;
import mage.abilities.effects.common.combat.GoadTargetEffect;
import mage.abilities.effects.common.continuous.GainAbilityTargetEffect;
import mage.abilities.effects.common.counter.AddCountersTargetEffect;
import mage.abilities.effects.common.search.SearchLibraryPutInHandEffect;
import mage.abilities.effects.keyword.ScryEffect;
import mage.abilities.keyword.HexproofAbility;
import mage.cards.Card;
import mage.cards.Cards;
import mage.cards.CardsImpl;
import mage.constants.Outcome;
import mage.constants.Zone;
import mage.counters.CounterType;
import mage.filter.StaticFilters;
import mage.game.Game;
import mage.game.command.Dungeon;
import mage.game.command.DungeonRoom;
import mage.game.permanent.Permanent;
import mage.game.permanent.token.SkeletonToken;
import mage.game.permanent.token.TreasureToken;
import mage.players.Player;
import mage.target.TargetPlayer;
import mage.target.common.TargetCardInLibrary;
import mage.target.common.TargetCreaturePermanent;
import mage.target.targetpointer.FixedTarget;
import mage.util.RandomUtil;
/**
* @author TheElk801
*/
public class UndercityDungeon extends Dungeon {
public UndercityDungeon() {
super("Undercity", "CLB");
DungeonRoom secretEntrance = new DungeonRoom(
"Secret Entrance",
new SearchLibraryPutInHandEffect(
new TargetCardInLibrary(StaticFilters.FILTER_CARD_BASIC_LAND)
)
);
DungeonRoom forge = new DungeonRoom(
"Forge", new AddCountersTargetEffect(CounterType.P1P1.createInstance())
);
forge.addTarget(new TargetCreaturePermanent());
DungeonRoom lostWell = new DungeonRoom(
"Lost Well", new ScryEffect(2, false)
);
DungeonRoom trap = new DungeonRoom("Trap!", new LoseLifeTargetEffect(5));
trap.addTarget(new TargetPlayer());
DungeonRoom arena = new DungeonRoom("Arena", new GoadTargetEffect());
arena.addTarget(new TargetCreaturePermanent());
DungeonRoom stash = new DungeonRoom("Stash", new CreateTokenEffect(new TreasureToken()));
DungeonRoom archives = new DungeonRoom("Archives", new DrawCardSourceControllerEffect(1));
DungeonRoom catacombs = new DungeonRoom("Catacombs", new CreateTokenEffect(new SkeletonToken()));
DungeonRoom throneOfTheDeadThree = new DungeonRoom("Throne of the Dead Three", new ThroneOfTheDeadThreeEffect());
secretEntrance.addNextRoom(forge);
secretEntrance.addNextRoom(lostWell);
forge.addNextRoom(trap);
forge.addNextRoom(arena);
lostWell.addNextRoom(arena);
lostWell.addNextRoom(stash);
trap.addNextRoom(archives);
arena.addNextRoom(archives);
arena.addNextRoom(catacombs);
archives.addNextRoom(throneOfTheDeadThree);
catacombs.addNextRoom(throneOfTheDeadThree);
this.addRoom(secretEntrance);
this.addRoom(forge);
this.addRoom(lostWell);
this.addRoom(trap);
this.addRoom(arena);
this.addRoom(stash);
this.addRoom(archives);
this.addRoom(catacombs);
this.addRoom(throneOfTheDeadThree);
}
private UndercityDungeon(final UndercityDungeon dungeon) {
super(dungeon);
}
public UndercityDungeon copy() {
return new UndercityDungeon(this);
}
}
class ThroneOfTheDeadThreeEffect extends OneShotEffect {
ThroneOfTheDeadThreeEffect() {
super(Outcome.Benefit);
staticText = "Reveal the top ten cards of your library. Put a creature card from among them onto the " +
"battlefield with three +1/+1 counters on it. It gains hexproof until your next turn. Then shuffle.";
}
private ThroneOfTheDeadThreeEffect(final ThroneOfTheDeadThreeEffect effect) {
super(effect);
}
@Override
public ThroneOfTheDeadThreeEffect copy() {
return new ThroneOfTheDeadThreeEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Player player = game.getPlayer(source.getControllerId());
if (player == null) {
return false;
}
Cards cards = new CardsImpl(player.getLibrary().getTopCards(game, 10));
player.revealCards(source, cards, game);
Card card;
switch (cards.count(StaticFilters.FILTER_CARD_CREATURE, game)) {
case 0:
card = null;
break;
case 1:
card = RandomUtil.randomFromCollection(cards.getCards(StaticFilters.FILTER_CARD_CREATURE, game));
break;
default:
TargetCardInLibrary target = new TargetCardInLibrary(StaticFilters.FILTER_CARD_CREATURE);
player.choose(outcome, cards, target, game);
card = cards.get(target.getFirstTarget(), game);
}
if (card != null) {
player.moveCards(card, Zone.BATTLEFIELD, source, game);
Permanent permanent = game.getPermanent(card.getId());
permanent.addCounters(CounterType.P1P1.createInstance(), source, game);
game.addEffect(new GainAbilityTargetEffect(HexproofAbility.getInstance())
.setTargetPointer(new FixedTarget(permanent, game)), source);
}
player.shuffleLibrary(source, game);
return true;
}
}

View file

@ -362,6 +362,7 @@ public class GameEvent implements Serializable {
*/
BECOME_MONARCH,
BECOMES_MONARCH,
TOOK_INITIATIVE,
BECOMES_DAY_NIGHT,
MEDITATED,
PHASE_OUT, PHASED_OUT,

View file

@ -44184,6 +44184,7 @@ Swamp|Streets of New Capenna|266|C||Basic Land - Swamp|||({T}: Add {B}.)|
Mountain|Streets of New Capenna|268|C||Basic Land - Mountain|||({T}: Add {R}.)|
Forest|Streets of New Capenna|270|C||Basic Land - Forest|||({T}: Add {G}.)|
Faceless One|Commander Legends: Battle for Baldur's Gate|1|C|{5}|Legendary Enchantment Creature - Background|3|3|If Faceless One is your commander, choose a color before the game begins. Faceless One is the chosen color.$Choose a Background|
White Plume Adventurer|Commander Legends: Battle for Baldur's Gate|49|R|{2}{W}|Creature - Orc Cleric|3|3|When White Plume Adventurer enters battlefield, you take the initiative.$At the beginning of each opponent's upkeep, untap a creature you control. If you've completed a dungeon, untap all creatures you control instead.|
Ancient Brass Dragon|Commander Legends: Battle for Baldur's Gate|111|M|{5}{B}{B}|Creature - Elder Dragon|7|6|Flying$Whenever Ancient Brass Dragon deals combat damage to a player, roll a d20. When you do, put any number of target creature cards with mana value X or less from graveyards onto the battlefield under your control, where X is the result.|
Elder Brain|Commander Legends: Battle for Baldur's Gate|125|R|{5}{B}{B}|Creature - Horror|6|6|Menace$Whenever Elder Brain attacks a player, exile all cards from that player's hand, then they draw that many cards. You may play lands and cast spells from among the exiled cards for as long as they remain exiled. If you cast a spell this way, you may spend mana as though it were mana of any color to cast it.|
Fireball|Commander Legends: Battle for Baldur's Gate|175|U|{X}{R}|Sorcery|||This spell costs {1} more to cast for each target beyond the first.$Fireball deals X damage divided evenly, rounded down, among any number of targets.|