Files
starruler-linux/scripts/server/empire_ai/weasel/War.as
T
2018-07-17 14:15:37 +02:00

894 lines
22 KiB
ActionScript

// 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<BattleMission@> 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<BattleMission>(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<Battle@> battles;
ScoutingMission@ currentScout;
void create() {
@fleets = cast<Fleets>(ai.fleets);
@intelligence = cast<Intelligence>(ai.intelligence);
@relations = cast<Relations>(ai.relations);
@movement = cast<Movement>(ai.movement);
@scouting = cast<Scouting>(ai.scouting);
@systems = cast<Systems>(ai.systems);
@military = cast<Military>(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<ScoutingMission>(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<BattleMission>(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();
}