package net.sf.colossus.server; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import net.sf.colossus.common.Constants; import net.sf.colossus.game.Battle; import net.sf.colossus.game.BattleCritter; import net.sf.colossus.game.BattlePhase; import net.sf.colossus.game.Creature; import net.sf.colossus.game.Legion; import net.sf.colossus.game.Player; import net.sf.colossus.game.actions.SummonUndo; import net.sf.colossus.variant.BattleHex; import net.sf.colossus.variant.MasterHex; /** * Class Battle holds data about a Titan battle. * * It has utility functions related to incrementing the phase, managing * moves, and managing strikes. * * @author David Ripton * @author Romain Dolbeau */ public final class BattleServerSide extends Battle { public static enum AngelSummoningStates { NO_KILLS, FIRST_BLOOD, TOO_LATE } public static enum LegionTags { DEFENDER, ATTACKER } private static final Logger LOGGER = Logger .getLogger(BattleServerSide.class.getName()); private Server server; private LegionTags activeLegionTag; private BattlePhase phase; private AngelSummoningStates summonState = AngelSummoningStates.NO_KILLS; private int carryDamage; private boolean attackerElim; private boolean defenderElim; private boolean battleOver; private boolean attackerEntered; private boolean conceded; private boolean preStrikeEffectsApplied = false; /** * Set of hexes for valid carry targets */ private final Set<BattleHex> carryTargets = new HashSet<BattleHex>(); private final PhaseAdvancer phaseAdvancer = new BattlePhaseAdvancer(); private int pointsScored = 0; private final BattleMovementServerSide battleMovement; BattleServerSide(GameServerSide game, Legion attacker, Legion defender, LegionTags activeLegionTag, MasterHex masterHex, BattlePhase phase) { super(game, attacker, defender, masterHex); this.server = game.getServer(); this.activeLegionTag = activeLegionTag; this.phase = phase; this.battleMovement = new BattleMovementServerSide(game.getOptions(), getGame()); // Set defender's entry side opposite attacker's. defender.setEntrySide(attacker.getEntrySide().getOpposingSide()); // Make sure defender can recruit, even if savegame is off. defender.setRecruit(null); // Make sure donor is null, if it remained set from an earlier battle ((LegionServerSide)attacker).getPlayer().setDonor(null); LOGGER.info(attacker + " (" + attacker.getPlayer() + ") attacks " + defender + " (" + defender.getPlayer() + ")" + " in " + masterHex); placeLegion(attacker); placeLegion(defender); } // Used when loading a game public void setServer(Server server) { this.server = server; } public void cleanRefs() { this.server = null; } private void placeLegion(Legion legion) { BattleHex entrance = getLocation().getTerrain().getEntrance( legion.getEntrySide()); for (CreatureServerSide critter : ((LegionServerSide)legion) .getCreatures()) { BattleHex currentHex = critter.getCurrentHex(); if (currentHex == null) { currentHex = entrance; } BattleHex startingHex = critter.getStartingHex(); if (startingHex == null) { startingHex = entrance; } critter.setBattleInfo(currentHex, startingHex, this); } } private void placeCritter(CreatureServerSide critter) { BattleHex entrance = getLocation().getTerrain().getEntrance( critter.getLegion().getEntrySide()); critter.setBattleInfo(entrance, entrance, this); server.allPlaceNewChit(critter); } private void initBattleChits(LegionServerSide legion) { for (CreatureServerSide creature : legion.getCreatures()) { server.allPlaceNewChit(creature); } } /** We need to do two-stage construction so that game.battle * is non-null earlier. */ void init() { server.allInitBattle(getLocation()); initBattleChits(getAttackingLegion()); initBattleChits(getDefendingLegion()); boolean advance = false; switch (phase) { case SUMMON: advance = setupSummon(); break; case RECRUIT: advance = setupRecruit(); break; case MOVE: advance = setupMove(); break; default: if (phase.isFightPhase()) { advance = setupFight(); } else { LOGGER.log(Level.SEVERE, "Bogus phase"); } } if (advance) { advancePhase(); } } /** * Override with covariant return type to ease transition into new model. */ @Override public GameServerSide getGame() { return (GameServerSide)super.getGame(); } // TODO perhaps temporary, only for BattleMovementServerSide right now public boolean isDefenderActive() { return activeLegionTag == LegionTags.DEFENDER; } @Override public Legion getBattleActiveLegion() { return getLegion(activeLegionTag); } Player getBattleActivePlayer() { return getBattleActiveLegion().getPlayer(); } /** * Override with covariant return type to ease transition into new model. */ @Override public LegionServerSide getAttackingLegion() { return (LegionServerSide)super.getAttackingLegion(); } /** * Override with covariant return type to ease transition into new model. */ @Override public LegionServerSide getDefendingLegion() { return (LegionServerSide)super.getDefendingLegion(); } LegionServerSide getActiveLegion() { return getLegion(activeLegionTag); } private LegionServerSide getInactiveLegion() { return getLegion((activeLegionTag == LegionTags.ATTACKER) ? LegionTags.DEFENDER : LegionTags.ATTACKER); } private LegionServerSide getLegion(LegionTags legionTag) { switch (legionTag) { case DEFENDER: return getDefendingLegion(); case ATTACKER: return getAttackingLegion(); } throw new IllegalArgumentException("Parameter out of range"); } BattlePhase getBattlePhase() { return phase; } private boolean isOver() { return battleOver; } private void advancePhase() { phaseAdvancer.advancePhase(); } private class BattlePhaseAdvancer implements PhaseAdvancer { private boolean again = false; /** Advance to the next battle phase. */ public void advancePhase() { if (!isOver()) { advancePhaseInternal(); } } public void advancePhaseInternal() { if (phase == BattlePhase.SUMMON) { phase = BattlePhase.MOVE; LOGGER.log(Level.INFO, "Battle phase advances to " + phase); again = setupMove(); } else if (phase == BattlePhase.RECRUIT) { phase = BattlePhase.MOVE; LOGGER.log(Level.INFO, "Battle phase advances to " + phase); again = setupMove(); } else if (phase == BattlePhase.MOVE) { // IF the attacker makes it to the end of his first movement // phase without conceding, even if he left all legions // off-board, the defender can recruit. if (activeLegionTag == LegionTags.ATTACKER && !conceded) { attackerEntered = true; } phase = BattlePhase.FIGHT; LOGGER.log(Level.INFO, "Battle phase advances to " + phase); if (conceded) { LOGGER .log(Level.INFO, "Conceded - setting again to true."); again = true; } else { again = setupFight(); } } else if (phase == BattlePhase.FIGHT) { // We switch the active legion between the fight and strikeback // phases, not at the end of the player turn. activeLegionTag = (activeLegionTag == LegionTags.ATTACKER) ? LegionTags.DEFENDER : LegionTags.ATTACKER; preStrikeEffectsApplied = false; phase = BattlePhase.STRIKEBACK; LOGGER.log(Level.INFO, "Battle phase advances to " + phase); if (conceded) { LOGGER .log(Level.INFO, "Conceded - setting again to true."); again = true; } else { again = setupFight(); } } else if (phase == BattlePhase.STRIKEBACK) { removeDeadCreatures(); checkForElimination(); advanceTurn(); } // Comment related to the 2 if conceded again=true else again=... // blocks above: // If conceded, ripple through all remaining phases automatically. // See also the comment in "void concede(Player player)", which is // related to the bug tracker items // 3133960 and 3160873 ("Conceding battle leads to hung game"). // Doing "if (again || conceded)" would leave to strange side // effects (I tried that ;-) if (again) { advancePhase(); } } public void advanceTurn() { if (isOver()) { return; } // Active legion is the one that was striking back. if (activeLegionTag == LegionTags.ATTACKER) { phase = BattlePhase.SUMMON; LOGGER.log(Level.INFO, getBattleActivePlayer() + "'s battle turn, number " + battleTurnNumber); again = setupSummon(); } else { battleTurnNumber++; if (battleTurnNumber > 7) { timeLoss(); } else { phase = BattlePhase.RECRUIT; again = setupRecruit(); if (getBattleActivePlayer() != null) { LOGGER.log(Level.INFO, getBattleActivePlayer() + "'s battle turn, number " + battleTurnNumber); } } } } private void timeLoss() { LOGGER.log(Level.INFO, "Time loss"); LegionServerSide attacker = getAttackingLegion(); // Time loss. Attacker is eliminated but defender gets no points. if (attacker.hasTitan()) { // This is the attacker's titan stack, so the defender gets // his markers plus half points for his unengaged legions. PlayerServerSide player = attacker.getPlayer(); attacker.remove(); player.die(getDefendingLegion().getPlayer()); getGame().checkForVictory(); } else { attacker.remove(); } cleanup(); again = false; } } private boolean setupSummon() { server.allSetupBattleSummon(); boolean advance = true; if (summonState == AngelSummoningStates.FIRST_BLOOD) { if (getAttackingLegion().canSummonAngel()) { getGame().createSummonAngel(getAttackingLegion()); advance = false; } // This is the last chance to summon an angel until the // battle is over. summonState = AngelSummoningStates.TOO_LATE; } return advance; } private boolean setupRecruit() { server.allSetupBattleRecruit(); return recruitReinforcement(); } private boolean setupMove() { server.allSetupBattleMove(); return false; } private boolean setupFight() { applyPreStrikeEffects(); server.allSetupBattleFight(); return false; } AngelSummoningStates getSummonState() { return summonState; } void setSummonState(AngelSummoningStates summonState) { this.summonState = summonState; } /** Called from Game after the SummonAngel finishes. */ void finishSummoningAngel(boolean placeNewChit) { if (placeNewChit) { LegionServerSide attacker = getAttackingLegion(); CreatureServerSide critter = attacker.getCritter(attacker .getHeight() - 1); placeCritter(critter); } if (phase == BattlePhase.SUMMON) { advancePhase(); } } private boolean recruitReinforcement() { LegionServerSide defender = getDefendingLegion(); if (battleTurnNumber == 4 && defender.canRecruit()) { LOGGER.log(Level.FINEST, "Calling Game.reinforce()" + " from Battle.recruitReinforcement()"); getGame().reinforce(defender); return false; } return true; } /** Needs to be called when reinforcement is done. */ void doneReinforcing() { LOGGER.log(Level.FINEST, "Called Battle.doneReinforcing()"); LegionServerSide defender = getDefendingLegion(); if (defender.hasRecruited()) { CreatureServerSide newCritter = defender.getCritter(defender .getHeight() - 1); placeCritter(newCritter); } getGame().doneReinforcing(); advancePhase(); } int getCarryDamage() { return carryDamage; } void setCarryDamage(int carryDamage) { this.carryDamage = carryDamage; } void undoMove(BattleHex hex) { CreatureServerSide critter = getCreatureSS(hex); if (critter != null) { BattleHex formerHexLabel = critter.getCurrentHex(); critter.undoMove(); getGame().getServer().allTellBattleMove(critter.getTag(), formerHexLabel, critter.getCurrentHex(), true); } else { LOGGER.log(Level.SEVERE, "Undo move error: no critter in " + hex.getLabel()); } } /** Mark all of the conceding player's critters as dead. */ void concede(Player player) { Legion legion = getLegionByPlayer(player); String markerId = legion.getMarkerId(); LOGGER.log(Level.INFO, markerId + " concedes the battle"); conceded = true; for (Creature creature : legion.getCreatures()) { creature.setDead(true); } // To fix 3133960 and 3160873 ("Conceding battle leads to hung game"): // Rather be not totally compliant to rules, instead of having // games hung (if player concedes hopeless case, or bails out // (e.g. connection lost), etc.) // TODO Created Feature Request 3182336 to get this back to track, ... // ... eventually. // // if (legion.getPlayer().equals(getBattleActivePlayer())) // { // advancePhase(); // } // To prevent the hang, do it also for not-phasing player: advancePhase(); } /** If any creatures were left off-board, kill them. If they were newly * summoned or recruited, unsummon or unrecruit them instead. */ private void removeOffboardCreatures() { LegionServerSide legion = getActiveLegion(); for (Creature critter : legion.getCreatures()) { if (critter.getCurrentHex().isEntrance()) { critter.setDead(true); } } removeDeadCreatures(); } private void commitMoves() { for (Creature critter : getActiveLegion().getCreatures()) { critter.commitMove(); } } void doneWithMoves() { removeOffboardCreatures(); commitMoves(); advancePhase(); } private void applyPreStrikeEffects() { // Certain effects are applied once per player turn, // during the strike phase, before the actual strike takes place. // These currently include damage from Drifts, damage from Poison, // healing from Springs and slowing from TarPits if (phase == BattlePhase.FIGHT && !preStrikeEffectsApplied) { preStrikeEffectsApplied = true; for (BattleCritter c : getAllCritters()) { CreatureServerSide critter = (CreatureServerSide)c; int dam = critter.getCurrentHex().damageToCreature( critter.getType()); dam += critter.getPoisonDamage(); if (dam != 0) { critter.adjustHits(dam); LOGGER.log(Level.INFO, critter.getDescription() + " preStrikeEffects: " + dam + " adjust to hits"); server.allTellHexDamageResults(critter, dam); } int slowValue = critter.getCurrentHex().slowsCreature( critter.getType()); if (slowValue != 0) { critter.addSlowed(slowValue); server.allTellHexSlowResults(critter, slowValue); } } } } boolean arePreStrikeEffectsApplied() { return preStrikeEffectsApplied; } void setPreStrikeEffectsApplied(boolean preStrikeEffectsApplied) { this.preStrikeEffectsApplied = preStrikeEffectsApplied; } void leaveCarryMode() { if (carryDamage > 0) { carryDamage = 0; } if (!carryTargets.isEmpty()) { carryTargets.clear(); } } private void removeDeadCreatures() { // Initialize these to true, and then set them to false when a // non-dead chit is found. attackerElim = true; defenderElim = true; LegionServerSide attacker = getAttackingLegion(); LegionServerSide defender = getDefendingLegion(); removeDeadCreaturesFromLegion(defender); removeDeadCreaturesFromLegion(attacker); if (attacker.getPlayer() == null || attacker.getPlayer().isTitanEliminated()) { attackerElim = true; } if (defender.getPlayer() == null || defender.getPlayer().isTitanEliminated()) { defenderElim = true; } server.allRemoveDeadBattleChits(); // to update number of creatures in status window: server.allUpdatePlayerInfo("removeDeadCreatures"); } private void removeDeadCreaturesFromLegion(LegionServerSide legion) { if (legion == null) { return; } List<CreatureServerSide> critters = legion.getCreatures(); if (critters != null) { Iterator<CreatureServerSide> it = critters.iterator(); while (it.hasNext()) { CreatureServerSide critter = it.next(); if (critter.isDead()) { cleanupOneDeadCritter(critter); it.remove(); } else // critter is alive { if (legion == getAttackingLegion()) { attackerElim = false; } else { defenderElim = false; } } } } } private void cleanupOneDeadCritter(Creature critter) { LegionServerSide legion = (LegionServerSide)critter.getLegion(); LegionServerSide donor = null; boolean reinforced = false; PlayerServerSide player = legion.getPlayer(); // After turn 1, off-board creatures are returned to the // stacks or the legion they were summoned from, with // no points awarded. if (critter.getCurrentHex().isEntrance() && battleTurnNumber > 1) { if (legion == getAttackingLegion()) { // Summoned angel. donor = player.getDonor(); if (donor != null) { donor.addCreature(critter.getType(), false); server.allTellAddCreature( new SummonUndo(donor, critter.getType()), true, Constants.reasonUndoSummon); LOGGER.log(Level.INFO, "undosummon critter " + critter + " back to marker " + donor + ""); // This summon doesn't count; the player can // summon again later this turn. player.setSummoned(false); player.setDonor(null); } else { LOGGER.log(Level.SEVERE, "Null donor in Battle.cleanupOneDeadCritter()"); } } else { // This reinforcement doesn't count. // Tell legion to do undo the reinforcement and trigger // sending of needed messages to clients: reinforced = true; player.undoReinforcement(legion); } } else if (legion == getAttackingLegion()) { getDefendingLegion().addToBattleTally(critter.getPointValue()); } else // defender { getAttackingLegion().addToBattleTally(critter.getPointValue()); // Creatures left off board do not trigger angel // summoning. if (summonState == AngelSummoningStates.NO_KILLS && !critter.getCurrentHex().isEntrance()) { summonState = AngelSummoningStates.FIRST_BLOOD; } } if (donor != null) { // If an angel or archangel was returned to its donor instead of // the stack, then don't put it back on the stack. legion.prepareToRemoveCritter(critter, false, true); } else if (reinforced) { // undoReinforce does the remove already ( = back to caretaker) // and also creates the removeCreatureEvent. // legion.prepareToRemoveCritter(critter, false, true); } else { legion.prepareToRemoveCritter(critter, true, true); } if (critter.isTitan()) { (legion.getPlayer()).eliminateTitan(); } } private void checkForElimination() { LegionServerSide attacker = getAttackingLegion(); LegionServerSide defender = getDefendingLegion(); PlayerServerSide attackerPlayer = attacker.getPlayer(); PlayerServerSide defenderPlayer = defender.getPlayer(); boolean attackerTitanDead = attackerPlayer.isTitanEliminated(); boolean defenderTitanDead = defenderPlayer.isTitanEliminated(); // Check for mutual Titan elimination. if (attackerTitanDead && defenderTitanDead) { // Nobody gets any points. // Make defender die first, to simplify turn advancing. defender.getPlayer().die(null); attacker.getPlayer().die(null); getGame().checkForVictory(); cleanup(); } // Check for single Titan elimination. else if (attackerTitanDead) { if (defenderElim) { defender.remove(); } else { pointsScored = defender.getBattleTally(); // award points and handle acquiring defender.addBattleTallyToPoints(); } attacker.getPlayer().die(defender.getPlayer()); getGame().checkForVictory(); cleanup(); } else if (defenderTitanDead) { if (attackerElim) { attacker.remove(); } else { pointsScored = attacker.getBattleTally(); // award points and handle acquiring attacker.addBattleTallyToPoints(); } defender.getPlayer().die(attacker.getPlayer()); getGame().checkForVictory(); cleanup(); } // Check for mutual legion elimination. else if (attackerElim && defenderElim) { attacker.remove(); defender.remove(); cleanup(); } // Check for single legion elimination. else if (attackerElim) { pointsScored = defender.getBattleTally(); // award points and handle acquiring defender.addBattleTallyToPoints(); attacker.remove(); cleanup(); } else if (defenderElim) { pointsScored = attacker.getBattleTally(); // award points and handle acquiring attacker.addBattleTallyToPoints(); defender.remove(); cleanup(); } } private void commitStrikes() { LegionServerSide legion = getActiveLegion(); if (legion != null) { for (Creature critter : legion.getCreatures()) { critter.setStruck(false); } } } public boolean isForcedStrikeRemaining() { LegionServerSide legion = getActiveLegion(); if (legion != null) { for (CreatureServerSide critter : legion.getCreatures()) { if (!critter.hasStruck() && isInContact(critter, false)) { return true; } } } return false; } /** Checks now all at same place in Server */ void doneWithStrikes() { commitStrikes(); advancePhase(); } /** * Return a set of hexes containing targets that the critter may strike * * @param battleUnit the striking creature * @param rangestrike Whether to include rangestrike targets * @return a set of hexes containing targets */ Set<BattleHex> findTargetHexes(CreatureServerSide critter, boolean rangestrike) { Set<BattleHex> set = new HashSet<BattleHex>(); // Each creature may strike only once per turn. if (critter.hasStruck()) { return set; } Player player = critter.getPlayer(); BattleHex currentHex = critter.getCurrentHex(); boolean adjacentEnemy = false; // First mark and count normal strikes. for (int i = 0; i < 6; i++) { // Adjacent creatures separated by a cliff are not engaged. if (!currentHex.isCliff(i)) { BattleHex targetHex = currentHex.getNeighbor(i); if (targetHex != null && isOccupied(targetHex)) { CreatureServerSide target = getCreatureSS(targetHex); if (target.getPlayer() != player) { adjacentEnemy = true; if (!target.isDead()) { set.add(targetHex); } } } } } // Then do rangestrikes if applicable. Rangestrikes are not allowed // if the creature can strike normally, so only look for them if // no targets have yet been found. if (rangestrike && !adjacentEnemy && critter.isRangestriker() && getBattlePhase() != BattlePhase.STRIKEBACK && critter.getLegion() == getActiveLegion()) { for (Creature target : getInactiveLegion().getCreatures()) { if (!target.isDead()) { BattleHex targetHex = target.getCurrentHex(); if (isRangestrikePossible(critter, target, currentHex, targetHex)) { set.add(targetHex); } } } } return set; } /** Return the set of hexes with valid carry targets. */ Set<BattleHex> getCarryTargets() { return Collections.unmodifiableSet(carryTargets); } Set<String> getCarryTargetDescriptions() { Set<String> set = new HashSet<String>(); for (BattleHex hex : getCarryTargets()) { CreatureServerSide critter = getCreatureSS(hex); set.add(critter.getDescription()); } return set; } void clearCarryTargets() { carryTargets.clear(); } void setCarryTargets(Set<BattleHex> carryTargets) { this.carryTargets.clear(); this.carryTargets.addAll(carryTargets); } void addCarryTarget(BattleHex hex) { carryTargets.add(hex); } void applyCarries(CreatureServerSide target) { if (!carryTargets.contains(target.getCurrentHex())) { LOGGER.log(Level.WARNING, "Tried illegal carry to " + target.getDescription()); return; } int dealt = carryDamage; carryDamage = target.adjustHits(carryDamage); dealt -= carryDamage; carryTargets.remove(target.getCurrentHex()); LOGGER.log(Level.INFO, dealt + (dealt == 1 ? " hit carries to " : " hits carry to ") + target.getDescription()); if (carryDamage <= 0 || getCarryTargets().isEmpty()) { leaveCarryMode(); } else { LOGGER.log(Level.INFO, carryDamage + (carryDamage == 1 ? " carry available" : " carries available")); } server.allTellCarryResults(target, dealt, carryDamage, getCarryTargetDescriptions()); } /** If legal, move critter to hex and return true. Else return false. */ String doMove(int tag, BattleHex hex) { String reasonFail = null; CreatureServerSide critter = getActiveLegion().getCritterByTag(tag); if (critter == null) { reasonFail = "No critter with tag " + tag + " found from legion " + getActiveLegion().getMarkerId() + " - can't move it to hex " + hex.getLabel(); LOGGER.severe(reasonFail); return reasonFail; } // Allow null moves. if (!critter.hasMoved() && hex.equals(critter.getCurrentHex())) { // Warning, for now, because actually this should never happen, at // least not for human players... LOGGER.warning(critter.getDescription() + " does not move"); // Call moveToHex() anyway to sync client. moveCritterToHexAndInformClients(critter, hex); return null; } else if (battleMovement.showMoves(critter, false).contains(hex)) { LOGGER .log( Level.INFO, critter.getName() + " moves from " + critter.getCurrentHex().getLabel() + " to " + hex.getLabel()); moveCritterToHexAndInformClients(critter, hex); return null; } else { LegionServerSide legion = getActiveLegion(); String markerId = legion.getMarkerId(); LOGGER.log(Level.WARNING, critter.getName() + " in " + critter.getCurrentHex().getLabel() + " tried to illegally move to " + hex.getLabel() + " in " + getLocation().getTerrain().getDisplayName() + " (" + getAttackingLegion().getMarkerId() + " attacking " + getDefendingLegion().getMarkerId() + ", active: " + markerId + ")"); reasonFail = critter.getName() + " in " + critter.getCurrentHex().getLabel() + " can't move to " + hex.getLabel() + " (Have you clicked this move " + "before previous move was completed by server?)"; return reasonFail; } } private void moveCritterToHexAndInformClients(CreatureServerSide critter, BattleHex hex) { critter.moveToHex(hex); getGame().getServer().allTellBattleMove(critter.getTag(), critter.getStartingHex(), critter.getCurrentHex(), false); } private void cleanup() { battleOver = true; getGame().finishBattle(getLocation(), attackerEntered, pointsScored, battleTurnNumber); } @Override public List<BattleCritter> getAllCritters() { List<BattleCritter> critters = new ArrayList<BattleCritter>(); LegionServerSide defender = getDefendingLegion(); if (defender != null) { critters.addAll(defender.getCreatures()); } LegionServerSide attacker = getAttackingLegion(); if (attacker != null) { critters.addAll(attacker.getCreatures()); } return critters; } // TODO use getCritter() instead CreatureServerSide getCreatureSS(BattleHex hex) { return (CreatureServerSide)getCritter(hex); } /** Return true if there are any enemies adjacent to this critter. * * @param critter The critter to check whether it is in contact with any enemy critter * @param countDead Dead critters count as being in contact only if countDead is true. */ @Override public boolean isInContact(BattleCritter critter, boolean countDead) { BattleHex hex = critter.getCurrentHex(); // Offboard creatures are not in contact. if (hex.isEntrance()) { return false; } for (int i = 0; i < 6; i++) { // Adjacent creatures separated by a cliff are not in contact. if (!hex.isCliff(i)) { BattleHex neighbor = hex.getNeighbor(i); if (neighbor != null) { BattleCritter other = getCreatureSS(neighbor); if (other != null && other.isDefender() != critter.isDefender() && (countDead || !other.isDead())) { return true; } } } } return false; } /** Return the number of enemy creatures in contact with this critter. * Dead critters count as being in contact only if countDead is true. */ public int numInContact(BattleCritter striker, boolean countDead) { BattleHex hex = striker.getCurrentHex(); // Offboard creatures are not in contact. if (hex.isEntrance()) { return 0; } int count = 0; for (int i = 0; i < 6; i++) { // Adjacent creatures separated by a cliff are not in contact. if (!hex.isCliff(i)) { BattleHex neighbor = hex.getNeighbor(i); if (neighbor != null) { BattleCritter other = getCreatureSS(neighbor); if (other != null && other.isDefender() != striker.isDefender() && (countDead || !other.isDead())) { count++; } } } } return count; } }