Ability refactor: new code to search abilities in cards and permanents;

This commit is contained in:
Oleg Agafonov 2020-05-28 22:34:27 +04:00
parent 978118148b
commit 8af43dc13a
31 changed files with 85 additions and 47 deletions

View file

@ -1321,7 +1321,7 @@ public class ComputerPlayer extends PlayerImpl implements Player {
if (ability != null && ability.canActivate(playerId, game).canActivate()
&& !game.getContinuousEffects().preventedByRuleModification(GameEvent.getEvent(GameEvent.EventType.CAST_SPELL, ability.getSourceId(), ability.getSourceId(), playerId), ability, game, true)) {
if (card.getCardType().contains(CardType.INSTANT)
|| card.hasAbility(FlashAbility.getInstance().getId(), game)) {
|| card.hasAbility(FlashAbility.getInstance(), game)) {
playableInstant.add(card);
} else {
playableNonInstant.add(card);

View file

@ -82,7 +82,7 @@ class ChaosphereEffect extends RestrictionEffect {
if (attacker == null) {
return true;
}
return attacker.hasAbility(FlyingAbility.getInstance().getId(), game);
return attacker.hasAbility(FlyingAbility.getInstance(), game);
}
@Override

View file

@ -71,7 +71,7 @@ class CrimsonRocTriggeredAbility extends TriggeredAbilityImpl {
Permanent permanent = game.getPermanent(event.getTargetId());
return permanent != null
&& permanent.isCreature()
&& !permanent.getAbilities(game).contains(FlyingAbility.getInstance());
&& !permanent.hasAbility(FlyingAbility.getInstance(), game);
}
@Override

View file

@ -66,7 +66,7 @@ class DenseCanopyCantBlockEffect extends RestrictionEffect {
if (attacker == null) {
return true;
}
return attacker.hasAbility(FlyingAbility.getInstance().getId(), game);
return attacker.hasAbility(FlyingAbility.getInstance(), game);
}
@Override

View file

@ -75,6 +75,6 @@ enum MomentumRumblerCondition implements Condition {
if (permanent == null) {
return false;
}
return hasAbility == permanent.getAbilities(game).containsKey(FirstStrikeAbility.getInstance().getId());
return hasAbility == permanent.hasAbility(FirstStrikeAbility.getInstance(), game);
}
}

View file

@ -82,7 +82,7 @@ class QuartzwoodCrasherTriggeredAbility extends TriggeredAbilityImpl {
&& ((DamagedPlayerEvent) event).isCombatDamage()) {
Permanent creature = game.getPermanent(event.getSourceId());
if (creature != null && creature.isControlledBy(controllerId)
&& creature.hasAbility(TrampleAbility.getInstance().getId(), game)
&& creature.hasAbility(TrampleAbility.getInstance(), game)
&& !damagedPlayerIds.contains(event.getTargetId())) {
damagedPlayerIds.add(event.getTargetId());
this.getEffects().setTargetPointer(new FixedTarget(event.getTargetId(), game));
@ -143,7 +143,7 @@ class QuartzwoodCrasherWatcher extends Watcher {
return;
}
Permanent creature = game.getPermanent(event.getSourceId());
if (creature == null || !creature.getAbilities(game).containsKey(TrampleAbility.getInstance().getId())) {
if (creature == null || !creature.hasAbility(TrampleAbility.getInstance(), game)) {
return;
}
damageMap

View file

@ -88,7 +88,7 @@ class ReturnFromExtinctionTarget extends TargetCardInYourGraveyard {
return false;
}
for (Card card : player.getGraveyard().getCards(filter, sourceId, sourceControllerId, game)) {
if (card.isAllCreatureTypes() || card.getAbilities(game).contains(ChangelingAbility.getInstance())) {
if (card.isAllCreatureTypes() || card.hasAbility(ChangelingAbility.getInstance(), game)) {
if (!subTypes.isEmpty()) {
return true;
} else {

View file

@ -80,8 +80,8 @@ class SidarKondoOfJamuraaCantBlockCreaturesSourceEffect extends RestrictionEffec
@Override
public boolean applies(Permanent permanent, Ability source, Game game) {
if (permanent.hasAbility(FlyingAbility.getInstance().getId(), game)
|| permanent.hasAbility(ReachAbility.getInstance().getId(), game)) {
if (permanent.hasAbility(FlyingAbility.getInstance(), game)
|| permanent.hasAbility(ReachAbility.getInstance(), game)) {
return false;
}
return game.getOpponents(source.getControllerId()).contains(permanent.getControllerId());

View file

@ -61,7 +61,7 @@ enum SlingbowTrapCondition implements Condition {
for (UUID attackingCreatureId : game.getCombat().getAttackers()) {
Permanent attackingCreature = game.getPermanent(attackingCreatureId);
if (attackingCreature != null) {
if (attackingCreature.getColor(game).isBlack() && attackingCreature.hasAbility(FlyingAbility.getInstance().getId(), game)) {
if (attackingCreature.getColor(game).isBlack() && attackingCreature.hasAbility(FlyingAbility.getInstance(), game)) {
return true;
}
}

View file

@ -91,7 +91,7 @@ public final class StormtideLeviathan extends CardImpl {
// land abilities are intrinsic, so add them here, not in layer 6
if (!land.hasSubtype(SubType.ISLAND, game)) {
land.getSubtype(game).add(SubType.ISLAND);
if (!land.getAbilities(game).contains(new BlueManaAbility())) {
if (!land.getAbilities(game).containsClass(BlueManaAbility.class)) {
land.addAbility(new BlueManaAbility(), source.getSourceId(), game);
}
}

View file

@ -150,7 +150,7 @@ class TaigamOjutaiMasterGainReboundEffect extends ContinuousEffectImpl {
}
private void addReboundAbility(Card card, Ability source, Game game) {
boolean found = card.getAbilities(game).stream().anyMatch(ability -> ability instanceof ReboundAbility);
boolean found = card.getAbilities(game).containsClass(ReboundAbility.class);
if (!found) {
Ability ability = new ReboundAbility();
game.getState().addOtherAbility(card, ability);

View file

@ -113,7 +113,6 @@ public class CommandersCastTest extends CardTestCommander4Players {
waitStackResolved(5, PhaseStep.POSTCOMBAT_MAIN);
checkPermanentCount("after cast 2", 5, PhaseStep.POSTCOMBAT_MAIN, playerA, "Academy Ruins", 1);
// showBattlefield("end battlefield", 5, PhaseStep.END_TURN, playerA);
setStopAt(5, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();

View file

@ -40,7 +40,7 @@ public class WonderTest extends CardTestPlayerBase {
// check no flying in graveyard
for (Card card : playerA.getGraveyard().getCards(currentGame)) {
if (card.getName().equals("Runeclaw Bear")) {
Assert.assertFalse(card.getAbilities().contains(FlyingAbility.getInstance()));
Assert.assertFalse(card.hasAbility(FlyingAbility.getInstance(), currentGame));
}
}
}

View file

@ -41,9 +41,13 @@ public interface MageObject extends MageItem, Serializable {
Set<SuperType> getSuperType();
/**
* For cards: return basic abilities (without dynamic added)
* For permanents: return all abilities (dynamic ability inserts into permanent)
*/
Abilities<Ability> getAbilities();
boolean hasAbility(UUID abilityId, Game game);
boolean hasAbility(Ability ability, Game game);
ObjectColor getColor(Game game);
@ -180,9 +184,9 @@ public interface MageObject extends MageItem, Serializable {
}
if (this.isCreature() && otherCard.isCreature()) {
if (this.getAbilities().contains(ChangelingAbility.getInstance())
if (this.hasAbility(ChangelingAbility.getInstance(), game)
|| this.isAllCreatureTypes()
|| otherCard.getAbilities().contains(ChangelingAbility.getInstance())
|| otherCard.hasAbility(ChangelingAbility.getInstance(), game)
|| otherCard.isAllCreatureTypes()) {
return true;
}

View file

@ -132,12 +132,12 @@ public abstract class MageObjectImpl implements MageObject {
}
@Override
public boolean hasAbility(UUID abilityId, Game game) {
if (this.getAbilities().containsKey(abilityId)) {
public boolean hasAbility(Ability ability, Game game) {
if (this.getAbilities().contains(ability)) {
return true;
}
Abilities<Ability> otherAbilities = game.getState().getAllOtherAbilities(getId());
return otherAbilities != null && otherAbilities.containsKey(abilityId);
return otherAbilities != null && otherAbilities.contains(ability);
}
@Override

View file

@ -71,7 +71,7 @@ public class SpellAbility extends ActivatedAbilityImpl {
}
return null != game.getContinuousEffects().asThough(sourceId, AsThoughEffectType.CAST_AS_INSTANT, this, playerId, game) // check this first to allow Offering in main phase
|| timing == TimingRule.INSTANT
|| object.hasAbility(FlashAbility.getInstance().getId(), game)
|| object.hasAbility(FlashAbility.getInstance(), game)
|| game.canPlaySorcery(playerId);
}

View file

@ -27,10 +27,10 @@ public enum SuspendedCondition implements Condition {
public boolean apply(Game game, Ability source) {
Card card = game.getCard(source.getSourceId());
if (card != null) {
boolean found = card.getAbilities().stream().anyMatch(ability -> ability instanceof SuspendAbility);
boolean found = card.getAbilities(game).containsClass(SuspendAbility.class);
if (!found) {
found = game.getState().getAllOtherAbilities(source.getSourceId()).stream().anyMatch(ability -> ability instanceof SuspendAbility);
found = game.getState().getAllOtherAbilities(source.getSourceId()).containsClass(SuspendAbility.class);
}
if (found) {

View file

@ -64,7 +64,7 @@ public class GainAbilitySpellsEffect extends ContinuousEffectImpl {
if (stackObject.isControlledBy(source.getControllerId())) {
Card card = game.getCard(stackObject.getSourceId());
if (card != null && filter.match(card, game)) {
if (!card.getAbilities().contains(ability)) {
if (!card.hasAbility(ability, game)) {
game.getState().addOtherAbility(card, ability);
}
}

View file

@ -33,7 +33,7 @@ public class CanBlockOnlyFlyingAttachedEffect extends RestrictionEffect {
if (attacker == null) {
return true;
}
return attacker.getAbilities().contains(FlyingAbility.getInstance());
return attacker.hasAbility(FlyingAbility.getInstance(), game);
}
@Override

View file

@ -33,7 +33,7 @@ public class CanBlockOnlyFlyingEffect extends RestrictionEffect {
if (attacker == null) {
return true;
}
return attacker.getAbilities().contains(FlyingAbility.getInstance());
return attacker.hasAbility(FlyingAbility.getInstance(), game);
}
@Override

View file

@ -51,7 +51,7 @@ public class GainAbilityControlledSpellsEffect extends ContinuousEffectImpl {
if ((stackObject instanceof Spell) && !stackObject.isCopy() && stackObject.isControlledBy(source.getControllerId())) {
Spell spell = (Spell) stackObject;
if (filter.match(spell, game)) {
if (!spell.getAbilities().contains(ability)) {
if (!spell.hasAbility(ability, game)) {
game.getState().addOtherAbility(spell.getCard(), ability);
}
}

View file

@ -213,7 +213,7 @@ public class SuspendAbility extends SpecialAction {
}
MageObject object = game.getObject(sourceId);
return new ActivationStatus(object.isInstant()
|| object.hasAbility(FlashAbility.getInstance().getId(), game)
|| object.hasAbility(FlashAbility.getInstance(), game)
|| null != game.getContinuousEffects().asThough(sourceId,
AsThoughEffectType.CAST_AS_INSTANT, this, playerId, game)
|| game.canPlaySorcery(playerId), null);
@ -356,6 +356,13 @@ class SuspendPlayCardEffect extends OneShotEffect {
}
}
// remove the abilities from the card
// TODO: will not work with Adventure Cards and another auto-generated abilities list
// TODO: is it work after blink or return to hand?
/*
bug example:
Epochrasite bug: It comes out of suspend, is cast and enters the battlefield. THEN if it's returned to
its owner's hand from battlefield, the bounced Epochrasite can't be cast for the rest of the game.
*/
card.getAbilities().removeAll(abilitiesToRemove);
}
// cast the card for free

View file

@ -285,7 +285,7 @@ public abstract class CardImpl extends MageObjectImpl implements Card {
/**
* Gets all current abilities - includes additional abilities added by other
* cards or effects
* cards or effects. Warning, you can't modify that list.
*
* @param game
* @return A list of {@link Ability} - this collection is not modifiable
@ -295,15 +295,23 @@ public abstract class CardImpl extends MageObjectImpl implements Card {
if (game == null) {
return abilities; // deck editor with empty game
}
CardState cardState = game.getState().getCardState(this.getId());
if (!cardState.hasLostAllAbilities() && (cardState.getAbilities() == null || cardState.getAbilities().isEmpty())) {
if (cardState == null) {
return abilities;
}
// collects all abilities
Abilities<Ability> all = new AbilitiesImpl<>();
// basic
if (!cardState.hasLostAllAbilities()) {
all.addAll(abilities);
}
// dynamic
all.addAll(cardState.getAbilities());
return all;
}
@ -314,6 +322,12 @@ public abstract class CardImpl extends MageObjectImpl implements Card {
cardState.getAbilities().clear();
}
@Override
public boolean hasAbility(Ability ability, Game game) {
// getAbilities(game) searches all abilities from base and dynamic lists (other)
return this.getAbilities(game).contains(ability);
}
/**
* Public in order to support adding abilities to SplitCardHalf's
*

View file

@ -171,8 +171,8 @@ public abstract class Designation implements MageObject {
}
@Override
public boolean hasAbility(UUID abilityId, Game game) {
return abilites.containsKey(abilityId);
public boolean hasAbility(Ability ability, Game game) {
return this.getAbilities().contains(ability);
}
@Override

View file

@ -306,7 +306,7 @@ public class Combat implements Serializable, Copyable<Combat> {
|| getAttackers().size() <= 1) {
return;
}
boolean canBand = attacker.getAbilities().containsKey(BandingAbility.getInstance().getId());
boolean canBand = attacker.hasAbility(BandingAbility.getInstance(), game);
List<Ability> bandsWithOther = new ArrayList<>();
for (Ability ability : attacker.getAbilities()) {
if (ability.getClass().equals(BandsWithOtherAbility.class)) {
@ -390,7 +390,7 @@ public class Combat implements Serializable, Copyable<Combat> {
permanent.addBandedCard(creatureId);
attacker.addBandedCard(targetId);
if (canBand) {
if (!permanent.getAbilities().containsKey(BandingAbility.getInstance().getId())) {
if (!permanent.hasAbility(BandingAbility.getInstance(), game)) {
filter.add(new AbilityPredicate(BandingAbility.class));
canBandWithOther = false;
}
@ -1289,7 +1289,8 @@ public class Combat implements Serializable, Copyable<Combat> {
if (attacker != null) {
if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.DECLARE_ATTACKER, defenderId, creatureId, playerId))) {
if (addAttackerToCombat(creatureId, defenderId, game)) {
if (!attacker.getAbilities().containsKey(VigilanceAbility.getInstance().getId()) && !attacker.getAbilities().containsKey(JohanVigilanceAbility.getInstance().getId())) {
if (!attacker.hasAbility(VigilanceAbility.getInstance(), game)
&& !attacker.hasAbility(JohanVigilanceAbility.getInstance(), game)) {
if (!attacker.isTapped()) {
attacker.setTapped(true);
attackersTappedByAttack.add(attacker.getId());

View file

@ -158,12 +158,12 @@ public class Commander implements CommandObject {
}
@Override
public boolean hasAbility(UUID abilityId, Game game) {
if (this.getAbilities().containsKey(abilityId)) {
public boolean hasAbility(Ability ability, Game game) {
if (this.getAbilities().contains(ability)) {
return true;
}
Abilities<Ability> otherAbilities = game.getState().getAllOtherAbilities(getId());
return otherAbilities != null && otherAbilities.containsKey(abilityId);
return otherAbilities != null && otherAbilities.contains(ability);
}
@Override

View file

@ -173,8 +173,8 @@ public class Emblem implements CommandObject {
}
@Override
public boolean hasAbility(UUID abilityId, Game game) {
return abilites.containsKey(abilityId);
public boolean hasAbility(Ability ability, Game game) {
return getAbilities().contains(ability);
}
@Override

View file

@ -182,8 +182,8 @@ public class Plane implements CommandObject {
}
@Override
public boolean hasAbility(UUID abilityId, Game game) {
return abilites.containsKey(abilityId);
public boolean hasAbility(Ability ability, Game game) {
return getAbilities().contains(ability);
}
@Override

View file

@ -120,7 +120,7 @@ class TrailOfTheMageRingsReboundEffect extends ContinuousEffectImpl {
private void addReboundAbility(Card card, Ability source, Game game) {
if (filter.match(card, game)) {
boolean found = card.getAbilities().stream().anyMatch(ability -> ability instanceof ReboundAbility);
boolean found = card.getAbilities(game).containsClass(ReboundAbility.class);
if (!found) {
Ability ability = new ReboundAbility();
game.getState().addOtherAbility(card, ability);

View file

@ -524,8 +524,8 @@ public class Spell extends StackObjImpl implements Card {
}
@Override
public boolean hasAbility(UUID abilityId, Game game) {
return card.hasAbility(abilityId, game);
public boolean hasAbility(Ability ability, Game game) {
return card.hasAbility(ability, game);
}
@Override

View file

@ -178,7 +178,7 @@ public class StackAbility extends StackObjImpl implements Ability {
}
@Override
public boolean hasAbility(UUID abilityId, Game game) {
public boolean hasAbility(Ability ability, Game game) {
return false;
}
@ -672,4 +672,17 @@ public class StackAbility extends StackObjImpl implements Ability {
public Outcome getCustomOutcome() {
return this.ability.getCustomOutcome();
}
@Override
public boolean isSameInstance(Ability ability) {
// same instance (by mtg rules) = same object, ID or class+text (you can't check class only cause it can be different by params/text)
if (ability == null) {
return false;
}
return (this == ability)
|| (this.getId().equals(ability.getId()))
|| (this.getOriginalId().equals(ability.getOriginalId()))
|| (this.getClass() == ability.getClass() && this.getRule().equals(ability.getRule()));
}
}