From 5b88484cb6f0f20fbe2a5350bc0ca4d042540569 Mon Sep 17 00:00:00 2001 From: Evan Kranzler Date: Wed, 14 Jul 2021 09:17:07 -0400 Subject: [PATCH] [AFR] Implementing Class enchantments (ready for review) (#7992) * [AFR] Implemented Druid Class * [AFR] Implemented Wizard Class * [AFR] Implemented Cleric Class * [AFR] Implemented Fighter Class * reworked class ability implementation * fixed an error with setting class level * small reworking of class triggers * added class level hint * added tests * small change * added common class for reminder text --- Mage.Sets/src/mage/cards/c/ClericClass.java | 141 +++++++++++++++ Mage.Sets/src/mage/cards/d/DruidClass.java | 97 +++++++++++ Mage.Sets/src/mage/cards/f/FighterClass.java | 161 ++++++++++++++++++ Mage.Sets/src/mage/cards/w/WizardClass.java | 67 ++++++++ .../sets/AdventuresInTheForgottenRealms.java | 4 + .../test/cards/enchantments/ClassTest.java | 133 +++++++++++++++ ...cksCreatureYouControlTriggeredAbility.java | 2 + .../BecomesClassLevelTriggeredAbility.java | 45 +++++ .../GainClassAbilitySourceEffect.java | 55 ++++++ ...bilitiesCostReductionControllerEffect.java | 17 +- .../abilities/hint/common/ClassLevelHint.java | 27 +++ .../abilities/keyword/ClassLevelAbility.java | 88 ++++++++++ .../keyword/ClassReminderAbility.java | 30 ++++ .../src/main/java/mage/constants/SubType.java | 1 + .../main/java/mage/game/events/GameEvent.java | 1 + .../java/mage/game/permanent/Permanent.java | 4 + .../mage/game/permanent/PermanentImpl.java | 16 ++ 17 files changed, 884 insertions(+), 5 deletions(-) create mode 100644 Mage.Sets/src/mage/cards/c/ClericClass.java create mode 100644 Mage.Sets/src/mage/cards/d/DruidClass.java create mode 100644 Mage.Sets/src/mage/cards/f/FighterClass.java create mode 100644 Mage.Sets/src/mage/cards/w/WizardClass.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/enchantments/ClassTest.java create mode 100644 Mage/src/main/java/mage/abilities/common/BecomesClassLevelTriggeredAbility.java create mode 100644 Mage/src/main/java/mage/abilities/effects/common/continuous/GainClassAbilitySourceEffect.java create mode 100644 Mage/src/main/java/mage/abilities/hint/common/ClassLevelHint.java create mode 100644 Mage/src/main/java/mage/abilities/keyword/ClassLevelAbility.java create mode 100644 Mage/src/main/java/mage/abilities/keyword/ClassReminderAbility.java diff --git a/Mage.Sets/src/mage/cards/c/ClericClass.java b/Mage.Sets/src/mage/cards/c/ClericClass.java new file mode 100644 index 0000000000..7ae9cf2c6c --- /dev/null +++ b/Mage.Sets/src/mage/cards/c/ClericClass.java @@ -0,0 +1,141 @@ +package mage.cards.c; + +import mage.abilities.Ability; +import mage.abilities.common.BecomesClassLevelTriggeredAbility; +import mage.abilities.common.GainLifeControllerTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.ReplacementEffectImpl; +import mage.abilities.effects.common.continuous.GainClassAbilitySourceEffect; +import mage.abilities.effects.common.counter.AddCountersTargetEffect; +import mage.abilities.keyword.ClassLevelAbility; +import mage.abilities.keyword.ClassReminderAbility; +import mage.cards.Card; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.*; +import mage.counters.CounterType; +import mage.filter.StaticFilters; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; +import mage.players.Player; +import mage.target.common.TargetCardInYourGraveyard; +import mage.target.common.TargetControlledCreaturePermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class ClericClass extends CardImpl { + + public ClericClass(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{W}"); + + this.subtype.add(SubType.CLASS); + + // (Gain the next level as a sorcery to add its ability.) + this.addAbility(new ClassReminderAbility()); + + // If you would gain life, you gain that much life plus 1 instead. + this.addAbility(new SimpleStaticAbility(new ClericClassLifeEffect())); + + // {3}{W}: Level 2 + this.addAbility(new ClassLevelAbility(2, "{3}{W}")); + + // Whenever you gain life, put a +1/+1 counter on target creature you control. + Ability ability = new GainLifeControllerTriggeredAbility( + new AddCountersTargetEffect(CounterType.P1P1.createInstance()) + ); + ability.addTarget(new TargetControlledCreaturePermanent()); + this.addAbility(new SimpleStaticAbility(new GainClassAbilitySourceEffect(ability, 2))); + + // {4}{W}: Level 3 + this.addAbility(new ClassLevelAbility(3, "{4}{W}")); + + // When this Class becomes level 3, return target creature card from your graveyard to the battlefield. You gain life equal to its toughness. + ability = new BecomesClassLevelTriggeredAbility(new ClericClassReturnEffect(), 3); + ability.addTarget(new TargetCardInYourGraveyard(StaticFilters.FILTER_CARD_CREATURE_YOUR_GRAVEYARD)); + this.addAbility(ability); + } + + private ClericClass(final ClericClass card) { + super(card); + } + + @Override + public ClericClass copy() { + return new ClericClass(this); + } +} + +class ClericClassLifeEffect extends ReplacementEffectImpl { + + ClericClassLifeEffect() { + super(Duration.WhileOnBattlefield, Outcome.Benefit); + staticText = "If you would gain life, you gain that much life plus 1 instead"; + } + + private ClericClassLifeEffect(final ClericClassLifeEffect effect) { + super(effect); + } + + @Override + public ClericClassLifeEffect copy() { + return new ClericClassLifeEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + return true; + } + + @Override + public boolean replaceEvent(GameEvent event, Ability source, Game game) { + event.setAmount(event.getAmount() + 1); + return false; + } + + @Override + public boolean checksEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.GAIN_LIFE; + } + + @Override + public boolean applies(GameEvent event, Ability source, Game game) { + return source.isControlledBy(event.getPlayerId()); + } +} + +class ClericClassReturnEffect extends OneShotEffect { + + ClericClassReturnEffect() { + super(Outcome.Benefit); + staticText = "return target creature card from your graveyard to the battlefield. " + + "You gain life equal to its toughness"; + } + + private ClericClassReturnEffect(final ClericClassReturnEffect effect) { + super(effect); + } + + @Override + public ClericClassReturnEffect copy() { + return new ClericClassReturnEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Player player = game.getPlayer(source.getControllerId()); + Card card = game.getCard(source.getFirstTarget()); + if (player == null || card == null) { + return false; + } + player.moveCards(card, Zone.BATTLEFIELD, source, game); + Permanent permanent = game.getPermanent(card.getId()); + int toughness = permanent != null ? permanent.getToughness().getValue() : card.getToughness().getValue(); + player.gainLife(toughness, game, source); + return true; + } +} diff --git a/Mage.Sets/src/mage/cards/d/DruidClass.java b/Mage.Sets/src/mage/cards/d/DruidClass.java new file mode 100644 index 0000000000..49b806e0e1 --- /dev/null +++ b/Mage.Sets/src/mage/cards/d/DruidClass.java @@ -0,0 +1,97 @@ +package mage.cards.d; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.BecomesClassLevelTriggeredAbility; +import mage.abilities.common.EntersBattlefieldControlledTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.dynamicvalue.common.LandsYouControlCount; +import mage.abilities.effects.common.GainLifeEffect; +import mage.abilities.effects.common.continuous.BecomesCreatureTargetEffect; +import mage.abilities.effects.common.continuous.GainClassAbilitySourceEffect; +import mage.abilities.effects.common.continuous.PlayAdditionalLandsControllerEffect; +import mage.abilities.effects.common.continuous.SetPowerToughnessSourceEffect; +import mage.abilities.keyword.ClassLevelAbility; +import mage.abilities.keyword.ClassReminderAbility; +import mage.abilities.keyword.HasteAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubLayer; +import mage.constants.SubType; +import mage.filter.StaticFilters; +import mage.game.permanent.token.TokenImpl; +import mage.target.TargetPermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class DruidClass extends CardImpl { + + public DruidClass(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{1}{G}"); + + this.subtype.add(SubType.CLASS); + + // (Gain the next level as a sorcery to add its ability.) + this.addAbility(new ClassReminderAbility()); + + // Whenever a land enters the battlefield under your control, you gain 1 life. + this.addAbility(new EntersBattlefieldControlledTriggeredAbility( + new GainLifeEffect(1), StaticFilters.FILTER_CONTROLLED_LAND_SHORT_TEXT + )); + + // {2}{G}: Level 2 + this.addAbility(new ClassLevelAbility(2, "{2}{G}")); + + // You may play an additional land on each of your turns. + this.addAbility(new SimpleStaticAbility(new GainClassAbilitySourceEffect( + new PlayAdditionalLandsControllerEffect(1, Duration.WhileOnBattlefield), 2 + ))); + + // {4}{G}: Level 3 + this.addAbility(new ClassLevelAbility(3, "{4}{G}")); + + // When this Class becomes level 3, target land you control becomes a creature with haste and "This creature's power and toughness are each equal to the number of lands you control." It's still a land. + Ability ability = new BecomesClassLevelTriggeredAbility(new BecomesCreatureTargetEffect( + new DruidClassToken(), false, true, Duration.Custom + ), 3); + ability.addTarget(new TargetPermanent(StaticFilters.FILTER_CONTROLLED_PERMANENT_LAND)); + this.addAbility(ability); + } + + private DruidClass(final DruidClass card) { + super(card); + } + + @Override + public DruidClass copy() { + return new DruidClass(this); + } +} + +class DruidClassToken extends TokenImpl { + + DruidClassToken() { + super("", "creature with haste and \"This creature's power and toughness are each equal to the number of lands you control.\""); + this.cardType.add(CardType.CREATURE); + this.power = new MageInt(0); + this.toughness = new MageInt(0); + + this.addAbility(HasteAbility.getInstance()); + this.addAbility(new SimpleStaticAbility(new SetPowerToughnessSourceEffect( + LandsYouControlCount.instance, Duration.EndOfGame, SubLayer.SetPT_7b + ).setText("this creature's power and toughness are each equal to the number of lands you control"))); + } + + private DruidClassToken(final DruidClassToken token) { + super(token); + } + + public DruidClassToken copy() { + return new DruidClassToken(this); + } +} diff --git a/Mage.Sets/src/mage/cards/f/FighterClass.java b/Mage.Sets/src/mage/cards/f/FighterClass.java new file mode 100644 index 0000000000..29e81e8eea --- /dev/null +++ b/Mage.Sets/src/mage/cards/f/FighterClass.java @@ -0,0 +1,161 @@ +package mage.cards.f; + +import mage.MageObjectReference; +import mage.abilities.Ability; +import mage.abilities.common.AttacksCreatureYouControlTriggeredAbility; +import mage.abilities.common.EntersBattlefieldTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.OneShotEffect; +import mage.abilities.effects.RequirementEffect; +import mage.abilities.effects.common.continuous.GainClassAbilitySourceEffect; +import mage.abilities.effects.common.cost.AbilitiesCostReductionControllerEffect; +import mage.abilities.effects.common.search.SearchLibraryPutInHandEffect; +import mage.abilities.keyword.ClassLevelAbility; +import mage.abilities.keyword.ClassReminderAbility; +import mage.abilities.keyword.EquipAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.Outcome; +import mage.constants.SubType; +import mage.filter.FilterCard; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.target.common.TargetCardInLibrary; +import mage.target.common.TargetCreaturePermanent; +import mage.target.targetpointer.FixedTarget; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class FighterClass extends CardImpl { + + private static final FilterCard filter = new FilterCard("an Equipment card"); + + static { + filter.add(SubType.EQUIPMENT.getPredicate()); + } + + public FighterClass(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{R}{W}"); + + this.subtype.add(SubType.CLASS); + + // (Gain the next level as a sorcery to add its ability.) + this.addAbility(new ClassReminderAbility()); + + // When Fighter Class enters the battlefield, search your library for an Equipment card, reveal it, put it into your hand, then shuffle. + this.addAbility(new EntersBattlefieldTriggeredAbility( + new SearchLibraryPutInHandEffect(new TargetCardInLibrary(filter)) + )); + + // {1}{R}{W}: Level 2 + this.addAbility(new ClassLevelAbility(2, "{1}{R}{W}")); + + // Equip abilities you activate cost {2} less to activate. + this.addAbility(new SimpleStaticAbility(new GainClassAbilitySourceEffect( + new AbilitiesCostReductionControllerEffect(EquipAbility.class, "Equip") + .setText("\"equip abilities you activate cost {2} less to activate\""), + 2 + ))); + + // {3}{R}{W}: Level 3 + this.addAbility(new ClassLevelAbility(3, "{3}{R}{W}")); + + // Whenever a creature you control attacks, up to one target creature blocks it this combat if able. + Ability ability = new AttacksCreatureYouControlTriggeredAbility(new FighterClassEffect(), false); + ability.addTarget(new TargetCreaturePermanent(0, 1)); + this.addAbility(new SimpleStaticAbility(new GainClassAbilitySourceEffect(ability, 3))); + } + + private FighterClass(final FighterClass card) { + super(card); + } + + @Override + public FighterClass copy() { + return new FighterClass(this); + } +} + +class FighterClassEffect extends OneShotEffect { + + FighterClassEffect() { + super(Outcome.Benefit); + staticText = "up to one target creature blocks it this combat if able"; + } + + private FighterClassEffect(final FighterClassEffect effect) { + super(effect); + } + + @Override + public FighterClassEffect copy() { + return new FighterClassEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + MageObjectReference attackerRef = (MageObjectReference) getValue("attackerRef"); + if (attackerRef == null) { + return false; + } + Permanent attacker = attackerRef.getPermanent(game); + Permanent permanent = game.getPermanent(source.getFirstTarget()); + if (attacker == null || permanent == null) { + return false; + } + game.addEffect(new FighterClassRequirementEffect(attackerRef).setTargetPointer(new FixedTarget(permanent, game)), source); + return true; + } +} + +class FighterClassRequirementEffect extends RequirementEffect { + + private final MageObjectReference mor; + + public FighterClassRequirementEffect(MageObjectReference mor) { + super(Duration.EndOfCombat); + this.mor = mor; + } + + public FighterClassRequirementEffect(final FighterClassRequirementEffect effect) { + super(effect); + this.mor = effect.mor; + } + + @Override + public boolean applies(Permanent permanent, Ability source, Game game) { + Permanent attacker = mor.getPermanent(game); + if (attacker == null) { + discard(); + return false; + } + return permanent != null + && permanent.getId().equals(this.getTargetPointer().getFirst(game, source)) + && permanent.canBlock(source.getSourceId(), game); + } + + @Override + public boolean mustAttack(Game game) { + return false; + } + + @Override + public boolean mustBlock(Game game) { + return true; + } + + @Override + public UUID mustBlockAttacker(Ability source, Game game) { + return mor.getSourceId(); + } + + @Override + public FighterClassRequirementEffect copy() { + return new FighterClassRequirementEffect(this); + } +} diff --git a/Mage.Sets/src/mage/cards/w/WizardClass.java b/Mage.Sets/src/mage/cards/w/WizardClass.java new file mode 100644 index 0000000000..4a845b479a --- /dev/null +++ b/Mage.Sets/src/mage/cards/w/WizardClass.java @@ -0,0 +1,67 @@ +package mage.cards.w; + +import mage.abilities.Ability; +import mage.abilities.common.BecomesClassLevelTriggeredAbility; +import mage.abilities.common.DrawCardControllerTriggeredAbility; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.effects.common.continuous.GainClassAbilitySourceEffect; +import mage.abilities.effects.common.continuous.MaximumHandSizeControllerEffect; +import mage.abilities.effects.common.counter.AddCountersTargetEffect; +import mage.abilities.keyword.ClassLevelAbility; +import mage.abilities.keyword.ClassReminderAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.SubType; +import mage.counters.CounterType; +import mage.target.common.TargetControlledCreaturePermanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public final class WizardClass extends CardImpl { + + public WizardClass(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{U}"); + + this.subtype.add(SubType.CLASS); + + // (Gain the next level as a sorcery to add its ability.) + this.addAbility(new ClassReminderAbility()); + + // You have no maximum hand size. + this.addAbility(new SimpleStaticAbility(new MaximumHandSizeControllerEffect( + Integer.MAX_VALUE, Duration.WhileOnBattlefield, + MaximumHandSizeControllerEffect.HandSizeModification.SET + ))); + + // {2}{U}: Level 2 + this.addAbility(new ClassLevelAbility(2, "{2}{U}")); + + // When this Class becomes level 2, draw two cards. + this.addAbility(new BecomesClassLevelTriggeredAbility(new DrawCardSourceControllerEffect(2), 2)); + + // {4}{U}: Level 3 + this.addAbility(new ClassLevelAbility(3, "{4}{U}")); + + // Whenever you draw a card, put a +1/+1 counter on target creature you control. + Ability ability = new DrawCardControllerTriggeredAbility( + new AddCountersTargetEffect(CounterType.P1P1.createInstance()), false + ); + ability.addTarget(new TargetControlledCreaturePermanent()); + this.addAbility(new SimpleStaticAbility(new GainClassAbilitySourceEffect(ability, 3))); + } + + private WizardClass(final WizardClass card) { + super(card); + } + + @Override + public WizardClass copy() { + return new WizardClass(this); + } +} diff --git a/Mage.Sets/src/mage/sets/AdventuresInTheForgottenRealms.java b/Mage.Sets/src/mage/sets/AdventuresInTheForgottenRealms.java index c2d12d510f..f0927484c1 100644 --- a/Mage.Sets/src/mage/sets/AdventuresInTheForgottenRealms.java +++ b/Mage.Sets/src/mage/sets/AdventuresInTheForgottenRealms.java @@ -55,6 +55,7 @@ public final class AdventuresInTheForgottenRealms extends ExpansionSet { cards.add(new SetCardInfo("Circle of Dreams Druid", 176, Rarity.RARE, mage.cards.c.CircleOfDreamsDruid.class)); cards.add(new SetCardInfo("Circle of the Moon Druid", 177, Rarity.COMMON, mage.cards.c.CircleOfTheMoonDruid.class)); cards.add(new SetCardInfo("Clattering Skeletons", 93, Rarity.COMMON, mage.cards.c.ClatteringSkeletons.class)); + cards.add(new SetCardInfo("Cleric Class", 6, Rarity.UNCOMMON, mage.cards.c.ClericClass.class)); cards.add(new SetCardInfo("Clever Conjurer", 51, Rarity.COMMON, mage.cards.c.CleverConjurer.class)); cards.add(new SetCardInfo("Cloister Gargoyle", 7, Rarity.UNCOMMON, mage.cards.c.CloisterGargoyle.class)); cards.add(new SetCardInfo("Compelled Duel", 178, Rarity.COMMON, mage.cards.c.CompelledDuel.class)); @@ -77,6 +78,7 @@ public final class AdventuresInTheForgottenRealms extends ExpansionSet { cards.add(new SetCardInfo("Dragon's Fire", 139, Rarity.COMMON, mage.cards.d.DragonsFire.class)); cards.add(new SetCardInfo("Drider", 98, Rarity.UNCOMMON, mage.cards.d.Drider.class)); cards.add(new SetCardInfo("Drizzt Do'Urden", 220, Rarity.RARE, mage.cards.d.DrizztDoUrden.class)); + cards.add(new SetCardInfo("Druid Class", 180, Rarity.UNCOMMON, mage.cards.d.DruidClass.class)); cards.add(new SetCardInfo("Dueling Rapier", 140, Rarity.COMMON, mage.cards.d.DuelingRapier.class)); cards.add(new SetCardInfo("Dungeon Crawler", 99, Rarity.UNCOMMON, mage.cards.d.DungeonCrawler.class)); cards.add(new SetCardInfo("Dungeon Descent", 255, Rarity.RARE, mage.cards.d.DungeonDescent.class)); @@ -94,6 +96,7 @@ public final class AdventuresInTheForgottenRealms extends ExpansionSet { cards.add(new SetCardInfo("Fates' Reversal", 102, Rarity.COMMON, mage.cards.f.FatesReversal.class)); cards.add(new SetCardInfo("Feign Death", 103, Rarity.COMMON, mage.cards.f.FeignDeath.class)); cards.add(new SetCardInfo("Fifty Feet of Rope", 244, Rarity.UNCOMMON, mage.cards.f.FiftyFeetOfRope.class)); + cards.add(new SetCardInfo("Fighter Class", 222, Rarity.RARE, mage.cards.f.FighterClass.class)); cards.add(new SetCardInfo("Find the Path", 183, Rarity.COMMON, mage.cards.f.FindThePath.class)); cards.add(new SetCardInfo("Flumph", 15, Rarity.RARE, mage.cards.f.Flumph.class)); cards.add(new SetCardInfo("Fly", 59, Rarity.UNCOMMON, mage.cards.f.Fly.class)); @@ -241,6 +244,7 @@ public final class AdventuresInTheForgottenRealms extends ExpansionSet { cards.add(new SetCardInfo("White Dragon", 41, Rarity.UNCOMMON, mage.cards.w.WhiteDragon.class)); cards.add(new SetCardInfo("Wight", 127, Rarity.RARE, mage.cards.w.Wight.class)); cards.add(new SetCardInfo("Wild Shape", 212, Rarity.UNCOMMON, mage.cards.w.WildShape.class)); + cards.add(new SetCardInfo("Wizard Class", 81, Rarity.UNCOMMON, mage.cards.w.WizardClass.class)); cards.add(new SetCardInfo("Xorn", 167, Rarity.RARE, mage.cards.x.Xorn.class)); cards.add(new SetCardInfo("You Come to a River", 83, Rarity.COMMON, mage.cards.y.YouComeToARiver.class)); cards.add(new SetCardInfo("You Come to the Gnoll Camp", 168, Rarity.COMMON, mage.cards.y.YouComeToTheGnollCamp.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/enchantments/ClassTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/enchantments/ClassTest.java new file mode 100644 index 0000000000..b3f1ff9c86 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/enchantments/ClassTest.java @@ -0,0 +1,133 @@ +package org.mage.test.cards.enchantments; + +import mage.abilities.keyword.HasteAbility; +import mage.constants.CardType; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import mage.game.permanent.Permanent; +import org.junit.Assert; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author TheElk801 + */ +public class ClassTest extends CardTestPlayerBase { + + private static final String wizard = "Wizard Class"; + private static final String merfolk = "Merfolk of the Pearl Trident"; + private static final String druid = "Druid Class"; + private static final String forest = "Forest"; + private static final String wastes = "Wastes"; + + private void assertClassLevel(String cardName, int level) { + Permanent permanent = getPermanent(cardName); + Assert.assertEquals( + cardName + " should be level " + level + + " but was level " + permanent.getClassLevel(), + level, permanent.getClassLevel() + ); + } + + @Test + public void test_WizardClass__FirstLevel() { + addCard(Zone.BATTLEFIELD, playerA, wizard); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertClassLevel(wizard, 1); + assertHandCount(playerA, 0); + } + + @Test + public void test_WizardClass__SecondLevel() { + addCard(Zone.BATTLEFIELD, playerA, "Island", 3); + addCard(Zone.BATTLEFIELD, playerA, wizard); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}{U}"); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertClassLevel(wizard, 2); + assertHandCount(playerA, 2); + } + + @Test + public void test_WizardClass__ThirdLevel() { + addCard(Zone.BATTLEFIELD, playerA, "Island", 8); + addCard(Zone.BATTLEFIELD, playerA, wizard); + addCard(Zone.BATTLEFIELD, playerA, merfolk); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}{U}"); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{4}{U}"); + + setStopAt(3, PhaseStep.PRECOMBAT_MAIN); + execute(); + assertAllCommandsUsed(); + + assertClassLevel(wizard, 3); + assertHandCount(playerA, 3); + assertCounterCount(merfolk, CounterType.P1P1, 1); + } + + @Test + public void test_DruidClass__FirstLevel() { + addCard(Zone.BATTLEFIELD, playerA, druid); + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + assertClassLevel(druid, 1); + assertHandCount(playerA, 0); + } + + @Test + public void test_DruidClass__SecondLevel() { + addCard(Zone.BATTLEFIELD, playerA, forest, 3); + addCard(Zone.BATTLEFIELD, playerA, druid); + addCard(Zone.HAND, playerA, forest, 2); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}{G}"); + playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, forest); + playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, forest); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertClassLevel(druid, 2); + assertHandCount(playerA, 0); + assertPermanentCount(playerA, forest, 5); + assertLife(playerA, 20 + 1 + 1); + } + + @Test + public void test_DruidClass__ThirdLevel() { + addCard(Zone.BATTLEFIELD, playerA, forest, 6); + addCard(Zone.BATTLEFIELD, playerA, druid); + addCard(Zone.HAND, playerA, forest); + addCard(Zone.HAND, playerA, wastes); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}{G}"); + playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, forest); + playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, wastes); + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{4}{G}"); + addTarget(playerA, wastes); + + setStopAt(1, PhaseStep.END_TURN); + execute(); + assertAllCommandsUsed(); + + assertClassLevel(druid, 3); + assertHandCount(playerA, 0); + assertPermanentCount(playerA, forest, 7); + assertLife(playerA, 20 + 1 + 1); + assertType(wastes, CardType.CREATURE, true); + assertPowerToughness(playerA, wastes, 8, 8); + assertAbility(playerA, wastes, HasteAbility.getInstance(), true); + } +} diff --git a/Mage/src/main/java/mage/abilities/common/AttacksCreatureYouControlTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/AttacksCreatureYouControlTriggeredAbility.java index 6ec8413e06..8d58ae0882 100644 --- a/Mage/src/main/java/mage/abilities/common/AttacksCreatureYouControlTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/common/AttacksCreatureYouControlTriggeredAbility.java @@ -1,5 +1,6 @@ package mage.abilities.common; +import mage.MageObjectReference; import mage.abilities.TriggeredAbilityImpl; import mage.abilities.effects.Effect; import mage.constants.Zone; @@ -64,6 +65,7 @@ public class AttacksCreatureYouControlTriggeredAbility extends TriggeredAbilityI if (setTargetPointer) { this.getEffects().setTargetPointer(new FixedTarget(event.getSourceId(), game)); } + this.getEffects().setValue("attackerRef", new MageObjectReference(sourcePermanent, game)); return true; } return false; diff --git a/Mage/src/main/java/mage/abilities/common/BecomesClassLevelTriggeredAbility.java b/Mage/src/main/java/mage/abilities/common/BecomesClassLevelTriggeredAbility.java new file mode 100644 index 0000000000..5ae84463b4 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/common/BecomesClassLevelTriggeredAbility.java @@ -0,0 +1,45 @@ +package mage.abilities.common; + +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.effects.Effect; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.events.GameEvent; + +/** + * @author TheElk801 + */ +public class BecomesClassLevelTriggeredAbility extends TriggeredAbilityImpl { + + private final int level; + + public BecomesClassLevelTriggeredAbility(Effect effect, int level) { + super(Zone.BATTLEFIELD, effect); + this.level = level; + } + + private BecomesClassLevelTriggeredAbility(final BecomesClassLevelTriggeredAbility ability) { + super(ability); + this.level = ability.level; + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.GAINS_CLASS_LEVEL; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + return event.getAmount() == level && event.getSourceId().equals(getSourceId()); + } + + @Override + public BecomesClassLevelTriggeredAbility copy() { + return new BecomesClassLevelTriggeredAbility(this); + } + + @Override + public String getRule() { + return "When this Class becomes level " + level + ", " + super.getRule(); + } +} diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/GainClassAbilitySourceEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainClassAbilitySourceEffect.java new file mode 100644 index 0000000000..38446722cb --- /dev/null +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/GainClassAbilitySourceEffect.java @@ -0,0 +1,55 @@ +package mage.abilities.effects.common.continuous; + +import mage.abilities.Ability; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.ContinuousEffectImpl; +import mage.abilities.effects.Effect; +import mage.constants.Duration; +import mage.constants.Layer; +import mage.constants.Outcome; +import mage.constants.SubLayer; +import mage.game.Game; +import mage.game.permanent.Permanent; + +/** + * @author TheElk801 + */ +public class GainClassAbilitySourceEffect extends ContinuousEffectImpl implements SourceEffect { + + private final Ability ability; + private final int level; + + public GainClassAbilitySourceEffect(Effect effect, int level) { + this(new SimpleStaticAbility(effect), level); + } + + public GainClassAbilitySourceEffect(Ability ability, int level) { + super(Duration.WhileOnBattlefield, Layer.AbilityAddingRemovingEffects_6, SubLayer.NA, Outcome.AddAbility); + staticText = ability.getRule(); + this.ability = ability; + this.level = level; + this.ability.setRuleVisible(false); + generateGainAbilityDependencies(ability, null); + } + + private GainClassAbilitySourceEffect(final GainClassAbilitySourceEffect effect) { + super(effect); + this.level = effect.level; + this.ability = effect.ability; + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent permanent = source.getSourcePermanentIfItStillExists(game); + if (permanent == null || permanent.getClassLevel() < level) { + return false; + } + permanent.addAbility(ability, source.getSourceId(), game); + return true; + } + + @Override + public GainClassAbilitySourceEffect copy() { + return new GainClassAbilitySourceEffect(this); + } +} diff --git a/Mage/src/main/java/mage/abilities/effects/common/cost/AbilitiesCostReductionControllerEffect.java b/Mage/src/main/java/mage/abilities/effects/common/cost/AbilitiesCostReductionControllerEffect.java index 5c87bd046f..6b504e47b3 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/cost/AbilitiesCostReductionControllerEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/cost/AbilitiesCostReductionControllerEffect.java @@ -1,6 +1,7 @@ package mage.abilities.effects.common.cost; import mage.abilities.Ability; +import mage.abilities.ActivatedAbility; import mage.constants.CostModificationType; import mage.constants.Duration; import mage.constants.Outcome; @@ -8,22 +9,28 @@ import mage.game.Game; import mage.util.CardUtil; /** - * * @author Styxo */ public class AbilitiesCostReductionControllerEffect extends CostModificationEffectImpl { - private final Class activatedAbility; + private final Class activatedAbility; + private final int amount; - public AbilitiesCostReductionControllerEffect(Class activatedAbility, String activatedAbilityName) { + public AbilitiesCostReductionControllerEffect(Class activatedAbility, String activatedAbilityName) { + this(activatedAbility, activatedAbilityName, 1); + } + + public AbilitiesCostReductionControllerEffect(Class activatedAbility, String activatedAbilityName, int amount) { super(Duration.WhileOnBattlefield, Outcome.Benefit, CostModificationType.REDUCE_COST); this.activatedAbility = activatedAbility; - staticText = activatedAbilityName + " costs you pay cost {1} less"; + staticText = activatedAbilityName + " costs you pay cost {" + amount + "} less"; + this.amount = amount; } public AbilitiesCostReductionControllerEffect(AbilitiesCostReductionControllerEffect effect) { super(effect); this.activatedAbility = effect.activatedAbility; + this.amount = effect.amount; } @Override @@ -33,7 +40,7 @@ public class AbilitiesCostReductionControllerEffect extends CostModificationEffe @Override public boolean apply(Game game, Ability source, Ability abilityToModify) { - CardUtil.reduceCost(abilityToModify, 1); + CardUtil.reduceCost(abilityToModify, amount); return true; } diff --git a/Mage/src/main/java/mage/abilities/hint/common/ClassLevelHint.java b/Mage/src/main/java/mage/abilities/hint/common/ClassLevelHint.java new file mode 100644 index 0000000000..aee3453c47 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/hint/common/ClassLevelHint.java @@ -0,0 +1,27 @@ +package mage.abilities.hint.common; + +import mage.abilities.Ability; +import mage.abilities.hint.Hint; +import mage.game.Game; +import mage.game.permanent.Permanent; + +/** + * @author TheElk801 + */ +public enum ClassLevelHint implements Hint { + instance; + + @Override + public String getText(Game game, Ability ability) { + Permanent permanent = ability.getSourcePermanentIfItStillExists(game); + if (permanent == null) { + return null; + } + return "Class level: " + permanent.getClassLevel(); + } + + @Override + public ClassLevelHint copy() { + return this; + } +} \ No newline at end of file diff --git a/Mage/src/main/java/mage/abilities/keyword/ClassLevelAbility.java b/Mage/src/main/java/mage/abilities/keyword/ClassLevelAbility.java new file mode 100644 index 0000000000..3061e6d8fd --- /dev/null +++ b/Mage/src/main/java/mage/abilities/keyword/ClassLevelAbility.java @@ -0,0 +1,88 @@ +package mage.abilities.keyword; + +import mage.abilities.Ability; +import mage.abilities.ActivatedAbilityImpl; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.OneShotEffect; +import mage.constants.Outcome; +import mage.constants.TimingRule; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.permanent.Permanent; + +import java.util.UUID; + +/** + * @author TheElk801 + */ +public class ClassLevelAbility extends ActivatedAbilityImpl { + + private final int level; + private final String manaString; + + public ClassLevelAbility(int level, String manaString) { + super(Zone.BATTLEFIELD, new SetClassLevelEffect(level), new ManaCostsImpl<>(manaString)); + this.level = level; + this.manaString = manaString; + setTiming(TimingRule.SORCERY); + } + + private ClassLevelAbility(final ClassLevelAbility ability) { + super(ability); + this.level = ability.level; + this.manaString = ability.manaString; + } + + @Override + public ClassLevelAbility copy() { + return new ClassLevelAbility(this); + } + + @Override + public String getRule() { + return manaString + ": Level " + level; + } + + @Override + public ActivationStatus canActivate(UUID playerId, Game game) { + Permanent permanent = getSourcePermanentIfItStillExists(game); + if (permanent != null && permanent.getClassLevel() == level - 1) { + return super.canActivate(playerId, game); + } + return ActivationStatus.getFalse(); + } +} + +class SetClassLevelEffect extends OneShotEffect { + + private final int level; + + SetClassLevelEffect(int level) { + super(Outcome.Benefit); + this.level = level; + } + + private SetClassLevelEffect(final SetClassLevelEffect effect) { + super(effect); + this.level = effect.level; + } + + @Override + public SetClassLevelEffect copy() { + return new SetClassLevelEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + Permanent permanent = source.getSourcePermanentIfItStillExists(game); + if (permanent == null || !permanent.setClassLevel(level)) { + return false; + } + game.fireEvent(GameEvent.getEvent( + GameEvent.EventType.GAINS_CLASS_LEVEL, source.getSourceId(), + source, source.getControllerId(), level + )); + return true; + } +} diff --git a/Mage/src/main/java/mage/abilities/keyword/ClassReminderAbility.java b/Mage/src/main/java/mage/abilities/keyword/ClassReminderAbility.java new file mode 100644 index 0000000000..b9964bf564 --- /dev/null +++ b/Mage/src/main/java/mage/abilities/keyword/ClassReminderAbility.java @@ -0,0 +1,30 @@ +package mage.abilities.keyword; + +import mage.abilities.StaticAbility; +import mage.abilities.hint.common.ClassLevelHint; +import mage.constants.Zone; + +/** + * @author TheElk801 + */ +public class ClassReminderAbility extends StaticAbility { + + public ClassReminderAbility() { + super(Zone.ALL, null); + this.addHint(ClassLevelHint.instance); + } + + private ClassReminderAbility(final ClassReminderAbility ability) { + super(ability); + } + + @Override + public ClassReminderAbility copy() { + return new ClassReminderAbility(this); + } + + @Override + public String getRule() { + return "(Gain the next level as a sorcery to add its ability.)"; + } +} diff --git a/Mage/src/main/java/mage/constants/SubType.java b/Mage/src/main/java/mage/constants/SubType.java index 90d22d437a..961900b695 100644 --- a/Mage/src/main/java/mage/constants/SubType.java +++ b/Mage/src/main/java/mage/constants/SubType.java @@ -32,6 +32,7 @@ public enum SubType { // 205.3h Enchantments have their own unique set of subtypes; these subtypes are called enchantment types. AURA("Aura", SubTypeSet.EnchantmentType), CARTOUCHE("Cartouche", SubTypeSet.EnchantmentType), + CLASS("Class", SubTypeSet.EnchantmentType), CURSE("Curse", SubTypeSet.EnchantmentType), RUNE("Rune", SubTypeSet.EnchantmentType), SAGA("Saga", SubTypeSet.EnchantmentType), diff --git a/Mage/src/main/java/mage/game/events/GameEvent.java b/Mage/src/main/java/mage/game/events/GameEvent.java index 5b266493ff..988fa97ae6 100644 --- a/Mage/src/main/java/mage/game/events/GameEvent.java +++ b/Mage/src/main/java/mage/game/events/GameEvent.java @@ -340,6 +340,7 @@ public class GameEvent implements Serializable { */ BECOMES_EXERTED, BECOMES_RENOWNED, + GAINS_CLASS_LEVEL, /* BECOMES_MONARCH targetId playerId of the player that becomes the monarch sourceId id of the source object that created that effect, if no effect exist it's null diff --git a/Mage/src/main/java/mage/game/permanent/Permanent.java b/Mage/src/main/java/mage/game/permanent/Permanent.java index 5e941a7571..afb3725c4b 100644 --- a/Mage/src/main/java/mage/game/permanent/Permanent.java +++ b/Mage/src/main/java/mage/game/permanent/Permanent.java @@ -73,6 +73,10 @@ public interface Permanent extends Card, Controllable { void setRenowned(boolean value); + int getClassLevel(); + + boolean setClassLevel(int classLevel); + void setCardNumber(String cid); void setExpansionSetCode(String expansionSetCode); diff --git a/Mage/src/main/java/mage/game/permanent/PermanentImpl.java b/Mage/src/main/java/mage/game/permanent/PermanentImpl.java index efab7e0d09..355c92e488 100644 --- a/Mage/src/main/java/mage/game/permanent/PermanentImpl.java +++ b/Mage/src/main/java/mage/game/permanent/PermanentImpl.java @@ -72,6 +72,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { protected boolean renowned; protected boolean manifested = false; protected boolean morphed = false; + protected int classLevel = 1; protected UUID originalControllerId; protected UUID controllerId; protected UUID beforeResetControllerId; @@ -163,6 +164,7 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { this.transformed = permanent.transformed; this.monstrous = permanent.monstrous; this.renowned = permanent.renowned; + this.classLevel = permanent.classLevel; this.pairedPermanent = permanent.pairedPermanent; this.bandedCards.addAll(permanent.bandedCards); this.timesLoyaltyUsed = permanent.timesLoyaltyUsed; @@ -1519,6 +1521,20 @@ public abstract class PermanentImpl extends CardImpl implements Permanent { this.renowned = value; } + @Override + public int getClassLevel() { + return classLevel; + } + + @Override + public boolean setClassLevel(int classLevel) { + if (this.classLevel == classLevel - 1) { + this.classLevel = classLevel; + return true; + } + return false; + } + @Override public void setPairedCard(MageObjectReference pairedCard) { this.pairedPermanent = pairedCard;