package games.strategy.triplea.delegate; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import games.strategy.engine.data.Change; import games.strategy.engine.data.GameData; import games.strategy.engine.data.PlayerID; import games.strategy.engine.data.Resource; import games.strategy.engine.data.Route; import games.strategy.engine.data.Territory; import games.strategy.engine.data.Unit; import games.strategy.engine.data.UnitType; import games.strategy.engine.data.changefactory.ChangeFactory; import games.strategy.engine.delegate.IDelegateBridge; import games.strategy.engine.message.ConnectionLostException; import games.strategy.engine.random.IRandomStats.DiceType; import games.strategy.sound.SoundPath; import games.strategy.triplea.Constants; import games.strategy.triplea.Properties; import games.strategy.triplea.TripleAUnit; import games.strategy.triplea.attachments.PlayerAttachment; import games.strategy.triplea.attachments.TechAbilityAttachment; import games.strategy.triplea.attachments.TerritoryAttachment; import games.strategy.triplea.attachments.UnitAttachment; import games.strategy.triplea.delegate.Die.DieType; import games.strategy.triplea.delegate.dataObjects.BattleRecord; import games.strategy.triplea.delegate.dataObjects.CasualtyDetails; import games.strategy.triplea.formatter.MyFormatter; import games.strategy.triplea.oddsCalculator.ta.BattleResults; import games.strategy.triplea.player.ITripleAPlayer; import games.strategy.util.CompositeMatchAnd; import games.strategy.util.CompositeMatchOr; import games.strategy.util.IntegerMap; import games.strategy.util.Match; public class StrategicBombingRaidBattle extends AbstractBattle implements BattleStepStrings { private static final long serialVersionUID = 8490171037606078890L; private static final String RAID = "Strategic bombing raid"; // these would be the factories or other targets. does not include aa. protected final HashMap<Unit, HashSet<Unit>> m_targets = new HashMap<>(); protected final ExecutionStack m_stack = new ExecutionStack(); protected List<String> m_steps; protected List<Unit> m_defendingAA; protected List<String> m_AAtypes; private int m_bombingRaidTotal; private final IntegerMap<Unit> m_bombingRaidDamage = new IntegerMap<>(); /** * Creates new StrategicBombingRaidBattle. * * @param battleSite * - battle territory * @param data * - game data * @param attacker * - attacker PlayerID * @param battleTracker * - BattleTracker */ public StrategicBombingRaidBattle(final Territory battleSite, final GameData data, final PlayerID attacker, final BattleTracker battleTracker) { super(battleSite, attacker, battleTracker, true, BattleType.BOMBING_RAID, data); m_isAmphibious = false; updateDefendingUnits(); } @Override protected void removeUnitsThatNoLongerExist() { if (m_headless) { return; } // we were having a problem with units that had been killed previously were still part of battle's variables, so we // double check that // the stuff still exists here. m_defendingUnits.retainAll(m_battleSite.getUnits().getUnits()); m_attackingUnits.retainAll(m_battleSite.getUnits().getUnits()); final Iterator<Unit> iter = m_targets.keySet().iterator(); while (iter.hasNext()) { if (!m_battleSite.getUnits().getUnits().contains(iter.next())) { iter.remove(); } } } protected void updateDefendingUnits() { // fill in defenders final HashMap<String, HashSet<UnitType>> airborneTechTargetsAllowed = TechAbilityAttachment.getAirborneTargettedByAA(m_attacker, m_data); final Match<Unit> defenders = new CompositeMatchAnd<>(Matches.enemyUnit(m_attacker, m_data), new CompositeMatchOr<Unit>(Matches.UnitIsAtMaxDamageOrNotCanBeDamaged(m_battleSite).invert(), Matches.UnitIsAAthatCanFire(m_attackingUnits, airborneTechTargetsAllowed, m_attacker, Matches.UnitIsAAforBombingThisUnitOnly, m_round, true, m_data))); if (m_targets.isEmpty()) { m_defendingUnits = Match.getMatches(m_battleSite.getUnits().getUnits(), defenders); } else { final List<Unit> targets = Match.getMatches(m_battleSite.getUnits().getUnits(), Matches.UnitIsAAthatCanFire(m_attackingUnits, airborneTechTargetsAllowed, m_attacker, Matches.UnitIsAAforBombingThisUnitOnly, m_round, true, m_data)); targets.addAll(m_targets.keySet()); m_defendingUnits = targets; } } @Override public boolean isEmpty() { return m_attackingUnits.isEmpty(); } @Override public void removeAttack(final Route route, final Collection<Unit> units) { removeAttackers(units, true); } private void removeAttackers(final Collection<Unit> units, final boolean removeTarget) { m_attackingUnits.removeAll(units); final Iterator<Unit> targetIter = m_targets.keySet().iterator(); while (targetIter.hasNext()) { final HashSet<Unit> currentAttackers = m_targets.get(targetIter.next()); currentAttackers.removeAll(units); if (currentAttackers.isEmpty() && removeTarget) { targetIter.remove(); } } } private Unit getTarget(final Unit attacker) { for (final Unit target : m_targets.keySet()) { if (m_targets.get(target).contains(attacker)) { return target; } } throw new IllegalStateException("Unit " + attacker.getType().getName() + " has no target"); } @Override public Change addAttackChange(final Route route, final Collection<Unit> units, final HashMap<Unit, HashSet<Unit>> targets) { m_attackingUnits.addAll(units); if (targets == null) { return ChangeFactory.EMPTY_CHANGE; } for (final Unit target : targets.keySet()) { HashSet<Unit> currentAttackers = m_targets.get(target); if (currentAttackers == null) { currentAttackers = new HashSet<>(); } currentAttackers.addAll(targets.get(target)); m_targets.put(target, currentAttackers); } return ChangeFactory.EMPTY_CHANGE; } @Override public void fight(final IDelegateBridge bridge) { // remove units that may already be dead due to a previous event (like they died from a strategic bombing raid, // rocket attack, etc) removeUnitsThatNoLongerExist(); // we were interrupted if (m_stack.isExecuting()) { showBattle(bridge); m_stack.execute(bridge); return; } // We update Defending Units twice: first time when the battle is created, and second time before the battle begins. // The reason is because when the battle is created, there are no attacking units yet in it, meaning that m_targets // is empty. We need to // update right as battle begins to know we have the full list of targets. updateDefendingUnits(); bridge.getHistoryWriter().startEvent("Strategic bombing raid in " + m_battleSite, m_battleSite); if (m_attackingUnits.isEmpty() || (m_defendingUnits.isEmpty() || Match.noneMatch(m_defendingUnits, Matches.UnitIsAtMaxDamageOrNotCanBeDamaged(m_battleSite).invert()))) { endBeforeRolling(bridge); return; } BattleCalculator.sortPreBattle(m_attackingUnits); // TODO: determine if the target has the property, not just any unit with the property isAAforBombingThisUnitOnly final HashMap<String, HashSet<UnitType>> airborneTechTargetsAllowed = TechAbilityAttachment.getAirborneTargettedByAA(m_attacker, m_data); m_defendingAA = m_battleSite.getUnits().getMatches(Matches.UnitIsAAthatCanFire(m_attackingUnits, airborneTechTargetsAllowed, m_attacker, Matches.UnitIsAAforBombingThisUnitOnly, m_round, true, m_data)); m_AAtypes = UnitAttachment.getAllOfTypeAAs(m_defendingAA); // reverse since stacks are in reverse order Collections.reverse(m_AAtypes); final boolean hasAA = m_defendingAA.size() > 0; m_steps = new ArrayList<>(); if (hasAA) { for (final String typeAA : UnitAttachment.getAllOfTypeAAs(m_defendingAA)) { m_steps.add(typeAA + AA_GUNS_FIRE_SUFFIX); m_steps.add(SELECT_PREFIX + typeAA + CASUALTIES_SUFFIX); m_steps.add(REMOVE_PREFIX + typeAA + CASUALTIES_SUFFIX); } } m_steps.add(RAID); showBattle(bridge); final List<IExecutable> steps = new ArrayList<>(); if (hasAA) { // global1940 rules - each target type fires an AA shot against the planes bombing it m_targets.entrySet().stream() .filter(entry -> entry.getKey().getUnitAttachment().getIsAAforBombingThisUnitOnly()) .forEach(entry -> steps.add(new FireAA(entry.getValue()))); // otherwise fire an AA shot at all the planes if (steps.isEmpty()) { steps.add(new FireAA()); } } steps.add(new ConductBombing()); steps.add(new IExecutable() { private static final long serialVersionUID = 4299575008166316488L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { getDisplay(bridge).gotoBattleStep(m_battleID, RAID); if (isDamageFromBombingDoneToUnitsInsteadOfTerritories()) { bridge.getHistoryWriter() .addChildToEvent("Bombing raid in " + m_battleSite.getName() + " causes " + m_bombingRaidTotal + " damage total. " + (m_bombingRaidDamage.size() > 1 ? (" Damaged units is as follows: " + MyFormatter.integerUnitMapToString(m_bombingRaidDamage, ", ", " = ", false)) : "")); } else { bridge.getHistoryWriter().addChildToEvent( "Bombing raid costs " + m_bombingRaidTotal + " " + MyFormatter.pluralize("PU", m_bombingRaidTotal)); } // TODO remove the reference to the constant.japanese- replace with a rule if (isPacificTheater() || isSBRVictoryPoints()) { if (m_defender.getName().equals(Constants.PLAYER_NAME_JAPANESE)) { Change changeVP; final PlayerAttachment pa = PlayerAttachment.get(m_defender); if (pa != null) { changeVP = ChangeFactory.attachmentPropertyChange(pa, ((-(m_bombingRaidTotal / 10)) + pa.getVps()), "vps"); bridge.addChange(changeVP); bridge.getHistoryWriter().addChildToEvent("Bombing raid costs " + (m_bombingRaidTotal / 10) + " " + MyFormatter.pluralize("vp", (m_bombingRaidTotal / 10))); } } } // kill any suicide attackers (veqryn) if (Match.someMatch(m_attackingUnits, Matches.UnitIsSuicide)) { final List<Unit> suicideUnits = Match.getMatches(m_attackingUnits, Matches.UnitIsSuicide); m_attackingUnits.removeAll(suicideUnits); final Change removeSuicide = ChangeFactory.removeUnits(m_battleSite, suicideUnits); final String transcriptText = MyFormatter.unitsToText(suicideUnits) + " lost in " + m_battleSite.getName(); final IntegerMap<UnitType> costs = BattleCalculator.getCostsForTUV(m_attacker, m_data); final int tuvLostAttacker = BattleCalculator.getTUV(suicideUnits, m_attacker, costs, m_data); m_attackerLostTUV += tuvLostAttacker; bridge.getHistoryWriter().addChildToEvent(transcriptText, suicideUnits); bridge.addChange(removeSuicide); } // kill any units that can die if they have reached max damage (veqryn) if (Match.someMatch(m_targets.keySet(), Matches.UnitCanDieFromReachingMaxDamage)) { final List<Unit> unitsCanDie = Match.getMatches(m_targets.keySet(), Matches.UnitCanDieFromReachingMaxDamage); unitsCanDie .retainAll(Match.getMatches(unitsCanDie, Matches.UnitIsAtMaxDamageOrNotCanBeDamaged(m_battleSite))); if (!unitsCanDie.isEmpty()) { // m_targets.removeAll(unitsCanDie); final Change removeDead = ChangeFactory.removeUnits(m_battleSite, unitsCanDie); final String transcriptText = MyFormatter.unitsToText(unitsCanDie) + " lost in " + m_battleSite.getName(); final IntegerMap<UnitType> costs = BattleCalculator.getCostsForTUV(m_defender, m_data); final int tuvLostDefender = BattleCalculator.getTUV(unitsCanDie, m_defender, costs, m_data); m_defenderLostTUV += tuvLostDefender; bridge.getHistoryWriter().addChildToEvent(transcriptText, unitsCanDie); bridge.addChange(removeDead); } } } }); steps.add(new IExecutable() { private static final long serialVersionUID = -7649516174883172328L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { end(bridge); } }); Collections.reverse(steps); for (final IExecutable executable : steps) { m_stack.push(executable); } m_stack.execute(bridge); } private void endBeforeRolling(final IDelegateBridge bridge) { getDisplay(bridge).battleEnd(m_battleID, "Bombing raid does no damage"); m_whoWon = WhoWon.DRAW; m_battleResultDescription = BattleRecord.BattleResultDescription.NO_BATTLE; m_battleTracker.getBattleRecords().addResultToBattle(m_attacker, m_battleID, m_defender, m_attackerLostTUV, m_defenderLostTUV, m_battleResultDescription, new BattleResults(this, m_data)); m_isOver = true; m_battleTracker.removeBattle(StrategicBombingRaidBattle.this); } private void end(final IDelegateBridge bridge) { if (isDamageFromBombingDoneToUnitsInsteadOfTerritories()) { getDisplay(bridge).battleEnd(m_battleID, "Raid causes " + m_bombingRaidTotal + " damage total." + (m_bombingRaidDamage.size() > 1 ? (" To units: " + MyFormatter.integerUnitMapToString(m_bombingRaidDamage, ", ", " = ", false)) : "")); } else { getDisplay(bridge).battleEnd(m_battleID, "Bombing raid cost " + m_bombingRaidTotal + " " + MyFormatter.pluralize("PU", m_bombingRaidTotal)); } if (m_bombingRaidTotal > 0) { m_whoWon = WhoWon.ATTACKER; m_battleResultDescription = BattleRecord.BattleResultDescription.BOMBED; } else { m_whoWon = WhoWon.DEFENDER; m_battleResultDescription = BattleRecord.BattleResultDescription.LOST; } m_battleTracker.getBattleRecords().addResultToBattle(m_attacker, m_battleID, m_defender, m_attackerLostTUV, m_defenderLostTUV, m_battleResultDescription, new BattleResults(this, m_data)); m_isOver = true; m_battleTracker.removeBattle(this); } private void showBattle(final IDelegateBridge bridge) { final String title = "Bombing raid in " + m_battleSite.getName(); getDisplay(bridge).showBattle(m_battleID, m_battleSite, title, m_attackingUnits, m_defendingUnits, null, null, null, Collections.emptyMap(), m_attacker, m_defender, isAmphibious(), getBattleType(), Collections.emptySet()); getDisplay(bridge).listBattleSteps(m_battleID, m_steps); } class FireAA implements IExecutable { private static final long serialVersionUID = -4667856856747597406L; DiceRoll m_dice; CasualtyDetails m_casualties; Collection<Unit> m_casualtiesSoFar = new ArrayList<>(); Collection<Unit> validAttackingUnitsForThisRoll; boolean determineAttackers; public FireAA(Collection<Unit> attackers) { validAttackingUnitsForThisRoll = attackers; determineAttackers = false; } public FireAA() { validAttackingUnitsForThisRoll = Collections.emptyList(); determineAttackers = true; } @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { final boolean isEditMode = BaseEditDelegate.getEditMode(bridge.getData()); for (final String currentTypeAA : m_AAtypes) { final Collection<Unit> currentPossibleAA = Match.getMatches(m_defendingAA, Matches.UnitIsAAofTypeAA(currentTypeAA)); final Set<UnitType> targetUnitTypesForThisTypeAA = UnitAttachment.get(currentPossibleAA.iterator().next().getType()).getTargetsAA(m_data); final Set<UnitType> airborneTypesTargettedToo = TechAbilityAttachment.getAirborneTargettedByAA(m_attacker, m_data).get(currentTypeAA); if (determineAttackers) { validAttackingUnitsForThisRoll = Match.getMatches(m_attackingUnits, new CompositeMatchOr<>( Matches.unitIsOfTypes(targetUnitTypesForThisTypeAA), new CompositeMatchAnd<Unit>(Matches.UnitIsAirborne, Matches.unitIsOfTypes(airborneTypesTargettedToo)))); } final IExecutable roll = new IExecutable() { private static final long serialVersionUID = 379538344036513009L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { validAttackingUnitsForThisRoll.removeAll(m_casualtiesSoFar); if (!validAttackingUnitsForThisRoll.isEmpty()) { m_dice = DiceRoll.rollAA(validAttackingUnitsForThisRoll, currentPossibleAA, bridge, m_battleSite, true); if (currentTypeAA.equals("AA")) { if (m_dice.getHits() > 0) { bridge.getSoundChannelBroadcaster().playSoundForAll(SoundPath.CLIP_BATTLE_AA_HIT, m_defender); } else { bridge.getSoundChannelBroadcaster().playSoundForAll(SoundPath.CLIP_BATTLE_AA_MISS, m_defender); } } else { if (m_dice.getHits() > 0) { bridge.getSoundChannelBroadcaster().playSoundForAll( SoundPath.CLIP_BATTLE_X_PREFIX + currentTypeAA.toLowerCase() + SoundPath.CLIP_BATTLE_X_HIT, m_defender); } else { bridge.getSoundChannelBroadcaster().playSoundForAll( SoundPath.CLIP_BATTLE_X_PREFIX + currentTypeAA.toLowerCase() + SoundPath.CLIP_BATTLE_X_MISS, m_defender); } } } } }; final IExecutable calculateCasualties = new IExecutable() { private static final long serialVersionUID = -4658133491636765763L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { if (!validAttackingUnitsForThisRoll.isEmpty()) { final CasualtyDetails details = calculateCasualties(validAttackingUnitsForThisRoll, currentPossibleAA, bridge, m_dice, currentTypeAA); markDamaged(details.getDamaged(), bridge); m_casualties = details; m_casualtiesSoFar.addAll(details.getKilled()); } } }; final IExecutable notifyCasualties = new IExecutable() { private static final long serialVersionUID = -4989154196975570919L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { if (!validAttackingUnitsForThisRoll.isEmpty()) { notifyAAHits(bridge, m_dice, m_casualties, currentTypeAA); } } }; final IExecutable removeHits = new IExecutable() { private static final long serialVersionUID = -3673833177336068509L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { if (!validAttackingUnitsForThisRoll.isEmpty()) { removeAAHits(bridge, m_casualties, currentTypeAA); } } }; // push in reverse order of execution stack.push(removeHits); stack.push(notifyCasualties); stack.push(calculateCasualties); if (!isEditMode) { stack.push(roll); } } } } private boolean isDamageFromBombingDoneToUnitsInsteadOfTerritories() { return games.strategy.triplea.Properties.getDamageFromBombingDoneToUnitsInsteadOfTerritories(m_data); } private boolean isWW2V2() { return games.strategy.triplea.Properties.getWW2V2(m_data); } private boolean isLimitSBRDamageToProduction() { return games.strategy.triplea.Properties.getLimitRocketAndSBRDamageToProduction(m_data); } private boolean isLimitSBRDamagePerTurn(final GameData data) { return games.strategy.triplea.Properties.getLimitSBRDamagePerTurn(data); } private boolean isPUCap(final GameData data) { return games.strategy.triplea.Properties.getPUCap(data); } private boolean isSBRVictoryPoints() { return games.strategy.triplea.Properties.getSBRVictoryPoint(m_data); } private boolean isPacificTheater() { return games.strategy.triplea.Properties.getPacificTheater(m_data); } private CasualtyDetails calculateCasualties(final Collection<Unit> validAttackingUnitsForThisRoll, final Collection<Unit> defendingAA, final IDelegateBridge bridge, final DiceRoll dice, final String currentTypeAA) { getDisplay(bridge).notifyDice(dice, SELECT_PREFIX + currentTypeAA + CASUALTIES_SUFFIX); final boolean isEditMode = BaseEditDelegate.getEditMode(m_data); final boolean allowMultipleHitsPerUnit = Match.allMatch(defendingAA, Matches.UnitAAShotDamageableInsteadOfKillingInstantly); if (isEditMode) { final String text = currentTypeAA + AA_GUNS_FIRE_SUFFIX; final CasualtyDetails casualtySelection = BattleCalculator.selectCasualties(RAID, m_attacker, validAttackingUnitsForThisRoll, m_attackingUnits, m_defender, m_defendingUnits, m_isAmphibious, m_amphibiousLandAttackers, m_battleSite, m_territoryEffects, bridge, text, /* dice */null, /* defending */false, m_battleID, /* head-less */false, 0, allowMultipleHitsPerUnit); return casualtySelection; } final CasualtyDetails casualties = BattleCalculator.getAACasualties(false, validAttackingUnitsForThisRoll, m_attackingUnits, defendingAA, m_defendingUnits, dice, bridge, m_defender, m_attacker, m_battleID, m_battleSite, m_territoryEffects, m_isAmphibious, m_amphibiousLandAttackers); final int totalExpectingHits = dice.getHits() > validAttackingUnitsForThisRoll.size() ? validAttackingUnitsForThisRoll.size() : dice.getHits(); if (casualties.size() != totalExpectingHits) { throw new IllegalStateException( "Wrong number of casualties, expecting:" + totalExpectingHits + " but got:" + casualties.size()); } return casualties; } private void notifyAAHits(final IDelegateBridge bridge, final DiceRoll dice, final CasualtyDetails casualties, final String currentTypeAA) { getDisplay(bridge).casualtyNotification(m_battleID, REMOVE_PREFIX + currentTypeAA + CASUALTIES_SUFFIX, dice, m_attacker, new ArrayList<>(casualties.getKilled()), new ArrayList<>(casualties.getDamaged()), Collections.emptyMap()); final Runnable r = () -> { try { final ITripleAPlayer defender = (ITripleAPlayer) bridge.getRemotePlayer(m_defender); defender.confirmEnemyCasualties(m_battleID, "Press space to continue", m_attacker); } catch (final ConnectionLostException cle) { // somone else will deal with this // System.out.println(cle.getMessage()); // cle.printStackTrace(System.out); } catch (final Exception e) { // ignore } }; final Thread t = new Thread(r, "click to continue waiter"); t.start(); final ITripleAPlayer attacker = (ITripleAPlayer) bridge.getRemotePlayer(m_attacker); attacker.confirmOwnCasualties(m_battleID, "Press space to continue"); try { bridge.leaveDelegateExecution(); t.join(); } catch (final InterruptedException e) { // ignore } finally { bridge.enterDelegateExecution(); } } private void removeAAHits(final IDelegateBridge bridge, final CasualtyDetails casualties, final String currentTypeAA) { final List<Unit> killed = casualties.getKilled(); if (!killed.isEmpty()) { bridge.getHistoryWriter().addChildToEvent(MyFormatter.unitsToTextNoOwner(killed) + " killed by " + currentTypeAA, new ArrayList<>(killed)); final IntegerMap<UnitType> costs = BattleCalculator.getCostsForTUV(m_attacker, m_data); final int tuvLostAttacker = BattleCalculator.getTUV(killed, m_attacker, costs, m_data); m_attackerLostTUV += tuvLostAttacker; // m_attackingUnits.removeAll(casualties); removeAttackers(killed, false); final Change remove = ChangeFactory.removeUnits(m_battleSite, killed); bridge.addChange(remove); } } class ConductBombing implements IExecutable { private static final long serialVersionUID = 5579796391988452213L; private int[] m_dice; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { final IExecutable rollDice = new IExecutable() { private static final long serialVersionUID = -4097858758514452368L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { rollDice(bridge); } }; final IExecutable findCost = new IExecutable() { private static final long serialVersionUID = 8573539936364094095L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { findCost(bridge); } }; // push in reverse order of execution m_stack.push(findCost); m_stack.push(rollDice); } private void rollDice(final IDelegateBridge bridge) { { final Set<Unit> duplicatesCheckSet1 = new HashSet<>(m_attackingUnits); if (m_attackingUnits.size() != duplicatesCheckSet1.size()) { throw new IllegalStateException( "Duplicate Units Detected: Original List:" + m_attackingUnits + " HashSet:" + duplicatesCheckSet1); } } final int rollCount = BattleCalculator.getRolls(m_attackingUnits, m_attacker, false, true, m_territoryEffects); if (rollCount == 0) { m_dice = null; return; } m_dice = new int[rollCount]; final boolean isEditMode = BaseEditDelegate.getEditMode(m_data); if (isEditMode) { final String annotation = m_attacker.getName() + " fixing dice to allocate cost of strategic bombing raid against " + m_defender.getName() + " in " + m_battleSite.getName(); final ITripleAPlayer attacker = (ITripleAPlayer) bridge.getRemotePlayer(m_attacker); // does not take into account bombers with dice sides higher than getDiceSides m_dice = attacker.selectFixedDice(rollCount, 0, true, annotation, m_data.getDiceSides()); } else { final boolean doNotUseBombingBonus = !games.strategy.triplea.Properties.getUseBombingMaxDiceSidesAndBonus(m_data); final String annotation = m_attacker.getName() + " rolling to allocate cost of strategic bombing raid against " + m_defender.getName() + " in " + m_battleSite.getName(); if (!games.strategy.triplea.Properties.getLL_DAMAGE_ONLY(m_data)) { if (doNotUseBombingBonus) { // no low luck, and no bonus, so just roll based on the map's dice sides m_dice = bridge.getRandom(m_data.getDiceSides(), rollCount, m_attacker, DiceType.BOMBING, annotation); } else { // we must use bombing bonus int i = 0; final int diceSides = m_data.getDiceSides(); for (final Unit u : m_attackingUnits) { final int rolls = BattleCalculator.getRolls(u, m_attacker, false, true, m_territoryEffects); if (rolls < 1) { continue; } final UnitAttachment ua = UnitAttachment.get(u.getType()); int maxDice = ua.getBombingMaxDieSides(); int bonus = ua.getBombingBonus(); // both could be -1, meaning they were not set. if they were not set, then we use default dice sides for // the map, and zero for the bonus. if (maxDice < 0) { maxDice = diceSides; } if (bonus < 0) { bonus = 0; } // now we roll, or don't if there is nothing to roll. if (maxDice > 0) { final int[] dicerolls = bridge.getRandom(maxDice, rolls, m_attacker, DiceType.BOMBING, annotation); for (final int die : dicerolls) { m_dice[i] = die + bonus; i++; } } else { for (int j = 0; j < rolls; j++) { m_dice[i] = bonus; i++; } } } } } else { int i = 0; final int diceSides = m_data.getDiceSides(); for (final Unit u : m_attackingUnits) { final int rolls = BattleCalculator.getRolls(u, m_attacker, false, true, m_territoryEffects); if (rolls < 1) { continue; } final UnitAttachment ua = UnitAttachment.get(u.getType()); int maxDice = ua.getBombingMaxDieSides(); int bonus = ua.getBombingBonus(); // both could be -1, meaning they were not set. if they were not set, then we use default dice sides for the // map, and zero for // the bonus. if (maxDice < 0 || doNotUseBombingBonus) { maxDice = diceSides; } if (bonus < 0 || doNotUseBombingBonus) { bonus = 0; } // now, regardless of whether they were set or not, we have to apply "low luck" to them, meaning in this // case that we reduce the // luck by 2/3. if (maxDice >= 5) { bonus += (maxDice + 1) / 3; maxDice = (maxDice + 1) / 3; } // now we roll, or don't if there is nothing to roll. if (maxDice > 0) { final int[] dicerolls = bridge.getRandom(maxDice, rolls, m_attacker, DiceType.BOMBING, annotation); for (final int die : dicerolls) { m_dice[i] = die + bonus; i++; } } else { for (int j = 0; j < rolls; j++) { m_dice[i] = bonus; i++; } } } } } } private void addToTargetDiceMap(final Unit attackerUnit, final Die roll, final HashMap<Unit, List<Die>> targetToDiceMap) { if (m_targets == null || m_targets.isEmpty()) { return; } final Unit target = getTarget(attackerUnit); List<Die> current = targetToDiceMap.get(target); if (current == null) { current = new ArrayList<>(); } current.add(roll); targetToDiceMap.put(target, current); } private void findCost(final IDelegateBridge bridge) { // if no planes left after aa fires, this is possible if (m_attackingUnits.isEmpty()) { return; } int damageLimit = TerritoryAttachment.getProduction(m_battleSite); int cost = 0; final boolean lhtrBombers = games.strategy.triplea.Properties.getLHTR_Heavy_Bombers(m_data); int index = 0; final boolean limitDamage = isWW2V2() || isLimitSBRDamageToProduction(); final List<Die> dice = new ArrayList<>(); final HashMap<Unit, List<Die>> targetToDiceMap = new HashMap<>(); // limit to maxDamage for (final Unit attacker : m_attackingUnits) { final UnitAttachment ua = UnitAttachment.get(attacker.getType()); int rolls; rolls = BattleCalculator.getRolls(attacker, m_attacker, false, true, m_territoryEffects); int costThisUnit = 0; if (rolls > 1 && (lhtrBombers || ua.getChooseBestRoll())) { // LHTR means we select the best Dice roll for the unit int max = 0; int maxIndex = index; int startIndex = index; for (int i = 0; i < rolls; i++) { // +1 since 0 based if (m_dice[index] + 1 > max) { max = m_dice[index] + 1; maxIndex = index; } index++; } costThisUnit = max; // for show final Die best = new Die(m_dice[maxIndex]); dice.add(best); addToTargetDiceMap(attacker, best, targetToDiceMap); for (int i = 0; i < rolls; i++) { if (startIndex != maxIndex) { final Die notBest = new Die(m_dice[startIndex], -1, DieType.IGNORED); dice.add(notBest); addToTargetDiceMap(attacker, notBest, targetToDiceMap); } startIndex++; } } else { for (int i = 0; i < rolls; i++) { costThisUnit += m_dice[index] + 1; final Die die = new Die(m_dice[index]); dice.add(die); addToTargetDiceMap(attacker, die, targetToDiceMap); index++; } } costThisUnit = Math.max(0, (costThisUnit + TechAbilityAttachment.getBombingBonus(attacker.getType(), attacker.getOwner(), m_data))); if (limitDamage) { costThisUnit = Math.min(costThisUnit, damageLimit); } cost += costThisUnit; if (!m_targets.isEmpty()) { m_bombingRaidDamage.add(getTarget(attacker), costThisUnit); } } // Limit PUs lost if we would like to cap PUs lost at territory value if (isPUCap(m_data) || isLimitSBRDamagePerTurn(m_data)) { final int alreadyLost = DelegateFinder.moveDelegate(m_data).PUsAlreadyLost(m_battleSite); final int limit = Math.max(0, damageLimit - alreadyLost); cost = Math.min(cost, limit); if (!m_targets.isEmpty()) { for (final Unit u : m_bombingRaidDamage.keySet()) { if (m_bombingRaidDamage.getInt(u) > limit) { m_bombingRaidDamage.put(u, limit); } } } } // If we damage units instead of territories if (isDamageFromBombingDoneToUnitsInsteadOfTerritories()) { // at this point, m_bombingRaidDamage should contain all units that m_targets contains if (!m_targets.keySet().containsAll(m_bombingRaidDamage.keySet())) { throw new IllegalStateException("targets should contain all damaged units"); } for (final Unit current : m_bombingRaidDamage.keySet()) { int currentUnitCost = m_bombingRaidDamage.getInt(current); // determine the max allowed damage // UnitAttachment ua = UnitAttachment.get(current.getType()); final TripleAUnit taUnit = (TripleAUnit) current; damageLimit = taUnit.getHowMuchMoreDamageCanThisUnitTake(current, m_battleSite); if (m_bombingRaidDamage.getInt(current) > damageLimit) { m_bombingRaidDamage.put(current, damageLimit); cost = (cost - currentUnitCost) + damageLimit; currentUnitCost = m_bombingRaidDamage.getInt(current); } final int totalDamage = taUnit.getUnitDamage() + currentUnitCost; // display the results getDisplay(bridge).bombingResults(m_battleID, dice, currentUnitCost); if (currentUnitCost > 0) { bridge.getSoundChannelBroadcaster().playSoundForAll(SoundPath.CLIP_BOMBING_STRATEGIC, m_attacker); } // Record production lost DelegateFinder.moveDelegate(m_data).PUsLost(m_battleSite, currentUnitCost); // apply the hits to the targets final IntegerMap<Unit> damageMap = new IntegerMap<>(); damageMap.put(current, totalDamage); bridge.addChange(ChangeFactory.bombingUnitDamage(damageMap)); bridge.getHistoryWriter() .addChildToEvent("Bombing raid in " + m_battleSite.getName() + " rolls: " + MyFormatter.asDice(targetToDiceMap.get(current)) + " and causes: " + currentUnitCost + " damage to unit: " + current.getType().getName()); getRemote(bridge).reportMessage( "Bombing raid in " + m_battleSite.getName() + " rolls: " + MyFormatter.asDice(targetToDiceMap.get(current)) + " and causes: " + currentUnitCost + " damage to unit: " + current.getType().getName(), "Bombing raid causes " + currentUnitCost + " damage to " + current.getType().getName()); } } else { // Record PUs lost DelegateFinder.moveDelegate(m_data).PUsLost(m_battleSite, cost); cost *= Properties.getPU_Multiplier(m_data); getDisplay(bridge).bombingResults(m_battleID, dice, cost); if (cost > 0) { bridge.getSoundChannelBroadcaster().playSoundForAll(SoundPath.CLIP_BOMBING_STRATEGIC, m_attacker); } // get resources final Resource PUs = m_data.getResourceList().getResource(Constants.PUS); final int have = m_defender.getResources().getQuantity(PUs); final int toRemove = Math.min(cost, have); final Change change = ChangeFactory.changeResourcesChange(m_defender, PUs, -toRemove); bridge.addChange(change); bridge.getHistoryWriter().addChildToEvent("Bombing raid in " + m_battleSite.getName() + " rolls: " + MyFormatter.asDice(m_dice) + " and costs: " + cost + " " + MyFormatter.pluralize("PU", cost) + "."); } m_bombingRaidTotal = cost; } } @Override public void unitsLostInPrecedingBattle(final IBattle battle, final Collection<Unit> units, final IDelegateBridge bridge, final boolean withdrawn) { // should never happen } }