Room: First Spell Part (#6044)

* Room: First Spell 

----

Co-authored-by: tool4ever <therealtoolkit@hotmail.com>
Co-authored-by: tool4EvEr <tool4EvEr@192.168.0.59>
Co-authored-by: TRT <>
Co-authored-by: Anthony Calosa <anthonycalosa@gmail.com>
This commit is contained in:
Hans Mackowiak
2024-10-06 20:11:28 +02:00
committed by GitHub
parent 88006ebe85
commit 80b30d9a6a
30 changed files with 527 additions and 34 deletions

View File

@@ -1530,6 +1530,24 @@ public class PlayerControllerAi extends PlayerController {
return SpellApiToAi.Converter.get(api).chooseCardName(player, sa, faces);
}
@Override
public ICardFace chooseSingleCardFace(SpellAbility sa, List<ICardFace> faces, String message) {
ApiType api = sa.getApi();
if (null == api) {
throw new InvalidParameterException("SA is not api-based, this is not supported yet");
}
return SpellApiToAi.Converter.get(api).chooseCardFace(player, sa, faces);
}
@Override
public CardState chooseSingleCardState(SpellAbility sa, List<CardState> states, String message, Map<String, Object> params) {
ApiType api = sa.getApi();
if (null == api) {
throw new InvalidParameterException("SA is not api-based, this is not supported yet");
}
return SpellApiToAi.Converter.get(api).chooseCardState(player, sa, states, params);
}
@Override
public Card chooseDungeon(Player ai, List<PaperCard> dungeonCards, String message) {
// TODO: improve the conditions that define which dungeon is a viable option to choose

View File

@@ -8,6 +8,7 @@ import forge.card.mana.ManaCost;
import forge.card.mana.ManaCostParser;
import forge.game.GameEntity;
import forge.game.card.Card;
import forge.game.card.CardState;
import forge.game.card.CounterType;
import forge.game.cost.Cost;
import forge.game.mana.ManaCostBeingPaid;
@@ -365,6 +366,18 @@ public abstract class SpellAbilityAi {
return face == null ? "" : face.getName();
}
public ICardFace chooseCardFace(Player ai, SpellAbility sa, List<ICardFace> faces) {
System.err.println("Warning: default (ie. inherited from base class) implementation of chooseCardFace is used for " + this.getClass().getName() + ". Consider declaring an overloaded method");
return Iterables.getFirst(faces, null);
}
public CardState chooseCardState(Player ai, SpellAbility sa, List<CardState> faces, Map<String, Object> params) {
System.err.println("Warning: default (ie. inherited from base class) implementation of chooseCardState is used for " + this.getClass().getName() + ". Consider declaring an overloaded method");
return Iterables.getFirst(faces, null);
}
public int chooseNumber(Player player, SpellAbility sa, int min, int max, Map<String, Object> params) {
return max;
}

View File

@@ -190,6 +190,7 @@ public enum SpellApiToAi {
.put(ApiType.TwoPiles, TwoPilesAi.class)
.put(ApiType.Unattach, CannotPlayAi.class)
.put(ApiType.UnattachAll, UnattachAllAi.class)
.put(ApiType.UnlockDoor, AlwaysPlayAi.class)
.put(ApiType.Untap, UntapAi.class)
.put(ApiType.UntapAll, UntapAllAi.class)
.put(ApiType.Venture, VentureAi.class)

View File

@@ -12,6 +12,7 @@ public enum CardStateName {
RightSplit,
Adventure,
Modal,
EmptyRoom,
SpecializeW,
SpecializeU,
SpecializeB,

View File

@@ -190,7 +190,12 @@ public class GameAction {
// Make sure the card returns from the battlefield as the original card with two halves
resetToOriginal = true;
}
} else if (!zoneTo.is(ZoneType.Stack)) {
} else if (zoneTo.is(ZoneType.Battlefield) && c.isRoom()) {
if (c.getCastSA() == null) {
// need to set as empty room
c.updateRooms();
}
} else if (!zoneTo.is(ZoneType.Stack) && !zoneTo.is(ZoneType.Battlefield)) {
// For regular splits, recreate the original state unless the card is going to stack as one half
resetToOriginal = true;
}
@@ -604,6 +609,9 @@ public class GameAction {
// CR 603.6b
if (toBattlefield) {
zoneTo.saveLKI(copied, lastKnownInfo);
if (copied.isRoom() && copied.getCastSA() != null) {
copied.unlockRoom(copied.getCastSA().getActivatingPlayer(), copied.getCastSA().getCardStateName());
}
}
// only now that the LKI preserved it

View File

@@ -30,6 +30,7 @@ public enum AbilityKey {
Blockers("Blockers"),
CanReveal("CanReveal"),
Card("Card"),
CardState("CardState"),
Cards("Cards"),
CardsFiltered("CardsFiltered"),
CardLKI("CardLKI"),

View File

@@ -194,6 +194,7 @@ public enum ApiType {
TwoPiles (TwoPilesEffect.class),
Unattach (UnattachEffect.class),
UnattachAll (UnattachAllEffect.class),
UnlockDoor (UnlockDoorEffect.class),
Untap (UntapEffect.class),
UntapAll (UntapAllEffect.class),
Venture (VentureEffect.class),

View File

@@ -144,6 +144,7 @@ public class CloneEffect extends SpellAbilityEffect {
final long ts = game.getNextTimestamp();
tgtCard.addCloneState(CardFactory.getCloneStates(cardToCopy, tgtCard, sa), ts);
tgtCard.updateRooms();
// set ETB tapped of clone
if (sa.hasParam("IntoPlayTapped")) {

View File

@@ -0,0 +1,106 @@
package forge.game.ability.effects;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import forge.card.CardStateName;
import forge.game.Game;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.card.CardState;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import forge.util.Localizer;
public class UnlockDoorEffect extends SpellAbilityEffect {
@Override
public void resolve(SpellAbility sa) {
final Card source = sa.getHostCard();
final Game game = source.getGame();
final Player activator = sa.getActivatingPlayer();
CardCollection list;
if (sa.hasParam("Choices")) {
Player chooser = activator;
String title = sa.hasParam("ChoiceTitle") ? sa.getParam("ChoiceTitle") : Localizer.getInstance().getMessage("lblChoose") + " ";
CardCollection choices = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), sa.getParam("Choices"), activator, source, sa);
Card c = chooser.getController().chooseSingleEntityForEffect(choices, sa, title, Maps.newHashMap());
if (c == null) {
return;
}
list = new CardCollection(c);
} else {
list = getTargetCards(sa);
}
for (Card c : list) {
Map<String, Object> params = Maps.newHashMap();
params.put("Object", c);
switch (sa.getParamOrDefault("Mode", "ThisDoor")) {
case "ThisDoor":
c.unlockRoom(activator, sa.getCardStateName());
break;
case "Unlock":
List<CardState> states = c.getLockedRooms().stream().map(stateName -> c.getState(stateName)).collect(Collectors.toList());
// need to choose Room Name
CardState chosen = activator.getController().chooseSingleCardState(sa, states, "Choose Room to unlock", params);
if (chosen == null) {
continue;
}
c.unlockRoom(activator, chosen.getStateName());
break;
case "LockOrUnlock":
switch (c.getLockedRooms().size()) {
case 0:
// no locked, all unlocked, can only lock door
List<CardState> unlockStates = c.getUnlockedRooms().stream().map(stateName -> c.getState(stateName)).collect(Collectors.toList());
CardState chosenUnlock = activator.getController().chooseSingleCardState(sa, unlockStates, "Choose Room to lock", params);
if (chosenUnlock == null) {
continue;
}
c.lockRoom(activator, chosenUnlock.getStateName());
break;
case 1:
// TODO check for Lock vs Unlock first?
List<CardState> bothStates = Lists.newArrayList();
bothStates.add(c.getState(CardStateName.LeftSplit));
bothStates.add(c.getState(CardStateName.RightSplit));
CardState chosenBoth = activator.getController().chooseSingleCardState(sa, bothStates, "Choose Room to lock or unlock", params);
if (chosenBoth == null) {
continue;
}
if (c.getLockedRooms().contains(chosenBoth.getStateName())) {
c.unlockRoom(activator, chosenBoth.getStateName());
} else {
c.lockRoom(activator, chosenBoth.getStateName());
}
break;
case 2:
List<CardState> lockStates = c.getLockedRooms().stream().map(stateName -> c.getState(stateName)).collect(Collectors.toList());
// need to choose Room Name
CardState chosenLock = activator.getController().chooseSingleCardState(sa, lockStates, "Choose Room to unlock", params);
if (chosenLock == null) {
continue;
}
c.unlockRoom(activator, chosenLock.getStateName());
break;
}
break;
}
}
}
}

View File

@@ -215,6 +215,9 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
private boolean plotted;
private Set<CardStateName> unlockedRooms = EnumSet.noneOf(CardStateName.class);
private Map<CardStateName, SpellAbility> unlockAbilities = Maps.newEnumMap(CardStateName.class);
private boolean specialized;
private int timesCrewedThisTurn = 0;
@@ -444,6 +447,9 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
if (state == CardStateName.FaceDown) {
return getFaceDownState();
}
if (state == CardStateName.EmptyRoom) {
return getEmptyRoomState();
}
CardCloneStates clStates = getLastClonedState();
if (clStates == null) {
return getOriginalState(state);
@@ -452,7 +458,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
}
public boolean hasState(final CardStateName state) {
if (state == CardStateName.FaceDown) {
if (state == CardStateName.FaceDown || state == CardStateName.EmptyRoom) {
return true;
}
CardCloneStates clStates = getLastClonedState();
@@ -466,6 +472,9 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
if (state == CardStateName.FaceDown) {
return getFaceDownState();
}
if (state == CardStateName.EmptyRoom) {
return getEmptyRoomState();
}
return states.get(state);
}
@@ -492,7 +501,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
boolean needsTransformAnimation = transform || rollback;
// faceDown has higher priority over clone states
// while text change states doesn't apply while the card is faceDown
if (state != CardStateName.FaceDown) {
if (state != CardStateName.FaceDown && state != CardStateName.EmptyRoom) {
CardCloneStates cloneStates = getLastClonedState();
if (cloneStates != null) {
if (!cloneStates.containsKey(state)) {
@@ -934,6 +943,10 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
return alt ? StaticData.instance().getCommonCards().getName(name, true) : name;
}
public final boolean hasNameOverwrite() {
return changedCardNames.values().stream().anyMatch(CardChangedName::isOverwrite);
}
public final boolean hasNonLegendaryCreatureNames() {
boolean result = false;
for (CardChangedName change : this.changedCardNames.values()) {
@@ -1045,7 +1058,12 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
}
public final boolean isSplitCard() {
return getRules() != null && getRules().getSplitType() == CardSplitType.Split;
// Normal Split Cards, these need to return true before Split States are added
if (getRules() != null && getRules().getSplitType() == CardSplitType.Split) {
return true;
};
// in case or clones or copies
return hasState(CardStateName.LeftSplit);
}
public final boolean isAdventureCard() {
@@ -1970,7 +1988,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
public final Integer getChosenNumber() {
return chosenNumber;
}
public final void setChosenNumber(final int i) { setChosenNumber(i, false); }
public final void setChosenNumber(final int i, final boolean secret) {
chosenNumber = i;
@@ -3126,7 +3144,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
} else if (keyword.startsWith("Entwine") || keyword.startsWith("Madness")
|| keyword.startsWith("Miracle") || keyword.startsWith("Recover")
|| keyword.startsWith("Escape") || keyword.startsWith("Foretell:")
|| keyword.startsWith("Disturb") || keyword.startsWith("Overload")
|| keyword.startsWith("Disturb") || keyword.startsWith("Overload")
|| keyword.startsWith("Plot")) {
final String[] k = keyword.split(":");
final Cost cost = new Cost(k[1], false);
@@ -5575,6 +5593,8 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
public final boolean isOutlaw() { return getType().isOutlaw(); }
public final boolean isRoom() { return getType().hasSubtype("Room"); }
/** {@inheritDoc} */
@Override
public final int compareTo(final Card that) {
@@ -5985,9 +6005,21 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
boolean shares = getName(true).equals(name);
// Split cards has extra logic to check if it does share a name with
if (isSplitCard()) {
shares |= name.equals(getState(CardStateName.LeftSplit).getName());
shares |= name.equals(getState(CardStateName.RightSplit).getName());
if (!shares && !hasNameOverwrite()) {
if (isInPlay()) {
// split cards in play are only rooms
for (String door : getUnlockedRoomNames()) {
shares |= name.equals(door);
}
} else { // not on the battlefield
if (hasState(CardStateName.LeftSplit)) {
shares |= name.equals(getState(CardStateName.LeftSplit).getName());
}
if (hasState(CardStateName.RightSplit)) {
shares |= name.equals(getState(CardStateName.RightSplit).getName());
}
}
// TODO does it need extra check for stack?
}
if (!shares && hasNonLegendaryCreatureNames()) {
@@ -6635,7 +6667,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
if (plotted == true && !isLKI()) {
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromCard(this);
game.getTriggerHandler().runTrigger(TriggerType.BecomesPlotted, runParams, false);
}
}
return true;
}
@@ -7474,6 +7506,15 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
}
}
if (isInPlay() && !isPhasedOut() && player.canCastSorcery()) {
if (getCurrentStateName() == CardStateName.RightSplit || getCurrentStateName() == CardStateName.EmptyRoom) {
abilities.add(getUnlockAbility(CardStateName.LeftSplit));
}
if (getCurrentStateName() == CardStateName.LeftSplit || getCurrentStateName() == CardStateName.EmptyRoom) {
abilities.add(getUnlockAbility(CardStateName.RightSplit));
}
}
if (isInPlay() && isFaceDown() && oState.getType().isCreature() && oState.getManaCost() != null && !oState.getManaCost().isNoCost())
{
if (isManifested()) {
@@ -8075,4 +8116,107 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
}
return StaticAbilityWitherDamage.isWitherDamage(this);
}
public Set<CardStateName> getUnlockedRooms() {
return this.unlockedRooms;
}
public void setUnlockedRooms(Set<CardStateName> set) {
this.unlockedRooms = set;
}
public List<String> getUnlockedRoomNames() {
List<String> result = Lists.newArrayList();
for (CardStateName stateName : unlockedRooms) {
if (this.hasState(stateName)) {
result.add(this.getState(stateName).getName());
}
}
return result;
}
public Set<CardStateName> getLockedRooms() {
Set<CardStateName> result = Sets.newHashSet(CardStateName.LeftSplit, CardStateName.RightSplit);
result.removeAll(this.unlockedRooms);
return result;
}
public List<String> getLockedRoomNames() {
List<String> result = Lists.newArrayList();
for (CardStateName stateName : getLockedRooms()) {
if (this.hasState(stateName)) {
result.add(this.getState(stateName).getName());
}
}
return result;
}
public boolean unlockRoom(Player p, CardStateName stateName) {
if (unlockedRooms.contains(stateName) || (stateName != CardStateName.LeftSplit && stateName != CardStateName.RightSplit)) {
return false;
}
unlockedRooms.add(stateName);
updateRooms();
Map<AbilityKey, Object> unlockParams = AbilityKey.mapFromPlayer(p);
unlockParams.put(AbilityKey.Card, this);
unlockParams.put(AbilityKey.CardState, getState(stateName));
getGame().getTriggerHandler().runTrigger(TriggerType.UnlockDoor, unlockParams, true);
// fully unlock
if (unlockedRooms.size() > 1) {
Map<AbilityKey, Object> fullyUnlockParams = AbilityKey.mapFromPlayer(p);
fullyUnlockParams.put(AbilityKey.Card, this);
getGame().getTriggerHandler().runTrigger(TriggerType.FullyUnlock, fullyUnlockParams, true);
}
return true;
}
public boolean lockRoom(Player p, CardStateName stateName) {
if (!unlockedRooms.contains(stateName) || (stateName != CardStateName.LeftSplit && stateName != CardStateName.RightSplit)) {
return false;
}
unlockedRooms.remove(stateName);
updateRooms();
return true;
}
public void updateRooms() {
if (!this.isRoom()) {
return;
}
if (this.isFaceDown()) {
return;
}
if (unlockedRooms.isEmpty()) {
this.setState(CardStateName.EmptyRoom, true);
} else if (unlockedRooms.size() > 1) {
this.setState(CardStateName.Original, true);
} else { // we already know the set is only one
for (CardStateName name : unlockedRooms) {
this.setState(name, true);
}
}
// update trigger after state change
getGame().getTriggerHandler().clearActiveTriggers(this, null);
getGame().getTriggerHandler().registerActiveTrigger(this, false);
}
public CardState getEmptyRoomState() {
if (!states.containsKey(CardStateName.EmptyRoom)) {
states.put(CardStateName.EmptyRoom, CardUtil.getEmptyRoomCharacteristic(this));
}
return states.get(CardStateName.EmptyRoom);
}
public SpellAbility getUnlockAbility(CardStateName state) {
if (!unlockAbilities.containsKey(state)) {
unlockAbilities.put(state, CardFactoryUtil.abilityUnlockRoom(getState(state)));
}
return unlockAbilities.get(state);
}
}

View File

@@ -258,6 +258,16 @@ public class CardFactory {
final CardState original = card.getState(CardStateName.Original);
original.addNonManaAbilities(card.getCurrentState().getNonManaAbilities());
original.addIntrinsicKeywords(card.getCurrentState().getIntrinsicKeywords()); // Copy 'Fuse' to original side
for (Trigger t : card.getCurrentState().getTriggers()) {
if (t.isIntrinsic()) {
original.addTrigger(t.copy(card, false));
}
}
for (StaticAbility st : card.getCurrentState().getStaticAbilities()) {
if (st.isIntrinsic()) {
original.addStaticAbility(st.copy(card, false));
}
}
original.getSVars().putAll(card.getCurrentState().getSVars()); // Unfortunately need to copy these to (Effect looks for sVars on execute)
} else if (state != CardStateName.Original) {
CardFactoryUtil.setupKeywordedAbilities(card);
@@ -415,7 +425,11 @@ public class CardFactory {
c.setAttractionLights(face.getAttractionLights());
// SpellPermanent only for Original State
if (c.getCurrentStateName() == CardStateName.Original || c.getCurrentStateName() == CardStateName.Modal || c.getCurrentStateName().toString().startsWith("Specialize")) {
if (c.getCurrentStateName() == CardStateName.Original ||
c.getCurrentStateName() == CardStateName.LeftSplit ||
c.getCurrentStateName() == CardStateName.RightSplit ||
c.getCurrentStateName() == CardStateName.Modal ||
c.getCurrentStateName().toString().startsWith("Specialize")) {
if (c.isLand()) {
SpellAbility sa = new LandAbility(c);
sa.setCardState(c.getCurrentState());

View File

@@ -119,6 +119,12 @@ public class CardFactoryUtil {
return morphDown;
}
public static SpellAbility abilityUnlockRoom(CardState cardState) {
String unlockStr = "ST$ UnlockDoor | Cost$ " + cardState.getManaCost().getShortString() + " | Unlock$ True | SpellDescription$ Unlock " + cardState.getName();
return AbilityFactory.getAbility(unlockStr, cardState);
}
/**
* <p>
* abilityMorphUp.

View File

@@ -215,6 +215,24 @@ public final class CardUtil {
return ret;
}
public static CardState getEmptyRoomCharacteristic(Card c) {
return getEmptyRoomCharacteristic(c, CardStateName.EmptyRoom);
}
public static CardState getEmptyRoomCharacteristic(Card c, CardStateName state) {
final CardType type = new CardType(false);
type.add("Enchantment");
type.add("Room");
final CardState ret = new CardState(c, state);
ret.setName("");
ret.setType(type);
// find new image key for empty room
ret.setImageKey(c.getImageKey());
return ret;
}
// a nice entry point with minimum parameters
public static Set<String> getReflectableManaColors(final SpellAbility sa) {
return getReflectableManaColors(sa, sa, Sets.newHashSet(), new CardCollection());

View File

@@ -1,6 +1,7 @@
package forge.game.card;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import forge.ImageKeys;
import forge.StaticData;
@@ -40,6 +41,14 @@ public class CardView extends GameEntityView {
return s == null ? null : s.getView();
}
public static Map<CardStateView, CardState> getStateMap(Iterable<CardState> states) {
Map<CardStateView, CardState> stateViewCache = Maps.newLinkedHashMap();
for (CardState state : states) {
stateViewCache.put(state.getView(), state);
}
return stateViewCache;
}
public CardView getBackup() {
if (get(TrackableProperty.PaperCardBackup) == null)
return null;
@@ -795,7 +804,7 @@ public class CardView extends GameEntityView {
sb.append(getOwner().getCommanderInfo(this)).append("\r\n");
}
if (isSplitCard() && !isFaceDown() && getZone() != ZoneType.Stack) {
if (isSplitCard() && !isFaceDown() && getZone() != ZoneType.Stack && getZone() != ZoneType.Battlefield) {
sb.append("(").append(getLeftSplitState().getName()).append(") ");
sb.append(getLeftSplitState().getAbilityText());
sb.append("\r\n\r\n").append("(").append(getRightSplitState().getName()).append(") ");
@@ -1183,6 +1192,11 @@ public class CardView extends GameEntityView {
return StringUtils.EMPTY;
}
@Override
public int hashCode() {
return Objects.hash(getId(), state);
}
@Override
public String toString() {
return (getName() + " (" + getDisplayId() + ")").trim();

View File

@@ -64,6 +64,8 @@ import org.apache.commons.lang3.tuple.Pair;
import java.util.*;
import java.util.Map.Entry;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
/**
* <p>
@@ -3954,4 +3956,12 @@ public class Player extends GameEntity implements Comparable<Player> {
Map.Entry<Long, Player> e = declaresBlockers.lastEntry();
return e == null ? null : e.getValue();
}
public List<String> getUnlockedDoors() {
return StreamSupport.stream(getCardsIn(ZoneType.Battlefield).spliterator(), false)
.filter(Card::isRoom)
.map(Card::getUnlockedRoomNames)
.flatMap(Collection::stream)
.collect(Collectors.toList());
}
}

View File

@@ -243,6 +243,8 @@ public abstract class PlayerController {
public abstract byte chooseColorAllowColorless(String message, Card c, ColorSet colors);
public abstract ICardFace chooseSingleCardFace(SpellAbility sa, String message, Predicate<ICardFace> cpp, String name);
public abstract ICardFace chooseSingleCardFace(SpellAbility sa, List<ICardFace> faces, String message);
public abstract CardState chooseSingleCardState(SpellAbility sa, List<CardState> states, String message, Map<String, Object> params);
public abstract List<String> chooseColors(String message, SpellAbility sa, int min, int max, List<String> options);
public abstract CounterType chooseCounterType(List<CounterType> options, SpellAbility sa, String prompt, Map<String, Object> params);

View File

@@ -144,6 +144,7 @@ public enum TriggerType {
TurnBegin(TriggerTurnBegin.class),
TurnFaceUp(TriggerTurnFaceUp.class),
Unattach(TriggerUnattach.class),
UnlockDoor(TriggerUnlockDoor.class),
UntapAll(TriggerUntapAll.class),
Untaps(TriggerUntaps.class),
VisitAttraction(TriggerVisitAttraction.class),

View File

@@ -0,0 +1,55 @@
package forge.game.trigger;
import java.util.Map;
import forge.game.ability.AbilityKey;
import forge.game.card.Card;
import forge.game.card.CardState;
import forge.game.spellability.SpellAbility;
import forge.util.Localizer;
public class TriggerUnlockDoor extends Trigger {
public TriggerUnlockDoor(final Map<String, String> params, final Card host, final boolean intrinsic) {
super(params, host, intrinsic);
}
@Override
public boolean performTest(Map<AbilityKey, Object> runParams) {
if (!matchesValidParam("ValidCard", runParams.get(AbilityKey.Card))) {
return false;
}
if (!matchesValidParam("ValidPlayer", runParams.get(AbilityKey.Player))) {
return false;
}
if (hasParam("ThisDoor")) {
CardState state = (CardState) runParams.get(AbilityKey.CardState);
// This Card
if (!getHostCard().equals(state.getCard())) {
return false;
}
// This Face
if (!getCardStateName().equals(state.getStateName())) {
return false;
}
}
return true;
}
@Override
public void setTriggeringObjects(SpellAbility sa, Map<AbilityKey, Object> runParams) {
sa.setTriggeringObjectsFrom(runParams, AbilityKey.Card, AbilityKey.Player);
}
@Override
public String getImportantStackObjects(SpellAbility sa) {
StringBuilder sb = new StringBuilder();
sb.append(Localizer.getInstance().getMessage("lblPlayer")).append(": ").append(sa.getTriggeringObject(AbilityKey.Player));
sb.append(", ").append(Localizer.getInstance().getMessage("lblCard")).append(": ").append(sa.getTriggeringObject(AbilityKey.Card));
return sb.toString();
}
}

View File

@@ -47,7 +47,7 @@ public abstract class TrackableObject implements IIdentifiable, Serializable {
@Override
public final boolean equals(final Object o) {
if (o == null) { return false; }
return o.hashCode() == id && o.getClass().equals(getClass());
return o.hashCode() == hashCode() && o.getClass().equals(getClass());
}
// don't know if this is really needed, but don't know a better way

View File

@@ -198,7 +198,7 @@ public class CardDetailPanel extends SkinnedPanel {
nameCost = name;
} else {
final String manaCost;
if (card.isSplitCard() && card.hasAlternateState() && !card.isFaceDown() && card.getZone() != ZoneType.Stack) { //only display current state's mana cost when on stack
if (card.isSplitCard() && card.hasAlternateState() && !card.isFaceDown() && card.getZone() != ZoneType.Stack && card.getZone() != ZoneType.Battlefield) { //only display current state's mana cost when on stack
manaCost = card.getLeftSplitState().getManaCost() + " // " + card.getAlternateState().getManaCost();
} else {
manaCost = state.getManaCost().toString();

View File

@@ -23,6 +23,7 @@ import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
import forge.card.CardRarity;
import forge.card.CardStateName;
import forge.card.mana.ManaCost;
import forge.game.card.CardView;
import forge.game.card.CardView.CardStateView;
@@ -748,8 +749,11 @@ public class FCardImageRenderer {
//draw type
x += padding;
w -= padding;
String typeLine = CardDetailUtil.formatCardType(state, true).replace(" - ", "");
drawVerticallyCenteredString(g, typeLine, new Rectangle(x, y, w, h), TYPE_FONT, TYPE_SIZE);
// check for shared type line
if (!state.getType().hasStringType("Room") || state.getState() != CardStateName.RightSplit) {
String typeLine = CardDetailUtil.formatCardType(state, true).replace(" - ", "");
drawVerticallyCenteredString(g, typeLine, new Rectangle(x, y, w, h), TYPE_FONT, TYPE_SIZE);
}
}
/**

View File

@@ -479,12 +479,15 @@ public class CardPanel extends SkinnedPanel implements CardContainer, IDisposabl
private void displayIconOverlay(final Graphics g, final boolean canShow) {
if (canShow && showCardManaCostOverlay() && cardWidth < 200) {
final boolean showSplitMana = card.isSplitCard();
final boolean showSplitMana = card.isSplitCard() && card.getZone() != ZoneType.Battlefield;
if (!showSplitMana) {
drawManaCost(g, card.getCurrentState().getManaCost(), 0);
} else {
if (!card.isFaceDown()) { // no need to draw mana symbols on face down split cards (e.g. manifested)
PaperCard pc = StaticData.instance().getCommonCards().getCard(card.getName());
PaperCard pc = null;
if (!card.getName().isEmpty()) {
pc = StaticData.instance().getCommonCards().getCard(card.getName());
}
int ofs = pc != null && Card.getCardForUi(pc).hasKeyword(Keyword.AFTERMATH) ? -12 : 12;
drawManaCost(g, card.getLeftSplitState().getManaCost(), ofs);

View File

@@ -727,6 +727,18 @@ public class PlayerControllerForTests extends PlayerController {
return null;
}
@Override
public ICardFace chooseSingleCardFace(SpellAbility sa, List<ICardFace> faces, String message) {
// TODO Auto-generated method stub
return null;
}
@Override
public CardState chooseSingleCardState(SpellAbility sa, List<CardState> states, String message, Map<String, Object> params) {
// TODO Auto-generated method stub
return null;
}
@Override
public Card chooseDungeon(Player player, List<PaperCard> dungeonCards, String message) {
// TODO Auto-generated method stub

View File

@@ -277,7 +277,7 @@ public class CardImageRenderer {
if (!noText && state != null) {
//draw mana cost for card
ManaCost mainManaCost = state.getManaCost();
if (card.isSplitCard() && card.getAlternateState() != null) {
if (card.isSplitCard() && card.getAlternateState() != null && !card.isFaceDown() && card.getZone() != ZoneType.Stack && card.getZone() != ZoneType.Battlefield) {
//handle rendering both parts of split card
mainManaCost = card.getLeftSplitState().getManaCost();
ManaCost otherManaCost = card.getRightSplitState().getManaCost();
@@ -1112,7 +1112,7 @@ public class CardImageRenderer {
float manaCostWidth = 0;
if (canShow) {
ManaCost mainManaCost = state.getManaCost();
if (card.isSplitCard() && card.hasAlternateState() && !card.isFaceDown() && card.getZone() != ZoneType.Stack) { //only display current state's mana cost when on stack
if (card.isSplitCard() && card.hasAlternateState() && !card.isFaceDown() && card.getZone() != ZoneType.Stack && card.getZone() != ZoneType.Battlefield) { //only display current state's mana cost when on stack
//handle rendering both parts of split card
mainManaCost = card.getLeftSplitState().getManaCost();
ManaCost otherManaCost = card.getAlternateState().getManaCost();

View File

@@ -852,19 +852,17 @@ public class CardRenderer {
}
if (showCardManaCostOverlay(card)) {
float manaSymbolSize = w / 4.5f;
if (card.isSplitCard() && card.hasAlternateState()) {
if (!card.isFaceDown()) { // no need to draw mana symbols on face down split cards (e.g. manifested)
if (isChoiceList) {
if (card.getRightSplitState().getName().equals(details.getName()))
drawManaCost(g, card.getRightSplitState().getManaCost(), x - padding, y, w + 2 * padding, h, manaSymbolSize);
else
drawManaCost(g, card.getLeftSplitState().getManaCost(), x - padding, y, w + 2 * padding, h, manaSymbolSize);
} else {
ManaCost leftManaCost = card.getLeftSplitState().getManaCost();
ManaCost rightManaCost = card.getRightSplitState().getManaCost();
drawManaCost(g, leftManaCost, x - padding, y-(manaSymbolSize/1.5f), w + 2 * padding, h, manaSymbolSize);
drawManaCost(g, rightManaCost, x - padding, y+(manaSymbolSize/1.5f), w + 2 * padding, h, manaSymbolSize);
}
if (card.isSplitCard() && card.hasAlternateState() && !card.isFaceDown() && card.getZone() != ZoneType.Stack && card.getZone() != ZoneType.Battlefield) {
if (isChoiceList) {
if (card.getRightSplitState().getName().equals(details.getName()))
drawManaCost(g, card.getRightSplitState().getManaCost(), x - padding, y, w + 2 * padding, h, manaSymbolSize);
else
drawManaCost(g, card.getLeftSplitState().getManaCost(), x - padding, y, w + 2 * padding, h, manaSymbolSize);
} else {
ManaCost leftManaCost = card.getLeftSplitState().getManaCost();
ManaCost rightManaCost = card.getRightSplitState().getManaCost();
drawManaCost(g, leftManaCost, x - padding, y-(manaSymbolSize/1.5f), w + 2 * padding, h, manaSymbolSize);
drawManaCost(g, rightManaCost, x - padding, y+(manaSymbolSize/1.5f), w + 2 * padding, h, manaSymbolSize);
}
} else {
drawManaCost(g, showAltState ? card.getAlternateState().getManaCost() : card.getCurrentState().getManaCost(), x - padding, y, w + 2 * padding, h, manaSymbolSize);

View File

@@ -0,0 +1,16 @@
Name:Dollmaker's Shop
ManaCost:1 W
Types:Enchantment Room
T:Mode$ AttackersDeclaredOneTarget | Execute$ TrigToken | AttackedTarget$ Player | ValidAttackers$ Creature.YouCtrl+!Toy | TriggerZones$ Battlefield | AttackingPlayer$ You | TriggerDescription$ Whenever one or more non-Toy creatures you control attack a player, create a 1/1 white Toy artifact creature token.
SVar:TrigToken:DB$ Token | TokenScript$ w_1_1_a_toy
AlternateMode:Split
Oracle:(You may cast either half. That door unlocks on the battlefield. As a sorcery, you may pay the mana cost of a locked door to unlock it.)\nWhenever one or more non-Toy creatures you control attack a player, create a 1/1 white Toy artifact creature token.
ALTERNATE
Name:Porcelain Gallery
ManaCost:4 W W
Types:Enchantment Room
S:Mode$ Continuous | Affected$ Creature.YouCtrl | AffectedZone$ Battlefield | SetPower$ X | SetToughness$ X | Description$ Creatures you control have base power and toughness each equal to the number of creatures you control.
SVar:X:Count$Valid Creature.YouCtrl
Oracle:(You may cast either half. That door unlocks on the battlefield. As a sorcery, you may pay the mana cost of a locked door to unlock it.)\nCreatures you control have base power and toughness each equal to the number of creatures you control.

View File

@@ -0,0 +1,8 @@
Name:Ghostly Keybearer
ManaCost:3 U
Types:Creature Spirit
PT:3/3
K:Flying
T:Mode$ DamageDone | ValidSource$ Card.Self | ValidTarget$ Player | Execute$ TrigUnlock | CombatDamage$ True | TriggerDescription$ Whenever CARDNAME deals combat damage to a player, unlock a locked door of up to one target Room you control.
SVar:TrigUnlock:DB$ UnlockDoor | Mode$ Unlock | ValidTgts$ Room.YouCtrl | TgtPrompt$ Choose target Room you control | TargetMin$ 0 | TargetMax$ 1
Oracle:Flying\nWhenever Ghostly Keybearer deals combat damage to a player, unlock a locked door of up to one target Room you control.

View File

@@ -0,0 +1,16 @@
Name:Glassworks
ManaCost:2 R
Types:Enchantment Room
T:Mode$ UnlockDoor | ValidPlayer$ You | ValidCard$ Card.Self | ThisDoor$ True | Execute$ TrigDamage | TriggerDescription$ When you unlock this door, this Room deals 4 damage to target creature an opponent controls.
SVar:TrigDamage:DB$ DealDamage | ValidTgts$ Creature.OppCtrl | TgtPrompt$ Select target creature an opponent controls | NumDmg$ 4
AlternateMode:Split
Oracle:(You may cast either half. That door unlocks on the battlefield. As a sorcery, you may pay the mana cost of a locked door to unlock it.)\nWhen you unlock this door, this Room deals 4 damage to target creature an opponent controls.
ALTERNATE
Name:Shattered Yard
ManaCost:4 R R
Types:Enchantment Room
T:Mode$ Phase | Phase$ End of Turn | ValidPlayer$ You | TriggerZones$ Battlefield | Execute$ TrigAllDamage | TriggerDescription$ At the beginning of your end step, this Room deals 1 damage to each opponent.
SVar:TrigAllDamage:DB$ DamageAll | ValidPlayers$ Player.Opponent | NumDmg$ 1
Oracle:(You may cast either half. That door unlocks on the battlefield. As a sorcery, you may pay the mana cost of a locked door to unlock it.)\nAt the beginning of your end step, this Room deals 1 damage to each opponent.

View File

@@ -0,0 +1,6 @@
Name:Toy Token
ManaCost:no cost
Types:Artifact Creature Toy
Colors:white
PT:1/1
Oracle:

View File

@@ -19,6 +19,7 @@ import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.*;
import forge.game.card.CardView.CardStateView;
import forge.game.card.token.TokenInfo;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
@@ -1829,6 +1830,11 @@ public class PlayerControllerHuman extends PlayerController implements IGameCont
return StaticData.instance().getCommonCards().getFaceByName(cardFaceView.getOracleName());
}
@Override
public ICardFace chooseSingleCardFace(SpellAbility sa, List<ICardFace> faces, String message) {
return getGui().one(message, faces);
}
@Override
public CounterType chooseCounterType(final List<CounterType> options, final SpellAbility sa, final String prompt,
Map<String, Object> params) {
@@ -1838,6 +1844,16 @@ public class PlayerControllerHuman extends PlayerController implements IGameCont
return getGui().one(prompt, options);
}
@Override
public CardState chooseSingleCardState(SpellAbility sa, List<CardState> states, String message, Map<String, Object> params) {
if (states.size() <= 1) {
return Iterables.getFirst(states, null);
}
Map<CardStateView, CardState> cache = CardView.getStateMap(states);
CardStateView chosen = getGui().one(message, Lists.newArrayList(cache.keySet()));
return cache.get(chosen);
}
@Override
public String chooseKeywordForPump(final List<String> options, final SpellAbility sa, final String prompt, final Card tgtCard) {
if (options.size() <= 1) {
@@ -3216,7 +3232,7 @@ public class PlayerControllerHuman extends PlayerController implements IGameCont
@Override
public String chooseCardName(SpellAbility sa, List<ICardFace> faces, String message) {
ICardFace face = getGui().one(message, faces);
ICardFace face = chooseSingleCardFace(sa, faces, message);
return face == null ? "" : face.getName();
}