diff --git a/forge-game/src/main/java/forge/game/ability/AbilityUtils.java b/forge-game/src/main/java/forge/game/ability/AbilityUtils.java index 88e5ff549da..c1578443353 100644 --- a/forge-game/src/main/java/forge/game/ability/AbilityUtils.java +++ b/forge-game/src/main/java/forge/game/ability/AbilityUtils.java @@ -12,16 +12,12 @@ import java.util.Map.Entry; import java.util.regex.Matcher; import java.util.regex.Pattern; +import com.google.common.collect.*; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; import com.google.common.base.Predicate; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Iterables; -import com.google.common.collect.Lists; -import com.google.common.collect.Maps; -import com.google.common.collect.Sets; import forge.card.CardStateName; import forge.card.CardType; @@ -2046,6 +2042,10 @@ public class AbilityUtils { return doXMath(ce == null ? 0 : getNumberOfTypes(ce), expr, c, ctb); } + if (sq[0].contains("CardNumNotedTypes")) { + return doXMath(c.getNumNotedTypes(), expr, c, ctb); + } + if (sq[0].contains("CardNumColors")) { return doXMath(c.getColor().countColors(), expr, c, ctb); } 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 17850725c26..a8afe1a805a 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 @@ -6,6 +6,7 @@ import java.util.Arrays; import java.util.List; import forge.card.CardType; +import forge.game.ability.AbilityUtils; import forge.game.ability.SpellAbilityEffect; import forge.game.card.Card; import forge.game.player.Player; @@ -49,7 +50,17 @@ public class ChooseTypeEffect extends SpellAbilityEffect { validTypes.addAll(CardType.getAllCardTypes()); break; case "Creature": - validTypes.addAll(CardType.getAllCreatureTypes()); + if (sa.hasParam("TypesFromDefined")) { + for (final Card c : AbilityUtils.getDefinedCards(card, sa.getParam("TypesFromDefined"), sa)) { + for (String t : c.getType()) { + if (CardType.isACreatureType(t)) { + validTypes.add(t); + } + } + } + } else { + validTypes.addAll(CardType.getAllCreatureTypes()); + } break; case "Basic Land": validTypes.addAll(CardType.getBasicTypes()); @@ -75,10 +86,17 @@ public class ChooseTypeEffect extends SpellAbilityEffect { for (final String s : invalidTypes) { validTypes.remove(s); } + if (sa.hasParam("Note") && card.hasAnyNotedType()) { + for (String noted : card.getNotedTypes()) { + validTypes.remove(noted); + } + } final TargetRestrictions tgt = sa.getTargetRestrictions(); - if (!validTypes.isEmpty()) { + if (validTypes.isEmpty() && sa.hasParam("Note")) { + // OK to end up with no choices/have nothing new to note + } else if (!validTypes.isEmpty()) { for (final Player p : tgtPlayers) { String choice; if ((tgt == null) || p.canBeTargetedBy(sa)) { @@ -89,7 +107,9 @@ public class ChooseTypeEffect extends SpellAbilityEffect { } else { choice = p.getController().chooseSomeType(type, sa, validTypes, invalidTypes); } - if (!sa.hasParam("ChooseType2")) { + if (sa.hasParam("Note")) { + card.addNotedType(choice); + } else if (!sa.hasParam("ChooseType2")) { card.setChosenType(choice); } else { card.setChosenType2(choice); diff --git a/forge-game/src/main/java/forge/game/ability/effects/DraftEffect.java b/forge-game/src/main/java/forge/game/ability/effects/DraftEffect.java index 8ded58eb4e0..10d01902209 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/DraftEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/DraftEffect.java @@ -26,12 +26,12 @@ import java.util.*; final StringBuilder sb = new StringBuilder(); sb.append(player).append(" drafts a card from ").append(source.getName()).append("'s spellbook"); - if (zone.equals("Hand")) { + if (zone.equals(ZoneType.Hand)) { sb.append("."); - } else if (zone.equals("Battlefield")) { + } else if (zone.equals(ZoneType.Battlefield)) { sb.append(" and puts it onto the battlefield."); - } else if (zone.equals("Exile")) { - sb.append(", then exiles it."); + } else if (zone.equals(ZoneType.Exile)) { + sb.append(sa.hasParam("ExileFaceDown") ? " and exiles it face down." : ", then exiles it."); } return sb.toString(); @@ -73,6 +73,14 @@ import java.util.*; final CardZoneTable triggerList = new CardZoneTable(); for (final Card c : drafted) { Card made = game.getAction().moveTo(zone, c, sa, moveParams); + if (zone.equals(ZoneType.Exile)) { + source.addExiledCard(made); + made.setExiledWith(source); + made.setExiledBy(source.getController()); + if (sa.hasParam("ExileFaceDown")) { + made.turnFaceDown(true); + } + } if (c != null) { triggerList.put(ZoneType.None, made.getZone().getZoneType(), made); } 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 2e13623b01a..eab8afa5f4e 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -276,6 +276,7 @@ public class Card extends GameEntity implements Comparable, IHasSVars { private String originalText = "", text = ""; private String chosenType = ""; private String chosenType2 = ""; + private List notedTypes = new ArrayList<>(); private List chosenColors; private String chosenName = ""; private String chosenName2 = ""; @@ -1745,6 +1746,29 @@ public class Card extends GameEntity implements Comparable, IHasSVars { return chosenType2 != null && !chosenType2.isEmpty(); } + public final boolean hasAnyNotedType() { + return notedTypes != null && !notedTypes.isEmpty(); + } + + public final void addNotedType(final String type) { + notedTypes.add(type); + view.updateNotedTypes(this); + } + + public final Iterable getNotedTypes() { + if (notedTypes == null) { + return Lists.newArrayList(); + } + return notedTypes; + } + + public final int getNumNotedTypes() { + if (notedTypes == null) { + return 0; + } + return notedTypes.size(); + } + public final String getChosenColor() { if (hasChosenColor()) { return chosenColors.get(0); diff --git a/forge-game/src/main/java/forge/game/card/CardView.java b/forge-game/src/main/java/forge/game/card/CardView.java index ce634bb9649..4c31d2c1cd6 100644 --- a/forge-game/src/main/java/forge/game/card/CardView.java +++ b/forge-game/src/main/java/forge/game/card/CardView.java @@ -359,6 +359,13 @@ public class CardView extends GameEntityView { set(TrackableProperty.ChosenType2, c.getChosenType2()); } + public List getNotedTypes() { + return get(TrackableProperty.NotedTypes); + } + void updateNotedTypes(Card c) { + set(TrackableProperty.NotedTypes, c.getNotedTypes()); + } + public String getChosenNumber() { return get(TrackableProperty.ChosenNumber); } diff --git a/forge-game/src/main/java/forge/trackable/TrackableProperty.java b/forge-game/src/main/java/forge/trackable/TrackableProperty.java index f1f397ab4a9..62dbe8e9b96 100644 --- a/forge-game/src/main/java/forge/trackable/TrackableProperty.java +++ b/forge-game/src/main/java/forge/trackable/TrackableProperty.java @@ -59,6 +59,7 @@ public enum TrackableProperty { ShieldCount(TrackableTypes.IntegerType), ChosenType(TrackableTypes.StringType), ChosenType2(TrackableTypes.StringType), + NotedTypes(TrackableTypes.StringListType), ChosenColors(TrackableTypes.StringListType), ChosenCards(TrackableTypes.CardViewCollectionType), ChosenNumber(TrackableTypes.StringType), diff --git a/forge-gui-mobile/src/forge/adventure/scene/RewardScene.java b/forge-gui-mobile/src/forge/adventure/scene/RewardScene.java index b5912a19fe5..eee8d441ce3 100644 --- a/forge-gui-mobile/src/forge/adventure/scene/RewardScene.java +++ b/forge-gui-mobile/src/forge/adventure/scene/RewardScene.java @@ -233,9 +233,9 @@ public class RewardScene extends UIScene { } else { //immersive | no navigation and/or showing cutout cam if (fW/fH > 2.3f) - mul *= Forge.isLandscapeMode() ? 1.1f : 1.5f; + mul *= Forge.isLandscapeMode() ? 1.1f : 1.6f; else if (fW/fH > 2f) - mul *= Forge.isLandscapeMode() ? 1.1f : 1.3f; + mul *= Forge.isLandscapeMode() ? 1.1f : 1.5f; } cardWidth = (cardHeight / CARD_WIDTH_TO_HEIGHT)*mul; diff --git a/forge-gui-mobile/src/forge/adventure/util/RewardActor.java b/forge-gui-mobile/src/forge/adventure/util/RewardActor.java index 3ae92a9647a..e19d9aa93ee 100644 --- a/forge-gui-mobile/src/forge/adventure/util/RewardActor.java +++ b/forge-gui-mobile/src/forge/adventure/util/RewardActor.java @@ -353,21 +353,13 @@ public class RewardActor extends Actor implements Disposable, ImageFetcher.Callb int y = Forge.getDeviceAdapter().getRealScreenSize(false).getRight(); int realX = Forge.getDeviceAdapter().getRealScreenSize(true).getLeft(); int realY = Forge.getDeviceAdapter().getRealScreenSize(true).getRight(); - float fW = x > y ? x : y; - float fH = x > y ? y : x; if (realX > x) { x *= 1.1f; } else if (realY > y) { y *= 1.1f; - } else { - if (fW/fH > 2f) { - //immersive | no navigation and showing cutout cam - if (Forge.isLandscapeMode()) - x *= 1.3f; - else - y *= 1.5f; - } } + float fW = x > y ? x : y; + float fH = x > y ? y : x; float mul = fW/fH < AR ? AR/(fW/fH) : (fW/fH)/AR; if (fW/fH >= 2f) {//tall display mul = (fW/fH) - ((fW/fH)/AR); diff --git a/forge-gui/res/cardsfolder/s/spelldrain_assassin.txt b/forge-gui/res/cardsfolder/s/spelldrain_assassin.txt new file mode 100644 index 00000000000..27ab1bf9680 --- /dev/null +++ b/forge-gui/res/cardsfolder/s/spelldrain_assassin.txt @@ -0,0 +1,10 @@ +Name:Spelldrain Assassin +ManaCost:U B R +Types:Creature Vampire Assassin +PT:3/3 +T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigChoose | TriggerDescription$ When CARDNAME enters the battlefield, choose an instant or sorcery card in your hand. It perpetually gains casualty 2. +SVar:TrigChoose:DB$ ChooseCard | ChoiceZone$ Hand | Choices$ Sorcery.YouOwn,Instant.YouOwn | ChoiceTitle$ Choose an instant or sorcery card in your hand | Amount$ 1 | SubAbility$ DBEffect +SVar:DBEffect:DB$ Effect | StaticAbilities$ PerpetualCasualty | Name$ Spelldrain Assassin's Perpetual Effect | Duration$ Permanent | SubAbility$ DBCleanup +SVar:PerpetualCasualty:Mode$ Continuous | Affected$ Card.ChosenCard | AddKeyword$ Casualty:2 | EffectZone$ Command | AffectedZone$ Battlefield,Hand,Graveyard,Exile,Stack,Library,Command | Description$ The chosen card perpetually gains casualty 2. +SVar:DBCleanup:DB$ Cleanup | ClearChosenCard$ True +Oracle:When Spelldrain Assassin enters the battlefield, choose an instant or sorcery card in your hand. It perpetually gains casualty 2. diff --git a/forge-gui/res/cardsfolder/upcoming/celestial_vault.txt b/forge-gui/res/cardsfolder/upcoming/celestial_vault.txt new file mode 100644 index 00000000000..8f81b206aad --- /dev/null +++ b/forge-gui/res/cardsfolder/upcoming/celestial_vault.txt @@ -0,0 +1,11 @@ +Name:Celestial Vault +ManaCost:1 W +Types:Artifact +A:AB$ Draft | Cost$ W T | Spellbook$ Angel of Destiny,Resplendent Angel,Angel of Vitality,Righteous Valkyrie,Angel of Invention,Angel of Sanctions,Valkyrie Harbinger,Emancipation Angel,Youthful Valkyrie,Resplendent Marshal,Enduring Angel,Sigardian Savior,Serra Angel,Stalwart Valkyrie,Segovian Angel | Zone$ Exile | ExileFaceDown$ True | RememberDrafted$ True | SpellDescription$ Draft a card from CARDNAME's spellbook and exile it face down. +A:AB$ ChangeZoneAll | Cost$ 1 Sac<1/CARDNAME> | Origin$ Exile | Destination$ Hand | ChangeType$ Card.IsRemembered+ExiledWithSource | SpellDescription$ Put each card exiled with CARDNAME into your hand. +T:Mode$ ChangesZone | Origin$ Exile | Destination$ Any | Static$ True | ValidCard$ Card.IsRemembered+ExiledWithSource | Execute$ DBForget +SVar:DBForget:DB$ Pump | ForgetObjects$ TriggeredCard +T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Any | Static$ True | ValidCard$ Card.Self | Execute$ DBCleanup +SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True +DeckHas:Type$Angel & Ability$LifeGain|Token|Counters +Oracle:{W}, {T}: Draft a card from Celestial Vault's spellbook and exile it face down.\n{1}, Sacrifice Celestial Vault: Put each card exiled with Celestial Vault into your hand. diff --git a/forge-gui/res/cardsfolder/upcoming/kenku_artificer.txt b/forge-gui/res/cardsfolder/upcoming/kenku_artificer.txt index fbc6977cb62..ecd87182438 100644 --- a/forge-gui/res/cardsfolder/upcoming/kenku_artificer.txt +++ b/forge-gui/res/cardsfolder/upcoming/kenku_artificer.txt @@ -1,5 +1,5 @@ Name:Kenku Artificer -ManaCost:2 G +ManaCost:2 U Types:Creature Bird Artificer PT:1/1 T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigPutCounter | TriggerDescription$ Homunculus Servant — When CARDNAME enters the battlefield, put three +1/+1 counters on up to one target noncreature artifact. That artifact becomes a 0/0 Homunculus artifact creature with flying. diff --git a/forge-gui/res/cardsfolder/upcoming/volo_itinerant_scholar.txt b/forge-gui/res/cardsfolder/upcoming/volo_itinerant_scholar.txt new file mode 100644 index 00000000000..d4c504a5b8d --- /dev/null +++ b/forge-gui/res/cardsfolder/upcoming/volo_itinerant_scholar.txt @@ -0,0 +1,12 @@ +Name:Volo, Itinerant Scholar +ManaCost:2 U +Types:Legendary Creature Human Wizard +PT:2/3 +T:Mode$ ChangesZone | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigToken | TriggerDescription$ When NICKNAME enters the battlefield, create Volo's Journal, a legendary colorless artifact token with hexproof and "Whenever you cast a creature spell, note one of its creature types that hasn't been noted for this artifact." +SVar:TrigToken:DB$ Token | TokenScript$ volos_journal +A:AB$ Pump | Cost$ 2 T | ValidTgts$ Permanent.namedVolo's Journal+YouCtrl | TgtPrompt$ Select target permanent you control named Volo's Journal | SubAbility$ DBDraw | StackDescription$ None | SpellDescription$ Draw a card for each creature type noted for target permanent you control named Volo's Journal. +SVar:DBDraw:DB$ Draw | NumCards$ X +SVar:X:Targeted$CardNumNotedTypes +K:Choose a Background +DeckHas:Ability$Token & Type$Artifact +Oracle:When Volo enters the battlefield, create Volo's Journal, a legendary colorless artifact token with hexproof and "Whenever you cast a creature spell, note one of its creature types that hasn't been noted for this artifact."\n{2}, {T}: Draw a card for each creature type noted for target permanent you control named Volo's Journal.\nChoose a Background diff --git a/forge-gui/res/tokenscripts/volos_journal.txt b/forge-gui/res/tokenscripts/volos_journal.txt new file mode 100644 index 00000000000..83d5007b930 --- /dev/null +++ b/forge-gui/res/tokenscripts/volos_journal.txt @@ -0,0 +1,7 @@ +Name:Volo's Journal +ManaCost:no cost +Types:Legendary Artifact +K:Hexproof +T:Mode$ SpellCast | ValidCard$ Creature | ValidActivatingPlayer$ You | Execute$ TrigNoteType | TriggerZones$ Battlefield | TriggerDescription$ Whenever you cast a creature spell, note one of its creature types that hasn't been noted for this artifact. +SVar:TrigNoteType:DB$ ChooseType | Type$ Creature | TypesFromDefined$ TriggeredCard | Note$ True +Oracle:Hexproof\nWhenever you cast a creature spell, note one of its creature types that hasn't been noted for this artifact. diff --git a/forge-gui/src/main/java/forge/gui/card/CardDetailUtil.java b/forge-gui/src/main/java/forge/gui/card/CardDetailUtil.java index 63b28987d0f..a64fc6ffe04 100644 --- a/forge-gui/src/main/java/forge/gui/card/CardDetailUtil.java +++ b/forge-gui/src/main/java/forge/gui/card/CardDetailUtil.java @@ -419,6 +419,16 @@ public class CardDetailUtil { area.append(")"); } + // noted types + if (card.getNotedTypes() != null && !card.getNotedTypes().isEmpty()) { + if (area.length() != 0) { + area.append("\n"); + } + area.append("(noted type").append(card.getNotedTypes().size() == 1 ? ": " : "s: "); + area.append(Lang.joinHomogenous(card.getNotedTypes())); + area.append(")"); + } + // chosen color if (card.getChosenColors() != null && !card.getChosenColors().isEmpty()) { if (area.length() != 0) {