forge.game.combat.Combat.java Source code

Java tutorial

Introduction

Here is the source code for forge.game.combat.Combat.java

Source

/*
 * Forge: Play Magic: the Gathering.
 * Copyright (C) 2011  Forge Team
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package forge.game.combat;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.commons.lang3.tuple.Pair;

import com.google.common.base.Function;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import forge.game.GameEntity;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.player.Player;
import forge.game.trigger.TriggerType;
import forge.util.collect.FCollection;
import forge.util.collect.FCollectionView;

/**
 * <p>
 * Combat class.
 * </p>
 * 
 * @author Forge
 * @version $Id: Combat.java 29786 2015-07-17 10:54:34Z Kulnaro $
 */
public class Combat {
    private final Player playerWhoAttacks;
    private final AttackConstraints attackConstraints;
    // Defenders, as they are attacked by hostile forces
    private final FCollection<GameEntity> attackableEntries = new FCollection<GameEntity>();

    // Keyed by attackable defender (player or planeswalker)
    private final Multimap<GameEntity, AttackingBand> attackedByBands = Multimaps
            .synchronizedMultimap(ArrayListMultimap.<GameEntity, AttackingBand>create());
    private final Multimap<AttackingBand, Card> blockedBands = Multimaps
            .synchronizedMultimap(ArrayListMultimap.<AttackingBand, Card>create());

    private final HashMap<Card, Integer> defendingDamageMap = new HashMap<Card, Integer>();

    private Map<Card, CardCollection> attackersOrderedForDamageAssignment = new HashMap<Card, CardCollection>();
    private Map<Card, CardCollection> blockersOrderedForDamageAssignment = new HashMap<Card, CardCollection>();
    private Map<GameEntity, CombatLki> lkiCache = new HashMap<GameEntity, CombatLki>();
    private Multimap<Card, GameEntity> dealtDamageTo = HashMultimap.create();
    private Multimap<Card, GameEntity> dealtDamageToThisCombat = HashMultimap.create();

    // List holds creatures who have dealt 1st strike damage to disallow them deal damage on regular basis (unless they have double-strike KW) 
    private CardCollection combatantsThatDealtFirstStrikeDamage = new CardCollection();

    public Combat(final Player attacker) {
        playerWhoAttacks = attacker;

        // Create keys for all possible attack targets
        for (final GameEntity defender : CombatUtil.getAllPossibleDefenders(playerWhoAttacks)) {
            attackableEntries.add(defender);
        }

        attackConstraints = new AttackConstraints(this);
    }

    public void endCombat() {
        //backup attackers and blockers
        CardCollection attackers = getAttackers();
        CardCollection blockers = getAllBlockers();

        //clear all combat-related collections
        attackableEntries.clear();
        attackedByBands.clear();
        blockedBands.clear();
        defendingDamageMap.clear();
        attackersOrderedForDamageAssignment.clear();
        blockersOrderedForDamageAssignment.clear();
        lkiCache.clear();
        combatantsThatDealtFirstStrikeDamage.clear();
        dealtDamageToThisCombat.clear();

        //update view for all attackers and blockers
        for (Card c : attackers) {
            c.updateAttackingForView();
        }
        for (Card c : blockers) {
            c.updateBlockingForView();
        }
    }

    public final void clearAttackers() {
        for (final Card attacker : getAttackers()) {
            removeFromCombat(attacker);
        }
    }

    public final Player getAttackingPlayer() {
        return playerWhoAttacks;
    }

    public final AttackConstraints getAttackConstraints() {
        return attackConstraints;
    }

    public final FCollectionView<GameEntity> getDefenders() {
        return attackableEntries;
    }

    public final FCollection<GameEntity> getDefendersControlledBy(Player who) {
        FCollection<GameEntity> res = new FCollection<GameEntity>();
        for (GameEntity ge : attackableEntries) {
            // if defender is the player himself or his cards
            if (ge == who || ge instanceof Card && ((Card) ge).getController() == who) {
                res.add(ge);
            }
        }
        return res;
    }

    public final FCollectionView<Player> getDefendingPlayers() {
        return new FCollection<Player>(Iterables.filter(attackableEntries, Player.class));
    }

    public final CardCollection getDefendingPlaneswalkers() {
        return new CardCollection(Iterables.filter(attackableEntries, Card.class));
    }

    public final Map<Card, GameEntity> getAttackersAndDefenders() {
        return Maps.asMap(getAttackers(), new Function<Card, GameEntity>() {
            @Override
            public GameEntity apply(final Card attacker) {
                return getDefenderByAttacker(attacker);
            }
        });
    }

    public final List<AttackingBand> getAttackingBandsOf(GameEntity defender) {
        return Lists.newArrayList(attackedByBands.get(defender));
    }

    public final CardCollection getAttackersOf(GameEntity defender) {
        CardCollection result = new CardCollection();
        for (AttackingBand v : attackedByBands.get(defender)) {
            result.addAll(v.getAttackers());
        }
        return result;
    }

    public final void addAttacker(final Card c, GameEntity defender) {
        addAttacker(c, defender, null);
    }

    public final void addAttacker(final Card c, GameEntity defender, AttackingBand band) {
        Collection<AttackingBand> attackersOfDefender = attackedByBands.get(defender);
        if (attackersOfDefender == null) {
            System.out.println("Trying to add Attacker " + c + " to missing defender " + defender);
            return;
        }

        // This is trying to fix the issue of an attacker existing in two bands at once
        AttackingBand existingBand = getBandOfAttacker(c);
        if (existingBand != null) {
            existingBand.removeAttacker(c);
        }

        if (band == null || !attackersOfDefender.contains(band)) {
            band = new AttackingBand(c);
            attackersOfDefender.add(band);
        } else {
            band.addAttacker(c);
        }
        c.updateAttackingForView();
    }

    public final GameEntity getDefenderByAttacker(final Card c) {
        return getDefenderByAttacker(getBandOfAttacker(c));
    }

    public final GameEntity getDefenderByAttacker(final AttackingBand c) {
        for (Entry<GameEntity, AttackingBand> e : attackedByBands.entries()) {
            if (e.getValue() == c) {
                return e.getKey();
            }
        }
        return null;
    }

    public final Player getDefenderPlayerByAttacker(final Card c) {
        GameEntity defender = getDefenderByAttacker(c);

        // System.out.println(c.toString() + " attacks " + defender.toString());
        if (defender instanceof Player) {
            return (Player) defender;
        }

        // maybe attack on a controlled planeswalker?
        if (defender instanceof Card) {
            return ((Card) defender).getController();
        }
        return null;
    }

    // takes LKI into consideration, should use it at all times (though a single iteration over multimap seems faster)
    public final AttackingBand getBandOfAttacker(final Card c) {
        for (AttackingBand ab : attackedByBands.values()) {
            if (ab.contains(c)) {
                return ab;
            }
        }
        CombatLki lki = lkiCache.get(c);
        return lki == null || !lki.isAttacker ? null : lki.getFirstBand();
    }

    public final AttackingBand getBandOfAttackerNotNull(final Card c) {
        AttackingBand band = getBandOfAttacker(c);
        if (band == null) {
            throw new NullPointerException("No band for attacker " + c);
        }
        return band;
    }

    public final List<AttackingBand> getAttackingBands() {
        return Lists.newArrayList(attackedByBands.values());
    }

    /**
     * Checks if a card is attacking, returns true if the card was attacking when it left the battlefield
     */
    public final boolean isLKIAttacking(final Card c) {
        AttackingBand ab = getBandOfAttacker(c);
        return ab != null;
    }

    public boolean isAttacking(Card card, GameEntity defender) {
        AttackingBand ab = getBandOfAttacker(card);
        for (Entry<GameEntity, AttackingBand> ee : attackedByBands.entries()) {
            if (ee.getValue() == ab) {
                return ee.getKey() == defender;
            }
        }
        return false;
    }

    /**
     * Checks if a card is currently attacking, returns false if the card is not currently attacking, even if its LKI was.
     */
    public final boolean isAttacking(Card card) {
        for (AttackingBand ab : attackedByBands.values()) {
            if (ab.contains(card)) {
                return true;
            }
        }
        return false;
    }

    public final CardCollection getAttackers() {
        CardCollection result = new CardCollection();
        for (AttackingBand ab : attackedByBands.values()) {
            result.addAll(ab.getAttackers());
        }
        return result;
    }

    public final CardCollection getBlockers(final Card card) {
        // If requesting the ordered blocking list pass true, directly. 
        AttackingBand band = getBandOfAttacker(card);
        Collection<Card> blockers = blockedBands.get(band);
        return blockers == null ? new CardCollection() : new CardCollection(blockers);
    }

    public final boolean isBlocked(final Card attacker) {
        AttackingBand band = getBandOfAttacker(attacker);
        return band == null ? false : Boolean.TRUE.equals(band.isBlocked());
    }

    // Some cards in Alpha may UNBLOCK an attacker, so second parameter is not always-true
    public final void setBlocked(final Card attacker, boolean value) {
        getBandOfAttackerNotNull(attacker).setBlocked(value); // called by Curtain of Light, Dazzling Beauty, Trap Runner
    }

    public final void addBlocker(final Card attacker, final Card blocker) {
        final AttackingBand band = getBandOfAttackerNotNull(attacker);
        blockedBands.put(band, blocker);
        // If damage is already assigned, add this blocker as a "late entry"
        if (blockersOrderedForDamageAssignment.containsKey(attacker)) {
            addBlockerToDamageAssignmentOrder(attacker, blocker);
        }
        blocker.updateBlockingForView();
    }

    // remove blocked from specific attacker
    public final void removeBlockAssignment(final Card attacker, final Card blocker) {
        AttackingBand band = getBandOfAttackerNotNull(attacker);
        Collection<Card> cc = blockedBands.get(band);
        if (cc != null) {
            cc.remove(blocker);
        }
        blocker.updateBlockingForView();
    }

    // remove blocker from everywhere
    public final void undoBlockingAssignment(final Card blocker) {
        CardCollection toRemove = new CardCollection(blocker);
        blockedBands.values().removeAll(toRemove);
        blocker.updateBlockingForView();
    }

    public final CardCollection getAllBlockers() {
        CardCollection result = new CardCollection();
        for (Card blocker : blockedBands.values()) {
            if (!result.contains(blocker)) {
                result.add(blocker);
            }
        }
        return result;
    }

    public final CardCollection getBlockers(final AttackingBand band) {
        Collection<Card> blockers = blockedBands.get(band);
        return blockers == null ? new CardCollection() : new CardCollection(blockers);
    }

    public final CardCollection getAttackersBlockedBy(final Card blocker) {
        CardCollection blocked = new CardCollection();
        for (Entry<AttackingBand, Card> s : blockedBands.entries()) {
            if (s.getValue().equals(blocker)) {
                blocked.addAll(s.getKey().getAttackers());
            }
        }
        return blocked;
    }

    public final FCollectionView<AttackingBand> getAttackingBandsBlockedBy(Card blocker) {
        FCollection<AttackingBand> bands = new FCollection<AttackingBand>();
        for (Entry<AttackingBand, Card> kv : blockedBands.entries()) {
            if (kv.getValue().equals(blocker)) {
                bands.add(kv.getKey());
            }
        }
        return bands;
    }

    public Player getDefendingPlayerRelatedTo(final Card source) {
        Card attacker = source;
        if (source.isAura()) {
            attacker = source.getEnchantingCard();
        } else if (source.isEquipment()) {
            attacker = source.getEquipping();
        } else if (source.isFortification()) {
            attacker = source.getFortifying();
        }

        // return the corresponding defender
        return getDefenderPlayerByAttacker(attacker);
    }

    /** If there are multiple blockers, the Attacker declares the Assignment Order */
    public void orderBlockersForDamageAssignment() { // this method performs controller's role 
        List<Pair<Card, CardCollection>> blockersNeedManualOrdering = new ArrayList<>();
        for (AttackingBand band : attackedByBands.values()) {
            if (band.isEmpty())
                continue;

            Collection<Card> blockers = blockedBands.get(band);
            if (blockers == null || blockers.isEmpty()) {
                continue;
            }

            for (Card attacker : band.getAttackers()) {
                if (blockers.size() <= 1) {
                    blockersOrderedForDamageAssignment.put(attacker, new CardCollection(blockers));
                } else { // process it a bit later
                    blockersNeedManualOrdering.add(Pair.of(attacker, new CardCollection(blockers))); // we know there's a list
                }
            }
        }

        // brought this out of iteration on bands to avoid concurrency problems 
        for (Pair<Card, CardCollection> pair : blockersNeedManualOrdering) {
            // Damage Ordering needs to take cards like Melee into account, is that happening?
            CardCollection orderedBlockers = playerWhoAttacks.getController().orderBlockers(pair.getLeft(),
                    pair.getRight()); // we know there's a list
            blockersOrderedForDamageAssignment.put(pair.getLeft(), orderedBlockers);
        }
    }

    /**
     * Add a blocker to the damage assignment order of an attacker. The
     * relative order of creatures already blocking the attacker may not be
     * changed. Performs controller's role.
     * 
     * @param attacker the attacking creature.
     * @param blocker the blocking creature.
     */
    public void addBlockerToDamageAssignmentOrder(Card attacker, Card blocker) {
        final CardCollection oldBlockers = blockersOrderedForDamageAssignment.get(attacker);
        if (oldBlockers == null || oldBlockers.isEmpty()) {
            blockersOrderedForDamageAssignment.put(attacker, new CardCollection(blocker));
        } else {
            CardCollection orderedBlockers = playerWhoAttacks.getController().orderBlocker(attacker, blocker,
                    oldBlockers);
            blockersOrderedForDamageAssignment.put(attacker, orderedBlockers);
        }
    }

    public void orderAttackersForDamageAssignment() { // this method performs controller's role
        // If there are multiple blockers, the Attacker declares the Assignment Order
        for (final Card blocker : getAllBlockers()) {
            orderAttackersForDamageAssignment(blocker);
        }
    }

    public void orderAttackersForDamageAssignment(Card blocker) { // this method performs controller's role
        CardCollection attackers = getAttackersBlockedBy(blocker);
        // They need a reverse map here: Blocker => List<Attacker>

        Player blockerCtrl = blocker.getController();
        CardCollection orderedAttacker = attackers.size() <= 1 ? attackers
                : blockerCtrl.getController().orderAttackers(blocker, attackers);

        // Damage Ordering needs to take cards like Melee into account, is that happening?
        attackersOrderedForDamageAssignment.put(blocker, orderedAttacker);
    }

    // removes references to this attacker from all indices and orders
    private void unregisterAttacker(final Card c, AttackingBand ab) {
        blockersOrderedForDamageAssignment.remove(c);

        Collection<Card> blockers = blockedBands.get(ab);
        if (blockers != null) {
            for (Card b : blockers) {
                // Clear removed attacker from assignment order 
                if (attackersOrderedForDamageAssignment.containsKey(b)) {
                    attackersOrderedForDamageAssignment.get(b).remove(c);
                }
            }
        }
    }

    // removes references to this defender from all indices and orders
    private void unregisterDefender(final Card c, AttackingBand bandBeingBlocked) {
        attackersOrderedForDamageAssignment.remove(c);
        for (Card atk : bandBeingBlocked.getAttackers()) {
            if (blockersOrderedForDamageAssignment.containsKey(atk)) {
                blockersOrderedForDamageAssignment.get(atk).remove(c);
            }
        }
    }

    // remove a combatant whose side is unknown
    public final void removeFromCombat(final Card c) {
        AttackingBand ab = getBandOfAttacker(c);
        if (ab != null) {
            unregisterAttacker(c, ab);
            ab.removeAttacker(c);
            c.updateAttackingForView();
            return;
        }

        // if not found in attackers, look for this card in blockers
        for (Entry<AttackingBand, Card> be : blockedBands.entries()) {
            if (be.getValue().equals(c)) {
                unregisterDefender(c, be.getKey());
            }
        }

        // remove card from map
        while (blockedBands.values().remove(c))
            ;
        c.updateBlockingForView();
    }

    public final boolean removeAbsentCombatants() {
        // iterate all attackers and remove illegal declarations
        CardCollection missingCombatants = new CardCollection();
        for (Entry<GameEntity, AttackingBand> ee : attackedByBands.entries()) {
            CardCollectionView atk = ee.getValue().getAttackers();
            for (int i = atk.size() - 1; i >= 0; i--) { // might remove items from collection, so no iterators
                Card c = atk.get(i);
                if (!c.isInPlay() || !c.isCreature()) {
                    missingCombatants.add(c);
                }
            }
        }

        for (Entry<AttackingBand, Card> be : blockedBands.entries()) {
            Card blocker = be.getValue();
            if (!blocker.isInPlay() || !blocker.isCreature()) {
                missingCombatants.add(blocker);
            }
        }

        if (missingCombatants.isEmpty()) {
            return false;
        }

        for (Card c : missingCombatants) {
            removeFromCombat(c);
        }
        return true;
    }

    // Call this method right after turn-based action of declare blockers has been performed
    public final void fireTriggersForUnblockedAttackers() {
        for (AttackingBand ab : attackedByBands.values()) {
            Collection<Card> blockers = blockedBands.get(ab);
            boolean isBlocked = blockers != null && !blockers.isEmpty();
            ab.setBlocked(isBlocked);

            if (!isBlocked) {
                for (Card attacker : ab.getAttackers()) {
                    // Run Unblocked Trigger
                    final HashMap<String, Object> runParams = new HashMap<String, Object>();
                    runParams.put("Attacker", attacker);
                    runParams.put("Defender", getDefenderByAttacker(attacker));
                    runParams.put("DefendingPlayer", getDefenderPlayerByAttacker(attacker));
                    attacker.getGame().getTriggerHandler().runTrigger(TriggerType.AttackerUnblocked, runParams,
                            false);
                }
            }
        }
    }

    private final boolean assignBlockersDamage(boolean firstStrikeDamage) {
        // Assign damage by Blockers
        final CardCollection blockers = getAllBlockers();
        boolean assignedDamage = false;

        for (final Card blocker : blockers) {
            if (!dealDamageThisPhase(blocker, firstStrikeDamage)) {
                continue;
            }

            if (firstStrikeDamage) {
                combatantsThatDealtFirstStrikeDamage.add(blocker);
            }

            CardCollection attackers = attackersOrderedForDamageAssignment.get(blocker);

            final int damage = blocker.getNetCombatDamage();

            if (!attackers.isEmpty()) {
                Player attackingPlayer = getAttackingPlayer();
                Player assigningPlayer = blocker.getController();

                if (AttackingBand.isValidBand(attackers, true))
                    assigningPlayer = attackingPlayer;

                assignedDamage = true;
                Map<Card, Integer> map = assigningPlayer.getController().assignCombatDamage(blocker, attackers,
                        damage, null, assigningPlayer != blocker.getController());
                for (Entry<Card, Integer> dt : map.entrySet()) {
                    dt.getKey().addAssignedDamage(dt.getValue(), blocker);
                }
            }
        }
        return assignedDamage;
    }

    private final boolean assignAttackersDamage(boolean firstStrikeDamage) {
        // Assign damage by Attackers
        defendingDamageMap.clear(); // this should really happen in deal damage
        CardCollection orderedBlockers = null;
        final CardCollection attackers = getAttackers();
        boolean assignedDamage = false;
        for (final Card attacker : attackers) {
            if (!dealDamageThisPhase(attacker, firstStrikeDamage)) {
                continue;
            }

            if (firstStrikeDamage) {
                combatantsThatDealtFirstStrikeDamage.add(attacker);
            }

            // If potential damage is 0, continue along
            final int damageDealt = attacker.getNetCombatDamage();
            if (damageDealt <= 0) {
                continue;
            }

            AttackingBand band = getBandOfAttacker(attacker);
            if (band == null) {
                continue;
            }

            boolean trampler = attacker.hasKeyword("Trample");
            orderedBlockers = blockersOrderedForDamageAssignment.get(attacker);
            assignedDamage = true;
            // If the Attacker is unblocked, or it's a trampler and has 0 blockers, deal damage to defender
            if (orderedBlockers == null || orderedBlockers.isEmpty()) {
                if (trampler || !band.isBlocked()) { // this is called after declare blockers, no worries 'bout nulls in isBlocked
                    addDefendingDamage(damageDealt, attacker);
                } // No damage happens if blocked but no blockers left
            } else {
                GameEntity defender = getDefenderByAttacker(band);
                Player assigningPlayer = getAttackingPlayer();
                // Defensive Formation is very similar to Banding with Blockers
                // It allows the defending player to assign damage instead of the attacking player
                if (defender instanceof Player
                        && defender.hasKeyword("You assign combat damage of each creature attacking you.")) {
                    assigningPlayer = (Player) defender;
                } else if (AttackingBand.isValidBand(orderedBlockers, true)) {
                    assigningPlayer = orderedBlockers.get(0).getController();
                }

                Map<Card, Integer> map = assigningPlayer.getController().assignCombatDamage(attacker,
                        orderedBlockers, damageDealt, defender, getAttackingPlayer() != assigningPlayer);
                for (Entry<Card, Integer> dt : map.entrySet()) {
                    if (dt.getKey() == null) {
                        if (dt.getValue() > 0)
                            addDefendingDamage(dt.getValue(), attacker);
                    } else {
                        dt.getKey().addAssignedDamage(dt.getValue(), attacker);
                    }
                }
            } // if !hasFirstStrike ...
        } // for
        return assignedDamage;
    }

    private final boolean dealDamageThisPhase(Card combatant, boolean firstStrikeDamage) {
        // During first strike damage, double strike and first strike deal damage
        // During regular strike damage, double strike and anyone who hasn't dealt damage deal damage
        if (combatant.hasDoubleStrike()) {
            return true;
        }
        if (firstStrikeDamage && combatant.hasFirstStrike()) {
            return true;
        }
        return !firstStrikeDamage && !combatantsThatDealtFirstStrikeDamage.contains(combatant);
    }

    // Damage to whatever was protected there. 
    private final void addDefendingDamage(final int n, final Card source) {
        final GameEntity ge = getDefenderByAttacker(source);

        if (ge instanceof Card) {
            final Card planeswalker = (Card) ge;
            planeswalker.addAssignedDamage(n, source);
            return;
        }

        if (!defendingDamageMap.containsKey(source)) {
            defendingDamageMap.put(source, n);
        } else {
            defendingDamageMap.put(source, defendingDamageMap.get(source) + n);
        }
    }

    public final boolean assignCombatDamage(boolean firstStrikeDamage) {
        boolean assignedDamage = assignAttackersDamage(firstStrikeDamage);
        assignedDamage |= assignBlockersDamage(firstStrikeDamage);
        if (!firstStrikeDamage) {
            // Clear first strike damage list since it doesn't matter anymore
            combatantsThatDealtFirstStrikeDamage.clear();
        }
        return assignedDamage;
    }

    public final void addDealtDamageTo(final Card source, final GameEntity ge) {
        dealtDamageTo.put(source, ge);
    }

    public void dealAssignedDamage() {
        // This function handles both Regular and First Strike combat assignment
        final HashMap<Card, Integer> defMap = defendingDamageMap;
        final HashMap<GameEntity, CardCollection> wasDamaged = new HashMap<GameEntity, CardCollection>();
        final Map<Card, Integer> damageDealtThisCombat = new HashMap<Card, Integer>();

        for (final Entry<Card, Integer> entry : defMap.entrySet()) {
            GameEntity defender = getDefenderByAttacker(entry.getKey());
            if (defender instanceof Player) { // player
                if (((Player) defender).addCombatDamage(entry.getValue(), entry.getKey())) {
                    if (wasDamaged.containsKey(defender)) {
                        wasDamaged.get(defender).add(entry.getKey());
                    } else {
                        CardCollection l = new CardCollection();
                        l.add(entry.getKey());
                        wasDamaged.put(defender, l);
                    }
                    damageDealtThisCombat.put(entry.getKey(), entry.getValue());
                }
            } else if (defender instanceof Card) { // planeswalker
                if (((Card) defender).getController().addCombatDamage(entry.getValue(), entry.getKey())) {
                    if (wasDamaged.containsKey(defender)) {
                        wasDamaged.get(defender).add(entry.getKey());
                    } else {
                        CardCollection l = new CardCollection();
                        l.add(entry.getKey());
                        wasDamaged.put(defender, l);
                    }
                }
            }
        }

        // this can be much better below here...

        final CardCollection combatants = new CardCollection();
        combatants.addAll(getAttackers());
        combatants.addAll(getAllBlockers());
        combatants.addAll(getDefendingPlaneswalkers());

        Card c;
        for (int i = 0; i < combatants.size(); i++) {
            c = combatants.get(i);

            // if no assigned damage to resolve, move to next
            if (c.getTotalAssignedDamage() == 0) {
                continue;
            }

            final Map<Card, Integer> assignedDamageMap = c.getAssignedDamageMap();
            final HashMap<Card, Integer> damageMap = new HashMap<Card, Integer>();

            for (final Entry<Card, Integer> entry : assignedDamageMap.entrySet()) {
                final Card crd = entry.getKey();
                damageMap.put(crd, entry.getValue());
                if (entry.getValue() > 0) {
                    if (damageDealtThisCombat.containsKey(crd)) {
                        damageDealtThisCombat.put(crd, damageDealtThisCombat.get(crd) + entry.getValue());
                    } else {
                        damageDealtThisCombat.put(crd, entry.getValue());
                    }
                }
            }
            c.addCombatDamage(damageMap);

            damageMap.clear();
            c.clearAssignedDamage();
        }

        // Run triggers
        for (final GameEntity ge : wasDamaged.keySet()) {
            final HashMap<String, Object> runParams = new HashMap<String, Object>();
            runParams.put("DamageSources", wasDamaged.get(ge));
            runParams.put("DamageTarget", ge);
            ge.getGame().getTriggerHandler().runTrigger(TriggerType.CombatDamageDoneOnce, runParams, false);
        }
        // This was deeper before, but that resulted in the stack entry acting like before.

        // when ... deals combat damage to one or more
        for (final Card damageSource : dealtDamageTo.keySet()) {
            final HashMap<String, Object> runParams = new HashMap<String, Object>();
            int dealtDamage = damageDealtThisCombat.containsKey(damageSource)
                    ? damageDealtThisCombat.get(damageSource)
                    : 0;
            runParams.put("DamageSource", damageSource);
            runParams.put("DamageTargets", dealtDamageTo.get(damageSource));
            runParams.put("DamageAmount", dealtDamage);
            damageSource.getGame().getTriggerHandler().runTrigger(TriggerType.DealtCombatDamageOnce, runParams,
                    false);
        }
        dealtDamageToThisCombat.putAll(dealtDamageTo);
        dealtDamageTo.clear();
    }

    public final boolean isUnblocked(final Card att) {
        AttackingBand band = getBandOfAttacker(att);
        return band == null ? false : Boolean.FALSE.equals(band.isBlocked());
    }

    public final CardCollection getUnblockedAttackers() {
        CardCollection unblocked = new CardCollection();
        for (AttackingBand ab : attackedByBands.values()) {
            if (Boolean.TRUE.equals(ab.isBlocked())) {
                unblocked.addAll(ab.getAttackers());
            }
        }
        return unblocked;
    }

    public boolean isPlayerAttacked(Player who) {
        for (GameEntity defender : attackedByBands.keySet()) {
            Card defenderAsCard = defender instanceof Card ? (Card) defender : null;
            if ((null != defenderAsCard && defenderAsCard.getController() != who)
                    || (null == defenderAsCard && defender != who)) {
                continue; // defender is not related to player 'who'
            }
            for (AttackingBand ab : attackedByBands.get(defender)) {
                if (!ab.isEmpty()) {
                    return true;
                }
            }
        }
        return false;
    }

    public boolean isBlocking(Card blocker) {
        if (!blocker.isInPlay()) {
            CombatLki lki = lkiCache.get(blocker);
            return null != lki && !lki.isAttacker; // was blocking something anyway
        }
        return blockedBands.containsValue(blocker);
    }

    public boolean isBlocking(Card blocker, Card attacker) {
        AttackingBand ab = getBandOfAttacker(attacker);
        if (!blocker.isInPlay()) {
            CombatLki lki = lkiCache.get(blocker);
            return null != lki && !lki.isAttacker && lki.relatedBands.contains(ab); // was blocking that very band
        }
        Collection<Card> blockers = blockedBands.get(ab);
        return blockers != null && blockers.contains(blocker);
    }

    public void saveLKI(Card lastKnownInfo) {
        FCollectionView<AttackingBand> attackersBlocked = null;
        final AttackingBand attackingBand = getBandOfAttacker(lastKnownInfo);
        final boolean isAttacker = attackingBand != null;
        if (!isAttacker) {
            attackersBlocked = getAttackingBandsBlockedBy(lastKnownInfo);
            if (attackersBlocked.isEmpty()) {
                return; // card was not even in combat
            }
        }
        final FCollectionView<AttackingBand> relatedBands = isAttacker
                ? new FCollection<AttackingBand>(attackingBand)
                : attackersBlocked;
        lkiCache.put(lastKnownInfo, new CombatLki(isAttacker, relatedBands));
    }
}