// War // --- // Attacks and defends from enemy attacks during situations of war. // import empire_ai.weasel.WeaselAI; import empire_ai.weasel.Intelligence; import empire_ai.weasel.Relations; import empire_ai.weasel.Fleets; import empire_ai.weasel.Systems; import empire_ai.weasel.Movement; import empire_ai.weasel.Scouting; import empire_ai.weasel.Military; import empire_ai.weasel.searches; import regions.regions; class BattleMission : Mission { Region@ battleIn; FleetAI@ fleet; MoveOrder@ move; Object@ defending; Planet@ capturing; Empire@ captureFrom; bool arrived = false; void save(Fleets& fleets, SaveFile& file) override { file << battleIn; fleets.saveAI(file, fleet); fleets.movement.saveMoveOrder(file, move); file << defending; file << capturing; file << captureFrom; file << arrived; } void load(Fleets& fleets, SaveFile& file) override { file >> battleIn; @fleet = fleets.loadAI(file); @move = fleets.movement.loadMoveOrder(file); file >> defending; file >> capturing; file >> captureFrom; file >> arrived; } }; double captureSupply(Empire& emp, Object& check) { double loy = check.getLoyaltyFacing(emp); double cost = config::SIEGE_LOYALTY_SUPPLY_COST * loy; cost *= emp.CaptureSupplyFactor; cost *= check.owner.CaptureSupplyDifficulty; return cost; } class Battle { SystemAI@ system; Region@ staging; array fleets; uint curPriority = MiP_Critical; bool isAttack = false; double enemyStrength; double ourStrength; double lastCombat = 0; double bestCapturePct; double lastHadFleets = 0; bool inCombat = false; bool isUnderSiege = false; Planet@ defendPlanet; Object@ eliminate; Battle() { lastHadFleets = gameTime; lastCombat = gameTime; } void save(War& war, SaveFile& file) { war.systems.saveAI(file, system); uint cnt = fleets.length; file << cnt; for(uint i = 0; i < cnt; ++i) war.fleets.saveMission(file, fleets[i]); file << curPriority; file << isAttack; file << lastCombat; file << inCombat; file << defendPlanet; file << eliminate; file << isUnderSiege; file << bestCapturePct; } void load(War& war, SaveFile& file) { @system = war.systems.loadAI(file); uint cnt = 0; file >> cnt; for(uint i = 0; i < cnt; ++i) { auto@ miss = cast(war.fleets.loadMission(file)); if(miss !is null) fleets.insertLast(miss); } file >> curPriority; file >> isAttack; file >> lastCombat; file >> inCombat; file >> defendPlanet; file >> eliminate; file >> isUnderSiege; file >> bestCapturePct; } BattleMission@ join(AI& ai, War& war, FleetAI@ flAI) { BattleMission mission; @mission.fleet = flAI; @mission.battleIn = system.obj; mission.priority = curPriority; Object@ moveTo = system.obj; if(defendPlanet !is null) @moveTo = defendPlanet; else if(eliminate !is null && eliminate.isShip) @moveTo = eliminate; @mission.move = war.movement.move(flAI.obj, moveTo, MP_Critical, spread=true, nearOnly=true); //Station this fleet nearby after the battle is over if(staging !is null) war.military.stationFleet(flAI, staging); if(war.log) ai.print("Assign to battle at "+system.obj.name +" for strength "+standardize(ourStrength * 0.001, true) + " vs their "+standardize(enemyStrength * 0.001, true), flAI.obj); fleets.insertLast(mission); war.fleets.performMission(flAI, mission); return mission; } bool stayingHere(Object@ other) { if(other is null || !other.hasMover) return true; if(!inRegion(system.obj, other.position)) return false; double acc = other.maxAcceleration; if(acc <= 0.0001) return true; vec3d compDest = other.computedDestination; if(inRegion(system.obj, compDest)) return true; if(inRegion(system.obj, other.position + other.velocity * 10.0)) return true; return false; } bool tick(AI& ai, War& war, double time) { //Compute strength values enemyStrength = getTotalFleetStrength(system.obj, ai.enemyMask, planets=true); ourStrength = 0; for(uint i = 0, cnt = fleets.length; i < cnt; ++i) ourStrength += sqrt(fleets[i].fleet.strength); ourStrength *= ourStrength; inCombat = false; bool ourPlanetsPresent = system.obj.PlanetsMask & (ai.allyMask | ai.mask) != 0; if((enemyStrength < 0.01 || !ourPlanetsPresent) && defendPlanet is null) isUnderSiege = false; //Remove lost fleets bool anyArrived = false; bestCapturePct = 0.0; for(uint i = 0, cnt = fleets.length; i < cnt; ++i) { auto@ miss = fleets[i]; miss.priority = curPriority; if(!miss.fleet.obj.valid || miss.canceled) { if(!miss.fleet.obj.valid) { for(uint i = 0, cnt = getEmpireCount(); i < cnt; ++i) { Empire@ other = getEmpire(i); if(!other.major || !ai.empire.isHostile(other)) continue; if(system.obj.ContestedMask & other.mask != 0) war.relations.recordLostTo(other, miss.fleet.obj); } } miss.canceled = true; if(war.log) ai.print("BATTLE: lost fleet "+miss.fleet.obj.name, system.obj); fleets.removeAt(i); --i; --cnt; if(fleets.length == 0) lastHadFleets = gameTime; continue; } if(miss.move !is null) { if(miss.move.failed) { miss.canceled = true; if(war.log) ai.print("BATTLE: move failed on lost fleet "+miss.fleet.obj.name, system.obj); fleets.removeAt(i); --i; --cnt; if(fleets.length == 0) lastHadFleets = gameTime; continue; } if(miss.move.completed) { miss.arrived = true; @miss.move = null; } } if(miss.arrived) { anyArrived = true; bool shouldRetreat = false; if(miss.fleet.supplies < 0.05) { if(isCapturingAny && eliminate is null) shouldRetreat = true; else if(ourStrength < enemyStrength * 0.75) shouldRetreat = true; } if(miss.fleet.fleetHealth < 0.25) { if(ourStrength < enemyStrength * 0.5) shouldRetreat = true; } if(shouldRetreat) { war.fleets.returnToBase(miss.fleet); fleets.removeAt(i); miss.canceled = true; --i; --cnt; if(fleets.length == 0) lastHadFleets = gameTime; continue; } } if(miss.capturing !is null) bestCapturePct = max(bestCapturePct, miss.capturing.capturePct); } //Defend our planets if(defendPlanet is null) { Planet@ defPl; double bestWeight = 0.0; for(uint i = 0, cnt = system.planets.length; i < cnt; ++i) { Planet@ pl = system.planets[i]; double w = 1.0; if(pl.owner is ai.empire) w *= 2.0; else if(pl.owner.mask & ai.allyMask != 0) w *= 0.5; else continue; double capt = pl.capturePct; if(capt <= 0.01) continue; w *= capt; if(!pl.enemiesInOrbit) continue; if(w > bestWeight) { bestWeight = w; @defPl = pl; } } if(defPl !is null) { if(war.log) ai.print("BATTLE: protect planet "+defPl.name, system.obj); @defendPlanet = defPl; for(uint i = 0, cnt = fleets.length; i < cnt; ++i) moveTo(fleets[i], defendPlanet, force=true); } } else { //Check if there are still enemies in orbit if(!defendPlanet.enemiesInOrbit || !defendPlanet.valid || defendPlanet.owner.isHostile(ai.empire)) @defendPlanet = null; } if(defendPlanet !is null) { //Make sure one fleet is in orbit inCombat = true; isUnderSiege = true; for(uint i = 0, cnt = fleets.length; i < cnt; ++i) moveTo(fleets[i], defendPlanet); } //Eliminate any remaining threats if(!inCombat) { //Eliminate any hostile targets in the system if(eliminate !is null) { //Make sure this is still a valid target to eliminate bool valid = true; if(!eliminate.valid) { valid = false; war.relations.recordTakenFrom(eliminate.owner, eliminate); } else if(!stayingHere(eliminate)) valid = false; else if(!eliminate.isVisibleTo(ai.empire)) valid = false; else if(!ai.empire.isHostile(eliminate.owner)) valid = false; if(!valid) { @eliminate = null; clearOrders(); } else { for(uint i = 0, cnt = fleets.length; i < cnt; ++i) attack(fleets[i], eliminate); inCombat = true; } } else { //Find a new target to eliminate Object@ check = findEnemy(system.obj, ai.empire, ai.enemyMask); if(check !is null) { if(stayingHere(check)) { @eliminate = check; for(uint i = 0, cnt = fleets.length; i < cnt; ++i) { auto@ obj = fleets[i].fleet.obj; if(!fleets[i].arrived) continue; obj.addAttackOrder(eliminate); } if(war.log) ai.print("BATTLE: Eliminate "+eliminate.name, system.obj); inCombat = true; for(uint i = 0, cnt = fleets.length; i < cnt; ++i) attack(fleets[i], eliminate, force=true); } } } } else { @eliminate = null; } //Capture enemy planets if possible //TODO: Respond to defense by abandoning all but 1 capture and swarming around the best one if(!inCombat) { for(uint i = 0, cnt = fleets.length; i < cnt; ++i) { auto@ miss = fleets[i]; if(miss.capturing !is null) { if(canCapture(ai, miss, miss.capturing) && miss.fleet.remainingSupplies > captureSupply(ai.empire, miss.capturing) && miss.fleet.obj.hasOrders) { inCombat = true; continue; } else { if(miss.capturing.owner is ai.empire && miss.captureFrom !is null) war.relations.recordTakenFrom(miss.captureFrom, miss.capturing); @miss.capturing = null; @miss.captureFrom = null; } } if(!miss.arrived) continue; Planet@ bestCapture; double totalWeight = 0; for(uint i = 0, cnt = system.planets.length; i < cnt; ++i) { Planet@ check = system.planets[i]; if(!canCapture(ai, miss, check)) continue; //Don't send two fleets to the same thing if(isCapturing(check)) continue; //Make sure we have the supplies remaining to capture if(miss.fleet.remainingSupplies < captureSupply(ai.empire, check) * ai.behavior.captureSupplyEstimate) continue; double str = check.getFleetStrength(); double w = 1.0; w *= check.getLoyaltyFacing(ai.empire); if(str != 0) w /= str; totalWeight += w; if(randomd() < w / totalWeight) @bestCapture = check; } if(bestCapture !is null) { if(war.log) ai.print("BATTLE: Capture "+bestCapture.name+" with "+miss.fleet.obj.name, system.obj); @miss.capturing = bestCapture; @miss.captureFrom = bestCapture.owner; miss.fleet.obj.addCaptureOrder(bestCapture); inCombat = true; } } } else { for(uint i = 0, cnt = fleets.length; i < cnt; ++i) { auto@ miss = fleets[i]; @miss.capturing = null; @miss.captureFrom = null; } } //Keep fleets here in non-critical mode for a few minutes if(!inCombat && (anyArrived || !isAttack)) { //TODO: Don't start this countdown until we've actually arrived if(gameTime > lastCombat + 90.0) { for(uint i = 0, cnt = fleets.length; i < cnt; ++i) fleets[i].completed = true; if(war.log) ai.print("BATTLE: ended", system.obj); return false; } else if(gameTime > lastCombat + 30.0) { curPriority = MiP_Normal; } else { curPriority = MiP_High; } } else { if(ourPlanetsPresent && isUnderSiege) { curPriority = MiP_Critical; if(war.winnability(this) < 0.5) curPriority = MiP_High; } else if(bestCapturePct > 0.75) curPriority = MiP_Critical; else curPriority = MiP_High; lastCombat = gameTime; } //If needed, claim fleets if(ourStrength < enemyStrength * ai.behavior.battleStrengthOverkill) { FleetAI@ claim; double bestWeight = 0; for(uint i = 0, cnt = war.fleets.fleets.length; i < cnt; ++i) { auto@ fleet = war.fleets.fleets[i]; double w = war.assignable(this, fleet); if(w > bestWeight) { bestWeight = w; @claim = fleet; } } if(claim !is null) join(ai, war, claim); } //Give up the battle when we should if(fleets.length == 0) { if(!ourPlanetsPresent && !isAttack) { //We lost all our planets before we could respond with anything. // We might be able to use an attack to claim it back later, but for now we just give up on it. if(war.log) ai.print("BATTLE: aborted defense, no fleets and no planets left", system.obj); for(uint i = 0, cnt = fleets.length; i < cnt; ++i) fleets[i].canceled = true; return false; } if(isAttack) { //We haven't been able to find any fleets to assign here for a while, //so just abort the attack if(gameTime - lastHadFleets > 60.0) { if(war.log) ai.print("BATTLE: aborted attack, no fleets available", system.obj); for(uint i = 0, cnt = fleets.length; i < cnt; ++i) fleets[i].canceled = true; return false; } } } return true; } void clearOrders() { for(uint i = 0, cnt = fleets.length; i < cnt; ++i) { auto@ obj = fleets[i].fleet.obj; if(!fleets[i].arrived) continue; if(obj.hasOrders) obj.clearOrders(); } } bool canCapture(AI& ai, BattleMission@ miss, Planet@ check) { if(!ai.empire.isHostile(check.owner)) return false; //TODO: Wait around a while maybe? if(check.isProtected(ai.empire)) return false; return true; } void moveTo(BattleMission@ miss, Planet@ defPl, bool force = false) { if(!miss.arrived) return; if(!force) { if(miss.fleet.obj.hasOrders) return; double dist = miss.fleet.obj.position.distanceTo(defPl.position); if(dist < defPl.OrbitSize) return; } vec3d pos = defPl.position; vec2d offset = random2d(defPl.OrbitSize * 0.85); pos.x += offset.x; pos.z += offset.y; miss.fleet.obj.addMoveOrder(pos); } void attack(BattleMission@ miss, Object@ target, bool force = false) { //TODO: make this not chase stuff out of the system like a madman? // (in attack logic as well) if(!miss.arrived) return; if(!force) { if(miss.fleet.obj.hasOrders) return; } miss.fleet.obj.addAttackOrder(target); } bool isCapturing(Planet@ pl) { for(uint i = 0, cnt = fleets.length; i < cnt; ++i) { if(fleets[i].capturing is pl) return true; } return false; } bool get_isCapturingAny() { for(uint i = 0, cnt = fleets.length; i < cnt; ++i) { if(fleets[i].capturing !is null) return true; } return false; } }; class War : AIComponent { Fleets@ fleets; Intelligence@ intelligence; Relations@ relations; Movement@ movement; Scouting@ scouting; Systems@ systems; Military@ military; array battles; ScoutingMission@ currentScout; void create() { @fleets = cast(ai.fleets); @intelligence = cast(ai.intelligence); @relations = cast(ai.relations); @movement = cast(ai.movement); @scouting = cast(ai.scouting); @systems = cast(ai.systems); @military = cast(ai.military); } void save(SaveFile& file) { uint cnt = battles.length; file << cnt; for(uint i = 0; i < cnt; ++i) battles[i].save(this, file); fleets.saveMission(file, currentScout); } void load(SaveFile& file) { uint cnt = 0; file >> cnt; battles.length = cnt; for(uint i = 0; i < cnt; ++i) { @battles[i] = Battle(); battles[i].load(this, file); } @currentScout = cast(fleets.loadMission(file)); ai.behavior.remnantAllowArbitraryClear = false; } double getCombatReadyStrength() { double str = 0; for(uint i = 0, cnt = fleets.fleets.length; i < cnt; ++i) { auto@ flAI = fleets.fleets[i]; if(flAI.fleetClass != FC_Combat) continue; if(!flAI.readyForAction) continue; str += flAI.strength; } return str * str; } Battle@ attack(SystemAI@ sys) { Battle atk; @atk.system = sys; atk.isAttack = true; atk.curPriority = MiP_High; @atk.staging = military.getStagingFor(sys.obj); if(log) ai.print("Organizing an attack against "+sys.obj.name); claimFleetsFor(atk); battles.insertLast(atk); return atk; } Battle@ defend(SystemAI@ sys) { Battle def; @def.system = sys; @def.staging = military.getClosestStaging(sys.obj); if(log) ai.print("Organizing defense for "+sys.obj.name); battles.insertLast(def); return def; } void claimFleetsFor(Battle@ atk) { //TODO: This currently claims everything not in use, should it //leave some reserves for defense? Is that good? for(uint i = 0, cnt = fleets.fleets.length; i < cnt; ++i) { auto@ flAI = fleets.fleets[i]; if(flAI.fleetClass != FC_Combat) continue; if(!flAI.readyForAction) continue; atk.join(ai, this, flAI); } } void sendFleetToJoin(Battle@ atk) { for(uint i = 0, cnt = fleets.fleets.length; i < cnt; ++i) { auto@ flAI = fleets.fleets[i]; if(flAI.fleetClass != FC_Combat) continue; if(!flAI.readyForAction) continue; atk.join(ai, this, flAI); break; } } bool isFightingIn(Region@ reg) { for(uint i = 0, cnt = battles.length; i < cnt; ++i) { if(battles[i].system.obj is reg) return true; } return false; } void tick(double time) override { for(uint i = 0, cnt = battles.length; i < cnt; ++i) { if(!battles[i].tick(ai, this, time)) { battles.removeAt(i); --i; --cnt; } } } double ourStrength; double curTime; SystemAI@ best; double totalWeight; SystemAI@ scout; uint scoutCount; void check(SystemAI@ sys, double baseWeight) { if(isFightingIn(sys.obj)) return; if(!sys.visible) { sys.strengthCheck(ai, minInterval=5*60.0); if(sys.lastStrengthCheck < curTime - 5 * 60.0) { scoutCount += 1; if(randomd() < 1.0 / double(scoutCount)) @scout = sys; return; } } else { sys.strengthCheck(ai, minInterval=60.0); } double theirStrength = sys.enemyStrength; if(ourStrength < theirStrength * ai.behavior.attackStrengthOverkill) return; double w = baseWeight; //Try to capture less important systems at first //TODO: This should flip when we go from border skirmishes to subjugation war uint capturable = 0; for(uint i = 0, cnt = sys.planets.length; i < cnt; ++i) { Planet@ pl = sys.planets[i]; if(!ai.empire.isHostile(pl.owner)) continue; if(pl.isProtected(ai.empire)) continue; w /= 1.0 + double(pl.level); capturable += 1; } //Ignore protected systems if(capturable == 0) return; //See where their defenses are low if(theirStrength != 0) { double defRatio = ourStrength / theirStrength; if(defRatio > 4.0) { //We prefer destroying some minor assets over fighting an entirely undefended system, //because it hurts more to lose stuff. w *= 6.0; } } else { w *= 2.0; } totalWeight += w; if(randomd() < w / totalWeight) @best = sys; } int totalEnemySize(SystemAI@ sys) { int size = 0; for(uint i = 0, cnt = getEmpireCount(); i < cnt; ++i) { Empire@ other = getEmpire(i); if(other.major && ai.empire.isHostile(other)) size += sys.obj.getStrength(other); } return size; } bool isUnderAttack(SystemAI@ sys) { if(sys.obj.ContestedMask & ai.mask == 0) return false; if(totalEnemySize(sys) < 100) { if(sys.obj.SiegedMask & ai.mask == 0) return false; } return true; } double assignable(Battle& battle, FleetAI& fleet) { if(fleet.fleetClass != FC_Combat) return 0.0; double w = 1.0; if(fleet.mission !is null) { w *= 0.1; if(fleet.mission.priority >= MiP_High) w *= 0.1; if(fleet.mission.priority >= battle.curPriority) return 0.0; auto@ miss = cast(fleet.mission); if(miss !is null && miss.battleIn is battle.system.obj) return 0.0; } else if(fleet.isHome && fleet.stationed is battle.system.obj) { //This should be allowed to fight always } else if(battle.curPriority >= MiP_Critical) { if(fleet.supplies < 0.25) return 0.0; if(fleet.fleetHealth < 0.25) return 0.0; if(fleet.filled < 0.2) return 0.0; if(fleet.obj.isMoving) { if(fleet.obj.velocity.length / fleet.obj.maxAcceleration > 16.0) w *= 0.1; } } else { if(!fleet.readyForAction) return 0.0; } double fleetStrength = fleet.strength; if(battle.ourStrength + fleetStrength < battle.enemyStrength * ai.behavior.battleStrengthOverkill) w *= 0.25; return w; } double winnability(Battle& battle) { double ours = sqrt(battle.ourStrength); double theirs = battle.enemyStrength; if(theirs <= 0.0) return 10.0; for(uint i = 0, cnt = fleets.fleets.length; i < cnt; ++i) { auto@ fleet = fleets.fleets[i]; double w = assignable(battle, fleet); if(w != 0.0) ours += sqrt(fleet.strength); } ours *= ours; return ours / theirs; } void focusTick(double time) override { if(currentScout !is null) { if(currentScout.canceled || currentScout.completed) @currentScout = null; } //Change our behavior a little depending on the state ai.behavior.remnantAllowArbitraryClear = !relations.isFightingWar(aggressive=true) && battles.length == 0; //Find any systems that need defending //TODO: Defend allies at lowered priority for(uint i = 0, cnt = systems.owned.length; i < cnt; ++i) { SystemAI@ sys = systems.owned[i]; if(!isUnderAttack(sys)) continue; if(isFightingIn(sys.obj)) continue; defend(sys); return; } //Do attacks uint ready = fleets.countCombatReadyFleets(); if(ready != 0) { //See if we can start a new attack if(battles.length < ai.behavior.maxBattles && relations.isFightingWar(aggressive=true) && (battles.length == 0 || ready > ai.behavior.battleReserveFleets)) { //Determine our own strength ourStrength = getCombatReadyStrength(); //Evaluate systems to attack in our aggressive war @best = null; totalWeight = 0; curTime = gameTime; @scout = null; scoutCount = 0; //TODO: Consider aggressive wars against an empire to also be against that empire's vassals for(uint i = 0, cnt = intelligence.intel.length; i < cnt; ++i) { auto@ intel = intelligence.intel[i]; if(intel is null) continue; auto@ relation = relations.get(intel.empire); if(!relation.atWar || !relation.aggressive) continue; for(uint n = 0, ncnt = intel.shared.length; n < ncnt; ++n) { auto@ sys = intel.shared[n]; check(sys, 20.0); } for(uint n = 0, ncnt = intel.theirBorder.length; n < ncnt; ++n) { auto@ sys = intel.theirBorder[n]; check(sys, 1.0); } } //Make the attack with our fleets if(best !is null) attack(best); else if(scout !is null && currentScout is null) { if(log) ai.print("War requests scout to flyby "+scout.obj.name); @currentScout = scouting.scout(scout.obj); } } } } }; AIComponent@ createWar() { return War(); }