Merge branch 'master' into CrypticSpire

This commit is contained in:
kevlahnota
2024-11-16 17:14:51 +08:00
committed by GitHub
31 changed files with 141 additions and 156 deletions

View File

@@ -8,11 +8,11 @@ on:
description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)'
required: false
default: false
upload_package:
type: boolean
description: 'Upload the completed Android package'
required: false
default: true
#upload_package:
# type: boolean
# description: 'Upload the completed Android package'
# required: false
# default: true
schedule:
# * is a special character in YAML so you have to quote this string
- cron: '00 19 * * *'
@@ -89,41 +89,23 @@ jobs:
- name: Build/Install/Publish to GitHub Packages Apache Maven
run: |
export _JAVA_OPTIONS="-Xmx2g"
d=$(date +%m.%d)
# Replace date in forge-gui-mobile/src/forge/Forge.java
# sed -i -e "s/-SNAPSHOT/-SNAPSHOT-${d}/g" forge-gui-mobile/src/forge/Forge.java
mvn -U -B -P android-release-build install -e -Dcardforge-repo.username=${{ secrets.FTP_USERNAME }} -Dcardforge-repo.password=${{ secrets.FTP_PASSWORD }} -Dandroid.sdk.path=/usr/local/lib/android/sdk -Dandroid.buildToolsVersion=35.0.0 -Dmaven.test.skip=true
mkdir -p forge-gui-android/target/upload
mv forge-gui-android/target/*-signed-aligned.apk forge-gui-android/target/upload/
mv forge-gui-android/target/assets.zip forge-gui-android/target/upload/
cd forge-gui-android/target/upload/
# Get the first APK file in the folder
mkdir upload
mv /home/runner/work/forge/forge/forge-gui-android/target/*-signed-aligned.apk upload/
mv /home/runner/work/forge/forge/forge-gui-android/target/assets.zip upload/
mv /home/runner/work/forge/forge/forge-gui-android/target/classes/assets/version.txt upload/
cd upload
ls
apk_file=$(find . -maxdepth 1 -type f -name '*.apk' -print -quit)
if [ -n "$apk_file" ]; then
version=$(echo "$apk_file" | grep -oP 'forge-android-\K\d+\.\d+\.\d+-SNAPSHOT' | sed 's/-signed-aligned.apk//')
echo "APK File: $apk_file"
echo "Version: $version"
# mv *.apk "forge-android-$version-$d-signed-aligned.apk"
echo "$version-$d" > version.txt
else
echo "No .apk files found in the specified folder."
fi
cd -
env:
GITHUB_TOKEN: ${{ github.token }}
- name: 📂 Sync files
uses: SamKirkland/FTP-Deploy-Action@v4.3.4
if: ${{ inputs.upload_package }}
#if: ${{ inputs.upload_package }}
with:
server: ftp.cardforge.org
username: ${{ secrets.FTP_USERNAME }}
password: ${{ secrets.FTP_PASSWORD }}
local-dir: forge-gui-android/target/upload/
local-dir: upload/
server-dir: downloads/dailysnapshots/
state-name: .ftp-deploy-android-sync-state.json

View File

@@ -22,6 +22,8 @@ import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.ai.AiCardMemory.MemorySet;
import forge.ai.ability.ChangeZoneAi;
import forge.ai.ability.LearnAi;
import forge.ai.simulation.SpellAbilityPicker;
@@ -845,6 +847,13 @@ public class AiController {
return AiPlayDecision.CantAfford;
}
// check if enough left (pass memory indirectly because we don't want to include those)
Set<Card> tappedForMana = AiCardMemory.getMemorySet(player, MemorySet.PAYS_TAP_COST);
if (tappedForMana != null && tappedForMana.isEmpty() &&
!ComputerUtilCost.checkTapTypeCost(player, sa.getPayCosts(), host, sa, new CardCollection(tappedForMana))) {
return AiPlayDecision.CantAfford;
}
// if we got here, looks like we can play the final cost and we could properly set up and target the API and
// are willing to play the SA
return AiPlayDecision.WillPlay;

View File

@@ -2,6 +2,8 @@ package forge.ai;
import com.google.common.base.Predicates;
import com.google.common.collect.Lists;
import forge.ai.AiCardMemory.MemorySet;
import forge.card.CardType;
import forge.card.MagicColor;
import forge.game.Game;
@@ -32,6 +34,10 @@ public class AiCostDecision extends CostDecisionMakerBase {
discarded = new CardCollection();
tapped = new CardCollection();
Set<Card> tappedForMana = AiCardMemory.getMemorySet(ai0, MemorySet.PAYS_TAP_COST);
if (tappedForMana != null) {
tapped.addAll(tappedForMana);
}
}
@Override
@@ -440,21 +446,6 @@ public class AiCostDecision extends CostDecisionMakerBase {
return null;
}
if ("DontPayTapCostWithManaSources".equals(source.getSVar("AIPaymentPreference"))) {
CardCollectionView toExclude =
CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield), type.split(";"),
ability.getActivatingPlayer(), ability.getHostCard(), ability);
toExclude = CardLists.filter(toExclude, card -> {
for (final SpellAbility sa : card.getSpellAbilities()) {
if (sa.isManaAbility() && sa.getPayCosts().hasTapCost()) {
return true;
}
}
return false;
});
exclude.addAll(toExclude);
}
String totalP = "";
CardCollectionView totap;
if (isVehicle) {

View File

@@ -782,6 +782,7 @@ public class ComputerUtil {
}
CardLists.sortByPowerAsc(typeList);
// TODO prefer noncreatures without tap abilities
final CardCollection tapList = new CardCollection();

View File

@@ -29,6 +29,7 @@ import forge.util.collect.FCollectionView;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import java.util.Collection;
import java.util.List;
import java.util.Set;
@@ -450,7 +451,7 @@ public class ComputerUtilCost {
* the source
* @return true, if successful
*/
public static boolean checkTapTypeCost(final Player ai, final Cost cost, final Card source, final SpellAbility sa, final CardCollection alreadyTapped) {
public static boolean checkTapTypeCost(final Player ai, final Cost cost, final Card source, final SpellAbility sa, final Collection<Card> alreadyTapped) {
if (cost == null) {
return true;
}
@@ -487,8 +488,8 @@ public class ComputerUtilCost {
c = AbilityUtils.calculateAmount(source, part.getAmount(), sa);
}
CardCollection exclude = new CardCollection();
if (AiCardMemory.getMemorySet(ai, MemorySet.PAYS_TAP_COST) != null) {
exclude.addAll(AiCardMemory.getMemorySet(ai, MemorySet.PAYS_TAP_COST));
if (alreadyTapped != null) {
exclude.addAll(alreadyTapped);
}
// trying to produce mana that includes tapping source that will already be tapped
if (exclude.contains(source) && cost.hasTapCost()) {
@@ -500,12 +501,12 @@ public class ComputerUtilCost {
}
CardCollection tapChoices = ComputerUtil.chooseTapType(ai, type, source, cost.hasTapCost(), c, exclude, sa);
if (tapChoices != null) {
for (Card choice : tapChoices) {
AiCardMemory.rememberCard(ai, choice, MemorySet.PAYS_TAP_COST);
}
if (alreadyTapped != null) {
alreadyTapped.addAll(tapChoices);
// if manasource gets tapped to produce it also can't help paying another
if (cost.hasTapCost()) {
AiCardMemory.rememberCard(ai, source, MemorySet.PAYS_TAP_COST);
alreadyTapped.add(source);
}
}
return true;
}
@@ -600,33 +601,6 @@ public class ComputerUtilCost {
}
}
// TODO: Alternate costs which involve both paying mana and tapping a card, e.g. Zahid, Djinn of the Lamp
// Current AI decides on each part separately, thus making it possible for the AI to cheat by
// tapping a mana source for mana and for the tap cost at the same time. Until this is improved, AI
// will not consider mana sources valid for paying the tap cost to avoid this exact situation.
if ("DontPayTapCostWithManaSources".equals(sa.getHostCard().getSVar("AIPaymentPreference"))) {
for (final CostPart part : sa.getPayCosts().getCostParts()) {
if (part instanceof CostTapType) {
CardCollectionView nonManaSources =
CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield), part.getType().split(";"),
sa.getActivatingPlayer(), sa.getHostCard(), sa);
nonManaSources = CardLists.filter(nonManaSources, card -> {
boolean hasManaSa = false;
for (final SpellAbility sa1 : card.getSpellAbilities()) {
if (sa1.isManaAbility() && sa1.getPayCosts().hasTapCost()) {
hasManaSa = true;
break;
}
}
return !hasManaSa;
});
if (nonManaSources.size() < part.convertAmount()) {
return false;
}
}
}
}
// Bail early on Casualty in case there are no cards that would make sense to pay with
if (sa.getHostCard().hasKeyword(Keyword.CASUALTY)) {
for (final CostPart part : sa.getPayCosts().getCostParts()) {

View File

@@ -268,6 +268,7 @@ public class ComputerUtilMana {
}
for (final SpellAbility ma : saList) {
// this rarely seems like a good idea
if (ma.getHostCard() == saHost) {
continue;
}
@@ -276,7 +277,7 @@ public class ComputerUtilMana {
continue;
}
if (!ComputerUtilCost.checkTapTypeCost(ai, ma.getPayCosts(), ma.getHostCard(), sa, new CardCollection())) {
if (!ComputerUtilCost.checkTapTypeCost(ai, ma.getPayCosts(), ma.getHostCard(), sa, AiCardMemory.getMemorySet(ai, MemorySet.PAYS_TAP_COST))) {
continue;
}

View File

@@ -85,7 +85,7 @@ public final class ImageKeys {
}
private static final Map<String, File> cachedCards = new HashMap<>(50000);
private static HashSet<String> missingCards = new HashSet<>();
public static HashSet<String> missingCards = new HashSet<>();
public static void clearMissingCards() {
missingCards.clear();
}
@@ -310,7 +310,7 @@ public final class ImageKeys {
}
// System.out.println("File not found, no image created: " + key);
//add missing cards - disable for desktop version for compatibility reasons with autodownloader
// add missing cards - disable for desktop version for compatibility reasons with autodownloader
if (isLibGDXPort && !hasSetLookup(filename)) //missing cards with setlookup is handled differently
missingCards.add(filename);
return null;

View File

@@ -2464,7 +2464,7 @@ public class GameAction {
game.getAction().reveal(milledPlayer, destination, p, false, message, addSuffix);
}
game.getGameLog().add(GameLogEntryType.ZONE_CHANGE, p + " milled " +
Lang.joinHomogenous(milled) + toZoneStr + ".");
Lang.joinHomogenous(milledPlayer) + toZoneStr + ".");
}
}

View File

@@ -306,6 +306,17 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
return new GameLogEntry(GameLogEntryType.MULLIGAN, message);
}
@Override
public GameLogEntry visit(GameEventCardForetold ev) {
String sb = TextUtil.concatWithSpace(ev.activatingPlayer.toString(), "has foretold.");
return new GameLogEntry(GameLogEntryType.STACK_RESOLVE, sb);
}
@Override
public GameLogEntry visit(GameEventCardPlotted ev) {
return new GameLogEntry(GameLogEntryType.STACK_RESOLVE, ev.toString());
}
@Subscribe
public void recieve(GameEvent ev) {
GameLogEntry le = ev.visit(this);

View File

@@ -1640,9 +1640,15 @@ public class AbilityUtils {
return doXMath(calculateAmount(c, sq[v ? 1 : 2], ctb), expr, c, ctb);
}
SpellAbility sa = null;
if (ctb instanceof SpellAbility) {
final SpellAbility sa = (SpellAbility) ctb;
sa = (SpellAbility) ctb;
} else if (sq[0].contains("xPaid") && ctb instanceof TriggerReplacementBase) {
// try avoid fallback
sa = ((TriggerReplacementBase) ctb).getOverridingAbility();
}
if (sa != null) {
// special logic for xPaid in SpellAbility
if (sq[0].contains("xPaid")) {
SpellAbility root = sa.getRootAbility();
@@ -1672,7 +1678,8 @@ public class AbilityUtils {
// and the spell that became that object as it resolved had a value of X chosen for any of its costs,
// the value of X for that ability is the same as the value of X for that spell, although the value of X for that permanent is 0.
if (TriggerType.ChangesZone.equals(t.getMode()) && ZoneType.Battlefield.name().equals(t.getParam("Destination"))) {
return doXMath(c.getXManaCostPaid(), expr, c, ctb);
int x = isUnlinkedFromCastSA(ctb, c) ? 0 : c.getXManaCostPaid();
return doXMath(x, expr, c, ctb);
} else if (TriggerType.SpellCast.equals(t.getMode())) {
// Cast Trigger like Hydroid Krasis
SpellAbilityStackInstance castSI = (SpellAbilityStackInstance) root.getTriggeringObject(AbilityKey.StackInstance);
@@ -1696,7 +1703,8 @@ public class AbilityUtils {
}
if (root.isReplacementAbility() && sa.hasParam("ETB")) {
return doXMath(c.getXManaCostPaid(), expr, c, ctb);
int x = isUnlinkedFromCastSA(ctb, c) ? 0 : c.getXManaCostPaid();
return doXMath(x, expr, c, ctb);
}
return doXMath(0, expr, c, ctb);

View File

@@ -4111,9 +4111,6 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars, ITr
}
public final boolean isModified() {
if (!isCreature()) {
return false;
}
if (this.isEquipped() || this.hasCounters()) {
return true;
}

View File

@@ -3101,8 +3101,6 @@ public class CardFactoryUtil {
// because it doesn't work other wise
c.setForetoldCostByEffect(true);
}
String sb = TextUtil.concatWithSpace(getActivatingPlayer().toString(), "has foretold.");
game.getGameLog().add(GameLogEntryType.STACK_RESOLVE, sb);
game.fireEvent(new GameEventCardForetold(getActivatingPlayer()));
}
};
@@ -3425,8 +3423,6 @@ public class CardFactoryUtil {
c.setPlotted(true);
String sb = TextUtil.concatWithSpace(getActivatingPlayer().toString(), "has plotted", c.toString());
game.getGameLog().add(GameLogEntryType.STACK_RESOLVE, sb);
game.fireEvent(new GameEventCardPlotted(c, getActivatingPlayer()));
}
};

View File

@@ -105,6 +105,7 @@ public interface ICostVisitor<T> {
public T visit(CostFlipCoin cost) {
return null;
}
@Override
public T visit(CostForage cost) {
return null;
@@ -146,7 +147,9 @@ public interface ICostVisitor<T> {
}
@Override
public T visit(CostPromiseGift cost) { return null; }
public T visit(CostPromiseGift cost) {
return null;
}
@Override
public T visit(CostPutCardToLib cost) {

View File

@@ -92,9 +92,6 @@ public class ReplaceDamage extends ReplacementEffect {
return false;
}
}
if (hasParam("IsEquipping") && !getHostCard().isEquipping()) {
return false;
}
if (hasParam("DamageTarget")) {
//Lava Burst and Whippoorwill check

View File

@@ -665,6 +665,7 @@ public class ReplacementHandler {
}
List<ReplacementEffect> possibleReplacers = new ArrayList<>(replaceCandidateMap.keySet());
// TODO should be able to choose different order for each entity
ReplacementEffect chosenRE = decider.getController().chooseSingleReplacementEffect(possibleReplacers);
List<Map<AbilityKey, Object>> runParamList = replaceCandidateMap.get(chosenRE);

View File

@@ -54,6 +54,19 @@
<failIfNoMatch>false</failIfNoMatch>
</configuration>
</execution>
<execution>
<id>released-version</id>
<phase>validate</phase>
<goals>
<goal>released-version</goal>
</goals>
</execution>
<execution>
<id>parse-version</id>
<goals>
<goal>parse-version</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
@@ -170,26 +183,6 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>3.6.0</version>
<executions>
<execution>
<id>released-version</id>
<phase>validate</phase>
<goals>
<goal>released-version</goal>
</goals>
</execution>
<execution>
<id>parse-version</id>
<goals>
<goal>parse-version</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>se.bjurr.gitchangelog</groupId>
<artifactId>git-changelog-maven-plugin</artifactId>

View File

@@ -78,9 +78,8 @@ public class CCombat implements ICDoc {
}
display.append("\n");
PlayerView controller = null;
if (defender instanceof CardView) {
controller = ((CardView) defender).getController();
PlayerView controller = ((CardView) defender).getController();
if (controller == null)
//shouldn't be null but display card's + controller ie Black Knight's controller
display.append(Lang.getInstance().getPossesive(defender.getName())).append(" controller");

View File

@@ -70,7 +70,6 @@ public class VAssignGenericAmount extends FDialog {
/** Constructor.
*
* @param attacker0 {@link forge.game.card.Card}
* @param targets Map<GameEntity, Integer>, map of GameEntity and its maximum assignable amount
* @param amount Total amount to be assigned
* @param atLeastOne Must assign at least one amount to each target
@@ -168,7 +167,7 @@ public class VAssignGenericAmount extends FDialog {
obj = add(new EffectSourcePanel((CardView)entity));
} else if (entity instanceof PlayerView) {
PlayerView player = (PlayerView)entity;
obj = add(new MiscTargetPanel(player.getName(), MatchController.getPlayerAvatar(player)));
obj = add(new MiscTargetPanel(player.getName(), MatchController.getPlayerAvatar(player), null));
} else if (entity instanceof Byte) {
FSkinImageInterface manaSymbol;
byte color = (Byte) entity;
@@ -185,9 +184,9 @@ public class VAssignGenericAmount extends FDialog {
} else { // Should never come here, but add this to avoid compile error
manaSymbol = Forge.getAssets().images().get(FSkinProp.IMG_MANA_COLORLESS);
}
obj = add(new MiscTargetPanel("", manaSymbol));
obj = add(new MiscTargetPanel("", manaSymbol, entity));
} else {
obj = add(new MiscTargetPanel(entity.toString(), FSkinImage.UNKNOWN));
obj = add(new MiscTargetPanel(entity.toString(), FSkinImage.UNKNOWN, null));
}
label = add(new FLabel.Builder().text("0").font(FSkinFont.get(18)).align(Align.center).build());
btnSubtract = add(new FLabel.ButtonBuilder().icon(FSkinImage.MINUS).command(e -> assignAmountTo(entity, false)).build());
@@ -232,19 +231,21 @@ public class VAssignGenericAmount extends FDialog {
}
}
private static class MiscTargetPanel extends FDisplayObject {
private static final FSkinFont FONT = FSkinFont.get(18);
private static FSkinColor getForeColor() {
private class MiscTargetPanel extends FDisplayObject {
private final FSkinFont FONT = FSkinFont.get(18);
private FSkinColor getForeColor() {
if (Forge.isMobileAdventureMode)
return FSkinColor.get(Colors.ADV_CLR_TEXT);
return FSkinColor.get(Colors.CLR_TEXT);
}
private final String name;
private final FImage image;
private final Object entity;
private MiscTargetPanel(String name0, FImage image0) {
private MiscTargetPanel(String name0, FImage image0, Object entity0) {
name = name0;
image = image0;
entity = entity0;
}
@Override
@@ -254,6 +255,24 @@ public class VAssignGenericAmount extends FDialog {
g.drawImage(image, 0, 0, w, w);
g.drawText(name, FONT, getForeColor(), 0, w, w, h - w, false, Align.center, true);
}
@Override
public boolean tap(float x, float y, int count) {
if (count > 1 && entity != null) {
AssignTarget at = targetsMap.get(entity);
int assigned = at.amount;
int leftToAssign = Math.max(0, at.max - assigned);
int amountToAdd = Math.min(getRemainingAmount(), leftToAssign);
if (0 == amountToAdd || amountToAdd + assigned < 0) {
return false;
}
addAssignedAmount(at, amountToAdd);
updateLabels();
}
return super.tap(x, y, count);
}
}
private void assignAmountTo(Object source, boolean isAdding) {

View File

@@ -2,7 +2,7 @@ Name:Infernal Vessel
ManaCost:2 B
Types:Creature Human Cleric
PT:2/1
T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Card.Self+notDemon | Execute$ TrigReturn | TriggerDescription$ When this creature dies, if it wasn't a Demon, return it to the battlefield under its owner's control with two +1/+1 counters on it. It's a Demon in addition to its other types.
T:Mode$ ChangesZone | Origin$ Battlefield | Destination$ Graveyard | ValidCard$ Card.Self+nonDemon | Execute$ TrigReturn | TriggerDescription$ When this creature dies, if it wasn't a Demon, return it to the battlefield under its owner's control with two +1/+1 counters on it. It's a Demon in addition to its other types.
SVar:TrigReturn:DB$ ChangeZone | Origin$ Graveyard | Destination$ Battlefield | Defined$ TriggeredNewCardLKICopy | WithCountersType$ P1P1 | WithCountersAmount$ 2 | AnimateSubAbility$ DBAnimate
SVar:DBAnimate:DB$ Animate | Defined$ Remembered | Types$ Demon | Duration$ Permanent
Oracle:When this creature dies, if it wasn't a Demon, return it to the battlefield under its owner's control with two +1/+1 counters on it. It's a Demon in addition to its other types.

View File

@@ -3,7 +3,7 @@ ManaCost:2 W B
Types:Legendary Creature Spirit Noble
PT:3/3
K:Flying
T:Mode$ BecomesTarget | ValidTarget$ Card.Self,Spirit.YouCtrl+Other | ValidSource$ Spell | TriggerZones$ Battlefield | Execute$ TrigPhaseOut | TriggerDescription$ Whenever CARDNAME or another Spirit you control becomes the target of a spell, it phases out. (Treat it and anything attached to it as though they don't exist until your next turn.)
T:Mode$ BecomesTarget | ValidTarget$ Card.Self,Spirit.YouCtrl+Other+inZoneBattlefield | ValidSource$ Spell | TriggerZones$ Battlefield | Execute$ TrigPhaseOut | TriggerDescription$ Whenever CARDNAME or another Spirit you control becomes the target of a spell, it phases out. (Treat it and anything attached to it as though they don't exist until your next turn.)
SVar:TrigPhaseOut:DB$ Phases | Defined$ TriggeredTargetLKICopy
T:Mode$ PhaseIn | ValidCard$ Card.Self,Spirit.YouCtrl+Other | TriggerZones$ Battlefield | Execute$ TrigToken | TriggerDescription$ Whenever CARDNAME or another Spirit you control phases in, create a tapped 1/1 white Spirit creature token with flying.
SVar:TrigToken:DB$ Token | TokenAmount$ 1 | TokenScript$ w_1_1_spirit_flying | TokenOwner$ You | TokenTapped$ True

View File

@@ -2,7 +2,7 @@ Name:Needletooth Pack
ManaCost:3 G G
Types:Creature Dinosaur
PT:4/5
T:Mode$ Phase | Phase$ End of Turn | CheckSVar$ Morbid | SVarCompare$ GE1 | TriggerZones$ Battlefield | Execute$ TrigPutCounter | TriggerDescription$ Morbid — At the beginning of your end step, if a creature died this turn, put two +1/+1 counters on target creature you control.
T:Mode$ Phase | Phase$ End of Turn | ValidPlayer$ You | CheckSVar$ Morbid | SVarCompare$ GE1 | TriggerZones$ Battlefield | Execute$ TrigPutCounter | TriggerDescription$ Morbid — At the beginning of your end step, if a creature died this turn, put two +1/+1 counters on target creature you control.
SVar:TrigPutCounter:DB$ PutCounter | ValidTgts$ Creature.YouCtrl | TgtPrompt$ Select target creature you control | CounterType$ P1P1 | CounterNum$ 2
SVar:Morbid:Count$Morbid.1.0
Oracle:Morbid — At the beginning of your end step, if a creature died this turn, put two +1/+1 counters on target creature you control.

View File

@@ -2,6 +2,6 @@ Name:Pariah's Shield
ManaCost:5
Types:Artifact Equipment
K:Equip:3
R:Event$ DamageDone | ActiveZones$ Battlefield | ValidTarget$ You | ReplaceWith$ DmgEquipped | IsEquipping$ True | DamageTarget$ Equipped | Description$ All damage that would be dealt to you is dealt to equipped creature instead.
R:Event$ DamageDone | ActiveZones$ Battlefield | ValidTarget$ You | ReplaceWith$ DmgEquipped | IsPresent$ Card.equipping | PresentDefined$ Self | DamageTarget$ Equipped | Description$ All damage that would be dealt to you is dealt to equipped creature instead.
SVar:DmgEquipped:DB$ ReplaceEffect | VarName$ Affected | VarValue$ Equipped | VarType$ Card
Oracle:All damage that would be dealt to you is dealt to equipped creature instead.\nEquip {3}

View File

@@ -3,7 +3,6 @@ ManaCost:4 W W W
Types:Legendary Creature Angel
PT:7/7
S:Mode$ AlternativeCost | ValidSA$ Spell.Self | EffectZone$ All | Cost$ W tapXType<4/Creature.withFlying> | Description$ You may pay {W} and tap four untapped creatures you control with flying rather than pay this spell's mana cost.
SVar:AIPaymentPreference:DontPayTapCostWithManaSources
K:Flying
K:Lifelink
S:Mode$ Continuous | Affected$ Creature.withFlying+Other+YouCtrl | AddKeyword$ Indestructible | Description$ Other creatures you control with flying have indestructible.

View File

@@ -3,7 +3,7 @@ ManaCost:U U U
Types:Legendary Enchantment Creature Nightmare
PT:10/1
S:Mode$ CantBlockBy | ValidAttacker$ Creature.Self | Description$ CARDNAME can't be blocked.
R:Event$ DamageDone | ActiveZones$ Battlefield | ValidSource$ Card.YouCtrl,Emblem.YouCtrl | ValidTarget$ Opponent | ReplaceWith$ Mill | PreventionEffect$ True | ExecuteMode$ PerTarget | Description$ If a source you control would deal damage to an opponent, prevent that damage and each opponent mills that many cards.
R:Event$ DamageDone | ActiveZones$ Battlefield | ValidSource$ Card.YouCtrl,Emblem.YouCtrl | ValidTarget$ Opponent | ReplaceWith$ Mill | PreventionEffect$ True | ExecuteMode$ PerSource | Description$ If a source you control would deal damage to an opponent, prevent that damage and each opponent mills that many cards.
SVar:Mill:DB$ Mill | Defined$ Opponent | NumCards$ X
SVar:X:ReplaceCount$DamageAmount
Oracle:The Mindskinner can't be blocked.\nIf a source you control would deal damage to an opponent, prevent that damage and each opponent mills that many cards.

View File

@@ -3,6 +3,5 @@ ManaCost:2 W
Types:Creature Human Soldier
PT:4/4
A:SP$ PermanentCreature | Cost$ 2 W tapXType<2/Artifact;Creature;Land/artifacts, creatures, and/or lands> | SpellDescription$ As an additional cost to cast this spell, tap two untapped artifacts, creatures, and/or lands you control.
SVar:AIPaymentPreference:DontPayTapCostWithManaSources
DeckHints:Type$Artifact
Oracle:As an additional cost to cast this spell, tap two untapped artifacts, creatures, and/or lands you control.

View File

@@ -4,6 +4,4 @@ Types:Legendary Creature Djinn
PT:5/6
K:Flying
S:Mode$ AlternativeCost | ValidSA$ Spell.Self | EffectZone$ All | Cost$ 3 U tapXType<1/Artifact> | Description$ You may pay {3}{U} and tap an untapped artifact you control rather than pay this spell's mana cost.
# TODO: Currently the AI may cheat without the following flag by tapping the same artifact for mana and for the tap cost, e.g. 2 Islands + Sol Ring. Remove this flag once the AI is smart enough not to do that.
SVar:AIPaymentPreference:DontPayTapCostWithManaSources
Oracle:You may pay {3}{U} and tap an untapped artifact you control rather than pay this spell's mana cost.\nFlying

View File

@@ -443,11 +443,7 @@ public abstract class GameLobby implements IHasGameType {
Deck deck = slot.getDeck();
RegisteredPlayer rp = new RegisteredPlayer(deck);
if (variantTypes.isEmpty()) {
rp.setTeamNumber(team);
players.add(rp.setPlayer(lobbyPlayer));
}
else {
if (!variantTypes.isEmpty()) {
if (isCommanderMatch) {
final GameType commanderGameType =
isOathbreakerMatch ? GameType.Oathbreaker :
@@ -481,7 +477,6 @@ public abstract class GameLobby implements IHasGameType {
Iterable<PaperCard> schemes = null;
Iterable<PaperCard> planes = null;
//Archenemy
if (variantTypes.contains(GameType.ArchenemyRumble)
|| (variantTypes.contains(GameType.Archenemy) && isArchenemy)) {
final CardPool schemePool = deck.get(DeckSection.Schemes);
@@ -495,7 +490,6 @@ public abstract class GameLobby implements IHasGameType {
schemes = schemePool == null ? Collections.emptyList() : schemePool.toFlatList();
}
//Planechase
if (variantTypes.contains(GameType.Planechase)) {
final CardPool planePool = deck.get(DeckSection.Planes);
if (checkLegality) {
@@ -508,7 +502,6 @@ public abstract class GameLobby implements IHasGameType {
planes = planePool == null ? Collections.emptyList() : planePool.toFlatList();
}
//Vanguard
if (variantTypes.contains(GameType.Vanguard)) {
if (avatarPool == null || avatarPool.countAll() == 0) { //ERROR! null if avatar deselected on list
SOptionPane.showMessageDialog(Localizer.getInstance().getMessage("lblNoSelectedVanguardAvatarForPlayer", name));
@@ -517,9 +510,10 @@ public abstract class GameLobby implements IHasGameType {
}
rp = RegisteredPlayer.forVariants(activeSlots.size(), variantTypes, deck, schemes, isArchenemy, planes, avatarPool);
}
rp.setTeamNumber(team);
players.add(rp.setPlayer(lobbyPlayer));
}
if (!isAI) {
guis.put(rp, gui);

View File

@@ -56,7 +56,8 @@ public class InputQueue extends Observable {
if (topMostInput != inp) {
System.out.println("Cannot remove input " + inp.getClass().getSimpleName() + " because it's not on top of stack. Stack = " + inputStack );
} else {
} else if (topMostInput != null) {
// if topMostInput is null then it means the inputstack is already empty, why this is called twice?
inputStack.pop();
}
updateObservers();

View File

@@ -119,9 +119,9 @@ public enum ItemManagerConfig {
null, null, 3, 0),
NET_ARCHIVE_BLOCK_DECKS(SColumnUtil.getDecksDefaultColumns(false, false), false, false, false,
null, null, 3, 0),
ADVENTURE_EDITOR_POOL(SColumnUtil.getConquestCollectionDefaultColumns(), false, false, false,
ADVENTURE_EDITOR_POOL(SColumnUtil.getAdventureCollectionDefaultColumns(), false, false, false,
null, null, 6, 0),
ADVENTURE_STORE_POOL(SColumnUtil.getConquestCollectionDefaultColumns(), false, false, true,
ADVENTURE_STORE_POOL(SColumnUtil.getAdventureCollectionDefaultColumns(), false, false, true,
null, null, 6, 0),
ADVENTURE_SIDEBOARD(SColumnUtil.getDeckEditorDefaultColumns(), false, false, true,
null, null, 6, 0),

View File

@@ -143,6 +143,13 @@ public final class SColumnUtil {
return columns;
}
public static Map<ColumnDef, ItemColumnConfig> getAdventureCollectionDefaultColumns() {
Map<ColumnDef, ItemColumnConfig> columns = getCardColumns(ColumnDef.QUANTITY, false, false, false, true, false);
columns.get(ColumnDef.NEW).setSortPriority(1);
columns.get(ColumnDef.NAME).setSortPriority(2);
return columns;
}
public static Map<ColumnDef, ItemColumnConfig> getAttractionPoolDefaultColumns() {
//Similar to special card pool, but show the collector number and hide the type.
List<ColumnDef> colDefs = new ArrayList<>();

View File

@@ -228,6 +228,11 @@ public abstract class ImageFetcher {
}
if (filename.equalsIgnoreCase("null.jpg"))
return;
if (ImageKeys.missingCards.contains(filename))
return;
ImageKeys.missingCards.add(filename);
System.err.println("No specified file for '" + filename + "'.. Attempting to download from default Url");
tokenUrl = String.format("%s%s", ForgeConstants.URL_TOKEN_DOWNLOAD, filename);
}