Codechanges and lots of fixes related to playing other player's cards from exile.

- Cards now keep track of who's allowed to cast them, fixing possible issues in multiplayer.
- Face-down cards can now be properly looked at when allowed to by a static ability.
- Improve support for casting a face-down exiled card in general.
- Add Shared Fate (including AI support).
This commit is contained in:
elcnesh
2014-12-03 08:49:49 +00:00
parent 4ae2687478
commit ea23bb33b2
28 changed files with 251 additions and 93 deletions

View File

@@ -29,6 +29,7 @@ import forge.game.ability.ApiType;
import forge.game.card.Card;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPlayOption;
import forge.game.card.CardPredicates;
import forge.game.cost.Cost;
import forge.game.mana.ManaCostBeingPaid;
@@ -143,20 +144,37 @@ public final class GameActionUtil {
/**
* <p>
* getAlternativeCosts.
* Find the alternative costs to a {@link SpellAbility}.
* </p>
*
* @param sa
* a SpellAbility.
* @return an ArrayList<SpellAbility>.
* get alternative costs as additional spell abilities
* a {@link SpellAbility}.
* @param activator
* the {@link Player} for which to calculate available
* @return a {@link List} of {@link SpellAbility} objects, each representing
* a possible alternative cost the provided activator can use to pay
* the provided {@link SpellAbility}.
*/
public static final ArrayList<SpellAbility> getAlternativeCosts(SpellAbility sa) {
ArrayList<SpellAbility> alternatives = new ArrayList<SpellAbility>();
Card source = sa.getHostCard();
public static final List<SpellAbility> getAlternativeCosts(final SpellAbility sa, final Player activator) {
final List<SpellAbility> alternatives = new ArrayList<SpellAbility>();
if (!sa.isBasicSpell()) {
return alternatives;
}
final Card source = sa.getHostCard();
final CardPlayOption playOption = source.mayPlay(activator);
if (sa.isSpell() && playOption != null && playOption.isWithoutManaCost()) {
final SpellAbility newSA = sa.copy();
final SpellAbilityRestriction sar = new SpellAbilityRestriction();
sar.setVariables(sa.getRestrictions());
sar.setZone(null);
newSA.setRestrictions(sar);
newSA.setBasicSpell(false);
newSA.setPayCosts(newSA.getPayCosts().copyWithNoMana());
newSA.setDescription(sa.getDescription() + " (without paying its mana cost)");
alternatives.add(newSA);
}
for (final String keyword : source.getKeywords()) {
if (sa.isSpell() && keyword.startsWith("Flashback")) {
final SpellAbility flashback = sa.copy();
@@ -183,18 +201,6 @@ public final class GameActionUtil {
newSA.setDescription(sa.getDescription() + " (without paying its mana cost)");
alternatives.add(newSA);
}
if (sa.isSpell() && keyword.equals("May be played by your opponent without paying its mana cost")) {
final SpellAbility newSA = sa.copy();
SpellAbilityRestriction sar = new SpellAbilityRestriction();
sar.setVariables(sa.getRestrictions());
sar.setZone(null);
sar.setOpponentOnly(true);
newSA.setRestrictions(sar);
newSA.setBasicSpell(false);
newSA.setPayCosts(newSA.getPayCosts().copyWithNoMana());
newSA.setDescription(sa.getDescription() + " (without paying its mana cost)");
alternatives.add(newSA);
}
if (sa.isSpell() && keyword.startsWith("May be played without paying its mana cost and as though it has flash")) {
final SpellAbility newSA = sa.copy();
SpellAbilityRestriction sar = new SpellAbilityRestriction();

View File

@@ -92,7 +92,7 @@ public class StaticEffects {
boolean setPT = false;
String[] addHiddenKeywords = null;
String addColors = null;
boolean removeMayLookAt = false;
boolean removeMayLookAt = false, removeMayPlay = false;
if (params.containsKey("ChangeColorWordsTo")) {
changeColorWordsTo = params.get("ChangeColorWordsTo");
@@ -158,6 +158,9 @@ public class StaticEffects {
if (params.containsKey("MayLookAt")) {
removeMayLookAt = true;
}
if (params.containsKey("MayPlay")) {
removeMayPlay = true;
}
if (params.containsKey("IgnoreEffectCost")) {
for (final SpellAbility s : se.getSource().getSpellAbilities()) {
@@ -254,6 +257,9 @@ public class StaticEffects {
if (removeMayLookAt) {
affectedCard.setMayLookAt(controller, false);
}
if (removeMayPlay) {
affectedCard.removeMayPlay(controller);
}
}
se.clearTimestamps();
return affectedCards;

View File

@@ -112,8 +112,6 @@ public class EffectEffect extends SpellAbilityEffect {
eff.setImmutable(true);
eff.setEffectSource(hostCard);
// Effects should be Orange or something probably
final Card e = eff;
// Grant SVars first in order to give references to granted abilities

View File

@@ -136,6 +136,8 @@ public class Card extends GameEntity implements Comparable<Card>, IIdentifiable
// if this card is an Aura, what Entity is it enchanting?
private GameEntity enchanting = null;
private final Map<Player, CardPlayOption> mayPlay = Maps.newTreeMap();
// changes by AF animate and continuous static effects - timestamp is the key of maps
private Map<Long, CardChangedType> changedCardTypes = new ConcurrentSkipListMap<Long, CardChangedType>();
private Map<Long, KeywordsChange> changedCardKeywords = new ConcurrentSkipListMap<Long, KeywordsChange>();
@@ -1503,10 +1505,22 @@ public class Card extends GameEntity implements Comparable<Card>, IIdentifiable
public String getAbilityText() {
return getAbilityText(currentState);
}
public String getAbilityText(CardState state) {
public String getAbilityText(final CardState state) {
final CardTypeView type = state.getType();
final StringBuilder sb = new StringBuilder();
if (!mayPlay.isEmpty()) {
sb.append("May be played by: ");
sb.append(Lang.joinHomogenous(mayPlay.entrySet(), new Function<Entry<Player, CardPlayOption>, String>() {
@Override public String apply(final Entry<Player, CardPlayOption> entry) {
return entry.getKey().toString() + entry.getValue().toString();
}
}));
sb.append("\r\n");
}
if (type.isInstant() || type.isSorcery()) {
final StringBuilder sb = abilityTextInstantSorcery(state);
sb.append(abilityTextInstantSorcery(state));
if (haunting != null) {
sb.append("Haunting: ").append(haunting);
@@ -1520,8 +1534,6 @@ public class Card extends GameEntity implements Comparable<Card>, IIdentifiable
return sb.toString().replaceAll("CARDNAME", state.getName());
}
final StringBuilder sb = new StringBuilder();
if (monstrous) {
sb.append("Monstrous\r\n");
}
@@ -2141,6 +2153,17 @@ public class Card extends GameEntity implements Comparable<Card>, IIdentifiable
view.setPlayerMayLook(player, mayLookAt, temp);
}
public final CardPlayOption mayPlay(final Player player) {
return mayPlay.get(player);
}
public final void setMayPlay(final Player player, final boolean withoutManaCost, final boolean ignoreColor) {
final CardPlayOption option = this.mayPlay.get(player);
this.mayPlay.put(player, option == null ? new CardPlayOption(withoutManaCost, ignoreColor) : option.add(withoutManaCost, ignoreColor));
}
public final void removeMayPlay(final Player player) {
this.mayPlay.remove(player);
}
public final CardCollectionView getEquippedBy(boolean allowModify) {
return CardCollection.getView(equippedBy, allowModify);
}
@@ -6246,13 +6269,18 @@ public class Card extends GameEntity implements Comparable<Card>, IIdentifiable
return null;
}
public List<SpellAbility> getAllPossibleAbilities(Player player, boolean removeUnplayable) {
public List<SpellAbility> getAllPossibleAbilities(final Player player, final boolean removeUnplayable) {
// this can only be called by the Human
final List<SpellAbility> abilities = new ArrayList<SpellAbility>();
for (SpellAbility sa : getSpellAbilities()) {
//add alternative costs as additional spell abilities
abilities.add(sa);
abilities.addAll(GameActionUtil.getAlternativeCosts(sa));
abilities.addAll(GameActionUtil.getAlternativeCosts(sa, player));
}
if (isFaceDown() && isInZone(ZoneType.Exile) && mayPlay(player) != null) {
for (final SpellAbility sa : getState(CardStateName.Original).getSpellAbilities()) {
abilities.add(sa);
}
}
for (int i = abilities.size() - 1; i >= 0; i--) {
@@ -6266,7 +6294,7 @@ public class Card extends GameEntity implements Comparable<Card>, IIdentifiable
}
}
if (isLand() && player.canPlayLand(this)) {
if (getState(CardStateName.Original).getType().isLand() && player.canPlayLand(this)) {
Ability.PLAY_LAND_SURROGATE.setHostCard(this);
abilities.add(Ability.PLAY_LAND_SURROGATE);
}

View File

@@ -0,0 +1,36 @@
package forge.game.card;
import org.apache.commons.lang3.StringUtils;
public final class CardPlayOption {
private final boolean withoutManaCost, ignoreManaCostColor;
public CardPlayOption(final boolean withoutManaCost, final boolean ignoreManaCostColor) {
this.withoutManaCost = withoutManaCost;
this.ignoreManaCostColor = ignoreManaCostColor;
}
public CardPlayOption add(final boolean withoutManaCost, final boolean ignoreManaCostColor) {
return new CardPlayOption(isWithoutManaCost() || withoutManaCost, isIgnoreManaCostColor() || ignoreManaCostColor);
}
public boolean isWithoutManaCost() {
return withoutManaCost;
}
public boolean isIgnoreManaCostColor() {
return ignoreManaCostColor;
}
@Override
public String toString() {
if (isWithoutManaCost()) {
return " (without paying its mana cost)";
}
if (isIgnoreManaCostColor()) {
return " (may spend mana as though it were mana of any color to cast it)";
}
return StringUtils.EMPTY;
}
}

View File

@@ -1,9 +1,12 @@
package forge.game.card;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.google.common.collect.Iterables;
import forge.ImageKeys;
import forge.card.CardStateName;
import forge.card.CardEdition;
@@ -102,6 +105,9 @@ public class CardView extends GameEntityView {
void updateZone(Card c) {
set(TrackableProperty.Zone, c.getZone() == null ? null : c.getZone().getZoneType());
}
public boolean isInZone(final Iterable<ZoneType> zones) {
return Iterables.contains(zones, getZone());
}
public boolean isCloned() {
return get(TrackableProperty.Cloned);
@@ -397,11 +403,17 @@ public class CardView extends GameEntityView {
}
//if viewer is controlled by another player, also check if face can be shown to that player
PlayerView mindSlaveMaster = viewer.getMindSlaveMaster();
final PlayerView mindSlaveMaster = viewer.getMindSlaveMaster();
if (mindSlaveMaster != null && canFaceDownBeShownTo(mindSlaveMaster)) {
return true;
}
return !getController().isOpponentOf(viewer) || getCurrentState().getOpponentMayLook();
if (isInZone(EnumSet.of(ZoneType.Battlefield, ZoneType.Stack, ZoneType.Sideboard)) && getController().equals(viewer)) {
return true;
}
if (getController().isOpponentOf(viewer) && getCurrentState().getOpponentMayLook()) {
return true;
}
return false;
}
public CardView getEquipping() {

View File

@@ -1386,6 +1386,9 @@ public class Player extends GameEntity implements Comparable<Player> {
// Dakkon Blackblade Avatar will use a similar effect
if (canPlayLand(land, ignoreZoneAndTiming)) {
land.setController(this, 0);
if (land.isFaceDown()) {
land.turnFaceUp();
}
game.getAction().moveTo(getZone(ZoneType.Battlefield), land);
// play a sound
@@ -1407,7 +1410,7 @@ public class Player extends GameEntity implements Comparable<Player> {
public final boolean canPlayLand(final Card land) {
return canPlayLand(land, false);
}
public final boolean canPlayLand(Card land, final boolean ignoreZoneAndTiming) {
public final boolean canPlayLand(final Card land, final boolean ignoreZoneAndTiming) {
if (!ignoreZoneAndTiming && !canCastSorcery()) {
return false;
}
@@ -1423,14 +1426,13 @@ public class Player extends GameEntity implements Comparable<Player> {
}
if (land != null && !ignoreZoneAndTiming) {
if (land.getOwner() != this && !land.hasKeyword("May be played by your opponent"))
return false;
if (land.getOwner() == this && land.hasKeyword("May be played by your opponent") && !land.hasKeyword("May be played"))
final boolean mayPlay = land.mayPlay(this) != null;
if (land.getOwner() != this && !mayPlay) {
return false;
}
final Zone zone = game.getZoneOf(land);
if (zone != null && (zone.is(ZoneType.Battlefield) || (!zone.is(ZoneType.Hand) && !land.hasStartOfKeyword("May be played")))) {
if (zone != null && (zone.is(ZoneType.Battlefield) || (!zone.is(ZoneType.Hand) && !(mayPlay || land.hasStartOfKeyword("May be played"))))) {
return false;
}
}

View File

@@ -17,6 +17,7 @@
*/
package forge.game.spellability;
import forge.card.CardStateName;
import forge.game.Game;
import forge.game.card.Card;
import forge.game.card.CardCollection;
@@ -91,7 +92,7 @@ public abstract class Spell extends SpellAbility implements java.io.Serializable
}
// for uncastables like lotus bloom, check if manaCost is blank (except for morph spells)
if (!isCastFaceDown() && isBasicSpell() && card.getManaCost().isNoCost()) {
if (!isCastFaceDown() && isBasicSpell() && card.getState(card.isFaceDown() ? CardStateName.Original : card.getCurrentStateName()).getManaCost().isNoCost()) {
return false;
}

View File

@@ -200,9 +200,8 @@ public class SpellAbilityRestriction extends SpellAbilityVariables {
return true;
}
Player activator = sa.getActivatingPlayer();
Zone cardZone = activator.getGame().getZoneOf(c);
final Player activator = sa.getActivatingPlayer();
final Zone cardZone = activator.getGame().getZoneOf(c);
if (cardZone == null || !cardZone.is(this.getZone())) {
// If Card is not in the default activating zone, do some additional checks
// Not a Spell, or on Battlefield, return false
@@ -211,12 +210,12 @@ public class SpellAbilityRestriction extends SpellAbilityVariables {
return false;
}
if (cardZone.is(ZoneType.Stack)) {
return false;
return false;
}
if (c.hasKeyword("May be played") && activator.equals(c.getController())) {
if (c.mayPlay(activator) != null) {
return true;
}
if (c.hasKeyword("May be played by your opponent") && !activator.equals(c.getController())) {
if (c.hasKeyword("May be played") && activator.equals(c.getController())) {
return true;
}
return false;
@@ -296,7 +295,7 @@ public class SpellAbilityRestriction extends SpellAbilityVariables {
return true;
}
if (sa.isSpell() && activator.isOpponentOf(c.getController()) && c.hasKeyword("May be played by your opponent")) {
if (sa.isSpell() && c.mayPlay(activator) != null) {
return true;
}

View File

@@ -257,9 +257,8 @@ public class StaticAbility extends CardTraitBase {
return in;
}
// apply the ability if it has the right mode
/**
* Apply ability.
* Apply ability if it has the right mode.
*
* @param mode
* the mode

View File

@@ -113,6 +113,7 @@ public class StaticAbilityContinuous {
boolean removeSubTypes = false;
boolean removeCreatureTypes = false;
boolean controllerMayLookAt = false;
boolean controllerMayPlay = false, mayPlayWithoutManaCost = false, mayPlayIgnoreColor = false;
//Global rules changes
if (params.containsKey("GlobalRule")) {
@@ -328,6 +329,14 @@ public class StaticAbilityContinuous {
if (params.containsKey("MayLookAt")) {
controllerMayLookAt = true;
}
if (params.containsKey("MayPlay")) {
controllerMayPlay = true;
if (params.containsKey("MayPlayWithoutManaCost")) {
mayPlayWithoutManaCost = true;
} else if (params.containsKey("MayPlayIgnoreColor")) {
mayPlayIgnoreColor = true;
}
}
if (params.containsKey("IgnoreEffectCost")) {
String cost = params.get("IgnoreEffectCost");
@@ -558,8 +567,11 @@ public class StaticAbilityContinuous {
if (controllerMayLookAt) {
affectedCard.setMayLookAt(controller, true);
}
if (controllerMayPlay) {
affectedCard.setMayPlay(controller, mayPlayWithoutManaCost, mayPlayIgnoreColor);
}
}
return affectedCards;
}

View File

@@ -39,14 +39,17 @@ import forge.game.zone.ZoneType;
import java.util.*;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimaps;
public class TriggerHandler {
private final ArrayList<TriggerType> suppressedModes = new ArrayList<TriggerType>();
private final ArrayList<Trigger> activeTriggers = new ArrayList<Trigger>();
private final List<TriggerType> suppressedModes = Collections.synchronizedList(new ArrayList<TriggerType>());
private final List<Trigger> activeTriggers = Collections.synchronizedList(new ArrayList<Trigger>());
private final ArrayList<Trigger> delayedTriggers = new ArrayList<Trigger>();
private final ArrayListMultimap<Player, Trigger> playerDefinedDelayedTriggers = ArrayListMultimap.create();
private final List<TriggerWaiting> waitingTriggers = new ArrayList<TriggerWaiting>();
private final List<Trigger> delayedTriggers = Collections.synchronizedList(new ArrayList<Trigger>());
private final ListMultimap<Player, Trigger> playerDefinedDelayedTriggers = Multimaps.synchronizedListMultimap(ArrayListMultimap.<Player, Trigger>create());
private final List<TriggerWaiting> waitingTriggers = Collections.synchronizedList(new ArrayList<TriggerWaiting>());
private final Game game;
public TriggerHandler(Game gameState) {
@@ -268,7 +271,7 @@ public class TriggerHandler {
boolean checkStatics = false;
// Static triggers
for (final Trigger t : activeTriggers) {
for (final Trigger t : Lists.newArrayList(activeTriggers)) {
if (t.isStatic() && canRunTrigger(t, mode, runParams)) {
runSingleTrigger(t, runParams);
checkStatics = true;

View File

@@ -216,9 +216,11 @@ public class MagicStack /* extends MyObservable */ implements Iterable<SpellAbil
if (sp.isSpell()) {
source.setController(activator, 0);
Spell spell = (Spell) sp;
final Spell spell = (Spell) sp;
if (spell.isCastFaceDown()) {
source.turnFaceDown();
} else if (source.isFaceDown()) {
source.turnFaceUp();
}
}

View File

@@ -38,8 +38,8 @@ public enum TrackableProperty {
ChosenDirection(TrackableTypes.EnumType(Direction.class)),
Remembered(TrackableTypes.StringType),
NamedCard(TrackableTypes.StringType),
PlayerMayLook(TrackableTypes.PlayerViewCollectionType),
PlayerMayLookTemp(TrackableTypes.PlayerViewCollectionType),
PlayerMayLook(TrackableTypes.PlayerViewCollectionType, false),
PlayerMayLookTemp(TrackableTypes.PlayerViewCollectionType, false),
Equipping(TrackableTypes.CardViewType),
EquippedBy(TrackableTypes.CardViewCollectionType),
Enchanting(TrackableTypes.GameEntityViewType),