-
Notifications
You must be signed in to change notification settings - Fork 835
Fix alternative costs from static abilities #4696
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c8f9774
764f763
0540ee9
f041bd5
7d9ed2e
0b3a898
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,7 @@ | |
| */ | ||
| package forge.game; | ||
|
|
||
| import java.util.Collection; | ||
| import java.util.Collections; | ||
| import java.util.EnumSet; | ||
| import java.util.List; | ||
|
|
@@ -52,6 +53,8 @@ | |
| import forge.game.zone.ZoneType; | ||
| import forge.util.Lang; | ||
| import forge.util.TextUtil; | ||
| import forge.util.collect.FCollection; | ||
| import forge.util.collect.FCollectionView; | ||
|
|
||
|
|
||
| /** | ||
|
|
@@ -269,35 +272,6 @@ public static final List<SpellAbility> getAlternativeCosts(final SpellAbility sa | |
| foretold.putParam("AfterDescription", "(Foretold)"); | ||
| alternatives.add(foretold); | ||
| } | ||
|
|
||
| // some needs to check after ability was put on the stack | ||
| // Currently this is only checked for Toolbox and that only cares about creature spells | ||
| if (source.isCreature() && game.getAction().hasStaticAbilityAffectingZone(ZoneType.Stack, StaticAbilityLayer.ABILITIES)) { | ||
| Zone oldZone = source.getLastKnownZone(); | ||
| Card blitzCopy = source; | ||
| if (!source.isLKI()) { | ||
| blitzCopy = CardUtil.getLKICopy(source); | ||
| } | ||
| blitzCopy.setLastKnownZone(game.getStackZone()); | ||
| lkicheck = true; | ||
|
|
||
| blitzCopy.clearStaticChangedCardKeywords(false); | ||
| CardCollection preList = new CardCollection(blitzCopy); | ||
| game.getAction().checkStaticAbilities(false, Sets.newHashSet(blitzCopy), preList); | ||
|
|
||
| // currently only for Keyword BLitz, but should affect Dash probably too | ||
| for (final KeywordInterface inst : blitzCopy.getKeywords(Keyword.BLITZ)) { | ||
| // TODO with mana value 4 or greater has blitz. | ||
| for (SpellAbility iSa : inst.getAbilities()) { | ||
| // do only non intrinsic | ||
| if (!iSa.isIntrinsic()) { | ||
| alternatives.add(iSa); | ||
| } | ||
| } | ||
| } | ||
| // need to reset to Old Zone, or canPlay would fail | ||
| blitzCopy.setLastKnownZone(oldZone); | ||
| } | ||
| } | ||
|
|
||
| // reset static abilities | ||
|
|
@@ -377,6 +351,72 @@ public static final List<SpellAbility> getAlternativeCosts(final SpellAbility sa | |
| } | ||
| return alternatives; | ||
| } | ||
|
|
||
| /** | ||
| * Checks a card to see if it qualifies to receive keywords from static | ||
| * abilities that grant alternative casting costs to spells on the stack, for | ||
| * example, Toolbox's ability that gives blitz to creature spells with mana | ||
| * value 4 or greater, and Hunting Velociraptor's ability that gives prowl to | ||
| * Dinosaur spells. | ||
| * | ||
| * @param source The card to check eligibility for. | ||
| * @return Zero or more spell abilities, each representing an alternative | ||
| * casting cost. | ||
| */ | ||
| public static final FCollectionView<SpellAbility> getAlternativeCostsGrantedByStaticAbilities(Card source) { | ||
| final FCollection<SpellAbility> alternatives = new FCollection<SpellAbility>(); | ||
| final Game game = source.getGame(); | ||
|
|
||
| if (!game.getAction().hasStaticAbilityAffectingZone(ZoneType.Stack, StaticAbilityLayer.ABILITIES)) { | ||
| return alternatives; | ||
| } | ||
|
Comment on lines
+370
to
+372
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The previous logic checked if the This will allow static abilities that grant alternative costs to non-creature spells to function, also. |
||
|
|
||
| // double freeze tracker, so it doesn't update view | ||
| game.getTracker().freeze(); | ||
|
|
||
| Zone oldZone = source.getLastKnownZone(); | ||
| Card creatureCandidate = source; // Candidate to receive keyword. | ||
|
|
||
| if (!source.isLKI()) { | ||
| creatureCandidate = CardUtil.getLKICopy(source); | ||
| } | ||
|
Comment on lines
+374
to
+382
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I admit I don't really understand the concept of Last Known Information, so I've tried to leave it untouched, but I noticed that some of this logic was being run before the static ability checks were happening in the old location, so I included it here in case it's necessary. There's also corresponding code that is executed afterward, which book-ends the logic. |
||
|
|
||
| creatureCandidate.setLastKnownZone(game.getStackZone()); | ||
| creatureCandidate.clearStaticChangedCardKeywords(false); | ||
| CardCollection preList = new CardCollection(creatureCandidate); | ||
| game.getAction().checkStaticAbilities(false, Sets.newHashSet(creatureCandidate), preList); | ||
|
|
||
| for (final KeywordInterface keywordInterface : creatureCandidate.getUnhiddenKeywords()) { | ||
| try { | ||
| // Try to find the keyword in the list of alternative cost keywords. | ||
| // If the operation doesn't throw an IllegalArgumentException, the keyword | ||
| // is an alternative cost. | ||
| AlternativeCost.valueOf(keywordInterface.getKeyword().toString()); | ||
|
|
||
| for (SpellAbility iSa : keywordInterface.getAbilities()) { | ||
| // do only non intrinsic | ||
| if (!iSa.isIntrinsic()) { | ||
| iSa.setHostCard(source); | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This line is new. Without it, the alternative cost isn't copied for additional |
||
| alternatives.add(iSa); | ||
| } | ||
| } | ||
| } catch (IllegalArgumentException e) { | ||
| // The keyword interface isn't for an alternative cost keyword. | ||
| continue; | ||
| } | ||
|
Comment on lines
+403
to
+406
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Some places I've worked frown upon using exceptions this way. If it violates convention here, I'll write a new function to verify the keyword represents an alternative cost rather than calling |
||
| } | ||
|
|
||
| // need to reset to Old Zone, or canPlay fails. | ||
| creatureCandidate.setLastKnownZone(oldZone); | ||
|
|
||
| game.getAction().checkStaticAbilities(false); | ||
| // clear delayed changes, this check should not have updated the view. | ||
| game.getTracker().clearDelayed(); | ||
| // need to unfreeze tracker. | ||
| game.getTracker().unfreeze(); | ||
|
|
||
| return alternatives; | ||
| } | ||
|
|
||
| public static List<OptionalCostValue> getOptionalCostValues(final SpellAbility sa) { | ||
| final List<OptionalCostValue> costs = Lists.newArrayList(); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7153,7 +7153,8 @@ public Game getGame() { | |
| public List<SpellAbility> getAllPossibleAbilities(final Player player, final boolean removeUnplayable) { | ||
| CardState oState = getState(CardStateName.Original); | ||
| final List<SpellAbility> abilities = Lists.newArrayList(); | ||
| for (SpellAbility sa : getSpellAbilities()) { | ||
| FCollectionView<SpellAbility> spellAbilitiesFromStaticAbilities = GameActionUtil.getAlternativeCostsGrantedByStaticAbilities(this); | ||
| for (SpellAbility sa : Iterables.concat(getSpellAbilities(), spellAbilitiesFromStaticAbilities)) { | ||
|
Comment on lines
+7156
to
+7157
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. with your change you can't Prowl/Blitz a Modular DFC (see below) that's why called by GameActionUtil.getAlternativeCosts is better |
||
| //adventure spell check | ||
| if (isAdventureCard() && sa.isAdventure()) { | ||
| if (getExiledWith() != null && getExiledWith().equals(this) && CardStateName.Adventure.equals(getExiledWith().getCurrentStateName())) | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,7 +2,7 @@ Name:Henzie "Toolbox" Torre | |||||||||||||||||||||||||||||||||||||||||||||
| ManaCost:B R G | ||||||||||||||||||||||||||||||||||||||||||||||
| Types: Legendary Creature Devil Rogue | ||||||||||||||||||||||||||||||||||||||||||||||
| PT:3/3 | ||||||||||||||||||||||||||||||||||||||||||||||
| S:Mode$ Continuous | Affected$ Creature.YouCtrl | AffectedZone$ Stack | AddKeyword$ Blitz:CardManaCost:Spell.Creature+cmcGE4 | Description$ Each creature spell you cast with mana value 4 or greater has blitz. The blitz cost is equal to its mana cost. (You may choose to cast that spell for its blitz cost. If you do, it gains haste and "When this creature dies, draw a card." Sacrifice it at the beginning of the next end step.) | ||||||||||||||||||||||||||||||||||||||||||||||
| S:Mode$ Continuous | Affected$ Creature.YouCtrl+cmcGE4 | AffectedZone$ Stack | AddKeyword$ Blitz:CardManaCost:Spell.Creature | Description$ Each creature spell you cast with mana value 4 or greater has blitz. The blitz cost is equal to its mana cost. (You may choose to cast that spell for its blitz cost. If you do, it gains haste and "When this creature dies, draw a card." Sacrifice it at the beginning of the next end step.) | ||||||||||||||||||||||||||||||||||||||||||||||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The previous implementation was so close to being correct!
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This Check is for MDFC is the affected checked again when casting the Backside?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll look into this later today (it's currently 3am local time for me).
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For Henzie, it also needs to check the Spell Mana Value because for X Creature Spells so you can only Blitz them when X is high enough
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, that wasn't happening at all before. The exclusive restriction wasn't defined in the right place. I moved it to the Also, the wording of Henzie is mana value, not mana cost, so it will always consider X to be zero when checking this restriction.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Just my luck! 😆
Right, fair enough. It is turning out to be much hairier than anticipated. Would it be better to just shelve this in lieu of a more comprehensive refactor later, then?
Thank you! I was very confused but this makes sense. I'm obviously very new to the codebase so I'm not clear on how Forge lines things up relative to the "proposal to cast a spell". When the user clicks a card in their hand, for example, Forge appears to get all costs and alternative costs at that point, and present them as a list for the user. For X cost creatures in Toolbox's case, should it present the Blitz option the way it normally would, but with X in the cost, then prompt for X, then check the total mana value of the spell on the stack and either cancel/prompt for mana?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for human it goes into this line: forge/forge-gui/src/main/java/forge/player/HumanPlaySpellAbility.java Lines 160 to 166 in 3fa89af
where it checks this: forge/forge-game/src/main/java/forge/game/spellability/SpellAbility.java Lines 2574 to 2579 in 3fa89af
which is set by forge/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java Lines 2712 to 2720 in 3fa89af
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
It's your choice I guess. I'm not saying you can't try to tackle this and splitting it across consecutive PR is also sometimes easier. Just don't want you getting frustrated by this too quickly ;) You noticed correctly that spell casting is one of the parts where we're not super accurate in the order according to the CR (sometimes it's also different for AI vs Human PlayerController). I suppose we could have less trouble in that regard if we would basically allow any card to be moved to the stack first as outlined by 601.2a. But then again this might end up too confusing GUI wise, which is one of the reasons we're doing some of these choices in sort of a "look-ahead" fashion. Another one would be this:
So yea, that gets messy pretty quickly... 🤹♂️
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, geez. It all makes sense now. That clears everything up, thanks @Hanmac. I saw the comment in It didn't help that I didn't know about rule Thanks for your patience, @Hanmac. I've been very obtuse. So, to summarise:
I thought that moving the static ability checking code back to where it came from and duplicating the loop over Thanks again, both of you, for taking the time to educate me on the codebase!
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Oh I'm happy to keep thrashing away at this. I've been a software engineer for over a decade now so I'm no stranger to frustration 😆 Thanks for the warm welcome. I'm looking forward to learning more about how Forge does things. I might take a look at some of the beginner friendly issues to get a bit more orientation, as well.
Yes, absolutely. Those two rules, if implemented faithfully, would turn Forge into a Christopher Nolan film. 😬 |
||||||||||||||||||||||||||||||||||||||||||||||
| S:Mode$ ReduceCost | ValidCard$ Card | ValidSpell$ Spell.Blitz | Activator$ You | Amount$ X | Description$ Blitz costs you pay cost {1} less for each time you've cast your commander from the command zone this game. | ||||||||||||||||||||||||||||||||||||||||||||||
| SVar:X:Count$TotalCommanderCastFromCommandZone | ||||||||||||||||||||||||||||||||||||||||||||||
| Oracle:Each creature spell you cast with mana value 4 or greater has blitz. The blitz cost is equal to its mana cost. (You may choose to cast that spell for its blitz cost. If you do, it gains haste and "When this creature dies, draw a card." Sacrifice it at the beginning of the next end step.)\nBlitz costs you pay cost {1} less for each time you've cast your commander from the command zone this game. | ||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This logic was moved, mostly unchanged, into
getAlternativeCostsGrantedByStaticAbilities.