package forge.card.abilityFactory; import java.util.ArrayList; import java.util.Map; import java.util.Iterator; import java.util.Random; import forge.AllZone; import forge.AllZoneUtil; import forge.Card; import forge.CardList; import forge.CardListFilter; import forge.CardUtil; import forge.CombatUtil; import forge.Command; import forge.ComputerUtil; import forge.Constant; import forge.Counters; import forge.MyRandom; import forge.Player; import forge.card.cardFactory.CardFactoryUtil; import forge.card.spellability.Ability_Sub; import forge.card.spellability.Cost; import forge.card.spellability.Spell; import forge.card.spellability.SpellAbility; import forge.card.spellability.Spell_Permanent; import forge.card.spellability.Target; import forge.card.staticAbility.StaticAbility; public class AbilityFactory_Attach { public static SpellAbility createSpellAttach(final AbilityFactory AF){ // There are two types of Spell Attachments: Auras and Instants/Sorceries // Auras generally target what that card will attach to // I/S generally target the Attacher and the Attachee SpellAbility spAttach = null; if (AF.getHostCard().isAura()){ // The 4th parameter is to resolve an issue with SetDescription in default Spell_Permanent constructor spAttach = new Spell_Permanent(AF.getHostCard(), AF.getAbCost(), AF.getAbTgt(), false){ private static final long serialVersionUID = 6631124959690157874L; final AbilityFactory af = AF; @Override public String getStackDescription(){ // when getStackDesc is called, just build exactly what is happening return attachStackDescription(af, this); } public boolean canPlayAI(){ return attachCanPlayAI(af, this); } @Override public void resolve() { // The Spell_Permanent (Auras) version of this AF needs to move the card into play before Attaching Card c = AllZone.getGameAction().moveToPlay(getSourceCard()); this.setSourceCard(c); attachResolve(af, this); } }; } else{ // This is here to be complete, however there's only a few cards that use it // And the Targeting system can't really handle them at this time (11/7/1) spAttach = new Spell(AF.getHostCard(), AF.getAbCost(), AF.getAbTgt()){ private static final long serialVersionUID = 6631124959690157874L; final AbilityFactory af = AF; @Override public String getStackDescription(){ // when getStackDesc is called, just build exactly what is happening return attachStackDescription(af, this); } public boolean canPlayAI(){ return attachCanPlayAI(af, this); } @Override public void resolve() { attachResolve(af, this); } }; } return spAttach; } // Attach Ability public static SpellAbility createAbilityAttach(final AbilityFactory AF){ // placeholder for Equip and other similar cards return null; } // Attach Drawback public static SpellAbility createDrawbackAttach(final AbilityFactory AF){ // placeholder for DBs that might attach return null; } public static String attachStackDescription(AbilityFactory af, SpellAbility sa){ StringBuilder sb = new StringBuilder(); if (!(sa instanceof Ability_Sub)) sb.append(sa.getSourceCard().getName()).append(" - "); else sb.append(" "); String conditionDesc = af.getMapParams().get("ConditionDescription"); if (conditionDesc != null) sb.append(conditionDesc).append(" "); sb.append(" Attach to "); ArrayList<Object> targets; // Should never allow more than one Attachment per card Target tgt = af.getAbTgt(); if (tgt != null) targets = tgt.getTargets(); else targets = AbilityFactory.getDefinedObjects(sa.getSourceCard(), af.getMapParams().get("Defined"), sa); for(Object o : targets) sb.append(o).append(" "); Ability_Sub abSub = sa.getSubAbility(); if (abSub != null) sb.append(abSub.getStackDescription()); return sb.toString(); } public static boolean attachPreference(AbilityFactory af, SpellAbility sa, Map<String,String> params, Target tgt, boolean mandatory){ Object o; if (tgt.canTgtPlayer()) o = attachToPlayerAIPreferences(af, sa, mandatory); else o = attachToCardAIPreferences(af, sa, params, mandatory); if (o == null) return false; tgt.addTarget(o); return true; } public static Card attachToCardAIPreferences(AbilityFactory af, final SpellAbility sa, Map<String,String> params, boolean mandatory){ Target tgt = sa.getTarget(); Card attachSource = sa.getSourceCard(); // TODO AttachSource is currently set for the Source of the Spell, but at some point can support attaching a different card CardList list = AllZoneUtil.getCardsInZone(tgt.getZone()); list = list.getValidCards(tgt.getValidTgts(), sa.getActivatingPlayer(), sa.getSourceCard()); // TODO: If Attaching without casting, don't need to actually target. // I believe this is the only case where mandatory will be true, so just check that when starting that work // But we shouldn't attach to things with Protection if (tgt.getZone().equals("Battlefield")) list = list.getTargetableCards(sa.getSourceCard()); if (list.size() == 0) return null; Card c = attachGeneralAI(sa, list, mandatory, attachSource, params.get("AILogic")); if (c == null && mandatory){ list.shuffle(); c = list.get(0); } return c; } public static Card attachGeneralAI(final SpellAbility sa, CardList list, boolean mandatory, Card attachSource, String logic){ Player prefPlayer = "Pump".equals(logic) ? AllZone.getComputerPlayer() : AllZone.getHumanPlayer(); // Some ChangeType cards are beneficial, and PrefPlayer should be changed to represent that CardList prefList = list.getController(prefPlayer); // If there are no preferred cards, and not mandatory bail out if (prefList.size() == 0) return chooseUnpreferred(mandatory, list); // Preferred list has at least one card in it to make to the actual Logic Card c = null; if ("GainControl".equals(logic)) c = attachAIControlPreference(sa, prefList, mandatory, attachSource); else if ("Curse".equals(logic)) c = attachAICursePreference(sa, prefList, mandatory, attachSource); else if ("Pump".equals(logic)) c = attachAIPumpPreference(sa, prefList, mandatory, attachSource); else if ("ChangeType".equals(logic)) // Evil Presence, Spreading Seas c = attachAIChangeTypePreference(sa, prefList, mandatory, attachSource); // TODO: Does KeepTapped need it's own list? Probably more efficient than just Curse return c; } public static Card chooseUnpreferred(boolean mandatory, CardList list){ if (!mandatory) return null; return CardFactoryUtil.AI_getWorstPermanent(list, true, true, true, false); } public static Card chooseLessPreferred(boolean mandatory, CardList list){ if (!mandatory) return null; return CardFactoryUtil.AI_getBest(list); } public static Card acceptableChoice(Card c, boolean mandatory){ if (mandatory) return c; // TODO: If Not Mandatory, make sure the card is "good enough" if (c.isCreature()){ int eval = CardFactoryUtil.evaluateCreature(c); if (eval < 160 && (eval < 130 || AllZone.getComputerPlayer().getLife() > 5)) return null; } return c; } // Should generalize this code a bit since they all have similar structures public static Card attachAIControlPreference(final SpellAbility sa, CardList list, boolean mandatory, Card attachSource){ // AI For choosing a Card to Gain Control of. if (sa.getTarget().canTgtPermanent()){ // If can target all Permanents, and Life isn't in eminent danger, grab Planeswalker first, then Creature // if Life < 5 grab Creature first, then Planeswalker. Lands, Enchantments and Artifacts are probably "not good enough" } Card c = CardFactoryUtil.AI_getBest(list); // If Mandatory (brought directly into play without casting) gotta choose something if (c == null) return chooseLessPreferred(mandatory, list); return acceptableChoice(c, mandatory); } public static Card attachAIPumpPreference(final SpellAbility sa, CardList list, boolean mandatory, Card attachSource){ // AI For choosing a Card to Pump Card c = null; CardList magnetList = null; String stCheck = null; if (attachSource.isAura()){ stCheck = "EnchantedBy"; magnetList = list.getEnchantMagnets(); } else if (attachSource.isEquipment()){ stCheck = "EquippedBy"; magnetList = list.getEquipMagnets(); } if (magnetList != null && !magnetList.isEmpty()){ // Always choose something from the Magnet List. // Probably want to "weight" the list by amount of Enchantments and choose the "lightest" magnetList = magnetList.filter(new CardListFilter() { @Override public boolean addCard(Card c) { return CombatUtil.canAttack(c); } }); return CardFactoryUtil.AI_getBest(magnetList); } int totToughness = 0; int totPower = 0; ArrayList<String> keywords = new ArrayList<String>(); boolean grantingAbilities = false; for (StaticAbility stAbility : attachSource.getStaticAbilities()){ Map<String,String> params = stAbility.getMapParams(); if (!params.get("Mode").equals("Continuous")) continue; String affected = params.get("Affected"); if (affected == null) continue; if ((affected.contains(stCheck) || affected.contains("AttachedBy")) ){ totToughness += CardFactoryUtil.parseSVar(attachSource, params.get("AddToughness")); totPower += CardFactoryUtil.parseSVar(attachSource, params.get("AddPower")); grantingAbilities |= params.containsKey("AddAbility"); String kws = params.get("AddKeyword"); if (kws != null){ for(String kw : kws.split(" & ")) keywords.add(kw); } } } CardList prefList = new CardList(list); if (totToughness < 0){ // Don't kill my own stuff with Negative toughness Auras final int tgh = totToughness; prefList = prefList.filter(new CardListFilter() { @Override public boolean addCard(Card c) { return c.getLethalDamage() > Math.abs(tgh); } }); } else if (totToughness == 0 && totPower == 0){ // Just granting Keywords don't assign stacking Keywords Iterator<String> it = keywords.iterator(); while(it.hasNext()){ String key = it.next(); if (CardUtil.isStackingKeyword(key)) it.remove(); } if (!keywords.isEmpty()){ final ArrayList<String> finalKWs = keywords; prefList = prefList.filter(new CardListFilter() { //If Aura grants only Keywords, don't Stack unstackable keywords @Override public boolean addCard(Card c) { for(String kw : finalKWs) if (c.hasKeyword(kw)) return false; return true; } }); } } if (attachSource.isAura()){ // TODO: For Auras like Rancor, that aren't as likely to lead to card disadvantage, this check should be skipped prefList = prefList.filter(new CardListFilter() { @Override public boolean addCard(Card c) { return !c.isEnchanted(); } }); } if (!grantingAbilities){ // Probably prefer to Enchant Creatures that Can Attack // Filter out creatures that can't Attack or have Defender prefList = prefList.filter(new CardListFilter() { @Override public boolean addCard(Card c) { return !c.isCreature() || CombatUtil.canAttack(c); } }); c = CardFactoryUtil.AI_getBest(prefList); } else // If we grant abilities, we may want to put it on something Weak? Possibly more defensive? c = CardFactoryUtil.AI_getWorstPermanent(prefList, false, false, false, false); if (c == null) return chooseLessPreferred(mandatory, list); return acceptableChoice(c, mandatory); } public static Card attachAICursePreference(final SpellAbility sa, CardList list, boolean mandatory, Card attachSource){ // AI For choosing a Card to Curse of. // TODO Figure out some way to combine The "gathering of data" from statics used in both Pump and Curse String stCheck = null; if (attachSource.isAura()){ stCheck = "EnchantedBy"; } else if (attachSource.isEquipment()){ stCheck = "EquippedBy"; } int totToughness = 0; int totPower = 0; ArrayList<String> keywords = new ArrayList<String>(); boolean grantingAbilities = false; for (StaticAbility stAbility : attachSource.getStaticAbilities()){ Map<String,String> params = stAbility.getMapParams(); if (!params.get("Mode").equals("Continuous")) continue; String affected = params.get("Affected"); if (affected == null) continue; if ((affected.contains(stCheck) || affected.contains("AttachedBy")) ){ totToughness += CardFactoryUtil.parseSVar(attachSource, params.get("AddToughness")); totPower += CardFactoryUtil.parseSVar(attachSource, params.get("AddPower")); grantingAbilities |= params.containsKey("AddAbility"); String kws = params.get("AddKeyword"); if (kws != null){ for(String kw : kws.split(" & ")) keywords.add(kw); } } } CardList prefList = null; if (totToughness < 0){ // Kill a creature if we can final int tgh = totToughness; prefList = list.filter(new CardListFilter() { @Override public boolean addCard(Card c) { if (c.hasKeyword("Indestructible") && c.getNetDefense() <= Math.abs(tgh)) return true; return c.getLethalDamage() <= Math.abs(tgh); } }); } Card c = null; if (prefList == null || prefList.size() == 0) prefList = new CardList(list); else if (prefList.size() > 0){ c = CardFactoryUtil.AI_getBest(prefList); if (c != null) return c; } if (!keywords.isEmpty()){ // Don't give Can't Attack or Defender to cards that can't do these things to begin with if (keywords.contains("CARDNAME can't attack") || keywords.contains("Defender") || keywords.contains("CARDNAME attacks each turn if able.")){ prefList = prefList.filter(new CardListFilter() { @Override public boolean addCard(Card c) { return !(c.hasKeyword("CARDNAME can't attack") || c.hasKeyword("Defender")); } }); } } c = CardFactoryUtil.AI_getBest(prefList); if (c == null) return chooseLessPreferred(mandatory, list); return acceptableChoice(c, mandatory); } public static Card attachAIChangeTypePreference(final SpellAbility sa, CardList list, boolean mandatory, Card attachSource){ // AI For Cards like Evil Presence or Spreading Seas // A few of these cards are actually good, most of the Animate to Creature ones // One or two of the give basic land types // Maybe require Curse$ on the specific ones and filter the list that way Card c = CardFactoryUtil.AI_getBest(list); // TODO: Port over some of the existing code, but rewrite most of it. // Filter out Basic Lands that have the same type as the changing type // Ultimately, these spells need to be used to reduce mana base of a color. So it might be better to choose a Basic over a Nonbasic if (c == null) return chooseLessPreferred(mandatory, list); return acceptableChoice(c, mandatory); } // Todo: Does RemainTapped need its own SubAttach AF? public static Player attachToPlayerAIPreferences(AbilityFactory af, final SpellAbility sa, boolean mandatory){ Target tgt = sa.getTarget(); Player p; if (tgt.canOnlyTgtOpponent()){ // If can Only Target Opponent, do so. p = AllZone.getHumanPlayer(); if (p.canTarget(sa.getSourceCard())) return p; else return null; } if (af.isCurse()) p = AllZone.getHumanPlayer(); else p = AllZone.getComputerPlayer(); if (p.canTarget(sa.getSourceCard())) return p; if (!mandatory) return null; p = p.getOpponent(); if (p.canTarget(sa.getSourceCard())) return p; return null; } public static boolean attachCanPlayAI(final AbilityFactory af, final SpellAbility sa){ Random r = MyRandom.random; Map<String,String> params = af.getMapParams(); Cost abCost = sa.getPayCosts(); final Card source = sa.getSourceCard(); if (abCost != null){ // No Aura spells have Additional Costs // AI currently disabled for these costs if (abCost.getSacCost()){ } if (abCost.getLifeCost()) return false; if (abCost.getDiscardCost()) return false; if (abCost.getSubCounter()){ //non +1/+1 counters should be used if (abCost.getCounterType().equals(Counters.P1P1)){ // A card has a 25% chance per counter to be able to pass through here // 4+ counters will always pass. 0 counters will never int currentNum = source.getCounters(abCost.getCounterType()); double percent = .25 * (currentNum / abCost.getCounterNum()); if (percent <= r.nextFloat()) return false; } } } // prevent run-away activations - first time will always return true boolean chance = r.nextFloat() <= .6667; // Attach spells always have a target Target tgt = sa.getTarget(); if (tgt != null){ tgt.resetTargets(); if (!attachPreference(af, sa, params, tgt, false)) return false; } if (abCost.getMana().contains("X") && source.getSVar("X").equals("Count$xPaid")){ // Set PayX here to maximum value. (Endless Scream and Venarian Gold) int xPay = ComputerUtil.determineLeftoverMana(sa); if (xPay == 0) return false; source.setSVar("PayX", Integer.toString(xPay)); } if (AbilityFactory.isSorcerySpeed(sa)){ if (AllZone.getPhase().is(Constant.Phase.Main1)) chance = r.nextFloat() <= .75; else // Don't Attach Sorcery Speed stuff after Main1 return false; } else chance &= r.nextFloat() <= .75; return chance; } public static boolean attachDoTriggerAI(AbilityFactory af, SpellAbility sa, boolean mandatory){ if (!ComputerUtil.canPayCost(sa)) // If there is a cost payment it's usually not mandatory return false; // Check if there are any valid targets // Now are Valid Targets better than my targets? // check SubAbilities DoTrigger? Ability_Sub abSub = sa.getSubAbility(); if (abSub != null) return abSub.doTrigger(mandatory); return true; } public static void attachResolve(final AbilityFactory af, final SpellAbility sa){ Map<String,String> params = af.getMapParams(); Card card = sa.getSourceCard(); ArrayList<Object> targets; Target tgt = af.getAbTgt(); if (tgt != null){ targets = tgt.getTargets(); // TODO Remove invalid targets (although more likely this will just fizzle earlier) } else targets = AbilityFactory.getDefinedObjects(sa.getSourceCard(), params.get("Defined"), sa); // If Cast Targets will be checked on the Stack for(Object o : targets){ handleAttachment(card, o, af); } } public static void handleAttachment(Card card, Object o, AbilityFactory af){ if (o instanceof Card){ Card c = (Card)o; if (card.isAura()){ // Most Auras can enchant permanents, a few can Enchant cards in graveyards // Spellweaver Volute, Dance of the Dead, Animate Dead // Although honestly, I'm not sure if the three of those could handle being scripted boolean gainControl = "GainControl".equals(af.getMapParams().get("AILogic")); handleAura(card, c, gainControl); } else if (card.isEquipment()) card.equipCard(c); //else if (card.isFortification()) // card.fortifyCard(c); } else if (o instanceof Player){ // Currently, a few cards can enchant players // Psychic Possession, Paradox Haze, Wheel of Sun and Moon // Player p = (Player)o; //if (card.isAura()) // card.enchantPlayer(p); } } public static void handleAura(final Card card, final Card tgt, boolean gainControl){ if (card.isEnchanting()){ // If this Card is already Enchanting something // Need to unenchant it, then clear out the commands Card oldEnchanted = card.getEnchantingCard(); card.removeEnchanting(oldEnchanted); card.clearEnchantCommand(); card.clearUnEnchantCommand(); card.clearTriggers(); // not sure if cleartriggers is needed? } if (gainControl){ // Handle GainControl Auras final Player[] pl = new Player[1]; pl[0] = tgt.getController(); Command onEnchant = new Command() { private static final long serialVersionUID = -2519887209491512000L; public void execute() { if(card.isEnchanting()) { Card crd = card.getEnchanting().get(0); pl[0] = crd.getController(); AllZone.getGameAction().changeController(new CardList(crd), pl[0], card.getController()); } }//execute() };//Command Command onUnEnchant = new Command() { private static final long serialVersionUID = 3426441132121179288L; public void execute() { if(card.isEnchanting()) { Card crd = card.getEnchanting().get(0); if(AllZoneUtil.isCardInPlay(crd)) { AllZone.getGameAction().changeController(new CardList(crd), crd.getController(), pl[0]); } } }//execute() };//Command Command onChangesControl = new Command() { /** automatically generated serialVersionUID. */ private static final long serialVersionUID = -65903786170234039L; public void execute() { if(card.isEnchanting()) { Card crd = card.getEnchanting().get(0); AllZone.getGameAction().changeController(new CardList(crd), crd.getController(),card.getController()); } }//execute() };//Command Command onLeavesPlay = new Command() { private static final long serialVersionUID = -639204333673364477L; public void execute() { if(card.isEnchanting()) { Card crd = card.getEnchanting().get(0); card.unEnchantCard(crd); } } };//Command // Add Enchant Commands card.addEnchantCommand(onEnchant); card.addUnEnchantCommand(onUnEnchant); card.addLeavesPlayCommand(onLeavesPlay); card.addChangeControllerCommand(onChangesControl); } card.enchantCard(tgt); } }