package onlinefrontlines.game; import java.util.*; import java.awt.Point; import onlinefrontlines.utils.Tools; /** * State of an active unit in the game * * @author jorrit * * Copyright (C) 2009-2013 Jorrit Rouwe * * This file is part of Online Frontlines. * * Online Frontlines is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Online Frontlines is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Online Frontlines. If not, see <http://www.gnu.org/licenses/>. */ public final class UnitState { // State public int id; public UnitConfig unitConfig; public UnitConfig initialUnitConfig; public int locationX; public int locationY; public final Faction initialFaction; public Faction faction; public int armour; public int ammo; public int movementPointsLeft = 0; public int actionsLeft = 0; public boolean lastActionWasMove = false; public ArrayList<UnitState> containedUnits = new ArrayList<UnitState>(); public UnitState container = null; private boolean detectedByEnemy = false; private boolean signalDetectionLost = false; // Flag to indicate that detectedByEnemy needs to be set to false again public boolean identifiedByEnemy = false; /** * Constructor */ public UnitState(int id, int locationX, int locationY, Faction faction, UnitConfig unitConfig) { // Fill in properties this.id = id; this.locationX = locationX; this.locationY = locationY; this.initialFaction = faction; this.faction = faction; this.unitConfig = unitConfig; this.initialUnitConfig = unitConfig; this.armour = unitConfig.maxArmour; this.ammo = unitConfig.maxAmmo; } /** * Save state of a unit */ public UnitState saveState() { UnitState u = new UnitState(id, locationX, locationY, faction, unitConfig); u.armour = armour; u.ammo = ammo; u.movementPointsLeft = movementPointsLeft; u.actionsLeft = actionsLeft; u.lastActionWasMove = lastActionWasMove; u.container = container; u.containedUnits.clear(); for (UnitState c : containedUnits) u.containedUnits.add(c); return u; } /** * Restore state of a unit */ public void restoreState(UnitState s) { id = s.id; unitConfig = s.unitConfig; locationX = s.locationX; locationY = s.locationY; faction = s.faction; armour = s.armour; ammo = s.ammo; movementPointsLeft = s.movementPointsLeft; actionsLeft = s.actionsLeft; lastActionWasMove = s.lastActionWasMove; container = s.container; containedUnits.clear(); for (UnitState c : s.containedUnits) containedUnits.add(c); } /** * Get location */ public Point getLocation() { return new Point(locationX, locationY); } /** * Get distance to specified location */ public int getDistanceTo(int x, int y) { return MapConfig.getDistance(locationX, locationY, x, y); } /** * Transform to other unit type */ public void transform() { // Get new config UnitConfig config = UnitConfig.allUnitsMap.get(unitConfig.transformableToUnitId); // Transform armour = (config.maxArmour * armour) / unitConfig.maxArmour; unitConfig = config; } /** * Check if a unit can be contained in this unit */ public boolean canHold(UnitConfig config) { return container == null && unitConfig.containerUnitIds.contains(config.id) && unitConfig.containerMaxUnits > 0 && containedUnits.size() < unitConfig.containerMaxUnits; } /** * Check if a unit can be contained in this unit */ public boolean canHold(UnitState unit) { return unit.faction == faction && unit.container == null && canHold(unit.unitConfig); } /** * Add unit as a contained unit */ public void addUnit(UnitState unit) { // Add to container containedUnits.add(unit); unit.container = this; unit.locationX = -1000; unit.locationY = -1000; if (!detectedByEnemy || !identifiedByEnemy) unit.setDetectedRecursive(false, Integer.MAX_VALUE); // Transform unit if needed UnitConfig otherConfig = UnitConfig.allUnitsMap.get(unit.unitConfig.transformableToUnitId); if (unit.unitConfig.transformableType == TransformableType.inBase && unitConfig.containerUnitIds.contains(otherConfig.id)) unit.transform(); } /** * Remove unit as contained unit */ public void removeUnit(UnitState unit) { // Remove from array assert(unit.container == this); assert(containedUnits.contains(unit)); containedUnits.remove(unit); unit.container = null; } /** * Check if this unit can attack another unit of type unitClass */ public boolean canAttackUnitClass(UnitClass unitClass, Faction unitFaction) { UnitStrengthProperties sp = unitConfig.getStrengthProperties(unitClass); return faction != unitFaction && armour > 0 && sp.getStrength(ammo > 0) > 0; } /** * Check if the unit can attack another unit */ public boolean canAttack(UnitState unit) { return canAttackFrom(unit.getDistanceTo(locationX, locationY), unit); } /** * Check if the unit can counter attack another unit */ public boolean canCounterAttack(UnitState unit) { return canCounterAttackFrom(unit.getDistanceTo(locationX, locationY), unit); } /** * Check if the unit can attack another unit from position x, y */ public boolean canAttackFrom(int distance, UnitState unit) { return actionsLeft > 0 && canCounterAttackFrom(distance, unit); } /** * Check if the unit can counter attack another unit from position x, y */ public boolean canCounterAttackFrom(int distance, UnitState unit) { return container == null && unit.container == null && canAttackUnitClass(unit.unitConfig.unitClass, unit.faction) && distance <= unitConfig.getStrengthProperties(unit.unitConfig.unitClass).attackRange; } /** * Determine the max armour loss an attack could have on unit of class unitClass */ public double getMaxArmourLossForAttack(UnitClass unitClass) { // Check if unit has armour if (unitConfig.maxArmour <= 0) return 0; // Get strength properties UnitStrengthProperties sp = unitConfig.getStrengthProperties(unitClass); // Get strength double strength = ((double)sp.getStrength(ammo > 0)) * armour / unitConfig.maxArmour; if (strength <= 0) return 0; // Calculate maximum armour loss for target double maxArmourLoss = Math.max(strength, 3.0); // Get max loss return maxArmourLoss; } /** * Determine terrain modifier for attack */ public double getTerrainModifierForAttack(TerrainConfig attackerTerrain, TerrainConfig defenderTerrain) { // Get terrain modifier multiplier return 1.0 + ((double)attackerTerrain.strengthModifier - defenderTerrain.strengthModifier) / 100.0; } /** * Average loss */ public double getAverageArmourLossForAttack(UnitClass unitClass, TerrainConfig attackerTerrain, TerrainConfig defenderTerrain) { // Get attack properties double maxArmourLoss = getMaxArmourLossForAttack(unitClass); double terrainModifier = getTerrainModifierForAttack(attackerTerrain, defenderTerrain); // Get uniformly distributed random number return 0.5 * terrainModifier * maxArmourLoss; } /** * Execute an attack from this unit to another unit (call again with parameters swapped to perform the counter attack) */ public void attack(UnitState unit, TerrainConfig attackerTerrain, TerrainConfig defenderTerrain, Random random) { // Check if other unit alive if (unit.armour <= 0) return; // Get attack properties double maxArmourLoss = getMaxArmourLossForAttack(unit.unitConfig.unitClass); if (maxArmourLoss == 0) return; double terrainModifier = getTerrainModifierForAttack(attackerTerrain, defenderTerrain); // Get uniformly distributed random number double average = 0.5 * terrainModifier * maxArmourLoss; double standardDeviation = 0.15 * maxArmourLoss; double rnd = Tools.gaussianRandom(average, standardDeviation, random); // Calculate loss int armourLoss = (int)Math.min(Math.max(Math.ceil(rnd), 0), maxArmourLoss); // Subtract loss unit.armour -= armourLoss; if (unit.armour <= 0) { if (!unit.unitConfig.isBase) { // This is not a base so it's broken now unit.armour = 0; } else { // Bases change faction when destroyed unit.faction = faction; unit.armour = unit.unitConfig.maxArmour; } } // Decrease ammo UnitStrengthProperties sp = unitConfig.getStrengthProperties(unit.unitConfig.unitClass); if (sp.maxStrengthWithAmmo > 0) { --ammo; if (ammo < 0) ammo = 0; } } /** * Execute an attack from this unit to another unit, then perform the counter attack */ public void attackAndCounter(UnitState unit, TerrainConfig attackerTerrain, TerrainConfig defenderTerrain, Random random) { // Attack the unit attack(unit, attackerTerrain, defenderTerrain, random); // Counter attack if (unit.armour > 0 && unit.canCounterAttack(this)) unit.attack(this, defenderTerrain, attackerTerrain, random); } /** * Recursively identify unit */ public void identifyUnitRecursive() { // Identify this unit setDetected(true); identifiedByEnemy = true; // Identify children for (UnitState u : containedUnits) u.identifyUnitRecursive(); } /** * Comparator for sorting units on their id's */ private static class SortOnId implements Comparator<UnitState> { public int compare(UnitState u1, UnitState u2) { return u2.id - u1.id; } } /** * Dump the state of the game */ public void dumpState(StringBuilder b, Faction localFaction, int level) { if (!detectedByEnemy && localFaction == Faction.opposite(faction)) return; for (int i = 0; i < level; ++i) b.append(" "); if (!identifiedByEnemy && localFaction == Faction.opposite(faction)) { b.append("id: "); b.append(id); b.append(", configId: unknown, locationX: "); b.append(locationX); b.append(", locationY: " + locationY); b.append(", faction: "); b.append(Faction.toInt(faction)); b.append("\n"); } else { b.append("id: "); b.append(id); b.append(", configId: "); b.append(unitConfig.id); b.append(", locationX: "); b.append(locationX); b.append(", locationY: " + locationY); b.append(", faction: "); b.append(Faction.toInt(faction)); b.append(", armour: "); b.append(armour); b.append(", ammo: "); b.append(ammo); b.append(", movementPointsLeft: "); b.append(movementPointsLeft); b.append(", actionsLeft: "); b.append(actionsLeft); b.append(", lastActionWasMove: "); b.append(lastActionWasMove); b.append("\n"); } // Sort contained units UnitState[] c = containedUnits.toArray(new UnitState[0]); Arrays.sort(c, new SortOnId()); // Dump contained units for (UnitState u : c) u.dumpState(b, localFaction, level + 1); } /** * Get number of containers this unit is in */ public int getNumContainers() { int num = 0; for (UnitState unit = container; unit != null; unit = unit.container) ++num; return num; } /** * Update detection state of unit */ public void setDetected(boolean detected) { // Update detection state if (detected) { detectedByEnemy = true; signalDetectionLost = false; } else { signalDetectionLost = detectedByEnemy; } } /** * Recursively update detection state of unit */ public void setDetectedRecursive(boolean detected, int maxDepth) { // Detect unit setDetected(detected); // Limit depth --maxDepth; if (maxDepth <= 0) return; // Recursively update children for (UnitState u : containedUnits) u.setDetectedRecursive(detected, maxDepth); } /** * Check if unit is currently detected by enemy */ public boolean isDetected() { return detectedByEnemy; } /** * Check if detection of unit has been lost since last action */ public boolean checkDetectionLost() { return signalDetectionLost; } /** * Confirm detection lost, reset internal state */ public void confirmDetectionLost() { detectedByEnemy = false; signalDetectionLost = false; identifiedByEnemy = false; } /** * Get total contained victory points */ public int getTotalVictoryPoints() { int points = unitConfig.victoryPoints; for (UnitState u : containedUnits) points += u.getTotalVictoryPoints(); return points; } }