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

This commit is contained in:
Simisays
2023-07-07 21:26:40 +02:00
221 changed files with 4108 additions and 760 deletions

View File

@@ -48,10 +48,7 @@ import forge.util.collect.FCollection;
import forge.util.collect.FCollectionView;
import org.apache.commons.lang3.tuple.Pair;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.*;
/**
@@ -360,35 +357,41 @@ public class AiAttackController {
}
// this checks to make sure that the computer player doesn't lose when the human player attacks
public final List<Card> notNeededAsBlockers(final List<Card> attackers) {
public final List<Card> notNeededAsBlockers(final List<Card> currentAttackers, final List<Card> potentialAttackers) {
//check for time walks
if (ai.getGame().getPhaseHandler().getNextTurn().equals(ai)) {
return attackers;
return potentialAttackers;
}
// no need to block (already holding mana to cast fog next turn)
if (!AiCardMemory.isMemorySetEmpty(ai, AiCardMemory.MemorySet.CHOSEN_FOG_EFFECT)) {
// Don't send the card that'll do the fog effect to attack, it's unsafe!
List<Card> toRemove = Lists.newArrayList();
for (Card c : attackers) {
for (Card c : potentialAttackers) {
if (AiCardMemory.isRememberedCard(ai, c, AiCardMemory.MemorySet.CHOSEN_FOG_EFFECT)) {
toRemove.add(c);
}
}
attackers.removeAll(toRemove);
return attackers;
potentialAttackers.removeAll(toRemove);
return potentialAttackers;
}
if (ai.isCardInPlay("Masako the Humorless")) {
// "Tapped creatures you control can block as though they were untapped."
return potentialAttackers;
}
final CardCollection notNeededAsBlockers = new CardCollection(potentialAttackers);
final List<Card> vigilantes = new ArrayList<>();
for (final Card c : myList) {
if (c.getName().equals("Masako the Humorless")) {
// "Tapped creatures you control can block as though they were untapped."
return attackers;
}
for (final Card c : Iterables.concat(currentAttackers, potentialAttackers)) {
// no need to block if an effect is in play which untaps all creatures
// (pseudo-Vigilance akin to Awakening or Prophet of Kruphix)
if (c.hasKeyword(Keyword.VIGILANCE) || ComputerUtilCard.willUntap(ai, c)) {
vigilantes.add(c);
} else if (currentAttackers.contains(c)) {
// already attacking so can't block
notNeededAsBlockers.add(c);
}
}
// reduce the search space
@@ -402,10 +405,8 @@ public class AiAttackController {
}
});
final CardCollection notNeededAsBlockers = new CardCollection(attackers);
// don't hold back creatures that can't block any of the human creatures
final List<Card> blockers = getPossibleBlockers(attackers, opponentsAttackers, true);
final List<Card> blockers = getPossibleBlockers(potentialAttackers, opponentsAttackers, true);
if (!blockers.isEmpty()) {
notNeededAsBlockers.removeAll(blockers);
@@ -476,12 +477,15 @@ public class AiAttackController {
// these creatures will be available to block anyway
notNeededAsBlockers.addAll(vigilantes);
// remove those that were only included to ensure a full picture for the baseline
notNeededAsBlockers.removeAll(currentAttackers);
// Increase the total number of blockers needed by 1 if Finest Hour in play
// (human will get an extra first attack with a creature that untaps)
// In addition, if the computer guesses it needs no blockers, make sure
// that it won't be surprised by Exalted
final int humanExaltedBonus = defendingOpponent.countExaltedBonus();
int blockersNeeded = attackers.size() - notNeededAsBlockers.size();
int blockersNeeded = potentialAttackers.size() - notNeededAsBlockers.size();
if (humanExaltedBonus > 0) {
final boolean finestHour = defendingOpponent.isCardInPlay("Finest Hour");
@@ -511,6 +515,88 @@ public class AiAttackController {
return notNeededAsBlockers;
}
public void reinforceWithBanding(final Combat combat) {
reinforceWithBanding(combat, null);
}
public void reinforceWithBanding(final Combat combat, final Card test) {
CardCollection attackers = combat.getAttackers();
if (attackers.isEmpty()) {
return;
}
List<String> bandsWithString = Arrays.asList("Bands with Other Legendary Creatures",
"Bands with Other Creatures named Wolves of the Hunt",
"Bands with Other Dinosaurs");
List<Card> bandingCreatures = null;
if (test == null) {
bandingCreatures = CardLists.filter(myList, card -> card.hasKeyword(Keyword.BANDING) || card.hasAnyKeyword(bandsWithString));
// filter out anything that can't legally attack or is already declared as an attacker
bandingCreatures = CardLists.filter(bandingCreatures, card -> !combat.isAttacking(card) && CombatUtil.canAttack(card));
bandingCreatures = notNeededAsBlockers(attackers, bandingCreatures);
} else {
// Test a specific creature for Banding
if (test.hasKeyword(Keyword.BANDING) || test.hasAnyKeyword(bandsWithString)) {
bandingCreatures = new CardCollection(test);
}
}
// respect global attack constraints
GlobalAttackRestrictions restrict = GlobalAttackRestrictions.getGlobalRestrictions(ai, combat.getDefenders());
int attackMax = restrict.getMax();
if (attackMax >= attackers.size()) {
return;
}
if (bandingCreatures != null) {
List<String> evasionKeywords = Arrays.asList("Flying", "Horsemanship", "Shadow", "Plainswalk", "Islandwalk",
"Forestwalk", "Mountainwalk", "Swampwalk");
// TODO: Assign to band with the best attacker for now, but needs better logic.
for (Card c : bandingCreatures) {
Card bestBand;
if (c.getNetPower() <= 0) {
// Don't band a zero power creature if there's already a banding creature in a band
attackers = CardLists.filter(attackers, card -> combat.getBandOfAttacker(card).getAttackers().size() == 1);
}
Card bestAttacker = ComputerUtilCard.getBestCreatureAI(attackers);
if (c.hasKeyword("Bands with Other Legendary Creatures")) {
bestBand = ComputerUtilCard.getBestCreatureAI(CardLists.getType(attackers, "Legendary"));
} else if (c.hasKeyword("Bands with Other Dinosaurs")) {
bestBand = ComputerUtilCard.getBestCreatureAI(CardLists.getType(attackers, "Dinosaur"));
} else if (c.hasKeyword("Bands with Other Creatures named Wolves of the Hunt")) {
bestBand = ComputerUtilCard.getBestCreatureAI(CardLists.filter(attackers, CardPredicates.nameEquals("Wolves of the Hunt")));
} else if (!c.hasAnyKeyword(evasionKeywords) && bestAttacker != null && bestAttacker.hasAnyKeyword(evasionKeywords)) {
bestBand = ComputerUtilCard.getBestCreatureAI(CardLists.filter(attackers, card -> !card.hasAnyKeyword(evasionKeywords)));
} else {
bestBand = bestAttacker;
}
if (c.getNetPower() <= 0) {
attackers = combat.getAttackers(); // restore the unfiltered attackers
}
if (bestBand != null) {
GameEntity defender = combat.getDefenderByAttacker(bestBand);
if (attackMax == -1) {
// check with the local limitations vs. the chosen defender
attackMax = restrict.getDefenderMax().get(defender) == null ? -1 : restrict.getDefenderMax().get(defender);
}
if (attackMax == -1 || attackMax > combat.getAttackers().size()) {
if (CombatUtil.canAttack(c, defender)) {
combat.addAttacker(c, defender, combat.getBandOfAttacker(bestBand));
}
}
}
}
}
}
private boolean doAssault() {
if (ai.isCardInPlay("Beastmaster Ascension") && this.attackers.size() > 1) {
final CardCollectionView beastions = ai.getCardsIn(ZoneType.Battlefield, "Beastmaster Ascension");
@@ -1199,7 +1285,7 @@ public class AiAttackController {
if ( LOG_AI_ATTACKS )
System.out.println("Normal attack");
attackersLeft = notNeededAsBlockers(attackersLeft);
attackersLeft = notNeededAsBlockers(combat.getAttackers(), attackersLeft);
attackersLeft = sortAttackers(attackersLeft);
if ( LOG_AI_ATTACKS )

View File

@@ -17,11 +17,7 @@
*/
package forge.ai;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.*;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
@@ -36,6 +32,7 @@ import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CounterEnumType;
import forge.game.combat.AttackingBand;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.cost.Cost;
@@ -573,7 +570,7 @@ public class AiBlockController {
// Try to block a Menace attacker with two blockers, neither of which will die
for (final Card attacker : attackersLeft) {
if (CombatUtil.getMinNumBlockersForAttacker(attacker, combat.getDefenderPlayerByAttacker(attacker)) <= 1) {
if (CombatUtil.getMinNumBlockersForAttacker(attacker, combat.getDefenderPlayerByAttacker(attacker)) != 2) {
continue;
}
@@ -744,16 +741,14 @@ public class AiBlockController {
combat.addBlocker(attacker, blocker);
usedBlockers.add(blocker);
if (CombatUtil.canAttackerBeBlockedWithAmount(attacker, usedBlockers.size(), combat)) {
attackersLeft.remove(attacker);
usedBlockers.clear();
break;
}
}
}
if (CombatUtil.canAttackerBeBlockedWithAmount(attacker, usedBlockers.size(), combat)) {
attackersLeft.remove(attacker);
} else {
for (Card blocker : usedBlockers) {
combat.removeBlockAssignment(attacker, blocker);
}
for (Card blocker : usedBlockers) {
combat.removeBlockAssignment(attacker, blocker);
}
}
}
@@ -770,21 +765,49 @@ public class AiBlockController {
tramplingAttackers = CardLists.filter(tramplingAttackers, Predicates.not(changesPTWhenBlocked(true)));
for (final Card attacker : tramplingAttackers) {
if (CombatUtil.getMinNumBlockersForAttacker(attacker, combat.getDefenderPlayerByAttacker(attacker)) > combat.getBlockers(attacker).size()
|| StaticAbilityAssignCombatDamageAsUnblocked.assignCombatDamageAsUnblocked(attacker)
|| attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")) {
if (CombatUtil.getMinNumBlockersForAttacker(attacker, combat.getDefenderPlayerByAttacker(attacker)) > combat.getBlockers(attacker).size()) {
continue;
}
boolean needsMoreChumpBlockers = true;
// See if it's possible to tank up the damage with Banding
List<String> bandsWithString = Arrays.asList("Bands with Other Legendary Creatures",
"Bands with Other Creatures named Wolves of the Hunt",
"Bands with Other Dinosaurs");
if (AttackingBand.isValidBand(combat.getBlockers(attacker), true)) {
continue;
}
chumpBlockers = getPossibleBlockers(combat, attacker, blockersLeft, false);
chumpBlockers.removeAll(combat.getBlockers(attacker));
// See if there's a Banding blocker that can tank the damage
for (final Card blocker : chumpBlockers) {
// Add an additional blocker if the current blockers are not
// enough and the new one would suck some of the damage
if (ComputerUtilCombat.getAttack(attacker) > ComputerUtilCombat.totalShieldDamage(attacker, combat.getBlockers(attacker))
&& ComputerUtilCombat.shieldDamage(attacker, blocker) > 0
&& CombatUtil.canBlock(attacker, blocker, combat) && ComputerUtilCombat.lifeInDanger(ai, combat)) {
combat.addBlocker(attacker, blocker);
if (blocker.hasKeyword(Keyword.BANDING) || blocker.hasAnyKeyword(bandsWithString)) {
if (ComputerUtilCombat.getAttack(attacker) > ComputerUtilCombat.totalShieldDamage(attacker, combat.getBlockers(attacker))
&& ComputerUtilCombat.shieldDamage(attacker, blocker) > 0
&& CombatUtil.canBlock(attacker, blocker, combat) && ComputerUtilCombat.lifeInDanger(ai, combat)) {
combat.addBlocker(attacker, blocker);
needsMoreChumpBlockers = false;
break;
}
}
}
if (!needsMoreChumpBlockers || StaticAbilityAssignCombatDamageAsUnblocked.assignCombatDamageAsUnblocked(attacker)) {
continue;
}
if (needsMoreChumpBlockers) {
for (final Card blocker : chumpBlockers) {
// Add an additional blocker if the current blockers are not
// enough and the new one would suck some of the damage
if (ComputerUtilCombat.getAttack(attacker) > ComputerUtilCombat.totalShieldDamage(attacker, combat.getBlockers(attacker))
&& ComputerUtilCombat.shieldDamage(attacker, blocker) > 0
&& CombatUtil.canBlock(attacker, blocker, combat) && ComputerUtilCombat.lifeInDanger(ai, combat)) {
combat.addBlocker(attacker, blocker);
}
}
}
}
@@ -907,21 +930,21 @@ public class AiBlockController {
CardLists.sortByPowerAsc(chumpPWDefenders);
if (!chumpPWDefenders.isEmpty()) {
for (final Card attacker : attackers) {
if (attacker.hasKeyword(Keyword.TRAMPLE)) {
// don't bother trying to chump a trampling creature
continue;
}
if (!combat.getBlockers(attacker).isEmpty()) {
// already blocked by something, no need to chump
continue;
}
GameEntity def = combat.getDefenderByAttacker(attacker);
if (def instanceof Card && threatenedPWs.contains(def)) {
if (attacker.hasKeyword(Keyword.TRAMPLE)) {
// don't bother trying to chump a trampling creature
continue;
}
if (!combat.getBlockers(attacker).isEmpty()) {
// already blocked by something, no need to chump
continue;
}
Card blockerDecided = null;
for (final Card blocker : chumpPWDefenders) {
if (CombatUtil.canBlock(attacker, blocker, combat)) {
combat.addBlocker(attacker, blocker);
pwsWithChumpBlocks.add((Card) combat.getDefenderByAttacker(attacker));
pwsWithChumpBlocks.add((Card) def);
chosenChumpBlockers.add(blocker);
blockerDecided = blocker;
blockersLeft.remove(blocker);
@@ -934,9 +957,9 @@ public class AiBlockController {
// check to see if we managed to cover all the blockers of the planeswalker; if not, bail
for (final Card pw : pwsWithChumpBlocks) {
CardCollection pwAttackers = combat.getAttackersOf(pw);
CardCollection pwDefenders = new CardCollection();
boolean isFullyBlocked = true;
if (!pwAttackers.isEmpty()) {
CardCollection pwDefenders = new CardCollection();
boolean isFullyBlocked = true;
int damageToPW = 0;
for (Card pwAtk : pwAttackers) {
if (!combat.getBlockers(pwAtk).isEmpty()) {

View File

@@ -1304,6 +1304,9 @@ public class AiController {
AiAttackController aiAtk = new AiAttackController(attacker);
lastAttackAggression = aiAtk.declareAttackers(combat);
// Check if we can reinforce with Banding creatures
aiAtk.reinforceWithBanding(combat);
// if invalid: just try an attack declaration that we know to be legal
if (!CombatUtil.validateAttackers(combat)) {
combat.clearAttackers();
@@ -1618,8 +1621,7 @@ public class AiController {
AiPlayDecision opinion = canPlayAndPayFor(sa);
// reset LastStateBattlefield
sa.setLastStateBattlefield(CardCollection.EMPTY);
sa.setLastStateGraveyard(CardCollection.EMPTY);
sa.clearLastState();
// PhaseHandler ph = game.getPhaseHandler();
// System.out.printf("Ai thinks '%s' of %s -> %s @ %s %s >>> \n", opinion, sa.getHostCard(), sa, Lang.getPossesive(ph.getPlayerTurn().getName()), ph.getPhase());

View File

@@ -2424,6 +2424,18 @@ public class ComputerUtil {
}
}
}
} else if ("ProtectionFromType".equals(logic)) {
// TODO: protection vs. damage-dealing and milling instants/sorceries in low creature decks and the like?
// Maybe non-creature artifacts in certain cases?
List<String> choices = ImmutableList.of("Creature", "Planeswalker"); // types that make sense to get protected against
CardCollection evalList = new CardCollection();
evalList.addAll(ai.getOpponents().getCardsIn(ZoneType.Battlefield));
chosen = ComputerUtilCard.getMostProminentCardType(evalList, choices);
if (StringUtils.isEmpty(chosen)) {
chosen = "Creature"; // if in doubt, choose Creature, I guess
}
}
else {
// Are we picking a type to reduce costs for that type?

View File

@@ -1406,6 +1406,39 @@ public class ComputerUtilCard {
}
}
if (keywords.contains("Banding") && !c.hasKeyword(Keyword.BANDING)) {
if (phase.is(PhaseType.COMBAT_BEGIN) && phase.isPlayerTurn(ai) && !ComputerUtilCard.doesCreatureAttackAI(ai, c)) {
// will this card participate in an attacking band?
Card bandingCard = getPumpedCreature(ai, sa, c, toughness, power, keywords);
// TODO: It may be possible to use AiController.getPredictedCombat here, but that makes it difficult to
// use reinforceWithBanding through the attack controller, especially with the extra card parameter in mind
AiAttackController aiAtk = new AiAttackController(ai);
Combat predicted = new Combat(ai);
aiAtk.declareAttackers(predicted);
aiAtk.reinforceWithBanding(predicted, bandingCard);
if (predicted.isAttacking(bandingCard) && predicted.getBandOfAttacker(bandingCard).getAttackers().size() > 1) {
return true;
}
} else if (phase.is(PhaseType.COMBAT_DECLARE_BLOCKERS) && combat != null) {
// does this card block a Trample card or participate in a multi block?
for (Card atk : combat.getAttackers()) {
if (atk.getController().isOpponentOf(ai)) {
CardCollection blockers = combat.getBlockers(atk);
boolean hasBanding = false;
for (Card blocker : blockers) {
if (blocker.hasKeyword(Keyword.BANDING)) {
hasBanding = true;
break;
}
}
if (!hasBanding && ((blockers.contains(c) && blockers.size() > 1) || atk.hasKeyword(Keyword.TRAMPLE))) {
return true;
}
}
}
}
}
final Player opp = ai.getWeakestOpponent();
Card pumped = getPumpedCreature(ai, sa, c, toughness, power, keywords);
List<Card> oppCreatures = opp.getCreaturesInPlay();

View File

@@ -17,26 +17,17 @@
*/
package forge.ai;
import java.util.List;
import java.util.Map;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import forge.game.Game;
import forge.game.GameEntity;
import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
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.CardUtil;
import forge.game.card.CounterEnumType;
import forge.game.card.*;
import forge.game.combat.AttackingBand;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.cost.CostPayment;
@@ -57,6 +48,9 @@ import forge.util.MyRandom;
import forge.util.TextUtil;
import forge.util.collect.FCollection;
import java.util.List;
import java.util.Map;
/**
* <p>
@@ -2023,6 +2017,8 @@ public class ComputerUtilCombat {
* distributeAIDamage.
* </p>
*
* @param self
* a {@link forge.game.player.Player} object.
* @param attacker
* a {@link forge.game.card.Card} object.
* @param block
@@ -2031,16 +2027,20 @@ public class ComputerUtilCombat {
* @param defender
* @param overrideOrder overriding combatant order
*/
public static Map<Card, Integer> distributeAIDamage(final Card attacker, final CardCollectionView block, final CardCollectionView remaining, int dmgCanDeal, GameEntity defender, boolean overrideOrder) {
// TODO: Distribute defensive Damage (AI controls how damage is dealt to own cards) for Banding and Defensive Formation
public static Map<Card, Integer> distributeAIDamage(final Player self, final Card attacker, final CardCollectionView block, final CardCollectionView remaining, int dmgCanDeal, GameEntity defender, boolean overrideOrder) {
Map<Card, Integer> damageMap = Maps.newHashMap();
Combat combat = attacker.getGame().getCombat();
boolean isAttacking = defender != null;
// Check for Banding, Defensive Formation
boolean isAttackingMe = isAttacking && combat.getDefenderPlayerByAttacker(attacker).equals(self);
boolean isBlockingMyBand = attacker.getController().isOpponentOf(self) && AttackingBand.isValidBand(block, true);
final boolean aiDistributesBandingDmg = isAttackingMe || isBlockingMyBand;
final boolean hasTrample = attacker.hasKeyword(Keyword.TRAMPLE);
if (combat != null && remaining != null && hasTrample && attacker.isAttacking()) {
if (combat != null && remaining != null && hasTrample && attacker.isAttacking() && !aiDistributesBandingDmg) {
// if attacker has trample and some of its blockers are also blocking others it's generally a good idea
// to assign those without trample first so we can maximize the damage to the defender
for (final Card c : remaining) {
@@ -2061,7 +2061,7 @@ public class ComputerUtilCombat {
final Card blocker = block.getFirst();
int dmgToBlocker = dmgCanDeal;
if (hasTrample && isAttacking) { // otherwise no entity to deliver damage via trample
if (hasTrample && isAttacking && !aiDistributesBandingDmg) { // otherwise no entity to deliver damage via trample
dmgToBlocker = getEnoughDamageToKill(blocker, dmgCanDeal, attacker, true);
if (dmgCanDeal < dmgToBlocker) {
@@ -2077,7 +2077,7 @@ public class ComputerUtilCombat {
}
damageMap.put(blocker, dmgToBlocker);
} // 1 blocker
else {
else if (!aiDistributesBandingDmg) {
// Does the attacker deal lethal damage to all blockers
//Blocking Order now determined after declare blockers
Card lastBlocker = null;
@@ -2098,13 +2098,26 @@ public class ComputerUtilCombat {
}
} // for
if (dmgCanDeal > 0 ) { // if any damage left undistributed,
if (dmgCanDeal > 0) { // if any damage left undistributed,
if (hasTrample && isAttacking) // if you have trample, deal damage to defending entity
damageMap.put(null, dmgCanDeal);
else if (lastBlocker != null) { // otherwise flush it into last blocker
damageMap.put(lastBlocker, dmgCanDeal + damageMap.get(lastBlocker));
}
}
} else {
// In the event of Banding or Defensive Formation, assign max damage to the blocker who
// can tank all the damage or to the worst blocker to lose as little as possible
for (final Card b : block) {
final int dmgToKill = getEnoughDamageToKill(b, dmgCanDeal, attacker, true);
if (dmgToKill > dmgCanDeal) {
damageMap.put(b, dmgCanDeal);
break;
}
}
if (damageMap.isEmpty()) {
damageMap.put(ComputerUtilCard.getWorstCreatureAI(block), dmgCanDeal);
}
}
return damageMap;
}

View File

@@ -962,7 +962,12 @@ public class ComputerUtilMana {
ManaCostShard toPay, SpellAbility saPayment) {
AbilityManaPart m = saPayment.getManaPart();
if (m.isComboMana()) {
m.setExpressChoice(ColorSet.fromMask(toPay.getColorMask()));
// usually we'll want to produce color that matches the shard
ColorSet shared = ColorSet.fromMask(toPay.getColorMask()).getSharedColors(ColorSet.fromNames(m.getComboColors(saPayment).split(" ")));
// but other effects might still lead to a more permissive payment
if (!shared.isColorless()) {
m.setExpressChoice(ColorSet.fromMask(shared.iterator().next()));
}
getComboManaChoice(ai, saPayment, sa, cost);
}
else if (saPayment.getApi() == ApiType.ManaReflected) {

View File

@@ -23,7 +23,10 @@ import forge.game.ability.effects.CharmEffect;
import forge.game.card.*;
import forge.game.card.CardPredicates.Presets;
import forge.game.combat.Combat;
import forge.game.cost.*;
import forge.game.cost.Cost;
import forge.game.cost.CostEnlist;
import forge.game.cost.CostPart;
import forge.game.cost.CostPartMana;
import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordInterface;
import forge.game.mana.Mana;
@@ -106,7 +109,7 @@ public class PlayerControllerAi extends PlayerController {
@Override
public Map<Card, Integer> assignCombatDamage(Card attacker, CardCollectionView blockers, CardCollectionView remaining, int damageDealt, GameEntity defender, boolean overrideOrder) {
return ComputerUtilCombat.distributeAIDamage(attacker, blockers, remaining, damageDealt, defender, overrideOrder);
return ComputerUtilCombat.distributeAIDamage(player, attacker, blockers, remaining, damageDealt, defender, overrideOrder);
}
@Override
@@ -257,9 +260,6 @@ public class PlayerControllerAi extends PlayerController {
public boolean confirmTrigger(WrappedAbility wrapper) {
final SpellAbility sa = wrapper.getWrappedAbility();
//final Trigger regtrig = wrapper.getTrigger();
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Deathmist Raptor")) {
return true;
}
if (wrapper.isMandatory()) {
return true;
}

View File

@@ -651,6 +651,21 @@ public class SpecialCardAi {
}
}
// Grothama, All-Devouring
public static class GrothamaAllDevouring {
public static boolean consider(final Player ai, final SpellAbility sa) {
final Card source = sa.getHostCard();
final Card devourer = AbilityUtils.getDefinedCards(source, sa.getParam("ExtraDefined"), sa).getFirst(); // maybe just getOriginalHost()?
if (ai.getTeamMates(true).contains(devourer.getController())) {
return false; // TODO: Currently, the AI doesn't ever fight its own (or allied) Grothama for card draw. This can be improved.
}
final Card fighter = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa).getFirst();
boolean goodTradeOrNoTrade = devourer.canBeDestroyed() && (devourer.getNetPower() < fighter.getNetToughness() || !fighter.canBeDestroyed()
|| ComputerUtilCard.evaluateCreature(devourer) > ComputerUtilCard.evaluateCreature(fighter));
return goodTradeOrNoTrade && fighter.getNetPower() >= devourer.getNetToughness();
}
}
// Guilty Conscience
public static class GuiltyConscience {
public static Card getBestAttachTarget(final Player ai, final SpellAbility sa, final List<Card> list) {

View File

@@ -28,6 +28,7 @@ import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityStackInstance;
import forge.game.spellability.TargetRestrictions;
import forge.game.staticability.StaticAbilityMustTarget;
import forge.game.zone.ZoneType;
@@ -116,6 +117,8 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (aiLogic != null) {
if (aiLogic.equals("Always")) {
return true;
} else if (aiLogic.startsWith("ExileSpell")) {
return doExileSpellLogic(aiPlayer, sa);
} else if (aiLogic.startsWith("SacAndUpgrade")) { // Birthing Pod, Natural Order, etc.
return doSacAndUpgradeLogic(aiPlayer, sa);
} else if (aiLogic.startsWith("SacAndRetFromGrave")) { // Recurring Nightmare, etc.
@@ -2075,6 +2078,36 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
}
private boolean doExileSpellLogic(final Player aiPlayer, final SpellAbility sa) {
String aiLogic = sa.getParamOrDefault("AILogic", "");
SpellAbilityStackInstance top = aiPlayer.getGame().getStack().peek();
List<ApiType> dangerousApi = Arrays.asList(ApiType.DealDamage, ApiType.DamageAll, ApiType.Destroy, ApiType.DestroyAll, ApiType.Sacrifice, ApiType.SacrificeAll);
int manaCost = 0;
int minCost = 0;
if (aiLogic.contains(".")) {
minCost = Integer.parseInt(aiLogic.substring(aiLogic.indexOf(".") + 1));
}
if (top != null) {
SpellAbility topSA = top.getSpellAbility(false);
if (topSA != null) {
if (topSA.getPayCosts().hasManaCost()) {
manaCost = topSA.getPayCosts().getTotalMana().getCMC();
}
if ((manaCost >= minCost || dangerousApi.contains(topSA.getApi()))
&& topSA.getActivatingPlayer().isOpponentOf(aiPlayer)
&& sa.canTargetSpellAbility(topSA)) {
sa.resetTargets();
sa.getTargets().add(topSA);
return sa.isTargetNumberValid();
}
}
}
return false;
}
private static CardCollection getSafeTargetsIfUnlessCostPaid(Player ai, SpellAbility sa, Iterable<Card> potentialTgts) {
// Determines if the controller of each potential target can negate the ChangeZone effect
// by paying the Unless cost. Returns the list of targets that can be saved that way.

View File

@@ -75,6 +75,9 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
}
return true; // perhaps the opponent(s) had Sigarda, Heron's Grace or another effect giving hexproof in play, still play the creature as 6/6
}
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Deathmist Raptor")) {
return true;
}
return super.doTriggerAINoCost(aiPlayer, sa, mandatory);
}

View File

@@ -112,7 +112,7 @@ public class ControlGainAi extends SpellAbilityAi {
// Don't steal something if I can't Attack without, or prevent it from blocking at least
if (lose.contains("EOT")
&& ai.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
&& game.getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
&& !sa.isTrigger()) {
return false;
}

View File

@@ -144,7 +144,7 @@ public class CounterAi extends SpellAbilityAi {
String logic = sa.getParam("AILogic");
if ("Never".equals(logic)) {
return false;
} else if (logic.startsWith("MinCMC.")) {
} else if (logic.startsWith("MinCMC.")) { // TODO fix Daze and fold into AITgts
int minCMC = Integer.parseInt(logic.substring(7));
if (tgtCMC < minCMC) {
return false;

View File

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

View File

@@ -1,12 +1,6 @@
package forge.ai.ability;
import java.util.List;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat;
import forge.ai.SpellAbilityAi;
import forge.ai.*;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.Card;
@@ -21,6 +15,8 @@ import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType;
import forge.util.MyRandom;
import java.util.List;
public class FightAi extends SpellAbilityAi {
@Override
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
@@ -32,9 +28,9 @@ public class FightAi extends SpellAbilityAi {
sa.resetTargets();
final Card source = sa.getHostCard();
// everything is defined or targeted above, can't do anything there?
// 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 or Grothama
// TODO extend Logic for cards like Arena
return true;
}
@@ -120,7 +116,12 @@ public class FightAi extends SpellAbilityAi {
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
if (canPlayAI(ai, sa)) {
final String aiLogic = sa.getParamOrDefault("AILogic", "");
if (aiLogic.equals("Grothama")) {
return mandatory ? true : SpecialCardAi.GrothamaAllDevouring.consider(ai, sa);
}
if (checkApiLogic(ai, sa)) {
return true;
}
if (!mandatory) {

View File

@@ -114,13 +114,14 @@ public class SacrificeAi extends SpellAbilityAi {
}
final String defined = sa.getParamOrDefault("Defined", "You");
final String targeted = sa.getParamOrDefault("ValidTgts", "");
final String valid = sa.getParamOrDefault("SacValid", "Self");
if (valid.equals("Self")) {
// Self Sacrifice.
} else if (defined.equals("Player")
} else if (defined.equals("Player") || targeted.equals("Player") || targeted.equals("Opponent")
|| ((defined.equals("Player.Opponent") || defined.equals("Opponent")) && !sa.isTrigger())) {
// is either "Defined$ Player.Opponent" or "Defined$ Opponent" obsolete?
// If Sacrifice hits both players:
// Only cast it if Human has the full amount of valid
// Only cast it if AI doesn't have the full amount of Valid

View File

@@ -223,7 +223,7 @@ public class GameAction {
// Don't copy Tokens, copy only cards leaving the battlefield
// and returning to hand (to recreate their spell ability information)
if (suppress || toBattlefield) {
if (toBattlefield || (suppress && zoneTo.getZoneType().isHidden())) {
copied = c;
if (lastKnownInfo == null) {
@@ -295,7 +295,6 @@ public class GameAction {
}
copied.setUnearthed(c.isUnearthed());
copied.setTapped(false);
// need to copy counters when card enters another zone than hand or library
if (lastKnownInfo.hasKeyword("Counters remain on CARDNAME as it moves to any zone other than a player's hand or library.") &&
@@ -386,7 +385,7 @@ public class GameAction {
}
}
if (!zoneTo.is(ZoneType.Stack) && !suppress) {
if (!zoneTo.is(ZoneType.Stack)) {
// reset timestamp in changezone effects so they have same timestamp if ETB simultaneously
copied.setTimestamp(game.getNextTimestamp());
}
@@ -600,7 +599,7 @@ public class GameAction {
}
// only now that the LKI preserved it
if (!zoneTo.is(ZoneType.Exile) && !zoneTo.is(ZoneType.Stack)) {
if (!zoneTo.is(ZoneType.Stack)) {
c.cleanupExiledWith();
}
@@ -636,7 +635,6 @@ public class GameAction {
}
game.getTriggerHandler().runTrigger(TriggerType.ChangesController, runParams2, false);
}
// AllZone.getStack().chooseOrderOfSimultaneousStackEntryAll();
if (suppress) {
game.getTriggerHandler().clearSuppression(TriggerType.ChangesZone);
@@ -923,10 +921,6 @@ public class GameAction {
return result;
}
public final Card exile(final Card c, SpellAbility cause, Map<AbilityKey, Object> params) {
if (game.isCardExiled(c)) {
return c;
}
final Zone origin = c.getZone();
final PlayerZone removed = c.getOwner().getZone(ZoneType.Exile);
final Card copied = moveTo(removed, c, cause, params);

View File

@@ -561,9 +561,6 @@ public class AbilityUtils {
}
} else if (calcX[0].equals("OriginalHost")) {
val = xCount(ability.getOriginalHost(), calcX[1], ability);
} else if (calcX[0].equals("LastStateBattlefield") && ability instanceof SpellAbility) {
Card c = ((SpellAbility) ability).getLastStateBattlefield().get(card);
val = c == null ? 0 : xCount(c, calcX[1], ability);
} else if (calcX[0].startsWith("ExiledWith")) {
val = handlePaid(card.getExiledCards(), calcX[1], card, ability);
} else if (calcX[0].startsWith("Convoked")) {

View File

@@ -393,11 +393,16 @@ public abstract class SpellAbilityEffect {
}
public static void addForgetOnMovedTrigger(final Card card, final String zone) {
String trig = "Mode$ ChangesZone | ValidCard$ Card.IsRemembered | Origin$ " + zone + " | ExcludedDestinations$ Stack | Destination$ Any | TriggerZones$ Command | Static$ True";
String trig = "Mode$ ChangesZone | ValidCard$ Card.IsRemembered | Origin$ " + zone + " | ExcludedDestinations$ Stack,Exile | Destination$ Any | TriggerZones$ Command | Static$ True";
String trig2 = "Mode$ Exiled | ValidCard$ Card.IsRemembered | ValidCause$ SpellAbility.!EffectSource | TriggerZones$ Command | Static$ True";
final Trigger parsedTrigger = TriggerHandler.parseTrigger(trig, card, true);
parsedTrigger.setOverridingAbility(getForgetSpellAbility(card));
final Trigger parsedTrigger2 = TriggerHandler.parseTrigger(trig2, card, true);
SpellAbility forget = getForgetSpellAbility(card);
parsedTrigger.setOverridingAbility(forget);
parsedTrigger2.setOverridingAbility(forget);
card.addTrigger(parsedTrigger);
card.addTrigger(parsedTrigger2);
}
protected static void addForgetOnCastTrigger(final Card card) {

View File

@@ -122,9 +122,8 @@ public class SacrificeEffect extends SpellAbilityEffect {
String [] msgArray = msg.split(" & ");
List<CardCollection> validTargetsList = new ArrayList<>(validArray.length);
for (String subValid : validArray) {
CardCollectionView validTargets = AbilityUtils.filterListByType(battlefield, subValid, sa);
validTargets = CardLists.filter(validTargets, CardPredicates.canBeSacrificedBy(sa, true));
validTargetsList.add(new CardCollection(validTargets));
CardCollection validTargets = CardLists.filter(AbilityUtils.filterListByType(battlefield, subValid, sa), CardPredicates.canBeSacrificedBy(sa, true));
validTargetsList.add(validTargets);
}
CardCollection chosenCards = new CardCollection();
for (int i = 0; i < validArray.length; ++i) {

View File

@@ -523,7 +523,13 @@ public class CardFactoryUtil {
public static List<String> sharedKeywords(final Iterable<String> kw, final String[] restrictions,
final Iterable<ZoneType> zones, final Card host, CardTraitBase ctb) {
final List<String> filteredkw = Lists.newArrayList();
final Player p = host.getController();
Player p = null;
if (ctb instanceof SpellAbility) {
p = ((SpellAbility)ctb).getActivatingPlayer();
}
if (p == null) {
p = host.getController();
}
CardCollectionView cardlist = p.getGame().getCardsIn(zones);
final Set<String> landkw = Sets.newHashSet();
final Set<String> protectionkw = Sets.newHashSet();

View File

@@ -1,14 +1,13 @@
package forge.game.combat;
import java.util.ArrayList;
import java.util.List;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.keyword.Keyword;
import java.util.List;
public class AttackingBand {
private CardCollection attackers = new CardCollection();
private Boolean blocked = null; // even if all blockers were killed before FS or CD, band remains blocked
@@ -26,7 +25,7 @@ public class AttackingBand {
public void addAttacker(Card card) { attackers.add(card); }
public void removeAttacker(Card card) { attackers.remove(card); }
public static boolean isValidBand(List<Card> band, boolean shareDamage) {
public static boolean isValidBand(CardCollectionView band, boolean shareDamage) {
if (band.isEmpty()) {
// An empty band is not a valid band
return false;
@@ -64,7 +63,7 @@ public class AttackingBand {
public boolean canJoinBand(Card card) {
// Trying to join an existing band, attackers should be non-empty and card should exist
List<Card> newBand = new ArrayList<>(attackers);
CardCollection newBand = new CardCollection(attackers);
if (card != null) {
newBand.add(card);
}

View File

@@ -1126,7 +1126,7 @@ public class CombatUtil {
if (!canBlock(blocker, nextTurn)) {
return false;
}
if (isUnblockableFromLandwalk(attacker, blocker.getController())
&& !blocker.hasKeyword("CARDNAME can block creatures with landwalk abilities as though they didn't have those abilities.")) {
return false;

View File

@@ -327,7 +327,6 @@ public class Player extends GameEntity implements Comparable<Player> {
game.getTriggerHandler().suppressMode(TriggerType.ChangesZone);
activeScheme = getZone(ZoneType.SchemeDeck).get(0);
// gameAction moveTo ?
game.getAction().moveTo(ZoneType.Command, activeScheme, null, moveParams);
game.getTriggerHandler().clearSuppression(TriggerType.ChangesZone);
@@ -800,7 +799,7 @@ public class Player extends GameEntity implements Comparable<Player> {
restDamage = 2;
}
} else if (c.getName().equals("Elderscale Wurm")) {
if (c.getController().equals(this) && getLife() - restDamage < 7) {
if (c.getController().equals(this) && getLife() >= 7 && getLife() - restDamage < 7) {
restDamage = getLife() - 7;
if (restDamage < 0) {
restDamage = 0;
@@ -3268,9 +3267,7 @@ public class Player extends GameEntity implements Comparable<Player> {
}
final TriggerHandler triggerHandler = game.getTriggerHandler();
triggerHandler.suppressMode(TriggerType.ChangesZone);
game.getAction().moveTo(ZoneType.Command, initiativeEffect, null, null);
triggerHandler.clearSuppression(TriggerType.ChangesZone);
com.add(initiativeEffect);
triggerHandler.clearActiveTriggers(initiativeEffect, null);
triggerHandler.registerActiveTrigger(initiativeEffect, false);

View File

@@ -231,7 +231,7 @@ public class ReplacementHandler {
chosenRE.setHasRun(true);
hasRun.add(chosenRE);
chosenRE.setOtherChoices(possibleReplacers);
ReplacementResult res = executeReplacement(runParams, chosenRE, decider, game);
ReplacementResult res = executeReplacement(runParams, chosenRE, decider);
if (res == ReplacementResult.NotReplaced) {
if (!possibleReplacers.isEmpty()) {
res = run(event, runParams);
@@ -287,8 +287,7 @@ public class ReplacementHandler {
* the replacement effect to run
*/
private ReplacementResult executeReplacement(final Map<AbilityKey, Object> runParams,
final ReplacementEffect replacementEffect, final Player decider, final Game game) {
final ReplacementEffect replacementEffect, final Player decider) {
SpellAbility effectSA = null;
Card host = replacementEffect.getHostCard();
@@ -441,7 +440,7 @@ public class ReplacementHandler {
int damage = (int) runParams.get(AbilityKey.DamageAmount);
Map<String, String> mapParams = re.getMapParams();
ReplacementResult res = executeReplacement(runParams, re, decider, game);
ReplacementResult res = executeReplacement(runParams, re, decider);
GameEntity newTarget = (GameEntity) runParams.get(AbilityKey.Affected);
int newDamage = (int) runParams.get(AbilityKey.DamageAmount);

View File

@@ -201,6 +201,11 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
this.lastStateGraveyard = new CardCollection(lastStateGraveyard);
}
public void clearLastState() {
lastStateBattlefield = null;
lastStateGraveyard = null;
}
protected SpellAbility(final Card iSourceCard, final Cost toPay) {
this(iSourceCard, toPay, null);
}
@@ -828,10 +833,6 @@ public abstract class SpellAbility extends CardTraitBase implements ISpellAbilit
if (isActivatedAbility()) {
setXManaCostPaid(null);
}
// reset last state when finished resolving
setLastStateBattlefield(CardCollection.EMPTY);
setLastStateGraveyard(CardCollection.EMPTY);
}
// key for autoyield - the card description (including number) (if there is a card) plus the effect description

View File

@@ -243,7 +243,7 @@ public class SpellAbilityCondition extends SpellAbilityVariables {
if (params.containsKey("ConditionTargetsSingleTarget")) {
this.setTargetsSingleTarget(true);
}
} // setConditions
}
/**
* <p>

View File

@@ -189,7 +189,7 @@ public class SpellAbilityRestriction extends SpellAbilityVariables {
this.setClassLevelOperator(params.get("ClassLevel").substring(0, 2));
this.setClassLevel(params.get("ClassLevel").substring(2));
}
} // end setRestrictions()
}
/**
* <p>

View File

@@ -73,19 +73,8 @@ public class TriggerExiled extends Trigger {
return false;
}
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;
}
return true;

View File

@@ -528,9 +528,6 @@ public class TriggerHandler {
sa = sa.copy(host, controller, false);
}
sa.setLastStateBattlefield(game.getLastStateBattlefield());
sa.setLastStateGraveyard(game.getLastStateGraveyard());
sa.setTrigger(regtrig);
sa.setSourceTrigger(regtrig.getId());
regtrig.setTriggeringObjects(sa, runParams);
@@ -565,7 +562,6 @@ public class TriggerHandler {
//wrapperAbility.setDescription(wrapperAbility.getStackDescription());
//wrapperAbility.setDescription(wrapperAbility.toUnsuppressedString());
wrapperAbility.setLastStateBattlefield(game.getLastStateBattlefield());
if (regtrig.isStatic()) {
wrapperAbility.getActivatingPlayer().getController().playTrigger(host, wrapperAbility, isMandatory);
} else {

View File

@@ -279,11 +279,12 @@ public class TriggerSpellAbilityCastOrCopy extends Trigger {
sa.setTriggeringObject(AbilityKey.LifeAmount, castSA.getAmountLifePaid());
sa.setTriggeringObjectsFrom(
runParams,
AbilityKey.Player,
AbilityKey.Activator,
AbilityKey.CurrentStormCount,
AbilityKey.CurrentCastSpells
);
AbilityKey.CardLKI,
AbilityKey.Player,
AbilityKey.Activator,
AbilityKey.CurrentStormCount,
AbilityKey.CurrentCastSpells
);
}
@Override

View File

@@ -44,7 +44,6 @@ import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardUtil;
import forge.game.event.EventValueChangeType;
import forge.game.event.GameEventCardStatsChanged;
@@ -321,6 +320,14 @@ public class MagicStack /* extends MyObservable */ implements Iterable<SpellAbil
// Copied spells aren't cast per se so triggers shouldn't run for them.
Map<AbilityKey, Object> runParams = AbilityKey.mapFromPlayer(sp.getHostCard().getController());
if (sp.isSpell() && !sp.isCopied()) {
final Card lki = CardUtil.getLKICopy(sp.getHostCard());
runParams.put(AbilityKey.CardLKI, lki);
thisTurnCast.add(lki);
sp.getActivatingPlayer().addSpellCastThisTurn();
}
runParams.put(AbilityKey.Cost, sp.getPayCosts());
runParams.put(AbilityKey.Activator, sp.getActivatingPlayer());
runParams.put(AbilityKey.CastSA, si.getSpellAbility(true));
@@ -462,10 +469,6 @@ public class MagicStack /* extends MyObservable */ implements Iterable<SpellAbil
GameActionUtil.checkStaticAfterPaying(sp.getHostCard());
if (sp.isSpell() && !sp.isCopied()) {
thisTurnCast.add(CardUtil.getLKICopy(sp.getHostCard()));
sp.getActivatingPlayer().addSpellCastThisTurn();
}
if (sp.isActivatedAbility() && sp.isPwAbility()) {
sp.getActivatingPlayer().setActivateLoyaltyAbilityThisTurn(true);
}
@@ -694,8 +697,6 @@ public class MagicStack /* extends MyObservable */ implements Iterable<SpellAbil
frozenStack.remove(si);
game.updateStackForView();
SpellAbility sa = si.getSpellAbility(false);
sa.setLastStateBattlefield(CardCollection.EMPTY);
sa.setLastStateGraveyard(CardCollection.EMPTY);
game.fireEvent(new GameEventSpellRemovedFromStack(sa));
}

View File

@@ -104,6 +104,7 @@ public class Forge implements ApplicationListener {
public static boolean isTabletDevice = false;
public static String locale = "en-US";
public Assets assets;
private ForgePreferences forgePreferences;
public static boolean hdbuttons = false;
public static boolean hdstart = false;
public static boolean isPortraitMode = false;
@@ -145,6 +146,11 @@ public class Forge implements ApplicationListener {
private Forge() {
}
private ForgePreferences getForgePreferences() {
if (forgePreferences == null)
forgePreferences = new ForgePreferences();
return forgePreferences;
}
public static Localizer getLocalizer() {
if (localizer == null)
localizer = Localizer.getInstance();
@@ -179,33 +185,38 @@ public class Forge implements ApplicationListener {
*/
Gdx.input.setCatchKey(Keys.BACK, true);
destroyThis = true; //Prevent back()
ForgePreferences prefs = new ForgePreferences();
if (Files.exists(Paths.get(ForgeConstants.DEFAULT_SKINS_DIR+ForgeConstants.ADV_TEXTURE_BG_FILE)))
selector = prefs.getPref(FPref.UI_SELECTOR_MODE);
selector = getForgePreferences().getPref(FPref.UI_SELECTOR_MODE);
boolean landscapeMode = GuiBase.isAndroid() ? !isPortraitMode : screenWidth > screenHeight;
//update landscape mode preference if it doesn't match what the app loaded as
if (getForgePreferences().getPrefBoolean(FPref.UI_LANDSCAPE_MODE) != landscapeMode) {
getForgePreferences().setPref(FPref.UI_LANDSCAPE_MODE, landscapeMode);
getForgePreferences().save();
}
String skinName;
if (FileUtil.doesFileExist(ForgeConstants.MAIN_PREFS_FILE)) {
skinName = prefs.getPref(FPref.UI_SKIN);
skinName = getForgePreferences().getPref(FPref.UI_SKIN);
} else {
skinName = "default"; //use default skin if preferences file doesn't exist yet
}
FSkin.loadLight(skinName, splashScreen);
textureFiltering = prefs.getPrefBoolean(FPref.UI_LIBGDX_TEXTURE_FILTERING);
showFPS = prefs.getPrefBoolean(FPref.UI_SHOW_FPS);
autoAIDeckSelection = prefs.getPrefBoolean(FPref.UI_AUTO_AIDECK_SELECTION);
altPlayerLayout = prefs.getPrefBoolean(FPref.UI_ALT_PLAYERINFOLAYOUT);
altZoneTabs = prefs.getPrefBoolean(FPref.UI_ALT_PLAYERZONETABS);
animatedCardTapUntap = prefs.getPrefBoolean(FPref.UI_ANIMATED_CARD_TAPUNTAP);
enableUIMask = prefs.getPref(FPref.UI_ENABLE_BORDER_MASKING);
if (prefs.getPref(FPref.UI_ENABLE_BORDER_MASKING).equals("true")) //override old settings if not updated
textureFiltering = getForgePreferences().getPrefBoolean(FPref.UI_LIBGDX_TEXTURE_FILTERING);
showFPS = getForgePreferences().getPrefBoolean(FPref.UI_SHOW_FPS);
autoAIDeckSelection = getForgePreferences().getPrefBoolean(FPref.UI_AUTO_AIDECK_SELECTION);
altPlayerLayout = getForgePreferences().getPrefBoolean(FPref.UI_ALT_PLAYERINFOLAYOUT);
altZoneTabs = getForgePreferences().getPrefBoolean(FPref.UI_ALT_PLAYERZONETABS);
animatedCardTapUntap = getForgePreferences().getPrefBoolean(FPref.UI_ANIMATED_CARD_TAPUNTAP);
enableUIMask = getForgePreferences().getPref(FPref.UI_ENABLE_BORDER_MASKING);
if (getForgePreferences().getPref(FPref.UI_ENABLE_BORDER_MASKING).equals("true")) //override old settings if not updated
enableUIMask = "Full";
else if (prefs.getPref(FPref.UI_ENABLE_BORDER_MASKING).equals("false"))
else if (getForgePreferences().getPref(FPref.UI_ENABLE_BORDER_MASKING).equals("false"))
enableUIMask = "Off";
enablePreloadExtendedArt = prefs.getPrefBoolean(FPref.UI_ENABLE_PRELOAD_EXTENDED_ART);
locale = prefs.getPref(FPref.UI_LANGUAGE);
autoCache = prefs.getPrefBoolean(FPref.UI_AUTO_CACHE_SIZE);
disposeTextures = prefs.getPrefBoolean(FPref.UI_ENABLE_DISPOSE_TEXTURES);
CJK_Font = prefs.getPref(FPref.UI_CJK_FONT);
enablePreloadExtendedArt = getForgePreferences().getPrefBoolean(FPref.UI_ENABLE_PRELOAD_EXTENDED_ART);
locale = getForgePreferences().getPref(FPref.UI_LANGUAGE);
autoCache = getForgePreferences().getPrefBoolean(FPref.UI_AUTO_CACHE_SIZE);
disposeTextures = getForgePreferences().getPrefBoolean(FPref.UI_ENABLE_DISPOSE_TEXTURES);
CJK_Font = getForgePreferences().getPref(FPref.UI_CJK_FONT);
if (autoCache) {
//increase cacheSize for devices with RAM more than 5GB, default is 300. Some phones have more than 10GB RAM (Mi 10, OnePlus 8, S20, etc..)
@@ -379,12 +390,6 @@ public class Forge implements ApplicationListener {
//adjust height modifier
adjustHeightModifier(getScreenWidth(), getScreenHeight());
//update landscape mode preference if it doesn't match what the app loaded as
if (FModel.getPreferences().getPrefBoolean(FPref.UI_LANDSCAPE_MODE) != isLandscapeMode()) {
FModel.getPreferences().setPref(FPref.UI_LANDSCAPE_MODE, isLandscapeMode());
FModel.getPreferences().save();
}
FThreads.invokeInBackgroundThread(() -> FThreads.invokeInEdtLater(() -> {
//load skin full
FSkin.loadFull(splashScreen);

View File

@@ -305,7 +305,7 @@ public class DuelScene extends ForgeScene {
} else if (this.eventData != null){
deck = eventData.nextOpponent.getDeck();
} else {
deck = currentEnemy.copyPlayerDeck ? this.playerDeck : currentEnemy.generateDeck(Current.player().isFantasyMode(), Current.player().isUsingCustomDeck() || Current.player().getDifficulty().name.equalsIgnoreCase("Hard"));
deck = currentEnemy.copyPlayerDeck ? this.playerDeck : currentEnemy.generateDeck(Current.player().isFantasyMode(), Current.player().isUsingCustomDeck() || Current.player().getDifficulty().name.equalsIgnoreCase("Insane") || Current.player().getDifficulty().name.equalsIgnoreCase("Hard"));
}
RegisteredPlayer aiPlayer = RegisteredPlayer.forVariants(playerCount, appliedVariants, deck, null, false, null, null);

View File

@@ -109,6 +109,7 @@ public class MapViewScene extends UIScene {
TextraButton questButton = ui.findActor("quest");
if (questButton != null) {
questButton.setDisabled(labels.isEmpty());
questButton.setVisible(!labels.isEmpty());
}
super.enter();
}

View File

@@ -30,7 +30,6 @@ import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
* Scene to handle settings of the base forge and adventure mode
*/
public class SettingsScene extends UIScene {
static public ForgePreferences Preference;
private final Table settingGroup;
TextraButton backButton;
//TextraButton newPlane;
@@ -99,9 +98,6 @@ public class SettingsScene extends UIScene {
super(Forge.isLandscapeMode() ? "ui/settings.json" : "ui/settings_portrait.json");
settingGroup = new Table();
if (Preference == null) {
Preference = FModel.getPreferences();
}
//temporary disable custom world until it works correctly on each update
/*selectSourcePlane = Controls.newComboBox();
newPlaneName = Controls.newTextField("");
@@ -131,10 +127,10 @@ public class SettingsScene extends UIScene {
mode = "720p";
Graphics.setVideoMode(mode);
//update preference for classic mode if needed
if (Preference.getPref(ForgePreferences.FPref.UI_VIDEO_MODE).equals(mode)) {
Preference.setPref(ForgePreferences.FPref.UI_VIDEO_MODE, mode);
Preference.save();
//update
if (!FModel.getPreferences().getPref(ForgePreferences.FPref.UI_VIDEO_MODE).equalsIgnoreCase(mode)) {
FModel.getPreferences().setPref(ForgePreferences.FPref.UI_VIDEO_MODE, mode);
FModel.getPreferences().save();
}
return null;
});
@@ -194,10 +190,8 @@ public class SettingsScene extends UIScene {
Config.instance().getSettingData().fullScreen = value;
Config.instance().saveSettings();
//update
if (Preference.getPrefBoolean(ForgePreferences.FPref.UI_FULLSCREEN_MODE) != value) {
Preference.setPref(ForgePreferences.FPref.UI_LANDSCAPE_MODE, value);
Preference.save();
}
FModel.getPreferences().setPref(ForgePreferences.FPref.UI_FULLSCREEN_MODE, Config.instance().getSettingData().fullScreen);
FModel.getPreferences().save();
}
});
}
@@ -237,16 +231,18 @@ public class SettingsScene extends UIScene {
addCheckBox(Forge.getLocalizer().getMessage("lblLandscapeMode"), ForgePreferences.FPref.UI_LANDSCAPE_MODE);
addCheckBox(Forge.getLocalizer().getMessage("lblAnimatedCardTapUntap"), ForgePreferences.FPref.UI_ANIMATED_CARD_TAPUNTAP);
if (!GuiBase.isAndroid()) {
final String[] item = {Preference.getPref(ForgePreferences.FPref.UI_ENABLE_BORDER_MASKING)};
final String[] item = {FModel.getPreferences().getPref(ForgePreferences.FPref.UI_ENABLE_BORDER_MASKING)};
SelectBox<String> borderMask = Controls.newComboBox(new String[]{"Off", "Crop", "Full", "Art"}, item[0], o -> {
String mode = (String) o;
if (mode == null)
mode = "Crop";
item[0] = mode;
//update preference for classic mode if needed
Preference.setPref(ForgePreferences.FPref.UI_ENABLE_BORDER_MASKING, mode);
Preference.save();
Forge.enableUIMask = Preference.getPref(ForgePreferences.FPref.UI_ENABLE_BORDER_MASKING);
//update
if (!FModel.getPreferences().getPref(ForgePreferences.FPref.UI_ENABLE_BORDER_MASKING).equalsIgnoreCase(mode)) {
FModel.getPreferences().setPref(ForgePreferences.FPref.UI_ENABLE_BORDER_MASKING, mode);
FModel.getPreferences().save();
Forge.enableUIMask = FModel.getPreferences().getPref(ForgePreferences.FPref.UI_ENABLE_BORDER_MASKING);
}
ImageCache.clearGeneratedCards();
ImageCache.disposeTextures();
return null;
@@ -277,12 +273,12 @@ public class SettingsScene extends UIScene {
private void addInputField(String name, ForgePreferences.FPref pref) {
TextField box = Controls.newTextField("");
box.setText(Preference.getPref(pref));
box.setText(FModel.getPreferences().getPref(pref));
box.addListener(new ChangeListener() {
@Override
public void changed(ChangeEvent event, Actor actor) {
Preference.setPref(pref, ((TextField) actor).getText());
Preference.save();
FModel.getPreferences().setPref(pref, ((TextField) actor).getText());
FModel.getPreferences().save();
}
});
@@ -292,12 +288,12 @@ public class SettingsScene extends UIScene {
private void addCheckBox(String name, ForgePreferences.FPref pref) {
CheckBox box = Controls.newCheckBox("");
box.setChecked(Preference.getPrefBoolean(pref));
box.setChecked(FModel.getPreferences().getPrefBoolean(pref));
box.addListener(new ChangeListener() {
@Override
public void changed(ChangeEvent event, Actor actor) {
Preference.setPref(pref, ((CheckBox) actor).isChecked());
Preference.save();
FModel.getPreferences().setPref(pref, ((CheckBox) actor).isChecked());
FModel.getPreferences().save();
}
});
@@ -307,12 +303,12 @@ public class SettingsScene extends UIScene {
private void addSettingSlider(String name, ForgePreferences.FPref pref, int min, int max) {
Slider slide = Controls.newSlider(min, max, 1, false);
slide.setValue(Preference.getPrefInt(pref));
slide.setValue(FModel.getPreferences().getPrefInt(pref));
slide.addListener(new ChangeListener() {
@Override
public void changed(ChangeEvent event, Actor actor) {
Preference.setPref(pref, String.valueOf((int) ((Slider) actor).getValue()));
Preference.save();
FModel.getPreferences().setPref(pref, String.valueOf((int) ((Slider) actor).getValue()));
FModel.getPreferences().save();
if (ForgePreferences.FPref.UI_VOL_MUSIC.equals(pref))
SoundSystem.instance.refreshVolume();
}

View File

@@ -490,12 +490,17 @@ public class GameHUD extends Stage {
private void setAudio(MusicPlaylist playlist) {
if (playlist.equals(currentAudioPlaylist))
return;
System.out.println("Playlist: "+playlist);
unloadAudio();
System.out.println("Playlist: "+playlist);
audio = getMusic(playlist);
}
private Pair<FileHandle, Music> getMusic(MusicPlaylist playlist) {
FileHandle file = Gdx.files.absolute(playlist.getNewRandomFilename());
String filename = playlist.getNewRandomFilename();
if (filename == null)
return null;
FileHandle file = Gdx.files.absolute(filename);
Music music = Forge.getAssets().getMusic(file);
if (music != null) {
currentAudioPlaylist = playlist;
@@ -776,7 +781,7 @@ public class GameHUD extends Stage {
changeBGM(MusicPlaylist.WHITE);
break;
case "waste":
changeBGM(MusicPlaylist.MENUS);
changeBGM(MusicPlaylist.COLORLESS);
break;
default:
break;

View File

@@ -15,7 +15,7 @@
"maxRoadDistance": 1000,
"biomesNames": [
"world/biomes/base.json",
"world/biomes/waste.json",
"world/biomes/colorless.json",
"world/biomes/white.json",
"world/biomes/blue.json",
"world/biomes/black.json",

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@@ -23,11 +23,8 @@
"height": 0.7,
"color": "110903",
"spriteNames": [
"SwampTree",
"SwampTree2",
"DarkGras",
"Skull",
"SwampRock",
"DarkWood",
"Reed",
"Waterlily",
@@ -120,22 +117,46 @@
"N": 2,
"x": 0.5,
"y": 0.5,
"structureAtlasPath": "world/tilesets/structures.atlas",
"sourcePath": "world/models/swamp_forest.png",
"maskPath": "world/masks/ring.png",
"structureAtlasPath": "world/structures/black_structures.atlas",
"sourcePath": "world/structures/models/black.png",
"maskPath": "world/structures/masks/ring.png",
"height": 0.5,
"width": 0.5,
"symmetry": 8,
"periodicOutput": false,
"mappingInfo": [
{
"name": "swamp_forest",
"color": "007000",
"name": "water",
"color": "00ffff",
"collision": true
},
{
"name": "swamp_water",
"color": "005050",
"name": "tree",
"color": "004000",
"collision": true
},
{
"name": "tree2",
"color": "008000",
"collision": true
},
{
"name": "tree3",
"color": "ff00ff",
"collision": true
},
{
"name": "tree4",
"color": "00f000",
"collision": true
},
{
"name": "rock",
"color": "808080",
"collision": true
},
{
"name": "rock2",
"color": "ff0000",
"collision": true
}
]
@@ -144,27 +165,41 @@
"N": 2,
"x": 0.5,
"y": 0.5,
"structureAtlasPath": "world/tilesets/structures.atlas",
"sourcePath": "world/models/swamp_ruins.png",
"maskPath": "world/masks/circle.png",
"structureAtlasPath": "world/structures/black_structures.atlas",
"sourcePath": "world/structures/models/black.png",
"maskPath": "world/structures/masks/circle.png",
"height": 0.20000002,
"width": 0.20000002,
"symmetry": 1,
"periodicOutput": false,
"symmetry": 8,
"mappingInfo": [
{
"name": "deep_swamp",
"color": "002000",
"name": "muck",
"color": "00ffff",
"collision": true
},
{
"name": "structure",
"color": "505050",
"name": "dead_tree",
"color": "004000",
"collision": true
},
{
"name": "swamp_forest2",
"color": "007000",
"name": "dead_tree2",
"color": "008000",
"collision": true
},
{
"name": "dead_tree3",
"color": "ff00ff",
"collision": true
},
{
"name": "rock",
"color": "808080",
"collision": true
},
{
"name": "rock2",
"color": "ff0000",
"collision": true
}
]

View File

@@ -23,8 +23,6 @@
"height": 0.7,
"color": "10a2e0",
"spriteNames": [
"IslandTree",
"Coral",
"Shell"
],
"enemies": [
@@ -108,47 +106,93 @@
],
"structures": [
{
"N":2,
"x": 0.5,
"y": 0.5,
"structureAtlasPath": "world/tilesets/structures.atlas",
"sourcePath": "world/models/water.png",
"maskPath": "world/masks/circle.png",
"height": 0.20000002,
"width": 0.20000002,
"structureAtlasPath": "world/structures/blue_structures.atlas",
"sourcePath": "world/structures/models/blue.png",
"maskPath": "world/structures/masks/circle.png",
"height": 0.1,
"width": 0.1,
"symmetry": 8,
"periodicOutput": false,
"mappingInfo": [
{
"name": "water",
"color": "0070a0",
"color": "00ffff",
"collision": true
},
{
"name": "island_forest",
"color": "00a000",
"name": "tree",
"color": "00ff00",
"collision": true
},
{
"name": "tree2",
"color": "008000",
"collision": true
},
{
"name": "pineapple",
"color": "ffff00",
"collision": true
},
{
"name": "rock",
"color": "ff8000",
"collision": true
},
{
"name": "rock2",
"color": "804000",
"collision": true
},
{
"name": "rock3",
"color": "402000",
"collision": true
},
{
"name": "rock4",
"color": "201000",
"collision": true
}
]
},
{
"N": 2,
"x": 0.5,
"y": 0.5,
"structureAtlasPath": "world/tilesets/structures.atlas",
"sourcePath": "world/models/island_forest.png",
"maskPath": "world/masks/ring.png",
"structureAtlasPath": "world/structures/blue_structures.atlas",
"sourcePath": "world/structures/models/beach.png",
"maskPath": "world/structures/masks/ring.png",
"height": 0.5,
"width": 0.5,
"symmetry": 8,
"periodicOutput": false,
"mappingInfo": [
{
"name": "water",
"color": "0070a0",
"color": "00ffff",
"collision": true
},
{
"name": "island_forest",
"color": "00a000",
"name": "tree",
"color": "00ff00",
"collision": true
},
{
"name": "tree2",
"color": "008000",
"collision": true
},
{
"name": "dune",
"color": "ff8000",
"collision": true
},
{
"name": "dune2",
"color": "402000",
"collision": true
}
]

View File

@@ -5,15 +5,15 @@
"distWeight": 1,
"name": "waste",
"tilesetAtlas": "world/tilesets/terrain.atlas",
"tilesetName": "Waste",
"tilesetName": "Colorless",
"terrain": [
{
"spriteName": "Waste_1",
"spriteName": "Colorless_1",
"max": 0.2,
"resolution": 5
},
{
"spriteName": "Waste_2",
"spriteName": "Colorless_2",
"min": 0.8,
"max": 1,
"resolution": 5
@@ -23,9 +23,7 @@
"height": 0.85,
"color": "aeaeae",
"spriteNames": [
"WasteTree",
"Stone",
"WasteRock"
"Stone"
],
"enemies": [
"Adept Black Wizard",
@@ -118,44 +116,94 @@
"N": 2,
"x": 0.5,
"y": 0.5,
"structureAtlasPath": "world/tilesets/structures.atlas",
"sourcePath": "world/models/waste_structure.png",
"maskPath": "world/masks/circle.png",
"periodicInput": false,
"height": 0.20000002,
"width": 0.20000002,
"symmetry": 4,
"structureAtlasPath": "world/structures/colorless_structures.atlas",
"sourcePath": "world/structures/models/colorless.png",
"maskPath": "world/structures/masks/circle.png",
"height": 0.25,
"width": 0.25,
"symmetry": 8,
"mappingInfo": [
{
"name": "waste_structure",
"color": "444444",
"name": "crater",
"color": "808080",
"collision": true
},
{
"name": "waste_mountain",
"color": "9a9a9a",
"name": "tree",
"color": "ff0000",
"collision": true
},
{
"name": "tree2",
"color": "00ff00",
"collision": true
},
{
"name": "tree3",
"color": "0000ff",
"collision": true
},
{
"name": "tree4",
"color": "00ffff",
"collision": true
},
{
"name": "rock",
"color": "ff00ff",
"collision": true
},
{
"name": "mountain",
"color": "000000",
"collision": true
}
]
},
{
"N": 2,
"x": 0.5,
"y": 0.5,
"structureAtlasPath": "world/tilesets/structures.atlas",
"sourcePath": "world/models/hole.png",
"maskPath": "world/masks/ring.png",
"structureAtlasPath": "world/structures/colorless_structures.atlas",
"sourcePath": "world/structures/models/colorless.png",
"maskPath": "world/structures/masks/ring.png",
"height": 0.5,
"width": 0.5,
"periodicOutput": false,
"symmetry": 8,
"mappingInfo": [
{
"name": "hole",
"color": "111111",
"color": "808080",
"collision": true
},
{
"name": "waste_mountain",
"color": "9a9a9a",
"name": "tree",
"color": "ff0000",
"collision": true
},
{
"name": "tree2",
"color": "00ff00",
"collision": true
},
{
"name": "tree3",
"color": "0000ff",
"collision": true
},
{
"name": "tree4",
"color": "00ffff",
"collision": true
},
{
"name": "rock",
"color": "ff00ff",
"collision": true
},
{
"name": "mountain",
"color": "000000",
"collision": true
}
]

View File

@@ -23,9 +23,6 @@
"height": 0.7,
"color": "59a650",
"spriteNames": [
"WoodTree",
"WoodTree2",
"Bush",
"Stump",
"Moss",
"Stone",
@@ -122,39 +119,66 @@
"N": 2,
"x": 0.5,
"y": 0.5,
"structureAtlasPath": "world/tilesets/structures.atlas",
"sourcePath": "world/models/forest.png",
"maskPath": "world/masks/circle.png",
"height": 0.20000002,
"width": 0.20000002,
"structureAtlasPath": "world/structures/green_structures.atlas",
"sourcePath": "world/structures/models/green.png",
"maskPath": "world/structures/masks/circle.png",
"height": 0.5,
"width": 0.5,
"symmetry": 1,
"mappingInfo": [
{
"name": "forest",
"color": "007000",
"collision": true
}
]
},
{
"N": 2,
"x": 0.5,
"y": 0.5,
"structureAtlasPath": "world/tilesets/structures.atlas",
"sourcePath": "world/models/lake.png",
"maskPath": "world/masks/ring.png",
"height": 0.5,
"width": 0.5,
"periodicOutput": false,
"mappingInfo": [
{
"name": "lake",
"color": "0070a0",
"name": "water",
"color": "000080",
"collision": true
},
{
"name": "forest2",
"color": "009000",
"name": "tree",
"color": "008000",
"collision": true
},
{
"name": "tree2",
"color": "004000",
"collision": true
},
{
"name": "vine",
"color": "8080ff",
"collision": true
},
{
"name": "tree3",
"color": "00c000",
"collision": true
},
{
"name": "tree4",
"color": "00f000",
"collision": true
},
{
"name": "tree5",
"color": "006000",
"collision": true
},
{
"name": "rock",
"color": "808080",
"collision": true
},
{
"name": "mountain",
"color": "ff0000",
"collision": true
},
{
"name": "plant",
"color": "800000",
"collision": true
},
{
"name": "bush",
"color": "ff8080",
"collision": true
}
]

View File

@@ -23,8 +23,7 @@
"height": 1,
"color": "110903",
"spriteNames": [
"Skull",
"PlainsRock"
"Skull"
],
"enemies": [
"Ammit",
@@ -60,9 +59,9 @@
"N": 2,
"x": 0.5,
"y": 0.5,
"structureAtlasPath": "world/tilesets/structures.atlas",
"sourcePath": "world/models/mountain.png",
"maskPath": "world/masks/circle.png",
"structureAtlasPath": "world/structures/structures.atlas",
"sourcePath": "world/structures/models/mountain.png",
"maskPath": "world/structures/masks/circle.png",
"height": 0.5,
"width": 0.5,
"symmetry": 8,

View File

@@ -31,7 +31,7 @@
"N": 2,
"x": 0.5,
"y": 0.5,
"structureAtlasPath": "world/tilesets/structures.atlas",
"structureAtlasPath": "world/structures/structures.atlas",
"sourcePath": "world/models/fill.png",
"maskPath": "world/masks/fill.png",
"height": 0.99,

View File

@@ -45,9 +45,9 @@
"N": 2,
"x": 0.5,
"y": 0.5,
"structureAtlasPath": "world/tilesets/structures.atlas",
"sourcePath": "world/models/mountain.png",
"maskPath": "world/masks/circle.png",
"structureAtlasPath": "world/structures/structures.atlas",
"sourcePath": "world/structures/models/mountain.png",
"maskPath": "world/structures/masks/circle.png",
"height": 0.5,
"width": 0.5,
"periodicOutput": false,

View File

@@ -23,9 +23,6 @@
"height": 0.7,
"color": "b63729",
"spriteNames": [
"MountainTree",
"MountainTree2",
"MountainRock",
"Gravel"
],
"enemies": [
@@ -127,21 +124,41 @@
"N": 2,
"x": 0.5,
"y": 0.5,
"structureAtlasPath": "world/tilesets/structures.atlas",
"sourcePath": "world/models/mountain.png",
"maskPath": "world/masks/ring.png",
"structureAtlasPath": "world/structures/red_structures.atlas",
"sourcePath": "world/structures/models/red.png",
"maskPath": "world/structures/masks/ring.png",
"height": 0.5,
"width": 0.5,
"periodicOutput": false,
"symmetry": 8,
"mappingInfo": [
{
"name": "mountain",
"color": "a07020",
"color": "ff0000",
"collision": true
},
{
"name": "mountain_forest",
"color": "007000",
"name": "tree",
"color": "00ff00",
"collision": true
},
{
"name": "tree2",
"color": "00ffff",
"collision": true
},
{
"name": "tree3",
"color": "0000ff",
"collision": true
},
{
"name": "tree4",
"color": "ff00ff",
"collision": true
},
{
"name": "rock",
"color": "ffff00",
"collision": true
}
]
@@ -149,15 +166,37 @@
{
"x": 0.5,
"y": 0.5,
"structureAtlasPath": "world/tilesets/structures.atlas",
"sourcePath": "world/models/lava.png",
"maskPath": "world/masks/circle.png",
"structureAtlasPath": "world/structures/red_structures.atlas",
"sourcePath": "world/structures/models/volcano.png",
"maskPath": "world/structures/masks/circle.png",
"height": 0.2,
"width": 0.2,
"N": 2,
"symmetry": 8,
"mappingInfo": [
{
"name": "lava",
"color": "ff5000",
"color": "ffff00",
"collision": true
},
{
"name": "mountain",
"color": "ff0000",
"collision": true
},
{
"name": "dead_tree",
"color": "000000",
"collision": true
},
{
"name": "dead_tree2",
"color": "808080",
"collision": true
},
{
"name": "rock",
"color": "0000ff",
"collision": true
}
]

View File

@@ -23,7 +23,6 @@
"height": 0.5,
"color": "efe697",
"spriteNames": [
"PlainsRock",
"Skull"
],
"enemies": [

View File

@@ -23,9 +23,6 @@
"height": 0.7,
"color": "efe697",
"spriteNames": [
"PlainsTree",
"Cactus",
"PlainsRock",
"DarkGras"
],
"enemies": [
@@ -117,33 +114,74 @@
"N": 2,
"x": 0.5,
"y": 0.5,
"structureAtlasPath": "world/tilesets/structures.atlas",
"sourcePath": "world/models/plains_forest.png",
"maskPath": "world/masks/circle.png",
"structureAtlasPath": "world/structures/white_structures.atlas",
"sourcePath": "world/structures/models/white.png",
"maskPath": "world/structures/masks/circle.png",
"height": 0.20000002,
"width": 0.20000002,
"symmetry": 8,
"mappingInfo": [
{
"name": "plains_forest",
"color": "9c4000",
"name": "tree",
"color": "ff8000",
"collision": true
},
{
"name": "tree2",
"color": "008000",
"collision": true
},
{
"name": "tree3",
"color": "00ff00",
"collision": true
}
]
},
{
"N": 2,
"x": 0.5,
"y": 0.5,
"structureAtlasPath": "world/tilesets/structures.atlas",
"sourcePath": "world/models/plateau.png",
"maskPath": "world/masks/ring.png",
"symmetry": 8,
"structureAtlasPath": "world/structures/white_structures.atlas",
"sourcePath": "world/structures/models/desert.png",
"maskPath": "world/structures/masks/ring.png",
"height": 0.5,
"width": 0.5,
"periodicOutput": false,
"mappingInfo": [
{
"name": "plateau",
"color": "caaa66",
"color": "804000",
"collision": true
},
{
"name": "rock",
"color": "402000",
"collision": true
},
{
"name": "mesa",
"color": "201000",
"collision": true
},
{
"name": "plateau",
"color": "804000",
"collision": true
},
{
"name": "cactus",
"color": "00ff00",
"collision": true
},
{
"name": "cactus2",
"color": "008000",
"collision": true
},
{
"name": "cactus3",
"color": "004000",
"collision": true
}
]

View File

@@ -6369,9 +6369,7 @@
"IdentityBlack",
"IdentityGreen",
"IdentityAbzan",
"BiomeColorless",
"BiomeWhite",
"BiomeBlack"
"BiomeBlue"
]
},
{
@@ -7242,7 +7240,7 @@
]
}
],
"colors": "W",
"colors": "GW",
"questTags": [
"Elephant",
"Beast",
@@ -12544,7 +12542,7 @@
"spawnRate": 1,
"difficulty": 0.1,
"speed": 25,
"scale": 0.6,
"scale": 1.3,
"life": 11,
"rewards": [
{

View File

@@ -5,289 +5,382 @@ filter: Nearest,Nearest
repeat: none
DarkWood
xy: 0, 0
size: 16, 16
size: 16, 16
DarkWood
xy: 16, 0
size: 16, 16
size: 16, 16
DarkWood
xy: 32, 0
size: 16, 16
size: 16, 16
DarkWood
xy: 48, 0
size: 16, 16
size: 16, 16
Reed
xy: 64, 0
size: 16, 16
size: 16, 16
Reed
xy: 80, 0
size: 16, 16
Reed
xy: 64, 16
size: 16, 16
Reed
xy: 80, 16
size: 16, 16
size: 16, 16
DarkWood
xy: 96, 0
size: 16, 16
size: 16, 16
DarkWood
xy: 112, 0
size: 16, 16
Waterlily
xy: 96, 16
size: 16, 16
Waterlily
xy: 112, 16
size: 16, 16
Shroom
xy: 96, 32
size: 16, 16
Shroom
xy: 96, 48
size: 16, 16
Shroom2
xy: 112, 32
size: 16, 16
Shroom2
xy: 112, 48
size: 16, 16
size: 16, 16
DarkWood
xy: 0, 16
size: 16, 16
size: 16, 16
DarkWood
xy: 16, 16
size: 16, 16
size: 16, 16
DarkWood
xy: 32, 16
size: 16, 16
size: 16, 16
DarkWood
xy: 48, 16
size: 16, 16
size: 16, 16
Reed
xy: 64, 16
size: 16, 16
Reed
xy: 80, 16
size: 16, 16
Waterlily
xy: 96, 16
size: 16, 16
Waterlily
xy: 112, 16
size: 16, 16
DarkGras
xy: 0, 32
size: 16, 16
size: 16, 16
DarkGras
xy: 16, 32
size: 16, 16
DarkGras
xy: 0, 48
size: 16, 16
DarkGras
xy: 16, 48
size: 16, 16
size: 16, 16
Stone
xy: 32, 32
size: 16, 16
size: 16, 16
Stone
xy: 48, 32
size: 16, 16
Stone
xy: 32, 48
size: 16, 16
Stone
xy: 48, 48
size: 16, 16
size: 16, 16
Gravel
xy: 64, 32
size: 16, 16
size: 16, 16
Gravel
xy: 80, 32
size: 16, 16
size: 16, 16
Shroom
xy: 96, 32
size: 16, 16
Shroom2
xy: 112, 32
size: 16, 16
DarkGras
xy: 0, 48
size: 16, 16
DarkGras
xy: 16, 48
size: 16, 16
Shroom
xy: 96, 48
size: 16, 16
Shroom2
xy: 112, 48
size: 16, 16
Stone
xy: 32, 48
size: 16, 16
Stone
xy: 48, 48
size: 16, 16
Gravel
xy: 64, 48
size: 16, 16
size: 16, 16
Gravel
xy: 80, 48
size: 16, 16
size: 16, 16
Flower
xy: 0, 64
size: 16, 16
size: 16, 16
Flower
xy: 16, 64
size: 16, 16
Flower
xy: 0, 80
size: 16, 16
Flower
xy: 16, 80
size: 16, 16
size: 16, 16
Stone
xy: 32, 64
size: 16, 16
size: 16, 16
Stone
xy: 48, 64
size: 16, 16
Stone
xy: 32, 80
size: 16, 16
Stone
xy: 48, 80
size: 16, 16
size: 16, 16
Moss
xy: 64, 64
size: 16, 16
size: 16, 16
Moss
xy: 80, 64
size: 16, 16
Moss
xy: 64, 80
size: 16, 16
Moss
xy: 80, 80
size: 16, 16
size: 16, 16
Wood
xy: 96, 64
size: 16, 16
size: 16, 16
Wood
xy: 112, 64
size: 16, 16
size: 16, 16
Flower
xy: 0, 80
size: 16, 16
Flower
xy: 16, 80
size: 16, 16
Stone
xy: 32, 80
size: 16, 16
Stone
xy: 48, 80
size: 16, 16
Moss
xy: 64, 80
size: 16, 16
Moss
xy: 80, 80
size: 16, 16
Wood
xy: 96, 80
size: 16, 16
size: 16, 16
Wood
xy: 112, 80
size: 16, 16
size: 16, 16
WasteTree
xy: 0, 96
size: 16, 16
size: 16, 16
WasteTree
xy: 16, 96
size: 16, 16
size: 16, 16
WasteTree
xy: 32, 96
size: 16, 16
size: 16, 16
WasteRock
xy: 48, 96
size: 16, 16
size: 16, 16
WasteRock
xy: 64, 96
size: 16, 16
SwampTree
xy: 0, 112
size: 16, 16
SwampTree
xy: 16, 112
size: 16, 16
SwampTree
xy: 32, 112
size: 16, 16
Skull
xy: 48, 112
size: 16, 16
Skull
xy: 112, 144
size: 16, 16
Skull
xy: 112, 128
size: 16, 16
SwampRock
xy: 64, 112
size: 16, 16
SwampRock
xy: 80, 112
size: 16, 16
SwampTree2
xy: 96, 112
size: 16, 16
SwampTree2
xy: 112, 112
size: 16, 16
size: 16, 16
Placeholder
xy: 80, 96
size: 16,16
SwampTree2
xy: 96, 96
size: 16, 16
size: 16, 16
SwampTree2
xy: 112, 96
size: 16, 16
size: 16, 16
SwampTree
xy: 0, 112
size: 16, 16
SwampTree
xy: 16, 112
size: 16, 16
SwampTree
xy: 32, 112
size: 16, 16
Skull
xy: 48, 112
size: 16, 16
SwampRock
xy: 64, 112
size: 16, 16
SwampRock
xy: 80, 112
size: 16, 16
SwampTree2
xy: 96, 112
size: 16, 16
SwampTree2
xy: 112, 112
size: 16, 16
PlainsTree
xy: 0, 128
size: 16, 16
size: 16, 16
PlainsTree
xy: 16, 128
size: 16, 16
size: 16, 16
Cactus
xy: 32, 128
size: 16, 16
size: 16, 16
Cactus
xy: 48, 128
size: 16, 16
size: 16, 16
Cactus
xy: 64, 128
size: 16, 16
size: 16, 16
PlainsRock
xy: 70, 128
size: 16, 16
xy: 80, 128
size: 16, 16
PlainsRock
xy: 96, 128
size: 16, 16
size: 16, 16
Skull
xy: 112, 128
size: 16, 16
IslandTree
xy: 0, 144
size: 16, 16
size: 16, 16
IslandTree
xy: 16, 144
size: 16, 16
size: 16, 16
Coral
xy: 32, 144
size: 16, 16
size: 16, 16
Shell
xy: 48, 144
size: 16, 16
size: 16, 16
Shell
xy: 64, 144
size: 16, 16
size: 16, 16
Placeholder
xy: 80, 144
size: 16, 16
Placeholder
xy: 96, 144
size: 16, 16
Skull
xy: 112, 144
size: 16, 16
WoodTree
xy: 0, 160
size: 16, 16
size: 16, 16
WoodTree
xy: 16, 160
size: 16, 16
size: 16, 16
WoodTree
xy: 32, 160
size: 16, 16
size: 16, 16
WoodTree
xy: 48, 160
size: 16, 16
size: 16, 16
WoodTree2
xy: 64, 160
size: 16, 16
size: 16, 16
WoodTree2
xy: 80, 160
size: 16, 16
size: 16, 16
Bush
xy: 96, 160
size: 16, 16
size: 16, 16
Stump
xy: 112, 160
size: 16, 16
size: 16, 16
MountainTree
xy: 0, 176
size: 16, 16
size: 16, 16
MountainTree
xy: 16, 176
size: 16, 16
size: 16, 16
MountainTree2
xy: 32, 176
size: 16, 16
size: 16, 16
MountainTree2
xy: 48, 176
size: 16, 16
MountainTree2
xy: 96, 176
size: 16, 16
MountainTree2
xy: 112, 176
size: 16, 16
size: 16, 16
MountainRock
xy: 64, 176
size: 16, 16
size: 16, 16
MountainRock
xy: 80, 176
size: 16, 16
size: 16, 16
MountainTree2
xy: 96, 176
size: 16, 16
MountainTree2
xy: 112, 176
size: 16, 16
WoodTree
xy: 0, 192
size: 16, 16
AutumnTree
xy: 16, 192
size: 16, 16
WinterTree
xy: 32, 192
size: 16, 16
AutumnTree
xy: 48, 192
size: 16, 16
Coral
xy: 64, 192
size: 16, 16
SnowMountain
xy: 80, 192
size: 16, 16
Coral
xy: 96, 192
size: 16, 16
AutumnTree
xy: 112, 192
size: 16, 16
Placeholder
xy: 0, 208
size: 16, 16
AutumnTree
xy: 16, 208
size: 16, 16
SwampTree
xy: 32, 208
size: 16, 16
Coral
xy: 48, 208
size: 16, 16
WoodTree
xy: 64, 208
size: 16, 16
IslandRock
xy: 80, 208
size: 16, 16
WoodRock
xy: 96, 208
size: 16, 16
Placeholder
xy: 112, 208
size: 16, 16
LargeWoodRock
xy: 0, 224
size: 32, 32
LargeIslandRock
xy: 32, 224
size: 32, 32
LargeWasteRock
xy: 64, 224
size: 32, 32
LargeMountainRock
xy: 96, 224
size: 32, 32
size: 32, 32
LargePlainsRock
xy: 96, 256
size: 32, 32
size: 32, 32
Placeholder
xy: 0, 256
size: 16, 16
WasteRock
xy: 16, 256
size: 16, 16
LargeSwampRock
xy: 32, 256
size: 32, 32
size: 32, 32
PlainsRock
xy: 64, 256
size: 16, 16
PlainsRock
xy: 80, 256
size: 16, 16
LargePlainsRock
xy: 96, 256
size: 32, 32
WoodRock
xy: 0, 272
size: 16, 16
SwampRock
xy: 16, 272
size: 16, 16
WinterTree:
xy: 64, 272
size: 16, 16
WinterTree:
xy: 80, 272
size: 16, 16

View File

@@ -2,7 +2,7 @@
"textureAtlas":"world/sprites/map_sprites.atlas",
"sprites":[
{
"name":"DarkWood",
"name":"DarkWood",
"startArea":0.2,
"endArea":0.7,
"layer":-1,
@@ -22,7 +22,7 @@
"layer":-1,
"density":0.03
},{
"name":"Reed",
"name":"Reed",
"startArea":0.9,
"endArea":0.99,
"layer":0,
@@ -46,7 +46,7 @@
"name":"Stone",
"startArea":0.2,
"endArea":0.7,
"layer":-1,
"layer":-1,
"resolution" :5,
"density":0.01
},{
@@ -79,21 +79,21 @@
"name":"WasteTree",
"startArea":0.0,
"endArea":0.2,
"layer":0,
"layer":1,
"resolution" :10,
"density":0.7
},{
"name":"WasteRock",
"startArea":0.8,
"endArea":1.0,
"layer":0,
"layer":1,
"resolution" :10,
"density":0.5
},{
"name":"SwampTree",
"startArea":0.8,
"endArea":1.0,
"layer":0,
"layer":1,
"resolution" :10,
"density":0.5
},{
@@ -106,45 +106,45 @@
"name":"SwampRock",
"startArea":0.5,
"endArea":0.6,
"layer":0,
"layer":1,
"density":0.1
},{
"name":"SwampTree2",
"startArea":0.0,
"endArea":0.2,
"layer":0,
"layer":1,
"resolution" :10,
"density":0.7
},{
"name":"PlainsTree",
"startArea":0.0,
"endArea":0.2,
"layer":0,
"layer":1,
"resolution" :10,
"density":0.7
},{
"name":"Cactus",
"startArea":0.5,
"layer":0,
"layer":1,
"endArea":0.7,
"density":0.06
},{
"name":"PlainsRock",
"startArea":0.7,
"layer":0,
"layer":1,
"endArea":0.99,
"density":0.06
},{
"name":"IslandTree",
"startArea":0.0,
"endArea":0.2,
"layer":0,
"layer":1,
"resolution" :10,
"density":0.7
},{
"name":"Coral",
"startArea":0.0,
"layer":0,
"layer":1,
"endArea":0.9,
"density":0.01
},{
@@ -157,65 +157,65 @@
"name":"WoodTree",
"startArea":0.0,
"endArea":0.2,
"layer":0,
"layer":1,
"resolution" :10,
"density":0.7
},{
"name":"WoodTree2",
"startArea":0.8,
"endArea":0.99,
"layer":0,
"layer":1,
"resolution" :5,
"density":0.7
},{
"name":"Bush",
"startArea":0.0,
"endArea":0.2,
"layer":0,
"layer":1,
"resolution" :5,
"density":0.4
},{
"name":"Stump",
"startArea":0.0,
"layer":0,
"layer":-1,
"endArea":0.9,
"density":0.01
},{
"name":"MountainTree",
"startArea":0.0,
"endArea":0.2,
"layer":0,
"layer":1,
"resolution" :5,
"density":0.7
},{
"name":"MountainTree2",
"startArea":0.8,
"endArea":0.99,
"layer":0,
"layer":1,
"resolution" :5,
"density":0.7
},{
"name":"MountainRock",
"startArea":0.1,
"layer":0,
"layer":1,
"endArea":0.9,
"density":0.08
},{
"name":"LargeMountainRock",
"startArea":0.0,
"layer":0,
"layer":1,
"endArea":0.9,
"density":0.02
},{
"name":"LargePlainsRock",
"startArea":0.0,
"layer":0,
"layer":1,
"endArea":0.9,
"density":0.01
},{
"name":"LargeSwampRock",
"startArea":0.0,
"layer":0,
"layer":1,
"endArea":0.9,
"density":0.01
}

View File

@@ -0,0 +1,38 @@
black_structures.png
size: 192,192
format: RGBA8888
filter: Nearest,Nearest
repeat: none
water
xy: 0, 0
size: 48, 64
muck
xy: 48,0
size:48,64
tree
xy: 96,0
size: 48,64
tree2
xy:144,0
size:48,64
dead_tree
xy: 0,64
size: 48,64
dead_tree2
xy: 48,64
size: 48,64
tree3
xy: 96,64
size: 48,64
tree4
xy:144,64
size:48,64
rock
xy: 0, 128
size: 48, 64
rock2
xy: 48, 128
size: 48,64
dead_tree3
xy:96,128
size:48,64

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@@ -0,0 +1,34 @@
blue_structures.png
size: 192,192
format: RGBA8888
filter: Nearest,Nearest
repeat: none
water
xy: 0, 0
size: 48, 64
tree
xy: 48,0
size:48,64
tree2
xy: 96,0
size: 48,64
rock
xy: 0,64
size: 48,64
rock2
xy: 48,64
size: 48,64
pineapple
xy: 96,64
size: 48,64
rock3
xy: 0, 128
size: 48, 64
rock4
xy: 48, 128
size: 48,64
dune
xy:96,128
size:48,64
dune2
xy:144,128

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@@ -0,0 +1,29 @@
colorless_structures.png
size: 144,192
format: RGBA8888
filter: Nearest,Nearest
repeat: none
hole
xy: 0, 0
size: 48, 64
crater
xy: 48,0
size:48,64
tree
xy: 96,0
size: 48,64
tree2
xy: 0,64
size: 48,64
tree3
xy: 48,64
size: 48,64
tree4
xy: 96,64
size: 48,64
rock
xy: 0, 128
size: 48, 64
mountain
xy: 48, 128
size: 48,64

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@@ -0,0 +1,41 @@
green_structures.png
size: 192,192
format: RGBA8888
filter: Nearest,Nearest
repeat: none
water
xy: 0, 0
size: 48, 64
tree
xy: 48,0
size:48,64
tree2
xy: 96,0
size: 48,64
vine
xy: 144,0
size: 48,64
tree3
xy: 0,64
size: 48,64
tree4
xy: 48,64
size: 48,64
tree5
xy: 96,64
size: 48,64
tree6
xy: 144, 64
size: 48,64
rock
xy: 0, 128
size: 48, 64
mountain
xy: 48, 128
size: 48,64
plant
xy:96,128
size:48,64
bush
xy:144,128
size:48,64

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View File

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

Before

Width:  |  Height:  |  Size: 508 B

After

Width:  |  Height:  |  Size: 508 B

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 B

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 B

View File

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

Before

Width:  |  Height:  |  Size: 508 B

After

Width:  |  Height:  |  Size: 508 B

View File

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 B

View File

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

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