Implementing Daybound/Nightbound mechanic (#8200)

* adding initial day/night support in game state

* remove card exclusion for testing

* added functional implementation to abilities from main branch

* functionally implemented NightCondition

* updated DayNightHint

* added support for nightbound entering transformed at night

* [MID] Implemented Unnatural Moonrise

* [MID] Implemented The Celestus

* added some docs

* changed access for state day/night methods

* added transformation to day/night switch

* re-added unfinished filter, removed day/night cards

* fixed some errors with transforming

* added hints to all day/night cards

* added transformation prevention plus a test

* added Immerwolf test

* [MID] Implemented Tovolar, Dire Overlord / Tovolar, The Midnight Scourge

* refactored some cards to not use isTransformable

* removed transformable parameter

* simplified some transform code

* fixed null pointer exception

* removed unnecessary canTransform method

* fixed a small error

* reworked implementation of rule 701.28f

* small change in transform logic

* fixed failiing test

* fixed verify failure

* small merge change

* added support for day/night switching based on spells cast

* [MID] Implemented Curse of Leeches / Leeching Lurkers

* moved day/night handling to untap step

* added tests for cards which set day and trigger from a change

* [MID] Implemented Ludevic, Necrogenius / Olag, Ludevic's Hubris

* added support for creatures transforming to match day/night when necessary

* fixed verify failures

* fixed another verify failure

* remove temporary verify skip

* added transform message

* removed unnecessary transform message

* [MID] Implemented Angelic Enforcer / Enduring Angel

* updated DayNightHint with more information

* fixed verify failure

* merge fix

* fixed Startled Awake / Persistent Nightmare / Moonmist interaction

* added another test for Moonmist

* merge fix

* merge fix

* [MID] Implemented Baneblade Scoundrel / Baneclaw Marauder

* merge fix

* [MID] various text fixes

* [MID] a few more text fixes

* Merge fix

* Improved transform game logs (hints, source), fixed day/night logs, fixed miss game param (due code style);

* fixed a test failure

* Merge fix

Co-authored-by: Oleg Agafonov <jaydi85@gmail.com>
This commit is contained in:
Evan Kranzler 2021-11-05 15:11:23 -04:00 committed by GitHub
parent 6d4e5672c3
commit 30afb11cd2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
305 changed files with 2174 additions and 1064 deletions

View file

@ -531,6 +531,10 @@ public interface Ability extends Controllable, Serializable {
*/
Permanent getSourcePermanentOrLKI(Game game);
void setSourcePermanentTransformCount(Game game);
boolean checkTransformCount(Permanent permanent, Game game);
String getTargetDescription(Targets targets, Game game);
void setCanFizzle(boolean canFizzle);

View file

@ -80,6 +80,7 @@ public abstract class AbilityImpl implements Ability {
protected Outcome customOutcome = null; // uses for AI decisions instead effects
protected MageIdentifier identifier; // used to identify specific ability (e.g. to match with corresponding watcher)
protected String appendToRule = null;
protected int sourcePermanentTransformCount = 0;
public AbilityImpl(AbilityType abilityType, Zone zone) {
this.id = UUID.randomUUID();
@ -135,6 +136,7 @@ public abstract class AbilityImpl implements Ability {
this.identifier = ability.identifier;
this.activated = ability.activated;
this.appendToRule = ability.appendToRule;
this.sourcePermanentTransformCount = ability.sourcePermanentTransformCount;
}
@Override
@ -246,6 +248,7 @@ public abstract class AbilityImpl implements Ability {
if (getSourceObjectZoneChangeCounter() == 0) {
setSourceObjectZoneChangeCounter(game.getState().getZoneChangeCounter(getSourceId()));
}
setSourcePermanentTransformCount(game);
/* 20130201 - 601.2b
* If the player wishes to splice any cards onto the spell (see rule 702.45), he
@ -1292,6 +1295,24 @@ public abstract class AbilityImpl implements Ability {
return sourceObjectZoneChangeCounter;
}
@Override
public void setSourcePermanentTransformCount(Game game) {
Permanent permanent = getSourcePermanentOrLKI(game);
if (permanent != null) {
this.sourcePermanentTransformCount = permanent.getTransformCount();
}
}
@Override
public boolean checkTransformCount(Permanent permanent, Game game) {
if (permanent == null
|| !permanent.getId().equals(sourceId)
|| permanent.getZoneChangeCounter(game) != sourceObjectZoneChangeCounter) {
return true;
}
return permanent.getTransformCount() == sourcePermanentTransformCount;
}
@Override
public boolean canFizzle() {
return canFizzle;

View file

@ -2,17 +2,18 @@ package mage.abilities.common;
import mage.abilities.Ability;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.hint.common.DayNightHint;
import mage.constants.Outcome;
import mage.game.Game;
/**
* @author TheElk801
* TODO: this is just a placeholder for the actual ability
*/
public class BecomeDayAsEntersAbility extends EntersBattlefieldAbility {
public BecomeDayAsEntersAbility() {
super(new BecomeDayEffect());
this.addHint(DayNightHint.instance);
}
private BecomeDayAsEntersAbility(final BecomeDayAsEntersAbility ability) {
@ -33,7 +34,7 @@ public class BecomeDayAsEntersAbility extends EntersBattlefieldAbility {
class BecomeDayEffect extends OneShotEffect {
BecomeDayEffect() {
super(Outcome.Benefit);
super(Outcome.Neutral);
}
private BecomeDayEffect(final BecomeDayEffect effect) {
@ -47,6 +48,10 @@ class BecomeDayEffect extends OneShotEffect {
@Override
public boolean apply(Game game, Ability source) {
return true;
if (!game.hasDayNight()) {
game.setDaytime(true);
return true;
}
return false;
}
}

View file

@ -8,7 +8,6 @@ import mage.game.events.GameEvent;
/**
* @author TheElk801
* TODO: this is just a placeholder for the actual ability
*/
public class BecomesDayOrNightTriggeredAbility extends TriggeredAbilityImpl {
@ -26,7 +25,7 @@ public class BecomesDayOrNightTriggeredAbility extends TriggeredAbilityImpl {
@Override
public boolean checkEventType(GameEvent event, Game game) {
return false;
return event.getType() == GameEvent.EventType.BECOMES_DAY_NIGHT;
}
@Override

View file

@ -11,7 +11,7 @@ import mage.game.Game;
public class WerewolfBackTriggeredAbility extends BeginningOfUpkeepTriggeredAbility {
public WerewolfBackTriggeredAbility() {
super(new TransformSourceEffect(false), TargetController.ANY, false);
super(new TransformSourceEffect(), TargetController.ANY, false);
}
private WerewolfBackTriggeredAbility(final WerewolfBackTriggeredAbility ability) {

View file

@ -11,7 +11,7 @@ import mage.game.Game;
public class WerewolfFrontTriggeredAbility extends BeginningOfUpkeepTriggeredAbility {
public WerewolfFrontTriggeredAbility() {
super(new TransformSourceEffect(true), TargetController.ANY, false);
super(new TransformSourceEffect(), TargetController.ANY, false);
}
private WerewolfFrontTriggeredAbility(final WerewolfFrontTriggeredAbility ability) {

View file

@ -6,14 +6,13 @@ import mage.game.Game;
/**
* @author TheElk801
* TODO: Implement this
*/
public enum NightCondition implements Condition {
instance;
@Override
public boolean apply(Game game, Ability source) {
return false;
return game.checkDayNight(false);
}
@Override

View file

@ -1,43 +1,23 @@
package mage.abilities.effects.common;
import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.effects.OneShotEffect;
import mage.cards.Card;
import mage.constants.Outcome;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.game.permanent.PermanentCard;
/**
*
* @author nantuko
*/
public class TransformSourceEffect extends OneShotEffect {
private boolean withoutTrigger;
private boolean fromDayToNight;
/**
* @param fromDayToNight Defines whether we transform from "day" side to
* "night" or vice versa.
*/
public TransformSourceEffect(boolean fromDayToNight) {
this(fromDayToNight, false);
}
public TransformSourceEffect(boolean fromDayToNight, boolean withoutTrigger) {
public TransformSourceEffect() {
super(Outcome.Transform);
this.withoutTrigger = withoutTrigger;
this.fromDayToNight = fromDayToNight;
staticText = "transform {this}";
}
public TransformSourceEffect(final TransformSourceEffect effect) {
super(effect);
this.withoutTrigger = effect.withoutTrigger;
this.fromDayToNight = effect.fromDayToNight;
}
@Override
@ -47,37 +27,8 @@ public class TransformSourceEffect extends OneShotEffect {
@Override
public boolean apply(Game game, Ability source) {
MageObject sourceObject = source.getSourceObjectIfItStillExists(game); // Transform only if it's the same object as the effect was put on the stack
if (sourceObject instanceof Permanent) {
Permanent sourcePermanent = (Permanent) sourceObject;
if (sourcePermanent.canTransform(source, game)) {
// check not to transform twice the same side
if (sourcePermanent.isTransformed() != fromDayToNight) {
if (withoutTrigger) {
sourcePermanent.setTransformed(fromDayToNight);
} else {
if (sourcePermanent.isTransformed()) {
Card orgCard = game.getCard(source.getSourceId());
sourcePermanent.getPower().modifyBaseValue(orgCard.getPower().getValue());
sourcePermanent.getToughness().modifyBaseValue(orgCard.getToughness().getValue());
}
sourcePermanent.transform(game);
}
if (!game.isSimulation()) {
if (fromDayToNight) {
if (sourcePermanent.getSecondCardFace() != null) {
if (sourcePermanent instanceof PermanentCard) {
game.informPlayers(((PermanentCard) sourcePermanent).getCard().getLogName() + " transforms into " + sourcePermanent.getSecondCardFace().getLogName());
}
}
} else {
game.informPlayers(sourcePermanent.getSecondCardFace().getLogName() + " transforms into " + sourcePermanent.getLogName());
}
}
}
}
}
return true;
Permanent permanent = source.getSourcePermanentIfItStillExists(game);
// check not to transform twice the same side
return permanent != null && permanent.transform(source, game);
}
}

View file

@ -1,88 +0,0 @@
package mage.abilities.effects.common;
import mage.abilities.Ability;
import mage.abilities.Mode;
import mage.abilities.effects.OneShotEffect;
import mage.constants.Outcome;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.game.permanent.PermanentCard;
import mage.target.Target;
import mage.util.CardUtil;
/**
*
* @author LevelX2
*/
public class TransformTargetEffect extends OneShotEffect {
private boolean withoutTrigger;
public TransformTargetEffect() {
this(true);
}
public TransformTargetEffect(boolean withoutTrigger) {
super(Outcome.Transform);
this.withoutTrigger = withoutTrigger;
}
public TransformTargetEffect(final TransformTargetEffect effect) {
super(effect);
this.withoutTrigger = effect.withoutTrigger;
}
@Override
public TransformTargetEffect copy() {
return new TransformTargetEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Permanent permanent = game.getPermanent(getTargetPointer().getFirst(game, source));
if (permanent != null) {
if (permanent.canTransform(source, game)) {
// check not to transform twice the same side
if (withoutTrigger) {
permanent.setTransformed(!permanent.isTransformed());
} else {
permanent.transform(game);
}
if (!game.isSimulation()) {
if (permanent.isTransformed()) {
if (permanent.getSecondCardFace() != null) {
if (permanent instanceof PermanentCard) {
game.informPlayers(((PermanentCard) permanent).getCard().getLogName() + " transforms into " + permanent.getSecondCardFace().getLogName());
}
}
} else {
game.informPlayers(permanent.getSecondCardFace().getLogName() + " transforms into " + permanent.getLogName());
}
}
}
return true;
}
return false;
}
@Override
public String getText(Mode mode) {
if (staticText != null && !staticText.isEmpty()) {
return staticText;
}
if (mode.getTargets().isEmpty()) {
return "transform target";
}
Target target = mode.getTargets().get(0);
if (target.getMaxNumberOfTargets() > 1) {
if (target.getMaxNumberOfTargets() == target.getNumberOfTargets()) {
return "transform " + CardUtil.numberToText(target.getNumberOfTargets()) + " target " + target.getTargetName();
} else {
return "transform up to " + CardUtil.numberToText(target.getMaxNumberOfTargets()) + " target " + target.getTargetName();
}
} else {
return "transform target " + mode.getTargets().get(0).getTargetName();
}
}
}

View file

@ -0,0 +1,40 @@
package mage.abilities.hint.common;
import mage.abilities.Ability;
import mage.abilities.hint.Hint;
import mage.game.Game;
import mage.watchers.common.CastSpellLastTurnWatcher;
/**
* @author TheElk801
*/
public enum DayNightHint implements Hint {
instance;
@Override
public String getText(Game game, Ability ability) {
if (!game.hasDayNight()) {
return "It's neither day nor night.";
}
boolean isDay = game.checkDayNight(true);
int spellsThisTurn = game
.getState()
.getWatcher(CastSpellLastTurnWatcher.class)
.getActivePlayerThisTurnCount();
StringBuilder sb = new StringBuilder("It's currently ");
sb.append(isDay ? "day" : "night");
sb.append(", active player has cast ");
sb.append(spellsThisTurn);
sb.append(" spells this turn. It will ");
sb.append((isDay ? spellsThisTurn == 0 : spellsThisTurn >= 2) ? "" : "not");
sb.append(" become ");
sb.append(isDay ? "night" : "day");
sb.append(" next turn.");
return sb.toString();
}
@Override
public DayNightHint copy() {
return this;
}
}

View file

@ -1,27 +0,0 @@
package mage.abilities.hint.common;
import mage.abilities.Ability;
import mage.abilities.condition.common.NightCondition;
import mage.abilities.hint.ConditionHint;
import mage.abilities.hint.Hint;
import mage.game.Game;
/**
* @author TheElk801
*/
public enum NightHint implements Hint {
instance;
private static final Hint hint = new ConditionHint(
NightCondition.instance, "It's currently night"
);
@Override
public String getText(Game game, Ability ability) {
return hint.getText(game, ability);
}
@Override
public Hint copy() {
return this;
}
}

View file

@ -1,16 +1,20 @@
package mage.abilities.keyword;
import mage.abilities.Ability;
import mage.abilities.StaticAbility;
import mage.constants.Zone;
import mage.abilities.effects.ContinuousEffectImpl;
import mage.abilities.hint.common.DayNightHint;
import mage.constants.*;
import mage.game.Game;
/**
* @author TheElk801
* TODO: Implement this
*/
public class DayboundAbility extends StaticAbility {
public DayboundAbility() {
super(Zone.BATTLEFIELD, null);
super(Zone.BATTLEFIELD, new DayboundEffect());
this.addHint(DayNightHint.instance);
}
private DayboundAbility(final DayboundAbility ability) {
@ -27,3 +31,27 @@ public class DayboundAbility extends StaticAbility {
return new DayboundAbility(this);
}
}
class DayboundEffect extends ContinuousEffectImpl {
DayboundEffect() {
super(Duration.WhileOnBattlefield, Layer.PlayerEffects, SubLayer.NA, Outcome.Benefit);
}
private DayboundEffect(final DayboundEffect effect) {
super(effect);
}
@Override
public DayboundEffect copy() {
return new DayboundEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
if (!game.hasDayNight()) {
game.setDaytime(true);
}
return true;
}
}

View file

@ -1,16 +1,21 @@
package mage.abilities.keyword;
import mage.abilities.Ability;
import mage.abilities.StaticAbility;
import mage.constants.Zone;
import mage.abilities.effects.ContinuousEffectImpl;
import mage.abilities.hint.common.DayNightHint;
import mage.cards.Card;
import mage.constants.*;
import mage.game.Game;
/**
* @author TheElk801
* TODO: Implement this
*/
public class NightboundAbility extends StaticAbility {
public NightboundAbility() {
super(Zone.BATTLEFIELD, null);
super(Zone.BATTLEFIELD, new NightboundEffect());
this.addHint(DayNightHint.instance);
}
private NightboundAbility(final NightboundAbility ability) {
@ -26,4 +31,34 @@ public class NightboundAbility extends StaticAbility {
public NightboundAbility copy() {
return new NightboundAbility(this);
}
public static boolean checkCard(Card card, Game game) {
return game.checkDayNight(false)
&& card.getSecondCardFace() != null
&& card.getSecondCardFace().getAbilities().containsClass(NightboundAbility.class);
}
}
class NightboundEffect extends ContinuousEffectImpl {
NightboundEffect() {
super(Duration.WhileOnBattlefield, Layer.PlayerEffects, SubLayer.NA, Outcome.Benefit);
}
private NightboundEffect(final NightboundEffect effect) {
super(effect);
}
@Override
public NightboundEffect copy() {
return new NightboundEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
if (!game.hasDayNight()) {
game.setDaytime(false);
}
return true;
}
}

View file

@ -64,7 +64,6 @@ public class TransformAbility extends SimpleStaticAbility {
}
permanent.getPower().modifyBaseValue(sourceCard.getPower().getValue());
permanent.getToughness().modifyBaseValue(sourceCard.getToughness().getValue());
permanent.setTransformable(sourceCard.isTransformable());
}
}