// Military // -------- // Military construction logic. Builds and restores fleets and defensive stations, // but does not deal with actually using those fleets to fight - that is the purview of // the War component. // import empire_ai.weasel.WeaselAI; import empire_ai.weasel.Designs; import empire_ai.weasel.Construction; import empire_ai.weasel.Budget; import empire_ai.weasel.Fleets; import empire_ai.weasel.Development; import empire_ai.weasel.Movement; import empire_ai.weasel.Systems; import empire_ai.weasel.Orbitals; import resources; class SupportOrder { DesignTarget@ design; Object@ onObject; AllocateBudget@ alloc; bool isGhostOrder = false; double expires = INFINITY; uint count = 0; void save(Military& military, SaveFile& file) { military.designs.saveDesign(file, design); file << onObject; military.budget.saveAlloc(file, alloc); file << isGhostOrder; file << expires; file << count; } void load(Military& military, SaveFile& file) { @design = military.designs.loadDesign(file); file >> onObject; @alloc = military.budget.loadAlloc(file); file >> isGhostOrder; file >> expires; file >> count; } bool tick(AI& ai, Military& military, double time) { if(alloc !is null) { if(alloc.allocated) { if(isGhostOrder) onObject.rebuildAllGhosts(); else onObject.orderSupports(design.active.mostUpdated(), count); if(military.log && design !is null) ai.print("Support order completed for "+count+"x "+design.active.name+" ("+design.active.size+")", onObject); return false; } } else if(design !is null) { if(design.active !is null) @alloc = military.budget.allocate(BT_Military, getBuildCost(design.active.mostUpdated()) * count); } if(expires < gameTime) { if(alloc !is null && !alloc.allocated) military.budget.remove(alloc); if(isGhostOrder) onObject.clearAllGhosts(); if(military.log) ai.print("Support order expired", onObject); return false; } return true; } }; class StagingBase { Region@ region; array fleets; double idleTime = 0.0; double occupiedTime = 0.0; OrbitalAI@ shipyard; BuildOrbital@ shipyardBuild; Factory@ factory; bool isUnderAttack = false; void save(Military& military, SaveFile& file) { file << region; file << idleTime; file << occupiedTime; file << isUnderAttack; military.orbitals.saveAI(file, shipyard); military.construction.saveConstruction(file, shipyardBuild); uint cnt = fleets.length; file << cnt; for(uint i = 0; i < cnt; ++i) military.fleets.saveAI(file, fleets[i]); } void load(Military& military, SaveFile& file) { file >> region; file >> idleTime; file >> occupiedTime; file >> isUnderAttack; @shipyard = military.orbitals.loadAI(file); @shipyardBuild = cast(military.construction.loadConstruction(file)); uint cnt = 0; file >> cnt; for(uint i = 0; i < cnt; ++i) { if(i > 200 && file < SV_0158) { //Something went preeeetty wrong in an old save if(file.readBit()) { Object@ obj; file >> obj; } } else { auto@ fleet = military.fleets.loadAI(file); if(fleet !is null) fleets.insertLast(fleet); } } } bool tick(AI& ai, Military& military, double time) { if(fleets.length == 0) { occupiedTime = 0.0; idleTime += time; } else { occupiedTime += time; idleTime = 0.0; } isUnderAttack = region.ContestedMask & ai.mask != 0; //Manage building our shipyard if(shipyardBuild !is null) { if(shipyardBuild.completed) { @shipyard = military.orbitals.getInSystem(ai.defs.Shipyard, region); if(shipyard !is null) @shipyardBuild = null; } } if(shipyard !is null) { if(!shipyard.obj.valid) { @shipyard = null; @shipyardBuild = null; } } if(factory !is null && (!factory.valid || factory.obj.region !is region)) @factory = null; if(factory is null) @factory = military.construction.getFactory(region); if(factory !is null) { factory.needsSupportLabor = false; factory.waitingSupportLabor = 0.0; if(factory.obj.hasOrderedSupports) { factory.needsSupportLabor = true; factory.waitingSupportLabor += double(factory.obj.SupplyOrdered) * ai.behavior.estSizeSupportLabor; } for(uint i = 0, cnt = fleets.length; i < cnt; ++i) { if(fleets[i].isHome && fleets[i].obj.hasOrderedSupports) { factory.needsSupportLabor = true; factory.waitingSupportLabor += double(fleets[i].obj.SupplyOrdered) * ai.behavior.estSizeSupportLabor; break; } } if(factory.waitingSupportLabor > 0) factory.aimForLabor(factory.waitingSupportLabor / ai.behavior.constructionMaxTime); } bool isFactorySufficient = false; if(factory !is null) { if(factory.waitingSupportLabor <= factory.laborIncome * ai.behavior.constructionMaxTime || factory.obj.canImportLabor || factory !is military.construction.primaryFactory) isFactorySufficient = true; } for(uint i = 0, cnt = fleets.length; i < cnt; ++i) { Object@ obj = fleets[i].obj; if(obj is null || !obj.valid) { fleets.removeAt(i); --i; --cnt; continue; } fleets[i].stationedFactory = isFactorySufficient; } if(occupiedTime >= 3.0 * 60.0 && ai.defs.Shipyard !is null && shipyard is null && shipyardBuild is null && !isUnderAttack && (!isFactorySufficient && factory !is military.construction.primaryFactory)) { //If any fleets need construction try to queue up a shipyard bool needYard = false; for(uint i = 0, cnt = fleets.length; i < cnt; ++i) { auto@ flt = fleets[i]; if(flt.obj.hasOrderedSupports || flt.filled < 0.8) { needYard = true; break; } } if(needYard) { @shipyard = military.orbitals.getInSystem(ai.defs.Shipyard, region); if(shipyard is null) { vec3d pos = region.position; vec2d offset = random2d(region.radius * 0.4, region.radius * 0.8); pos.x += offset.x; pos.z += offset.y; @shipyardBuild = military.construction.buildOrbital(ai.defs.Shipyard, pos); } } } if((idleTime >= 10.0 * 60.0 || region.PlanetsMask & ai.mask == 0) && (shipyardBuild is null || shipyard !is null) && (factory is null || (shipyard !is null && factory.obj is shipyard.obj)) && military.stagingBases.length >= 2) { if(shipyard !is null) { cast(shipyard.obj).scuttle(); } else { if(factory !is null) { factory.needsSupportLabor = false; @factory = null; } return false; } } return true; } }; class Military : AIComponent { Fleets@ fleets; Development@ development; Designs@ designs; Construction@ construction; Budget@ budget; Systems@ systems; Orbitals@ orbitals; array supportOrders; array stagingBases; AllocateConstruction@ mainWait; bool spentMoney = true; void create() { @fleets = cast(ai.fleets); @development = cast(ai.development); @designs = cast(ai.designs); @construction = cast(ai.construction); @budget = cast(ai.budget); @systems = cast(ai.systems); @orbitals = cast(ai.orbitals); } void save(SaveFile& file) { uint cnt = supportOrders.length; file << cnt; for(uint i = 0; i < cnt; ++i) supportOrders[i].save(this, file); construction.saveConstruction(file, mainWait); file << spentMoney; cnt = stagingBases.length; file << cnt; for(uint i = 0; i < cnt; ++i) { saveStaging(file, stagingBases[i]); stagingBases[i].save(this, file); } } void load(SaveFile& file) { uint cnt = 0; file >> cnt; for(uint i = 0; i < cnt; ++i) { SupportOrder ord; ord.load(this, file); if(ord.onObject !is null) supportOrders.insertLast(ord); } @mainWait = construction.loadConstruction(file); file >> spentMoney; file >> cnt; for(uint i = 0; i < cnt; ++i) { StagingBase@ base = loadStaging(file); if(base !is null) { base.load(this, file); if(stagingBases.find(base) == -1) stagingBases.insertLast(base); } else { StagingBase().load(this, file); } } } void loadFinalize(AI& ai) override { for(uint i = 0, cnt = stagingBases.length; i < cnt; ++i) { auto@ base = stagingBases[i]; for(uint n = 0, ncnt = base.fleets.length; n < ncnt; ++n) { Object@ obj = base.fleets[n].obj; if(obj is null || !obj.valid || !obj.initialized) { base.fleets.removeAt(n); --n; --ncnt; } } } } StagingBase@ loadStaging(SaveFile& file) { Region@ reg; file >> reg; if(reg is null) return null; StagingBase@ base = getBase(reg); if(base is null) { @base = StagingBase(); @base.region = reg; stagingBases.insertLast(base); } return base; } void saveStaging(SaveFile& file, StagingBase@ base) { Region@ reg; if(base !is null) @reg = base.region; file << reg; } Region@ getClosestStaging(Region& targetRegion) { //Check if we have anything close enough StagingBase@ best; int minHops = INT_MAX; for(uint i = 0, cnt = stagingBases.length; i < cnt; ++i) { int d = systems.hopDistance(stagingBases[i].region, targetRegion); if(d < minHops) { minHops = d; @best = stagingBases[i]; } } if(best !is null) return best.region; return null; } Region@ getStagingFor(Region& targetRegion) { //Check if we have anything close enough StagingBase@ best; int minHops = INT_MAX; for(uint i = 0, cnt = stagingBases.length; i < cnt; ++i) { int d = systems.hopDistance(stagingBases[i].region, targetRegion); if(d < minHops) { minHops = d; @best = stagingBases[i]; } } if(minHops < ai.behavior.stagingMaxHops) return best.region; //Create a new staging base for this Region@ bestNew; minHops = INT_MAX; for(uint i = 0, cnt = systems.border.length; i < cnt; ++i) { auto@ sys = systems.border[i].obj; int d = systems.hopDistance(sys, targetRegion); if(d < minHops) { minHops = d; @bestNew = sys; } } if(minHops > ai.behavior.stagingMaxHops && best !is null) return best.region; auto@ base = getBase(bestNew); if(base !is null) return base.region; else return createStaging(bestNew).region; } StagingBase@ createStaging(Region@ region) { if(region is null) return null; if(log) ai.print("Create new staging base.", region); StagingBase newBase; @newBase.region = region; for(uint i = 0, cnt = fleets.fleets.length; i < cnt; ++i) { if(fleets.fleets[i].stationed is region) newBase.fleets.insertLast(fleets.fleets[i]); } stagingBases.insertLast(newBase); return newBase; } StagingBase@ getBase(Region@ inRegion) { if(inRegion is null) return null; for(uint i = 0, cnt = stagingBases.length; i < cnt; ++i) { if(stagingBases[i].region is inRegion) return stagingBases[i]; } return null; } vec3d getStationPosition(Region& inRegion, double distance = 100.0) { auto@ base = getBase(inRegion); if(base !is null) { if(base.shipyard !is null) { vec3d pos = base.shipyard.obj.position; vec2d offset = random2d(distance * 0.5, distance * 1.5); pos.x += offset.x; pos.z += offset.y; return pos; } } vec3d pos = inRegion.position; vec2d offset = random2d(inRegion.radius * 0.4, inRegion.radius * 0.8); pos.x += offset.x; pos.z += offset.y; return pos; } void stationFleet(FleetAI@ fleet, Region@ inRegion) { if(inRegion is null || fleet.stationed is inRegion) return; auto@ prevBase = getBase(fleet.stationed); if(prevBase !is null) prevBase.fleets.remove(fleet); auto@ base = getBase(inRegion); if(base !is null) base.fleets.insertLast(fleet); @fleet.stationed = inRegion; fleet.stationedFactory = construction.getFactory(inRegion) !is null; if(fleet.mission is null) fleets.returnToBase(fleet); } void orderSupportsOn(Object& obj, double expire = 60.0) { if(obj.SupplyGhost > 0) { if(ai.behavior.fleetsRebuildGhosts) { //Try to rebuild the fleet's ghosts SupportOrder ord; @ord.onObject = obj; @ord.alloc = budget.allocate(BT_Military, obj.rebuildGhostsCost()); ord.expires = gameTime + expire; ord.isGhostOrder = true; supportOrders.insertLast(ord); if(log) ai.print("Attempting to rebuild ghosts", obj); return; } else { obj.clearAllGhosts(); } } int supCap = obj.SupplyCapacity; int supHave = obj.SupplyUsed - obj.SupplyGhost; //Build some supports int supSize = pow(2, round(::log(double(supCap) * randomd(0.005, 0.03))/::log(2.0))); supSize = max(min(supSize, supCap - supHave), 1); SupportOrder ord; @ord.onObject = obj; @ord.design = designs.design(DP_Support, supSize); ord.expires = gameTime + expire; ord.count = clamp((supCap - supHave)/supSize, 1, int(ceil((randomd(0.01, 0.1)*supCap)/double(supSize)))); if(log) ai.print("Attempting to build supports: "+ord.count+"x size "+supSize, obj); supportOrders.insertLast(ord); } void findSomethingToDo() { //See if we should retrofit anything if(mainWait is null && !spentMoney && gameTime > ai.behavior.flagshipBuildMinGameTime) { int availMoney = budget.spendable(BT_Military); int moneyTargetSize = floor(double(availMoney) * ai.behavior.shipSizePerMoney); //See if one of our fleets is old enough that we can retrofit it for(uint i = 0, cnt = fleets.fleets.length; i < cnt; ++i) { FleetAI@ fleet = fleets.fleets[i]; if(fleet.mission !is null && fleet.mission.isActive) continue; if(fleet.fleetClass != FC_Combat) continue; if(fleet.obj.hasOrders) continue; Ship@ ship = cast(fleet.obj); if(ship is null) continue; //Don't retrofit free fleets if(ship.isFree && !ai.behavior.retrofitFreeFleets) continue; //Find the factory assigned to this Factory@ factory; if(fleet.isHome) { Region@ reg = fleet.obj.region; @factory = construction.getFactory(reg); } if(factory is null) continue; if(factory.busy) continue; //Find how large we can make this flagship const Design@ dsg = ship.blueprint.design; int targetSize = min(int(moneyTargetSize * 1.2), int(factory.laborToBear(ai) * 1.3 * ai.behavior.shipSizePerLabor)); targetSize = 5 * floor(double(targetSize) / 5.0); //See if we should retrofit this int size = ship.blueprint.design.size; if(size > targetSize) continue; double pctDiff = (double(targetSize) / double(size)) - 1.0; if(pctDiff < ai.behavior.shipRetrofitThreshold) continue; DesignTarget@ newDesign = designs.scale(dsg, targetSize); spentMoney = true; auto@ retrofit = construction.retrofit(ship); @mainWait = construction.buildNow(retrofit, factory); if(log) ai.print("Retrofitting to size "+targetSize, fleet.obj); //TODO: This should mark the fleet as occupied for missions while we retrofit return; } //See if we should build a new fleet Factory@ factory = construction.primaryFactory; if(factory !is null && !factory.busy) { //Figure out how large our flagship would be if we built one factory.aimForLabor((double(moneyTargetSize) / ai.behavior.shipSizePerLabor) / ai.behavior.constructionMaxTime); int targetSize = min(moneyTargetSize, int(factory.laborToBear(ai) * ai.behavior.shipSizePerLabor)); targetSize = 5 * floor(double(targetSize) / 5.0); int expMaint = double(targetSize) * ai.behavior.maintenancePerShipSize; int expCost = double(targetSize) / ai.behavior.shipSizePerMoney; if(budget.canSpend(BT_Military, expCost, expMaint)) { //Make sure we're building an adequately sized flagship uint count = 0; double avgSize = 0.0; for(uint i = 0, cnt = fleets.fleets.length; i < cnt; ++i) { FleetAI@ fleet = fleets.fleets[i]; Ship@ ship = cast(fleet.obj); if(ship !is null && fleet.fleetClass == FC_Combat) { avgSize += ship.blueprint.design.size; count += 1; } } if(count != 0) avgSize /= double(count); if(count < ai.behavior.maxActiveFleets && targetSize >= avgSize * ai.behavior.flagshipBuildMinAvgSize) { //Build the flagship DesignTarget@ design = designs.design(DP_Combat, targetSize, availMoney, budget.maintainable(BT_Military), factory.laborToBear(ai), findSize=true); @mainWait = construction.buildFlagship(design); mainWait.maxTime *= 1.5; spentMoney = true; if(log) ai.print("Ordering a new fleet at size "+targetSize); return; } } } } //See if any of our fleets need refilling //TODO: Aim for labor on the factory so that the supports are built in reasonable time for(uint i = 0, cnt = fleets.fleets.length; i < cnt; ++i) { FleetAI@ fleet = fleets.fleets[i]; if(fleet.mission !is null && fleet.mission.isActive) continue; if(fleet.fleetClass != FC_Combat) continue; if(fleet.obj.hasOrders) continue; if(fleet.filled >= 1.0) continue; if(hasSupportOrderFor(fleet.obj)) continue; if(!fleet.isHome) continue; //Re-station to our factory if we're idle and need refill without being near a factory Factory@ f = construction.getFactory(fleet.obj.region); if(f is null) { if(fleet.filled < ai.behavior.stagingToFactoryFill && construction.primaryFactory !is null) stationFleet(fleet, construction.primaryFactory.obj.region); continue; } //Don't order if the factory has support orders, it'll just make everything take longer if(f !is null && ai.behavior.supportOrderWaitOnFactory && fleet.filled < 0.9 && fleet.obj.SupplyGhost == 0) { if(f.obj.hasOrderedSupports && f.obj.SupplyUsed < f.obj.SupplyCapacity) continue; } int supCap = fleet.obj.SupplyCapacity; int supHave = fleet.obj.SupplyUsed - fleet.obj.SupplyGhost; if(supHave < supCap) { orderSupportsOn(fleet.obj); spentMoney = true; return; } } budget.checkedMilitarySpending = spentMoney; //TODO: Build defense stations } bool hasSupportOrderFor(Object& obj) { for(uint i = 0, cnt = supportOrders.length; i < cnt; ++i) { if(supportOrders[i].onObject is obj) return true; } return false; } void tick(double time) override { //Manage our orders for support ships for(uint i = 0, cnt = supportOrders.length; i < cnt; ++i) { if(!supportOrders[i].tick(ai, this, time)) { supportOrders.removeAt(i); --i; --cnt; } } } void focusTick(double time) override { //Find something for us to do findSomethingToDo(); //If we're far into the budget, spend our money on building supports at our factories if(budget.Progress > 0.9 && budget.canSpend(BT_Military, 10)) { for(uint i = 0, cnt = construction.factories.length; i < cnt; ++i) { //TODO: Build on planets in the system if this is full auto@ f = construction.factories[i]; if(f.obj.SupplyUsed < f.obj.SupplyCapacity && !hasSupportOrderFor(f.obj)) { orderSupportsOn(f.obj, expire=budget.RemainingTime); break; } } } //Check if we should re-station any of our fleets for(uint i = 0, cnt = fleets.fleets.length; i < cnt; ++i) { auto@ flAI = fleets.fleets[i]; if(flAI.stationed is null) { Region@ reg = flAI.obj.region; if(reg !is null && reg.PlanetsMask & ai.mask != 0) stationFleet(flAI, reg); } } //Make sure all our major factories are considered staging bases for(uint i = 0, cnt = construction.factories.length; i < cnt; ++i) { auto@ f = construction.factories[i]; if(f.obj.isShip) continue; Region@ reg = f.obj.region; if(reg is null) continue; auto@ base = getBase(reg); if(base is null) createStaging(reg); } //If we don't have any staging bases, make one at a focus if(stagingBases.length == 0 && development.focuses.length != 0) { Region@ reg = development.focuses[0].obj.region; if(reg !is null) createStaging(reg); } //Update our staging bases for(uint i = 0, cnt = stagingBases.length; i < cnt; ++i) { auto@ base = stagingBases[i]; if(!base.tick(ai, this, time)) { stagingBases.removeAt(i); --i; --cnt; } } } void turn() override { //Fleet construction happens in the beginning of the turn, because we want //to use our entire military budget on it. if(mainWait !is null) { if(mainWait.completed) { @mainWait = null; } else if(!mainWait.started) { if(log) ai.print("Failed current main construction wait."); construction.cancel(mainWait); @mainWait = null; } } spentMoney = false; } }; AIComponent@ createMilitary() { return Military(); }