From eac062a1c2e1f4954dbc35c6c85d0885e4cb796a Mon Sep 17 00:00:00 2001 From: Northmoc Date: Mon, 20 Nov 2023 22:45:03 -0500 Subject: [PATCH] eye_of_ojer_taq_apex_observatory.txt + support --- .../src/main/java/forge/game/GameAction.java | 8 +------ .../ability/effects/ChooseTypeEffect.java | 19 +++++++++++++++ .../src/main/java/forge/game/card/Card.java | 14 +++++++++++ .../java/forge/game/card/CardFactoryUtil.java | 4 ++-- .../main/java/forge/game/cost/CostExile.java | 23 +++++++++++++++++-- .../eye_of_ojer_taq_apex_observatory.txt | 23 +++++++++++++++++++ .../upcoming/fabrication_foundry.txt | 1 + .../java/forge/player/HumanCostDecision.java | 18 ++++++++++++--- 8 files changed, 96 insertions(+), 14 deletions(-) create mode 100644 forge-gui/res/cardsfolder/upcoming/eye_of_ojer_taq_apex_observatory.txt diff --git a/forge-game/src/main/java/forge/game/GameAction.java b/forge-game/src/main/java/forge/game/GameAction.java index b1e536acbcb..510ba1ece5c 100644 --- a/forge-game/src/main/java/forge/game/GameAction.java +++ b/forge-game/src/main/java/forge/game/GameAction.java @@ -506,13 +506,7 @@ public class GameAction { } if (cause != null && cause.isCraft() && toBattlefield) { // retain cards crafted while ETB transformed - for (Card craft : cause.getPaidList("ExiledCards")) { - if (!craft.equals(copied) && !craft.isToken()) { - copied.addExiledCard(craft); - craft.setExiledWith(copied); - craft.setExiledBy(cause.getActivatingPlayer()); - } - } + copied.retainPaidList(cause, "ExiledCards"); } } diff --git a/forge-game/src/main/java/forge/game/ability/effects/ChooseTypeEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ChooseTypeEffect.java index 30368cb769f..7d5a2dbaeed 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/ChooseTypeEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/ChooseTypeEffect.java @@ -9,6 +9,7 @@ import forge.card.CardType; import forge.game.ability.AbilityUtils; import forge.game.ability.SpellAbilityEffect; import forge.game.card.Card; +import forge.game.card.CardCollection; import forge.game.card.CardCollectionView; import forge.game.card.CardFactoryUtil; import forge.game.player.Player; @@ -88,6 +89,24 @@ public class ChooseTypeEffect extends SpellAbilityEffect { } } } + break; + case "Shared": + if (sa.hasParam("TypesFromDefined")) { + CardCollection def = AbilityUtils.getDefinedCards(card, sa.getParam("TypesFromDefined"), sa); + if (def.size() < 2) break; // need at least 2 cards to work with to find shared types + final Card card1 = def.get(0); + def.remove(0); + for (final CardType.CoreType ct : card1.getType().getCoreTypes()) { + boolean shared = true; + for (final Card c : def) { + if (!c.getType().hasType(ct)) { + shared = false; + break; + } + } + if (shared) validTypes.add(ct.name()); + } + } } } diff --git a/forge-game/src/main/java/forge/game/card/Card.java b/forge-game/src/main/java/forge/game/card/Card.java index 28a4da774e4..db10e3c9192 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -622,6 +622,9 @@ public class Card extends GameEntity implements Comparable, IHasSVars { boolean result = c.changeToState(c.backside ? CardStateName.Transformed : CardStateName.Original); retResult = retResult || result; + if (cause != null && cause.isCraft()) { // retain cards crafted while transforming in exile + c.retainPaidList(cause, "ExiledCards"); + } } if (hasMergedCard()) { rebuildMutatedStates(cause); @@ -1283,6 +1286,17 @@ public class Card extends GameEntity implements Comparable, IHasSVars { } } + public final void retainPaidList(final SpellAbility cause, final String list) { + for (Card craft : cause.getPaidList(list)) { + if (!craft.equals(this) && !craft.isToken()) { + addExiledCard(craft); + craft.setExiledWith(this); + craft.setExiledBy(cause.getActivatingPlayer()); + } + } + + } + public final List getStoredRolls() { return storedRolls; } diff --git a/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java b/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java index 0e12ee2faab..77d7314f653 100644 --- a/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java +++ b/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java @@ -2942,8 +2942,8 @@ public class CardFactoryUtil { // Create return transformed ability string String ab = "AB$ ChangeZone | CostDesc$ " + cd.toString() + " | Cost$ " + cost + " | Origin$ Exile | " + "Destination$ Battlefield | Transformed$ True | Defined$ CorrectedSelf | Craft$ True | " + - "XAnnounceTitle$ " + Localizer.getInstance().getMessage("lblCraft") + " | " + - "StackDescription$ Return this card transformed under its owner's control. (Craft) | " + + "XAnnounceTitle$ " + Localizer.getInstance().getMessage("lblCraft") + " | SorcerySpeed$ True" + + " | StackDescription$ Return this card transformed under its owner's control. (Craft) | " + "SpellDescription$ (" + inst.getReminderText() + ")"; final SpellAbility newSA = AbilityFactory.getAbility(ab, card); newSA.setIntrinsic(intrinsic); diff --git a/forge-game/src/main/java/forge/game/cost/CostExile.java b/forge-game/src/main/java/forge/game/cost/CostExile.java index ded622db7cb..8c67d8abe57 100644 --- a/forge-game/src/main/java/forge/game/cost/CostExile.java +++ b/forge-game/src/main/java/forge/game/cost/CostExile.java @@ -195,10 +195,31 @@ public class CostExile extends CostPartWithList { type = TextUtil.fastReplace(type, TextUtil.concatNoSpace("+withTotalCMCEQ", totalM), ""); } + boolean sharedType = false; + if (type.contains("+withSharedCardType")) { + sharedType = true; + type = TextUtil.fastReplace(type, "+withSharedCardType", ""); + } + if (!type.contains("X") || ability.getXManaCostPaid() != null) { list = CardLists.getValidCards(list, type.split(";"), payer, source, ability); } + int amount = this.getAbilityAmount(ability); + + if (sharedType) { // will need more logic if cost ever wants more than 2 that share a type + if (list.size() < amount) return false; + for (int i = 0; i < list.size(); i++) { + final Card card1 = list.get(i); + for (final Card compare : list) { + if (!compare.equals(card1) && compare.sharesCardTypeWith(card1)) { + return true; + } + } + } + return false; + } + if (totalCMC) { int needed = Integer.parseInt(this.getAmount().split("\\+")[0]); if (list.size() < needed) return false; @@ -209,8 +230,6 @@ public class CostExile extends CostPartWithList { return CardLists.cmcCanSumTo(i, list); } - int amount = this.getAbilityAmount(ability); - // for cards like Allosaurus Rider, do not count it if (this.from.size() == 1 && this.from.get(0).equals(ZoneType.Hand) && source.isInZone(ZoneType.Hand) && list.contains(source)) { diff --git a/forge-gui/res/cardsfolder/upcoming/eye_of_ojer_taq_apex_observatory.txt b/forge-gui/res/cardsfolder/upcoming/eye_of_ojer_taq_apex_observatory.txt new file mode 100644 index 00000000000..dc4b81c0ca6 --- /dev/null +++ b/forge-gui/res/cardsfolder/upcoming/eye_of_ojer_taq_apex_observatory.txt @@ -0,0 +1,23 @@ +Name:Eye of Ojer Taq +ManaCost:3 +Types:Artifact +A:AB$ Mana | Cost$ T | Produced$ Any | SpellDescription$ Add one mana of any color. +K:Craft:6 ExileCtrlOrGrave<2/Permanent.Other+withSharedCardType/permanent>:two that share a card type:the two +AlternateMode:DoubleFaced +DeckHints:Ability$Discard|Mill|Sacrifice +DeckHas:Ability$Mill|Graveyard|Token +Oracle:{T}: Add one mana of any color.\nCraft with two that share a card type {6} ({6}, Exile this artifact, Exile the two from among other permanents you control and/or cards from your graveyard: Return this card transformed under its owner's control. Craft only as a sorcery.) + +ALTERNATE + +Name:Apex Observatory +ManaCost:no cost +Types:Artifact +K:ETBReplacement:Other:Tap +SVar:Tap:DB$ Tap | Defined$ Self | ETB$ True | SubAbility$ DBChooseType | SpellDescription$ CARDNAME enters the battlefield tapped. As it enters, choose a card type shared among two exiled cards used to craft it. +SVar:DBChooseType:DB$ ChooseType | Type$ Shared | TypesFromDefined$ ExiledWith | AILogic$ MostProminentComputerControlsOrOwns +A:AB$ Effect | Cost$ T | StaticAbilities$ Play | Triggers$ CastTrig | SpellDescription$ The next spell you cast this turn of the chosen type can be cast without paying its mana cost. +SVar:Play:Mode$ Continuous | MayPlay$ True | MayPlayWithoutManaCost$ True | MayPlayDontGrantZonePermissions$ True | EffectZone$ Command | Affected$ Card.ChosenType | AffectedZone$ Hand,Graveyard,Library,Exile,Command | Description$ The next spell you cast this turn of the chosen type can be cast without paying its mana cost. +SVar:CastTrig:Mode$ SpellCast | ValidCard$ Card.ChosenType | ValidActivatingPlayer$ You | Execute$ ExileSelf | Static$ True +SVar:ExileSelf:DB$ ChangeZone | Defined$ Self | Origin$ Command | Destination$ Exile +Oracle:Apex Observatory enters the battlefield tapped. As it enters, choose a card type shared among two exiled cards used to craft it.\n{T}: The next spell you cast this turn of the chosen type can be cast without paying its mana cost. diff --git a/forge-gui/res/cardsfolder/upcoming/fabrication_foundry.txt b/forge-gui/res/cardsfolder/upcoming/fabrication_foundry.txt index b2cdabfdb0e..d67106b223c 100644 --- a/forge-gui/res/cardsfolder/upcoming/fabrication_foundry.txt +++ b/forge-gui/res/cardsfolder/upcoming/fabrication_foundry.txt @@ -5,4 +5,5 @@ A:AB$ Mana | Cost$ T | Produced$ W | RestrictValid$ Spell.Artifact,Activated.Art A:AB$ ChangeZone | Cost$ 2 W T Exile<1+/Artifact.Other+withTotalCMCEQX/other artifacts you control with total mana value X> | Announce$ X | Origin$ Graveyard | Destination$ Battlefield | ValidTgts$ Artifact.YouOwn+cmcLEX | TgtPrompt$ Select target artifact card with mana value X or less | SorcerySpeed$ True | SpellDescription$ Return target artifact card with mana value X or less from your graveyard to the battlefield. Activate only as a sorcery. SVar:X:Count$xPaid DeckNeeds:Type$Artifact +AI:RemoveDeck:All Oracle:{T}: Add {W}. Spend this mana only to cast an artifact spell or activate an ability of an artifact source.\n{2}{W}, {T}, Exile one or more other artifacts you control with total mana value X: Return target artifact card with mana value X or less from your graveyard to the battlefield. Activate only as a sorcery. diff --git a/forge-gui/src/main/java/forge/player/HumanCostDecision.java b/forge-gui/src/main/java/forge/player/HumanCostDecision.java index 2f9dd4ad1e6..78933606020 100644 --- a/forge-gui/src/main/java/forge/player/HumanCostDecision.java +++ b/forge-gui/src/main/java/forge/player/HumanCostDecision.java @@ -241,6 +241,11 @@ public class HumanCostDecision extends CostDecisionMakerBase { totalM = type.split("withTotalCMCEQ")[1]; type = TextUtil.fastReplace(type, TextUtil.concatNoSpace("+withTotalCMCEQ", totalM), ""); } + boolean sharedType = false; + if (type.contains("+withSharedCardType")) { + sharedType = true; + type = TextUtil.fastReplace(type, "+withSharedCardType", ""); + } CardCollection list; if (cost.zoneRestriction != 1) { @@ -292,7 +297,7 @@ public class HumanCostDecision extends CostDecisionMakerBase { if (fromZone == ZoneType.Library) { return exileFromTop(cost, c); } } if (fromTopGrave) { return exileFromTopGraveType(c, list); } - if (cost.zoneRestriction != 0) { return exileFromMiscZone(cost, c, list); } + if (cost.zoneRestriction != 0) { return exileFromMiscZone(cost, c, list, sharedType); } final FCollectionView players = game.getPlayers(); final List payableZone = new ArrayList<>(); @@ -394,7 +399,8 @@ public class HumanCostDecision extends CostDecisionMakerBase { return PaymentDecision.card(list); } - private PaymentDecision exileFromMiscZone(final CostExile cost, final int nNeeded, final CardCollection typeList) { + private PaymentDecision exileFromMiscZone(final CostExile cost, final int nNeeded, final CardCollection typeList, + final boolean sharedType) { // when it's always a single triggered card getting exiled don't act like it might be different by offering the zone for choice if (cost.zoneRestriction == -1 && ability.isTrigger() && nNeeded == 1 && typeList.size() == 1) { if (confirmAction(cost, Localizer.getInstance().getMessage("lblExileConfirm", CardTranslation.getTranslatedName(typeList.getFirst().getName())))) { @@ -405,9 +411,15 @@ public class HumanCostDecision extends CostDecisionMakerBase { final List origin = Lists.newArrayList(cost.from); final CardCollection exiled = new CardCollection(); + final String required = sharedType ? " (must share a card type)" : ""; + final List chosen = controller.chooseCardsForZoneChange(ZoneType.Exile, origin, ability, typeList, - mandatory ? nNeeded : 0, nNeeded, null, cost.toString(nNeeded), null); + mandatory ? nNeeded : 0, nNeeded, null, cost.toString(nNeeded) + required, + null); + if (sharedType) { + if (!chosen.get(1).sharesCardTypeWith(chosen.get(0))) return null; + } exiled.addAll(chosen); if (exiled.size() < nNeeded) {