Merge remote-tracking branch 'upstream/master' into LOTR28

This commit is contained in:
Simisays
2023-07-11 20:03:58 +02:00
133 changed files with 3356 additions and 395 deletions

View File

@@ -35,14 +35,19 @@ public class AIDeckStatistics {
this.numLands = numLands;
}
public static AIDeckStatistics fromCardList(List<CardRules> cards) {
public static AIDeckStatistics fromCards(List<Card> cards) {
int totalCMC = 0;
int totalCount = 0;
int numLands = 0;
int maxCost = 0;
int[] maxPips = new int[6];
int maxColoredCost = 0;
for (CardRules rules : cards) {
for (Card c : cards) {
CardRules rules = c.getRules();
if (rules == null) {
System.err.println(c + " CardRules is null" + (c.isToken() ? "/token" : "."));
continue;
}
CardType type = rules.getType();
if (type.isLand()) {
numLands += 1;
@@ -80,15 +85,15 @@ public class AIDeckStatistics {
}
public static AIDeckStatistics fromDeck(Deck deck) {
List<CardRules> rules_list = new ArrayList<>();
public static AIDeckStatistics fromDeck(Deck deck, Player player) {
List<Card> cardlist = new ArrayList<>();
for (final Map.Entry<DeckSection, CardPool> deckEntry : deck) {
switch (deckEntry.getKey()) {
case Main:
case Commander:
for (final Map.Entry<PaperCard, Integer> poolEntry : deckEntry.getValue()) {
CardRules rules = poolEntry.getKey().getRules();
rules_list.add(rules);
Card card = Card.fromPaperCard(poolEntry.getKey(), player);
cardlist.add(card);
}
break;
default:
@@ -96,25 +101,25 @@ public class AIDeckStatistics {
}
}
return fromCardList(rules_list);
return fromCards(cardlist);
}
public static AIDeckStatistics fromPlayer(Player player) {
Deck deck = player.getRegisteredPlayer().getDeck();
if (deck.isEmpty()) {
// we're in a test or some weird match, search through the hand and library and build the decklist
List<CardRules> rules_list = new ArrayList<>();
List<Card> cardlist = new ArrayList<>();
for (Card c : player.getAllCards()) {
if (c.getPaperCard() == null) {
continue;
}
rules_list.add(c.getRules());
cardlist.add(c);
}
return fromCardList(rules_list);
return fromCards(cardlist);
}
return fromDeck(deck);
return fromDeck(deck, player);
}

View File

@@ -277,7 +277,7 @@ public class AiBlockController {
if (mode == TriggerType.DamageDone) {
if (trigger.matchesValidParam("ValidSource", attacker)
&& attacker.getNetCombatDamage() > 0
&& !"False".equals(trigger.getParam("CombatDamage")) && attacker.getNetCombatDamage() > 0
&& trigger.matchesValidParam("ValidTarget", combat.getDefenderByAttacker(attacker))) {
value += 50;
}

View File

@@ -807,29 +807,26 @@ public class AiController {
// Account for possible Ward after the spell is fully targeted
// TODO: ideally, this should be done while targeting, so that a different target can be preferred if the best
// one is warded and can't be paid for. (currently it will be stuck with the target until it could pay)
if (sa.usesTargeting() && (!sa.isSpell() || CardFactoryUtil.isCounterable(host))) {
for (Card tgt : sa.getTargets().getTargetCards()) {
// TODO some older cards don't use the keyword, so check for trigger instead
if (tgt.hasKeyword(Keyword.WARD) && tgt.isInPlay() && tgt.getController().isOpponentOf(host.getController())) {
int amount = 0;
Cost wardCost = ComputerUtilCard.getTotalWardCost(tgt);
if (wardCost.hasManaCost()) {
amount = wardCost.getTotalMana().getCMC();
if (amount > 0 && !ComputerUtilCost.canPayCost(sa, player, true)) {
return AiPlayDecision.CantAfford;
if (!sa.isSpell() || CardFactoryUtil.isCounterable(host)) {
for (TargetChoices tc : sa.getAllTargetChoices()) {
for (Card tgt : tc.getTargetCards()) {
// TODO some older cards don't use the keyword, so check for trigger instead
if (tgt.hasKeyword(Keyword.WARD) && tgt.isInPlay() && tgt.getController().isOpponentOf(host.getController())) {
Cost wardCost = ComputerUtilCard.getTotalWardCost(tgt);
if (wardCost.hasManaCost()) {
xCost = wardCost.getTotalMana().getCMC() > 0;
}
SpellAbilityAi topAI = new SpellAbilityAi() {};
if (!topAI.willPayCosts(player, sa, wardCost, host)) {
return AiPlayDecision.CostNotAcceptable;
}
}
SpellAbilityAi topAI = new SpellAbilityAi() {
};
if (!topAI.willPayCosts(player, sa, wardCost, host)) {
return AiPlayDecision.CostNotAcceptable;
}
}
}
}
// check if some target raised cost
if (oldCMC > -1) {
if (!xCost && oldCMC > -1) {
int finalCMC = CostAdjustment.adjust(sa.getPayCosts(), sa).getTotalMana().getCMC();
if (finalCMC > oldCMC) {
xCost = true;
@@ -1332,10 +1329,7 @@ public class AiController {
if (sa == null) {
return null;
}
final List<SpellAbility> abilities = Lists.newArrayList();
abilities.add(sa);
return abilities;
return Lists.newArrayList(sa);
}
public List<SpellAbility> chooseSpellAbilityToPlay() {

View File

@@ -1720,14 +1720,10 @@ public class ComputerUtil {
return threatened;
}
} else {
objects = topStack.getTargets();
final List<GameObject> canBeTargeted = new ArrayList<>();
for (Object o : objects) {
if (o instanceof GameEntity) {
final GameEntity ge = (GameEntity) o;
if (ge.canBeTargetedBy(topStack)) {
canBeTargeted.add(ge);
}
for (GameEntity ge : topStack.getTargets().getTargetEntities()) {
if (ge.canBeTargetedBy(topStack)) {
canBeTargeted.add(ge);
}
}
if (canBeTargeted.isEmpty()) {

View File

@@ -1865,7 +1865,6 @@ public class ComputerUtilCard {
*/
public static boolean canPumpAgainstRemoval(Player ai, SpellAbility sa) {
final List<GameObject> objects = ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), sa, true);
final CardCollection threatenedTargets = new CardCollection();
if (!sa.usesTargeting()) {
final List<Card> cards = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
@@ -1877,14 +1876,11 @@ public class ComputerUtilCard {
// For pumps without targeting restrictions, just return immediately until this is fleshed out.
return false;
}
CardCollection targetables = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
targetables = ComputerUtil.getSafeTargets(ai, sa, targetables);
for (final Card c : targetables) {
if (objects.contains(c)) {
threatenedTargets.add(c);
}
}
CardCollection threatenedTargets = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
threatenedTargets = ComputerUtil.getSafeTargets(ai, sa, threatenedTargets);
threatenedTargets.retainAll(objects);
if (!threatenedTargets.isEmpty()) {
sortByEvaluateCreature(threatenedTargets);
for (Card c : threatenedTargets) {

View File

@@ -673,7 +673,6 @@ public class ComputerUtilCombat {
&& !blocker.hasKeyword(Keyword.INDESTRUCTIBLE)) {
return 0;
}
} // flanking
final int defBushidoMagnitude = blocker.getKeywordMagnitude(Keyword.BUSHIDO);
@@ -859,7 +858,7 @@ public class ComputerUtilCombat {
}
} else if (mode == TriggerType.DamageDone) {
willTrigger = true;
if (trigger.hasParam("ValidSource")) {
if (trigger.hasParam("ValidSource") && !"False".equals(trigger.getParam("CombatDamage"))) {
if (!(trigger.matchesValidParam("ValidSource", defender)
&& defender.getNetCombatDamage() > 0
&& trigger.matchesValidParam("ValidTarget", attacker))) {
@@ -2461,7 +2460,7 @@ public class ComputerUtilCombat {
CardCollectionView trigCards = attacker.getController().getCardsIn(ZoneType.Battlefield);
for (Card c : trigCards) {
for (Trigger t : c.getTriggers()) {
if (t.getMode() == TriggerType.DamageDone && "True".equals(t.getParam("CombatDamage")) && t.matchesValidParam("ValidSource", attacker)) {
if (t.getMode() == TriggerType.DamageDone && !"False".equals(t.getParam("CombatDamage")) && t.matchesValidParam("ValidSource", attacker)) {
SpellAbility ab = t.getOverridingAbility();
if (ab.getApi() == ApiType.Poison && "TriggeredTarget".equals(ab.getParam("Defined"))) {
pd += AbilityUtils.calculateAmount(attacker, ab.getParam("Num"), ab);

View File

@@ -22,6 +22,7 @@ import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.Spell;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetChoices;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import forge.util.TextUtil;
@@ -556,12 +557,14 @@ public class ComputerUtilCost {
}
// Ward - will be accounted for when rechecking a targeted ability
if (!sa.isTrigger() && sa.usesTargeting() && (!sa.isSpell() || !cannotBeCountered)) {
for (Card tgt : sa.getTargets().getTargetCards()) {
if (tgt.hasKeyword(Keyword.WARD) && tgt.isInPlay() && tgt.getController().isOpponentOf(sa.getHostCard().getController())) {
Cost wardCost = ComputerUtilCard.getTotalWardCost(tgt);
if (wardCost.hasManaCost()) {
extraManaNeeded += wardCost.getTotalMana().getCMC();
if (!sa.isTrigger() && (!sa.isSpell() || !cannotBeCountered)) {
for (TargetChoices tc : sa.getAllTargetChoices()) {
for (Card tgt : tc.getTargetCards()) {
if (tgt.hasKeyword(Keyword.WARD) && tgt.isInPlay() && tgt.getController().isOpponentOf(sa.getHostCard().getController())) {
Cost wardCost = ComputerUtilCard.getTotalWardCost(tgt);
if (wardCost.hasManaCost()) {
extraManaNeeded += wardCost.getTotalMana().getCMC();
}
}
}
}

View File

@@ -22,6 +22,7 @@ import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.ai.ability.AnimateAi;
import forge.ai.ability.FightAi;
import forge.card.ColorSet;
import forge.card.MagicColor;
import forge.card.mana.ManaCost;
@@ -79,6 +80,49 @@ import java.util.List;
*/
public class SpecialCardAi {
// Arena and Magus of the Arena
public static class Arena {
public static boolean consider(final Player ai, final SpellAbility sa) {
final Game game = ai.getGame();
if (!game.getPhaseHandler().is(PhaseType.END_OF_TURN) || game.getPhaseHandler().getNextTurn() != ai) {
return false; // at opponent's EOT only, to conserve mana
}
CardCollection aiCreatures = ai.getCreaturesInPlay();
if (aiCreatures.isEmpty()) {
return false;
}
for (Player opp : ai.getOpponents()) {
CardCollection oppCreatures = opp.getCreaturesInPlay();
if (oppCreatures.isEmpty()) {
continue;
}
for (Card aiCreature : aiCreatures) {
boolean canKillAll = true;
for (Card oppCreature : oppCreatures) {
if (FightAi.canKill(oppCreature, aiCreature, 0)) {
canKillAll = false;
break;
}
if (!FightAi.canKill(aiCreature, oppCreature, 0)) {
canKillAll = false;
break;
}
}
if (canKillAll) {
sa.getTargets().clear();
sa.getTargets().add(aiCreature);
return true;
}
}
}
return sa.isTargetNumberValid();
}
}
// Black Lotus and Lotus Bloom
public static class BlackLotus {
public static boolean consider(final Player ai, final SpellAbility sa, final ManaCostBeingPaid cost) {

View File

@@ -86,7 +86,7 @@ public class AnimateAi extends SpellAbilityAi {
}
}
// Don't use instant speed animate abilities before AI's COMBAT_BEGIN
if (!ph.is(PhaseType.COMBAT_BEGIN) && ph.isPlayerTurn(ai) && !SpellAbilityAi.isSorcerySpeed(sa, ai)
if (!ph.is(PhaseType.COMBAT_BEGIN) && ph.isPlayerTurn(ai) && !isSorcerySpeed(sa, ai)
&& !sa.hasParam("ActivationPhases") && !"Permanent".equals(sa.getParam("Duration"))) {
return false;
}
@@ -174,7 +174,7 @@ public class AnimateAi extends SpellAbilityAi {
}
}
if (!SpellAbilityAi.isSorcerySpeed(sa, aiPlayer) && !"Permanent".equals(sa.getParam("Duration"))) {
if (!isSorcerySpeed(sa, aiPlayer) && !"Permanent".equals(sa.getParam("Duration"))) {
if (sa.hasParam("Crew") && c.isCreature()) {
// Do not try to crew a vehicle which is already a creature
return false;

View File

@@ -1069,7 +1069,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
if (!immediately && (!game.getPhaseHandler().getNextTurn().equals(ai)
|| game.getPhaseHandler().getPhase().isBefore(PhaseType.END_OF_TURN))
&& !sa.hasParam("PlayerTurn") && !SpellAbilityAi.isSorcerySpeed(sa, ai)
&& !sa.hasParam("PlayerTurn") && !isSorcerySpeed(sa, ai)
&& !ComputerUtil.activateForCost(sa, ai)) {
return false;
}

View File

@@ -242,7 +242,7 @@ public class CloneAi extends SpellAbilityAi {
// don't use instant speed clone abilities outside computers
// Combat_Begin step
if (!ph.is(PhaseType.COMBAT_BEGIN)
&& ph.isPlayerTurn(ai) && !SpellAbilityAi.isSorcerySpeed(sa, ai)
&& ph.isPlayerTurn(ai) && !isSorcerySpeed(sa, ai)
&& !sa.hasParam("ActivationPhases") && sa.hasParam("Duration")) {
return false;
}

View File

@@ -123,7 +123,7 @@ public class CounterAi extends SpellAbilityAi {
if (toPay <= usableManaSources) {
// If this is a reusable Resource, feel free to play it most of the time
if (!SpellAbilityAi.playReusable(ai, sa)) {
if (!playReusable(ai, sa)) {
return false;
}
}
@@ -287,7 +287,7 @@ public class CounterAi extends SpellAbilityAi {
if (toPay <= usableManaSources) {
// If this is a reusable Resource, feel free to play it most
// of the time
if (!SpellAbilityAi.playReusable(ai,sa) || (MyRandom.getRandom().nextFloat() < .4)) {
if (!playReusable(ai,sa) || (MyRandom.getRandom().nextFloat() < .4)) {
return false;
}
}

View File

@@ -36,7 +36,7 @@ public class CountersMoveAi extends SpellAbilityAi {
}
}
if (!SpellAbilityAi.playReusable(ai, sa)) {
if (!playReusable(ai, sa)) {
return false;
}

View File

@@ -437,7 +437,7 @@ public class CountersPutAi extends CountersAi {
}
if (sa.usesTargeting()) {
if (!ai.getGame().getStack().isEmpty() && !SpellAbilityAi.isSorcerySpeed(sa, ai)) {
if (!ai.getGame().getStack().isEmpty() && !isSorcerySpeed(sa, ai)) {
// only evaluates case where all tokens are placed on a single target
if (sa.getMinTargets() < 2) {
if (ComputerUtilCard.canPumpAgainstRemoval(ai, sa)) {
@@ -550,7 +550,7 @@ public class CountersPutAi extends CountersAi {
if (sa.isCurse()) {
choice = chooseCursedTarget(list, type, amount, ai);
} else {
if (type.equals("P1P1") && !SpellAbilityAi.isSorcerySpeed(sa, ai)) {
if (type.equals("P1P1") && !isSorcerySpeed(sa, ai)) {
for (Card c : list) {
if (ComputerUtilCard.shouldPumpCard(ai, sa, c, amount, amount, Lists.newArrayList())) {
choice = c;
@@ -623,7 +623,7 @@ public class CountersPutAi extends CountersAi {
return false;
}
// Instant +1/+1
if (type.equals("P1P1") && !SpellAbilityAi.isSorcerySpeed(sa, ai)) {
if (type.equals("P1P1") && !isSorcerySpeed(sa, ai)) {
if (!(ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN) && abCost.isReusuableResource())) {
return false; // only if next turn and cost is reusable
}

View File

@@ -137,7 +137,7 @@ public class CountersPutAllAi extends SpellAbilityAi {
}
}
if (SpellAbilityAi.playReusable(ai, sa)) {
if (playReusable(ai, sa)) {
return chance;
}

View File

@@ -1,14 +1,11 @@
package forge.ai.ability;
import com.google.common.collect.Iterables;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCombat;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
import forge.game.card.Card;
import forge.game.card.CardCollectionView;
import forge.game.card.CardPredicates;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
@@ -46,11 +43,6 @@ public abstract class DamageAiBase extends SpellAbilityAi {
}
protected boolean shouldTgtP(final Player comp, final SpellAbility sa, final int d, final boolean noPrevention) {
// TODO: once the "planeswalker redirection" rule is removed completely, just remove this code and
// remove the "burn Planeswalkers" code in the called method.
return shouldTgtP(comp, sa, d, noPrevention, false);
}
protected boolean shouldTgtP(final Player comp, final SpellAbility sa, final int d, final boolean noPrevention, final boolean noPlaneswalkerRedirection) {
int restDamage = d;
final Game game = comp.getGame();
Player enemy = comp.getWeakestOpponent();
@@ -89,13 +81,6 @@ public abstract class DamageAiBase extends SpellAbilityAi {
}
}
// burn Planeswalkers
// TODO: Must be removed completely when the "planeswalker redirection" rule is removed.
if (!noPlaneswalkerRedirection
&& Iterables.any(enemy.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.PLANESWALKERS)) {
return true;
}
if (avoidTargetP(comp, sa)) {
return false;
}
@@ -131,7 +116,7 @@ public abstract class DamageAiBase extends SpellAbilityAi {
// chance to burn player based on current hand size
if (hand.size() > 2) {
float value = 0;
if (SpellAbilityAi.isSorcerySpeed(sa, comp)) {
if (isSorcerySpeed(sa, comp)) {
//lower chance for sorcery as other spells may be cast in main2
if (phase.isPlayerTurn(comp) && phase.is(PhaseType.MAIN2)) {
value = 1.0f * restDamage / enemy.getLife();

View File

@@ -1,28 +1,10 @@
package forge.ai.ability;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import forge.ai.AiController;
import forge.ai.AiProps;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat;
import forge.ai.ComputerUtilCost;
import forge.ai.ComputerUtilMana;
import forge.ai.PlayerControllerAi;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.ai.*;
import forge.card.MagicColor;
import forge.card.mana.ManaCost;
import forge.game.Game;
@@ -30,11 +12,7 @@ import forge.game.GameEntity;
import forge.game.GameObject;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CounterEnumType;
import forge.game.card.*;
import forge.game.cost.Cost;
import forge.game.cost.CostPartMana;
import forge.game.keyword.Keyword;
@@ -51,6 +29,12 @@ import forge.game.staticability.StaticAbilityMustTarget;
import forge.game.zone.ZoneType;
import forge.util.Aggregates;
import forge.util.MyRandom;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
public class DamageDealAi extends DamageAiBase {
@Override
@@ -630,7 +614,7 @@ public class DamageDealAi extends DamageAiBase {
if (tgt.canTgtPlaneswalker()) {
// We can damage planeswalkers with this, consider targeting.
Card c = dealDamageChooseTgtPW(ai, sa, dmg, noPrevention, enemy, false);
if (c != null && !shouldTgtP(ai, sa, dmg, noPrevention, true)) {
if (c != null && !shouldTgtP(ai, sa, dmg, noPrevention)) {
tcs.add(c);
if (divided) {
int assignedDamage = ComputerUtilCombat.getEnoughDamageToKill(c, dmg, source, false, noPrevention);
@@ -746,17 +730,17 @@ public class DamageDealAi extends DamageAiBase {
}
return false;
}
// TODO: Improve Damage, we shouldn't just target the player just because we can
if (sa.canTarget(enemy) && sa.canAddMoreTarget()) {
if (((phase.is(PhaseType.END_OF_TURN) && phase.getNextTurn().equals(ai))
|| (SpellAbilityAi.isSorcerySpeed(sa, ai) && phase.is(PhaseType.MAIN2))
|| ("PingAfterAttack".equals(logic) && phase.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS) && phase.isPlayerTurn(ai))
|| immediately || shouldTgtP(ai, sa, dmg, noPrevention)) &&
(!avoidTargetP(ai, sa))) {
tcs.add(enemy);
if (divided) {
sa.addDividedAllocation(enemy, dmg);
break;
if ((phase.is(PhaseType.END_OF_TURN) && phase.getNextTurn().equals(ai))
|| (isSorcerySpeed(sa, ai) && phase.is(PhaseType.MAIN2))
|| immediately) {
boolean pingAfterAttack = "PingAfterAttack".equals(logic) && phase.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS) && phase.isPlayerTurn(ai);
if ((pingAfterAttack && !avoidTargetP(ai, sa)) || shouldTgtP(ai, sa, dmg, noPrevention)) {
tcs.add(enemy);
if (divided) {
sa.addDividedAllocation(enemy, dmg);
break;
}
}
}
}
@@ -836,7 +820,7 @@ public class DamageDealAi extends DamageAiBase {
if (!positive && !(saMe instanceof AbilitySub)) {
return false;
}
if (!urgent && !SpellAbilityAi.playReusable(ai, saMe)) {
if (!urgent && !playReusable(ai, saMe)) {
return false;
}
return true;

View File

@@ -16,7 +16,7 @@ public class DayTimeAi extends SpellAbilityAi {
if ((sa.getHostCard().isCreature() && sa.getPayCosts().hasTapCost()) || sa.getPayCosts().hasManaCost()) {
// If it involves a cost that may put us at a disadvantage, better activate before own turn if possible
if (!SpellAbilityAi.isSorcerySpeed(sa, aiPlayer)) {
if (!isSorcerySpeed(sa, aiPlayer)) {
return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == aiPlayer;
} else {
return ph.is(PhaseType.MAIN2, aiPlayer); // Give other things a chance to be cast (e.g. Celestus)

View File

@@ -60,7 +60,7 @@ public class DebuffAi extends SpellAbilityAi {
|| !game.getStack().isEmpty()) {
// Instant-speed pumps should not be cast outside of combat when the
// stack is empty, unless there are specific activation phase requirements
if (!SpellAbilityAi.isSorcerySpeed(sa, ai) && !sa.hasParam("ActivationPhases")) {
if (!isSorcerySpeed(sa, ai) && !sa.hasParam("ActivationPhases")) {
return false;
}
}

View File

@@ -174,7 +174,7 @@ public class DestroyAi extends SpellAbilityAi {
if (CardLists.getNotType(list, "Creature").isEmpty()) {
list = ComputerUtilCard.prioritizeCreaturesWorthRemovingNow(ai, list, false);
}
if (!SpellAbilityAi.playReusable(ai, sa)) {
if (!playReusable(ai, sa)) {
list = CardLists.filter(list, Predicates.not(CardPredicates.hasCounter(CounterEnumType.SHIELD, 1)));
list = CardLists.filter(list, new Predicate<Card>() {

View File

@@ -101,13 +101,13 @@ public class DigAi extends SpellAbilityAi {
}
}
if (SpellAbilityAi.playReusable(ai, sa)) {
if (playReusable(ai, sa)) {
return true;
}
if ((!game.getPhaseHandler().getNextTurn().equals(ai)
|| game.getPhaseHandler().getPhase().isBefore(PhaseType.END_OF_TURN))
&& !sa.hasParam("PlayerTurn") && !SpellAbilityAi.isSorcerySpeed(sa, ai)
&& !sa.hasParam("PlayerTurn") && !isSorcerySpeed(sa, ai)
&& (ai.getCardsIn(ZoneType.Hand).size() > 1 || game.getPhaseHandler().getPhase().isBefore(PhaseType.DRAW))
&& !ComputerUtil.activateForCost(sa, ai)) {
return false;

View File

@@ -62,13 +62,13 @@ public class DigMultipleAi extends SpellAbilityAi {
return false;
}
if (SpellAbilityAi.playReusable(ai, sa)) {
if (playReusable(ai, sa)) {
return true;
}
if ((!game.getPhaseHandler().getNextTurn().equals(ai)
|| game.getPhaseHandler().getPhase().isBefore(PhaseType.END_OF_TURN))
&& !sa.hasParam("PlayerTurn") && !SpellAbilityAi.isSorcerySpeed(sa, ai)
&& !sa.hasParam("PlayerTurn") && !isSorcerySpeed(sa, ai)
&& (ai.getCardsIn(ZoneType.Hand).size() > 1 || game.getPhaseHandler().getPhase().isBefore(PhaseType.DRAW))
&& !ComputerUtil.activateForCost(sa, ai)) {
return false;

View File

@@ -23,7 +23,7 @@ public class DigUntilAi extends SpellAbilityAi {
Card source = sa.getHostCard();
final String logic = sa.getParamOrDefault("AILogic", "");
double chance = .4; // 40 percent chance with instant speed stuff
if (SpellAbilityAi.isSorcerySpeed(sa, ai)) {
if (isSorcerySpeed(sa, ai)) {
chance = .667; // 66.7% chance for sorcery speed (since it will
// never activate EOT)
}

View File

@@ -168,7 +168,7 @@ public class DrawAi extends SpellAbilityAi {
@Override
protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandler ph, String logic) {
if ((!ph.getNextTurn().equals(ai) || ph.getPhase().isBefore(PhaseType.END_OF_TURN))
&& !sa.hasParam("PlayerTurn") && !SpellAbilityAi.isSorcerySpeed(sa, ai)
&& !sa.hasParam("PlayerTurn") && !isSorcerySpeed(sa, ai)
&& ai.getCardsIn(ZoneType.Hand).size() > 1 && !ComputerUtil.activateForCost(sa, ai)
&& !"YawgmothsBargain".equals(logic)) {
return false;

View File

@@ -30,7 +30,6 @@ public class FightAi extends SpellAbilityAi {
// everything is defined or targeted above, can't do anything there unless a specific logic is set
if (sa.hasParam("Defined") && !sa.usesTargeting()) {
// TODO extend Logic for cards like Arena
return true;
}

View File

@@ -20,7 +20,7 @@ public class FlipOntoBattlefieldAi extends SpellAbilityAi {
PhaseHandler ph = sa.getHostCard().getGame().getPhaseHandler();
String logic = sa.getParamOrDefault("AILogic", "");
if (!SpellAbilityAi.isSorcerySpeed(sa, aiPlayer) && sa.getPayCosts().hasManaCost()) {
if (!isSorcerySpeed(sa, aiPlayer) && sa.getPayCosts().hasManaCost()) {
return ph.is(PhaseType.END_OF_TURN);
}

View File

@@ -111,7 +111,7 @@ public class LifeGainAi extends SpellAbilityAi {
return lifeCritical || activateForCost
|| (ph.getNextTurn().equals(ai) && !ph.getPhase().isBefore(PhaseType.END_OF_TURN))
|| sa.hasParam("PlayerTurn") || SpellAbilityAi.isSorcerySpeed(sa, ai);
|| sa.hasParam("PlayerTurn") || isSorcerySpeed(sa, ai);
}
/*
@@ -180,8 +180,8 @@ public class LifeGainAi extends SpellAbilityAi {
return true;
}
if (SpellAbilityAi.isSorcerySpeed(sa, ai)
|| sa.getSubAbility() != null || SpellAbilityAi.playReusable(ai, sa)) {
if (isSorcerySpeed(sa, ai)
|| sa.getSubAbility() != null || playReusable(ai, sa)) {
return true;
}

View File

@@ -144,7 +144,7 @@ public class LifeLoseAi extends SpellAbilityAi {
return false;
}
if (SpellAbilityAi.isSorcerySpeed(sa, ai) || sa.hasParam("ActivationPhases") || SpellAbilityAi.playReusable(ai, sa)
if (isSorcerySpeed(sa, ai) || sa.hasParam("ActivationPhases") || playReusable(ai, sa)
|| ComputerUtil.activateForCost(sa, ai)) {
return true;
}

View File

@@ -61,7 +61,7 @@ public class ManifestAi extends SpellAbilityAi {
if (!buff) {
return false;
}
} else if (!SpellAbilityAi.isSorcerySpeed(sa, ai)) {
} else if (!isSorcerySpeed(sa, ai)) {
return false;
}
} else {
@@ -152,7 +152,7 @@ public class ManifestAi extends SpellAbilityAi {
}
}
// Probably should be a little more discerning on playing during OPPs turn
if (SpellAbilityAi.playReusable(ai, sa)) {
if (playReusable(ai, sa)) {
return true;
}
if (game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_ATTACKERS)) {

View File

@@ -51,7 +51,7 @@ public class MillAi extends SpellAbilityAi {
return (ph.is(PhaseType.MAIN1) || ph.is(PhaseType.MAIN2)) && ph.isPlayerTurn(ai); // Chandra, Torch of Defiance and similar
}
if (!sa.isPwAbility()) { // Planeswalker abilities are only activated at sorcery speed
if ("You".equals(sa.getParam("Defined")) && !(!SpellAbilityAi.isSorcerySpeed(sa, ai) && ph.is(PhaseType.END_OF_TURN)
if ("You".equals(sa.getParam("Defined")) && !(!isSorcerySpeed(sa, ai) && ph.is(PhaseType.END_OF_TURN)
&& ph.getNextTurn().equals(ai))) {
return false; // only self-mill at opponent EOT
}

View File

@@ -1,21 +1,15 @@
package forge.ai.ability;
import org.apache.commons.lang3.StringUtils;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCost;
import forge.ai.ComputerUtilMana;
import forge.ai.SpellAbilityAi;
import forge.card.CardStateName;
import forge.card.CardType.Supertype;
import forge.card.mana.ManaCost;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.*;
import forge.game.cost.Cost;
import forge.game.keyword.KeywordInterface;
import forge.game.mana.ManaCostBeingPaid;
@@ -24,6 +18,7 @@ import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import org.apache.commons.lang3.StringUtils;
public class PermanentAi extends SpellAbilityAi {
@@ -49,19 +44,19 @@ public class PermanentAi extends SpellAbilityAi {
*/
@Override
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
final Card card = sa.getHostCard();
final Card source = sa.getHostCard();
// check on legendary
if (!card.ignoreLegendRule() && ai.isCardInPlay(card.getName())) {
if (!source.ignoreLegendRule() && ai.isCardInPlay(source.getName())) {
// TODO check the risk we'd lose the effect with bad timing
if (!card.hasSVar("AILegendaryException")) {
if (!source.hasSVar("AILegendaryException")) {
// AiPlayDecision.WouldDestroyLegend
return false;
} else {
String specialRule = card.getSVar("AILegendaryException");
String specialRule = source.getSVar("AILegendaryException");
if ("TwoCopiesAllowed".equals(specialRule)) {
// One extra copy allowed on the battlefield, e.g. Brothers Yamazaki
if (CardLists.count(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals(card.getName())) > 1) {
if (CardLists.count(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals(source.getName())) > 1) {
return false;
}
} else if ("AlwaysAllowed".equals(specialRule)) {
@@ -73,23 +68,7 @@ public class PermanentAi extends SpellAbilityAi {
}
}
/* -- not used anymore after Ixalan (Planeswalkers are now legendary, not unique by subtype) --
if (card.isPlaneswalker()) {
CardCollection list = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield),
CardPredicates.Presets.PLANESWALKERS);
for (String type : card.getType().getSubtypes()) { // determine
// planewalker
// subtype
final CardCollection cl = CardLists.getType(list, type);
if (!cl.isEmpty()) {
// AiPlayDecision.WouldDestroyOtherPlaneswalker
return false;
}
break;
}
}*/
if (card.getType().hasSupertype(Supertype.World)) {
if (source.getType().hasSupertype(Supertype.World)) {
CardCollection list = CardLists.getType(ai.getCardsIn(ZoneType.Battlefield), "World");
if (!list.isEmpty()) {
// AiPlayDecision.WouldDestroyWorldEnchantment
@@ -101,7 +80,6 @@ public class PermanentAi extends SpellAbilityAi {
if (mana.countX() > 0) {
// Set PayX here to maximum value.
final int xPay = ComputerUtilCost.getMaxXValue(sa, ai, false);
final Card source = sa.getHostCard();
if (source.hasConverge()) {
int nColors = ComputerUtilMana.getConvergeCount(sa, ai);
for (int i = 1; i <= xPay; i++) {
@@ -123,7 +101,7 @@ public class PermanentAi extends SpellAbilityAi {
}
} else if (mana.isZero()) {
// if mana is zero, but card mana cost does have X, then something is wrong
ManaCost cardCost = card.getManaCost();
ManaCost cardCost = source.getManaCost();
if (cardCost != null && cardCost.countX() > 0) {
// AiPlayDecision.CantPlayAi
return false;
@@ -143,6 +121,38 @@ public class PermanentAi extends SpellAbilityAi {
sa.setXManaCostPaid(xPay);
}
if ("ChaliceOfTheVoid".equals(source.getSVar("AICurseEffect"))) {
int maxX = sa.getXManaCostPaid(); // as set above
CardCollection otherChalices = CardLists.filter(ai.getGame().getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Chalice of the Void"));
outer: for (int i = 0; i <= maxX; i++) {
for (Card chalice : otherChalices) {
if (chalice.getCounters(CounterEnumType.CHARGE) == i) {
continue outer; // already disabled, no point in adding another one
}
}
// assume the AI knows the deck lists of its opponents and if we see a card in a certain zone except for the library or hand,
// it likely won't be cast unless it's bounced back (ideally, this should also somehow account for hidden information such as face down cards in exile)
final int manaValue = i;
CardCollection aiCards = CardLists.filter(ai.getAllCards(), card -> (card.isInZone(ZoneType.Library) || !card.isInZone(ZoneType.Hand))
&& card.getState(CardStateName.Original).getManaCost() != null
&& card.getState(CardStateName.Original).getManaCost().getCMC() == manaValue);
CardCollection oppCards = CardLists.filter(ai.getStrongestOpponent().getAllCards(), card -> (card.isInZone(ZoneType.Library) || !card.isInZone(ZoneType.Hand))
&& card.getState(CardStateName.Original).getManaCost() != null
&& card.getState(CardStateName.Original).getManaCost().getCMC() == manaValue);
if (manaValue == 0) {
aiCards = CardLists.filter(aiCards, Predicates.not(CardPredicates.isType("Land")));
oppCards = CardLists.filter(oppCards, Predicates.not(CardPredicates.isType("Land")));
// also filter out other Chalices in our own deck
aiCards = CardLists.filter(aiCards, Predicates.not(CardPredicates.nameEquals("Chalice of the Void")));
}
if (oppCards.size() > 3 && oppCards.size() >= aiCards.size() * 2) {
sa.setXManaCostPaid(manaValue);
return true;
}
}
return false;
}
if (sa.hasParam("Announce") && sa.getParam("Announce").startsWith("Multikicker")) {
// String announce = sa.getParam("Announce");
ManaCost mkCost = sa.getMultiKickerManaCost();
@@ -152,13 +162,13 @@ public class PermanentAi extends SpellAbilityAi {
mCost = ManaCost.combine(mCost, mkCost);
ManaCostBeingPaid mcbp = new ManaCostBeingPaid(mCost);
if (!ComputerUtilMana.canPayManaCost(mcbp, sa, ai, false)) {
card.setKickerMagnitude(i);
source.setKickerMagnitude(i);
sa.setSVar("Multikicker", String.valueOf(i));
break;
}
card.setKickerMagnitude(i + 1);
source.setKickerMagnitude(i + 1);
}
if (isZeroCost && card.getKickerMagnitude() == 0) {
if (isZeroCost && source.getKickerMagnitude() == 0) {
// Bail if the card cost was {0} and no multikicker was paid (e.g. Everflowing Chalice).
// TODO: update this if there's ever a card where it makes sense to play it for {0} with no multikicker
return false;
@@ -166,13 +176,13 @@ public class PermanentAi extends SpellAbilityAi {
}
// don't play cards without being able to pay the upkeep for
for (KeywordInterface inst : card.getKeywords()) {
for (KeywordInterface inst : source.getKeywords()) {
String ability = inst.getOriginal();
if (ability.startsWith("UpkeepCost")) {
final String[] k = ability.split(":");
final String costs = k[1];
final SpellAbility emptyAbility = new SpellAbility.EmptySa(card, ai);
final SpellAbility emptyAbility = new SpellAbility.EmptySa(source, ai);
emptyAbility.setPayCosts(new Cost(costs, true));
emptyAbility.setTargetRestrictions(sa.getTargetRestrictions());
emptyAbility.setCardState(sa.getCardState());
@@ -186,8 +196,8 @@ public class PermanentAi extends SpellAbilityAi {
}
// check for specific AI preferences
if (card.hasSVar("AICastPreference")) {
String pref = card.getSVar("AICastPreference");
if (source.hasSVar("AICastPreference")) {
String pref = source.getSVar("AICastPreference");
String[] groups = StringUtils.split(pref, "|");
boolean dontCast = false;
for (String group : groups) {
@@ -205,7 +215,7 @@ public class PermanentAi extends SpellAbilityAi {
// Only cast unless there are X or more cards like this on the battlefield under AI control already,
CardCollectionView valid = param.contains("Globally") ? ai.getGame().getCardsIn(ZoneType.Battlefield)
: ai.getCardsIn(ZoneType.Battlefield);
CardCollection ctrld = CardLists.filter(valid, CardPredicates.nameEquals(card.getName()));
CardCollection ctrld = CardLists.filter(valid, CardPredicates.nameEquals(source.getName()));
int numControlled = 0;
if (param.endsWith("WithoutOppAuras")) {
@@ -242,7 +252,7 @@ public class PermanentAi extends SpellAbilityAi {
// if there are X-1 mana sources in play but the AI has an extra land in hand
CardCollection m = ComputerUtilMana.getAvailableManaSources(ai, true);
int extraMana = CardLists.count(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS) > 0 ? 1 : 0;
if (card.getName().equals("Illusions of Grandeur")) {
if (source.getName().equals("Illusions of Grandeur")) {
// TODO: this is currently hardcoded for specific Illusions-Donate cost reduction spells, need to make this generic.
extraMana += Math.min(3, CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), Predicates.or(CardPredicates.nameEquals("Sapphire Medallion"), CardPredicates.nameEquals("Helm of Awakening"))).size()) * 2; // each cost-reduction spell accounts for {1} in both Illusions and Donate
}
@@ -260,7 +270,7 @@ public class PermanentAi extends SpellAbilityAi {
break; // disregard other preferences, always cast as a last resort
}
} else if (param.equals("OnlyFromZone")) {
if (!card.getZone().getZoneType().toString().equals(value)) {
if (!source.getZone().getZoneType().toString().equals(value)) {
dontCast = true;
break; // limit casting to a specific zone only
}

View File

@@ -235,11 +235,19 @@ public class PermanentCreatureAi extends PermanentAi {
* worth it. Not sure what 4. is for. 5. needs to be updated to ensure
* that the net toughness is still positive after static effects.
*/
// AiPlayDecision.WouldBecomeZeroToughnessCreature
if (card.hasStartOfKeyword("etbCounter") || mana.countX() != 0
|| card.hasETBTrigger(false) || card.hasETBReplacement() || card.hasSVar("NoZeroToughnessAI")) {
return true;
}
final Card copy = CardUtil.getLKICopy(card);
ComputerUtilCard.applyStaticContPT(game, copy, null);
// AiPlayDecision.WouldBecomeZeroToughnessCreature
return copy.getNetToughness() > 0 || copy.hasStartOfKeyword("etbCounter") || mana.countX() != 0
|| copy.hasETBTrigger(false) || copy.hasETBReplacement() || copy.hasSVar("NoZeroToughnessAI");
if (copy.getNetToughness() > 0) {
return true;
}
return false;
}
}

View File

@@ -163,7 +163,7 @@ public class ProtectAi extends SpellAbilityAi {
protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph) {
final boolean notAiMain1 = !(ph.getPlayerTurn() == ai && ph.getPhase() == PhaseType.MAIN1);
// sorceries can only give protection in order to create an unblockable attacker
return !SpellAbilityAi.isSorcerySpeed(sa, ai) || !notAiMain1;
return !isSorcerySpeed(sa, ai) || !notAiMain1;
}
@Override

View File

@@ -87,7 +87,7 @@ public class PumpAi extends PumpAiBase {
return true;
}
return SpellAbilityAi.isSorcerySpeed(sa, ai) || (ph.getNextTurn().equals(ai) && !ph.getPhase().isBefore(PhaseType.END_OF_TURN));
return isSorcerySpeed(sa, ai) || (ph.getNextTurn().equals(ai) && !ph.getPhase().isBefore(PhaseType.END_OF_TURN));
} else if (logic.equals("Aristocrat")) {
final boolean isThreatened = ComputerUtil.predictThreatenedObjects(ai, null, true).contains(sa.getHostCard());
if (!ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS) && !isThreatened) {
@@ -118,7 +118,7 @@ public class PumpAi extends PumpAiBase {
|| ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_BLOCKERS))) {
// Instant-speed pumps should not be cast outside of combat when the
// stack is empty
return sa.isCurse() || SpellAbilityAi.isSorcerySpeed(sa, ai) || main1Preferred;
return sa.isCurse() || isSorcerySpeed(sa, ai) || main1Preferred;
}
return true;
}
@@ -292,10 +292,6 @@ public class PumpAi extends PumpAiBase {
if (numDefense.contains("X") && sa.getSVar("X").equals("Count$xPaid")) {
// Set PayX here to maximum value.
int xPay = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
if (sourceName.equals("Necropolis Fiend")) {
xPay = Math.min(xPay, sa.getActivatingPlayer().getCardsIn(ZoneType.Graveyard).size());
sa.setSVar("X", Integer.toString(xPay));
}
sa.setXManaCostPaid(xPay);
defense = xPay;
if (numDefense.equals("-X")) {
@@ -371,7 +367,7 @@ public class PumpAi extends PumpAiBase {
if (ComputerUtilCard.shouldPumpCard(ai, sa, card, defense, attack, keywords, false)) {
return true;
} else if (containsUsefulKeyword(ai, keywords, card, sa, attack)) {
if (game.getPhaseHandler().isPreCombatMain() && SpellAbilityAi.isSorcerySpeed(sa, ai) ||
if (game.getPhaseHandler().isPreCombatMain() && isSorcerySpeed(sa, ai) ||
game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_ATTACKERS, ai) ||
game.getPhaseHandler().is(PhaseType.COMBAT_BEGIN, ai)) {
Card pumped = ComputerUtilCard.getPumpedCreature(ai, sa, card, 0, 0, keywords);

View File

@@ -4,7 +4,6 @@ import com.google.common.collect.Iterables;
import forge.ai.AiPlayDecision;
import forge.ai.PlayerControllerAi;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.cost.Cost;
@@ -26,7 +25,7 @@ public class RevealAi extends RevealAiBase {
boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(.667, sa.getActivationsThisTurn() + 1);
if (SpellAbilityAi.playReusable(ai, sa)) {
if (playReusable(ai, sa)) {
randomReturn = true;
}
return randomReturn;

View File

@@ -1,6 +1,5 @@
package forge.ai.ability;
import forge.ai.SpellAbilityAi;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.util.MyRandom;
@@ -20,7 +19,7 @@ public class RevealHandAi extends RevealAiBase {
boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(.667, sa.getActivationsThisTurn() + 1);
if (SpellAbilityAi.playReusable(ai, sa)) {
if (playReusable(ai, sa)) {
randomReturn = true;
}

View File

@@ -57,7 +57,7 @@ public class ScryAi extends SpellAbilityAi {
// even if there's no mana cost.
if (sa.getPayCosts().hasTapCost()
&& (sa.getPayCosts().hasManaCost() || (sa.getHostCard() != null && sa.getHostCard().isCreature()))
&& !SpellAbilityAi.isSorcerySpeed(sa, ai)) {
&& !isSorcerySpeed(sa, ai)) {
return ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN);
}
@@ -69,7 +69,7 @@ public class ScryAi extends SpellAbilityAi {
// in the playerturn Scry should only be done in Main1 or in upkeep if able
if (ph.isPlayerTurn(ai)) {
if (SpellAbilityAi.isSorcerySpeed(sa, ai)) {
if (isSorcerySpeed(sa, ai)) {
return ph.is(PhaseType.MAIN1) || sa.isPwAbility();
} else {
return ph.is(PhaseType.UPKEEP);
@@ -121,12 +121,12 @@ public class ScryAi extends SpellAbilityAi {
}
double chance = .4; // 40 percent chance of milling with instant speed stuff
if (SpellAbilityAi.isSorcerySpeed(sa, ai)) {
if (isSorcerySpeed(sa, ai)) {
chance = .667; // 66.7% chance for sorcery speed (since it will never activate EOT)
}
boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(chance, sa.getActivationsThisTurn() + 1);
if (SpellAbilityAi.playReusable(ai, sa)) {
if (playReusable(ai, sa)) {
randomReturn = true;
}

View File

@@ -54,13 +54,13 @@ public class SurveilAi extends SpellAbilityAi {
// even if there's no mana cost.
if (sa.getPayCosts().hasTapCost()
&& (sa.getPayCosts().hasManaCost() || (sa.getHostCard() != null && sa.getHostCard().isCreature()))
&& !SpellAbilityAi.isSorcerySpeed(sa, ai)) {
&& !isSorcerySpeed(sa, ai)) {
return ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN);
}
// in the player's turn Surveil should only be done in Main1 or in Upkeep if able
if (ph.isPlayerTurn(ai)) {
if (SpellAbilityAi.isSorcerySpeed(sa, ai)) {
if (isSorcerySpeed(sa, ai)) {
return ph.is(PhaseType.MAIN1) || sa.isPwAbility();
} else {
return ph.is(PhaseType.UPKEEP);
@@ -104,12 +104,12 @@ public class SurveilAi extends SpellAbilityAi {
}
double chance = .4; // 40 percent chance for instant speed
if (SpellAbilityAi.isSorcerySpeed(sa, ai)) {
if (isSorcerySpeed(sa, ai)) {
chance = .667; // 66.7% chance for sorcery speed (since it will never activate EOT)
}
boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(chance, sa.getActivationsThisTurn() + 1);
if (SpellAbilityAi.playReusable(ai, sa)) {
if (playReusable(ai, sa)) {
randomReturn = true;
}

View File

@@ -21,7 +21,7 @@ public class TapAi extends TapAiBase {
if (turn.isOpponentOf(ai) && phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
// Tap things down if it's Human's turn
} else if (turn.equals(ai)) {
if (SpellAbilityAi.isSorcerySpeed(sa, ai) && phase.getPhase().isBefore(PhaseType.COMBAT_BEGIN)) {
if (isSorcerySpeed(sa, ai) && phase.getPhase().isBefore(PhaseType.COMBAT_BEGIN)) {
// Cast it if it's a sorcery.
} else if (phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
// Aggro Brains are willing to use TapEffects aggressively instead of defensively
@@ -33,7 +33,7 @@ public class TapAi extends TapAiBase {
// Don't tap down after blockers
return false;
}
} else if (!SpellAbilityAi.playReusable(ai, sa)) {
} else if (!playReusable(ai, sa)) {
// Generally don't want to tap things with an Instant during Players turn outside of combat
return false;
}
@@ -46,8 +46,11 @@ public class TapAi extends TapAiBase {
final Card source = sa.getHostCard();
final Cost abCost = sa.getPayCosts();
if ("GoblinPolkaBand".equals(sa.getParam("AILogic"))) {
final String aiLogic = sa.getParamOrDefault("AILogic", "");
if ("GoblinPolkaBand".equals(aiLogic)) {
return SpecialCardAi.GoblinPolkaBand.consider(ai, sa);
} else if ("Arena".equals(aiLogic)) {
return SpecialCardAi.Arena.consider(ai, sa);
}
if (!ComputerUtilCost.checkDiscardCost(ai, abCost, source, sa)) {

View File

@@ -325,6 +325,15 @@ public abstract class TapAiBase extends SpellAbilityAi {
@Override
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
final Card source = sa.getHostCard();
final boolean oppTargetsChoice = sa.hasParam("TargetingPlayer");
if (oppTargetsChoice && sa.getActivatingPlayer().equals(ai) && !sa.isTrigger()) {
// canPlayAI (sa activated by ai)
Player targetingPlayer = AbilityUtils.getDefinedPlayers(source, sa.getParam("TargetingPlayer"), sa).get(0);
sa.setTargetingPlayer(targetingPlayer);
sa.getTargets().clear();
return targetingPlayer.getController().chooseTargetsFor(sa);
}
boolean randomReturn = true;

View File

@@ -133,7 +133,7 @@ public class TokenAi extends SpellAbilityAi {
}
}
if ((ph.isPlayerTurn(ai) || ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS))
&& !sa.hasParam("ActivationPhases") && !sa.hasParam("PlayerTurn") && !SpellAbilityAi.isSorcerySpeed(sa, ai)
&& !sa.hasParam("ActivationPhases") && !sa.hasParam("PlayerTurn") && !isSorcerySpeed(sa, ai)
&& !haste && !pwMinus) {
return false;
}

View File

@@ -20,7 +20,7 @@ public class VentureAi extends SpellAbilityAi {
// If this has a mana cost, do it at opponent's EOT if able to prevent spending mana early; if sorcery, do it in Main2
PhaseHandler ph = aiPlayer.getGame().getPhaseHandler();
if (sa.getPayCosts().hasManaCost() || sa.getPayCosts().hasTapCost()) {
if (SpellAbilityAi.isSorcerySpeed(sa, aiPlayer)) {
if (isSorcerySpeed(sa, aiPlayer)) {
return ph.is(PhaseType.MAIN2, aiPlayer);
} else {
return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == aiPlayer;

View File

@@ -1,5 +1,6 @@
package forge.util;
import java.text.DecimalFormat;
import java.text.Normalizer;
import java.util.ArrayList;
import java.util.List;
@@ -42,6 +43,10 @@ public class TextUtil {
return Normalizer.normalize(text, Normalizer.Form.NFD);
}
private static final DecimalFormat df = new DecimalFormat("#.##");
public static String decimalFormat(float value) {
return df.format(value);
}
/**
* Safely converts an object to a String.
*

View File

@@ -1582,10 +1582,6 @@ public class AbilityUtils {
host.addRemembered(sa.getTargets());
}
if (sa.hasParam("ImprintTargets") && sa.usesTargeting()) {
host.addImprintedCards(sa.getTargets().getTargetCards());
}
if (sa.hasParam("RememberCostMana")) {
host.clearRemembered();
ManaCostBeingPaid activationMana = new ManaCostBeingPaid(sa.getPayCosts().getTotalMana());

View File

@@ -1252,6 +1252,10 @@ public class ChangeZoneEffect extends SpellAbilityEffect {
player.shuffle(sa);
}
if (sa.hasParam("Reorder")) {
chosenCards = new CardCollection(decider.getController().orderMoveToZoneList(chosenCards, destination, sa));
}
// remove Controlled While Searching
if (controlTimestamp != null) {
player.removeController(controlTimestamp);

View File

@@ -62,10 +62,6 @@ public class FightEffect extends DamageBaseEffect {
return;
}
if (sa.hasParam("RememberObjects")) {
host.addRemembered(AbilityUtils.getDefinedObjects(host, sa.getParam("RememberObjects"), sa));
}
Player controller = host.getController();
boolean isOptional = sa.hasParam("Optional");
@@ -147,7 +143,6 @@ public class FightEffect extends DamageBaseEffect {
}
private void dealDamage(final SpellAbility sa, Card fighterA, Card fighterB) {
boolean usedDamageMap = true;
CardDamageMap damageMap = sa.getDamageMap();
CardDamageMap preventMap = sa.getPreventMap();

View File

@@ -6941,10 +6941,29 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
if (StringUtils.isNotBlank(set)) {
cp = StaticData.instance().getVariantCards().getCard(name, set);
return cp == null ? StaticData.instance().getCommonCards().getCard(name, set) : cp;
if (cp != null) {
return cp;
}
cp = StaticData.instance().getCommonCards().getCard(name, set);
if (cp != null) {
return cp;
}
}
//no specific set for variant
cp = StaticData.instance().getVariantCards().getCard(name);
return cp != null ? cp : StaticData.instance().getCommonCards().getCardFromEditions(name, CardArtPreference.LATEST_ART_ALL_EDITIONS);
if (cp != null) {
return cp;
}
//try to get from user preference if available
CardDb.CardArtPreference cardArtPreference = StaticData.instance().getCardArtPreference();
if (cardArtPreference == null) //fallback
cardArtPreference = CardArtPreference.ORIGINAL_ART_CORE_EXPANSIONS_REPRINT_ONLY;
cp = StaticData.instance().getCommonCards().getCardFromEditions(name, cardArtPreference);
if (cp != null) {
return cp;
}
//lastoption
return StaticData.instance().getCommonCards().getCard(name);
}
/**

View File

@@ -70,7 +70,11 @@ public class StaticAbilityDisableTriggers {
if (trigMode.equals(TriggerType.ChangesZone)) {
// Cause of the trigger the card changing zones
if (!stAb.matchesValidParam("ValidCause", runParams.get(AbilityKey.Card))) {
Card moved = (Card) runParams.get(AbilityKey.Card);
if ("Battlefield".equals(regtrig.getParam("Origin"))) {
moved = (Card) runParams.get(AbilityKey.CardLKI);
}
if (!stAb.matchesValidParam("ValidCause", moved)) {
return false;
}
if (!stAb.matchesValidParam("Destination", runParams.get(AbilityKey.Destination))) {

View File

@@ -89,7 +89,11 @@ public class StaticAbilityPanharmonicon {
if (trigMode.equals(TriggerType.ChangesZone)) {
// Cause of the trigger the card changing zones
if (!stAb.matchesValidParam("ValidCause", runParams.get(AbilityKey.Card))) {
Card moved = (Card) runParams.get(AbilityKey.Card);
if ("Battlefield".equals(trigger.getParam("Origin"))) {
moved = (Card) runParams.get(AbilityKey.CardLKI);
}
if (!stAb.matchesValidParam("ValidCause", moved)) {
return false;
}
if (!stAb.matchesValidParam("Origin", runParams.get(AbilityKey.Origin))) {

View File

@@ -120,19 +120,8 @@ public class TriggerChangesZone extends Trigger {
}
}
if (hasParam("ValidCause")) {
if (!runParams.containsKey(AbilityKey.Cause)) {
return false;
}
SpellAbility cause = (SpellAbility) runParams.get(AbilityKey.Cause);
if (cause == null) {
return false;
}
if (!matchesValid(cause, getParam("ValidCause").split(","))) {
if (!matchesValid(cause.getHostCard(), getParam("ValidCause").split(","))) {
return false;
}
}
if (!matchesValidParam("ValidCause", runParams.get(AbilityKey.Cause))) {
return false;
}
if (hasParam("Fizzle")) {

View File

@@ -25,7 +25,9 @@ public class TriggerChangesZoneAll extends Trigger {
if (!matchesValidParam("ValidCause", runParams.get(AbilityKey.Cause))) {
return false;
} else if (hasParam("ValidAmount")) {
}
if (hasParam("ValidAmount")) {
int right = AbilityUtils.calculateAmount(hostCard, getParam("ValidAmount").substring(2), this);
if (!Expressions.compare(this.filterCards(table).size(), getParam("ValidAmount").substring(0, 2), right)) { return false; }
}

View File

@@ -187,7 +187,9 @@ public class AdventureEventData implements Serializable {
List<CardBlock> legalBlocks = new ArrayList<>();
for (CardBlock b : src) { // for each block
boolean isOkay = !(b.getSets().isEmpty() && b.getCntBoostersDraft() > 0);
if (b.getSets().isEmpty() || (b.getCntBoostersDraft() < 1))
continue;
boolean isOkay = true;
for (CardEdition c : b.getSets()) {
if (!allEditions.contains(c)) {
isOkay = false;

View File

@@ -13,6 +13,7 @@ import forge.Forge;
import forge.adventure.data.DifficultyData;
import forge.adventure.data.HeroListData;
import forge.adventure.util.*;
import forge.adventure.stage.WorldStage;
import forge.adventure.world.WorldSave;
import forge.card.CardEdition;
import forge.card.ColorSet;
@@ -199,6 +200,7 @@ public class NewGameScene extends UIScene {
editionIds[starterEdition.getCurrentIndex()], 0);//maybe replace with enum
GamePlayerUtil.getGuiPlayer().setName(selectedName.getText());
SoundSystem.instance.changeBackgroundTrack();
WorldStage.getInstance().setupNewGame();
Forge.switchScene(GameScene.instance());
};
Forge.setTransitionScreen(new TransitionScreen(runnable, null, false, true, "Generating World..."));

View File

@@ -32,6 +32,7 @@ import forge.localinstance.achievements.CardActivationAchievements;
import forge.localinstance.achievements.PlaneswalkerAchievements;
import forge.model.FModel;
import forge.player.GamePlayerUtil;
import forge.util.TextUtil;
import org.apache.commons.lang3.tuple.Pair;
import java.util.Map;
@@ -216,7 +217,7 @@ public class PlayerStatisticScene extends UIScene {
totalLoss.setText(String.valueOf(Current.player().getStatistic().totalLoss()));
}
if (lossWinRatio != null) {
lossWinRatio.setText(Float.toString(Current.player().getStatistic().winLossRatio()));
lossWinRatio.setText(TextUtil.decimalFormat(Current.player().getStatistic().winLossRatio()));
}
if (eventMatchWins != null) {
eventMatchWins.setText(String.valueOf(Current.player().getStatistic().eventWins()));

View File

@@ -43,6 +43,7 @@ public class WorldStage extends GameStage implements SaveFileContent {
protected ArrayList<Pair<Float, EnemySprite>> enemies = new ArrayList<>();
private final static Float dieTimer = 20f;//todo config
private Float globalTimer = 0f;
private transient boolean newGame = false;
NavArrowActor navArrow;
public WorldStage() {
@@ -333,15 +334,24 @@ public class WorldStage extends GameStage implements SaveFileContent {
}
}
public void setupNewGame(){
newGame = true; //On a new game, we want to automatically enter any POI the player overlaps with.
}
@Override
public void enter() {
getPlayerSprite().LoadPos();
getPlayerSprite().setMovementDirection(Vector2.Zero);
for (Actor actor : foregroundSprites.getChildren()) {
if (actor.getClass() == PointOfInterestMapSprite.class) {
PointOfInterestMapSprite point = (PointOfInterestMapSprite) actor;
if (player.collideWith(point.getBoundingRect())) {
collidingPoint = point;
if (newGame) {
newGame = false;
}
else {
for (Actor actor : foregroundSprites.getChildren()) {
if (actor.getClass() == PointOfInterestMapSprite.class) {
PointOfInterestMapSprite point = (PointOfInterestMapSprite) actor;
if (player.collideWith(point.getBoundingRect())) {
collidingPoint = point;
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 490 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

View File

@@ -0,0 +1,7 @@
Name:Hallowed Sigil
ManaCost:no cost
Colors:white
Types:Enchantment
A:AB$ Pump | Cost$ PayShards<4> T | ValidTgts$ Creature.YouCtrl | KW$ Hexproof | SubAbility$ Eject | ActivationLimit$ 1 | SpellDescription$ Target creature you control gains hexproof until end of turn. Exile Hallowed Sigil.
SVar:Eject:DB$ ChangeZone | Defined$ Self | Origin$ Battlefield | Destination$ Exile
Oracle:{M},{T}:Target creature you control gains hexproof until end of turn. Exile Hallowed Sigil.

View File

@@ -0,0 +1,8 @@
Name:Sigil of Torment
ManaCost:no cost
Colors:black
Types:Enchantment
A:AB$ Destroy | Cost$ 4 B T PayShards<5> | ValidTgts$ Creature | TgtPrompt$ Select target creature | ActivationLimit$ 1 | SubAbility$ DBLifeGain | SpellDescription$ Destroy target creature. You gain 3 life. Exile Sigil of Torment.
SVar:DBLifeGain:DB$ GainLife | Defined$ You | LifeAmount$ 3 | SubAbility$ Eject
SVar:Eject:DB$ ChangeZone | Defined$ Self | Origin$ Battlefield | Destination$ Exile
Oracle:{M},{T}: Destroy target creature. You gain 3 life. Exile Sigil of Torment.

View File

@@ -0,0 +1,8 @@
Name:Power of Valyx
ManaCost:no cost
Colors:black
Types:Enchantment
K:Hexproof
A:AB$ Destroy | Cost$ 4 B T Sac<1/Creature.YouCtrl/creature you control>| ValidTgts$ Creature | TgtPrompt$ Select target creature | SubAbility$ DBLifeGain | SpellDescription$ Destroy target creature. You gain 3 life.
SVar:DBLifeGain:DB$ GainLife | Defined$ You | LifeAmount$ 3
Oracle:{M},{T}, Sacrifice a creature: Destroy target creature. You gain 3 life.

View File

@@ -0,0 +1,23 @@
[metadata]
Name=valyx
[Main]
3 Damnation|MM3|1
2 Deathrender|CNS|1
2 Deathrender|LRW|1
1 Doomed Dissenter|DBL|1
2 Doomed Dissenter|GN3|1
1 Ecstatic Awakener|DBL|1
2 Ecstatic Awakener|MID|1
3 Indulgent Tormentor|PM15|1
3 Lord of the Void|GTC|1
4 Mark of the Oni|BOK|1
3 Murder|CMR|1
1 Phyrexian Reclamation|C15|1
1 Phyrexian Reclamation|J22|1
2 Reaper from the Abyss|J22|1
3 Sign in Blood|SCD|1
1 Skirsdag High Priest|C14|1
2 Skirsdag High Priest|J22|1
19 Swamp|MOM|1
1 Swamp|MOM|3
4 Westvale Abbey|SOI|1

View File

@@ -0,0 +1,48 @@
[metadata]
Name=cultist
[Avatar]
[Main]
2 Bloodgift Demon|SCD|1
2 Bloodsoaked Champion|CLB|1
2 Bloodsoaked Champion|NCC|1
2 Demon of Catastrophes|J22|1
1 Doomed Dissenter|BBD|1
1 Doomed Dissenter|MB1|1
2 Ecstatic Awakener|DBL|1
2 Ecstatic Awakener|MID|1
2 Feaster of Fools|MH1|1
1 Grave Pact|10E|1
2 Grave Pact|CM2|1
1 Grave Pact|COM|1
2 Graven Archfiend|YSNC|1
1 Grim Haruspex|C19|1
1 Grim Haruspex|CLB|1
2 Harvester of Souls|CN2|1
2 Herald of Torment|BNG|1
1 Murder|CMR|1
2 Murder|SNC|1
2 Sign in Blood|ARC|1
1 Sign in Blood|STA|1
4 Skirsdag High Priest|2XM|1
11 Swamp|MOM|1
2 Swamp|SHM|1
2 Swamp|SHM|2
5 Swamp|SHM|3
2 Swamp|SHM|4
[Sideboard]
2 Culling Dais|2XM|1
2 Furnace Celebration|CMR|1
4 Glaring Spotlight|GTC|1
2 Grim Return|M14|1
1 Lord of the Void|GTC|1
2 Ravenous Demon|DKA|1
2 Reaper from the Abyss|C14|1
[Planes]
[Schemes]
[Conspiracy]
[Dungeon]

View File

@@ -0,0 +1,12 @@
{
"automappingRulesFile": "",
"commands": [
],
"compatibilityVersion": 1100,
"extensionsPath": "extensions",
"folders": [
"."
],
"propertyTypes": [
]
}

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.10" tiledversion="1.10.1" name="desertbuildingtiles" tilewidth="16" tileheight="16" tilecount="1036" columns="28">
<image source="../tileset/desertbuildingtiles.png" width="448" height="592"/>
</tileset>

View File

@@ -10,6 +10,7 @@
</properties>
<tileset firstgid="1" source="../tileset/main.tsx"/>
<tileset firstgid="10113" source="../tileset/buildings.tsx"/>
<tileset firstgid="11905" source="../tileset/main-nocollide.tsx"/>
<layer id="6" name="Collision" width="30" height="17">
<data encoding="base64" compression="zlib">
eJzFVNEOgyAMLG/yYXOGH102/Ssz4mcMszbrTsTCTHYJgYDcXYst0RuPjv6Cqz+HZ/U/pjEZ44hJ9w7fBvbi1NC8vf8e69mT78zerj1AzKgrvNqHK/AFYw51zLc0L6A7KM3gt/Gi75qYI99dFIdoiS/RxXhLb3WE3n9mzKdoO/aVy/Nezn7VlX08R20iu2arbonjDN09zpK3EqQmdb3X5rlFV///M6+nrl1Xeq61hjSk9rAfjexnz1drz8ohxy+cuC+9Bf1bIbWn443wXhfoZ0TbnlULa59BoLda1NR8C16fd0IQ
@@ -17,7 +18,7 @@
</layer>
<layer id="1" name="Background" width="30" height="17">
<data encoding="base64" compression="zlib">
eJzL4GFgyBjFo3gUj+JRPIpH8YjAALfm5xk=
eJw7Zs7AcGwUj+JRPIpH8SgexSMCAwA4PvgW
</data>
</layer>
<layer id="12" name="Shroud" width="30" height="17" opacity="0.15">

View File

@@ -31,11 +31,6 @@
</object>
<object id="48" template="../obj/treasure.tx" x="176.334" y="32.5833"/>
<object id="63" template="../obj/gold.tx" x="192.333" y="29.167"/>
<object id="65" template="../obj/enemy.tx" x="185.415" y="-58.8264">
<properties>
<property name="enemy" value="Hydra"/>
</properties>
</object>
<object id="66" template="../obj/enemy.tx" x="190.267" y="79.6539">
<properties>
<property name="enemy" value="Hidden Bush"/>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.10.1" orientation="orthogonal" renderorder="right-down" width="64" height="64" tilewidth="16" tileheight="16" infinite="0" nextlayerid="16" nextobjectid="47">
<map version="1.10" tiledversion="1.10.1" orientation="orthogonal" renderorder="right-down" width="64" height="64" tilewidth="16" tileheight="16" infinite="0" nextlayerid="16" nextobjectid="48">
<tileset firstgid="1" name="autotiles" tilewidth="16" tileheight="16" tilecount="432" columns="12">
<image source="../../world/tilesets/autotiles.png" width="192" height="576"/>
</tileset>
@@ -518,7 +518,82 @@
</tile>
</tileset>
<tileset firstgid="10581" source="../tileset/buildings.tsx"/>
<tileset firstgid="12373" source="../tileset/buildings.tsx"/>
<tileset firstgid="12373" source="../tileset/buildings-nocollide.tsx"/>
<objectgroup id="7" name="Invisible">
<object id="4" template="../obj/exit.tx" name="Hekma" x="8" y="0" width="1016" height="1" visible="0"/>
<object id="5" template="../obj/exit.tx" name="Hekma" x="1023" y="-1" width="1" height="1025" visible="0"/>
<object id="6" template="../obj/exit.tx" name="Hekma" x="0" y="1018" width="1022" height="6" visible="0"/>
<object id="7" template="../obj/exit.tx" name="Hekma" x="0" y="0" width="4" height="1016" visible="0"/>
<object id="25" template="../obj/shop.tx" x="721" y="80" width="14" height="14" visible="0">
<properties>
<property name="signXOffset" type="float" value="3"/>
<property name="signYOffset" type="float" value="8"/>
</properties>
</object>
<object id="26" template="../obj/shop.tx" x="721" y="192" width="14" height="14" visible="0">
<properties>
<property name="signXOffset" type="float" value="3"/>
<property name="signYOffset" type="float" value="8"/>
</properties>
</object>
<object id="27" template="../obj/shop.tx" x="673" y="144" width="14" height="14" visible="0">
<properties>
<property name="signXOffset" type="float" value="3"/>
<property name="signYOffset" type="float" value="8"/>
</properties>
</object>
<object id="33" template="../obj/shop.tx" x="641" y="96" width="14" height="14" visible="0">
<properties>
<property name="signXOffset" type="float" value="3"/>
<property name="signYOffset" type="float" value="8"/>
</properties>
</object>
<object id="28" template="../obj/shop.tx" x="753" y="63" width="14" height="13" visible="0">
<properties>
<property name="signXOffset" type="float" value="3"/>
<property name="signYOffset" type="float" value="8"/>
</properties>
</object>
<object id="31" template="../obj/shop.tx" x="833" y="48" width="14" height="15" visible="0">
<properties>
<property name="signXOffset" type="float" value="3"/>
<property name="signYOffset" type="float" value="8"/>
</properties>
</object>
<object id="29" template="../obj/shop.tx" x="769" y="128" width="14" height="14" visible="0">
<properties>
<property name="signXOffset" type="float" value="3"/>
<property name="signYOffset" type="float" value="8"/>
</properties>
</object>
<object id="30" template="../obj/inn.tx" x="721" y="128" width="14" height="16" visible="0"/>
<object id="36" template="../obj/portal.tx" x="709" y="333" visible="1">
<properties>
<property name="direction" value="left"/>
<property name="portalState" value="active"/>
<property name="teleport" value="../common/maps/map/naktamun.tmx"/>
<property name="teleportObjectId" value="37"/>
</properties>
</object>
<object id="37" template="../obj/portal.tx" x="653" y="349" visible="1">
<properties>
<property name="direction" value="right"/>
<property name="portalState" value="active"/>
<property name="teleport" value="../common/maps/map/naktamun.tmx"/>
<property name="teleportObjectId" value="36"/>
</properties>
</object>
<object id="38" template="../obj/waypoint.tx" x="150" y="396" visible="0"/>
<object id="8" template="../obj/entry_down.tx" x="592" y="32" width="16" height="16" visible="1"/>
<object id="47" template="../obj/portal.tx" name="Training Area" x="144" y="417" visible="1">
<properties>
<property name="direction" value="down"/>
<property name="portalState" value="active"/>
<property name="teleport" value="../common/maps/map/naktamun/gym.tmx"/>
<property name="teleportObjectId" value="1"/>
</properties>
</object>
</objectgroup>
<layer id="1" name="Ground" width="64" height="64" locked="1">
<data encoding="csv">
74,74,74,74,74,74,74,74,74,74,74,74,74,74,74,74,74,74,74,74,74,74,74,74,74,4657,32,4184,4184,4184,4184,4184,4184,4184,32,4657,74,74,74,74,74,74,74,74,74,74,74,74,74,74,74,74,74,74,74,74,74,74,74,74,74,74,74,74,
@@ -933,73 +1008,6 @@
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
</data>
</layer>
<objectgroup id="7" name="Invisible">
<object id="4" template="../obj/exit.tx" name="Hekma" x="8" y="0" width="1016" height="1" visible="0"/>
<object id="5" template="../obj/exit.tx" name="Hekma" x="1023" y="-1" width="1" height="1025" visible="0"/>
<object id="6" template="../obj/exit.tx" name="Hekma" x="0" y="1018" width="1022" height="6" visible="0"/>
<object id="7" template="../obj/exit.tx" name="Hekma" x="0" y="0" width="4" height="1016" visible="0"/>
<object id="25" template="../obj/shop.tx" x="721" y="80" width="14" height="14" visible="0">
<properties>
<property name="signXOffset" type="float" value="3"/>
<property name="signYOffset" type="float" value="8"/>
</properties>
</object>
<object id="26" template="../obj/shop.tx" x="721" y="192" width="14" height="14" visible="0">
<properties>
<property name="signXOffset" type="float" value="3"/>
<property name="signYOffset" type="float" value="8"/>
</properties>
</object>
<object id="27" template="../obj/shop.tx" x="673" y="144" width="14" height="14" visible="0">
<properties>
<property name="signXOffset" type="float" value="3"/>
<property name="signYOffset" type="float" value="8"/>
</properties>
</object>
<object id="33" template="../obj/shop.tx" x="641" y="96" width="14" height="14" visible="0">
<properties>
<property name="signXOffset" type="float" value="3"/>
<property name="signYOffset" type="float" value="8"/>
</properties>
</object>
<object id="28" template="../obj/shop.tx" x="753" y="63" width="14" height="13" visible="0">
<properties>
<property name="signXOffset" type="float" value="3"/>
<property name="signYOffset" type="float" value="8"/>
</properties>
</object>
<object id="31" template="../obj/shop.tx" x="833" y="48" width="14" height="15" visible="0">
<properties>
<property name="signXOffset" type="float" value="3"/>
<property name="signYOffset" type="float" value="8"/>
</properties>
</object>
<object id="29" template="../obj/shop.tx" x="769" y="128" width="14" height="14" visible="0">
<properties>
<property name="signXOffset" type="float" value="3"/>
<property name="signYOffset" type="float" value="8"/>
</properties>
</object>
<object id="30" template="../obj/inn.tx" x="721" y="128" width="14" height="16" visible="0"/>
<object id="36" template="../obj/portal.tx" x="709" y="333" visible="1">
<properties>
<property name="direction" value="left"/>
<property name="portalState" value="active"/>
<property name="teleport" value="../common/maps/map/naktamun.tmx"/>
<property name="teleportObjectId" value="37"/>
</properties>
</object>
<object id="37" template="../obj/portal.tx" x="653" y="349" visible="1">
<properties>
<property name="direction" value="right"/>
<property name="portalState" value="active"/>
<property name="teleport" value="../common/maps/map/naktamun.tmx"/>
<property name="teleportObjectId" value="36"/>
</properties>
</object>
<object id="38" template="../obj/waypoint.tx" x="150" y="396" visible="0"/>
<object id="8" template="../obj/entry_down.tx" x="592" y="32" width="16" height="16" visible="1"/>
</objectgroup>
<objectgroup id="15" name="Enemies">
<object id="42" template="../obj/enemy.tx" name="Khenra" x="137" y="396">
<properties>
@@ -1052,7 +1060,7 @@
]
}
]</property>
<property name="enemy" value="Master White Wizard"/>
<property name="enemy" value="Warrior"/>
</properties>
</object>
<object id="45" template="../obj/enemy.tx" name="Mummy" gid="2147495535" x="151" y="396" width="16" height="16">
@@ -1061,6 +1069,5 @@
<property name="waypoints" value="38"/>
</properties>
</object>
<object id="46" template="../obj/portal.tx" name="Training Area" x="144" y="416"/>
</objectgroup>
</map>

View File

@@ -1,9 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.10" tiledversion="1.10.1" orientation="orthogonal" renderorder="right-down" width="100" height="50" tilewidth="16" tileheight="16" infinite="0" nextlayerid="6" nextobjectid="20">
<tileset firstgid="1" source="autotiles.tsx"/>
<tileset firstgid="433" source="../tileset/main.tsx"/>
<tileset firstgid="10545" source="desertbuildingtiles.tsx"/>
<tileset firstgid="11581" source="../tileset/buildings.tsx"/>
<tileset firstgid="1" source="../autotiles.tsx"/>
<tileset firstgid="433" source="../../tileset/main.tsx"/>
<tileset firstgid="10545" source="../../tileset/desertbuildingtiles.tsx"/>
<tileset firstgid="11581" source="../../tileset/buildings.tsx"/>
<objectgroup id="3" name="Hidden Objects" locked="1">
<object id="1" template="../../obj/portal.tx" x="672" y="333" width="16" height="16" visible="1">
<properties>
<property name="direction" value="up"/>
<property name="portalState" value="active"/>
<property name="teleport" value="../common/maps/map/naktamun.tmx"/>
<property name="teleportObjectId" value="47"/>
</properties>
</object>
</objectgroup>
<layer id="1" name="Ground" width="100" height="50" locked="1">
<data encoding="csv">
2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,
@@ -58,14 +68,6 @@
2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452,2452
</data>
</layer>
<objectgroup id="3" name="Hidden Objects" locked="1">
<object id="1" template="../obj/portal.tx" x="671.5" y="351.5">
<properties>
<property name="direction" value="up"/>
</properties>
</object>
<object id="10" template="../obj/entry_up.tx" x="672" y="352"/>
</objectgroup>
<layer id="2" name="Buildings" width="100" height="50" locked="1">
<properties>
<property name="spriteLayer" type="bool" value="true"/>
@@ -178,37 +180,37 @@
</data>
</layer>
<objectgroup id="4" name="Enemies" locked="1">
<object id="12" template="../obj/enemy.tx" name="Falcon-headed Aven Initiate" x="592" y="320">
<object id="12" template="../../obj/enemy.tx" name="Falcon-headed Aven Initiate" x="592" y="320">
<properties>
<property name="enemy" value="Falcon-headed Aven Warrior"/>
</properties>
</object>
<object id="13" template="../obj/enemy.tx" name="Ibis-headed Aven Initiate" x="592" y="272">
<object id="13" template="../../obj/enemy.tx" name="Ibis-headed Aven Initiate" x="592" y="272">
<properties>
<property name="enemy" value="Ibis-headed Aven Warrior"/>
</properties>
</object>
<object id="14" template="../obj/enemy.tx" name="Minotaur Initiate" x="592" y="224">
<object id="14" template="../../obj/enemy.tx" name="Minotaur Initiate" x="592" y="224">
<properties>
<property name="enemy" value="Amonkhet Minotaur Warrior"/>
</properties>
</object>
<object id="15" template="../obj/enemy.tx" name="Khenra Initiate" x="752" y="224">
<object id="15" template="../../obj/enemy.tx" name="Khenra Initiate" x="752" y="224">
<properties>
<property name="enemy" value="Khenra Warrior"/>
</properties>
</object>
<object id="16" template="../obj/enemy.tx" name="Naga Initiate" x="752" y="272">
<object id="16" template="../../obj/enemy.tx" name="Naga Initiate" x="752" y="272">
<properties>
<property name="enemy" value="Naga Warrior"/>
</properties>
</object>
<object id="17" template="../obj/enemy.tx" name="Human Initiate" x="752" y="320">
<object id="17" template="../../obj/enemy.tx" name="Human Initiate" x="752" y="320">
<properties>
<property name="enemy" value="Warrior"/>
</properties>
</object>
<object id="18" template="../obj/enemy.tx" name="Anointed" x="672" y="224">
<object id="18" template="../../obj/enemy.tx" name="Anointed" x="672" y="224">
<properties>
<property name="enemy" value="Mummy"/>
</properties>

View File

@@ -0,0 +1,196 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.9" tiledversion="1.9.2" orientation="orthogonal" renderorder="right-down" width="30" height="22" tilewidth="16" tileheight="16" infinite="0" nextlayerid="9" nextobjectid="90">
<editorsettings>
<export target="wastetown..tmx" format="tmx"/>
</editorsettings>
<tileset firstgid="1" source="../tileset/main.tsx"/>
<tileset firstgid="10113" source="../tileset/buildings.tsx"/>
<layer id="6" name="Collision" width="30" height="22">
<data encoding="base64" compression="zlib">
eJzt1dEJgCAQBmAfDW6DaJxs1GiEapxoj4o8uEwl9ZSIfriX5Pz4oUhVQqiXjxnOu1xxuTnj6/tF12WU6kt7x74nId66TwNC1HA9x2cLs40mNWzuES6bmkcmOAfPzGcctmmGJMW27WG3Wfcb4d7Xtx/rltj/+5brO8i43V6mf0vUftI31XTZvnCZITa3SW2cTt/fZvwf2XzTjc0GSO1G/w==
</data>
</layer>
<layer id="1" name="Background" width="30" height="22">
<data encoding="base64" compression="zlib">
eJxjYBhZwI2TeEyJXkrwcLD3CTvxGARIUT+KR/EoHsWjeBSP4sGFARkE62E=
</data>
</layer>
<layer id="2" name="Ground" width="30" height="22">
<data encoding="base64" compression="zlib">
eJzt1M0NgzAMBWDfOCSZIHQI1imz0K7VH9img5AIWaRRRZ4NgqqqpXeAyHwxikL0/fWq3nOUu5cdncYfP++aulk8uSvprbMgPa0jetg5sdLns9vO5fVoPu3yP7sDtmRexERt1JWYiI24GrNkI+5JaXJ55bx/97fdIeRqdGYX+nqlWyttNj99V3JPSuwlU3pPonbJlLqpfTFTeA+dmd+VTI2b9/BZ94r9pxkBhlPcCw==
</data>
</layer>
<layer id="7" name="Ornamental Walls" width="30" height="22">
<data encoding="base64" compression="zlib">
eJzt1TEOgCAQBMCrkQ9YGZ+KPNVY+wUbL8ELGljW7ja5hkCmgA0ibTnn+vydmuGuu2j2ILJNz1GzXDsC31zj974l8uxWk2n3miwbMa2NuiNx1923c2gXtINI0B6ivR+xGWavzTStrZPvu0vmj2KaNsm4eeDdXx51O/I=
</data>
</layer>
<layer id="3" name="Clutter" width="30" height="22">
<properties>
<property name="spriteLayer" type="bool" value="true"/>
</properties>
<data encoding="base64" compression="zlib">
eJxjYBgFo2AU0AtIazEwyGihimlqMjBoadLWXnOgnRZo9roC7XSjsb3I4J8k/eyiF2hD4yvwQLAiD4Qvz4MQw6dvqAJi/TtUwWj8Du/4xQYKNPHzRwFuAABfLAtS
</data>
</layer>
<layer id="8" name="Overhang" width="30" height="22">
<data encoding="base64" compression="zlib">
eJxjYBgFo2BkAwWegXbBKBgFo2AUjIJRMLQBAEtjAC0=
</data>
</layer>
<objectgroup id="4" name="Objects">
<object id="38" template="../obj/entry_up.tx" x="208.333" y="350.665">
<properties>
<property name="teleport" value=""/>
</properties>
</object>
<object id="50" template="../obj/enemy.tx" x="208.402" y="144.809">
<properties>
<property name="enemy" value="High Cultist"/>
<property name="reward">[
{
&quot;type&quot;: &quot;item&quot;,
&quot;probability&quot;: 1,
&quot;count&quot;: 1,
&quot;itemName&quot;: &quot;Cultist's Key&quot;
}
]
</property>
<property name="threatRange" type="int" value="30"/>
</properties>
</object>
<object id="66" template="../obj/treasure.tx" x="224" y="95.5">
<properties>
<property name="reward">[{
&quot;type&quot;: &quot;randomCard&quot;,
&quot;count&quot;: 2,
&quot;colors&quot;: [ &quot;colorID&quot; ]
},{
&quot;type&quot;: &quot;randomCard&quot;,
&quot;count&quot;: 1,
&quot;probability&quot;: 0.5,
&quot;rarity&quot;: [ &quot;rare&quot; ],
&quot;colors&quot;: [ &quot;colorID&quot; ]
},{
&quot;type&quot;: &quot;randomCard&quot;,
&quot;count&quot;: 3,
&quot;addMaxCount&quot;: 2
}]</property>
</properties>
</object>
<object id="67" template="../obj/booster.tx" x="224.25" y="80.25">
<properties>
<property name="reward">[
{
&quot;editions&quot;: [ &quot;SOI&quot;, &quot;EMN&quot;, &quot;MID&quot;, &quot;VOW&quot; ],
&quot;type&quot;: &quot;card&quot;,
&quot;count&quot;: 10,
&quot;rarity&quot;: [ &quot;Common&quot; ]
},
{
&quot;editions&quot;: [ &quot;SOI&quot;, &quot;EMN&quot;, &quot;MID&quot;, &quot;VOW&quot; ],
&quot;type&quot;: &quot;card&quot;,
&quot;count&quot;: 3,
&quot;rarity&quot;: [ &quot;Uncommon&quot; ]
},
{
&quot;editions&quot;: [ &quot;SOI&quot;, &quot;EMN&quot;, &quot;MID&quot;, &quot;VOW&quot; ],
&quot;type&quot;: &quot;card&quot;,
&quot;count&quot;: 1,
&quot;rarity&quot;: [ &quot;Rare&quot;, &quot;Mythic Rare&quot; ]
}
]
</property>
</properties>
</object>
<object id="68" template="../obj/enemy.tx" x="172" y="205.5">
<properties>
<property name="enemy" value="False Monk"/>
<property name="threatRange" type="int" value="40"/>
</properties>
</object>
<object id="69" template="../obj/enemy.tx" x="174" y="236.5">
<properties>
<property name="enemy" value="False Monk"/>
<property name="threatRange" type="int" value="40"/>
</properties>
</object>
<object id="70" template="../obj/enemy.tx" x="170" y="270">
<properties>
<property name="enemy" value="False Monk"/>
<property name="threatRange" type="int" value="40"/>
</properties>
</object>
<object id="71" template="../obj/enemy.tx" x="243" y="270">
<properties>
<property name="enemy" value="False Monk"/>
<property name="threatRange" type="int" value="40"/>
</properties>
</object>
<object id="72" template="../obj/enemy.tx" x="243.5" y="235">
<properties>
<property name="enemy" value="False Monk"/>
<property name="threatRange" type="int" value="40"/>
</properties>
</object>
<object id="73" template="../obj/enemy.tx" x="243" y="204.5">
<properties>
<property name="enemy" value="False Monk"/>
<property name="threatRange" type="int" value="40"/>
</properties>
</object>
<object id="74" template="../obj/enemy.tx" x="120" y="182">
<properties>
<property name="enemy" value="False Knight"/>
<property name="threatRange" type="int" value="50"/>
<property name="waypoints" value="78,83,79,81,80,82,77,76"/>
</properties>
</object>
<object id="75" template="../obj/enemy.tx" x="300.5" y="186">
<properties>
<property name="enemy" value="False Knight"/>
<property name="threatRange" type="int" value="50"/>
<property name="waypoints" value="81,80,82,77,76,78,83,79"/>
</properties>
</object>
<object id="76" template="../obj/waypoint.tx" x="124" y="216"/>
<object id="77" template="../obj/waypoint.tx" x="121.5" y="267"/>
<object id="78" template="../obj/waypoint.tx" x="134.5" y="165.5"/>
<object id="79" template="../obj/waypoint.tx" x="281.5" y="171"/>
<object id="80" template="../obj/waypoint.tx" x="293" y="268"/>
<object id="81" template="../obj/waypoint.tx" x="301" y="215.5"/>
<object id="82" template="../obj/waypoint.tx" x="208" y="270"/>
<object id="83" template="../obj/waypoint.tx" x="209" y="175.5"/>
<object id="84" template="../obj/enemy.tx" x="207.5" y="224.5">
<properties>
<property name="enemy" value="False Knight"/>
<property name="threatRange" type="int" value="50"/>
<property name="waypoints" value="83,82"/>
</properties>
</object>
<object id="85" template="../obj/manashards.tx" x="98.5" y="224"/>
<object id="86" template="../obj/gate.tx" x="207.75" y="124.5" width="16.4999" height="22">
<properties>
<property name="dialog">[{
&quot;text&quot;:&quot;This door is locked&quot;,
&quot;options&quot;:[
{ &quot;name&quot;:&quot;Leave&quot; },
{
&quot;name&quot;:&quot;Unlock with Cultist's Key&quot;,
&quot;condition&quot;:[{&quot;item&quot;:&quot;Cultist's Key&quot;}],
&quot;text&quot;:&quot;The gate is unlocked.&quot;,
&quot;options&quot;:[{&quot;name&quot;:&quot;Continue.&quot;, &quot;action&quot;:[ {&quot;deleteMapObject&quot;:-1},{&quot;removeItem&quot;:&quot;Cultist's Key&quot;}]} ]
}
]
}]</property>
</properties>
</object>
<object id="87" template="../obj/manashards.tx" x="321.5" y="217.75"/>
<object id="88" template="../obj/manashards.tx" x="192.25" y="128.5"/>
<object id="89" template="../obj/entry_up.tx" gid="1073753167" x="192.087" y="64" rotation="0">
<properties>
<property name="teleport" value="../common/maps/map/unhallowed_abbey_2F.tmx"/>
</properties>
</object>
</objectgroup>
</map>

View File

@@ -0,0 +1,411 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.9" tiledversion="1.9.2" orientation="orthogonal" renderorder="right-down" width="30" height="19" tilewidth="16" tileheight="16" infinite="0" nextlayerid="8" nextobjectid="117">
<editorsettings>
<export target="wastetown..tmx" format="tmx"/>
</editorsettings>
<properties>
<property name="dungeonEffect" value=""/>
</properties>
<tileset firstgid="1" source="../tileset/main.tsx"/>
<tileset firstgid="10113" source="../tileset/buildings.tsx"/>
<tileset firstgid="11905" source="../tileset/DarkAbbeyTiles.tsx"/>
<layer id="6" name="Collision" width="30" height="19">
<data encoding="base64" compression="zlib">
eJxjYBi8wI2TMKaFnU8JmPuYynYTYye17SbFTmrZjctOdDOx2UGJ3SB903gxMUh8Nw9x6si1FxsAmYmPj6yfHLuxhScuc7DJDXV76aUfpG8lB3l6l3NQnpdgdhMbzpTaiW43MfZSy050u/EBatuJbDe+dEurOgnZHnz84WgvtcIWAIOHLCo=
</data>
</layer>
<layer id="1" name="Background" width="30" height="19">
<data encoding="base64" compression="zlib">
eJx7ws7A8GQI4UJ54jE1zCAVD4Sd+Oynt33UjD9y7B6u4QyzE91uevkRXYxe4YzN/8MtXvHZTUt34Ms/tPY/NrMHMv8OZN1ArDtM+BhQQBM3+XaBzCIFIwNS9Q4GDAAw7Fue
</data>
</layer>
<layer id="2" name="Ground" width="30" height="19">
<data encoding="base64" compression="zlib">
eJzV1F0KwkAMBOB9788F2tIzCD2NnsIDqNeqtb2bK7IQFpvMBFcwMBS2bL/koZnrEOYfpM+C3Dk1ISz1fo7N99z0/mU+4lOrO2Az8yImaqMuYyI24npMy0bcQZjn0U5enXPeQZkV6aGEm2ytp1KuVaXcQ4e5U8u5W8yt8s16ifdW57y9007mp+8ye5KxNZPdk6htmawr7Wv1jvxP05lletz8jtwRbP8ybGn78R/qCRGT22E=
</data>
</layer>
<layer id="7" name="Ornamental Walls" width="30" height="19">
<data encoding="base64" compression="zlib">
eJzl1G0OQDAMBuD+ZnoFcRXBwdzFvTDugcwSZsOqPhJv0tik2xM/CuCbkQFAGR5XH/CbidjviwWffdbktF1mFaky11y26zszXO9T3PZom+p2450S18/JLWYrR+Xa+q64tjTjne3s1qj2Puep7t3n/+hSZ0HPICXUOfT913DYHKavzWmatq5llu85TTM294m86ZpFzQDLEj8K
</data>
</layer>
<layer id="3" name="Clutter" width="30" height="19">
<properties>
<property name="spriteLayer" type="bool" value="true"/>
</properties>
<data encoding="base64" compression="zlib">
eJxjYBgFo2AUDDZQKE89sx5rMjA80SRdjlLwH2SuFulyo4A0MBq/o2AU4AcAe0YOPQ==
</data>
</layer>
<objectgroup id="4" name="Objects">
<object id="66" template="../obj/treasure.tx" x="256.447" y="95.763">
<properties>
<property name="reward">[{
&quot;type&quot;: &quot;randomCard&quot;,
&quot;count&quot;: 2,
&quot;colors&quot;: [ &quot;colorID&quot; ]
},{
&quot;type&quot;: &quot;randomCard&quot;,
&quot;count&quot;: 1,
&quot;probability&quot;: 0.5,
&quot;rarity&quot;: [ &quot;rare&quot; ],
&quot;colors&quot;: [ &quot;colorID&quot; ]
},{
&quot;type&quot;: &quot;randomCard&quot;,
&quot;count&quot;: 3,
&quot;addMaxCount&quot;: 2
}]</property>
</properties>
</object>
<object id="75" template="../obj/enemy.tx" x="291.571" y="166.141">
<properties>
<property name="enemy" value="Demon"/>
<property name="threatRange" type="int" value="30"/>
<property name="waypoints" value="100,101,99"/>
</properties>
</object>
<object id="83" template="../obj/waypoint.tx" x="193.014" y="286.598"/>
<object id="86" template="../obj/entry_up.tx" x="208.316" y="175.684">
<properties>
<property name="teleport" value="../common/maps/map/unhallowed_abbey_1F.tmx"/>
</properties>
</object>
<object id="87" template="../obj/booster.tx" x="272.228" y="95.7719">
<properties>
<property name="reward">[
{
&quot;editions&quot;: [ &quot;VOW&quot; ],
&quot;type&quot;: &quot;card&quot;,
&quot;count&quot;: 10,
&quot;rarity&quot;: [ &quot;Common&quot; ]
},
{
&quot;editions&quot;: [ &quot;VOW&quot; ],
&quot;type&quot;: &quot;card&quot;,
&quot;count&quot;: 3,
&quot;rarity&quot;: [ &quot;Uncommon&quot; ]
},
{
&quot;editions&quot;: [ &quot;VOW&quot; ],
&quot;type&quot;: &quot;card&quot;,
&quot;count&quot;: 1,
&quot;rarity&quot;: [ &quot;Rare&quot;, &quot;Mythic Rare&quot; ]
}
]
</property>
</properties>
</object>
<object id="88" template="../obj/treasure.tx" x="240.333" y="47.6667">
<properties>
<property name="reward">[{
&quot;type&quot;: &quot;randomCard&quot;,
&quot;count&quot;: 2,
&quot;colors&quot;: [ &quot;colorID&quot; ]
},{
&quot;type&quot;: &quot;randomCard&quot;,
&quot;count&quot;: 1,
&quot;probability&quot;: 0.5,
&quot;rarity&quot;: [ &quot;rare&quot; ],
&quot;colors&quot;: [ &quot;colorID&quot; ]
},{
&quot;type&quot;: &quot;randomCard&quot;,
&quot;count&quot;: 3,
&quot;addMaxCount&quot;: 2
}]</property>
</properties>
</object>
<object id="89" template="../obj/booster.tx" x="257.333" y="47.3333">
<properties>
<property name="reward">[
{
&quot;editions&quot;: [ &quot;MID&quot; ],
&quot;type&quot;: &quot;card&quot;,
&quot;count&quot;: 10,
&quot;rarity&quot;: [ &quot;Common&quot; ]
},
{
&quot;editions&quot;: [ &quot;MID&quot; ],
&quot;type&quot;: &quot;card&quot;,
&quot;count&quot;: 3,
&quot;rarity&quot;: [ &quot;Uncommon&quot; ]
},
{
&quot;editions&quot;: [ &quot;MID&quot; ],
&quot;type&quot;: &quot;card&quot;,
&quot;count&quot;: 1,
&quot;rarity&quot;: [ &quot;Rare&quot;, &quot;Mythic Rare&quot; ]
}
]
</property>
</properties>
</object>
<object id="90" template="../obj/treasure.tx" x="175.667" y="48.3333">
<properties>
<property name="reward">[{
&quot;type&quot;: &quot;randomCard&quot;,
&quot;count&quot;: 2,
&quot;colors&quot;: [ &quot;colorID&quot; ]
},{
&quot;type&quot;: &quot;randomCard&quot;,
&quot;count&quot;: 1,
&quot;probability&quot;: 0.5,
&quot;rarity&quot;: [ &quot;rare&quot; ],
&quot;colors&quot;: [ &quot;colorID&quot; ]
},{
&quot;type&quot;: &quot;randomCard&quot;,
&quot;count&quot;: 3,
&quot;addMaxCount&quot;: 2
}]</property>
</properties>
</object>
<object id="91" template="../obj/treasure.tx" x="144.333" y="95">
<properties>
<property name="reward">[{
&quot;type&quot;: &quot;randomCard&quot;,
&quot;count&quot;: 2,
&quot;colors&quot;: [ &quot;colorID&quot; ]
},{
&quot;type&quot;: &quot;randomCard&quot;,
&quot;count&quot;: 1,
&quot;probability&quot;: 0.5,
&quot;rarity&quot;: [ &quot;rare&quot; ],
&quot;colors&quot;: [ &quot;colorID&quot; ]
},{
&quot;type&quot;: &quot;randomCard&quot;,
&quot;count&quot;: 3,
&quot;addMaxCount&quot;: 2
}]</property>
</properties>
</object>
<object id="92" template="../obj/booster.tx" x="127.667" y="94">
<properties>
<property name="reward">[
{
&quot;editions&quot;: [ &quot;SOI&quot; ],
&quot;type&quot;: &quot;card&quot;,
&quot;count&quot;: 10,
&quot;rarity&quot;: [ &quot;Common&quot; ]
},
{
&quot;editions&quot;: [ &quot;SOI&quot; ],
&quot;type&quot;: &quot;card&quot;,
&quot;count&quot;: 3,
&quot;rarity&quot;: [ &quot;Uncommon&quot; ]
},
{
&quot;editions&quot;: [ &quot;SOI&quot; ],
&quot;type&quot;: &quot;card&quot;,
&quot;count&quot;: 1,
&quot;rarity&quot;: [ &quot;Rare&quot;, &quot;Mythic Rare&quot; ]
}
]
</property>
</properties>
</object>
<object id="93" template="../obj/booster.tx" x="159" y="47">
<properties>
<property name="reward">[
{
&quot;editions&quot;: [ &quot;EMN&quot; ],
&quot;type&quot;: &quot;card&quot;,
&quot;count&quot;: 10,
&quot;rarity&quot;: [ &quot;Common&quot; ]
},
{
&quot;editions&quot;: [ &quot;EMN&quot; ],
&quot;type&quot;: &quot;card&quot;,
&quot;count&quot;: 3,
&quot;rarity&quot;: [ &quot;Uncommon&quot; ]
},
{
&quot;editions&quot;: [ &quot;EMN&quot; ],
&quot;type&quot;: &quot;card&quot;,
&quot;count&quot;: 1,
&quot;rarity&quot;: [ &quot;Rare&quot;, &quot;Mythic Rare&quot; ]
}
]
</property>
</properties>
</object>
<object id="94" template="../obj/enemy.tx" x="208.666" y="270.394">
<properties>
<property name="enemy" value="Demon"/>
<property name="threatRange" type="int" value="30"/>
<property name="waypoints" value="98,97,83"/>
</properties>
</object>
<object id="95" template="../obj/enemy.tx" x="122.212" y="172.727">
<properties>
<property name="enemy" value="Demon"/>
<property name="threatRange" type="int" value="30"/>
<property name="waypoints" value="104,102,103"/>
</properties>
</object>
<object id="97" template="../obj/waypoint.tx" x="192.727" y="255.455"/>
<object id="98" template="../obj/waypoint.tx" x="223.091" y="255.455"/>
<object id="99" template="../obj/waypoint.tx" x="288.545" y="127.273"/>
<object id="100" template="../obj/waypoint.tx" x="287.636" y="192"/>
<object id="101" template="../obj/waypoint.tx" x="319.091" y="159.273"/>
<object id="102" template="../obj/waypoint.tx" x="97.4545" y="159.636"/>
<object id="103" template="../obj/waypoint.tx" x="126.545" y="127.455"/>
<object id="104" template="../obj/waypoint.tx" x="125.636" y="200.909"/>
<object id="108" name="Ward" class="dialog" gid="11912" x="192.063" y="128.292" width="48" height="16">
<properties>
<property name="dialog">[{
&quot;text&quot;:&quot;A translucent, shimmering red field blocks your path. Pained screams echo through the room behind you.&quot;,
&quot;options&quot;:[
{ &quot;name&quot;:&quot;Leave.&quot; }
]
}]</property>
</properties>
</object>
<object id="109" name="Sacrifice1" class="dialog" gid="11905" x="159.667" y="123.333" width="16" height="16">
<properties>
<property name="dialog">[
{
&quot;text&quot;:&quot;A captive lies tied to the altar. Glowing red runes encircle them.&quot;,
&quot;options&quot;:[
{
&quot;text&quot;:&quot;As the third captive is freed, you hear the sound of shattering as the barrier in the center of the chamber fails. Time to end this.&quot;,
&quot;action&quot;:[{&quot;deleteMapObject&quot;:-1},{&quot;advanceMapFlag&quot;:&quot;gate&quot;}],
&quot;name&quot;:&quot;Free them.&quot;
&quot;options&quot;:[{
&quot;condition&quot;:[{&quot;getMapFlag&quot;:{&quot;key&quot;:&quot;gate&quot;,&quot;op&quot;:&quot;&gt;=&quot;,&quot;val&quot;:3}}],
&quot;action&quot;:[{&quot;deleteMapObject&quot;:108}],
&quot;name&quot;:&quot;ok&quot; }]
},
{ &quot;name&quot;:&quot;Leave.&quot; }
]
}
]</property>
</properties>
</object>
<object id="110" name="Sacrifice2" class="dialog" gid="11906" x="256.667" y="123.667" width="16" height="16">
<properties>
<property name="dialog">[
{
&quot;text&quot;:&quot;A captive lies tied to the altar. Glowing red runes encircle them.&quot;,
&quot;options&quot;:[
{
&quot;text&quot;:&quot;As the third captive is freed, you hear the sound of shattering as the barrier in the center of the chamber fails. Time to end this.&quot;,
&quot;action&quot;:[{&quot;deleteMapObject&quot;:-1},{&quot;advanceMapFlag&quot;:&quot;gate&quot;}],
&quot;name&quot;:&quot;Free them.&quot;
&quot;options&quot;:[{
&quot;condition&quot;:[{&quot;getMapFlag&quot;:{&quot;key&quot;:&quot;gate&quot;,&quot;op&quot;:&quot;&gt;=&quot;,&quot;val&quot;:3}}],
&quot;action&quot;:[{&quot;deleteMapObject&quot;:108}],
&quot;name&quot;:&quot;ok&quot; }]
},
{ &quot;name&quot;:&quot;Leave.&quot; }
]
}
]</property>
</properties>
</object>
<object id="111" name="Sacrifice3" class="dialog" gid="11907" x="224" y="285.333" width="16" height="16">
<properties>
<property name="dialog">[
{
&quot;text&quot;:&quot;A captive lies tied to the altar. Glowing red runes encircle them.&quot;,
&quot;options&quot;:[
{
&quot;text&quot;:&quot;As the third captive is freed, you hear the sound of shattering as the barrier in the center of the chamber fails. Time to end this.&quot;,
&quot;action&quot;:[{&quot;deleteMapObject&quot;:-1},{&quot;advanceMapFlag&quot;:&quot;gate&quot;}],
&quot;name&quot;:&quot;Free them.&quot;
&quot;options&quot;:[{
&quot;condition&quot;:[{&quot;getMapFlag&quot;:{&quot;key&quot;:&quot;gate&quot;,&quot;op&quot;:&quot;&gt;=&quot;,&quot;val&quot;:3}}],
&quot;action&quot;:[{&quot;deleteMapObject&quot;:108}],
&quot;name&quot;:&quot;ok&quot; }]
},
{ &quot;name&quot;:&quot;Leave.&quot; }
]
}
]</property>
</properties>
</object>
<object id="113" template="../obj/dialog.tx" x="209.364" y="114.091" width="13.8182" height="13.3636">
<properties>
<property name="dialog">[{
&quot;text&quot;:&quot;*The large, imposing demon before you smirks*\n Ah, so you must be the one who's been freeing my sacrifices...and the volunteer to be my new one. Tell me, mortal, as your last words that aren't a howl of pain - why challenge me?&quot;,
&quot;options&quot;:[{
&quot;name&quot;:&quot;Because you're a monster, and you should be stopped!&quot;,
&quot;text&quot;:&quot;*The demon sneers.*\n Ah, a noble *hero*. I should have guessed. Your kind die like anyone else when your power runs dry - allow me to demonstrate!&quot;,
&quot;options&quot;:[{
&quot;name&quot;:&quot;End&quot;,
&quot;action&quot;:[{&quot;deleteMapObject&quot;:113}]
}]
},
{
&quot;name&quot;:&quot;I want power. I'll take it from what's left of you.&quot;,
&quot;text&quot;:&quot;*The demon chuckles.*\n I'll commend your ambition, if not your sense. Fight hard enough, and I might let you replace that failure you dealt with upstairs.&quot;,
&quot;options&quot;:[{
&quot;name&quot;:&quot;End&quot;,
&quot;action&quot;:[{&quot;deleteMapObject&quot;:113}]
}]
},
{
&quot;name&quot;:&quot;Honestly, you just looked like you'd be a good fight.&quot;,
&quot;text&quot;:&quot;*The demon blinks in surprise, then laughs.*\n Well, if that's what you seek, you'll find more than you bargained for here. I hope you enjoy the last battle of your life, *mortal*.&quot;,
&quot;options&quot;:[{
&quot;name&quot;:&quot;End&quot;,
&quot;action&quot;:[{&quot;deleteMapObject&quot;:113}]
}]
},
{
&quot;name&quot;:&quot;...&quot;,
&quot;text&quot;:&quot;*The fiend's eyes narrow.*\n Too scared for words? So be it, mortal. You'll die all the same.&quot;,
&quot;options&quot;:[{
&quot;name&quot;:&quot;End&quot;,
&quot;action&quot;:[{&quot;deleteMapObject&quot;:113}]
}]
}]
}]</property>
</properties>
</object>
<object id="116" template="../obj/enemy.tx" x="203.566" y="96.2358" width="24" height="24">
<properties>
<property name="defeatDialog">[{
&quot;text&quot;:&quot;*With a snarl of pain, the demon collapses to the floor.* \n Congratulations, mortal, you've bested me. In exchange for my life, I offer a lesson - the same killing power I wield.&quot;,
&quot;options&quot;:[{
&quot;name&quot;:&quot;I have no need for power from something as vile as you. Die!&quot;,
&quot;text&quot;:&quot;*The demon's eyes widen in fear.* \n No! I will not be destroyed by- \n *A final blast of power reduces him to mana in the air of this place, and something clatters to the ground. A holy symbol of this place - or rather, what it once was. As the unholy energy around it fades, you can still feel magic coursing through it.*&quot;
&quot;options&quot;:[{
&quot;name&quot;:&quot;End&quot;,
&quot;action&quot;:[{&quot;addItem&quot;:&quot;Hallowed Sigil&quot;},{&quot;deleteMapObject&quot;:116}]
}]
},
{
&quot;name&quot;:&quot;If you knew anything worth teaching me directly, I wouldn't have been able to defeat you.&quot;,
&quot;text&quot;:&quot;*The demon's eyes widen in fear.* \n No! I will not be destroyed by- \n *A final blast of power reduces him to mana in the air of this place, and something clatters to the ground. A holy symbol of this place - or rather, what it once was. As the unholy energy around it fades, you can still feel magic coursing through it.*&quot;
&quot;options&quot;:[{
&quot;name&quot;:&quot;End&quot;,
&quot;action&quot;:[{&quot;addItem&quot;:&quot;Hallowed Sigil&quot;},{&quot;deleteMapObject&quot;:116}]
}]
},
{
&quot;name&quot;:&quot;...Very well, even a monster like you deserves mercy. *Once.*&quot;,
&quot;text&quot;:&quot;*The demon smiles, moving his hand in an arcane gesture.* \n *As you unconsciously mimic it, you feel a dark, repulsive power crystallize in your hand. \n *The demon smiles as he begins to fade into a cloud of smoke.* \n Very well, mortal. My power is yours to wield...until next time.&quot;
&quot;options&quot;:[{
&quot;name&quot;:&quot;End&quot;,
&quot;action&quot;:[{&quot;addItem&quot;:&quot;Unhallowed Sigil&quot;},{&quot;deleteMapObject&quot;:116}]
}]
},
{
&quot;name&quot;:&quot;As you should. I'll take your offer.&quot;,
&quot;text&quot;:&quot;*The demon smiles, moving his hand in an arcane gesture.* \n *As you unconsciously mimic it, you feel a dark, repulsive power crystallize in your hand. \n *The demon smiles as he begins to fade into a cloud of smoke.* \n Very well, mortal. My power is yours to wield...until next time.&quot;
&quot;options&quot;:[{
&quot;name&quot;:&quot;End&quot;,
&quot;action&quot;:[{&quot;addItem&quot;:&quot;Unhallowed Sigil&quot;},{&quot;deleteMapObject&quot;:116}]
}]
}]
}]
</property>
<property name="effect">{ &quot;startBattleWithCard&quot;: [ &quot;Mox Jet&quot;, &quot;Power of Valyx&quot;]
}</property>
<property name="enemy" value="Valyx Feaster of Torment"/>
</properties>
</object>
</objectgroup>
</map>

Binary file not shown.

After

Width:  |  Height:  |  Size: 784 B

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<tileset version="1.9" tiledversion="1.9.2" name="DarkAbbeyTiles" tilewidth="16" tileheight="16" tilecount="30" columns="6">
<image source="DarkAbbeyTiles.png" width="96" height="80"/>
</tileset>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,68 @@
valyx.png
size: 136,96
format: RGBA8888
filter: Nearest,Nearest
repeat: none
Avatar
xy: 0, 0
size: 16, 16
Idle
xy: 0, 16
size: 24, 24
Idle
xy: 24, 16
size: 24, 24
Idle
xy: 48, 16
size: 24, 24
Idle
xy: 72, 16
size: 24, 24
Walk
xy: 0, 40
size: 24, 24
Walk
xy: 24, 40
size: 24, 24
Walk
xy: 48, 40
size: 24, 24
Walk
xy: 72, 40
size: 24, 24
Attack
xy: 0, 64
size: 24, 24
Attack
xy: 24, 64
size: 24, 24
Attack
xy: 48, 64
size: 24, 24
Attack
xy: 72, 64
size: 24, 24
Hit
xy: 0, 88
size: 24, 24
Hit
xy: 24, 88
size: 24, 24
Hit
xy: 48, 88
size: 24, 24
Hit
xy: 72, 88
size: 24, 24
Death
xy: 0, 112
size: 24, 24
Death
xy: 24, 112
size: 24, 24
Death
xy: 48, 112
size: 24, 24
Death
xy: 72, 112
size: 24, 24

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -0,0 +1,68 @@
false_knight.png
size: 64,96
format: RGBA8888
filter: Nearest,Nearest
repeat: none
Avatar
xy: 0, 0
size: 16, 16
Idle
xy: 0, 16
size: 16, 16
Idle
xy: 16, 16
size: 16, 16
Idle
xy: 32, 16
size: 16, 16
Idle
xy: 48, 16
size: 16, 16
Walk
xy: 0, 32
size: 16, 16
Walk
xy: 16, 32
size: 16, 16
Walk
xy: 32, 32
size: 16, 16
Walk
xy: 48, 32
size: 16, 16
Attack
xy: 0, 48
size: 16, 16
Attack
xy: 16, 48
size: 16, 16
Attack
xy: 32, 48
size: 16, 16
Attack
xy: 48, 48
size: 16, 16
Hit
xy: 0, 64
size: 16, 16
Hit
xy: 16, 64
size: 16, 16
Hit
xy: 32, 64
size: 16, 16
Hit
xy: 48, 64
size: 16, 16
Death
xy: 0, 80
size: 16, 16
Death
xy: 16, 80
size: 16, 16
Death
xy: 32, 80
size: 16, 16
Death
xy: 48, 80
size: 16, 16

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -0,0 +1,68 @@
false_monk.png
size: 64,96
format: RGBA8888
filter: Nearest,Nearest
repeat: none
Avatar
xy: 0, 0
size: 16, 16
Idle
xy: 0, 16
size: 16, 16
Idle
xy: 16, 16
size: 16, 16
Idle
xy: 32, 16
size: 16, 16
Idle
xy: 48, 16
size: 16, 16
Walk
xy: 0, 32
size: 16, 16
Walk
xy: 16, 32
size: 16, 16
Walk
xy: 32, 32
size: 16, 16
Walk
xy: 48, 32
size: 16, 16
Attack
xy: 0, 48
size: 16, 16
Attack
xy: 16, 48
size: 16, 16
Attack
xy: 32, 48
size: 16, 16
Attack
xy: 48, 48
size: 16, 16
Hit
xy: 0, 64
size: 16, 16
Hit
xy: 16, 64
size: 16, 16
Hit
xy: 32, 64
size: 16, 16
Hit
xy: 48, 64
size: 16, 16
Death
xy: 0, 80
size: 16, 16
Death
xy: 16, 80
size: 16, 16
Death
xy: 32, 80
size: 16, 16
Death
xy: 48, 80
size: 16, 16

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,68 @@
high_cultist.png
size: 64,96
format: RGBA8888
filter: Nearest,Nearest
repeat: none
Avatar
xy: 0, 0
size: 16, 16
Idle
xy: 0, 16
size: 16, 16
Idle
xy: 16, 16
size: 16, 16
Idle
xy: 32, 16
size: 16, 16
Idle
xy: 48, 16
size: 16, 16
Walk
xy: 0, 32
size: 16, 16
Walk
xy: 16, 32
size: 16, 16
Walk
xy: 32, 32
size: 16, 16
Walk
xy: 48, 32
size: 16, 16
Attack
xy: 0, 48
size: 16, 16
Attack
xy: 16, 48
size: 16, 16
Attack
xy: 32, 48
size: 16, 16
Attack
xy: 48, 48
size: 16, 16
Hit
xy: 0, 64
size: 16, 16
Hit
xy: 16, 64
size: 16, 16
Hit
xy: 32, 64
size: 16, 16
Hit
xy: 48, 64
size: 16, 16
Death
xy: 0, 80
size: 16, 16
Death
xy: 16, 80
size: 16, 16
Death
xy: 32, 80
size: 16, 16
Death
xy: 48, 80
size: 16, 16

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -468,4 +468,9 @@ CartoucheOfAmbition
CartoucheOfZeal
xy:320,240
size:16,16
HallowedSigil
xy:320,256
size:16,16
UnhallowedSigil
xy:320,272
size:16,16

Binary file not shown.

Before

Width:  |  Height:  |  Size: 273 KiB

After

Width:  |  Height:  |  Size: 272 KiB

View File

@@ -43,8 +43,8 @@
"x": 0.5,
"y": 0.5,
"structureAtlasPath": "world/tilesets/terrain.atlas",
"sourcePath": "world/models/fill.png",
"maskPath": "world/masks/fill.png",
"sourcePath": "world/structures/models/fill.png",
"maskPath": "world/structures/masks/fill.png",
"height": 0.99,
"width": 0.9,
"periodicOutput": false,

View File

@@ -32,8 +32,8 @@
"x": 0.5,
"y": 0.5,
"structureAtlasPath": "world/structures/structures.atlas",
"sourcePath": "world/models/fill.png",
"maskPath": "world/masks/fill.png",
"sourcePath": "world/structures/models/fill.png",
"maskPath": "world/structures/masks/fill.png",
"height": 0.99,
"width": 0.9,
"periodicOutput": false,

View File

@@ -107,7 +107,8 @@
"CaveW6",
"OrthodoxyBasilica",
"Nahiri Encampment",
"MageTower White"
"MageTower White",
"UnhallowedAbbey"
],
"structures": [
{

View File

@@ -7840,6 +7840,168 @@
"BiomeWhite"
]
},
{
"name": "False Knight",
"sprite": "sprites/enemy/humanoid/human/knight/false_knight.atlas",
"deck": [
"decks/standard/death_knight.dck"
],
"ai": "",
"spawnRate": 1,
"difficulty": 0.1,
"speed": 30,
"life": 18,
"rewards": [
{
"type": "deckCard",
"probability": 1,
"count": 2,
"addMaxCount": 4,
"rarity": [
"common"
]
},
{
"type": "deckCard",
"probability": 0.5,
"count": 1,
"addMaxCount": 2,
"rarity": [
"uncommon"
],
"cardTypes": [
"Creature",
"Artifact",
"Enchantment",
"Instant",
"Sorcery"
]
},
{
"type": "deckCard",
"probability": 0.25,
"count": 1,
"addMaxCount": 1,
"rarity": [
"rare",
"mythicrare"
],
"cardTypes": [
"Creature",
"Artifact",
"Enchantment",
"Instant",
"Sorcery"
]
},
{
"type": "deckCard",
"probability": 0.1,
"count": 1,
"rarity": [
"rare"
],
"cardTypes": [
"Land"
]
},
{
"type": "gold",
"probability": 0.3,
"count": 10,
"addMaxCount": 90
}
],
"colors": "B",
"questTags": [
"Disguised",
"Soldier",
"Human",
"Knight",
"Unholy",
"IdentityBlack"
]
},
{
"name": "False Monk",
"sprite": "sprites/enemy/humanoid/human/warlock/false_monk.atlas",
"deck": [
"decks/standard/cultist.dck"
],
"randomizeDeck": false,
"spawnRate": 1,
"difficulty": 0.1,
"speed": 24,
"life": 15,
"rewards": [
{
"type": "deckCard",
"probability": 1
"count": 2,
"addMaxCount": 2,
"rarity": [
"common"
]
},
{
"type": "deckCard",
"probability": 0.5,
"count": 1,
"addMaxCount": 1,
"rarity": [
"uncommon"
],
"cardTypes": [
"Creature",
"Artifact",
"Enchantment",
"Instant",
"Sorcery"
]
},
{
"type": "deckCard",
"probability": 0.25,
"count": 1,
"addMaxCount": 1,
"rarity": [
"rare",
"mythicrare"
],
"cardTypes": [
"Creature",
"Artifact",
"Enchantment",
"Instant",
"Sorcery"
]
},
{
"type": "deckCard",
"probability": 0.1,
"count": 1,
"rarity": [
"rare"
],
"cardTypes": [
"Land"
]
},
{
"type": "gold",
"probability": 0.3,
"count": 10,
"addMaxCount": 90
}
],
"colors": "B",
"questTags": [
"Human",
"Disguised",
"Unholy",
"IdentityBlack",
]
},
{
"name": "Farmer",
"nameOverride": "",
@@ -11673,6 +11835,86 @@
null
]
},
{
"name": "High Cultist",
"sprite": "sprites/enemy/humanoid/human/warlock/high_cultist.atlas",
"deck": [
"decks/standard/cultist.dck"
],
"randomizeDeck": false,
"spawnRate": 1,
"difficulty": 0.1,
"speed": 24,
"life": 30,
"rewards": [
{
"type": "deckCard",
"probability": 1,
"count": 2,
"addMaxCount": 4,
"rarity": [
"common"
]
},
{
"type": "deckCard",
"probability": 0.5,
"count": 1,
"addMaxCount": 2,
"rarity": [
"uncommon"
],
"cardTypes": [
"Creature",
"Artifact",
"Enchantment",
"Instant",
"Sorcery"
]
},
{
"type": "deckCard",
"probability": 0.25,
"count": 1,
"addMaxCount": 1,
"rarity": [
"rare",
"mythicrare"
],
"cardTypes": [
"Creature",
"Artifact",
"Enchantment",
"Instant",
"Sorcery"
]
},
{
"type": "deckCard",
"probability": 0.1,
"count": 1,
"rarity": [
"rare"
],
"cardTypes": [
"Land"
]
},
{
"type": "gold",
"probability": 0.3,
"count": 10,
"addMaxCount": 90
}
],
"colors": "B",
"questTags": [
"Human",
"Disguised",
"Unholy",
"IdentityBlack",
]
},
{
"name": "High Elf",
"sprite": "sprites/enemy/humanoid/elf/druid_2.atlas",
@@ -21035,6 +21277,69 @@
"BiomeRed"
]
},
{
"name": "Valyx Feaster of Torment",
"sprite": "sprites/enemy/fiend/valyx.atlas",
"deck": [
"decks/miniboss/valyx.dck"
],
"ai": "",
"spawnRate": 1,
"difficulty": 0.1,
"speed": 31,
"life": 80,
"rewards": [
{
"type": "deckCard",
"probability": 1,
"count": 2,
"addMaxCount": 4,
"rarity": [
"common"
]
},
{
"type": "deckCard",
"probability": 1,
"count": 2,
"addMaxCount": 2,
"rarity": [
"uncommon"
],
"cardTypes": [
"Creature",
"Artifact",
"Enchantment",
"Instant",
"Sorcery"
]
},
{
"type": "deckCard",
"probability": 1,
"count": 2,
"addMaxCount": 1,
"rarity": [
"rare",
"mythicrare"
],
"cardTypes": [
"Creature",
"Artifact",
"Enchantment",
"Instant",
"Sorcery"
]
}
],
"colors": "B",
"questTags": [
"Demon",
"Humanoid",
"Unholy",
"IdentityBlack"
]
},
{
"name": "Vampire",
"sprite": "sprites/enemy/undead/vampire.atlas",

View File

@@ -1234,5 +1234,32 @@
"Slobad's Iron Boots"
]
}
}
},
{
"name": "Hallowed Sigil",
"description": "Turn a creature hexproof until end of turn.",
"equipmentSlot": "Neck",
"iconName": "HallowedSigil",
"effect": {
"startBattleWithCard": [
"Hallowed Sigil"
]
}
},
{
"name": "Unhallowed Sigil",
"description": "Devour the life of an enemy creature, killing it.",
"equipmentSlot": "Right",
"iconName": "UnhallowedSigil",
"effect": {
"startBattleWithCard": [
"Sigil of Torment"
]
}
},
{
"name": "Cultist's Key",
"iconName": "StrangeKey",
"questItem": true
}
]

View File

@@ -3130,6 +3130,18 @@
"Planeswalker"
]
},
{
"name": "UnhallowedAbbey",
"type": "dungeon",
"count": 1,
"spriteAtlas": "maps/tileset/buildings.atlas",
"sprite": "Monastery",
"map": "../common/maps/map/unhallowed_abbey_1F.tmx",
"radiusFactor": 0.8,
"questTags": [
"UnhallowedAbbey"
]
},
{
"name": "VampireCastle",
"type": "dungeon",

View File

@@ -12,5 +12,5 @@ ALTERNATE
Name:Explosive Crystal
ManaCost:4 R
Types:Sorcery Adventure
A:SP$ DealDamage | ValidTgts$ Any to distribute damage to | NumDmg$ 4 | TargetMin$ 0 | TargetMax$ 4 | DividedAsYouChoose$ 4 | SpellDescription$ CARDNAME deals 4 damage divided as you choose among any number of targets.
A:SP$ DealDamage | ValidTgts$ Any | NumDmg$ 4 | TargetMin$ 0 | TargetMax$ 4 | DividedAsYouChoose$ 4 | SpellDescription$ CARDNAME deals 4 damage divided as you choose among any number of targets.
Oracle:Explosive Crystal deals 4 damage divided as you choose among any number of targets.

View File

@@ -1,9 +1,7 @@
Name:Arena
ManaCost:no cost
Types:Land
A:AB$ Tap | Cost$ 3 T | ValidTgts$ Creature.YouCtrl | TgtPrompt$ Select target creature you control | SubAbility$ DBTap | AlwaysRemember$ True | SpellDescription$ Tap target creature you control and target creature of an opponent's choice they control. Those creatures fight each other. (Each deals damage equal to its power to the other.)
SVar:DBTap:DB$ Tap | TargetingPlayer$ Player.Opponent | TargetingPlayerControls$ True | ValidTgts$ Creature | TgtPrompt$ Select target creature you control | SubAbility$ DBFight | AlwaysRemember$ True
SVar:DBFight:DB$ Fight | Defined$ Remembered | SubAbility$ DBCleanup
SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True
AI:RemoveDeck:All
A:AB$ Tap | Cost$ 3 T | ValidTgts$ Creature.YouCtrl | TgtPrompt$ Select target creature you control | SubAbility$ DBTap | AILogic$ Arena | SpellDescription$ Tap target creature you control and target creature of an opponent's choice they control. Those creatures fight each other. (Each deals damage equal to its power to the other.)
SVar:DBTap:DB$ Tap | TargetingPlayer$ Player.Opponent | TargetingPlayerControls$ True | ValidTgts$ Creature | TgtPrompt$ Select target creature you control | SubAbility$ DBFight
SVar:DBFight:DB$ Fight | Defined$ Targeted
Oracle:{3}, {T}: Tap target creature you control and target creature of an opponent's choice they control. Those creatures fight each other. (Each deals damage equal to its power to the other.)

View File

@@ -7,5 +7,5 @@ SVar:TrigCounter:DB$ Counter | Defined$ TriggeredSpellAbility
SVar:X:Count$xPaid
SVar:Y:Count$CardCounters.CHARGE
SVar:AICurseEffect:ChaliceOfTheVoid
AI:RemoveDeck:All
AI:RemoveDeck:Random
Oracle:Chalice of the Void enters the battlefield with X charge counters on it.\nWhenever a player casts a spell with mana value equal to the number of charge counters on Chalice of the Void, counter that spell.

View File

@@ -4,7 +4,7 @@ Types:Legendary Creature Human Shaman
PT:2/2
T:Mode$ SpellCast | ValidCard$ Card.Red | ValidActivatingPlayer$ You | TriggerZones$ Battlefield | Execute$ TrigUntap | TriggerDescription$ Whenever you cast a red spell, untap CARDNAME.
SVar:TrigUntap:DB$ Untap | Defined$ Self
A:AB$ DealDamage | Cost$ T | ValidTgts$ Player,Planeswalker | TgtPrompt$ Select target player or planeswalker | NumDmg$ 1 | SubAbility$ DBTransform | AILogic$ PingAfterAttack | SpellDescription$ CARDNAME deals 1 damage to target player or planeswalker. If CARDNAME has dealt 3 or more damage this turn, exile her, then return her to the battlefield transformed under her owner's control.
A:AB$ DealDamage | Cost$ T | ValidTgts$ Player,Planeswalker | TgtPrompt$ Select target player or planeswalker | NumDmg$ 1 | SubAbility$ DBTransform | AILogic$ PingAfterAttack | SpellDescription$ CARDNAME deals 1 damage to target player or planeswalker. If NICKNAME has dealt 3 or more damage this turn, exile her, then return her to the battlefield transformed under her owner's control.
SVar:DBTransform:DB$ ChangeZone | Origin$ Battlefield | Destination$ Exile | RememberChanged$ True | SubAbility$ DBReturn | ConditionCheckSVar$ X | ConditionSVarCompare$ GE3 | StackDescription$ If CARDNAME has dealt 3 or more damage this turn, exile her, then return her to the battlefield transformed under her owner's control.
SVar:DBReturn:DB$ ChangeZone | Defined$ Remembered | Origin$ Exile | Destination$ Battlefield | Transformed$ True | SubAbility$ DBCleanup | StackDescription$
SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True

View File

@@ -2,7 +2,7 @@ Name:Cleaver Skaab
ManaCost:3 U
Types:Creature Zombie Horror
PT:2/4
A:AB$ CopyPermanent | Cost$ 3 T Sac<1/Zombie.Other/another zombie> | Defined$ Sacrificed | NumCopies$ 2 | AILogic$ AtOppEOT
A:AB$ CopyPermanent | Cost$ 3 T Sac<1/Zombie.Other/another zombie> | Defined$ Sacrificed | NumCopies$ 2 | AILogic$ AtOppEOT | SpellDescription$ Create two tokens that are copies of the sacrificed creature.
DeckNeeds:Type$Zombie
DeckHas:Ability$Sacrifice|Token
SVar:AIPreference:SacCost$Zombie.Other

View File

@@ -1,5 +1,5 @@
Name:Congregation at Dawn
ManaCost:G G W
Types:Instant
A:SP$ ChangeZone | Cost$ G G W | Origin$ Library | Destination$ Library | LibraryPosition$ 0 | ChangeType$ Creature | ChangeNum$ 3 | SpellDescription$ Search your library for up to three creature cards, reveal them, then shuffle and put those cards on top in any order.
A:SP$ ChangeZone | Cost$ G G W | Origin$ Library | Destination$ Library | LibraryPosition$ 0 | ChangeType$ Creature | ChangeNum$ 3 | Reorder$ True | SpellDescription$ Search your library for up to three creature cards, reveal them, then shuffle and put those cards on top in any order.
Oracle:Search your library for up to three creature cards, reveal them, then shuffle and put those cards on top in any order.

View File

@@ -3,7 +3,7 @@ ManaCost:2 R
Types:Creature Dwarf
PT:2/2
T:Mode$ ChangesZone | Origin$ Any | Destination$ Battlefield | ValidCard$ Card.Self | Execute$ TrigChangeZone | TriggerDescription$ When CARDNAME enters the battlefield, search your library for any number of Dwarf cards, reveal them, then shuffle and put those cards on top in any order.
SVar:TrigChangeZone:DB$ ChangeZone | ChangeNum$ X | ChangeType$ Dwarf | Origin$ Library | Destination$ Library | LibraryPosition$ 0
SVar:TrigChangeZone:DB$ ChangeZone | ChangeNum$ X | ChangeType$ Dwarf | Origin$ Library | Destination$ Library | LibraryPosition$ 0 | Reorder$ True
SVar:X:Count$InYourLibrary.Dwarf
AI:RemoveDeck:All
DeckNeeds:Type$Dwarf

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