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

@@ -20,14 +20,14 @@ public class AirbendEffect extends SpellAbilityEffect {
@Override
protected String getStackDescription(SpellAbility sa) {
final StringBuilder sb = new StringBuilder("Airbend ");
Iterable<Card> tgts;
if (sa.usesTargeting()) {
tgts = getCardsfromTargets(sa);
} else { // otherwise add self to list and go from there
tgts = sa.knownDetermineDefined(sa.getParam("Defined"));
}
sb.append(sa.getParamOrDefault("DefinedDesc", Lang.joinHomogenous(tgts)));
sb.append(".");
if (Iterables.size(tgts) > 1) {
@@ -46,7 +46,7 @@ public class AirbendEffect extends SpellAbilityEffect {
final Player pl = sa.getActivatingPlayer();
final CardZoneTable triggerList = CardZoneTable.getSimultaneousInstance(sa);
for (Card c : getTargetCards(sa)) {
final Card gameCard = game.getCardState(c, null);
// gameCard is LKI in that case, the card is not in game anymore
@@ -55,7 +55,7 @@ public class AirbendEffect extends SpellAbilityEffect {
if (gameCard == null || !c.equalsWithGameTimestamp(gameCard) || gameCard.isPhasedOut()) {
continue;
}
if (!gameCard.canExiledBy(sa, true)) {
continue;
}
@@ -86,7 +86,7 @@ public class AirbendEffect extends SpellAbilityEffect {
}
triggerList.triggerChangesZoneAll(game, sa);
handleExiledWith(triggerList.allCards(), sa);
pl.triggerElementalBend(TriggerType.Airbend);
}

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

@@ -47,7 +47,7 @@ public class EarthbendEffect extends SpellAbilityEffect {
final Game game = source.getGame();
final Player pl = sa.getActivatingPlayer();
int num = AbilityUtils.calculateAmount(source, sa.getParamOrDefault("Num", "1"), sa);
long ts = game.getNextTimestamp();
String desc = "When it dies or is exiled, return it to the battlefield tapped.";
@@ -59,17 +59,17 @@ public class EarthbendEffect extends SpellAbilityEffect {
c.addNewPT(0, 0, ts, 0);
c.addChangedCardTypes(Arrays.asList("Creature"), null, false, EnumSet.noneOf(RemoveType.class), ts, 0, true, false);
c.addChangedCardKeywords(Arrays.asList("Haste"), null, false, ts, null);
GameEntityCounterTable table = new GameEntityCounterTable();
c.addCounter(CounterEnumType.P1P1, num, pl, table);
table.replaceCounterEffect(game, sa, true);
buildTrigger(sa, c, sbTrigA, "Graveyard");
buildTrigger(sa, c, sbTrigB, "Exile");
}
pl.triggerElementalBend(TriggerType.Earthbend);
}
protected void buildTrigger(SpellAbility sa, Card c, String sbTrig, String zone) {
final Card source = sa.getHostCard();
final Game game = source.getGame();

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

@@ -6,4 +6,4 @@ T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.S
SVar:TrigEarthbend:DB$ Earthbend | Num$ 2
S:Mode$ Continuous | Affected$ Creature.YouCtrl+counters_GE1_P1P1 | AddKeyword$ Trample | Description$ Creatures you control with +1/+1 counters on them have trample.
DeckHas:Ability$Counters
Oracle:When this creature enters, earthbend 2. (Target land you control becomes a 0/0 creature with haste that's still a land. Put two +1/+1 counters on it. When it dies or is exiled, return it to the battlefield tapped.)\nCreatures you control with +1/+1 counters on them have trample.
Oracle:When this creature enters, earthbend 2. (Target land you control becomes a 0/0 creature with haste that's still a land. Put two +1/+1 counters on it. When it dies or is exiled, return it to the battlefield tapped.)\nCreatures you control with +1/+1 counters on them have trample.

View File

@@ -3,4 +3,4 @@ ManaCost:6 B
Types:Sorcery
A:SP$ ChangeZone | Origin$ Graveyard | Destination$ Battlefield | TgtPrompt$ Choose up to six target creature cards with different names in your graveyard | ValidTgts$ Creature.YouOwn | TargetsWithDifferentNames$ True | TargetMin$ 0 | TargetMax$ 6 | SpellDescription$ Return up to six target creature cards with different names from your graveyard to the battlefield.
DeckHas:Ability$Graveyard
Oracle:Return up to six target creature cards with different names from your graveyard to the battlefield.
Oracle:Return up to six target creature cards with different names from your graveyard to the battlefield.

View File

@@ -7,4 +7,4 @@ SVar:TrigEarthbend:DB$ Earthbend | Num$ 1
T:Mode$ Attacks | ValidCard$ Card.Self | Execute$ TrigPutCounterAll | TriggerDescription$ Whenever NICKNAME attacks, put two +1/+1 counters on each land creature you control.
SVar:TrigPutCounterAll:DB$ PutCounterAll | ValidCards$ Creature.Land+YouCtrl | CounterType$ P1P1 | CounterNum$ 2
DeckHas:Ability$Counters
Oracle:When Bumi enters, earthbend 1. (Target land you control becomes a 0/0 creature with haste that's still a land. Put a +1/+1 counter on it. When it dies or is exiled, return it to the battlefield tapped.)\nWhenever Bumi attacks, put two +1/+1 counters on each land creature you control.
Oracle:When Bumi enters, earthbend 1. (Target land you control becomes a 0/0 creature with haste that's still a land. Put a +1/+1 counter on it. When it dies or is exiled, return it to the battlefield tapped.)\nWhenever Bumi attacks, put two +1/+1 counters on each land creature you control.

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

@@ -5,4 +5,4 @@ A:SP$ Charm | Choices$ DBDiscard,DBEarthbend
SVar:DBDiscard:DB$ Discard | ValidTgts$ Opponent | Mode$ RevealYouChoose | DiscardValid$ Permanent.nonLand | NumCards$ 1 | SpellDescription$ Target opponent reveals their hand. You choose a nonland permanent card from it. That player discards that card.
SVar:DBEarthbend:DB$ Earthbend | Num$ 2 | SpellDescription$ Earthbend 2. (Target land you control becomes a 0/0 creature with haste that's still a land. Put two +1/+1 counters on it. When it dies or is exiled, return it to the battlefield tapped.)
DeckHas:Ability$Discard|Counters
Oracle:Choose one —\n• Target opponent reveals their hand. You choose a nonland permanent card from it. That player discards that card.\n• Earthbend 2. (Target land you control becomes a 0/0 creature with haste that's still a land. Put two +1/+1 counters on it. When it dies or is exiled, return it to the battlefield tapped.)
Oracle:Choose one —\n• Target opponent reveals their hand. You choose a nonland permanent card from it. That player discards that card.\n• Earthbend 2. (Target land you control becomes a 0/0 creature with haste that's still a land. Put two +1/+1 counters on it. When it dies or is exiled, return it to the battlefield tapped.)

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

@@ -5,4 +5,4 @@ PT:3/1
T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Card.Self | Execute$ TrigEarthbend | TriggerDescription$ When this creature dies, earthbend 2. (Target land you control becomes a 0/0 creature with haste that's still a land. Put two +1/+1 counters on it. When it dies or is exiled, return it to the battlefield tapped.)
SVar:TrigEarthbend:DB$ Earthbend | Num$ 2
DeckHas:Ability$Counters
Oracle:When this creature dies, earthbend 2. (Target land you control becomes a 0/0 creature with haste that's still a land. Put two +1/+1 counters on it. When it dies or is exiled, return it to the battlefield tapped.)
Oracle:When this creature dies, earthbend 2. (Target land you control becomes a 0/0 creature with haste that's still a land. Put two +1/+1 counters on it. When it dies or is exiled, return it to the battlefield tapped.)

View File

@@ -6,4 +6,4 @@ T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.S
SVar:TrigEarthbend:DB$ Earthbend | Num$ 2
S:Mode$ Continuous | Affected$ Creature.Land+YouCtrl | AddKeyword$ Vigilance | Description$ Land creatures you control have vigilance. (Attacking doesn't cause them to tap.)
DeckHas:Ability$Counters
Oracle:When this creature enters, earthbend 2. (Target land you control becomes a 0/0 creature with haste that's still a land. Put two +1/+1 counters on it. When it dies or is exiled, return it to the battlefield tapped.)\nLand creatures you control have vigilance. (Attacking doesn't cause them to tap.)
Oracle:When this creature enters, earthbend 2. (Target land you control becomes a 0/0 creature with haste that's still a land. Put two +1/+1 counters on it. When it dies or is exiled, return it to the battlefield tapped.)\nLand creatures you control have vigilance. (Attacking doesn't cause them to tap.)

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

@@ -7,4 +7,4 @@ T:Mode$ SpellCast | ValidCard$ Card.wasCastFromExile | ValidActivatingPlayer$ Yo
T:Mode$ ChangesZone | Origin$ Exile | Destination$ Battlefield | ValidCard$ Permanent.YouCtrl | TriggerZones$ Battlefield | Execute$ TrigPutCounterAll | Secondary$ True | TriggerDescription$ Whenever you cast a spell from exile and whenever a permanent you control enters from exile, put a +1/+1 counter on each creature you control.
SVar:TrigPutCounterAll:DB$ PutCounterAll | ValidCards$ Creature.YouCtrl | CounterType$ P1P1 | CounterNum$ 1
SVar:X:Count$CardPower
Oracle:Firebending X, where X is Fire Lord Zuko's power. (Whenever this creature attacks, add X {R}. This mana lasts until end of combat.)\nWhenever you cast a spell from exile and whenever a permanent you control enters from exile, put a +1/+1 counter on each creature you control.
Oracle:Firebending X, where X is Fire Lord Zuko's power. (Whenever this creature attacks, add X {R}. This mana lasts until end of combat.)\nWhenever you cast a spell from exile and whenever a permanent you control enters from exile, put a +1/+1 counter on each creature you control.

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,4 +4,4 @@ Types:Creature Human Cleric
PT:2/2
K:Firebending:1
A:AB$ PutCounter | Cost$ 1 R R | CounterType$ P1P1 | CounterNum$ 1 | SpellDescription$ Put a +1/+1 counter on this creature.
Oracle:Firebending 1 (Whenever this creature attacks, add {R}. This mana lasts until end of combat.)\n{1}{R}{R}: Put a +1/+1 counter on this creature.
Oracle:Firebending 1 (Whenever this creature attacks, add {R}. This mana lasts until end of combat.)\n{1}{R}{R}: Put a +1/+1 counter on this creature.

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

@@ -6,4 +6,4 @@ T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Ally.O
SVar:TrigEarthbend:DB$ Earthbend | Num$ 1
DeckHas:Ability$Counters
DeckNeeds:Type$Ally
Oracle:Whenever another Ally you control enters, earthbend 1. (Target land you control becomes a 0/0 creature with haste that's still a land. Put a +1/+1 counter on it. When it dies or is exiled, return it to the battlefield tapped.)
Oracle:Whenever another Ally you control enters, earthbend 1. (Target land you control becomes a 0/0 creature with haste that's still a land. Put a +1/+1 counter on it. When it dies or is exiled, return it to the battlefield tapped.)

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

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