// Construction // ------------ // Manages factories and allows build requests for flagships, orbitals, and // anything else that requires labor. // import empire_ai.weasel.WeaselAI; import empire_ai.weasel.Budget; import empire_ai.weasel.Planets; import empire_ai.weasel.Designs; import empire_ai.weasel.Resources; import empire_ai.weasel.Systems; import empire_ai.weasel.Orbitals; import orbitals; import saving; import systems; import regions.regions; from constructible import ConstructibleType; from constructions import ConstructionType, getConstructionType; class AllocateConstruction { int id = -1; uint moneyType = BT_Development; Factory@ tryFactory; double maxTime = INFINITY; bool completed = false; bool started = false; double completedAt = 0; AllocateBudget@ alloc; int cost = 0; int maintenance = 0; double priority = 1.0; AllocateConstruction() { } void _save(Construction& construction, SaveFile& file) { file << moneyType; construction.saveFactory(file, tryFactory); file << maxTime; file << completed; file << started; file << completedAt; construction.budget.saveAlloc(file, alloc); file << cost; file << maintenance; file << priority; save(construction, file); } void save(Construction& construction, SaveFile& file) { } void _load(Construction& construction, SaveFile& file) { file >> moneyType; @tryFactory = construction.loadFactory(file); file >> maxTime; file >> completed; file >> started; file >> completedAt; @alloc = construction.budget.loadAlloc(file); file >> cost; file >> maintenance; file >> priority; load(construction, file); } void load(Construction& construction, SaveFile& file) { } bool tick(AI& ai, Construction& construction, double time) { if(tryFactory !is null && alloc.allocated) { construction.start(tryFactory, this); return false; } return true; } void update(AI& ai, Factory@ f) { @alloc = cast(ai.budget).allocate(moneyType, cost, maintenance, priority); } double laborCost(AI& ai, Object@ obj) { return 0.0; } bool canBuild(AI& ai, Factory@ f) { return true; } void construct(AI& ai, Factory@ f) { started = true; } string toString() { return "construction"; } }; class BuildFlagship : AllocateConstruction { double baseLabor = 0.0; const Design@ design; DesignTarget@ target; BuildFlagship() { } BuildFlagship(const Design@ dsg) { set(dsg); } BuildFlagship(DesignTarget@ target) { @this.target = target; } void save(Construction& construction, SaveFile& file) { file << baseLabor; if(design !is null) { file.write1(); file << design; } else { file.write0(); } construction.designs.saveDesign(file, target); } void load(Construction& construction, SaveFile& file) { file >> baseLabor; if(file.readBit()) file >> design; @target = construction.designs.loadDesign(file); } void set(const Design& dsg) { @design = dsg.mostUpdated(); baseLabor = design.total(HV_LaborCost); } double laborCost(AI& ai, Object@ obj) { return baseLabor; } bool tick(AI& ai, Construction& construction, double time) override { if(target !is null) { if(target.active !is null) { set(target.active); @target = null; } } return AllocateConstruction::tick(ai, construction, time); } bool canBuild(AI& ai, Factory@ f) override { if(!f.obj.canBuildShips) return false; return design !is null; } void update(AI& ai, Factory@ f) { double c = design.total(HV_BuildCost); c *= double(f.obj.shipBuildCost) / 100.0; c *= f.obj.constructionCostMod; cost = ceil(c); maintenance = ceil(design.total(HV_MaintainCost)); AllocateConstruction::update(ai, f); } void construct(AI& ai, Factory@ f) { f.obj.buildFlagship(design); AllocateConstruction::construct(ai, f); } string toString() { if(design is null) return "flagship (design in progress)"; return "flagship "+design.name; } }; class BuildFlagshipSourced : BuildFlagship { Object@ buildAt; Object@ buildFrom; BuildFlagshipSourced() { } BuildFlagshipSourced(const Design@ dsg) { set(dsg); } BuildFlagshipSourced(DesignTarget@ target) { @this.target = target; } void save(Construction& construction, SaveFile& file) override { BuildFlagship::save(construction, file); file << buildAt; file << buildFrom; } void load(Construction& construction, SaveFile& file) override { BuildFlagship::load(construction, file); file >> buildAt; file >> buildFrom; } bool canBuild(AI& ai, Factory@ f) override { if(buildAt !is null && f.obj !is buildAt) return false; return BuildFlagship::canBuild(ai, f); } void construct(AI& ai, Factory@ f) override { f.obj.buildFlagship(design, constructFrom=buildFrom); AllocateConstruction::construct(ai, f); } }; class BuildStation : AllocateConstruction { double baseLabor = 0.0; const Design@ design; DesignTarget@ target; vec3d position; bool local = false; BuildStation() { } BuildStation(const Design@ dsg, const vec3d& position) { this.position = position; set(dsg); } BuildStation(DesignTarget@ target, const vec3d& position) { this.position = position; @this.target = target; } BuildStation(const Design@ dsg, bool local) { this.local = true; set(dsg); } BuildStation(DesignTarget@ target, bool local) { @this.target = target; this.local = true; } void save(Construction& construction, SaveFile& file) { file << baseLabor; file << position; file << local; if(design !is null) { file.write1(); file << design; } else { file.write0(); } construction.designs.saveDesign(file, target); } void load(Construction& construction, SaveFile& file) { file >> baseLabor; file >> position; file >> local; if(file.readBit()) file >> design; @target = construction.designs.loadDesign(file); } void set(const Design& dsg) { @design = dsg.mostUpdated(); baseLabor = design.total(HV_LaborCost); } double laborCost(AI& ai, Object@ obj) { double labor = baseLabor; labor *= obj.owner.OrbitalLaborCostFactor; if(!local) { Region@ reg = getRegion(position); Region@ targReg = obj.region; if(reg !is null && targReg !is null) { int hops = cast(ai.systems).tradeDistance(targReg, reg); if(hops > 0) { double penalty = 1.0 + config::ORBITAL_LABOR_COST_STEP * double(hops); baseLabor *= penalty; } } } return labor; } bool tick(AI& ai, Construction& construction, double time) override { if(target !is null) { if(target.active !is null) { set(target.active); @target = null; } } return AllocateConstruction::tick(ai, construction, time); } bool canBuild(AI& ai, Factory@ f) override { if(design is null) return false; if(!f.obj.canBuildOrbitals) return false; Region@ targReg = f.obj.region; if(targReg is null) return false; if(!local) { Region@ reg = getRegion(position); if(reg is null) return false; if(!cast(ai.systems).canTrade(targReg, reg)) return false; } return true; } void update(AI& ai, Factory@ f) { double c = design.total(HV_BuildCost); c *= f.obj.owner.OrbitalBuildCostFactor; c *= f.obj.constructionCostMod; cost = ceil(c); maintenance = ceil(design.total(HV_MaintainCost)); AllocateConstruction::update(ai, f); } void construct(AI& ai, Factory@ f) { if(local) { position = f.obj.position; vec2d offset = random2d(f.obj.radius + 10.0, f.obj.radius + 100.0); position.x += offset.x; position.z += offset.y; } f.obj.buildStation(design, position); AllocateConstruction::construct(ai, f); } string toString() { if(design is null) return "station (design in progress)"; return "station "+design.name; } }; class BuildOrbital : AllocateConstruction { double baseLabor = 0.0; const OrbitalModule@ module; bool local = false; vec3d position; BuildOrbital() { } BuildOrbital(const OrbitalModule@ module, const vec3d& position) { this.position = position; @this.module = module; baseLabor = module.laborCost; } BuildOrbital(const OrbitalModule@ module, bool local) { this.local = true; @this.module = module; baseLabor = module.laborCost; } void save(Construction& construction, SaveFile& file) { file << baseLabor; file << position; file << local; file.writeIdentifier(SI_Orbital, module.id); } void load(Construction& construction, SaveFile& file) { file >> baseLabor; file >> position; file >> local; @module = getOrbitalModule(file.readIdentifier(SI_Orbital)); } double laborCost(AI& ai, Object@ obj) { double labor = baseLabor; labor *= obj.owner.OrbitalLaborCostFactor; if(!local) { Region@ reg = getRegion(position); Region@ targReg = obj.region; if(reg !is null && targReg !is null) { int hops = cast(ai.systems).tradeDistance(targReg, reg); if(hops > 0) { double penalty = 1.0 + config::ORBITAL_LABOR_COST_STEP * double(hops); baseLabor *= penalty; } } } return labor; } bool tick(AI& ai, Construction& construction, double time) override { return AllocateConstruction::tick(ai, construction, time); } bool canBuild(AI& ai, Factory@ f) override { if(module is null) return false; if(!f.obj.canBuildOrbitals) return false; Region@ targReg = f.obj.region; if(targReg is null) return false; if(!local) { Region@ reg = getRegion(position); if(reg is null) return false; if(!cast(ai.systems).canTrade(targReg, reg)) return false; } return true; } void update(AI& ai, Factory@ f) { double c = module.buildCost; c *= f.obj.owner.OrbitalBuildCostFactor; c *= f.obj.constructionCostMod; cost = ceil(c); maintenance = module.maintenance; AllocateConstruction::update(ai, f); } void construct(AI& ai, Factory@ f) { if(local) { position = f.obj.position; vec2d offset = random2d(f.obj.radius + 10.0, f.obj.radius + 100.0); position.x += offset.x; position.z += offset.y; } f.obj.buildOrbital(module.id, position); AllocateConstruction::construct(ai, f); } string toString() { return "orbital "+module.name; } }; class RetrofitShip : AllocateConstruction { Ship@ ship; double labor; RetrofitShip() { } RetrofitShip(Ship@ ship) { @this.ship = ship; labor = ship.getRetrofitLabor(); cost = ship.getRetrofitCost(); } void save(Construction& construction, SaveFile& file) { file << ship; file << labor; } void load(Construction& construction, SaveFile& file) { file >> ship; file >> labor; } double laborCost(AI& ai, Object@ obj) { return labor; } bool canBuild(AI& ai, Factory@ f) override { if(!f.obj.canBuildShips) return false; Region@ reg = ship.region; return reg !is null && reg is f.obj.region; } void construct(AI& ai, Factory@ f) { ship.retrofitFleetAt(f.obj); AllocateConstruction::construct(ai, f); } string toString() { return "retrofit "+ship.name; } }; class BuildConstruction : AllocateConstruction { const ConstructionType@ consType; BuildConstruction() { } BuildConstruction(const ConstructionType@ consType) { @this.consType = consType; } void save(Construction& construction, SaveFile& file) { file.writeIdentifier(SI_ConstructionType, consType.id); } void load(Construction& construction, SaveFile& file) { @consType = getConstructionType(file.readIdentifier(SI_ConstructionType)); } double laborCost(AI& ai, Object@ obj) { if(obj is null) return consType.laborCost; return consType.getLaborCost(obj); } bool canBuild(AI& ai, Factory@ f) override { return consType.canBuild(f.obj, ignoreCost=true); } void update(AI& ai, Factory@ f) { cost = consType.getBuildCost(f.obj); maintenance = consType.getMaintainCost(f.obj); AllocateConstruction::update(ai, f); } void construct(AI& ai, Factory@ f) { f.obj.buildConstruction(consType.id); AllocateConstruction::construct(ai, f); } string toString() { return "construction "+consType.name; } }; class Factory { Object@ obj; PlanetAI@ plAI; Factory@ exportingTo; AllocateConstruction@ active; double laborAim = 0.0; double laborIncome = 0.0; double idleSince = 0.0; double storedLabor = 0.0; double laborMaxStorage = 0.0; double buildingPenalty = 0.0; bool needsSupportLabor = false; double waitingSupportLabor = 0.0; uint curConstructionType = 0; bool valid = true; bool significantLabor = true; uint backgrounded = 0; Asteroid@ bgAsteroid; BuildingRequest@ curBuilding; ImportData@ curImport; void save(Construction& construction, SaveFile& file) { construction.planets.saveAI(file, plAI); construction.saveConstruction(file, active); file << laborAim; file << laborIncome; file << idleSince; file << storedLabor; file << laborMaxStorage; file << buildingPenalty; construction.planets.saveBuildingRequest(file, curBuilding); construction.resources.saveImport(file, curImport); file << backgrounded; file << bgAsteroid; file << curConstructionType; file << valid; file << needsSupportLabor; file << waitingSupportLabor; construction.saveFactory(file, exportingTo); } void load(Construction& construction, SaveFile& file) { @plAI = construction.planets.loadAI(file); @active = construction.loadConstruction(file); file >> laborAim; file >> laborIncome; file >> idleSince; file >> storedLabor; file >> laborMaxStorage; file >> buildingPenalty; @curBuilding = construction.planets.loadBuildingRequest(file); @curImport = construction.resources.loadImport(file); file >> backgrounded; file >> bgAsteroid; file >> curConstructionType; file >> valid; file >> needsSupportLabor; file >> waitingSupportLabor; @exportingTo = construction.loadFactory(file); } bool get_busy() { return active !is null; } bool get_needsLabor() { if(!valid) return false; if(obj.hasOrderedSupports) return true; if(active !is null) return true; if(needsSupportLabor) return true; if(obj.constructionCount > 0 && curConstructionType != CT_Export) return true; return false; } double laborToBear(AI& ai) { return laborIncome * ai.behavior.constructionMaxTime + storedLabor; } bool viable(AI& ai, AllocateConstruction@ alloc) { double labor = obj.laborIncome; double estTime = (alloc.laborCost(ai, obj) - storedLabor) / labor; if(estTime > alloc.maxTime) return false; return true; } bool tick(AI& ai, Construction& construction, double time) { if(obj is null || !obj.valid || obj.owner !is ai.empire) { valid = false; return false; } uint curCount = obj.constructionCount; curConstructionType = 0; bool isBackground = false; if(curCount != 0) { curConstructionType = obj.constructionType; isBackground = curConstructionType == CT_Asteroid || curConstructionType == CT_Export; } if(active !is null) { if(curCount <= backgrounded || (curCount == 1 && isBackground)) { if(construction.log) ai.print("Completed construction of "+active.toString()+" "+backgrounded+" / "+curCount, obj); active.completed = true; active.completedAt = gameTime; @active = null; idleSince = gameTime; backgrounded = 0; } } else { if(curCount < backgrounded) { backgrounded = curCount; } } //Background constructibles we don't need to do right now if(curCount > 1 && curConstructionType == CT_Asteroid && bgAsteroid !is null) { obj.moveConstruction(obj.constructionID[0], -1); backgrounded += 1; } if(curCount > 1 && curConstructionType == CT_Export && exportingTo !is null) { obj.cancelConstruction(obj.constructionID[0]); @exportingTo = null; } if(bgAsteroid !is null && (bgAsteroid.owner.valid || curCount == 0)) { if(bgAsteroid.owner is ai.empire) construction.planets.register(bgAsteroid); @bgAsteroid = null; } //Build warehouse(s) if we've been idle laborIncome = obj.laborIncome; storedLabor = obj.currentLaborStored; laborMaxStorage = obj.laborStorageCapacity; significantLabor = laborIncome >= 0.4 * construction.bestLabor && obj.baseLaborIncome > 4.0/60.0; if(storedLabor < laborMaxStorage) idleSince = gameTime; if(active is null && curBuilding is null && plAI !is null && gameTime - idleSince > ai.behavior.laborStoreIdleTimer && ai.behavior.buildLaborStorage && (laborMaxStorage+50) < ai.behavior.laborStoreMaxFillTime * max(obj.baseLaborIncome, laborAim) && significantLabor) { auto@ bld = ai.defs.LaborStorage; if(bld !is null && buildingPenalty < gameTime) { if(construction.log) ai.print("Build building "+bld.name+" for labor storage", obj); @curBuilding = construction.planets.requestBuilding(plAI, bld); } } //Remove waits on completed labor gains if(curBuilding !is null) { if(curBuilding.canceled || (curBuilding.built && curBuilding.getProgress() >= 1.0)) { if(construction.log) ai.print("Building construction for labor finished", obj); if(curBuilding.canceled) buildingPenalty = gameTime + 60.0; @curBuilding = null; } } if(curImport !is null) { if(curImport.beingMet) { if(construction.log) ai.print("Resource import for labor finished", obj); @curImport = null; } } //See if we need a new labor gain if(laborIncome < laborAim) { if(curImport is null && plAI !is null && obj.isPressureSaturated(TR_Labor) && obj.pressureCap < uint(obj.totalPressure) && gameTime > 6.0 * 60.0 && ai.behavior.buildFactoryForLabor) { ResourceSpec spec; spec.type = RST_Pressure_Level0; spec.pressureType = TR_Labor; if(construction.log) ai.print("Queue resource import for labor", obj); @curImport = construction.resources.requestResource(obj, spec, prioritize=true); } if(curBuilding is null && plAI !is null && ai.behavior.buildLaborStorage) { auto@ bld = ai.defs.Factory; if(bld !is null && buildingPenalty < gameTime) { if(construction.log) ai.print("Build building "+bld.name+" for labor", obj); @curBuilding = construction.planets.requestBuilding(plAI, bld); } } } //See if we should spend our labor on a labor export somewhere else if(exportingTo !is null && curConstructionType == CT_Export) { if(!exportingTo.valid || (!exportingTo.needsLabor && exportingTo !is construction.primaryFactory)) { obj.cancelConstruction(obj.constructionID[0]); @exportingTo = null; } } if(ai.behavior.distributeLaborExports) { if(curCount == 0 && obj.canExportLabor) { uint offset = randomi(0, construction.factories.length-1); for(uint i = 0, cnt = construction.factories.length; i < cnt; ++i) { auto@ other = construction.factories[(i+offset) % cnt]; if(other is this) continue; if(!other.obj.canImportLabor) continue; //Check if this is currently busy if(other !is construction.primaryFactory) { if(!other.needsLabor) continue; } obj.exportLaborTo(other.obj); @exportingTo = other; } } } //See if we should spend our labor trying to build an asteroid if(ai.behavior.backgroundBuildAsteroids) { if((curCount == 0 || (curConstructionType == CT_Export && curCount == 1)) && storedLabor >= laborMaxStorage * 0.5 && obj.canBuildAsteroids) { Asteroid@ roid = construction.getBackgroundAsteroid(this); if(roid !is null) { uint resCount = roid.getAvailableCount(); if(resCount != 0) { uint bestIndex = 0; int bestId = -1; double bestWeight = 0.0; if(ai.behavior.chooseAsteroidResource) { for(uint i = 0; i < resCount; ++i) { int resourceId = roid.getAvailable(i); double w = asteroidResourceValue(getResource(resourceId)); if(w > bestWeight) { bestWeight = w; bestId = resourceId; bestIndex = i; } } } else { bestIndex = randomi(0, resCount-1); bestId = roid.getAvailable(bestIndex); } double laborCost = roid.getAvailableCost(bestIndex); Region@ fromReg = obj.region; Region@ toReg = roid.region; if(fromReg !is null && toReg !is null) laborCost *= 1.0 + config::ASTEROID_COST_STEP * double(construction.systems.hopDistance(fromReg, toReg)); double timeTaken = laborIncome / laborCost; if(timeTaken < ai.behavior.constructionMaxTime || storedLabor >= laborMaxStorage * 0.95) { @bgAsteroid = roid; obj.buildAsteroid(roid, bestId); if(construction.log) ai.print("Use background labor to mine "+roid.name+" in "+roid.region.name, obj); } } } } } return true; } void aimForLabor(double labor) { if(labor > laborAim) laborAim = labor; } }; class Construction : AIComponent { array factories; Factory@ primaryFactory; double noFactoryTimer = 0.0; int nextAllocId = 0; array allocations; double totalLabor = 0.0; double bestLabor = 0.0; BuildOrbital@ buildConsolidate; Budget@ budget; Planets@ planets; Orbitals@ orbitals; Resources@ resources; Designs@ designs; Systems@ systems; void create() { @budget = cast(ai.budget); @planets = cast(ai.planets); @resources = cast(ai.resources); @designs = cast(ai.designs); @systems = cast(ai.systems); @orbitals = cast(ai.orbitals); } void save(SaveFile& file) { file << nextAllocId; uint cnt = allocations.length; file << cnt; for(uint i = 0; i < cnt; ++i) saveConstruction(file, allocations[i]); cnt = factories.length; file << cnt; for(uint i = 0; i < cnt; ++i) { saveFactory(file, factories[i]); factories[i].save(this, file); } saveFactory(file, primaryFactory); file << noFactoryTimer; saveConstruction(file, buildConsolidate); } void load(SaveFile& file) { file >> nextAllocId; uint cnt = 0; file >> cnt; for(uint i = 0; i < cnt; ++i) { auto@ alloc = loadConstruction(file); if(alloc !is null) allocations.insertLast(alloc); } file >> cnt; for(uint i = 0; i < cnt; ++i) { Factory@ f = loadFactory(file); if(f !is null) f.load(this, file); else Factory().load(this, file); } @primaryFactory = loadFactory(file); file >> noFactoryTimer; @buildConsolidate = cast(loadConstruction(file)); } void saveFactory(SaveFile& file, Factory@ f) { if(f !is null) { file.write1(); file << f.obj; } else { file.write0(); } } Factory@ loadFactory(SaveFile& file) { if(!file.readBit()) return null; Object@ obj; file >> obj; if(obj is null) return null; for(uint i = 0, cnt = factories.length; i < cnt; ++i) { if(factories[i].obj is obj) return factories[i]; } Factory f; @f.obj = obj; factories.insertLast(f); return f; } array savedConstructions; array loadedConstructions; void postSave(AI& ai) { savedConstructions.length = 0; } void postLoad(AI& ai) { loadedConstructions.length = 0; } void saveConstruction(SaveFile& file, AllocateConstruction@ alloc) { if(alloc is null) { file.write0(); return; } file.write1(); file << alloc.id; if(alloc.id == -1) { storeConstruction(file, alloc); } else { bool found = false; for(uint i = 0, cnt = savedConstructions.length; i < cnt; ++i) { if(savedConstructions[i] is alloc) { found = true; break; } } if(!found) { storeConstruction(file, alloc); savedConstructions.insertLast(alloc); } } } AllocateConstruction@ loadConstruction(SaveFile& file) { if(!file.readBit()) return null; int id = 0; file >> id; if(id == -1) { AllocateConstruction@ alloc = createConstruction(file); alloc.id = id; return alloc; } else { for(uint i = 0, cnt = loadedConstructions.length; i < cnt; ++i) { if(loadedConstructions[i].id == id) return loadedConstructions[i]; } AllocateConstruction@ alloc = createConstruction(file); alloc.id = id; loadedConstructions.insertLast(alloc); return alloc; } } void storeConstruction(SaveFile& file, AllocateConstruction@ alloc) { auto@ cls = getClass(alloc); auto@ mod = cls.module; file << mod.name; file << cls.name; alloc._save(this, file); } AllocateConstruction@ createConstruction(SaveFile& file) { string modName; string clsName; file >> modName; file >> clsName; auto@ mod = getScriptModule(modName); if(mod is null) { error("ERROR: AI Load could not find module for alloc "+modName+"::"+clsName); return null; } auto@ cls = mod.getClass(clsName); if(cls is null) { error("ERROR: AI Load could not find class for alloc "+modName+"::"+clsName); return null; } auto@ alloc = cast(cls.create()); if(alloc is null) { error("ERROR: AI Load could not create class instance for alloc "+modName+"::"+clsName); return null; } alloc._load(this, file); return alloc; } void start() { Object@ hw = ai.empire.Homeworld; if(hw !is null) { Factory f; @f.obj = hw; @f.plAI = planets.getAI(cast(hw)); factories.insertLast(f); } } Factory@ get(Object@ obj) { for(uint i = 0, cnt = factories.length; i < cnt; ++i) { if(factories[i].obj is obj) return factories[i]; } return null; } Factory@ registerFactory(Object@ obj) { for(uint i = 0, cnt = factories.length; i < cnt; ++i) { if(factories[i].obj is obj) return factories[i]; } Factory f; @f.obj = obj; factories.insertLast(f); return f; } Factory@ getFactory(Region@ region) { Factory@ best; double bestLabor = 0; for(uint i = 0, cnt = factories.length; i < cnt; ++i) { if(factories[i].obj.region !is region) continue; double l = factories[i].obj.laborIncome; if(l > bestLabor) { bestLabor = l; @best = factories[i]; } } return best; } BuildConstruction@ buildConstruction(const ConstructionType@ type, double priority = 1.0, bool force = false, uint moneyType = BT_Development) { //Potentially build a flagship BuildConstruction f(type); f.moneyType = moneyType; f.priority = priority; build(f, force=force); return f; } BuildFlagship@ buildFlagship(const Design@ dsg, double priority = 1.0, bool force = false) { //Potentially build a flagship BuildFlagship f(dsg); f.moneyType = BT_Military; f.priority = priority; build(f, force=force); return f; } BuildFlagship@ buildFlagship(DesignTarget@ target, double priority = 1.0, bool force = false) { //Potentially build a flagship BuildFlagship f(target); f.moneyType = BT_Military; f.priority = priority; build(f, force=force); return f; } BuildStation@ buildStation(const Design@ dsg, const vec3d& position, double priority = 1.0, bool force = false) { //Potentially build a flagship BuildStation f(dsg, position); f.moneyType = BT_Military; f.priority = priority; build(f, force=force); return f; } BuildStation@ buildStation(DesignTarget@ target, const vec3d& position, double priority = 1.0, bool force = false) { //Potentially build a flagship BuildStation f(target, position); f.moneyType = BT_Military; f.priority = priority; build(f, force=force); return f; } BuildOrbital@ buildOrbital(const OrbitalModule@ module, const vec3d& position, double priority = 1.0, bool force = false) { //Potentially build a flagship BuildOrbital f(module, position); f.moneyType = BT_Military; f.priority = priority; build(f, force=force); return f; } BuildStation@ buildLocalStation(const Design@ dsg, double priority = 1.0, bool force = false) { //Potentially build a flagship BuildStation f(dsg, local=true); f.moneyType = BT_Military; f.priority = priority; build(f, force=force); return f; } BuildStation@ buildLocalStation(DesignTarget@ target, double priority = 1.0, bool force = false) { //Potentially build a flagship BuildStation f(target, local=true); f.moneyType = BT_Military; f.priority = priority; build(f, force=force); return f; } BuildOrbital@ buildLocalOrbital(const OrbitalModule@ module, double priority = 1.0, bool force = false) { //Potentially build a flagship BuildOrbital f(module, local=true); f.moneyType = BT_Military; f.priority = priority; build(f, force=force); return f; } RetrofitShip@ retrofit(Ship@ ship, double priority = 1.0, bool force = false) { //Potentially build a flagship RetrofitShip f(ship); f.moneyType = BT_Military; f.priority = priority; build(f, force=force); return f; } AllocateConstruction@ build(AllocateConstruction@ alloc, bool force = false) { //Add a construction into the potential constructions queue if(!force) alloc.maxTime = ai.behavior.constructionMaxTime; alloc.id = nextAllocId++; allocations.insertLast(alloc); if(log) ai.print("Queue construction: "+alloc.toString()); return alloc; } AllocateConstruction@ buildNow(AllocateConstruction@ alloc, Factory@ f) { if(f.busy) return null; if(alloc.alloc !is null) budget.applyNow(alloc.alloc); start(f, alloc); allocations.remove(alloc); return alloc; } void cancel(AllocateConstruction@ alloc) { if(alloc.started || (alloc.alloc !is null && alloc.alloc.allocated)) return; //TODO allocations.remove(alloc); for(uint i = 0, cnt = factories.length; i < cnt; ++i) { if(factories[i].active is alloc) @factories[i].active = null; } if(alloc.alloc !is null) budget.remove(alloc.alloc); } uint factInd = 0; void tick(double time) { //Manage factories if(factories.length != 0) { factInd = (factInd+1) % factories.length; auto@ f = factories[factInd]; if(!f.tick(ai, this, time)) factories.removeAt(factInd); } } void start(Factory@ f, AllocateConstruction@ c) { //Actually construct something we've allocated budget for @f.active = c; @c.tryFactory = null; c.construct(ai, f); if(log) ai.print("Construct: "+c.toString(), f.obj); for(uint i = 0, cnt = allocations.length; i < cnt; ++i) { if(allocations[i].tryFactory is f) @allocations[i].tryFactory = null; } } uint plCheck = 0; uint orbCheck = 0; double consTimer = 0.0; void focusTick(double time) { //Progress the allocations for(uint n = 0, ncnt = allocations.length; n < ncnt; ++n) { if(!allocations[n].tick(ai, this, time)) { allocations.removeAt(n); --n; --ncnt; } } //See if anything we can potentially construct is constructible totalLabor = 0.0; bestLabor = 0.0; for(uint i = 0, cnt = factories.length; i < cnt; ++i) { auto@ f = factories[i]; totalLabor += f.laborIncome; if(f.laborIncome > bestLabor) bestLabor = f.laborIncome; } for(uint n = 0, ncnt = allocations.length; n < ncnt; ++n) { auto@ alloc = allocations[n]; if(alloc.tryFactory !is null) continue; Factory@ bestFact; double bestCur = 0.0; for(uint i = 0, cnt = factories.length; i < cnt; ++i) { auto@ f = factories[i]; if(f.busy) continue; if(!alloc.canBuild(ai, f)) continue; if(!f.viable(ai, alloc)) continue; double w = f.laborIncome; if(f is primaryFactory) w *= 1.5; if(f.exportingTo !is null) w /= 0.75; if(w > bestCur) { bestCur = w; @bestFact = f; } } if(bestFact !is null) { @alloc.tryFactory = bestFact; alloc.update(ai, bestFact); } } //Classify our primary factory if(primaryFactory is null) { //Find our best factory Factory@ best; double bestWeight = 0.0; for(uint i = 0, cnt = factories.length; i < cnt; ++i) { auto@ f = factories[i]; double w = f.laborIncome; w += 0.1 * f.laborAim; if(f.obj.isPlanet) w *= 100.0; if(f.obj.isShip) w *= 0.1; if(w > bestWeight) { bestWeight = w; @best = f; } } if(best !is null) { @primaryFactory = best; } else { noFactoryTimer += time; if(noFactoryTimer > 3.0 * 60.0 && ai.defs.Factory !is null) { //Just pick our highest level planet and hope for the best PlanetAI@ best; double bestWeight = 0.0; for(uint i = 0, cnt = planets.planets.length; i < cnt; ++i) { auto@ plAI = planets.planets[i]; double w = plAI.obj.level; w += 0.5 * plAI.obj.resourceLevel; if(w > bestWeight) { bestWeight = w; @best = plAI; } } if(best !is null) { Factory f; @f.obj = best.obj; @f.plAI = best; factories.insertLast(f); @primaryFactory = f; } noFactoryTimer = 0.0; } } } else { noFactoryTimer = 0.0; } //Find new factories if(planets.planets.length != 0) { plCheck = (plCheck+1) % planets.planets.length; PlanetAI@ plAI = planets.planets[plCheck]; if(plAI.obj.laborIncome > 0 && plAI.obj.canBuildShips) { if(get(plAI.obj) is null) { Factory f; @f.obj = plAI.obj; @f.plAI = plAI; factories.insertLast(f); } } } if(orbitals.orbitals.length != 0) { orbCheck = (orbCheck+1) % orbitals.orbitals.length; OrbitalAI@ orbAI = orbitals.orbitals[orbCheck]; if(orbAI.obj.hasConstruction && orbAI.obj.laborIncome > 0 && !cast(orbAI.obj).hasMaster()) { if(get(orbAI.obj) is null) { Factory f; @f.obj = orbAI.obj; factories.insertLast(f); } } } //See if we should switch our primary factory if(primaryFactory !is null) { if(!primaryFactory.valid) { @primaryFactory = null; } else { Factory@ best; double bestLabor = 0.0; double primaryLabor = primaryFactory.laborIncome; bool canImport = primaryFactory.obj.canImportLabor; for(uint i = 0, cnt = factories.length; i < cnt; ++i) { auto@ f = factories[i]; double checkLabor = f.laborIncome; if(f.obj.isShip) checkLabor *= 0.1; if(f.exportingTo !is primaryFactory && canImport) primaryLabor += checkLabor * 0.75; if(checkLabor > bestLabor) { bestLabor = checkLabor; @best = f; } } if(best !is null && bestLabor > 1.5 * primaryLabor) @primaryFactory = best; } } //See if we should consolidate at a shipyard if(buildConsolidate !is null && buildConsolidate.completed) { @buildConsolidate = null; consTimer = gameTime + 60.0; } else if(ai.behavior.consolidateLaborExports && primaryFactory !is null && ai.defs.Shipyard !is null && buildConsolidate is null && !primaryFactory.obj.canImportLabor && consTimer < gameTime) { double totalLabor = 0.0, bestLabor = 0.0; for(uint i = 0, cnt = factories.length; i < cnt; ++i) { double inc = factories[i].obj.baseLaborIncome; if(factories[i].obj.canExportLabor) totalLabor += inc; if(factories[i].laborIncome > bestLabor) bestLabor = factories[i].laborIncome; } if(bestLabor < totalLabor * 0.6) { Factory@ bestConsolidate; double bestWeight = 0.0; for(uint i = 0, cnt = factories.length; i < cnt; ++i) { auto@ f = factories[i]; if(!f.obj.canImportLabor) continue; double w = f.obj.baseLaborIncome; w /= f.obj.position.distanceTo(primaryFactory.obj.position); if(w > bestWeight) { bestWeight = w; @bestConsolidate = f; } } if(bestConsolidate !is null) { if(log) ai.print("Set shipyard for consolidate.", bestConsolidate.obj.region); @primaryFactory = bestConsolidate; } else { Region@ reg = primaryFactory.obj.region; if(reg !is null) { vec3d pos = reg.position; vec2d offset = random2d(reg.radius * 0.4, reg.radius * 0.8); pos.x += offset.x; pos.z += offset.y; if(log) ai.print("Build shipyard for consolidate.", reg); @buildConsolidate = buildOrbital(ai.defs.Shipyard, pos); } } } } } bool isGettingAsteroid(Asteroid@ asteroid) { for(uint i = 0, cnt = factories.length; i < cnt; ++i) { if(factories[i].bgAsteroid is asteroid) return true; } return false; } Asteroid@ getBackgroundAsteroid(Factory& f) { double closest = INFINITY; Asteroid@ best; Region@ reg = f.obj.region; if(reg is null) return null; uint cnt = systems.owned.length; uint offset = randomi(0, cnt-1); for(uint i = 0, check = min(3, cnt); i < check; ++i) { auto@ sys = systems.owned[(i+offset)%cnt]; double dist = sys.obj.position.distanceToSQ(f.obj.position); if(dist > closest) continue; if(!sys.obj.sharesTerritory(ai.empire, reg)) continue; for(uint n = 0, ncnt = sys.asteroids.length; n < ncnt; ++n) { Asteroid@ roid = sys.asteroids[n]; if(roid.owner.valid) continue; if(roid.getAvailableCount() == 0) continue; if(isGettingAsteroid(roid)) continue; closest = dist; @best = roid; break; } } cnt = systems.outsideBorder.length; offset = randomi(0, cnt-1); for(uint i = 0, check = min(3, cnt); i < check; ++i) { auto@ sys = systems.outsideBorder[(i+offset)%cnt]; double dist = sys.obj.position.distanceToSQ(f.obj.position); if(dist > closest) continue; if(!sys.obj.sharesTerritory(ai.empire, reg)) continue; for(uint n = 0, ncnt = sys.asteroids.length; n < ncnt; ++n) { Asteroid@ roid = sys.asteroids[n]; if(roid.owner.valid) continue; if(roid.getAvailableCount() == 0) continue; if(isGettingAsteroid(roid)) continue; closest = dist; @best = roid; break; } } return best; } }; double asteroidResourceValue(const ResourceType@ type) { if(type is null) return 0.0; double w = 1.0; w += type.level * 10.0; w += type.totalPressure; if(type.cls !is null) w += 5.0; return w; } AIComponent@ createConstruction() { return Construction(); }