Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Room: First Spell Part #6044

Draft
wants to merge 14 commits into
base: master
Choose a base branch
from
9 changes: 9 additions & 0 deletions forge-ai/src/main/java/forge/ai/PlayerControllerAi.java
Original file line number Diff line number Diff line change
Expand Up @@ -1524,6 +1524,15 @@ public String chooseCardName(SpellAbility sa, List<ICardFace> faces, String mess
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 Card chooseDungeon(Player ai, List<PaperCard> dungeonCards, String message) {
// TODO: improve the conditions that define which dungeon is a viable option to choose
Expand Down
6 changes: 6 additions & 0 deletions forge-ai/src/main/java/forge/ai/SpellAbilityAi.java
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,12 @@ public String chooseCardName(Player ai, SpellAbility sa, List<ICardFace> faces)
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 int chooseNumber(Player player, SpellAbility sa, int min, int max, Map<String, Object> params) {
return max;
}
Expand Down
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
10 changes: 9 additions & 1 deletion forge-game/src/main/java/forge/game/GameAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,12 @@ 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.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;
}
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
1 change: 1 addition & 0 deletions forge-game/src/main/java/forge/game/ability/ApiType.java
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ public void resolve(SpellAbility sa) {

final long ts = game.getNextTimestamp();
tgtCard.addCloneState(CardFactory.getCloneStates(cardToCopy, tgtCard, sa), ts);
tgtCard.updateRooms();

// set ETB tapped of clone
if (sa.hasParam("IntoPlayTapped")) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package forge.game.ability.effects;

import java.util.Map;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;

import forge.StaticData;
import forge.card.CardStateName;
import forge.card.ICardFace;
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.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) {
switch (sa.getParamOrDefault("Mode", "ThisDoor")) {
case "ThisDoor":
c.unlockRoom(activator, sa.getCardStateName());
break;
case "Unlock":
//List<ICardFace> faces = c.getLockedRoomNames().stream().map(face -> StaticData.instance().getCommonCards().getFaceByName(face)).collect(Collectors.toList());

Map<ICardFace, CardStateName> map = Maps.newHashMap();

for (CardStateName faceStateName : c.getLockedRooms()) {
if (!c.hasState(faceStateName)) {
continue;
}
String faceName = c.getState(faceStateName).getName();
ICardFace face = StaticData.instance().getCommonCards().getFaceByName(faceName);
map.put(face, faceStateName);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like at least on Desktop GuiChoose should accept a list of CardStateView just fine...
might be worth a shot to see how it'd display, then doing it without the Map seems possible 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need to be careful with that, because for Network, the object needs to be Serializeable

if you can get CardStateView working, then you could try this too

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it does extend TrackableObject so like all View classes I except it can be serialized

}

// need to choose Room Name
ICardFace chosen = activator.getController().chooseSingleCardFace(sa, Lists.newArrayList(map.keySet()), "Choose Room to unlock");
if (chosen == null) {
continue;
}
c.unlockRoom(activator, map.get(chosen));
break;
}
}
}

}
135 changes: 130 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 @@ -5576,6 +5585,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 @@ -6636,7 +6647,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 @@ -7475,6 +7486,15 @@ public List<SpellAbility> getAllPossibleAbilities(final Player player, final boo
}
}

if (isInPlay() && isRoom() && !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()) {
Expand Down Expand Up @@ -8082,4 +8102,109 @@ 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 Set<CardStateName> getLockedRooms() {
if (!this.isSplitCard())
return ImmutableSet.of();
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);
}
}
Loading
Loading