Implement Dungeon mechanism and related spoiled cards

This commit is contained in:
Lyu Zong-Hong
2021-06-26 18:29:14 +09:00
parent 9eabe65496
commit be40fce2c6
31 changed files with 482 additions and 28 deletions

View File

@@ -1192,6 +1192,8 @@ public class GameAction {
for (final Card c : cards) {
// If a token is in a zone other than the battlefield, it ceases to exist.
checkAgain |= stateBasedAction704_5d(c);
// Dungeon Card won't affect other cards, so don't need to set checkAgain
stateBasedAction_Dungeon(c);
}
}
}
@@ -1382,6 +1384,15 @@ public class GameAction {
return checkAgain;
}
private void stateBasedAction_Dungeon(Card c) {
if (!c.getType().isDungeon() || !c.isInLastRoom()) {
return;
}
if (!game.getStack().hasSourceOnStack(c, null)) {
completeDungeon(c.getController(), c);
}
}
private boolean stateBasedAction704_attach(Card c, CardZoneTable table) {
boolean checkAgain = false;
@@ -1706,7 +1717,7 @@ public class GameAction {
game.getTriggerHandler().runTrigger(TriggerType.Destroyed, runParams, false);
// in case the destroyed card has such a trigger
game.getTriggerHandler().registerActiveLTBTrigger(c);
final Card sacrificed = sacrificeDestroy(c, sa, table, params);
return sacrificed != null;
}
@@ -2187,4 +2198,14 @@ public class GameAction {
counterTable.triggerCountersPutAll(game);
counterTable.clear();
}
public void completeDungeon(Player player, Card dungeon) {
player.addCompletedDungeon(dungeon);
ceaseToExist(dungeon, true);
// Run RoomEntered trigger
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromCard(dungeon);
runParams.put(AbilityKey.Player, player);
game.getTriggerHandler().runTrigger(TriggerType.DungeonCompleted, runParams, false);
}
}

View File

@@ -107,6 +107,7 @@ public enum AbilityKey {
ReplacementResult("ReplacementResult"),
ReplacementResultMap("ReplacementResultMap"),
Result("Result"),
RoomName("RoomName"),
Scheme("Scheme"),
Source("Source"),
Sources("Sources"),

View File

@@ -3359,6 +3359,29 @@ public class AbilityUtils {
return doXMath(opps == null ? 0 : opps.size(), m, source, ctb);
}
if (value.equals("DungeonsCompleted")) {
return doXMath(player.getCompletedDungeons().size(), m, source, ctb);
}
if (value.equals("DifferentlyNamedDungeonsCompleted")) {
int amount = 0;
List<Card> dungeons = player.getCompletedDungeons();
for (int i = 0; i < dungeons.size(); ++i) {
Card d1 = dungeons.get(i);
boolean hasSameName = false;
for (int j = i - 1; j >= 0; --j) {
Card d2 = dungeons.get(j);
if (d1.getName().equals(d2.getName())) {
hasSameName = true;
break;
}
}
if (!hasSameName) {
++amount;
}
}
return doXMath(amount, m, source, ctb);
}
return doXMath(0, m, source, ctb);
}

View File

@@ -7,7 +7,7 @@ import java.util.Map;
import forge.game.ability.effects.*;
import forge.util.ReflectionUtil;
/**
/**
* TODO: Write javadoc for this type.
*
*/
@@ -172,6 +172,7 @@ public enum ApiType {
UnattachAll (UnattachAllEffect.class),
Untap (UntapEffect.class),
UntapAll (UntapAllEffect.class),
Venture (VentureEffect.class),
Vote (VoteEffect.class),
WinsGame (GameWinEffect.class),
@@ -187,7 +188,7 @@ public enum ApiType {
private final Class<? extends SpellAbilityEffect> clsEffect;
private static final Map<String, ApiType> allValues = new HashMap<>();
static {
for(ApiType t : ApiType.values()) {
allValues.put(t.name().toLowerCase(), t);
@@ -197,7 +198,7 @@ public enum ApiType {
ApiType(Class<? extends SpellAbilityEffect> clsEf) { this(clsEf, true); }
ApiType(Class<? extends SpellAbilityEffect> clsEf, final boolean isStateLess) {
clsEffect = clsEf;
instanceEffect = isStateLess ? ReflectionUtil.makeDefaultInstanceOf(clsEf) : null;
instanceEffect = isStateLess ? ReflectionUtil.makeDefaultInstanceOf(clsEf) : null;
}
public static ApiType smartValueOf(String value) {

View File

@@ -0,0 +1,125 @@
package forge.game.ability.effects;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import com.google.common.base.Predicates;
import forge.StaticData;
import forge.card.CardRulesPredicates;
import forge.card.ICardFace;
import forge.game.Game;
import forge.game.ability.AbilityKey;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.card.CardCollectionView;
import forge.game.card.CounterType;
import forge.game.event.GameEventCardCounters;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType;
import forge.game.trigger.WrappedAbility;
import forge.game.zone.ZoneType;
import forge.item.PaperCard;
import forge.util.Localizer;
public class VentureEffect extends SpellAbilityEffect {
private Card getDungeonCard(SpellAbility sa, Player player) {
final Game game = player.getGame();
CardCollectionView commandCards = player.getCardsIn(ZoneType.Command);
for (Card card : commandCards) {
if (card.getType().isDungeon()) {
if (!card.isInLastRoom()) {
return card;
}
// If the current dungeon is already in last room, complete it first.
game.getAction().completeDungeon(player, card);
break;
}
}
// Create a new dugeon card chosen by player in command zone.
List<PaperCard> dungeonCards = StaticData.instance().getVariantCards().getAllCards(
Predicates.compose(CardRulesPredicates.Presets.IS_DUNGEON, PaperCard.FN_GET_RULES));
List<ICardFace> faces = new ArrayList<>();
for (PaperCard pc : dungeonCards) {
faces.add(pc.getRules().getMainPart());
}
String message = Localizer.getInstance().getMessage("lblChooseACardName");
String chosen = player.getController().chooseCardName(sa, faces, message);
Card dungeon = Card.fromPaperCard(StaticData.instance().getVariantCards().getUniqueByName(chosen), player);
game.getTriggerHandler().suppressMode(TriggerType.ChangesZone);
game.getAction().moveTo(ZoneType.Command, dungeon, sa);
game.getTriggerHandler().clearSuppression(TriggerType.ChangesZone);
return dungeon;
}
private String chooseNextRoom(SpellAbility sa, Player player, Card dungeon, String room) {
String nextRoomParam = "";
for (final Trigger t : dungeon.getTriggers()) {
SpellAbility roomSA = t.getOverridingAbility();
if (roomSA.getParam("RoomName").equals(room)) {
nextRoomParam = roomSA.getParam("NextRoomName");
break;
}
}
String [] nextRoomNames = nextRoomParam.split(",");
if (nextRoomNames.length > 1) {
List<SpellAbility> candidates = new ArrayList<>();
for (String nextRoomName : nextRoomNames) {
for (final Trigger t : dungeon.getTriggers()) {
SpellAbility roomSA = t.getOverridingAbility();
if (roomSA.getParam("RoomName").equals(nextRoomName)) {
candidates.add(new WrappedAbility(t, roomSA, player));
break;
}
}
}
final String title = Localizer.getInstance().getMessage("lblChooseAbilityForObject", dungeon.toString());
SpellAbility chosen = player.getController().chooseSingleSpellForEffect(candidates, sa, title, null);
return chosen.getParam("RoomName");
} else {
return nextRoomNames[0];
}
}
private void ventureIntoDungeon(SpellAbility sa, Player player) {
final Game game = player.getGame();
Card dungeon = getDungeonCard(sa, player);
String room = dungeon.getCurrentRoom();
String nextRoom = null;
// Determine next room to venture into
if (room == null || room.isEmpty()) {
SpellAbility roomSA = dungeon.getTriggers().get(0).getOverridingAbility();
nextRoom = roomSA.getParam("RoomName");
} else {
nextRoom = chooseNextRoom(sa, player, dungeon, room);
}
dungeon.setCurrentRoom(nextRoom);
// TODO: Currently play the Add Counter sound, but maybe add soundeffect for marker?
game.fireEvent(new GameEventCardCounters(dungeon, CounterType.getType("LEVEL"), 0, 1));
// Run RoomEntered trigger
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromCard(dungeon);
runParams.put(AbilityKey.RoomName, nextRoom);
game.getTriggerHandler().runTrigger(TriggerType.RoomEntered, runParams, false);
}
@Override
public void resolve(SpellAbility sa) {
for (final Player p : getTargetPlayers(sa)) {
if (!sa.usesTargeting() || p.canBeTargetedBy(sa)) {
ventureIntoDungeon(sa, p);
}
}
}
}

View File

@@ -302,6 +302,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
private EvenOdd chosenEvenOdd = null;
private Direction chosenDirection = null;
private String chosenMode = "";
private String currentRoom = null;
private Card exiledWith = null;
private Player exiledBy = null;
@@ -1782,6 +1783,23 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
view.updateChosenMode(this);
}
public String getCurrentRoom() {
return currentRoom;
}
public void setCurrentRoom(String room) {
currentRoom = room;
view.updateCurrentRoom(this);
}
public boolean isInLastRoom() {
for (final Trigger t : getTriggers()) {
SpellAbility sa = t.getOverridingAbility();
if (sa.getParam("RoomName").equals(currentRoom) && !sa.hasParam("NextRoom")) {
return true;
}
}
return false;
}
public boolean hasChosenName() {
return chosenName != null;
}
@@ -2112,7 +2130,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
|| keyword.startsWith("Amplify") || keyword.startsWith("Ninjutsu") || keyword.startsWith("Adapt")
|| keyword.startsWith("Transfigure") || keyword.startsWith("Aura swap")
|| keyword.startsWith("Cycling") || keyword.startsWith("TypeCycling")
|| keyword.startsWith("Encore") || keyword.startsWith("Mutate")) {
|| keyword.startsWith("Encore") || keyword.startsWith("Mutate") || keyword.startsWith("Dungeon")) {
// keyword parsing takes care of adding a proper description
} else if (keyword.startsWith("CantBeBlockedByAmount")) {
sbLong.append(getName()).append(" can't be blocked ");
@@ -4666,7 +4684,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
getGame().getTriggerHandler().registerActiveTrigger(this, false);
getGame().getTriggerHandler().runTrigger(TriggerType.PhaseIn, runParams, false);
}
game.updateLastStateForCard(this);
return true;

View File

@@ -19,6 +19,7 @@ package forge.game.card;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
@@ -1817,6 +1818,41 @@ public class CardFactoryUtil {
inst.addTrigger(parsedUpkeepTrig);
inst.addTrigger(parsedSacTrigger);
} else if (keyword.startsWith("Dungeon")) {
final List<String> abs = Arrays.asList(keyword.substring("Dungeon:".length()).split(","));
final Map<String, SpellAbility> saMap = new LinkedHashMap<>();
for(String ab : abs) {
saMap.put(ab, AbilityFactory.getAbility(card, ab));
}
for (SpellAbility sa : saMap.values()) {
String roomName = sa.getParam("RoomName");
StringBuilder trigStr = new StringBuilder("Mode$ RoomEntered | TriggerZones$ Command");
trigStr.append(" | ValidCard$ Card.Self | ValidRoom$ ").append(roomName);
trigStr.append(" | TriggerDescription$ ").append(roomName).append("").append(sa.getDescription());
if (sa.hasParam("NextRoom")) {
boolean first = true;
StringBuilder nextRoomParam = new StringBuilder();
trigStr.append(" (→ ");
for (String nextRoomSVar : sa.getParam("NextRoom").split(",")) {
if (!first) {
trigStr.append(" or ");
nextRoomParam.append(",");
}
String nextRoomName = saMap.get(nextRoomSVar).getParam("RoomName");
trigStr.append(nextRoomName);
nextRoomParam.append(nextRoomName);
first = false;
}
trigStr.append(")");
sa.putParam("NextRoomName", nextRoomParam.toString());
}
// Need to set intrinsic to false here, else the first room won't get triggered
final Trigger t = TriggerHandler.parseTrigger(trigStr.toString(), card, false);
t.setOverridingAbility(sa);
inst.addTrigger(t);
}
} else if (keyword.startsWith("Ward")) {
final String[] k = keyword.split(":");
final Cost cost = new Cost(k[1], false);

View File

@@ -389,6 +389,13 @@ public class CardView extends GameEntityView {
set(TrackableProperty.ChosenMode, c.getChosenMode());
}
public String getCurrentRoom() {
return get(TrackableProperty.CurrentRoom);
}
void updateCurrentRoom(Card c) {
set(TrackableProperty.CurrentRoom, c.getCurrentRoom());
}
private String getRemembered() {
return get(TrackableProperty.Remembered);
}

View File

@@ -194,6 +194,7 @@ public class Player extends GameEntity implements Comparable<Player> {
private boolean tappedLandForManaThisTurn = false;
private int attackersDeclaredThisTurn = 0;
private PlayerCollection attackedOpponentsThisTurn = new PlayerCollection();
private List<Card> completedDungeons = new ArrayList<>();
private final Map<ZoneType, PlayerZone> zones = Maps.newEnumMap(ZoneType.class);
private final Map<Long, Integer> adjustLandPlays = Maps.newHashMap();
@@ -1947,6 +1948,13 @@ public class Player extends GameEntity implements Comparable<Player> {
attackersDeclaredThisTurn = 0;
}
public final List<Card> getCompletedDungeons() {
return completedDungeons;
}
public void addCompletedDungeon(Card dungeon) {
completedDungeons.add(dungeon);
}
public final void altWinBySpellEffect(final String sourceName) {
if (cantWin()) {
System.out.println("Tried to win, but currently can't.");

View File

@@ -0,0 +1,35 @@
package forge.game.trigger;
import java.util.Map;
import forge.game.ability.AbilityKey;
import forge.game.card.Card;
import forge.game.spellability.SpellAbility;
import forge.util.Localizer;
public class TriggerCompletedDungeon extends Trigger {
public TriggerCompletedDungeon(final Map<String, String> params, final Card host, final boolean intrinsic) {
super(params, host, intrinsic);
}
@Override
public final boolean performTest(final Map<AbilityKey, Object> runParams) {
if (!matchesValidParam("ValidPlayer", runParams.get(AbilityKey.Player))) {
return false;
}
return true;
}
@Override
public final void setTriggeringObjects(final SpellAbility sa, Map<AbilityKey, Object> runParams) {
sa.setTriggeringObjectsFrom(runParams, AbilityKey.Player);
}
public String getImportantStackObjects(SpellAbility sa) {
StringBuilder sb = new StringBuilder();
sb.append(Localizer.getInstance().getMessage("lblPlayer")).append(": ").append(sa.getTriggeringObject(AbilityKey.Player));
return sb.toString();
}
}

View File

@@ -0,0 +1,42 @@
package forge.game.trigger;
import java.util.Map;
import forge.game.ability.AbilityKey;
import forge.game.card.Card;
import forge.game.spellability.SpellAbility;
public class TriggerEnteredRoom extends Trigger {
public TriggerEnteredRoom(final Map<String, String> params, final Card host, final boolean intrinsic) {
super(params, host, intrinsic);
}
@Override
public final boolean performTest(final Map<AbilityKey, Object> runParams) {
if (!matchesValidParam("ValidCard", runParams.get(AbilityKey.Card))) {
return false;
}
if (!matchesValidParam("ValidRoom", runParams.get(AbilityKey.RoomName))) {
return false;
}
return true;
}
@Override
public final void setTriggeringObjects(final SpellAbility sa, Map<AbilityKey, Object> runParams) {
sa.setTriggeringObjectsFrom(runParams, AbilityKey.RoomName);
}
public String getImportantStackObjects(SpellAbility sa) {
Object roomName = sa.getTriggeringObject(AbilityKey.RoomName);
if (roomName != null) {
StringBuilder sb = new StringBuilder("Room: ");
sb.append(roomName);
return sb.toString();
}
return "";
}
}

View File

@@ -7,7 +7,7 @@ import java.util.Map;
import forge.game.card.Card;
/**
/**
* TODO: Write javadoc for this type.
*
*/
@@ -58,6 +58,7 @@ public enum TriggerType {
Discarded(TriggerDiscarded.class),
DiscardedAll(TriggerDiscardedAll.class),
Drawn(TriggerDrawn.class),
DungeonCompleted(TriggerCompletedDungeon.class),
Evolved(TriggerEvolved.class),
ExcessDamage(TriggerExcessDamage.class),
Exerted(TriggerExerted.class),
@@ -88,6 +89,7 @@ public enum TriggerType {
Regenerated(TriggerRegenerated.class),
Revealed(TriggerRevealed.class),
RolledDie(TriggerRolledDie.class),
RoomEntered(TriggerEnteredRoom.class),
Sacrificed(TriggerSacrificed.class),
Scry(TriggerScry.class),
SearchedLibrary(TriggerSearchedLibrary.class),

View File

@@ -63,6 +63,7 @@ public enum TrackableProperty {
ChosenDirection(TrackableTypes.EnumType(Direction.class)),
ChosenEvenOdd(TrackableTypes.EnumType(EvenOdd.class)),
ChosenMode(TrackableTypes.StringType),
CurrentRoom(TrackableTypes.StringType),
Remembered(TrackableTypes.StringType),
NamedCard(TrackableTypes.StringType),
NamedCard2(TrackableTypes.StringType),