WIP: Implement Role mechanic (#10816)

* [WOE] Implement Embereth Veteran

* add SBA for role tokens

* [WOE] Implement Cursed Courtier

* [WOE] Implement Conceited Witch

* [WOE] Implement Besotted Knight

* [WOE] Implement Syr Armont, the Redeemer

* [WOE] Implement Living Lectern

* add role test

* [WOE] Implement Lord Skitter's Blessing

* [WOE] Implement Faunsbane Troll

* [WOE] Implement Twisted Fealty

* [WOC] Implement Ellivere of the Wild Court

* [WOE] Implement Monstrous Rage

* [WOE] Implement Spellbook Vendor

* add verify skips

* extra fix
This commit is contained in:
Evan Kranzler 2023-08-17 10:18:21 -04:00 committed by GitHub
parent bf508aca9b
commit b20bdcede7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 1230 additions and 6 deletions

View file

@ -0,0 +1,50 @@
package mage.abilities.effects.common;
import mage.abilities.Ability;
import mage.abilities.Mode;
import mage.abilities.effects.OneShotEffect;
import mage.constants.Outcome;
import mage.constants.RoleType;
import mage.game.Game;
import mage.game.permanent.Permanent;
/**
* @author TheElk801
*/
public class CreateRoleAttachedSourceEffect extends OneShotEffect {
private final RoleType roleType;
public CreateRoleAttachedSourceEffect(RoleType roleType) {
super(Outcome.Benefit);
this.roleType = roleType;
}
private CreateRoleAttachedSourceEffect(final CreateRoleAttachedSourceEffect effect) {
super(effect);
this.roleType = effect.roleType;
}
@Override
public CreateRoleAttachedSourceEffect copy() {
return new CreateRoleAttachedSourceEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Permanent permanent = source.getSourcePermanentIfItStillExists(game);
if (permanent == null) {
return false;
}
roleType.createToken(permanent, game, source);
return true;
}
@Override
public String getText(Mode mode) {
if (staticText != null && !staticText.isEmpty()) {
return staticText;
}
return "create a " + roleType.getName() + " Role token attached to it";
}
}

View file

@ -0,0 +1,51 @@
package mage.abilities.effects.common;
import mage.abilities.Ability;
import mage.abilities.Mode;
import mage.abilities.effects.OneShotEffect;
import mage.constants.Outcome;
import mage.constants.RoleType;
import mage.game.Game;
import mage.game.permanent.Permanent;
/**
* @author TheElk801
*/
public class CreateRoleAttachedTargetEffect extends OneShotEffect {
private final RoleType roleType;
public CreateRoleAttachedTargetEffect(RoleType roleType) {
super(Outcome.Benefit);
this.roleType = roleType;
}
private CreateRoleAttachedTargetEffect(final CreateRoleAttachedTargetEffect effect) {
super(effect);
this.roleType = effect.roleType;
}
@Override
public CreateRoleAttachedTargetEffect copy() {
return new CreateRoleAttachedTargetEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Permanent permanent = game.getPermanent(getTargetPointer().getFirst(game, source));
if (permanent == null) {
return false;
}
roleType.createToken(permanent, game, source);
return true;
}
@Override
public String getText(Mode mode) {
if (staticText != null && !staticText.isEmpty()) {
return staticText;
}
return "create a " + roleType.getName() + " Role token attached to " +
getTargetPointer().describeTargets(mode.getTargets(), "it");
}
}

View file

@ -0,0 +1,53 @@
package mage.constants;
import mage.abilities.Ability;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.game.permanent.token.*;
import java.util.UUID;
import java.util.function.Supplier;
/**
* @author TheElk801
*/
public enum RoleType {
CURSED("Cursed", CursedRoleToken::new),
MONSTER("Monster", MonsterRoleToken::new),
ROYAL("Royal", RoyalRoleToken::new),
SORCERER("Sorcerer", SorcererRoleToken::new),
VIRTUOUS("Virtuous", VirtuousRoleToken::new),
WICKED("Wicked", WickedRoleToken::new),
YOUNG_HERO("Young Hero", YoungHeroRoleToken::new);
private final String name;
private final Supplier<Token> supplier;
RoleType(String name, Supplier<Token> supplier) {
this.name = name;
this.supplier = supplier;
}
public String getName() {
return name;
}
@Override
public String toString() {
return name;
}
public Token createToken(Permanent permanent, Game game, Ability source) {
Token token = supplier.get();
token.putOntoBattlefield(1, game, source);
for (UUID tokenId : token.getLastAddedTokenIds()) {
Permanent aura = game.getPermanent(tokenId);
if (aura == null || !aura.hasSubtype(SubType.AURA, game)) {
continue;
}
aura.getAbilities().get(0).getTargets().get(0).add(permanent.getId(), game);
aura.getAbilities().get(0).getEffects().get(0).apply(game, aura.getAbilities().get(0));
}
return token;
}
}

View file

@ -40,6 +40,7 @@ public enum SubType {
CARTOUCHE("Cartouche", SubTypeSet.EnchantmentType),
CLASS("Class", SubTypeSet.EnchantmentType),
CURSE("Curse", SubTypeSet.EnchantmentType),
ROLE("Role", SubTypeSet.EnchantmentType),
RUNE("Rune", SubTypeSet.EnchantmentType),
SAGA("Saga", SubTypeSet.EnchantmentType),
SHARD("Shard", SubTypeSet.EnchantmentType),

View file

@ -63,11 +63,7 @@ import mage.target.Target;
import mage.target.TargetCard;
import mage.target.TargetPermanent;
import mage.target.TargetPlayer;
import mage.util.CardUtil;
import mage.util.GameLog;
import mage.util.MessageToClient;
import mage.util.MultiAmountMessage;
import mage.util.RandomUtil;
import mage.util.*;
import mage.util.functions.CopyApplier;
import mage.watchers.Watcher;
import mage.watchers.common.*;
@ -2331,6 +2327,7 @@ public abstract class GameImpl implements Game {
List<Permanent> legendary = new ArrayList<>();
List<Permanent> worldEnchantment = new ArrayList<>();
Map<UUID, Map<UUID, Set<Permanent>>> roleMap = new HashMap<>();
List<FilterCreaturePermanent> usePowerInsteadOfToughnessForDamageLethalityFilters = getState().getActivePowerInsteadOfToughnessForDamageLethalityFilters();
for (Permanent perm : getBattlefield().getAllActivePermanents()) {
if (perm.isCreature(this)) {
@ -2510,6 +2507,11 @@ public abstract class GameImpl implements Game {
}
}
}
if (perm.hasSubtype(SubType.ROLE, this) && state.getZone(perm.getId()) == Zone.BATTLEFIELD) {
roleMap.computeIfAbsent(perm.getControllerId(), x -> new HashMap<>())
.computeIfAbsent(perm.getAttachedTo(), x -> new HashSet<>())
.add(perm);
}
}
// 704.5s If the number of lore counters on a Saga permanent is greater than or equal to its final chapter number
// and it isn't the source of a chapter ability that has triggered but not yet left the stack, that Saga's controller sacrifices it.
@ -2733,6 +2735,29 @@ public abstract class GameImpl implements Game {
}
}
if (!roleMap.isEmpty()) {
List<Set<Permanent>> rolesToHandle = roleMap.values()
.stream()
.map(Map::values)
.flatMap(Collection::stream)
.filter(s -> s.size() > 1)
.collect(Collectors.toList());
if (!rolesToHandle.isEmpty()) {
for (Set<Permanent> roleSet : rolesToHandle) {
int newest = roleSet
.stream()
.mapToInt(Permanent::getCreateOrder)
.max()
.orElse(-1);
roleSet.removeIf(permanent -> permanent.getCreateOrder() == newest);
for (Permanent permanent : roleSet) {
movePermanentToGraveyardWithInfo(permanent);
somethingHappened = true;
}
}
}
}
// Daybound/Nightbound permanents should be transformed according to day/night
// This is not a state-based action but it's unclear where else to put it
if (hasDayNight()) {

View file

@ -0,0 +1,42 @@
package mage.game.permanent.token;
import mage.abilities.Ability;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.common.AttachEffect;
import mage.abilities.effects.common.continuous.SetBasePowerToughnessEnchantedEffect;
import mage.abilities.keyword.EnchantAbility;
import mage.constants.CardType;
import mage.constants.Outcome;
import mage.constants.SubType;
import mage.target.TargetPermanent;
import mage.target.common.TargetCreaturePermanent;
/**
* @author TheElk801
*/
public final class CursedRoleToken extends TokenImpl {
public CursedRoleToken() {
super("Cursed", "Cursed Role token");
cardType.add(CardType.ENCHANTMENT);
subtype.add(SubType.AURA);
subtype.add(SubType.ROLE);
TargetPermanent auraTarget = new TargetCreaturePermanent();
Ability ability = new EnchantAbility(auraTarget);
ability.addTarget(auraTarget);
ability.addEffect(new AttachEffect(Outcome.BoostCreature));
this.addAbility(ability);
// Enchanted creature is 1/1.
this.addAbility(new SimpleStaticAbility(new SetBasePowerToughnessEnchantedEffect(1, 1)));
}
private CursedRoleToken(final CursedRoleToken token) {
super(token);
}
public CursedRoleToken copy() {
return new CursedRoleToken(this);
}
}

View file

@ -0,0 +1,49 @@
package mage.game.permanent.token;
import mage.abilities.Ability;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.common.AttachEffect;
import mage.abilities.effects.common.continuous.BoostEnchantedEffect;
import mage.abilities.effects.common.continuous.GainAbilityAttachedEffect;
import mage.abilities.keyword.EnchantAbility;
import mage.abilities.keyword.TrampleAbility;
import mage.constants.AttachmentType;
import mage.constants.CardType;
import mage.constants.Outcome;
import mage.constants.SubType;
import mage.target.TargetPermanent;
import mage.target.common.TargetCreaturePermanent;
/**
* @author TheElk801
*/
public final class MonsterRoleToken extends TokenImpl {
public MonsterRoleToken() {
super("Monster", "Monster Role token");
cardType.add(CardType.ENCHANTMENT);
subtype.add(SubType.AURA);
subtype.add(SubType.ROLE);
TargetPermanent auraTarget = new TargetCreaturePermanent();
Ability ability = new EnchantAbility(auraTarget);
ability.addTarget(auraTarget);
ability.addEffect(new AttachEffect(Outcome.BoostCreature));
this.addAbility(ability);
// Enchanted creature gets +1/+1 and has trample
ability = new SimpleStaticAbility(new BoostEnchantedEffect(1, 1));
ability.addEffect(new GainAbilityAttachedEffect(
TrampleAbility.getInstance(), AttachmentType.AURA
).setText("and has trample"));
this.addAbility(ability);
}
private MonsterRoleToken(final MonsterRoleToken token) {
super(token);
}
public MonsterRoleToken copy() {
return new MonsterRoleToken(this);
}
}

View file

@ -0,0 +1,50 @@
package mage.game.permanent.token;
import mage.abilities.Ability;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.costs.mana.GenericManaCost;
import mage.abilities.effects.common.AttachEffect;
import mage.abilities.effects.common.continuous.BoostEnchantedEffect;
import mage.abilities.effects.common.continuous.GainAbilityAttachedEffect;
import mage.abilities.keyword.EnchantAbility;
import mage.abilities.keyword.WardAbility;
import mage.constants.AttachmentType;
import mage.constants.CardType;
import mage.constants.Outcome;
import mage.constants.SubType;
import mage.target.TargetPermanent;
import mage.target.common.TargetCreaturePermanent;
/**
* @author TheElk801
*/
public final class RoyalRoleToken extends TokenImpl {
public RoyalRoleToken() {
super("Royal", "Royal Role token");
cardType.add(CardType.ENCHANTMENT);
subtype.add(SubType.AURA);
subtype.add(SubType.ROLE);
TargetPermanent auraTarget = new TargetCreaturePermanent();
Ability ability = new EnchantAbility(auraTarget);
ability.addTarget(auraTarget);
ability.addEffect(new AttachEffect(Outcome.BoostCreature));
this.addAbility(ability);
// Enchanted creature gets +1/+1 and has ward {1}
ability = new SimpleStaticAbility(new BoostEnchantedEffect(1, 1));
ability.addEffect(new GainAbilityAttachedEffect(
new WardAbility(new GenericManaCost(1), false), AttachmentType.AURA
).setText("and has ward {1}"));
this.addAbility(ability);
}
private RoyalRoleToken(final RoyalRoleToken token) {
super(token);
}
public RoyalRoleToken copy() {
return new RoyalRoleToken(this);
}
}

View file

@ -0,0 +1,52 @@
package mage.game.permanent.token;
import mage.abilities.Ability;
import mage.abilities.common.AttacksTriggeredAbility;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.common.AttachEffect;
import mage.abilities.effects.common.continuous.BoostEnchantedEffect;
import mage.abilities.effects.common.continuous.GainAbilityAttachedEffect;
import mage.abilities.effects.keyword.ScryEffect;
import mage.abilities.keyword.EnchantAbility;
import mage.constants.AttachmentType;
import mage.constants.CardType;
import mage.constants.Outcome;
import mage.constants.SubType;
import mage.target.TargetPermanent;
import mage.target.common.TargetCreaturePermanent;
/**
* @author TheElk801
*/
public final class SorcererRoleToken extends TokenImpl {
public SorcererRoleToken() {
super("Sorcerer", "Sorcerer Role token");
cardType.add(CardType.ENCHANTMENT);
subtype.add(SubType.AURA);
subtype.add(SubType.ROLE);
TargetPermanent auraTarget = new TargetCreaturePermanent();
Ability ability = new EnchantAbility(auraTarget);
ability.addTarget(auraTarget);
ability.addEffect(new AttachEffect(Outcome.BoostCreature));
this.addAbility(ability);
// Enchanted creature gets +1/+1 and has "Whenever this creature attacks, scry 1."
ability = new SimpleStaticAbility(new BoostEnchantedEffect(1, 1));
ability.addEffect(new GainAbilityAttachedEffect(
new AttacksTriggeredAbility(new ScryEffect(1, false))
.setTriggerPhrase("Whenever this creature attacks, "),
AttachmentType.AURA
).setText("and has \"Whenever this creature attacks, scry 1.\""));
this.addAbility(ability);
}
private SorcererRoleToken(final SorcererRoleToken token) {
super(token);
}
public SorcererRoleToken copy() {
return new SorcererRoleToken(this);
}
}

View file

@ -0,0 +1,47 @@
package mage.game.permanent.token;
import mage.abilities.Ability;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.dynamicvalue.common.PermanentsOnBattlefieldCount;
import mage.abilities.effects.common.AttachEffect;
import mage.abilities.effects.common.continuous.BoostEnchantedEffect;
import mage.abilities.keyword.EnchantAbility;
import mage.constants.CardType;
import mage.constants.Outcome;
import mage.constants.SubType;
import mage.filter.StaticFilters;
import mage.target.TargetPermanent;
import mage.target.common.TargetCreaturePermanent;
/**
* @author TheElk801
*/
public final class VirtuousRoleToken extends TokenImpl {
private static final DynamicValue xValue = new PermanentsOnBattlefieldCount(StaticFilters.FILTER_CONTROLLED_PERMANENT_ENCHANTMENT);
public VirtuousRoleToken() {
super("Virtuous", "Virtuous Role token");
cardType.add(CardType.ENCHANTMENT);
subtype.add(SubType.AURA);
subtype.add(SubType.ROLE);
TargetPermanent auraTarget = new TargetCreaturePermanent();
Ability ability = new EnchantAbility(auraTarget);
ability.addTarget(auraTarget);
ability.addEffect(new AttachEffect(Outcome.BoostCreature));
this.addAbility(ability);
// Enchanted creature gets +1/+1 for each enchantment you control.
this.addAbility(new SimpleStaticAbility(new BoostEnchantedEffect(xValue, xValue)));
}
private VirtuousRoleToken(final VirtuousRoleToken token) {
super(token);
}
public VirtuousRoleToken copy() {
return new VirtuousRoleToken(this);
}
}

View file

@ -0,0 +1,48 @@
package mage.game.permanent.token;
import mage.abilities.Ability;
import mage.abilities.common.PutIntoGraveFromBattlefieldSourceTriggeredAbility;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.effects.common.AttachEffect;
import mage.abilities.effects.common.LoseLifeOpponentsEffect;
import mage.abilities.effects.common.continuous.BoostEnchantedEffect;
import mage.abilities.keyword.EnchantAbility;
import mage.constants.CardType;
import mage.constants.Outcome;
import mage.constants.SubType;
import mage.target.TargetPermanent;
import mage.target.common.TargetCreaturePermanent;
/**
* @author TheElk801
*/
public final class WickedRoleToken extends TokenImpl {
public WickedRoleToken() {
super("Wicked", "Wicked Role token");
cardType.add(CardType.ENCHANTMENT);
subtype.add(SubType.AURA);
subtype.add(SubType.ROLE);
TargetPermanent auraTarget = new TargetCreaturePermanent();
Ability ability = new EnchantAbility(auraTarget);
ability.addTarget(auraTarget);
ability.addEffect(new AttachEffect(Outcome.BoostCreature));
this.addAbility(ability);
// Enchanted creature gets +1/+1
this.addAbility(new SimpleStaticAbility(new BoostEnchantedEffect(1, 1)));
// When this Aura is put into a graveyard from the battlefield, each opponent loses 1 life."
this.addAbility(new PutIntoGraveFromBattlefieldSourceTriggeredAbility(new LoseLifeOpponentsEffect(1))
.setTriggerPhrase("When this Aura is put into a graveyard from the battlefield, "));
}
private WickedRoleToken(final WickedRoleToken token) {
super(token);
}
public WickedRoleToken copy() {
return new WickedRoleToken(this);
}
}

View file

@ -0,0 +1,62 @@
package mage.game.permanent.token;
import mage.abilities.Ability;
import mage.abilities.common.AttacksTriggeredAbility;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.condition.Condition;
import mage.abilities.condition.common.SourceMatchesFilterCondition;
import mage.abilities.decorator.ConditionalInterveningIfTriggeredAbility;
import mage.abilities.effects.common.AttachEffect;
import mage.abilities.effects.common.continuous.GainAbilityAttachedEffect;
import mage.abilities.effects.common.counter.AddCountersSourceEffect;
import mage.abilities.keyword.EnchantAbility;
import mage.constants.*;
import mage.counters.CounterType;
import mage.filter.FilterPermanent;
import mage.filter.common.FilterCreaturePermanent;
import mage.filter.predicate.mageobject.ToughnessPredicate;
import mage.target.TargetPermanent;
import mage.target.common.TargetCreaturePermanent;
/**
* @author TheElk801
*/
public final class YoungHeroRoleToken extends TokenImpl {
private static final FilterPermanent filter = new FilterCreaturePermanent();
static {
filter.add(new ToughnessPredicate(ComparisonType.FEWER_THAN, 4));
}
private static final Condition condition = new SourceMatchesFilterCondition(filter);
public YoungHeroRoleToken() {
super("Young Hero", "Young Hero Role token");
cardType.add(CardType.ENCHANTMENT);
subtype.add(SubType.AURA);
subtype.add(SubType.ROLE);
TargetPermanent auraTarget = new TargetCreaturePermanent();
Ability ability = new EnchantAbility(auraTarget);
ability.addTarget(auraTarget);
ability.addEffect(new AttachEffect(Outcome.BoostCreature));
this.addAbility(ability);
// Enchanted creature has "Whenever this creature attacks, if its toughness is 3 or less, put a +1/+1 counter on it."
this.addAbility(new SimpleStaticAbility(new GainAbilityAttachedEffect(
new ConditionalInterveningIfTriggeredAbility(
new AttacksTriggeredAbility(new AddCountersSourceEffect(CounterType.P1P1.createInstance())),
condition, "Whenever this creature attacks, if its toughness is 3 or less, put a +1/+1 counter on it."
), AttachmentType.AURA
)));
}
private YoungHeroRoleToken(final YoungHeroRoleToken token) {
super(token);
}
public YoungHeroRoleToken copy() {
return new YoungHeroRoleToken(this);
}
}