mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-19 20:28:00 +00:00
Regenerate rework (#3385)
* CardFactoryUtil: remove false keyword * CardFactoryUtil: remove wrong regenerate Keyword * CantRegenerate as Static * ~small fixes * RegenerationAbility as SubAbility to the Regeneration Effect * Card: use shieldCount instead of Collection * remove deprecated trigger * fix AiLogic vs AILogic * EffectAi: start of logic for CantRegenerate * EffectAi: try to do StackPeek for CantRegenerate * ~ use wither damage * AI prediction against Damage * CantRegenerate: begin logic against Combat Damage * AnimateAi: start logic for Bone Shaman * fix Runesword
This commit is contained in:
@@ -2141,7 +2141,7 @@ public class AiController {
|
||||
}
|
||||
|
||||
private <T extends CardTraitBase> List<T> filterListByAiLogic(List<T> list, final String logic) {
|
||||
return filterList(list, CardTraitPredicates.hasParam("AiLogic", logic));
|
||||
return filterList(list, CardTraitPredicates.hasParam("AILogic", logic));
|
||||
}
|
||||
|
||||
public List<AbilitySub> chooseModeForAbility(SpellAbility sa, List<AbilitySub> possible, int min, int num, boolean allowRepeat) {
|
||||
|
||||
@@ -3007,11 +3007,11 @@ public class ComputerUtil {
|
||||
repParams,
|
||||
ReplacementLayer.Other);
|
||||
|
||||
if (Iterables.any(list, CardTraitPredicates.hasParam("AiLogic", "NoLife"))) {
|
||||
if (Iterables.any(list, CardTraitPredicates.hasParam("AILogic", "NoLife"))) {
|
||||
return false;
|
||||
} else if (Iterables.any(list, CardTraitPredicates.hasParam("AiLogic", "LoseLife"))) {
|
||||
} else if (Iterables.any(list, CardTraitPredicates.hasParam("AILogic", "LoseLife"))) {
|
||||
return false;
|
||||
} else if (Iterables.any(list, CardTraitPredicates.hasParam("AiLogic", "LichDraw"))) {
|
||||
} else if (Iterables.any(list, CardTraitPredicates.hasParam("AILogic", "LichDraw"))) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -3036,13 +3036,13 @@ public class ComputerUtil {
|
||||
ReplacementLayer.Other
|
||||
);
|
||||
|
||||
if (Iterables.any(list, CardTraitPredicates.hasParam("AiLogic", "NoLife"))) {
|
||||
if (Iterables.any(list, CardTraitPredicates.hasParam("AILogic", "NoLife"))) {
|
||||
// no life gain is not negative
|
||||
return false;
|
||||
} else if (Iterables.any(list, CardTraitPredicates.hasParam("AiLogic", "LoseLife"))) {
|
||||
} else if (Iterables.any(list, CardTraitPredicates.hasParam("AILogic", "LoseLife"))) {
|
||||
// lose life is only negative is the player can lose life
|
||||
return player.canLoseLife();
|
||||
} else if (Iterables.any(list, CardTraitPredicates.hasParam("AiLogic", "LichDraw"))) {
|
||||
} else if (Iterables.any(list, CardTraitPredicates.hasParam("AILogic", "LichDraw"))) {
|
||||
// if it would draw more cards than player has, then its negative
|
||||
return player.getCardsIn(ZoneType.Library).size() <= n;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import forge.ai.*;
|
||||
import forge.card.CardType;
|
||||
import forge.card.ColorSet;
|
||||
import forge.game.CardTraitPredicates;
|
||||
import forge.game.Game;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.ApiType;
|
||||
@@ -20,6 +22,7 @@ import forge.game.staticability.StaticAbility;
|
||||
import forge.game.staticability.StaticAbilityContinuous;
|
||||
import forge.game.staticability.StaticAbilityLayer;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.FileSection;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
@@ -54,6 +57,23 @@ public class AnimateAi extends SpellAbilityAi {
|
||||
if ("EOT".equals(aiLogic) && ph.getPhase().isBefore(PhaseType.MAIN2)) {
|
||||
return false;
|
||||
}
|
||||
if ("BoneManCantRegenerate".equals(aiLogic)) {
|
||||
Card host = sa.getHostCard();
|
||||
String svar = AbilityUtils.getSVar(sa, sa.getParam("staticAbilities"));
|
||||
if (svar == null) {
|
||||
return false;
|
||||
}
|
||||
Map<String, String> map = FileSection.parseToMap(svar, FileSection.DOLLAR_SIGN_KV_SEPARATOR);
|
||||
if (!map.containsKey("Description")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// check for duplicate static ability
|
||||
if (Iterables.any(host.getStaticAbilities(), CardTraitPredicates.hasParam("Description", map.get("Description")))) {
|
||||
return false;
|
||||
}
|
||||
// TODO check if Bone Man would deal damage to something that otherwise would regenerate
|
||||
}
|
||||
return super.checkAiLogic(ai, sa, aiLogic);
|
||||
}
|
||||
|
||||
|
||||
@@ -475,8 +475,8 @@ public class CountersMoveAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional,
|
||||
Player targetedPlayer, Map<String, Object> params) {
|
||||
if (sa.hasParam("AiLogic")) {
|
||||
String logic = sa.getParam("AiLogic");
|
||||
if (sa.hasParam("AILogic")) {
|
||||
String logic = sa.getParam("AILogic");
|
||||
|
||||
if ("ToValid".equals(logic)) {
|
||||
// cards like Forgotten Ancient
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.base.Predicates;
|
||||
@@ -15,7 +18,10 @@ import forge.ai.PlayerControllerAi;
|
||||
import forge.ai.SpecialCardAi;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.ai.SpellApiToAi;
|
||||
import forge.game.CardTraitPredicates;
|
||||
import forge.game.Game;
|
||||
import forge.game.ability.AbilityKey;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
@@ -28,11 +34,17 @@ import forge.game.combat.CombatUtil;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerCollection;
|
||||
import forge.game.replacement.ReplacementEffect;
|
||||
import forge.game.replacement.ReplacementLayer;
|
||||
import forge.game.replacement.ReplacementType;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.SpellAbilityStackInstance;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.zone.MagicStack;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.MyRandom;
|
||||
import forge.util.TextUtil;
|
||||
|
||||
public class EffectAi extends SpellAbilityAi {
|
||||
@Override
|
||||
@@ -256,6 +268,59 @@ public class EffectAi extends SpellAbilityAi {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} else if (logic.equals("CantRegenerate")) {
|
||||
if (sa.usesTargeting()) {
|
||||
CardCollection list = CardLists.getTargetableCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), sa);
|
||||
list = CardLists.filter(list, CardPredicates.Presets.CAN_BE_DESTROYED, new Predicate<Card>() {
|
||||
|
||||
@Override
|
||||
public boolean apply(@Nullable Card input) {
|
||||
Map<AbilityKey, Object> runParams = AbilityKey.mapFromAffected(input);
|
||||
runParams.put(AbilityKey.Regeneration, true);
|
||||
List<ReplacementEffect> repDestoryList = game.getReplacementHandler().getReplacementList(ReplacementType.Destroy, runParams, ReplacementLayer.Other);
|
||||
// no Destroy Replacement, or one non-Regeneration one like Totem-Armor
|
||||
if (repDestoryList.isEmpty() || Iterables.any(repDestoryList, Predicates.not(CardTraitPredicates.hasParam("Regeneration")))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cantRegenerateCheckCombat(input) || cantRegenerateCheckStack(input)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (list.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
// TODO check Stack for Effects that would destroy the selected card?
|
||||
sa.getTargets().add(ComputerUtilCard.getBestAI(list));
|
||||
return true;
|
||||
} else if (sa.getParent() != null) {
|
||||
// sub ability should be okay
|
||||
return true;
|
||||
} else if ("Self".equals(sa.getParam("RememberObjects"))) {
|
||||
// the ones affecting itself are Nimbus cards, were opponent can activate this effect
|
||||
Card host = sa.getHostCard();
|
||||
if (!host.canBeDestroyed()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Map<AbilityKey, Object> runParams = AbilityKey.mapFromAffected(sa.getHostCard());
|
||||
runParams.put(AbilityKey.Regeneration, true);
|
||||
List<ReplacementEffect> repDestoryList = game.getReplacementHandler().getReplacementList(ReplacementType.Destroy, runParams, ReplacementLayer.Other);
|
||||
// no Destroy Replacement, or one non-Regeneration one like Totem-Armor
|
||||
if (repDestoryList.isEmpty() || Iterables.any(repDestoryList, Predicates.not(CardTraitPredicates.hasParam("Regeneration")))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cantRegenerateCheckCombat(host) || cantRegenerateCheckStack(host)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else { //no AILogic
|
||||
return false;
|
||||
@@ -319,4 +384,193 @@ public class EffectAi extends SpellAbilityAi {
|
||||
|
||||
return super.doTriggerAINoCost(aiPlayer, sa, mandatory);
|
||||
}
|
||||
|
||||
protected boolean cantRegenerateCheckCombat(Card host) {
|
||||
final Game game = host.getGame();
|
||||
if (!game.getPhaseHandler().inCombat()) {
|
||||
return false;
|
||||
}
|
||||
if (!game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DAMAGE)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Combat combat = game.getCombat();
|
||||
|
||||
if (game.getPhaseHandler().isPlayerTurn(host.getController())) {
|
||||
// attacking player
|
||||
if (!combat.isAttacking(host)) {
|
||||
return false;
|
||||
}
|
||||
// TODO predict lethal combat damage
|
||||
return combat.isBlocked(host);
|
||||
} else {
|
||||
// TODO predict lethal combat damage
|
||||
return combat.isBlocking(host);
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean cantRegenerateCheckStack(Card host) {
|
||||
final Game game = host.getGame();
|
||||
|
||||
// do this only in reaction to a threatening spell on directly on the stack
|
||||
MagicStack stack = game.getStack();
|
||||
if (stack.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
// TODO check Stack for Effects that would destroy host, either direct or indirect
|
||||
SpellAbility stackSa = stack.peekAbility();
|
||||
if (stackSa == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// regenerate is a replace destroy, meaning either destroyed by effect
|
||||
// or destroyed by state based action, when dying by lethal damage
|
||||
SpellAbility subAbility = stackSa;
|
||||
while (subAbility != null) {
|
||||
ApiType apiType = subAbility.getApi();
|
||||
if (apiType == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ApiType.DestroyAll == apiType) {
|
||||
// or skip to sub abilities?
|
||||
if (subAbility.hasParam("NoRegen")) {
|
||||
return false;
|
||||
}
|
||||
if (subAbility.usesTargeting() && !Iterables.contains(subAbility.getTargets().getTargetPlayers(), host.getController())) {
|
||||
return false;
|
||||
}
|
||||
String valid = subAbility.getParamOrDefault("ValidCards", "");
|
||||
|
||||
// Ugh. If calculateAmount needs to be called with DestroyAll it _needs_
|
||||
// to use the X variable
|
||||
// We really need a better solution to this
|
||||
if (valid.contains("X")) {
|
||||
valid = TextUtil.fastReplace(valid,
|
||||
"X", Integer.toString(AbilityUtils.calculateAmount(subAbility.getHostCard(), "X", subAbility)));
|
||||
}
|
||||
|
||||
// host card is valid
|
||||
if (host.isValid(valid.split(","), subAbility.getActivatingPlayer(), subAbility.getHostCard(), subAbility)) {
|
||||
return true;
|
||||
}
|
||||
// failed to check via valid, need to pass through the filterList method
|
||||
CardCollectionView list = game.getCardsIn(ZoneType.Battlefield);
|
||||
|
||||
if (subAbility.usesTargeting()) {
|
||||
list = CardLists.filterControlledBy(list, new PlayerCollection(subAbility.getTargets().getTargetPlayers()));
|
||||
}
|
||||
|
||||
list = AbilityUtils.filterListByType(list, valid, subAbility);
|
||||
if (list.contains(host)) {
|
||||
return true;
|
||||
}
|
||||
// check for defined
|
||||
} else if (ApiType.Destroy == apiType) {
|
||||
if (subAbility.hasParam("NoRegen")) {
|
||||
return false;
|
||||
}
|
||||
if (subAbility.hasParam("Sacrifice")) {
|
||||
return false;
|
||||
}
|
||||
// simulate getTargetCards
|
||||
if (subAbility.usesTargeting()) {
|
||||
// isTargeting checks parents, i think that might be wrong
|
||||
if (subAbility.getTargets().contains(host)) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
if (AbilityUtils.getDefinedObjects(subAbility.getHostCard(), subAbility.getParam("Defined"), subAbility).contains(host)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (CardUtil.getRadiance(subAbility).contains(host)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// check for target or indirect target
|
||||
} else if (ApiType.DamageAll == apiType) {
|
||||
if (!subAbility.hasParam("ValidCards")) {
|
||||
continue;
|
||||
}
|
||||
String valid = subAbility.getParamOrDefault("ValidCards", "");
|
||||
if (valid.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Card source = game.getChangeZoneLKIInfo(subAbility.getHostCard());
|
||||
if (source.isWitherDamage()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// host card is valid
|
||||
if (host.isValid(valid.split(","), subAbility.getActivatingPlayer(), subAbility.getHostCard(), subAbility)) {
|
||||
// TODO check if damage would be lethal
|
||||
return true;
|
||||
}
|
||||
// failed to check via valid, need to pass through the filterList method
|
||||
CardCollectionView list = game.getCardsIn(ZoneType.Battlefield);
|
||||
if (subAbility.usesTargeting()) {
|
||||
list = CardLists.filterControlledBy(list, new PlayerCollection(subAbility.getTargets().getTargetPlayers()));
|
||||
}
|
||||
|
||||
list = AbilityUtils.filterListByType(list, valid, subAbility);
|
||||
if (list.contains(host)) {
|
||||
// TODO check if damage would be lethal
|
||||
return true;
|
||||
}
|
||||
} else if (ApiType.DealDamage == apiType) {
|
||||
// skip choices
|
||||
if (subAbility.hasParam("CardChoices") || subAbility.hasParam("PlayerChoices")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final List<Card> definedSources = AbilityUtils.getDefinedCards(subAbility.getHostCard(), subAbility.getParam("DamageSource"), subAbility);
|
||||
if (definedSources == null || definedSources.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
boolean targeting = false;
|
||||
// simulate getTargetCards
|
||||
if (subAbility.usesTargeting()) {
|
||||
// isTargeting checks parents, i think that might be wrong
|
||||
if (subAbility.getTargets().contains(host)) {
|
||||
targeting = true;
|
||||
}
|
||||
} else {
|
||||
if (AbilityUtils.getDefinedObjects(subAbility.getHostCard(), subAbility.getParam("Defined"), subAbility).contains(host)) {
|
||||
targeting = true;
|
||||
}
|
||||
}
|
||||
|
||||
for (Card source : definedSources) {
|
||||
final Card sourceLKI = game.getChangeZoneLKIInfo(source);
|
||||
|
||||
if (sourceLKI.isWitherDamage()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (subAbility.hasParam("RelativeTarget")) {
|
||||
targeting = false;
|
||||
if (AbilityUtils.getDefinedEntities(subAbility.getHostCard(), subAbility.getParam("Defined"), subAbility).contains(host)) {
|
||||
targeting = true;
|
||||
}
|
||||
}
|
||||
// TODO predict damage
|
||||
if (targeting) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (CardUtil.getRadiance(subAbility).contains(host)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
subAbility = subAbility.getSubAbility();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,12 +166,6 @@ public abstract class PumpAiBase extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
return ph.isPlayerTurn(ai) || (combat != null && combat.isAttacking(card) && card.getNetCombatDamage() > 0);
|
||||
} else if (keyword.endsWith("CARDNAME can't be regenerated.")) {
|
||||
if (card.getShieldCount() > 0) {
|
||||
return true;
|
||||
}
|
||||
return card.hasKeyword("If CARDNAME would be destroyed, regenerate it.") && combat != null
|
||||
&& (combat.isBlocked(card) || combat.isBlocking(card));
|
||||
} else return !keyword.endsWith("CARDNAME's activated abilities can't be activated."); //too complex
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user