package net.demilich.metastone.game.logic;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ThreadLocalRandom;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.demilich.metastone.BuildConfig;
import net.demilich.metastone.game.Attribute;
import net.demilich.metastone.game.Environment;
import net.demilich.metastone.game.GameContext;
import net.demilich.metastone.game.Player;
import net.demilich.metastone.game.actions.ActionType;
import net.demilich.metastone.game.actions.BattlecryAction;
import net.demilich.metastone.game.actions.GameAction;
import net.demilich.metastone.game.actions.PlaySpellCardAction;
import net.demilich.metastone.game.cards.Card;
import net.demilich.metastone.game.cards.CardCatalogue;
import net.demilich.metastone.game.cards.CardCollection;
import net.demilich.metastone.game.cards.CardType;
import net.demilich.metastone.game.cards.QuestCard;
import net.demilich.metastone.game.cards.SecretCard;
import net.demilich.metastone.game.cards.SpellCard;
import net.demilich.metastone.game.cards.costmodifier.CardCostModifier;
import net.demilich.metastone.game.entities.Actor;
import net.demilich.metastone.game.entities.Entity;
import net.demilich.metastone.game.entities.EntityType;
import net.demilich.metastone.game.entities.heroes.Hero;
import net.demilich.metastone.game.entities.heroes.HeroClass;
import net.demilich.metastone.game.entities.minions.Minion;
import net.demilich.metastone.game.entities.minions.Permanent;
import net.demilich.metastone.game.entities.minions.Race;
import net.demilich.metastone.game.entities.minions.Summon;
import net.demilich.metastone.game.entities.weapons.Weapon;
import net.demilich.metastone.game.events.AfterPhysicalAttackEvent;
import net.demilich.metastone.game.events.AfterSpellCastedEvent;
import net.demilich.metastone.game.events.AfterSummonEvent;
import net.demilich.metastone.game.events.ArmorGainedEvent;
import net.demilich.metastone.game.events.BeforeSummonEvent;
import net.demilich.metastone.game.events.BoardChangedEvent;
import net.demilich.metastone.game.events.CardPlayedEvent;
import net.demilich.metastone.game.events.DamageEvent;
import net.demilich.metastone.game.events.DiscardEvent;
import net.demilich.metastone.game.events.DrawCardEvent;
import net.demilich.metastone.game.events.EnrageChangedEvent;
import net.demilich.metastone.game.events.GameEvent;
import net.demilich.metastone.game.events.GameStartEvent;
import net.demilich.metastone.game.events.HealEvent;
import net.demilich.metastone.game.events.HeroPowerUsedEvent;
import net.demilich.metastone.game.events.JoustEvent;
import net.demilich.metastone.game.events.KillEvent;
import net.demilich.metastone.game.events.OverloadEvent;
import net.demilich.metastone.game.events.PhysicalAttackEvent;
import net.demilich.metastone.game.events.PreDamageEvent;
import net.demilich.metastone.game.events.QuestPlayedEvent;
import net.demilich.metastone.game.events.QuestSuccessfulEvent;
import net.demilich.metastone.game.events.SecretPlayedEvent;
import net.demilich.metastone.game.events.SecretRevealedEvent;
import net.demilich.metastone.game.events.SilenceEvent;
import net.demilich.metastone.game.events.SpellCastedEvent;
import net.demilich.metastone.game.events.SummonEvent;
import net.demilich.metastone.game.events.TargetAcquisitionEvent;
import net.demilich.metastone.game.events.TurnEndEvent;
import net.demilich.metastone.game.events.TurnStartEvent;
import net.demilich.metastone.game.events.WeaponDestroyedEvent;
import net.demilich.metastone.game.events.WeaponEquippedEvent;
import net.demilich.metastone.game.heroes.powers.HeroPower;
import net.demilich.metastone.game.spells.Spell;
import net.demilich.metastone.game.spells.SpellUtils;
import net.demilich.metastone.game.spells.aura.Aura;
import net.demilich.metastone.game.spells.desc.SpellArg;
import net.demilich.metastone.game.spells.desc.SpellDesc;
import net.demilich.metastone.game.spells.desc.SpellFactory;
import net.demilich.metastone.game.spells.desc.trigger.TriggerDesc;
import net.demilich.metastone.game.spells.trigger.IGameEventListener;
import net.demilich.metastone.game.spells.trigger.SpellTrigger;
import net.demilich.metastone.game.spells.trigger.types.Quest;
import net.demilich.metastone.game.spells.trigger.types.Secret;
import net.demilich.metastone.game.targeting.CardLocation;
import net.demilich.metastone.game.targeting.CardReference;
import net.demilich.metastone.game.targeting.EntityReference;
import net.demilich.metastone.game.targeting.IdFactory;
import net.demilich.metastone.game.targeting.TargetSelection;
import net.demilich.metastone.utils.MathUtils;
public class GameLogic implements Cloneable {
public static Logger logger = LoggerFactory.getLogger(GameLogic.class);
public static final int MAX_PLAYERS = 2;
public static final int MAX_MINIONS = 7;
public static final int MAX_HAND_CARDS = 10;
public static final int MAX_HERO_HP = 30;
public static final int STARTER_CARDS = 3;
public static final int MAX_MANA = 10;
public static final int MAX_QUESTS = 1;
public static final int MAX_SECRETS = 5;
public static final int DECK_SIZE = 30;
public static final int MAX_DECK_SIZE = 60;
public static final int TURN_LIMIT = 100;
public static final int WINDFURY_ATTACKS = 2;
public static final int MEGA_WINDFURY_ATTACKS = 4;
public static final String TEMP_CARD_LABEL = "temp_card_id_";
private static final int INFINITE = -1;
private static boolean hasPlayerLost(Player player) {
return player.getHero().getHp() < 1 || player.getHero().hasAttribute(Attribute.DESTROYED)
|| player.hasAttribute(Attribute.DESTROYED);
}
private final TargetLogic targetLogic = new TargetLogic();
private final ActionLogic actionLogic = new ActionLogic();
private final SpellFactory spellFactory = new SpellFactory();
private final IdFactory idFactory;
private GameContext context;
private boolean loggingEnabled = true;
// DEBUG
private final int MAX_HISTORY_ENTRIES = 100;
private Queue<String> debugHistory = new LinkedList<>();
public GameLogic() {
idFactory = new IdFactory();
}
private GameLogic(IdFactory idFactory) {
this.idFactory = idFactory;
}
public void addGameEventListener(Player player, IGameEventListener gameEventListener, Entity target) {
debugHistory.add("Player " + player.getId() + " has set event listener " + gameEventListener.getClass().getName() + " from entity " + target.getName() + "[Reference ID: " + target.getId() + "]");
gameEventListener.setHost(target);
if (!gameEventListener.hasPersistentOwner() || gameEventListener.getOwner() == -1) {
gameEventListener.setOwner(player.getId());
}
gameEventListener.onAdd(context);
context.addTrigger(gameEventListener);
log("New spelltrigger was added for {} on {}: {}", player.getName(), target, gameEventListener);
}
public void addManaModifier(Player player, CardCostModifier cardCostModifier, Entity target) {
context.getCardCostModifiers().add(cardCostModifier);
addGameEventListener(player, cardCostModifier, target);
}
public void afterCardPlayed(int playerId, CardReference cardReference) {
Player player = context.getPlayer(playerId);
player.modifyAttribute(Attribute.COMBO, +1);
Card card = context.resolveCardReference(cardReference);
card.removeAttribute(Attribute.MANA_COST_MODIFIER);
}
public int applyAmplify(Player player, int baseValue, Attribute attribute) {
int amplify = getTotalAttributeMultiplier(player, attribute);
return baseValue * amplify;
}
public void applyAttribute(Entity entity, Attribute attr) {
if (attr == Attribute.MEGA_WINDFURY && entity.hasAttribute(Attribute.WINDFURY) && !entity.hasAttribute(Attribute.MEGA_WINDFURY)) {
entity.modifyAttribute(Attribute.NUMBER_OF_ATTACKS, MEGA_WINDFURY_ATTACKS - WINDFURY_ATTACKS);
} else
if (attr == Attribute.WINDFURY && !entity.hasAttribute(Attribute.WINDFURY) && !entity.hasAttribute(Attribute.MEGA_WINDFURY)) {
entity.modifyAttribute(Attribute.NUMBER_OF_ATTACKS, WINDFURY_ATTACKS - 1);
} else if (attr == Attribute.MEGA_WINDFURY && !entity.hasAttribute(Attribute.WINDFURY) && !entity.hasAttribute(Attribute.MEGA_WINDFURY)) {
entity.modifyAttribute(Attribute.NUMBER_OF_ATTACKS, MEGA_WINDFURY_ATTACKS - 1);
}
entity.setAttribute(attr);
log("Applying attr {} to {}", attr, entity);
}
/**
* Applies hero power damage increases
* @param player
* The Player to grab additional hero power damage from
* @param baseValue
* The base damage the hero power does
* @return
* Increased hero power damage
*/
public int applyHeroPowerDamage(Player player, int baseValue) {
int spellpower = getTotalAttributeValue(player, Attribute.HERO_POWER_DAMAGE);
return baseValue + spellpower;
}
/**
* Applies spell damage increases
* @param player
* The Player to grab the additional spell damage from
* @param source
* The source Card
* @param baseValue
* The base damage the spell does
* @return
* Increased spell damage
*/
public int applySpellpower(Player player, Entity source, int baseValue) {
int spellpower = getTotalAttributeValue(player, Attribute.SPELL_DAMAGE)
+ getTotalAttributeValue(context.getOpponent(player), Attribute.OPPONENT_SPELL_DAMAGE);
if (source.hasAttribute(Attribute.SPELL_DAMAGE_MULTIPLIER)) {
spellpower *= source.getAttributeValue(Attribute.SPELL_DAMAGE_MULTIPLIER);
}
return baseValue + spellpower;
}
/**
* Assigns an ID to each Card in a given deck
* @param cardCollection
* The Deck to assign IDs to
*/
private void assignCardIds(CardCollection cardCollection) {
for (Card card : cardCollection) {
card.setId(idFactory.generateId());
card.setLocation(CardLocation.DECK);
}
}
public boolean attributeExists(Attribute attr) {
for (Player player : context.getPlayers()) {
if (player.getHero().hasAttribute(attr)) {
return true;
}
for (Entity summon : player.getSummons()) {
if (summon.hasAttribute(attr) && !summon.hasAttribute(Attribute.PENDING_DESTROY)) {
return true;
}
}
}
return false;
}
public boolean canPlayCard(int playerId, CardReference cardReference) {
Player player = context.getPlayer(playerId);
Card card = context.resolveCardReference(cardReference);
int manaCost = getModifiedManaCost(player, card);
if (card.getCardType().isCardType(CardType.SPELL)
&& player.hasAttribute(Attribute.SPELLS_COST_HEALTH)
&& player.getHero().getEffectiveHp() < manaCost) {
return false;
} else if (card.getCardType().isCardType(CardType.MINION)
&& (Race) card.getAttribute(Attribute.RACE) == Race.MURLOC
&& player.hasAttribute(Attribute.MURLOCS_COST_HEALTH)
&& player.getHero().getEffectiveHp() < manaCost) {
return false;
} else if (player.getMana() < manaCost && manaCost != 0
&& !((card.getCardType().isCardType(CardType.SPELL)
&& player.hasAttribute(Attribute.SPELLS_COST_HEALTH))
|| ((Race) card.getAttribute(Attribute.RACE) == Race.MURLOC
&& player.hasAttribute(Attribute.MURLOCS_COST_HEALTH)))) {
return false;
}
if (card.getCardType().isCardType(CardType.HERO_POWER)) {
HeroPower power = (HeroPower) card;
int heroPowerUsages = getGreatestAttributeValue(player, Attribute.HERO_POWER_USAGES);
if (heroPowerUsages == 0) {
heroPowerUsages = 1;
}
if (heroPowerUsages != INFINITE && power.hasBeenUsed() >= heroPowerUsages) {
return false;
}
} else if (card.getCardType().isCardType(CardType.MINION)) {
return canSummonMoreMinions(player);
}
if (card instanceof SpellCard) {
SpellCard spellCard = (SpellCard) card;
return spellCard.canBeCast(context, player);
}
return true;
}
public boolean canPlayQuest(Player player, QuestCard card) {
return player.getSecrets().size() < MAX_SECRETS && player.getQuests().size() < MAX_QUESTS && !player.getQuests().contains(card.getCardId());
}
public boolean canPlaySecret(Player player, SecretCard card) {
return player.getSecrets().size() < MAX_SECRETS && !player.getSecrets().contains(card.getCardId());
}
public boolean canSummonMoreMinions(Player player) {
return player.getSummons().size() < MAX_MINIONS;
}
public void castChooseOneSpell(int playerId, SpellDesc spellDesc, EntityReference sourceReference, EntityReference targetReference, String cardId) {
Player player = context.getPlayer(playerId);
Entity source = null;
if (sourceReference != null) {
try {
source = context.resolveSingleTarget(sourceReference);
} catch (Exception e) {
e.printStackTrace();
logger.error("Error resolving source entity while casting spell: " + spellDesc);
}
}
EntityReference spellTarget = spellDesc.hasPredefinedTarget() ? spellDesc.getTarget() : targetReference;
List<Entity> targets = targetLogic.resolveTargetKey(context, player, source, spellTarget);
Card sourceCard = null;
SpellCard chosenCard = (SpellCard) context.getCardById(cardId);
sourceCard = source.getEntityType() == EntityType.CARD ? (Card) source : null;
if (!spellDesc.hasPredefinedTarget() && targets != null && targets.size() == 1) {
if (chosenCard.getTargetRequirement() != TargetSelection.NONE) {
context.getEnvironment().remove(Environment.TARGET_OVERRIDE);
context.getEnvironment().put(Environment.CHOOSE_ONE_CARD, chosenCard.getCardId());
GameEvent spellTargetEvent = new TargetAcquisitionEvent(context, playerId, ActionType.SPELL, chosenCard, targets.get(0));
context.fireGameEvent(spellTargetEvent);
Entity targetOverride = context
.resolveSingleTarget((EntityReference) context.getEnvironment().get(Environment.TARGET_OVERRIDE));
if (targetOverride != null && targetOverride.getId() != IdFactory.UNASSIGNED) {
targets.remove(0);
targets.add(targetOverride);
spellDesc = spellDesc.addArg(SpellArg.FILTER, null);
log("Target for spell {} has been changed! New target {}", chosenCard, targets.get(0));
}
}
}
try {
Spell spell = spellFactory.getSpell(spellDesc);
spell.cast(context, player, spellDesc, source, targets);
} catch (Exception e) {
if (source != null) {
logger.error("Error while playing card: " + source.getName());
}
logger.error("Error while casting spell: " + spellDesc);
panicDump();
e.printStackTrace();
}
context.getEnvironment().remove(Environment.TARGET_OVERRIDE);
context.getEnvironment().remove(Environment.CHOOSE_ONE_CARD);
checkForDeadEntities();
if (targets == null || targets.size() != 1) {
context.fireGameEvent(new AfterSpellCastedEvent(context, playerId, sourceCard, null));
} else {
context.fireGameEvent(new AfterSpellCastedEvent(context, playerId, sourceCard, targets.get(0)));
}
}
public void castSpell(int playerId, SpellDesc spellDesc, EntityReference sourceReference, EntityReference targetReference,
boolean childSpell) {
castSpell(playerId, spellDesc, sourceReference, targetReference, TargetSelection.NONE, childSpell);
}
public void castSpell(int playerId, SpellDesc spellDesc, EntityReference sourceReference, EntityReference targetReference,
TargetSelection targetSelection, boolean childSpell) {
Player player = context.getPlayer(playerId);
Entity source = null;
if (sourceReference != null) {
try {
source = context.resolveSingleTarget(sourceReference);
} catch (Exception e) {
e.printStackTrace();
logger.error("Error resolving source entity while casting spell: " + spellDesc);
}
}
//SpellCard spellCard = null;
EntityReference spellTarget = spellDesc.hasPredefinedTarget() ? spellDesc.getTarget() : targetReference;
List<Entity> targets = targetLogic.resolveTargetKey(context, player, source, spellTarget);
// target can only be changed when there is one target
// note: this code block is basically exclusively for the SpellBender
// Secret, but it can easily be expanded if targets of area of effect
// spell should be changeable as well
Card sourceCard = null;
if (source != null) {
sourceCard = source.getEntityType() == EntityType.CARD ? (Card) source : null;
}
if (sourceCard != null && sourceCard.getCardType().isCardType(CardType.SPELL) && !spellDesc.hasPredefinedTarget() && targets != null
&& targets.size() == 1) {
if (sourceCard.getCardType().isCardType(CardType.SPELL) && targetSelection != TargetSelection.NONE && !childSpell) {
GameEvent spellTargetEvent = new TargetAcquisitionEvent(context, playerId, ActionType.SPELL, sourceCard, targets.get(0));
context.fireGameEvent(spellTargetEvent);
Entity targetOverride = context
.resolveSingleTarget((EntityReference) context.getEnvironment().get(Environment.TARGET_OVERRIDE));
if (targetOverride != null && targetOverride.getId() != IdFactory.UNASSIGNED) {
targets.remove(0);
targets.add(targetOverride);
spellDesc = spellDesc.addArg(SpellArg.FILTER, null);
log("Target for spell {} has been changed! New target {}", sourceCard, targets.get(0));
}
}
}
try {
Spell spell = spellFactory.getSpell(spellDesc);
spell.cast(context, player, spellDesc, source, targets);
} catch (Exception e) {
if (source != null) {
logger.error("Error while playing card: " + source.getName());
}
logger.error("Error while casting spell: " + spellDesc);
panicDump();
e.printStackTrace();
}
if (sourceCard != null && sourceCard.getCardType().isCardType(CardType.SPELL) && !childSpell) {
context.getEnvironment().remove(Environment.TARGET_OVERRIDE);
checkForDeadEntities();
if (targets == null || targets.size() != 1) {
context.fireGameEvent(new AfterSpellCastedEvent(context, playerId, sourceCard, null));
} else {
context.fireGameEvent(new AfterSpellCastedEvent(context, playerId, sourceCard, targets.get(0)));
}
}
}
public void changeHero(Player player, Hero hero) {
hero.setId(player.getHero().getId());
if (hero.getHeroClass() == null || hero.getHeroClass() == HeroClass.ANY) {
hero.setHeroClass(player.getHero().getHeroClass());
}
log("{}'s hero has been changed to {}", player.getName(), hero);
hero.setOwner(player.getId());
hero.setWeapon(player.getHero().getWeapon());
player.setHero(hero);
refreshAttacksPerRound(hero);
}
public void checkForDeadEntities() {
checkForDeadEntities(0);
}
/**
* Checks all player minions and weapons for destroyed actors and proceeds
* with the removal in correct order
*/
public void checkForDeadEntities(int i) {
// sanity check, this method should never call itself that often
if (i > 20) {
panicDump();
throw new RuntimeException("Infinite death checking loop");
}
List<Actor> destroyList = new ArrayList<>();
for (Player player : context.getPlayers()) {
if (player.getHero().isDestroyed() || player.hasAttribute(Attribute.DESTROYED)) {
destroyList.add(player.getHero());
}
for (Summon summon : player.getSummons()) {
if (summon.isDestroyed()) {
destroyList.add(summon);
}
}
if (player.getHero().getWeapon() != null && player.getHero().getWeapon().isDestroyed()) {
destroyList.add(player.getHero().getWeapon());
}
}
if (destroyList.isEmpty()) {
return;
}
// sort the destroyed actors by their id. This implies that actors with a lower id entered the game ealier than those with higher ids!
Collections.sort(destroyList, (a1, a2) -> Integer.compare(a1.getId(), a2.getId()));
// this method performs the actual removal
destroy(destroyList.toArray(new Actor[0]));
if (context.gameDecided()) {
return;
}
// deathrattles have been resolved, which may lead to other actors being destroyed now, so we need to check again
checkForDeadEntities(i + 1);
}
@Override
public GameLogic clone() {
GameLogic clone = new GameLogic(idFactory.clone());
clone.debugHistory = new LinkedList<>(debugHistory);
return clone;
}
public int damage(Player player, Actor target, int baseDamage, Entity source) {
return damage(player, target, baseDamage, source, false);
}
public int damage(Player player, Actor target, int baseDamage, Entity source, boolean ignoreSpellDamage) {
// sanity check to prevent StackOverFlowError with Mistress of Pain +
// Auchenai Soulpriest
if (target.getHp() < -100) {
return 0;
}
int damage = baseDamage;
Card sourceCard = source != null && source.getEntityType() == EntityType.CARD ? (Card) source : null;
if (!ignoreSpellDamage && sourceCard != null) {
if (sourceCard.getCardType().isCardType(CardType.SPELL)) {
damage = applySpellpower(player, source, baseDamage);
} else if (sourceCard.getCardType().isCardType(CardType.HERO_POWER)) {
damage = applyHeroPowerDamage(player, damage);
}
if (sourceCard.getCardType().isCardType(CardType.SPELL) || sourceCard.getCardType().isCardType(CardType.HERO_POWER)) {
damage = applyAmplify(player, damage, Attribute.SPELL_AMPLIFY_MULTIPLIER);
}
}
int damageDealt = 0;
if (target.hasAttribute(Attribute.TAKE_DOUBLE_DAMAGE)) {
damage *= 2;
}
context.getDamageStack().push(damage);
context.fireGameEvent(new PreDamageEvent(context, target, source));
damage = context.getDamageStack().pop();
if (damage > 0) {
source.removeAttribute(Attribute.STEALTH);
}
switch (target.getEntityType()) {
case MINION:
damageDealt = damageMinion((Actor) target, damage);
break;
case HERO:
damageDealt = damageHero((Hero) target, damage);
break;
default:
break;
}
target.setAttribute(Attribute.LAST_HIT, damageDealt);
if (damageDealt > 0) {
DamageEvent damageEvent = new DamageEvent(context, target, source, damageDealt);
context.fireGameEvent(damageEvent);
player.getStatistics().damageDealt(damageDealt);
}
return damageDealt;
}
private int damageHero(Hero hero, int damage) {
if (hero.hasAttribute(Attribute.IMMUNE) || hasAttribute(context.getPlayer(hero.getOwner()), Attribute.IMMUNE_HERO)) {
log("{} is IMMUNE and does not take damage", hero);
return 0;
}
int effectiveHp = hero.getHp() + hero.getArmor();
hero.modifyArmor(-damage);
int newHp = Math.min(hero.getHp(), effectiveHp - damage);
hero.setHp(newHp);
log(hero.getName() + " receives " + damage + " damage, hp now: " + hero.getHp() + "(" + hero.getArmor() + ")");
return damage;
}
private int damageMinion(Actor minion, int damage) {
if (minion.hasAttribute(Attribute.DIVINE_SHIELD)) {
removeAttribute(minion, Attribute.DIVINE_SHIELD);
log("{}'s DIVINE SHIELD absorbs the damage", minion);
return 0;
}
if (minion.hasAttribute(Attribute.IMMUNE)) {
log("{} is IMMUNE and does not take damage", minion);
return 0;
}
if (damage >= minion.getHp() && minion.hasAttribute(Attribute.CANNOT_REDUCE_HP_BELOW_1)) {
damage = minion.getHp() - 1;
}
log("{} is damaged for {}", minion, damage);
minion.setHp(minion.getHp() - damage);
handleEnrage(minion);
return damage;
}
public void destroy(Actor... targets) {
int[] boardPositions = new int[targets.length];
for (int i = 0; i < targets.length; i++) {
Actor target = targets[i];
removeSpellTriggers(target, false);
Player owner = context.getPlayer(target.getOwner());
context.getPlayer(target.getOwner()).getGraveyard().add(target);
int boardPosition = owner.getSummons().indexOf(target);
boardPositions[i] = boardPosition;
}
for (int i = 0; i < targets.length; i++) {
Actor target = targets[i];
log("{} is destroyed", target);
Player owner = context.getPlayer(target.getOwner());
owner.getSummons().remove(target);
}
for (int i = 0; i < targets.length; i++) {
Actor target = targets[i];
Player owner = context.getPlayer(target.getOwner());
switch (target.getEntityType()) {
case HERO:
log("Hero {} has been destroyed.", target.getName());
applyAttribute(target, Attribute.DESTROYED);
applyAttribute(context.getPlayer(target.getOwner()), Attribute.DESTROYED);
break;
case MINION:
destroyMinion((Minion) target);
break;
case WEAPON:
destroyWeapon((Weapon) target);
break;
case ANY:
default:
logger.error("Trying to destroy unknown entity type {}", target.getEntityType());
break;
}
resolveDeathrattles(owner, target, boardPositions[i]);
}
for (Actor target : targets) {
removeSpellTriggers(target, true);
}
context.fireGameEvent(new BoardChangedEvent(context));
}
private void destroyMinion(Minion minion) {
context.getEnvironment().put(Environment.KILLED_MINION, minion.getReference());
KillEvent killEvent = new KillEvent(context, minion);
context.fireGameEvent(killEvent);
context.getEnvironment().remove(Environment.KILLED_MINION);
minion.setAttribute(Attribute.DESTROYED);
minion.setAttribute(Attribute.DIED_ON_TURN, context.getTurn());
}
private void destroyWeapon(Weapon weapon) {
Player owner = context.getPlayer(weapon.getOwner());
// resolveDeathrattles(owner, weapon);
if (owner.getHero().getWeapon() != null && owner.getHero().getWeapon().getId() == weapon.getId()) {
owner.getHero().setWeapon(null);
}
weapon.onUnequip(context, owner);
context.fireGameEvent(new WeaponDestroyedEvent(context, weapon));
}
public int determineBeginner(int... playerIds) {
return ThreadLocalRandom.current().nextBoolean() ? playerIds[0] : playerIds[1];
}
public void discardCard(Player player, Card card) {
logger.debug("{} discards {}", player.getName(), card);
// only a 'real' discard should fire a DiscardEvent
if (card.getLocation() == CardLocation.HAND) {
context.fireGameEvent(new DiscardEvent(context, player.getId(), card));
}
removeCard(player.getId(), card);
}
public Card drawCard(int playerId, Entity source) {
Player player = context.getPlayer(playerId);
CardCollection deck = player.getDeck();
if (deck.isEmpty()) {
Hero hero = player.getHero();
int fatigue = player.hasAttribute(Attribute.FATIGUE) ? player.getAttributeValue(Attribute.FATIGUE) : 0;
fatigue++;
player.setAttribute(Attribute.FATIGUE, fatigue);
damage(player, hero, fatigue, hero);
log("{}'s deck is empty, taking {} fatigue damage!", player.getName(), fatigue);
player.getStatistics().fatigueDamage(fatigue);
return null;
}
Card card = deck.getRandom();
return drawCard(playerId, card, source);
}
public Card drawCard(int playerId, Card card, Entity source) {
Player player = context.getPlayer(playerId);
player.getStatistics().cardDrawn();
player.getDeck().remove(card);
receiveCard(playerId, card, source, true);
return card;
}
public void drawSetAsideCard(int playerId, Card card) {
if (card.getId() == IdFactory.UNASSIGNED) {
card.setId(idFactory.generateId());
}
card.setOwner(playerId);
Player player = context.getPlayer(playerId);
player.getSetAsideZone().add(card);
}
public void endTurn(int playerId) {
Player player = context.getPlayer(playerId);
Hero hero = player.getHero();
hero.removeAttribute(Attribute.TEMPORARY_ATTACK_BONUS);
hero.removeAttribute(Attribute.HERO_POWER_USAGES);
handleFrozen(hero);
for (Summon summon : player.getSummons()) {
summon.removeAttribute(Attribute.TEMPORARY_ATTACK_BONUS);
handleFrozen(summon);
}
player.removeAttribute(Attribute.COMBO);
hero.activateWeapon(false);
log("{} ends his turn.", player.getName());
context.fireGameEvent(new TurnEndEvent(context, playerId));
for (Iterator<CardCostModifier> iterator = context.getCardCostModifiers().iterator(); iterator.hasNext();) {
CardCostModifier cardCostModifier = iterator.next();
if (cardCostModifier.isExpired()) {
iterator.remove();
}
}
checkForDeadEntities();
}
public void equipWeapon(int playerId, Weapon weapon) {
equipWeapon(playerId, weapon, false);
}
public void equipWeapon(int playerId, Weapon weapon, boolean battlecry) {
Player player = context.getPlayer(playerId);
weapon.setId(idFactory.generateId());
Weapon currentWeapon = player.getHero().getWeapon();
if (currentWeapon != null) {
player.getSetAsideZone().add(currentWeapon);
}
log("{} equips weapon {}", player.getHero(), weapon);
player.getHero().setWeapon(weapon);
if (battlecry && weapon.getBattlecry() != null) {
resolveBattlecry(playerId, weapon);
}
if (currentWeapon != null) {
log("{} discards currently equipped weapon {}", player.getHero(), currentWeapon);
destroy(currentWeapon);
player.getSetAsideZone().remove(currentWeapon);
}
player.getStatistics().equipWeapon(weapon);
weapon.onEquip(context, player);
weapon.setActive(context.getActivePlayerId() == playerId);
if (weapon.hasSpellTrigger()) {
for (SpellTrigger spellTrigger : weapon.getSpellTriggers()) {
addGameEventListener(player, spellTrigger, weapon);
}
}
if (weapon.getCardCostModifier() != null) {
addManaModifier(player, weapon.getCardCostModifier(), weapon);
}
checkForDeadEntities();
context.fireGameEvent(new WeaponEquippedEvent(context, weapon));
context.fireGameEvent(new BoardChangedEvent(context));
}
public void fight(Player player, Actor attacker, Actor defender) {
log("{} attacks {}", attacker, defender);
context.getEnvironment().put(Environment.ATTACKER_REFERENCE, attacker.getReference());
TargetAcquisitionEvent targetAcquisitionEvent = new TargetAcquisitionEvent(context, player.getId(), ActionType.PHYSICAL_ATTACK,
attacker, defender);
context.fireGameEvent(targetAcquisitionEvent);
Actor target = defender;
if (context.getEnvironment().containsKey(Environment.TARGET_OVERRIDE)) {
target = (Actor) context.resolveSingleTarget((EntityReference) context.getEnvironment().get(Environment.TARGET_OVERRIDE));
}
context.getEnvironment().remove(Environment.TARGET_OVERRIDE);
// if (attacker.hasTag(GameTag.FUMBLE) && randomBool()) {
// log("{} fumbled and hits another target", attacker);
// target = getAnotherRandomTarget(player, attacker, defender,
// EntityReference.ENEMY_CHARACTERS);
// }
if (target != defender) {
log("Target of attack was changed! New Target: {}", target);
}
if (attacker.hasAttribute(Attribute.IMMUNE_WHILE_ATTACKING)) {
applyAttribute(attacker, Attribute.IMMUNE);
}
removeAttribute(attacker, Attribute.STEALTH);
int attackerDamage = attacker.getAttack();
int defenderDamage = target.getAttack();
context.fireGameEvent(new PhysicalAttackEvent(context, attacker, target, attackerDamage));
// secret may have killed attacker ADDENDUM: or defender
if (attacker.isDestroyed() || target.isDestroyed()) {
context.getEnvironment().remove(Environment.ATTACKER_REFERENCE);
return;
}
if (target.getOwner() == -1) {
logger.error("Target has no owner!! {}", target);
}
Player owningPlayer = context.getPlayer(target.getOwner());
boolean damaged = damage(owningPlayer, target, attackerDamage, attacker) > 0;
if (defenderDamage > 0) {
damage(player, attacker, defenderDamage, target);
}
if (attacker.hasAttribute(Attribute.IMMUNE_WHILE_ATTACKING)) {
attacker.removeAttribute(Attribute.IMMUNE);
}
if (attacker.getEntityType() == EntityType.HERO) {
Hero hero = (Hero) attacker;
Weapon weapon = hero.getWeapon();
if (weapon != null && weapon.isActive()) {
modifyDurability(hero.getWeapon(), -1);
}
}
attacker.modifyAttribute(Attribute.NUMBER_OF_ATTACKS, -1);
context.fireGameEvent(new AfterPhysicalAttackEvent(context, attacker, target, damaged ? attackerDamage : 0));
context.getEnvironment().remove(Environment.ATTACKER_REFERENCE);
}
public void gainArmor(Player player, int armor) {
logger.debug("{} gains {} armor", player.getHero(), armor);
player.getHero().modifyArmor(armor);
player.getStatistics().armorGained(armor);
if (armor > 0) {
context.fireGameEvent(new ArmorGainedEvent(context, player.getHero()));
}
}
public String generateCardID() {
return TEMP_CARD_LABEL + idFactory.generateId();
}
public Actor getAnotherRandomTarget(Player player, Actor attacker, Actor originalTarget, EntityReference potentialTargets) {
List<Entity> validTargets = context.resolveTarget(player, null, potentialTargets);
// cannot redirect to attacker
validTargets.remove(attacker);
// cannot redirect to original target
validTargets.remove(originalTarget);
if (validTargets.isEmpty()) {
return originalTarget;
}
return (Actor) SpellUtils.getRandomTarget(validTargets);
}
/**
* Returns the first value of the attribute encountered. This method should
* be used with caution, as the result is random if there are different
* values of the same attribute in play.
*
* @param player
* @param attr
* Which attribute to find
* @param defaultValue
* The value returned if no occurrence of the attribute is found
* @return the first occurrence of the value of attribute or defaultValue
*/
public int getAttributeValue(Player player, Attribute attr, int defaultValue) {
for (Summon summon : player.getSummons()) {
if (summon.hasAttribute(attr)) {
return summon.getAttributeValue(attr);
}
}
return defaultValue;
}
public GameAction getAutoHeroPowerAction(int playerId) {
return actionLogic.getAutoHeroPower(context, context.getPlayer(playerId));
}
/**
* Return the greatest value of the attribute from all Actors of a Player.
* This method will return infinite if an Attribute value is negative, so
* use this method with caution.
*
* @param player
* Which Player to check
* @param attr
* Which attribute to find
* @return The highest value from all sources. -1 is considered infinite.
*/
public int getGreatestAttributeValue(Player player, Attribute attr) {
int greatest = Math.max(INFINITE, player.getHero().getAttributeValue(attr));
if (greatest == INFINITE) {
return greatest;
}
for (Summon summon : player.getSummons()) {
if (summon.hasAttribute(attr)) {
if (summon.getAttributeValue(attr) > greatest) {
greatest = summon.getAttributeValue(attr);
}
if (summon.getAttributeValue(attr) == INFINITE) {
return INFINITE;
}
}
}
return greatest;
}
public MatchResult getMatchResult(Player player, Player opponent) {
boolean playerLost = hasPlayerLost(player);
boolean opponentLost = hasPlayerLost(opponent);
if (playerLost && opponentLost) {
return MatchResult.DOUBLE_LOSS;
} else if (playerLost || opponentLost) {
return MatchResult.WON;
}
return MatchResult.RUNNING;
}
public int getModifiedManaCost(Player player, Card card) {
int manaCost = card.getManaCost(context, player);
int minValue = 0;
for (CardCostModifier costModifier : context.getCardCostModifiers()) {
if (!costModifier.appliesTo(card)) {
continue;
}
manaCost = costModifier.process(card, manaCost);
if (costModifier.getMinValue() > minValue) {
minValue = costModifier.getMinValue();
}
}
if (card.hasAttribute(Attribute.MANA_COST_MODIFIER)) {
manaCost += card.getAttributeValue(Attribute.MANA_COST_MODIFIER);
}
manaCost = MathUtils.clamp(manaCost, minValue, Integer.MAX_VALUE);
return manaCost;
}
public List<IGameEventListener> getQuests(Player player) {
List<IGameEventListener> quests = context.getTriggersAssociatedWith(player.getHero().getReference());
for (Iterator<IGameEventListener> iterator = quests.iterator(); iterator.hasNext();) {
IGameEventListener trigger = iterator.next();
if (!(trigger instanceof Quest)) {
iterator.remove();
}
}
return quests;
}
public List<IGameEventListener> getSecrets(Player player) {
List<IGameEventListener> secrets = context.getTriggersAssociatedWith(player.getHero().getReference());
for (Iterator<IGameEventListener> iterator = secrets.iterator(); iterator.hasNext();) {
IGameEventListener trigger = iterator.next();
if (!(trigger instanceof Secret)) {
iterator.remove();
}
}
return secrets;
}
public int getTotalAttributeValue(Attribute attr) {
int total = 0;
for (Player player : context.getPlayers()) {
total += getTotalAttributeValue(player, attr);
}
return total;
}
public int getTotalAttributeValue(Player player, Attribute attr) {
int total = player.getHero().getAttributeValue(attr);
for (Summon summon : player.getSummons()) {
if (!summon.hasAttribute(attr)) {
continue;
}
total += summon.getAttributeValue(attr);
}
return total;
}
public int getTotalAttributeMultiplier(Player player, Attribute attribute) {
int total = 1;
if (player.getHero().hasAttribute(attribute)) {
player.getHero().getAttributeValue(attribute);
}
for (Summon summon : player.getSummons()) {
if (summon.hasAttribute(attribute)) {
total *= summon.getAttributeValue(attribute);
}
}
return total;
}
public List<GameAction> getValidActions(int playerId) {
Player player = context.getPlayer(playerId);
return actionLogic.getValidActions(context, player);
}
public List<Entity> getValidTargets(int playerId, GameAction action) {
Player player = context.getPlayer(playerId);
return targetLogic.getValidTargets(context, player, action);
}
public Player getWinner(Player player, Player opponent) {
boolean playerLost = hasPlayerLost(player);
boolean opponentLost = hasPlayerLost(opponent);
if (playerLost && opponentLost) {
return null;
} else if (opponentLost) {
return player;
} else if (playerLost) {
return opponent;
}
return null;
}
private void handleEnrage(Actor entity) {
if (!entity.hasAttribute(Attribute.ENRAGABLE)) {
return;
}
boolean enraged = entity.getHp() < entity.getMaxHp();
// enrage state has not changed; do nothing
if (entity.hasAttribute(Attribute.ENRAGED) == enraged) {
return;
}
if (enraged) {
log("{} is now enraged", entity);
entity.setAttribute(Attribute.ENRAGED);
} else {
log("{} is no longer enraged", entity);
entity.removeAttribute(Attribute.ENRAGED);
}
context.fireGameEvent(new EnrageChangedEvent(context, entity));
}
private void handleFrozen(Actor actor) {
if (!actor.hasAttribute(Attribute.FROZEN)) {
return;
}
if (actor.getAttributeValue(Attribute.NUMBER_OF_ATTACKS) >= actor.getMaxNumberOfAttacks()) {
removeAttribute(actor, Attribute.FROZEN);
}
}
public boolean hasAttribute(Player player, Attribute attr) {
if (player.getHero().hasAttribute(attr)) {
return true;
}
for (Summon summon : player.getSummons()) {
if (summon.hasAttribute(attr) && !summon.hasAttribute(Attribute.PENDING_DESTROY)) {
return true;
}
}
return false;
}
public boolean hasAutoHeroPower(int player) {
return actionLogic.hasAutoHeroPower(context, context.getPlayer(player));
}
public boolean hasCard(Player player, Card card) {
for (Card heldCard : player.getHand()) {
if (card.getCardId().equals(heldCard.getCardId())) {
return true;
}
}
if (player.getHero().getHeroPower().getCardId().equals(card.getCardId())) {
return true;
}
return false;
}
public void heal(Player player, Actor target, int healing, Entity source) {
if (hasAttribute(player, Attribute.INVERT_HEALING)) {
log("All healing inverted, deal damage instead!");
damage(player, target, healing, source);
return;
}
if (source != null && source instanceof Card
&& (((Card) source).getCardType().isCardType(CardType.SPELL)
|| ((Card) source).getCardType().isCardType(CardType.HERO_POWER))) {
healing = applyAmplify(player, healing, Attribute.HEAL_AMPLIFY_MULTIPLIER);
}
boolean success = false;
switch (target.getEntityType()) {
case MINION:
success = healMinion((Actor) target, healing);
break;
case HERO:
success = healHero((Hero) target, healing);
break;
default:
break;
}
if (success) {
HealEvent healEvent = new HealEvent(context, player.getId(), target, healing);
context.fireGameEvent(healEvent);
player.getStatistics().heal(healing);
}
}
private boolean healHero(Hero hero, int healing) {
int newHp = Math.min(hero.getMaxHp(), hero.getHp() + healing);
int oldHp = hero.getHp();
if (logger.isDebugEnabled()) {
log(hero + " is healed for " + healing + ", hp now: " + newHp / hero.getMaxHp());
}
hero.setHp(newHp);
return newHp != oldHp;
}
private boolean healMinion(Actor minion, int healing) {
int newHp = Math.min(minion.getMaxHp(), minion.getHp() + healing);
int oldHp = minion.getHp();
if (logger.isDebugEnabled()) {
log(minion + " is healed for " + healing + ", hp now: " + newHp + "/" + minion.getMaxHp());
}
minion.setHp(newHp);
handleEnrage(minion);
return newHp != oldHp;
}
public void init(int playerId, boolean begins) {
Player player = context.getPlayer(playerId);
player.getHero().setId(idFactory.generateId());
player.getHero().setOwner(player.getId());
player.getHero().setMaxHp(player.getHero().getAttributeValue(Attribute.BASE_HP));
player.getHero().setHp(player.getHero().getAttributeValue(Attribute.BASE_HP));
player.getHero().getHeroPower().setId(idFactory.generateId());
assignCardIds(player.getDeck());
assignCardIds(player.getHand());
log("Setting hero hp to {} for {}", player.getHero().getHp(), player.getName());
player.getDeck().shuffle();
mulligan(player, begins);
for (Card card : player.getDeck()) {
if (card.getAttribute(Attribute.DECK_TRIGGER) != null) {
TriggerDesc triggerDesc = (TriggerDesc) card.getAttribute(Attribute.DECK_TRIGGER);
addGameEventListener(player, triggerDesc.create(), card);
}
}
for (Card card : player.getHand()) {
if (card.getAttribute(Attribute.DECK_TRIGGER) != null) {
TriggerDesc triggerDesc = (TriggerDesc) card.getAttribute(Attribute.DECK_TRIGGER);
addGameEventListener(player, triggerDesc.create(), card);
}
}
GameStartEvent gameStartEvent = new GameStartEvent(context, player.getId());
context.fireGameEvent(gameStartEvent);
}
public boolean isLoggingEnabled() {
return loggingEnabled;
}
public JoustEvent joust(Player player) {
Card ownCard = player.getDeck().getRandomOfType(CardType.MINION);
Card opponentCard = null;
boolean won = false;
// no minions left in deck - automatically loose joust
if (ownCard == null) {
won = false;
log("Jousting LOST - no minion card left");
} else {
Player opponent = context.getOpponent(player);
opponentCard = opponent.getDeck().getRandomOfType(CardType.MINION);
// opponent has no minions left in deck - automatically win joust
if (opponentCard == null) {
won = true;
log("Jousting WON - opponent has no minion card left");
} else {
// both players have minion cards left, the initiator needs to
// have the one with
// higher mana cost to win the joust
won = ownCard.getBaseManaCost() > opponentCard.getBaseManaCost();
log("Jousting {} - {} vs. {}", won ? "WON" : "LOST", ownCard, opponentCard);
}
}
JoustEvent joustEvent = new JoustEvent(context, player.getId(), won, ownCard, opponentCard);
context.fireGameEvent(joustEvent);
return joustEvent;
}
private void log(String message) {
logToDebugHistory(message);
if (isLoggingEnabled() && logger.isDebugEnabled()) {
logger.debug(message);
}
}
private void log(String message, Object param1) {
logToDebugHistory(message, param1);
if (isLoggingEnabled() && logger.isDebugEnabled()) {
logger.debug(message, param1);
}
}
private void log(String message, Object param1, Object param2) {
logToDebugHistory(message, param1, param2);
if (isLoggingEnabled() && logger.isDebugEnabled()) {
logger.debug(message, param1, param2);
}
}
private void log(String message, Object param1, Object param2, Object param3) {
logToDebugHistory(message, param1, param2, param3);
if (isLoggingEnabled() && logger.isDebugEnabled()) {
logger.debug(message, param1, param2, param3);
}
}
private void logToDebugHistory(String message, Object... params) {
if (!BuildConfig.DEV_BUILD) {
return;
}
if (debugHistory.size() == MAX_HISTORY_ENTRIES) {
debugHistory.poll();
}
if (params != null && params.length > 0) {
message = message.replaceAll("\\{\\}", "%s");
message = String.format(message, params);
}
debugHistory.add(message);
}
public void markAsDestroyed(Actor target) {
if (target != null) {
target.setAttribute(Attribute.DESTROYED);
}
}
public void mindControl(Player player, Summon summon) {
log("{} mind controls {}", player.getName(), summon);
Player opponent = context.getOpponent(player);
if (!opponent.getSummons().contains(summon)) {
// logger.warn("Minion {} cannot be mind-controlled, because
// opponent does not own it.", minion);
return;
}
if (canSummonMoreMinions(player)) {
context.getOpponent(player).getSummons().remove(summon);
player.getSummons().add(summon);
summon.setOwner(player.getId());
applyAttribute(summon, Attribute.SUMMONING_SICKNESS);
refreshAttacksPerRound(summon);
List<IGameEventListener> triggers = context.getTriggersAssociatedWith(summon.getReference());
removeSpellTriggers(summon);
for (IGameEventListener trigger : triggers) {
addGameEventListener(player, trigger, summon);
}
context.fireGameEvent(new BoardChangedEvent(context));
} else {
markAsDestroyed(summon);
}
}
public void modifyCurrentMana(int playerId, int mana) {
Player player = context.getPlayer(playerId);
int newMana = Math.min(player.getMana() + mana, MAX_MANA);
player.setMana(newMana);
}
public void modifyDurability(Weapon weapon, int durability) {
log("Durability of weapon {} is changed by {}", weapon, durability);
weapon.modifyAttribute(Attribute.HP, durability);
if (durability > 0) {
weapon.modifyAttribute(Attribute.MAX_HP, durability);
}
}
public void modifyMaxHp(Actor actor, int value) {
actor.setMaxHp(value);
actor.setHp(value);
handleEnrage(actor);
}
public void modifyMaxMana(Player player, int delta) {
log("Maximum mana was changed by {} for {}", delta, player.getName());
int maxMana = MathUtils.clamp(player.getMaxMana() + delta, 0, GameLogic.MAX_MANA);
player.setMaxMana(maxMana);
if (delta < 0 && player.getMana() > player.getMaxMana()) {
modifyCurrentMana(player.getId(), delta);
}
}
private void mulligan(Player player, boolean begins) {
int numberOfStarterCards = begins ? STARTER_CARDS : STARTER_CARDS + 1;
List<Card> starterCards = new ArrayList<>();
for (int j = 0; j < numberOfStarterCards; j++) {
Card randomCard = player.getDeck().getRandom();
if (randomCard != null) {
player.getDeck().remove(randomCard);
log("Player {} been offered card {} for mulligan", player.getName(), randomCard);
starterCards.add(randomCard);
}
}
List<Card> discardedCards = player.getBehaviour().mulligan(context, player, starterCards);
// remove player selected cards from starter cards
for (Card discardedCard : discardedCards) {
log("Player {} mulligans {} ", player.getName(), discardedCard);
starterCards.remove(discardedCard);
}
// draw random cards from deck until required starter card count is
// reached
while (starterCards.size() < numberOfStarterCards) {
Card randomCard = player.getDeck().getRandom();
player.getDeck().remove(randomCard);
starterCards.add(randomCard);
}
// put the mulligan cards back in the deck
for (Card discardedCard : discardedCards) {
player.getDeck().add(discardedCard);
}
for (Card starterCard : starterCards) {
if (starterCard != null) {
receiveCard(player.getId(), starterCard);
}
}
// second player gets the coin additionally
if (!begins) {
Card theCoin = CardCatalogue.getCardById("spell_the_coin");
receiveCard(player.getId(), theCoin);
}
}
public void panicDump() {
logger.error("=========PANIC DUMP=========");
for (String entry : debugHistory) {
logger.error(entry);
}
}
public void performGameAction(int playerId, GameAction action) {
debugHistory.add(action.toString());
if (playerId != context.getActivePlayerId()) {
logger.warn("Player {} tries to perform an action, but it is not his turn!", context.getPlayer(playerId).getName());
}
if (action.getTargetRequirement() != TargetSelection.NONE) {
Entity target = context.resolveSingleTarget(action.getTargetKey());
if (target != null) {
context.getEnvironment().put(Environment.TARGET, target.getReference());
} else {
context.getEnvironment().put(Environment.TARGET, null);
}
}
action.execute(context, playerId);
context.getEnvironment().remove(Environment.TARGET);
if (action.getActionType() != ActionType.BATTLECRY) {
checkForDeadEntities();
}
}
public void playCard(int playerId, CardReference cardReference) {
Player player = context.getPlayer(playerId);
Card card = context.resolveCardReference(cardReference);
int modifiedManaCost = getModifiedManaCost(player, card);
if (card.getCardType().isCardType(CardType.SPELL)
&& player.hasAttribute(Attribute.SPELLS_COST_HEALTH)) {
context.getEnvironment().put(Environment.LAST_MANA_COST, 0);
damage(player, player.getHero(), modifiedManaCost, card, true);
} else if ((Race) card.getAttribute(Attribute.RACE) == Race.MURLOC
&& player.getHero().hasAttribute(Attribute.MURLOCS_COST_HEALTH)) {
context.getEnvironment().put(Environment.LAST_MANA_COST, 0);
damage(player, player.getHero(), modifiedManaCost, card, true);
} else {
context.getEnvironment().put(Environment.LAST_MANA_COST, modifiedManaCost);
modifyCurrentMana(playerId, -modifiedManaCost);
player.getStatistics().manaSpent(modifiedManaCost);
}
log("{} plays {}", player.getName(), card);
player.getStatistics().cardPlayed(card, context.getTurn());
CardPlayedEvent cardPlayedEvent = new CardPlayedEvent(context, playerId, card);
context.fireGameEvent(cardPlayedEvent);
if (card.hasAttribute(Attribute.OVERLOAD)) {
context.fireGameEvent(new OverloadEvent(context, playerId, card));
}
removeCard(playerId, card);
if ((card.getCardType().isCardType(CardType.SPELL))) {
GameEvent spellCastedEvent = new SpellCastedEvent(context, playerId, card);
context.fireGameEvent(spellCastedEvent);
if (card.hasAttribute(Attribute.COUNTERED)) {
log("{} was countered!", card.getName());
return;
}
}
if (card.hasAttribute(Attribute.OVERLOAD)) {
player.modifyAttribute(Attribute.OVERLOAD, card.getAttributeValue(Attribute.OVERLOAD));
}
}
public void playQuest(Player player, Quest quest) {
playQuest(player, quest, true);
}
public void playQuest(Player player, Quest quest, boolean fromHand) {
log("{} has a new quest activated: {}", player.getName(), quest.getSource());
addGameEventListener(player, quest, player.getHero());
player.getSecrets().add(quest.getSource().getCardId());
player.getQuests().add(quest.getSource().getCardId());
if (fromHand) {
context.fireGameEvent(new QuestPlayedEvent(context, player.getId(), (QuestCard) quest.getSource()));
}
}
public void playSecret(Player player, Secret secret) {
playSecret(player, secret, true);
}
public void playSecret(Player player, Secret secret, boolean fromHand) {
log("{} has a new secret activated: {}", player.getName(), secret.getSource());
addGameEventListener(player, secret, player.getHero());
player.getSecrets().add(secret.getSource().getCardId());
if (fromHand) {
context.fireGameEvent(new SecretPlayedEvent(context, player.getId(), (SecretCard) secret.getSource()));
}
}
public void processTargetModifiers(Player player, GameAction action) {
HeroPower heroPower = player.getHero().getHeroPower();
if (heroPower.getHeroClass() != HeroClass.HUNTER) {
return;
}
if (action.getActionType() == ActionType.HERO_POWER && hasAttribute(player, Attribute.HERO_POWER_CAN_TARGET_MINIONS)) {
PlaySpellCardAction spellCardAction = (PlaySpellCardAction) action;
SpellDesc targetChangedSpell = spellCardAction.getSpell().removeArg(SpellArg.TARGET);
spellCardAction.setSpell(targetChangedSpell);
spellCardAction.setTargetRequirement(TargetSelection.ANY);
}
}
/**
*
* @param max
* Upper bound of random number (exclusive)
* @return Random number between 0 and max (exclusive)
*/
public int random(int max) {
return ThreadLocalRandom.current().nextInt(max);
}
public boolean randomBool() {
return ThreadLocalRandom.current().nextBoolean();
}
public void receiveCard(int playerId, Card card) {
receiveCard(playerId, card, null);
}
public void receiveCard(int playerId, Card card, Entity source) {
receiveCard(playerId, card, source, false);
}
public void receiveCard(int playerId, Card card, Entity source, boolean drawn) {
Player player = context.getPlayer(playerId);
if (card.getId() == IdFactory.UNASSIGNED) {
card.setId(idFactory.generateId());
}
card.setOwner(playerId);
CardCollection hand = player.getHand();
if (hand.getCount() < MAX_HAND_CARDS) {
if (card.getAttribute(Attribute.PASSIVE_TRIGGER) != null) {
TriggerDesc triggerDesc = (TriggerDesc) card.getAttribute(Attribute.PASSIVE_TRIGGER);
addGameEventListener(player, triggerDesc.create(), card);
}
log("{} receives card {}", player.getName(), card);
hand.add(card);
card.setLocation(CardLocation.HAND);
CardType sourceType = null;
if (source instanceof Card) {
Card sourceCard = (Card) source;
sourceType = sourceCard.getCardType();
}
context.fireGameEvent(new DrawCardEvent(context, playerId, card, sourceType, drawn));
} else {
log("{} has too many cards on his hand, card destroyed: {}", player.getName(), card);
discardCard(player, card);
}
}
public void refreshAttacksPerRound(Entity entity) {
int attacks = 1;
if (entity.hasAttribute(Attribute.MEGA_WINDFURY)) {
attacks = MEGA_WINDFURY_ATTACKS;
} else if (entity.hasAttribute(Attribute.WINDFURY)) {
attacks = WINDFURY_ATTACKS;
}
entity.setAttribute(Attribute.NUMBER_OF_ATTACKS, attacks);
}
public void removeAttribute(Entity entity, Attribute attr) {
if (!entity.hasAttribute(attr)) {
return;
}
if (attr == Attribute.MEGA_WINDFURY && entity.hasAttribute(Attribute.WINDFURY)) {
entity.modifyAttribute(Attribute.NUMBER_OF_ATTACKS, WINDFURY_ATTACKS - MEGA_WINDFURY_ATTACKS);
}
if (attr == Attribute.WINDFURY && !entity.hasAttribute(Attribute.MEGA_WINDFURY)) {
entity.modifyAttribute(Attribute.NUMBER_OF_ATTACKS, 1 - WINDFURY_ATTACKS);
} else if (attr == Attribute.MEGA_WINDFURY) {
entity.modifyAttribute(Attribute.NUMBER_OF_ATTACKS, 1 - MEGA_WINDFURY_ATTACKS);
}
entity.removeAttribute(attr);
log("Removing attribute {} from {}", attr, entity);
}
public void removeCard(int playerId, Card card) {
Player player = context.getPlayer(playerId);
log("Card {} has been moved from the HAND to the GRAVEYARD", card);
card.setLocation(CardLocation.GRAVEYARD);
removeSpellTriggers(card);
player.getHand().remove(card);
player.getGraveyard().add(card);
}
public void removeAllCards(int playerId) {
for (Card card : context.getPlayer(playerId).getHand().toList()) {
removeCard(playerId, card);
}
}
public void removeCardFromDeck(int playerID, Card card) {
Player player = context.getPlayer(playerID);
log("Card {} has been moved from the DECK to the GRAVEYARD", card);
card.setLocation(CardLocation.GRAVEYARD);
removeSpellTriggers(card);
player.getDeck().remove(card);
player.getGraveyard().add(card);
}
public void removeQuests(Player player) {
log("All quests for {} have been destroyed", player.getName());
// This actually works amazingly
for (IGameEventListener quest : getQuests(player)) {
quest.onRemove(context);
context.removeTrigger(quest);
}
player.getSecrets().removeAll(player.getQuests());
player.getQuests().clear();
}
public void removeSummon(Summon summon, boolean peacefully) {
removeSpellTriggers(summon);
log("{} was removed", summon);
summon.setAttribute(Attribute.DESTROYED);
Player owner = context.getPlayer(summon.getOwner());
owner.getSummons().remove(summon);
if (peacefully) {
owner.getSetAsideZone().add(summon);
} else {
owner.getGraveyard().add(summon);
}
context.fireGameEvent(new BoardChangedEvent(context));
}
public void removeSecrets(Player player) {
log("All secrets for {} have been destroyed", player.getName());
// this only works while Secrets are the only SpellTrigger on the heroes
// Web - Lol, it works now.
for (IGameEventListener secret : getSecrets(player)) {
secret.onRemove(context);
context.removeTrigger(secret);
}
player.getSecrets().clear();
player.getSecrets().addAll(player.getQuests());
}
private void removeSpellTriggers(Entity entity) {
removeSpellTriggers(entity, true);
}
private void removeSpellTriggers(Entity entity, boolean removeAuras) {
EntityReference entityReference = entity.getReference();
for (IGameEventListener trigger : context.getTriggersAssociatedWith(entityReference)) {
if (!removeAuras && trigger instanceof Aura) {
continue;
}
log("SpellTrigger {} was removed for {}", trigger, entity);
trigger.onRemove(context);
}
context.removeTriggersAssociatedWith(entityReference, removeAuras);
for (Iterator<CardCostModifier> iterator = context.getCardCostModifiers().iterator(); iterator.hasNext();) {
CardCostModifier cardCostModifier = iterator.next();
if (cardCostModifier.getHostReference().equals(entityReference)) {
iterator.remove();
}
}
}
public void replaceCard(int playerId, Card oldCard, Card newCard) {
Player player = context.getPlayer(playerId);
if (newCard.getId() == IdFactory.UNASSIGNED) {
newCard.setId(idFactory.generateId());
}
if (!player.getHand().contains(oldCard)) {
return;
}
newCard.setOwner(playerId);
CardCollection hand = player.getHand();
if (newCard.getAttribute(Attribute.PASSIVE_TRIGGER) != null) {
TriggerDesc triggerDesc = (TriggerDesc) newCard.getAttribute(Attribute.PASSIVE_TRIGGER);
addGameEventListener(player, triggerDesc.create(), newCard);
}
log("{} replaces card {} with card {}", player.getName(), oldCard, newCard);
hand.replace(oldCard, newCard);
removeCard(playerId, oldCard);
newCard.setLocation(CardLocation.HAND);
context.fireGameEvent(new DrawCardEvent(context, playerId, newCard, null, false));
}
public void replaceCardInDeck(int playerId, Card oldCard, Card newCard) {
Player player = context.getPlayer(playerId);
if (newCard.getId() == IdFactory.UNASSIGNED) {
newCard.setId(idFactory.generateId());
}
if (!player.getDeck().contains(oldCard)) {
return;
}
newCard.setOwner(playerId);
CardCollection deck = player.getDeck();
if (newCard.getAttribute(Attribute.DECK_TRIGGER) != null) {
TriggerDesc triggerDesc = (TriggerDesc) newCard.getAttribute(Attribute.DECK_TRIGGER);
addGameEventListener(player, triggerDesc.create(), newCard);
}
log("{} replaces card {} with card {}", player.getName(), oldCard, newCard);
deck.replace(oldCard, newCard);
removeCardFromDeck(playerId, oldCard);
newCard.setLocation(CardLocation.DECK);
}
private void resolveBattlecry(int playerId, Actor actor) {
BattlecryAction battlecry = actor.getBattlecry();
Player player = context.getPlayer(playerId);
if (!battlecry.canBeExecuted(context, player)) {
return;
}
GameAction battlecryAction = null;
battlecry.setSource(actor.getReference());
if (battlecry.getTargetRequirement() != TargetSelection.NONE) {
List<Entity> validTargets = targetLogic.getValidTargets(context, player, battlecry);
if (validTargets.isEmpty()) {
return;
}
List<GameAction> battlecryActions = new ArrayList<>();
for (Entity validTarget : validTargets) {
GameAction targetedBattlecry = battlecry.clone();
targetedBattlecry.setTarget(validTarget);
battlecryActions.add(targetedBattlecry);
}
if (attributeExists(Attribute.ALL_RANDOM_FINAL_DESTINATION)) {
battlecryAction = battlecryActions.get(random(battlecryActions.size()));
} else {
battlecryAction = player.getBehaviour().requestAction(context, player, battlecryActions);
}
} else {
battlecryAction = battlecry;
}
if (hasAttribute(player, Attribute.DOUBLE_BATTLECRIES) && actor.getSourceCard().hasAttribute(Attribute.BATTLECRY)) {
// You need DOUBLE_BATTLECRIES before your battlecry action, not after.
performGameAction(playerId, battlecryAction);
if (!battlecry.canBeExecuted(context, player)) {
return;
}
performGameAction(playerId, battlecryAction);
} else {
performGameAction(playerId, battlecryAction);
}
}
public void resolveDeathrattles(Player player, Actor actor) {
resolveDeathrattles(player, actor, -1);
}
public void resolveDeathrattles(Player player, Actor actor, int boardPosition) {
if (!actor.hasAttribute(Attribute.DEATHRATTLES)) {
return;
}
if (boardPosition == -1) {
player.getSummons().indexOf(actor);
}
boolean doubleDeathrattles = hasAttribute(player, Attribute.DOUBLE_DEATHRATTLES);
EntityReference sourceReference = actor.getReference();
for (SpellDesc deathrattleTemplate : actor.getDeathrattles()) {
SpellDesc deathrattle = deathrattleTemplate.addArg(SpellArg.BOARD_POSITION_ABSOLUTE, boardPosition);
castSpell(player.getId(), deathrattle, sourceReference, EntityReference.NONE, false);
if (doubleDeathrattles) {
castSpell(player.getId(), deathrattle, sourceReference, EntityReference.NONE, false);
}
}
}
public void questTriggered(Player player, Quest quest) {
log("Quest was trigged: {}", quest.getSource());
player.getSecrets().remove(quest.getSource().getCardId());
player.getQuests().remove(quest.getSource().getCardId());
context.fireGameEvent(new QuestSuccessfulEvent(context, (QuestCard) quest.getSource(), player.getId()));
}
public void secretTriggered(Player player, Secret secret) {
log("Secret was trigged: {}", secret.getSource());
player.getSecrets().remove(secret.getSource().getCardId());
context.fireGameEvent(new SecretRevealedEvent(context, (SecretCard) secret.getSource(), player.getId()));
}
// TODO: circular dependency. Very ugly, refactor!
public void setContext(GameContext context) {
this.context = context;
}
public void setLoggingEnabled(boolean loggingEnabled) {
this.loggingEnabled = loggingEnabled;
}
public void shuffleToDeck(Player player, Card card) {
if (card.getId() == IdFactory.UNASSIGNED) {
card.setId(idFactory.generateId());
}
card.setLocation(CardLocation.DECK);
if (player.getDeck().getCount() < MAX_DECK_SIZE) {
player.getDeck().addRandomly(card);
if (card.getAttribute(Attribute.DECK_TRIGGER) != null) {
TriggerDesc triggerDesc = (TriggerDesc) card.getAttribute(Attribute.DECK_TRIGGER);
addGameEventListener(player, triggerDesc.create(), card);
}
log("Card {} has been shuffled to {}'s deck", card, player.getName());
}
}
public void silence(int playerId, Minion target) {
context.fireGameEvent(new SilenceEvent(context, playerId, target));
final HashSet<Attribute> immuneToSilence = new HashSet<Attribute>();
immuneToSilence.add(Attribute.HP);
immuneToSilence.add(Attribute.MAX_HP);
immuneToSilence.add(Attribute.BASE_HP);
immuneToSilence.add(Attribute.BASE_ATTACK);
immuneToSilence.add(Attribute.SUMMONING_SICKNESS);
immuneToSilence.add(Attribute.AURA_ATTACK_BONUS);
immuneToSilence.add(Attribute.AURA_HP_BONUS);
immuneToSilence.add(Attribute.AURA_UNTARGETABLE_BY_SPELLS);
immuneToSilence.add(Attribute.RACE);
immuneToSilence.add(Attribute.NUMBER_OF_ATTACKS);
List<Attribute> tags = new ArrayList<Attribute>();
tags.addAll(target.getAttributes().keySet());
for (Attribute attr : tags) {
if (immuneToSilence.contains(attr)) {
continue;
}
removeAttribute(target, attr);
}
removeSpellTriggers(target);
int oldMaxHp = target.getMaxHp();
target.setMaxHp(target.getAttributeValue(Attribute.BASE_HP));
target.setAttack(target.getAttributeValue(Attribute.BASE_ATTACK));
if (target.getHp() > target.getMaxHp()) {
target.setHp(target.getMaxHp());
} else if (oldMaxHp < target.getMaxHp()) {
target.setHp(target.getHp() + target.getMaxHp() - oldMaxHp);
}
log("{} was silenced", target);
}
public void startTurn(int playerId) {
Player player = context.getPlayer(playerId);
if (player.getMaxMana() < MAX_MANA) {
player.setMaxMana(player.getMaxMana() + 1);
}
player.getStatistics().startTurn();
player.setLockedMana(player.getAttributeValue(Attribute.OVERLOAD));
int mana = Math.min(player.getMaxMana() - player.getLockedMana(), MAX_MANA);
player.setMana(mana);
String manaString = player.getMana() + "/" + player.getMaxMana();
if (player.getLockedMana() > 0) {
manaString += " (" + player.getLockedMana() + " locked by overload)";
}
log("{} starts his turn with {} mana", player.getName(), manaString);
player.removeAttribute(Attribute.OVERLOAD);
for (Summon summon : player.getSummons()) {
summon.removeAttribute(Attribute.TEMPORARY_ATTACK_BONUS);
}
player.getHero().getHeroPower().setUsed(0);
player.getHero().activateWeapon(true);
refreshAttacksPerRound(player.getHero());
for (Summon summon : player.getSummons()) {
summon.removeAttribute(Attribute.SUMMONING_SICKNESS);
refreshAttacksPerRound(summon);
}
context.fireGameEvent(new TurnStartEvent(context, player.getId()));
drawCard(playerId, null);
checkForDeadEntities();
}
public boolean summon(int playerId, Summon summon) {
return summon(playerId, summon, null, -1, false);
}
public boolean summon(int playerId, Summon summon, Card source, int index, boolean resolveBattlecry) {
Player player = context.getPlayer(playerId);
if (!canSummonMoreMinions(player)) {
log("{} cannot summon any more summons, {} is destroyed", player.getName(), summon);
return false;
}
summon.setId(idFactory.generateId());
summon.setOwner(player.getId());
context.getSummonReferenceStack().push(summon.getReference());
log("{} summons {}", player.getName(), summon);
if (index < 0 || index >= player.getSummons().size()) {
player.getSummons().add(summon);
} else {
player.getSummons().add(index, summon);
}
if (summon instanceof Minion) {
Minion minion = (Minion) summon;
context.fireGameEvent(new BeforeSummonEvent(context, minion, source));
}
context.fireGameEvent(new BoardChangedEvent(context));
if (resolveBattlecry && summon.getBattlecry() != null) {
resolveBattlecry(player.getId(), summon);
checkForDeadEntities();
}
if (context.getEnvironment().get(Environment.TRANSFORM_REFERENCE) != null) {
summon = (Summon) context.resolveSingleTarget((EntityReference) context.getEnvironment().get(Environment.TRANSFORM_REFERENCE));
summon.setBattlecry(null);
context.getEnvironment().remove(Environment.TRANSFORM_REFERENCE);
}
context.fireGameEvent(new BoardChangedEvent(context));
if (summon instanceof Minion) {
Minion minion = (Minion) summon;
player.getStatistics().minionSummoned(minion, context.getTurn());
if (context.getEnvironment().get(Environment.TARGET_OVERRIDE) != null) {
Actor actor = (Actor) context.resolveSingleTarget((EntityReference) context.getEnvironment().get(Environment.TARGET_OVERRIDE));
context.getEnvironment().remove(Environment.TARGET_OVERRIDE);
SummonEvent summonEvent = new SummonEvent(context, actor, source);
context.fireGameEvent(summonEvent);
} else {
SummonEvent summonEvent = new SummonEvent(context, minion, source);
context.fireGameEvent(summonEvent);
}
applyAttribute(minion, Attribute.SUMMONING_SICKNESS);
refreshAttacksPerRound(minion);
} else if (summon instanceof Permanent) {
Permanent permanent = (Permanent) summon;
player.getStatistics().permanentSummoned(permanent, context.getTurn());
}
if (summon.hasSpellTrigger()) {
for (SpellTrigger trigger : summon.getSpellTriggers()) {
addGameEventListener(player, trigger, summon);
}
}
if (summon.getCardCostModifier() != null) {
addManaModifier(player, summon.getCardCostModifier(), summon);
}
if (summon instanceof Minion) {
Minion minion = (Minion) summon;
if (source != null) {
source.setAttribute(Attribute.ATTACK, source.getAttributeValue(Attribute.BASE_ATTACK));
source.setAttribute(Attribute.ATTACK_BONUS, 0);
source.setAttribute(Attribute.MAX_HP, source.getAttributeValue(Attribute.BASE_HP));
source.setAttribute(Attribute.HP, source.getAttributeValue(Attribute.BASE_HP));
source.setAttribute(Attribute.HP_BONUS, 0);
}
handleEnrage(minion);
context.getSummonReferenceStack().pop();
if (player.getSummons().contains(minion)) {
context.fireGameEvent(new AfterSummonEvent(context, minion, source));
}
}
context.fireGameEvent(new BoardChangedEvent(context));
return true;
}
/**
* Transforms a Minion into a new Minion.
*
* @param minion
* The original minion in play
* @param newMinion
* The new minion to transform into
*/
public void transformMinion(Summon summon, Summon newSummon) {
// Remove any spell triggers associated with the old minion.
removeSpellTriggers(summon);
Player owner = context.getPlayer(summon.getOwner());
int index = owner.getSummons().indexOf(summon);
owner.getSummons().remove(summon);
// If we want to straight up remove a minion from existence without
// killing it, this would be the best way.
if (newSummon != null) {
log("{} was transformed to {}", summon, newSummon);
// Give the new minion an ID.
newSummon.setId(idFactory.generateId());
newSummon.setOwner(owner.getId());
// If the minion being transforms is being summoned, replace the old
// minion on the stack.
// Otherwise, summon the add the new minion.
// However, do not give a summon event.
if (!context.getSummonReferenceStack().isEmpty() && context.getSummonReferenceStack().peek().equals(summon.getReference())
&& !context.getEnvironment().containsKey(Environment.TRANSFORM_REFERENCE)) {
context.getEnvironment().put(Environment.TRANSFORM_REFERENCE, newSummon.getReference());
owner.getSummons().add(index, newSummon);
// It's quite possible that this is actually supposed to add the
// minion to the zone it was originally in.
// This means minions in the SetAsideZone or the Graveyard that are
// targeted (through bizarre mechanics)
// add the minion to there. This will be tested eventually with
// Resurrect, Recombobulator, and Illidan.
// Since this is unknown, this is the patch for it.
} else if (!owner.getSetAsideZone().contains(summon)) {
if (index < 0 || index >= owner.getSummons().size()) {
owner.getSummons().add(newSummon);
} else {
owner.getSummons().add(index, newSummon);
}
applyAttribute(newSummon, Attribute.SUMMONING_SICKNESS);
refreshAttacksPerRound(newSummon);
if (newSummon.hasSpellTrigger()) {
for (SpellTrigger spellTrigger : newSummon.getSpellTriggers()) {
addGameEventListener(owner, spellTrigger, newSummon);
}
}
if (newSummon.getCardCostModifier() != null) {
addManaModifier(owner, newSummon.getCardCostModifier(), newSummon);
}
handleEnrage(newSummon);
} else {
owner.getSetAsideZone().add(newSummon);
newSummon.setId(idFactory.generateId());
newSummon.setOwner(owner.getId());
removeSpellTriggers(newSummon);
return;
}
}
// Move the old minion to the Set Aside Zone
owner.getSetAsideZone().add(summon);
context.fireGameEvent(new BoardChangedEvent(context));
}
public void useHeroPower(int playerId) {
Player player = context.getPlayer(playerId);
HeroPower power = player.getHero().getHeroPower();
int modifiedManaCost = getModifiedManaCost(player, power);
modifyCurrentMana(playerId, -modifiedManaCost);
log("{} uses {}", player.getName(), power);
power.markUsed();
player.getStatistics().cardPlayed(power, context.getTurn());
context.fireGameEvent(new HeroPowerUsedEvent(context, playerId, power));
}
}