package games.strategy.triplea.delegate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
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.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.TechAbilityAttachment;
import games.strategy.triplea.attachments.TerritoryAttachment;
import games.strategy.triplea.attachments.UnitAttachment;
import games.strategy.triplea.formatter.MyFormatter;
import games.strategy.triplea.player.ITripleAPlayer;
import games.strategy.util.CompositeMatch;
import games.strategy.util.CompositeMatchAnd;
import games.strategy.util.IntegerMap;
import games.strategy.util.Match;
/**
* Logic to fire rockets.
*/
public class RocketsFireHelper {
private boolean isWW2V2(final GameData data) {
return games.strategy.triplea.Properties.getWW2V2(data);
}
private boolean isAllRocketsAttack(final GameData data) {
return games.strategy.triplea.Properties.getAllRocketsAttack(data);
}
private boolean isRocketsCanFlyOverImpassables(final GameData data) {
return games.strategy.triplea.Properties.getRocketsCanFlyOverImpassables(data);
}
private boolean isDamageFromBombingDoneToUnitsInsteadOfTerritories(final GameData data) {
return games.strategy.triplea.Properties.getDamageFromBombingDoneToUnitsInsteadOfTerritories(data);
}
private boolean isRocketAttacksPerFactoryInfinite(final GameData data) {
return games.strategy.triplea.Properties.getRocketAttacksPerFactoryInfinite(data);
}
private boolean isPUCap(final GameData data) {
return games.strategy.triplea.Properties.getPUCap(data);
}
private boolean isLimitRocketDamagePerTurn(final GameData data) {
return games.strategy.triplea.Properties.getLimitRocketDamagePerTurn(data);
}
private boolean isLimitRocketDamageToProduction(final GameData data) {
return games.strategy.triplea.Properties.getLimitRocketAndSBRDamageToProduction(data);
}
public RocketsFireHelper() {}
public void fireRockets(final IDelegateBridge bridge, final PlayerID player) {
final GameData data = bridge.getData();
final Set<Territory> rocketTerritories = getTerritoriesWithRockets(data, player);
if (rocketTerritories.isEmpty()) {
bridge.getHistoryWriter().startEvent(player.getName() + " has no rockets to fire");
// getRemote(bridge).reportMessage("No rockets to fire", "No rockets to fire");
return;
}
if (isWW2V2(data) || isAllRocketsAttack(data)) {
fireWW2V2(bridge, player, rocketTerritories);
} else {
fireWW2V1(bridge, player, rocketTerritories);
}
}
private void fireWW2V2(final IDelegateBridge bridge, final PlayerID player, final Set<Territory> rocketTerritories) {
final GameData data = bridge.getData();
final Set<Territory> attackedTerritories = new HashSet<>();
final Map<Territory,Territory> attackingFromTerritories = new LinkedHashMap<>();
final boolean oneAttackPerTerritory = !isRocketAttacksPerFactoryInfinite(data);
for (final Territory territory : rocketTerritories) {
final Set<Territory> targets = getTargetsWithinRange(territory, data, player);
if (oneAttackPerTerritory) {
targets.removeAll(attackedTerritories);
}
if (targets.isEmpty()) {
continue;
}
// Ask the user where each rocket launcher should target.
final Territory target = getTarget(targets, bridge, territory);
if (target != null) {
attackedTerritories.add(target);
attackingFromTerritories.put(target,territory);
}
}
for (final Territory target : attackedTerritories) {
// Roll dice for the rocket attack damage and apply it
fireRocket(player, target, bridge, attackingFromTerritories.get(target));
}
}
/** In this rule set, each player only gets one rocket attack per turn. */
private void fireWW2V1(final IDelegateBridge bridge, final PlayerID player, final Set<Territory> rocketTerritories) {
final GameData data = bridge.getData();
final Set<Territory> targets = new HashSet<>();
for (final Territory territory : rocketTerritories) {
targets.addAll(getTargetsWithinRange(territory, data, player));
}
if (targets.isEmpty()) {
bridge.getHistoryWriter().startEvent(player.getName() + " has no targets to attack with rockets");
return;
}
final Territory attacked = getTarget(targets, bridge, null);
if (attacked != null) {
fireRocket(player, attacked, bridge, null);
}
}
Set<Territory> getTerritoriesWithRockets(final GameData data, final PlayerID player) {
final Set<Territory> territories = new HashSet<>();
final CompositeMatch<Unit> ownedRockets = rocketMatch(player, data);
final BattleTracker tracker = AbstractMoveDelegate.getBattleTracker(data);
for (final Territory current : data.getMap()) {
if (tracker.wasConquered(current)) {
continue;
}
if (current.getUnits().someMatch(ownedRockets)) {
territories.add(current);
}
}
return territories;
}
CompositeMatch<Unit> rocketMatch(final PlayerID player, final GameData data) {
return new CompositeMatchAnd<>(Matches.UnitIsRocket, Matches.unitIsOwnedBy(player), Matches.UnitIsNotDisabled,
Matches.unitIsBeingTransported().invert(), Matches.UnitIsSubmerged.invert(), Matches.unitHasNotMoved);
}
private Set<Territory> getTargetsWithinRange(final Territory territory, final GameData data, final PlayerID player) {
final int maxDistance = TechAbilityAttachment.getRocketDistance(player, data);
final Collection<Territory> possible = data.getMap().getNeighbors(territory, maxDistance);
final Set<Territory> hasFactory = new HashSet<>();
final CompositeMatchAnd<Territory> allowed =
new CompositeMatchAnd<>(Matches.territoryAllowsRocketsCanFlyOver(player, data));
if (!isRocketsCanFlyOverImpassables(data)) {
allowed.add(Matches.TerritoryIsNotImpassable);
}
final CompositeMatchAnd<Unit> attackableUnits =
new CompositeMatchAnd<>(Matches.enemyUnit(player, data), Matches.unitIsBeingTransported().invert());
for (final Territory current : possible) {
final Route route = data.getMap().getRoute(territory, current, allowed);
if (route != null && route.numberOfSteps() <= maxDistance) {
if (current.getUnits().someMatch(new CompositeMatchAnd<>(attackableUnits,
Matches.UnitIsAtMaxDamageOrNotCanBeDamaged(current).invert()))) {
hasFactory.add(current);
}
}
}
return hasFactory;
}
private Territory getTarget(final Collection<Territory> targets, final IDelegateBridge bridge,
final Territory from) {
// ask even if there is only once choice, that will allow the user to not attack if he doesn't want to
return ((ITripleAPlayer) bridge.getRemotePlayer()).whereShouldRocketsAttack(targets, from);
}
private void fireRocket(final PlayerID player, final Territory attackedTerritory, final IDelegateBridge bridge,
final Territory attackFrom) {
final GameData data = bridge.getData();
final PlayerID attacked = attackedTerritory.getOwner();
final Resource PUs = data.getResourceList().getResource(Constants.PUS);
final boolean DamageFromBombingDoneToUnits = isDamageFromBombingDoneToUnitsInsteadOfTerritories(data);
// unit damage vs territory damage
final Collection<Unit> enemyUnits = attackedTerritory.getUnits().getMatches(
new CompositeMatchAnd<>(Matches.enemyUnit(player, data), Matches.unitIsBeingTransported().invert()));
final Collection<Unit> enemyTargetsTotal =
Match.getMatches(enemyUnits, Matches.UnitIsAtMaxDamageOrNotCanBeDamaged(attackedTerritory).invert());
final Collection<Unit> targets = new ArrayList<>();
final Collection<Unit> rockets;
// attackFrom could be null if WW2V1
if (attackFrom == null) {
rockets = null;
} else {
rockets = new ArrayList<>(Match.getMatches(attackFrom.getUnits().getUnits(), rocketMatch(player, data)));
}
final int numberOfAttacks = (rockets == null ? 1
: Math.min(TechAbilityAttachment.getRocketNumberPerTerritory(player, data),
TechAbilityAttachment.getRocketDiceNumber(rockets, data)));
if (numberOfAttacks <= 0) {
return;
}
final String transcript;
if (DamageFromBombingDoneToUnits) {
// TODO: rockets needs to be completely redone to allow for multiple rockets to fire at different targets, etc
// etc.
final HashSet<UnitType> legalTargetsForTheseRockets = new HashSet<>();
if (rockets == null) {
legalTargetsForTheseRockets.addAll(data.getUnitTypeList().getAllUnitTypes());
} else {
// a hack for now, we let the rockets fire at anyone who could be targetted by any rocket
for (final Unit r : rockets) {
legalTargetsForTheseRockets.addAll(UnitAttachment.get(r.getType()).getBombingTargets(data));
}
}
final Collection<Unit> enemyTargets =
Match.getMatches(enemyTargetsTotal, Matches.unitIsOfTypes(legalTargetsForTheseRockets));
if (enemyTargets.isEmpty()) {
// TODO: this sucks
return;
}
Unit target = null;
if (enemyTargets.size() == 1) {
target = enemyTargets.iterator().next();
} else {
while (target == null) {
final ITripleAPlayer iplayer = (ITripleAPlayer) bridge.getRemotePlayer(player);
target = iplayer.whatShouldBomberBomb(attackedTerritory, enemyTargets, rockets);
}
}
if (target == null) {
throw new IllegalStateException("No Targets in " + attackedTerritory.getName());
}
targets.add(target);
}
final boolean doNotUseBombingBonus =
!games.strategy.triplea.Properties.getUseBombingMaxDiceSidesAndBonus(data) || rockets == null;
int cost = 0;
if (!games.strategy.triplea.Properties.getLL_DAMAGE_ONLY(data)) {
if (doNotUseBombingBonus || rockets == null) {
// no low luck, and no bonus, so just roll based on the map's dice sides
final int[] rolls = bridge.getRandom(data.getDiceSides(), numberOfAttacks, player, DiceType.BOMBING,
"Rocket fired by " + player.getName() + " at " + attacked.getName());
for (final int r : rolls) {
// we are zero based
cost += r + 1;
}
transcript = "Rockets " + (attackFrom == null ? "" : "in " + attackFrom.getName()) + " roll: "
+ MyFormatter.asDice(rolls);
} else {
// we must use bombing bonus
int highestMaxDice = 0;
int highestBonus = 0;
final int diceSides = data.getDiceSides();
for (final Unit u : rockets) {
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;
}
// we only roll once for rockets, so if there are other rockets here we just roll for the best rocket
if ((bonus + ((maxDice + 1) / 2)) > (highestBonus + ((highestMaxDice + 1) / 2))) {
highestMaxDice = maxDice;
highestBonus = bonus;
}
}
// now we roll, or don't if there is nothing to roll.
if (highestMaxDice > 0) {
final int[] rolls = bridge.getRandom(highestMaxDice, numberOfAttacks, player, DiceType.BOMBING,
"Rocket fired by " + player.getName() + " at " + attacked.getName());
for (int i = 0; i < rolls.length; i++) {
final int r = rolls[i] + highestBonus;
rolls[i] = r;
// we are zero based
cost += r + 1;
}
transcript = "Rockets " + (attackFrom == null ? "" : "in " + attackFrom.getName()) + " roll: "
+ MyFormatter.asDice(rolls);
} else {
cost = highestBonus * numberOfAttacks;
transcript = "Rockets " + (attackFrom == null ? "" : "in " + attackFrom.getName()) + " do " + highestBonus
+ " damage for each rocket";
}
}
} else {
if (doNotUseBombingBonus || rockets == null) {
// no bonus, so just roll based on the map's dice sides, but modify for LL
final int maxDice = (data.getDiceSides() + 1) / 3;
final int bonus = (data.getDiceSides() + 1) / 3;
final int[] rolls = bridge.getRandom(maxDice, numberOfAttacks, player, DiceType.BOMBING,
"Rocket fired by " + player.getName() + " at " + attacked.getName());
for (int i = 0; i < rolls.length; i++) {
final int r = rolls[i] + bonus;
rolls[i] = r;
// we are zero based
cost += r + 1;
}
transcript = "Rockets " + (attackFrom == null ? "" : "in " + attackFrom.getName()) + " roll: "
+ MyFormatter.asDice(rolls);
} else {
int highestMaxDice = 0;
int highestBonus = 0;
final int diceSides = data.getDiceSides();
for (final Unit u : rockets) {
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;
}
// we only roll once for rockets, so if there are other rockets here we just roll for the best rocket
if ((bonus + ((maxDice + 1) / 2)) > (highestBonus + ((highestMaxDice + 1) / 2))) {
highestMaxDice = maxDice;
highestBonus = bonus;
}
}
// now we roll, or don't if there is nothing to roll.
if (highestMaxDice > 0) {
final int[] rolls = bridge.getRandom(highestMaxDice, numberOfAttacks, player, DiceType.BOMBING,
"Rocket fired by " + player.getName() + " at " + attacked.getName());
for (int i = 0; i < rolls.length; i++) {
final int r = rolls[i] + highestBonus;
rolls[i] = r;
// we are zero based
cost += r + 1;
}
transcript = "Rockets " + (attackFrom == null ? "" : "in " + attackFrom.getName()) + " roll: "
+ MyFormatter.asDice(rolls);
} else {
cost = highestBonus * numberOfAttacks;
transcript = "Rockets " + (attackFrom == null ? "" : "in " + attackFrom.getName()) + " do " + highestBonus
+ " damage for each rocket";
}
}
}
int territoryProduction = TerritoryAttachment.getProduction(attackedTerritory);
if (DamageFromBombingDoneToUnits && !targets.isEmpty()) {
// we are doing damage to 'target', not to the territory
final Unit target = targets.iterator().next();
// UnitAttachment ua = UnitAttachment.get(target.getType());
final TripleAUnit taUnit = (TripleAUnit) target;
final int damageLimit = taUnit.getHowMuchMoreDamageCanThisUnitTake(target, attackedTerritory);
cost = Math.max(0, Math.min(cost, damageLimit));
final int totalDamage = taUnit.getUnitDamage() + cost;
// Record production lost
// DelegateFinder.moveDelegate(data).PUsLost(attackedTerritory, cost);
// apply the hits to the targets
final IntegerMap<Unit> damageMap = new IntegerMap<>();
damageMap.put(target, totalDamage);
bridge.addChange(ChangeFactory.bombingUnitDamage(damageMap));
// attackedTerritory.notifyChanged();
// in WW2V2, limit rocket attack cost to production value of factory.
} else if (isWW2V2(data) || isLimitRocketDamageToProduction(data)) {
// If we are limiting total PUs lost then take that into account
if (isPUCap(data) || isLimitRocketDamagePerTurn(data)) {
final int alreadyLost = DelegateFinder.moveDelegate(data).PUsAlreadyLost(attackedTerritory);
territoryProduction -= alreadyLost;
territoryProduction = Math.max(0, territoryProduction);
}
if (cost > territoryProduction) {
cost = territoryProduction;
}
}
// Record the PUs lost
DelegateFinder.moveDelegate(data).PUsLost(attackedTerritory, cost);
if (DamageFromBombingDoneToUnits && !targets.isEmpty()) {
getRemote(bridge).reportMessage(
"Rocket attack in " + attackedTerritory.getName() + " does " + cost + " damage to "
+ targets.iterator().next(),
"Rocket attack in " + attackedTerritory.getName() + " does " + cost + " damage to "
+ targets.iterator().next());
bridge.getHistoryWriter().startEvent("Rocket attack in " + attackedTerritory.getName() + " does " + cost
+ " damage to " + targets.iterator().next());
} else {
cost *= Properties.getPU_Multiplier(data);
getRemote(bridge).reportMessage("Rocket attack in " + attackedTerritory.getName() + " costs:" + cost,
"Rocket attack in " + attackedTerritory.getName() + " costs:" + cost);
// Trying to remove more PUs than the victim has is A Bad Thing[tm]
final int availForRemoval = attacked.getResources().getQuantity(PUs);
if (cost > availForRemoval) {
cost = availForRemoval;
}
final String transcriptText =
attacked.getName() + " lost " + cost + " PUs to rocket attack by " + player.getName();
bridge.getHistoryWriter().startEvent(transcriptText);
final Change rocketCharge = ChangeFactory.changeResourcesChange(attacked, PUs, -cost);
bridge.addChange(rocketCharge);
}
bridge.getHistoryWriter().addChildToEvent(transcript, rockets == null ? null : new ArrayList<>(rockets));
// this is null in WW2V1
if (attackFrom != null) {
if (rockets != null && !rockets.isEmpty()) {
// TODO: only a certain number fired...
final Change change = ChangeFactory.markNoMovementChange(Collections.singleton(rockets.iterator().next()));
bridge.addChange(change);
} else {
throw new IllegalStateException("No rockets?" + attackFrom.getUnits().getUnits());
}
}
// kill any units that can die if they have reached max damage (veqryn)
if (Match.someMatch(targets, Matches.UnitCanDieFromReachingMaxDamage)) {
final List<Unit> unitsCanDie = Match.getMatches(targets, Matches.UnitCanDieFromReachingMaxDamage);
unitsCanDie
.retainAll(Match.getMatches(unitsCanDie, Matches.UnitIsAtMaxDamageOrNotCanBeDamaged(attackedTerritory)));
if (!unitsCanDie.isEmpty()) {
// targets.removeAll(unitsCanDie);
final Change removeDead = ChangeFactory.removeUnits(attackedTerritory, unitsCanDie);
final String transcriptText = MyFormatter.unitsToText(unitsCanDie) + " lost in " + attackedTerritory.getName();
bridge.getHistoryWriter().addChildToEvent(transcriptText, unitsCanDie);
bridge.addChange(removeDead);
}
}
// play a sound
if (cost > 0) {
bridge.getSoundChannelBroadcaster().playSoundForAll(SoundPath.CLIP_BOMBING_ROCKET, player);
}
}
private ITripleAPlayer getRemote(final IDelegateBridge bridge) {
return (ITripleAPlayer) bridge.getRemotePlayer();
}
}