Compare commits

..

1 Commits

Author SHA1 Message Date
Chris H
54b4f3edf1 Migrate TLA 2025-11-15 20:48:05 -05:00
17 changed files with 103 additions and 118 deletions

View File

@@ -1,8 +1,6 @@
Forge provides an in-game console in adventure mode. Forge provides an in-game console in adventure mode.
You can access (and close) the console while exploring by pressing F9 (or Fn-F9). You can access (and close) the console while exploring by pressing F9 (or Fn-F9).
The equivalent method to access the console on mobile is to hold down the character image in the right top of the screen.
Holding the character image again will close the console (as will typing `exit`).
To scroll the console window, click and drag the text box. To scroll the console window, click and drag the text box.

View File

@@ -1,18 +1,14 @@
AbilityFactory parses differently from the Keyword parser. Your Ability line will look more like this: AbilityFactory parses differently from the Keyword parser. Your Ability line will look more like this:
`A:<AB/SP/DB/ST>$ <AFSubclass> | <Necessary$ Parameters> | (<Separated$ By> | <Pipes$ Here>) | [Optional$ {Values} [Nested$ Dependency]]` `A:{AB/SP/DB/ST}$ <AFSubclass> | {Necessary$ Parameters} | {Separated$ By} | {Pipes$ Here} | [Optional$ Values]`
The ability types are: In most cases, each AF subclass implements both the Spell and Ability.
- **AB** for Activated Abilities Much of the code is shared, so creating the data object will look very similar.
- **SP** for Spell
- **DB** for Drawback and many abilities that are subsidiary to other things, like replacements. They are only used to chain AFs together, and will never be the root AF
- **ST** for Static, this gets used in case the API should resolve without using the stack<br /> (e.g. the unique *Circling Vultures* special action is directly implemented in the script this way)
Syntax definitions like the above will use different symbols to separate the variable parts from the plaintext: - **AB** is for Activated Abilities
- angle brackets for mandatory parts - **SP** is for Spell
- square brackets for optional parts - **DB** is for Drawback and many abilities that are subsidiary to other things, like replacements. They are only used to chain AFs together, and will never be the root AF
- round brackets for grouping parts that are exclusive to each other - **ST** is for Static, this gets used in case the API should resolve without using the stack<br /> (e.g. the unique *Circling Vultures* special action is directly implemented in the script this way)
- curly brackets to denote the type of a param
>*NOTE:* >*NOTE:*
> - these factories are refactored from time to time (often to adapt to new sets), so while some entries could be slightly outdated, the base information should still be correct > - these factories are refactored from time to time (often to adapt to new sets), so while some entries could be slightly outdated, the base information should still be correct
@@ -24,7 +20,7 @@ Syntax definitions like the above will use different symbols to separate the var
## Cost / UnlessCost ## Cost / UnlessCost
`Cost$ {AbilityCost}` is the appropriate way to set the cost of the ability. Currently for spells, any additional costs including the original Mana cost need to appear in the Cost parameter in the AbilityFactory. For each card that uses it, the order in which the cost is paid will always be the same. `Cost$ <AbilityCost>` is the appropriate way to set the cost of the ability. Currently for spells, any additional costs including the original Mana cost need to appear in the Cost parameter in the AbilityFactory. For each card that uses it, the order in which the cost is paid will always be the same.
Secondary abilities such as the DB executed by triggers or replacements (usually) don't need costs. (This is one reason to use DB over AB in these cases.) Secondary abilities such as the DB executed by triggers or replacements (usually) don't need costs. (This is one reason to use DB over AB in these cases.)
@@ -519,6 +515,8 @@ Used in the script of *Karn Liberated*
## Goad ## Goad
## Investigate
## Mana ## Mana
For lands or other permanent to produce mana. For lands or other permanent to produce mana.
@@ -724,13 +722,7 @@ player chooses (eg: Burning of Xinye, or Imperial Edict).
## StoreSVar ## StoreSVar
## Tokens ## Token
### Amass
### Investigate
### Token
Token simply lets you create tokens of any type. Token simply lets you create tokens of any type.
@@ -767,8 +759,6 @@ If possible split the SpellDescription of the effect so the part for the trigger
### ImmediateTrigger ### ImmediateTrigger
TriggerAmount
## Turn structure ## Turn structure
### AddPhase ### AddPhase

View File

@@ -25,7 +25,7 @@ There are a few other properties that will appear in many cards. These are
| Property | Description | Property | Description
| - | - | - | -
|`A`|[Ability effect](Card-scripting-API/AbilityFactory.md) |`A`|[Ability effect](AbilityFactory)
|`AI`|RemoveDeck:<br />* `All`<br />This will prevent the card from appearing in random AI decks. It is applicable for cards the AI can't use at all like Dark Ritual and also for cards that the AI could use, but only ineffectively like Tortoise Formation. The AI won't draft these cards.<br />* `Random`<br /> This will prevent the card from appearing in random decks. It is only applicable for cards that are too narrow for random decks like Root Cage or Into the North. The AI won't draft these cards.<br />* `NonCommander`<br /> |`AI`|RemoveDeck:<br />* `All`<br />This will prevent the card from appearing in random AI decks. It is applicable for cards the AI can't use at all like Dark Ritual and also for cards that the AI could use, but only ineffectively like Tortoise Formation. The AI won't draft these cards.<br />* `Random`<br /> This will prevent the card from appearing in random decks. It is only applicable for cards that are too narrow for random decks like Root Cage or Into the North. The AI won't draft these cards.<br />* `NonCommander`<br />
|`Colors`|Color(s) of the card<br /><br />When a card's color is determined by a color indicator rather than shards in a mana cost, this property must be defined. If no identifier is needed, this property should be omitted.<br /><br />* `Colors:red` - This is used on Kobolds of Kher Keep, which has a casting cost of {0} and requires a red indicator to make it red.<br /><br />* `Colors:red,green` - Since Arlinn, Embraced by the Moon has no casting cost (it's the back of a double-faced card), the red and green indicator must be included. |`Colors`|Color(s) of the card<br /><br />When a card's color is determined by a color indicator rather than shards in a mana cost, this property must be defined. If no identifier is needed, this property should be omitted.<br /><br />* `Colors:red` - This is used on Kobolds of Kher Keep, which has a casting cost of {0} and requires a red indicator to make it red.<br /><br />* `Colors:red,green` - Since Arlinn, Embraced by the Moon has no casting cost (it's the back of a double-faced card), the red and green indicator must be included.
|`DeckHints`|AI-related hints for a deck including this card<br /><br />To improve synergy this will increase the rank of of all other cards that share some of its DeckHints types. This helps with smoothing the selection so cards without these Entries won't be at an unfair disadvantage.<br /><br />The relevant code can be found in the [CardRanker](https://github.com/Card-Forge/forge/blob/master/forge-gui/src/main/java/forge/gamemodes/limited/CardRanker.java) class. |`DeckHints`|AI-related hints for a deck including this card<br /><br />To improve synergy this will increase the rank of of all other cards that share some of its DeckHints types. This helps with smoothing the selection so cards without these Entries won't be at an unfair disadvantage.<br /><br />The relevant code can be found in the [CardRanker](https://github.com/Card-Forge/forge/blob/master/forge-gui/src/main/java/forge/gamemodes/limited/CardRanker.java) class.
@@ -37,10 +37,10 @@ There are a few other properties that will appear in many cards. These are
|`Name`|Name of the card<br /><br />A string of text that serves as the name of the card. Note that the registered trademark symbol cannot be included, and this property must have at least one character.<br /><br />Example:<br />* `Name:A Display of My Dark Power` sets the card's name to "A Display of My Dark Power" |`Name`|Name of the card<br /><br />A string of text that serves as the name of the card. Note that the registered trademark symbol cannot be included, and this property must have at least one character.<br /><br />Example:<br />* `Name:A Display of My Dark Power` sets the card's name to "A Display of My Dark Power"
|`Oracle`|The current Oracle text used by the card.<br /><br />We actually have a Python Script that runs to be able to fill in this information, so don't worry about manually editing a lot of cards when Wizards decides to change the rules. <br /><br />This field is used by the Deck Editor to allow non-Legendary Creatures to be marked as potential commanders. Make sure "CARDNAME can be your commander." appears in the oracle text. |`Oracle`|The current Oracle text used by the card.<br /><br />We actually have a Python Script that runs to be able to fill in this information, so don't worry about manually editing a lot of cards when Wizards decides to change the rules. <br /><br />This field is used by the Deck Editor to allow non-Legendary Creatures to be marked as potential commanders. Make sure "CARDNAME can be your commander." appears in the oracle text.
|`PT`|Power and toughness |`PT`|Power and toughness
|`R`|[Replacement effect](Card-scripting-API/Replacements.md) |`R`|[Replacement effect](Replacements)
|`S`|[Static ability](Card-scripting-API/Statics.md) |`S`|[Static ability](static-abilities)
|`SVar`|String variable. Used throughout scripting in a handful of different ways. |`SVar`|String variable. Used throughout scripting in a handful of different ways.
|`T`|[Triggered ability](Card-scripting-API/Triggers.md) |`T`|[Triggered ability](Triggers)
|`Text`|Additional text that needs to be displayed on the CardDetailPanel that doesn't have any spell/ability that generates a description for it, for example "CARDNAME can be your commander." or "X can't be 0.". |`Text`|Additional text that needs to be displayed on the CardDetailPanel that doesn't have any spell/ability that generates a description for it, for example "CARDNAME can be your commander." or "X can't be 0.".
|`Types`|Card types and subtypes<br /><br />Include all card types and subtypes, separated by spaces.<br /><br />Example:<br />* `Types:Enchantment Artifact Creature Golem` for a card that reads Enchantment Artifact Creature -- Golem |`Types`|Card types and subtypes<br /><br />Include all card types and subtypes, separated by spaces.<br /><br />Example:<br />* `Types:Enchantment Artifact Creature Golem` for a card that reads Enchantment Artifact Creature -- Golem

View File

@@ -1,23 +0,0 @@
There are two major groups of static abilities:
# Statics for the main 7 layers
Syntax:
`S:Mode$ <Continuous> | <Affected$ {Valid Player/Card}> | <Layer-specific$ Params> | [Description$ {String}]`
Here's an example for layer 7c:
`Affected$ Creature.YouCtrl | AddPower$ 1 | AddToughness$ 1 | Description$ Creatures you control get +1/+1.`
See [StaticAbility.generateLayer()](https://github.com/Card-Forge/forge/blob/master/forge-game/src/main/java/forge/game/staticability/StaticAbility.java) for the full list of params on each Layer.
*Note:* Layer 1 is currently only implemented as a resolving effect instead.
# Statics for the concluding "game rules layer" ([CR 613.11](https://yawgatog.com/resources/magic-rules/#R61311))
The available effects are defined here: [StaticAbilityMode](https://github.com/Card-Forge/forge/blob/master/forge-game/src/main/java/forge/game/staticability/StaticAbilityMode.java).
*Note:* some rules-modifying parts are still coded via `Continuous` mode for now, e.g. `SetMaxHandSize$ {Integer}`.
## Combat
## Costs

View File

@@ -38,7 +38,7 @@
- [Ability effects](Card-scripting-API/AbilityFactory.md) - [Ability effects](Card-scripting-API/AbilityFactory.md)
- [Triggers](Card-scripting-API/Triggers.md) - [Triggers](Card-scripting-API/Triggers.md)
- [Replacements](Card-scripting-API/Replacements.md) - [Replacements](Card-scripting-API/Replacements.md)
- [Statics](Card-scripting-API/Statics.md) - Statics
- [Costs](Card-scripting-API/Costs.md) - [Costs](Card-scripting-API/Costs.md)
- [Affected / Targets](Card-scripting-API/Targeting.md) - [Affected / Targets](Card-scripting-API/Targeting.md)
- [Restrictions / Conditions](Card-scripting-API/Restrictions.md) - [Restrictions / Conditions](Card-scripting-API/Restrictions.md)

View File

@@ -810,13 +810,13 @@ public class AiController {
return reserveManaSources(sa, phaseType, enemy, true, null); return reserveManaSources(sa, phaseType, enemy, true, null);
} }
public boolean reserveManaSources(SpellAbility sa, PhaseType phaseType, boolean enemy, boolean forNextSpell, SpellAbility exceptForThisSa) { public boolean reserveManaSources(SpellAbility sa, PhaseType phaseType, boolean enemy, boolean forNextSpell, SpellAbility exceptForThisSa) {
ManaCostBeingPaid cost = ComputerUtilMana.calculateManaCost(sa.getPayCosts(), sa, player, true, 0, false); ManaCostBeingPaid cost = ComputerUtilMana.calculateManaCost(sa.getPayCosts(), sa, true, 0, false);
CardCollection manaSources = ComputerUtilMana.getManaSourcesToPayCost(cost, sa, player); CardCollection manaSources = ComputerUtilMana.getManaSourcesToPayCost(cost, sa, player);
// used for chained spells where two spells need to be cast in succession // used for chained spells where two spells need to be cast in succession
if (exceptForThisSa != null) { if (exceptForThisSa != null) {
manaSources.removeAll(ComputerUtilMana.getManaSourcesToPayCost( manaSources.removeAll(ComputerUtilMana.getManaSourcesToPayCost(
ComputerUtilMana.calculateManaCost(exceptForThisSa.getPayCosts(), exceptForThisSa, player, true, 0, false), ComputerUtilMana.calculateManaCost(exceptForThisSa.getPayCosts(), exceptForThisSa, true, 0, false),
exceptForThisSa, player)); exceptForThisSa, player));
} }

View File

@@ -509,16 +509,16 @@ public class ComputerUtilCost {
* *
* @param sa * @param sa
* a {@link forge.game.spellability.SpellAbility} object. * a {@link forge.game.spellability.SpellAbility} object.
* @param payer * @param player
* a {@link forge.game.player.Player} object. * a {@link forge.game.player.Player} object.
* @return a boolean. * @return a boolean.
*/ */
public static boolean canPayCost(final SpellAbility sa, final Player payer, final boolean effect) { public static boolean canPayCost(final SpellAbility sa, final Player player, final boolean effect) {
return canPayCost(sa.getPayCosts(), sa, payer, effect); return canPayCost(sa.getPayCosts(), sa, player, effect);
} }
public static boolean canPayCost(final Cost cost, final SpellAbility sa, final Player payer, final boolean effect) { public static boolean canPayCost(final Cost cost, final SpellAbility sa, final Player player, final boolean effect) {
if (sa.getActivatingPlayer() == null) { if (sa.getActivatingPlayer() == null) {
sa.setActivatingPlayer(payer); // complaints on NPE had came before this line was added. sa.setActivatingPlayer(player); // complaints on NPE had came before this line was added.
} }
// Check for stuff like Nether Void // Check for stuff like Nether Void
@@ -527,14 +527,14 @@ public class ComputerUtilCost {
boolean cannotBeCountered = !sa.isCounterableBy(null); boolean cannotBeCountered = !sa.isCounterableBy(null);
if (sa instanceof Spell) { if (sa instanceof Spell) {
for (Card c : payer.getGame().getCardsIn(ZoneType.Battlefield)) { for (Card c : player.getGame().getCardsIn(ZoneType.Battlefield)) {
final String snem = c.getSVar("AI_SpellsNeedExtraMana"); final String snem = c.getSVar("AI_SpellsNeedExtraMana");
if (!StringUtils.isBlank(snem)) { if (!StringUtils.isBlank(snem)) {
if (cannotBeCountered && c.getName().equals("Nether Void")) { if (cannotBeCountered && c.getName().equals("Nether Void")) {
continue; continue;
} }
String[] parts = TextUtil.split(snem, ' '); String[] parts = TextUtil.split(snem, ' ');
boolean meetsRestriction = parts.length == 1 || payer.isValid(parts[1], c.getController(), c, sa); boolean meetsRestriction = parts.length == 1 || player.isValid(parts[1], c.getController(), c, sa);
if(!meetsRestriction) if(!meetsRestriction)
continue; continue;
@@ -545,7 +545,7 @@ public class ComputerUtilCost {
} }
} }
} }
for (Card c : payer.getCardsIn(ZoneType.Command)) { for (Card c : player.getCardsIn(ZoneType.Command)) {
if (cannotBeCountered) { if (cannotBeCountered) {
continue; continue;
} }
@@ -567,7 +567,7 @@ public class ComputerUtilCost {
if (part.convertAmount() != null && part.convertAmount() == sa.getHostCard().getCurrentLoyalty()) { if (part.convertAmount() != null && part.convertAmount() == sa.getHostCard().getCurrentLoyalty()) {
// refuse to pay if opponent has no creature threats or // refuse to pay if opponent has no creature threats or
// 50% chance otherwise // 50% chance otherwise
if (payer.getOpponents().getCreaturesInPlay().isEmpty() if (player.getOpponents().getCreaturesInPlay().isEmpty()
|| MyRandom.getRandom().nextFloat() < .5f) { || MyRandom.getRandom().nextFloat() < .5f) {
return false; return false;
} }
@@ -591,7 +591,7 @@ public class ComputerUtilCost {
Cost wardCost = ComputerUtilCard.getTotalWardCost(tgt); Cost wardCost = ComputerUtilCard.getTotalWardCost(tgt);
// don't use API converter since it might have special part logic not meant for Ward cost // don't use API converter since it might have special part logic not meant for Ward cost
SpellAbilityAi topAI = new SpellAbilityAi() {}; SpellAbilityAi topAI = new SpellAbilityAi() {};
if (!topAI.willPayCosts(payer, sa, wardCost, sa.getHostCard())) { if (!topAI.willPayCosts(player, sa, wardCost, sa.getHostCard())) {
return false; return false;
} }
if (wardCost.hasManaCost()) { if (wardCost.hasManaCost()) {
@@ -606,7 +606,7 @@ public class ComputerUtilCost {
if (sa.getHostCard().hasKeyword(Keyword.CASUALTY)) { if (sa.getHostCard().hasKeyword(Keyword.CASUALTY)) {
for (final CostPart part : cost.getCostParts()) { for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostSacrifice) { if (part instanceof CostSacrifice) {
CardCollection valid = CardLists.getValidCards(payer.getCardsIn(ZoneType.Battlefield), part.getType().split(";"), CardCollection valid = CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield), part.getType().split(";"),
sa.getActivatingPlayer(), sa.getHostCard(), sa); sa.getActivatingPlayer(), sa.getHostCard(), sa);
valid = CardLists.filter(valid, CardPredicates.hasSVar("AIDontSacToCasualty").negate()); valid = CardLists.filter(valid, CardPredicates.hasSVar("AIDontSacToCasualty").negate());
if (valid.isEmpty()) { if (valid.isEmpty()) {
@@ -618,8 +618,8 @@ public class ComputerUtilCost {
} }
// TODO both of these call CostAdjustment.adjust, try to reuse instead // TODO both of these call CostAdjustment.adjust, try to reuse instead
return ComputerUtilMana.canPayManaCost(cost, sa, payer, extraManaNeeded, effect) return ComputerUtilMana.canPayManaCost(cost, sa, player, extraManaNeeded, effect)
&& CostPayment.canPayAdditionalCosts(cost, sa, effect, payer); && CostPayment.canPayAdditionalCosts(cost, sa, effect, player);
} }
public static Set<String> getAvailableManaColors(Player ai, Card additionalLand) { public static Set<String> getAvailableManaColors(Player ai, Card additionalLand) {

View File

@@ -69,7 +69,7 @@ public class ComputerUtilMana {
return payManaCost(cost, sa, ai, false, 0, true, effect); return payManaCost(cost, sa, ai, false, 0, true, effect);
} }
private static boolean payManaCost(final Cost cost, final SpellAbility sa, final Player ai, final boolean test, final int extraMana, boolean checkPlayable, final boolean effect) { private static boolean payManaCost(final Cost cost, final SpellAbility sa, final Player ai, final boolean test, final int extraMana, boolean checkPlayable, final boolean effect) {
ManaCostBeingPaid manaCost = calculateManaCost(cost, sa, ai, test, extraMana, effect); ManaCostBeingPaid manaCost = calculateManaCost(cost, sa, test, extraMana, effect);
return payManaCost(manaCost, sa, ai, test, checkPlayable, effect); return payManaCost(manaCost, sa, ai, test, checkPlayable, effect);
} }
@@ -77,7 +77,7 @@ public class ComputerUtilMana {
* Return the number of colors used for payment for Converge * Return the number of colors used for payment for Converge
*/ */
public static int getConvergeCount(final SpellAbility sa, final Player ai) { public static int getConvergeCount(final SpellAbility sa, final Player ai) {
ManaCostBeingPaid cost = calculateManaCost(sa.getPayCosts(), sa, ai, true, 0, false); ManaCostBeingPaid cost = calculateManaCost(sa.getPayCosts(), sa, true, 0, false);
if (payManaCost(cost, sa, ai, true, true, false)) { if (payManaCost(cost, sa, ai, true, true, false)) {
return cost.getSunburst(); return cost.getSunburst();
} }
@@ -1291,7 +1291,7 @@ public class ComputerUtilMana {
* @param extraMana extraMana * @param extraMana extraMana
* @return ManaCost * @return ManaCost
*/ */
public static ManaCostBeingPaid calculateManaCost(final Cost cost, final SpellAbility sa, final Player payer, final boolean test, final int extraMana, final boolean effect) { public static ManaCostBeingPaid calculateManaCost(final Cost cost, final SpellAbility sa, final boolean test, final int extraMana, final boolean effect) {
Card host = sa.getHostCard(); Card host = sa.getHostCard();
Zone castFromBackup = null; Zone castFromBackup = null;
if (test && sa.isSpell() && !host.isInZone(ZoneType.Stack)) { if (test && sa.isSpell() && !host.isInZone(ZoneType.Stack)) {
@@ -1345,7 +1345,7 @@ public class ComputerUtilMana {
} }
} }
CostAdjustment.adjust(manaCost, sa, payer, null, test, effect); CostAdjustment.adjust(manaCost, sa, null, test, effect);
if ("NumTimes".equals(sa.getParam("Announce"))) { // e.g. the Adversary cycle if ("NumTimes".equals(sa.getParam("Announce"))) { // e.g. the Adversary cycle
ManaCost mkCost = sa.getPayCosts().getTotalMana(); ManaCost mkCost = sa.getPayCosts().getTotalMana();

View File

@@ -108,7 +108,7 @@ public class PermanentAi extends SpellAbilityAi {
if ("SacToReduceCost".equals(sa.getParam("AILogic"))) { if ("SacToReduceCost".equals(sa.getParam("AILogic"))) {
// reset X to better calculate // reset X to better calculate
sa.setXManaCostPaid(0); sa.setXManaCostPaid(0);
ManaCostBeingPaid paidCost = ComputerUtilMana.calculateManaCost(sa.getPayCosts(), sa, ai, true, 0, false); ManaCostBeingPaid paidCost = ComputerUtilMana.calculateManaCost(sa.getPayCosts(), sa, true, 0, false);
int generic = paidCost.getGenericManaAmount(); int generic = paidCost.getGenericManaAmount();
// Set PayX here to maximum value. // Set PayX here to maximum value.

View File

@@ -702,7 +702,7 @@ public class GameAction {
eff.addRemembered(copied); eff.addRemembered(copied);
// refresh needed for canEnchant checks // refresh needed for canEnchant checks
checkStaticAbilities(false, Sets.newHashSet(copied), new CardCollection(copied)); game.getAction().checkStaticAbilities(false, Sets.newHashSet(copied), new CardCollection(copied));
return eff; return eff;
} }
private void cleanStaticEffect(Card eff, Card copied) { private void cleanStaticEffect(Card eff, Card copied) {
@@ -809,7 +809,8 @@ public class GameAction {
} }
} }
// CR 720.4a Move card in maingame if take card from subgame // Move card in maingame if take card from subgame
// 720.4a
if (zoneFrom != null && zoneFrom.is(ZoneType.Sideboard) && game.getMaingame() != null) { if (zoneFrom != null && zoneFrom.is(ZoneType.Sideboard) && game.getMaingame() != null) {
Card maingameCard = c.getOwner().getMappingMaingameCard(c); Card maingameCard = c.getOwner().getMappingMaingameCard(c);
if (maingameCard != null) { if (maingameCard != null) {
@@ -1239,7 +1240,7 @@ public class GameAction {
} }
private StaticAbility findStaticAbilityToApply(StaticAbilityLayer layer, List<StaticAbility> staticsForLayer, CardCollectionView preList, Map<StaticAbility, CardCollectionView> affectedPerAbility, private StaticAbility findStaticAbilityToApply(StaticAbilityLayer layer, List<StaticAbility> staticsForLayer, CardCollectionView preList, Map<StaticAbility, CardCollectionView> affectedPerAbility,
Table<StaticAbility, StaticAbility, Set<StaticAbilityLayer>> dependencies) { Table<StaticAbility, StaticAbility, Set<StaticAbilityLayer>> dependencies) {
if (staticsForLayer.size() == 1) { if (staticsForLayer.size() == 1) {
return staticsForLayer.get(0); return staticsForLayer.get(0);
} }
@@ -2079,7 +2080,7 @@ public class GameAction {
} }
if (showRevealDialog) { if (showRevealDialog) {
final String message = Localizer.getInstance().getMessage("lblSacrifice"); final String message = Localizer.getInstance().getMessage("lblSacrifice");
reveal(result, ZoneType.Graveyard, c.getOwner(), false, message, false); game.getAction().reveal(result, ZoneType.Graveyard, c.getOwner(), false, message, false);
} }
} }
for (Map.Entry<Player, Collection<Card>> e : lki.asMap().entrySet()) { for (Map.Entry<Player, Collection<Card>> e : lki.asMap().entrySet()) {
@@ -2538,9 +2539,19 @@ public class GameAction {
game.getTriggerHandler().runTrigger(TriggerType.TakesInitiative, runParams, false); game.getTriggerHandler().runTrigger(TriggerType.TakesInitiative, runParams, false);
} }
// Make scry an action function so that it can be used for mulligans (with a null cause)
// Assumes that the list of players is in APNAP order, which should be the case
// Optional here as well to handle the way that mulligans do the choice
// 701.17. Scry
// 701.17a To "scry N" means to look at the top N cards of your library, then put any number of them
// on the bottom of your library in any order and the rest on top of your library in any order.
// 701.17b If a player is instructed to scry 0, no scry event occurs. Abilities that trigger whenever a
// player scries won't trigger.
// 701.17c If multiple players scry at once, each of those players looks at the top cards of their library
// at the same time. Those players decide in APNAP order (see rule 101.4) where to put those
// cards, then those cards move at the same time.
public void scry(final List<Player> players, int numScry, SpellAbility cause) { public void scry(final List<Player> players, int numScry, SpellAbility cause) {
if (numScry <= 0) { if (numScry <= 0) {
// CR 701.22b If a player is instructed to scry 0, no scry event occurs.
return; return;
} }
@@ -2566,9 +2577,12 @@ public class GameAction {
if (playerScry > 0) { if (playerScry > 0) {
actualPlayers.put(p, playerScry); actualPlayers.put(p, playerScry);
// no real need to separate out the look if there is only one player scrying // reveal the top N library cards to the player (only)
// no real need to separate out the look if
// there is only one player scrying
if (players.size() > 1) { if (players.size() > 1) {
revealTo(p.getCardsIn(ZoneType.Library, playerScry), p); final CardCollection topN = new CardCollection(p.getCardsIn(ZoneType.Library, playerScry));
revealTo(topN, p);
} }
} }
} }
@@ -2609,6 +2623,7 @@ public class GameAction {
} }
if (cause != null) { if (cause != null) {
// set up triggers (but not actually do them until later)
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(p); final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(p);
runParams.put(AbilityKey.ScryNum, numLookedAt); runParams.put(AbilityKey.ScryNum, numLookedAt);
runParams.put(AbilityKey.ScryBottom, toBottom == null ? 0 : toBottom.size()); runParams.put(AbilityKey.ScryBottom, toBottom == null ? 0 : toBottom.size());
@@ -2639,7 +2654,7 @@ public class GameAction {
if (showRevealDialog) { if (showRevealDialog) {
final String message = Localizer.getInstance().getMessage("lblMilledCards"); final String message = Localizer.getInstance().getMessage("lblMilledCards");
final boolean addSuffix = !toZoneStr.isEmpty(); final boolean addSuffix = !toZoneStr.isEmpty();
reveal(milledPlayer, destination, p, false, message, addSuffix); game.getAction().reveal(milledPlayer, destination, p, false, message, addSuffix);
} }
game.getGameLog().add(GameLogEntryType.ZONE_CHANGE, p + " milled " + game.getGameLog().add(GameLogEntryType.ZONE_CHANGE, p + " milled " +
Lang.joinHomogenous(milledPlayer) + toZoneStr + "."); Lang.joinHomogenous(milledPlayer) + toZoneStr + ".");
@@ -2656,7 +2671,7 @@ public class GameAction {
} }
public void dealDamage(final boolean isCombat, final CardDamageMap damageMap, final CardDamageMap preventMap, public void dealDamage(final boolean isCombat, final CardDamageMap damageMap, final CardDamageMap preventMap,
final GameEntityCounterTable counterTable, final SpellAbility cause) { final GameEntityCounterTable counterTable, final SpellAbility cause) {
// Clear assigned damage if is combat // Clear assigned damage if is combat
if (isCombat) { if (isCombat) {
for (Map.Entry<GameEntity, Map<Card, Integer>> et : damageMap.columnMap().entrySet()) { for (Map.Entry<GameEntity, Map<Card, Integer>> et : damageMap.columnMap().entrySet()) {

View File

@@ -1,5 +1,6 @@
package forge.game.ability.effects; package forge.game.ability.effects;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import forge.game.Game; import forge.game.Game;

View File

@@ -41,9 +41,9 @@ public class CostAdjustment {
return cost; return cost;
} }
final Player activator = sa.getActivatingPlayer(); final Player player = sa.getActivatingPlayer();
final Card host = sa.getHostCard(); final Card host = sa.getHostCard();
final Game game = activator.getGame(); final Game game = player.getGame();
Cost result = cost.copy(); Cost result = cost.copy();
boolean isStateChangeToFaceDown = false; boolean isStateChangeToFaceDown = false;
@@ -56,7 +56,7 @@ public class CostAdjustment {
// Commander Tax there // Commander Tax there
if (host.isCommander() && host.getCastFrom() != null && ZoneType.Command.equals(host.getCastFrom().getZoneType())) { if (host.isCommander() && host.getCastFrom() != null && ZoneType.Command.equals(host.getCastFrom().getZoneType())) {
int n = activator.getCommanderCast(host) * 2; int n = player.getCommanderCast(host) * 2;
if (n > 0) { if (n > 0) {
result.add(new Cost(ManaCost.get(n), false)); result.add(new Cost(ManaCost.get(n), false));
} }
@@ -152,13 +152,17 @@ public class CostAdjustment {
} }
sub = sub.getSubAbility(); sub = sub.getSubAbility();
} }
} else if (StringUtils.isNumeric(amount)) {
count = Integer.parseInt(amount);
} else if (st.hasParam("Relative")) {
// grab SVar here already to avoid potential collision when SA has one with same name
count = AbilityUtils.calculateAmount(hostCard, st.hasSVar(amount) ? st.getSVar(amount) : amount, sa);
} else { } else {
count = AbilityUtils.calculateAmount(hostCard, amount, st); if (StringUtils.isNumeric(amount)) {
count = Integer.parseInt(amount);
} else {
if (st.hasParam("Relative")) {
// grab SVar here already to avoid potential collision when SA has one with same name
count = AbilityUtils.calculateAmount(hostCard, st.hasSVar(amount) ? st.getSVar(amount) : amount, sa);
} else {
count = AbilityUtils.calculateAmount(hostCard, amount, st);
}
}
} }
} else { } else {
// Amount 1 as default // Amount 1 as default
@@ -172,16 +176,15 @@ public class CostAdjustment {
// If cardsToDelveOut is null, will immediately exile the delved cards and remember them on the host card. // If cardsToDelveOut is null, will immediately exile the delved cards and remember them on the host card.
// Otherwise, will return them in cardsToDelveOut and the caller is responsible for doing the above. // Otherwise, will return them in cardsToDelveOut and the caller is responsible for doing the above.
public static boolean adjust(ManaCostBeingPaid cost, final SpellAbility sa, final Player payer, CardCollection cardsToDelveOut, boolean test, boolean effect) { public static boolean adjust(ManaCostBeingPaid cost, final SpellAbility sa, CardCollection cardsToDelveOut, boolean test, boolean effect) {
if (effect) { if (effect) {
adjustCostByWaterbend(cost, sa, payer, test); adjustCostByWaterbend(cost, sa, test);
} }
if (effect || sa.isTrigger() || sa.isReplacementAbility()) { if (effect || sa.isTrigger() || sa.isReplacementAbility()) {
return true; return true;
} }
final Player activator = sa.getActivatingPlayer(); final Game game = sa.getActivatingPlayer().getGame();
final Game game = activator.getGame();
final Card originalCard = sa.getHostCard(); final Card originalCard = sa.getHostCard();
boolean isStateChangeToFaceDown = false; boolean isStateChangeToFaceDown = false;
@@ -227,7 +230,7 @@ public class CostAdjustment {
} }
while (!reduceAbilities.isEmpty()) { while (!reduceAbilities.isEmpty()) {
StaticAbility choice = activator.getController().chooseSingleStaticAbility(Localizer.getInstance().getMessage("lblChooseCostReduction"), reduceAbilities); StaticAbility choice = sa.getActivatingPlayer().getController().chooseSingleStaticAbility(Localizer.getInstance().getMessage("lblChooseCostReduction"), reduceAbilities);
reduceAbilities.remove(choice); reduceAbilities.remove(choice);
sumGeneric += applyReduceCostAbility(choice, sa, cost, sumGeneric); sumGeneric += applyReduceCostAbility(choice, sa, cost, sumGeneric);
} }
@@ -260,8 +263,9 @@ public class CostAdjustment {
sa.getHostCard().clearDelved(); sa.getHostCard().clearDelved();
final CardZoneTable table = new CardZoneTable(); final CardZoneTable table = new CardZoneTable();
final CardCollection mutableGrave = new CardCollection(activator.getCardsIn(ZoneType.Graveyard)); final Player pc = sa.getActivatingPlayer();
final CardCollectionView toExile = activator.getController().chooseCardsToDelve(cost.getUnpaidShards(ManaCostShard.GENERIC), mutableGrave); final CardCollection mutableGrave = new CardCollection(pc.getCardsIn(ZoneType.Graveyard));
final CardCollectionView toExile = pc.getController().chooseCardsToDelve(cost.getUnpaidShards(ManaCostShard.GENERIC), mutableGrave);
for (final Card c : toExile) { for (final Card c : toExile) {
cost.decreaseGenericMana(1); cost.decreaseGenericMana(1);
if (cardsToDelveOut != null) { if (cardsToDelveOut != null) {
@@ -280,18 +284,18 @@ public class CostAdjustment {
table.triggerChangesZoneAll(game, sa); table.triggerChangesZoneAll(game, sa);
} }
if (sa.getHostCard().hasKeyword(Keyword.CONVOKE)) { if (sa.getHostCard().hasKeyword(Keyword.CONVOKE)) {
adjustCostByConvokeOrImprovise(cost, sa, activator, false, true, test); adjustCostByConvokeOrImprovise(cost, sa, false, true, test);
} }
if (sa.getHostCard().hasKeyword(Keyword.IMPROVISE)) { if (sa.getHostCard().hasKeyword(Keyword.IMPROVISE)) {
adjustCostByConvokeOrImprovise(cost, sa, activator, true, false, test); adjustCostByConvokeOrImprovise(cost, sa, true, false, test);
} }
} // isSpell } // isSpell
if (sa.hasParam("TapCreaturesForMana")) { if (sa.hasParam("TapCreaturesForMana")) {
adjustCostByConvokeOrImprovise(cost, sa, activator, false, true, test); adjustCostByConvokeOrImprovise(cost, sa, false, true, test);
} }
adjustCostByWaterbend(cost, sa, payer, test); adjustCostByWaterbend(cost, sa, test);
// Reset card state (if changed) // Reset card state (if changed)
if (isStateChangeToFaceDown) { if (isStateChangeToFaceDown) {
@@ -303,6 +307,13 @@ public class CostAdjustment {
} }
// GetSpellCostChange // GetSpellCostChange
private static void adjustCostByWaterbend(ManaCostBeingPaid cost, SpellAbility sa, boolean test) {
Integer maxWaterbend = sa.getMaxWaterbend();
if (maxWaterbend != null && maxWaterbend > 0) {
adjustCostByConvokeOrImprovise(cost, sa, true, true, test);
}
}
private static boolean adjustCostByAssist(ManaCostBeingPaid cost, final SpellAbility sa, boolean test) { private static boolean adjustCostByAssist(ManaCostBeingPaid cost, final SpellAbility sa, boolean test) {
// 702.132a Assist is a static ability that modifies the rules of paying for the spell with assist (see rules 601.2g-h). // 702.132a Assist is a static ability that modifies the rules of paying for the spell with assist (see rules 601.2g-h).
// If the total cost to cast a spell with assist includes a generic mana component, before you activate mana abilities while casting it, you may choose another player. // If the total cost to cast a spell with assist includes a generic mana component, before you activate mana abilities while casting it, you may choose another player.
@@ -325,19 +336,13 @@ public class CostAdjustment {
return assistant.getController().helpPayForAssistSpell(cost, sa, genericLeft, requestedAmount); return assistant.getController().helpPayForAssistSpell(cost, sa, genericLeft, requestedAmount);
} }
private static void adjustCostByWaterbend(ManaCostBeingPaid cost, SpellAbility sa, Player payer, boolean test) { private static void adjustCostByConvokeOrImprovise(ManaCostBeingPaid cost, final SpellAbility sa, boolean artifacts, boolean creatures, boolean test) {
Integer maxWaterbend = sa.getMaxWaterbend();
if (maxWaterbend != null && maxWaterbend > 0) {
adjustCostByConvokeOrImprovise(cost, sa, payer, true, true, test);
}
}
private static void adjustCostByConvokeOrImprovise(ManaCostBeingPaid cost, final SpellAbility sa, final Player payer, boolean artifacts, boolean creatures, boolean test) {
if (creatures && !artifacts) { if (creatures && !artifacts) {
sa.clearTappedForConvoke(); sa.clearTappedForConvoke();
} }
CardCollectionView untappedCards = CardLists.filter(payer.getCardsIn(ZoneType.Battlefield), final Player activator = sa.getActivatingPlayer();
CardCollectionView untappedCards = CardLists.filter(activator.getCardsIn(ZoneType.Battlefield),
CardPredicates.CAN_TAP); CardPredicates.CAN_TAP);
Integer maxReduction = null; Integer maxReduction = null;
@@ -351,7 +356,7 @@ public class CostAdjustment {
untappedCards = CardLists.filter(untappedCards, CardPredicates.CREATURES); untappedCards = CardLists.filter(untappedCards, CardPredicates.CREATURES);
} }
Map<Card, ManaCostShard> convokedCards = payer.getController().chooseCardsForConvokeOrImprovise(sa, Map<Card, ManaCostShard> convokedCards = activator.getController().chooseCardsForConvokeOrImprovise(sa,
cost.toManaCost(), untappedCards, artifacts, creatures, maxReduction); cost.toManaCost(), untappedCards, artifacts, creatures, maxReduction);
CardCollection tapped = new CardCollection(); CardCollection tapped = new CardCollection();
@@ -362,13 +367,13 @@ public class CostAdjustment {
} }
cost.decreaseShard(conv.getValue(), 1); cost.decreaseShard(conv.getValue(), 1);
if (!test) { if (!test) {
if (c.tap(true, sa, payer)) tapped.add(c); if (c.tap(true, sa, activator)) tapped.add(c);
} }
} }
if (!tapped.isEmpty()) { if (!tapped.isEmpty()) {
final Map<AbilityKey, Object> runParams = AbilityKey.newMap(); final Map<AbilityKey, Object> runParams = AbilityKey.newMap();
runParams.put(AbilityKey.Cards, tapped); runParams.put(AbilityKey.Cards, tapped);
payer.getGame().getTriggerHandler().runTrigger(TriggerType.TapAll, runParams, false); activator.getGame().getTriggerHandler().runTrigger(TriggerType.TapAll, runParams, false);
} }
} }

View File

@@ -235,7 +235,6 @@ public class ReplacementHandler {
Map<AbilityKey, Object> params = AbilityKey.newMap(runParams); Map<AbilityKey, Object> params = AbilityKey.newMap(runParams);
params.remove(AbilityKey.ReplacementResult); params.remove(AbilityKey.ReplacementResult);
// CR 614.16
if (params.containsKey(AbilityKey.EffectOnly)) { if (params.containsKey(AbilityKey.EffectOnly)) {
params.put(AbilityKey.EffectOnly, true); params.put(AbilityKey.EffectOnly, true);
} }

View File

@@ -6,7 +6,7 @@ K:Flying
T:Mode$ ChangesZone | ValidCard$ Card.Self | Origin$ Any | Destination$ Battlefield | Execute$ TrigDig | TriggerDescription$ When NICKNAME enters, look at the top five cards of your library. You may put a creature card with mana value 4 or less from among them onto the battlefield. Put the rest on the bottom of your library in a random order. T:Mode$ ChangesZone | ValidCard$ Card.Self | Origin$ Any | Destination$ Battlefield | Execute$ TrigDig | TriggerDescription$ When NICKNAME enters, look at the top five cards of your library. You may put a creature card with mana value 4 or less from among them onto the battlefield. Put the rest on the bottom of your library in a random order.
SVar:TrigDig:DB$ Dig | DigNum$ 5 | ChangeNum$ 1 | ChangeValid$ Creature.cmcLE4 | Optional$ True | DestinationZone$ Battlefield | DestinationZone2$ Library | LibraryPosition$ -1 | RestRandomOrder$ True SVar:TrigDig:DB$ Dig | DigNum$ 5 | ChangeNum$ 1 | ChangeValid$ Creature.cmcLE4 | Optional$ True | DestinationZone$ Battlefield | DestinationZone2$ Library | LibraryPosition$ -1 | RestRandomOrder$ True
T:Mode$ ChangesZone | ValidCard$ Creature.Other+YouCtrl | Origin$ Battlefield | Destination$ Any | Execute$ TrigDelayTransform | TriggerZones$ Battlefield | TriggerDescription$ When another creature you control leaves the battlefield, transform NICKNAME at the beginning of the next upkeep. T:Mode$ ChangesZone | ValidCard$ Creature.Other+YouCtrl | Origin$ Battlefield | Destination$ Any | Execute$ TrigDelayTransform | TriggerZones$ Battlefield | TriggerDescription$ When another creature you control leaves the battlefield, transform NICKNAME at the beginning of the next upkeep.
SVar:TrigDelayTransform:DB$ DelayedTrigger | Mode$ Phase | Phase$ Upkeep | Execute$ TrigTransform | TriggerDescription$ CARDNAME — Transform him at the beginning of the next upkeep SVar:TrigDelayTransform:DB$ DelayedTrigger | Mode$ Phase | Phase$ Upkeep | Execute$ TrigTransform | TriggerDescription$ CARDNAME — Transform him at the beginning of the next end step.
SVar:TrigTransform:DB$ SetState | Defined$ Self | Mode$ Transform SVar:TrigTransform:DB$ SetState | Defined$ Self | Mode$ Transform
AlternateMode:DoubleFaced AlternateMode:DoubleFaced
Oracle:Flying\nWhen Aang enters, look at the top five cards of your library. You may put a creature card with mana value 4 or less from among them onto the battlefield. Put the rest on the bottom of your library in a random order.\nWhen another creature you control leaves the battlefield, transform Aang at the beginning of the next upkeep. Oracle:Flying\nWhen Aang enters, look at the top five cards of your library. You may put a creature card with mana value 4 or less from among them onto the battlefield. Put the rest on the bottom of your library in a random order.\nWhen another creature you control leaves the battlefield, transform Aang at the beginning of the next upkeep.

View File

@@ -3,7 +3,7 @@ ManaCost:2 B B
Types:Artifact Vehicle Types:Artifact Vehicle
PT:4/4 PT:4/4
K:Flying K:Flying
T:Mode$ Phase | Phase$ End of Turn | ValidPlayer$ You | TriggerZones$ Battlefield | CheckSVar$ X | SVarCompare$ GE1 | Execute$ TrigCopy | TriggerDescription$ At the beginning of your end step, if you sacrificed a permanent this turn, create a token that's a copy of this Vehicle. T:Mode$ Phase | Phase$ End of Turn | TriggerZones$ Battlefield | CheckSVar$ X | SVarCompare$ GE1 | Execute$ TrigCopy | TriggerDescription$ At the beginning of your end step, if you sacrificed a permanent this turn, create a token that's a copy of this Vehicle.
SVar:TrigCopy:DB$ CopyPermanent | Defined$ Self | NumCopies$ 1 SVar:TrigCopy:DB$ CopyPermanent | Defined$ Self | NumCopies$ 1
S:Mode$ Continuous | Affected$ Card.Self | IsPresent$ Permanent.YouCtrl+namedPhoenix Fleet Airship | PresentCompare$ GE8 | AddType$ Artifact & Creature | Description$ As long as you control eight or more permanents named Phoenix Fleet Airship, this Vehicle is an artifact creature. S:Mode$ Continuous | Affected$ Card.Self | IsPresent$ Permanent.YouCtrl+namedPhoenix Fleet Airship | PresentCompare$ GE8 | AddType$ Artifact & Creature | Description$ As long as you control eight or more permanents named Phoenix Fleet Airship, this Vehicle is an artifact creature.
K:Crew:1 K:Crew:1

View File

@@ -3,7 +3,7 @@ ManaCost:3 R
Types:Sorcery Types:Sorcery
A:SP$ Charm | MinCharmNum$ 1 | CharmNum$ Count$Compare Y GE1.2.1 | Choices$ DBWheel,DBFlames | AdditionalDescription$ . If you control a commander as you cast this spell, you may choose both instead. A:SP$ Charm | MinCharmNum$ 1 | CharmNum$ Count$Compare Y GE1.2.1 | Choices$ DBWheel,DBFlames | AdditionalDescription$ . If you control a commander as you cast this spell, you may choose both instead.
SVar:DBWheel:DB$ Discard | Mode$ Hand | Defined$ Player | Optional$ True | RememberDiscardingPlayers$ True | SubAbility$ DBDraw | SpellDescription$ If embark gets more votes or the vote is tied, each player may discard their hand and draw seven cards. SVar:DBWheel:DB$ Discard | Mode$ Hand | Defined$ Player | Optional$ True | RememberDiscardingPlayers$ True | SubAbility$ DBDraw | SpellDescription$ If embark gets more votes or the vote is tied, each player may discard their hand and draw seven cards.
SVar:DBDraw:DB$ Draw | Defined$ Remembered | NumCards$ 5 | SubAbility$ DBCleanup SVar:DBDraw:DB$ Draw | Defined$ Remembered | NumCards$ 7 | SubAbility$ DBCleanup
SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True
SVar:DBFlames:DB$ PumpAll | ValidCards$ Instant.YouCtrl,Sorcery.YouCtrl | KW$ Flashback | PumpZone$ Graveyard | SpellDescription$ Each instant and sorcery card in your graveyard gains flashback until end of turn. The flashback cost is equal to its mana cost. SVar:DBFlames:DB$ PumpAll | ValidCards$ Instant.YouCtrl,Sorcery.YouCtrl | KW$ Flashback | PumpZone$ Graveyard | SpellDescription$ Each instant and sorcery card in your graveyard gains flashback until end of turn. The flashback cost is equal to its mana cost.
SVar:Y:Count$Valid Card.IsCommander+YouCtrl SVar:Y:Count$Valid Card.IsCommander+YouCtrl

View File

@@ -560,7 +560,7 @@ public class HumanPlay {
} }
CardCollection cardsToDelve = new CardCollection(); CardCollection cardsToDelve = new CardCollection();
CostAdjustment.adjust(toPay, ability, activator, cardsToDelve, false, effect); CostAdjustment.adjust(toPay, ability, cardsToDelve, false, effect);
Card offering = null; Card offering = null;
Card emerge = null; Card emerge = null;