diff --git a/game-app/game-core/src/main/java/games/strategy/engine/data/UnitTypeList.java b/game-app/game-core/src/main/java/games/strategy/engine/data/UnitTypeList.java index b74c8a8b0ec..b6d7bb16450 100644 --- a/game-app/game-core/src/main/java/games/strategy/engine/data/UnitTypeList.java +++ b/game-app/game-core/src/main/java/games/strategy/engine/data/UnitTypeList.java @@ -24,6 +24,7 @@ public class UnitTypeList extends GameDataComponent implements Iterable supportRules; private transient @Nullable Set supportAaRules; + private transient @Nullable Set supportAirRules; public UnitTypeList(final GameData data) { super(data); @@ -81,6 +82,21 @@ public Set getSupportAaRules() { return supportAaRules; } + /** + * Returns the unit support rules for the unit types. Computed once and cached afterwards. + * + * @return The unit support rules. + */ + public Set getSupportAirRules() { + if (supportAirRules == null) { + supportAirRules = + UnitSupportAttachment.get(this).stream() + .filter(usa -> (usa.getAirRoll() || usa.getAirStrength())) + .collect(Collectors.toSet()); + } + return supportAirRules; + } + public int size() { return unitTypes.size(); } diff --git a/game-app/game-core/src/main/java/games/strategy/triplea/attachments/RulesAttachment.java b/game-app/game-core/src/main/java/games/strategy/triplea/attachments/RulesAttachment.java index 1908d9db9cc..526e7973026 100644 --- a/game-app/game-core/src/main/java/games/strategy/triplea/attachments/RulesAttachment.java +++ b/game-app/game-core/src/main/java/games/strategy/triplea/attachments/RulesAttachment.java @@ -3,6 +3,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; +import com.google.common.annotations.VisibleForTesting; import games.strategy.engine.data.Attachable; import games.strategy.engine.data.BattleRecordsList; import games.strategy.engine.data.GameData; @@ -13,6 +14,7 @@ import games.strategy.engine.data.MutableProperty; import games.strategy.engine.data.RelationshipTracker.Relationship; import games.strategy.engine.data.RelationshipType; +import games.strategy.engine.data.Resource; import games.strategy.engine.data.TechnologyFrontier; import games.strategy.engine.data.Territory; import games.strategy.engine.data.Unit; @@ -55,9 +57,13 @@ public class RulesAttachment extends AbstractPlayerRulesAttachment { private int techCount = -1; // condition for having specific relationships private @Nullable List relationship = null; + // condition for checking ai player + private boolean checkAIPlayer = false; // condition for being at war private @Nullable Set atWarPlayers = null; private int atWarCount = -1; + // condition for checking resources + private @Nullable String[] haveResources = null; // condition for having destroyed at least X enemy non-neutral TUV (total unit value) [according // to // the prices the defender pays for the units] @@ -145,6 +151,41 @@ public static Set getNationalObjectives(final GamePlayer player return natObjs; } + @VisibleForTesting + public void setHaveResources(final String value) throws GameParseException { + final String[] s = splitOnColon(value); + if (s.length <= 1) { + throw new GameParseException( + "haveResources must have at least 2 fields. Format value=resource1 count=number, or " + + "value=resource1:resource2:resource3 count=number" + + thisErrorMsg()); + } + final int n = getInt(s[0]); + if (n < 1) { + throw new GameParseException("haveResources must be a positive integer" + thisErrorMsg()); + } + for (int i = 1; i < s.length; i++) { + // validate that this resource exists in the xml + final Resource r = getData().getResourceList().getResource(s[i]); + if (r == null) { + throw new GameParseException("No resource called: " + s[i] + thisErrorMsg()); + } + } + haveResources = s; + } + + private void setHaveResources(final String[] value) { + haveResources = value; + } + + public String[] getHaveResources() { + return haveResources; + } + + private void resetHaveResources() { + haveResources = null; + } + private void setDestroyedTuv(final String value) throws GameParseException { final String[] s = splitOnColon(value); if (s.length != 2) { @@ -489,6 +530,22 @@ private void resetUnitPresence() { unitPresence = null; } + private void setCheckAIPlayer(final String s) { + checkAIPlayer = getBool(s); + } + + private void setCheckAIPlayer(final Boolean s) { + checkAIPlayer = s; + } + + public boolean getCheckAIPlayer() { + return checkAIPlayer; + } + + private void resetCheckAIPlayer() { + checkAIPlayer = false; + } + private int getAtWarCount() { return atWarCount; } @@ -735,11 +792,21 @@ public boolean isSatisfied( } objectiveMet = checkDirectOwnership(listedTerritories, players); } + // check for ai controlled player + if (objectiveMet && getCheckAIPlayer()) { + objectiveMet = checkCheckAIPlayer(players); + } + // check for resources + if (objectiveMet && haveResources != null) { + objectiveMet = checkHaveResources(players); + } // get attached to player final GamePlayer playerAttachedTo = (GamePlayer) getAttachedTo(); + // check for players at war if (objectiveMet && !getAtWarPlayers().isEmpty()) { objectiveMet = checkAtWar(playerAttachedTo, getAtWarPlayers(), getAtWarCount()); } + // check for techs if (objectiveMet && !getTechs().isEmpty()) { objectiveMet = checkTechs(playerAttachedTo, data.getTechnologyFrontier()); } @@ -1001,6 +1068,14 @@ private boolean matchTerritories( return numberMet >= getTerritoryCount(); } + private boolean checkCheckAIPlayer(final List players) { + boolean bcheck = true; + for (GamePlayer player : players) { + bcheck = (bcheck && player.isAi()); + } + return bcheck; + } + private boolean checkAtWar( final GamePlayer player, final Set enemies, final int count) { int found = CollectionUtils.countMatches(enemies, player::isAtWar); @@ -1031,6 +1106,18 @@ private boolean checkTechs(final GamePlayer player, final TechnologyFrontier tec return found >= techCount; } + @VisibleForTesting + public boolean checkHaveResources(final List players) { + int itotal = 0; + for (GamePlayer player : players) { + for (int i = 1; i < haveResources.length; i++) { + final Resource resource = getData().getResourceList().getResource(haveResources[i]); + itotal += player.getResources().getQuantity(resource); + } + } + return itotal >= getInt(haveResources[0]); + } + @Override public void validate(final GameState data) { validateNames(alliedOwnershipTerritories); @@ -1057,6 +1144,12 @@ public MutableProperty getPropertyOrNull(String propertyName) { this::setRelationship, this::getRelationship, this::resetRelationship); + case "checkAIPlayer": + return MutableProperty.of( + this::setCheckAIPlayer, + this::setCheckAIPlayer, + this::getCheckAIPlayer, + this::resetCheckAIPlayer); case "atWarPlayers": return MutableProperty.of( this::setAtWarPlayers, @@ -1065,6 +1158,12 @@ public MutableProperty getPropertyOrNull(String propertyName) { this::resetAtWarPlayers); case "atWarCount": return MutableProperty.ofReadOnly(this::getAtWarCount); + case "haveResources": + return MutableProperty.of( + this::setHaveResources, + this::setHaveResources, + this::getHaveResources, + this::resetHaveResources); case "destroyedTUV": return MutableProperty.ofString( this::setDestroyedTuv, this::getDestroyedTuv, this::resetDestroyedTuv); diff --git a/game-app/game-core/src/main/java/games/strategy/triplea/attachments/TechAbilityAttachment.java b/game-app/game-core/src/main/java/games/strategy/triplea/attachments/TechAbilityAttachment.java index f683c448764..01bfbdc9aae 100644 --- a/game-app/game-core/src/main/java/games/strategy/triplea/attachments/TechAbilityAttachment.java +++ b/game-app/game-core/src/main/java/games/strategy/triplea/attachments/TechAbilityAttachment.java @@ -68,6 +68,7 @@ public class TechAbilityAttachment extends DefaultAttachment { private @Nullable IntegerMap attackRollsBonus = null; private @Nullable IntegerMap defenseRollsBonus = null; private @Nullable IntegerMap bombingBonus = null; + private @Nullable IntegerMap tuvBonus = null; public TechAbilityAttachment( final String name, final Attachable attachable, final GameData gameData) { @@ -692,6 +693,24 @@ private void resetBombingBonus() { bombingBonus = null; } + private void setTUVBonus(final String value) throws GameParseException { + if (tuvBonus == null) { + tuvBonus = new IntegerMap<>(); + } + applyCheckedValue("tuvBonus", value, tuvBonus::put); + } + + private void setTUVBonus(final IntegerMap value) { + tuvBonus = value; + } + + public IntegerMap getTUVBonus() { + return getIntegerMapProperty(tuvBonus); + } + + private void resetTUVBonus() { + tuvBonus = null; + } public static boolean getAllowAirborneForces(final Collection techAdvances) { return techAdvances.stream() .map(TechAbilityAttachment::get) @@ -1029,6 +1048,12 @@ public MutableProperty getPropertyOrNull(String propertyName) { this::setBombingBonus, this::getBombingBonus, this::resetBombingBonus); + case "tuvBonus": + return MutableProperty.of( + this::setTUVBonus, + this::setTUVBonus, + this::getTUVBonus, + this::resetTUVBonus); default: return null; } diff --git a/game-app/game-core/src/main/java/games/strategy/triplea/attachments/UnitAttachment.java b/game-app/game-core/src/main/java/games/strategy/triplea/attachments/UnitAttachment.java index 1d95388c04f..84cc8a58e7a 100644 --- a/game-app/game-core/src/main/java/games/strategy/triplea/attachments/UnitAttachment.java +++ b/game-app/game-core/src/main/java/games/strategy/triplea/attachments/UnitAttachment.java @@ -2525,13 +2525,19 @@ private void setTuv(final String s) throws GameParseException { tuv = getInt(s); } - private void setTuv(final Integer s) { + @VisibleForTesting + public void setTuv(final Integer s) { tuv = s; } - public int getTuv() { + private int getTuv() { return tuv; } + public int getTuv(final GamePlayer player) { + // must account for -1 value, so that TUV can be calculated + final int bonus = getTechTracker().tuv(player, getUnitType()); + return Math.max(-1, bonus + tuv); + } private void resetTuv() { tuv = -1; diff --git a/game-app/game-core/src/main/java/games/strategy/triplea/attachments/UnitSupportAttachment.java b/game-app/game-core/src/main/java/games/strategy/triplea/attachments/UnitSupportAttachment.java index 24b6c68471f..1e5321248c6 100644 --- a/game-app/game-core/src/main/java/games/strategy/triplea/attachments/UnitSupportAttachment.java +++ b/game-app/game-core/src/main/java/games/strategy/triplea/attachments/UnitSupportAttachment.java @@ -45,6 +45,8 @@ public class UnitSupportAttachment extends DefaultAttachment { private boolean strength = false; private boolean aaRoll = false; private boolean aaStrength = false; + private boolean airRoll = false; + private boolean airStrength = false; private int bonus = 0; private int number = 0; private boolean allied = false; @@ -184,9 +186,13 @@ public UnitSupportAttachment setDice(final String dice) throws GameParseExceptio aaRoll = true; } else if (element.equalsIgnoreCase("AAstrength")) { aaStrength = true; + } else if (element.equalsIgnoreCase("airRoll")) { + airRoll = true; + } else if (element.equalsIgnoreCase("airStrength")) { + airStrength = true; } else { throw new GameParseException( - dice + " dice must be roll, strength, AAroll, or AAstrength: " + thisErrorMsg()); + dice + " dice must be roll, strength, AAroll, AAstrength, airRoll, or airStrength: " + thisErrorMsg()); } } return this; @@ -203,6 +209,8 @@ private void resetDice() { strength = false; aaRoll = false; aaStrength = false; + airRoll = false; + airStrength = false; } private void setBonus(final String bonus) { @@ -327,6 +335,14 @@ public boolean getAaStrength() { return aaStrength; } + public boolean getAirRoll() { + return airRoll; + } + + public boolean getAirStrength() { + return airStrength; + } + public boolean getDefence() { return defence; } @@ -437,6 +453,10 @@ public MutableProperty getPropertyOrNull(String propertyName) { return MutableProperty.ofReadOnly(this::getAaRoll); case "aaStrength": return MutableProperty.ofReadOnly(this::getAaStrength); + case "airRoll": + return MutableProperty.ofReadOnly(this::getAirRoll); + case "airStrength": + return MutableProperty.ofReadOnly(this::getAirStrength); case BONUS: return MutableProperty.of(this::setBonus, this::setBonus, this::getBonus, this::resetBonus); case "number": diff --git a/game-app/game-core/src/main/java/games/strategy/triplea/delegate/TechTracker.java b/game-app/game-core/src/main/java/games/strategy/triplea/delegate/TechTracker.java index b041a2828ef..c75cb2f83d2 100644 --- a/game-app/game-core/src/main/java/games/strategy/triplea/delegate/TechTracker.java +++ b/game-app/game-core/src/main/java/games/strategy/triplea/delegate/TechTracker.java @@ -120,6 +120,14 @@ public boolean canBombard(GamePlayer player, UnitType type) { return getCached(player, type, "canBombard", getter); } + public int tuv(GamePlayer player, UnitType type) { + //sum will be 0 if there are no TUV bonus + //the return value must be checked against -1 calculate + final Supplier getter = + () -> getSumOfBonuses(TechAbilityAttachment::getTUVBonus, type, player); + return getCached(player, type, "getTUVBonus", getter); + } + public int getMinimumTerritoryValueForProductionBonus(final GamePlayer player) { final Supplier getter = () -> diff --git a/game-app/game-core/src/main/java/games/strategy/triplea/delegate/battle/AirBattle.java b/game-app/game-core/src/main/java/games/strategy/triplea/delegate/battle/AirBattle.java index c645b539755..a9b35bcca92 100644 --- a/game-app/game-core/src/main/java/games/strategy/triplea/delegate/battle/AirBattle.java +++ b/game-app/game-core/src/main/java/games/strategy/triplea/delegate/battle/AirBattle.java @@ -696,9 +696,10 @@ public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { bridge, "Attackers Fire, ", CombatValueBuilder.airBattleCombatValue() + .enemyUnits(defendingUnits) + .friendlyUnits(attackingUnits) .side(BattleState.Side.OFFENSE) - .lhtrHeavyBombers( - Properties.getLhtrHeavyBombers(bridge.getData().getProperties())) + .supportAttachments(bridge.getData().getUnitTypeList().getSupportAirRules()) .gameDiceSides(bridge.getData().getDiceSides()) .build()); } @@ -713,16 +714,12 @@ public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { CasualtySelector.selectCasualties( defender, defendingUnits, - CombatValueBuilder.mainCombatValue() + CombatValueBuilder.airBattleCombatValue() .enemyUnits(attackingUnits) .friendlyUnits(defendingUnits) .side(BattleState.Side.DEFENSE) - .gameSequence(bridge.getData().getSequence()) - .supportAttachments(bridge.getData().getUnitTypeList().getSupportRules()) - .lhtrHeavyBombers( - Properties.getLhtrHeavyBombers(bridge.getData().getProperties())) + .supportAttachments(bridge.getData().getUnitTypeList().getSupportAirRules()) .gameDiceSides(bridge.getData().getDiceSides()) - .territoryEffects(List.of()) .build(), battleSite, bridge, @@ -776,9 +773,10 @@ public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { bridge, "Defenders Fire, ", CombatValueBuilder.airBattleCombatValue() + .enemyUnits(attackingUnits) + .friendlyUnits(defendingUnits) .side(BattleState.Side.DEFENSE) - .lhtrHeavyBombers( - Properties.getLhtrHeavyBombers(bridge.getData().getProperties())) + .supportAttachments(bridge.getData().getUnitTypeList().getSupportAirRules()) .gameDiceSides(bridge.getData().getDiceSides()) .build()); } @@ -793,16 +791,12 @@ public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { CasualtySelector.selectCasualties( attacker, attackingUnits, - CombatValueBuilder.mainCombatValue() + CombatValueBuilder.airBattleCombatValue() .enemyUnits(defendingUnits) .friendlyUnits(attackingUnits) .side(BattleState.Side.OFFENSE) - .gameSequence(bridge.getData().getSequence()) - .supportAttachments(bridge.getData().getUnitTypeList().getSupportRules()) - .lhtrHeavyBombers( - Properties.getLhtrHeavyBombers(bridge.getData().getProperties())) + .supportAttachments(bridge.getData().getUnitTypeList().getSupportAirRules()) .gameDiceSides(bridge.getData().getDiceSides()) - .territoryEffects(List.of()) .build(), battleSite, bridge, diff --git a/game-app/game-core/src/main/java/games/strategy/triplea/delegate/power/calculator/AirBattleDefenseCombatValue.java b/game-app/game-core/src/main/java/games/strategy/triplea/delegate/power/calculator/AirBattleDefenseCombatValue.java index 2a943fdb8cf..4b3b9df07ef 100644 --- a/game-app/game-core/src/main/java/games/strategy/triplea/delegate/power/calculator/AirBattleDefenseCombatValue.java +++ b/game-app/game-core/src/main/java/games/strategy/triplea/delegate/power/calculator/AirBattleDefenseCombatValue.java @@ -23,7 +23,10 @@ class AirBattleDefenseCombatValue implements CombatValue { @Nonnull Integer gameDiceSides; - @Nonnull Boolean lhtrHeavyBombers; + @Nonnull AvailableSupports strengthSupportFromFriends; + @Nonnull AvailableSupports strengthSupportFromEnemies; + @Nonnull AvailableSupports rollSupportFromFriends; + @Nonnull AvailableSupports rollSupportFromEnemies; @Getter(onMethod_ = @Override) @Nonnull @@ -37,34 +40,42 @@ class AirBattleDefenseCombatValue implements CombatValue { @Override public RollCalculator getRoll() { - return new AirBattleDefenseRoll(); + return new AirBattleDefenseRoll(rollSupportFromFriends.copy(), rollSupportFromEnemies.copy()); } @Override public StrengthCalculator getStrength() { - return new AirBattleDefenseStrength(gameDiceSides); + return new AirBattleDefenseStrength( + gameDiceSides, + strengthSupportFromFriends.copy(), + strengthSupportFromEnemies.copy()); } @Override - public BattleState.Side getBattleSide() { - return BattleState.Side.DEFENSE; + public int getDiceSides(final Unit unit) { + return gameDiceSides; } @Override - public int getDiceSides(final Unit unit) { - return gameDiceSides; + public BattleState.Side getBattleSide() { + return BattleState.Side.DEFENSE; } @Override public boolean chooseBestRoll(final Unit unit) { - return lhtrHeavyBombers || unit.getUnitAttachment().getChooseBestRoll(); + return false; } @Override public CombatValue buildWithNoUnitSupports() { return AirBattleDefenseCombatValue.builder() .gameDiceSides(gameDiceSides) - .lhtrHeavyBombers(lhtrHeavyBombers) + .rollSupportFromFriends(AvailableSupports.EMPTY_RESULT) + .rollSupportFromEnemies(AvailableSupports.EMPTY_RESULT) + .strengthSupportFromFriends(AvailableSupports.EMPTY_RESULT) + .strengthSupportFromEnemies(AvailableSupports.EMPTY_RESULT) + .friendUnits(List.of()) + .enemyUnits(List.of()) .build(); } @@ -72,21 +83,31 @@ public CombatValue buildWithNoUnitSupports() { public CombatValue buildOppositeCombatValue() { return AirBattleOffenseCombatValue.builder() .gameDiceSides(gameDiceSides) - .lhtrHeavyBombers(lhtrHeavyBombers) + .rollSupportFromFriends(rollSupportFromEnemies) + .rollSupportFromEnemies(rollSupportFromFriends) + .strengthSupportFromFriends(strengthSupportFromEnemies) + .strengthSupportFromEnemies(strengthSupportFromFriends) + .friendUnits(enemyUnits) + .enemyUnits(friendUnits) .build(); } @Value static class AirBattleDefenseRoll implements RollCalculator { + AvailableSupports supportFromFriends; + AvailableSupports supportFromEnemies; + @Override public RollValue getRoll(final Unit unit) { - return RollValue.of(unit.getUnitAttachment().getDefenseRolls(unit.getOwner())); + return RollValue.of(1) + .add(supportFromFriends.giveSupportToUnit(unit)) + .add(supportFromEnemies.giveSupportToUnit(unit)); } @Override public Map> getSupportGiven() { - return Map.of(); + return SupportCalculator.getCombinedSupportGiven(supportFromFriends, supportFromEnemies); } } @@ -94,15 +115,20 @@ public Map> getSupportGiven() { static class AirBattleDefenseStrength implements StrengthCalculator { int diceSides; + AvailableSupports supportFromFriends; + AvailableSupports supportFromEnemies; @Override public StrengthValue getStrength(final Unit unit) { - return StrengthValue.of(diceSides, unit.getUnitAttachment().getAirDefense(unit.getOwner())); + return StrengthValue.of(diceSides, + unit.getUnitAttachment().getAirDefense(unit.getOwner())) + .add(supportFromFriends.giveSupportToUnit(unit)) + .add(supportFromEnemies.giveSupportToUnit(unit)); } @Override public Map> getSupportGiven() { - return Map.of(); + return SupportCalculator.getCombinedSupportGiven(supportFromFriends, supportFromEnemies); } } } diff --git a/game-app/game-core/src/main/java/games/strategy/triplea/delegate/power/calculator/AirBattleOffenseCombatValue.java b/game-app/game-core/src/main/java/games/strategy/triplea/delegate/power/calculator/AirBattleOffenseCombatValue.java index 67bded7a37c..d2cf8661f50 100644 --- a/game-app/game-core/src/main/java/games/strategy/triplea/delegate/power/calculator/AirBattleOffenseCombatValue.java +++ b/game-app/game-core/src/main/java/games/strategy/triplea/delegate/power/calculator/AirBattleOffenseCombatValue.java @@ -23,7 +23,10 @@ class AirBattleOffenseCombatValue implements CombatValue { @Nonnull Integer gameDiceSides; - @Nonnull Boolean lhtrHeavyBombers; + @Nonnull AvailableSupports strengthSupportFromFriends; + @Nonnull AvailableSupports strengthSupportFromEnemies; + @Nonnull AvailableSupports rollSupportFromFriends; + @Nonnull AvailableSupports rollSupportFromEnemies; @Getter(onMethod_ = @Override) @Nonnull @@ -37,12 +40,15 @@ class AirBattleOffenseCombatValue implements CombatValue { @Override public RollCalculator getRoll() { - return new AirBattleOffenseRoll(); + return new AirBattleOffenseRoll(rollSupportFromFriends.copy(), rollSupportFromEnemies.copy()); } @Override public StrengthCalculator getStrength() { - return new AirBattleOffenseStrength(gameDiceSides); + return new AirBattleOffenseStrength( + gameDiceSides, + strengthSupportFromFriends.copy(), + strengthSupportFromEnemies.copy()); } @Override @@ -57,14 +63,19 @@ public int getDiceSides(final Unit unit) { @Override public boolean chooseBestRoll(final Unit unit) { - return lhtrHeavyBombers || unit.getUnitAttachment().getChooseBestRoll(); + return false; } @Override public CombatValue buildWithNoUnitSupports() { return AirBattleOffenseCombatValue.builder() .gameDiceSides(gameDiceSides) - .lhtrHeavyBombers(lhtrHeavyBombers) + .rollSupportFromFriends(AvailableSupports.EMPTY_RESULT) + .rollSupportFromEnemies(AvailableSupports.EMPTY_RESULT) + .strengthSupportFromFriends(AvailableSupports.EMPTY_RESULT) + .strengthSupportFromEnemies(AvailableSupports.EMPTY_RESULT) + .friendUnits(List.of()) + .enemyUnits(List.of()) .build(); } @@ -72,21 +83,30 @@ public CombatValue buildWithNoUnitSupports() { public CombatValue buildOppositeCombatValue() { return AirBattleDefenseCombatValue.builder() .gameDiceSides(gameDiceSides) - .lhtrHeavyBombers(lhtrHeavyBombers) + .rollSupportFromFriends(rollSupportFromEnemies) + .rollSupportFromEnemies(rollSupportFromFriends) + .strengthSupportFromFriends(strengthSupportFromEnemies) + .strengthSupportFromEnemies(strengthSupportFromFriends) + .friendUnits(enemyUnits) + .enemyUnits(friendUnits) .build(); } @Value static class AirBattleOffenseRoll implements RollCalculator { + AvailableSupports supportFromFriends; + AvailableSupports supportFromEnemies; @Override public RollValue getRoll(final Unit unit) { - return RollValue.of(unit.getUnitAttachment().getAttackRolls(unit.getOwner())); + return RollValue.of(1) + .add(supportFromFriends.giveSupportToUnit(unit)) + .add(supportFromEnemies.giveSupportToUnit(unit)); } @Override public Map> getSupportGiven() { - return Map.of(); + return SupportCalculator.getCombinedSupportGiven(supportFromFriends, supportFromEnemies); } } @@ -94,15 +114,20 @@ public Map> getSupportGiven() { static class AirBattleOffenseStrength implements StrengthCalculator { int diceSides; + AvailableSupports supportFromFriends; + AvailableSupports supportFromEnemies; @Override public StrengthValue getStrength(final Unit unit) { - return StrengthValue.of(diceSides, unit.getUnitAttachment().getAirAttack(unit.getOwner())); + return StrengthValue.of(diceSides, + unit.getUnitAttachment().getAirAttack(unit.getOwner())) + .add(supportFromFriends.giveSupportToUnit(unit)) + .add(supportFromEnemies.giveSupportToUnit(unit)); } @Override public Map> getSupportGiven() { - return Map.of(); + return SupportCalculator.getCombinedSupportGiven(supportFromFriends, supportFromEnemies); } } } diff --git a/game-app/game-core/src/main/java/games/strategy/triplea/delegate/power/calculator/CombatValueBuilder.java b/game-app/game-core/src/main/java/games/strategy/triplea/delegate/power/calculator/CombatValueBuilder.java index b9a3db6c0a1..039556d5353 100644 --- a/game-app/game-core/src/main/java/games/strategy/triplea/delegate/power/calculator/CombatValueBuilder.java +++ b/game-app/game-core/src/main/java/games/strategy/triplea/delegate/power/calculator/CombatValueBuilder.java @@ -149,16 +149,52 @@ static CombatValue buildBombardmentCombatValue( @Builder(builderMethodName = "airBattleCombatValue", builderClassName = "AirBattleBuilder") static CombatValue buildAirBattleCombatValue( - final BattleState.Side side, final boolean lhtrHeavyBombers, final int gameDiceSides) { + final Collection enemyUnits, + final Collection friendlyUnits, + final BattleState.Side side, + final Collection supportAttachments, + final int gameDiceSides) { + + // Get all friendly supports + final AvailableSupports supportFromFriends = + AvailableSupports.getSortedSupport( + new SupportCalculator( + friendlyUnits, // + supportAttachments, + side, + true)); + + // Get all enemy supports + final AvailableSupports supportFromEnemies = + AvailableSupports.getSortedSupport( + new SupportCalculator( + enemyUnits, // + supportAttachments, + side.getOpposite(), + false)); return side == BattleState.Side.DEFENSE - ? AirBattleDefenseCombatValue.builder() + ? AirBattleDefenseCombatValue.builder() + .strengthSupportFromFriends( + supportFromFriends.filter(UnitSupportAttachment::getAirStrength)) + .strengthSupportFromEnemies( + supportFromEnemies.filter(UnitSupportAttachment::getAirStrength)) + .rollSupportFromFriends(supportFromFriends.filter(UnitSupportAttachment::getAirRoll)) + .rollSupportFromEnemies(supportFromEnemies.filter(UnitSupportAttachment::getAirRoll)) + .friendUnits(friendlyUnits) + .enemyUnits(enemyUnits) .gameDiceSides(gameDiceSides) - .lhtrHeavyBombers(lhtrHeavyBombers) .build() - : AirBattleOffenseCombatValue.builder() + : AirBattleOffenseCombatValue.builder() + .strengthSupportFromFriends( + supportFromFriends.filter(UnitSupportAttachment::getAirStrength)) + .strengthSupportFromEnemies( + supportFromEnemies.filter(UnitSupportAttachment::getAirStrength)) + .rollSupportFromFriends(supportFromFriends.filter(UnitSupportAttachment::getAirRoll)) + .rollSupportFromEnemies(supportFromEnemies.filter(UnitSupportAttachment::getAirRoll)) + .friendUnits(friendlyUnits) + .enemyUnits(enemyUnits) .gameDiceSides(gameDiceSides) - .lhtrHeavyBombers(lhtrHeavyBombers) .build(); } } diff --git a/game-app/game-core/src/main/java/games/strategy/triplea/ui/BattleModel.java b/game-app/game-core/src/main/java/games/strategy/triplea/ui/BattleModel.java index f76f8495aed..63acedf85a2 100644 --- a/game-app/game-core/src/main/java/games/strategy/triplea/ui/BattleModel.java +++ b/game-app/game-core/src/main/java/games/strategy/triplea/ui/BattleModel.java @@ -109,8 +109,10 @@ void refresh() { if (isAirPreBattleOrPreRaid) { combatValue = CombatValueBuilder.airBattleCombatValue() + .enemyUnits(new ArrayList<>(enemyBattleModel.getUnits())) + .friendlyUnits(units) .side(BattleState.Side.DEFENSE) - .lhtrHeavyBombers(lhtrHeavyBombers) + .supportAttachments(gameData.getUnitTypeList().getSupportAirRules()) .gameDiceSides(gameData.getDiceSides()) .build(); } else { diff --git a/game-app/game-core/src/main/java/games/strategy/triplea/util/TuvCostsCalculator.java b/game-app/game-core/src/main/java/games/strategy/triplea/util/TuvCostsCalculator.java index dd5c4468d01..5a754b8af05 100644 --- a/game-app/game-core/src/main/java/games/strategy/triplea/util/TuvCostsCalculator.java +++ b/game-app/game-core/src/main/java/games/strategy/triplea/util/TuvCostsCalculator.java @@ -52,7 +52,7 @@ public IntegerMap computeCostsForTuv(final GamePlayer player) { // Override with XML TUV or consumesUnit sum final IntegerMap result = new IntegerMap<>(costs); for (final UnitType unitType : costs.keySet()) { - result.put(unitType, getTotalTuv(unitType, costs, new HashSet<>())); + result.put(unitType, getTotalTuv(player, unitType, costs, new HashSet<>())); } return result; @@ -88,6 +88,7 @@ private IntegerMap computeBaseCostsForPlayer(GamePlayer player) { */ private static IntegerMap getCostsForTuvForAllPlayersMergedAndAveraged( final GameData data) { + final GamePlayer nullPlayer = data.getPlayerList().getNullPlayer(); final Resource pus = data.getResourceList().getResource(Constants.PUS); final IntegerMap costs = new IntegerMap<>(); final Map> differentCosts = new HashMap<>(); @@ -120,19 +121,19 @@ private static IntegerMap getCostsForTuvForAllPlayersMergedAndAveraged // Add any units that have XML TUV even if they aren't purchasable for (final UnitType unitType : data.getUnitTypeList()) { final UnitAttachment ua = unitType.getUnitAttachment(); - if (ua.getTuv() > -1) { - costs.put(unitType, ua.getTuv()); + if (ua.getTuv(nullPlayer) > -1) { + costs.put(unitType, ua.getTuv(nullPlayer)); } } return costs; } - private static int getTotalTuv( + private static int getTotalTuv(final GamePlayer player, final UnitType unitType, final IntegerMap costs, final Set alreadyAdded) { final UnitAttachment ua = unitType.getUnitAttachment(); - if (ua.getTuv() > -1) { - return ua.getTuv(); + if (ua.getTuv(player) > -1) { + return ua.getTuv(player); } int tuv = costs.getInt(unitType); if (ua.getConsumesUnits().isEmpty() || alreadyAdded.contains(unitType)) { @@ -140,7 +141,7 @@ private static int getTotalTuv( } alreadyAdded.add(unitType); for (final UnitType ut : ua.getConsumesUnits().keySet()) { - tuv += ua.getConsumesUnits().getInt(ut) * getTotalTuv(ut, costs, alreadyAdded); + tuv += ua.getConsumesUnits().getInt(ut) * getTotalTuv(player, ut, costs, alreadyAdded); } alreadyAdded.remove(unitType); return tuv; diff --git a/game-app/game-core/src/test/java/games/strategy/triplea/attachments/RulesAttachmentTest.java b/game-app/game-core/src/test/java/games/strategy/triplea/attachments/RulesAttachmentTest.java new file mode 100644 index 00000000000..4e8e7bb48d4 --- /dev/null +++ b/game-app/game-core/src/test/java/games/strategy/triplea/attachments/RulesAttachmentTest.java @@ -0,0 +1,118 @@ +package games.strategy.triplea.attachments; + +import static games.strategy.triplea.Constants.PUS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import games.strategy.engine.data.GameData; +import games.strategy.engine.data.GamePlayer; +import games.strategy.engine.data.gameparser.GameParseException; +import games.strategy.triplea.delegate.GameDataTestUtil; +import games.strategy.triplea.xml.TestMapGameData; +import java.security.SecureRandom; +import java.util.List; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class RulesAttachmentTest { + + /** + * "Victory" map is just a branch/mod of Pact of Steel 2. POS2 is an actual game with good + * gameplay that we don't want to mess with, so "Victory" is more of an xml purely for testing + * purposes, and probably should never be played. + */ + private final GameData gameData = TestMapGameData.VICTORY_TEST.getGameData(); + + private final RulesAttachment attachment = new RulesAttachment("Test attachment", null, gameData); + + @Nested + class HaveResources { + + private final GamePlayer italians = GameDataTestUtil.italians(gameData); + private final GamePlayer germans = GameDataTestUtil.germans(gameData); + private final String fuel = "Fuel"; + private final String ore = "Ore"; + /* Length test for haveResources */ + @Test + void setHaveResourcesInvalidLength() { + assertThrows(GameParseException.class, () -> attachment.setHaveResources("")); + assertThrows(GameParseException.class, () -> attachment.setHaveResources("a")); + } + + /* Invalid arguments for haveResources */ + @Test + void setHaveResourcesInvalidArgs() { + /* Not a number (NAN) test */ + assertThrows(IllegalArgumentException.class, () -> attachment.setHaveResources("NAN:PUs")); + /* 0 value test */ + assertThrows(GameParseException.class, () -> attachment.setHaveResources("0:PUs")); + /* Not a resource test */ + assertThrows(GameParseException.class, () -> attachment.setHaveResources("1:NOT A RESOURCE")); + assertThrows(GameParseException.class, () -> attachment.setHaveResources("0:w")); + assertThrows(GameParseException.class, () -> attachment.setHaveResources("0:w:e")); + assertThrows(GameParseException.class, () -> attachment.setHaveResources("0:add:w")); + assertThrows(GameParseException.class, () -> attachment.setHaveResources("0:add:w:e")); + } + + /* Testing stored values with getHaveResources */ + @Test + void setHaveResourcesTest() throws Exception { + final SecureRandom rand = new SecureRandom(); + final String random1 = Integer.toString(Math.abs(rand.nextInt())); + final String[] expected1 = new String[] {random1, PUS}; + + attachment.setHaveResources(concatWithColon(random1, PUS)); + assertEquals(expected1[0], attachment.getHaveResources()[0]); + assertEquals(expected1[1], attachment.getHaveResources()[1]); + } + + /* Testing checkHaveResources */ + @Test + void testCheckHaveResources() throws Exception { + final int italianFuelAmount = italians.getResources().getQuantity(fuel); + final int italianPuAmount = italians.getResources().getQuantity(PUS); + final int italianOreAmount = italians.getResources().getQuantity(ore); + final int germanFuelAmount = germans.getResources().getQuantity(fuel); + final int germanPuAmount = germans.getResources().getQuantity(PUS); + final int germanOreAmount = germans.getResources().getQuantity(ore); + final int testItalianPU = italianPuAmount; + final int testItalianResources = italianOreAmount + italianFuelAmount + italianPuAmount; + final int testPUs = testItalianPU + germanPuAmount; + final int testResources = + testItalianResources + germanPuAmount + germanFuelAmount + germanOreAmount; + + /* testing with 1 player */ + final List players = List.of(italians); + attachment.setHaveResources(concatWithColon(String.valueOf(testItalianPU), PUS)); + assertTrue(attachment.checkHaveResources(players)); + attachment.setHaveResources(concatWithColon(String.valueOf(testItalianResources), PUS)); + assertFalse(attachment.checkHaveResources(players)); + attachment.setHaveResources( + concatWithColon(String.valueOf(testItalianResources), PUS, fuel)); + assertFalse(attachment.checkHaveResources(players)); + attachment.setHaveResources( + concatWithColon(String.valueOf(testItalianResources), PUS, fuel, ore)); + assertTrue(attachment.checkHaveResources(players)); + + /* testing with 2 players */ + final List players1 = List.of(italians, germans); + attachment.setHaveResources(concatWithColon(String.valueOf(testPUs), PUS)); + assertTrue(attachment.checkHaveResources(players1)); + attachment.setHaveResources(concatWithColon(String.valueOf(testResources), PUS)); + assertFalse(attachment.checkHaveResources(players1)); + attachment.setHaveResources(concatWithColon(String.valueOf(testResources), PUS, fuel)); + assertFalse(attachment.checkHaveResources(players1)); + attachment.setHaveResources(concatWithColon(String.valueOf(testResources), PUS, fuel, ore)); + assertTrue(attachment.checkHaveResources(players1)); + } + + private String concatWithColon(final String... args) { + return String.join(":", args); + } + } +} diff --git a/game-app/game-core/src/test/java/games/strategy/triplea/delegate/power/calculator/AirBattleDefenseCombatValueTest.java b/game-app/game-core/src/test/java/games/strategy/triplea/delegate/power/calculator/AirBattleDefenseCombatValueTest.java index 3d9f7363e92..8c9d65adf96 100644 --- a/game-app/game-core/src/test/java/games/strategy/triplea/delegate/power/calculator/AirBattleDefenseCombatValueTest.java +++ b/game-app/game-core/src/test/java/games/strategy/triplea/delegate/power/calculator/AirBattleDefenseCombatValueTest.java @@ -10,9 +10,16 @@ import games.strategy.engine.data.GamePlayer; import games.strategy.engine.data.Unit; import games.strategy.engine.data.UnitType; +import games.strategy.engine.data.gameparser.GameParseException; import games.strategy.triplea.attachments.UnitAttachment; +import games.strategy.triplea.attachments.UnitSupportAttachment; +import games.strategy.triplea.delegate.battle.BattleState; +import java.util.List; +import java.util.Map; +import java.util.Set; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.triplea.java.collections.IntegerMap; class AirBattleDefenseCombatValueTest { @@ -20,7 +27,7 @@ class AirBattleDefenseCombatValueTest { class AirBattleDefenseStrengthTest { @Test - void calculatesValue() { + void calculatesValue() throws GameParseException { final GameData gameData = givenGameData().withDiceSides(6).build(); final GamePlayer player = mock(GamePlayer.class); @@ -31,9 +38,55 @@ void calculatesValue() { final Unit unit = unitType.createTemp(1, player).get(0); unit.getUnitAttachment().setAirDefense(3); + final Unit supportUnit = unitType.createTemp(1, player).get(0); + final UnitSupportAttachment unitSupportAttachment = + givenUnitSupportAttachment(gameData, unitType, "test") + .setBonus(3) + .setPlayers(List.of(player)) + .setUnitType(Set.of(unitType)); + + final AvailableSupports friendlySupport = + AvailableSupports.getSupport( + new SupportCalculator( + List.of(supportUnit), + Set.of(unitSupportAttachment), + BattleState.Side.OFFENSE, + true)); + + final Unit enemySupportUnit = unitType.createTemp(1, player).get(0); + final UnitSupportAttachment enemyUnitSupportAttachment = + givenUnitSupportAttachment(gameData, unitType, "test2") + .setBonus(-2) + .setPlayers(List.of(player)) + .setUnitType(Set.of(unitType)); + + final AvailableSupports enemySupport = + AvailableSupports.getSupport( + new SupportCalculator( + List.of(enemySupportUnit), + Set.of(enemyUnitSupportAttachment), + BattleState.Side.OFFENSE, + true)); + final AirBattleDefenseCombatValue.AirBattleDefenseStrength strength = - new AirBattleDefenseCombatValue.AirBattleDefenseStrength(6); - assertThat("Air defense is 3", strength.getStrength(unit).getValue(), is(3)); + new AirBattleDefenseCombatValue.AirBattleDefenseStrength( + 6, friendlySupport, enemySupport); + assertThat( + "Strength starts at 3, friendly adds 3, enemy removes 2: total 4", + strength.getStrength(unit).getValue(), + is(4)); + } + + UnitSupportAttachment givenUnitSupportAttachment( + final GameData gameData, final UnitType unitType, final String name) + throws GameParseException { + return new UnitSupportAttachment("rule" + name, unitType, gameData) + .setBonus(1) + .setBonusType("bonus" + name) + .setDice("airStrength") + .setNumber(1) + .setSide("offence") + .setFaction("allied"); } @Test @@ -49,11 +102,68 @@ void limitsToDiceSides() { unit.getUnitAttachment().setAirDefense(8); final AirBattleDefenseCombatValue.AirBattleDefenseStrength strength = - new AirBattleDefenseCombatValue.AirBattleDefenseStrength(6); + new AirBattleDefenseCombatValue.AirBattleDefenseStrength(6, AvailableSupports.EMPTY_RESULT, AvailableSupports.EMPTY_RESULT); assertThat( - "Air defense is 8 but dice sides is 6 so it is limited to 6", - strength.getStrength(unit).getValue(), - is(6)); + "Air defense is 8 but dice sides is 6 so it is limited to 6", + strength.getStrength(unit).getValue(), + is(6)); + } + + @Test + void calculatesSupportUsed() throws GameParseException { + final GameData gameData = givenGameData().withDiceSides(6).build(); + + final GamePlayer player = mock(GamePlayer.class); + + final UnitType unitType = new UnitType("test", gameData); + final UnitAttachment unitAttachment = new UnitAttachment("attachment", unitType, gameData); + unitType.addAttachment(UNIT_ATTACHMENT_NAME, unitAttachment); + final Unit unit = unitType.createTemp(1, player).get(0); + unit.getUnitAttachment().setAirDefense(3); + + final Unit supportUnit = unitType.createTemp(1, player).get(0); + final UnitSupportAttachment unitSupportAttachment = + givenUnitSupportAttachment(gameData, unitType, "test") + .setBonus(2) + .setPlayers(List.of(player)) + .setUnitType(Set.of(unitType)); + + final AvailableSupports friendlySupport = + AvailableSupports.getSupport( + new SupportCalculator( + List.of(supportUnit), + Set.of(unitSupportAttachment), + BattleState.Side.OFFENSE, + true)); + + final Unit enemySupportUnit = unitType.createTemp(1, player).get(0); + final UnitSupportAttachment enemyUnitSupportAttachment = + givenUnitSupportAttachment(gameData, unitType, "test2") + .setBonus(-1) + .setPlayers(List.of(player)) + .setUnitType(Set.of(unitType)); + + final AvailableSupports enemySupport = + AvailableSupports.getSupport( + new SupportCalculator( + List.of(enemySupportUnit), + Set.of(enemyUnitSupportAttachment), + BattleState.Side.OFFENSE, + true)); + + final AirBattleDefenseCombatValue.AirBattleDefenseStrength strength = + new AirBattleDefenseCombatValue.AirBattleDefenseStrength( + 6, friendlySupport, enemySupport); + strength.getStrength(unit); + assertThat( + "Friendly gave 2 and enemy gave -1", + strength.getSupportGiven(), + is( + Map.of( + supportUnit, + IntegerMap.of(Map.of(unit, 2)), + enemySupportUnit, + IntegerMap.of(Map.of(unit, -1))))); } } } diff --git a/game-app/game-core/src/test/java/games/strategy/triplea/delegate/power/calculator/AirBattleOffenseCombatValueTest.java b/game-app/game-core/src/test/java/games/strategy/triplea/delegate/power/calculator/AirBattleOffenseCombatValueTest.java index a49d8984c62..3be8bf3b25d 100644 --- a/game-app/game-core/src/test/java/games/strategy/triplea/delegate/power/calculator/AirBattleOffenseCombatValueTest.java +++ b/game-app/game-core/src/test/java/games/strategy/triplea/delegate/power/calculator/AirBattleOffenseCombatValueTest.java @@ -10,9 +10,17 @@ import games.strategy.engine.data.GamePlayer; import games.strategy.engine.data.Unit; import games.strategy.engine.data.UnitType; +import games.strategy.engine.data.gameparser.GameParseException; import games.strategy.triplea.attachments.UnitAttachment; +import games.strategy.triplea.attachments.UnitSupportAttachment; +import games.strategy.triplea.delegate.battle.BattleState; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.triplea.java.collections.IntegerMap; + +import java.util.List; +import java.util.Map; +import java.util.Set; class AirBattleOffenseCombatValueTest { @@ -20,7 +28,7 @@ class AirBattleOffenseCombatValueTest { class AirBattleOffenseStrengthTest { @Test - void calculatesValue() { + void calculatesValue() throws GameParseException { final GameData gameData = givenGameData().withDiceSides(6).build(); final GamePlayer player = mock(GamePlayer.class); @@ -31,13 +39,59 @@ void calculatesValue() { final Unit unit = unitType.createTemp(1, player).get(0); unit.getUnitAttachment().setAirAttack(3); + final Unit supportUnit = unitType.createTemp(1, player).get(0); + final UnitSupportAttachment unitSupportAttachment = + givenUnitSupportAttachment(gameData, unitType, "test") + .setBonus(3) + .setPlayers(List.of(player)) + .setUnitType(Set.of(unitType)); + + final AvailableSupports friendlySupport = + AvailableSupports.getSupport( + new SupportCalculator( + List.of(supportUnit), + Set.of(unitSupportAttachment), + BattleState.Side.OFFENSE, + true)); + + final Unit enemySupportUnit = unitType.createTemp(1, player).get(0); + final UnitSupportAttachment enemyUnitSupportAttachment = + givenUnitSupportAttachment(gameData, unitType, "test2") + .setBonus(-2) + .setPlayers(List.of(player)) + .setUnitType(Set.of(unitType)); + + final AvailableSupports enemySupport = + AvailableSupports.getSupport( + new SupportCalculator( + List.of(enemySupportUnit), + Set.of(enemyUnitSupportAttachment), + BattleState.Side.OFFENSE, + true)); + final AirBattleOffenseCombatValue.AirBattleOffenseStrength strength = - new AirBattleOffenseCombatValue.AirBattleOffenseStrength(6); - assertThat("Air attack is 3", strength.getStrength(unit).getValue(), is(3)); + new AirBattleOffenseCombatValue.AirBattleOffenseStrength( + 6, friendlySupport, enemySupport); + assertThat( + "Strength starts at 3, friendly adds 3, enemy removes 2: total 4", + strength.getStrength(unit).getValue(), + is(4)); + } + + UnitSupportAttachment givenUnitSupportAttachment( + final GameData gameData, final UnitType unitType, final String name) + throws GameParseException { + return new UnitSupportAttachment("rule" + name, unitType, gameData) + .setBonus(1) + .setBonusType("bonus" + name) + .setDice("airStrength") + .setNumber(1) + .setSide("offence") + .setFaction("allied"); } @Test - void limitsToDiceSides() { + void limitsToDiceSides() { final GameData gameData = givenGameData().withDiceSides(6).build(); final GamePlayer player = mock(GamePlayer.class); @@ -48,12 +102,70 @@ void limitsToDiceSides() { final Unit unit = unitType.createTemp(1, player).get(0); unit.getUnitAttachment().setAirAttack(8); + final AirBattleOffenseCombatValue.AirBattleOffenseStrength strength = - new AirBattleOffenseCombatValue.AirBattleOffenseStrength(6); + new AirBattleOffenseCombatValue.AirBattleOffenseStrength(6, AvailableSupports.EMPTY_RESULT, AvailableSupports.EMPTY_RESULT); + assertThat( + "Air defense is 8 but dice sides is 6 so it is limited to 6", + strength.getStrength(unit).getValue(), + is(6)); + } + + @Test + void calculatesSupportUsed() throws GameParseException { + final GameData gameData = givenGameData().withDiceSides(6).build(); + + final GamePlayer player = mock(GamePlayer.class); + + final UnitType unitType = new UnitType("test", gameData); + final UnitAttachment unitAttachment = new UnitAttachment("attachment", unitType, gameData); + unitType.addAttachment(UNIT_ATTACHMENT_NAME, unitAttachment); + final Unit unit = unitType.createTemp(1, player).get(0); + unit.getUnitAttachment().setAirAttack(3); + + final Unit supportUnit = unitType.createTemp(1, player).get(0); + final UnitSupportAttachment unitSupportAttachment = + givenUnitSupportAttachment(gameData, unitType, "test") + .setBonus(2) + .setPlayers(List.of(player)) + .setUnitType(Set.of(unitType)); + + final AvailableSupports friendlySupport = + AvailableSupports.getSupport( + new SupportCalculator( + List.of(supportUnit), + Set.of(unitSupportAttachment), + BattleState.Side.OFFENSE, + true)); + + final Unit enemySupportUnit = unitType.createTemp(1, player).get(0); + final UnitSupportAttachment enemyUnitSupportAttachment = + givenUnitSupportAttachment(gameData, unitType, "test2") + .setBonus(-1) + .setPlayers(List.of(player)) + .setUnitType(Set.of(unitType)); + + final AvailableSupports enemySupport = + AvailableSupports.getSupport( + new SupportCalculator( + List.of(enemySupportUnit), + Set.of(enemyUnitSupportAttachment), + BattleState.Side.OFFENSE, + true)); + + final AirBattleDefenseCombatValue.AirBattleDefenseStrength strength = + new AirBattleDefenseCombatValue.AirBattleDefenseStrength( + 6, friendlySupport, enemySupport); + strength.getStrength(unit); assertThat( - "Air attack is 8 but dice sides is 6 so it is limited to 6", - strength.getStrength(unit).getValue(), - is(6)); + "Friendly gave 2 and enemy gave -1", + strength.getSupportGiven(), + is( + Map.of( + supportUnit, + IntegerMap.of(Map.of(unit, 2)), + enemySupportUnit, + IntegerMap.of(Map.of(unit, -1))))); } } }