Compare commits

...

62 Commits

Author SHA1 Message Date
GitHub Actions
a69333b528 [maven-release-plugin] prepare release forge-2.0.06 2025-09-21 23:31:36 +00:00
Chris H
2beb71cdce Update apt-get so latest libxml2-utils is available 2025-09-21 19:21:23 -04:00
kevlahnota
b50cdea5df Update drop_tower.txt 2025-09-22 06:37:26 +08:00
Renato Filipe Vidal Santos
a7568b0845 Cleanup 2025-09-21 2025-09-21 07:30:43 +00:00
kevlahnota
e158b5966e Merge pull request #8755 from kevlahnota/master4
use Supplier for FModel for thread safety
2025-09-21 09:01:18 +08:00
kevlahnota
bea5ffb0a8 Update FModel.java
unused var
2025-09-21 08:51:30 +08:00
Anthony Calosa
ac492fac35 use Supplier for FModel for thread safety 2025-09-21 08:47:28 +08:00
Renato Filipe Vidal Santos
35e6268a95 Add files via upload (#8754) 2025-09-20 19:58:28 +03:00
Hans Mackowiak
90bd0c73d0 Transform: all DFC can transform now 2025-09-20 18:11:39 +02:00
kevlahnota
ea293a46b1 Merge pull request #8750 from kevlahnota/master4
update FModel, add Icon Overlay for Horizontal tabs
2025-09-19 22:58:47 +08:00
Anthony Calosa
6ec6d64cb2 update FModel, add Icon Overlay for Horizontal tabs 2025-09-19 22:06:38 +08:00
Chris H
9bc6ab747d Update Venom, Eddie Brock's power and toughness 2025-09-19 09:22:54 -04:00
kevlahnota
a72df6ad16 updateRarityFilterOdds (#8746) 2025-09-19 07:19:50 +02:00
Renato Filipe Vidal Santos
1105b3fcbd Add files via upload 2025-09-19 07:14:55 +02:00
kevlahnota
08239def86 Merge pull request #8745 from kevlahnota/master4
fix gap and visibility on horizontal layout
2025-09-19 10:39:52 +08:00
Anthony Calosa
cb7fc3df4e fix gap and visibility on horizontal layout 2025-09-19 10:32:18 +08:00
tool4EvEr
9b8020bb30 Fix copied SA using old replacingObjects 2025-09-18 21:20:21 +02:00
Eradev
398917d010 Support named abilities (#8464)
* Support named abilities

* Check if spell was cast from a named ability
2025-09-18 20:15:07 +03:00
tool4ever
10a68c27d5 Update omarthis_ghostfire_initiate.txt 2025-09-18 16:36:25 +00:00
kevlahnota
ba4ee98c3b Merge pull request #8741 from kevlahnota/master4
remove duplicated code and minor format
2025-09-18 19:44:29 +08:00
Anthony Calosa
c2c3add52b update switch 2025-09-18 19:35:23 +08:00
Anthony Calosa
1c7bc1c0d3 remove duplicated code and minor format 2025-09-18 19:27:55 +08:00
tool4EvEr
14f146f8d0 Remove TargetsFromSingleZone 2025-09-18 08:02:15 +02:00
kevlahnota
0cab03b96c Update VManaPool.java 2025-09-18 06:37:45 +08:00
tool4ever
8f5f2059a2 Fix Self-Destruct (#8730) 2025-09-17 18:58:40 +00:00
Hans Mackowiak
34323107c9 AetherFilter: use Predicate for Caption (#8724)
* AetherFilter: use Predicate for Caption
2025-09-17 18:00:08 +02:00
kevlahnota
dbf9568866 Merge pull request #8726 from kevlahnota/master4
update setlookup files
2025-09-17 14:13:38 +08:00
Anthony Calosa
7933893dcb update setlookup files
- closes #8721
2025-09-17 14:08:54 +08:00
kevlahnota
415e2c12e1 Merge pull request #8725 from kevlahnota/master4
fix tab display area size on Horizontal layout
2025-09-17 12:28:25 +08:00
Anthony Calosa
930019db40 fix tab display area size on Horizontal layout 2025-09-17 12:24:35 +08:00
tool4ever
72139b523c Improve AI timeout detection (#8723) 2025-09-16 18:04:00 +00:00
Hans Mackowiak
6cf2f20cdc ColorSet: store EnumSet and toString as Final (#8720) 2025-09-16 16:32:16 +02:00
kevlahnota
cebddb7f4b Update damage_control_crew.txt 2025-09-16 15:38:33 +08:00
kevlahnota
4a6275d282 Merge pull request #8710 from Card-Forge/enhance-prize-visiblity
Only show event cost before you sign up for the event
2025-09-16 15:15:58 +08:00
kevlahnota
bdeaf8cef1 Merge pull request #8719 from kevlahnota/master4
update AltZoneLayout VCardDisplayArea for Landscape
2025-09-16 12:11:22 +08:00
Anthony Calosa
40f6a5b472 update 2025-09-16 12:03:18 +08:00
Anthony Calosa
f4bff30680 update AltZoneLayout VCardDisplayArea for Landscape
- Off (Default Layout)
- Vertical (Alt Layout with old Vertical Scroll Card Display Area)
- Horizontal (Alt Layout with Horizontal Scroll Card Display Area)
- fix mouse scroll to scroll horizontall while ShiftKey is pressed
2025-09-16 11:50:15 +08:00
marthinwurer
5f32c23dc5 moved stopwatch to a class field 2025-09-15 14:57:16 -04:00
marthinwurer
ae7159297b made simulation test extend ai test 2025-09-15 14:57:16 -04:00
marthinwurer
cedfa68c16 removed unused imports 2025-09-15 14:57:16 -04:00
marthinwurer
1f235c5e71 remove todos 2025-09-15 14:57:16 -04:00
marthinwurer
9f8bb8ae4d added integration test 2025-09-15 14:57:16 -04:00
marthinwurer
cd3a26e434 break out main loop step 2025-09-15 14:57:16 -04:00
marthinwurer
d2d6b7bc53 break out main game loop function 2025-09-15 14:57:16 -04:00
marthinwurer
052e72d8ea break out first turn setup 2025-09-15 14:57:16 -04:00
marthinwurer
b902d516a7 Added todos for known problems 2025-09-15 14:57:16 -04:00
tool4ever
79fd4a3f8d Fix crash after removing target when copying divided spell (#8714)
Co-authored-by: tool4EvEr <tool4EvEr@>
2025-09-15 14:27:21 +02:00
Hans Mackowiak
10d359e7d7 Update MagicColor.Color enum (#8715)
Adds ShortName and Symbol
2025-09-15 14:12:46 +02:00
kevlahnota
1cb9e78b21 Merge pull request #8712 from kevlahnota/master4
Prevent NPE
2025-09-15 10:30:31 +08:00
Anthony Calosa
d999c5e213 Prevent NPE 2025-09-15 09:48:45 +08:00
tool4ever
1f7862c970 Update superior_foes_of_spider_man.txt
Closes #8711
2025-09-14 19:26:44 +00:00
Chris H
cdc696c506 Only show event cost before you sign up for the event 2025-09-14 11:13:29 -04:00
Chris H
64897f1ab4 Simplify logic 2025-09-14 10:42:19 -04:00
Chris H
905cf52c5e Disable 4 player drafts for Quest and Adventure until the UIs can handle it 2025-09-14 10:42:19 -04:00
kevlahnota
3139d593a3 Merge pull request #8708 from kevlahnota/master4
throw RuntimeException message for BoosterGenerator makesheet
2025-09-14 08:49:04 +08:00
Anthony Calosa
8f71a5b06e throw RuntimeException message for BoosterGenerator makesheet 2025-09-14 08:44:10 +08:00
kevlahnota
e9742a2292 Merge pull request #8707 from kevlahnota/master4
use player or geneticAI deck for missing decks
2025-09-14 08:17:11 +08:00
Anthony Calosa
a6c893693d remove duplicated code 2025-09-14 08:14:28 +08:00
Anthony Calosa
327ee6853f use player or geneticAI deck for missing decks 2025-09-14 08:04:22 +08:00
kevlahnota
3e7f98db6a Merge pull request #8706 from kevlahnota/master4
Different effect card for Emrakul Castle difficulty
2025-09-13 21:19:19 +08:00
Anthony Calosa
e49021ffdd Different effect card for Emrakul Castle difficulty 2025-09-13 21:13:26 +08:00
Chris H
0c5ff17e94 Fix typo for SPM draft 2025-09-12 23:36:36 -04:00
220 changed files with 1487 additions and 1175 deletions

View File

@@ -129,7 +129,9 @@ jobs:
makeLatest: true
- name: 🔧 Install XML tools
run: sudo apt-get install -y libxml2-utils
run: |
sudo apt-get update
sudo apt-get install -y libxml2-utils
- name: 🔼 Bump versionCode in root POM
id: bump_version

View File

@@ -3,7 +3,7 @@
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>${revision}</version>
<version>2.0.06</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -6,7 +6,7 @@
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>${revision}</version>
<version>2.0.06</version>
</parent>
<artifactId>forge-ai</artifactId>

View File

@@ -97,6 +97,7 @@ public class AiController {
private int lastAttackAggression;
private boolean useLivingEnd;
private List<SpellAbility> skipped;
private boolean timeoutReached;
public AiController(final Player computerPlayer, final Game game0) {
player = computerPlayer;
@@ -1664,6 +1665,9 @@ public class AiController {
Sentry.captureMessage(ex.getMessage() + "\nAssertionError [verifyTransitivity]: " + assertex);
}
// in case of infinite loop reset below would not be reached
timeoutReached = false;
FutureTask<SpellAbility> future = new FutureTask<>(() -> {
//avoid ComputerUtil.aiLifeInDanger in loops as it slows down a lot.. call this outside loops will generally be fast...
boolean isLifeInDanger = useLivingEnd && ComputerUtil.aiLifeInDanger(player, true, 0);
@@ -1673,6 +1677,11 @@ public class AiController {
continue;
}
if (timeoutReached) {
timeoutReached = false;
break;
}
if (sa.getHostCard().hasKeyword(Keyword.STORM)
&& sa.getApi() != ApiType.Counter // AI would suck at trying to deliberately proc a Storm counterspell
&& player.getZone(ZoneType.Hand).contains(
@@ -1752,7 +1761,10 @@ public class AiController {
t.stop();
} catch (UnsupportedOperationException ex) {
// Android and Java 20 dropped support to stop so sadly thread will keep running
timeoutReached = true;
future.cancel(true);
// TODO wait a few more seconds to try and exit at a safe point before letting the engine continue
// TODO mark some as skipped to increase chance to find something playable next priority
}
return null;
}

View File

@@ -298,13 +298,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
boolean activateForCost = ComputerUtil.activateForCost(sa, ai);
if (sa.hasParam("Origin")) {
try {
origin = ZoneType.listValueOf(sa.getParam("Origin"));
} catch (IllegalArgumentException ex) {
// This happens when Origin is something like
// "Graveyard,Library" (Doomsday)
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
origin = ZoneType.listValueOf(sa.getParam("Origin"));
}
final String destination = sa.getParam("Destination");
@@ -908,8 +902,6 @@ public class ChangeZoneAi extends SpellAbilityAi {
list.remove(source); // spells can't target their own source, because it's actually in the stack zone
}
// list = CardLists.canSubsequentlyTarget(list, sa);
if (sa.hasParam("AttachedTo")) {
list = CardLists.filter(list, c -> {
for (Card card : game.getCardsIn(ZoneType.Battlefield)) {
@@ -1252,53 +1244,12 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
}
// if max CMC exceeded, do not choose this card (but keep looking for other options)
if (sa.hasParam("MaxTotalTargetCMC")) {
if (choice.getCMC() > sa.getTargetRestrictions().getMaxTotalCMC(choice, sa) - sa.getTargets().getTotalTargetedCMC()) {
list.remove(choice);
continue;
}
}
// if max power exceeded, do not choose this card (but keep looking for other options)
if (sa.hasParam("MaxTotalTargetPower")) {
if (choice.getNetPower() > sa.getTargetRestrictions().getMaxTotalPower(choice, sa) -sa.getTargets().getTotalTargetedPower()) {
list.remove(choice);
continue;
}
}
// honor the Same Creature Type restriction
if (sa.getTargetRestrictions().isWithSameCreatureType()) {
Card firstTarget = sa.getTargetCard();
if (firstTarget != null && !choice.sharesCreatureTypeWith(firstTarget)) {
list.remove(choice);
continue;
}
}
list.remove(choice);
if (sa.canTarget(choice)) {
sa.getTargets().add(choice);
}
}
// Honor the Single Zone restriction. For now, simply remove targets that do not belong to the same zone as the first targeted card.
// TODO: ideally the AI should consider at this point which targets exactly to pick (e.g. one card in the first player's graveyard
// vs. two cards in the second player's graveyard, which cards are more relevant to be targeted, etc.). Consider improving.
if (sa.getTargetRestrictions().isSingleZone()) {
Card firstTgt = sa.getTargetCard();
CardCollection toRemove = new CardCollection();
if (firstTgt != null) {
for (Card t : sa.getTargets().getTargetCards()) {
if (!t.getController().equals(firstTgt.getController())) {
toRemove.add(t);
}
}
sa.getTargets().removeAll(toRemove);
}
}
return true;
}

View File

@@ -6,7 +6,7 @@
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>${revision}</version>
<version>2.0.06</version>
</parent>
<artifactId>forge-core</artifactId>

View File

@@ -168,21 +168,7 @@ public final class CardRules implements ICardCharacteristics {
}
public boolean isTransformable() {
if (CardSplitType.Transform == getSplitType()) {
return true;
}
if (CardSplitType.Modal != getSplitType()) {
return false;
}
for (ICardFace face : getAllFaces()) {
for (String spell : face.getAbilities()) {
if (spell.contains("AB$ SetState") && spell.contains("Mode$ Transform")) {
return true;
}
}
// TODO check keywords if needed
}
return false;
return CardSplitType.Transform == getSplitType() || CardSplitType.Modal == getSplitType();
}
public ICardFace getWSpecialize() {

View File

@@ -20,7 +20,6 @@ package forge.card;
import com.google.common.collect.UnmodifiableIterator;
import forge.card.MagicColor.Color;
import forge.card.mana.ManaCost;
import forge.card.mana.ManaCostShard;
import forge.util.BinaryUtil;
import java.io.Serializable;
@@ -41,27 +40,97 @@ import java.util.stream.Stream;
public final class ColorSet implements Comparable<ColorSet>, Iterable<Byte>, Serializable {
private static final long serialVersionUID = 794691267379929080L;
// needs to be before other static
private static final ColorSet[] cache = new ColorSet[MagicColor.ALL_COLORS + 1];
static {
byte COLORLESS = MagicColor.COLORLESS;
byte WHITE = MagicColor.WHITE;
byte BLUE = MagicColor.BLUE;
byte BLACK = MagicColor.BLACK;
byte RED = MagicColor.RED;
byte GREEN = MagicColor.GREEN;
Color C = Color.COLORLESS;
Color W = Color.WHITE;
Color U = Color.BLUE;
Color B = Color.BLACK;
Color R = Color.RED;
Color G = Color.GREEN;
//colorless
cache[COLORLESS] = new ColorSet(C);
//mono-color
cache[WHITE] = new ColorSet(W);
cache[BLUE] = new ColorSet(U);
cache[BLACK] = new ColorSet(B);
cache[RED] = new ColorSet(R);
cache[GREEN] = new ColorSet(G);
//two-color
cache[WHITE | BLUE] = new ColorSet(W, U);
cache[WHITE | BLACK] = new ColorSet(W, B);
cache[BLUE | BLACK] = new ColorSet(U, B);
cache[BLUE | RED] = new ColorSet(U, R);
cache[BLACK | RED] = new ColorSet(B, R);
cache[BLACK | GREEN] = new ColorSet(B, G);
cache[RED | GREEN] = new ColorSet(R, G);
cache[RED | WHITE] = new ColorSet(R, W);
cache[GREEN | WHITE] = new ColorSet(G, W);
cache[GREEN | BLUE] = new ColorSet(G, U);
//three-color
cache[WHITE | BLUE | BLACK] = new ColorSet(W, U, B);
cache[WHITE | BLACK | GREEN] = new ColorSet(W, B, G);
cache[BLUE | BLACK | RED] = new ColorSet(U, B, R);
cache[BLUE | RED | WHITE] = new ColorSet(U, R, W);
cache[BLACK | RED | GREEN] = new ColorSet(B, R, G);
cache[BLACK | GREEN | BLUE] = new ColorSet(B, G, U);
cache[RED | GREEN | WHITE] = new ColorSet(R, G, W);
cache[RED | WHITE | BLACK] = new ColorSet(R, W, B);
cache[GREEN | WHITE | BLUE] = new ColorSet(G, W, U);
cache[GREEN | BLUE | RED] = new ColorSet(G, U, R);
//four-color
cache[WHITE | BLUE | BLACK | RED] = new ColorSet(W, U, B, R);
cache[BLUE | BLACK | RED | GREEN] = new ColorSet(U, B, R, G);
cache[BLACK | RED | GREEN | WHITE] = new ColorSet(B, R, G, W);
cache[RED | GREEN | WHITE | BLUE] = new ColorSet(R, G, W, U);
cache[GREEN | WHITE | BLUE | BLACK] = new ColorSet(G, W, U, B);
//five-color
cache[WHITE | BLUE | BLACK | RED | GREEN] = new ColorSet(W, U, B, R, G);
}
private final Collection<Color> orderedShards;
private final byte myColor;
private final float orderWeight;
private static final ColorSet[] cache = new ColorSet[32];
private final Set<Color> enumSet;
private final String desc;
public static final ColorSet ALL_COLORS = fromMask(MagicColor.ALL_COLORS);
private static final ColorSet NO_COLORS = fromMask(MagicColor.COLORLESS);
public static final ColorSet NO_COLORS = fromMask(MagicColor.COLORLESS);
private ColorSet(final byte mask) {
this.myColor = mask;
private ColorSet(final Color... ordered) {
this.orderedShards = Arrays.asList(ordered);
this.myColor = orderedShards.stream().map(Color::getColorMask).reduce((byte)0, (a, b) -> (byte)(a | b));
this.orderWeight = this.getOrderWeight();
this.enumSet = EnumSet.copyOf(orderedShards);
this.desc = orderedShards.stream().map(Color::getShortName).collect(Collectors.joining());
}
public static ColorSet fromMask(final int mask) {
final int mask32 = mask & MagicColor.ALL_COLORS;
if (cache[mask32] == null) {
cache[mask32] = new ColorSet((byte) mask32);
}
return cache[mask32];
}
public static ColorSet fromEnums(final Color... colors) {
byte mask = 0;
for (Color e : colors) {
mask |= e.getColorMask();
}
return fromMask(mask);
}
public static ColorSet fromNames(final String... colors) {
byte mask = 0;
for (final String s : colors) {
@@ -293,17 +362,7 @@ public final class ColorSet implements Comparable<ColorSet>, Iterable<Byte>, Ser
*/
@Override
public String toString() {
final ManaCostShard[] orderedShards = getOrderedShards();
return Arrays.stream(orderedShards).map(ManaCostShard::toShortString).collect(Collectors.joining());
}
/**
* Gets the null color.
*
* @return the nullColor
*/
public static ColorSet getNullColor() {
return NO_COLORS;
return desc;
}
/**
@@ -325,16 +384,7 @@ public final class ColorSet implements Comparable<ColorSet>, Iterable<Byte>, Ser
}
public Set<Color> toEnumSet() {
if (isColorless()) {
return EnumSet.of(Color.COLORLESS);
}
List<Color> list = new ArrayList<>();
for (Color c : Color.values()) {
if (hasAnyColor(c.getColormask())) {
list.add(c);
}
}
return EnumSet.copyOf(list);
return EnumSet.copyOf(enumSet);
}
@Override
@@ -372,72 +422,12 @@ public final class ColorSet implements Comparable<ColorSet>, Iterable<Byte>, Ser
}
}
public Stream<MagicColor.Color> stream() {
public Stream<Color> stream() {
return this.toEnumSet().stream();
}
//Get array of mana cost shards for color set in the proper order
public ManaCostShard[] getOrderedShards() {
return shardOrderLookup[myColor];
}
private static final ManaCostShard[][] shardOrderLookup = new ManaCostShard[MagicColor.ALL_COLORS + 1][];
static {
byte COLORLESS = MagicColor.COLORLESS;
byte WHITE = MagicColor.WHITE;
byte BLUE = MagicColor.BLUE;
byte BLACK = MagicColor.BLACK;
byte RED = MagicColor.RED;
byte GREEN = MagicColor.GREEN;
ManaCostShard C = ManaCostShard.COLORLESS;
ManaCostShard W = ManaCostShard.WHITE;
ManaCostShard U = ManaCostShard.BLUE;
ManaCostShard B = ManaCostShard.BLACK;
ManaCostShard R = ManaCostShard.RED;
ManaCostShard G = ManaCostShard.GREEN;
//colorless
shardOrderLookup[COLORLESS] = new ManaCostShard[] { C };
//mono-color
shardOrderLookup[WHITE] = new ManaCostShard[] { W };
shardOrderLookup[BLUE] = new ManaCostShard[] { U };
shardOrderLookup[BLACK] = new ManaCostShard[] { B };
shardOrderLookup[RED] = new ManaCostShard[] { R };
shardOrderLookup[GREEN] = new ManaCostShard[] { G };
//two-color
shardOrderLookup[WHITE | BLUE] = new ManaCostShard[] { W, U };
shardOrderLookup[WHITE | BLACK] = new ManaCostShard[] { W, B };
shardOrderLookup[BLUE | BLACK] = new ManaCostShard[] { U, B };
shardOrderLookup[BLUE | RED] = new ManaCostShard[] { U, R };
shardOrderLookup[BLACK | RED] = new ManaCostShard[] { B, R };
shardOrderLookup[BLACK | GREEN] = new ManaCostShard[] { B, G };
shardOrderLookup[RED | GREEN] = new ManaCostShard[] { R, G };
shardOrderLookup[RED | WHITE] = new ManaCostShard[] { R, W };
shardOrderLookup[GREEN | WHITE] = new ManaCostShard[] { G, W };
shardOrderLookup[GREEN | BLUE] = new ManaCostShard[] { G, U };
//three-color
shardOrderLookup[WHITE | BLUE | BLACK] = new ManaCostShard[] { W, U, B };
shardOrderLookup[WHITE | BLACK | GREEN] = new ManaCostShard[] { W, B, G };
shardOrderLookup[BLUE | BLACK | RED] = new ManaCostShard[] { U, B, R };
shardOrderLookup[BLUE | RED | WHITE] = new ManaCostShard[] { U, R, W };
shardOrderLookup[BLACK | RED | GREEN] = new ManaCostShard[] { B, R, G };
shardOrderLookup[BLACK | GREEN | BLUE] = new ManaCostShard[] { B, G, U };
shardOrderLookup[RED | GREEN | WHITE] = new ManaCostShard[] { R, G, W };
shardOrderLookup[RED | WHITE | BLACK] = new ManaCostShard[] { R, W, B };
shardOrderLookup[GREEN | WHITE | BLUE] = new ManaCostShard[] { G, W, U };
shardOrderLookup[GREEN | BLUE | RED] = new ManaCostShard[] { G, U, R };
//four-color
shardOrderLookup[WHITE | BLUE | BLACK | RED] = new ManaCostShard[] { W, U, B, R };
shardOrderLookup[BLUE | BLACK | RED | GREEN] = new ManaCostShard[] { U, B, R, G };
shardOrderLookup[BLACK | RED | GREEN | WHITE] = new ManaCostShard[] { B, R, G, W };
shardOrderLookup[RED | GREEN | WHITE | BLUE] = new ManaCostShard[] { R, G, W, U };
shardOrderLookup[GREEN | WHITE | BLUE | BLACK] = new ManaCostShard[] { G, W, U, B };
//five-color
shardOrderLookup[WHITE | BLUE | BLACK | RED | GREEN] = new ManaCostShard[] { W, U, B, R, G };
public Collection<Color> getOrderedColors() {
return orderedShards;
}
}

View File

@@ -1,7 +1,7 @@
package forge.card;
import com.google.common.collect.ImmutableList;
import forge.deck.DeckRecognizer;
import forge.util.Localizer;
/**
* Holds byte values for each color magic has.
@@ -158,20 +158,23 @@ public final class MagicColor {
}
public enum Color {
WHITE(Constant.WHITE, MagicColor.WHITE, "{W}"),
BLUE(Constant.BLUE, MagicColor.BLUE, "{U}"),
BLACK(Constant.BLACK, MagicColor.BLACK, "{B}"),
RED(Constant.RED, MagicColor.RED, "{R}"),
GREEN(Constant.GREEN, MagicColor.GREEN, "{G}"),
COLORLESS(Constant.COLORLESS, MagicColor.COLORLESS, "{C}");
WHITE(Constant.WHITE, MagicColor.WHITE, "W", "lblWhite"),
BLUE(Constant.BLUE, MagicColor.BLUE, "U", "lblBlue"),
BLACK(Constant.BLACK, MagicColor.BLACK, "B", "lblBlack"),
RED(Constant.RED, MagicColor.RED, "R", "lblRed"),
GREEN(Constant.GREEN, MagicColor.GREEN, "G", "lblGreen"),
COLORLESS(Constant.COLORLESS, MagicColor.COLORLESS, "C", "lblColorless");
private final String name, symbol;
private final String name, shortName, symbol;
private final String label;
private final byte colormask;
Color(String name0, byte colormask0, String symbol0) {
Color(String name0, byte colormask0, String shortName, String label) {
name = name0;
colormask = colormask0;
symbol = symbol0;
this.shortName = shortName;
symbol = "{" + shortName + "}";
this.label = label;
}
public static Color fromByte(final byte color) {
@@ -188,13 +191,15 @@ public final class MagicColor {
public String getName() {
return name;
}
public String getLocalizedName() {
//Should probably move some of this logic back here, or at least to a more general location.
return DeckRecognizer.getLocalisedMagicColorName(getName());
public String getShortName() {
return shortName;
}
public byte getColormask() {
public String getLocalizedName() {
return Localizer.getInstance().getMessage(label);
}
public byte getColorMask() {
return colormask;
}
public String getSymbol() {
@@ -202,7 +207,7 @@ public final class MagicColor {
}
@Override
public String toString() {
return name;
return getLocalizedName();
}
}

View File

@@ -1015,7 +1015,7 @@ public class DeckRecognizer {
return String.format("%s // %s", getMagicColourLabel(magicColor1), getMagicColourLabel(magicColor2));
String localisedName1 = magicColor1.getLocalizedName();
String localisedName2 = magicColor2.getLocalizedName();
String comboManaSymbol = manaSymbolsMap.get(magicColor1.getColormask() | magicColor2.getColormask());
String comboManaSymbol = manaSymbolsMap.get(magicColor1.getColorMask() | magicColor2.getColorMask());
return String.format("%s/%s {%s}", localisedName1, localisedName2, comboManaSymbol);
}

View File

@@ -593,7 +593,7 @@ public class PaperCard implements Comparable<IPaperCard>, InventoryItemFromSet,
public PaperCardFlags withMarkedColors(ColorSet markedColors) {
if(markedColors == null)
markedColors = ColorSet.getNullColor();
markedColors = ColorSet.NO_COLORS;
return new PaperCardFlags(this, markedColors, null);
}

View File

@@ -156,7 +156,7 @@ public class PaperToken implements InventoryItemFromSet, IPaperCard {
return false;
CardSplitType cst = this.cardRules.getSplitType();
//expand this on future for other tokens that has other backsides besides transform..
return cst == CardSplitType.Transform;
return cst == CardSplitType.Transform || cst == CardSplitType.Modal;
}
@Override

View File

@@ -633,7 +633,10 @@ public class BoosterGenerator {
System.out.println("Parsing from main code: " + mainCode);
String sheetName = StringUtils.strip(mainCode.substring(10), "()\" ");
System.out.println("Attempting to lookup: " + sheetName);
src = tryGetStaticSheet(sheetName).toFlatList();
PrintSheet fromSheet = tryGetStaticSheet(sheetName);
if (fromSheet == null)
throw new RuntimeException("PrintSheet Error: " + ps.getName() + " didn't find " + sheetName + " from " + mainCode);
src = fromSheet.toFlatList();
setPred = x -> true;
} else if (mainCode.startsWith("promo") || mainCode.startsWith("name")) { // get exactly the named cards, that's a tiny inlined print sheet

View File

@@ -6,7 +6,7 @@
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>${revision}</version>
<version>2.0.06</version>
</parent>
<artifactId>forge-game</artifactId>

View File

@@ -35,7 +35,7 @@ public class ForgeScript {
boolean withSource = property.endsWith("Source");
final ColorSet colors;
if (withSource && StaticAbilityColorlessDamageSource.colorlessDamageSource(cardState)) {
colors = ColorSet.getNullColor();
colors = ColorSet.NO_COLORS;
} else {
colors = cardState.getCard().getColor(cardState);
}
@@ -412,6 +412,8 @@ public class ForgeScript {
return !sa.isPwAbility() && !sa.getRestrictions().isSorcerySpeed();
}
return true;
} else if(property.startsWith("NamedAbility")) {
return sa.getName().equals(property.substring(12));
} else if (sa.getHostCard() != null) {
return sa.getHostCard().hasProperty(property, sourceController, source, spellAbility);
}

View File

@@ -845,6 +845,8 @@ public class Game {
p.revealFaceDownCards();
}
// TODO free any mindslaves
for (Card c : cards) {
// CR 800.4d if card is controlled by opponent, LTB should trigger
if (c.getOwner().equals(p) && c.getController().equals(p)) {
@@ -880,8 +882,6 @@ public class Game {
}
triggerList.put(c.getZone().getZoneType(), null, c);
getAction().ceaseToExist(c, false);
// CR 603.2f owner of trigger source lost game
getTriggerHandler().clearDelayedTrigger(c);
}
} else {
// return stolen permanents

View File

@@ -220,10 +220,6 @@ public class GameAction {
//copied.setGamePieceType(GamePieceType.COPIED_SPELL);
}
if (c.isTransformed()) {
copied.incrementTransformedTimestamp();
}
if (cause != null && cause.isSpell() && c.equals(cause.getHostCard())) {
copied.setCastSA(cause);
copied.setSplitStateToPlayAbility(cause);
@@ -974,6 +970,7 @@ public class GameAction {
// in some corner cases there's no zone yet (copied spell that failed targeting)
if (z != null) {
z.remove(c);
c.setZone(c.getOwner().getZone(ZoneType.None));
if (z.is(ZoneType.Battlefield)) {
c.runLeavesPlayCommands();
}

View File

@@ -993,9 +993,6 @@ public final class GameActionUtil {
oldCard.setBackSide(false);
oldCard.setState(oldCard.getFaceupCardStateName(), true);
oldCard.unanimateBestow();
if (ability.isDisturb() || ability.hasParam("CastTransformed")) {
oldCard.undoIncrementTransformedTimestamp();
}
if (ability.hasParam("Prototype")) {
oldCard.removeCloneState(oldCard.getPrototypeTimestamp());

View File

@@ -239,6 +239,10 @@ public final class AbilityFactory {
spellAbility.putParam("PrecostDesc", "Exhaust — ");
}
if (mapParams.containsKey("Named")) {
spellAbility.setName(mapParams.get("Named"));
}
// *********************************************
// set universal properties of the SpellAbility
@@ -359,9 +363,6 @@ public final class AbilityFactory {
if (mapParams.containsKey("TargetUnique")) {
abTgt.setUniqueTargets(true);
}
if (mapParams.containsKey("TargetsFromSingleZone")) {
abTgt.setSingleZone(true);
}
if (mapParams.containsKey("TargetsWithoutSameCreatureType")) {
abTgt.setWithoutSameCreatureType(true);
}

View File

@@ -1870,6 +1870,14 @@ public class AbilityUtils {
}
return doXMath(v, expr, c, ctb);
}
// Count$FromNamedAbility[abilityName].<True>.<False>
if (sq[0].startsWith("FromNamedAbility")) {
String abilityNamed = sq[0].substring(16);
SpellAbility trigSA = sa.getHostCard().getCastSA();
boolean fromNamedAbility = trigSA != null && trigSA.getName().equals(abilityNamed);
return doXMath(calculateAmount(c, sq[fromNamedAbility ? 1 : 2], ctb), expr, c, ctb);
}
} else {
// fallback if ctb isn't a spellability
if (sq[0].startsWith("LastStateBattlefield")) {

View File

@@ -928,7 +928,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
List<ZoneType> origin = Lists.newArrayList();
if (sa.hasParam("Origin")) {
origin = ZoneType.listValueOf(sa.getParam("Origin"));
origin.addAll(ZoneType.listValueOf(sa.getParam("Origin")));
}
ZoneType destination = ZoneType.smartValueOf(sa.getParam("Destination"));
@@ -1474,7 +1474,7 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
}
if (ZoneType.Exile.equals(destination) && sa.hasParam("WithCountersType")) {
CounterType cType = CounterType.getType(sa.getParam("WithCountersType"));
int cAmount = AbilityUtils.calculateAmount(sa.getOriginalHost(), sa.getParamOrDefault("WithCountersAmount", "1"), sa);
int cAmount = AbilityUtils.calculateAmount(source, sa.getParamOrDefault("WithCountersAmount", "1"), sa);
GameEntityCounterTable table = new GameEntityCounterTable();
movedCard.addCounter(cType, cAmount, player, table);
table.replaceCounterEffect(game, sa, true);

View File

@@ -287,22 +287,17 @@ public class CopyPermanentEffect extends TokenEffectBase {
int id = newOwner == null ? 0 : newOwner.getGame().nextCardId();
// need to create a physical card first, i need the original card faces
copy = CardFactory.getCard(original.getPaperCard(), newOwner, id, host.getGame());
copy.setStates(CardFactory.getCloneStates(original, copy, sa));
// force update the now set State
if (original.isTransformable()) {
copy.setState(original.isTransformed() ? CardStateName.Backside : CardStateName.Original, true, true);
// 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();
}
}
copy.setStates(CardFactory.getCloneStates(original, copy, sa));
// force update the now set State
if (original.isTransformable()) {
copy.setState(original.isTransformed() ? CardStateName.Backside : CardStateName.Original, true, true);
} else {
copy.setState(copy.getCurrentStateName(), true, true);
}

View File

@@ -18,6 +18,10 @@ public class DamageResolveEffect extends SpellAbilityEffect {
@Override
public void resolve(SpellAbility sa) {
CardDamageMap damageMap = sa.getDamageMap();
if (damageMap == null) {
// this can happen if damagesource was missing
return;
}
CardDamageMap preventMap = sa.getPreventMap();
GameEntityCounterTable counterTable = sa.getCounterTable();

View File

@@ -428,6 +428,10 @@ public class PlayEffect extends SpellAbilityEffect {
tgtSA.getTargetRestrictions().setMandatory(true);
}
if (sa.hasParam("Named")) {
tgtSA.setName(sa.getName());
}
// can't be done later
if (sa.hasParam("ReplaceGraveyard")) {
if (!sa.hasParam("ReplaceGraveyardValid")

View File

@@ -92,12 +92,11 @@ public class SacrificeEffect extends SpellAbilityEffect {
CardZoneTable zoneMovements = AbilityKey.addCardZoneTableParams(params, sa);
if (valid.equals("Self") && game.getZoneOf(host) != null) {
if (host.getController().equals(activator) && game.getZoneOf(host).is(ZoneType.Battlefield)) {
if (!optional || activator.getController().confirmAction(sa, null,
Localizer.getInstance().getMessage("lblDoYouWantSacrificeThis", host.getName()), null)) {
if (game.getAction().sacrifice(new CardCollection(host), sa, true, params) != null && remSacrificed) {
host.addRemembered(host);
}
if (host.getController().equals(activator) && game.getZoneOf(host).is(ZoneType.Battlefield) &&
(!optional || activator.getController().confirmAction(sa, null,
Localizer.getInstance().getMessage("lblDoYouWantSacrificeThis", host.getName()), null))) {
if (game.getAction().sacrifice(new CardCollection(host), sa, true, params) != null && remSacrificed) {
host.addRemembered(host);
}
}
} else {

View File

@@ -9,6 +9,7 @@ import forge.game.GameEntityCounterTable;
import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CounterType;
@@ -20,7 +21,6 @@ import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import forge.util.CardTranslation;
import forge.util.Localizer;
import forge.util.collect.FCollection;
public class TimeTravelEffect extends SpellAbilityEffect {
@@ -41,10 +41,8 @@ public class TimeTravelEffect extends SpellAbilityEffect {
final CounterType counterType = CounterEnumType.TIME;
for (int i = 0; i < num; i++) {
FCollection<Card> list = new FCollection<>();
// card you own that is suspended
list.addAll(CardLists.filter(activator.getCardsIn(ZoneType.Exile), CardPredicates.hasSuspend()));
CardCollection list = CardLists.filter(activator.getCardsIn(ZoneType.Exile), CardPredicates.hasSuspend());
// permanent you control with time counter
list.addAll(CardLists.filter(activator.getCardsIn(ZoneType.Battlefield), CardPredicates.hasCounter(counterType)));

View File

@@ -257,7 +257,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
private long worldTimestamp = -1;
private long bestowTimestamp = -1;
private long transformedTimestamp = 0;
private long transformedTimestamp = -1;
private long prototypeTimestamp = -1;
private long mutatedTimestamp = -1;
private int timesMutated = 0;
@@ -425,8 +425,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
public long getPrototypeTimestamp() { return prototypeTimestamp; }
public long getTransformedTimestamp() { return transformedTimestamp; }
public void incrementTransformedTimestamp() { this.transformedTimestamp++; }
public void undoIncrementTransformedTimestamp() { this.transformedTimestamp--; }
public void setTransformedTimestamp(long ts) { this.transformedTimestamp = ts; }
// The following methods are used to selectively update certain view components (text,
// P/T, card types) in order to avoid card flickering due to aggressive full update
@@ -696,7 +695,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromCard(this);
getGame().getTriggerHandler().runTrigger(TriggerType.Transformed, runParams, false);
}
incrementTransformedTimestamp();
setTransformedTimestamp(ts);
return retResult;
} else if (mode.equals("Flip")) {
@@ -1070,7 +1069,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
}
public final boolean isDoubleFaced() {
return isTransformable() || isMeldable() || isModal();
return isTransformable() || isMeldable();
}
public final boolean isFlipCard() {
@@ -1132,7 +1131,10 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
}
public final boolean isTransformed() {
return getTransformedTimestamp() != 0;
if (isMeldable() || hasMergedCard()) {
return false;
}
return this.isTransformable() && isBackSide();
}
public final boolean isFlipped() {
@@ -2263,7 +2265,7 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
public final ColorSet getMarkedColors() {
if (markedColor == null) {
return ColorSet.getNullColor();
return ColorSet.NO_COLORS;
}
return markedColor;
}
@@ -7638,9 +7640,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
if (sa.isBestow()) {
animateBestow();
}
if (sa.isDisturb() || sa.hasParam("CastTransformed")) {
incrementTransformedTimestamp();
}
if (sa.hasParam("Prototype") && prototypeTimestamp == -1) {
long next = game.getNextTimestamp();
addCloneState(CardFactory.getCloneStates(this, this, sa), next);

View File

@@ -131,9 +131,7 @@ public class CardCopyService {
c.setState(in.getCurrentStateName(), false);
c.setRules(in.getRules());
if (in.isTransformed()) {
c.incrementTransformedTimestamp();
}
c.setBackSide(in.isBackSide());
return c;
}
@@ -168,9 +166,6 @@ public class CardCopyService {
// The characteristics of its front and back face are determined by the copiable values of the same face of the spell it is a copy of, as modified by any other copy effects.
// If the spell it is a copy of has its back face up, the copy is created with its back face up. The token thats put onto the battlefield as that spell resolves is a transforming token.
to.setBackSide(copyFrom.isBackSide());
if (copyFrom.isTransformed()) {
to.incrementTransformedTimestamp();
}
} else if (fromIsTransformedCard) {
copyState(copyFrom, copyFrom.getCurrentStateName(), to, CardStateName.Original);
} else {
@@ -274,9 +269,6 @@ public class CardCopyService {
}
newCopy.setFlipped(copyFrom.isFlipped());
newCopy.setBackSide(copyFrom.isBackSide());
if (copyFrom.isTransformed()) {
newCopy.incrementTransformedTimestamp();
}
if (newCopy.hasAlternateState()) {
newCopy.setState(copyFrom.getCurrentStateName(), false, true);
}

View File

@@ -87,22 +87,16 @@ public class CardFactory {
// need to create a physical card first, i need the original card faces
final Card copy = getCard(original.getPaperCard(), controller, id, game);
copy.setStates(getCloneStates(original, copy, sourceSA));
// force update the now set State
if (original.isTransformable()) {
copy.setState(original.isTransformed() ? CardStateName.Backside : CardStateName.Original, true, true);
// 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();
}
}
copy.setStates(getCloneStates(original, copy, sourceSA));
// force update the now set State
if (original.isTransformable()) {
copy.setState(original.isTransformed() ? CardStateName.Backside : CardStateName.Original, true, true);
} else {
copy.setState(copy.getCurrentStateName(), true, true);
}

View File

@@ -62,6 +62,9 @@ import java.util.*;
public class PhaseHandler implements java.io.Serializable {
private static final long serialVersionUID = 5207222278370963197L;
// used for debugging phase timing
private final StopWatch sw = new StopWatch();
// Start turn at 0, since we start even before first untap
private PhaseType phase = null;
private int turn = 0;
@@ -92,6 +95,7 @@ public class PhaseHandler implements java.io.Serializable {
private final transient Game game;
public PhaseHandler(final Game game0) {
game = game0;
}
@@ -1003,12 +1007,7 @@ public class PhaseHandler implements java.io.Serializable {
private final static boolean DEBUG_PHASES = false;
public void startFirstTurn(Player goesFirst) {
startFirstTurn(goesFirst, null);
}
public void startFirstTurn(Player goesFirst, Runnable startGameHook) {
StopWatch sw = new StopWatch();
public void setupFirstTurn(Player goesFirst, Runnable startGameHook) {
if (phase != null) {
throw new IllegalStateException("Turns already started, call this only once per game");
}
@@ -1024,132 +1023,146 @@ public class PhaseHandler implements java.io.Serializable {
startGameHook.run();
givePriorityToPlayer = true;
}
}
public void startFirstTurn(Player goesFirst) {
startFirstTurn(goesFirst, null);
}
public void startFirstTurn(Player goesFirst, Runnable startGameHook) {
setupFirstTurn(goesFirst, startGameHook);
mainGameLoop();
}
public void mainGameLoop() {
// MAIN GAME LOOP
while (!game.isGameOver()) {
if (givePriorityToPlayer) {
if (DEBUG_PHASES) {
sw.start();
}
while (!game.isGameOver() && !(game.getAge() == GameStage.RestartedByKarn)) {
mainLoopStep();
}
}
game.fireEvent(new GameEventPlayerPriority(playerTurn, phase, getPriorityPlayer()));
List<SpellAbility> chosenSa = null;
int loopCount = 0;
do {
if (checkStateBasedEffects()) {
// state-based effects check could lead to game over
return;
}
game.stashGameState();
chosenSa = pPlayerPriority.getController().chooseSpellAbilityToPlay();
// this needs to come after chosenSa so it sees you conceding on own turn
if (playerTurn.hasLost() && pPlayerPriority.equals(playerTurn) && pFirstPriority.equals(playerTurn)) {
// If the active player has lost, and they have priority, set the next player to have priority
System.out.println("Active player is no longer in the game...");
pPlayerPriority = game.getNextPlayerAfter(getPriorityPlayer());
pFirstPriority = pPlayerPriority;
}
if (chosenSa == null) {
break; // that means 'I pass'
}
if (DEBUG_PHASES) {
System.out.print("... " + pPlayerPriority + " plays " + chosenSa);
}
boolean rollback = false;
for (SpellAbility sa : chosenSa) {
Card saHost = sa.getHostCard();
final Zone originZone = saHost.getZone();
final CardZoneTable triggerList = new CardZoneTable(game.getLastStateBattlefield(), game.getLastStateGraveyard());
if (pPlayerPriority.getController().playChosenSpellAbility(sa)) {
// 117.3c If a player has priority when they cast a spell, activate an ability, [play a land]
// that player receives priority afterward.
pFirstPriority = pPlayerPriority; // all opponents have to pass before stack is allowed to resolve
} else if (game.EXPERIMENTAL_RESTORE_SNAPSHOT) {
rollback = true;
}
saHost = game.getCardState(saHost);
final Zone currentZone = saHost.getZone();
// Need to check if Zone did change
if (currentZone != null && originZone != null && !currentZone.equals(originZone) && (sa.isSpell() || sa.isLandAbility())) {
// currently there can be only one Spell put on the Stack at once, or Land Abilities be played
triggerList.put(originZone.getZoneType(), currentZone.getZoneType(), saHost);
triggerList.triggerChangesZoneAll(game, sa);
}
}
// Don't copy last state if we're in the middle of rolling back a spell...
if (!rollback) {
game.copyLastState();
}
loopCount++;
} while (loopCount < 999 || !pPlayerPriority.getController().isAI());
if (loopCount >= 999 && pPlayerPriority.getController().isAI()) {
System.out.print("AI looped too much with: " + chosenSa);
}
if (DEBUG_PHASES) {
sw.stop();
System.out.print("... passed in " + sw.getTime()/1000f + " s\n");
System.out.println("\t\tStack: " + game.getStack());
sw.reset();
}
}
else if (DEBUG_PHASES) {
System.out.print(" >> (no priority given to " + getPriorityPlayer() + ")\n");
public void mainLoopStep() {
if (givePriorityToPlayer) {
if (DEBUG_PHASES) {
sw.start();
}
// actingPlayer is the player who may act
// the firstAction is the player who gained Priority First in this segment
// of Priority
Player nextPlayer = game.getNextPlayerAfter(getPriorityPlayer());
game.fireEvent(new GameEventPlayerPriority(playerTurn, phase, getPriorityPlayer()));
List<SpellAbility> chosenSa = null;
if (game.isGameOver() || nextPlayer == null) { return; } // conceded?
int loopCount = 0;
do {
if (checkStateBasedEffects()) {
// state-based effects check could lead to game over
return;
}
game.stashGameState();
chosenSa = pPlayerPriority.getController().chooseSpellAbilityToPlay();
// this needs to come after chosenSa so it sees you conceding on own turn
if (playerTurn.hasLost() && pPlayerPriority.equals(playerTurn) && pFirstPriority.equals(playerTurn)) {
// If the active player has lost, and they have priority, set the next player to have priority
System.out.println("Active player is no longer in the game...");
pPlayerPriority = game.getNextPlayerAfter(getPriorityPlayer());
pFirstPriority = pPlayerPriority;
}
if (chosenSa == null) {
break; // that means 'I pass'
}
if (DEBUG_PHASES) {
System.out.print("... " + pPlayerPriority + " plays " + chosenSa);
}
boolean rollback = false;
for (SpellAbility sa : chosenSa) {
Card saHost = sa.getHostCard();
final Zone originZone = saHost.getZone();
final CardZoneTable triggerList = new CardZoneTable(game.getLastStateBattlefield(), game.getLastStateGraveyard());
if (pPlayerPriority.getController().playChosenSpellAbility(sa)) {
// 117.3c If a player has priority when they cast a spell, activate an ability, [play a land]
// that player receives priority afterward.
pFirstPriority = pPlayerPriority; // all opponents have to pass before stack is allowed to resolve
} else if (game.EXPERIMENTAL_RESTORE_SNAPSHOT) {
rollback = true;
}
saHost = game.getCardState(saHost);
final Zone currentZone = saHost.getZone();
// Need to check if Zone did change
if (currentZone != null && originZone != null && !currentZone.equals(originZone) && (sa.isSpell() || sa.isLandAbility())) {
// currently there can be only one Spell put on the Stack at once, or Land Abilities be played
triggerList.put(originZone.getZoneType(), currentZone.getZoneType(), saHost);
triggerList.triggerChangesZoneAll(game, sa);
}
}
// Don't copy last state if we're in the middle of rolling back a spell...
if (!rollback) {
game.copyLastState();
}
loopCount++;
} while (loopCount < 999 || !pPlayerPriority.getController().isAI());
if (loopCount >= 999 && pPlayerPriority.getController().isAI()) {
System.out.print("AI looped too much with: " + chosenSa);
}
if (DEBUG_PHASES) {
System.out.println(TextUtil.concatWithSpace(playerTurn.toString(),TextUtil.addSuffix(phase.toString(),":"), pPlayerPriority.toString(),"is active, previous was", nextPlayer.toString()));
sw.stop();
System.out.print("... passed in " + sw.getTime()/1000f + " s\n");
System.out.println("\t\tStack: " + game.getStack());
sw.reset();
}
if (pFirstPriority == nextPlayer) {
if (game.getStack().isEmpty()) {
if (playerTurn.hasLost()) {
setPriority(game.getNextPlayerAfter(playerTurn));
} else {
setPriority(playerTurn);
}
}
else if (DEBUG_PHASES) {
System.out.print(" >> (no priority given to " + getPriorityPlayer() + ")\n");
}
// end phase
givePriorityToPlayer = true;
onPhaseEnd();
advanceToNextPhase();
onPhaseBegin();
// actingPlayer is the player who may act
// the firstAction is the player who gained Priority First in this segment
// of Priority
Player nextPlayer = game.getNextPlayerAfter(getPriorityPlayer());
if (game.isGameOver() || nextPlayer == null) { return; } // conceded?
if (DEBUG_PHASES) {
System.out.println(TextUtil.concatWithSpace(playerTurn.toString(),TextUtil.addSuffix(phase.toString(),":"), pPlayerPriority.toString(),"is active, previous was", nextPlayer.toString()));
}
if (pFirstPriority == nextPlayer) {
if (game.getStack().isEmpty()) {
if (playerTurn.hasLost()) {
setPriority(game.getNextPlayerAfter(playerTurn));
} else {
setPriority(playerTurn);
}
else if (!game.getStack().hasSimultaneousStackEntries()) {
game.getStack().resolveStack();
}
} else {
// pass the priority to other player
pPlayerPriority = nextPlayer;
}
// If ever the karn's ultimate resolved
if (game.getAge() == GameStage.RestartedByKarn) {
setPhase(null);
game.updatePhaseForView();
game.fireEvent(new GameEventGameRestarted(playerTurn));
return;
// end phase
givePriorityToPlayer = true;
onPhaseEnd();
advanceToNextPhase();
onPhaseBegin();
}
else if (!game.getStack().hasSimultaneousStackEntries()) {
game.getStack().resolveStack();
}
} else {
// pass the priority to other player
pPlayerPriority = nextPlayer;
}
// update Priority for all players
for (final Player p : game.getPlayers()) {
p.setHasPriority(getPriorityPlayer() == p);
}
// If ever the karn's ultimate resolved
if (game.getAge() == GameStage.RestartedByKarn) {
setPhase(null);
game.updatePhaseForView();
game.fireEvent(new GameEventGameRestarted(playerTurn));
return;
}
// update Priority for all players
for (final Player p : game.getPlayers()) {
p.setHasPriority(getPriorityPlayer() == p);
}
}

View File

@@ -1120,13 +1120,14 @@ public class Player extends GameEntity implements Comparable<Player> {
getGame().fireEvent(new GameEventSurveil(this, numToTop, numToGrave));
}
surveilThisTurn++;
final Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(this);
runParams.put(AbilityKey.FirstTime, surveilThisTurn == 1);
runParams.put(AbilityKey.FirstTime, surveilThisTurn == 0);
if (params != null) {
runParams.putAll(params);
}
getGame().getTriggerHandler().runTrigger(TriggerType.Surveil, runParams, false);
surveilThisTurn++;
}
public int getSurveilThisTurn() {

View File

@@ -280,15 +280,8 @@ public class ReplacementHandler {
host = game.getCardState(host);
}
if (replacementEffect.getOverridingAbility() == null && replacementEffect.hasParam("ReplaceWith")) {
// TODO: the source of replacement effect should be the source of the original effect
effectSA = AbilityFactory.getAbility(host, replacementEffect.getParam("ReplaceWith"), replacementEffect);
//replacementEffect.setOverridingAbility(effectSA);
//effectSA.setTrigger(true);
} else if (replacementEffect.getOverridingAbility() != null) {
effectSA = replacementEffect.getOverridingAbility();
}
// TODO: the source of replacement effect should be the source of the original effect
effectSA = replacementEffect.ensureAbility();
if (effectSA != null) {
SpellAbility tailend = effectSA;
do {

View File

@@ -174,6 +174,8 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
private CardZoneTable changeZoneTable;
private Map<Player, Integer> loseLifeMap;
private String name = "";
public CardCollection getLastStateBattlefield() {
return lastStateBattlefield;
}
@@ -1246,6 +1248,9 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
clone.mayChooseNewTargets = false;
clone.triggeringObjects = AbilityKey.newMap(this.triggeringObjects);
if (!lki) {
clone.replacingObjects = AbilityKey.newMap();
}
clone.setPayCosts(getPayCosts().copy());
if (manaPart != null) {
@@ -2675,4 +2680,12 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
public void clearOptionalKeywordAmount() {
optionalKeywordAmount.clear();
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

View File

@@ -55,7 +55,6 @@ public class TargetRestrictions {
// Additional restrictions that may not fit into Valid
private boolean uniqueTargets = false;
private boolean singleZone = false;
private boolean forEachPlayer = false;
private boolean differentControllers = false;
private boolean differentCMC = false;
@@ -100,7 +99,6 @@ public class TargetRestrictions {
this.tgtZone = target.getZone();
this.saValidTargeting = target.getSAValidTargeting();
this.uniqueTargets = target.isUniqueTargets();
this.singleZone = target.isSingleZone();
this.forEachPlayer = target.isForEachPlayer();
this.differentControllers = target.isDifferentControllers();
this.differentCMC = target.isDifferentCMC();
@@ -538,12 +536,6 @@ public class TargetRestrictions {
public final void setUniqueTargets(final boolean unique) {
this.uniqueTargets = unique;
}
public final boolean isSingleZone() {
return this.singleZone;
}
public final void setSingleZone(final boolean single) {
this.singleZone = single;
}
public boolean isWithoutSameCreatureType() {
return withoutSameCreatureType;
}

View File

@@ -392,6 +392,10 @@ public abstract class Trigger extends TriggerReplacementBase {
}
}
if (condition == null) {
return true;
}
if ("LifePaid".equals(condition)) {
final SpellAbility trigSA = (SpellAbility) runParams.get(AbilityKey.SpellAbility);
if (trigSA != null && trigSA.getAmountLifePaid() <= 0) {
@@ -442,7 +446,15 @@ public abstract class Trigger extends TriggerReplacementBase {
if (game.getCombat().getAttackersAndDefenders().values().containsAll(attacker.getOpponents())) {
return false;
}
} else if (condition.startsWith("FromNamedAbility")) {
var rest = condition.substring(16);
final SpellAbility trigSA = (SpellAbility) runParams.get(AbilityKey.Cause);
if (trigSA != null && !trigSA.getName().equals(rest)) {
return false;
}
}
return true;
}

View File

@@ -464,7 +464,7 @@ public class TrackableTypes {
public static final TrackableType<ColorSet> ColorSetType = new TrackableType<ColorSet>() {
@Override
public ColorSet getDefaultValue() {
return ColorSet.getNullColor();
return ColorSet.NO_COLORS;
}
@Override

View File

@@ -23,7 +23,7 @@
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>${revision}</version>
<version>2.0.06</version>
</parent>
<artifactId>forge-gui-android</artifactId>

View File

@@ -4,7 +4,7 @@
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>${revision}</version>
<version>2.0.06</version>
</parent>
<artifactId>forge-gui-desktop</artifactId>

View File

@@ -6,7 +6,7 @@ import java.awt.Graphics;
import javax.swing.JTable;
import forge.card.ColorSet;
import forge.card.mana.ManaCostShard;
import forge.card.MagicColor;
import forge.toolbox.CardFaceSymbols;
public class ColorSetRenderer extends ItemCellRenderer {
@@ -33,7 +33,7 @@ public class ColorSetRenderer extends ItemCellRenderer {
this.cs = (ColorSet) value;
}
else {
this.cs = ColorSet.getNullColor();
this.cs = ColorSet.NO_COLORS;
}
this.setToolTipText(cs.toString());
return super.getTableCellRendererComponent(table, "", isSelected, hasFocus, row, column);
@@ -57,8 +57,8 @@ public class ColorSetRenderer extends ItemCellRenderer {
final int offsetIfNoSpace = cntGlyphs > 1 ? (cellWidth - padding0 - elemtWidth) / (cntGlyphs - 1) : elemtWidth + elemtGap;
final int dx = Math.min(elemtWidth + elemtGap, offsetIfNoSpace);
for (final ManaCostShard s : cs.getOrderedShards()) {
CardFaceSymbols.drawManaSymbol(s.getImageKey(), g, x, y);
for (final MagicColor.Color s : cs.getOrderedColors()) {
CardFaceSymbols.drawManaSymbol(s.getShortName(), g, x, y);
x += dx;
}
}

View File

@@ -8,6 +8,7 @@ import java.util.StringTokenizer;
import com.esotericsoftware.minlog.Log;
import forge.card.ColorSet;
import forge.card.MagicColor;
import forge.card.mana.ManaCost;
import forge.card.mana.ManaCostShard;
import forge.gui.GuiBase;
@@ -204,9 +205,9 @@ public class CardFaceSymbols {
}
public static void drawColorSet(Graphics g, ColorSet colorSet, int x, int y, int imageSize, boolean vertical) {
for (final ManaCostShard s : colorSet.getOrderedShards()) {
if (DECK_COLORSET.get(s.getImageKey())!=null)
FSkin.drawImage(g, DECK_COLORSET.get(s.getImageKey()), x, y, imageSize, imageSize);
for (final MagicColor.Color s : colorSet.getOrderedColors()) {
if (DECK_COLORSET.get(s.getShortName())!=null)
FSkin.drawImage(g, DECK_COLORSET.get(s.getShortName()), x, y, imageSize, imageSize);
if (!vertical)
x += imageSize;
else

View File

@@ -0,0 +1,80 @@
package forge.ai;
import forge.game.Game;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.zone.ZoneType;
import org.testng.AssertJUnit;
import org.testng.annotations.Test;
public class AIIntegrationTests extends AITest {
@Test
public void testSwingForLethal() {
Game game = initAndCreateGame();
Player p = game.getPlayers().get(1);
p.setTeam(0);
addCard("Nest Robber", p);
addCard("Nest Robber", p);
Player opponent = game.getPlayers().get(0);
opponent.setTeam(1);
addCard("Runeclaw Bear", opponent);
opponent.setLife(2, null);
this.playUntilPhase(game, PhaseType.END_OF_TURN);
AssertJUnit.assertTrue(game.isGameOver());
}
@Test
public void testSuspendAI() {
// Test that the AI can cast a suspend spell
// They should suspend it, then deal three damage to their opponent
Game game = initAndCreateGame();
Player p = game.getPlayers().get(1);
p.setTeam(0);
addCard("Mountain", p);
addCardToZone("Rift Bolt", p, ZoneType.Hand);
Player opponent = game.getPlayers().get(0);
opponent.setTeam(1);
// Fill deck with lands so we can goldfish a few turns
for (int i = 0; i < 60; i++) {
addCardToZone("Island", opponent, ZoneType.Library);
// Add something they can't cast
addCardToZone("Stone Golem", p, ZoneType.Library);
}
for (int i = 0; i < 3; i++) {
this.playUntilNextTurn(game);
}
AssertJUnit.assertEquals(17, opponent.getLife());
}
@Test
public void testAttackTriggers() {
// Test that attack triggers actually trigger
Game game = initAndCreateGame();
Player p = game.getPlayers().get(1);
p.setTeam(0);
addCard("Hellrider", p);
addCard("Raging Goblin", p);
Player opponent = game.getPlayers().get(0);
opponent.setTeam(1);
// Fill deck with lands so we can goldfish a few turns
for (int i = 0; i < 60; i++) {
addCardToZone("Island", opponent, ZoneType.Library);
// Add something they can't cast
addCardToZone("Stone Golem", p, ZoneType.Library);
}
this.playUntilNextTurn(game);
AssertJUnit.assertEquals(14, opponent.getLife());
}
}

View File

@@ -0,0 +1,190 @@
package forge.ai;
import java.util.ArrayList;
import java.util.List;
import com.google.common.collect.Lists;
import forge.GuiDesktop;
import forge.StaticData;
import forge.deck.Deck;
import forge.game.Game;
import forge.game.GameRules;
import forge.game.GameStage;
import forge.game.GameType;
import forge.game.Match;
import forge.game.card.Card;
import forge.game.card.CardCollectionView;
import forge.game.card.CardFactory;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.player.RegisteredPlayer;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import forge.gui.GuiBase;
import forge.item.IPaperCard;
import forge.item.PaperToken;
import forge.localinstance.properties.ForgePreferences.FPref;
import forge.model.FModel;
public class AITest {
private static boolean initialized = false;
public Game resetGame() {
// need to be done after FModel.initialize, or the Localizer isn't loaded yet
List<RegisteredPlayer> players = Lists.newArrayList();
Deck d1 = new Deck();
players.add(new RegisteredPlayer(d1).setPlayer(new LobbyPlayerAi("p2", null)));
players.add(new RegisteredPlayer(d1).setPlayer(new LobbyPlayerAi("p1", null)));
GameRules rules = new GameRules(GameType.Constructed);
Match match = new Match(rules, players, "Test");
Game game = new Game(players, rules, match);
Player p = game.getPlayers().get(1);
game.setAge(GameStage.Play);
game.getPhaseHandler().devModeSet(PhaseType.MAIN1, p);
game.getPhaseHandler().onStackResolved();
// game.getAction().checkStateEffects(true);
return game;
}
protected Game initAndCreateGame() {
if (!initialized) {
GuiBase.setInterface(new GuiDesktop());
FModel.initialize(null, preferences -> {
preferences.setPref(FPref.LOAD_CARD_SCRIPTS_LAZILY, false);
preferences.setPref(FPref.UI_LANGUAGE, "en-US");
return null;
});
initialized = true;
}
return resetGame();
}
protected int countCardsWithName(Game game, String name) {
int i = 0;
for (Card c : game.getCardsIn(ZoneType.Battlefield)) {
if (c.getName().equals(name)) {
i++;
}
}
return i;
}
protected Card findCardWithName(Game game, String name) {
for (Card c : game.getCardsIn(ZoneType.Battlefield)) {
if (c.getName().equals(name)) {
return c;
}
}
return null;
}
protected SpellAbility findSAWithPrefix(Card c, String prefix) {
return findSAWithPrefix(c.getSpellAbilities(), prefix);
}
protected SpellAbility findSAWithPrefix(Iterable<SpellAbility> abilities, String prefix) {
for (SpellAbility sa : abilities) {
if (sa.getDescription().startsWith(prefix)) {
return sa;
}
}
return null;
}
protected Card createCard(String name, Player p) {
IPaperCard paperCard = FModel.getMagicDb().getCommonCards().getCard(name);
if (paperCard == null) {
StaticData.instance().attemptToLoadCard(name);
paperCard = FModel.getMagicDb().getCommonCards().getCard(name);
}
return Card.fromPaperCard(paperCard, p);
}
protected Card addCardToZone(String name, Player p, ZoneType zone) {
Card c = createCard(name, p);
// card need a new Timestamp otherwise Static Abilities might collide
c.setGameTimestamp(p.getGame().getNextTimestamp());
p.getZone(zone).add(c);
return c;
}
protected Card addCard(String name, Player p) {
return addCardToZone(name, p, ZoneType.Battlefield);
}
protected List<Card> addCards(String name, int count, Player p) {
List<Card> cards = Lists.newArrayList();
for (int i = 0; i < count; i++) {
cards.add(addCard(name, p));
}
return cards;
}
protected Card createToken(String name, Player p) {
PaperToken token = FModel.getMagicDb().getAllTokens().getToken(name);
if (token == null) {
System.out.println("Failed to find token name " + name);
return null;
}
return CardFactory.getCard(token, p, p.getGame());
}
protected List<Card> addTokens(String name, int amount, Player p) {
List<Card> cards = new ArrayList<>();
for(int i = 0; i < amount; i++) {
cards.add(addToken(name, p));
}
return cards;
}
protected Card addToken(String name, Player p) {
Card c = createToken(name, p);
// card need a new Timestamp otherwise Static Abilities might collide
c.setGameTimestamp(p.getGame().getNextTimestamp());
p.getZone(ZoneType.Battlefield).add(c);
return c;
}
void playUntilStackClear(Game game) {
do {
game.getPhaseHandler().mainLoopStep();
} while (!game.isGameOver() && !game.getStack().isEmpty());
}
void playUntilPhase(Game game, PhaseType phase) {
do {
game.getPhaseHandler().mainLoopStep();
} while (!game.isGameOver() && !game.getPhaseHandler().is(phase));
}
void playUntilNextTurn(Game game) {
Player current = game.getPhaseHandler().getPlayerTurn();
do {
game.getPhaseHandler().mainLoopStep();
} while (!game.isGameOver() && game.getPhaseHandler().getPlayerTurn().equals(current));
}
protected String gameStateToString(Game game) {
StringBuilder sb = new StringBuilder();
for (ZoneType zone : ZoneType.values()) {
CardCollectionView cards = game.getCardsIn(zone);
if (!cards.isEmpty()) {
sb.append("Zone ").append(zone.name()).append(":\n");
for (Card c : game.getCardsIn(zone)) {
sb.append(" ").append(c);
if (c.isTapped()) {
sb.append(" (T)");
}
sb.append("\n");
}
}
}
return sb.toString();
}
}

View File

@@ -1,15 +1,13 @@
package forge.ai.simulation;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import com.google.common.collect.Lists;
import forge.GuiDesktop;
import forge.StaticData;
import forge.ai.AIOption;
import forge.ai.AITest;
import forge.ai.LobbyPlayerAi;
import forge.ai.simulation.GameStateEvaluator.Score;
import forge.deck.Deck;
@@ -18,21 +16,12 @@ import forge.game.GameRules;
import forge.game.GameStage;
import forge.game.GameType;
import forge.game.Match;
import forge.game.card.Card;
import forge.game.card.CardCollectionView;
import forge.game.card.CardFactory;
import forge.game.player.Player;
import forge.game.player.RegisteredPlayer;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import forge.gui.GuiBase;
import forge.item.IPaperCard;
import forge.item.PaperToken;
import forge.localinstance.properties.ForgePreferences.FPref;
import forge.model.FModel;
public class SimulationTest {
private static boolean initialized = false;
public class SimulationTest extends AITest {
public Game resetGame() {
// need to be done after FModel.initialize, or the Localizer isn't loaded yet
@@ -53,19 +42,6 @@ public class SimulationTest {
return game;
}
protected Game initAndCreateGame() {
if (!initialized) {
GuiBase.setInterface(new GuiDesktop());
FModel.initialize(null, preferences -> {
preferences.setPref(FPref.LOAD_CARD_SCRIPTS_LAZILY, false);
preferences.setPref(FPref.UI_LANGUAGE, "en-US");
return null;
});
initialized = true;
}
return resetGame();
}
protected GameSimulator createSimulator(Game game, Player p) {
return new GameSimulator(new SimulationController(new Score(0)) {
@@ -75,106 +51,4 @@ public class SimulationTest {
}
}, game, p, null);
}
protected int countCardsWithName(Game game, String name) {
int i = 0;
for (Card c : game.getCardsIn(ZoneType.Battlefield)) {
if (c.getName().equals(name)) {
i++;
}
}
return i;
}
protected Card findCardWithName(Game game, String name) {
for (Card c : game.getCardsIn(ZoneType.Battlefield)) {
if (c.getName().equals(name)) {
return c;
}
}
return null;
}
protected String gameStateToString(Game game) {
StringBuilder sb = new StringBuilder();
for (ZoneType zone : ZoneType.values()) {
CardCollectionView cards = game.getCardsIn(zone);
if (!cards.isEmpty()) {
sb.append("Zone ").append(zone.name()).append(":\n");
for (Card c : game.getCardsIn(zone)) {
sb.append(" ").append(c).append("\n");
}
}
}
return sb.toString();
}
protected SpellAbility findSAWithPrefix(Card c, String prefix) {
return findSAWithPrefix(c.getSpellAbilities(), prefix);
}
protected SpellAbility findSAWithPrefix(Iterable<SpellAbility> abilities, String prefix) {
for (SpellAbility sa : abilities) {
if (sa.getDescription().startsWith(prefix)) {
return sa;
}
}
return null;
}
protected Card createCard(String name, Player p) {
IPaperCard paperCard = FModel.getMagicDb().getCommonCards().getCard(name);
if (paperCard == null) {
StaticData.instance().attemptToLoadCard(name);
paperCard = FModel.getMagicDb().getCommonCards().getCard(name);
}
return Card.fromPaperCard(paperCard, p);
}
protected Card addCardToZone(String name, Player p, ZoneType zone) {
Card c = createCard(name, p);
// card need a new Timestamp otherwise Static Abilities might collide
c.setGameTimestamp(p.getGame().getNextTimestamp());
p.getZone(zone).add(c);
return c;
}
protected Card addCard(String name, Player p) {
return addCardToZone(name, p, ZoneType.Battlefield);
}
protected List<Card> addCards(String name, int count, Player p) {
List<Card> cards = Lists.newArrayList();
for (int i = 0; i < count; i++) {
cards.add(addCard(name, p));
}
return cards;
}
protected Card createToken(String name, Player p) {
PaperToken token = FModel.getMagicDb().getAllTokens().getToken(name);
if (token == null) {
System.out.println("Failed to find token name " + name);
return null;
}
return CardFactory.getCard(token, p, p.getGame());
}
protected List<Card> addTokens(String name, int amount, Player p) {
List<Card> cards = new ArrayList<>();
for(int i = 0; i < amount; i++) {
cards.add(addToken(name, p));
}
return cards;
}
protected Card addToken(String name, Player p) {
Card c = createToken(name, p);
// card need a new Timestamp otherwise Static Abilities might collide
c.setGameTimestamp(p.getGame().getNextTimestamp());
p.getZone(ZoneType.Battlefield).add(c);
return c;
}
}

View File

@@ -11,7 +11,7 @@
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>${revision}</version>
<version>2.0.06</version>
</parent>
<artifactId>forge-gui-ios</artifactId>

View File

@@ -9,7 +9,7 @@
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>${revision}</version>
<version>2.0.06</version>
</parent>
<artifactId>forge-gui-mobile-dev</artifactId>

View File

@@ -4,7 +4,7 @@
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>${revision}</version>
<version>2.0.06</version>
</parent>
<artifactId>forge-gui-mobile</artifactId>

View File

@@ -101,6 +101,7 @@ public class Forge implements ApplicationListener {
public static boolean allowCardBG = false;
public static boolean altPlayerLayout = false;
public static boolean altZoneTabs = false;
public static String altZoneTabMode = "Off";
public static boolean animatedCardTapUntap = false;
public static String enableUIMask = "Crop";
public static String selector = "Default";
@@ -222,7 +223,7 @@ public class Forge implements ApplicationListener {
reversedPrompt = getForgePreferences().getPrefBoolean(FPref.UI_REVERSE_PROMPT_BUTTON);
autoAIDeckSelection = getForgePreferences().getPrefBoolean(FPref.UI_AUTO_AIDECK_SELECTION);
altPlayerLayout = getForgePreferences().getPrefBoolean(FPref.UI_ALT_PLAYERINFOLAYOUT);
altZoneTabs = getForgePreferences().getPrefBoolean(FPref.UI_ALT_PLAYERZONETABS);
setAltZoneTabMode(getForgePreferences().getPref(FPref.UI_ALT_PLAYERZONETABS));
animatedCardTapUntap = getForgePreferences().getPrefBoolean(FPref.UI_ANIMATED_CARD_TAPUNTAP);
enableUIMask = getForgePreferences().getPref(FPref.UI_ENABLE_BORDER_MASKING);
if (getForgePreferences().getPref(FPref.UI_ENABLE_BORDER_MASKING).equals("true")) //override old settings if not updated
@@ -260,6 +261,17 @@ public class Forge implements ApplicationListener {
FThreads.invokeInBackgroundThread(() -> AssetsDownloader.checkForUpdates(exited, runnable));
}
}
public static void setAltZoneTabMode(String mode) {
Forge.altZoneTabMode = mode;
switch (Forge.altZoneTabMode) {
case "Vertical", "Horizontal" -> Forge.altZoneTabs = true;
case "Off" -> Forge.altZoneTabs = false;
default -> Forge.altZoneTabs = false;
}
}
public static boolean isHorizontalTabLayout() {
return Forge.altZoneTabs && "Horizontal".equalsIgnoreCase(Forge.altZoneTabMode);
}
public static boolean hasGamepad() {
//Classic Mode Various Screen GUI are not yet supported, needs control mapping for each screens
if (isMobileAdventureMode) {
@@ -337,8 +349,11 @@ public class Forge implements ApplicationListener {
GuiBase.setIsAdventureMode(true);
advStartup = false;
isMobileAdventureMode = true;
if (GuiBase.isAndroid()) //force it for adventure mode
altZoneTabs = true;
//force it for adventure mode if the prefs is not updated from boolean value to string value
if ("true".equalsIgnoreCase(FModel.getPreferences().getPref(FPref.UI_ALT_PLAYERZONETABS)) ||
"false".equalsIgnoreCase(FModel.getPreferences().getPref(FPref.UI_ALT_PLAYERZONETABS))) {
setAltZoneTabMode("Vertical");
}
//pixl cursor for adventure
setCursor(null, "0");
if (!GuiBase.isAndroid() || !getDeviceAdapter().getGamepads().isEmpty())
@@ -755,7 +770,7 @@ public class Forge implements ApplicationListener {
isMobileAdventureMode = false;
GuiBase.setIsAdventureMode(false);
setCursor(FSkin.getCursor().get(0), "0");
altZoneTabs = FModel.getPreferences().getPrefBoolean(FPref.UI_ALT_PLAYERZONETABS);
setAltZoneTabMode(FModel.getPreferences().getPref(FPref.UI_ALT_PLAYERZONETABS));
Gdx.input.setInputProcessor(getInputProcessor());
clearTransitionScreen();
openHomeDefault();
@@ -1468,7 +1483,7 @@ public class Forge implements ApplicationListener {
boolean handled;
if (KeyInputAdapter.isShiftKeyDown()) {
handled = pan(mouseMovedX, mouseMovedY, -Utils.AVG_FINGER_WIDTH * amountX, 0, false);
handled = pan(mouseMovedX, mouseMovedY, -Utils.AVG_FINGER_WIDTH * amountY, 0, false);
} else {
handled = pan(mouseMovedX, mouseMovedY, 0, -Utils.AVG_FINGER_HEIGHT * amountY, true);
}

View File

@@ -292,7 +292,7 @@ public class AdventureEventData implements Serializable {
Random placeholder = MyRandom.getRandom();
MyRandom.setRandom(getEventRandom());
if (draft == null && (eventStatus == AdventureEventController.EventStatus.Available || eventStatus == AdventureEventController.EventStatus.Entered)) {
draft = BoosterDraft.createDraft(LimitedPoolType.Block, getCardBlock(), packConfiguration);
draft = BoosterDraft.createDraft(LimitedPoolType.Block, getCardBlock(), packConfiguration, 8);
registeredDeck = draft.getHumanPlayer().getDeck();
assignPlayerNames(draft);
}
@@ -607,15 +607,18 @@ public class AdventureEventData implements Serializable {
description += "Block: " + getCardBlock() + "\n";
description += "Boosters: " + String.join(", ", packConfiguration) + "\n";
description += "Competition Style: " + participants.length + " players, matches played as best of " + eventRules.gamesPerMatch + ", " + (getPairingDescription()) + "\n\n";
description += String.format("Pay 1 Entry Fee\n- Gold %d[][+Gold][BLACK]\n- Mana Shards %d[][+Shards][BLACK]\n", Math.round(eventRules.goldToEnter * townPriceModifier), Math.round(eventRules.shardsToEnter * townPriceModifier));
if (eventRules.acceptsBronzeChallengeCoin) {
description += "- Bronze Challenge Coin [][+BronzeChallengeCoin][BLACK]\n\n";
} else if (eventRules.acceptsSilverChallengeCoin) {
description += "- Silver Challenge Coin [][+SilverChallengeCoin][BLACK]\n\n";
} else if (eventRules.acceptsChallengeCoin) {
description += "- Gold Challenge Coin [][+ChallengeCoin][BLACK]\n\n";
} else {
description += "\n";
if (eventStatus == AdventureEventController.EventStatus.Available) {
description += String.format("Pay 1 Entry Fee\n- Gold %d[][+Gold][BLACK]\n- Mana Shards %d[][+Shards][BLACK]\n", Math.round(eventRules.goldToEnter * townPriceModifier), Math.round(eventRules.shardsToEnter * townPriceModifier));
if (eventRules.acceptsBronzeChallengeCoin) {
description += "- Bronze Challenge Coin [][+BronzeChallengeCoin][BLACK]\n\n";
} else if (eventRules.acceptsSilverChallengeCoin) {
description += "- Silver Challenge Coin [][+SilverChallengeCoin][BLACK]\n\n";
} else if (eventRules.acceptsChallengeCoin) {
description += "- Gold Challenge Coin [][+ChallengeCoin][BLACK]\n\n";
} else {
description += "\n";
}
}
description += String.format("Prizes\nChampion: Keep drafted deck\n2+ round wins: Challenge Coin \n1+ round wins: %s Booster, %s Booster\n0 round wins: %s Booster", rewardPacks[0].getComment(), rewardPacks[1].getComment(), rewardPacks[2].getComment());
} else if (format.equals(AdventureEventController.EventFormat.Jumpstart)) {

View File

@@ -193,6 +193,8 @@ public class DuelScene extends ForgeScene {
public void enter() {
GameHUD.getInstance().unloadAudio();
GameType mainGameType;
boolean isDeckMissing = false;
String isDeckMissingMsg = "";
if (eventData != null && eventData.eventRules != null) {
mainGameType = eventData.eventRules.gameType;
} else {
@@ -295,6 +297,12 @@ public class DuelScene extends ForgeScene {
} else {
deck = currentEnemy.copyPlayerDeck ? this.playerDeck : currentEnemy.generateDeck(Current.player().isFantasyMode(), Current.player().isUsingCustomDeck() || Current.player().isHardorInsaneDifficulty());
}
if (deck == null) {
isDeckMissing = true;
isDeckMissingMsg = "Deck for " + currentEnemy.getName() + " is missing! " + (this.eventData == null ? "Genetic AI deck will be used." : "Player deck will be used.");
System.err.println(isDeckMissingMsg);
deck = this.eventData == null ? Aggregates.random(DeckProxy.getAllGeneticAIDecks()).getDeck() : this.playerDeck;
}
RegisteredPlayer aiPlayer = RegisteredPlayer.forVariants(playerCount, appliedVariants, deck, null, false, null, null);
LobbyPlayer enemyPlayer = GamePlayerUtil.createAiPlayer(currentEnemy.getName(), selectAI(currentEnemy.ai));
@@ -368,9 +376,9 @@ public class DuelScene extends ForgeScene {
hostedMatch.startMatch(rules, appliedVariants, players, guiMap, bossBattle ? MusicPlaylist.BOSS : MusicPlaylist.MATCH);
MatchController.instance.setGameView(hostedMatch.getGameView());
boolean showMessages = enemy.getData().boss || (enemy.getData().copyPlayerDeck && Current.player().isUsingCustomDeck());
if (chaosBattle || showMessages) {
if (chaosBattle || showMessages || isDeckMissing) {
final FBufferedImage fb = getFBEnemyAvatar();
bossDialogue = createFOption(Forge.getLocalizer().getMessage("AdvBossIntro" + Aggregates.randomInt(1, 35)),
bossDialogue = createFOption(isDeckMissing ? isDeckMissingMsg : Forge.getLocalizer().getMessage("AdvBossIntro" + Aggregates.randomInt(1, 35)),
enemy.getName(), fb, fb::dispose);
matchOverlay = new LoadingOverlay(() -> FThreads.delayInEDT(300, () -> FThreads.invokeInEdtNowOrLater(() ->
bossDialogue.show())), false, true);

View File

@@ -259,7 +259,7 @@ public class SettingsScene extends UIScene {
if (!GuiBase.isAndroid()) {
addCheckBox(Forge.getLocalizer().getMessage("lblBattlefieldTextureFiltering"), ForgePreferences.FPref.UI_LIBGDX_TEXTURE_FILTERING);
addCheckBox(Forge.getLocalizer().getMessage("lblAltZoneTabs"), ForgePreferences.FPref.UI_ALT_PLAYERZONETABS);
//addCheckBox(Forge.getLocalizer().getMessage("lblAltZoneTabs"), ForgePreferences.FPref.UI_ALT_PLAYERZONETABS);
} else {
addCheckBox(Forge.getLocalizer().getMessage("lblLandscapeMode") + " (" +
Forge.getLocalizer().getMessage("lblRestartRequired") + ")",

View File

@@ -186,8 +186,8 @@ public class CardFaceSymbols {
public static void drawColorSet(Graphics g, ColorSet colorSet, float x, float y, final float imageSize, boolean vertical) {
final float dx = imageSize;
for (final ManaCostShard s : colorSet.getOrderedShards()) {
drawSymbol(s.getImageKey(), g, x, y, imageSize, imageSize);
for (final MagicColor.Color s : colorSet.getOrderedColors()) {
drawSymbol(s.getShortName(), g, x, y, imageSize, imageSize);
if (!vertical)
x += dx;
else

View File

@@ -49,6 +49,24 @@ public class CardImageRenderer {
return FSkinColor.fromRGB(detailColor.r, detailColor.g, detailColor.b);
}
private static float getCapHeight(FSkinFont fSkinFont) {
if (fSkinFont == null)
return 0f;
return fSkinFont.getCapHeight();
}
private static float getAscent(FSkinFont fSkinFont) {
if (fSkinFont == null)
return 0f;
return fSkinFont.getAscent();
}
private static float getBoundsWidth(String sequence, FSkinFont fSkinFont) {
if (fSkinFont == null)
return 0f;
return fSkinFont.getBounds(sequence).width;
}
public static void forceStaticFieldUpdate() {
//force static fields to be updated the next time a card image is rendered
prevImageWidth = 0;
@@ -143,7 +161,7 @@ public class CardImageRenderer {
x += outerBorderThickness;
y += outerBorderThickness;
w -= 2 * outerBorderThickness;
float headerHeight = Math.max(MANA_SYMBOL_SIZE + 2 * HEADER_PADDING, 2 * NAME_FONT.getCapHeight()) + 2;
float headerHeight = Math.max(MANA_SYMBOL_SIZE + 2 * HEADER_PADDING, 2 * getCapHeight(NAME_FONT)) + 2;
//draw header containing name and mana cost
Color[] headerColors = FSkinColor.tintColors(Color.WHITE, colors, CardRenderer.NAME_BOX_TINT);
@@ -158,15 +176,15 @@ public class CardImageRenderer {
float artWidth = w - 2 * artInset;
float artHeight = !showArtBox ? 0f : artWidth / CardRenderer.CARD_ART_RATIO;
float typeBoxHeight = 2 * TYPE_FONT.getCapHeight();
float typeBoxHeight = 2 * getCapHeight(TYPE_FONT);
float ptBoxHeight = 0;
float textBoxHeight = h - headerHeight - artHeight - typeBoxHeight - outerBorderThickness - artInset;
if (state.isCreature() || state.isPlaneswalker() || state.hasPrintedPT() || state.isBattle()) {
ptBoxHeight = 2 * PT_FONT.getCapHeight();
ptBoxHeight = 2 * getCapHeight(PT_FONT);
}
//space for artist
textBoxHeight -= 2 * PT_FONT.getCapHeight();
textBoxHeight -= 2 * getCapHeight(PT_FONT);
PaperCard paperCard = null;
try {
paperCard = ImageUtil.getPaperCardFromImageKey(state.getImageKey());
@@ -246,7 +264,7 @@ public class CardImageRenderer {
}
//draw artist
if (showArtist)
g.drawOutlinedText(artist, TEXT_FONT, Color.WHITE, Color.DARK_GRAY, x + (TYPE_FONT.getCapHeight() / 2), y + (TYPE_FONT.getCapHeight() / 2), w, h, false, Align.left, false);
g.drawOutlinedText(artist, TEXT_FONT, Color.WHITE, Color.DARK_GRAY, x + (getCapHeight(TYPE_FONT) / 2), y + (getCapHeight(TYPE_FONT) / 2), w, h, false, Align.left, false);
}
private static void drawOutlineColor(Graphics g, ColorSet colors, float x, float y, float w, float h) {
if (colors == null)
@@ -296,7 +314,7 @@ public class CardImageRenderer {
manaCostWidth = CardFaceSymbols.getWidth(otherManaCost, manaSymbolSize) + HEADER_PADDING;
CardFaceSymbols.drawManaCost(g, otherManaCost, x + w - manaCostWidth, y + (h - manaSymbolSize) / 2, manaSymbolSize);
//draw "//" between two parts of mana cost
manaCostWidth += NAME_FONT.getBounds("//").width + HEADER_PADDING;
manaCostWidth += getBoundsWidth("//", NAME_FONT) + HEADER_PADDING;
g.drawText("//", NAME_FONT, Color.BLACK, x + w - manaCostWidth, y, w, h, false, Align.left, true);
}
manaCostWidth += CardFaceSymbols.getWidth(mainManaCost, manaSymbolSize) + HEADER_PADDING;
@@ -517,7 +535,7 @@ public class CardImageRenderer {
} else {
//left
//float headerHeight = Math.max(MANA_SYMBOL_SIZE + 2 * HEADER_PADDING, 2 * TYPE_FONT.getCapHeight()) + 2;
float typeBoxHeight = 2 * TYPE_FONT.getCapHeight();
float typeBoxHeight = 2 * getCapHeight(TYPE_FONT);
drawHeader(g, card, card.getState(true), altcolors, x, y, w - (w / 2), typeBoxHeight, noText, true);
drawTypeLine(g, card.getState(true), canShow, altcolors, x, y + typeBoxHeight, w - (w / 2), typeBoxHeight, noText, true, true);
float mod = (typeBoxHeight + typeBoxHeight);
@@ -714,7 +732,7 @@ public class CardImageRenderer {
return;
}
float padding = TEXT_FONT.getCapHeight() * 0.75f;
float padding = getCapHeight(TEXT_FONT) * 0.75f;
x += padding;
y += padding;
w -= 2 * padding;
@@ -747,15 +765,15 @@ public class CardImageRenderer {
return;
}
float padding = Math.round(PT_FONT.getCapHeight() / 4);
float padding = Math.round(getCapHeight(PT_FONT) / 4);
float totalPieceWidth = -padding;
float[] pieceWidths = new float[pieces.size()];
for (int i = 0; i < pieces.size(); i++) {
float pieceWidth = PT_FONT.getBounds(pieces.get(i)).width + padding;
float pieceWidth = getBoundsWidth(pieces.get(i), PT_FONT) + padding;
pieceWidths[i] = pieceWidth;
totalPieceWidth += pieceWidth;
}
float boxHeight = PT_FONT.getCapHeight() + PT_FONT.getAscent() + 3 * padding;
float boxHeight = getCapHeight(PT_FONT) + getAscent(PT_FONT) + 3 * padding;
float boxWidth = Math.max(PT_BOX_WIDTH, totalPieceWidth + 2 * padding);
x += w - boxWidth;
@@ -951,14 +969,14 @@ public class CardImageRenderer {
x += outerBorderThickness;
y += outerBorderThickness;
w -= 2 * outerBorderThickness;
float cardNameBoxHeight = Math.max(MANA_SYMBOL_SIZE + 2 * HEADER_PADDING, 2 * NAME_FONT.getCapHeight()) + 2 * TYPE_FONT.getCapHeight() + 2;
float cardNameBoxHeight = Math.max(MANA_SYMBOL_SIZE + 2 * HEADER_PADDING, 2 * getCapHeight(NAME_FONT)) + 2 * getCapHeight(TYPE_FONT) + 2;
//draw name/type box
Color[] nameBoxColors = FSkinColor.tintColors(Color.WHITE, colors, CardRenderer.NAME_BOX_TINT);
drawDetailsNameBox(g, card, state, canShow, nameBoxColors, x, y, w, cardNameBoxHeight);
float innerBorderThickness = outerBorderThickness / 2;
float ptBoxHeight = 2 * PT_FONT.getCapHeight();
float ptBoxHeight = 2 * getCapHeight(PT_FONT);
float textBoxHeight = h - cardNameBoxHeight - ptBoxHeight - outerBorderThickness - 3 * innerBorderThickness;
y += cardNameBoxHeight + innerBorderThickness;
@@ -1141,7 +1159,7 @@ public class CardImageRenderer {
float padding = h / 8;
//make sure name/mana cost row height is tall enough for both
h = Math.max(MANA_SYMBOL_SIZE + 2 * HEADER_PADDING, 2 * NAME_FONT.getCapHeight());
h = Math.max(MANA_SYMBOL_SIZE + 2 * HEADER_PADDING, 2 * getCapHeight(NAME_FONT));
//draw mana cost for card
float manaCostWidth = 0;
@@ -1154,7 +1172,7 @@ public class CardImageRenderer {
manaCostWidth = CardFaceSymbols.getWidth(otherManaCost, MANA_SYMBOL_SIZE) + HEADER_PADDING;
CardFaceSymbols.drawManaCost(g, otherManaCost, x + w - manaCostWidth, y + (h - MANA_SYMBOL_SIZE) / 2, MANA_SYMBOL_SIZE);
//draw "//" between two parts of mana cost
manaCostWidth += NAME_FONT.getBounds("//").width + HEADER_PADDING;
manaCostWidth += getBoundsWidth("//", NAME_FONT) + HEADER_PADDING;
g.drawText("//", NAME_FONT, Color.BLACK, x + w - manaCostWidth, y, w, h, false, Align.left, true);
}
manaCostWidth += CardFaceSymbols.getWidth(mainManaCost, MANA_SYMBOL_SIZE) + HEADER_PADDING;
@@ -1168,7 +1186,7 @@ public class CardImageRenderer {
//draw type and set label for card
y += h;
h = 2 * TYPE_FONT.getCapHeight();
h = 2 * getCapHeight(TYPE_FONT);
String set = state.getSetCode();
CardRarity rarity = state.getRarity();
@@ -1189,7 +1207,7 @@ public class CardImageRenderer {
fillColorBackground(g, colors, x, y, w, h);
g.drawRect(BORDER_THICKNESS, Color.BLACK, x, y, w, h);
float padX = TEXT_FONT.getCapHeight() / 2;
float padX = getCapHeight(TEXT_FONT) / 2;
float padY = padX + Utils.scale(2); //add a little more vertical padding
x += padX;
y += padY;
@@ -1202,8 +1220,8 @@ public class CardImageRenderer {
float idWidth = 0;
if (canShow) {
String idText = CardDetailUtil.formatCardId(state);
g.drawText(idText, TYPE_FONT, idForeColor, x, y + TYPE_FONT.getCapHeight() / 2, w, h, false, Align.left, false);
idWidth = TYPE_FONT.getBounds(idText).width;
g.drawText(idText, TYPE_FONT, idForeColor, x, y + getCapHeight(TYPE_FONT) / 2, w, h, false, Align.left, false);
idWidth = getBoundsWidth(idText, TYPE_FONT);
}
String ptText = CardDetailUtil.formatPrimaryCharacteristic(state, canShow);
@@ -1212,7 +1230,7 @@ public class CardImageRenderer {
}
TextBounds bounds = cardTextRenderer.getBounds(ptText, PT_FONT);
float padding = PT_FONT.getCapHeight() / 2;
float padding = getCapHeight(PT_FONT) / 2;
float boxWidth = Math.min(bounds.width + 2 * padding,
w - idWidth - padding); //prevent box overlapping ID
x += w - boxWidth;

View File

@@ -11,7 +11,7 @@ public class ColorSetImage implements FImage {
public ColorSetImage(ColorSet colorSet0) {
colorSet = colorSet0;
shardCount = colorSet.getOrderedShards().length;
shardCount = colorSet.getOrderedColors().size();
}
@Override

View File

@@ -16,7 +16,6 @@ import forge.assets.*;
import forge.assets.FSkinColor.Colors;
import forge.card.*;
import forge.card.CardRenderer.CardStackPosition;
import forge.card.mana.ManaCostShard;
import forge.deck.*;
import forge.deck.io.DeckPreferences;
import forge.game.card.CardView;
@@ -37,7 +36,6 @@ import forge.util.Utils;
import java.util.*;
import java.util.Map.Entry;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import static forge.assets.FSkin.getDefaultSkinFile;
@@ -1100,10 +1098,10 @@ public class ImageView<T extends InventoryItem> extends ItemView<T> {
deckSelectMode = true;
deckProxy = (DeckProxy) item;
}
if (item instanceof PaperCard) {
if (item instanceof PaperCard pc) {
showRanking = itemManager.getShowRanking() && FModel.getPreferences().getPrefBoolean(ForgePreferences.FPref.UI_OVERLAY_DRAFT_RANKING);
if (showRanking) {
double score = CardRanker.getRawScore((PaperCard) item);
double score = CardRanker.getRawScore(pc);
draftRank = score <= 0 ? 0 : score > 99 ? 99 : (int) Math.round(CardRanker.getRawScore((PaperCard) item));
if (draftRank >= 90) {
draftRankImage = FSkinImage.DRAFTRANK_S;
@@ -1115,10 +1113,8 @@ public class ImageView<T extends InventoryItem> extends ItemView<T> {
draftRankImage = FSkinImage.DRAFTRANK_C;
}
}
if (((PaperCard) item).getMarkedColors() != null) {
markedColors = Arrays.stream(((PaperCard) item).getMarkedColors().getOrderedShards())
.map(ManaCostShard::toString)
.collect(Collectors.joining());
if (pc.getMarkedColors() != null) {
markedColors = pc.getMarkedColors().toString();
}
}
if(fnPrice != null) {

View File

@@ -174,10 +174,13 @@ public class MatchController extends AbstractGuiGame {
final VPlayerPanel playerPanel = new VPlayerPanel(p, isLocal || noHumans, players.size());
if (isLocal && !init) {
playerPanels.add(0, playerPanel); //ensure local player always first among player panels
playerPanel.setBottomPlayer(true);
init = true;
}
else {
playerPanels.add(playerPanel);
if (playerPanel.equals(playerPanels.get(0)))
playerPanel.setBottomPlayer(true);
}
}
view = new MatchScreen(playerPanels);

View File

@@ -53,6 +53,12 @@ public abstract class VCardDisplayArea extends VDisplayArea implements ActivateH
rotateCards180 = b0;
}
private float getCardStackOffset() {
if (Forge.isHorizontalTabLayout())
return 0.125f;
return CARD_STACK_OFFSET;
}
protected void refreshCardPanels(Iterable<CardView> model) {
clear();
@@ -78,7 +84,9 @@ public abstract class VCardDisplayArea extends VDisplayArea implements ActivateH
@Override
public void setVisible(boolean b0) {
if (isVisible() == b0) { return; }
if (isVisible() == b0) {
return;
}
super.setVisible(b0);
if (b0) { //when zone becomes visible, ensure display area of panels is updated and panels layed out
for (CardAreaPanel pnl : cardPanels.get()) {
@@ -146,7 +154,7 @@ public abstract class VCardDisplayArea extends VDisplayArea implements ActivateH
CardAreaPanel attachedPanel = attachedPanels.get(i);
if (attachedPanel != null) {
int count = addCards(attachedPanel, x, y, cardWidth, cardHeight);
x += count * cardWidth * CARD_STACK_OFFSET;
x += count * cardWidth * getCardStackOffset();
totalCount += count;
}
}
@@ -156,7 +164,7 @@ public abstract class VCardDisplayArea extends VDisplayArea implements ActivateH
cardPanel.setBounds(x, y, cardWidth, cardHeight);
if (cardPanel.getNextPanelInStack() != null) { //add next panel in stack if needed
x += cardWidth * CARD_STACK_OFFSET;
x += cardWidth * getCardStackOffset();
totalCount += addCards(cardPanel.getNextPanelInStack(), x, y, cardWidth, cardHeight);
}
return totalCount + 1;
@@ -165,6 +173,7 @@ public abstract class VCardDisplayArea extends VDisplayArea implements ActivateH
protected float getCardWidth(float cardHeight) {
return (cardHeight - 2 * FCardPanel.PADDING) / FCardPanel.ASPECT_RATIO + 2 * FCardPanel.PADDING; //ensure aspect ratio maintained after padding applied
}
protected float getCardHeight(float cardWidth) {
return (cardWidth - 2 * FCardPanel.PADDING) * FCardPanel.ASPECT_RATIO + 2 * FCardPanel.PADDING; //ensure aspect ratio maintained after padding applied
}
@@ -181,7 +190,7 @@ public abstract class VCardDisplayArea extends VDisplayArea implements ActivateH
for (CardAreaPanel cardPanel : new ArrayList<>(cardPanels.get())) {
if (cardPanel != null) {
int count = addCards(cardPanel, x, y, cardWidth, cardHeight);
x += cardWidth + (count - 1) * cardWidth * CARD_STACK_OFFSET;
x += cardWidth + (count - 1) * cardWidth * getCardStackOffset();
}
}
@@ -197,7 +206,7 @@ public abstract class VCardDisplayArea extends VDisplayArea implements ActivateH
@Override
public String getActivateAction(int index) {
if(!GuiBase.isNetworkplay()) {
if (!GuiBase.isNetworkplay()) {
//causes lag on netplay client side, also index shouldn't be out of bounds
if (index >= 0 && index < orderedCards.get().size())
return MatchController.instance.getGameController().getActivateDescription(orderedCards.get().get(index));
@@ -266,9 +275,11 @@ public abstract class VCardDisplayArea extends VDisplayArea implements ActivateH
public CardAreaPanel getAttachedToPanel() {
return attachedToPanel;
}
public void setAttachedToPanel(final CardAreaPanel attachedToPanel0) {
attachedToPanel = attachedToPanel0;
}
public List<CardAreaPanel> getAttachedPanels() {
if (attachedPanels == null) {
attachedPanels = new ArrayList<>();
@@ -278,15 +289,19 @@ public abstract class VCardDisplayArea extends VDisplayArea implements ActivateH
}
return attachedPanels;
}
public CardAreaPanel getNextPanelInStack() {
return nextPanelInStack;
}
public void setNextPanelInStack(CardAreaPanel nextPanelInStack0) {
nextPanelInStack = nextPanelInStack0;
}
public CardAreaPanel getPrevPanelInStack() {
return prevPanelInStack;
}
public void setPrevPanelInStack(CardAreaPanel prevPanelInStack0) {
prevPanelInStack = prevPanelInStack0;
}
@@ -324,8 +339,7 @@ public abstract class VCardDisplayArea extends VDisplayArea implements ActivateH
attachedPanels.remove(CardAreaPanel.get(getAttachedto));
setAttachedToPanel(null);
}
}
else {
} else {
setAttachedToPanel(null);
}
}
@@ -423,7 +437,9 @@ public abstract class VCardDisplayArea extends VDisplayArea implements ActivateH
}
public void showZoom() {
if (displayArea == null) { return; }
if (displayArea == null) {
return;
}
final List<CardView> cards = displayArea.orderedCards.get();
CardZoom.show(cards, cards.indexOf(getCard()), displayArea);
@@ -444,10 +460,14 @@ public abstract class VCardDisplayArea extends VDisplayArea implements ActivateH
}
private List<CardView> getOtherCardsToSelect(boolean selectOtherCardsInStack) {
if (!selectOtherCardsInStack) { return null; }
if (!selectOtherCardsInStack) {
return null;
}
//on double-tap select all other cards in stack if any
if (prevPanelInStack == null && nextPanelInStack == null) { return null; }
if (prevPanelInStack == null && nextPanelInStack == null) {
return null;
}
List<CardView> cards = new ArrayList<>();
@@ -490,7 +510,9 @@ public abstract class VCardDisplayArea extends VDisplayArea implements ActivateH
public Vector2 getTargetingArrowOrigin() {
//don't show targeting arrow unless in display area that's visible
if (displayArea == null || !displayArea.isVisible()) { return null; }
if (displayArea == null || !displayArea.isVisible()) {
return null;
}
return getTargetingArrowOrigin(this, isTapped());
}
@@ -517,8 +539,7 @@ public abstract class VCardDisplayArea extends VDisplayArea implements ActivateH
g.startRotateTransform(x + w / 2, y + h / 2, 180);
super.draw(g);
g.endTransform();
}
else {
} else {
super.draw(g);
}
}

View File

@@ -2,6 +2,7 @@ package forge.screens.match.views;
import java.util.*;
import forge.Forge;
import forge.game.card.CardView;
import forge.game.card.CardView.CardStateView;
import forge.game.player.PlayerView;
@@ -33,6 +34,7 @@ public class VField extends FContainer {
public boolean isFlipped() {
return flipped;
}
public void setFlipped(boolean flipped0) {
flipped = flipped0;
}
@@ -61,7 +63,9 @@ public class VField extends FContainer {
clear();
Iterable<CardView> model = player.getBattlefield();
if (model == null) { return; }
if (model == null) {
return;
}
for (CardView card : model) {
CardAreaPanel cardPanel = CardAreaPanel.get(card);
@@ -84,18 +88,15 @@ public class VField extends FContainer {
if (!tryStackCard(card, creatures)) {
creatures.add(card);
}
}
else if (details.isLand()) {
} else if (details.isLand()) {
if (!tryStackCard(card, lands)) {
lands.add(card);
}
}
else if (details.isArtifact() && (details.isContraption() || details.isAttraction())) {
} else if (details.isArtifact() && (details.isContraption() || details.isAttraction())) {
if (contraptions == null)
contraptions = new ArrayList<>();
contraptions.add(card); //Arrange these later.
}
else {
} else {
if (!tryStackCard(card, otherPermanents)) {
otherPermanents.add(card);
}
@@ -103,7 +104,7 @@ public class VField extends FContainer {
}
}
if(contraptions != null) {
if (contraptions != null) {
contraptions = arrangeContraptions(contraptions);
otherPermanents.addAll(contraptions);
}
@@ -111,8 +112,7 @@ public class VField extends FContainer {
if (creatures.isEmpty()) {
row1.refreshCardPanels(otherPermanents);
row2.refreshCardPanels(lands);
}
else {
} else {
row1.refreshCardPanels(creatures);
lands.addAll(otherPermanents);
row2.refreshCardPanels(lands);
@@ -173,13 +173,14 @@ public class VField extends FContainer {
TreeSet<CardView> row = new TreeSet<>((c1, c2) -> {
//Order is sprocket-less cards, then sprocket 1, sprocket 2, sprocket 3, and finally attractions.
int sprocket1 = c1.getSprocket(), sprocket2 = c2.getSprocket();
if(sprocket1 == 0 && c1.getCurrentState().isAttraction())
if (sprocket1 == 0 && c1.getCurrentState().isAttraction())
sprocket1 = 4;
if(sprocket2 == 0 && c2.getCurrentState().isAttraction())
if (sprocket2 == 0 && c2.getCurrentState().isAttraction())
sprocket2 = 4;
return sprocket1 - sprocket2;
});
outer: for (CardView card : contraptions) {
outer:
for (CardView card : contraptions) {
if (card.hasCardAttachments()) {
row.add(card); //Don't stack contraptions or attractions with attachments.
continue;
@@ -187,7 +188,7 @@ public class VField extends FContainer {
if (card.getCurrentState().isAttraction()) {
//Stack attractions with other attractions.
for (CardView c : row) {
if(c.getCurrentState().isAttraction() && !c.hasCardAttachments()) {
if (c.getCurrentState().isAttraction() && !c.hasCardAttachments()) {
stackOnto(card, c);
continue outer;
}
@@ -240,13 +241,17 @@ public class VField extends FContainer {
if (flipped) {
y1 = cardSize;
y2 = 0;
}
else {
} else {
y1 = 0;
y2 = cardSize;
}
row1.setBounds(0, y1, width-fieldModifier, cardSize);
row2.setBounds(0, y2, (width - commandZoneWidth)-fieldModifier, cardSize);
if (Forge.isHorizontalTabLayout()) {
row1.setBounds(0, y1, width, cardSize);
row2.setBounds(0, y2, width, cardSize);
} else {
row1.setBounds(0, y1, width - fieldModifier, cardSize);
row2.setBounds(0, y2, (width - commandZoneWidth) - fieldModifier, cardSize);
}
}
public class FieldRow extends VCardDisplayArea {
@@ -270,13 +275,14 @@ public class VField extends FContainer {
public void setNextSelected(int val) {
this.selected++;
if (this.selected >= this.getChildCount())
this.selected = this.getChildCount()-1;
this.selected = this.getChildCount() - 1;
if (this.selectedChild != null)
this.selectedChild.setHovered(false);
this.selectedChild = getChildAt(this.selected);
this.selectedChild.setHovered(true);
MatchScreen.setPotentialListener(Arrays.asList(this.selectedChild));
}
public void selectCurrent() {
if (this.selectedChild != null) {
this.selectedChild.setHovered(true);
@@ -285,6 +291,7 @@ public class VField extends FContainer {
this.setNextSelected(1);
}
}
public void unselectCurrent() {
if (this.selectedChild != null) {
this.selectedChild.setHovered(false);

View File

@@ -28,6 +28,7 @@ public class VManaPool extends VDisplayArea {
return FSkinColor.get(Colors.ADV_CLR_TEXT);
return FSkinColor.get(Colors.CLR_TEXT);
}
private static final FSkinFont FONT = FSkinFont.get(16);
private final PlayerView player;
@@ -37,7 +38,7 @@ public class VManaPool extends VDisplayArea {
public VManaPool(PlayerView player0) {
player = player0;
addManaLabel(FSkinProp.IMG_MANA_COLORLESS, (byte)ManaAtom.COLORLESS);
addManaLabel(FSkinProp.IMG_MANA_COLORLESS, (byte) ManaAtom.COLORLESS);
addManaLabel(FSkinProp.IMG_MANA_W, MagicColor.WHITE);
addManaLabel(FSkinProp.IMG_MANA_U, MagicColor.BLUE);
addManaLabel(FSkinProp.IMG_MANA_B, MagicColor.BLACK);
@@ -69,7 +70,7 @@ public class VManaPool extends VDisplayArea {
float x = 0;
float y = 0;
if (Forge.isLandscapeMode()) {
if (Forge.isLandscapeMode() && !Forge.altZoneTabs) {
float labelWidth = visibleWidth / 2;
float labelHeight = visibleHeight / 3;
@@ -79,13 +80,11 @@ public class VManaPool extends VDisplayArea {
if (++count % 2 == 0) {
x = 0;
y += labelHeight;
}
else {
} else {
x += labelWidth;
}
}
}
else {
} else {
float labelWidth = visibleWidth / manaLabels.size();
float labelHeight = visibleHeight;
@@ -113,14 +112,16 @@ public class VManaPool extends VDisplayArea {
activate();
return true;
}
public void activate() {
if(!(MatchController.instance.getGameController() instanceof PlayerControllerHuman))
if (!(MatchController.instance.getGameController() instanceof PlayerControllerHuman))
return;
PlayerControllerHuman controller = (PlayerControllerHuman) MatchController.instance.getGameController();
final Input ipm = controller.getInputQueue().getInput();
if(ipm instanceof InputPayMana && ipm.getOwner().equals(player))
if (ipm instanceof InputPayMana && ipm.getOwner().equals(player))
controller.useMana(colorCode);
}
@Override
public boolean flick(float x, float y) {
if (player.isLobbyPlayer(GamePlayerUtil.getGuiPlayer())) {
@@ -146,17 +147,18 @@ public class VManaPool extends VDisplayArea {
if (h > maxImageHeight) {
h /= 2;
}
float w = image.getWidth() * h / image.getHeight();
float modifier = Forge.isHorizontalTabLayout() ? 0.7f : 1f;
float w = image.getWidth() * h * modifier / image.getHeight();
while (w > getWidth()) {
h /= 2;
w = image.getWidth() * h / image.getHeight();
w = image.getWidth() * h * modifier / image.getHeight();
}
float x = (getWidth() - w) / 2;
float y = gapY + (maxImageHeight - h) / 2;
if (isHovered())
g.fillRect(FSkinColor.getStandardColor(50, 200, 150).alphaColor(0.3f), 0, 0, getWidth(), getHeight());
g.drawImage(image, x, y, w, h);
g.drawImage(image, x, y, w, Forge.isHorizontalTabLayout() ? w : h);
x = 0;
y += h + gapY;

View File

@@ -38,21 +38,31 @@ public class VPlayerPanel extends FContainer {
private static final FSkinFont LIFE_FONT_ALT = FSkinFont.get(22);
private static final FSkinFont INFO_FONT = FSkinFont.get(12);
private static final FSkinFont INFO2_FONT = FSkinFont.get(14);
private static FSkinColor getInfoForeColor() {
if (Forge.isMobileAdventureMode)
return FSkinColor.get(Colors.ADV_CLR_TEXT);
return FSkinColor.get(Colors.CLR_TEXT);
}
private static FSkinColor getDisplayAreaBackColor() {
if (Forge.isMobileAdventureMode)
return FSkinColor.get(Colors.ADV_CLR_INACTIVE).alphaColor(0.5f);
return FSkinColor.get(Colors.CLR_INACTIVE).alphaColor(0.5f);
}
private static FSkinColor getAltDisplayAreaBackColor() {
if (Forge.isMobileAdventureMode)
return FSkinColor.get(Colors.ADV_CLR_PHASE_INACTIVE_ENABLED).alphaColor(0.3f);
return FSkinColor.get(Colors.CLR_PHASE_INACTIVE_ENABLED).alphaColor(0.3f);
}
private static FSkinColor getDeliriumHighlight() {
if (Forge.isMobileAdventureMode)
return FSkinColor.get(Colors.ADV_CLR_PHASE_ACTIVE_ENABLED).alphaColor(0.5f);
return FSkinColor.get(Colors.CLR_PHASE_ACTIVE_ENABLED).alphaColor(0.5f);
}
private static final float INFO_TAB_PADDING_X = Utils.scale(2);
private static final float INFO_TAB_PADDING_Y = Utils.scale(2);
@@ -79,11 +89,13 @@ public class VPlayerPanel extends FContainer {
private boolean forMultiPlayer = false;
public int adjustHeight = 1;
private int selected = 0;
private boolean isBottomPlayer = false;
public VPlayerPanel(PlayerView player0, boolean showHand, int playerCount) {
player = player0;
phaseIndicator = add(new VPhaseIndicator());
if(playerCount > 2){
if (playerCount > 2) {
forMultiPlayer = true;
avatarHeight *= 0.5f;
//displayAreaHeightFactor *= 0.7f;
@@ -116,6 +128,10 @@ public class VPlayerPanel extends FContainer {
return player;
}
public void setBottomPlayer(boolean val) {
isBottomPlayer = val;
}
public void addZoneDisplay(ZoneType zoneType) {
VZoneDisplay zoneDisplay = add(new VZoneDisplay(player, zoneType));
InfoTabZone zoneTab = add(new InfoTabZone(zoneDisplay, zoneType));
@@ -124,22 +140,22 @@ public class VPlayerPanel extends FContainer {
}
public static FSkinImage iconFromZone(ZoneType zoneType) {
switch (zoneType) {
case Hand: return Forge.hdbuttons ? FSkinImage.HDHAND : FSkinImage.HAND;
case Library: return Forge.hdbuttons ? FSkinImage.HDLIBRARY : FSkinImage.LIBRARY;
case Graveyard: return Forge.hdbuttons ? FSkinImage.HDGRAVEYARD : FSkinImage.GRAVEYARD;
case Exile: return Forge.hdbuttons ? FSkinImage.HDEXILE : FSkinImage.EXILE;
case Sideboard: return Forge.hdbuttons ? FSkinImage.HDSIDEBOARD :FSkinImage.SIDEBOARD;
case Flashback: return Forge.hdbuttons ? FSkinImage.HDFLASHBACK :FSkinImage.FLASHBACK;
case Command: return FSkinImage.COMMAND;
case PlanarDeck: return FSkinImage.PLANAR;
case SchemeDeck: return FSkinImage.SCHEME;
case AttractionDeck: return FSkinImage.ATTRACTION;
case ContraptionDeck: return FSkinImage.CONTRAPTION;
case Ante: return FSkinImage.ANTE;
case Junkyard: return FSkinImage.JUNKYARD;
default: return FSkinImage.HDLIBRARY;
}
return switch (zoneType) {
case Hand -> Forge.hdbuttons ? FSkinImage.HDHAND : FSkinImage.HAND;
case Library -> Forge.hdbuttons ? FSkinImage.HDLIBRARY : FSkinImage.LIBRARY;
case Graveyard -> Forge.hdbuttons ? FSkinImage.HDGRAVEYARD : FSkinImage.GRAVEYARD;
case Exile -> Forge.hdbuttons ? FSkinImage.HDEXILE : FSkinImage.EXILE;
case Sideboard -> Forge.hdbuttons ? FSkinImage.HDSIDEBOARD : FSkinImage.SIDEBOARD;
case Flashback -> Forge.hdbuttons ? FSkinImage.HDFLASHBACK : FSkinImage.FLASHBACK;
case Command -> FSkinImage.COMMAND;
case PlanarDeck -> FSkinImage.PLANAR;
case SchemeDeck -> FSkinImage.SCHEME;
case AttractionDeck -> FSkinImage.ATTRACTION;
case ContraptionDeck -> FSkinImage.CONTRAPTION;
case Ante -> FSkinImage.ANTE;
case Junkyard -> FSkinImage.JUNKYARD;
default -> FSkinImage.HDLIBRARY;
};
}
public Iterable<InfoTab> getTabs() {
@@ -151,12 +167,12 @@ public class VPlayerPanel extends FContainer {
}
public void resetZoneTabs() {
for(InfoTab tab : tabs)
for (InfoTab tab : tabs)
tab.reset();
}
public void setSelectedZone(ZoneType zoneType) {
if(zoneTabs.containsKey(zoneType))
if (zoneTabs.containsKey(zoneType))
setSelectedTab(zoneTabs.get(zoneType));
else {
extraTab.setActiveZone(zoneType);
@@ -189,6 +205,7 @@ public class VPlayerPanel extends FContainer {
MatchController.getView().revalidate();
}
}
public void setNextSelectedTab(boolean change) {
if (change) {
if (selectedTab != null) {
@@ -207,13 +224,13 @@ public class VPlayerPanel extends FContainer {
int numExtraTabs = extraTab.displayAreas.size();
if (selected < 0 || selected >= numTabs + numExtraTabs)
selected = 0;
if(selected >= numTabs) {
if (selected >= numTabs) {
extraTab.setActiveZoneByIndex(selected - numTabs);
setSelectedTab(extraTab);
}
else
} else
setSelectedTab(tabs.get(selected));
}
public void closeSelectedTab() {
if (selectedTab != null) {
selectedTab.setDisplayVisible(false);
@@ -234,6 +251,7 @@ public class VPlayerPanel extends FContainer {
public boolean isFlipped() {
return field.isFlipped();
}
public void setFlipped(boolean flipped0) {
field.setFlipped(flipped0);
}
@@ -254,9 +272,11 @@ public class VPlayerPanel extends FContainer {
public VField getField() {
return field;
}
public VField.FieldRow getSelectedRow() {
return selectedRow;
}
public void switchRow() {
if (selectedRow == field.getRow1())
selectedRow = field.getRow2();
@@ -289,29 +309,22 @@ public class VPlayerPanel extends FContainer {
}
public void updateZone(ZoneType zoneType) {
if (zoneType == ZoneType.Battlefield ) {
if (zoneType == ZoneType.Battlefield) {
field.update(true);
}
else if (zoneType == ZoneType.Command) {
} else if (zoneType == ZoneType.Command) {
commandZone.update();
}
else {
if(zoneTabs.containsKey(zoneType))
if (selectedTab != null && Forge.isHorizontalTabLayout())
updateTabLayout(initW, initH);
} else {
if (zoneTabs.containsKey(zoneType))
zoneTabs.get(zoneType).update();
else if(EXTRA_ZONES.contains(zoneType)) {
else if (EXTRA_ZONES.contains(zoneType)) {
extraTab.update(zoneType);
}
//update flashback zone when graveyard, library, exile, or stack zones updated
switch (zoneType) {
case Graveyard:
case Library:
case Exile:
case Stack:
zoneTabs.get(ZoneType.Flashback).update();
break;
default:
break;
case Graveyard, Library, Exile, Stack -> zoneTabs.get(ZoneType.Flashback).update();
}
}
}
@@ -327,7 +340,7 @@ public class VPlayerPanel extends FContainer {
float x = avatarHeight;
float w = width - avatarHeight;
float indicatorScale = 1f;
if(avatarHeight<VAvatar.HEIGHT){
if (avatarHeight < VAvatar.HEIGHT) {
indicatorScale = 0.6f;
}
float h = phaseIndicator.getPreferredHeight(w) * indicatorScale;
@@ -366,8 +379,7 @@ public class VPlayerPanel extends FContainer {
commandZone.setBounds(width - commandZoneWidth, y - commandZoneHeight, commandZoneWidth, commandZoneHeight);
field.setCommandZoneWidth(commandZoneWidth + 1); //ensure second row of field accounts for width of command zone and its border
}
else {
} else {
field.setCommandZoneWidth(0);
}
@@ -383,18 +395,23 @@ public class VPlayerPanel extends FContainer {
field.setFieldModifier(0);
}
private float initW, initH, commandZoneWidth, commandZoneCount, avatarWidth, prefWidth;
private final float mod = 2.4f;
private void doLandscapeLayout(float width, float height) {
initW = width;
initH = height;
float x = 0;
float y = 0;
float yAlt = 0;
float avatarWidth = Forge.altZoneTabs ? avatar.getWidth() : 0;
avatarWidth = Forge.altZoneTabs ? avatar.getWidth() : 0;
avatar.setPosition(x, y);
y += avatar.getHeight();
lblLife.setBounds(x, (Forge.altPlayerLayout && !Forge.altZoneTabs) ? 0 : y, avatar.getWidth(), (Forge.altPlayerLayout && !Forge.altZoneTabs) ? INFO_FONT.getLineHeight() : Forge.altZoneTabs ? LIFE_FONT_ALT.getLineHeight() : LIFE_FONT.getLineHeight());
if (Forge.altPlayerLayout && !Forge.altZoneTabs) {
if (adjustHeight > 2)
y += INFO_FONT.getLineHeight()/2;
y += INFO_FONT.getLineHeight() / 2;
} else
y += lblLife.getHeight();
@@ -412,12 +429,16 @@ public class VPlayerPanel extends FContainer {
tab.setBounds(x, y, infoTabWidth, infoTabHeight);
y += infoTabHeight;
} else {
tab.setBounds(x+width-avatarWidth, yAlt, avatarWidth, infoTabHeightAlt);
tab.setBounds(x + width - avatarWidth, yAlt, avatarWidth, infoTabHeightAlt);
yAlt += infoTabHeightAlt;
}
}
}
x = avatar.getRight();
updateTabLayout(width, height);
}
private void updateTabLayout(float width, float height) {
float x = avatar.getRight();
phaseIndicator.resetFont();
phaseIndicator.setBounds(x, 0, avatar.getWidth() * 0.6f, height);
x += phaseIndicator.getWidth();
@@ -429,38 +450,77 @@ public class VPlayerPanel extends FContainer {
}
//account for command zone if needed
int commandZoneCount = commandZone.getCount();
commandZoneWidth = 0f;
commandZoneCount = commandZone.getCount();
if (commandZoneCount > 0) {
float commandZoneHeight = height / 2;
float commandZoneWidth = Math.min(commandZoneCount, 2) * commandZone.getCardWidth(commandZoneHeight);
commandZone.setBounds(x + fieldWidth - commandZoneWidth, height - commandZoneHeight, commandZoneWidth, commandZoneHeight);
float minCommandCards = Forge.isHorizontalTabLayout() ? 5 : 2;
commandZoneWidth = Math.min(commandZoneCount, minCommandCards) * commandZone.getCardWidth(commandZoneHeight);
float x2 = x + fieldWidth - commandZoneWidth;
float y2 = height - commandZoneHeight;
if (Forge.isHorizontalTabLayout()) {
x2 = width - avatarWidth - commandZoneWidth;
y2 = 0;
}
commandZone.setBounds(x2, y2, commandZoneWidth, commandZoneHeight);
if (isFlipped()) { //flip across x-axis if needed
commandZone.setTop(height - commandZone.getBottom());
}
field.setCommandZoneWidth(commandZoneWidth + 1); //ensure second row of field accounts for width of command zone and its border
}
else {
} else {
field.setCommandZoneWidth(0);
}
prefWidth = width / mod;
if (Forge.isHorizontalTabLayout()) {
field.setBounds(x, 0, width - avatarWidth, height);
field.getRow1().setWidth(width - (commandZoneCount > 0 ? commandZone.getWidth() + (avatarWidth * commandZoneCount) : avatarWidth));
field.getRow2().setWidth(width - (avatarWidth / 4f) - (selectedTab == null ? 0 : selectedTab.getIdealWidth(prefWidth) + 1) - avatarWidth * mod);
} else
field.setBounds(x, 0, fieldWidth, height);
field.setBounds(x, 0, fieldWidth, height);
x = width - displayAreaWidth-avatarWidth;
x = width - displayAreaWidth - avatarWidth;
for (InfoTab tab : tabs) {
tab.setDisplayBounds(x, 0, displayAreaWidth, height);
if (Forge.isHorizontalTabLayout()) {
float w = tab.getIdealWidth(prefWidth);
float h = height / 2f;
tab.setDisplayBounds(width - w - avatarWidth, isBottomPlayer ? h : 0, w, h);
} else {
tab.setDisplayBounds(x, 0, displayAreaWidth, height);
}
}
if (!Forge.altZoneTabs)
if (!Forge.altZoneTabs) {
field.setFieldModifier(0);
else
field.setFieldModifier(avatarWidth/16);
} else {
if (!"Horizontal".equalsIgnoreCase(Forge.altZoneTabMode))
field.setFieldModifier(avatarWidth / 16);
}
}
@Override
protected void drawOverlay(Graphics g) {
if (Forge.isHorizontalTabLayout()) {
InfoTab infoTab = selectedTab;
if (infoTab != null) {
VDisplayArea selectedDisplayArea = infoTab.getDisplayArea();
if (selectedDisplayArea != null && selectedDisplayArea.getCount() > 0) {
float scale = avatarWidth / 2f;
float x = selectedDisplayArea.getLeft();
float y = selectedDisplayArea.getBottom() - scale;
g.fillRect(getAltDisplayAreaBackColor(), x, y, scale, scale);
infoTab.icon.draw(g, x, y, scale, scale);
}
}
}
super.drawOverlay(g);
}
@Override
public void drawBackground(Graphics g) {
float y;
InfoTab infoTab = selectedTab;
float pad = Forge.isHorizontalTabLayout() ? avatarWidth / 16f : 0f;
if (infoTab != null) { //draw background and border for selected zone if needed
VDisplayArea selectedDisplayArea = infoTab.getDisplayArea();
float x = selectedDisplayArea == null ? 0 : selectedDisplayArea.getLeft();
@@ -468,12 +528,13 @@ public class VPlayerPanel extends FContainer {
float top = selectedDisplayArea == null ? 0 : selectedDisplayArea.getTop();
float h = selectedDisplayArea == null ? 0 : selectedDisplayArea.getHeight();
float bottom = selectedDisplayArea == null ? 0 : selectedDisplayArea.getBottom();
g.fillRect(getDisplayAreaBackColor(), x, top, w, h);
g.fillRect(Forge.isHorizontalTabLayout() ? getAltDisplayAreaBackColor() : getDisplayAreaBackColor(), x - pad, top, w + pad, h + pad);
if (Forge.isHorizontalTabLayout())
g.drawLine(1, MatchScreen.getBorderColor(), x, isFlipped() ? bottom : top, x + w, isFlipped() ? bottom : top);
if (Forge.isLandscapeMode()) {
g.drawLine(1, MatchScreen.getBorderColor(), x, top, x, bottom);
}
else {
} else {
y = isFlipped() ? top + 1 : bottom;
//don't know why infotab gets null here, either way don't crash the gui..
float left = infoTab == null ? 0 : infoTab.getLeft();
@@ -487,6 +548,8 @@ public class VPlayerPanel extends FContainer {
float x = commandZone.getLeft();
y = commandZone.getTop();
g.drawLine(1, MatchScreen.getBorderColor(), x, y, x, y + commandZone.getHeight());
/*if (Forge.isHorizontalTabLayout())
g.fillRect(getAltDisplayAreaBackColor(), x - pad, y, commandZoneWidth + pad, commandZone.getHeight() + pad);*/
if (isFlipped()) {
y += commandZone.getHeight();
}
@@ -499,7 +562,7 @@ public class VPlayerPanel extends FContainer {
ArrayList<FScrollPane> out = new ArrayList<>();
out.add(field.getRow1());
out.add(field.getRow2());
for(InfoTabZone tab : zoneTabs.values())
for (InfoTabZone tab : zoneTabs.values())
out.add(tab.displayArea);
out.add(commandZone);
out.addAll(extraTab.displayAreas.values());
@@ -522,7 +585,7 @@ public class VPlayerPanel extends FContainer {
private void update() {
int vibrateDuration = 0;
int delta = player.getLife() - life;
player.setAvatarLifeDifference(player.getAvatarLifeDifference()+delta);
player.setAvatarLifeDifference(player.getAvatarLifeDifference() + delta);
if (delta != 0) {
if (delta < 0) {
vibrateDuration += delta * -100;
@@ -553,6 +616,7 @@ public class VPlayerPanel extends FContainer {
Gdx.input.vibrate(Math.min(vibrateDuration, 2000));
}
}
private void updateShards() {
manaShards = player.getNumManaShards();
}
@@ -567,55 +631,55 @@ public class VPlayerPanel extends FContainer {
public void draw(Graphics g) {
adjustHeight = 1;
float divider = Gdx.app.getGraphics().getHeight() > 900 ? 1.2f : 2f;
if(Forge.altPlayerLayout && !Forge.altZoneTabs && Forge.isLandscapeMode()) {
if (Forge.altPlayerLayout && !Forge.altZoneTabs && Forge.isLandscapeMode()) {
if (poisonCounters == 0 && energyCounters == 0 && experienceCounters == 0 && ticketCounters == 0 && radCounters == 0 && manaShards == 0) {
g.fillRect(Color.DARK_GRAY, 0, 0, INFO2_FONT.getBounds(lifeStr).width+1, INFO2_FONT.getBounds(lifeStr).height+1);
g.fillRect(Color.DARK_GRAY, 0, 0, INFO2_FONT.getBounds(lifeStr).width + 1, INFO2_FONT.getBounds(lifeStr).height + 1);
g.drawText(lifeStr, INFO2_FONT, getInfoForeColor().getColor(), 0, 0, getWidth(), getHeight(), false, Align.left, false);
} else {
float halfHeight = getHeight() / divider;
float textStart = halfHeight + Utils.scale(1);
float textWidth = getWidth() - textStart;
int mod = 1;
g.fillRect(Color.DARK_GRAY, 0, 0, INFO_FONT.getBounds(lifeStr).width+halfHeight+1, INFO_FONT.getBounds(lifeStr).height+1);
g.fillRect(Color.DARK_GRAY, 0, 0, INFO_FONT.getBounds(lifeStr).width + halfHeight + 1, INFO_FONT.getBounds(lifeStr).height + 1);
g.drawImage(FSkinImage.QUEST_LIFE, 0, 0, halfHeight, halfHeight);
g.drawText(lifeStr, INFO_FONT, getInfoForeColor().getColor(), textStart, 0, textWidth, halfHeight, false, Align.left, false);
if (poisonCounters > 0) {
g.fillRect(Color.DARK_GRAY, 0, halfHeight+2, INFO_FONT.getBounds(String.valueOf(poisonCounters)).width+halfHeight+1, INFO_FONT.getBounds(String.valueOf(poisonCounters)).height+1);
g.drawImage(FSkinImage.POISON, 0, halfHeight+2, halfHeight, halfHeight);
g.drawText(String.valueOf(poisonCounters), INFO_FONT, getInfoForeColor().getColor(), textStart, halfHeight+2, textWidth, halfHeight, false, Align.left, false);
mod+=1;
g.fillRect(Color.DARK_GRAY, 0, halfHeight + 2, INFO_FONT.getBounds(String.valueOf(poisonCounters)).width + halfHeight + 1, INFO_FONT.getBounds(String.valueOf(poisonCounters)).height + 1);
g.drawImage(FSkinImage.POISON, 0, halfHeight + 2, halfHeight, halfHeight);
g.drawText(String.valueOf(poisonCounters), INFO_FONT, getInfoForeColor().getColor(), textStart, halfHeight + 2, textWidth, halfHeight, false, Align.left, false);
mod += 1;
}
if (energyCounters > 0) {
g.fillRect(Color.DARK_GRAY, 0, (halfHeight*mod)+2, INFO_FONT.getBounds(String.valueOf(energyCounters)).width+halfHeight+1, INFO_FONT.getBounds(String.valueOf(energyCounters)).height+1);
g.drawImage(FSkinImage.ENERGY, 0, (halfHeight*mod)+2, halfHeight, halfHeight);
g.drawText(String.valueOf(energyCounters), INFO_FONT, getInfoForeColor().getColor(), textStart, (halfHeight*mod)+2, textWidth, halfHeight, false, Align.left, false);
mod+=1;
g.fillRect(Color.DARK_GRAY, 0, (halfHeight * mod) + 2, INFO_FONT.getBounds(String.valueOf(energyCounters)).width + halfHeight + 1, INFO_FONT.getBounds(String.valueOf(energyCounters)).height + 1);
g.drawImage(FSkinImage.ENERGY, 0, (halfHeight * mod) + 2, halfHeight, halfHeight);
g.drawText(String.valueOf(energyCounters), INFO_FONT, getInfoForeColor().getColor(), textStart, (halfHeight * mod) + 2, textWidth, halfHeight, false, Align.left, false);
mod += 1;
}
if (experienceCounters > 0) {
g.fillRect(Color.DARK_GRAY, 0, (halfHeight*mod)+2, INFO_FONT.getBounds(String.valueOf(experienceCounters)).width+halfHeight+1, INFO_FONT.getBounds(String.valueOf(experienceCounters)).height+1);
g.drawImage(FSkinImage.COMMANDER, 0, (halfHeight*mod)+2, halfHeight, halfHeight);
g.drawText(String.valueOf(experienceCounters), INFO_FONT, getInfoForeColor().getColor(), textStart, (halfHeight*mod)+2, textWidth, halfHeight, false, Align.left, false);
mod+=1;
g.fillRect(Color.DARK_GRAY, 0, (halfHeight * mod) + 2, INFO_FONT.getBounds(String.valueOf(experienceCounters)).width + halfHeight + 1, INFO_FONT.getBounds(String.valueOf(experienceCounters)).height + 1);
g.drawImage(FSkinImage.COMMANDER, 0, (halfHeight * mod) + 2, halfHeight, halfHeight);
g.drawText(String.valueOf(experienceCounters), INFO_FONT, getInfoForeColor().getColor(), textStart, (halfHeight * mod) + 2, textWidth, halfHeight, false, Align.left, false);
mod += 1;
}
if (radCounters > 0) {
g.fillRect(Color.DARK_GRAY, 0, (halfHeight*mod)+2, INFO_FONT.getBounds(String.valueOf(radCounters)).width+halfHeight+1, INFO_FONT.getBounds(String.valueOf(radCounters)).height+1);
g.drawImage(FSkinImage.RAD, 0, (halfHeight*mod)+2, halfHeight, halfHeight);
g.drawText(String.valueOf(radCounters), INFO_FONT, getInfoForeColor().getColor(), textStart, (halfHeight*mod)+2, textWidth, halfHeight, false, Align.left, false);
mod+=1;
g.fillRect(Color.DARK_GRAY, 0, (halfHeight * mod) + 2, INFO_FONT.getBounds(String.valueOf(radCounters)).width + halfHeight + 1, INFO_FONT.getBounds(String.valueOf(radCounters)).height + 1);
g.drawImage(FSkinImage.RAD, 0, (halfHeight * mod) + 2, halfHeight, halfHeight);
g.drawText(String.valueOf(radCounters), INFO_FONT, getInfoForeColor().getColor(), textStart, (halfHeight * mod) + 2, textWidth, halfHeight, false, Align.left, false);
mod += 1;
}
if (ticketCounters > 0) {
g.fillRect(Color.DARK_GRAY, 0, (halfHeight*mod)+2, INFO_FONT.getBounds(String.valueOf(ticketCounters)).width+halfHeight+1, INFO_FONT.getBounds(String.valueOf(ticketCounters)).height+1);
g.drawImage(FSkinImage.TICKET, 0, (halfHeight*mod)+2, halfHeight, halfHeight);
g.drawText(String.valueOf(ticketCounters), INFO_FONT, getInfoForeColor().getColor(), textStart, (halfHeight*mod)+2, textWidth, halfHeight, false, Align.left, false);
mod+=1;
g.fillRect(Color.DARK_GRAY, 0, (halfHeight * mod) + 2, INFO_FONT.getBounds(String.valueOf(ticketCounters)).width + halfHeight + 1, INFO_FONT.getBounds(String.valueOf(ticketCounters)).height + 1);
g.drawImage(FSkinImage.TICKET, 0, (halfHeight * mod) + 2, halfHeight, halfHeight);
g.drawText(String.valueOf(ticketCounters), INFO_FONT, getInfoForeColor().getColor(), textStart, (halfHeight * mod) + 2, textWidth, halfHeight, false, Align.left, false);
mod += 1;
}
if (manaShards > 0) {
g.fillRect(Color.DARK_GRAY, 0, (halfHeight*mod)+2, INFO_FONT.getBounds(String.valueOf(manaShards)).width+halfHeight+1, INFO_FONT.getBounds(String.valueOf(manaShards)).height+1);
g.drawImage(FSkinImage.AETHER_SHARD, 0, (halfHeight*mod)+2, halfHeight, halfHeight);
g.drawText(String.valueOf(manaShards), INFO_FONT, getInfoForeColor().getColor(), textStart, (halfHeight*mod)+2, textWidth, halfHeight, false, Align.left, false);
mod+=1;
g.fillRect(Color.DARK_GRAY, 0, (halfHeight * mod) + 2, INFO_FONT.getBounds(String.valueOf(manaShards)).width + halfHeight + 1, INFO_FONT.getBounds(String.valueOf(manaShards)).height + 1);
g.drawImage(FSkinImage.AETHER_SHARD, 0, (halfHeight * mod) + 2, halfHeight, halfHeight);
g.drawText(String.valueOf(manaShards), INFO_FONT, getInfoForeColor().getColor(), textStart, (halfHeight * mod) + 2, textWidth, halfHeight, false, Align.left, false);
mod += 1;
}
adjustHeight = (mod > 2) && (avatar.getHeight() < halfHeight*mod)? mod : 1;
adjustHeight = (mod > 2) && (avatar.getHeight() < halfHeight * mod) ? mod : 1;
}
} else {
if (poisonCounters == 0 && energyCounters == 0 && manaShards == 0) {
@@ -632,8 +696,7 @@ public class VPlayerPanel extends FContainer {
} else if (energyCounters > 0) { //prioritize showing energy counters over mana shards
g.drawImage(FSkinImage.ENERGY, 0, halfHeight, halfHeight, halfHeight);
g.drawText(String.valueOf(energyCounters), INFO_FONT, getInfoForeColor(), textStart, halfHeight, textWidth, halfHeight, false, Align.center, true);
}
else {
} else {
g.drawImage(FSkinImage.MANASHARD, 0, halfHeight, halfHeight, halfHeight);
g.drawText(String.valueOf(manaShards), INFO_FONT, getInfoForeColor(), textStart, halfHeight, textWidth, halfHeight, false, Align.center, true);
}
@@ -658,18 +721,27 @@ public class VPlayerPanel extends FContainer {
}
public abstract VDisplayArea getDisplayArea();
public abstract void setDisplayVisible(boolean visible);
public abstract void setDisplayBounds(float x, float y, float width, float height);
public abstract void setRotate180(boolean rotate180);
public abstract void update();
public abstract void reset();
public abstract float getIdealWidth(float pref);
protected boolean isSelected() {
return selectedTab == this;
}
protected FSkinColor getSelectedBackgroundColor() {
return getDisplayAreaBackColor();
}
protected boolean isAlignedRightForAltDisplay() {
return false;
}
@@ -745,8 +817,8 @@ public class VPlayerPanel extends FContainer {
if (lblLife.getRotate180()) {
g.startRotateTransform(x + w / 2, y + h / 2, 180);
}
float mod = isHovered() ? w/8f:0;
g.drawImage(icon, x-mod/2, y-mod/2, w+mod, h+mod);
float mod = isHovered() ? w / 8f : 0;
g.drawImage(icon, x - mod / 2, y - mod / 2, w + mod, h + mod);
if (lblLife.getRotate180()) {
g.endTransform();
}
@@ -773,8 +845,8 @@ public class VPlayerPanel extends FContainer {
h = icon.getHeight() * w / icon.getWidth();
x = (getWidth() - w) / 2;
y = INFO_TAB_PADDING_Y;
float mod = isHovered() ? w/8f:0;
g.drawImage(icon, x-mod/2, y-mod/2, w+mod, h+mod);
float mod = isHovered() ? w / 8f : 0;
g.drawImage(icon, x - mod / 2, y - mod / 2, w + mod, h + mod);
y += h + INFO_TAB_PADDING_Y;
g.drawText(value, INFO_FONT, getInfoForeColor(), 0, y, getWidth(), getHeight() - y + 1, false, Align.center, false);
@@ -827,7 +899,13 @@ public class VPlayerPanel extends FContainer {
}
@Override
public void reset() {} //Mana Display does not get cleared.
public void reset() {
} //Mana Display does not get cleared.
@Override
public float getIdealWidth(float pref) {
return pref;
}
}
/**
@@ -842,6 +920,7 @@ public class VPlayerPanel extends FContainer {
}
private final EnumSet<ZoneType> altDisplayZones = EnumSet.of(ZoneType.Hand, ZoneType.Library, ZoneType.Graveyard, ZoneType.Exile);
public boolean isAlignedRightForAltDisplay() {
return altDisplayZones.contains(this.zoneType);
}
@@ -857,6 +936,23 @@ public class VPlayerPanel extends FContainer {
public void reset() {
displayArea.clear();
}
@Override
public float getIdealWidth(float pref) {
if (displayArea instanceof VCardDisplayArea vCardDisplayArea) {
float cardWidth = vCardDisplayArea.getCardWidth(vCardDisplayArea.getHeight());
float size = vCardDisplayArea.getCount();
return Math.min(cardWidth * size, pref);
}
return pref;
}
@Override
public void update() {
super.update();
if (selectedTab != null && Forge.isHorizontalTabLayout())
updateTabLayout(initW, initH);
}
}
/**
@@ -873,9 +969,9 @@ public class VPlayerPanel extends FContainer {
private InfoTabExtra() {
super(DEFAULT_ICON);
this.displayAreas = new EnumMap<>(ZoneType.class);
for(ZoneType zoneType : EXTRA_ZONES) {
for (ZoneType zoneType : EXTRA_ZONES) {
FCollectionView<CardView> cards = player.getCards(zoneType);
if(cards == null || cards.isEmpty())
if (cards == null || cards.isEmpty())
continue;
createZoneIfMissing(zoneType);
hasCardsInExtraZone = true;
@@ -887,38 +983,39 @@ public class VPlayerPanel extends FContainer {
}
public void createZoneIfMissing(ZoneType zone) {
if(this.displayAreas.containsKey(zone))
if (this.displayAreas.containsKey(zone))
return;
VZoneDisplay display = VPlayerPanel.this.add(new VZoneDisplay(player, zone));
this.displayAreas.put(zone, display);
this.hasCardsInExtraZone = true;
if(zone == ZoneType.AttractionDeck || zone == ZoneType.ContraptionDeck)
if (zone == ZoneType.AttractionDeck || zone == ZoneType.ContraptionDeck)
createZoneIfMissing(ZoneType.Junkyard); //If the game uses one, it uses both.
}
public void setActiveZone(ZoneType zone) {
if(this.activeZone == zone)
if (this.activeZone == zone)
return;
createZoneIfMissing(zone);
getDisplayArea().setVisible(false);
this.activeZone = zone;
if(isSelected())
if (isSelected())
getDisplayArea().setVisible(true);
updateTab();
}
public void setActiveZoneByIndex(int index) {
List<ZoneType> keyList = List.copyOf(displayAreas.keySet());
setActiveZone(keyList.get(index % keyList.size()));
}
private void updateTab() {
if(!hasCardsInExtraZone)
if (!hasCardsInExtraZone)
this.value = "";
else if(!getDisplayArea().isVisible())
else if (!getDisplayArea().isVisible())
this.value = "+";
else
this.value = String.valueOf(displayAreas.get(this.activeZone).getCount());
if(getDisplayArea().isVisible())
if (getDisplayArea().isVisible())
this.icon = iconFromZone(this.activeZone);
else
this.icon = DEFAULT_ICON;
@@ -932,7 +1029,7 @@ public class VPlayerPanel extends FContainer {
@Override
public void setDisplayVisible(boolean visible) {
if(!visible)
if (!visible)
displayAreas.values().forEach(d -> d.setVisible(false));
else
getDisplayArea().setVisible(true);
@@ -954,12 +1051,13 @@ public class VPlayerPanel extends FContainer {
displayAreas.values().forEach(VDisplayArea::update);
updateTab();
}
public void update(ZoneType zoneType) {
if(!displayAreas.containsKey(zoneType)) {
if(!EXTRA_ZONES.contains(zoneType))
if (!displayAreas.containsKey(zoneType)) {
if (!EXTRA_ZONES.contains(zoneType))
return;
FCollectionView<CardView> cards = player.getCards(zoneType);
if(cards == null || cards.isEmpty())
if (cards == null || cards.isEmpty())
return;
createZoneIfMissing(zoneType);
}
@@ -987,18 +1085,28 @@ public class VPlayerPanel extends FContainer {
activeZone = ZoneType.Sideboard;
}
@Override
public float getIdealWidth(float pref) {
if (getDisplayArea() instanceof VCardDisplayArea vCardDisplayArea) {
float cardWidth = vCardDisplayArea.getCardWidth(vCardDisplayArea.getHeight());
float size = vCardDisplayArea.getCount();
return Math.min(cardWidth * size, pref);
}
return pref;
}
@Override
public boolean tap(float x, float y, int count) {
if(this.displayAreas.isEmpty())
if (this.displayAreas.isEmpty())
return false;
if(count >= 2) {
if (count >= 2) {
onClickZone(this.activeZone);
return true;
}
FPopupMenu menu = new FPopupMenu() {
@Override
protected void buildMenu() {
for(ZoneType zone : displayAreas.keySet()) {
for (ZoneType zone : displayAreas.keySet()) {
String label = WordUtils.capitalize(zone.getTranslatedName());
addItem(new FMenuItem(label, iconFromZone(zone), (e) -> onClickZone(zone)));
}
@@ -1009,7 +1117,7 @@ public class VPlayerPanel extends FContainer {
}
public void onClickZone(ZoneType zone) {
if(activeZone == zone && this.isSelected()) {
if (activeZone == zone && this.isSelected()) {
setSelectedTab(null);
return;
}
@@ -1042,7 +1150,7 @@ public class VPlayerPanel extends FContainer {
@Override
public boolean keyDown(int keyCode) {
if (MatchController.getView().selectedPlayerPanel() == this && !((FMenuBar)MatchController.getView().getHeader()).isShowingMenu(true)) {
if (MatchController.getView().selectedPlayerPanel() == this && !((FMenuBar) MatchController.getView().getHeader()).isShowingMenu(true)) {
if (keyCode == Input.Keys.BUTTON_B) {
MatchScreen.nullPotentialListener();
closeSelectedTab();

View File

@@ -113,7 +113,7 @@ public class VZoneDisplay extends VCardDisplayArea {
}
protected boolean layoutVerticallyForLandscapeMode() {
return true;
return !Forge.altZoneTabs || !"Horizontal".equalsIgnoreCase(Forge.altZoneTabMode);
}
@Override

View File

@@ -100,17 +100,17 @@ public class ConquestAEtherScreen extends FScreen {
}
private void updateFilteredPool() {
Predicate<PaperCard> predicate = btnColorFilter.buildFilterPredicate(null);
predicate = btnTypeFilter.buildFilterPredicate(predicate);
predicate = btnRarityFilter.buildFilterPredicate(predicate);
predicate = btnCMCFilter.buildFilterPredicate(predicate);
Predicate<PaperCard> predicate = btnColorFilter
.and(btnTypeFilter)
.and(btnRarityFilter)
.and(btnCMCFilter);
final CardRarity selectedRarity = btnRarityFilter.selectedOption.getRarity();
filteredPool.clear();
strictPool.clear();
for (PaperCard card : pool) {
if (predicate == null || predicate.test(card)) {
if (predicate.test(card)) {
filteredPool.add(card);
if (selectedRarity == card.getRarity()) {
strictPool.add(card);
@@ -344,7 +344,7 @@ public class ConquestAEtherScreen extends FScreen {
}
}
private class FilterButton extends FLabel {
private class FilterButton extends FLabel implements Predicate<PaperCard> {
private final String caption;
private final List<AEtherFilter> options;
private AEtherFilter selectedOption;
@@ -383,11 +383,9 @@ public class ConquestAEtherScreen extends FScreen {
}
}
private Predicate<PaperCard> buildFilterPredicate(Predicate<PaperCard> predicate) {
if (predicate == null) {
return selectedOption.getPredicate();
}
return predicate.and(selectedOption.getPredicate());
@Override
public boolean test(PaperCard card) {
return selectedOption.test(card);
}
@Override

View File

@@ -274,14 +274,15 @@ public class SettingsPage extends TabPage<SettingsScreen> {
MatchController.instance.resetPlayerPanels();
}
}, 1);
lstSettings.addItem(new BooleanSetting(FPref.UI_ALT_PLAYERZONETABS,
lstSettings.addItem(new CustomSelectSetting(FPref.UI_ALT_PLAYERZONETABS,
Forge.getLocalizer().getMessage("lblAltZoneTabs"),
Forge.getLocalizer().getMessage("nlAltZoneTabs")) {
Forge.getLocalizer().getMessage("nlAltZoneTabs"),
Lists.newArrayList("Off", "Vertical", "Horizontal")) {
@Override
public void select() {
super.select();
public void valueChanged(String newValue) {
super.valueChanged(newValue);
//update
Forge.altZoneTabs = FModel.getPreferences().getPrefBoolean(FPref.UI_ALT_PLAYERZONETABS);
Forge.setAltZoneTabMode(FModel.getPreferences().getPref(FPref.UI_ALT_PLAYERZONETABS));
if (MatchController.instance != null)
MatchController.instance.resetPlayerPanels();
}

View File

@@ -4,7 +4,7 @@
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>${revision}</version>
<version>2.0.06</version>
</parent>
<artifactId>forge-gui</artifactId>

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@@ -0,0 +1,11 @@
Name:Emrakul's Awakening
ManaCost:no cost
Types:Artifact
T:Mode$ Phase | Phase$ End of Turn | ValidPlayer$ You | TriggerZones$ Command | CheckSVar$ X | SVarCompare$ LT7 | Execute$ TrigFlip | TriggerDescription$ At the beginning of your end step, if you have fewer than seven cards in hand, flip a coin. If you win the flip, draw cards equal to the difference.
SVar:TrigFlip:DB$ FlipACoin | Defined$ You | WinSubAbility$ TrigDraw
SVar:TrigDraw:DB$ Draw | Defined$ You | NumCards$ Difference
SVar:X:Count$ValidHand Card.YouOwn
SVar:Difference:Number$7/Minus.X
T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | TriggerZones$ Command | Execute$ TrigWish | TriggerDescription$ At the beginning of your upkeep, put a land card from your library onto the battlefield at random.
SVar:TrigWish:DB$ ChangeZone | Origin$ Library | Destination$ Battlefield | ChangeType$ Land.YouCtrl | Hidden$ True | AtRandom$ True
Oracle:At the beginning of your upkeep, put a land card from your library onto the battlefield at random.\nAt the beginning of your end step, if you have fewer than seven cards in hand, flip a coin. If you win the flip, draw cards equal to the difference.

View File

@@ -3,10 +3,6 @@
<editorsettings>
<export target="wastetown..tmx" format="tmx"/>
</editorsettings>
<properties>
<property name="dungeonEffect">{&quot;startBattleWithCardInCommandZone&quot;: [ &quot;Emrakul's Presence&quot; ]
}</property>
</properties>
<tileset firstgid="1" source="../../tileset/main.tsx"/>
<tileset firstgid="10113" source="../../tileset/buildings.tsx"/>
<layer id="1" name="Background" width="30" height="30">
@@ -42,7 +38,12 @@
<object id="49" template="../../obj/gold.tx" x="106" y="216"/>
<object id="50" template="../../obj/enemy.tx" x="182.772" y="284.992" width="64" height="64">
<properties>
<property name="effect" value="{ &quot;startBattleWithCardInCommandZone&quot;: [ &quot;Emrakul's Presence&quot;]}"/>
<property name="enemy" value="Emrakul"/>
<property name="spawn.Easy" type="bool" value="false"/>
<property name="spawn.Hard" type="bool" value="true"/>
<property name="spawn.Insane" type="bool" value="true"/>
<property name="spawn.Normal" type="bool" value="false"/>
</properties>
</object>
<object id="51" template="../../obj/entry_up.tx" x="209" y="480">
@@ -50,6 +51,16 @@
<property name="teleport" value=""/>
</properties>
</object>
<object id="52" template="../../obj/enemy.tx" x="182.772" y="284.992" width="64" height="64">
<properties>
<property name="effect" value="{ &quot;startBattleWithCardInCommandZone&quot;: [ &quot;Emrakul's Awakening&quot;]}"/>
<property name="enemy" value="Emrakul"/>
<property name="spawn.Easy" type="bool" value="true"/>
<property name="spawn.Hard" type="bool" value="false"/>
<property name="spawn.Insane" type="bool" value="false"/>
<property name="spawn.Normal" type="bool" value="true"/>
</properties>
</object>
<object id="69" template="../../obj/enemy.tx" x="132.844" y="401.619">
<properties>
<property name="dialog">[{

View File

@@ -1,6 +1,6 @@
Name:Airbending Lesson
ManaCost:2 W
Types:Instant Lesson
A:SP$ Airbend | ValidTgts$ Permanent.nonLand | TgtPrompt$ Select target nonland permanent | SubAbility$ DBDraw | SpellDescription$ Airbend target nonland permanent. (Exile it. While its exiled, its owner may cast it for {2} rather than its mana cost.)
A:SP$ Airbend | ValidTgts$ Permanent.nonLand | TgtPrompt$ Select target nonland permanent | SubAbility$ DBDraw | SpellDescription$ Airbend target nonland permanent. (Exile it. While it's exiled, its owner may cast it for {2} rather than its mana cost.)
SVar:DBDraw:DB$ Draw | Defined$ You | NumCards$ 1 | SpellDescription$ Draw a card.
Oracle:Airbend target nonland permanent. (Exile it. While its exiled, its owner may cast it for {2} rather than its mana cost.)\nDraw a card.
Oracle:Airbend target nonland permanent. (Exile it. While it's exiled, its owner may cast it for {2} rather than its mana cost.)\nDraw a card.

View File

@@ -3,7 +3,7 @@ ManaCost:3 W
Types:Legendary Creature Bison Ally
PT:2/4
K:Flying
T:Mode$ Attacks | ValidCard$ Card.Self | TriggerZones$ Battlefield | Execute$ TrigPump | TriggerDescription$ Whenever NICKNAME attacks, another target attacking creature without flying gains flying until until end of turn.
T:Mode$ Attacks | ValidCard$ Card.Self | TriggerZones$ Battlefield | Execute$ TrigPump | TriggerDescription$ Whenever NICKNAME attacks, another target attacking creature without flying gains flying until end of turn.
SVar:TrigPump:DB$ Pump | ValidTgts$ Creature.Other+attacking+withoutFlying | TgtPrompt$ Select another target attacking creature without flying | KW$ Flying
SVar:HasAttackEffect:TRUE
Oracle:Flying (This creature can't be blocked except by creatures with flying or reach.)\nWhenever Appa attacks, another target attacking creature without flying gains flying until until end of turn.
Oracle:Flying (This creature can't be blocked except by creatures with flying or reach.)\nWhenever Appa attacks, another target attacking creature without flying gains flying until end of turn.

View File

@@ -3,7 +3,7 @@ ManaCost:3 W
Types:Creature Human Warrior
PT:3/4
T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigChange | TriggerDescription$ When this creature enters, exile up to two target cards from a single graveyard.
SVar:TrigChange:DB$ ChangeZone | Origin$ Graveyard | Destination$ Exile | TargetMin$ 0 | TargetMax$ 2 | TargetsFromSingleZone$ True | ValidTgts$ Card | TgtPrompt$ Select up to two target cards from a single graveyard
SVar:TrigChange:DB$ ChangeZone | Origin$ Graveyard | Destination$ Exile | TargetMin$ 0 | TargetMax$ 2 | TargetsWithSameController$ True | ValidTgts$ Card | TgtPrompt$ Select up to two target cards from a single graveyard
A:AB$ Tap | Cost$ W T | ValidTgts$ Creature | TgtPrompt$ Select target creature | SpellDescription$ Tap target creature.
DeckHas:Ability$Graveyard
Oracle:When this creature enters, exile up to two target cards from a single graveyard.\n{W}, {T}: Tap target creature.

View File

@@ -2,5 +2,5 @@ Name:Carrion Beetles
ManaCost:B
Types:Creature Insect
PT:1/1
A:AB$ ChangeZone | Cost$ 2 B T | TargetMin$ 0 | TargetMax$ 3 | TargetsFromSingleZone$ True | Origin$ Graveyard | Destination$ Exile | TgtPrompt$ Choose target card in a graveyard | ValidTgts$ Card | SpellDescription$ Exile up to three target cards from a single graveyard.
A:AB$ ChangeZone | Cost$ 2 B T | TargetMin$ 0 | TargetMax$ 3 | TargetsWithSameController$ True | Origin$ Graveyard | Destination$ Exile | TgtPrompt$ Choose target card in a graveyard | ValidTgts$ Card | SpellDescription$ Exile up to three target cards from a single graveyard.
Oracle:{2}{B}, {T}: Exile up to three target cards from a single graveyard.

View File

@@ -1,7 +1,7 @@
Name:Cease
ManaCost:1 BG
Types:Instant
A:SP$ ChangeZone | TargetMin$ 0 | TargetMax$ 2 | TgtPrompt$ Select up to two target cards from a single graveyard | TargetsFromSingleZone$ True | Origin$ Graveyard | Destination$ Exile | ValidTgts$ Card | SubAbility$ DBGainLife | SpellDescription$ Exile up to two target cards from a single graveyard.
A:SP$ ChangeZone | TargetMin$ 0 | TargetMax$ 2 | TgtPrompt$ Select up to two target cards from a single graveyard | TargetsWithSameController$ True | Origin$ Graveyard | Destination$ Exile | ValidTgts$ Card | SubAbility$ DBGainLife | SpellDescription$ Exile up to two target cards from a single graveyard.
SVar:DBGainLife:DB$ GainLife | ValidTgts$ Player | LifeAmount$ 2 | SubAbility$ DBDraw | SpellDescription$ Target player gains 2 life and draws a card.
SVar:DBDraw:DB$ Draw | Defined$ TargetedPlayer
DeckHas:Ability$Graveyard|LifeGain

View File

@@ -6,4 +6,4 @@ T:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | TriggerZones$ Battlefield | O
S:Mode$ Continuous | Affected$ Card.Self | AddTrigger$ TrigCounter | Condition$ Threshold | Description$ Threshold — As long as there are seven or more cards in your graveyard, CARDNAME has "At the beginning of your upkeep, you may put another +1/+1 counter on CARDNAME."
SVar:TrigCounter:Mode$ Phase | Phase$ Upkeep | ValidPlayer$ You | TriggerZones$ Battlefield | OptionalDecider$ You | Execute$ GimmeSome | TriggerDescription$ At the beginning of your upkeep, you may put another +1/+1 counter on CARDNAME.
SVar:GimmeSome:DB$ PutCounter | Defined$ Self | CounterType$ P1P1 | CounterNum$ 1
Oracle:At the beginning of your upkeep, you may put a +1/+1 counter on Chlorophant.\nThreshold — As long as there are seven or more cards in your graveyard,, Chlorophant has "At the beginning of your upkeep, you may put another +1/+1 counter on Chlorophant."
Oracle:At the beginning of your upkeep, you may put a +1/+1 counter on Chlorophant.\nThreshold — As long as there are seven or more cards in your graveyard, Chlorophant has "At the beginning of your upkeep, you may put another +1/+1 counter on Chlorophant."

View File

@@ -4,7 +4,7 @@ Types:Creature Human Citizen
PT:3/3
T:Mode$ ChangesZone | ValidCard$ Card.Self | Origin$ Any | Destination$ Battlefield | Execute$ TrigCharm | TriggerDescription$ When this creature enters, ABILITY
SVar:TrigCharm:DB$ Charm | Choices$ DBRepair,DBImpound | CharmNum$ 1
SVar:DBRepair:DB$ ChangeZone | Origin$ Graveyard | Destination$ Hand | ValidTgts$ Card.cmcGE4 | SpellDescription$ Repair — Return target card with mana value 4 or greater from your graveyard to your hand.
SVar:DBRepair:DB$ ChangeZone | Origin$ Graveyard | Destination$ Hand | ValidTgts$ Card.YouCtrl+cmcGE4 | SpellDescription$ Repair — Return target card with mana value 4 or greater from your graveyard to your hand.
SVar:DBImpound:DB$ ChangeZone | ValidTgts$ Artifact,Enchantment | TgtPrompt$ Select target artifact or enchantment | Origin$ Battlefield | Destination$ Exile | SpellDescription$ Impound — Exile target artifact or enchantment.
DeckHas:Ability$Graveyard
Oracle:When this creature enters, choose one —\n• Repair — Return target card with mana value 4 or greater from your graveyard to your hand.\n• Impound — Exile target artifact or enchantment.

View File

@@ -1,6 +1,6 @@
Name:Decompose
ManaCost:1 B
Types:Sorcery
A:SP$ ChangeZone | TargetMin$ 0 | TargetMax$ 3 | TargetsFromSingleZone$ True | Origin$ Graveyard | Destination$ Exile | TgtPrompt$ Choose target card in a graveyard | ValidTgts$ Card | SpellDescription$ Exile up to three target cards from a single graveyard.
A:SP$ ChangeZone | TargetMin$ 0 | TargetMax$ 3 | TargetsWithSameController$ True | Origin$ Graveyard | Destination$ Exile | TgtPrompt$ Choose target card in a graveyard | ValidTgts$ Card | SpellDescription$ Exile up to three target cards from a single graveyard.
AI:RemoveDeck:Random
Oracle:Exile up to three target cards from a single graveyard.

View File

@@ -2,7 +2,7 @@ Name:Digsite Conservator
ManaCost:2
Types:Artifact Creature Gnome
PT:2/1
A:AB$ ChangeZone | Cost$ Sac<1/CARDNAME> | TargetMin$ 0 | TargetMax$ 4 | TargetsFromSingleZone$ True | Origin$ Graveyard | Destination$ Exile | TgtPrompt$ Select up to four target cards from a single graveyard | ValidTgts$ Card | SorcerySpeed$ True | SpellDescription$ Exile up to four target cards from a single graveyard. Activate only as a sorcery.
A:AB$ ChangeZone | Cost$ Sac<1/CARDNAME> | TargetMin$ 0 | TargetMax$ 4 | TargetsWithSameController$ True | Origin$ Graveyard | Destination$ Exile | TgtPrompt$ Select up to four target cards from a single graveyard | ValidTgts$ Card | SorcerySpeed$ True | SpellDescription$ Exile up to four target cards from a single graveyard. Activate only as a sorcery.
T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Card.Self | Execute$ TrigDiscover | TriggerDescription$ When CARDNAME dies, you may pay {4}. If you do, discover 4. (Exile cards from the top of your library until you exile a nonland card with mana value 4 or less. Cast it without paying its mana cost or put it into your hand. Put the rest on the bottom in a random order.)
SVar:TrigDiscover:AB$ Discover | Cost$ 4 | Num$ 4
DeckHas:Ability$Sacrifice

View File

@@ -5,7 +5,7 @@ K:Visit:TrigPump
SVar:TrigPump:DB$ Effect | StaticAbilities$ Pump | ValidTgts$ Creature | Triggers$ ExileSelf | RememberObjects$ Targeted | SpellDescription$ Target creature gains flying until end of turn, or until any player rolls a 1, whichever comes first.
SVar:Pump:Mode$ Continuous | Affected$ Card.IsRemembered | AddKeyword$ Flying | Description$ This creature gains flying until end of turn, or until any player rolls a 1.
SVar:ExileSelf:Mode$ RolledDie | Execute$ TrigRemove | ValidResult$ EQ1 | Static$ True
SVar:TrigRemove:DB$ ChangeZone | Origin$ Command | Defined$ Self | Destination$ Exiled
SVar:TrigRemove:DB$ ChangeZone | Origin$ Command | Defined$ Self | Destination$ Exile
Oracle:Visit — Target creature gains flying until end of turn, or until any player rolls a 1, whichever comes first.
# --- VARIANTS ---

View File

@@ -4,7 +4,7 @@ Types:Instant
A:SP$ Charm | Choices$ EbonyDrain,EbonyExile,EbonyFear | Defined$ You
SVar:EbonyDrain:DB$ LoseLife | ValidTgts$ Opponent | TgtPrompt$ Select target opponent | LifeAmount$ 1 | SubAbility$ EbonyGain | SpellDescription$ Target opponent loses 1 life and you gain 1 life.
SVar:EbonyGain:DB$ GainLife | Defined$ You | LifeAmount$ 1
SVar:EbonyExile:DB$ ChangeZone | TargetMin$ 0 | TargetMax$ 3 | TargetsFromSingleZone$ True | Origin$ Graveyard | Destination$ Exile | TgtPrompt$ Choose target card in a graveyard | ValidTgts$ Card | SpellDescription$ Exile up to three target cards from a single graveyard.
SVar:EbonyExile:DB$ ChangeZone | TargetMin$ 0 | TargetMax$ 3 | TargetsWithSameController$ True | Origin$ Graveyard | Destination$ Exile | TgtPrompt$ Choose target card in a graveyard | ValidTgts$ Card | SpellDescription$ Exile up to three target cards from a single graveyard.
SVar:EbonyFear:DB$ Pump | ValidTgts$ Creature | TgtPrompt$ Select target creature | KW$ Fear | SpellDescription$ Target creature gains fear until end of turn.
AI:RemoveDeck:All
Oracle:Choose one —\n• Target opponent loses 1 life and you gain 1 life.\n• Exile up to three target cards from a single graveyard.\n• Target creature gains fear until end of turn. (It can't be blocked except by artifact creatures and/or black creatures.)

View File

@@ -3,5 +3,5 @@ ManaCost:2 R
Types:Sorcery
A:SP$ DealDamage | ValidTgts$ Creature | NumDmg$ 4 | SpellDescription$ CARDNAME deals 4 damage to target creature.
K:Mayhem:1 R
Oracle:Electros Bolt deals 4 damage to target creature.\nMayhem {1}{R} (You may cast this card from your graveyard for {1}{R} if you discarded it this turn. Timing rules still apply.)
Oracle:Electro's Bolt deals 4 damage to target creature.\nMayhem {1}{R} (You may cast this card from your graveyard for {1}{R} if you discarded it this turn. Timing rules still apply.)

View File

@@ -2,6 +2,6 @@ Name:Famished Ghoul
ManaCost:3 B
Types:Creature Zombie
PT:3/2
A:AB$ ChangeZone | Cost$ 1 B Sac<1/CARDNAME> | TargetMin$ 0 | TargetMax$ 2 | TargetsFromSingleZone$ True | Origin$ Graveyard | Destination$ Exile | TgtPrompt$ Choose target card in a graveyard | ValidTgts$ Card | SpellDescription$ Exile up to two target cards from a single graveyard.
A:AB$ ChangeZone | Cost$ 1 B Sac<1/CARDNAME> | TargetMin$ 0 | TargetMax$ 2 | TargetsWithSameController$ True | Origin$ Graveyard | Destination$ Exile | TgtPrompt$ Choose target card in a graveyard | ValidTgts$ Card | SpellDescription$ Exile up to two target cards from a single graveyard.
AI:RemoveDeck:All
Oracle:{1}{B}, Sacrifice Famished Ghoul: Exile up to two target cards from a single graveyard.

View File

@@ -5,7 +5,7 @@ PT:3/5
K:Flying
K:Deathtouch
T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigExile | TriggerDescription$ When this creature enters, exile up to two target cards from a single graveyard.
SVar:TrigExile:DB$ ChangeZone | TargetMin$ 0 | TargetMax$ 2 | TargetsFromSingleZone$ True | Origin$ Graveyard | Destination$ Exile | ValidTgts$ Card
SVar:TrigExile:DB$ ChangeZone | TargetMin$ 0 | TargetMax$ 2 | TargetsWithSameController$ True | Origin$ Graveyard | Destination$ Exile | ValidTgts$ Card
AlternateMode:Omen
Oracle:Flying, deathtouch\nWhen this creature enters, exile up to two target cards from a single graveyard.

View File

@@ -6,6 +6,6 @@ SVar:DBDraw:DB$ Draw | NumCards$ X | SubAbility$ DBUntap | AILogic$ ConsiderPrim
SVar:DBUntap:DB$ Untap | UntapUpTo$ True | UntapType$ Land | Amount$ 5 | SubAbility$ DBEffect | ConditionCheckSVar$ X | ConditionSVarCompare$ GE10
SVar:DBEffect:DB$ Effect | StaticAbilities$ STHandSize | Duration$ Permanent | SubAbility$ DBChange | ConditionCheckSVar$ X | ConditionSVarCompare$ GE10
SVar:STHandSize:Mode$ Continuous | Affected$ You | SetMaxHandSize$ Unlimited | Description$ You have no maximum hand size.
SVar:DBChange:DB$ ChangeZone | Origin$ Stack | Destination$ Exile | StackDescription$ Exile CARDNAME
SVar:DBChange:DB$ ChangeZone | Origin$ Stack | Destination$ Exile | StackDescription$ Exile CARDNAME.
SVar:X:Count$xPaid
Oracle:Draw X cards. If X is 10 or more, instead shuffle your graveyard into your library, draw X cards, untap up to five lands, and you have no maximum hand size for the rest of the game.\nExile Finale of Revelation.

View File

@@ -3,4 +3,4 @@ ManaCost:4 R
Types:Instant
A:SP$ Token | TokenAmount$ 2 | TokenScript$ r_2_2_soldier_firebending_1 | TokenOwner$ You | SpellDescription$ Create two 2/2 red Soldier creature tokens with firebending 1. (Whenever a creature with firebending 1 attacks, add {R}. This mana lasts until end of combat.)
K:Flashback:8 R
Oracle:Create two 2/2 red Soldier creature tokens with firebending 1. (Whenever a creature with firebending 1 attacks, add {R}. This mana lasts until end of combat.)\nFlashback {8}{R} (You may may cast this card from your graveyard for its flashback cost. Then exile it.)
Oracle:Create two 2/2 red Soldier creature tokens with firebending 1. (Whenever a creature with firebending 1 attacks, add {R}. This mana lasts until end of combat.)\nFlashback {8}{R} (You may cast this card from your graveyard for its flashback cost. Then exile it.)

View File

@@ -4,7 +4,7 @@ Types:Legendary Artifact Creature Robot
PT:3/2
K:More Than Meets the Eye:B R
A:AB$ PutCounter | Cost$ Sac<1/Artifact.Other/another artifact> | Defined$ Self | CounterType$ P1P1 | CounterNum$ 1 | SorcerySpeed$ True | SubAbility$ DBConvert | AILogic$ AristocratCounters | SpellDescription$ Put a +1/+1 counter on NICKNAME and convert it. Activate only as a sorcery.
SVar:DBConvert:DB$ SetState | Mode$ Transform | StackDescription$ Convert NICKNAME
SVar:DBConvert:DB$ SetState | Mode$ Transform | StackDescription$ Convert NICKNAME.
A:AB$ ChangeZoneAll | Cost$ 1 Discard<1/Hand> | ChangeType$ Card.YouOwn+counters_GE1_INTEL | Origin$ Exile | Destination$ Hand | SpellDescription$ Put all exiled cards you own with intel counters on them into your hand.
DeckHints:Ability$Counters
DeckHas:Ability$Sacrifice|Discard|Counters

View File

@@ -1,7 +1,6 @@
Name:Footsteps of the Goryo
ManaCost:2 B
Types:Sorcery Arcane
A:SP$ ChangeZone | Origin$ Graveyard | Destination$ Battlefield | ValidTgts$ Creature.YouCtrl | TgtPrompt$ Select target creature in your graveyard | GainControl$ True | SubAbility$ DBPump | AILogic$ BeforeCombat | SpellDescription$ Return target creature card from your graveyard to the battlefield. Sacrifice that creature at the beginning of the next end step.
SVar:DBPump:DB$ Pump | Defined$ Targeted | AtEOT$ Sacrifice
A:SP$ ChangeZone | Origin$ Graveyard | Destination$ Battlefield | ValidTgts$ Creature.YouCtrl | TgtPrompt$ Select target creature in your graveyard | GainControl$ True | AtEOT$ Sacrifice | AILogic$ BeforeCombat | SpellDescription$ Return target creature card from your graveyard to the battlefield. Sacrifice that creature at the beginning of the next end step.
AI:RemoveDeck:Random
Oracle:Return target creature card from your graveyard to the battlefield. Sacrifice that creature at the beginning of the next end step.

View File

@@ -4,6 +4,6 @@ Types:Creature Nightmare Horror
PT:2/2
T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigExile | TriggerDescription$ When CARDNAME enters, exile up to two target cards from a single graveyard.
T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Any | ValidCard$ Card.Self | Execute$ TrigReturn | TriggerDescription$ When CARDNAME leaves the battlefield, return the exiled cards to their owner's graveyard.
SVar:TrigExile:DB$ ChangeZone | TargetMin$ 0 | TargetMax$ 2 | TargetsFromSingleZone$ True | Origin$ Graveyard | Destination$ Exile | ValidTgts$ Card | RememberTargets$ True | ForgetOtherTargets$ True
SVar:TrigExile:DB$ ChangeZone | TargetMin$ 0 | TargetMax$ 2 | TargetsWithSameController$ True | Origin$ Graveyard | Destination$ Exile | ValidTgts$ Card | RememberTargets$ True | ForgetOtherTargets$ True
SVar:TrigReturn:DB$ ChangeZone | Defined$ Remembered | Origin$ Exile | Destination$ Graveyard
Oracle:When Gravegouger enters, exile up to two target cards from a single graveyard.\nWhen Gravegouger leaves the battlefield, return the exiled cards to their owner's graveyard.

View File

@@ -4,6 +4,6 @@ Types:Creature Human Detective
PT:3/2
K:Flying
T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigChange | TriggerDescription$ When CARDNAME enters, exile up to two target cards from a single graveyard.
SVar:TrigChange:DB$ ChangeZone | Origin$ Graveyard | Destination$ Exile | TargetMin$ 0 | TargetMax$ 2 | TargetsFromSingleZone$ True | ValidTgts$ Card | TgtPrompt$ Select up to two target cards from a single graveyard
SVar:TrigChange:DB$ ChangeZone | Origin$ Graveyard | Destination$ Exile | TargetMin$ 0 | TargetMax$ 2 | TargetsWithSameController$ True | ValidTgts$ Card | TgtPrompt$ Select up to two target cards from a single graveyard
DeckHas:Ability$Graveyard
Oracle:Flying\nWhen Griffnaut Tracker enters, exile up to two target cards from a single graveyard.

View File

@@ -2,5 +2,5 @@ Name:Inner Demons Gangsters
ManaCost:3 B
Types:Creature Human Rogue Villain
PT:3/4
A:AB$ Pump | Cost$ Discard<1/Card> | Defined$ Self | NumAtt$ +1 | KW$ Menace | SorcerySpeed$ True | SpellDescription$ This creature gets +1/+0 and gains menace until end of turn. Activate only as a sorcery. (It can't be blocked except by two or more creatures.)
A:AB$ Pump | Cost$ Discard<1/Card> | Defined$ Self | NumAtt$ +1 | KW$ Menace | SorcerySpeed$ True | SpellDescription$ This creature gets +1/+0 and gains menace until end of turn. Activate only as a sorcery. (It can't be blocked except by two or more creatures.)
Oracle:Discard a card: This creature gets +1/+0 and gains menace until end of turn. Activate only as a sorcery. (It can't be blocked except by two or more creatures.)

View File

@@ -2,7 +2,7 @@ Name:Kaya, Orzhov Usurper
ManaCost:1 W B
Types:Legendary Planeswalker Kaya
Loyalty:3
A:AB$ ChangeZone | Cost$ AddCounter<1/LOYALTY> | Origin$ Graveyard | Destination$ Exile | TargetMin$ 0 | TargetMax$ 2 | TargetsFromSingleZone$ True | ValidTgts$ Card | TgtPrompt$ Select target card from a graveyard | Planeswalker$ True | SubAbility$ DBGainLife | RememberChanged$ True | SpellDescription$ Exile up to two target cards from a single graveyard. You gain 2 life if at least one creature card was exiled this way.
A:AB$ ChangeZone | Cost$ AddCounter<1/LOYALTY> | Origin$ Graveyard | Destination$ Exile | TargetMin$ 0 | TargetMax$ 2 | TargetsWithSameController$ True | ValidTgts$ Card | TgtPrompt$ Select target card from a graveyard | Planeswalker$ True | SubAbility$ DBGainLife | RememberChanged$ True | SpellDescription$ Exile up to two target cards from a single graveyard. You gain 2 life if at least one creature card was exiled this way.
SVar:DBGainLife:DB$ GainLife | Defined$ You | LifeAmount$ 2 | ConditionDefined$ Remembered | ConditionPresent$ Creature | ConditionCompare$ GE1 | SubAbility$ DBCleanup
SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True
A:AB$ ChangeZone | Cost$ SubCounter<1/LOYALTY> | Planeswalker$ True | Origin$ Battlefield | Destination$ Exile | TgtPrompt$ Choose target nonland permanent with mana value 1 or less | ValidTgts$ Permanent.nonLand+cmcLE1 | SpellDescription$ Exile target nonland permanent with mana value 1 or less.

View File

@@ -3,7 +3,7 @@ ManaCost:6 G G
Types:Sorcery
R:Event$ Counter | ValidCard$ Card.Self | ValidSA$ Spell | Layer$ CantHappen | Description$ This spell can't be countered.
A:SP$ Draw | Defined$ You | NumCards$ X | SubAbility$ CheatBattlefield | SpellDescription$ Draw cards equal to the greatest toughness among creatures you control, then put any number of creature cards from your hand onto the battlefield.
SVar:CheatBattlefield:DB$ ChangeZone | Origin$ Hand | Destination$ Battlefield | ChangeType$ Creature | ChangeNum$ Y | StackDescription$ {p:You} puts any number of creature cards from their hand onto the battlefield
SVar:CheatBattlefield:DB$ ChangeZone | Origin$ Hand | Destination$ Battlefield | ChangeType$ Creature | ChangeNum$ Y | StackDescription$ {p:You} puts any number of creature cards from their hand onto the battlefield.
SVar:X:Count$Valid Creature.YouCtrl$GreatestToughness
SVar:Y:Count$ValidHand Creature.YouCtrl
DeckHints:Type$Wall|Plant|Treefolk

View File

@@ -1,7 +1,7 @@
Name:Lodestone Bauble
ManaCost:0
Types:Artifact
A:AB$ ChangeZone | Cost$ 1 T Sac<1/CARDNAME> | TargetMin$ 0 | TargetMax$ 4 | TargetsFromSingleZone$ True | Origin$ Graveyard | Destination$ Library | LibraryPosition$ 0 | TgtPrompt$ Choose target basic land card in a graveyard | ValidTgts$ Land.Basic | Chooser$ You | SubAbility$ ChooseZero | SpellDescription$ Put up to four target basic land cards from a player's graveyard on top of their library in any order. That player draws a card at the beginning of the next turn's upkeep.
A:AB$ ChangeZone | Cost$ 1 T Sac<1/CARDNAME> | TargetMin$ 0 | TargetMax$ 4 | TargetsWithSameController$ True | Origin$ Graveyard | Destination$ Library | LibraryPosition$ 0 | TgtPrompt$ Choose target basic land card in a graveyard | ValidTgts$ Land.Basic | Chooser$ You | SubAbility$ ChooseZero | SpellDescription$ Put up to four target basic land cards from a player's graveyard on top of their library in any order. That player draws a card at the beginning of the next turn's upkeep.
SVar:ChooseZero:DB$ ChoosePlayer | Choices$ Player | ConditionDefined$ Targeted | ConditionNotPresent$ Card | SubAbility$ DelTrigSlowtrip
SVar:DelTrigSlowtrip:DB$ DelayedTrigger | NextTurn$ True | Mode$ Phase | Phase$ Upkeep | ValidPlayer$ Player | Execute$ DrawSlowtrip | RememberObjects$ TargetedOwner,ChosenPlayer | TriggerDescription$ Draw a card.
SVar:DrawSlowtrip:DB$ Draw | NumCards$ 1 | Defined$ DelayTriggerRemembered

View File

@@ -2,7 +2,7 @@ Name:Martyr of Bones
ManaCost:B
Types:Creature Human Wizard
PT:1/1
A:AB$ ChangeZone | Cost$ 1 Reveal<X/Card.Black> Sac<1/CARDNAME> | CostDesc$ {1}, Reveal X black cards from your hand, Sacrifice CARDNAME: | Origin$ Graveyard | TargetsFromSingleZone$ True | Destination$ Exile | TargetMin$ 0 | TargetMax$ Y | ValidTgts$ Card | SpellDescription$ Exile up to X target cards from a single graveyard.
A:AB$ ChangeZone | Cost$ 1 Reveal<X/Card.Black> Sac<1/CARDNAME> | CostDesc$ {1}, Reveal X black cards from your hand, Sacrifice CARDNAME: | Origin$ Graveyard | TargetsWithSameController$ True | Destination$ Exile | TargetMin$ 0 | TargetMax$ Y | ValidTgts$ Card | SpellDescription$ Exile up to X target cards from a single graveyard.
SVar:X:TargetedObjects$Amount
SVar:Y:Count$ValidHand Card.Black+YouOwn
AI:RemoveDeck:All

View File

@@ -7,4 +7,4 @@ SVar:TrigRollDice:DB$ RollDice | Sides$ 20 | ResultSubAbilities$ 1-6:DBMana1,7-1
SVar:DBMana1:DB$ Mana | Produced$ R | Amount$ 4 | SpellDescription$ 1-6 VERT Add {R}{R}{R}{R}.
SVar:DBMana2:DB$ Mana | Produced$ R | Amount$ 5 | SpellDescription$ 7-14 VERT Add {R}{R}{R}{R}{R}.
SVar:DBMana3:DB$ Mana | Produced$ R | Amount$ 6 | SpellDescription$ 15-20 VERT Add {R}{R}{R}{R}{R}{R}.
Oracle:When this creature enters from anywhere other than a graveyard or exile, if its on the battlefield and you control 9 or fewer creatures named Name Sticker Goblin, roll a 20-sided die.\n1-6 | Add {R}{R}{R}{R}.\n7-14 | Add {R}{R}{R}{R}{R}.\n15-20 | Add {R}{R}{R}{R}{R}{R}.
Oracle:When this creature enters from anywhere other than a graveyard or exile, if it's on the battlefield and you control 9 or fewer creatures named "Name Sticker" Goblin, roll a 20-sided die.\n1-6 | Add {R}{R}{R}{R}.\n7-14 | Add {R}{R}{R}{R}{R}.\n15-20 | Add {R}{R}{R}{R}{R}{R}.

View File

@@ -8,7 +8,7 @@ T:Mode$ CounterAddedOnce | ValidCard$ Creature.Other+inZoneBattlefield+Colorless
SVar:TrigPutCounter:DB$ PutCounter | CounterType$ P1P1 | CounterNum$ 1
T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Card.Self | Execute$ TrigManifest | TriggerDescription$ When NICKNAME dies, manifest a number of cards from the top of your library equal to the number of counters on it.
SVar:TrigManifest:DB$ Manifest | Amount$ Y
SVar:Y:TriggeredCard$CardCounters.P1P1
SVar:Y:TriggeredCard$CardCounters.ALL
DeckHas:Ability$Counters
DeckHints:Ability$Counters
Oracle:Omarthis, Ghostfire Initiate enters with X +1/+1 counters on it.\nWhenever you put one or more +1/+1 counters on another colorless creature, you may put a +1/+1 counter on Omarthis.\nWhen Omarthis dies, manifest a number of cards from the top of your library equal to the number of counters on it.

View File

@@ -3,7 +3,7 @@ ManaCost:2 B
Types:Artifact
A:AB$ Token | Cost$ T Discard<1/Card> | TokenScript$ bg_1_1_pest_lifegain | StackDescription$ SpellDescription | SpellDescription$ Create a 1/1 black and green Pest creature token with "When this creature dies, you gain 1 life."
A:AB$ Mill | Cost$ 1 T | Defined$ Opponent | NumCards$ X | SpellDescription$ Each opponent mills cards equal to the amount of life you gained this turn.
A:AB$ ChangeZone | Cost$ 4 T | TargetMin$ 4 | TargetMax$ 4 | TargetsFromSingleZone$ True | Origin$ Graveyard | Destination$ Exile | TgtPrompt$ Choose four target cards from a single graveyard | ValidTgts$ Card | SubAbility$ DBDraw | StackDescription$ Exile four target cards from a single graveyard. ({c:Targeted}) | SpellDescription$ Exile four target cards from a single graveyard.
A:AB$ ChangeZone | Cost$ 4 T | TargetMin$ 4 | TargetMax$ 4 | TargetsWithSameController$ True | Origin$ Graveyard | Destination$ Exile | TgtPrompt$ Choose four target cards from a single graveyard | ValidTgts$ Card | SubAbility$ DBDraw | StackDescription$ Exile four target cards from a single graveyard. ({c:Targeted}) | SpellDescription$ Exile four target cards from a single graveyard.
SVar:DBDraw:DB$ Draw | SpellDescription$ Draw a card.
SVar:X:Count$LifeYouGainedThisTurn
DeckHas:Ability$Discard|Token|LifeGain|Mill|Graveyard

View File

@@ -17,5 +17,4 @@ PT:4/4
K:Vigilance
K:Reach
S:Mode$ Continuous | Affected$ Legendary.YouCtrl+wasCast+nonColorless | AffectedZone$ Stack | AddKeyword$ Web-slinging:G W U:Spell.Legendary+nonColorless | Description$ Each legendary spell you cast that's one or more colors has web-slinging {G}{W}{U}. (You may cast a spell for its web-slinging cost if you also return a tapped creature you control to its owner's hand.)
Oracle:Vigilance, reach\nEach legendary spell you cast thats one or more colors has web-slinging {G}{W}{U}. (You may cast a spell for its web-slinging cost if you also return a tapped creature you control to its owner's hand.)
Oracle:Vigilance, reach\nEach legendary spell you cast that's one or more colors has web-slinging {G}{W}{U}. (You may cast a spell for its web-slinging cost if you also return a tapped creature you control to its owner's hand.)

View File

@@ -11,6 +11,6 @@ ALTERNATE
Name:Pull
ManaCost:4 BR BR
Types:Sorcery
A:SP$ ChangeZone | Origin$ Graveyard | Destination$ Battlefield | TargetMin$ 0 | TargetMax$ 2 | TargetsFromSingleZone$ True | ValidTgts$ Creature | TgtPrompt$ Select up to two target creature cards from a single graveyard | GainControl$ True | SubAbility$ DBPump | SpellDescription$ Put up to two target creature cards from a single graveyard onto the battlefield under your control. They gain haste until end of turn. Sacrifice them at the beginning of the next end step.
A:SP$ ChangeZone | Origin$ Graveyard | Destination$ Battlefield | TargetMin$ 0 | TargetMax$ 2 | TargetsWithSameController$ True | ValidTgts$ Creature | TgtPrompt$ Select up to two target creature cards from a single graveyard | GainControl$ True | SubAbility$ DBPump | SpellDescription$ Put up to two target creature cards from a single graveyard onto the battlefield under your control. They gain haste until end of turn. Sacrifice them at the beginning of the next end step.
SVar:DBPump:DB$ Pump | Defined$ Targeted | KW$ Haste | AtEOT$ Sacrifice
Oracle:Put up to two target creature cards from a single graveyard onto the battlefield under your control. They gain haste until end of turn. Sacrifice them at the beginning of the next end step.

View File

@@ -1,11 +1,9 @@
Name:Push the Limit
ManaCost:5 R R
Types:Sorcery
A:SP$ ChangeZoneAll | Origin$ Graveyard | Destination$ Battlefield | ChangeType$ Mount.YouOwn,Vehicle.YouOwn | RememberChanged$ True | SubAbility$ DBPump | SpellDescription$ Return all Mount and Vehicle cards from your graveyard to the battlefield. Sacrifice them at the beginning of the next end step. Vehicles you control become artifact creatures until end of turn. Creatures you control gain haste until end of turn.
SVar:DBPump:DB$ Pump | Defined$ Remembered | AtEOT$ Sacrifice | SubAbility$ DBAnimateAll
A:SP$ ChangeZoneAll | Origin$ Graveyard | Destination$ Battlefield | ChangeType$ Mount.YouOwn,Vehicle.YouOwn | AtEOT$ Sacrifice | SubAbility$ DBAnimateAll | SpellDescription$ Return all Mount and Vehicle cards from your graveyard to the battlefield. Sacrifice them at the beginning of the next end step. Vehicles you control become artifact creatures until end of turn. Creatures you control gain haste until end of turn.
SVar:DBAnimateAll:DB$ AnimateAll | Types$ Artifact,Creature | ValidCards$ Vehicle.YouCtrl | SubAbility$ DBPumpAll
SVar:DBPumpAll:DB$ PumpAll | ValidCards$ Creature.YouCtrl | KW$ Haste | SubAbility$ DBCleanup
SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True
SVar:DBPumpAll:DB$ PumpAll | ValidCards$ Creature.YouCtrl | KW$ Haste
SVar:PlayMain1:TRUE
DeckHas:Ability$Graveyard
DeckHints:Ability$Discard|Sacrifice

View File

@@ -5,5 +5,5 @@ PT:3/2
T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigCharm | TriggerDescription$ When this creature enters, ABILITY
SVar:TrigCharm:DB$ Charm | Choices$ DBDestroy,DBExile
SVar:DBDestroy:DB$ Destroy | ValidTgts$ Creature.wasDealtDamageThisTurn | TgtPrompt$ Select target creature that was dealt damage this turn | SpellDescription$ Destroy target creature that was dealt damage this turn.
SVar:DBExile:DB$ ChangeZone | Origin$ Graveyard | Destination$ Exile | TargetMin$ 0 | TargetMax$ 2 | TargetsFromSingleZone$ True | ValidTgts$ Card | TgtPrompt$ Select up to two target cards from a single graveyard | SpellDescription$ Exile up to two target cards from a single graveyard.
SVar:DBExile:DB$ ChangeZone | Origin$ Graveyard | Destination$ Exile | TargetMin$ 0 | TargetMax$ 2 | TargetsWithSameController$ True | ValidTgts$ Card | TgtPrompt$ Select up to two target cards from a single graveyard | SpellDescription$ Exile up to two target cards from a single graveyard.
Oracle:When this creature enters, choose one —\n• Destroy target creature that was dealt damage this turn.\n• Exile up to two target cards from a single graveyard.

View File

@@ -2,5 +2,5 @@ Name:Rag Dealer
ManaCost:B
Types:Creature Human Rogue
PT:1/1
A:AB$ ChangeZone | Cost$ 2 B T | TargetMin$ 0 | TargetMax$ 3 | TargetsFromSingleZone$ True | Origin$ Graveyard | Destination$ Exile | TgtPrompt$ Choose target card in a graveyard | ValidTgts$ Card | SpellDescription$ Exile up to three target cards from a single graveyard.
A:AB$ ChangeZone | Cost$ 2 B T | TargetMin$ 0 | TargetMax$ 3 | TargetsWithSameController$ True | Origin$ Graveyard | Destination$ Exile | TgtPrompt$ Choose target card in a graveyard | ValidTgts$ Card | SpellDescription$ Exile up to three target cards from a single graveyard.
Oracle:{2}{B}, {T}: Exile up to three target cards from a single graveyard.

View File

@@ -1,7 +1,7 @@
Name:Rapid Decay
ManaCost:1 B
Types:Instant
A:SP$ ChangeZone | TargetMin$ 0 | TargetMax$ 3 | TargetsFromSingleZone$ True | Origin$ Graveyard | Destination$ Exile | TgtPrompt$ Choose target card in a graveyard | ValidTgts$ Card | SpellDescription$ Exile up to three target cards from a single graveyard.
A:SP$ ChangeZone | TargetMin$ 0 | TargetMax$ 3 | TargetsWithSameController$ True | Origin$ Graveyard | Destination$ Exile | TgtPrompt$ Choose target card in a graveyard | ValidTgts$ Card | SpellDescription$ Exile up to three target cards from a single graveyard.
K:Cycling:2
AI:RemoveDeck:All
Oracle:Exile up to three target cards from a single graveyard.\nCycling {2} ({2}, Discard this card: Draw a card.)

Some files were not shown because too many files have changed in this diff Show More