Skip to content

Commit

Permalink
Add Unlock Static Action
Browse files Browse the repository at this point in the history
  • Loading branch information
Hanmac committed Sep 8, 2024
1 parent fdc6ec5 commit 8bb4ea5
Show file tree
Hide file tree
Showing 11 changed files with 240 additions and 5 deletions.
1 change: 1 addition & 0 deletions forge-core/src/main/java/forge/card/CardStateName.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public enum CardStateName {
RightSplit,
Adventure,
Modal,
EmptyRoom,
SpecializeW,
SpecializeU,
SpecializeB,
Expand Down
8 changes: 8 additions & 0 deletions forge-game/src/main/java/forge/game/GameAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,11 @@ private Card changeZone(final Zone zoneFrom, Zone zoneTo, final Card c, Integer
// Make sure the card returns from the battlefield as the original card with two halves
resetToOriginal = true;
}
} 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;
Expand Down Expand Up @@ -608,6 +613,9 @@ private Card changeZone(final Zone zoneFrom, Zone zoneTo, final Card c, Integer
// 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public enum AbilityKey {
Blockers("Blockers"),
CanReveal("CanReveal"),
Card("Card"),
CardState("CardState"),
Cards("Cards"),
CardsFiltered("CardsFiltered"),
CardLKI("CardLKI"),
Expand Down
106 changes: 101 additions & 5 deletions forge-game/src/main/java/forge/game/card/Card.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -444,6 +447,9 @@ public CardState getState(final CardStateName state) {
if (state == CardStateName.FaceDown) {
return getFaceDownState();
}
if (state == CardStateName.EmptyRoom) {
return getEmptyRoomState();
}
CardCloneStates clStates = getLastClonedState();
if (clStates == null) {
return getOriginalState(state);
Expand All @@ -452,7 +458,7 @@ public CardState getState(final CardStateName state) {
}

public boolean hasState(final CardStateName state) {
if (state == CardStateName.FaceDown) {
if (state == CardStateName.FaceDown || state == CardStateName.EmptyRoom) {
return true;
}
CardCloneStates clStates = getLastClonedState();
Expand All @@ -466,6 +472,9 @@ public CardState getOriginalState(final CardStateName state) {
if (state == CardStateName.FaceDown) {
return getFaceDownState();
}
if (state == CardStateName.EmptyRoom) {
return getEmptyRoomState();
}
return states.get(state);
}

Expand All @@ -492,7 +501,7 @@ public boolean setState(final CardStateName state, boolean updateView, boolean f
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)) {
Expand Down Expand Up @@ -1966,7 +1975,7 @@ public final boolean hasChosenNumber() {
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;
Expand Down Expand Up @@ -3121,7 +3130,7 @@ private StringBuilder abilityTextInstantSorcery(CardState state) {
} 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);
Expand Down Expand Up @@ -5570,6 +5579,8 @@ public final boolean isSpell() {

public final boolean isOutlaw() { return getType().isOutlaw(); }

public final boolean isRoom() { return getType().hasSubtype("Room"); }

/** {@inheritDoc} */
@Override
public final int compareTo(final Card that) {
Expand Down Expand Up @@ -6630,7 +6641,7 @@ public final boolean setPlotted(final boolean plotted) {
if (plotted == true && !isLKI()) {
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromCard(this);
game.getTriggerHandler().runTrigger(TriggerType.BecomesPlotted, runParams, false);
}
}
return true;
}

Expand Down Expand Up @@ -7468,6 +7479,15 @@ public List<SpellAbility> getAllPossibleAbilities(final Player player, final boo
}
}

if (isInPlay() && isRoom()) {
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()) {
Expand Down Expand Up @@ -8075,4 +8095,80 @@ public boolean isWitherDamage() {
}
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 boolean unlockRoom(Player p, CardStateName stateName) {
if (unlockedRooms.contains(stateName)) {
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 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);
}
}
10 changes: 10 additions & 0 deletions forge-game/src/main/java/forge/game/card/CardFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,16 @@ private static void buildAbilities(final Card card) {
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);
Expand Down
24 changes: 24 additions & 0 deletions forge-game/src/main/java/forge/game/card/CardFactoryUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,30 @@ public boolean canPlay() {
return morphDown;
}

public static SpellAbility abilityUnlockRoom(CardState cardState) {
final AbilityStatic unlock = new AbilityStatic(cardState.getCard(), cardState.getManaCost()) {

@Override
public void resolve() {
// TODO Auto-generated method stub
hostCard.unlockRoom(getActivatingPlayer(), getCardStateName());
}

@Override
public boolean canPlay() {
if (!hostCard.isInPlay()) {
return false; // cut short if already on the battlefield, avoids side effects when checking statics
}
return true;
}
};

unlock.setDescription("Unlock " + cardState.getName());
unlock.setCardState(cardState);

return unlock;
}

/**
* <p>
* abilityMorphUp.
Expand Down
18 changes: 18 additions & 0 deletions forge-game/src/main/java/forge/game/card/CardUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,24 @@ public static CardState getFaceDownCharacteristic(Card c, CardStateName state) {
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());
Expand Down
10 changes: 10 additions & 0 deletions forge-game/src/main/java/forge/game/player/Player.java
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@
import java.util.*;
import java.util.Map.Entry;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

/**
* <p>
Expand Down Expand Up @@ -3979,4 +3981,12 @@ public Player getDeclaresBlockers() {
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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,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),
Expand Down
50 changes: 50 additions & 0 deletions forge-game/src/main/java/forge/game/trigger/TriggerUnlockDoor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
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);
if (!getCardState().equals(state)) {
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();
}

}
16 changes: 16 additions & 0 deletions forge-gui/res/cardsfolder/upcoming/glassworks_shattered_yard.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Name:Glassworks
ManaCost:2 R
Types:Enchantment Room
T:Mode$ UnlockDoor | ValidPlayer$ You | 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.

0 comments on commit 8bb4ea5

Please sign in to comment.