Merge branch 'Card-Forge:master' into master

This commit is contained in:
TabletopGeneral
2023-07-09 22:51:31 -04:00
committed by GitHub
22 changed files with 109 additions and 61 deletions

View File

@@ -807,8 +807,9 @@ public class AiController {
// Account for possible Ward after the spell is fully targeted
// TODO: ideally, this should be done while targeting, so that a different target can be preferred if the best
// one is warded and can't be paid for. (currently it will be stuck with the target until it could pay)
if (sa.usesTargeting() && (!sa.isSpell() || CardFactoryUtil.isCounterable(host))) {
for (Card tgt : sa.getTargets().getTargetCards()) {
if (!sa.isSpell() || CardFactoryUtil.isCounterable(host)) {
for (TargetChoices tc : sa.getAllTargetChoices()) {
for (Card tgt : tc.getTargetCards()) {
// TODO some older cards don't use the keyword, so check for trigger instead
if (tgt.hasKeyword(Keyword.WARD) && tgt.isInPlay() && tgt.getController().isOpponentOf(host.getController())) {
int amount = 0;
@@ -827,6 +828,7 @@ public class AiController {
}
}
}
}
// check if some target raised cost
if (oldCMC > -1) {
@@ -1332,10 +1334,7 @@ public class AiController {
if (sa == null) {
return null;
}
final List<SpellAbility> abilities = Lists.newArrayList();
abilities.add(sa);
return abilities;
return Lists.newArrayList(sa);
}
public List<SpellAbility> chooseSpellAbilityToPlay() {

View File

@@ -1720,16 +1720,12 @@ public class ComputerUtil {
return threatened;
}
} else {
objects = topStack.getTargets();
final List<GameObject> canBeTargeted = new ArrayList<>();
for (Object o : objects) {
if (o instanceof GameEntity) {
final GameEntity ge = (GameEntity) o;
for (GameEntity ge : topStack.getTargets().getTargetEntities()) {
if (ge.canBeTargetedBy(topStack)) {
canBeTargeted.add(ge);
}
}
}
if (canBeTargeted.isEmpty()) {
return threatened;
}

View File

@@ -1865,7 +1865,6 @@ public class ComputerUtilCard {
*/
public static boolean canPumpAgainstRemoval(Player ai, SpellAbility sa) {
final List<GameObject> objects = ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), sa, true);
final CardCollection threatenedTargets = new CardCollection();
if (!sa.usesTargeting()) {
final List<Card> cards = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
@@ -1877,14 +1876,11 @@ public class ComputerUtilCard {
// For pumps without targeting restrictions, just return immediately until this is fleshed out.
return false;
}
CardCollection targetables = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
targetables = ComputerUtil.getSafeTargets(ai, sa, targetables);
for (final Card c : targetables) {
if (objects.contains(c)) {
threatenedTargets.add(c);
}
}
CardCollection threatenedTargets = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
threatenedTargets = ComputerUtil.getSafeTargets(ai, sa, threatenedTargets);
threatenedTargets.retainAll(objects);
if (!threatenedTargets.isEmpty()) {
sortByEvaluateCreature(threatenedTargets);
for (Card c : threatenedTargets) {

View File

@@ -22,6 +22,7 @@ import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.ai.ability.AnimateAi;
import forge.ai.ability.FightAi;
import forge.card.ColorSet;
import forge.card.MagicColor;
import forge.card.mana.ManaCost;
@@ -79,6 +80,49 @@ import java.util.List;
*/
public class SpecialCardAi {
// Arena and Magus of the Arena
public static class Arena {
public static boolean consider(final Player ai, final SpellAbility sa) {
final Game game = ai.getGame();
if (!game.getPhaseHandler().is(PhaseType.END_OF_TURN) || game.getPhaseHandler().getNextTurn() != ai) {
return false; // at opponent's EOT only, to conserve mana
}
CardCollection aiCreatures = ai.getCreaturesInPlay();
if (aiCreatures.isEmpty()) {
return false;
}
for (Player opp : ai.getOpponents()) {
CardCollection oppCreatures = opp.getCreaturesInPlay();
if (oppCreatures.isEmpty()) {
continue;
}
for (Card aiCreature : aiCreatures) {
boolean canKillAll = true;
for (Card oppCreature : oppCreatures) {
if (FightAi.canKill(oppCreature, aiCreature, 0)) {
canKillAll = false;
break;
}
if (!FightAi.canKill(aiCreature, oppCreature, 0)) {
canKillAll = false;
break;
}
}
if (canKillAll) {
sa.getTargets().clear();
sa.getTargets().add(aiCreature);
return true;
}
}
}
return sa.isTargetNumberValid();
}
}
// Black Lotus and Lotus Bloom
public static class BlackLotus {
public static boolean consider(final Player ai, final SpellAbility sa, final ManaCostBeingPaid cost) {

View File

@@ -30,7 +30,6 @@ public class FightAi extends SpellAbilityAi {
// everything is defined or targeted above, can't do anything there unless a specific logic is set
if (sa.hasParam("Defined") && !sa.usesTargeting()) {
// TODO extend Logic for cards like Arena
return true;
}

View File

@@ -292,10 +292,6 @@ public class PumpAi extends PumpAiBase {
if (numDefense.contains("X") && sa.getSVar("X").equals("Count$xPaid")) {
// Set PayX here to maximum value.
int xPay = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
if (sourceName.equals("Necropolis Fiend")) {
xPay = Math.min(xPay, sa.getActivatingPlayer().getCardsIn(ZoneType.Graveyard).size());
sa.setSVar("X", Integer.toString(xPay));
}
sa.setXManaCostPaid(xPay);
defense = xPay;
if (numDefense.equals("-X")) {

View File

@@ -46,8 +46,11 @@ public class TapAi extends TapAiBase {
final Card source = sa.getHostCard();
final Cost abCost = sa.getPayCosts();
if ("GoblinPolkaBand".equals(sa.getParam("AILogic"))) {
final String aiLogic = sa.getParamOrDefault("AILogic", "");
if ("GoblinPolkaBand".equals(aiLogic)) {
return SpecialCardAi.GoblinPolkaBand.consider(ai, sa);
} else if ("Arena".equals(aiLogic)) {
return SpecialCardAi.Arena.consider(ai, sa);
}
if (!ComputerUtilCost.checkDiscardCost(ai, abCost, source, sa)) {

View File

@@ -325,6 +325,15 @@ public abstract class TapAiBase extends SpellAbilityAi {
@Override
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
final Card source = sa.getHostCard();
final boolean oppTargetsChoice = sa.hasParam("TargetingPlayer");
if (oppTargetsChoice && sa.getActivatingPlayer().equals(ai) && !sa.isTrigger()) {
// canPlayAI (sa activated by ai)
Player targetingPlayer = AbilityUtils.getDefinedPlayers(source, sa.getParam("TargetingPlayer"), sa).get(0);
sa.setTargetingPlayer(targetingPlayer);
sa.getTargets().clear();
return targetingPlayer.getController().chooseTargetsFor(sa);
}
boolean randomReturn = true;

View File

@@ -1,5 +1,6 @@
package forge.util;
import java.text.DecimalFormat;
import java.text.Normalizer;
import java.util.ArrayList;
import java.util.List;
@@ -42,6 +43,10 @@ public class TextUtil {
return Normalizer.normalize(text, Normalizer.Form.NFD);
}
private static final DecimalFormat df = new DecimalFormat("#.##");
public static String decimalFormat(float value) {
return df.format(value);
}
/**
* Safely converts an object to a String.
*

View File

@@ -1252,6 +1252,10 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
player.shuffle(sa);
}
if (sa.hasParam("Reorder")) {
chosenCards = new CardCollection(decider.getController().orderMoveToZoneList(chosenCards, destination, sa));
}
// remove Controlled While Searching
if (controlTimestamp != null) {
player.removeController(controlTimestamp);

View File

@@ -32,6 +32,7 @@ import forge.localinstance.achievements.CardActivationAchievements;
import forge.localinstance.achievements.PlaneswalkerAchievements;
import forge.model.FModel;
import forge.player.GamePlayerUtil;
import forge.util.TextUtil;
import org.apache.commons.lang3.tuple.Pair;
import java.util.Map;
@@ -216,7 +217,7 @@ public class PlayerStatisticScene extends UIScene {
totalLoss.setText(String.valueOf(Current.player().getStatistic().totalLoss()));
}
if (lossWinRatio != null) {
lossWinRatio.setText(Float.toString(Current.player().getStatistic().winLossRatio()));
lossWinRatio.setText(TextUtil.decimalFormat(Current.player().getStatistic().winLossRatio()));
}
if (eventMatchWins != null) {
eventMatchWins.setText(String.valueOf(Current.player().getStatistic().eventWins()));

View File

@@ -12,5 +12,5 @@ ALTERNATE
Name:Explosive Crystal
ManaCost:4 R
Types:Sorcery Adventure
A:SP$ DealDamage | ValidTgts$ Any to distribute damage to | NumDmg$ 4 | TargetMin$ 0 | TargetMax$ 4 | DividedAsYouChoose$ 4 | SpellDescription$ CARDNAME deals 4 damage divided as you choose among any number of targets.
A:SP$ DealDamage | ValidTgts$ Any | NumDmg$ 4 | TargetMin$ 0 | TargetMax$ 4 | DividedAsYouChoose$ 4 | SpellDescription$ CARDNAME deals 4 damage divided as you choose among any number of targets.
Oracle:Explosive Crystal deals 4 damage divided as you choose among any number of targets.

View File

@@ -1,9 +1,7 @@
Name:Arena
ManaCost:no cost
Types:Land
A:AB$ Tap | Cost$ 3 T | ValidTgts$ Creature.YouCtrl | TgtPrompt$ Select target creature you control | SubAbility$ DBTap | AlwaysRemember$ True | SpellDescription$ Tap target creature you control and target creature of an opponent's choice they control. Those creatures fight each other. (Each deals damage equal to its power to the other.)
SVar:DBTap:DB$ Tap | TargetingPlayer$ Player.Opponent | TargetingPlayerControls$ True | ValidTgts$ Creature | TgtPrompt$ Select target creature you control | SubAbility$ DBFight | AlwaysRemember$ True
SVar:DBFight:DB$ Fight | Defined$ Remembered | SubAbility$ DBCleanup
SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True
AI:RemoveDeck:All
A:AB$ Tap | Cost$ 3 T | ValidTgts$ Creature.YouCtrl | TgtPrompt$ Select target creature you control | SubAbility$ DBTap | AILogic$ Arena | SpellDescription$ Tap target creature you control and target creature of an opponent's choice they control. Those creatures fight each other. (Each deals damage equal to its power to the other.)
SVar:DBTap:DB$ Tap | TargetingPlayer$ Player.Opponent | TargetingPlayerControls$ True | ValidTgts$ Creature | TgtPrompt$ Select target creature you control | SubAbility$ DBFight
SVar:DBFight:DB$ Fight | Defined$ Targeted
Oracle:{3}, {T}: Tap target creature you control and target creature of an opponent's choice they control. Those creatures fight each other. (Each deals damage equal to its power to the other.)

View File

@@ -1,5 +1,5 @@
Name:Congregation at Dawn
ManaCost:G G W
Types:Instant
A:SP$ ChangeZone | Cost$ G G W | Origin$ Library | Destination$ Library | LibraryPosition$ 0 | ChangeType$ Creature | ChangeNum$ 3 | SpellDescription$ Search your library for up to three creature cards, reveal them, then shuffle and put those cards on top in any order.
A:SP$ ChangeZone | Cost$ G G W | Origin$ Library | Destination$ Library | LibraryPosition$ 0 | ChangeType$ Creature | ChangeNum$ 3 | Reorder$ True | SpellDescription$ Search your library for up to three creature cards, reveal them, then shuffle and put those cards on top in any order.
Oracle:Search your library for up to three creature cards, reveal them, then shuffle and put those cards on top in any order.

View File

@@ -3,7 +3,7 @@ ManaCost:2 R
Types:Creature Dwarf
PT:2/2
T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigChangeZone | TriggerDescription$ When CARDNAME enters the battlefield, search your library for any number of Dwarf cards, reveal them, then shuffle and put those cards on top in any order.
SVar:TrigChangeZone:DB$ ChangeZone | ChangeNum$ X | ChangeType$ Dwarf | Origin$ Library | Destination$ Library | LibraryPosition$ 0
SVar:TrigChangeZone:DB$ ChangeZone | ChangeNum$ X | ChangeType$ Dwarf | Origin$ Library | Destination$ Library | LibraryPosition$ 0 | Reorder$ True
SVar:X:Count$InYourLibrary.Dwarf
AI:RemoveDeck:All
DeckNeeds:Type$Dwarf

View File

@@ -3,7 +3,7 @@ ManaCost:1 R
Types:Creature Goblin
PT:1/1
T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigChangeZone | TriggerDescription$ When CARDNAME enters the battlefield, search your library for any number of Goblin cards, reveal them, then shuffle and put those cards on top in any order.
SVar:TrigChangeZone:DB$ ChangeZone | ChangeNum$ X | ChangeType$ Goblin | Origin$ Library | Destination$ Library | LibraryPosition$ 0
SVar:TrigChangeZone:DB$ ChangeZone | ChangeNum$ X | ChangeType$ Goblin | Origin$ Library | Destination$ Library | LibraryPosition$ 0 | Reorder$ True
SVar:X:Count$InYourLibrary.Goblin
DeckNeeds:Type$Goblin
#TODO: The AI generally is able to use this card, but will basically place all of its goblins on top of the library in no specific order, which is not very smart. Might need some improvement before RemoveDeck is removed. Currently adding a restriction to at least only play it if the AI has enough lands out on the battlefield already.

View File

@@ -1,7 +1,7 @@
Name:Insidious Dreams
ManaCost:3 B
Types:Instant
A:SP$ ChangeZone | Cost$ 3 B Discard<X/Card> | Origin$ Library | Destination$ Library | ChangeType$ Card | ChangeNum$ X | LibraryPosition$ 0 | Mandatory$ True | SpellDescription$ Search your library for X cards, then shuffle and put those cards on top in any order.
A:SP$ ChangeZone | Cost$ 3 B Discard<X/Card> | Origin$ Library | Destination$ Library | ChangeType$ Card | ChangeNum$ X | LibraryPosition$ 0 | Mandatory$ True | Reorder$ True | SpellDescription$ Search your library for X cards, then shuffle and put those cards on top in any order.
SVar:X:Count$xPaid
AI:RemoveDeck:All
Oracle:As an additional cost to cast this spell, discard X cards.\nSearch your library for X cards, then shuffle and put those cards on top in any order.

View File

@@ -7,7 +7,7 @@ SVar:CheckLifePaid:DB$ StoreSVar | SVar$ LifePaid | Type$ Number | Expression$ 1
SVar:DBResetRem:DB$ Cleanup | ClearRemembered$ True | SubAbility$ GoToBottom
SVar:GoToBottom:DB$ Dig | DigNum$ 5 | ChangeNum$ All | DestinationZone$ Library | LibraryPosition$ -1 | NoLooking$ True | SubAbility$ DBLookAgain | StackDescription$ None
SVar:DBLookAgain:DB$ PeekAndReveal | PeekAmount$ 5 | NoReveal$ True | RememberPeeked$ True | StackDescription$ None
SVar:DBShuffle:DB$ ChangeZone | Origin$ Library | Destination$ Library | LibraryPosition$ 0 | ChangeType$ Card.IsRemembered | ChangeNum$ 5 | SubAbility$ DBReset | Hidden$ True | SelectPrompt$ Pick 1 on the top of library | Mandatory$ True | NoReveal$ True | NoLooking$ True | StackDescription$ None
SVar:DBShuffle:DB$ ChangeZone | Origin$ Library | Destination$ Library | LibraryPosition$ 0 | ChangeType$ Card.IsRemembered | ChangeNum$ 5 | SubAbility$ DBReset | Hidden$ True | SelectPrompt$ Pick 1 on the top of library | Mandatory$ True | NoReveal$ True | NoLooking$ True | Reorder$ True | StackDescription$ None
SVar:DBReset:DB$ StoreSVar | SVar$ LifePaid | Type$ Number | Expression$ 0 | SubAbility$ DBCleanup
SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True
SVar:LifePaid:Number$0

View File

@@ -2,9 +2,7 @@ Name:Magus of the Arena
ManaCost:4 R R
Types:Creature Human Wizard
PT:5/5
A:AB$ Tap | Cost$ 3 T | ValidTgts$ Creature.YouCtrl | TgtPrompt$ Select target creature you control | SubAbility$ DBTap | AlwaysRemember$ True | SpellDescription$ Tap target creature you control and target creature of an opponent's choice they control. Those creatures fight each other. (Each deals damage equal to its power to the other.)
SVar:DBTap:DB$ Tap | TargetingPlayer$ Player.Opponent | TargetingPlayerControls$ True | ValidTgts$ Creature | TgtPrompt$ Select target creature you control | SubAbility$ DBFight | AlwaysRemember$ True
SVar:DBFight:DB$ Fight | Defined$ Remembered | SubAbility$ DBCleanup
SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True
AI:RemoveDeck:All
A:AB$ Tap | Cost$ 3 T | ValidTgts$ Creature.YouCtrl | TgtPrompt$ Select target creature you control | SubAbility$ DBTap | AILogic$ Arena | SpellDescription$ Tap target creature you control and target creature of an opponent's choice they control. Those creatures fight each other. (Each deals damage equal to its power to the other.)
SVar:DBTap:DB$ Tap | TargetingPlayer$ Player.Opponent | TargetingPlayerControls$ True | ValidTgts$ Creature | TgtPrompt$ Select target creature you control | SubAbility$ DBFight
SVar:DBFight:DB$ Fight | Defined$ Targeted
Oracle:{3}, {T}: Tap target creature you control and target creature of an opponent's choice they control. Those creatures fight each other. (Each deals damage equal to its power to the other.)

View File

@@ -2,7 +2,7 @@ Name:Nova Pentacle
ManaCost:4
Types:Artifact
A:AB$ ChooseSource | Cost$ 3 T | Choices$ Card,Emblem | AILogic$ NeedsPrevention | SubAbility$ DBEffect | SpellDescription$ The next time a source of your choice would deal damage to you this turn, that damage is dealt to target creature of an opponent's choice instead.
SVar:DBEffect:DB$ Effect | TargetingPlayer$ Player.Opponent | ValidTgts$ Creature | TgtPrompt$ Select target creature to redirect the damage to | ReplacementEffects$ SelflessDamage | ExileOnMoved$ Battlefield | RememberObjects$ Targeted | ExileOnMoved$ Battlefield | SubAbility$ DBCleanup | ConditionDefined$ ChosenCard | ConditionPresent$ Card,Emblem
SVar:DBEffect:DB$ Effect | TargetingPlayer$ Player.Opponent | ValidTgts$ Creature | TgtPrompt$ Select target creature to redirect the damage to | ReplacementEffects$ SelflessDamage | ExileOnMoved$ Battlefield | RememberObjects$ Targeted | SubAbility$ DBCleanup | ConditionDefined$ ChosenCard | ConditionPresent$ Card,Emblem
SVar:SelflessDamage:Event$ DamageDone | ValidTarget$ You | ValidSource$ Card.ChosenCardStrict,Emblem.ChosenCard | ReplaceWith$ SelflessDmg | DamageTarget$ Remembered | Description$ The next time a source of your choice would deal damage to you this turn, that damage is dealt to target creature of an opponent's choice instead.
SVar:SelflessDmg:DB$ ReplaceEffect | VarName$ Affected | VarValue$ Remembered | VarType$ Card | SubAbility$ ExileEffect
SVar:ExileEffect:DB$ ChangeZone | Defined$ Self | Origin$ Command | Destination$ Exile

View File

@@ -1,7 +1,7 @@
Name:Scouting Trek
ManaCost:1 G
Types:Sorcery
A:SP$ ChangeZone | Cost$ 1 G | ChangeNum$ X | ChangeType$ Land.Basic | Origin$ Library | Destination$ Library | LibraryPosition$ 0 | SpellDescription$ Search your library for any number of basic land cards, reveal those cards, then shuffle and put them on top.
A:SP$ ChangeZone | Cost$ 1 G | ChangeNum$ X | ChangeType$ Land.Basic | Origin$ Library | Destination$ Library | Reorder$ True | LibraryPosition$ 0 | SpellDescription$ Search your library for any number of basic land cards, reveal those cards, then shuffle and put them on top.
SVar:X:Count$InYourLibrary.Land.Basic
AI:RemoveDeck:All
Oracle:Search your library for any number of basic land cards, reveal those cards, then shuffle and put them on top.

View File

@@ -2,7 +2,7 @@ Name:Ulvenwald Tracker
ManaCost:G
Types:Creature Human Shaman
PT:1/1
A:AB$ Pump | Cost$ 1 G T | ValidTgts$ Creature.YouCtrl | TgtPrompt$ Select target creature you control | SubAbility$ TrackerFight | StackDescription$ None
SVar:TrackerFight:DB$ Fight | Defined$ ParentTarget | ValidTgts$ Creature | TargetUnique$ True | TgtPrompt$ Select another target creature | SpellDescription$ Target creature you control fights another target creature.
A:AB$ Pump | Cost$ 1 G T | ValidTgts$ Creature.YouCtrl | TgtPrompt$ Select target creature you control | SubAbility$ TrackerFight | StackDescription$ None | SpellDescription$ Target creature you control fights another target creature.
SVar:TrackerFight:DB$ Fight | Defined$ ParentTarget | ValidTgts$ Creature | TargetUnique$ True | TgtPrompt$ Select another target creature
AI:RemoveDeck:All
Oracle:{1}{G}, {T}: Target creature you control fights another target creature.