Fix getTotalPreventionShieldAmount (#3654)

* Fix scripts

* Fix damage prevention display

* Optimize attachment tracking for netplay

* Phasing fix

* Fix NPE

* Tweak logic order

* Clean up

* Fix scripts

* Clean up

* Fix FailedToTarget

---------

Co-authored-by: TRT <>
This commit is contained in:
tool4ever
2023-08-23 10:34:50 +02:00
committed by GitHub
parent de72773634
commit 2025487c34
22 changed files with 74 additions and 56 deletions

View File

@@ -398,12 +398,10 @@ public class ComputerUtilCost {
return false;
}
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostSacrifice) {
if ("CARDNAME".equals(part.getType())) {
if (part instanceof CostSacrifice && part.payCostFromSource()) {
return true;
}
}
}
return false;
}

View File

@@ -85,7 +85,7 @@ public class DamagePreventAi extends SpellAbilityAi {
// check stack for something on the stack will kill anything i control
final List<GameObject> objects = ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), sa);
if (objects.contains(ai)) {
if (objects.contains(ai) && sa.canTarget(ai)) {
tcs.add(ai);
chance = true;
}

View File

@@ -216,6 +216,7 @@ public class EffectAi extends SpellAbilityAi {
} else if (logic.equals("Fight")) {
return FightAi.canFightAi(ai, sa, 0, 0);
} else if (logic.equals("Pump")) {
sa.resetTargets();
List<Card> options = CardUtil.getValidCardsToTarget(sa);
options = CardLists.filterControlledBy(options, ai);
if (sa.getPayCosts().hasTapCost()) {

View File

@@ -89,7 +89,7 @@ public class ManaEffectAi extends SpellAbilityAi {
if (logic.startsWith("ManaRitual")) {
return ph.is(PhaseType.MAIN2, ai) || ph.is(PhaseType.MAIN1, ai);
} else if ("AtOppEOT".equals(logic)) {
return !ai.getManaPool().hasBurn() && ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai;
return (!ai.getManaPool().hasBurn() || !ai.canLoseLife() || ai.cantLoseForZeroOrLessLife()) && ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai;
}
return super.checkPhaseRestrictions(ai, sa, ph, logic);
}
@@ -261,7 +261,8 @@ public class ManaEffectAi extends SpellAbilityAi {
}
private boolean improvesPosition(Player ai, SpellAbility sa) {
boolean activateForTrigger = Iterables.any(Iterables.filter(sa.getHostCard().getTriggers(), CardTraitPredicates.hasParam("AILogic", "ActivateOnce")),
boolean activateForTrigger = (!ai.getManaPool().hasBurn() || !ai.canLoseLife() || ai.cantLoseForZeroOrLessLife()) &&
Iterables.any(Iterables.filter(sa.getHostCard().getTriggers(), CardTraitPredicates.hasParam("AILogic", "ActivateOnce")),
t -> sa.getHostCard().getAbilityActivatedThisTurn(t.getOverridingAbility()) == 0);
PhaseHandler ph = ai.getGame().getPhaseHandler();

View File

@@ -1,6 +1,7 @@
package forge.game;
import forge.game.card.CardCollectionView;
import com.google.common.collect.Iterables;
import forge.game.card.CardView;
import forge.trackable.TrackableCollection;
import forge.trackable.TrackableObject;
@@ -45,35 +46,34 @@ public abstract class GameEntityView extends TrackableObject {
public int getPreventNextDamage() {
return get(TrackableProperty.PreventNextDamage);
}
protected void updatePreventNextDamage(GameEntity e) {
public void updatePreventNextDamage(GameEntity e) {
set(TrackableProperty.PreventNextDamage, e.getPreventNextDamageTotalShields());
}
public Iterable<CardView> getAttachedCards() {
return get(TrackableProperty.AttachedCards);
if (hasAnyCardAttachments()) {
Iterable<CardView> active = Iterables.filter(get(TrackableProperty.AttachedCards), c -> !c.isPhasedOut());
if (!Iterables.isEmpty(active)) {
return active;
}
}
return null;
}
public boolean hasCardAttachments() {
return getAttachedCards() != null;
}
public Iterable<CardView> getAllAttachedCards() {
return get(TrackableProperty.AllAttachedCards);
return get(TrackableProperty.AttachedCards);
}
public boolean hasAnyCardAttachments() {
return getAllAttachedCards() != null;
}
protected void updateAttachedCards(GameEntity e) {
if (e.hasCardAttachments()) {
set(TrackableProperty.AttachedCards, CardView.getCollection(e.getAttachedCards()));
}
else {
set(TrackableProperty.AttachedCards, null);
}
CardCollectionView all = e.getAllAttachedCards();
if (all.isEmpty()) {
set(TrackableProperty.AllAttachedCards, null);
if (!e.getAllAttachedCards().isEmpty()) {
set(TrackableProperty.AttachedCards, CardView.getCollection(e.getAllAttachedCards()));
} else {
set(TrackableProperty.AllAttachedCards, CardView.getCollection(all));
set(TrackableProperty.AttachedCards, null);
}
}
}

View File

@@ -1999,13 +1999,6 @@ public class AbilityUtils {
return doXMath(calculateAmount(c, sq[c.isOptionalCostPaid(OptionalCost.Generic) ? 1 : 2], ctb), expr, c, ctb);
}
if (sq[0].equals("TotalDamageDoneByThisTurn")) {
return doXMath(c.getTotalDamageDoneBy(), expr, c, ctb);
}
if (sq[0].equals("TotalDamageReceivedThisTurn")) {
return doXMath(c.getAssignedDamage(), expr, c, ctb);
}
if (sq[0].contains("CardPower")) {
return doXMath(c.getNetPower(), expr, c, ctb);
}
@@ -2335,6 +2328,13 @@ public class AbilityUtils {
return doXMath(player.getOpponentsTotalPoisonCounters(), expr, c, ctb);
}
if (sq[0].equals("TotalDamageDoneByThisTurn")) {
return doXMath(c.getTotalDamageDoneBy(), expr, c, ctb);
}
if (sq[0].equals("TotalDamageReceivedThisTurn")) {
return doXMath(c.getAssignedDamage(), expr, c, ctb);
}
if (sq[0].equals("MaxOppDamageThisTurn")) {
return doXMath(player.getMaxOpponentAssignedDamage(), expr, c, ctb);
}
@@ -3493,6 +3493,7 @@ public class AbilityUtils {
if (value.equals("RingTemptedYou")) {
return doXMath(player.getNumRingTemptedYou(), m, source, ctb);
}
if (value.startsWith("DungeonCompletedNamed")) {
String [] full = value.split("_");
String name = full[1];

View File

@@ -4,12 +4,13 @@ import java.util.List;
import forge.GameCommand;
import forge.game.Game;
import forge.game.GameObject;
import forge.game.GameEntity;
import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.event.GameEventPlayerStatsChanged;
import forge.game.player.Player;
import forge.game.replacement.ReplacementEffect;
import forge.game.replacement.ReplacementHandler;
@@ -20,7 +21,7 @@ import forge.game.zone.ZoneType;
import forge.util.TextUtil;
public abstract class DamagePreventEffectBase extends SpellAbilityEffect {
public static void addPreventNextDamage(SpellAbility sa, GameObject o, int numDam) {
public static void addPreventNextDamage(SpellAbility sa, GameEntity o, int numDam) {
final Card hostCard = sa.getHostCard();
final Game game = hostCard.getGame();
final Player player = hostCard.getController();
@@ -40,9 +41,9 @@ public abstract class DamagePreventEffectBase extends SpellAbilityEffect {
if (sa.hasParam("PreventionSubAbility")) {
String subAbString = sa.getSVar(sa.getParam("PreventionSubAbility"));
if (sa.hasParam("ShieldEffectTarget")) {
List<GameObject> effTgts = AbilityUtils.getDefinedObjects(hostCard, sa.getParam("ShieldEffectTarget"), sa);
List<GameEntity> effTgts = AbilityUtils.getDefinedEntities(hostCard, sa.getParam("ShieldEffectTarget"), sa);
String effTgtString = "";
for (final GameObject effTgt : effTgts) {
for (final GameEntity effTgt : effTgts) {
if (effTgt instanceof Card) {
effTgtString = "CardUID_" + String.valueOf(((Card) effTgt).getId());
} else if (effTgt instanceof Player) {
@@ -70,12 +71,21 @@ public abstract class DamagePreventEffectBase extends SpellAbilityEffect {
eff.updateStateForView();
game.getTriggerHandler().clearSuppression(TriggerType.ChangesZone);
o.getView().updatePreventNextDamage(o);
if (o instanceof Player) {
game.fireEvent(new GameEventPlayerStatsChanged((Player) o, false));
}
game.getEndOfTurn().addUntil(new GameCommand() {
private static final long serialVersionUID = 1L;
@Override
public void run() {
game.getAction().exile(eff, null);
o.getView().updatePreventNextDamage(o);
if (o instanceof Player) {
game.fireEvent(new GameEventPlayerStatsChanged((Player) o, false));
}
}
});
}

View File

@@ -5636,7 +5636,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
/**
* Gets the total damage done by card this turn (after prevention and redirects).
*
* @return the damage done to player p this turn
* @return the damage done by the card this turn
*/
public final int getTotalDamageDoneBy() {
return getDamageHistory().getDamageDoneThisTurn(null, false, null, null, this, getController(), null);

View File

@@ -19,6 +19,7 @@ import forge.game.Game;
import forge.game.GameEntity;
import forge.game.GameObjectPredicates;
import forge.game.ability.AbilityKey;
import forge.game.event.GameEventPlayerStatsChanged;
import forge.game.player.Player;
import forge.game.player.PlayerCollection;
import forge.game.spellability.SpellAbility;
@@ -48,6 +49,11 @@ public class CardDamageMap extends ForwardingTable<Card, GameEntity, Integer> {
runParams.put(AbilityKey.IsCombatDamage, isCombat);
ge.getGame().getTriggerHandler().runTrigger(TriggerType.DamagePreventedOnce, runParams, false);
ge.getView().updatePreventNextDamage(ge);
if (ge instanceof Player) {
ge.getGame().fireEvent(new GameEventPlayerStatsChanged((Player) ge, false));
}
}
}
}

View File

@@ -362,7 +362,6 @@ public class AttackConstraints {
MapToAmount<GameEntity> sortedPlayerReqs = new LinkedHashMapToAmount<>();
sortedPlayerReqs.addAll(Iterables.concat(playerReqs));
while (!sortedPlayerReqs.isEmpty()) {
sortedPlayerReqs.keySet().removeAll(excludedDefenders);
Pair<GameEntity, Integer> playerReq = MapToAmountUtil.max(sortedPlayerReqs);
// find best attack to also fulfill the additional requirements
Attack bestMatch = Iterables.getLast(Iterables.filter(result, att -> !usedAttackers.contains(att.attacker) && att.defender.equals(playerReq.getLeft())), null);
@@ -376,6 +375,7 @@ public class AttackConstraints {
} else {
excludedDefenders.add(playerReq.getLeft());
}
sortedPlayerReqs.keySet().removeAll(excludedDefenders);
}
if (!usedAttackers.isEmpty()) {
// order could have changed

View File

@@ -42,6 +42,7 @@ import forge.game.keyword.KeywordInterface;
import forge.game.player.Player;
import forge.game.player.PlayerController.BinaryChoiceType;
import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbilityCantPhaseOut;
import forge.game.zone.ZoneType;
/**
@@ -266,14 +267,14 @@ public class Untap extends Phase {
// If c is attached to something, it will phase out on its own, and try
// to attach back to that thing when it comes back
for (final Card c : list) {
if (c.isPhasedOut()) {
if (c.isPhasedOut() && c.isDirectlyPhasedOut()) {
c.phase(true);
} else if (c.hasKeyword(Keyword.PHASING)) {
// CR 702.26h If an object would simultaneously phase out directly
// and indirectly, it just phases out indirectly.
if (c.isAttachment()) {
final Card ent = c.getAttachedTo();
if (ent != null && list.contains(ent)) {
if (ent != null && list.contains(ent) && !StaticAbilityCantPhaseOut.cantPhaseOut(ent)) {
continue;
}
}

View File

@@ -145,7 +145,6 @@ public abstract class ReplacementEffect extends TriggerReplacementBase {
public boolean requirementsCheck(Game game) {
return this.requirementsCheck(game, this.getMapParams());
}
public boolean requirementsCheck(Game game, Map<String,String> params) {
if (this.isSuppressed()) {
return false; // Effect removed by effect

View File

@@ -911,7 +911,8 @@ public class ReplacementHandler {
&& re.hasParam("PreventionEffect")
&& re.zonesCheck(game.getZoneOf(c))
&& re.getOverridingAbility() != null
&& re.getOverridingAbility().getApi() == ApiType.ReplaceDamage) {
&& re.getOverridingAbility().getApi() == ApiType.ReplaceDamage
&& re.matchesValidParam("ValidTarget", o)) {
list.add(re);
}
}

View File

@@ -14,7 +14,6 @@ public enum TrackableProperty {
Text(TrackableTypes.StringType),
PreventNextDamage(TrackableTypes.IntegerType),
AttachedCards(TrackableTypes.CardViewCollectionType),
AllAttachedCards(TrackableTypes.CardViewCollectionType),
Counters(TrackableTypes.CounterMapType),
CurrentPlane(TrackableTypes.StringType),
PlanarPlayer(TrackableTypes.PlayerViewType),

View File

@@ -595,8 +595,7 @@ public class PlayArea extends CardPanelContainer implements CardPanelMouseListen
Iterable<CardView> cards = model.getCards(zone);
if (cards != null) {
modelCopy = Lists.newArrayList(cards);
}
else {
} else {
modelCopy = Lists.newArrayList();
}
}

View File

@@ -2,5 +2,6 @@ Name:Break of Day
ManaCost:1 W
Types:Instant
A:SP$ PumpAll | Cost$ 1 W | ValidCards$ Creature.YouCtrl | NumAtt$ +1 | NumDef$ +1 | SubAbility$ FatefulHourPump | SpellDescription$ Creatures you control get +1/+1 until end of turn.
SVar:FatefulHourPump:DB$ PumpAll | ValidCards$ Creature.YouCtrl | KW$ Indestructible | FatefulHour$ True | SpellDescription$ Fateful hour — If you have 5 or less life, those creatures gain indestructible until end of turn.
SVar:FatefulHourPump:DB$ PumpAll | ValidCards$ Creature.YouCtrl | KW$ Indestructible | ConditionCheckSVar$ FatefulHour | ConditionSVarCompare$ LE5 | SpellDescription$ Fateful hour — If you have 5 or less life, those creatures gain indestructible until end of turn.
SVar:FatefulHour:Count$YourLifeTotal
Oracle:Creatures you control get +1/+1 until end of turn.\nFateful hour — If you have 5 or less life, those creatures gain indestructible until end of turn. (Damage and effects that say "destroy" don't destroy them.)

View File

@@ -2,7 +2,7 @@ Name:Ogre Slumlord
ManaCost:3 B B
Types:Creature Ogre Rogue
PT:3/3
T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Creature.nonToken+Other | TriggerZones$ Battlefield | Execute$ TrigToken | TriggerDescription$ Whenever another nontoken creature dies, you may create a 1/1 black Rat creature token.
T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Creature.nonToken+Other | OptionalDecider$ You | TriggerZones$ Battlefield | Execute$ TrigToken | TriggerDescription$ Whenever another nontoken creature dies, you may create a 1/1 black Rat creature token.
SVar:TrigToken:DB$ Token | TokenAmount$ 1 | TokenScript$ b_1_1_rat | TokenOwner$ You
S:Mode$ Continuous | Affected$ Creature.Rat+YouCtrl | AddKeyword$ Deathtouch | Description$ Rats you control have deathtouch.
DeckHas:Ability$Token

View File

@@ -4,7 +4,7 @@ Types:Legendary Creature Spirit Naga
PT:0/0
K:etbCounter:P1P1:X
SVar:X:Count$xPaid
T:Mode$ CounterAddedOnce | ValidCard$ Creature.Other+inZoneBattlefield+Colorless | OptionalDecider$ You | TriggerZones$ Battlefield | CounterType$ P1P1 | Execute$ TrigPutCounter | TriggerDescription$ Whenever you put one or more +1/+1 counters on another colorless creature, you may put a +1/+1 counter on NICKNAME.
T:Mode$ CounterAddedOnce | ValidCard$ Creature.Other+inZoneBattlefield+Colorless | ValidPlayer$ You | OptionalDecider$ You | TriggerZones$ Battlefield | CounterType$ P1P1 | Execute$ TrigPutCounter | TriggerDescription$ Whenever you put one or more +1/+1 counters on another colorless creature, you may put a +1/+1 counter on NICKNAME.
SVar:TrigPutCounter:DB$ PutCounter | CounterType$ P1P1 | CounterNum$ 1
T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Card.Self | Execute$ TrigManifest | TriggerDescription$ When NICKNAME dies, manifest a number of cards from the top of your library equal to the number of counters on it.
SVar:TrigManifest:DB$ Manifest | Amount$ Y | Defined$ TopOfLibrary

View File

@@ -4,7 +4,7 @@ Types:Enchantment Aura
K:Flash
K:Enchant creature you control
A:SP$ Attach | ValidTgts$ Creature.YouCtrl | TgtPrompt$ Select target creature you control | AILogic$ Pump
T:Mode$ ChangesZone | ValidCard$ Creature.EnchantedBy,Creature.modified+NotEnchantedBy | Origin$ Battlefield | Destination$ Graveyard | Execute$ TrigToken | TriggerDescription$ Whenever enchanted creature or another modified creature you control dies, create X 1/1 colorless Spirit creature tokens, where X is that creature's power. (Equipment, Auras you control, and counters are modifications.)
T:Mode$ ChangesZone | ValidCard$ Creature.EnchantedBy,Creature.YouCtrl+modified+NotEnchantedBy | Origin$ Battlefield | Destination$ Graveyard | Execute$ TrigToken | TriggerDescription$ Whenever enchanted creature or another modified creature you control dies, create X 1/1 colorless Spirit creature tokens, where X is that creature's power. (Equipment, Auras you control, and counters are modifications.)
SVar:TrigToken:DB$ Token | TokenScript$ c_1_1_spirit | TokenAmount$ X
SVar:X:TriggeredCard$CardPower
DeckHas:Ability$Token & Type$Spirit

View File

@@ -3,5 +3,5 @@ ManaCost:2 R
Types:Creature Goblin Rogue
PT:3/2
T:Mode$ TapsForMana | ValidCard$ Artifact | Activator$ Opponent | TriggerZones$ Battlefield | Execute$ TrigControl | TriggerDescription$ Whenever an opponent taps an artifact for mana, gain control of that artifact until the end of your next turn.
SVar:TrigControl:DB$ GainControl | Defined$ TriggeredCard | LoseControl$ UntilTheEndOfYourNextTurn
SVar:TrigControl:DB$ GainControl | Defined$ TriggeredCardLKICopy | LoseControl$ UntilTheEndOfYourNextTurn
Oracle:Whenever an opponent taps an artifact for mana, gain control of that artifact until the end of your next turn.

View File

@@ -2,7 +2,7 @@ Name:Unexpected Allies
ManaCost:1 R
Types:Sorcery
A:SP$ Pump | ValidTgts$ Creature.YouCtrl+nonToken | TgtPrompt$ Select target nontoken creature you control | KW$ Double team | NumAtt$ 2 | SubAbility$ TrigEffect | SpellDescription$ Target nontoken creature you control gets +2/+0 and gains double team until end of turn. It also gains first strike until end of turn if it has the same name as another creature you control or a creature card in your graveyard.
SVar:TrigEffect:DB$ Pump | ConditionCheckSVar$ X | Defined$ Targeted | KW$ First strike
SVar:TrigEffect:DB$ Pump | ConditionCheckSVar$ X | Defined$ Targeted | KW$ First Strike
SVar:X:Count$Valid Creature.NotDefinedTargeted+YouCtrl+sharesNameWith Targeted/Plus.Y
SVar:Y:Count$ValidGraveyard Creature.YouOwn+sharesNameWith Targeted
DeckHas:Keyword$Double Team|First Strike

View File

@@ -69,7 +69,6 @@ public class InputAttack extends InputSyncronizedBase {
@Override
public final void showMessage() {
// TODO still seems to have some issues with multiple planeswalkers
setCurrentDefender(defenders.getFirst());
if (currentDefender == null) {
@@ -295,7 +294,9 @@ public class InputAttack extends InputSyncronizedBase {
getController().getGui().setHighlighted(PlayerView.get((Player) ge), ge == def);
}
}
if (def != null) {
potentialBanding = isBandingPossible();
}
updateMessage();
}