mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-15 18:28:00 +00:00
Merge remote-tracking branch 'upstream/master' into formats/pauper
This commit is contained in:
3
.github/workflows/stale.yml
vendored
3
.github/workflows/stale.yml
vendored
@@ -26,7 +26,8 @@ jobs:
|
||||
close-issue-message: 'This issue was closed because it has been stalled for 5 days with no activity.'
|
||||
stale-issue-label: 'no-issue-activity'
|
||||
stale-pr-label: 'no-pr-activity'
|
||||
exempt-pr-labels: 'awaiting-approval,work-in-progress'
|
||||
exempt-issue-labels: 'keep'
|
||||
exempt-pr-labels: 'awaiting-approval,work-in-progress,keep'
|
||||
days-before-issue-stale: 30
|
||||
days-before-pr-stale: 45
|
||||
days-before-issue-close: 5
|
||||
|
||||
@@ -655,10 +655,11 @@ public class AiController {
|
||||
List<SpellAbility> all = ComputerUtilAbility.getSpellAbilities(cards, player);
|
||||
|
||||
try {
|
||||
Collections.sort(all, saComparator); // put best spells first
|
||||
Collections.sort(all, ComputerUtilAbility.saEvaluator); // put best spells first
|
||||
ComputerUtilAbility.sortCreatureSpells(all);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
System.err.println(ex.getMessage());
|
||||
String assertex = ComparatorUtil.verifyTransitivity(saComparator, all);
|
||||
String assertex = ComparatorUtil.verifyTransitivity(ComputerUtilAbility.saEvaluator, all);
|
||||
Sentry.captureMessage(ex.getMessage() + "\nAssertionError [verifyTransitivity]: " + assertex);
|
||||
}
|
||||
|
||||
@@ -1016,167 +1017,6 @@ public class AiController {
|
||||
return false;
|
||||
}
|
||||
|
||||
// not sure "playing biggest spell" matters?
|
||||
private final static Comparator<SpellAbility> saComparator = new Comparator<SpellAbility>() {
|
||||
@Override
|
||||
public int compare(final SpellAbility a, final SpellAbility b) {
|
||||
// sort from highest cost to lowest
|
||||
// we want the highest costs first
|
||||
int a1 = a.getPayCosts().getTotalMana().getCMC();
|
||||
int b1 = b.getPayCosts().getTotalMana().getCMC();
|
||||
|
||||
// deprioritize SAs explicitly marked as preferred to be activated last compared to all other SAs
|
||||
if (a.hasParam("AIActivateLast") && !b.hasParam("AIActivateLast")) {
|
||||
return 1;
|
||||
} else if (b.hasParam("AIActivateLast") && !a.hasParam("AIActivateLast")) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// deprioritize planar die roll marked with AIRollPlanarDieParams:LowPriority$ True
|
||||
if (ApiType.RollPlanarDice == a.getApi() && a.getHostCard() != null && a.getHostCard().hasSVar("AIRollPlanarDieParams") && a.getHostCard().getSVar("AIRollPlanarDieParams").toLowerCase().matches(".*lowpriority\\$\\s*true.*")) {
|
||||
return 1;
|
||||
} else if (ApiType.RollPlanarDice == b.getApi() && b.getHostCard() != null && b.getHostCard().hasSVar("AIRollPlanarDieParams") && b.getHostCard().getSVar("AIRollPlanarDieParams").toLowerCase().matches(".*lowpriority\\$\\s*true.*")) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// deprioritize pump spells with pure energy cost (can be activated last,
|
||||
// since energy is generally scarce, plus can benefit e.g. Electrostatic Pummeler)
|
||||
int a2 = 0, b2 = 0;
|
||||
if (a.getApi() == ApiType.Pump && a.getPayCosts().getCostEnergy() != null) {
|
||||
if (a.getPayCosts().hasOnlySpecificCostType(CostPayEnergy.class)) {
|
||||
a2 = a.getPayCosts().getCostEnergy().convertAmount();
|
||||
}
|
||||
}
|
||||
if (b.getApi() == ApiType.Pump && b.getPayCosts().getCostEnergy() != null) {
|
||||
if (b.getPayCosts().hasOnlySpecificCostType(CostPayEnergy.class)) {
|
||||
b2 = b.getPayCosts().getCostEnergy().convertAmount();
|
||||
}
|
||||
}
|
||||
if (a2 == 0 && b2 > 0) {
|
||||
return -1;
|
||||
} else if (b2 == 0 && a2 > 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// cast 0 mana cost spells first (might be a Mox)
|
||||
if (a1 == 0 && b1 > 0 && ApiType.Mana != a.getApi()) {
|
||||
return -1;
|
||||
} else if (a1 > 0 && b1 == 0 && ApiType.Mana != b.getApi()) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (a.getHostCard() != null && a.getHostCard().hasSVar("FreeSpellAI")) {
|
||||
return -1;
|
||||
} else if (b.getHostCard() != null && b.getHostCard().hasSVar("FreeSpellAI")) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (a.getHostCard().equals(b.getHostCard()) && a.getApi() == b.getApi()) {
|
||||
// Cheaper Spectacle costs should be preferred
|
||||
// FIXME: Any better way to identify that these are the same ability, one with Spectacle and one not?
|
||||
// (looks like it's not a full-fledged alternative cost as such, and is not processed with other alt costs)
|
||||
if (a.isSpectacle() && !b.isSpectacle() && a1 < b1) {
|
||||
return 1;
|
||||
} else if (b.isSpectacle() && !a.isSpectacle() && b1 < a1) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// If both are permanent creature spells, prefer the one that evaluates higher
|
||||
if (a1 == b1 && a.getApi() == ApiType.PermanentCreature && b.getApi() == ApiType.PermanentCreature) {
|
||||
int evalA = ComputerUtilCard.evaluateCreature(a);
|
||||
int evalB = ComputerUtilCard.evaluateCreature(b);
|
||||
if (evalA > evalB) {
|
||||
a1++;
|
||||
} else if (evalB > evalA) {
|
||||
b1++;
|
||||
}
|
||||
}
|
||||
|
||||
a1 += getSpellAbilityPriority(a);
|
||||
b1 += getSpellAbilityPriority(b);
|
||||
|
||||
return b1 - a1;
|
||||
}
|
||||
|
||||
private int getSpellAbilityPriority(SpellAbility sa) {
|
||||
int p = 0;
|
||||
Card source = sa.getHostCard();
|
||||
final Player ai = source == null ? sa.getActivatingPlayer() : source.getController();
|
||||
if (ai == null) {
|
||||
System.err.println("Error: couldn't figure out the activating player and host card for SA: " + sa);
|
||||
return 0;
|
||||
}
|
||||
final boolean noCreatures = ai.getCreaturesInPlay().isEmpty();
|
||||
|
||||
if (source != null) {
|
||||
// puts creatures in front of spells
|
||||
if (source.isCreature()) {
|
||||
p += 1;
|
||||
}
|
||||
if (source.hasSVar("AIPriorityModifier")) {
|
||||
p += Integer.parseInt(source.getSVar("AIPriorityModifier"));
|
||||
}
|
||||
if (ComputerUtilCard.isCardRemAIDeck(sa.getOriginalHost() != null ? sa.getOriginalHost() : source)) {
|
||||
p -= 10;
|
||||
}
|
||||
// don't play equipments before having any creatures
|
||||
if (source.isEquipment() && noCreatures) {
|
||||
p -= 9;
|
||||
}
|
||||
// don't equip stuff in main 2 if there's more stuff to cast at the moment
|
||||
if (sa.getApi() == ApiType.Attach && !sa.isCurse() && source.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
|
||||
p -= 1;
|
||||
}
|
||||
// 1. increase chance of using Surge effects
|
||||
// 2. non-surged versions are usually inefficient
|
||||
if (source.getOracleText().contains("surge cost") && !sa.isSurged()) {
|
||||
p -= 9;
|
||||
}
|
||||
// move snap-casted spells to front
|
||||
if (source.isInZone(ZoneType.Graveyard)) {
|
||||
if (sa.getMayPlay() != null && source.mayPlay(sa.getMayPlay()) != null) {
|
||||
p += 50;
|
||||
}
|
||||
}
|
||||
// if the profile specifies it, deprioritize Storm spells in an attempt to build up storm count
|
||||
if (source.hasKeyword(Keyword.STORM) && ai.getController() instanceof PlayerControllerAi) {
|
||||
p -= (((PlayerControllerAi) ai.getController()).getAi().getIntProperty(AiProps.PRIORITY_REDUCTION_FOR_STORM_SPELLS));
|
||||
}
|
||||
}
|
||||
|
||||
// use Surge and Prowl costs when able to
|
||||
if (sa.isSurged() || sa.isProwl()) {
|
||||
p += 9;
|
||||
}
|
||||
// sort planeswalker abilities with most costly first
|
||||
if (sa.isPwAbility()) {
|
||||
final CostPart cost = sa.getPayCosts().getCostParts().get(0);
|
||||
if (cost instanceof CostRemoveCounter) {
|
||||
p += cost.convertAmount() == null ? 1 : cost.convertAmount();
|
||||
} else if (cost instanceof CostPutCounter) {
|
||||
p -= cost.convertAmount();
|
||||
}
|
||||
if (sa.hasParam("Ultimate")) {
|
||||
p += 9;
|
||||
}
|
||||
}
|
||||
|
||||
if (ApiType.DestroyAll == sa.getApi()) {
|
||||
p += 4;
|
||||
} else if (ApiType.Mana == sa.getApi()) {
|
||||
p -= 9;
|
||||
}
|
||||
|
||||
// try to cast mana ritual spells before casting spells to maximize potential mana
|
||||
if ("ManaRitual".equals(sa.getParam("AILogic"))) {
|
||||
p += 9;
|
||||
}
|
||||
|
||||
return p;
|
||||
}
|
||||
};
|
||||
|
||||
public CardCollection getCardsToDiscard(final int numDiscard, final String[] uTypes, final SpellAbility sa) {
|
||||
return getCardsToDiscard(numDiscard, uTypes, sa, CardCollection.EMPTY);
|
||||
}
|
||||
@@ -1744,10 +1584,11 @@ public class AiController {
|
||||
return null;
|
||||
|
||||
try {
|
||||
Collections.sort(all, saComparator); // put best spells first
|
||||
Collections.sort(all, ComputerUtilAbility.saEvaluator); // put best spells first
|
||||
ComputerUtilAbility.sortCreatureSpells(all);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
System.err.println(ex.getMessage());
|
||||
String assertex = ComparatorUtil.verifyTransitivity(saComparator, all);
|
||||
String assertex = ComparatorUtil.verifyTransitivity(ComputerUtilAbility.saEvaluator, all);
|
||||
Sentry.captureMessage(ex.getMessage() + "\nAssertionError [verifyTransitivity]: " + assertex);
|
||||
}
|
||||
|
||||
@@ -2292,14 +2133,14 @@ public class AiController {
|
||||
}
|
||||
|
||||
// TODO move to more common place
|
||||
private <T> List<T> filterList(List<T> input, Predicate<? super T> pred) {
|
||||
private static <T> List<T> filterList(List<T> input, Predicate<? super T> pred) {
|
||||
List<T> filtered = Lists.newArrayList(Iterables.filter(input, pred));
|
||||
input.removeAll(filtered);
|
||||
return filtered;
|
||||
}
|
||||
|
||||
// TODO move to more common place
|
||||
private List<SpellAbility> filterListByApi(List<SpellAbility> input, ApiType type) {
|
||||
public static List<SpellAbility> filterListByApi(List<SpellAbility> input, ApiType type) {
|
||||
return filterList(input, SpellAbilityPredicates.isApi(type));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package forge.ai;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
@@ -15,6 +17,12 @@ import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardCollectionView;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates.Presets;
|
||||
import forge.game.cost.CostPart;
|
||||
import forge.game.cost.CostPayEnergy;
|
||||
import forge.game.cost.CostPutCounter;
|
||||
import forge.game.cost.CostRemoveCounter;
|
||||
import forge.game.keyword.Keyword;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.OptionalCostValue;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
@@ -223,4 +231,183 @@ public class ComputerUtilAbility {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public final static saComparator saEvaluator = new saComparator();
|
||||
|
||||
// not sure "playing biggest spell" matters?
|
||||
public final static class saComparator implements Comparator<SpellAbility> {
|
||||
@Override
|
||||
public int compare(final SpellAbility a, final SpellAbility b) {
|
||||
return compareEvaluator(a, b, false);
|
||||
}
|
||||
public int compareEvaluator(final SpellAbility a, final SpellAbility b, boolean safeToEvaluateCreatures) {
|
||||
// sort from highest cost to lowest
|
||||
// we want the highest costs first
|
||||
int a1 = a.getPayCosts().getTotalMana().getCMC();
|
||||
int b1 = b.getPayCosts().getTotalMana().getCMC();
|
||||
|
||||
// deprioritize SAs explicitly marked as preferred to be activated last compared to all other SAs
|
||||
if (a.hasParam("AIActivateLast") && !b.hasParam("AIActivateLast")) {
|
||||
return 1;
|
||||
} else if (b.hasParam("AIActivateLast") && !a.hasParam("AIActivateLast")) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// deprioritize planar die roll marked with AIRollPlanarDieParams:LowPriority$ True
|
||||
if (ApiType.RollPlanarDice == a.getApi() && a.getHostCard() != null && a.getHostCard().hasSVar("AIRollPlanarDieParams") && a.getHostCard().getSVar("AIRollPlanarDieParams").toLowerCase().matches(".*lowpriority\\$\\s*true.*")) {
|
||||
return 1;
|
||||
} else if (ApiType.RollPlanarDice == b.getApi() && b.getHostCard() != null && b.getHostCard().hasSVar("AIRollPlanarDieParams") && b.getHostCard().getSVar("AIRollPlanarDieParams").toLowerCase().matches(".*lowpriority\\$\\s*true.*")) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// deprioritize pump spells with pure energy cost (can be activated last,
|
||||
// since energy is generally scarce, plus can benefit e.g. Electrostatic Pummeler)
|
||||
int a2 = 0, b2 = 0;
|
||||
if (a.getApi() == ApiType.Pump && a.getPayCosts().getCostEnergy() != null) {
|
||||
if (a.getPayCosts().hasOnlySpecificCostType(CostPayEnergy.class)) {
|
||||
a2 = a.getPayCosts().getCostEnergy().convertAmount();
|
||||
}
|
||||
}
|
||||
if (b.getApi() == ApiType.Pump && b.getPayCosts().getCostEnergy() != null) {
|
||||
if (b.getPayCosts().hasOnlySpecificCostType(CostPayEnergy.class)) {
|
||||
b2 = b.getPayCosts().getCostEnergy().convertAmount();
|
||||
}
|
||||
}
|
||||
if (a2 == 0 && b2 > 0) {
|
||||
return -1;
|
||||
} else if (b2 == 0 && a2 > 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// cast 0 mana cost spells first (might be a Mox)
|
||||
if (a1 == 0 && b1 > 0 && ApiType.Mana != a.getApi()) {
|
||||
return -1;
|
||||
} else if (a1 > 0 && b1 == 0 && ApiType.Mana != b.getApi()) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (a.getHostCard() != null && a.getHostCard().hasSVar("FreeSpellAI")) {
|
||||
return -1;
|
||||
} else if (b.getHostCard() != null && b.getHostCard().hasSVar("FreeSpellAI")) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (a.getHostCard().equals(b.getHostCard()) && a.getApi() == b.getApi()) {
|
||||
// Cheaper Spectacle costs should be preferred
|
||||
// FIXME: Any better way to identify that these are the same ability, one with Spectacle and one not?
|
||||
// (looks like it's not a full-fledged alternative cost as such, and is not processed with other alt costs)
|
||||
if (a.isSpectacle() && !b.isSpectacle() && a1 < b1) {
|
||||
return 1;
|
||||
} else if (b.isSpectacle() && !a.isSpectacle() && b1 < a1) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
a1 += getSpellAbilityPriority(a);
|
||||
b1 += getSpellAbilityPriority(b);
|
||||
|
||||
// If both are creature spells sort them after
|
||||
if (safeToEvaluateCreatures) {
|
||||
a1 += Math.round(ComputerUtilCard.evaluateCreature(a) / 100f);
|
||||
b1 += Math.round(ComputerUtilCard.evaluateCreature(b) / 100f);
|
||||
}
|
||||
|
||||
return b1 - a1;
|
||||
}
|
||||
|
||||
private static int getSpellAbilityPriority(SpellAbility sa) {
|
||||
int p = 0;
|
||||
Card source = sa.getHostCard();
|
||||
final Player ai = source == null ? sa.getActivatingPlayer() : source.getController();
|
||||
if (ai == null) {
|
||||
System.err.println("Error: couldn't figure out the activating player and host card for SA: " + sa);
|
||||
return 0;
|
||||
}
|
||||
final boolean noCreatures = ai.getCreaturesInPlay().isEmpty();
|
||||
|
||||
if (source != null) {
|
||||
// puts creatures in front of spells
|
||||
if (source.isCreature()) {
|
||||
p += 1;
|
||||
}
|
||||
if (source.hasSVar("AIPriorityModifier")) {
|
||||
p += Integer.parseInt(source.getSVar("AIPriorityModifier"));
|
||||
}
|
||||
if (ComputerUtilCard.isCardRemAIDeck(sa.getOriginalHost() != null ? sa.getOriginalHost() : source)) {
|
||||
p -= 10;
|
||||
}
|
||||
// don't play equipments before having any creatures
|
||||
if (source.isEquipment() && noCreatures) {
|
||||
p -= 9;
|
||||
}
|
||||
// don't equip stuff in main 2 if there's more stuff to cast at the moment
|
||||
if (sa.getApi() == ApiType.Attach && !sa.isCurse() && source.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
|
||||
p -= 1;
|
||||
}
|
||||
// 1. increase chance of using Surge effects
|
||||
// 2. non-surged versions are usually inefficient
|
||||
if (source.getOracleText().contains("surge cost") && !sa.isSurged()) {
|
||||
p -= 9;
|
||||
}
|
||||
// move snap-casted spells to front
|
||||
if (source.isInZone(ZoneType.Graveyard)) {
|
||||
if (sa.getMayPlay() != null && source.mayPlay(sa.getMayPlay()) != null) {
|
||||
p += 50;
|
||||
}
|
||||
}
|
||||
// if the profile specifies it, deprioritize Storm spells in an attempt to build up storm count
|
||||
if (source.hasKeyword(Keyword.STORM) && ai.getController() instanceof PlayerControllerAi) {
|
||||
p -= (((PlayerControllerAi) ai.getController()).getAi().getIntProperty(AiProps.PRIORITY_REDUCTION_FOR_STORM_SPELLS));
|
||||
}
|
||||
}
|
||||
|
||||
// use Surge and Prowl costs when able to
|
||||
if (sa.isSurged() || sa.isProwl()) {
|
||||
p += 9;
|
||||
}
|
||||
// sort planeswalker abilities with most costly first
|
||||
if (sa.isPwAbility()) {
|
||||
final CostPart cost = sa.getPayCosts().getCostParts().get(0);
|
||||
if (cost instanceof CostRemoveCounter) {
|
||||
p += cost.convertAmount() == null ? 1 : cost.convertAmount();
|
||||
} else if (cost instanceof CostPutCounter) {
|
||||
p -= cost.convertAmount();
|
||||
}
|
||||
if (sa.hasParam("Ultimate")) {
|
||||
p += 9;
|
||||
}
|
||||
}
|
||||
|
||||
if (ApiType.DestroyAll == sa.getApi()) {
|
||||
p += 4;
|
||||
} else if (ApiType.Mana == sa.getApi()) {
|
||||
p -= 9;
|
||||
}
|
||||
|
||||
// try to cast mana ritual spells before casting spells to maximize potential mana
|
||||
if ("ManaRitual".equals(sa.getParam("AILogic"))) {
|
||||
p += 9;
|
||||
}
|
||||
|
||||
return p;
|
||||
}
|
||||
};
|
||||
|
||||
public static List<SpellAbility> sortCreatureSpells(final List<SpellAbility> all) {
|
||||
// try to smoothen power creep by making CMC less of a factor
|
||||
final List<SpellAbility> creatures = AiController.filterListByApi(Lists.newArrayList(all), ApiType.PermanentCreature);
|
||||
if (creatures.size() <= 1) {
|
||||
return all;
|
||||
}
|
||||
// TODO this doesn't account for nearly identical creatures where one is a newer but more cost efficient variant
|
||||
Collections.sort(creatures, ComputerUtilCard.EvaluateCreatureSpellComparator);
|
||||
int idx = 0;
|
||||
for (int i = 0; i < all.size(); i++) {
|
||||
if (all.get(i).getApi() == ApiType.PermanentCreature) {
|
||||
all.set(i, creatures.get(idx));
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
return all;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -567,6 +567,13 @@ public class ComputerUtilCard {
|
||||
return evaluateCreature(b) - evaluateCreature(a);
|
||||
}
|
||||
};
|
||||
public static final Comparator<SpellAbility> EvaluateCreatureSpellComparator = new Comparator<SpellAbility>() {
|
||||
@Override
|
||||
public int compare(final SpellAbility a, final SpellAbility b) {
|
||||
// TODO ideally we could reuse the value from the previous pass with false
|
||||
return ComputerUtilAbility.saEvaluator.compareEvaluator(a, b, true);
|
||||
}
|
||||
};
|
||||
|
||||
private static final CreatureEvaluator creatureEvaluator = new CreatureEvaluator();
|
||||
private static final LandEvaluator landEvaluator = new LandEvaluator();
|
||||
@@ -596,7 +603,7 @@ public class ComputerUtilCard {
|
||||
host.setState(sa.getCardStateName(), false);
|
||||
}
|
||||
|
||||
int eval = creatureEvaluator.evaluateCreature(host);
|
||||
int eval = evaluateCreature(host);
|
||||
|
||||
if (currentState != null) {
|
||||
host.setState(currentState, false);
|
||||
|
||||
@@ -471,6 +471,13 @@ public class CountersPutAi extends CountersAi {
|
||||
if (sacSelf && c.equals(source)) {
|
||||
return false;
|
||||
}
|
||||
if ("NoCounterOfType".equals(sa.getParam("AILogic"))) {
|
||||
for (String ctrType : types) {
|
||||
if (c.getCounters(CounterType.getType(ctrType)) > 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return sa.canTarget(c) && c.canReceiveCounters(CounterType.getType(type));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -590,9 +590,7 @@ public class GameAction {
|
||||
// 400.7g try adding keyword back into card if it doesn't already have it
|
||||
if (zoneTo.is(ZoneType.Stack) && cause != null && cause.isSpell() && !cause.isIntrinsic() && c.equals(cause.getHostCard())) {
|
||||
if (cause.getKeyword() != null && !copied.getKeywords().contains(cause.getKeyword())) {
|
||||
copied.addChangedCardKeywordsInternal(ImmutableList.of(cause.getKeyword()), null, false, game.getNextTimestamp(), 0, false);
|
||||
// update Keyword Cache
|
||||
copied.updateKeywords();
|
||||
copied.addChangedCardKeywordsInternal(ImmutableList.of(cause.getKeyword()), null, false, game.getNextTimestamp(), 0, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -636,10 +636,8 @@ public abstract class SpellAbilityEffect {
|
||||
combat.initConstraints();
|
||||
if (sa.hasParam("ChoosePlayerOrPlaneswalker")) {
|
||||
PlayerCollection defendingPlayers = AbilityUtils.getDefinedPlayers(sa.hasParam("ForEach") ? c : host, attacking, sa);
|
||||
defs = new FCollection<>();
|
||||
for (Player p : defendingPlayers) {
|
||||
defs.addAll(combat.getDefendersControlledBy(p));
|
||||
}
|
||||
defs = new FCollection<>(defendingPlayers);
|
||||
defs.addAll(Iterables.filter(combat.getDefendingPlaneswalkers(), CardPredicates.isControlledByAnyOf(defendingPlayers)));
|
||||
} else if ("True".equalsIgnoreCase(attacking)) {
|
||||
defs = (FCollection<GameEntity>) combat.getDefenders();
|
||||
} else {
|
||||
|
||||
@@ -66,7 +66,7 @@ public class ConniveEffect extends SpellAbilityEffect {
|
||||
|
||||
for (final Player p : controllers) {
|
||||
CardCollection connivers = CardLists.filterControlledBy(toConnive, p);
|
||||
while (connivers.size() > 0) {
|
||||
while (!connivers.isEmpty()) {
|
||||
GameEntityCounterTable table = new GameEntityCounterTable();
|
||||
final CardZoneTable triggerList = new CardZoneTable();
|
||||
Map<Player, CardCollectionView> discardedMap = Maps.newHashMap();
|
||||
@@ -76,18 +76,18 @@ public class ConniveEffect extends SpellAbilityEffect {
|
||||
|
||||
Card conniver = connivers.size() > 1 ? p.getController().chooseSingleEntityForEffect(connivers, sa,
|
||||
Localizer.getInstance().getMessage("lblChooseConniver"), null) : connivers.get(0);
|
||||
connivers.remove(conniver);
|
||||
|
||||
p.drawCards(num, sa, moveParams);
|
||||
|
||||
CardCollection validDisards =
|
||||
CardLists.filter(p.getCardsIn(ZoneType.Hand), CardPredicates.Presets.NON_TOKEN);
|
||||
if (validDisards.isEmpty() || !p.canDiscardBy(sa, true)) { // hand being empty unlikely, just to be safe
|
||||
CardCollection validDiscards = CardLists.filter(p.getCardsIn(ZoneType.Hand), CardPredicates.Presets.NON_TOKEN);
|
||||
if (validDiscards.isEmpty() || !p.canDiscardBy(sa, true)) { // hand being empty unlikely, just to be safe
|
||||
continue;
|
||||
}
|
||||
|
||||
int amt = Math.min(validDisards.size(), num);
|
||||
int amt = Math.min(validDiscards.size(), num);
|
||||
CardCollectionView toBeDiscarded = amt == 0 ? CardCollection.EMPTY :
|
||||
p.getController().chooseCardsToDiscardFrom(p, sa, validDisards, amt, amt);
|
||||
p.getController().chooseCardsToDiscardFrom(p, sa, validDiscards, amt, amt);
|
||||
|
||||
if (toBeDiscarded.size() > 1) {
|
||||
toBeDiscarded = GameActionUtil.orderCardsByTheirOwners(game, toBeDiscarded, ZoneType.Graveyard, sa);
|
||||
@@ -101,7 +101,6 @@ public class ConniveEffect extends SpellAbilityEffect {
|
||||
if (game.getZoneOf(gamec).is(ZoneType.Battlefield) && gamec.equalsWithTimestamp(conniver)) {
|
||||
conniver.addCounter(CounterEnumType.P1P1, numCntrs, p, table);
|
||||
}
|
||||
connivers.remove(conniver);
|
||||
discardedMap.put(p, CardCollection.getView(toBeDiscarded));
|
||||
discard(sa, triggerList, true, discardedMap, moveParams);
|
||||
table.replaceCounterEffect(game, sa, true);
|
||||
|
||||
@@ -4721,7 +4721,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
|
||||
final List<KeywordInterface> keywords, final List<KeywordInterface> removeKeywords,
|
||||
final boolean removeAllKeywords,
|
||||
final long timestamp, final long staticId, final boolean updateView) {
|
||||
|
||||
final KeywordsChange newCks = new KeywordsChange(keywords, removeKeywords, removeAllKeywords);
|
||||
changedCardKeywords.put(timestamp, staticId, newCks);
|
||||
|
||||
|
||||
@@ -124,62 +124,49 @@ public class CardFactory {
|
||||
final Card source = sourceSA.getHostCard();
|
||||
final Card original = targetSA.getHostCard();
|
||||
final Game game = source.getGame();
|
||||
final Card c = new Card(game.nextCardId(), original.getPaperCard(), game);
|
||||
copyCopiableCharacteristics(original, c, sourceSA, targetSA);
|
||||
int id = game.nextCardId();
|
||||
|
||||
if (sourceSA.hasParam("NonLegendary")) {
|
||||
c.removeType(CardType.Supertype.Legendary);
|
||||
}
|
||||
// need to create a physical card first, i need the original card faces
|
||||
final Card copy = CardFactory.getCard(original.getPaperCard(), controller, id, game);
|
||||
|
||||
if (sourceSA.hasParam("CopySetPower")) {
|
||||
c.setBasePower(Integer.parseInt(sourceSA.getParam("CopySetPower")));
|
||||
}
|
||||
|
||||
if (sourceSA.hasParam("CopySetToughness")) {
|
||||
c.setBaseToughness(Integer.parseInt(sourceSA.getParam("CopySetToughness")));
|
||||
}
|
||||
|
||||
if (sourceSA.hasParam("CopySetLoyalty")) {
|
||||
c.setBaseLoyalty(AbilityUtils.calculateAmount(source, sourceSA.getParam("CopySetLoyalty"), sourceSA));
|
||||
}
|
||||
|
||||
if (sourceSA.hasParam("CopyAddTypes")) {
|
||||
c.addType(Arrays.asList(sourceSA.getParam("CopyAddTypes").split(" & ")));
|
||||
}
|
||||
|
||||
// change the color of the copy (eg: Fork)
|
||||
if (sourceSA.hasParam("CopyIsColor")) {
|
||||
ColorSet finalColors;
|
||||
final String newColor = sourceSA.getParam("CopyIsColor");
|
||||
if (newColor.equals("ChosenColor")) {
|
||||
finalColors = ColorSet.fromNames(source.getChosenColors());
|
||||
} else {
|
||||
finalColors = ColorSet.fromNames(newColor.split(","));
|
||||
if (original.isTransformable()) {
|
||||
// 707.8a If an effect creates a token that is a copy of a transforming permanent or a transforming double-faced card not on the battlefield,
|
||||
// the resulting token is a transforming token that has both a front face and a back face.
|
||||
// The characteristics of each face are determined by the copiable values of the same face of the permanent it is a copy of, as modified by any other copy effects that apply to that permanent.
|
||||
// If the token is a copy of a transforming permanent with its back face up, the token enters the battlefield with its back face up.
|
||||
// This rule does not apply to tokens that are created with their own set of characteristics and enter the battlefield as a copy of a transforming permanent due to a replacement effect.
|
||||
copy.setBackSide(original.isBackSide());
|
||||
if (original.isTransformed()) {
|
||||
copy.incrementTransformedTimestamp();
|
||||
}
|
||||
|
||||
c.addColor(finalColors, !sourceSA.hasParam("OverwriteColors"), c.getTimestamp(), 0, false);
|
||||
}
|
||||
|
||||
c.clearControllers();
|
||||
c.setOwner(controller);
|
||||
c.setCopiedSpell(true);
|
||||
c.setCopiedPermanent(original);
|
||||
copy.setStates(getCloneStates(original, copy, sourceSA));
|
||||
// force update the now set State
|
||||
if (original.isTransformable()) {
|
||||
copy.setState(original.isTransformed() ? CardStateName.Transformed : CardStateName.Original, true, true);
|
||||
} else {
|
||||
copy.setState(copy.getCurrentStateName(), true, true);
|
||||
}
|
||||
|
||||
c.setXManaCostPaidByColor(original.getXManaCostPaidByColor());
|
||||
c.setKickerMagnitude(original.getKickerMagnitude());
|
||||
copy.setCopiedSpell(true);
|
||||
copy.setCopiedPermanent(original);
|
||||
|
||||
copy.setXManaCostPaidByColor(original.getXManaCostPaidByColor());
|
||||
copy.setKickerMagnitude(original.getKickerMagnitude());
|
||||
|
||||
for (OptionalCost cost : original.getOptionalCostsPaid()) {
|
||||
c.addOptionalCostPaid(cost);
|
||||
copy.addOptionalCostPaid(cost);
|
||||
}
|
||||
if (targetSA.isBestow()) {
|
||||
c.animateBestow();
|
||||
copy.animateBestow();
|
||||
}
|
||||
|
||||
if (sourceSA.hasParam("RememberNewCard")) {
|
||||
source.addRemembered(c);
|
||||
source.addRemembered(copy);
|
||||
}
|
||||
|
||||
return c;
|
||||
return copy;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -525,6 +512,7 @@ public class CardFactory {
|
||||
* @param from the {@link Card} to copy from.
|
||||
* @param to the {@link Card} to copy to.
|
||||
*/
|
||||
@Deprecated
|
||||
public static void copyCopiableCharacteristics(final Card from, final Card to, SpellAbility sourceSA, SpellAbility targetSA) {
|
||||
final boolean toIsFaceDown = to.isFaceDown();
|
||||
if (toIsFaceDown) {
|
||||
@@ -753,7 +741,10 @@ public class CardFactory {
|
||||
final CardState ret2 = new CardState(out, CardStateName.Adventure);
|
||||
ret2.copyFrom(in.getState(CardStateName.Adventure), false, sa);
|
||||
result.put(CardStateName.Adventure, ret2);
|
||||
} else if (in.isTransformable() && sa instanceof SpellAbility && ApiType.CopyPermanent.equals(((SpellAbility)sa).getApi())) {
|
||||
} else if (in.isTransformable() && sa instanceof SpellAbility && (
|
||||
ApiType.CopyPermanent.equals(((SpellAbility)sa).getApi()) ||
|
||||
ApiType.CopySpellAbility.equals(((SpellAbility)sa).getApi())
|
||||
)) {
|
||||
// CopyPermanent can copy token
|
||||
final CardState ret1 = new CardState(out, CardStateName.Original);
|
||||
ret1.copyFrom(in.getState(CardStateName.Original), false, sa);
|
||||
@@ -820,7 +811,7 @@ public class CardFactory {
|
||||
}
|
||||
|
||||
if (state.getType().isPlaneswalker() && sa.hasParam("SetLoyalty")) {
|
||||
state.setBaseLoyalty(String.valueOf(sa.getParam("SetLoyalty")));
|
||||
state.setBaseLoyalty(String.valueOf(AbilityUtils.calculateAmount(host, sa.getParam("SetLoyalty"), sa)));
|
||||
}
|
||||
|
||||
// Planning a Vizier of Many Faces rework; always might come in handy
|
||||
|
||||
@@ -239,7 +239,7 @@ public class CombatUtil {
|
||||
if (!defender.equals(ge) && ge instanceof Player) {
|
||||
// found a player which does not goad that creature
|
||||
// and creature can attack this player or planeswalker
|
||||
if (!attacker.isGoadedBy((Player) ge) && canAttack(attacker, ge)) {
|
||||
if (!attacker.isGoadedBy((Player) ge) && !ge.hasKeyword("Creatures your opponents control attack a player other than you if able.") && canAttack(attacker, ge)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -251,7 +251,7 @@ public class CombatUtil {
|
||||
if (defender != null && defender.hasKeyword("Creatures your opponents control attack a player other than you if able.")) {
|
||||
for (GameEntity ge : getAllPossibleDefenders(attacker.getController())) {
|
||||
if (!defender.equals(ge) && ge instanceof Player) {
|
||||
if (canAttack(attacker, ge)) {
|
||||
if (!ge.hasKeyword("Creatures your opponents control attack a player other than you if able.") && canAttack(attacker, ge)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
<object id="55" template="../../obj/gate.tx" x="368" y="640">
|
||||
<properties>
|
||||
<property name="dialog">[{
|
||||
"text":"A gate is blocking the path. I looks like it is open elsewhere",
|
||||
"text":"A gate is blocking the path. It looks like it is opened elsewhere.",
|
||||
"options":[
|
||||
{ "name":"continue" }
|
||||
]
|
||||
@@ -84,12 +84,12 @@
|
||||
<properties>
|
||||
<property name="dialog">[
|
||||
{
|
||||
"text":"Hmm a big switch is embedded into the wall",
|
||||
"text":"Hmm... A big switch is embedded into the wall.",
|
||||
"options":[
|
||||
{
|
||||
"text":"You hear some loud sounds from the center",
|
||||
"text":"You hear some loud sounds coming from the center.",
|
||||
"action":[{"deleteMapObject":-1},{"advanceMapFlag":"gate"}],
|
||||
"name":"flip the switch"
|
||||
"name":"flip the switch"
|
||||
"options":[{
|
||||
"condition":[{"getMapFlag":{"key":"gate","op":">=","val":3}}],
|
||||
"action":[{"deleteMapObject":55}],
|
||||
@@ -106,12 +106,12 @@
|
||||
<properties>
|
||||
<property name="dialog">[
|
||||
{
|
||||
"text":"Hmm a big switch is embedded into the wall",
|
||||
"text":"Hmm... A big switch is embedded into the wall.",
|
||||
"options":[
|
||||
{
|
||||
"text":"You hear some loud sounds from the center",
|
||||
"text":"You hear some loud sounds coming from the center.",
|
||||
"action":[{"deleteMapObject":-1},{"advanceMapFlag":"gate"}],
|
||||
"name":"flip the switch"
|
||||
"name":"flip the switch"
|
||||
"options":[{
|
||||
"condition":[{"getMapFlag":{"key":"gate","op":">=","val":3}}],
|
||||
"action":[{"deleteMapObject":55}],
|
||||
@@ -128,12 +128,12 @@
|
||||
<properties>
|
||||
<property name="dialog">[
|
||||
{
|
||||
"text":"Hmm a big switch is embedded into the wall",
|
||||
"text":"Hmm... A big switch is embedded into the wall.",
|
||||
"options":[
|
||||
{
|
||||
"text":"You hear some loud sounds from the center",
|
||||
"text":"You hear some loud sounds coming from the center.",
|
||||
"action":[{"deleteMapObject":-1},{"advanceMapFlag":"gate"}],
|
||||
"name":"flip the switch"
|
||||
"name":"flip the switch"
|
||||
"options":[{
|
||||
"condition":[{"getMapFlag":{"key":"gate","op":">=","val":3}}],
|
||||
"action":[{"deleteMapObject":55}],
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
<object id="55" template="../../obj/gate.tx" x="368" y="640">
|
||||
<properties>
|
||||
<property name="dialog">[{
|
||||
"text":"A gate is blocking the path. I looks like it is open elsewhere",
|
||||
"text":"A gate is blocking the path. It looks like it is opened elsewhere.",
|
||||
"options":[
|
||||
{ "name":"continue" }
|
||||
]
|
||||
@@ -84,12 +84,12 @@
|
||||
<properties>
|
||||
<property name="dialog">[
|
||||
{
|
||||
"text":"Hmm a big switch is embedded into the wall",
|
||||
"text":"Hmm... A big switch is embedded into the wall.",
|
||||
"options":[
|
||||
{
|
||||
"text":"You hear some loud sounds from the center",
|
||||
"text":"You hear some loud sounds coming from the center.",
|
||||
"action":[{"deleteMapObject":-1},{"advanceMapFlag":"gate"}],
|
||||
"name":"flip the switch"
|
||||
"name":"flip the switch"
|
||||
"options":[{
|
||||
"condition":[{"getMapFlag":{"key":"gate","op":">=","val":3}}],
|
||||
"action":[{"deleteMapObject":55}],
|
||||
@@ -106,12 +106,12 @@
|
||||
<properties>
|
||||
<property name="dialog">[
|
||||
{
|
||||
"text":"Hmm a big switch is embedded into the wall",
|
||||
"text":"Hmm... A big switch is embedded into the wall.",
|
||||
"options":[
|
||||
{
|
||||
"text":"You hear some loud sounds from the center",
|
||||
"text":"You hear some loud sounds coming from the center.",
|
||||
"action":[{"deleteMapObject":-1},{"advanceMapFlag":"gate"}],
|
||||
"name":"flip the switch"
|
||||
"name":"flip the switch"
|
||||
"options":[{
|
||||
"condition":[{"getMapFlag":{"key":"gate","op":">=","val":3}}],
|
||||
"action":[{"deleteMapObject":55}],
|
||||
@@ -128,12 +128,12 @@
|
||||
<properties>
|
||||
<property name="dialog">[
|
||||
{
|
||||
"text":"Hmm a big switch is embedded into the wall",
|
||||
"text":"Hmm... A big switch is embedded into the wall.",
|
||||
"options":[
|
||||
{
|
||||
"text":"You hear some loud sounds from the center",
|
||||
"text":"You hear some loud sounds coming from the center.",
|
||||
"action":[{"deleteMapObject":-1},{"advanceMapFlag":"gate"}],
|
||||
"name":"flip the switch"
|
||||
"name":"flip the switch"
|
||||
"options":[{
|
||||
"condition":[{"getMapFlag":{"key":"gate","op":">=","val":3}}],
|
||||
"action":[{"deleteMapObject":55}],
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
<object id="55" template="../../obj/gate.tx" x="368" y="640">
|
||||
<properties>
|
||||
<property name="dialog">[{
|
||||
"text":"A gate is blocking the path. I looks like it is open elsewhere",
|
||||
"text":"A gate is blocking the path. It looks like it is opened elsewhere.",
|
||||
"options":[
|
||||
{ "name":"continue" }
|
||||
]
|
||||
@@ -84,12 +84,12 @@
|
||||
<properties>
|
||||
<property name="dialog">[
|
||||
{
|
||||
"text":"Hmm a big switch is embedded into the wall",
|
||||
"text":"Hmm... A big switch is embedded into the wall.",
|
||||
"options":[
|
||||
{
|
||||
"text":"You hear some loud sounds from the center",
|
||||
"text":"You hear some loud sounds coming from the center.",
|
||||
"action":[{"deleteMapObject":-1},{"advanceMapFlag":"gate"}],
|
||||
"name":"flip the switch"
|
||||
"name":"flip the switch"
|
||||
"options":[{
|
||||
"condition":[{"getMapFlag":{"key":"gate","op":">=","val":3}}],
|
||||
"action":[{"deleteMapObject":55}],
|
||||
@@ -106,12 +106,12 @@
|
||||
<properties>
|
||||
<property name="dialog">[
|
||||
{
|
||||
"text":"Hmm a big switch is embedded into the wall",
|
||||
"text":"Hmm... A big switch is embedded into the wall.",
|
||||
"options":[
|
||||
{
|
||||
"text":"You hear some loud sounds from the center",
|
||||
"text":"You hear some loud sounds coming from the center.",
|
||||
"action":[{"deleteMapObject":-1},{"advanceMapFlag":"gate"}],
|
||||
"name":"flip the switch"
|
||||
"name":"flip the switch"
|
||||
"options":[{
|
||||
"condition":[{"getMapFlag":{"key":"gate","op":">=","val":3}}],
|
||||
"action":[{"deleteMapObject":55}],
|
||||
@@ -128,12 +128,12 @@
|
||||
<properties>
|
||||
<property name="dialog">[
|
||||
{
|
||||
"text":"Hmm a big switch is embedded into the wall",
|
||||
"text":"Hmm... A big switch is embedded into the wall.",
|
||||
"options":[
|
||||
{
|
||||
"text":"You hear some loud sounds from the center",
|
||||
"text":"You hear some loud sounds coming from the center.",
|
||||
"action":[{"deleteMapObject":-1},{"advanceMapFlag":"gate"}],
|
||||
"name":"flip the switch"
|
||||
"name":"flip the switch"
|
||||
"options":[{
|
||||
"condition":[{"getMapFlag":{"key":"gate","op":">=","val":3}}],
|
||||
"action":[{"deleteMapObject":55}],
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
<object id="55" template="../../obj/gate.tx" x="368" y="640">
|
||||
<properties>
|
||||
<property name="dialog">[{
|
||||
"text":"A gate is blocking the path. I looks like it is open elsewhere",
|
||||
"text":"A gate is blocking the path. It looks like it is opened elsewhere.",
|
||||
"options":[
|
||||
{ "name":"continue" }
|
||||
]
|
||||
@@ -84,12 +84,12 @@
|
||||
<properties>
|
||||
<property name="dialog">[
|
||||
{
|
||||
"text":"Hmm a big switch is embedded into the wall",
|
||||
"text":"Hmm... A big switch is embedded into the wall.",
|
||||
"options":[
|
||||
{
|
||||
"text":"You hear some loud sounds from the center",
|
||||
"text":"You hear some loud sounds coming from the center.",
|
||||
"action":[{"deleteMapObject":-1},{"advanceMapFlag":"gate"}],
|
||||
"name":"flip the switch"
|
||||
"name":"flip the switch"
|
||||
"options":[{
|
||||
"condition":[{"getMapFlag":{"key":"gate","op":">=","val":3}}],
|
||||
"action":[{"deleteMapObject":55}],
|
||||
@@ -106,12 +106,12 @@
|
||||
<properties>
|
||||
<property name="dialog">[
|
||||
{
|
||||
"text":"Hmm a big switch is embedded into the wall",
|
||||
"text":"Hmm... A big switch is embedded into the wall.",
|
||||
"options":[
|
||||
{
|
||||
"text":"You hear some loud sounds from the center",
|
||||
"text":"You hear some loud sounds coming from the center.",
|
||||
"action":[{"deleteMapObject":-1},{"advanceMapFlag":"gate"}],
|
||||
"name":"flip the switch"
|
||||
"name":"flip the switch"
|
||||
"options":[{
|
||||
"condition":[{"getMapFlag":{"key":"gate","op":">=","val":3}}],
|
||||
"action":[{"deleteMapObject":55}],
|
||||
@@ -128,12 +128,12 @@
|
||||
<properties>
|
||||
<property name="dialog">[
|
||||
{
|
||||
"text":"Hmm a big switch is embedded into the wall",
|
||||
"text":"Hmm... A big switch is embedded into the wall.",
|
||||
"options":[
|
||||
{
|
||||
"text":"You hear some loud sounds from the center",
|
||||
"text":"You hear some loud sounds coming from the center.",
|
||||
"action":[{"deleteMapObject":-1},{"advanceMapFlag":"gate"}],
|
||||
"name":"flip the switch"
|
||||
"name":"flip the switch"
|
||||
"options":[{
|
||||
"condition":[{"getMapFlag":{"key":"gate","op":">=","val":3}}],
|
||||
"action":[{"deleteMapObject":55}],
|
||||
|
||||
@@ -357,12 +357,12 @@
|
||||
<properties>
|
||||
<property name="dialog">[
|
||||
{
|
||||
"text":"Hmm a big switch is embedded into the wall",
|
||||
"text":"Hmm... A big switch is embedded into the wall.",
|
||||
"options":[
|
||||
{
|
||||
"text":"You hear some loud sounds from the east",
|
||||
"text":"You hear some loud sounds coming from the east",
|
||||
"action":[{"deleteMapObject":-1},{"advanceMapFlag":"gate"}],
|
||||
"name":"flip the switch"
|
||||
"name":"flip the switch"
|
||||
"options":[{
|
||||
"condition":[{"getMapFlag":{"key":"gate","op":">=","val":2}}],
|
||||
"action":[{"deleteMapObject":18}],
|
||||
@@ -379,12 +379,12 @@
|
||||
<properties>
|
||||
<property name="dialog">[
|
||||
{
|
||||
"text":"Hmm a big switch is embedded into the wall",
|
||||
"text":"Hmm... A big switch is embedded into the wall.",
|
||||
"options":[
|
||||
{
|
||||
"text":"You hear some loud sounds from the east",
|
||||
"text":"You hear some loud sounds coming from the east",
|
||||
"action":[{"deleteMapObject":-1},{"advanceMapFlag":"gate"}],
|
||||
"name":"flip the switch"
|
||||
"name":"flip the switch"
|
||||
"options":[{
|
||||
"condition":[{"getMapFlag":{"key":"gate","op":">=","val":2}}],
|
||||
"action":[{"deleteMapObject":18}],
|
||||
@@ -401,11 +401,11 @@
|
||||
<properties>
|
||||
<property name="dialog">[
|
||||
{
|
||||
"text":"Hmm a big switch is embedded into the wall",
|
||||
"text":"Hmm... A big switch is embedded into the wall.",
|
||||
"options":[
|
||||
{
|
||||
"text":"You hear some loud sounds from the north",
|
||||
"name":"flip the switch"
|
||||
"text":"You hear some loud sounds coming from the north",
|
||||
"name":"flip the switch"
|
||||
"options":[{
|
||||
"action":[{"deleteMapObject":17}],
|
||||
"name":"ok" }]
|
||||
@@ -444,11 +444,11 @@
|
||||
<properties>
|
||||
<property name="dialog">[
|
||||
{
|
||||
"text":"Hmm a big switch is embedded into the wall",
|
||||
"text":"Hmm... A big switch is embedded into the wall.",
|
||||
"options":[
|
||||
{
|
||||
"text":"You hear some rumbling and commotion from the south. The final gate appears to be open.",
|
||||
"name":"flip the switch"
|
||||
"name":"flip the switch"
|
||||
"options":[{
|
||||
"action":[{"deleteMapObject":6}],
|
||||
"name":"ok" }]
|
||||
@@ -502,7 +502,7 @@
|
||||
{
|
||||
"text":"The connected machinery whirrs to life, and the gate to your east opens.",
|
||||
"action":[{"deleteMapObject":-1}],
|
||||
"name":"flip the switch"
|
||||
"name":"flip the switch"
|
||||
"options":[{
|
||||
"action":[{"deleteMapObject":11}],
|
||||
"name":"ok" }]
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
<object id="55" template="../../obj/gate.tx" x="368" y="640">
|
||||
<properties>
|
||||
<property name="dialog">[{
|
||||
"text":"A gate is blocking the path. I looks like it is open elsewhere",
|
||||
"text":"A gate is blocking the path. It looks like it is opened elsewhere.",
|
||||
"options":[
|
||||
{ "name":"continue" }
|
||||
]
|
||||
@@ -84,12 +84,12 @@
|
||||
<properties>
|
||||
<property name="dialog">[
|
||||
{
|
||||
"text":"Hmm a big switch is embedded into the wall",
|
||||
"text":"Hmm... A big switch is embedded into the wall.",
|
||||
"options":[
|
||||
{
|
||||
"text":"You hear some loud sounds from the center",
|
||||
"text":"You hear some loud sounds coming from the center.",
|
||||
"action":[{"deleteMapObject":-1},{"advanceMapFlag":"gate"}],
|
||||
"name":"flip the switch"
|
||||
"name":"flip the switch"
|
||||
"options":[{
|
||||
"condition":[{"getMapFlag":{"key":"gate","op":">=","val":3}}],
|
||||
"action":[{"deleteMapObject":55}],
|
||||
@@ -106,12 +106,12 @@
|
||||
<properties>
|
||||
<property name="dialog">[
|
||||
{
|
||||
"text":"Hmm a big switch is embedded into the wall",
|
||||
"text":"Hmm... A big switch is embedded into the wall.",
|
||||
"options":[
|
||||
{
|
||||
"text":"You hear some loud sounds from the center",
|
||||
"text":"You hear some loud sounds coming from the center.",
|
||||
"action":[{"deleteMapObject":-1},{"advanceMapFlag":"gate"}],
|
||||
"name":"flip the switch"
|
||||
"name":"flip the switch"
|
||||
"options":[{
|
||||
"condition":[{"getMapFlag":{"key":"gate","op":">=","val":3}}],
|
||||
"action":[{"deleteMapObject":55}],
|
||||
@@ -128,12 +128,12 @@
|
||||
<properties>
|
||||
<property name="dialog">[
|
||||
{
|
||||
"text":"Hmm a big switch is embedded into the wall",
|
||||
"text":"Hmm... A big switch is embedded into the wall.",
|
||||
"options":[
|
||||
{
|
||||
"text":"You hear some loud sounds from the center",
|
||||
"text":"You hear some loud sounds coming from the center.",
|
||||
"action":[{"deleteMapObject":-1},{"advanceMapFlag":"gate"}],
|
||||
"name":"flip the switch"
|
||||
"name":"flip the switch"
|
||||
"options":[{
|
||||
"condition":[{"getMapFlag":{"key":"gate","op":">=","val":3}}],
|
||||
"action":[{"deleteMapObject":55}],
|
||||
|
||||
@@ -3,7 +3,7 @@ ManaCost:2 U U
|
||||
Types:Legendary Creature Human Wizard
|
||||
PT:3/3
|
||||
T:Mode$ SpellCast | TriggerZones$ Battlefield | ValidCard$ Creature.withFlying+nonLegendary | ValidActivatingPlayer$ You | ResolvedLimit$ 1 | Execute$ TrigCopy | OptionalDecider$ You | TriggerDescription$ Whenever you cast a nonlegendary creature spell with flying, you may copy it, except the copy is a 1/1 Spirit in addition to its other types. Do this only once each turn. (The copy becomes a token.)
|
||||
SVar:TrigCopy:DB$ CopySpellAbility | Defined$ TriggeredSpellAbility | CopySetPower$ 1 | CopySetToughness$ 1 | CopyAddTypes$ Spirit
|
||||
SVar:TrigCopy:DB$ CopySpellAbility | Defined$ TriggeredSpellAbility | SetPower$ 1 | SetToughness$ 1 | AddTypes$ Spirit
|
||||
DeckHas:Ability$Token
|
||||
DeckHints:Keyword$Flying
|
||||
SVar:BuffedBy:Creature.withFlying
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Name:Fork
|
||||
ManaCost:R R
|
||||
Types:Instant
|
||||
A:SP$ CopySpellAbility | Cost$ R R | ValidTgts$ Instant,Sorcery | TargetType$ Spell | CopyIsColor$ Red | OverwriteColors$ True | MayChooseTarget$ True | SpellDescription$ Copy target instant or sorcery spell, except that the copy is red. You may choose new targets for the copy.
|
||||
A:SP$ CopySpellAbility | Cost$ R R | ValidTgts$ Instant,Sorcery | TargetType$ Spell | SetColor$ Red | MayChooseTarget$ True | SpellDescription$ Copy target instant or sorcery spell, except that the copy is red. You may choose new targets for the copy.
|
||||
Oracle:Copy target instant or sorcery spell, except that the copy is red. You may choose new targets for the copy.
|
||||
|
||||
@@ -19,7 +19,7 @@ PT:3/6
|
||||
K:Menace
|
||||
K:Deathtouch
|
||||
T:Mode$ Attacks | ValidCard$ Card.Self | Execute$ TrigRemoveCounter | TriggerDescription$ Whenever CARDNAME attacks, remove all counters from up to one target permanent.
|
||||
SVar:TrigRemoveCounter:DB$ RemoveCounter | ValidTgts$ Permanent | CounterType$ All | CounterNum$ All
|
||||
SVar:TrigRemoveCounter:DB$ RemoveCounter | ValidTgts$ Permanent | TargetMin$ 0 | TargetMax$ 1 | CounterType$ All | CounterNum$ All
|
||||
T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | TriggerZones$ Battlefield | CheckSVar$ X | SVarCompare$ EQ0 | Execute$ TrigDraw | TriggerDescription$ At the beginning of your upkeep, if you haven't been dealt combat damage since your last turn, you draw a card and you lose 1 life.
|
||||
SVar:TrigDraw:DB$ Draw | SubAbility$ DBLoseLife
|
||||
SVar:DBLoseLife:DB$ LoseLife | LifeAmount$ 1
|
||||
|
||||
@@ -3,10 +3,10 @@ ManaCost:3 W R
|
||||
Types:Instant
|
||||
A:SP$ Charm | Cost$ 3 W R | Choices$ DBSpirit,DBIndestructible,DBHelix,DBSacrifice | CharmNum$ 2
|
||||
SVar:DBSpirit:DB$ Token | TokenAmount$ 1 | TokenScript$ rw_3_2_spirit | TokenOwner$ You | SpellDescription$ Create a 3/2 red and white Spirit token.
|
||||
SVar:DBIndestructible:DB$ PumpAll | ValidCards$ Creature.YouCtrl | NumAtt$ +1 | KW$ Indestructible & haste | SpellDescription$ • Creatures you control get +1/+0 and gain indestructible and haste until end of turn.
|
||||
SVar:DBHelix:DB$ DealDamage | ValidTgts$ Any | NumDmg$ 3 | SubAbility$ DBGainLife | SpellDescription$ • CARDNAME deals 3 damage to any target. Target player gains 3 life.
|
||||
SVar:DBGainLife:DB$ GainLife | ValidTgts$ Player | TgtPrompt$ Select target player (to gain 3 life) | LifeAmount$ 3 | SpellDescription$ Target player gains 3 life.
|
||||
SVar:DBSacrifice:DB$ Sacrifice | Defined$ You | SacValid$ Permanent | SubAbility$ DBDraw
|
||||
SVar:DBDraw:DB$ Draw | NumCards$ 2 | SpellDescription$ Sacrifice a permanent,draw two cards.
|
||||
SVar:DBIndestructible:DB$ PumpAll | ValidCards$ Creature.YouCtrl | NumAtt$ +1 | KW$ Indestructible & Haste | SpellDescription$ Creatures you control get +1/+0 and gain indestructible and haste until end of turn.
|
||||
SVar:DBHelix:DB$ DealDamage | ValidTgts$ Any | NumDmg$ 3 | SubAbility$ DBGainLife | SpellDescription$ CARDNAME deals 3 damage to any target. Target player gains 3 life.
|
||||
SVar:DBGainLife:DB$ GainLife | ValidTgts$ Player | TgtPrompt$ Select target player (to gain 3 life) | LifeAmount$ 3
|
||||
SVar:DBSacrifice:DB$ Sacrifice | Defined$ You | SacValid$ Permanent | SpellDescription$ Sacrifice a permanent, then draw two cards. | SubAbility$ DBDraw
|
||||
SVar:DBDraw:DB$ Draw | NumCards$ 2
|
||||
DeckHas:Ability$Token|LifeGain
|
||||
Oracle:Choose two —\n• Create a 3/2 red and white Spirit creature token.\n• Creatures you control get +1/+0 and gain indestructible and haste until end of turn.\n• Lorehold Command deals 3 damage to any target. Target player gains 3 life.\n• Sacrifice a permanent, then draw two cards.
|
||||
|
||||
@@ -4,13 +4,10 @@ Types:Legendary Creature Kor Artificer
|
||||
PT:5/4
|
||||
K:Affinity:Artifact.Equipment:equipment
|
||||
T:Mode$ Attacks | ValidCard$ Creature.equipped+YouCtrl | TriggerZones$ Battlefield | Execute$ TrigExile | TriggerDescription$ Whenever an equipped creature you control attacks, exile the top card of your library. You may play that card this turn. You may cast Equipment spells this way without paying their mana costs.
|
||||
SVar:TrigExile:DB$ Dig | Defined$ You | DigNum$ 1 | ChangeNum$ All | DestinationZone$ Exile | RememberChanged$ True | SubAbility$ DBBranch
|
||||
SVar:DBBranch:DB$ Branch | BranchConditionSVar$ X | TrueSubAbility$ DBEffect2 | FalseSubAbility$ DBEffect | SubAbility$ DBCleanup
|
||||
SVar:DBEffect2:DB$ Effect | StaticAbilities$ STPlay2 | RememberObjects$ Remembered | ForgetOnMoved$ Exile
|
||||
SVar:STPlay2:Mode$ Continuous | MayPlay$ True | MayPlayWithoutManaCost$ True | EffectZone$ Command | Affected$ Card.IsRemembered | AffectedZone$ Exile | Description$ You may play that card this turn without paying its mana cost.
|
||||
SVar:DBEffect:DB$ Effect | StaticAbilities$ STPlay | RememberObjects$ Remembered | ForgetOnMoved$ Exile
|
||||
SVar:TrigExile:DB$ Dig | Defined$ You | DigNum$ 1 | ChangeNum$ All | DestinationZone$ Exile | RememberChanged$ True | SubAbility$ DBEffect
|
||||
SVar:DBEffect:DB$ Effect | StaticAbilities$ STPlay,STPlay2 | RememberObjects$ Remembered | ForgetOnMoved$ Exile | SubAbility$ DBCleanup
|
||||
SVar:STPlay:Mode$ Continuous | MayPlay$ True | EffectZone$ Command | Affected$ Card.IsRemembered | AffectedZone$ Exile | Description$ You may play that card this turn.
|
||||
SVar:STPlay2:Mode$ Continuous | MayPlay$ True | MayPlayWithoutManaCost$ True | EffectZone$ Command | Affected$ Equipment.IsRemembered | ValidAfterStack$ Spell.Equipment | AffectedZone$ Exile | Description$ You may cast Equipment spells this way without paying their mana costs.
|
||||
SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True
|
||||
SVar:X:Remembered$Valid Equipment
|
||||
DeckNeeds:Type$Equipment
|
||||
Oracle:Affinity for Equipment\nWhenever an equipped creature you control attacks, exile the top card of your library. You may play that card this turn. You may cast Equipment spells this way without paying their mana costs.
|
||||
|
||||
@@ -3,7 +3,7 @@ ManaCost:5 G G
|
||||
Types:Legendary Planeswalker Nissa
|
||||
Loyalty:5
|
||||
A:AB$ Untap | Cost$ AddCounter<2/LOYALTY> | ValidTgts$ Creature | TargetMin$ 0 | TargetMax$ 2 | Planeswalker$ True | SubAbility$ DBUntap | TgtPrompt$ Select target creature | SpellDescription$ Untap up to two target creatures and up to two target lands.
|
||||
SVar:DBUntap:DB$ Untap | ValidTgts$ Land | TargetMin$ 0 | TargetMax$ 2 | TgtPrompt$ Select target Land
|
||||
SVar:DBUntap:DB$ Untap | ValidTgts$ Land | TargetMin$ 0 | TargetMax$ 2 | AILogic$ Always | TgtPrompt$ Select target Land
|
||||
A:AB$ Pump | Cost$ SubCounter<3/LOYALTY> | Planeswalker$ True | ValidTgts$ Creature | TgtPrompt$ Select target creature | NumAtt$ +5 | NumDef$ +5 | SpellDescription$ Target creature gets +5/+5 until end of turn.
|
||||
A:AB$ Dig | Cost$ SubCounter<10/LOYALTY> | DigNum$ 10 | AnyNumber$ True | ChangeValid$ Creature,Land | DestinationZone$ Battlefield | Ultimate$ True | Planeswalker$ True | DestinationZone2$ Library | LibraryPosition$ -1 | RestRandomOrder$ True | SpellDescription$ Look at the top ten cards of your library. You may put any number of creature and/or land cards from among them onto the battlefield. Put the rest on the bottom of your library in a random order.
|
||||
DeckHints:Name$Nissa's Encouragement|Brambleweft Behemoth|Forest
|
||||
|
||||
@@ -4,7 +4,8 @@ Types:Legendary Creature Dragon Avatar
|
||||
PT:5/5
|
||||
K:Flying
|
||||
K:Hexproof:Card.MonoColor:monocolored
|
||||
S:Mode$ Continuous | Affected$ Instant.YouCtrl+numColorsEQ2,Sorcery.YouCtrl+numColorsEQ2 | AffectedZone$ Graveyard | AddKeyword$ Jump-start | Description$ Each instant and sorcery card in your graveyard that's exactly two colors has jump-start.
|
||||
# TODO the AffectedZone Stack is needed for now but should be handled by the engine instead
|
||||
S:Mode$ Continuous | Affected$ Instant.YouCtrl+numColorsEQ2,Sorcery.YouCtrl+numColorsEQ2 | AffectedZone$ Graveyard,Stack | AddKeyword$ Jump-start | Description$ Each instant and sorcery card in your graveyard that's exactly two colors has jump-start.
|
||||
DeckHints:Type$Instant|Sorcery & Ability$Mill|Graveyard
|
||||
DeckHas:Ability$Graveyard|Discard
|
||||
Oracle:Flying, hexproof from monocolored\nEach instant and sorcery card in your graveyard that's exactly two colors has jump-start.
|
||||
|
||||
@@ -2,7 +2,7 @@ Name:Ob Nixilis, the Adversary
|
||||
ManaCost:1 B R
|
||||
Types:Legendary Planeswalker Nixilis
|
||||
Loyalty:3
|
||||
K:Casualty:X:NonLegendary$ True | CopySetLoyalty$ Casualty:The copy isn't legendary and has starting loyalty X.
|
||||
K:Casualty:X:NonLegendary$ True | SetLoyalty$ Casualty:The copy isn't legendary and has starting loyalty X.
|
||||
A:AB$ RepeatEach | Cost$ AddCounter<1/LOYALTY> | Planeswalker$ True | RepeatPlayers$ Opponent | RepeatSubAbility$ DBDrain | SubAbility$ DBGainLife | SpellDescription$ Each opponent loses 2 life unless they discard a card. If you control a Demon or Devil, you gain 2 life.
|
||||
SVar:DBDrain:DB$ LoseLife | Defined$ Player.IsRemembered | LifeAmount$ 2 | UnlessCost$ Discard<1/Card> | UnlessPayer$ Player.IsRemembered
|
||||
SVar:DBGainLife:DB$ GainLife | LifeAmount$ 2 | ConditionPresent$ Demon.YouCtrl,Devil.YouCtrl | StackDescription$ None
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
Name:Outlaws' Merriment
|
||||
ManaCost:1 R W W
|
||||
Types:Enchantment
|
||||
T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | Execute$ TrigCharm | TriggerZones$ Battlefield | TriggerDescription$ At the beginning of your upkeep, choose one at random. Create a red and white creature token with those characteristics.
|
||||
SVar:TrigCharm:DB$ Charm | Random$ True | Choices$ DBToken1,DBToken2,DBToken3
|
||||
T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | Execute$ TrigCharm | TriggerZones$ Battlefield | TriggerDescription$ At the beginning of your upkeep, ABILITY
|
||||
SVar:TrigCharm:DB$ Charm | Random$ True | Choices$ DBToken1,DBToken2,DBToken3 | AdditionalDescription$ Create a red and white creature token with those characteristics.
|
||||
SVar:DBToken1:DB$ Token | TokenAmount$ 1 | TokenScript$ rw_3_1_human_warrior_trample_haste | TokenOwner$ You | SpellDescription$ 3/1 Human Warrior with trample and haste.
|
||||
SVar:DBToken2:DB$ Token | TokenAmount$ 1 | TokenScript$ rw_2_1_human_cleric_lifelink_haste | TokenOwner$ You | SpellDescription$ 2/1 Human Cleric with lifelink and haste.
|
||||
SVar:DBToken3:DB$ Token | TokenAmount$ 1 | TokenScript$ rw_1_2_human_rogue_haste_damage | TokenOwner$ You | SpellDescription$ 1/2 Human Rogue with haste and "When this creature enters the battlefield, it deals 1 damage to any target."
|
||||
|
||||
@@ -3,6 +3,6 @@ ManaCost:R G
|
||||
Types:Legendary Creature Elf Warrior
|
||||
PT:2/2
|
||||
A:AB$ Mana | Cost$ T | Produced$ G | SpellDescription$ Add {G}.
|
||||
T:Mode$ Attacks | ValidCard$ Card.Self | Execute$ TrigMana | TriggerDescription$ Whenever CARDNAME attacks, add R R.
|
||||
T:Mode$ Attacks | ValidCard$ Card.Self | Execute$ TrigMana | TriggerDescription$ Whenever CARDNAME attacks, add {R}{R}.
|
||||
SVar:TrigMana:DB$ Mana | Produced$ R | Amount$ 2 | SpellDescription$ Add {R}{R}.
|
||||
Oracle:Whenever Radha, Heir to Keld attacks, you may add {R}{R}.\n{T}: Add {G}.
|
||||
|
||||
@@ -3,7 +3,7 @@ ManaCost:3 G U
|
||||
Types:Legendary Creature Human Artificer
|
||||
PT:3/5
|
||||
T:Mode$ SpellCast | TriggerZones$ Battlefield | OptionalDecider$ You | ValidCard$ Creature.Bird,Creature.Beast | ValidActivatingPlayer$ You | NoResolvingCheck$ True | Execute$ TrigCopy | TriggerDescription$ Whenever you cast a Beast or Bird creature spell, you may copy it, except it's an artifact in addition to its other types. (The copy becomes a token.)
|
||||
SVar:TrigCopy:DB$ CopySpellAbility | Defined$ TriggeredSpellAbility | CopyAddTypes$ Artifact
|
||||
SVar:TrigCopy:DB$ CopySpellAbility | Defined$ TriggeredSpellAbility | AddTypes$ Artifact
|
||||
DeckNeeds:Type$Beast|Bird
|
||||
DeckHas:Ability$Token & Type$Artifact
|
||||
Oracle:Whenever you cast a Beast or Bird creature spell, you may copy it, except it's an artifact in addition to its other types. (The copy becomes a token.)
|
||||
|
||||
@@ -3,6 +3,6 @@ ManaCost:1 W
|
||||
Types:Creature Human Knight
|
||||
PT:1/2
|
||||
A:AB$ PutCounter | Cost$ W T | CounterType$ P1P1 | CounterNum$ 1 | ValidTgts$ Creature.counters_GE1_P1P1 | TgtPrompt$ Select target creature with a +1/+1 counter | SpellDescription$ Put a +1/+1 counter on target creature with a +1/+1 counter on it.
|
||||
A:AB$ PutCounter | Cost$ 4 W W T | CounterType$ P1P1 | CounterNum$ 1 | ValidTgts$ Creature | TgtPrompt$ Select target creature | SpellDescription$ Put a +1/+1 counter on target creature.
|
||||
A:AB$ PutCounter | Cost$ 4 W W T | CounterType$ P1P1 | CounterNum$ 1 | ValidTgts$ Creature | TgtPrompt$ Select target creature | AILogic$ NoCounterOfType | SpellDescription$ Put a +1/+1 counter on target creature.
|
||||
DeckHas:Ability$Counters
|
||||
Oracle:{W}, {T}: Put a +1/+1 counter on target creature with a +1/+1 counter on it.\n{4}{W}{W}, {T}: Put a +1/+1 counter on target creature.
|
||||
|
||||
@@ -126,12 +126,14 @@ public class GauntletUtil {
|
||||
break;
|
||||
case COMMANDER_DECK:
|
||||
deck = DeckgenUtil.getCommanderDeck();
|
||||
eventNames.add(deck.getName());
|
||||
if (deck != null)
|
||||
eventNames.add(deck.getName());
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
decks.add(deck);
|
||||
if (deck != null)
|
||||
decks.add(deck);
|
||||
}
|
||||
|
||||
gauntlet.setDecks(decks);
|
||||
|
||||
@@ -164,6 +164,8 @@ public final class FModel {
|
||||
if (new AutoUpdater(true).attemptToUpdate()) {
|
||||
//
|
||||
}
|
||||
// load types before loading cards
|
||||
loadDynamicGamedata();
|
||||
|
||||
//load card database
|
||||
final CardStorageReader reader = new CardStorageReader(ForgeConstants.CARD_DATA_DIR, progressBarBridge,
|
||||
@@ -243,8 +245,6 @@ public final class FModel {
|
||||
|
||||
Spell.setPerformanceMode(preferences.getPrefBoolean(FPref.PERFORMANCE_MODE));
|
||||
|
||||
loadDynamicGamedata();
|
||||
|
||||
if (progressBar != null) {
|
||||
FThreads.invokeInEdtLater(new Runnable() {
|
||||
@Override
|
||||
|
||||
Reference in New Issue
Block a user