[TLA] Implement Razor Rings, rework excess damage (#13910)

* [TLA] Implement Razor Rings

* add overflow protection
This commit is contained in:
Evan Kranzler 2025-08-15 16:37:55 -04:00 committed by GitHub
parent 96dbfc757e
commit b64f1dce45
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 209 additions and 153 deletions

View file

@ -62,8 +62,8 @@ class BottleCapBlastEffect extends OneShotEffect {
@Override
public boolean apply(Game game, Ability source) {
UUID target = getTargetPointer().getFirst(game, source);
Player player = game.getPlayer(target);
UUID targetId = getTargetPointer().getFirst(game, source);
Player player = game.getPlayer(targetId);
if (player != null) {
player.damage(5, source, game);
return true;
@ -72,11 +72,10 @@ class BottleCapBlastEffect extends OneShotEffect {
if (permanent == null) {
return false;
}
int lethal = Math.min(permanent.getLethalDamage(source.getSourceId(), game), 5);
permanent.damage(5, source.getSourceId(), source, game);
if (lethal < 5) {
int excess = permanent.damageWithExcess(5, source, game);
if (excess > 0) {
new TreasureToken().putOntoBattlefield(
5 - lethal, game, source, source.getControllerId(), true, false
excess, game, source, source.getControllerId(), true, false
);
}
return true;

View file

@ -1,7 +1,5 @@
package mage.cards.c;
import java.util.UUID;
import mage.abilities.Ability;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.keyword.DiscoverEffect;
@ -9,19 +7,18 @@ import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Outcome;
import mage.filter.StaticFilters;
import mage.game.Controllable;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.target.Target;
import mage.target.TargetPermanent;
import mage.target.common.TargetControlledCreaturePermanent;
import mage.target.common.TargetCreaturePermanent;
import java.util.Optional;
import java.util.UUID;
import static mage.filter.StaticFilters.FILTER_ANOTHER_CREATURE_TARGET_2;
/**
*
* @author jimga150
*/
public final class ContestOfClaws extends CardImpl {
@ -29,14 +26,10 @@ public final class ContestOfClaws extends CardImpl {
public ContestOfClaws(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{1}{G}");
// Target creature you control deals damage equal to its power to another target creature. If excess damage was dealt this way, discover X, where X is that excess damage.
this.getSpellAbility().addEffect(new ContestOfClawsDamageEffect());
Target target = new TargetControlledCreaturePermanent().setTargetTag(1);
this.getSpellAbility().addTarget(target);
Target target2 = new TargetPermanent(FILTER_ANOTHER_CREATURE_TARGET_2).setTargetTag(2);
this.getSpellAbility().addTarget(target2);
this.getSpellAbility().addTarget(new TargetControlledCreaturePermanent().setTargetTag(1));
this.getSpellAbility().addTarget(new TargetPermanent(FILTER_ANOTHER_CREATURE_TARGET_2).setTargetTag(2));
}
private ContestOfClaws(final ContestOfClaws card) {
@ -74,22 +67,13 @@ class ContestOfClawsDamageEffect extends OneShotEffect {
if (ownCreature == null || targetCreature == null) {
return false;
}
int damage = ownCreature.getPower().getValue();
int lethalDamage = targetCreature.getLethalDamage(source.getSourceId(), game);
targetCreature.damage(damage, ownCreature.getId(), source, game, false, true);
if (damage < lethalDamage){
return true;
int excess = targetCreature.damageWithExcess( ownCreature.getPower().getValue(), ownCreature.getId(), source, game);
if (excess > 0) {
Optional.ofNullable(source)
.map(Controllable::getControllerId)
.map(game::getPlayer)
.ifPresent(player -> DiscoverEffect.doDiscover(player, excess, game, source));
}
int discoverValue = damage - lethalDamage;
Player player = game.getPlayer(source.getControllerId());
if (player == null){
// If somehow this case is hit, the damage still technically happened, so i guess it applied?
return true;
}
DiscoverEffect.doDiscover(player, discoverValue, game, source);
return true;
}
}

View file

@ -15,7 +15,6 @@ import mage.util.CardUtil;
import java.util.UUID;
/**
*
* @author ciaccona007
*/
public final class GoblinNegotiation extends CardImpl {
@ -62,13 +61,11 @@ class GoblinNegotiationEffect extends OneShotEffect {
if (permanent == null) {
return false;
}
int damage = CardUtil.getSourceCostsTag(game, source, "X", 0);
int lethal = Math.min(permanent.getLethalDamage(source.getSourceId(), game), damage);
permanent.damage(damage, source.getSourceId(), source, game);
if (damage > lethal) {
new GoblinToken().putOntoBattlefield(
damage - lethal, game, source, source.getControllerId()
);
int excess = permanent.damageWithExcess(
CardUtil.getSourceCostsTag(game, source, "X", 0), source, game
);
if (excess > 0) {
new GoblinToken().putOntoBattlefield(excess, game, source);
}
return true;
}

View file

@ -60,13 +60,11 @@ class HellToPayEffect extends OneShotEffect {
if (permanent == null) {
return false;
}
int damage = CardUtil.getSourceCostsTag(game, source, "X", 0);
int lethal = Math.min(permanent.getLethalDamage(source.getSourceId(), game), damage);
permanent.damage(damage, source.getSourceId(), source, game);
if (damage > lethal) {
new TreasureToken().putOntoBattlefield(
damage - lethal, game, source, source.getControllerId(), true, false
);
int excess = permanent.damageWithExcess(
CardUtil.getSourceCostsTag(game, source, "X", 0), source, game
);
if (excess > 0) {
new TreasureToken().putOntoBattlefield(excess, game, source, source.getControllerId(), true, false);
}
return true;
}

View file

@ -55,14 +55,13 @@ class LacerateFleshEffect extends OneShotEffect {
@Override
public boolean apply(Game game, Ability source) {
Permanent permanent = game.getPermanent(source.getFirstTarget());
Permanent permanent = game.getPermanent(getTargetPointer().getFirst(game, source));
if (permanent == null) {
return false;
}
int lethal = Math.min(permanent.getLethalDamage(source.getSourceId(), game), 4);
permanent.damage(4, source.getSourceId(), source, game);
if (lethal < 4) {
new BloodToken().putOntoBattlefield(4 - lethal, game, source);
int excess = permanent.damageWithExcess(4, source, game);
if (excess > 0) {
new BloodToken().putOntoBattlefield(excess, game, source);
}
return true;
}

View file

@ -79,7 +79,7 @@ class MegatronDestructiveForceEffect extends OneShotEffect {
return false;
}
TargetSacrifice target = new TargetSacrifice(
0, 1, StaticFilters.FILTER_CONTROLLED_ANOTHER_ARTIFACT
0, 1, StaticFilters.FILTER_CONTROLLED_ANOTHER_ARTIFACT
);
player.choose(outcome, target, source, game);
Permanent permanent = game.getPermanent(target.getFirstTarget());
@ -93,7 +93,7 @@ class MegatronDestructiveForceEffect extends OneShotEffect {
}
ReflexiveTriggeredAbility ability = new ReflexiveTriggeredAbility(
new MegatronDestructiveForceReflexiveEffect(manaValue), false
new MegatronDestructiveForceReflexiveEffect(manaValue), false
);
ability.addHint(new StaticHint("Sacrificed artifact mana value: " + manaValue));
ability.addTarget(new TargetCreaturePermanent());
@ -109,8 +109,8 @@ class MegatronDestructiveForceReflexiveEffect extends OneShotEffect {
MegatronDestructiveForceReflexiveEffect(int value) {
super(Outcome.Damage);
staticText = "{this} deals damage equal to the sacrificed artifact's mana value to target " +
"creature. If excess damage would be dealt to that creature this way, instead that damage " +
"is dealt to that creature's controller and you convert {this}.";
"creature. If excess damage would be dealt to that creature this way, instead that damage " +
"is dealt to that creature's controller and you convert {this}.";
this.value = value;
}
@ -126,36 +126,22 @@ class MegatronDestructiveForceReflexiveEffect extends OneShotEffect {
@Override
public boolean apply(Game game, Ability source) {
Permanent sourcePermanent = source.getSourcePermanentOrLKI(game);
if (sourcePermanent == null) {
return false;
}
if (value < 1) {
return false;
}
Permanent permanent = game.getPermanent(getTargetPointer().getFirst(game, source));
if (permanent == null) {
return false;
}
int lethal = permanent.getLethalDamage(source.getSourceId(), game);
int excess = value - lethal;
if (excess <= 0) {
// no excess damage.
permanent.damage(value, source.getSourceId(), source, game);
int excess = permanent.damageWithExcess(value, source, game);
if (excess < 1) {
return true;
}
// excess damage. dealing excess to controller's instead. And convert Megatron.
permanent.damage(lethal, source.getSourceId(), source, game);
Player player = game.getPlayer(permanent.getControllerId());
if (player != null) {
player.damage(excess, source, game);
}
new TransformSourceEffect().apply(game, source);
return true;
}
}

View file

@ -80,9 +80,8 @@ class NahirisWarcraftingEffect extends OneShotEffect {
if (player == null || permanent == null) {
return false;
}
int lethal = permanent.getLethalDamage(source.getSourceId(), game);
int excess = permanent.damage(5, source, game) - lethal;
if (excess <= 0) {
int excess = permanent.damageWithExcess(5, source, game);
if (excess < 1) {
return true;
}
Cards cards = new CardsImpl(player.getLibrary().getTopCards(game, excess));

View file

@ -2,7 +2,6 @@ package mage.cards.o;
import mage.abilities.Ability;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.CreateTokenEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
@ -61,10 +60,8 @@ class OrbitalPlungeEffect extends OneShotEffect {
if (permanent == null) {
return false;
}
int lethal = permanent.getLethalDamage(source.getSourceId(), game);
permanent.damage(6, source.getSourceId(), source, game);
if (lethal < 6) {
new CreateTokenEffect(new LanderToken()).apply(game, source);
if (permanent.damageWithExcess(6, source, game) > 0) {
new LanderToken().putOntoBattlefield(1, game, source);
}
return true;
}

View file

@ -7,15 +7,18 @@ import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Outcome;
import mage.filter.StaticFilters;
import mage.game.Controllable;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.target.TargetPermanent;
import mage.target.common.TargetControlledCreaturePermanent;
import mage.target.common.TargetCreaturePermanent;
import mage.target.targetpointer.EachTargetPointer;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
import static mage.filter.StaticFilters.FILTER_CREATURE_YOU_DONT_CONTROL;
@ -47,6 +50,7 @@ class RamThroughEffect extends OneShotEffect {
RamThroughEffect() {
super(Outcome.Benefit);
this.setTargetPointer(new EachTargetPointer());
staticText = "Target creature you control deals damage equal to its power to target creature you don't control. " +
"If the creature you control has trample, excess damage is dealt to that creature's controller instead.";
}
@ -62,29 +66,31 @@ class RamThroughEffect extends OneShotEffect {
@Override
public boolean apply(Game game, Ability source) {
if (source.getTargets().size() != 2) {
throw new IllegalStateException("It must have two targets, but found " + source.getTargets().size());
}
Permanent myPermanent = game.getPermanent(getTargetPointer().getFirst(game, source));
Permanent anotherPermanent = game.getPermanent(source.getTargets().get(1).getFirstTarget());
if (myPermanent == null || anotherPermanent == null) {
List<Permanent> permanents = this
.getTargetPointer()
.getTargets(game, source)
.stream()
.map(game::getPermanent)
.filter(Objects::nonNull)
.collect(Collectors.toList());
if (permanents.size() < 2) {
return false;
}
int power = myPermanent.getPower().getValue();
Permanent permanent = permanents.get(0);
int power = permanent.getPower().getValue();
if (power < 1) {
return false;
}
if (!myPermanent.getAbilities().containsKey(TrampleAbility.getInstance().getId())) {
return anotherPermanent.damage(power, myPermanent.getId(), source, game, false, true) > 0;
Permanent creature = permanents.get(1);
if (!permanent.hasAbility(TrampleAbility.getInstance(), game)) {
return creature.damage(power, permanent.getId(), source, game) > 0;
}
int lethal = anotherPermanent.getLethalDamage(myPermanent.getId(), game);
lethal = Math.min(lethal, power);
anotherPermanent.damage(lethal, myPermanent.getId(), source, game);
Player player = game.getPlayer(anotherPermanent.getControllerId());
if (player != null && lethal < power) {
player.damage(power - lethal, myPermanent.getId(), source, game);
int excess = creature.damageWithExcess(power, permanent.getId(), source, game);
if (excess > 0) {
Optional.ofNullable(creature)
.map(Controllable::getControllerId)
.map(game::getPlayer)
.ifPresent(player -> player.damage(excess, permanent.getId(), source, game));
}
return true;
}

View file

@ -0,0 +1,72 @@
package mage.cards.r;
import mage.abilities.Ability;
import mage.abilities.effects.OneShotEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Outcome;
import mage.game.Controllable;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.target.common.TargetAttackingOrBlockingCreature;
import java.util.Optional;
import java.util.UUID;
/**
* @author TheElk801
*/
public final class RazorRings extends CardImpl {
public RazorRings(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{1}{W}");
// Razor Rings deals 4 damage to target attacking or blocking creature. You gain life equal to the excess damage dealt this way.
this.getSpellAbility().addEffect(new RazorRingsEffect());
this.getSpellAbility().addTarget(new TargetAttackingOrBlockingCreature());
}
private RazorRings(final RazorRings card) {
super(card);
}
@Override
public RazorRings copy() {
return new RazorRings(this);
}
}
class RazorRingsEffect extends OneShotEffect {
RazorRingsEffect() {
super(Outcome.Benefit);
staticText = "{this} deals 4 damage to target attacking or blocking creature. " +
"You gain life equal to the excess damage dealt this way";
}
private RazorRingsEffect(final RazorRingsEffect effect) {
super(effect);
}
@Override
public RazorRingsEffect copy() {
return new RazorRingsEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Permanent permanent = game.getPermanent(getTargetPointer().getFirst(game, source));
if (permanent == null) {
return false;
}
int excess = permanent.damageWithExcess(4, source, game);
if (excess > 0) {
Optional.ofNullable(source)
.map(Controllable::getControllerId)
.map(game::getPlayer)
.ifPresent(player -> player.gainLife(excess, game, source));
}
return true;
}
}

View file

@ -60,8 +60,7 @@ class TorchTheWitnessEffect extends OneShotEffect {
if (permanent == null) {
return false;
}
int lethal = permanent.getLethalDamage(source.getSourceId(), game);
if (lethal < permanent.damage(2 * CardUtil.getSourceCostsTag(game, source, "X", 0), source, game)) {
if (permanent.damageWithExcess(2 * CardUtil.getSourceCostsTag(game, source, "X", 0), source, game) > 0) {
InvestigateEffect.doInvestigate(source.getControllerId(), 1, game, source);
}
return true;

View file

@ -67,19 +67,18 @@ class UnleashTheInfernoEffect extends OneShotEffect {
if (permanent == null) {
return false;
}
int lethal = Math.min(permanent.getLethalDamage(source.getSourceId(), game), 7);
permanent.damage(7, source, game);
int excess = 7 - lethal;
if (excess > 0) {
ReflexiveTriggeredAbility ability = new ReflexiveTriggeredAbility(new DestroyTargetEffect(), false);
FilterPermanent filter = new FilterArtifactOrEnchantmentPermanent(
"artifact or enchantment an opponent controls with mana value less than or equal to " + excess
);
filter.add(TargetController.OPPONENT.getControllerPredicate());
filter.add(new ManaValuePredicate(ComparisonType.FEWER_THAN, excess + 1));
ability.addTarget(new TargetPermanent(filter));
game.fireReflexiveTriggeredAbility(ability, source);
int excess = permanent.damageWithExcess(7, source, game);
if (excess < 1) {
return true;
}
ReflexiveTriggeredAbility ability = new ReflexiveTriggeredAbility(new DestroyTargetEffect(), false);
FilterPermanent filter = new FilterArtifactOrEnchantmentPermanent(
"artifact or enchantment an opponent controls with mana value less than or equal to " + excess
);
filter.add(TargetController.OPPONENT.getControllerPredicate());
filter.add(new ManaValuePredicate(ComparisonType.FEWER_THAN, excess + 1));
ability.addTarget(new TargetPermanent(filter));
game.fireReflexiveTriggeredAbility(ability, source);
return true;
}
}

View file

@ -14,11 +14,13 @@ import mage.constants.CardType;
import mage.constants.Outcome;
import mage.constants.SubType;
import mage.constants.SuperType;
import mage.game.Controllable;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.target.common.TargetAnyTarget;
import java.util.Optional;
import java.util.UUID;
/**
@ -91,14 +93,11 @@ class VikyaScorchingStalwartEffect extends OneShotEffect {
if (!permanent.isCreature(game)) {
return permanent.damage(amount, source, game) > 0;
}
int lethal = permanent.getLethalDamage(source.getSourceId(), game);
permanent.damage(amount, source.getSourceId(), source, game);
if (lethal >= amount) {
return true;
}
Player player = game.getPlayer(source.getControllerId());
if (player != null) {
player.drawCards(1, source, game);
if (permanent.damageWithExcess(amount, source, game) > 0) {
Optional.ofNullable(source)
.map(Controllable::getControllerId)
.map(game::getPlayer)
.ifPresent(player -> player.drawCards(1, source, game));
}
return true;
}

View file

@ -1,25 +1,26 @@
package mage.cards.w;
import java.util.UUID;
import mage.abilities.Ability;
import mage.abilities.effects.OneShotEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Outcome;
import mage.filter.StaticFilters;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.game.permanent.token.ElfWarriorToken;
import mage.target.TargetPermanent;
import mage.target.common.TargetControlledCreaturePermanent;
import mage.target.common.TargetCreaturePermanent;
import mage.target.targetpointer.EachTargetPointer;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Collectors;
import static mage.filter.StaticFilters.FILTER_CREATURE_YOU_DONT_CONTROL;
/**
*
* @author bwsinger
*/
public final class WindswiftSlice extends CardImpl {
@ -48,6 +49,7 @@ class WindswiftSliceEffect extends OneShotEffect {
WindswiftSliceEffect() {
super(Outcome.Benefit);
this.setTargetPointer(new EachTargetPointer());
staticText = "Target creature you control deals damage equal to its power to target " +
"creature you don't control. Create a number of 1/1 green Elf Warrior creature " +
"tokens equal to the amount of excess damage dealt this way.";
@ -64,27 +66,26 @@ class WindswiftSliceEffect extends OneShotEffect {
@Override
public boolean apply(Game game, Ability source) {
Permanent myPermanent = game.getPermanent(getTargetPointer().getFirst(game, source));
Permanent anotherPermanent = game.getPermanent(source.getTargets().get(1).getFirstTarget());
if (myPermanent == null || anotherPermanent == null) {
List<Permanent> permanents = this
.getTargetPointer()
.getTargets(game, source)
.stream()
.map(game::getPermanent)
.filter(Objects::nonNull)
.collect(Collectors.toList());
if (permanents.size() < 2) {
return false;
}
int power = myPermanent.getPower().getValue();
Permanent permanent = permanents.get(0);
int power = permanent.getPower().getValue();
if (power < 1) {
return false;
}
int lethal = anotherPermanent.getLethalDamage(myPermanent.getId(), game);
lethal = Math.min(lethal, power);
anotherPermanent.damage(power, myPermanent.getId(), source, game);
if (lethal < power) {
new ElfWarriorToken().putOntoBattlefield(power - lethal, game, source, source.getControllerId());
Permanent creature = permanents.get(1);
int excess = creature.damageWithExcess(power, permanent.getId(), source, game);
if (excess > 0) {
new ElfWarriorToken().putOntoBattlefield(excess, game, source);
}
return true;
}
}

View file

@ -98,6 +98,7 @@ public final class AvatarTheLastAirbender extends ExpansionSet {
cards.add(new SetCardInfo("Pretending Poxbearers", 237, Rarity.COMMON, mage.cards.p.PretendingPoxbearers.class));
cards.add(new SetCardInfo("Rabaroo Troop", 32, Rarity.COMMON, mage.cards.r.RabarooTroop.class));
cards.add(new SetCardInfo("Raucous Audience", 190, Rarity.COMMON, mage.cards.r.RaucousAudience.class));
cards.add(new SetCardInfo("Razor Rings", 33, Rarity.COMMON, mage.cards.r.RazorRings.class));
cards.add(new SetCardInfo("Rebellious Captives", 191, Rarity.COMMON, mage.cards.r.RebelliousCaptives.class));
cards.add(new SetCardInfo("Redirect Lightning", 151, Rarity.RARE, mage.cards.r.RedirectLightning.class, NON_FULL_USE_VARIOUS));
cards.add(new SetCardInfo("Redirect Lightning", 343, Rarity.RARE, mage.cards.r.RedirectLightning.class, NON_FULL_USE_VARIOUS));

View file

@ -6,9 +6,11 @@ import mage.abilities.dynamicvalue.DynamicValue;
import mage.abilities.dynamicvalue.common.StaticValue;
import mage.abilities.effects.OneShotEffect;
import mage.constants.Outcome;
import mage.game.Controllable;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
import java.util.Optional;
/**
* @author TheElk801
@ -47,12 +49,12 @@ public class DamageWithExcessEffect extends OneShotEffect {
return false;
}
int damage = amount.calculate(game, source, this);
int lethal = permanent.getLethalDamage(source.getSourceId(), game);
lethal = Math.min(lethal, damage);
permanent.damage(lethal, source.getSourceId(), source, game);
Player player = game.getPlayer(permanent.getControllerId());
if (player != null && lethal < damage) {
player.damage(damage - lethal, source.getSourceId(), source, game);
int excess = permanent.damageWithExcess(damage, source, game);
if (excess > 0) {
Optional.ofNullable(permanent)
.map(Controllable::getControllerId)
.map(game::getPlayer)
.ifPresent(player -> player.damage(excess, source, game));
}
return true;
}

View file

@ -8,6 +8,7 @@ import mage.constants.Zone;
import mage.game.Controllable;
import mage.game.Game;
import mage.game.GameState;
import mage.util.CardUtil;
import java.util.List;
import java.util.Set;
@ -177,6 +178,23 @@ public interface Permanent extends Card, Controllable {
int getLethalDamage(UUID attackerId, Game game);
/**
* Same arguments as regular damage method, but returns the amount of excess damage dealt instead
*
* @return
*/
default int damageWithExcess(int damage, Ability source, Game game) {
return this.damageWithExcess(damage, source.getSourceId(), source, game);
}
default int damageWithExcess(int damage, UUID attackerId, Ability source, Game game) {
int lethal = getLethalDamage(attackerId, game);
int excess = Math.max(CardUtil.overflowDec(damage, lethal), 0);
int dealt = Math.min(lethal, damage);
this.damage(dealt, attackerId, source, game);
return excess;
}
void removeAllDamage(Game game);
void reset(Game game);