package games.strategy.triplea.attachments; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map.Entry; import java.util.Set; import games.strategy.engine.data.Attachable; import games.strategy.engine.data.DefaultAttachment; import games.strategy.engine.data.GameData; import games.strategy.engine.data.GameParseException; import games.strategy.engine.data.PlayerID; import games.strategy.engine.data.Unit; import games.strategy.engine.data.UnitType; import games.strategy.engine.data.annotations.GameProperty; import games.strategy.engine.data.annotations.InternalDoNotExport; import games.strategy.triplea.Constants; import games.strategy.triplea.MapSupport; import games.strategy.triplea.TripleAUnit; import games.strategy.triplea.delegate.GenericTechAdvance; import games.strategy.triplea.delegate.Matches; import games.strategy.triplea.delegate.TechAdvance; import games.strategy.triplea.delegate.TechTracker; import games.strategy.util.CompositeMatchAnd; import games.strategy.util.IntegerMap; import games.strategy.util.Match; /** * Attaches to technologies. * Also contains static methods of interpreting data from all technology attachments that a player has. */ @MapSupport public class TechAbilityAttachment extends DefaultAttachment { private static final long serialVersionUID = 1866305599625384294L; /** * Convenience method. */ public static TechAbilityAttachment get(final TechAdvance type) { if (type instanceof GenericTechAdvance) { // generic techs can name a hardcoded tech, therefore if it exists we should use the hard coded tech's attachment. // (if the map maker doesn't want to use the hardcoded tech's attachment, they should not name a hardcoded tech) final TechAdvance hardCodedAdvance = ((GenericTechAdvance) type).getAdvance(); if (hardCodedAdvance != null) { final TechAbilityAttachment hardCodedTechAttachment = (TechAbilityAttachment) hardCodedAdvance.getAttachment(Constants.TECH_ABILITY_ATTACHMENT_NAME); return hardCodedTechAttachment; } } final TechAbilityAttachment rVal = (TechAbilityAttachment) type.getAttachment(Constants.TECH_ABILITY_ATTACHMENT_NAME); return rVal; } /** * Convenience method. */ public static TechAbilityAttachment get(final TechAdvance type, final String nameOfAttachment) { final TechAbilityAttachment rVal = (TechAbilityAttachment) type.getAttachment(nameOfAttachment); if (rVal == null) { throw new IllegalStateException( "No technology attachment for:" + type.getName() + " with name:" + nameOfAttachment); } return rVal; } // unitAbilitiesGained Static Strings public static final String ABILITY_CAN_BLITZ = "canBlitz"; public static final String ABILITY_CAN_BOMBARD = "canBombard"; // attachment fields private IntegerMap<UnitType> m_attackBonus = new IntegerMap<>(); private IntegerMap<UnitType> m_defenseBonus = new IntegerMap<>(); private IntegerMap<UnitType> m_movementBonus = new IntegerMap<>(); private IntegerMap<UnitType> m_radarBonus = new IntegerMap<>(); private IntegerMap<UnitType> m_airAttackBonus = new IntegerMap<>(); private IntegerMap<UnitType> m_airDefenseBonus = new IntegerMap<>(); private IntegerMap<UnitType> m_productionBonus = new IntegerMap<>(); // -1 means not set private int m_minimumTerritoryValueForProductionBonus = -1; // -1 means not set private int m_repairDiscount = -1; // -1 means not set private int m_warBondDiceSides = -1; private int m_warBondDiceNumber = 0; // -1 means not set // not needed because this is controlled in the unit attachment with // private int m_rocketDiceSides = -1; // bombingBonus and bombingMaxDieSides private IntegerMap<UnitType> m_rocketDiceNumber = new IntegerMap<>(); private int m_rocketDistance = 0; private int m_rocketNumberPerTerritory = 0; private HashMap<UnitType, HashSet<String>> m_unitAbilitiesGained = new HashMap<>(); private boolean m_airborneForces = false; private IntegerMap<UnitType> m_airborneCapacity = new IntegerMap<>(); private HashSet<UnitType> m_airborneTypes = new HashSet<>(); private int m_airborneDistance = 0; private HashSet<UnitType> m_airborneBases = new HashSet<>(); private HashMap<String, HashSet<UnitType>> m_airborneTargettedByAA = new HashMap<>(); private IntegerMap<UnitType> m_attackRollsBonus = new IntegerMap<>(); private IntegerMap<UnitType> m_defenseRollsBonus = new IntegerMap<>(); private IntegerMap<UnitType> m_bombingBonus = new IntegerMap<>(); public TechAbilityAttachment(final String name, final Attachable attachable, final GameData gameData) { super(name, attachable, gameData); } // setters and getters /** * Adds to, not sets. Anything that adds to instead of setting needs a clear function as well. */ @GameProperty(xmlProperty = true, gameProperty = true, adds = true) public void setAttackBonus(final String value) throws GameParseException { final String[] s = value.split(":"); if (s.length <= 0 || s.length > 2) { throw new GameParseException("attackBonus cannot be empty or have more than two fields" + thisErrorMsg()); } String unitType; unitType = s[1]; // validate that this unit exists in the xml final UnitType ut = getData().getUnitTypeList().getUnitType(unitType); if (ut == null) { throw new GameParseException("No unit called:" + unitType + thisErrorMsg()); } // we should allow positive and negative numbers final int n = getInt(s[0]); m_attackBonus.put(ut, n); } @GameProperty(xmlProperty = true, gameProperty = true, adds = false) public void setAttackBonus(final IntegerMap<UnitType> value) { m_attackBonus = value; } public IntegerMap<UnitType> getAttackBonus() { return m_attackBonus; } public void clearAttackBonus() { m_attackBonus.clear(); } public void resetAttackBonus() { m_attackBonus = new IntegerMap<>(); } /** * Adds to, not sets. Anything that adds to instead of setting needs a clear function as well. */ @GameProperty(xmlProperty = true, gameProperty = true, adds = true) public void setDefenseBonus(final String value) throws GameParseException { final String[] s = value.split(":"); if (s.length <= 0 || s.length > 2) { throw new GameParseException("defenseBonus cannot be empty or have more than two fields" + thisErrorMsg()); } String unitType; unitType = s[1]; // validate that this unit exists in the xml final UnitType ut = getData().getUnitTypeList().getUnitType(unitType); if (ut == null) { throw new GameParseException("No unit called:" + unitType + thisErrorMsg()); } // we should allow positive and negative numbers final int n = getInt(s[0]); m_defenseBonus.put(ut, n); } @GameProperty(xmlProperty = true, gameProperty = true, adds = false) public void setDefenseBonus(final IntegerMap<UnitType> value) { m_defenseBonus = value; } public IntegerMap<UnitType> getDefenseBonus() { return m_defenseBonus; } public void clearDefenseBonus() { m_defenseBonus.clear(); } public void resetDefenseBonus() { m_defenseBonus = new IntegerMap<>(); } /** * Adds to, not sets. Anything that adds to instead of setting needs a clear function as well. */ @GameProperty(xmlProperty = true, gameProperty = true, adds = true) public void setMovementBonus(final String value) throws GameParseException { final String[] s = value.split(":"); if (s.length <= 0 || s.length > 2) { throw new GameParseException("movementBonus cannot be empty or have more than two fields" + thisErrorMsg()); } String unitType; unitType = s[1]; // validate that this unit exists in the xml final UnitType ut = getData().getUnitTypeList().getUnitType(unitType); if (ut == null) { throw new GameParseException("No unit called:" + unitType + thisErrorMsg()); } // we should allow positive and negative numbers final int n = getInt(s[0]); m_movementBonus.put(ut, n); } @GameProperty(xmlProperty = true, gameProperty = true, adds = false) public void setMovementBonus(final IntegerMap<UnitType> value) { m_movementBonus = value; } public IntegerMap<UnitType> getMovementBonus() { return m_movementBonus; } public void clearMovementBonus() { m_movementBonus.clear(); } public void resetMovementBonus() { m_movementBonus = new IntegerMap<>(); } /** * Adds to, not sets. Anything that adds to instead of setting needs a clear function as well. */ @GameProperty(xmlProperty = true, gameProperty = true, adds = true) public void setRadarBonus(final String value) throws GameParseException { final String[] s = value.split(":"); if (s.length <= 0 || s.length > 2) { throw new GameParseException("radarBonus cannot be empty or have more than two fields" + thisErrorMsg()); } String unitType; unitType = s[1]; // validate that this unit exists in the xml final UnitType ut = getData().getUnitTypeList().getUnitType(unitType); if (ut == null) { throw new GameParseException("No unit called:" + unitType + thisErrorMsg()); } // we should allow positive and negative numbers final int n = getInt(s[0]); m_radarBonus.put(ut, n); } @GameProperty(xmlProperty = true, gameProperty = true, adds = false) public void setRadarBonus(final IntegerMap<UnitType> value) { m_radarBonus = value; } public IntegerMap<UnitType> getRadarBonus() { return m_radarBonus; } public void clearRadarBonus() { m_radarBonus.clear(); } public void resetRadarBonus() { m_radarBonus = new IntegerMap<>(); } /** * Adds to, not sets. Anything that adds to instead of setting needs a clear function as well. */ @GameProperty(xmlProperty = true, gameProperty = true, adds = true) public void setAirAttackBonus(final String value) throws GameParseException { final String[] s = value.split(":"); if (s.length <= 0 || s.length > 2) { throw new GameParseException("airAttackBonus cannot be empty or have more than two fields" + thisErrorMsg()); } String unitType; unitType = s[1]; // validate that this unit exists in the xml final UnitType ut = getData().getUnitTypeList().getUnitType(unitType); if (ut == null) { throw new GameParseException("No unit called:" + unitType + thisErrorMsg()); } // we should allow positive and negative numbers final int n = getInt(s[0]); m_airAttackBonus.put(ut, n); } @GameProperty(xmlProperty = true, gameProperty = true, adds = false) public void setAirAttackBonus(final IntegerMap<UnitType> value) { m_airAttackBonus = value; } public IntegerMap<UnitType> getAirAttackBonus() { return m_airAttackBonus; } public void clearAirAttackBonus() { m_airAttackBonus.clear(); } public void resetAirAttackBonus() { m_airAttackBonus = new IntegerMap<>(); } /** * Adds to, not sets. Anything that adds to instead of setting needs a clear function as well. */ @GameProperty(xmlProperty = true, gameProperty = true, adds = true) public void setAirDefenseBonus(final String value) throws GameParseException { final String[] s = value.split(":"); if (s.length <= 0 || s.length > 2) { throw new GameParseException("airDefenseBonus cannot be empty or have more than two fields" + thisErrorMsg()); } String unitType; unitType = s[1]; // validate that this unit exists in the xml final UnitType ut = getData().getUnitTypeList().getUnitType(unitType); if (ut == null) { throw new GameParseException("No unit called:" + unitType + thisErrorMsg()); } // we should allow positive and negative numbers final int n = getInt(s[0]); m_airDefenseBonus.put(ut, n); } @GameProperty(xmlProperty = true, gameProperty = true, adds = false) public void setAirDefenseBonus(final IntegerMap<UnitType> value) { m_airDefenseBonus = value; } public IntegerMap<UnitType> getAirDefenseBonus() { return m_airDefenseBonus; } public void clearAirDefenseBonus() { m_airDefenseBonus.clear(); } public void resetAirDefenseBonus() { m_airDefenseBonus = new IntegerMap<>(); } /** * Adds to, not sets. Anything that adds to instead of setting needs a clear function as well. */ @GameProperty(xmlProperty = true, gameProperty = true, adds = true) public void setProductionBonus(final String value) throws GameParseException { final String[] s = value.split(":"); if (s.length <= 0 || s.length > 2) { throw new GameParseException("productionBonus cannot be empty or have more than two fields" + thisErrorMsg()); } String unitType; unitType = s[1]; // validate that this unit exists in the xml final UnitType ut = getData().getUnitTypeList().getUnitType(unitType); if (ut == null) { throw new GameParseException("No unit called:" + unitType + thisErrorMsg()); } // we should allow positive and negative numbers final int n = getInt(s[0]); m_productionBonus.put(ut, n); } @GameProperty(xmlProperty = true, gameProperty = true, adds = false) public void setProductionBonus(final IntegerMap<UnitType> value) { m_productionBonus = value; } public IntegerMap<UnitType> getProductionBonus() { return m_productionBonus; } public void clearProductionBonus() { m_productionBonus.clear(); } public void resetProductionBonus() { m_productionBonus = new IntegerMap<>(); } @GameProperty(xmlProperty = true, gameProperty = true, adds = false) public void setMinimumTerritoryValueForProductionBonus(final String value) throws GameParseException { final int v = getInt(value); if ((v != -1) && (v < 0 || v > 10000)) { throw new GameParseException( "minimumTerritoryValueForProductionBonus must be -1 (no effect), or be between 0 and 10000" + thisErrorMsg()); } m_minimumTerritoryValueForProductionBonus = v; } @GameProperty(xmlProperty = true, gameProperty = true, adds = false) public void setMinimumTerritoryValueForProductionBonus(final Integer value) { m_minimumTerritoryValueForProductionBonus = value; } public int getMinimumTerritoryValueForProductionBonus() { return m_minimumTerritoryValueForProductionBonus; } public void resetMinimumTerritoryValueForProductionBonus() { m_minimumTerritoryValueForProductionBonus = -1; } @GameProperty(xmlProperty = true, gameProperty = true, adds = false) public void setRepairDiscount(final String value) throws GameParseException { final int v = getInt(value); if ((v != -1) && (v < 0 || v > 100)) { throw new GameParseException("m_repairDiscount must be -1 (no effect), or be between 0 and 100" + thisErrorMsg()); } m_repairDiscount = v; } @GameProperty(xmlProperty = true, gameProperty = true, adds = false) public void setRepairDiscount(final Integer value) { m_repairDiscount = value; } public int getRepairDiscount() { return m_repairDiscount; } public void resetRepairDiscount() { m_repairDiscount = -1; } @GameProperty(xmlProperty = true, gameProperty = true, adds = false) public void setWarBondDiceSides(final String value) throws GameParseException { final int v = getInt(value); if ((v != -1) && (v < 0 || v > 200)) { throw new GameParseException("warBondDiceSides must be -1 (no effect), or be between 0 and 200" + thisErrorMsg()); } m_warBondDiceSides = v; } @GameProperty(xmlProperty = true, gameProperty = true, adds = false) public void setWarBondDiceSides(final Integer value) { m_warBondDiceSides = value; } public int getWarBondDiceSides() { return m_warBondDiceSides; } public void resetWarBondDiceSides() { m_warBondDiceSides = -1; } @GameProperty(xmlProperty = true, gameProperty = true, adds = false) public void setWarBondDiceNumber(final String value) throws GameParseException { final int v = getInt(value); if (v < 0 || v > 100) { throw new GameParseException("warBondDiceNumber must be between 0 and 100" + thisErrorMsg()); } m_warBondDiceNumber = v; } @GameProperty(xmlProperty = true, gameProperty = true, adds = false) public void setWarBondDiceNumber(final Integer value) { m_warBondDiceNumber = value; } public int getWarBondDiceNumber() { return m_warBondDiceNumber; } public void resetWarBondDiceNumber() { m_warBondDiceNumber = 0; } /** * Adds to, not sets. Anything that adds to instead of setting needs a clear function as well. */ @GameProperty(xmlProperty = true, gameProperty = true, adds = true) public void setRocketDiceNumber(final String value) throws GameParseException { final String[] s = value.split(":"); if (s.length != 2) { throw new GameParseException("rocketDiceNumber must have two fields" + thisErrorMsg()); } String unitType; unitType = s[1]; // validate that this unit exists in the xml final UnitType ut = getData().getUnitTypeList().getUnitType(unitType); if (ut == null) { throw new GameParseException("No unit called:" + unitType + thisErrorMsg()); } // we should allow positive and negative numbers final int n = getInt(s[0]); m_rocketDiceNumber.put(ut, n); } @GameProperty(xmlProperty = true, gameProperty = true, adds = false) public void setRocketDiceNumber(final IntegerMap<UnitType> value) { m_rocketDiceNumber = value; } public IntegerMap<UnitType> getRocketDiceNumber() { return m_rocketDiceNumber; } public void clearRocketDiceNumber() { m_rocketDiceNumber.clear(); } public void resetRocketDiceNumber() { m_rocketDiceNumber = new IntegerMap<>(); } @GameProperty(xmlProperty = true, gameProperty = true, adds = false) public void setRocketDistance(final String value) throws GameParseException { final int v = getInt(value); if (v < 0 || v > 100) { throw new GameParseException("rocketDistance must be between 0 and 100" + thisErrorMsg()); } m_rocketDistance = v; } @GameProperty(xmlProperty = true, gameProperty = true, adds = false) public void setRocketDistance(final Integer value) { m_rocketDistance = value; } public int getRocketDistance() { return m_rocketDistance; } public void resetRocketDistance() { m_rocketDistance = 0; } @GameProperty(xmlProperty = true, gameProperty = true, adds = false) public void setRocketNumberPerTerritory(final String value) throws GameParseException { final int v = getInt(value); if (v < 0 || v > 200) { throw new GameParseException("rocketNumberPerTerritory must be between 0 and 200" + thisErrorMsg()); } m_rocketNumberPerTerritory = v; } @GameProperty(xmlProperty = true, gameProperty = true, adds = false) public void setRocketNumberPerTerritory(final int value) { m_rocketNumberPerTerritory = value; } public int getRocketNumberPerTerritory() { return m_rocketNumberPerTerritory; } public void resetRocketNumberPerTerritory() { m_rocketNumberPerTerritory = 0; } /** * Adds to, not sets. Anything that adds to instead of setting needs a clear function as well. */ @GameProperty(xmlProperty = true, gameProperty = true, adds = true) public void setUnitAbilitiesGained(final String value) throws GameParseException { final String[] s = value.split(":"); if (s.length < 2) { throw new GameParseException( "unitAbilitiesGained must list the unit type, then all abilities gained" + thisErrorMsg()); } String unitType; unitType = s[0]; // validate that this unit exists in the xml final UnitType ut = getData().getUnitTypeList().getUnitType(unitType); if (ut == null) { throw new GameParseException("No unit called:" + unitType + thisErrorMsg()); } HashSet<String> abilities = m_unitAbilitiesGained.get(ut); if (abilities == null) { abilities = new HashSet<>(); } // start at 1 for (int i = 1; i < s.length; i++) { final String ability = s[i]; if (!(ability.equals(ABILITY_CAN_BLITZ) || ability.equals(ABILITY_CAN_BOMBARD))) { throw new GameParseException("unitAbilitiesGained so far only supports: " + ABILITY_CAN_BLITZ + " and " + ABILITY_CAN_BOMBARD + thisErrorMsg()); } abilities.add(ability); } m_unitAbilitiesGained.put(ut, abilities); } @GameProperty(xmlProperty = true, gameProperty = true, adds = false) public void setUnitAbilitiesGained(final HashMap<UnitType, HashSet<String>> value) { m_unitAbilitiesGained = value; } public HashMap<UnitType, HashSet<String>> getUnitAbilitiesGained() { return m_unitAbilitiesGained; } public void clearUnitAbilitiesGained() { m_unitAbilitiesGained.clear(); } public void resetUnitAbilitiesGained() { m_unitAbilitiesGained = new HashMap<>(); } @GameProperty(xmlProperty = true, gameProperty = true, adds = false) public void setAirborneForces(final String value) { m_airborneForces = getBool(value); } @GameProperty(xmlProperty = true, gameProperty = true, adds = false) public void setAirborneForces(final Boolean value) { m_airborneForces = value; } public boolean getAirborneForces() { return m_airborneForces; } public void resetAirborneForces() { m_airborneForces = false; } /** * Adds to, not sets. Anything that adds to instead of setting needs a clear function as well. */ @GameProperty(xmlProperty = true, gameProperty = true, adds = true) public void setAirborneCapacity(final String value) throws GameParseException { final String[] s = value.split(":"); if (s.length <= 0 || s.length > 2) { throw new GameParseException("airborneCapacity cannot be empty or have more than two fields" + thisErrorMsg()); } String unitType; unitType = s[1]; // validate that this unit exists in the xml final UnitType ut = getData().getUnitTypeList().getUnitType(unitType); if (ut == null) { throw new GameParseException("No unit called:" + unitType + thisErrorMsg()); } // we should allow positive and negative numbers final int n = getInt(s[0]); m_airborneCapacity.put(ut, n); } @GameProperty(xmlProperty = true, gameProperty = true, adds = false) public void setAirborneCapacity(final IntegerMap<UnitType> value) { m_airborneCapacity = value; } public IntegerMap<UnitType> getAirborneCapacity() { return m_airborneCapacity; } public void clearAirborneCapacity() { m_airborneCapacity.clear(); } public void resetAirborneCapacity() { m_airborneCapacity = new IntegerMap<>(); } /** * Adds to, not sets. Anything that adds to instead of setting needs a clear function as well. */ @GameProperty(xmlProperty = true, gameProperty = true, adds = true) public void setAirborneTypes(final String value) throws GameParseException { final String[] s = value.split(":"); for (final String u : s) { final UnitType ut = getData().getUnitTypeList().getUnitType(u); if (ut == null) { throw new GameParseException("airborneTypes: no such unit type: " + u + thisErrorMsg()); } m_airborneTypes.add(ut); } } @GameProperty(xmlProperty = true, gameProperty = true, adds = false) public void setAirborneTypes(final HashSet<UnitType> value) { m_airborneTypes = value; } public HashSet<UnitType> getAirborneTypes() { return m_airborneTypes; } public void clearAirborneTypes() { m_airborneTypes.clear(); } public void resetAirborneTypes() { m_airborneTypes = new HashSet<>(); } @GameProperty(xmlProperty = true, gameProperty = true, adds = false) public void setAirborneDistance(final String value) throws GameParseException { final int v = getInt(value); if (v < 0 || v > 100) { throw new GameParseException("airborneDistance must be between 0 and 100" + thisErrorMsg()); } m_airborneDistance = v; } @GameProperty(xmlProperty = true, gameProperty = true, adds = false) public void setAirborneDistance(final Integer value) { m_airborneDistance = value; } public int getAirborneDistance() { return m_airborneDistance; } public void resetAirborneDistance() { m_airborneDistance = 0; } /** * Adds to, not sets. Anything that adds to instead of setting needs a clear function as well. */ @GameProperty(xmlProperty = true, gameProperty = true, adds = true) public void setAirborneBases(final String value) throws GameParseException { final String[] s = value.split(":"); for (final String u : s) { final UnitType ut = getData().getUnitTypeList().getUnitType(u); if (ut == null) { throw new GameParseException("airborneBases: no such unit type: " + u + thisErrorMsg()); } m_airborneBases.add(ut); } } @GameProperty(xmlProperty = true, gameProperty = true, adds = false) public void setAirborneBases(final HashSet<UnitType> value) { m_airborneBases = value; } public HashSet<UnitType> getAirborneBases() { return m_airborneBases; } public void clearAirborneBases() { m_airborneBases.clear(); } public void resetAirborneBases() { m_airborneBases = new HashSet<>(); } /** * Adds to, not sets. Anything that adds to instead of setting needs a clear function as well. */ @GameProperty(xmlProperty = true, gameProperty = true, adds = true) public void setAirborneTargettedByAA(final String value) throws GameParseException { final String[] s = value.split(":"); if (s.length < 2) { throw new GameParseException("airborneTargettedByAA must have at least two fields" + thisErrorMsg()); } final String aaType = s[0]; final HashSet<UnitType> unitTypes = new HashSet<>(); for (int i = 1; i < s.length; i++) { final UnitType ut = getData().getUnitTypeList().getUnitType(s[i]); if (ut == null) { throw new GameParseException("airborneTargettedByAA: no such unit type: " + s[i] + thisErrorMsg()); } unitTypes.add(ut); } m_airborneTargettedByAA.put(aaType, unitTypes); } @GameProperty(xmlProperty = true, gameProperty = true, adds = false) public void setAirborneTargettedByAA(final HashMap<String, HashSet<UnitType>> value) { m_airborneTargettedByAA = value; } public HashMap<String, HashSet<UnitType>> getAirborneTargettedByAA() { return m_airborneTargettedByAA; } public void clearAirborneTargettedByAA() { m_airborneTargettedByAA.clear(); } public void resetAirborneTargettedByAA() { m_airborneTargettedByAA = new HashMap<>(); } /** * Adds to, not sets. Anything that adds to instead of setting needs a clear function as well. */ @GameProperty(xmlProperty = true, gameProperty = true, adds = true) public void setAttackRollsBonus(final String value) throws GameParseException { final String[] s = value.split(":"); if (s.length <= 0 || s.length > 2) { throw new GameParseException("attackRollsBonus cannot be empty or have more than two fields" + thisErrorMsg()); } String unitType; unitType = s[1]; // validate that this unit exists in the xml final UnitType ut = getData().getUnitTypeList().getUnitType(unitType); if (ut == null) { throw new GameParseException("No unit called:" + unitType + thisErrorMsg()); } // we should allow positive and negative numbers final int n = getInt(s[0]); m_attackRollsBonus.put(ut, n); } @GameProperty(xmlProperty = true, gameProperty = true, adds = false) public void setAttackRollsBonus(final IntegerMap<UnitType> value) { m_attackRollsBonus = value; } public IntegerMap<UnitType> getAttackRollsBonus() { return m_attackRollsBonus; } public void clearAttackRollsBonus() { m_attackRollsBonus.clear(); } public void resetAttackRollsBonus() { m_attackRollsBonus = new IntegerMap<>(); } /** * Adds to, not sets. Anything that adds to instead of setting needs a clear function as well. */ @GameProperty(xmlProperty = true, gameProperty = true, adds = true) public void setDefenseRollsBonus(final String value) throws GameParseException { final String[] s = value.split(":"); if (s.length <= 0 || s.length > 2) { throw new GameParseException("defenseRollsBonus cannot be empty or have more than two fields" + thisErrorMsg()); } String unitType; unitType = s[1]; // validate that this unit exists in the xml final UnitType ut = getData().getUnitTypeList().getUnitType(unitType); if (ut == null) { throw new GameParseException("No unit called:" + unitType + thisErrorMsg()); } // we should allow positive and negative numbers final int n = getInt(s[0]); m_defenseRollsBonus.put(ut, n); } @GameProperty(xmlProperty = true, gameProperty = true, adds = false) public void setDefenseRollsBonus(final IntegerMap<UnitType> value) { m_defenseRollsBonus = value; } public IntegerMap<UnitType> getDefenseRollsBonus() { return m_defenseRollsBonus; } public void clearDefenseRollsBonus() { m_defenseRollsBonus.clear(); } public void resetDefenseRollsBonus() { m_defenseRollsBonus = new IntegerMap<>(); } /** * Adds to, not sets. Anything that adds to instead of setting needs a clear function as well. */ @GameProperty(xmlProperty = true, gameProperty = true, adds = true) public void setBombingBonus(final String value) throws GameParseException { final String[] s = value.split(":"); if (s.length <= 0 || s.length > 2) { throw new GameParseException("bombingBonus cannot be empty or have more than two fields" + thisErrorMsg()); } String unitType; unitType = s[1]; // validate that this unit exists in the xml final UnitType ut = getData().getUnitTypeList().getUnitType(unitType); if (ut == null) { throw new GameParseException("No unit called:" + unitType + thisErrorMsg()); } // we should allow positive and negative numbers final int n = getInt(s[0]); m_bombingBonus.put(ut, n); } @GameProperty(xmlProperty = true, gameProperty = true, adds = false) public void setBombingBonus(final IntegerMap<UnitType> value) { m_bombingBonus = value; } public IntegerMap<UnitType> getBombingBonus() { return m_bombingBonus; } public void clearBombingBonus() { m_bombingBonus.clear(); } public void resetBombingBonus() { m_bombingBonus = new IntegerMap<>(); } // Static Methods for interpreting data in attachments public static int getAttackBonus(final UnitType ut, final PlayerID player, final GameData data) { int rVal = 0; for (final TechAdvance ta : TechTracker.getCurrentTechAdvances(player, data)) { final TechAbilityAttachment taa = TechAbilityAttachment.get(ta); if (taa != null) { rVal += taa.getAttackBonus().getInt(ut); } } return rVal; } public static int getDefenseBonus(final UnitType ut, final PlayerID player, final GameData data) { int rVal = 0; for (final TechAdvance ta : TechTracker.getCurrentTechAdvances(player, data)) { final TechAbilityAttachment taa = TechAbilityAttachment.get(ta); if (taa != null) { rVal += taa.getDefenseBonus().getInt(ut); } } return rVal; } public static int getMovementBonus(final UnitType ut, final PlayerID player, final GameData data) { int rVal = 0; for (final TechAdvance ta : TechTracker.getCurrentTechAdvances(player, data)) { final TechAbilityAttachment taa = TechAbilityAttachment.get(ta); if (taa != null) { rVal += taa.getMovementBonus().getInt(ut); } } return rVal; } public static int getRadarBonus(final UnitType ut, final PlayerID player, final GameData data) { int rVal = 0; for (final TechAdvance ta : TechTracker.getCurrentTechAdvances(player, data)) { final TechAbilityAttachment taa = TechAbilityAttachment.get(ta); if (taa != null) { rVal += taa.getRadarBonus().getInt(ut); } } return rVal; } public static int getAirAttackBonus(final UnitType ut, final PlayerID player, final GameData data) { int rVal = 0; for (final TechAdvance ta : TechTracker.getCurrentTechAdvances(player, data)) { final TechAbilityAttachment taa = TechAbilityAttachment.get(ta); if (taa != null) { rVal += taa.getAirAttackBonus().getInt(ut); } } return rVal; } public static int getAirDefenseBonus(final UnitType ut, final PlayerID player, final GameData data) { int rVal = 0; for (final TechAdvance ta : TechTracker.getCurrentTechAdvances(player, data)) { final TechAbilityAttachment taa = TechAbilityAttachment.get(ta); if (taa != null) { rVal += taa.getAirDefenseBonus().getInt(ut); } } return rVal; } public static int getProductionBonus(final UnitType ut, final PlayerID player, final GameData data) { int rVal = 0; for (final TechAdvance ta : TechTracker.getCurrentTechAdvances(player, data)) { final TechAbilityAttachment taa = TechAbilityAttachment.get(ta); if (taa != null) { rVal += taa.getProductionBonus().getInt(ut); } } return rVal; } public static int getMinimumTerritoryValueForProductionBonus(final PlayerID player, final GameData data) { int rVal = -1; for (final TechAdvance ta : TechTracker.getCurrentTechAdvances(player, data)) { final TechAbilityAttachment taa = TechAbilityAttachment.get(ta); if (taa != null) { final int min = taa.getMinimumTerritoryValueForProductionBonus(); if (min == -1) { continue; } else if (rVal == -1 || min < rVal) { rVal = min; } } } return Math.max(0, rVal); } public static double getRepairDiscount(final PlayerID player, final GameData data) { double rVal = 1.0D; for (final TechAdvance ta : TechTracker.getCurrentTechAdvances(player, data)) { final TechAbilityAttachment taa = TechAbilityAttachment.get(ta); if (taa != null) { final int min = taa.getRepairDiscount(); if (min == -1) { continue; } else { double fmin = min; fmin = fmin / 100.0F; rVal -= fmin; } } } return Math.max(0.0D, rVal); } public static int getWarBondDiceSides(final PlayerID player, final GameData data) { int rVal = 0; for (final TechAdvance ta : TechTracker.getCurrentTechAdvances(player, data)) { final TechAbilityAttachment taa = TechAbilityAttachment.get(ta); if (taa != null) { final int sides = taa.getWarBondDiceSides(); if (sides > 0) { rVal += sides; } } } return Math.max(0, rVal); } public static int getWarBondDiceNumber(final PlayerID player, final GameData data) { int rVal = 0; for (final TechAdvance ta : TechTracker.getCurrentTechAdvances(player, data)) { final TechAbilityAttachment taa = TechAbilityAttachment.get(ta); if (taa != null) { final int number = taa.getWarBondDiceNumber(); if (number > 0) { rVal += number; } } } return Math.max(0, rVal); } private static int getRocketDiceNumber(final UnitType ut, final PlayerID player, final GameData data) { int rVal = 0; for (final TechAdvance ta : TechTracker.getCurrentTechAdvances(player, data)) { final TechAbilityAttachment taa = TechAbilityAttachment.get(ta); if (taa != null) { rVal += taa.getRocketDiceNumber().getInt(ut); } } return rVal; } public static int getRocketDiceNumber(final Collection<Unit> rockets, final GameData data) { int rVal = 0; for (final Unit u : rockets) { rVal += getRocketDiceNumber(u.getType(), u.getOwner(), data); } return rVal; } public static int getRocketDistance(final PlayerID player, final GameData data) { int rVal = 0; for (final TechAdvance ta : TechTracker.getCurrentTechAdvances(player, data)) { final TechAbilityAttachment taa = TechAbilityAttachment.get(ta); if (taa != null) { final int distance = taa.getRocketDistance(); if (distance > 0) { rVal += distance; } } } return Math.max(0, rVal); } public static int getRocketNumberPerTerritory(final PlayerID player, final GameData data) { int rVal = 0; for (final TechAdvance ta : TechTracker.getCurrentTechAdvances(player, data)) { final TechAbilityAttachment taa = TechAbilityAttachment.get(ta); if (taa != null) { final int number = taa.getRocketNumberPerTerritory(); if (number > 0) { rVal += number; } } } return Math.max(0, rVal); } private static HashSet<String> getUnitAbilitiesGained(final UnitType ut, final PlayerID player, final GameData data) { final HashSet<String> rVal = new HashSet<>(); for (final TechAdvance ta : TechTracker.getCurrentTechAdvances(player, data)) { final TechAbilityAttachment taa = TechAbilityAttachment.get(ta); if (taa != null) { final HashSet<String> abilities = taa.getUnitAbilitiesGained().get(ut); if (abilities != null) { rVal.addAll(abilities); } } } return rVal; } public static boolean getUnitAbilitiesGained(final String filterForAbility, final UnitType ut, final PlayerID player, final GameData data) { final HashSet<String> abilities = getUnitAbilitiesGained(ut, player, data); return abilities.contains(filterForAbility); } public static boolean getAllowAirborneForces(final PlayerID player, final GameData data) { for (final TechAdvance ta : TechTracker.getCurrentTechAdvances(player, data)) { final TechAbilityAttachment taa = TechAbilityAttachment.get(ta); if (taa != null) { if (taa.getAirborneForces()) { return true; } } } return false; } public static IntegerMap<UnitType> getAirborneCapacity(final PlayerID player, final GameData data) { final IntegerMap<UnitType> capacityMap = new IntegerMap<>(); for (final TechAdvance ta : TechTracker.getCurrentTechAdvances(player, data)) { final TechAbilityAttachment taa = TechAbilityAttachment.get(ta); if (taa != null) { capacityMap.add(taa.getAirborneCapacity()); } } return capacityMap; } public static int getAirborneCapacity(final Collection<Unit> units, final PlayerID player, final GameData data) { final IntegerMap<UnitType> capacityMap = getAirborneCapacity(player, data); int rVal = 0; for (final Unit u : units) { rVal += Math.max(0, (capacityMap.getInt(u.getType()) - ((TripleAUnit) u).getLaunched())); } return rVal; } public static Set<UnitType> getAirborneTypes(final PlayerID player, final GameData data) { final Set<UnitType> airborneUnits = new HashSet<>(); for (final TechAdvance ta : TechTracker.getCurrentTechAdvances(player, data)) { final TechAbilityAttachment taa = TechAbilityAttachment.get(ta); if (taa != null) { airborneUnits.addAll(taa.getAirborneTypes()); } } return airborneUnits; } public static int getAirborneDistance(final PlayerID player, final GameData data) { int rVal = 0; for (final TechAdvance ta : TechTracker.getCurrentTechAdvances(player, data)) { final TechAbilityAttachment taa = TechAbilityAttachment.get(ta); if (taa != null) { rVal += taa.getAirborneDistance(); } } return Math.max(0, rVal); } public static Set<UnitType> getAirborneBases(final PlayerID player, final GameData data) { final Set<UnitType> airborneBases = new HashSet<>(); for (final TechAdvance ta : TechTracker.getCurrentTechAdvances(player, data)) { final TechAbilityAttachment taa = TechAbilityAttachment.get(ta); if (taa != null) { airborneBases.addAll(taa.getAirborneBases()); } } return airborneBases; } public static HashMap<String, HashSet<UnitType>> getAirborneTargettedByAA(final PlayerID player, final GameData data) { final HashMap<String, HashSet<UnitType>> rVal = new HashMap<>(); for (final TechAdvance ta : TechTracker.getCurrentTechAdvances(player, data)) { final TechAbilityAttachment taa = TechAbilityAttachment.get(ta); if (taa != null) { final HashMap<String, HashSet<UnitType>> mapAA = taa.getAirborneTargettedByAA(); if (mapAA != null && !mapAA.isEmpty()) { for (final Entry<String, HashSet<UnitType>> entry : mapAA.entrySet()) { HashSet<UnitType> current = rVal.get(entry.getKey()); if (current == null) { current = new HashSet<>(); } current.addAll(entry.getValue()); rVal.put(entry.getKey(), current); } } } } return rVal; } public static int getAttackRollsBonus(final UnitType ut, final PlayerID player, final GameData data) { int rVal = 0; for (final TechAdvance ta : TechTracker.getCurrentTechAdvances(player, data)) { final TechAbilityAttachment taa = TechAbilityAttachment.get(ta); if (taa != null) { rVal += taa.getAttackRollsBonus().getInt(ut); } } return rVal; } public static int getDefenseRollsBonus(final UnitType ut, final PlayerID player, final GameData data) { int rVal = 0; for (final TechAdvance ta : TechTracker.getCurrentTechAdvances(player, data)) { final TechAbilityAttachment taa = TechAbilityAttachment.get(ta); if (taa != null) { rVal += taa.getDefenseRollsBonus().getInt(ut); } } return rVal; } public static int getBombingBonus(final UnitType ut, final PlayerID player, final GameData data) { int rVal = 0; for (final TechAdvance ta : TechTracker.getCurrentTechAdvances(player, data)) { final TechAbilityAttachment taa = TechAbilityAttachment.get(ta); if (taa != null) { rVal += taa.getBombingBonus().getInt(ut); } } return rVal; } /** * Must be done only in GameParser, and only after we have already parsed ALL technologies, attachments, and game * options/properties. */ @InternalDoNotExport public static void setDefaultTechnologyAttachments(final GameData data) throws GameParseException { // loop through all technologies. any "default/hard-coded" tech that doesn't have an attachment, will get its // "default" attachment. any // non-default tech are ignored. for (final TechAdvance techAdvance : TechAdvance.getTechAdvances(data)) { final TechAdvance ta; if (techAdvance instanceof GenericTechAdvance) { final TechAdvance adv = ((GenericTechAdvance) techAdvance).getAdvance(); if (adv != null) { ta = adv; } else { continue; } } else { ta = techAdvance; } final String propertyString = ta.getProperty(); TechAbilityAttachment taa = TechAbilityAttachment.get(ta); if (taa == null) { // debating if we should have flags for things like "air", "land", "sea", "aaGun", "factory", "strategic // bomber", etc. // perhaps just the easy ones, of air, land, and sea? if (propertyString.equals(TechAdvance.TECH_PROPERTY_LONG_RANGE_AIRCRAFT)) { taa = new TechAbilityAttachment(Constants.TECH_ABILITY_ATTACHMENT_NAME, ta, data); ta.addAttachment(Constants.TECH_ABILITY_ATTACHMENT_NAME, taa); final List<UnitType> allAir = Match.getMatches(data.getUnitTypeList().getAllUnitTypes(), Matches.UnitTypeIsAir); for (final UnitType air : allAir) { taa.setMovementBonus("2:" + air.getName()); } } else if (propertyString.equals(TechAdvance.TECH_PROPERTY_AA_RADAR)) { taa = new TechAbilityAttachment(Constants.TECH_ABILITY_ATTACHMENT_NAME, ta, data); ta.addAttachment(Constants.TECH_ABILITY_ATTACHMENT_NAME, taa); final List<UnitType> allAA = Match.getMatches(data.getUnitTypeList().getAllUnitTypes(), Matches.UnitTypeIsAAforAnything); for (final UnitType aa : allAA) { taa.setRadarBonus("1:" + aa.getName()); } } else if (propertyString.equals(TechAdvance.TECH_PROPERTY_SUPER_SUBS)) { taa = new TechAbilityAttachment(Constants.TECH_ABILITY_ATTACHMENT_NAME, ta, data); ta.addAttachment(Constants.TECH_ABILITY_ATTACHMENT_NAME, taa); final List<UnitType> allSubs = Match.getMatches(data.getUnitTypeList().getAllUnitTypes(), Matches.UnitTypeIsSub); for (final UnitType sub : allSubs) { taa.setAttackBonus("1:" + sub.getName()); } } else if (propertyString.equals(TechAdvance.TECH_PROPERTY_JET_POWER)) { taa = new TechAbilityAttachment(Constants.TECH_ABILITY_ATTACHMENT_NAME, ta, data); ta.addAttachment(Constants.TECH_ABILITY_ATTACHMENT_NAME, taa); final List<UnitType> allJets = Match.getMatches(data.getUnitTypeList().getAllUnitTypes(), new CompositeMatchAnd<>(Matches.UnitTypeIsAir, Matches.UnitTypeIsStrategicBomber.invert())); final boolean ww2v3TechModel = games.strategy.triplea.Properties.getWW2V3TechModel(data); for (final UnitType jet : allJets) { if (ww2v3TechModel) { taa.setAttackBonus("1:" + jet.getName()); } else { taa.setDefenseBonus("1:" + jet.getName()); } } } else if (propertyString.equals(TechAdvance.TECH_PROPERTY_INCREASED_FACTORY_PRODUCTION)) { taa = new TechAbilityAttachment(Constants.TECH_ABILITY_ATTACHMENT_NAME, ta, data); ta.addAttachment(Constants.TECH_ABILITY_ATTACHMENT_NAME, taa); final List<UnitType> allFactories = Match.getMatches(data.getUnitTypeList().getAllUnitTypes(), Matches.UnitTypeCanProduceUnits); for (final UnitType factory : allFactories) { taa.setProductionBonus("2:" + factory.getName()); taa.setMinimumTerritoryValueForProductionBonus("3"); // means a 50% discount, which is half price taa.setRepairDiscount("50"); } } else if (propertyString.equals(TechAdvance.TECH_PROPERTY_WAR_BONDS)) { taa = new TechAbilityAttachment(Constants.TECH_ABILITY_ATTACHMENT_NAME, ta, data); ta.addAttachment(Constants.TECH_ABILITY_ATTACHMENT_NAME, taa); taa.setWarBondDiceSides(Integer.toString(data.getDiceSides())); taa.setWarBondDiceNumber("1"); } else if (propertyString.equals(TechAdvance.TECH_PROPERTY_ROCKETS)) { taa = new TechAbilityAttachment(Constants.TECH_ABILITY_ATTACHMENT_NAME, ta, data); ta.addAttachment(Constants.TECH_ABILITY_ATTACHMENT_NAME, taa); final List<UnitType> allRockets = Match.getMatches(data.getUnitTypeList().getAllUnitTypes(), Matches.UnitTypeIsRocket); for (final UnitType rocket : allRockets) { taa.setRocketDiceNumber("1:" + rocket.getName()); } // taa.setRocketDiceSides(Integer.toString(data.getDiceSides())); taa.setRocketDistance("3"); taa.setRocketNumberPerTerritory("1"); } else if (propertyString.equals(TechAdvance.TECH_PROPERTY_DESTROYER_BOMBARD)) { taa = new TechAbilityAttachment(Constants.TECH_ABILITY_ATTACHMENT_NAME, ta, data); ta.addAttachment(Constants.TECH_ABILITY_ATTACHMENT_NAME, taa); final List<UnitType> allDestroyers = Match.getMatches(data.getUnitTypeList().getAllUnitTypes(), Matches.UnitTypeIsDestroyer); for (final UnitType destroyer : allDestroyers) { taa.setUnitAbilitiesGained(destroyer.getName() + ":" + ABILITY_CAN_BOMBARD); } } else if (propertyString.equals(TechAdvance.TECH_PROPERTY_HEAVY_BOMBER)) { taa = new TechAbilityAttachment(Constants.TECH_ABILITY_ATTACHMENT_NAME, ta, data); ta.addAttachment(Constants.TECH_ABILITY_ATTACHMENT_NAME, taa); final List<UnitType> allBombers = Match.getMatches(data.getUnitTypeList().getAllUnitTypes(), Matches.UnitTypeIsStrategicBomber); final int heavyBomberDiceRollsTotal = games.strategy.triplea.Properties.getHeavyBomberDiceRolls(data); final boolean heavyBombersLHTR = games.strategy.triplea.Properties.getLHTR_Heavy_Bombers(data); for (final UnitType bomber : allBombers) { // TODO: The bomber dice rolls get set when the xml is parsed. // we subtract the base rolls to get the bonus final int heavyBomberDiceRollsBonus = heavyBomberDiceRollsTotal - UnitAttachment.get(bomber).getAttackRolls(PlayerID.NULL_PLAYERID); taa.setAttackRollsBonus(heavyBomberDiceRollsBonus + ":" + bomber.getName()); if (heavyBombersLHTR) { // TODO: this all happens WHEN the xml is parsed. Which means if the user changes the game options, this // does not get changed. // (meaning, turning on LHTR bombers will not result in this bonus damage, etc. It would have to start on, // in the xml.) taa.setDefenseRollsBonus(heavyBomberDiceRollsBonus + ":" + bomber.getName()); // LHTR adds 1 to base roll taa.setBombingBonus("1:" + bomber.getName()); } } } // The following technologies should NOT have ability attachments for them: // shipyards and industrialTechnology = because it is better to use a Trigger to change player's production // improvedArtillerySupport = because it is already completely atomized and controlled through support // attachments // paratroopers = because it is already completely atomized and controlled through unit attachments + game // options // mechanizedInfantry = because it is already completely atomized and controlled through unit attachments // IF one of the above named techs changes what it does in a future version of a&a, and the change is large // enough or different // enough that it cannot be done easily with a new game option, // then it is better to create a new tech rather than change the old one, and give the new one a new name, like // paratroopers2 or // paratroopersAttack or Airborne_Forces, or some crap. } } } // validator @Override public void validate(final GameData data) throws GameParseException { final TechAdvance ta = (TechAdvance) this.getAttachedTo(); if (ta instanceof GenericTechAdvance) { final TechAdvance hardCodedAdvance = ((GenericTechAdvance) ta).getAdvance(); if (hardCodedAdvance != null) { throw new GameParseException( "A custom Generic Tech Advance naming a hardcoded tech, may not have a Tech Ability Attachment!" + this.thisErrorMsg()); } } } }