It is currently 23 Apr 2024, 21:34
   
Text Size

Help implementing a card

Moderators: North, BetaSteward, noxx, jeffwadsworth, JayDi, TheElk801, LevelX, CCGHQ Admins

Re: Help implementing a card

Postby djbrez » 04 Mar 2016, 17:18

escplan9 wrote:What I like to do is use gatherer . wizards . com advanced search to find similar cards. For instance I put in rules text to search for "prevent all damage" "by creatures" and results are at link below.

Uncle Istvan is amongst those cards as you'll see. Check out the other similar cards there and how they are implemented in XMage.

For instance, Ethereal Haze uses this for a similar effect:
Code: Select all
this.getSpellAbility().addEffect(new PreventAllDamageByAllEffect(new FilterCreaturePermanent("creatures"), Duration.EndOfTurn, false));
its funny, this is exactly the one I copied... the problem I had was that I couldn't limit the damage prevention to only Uncle Istvan... PreventAllDamageByAllEffect appeared to extend the protection all creatures. The part I seem to be missing is how to limit the protection to just this one card. I think I have to add an additional filter to do that, not sure how.

thanks!!
Don
djbrez
 
Posts: 3
Joined: 14 Feb 2016, 20:57
Has thanked: 0 time
Been thanked: 1 time

Re: Help implementing a card

Postby escplan9 » 04 Mar 2016, 17:25

With regards to Glint Hawk, Faerie Impostor and Drake Familiar, I created a new pull request for the bug fixes and new card. Tested good. Having some issues with the test project at the moment (trying to do the mvn clean install -DskipTests now to see if that resolves the weird errors I'm seeing), but have tests that should work for the cards as well up shortly.

edit: the Maven clean command fixed up the issues. Added the tests to the pull request.
escplan9
 
Posts: 257
Joined: 10 Aug 2015, 22:38
Has thanked: 26 times
Been thanked: 40 times

Re: Help implementing a card

Postby escplan9 » 04 Mar 2016, 19:36

djbrez wrote:
escplan9 wrote:What I like to do is use gatherer . wizards . com advanced search to find similar cards. For instance I put in rules text to search for "prevent all damage" "by creatures" and results are at link below.

Uncle Istvan is amongst those cards as you'll see. Check out the other similar cards there and how they are implemented in XMage.

For instance, Ethereal Haze uses this for a similar effect:
Code: Select all
this.getSpellAbility().addEffect(new PreventAllDamageByAllEffect(new FilterCreaturePermanent("creatures"), Duration.EndOfTurn, false));
its funny, this is exactly the one I copied... the problem I had was that I couldn't limit the damage prevention to only Uncle Istvan... PreventAllDamageByAllEffect appeared to extend the protection all creatures. The part I seem to be missing is how to limit the protection to just this one card. I think I have to add an additional filter to do that, not sure how.

thanks!!
Don
I see what you mean. Possibly look around in mage.filter.predicate for something that can be used so the FilterCreatePermanent is only with regards to the source (Uncle Istvan). I'm sure there's an easy way that I'm just not seeing (I'm pretty new to developing in XMage myself, so it hasn't jumped out to me either). Also on
Code: Select all
http://ct-magefree.rhcloud.com/cards
if you just search for "prevent all damage" there you'll find all the XMage implemented cards that might help you further. (I notice if you add in % by creatures, there's only 1 implemented card, which you already looked at)
escplan9
 
Posts: 257
Joined: 10 Aug 2015, 22:38
Has thanked: 26 times
Been thanked: 40 times

Re: Help implementing a card

Postby escplan9 » 05 Mar 2016, 14:48

I'm working on the requested card Circle of Affliction and I think I have a working version, but not entirely sure. I do not think it is registering each time a source deals damage - if two creatures of the chosen color attack me, there should be two separate triggers for Circle of Affliction I imagine. Instead right after combat damage it just triggers once.

Code: Select all
Ability ability = new CircleOfAfflictionTriggeredAbility();
ability.addTarget(new TargetPlayer());
this.addAbility(ability);

... ( inside CircleOfAfflictionTriggeredAbility() ) ...

    public CircleOfAfflictionTriggeredAbility() {
        super(Zone.BATTLEFIELD, new DoIfCostPaid(new CircleOfAfflictionEffect(), new GenericManaCost(1)), false);
    }

    @Override
    public boolean checkEventType(GameEvent event, Game game) {
        return event.getType() == GameEvent.EventType.DAMAGED_PLAYER;   
    }

    @Override
    public boolean checkTrigger(GameEvent event, Game game) {
       
        boolean shouldTrigger = false;
       
        Permanent circleOfAffliction = game.getPermanent(getSourceId());
        if (circleOfAffliction != null && event.getType() == GameEvent.EventType.DAMAGED_PLAYER) {
           
            ObjectColor chosenColor = (ObjectColor) game.getState().getValue(circleOfAffliction.getId() + "_color");
            if (chosenColor != null) {
                MageObject damageSource = game.getObject(event.getSourceId());       
                if (damageSource != null) {
                    shouldTrigger = damageSource.getColor(game).shares(chosenColor);
                }
            }
        }

        return shouldTrigger;
    }

... (there also is a class for CircleOfAfflictionEffect that handles the life drain portion, but that seems fine and unrelated to this issue) ...
I'm thinking either the card is not supposed to trigger for each individual creature of the chosen color that hits it, or I need to get the card to trigger for each creature of the chosen color. It works as I would expect vs non-creature spells like Lightning Bolt and so on.

Maybe I need to add a loop so it triggers for each creature of the color that dealt damage? I would need to do something like get a collection of all sources of damage during combat, and then have it trigger for each one of those.

Or the EventType I'm using isn't what I need it to be. Looking at the other Event Types though I'm not sure what else would work here. Or I just need to rework the code quite a bit more and not rely on the event types.

I see other cards that look at when sources deal damage to the player doing things like calculating the total amount of damage done
Code: Select all
        if ((event.getTargetId().equals(this.getControllerId()))) {
            this.getEffects().get(0).setValue("damageAmount", event.getAmount());
            return true;
        }

... (then later)...
int damage = (Integer) this.getValue("damageAmount");
Perhaps I can do something similar to get the total amount of creatures of the given color that dealt damage in combat? The issue again is combat damage is all at the same time, so this event is only triggering once at that time but this card needs it to trigger for each creature.
escplan9
 
Posts: 257
Joined: 10 Aug 2015, 22:38
Has thanked: 26 times
Been thanked: 40 times

Re: Help implementing a card

Postby LevelX » 06 Mar 2016, 00:39

escplan9 wrote: Instead right after combat damage it just triggers once.[/code]
It should trigger for each permanent doing combat damage in a damage step to a player.
If not, something is wrong (your card or the Framework class) and one has to look for that.
Don't check again for the event.getType() its already checked in checkEventType method.

Also use
Code: Select all
Permanent circleOfAffliction = game.getPermanentOrLKIBattlefield(getSourceId());
because the effect should still work if the circle has left battlefield before resolving the ability.

And please post always your complete code of a card with questions, it makes it a lot easier to test something or to find a bug in the code.
User avatar
LevelX
DEVELOPER
 
Posts: 1677
Joined: 08 Dec 2011, 15:08
Has thanked: 174 times
Been thanked: 374 times

Re: Help implementing a card

Postby escplan9 » 06 Mar 2016, 03:03

Full code for the card - I took out the extra getEventType() check like you recommended. I'll do more testing on it tomorrow.

Code: Select all
package mage.sets.planarchaos;

import java.util.UUID;
import mage.MageObject;
import mage.ObjectColor;
import mage.abilities.Ability;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.common.EntersBattlefieldAbility;
import mage.abilities.costs.mana.GenericManaCost;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.ChooseColorEffect;
import mage.abilities.effects.common.DoIfCostPaid;
import mage.cards.CardImpl;
import mage.constants.CardType;
import mage.constants.Outcome;
import mage.constants.Rarity;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.target.TargetPlayer;

/**
 *
 * @author escplan9 (Derek Monturo - dmontur1 at gmail dot com)
 */
public class CircleOfAffliction extends CardImpl {

    public CircleOfAffliction(UUID ownerId) {
        super(ownerId, 66, "Circle of Affliction", Rarity.UNCOMMON, new CardType[]{CardType.ENCHANTMENT}, "{1}{B}");
        this.expansionSetCode = "PLC";
               
        // As Circle of Affliction enters the battlefield, choose a color.
        this.addAbility(new EntersBattlefieldAbility(new ChooseColorEffect(Outcome.Neutral)));
             
        // Whenever a source of the chosen color deals damage to you, you may pay {1}. If you do, target player loses 1 life and you gain 1 life.
        Ability ability = new CircleOfAfflictionTriggeredAbility();
        ability.addTarget(new TargetPlayer());
        this.addAbility(ability);
    }

    public CircleOfAffliction(final CircleOfAffliction card) {
        super(card);
    }

    @Override
    public CircleOfAffliction copy() {
        return new CircleOfAffliction(this);
    }
}

class CircleOfAfflictionTriggeredAbility extends TriggeredAbilityImpl {

    public CircleOfAfflictionTriggeredAbility() {
        super(Zone.BATTLEFIELD, new DoIfCostPaid(new CircleOfAfflictionEffect(), new GenericManaCost(1)), false);
    }

    public CircleOfAfflictionTriggeredAbility(final CircleOfAfflictionTriggeredAbility ability) {
        super(ability);
    }

    @Override
    public boolean checkEventType(GameEvent event, Game game) {
        return event.getType() == GameEvent.EventType.DAMAGED_PLAYER;   
    }

    @Override
    public boolean checkTrigger(GameEvent event, Game game) {
       
        boolean shouldTrigger = false;
       
        Permanent circleOfAffliction = game.getPermanent(getSourceId());
        if (circleOfAffliction != null) {
           
            ObjectColor chosenColor = (ObjectColor) game.getState().getValue(circleOfAffliction.getId() + "_color");
            if (chosenColor != null) {
                MageObject damageSource = game.getObject(event.getSourceId());       
                if (damageSource != null) {
                    shouldTrigger = damageSource.getColor(game).shares(chosenColor);
                }
            }
        }

        return shouldTrigger;
    }

    @Override
    public CircleOfAfflictionTriggeredAbility copy() {
        return new CircleOfAfflictionTriggeredAbility(this);
    }

    @Override
    public String getRule() {
        return "Whenever a source of the chosen color deals damage to you, you may pay {1}. If you do, target player loses 1 life and you gain 1 life.";
    }
}

class CircleOfAfflictionEffect extends OneShotEffect {

    CircleOfAfflictionEffect() {
        super(Outcome.Detriment);
        staticText = "target player loses 1 life and you gain 1 life";
    }

    CircleOfAfflictionEffect(final CircleOfAfflictionEffect effect) {
        super(effect);
    }

    @Override
    public boolean apply(Game game, Ability source) {
        Player targetPlayer = game.getPlayer(source.getFirstTarget());
        Player you = game.getPlayer(source.getControllerId());
        if (targetPlayer != null) {
            targetPlayer.loseLife(1, game);
        }
        if (you != null) {
            you.gainLife(1, game);
        }
        return true;
    }

    @Override
    public CircleOfAfflictionEffect copy() {
        return new CircleOfAfflictionEffect(this);
    }
}
escplan9
 
Posts: 257
Joined: 10 Aug 2015, 22:38
Has thanked: 26 times
Been thanked: 40 times

Re: Help implementing a card

Postby LevelX » 06 Mar 2016, 08:52

escplan9 wrote:Full code for the card - I took out the extra getEventType() check like you recommended. I'll do more testing on it tomorrow
There is no problem:
Code only shortened
Code: Select all
/*
 *  Copyright 2010 BetaSteward_at_googlemail.com. All rights reserved.
 *
 *  Redistribution and use in source and binary forms, with or without modification, are
 *  permitted provided that the following conditions are met:
 *
 *     1. Redistributions of source code must retain the above copyright notice, this list of
 *        conditions and the following disclaimer.
 *
 *     2. Redistributions in binary form must reproduce the above copyright notice, this list
 *        of conditions and the following disclaimer in the documentation and/or other materials
 *        provided with the distribution.
 *
 *  THIS SOFTWARE IS PROVIDED BY BetaSteward_at_googlemail.com ``AS IS'' AND ANY EXPRESS OR IMPLIED
 *  WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 *  FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BetaSteward_at_googlemail.com OR
 *  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 *  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 *  SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 *  ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 *  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 *  ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 *  The views and conclusions contained in the software and documentation are those of the
 *  authors and should not be interpreted as representing official policies, either expressed
 *  or implied, of BetaSteward_at_googlemail.com.
 */
package mage.sets.planarchaos;

import java.util.UUID;
import mage.MageObject;
import mage.ObjectColor;
import mage.abilities.Ability;
import mage.abilities.TriggeredAbilityImpl;
import mage.abilities.common.EntersBattlefieldAbility;
import mage.abilities.costs.mana.GenericManaCost;
import mage.abilities.effects.common.ChooseColorEffect;
import mage.abilities.effects.common.DoIfCostPaid;
import mage.abilities.effects.common.GainLifeEffect;
import mage.abilities.effects.common.LoseLifeTargetEffect;
import mage.cards.CardImpl;
import mage.constants.CardType;
import mage.constants.Outcome;
import mage.constants.Rarity;
import mage.constants.Zone;
import mage.game.Game;
import mage.game.events.GameEvent;
import mage.game.permanent.Permanent;
import mage.target.TargetPlayer;

/**
 *
 * @author escplan9 (Derek Monturo - dmontur1 at gmail dot com)
 */
public class CircleOfAffliction extends CardImpl {

    public CircleOfAffliction(UUID ownerId) {
        super(ownerId, 66, "Circle of Affliction", Rarity.UNCOMMON, new CardType[]{CardType.ENCHANTMENT}, "{1}{B}");
        this.expansionSetCode = "PLC";

        // As Circle of Affliction enters the battlefield, choose a color.
        this.addAbility(new EntersBattlefieldAbility(new ChooseColorEffect(Outcome.Neutral)));

        // Whenever a source of the chosen color deals damage to you, you may pay {1}. If you do, target player loses 1 life and you gain 1 life.
        Ability ability = new CircleOfAfflictionTriggeredAbility();
        ability.addTarget(new TargetPlayer());
        this.addAbility(ability);
    }

    public CircleOfAffliction(final CircleOfAffliction card) {
        super(card);
    }

    @Override
    public CircleOfAffliction copy() {
        return new CircleOfAffliction(this);
    }
}

class CircleOfAfflictionTriggeredAbility extends TriggeredAbilityImpl {

    public CircleOfAfflictionTriggeredAbility() {
        super(Zone.BATTLEFIELD, new DoIfCostPaid(new LoseLifeTargetEffect(1), new GenericManaCost(1)), false);
        ((DoIfCostPaid) getEffects().get(0)).addEffect(new GainLifeEffect(1));
    }

    public CircleOfAfflictionTriggeredAbility(final CircleOfAfflictionTriggeredAbility ability) {
        super(ability);
    }

    @Override
    public boolean checkEventType(GameEvent event, Game game) {
        return event.getType() == GameEvent.EventType.DAMAGED_PLAYER;
    }

    @Override
    public boolean checkTrigger(GameEvent event, Game game) {
        Permanent circleOfAffliction = game.getPermanentOrLKIBattlefield(getSourceId());
        if (circleOfAffliction != null) {
            ObjectColor chosenColor = (ObjectColor) game.getState().getValue(circleOfAffliction.getId() + "_color");
            if (chosenColor != null) {
                MageObject damageSource = game.getObject(event.getSourceId());
                if (damageSource != null) {
                    return true;
                }
            }
        }
        return false;
    }

    @Override
    public CircleOfAfflictionTriggeredAbility copy() {
        return new CircleOfAfflictionTriggeredAbility(this);
    }

    @Override
    public String getRule() {
        return "Whenever a source of the chosen color deals damage to you, you may pay {1}. If you do, target player loses 1 life and you gain 1 life.";
    }
}
And the test - event is fired twice as expected
Code: Select all
/*
 *  Copyright 2010 BetaSteward_at_googlemail.com. All rights reserved.
 *
 *  Redistribution and use in source and binary forms, with or without modification, are
 *  permitted provided that the following conditions are met:
 *
 *     1. Redistributions of source code must retain the above copyright notice, this list of
 *        conditions and the following disclaimer.
 *
 *     2. Redistributions in binary form must reproduce the above copyright notice, this list
 *        of conditions and the following disclaimer in the documentation and/or other materials
 *        provided with the distribution.
 *
 *  THIS SOFTWARE IS PROVIDED BY BetaSteward_at_googlemail.com ``AS IS'' AND ANY EXPRESS OR IMPLIED
 *  WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 *  FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BetaSteward_at_googlemail.com OR
 *  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 *  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 *  SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 *  ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 *  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 *  ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 *  The views and conclusions contained in the software and documentation are those of the
 *  authors and should not be interpreted as representing official policies, either expressed
 *  or implied, of BetaSteward_at_googlemail.com.
 */
package org.mage.test.cards.triggers.damage;

import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;

/**
 *
 * @author LevelX2
 */
public class CircleOfAfflictionTest extends CardTestPlayerBase {

    @Test
    public void testTwoAttackersDamage() {
        addCard(Zone.BATTLEFIELD, playerB, "Silvercoat Lion", 2);

        addCard(Zone.BATTLEFIELD, playerA, "Swamp", 4);
        // As Circle of Affliction enters the battlefield, choose a color.
        // Whenever a source of the chosen color deals damage to you, you may pay {1}. If you do, target player loses 1 life and you gain 1 life.
        addCard(Zone.HAND, playerA, "Circle of Affliction", 1);// {1}{B}

        castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Circle of Affliction");
        setChoice(playerA, "White");

        attack(2, playerB, "Silvercoat Lion");
        attack(2, playerB, "Silvercoat Lion");
        addTarget(playerA, playerB);
        addTarget(playerA, playerB);

        setStopAt(2, PhaseStep.POSTCOMBAT_MAIN);
        execute();

        assertPermanentCount(playerA, "Circle of Affliction", 1);

        assertLife(playerA, 18);
        assertLife(playerB, 18);

    }

}
User avatar
LevelX
DEVELOPER
 
Posts: 1677
Joined: 08 Dec 2011, 15:08
Has thanked: 174 times
Been thanked: 374 times

Re: Help implementing a card

Postby escplan9 » 06 Mar 2016, 12:55

Awesome. Thanks for figuring that out and simplifying it for Circle of Affliction. I was basing the programming off some work done on various similar cards like Story Circle, Retaliator Griffin, Sludge Strider, Teferi's Moat that involved choosing colors, sources dealing damage, and the life drain effect.

Only question is why your version cut out the check for the source being of the right color?
Code: Select all
            ObjectColor chosenColor = (ObjectColor) game.getState().getValue(circleOfAffliction.getId() + "_color");
            if (chosenColor != null) {
                MageObject damageSource = game.getObject(event.getSourceId());
                if (damageSource != null) {
                    return true;
                }
            }
It needs to only trigger when sources of the chosen color are the ones doing the damage.

Code: Select all
            ObjectColor chosenColor = (ObjectColor) game.getState().getValue(circleOfAffliction.getId() + "_color");
            if (chosenColor != null) {
                MageObject damageSource = game.getObject(event.getSourceId());       
                if (damageSource != null) {
                    shouldTrigger = damageSource.getColor(game).shares(chosenColor);
                }
            }
I'll test with the changes you made above. Hopefully that's all it takes! Thanks again for all the help.

edit: The simplifications you made were where I borrowed a lot of code from Retaliator Griffin a card from Alara Reborn. I'll also test if that card is triggering properly since it's another "whenever a source deals damage to you..." and it largely used the old code I had in there. Tested, and Retaliator Griffin checks out. I'll have to go over the code changes you made to see if I can figure out why it wasn't triggering twice on mine before.
escplan9
 
Posts: 257
Joined: 10 Aug 2015, 22:38
Has thanked: 26 times
Been thanked: 40 times

Re: Help implementing a card

Postby LevelX » 06 Mar 2016, 13:40

escplan9 wrote:Only question is why your version cut out the check for the source being of the right color?
I deleted too much... my mistake :oops:

That's why we should add another test case with a creature of non chosen color attacking the controller.
User avatar
LevelX
DEVELOPER
 
Posts: 1677
Joined: 08 Dec 2011, 15:08
Has thanked: 174 times
Been thanked: 374 times

Re: Help implementing a card

Postby escplan9 » 06 Mar 2016, 14:00

LevelX wrote:
escplan9 wrote:Only question is why your version cut out the check for the source being of the right color?
I deleted too much... my mistake :oops:

That's why we should add another test case with a creature of non chosen color attacking the controller.
I figured so much and added the color-compare check back in. I also added a bunch of tests and a pull request for the card and the tests. Looks good now!

edit: So trying to understand the wizardry you did with simplifying / shortening the code. Could you walk me through what is going on here?

Code: Select all
super(Zone.BATTLEFIELD, new DoIfCostPaid(new LoseLifeTargetEffect(1), new GenericManaCost(1)), false);
((DoIfCostPaid) getEffects().get(0)).addEffect(new GainLifeEffect(1));
The getEffects() function returns a set of Effects. You're getting the first index returned there because... it's the only effect? The last effect? Other than that, I think I get the rest : as long as the cost was paid for targetting a player to lose life, then add the effect for gaining life.
escplan9
 
Posts: 257
Joined: 10 Aug 2015, 22:38
Has thanked: 26 times
Been thanked: 40 times

Re: Help implementing a card

Postby LevelX » 06 Mar 2016, 19:01

escplan9 wrote:The getEffects() function returns a set of Effects. You're getting the first index returned there because... it's the only effect
Yes it's simply getting the DoIfCostPaid effect and adding another effect to its effects that will be applied if the cost was paid.
User avatar
LevelX
DEVELOPER
 
Posts: 1677
Joined: 08 Dec 2011, 15:08
Has thanked: 174 times
Been thanked: 374 times

Re: Help implementing a card

Postby escplan9 » 07 Mar 2016, 14:31

I recently implemented the card Words of Worship. It was a simple card to implement thanks to many cards with similar effects already implemented such as Words of War. When I manually test the card it works fine. However, I wanted to create some automated tests in Mage.Tests for it and don't understand how activateAbility works there, or what is wrong with my test otherwise:

Code: Select all
/*
 *  Copyright 2010 BetaSteward_at_googlemail.com. All rights reserved.
 *
 *  Redistribution and use in source and binary forms, with or without modification, are
 *  permitted provided that the following conditions are met:
 *
 *     1. Redistributions of source code must retain the above copyright notice, this list of
 *        conditions and the following disclaimer.
 *
 *     2. Redistributions in binary form must reproduce the above copyright notice, this list
 *        of conditions and the following disclaimer in the documentation and/or other materials
 *        provided with the distribution.
 *
 *  THIS SOFTWARE IS PROVIDED BY BetaSteward_at_googlemail.com ``AS IS'' AND ANY EXPRESS OR IMPLIED
 *  WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 *  FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BetaSteward_at_googlemail.com OR
 *  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 *  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 *  SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 *  ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 *  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 *  ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 *  The views and conclusions contained in the software and documentation are those of the
 *  authors and should not be interpreted as representing official policies, either expressed
 *  or implied, of BetaSteward_at_googlemail.com.
 */
package org.mage.test.cards.enchantments;

import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;

/**
 *  Tests for the enchantments "Word of X" such as Words of Worship and Words of War
 *
 *  @author escplan9 (Derek Monturo - dmontur1 at gmail dot com)
 */
public class WordsOfXTests extends CardTestPlayerBase {

    /**
     * Basic test - during upkeep, pay to activate and gain 5 life instead of draw a card.
     */
    @Test
    public void testWordsOfWorshipUpkeepActivate() {
       
        // Enchantment {2}{W}
        // {1}: The next time you would draw a card this turn, you gain 5 life instead.
        addCard(Zone.BATTLEFIELD, playerA, "Words of Worship", 1);
        addCard(Zone.BATTLEFIELD, playerA, "Plains", 4);
        addCard(Zone.LIBRARY, playerA, "White Knight", 4);
        activateAbility(1, PhaseStep.UPKEEP, playerA, "{1}:");
       
        setStopAt(1, PhaseStep.PRECOMBAT_MAIN);
        execute();
           
        assertHandCount(playerA, 0); // should not have drawn a card
        assertLife(playerA, 25); // gain 5 life instead of draw card
    }   
}
Could someone clarify how activateAbility works? I see in other tests a few different ways it should work. For equipment it'll just have the string "Equip". For some abilities it will just have the activated cost as I have done here. For some other cards it will have some or all of the text that follows the activation like Spellskite "{UP}: Change a target of target spell or ability to {this}."

edit: The test fails on the assertLife portion - it returns 20 life instead of 25 that is expected.
escplan9
 
Posts: 257
Joined: 10 Aug 2015, 22:38
Has thanked: 26 times
Been thanked: 40 times

Re: Help implementing a card

Postby LevelX » 07 Mar 2016, 21:47

escplan9 wrote:Could someone clarify how activateAbility works? I see in other tests a few different ways it should work. For equipment it'll just have the string "Equip". For some abilities it will just have the activated cost as I have done here. For some other cards it will have some or all of the text that follows the activation like Spellskite "{UP}: Change a target of target spell or ability to {this}."
It compares if the rules text of the ability starts with the string you set in the activateAbility parameter.
You have to be sure that the part you give is unique over all available activated abilities to get the one you want.
Also it can sometimes tricky if the ability has a slighty other rule text as you think. If I'not sure if the text is correct I check it with the debugger in Tetsplayer.priority method where the string comparison is done.

Tests can sometimes be tricky to define, because of all the parameters that can be wrong and sometimes only debugging helps to understand and find the reason why the test fails.
User avatar
LevelX
DEVELOPER
 
Posts: 1677
Joined: 08 Dec 2011, 15:08
Has thanked: 174 times
Been thanked: 374 times

Re: Help implementing a card

Postby MarcoMarin » 26 Mar 2016, 07:10

Regarding Uncle Istvan:

The ideal would be to have a combination of PreventAllDamageToSourceEffect AND PreventDamageBySourceEffect.

I've actually just made one. :D

It's for Arabian Nights' Desert Nomads, by inheriting from PreventAllDamageToSourceEffect and copying 1 condition from PreventDamageBySourceEffect.

Well, it's a simplistic version which applies only to subtypes. :oops:

But it should be trivial to copy over more stuff from the latter class and do a "PreventDamageToSourceBySourceEffect", for someone who knows more Java (which I don't), just beware that it expects a target choice from the player, while e.g. Uncle Istvan is unconditional to all creatures.

So my 'bySubtype' class should be enough, but if you could test it please check it out here (until I get pull request rights :?): https://github.com/Marco-Marin/mage

(Edit: btw, I've placed the class inside the cards themselves, so not to mess up the rest of the code, hopefully this will be replaced by the more powerful/general effect, not just subtypes)

For the devs:

I noticed something on "PreventionEffectImpl": where a comment says:
Code: Select all
 // damage amount is reduced or set to 0 so complete replacement of damage event is never neccessary
But then cards like "if cause damage then effect" would always have to check whether dmg > 0, no? Instead of simply relying on triggered dmg.
MarcoMarin
 
Posts: 32
Joined: 20 Mar 2016, 07:49
Has thanked: 16 times
Been thanked: 2 times

Re: Help implementing a card

Postby escplan9 » 08 Apr 2016, 21:32

Working on the card Vanishing and not 100% sure on the expected behavior. Right now when the enchanted creature phases out, the Enchantment card Vanishing just "lingers" on the battlefield where the creature used to be. Then when the creature phases back in, the Enchantment is re-attached to it. I'm not sure if this is an issue requiring a change in handling for Phasing, or a change in handling for the card I coded in.

http://www.mtgsalvation.com/forums/magi ... -vanishing

"Enchantments attached to the creature will "phase out indirectly," and stay attached to it, even though it is phased out." and "Enchantments and equipment phase in/out with the permanents they are associated with."

My code below:

Vanishing
Code: Select all
package mage.sets.visions;

import java.util.UUID;
import mage.abilities.Ability;
import mage.abilities.common.SimpleActivatedAbility;
import mage.abilities.costs.mana.ManaCostsImpl;
import mage.abilities.effects.common.AttachEffect;
import mage.abilities.effects.common.PhaseOutAttachedEffect;
import mage.abilities.keyword.EnchantAbility;
import mage.cards.CardImpl;
import mage.constants.CardType;
import mage.constants.Outcome;
import mage.constants.Rarity;
import mage.constants.Zone;
import mage.target.TargetPermanent;
import mage.target.common.TargetCreaturePermanent;

/**
 *
 * @author escplan9 (Derek Monturo - dmontur1 at gmail dot com)
 */
public class Vanishing extends CardImpl {

    public Vanishing(UUID ownerId) {
        super(ownerId, 39, "Vanishing", Rarity.COMMON, new CardType[]{CardType.ENCHANTMENT}, "{U}");
        this.expansionSetCode = "VIS";
        this.subtype.add("Aura");

        // Enchant creature
        TargetPermanent auraTarget = new TargetCreaturePermanent();
        this.getSpellAbility().addTarget(auraTarget);
        this.getSpellAbility().addEffect(new AttachEffect(Outcome.Detriment));
        Ability ability = new EnchantAbility(auraTarget.getTargetName());
        this.addAbility(ability);
       
        // {U}{U}: Enchanted creature phases out.
        this.addAbility(new SimpleActivatedAbility(Zone.BATTLEFIELD, new PhaseOutAttachedEffect(), new ManaCostsImpl("{U}{U}")));
    }

    public Vanishing(final Vanishing card) {
        super(card);
    }

    @Override
    public Vanishing copy() {
        return new Vanishing(this);
    }
}
PhaseOutAttachedEffect
Code: Select all
package mage.abilities.effects.common;

import mage.abilities.Ability;
import mage.abilities.effects.OneShotEffect;
import mage.constants.Outcome;
import mage.game.Game;
import mage.game.permanent.Permanent;

/**
 *
 * @author escplan9 (Derek Monturo - dmontur1 at gmail dot com)
 */
public class PhaseOutAttachedEffect extends OneShotEffect {

    public PhaseOutAttachedEffect() {
        super(Outcome.Detriment);
        this.staticText = "Enchanted creature phases out";
    }

    public PhaseOutAttachedEffect(final PhaseOutAttachedEffect effect) {
        super(effect);
    }

    @Override
    public PhaseOutAttachedEffect copy() {
        return new PhaseOutAttachedEffect(this);
    }

    @Override
    public boolean apply(Game game, Ability source) {
        Permanent enchantment = game.getPermanentOrLKIBattlefield(source.getSourceId());
        if(enchantment != null) {
            Permanent enchanted = game.getPermanent(enchantment.getAttachedTo());
            if(enchanted != null) {
                return enchanted.phaseOut(game);
            }
        }
        return false;
    }
}
I believe the phasing handling needs to be adjusted for it. Created "refactoring" issue in Github on phasing out enchantments/equipments attached to permanents: https://github.com/magefree/mage/issues/1829

Not a high priority by any means.
escplan9
 
Posts: 257
Joined: 10 Aug 2015, 22:38
Has thanked: 26 times
Been thanked: 40 times

PreviousNext

Return to Developers Talk

Who is online

Users browsing this forum: No registered users and 2 guests


Who is online

In total there are 2 users online :: 0 registered, 0 hidden and 2 guests (based on users active over the past 10 minutes)
Most users ever online was 4143 on 23 Jan 2024, 08:21

Users browsing this forum: No registered users and 2 guests

Login Form