Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackManager.js (revision 25875) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackManager.js (revision 25876) @@ -1,803 +1,803 @@ /** * Attack Manager */ PETRA.AttackManager = function(Config) { this.Config = Config; this.totalNumber = 0; this.attackNumber = 0; this.rushNumber = 0; this.raidNumber = 0; this.upcomingAttacks = { "Rush": [], "Raid": [], "Attack": [], "HugeAttack": [] }; this.startedAttacks = { "Rush": [], "Raid": [], "Attack": [], "HugeAttack": [] }; this.bombingAttacks = new Map();// Temporary attacks for siege units while waiting their current attack to start this.debugTime = 0; this.maxRushes = 0; this.rushSize = []; this.currentEnemyPlayer = undefined; // enemy player we are currently targeting this.defeated = {}; }; /** More initialisation for stuff that needs the gameState */ PETRA.AttackManager.prototype.init = function(gameState) { this.outOfPlan = gameState.getOwnUnits().filter(API3.Filters.byMetadata(PlayerID, "plan", -1)); this.outOfPlan.registerUpdates(); }; PETRA.AttackManager.prototype.setRushes = function(allowed) { if (this.Config.personality.aggressive > this.Config.personalityCut.strong && allowed > 2) { this.maxRushes = 3; this.rushSize = [ 16, 20, 24 ]; } else if (this.Config.personality.aggressive > this.Config.personalityCut.medium && allowed > 1) { this.maxRushes = 2; this.rushSize = [ 18, 22 ]; } else if (this.Config.personality.aggressive > this.Config.personalityCut.weak && allowed > 0) { this.maxRushes = 1; this.rushSize = [ 20 ]; } }; PETRA.AttackManager.prototype.checkEvents = function(gameState, events) { for (let evt of events.PlayerDefeated) this.defeated[evt.playerId] = true; let answer = "decline"; let other; let targetPlayer; for (let evt of events.AttackRequest) { if (evt.source === PlayerID || !gameState.isPlayerAlly(evt.source) || !gameState.isPlayerEnemy(evt.player)) continue; targetPlayer = evt.player; let available = 0; for (let attackType in this.upcomingAttacks) { for (let attack of this.upcomingAttacks[attackType]) { if (attack.state === "completing") { if (attack.targetPlayer === targetPlayer) available += attack.unitCollection.length; else if (attack.targetPlayer !== undefined && attack.targetPlayer !== targetPlayer) other = attack.targetPlayer; continue; } attack.targetPlayer = targetPlayer; if (attack.unitCollection.length > 2) available += attack.unitCollection.length; } } if (available > 12) // launch the attack immediately { for (let attackType in this.upcomingAttacks) { for (let attack of this.upcomingAttacks[attackType]) { if (attack.state === "completing" || attack.targetPlayer !== targetPlayer || attack.unitCollection.length < 3) continue; attack.forceStart(); attack.requested = true; } } answer = "join"; } else if (other !== undefined) answer = "other"; break; // take only the first attack request into account } if (targetPlayer !== undefined) PETRA.chatAnswerRequestAttack(gameState, targetPlayer, answer, other); for (let evt of events.EntityRenamed) // take care of packing units in bombing attacks { for (let [targetId, unitIds] of this.bombingAttacks) { if (targetId == evt.entity) { this.bombingAttacks.set(evt.newentity, unitIds); this.bombingAttacks.delete(evt.entity); } else if (unitIds.has(evt.entity)) { unitIds.add(evt.newentity); unitIds.delete(evt.entity); } } } }; /** * Check for any structure in range from within our territory, and bomb it */ PETRA.AttackManager.prototype.assignBombers = function(gameState) { // First some cleaning of current bombing attacks for (let [targetId, unitIds] of this.bombingAttacks) { let target = gameState.getEntityById(targetId); if (!target || !gameState.isPlayerEnemy(target.owner())) this.bombingAttacks.delete(targetId); else { for (let entId of unitIds.values()) { let ent = gameState.getEntityById(entId); if (ent && ent.owner() == PlayerID) { let plan = ent.getMetadata(PlayerID, "plan"); let orders = ent.unitAIOrderData(); let lastOrder = orders && orders.length ? orders[orders.length-1] : null; if (lastOrder && lastOrder.target && lastOrder.target == targetId && plan != -2 && plan != -3) continue; } unitIds.delete(entId); } if (!unitIds.size) this.bombingAttacks.delete(targetId); } } const bombers = gameState.updatingCollection("bombers", API3.Filters.byClasses(["BoltShooter", "StoneThrower"]), gameState.getOwnUnits()); for (let ent of bombers.values()) { if (!ent.position() || !ent.isIdle() || !ent.attackRange("Ranged")) continue; if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3) continue; if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") != -1) { let subrole = ent.getMetadata(PlayerID, "subrole"); if (subrole && (subrole == "completing" || subrole == "walking" || subrole == "attacking")) continue; } let alreadyBombing = false; for (let unitIds of this.bombingAttacks.values()) { if (!unitIds.has(ent.id())) continue; alreadyBombing = true; break; } if (alreadyBombing) break; let range = ent.attackRange("Ranged").max; let entPos = ent.position(); let access = PETRA.getLandAccess(gameState, ent); for (let struct of gameState.getEnemyStructures().values()) { if (!ent.canAttackTarget(struct, PETRA.allowCapture(gameState, ent, struct))) continue; let structPos = struct.position(); let x; let z; if (struct.hasClass("Field")) { if (!struct.resourceSupplyNumGatherers() || !gameState.isPlayerEnemy(gameState.ai.HQ.territoryMap.getOwner(structPos))) continue; } let dist = API3.VectorDistance(entPos, structPos); if (dist > range) { let safety = struct.footprintRadius() + 30; x = structPos[0] + (entPos[0] - structPos[0]) * safety / dist; z = structPos[1] + (entPos[1] - structPos[1]) * safety / dist; let owner = gameState.ai.HQ.territoryMap.getOwner([x, z]); if (owner != 0 && gameState.isPlayerEnemy(owner)) continue; x = structPos[0] + (entPos[0] - structPos[0]) * range / dist; z = structPos[1] + (entPos[1] - structPos[1]) * range / dist; if (gameState.ai.HQ.territoryMap.getOwner([x, z]) != PlayerID || gameState.ai.accessibility.getAccessValue([x, z]) != access) continue; } let attackingUnits; for (let [targetId, unitIds] of this.bombingAttacks) { if (targetId != struct.id()) continue; attackingUnits = unitIds; break; } if (attackingUnits && attackingUnits.size > 4) continue; // already enough units against that target if (!attackingUnits) { attackingUnits = new Set(); this.bombingAttacks.set(struct.id(), attackingUnits); } attackingUnits.add(ent.id()); if (dist > range) ent.move(x, z); ent.attack(struct.id(), false, dist > range); break; } } }; /** * Some functions are run every turn * Others once in a while */ PETRA.AttackManager.prototype.update = function(gameState, queues, events) { if (this.Config.debug > 2 && gameState.ai.elapsedTime > this.debugTime + 60) { this.debugTime = gameState.ai.elapsedTime; API3.warn(" upcoming attacks ================="); for (let attackType in this.upcomingAttacks) for (let attack of this.upcomingAttacks[attackType]) API3.warn(" plan " + attack.name + " type " + attackType + " state " + attack.state + " units " + attack.unitCollection.length); API3.warn(" started attacks =================="); for (let attackType in this.startedAttacks) for (let attack of this.startedAttacks[attackType]) API3.warn(" plan " + attack.name + " type " + attackType + " state " + attack.state + " units " + attack.unitCollection.length); API3.warn(" =================================="); } this.checkEvents(gameState, events); let unexecutedAttacks = { "Rush": 0, "Raid": 0, "Attack": 0, "HugeAttack": 0 }; for (let attackType in this.upcomingAttacks) { for (let i = 0; i < this.upcomingAttacks[attackType].length; ++i) { let attack = this.upcomingAttacks[attackType][i]; attack.checkEvents(gameState, events); if (attack.isStarted()) API3.warn("Petra problem in attackManager: attack in preparation has already started ???"); let updateStep = attack.updatePreparation(gameState); // now we're gonna check if the preparation time is over if (updateStep == 1 || attack.isPaused()) { // just chillin' if (attack.state == "unexecuted") ++unexecutedAttacks[attackType]; } else if (updateStep == 0) { if (this.Config.debug > 1) API3.warn("Attack Manager: " + attack.getType() + " plan " + attack.getName() + " aborted."); attack.Abort(gameState); this.upcomingAttacks[attackType].splice(i--, 1); } else if (updateStep == 2) { if (attack.StartAttack(gameState)) { if (this.Config.debug > 1) API3.warn("Attack Manager: Starting " + attack.getType() + " plan " + attack.getName()); if (this.Config.chat) PETRA.chatLaunchAttack(gameState, attack.targetPlayer, attack.getType()); this.startedAttacks[attackType].push(attack); } else attack.Abort(gameState); this.upcomingAttacks[attackType].splice(i--, 1); } } } for (let attackType in this.startedAttacks) { for (let i = 0; i < this.startedAttacks[attackType].length; ++i) { let attack = this.startedAttacks[attackType][i]; attack.checkEvents(gameState, events); // okay so then we'll update the attack. if (attack.isPaused()) continue; let remaining = attack.update(gameState, events); if (!remaining) { if (this.Config.debug > 1) API3.warn("Military Manager: " + attack.getType() + " plan " + attack.getName() + " is finished with remaining " + remaining); attack.Abort(gameState); this.startedAttacks[attackType].splice(i--, 1); } } } // creating plans after updating because an aborted plan might be reused in that case. let barracksNb = gameState.getOwnEntitiesByClass("Barracks", true).filter(API3.Filters.isBuilt()).length; if (this.rushNumber < this.maxRushes && barracksNb >= 1) { if (unexecutedAttacks.Rush === 0) { // we have a barracks and we want to rush, rush. let data = { "targetSize": this.rushSize[this.rushNumber] }; let attackPlan = new PETRA.AttackPlan(gameState, this.Config, this.totalNumber, "Rush", data); if (!attackPlan.failed) { if (this.Config.debug > 1) API3.warn("Military Manager: Rushing plan " + this.totalNumber + " with maxRushes " + this.maxRushes); this.totalNumber++; attackPlan.init(gameState); this.upcomingAttacks.Rush.push(attackPlan); } this.rushNumber++; } } else if (unexecutedAttacks.Attack == 0 && unexecutedAttacks.HugeAttack == 0 && this.startedAttacks.Attack.length + this.startedAttacks.HugeAttack.length < Math.min(2, 1 + Math.round(gameState.getPopulationMax()/100)) && (this.startedAttacks.Attack.length + this.startedAttacks.HugeAttack.length == 0 || gameState.getPopulationMax() - gameState.getPopulation() > 12)) { if (barracksNb >= 1 && (gameState.currentPhase() > 1 || gameState.isResearching(gameState.getPhaseName(2))) || - !gameState.ai.HQ.baseManagers[1]) // if we have no base ... nothing else to do than attack + !gameState.ai.HQ.hasPotentialBase()) // if we have no base ... nothing else to do than attack { let type = this.attackNumber < 2 || this.startedAttacks.HugeAttack.length > 0 ? "Attack" : "HugeAttack"; let attackPlan = new PETRA.AttackPlan(gameState, this.Config, this.totalNumber, type); if (attackPlan.failed) this.attackPlansEncounteredWater = true; // hack else { if (this.Config.debug > 1) API3.warn("Military Manager: Creating the plan " + type + " " + this.totalNumber); this.totalNumber++; attackPlan.init(gameState); this.upcomingAttacks[type].push(attackPlan); } this.attackNumber++; } } if (unexecutedAttacks.Raid === 0 && gameState.ai.HQ.defenseManager.targetList.length) { let target; for (let targetId of gameState.ai.HQ.defenseManager.targetList) { target = gameState.getEntityById(targetId); if (!target) continue; if (gameState.isPlayerEnemy(target.owner())) break; target = undefined; } if (target) // prepare a raid against this target this.raidTargetEntity(gameState, target); } // Check if we have some unused ranged siege unit which could do something useful while waiting if (this.Config.difficulty > 1 && gameState.ai.playedTurn % 5 == 0) this.assignBombers(gameState); }; PETRA.AttackManager.prototype.getPlan = function(planName) { for (let attackType in this.upcomingAttacks) { for (let attack of this.upcomingAttacks[attackType]) if (attack.getName() == planName) return attack; } for (let attackType in this.startedAttacks) { for (let attack of this.startedAttacks[attackType]) if (attack.getName() == planName) return attack; } return undefined; }; PETRA.AttackManager.prototype.pausePlan = function(planName) { let attack = this.getPlan(planName); if (attack) attack.setPaused(true); }; PETRA.AttackManager.prototype.unpausePlan = function(planName) { let attack = this.getPlan(planName); if (attack) attack.setPaused(false); }; PETRA.AttackManager.prototype.pauseAllPlans = function() { for (let attackType in this.upcomingAttacks) for (let attack of this.upcomingAttacks[attackType]) attack.setPaused(true); for (let attackType in this.startedAttacks) for (let attack of this.startedAttacks[attackType]) attack.setPaused(true); }; PETRA.AttackManager.prototype.unpauseAllPlans = function() { for (let attackType in this.upcomingAttacks) for (let attack of this.upcomingAttacks[attackType]) attack.setPaused(false); for (let attackType in this.startedAttacks) for (let attack of this.startedAttacks[attackType]) attack.setPaused(false); }; PETRA.AttackManager.prototype.getAttackInPreparation = function(type) { return this.upcomingAttacks[type].length ? this.upcomingAttacks[type][0] : undefined; }; /** * Determine which player should be attacked: when called when starting the attack, * attack.targetPlayer is undefined and in that case, we keep track of the chosen target * for future attacks. */ PETRA.AttackManager.prototype.getEnemyPlayer = function(gameState, attack) { let enemyPlayer; // First check if there is a preferred enemy based on our victory conditions. // If both wonder and relic, choose randomly between them TODO should combine decisions if (gameState.getVictoryConditions().has("wonder")) enemyPlayer = this.getWonderEnemyPlayer(gameState, attack); if (gameState.getVictoryConditions().has("capture_the_relic")) if (!enemyPlayer || randBool()) enemyPlayer = this.getRelicEnemyPlayer(gameState, attack) || enemyPlayer; if (enemyPlayer) return enemyPlayer; let veto = {}; for (let i in this.defeated) veto[i] = true; // No rush if enemy too well defended (i.e. iberians) if (attack.type == "Rush") { for (let i = 1; i < gameState.sharedScript.playersData.length; ++i) { if (!gameState.isPlayerEnemy(i) || veto[i]) continue; if (this.defeated[i]) continue; let enemyDefense = 0; for (let ent of gameState.getEnemyStructures(i).values()) if (ent.hasClasses(["Tower", "WallTower", "Fortress"])) enemyDefense++; if (enemyDefense > 6) veto[i] = true; } } // then if not a huge attack, continue attacking our previous target as long as it has some entities, // otherwise target the most accessible one if (attack.type != "HugeAttack") { if (attack.targetPlayer === undefined && this.currentEnemyPlayer !== undefined && !this.defeated[this.currentEnemyPlayer] && gameState.isPlayerEnemy(this.currentEnemyPlayer) && gameState.getEntities(this.currentEnemyPlayer).hasEntities()) return this.currentEnemyPlayer; let distmin; let ccmin; let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); for (let ourcc of ccEnts.values()) { if (ourcc.owner() != PlayerID) continue; let ourPos = ourcc.position(); let access = PETRA.getLandAccess(gameState, ourcc); for (let enemycc of ccEnts.values()) { if (veto[enemycc.owner()]) continue; if (!gameState.isPlayerEnemy(enemycc.owner())) continue; if (access != PETRA.getLandAccess(gameState, enemycc)) continue; let dist = API3.SquareVectorDistance(ourPos, enemycc.position()); if (distmin && dist > distmin) continue; ccmin = enemycc; distmin = dist; } } if (ccmin) { enemyPlayer = ccmin.owner(); if (attack.targetPlayer === undefined) this.currentEnemyPlayer = enemyPlayer; return enemyPlayer; } } // then let's target our strongest enemy (basically counting enemies units) // with priority to enemies with civ center let max = 0; for (let i = 1; i < gameState.sharedScript.playersData.length; ++i) { if (veto[i]) continue; if (!gameState.isPlayerEnemy(i)) continue; let enemyCount = 0; let enemyCivCentre = false; for (let ent of gameState.getEntities(i).values()) { enemyCount++; if (ent.hasClass("CivCentre")) enemyCivCentre = true; } if (enemyCivCentre) enemyCount += 500; if (!enemyCount || enemyCount < max) continue; max = enemyCount; enemyPlayer = i; } if (attack.targetPlayer === undefined) this.currentEnemyPlayer = enemyPlayer; return enemyPlayer; }; /** * Target the player with the most advanced wonder. * TODO currently the first built wonder is kept, should chek on the minimum wonderDuration left instead. */ PETRA.AttackManager.prototype.getWonderEnemyPlayer = function(gameState, attack) { let enemyPlayer; let enemyWonder; let moreAdvanced; for (let wonder of gameState.getEnemyStructures().filter(API3.Filters.byClass("Wonder")).values()) { if (wonder.owner() == 0) continue; let progress = wonder.foundationProgress(); if (progress === undefined) { enemyWonder = wonder; break; } if (enemyWonder && moreAdvanced > progress) continue; enemyWonder = wonder; moreAdvanced = progress; } if (enemyWonder) { enemyPlayer = enemyWonder.owner(); if (attack.targetPlayer === undefined) this.currentEnemyPlayer = enemyPlayer; } return enemyPlayer; }; /** * Target the player with the most relics (including gaia). */ PETRA.AttackManager.prototype.getRelicEnemyPlayer = function(gameState, attack) { let enemyPlayer; let allRelics = gameState.updatingGlobalCollection("allRelics", API3.Filters.byClass("Relic")); let maxRelicsOwned = 0; for (let i = 0; i < gameState.sharedScript.playersData.length; ++i) { if (!gameState.isPlayerEnemy(i) || this.defeated[i] || i == 0 && !gameState.ai.HQ.victoryManager.tryCaptureGaiaRelic) continue; let relicsCount = allRelics.filter(relic => relic.owner() == i).length; if (relicsCount <= maxRelicsOwned) continue; maxRelicsOwned = relicsCount; enemyPlayer = i; } if (enemyPlayer !== undefined) { if (attack.targetPlayer === undefined) this.currentEnemyPlayer = enemyPlayer; if (enemyPlayer == 0) gameState.ai.HQ.victoryManager.resetCaptureGaiaRelic(gameState); } return enemyPlayer; }; /** f.e. if we have changed diplomacy with another player. */ PETRA.AttackManager.prototype.cancelAttacksAgainstPlayer = function(gameState, player) { for (let attackType in this.upcomingAttacks) for (let attack of this.upcomingAttacks[attackType]) if (attack.targetPlayer === player) attack.targetPlayer = undefined; for (let attackType in this.startedAttacks) for (let i = 0; i < this.startedAttacks[attackType].length; ++i) { let attack = this.startedAttacks[attackType][i]; if (attack.targetPlayer === player) { attack.Abort(gameState); this.startedAttacks[attackType].splice(i--, 1); } } }; PETRA.AttackManager.prototype.raidTargetEntity = function(gameState, ent) { let data = { "target": ent }; let attackPlan = new PETRA.AttackPlan(gameState, this.Config, this.totalNumber, "Raid", data); if (attackPlan.failed) return null; if (this.Config.debug > 1) API3.warn("Military Manager: Raiding plan " + this.totalNumber); this.raidNumber++; this.totalNumber++; attackPlan.init(gameState); this.upcomingAttacks.Raid.push(attackPlan); return attackPlan; }; /** * Return the number of units from any of our attacking armies around this position */ PETRA.AttackManager.prototype.numAttackingUnitsAround = function(pos, dist) { let num = 0; for (let attackType in this.startedAttacks) for (let attack of this.startedAttacks[attackType]) { if (!attack.position) // this attack may be inside a transport continue; if (API3.SquareVectorDistance(pos, attack.position) < dist*dist) num += attack.unitCollection.length; } return num; }; /** * Switch defense armies into an attack one against the given target * data.range: transform all defense armies inside range of the target into a new attack * data.armyID: transform only the defense army ID into a new attack * data.uniqueTarget: the attack will stop when the target is destroyed or captured */ PETRA.AttackManager.prototype.switchDefenseToAttack = function(gameState, target, data) { if (!target || !target.position()) return false; if (!data.range && !data.armyID) { API3.warn(" attackManager.switchDefenseToAttack inconsistent data " + uneval(data)); return false; } let attackData = data.uniqueTarget ? { "uniqueTargetId": target.id() } : undefined; let pos = target.position(); let attackType = "Attack"; let attackPlan = new PETRA.AttackPlan(gameState, this.Config, this.totalNumber, attackType, attackData); if (attackPlan.failed) return false; this.totalNumber++; attackPlan.init(gameState); this.startedAttacks[attackType].push(attackPlan); let targetAccess = PETRA.getLandAccess(gameState, target); for (let army of gameState.ai.HQ.defenseManager.armies) { if (data.range) { army.recalculatePosition(gameState); if (API3.SquareVectorDistance(pos, army.foePosition) > data.range * data.range) continue; } else if (army.ID != +data.armyID) continue; while (army.foeEntities.length > 0) army.removeFoe(gameState, army.foeEntities[0]); while (army.ownEntities.length > 0) { let unitId = army.ownEntities[0]; army.removeOwn(gameState, unitId); let unit = gameState.getEntityById(unitId); let accessOk = unit.getMetadata(PlayerID, "transport") !== undefined || unit.position() && PETRA.getLandAccess(gameState, unit) == targetAccess; if (unit && accessOk && attackPlan.isAvailableUnit(gameState, unit)) { unit.setMetadata(PlayerID, "plan", attackPlan.name); unit.setMetadata(PlayerID, "role", "attack"); attackPlan.unitCollection.updateEnt(unit); } } } if (!attackPlan.unitCollection.hasEntities()) { attackPlan.Abort(gameState); return false; } for (let unit of attackPlan.unitCollection.values()) unit.setMetadata(PlayerID, "role", "attack"); attackPlan.targetPlayer = target.owner(); attackPlan.targetPos = pos; attackPlan.target = target; attackPlan.state = "arrived"; return true; }; PETRA.AttackManager.prototype.Serialize = function() { let properties = { "totalNumber": this.totalNumber, "attackNumber": this.attackNumber, "rushNumber": this.rushNumber, "raidNumber": this.raidNumber, "debugTime": this.debugTime, "maxRushes": this.maxRushes, "rushSize": this.rushSize, "currentEnemyPlayer": this.currentEnemyPlayer, "defeated": this.defeated }; let upcomingAttacks = {}; for (let key in this.upcomingAttacks) { upcomingAttacks[key] = []; for (let attack of this.upcomingAttacks[key]) upcomingAttacks[key].push(attack.Serialize()); } let startedAttacks = {}; for (let key in this.startedAttacks) { startedAttacks[key] = []; for (let attack of this.startedAttacks[key]) startedAttacks[key].push(attack.Serialize()); } return { "properties": properties, "upcomingAttacks": upcomingAttacks, "startedAttacks": startedAttacks }; }; PETRA.AttackManager.prototype.Deserialize = function(gameState, data) { for (let key in data.properties) this[key] = data.properties[key]; this.upcomingAttacks = {}; for (let key in data.upcomingAttacks) { this.upcomingAttacks[key] = []; for (let dataAttack of data.upcomingAttacks[key]) { let attack = new PETRA.AttackPlan(gameState, this.Config, dataAttack.properties.name); attack.Deserialize(gameState, dataAttack); attack.init(gameState); this.upcomingAttacks[key].push(attack); } } this.startedAttacks = {}; for (let key in data.startedAttacks) { this.startedAttacks[key] = []; for (let dataAttack of data.startedAttacks[key]) { let attack = new PETRA.AttackPlan(gameState, this.Config, dataAttack.properties.name); attack.Deserialize(gameState, dataAttack); attack.init(gameState); this.startedAttacks[key].push(attack); } } }; Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackPlan.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackPlan.js (revision 25875) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackPlan.js (revision 25876) @@ -1,2188 +1,2188 @@ /** * This is an attack plan: * It deals with everything in an attack, from picking a target to picking a path to it * To making sure units are built, and pushing elements to the queue manager otherwise * It also handles the actual attack, though much work is needed on that. */ PETRA.AttackPlan = function(gameState, Config, uniqueID, type, data) { this.Config = Config; this.name = uniqueID; this.type = type || "Attack"; this.state = "unexecuted"; this.forced = false; // true when this attacked has been forced to help an ally if (data && data.target) { this.target = data.target; this.targetPos = this.target.position(); this.targetPlayer = this.target.owner(); } else { this.target = undefined; this.targetPos = undefined; this.targetPlayer = undefined; } this.uniqueTargetId = data && data.uniqueTargetId || undefined; // get a starting rallyPoint ... will be improved later let rallyPoint; let rallyAccess; let allAccesses = {}; - for (let base of gameState.ai.HQ.baseManagers) + for (const base of gameState.ai.HQ.baseManagers()) { if (!base.anchor || !base.anchor.position()) continue; let access = PETRA.getLandAccess(gameState, base.anchor); if (!rallyPoint) { rallyPoint = base.anchor.position(); rallyAccess = access; } if (!allAccesses[access]) allAccesses[access] = base.anchor.position(); } if (!rallyPoint) // no base ? take the position of any of our entities { for (let ent of gameState.getOwnEntities().values()) { if (!ent.position()) continue; let access = PETRA.getLandAccess(gameState, ent); rallyPoint = ent.position(); rallyAccess = access; allAccesses[access] = rallyPoint; break; } if (!rallyPoint) { this.failed = true; return false; } } this.rallyPoint = rallyPoint; this.overseas = 0; if (gameState.ai.HQ.navalMap) { for (let structure of gameState.getEnemyStructures().values()) { if (this.target && structure.id() != this.target.id()) continue; if (!structure.position()) continue; let access = PETRA.getLandAccess(gameState, structure); if (access in allAccesses) { this.overseas = 0; this.rallyPoint = allAccesses[access]; break; } else if (!this.overseas) { let sea = gameState.ai.HQ.getSeaBetweenIndices(gameState, rallyAccess, access); if (!sea) { if (this.target) { API3.warn("Petra: " + this.type + " " + this.name + " has an inaccessible target " + this.target.templateName() + " indices " + rallyAccess + " " + access); this.failed = true; return false; } continue; } this.overseas = sea; gameState.ai.HQ.navalManager.setMinimalTransportShips(gameState, sea, 1); } } } this.paused = false; this.maxCompletingTime = 0; // priority of the queues we'll create. let priority = 70; // unitStat priority is relative. If all are 0, the only relevant criteria is "currentsize/targetsize". // if not, this is a "bonus". The higher the priority, the faster this unit will get built. // Should really be clamped to [0.1-1.5] (assuming 1 is default/the norm) // Eg: if all are priority 1, and the siege is 0.5, the siege units will get built // only once every other category is at least 50% of its target size. // note: siege build order is currently added by the military manager if a fortress is there. this.unitStat = {}; // neededShips is the minimal number of ships which should be available for transport if (type == "Rush") { priority = 250; this.unitStat.Infantry = { "priority": 1, "minSize": 10, "targetSize": 20, "batchSize": 2, "classes": ["Infantry"], "interests": [["strength", 1], ["costsResource", 0.5, "stone"], ["costsResource", 0.6, "metal"]] }; this.unitStat.FastMoving = { "priority": 1, "minSize": 2, "targetSize": 4, "batchSize": 2, "classes": ["FastMoving+CitizenSoldier"], "interests": [["strength", 1]] }; if (data && data.targetSize) this.unitStat.Infantry.targetSize = data.targetSize; this.neededShips = 1; } else if (type == "Raid") { priority = 150; this.unitStat.FastMoving = { "priority": 1, "minSize": 3, "targetSize": 4, "batchSize": 2, "classes": ["FastMoving+CitizenSoldier"], "interests": [ ["strength", 1] ] }; this.neededShips = 1; } else if (type == "HugeAttack") { priority = 90; // basically we want a mix of citizen soldiers so our barracks have a purpose, and champion units. this.unitStat.RangedInfantry = { "priority": 0.7, "minSize": 5, "targetSize": 20, "batchSize": 5, "classes": ["Infantry+Ranged+CitizenSoldier"], "interests": [["strength", 3]] }; this.unitStat.MeleeInfantry = { "priority": 0.7, "minSize": 5, "targetSize": 20, "batchSize": 5, "classes": ["Infantry+Melee+CitizenSoldier"], "interests": [["strength", 3]] }; this.unitStat.ChampRangedInfantry = { "priority": 1, "minSize": 3, "targetSize": 18, "batchSize": 3, "classes": ["Infantry+Ranged+Champion"], "interests": [["strength", 3]] }; this.unitStat.ChampMeleeInfantry = { "priority": 1, "minSize": 3, "targetSize": 18, "batchSize": 3, "classes": ["Infantry+Melee+Champion"], "interests": [["strength", 3]] }; this.unitStat.RangedFastMoving = { "priority": 0.7, "minSize": 4, "targetSize": 20, "batchSize": 4, "classes": ["FastMoving+Ranged+CitizenSoldier"], "interests": [["strength", 2]] }; this.unitStat.MeleeFastMoving = { "priority": 0.7, "minSize": 4, "targetSize": 20, "batchSize": 4, "classes": ["FastMoving+Melee+CitizenSoldier"], "interests": [["strength", 2]] }; this.unitStat.ChampRangedFastMoving = { "priority": 1, "minSize": 3, "targetSize": 15, "batchSize": 3, "classes": ["FastMoving+Ranged+Champion"], "interests": [["strength", 3]] }; this.unitStat.ChampMeleeFastMoving = { "priority": 1, "minSize": 3, "targetSize": 15, "batchSize": 3, "classes": ["FastMoving+Melee+Champion"], "interests": [["strength", 2]] }; this.unitStat.Hero = { "priority": 1, "minSize": 0, "targetSize": 1, "batchSize": 1, "classes": ["Hero"], "interests": [["strength", 2]] }; this.neededShips = 5; } else { priority = 70; this.unitStat.RangedInfantry = { "priority": 1, "minSize": 6, "targetSize": 16, "batchSize": 3, "classes": ["Infantry+Ranged"], "interests": [["canGather", 1], ["strength", 1.6], ["costsResource", 0.3, "stone"], ["costsResource", 0.3, "metal"]] }; this.unitStat.MeleeInfantry = { "priority": 1, "minSize": 6, "targetSize": 16, "batchSize": 3, "classes": ["Infantry+Melee"], "interests": [["canGather", 1], ["strength", 1.6], ["costsResource", 0.3, "stone"], ["costsResource", 0.3, "metal"]] }; this.unitStat.FastMoving = { "priority": 1, "minSize": 2, "targetSize": 6, "batchSize": 2, "classes": ["FastMoving+CitizenSoldier"], "interests": [["strength", 1]] }; this.neededShips = 3; } // Put some randomness on the attack size let variation = randFloat(0.8, 1.2); // and lower priority and smaller sizes for easier difficulty levels if (this.Config.difficulty < 2) { priority *= 0.4; variation *= 0.2; } else if (this.Config.difficulty < 3) { priority *= 0.8; variation *= 0.6; } if (this.Config.difficulty < 2) { for (const cat in this.unitStat) { this.unitStat[cat].targetSize = Math.ceil(variation * this.unitStat[cat].targetSize); this.unitStat[cat].minSize = Math.min(this.unitStat[cat].targetSize, Math.min(this.unitStat[cat].minSize, 2)); this.unitStat[cat].batchSize = this.unitStat[cat].minSize; } } else { for (const cat in this.unitStat) { this.unitStat[cat].targetSize = Math.ceil(variation * this.unitStat[cat].targetSize); this.unitStat[cat].minSize = Math.min(this.unitStat[cat].minSize, this.unitStat[cat].targetSize); } } // change the sizes according to max population this.neededShips = Math.ceil(this.Config.popScaling * this.neededShips); for (let cat in this.unitStat) { this.unitStat[cat].targetSize = Math.ceil(this.Config.popScaling * this.unitStat[cat].targetSize); this.unitStat[cat].minSize = Math.ceil(this.Config.popScaling * this.unitStat[cat].minSize); } // TODO: there should probably be one queue per type of training building gameState.ai.queueManager.addQueue("plan_" + this.name, priority); gameState.ai.queueManager.addQueue("plan_" + this.name +"_champ", priority+1); gameState.ai.queueManager.addQueue("plan_" + this.name +"_siege", priority); // each array is [ratio, [associated classes], associated EntityColl, associated unitStat, name ] this.buildOrders = []; this.canBuildUnits = gameState.ai.HQ.canBuildUnits; this.siegeState = 0; // 0 = not yet tested, 1 = not yet any siege trainer, 2 = siege added in build orders // some variables used during the attack this.position5TurnsAgo = [0, 0]; this.lastPosition = [0, 0]; this.position = [0, 0]; this.isBlocked = false; // true when this attack faces walls return true; }; PETRA.AttackPlan.prototype.init = function(gameState) { this.queue = gameState.ai.queues["plan_" + this.name]; this.queueChamp = gameState.ai.queues["plan_" + this.name +"_champ"]; this.queueSiege = gameState.ai.queues["plan_" + this.name +"_siege"]; this.unitCollection = gameState.getOwnUnits().filter(API3.Filters.byMetadata(PlayerID, "plan", this.name)); this.unitCollection.registerUpdates(); this.unit = {}; // defining the entity collections. Will look for units I own, that are part of this plan. // Also defining the buildOrders. for (let cat in this.unitStat) { let Unit = this.unitStat[cat]; this.unit[cat] = this.unitCollection.filter(API3.Filters.byClasses(Unit.classes)); this.unit[cat].registerUpdates(); if (this.canBuildUnits) this.buildOrders.push([0, Unit.classes, this.unit[cat], Unit, cat]); } }; PETRA.AttackPlan.prototype.getName = function() { return this.name; }; PETRA.AttackPlan.prototype.getType = function() { return this.type; }; PETRA.AttackPlan.prototype.isStarted = function() { return this.state !== "unexecuted" && this.state !== "completing"; }; PETRA.AttackPlan.prototype.isPaused = function() { return this.paused; }; PETRA.AttackPlan.prototype.setPaused = function(boolValue) { this.paused = boolValue; }; /** * Returns true if the attack can be executed at the current time * Basically it checks we have enough units. */ PETRA.AttackPlan.prototype.canStart = function() { if (!this.canBuildUnits) return true; for (let unitCat in this.unitStat) if (this.unit[unitCat].length < this.unitStat[unitCat].minSize) return false; return true; }; PETRA.AttackPlan.prototype.mustStart = function() { if (this.isPaused()) return false; if (!this.canBuildUnits) return this.unitCollection.hasEntities(); let MaxReachedEverywhere = true; let MinReachedEverywhere = true; for (let unitCat in this.unitStat) { let Unit = this.unitStat[unitCat]; if (this.unit[unitCat].length < Unit.targetSize) MaxReachedEverywhere = false; if (this.unit[unitCat].length < Unit.minSize) { MinReachedEverywhere = false; break; } } if (MaxReachedEverywhere) return true; if (MinReachedEverywhere) return this.type == "Raid" && this.target && this.target.foundationProgress() && this.target.foundationProgress() > 50; return false; }; PETRA.AttackPlan.prototype.forceStart = function() { for (let unitCat in this.unitStat) { let Unit = this.unitStat[unitCat]; Unit.targetSize = 0; Unit.minSize = 0; } this.forced = true; }; PETRA.AttackPlan.prototype.emptyQueues = function() { this.queue.empty(); this.queueChamp.empty(); this.queueSiege.empty(); }; PETRA.AttackPlan.prototype.removeQueues = function(gameState) { gameState.ai.queueManager.removeQueue("plan_" + this.name); gameState.ai.queueManager.removeQueue("plan_" + this.name + "_champ"); gameState.ai.queueManager.removeQueue("plan_" + this.name + "_siege"); }; /** Adds a build order. If resetQueue is true, this will reset the queue. */ PETRA.AttackPlan.prototype.addBuildOrder = function(gameState, name, unitStats, resetQueue) { if (!this.isStarted()) { // no minsize as we don't want the plan to fail at the last minute though. this.unitStat[name] = unitStats; let Unit = this.unitStat[name]; this.unit[name] = this.unitCollection.filter(API3.Filters.byClasses(Unit.classes)); this.unit[name].registerUpdates(); this.buildOrders.push([0, Unit.classes, this.unit[name], Unit, name]); if (resetQueue) this.emptyQueues(); } }; PETRA.AttackPlan.prototype.addSiegeUnits = function(gameState) { if (this.siegeState == 2 || this.state !== "unexecuted") return false; let civ = gameState.getPlayerCiv(); const classes = [["Siege+Melee"], ["Siege+Ranged"], ["Elephant+Melee"]]; let hasTrainer = [false, false, false]; for (let ent of gameState.getOwnTrainingFacilities().values()) { let trainables = ent.trainableEntities(civ); if (!trainables) continue; for (let trainable of trainables) { if (gameState.isTemplateDisabled(trainable)) continue; let template = gameState.getTemplate(trainable); if (!template || !template.available(gameState)) continue; for (let i = 0; i < classes.length; ++i) if (template.hasClasses(classes[i])) hasTrainer[i] = true; } } if (hasTrainer.every(e => !e)) return false; let i = this.name % classes.length; for (let k = 0; k < classes.length; ++k) { if (hasTrainer[i]) break; i = ++i % classes.length; } this.siegeState = 2; let targetSize; if (this.Config.difficulty < 3) targetSize = this.type == "HugeAttack" ? Math.max(this.Config.difficulty, 1) : Math.max(this.Config.difficulty - 1, 0); else targetSize = this.type == "HugeAttack" ? this.Config.difficulty + 1 : this.Config.difficulty - 1; targetSize = Math.max(Math.round(this.Config.popScaling * targetSize), this.type == "HugeAttack" ? 1 : 0); if (!targetSize) return true; // no minsize as we don't want the plan to fail at the last minute though. let stat = { "priority": 1, "minSize": 0, "targetSize": targetSize, "batchSize": Math.min(targetSize, 2), "classes": classes[i], "interests": [ ["siegeStrength", 3] ] }; this.addBuildOrder(gameState, "Siege", stat, true); return true; }; /** Three returns possible: 1 is "keep going", 0 is "failed plan", 2 is "start". */ PETRA.AttackPlan.prototype.updatePreparation = function(gameState) { // the completing step is used to return resources and regroup the units // so we check that we have no more forced order before starting the attack if (this.state == "completing") { // if our target was destroyed, go back to "unexecuted" state if (this.targetPlayer === undefined || !this.target || !gameState.getEntityById(this.target.id())) { this.state = "unexecuted"; this.target = undefined; } else { // check that all units have finished with their transport if needed if (this.waitingForTransport()) return 1; // bloqued units which cannot finish their order should not stop the attack if (gameState.ai.elapsedTime < this.maxCompletingTime && this.hasForceOrder()) return 1; return 2; } } if (this.Config.debug > 3 && gameState.ai.playedTurn % 50 === 0) this.debugAttack(); // if we need a transport, wait for some transport ships if (this.overseas && !gameState.ai.HQ.navalManager.seaTransportShips[this.overseas].length) return 1; if (this.type != "Raid" || !this.forced) // Forced Raids have special purposes (as relic capture) this.assignUnits(gameState); if (this.type != "Raid" && gameState.ai.HQ.attackManager.getAttackInPreparation("Raid") !== undefined) this.reassignFastUnit(gameState); // reassign some fast units (if any) to fasten raid preparations // Fasten the end game. if (gameState.ai.playedTurn % 5 == 0 && this.hasSiegeUnits()) { let totEnemies = 0; let hasEnemies = false; for (let i = 1; i < gameState.sharedScript.playersData.length; ++i) { if (!gameState.isPlayerEnemy(i) || gameState.ai.HQ.attackManager.defeated[i]) continue; hasEnemies = true; totEnemies += gameState.getEnemyUnits(i).length; } if (hasEnemies && this.unitCollection.length > 20 + 2 * totEnemies) this.forceStart(); } // special case: if we've reached max pop, and we can start the plan, start it. if (gameState.getPopulationMax() - gameState.getPopulation() < 5) { let lengthMin = 16; if (gameState.getPopulationMax() < 300) lengthMin -= Math.floor(8 * (300 - gameState.getPopulationMax()) / 300); if (this.canStart() || this.unitCollection.length > lengthMin) { this.emptyQueues(); } else // Abort the plan so that its units will be reassigned to other plans. { if (this.Config.debug > 1) { let am = gameState.ai.HQ.attackManager; API3.warn(" attacks upcoming: raid " + am.upcomingAttacks.Raid.length + " rush " + am.upcomingAttacks.Rush.length + " attack " + am.upcomingAttacks.Attack.length + " huge " + am.upcomingAttacks.HugeAttack.length); API3.warn(" attacks started: raid " + am.startedAttacks.Raid.length + " rush " + am.startedAttacks.Rush.length + " attack " + am.startedAttacks.Attack.length + " huge " + am.startedAttacks.HugeAttack.length); } return 0; } } else if (this.mustStart()) { if (gameState.countOwnQueuedEntitiesWithMetadata("plan", +this.name) > 0) { // keep on while the units finish being trained, then we'll start this.emptyQueues(); return 1; } } else { if (this.canBuildUnits) { // We still have time left to recruit units and do stuffs. if (this.siegeState == 0 || this.siegeState == 1 && gameState.ai.playedTurn % 5 == 0) this.addSiegeUnits(gameState); this.trainMoreUnits(gameState); // may happen if we have no more training facilities and build orders are canceled if (!this.buildOrders.length) return 0; // will abort the plan } return 1; } // if we're here, it means we must start this.state = "completing"; // Raids have their predefined target if (!this.target && !this.chooseTarget(gameState)) return 0; if (!this.overseas) this.getPathToTarget(gameState); if (this.type == "Raid") this.maxCompletingTime = this.forced ? 0 : gameState.ai.elapsedTime + 20; else { if (this.type == "Rush" || this.forced) this.maxCompletingTime = gameState.ai.elapsedTime + 40; else this.maxCompletingTime = gameState.ai.elapsedTime + 60; // warn our allies so that they can help if possible if (!this.requested) Engine.PostCommand(PlayerID, { "type": "attack-request", "source": PlayerID, "player": this.targetPlayer }); } // Remove those units which were in a temporary bombing attack for (let unitIds of gameState.ai.HQ.attackManager.bombingAttacks.values()) { for (let entId of unitIds.values()) { let ent = gameState.getEntityById(entId); if (!ent || ent.getMetadata(PlayerID, "plan") != this.name) continue; unitIds.delete(entId); ent.stopMoving(); } } let rallyPoint = this.rallyPoint; let rallyIndex = gameState.ai.accessibility.getAccessValue(rallyPoint); for (let ent of this.unitCollection.values()) { // For the time being, if occupied in a transport, remove the unit from this plan TODO improve that if (ent.getMetadata(PlayerID, "transport") !== undefined || ent.getMetadata(PlayerID, "transporter") !== undefined) { ent.setMetadata(PlayerID, "plan", -1); continue; } ent.setMetadata(PlayerID, "role", "attack"); ent.setMetadata(PlayerID, "subrole", "completing"); let queued = false; if (ent.resourceCarrying() && ent.resourceCarrying().length) queued = PETRA.returnResources(gameState, ent); let index = PETRA.getLandAccess(gameState, ent); if (index == rallyIndex) ent.moveToRange(rallyPoint[0], rallyPoint[1], 0, 15, queued); else gameState.ai.HQ.navalManager.requireTransport(gameState, ent, index, rallyIndex, rallyPoint); } // reset all queued units this.removeQueues(gameState); return 1; }; PETRA.AttackPlan.prototype.trainMoreUnits = function(gameState) { // let's sort by training advancement, ie 'current size / target size' // count the number of queued units too. // substract priority. for (let order of this.buildOrders) { let special = "Plan_" + this.name + "_" + order[4]; let aQueued = gameState.countOwnQueuedEntitiesWithMetadata("special", special); aQueued += this.queue.countQueuedUnitsWithMetadata("special", special); aQueued += this.queueChamp.countQueuedUnitsWithMetadata("special", special); aQueued += this.queueSiege.countQueuedUnitsWithMetadata("special", special); order[0] = order[2].length + aQueued; } this.buildOrders.sort((a, b) => { let va = a[0]/a[3].targetSize - a[3].priority; if (a[0] >= a[3].targetSize) va += 1000; let vb = b[0]/b[3].targetSize - b[3].priority; if (b[0] >= b[3].targetSize) vb += 1000; return va - vb; }); if (this.Config.debug > 1 && gameState.ai.playedTurn%50 === 0) { API3.warn("===================================="); API3.warn("======== build order for plan " + this.name); for (let order of this.buildOrders) { let specialData = "Plan_"+this.name+"_"+order[4]; let inTraining = gameState.countOwnQueuedEntitiesWithMetadata("special", specialData); let queue1 = this.queue.countQueuedUnitsWithMetadata("special", specialData); let queue2 = this.queueChamp.countQueuedUnitsWithMetadata("special", specialData); let queue3 = this.queueSiege.countQueuedUnitsWithMetadata("special", specialData); API3.warn(" >>> " + order[4] + " done " + order[2].length + " training " + inTraining + " queue " + queue1 + " champ " + queue2 + " siege " + queue3 + " >> need " + order[3].targetSize); } API3.warn("===================================="); } let firstOrder = this.buildOrders[0]; if (firstOrder[0] < firstOrder[3].targetSize) { // find the actual queue we want let queue = this.queue; if (firstOrder[4] == "Siege") queue = this.queueSiege; else if (firstOrder[3].classes.indexOf("Hero") != -1) queue = this.queueSiege; else if (firstOrder[3].classes.indexOf("Champion") != -1) queue = this.queueChamp; if (queue.length() <= 5) { let template = gameState.ai.HQ.findBestTrainableUnit(gameState, firstOrder[1], firstOrder[3].interests); // HACK (TODO replace) : if we have no trainable template... Then we'll simply remove the buildOrder, // effectively removing the unit from the plan. if (template === undefined) { if (this.Config.debug > 1) API3.warn("attack no template found " + firstOrder[1]); delete this.unitStat[firstOrder[4]]; // deleting the associated unitstat. this.buildOrders.splice(0, 1); } else { if (this.Config.debug > 2) API3.warn("attack template " + template + " added for plan " + this.name); let max = firstOrder[3].batchSize; let specialData = "Plan_" + this.name + "_" + firstOrder[4]; let data = { "plan": this.name, "special": specialData, "base": 0 }; data.role = gameState.getTemplate(template).hasClass("CitizenSoldier") ? "worker" : "attack"; let trainingPlan = new PETRA.TrainingPlan(gameState, template, data, max, max); if (trainingPlan.template) queue.addPlan(trainingPlan); else if (this.Config.debug > 1) API3.warn("training plan canceled because no template for " + template + " build1 " + uneval(firstOrder[1]) + " build3 " + uneval(firstOrder[3].interests)); } } } }; PETRA.AttackPlan.prototype.assignUnits = function(gameState) { let plan = this.name; let added = false; // If we can not build units, assign all available except those affected to allied defense to the current attack. if (!this.canBuildUnits) { for (let ent of gameState.getOwnUnits().values()) { if (ent.getMetadata(PlayerID, "allied") || !this.isAvailableUnit(gameState, ent)) continue; ent.setMetadata(PlayerID, "plan", plan); this.unitCollection.updateEnt(ent); added = true; } return added; } if (this.type == "Raid") { // Raids are quick attacks: assign all FastMoving soldiers except some for hunting. let num = 0; for (let ent of gameState.getOwnUnits().values()) { if (!ent.hasClass("FastMoving") || !this.isAvailableUnit(gameState, ent)) continue; if (num++ < 2) continue; ent.setMetadata(PlayerID, "plan", plan); this.unitCollection.updateEnt(ent); added = true; } return added; } // Assign all units without specific role. for (let ent of gameState.getOwnEntitiesByRole(undefined, true).values()) { if (ent.hasClasses(["!Unit", "Ship", "Support"]) || !this.isAvailableUnit(gameState, ent) || ent.attackTypes() === undefined) continue; ent.setMetadata(PlayerID, "plan", plan); this.unitCollection.updateEnt(ent); added = true; } // Add units previously in a plan, but which left it because needed for defense or attack finished. for (let ent of gameState.ai.HQ.attackManager.outOfPlan.values()) { if (!this.isAvailableUnit(gameState, ent)) continue; ent.setMetadata(PlayerID, "plan", plan); this.unitCollection.updateEnt(ent); added = true; } // Finally add also some workers for the higher difficulties, // If Rush, assign all kind of workers, keeping only a minimum number of defenders // Otherwise, assign only some idle workers if too much of them if (this.Config.difficulty <= 2) return added; let num = 0; const numbase = {}; let keep = this.type != "Rush" ? 6 + 4 * gameState.getNumPlayerEnemies() + 8 * this.Config.personality.defensive : 8; keep = Math.round(this.Config.popScaling * keep); for (const ent of gameState.getOwnEntitiesByRole("worker", true).values()) { if (!ent.hasClass("CitizenSoldier") || !this.isAvailableUnit(gameState, ent)) continue; const baseID = ent.getMetadata(PlayerID, "base"); if (baseID) numbase[baseID] = numbase[baseID] ? ++numbase[baseID] : 1; else { API3.warn("Petra problem ent without base "); PETRA.dumpEntity(ent); continue; } if (num++ < keep || numbase[baseID] < 5) continue; if (this.type != "Rush" && ent.getMetadata(PlayerID, "subrole") != "idle") continue; ent.setMetadata(PlayerID, "plan", plan); this.unitCollection.updateEnt(ent); added = true; } return added; }; PETRA.AttackPlan.prototype.isAvailableUnit = function(gameState, ent) { if (!ent.position()) return false; if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") !== -1 || ent.getMetadata(PlayerID, "transport") !== undefined || ent.getMetadata(PlayerID, "transporter") !== undefined) return false; if (gameState.ai.HQ.victoryManager.criticalEnts.has(ent.id()) && (this.overseas || ent.healthLevel() < 0.8)) return false; return true; }; /** Reassign one (at each turn) FastMoving unit to fasten raid preparation. */ PETRA.AttackPlan.prototype.reassignFastUnit = function(gameState) { for (let ent of this.unitCollection.values()) { if (!ent.position() || ent.getMetadata(PlayerID, "transport") !== undefined) continue; if (!ent.hasClasses(["FastMoving", "CitizenSoldier"])) continue; let raid = gameState.ai.HQ.attackManager.getAttackInPreparation("Raid"); ent.setMetadata(PlayerID, "plan", raid.name); this.unitCollection.updateEnt(ent); raid.unitCollection.updateEnt(ent); return; } }; PETRA.AttackPlan.prototype.chooseTarget = function(gameState) { if (this.targetPlayer === undefined) { this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this); if (this.targetPlayer === undefined) return false; } this.target = this.getNearestTarget(gameState, this.rallyPoint); if (!this.target) { if (this.uniqueTargetId) return false; // may-be all our previous enemey target (if not recomputed here) have been destroyed ? this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this); if (this.targetPlayer !== undefined) this.target = this.getNearestTarget(gameState, this.rallyPoint); if (!this.target) return false; } this.targetPos = this.target.position(); // redefine a new rally point for this target if we have a base on the same land // find a new one on the pseudo-nearest base (dist weighted by the size of the island) let targetIndex = PETRA.getLandAccess(gameState, this.target); let rallyIndex = gameState.ai.accessibility.getAccessValue(this.rallyPoint); if (targetIndex != rallyIndex) { let distminSame = Math.min(); let rallySame; let distminDiff = Math.min(); let rallyDiff; - for (let base of gameState.ai.HQ.baseManagers) + for (const base of gameState.ai.HQ.baseManagers()) { let anchor = base.anchor; if (!anchor || !anchor.position()) continue; let dist = API3.SquareVectorDistance(anchor.position(), this.targetPos); if (base.accessIndex == targetIndex) { if (dist >= distminSame) continue; distminSame = dist; rallySame = anchor.position(); } else { dist /= Math.sqrt(gameState.ai.accessibility.regionSize[base.accessIndex]); if (dist >= distminDiff) continue; distminDiff = dist; rallyDiff = anchor.position(); } } if (rallySame) { this.rallyPoint = rallySame; this.overseas = 0; } else if (rallyDiff) { rallyIndex = gameState.ai.accessibility.getAccessValue(rallyDiff); this.rallyPoint = rallyDiff; let sea = gameState.ai.HQ.getSeaBetweenIndices(gameState, rallyIndex, targetIndex); if (sea) { this.overseas = sea; gameState.ai.HQ.navalManager.setMinimalTransportShips(gameState, this.overseas, this.neededShips); } else { API3.warn("Petra: " + this.type + " " + this.name + " has an inaccessible target" + " with indices " + rallyIndex + " " + targetIndex + " from " + this.target.templateName()); return false; } } } else if (this.overseas) this.overseas = 0; return true; }; /** * sameLand true means that we look for a target for which we do not need to take a transport */ PETRA.AttackPlan.prototype.getNearestTarget = function(gameState, position, sameLand) { this.isBlocked = false; // Temporary variables needed by isValidTarget this.gameState = gameState; this.sameLand = sameLand && sameLand > 1 ? sameLand : false; let targets; if (this.uniqueTargetId) { targets = new API3.EntityCollection(gameState.sharedScript); let ent = gameState.getEntityById(this.uniqueTargetId); if (ent) targets.addEnt(ent); } else { if (this.type == "Raid") targets = this.raidTargetFinder(gameState); else if (this.type == "Rush" || this.type == "Attack") { targets = this.rushTargetFinder(gameState, this.targetPlayer); if (!targets.hasEntities() && (this.hasSiegeUnits() || this.forced)) targets = this.defaultTargetFinder(gameState, this.targetPlayer); } else targets = this.defaultTargetFinder(gameState, this.targetPlayer); } if (!targets.hasEntities()) return undefined; // picking the nearest target let target; let minDist = Math.min(); for (let ent of targets.values()) { if (this.targetPlayer == 0 && gameState.getVictoryConditions().has("capture_the_relic") && (!ent.hasClass("Relic") || gameState.ai.HQ.victoryManager.targetedGaiaRelics.has(ent.id()))) continue; // Do not bother with some pointless targets if (!this.isValidTarget(ent)) continue; let dist = API3.SquareVectorDistance(ent.position(), position); // In normal attacks, disfavor fields if (this.type != "Rush" && this.type != "Raid" && ent.hasClass("Field")) dist += 100000; if (dist < minDist) { minDist = dist; target = ent; } } if (!target) return undefined; // Check that we can reach this target target = this.checkTargetObstruction(gameState, target, position); if (!target) return undefined; if (this.targetPlayer == 0 && gameState.getVictoryConditions().has("capture_the_relic") && target.hasClass("Relic")) gameState.ai.HQ.victoryManager.targetedGaiaRelics.set(target.id(), [this.name]); // Rushes can change their enemy target if nothing found with the preferred enemy // Obstruction also can change the enemy target this.targetPlayer = target.owner(); return target; }; /** * Default target finder aims for conquest critical targets * We must apply the *same* selection (isValidTarget) as done in getNearestTarget */ PETRA.AttackPlan.prototype.defaultTargetFinder = function(gameState, playerEnemy) { let targets = new API3.EntityCollection(gameState.sharedScript); if (gameState.getVictoryConditions().has("wonder")) for (let ent of gameState.getEnemyStructures(playerEnemy).filter(API3.Filters.byClass("Wonder")).values()) targets.addEnt(ent); if (gameState.getVictoryConditions().has("regicide")) for (let ent of gameState.getEnemyUnits(playerEnemy).filter(API3.Filters.byClass("Hero")).values()) targets.addEnt(ent); if (gameState.getVictoryConditions().has("capture_the_relic")) for (let ent of gameState.updatingGlobalCollection("allRelics", API3.Filters.byClass("Relic")).filter(relic => relic.owner() == playerEnemy).values()) targets.addEnt(ent); targets = targets.filter(this.isValidTarget, this); if (targets.hasEntities()) return targets; let validTargets = gameState.getEnemyStructures(playerEnemy).filter(this.isValidTarget, this); targets = validTargets.filter(API3.Filters.byClass("CivCentre")); if (!targets.hasEntities()) targets = validTargets.filter(API3.Filters.byClass("ConquestCritical")); // If there's nothing, attack anything else that's less critical if (!targets.hasEntities()) targets = validTargets.filter(API3.Filters.byClass("Town")); if (!targets.hasEntities()) targets = validTargets.filter(API3.Filters.byClass("Village")); // No buildings, attack anything conquest critical, units included. // TODO Should add naval attacks against the last remaining ships. if (!targets.hasEntities()) targets = gameState.getEntities(playerEnemy).filter(API3.Filters.byClass("ConquestCritical")). filter(API3.Filters.not(API3.Filters.byClass("Ship"))); return targets; }; PETRA.AttackPlan.prototype.isValidTarget = function(ent) { if (!ent.position()) return false; if (this.sameLand && PETRA.getLandAccess(this.gameState, ent) != this.sameLand) return false; return !ent.decaying() || ent.getDefaultArrow() || ent.isGarrisonHolder() && ent.garrisoned().length; }; /** Rush target finder aims at isolated non-defended buildings */ PETRA.AttackPlan.prototype.rushTargetFinder = function(gameState, playerEnemy) { let targets = new API3.EntityCollection(gameState.sharedScript); let buildings; if (playerEnemy !== undefined) buildings = gameState.getEnemyStructures(playerEnemy).toEntityArray(); else buildings = gameState.getEnemyStructures().toEntityArray(); if (!buildings.length) return targets; this.position = this.unitCollection.getCentrePosition(); if (!this.position) this.position = this.rallyPoint; let target; let minDist = Math.min(); for (let building of buildings) { if (building.owner() == 0) continue; if (building.hasDefensiveFire()) continue; if (!this.isValidTarget(building)) continue; let pos = building.position(); let defended = false; for (let defense of buildings) { if (!defense.hasDefensiveFire()) continue; let dist = API3.SquareVectorDistance(pos, defense.position()); if (dist < 6400) // TODO check on defense range rather than this fixed 80*80 { defended = true; break; } } if (defended) continue; let dist = API3.SquareVectorDistance(pos, this.position); if (dist > minDist) continue; minDist = dist; target = building; } if (target) targets.addEnt(target); if (!targets.hasEntities() && this.type == "Rush" && playerEnemy) targets = this.rushTargetFinder(gameState); return targets; }; /** Raid target finder aims at destructing foundations from which our defenseManager has attacked the builders */ PETRA.AttackPlan.prototype.raidTargetFinder = function(gameState) { let targets = new API3.EntityCollection(gameState.sharedScript); for (let targetId of gameState.ai.HQ.defenseManager.targetList) { let target = gameState.getEntityById(targetId); if (target && target.position()) targets.addEnt(target); } return targets; }; /** * Check that we can have a path to this target * otherwise we may be blocked by walls and try to react accordingly * This is done only when attacker and target are on the same land */ PETRA.AttackPlan.prototype.checkTargetObstruction = function(gameState, target, position) { if (PETRA.getLandAccess(gameState, target) != gameState.ai.accessibility.getAccessValue(position)) return target; let targetPos = target.position(); let startPos = { "x": position[0], "y": position[1] }; let endPos = { "x": targetPos[0], "y": targetPos[1] }; let blocker; let path = Engine.ComputePath(startPos, endPos, gameState.getPassabilityClassMask("default")); if (!path.length) return undefined; let pathPos = [path[0].x, path[0].y]; let dist = API3.VectorDistance(pathPos, targetPos); let radius = target.obstructionRadius().max; for (let struct of gameState.getEnemyStructures().values()) { if (!struct.position() || !struct.get("Obstruction") || struct.hasClass("Field")) continue; // we consider that we can reach the target, but nonetheless check that we did not cross any enemy gate if (dist < radius + 10 && !struct.hasClass("Gate")) continue; // Check that we are really blocked by this structure, i.e. advancing by 1+0.8(clearance)m // in the target direction would bring us inside its obstruction. let structPos = struct.position(); let x = pathPos[0] - structPos[0] + 1.8 * (targetPos[0] - pathPos[0]) / dist; let y = pathPos[1] - structPos[1] + 1.8 * (targetPos[1] - pathPos[1]) / dist; if (struct.get("Obstruction/Static")) { if (!struct.angle()) continue; let angle = struct.angle(); let width = +struct.get("Obstruction/Static/@width"); let depth = +struct.get("Obstruction/Static/@depth"); let cosa = Math.cos(angle); let sina = Math.sin(angle); let u = x * cosa - y * sina; let v = x * sina + y * cosa; if (Math.abs(u) < width/2 && Math.abs(v) < depth/2) { blocker = struct; break; } } else if (struct.get("Obstruction/Obstructions")) { if (!struct.angle()) continue; let angle = struct.angle(); let width = +struct.get("Obstruction/Obstructions/Door/@width"); let depth = +struct.get("Obstruction/Obstructions/Door/@depth"); let doorHalfWidth = width / 2; width += +struct.get("Obstruction/Obstructions/Left/@width"); depth = Math.max(depth, +struct.get("Obstruction/Obstructions/Left/@depth")); width += +struct.get("Obstruction/Obstructions/Right/@width"); depth = Math.max(depth, +struct.get("Obstruction/Obstructions/Right/@depth")); let cosa = Math.cos(angle); let sina = Math.sin(angle); let u = x * cosa - y * sina; let v = x * sina + y * cosa; if (Math.abs(u) < width/2 && Math.abs(v) < depth/2) { blocker = struct; break; } // check that the path does not cross this gate (could happen if not locked) for (let i = 1; i < path.length; ++i) { let u1 = (path[i-1].x - structPos[0]) * cosa - (path[i-1].y - structPos[1]) * sina; let v1 = (path[i-1].x - structPos[0]) * sina + (path[i-1].y - structPos[1]) * cosa; let u2 = (path[i].x - structPos[0]) * cosa - (path[i].y - structPos[1]) * sina; let v2 = (path[i].x - structPos[0]) * sina + (path[i].y - structPos[1]) * cosa; if (v1 * v2 < 0) { let u0 = (u1*v2 - u2*v1) / (v2-v1); if (Math.abs(u0) > doorHalfWidth) continue; blocker = struct; break; } } if (blocker) break; } else if (struct.get("Obstruction/Unit")) { let r = +this.get("Obstruction/Unit/@radius"); if (x*x + y*y < r*r) { blocker = struct; break; } } } if (blocker) { this.isBlocked = true; return blocker; } return target; }; PETRA.AttackPlan.prototype.getPathToTarget = function(gameState, fixedRallyPoint = false) { let startAccess = gameState.ai.accessibility.getAccessValue(this.rallyPoint); let endAccess = PETRA.getLandAccess(gameState, this.target); if (startAccess != endAccess) return false; Engine.ProfileStart("AI Compute path"); let startPos = { "x": this.rallyPoint[0], "y": this.rallyPoint[1] }; let endPos = { "x": this.targetPos[0], "y": this.targetPos[1] }; let path = Engine.ComputePath(startPos, endPos, gameState.getPassabilityClassMask("large")); this.path = []; this.path.push(this.targetPos); for (let p in path) this.path.push([path[p].x, path[p].y]); this.path.push(this.rallyPoint); this.path.reverse(); // Change the rally point to something useful if (!fixedRallyPoint) this.setRallyPoint(gameState); Engine.ProfileStop(); return true; }; /** Set rally point at the border of our territory */ PETRA.AttackPlan.prototype.setRallyPoint = function(gameState) { for (let i = 0; i < this.path.length; ++i) { if (gameState.ai.HQ.territoryMap.getOwner(this.path[i]) === PlayerID) continue; if (i === 0) this.rallyPoint = this.path[0]; else if (i > 1 && gameState.ai.HQ.isDangerousLocation(gameState, this.path[i-1], 20)) { this.rallyPoint = this.path[i-2]; this.path.splice(0, i-2); } else { this.rallyPoint = this.path[i-1]; this.path.splice(0, i-1); } break; } }; /** * Executes the attack plan, after this is executed the update function will be run every turn * If we're here, it's because we have enough units. */ PETRA.AttackPlan.prototype.StartAttack = function(gameState) { if (this.Config.debug > 1) API3.warn("start attack " + this.name + " with type " + this.type); // if our target was destroyed during preparation, choose a new one if ((this.targetPlayer === undefined || !this.target || !gameState.getEntityById(this.target.id())) && !this.chooseTarget(gameState)) return false; // erase our queue. This will stop any leftover unit from being trained. this.removeQueues(gameState); for (let ent of this.unitCollection.values()) { ent.setMetadata(PlayerID, "subrole", "walking"); let stance = ent.isPackable() ? "standground" : "aggressive"; if (ent.getStance() != stance) ent.setStance(stance); } let rallyAccess = gameState.ai.accessibility.getAccessValue(this.rallyPoint); let targetAccess = PETRA.getLandAccess(gameState, this.target); if (rallyAccess == targetAccess) { if (!this.path) this.getPathToTarget(gameState, true); if (!this.path || !this.path[0][0] || !this.path[0][1]) return false; this.overseas = 0; this.state = "walking"; this.unitCollection.moveToRange(this.path[0][0], this.path[0][1], 0, 15); } else { this.overseas = gameState.ai.HQ.getSeaBetweenIndices(gameState, rallyAccess, targetAccess); if (!this.overseas) return false; this.state = "transporting"; // TODO require a global transport for the collection, // and put back its state to "walking" when the transport is finished for (let ent of this.unitCollection.values()) gameState.ai.HQ.navalManager.requireTransport(gameState, ent, rallyAccess, targetAccess, this.targetPos); } return true; }; /** Runs every turn after the attack is executed */ PETRA.AttackPlan.prototype.update = function(gameState, events) { if (!this.unitCollection.hasEntities()) return 0; Engine.ProfileStart("Update Attack"); this.position = this.unitCollection.getCentrePosition(); // we are transporting our units, let's wait // TODO instead of state "arrived", made a state "walking" with a new path if (this.state == "transporting") this.UpdateTransporting(gameState, events); if (this.state == "walking" && !this.UpdateWalking(gameState, events)) { Engine.ProfileStop(); return 0; } if (this.state == "arrived") { // let's proceed on with whatever happens now. this.state = ""; this.startingAttack = true; this.unitCollection.forEach(ent => { ent.stopMoving(); ent.setMetadata(PlayerID, "subrole", "attacking"); }); if (this.type == "Rush") // try to find a better target for rush { let newtarget = this.getNearestTarget(gameState, this.position); if (newtarget) { this.target = newtarget; this.targetPos = this.target.position(); } } } // basic state of attacking. if (this.state == "") { // First update the target and/or its position if needed if (!this.UpdateTarget(gameState)) { Engine.ProfileStop(); return false; } let time = gameState.ai.elapsedTime; let attackedByStructure = {}; for (let evt of events.Attacked) { if (!this.unitCollection.hasEntId(evt.target)) continue; let attacker = gameState.getEntityById(evt.attacker); let ourUnit = gameState.getEntityById(evt.target); if (!ourUnit || !ourUnit.position() || !attacker || !attacker.position()) continue; if (!attacker.hasClass("Unit")) { attackedByStructure[evt.target] = true; continue; } if (PETRA.isSiegeUnit(ourUnit)) { // if our siege units are attacked, we'll send some units to deal with enemies. let collec = this.unitCollection.filter(API3.Filters.not(API3.Filters.byClass("Siege"))).filterNearest(ourUnit.position(), 5); for (let ent of collec.values()) { if (PETRA.isSiegeUnit(ent)) // needed as mauryan elephants are not filtered out continue; let allowCapture = PETRA.allowCapture(gameState, ent, attacker); if (!ent.canAttackTarget(attacker, allowCapture)) continue; ent.attack(attacker.id(), allowCapture); ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); } // And if this attacker is a non-ranged siege unit and our unit also, attack it if (PETRA.isSiegeUnit(attacker) && attacker.hasClass("Melee") && ourUnit.hasClass("Melee") && ourUnit.canAttackTarget(attacker, PETRA.allowCapture(gameState, ourUnit, attacker))) { ourUnit.attack(attacker.id(), PETRA.allowCapture(gameState, ourUnit, attacker)); ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); } } else { if (this.isBlocked && !ourUnit.hasClass("Ranged") && attacker.hasClass("Ranged")) { // do not react if our melee units are attacked by ranged one and we are blocked by walls // TODO check that the attacker is from behind the wall continue; } else if (PETRA.isSiegeUnit(attacker)) { // if our unit is attacked by a siege unit, we'll send some melee units to help it. let collec = this.unitCollection.filter(API3.Filters.byClass("Melee")).filterNearest(ourUnit.position(), 5); for (let ent of collec.values()) { let allowCapture = PETRA.allowCapture(gameState, ent, attacker); if (!ent.canAttackTarget(attacker, allowCapture)) continue; ent.attack(attacker.id(), allowCapture); ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); } } else { // Look first for nearby units to help us if possible let collec = this.unitCollection.filterNearest(ourUnit.position(), 2); for (let ent of collec.values()) { let allowCapture = PETRA.allowCapture(gameState, ent, attacker); if (PETRA.isSiegeUnit(ent) || !ent.canAttackTarget(attacker, allowCapture)) continue; let orderData = ent.unitAIOrderData(); if (orderData && orderData.length && orderData[0].target) { if (orderData[0].target === attacker.id()) continue; let target = gameState.getEntityById(orderData[0].target); if (target && !target.hasClasses(["Structure", "Support"])) continue; } ent.attack(attacker.id(), allowCapture); ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); } // Then the unit under attack: abandon its target (if it was a structure or a support) and retaliate // also if our unit is attacking a range unit and the attacker is a melee unit, retaliate let orderData = ourUnit.unitAIOrderData(); if (orderData && orderData.length && orderData[0].target) { if (orderData[0].target === attacker.id()) continue; let target = gameState.getEntityById(orderData[0].target); if (target && !target.hasClasses(["Structure", "Support"])) { if (!target.hasClass("Ranged") || !attacker.hasClass("Melee")) continue; } } let allowCapture = PETRA.allowCapture(gameState, ourUnit, attacker); if (ourUnit.canAttackTarget(attacker, allowCapture)) { ourUnit.attack(attacker.id(), allowCapture); ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); } } } } let enemyUnits = gameState.getEnemyUnits(this.targetPlayer); let enemyStructures = gameState.getEnemyStructures(this.targetPlayer); // Count the number of times an enemy is targeted, to prevent all units to follow the same target let unitTargets = {}; for (let ent of this.unitCollection.values()) { if (ent.hasClass("Ship")) // TODO What to do with ships continue; let orderData = ent.unitAIOrderData(); if (!orderData || !orderData.length || !orderData[0].target) continue; let targetId = orderData[0].target; let target = gameState.getEntityById(targetId); if (!target || target.hasClass("Structure")) continue; if (!(targetId in unitTargets)) { if (PETRA.isSiegeUnit(target) || target.hasClass("Hero")) unitTargets[targetId] = -8; else if (target.hasClasses(["Champion", "Ship"])) unitTargets[targetId] = -5; else unitTargets[targetId] = -3; } ++unitTargets[targetId]; } let veto = {}; for (let target in unitTargets) if (unitTargets[target] > 0) veto[target] = true; let targetClassesUnit; let targetClassesSiege; if (this.type == "Rush") targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Palisade", "Wall", "Tower", "Fortress"], "vetoEntities": veto }; else { if (this.target.hasClass("Fortress")) targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Palisade", "Wall"], "vetoEntities": veto }; else if (this.target.hasClasses(["Palisade", "Wall"])) targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Fortress"], "vetoEntities": veto }; else targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Palisade", "Wall", "Fortress"], "vetoEntities": veto }; } if (this.target.hasClass("Structure")) targetClassesSiege = { "attack": ["Structure"], "avoid": [], "vetoEntities": veto }; else targetClassesSiege = { "attack": ["Unit", "Structure"], "avoid": [], "vetoEntities": veto }; // do not loose time destroying buildings which do not help enemy's defense and can be easily captured later if (this.target.hasDefensiveFire()) { targetClassesUnit.avoid = targetClassesUnit.avoid.concat("House", "Storehouse", "Farmstead", "Field", "Forge"); targetClassesSiege.avoid = targetClassesSiege.avoid.concat("House", "Storehouse", "Farmstead", "Field", "Forge"); } if (this.unitCollUpdateArray === undefined || !this.unitCollUpdateArray.length) this.unitCollUpdateArray = this.unitCollection.toIdArray(); // Let's check a few units each time we update (currently 10) except when attack starts let lgth = this.unitCollUpdateArray.length < 15 || this.startingAttack ? this.unitCollUpdateArray.length : 10; for (let check = 0; check < lgth; check++) { let ent = gameState.getEntityById(this.unitCollUpdateArray[check]); if (!ent || !ent.position()) continue; // Do not reassign units which have reacted to an attack in that same turn if (ent.getMetadata(PlayerID, "lastAttackPlanUpdateTime") == time) continue; let targetId; let orderData = ent.unitAIOrderData(); if (orderData && orderData.length && orderData[0].target) targetId = orderData[0].target; // update the order if needed let needsUpdate = false; let maybeUpdate = false; let siegeUnit = PETRA.isSiegeUnit(ent); if (ent.isIdle()) needsUpdate = true; else if (siegeUnit && targetId) { let target = gameState.getEntityById(targetId); if (!target || gameState.isPlayerAlly(target.owner())) needsUpdate = true; else if (unitTargets[targetId] && unitTargets[targetId] > 0) { needsUpdate = true; --unitTargets[targetId]; } else if (!target.hasClass("Structure")) maybeUpdate = true; } else if (targetId) { let target = gameState.getEntityById(targetId); if (!target || gameState.isPlayerAlly(target.owner())) needsUpdate = true; else if (unitTargets[targetId] && unitTargets[targetId] > 0) { needsUpdate = true; --unitTargets[targetId]; } else if (target.hasClass("Ship") && !ent.hasClass("Ship")) maybeUpdate = true; else if (attackedByStructure[ent.id()] && target.hasClass("Field")) maybeUpdate = true; else if (!ent.hasClass("FastMoving") && !ent.hasClass("Ranged") && target.hasClass("FemaleCitizen") && target.unitAIState().split(".")[1] == "FLEEING") maybeUpdate = true; } // don't update too soon if not necessary if (!needsUpdate) { if (!maybeUpdate) continue; let deltat = ent.unitAIState() === "INDIVIDUAL.COMBAT.APPROACHING" ? 10 : 5; let lastAttackPlanUpdateTime = ent.getMetadata(PlayerID, "lastAttackPlanUpdateTime"); if (lastAttackPlanUpdateTime && time - lastAttackPlanUpdateTime < deltat) continue; } ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); let range = 60; let attackTypes = ent.attackTypes(); if (this.isBlocked) { if (attackTypes && attackTypes.indexOf("Ranged") !== -1) range = ent.attackRange("Ranged").max; else if (attackTypes && attackTypes.indexOf("Melee") !== -1) range = ent.attackRange("Melee").max; else range = 10; } else if (attackTypes && attackTypes.indexOf("Ranged") !== -1) range = 30 + ent.attackRange("Ranged").max; else if (ent.hasClass("FastMoving")) range += 30; range *= range; let entAccess = PETRA.getLandAccess(gameState, ent); // Checking for gates if we're a siege unit. if (siegeUnit) { let mStruct = enemyStructures.filter(enemy => { if (!enemy.position() || !ent.canAttackTarget(enemy, PETRA.allowCapture(gameState, ent, enemy))) return false; if (API3.SquareVectorDistance(enemy.position(), ent.position()) > range) return false; if (enemy.foundationProgress() == 0) return false; if (PETRA.getLandAccess(gameState, enemy) != entAccess) return false; return true; }).toEntityArray(); if (mStruct.length) { mStruct.sort((structa, structb) => { let vala = structa.costSum(); if (structa.hasClass("Gate") && ent.canAttackClass("Wall")) vala += 10000; else if (structa.hasDefensiveFire()) vala += 1000; else if (structa.hasClass("ConquestCritical")) vala += 200; let valb = structb.costSum(); if (structb.hasClass("Gate") && ent.canAttackClass("Wall")) valb += 10000; else if (structb.hasDefensiveFire()) valb += 1000; else if (structb.hasClass("ConquestCritical")) valb += 200; return valb - vala; }); if (mStruct[0].hasClass("Gate")) ent.attack(mStruct[0].id(), PETRA.allowCapture(gameState, ent, mStruct[0])); else { let rand = randIntExclusive(0, mStruct.length * 0.2); ent.attack(mStruct[rand].id(), PETRA.allowCapture(gameState, ent, mStruct[rand])); } } else { if (!ent.hasClass("Ranged")) { let targetClasses = { "attack": targetClassesSiege.attack, "avoid": targetClassesSiege.avoid.concat("Ship"), "vetoEntities": veto }; ent.attackMove(this.targetPos[0], this.targetPos[1], targetClasses); } else ent.attackMove(this.targetPos[0], this.targetPos[1], targetClassesSiege); } } else { const nearby = !ent.hasClasses(["FastMoving", "Ranged"]); let mUnit = enemyUnits.filter(enemy => { if (!enemy.position() || !ent.canAttackTarget(enemy, PETRA.allowCapture(gameState, ent, enemy))) return false; if (enemy.hasClass("Animal")) return false; if (nearby && enemy.hasClass("FemaleCitizen") && enemy.unitAIState().split(".")[1] == "FLEEING") return false; let dist = API3.SquareVectorDistance(enemy.position(), ent.position()); if (dist > range) return false; if (PETRA.getLandAccess(gameState, enemy) != entAccess) return false; // if already too much units targeting this enemy, let's continue towards our main target if (veto[enemy.id()] && API3.SquareVectorDistance(this.targetPos, ent.position()) > 2500) return false; enemy.setMetadata(PlayerID, "distance", Math.sqrt(dist)); return true; }, this).toEntityArray(); if (mUnit.length) { mUnit.sort((unitA, unitB) => { let vala = unitA.hasClass("Support") ? 50 : 0; if (ent.counters(unitA)) vala += 100; let valb = unitB.hasClass("Support") ? 50 : 0; if (ent.counters(unitB)) valb += 100; let distA = unitA.getMetadata(PlayerID, "distance"); let distB = unitB.getMetadata(PlayerID, "distance"); if (distA && distB) { vala -= distA; valb -= distB; } if (veto[unitA.id()]) vala -= 20000; if (veto[unitB.id()]) valb -= 20000; return valb - vala; }); let rand = randIntExclusive(0, mUnit.length * 0.1); ent.attack(mUnit[rand].id(), PETRA.allowCapture(gameState, ent, mUnit[rand])); } // This may prove dangerous as we may be blocked by something we // cannot attack. See similar behaviour at #5741. else if (this.isBlocked && ent.canAttackTarget(this.target, false)) ent.attack(this.target.id(), false); else if (API3.SquareVectorDistance(this.targetPos, ent.position()) > 2500) { let targetClasses = targetClassesUnit; if (maybeUpdate && ent.unitAIState() === "INDIVIDUAL.COMBAT.APPROACHING") // we may be blocked by walls, attack everything { if (!ent.hasClasses(["Ranged", "Ship"])) targetClasses = { "attack": ["Unit", "Structure"], "avoid": ["Ship"], "vetoEntities": veto }; else targetClasses = { "attack": ["Unit", "Structure"], "vetoEntities": veto }; } else if (!ent.hasClasses(["Ranged", "Ship"])) targetClasses = { "attack": targetClassesUnit.attack, "avoid": targetClassesUnit.avoid.concat("Ship"), "vetoEntities": veto }; ent.attackMove(this.targetPos[0], this.targetPos[1], targetClasses); } else { let mStruct = enemyStructures.filter(enemy => { if (this.isBlocked && enemy.id() != this.target.id()) return false; if (!enemy.position() || !ent.canAttackTarget(enemy, PETRA.allowCapture(gameState, ent, enemy))) return false; if (API3.SquareVectorDistance(enemy.position(), ent.position()) > range) return false; if (PETRA.getLandAccess(gameState, enemy) != entAccess) return false; return true; }, this).toEntityArray(); if (mStruct.length) { mStruct.sort((structa, structb) => { let vala = structa.costSum(); if (structa.hasClass("Gate") && ent.canAttackClass("Wall")) vala += 10000; else if (structa.hasClass("ConquestCritical")) vala += 100; let valb = structb.costSum(); if (structb.hasClass("Gate") && ent.canAttackClass("Wall")) valb += 10000; else if (structb.hasClass("ConquestCritical")) valb += 100; return valb - vala; }); if (mStruct[0].hasClass("Gate")) ent.attack(mStruct[0].id(), false); else { let rand = randIntExclusive(0, mStruct.length * 0.2); ent.attack(mStruct[rand].id(), PETRA.allowCapture(gameState, ent, mStruct[rand])); } } else if (needsUpdate) // really nothing let's try to help our nearest unit { let distmin = Math.min(); let attacker; this.unitCollection.forEach(unit => { if (!unit.position()) return; if (unit.unitAIState().split(".")[1] != "COMBAT" || !unit.unitAIOrderData().length || !unit.unitAIOrderData()[0].target) return; let target = gameState.getEntityById(unit.unitAIOrderData()[0].target); if (!target) return; let dist = API3.SquareVectorDistance(unit.position(), ent.position()); if (dist > distmin) return; distmin = dist; if (!ent.canAttackTarget(target, PETRA.allowCapture(gameState, ent, target))) return; attacker = target; }); if (attacker) ent.attack(attacker.id(), PETRA.allowCapture(gameState, ent, attacker)); } } } } this.unitCollUpdateArray.splice(0, lgth); this.startingAttack = false; // check if this enemy has resigned if (this.target && this.target.owner() === 0 && this.targetPlayer !== 0) this.target = undefined; } this.lastPosition = this.position; Engine.ProfileStop(); return this.unitCollection.length; }; PETRA.AttackPlan.prototype.UpdateTransporting = function(gameState, events) { let done = true; for (let ent of this.unitCollection.values()) { if (this.Config.debug > 1 && ent.getMetadata(PlayerID, "transport") !== undefined) Engine.PostCommand(PlayerID, { "type": "set-shading-color", "entities": [ent.id()], "rgb": [2, 2, 0] }); else if (this.Config.debug > 1) Engine.PostCommand(PlayerID, { "type": "set-shading-color", "entities": [ent.id()], "rgb": [1, 1, 1] }); if (!done) continue; if (ent.getMetadata(PlayerID, "transport") !== undefined) done = false; } if (done) { this.state = "arrived"; return; } // if we are attacked while waiting the rest of the army, retaliate for (let evt of events.Attacked) { if (!this.unitCollection.hasEntId(evt.target)) continue; let attacker = gameState.getEntityById(evt.attacker); if (!attacker || !gameState.getEntityById(evt.target)) continue; for (let ent of this.unitCollection.values()) { if (ent.getMetadata(PlayerID, "transport") !== undefined) continue; let allowCapture = PETRA.allowCapture(gameState, ent, attacker); if (!ent.isIdle() || !ent.canAttackTarget(attacker, allowCapture)) continue; ent.attack(attacker.id(), allowCapture); } break; } }; PETRA.AttackPlan.prototype.UpdateWalking = function(gameState, events) { // we're marching towards the target // Let's check if any of our unit has been attacked. // In case yes, we'll determine if we're simply off against an enemy army, a lone unit/building // or if we reached the enemy base. Different plans may react differently. let attackedNB = 0; let attackedUnitNB = 0; for (let evt of events.Attacked) { if (!this.unitCollection.hasEntId(evt.target)) continue; let attacker = gameState.getEntityById(evt.attacker); if (attacker && (attacker.owner() !== 0 || this.targetPlayer === 0)) { attackedNB++; if (attacker.hasClass("Unit")) attackedUnitNB++; } } // Are we arrived at destination ? if (attackedNB > 1 && (attackedUnitNB || this.hasSiegeUnits())) { if (gameState.ai.HQ.territoryMap.getOwner(this.position) === this.targetPlayer || attackedNB > 3) { this.state = "arrived"; return true; } } // basically haven't moved an inch: very likely stuck) if (API3.SquareVectorDistance(this.position, this.position5TurnsAgo) < 10 && this.path.length > 0 && gameState.ai.playedTurn % 5 === 0) { // check for stuck siege units let farthest = 0; let farthestEnt; for (let ent of this.unitCollection.filter(API3.Filters.byClass("Siege")).values()) { let dist = API3.SquareVectorDistance(ent.position(), this.position); if (dist < farthest) continue; farthest = dist; farthestEnt = ent; } if (farthestEnt) farthestEnt.destroy(); } if (gameState.ai.playedTurn % 5 === 0) this.position5TurnsAgo = this.position; if (this.lastPosition && API3.SquareVectorDistance(this.position, this.lastPosition) < 16 && this.path.length > 0) { if (!this.path[0][0] || !this.path[0][1]) API3.warn("Start: Problem with path " + uneval(this.path)); // We're stuck, presumably. Check if there are no walls just close to us. for (let ent of gameState.getEnemyStructures().filter(API3.Filters.byClass(["Palisade", "Wall"])).values()) { if (API3.SquareVectorDistance(this.position, ent.position()) > 800) continue; let enemyClass = ent.hasClass("Wall") ? "Wall" : "Palisade"; // there are walls, so check if we can attack if (this.unitCollection.filter(API3.Filters.byCanAttackClass(enemyClass)).hasEntities()) { if (this.Config.debug > 1) API3.warn("Attack Plan " + this.type + " " + this.name + " has met walls and is not happy."); this.state = "arrived"; return true; } // abort plan if (this.Config.debug > 1) API3.warn("Attack Plan " + this.type + " " + this.name + " has met walls and gives up."); return false; } // this.unitCollection.move(this.path[0][0], this.path[0][1]); this.unitCollection.moveIndiv(this.path[0][0], this.path[0][1]); } // check if our units are close enough from the next waypoint. if (API3.SquareVectorDistance(this.position, this.targetPos) < 10000) { if (this.Config.debug > 1) API3.warn("Attack Plan " + this.type + " " + this.name + " has arrived to destination."); this.state = "arrived"; return true; } else if (this.path.length && API3.SquareVectorDistance(this.position, this.path[0]) < 1600) { this.path.shift(); if (this.path.length) this.unitCollection.moveToRange(this.path[0][0], this.path[0][1], 0, 15); else { if (this.Config.debug > 1) API3.warn("Attack Plan " + this.type + " " + this.name + " has arrived to destination."); this.state = "arrived"; return true; } } return true; }; PETRA.AttackPlan.prototype.UpdateTarget = function(gameState) { // First update the target position in case it's a unit (and check if it has garrisoned) if (this.target && this.target.hasClass("Unit")) { this.targetPos = this.target.position(); if (!this.targetPos) { let holder = PETRA.getHolder(gameState, this.target); if (holder && gameState.isPlayerEnemy(holder.owner())) { this.target = holder; this.targetPos = holder.position(); } else this.target = undefined; } } // Then update the target if needed: if (this.targetPlayer === undefined || !gameState.isPlayerEnemy(this.targetPlayer)) { this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this); if (this.targetPlayer === undefined) return false; if (this.target && this.target.owner() !== this.targetPlayer) this.target = undefined; } if (this.target && this.target.owner() === 0 && this.targetPlayer !== 0) // this enemy has resigned this.target = undefined; if (!this.target || !gameState.getEntityById(this.target.id())) { if (this.Config.debug > 1) API3.warn("Seems like our target for plan " + this.name + " has been destroyed or captured. Switching."); let accessIndex = this.getAttackAccess(gameState); this.target = this.getNearestTarget(gameState, this.position, accessIndex); if (!this.target) { if (this.uniqueTargetId) return false; // Check if we could help any current attack let attackManager = gameState.ai.HQ.attackManager; for (let attackType in attackManager.startedAttacks) { for (let attack of attackManager.startedAttacks[attackType]) { if (attack.name == this.name) continue; if (!attack.target || !gameState.getEntityById(attack.target.id()) || !gameState.isPlayerEnemy(attack.target.owner())) continue; if (accessIndex != PETRA.getLandAccess(gameState, attack.target)) continue; if (attack.target.owner() == 0 && attack.targetPlayer != 0) // looks like it has resigned continue; if (!gameState.isPlayerEnemy(attack.targetPlayer)) continue; this.target = attack.target; this.targetPlayer = attack.targetPlayer; this.targetPos = this.target.position(); return true; } } // If not, let's look for another enemy if (!this.target) { this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this); if (this.targetPlayer !== undefined) this.target = this.getNearestTarget(gameState, this.position, accessIndex); if (!this.target) { if (this.Config.debug > 1) API3.warn("No new target found. Remaining units " + this.unitCollection.length); return false; } } if (this.Config.debug > 1) API3.warn("We will help one of our other attacks"); } this.targetPos = this.target.position(); } return true; }; /** reset any units */ PETRA.AttackPlan.prototype.Abort = function(gameState) { this.unitCollection.unregister(); if (this.unitCollection.hasEntities()) { // If the attack was started, look for a good rallyPoint to withdraw let rallyPoint; if (this.isStarted()) { let access = this.getAttackAccess(gameState); let dist = Math.min(); if (this.rallyPoint && gameState.ai.accessibility.getAccessValue(this.rallyPoint) == access) { rallyPoint = this.rallyPoint; dist = API3.SquareVectorDistance(this.position, rallyPoint); } // Then check if we have a nearer base (in case this attack has captured one) - for (let base of gameState.ai.HQ.baseManagers) + for (const base of gameState.ai.HQ.baseManagers()) { if (!base.anchor || !base.anchor.position()) continue; if (PETRA.getLandAccess(gameState, base.anchor) != access) continue; let newdist = API3.SquareVectorDistance(this.position, base.anchor.position()); if (newdist > dist) continue; dist = newdist; rallyPoint = base.anchor.position(); } } for (let ent of this.unitCollection.values()) { if (ent.getMetadata(PlayerID, "role") == "attack") ent.stopMoving(); if (rallyPoint) ent.moveToRange(rallyPoint[0], rallyPoint[1], 0, 15); this.removeUnit(ent); } } for (let unitCat in this.unitStat) this.unit[unitCat].unregister(); this.removeQueues(gameState); }; PETRA.AttackPlan.prototype.removeUnit = function(ent, update) { if (ent.getMetadata(PlayerID, "role") == "attack") { if (ent.hasClass("CitizenSoldier")) ent.setMetadata(PlayerID, "role", "worker"); else ent.setMetadata(PlayerID, "role", undefined); ent.setMetadata(PlayerID, "subrole", undefined); } ent.setMetadata(PlayerID, "plan", -1); if (update) this.unitCollection.updateEnt(ent); }; PETRA.AttackPlan.prototype.checkEvents = function(gameState, events) { for (let evt of events.EntityRenamed) { if (!this.target || this.target.id() != evt.entity) continue; if (this.type == "Raid" && !this.isStarted()) this.target = undefined; else this.target = gameState.getEntityById(evt.newentity); if (this.target) this.targetPos = this.target.position(); } for (let evt of events.OwnershipChanged) // capture event if (this.target && this.target.id() == evt.entity && gameState.isPlayerAlly(evt.to)) this.target = undefined; for (let evt of events.PlayerDefeated) { if (this.targetPlayer !== evt.playerId) continue; this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this); this.target = undefined; } if (!this.overseas || this.state !== "unexecuted") return; // let's check if an enemy has built a structure at our access for (let evt of events.Create) { let ent = gameState.getEntityById(evt.entity); if (!ent || !ent.position() || !ent.hasClass("Structure")) continue; if (!gameState.isPlayerEnemy(ent.owner())) continue; let access = PETRA.getLandAccess(gameState, ent); - for (let base of gameState.ai.HQ.baseManagers) + for (const base of gameState.ai.HQ.baseManagers()) { if (!base.anchor || !base.anchor.position()) continue; if (base.accessIndex != access) continue; this.overseas = 0; this.rallyPoint = base.anchor.position(); } } }; PETRA.AttackPlan.prototype.waitingForTransport = function() { for (let ent of this.unitCollection.values()) if (ent.getMetadata(PlayerID, "transport") !== undefined) return true; return false; }; PETRA.AttackPlan.prototype.hasSiegeUnits = function() { for (let ent of this.unitCollection.values()) if (PETRA.isSiegeUnit(ent)) return true; return false; }; PETRA.AttackPlan.prototype.hasForceOrder = function(data, value) { for (let ent of this.unitCollection.values()) { if (data && +ent.getMetadata(PlayerID, data) !== value) continue; let orders = ent.unitAIOrderData(); for (let order of orders) if (order.force) return true; } return false; }; /** * The center position of this attack may be in an inaccessible area. So we use the access * of the unit nearest to this center position. */ PETRA.AttackPlan.prototype.getAttackAccess = function(gameState) { for (let ent of this.unitCollection.filterNearest(this.position, 1).values()) return PETRA.getLandAccess(gameState, ent); return 0; }; PETRA.AttackPlan.prototype.debugAttack = function() { API3.warn("---------- attack " + this.name); for (let unitCat in this.unitStat) { let Unit = this.unitStat[unitCat]; API3.warn(unitCat + " num=" + this.unit[unitCat].length + " min=" + Unit.minSize + " need=" + Unit.targetSize); } API3.warn("------------------------------"); }; PETRA.AttackPlan.prototype.Serialize = function() { let properties = { "name": this.name, "type": this.type, "state": this.state, "forced": this.forced, "rallyPoint": this.rallyPoint, "overseas": this.overseas, "paused": this.paused, "maxCompletingTime": this.maxCompletingTime, "neededShips": this.neededShips, "unitStat": this.unitStat, "siegeState": this.siegeState, "position5TurnsAgo": this.position5TurnsAgo, "lastPosition": this.lastPosition, "position": this.position, "isBlocked": this.isBlocked, "targetPlayer": this.targetPlayer, "target": this.target !== undefined ? this.target.id() : undefined, "targetPos": this.targetPos, "uniqueTargetId": this.uniqueTargetId, "path": this.path }; return { "properties": properties }; }; PETRA.AttackPlan.prototype.Deserialize = function(gameState, data) { for (let key in data.properties) this[key] = data.properties[key]; if (this.target) this.target = gameState.getEntityById(this.target); this.failed = undefined; }; Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/baseManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/baseManager.js (revision 25875) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/baseManager.js (revision 25876) @@ -1,1104 +1,1113 @@ /** * Base Manager * Handles lower level economic stuffs. * Some tasks: * -tasking workers: gathering/hunting/building/repairing?/scouting/plans. * -giving feedback/estimates on GR * -achieving building stuff plans (scouting/getting ressource/building) or other long-staying plans. * -getting good spots for dropsites * -managing dropsite use in the base * -updating whatever needs updating, keeping track of stuffs (rebuilding needs…) */ -PETRA.BaseManager = function(gameState, Config) +PETRA.BaseManager = function(gameState, basesManager) { - this.Config = Config; + this.Config = basesManager.Config; this.ID = gameState.ai.uniqueIDs.bases++; + this.basesManager = basesManager; // anchor building: seen as the main building of the base. Needs to have territorial influence this.anchor = undefined; this.anchorId = undefined; this.accessIndex = undefined; // Maximum distance (from any dropsite) to look for resources // 3 areas are used: from 0 to max/4, from max/4 to max/2 and from max/2 to max this.maxDistResourceSquare = 360*360; this.constructing = false; // Defenders to train in this cc when its construction is finished this.neededDefenders = this.Config.difficulty > 2 ? 3 + 2*(this.Config.difficulty - 3) : 0; // vector for iterating, to check one use the HQ map. this.territoryIndices = []; this.timeNextIdleCheck = 0; }; PETRA.BaseManager.prototype.init = function(gameState, state) { if (state == "unconstructed") this.constructing = true; else if (state != "captured") this.neededDefenders = 0; this.workerObject = new PETRA.Worker(this); // entitycollections this.units = gameState.getOwnUnits().filter(API3.Filters.byMetadata(PlayerID, "base", this.ID)); this.workers = this.units.filter(API3.Filters.byMetadata(PlayerID, "role", "worker")); this.buildings = gameState.getOwnStructures().filter(API3.Filters.byMetadata(PlayerID, "base", this.ID)); this.mobileDropsites = this.units.filter(API3.Filters.isDropsite()); this.units.registerUpdates(); this.workers.registerUpdates(); this.buildings.registerUpdates(); this.mobileDropsites.registerUpdates(); // array of entity IDs, with each being this.dropsites = {}; this.dropsiteSupplies = {}; this.gatherers = {}; for (let res of Resources.GetCodes()) { this.dropsiteSupplies[res] = { "nearby": [], "medium": [], "faraway": [] }; this.gatherers[res] = { "nextCheck": 0, "used": 0, "lost": 0 }; } }; PETRA.BaseManager.prototype.reset = function(gameState, state) { if (state == "unconstructed") this.constructing = true; else this.constructing = false; if (state != "captured" || this.Config.difficulty < 3) this.neededDefenders = 0; else this.neededDefenders = 3 + 2 * (this.Config.difficulty - 3); }; PETRA.BaseManager.prototype.assignEntity = function(gameState, ent) { ent.setMetadata(PlayerID, "base", this.ID); this.units.updateEnt(ent); this.workers.updateEnt(ent); this.buildings.updateEnt(ent); if (ent.resourceDropsiteTypes() && !ent.hasClass("Unit")) this.assignResourceToDropsite(gameState, ent); }; PETRA.BaseManager.prototype.setAnchor = function(gameState, anchorEntity) { if (!anchorEntity.hasClass("CivCentre")) API3.warn("Error: Petra base " + this.ID + " has been assigned " + ent.templateName() + " as anchor."); else { this.anchor = anchorEntity; this.anchorId = anchorEntity.id(); this.anchor.setMetadata(PlayerID, "baseAnchor", true); - gameState.ai.HQ.resetBaseCache(); + this.basesManager.resetBaseCache(); } anchorEntity.setMetadata(PlayerID, "base", this.ID); this.buildings.updateEnt(anchorEntity); this.accessIndex = PETRA.getLandAccess(gameState, anchorEntity); return true; }; /* we lost our anchor. Let's reassign our units and buildings */ PETRA.BaseManager.prototype.anchorLost = function(gameState, ent) { this.anchor = undefined; this.anchorId = undefined; this.neededDefenders = 0; - gameState.ai.HQ.resetBaseCache(); + this.basesManager.resetBaseCache(); }; /** Set a building of an anchorless base */ PETRA.BaseManager.prototype.setAnchorlessEntity = function(gameState, ent) { if (!this.buildings.hasEntities()) { if (!PETRA.getBuiltEntity(gameState, ent).resourceDropsiteTypes()) API3.warn("Error: Petra base " + this.ID + " has been assigned " + ent.templateName() + " as origin."); this.accessIndex = PETRA.getLandAccess(gameState, ent); } else if (this.accessIndex != PETRA.getLandAccess(gameState, ent)) API3.warn(" Error: Petra base " + this.ID + " with access " + this.accessIndex + " has been assigned " + ent.templateName() + " with access" + PETRA.getLandAccess(gameState, ent)); ent.setMetadata(PlayerID, "base", this.ID); this.buildings.updateEnt(ent); return true; }; /** * Assign the resources around the dropsites of this basis in three areas according to distance, and sort them in each area. * Moving resources (animals) and buildable resources (fields) are treated elsewhere. */ PETRA.BaseManager.prototype.assignResourceToDropsite = function(gameState, dropsite) { if (this.dropsites[dropsite.id()]) { if (this.Config.debug > 0) warn("assignResourceToDropsite: dropsite already in the list. Should never happen"); return; } let accessIndex = this.accessIndex; let dropsitePos = dropsite.position(); let dropsiteId = dropsite.id(); this.dropsites[dropsiteId] = true; - if (this.ID == gameState.ai.HQ.baseManagers[0].ID) + if (this.ID == this.basesManager.baselessBase().ID) accessIndex = PETRA.getLandAccess(gameState, dropsite); let maxDistResourceSquare = this.maxDistResourceSquare; for (let type of dropsite.resourceDropsiteTypes()) { let resources = gameState.getResourceSupplies(type); if (!resources.length) continue; let nearby = this.dropsiteSupplies[type].nearby; let medium = this.dropsiteSupplies[type].medium; let faraway = this.dropsiteSupplies[type].faraway; resources.forEach(function(supply) { if (!supply.position()) return; // Moving resources and fields are treated differently. if (supply.hasClasses(["Animal", "Field"])) return; // quick accessibility check if (PETRA.getLandAccess(gameState, supply) != accessIndex) return; let dist = API3.SquareVectorDistance(supply.position(), dropsitePos); if (dist < maxDistResourceSquare) { if (dist < maxDistResourceSquare/16) // distmax/4 nearby.push({ "dropsite": dropsiteId, "id": supply.id(), "ent": supply, "dist": dist }); else if (dist < maxDistResourceSquare/4) // distmax/2 medium.push({ "dropsite": dropsiteId, "id": supply.id(), "ent": supply, "dist": dist }); else faraway.push({ "dropsite": dropsiteId, "id": supply.id(), "ent": supply, "dist": dist }); } }); nearby.sort((r1, r2) => r1.dist - r2.dist); medium.sort((r1, r2) => r1.dist - r2.dist); faraway.sort((r1, r2) => r1.dist - r2.dist); /* let debug = false; if (debug) { faraway.forEach(function(res){ Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [res.ent.id()], "rgb": [2,0,0]}); }); medium.forEach(function(res){ Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [res.ent.id()], "rgb": [0,2,0]}); }); nearby.forEach(function(res){ Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [res.ent.id()], "rgb": [0,0,2]}); }); } */ } // Allows all allies to use this dropsite except if base anchor to be sure to keep // a minimum of resources for this base Engine.PostCommand(PlayerID, { "type": "set-dropsite-sharing", "entities": [dropsiteId], "shared": dropsiteId != this.anchorId }); }; // completely remove the dropsite resources from our list. PETRA.BaseManager.prototype.removeDropsite = function(gameState, ent) { if (!ent.id()) return; let removeSupply = function(entId, supply){ for (let i = 0; i < supply.length; ++i) { // exhausted resource, remove it from this list if (!supply[i].ent || !gameState.getEntityById(supply[i].id)) supply.splice(i--, 1); // resource assigned to the removed dropsite, remove it else if (supply[i].dropsite == entId) supply.splice(i--, 1); } }; for (let type in this.dropsiteSupplies) { removeSupply(ent.id(), this.dropsiteSupplies[type].nearby); removeSupply(ent.id(), this.dropsiteSupplies[type].medium); removeSupply(ent.id(), this.dropsiteSupplies[type].faraway); } this.dropsites[ent.id()] = undefined; }; /** * @return {Object} - The position of the best place to build a new dropsite for the specified resource, * its quality and its template name. */ PETRA.BaseManager.prototype.findBestDropsiteAndLocation = function(gameState, resource) { let bestResult = { "quality": 0, "pos": [0, 0] }; for (const templateName of gameState.ai.HQ.buildManager.findStructuresByFilter(gameState, API3.Filters.isDropsite(resource))) { const dp = this.findBestDropsiteLocation(gameState, resource, templateName); if (dp.quality < bestResult.quality) continue; bestResult = dp; bestResult.templateName = templateName; } return bestResult; }; /** * Returns the position of the best place to build a new dropsite for the specified resource and dropsite template. */ PETRA.BaseManager.prototype.findBestDropsiteLocation = function(gameState, resource, templateName) { const template = gameState.getTemplate(gameState.applyCiv(templateName)); // CCs and Docks are handled elsewhere. if (template.hasClasses(["CivCentre", "Dock"])) return { "quality": 0, "pos": [0, 0] }; let halfSize = 0; if (template.get("Footprint/Square")) halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; else if (template.get("Footprint/Circle")) halfSize = +template.get("Footprint/Circle/@radius"); // This builds a map. The procedure is fairly simple. It adds the resource maps // (which are dynamically updated and are made so that they will facilitate DP placement) // Then checks for a good spot in the territory. If none, and town/city phase, checks outside // The AI will currently not build a CC if it wouldn't connect with an existing CC. let obstructions = PETRA.createObstructionMap(gameState, this.accessIndex, template); const dpEnts = gameState.getOwnStructures().filter(API3.Filters.isDropsite(resource)).toEntityArray(); // Foundations don't have the dropsite properties yet, so treat them separately. for (const foundation of gameState.getOwnFoundations().toEntityArray()) if (PETRA.getBuiltEntity(gameState, foundation).isResourceDropsite(resource)) dpEnts.push(foundation); let bestIdx; let bestVal = 0; let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); let territoryMap = gameState.ai.HQ.territoryMap; let width = territoryMap.width; let cellSize = territoryMap.cellSize; const droppableResources = template.resourceDropsiteTypes(); for (let j of this.territoryIndices) { let i = territoryMap.getNonObstructedTile(j, radius, obstructions); if (i < 0) // no room around continue; // We add 3 times the needed resource and once others that can be dropped here. let total = 2 * gameState.sharedScript.resourceMaps[resource].map[j]; for (const res in gameState.sharedScript.resourceMaps) if (droppableResources.indexOf(res) != -1) total += gameState.sharedScript.resourceMaps[res].map[j]; total *= 0.7; // Just a normalisation factor as the locateMap is limited to 255 if (total <= bestVal) continue; let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; for (let dp of dpEnts) { let dpPos = dp.position(); if (!dpPos) continue; let dist = API3.SquareVectorDistance(dpPos, pos); if (dist < 3600) { total = 0; break; } else if (dist < 6400) total *= (Math.sqrt(dist)-60)/20; } if (total <= bestVal) continue; if (gameState.ai.HQ.isDangerousLocation(gameState, pos, halfSize)) continue; bestVal = total; bestIdx = i; } if (this.Config.debug > 2) warn(" for dropsite best is " + bestVal); if (bestVal <= 0) return { "quality": bestVal, "pos": [0, 0] }; let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; return { "quality": bestVal, "pos": [x, z] }; }; -PETRA.BaseManager.prototype.getResourceLevel = function(gameState, type, nearbyOnly = false) +PETRA.BaseManager.prototype.getResourceLevel = function(gameState, type, distances = ["nearby", "medium", "faraway"]) { let count = 0; let check = {}; - for (let supply of this.dropsiteSupplies[type].nearby) - { - if (check[supply.id]) // avoid double counting as same resource can appear several time - continue; - check[supply.id] = true; - count += supply.ent.resourceSupplyAmount(); - } - if (nearbyOnly) - return count; - - for (let supply of this.dropsiteSupplies[type].medium) - { - if (check[supply.id]) - continue; - check[supply.id] = true; - count += 0.6*supply.ent.resourceSupplyAmount(); - } + for (const proxim of distances) + for (const supply of this.dropsiteSupplies[type][proxim]) + { + if (check[supply.id]) // avoid double counting as same resource can appear several time + continue; + check[supply.id] = true; + count += supply.ent.resourceSupplyAmount(); + } return count; }; /** check our resource levels and react accordingly */ PETRA.BaseManager.prototype.checkResourceLevels = function(gameState, queues) { for (let type of Resources.GetCodes()) { if (type == "food") { + const prox = ["nearby"]; + if (gameState.currentPhase() < 2) + prox.push("medium"); if (gameState.ai.HQ.canBuild(gameState, "structures/{civ}/field")) // let's see if we need to add new farms. { - let count = this.getResourceLevel(gameState, type, gameState.currentPhase() > 1); // animals are not accounted + const count = this.getResourceLevel(gameState, type, prox); // animals are not accounted let numFarms = gameState.getOwnStructures().filter(API3.Filters.byClass("Field")).length; // including foundations let numQueue = queues.field.countQueuedUnits(); // TODO if not yet farms, add a check on time used/lost and build farmstead if needed if (numFarms + numQueue == 0) // starting game, rely on fruits as long as we have enough of them { if (count < 600) { queues.field.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/field", { "favoredBase": this.ID })); gameState.ai.HQ.needFarm = true; } } else if (!gameState.ai.HQ.maxFields || numFarms + numQueue < gameState.ai.HQ.maxFields) { let numFound = gameState.getOwnFoundations().filter(API3.Filters.byClass("Field")).length; let goal = this.Config.Economy.provisionFields; if (gameState.ai.HQ.saveResources || gameState.ai.HQ.saveSpace || count > 300 || numFarms > 5) goal = Math.max(goal-1, 1); if (numFound + numQueue < goal) queues.field.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/field", { "favoredBase": this.ID })); } else if (gameState.ai.HQ.needCorral && !gameState.getOwnEntitiesByClass("Corral", true).hasEntities() && !queues.corral.hasQueuedUnits() && gameState.ai.HQ.canBuild(gameState, "structures/{civ}/corral")) queues.corral.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/corral", { "favoredBase": this.ID })); continue; } if (!gameState.getOwnEntitiesByClass("Corral", true).hasEntities() && !queues.corral.hasQueuedUnits() && gameState.ai.HQ.canBuild(gameState, "structures/{civ}/corral")) { - let count = this.getResourceLevel(gameState, type, gameState.currentPhase() > 1); // animals are not accounted + const count = this.getResourceLevel(gameState, type, prox); // animals are not accounted if (count < 900) { queues.corral.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/corral", { "favoredBase": this.ID })); gameState.ai.HQ.needCorral = true; } } continue; } // Non food stuff if (!gameState.sharedScript.resourceMaps[type] || queues.dropsites.hasQueuedUnits() || gameState.getOwnFoundations().filter(API3.Filters.byClass("Storehouse")).hasEntities()) { this.gatherers[type].nextCheck = gameState.ai.playedTurn; this.gatherers[type].used = 0; this.gatherers[type].lost = 0; continue; } if (gameState.ai.playedTurn < this.gatherers[type].nextCheck) continue; for (let ent of this.gatherersByType(gameState, type).values()) { if (ent.unitAIState() == "INDIVIDUAL.GATHER.GATHERING") ++this.gatherers[type].used; else if (ent.unitAIState() == "INDIVIDUAL.GATHER.RETURNINGRESOURCE.APPROACHING") ++this.gatherers[type].lost; } // TODO add also a test on remaining resources. let total = this.gatherers[type].used + this.gatherers[type].lost; if (total > 150 || total > 60 && type != "wood") { let ratio = this.gatherers[type].lost / total; if (ratio > 0.15) { const newDP = this.findBestDropsiteAndLocation(gameState, type); if (newDP.quality > 50 && gameState.ai.HQ.canBuild(gameState, newDP.templateName)) queues.dropsites.addPlan(new PETRA.ConstructionPlan(gameState, newDP.templateName, { "base": this.ID, "type": type }, newDP.pos)); else if (!gameState.getOwnFoundations().filter(API3.Filters.byClass("CivCentre")).hasEntities() && !queues.civilCentre.hasQueuedUnits()) { // No good dropsite, try to build a new base if no base already planned, // and if not possible, be less strict on dropsite quality. if ((!gameState.ai.HQ.canExpand || !gameState.ai.HQ.buildNewBase(gameState, queues, type)) && newDP.quality > Math.min(25, 50*0.15/ratio) && gameState.ai.HQ.canBuild(gameState, newDP.templateName)) queues.dropsites.addPlan(new PETRA.ConstructionPlan(gameState, newDP.templateName, { "base": this.ID, "type": type }, newDP.pos)); } } this.gatherers[type].nextCheck = gameState.ai.playedTurn + 20; this.gatherers[type].used = 0; this.gatherers[type].lost = 0; } else if (total == 0) this.gatherers[type].nextCheck = gameState.ai.playedTurn + 10; } }; /** Adds the estimated gather rates from this base to the currentRates */ PETRA.BaseManager.prototype.addGatherRates = function(gameState, currentRates) { for (let res in currentRates) { // I calculate the exact gathering rate for each unit. // I must then lower that to account for travel time. // Given that the faster you gather, the more travel time matters, // I use some logarithms. // TODO: this should take into account for unit speed and/or distance to target this.gatherersByType(gameState, res).forEach(ent => { if (ent.isIdle() || !ent.position()) return; let gRate = ent.currentGatherRate(); if (gRate) currentRates[res] += Math.log(1+gRate)/1.1; }); if (res == "food") { this.workersBySubrole(gameState, "hunter").forEach(ent => { if (ent.isIdle() || !ent.position()) return; let gRate = ent.currentGatherRate(); if (gRate) currentRates[res] += Math.log(1+gRate)/1.1; }); this.workersBySubrole(gameState, "fisher").forEach(ent => { if (ent.isIdle() || !ent.position()) return; let gRate = ent.currentGatherRate(); if (gRate) currentRates[res] += Math.log(1+gRate)/1.1; }); } } }; PETRA.BaseManager.prototype.assignRolelessUnits = function(gameState, roleless) { if (!roleless) roleless = this.units.filter(API3.Filters.not(API3.Filters.byHasMetadata(PlayerID, "role"))).values(); for (let ent of roleless) { if (ent.hasClasses(["Worker", "CitizenSoldier", "FishingBoat"])) ent.setMetadata(PlayerID, "role", "worker"); } }; /** * If the numbers of workers on the resources is unbalanced then set some of workers to idle so * they can be reassigned by reassignIdleWorkers. * TODO: actually this probably should be in the HQ. */ PETRA.BaseManager.prototype.setWorkersIdleByPriority = function(gameState) { this.timeNextIdleCheck = gameState.ai.elapsedTime + 8; // change resource only towards one which is more needed, and if changing will not change this order let nb = 1; // no more than 1 change per turn (otherwise we should update the rates) let mostNeeded = gameState.ai.HQ.pickMostNeededResources(gameState); let sumWanted = 0; let sumCurrent = 0; for (let need of mostNeeded) { sumWanted += need.wanted; sumCurrent += need.current; } let scale = 1; if (sumWanted > 0) scale = sumCurrent / sumWanted; for (let i = mostNeeded.length-1; i > 0; --i) { let lessNeed = mostNeeded[i]; for (let j = 0; j < i; ++j) { let moreNeed = mostNeeded[j]; let lastFailed = gameState.ai.HQ.lastFailedGather[moreNeed.type]; if (lastFailed && gameState.ai.elapsedTime - lastFailed < 20) continue; // Ensure that the most wanted resource is not exhausted - if (moreNeed.type != "food" && gameState.ai.HQ.isResourceExhausted(moreNeed.type)) + if (moreNeed.type != "food" && this.basesManager.isResourceExhausted(moreNeed.type)) { - if (lessNeed.type != "food" && gameState.ai.HQ.isResourceExhausted(lessNeed.type)) + if (lessNeed.type != "food" && this.basesManager.isResourceExhausted(lessNeed.type)) continue; // And if so, move the gatherer to the less wanted one. nb = this.switchGatherer(gameState, moreNeed.type, lessNeed.type, nb); if (nb == 0) return; } // If we assume a mean rate of 0.5 per gatherer, this diff should be > 1 // but we require a bit more to avoid too frequent changes if (scale*moreNeed.wanted - moreNeed.current - scale*lessNeed.wanted + lessNeed.current > 1.5 || - lessNeed.type != "food" && gameState.ai.HQ.isResourceExhausted(lessNeed.type)) + lessNeed.type != "food" && this.basesManager.isResourceExhausted(lessNeed.type)) { nb = this.switchGatherer(gameState, lessNeed.type, moreNeed.type, nb); if (nb == 0) return; } } } }; /** * Switch some gatherers (limited to number) from resource "from" to resource "to" * and return remaining number of possible switches. * Prefer FemaleCitizen for food and CitizenSoldier for other resources. */ PETRA.BaseManager.prototype.switchGatherer = function(gameState, from, to, number) { let num = number; let only; let gatherers = this.gatherersByType(gameState, from); if (from == "food" && gatherers.filter(API3.Filters.byClass("CitizenSoldier")).hasEntities()) only = "CitizenSoldier"; else if (to == "food" && gatherers.filter(API3.Filters.byClass("FemaleCitizen")).hasEntities()) only = "FemaleCitizen"; for (let ent of gatherers.values()) { if (num == 0) return num; if (!ent.canGather(to)) continue; if (only && !ent.hasClass(only)) continue; --num; ent.stopMoving(); ent.setMetadata(PlayerID, "gather-type", to); - gameState.ai.HQ.AddTCResGatherer(to); + this.basesManager.AddTCResGatherer(to); } return num; }; PETRA.BaseManager.prototype.reassignIdleWorkers = function(gameState, idleWorkers) { // Search for idle workers, and tell them to gather resources based on demand if (!idleWorkers) { let filter = API3.Filters.byMetadata(PlayerID, "subrole", "idle"); idleWorkers = gameState.updatingCollection("idle-workers-base-" + this.ID, filter, this.workers).values(); } for (let ent of idleWorkers) { // Check that the worker isn't garrisoned if (!ent.position()) continue; if (ent.hasClass("Worker")) { // Just emergency repairing here. It is better managed in assignToFoundations if (ent.isBuilder() && this.anchor && this.anchor.needsRepair() && gameState.getOwnEntitiesByMetadata("target-foundation", this.anchor.id()).length < 2) ent.repair(this.anchor); else if (ent.isGatherer()) { let mostNeeded = gameState.ai.HQ.pickMostNeededResources(gameState); for (let needed of mostNeeded) { if (!ent.canGather(needed.type)) continue; let lastFailed = gameState.ai.HQ.lastFailedGather[needed.type]; if (lastFailed && gameState.ai.elapsedTime - lastFailed < 20) continue; - if (needed.type != "food" && gameState.ai.HQ.isResourceExhausted(needed.type)) + if (needed.type != "food" && this.basesManager.isResourceExhausted(needed.type)) continue; ent.setMetadata(PlayerID, "subrole", "gatherer"); ent.setMetadata(PlayerID, "gather-type", needed.type); - gameState.ai.HQ.AddTCResGatherer(needed.type); + this.basesManager.AddTCResGatherer(needed.type); break; } } } else if (PETRA.isFastMoving(ent) && ent.canGather("food") && ent.canAttackClass("Animal")) ent.setMetadata(PlayerID, "subrole", "hunter"); else if (ent.hasClass("FishingBoat")) ent.setMetadata(PlayerID, "subrole", "fisher"); } }; PETRA.BaseManager.prototype.workersBySubrole = function(gameState, subrole) { return gameState.updatingCollection("subrole-" + subrole +"-base-" + this.ID, API3.Filters.byMetadata(PlayerID, "subrole", subrole), this.workers); }; PETRA.BaseManager.prototype.gatherersByType = function(gameState, type) { return gameState.updatingCollection("workers-gathering-" + type +"-base-" + this.ID, API3.Filters.byMetadata(PlayerID, "gather-type", type), this.workersBySubrole(gameState, "gatherer")); }; /** * returns an entity collection of workers. * They are idled immediatly and their subrole set to idle. */ PETRA.BaseManager.prototype.pickBuilders = function(gameState, workers, number) { let availableWorkers = this.workers.filter(ent => { if (!ent.position() || !ent.isBuilder()) return false; if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3) return false; if (ent.getMetadata(PlayerID, "transport")) return false; return true; }).toEntityArray(); availableWorkers.sort((a, b) => { let vala = 0; let valb = 0; if (a.getMetadata(PlayerID, "subrole") == "builder") vala = 100; if (b.getMetadata(PlayerID, "subrole") == "builder") valb = 100; if (a.getMetadata(PlayerID, "subrole") == "idle") vala = -50; if (b.getMetadata(PlayerID, "subrole") == "idle") valb = -50; if (a.getMetadata(PlayerID, "plan") === undefined) vala = -20; if (b.getMetadata(PlayerID, "plan") === undefined) valb = -20; return vala - valb; }); let needed = Math.min(number, availableWorkers.length - 3); for (let i = 0; i < needed; ++i) { availableWorkers[i].stopMoving(); availableWorkers[i].setMetadata(PlayerID, "subrole", "idle"); workers.addEnt(availableWorkers[i]); } return; }; /** * If we have some foundations, and we don't have enough builder-workers, * try reassigning some other workers who are nearby * AI tries to use builders sensibly, not completely stopping its econ. */ PETRA.BaseManager.prototype.assignToFoundations = function(gameState, noRepair) { let foundations = this.buildings.filter(API3.Filters.and(API3.Filters.isFoundation(), API3.Filters.not(API3.Filters.byClass("Field")))); let damagedBuildings = this.buildings.filter(ent => ent.foundationProgress() === undefined && ent.needsRepair()); // Check if nothing to build if (!foundations.length && !damagedBuildings.length) return; let workers = this.workers.filter(ent => ent.isBuilder()); let builderWorkers = this.workersBySubrole(gameState, "builder"); let idleBuilderWorkers = builderWorkers.filter(API3.Filters.isIdle()); // if we're constructing and we have the foundations to our base anchor, only try building that. if (this.constructing && foundations.filter(API3.Filters.byMetadata(PlayerID, "baseAnchor", true)).hasEntities()) { foundations = foundations.filter(API3.Filters.byMetadata(PlayerID, "baseAnchor", true)); let tID = foundations.toEntityArray()[0].id(); workers.forEach(ent => { let target = ent.getMetadata(PlayerID, "target-foundation"); if (target && target != tID) { ent.stopMoving(); ent.setMetadata(PlayerID, "target-foundation", tID); } }); } if (workers.length < 3) { - let fromOtherBase = gameState.ai.HQ.bulkPickWorkers(gameState, this, 2); + const fromOtherBase = this.basesManager.bulkPickWorkers(gameState, this, 2); if (fromOtherBase) { let baseID = this.ID; fromOtherBase.forEach(worker => { worker.setMetadata(PlayerID, "base", baseID); worker.setMetadata(PlayerID, "subrole", "builder"); workers.updateEnt(worker); builderWorkers.updateEnt(worker); idleBuilderWorkers.updateEnt(worker); }); } } let builderTot = builderWorkers.length - idleBuilderWorkers.length; // Make the limit on number of builders depends on the available resources let availableResources = gameState.ai.queueManager.getAvailableResources(gameState); let builderRatio = 1; for (let res of Resources.GetCodes()) { if (availableResources[res] < 200) { builderRatio = 0.2; break; } else if (availableResources[res] < 1000) builderRatio = Math.min(builderRatio, availableResources[res] / 1000); } for (let target of foundations.values()) { if (target.hasClass("Field")) continue; // we do not build fields if (gameState.ai.HQ.isNearInvadingArmy(target.position())) if (!target.hasClasses(["CivCentre", "Wall"]) && (!target.hasClass("Wonder") || !gameState.getVictoryConditions().has("wonder"))) continue; // if our territory has shrinked since this foundation was positioned, do not build it if (PETRA.isNotWorthBuilding(gameState, target)) continue; let assigned = gameState.getOwnEntitiesByMetadata("target-foundation", target.id()).length; let maxTotalBuilders = Math.ceil(workers.length * builderRatio); if (maxTotalBuilders < 2 && workers.length > 1) maxTotalBuilders = 2; if (target.hasClass("House") && gameState.getPopulationLimit() < gameState.getPopulation() + 5 && gameState.getPopulationLimit() < gameState.getPopulationMax()) maxTotalBuilders += 2; let targetNB = 2; if (target.hasClasses(["Fortress", "Wonder"]) || target.getMetadata(PlayerID, "phaseUp") == true) targetNB = 7; else if (target.hasClasses(["Barracks", "Range", "Stable", "Tower", "Market"])) targetNB = 4; else if (target.hasClasses(["House", "DropsiteWood"])) targetNB = 3; if (target.getMetadata(PlayerID, "baseAnchor") == true || target.hasClass("Wonder") && gameState.getVictoryConditions().has("wonder")) { targetNB = 15; maxTotalBuilders = Math.max(maxTotalBuilders, 15); } - // if no base yet, everybody should build - if (gameState.ai.HQ.numActiveBases() == 0) + if (!this.basesManager.hasActiveBase()) { targetNB = workers.length; maxTotalBuilders = targetNB; } if (assigned >= targetNB) continue; idleBuilderWorkers.forEach(function(ent) { if (ent.getMetadata(PlayerID, "target-foundation") !== undefined) return; if (assigned >= targetNB || !ent.position() || API3.SquareVectorDistance(ent.position(), target.position()) > 40000) return; ++assigned; ++builderTot; ent.setMetadata(PlayerID, "target-foundation", target.id()); }); if (assigned >= targetNB || builderTot >= maxTotalBuilders) continue; let nonBuilderWorkers = workers.filter(function(ent) { if (ent.getMetadata(PlayerID, "subrole") == "builder") return false; if (!ent.position()) return false; if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3) return false; if (ent.getMetadata(PlayerID, "transport")) return false; return true; }).toEntityArray(); let time = target.buildTime(); nonBuilderWorkers.sort((workerA, workerB) => { let coeffA = API3.SquareVectorDistance(target.position(), workerA.position()); if (workerA.getMetadata(PlayerID, "gather-type") == "food") coeffA *= 3; let coeffB = API3.SquareVectorDistance(target.position(), workerB.position()); if (workerB.getMetadata(PlayerID, "gather-type") == "food") coeffB *= 3; return coeffA - coeffB; }); let current = 0; let nonBuilderTot = nonBuilderWorkers.length; while (assigned < targetNB && builderTot < maxTotalBuilders && current < nonBuilderTot) { ++assigned; ++builderTot; let ent = nonBuilderWorkers[current++]; ent.stopMoving(); ent.setMetadata(PlayerID, "subrole", "builder"); ent.setMetadata(PlayerID, "target-foundation", target.id()); } } for (let target of damagedBuildings.values()) { // Don't repair if we're still under attack, unless it's a vital (civcentre or wall) building // that's being destroyed. if (gameState.ai.HQ.isNearInvadingArmy(target.position())) { if (target.healthLevel() > 0.5 || !target.hasClasses(["CivCentre", "Wall"]) && (!target.hasClass("Wonder") || !gameState.getVictoryConditions().has("wonder"))) continue; } else if (noRepair && !target.hasClass("CivCentre")) continue; if (target.decaying()) continue; let assigned = gameState.getOwnEntitiesByMetadata("target-foundation", target.id()).length; let maxTotalBuilders = Math.ceil(workers.length * builderRatio); let targetNB = 1; if (target.hasClasses(["Fortress", "Wonder"])) targetNB = 3; if (target.getMetadata(PlayerID, "baseAnchor") == true || target.hasClass("Wonder") && gameState.getVictoryConditions().has("wonder")) { maxTotalBuilders = Math.ceil(workers.length * Math.max(0.3, builderRatio)); targetNB = 5; if (target.healthLevel() < 0.3) { maxTotalBuilders = Math.ceil(workers.length * Math.max(0.6, builderRatio)); targetNB = 7; } } if (assigned >= targetNB) continue; idleBuilderWorkers.forEach(function(ent) { if (ent.getMetadata(PlayerID, "target-foundation") !== undefined) return; if (assigned >= targetNB || !ent.position() || API3.SquareVectorDistance(ent.position(), target.position()) > 40000) return; ++assigned; ++builderTot; ent.setMetadata(PlayerID, "target-foundation", target.id()); }); if (assigned >= targetNB || builderTot >= maxTotalBuilders) continue; let nonBuilderWorkers = workers.filter(function(ent) { if (ent.getMetadata(PlayerID, "subrole") == "builder") return false; if (!ent.position()) return false; if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3) return false; if (ent.getMetadata(PlayerID, "transport")) return false; return true; }); let num = Math.min(nonBuilderWorkers.length, targetNB-assigned); let nearestNonBuilders = nonBuilderWorkers.filterNearest(target.position(), num); nearestNonBuilders.forEach(function(ent) { ++assigned; ++builderTot; ent.stopMoving(); ent.setMetadata(PlayerID, "subrole", "builder"); ent.setMetadata(PlayerID, "target-foundation", target.id()); }); } }; /** Return false when the base is not active (no workers on it) */ PETRA.BaseManager.prototype.update = function(gameState, queues, events) { - if (this.ID == gameState.ai.HQ.baseManagers[0].ID) // base for unaffected units + if (this.ID == this.basesManager.baselessBase().ID) { // if some active base, reassigns the workers/buildings // otherwise look for anything useful to do, i.e. treasures to gather - if (gameState.ai.HQ.numActiveBases() > 0) + if (this.basesManager.hasActiveBase()) { for (let ent of this.units.values()) { let bestBase = PETRA.getBestBase(gameState, ent); if (bestBase.ID != this.ID) bestBase.assignEntity(gameState, ent); } for (let ent of this.buildings.values()) { let bestBase = PETRA.getBestBase(gameState, ent); if (!bestBase) { if (ent.hasClass("Dock")) - API3.warn("Petra: dock in baseManager[0]. It may be useful to do an anchorless base for " + ent.templateName()); + API3.warn("Petra: dock in 'noBase' baseManager. It may be useful to do an anchorless base for " + ent.templateName()); continue; } if (ent.resourceDropsiteTypes()) this.removeDropsite(gameState, ent); bestBase.assignEntity(gameState, ent); } } else if (gameState.ai.HQ.canBuildUnits) { this.assignToFoundations(gameState); if (gameState.ai.elapsedTime > this.timeNextIdleCheck) this.setWorkersIdleByPriority(gameState); this.assignRolelessUnits(gameState); this.reassignIdleWorkers(gameState); for (let ent of this.workers.values()) this.workerObject.update(gameState, ent); for (let ent of this.mobileDropsites.values()) this.workerObject.moveToGatherer(gameState, ent, false); } return false; } if (!this.anchor) // This anchor has been destroyed, but the base may still be usable { if (!this.buildings.hasEntities()) { // Reassign all remaining entities to its nearest base for (let ent of this.units.values()) { let base = PETRA.getBestBase(gameState, ent, false, this.ID); base.assignEntity(gameState, ent); } return false; } // If we have a base with anchor on the same land, reassign everything to it let reassignedBase; for (let ent of this.buildings.values()) { if (!ent.position()) continue; let base = PETRA.getBestBase(gameState, ent); if (base.anchor) reassignedBase = base; break; } if (reassignedBase) { for (let ent of this.units.values()) reassignedBase.assignEntity(gameState, ent); for (let ent of this.buildings.values()) { if (ent.resourceDropsiteTypes()) this.removeDropsite(gameState, ent); reassignedBase.assignEntity(gameState, ent); } return false; } this.assignToFoundations(gameState); if (gameState.ai.elapsedTime > this.timeNextIdleCheck) this.setWorkersIdleByPriority(gameState); this.assignRolelessUnits(gameState); this.reassignIdleWorkers(gameState); for (let ent of this.workers.values()) this.workerObject.update(gameState, ent); for (let ent of this.mobileDropsites.values()) this.workerObject.moveToGatherer(gameState, ent, false); return true; } Engine.ProfileStart("Base update - base " + this.ID); this.checkResourceLevels(gameState, queues); this.assignToFoundations(gameState); if (this.constructing) { let owner = gameState.ai.HQ.territoryMap.getOwner(this.anchor.position()); if(owner != 0 && !gameState.isPlayerAlly(owner)) { // we're in enemy territory. If we're too close from the enemy, destroy us. let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); for (let cc of ccEnts.values()) { if (cc.owner() != owner) continue; if (API3.SquareVectorDistance(cc.position(), this.anchor.position()) > 8000) continue; this.anchor.destroy(); - gameState.ai.HQ.resetBaseCache(); + this.basesManager.resetBaseCache(); break; } } } else if (this.neededDefenders && gameState.ai.HQ.trainEmergencyUnits(gameState, [this.anchor.position()])) --this.neededDefenders; if (gameState.ai.elapsedTime > this.timeNextIdleCheck && (gameState.currentPhase() > 1 || gameState.ai.HQ.phasing == 2)) this.setWorkersIdleByPriority(gameState); this.assignRolelessUnits(gameState); this.reassignIdleWorkers(gameState); // check if workers can find something useful to do for (let ent of this.workers.values()) this.workerObject.update(gameState, ent); for (let ent of this.mobileDropsites.values()) this.workerObject.moveToGatherer(gameState, ent, false); Engine.ProfileStop(); return true; }; +PETRA.BaseManager.prototype.AddTCGatherer = function(supplyID) +{ + return this.basesManager.AddTCGatherer(supplyID); +}; + +PETRA.BaseManager.prototype.RemoveTCGatherer = function(supplyID) +{ + this.basesManager.RemoveTCGatherer(supplyID); +}; + +PETRA.BaseManager.prototype.GetTCGatherer = function(supplyID) +{ + return this.basesManager.GetTCGatherer(supplyID); +}; + PETRA.BaseManager.prototype.Serialize = function() { return { "ID": this.ID, "anchorId": this.anchorId, "accessIndex": this.accessIndex, "maxDistResourceSquare": this.maxDistResourceSquare, "constructing": this.constructing, "gatherers": this.gatherers, "neededDefenders": this.neededDefenders, "territoryIndices": this.territoryIndices, "timeNextIdleCheck": this.timeNextIdleCheck }; }; PETRA.BaseManager.prototype.Deserialize = function(gameState, data) { for (let key in data) this[key] = data[key]; this.anchor = this.anchorId !== undefined ? gameState.getEntityById(this.anchorId) : undefined; }; Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/basesManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/basesManager.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/basesManager.js (revision 25876) @@ -0,0 +1,792 @@ +/** + * Bases Manager + * Manages the list of available bases and queries information from those (e.g. resource levels). + * Only one base is run every turn. + */ + +PETRA.BasesManager = function(Config) +{ + this.Config = Config; + + this.currentBase = 0; + + // Cache some quantities for performance. + this.turnCache = {}; + + // Deals with unit/structure without base. + this.noBase = undefined; + + this.baseManagers = []; +}; + +PETRA.BasesManager.prototype.init = function(gameState) +{ + // Initialize base map. Each pixel is a base ID, or 0 if not or not accessible. + this.basesMap = new API3.Map(gameState.sharedScript, "territory"); + + this.noBase = new PETRA.BaseManager(gameState, this); + this.noBase.init(gameState); + this.noBase.accessIndex = 0; + + for (const cc of gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")).values()) + if (cc.foundationProgress() === undefined) + this.createBase(gameState, cc); + else + this.createBase(gameState, cc, "unconstructed"); +}; + +/** + * Initialization needed after deserialization (only called when deserialising). + */ +PETRA.BasesManager.prototype.postinit = function(gameState) +{ + // Rebuild the base maps from the territory indices of each base. + this.basesMap = new API3.Map(gameState.sharedScript, "territory"); + for (const base of this.baseManagers) + for (const j of base.territoryIndices) + this.basesMap.map[j] = base.ID; + + for (const ent of gameState.getOwnEntities().values()) + { + if (!ent.resourceDropsiteTypes() || !ent.hasClass("Structure")) + continue; + // Entities which have been built or have changed ownership after the last AI turn have no base. + // they will be dealt with in the next checkEvents + const baseID = ent.getMetadata(PlayerID, "base"); + if (baseID === undefined) + continue; + const base = this.getBaseByID(baseID); + base.assignResourceToDropsite(gameState, ent); + } +}; + +/** + * Create a new base in the baseManager: + * If an existing one without anchor already exist, use it. + * Otherwise create a new one. + * TODO when buildings, criteria should depend on distance + * allowedType: undefined => new base with an anchor + * "unconstructed" => new base with a foundation anchor + * "captured" => captured base with an anchor + * "anchorless" => anchorless base, currently with dock + */ +PETRA.BasesManager.prototype.createBase = function(gameState, ent, type) +{ + const access = PETRA.getLandAccess(gameState, ent); + let newbase; + for (const base of this.baseManagers) + { + if (base.accessIndex != access) + continue; + if (type != "anchorless" && base.anchor) + continue; + if (type != "anchorless") + { + // TODO we keep the first one, we should rather use the nearest if buildings + // and possibly also cut on distance + newbase = base; + break; + } + else + { + // TODO here also test on distance instead of first + if (newbase && !base.anchor) + continue; + newbase = base; + if (newbase.anchor) + break; + } + } + + if (this.Config.debug > 0) + { + API3.warn(" ----------------------------------------------------------"); + API3.warn(" BasesManager createBase entrance avec access " + access + " and type " + type); + API3.warn(" with access " + uneval(this.baseManagers.map(base => base.accessIndex)) + + " and base nbr " + uneval(this.baseManagers.map(base => base.ID)) + + " and anchor " + uneval(this.baseManagers.map(base => !!base.anchor))); + } + + if (!newbase) + { + newbase = new PETRA.BaseManager(gameState, this); + newbase.init(gameState, type); + this.baseManagers.push(newbase); + } + else + newbase.reset(type); + + if (type != "anchorless") + newbase.setAnchor(gameState, ent); + else + newbase.setAnchorlessEntity(gameState, ent); + + return newbase; +}; + +/** TODO check if the new anchorless bases should be added to addBase */ +PETRA.BasesManager.prototype.checkEvents = function(gameState, events) +{ + let addBase = false; + + for (const evt of events.Destroy) + { + // Let's check we haven't lost an important building here. + if (evt && !evt.SuccessfulFoundation && evt.entityObj && evt.metadata && evt.metadata[PlayerID] && + evt.metadata[PlayerID].base) + { + const ent = evt.entityObj; + if (ent.owner() != PlayerID) + continue; + // A new base foundation was created and destroyed on the same (AI) turn + if (evt.metadata[PlayerID].base == -1 || evt.metadata[PlayerID].base == -2) + continue; + const base = this.getBaseByID(evt.metadata[PlayerID].base); + if (ent.resourceDropsiteTypes() && ent.hasClass("Structure")) + base.removeDropsite(gameState, ent); + if (evt.metadata[PlayerID].baseAnchor && evt.metadata[PlayerID].baseAnchor === true) + base.anchorLost(gameState, ent); + } + } + + for (const evt of events.EntityRenamed) + { + const ent = gameState.getEntityById(evt.newentity); + if (!ent || ent.owner() != PlayerID || ent.getMetadata(PlayerID, "base") === undefined) + continue; + const base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); + if (!base.anchorId || base.anchorId != evt.entity) + continue; + base.anchorId = evt.newentity; + base.anchor = ent; + } + + for (const evt of events.Create) + { + // Let's check if we have a valuable foundation needing builders quickly + // (normal foundations are taken care in baseManager.assignToFoundations) + const ent = gameState.getEntityById(evt.entity); + if (!ent || ent.owner() != PlayerID || ent.foundationProgress() === undefined) + continue; + + if (ent.getMetadata(PlayerID, "base") == -1) // Standard base around a cc + { + // Okay so let's try to create a new base around this. + const newbase = this.createBase(gameState, ent, "unconstructed"); + // Let's get a few units from other bases there to build this. + const builders = this.bulkPickWorkers(gameState, newbase, 10); + if (builders !== false) + { + builders.forEach(worker => { + worker.setMetadata(PlayerID, "base", newbase.ID); + worker.setMetadata(PlayerID, "subrole", "builder"); + worker.setMetadata(PlayerID, "target-foundation", ent.id()); + }); + } + } + else if (ent.getMetadata(PlayerID, "base") == -2) // anchorless base around a dock + { + const newbase = this.createBase(gameState, ent, "anchorless"); + // Let's get a few units from other bases there to build this. + const builders = this.bulkPickWorkers(gameState, newbase, 4); + if (builders != false) + { + builders.forEach(worker => { + worker.setMetadata(PlayerID, "base", newbase.ID); + worker.setMetadata(PlayerID, "subrole", "builder"); + worker.setMetadata(PlayerID, "target-foundation", ent.id()); + }); + } + } + } + + for (const evt of events.ConstructionFinished) + { + if (evt.newentity == evt.entity) // repaired building + continue; + const ent = gameState.getEntityById(evt.newentity); + if (!ent || ent.owner() != PlayerID) + continue; + if (ent.getMetadata(PlayerID, "base") === undefined) + continue; + const base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); + base.buildings.updateEnt(ent); + if (ent.resourceDropsiteTypes()) + base.assignResourceToDropsite(gameState, ent); + + if (ent.getMetadata(PlayerID, "baseAnchor") === true) + { + if (base.constructing) + base.constructing = false; + addBase = true; + } + } + + for (const evt of events.OwnershipChanged) + { + if (evt.from == PlayerID) + { + const ent = gameState.getEntityById(evt.entity); + if (!ent || ent.getMetadata(PlayerID, "base") === undefined) + continue; + const base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); + if (ent.resourceDropsiteTypes() && ent.hasClass("Structure")) + base.removeDropsite(gameState, ent); + if (ent.getMetadata(PlayerID, "baseAnchor") === true) + base.anchorLost(gameState, ent); + } + + if (evt.to != PlayerID) + continue; + const ent = gameState.getEntityById(evt.entity); + if (!ent) + continue; + if (ent.hasClass("Unit")) + { + PETRA.getBestBase(gameState, ent).assignEntity(gameState, ent); + continue; + } + if (ent.hasClass("CivCentre")) // build a new base around it + { + let newbase; + if (ent.foundationProgress() !== undefined) + newbase = this.createBase(gameState, ent, "unconstructed"); + else + { + newbase = this.createBase(gameState, ent, "captured"); + addBase = true; + } + newbase.assignEntity(gameState, ent); + } + else + { + let base; + // If dropsite on new island, create a base around it + if (!ent.decaying() && ent.resourceDropsiteTypes()) + base = this.createBase(gameState, ent, "anchorless"); + else + base = PETRA.getBestBase(gameState, ent) || this.noBase; + base.assignEntity(gameState, ent); + } + } + + for (const evt of events.TrainingFinished) + { + for (const entId of evt.entities) + { + const ent = gameState.getEntityById(entId); + if (!ent || !ent.isOwn(PlayerID)) + continue; + + // Assign it immediately to something useful to do. + if (ent.getMetadata(PlayerID, "role") == "worker") + { + let base; + if (ent.getMetadata(PlayerID, "base") === undefined) + { + base = PETRA.getBestBase(gameState, ent); + base.assignEntity(gameState, ent); + } + else + base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); + base.reassignIdleWorkers(gameState, [ent]); + base.workerObject.update(gameState, ent); + } + else if (ent.resourceSupplyType() && ent.position()) + { + const type = ent.resourceSupplyType(); + if (!type.generic) + continue; + const dropsites = gameState.getOwnDropsites(type.generic); + const pos = ent.position(); + const access = PETRA.getLandAccess(gameState, ent); + let distmin = Math.min(); + let goal; + for (const dropsite of dropsites.values()) + { + if (!dropsite.position() || PETRA.getLandAccess(gameState, dropsite) != access) + continue; + const dist = API3.SquareVectorDistance(pos, dropsite.position()); + if (dist > distmin) + continue; + distmin = dist; + goal = dropsite.position(); + } + if (goal) + ent.moveToRange(goal[0], goal[1]); + } + } + } + + if (addBase) + gameState.ai.HQ.handleNewBase(gameState); +}; + +/** + * returns an entity collection of workers through BaseManager.pickBuilders + * TODO: when same accessIndex, sort by distance + */ +PETRA.BasesManager.prototype.bulkPickWorkers = function(gameState, baseRef, number) +{ + const accessIndex = baseRef.accessIndex; + if (!accessIndex) + return false; + const baseBest = this.baseManagers.slice(); + // We can also use workers without a base. + baseBest.push(this.noBase); + baseBest.sort((a, b) => { + if (a.accessIndex == accessIndex && b.accessIndex != accessIndex) + return -1; + else if (b.accessIndex == accessIndex && a.accessIndex != accessIndex) + return 1; + return 0; + }); + + let needed = number; + const workers = new API3.EntityCollection(gameState.sharedScript); + for (const base of baseBest) + { + if (base.ID == baseRef.ID) + continue; + base.pickBuilders(gameState, workers, needed); + if (workers.length >= number) + break; + needed = number - workers.length; + } + if (!workers.length) + return false; + return workers; +}; + +/** + * @return {Object} - Resources (estimation) still gatherable in our territory. + */ +PETRA.BasesManager.prototype.getTotalResourceLevel = function(gameState, resources = Resources.GetCodes(), proximity = ["nearby", "medium"]) +{ + const total = {}; + for (const res of resources) + total[res] = 0; + for (const base of this.baseManagers) + for (const res in total) + total[res] += base.getResourceLevel(gameState, res, proximity); + + return total; +}; + +/** + * Returns the current gather rate + * This is not per-se exact, it performs a few adjustments ad-hoc to account for travel distance, stuffs like that. + */ +PETRA.BasesManager.prototype.GetCurrentGatherRates = function(gameState) +{ + if (!this.turnCache.currentRates) + { + const currentRates = {}; + for (const res of Resources.GetCodes()) + currentRates[res] = 0.5 * this.GetTCResGatherer(res); + + this.addGatherRates(gameState, currentRates); + + for (const res of Resources.GetCodes()) + currentRates[res] = Math.max(currentRates[res], 0); + + this.turnCache.currentRates = currentRates; + } + + return this.turnCache.currentRates; +}; + +/** Some functions that register that we assigned a gatherer to a resource this turn */ + +/** Add a gatherer to the turn cache for this supply. */ +PETRA.BasesManager.prototype.AddTCGatherer = function(supplyID) +{ + if (this.turnCache.resourceGatherer && this.turnCache.resourceGatherer[supplyID] !== undefined) + ++this.turnCache.resourceGatherer[supplyID]; + else + { + if (!this.turnCache.resourceGatherer) + this.turnCache.resourceGatherer = {}; + this.turnCache.resourceGatherer[supplyID] = 1; + } +}; + +/** Remove a gatherer from the turn cache for this supply. */ +PETRA.BasesManager.prototype.RemoveTCGatherer = function(supplyID) +{ + if (this.turnCache.resourceGatherer && this.turnCache.resourceGatherer[supplyID]) + --this.turnCache.resourceGatherer[supplyID]; + else + { + if (!this.turnCache.resourceGatherer) + this.turnCache.resourceGatherer = {}; + this.turnCache.resourceGatherer[supplyID] = -1; + } +}; + +PETRA.BasesManager.prototype.GetTCGatherer = function(supplyID) +{ + if (this.turnCache.resourceGatherer && this.turnCache.resourceGatherer[supplyID]) + return this.turnCache.resourceGatherer[supplyID]; + + return 0; +}; + +/** The next two are to register that we assigned a gatherer to a resource this turn. */ +PETRA.BasesManager.prototype.AddTCResGatherer = function(resource) +{ + const check = "resourceGatherer-" + resource; + if (this.turnCache[check]) + ++this.turnCache[check]; + else + this.turnCache[check] = 1; + + if (this.turnCache.currentRates) + this.turnCache.currentRates[resource] += 0.5; +}; + +PETRA.BasesManager.prototype.GetTCResGatherer = function(resource) +{ + const check = "resourceGatherer-" + resource; + if (this.turnCache[check]) + return this.turnCache[check]; + + return 0; +}; + +/** + * flag a resource as exhausted + */ +PETRA.BasesManager.prototype.isResourceExhausted = function(resource) +{ + const check = "exhausted-" + resource; + if (this.turnCache[check] == undefined) + this.turnCache[check] = this.basesManager.isResourceExhausted(resource); + + return this.turnCache[check]; +}; + +/** + * returns the number of bases with a cc + * ActiveBases includes only those with a built cc + * PotentialBases includes also those with a cc in construction + */ +PETRA.BasesManager.prototype.numActiveBases = function() +{ + if (!this.turnCache.base) + this.updateBaseCache(); + return this.turnCache.base.active; +}; + +PETRA.BasesManager.prototype.hasActiveBase = function() +{ + return !!this.numActiveBases(); +}; + +PETRA.BasesManager.prototype.numPotentialBases = function() +{ + if (!this.turnCache.base) + this.updateBaseCache(); + return this.turnCache.base.potential; +}; + +PETRA.BasesManager.prototype.hasPotentialBase = function() +{ + return !!this.numPotentialBases(); +}; + +/** + * Updates the number of active and potential bases. + * .potential {number} - Bases that may or may not still be a foundation. + * .active {number} - Usable bases. + */ +PETRA.BasesManager.prototype.updateBaseCache = function() +{ + this.turnCache.base = { "active": 0, "potential": 0 }; + for (const base of this.baseManagers) + { + if (!base.anchor) + continue; + ++this.turnCache.base.potential; + if (base.anchor.foundationProgress() === undefined) + ++this.turnCache.base.active; + } +}; + +PETRA.BasesManager.prototype.resetBaseCache = function() +{ + this.turnCache.base = undefined; +}; + +PETRA.BasesManager.prototype.baselessBase = function() +{ + return this.noBase; +}; + +/** + * @param {number} baseID + * @return {Object} - The base corresponding to baseID. + */ +PETRA.BasesManager.prototype.getBaseByID = function(baseID) +{ + if (this.noBase.ID === baseID) + return this.noBase; + return this.baseManagers.find(base => base.ID === baseID); +}; + +/** + * flag a resource as exhausted + */ +PETRA.BasesManager.prototype.isResourceExhausted = function(resource) +{ + return this.baseManagers.every(base => + !base.dropsiteSupplies[resource].nearby.length && + !base.dropsiteSupplies[resource].medium.length && + !base.dropsiteSupplies[resource].faraway.length); +}; + +/** + * Count gatherers returning resources in the number of gatherers of resourceSupplies + * to prevent the AI always reassigning idle workers to these resourceSupplies (specially in naval maps). + */ +PETRA.BasesManager.prototype.assignGatherers = function() +{ + for (const base of this.baseManagers) + for (const worker of base.workers.values()) + { + if (worker.unitAIState().split(".").indexOf("RETURNRESOURCE") === -1) + continue; + const orders = worker.unitAIOrderData(); + if (orders.length < 2 || !orders[1].target || orders[1].target != worker.getMetadata(PlayerID, "supply")) + continue; + this.AddTCGatherer(orders[1].target); + } +}; + +/** + * Assign an entity to the closest base. + * Used by the starting strategy. + */ +PETRA.BasesManager.prototype.assignEntity = function(gameState, ent, territoryIndex) +{ + let bestbase; + for (const base of this.baseManagers) + { + if ((!ent.getMetadata(PlayerID, "base") || ent.getMetadata(PlayerID, "base") != base.ID) && + base.territoryIndices.indexOf(territoryIndex) == -1) + continue; + base.assignEntity(gameState, ent); + bestbase = base; + break; + } + if (!bestbase) // entity outside our territory + { + if (ent.hasClass("Structure") && !ent.decaying() && ent.resourceDropsiteTypes()) + bestbase = this.createBase(gameState, ent, "anchorless"); + else + bestbase = PETRA.getBestBase(gameState, ent) || this.noBase; + bestbase.assignEntity(gameState, ent); + } + // now assign entities garrisoned inside this entity + if (ent.isGarrisonHolder() && ent.garrisoned().length) + for (const id of ent.garrisoned()) + bestbase.assignEntity(gameState, gameState.getEntityById(id)); + // and find something useful to do if we already have a base + if (ent.position() && bestbase.ID !== this.noBase.ID) + { + bestbase.assignRolelessUnits(gameState, [ent]); + if (ent.getMetadata(PlayerID, "role") === "worker") + { + bestbase.reassignIdleWorkers(gameState, [ent]); + bestbase.workerObject.update(gameState, ent); + } + } +}; + +/** + * Adds the gather rates of individual bases to a shared object. + * @param {Object} gameState + * @param {Object} rates - The rates to add the gather rates to. + */ +PETRA.BasesManager.prototype.addGatherRates = function(gameState, rates) +{ + for (const base of this.baseManagers) + base.addGatherRates(gameState, rates); +}; + +/** + * @param {number} territoryIndex + * @return {number} - The ID of the base at the given territory index. + */ +PETRA.BasesManager.prototype.baseAtIndex = function(territoryIndex) +{ + return this.basesMap.map[territoryIndex]; +}; + +/** + * @param {number} territoryIndex + */ +PETRA.BasesManager.prototype.removeBaseFromTerritoryIndex = function(territoryIndex) +{ + const baseID = this.basesMap.map[territoryIndex]; + if (baseID == 0) + return; + const base = this.getBaseByID(baseID); + if (base) + { + const index = base.territoryIndices.indexOf(territoryIndex); + if (index != -1) + base.territoryIndices.splice(index, 1); + else + API3.warn(" problem in headquarters::updateTerritories for base " + baseID); + } + else + API3.warn(" problem in headquarters::updateTerritories without base " + baseID); + this.basesMap.map[territoryIndex] = 0; +}; + +/** + * @return {boolean} - Whether the index was added to a base. + */ +PETRA.BasesManager.prototype.addTerritoryIndexToBase = function(gameState, territoryIndex, passabilityMap) +{ + if (this.baseAtIndex(territoryIndex) != 0) + return false; + let landPassable = false; + const ind = API3.getMapIndices(territoryIndex, gameState.ai.HQ.territoryMap, passabilityMap); + let access; + for (const k of ind) + { + if (!gameState.ai.HQ.landRegions[gameState.ai.accessibility.landPassMap[k]]) + continue; + landPassable = true; + access = gameState.ai.accessibility.landPassMap[k]; + break; + } + if (!landPassable) + return false; + let distmin = Math.min(); + let baseID; + const pos = [gameState.ai.HQ.territoryMap.cellSize * (territoryIndex % gameState.ai.HQ.territoryMap.width + 0.5), gameState.ai.HQ.territoryMap.cellSize * (Math.floor(territoryIndex / gameState.ai.HQ.territoryMap.width) + 0.5)]; + for (const base of this.baseManagers) + { + if (!base.anchor || !base.anchor.position()) + continue; + if (base.accessIndex != access) + continue; + const dist = API3.SquareVectorDistance(base.anchor.position(), pos); + if (dist >= distmin) + continue; + distmin = dist; + baseID = base.ID; + } + if (!baseID) + return false; + this.getBaseByID(baseID).territoryIndices.push(territoryIndex); + this.basesMap.map[territoryIndex] = baseID; + return true; +}; + +/** Reassign territories when a base is going to be deleted */ +PETRA.BasesManager.prototype.reassignTerritories = function(deletedBase, territoryMap) +{ + const cellSize = territoryMap.cellSize; + const width = territoryMap.width; + for (let j = 0; j < territoryMap.length; ++j) + { + if (this.basesMap.map[j] != deletedBase.ID) + continue; + if (territoryMap.getOwnerIndex(j) != PlayerID) + { + API3.warn("Petra reassignTerritories: should never happen"); + this.basesMap.map[j] = 0; + continue; + } + + let distmin = Math.min(); + let baseID; + const pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; + for (const base of this.baseManagers) + { + if (!base.anchor || !base.anchor.position()) + continue; + if (base.accessIndex != deletedBase.accessIndex) + continue; + const dist = API3.SquareVectorDistance(base.anchor.position(), pos); + if (dist >= distmin) + continue; + distmin = dist; + baseID = base.ID; + } + if (baseID) + { + this.getBaseByID(baseID).territoryIndices.push(j); + this.basesMap.map[j] = baseID; + } + else + this.basesMap.map[j] = 0; + } +}; + +/** + * We will loop only on one active base per turn. + */ +PETRA.BasesManager.prototype.update = function(gameState, queues, events) +{ + Engine.ProfileStart("BasesManager update"); + + this.turnCache = {}; + this.assignGatherers(); + let nbBases = this.baseManagers.length; + let activeBase; + this.noBase.update(gameState, queues, events); + do + { + this.currentBase %= this.baseManagers.length; + activeBase = this.baseManagers[this.currentBase++].update(gameState, queues, events); + --nbBases; + // TODO what to do with this.reassignTerritories(this.baseManagers[this.currentBase]); + } + while (!activeBase && nbBases != 0); + + Engine.ProfileStop(); +}; + +PETRA.BasesManager.prototype.Serialize = function() +{ + const properties = { + "currentBase": this.currentBase + }; + + const baseManagers = []; + for (const base of this.baseManagers) + baseManagers.push(base.Serialize()); + + return { + "properties": properties, + "noBase": this.noBase.Serialize(), + "baseManagers": baseManagers + }; +}; + +PETRA.BasesManager.prototype.Deserialize = function(gameState, data) +{ + for (const key in data.properties) + this[key] = data.properties[key]; + + this.noBase = new PETRA.BaseManager(gameState, this); + this.noBase.Deserialize(gameState, data.noBase); + this.noBase.init(gameState); + this.noBase.Deserialize(gameState, data.noBase); + + this.baseManagers = []; + for (const basedata of data.baseManagers) + { + // The first call to deserialize set the ID base needed by entitycollections. + const newbase = new PETRA.BaseManager(gameState, this); + newbase.Deserialize(gameState, basedata); + newbase.init(gameState); + newbase.Deserialize(gameState, basedata); + this.baseManagers.push(newbase); + } +}; Property changes on: ps/trunk/binaries/data/mods/public/simulation/ai/petra/basesManager.js ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Added: svn:mime-type ## -0,0 +1 ## +text/plain \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/defenseManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/defenseManager.js (revision 25875) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/defenseManager.js (revision 25876) @@ -1,953 +1,953 @@ PETRA.DefenseManager = function(Config) { // Array of "army" Objects. this.armies = []; this.Config = Config; this.targetList = []; this.armyMergeSize = this.Config.Defense.armyMergeSize; // Stats on how many enemies are currently attacking our allies // this.attackingArmies[enemy][ally] = number of enemy armies inside allied territory // this.attackingUnits[enemy][ally] = number of enemy units not in armies inside allied territory // this.attackedAllies[ally] = number of enemies attacking the ally this.attackingArmies = {}; this.attackingUnits = {}; this.attackedAllies = {}; }; PETRA.DefenseManager.prototype.update = function(gameState, events) { Engine.ProfileStart("Defense Manager"); this.territoryMap = gameState.ai.HQ.territoryMap; this.checkEvents(gameState, events); // Check if our potential targets are still valid. for (let i = 0; i < this.targetList.length; ++i) { let target = gameState.getEntityById(this.targetList[i]); if (!target || !target.position() || !gameState.isPlayerEnemy(target.owner())) this.targetList.splice(i--, 1); } // Count the number of enemies attacking our allies in the previous turn. // We'll be more cooperative if several enemies are attacking him simultaneously. this.attackedAllies = {}; let attackingArmies = clone(this.attackingArmies); for (let enemy in this.attackingUnits) { if (!this.attackingUnits[enemy]) continue; for (let ally in this.attackingUnits[enemy]) { if (this.attackingUnits[enemy][ally] < 8) continue; if (attackingArmies[enemy] === undefined) attackingArmies[enemy] = {}; if (attackingArmies[enemy][ally] === undefined) attackingArmies[enemy][ally] = 0; attackingArmies[enemy][ally] += 1; } } for (let enemy in attackingArmies) { for (let ally in attackingArmies[enemy]) { if (this.attackedAllies[ally] === undefined) this.attackedAllies[ally] = 0; this.attackedAllies[ally] += 1; } } this.checkEnemyArmies(gameState); this.checkEnemyUnits(gameState); this.assignDefenders(gameState); Engine.ProfileStop(); }; PETRA.DefenseManager.prototype.makeIntoArmy = function(gameState, entityID, type = "default") { if (type == "default") { for (let army of this.armies) if (army.getType() == type && army.addFoe(gameState, entityID)) return; } this.armies.push(new PETRA.DefenseArmy(gameState, [entityID], type)); }; PETRA.DefenseManager.prototype.getArmy = function(partOfArmy) { return this.armies.find(army => army.ID == partOfArmy); }; PETRA.DefenseManager.prototype.isDangerous = function(gameState, entity) { if (!entity.position()) return false; let territoryOwner = this.territoryMap.getOwner(entity.position()); if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) return false; // Check if the entity is trying to build a new base near our buildings, // and if yes, add this base in our target list. if (entity.unitAIState() && entity.unitAIState() == "INDIVIDUAL.REPAIR.REPAIRING") { let targetId = entity.unitAIOrderData()[0].target; if (this.targetList.indexOf(targetId) != -1) return true; let target = gameState.getEntityById(targetId); if (target) { let isTargetEnemy = gameState.isPlayerEnemy(target.owner()); if (isTargetEnemy && territoryOwner == PlayerID) { if (target.hasClass("Structure")) this.targetList.push(targetId); return true; } else if (isTargetEnemy && target.hasClass("CivCentre")) { let myBuildings = gameState.getOwnStructures(); for (let building of myBuildings.values()) { if (building.foundationProgress() == 0) continue; if (API3.SquareVectorDistance(building.position(), entity.position()) > 30000) continue; this.targetList.push(targetId); return true; } } } } if (entity.attackTypes() === undefined || entity.hasClass("Support")) return false; let dist2Min = 6000; // TODO the 30 is to take roughly into account the structure size in following checks. Can be improved. if (entity.attackTypes().indexOf("Ranged") != -1) dist2Min = (entity.attackRange("Ranged").max + 30) * (entity.attackRange("Ranged").max + 30); for (let targetId of this.targetList) { let target = gameState.getEntityById(targetId); // The enemy base is either destroyed or built. if (!target || !target.position()) continue; if (API3.SquareVectorDistance(target.position(), entity.position()) < dist2Min) return true; } let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); for (let cc of ccEnts.values()) { if (!gameState.isEntityExclusiveAlly(cc) || cc.foundationProgress() == 0) continue; let cooperation = this.GetCooperationLevel(cc.owner()); if (cooperation < 0.3 || cooperation < 0.6 && !!cc.foundationProgress()) continue; if (API3.SquareVectorDistance(cc.position(), entity.position()) < dist2Min) return true; } for (let building of gameState.getOwnStructures().values()) { if (building.foundationProgress() == 0 || API3.SquareVectorDistance(building.position(), entity.position()) > dist2Min) continue; if (!this.territoryMap.isBlinking(building.position()) || gameState.ai.HQ.isDefendable(building)) return true; } if (gameState.isPlayerMutualAlly(territoryOwner)) { // If ally attacked by more than 2 enemies, help him not only for cc but also for structures. if (territoryOwner != PlayerID && this.attackedAllies[territoryOwner] && this.attackedAllies[territoryOwner] > 1 && this.GetCooperationLevel(territoryOwner) > 0.7) { for (let building of gameState.getAllyStructures(territoryOwner).values()) { if (building.foundationProgress() == 0 || API3.SquareVectorDistance(building.position(), entity.position()) > dist2Min) continue; if (!this.territoryMap.isBlinking(building.position())) return true; } } // Update the number of enemies attacking this ally. let enemy = entity.owner(); if (this.attackingUnits[enemy] === undefined) this.attackingUnits[enemy] = {}; if (this.attackingUnits[enemy][territoryOwner] === undefined) this.attackingUnits[enemy][territoryOwner] = 0; this.attackingUnits[enemy][territoryOwner] += 1; } return false; }; PETRA.DefenseManager.prototype.checkEnemyUnits = function(gameState) { const nbPlayers = gameState.sharedScript.playersData.length; let i = gameState.ai.playedTurn % nbPlayers; this.attackingUnits[i] = undefined; if (i == PlayerID) { if (!this.armies.length) { // Check if we can recover capture points from any of our notdecaying structures. for (let ent of gameState.getOwnStructures().values()) { if (ent.decaying()) continue; let capture = ent.capturePoints(); if (capture === undefined) continue; let lost = 0; for (let j = 0; j < capture.length; ++j) if (gameState.isPlayerEnemy(j)) lost += capture[j]; if (lost < Math.ceil(0.25 * capture[i])) continue; this.makeIntoArmy(gameState, ent.id(), "capturing"); break; } } return; } else if (!gameState.isPlayerEnemy(i)) return; for (let ent of gameState.getEnemyUnits(i).values()) { if (ent.getMetadata(PlayerID, "PartOfArmy") !== undefined) continue; // Keep animals attacking us or our allies. if (ent.hasClass("Animal")) { if (!ent.unitAIState() || ent.unitAIState().split(".")[1] != "COMBAT") continue; let orders = ent.unitAIOrderData(); if (!orders || !orders.length || !orders[0].target) continue; let target = gameState.getEntityById(orders[0].target); if (!target || !gameState.isPlayerAlly(target.owner())) continue; } // TODO what to do for ships ? if (ent.hasClasses(["Ship", "Trader"])) continue; // Check if unit is dangerous "a priori". if (this.isDangerous(gameState, ent)) this.makeIntoArmy(gameState, ent.id()); } - if (i != 0 || this.armies.length > 1 || gameState.ai.HQ.numActiveBases() == 0) + if (i != 0 || this.armies.length > 1 || !gameState.ai.HQ.hasActiveBase()) return; // Look for possible gaia buildings inside our territory (may happen when enemy resign or after structure decay) // and attack it only if useful (and capturable) or dangereous. for (let ent of gameState.getEnemyStructures(i).values()) { if (!ent.position() || ent.getMetadata(PlayerID, "PartOfArmy") !== undefined) continue; if (!ent.capturePoints() && !ent.hasDefensiveFire()) continue; let owner = this.territoryMap.getOwner(ent.position()); if (owner == PlayerID) this.makeIntoArmy(gameState, ent.id(), "capturing"); } }; PETRA.DefenseManager.prototype.checkEnemyArmies = function(gameState) { for (let i = 0; i < this.armies.length; ++i) { let army = this.armies[i]; // This returns a list of IDs: the units that broke away from the army for being too far. let breakaways = army.update(gameState); // Assume dangerosity. for (let breaker of breakaways) this.makeIntoArmy(gameState, breaker); if (army.getState() == 0) { if (army.getType() == "default") this.switchToAttack(gameState, army); army.clear(gameState); this.armies.splice(i--, 1); } } // Check if we can't merge it with another. for (let i = 0; i < this.armies.length - 1; ++i) { let army = this.armies[i]; if (army.getType() != "default") continue; for (let j = i+1; j < this.armies.length; ++j) { let otherArmy = this.armies[j]; if (otherArmy.getType() != "default" || API3.SquareVectorDistance(army.foePosition, otherArmy.foePosition) > this.armyMergeSize) continue; // No need to clear here. army.merge(gameState, otherArmy); this.armies.splice(j--, 1); } } if (gameState.ai.playedTurn % 5 != 0) return; // Check if any army is no more dangerous (possibly because it has defeated us and destroyed our base). this.attackingArmies = {}; for (let i = 0; i < this.armies.length; ++i) { let army = this.armies[i]; army.recalculatePosition(gameState); let owner = this.territoryMap.getOwner(army.foePosition); if (!gameState.isPlayerEnemy(owner)) { if (gameState.isPlayerMutualAlly(owner)) { // Update the number of enemies attacking this ally. for (let id of army.foeEntities) { let ent = gameState.getEntityById(id); if (!ent) continue; let enemy = ent.owner(); if (this.attackingArmies[enemy] === undefined) this.attackingArmies[enemy] = {}; if (this.attackingArmies[enemy][owner] === undefined) this.attackingArmies[enemy][owner] = 0; this.attackingArmies[enemy][owner] += 1; break; } } continue; } // Enemy army back in its territory. else if (owner != 0) { army.clear(gameState); this.armies.splice(i--, 1); continue; } // Army in neutral territory. // TODO check smaller distance with all our buildings instead of only ccs with big distance. let stillDangerous = false; let bases = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); for (let base of bases.values()) { if (!gameState.isEntityAlly(base)) continue; let cooperation = this.GetCooperationLevel(base.owner()); if (cooperation < 0.3 && !gameState.isEntityOwn(base)) continue; if (API3.SquareVectorDistance(base.position(), army.foePosition) > 40000) continue; if(this.Config.debug > 1) API3.warn("army in neutral territory, but still near one of our CC"); stillDangerous = true; break; } if (stillDangerous) continue; // Need to also check docks because of oversea bases. for (let dock of gameState.getOwnStructures().filter(API3.Filters.byClass("Dock")).values()) { if (API3.SquareVectorDistance(dock.position(), army.foePosition) > 10000) continue; stillDangerous = true; break; } if (stillDangerous) continue; if (army.getType() == "default") this.switchToAttack(gameState, army); army.clear(gameState); this.armies.splice(i--, 1); } }; PETRA.DefenseManager.prototype.assignDefenders = function(gameState) { if (!this.armies.length) return; let armiesNeeding = []; // Let's add defenders. for (let army of this.armies) { let needsDef = army.needsDefenders(gameState); if (needsDef === false) continue; let armyAccess; for (let entId of army.foeEntities) { let ent = gameState.getEntityById(entId); if (!ent || !ent.position()) continue; armyAccess = PETRA.getLandAccess(gameState, ent); break; } if (!armyAccess) API3.warn(" Petra error: attacking army " + army.ID + " without access"); army.recalculatePosition(gameState); armiesNeeding.push({ "army": army, "access": armyAccess, "need": needsDef }); } if (!armiesNeeding.length) return; // Let's get our potential units. let potentialDefenders = []; gameState.getOwnUnits().forEach(function(ent) { if (!ent.position()) return; if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3) return; if (ent.hasClass("Support") || ent.attackTypes() === undefined) return; if (ent.hasClasses(["StoneThrower", "Support", "FishingBoat"])) return; if (ent.getMetadata(PlayerID, "transport") !== undefined || ent.getMetadata(PlayerID, "transporter") !== undefined) return; if (gameState.ai.HQ.victoryManager.criticalEnts.has(ent.id())) return; if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") != -1) { let subrole = ent.getMetadata(PlayerID, "subrole"); if (subrole && (subrole == "completing" || subrole == "walking" || subrole == "attacking")) return; } potentialDefenders.push(ent.id()); }); for (let ipass = 0; ipass < 2; ++ipass) { // First pass only assign defenders with the right access. // Second pass assign all defenders. // TODO could sort them by distance. let backup = 0; for (let i = 0; i < potentialDefenders.length; ++i) { let ent = gameState.getEntityById(potentialDefenders[i]); if (!ent || !ent.position()) continue; let aMin; let distMin; let access = ipass == 0 ? PETRA.getLandAccess(gameState, ent) : undefined; for (let a = 0; a < armiesNeeding.length; ++a) { if (access && armiesNeeding[a].access != access) continue; // Do not assign defender if it cannot attack at least part of the attacking army. if (!armiesNeeding[a].army.foeEntities.some(eEnt => { let eEntID = gameState.getEntityById(eEnt); return ent.canAttackTarget(eEntID, PETRA.allowCapture(gameState, ent, eEntID)); })) continue; let dist = API3.SquareVectorDistance(ent.position(), armiesNeeding[a].army.foePosition); if (aMin !== undefined && dist > distMin) continue; aMin = a; distMin = dist; } // If outside our territory (helping an ally or attacking a cc foundation) // or if in another access, keep some troops in backup. if (backup < 12 && (aMin == undefined || distMin > 40000 && this.territoryMap.getOwner(armiesNeeding[aMin].army.foePosition) != PlayerID)) { ++backup; potentialDefenders[i] = undefined; continue; } else if (aMin === undefined) continue; armiesNeeding[aMin].need -= PETRA.getMaxStrength(ent, this.Config.debug, this.Config.DamageTypeImportance); armiesNeeding[aMin].army.addOwn(gameState, potentialDefenders[i]); armiesNeeding[aMin].army.assignUnit(gameState, potentialDefenders[i]); potentialDefenders[i] = undefined; if (armiesNeeding[aMin].need <= 0) armiesNeeding.splice(aMin, 1); if (!armiesNeeding.length) return; } } // If shortage of defenders, produce infantry garrisoned in nearest civil center. let armiesPos = []; for (let a = 0; a < armiesNeeding.length; ++a) armiesPos.push(armiesNeeding[a].army.foePosition); gameState.ai.HQ.trainEmergencyUnits(gameState, armiesPos); }; PETRA.DefenseManager.prototype.abortArmy = function(gameState, army) { army.clear(gameState); for (let i = 0; i < this.armies.length; ++i) { if (this.armies[i].ID != army.ID) continue; this.armies.splice(i, 1); break; } }; /** * If our defense structures are attacked, garrison soldiers inside when possible * and if a support unit is attacked and has less than 55% health, garrison it inside the nearest healing structure * and if a ranged siege unit (not used for defense) is attacked, garrison it in the nearest fortress. * If our hero is attacked with regicide victory condition, the victoryManager will handle it. */ PETRA.DefenseManager.prototype.checkEvents = function(gameState, events) { // Must be called every turn for all armies. for (let army of this.armies) army.checkEvents(gameState, events); // Capture events. for (let evt of events.OwnershipChanged) { if (gameState.isPlayerMutualAlly(evt.from) && evt.to > 0) { let ent = gameState.getEntityById(evt.entity); // One of our cc has been captured. if (ent && ent.hasClass("CivCentre")) gameState.ai.HQ.attackManager.switchDefenseToAttack(gameState, ent, { "range": 150 }); } } let allAttacked = {}; for (let evt of events.Attacked) allAttacked[evt.target] = evt.attacker; for (let evt of events.Attacked) { let target = gameState.getEntityById(evt.target); if (!target || !target.position()) continue; let attacker = gameState.getEntityById(evt.attacker); if (attacker && gameState.isEntityOwn(attacker) && gameState.isEntityEnemy(target) && !attacker.hasClass("Ship") && (!target.hasClass("Structure") || target.attackRange("Ranged"))) { // If enemies are in range of one of our defensive structures, garrison it for arrow multiplier // (enemy non-defensive structure are not considered to stay in sync with garrisonManager). if (attacker.position() && attacker.isGarrisonHolder() && attacker.getArrowMultiplier() && (target.owner() != 0 || !target.hasClass("Unit") || target.unitAIState() && target.unitAIState().split(".")[1] == "COMBAT")) this.garrisonUnitsInside(gameState, attacker, { "attacker": target }); } if (!gameState.isEntityOwn(target)) continue; // If attacked by one of our allies (he must trying to recover capture points), do not react. if (attacker && gameState.isEntityAlly(attacker)) continue; if (attacker && attacker.position() && target.hasClass("FishingBoat")) { let unitAIState = target.unitAIState(); let unitAIStateOrder = unitAIState ? unitAIState.split(".")[1] : ""; if (target.isIdle() || unitAIStateOrder == "GATHER") { let pos = attacker.position(); let range = attacker.attackRange("Ranged") ? attacker.attackRange("Ranged").max + 15 : 25; if (range * range > API3.SquareVectorDistance(pos, target.position())) target.moveToRange(pos[0], pos[1], range, range + 5); } continue; } // TODO integrate other ships later, need to be sure it is accessible. if (target.hasClass("Ship")) continue; // If a building on a blinking tile is attacked, check if it can be defended. // Same thing for a building in an isolated base (not connected to a base with anchor). if (target.hasClass("Structure")) { let base = gameState.ai.HQ.getBaseByID(target.getMetadata(PlayerID, "base")); if (this.territoryMap.isBlinking(target.position()) && !gameState.ai.HQ.isDefendable(target) || - !base || gameState.ai.HQ.baseManagers.every(b => !b.anchor || b.accessIndex != base.accessIndex)) + !base || gameState.ai.HQ.baseManagers().every(b => !b.anchor || b.accessIndex != base.accessIndex)) { let capture = target.capturePoints(); if (!capture) continue; let captureRatio = capture[PlayerID] / capture.reduce((a, b) => a + b); if (captureRatio > 0.50 && captureRatio < 0.70) target.destroy(); continue; } } // If inside a started attack plan, let the plan deal with this unit. let plan = target.getMetadata(PlayerID, "plan"); if (plan !== undefined && plan >= 0) { let attack = gameState.ai.HQ.attackManager.getPlan(plan); if (attack && attack.state != "unexecuted") continue; } // Signal this attacker to our defense manager, except if we are in enemy territory. // TODO treat ship attack. if (attacker && attacker.position() && attacker.getMetadata(PlayerID, "PartOfArmy") === undefined && !attacker.hasClasses(["Structure", "Ship"])) { let territoryOwner = this.territoryMap.getOwner(attacker.position()); if (territoryOwner == 0 || gameState.isPlayerAlly(territoryOwner)) this.makeIntoArmy(gameState, attacker.id()); } if (target.getMetadata(PlayerID, "PartOfArmy") !== undefined) { let army = this.getArmy(target.getMetadata(PlayerID, "PartOfArmy")); if (army.getType() == "capturing") { let abort = false; // If one of the units trying to capture a structure is attacked, // abort the army so that the unit can defend itself if (army.ownEntities.indexOf(target.id()) != -1) abort = true; else if (army.foeEntities[0] == target.id() && target.owner() == PlayerID) { // else we may be trying to regain some capture point from one of our structure. abort = true; let capture = target.capturePoints(); for (let j = 0; j < capture.length; ++j) { if (!gameState.isPlayerEnemy(j) || capture[j] == 0) continue; abort = false; break; } } if (abort) this.abortArmy(gameState, army); } continue; } // Try to garrison any attacked support unit if low health. if (target.hasClass("Support") && target.healthLevel() < this.Config.garrisonHealthLevel.medium && !target.getMetadata(PlayerID, "transport") && plan != -2 && plan != -3) { this.garrisonAttackedUnit(gameState, target); continue; } // Try to garrison any attacked stone thrower. if (target.hasClass("StoneThrower") && !target.getMetadata(PlayerID, "transport") && plan != -2 && plan != -3) { this.garrisonSiegeUnit(gameState, target); continue; } if (!attacker || !attacker.position()) continue; if (target.isGarrisonHolder() && target.getArrowMultiplier()) this.garrisonUnitsInside(gameState, target, { "attacker": attacker }); if (target.hasClass("Unit") && attacker.hasClass("Unit")) { // Consider whether we should retaliate or continue our task. if (target.hasClass("Support") || target.attackTypes() === undefined) continue; let orderData = target.unitAIOrderData(); let currentTarget = orderData && orderData.length && orderData[0].target ? gameState.getEntityById(orderData[0].target) : undefined; if (currentTarget) { let unitAIState = target.unitAIState(); let unitAIStateOrder = unitAIState ? unitAIState.split(".")[1] : ""; if (unitAIStateOrder == "COMBAT" && (currentTarget == attacker.id() || !currentTarget.hasClasses(["Structure", "Support"]))) continue; if (unitAIStateOrder == "REPAIR" && currentTarget.hasDefensiveFire()) continue; if (unitAIStateOrder == "COMBAT" && !PETRA.isSiegeUnit(currentTarget) && gameState.ai.HQ.capturableTargets.has(orderData[0].target)) { // Take the nearest unit also attacking this structure to help us. let capturableTarget = gameState.ai.HQ.capturableTargets.get(orderData[0].target); let minDist; let minEnt; let pos = attacker.position(); capturableTarget.ents.delete(target.id()); for (let entId of capturableTarget.ents) { if (allAttacked[entId]) continue; let ent = gameState.getEntityById(entId); if (!ent || !ent.position() || !ent.canAttackTarget(attacker, PETRA.allowCapture(gameState, ent, attacker))) continue; // Check that the unit is still attacking the structure (since the last played turn). let state = ent.unitAIState(); if (!state || !state.split(".")[1] || state.split(".")[1] != "COMBAT") continue; let entOrderData = ent.unitAIOrderData(); if (!entOrderData || !entOrderData.length || !entOrderData[0].target || entOrderData[0].target != orderData[0].target) continue; let dist = API3.SquareVectorDistance(pos, ent.position()); if (minEnt && dist > minDist) continue; minDist = dist; minEnt = ent; } if (minEnt) { capturableTarget.ents.delete(minEnt.id()); minEnt.attack(attacker.id(), PETRA.allowCapture(gameState, minEnt, attacker)); } } } let allowCapture = PETRA.allowCapture(gameState, target, attacker); if (target.canAttackTarget(attacker, allowCapture)) target.attack(attacker.id(), allowCapture); } } }; PETRA.DefenseManager.prototype.garrisonUnitsInside = function(gameState, target, data) { if (target.hitpoints() < target.garrisonEjectHealth() * target.maxHitpoints()) return false; let minGarrison = data.min || target.garrisonMax(); if (gameState.ai.HQ.garrisonManager.numberOfGarrisonedSlots(target) >= minGarrison) return false; if (data.attacker) { let attackTypes = target.attackTypes(); if (!attackTypes || attackTypes.indexOf("Ranged") == -1) return false; let dist = API3.SquareVectorDistance(data.attacker.position(), target.position()); let range = target.attackRange("Ranged").max; if (dist >= range*range) return false; } let access = PETRA.getLandAccess(gameState, target); let garrisonManager = gameState.ai.HQ.garrisonManager; let garrisonArrowClasses = target.getGarrisonArrowClasses(); let typeGarrison = data.type || "protection"; let allowMelee = gameState.ai.HQ.garrisonManager.allowMelee(target); if (allowMelee === undefined) { // Should be kept in sync with garrisonManager to avoid garrisoning-ungarrisoning some units. if (data.attacker) allowMelee = data.attacker.hasClass("Structure") ? data.attacker.attackRange("Ranged") : !PETRA.isSiegeUnit(data.attacker); else allowMelee = true; } let units = gameState.getOwnUnits().filter(ent => { if (!ent.position()) return false; if (!ent.hasClasses(garrisonArrowClasses)) return false; if (typeGarrison != "decay" && !allowMelee && ent.attackTypes().indexOf("Melee") != -1) return false; if (ent.getMetadata(PlayerID, "transport") !== undefined) return false; let army = ent.getMetadata(PlayerID, "PartOfArmy") ? this.getArmy(ent.getMetadata(PlayerID, "PartOfArmy")) : undefined; if (!army && (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3)) return false; if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") >= 0) { let subrole = ent.getMetadata(PlayerID, "subrole"); // When structure decaying (usually because we've just captured it in enemy territory), also allow units from an attack plan. if (typeGarrison != "decay" && subrole && (subrole == "completing" || subrole == "walking" || subrole == "attacking")) return false; } if (PETRA.getLandAccess(gameState, ent) != access) return false; return true; }).filterNearest(target.position()); let ret = false; for (let ent of units.values()) { if (garrisonManager.numberOfGarrisonedSlots(target) >= minGarrison) break; if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") >= 0) { let attackPlan = gameState.ai.HQ.attackManager.getPlan(ent.getMetadata(PlayerID, "plan")); if (attackPlan) attackPlan.removeUnit(ent, true); } let army = ent.getMetadata(PlayerID, "PartOfArmy") ? this.getArmy(ent.getMetadata(PlayerID, "PartOfArmy")) : undefined; if (army) army.removeOwn(gameState, ent.id()); garrisonManager.garrison(gameState, ent, target, typeGarrison); ret = true; } return ret; }; /** Garrison a attacked siege ranged unit inside the nearest fortress. */ PETRA.DefenseManager.prototype.garrisonSiegeUnit = function(gameState, unit) { let distmin = Math.min(); let nearest; let unitAccess = PETRA.getLandAccess(gameState, unit); let garrisonManager = gameState.ai.HQ.garrisonManager; for (let ent of gameState.getAllyStructures().values()) { if (!ent.isGarrisonHolder()) continue; if (!unit.hasClasses(ent.garrisonableClasses())) continue; if (garrisonManager.numberOfGarrisonedSlots(ent) >= ent.garrisonMax()) continue; if (ent.hitpoints() < ent.garrisonEjectHealth() * ent.maxHitpoints()) continue; if (PETRA.getLandAccess(gameState, ent) != unitAccess) continue; let dist = API3.SquareVectorDistance(ent.position(), unit.position()); if (dist > distmin) continue; distmin = dist; nearest = ent; } if (nearest) garrisonManager.garrison(gameState, unit, nearest, "protection"); return nearest !== undefined; }; /** * Garrison a hurt unit inside a player-owned or allied structure. * If emergency is true, the unit will be garrisoned in the closest possible structure. * Otherwise, it will garrison in the closest healing structure. */ PETRA.DefenseManager.prototype.garrisonAttackedUnit = function(gameState, unit, emergency = false) { let distmin = Math.min(); let nearest; let unitAccess = PETRA.getLandAccess(gameState, unit); let garrisonManager = gameState.ai.HQ.garrisonManager; for (let ent of gameState.getAllyStructures().values()) { if (!ent.isGarrisonHolder()) continue; if (!emergency && !ent.buffHeal()) continue; if (!unit.hasClasses(ent.garrisonableClasses())) continue; if (garrisonManager.numberOfGarrisonedSlots(ent) >= ent.garrisonMax() && (!emergency || !ent.garrisoned().length)) continue; if (ent.hitpoints() < ent.garrisonEjectHealth() * ent.maxHitpoints()) continue; if (PETRA.getLandAccess(gameState, ent) != unitAccess) continue; let dist = API3.SquareVectorDistance(ent.position(), unit.position()); if (dist > distmin) continue; distmin = dist; nearest = ent; } if (!nearest) return false; if (!emergency) { garrisonManager.garrison(gameState, unit, nearest, "protection"); return true; } if (garrisonManager.numberOfGarrisonedSlots(nearest) >= nearest.garrisonMax()) // make room for this ent nearest.unload(nearest.garrisoned()[0]); garrisonManager.garrison(gameState, unit, nearest, nearest.buffHeal() ? "protection" : "emergency"); return true; }; /** * Be more inclined to help an ally attacked by several enemies. */ PETRA.DefenseManager.prototype.GetCooperationLevel = function(ally) { let cooperation = this.Config.personality.cooperative; if (this.attackedAllies[ally] && this.attackedAllies[ally] > 1) cooperation += 0.2 * (this.attackedAllies[ally] - 1); return cooperation; }; /** * Switch a defense army into an attack if needed. */ PETRA.DefenseManager.prototype.switchToAttack = function(gameState, army) { if (!army) return; for (let targetId of this.targetList) { let target = gameState.getEntityById(targetId); if (!target || !target.position() || !gameState.isPlayerEnemy(target.owner())) continue; let targetAccess = PETRA.getLandAccess(gameState, target); let targetPos = target.position(); for (let entId of army.ownEntities) { let ent = gameState.getEntityById(entId); if (!ent || !ent.position() || PETRA.getLandAccess(gameState, ent) != targetAccess) continue; if (API3.SquareVectorDistance(targetPos, ent.position()) > 14400) continue; gameState.ai.HQ.attackManager.switchDefenseToAttack(gameState, target, { "armyID": army.ID, "uniqueTarget": true }); return; } } }; PETRA.DefenseManager.prototype.Serialize = function() { let properties = { "targetList": this.targetList, "armyMergeSize": this.armyMergeSize, "attackingUnits": this.attackingUnits, "attackingArmies": this.attackingArmies, "attackedAllies": this.attackedAllies }; let armies = []; for (let army of this.armies) armies.push(army.Serialize()); return { "properties": properties, "armies": armies }; }; PETRA.DefenseManager.prototype.Deserialize = function(gameState, data) { for (let key in data.properties) this[key] = data.properties[key]; this.armies = []; for (let dataArmy of data.armies) { let army = new PETRA.DefenseArmy(gameState, []); army.Deserialize(dataArmy); this.armies.push(army); } }; Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/entityExtend.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/entityExtend.js (revision 25875) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/entityExtend.js (revision 25876) @@ -1,435 +1,435 @@ /** returns true if this unit should be considered as a siege unit */ PETRA.isSiegeUnit = function(ent) { return ent.hasClasses(["Siege", "Elephant+Melee"]); }; /** returns true if this unit should be considered as "fast". */ PETRA.isFastMoving = function(ent) { // TODO: use clever logic based on walkspeed comparisons. return ent.hasClass("FastMoving"); }; /** returns some sort of DPS * health factor. If you specify a class, it'll use the modifiers against that class too. */ PETRA.getMaxStrength = function(ent, debugLevel, DamageTypeImportance, againstClass) { let strength = 0; let attackTypes = ent.attackTypes(); let damageTypes = Object.keys(DamageTypeImportance); if (!attackTypes) return strength; for (let type of attackTypes) { if (type == "Slaughter") continue; let attackStrength = ent.attackStrengths(type); for (let str in attackStrength) { let val = parseFloat(attackStrength[str]); if (againstClass) val *= ent.getMultiplierAgainst(type, againstClass); if (DamageTypeImportance[str]) strength += DamageTypeImportance[str] * val / damageTypes.length; else if (debugLevel > 0) API3.warn("Petra: " + str + " unknown attackStrength in getMaxStrength (please add " + str + " to config.js)."); } let attackRange = ent.attackRange(type); if (attackRange) strength += attackRange.max * 0.0125; let attackTimes = ent.attackTimes(type); for (let str in attackTimes) { let val = parseFloat(attackTimes[str]); switch (str) { case "repeat": strength += val / 100000; break; case "prepare": strength -= val / 100000; break; default: API3.warn("Petra: " + str + " unknown attackTimes in getMaxStrength"); } } } let resistanceStrength = ent.resistanceStrengths(); if (resistanceStrength.Damage) for (let str in resistanceStrength.Damage) { let val = +resistanceStrength.Damage[str]; if (DamageTypeImportance[str]) strength += DamageTypeImportance[str] * val / damageTypes.length; else if (debugLevel > 0) API3.warn("Petra: " + str + " unknown resistanceStrength in getMaxStrength (please add " + str + " to config.js)."); } // ToDo: Add support for StatusEffects and Capture. return strength * ent.maxHitpoints() / 100.0; }; /** Get access and cache it (except for units as it can change) in metadata if not already done */ PETRA.getLandAccess = function(gameState, ent) { if (ent.hasClass("Unit")) { let pos = ent.position(); if (!pos) { let holder = PETRA.getHolder(gameState, ent); if (holder) return PETRA.getLandAccess(gameState, holder); API3.warn("Petra error: entity without position, but not garrisoned"); PETRA.dumpEntity(ent); return undefined; } return gameState.ai.accessibility.getAccessValue(pos); } let access = ent.getMetadata(PlayerID, "access"); if (!access) { access = gameState.ai.accessibility.getAccessValue(ent.position()); // Docks are sometimes not as expected if (access < 2 && ent.buildPlacementType() == "shore") { let halfDepth = 0; if (ent.get("Footprint/Square")) halfDepth = +ent.get("Footprint/Square/@depth") / 2; else if (ent.get("Footprint/Circle")) halfDepth = +ent.get("Footprint/Circle/@radius"); let entPos = ent.position(); let cosa = Math.cos(ent.angle()); let sina = Math.sin(ent.angle()); for (let d = 3; d < halfDepth; d += 3) { let pos = [ entPos[0] - d * sina, entPos[1] - d * cosa]; access = gameState.ai.accessibility.getAccessValue(pos); if (access > 1) break; } } ent.setMetadata(PlayerID, "access", access); } return access; }; /** Sea access always cached as it never changes */ PETRA.getSeaAccess = function(gameState, ent) { let sea = ent.getMetadata(PlayerID, "sea"); if (!sea) { sea = gameState.ai.accessibility.getAccessValue(ent.position(), true); // Docks are sometimes not as expected if (sea < 2 && ent.buildPlacementType() == "shore") { let entPos = ent.position(); let cosa = Math.cos(ent.angle()); let sina = Math.sin(ent.angle()); for (let d = 3; d < 15; d += 3) { let pos = [ entPos[0] + d * sina, entPos[1] + d * cosa]; sea = gameState.ai.accessibility.getAccessValue(pos, true); if (sea > 1) break; } } ent.setMetadata(PlayerID, "sea", sea); } return sea; }; PETRA.setSeaAccess = function(gameState, ent) { PETRA.getSeaAccess(gameState, ent); }; /** Decide if we should try to capture (returns true) or destroy (return false) */ PETRA.allowCapture = function(gameState, ent, target) { if (!target.isCapturable() || !ent.canCapture(target)) return false; if (target.isInvulnerable()) return true; // always try to recapture capture points from an allied, except if it's decaying if (gameState.isPlayerAlly(target.owner())) return !target.decaying(); let antiCapture = target.defaultRegenRate(); if (target.isGarrisonHolder() && target.garrisoned()) antiCapture += target.garrisonRegenRate() * target.garrisoned().length; if (target.decaying()) antiCapture -= target.territoryDecayRate(); let capture; let capturableTargets = gameState.ai.HQ.capturableTargets; if (!capturableTargets.has(target.id())) { capture = ent.captureStrength() * PETRA.getAttackBonus(ent, target, "Capture"); capturableTargets.set(target.id(), { "strength": capture, "ents": new Set([ent.id()]) }); } else { let capturable = capturableTargets.get(target.id()); if (!capturable.ents.has(ent.id())) { capturable.strength += ent.captureStrength() * PETRA.getAttackBonus(ent, target, "Capture"); capturable.ents.add(ent.id()); } capture = capturable.strength; } capture *= 1 / (0.1 + 0.9*target.healthLevel()); let sumCapturePoints = target.capturePoints().reduce((a, b) => a + b); if (target.hasDefensiveFire() && target.isGarrisonHolder() && target.garrisoned()) return capture > antiCapture + sumCapturePoints/50; return capture > antiCapture + sumCapturePoints/80; }; PETRA.getAttackBonus = function(ent, target, type) { let attackBonus = 1; if (!ent.get("Attack/" + type) || !ent.get("Attack/" + type + "/Bonuses")) return attackBonus; let bonuses = ent.get("Attack/" + type + "/Bonuses"); for (let key in bonuses) { let bonus = bonuses[key]; if (bonus.Civ && bonus.Civ !== target.civ()) continue; if (!bonus.Classes || target.hasClasses(bonus.Classes)) attackBonus *= bonus.Multiplier; } return attackBonus; }; /** Makes the worker deposit the currently carried resources at the closest accessible dropsite */ PETRA.returnResources = function(gameState, ent) { if (!ent.resourceCarrying() || !ent.resourceCarrying().length || !ent.position()) return false; let resource = ent.resourceCarrying()[0].type; let closestDropsite; let distmin = Math.min(); let access = PETRA.getLandAccess(gameState, ent); let dropsiteCollection = gameState.playerData.hasSharedDropsites ? gameState.getAnyDropsites(resource) : gameState.getOwnDropsites(resource); for (let dropsite of dropsiteCollection.values()) { if (!dropsite.position()) continue; let owner = dropsite.owner(); // owner !== PlayerID can only happen when hasSharedDropsites === true, so no need to test it again if (owner !== PlayerID && (!dropsite.isSharedDropsite() || !gameState.isPlayerMutualAlly(owner))) continue; if (PETRA.getLandAccess(gameState, dropsite) != access) continue; let dist = API3.SquareVectorDistance(ent.position(), dropsite.position()); if (dist > distmin) continue; distmin = dist; closestDropsite = dropsite; } if (!closestDropsite) return false; ent.returnResources(closestDropsite); return true; }; /** is supply full taking into account gatherers affected during this turn */ PETRA.IsSupplyFull = function(gameState, ent) { return ent.isFull() === true || - ent.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(ent.id()) >= ent.maxGatherers(); + ent.resourceSupplyNumGatherers() + gameState.ai.HQ.basesManager.GetTCGatherer(ent.id()) >= ent.maxGatherers(); }; /** * Get the best base (in terms of distance and accessIndex) for an entity. * It should be on the same accessIndex for structures. - * If nothing found, return the base[0] for units and undefined for structures. + * If nothing found, return the noBase for units and undefined for structures. * If exclude is given, we exclude the base with ID = exclude. */ PETRA.getBestBase = function(gameState, ent, onlyConstructedBase = false, exclude = false) { let pos = ent.position(); let accessIndex; if (!pos) { let holder = PETRA.getHolder(gameState, ent); if (!holder || !holder.position()) { API3.warn("Petra error: entity without position, but not garrisoned"); PETRA.dumpEntity(ent); - return gameState.ai.HQ.baseManagers[0]; + return gameState.ai.HQ.basesManager.baselessBase(); } pos = holder.position(); accessIndex = PETRA.getLandAccess(gameState, holder); } else accessIndex = PETRA.getLandAccess(gameState, ent); let distmin = Math.min(); let dist; let bestbase; - for (let base of gameState.ai.HQ.baseManagers) + for (const base of gameState.ai.HQ.baseManagers()) { - if (base.ID == gameState.ai.HQ.baseManagers[0].ID || exclude && base.ID == exclude) + if (base.ID == gameState.ai.HQ.basesManager.baselessBase().ID || exclude && base.ID == exclude) continue; if (onlyConstructedBase && (!base.anchor || base.anchor.foundationProgress() !== undefined)) continue; if (ent.hasClass("Structure") && base.accessIndex != accessIndex) continue; if (base.anchor && base.anchor.position()) dist = API3.SquareVectorDistance(base.anchor.position(), pos); else { let found = false; for (let structure of base.buildings.values()) { if (!structure.position()) continue; dist = API3.SquareVectorDistance(structure.position(), pos); found = true; break; } if (!found) continue; } if (base.accessIndex != accessIndex) dist += 50000000; if (!base.anchor) dist += 50000000; if (dist > distmin) continue; distmin = dist; bestbase = base; } if (!bestbase && !ent.hasClass("Structure")) - bestbase = gameState.ai.HQ.baseManagers[0]; + bestbase = gameState.ai.HQ.basesManager.baselessBase(); return bestbase; }; PETRA.getHolder = function(gameState, ent) { for (let holder of gameState.getEntities().values()) { if (holder.isGarrisonHolder() && holder.garrisoned().indexOf(ent.id()) !== -1) return holder; } return undefined; }; /** return the template of the built foundation if a foundation, otherwise return the entity itself */ PETRA.getBuiltEntity = function(gameState, ent) { if (ent.foundationProgress() !== undefined) return gameState.getBuiltTemplate(ent.templateName()); return ent; }; /** * return true if it is not worth finishing this building (it would surely decay) * TODO implement the other conditions */ PETRA.isNotWorthBuilding = function(gameState, ent) { if (gameState.ai.HQ.territoryMap.getOwner(ent.position()) !== PlayerID) { let buildTerritories = ent.buildTerritories(); if (buildTerritories && (!buildTerritories.length || buildTerritories.length === 1 && buildTerritories[0] === "own")) return true; } return false; }; /** * Check if the straight line between the two positions crosses an enemy territory */ PETRA.isLineInsideEnemyTerritory = function(gameState, pos1, pos2, step=70) { let n = Math.floor(Math.sqrt(API3.SquareVectorDistance(pos1, pos2))/step) + 1; let stepx = (pos2[0] - pos1[0]) / n; let stepy = (pos2[1] - pos1[1]) / n; for (let i = 1; i < n; ++i) { let pos = [pos1[0]+i*stepx, pos1[1]+i*stepy]; let owner = gameState.ai.HQ.territoryMap.getOwner(pos); if (owner && gameState.isPlayerEnemy(owner)) return true; } return false; }; PETRA.gatherTreasure = function(gameState, ent, water = false) { if (!gameState.ai.HQ.treasures.hasEntities()) return false; if (!ent || !ent.position()) return false; if (!ent.isTreasureCollector()) return false; let treasureFound; let distmin = Math.min(); let access = water ? PETRA.getSeaAccess(gameState, ent) : PETRA.getLandAccess(gameState, ent); for (let treasure of gameState.ai.HQ.treasures.values()) { // let some time for the previous gatherer to reach the treasure before trying again let lastGathered = treasure.getMetadata(PlayerID, "lastGathered"); if (lastGathered && gameState.ai.elapsedTime - lastGathered < 20) continue; if (!water && access != PETRA.getLandAccess(gameState, treasure)) continue; if (water && access != PETRA.getSeaAccess(gameState, treasure)) continue; let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(treasure.position()); if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) continue; let dist = API3.SquareVectorDistance(ent.position(), treasure.position()); if (dist > 120000 || territoryOwner != PlayerID && dist > 14000) // AI has no LOS, so restrict it a bit continue; if (dist > distmin) continue; distmin = dist; treasureFound = treasure; } if (!treasureFound) return false; treasureFound.setMetadata(PlayerID, "lastGathered", gameState.ai.elapsedTime); ent.collectTreasure(treasureFound); ent.setMetadata(PlayerID, "treasure", treasureFound.id()); return true; }; PETRA.dumpEntity = function(ent) { if (!ent) return; API3.warn(" >>> id " + ent.id() + " name " + ent.genericName() + " pos " + ent.position() + " state " + ent.unitAIState()); API3.warn(" base " + ent.getMetadata(PlayerID, "base") + " >>> role " + ent.getMetadata(PlayerID, "role") + " subrole " + ent.getMetadata(PlayerID, "subrole")); API3.warn("owner " + ent.owner() + " health " + ent.hitpoints() + " healthMax " + ent.maxHitpoints() + " foundationProgress " + ent.foundationProgress()); API3.warn(" garrisoning " + ent.getMetadata(PlayerID, "garrisoning") + " garrisonHolder " + ent.getMetadata(PlayerID, "garrisonHolder") + " plan " + ent.getMetadata(PlayerID, "plan") + " transport " + ent.getMetadata(PlayerID, "transport")); API3.warn(" stance " + ent.getStance() + " transporter " + ent.getMetadata(PlayerID, "transporter") + " gather-type " + ent.getMetadata(PlayerID, "gather-type") + " target-foundation " + ent.getMetadata(PlayerID, "target-foundation") + " PartOfArmy " + ent.getMetadata(PlayerID, "PartOfArmy")); }; Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/headquarters.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/headquarters.js (revision 25875) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/headquarters.js (revision 25876) @@ -1,2892 +1,2398 @@ /** * Headquarters * Deal with high level logic for the AI. Most of the interesting stuff gets done here. * Some tasks: * -defining RESS needs * -BO decisions. * > training workers * > building stuff (though we'll send that to bases) * -picking strategy (specific manager?) * -diplomacy -> diplomacyManager * -planning attacks -> attackManager * -picking new CC locations. */ PETRA.HQ = function(Config) { this.Config = Config; this.phasing = 0; // existing values: 0 means no, i > 0 means phasing towards phase i // Cache various quantities. this.turnCache = {}; this.lastFailedGather = {}; this.firstBaseConfig = false; - this.currentBase = 0; // Only one base (from baseManager) is run every turn. // Workers configuration. this.targetNumWorkers = this.Config.Economy.targetNumWorkers; this.supportRatio = this.Config.Economy.supportRatio; this.fortStartTime = 180; // Sentry towers, will start at fortStartTime + towerLapseTime. this.towerStartTime = 0; // Stone towers, will start as soon as available (town phase). this.towerLapseTime = this.Config.Military.towerLapseTime; this.fortressStartTime = 0; // Fortresses, will start as soon as available (city phase). this.fortressLapseTime = this.Config.Military.fortressLapseTime; this.extraTowers = Math.round(Math.min(this.Config.difficulty, 3) * this.Config.personality.defensive); this.extraFortresses = Math.round(Math.max(Math.min(this.Config.difficulty - 1, 2), 0) * this.Config.personality.defensive); - this.baseManagers = []; + this.basesManager = new PETRA.BasesManager(this.Config); this.attackManager = new PETRA.AttackManager(this.Config); this.buildManager = new PETRA.BuildManager(); this.defenseManager = new PETRA.DefenseManager(this.Config); this.tradeManager = new PETRA.TradeManager(this.Config); this.navalManager = new PETRA.NavalManager(this.Config); this.researchManager = new PETRA.ResearchManager(this.Config); this.diplomacyManager = new PETRA.DiplomacyManager(this.Config); this.garrisonManager = new PETRA.GarrisonManager(this.Config); this.victoryManager = new PETRA.VictoryManager(this.Config); this.capturableTargets = new Map(); this.capturableTargetsTime = 0; }; /** More initialisation for stuff that needs the gameState */ PETRA.HQ.prototype.init = function(gameState, queues) { this.territoryMap = PETRA.createTerritoryMap(gameState); - // initialize base map. Each pixel is a base ID, or 0 if not or not accessible - this.basesMap = new API3.Map(gameState.sharedScript, "territory"); // create borderMap: flag cells on the border of the map // then this map will be completed with our frontier in updateTerritories this.borderMap = PETRA.createBorderMap(gameState); // list of allowed regions this.landRegions = {}; // try to determine if we have a water map this.navalMap = false; this.navalRegions = {}; this.treasures = gameState.getEntities().filter(ent => ent.isTreasure()); this.treasures.registerUpdates(); this.currentPhase = gameState.currentPhase(); this.decayingStructures = new Set(); }; /** * initialization needed after deserialization (only called when deserialization) */ PETRA.HQ.prototype.postinit = function(gameState) { - // Rebuild the base maps from the territory indices of each base - this.basesMap = new API3.Map(gameState.sharedScript, "territory"); - for (let base of this.baseManagers) - for (let j of base.territoryIndices) - this.basesMap.map[j] = base.ID; - - for (let ent of gameState.getOwnEntities().values()) - { - if (!ent.resourceDropsiteTypes() || !ent.hasClass("Structure")) - continue; - // Entities which have been built or have changed ownership after the last AI turn have no base. - // they will be dealt with in the next checkEvents - let baseID = ent.getMetadata(PlayerID, "base"); - if (baseID === undefined) - continue; - let base = this.getBaseByID(baseID); - base.assignResourceToDropsite(gameState, ent); - } - + this.basesManager.postinit(gameState); this.updateTerritories(gameState); }; /** - * Create a new base in the baseManager: - * If an existing one without anchor already exist, use it. - * Otherwise create a new one. - * TODO when buildings, criteria should depend on distance - * allowedType: undefined => new base with an anchor - * "unconstructed" => new base with a foundation anchor - * "captured" => captured base with an anchor - * "anchorless" => anchorless base, currently with dock - */ -PETRA.HQ.prototype.createBase = function(gameState, ent, type) -{ - let access = PETRA.getLandAccess(gameState, ent); - let newbase; - for (let base of this.baseManagers) - { - if (base.accessIndex != access) - continue; - if (type != "anchorless" && base.anchor) - continue; - if (type != "anchorless") - { - // TODO we keep the fisrt one, we should rather use the nearest if buildings - // and possibly also cut on distance - newbase = base; - break; - } - else - { - // TODO here also test on distance instead of first - if (newbase && !base.anchor) - continue; - newbase = base; - if (newbase.anchor) - break; - } - } - - if (this.Config.debug > 0) - { - API3.warn(" ----------------------------------------------------------"); - API3.warn(" HQ createBase entrance avec access " + access + " and type " + type); - API3.warn(" with access " + uneval(this.baseManagers.map(base => base.accessIndex)) + - " and base nbr " + uneval(this.baseManagers.map(base => base.ID)) + - " and anchor " + uneval(this.baseManagers.map(base => !!base.anchor))); - } - - if (!newbase) - { - newbase = new PETRA.BaseManager(gameState, this.Config); - newbase.init(gameState, type); - this.baseManagers.push(newbase); - } - else - newbase.reset(type); - - if (type != "anchorless") - newbase.setAnchor(gameState, ent); - else - newbase.setAnchorlessEntity(gameState, ent); - - return newbase; -}; - -/** * returns the sea index linking regions 1 and region 2 (supposed to be different land region) * otherwise return undefined * for the moment, only the case land-sea-land is supported */ PETRA.HQ.prototype.getSeaBetweenIndices = function(gameState, index1, index2) { let path = gameState.ai.accessibility.getTrajectToIndex(index1, index2); if (path && path.length == 3 && gameState.ai.accessibility.regionType[path[1]] == "water") return path[1]; if (this.Config.debug > 1) { API3.warn("bad path from " + index1 + " to " + index2 + " ??? " + uneval(path)); API3.warn(" regionLinks start " + uneval(gameState.ai.accessibility.regionLinks[index1])); API3.warn(" regionLinks end " + uneval(gameState.ai.accessibility.regionLinks[index2])); } return undefined; }; -/** TODO check if the new anchorless bases should be added to addBase */ PETRA.HQ.prototype.checkEvents = function(gameState, events) { - let addBase = false; - this.buildManager.checkEvents(gameState, events); if (events.TerritoriesChanged.length || events.DiplomacyChanged.length) this.updateTerritories(gameState); for (let evt of events.DiplomacyChanged) { if (evt.player != PlayerID && evt.otherPlayer != PlayerID) continue; // Reset the entities collections which depend on diplomacy gameState.resetOnDiplomacyChanged(); break; } - for (let evt of events.Destroy) - { - // Let's check we haven't lost an important building here. - if (evt && !evt.SuccessfulFoundation && evt.entityObj && evt.metadata && evt.metadata[PlayerID] && - evt.metadata[PlayerID].base) - { - let ent = evt.entityObj; - if (ent.owner() != PlayerID) - continue; - // A new base foundation was created and destroyed on the same (AI) turn - if (evt.metadata[PlayerID].base == -1 || evt.metadata[PlayerID].base == -2) - continue; - let base = this.getBaseByID(evt.metadata[PlayerID].base); - if (ent.resourceDropsiteTypes() && ent.hasClass("Structure")) - base.removeDropsite(gameState, ent); - if (evt.metadata[PlayerID].baseAnchor && evt.metadata[PlayerID].baseAnchor === true) - base.anchorLost(gameState, ent); - } - } - - for (let evt of events.EntityRenamed) - { - let ent = gameState.getEntityById(evt.newentity); - if (!ent || ent.owner() != PlayerID || ent.getMetadata(PlayerID, "base") === undefined) - continue; - let base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); - if (!base.anchorId || base.anchorId != evt.entity) - continue; - base.anchorId = evt.newentity; - base.anchor = ent; - } - - for (let evt of events.Create) - { - // Let's check if we have a valuable foundation needing builders quickly - // (normal foundations are taken care in baseManager.assignToFoundations) - let ent = gameState.getEntityById(evt.entity); - if (!ent || ent.owner() != PlayerID || ent.foundationProgress() === undefined) - continue; - - if (ent.getMetadata(PlayerID, "base") == -1) // Standard base around a cc - { - // Okay so let's try to create a new base around this. - let newbase = this.createBase(gameState, ent, "unconstructed"); - // Let's get a few units from other bases there to build this. - let builders = this.bulkPickWorkers(gameState, newbase, 10); - if (builders !== false) - { - builders.forEach(worker => { - worker.setMetadata(PlayerID, "base", newbase.ID); - worker.setMetadata(PlayerID, "subrole", "builder"); - worker.setMetadata(PlayerID, "target-foundation", ent.id()); - }); - } - } - else if (ent.getMetadata(PlayerID, "base") == -2) // anchorless base around a dock - { - let newbase = this.createBase(gameState, ent, "anchorless"); - // Let's get a few units from other bases there to build this. - let builders = this.bulkPickWorkers(gameState, newbase, 4); - if (builders != false) - { - builders.forEach(worker => { - worker.setMetadata(PlayerID, "base", newbase.ID); - worker.setMetadata(PlayerID, "subrole", "builder"); - worker.setMetadata(PlayerID, "target-foundation", ent.id()); - }); - } - } - } + this.basesManager.checkEvents(gameState, events); for (let evt of events.ConstructionFinished) { if (evt.newentity == evt.entity) // repaired building continue; let ent = gameState.getEntityById(evt.newentity); if (!ent || ent.owner() != PlayerID) continue; if (ent.hasClass("Market") && this.maxFields) this.maxFields = false; - if (ent.getMetadata(PlayerID, "base") === undefined) - continue; - let base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); - base.buildings.updateEnt(ent); - if (ent.resourceDropsiteTypes()) - base.assignResourceToDropsite(gameState, ent); - - if (ent.getMetadata(PlayerID, "baseAnchor") === true) - { - if (base.constructing) - base.constructing = false; - addBase = true; - } } for (let evt of events.OwnershipChanged) // capture events { - if (evt.from == PlayerID) - { - let ent = gameState.getEntityById(evt.entity); - if (!ent || ent.getMetadata(PlayerID, "base") === undefined) - continue; - let base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); - if (ent.resourceDropsiteTypes() && ent.hasClass("Structure")) - base.removeDropsite(gameState, ent); - if (ent.getMetadata(PlayerID, "baseAnchor") === true) - base.anchorLost(gameState, ent); - } - if (evt.to != PlayerID) continue; let ent = gameState.getEntityById(evt.entity); if (!ent) continue; - if (ent.hasClass("Unit")) + if (!ent.hasClass("Unit")) { - PETRA.getBestBase(gameState, ent).assignEntity(gameState, ent); - ent.setMetadata(PlayerID, "role", undefined); - ent.setMetadata(PlayerID, "subrole", undefined); - ent.setMetadata(PlayerID, "plan", undefined); - ent.setMetadata(PlayerID, "PartOfArmy", undefined); - if (ent.hasClass("Trader")) - { - ent.setMetadata(PlayerID, "role", "trader"); - ent.setMetadata(PlayerID, "route", undefined); - } - if (ent.hasClass("Worker")) - { - ent.setMetadata(PlayerID, "role", "worker"); - ent.setMetadata(PlayerID, "subrole", "idle"); - } - if (ent.hasClass("Ship")) - PETRA.setSeaAccess(gameState, ent); - if (!ent.hasClasses(["Support", "Ship"]) && ent.attackTypes() !== undefined) - ent.setMetadata(PlayerID, "plan", -1); - continue; - } - if (ent.hasClass("CivCentre")) // build a new base around it - { - let newbase; - if (ent.foundationProgress() !== undefined) - newbase = this.createBase(gameState, ent, "unconstructed"); - else - { - newbase = this.createBase(gameState, ent, "captured"); - addBase = true; - } - newbase.assignEntity(gameState, ent); - } - else - { - let base; - // If dropsite on new island, create a base around it - if (!ent.decaying() && ent.resourceDropsiteTypes()) - base = this.createBase(gameState, ent, "anchorless"); - else - base = PETRA.getBestBase(gameState, ent) || this.baseManagers[0]; - base.assignEntity(gameState, ent); if (ent.decaying()) { if (ent.isGarrisonHolder() && this.garrisonManager.addDecayingStructure(gameState, evt.entity, true)) continue; if (!this.decayingStructures.has(evt.entity)) this.decayingStructures.add(evt.entity); } + continue; } + + ent.setMetadata(PlayerID, "role", undefined); + ent.setMetadata(PlayerID, "subrole", undefined); + ent.setMetadata(PlayerID, "plan", undefined); + ent.setMetadata(PlayerID, "PartOfArmy", undefined); + if (ent.hasClass("Trader")) + { + ent.setMetadata(PlayerID, "role", "trader"); + ent.setMetadata(PlayerID, "route", undefined); + } + if (ent.hasClass("Worker")) + { + ent.setMetadata(PlayerID, "role", "worker"); + ent.setMetadata(PlayerID, "subrole", "idle"); + } + if (ent.hasClass("Ship")) + PETRA.setSeaAccess(gameState, ent); + if (!ent.hasClasses(["Support", "Ship"]) && ent.attackTypes() !== undefined) + ent.setMetadata(PlayerID, "plan", -1); } // deal with the different rally points of training units: the rally point is set when the training starts // for the time being, only autogarrison is used for (let evt of events.TrainingStarted) { let ent = gameState.getEntityById(evt.entity); if (!ent || !ent.isOwn(PlayerID)) continue; if (!ent._entity.trainingQueue || !ent._entity.trainingQueue.length) continue; let metadata = ent._entity.trainingQueue[0].metadata; if (metadata && metadata.garrisonType) ent.setRallyPoint(ent, "garrison"); // trained units will autogarrison else ent.unsetRallyPoint(); } for (let evt of events.TrainingFinished) { for (let entId of evt.entities) { let ent = gameState.getEntityById(entId); if (!ent || !ent.isOwn(PlayerID)) continue; if (!ent.position()) { // we are autogarrisoned, check that the holder is registered in the garrisonManager let holder = gameState.getEntityById(ent.garrisonHolderID()); if (holder) this.garrisonManager.registerHolder(gameState, holder); } else if (ent.getMetadata(PlayerID, "garrisonType")) { // we were supposed to be autogarrisoned, but this has failed (may-be full) ent.setMetadata(PlayerID, "garrisonType", undefined); } // Check if this unit is no more needed in its attack plan // (happen when the training ends after the attack is started or aborted) let plan = ent.getMetadata(PlayerID, "plan"); if (plan !== undefined && plan >= 0) { let attack = this.attackManager.getPlan(plan); if (!attack || attack.state != "unexecuted") ent.setMetadata(PlayerID, "plan", -1); } - // Assign it immediately to something useful to do - if (ent.getMetadata(PlayerID, "role") == "worker") - { - let base; - if (ent.getMetadata(PlayerID, "base") === undefined) - { - base = PETRA.getBestBase(gameState, ent); - base.assignEntity(gameState, ent); - } - else - base = this.getBaseByID(ent.getMetadata(PlayerID, "base")); - base.reassignIdleWorkers(gameState, [ent]); - base.workerObject.update(gameState, ent); - } - else if (ent.resourceSupplyType() && ent.position()) - { - let type = ent.resourceSupplyType(); - if (!type.generic) - continue; - let dropsites = gameState.getOwnDropsites(type.generic); - let pos = ent.position(); - let access = PETRA.getLandAccess(gameState, ent); - let distmin = Math.min(); - let goal; - for (let dropsite of dropsites.values()) - { - if (!dropsite.position() || PETRA.getLandAccess(gameState, dropsite) != access) - continue; - let dist = API3.SquareVectorDistance(pos, dropsite.position()); - if (dist > distmin) - continue; - distmin = dist; - goal = dropsite.position(); - } - if (goal) - ent.moveToRange(goal[0], goal[1]); - } } } for (let evt of events.TerritoryDecayChanged) { let ent = gameState.getEntityById(evt.entity); if (!ent || !ent.isOwn(PlayerID) || ent.foundationProgress() !== undefined) continue; if (evt.to) { if (ent.isGarrisonHolder() && this.garrisonManager.addDecayingStructure(gameState, evt.entity)) continue; if (!this.decayingStructures.has(evt.entity)) this.decayingStructures.add(evt.entity); } else if (ent.isGarrisonHolder()) this.garrisonManager.removeDecayingStructure(evt.entity); } - if (addBase) - { - if (!this.firstBaseConfig) - { - // This is our first base, let us configure our starting resources - this.configFirstBase(gameState); - } - else - { - // Let us hope this new base will fix our possible resource shortage - this.saveResources = undefined; - this.saveSpace = undefined; - this.maxFields = false; - } - } - // Then deals with decaying structures: destroy them if being lost to enemy (except in easier difficulties) if (this.Config.difficulty < 2) return; for (let entId of this.decayingStructures) { let ent = gameState.getEntityById(entId); if (ent && ent.decaying() && ent.isOwn(PlayerID)) { let capture = ent.capturePoints(); if (!capture) continue; let captureRatio = capture[PlayerID] / capture.reduce((a, b) => a + b); if (captureRatio < 0.50) continue; let decayToGaia = true; for (let i = 1; i < capture.length; ++i) { if (gameState.isPlayerAlly(i) || !capture[i]) continue; decayToGaia = false; break; } if (decayToGaia) continue; let ratioMax = 0.7 + randFloat(0, 0.1); for (let evt of events.Attacked) { if (ent.id() != evt.target) continue; ratioMax = 0.85 + randFloat(0, 0.1); break; } if (captureRatio > ratioMax) continue; ent.destroy(); } this.decayingStructures.delete(entId); } }; +PETRA.HQ.prototype.handleNewBase = function(gameState) +{ + if (!this.firstBaseConfig) + // This is our first base, let us configure our starting resources. + this.configFirstBase(gameState); + else + { + // Let us hope this new base will fix our possible resource shortage. + this.saveResources = undefined; + this.saveSpace = undefined; + this.maxFields = false; + } +}; + /** Ensure that all requirements are met when phasing up*/ PETRA.HQ.prototype.checkPhaseRequirements = function(gameState, queues) { if (gameState.getNumberOfPhases() == this.currentPhase) return; let requirements = gameState.getPhaseEntityRequirements(this.currentPhase + 1); let plan; let queue; for (let entityReq of requirements) { // Village requirements are met elsewhere by constructing more houses if (entityReq.class == "Village" || entityReq.class == "NotField") continue; if (gameState.getOwnEntitiesByClass(entityReq.class, true).length >= entityReq.count) continue; switch (entityReq.class) { case "Town": if (!queues.economicBuilding.hasQueuedUnits() && !queues.militaryBuilding.hasQueuedUnits()) { if (!gameState.getOwnEntitiesByClass("Market", true).hasEntities() && this.canBuild(gameState, "structures/{civ}/market")) { plan = new PETRA.ConstructionPlan(gameState, "structures/{civ}/market", { "phaseUp": true }); queue = "economicBuilding"; break; } if (!gameState.getOwnEntitiesByClass("Temple", true).hasEntities() && this.canBuild(gameState, "structures/{civ}/temple")) { plan = new PETRA.ConstructionPlan(gameState, "structures/{civ}/temple", { "phaseUp": true }); queue = "economicBuilding"; break; } if (!gameState.getOwnEntitiesByClass("Forge", true).hasEntities() && this.canBuild(gameState, "structures/{civ}/forge")) { plan = new PETRA.ConstructionPlan(gameState, "structures/{civ}/forge", { "phaseUp": true }); queue = "militaryBuilding"; break; } } break; default: // All classes not dealt with inside vanilla game. // We put them for the time being on the economic queue, except if wonder queue = entityReq.class == "Wonder" ? "wonder" : "economicBuilding"; if (!queues[queue].hasQueuedUnits()) { let structure = this.buildManager.findStructureWithClass(gameState, [entityReq.class]); if (structure && this.canBuild(gameState, structure)) plan = new PETRA.ConstructionPlan(gameState, structure, { "phaseUp": true }); } } if (plan) { if (queue == "wonder") { gameState.ai.queueManager.changePriority("majorTech", 400, { "phaseUp": true }); plan.queueToReset = "majorTech"; } else { gameState.ai.queueManager.changePriority(queue, 1000, { "phaseUp": true }); plan.queueToReset = queue; } queues[queue].addPlan(plan); return; } } }; /** Called by any "phase" research plan once it's started */ PETRA.HQ.prototype.OnPhaseUp = function(gameState, phase) { }; /** This code trains citizen workers, trying to keep close to a ratio of worker/soldiers */ PETRA.HQ.prototype.trainMoreWorkers = function(gameState, queues) { // default template let requirementsDef = [ ["costsResource", 1, "food"] ]; const classesDef = ["Support+Worker"]; let templateDef = this.findBestTrainableUnit(gameState, classesDef, requirementsDef); // counting the workers that aren't part of a plan let numberOfWorkers = 0; // all workers let numberOfSupports = 0; // only support workers (i.e. non fighting) gameState.getOwnUnits().forEach(ent => { if (ent.getMetadata(PlayerID, "role") == "worker" && ent.getMetadata(PlayerID, "plan") === undefined) { ++numberOfWorkers; if (ent.hasClass("Support")) ++numberOfSupports; } }); let numberInTraining = 0; gameState.getOwnTrainingFacilities().forEach(function(ent) { for (let item of ent.trainingQueue()) { numberInTraining += item.count; if (item.metadata && item.metadata.role && item.metadata.role == "worker" && item.metadata.plan === undefined) { numberOfWorkers += item.count; if (item.metadata.support) numberOfSupports += item.count; } } }); // Anticipate the optimal batch size when this queue will start // and adapt the batch size of the first and second queued workers to the present population // to ease a possible recovery if our population was drastically reduced by an attack // (need to go up to second queued as it is accounted in queueManager) let size = numberOfWorkers < 12 ? 1 : Math.min(5, Math.ceil(numberOfWorkers / 10)); if (queues.villager.plans[0]) { queues.villager.plans[0].number = Math.min(queues.villager.plans[0].number, size); if (queues.villager.plans[1]) queues.villager.plans[1].number = Math.min(queues.villager.plans[1].number, size); } if (queues.citizenSoldier.plans[0]) { queues.citizenSoldier.plans[0].number = Math.min(queues.citizenSoldier.plans[0].number, size); if (queues.citizenSoldier.plans[1]) queues.citizenSoldier.plans[1].number = Math.min(queues.citizenSoldier.plans[1].number, size); } let numberOfQueuedSupports = queues.villager.countQueuedUnits(); let numberOfQueuedSoldiers = queues.citizenSoldier.countQueuedUnits(); let numberQueued = numberOfQueuedSupports + numberOfQueuedSoldiers; let numberTotal = numberOfWorkers + numberQueued; if (this.saveResources && numberTotal > this.Config.Economy.popPhase2 + 10) return; if (numberTotal > this.targetNumWorkers || (numberTotal >= this.Config.Economy.popPhase2 && this.currentPhase == 1 && !gameState.isResearching(gameState.getPhaseName(2)))) return; if (numberQueued > 50 || (numberOfQueuedSupports > 20 && numberOfQueuedSoldiers > 20) || numberInTraining > 15) return; // Choose whether we want soldiers or support units: when full pop, we aim at targetNumWorkers workers // with supportRatio fraction of support units. But we want to have more support (less cost) at startup. // So we take: supportRatio*targetNumWorkers*(1 - exp(-alfa*currentWorkers/supportRatio/targetNumWorkers)) // This gives back supportRatio*targetNumWorkers when currentWorkers >> supportRatio*targetNumWorkers // and gives a ratio alfa at startup. let supportRatio = this.supportRatio; let alpha = 0.85; if (!gameState.isTemplateAvailable(gameState.applyCiv("structures/{civ}/field"))) supportRatio = Math.min(this.supportRatio, 0.1); if (this.attackManager.rushNumber < this.attackManager.maxRushes || this.attackManager.upcomingAttacks.Rush.length) alpha = 0.7; if (gameState.isCeasefireActive()) alpha += (1 - alpha) * Math.min(Math.max(gameState.ceasefireTimeRemaining - 120, 0), 180) / 180; let supportMax = supportRatio * this.targetNumWorkers; let supportNum = supportMax * (1 - Math.exp(-alpha*numberTotal/supportMax)); let template; if (!templateDef || numberOfSupports + numberOfQueuedSupports > supportNum) { let requirements; if (numberTotal < 45) requirements = [ ["speed", 0.5], ["costsResource", 0.5, "stone"], ["costsResource", 0.5, "metal"] ]; else requirements = [ ["strength", 1] ]; const classes = [["CitizenSoldier", "Infantry"]]; // We want at least 33% ranged and 33% melee. classes[0].push(pickRandom(["Ranged", "Melee", "Infantry"])); template = this.findBestTrainableUnit(gameState, classes, requirements); } // If the template variable is empty, the default unit (Support unit) will be used // base "0" means automatic choice of base if (!template && templateDef) queues.villager.addPlan(new PETRA.TrainingPlan(gameState, templateDef, { "role": "worker", "base": 0, "support": true }, size, size)); else if (template) queues.citizenSoldier.addPlan(new PETRA.TrainingPlan(gameState, template, { "role": "worker", "base": 0 }, size, size)); }; /** picks the best template based on parameters and classes */ PETRA.HQ.prototype.findBestTrainableUnit = function(gameState, classes, requirements) { let units; if (classes.indexOf("Hero") != -1) units = gameState.findTrainableUnits(classes, []); // We do not want siege tower as AI does not know how to use it nor hero when not explicitely specified. else units = gameState.findTrainableUnits(classes, ["Hero", "SiegeTower"]); if (!units.length) return undefined; let parameters = requirements.slice(); let remainingResources = this.getTotalResourceLevel(gameState); // resources (estimation) still gatherable in our territory let availableResources = gameState.ai.queueManager.getAvailableResources(gameState); // available (gathered) resources for (let type in remainingResources) { if (availableResources[type] > 800) continue; if (remainingResources[type] > 800) continue; let costsResource = remainingResources[type] > 400 ? 0.6 : 0.2; let toAdd = true; for (let param of parameters) { if (param[0] != "costsResource" || param[2] != type) continue; param[1] = Math.min(param[1], costsResource); toAdd = false; break; } if (toAdd) parameters.push(["costsResource", costsResource, type]); } units.sort((a, b) => { let aCost = 1 + a[1].costSum(); let bCost = 1 + b[1].costSum(); let aValue = 0.1; let bValue = 0.1; for (let param of parameters) { if (param[0] == "strength") { aValue += PETRA.getMaxStrength(a[1], gameState.ai.Config.debug, gameState.ai.Config.DamageTypeImportance) * param[1]; bValue += PETRA.getMaxStrength(b[1], gameState.ai.Config.debug, gameState.ai.Config.DamageTypeImportance) * param[1]; } else if (param[0] == "siegeStrength") { aValue += PETRA.getMaxStrength(a[1], gameState.ai.Config.debug, gameState.ai.Config.DamageTypeImportance, "Structure") * param[1]; bValue += PETRA.getMaxStrength(b[1], gameState.ai.Config.debug, gameState.ai.Config.DamageTypeImportance, "Structure") * param[1]; } else if (param[0] == "speed") { aValue += a[1].walkSpeed() * param[1]; bValue += b[1].walkSpeed() * param[1]; } else if (param[0] == "costsResource") { // requires a third parameter which is the resource if (a[1].cost()[param[2]]) aValue *= param[1]; if (b[1].cost()[param[2]]) bValue *= param[1]; } else if (param[0] == "canGather") { // checking against wood, could be anything else really. if (a[1].resourceGatherRates() && a[1].resourceGatherRates()["wood.tree"]) aValue *= param[1]; if (b[1].resourceGatherRates() && b[1].resourceGatherRates()["wood.tree"]) bValue *= param[1]; } else API3.warn(" trainMoreUnits avec non prevu " + uneval(param)); } return -aValue/aCost + bValue/bCost; }); return units[0][0]; }; /** * returns an entity collection of workers through BaseManager.pickBuilders * TODO: when same accessIndex, sort by distance */ PETRA.HQ.prototype.bulkPickWorkers = function(gameState, baseRef, number) { - let accessIndex = baseRef.accessIndex; - if (!accessIndex) - return false; - // sorting bases by whether they are on the same accessindex or not. - let baseBest = this.baseManagers.slice().sort((a, b) => { - if (a.accessIndex == accessIndex && b.accessIndex != accessIndex) - return -1; - else if (b.accessIndex == accessIndex && a.accessIndex != accessIndex) - return 1; - return 0; - }); - - let needed = number; - let workers = new API3.EntityCollection(gameState.sharedScript); - for (let base of baseBest) - { - if (base.ID == baseRef.ID) - continue; - base.pickBuilders(gameState, workers, needed); - if (workers.length >= number) - break; - needed = number - workers.length; - } - if (!workers.length) - return false; - return workers; + return this.basesManager.bulkPickWorkers(gameState, baseRef, number); }; -PETRA.HQ.prototype.getTotalResourceLevel = function(gameState) +PETRA.HQ.prototype.getTotalResourceLevel = function(gameState, resources, proximity) { - let total = {}; - for (let res of Resources.GetCodes()) - total[res] = 0; - for (let base of this.baseManagers) - for (let res in total) - total[res] += base.getResourceLevel(gameState, res); - - return total; + return this.basesManager.getTotalResourceLevel(gameState, resources, proximity); }; /** * Returns the current gather rate * This is not per-se exact, it performs a few adjustments ad-hoc to account for travel distance, stuffs like that. */ PETRA.HQ.prototype.GetCurrentGatherRates = function(gameState) { - if (!this.turnCache.currentRates) - { - let currentRates = {}; - for (let res of Resources.GetCodes()) - currentRates[res] = 0.5 * this.GetTCResGatherer(res); - - for (let base of this.baseManagers) - base.addGatherRates(gameState, currentRates); - - for (let res of Resources.GetCodes()) - currentRates[res] = Math.max(currentRates[res], 0); - - this.turnCache.currentRates = currentRates; - } - - return this.turnCache.currentRates; + return this.basesManager.GetCurrentGatherRates(gameState); }; /** * Returns the wanted gather rate. */ PETRA.HQ.prototype.GetWantedGatherRates = function(gameState) { if (!this.turnCache.wantedRates) this.turnCache.wantedRates = gameState.ai.queueManager.wantedGatherRates(gameState); return this.turnCache.wantedRates; }; /** * Pick the resource which most needs another worker * How this works: * We get the rates we would want to have to be able to deal with our plans * We get our current rates * We compare; we pick the one where the discrepancy is highest. * Need to balance long-term needs and possible short-term needs. */ PETRA.HQ.prototype.pickMostNeededResources = function(gameState, allowedResources = []) { let wantedRates = this.GetWantedGatherRates(gameState); let currentRates = this.GetCurrentGatherRates(gameState); if (!allowedResources.length) allowedResources = Resources.GetCodes(); let needed = []; for (let res of allowedResources) needed.push({ "type": res, "wanted": wantedRates[res], "current": currentRates[res] }); needed.sort((a, b) => { if (a.current < a.wanted && b.current < b.wanted) { if (a.current && b.current) return b.wanted / b.current - a.wanted / a.current; if (a.current) return 1; if (b.current) return -1; return b.wanted - a.wanted; } if (a.current < a.wanted || a.wanted && !b.wanted) return -1; if (b.current < b.wanted || b.wanted && !a.wanted) return 1; return a.current - a.wanted - b.current + b.wanted; }); return needed; }; /** * Returns the best position to build a new Civil Center * Whose primary function would be to reach new resources of type "resource". */ PETRA.HQ.prototype.findEconomicCCLocation = function(gameState, template, resource, proximity, fromStrategic) { // This builds a map. The procedure is fairly simple. It adds the resource maps // (which are dynamically updated and are made so that they will facilitate DP placement) // Then look for a good spot. Engine.ProfileStart("findEconomicCCLocation"); // obstruction map let obstructions = PETRA.createObstructionMap(gameState, 0, template); let halfSize = 0; if (template.get("Footprint/Square")) halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; else if (template.get("Footprint/Circle")) halfSize = +template.get("Footprint/Circle/@radius"); let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); const dpEnts = gameState.getOwnDropsites().filter(API3.Filters.not(API3.Filters.byClasses(["CivCentre", "Unit"]))); let ccList = []; for (let cc of ccEnts.values()) ccList.push({ "ent": cc, "pos": cc.position(), "ally": gameState.isPlayerAlly(cc.owner()) }); let dpList = []; for (let dp of dpEnts.values()) dpList.push({ "ent": dp, "pos": dp.position(), "territory": this.territoryMap.getOwner(dp.position()) }); let bestIdx; let bestVal; let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); let scale = 250 * 250; let proxyAccess; let nbShips = this.navalManager.transportShips.length; if (proximity) // this is our first base { // if our first base, ensure room around radius = Math.ceil((template.obstructionRadius().max + 8) / obstructions.cellSize); // scale is the typical scale at which we want to find a location for our first base // look for bigger scale if we start from a ship (access < 2) or from a small island let cellArea = gameState.getPassabilityMap().cellSize * gameState.getPassabilityMap().cellSize; proxyAccess = gameState.ai.accessibility.getAccessValue(proximity); if (proxyAccess < 2 || cellArea*gameState.ai.accessibility.regionSize[proxyAccess] < 24000) scale = 400 * 400; } let width = this.territoryMap.width; let cellSize = this.territoryMap.cellSize; // DistanceSquare cuts to other ccs (bigger or no cuts on inaccessible ccs to allow colonizing other islands). let reduce = (template.hasClass("Colony") ? 30 : 0) + 30 * this.Config.personality.defensive; let nearbyRejected = Math.square(120); // Reject if too near from any cc let nearbyAllyRejected = Math.square(200); // Reject if too near from an allied cc let nearbyAllyDisfavored = Math.square(250); // Disfavor if quite near an allied cc let maxAccessRejected = Math.square(410); // Reject if too far from an accessible ally cc let maxAccessDisfavored = Math.square(360 - reduce); // Disfavor if quite far from an accessible ally cc let maxNoAccessDisfavored = Math.square(500); // Disfavor if quite far from an inaccessible ally cc let cut = 60; if (fromStrategic || proximity) // be less restrictive cut = 30; for (let j = 0; j < this.territoryMap.length; ++j) { if (this.territoryMap.getOwnerIndex(j) != 0) continue; // With enough room around to build the cc let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); if (i < 0) continue; // We require that it is accessible let index = gameState.ai.accessibility.landPassMap[i]; if (!this.landRegions[index]) continue; if (proxyAccess && nbShips == 0 && proxyAccess != index) continue; let norm = 0.5; // TODO adjust it, knowing that we will sum 5 maps // Checking distance to other cc let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; // We will be more tolerant for cc around our oversea docks let oversea = false; if (proximity) // This is our first cc, let's do it near our units norm /= 1 + API3.SquareVectorDistance(proximity, pos) / scale; else { let minDist = Math.min(); let accessible = false; for (let cc of ccList) { let dist = API3.SquareVectorDistance(cc.pos, pos); if (dist < nearbyRejected) { norm = 0; break; } if (!cc.ally) continue; if (dist < nearbyAllyRejected) { norm = 0; break; } if (dist < nearbyAllyDisfavored) norm *= 0.5; if (dist < minDist) minDist = dist; accessible = accessible || index == PETRA.getLandAccess(gameState, cc.ent); } if (norm == 0) continue; if (accessible && minDist > maxAccessRejected) continue; if (minDist > maxAccessDisfavored) // Disfavor if quite far from any allied cc { if (!accessible) { if (minDist > maxNoAccessDisfavored) norm *= 0.5; else norm *= 0.8; } else norm *= 0.5; } // Not near any of our dropsite, except for oversea docks oversea = !accessible && dpList.some(dp => PETRA.getLandAccess(gameState, dp.ent) == index); if (!oversea) { for (let dp of dpList) { let dist = API3.SquareVectorDistance(dp.pos, pos); if (dist < 3600) { norm = 0; break; } else if (dist < 6400) norm *= 0.5; } } if (norm == 0) continue; } if (this.borderMap.map[j] & PETRA.fullBorder_Mask) // disfavor the borders of the map norm *= 0.5; let val = 2 * gameState.sharedScript.ccResourceMaps[resource].map[j]; for (let res in gameState.sharedScript.resourceMaps) if (res != "food") val += gameState.sharedScript.ccResourceMaps[res].map[j]; val *= norm; // If oversea, be just above threshold to be accepted if nothing else if (oversea) val = Math.max(val, cut + 0.1); if (bestVal !== undefined && val < bestVal) continue; if (this.isDangerousLocation(gameState, pos, halfSize)) continue; bestVal = val; bestIdx = i; } Engine.ProfileStop(); if (bestVal === undefined) return false; if (this.Config.debug > 1) API3.warn("we have found a base for " + resource + " with best (cut=" + cut + ") = " + bestVal); // not good enough. if (bestVal < cut) return false; let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; // Define a minimal number of wanted ships in the seas reaching this new base let indexIdx = gameState.ai.accessibility.landPassMap[bestIdx]; - for (let base of this.baseManagers) + for (const base of this.baseManagers()) { if (!base.anchor || base.accessIndex == indexIdx) continue; let sea = this.getSeaBetweenIndices(gameState, base.accessIndex, indexIdx); if (sea !== undefined) this.navalManager.setMinimalTransportShips(gameState, sea, 1); } return [x, z]; }; /** * Returns the best position to build a new Civil Center * Whose primary function would be to assure territorial continuity with our allies */ PETRA.HQ.prototype.findStrategicCCLocation = function(gameState, template) { // This builds a map. The procedure is fairly simple. // We minimize the Sum((dist - 300)^2) where the sum is on the three nearest allied CC // with the constraints that all CC have dist > 200 and at least one have dist < 400 // This needs at least 2 CC. Otherwise, go back to economic CC. let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); let ccList = []; let numAllyCC = 0; for (let cc of ccEnts.values()) { let ally = gameState.isPlayerAlly(cc.owner()); ccList.push({ "pos": cc.position(), "ally": ally }); if (ally) ++numAllyCC; } if (numAllyCC < 2) return this.findEconomicCCLocation(gameState, template, "wood", undefined, true); Engine.ProfileStart("findStrategicCCLocation"); // obstruction map let obstructions = PETRA.createObstructionMap(gameState, 0, template); let halfSize = 0; if (template.get("Footprint/Square")) halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; else if (template.get("Footprint/Circle")) halfSize = +template.get("Footprint/Circle/@radius"); let bestIdx; let bestVal; let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); let width = this.territoryMap.width; let cellSize = this.territoryMap.cellSize; let currentVal, delta; let distcc0, distcc1, distcc2; let favoredDistance = (template.hasClass("Colony") ? 220 : 280) - 40 * this.Config.personality.defensive; for (let j = 0; j < this.territoryMap.length; ++j) { if (this.territoryMap.getOwnerIndex(j) != 0) continue; // with enough room around to build the cc let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); if (i < 0) continue; // we require that it is accessible let index = gameState.ai.accessibility.landPassMap[i]; if (!this.landRegions[index]) continue; // checking distances to other cc let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; let minDist = Math.min(); distcc0 = undefined; for (let cc of ccList) { let dist = API3.SquareVectorDistance(cc.pos, pos); if (dist < 14000) // Reject if too near from any cc { minDist = 0; break; } if (!cc.ally) continue; if (dist < 62000) // Reject if quite near from ally cc { minDist = 0; break; } if (dist < minDist) minDist = dist; if (!distcc0 || dist < distcc0) { distcc2 = distcc1; distcc1 = distcc0; distcc0 = dist; } else if (!distcc1 || dist < distcc1) { distcc2 = distcc1; distcc1 = dist; } else if (!distcc2 || dist < distcc2) distcc2 = dist; } if (minDist < 1 || minDist > 170000 && !this.navalMap) continue; delta = Math.sqrt(distcc0) - favoredDistance; currentVal = delta*delta; delta = Math.sqrt(distcc1) - favoredDistance; currentVal += delta*delta; if (distcc2) { delta = Math.sqrt(distcc2) - favoredDistance; currentVal += delta*delta; } // disfavor border of the map if (this.borderMap.map[j] & PETRA.fullBorder_Mask) currentVal += 10000; if (bestVal !== undefined && currentVal > bestVal) continue; if (this.isDangerousLocation(gameState, pos, halfSize)) continue; bestVal = currentVal; bestIdx = i; } if (this.Config.debug > 1) API3.warn("We've found a strategic base with bestVal = " + bestVal); Engine.ProfileStop(); if (bestVal === undefined) return undefined; let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; // Define a minimal number of wanted ships in the seas reaching this new base let indexIdx = gameState.ai.accessibility.landPassMap[bestIdx]; - for (let base of this.baseManagers) + for (const base of this.baseManagers()) { if (!base.anchor || base.accessIndex == indexIdx) continue; let sea = this.getSeaBetweenIndices(gameState, base.accessIndex, indexIdx); if (sea !== undefined) this.navalManager.setMinimalTransportShips(gameState, sea, 1); } return [x, z]; }; /** * Returns the best position to build a new market: if the allies already have a market, build it as far as possible * from it, although not in our border to be able to defend it easily. If no allied market, our second market will * follow the same logic. * To do so, we suppose that the gain/distance is an increasing function of distance and look for the max distance * for performance reasons. */ PETRA.HQ.prototype.findMarketLocation = function(gameState, template) { let markets = gameState.updatingCollection("diplo-ExclusiveAllyMarkets", API3.Filters.byClass("Trade"), gameState.getExclusiveAllyEntities()).toEntityArray(); if (!markets.length) markets = gameState.updatingCollection("OwnMarkets", API3.Filters.byClass("Trade"), gameState.getOwnStructures()).toEntityArray(); if (!markets.length) // this is the first market. For the time being, place it arbitrarily by the ConstructionPlan return [-1, -1, -1, 0]; // No need for more than one market when we cannot trade. if (!Resources.GetTradableCodes().length) return false; // obstruction map let obstructions = PETRA.createObstructionMap(gameState, 0, template); let halfSize = 0; if (template.get("Footprint/Square")) halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; else if (template.get("Footprint/Circle")) halfSize = +template.get("Footprint/Circle/@radius"); let bestIdx; let bestJdx; let bestVal; let bestDistSq; let bestGainMult; let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); const isNavalMarket = template.hasClasses(["Naval+Trade"]); let width = this.territoryMap.width; let cellSize = this.territoryMap.cellSize; let traderTemplatesGains = gameState.getTraderTemplatesGains(); for (let j = 0; j < this.territoryMap.length; ++j) { // do not try on the narrow border of our territory if (this.borderMap.map[j] & PETRA.narrowFrontier_Mask) continue; - if (this.basesMap.map[j] == 0) // only in our territory + if (this.baseAtIndex(j) == 0) // only in our territory continue; // with enough room around to build the market let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); if (i < 0) continue; let index = gameState.ai.accessibility.landPassMap[i]; if (!this.landRegions[index]) continue; let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; // checking distances to other markets let maxVal = 0; let maxDistSq; let maxGainMult; let gainMultiplier; for (let market of markets) { if (isNavalMarket && template.hasClasses(["Naval+Trade"])) { if (PETRA.getSeaAccess(gameState, market) != gameState.ai.accessibility.getAccessValue(pos, true)) continue; gainMultiplier = traderTemplatesGains.navalGainMultiplier; } else if (PETRA.getLandAccess(gameState, market) == index && !PETRA.isLineInsideEnemyTerritory(gameState, market.position(), pos)) gainMultiplier = traderTemplatesGains.landGainMultiplier; else continue; if (!gainMultiplier) continue; let distSq = API3.SquareVectorDistance(market.position(), pos); if (gainMultiplier * distSq > maxVal) { maxVal = gainMultiplier * distSq; maxDistSq = distSq; maxGainMult = gainMultiplier; } } if (maxVal == 0) continue; if (bestVal !== undefined && maxVal < bestVal) continue; if (this.isDangerousLocation(gameState, pos, halfSize)) continue; bestVal = maxVal; bestDistSq = maxDistSq; bestGainMult = maxGainMult; bestIdx = i; bestJdx = j; } if (this.Config.debug > 1) API3.warn("We found a market position with bestVal = " + bestVal); if (bestVal === undefined) // no constraints. For the time being, place it arbitrarily by the ConstructionPlan return [-1, -1, -1, 0]; let expectedGain = Math.round(bestGainMult * TradeGain(bestDistSq, gameState.sharedScript.mapSize)); if (this.Config.debug > 1) API3.warn("this would give a trading gain of " + expectedGain); // Do not keep it if gain is too small, except if this is our first Market. let idx; if (expectedGain < this.tradeManager.minimalGain) { if (template.hasClass("Market") && !gameState.getOwnEntitiesByClass("Market", true).hasEntities()) idx = -1; // Needed by queueplanBuilding manager to keep that Market. else return false; } else - idx = this.basesMap.map[bestJdx]; + idx = this.baseAtIndex(bestJdx); let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; return [x, z, idx, expectedGain]; }; /** * Returns the best position to build defensive buildings (fortress and towers) * Whose primary function is to defend our borders */ PETRA.HQ.prototype.findDefensiveLocation = function(gameState, template) { // We take the point in our territory which is the nearest to any enemy cc // but requiring a minimal distance with our other defensive structures // and not in range of any enemy defensive structure to avoid building under fire. const ownStructures = gameState.getOwnStructures().filter(API3.Filters.byClasses(["Fortress", "Tower"])).toEntityArray(); let enemyStructures = gameState.getEnemyStructures().filter(API3.Filters.not(API3.Filters.byOwner(0))). filter(API3.Filters.byClasses(["CivCentre", "Fortress", "Tower"])); if (!enemyStructures.hasEntities()) // we may be in cease fire mode, build defense against neutrals { enemyStructures = gameState.getNeutralStructures().filter(API3.Filters.not(API3.Filters.byOwner(0))). filter(API3.Filters.byClasses(["CivCentre", "Fortress", "Tower"])); if (!enemyStructures.hasEntities() && !gameState.getAlliedVictory()) enemyStructures = gameState.getAllyStructures().filter(API3.Filters.not(API3.Filters.byOwner(PlayerID))). filter(API3.Filters.byClasses(["CivCentre", "Fortress", "Tower"])); if (!enemyStructures.hasEntities()) return undefined; } enemyStructures = enemyStructures.toEntityArray(); let wonderMode = gameState.getVictoryConditions().has("wonder"); let wonderDistmin; let wonders; if (wonderMode) { wonders = gameState.getOwnStructures().filter(API3.Filters.byClass("Wonder")).toEntityArray(); wonderMode = wonders.length != 0; if (wonderMode) wonderDistmin = (50 + wonders[0].footprintRadius()) * (50 + wonders[0].footprintRadius()); } // obstruction map let obstructions = PETRA.createObstructionMap(gameState, 0, template); let halfSize = 0; if (template.get("Footprint/Square")) halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; else if (template.get("Footprint/Circle")) halfSize = +template.get("Footprint/Circle/@radius"); let bestIdx; let bestJdx; let bestVal; let width = this.territoryMap.width; let cellSize = this.territoryMap.cellSize; let isTower = template.hasClass("Tower"); let isFortress = template.hasClass("Fortress"); let radius; if (isFortress) radius = Math.floor((template.obstructionRadius().max + 8) / obstructions.cellSize); else radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); for (let j = 0; j < this.territoryMap.length; ++j) { if (!wonderMode) { // do not try if well inside or outside territory if (!(this.borderMap.map[j] & PETRA.fullFrontier_Mask)) continue; if (this.borderMap.map[j] & PETRA.largeFrontier_Mask && isTower) continue; } - if (this.basesMap.map[j] == 0) // inaccessible cell + if (this.baseAtIndex(j) == 0) // inaccessible cell continue; // with enough room around to build the cc let i = this.territoryMap.getNonObstructedTile(j, radius, obstructions); if (i < 0) continue; let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; // checking distances to other structures let minDist = Math.min(); let dista = 0; if (wonderMode) { dista = API3.SquareVectorDistance(wonders[0].position(), pos); if (dista < wonderDistmin) continue; dista *= 200; // empirical factor (TODO should depend on map size) to stay near the wonder } for (let str of enemyStructures) { if (str.foundationProgress() !== undefined) continue; let strPos = str.position(); if (!strPos) continue; let dist = API3.SquareVectorDistance(strPos, pos); if (dist < 6400) // TODO check on true attack range instead of this 80×80 { minDist = -1; break; } if (str.hasClass("CivCentre") && dist + dista < minDist) minDist = dist + dista; } if (minDist < 0) continue; let cutDist = 900; // 30×30 TODO maybe increase it for (let str of ownStructures) { let strPos = str.position(); if (!strPos) continue; if (API3.SquareVectorDistance(strPos, pos) < cutDist) { minDist = -1; break; } } if (minDist < 0 || minDist == Math.min()) continue; if (bestVal !== undefined && minDist > bestVal) continue; if (this.isDangerousLocation(gameState, pos, halfSize)) continue; bestVal = minDist; bestIdx = i; bestJdx = j; } if (bestVal === undefined) return undefined; let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; - return [x, z, this.basesMap.map[bestJdx]]; + return [x, z, this.baseAtIndex(bestJdx)]; }; PETRA.HQ.prototype.buildTemple = function(gameState, queues) { // at least one market (which have the same queue) should be build before any temple if (queues.economicBuilding.hasQueuedUnits() || gameState.getOwnEntitiesByClass("Temple", true).hasEntities() || !gameState.getOwnEntitiesByClass("Market", true).hasEntities()) return; // Try to build a temple earlier if in regicide to recruit healer guards if (this.currentPhase < 3 && !gameState.getVictoryConditions().has("regicide")) return; let templateName = "structures/{civ}/temple"; if (this.canBuild(gameState, "structures/{civ}/temple_vesta")) templateName = "structures/{civ}/temple_vesta"; else if (!this.canBuild(gameState, templateName)) return; queues.economicBuilding.addPlan(new PETRA.ConstructionPlan(gameState, templateName)); }; PETRA.HQ.prototype.buildMarket = function(gameState, queues) { if (gameState.getOwnEntitiesByClass("Market", true).hasEntities() || !this.canBuild(gameState, "structures/{civ}/market")) return; if (queues.economicBuilding.hasQueuedUnitsWithClass("Market")) { if (!queues.economicBuilding.paused) { // Put available resources in this market let queueManager = gameState.ai.queueManager; let cost = queues.economicBuilding.plans[0].getCost(); queueManager.setAccounts(gameState, cost, "economicBuilding"); if (!queueManager.canAfford("economicBuilding", cost)) { for (let q in queueManager.queues) { if (q == "economicBuilding") continue; queueManager.transferAccounts(cost, q, "economicBuilding"); if (queueManager.canAfford("economicBuilding", cost)) break; } } } return; } gameState.ai.queueManager.changePriority("economicBuilding", 3 * this.Config.priorities.economicBuilding); let plan = new PETRA.ConstructionPlan(gameState, "structures/{civ}/market"); plan.queueToReset = "economicBuilding"; queues.economicBuilding.addPlan(plan); }; /** Build a farmstead */ PETRA.HQ.prototype.buildFarmstead = function(gameState, queues) { // Only build one farmstead for the time being ("DropsiteFood" does not refer to CCs) if (gameState.getOwnEntitiesByClass("Farmstead", true).hasEntities()) return; // Wait to have at least one dropsite and house before the farmstead if (!gameState.getOwnEntitiesByClass("Storehouse", true).hasEntities()) return; if (!gameState.getOwnEntitiesByClass("House", true).hasEntities()) return; if (queues.economicBuilding.hasQueuedUnitsWithClass("DropsiteFood")) return; if (!this.canBuild(gameState, "structures/{civ}/farmstead")) return; queues.economicBuilding.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/farmstead")); }; /** * Try to build a wonder when required * force = true when called from the victoryManager in case of Wonder victory condition. */ PETRA.HQ.prototype.buildWonder = function(gameState, queues, force = false) { if (queues.wonder && queues.wonder.hasQueuedUnits() || gameState.getOwnEntitiesByClass("Wonder", true).hasEntities() || !this.canBuild(gameState, "structures/{civ}/wonder")) return; if (!force) { let template = gameState.getTemplate(gameState.applyCiv("structures/{civ}/wonder")); // Check that we have enough resources to start thinking to build a wonder let cost = template.cost(); let resources = gameState.getResources(); let highLevel = 0; let lowLevel = 0; for (let res in cost) { if (resources[res] && resources[res] > 0.7 * cost[res]) ++highLevel; else if (!resources[res] || resources[res] < 0.3 * cost[res]) ++lowLevel; } if (highLevel == 0 || lowLevel > 1) return; } queues.wonder.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/wonder")); }; /** Build a corral, and train animals there */ PETRA.HQ.prototype.manageCorral = function(gameState, queues) { if (queues.corral.hasQueuedUnits()) return; let nCorral = gameState.getOwnEntitiesByClass("Corral", true).length; if (!nCorral || !gameState.isTemplateAvailable(gameState.applyCiv("structures/{civ}/field")) && nCorral < this.currentPhase && gameState.getPopulation() > 30 * nCorral) { if (this.canBuild(gameState, "structures/{civ}/corral")) { queues.corral.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/corral")); return; } if (!nCorral) return; } // And train some animals let civ = gameState.getPlayerCiv(); for (let corral of gameState.getOwnEntitiesByClass("Corral", true).values()) { if (corral.foundationProgress() !== undefined) continue; let trainables = corral.trainableEntities(civ); for (let trainable of trainables) { if (gameState.isTemplateDisabled(trainable)) continue; let template = gameState.getTemplate(trainable); if (!template || !template.isHuntable()) continue; let count = gameState.countEntitiesByType(trainable, true); for (let item of corral.trainingQueue()) count += item.count; if (count > nCorral) continue; queues.corral.addPlan(new PETRA.TrainingPlan(gameState, trainable, { "trainer": corral.id() })); return; } } }; /** * build more houses if needed. * kinda ugly, lots of special cases to both build enough houses but not tooo many… */ PETRA.HQ.prototype.buildMoreHouses = function(gameState, queues) { let houseTemplateString = "structures/{civ}/apartment"; if (!gameState.isTemplateAvailable(gameState.applyCiv(houseTemplateString)) || !this.canBuild(gameState, houseTemplateString)) { houseTemplateString = "structures/{civ}/house"; if (!gameState.isTemplateAvailable(gameState.applyCiv(houseTemplateString))) return; } if (gameState.getPopulationMax() <= gameState.getPopulationLimit()) return; let numPlanned = queues.house.length(); if (numPlanned < 3 || numPlanned < 5 && gameState.getPopulation() > 80) { let plan = new PETRA.ConstructionPlan(gameState, houseTemplateString); // change the starting condition according to the situation. plan.goRequirement = "houseNeeded"; queues.house.addPlan(plan); } if (numPlanned > 0 && this.phasing && gameState.getPhaseEntityRequirements(this.phasing).length) { let houseTemplateName = gameState.applyCiv(houseTemplateString); let houseTemplate = gameState.getTemplate(houseTemplateName); let needed = 0; for (let entityReq of gameState.getPhaseEntityRequirements(this.phasing)) { if (!houseTemplate.hasClass(entityReq.class)) continue; let count = gameState.getOwnStructures().filter(API3.Filters.byClass(entityReq.class)).length; if (count < entityReq.count && this.buildManager.isUnbuildable(gameState, houseTemplateName)) { if (this.Config.debug > 1) API3.warn("no room to place a house ... try to be less restrictive"); this.buildManager.setBuildable(houseTemplateName); this.requireHouses = true; } needed = Math.max(needed, entityReq.count - count); } let houseQueue = queues.house.plans; for (let i = 0; i < numPlanned; ++i) if (houseQueue[i].isGo(gameState)) --needed; else if (needed > 0) { houseQueue[i].goRequirement = undefined; --needed; } } if (this.requireHouses) { let houseTemplate = gameState.getTemplate(gameState.applyCiv(houseTemplateString)); if (!this.phasing || gameState.getPhaseEntityRequirements(this.phasing).every(req => !houseTemplate.hasClass(req.class) || gameState.getOwnStructures().filter(API3.Filters.byClass(req.class)).length >= req.count)) this.requireHouses = undefined; } // When population limit too tight // - if no room to build, try to improve with technology // - otherwise increase temporarily the priority of houses let house = gameState.applyCiv(houseTemplateString); let HouseNb = gameState.getOwnFoundations().filter(API3.Filters.byClass("House")).length; let popBonus = gameState.getTemplate(house).getPopulationBonus(); let freeSlots = gameState.getPopulationLimit() + HouseNb*popBonus - this.getAccountedPopulation(gameState); let priority; if (freeSlots < 5) { if (this.buildManager.isUnbuildable(gameState, house)) { if (this.Config.debug > 1) API3.warn("no room to place a house ... try to improve with technology"); this.researchManager.researchPopulationBonus(gameState, queues); } else priority = 2 * this.Config.priorities.house; } else priority = this.Config.priorities.house; if (priority && priority != gameState.ai.queueManager.getPriority("house")) gameState.ai.queueManager.changePriority("house", priority); }; /** Checks the status of the territory expansion. If no new economic bases created, build some strategic ones. */ PETRA.HQ.prototype.checkBaseExpansion = function(gameState, queues) { if (queues.civilCentre.hasQueuedUnits()) return; // First build one cc if all have been destroyed - if (this.numPotentialBases() == 0) + if (!this.hasPotentialBase()) { this.buildFirstBase(gameState); return; } // Then expand if we have not enough room available for buildings if (this.buildManager.numberMissingRoom(gameState) > 1) { if (this.Config.debug > 2) API3.warn("try to build a new base because not enough room to build "); this.buildNewBase(gameState, queues); return; } // If we've already planned to phase up, wait a bit before trying to expand if (this.phasing) return; // Finally expand if we have lots of units (threshold depending on the aggressivity value) let activeBases = this.numActiveBases(); let numUnits = gameState.getOwnUnits().length; let numvar = 10 * (1 - this.Config.personality.aggressive); if (numUnits > activeBases * (65 + numvar + (10 + numvar)*(activeBases-1)) || this.saveResources && numUnits > 50) { if (this.Config.debug > 2) API3.warn("try to build a new base because of population " + numUnits + " for " + activeBases + " CCs"); this.buildNewBase(gameState, queues); } }; PETRA.HQ.prototype.buildNewBase = function(gameState, queues, resource) { - if (this.numPotentialBases() > 0 && this.currentPhase == 1 && !gameState.isResearching(gameState.getPhaseName(2))) + if (this.hasPotentialBase() && this.currentPhase == 1 && !gameState.isResearching(gameState.getPhaseName(2))) return false; if (gameState.getOwnFoundations().filter(API3.Filters.byClass("CivCentre")).hasEntities() || queues.civilCentre.hasQueuedUnits()) return false; let template; // We require at least one of this civ civCentre as they may allow specific units or techs let hasOwnCC = false; for (let ent of gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")).values()) { if (ent.owner() != PlayerID || ent.templateName() != gameState.applyCiv("structures/{civ}/civil_centre")) continue; hasOwnCC = true; break; } if (hasOwnCC && this.canBuild(gameState, "structures/{civ}/military_colony")) template = "structures/{civ}/military_colony"; else if (this.canBuild(gameState, "structures/{civ}/civil_centre")) template = "structures/{civ}/civil_centre"; else if (!hasOwnCC && this.canBuild(gameState, "structures/{civ}/military_colony")) template = "structures/{civ}/military_colony"; else return false; // base "-1" means new base. if (this.Config.debug > 1) API3.warn("new base " + gameState.applyCiv(template) + " planned with resource " + resource); queues.civilCentre.addPlan(new PETRA.ConstructionPlan(gameState, template, { "base": -1, "resource": resource })); return true; }; /** Deals with building fortresses and towers along our border with enemies. */ PETRA.HQ.prototype.buildDefenses = function(gameState, queues) { if (this.saveResources && !this.canBarter || queues.defenseBuilding.hasQueuedUnits()) return; if (!this.saveResources && (this.currentPhase > 2 || gameState.isResearching(gameState.getPhaseName(3)))) { // Try to build fortresses. if (this.canBuild(gameState, "structures/{civ}/fortress")) { let numFortresses = gameState.getOwnEntitiesByClass("Fortress", true).length; if ((!numFortresses || gameState.ai.elapsedTime > (1 + 0.10 * numFortresses) * this.fortressLapseTime + this.fortressStartTime) && numFortresses < this.numActiveBases() + 1 + this.extraFortresses && numFortresses < Math.floor(gameState.getPopulation() / 25) && gameState.getOwnFoundationsByClass("Fortress").length < 2) { this.fortressStartTime = gameState.ai.elapsedTime; if (!numFortresses) gameState.ai.queueManager.changePriority("defenseBuilding", 2 * this.Config.priorities.defenseBuilding); let plan = new PETRA.ConstructionPlan(gameState, "structures/{civ}/fortress"); plan.queueToReset = "defenseBuilding"; queues.defenseBuilding.addPlan(plan); return; } } } if (this.Config.Military.numSentryTowers && this.currentPhase < 2 && this.canBuild(gameState, "structures/{civ}/sentry_tower")) { // Count all towers + wall towers. let numTowers = gameState.getOwnEntitiesByClass("Tower", true).length + gameState.getOwnEntitiesByClass("WallTower", true).length; let towerLapseTime = this.saveResource ? (1 + 0.5 * numTowers) * this.towerLapseTime : this.towerLapseTime; if (numTowers < this.Config.Military.numSentryTowers && gameState.ai.elapsedTime > towerLapseTime + this.fortStartTime) { this.fortStartTime = gameState.ai.elapsedTime; queues.defenseBuilding.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/sentry_tower")); } return; } if (this.currentPhase < 2 || !this.canBuild(gameState, "structures/{civ}/defense_tower")) return; let numTowers = gameState.getOwnEntitiesByClass("StoneTower", true).length; let towerLapseTime = this.saveResource ? (1 + numTowers) * this.towerLapseTime : this.towerLapseTime; if ((!numTowers || gameState.ai.elapsedTime > (1 + 0.1 * numTowers) * towerLapseTime + this.towerStartTime) && numTowers < 2 * this.numActiveBases() + 3 + this.extraTowers && numTowers < Math.floor(gameState.getPopulation() / 8) && gameState.getOwnFoundationsByClass("Tower").length < 3) { this.towerStartTime = gameState.ai.elapsedTime; if (numTowers > 2 * this.numActiveBases() + 3) gameState.ai.queueManager.changePriority("defenseBuilding", Math.round(0.7 * this.Config.priorities.defenseBuilding)); let plan = new PETRA.ConstructionPlan(gameState, "structures/{civ}/defense_tower"); plan.queueToReset = "defenseBuilding"; queues.defenseBuilding.addPlan(plan); } }; PETRA.HQ.prototype.buildForge = function(gameState, queues) { if (this.getAccountedPopulation(gameState) < this.Config.Military.popForForge || queues.militaryBuilding.hasQueuedUnits() || gameState.getOwnEntitiesByClass("Forge", true).length) return; // Build a Market before the Forge. if (!gameState.getOwnEntitiesByClass("Market", true).hasEntities()) return; if (this.canBuild(gameState, "structures/{civ}/forge")) queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/forge")); }; /** * Deals with constructing military buildings (e.g. barracks, stable). * They are mostly defined by Config.js. This is unreliable since changes could be done easily. */ PETRA.HQ.prototype.constructTrainingBuildings = function(gameState, queues) { if (this.saveResources && !this.canBarter || queues.militaryBuilding.hasQueuedUnits()) return; let numBarracks = gameState.getOwnEntitiesByClass("Barracks", true).length; if (this.saveResources && numBarracks != 0) return; let barracksTemplate = this.canBuild(gameState, "structures/{civ}/barracks") ? "structures/{civ}/barracks" : undefined; let rangeTemplate = this.canBuild(gameState, "structures/{civ}/range") ? "structures/{civ}/range" : undefined; let numRanges = gameState.getOwnEntitiesByClass("Range", true).length; let stableTemplate = this.canBuild(gameState, "structures/{civ}/stable") ? "structures/{civ}/stable" : undefined; let numStables = gameState.getOwnEntitiesByClass("Stable", true).length; if (this.getAccountedPopulation(gameState) > this.Config.Military.popForBarracks1 || this.phasing == 2 && gameState.getOwnStructures().filter(API3.Filters.byClass("Village")).length < 5) { // First barracks/range and stable. if (numBarracks + numRanges == 0) { let template = barracksTemplate || rangeTemplate; if (template) { gameState.ai.queueManager.changePriority("militaryBuilding", 2 * this.Config.priorities.militaryBuilding); let plan = new PETRA.ConstructionPlan(gameState, template, { "militaryBase": true }); plan.queueToReset = "militaryBuilding"; queues.militaryBuilding.addPlan(plan); return; } } if (numStables == 0 && stableTemplate) { queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, stableTemplate, { "militaryBase": true })); return; } // Second barracks/range and stable. if (numBarracks + numRanges == 1 && this.getAccountedPopulation(gameState) > this.Config.Military.popForBarracks2) { let template = numBarracks == 0 ? (barracksTemplate || rangeTemplate) : (rangeTemplate || barracksTemplate); if (template) { queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, template, { "militaryBase": true })); return; } } if (numStables == 1 && stableTemplate && this.getAccountedPopulation(gameState) > this.Config.Military.popForBarracks2) { queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, stableTemplate, { "militaryBase": true })); return; } // Third barracks/range and stable, if needed. if (numBarracks + numRanges + numStables == 2 && this.getAccountedPopulation(gameState) > this.Config.Military.popForBarracks2 + 30) { let template = barracksTemplate || stableTemplate || rangeTemplate; if (template) { queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, template, { "militaryBase": true })); return; } } } if (this.saveResources) return; if (this.currentPhase < 3) return; if (this.canBuild(gameState, "structures/{civ}/elephant_stable") && !gameState.getOwnEntitiesByClass("ElephantStable", true).hasEntities()) { queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/elephant_stable", { "militaryBase": true })); return; } if (this.canBuild(gameState, "structures/{civ}/arsenal") && !gameState.getOwnEntitiesByClass("Arsenal", true).hasEntities()) { queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/arsenal", { "militaryBase": true })); return; } if (this.getAccountedPopulation(gameState) < 80 || !this.bAdvanced.length) return; // Build advanced military buildings let nAdvanced = 0; for (let advanced of this.bAdvanced) nAdvanced += gameState.countEntitiesAndQueuedByType(advanced, true); if (!nAdvanced || nAdvanced < this.bAdvanced.length && this.getAccountedPopulation(gameState) > 110) { for (let advanced of this.bAdvanced) { if (gameState.countEntitiesAndQueuedByType(advanced, true) > 0 || !this.canBuild(gameState, advanced)) continue; let template = gameState.getTemplate(advanced); if (!template) continue; let civ = gameState.getPlayerCiv(); if (template.hasDefensiveFire() || template.trainableEntities(civ)) queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, advanced, { "militaryBase": true })); else // not a military building, but still use this queue queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, advanced)); return; } } }; /** * Find base nearest to ennemies for military buildings. */ PETRA.HQ.prototype.findBestBaseForMilitary = function(gameState) { let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")).toEntityArray(); let bestBase; let enemyFound = false; let distMin = Math.min(); for (let cce of ccEnts) { if (gameState.isPlayerAlly(cce.owner())) continue; if (enemyFound && !gameState.isPlayerEnemy(cce.owner())) continue; let access = PETRA.getLandAccess(gameState, cce); let isEnemy = gameState.isPlayerEnemy(cce.owner()); for (let cc of ccEnts) { if (cc.owner() != PlayerID) continue; if (PETRA.getLandAccess(gameState, cc) != access) continue; let dist = API3.SquareVectorDistance(cc.position(), cce.position()); if (!enemyFound && isEnemy) enemyFound = true; else if (dist > distMin) continue; bestBase = cc.getMetadata(PlayerID, "base"); distMin = dist; } } return bestBase; }; /** * train with highest priority ranged infantry in the nearest civil center from a given set of positions * and garrison them there for defense */ PETRA.HQ.prototype.trainEmergencyUnits = function(gameState, positions) { if (gameState.ai.queues.emergency.hasQueuedUnits()) return false; let civ = gameState.getPlayerCiv(); // find nearest base anchor let distcut = 20000; let nearestAnchor; let distmin; for (let pos of positions) { let access = gameState.ai.accessibility.getAccessValue(pos); // check nearest base anchor - for (let base of this.baseManagers) + for (const base of this.baseManagers()) { if (!base.anchor || !base.anchor.position()) continue; if (PETRA.getLandAccess(gameState, base.anchor) != access) continue; if (!base.anchor.trainableEntities(civ)) // base still in construction continue; let queue = base.anchor._entity.trainingQueue; if (queue) { let time = 0; for (let item of queue) if (item.progress > 0 || item.metadata && item.metadata.garrisonType) time += item.timeRemaining; if (time/1000 > 5) continue; } let dist = API3.SquareVectorDistance(base.anchor.position(), pos); if (nearestAnchor && dist > distmin) continue; distmin = dist; nearestAnchor = base.anchor; } } if (!nearestAnchor || distmin > distcut) return false; // We will choose randomly ranged and melee units, except when garrisonHolder is full // in which case we prefer melee units let numGarrisoned = this.garrisonManager.numberOfGarrisonedSlots(nearestAnchor); if (nearestAnchor._entity.trainingQueue) { for (let item of nearestAnchor._entity.trainingQueue) { if (item.metadata && item.metadata.garrisonType) numGarrisoned += item.count; else if (!item.progress && (!item.metadata || !item.metadata.trainer)) nearestAnchor.stopProduction(item.id); } } let autogarrison = numGarrisoned < nearestAnchor.garrisonMax() && nearestAnchor.hitpoints() > nearestAnchor.garrisonEjectHealth() * nearestAnchor.maxHitpoints(); let rangedWanted = randBool() && autogarrison; let total = gameState.getResources(); let templateFound; let trainables = nearestAnchor.trainableEntities(civ); let garrisonArrowClasses = nearestAnchor.getGarrisonArrowClasses(); for (let trainable of trainables) { if (gameState.isTemplateDisabled(trainable)) continue; let template = gameState.getTemplate(trainable); if (!template || !template.hasClasses(["Infantry+CitizenSoldier"])) continue; if (autogarrison && !template.hasClasses(garrisonArrowClasses)) continue; if (!total.canAfford(new API3.Resources(template.cost()))) continue; templateFound = [trainable, template]; if (template.hasClass("Ranged") == rangedWanted) break; } if (!templateFound) return false; // Check first if we can afford it without touching the other accounts // and if not, take some of other accounted resources // TODO sort the queues to be substracted let queueManager = gameState.ai.queueManager; let cost = new API3.Resources(templateFound[1].cost()); queueManager.setAccounts(gameState, cost, "emergency"); if (!queueManager.canAfford("emergency", cost)) { for (let q in queueManager.queues) { if (q == "emergency") continue; queueManager.transferAccounts(cost, q, "emergency"); if (queueManager.canAfford("emergency", cost)) break; } } let metadata = { "role": "worker", "base": nearestAnchor.getMetadata(PlayerID, "base"), "plan": -1, "trainer": nearestAnchor.id() }; if (autogarrison) metadata.garrisonType = "protection"; gameState.ai.queues.emergency.addPlan(new PETRA.TrainingPlan(gameState, templateFound[0], metadata, 1, 1)); return true; }; PETRA.HQ.prototype.canBuild = function(gameState, structure) { let type = gameState.applyCiv(structure); if (this.buildManager.isUnbuildable(gameState, type)) return false; if (gameState.isTemplateDisabled(type)) { this.buildManager.setUnbuildable(gameState, type, Infinity, "disabled"); return false; } let template = gameState.getTemplate(type); if (!template) { this.buildManager.setUnbuildable(gameState, type, Infinity, "notemplate"); return false; } if (!template.available(gameState)) { this.buildManager.setUnbuildable(gameState, type, 30, "tech"); return false; } if (!this.buildManager.hasBuilder(type)) { this.buildManager.setUnbuildable(gameState, type, 120, "nobuilder"); return false; } - if (this.numActiveBases() < 1) + if (!this.hasActiveBase()) { // if no base, check that we can build outside our territory let buildTerritories = template.buildTerritories(); if (buildTerritories && (!buildTerritories.length || buildTerritories.length == 1 && buildTerritories[0] == "own")) { this.buildManager.setUnbuildable(gameState, type, 180, "room"); return false; } } // build limits let limits = gameState.getEntityLimits(); let category = template.buildCategory(); if (category && limits[category] !== undefined && gameState.getEntityCounts()[category] >= limits[category]) { this.buildManager.setUnbuildable(gameState, type, 90, "limit"); return false; } return true; }; PETRA.HQ.prototype.updateTerritories = function(gameState) { const around = [ [-0.7, 0.7], [0, 1], [0.7, 0.7], [1, 0], [0.7, -0.7], [0, -1], [-0.7, -0.7], [-1, 0] ]; let alliedVictory = gameState.getAlliedVictory(); let passabilityMap = gameState.getPassabilityMap(); let width = this.territoryMap.width; let cellSize = this.territoryMap.cellSize; let insideSmall = Math.round(45 / cellSize); let insideLarge = Math.round(80 / cellSize); // should be about the range of towers let expansion = 0; for (let j = 0; j < this.territoryMap.length; ++j) { if (this.borderMap.map[j] & PETRA.outside_Mask) continue; if (this.borderMap.map[j] & PETRA.fullFrontier_Mask) this.borderMap.map[j] &= ~PETRA.fullFrontier_Mask; // reset the frontier if (this.territoryMap.getOwnerIndex(j) != PlayerID) - { - // If this tile was already accounted, remove it - if (this.basesMap.map[j] == 0) - continue; - let base = this.getBaseByID(this.basesMap.map[j]); - if (base) - { - let index = base.territoryIndices.indexOf(j); - if (index != -1) - base.territoryIndices.splice(index, 1); - else - API3.warn(" problem in headquarters::updateTerritories for base " + this.basesMap.map[j]); - } - else - API3.warn(" problem in headquarters::updateTerritories without base " + this.basesMap.map[j]); - this.basesMap.map[j] = 0; - } + this.basesManager.removeBaseFromTerritoryIndex(j); else { // Update the frontier let ix = j%width; let iz = Math.floor(j/width); let onFrontier = false; for (let a of around) { let jx = ix + Math.round(insideSmall*a[0]); if (jx < 0 || jx >= width) continue; let jz = iz + Math.round(insideSmall*a[1]); if (jz < 0 || jz >= width) continue; if (this.borderMap.map[jx+width*jz] & PETRA.outside_Mask) continue; let territoryOwner = this.territoryMap.getOwnerIndex(jx+width*jz); if (territoryOwner != PlayerID && !(alliedVictory && gameState.isPlayerAlly(territoryOwner))) { this.borderMap.map[j] |= PETRA.narrowFrontier_Mask; break; } jx = ix + Math.round(insideLarge*a[0]); if (jx < 0 || jx >= width) continue; jz = iz + Math.round(insideLarge*a[1]); if (jz < 0 || jz >= width) continue; if (this.borderMap.map[jx+width*jz] & PETRA.outside_Mask) continue; territoryOwner = this.territoryMap.getOwnerIndex(jx+width*jz); if (territoryOwner != PlayerID && !(alliedVictory && gameState.isPlayerAlly(territoryOwner))) onFrontier = true; } if (onFrontier && !(this.borderMap.map[j] & PETRA.narrowFrontier_Mask)) this.borderMap.map[j] |= PETRA.largeFrontier_Mask; - // If this tile was not already accounted, add it. - if (this.basesMap.map[j] != 0) - continue; - let landPassable = false; - let ind = API3.getMapIndices(j, this.territoryMap, passabilityMap); - let access; - for (let k of ind) - { - if (!this.landRegions[gameState.ai.accessibility.landPassMap[k]]) - continue; - landPassable = true; - access = gameState.ai.accessibility.landPassMap[k]; - break; - } - if (!landPassable) - continue; - let distmin = Math.min(); - let baseID; - let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; - for (let base of this.baseManagers) - { - if (!base.anchor || !base.anchor.position()) - continue; - if (base.accessIndex != access) - continue; - let dist = API3.SquareVectorDistance(base.anchor.position(), pos); - if (dist >= distmin) - continue; - distmin = dist; - baseID = base.ID; - } - if (!baseID) - continue; - this.getBaseByID(baseID).territoryIndices.push(j); - this.basesMap.map[j] = baseID; - expansion++; + if (this.basesManager.addTerritoryIndexToBase(gameState, j, passabilityMap)) + expansion++; } } if (!expansion) return; // We've increased our territory, so we may have some new room to build this.buildManager.resetMissingRoom(gameState); // And if sufficient expansion, check if building a new market would improve our present trade routes let cellArea = this.territoryMap.cellSize * this.territoryMap.cellSize; if (expansion * cellArea > 960) this.tradeManager.routeProspection = true; }; -/** Reassign territories when a base is going to be deleted */ -PETRA.HQ.prototype.reassignTerritories = function(deletedBase) -{ - let cellSize = this.territoryMap.cellSize; - let width = this.territoryMap.width; - for (let j = 0; j < this.territoryMap.length; ++j) - { - if (this.basesMap.map[j] != deletedBase.ID) - continue; - if (this.territoryMap.getOwnerIndex(j) != PlayerID) - { - API3.warn("Petra reassignTerritories: should never happen"); - this.basesMap.map[j] = 0; - continue; - } - - let distmin = Math.min(); - let baseID; - let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; - for (let base of this.baseManagers) - { - if (!base.anchor || !base.anchor.position()) - continue; - if (base.accessIndex != deletedBase.accessIndex) - continue; - let dist = API3.SquareVectorDistance(base.anchor.position(), pos); - if (dist >= distmin) - continue; - distmin = dist; - baseID = base.ID; - } - if (baseID) - { - this.getBaseByID(baseID).territoryIndices.push(j); - this.basesMap.map[j] = baseID; - } - else - this.basesMap.map[j] = 0; - } -}; - /** * returns the base corresponding to baseID */ PETRA.HQ.prototype.getBaseByID = function(baseID) { - for (let base of this.baseManagers) - if (base.ID == baseID) - return base; - - return undefined; + return this.basesManager.getBaseByID(baseID); }; /** * returns the number of bases with a cc * ActiveBases includes only those with a built cc * PotentialBases includes also those with a cc in construction */ PETRA.HQ.prototype.numActiveBases = function() { - if (!this.turnCache.base) - this.updateBaseCache(); - return this.turnCache.base.active; + return this.basesManager.numActiveBases(); }; -PETRA.HQ.prototype.numPotentialBases = function() +PETRA.HQ.prototype.hasActiveBase = function() { - if (!this.turnCache.base) - this.updateBaseCache(); - return this.turnCache.base.potential; -}; - -PETRA.HQ.prototype.updateBaseCache = function() -{ - this.turnCache.base = { "active": 0, "potential": 0 }; - for (let base of this.baseManagers) - { - if (!base.anchor) - continue; - ++this.turnCache.base.potential; - if (base.anchor.foundationProgress() === undefined) - ++this.turnCache.base.active; - } + return this.basesManager.hasActiveBase(); }; -PETRA.HQ.prototype.resetBaseCache = function() +PETRA.HQ.prototype.numPotentialBases = function() { - this.turnCache.base = undefined; + return this.basesManager.numPotentialBases(); }; -/** - * Count gatherers returning resources in the number of gatherers of resourceSupplies - * to prevent the AI always reassigning idle workers to these resourceSupplies (specially in naval maps). - */ -PETRA.HQ.prototype.assignGatherers = function() +PETRA.HQ.prototype.hasPotentialBase = function() { - for (let base of this.baseManagers) - { - for (let worker of base.workers.values()) - { - if (worker.unitAIState().split(".").indexOf("RETURNRESOURCE") === -1) - continue; - let orders = worker.unitAIOrderData(); - if (orders.length < 2 || !orders[1].target || orders[1].target != worker.getMetadata(PlayerID, "supply")) - continue; - this.AddTCGatherer(orders[1].target); - } - } + return this.basesManager.hasPotentialBase(); }; PETRA.HQ.prototype.isDangerousLocation = function(gameState, pos, radius) { return this.isNearInvadingArmy(pos) || this.isUnderEnemyFire(gameState, pos, radius); }; /** Check that the chosen position is not too near from an invading army */ PETRA.HQ.prototype.isNearInvadingArmy = function(pos) { for (let army of this.defenseManager.armies) if (army.foePosition && API3.SquareVectorDistance(army.foePosition, pos) < 12000) return true; return false; }; PETRA.HQ.prototype.isUnderEnemyFire = function(gameState, pos, radius = 0) { if (!this.turnCache.firingStructures) this.turnCache.firingStructures = gameState.updatingCollection("diplo-FiringStructures", API3.Filters.hasDefensiveFire(), gameState.getEnemyStructures()); for (let ent of this.turnCache.firingStructures.values()) { let range = radius + ent.attackRange("Ranged").max; if (API3.SquareVectorDistance(ent.position(), pos) < range*range) return true; } return false; }; /** Compute the capture strength of all units attacking a capturable target */ PETRA.HQ.prototype.updateCaptureStrength = function(gameState) { this.capturableTargets.clear(); for (let ent of gameState.getOwnUnits().values()) { if (!ent.canCapture()) continue; let state = ent.unitAIState(); if (!state || !state.split(".")[1] || state.split(".")[1] != "COMBAT") continue; let orderData = ent.unitAIOrderData(); if (!orderData || !orderData.length || !orderData[0].target) continue; let targetId = orderData[0].target; let target = gameState.getEntityById(targetId); if (!target || !target.isCapturable() || !ent.canCapture(target)) continue; if (!this.capturableTargets.has(targetId)) this.capturableTargets.set(targetId, { "strength": ent.captureStrength() * PETRA.getAttackBonus(ent, target, "Capture"), "ents": new Set([ent.id()]) }); else { let capturableTarget = this.capturableTargets.get(target.id()); capturableTarget.strength += ent.captureStrength() * PETRA.getAttackBonus(ent, target, "Capture"); capturableTarget.ents.add(ent.id()); } } for (let [targetId, capturableTarget] of this.capturableTargets) { let target = gameState.getEntityById(targetId); let allowCapture; for (let entId of capturableTarget.ents) { let ent = gameState.getEntityById(entId); if (allowCapture === undefined) allowCapture = PETRA.allowCapture(gameState, ent, target); let orderData = ent.unitAIOrderData(); if (!orderData || !orderData.length || !orderData[0].attackType) continue; if ((orderData[0].attackType == "Capture") !== allowCapture) ent.attack(targetId, allowCapture); } } this.capturableTargetsTime = gameState.ai.elapsedTime; }; -/** Some functions that register that we assigned a gatherer to a resource this turn */ - -/** add a gatherer to the turn cache for this supply. */ -PETRA.HQ.prototype.AddTCGatherer = function(supplyID) -{ - if (this.turnCache.resourceGatherer && this.turnCache.resourceGatherer[supplyID] !== undefined) - ++this.turnCache.resourceGatherer[supplyID]; - else - { - if (!this.turnCache.resourceGatherer) - this.turnCache.resourceGatherer = {}; - this.turnCache.resourceGatherer[supplyID] = 1; - } -}; - -/** remove a gatherer to the turn cache for this supply. */ -PETRA.HQ.prototype.RemoveTCGatherer = function(supplyID) -{ - if (this.turnCache.resourceGatherer && this.turnCache.resourceGatherer[supplyID]) - --this.turnCache.resourceGatherer[supplyID]; - else - { - if (!this.turnCache.resourceGatherer) - this.turnCache.resourceGatherer = {}; - this.turnCache.resourceGatherer[supplyID] = -1; - } -}; - -PETRA.HQ.prototype.GetTCGatherer = function(supplyID) -{ - if (this.turnCache.resourceGatherer && this.turnCache.resourceGatherer[supplyID]) - return this.turnCache.resourceGatherer[supplyID]; - - return 0; -}; - -/** The next two are to register that we assigned a gatherer to a resource this turn. */ -PETRA.HQ.prototype.AddTCResGatherer = function(resource) -{ - if (this.turnCache["resourceGatherer-" + resource]) - ++this.turnCache["resourceGatherer-" + resource]; - else - this.turnCache["resourceGatherer-" + resource] = 1; - - if (this.turnCache.currentRates) - this.turnCache.currentRates[resource] += 0.5; -}; - -PETRA.HQ.prototype.GetTCResGatherer = function(resource) -{ - if (this.turnCache["resourceGatherer-" + resource]) - return this.turnCache["resourceGatherer-" + resource]; - - return 0; -}; - -/** - * flag a resource as exhausted - */ -PETRA.HQ.prototype.isResourceExhausted = function(resource) -{ - if (this.turnCache["exhausted-" + resource] == undefined) - this.turnCache["exhausted-" + resource] = this.baseManagers.every(base => - !base.dropsiteSupplies[resource].nearby.length && - !base.dropsiteSupplies[resource].medium.length && - !base.dropsiteSupplies[resource].faraway.length); - - return this.turnCache["exhausted-" + resource]; -}; - /** * Check if a structure in blinking territory should/can be defended (currently if it has some attacking armies around) */ PETRA.HQ.prototype.isDefendable = function(ent) { if (!this.turnCache.numAround) this.turnCache.numAround = {}; if (this.turnCache.numAround[ent.id()] === undefined) this.turnCache.numAround[ent.id()] = this.attackManager.numAttackingUnitsAround(ent.position(), 130); return +this.turnCache.numAround[ent.id()] > 8; }; /** * Get the number of population already accounted for */ PETRA.HQ.prototype.getAccountedPopulation = function(gameState) { if (this.turnCache.accountedPopulation == undefined) { let pop = gameState.getPopulation(); for (let ent of gameState.getOwnTrainingFacilities().values()) { for (let item of ent.trainingQueue()) { if (!item.unitTemplate) continue; let unitPop = gameState.getTemplate(item.unitTemplate).get("Cost/Population"); if (unitPop) pop += item.count * unitPop; } } this.turnCache.accountedPopulation = pop; } return this.turnCache.accountedPopulation; }; /** * Get the number of workers already accounted for */ PETRA.HQ.prototype.getAccountedWorkers = function(gameState) { if (this.turnCache.accountedWorkers == undefined) { let workers = gameState.getOwnEntitiesByRole("worker", true).length; for (let ent of gameState.getOwnTrainingFacilities().values()) { for (let item of ent.trainingQueue()) { if (!item.metadata || !item.metadata.role || item.metadata.role != "worker") continue; workers += item.count; } } this.turnCache.accountedWorkers = workers; } return this.turnCache.accountedWorkers; }; +PETRA.HQ.prototype.baseManagers = function() +{ + return this.basesManager.baseManagers; +}; + +/** + * @param {number} territoryIndex - The index to get the map for. + * @return {number} - The ID of the base at the given territory index. + */ +PETRA.HQ.prototype.baseAtIndex = function(territoryIndex) +{ + return this.basesManager.baseAtIndex(territoryIndex); +}; + /** * Some functions are run every turn * Others once in a while */ PETRA.HQ.prototype.update = function(gameState, queues, events) { Engine.ProfileStart("Headquarters update"); this.turnCache = {}; this.territoryMap = PETRA.createTerritoryMap(gameState); this.canBarter = gameState.getOwnEntitiesByClass("Market", true).filter(API3.Filters.isBuilt()).hasEntities(); // TODO find a better way to update if (this.currentPhase != gameState.currentPhase()) { if (this.Config.debug > 0) API3.warn(" civ " + gameState.getPlayerCiv() + " has phasedUp from " + this.currentPhase + " to " + gameState.currentPhase() + " at time " + gameState.ai.elapsedTime + " phasing " + this.phasing); this.currentPhase = gameState.currentPhase(); // In principle, this.phasing should be already reset to 0 when starting the research // but this does not work in case of an autoResearch tech if (this.phasing) this.phasing = 0; } /* if (this.Config.debug > 1) { gameState.getOwnUnits().forEach (function (ent) { if (!ent.position()) return; PETRA.dumpEntity(ent); }); } */ this.checkEvents(gameState, events); this.navalManager.checkEvents(gameState, queues, events); if (this.phasing) this.checkPhaseRequirements(gameState, queues); else this.researchManager.checkPhase(gameState, queues); - if (this.numActiveBases() > 0) + if (this.hasActiveBase()) { if (gameState.ai.playedTurn % 4 == 0) this.trainMoreWorkers(gameState, queues); if (gameState.ai.playedTurn % 4 == 1) this.buildMoreHouses(gameState, queues); if ((!this.saveResources || this.canBarter) && gameState.ai.playedTurn % 4 == 2) this.buildFarmstead(gameState, queues); if (this.needCorral && gameState.ai.playedTurn % 4 == 3) this.manageCorral(gameState, queues); if (gameState.ai.playedTurn % 5 == 1) this.researchManager.update(gameState, queues); } - if (this.numPotentialBases() < 1 || + if (!this.hasPotentialBase() || this.canExpand && gameState.ai.playedTurn % 10 == 7 && this.currentPhase > 1) this.checkBaseExpansion(gameState, queues); if (this.currentPhase > 1 && gameState.ai.playedTurn % 3 == 0) { if (!this.canBarter) this.buildMarket(gameState, queues); if (!this.saveResources) { this.buildForge(gameState, queues); this.buildTemple(gameState, queues); } if (gameState.ai.playedTurn % 30 == 0 && gameState.getPopulation() > 0.9 * gameState.getPopulationMax()) this.buildWonder(gameState, queues, false); } this.tradeManager.update(gameState, events, queues); this.garrisonManager.update(gameState, events); this.defenseManager.update(gameState, events); if (gameState.ai.playedTurn % 3 == 0) { this.constructTrainingBuildings(gameState, queues); if (this.Config.difficulty > 0) this.buildDefenses(gameState, queues); } - this.assignGatherers(); - let nbBases = this.baseManagers.length; - let activeBase; // We will loop only on 1 active base per turn - do - { - this.currentBase %= this.baseManagers.length; - activeBase = this.baseManagers[this.currentBase++].update(gameState, queues, events); - --nbBases; - // TODO what to do with this.reassignTerritories(this.baseManagers[this.currentBase]); - } - while (!activeBase && nbBases != 0); + this.basesManager.update(gameState, queues, events); this.navalManager.update(gameState, queues, events); - if (this.Config.difficulty > 0 && (this.numActiveBases() > 0 || !this.canBuildUnits)) + if (this.Config.difficulty > 0 && (this.hasActiveBase() || !this.canBuildUnits)) this.attackManager.update(gameState, queues, events); this.diplomacyManager.update(gameState, events); this.victoryManager.update(gameState, events, queues); // We update the capture strength at the end as it can change attack orders if (gameState.ai.elapsedTime - this.capturableTargetsTime > 3) this.updateCaptureStrength(gameState); Engine.ProfileStop(); }; PETRA.HQ.prototype.Serialize = function() { let properties = { "phasing": this.phasing, - "currentBase": this.currentBase, "lastFailedGather": this.lastFailedGather, "firstBaseConfig": this.firstBaseConfig, "supportRatio": this.supportRatio, "targetNumWorkers": this.targetNumWorkers, "fortStartTime": this.fortStartTime, "towerStartTime": this.towerStartTime, "fortressStartTime": this.fortressStartTime, "bAdvanced": this.bAdvanced, "saveResources": this.saveResources, "saveSpace": this.saveSpace, "needCorral": this.needCorral, "needFarm": this.needFarm, "needFish": this.needFish, "maxFields": this.maxFields, "canExpand": this.canExpand, "canBuildUnits": this.canBuildUnits, "navalMap": this.navalMap, "landRegions": this.landRegions, "navalRegions": this.navalRegions, "decayingStructures": this.decayingStructures, "capturableTargets": this.capturableTargets, "capturableTargetsTime": this.capturableTargetsTime }; - let baseManagers = []; - for (let base of this.baseManagers) - baseManagers.push(base.Serialize()); - if (this.Config.debug == -100) { API3.warn(" HQ serialization ---------------------"); API3.warn(" properties " + uneval(properties)); - API3.warn(" baseManagers " + uneval(baseManagers)); + API3.warn(" baseManagers " + uneval(this.basesManager.Serialize())); API3.warn(" attackManager " + uneval(this.attackManager.Serialize())); API3.warn(" buildManager " + uneval(this.buildManager.Serialize())); API3.warn(" defenseManager " + uneval(this.defenseManager.Serialize())); API3.warn(" tradeManager " + uneval(this.tradeManager.Serialize())); API3.warn(" navalManager " + uneval(this.navalManager.Serialize())); API3.warn(" researchManager " + uneval(this.researchManager.Serialize())); API3.warn(" diplomacyManager " + uneval(this.diplomacyManager.Serialize())); API3.warn(" garrisonManager " + uneval(this.garrisonManager.Serialize())); API3.warn(" victoryManager " + uneval(this.victoryManager.Serialize())); } return { "properties": properties, - "baseManagers": baseManagers, + "basesManager": this.basesManager.Serialize(), "attackManager": this.attackManager.Serialize(), "buildManager": this.buildManager.Serialize(), "defenseManager": this.defenseManager.Serialize(), "tradeManager": this.tradeManager.Serialize(), "navalManager": this.navalManager.Serialize(), "researchManager": this.researchManager.Serialize(), "diplomacyManager": this.diplomacyManager.Serialize(), "garrisonManager": this.garrisonManager.Serialize(), "victoryManager": this.victoryManager.Serialize(), }; }; PETRA.HQ.prototype.Deserialize = function(gameState, data) { for (let key in data.properties) this[key] = data.properties[key]; - this.baseManagers = []; - for (let base of data.baseManagers) - { - // the first call to deserialize set the ID base needed by entitycollections - let newbase = new PETRA.BaseManager(gameState, this.Config); - newbase.Deserialize(gameState, base); - newbase.init(gameState); - newbase.Deserialize(gameState, base); - this.baseManagers.push(newbase); - } + + this.basesManager = new PETRA.BasesManager(this.Config); + this.basesManager.init(gameState); + this.basesManager.Deserialize(gameState, data.basesManager); this.navalManager = new PETRA.NavalManager(this.Config); this.navalManager.init(gameState, true); this.navalManager.Deserialize(gameState, data.navalManager); this.attackManager = new PETRA.AttackManager(this.Config); this.attackManager.Deserialize(gameState, data.attackManager); this.attackManager.init(gameState); this.attackManager.Deserialize(gameState, data.attackManager); this.buildManager = new PETRA.BuildManager(); this.buildManager.Deserialize(data.buildManager); this.defenseManager = new PETRA.DefenseManager(this.Config); this.defenseManager.Deserialize(gameState, data.defenseManager); this.tradeManager = new PETRA.TradeManager(this.Config); this.tradeManager.init(gameState); this.tradeManager.Deserialize(gameState, data.tradeManager); this.researchManager = new PETRA.ResearchManager(this.Config); this.researchManager.Deserialize(data.researchManager); this.diplomacyManager = new PETRA.DiplomacyManager(this.Config); this.diplomacyManager.Deserialize(data.diplomacyManager); this.garrisonManager = new PETRA.GarrisonManager(this.Config); this.garrisonManager.Deserialize(data.garrisonManager); this.victoryManager = new PETRA.VictoryManager(this.Config); this.victoryManager.Deserialize(data.victoryManager); }; Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/navalManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/navalManager.js (revision 25875) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/navalManager.js (revision 25876) @@ -1,888 +1,888 @@ /** * Naval Manager * Will deal with anything ships. * -Basically trade over water (with fleets and goals commissioned by the economy manager) * -Defense over water (commissioned by the defense manager) * -Transport of units over water (a few units). * -Scouting, ultimately. * Also deals with handling docks, making sure we have access and stuffs like that. */ PETRA.NavalManager = function(Config) { this.Config = Config; // ship subCollections. Also exist for land zones, idem, not caring. this.seaShips = []; this.seaTransportShips = []; this.seaWarShips = []; this.seaFishShips = []; // wanted NB per zone. this.wantedTransportShips = []; this.wantedWarShips = []; this.wantedFishShips = []; // needed NB per zone. this.neededTransportShips = []; this.neededWarShips = []; this.transportPlans = []; // shore-line regions where we can load and unload units this.landingZones = {}; }; /** More initialisation for stuff that needs the gameState */ PETRA.NavalManager.prototype.init = function(gameState, deserializing) { // docks this.docks = gameState.getOwnStructures().filter(API3.Filters.byClasses(["Dock", "Shipyard"])); this.docks.registerUpdates(); this.ships = gameState.getOwnUnits().filter(API3.Filters.and(API3.Filters.byClass("Ship"), API3.Filters.not(API3.Filters.byMetadata(PlayerID, "role", "trader")))); // note: those two can overlap (some transport ships are warships too and vice-versa). this.transportShips = this.ships.filter(API3.Filters.and(API3.Filters.byCanGarrison(), API3.Filters.not(API3.Filters.byClass("FishingBoat")))); this.warShips = this.ships.filter(API3.Filters.byClass("Warship")); this.fishShips = this.ships.filter(API3.Filters.byClass("FishingBoat")); this.ships.registerUpdates(); this.transportShips.registerUpdates(); this.warShips.registerUpdates(); this.fishShips.registerUpdates(); let availableFishes = {}; for (let fish of gameState.getFishableSupplies().values()) { let sea = this.getFishSea(gameState, fish); if (sea && availableFishes[sea]) availableFishes[sea] += fish.resourceSupplyAmount(); else if (sea) availableFishes[sea] = fish.resourceSupplyAmount(); } for (let i = 0; i < gameState.ai.accessibility.regionSize.length; ++i) { if (!gameState.ai.HQ.navalRegions[i]) { // push dummies this.seaShips.push(undefined); this.seaTransportShips.push(undefined); this.seaWarShips.push(undefined); this.seaFishShips.push(undefined); this.wantedTransportShips.push(0); this.wantedWarShips.push(0); this.wantedFishShips.push(0); this.neededTransportShips.push(0); this.neededWarShips.push(0); } else { let collec = this.ships.filter(API3.Filters.byMetadata(PlayerID, "sea", i)); collec.registerUpdates(); this.seaShips.push(collec); collec = this.transportShips.filter(API3.Filters.byMetadata(PlayerID, "sea", i)); collec.registerUpdates(); this.seaTransportShips.push(collec); collec = this.warShips.filter(API3.Filters.byMetadata(PlayerID, "sea", i)); collec.registerUpdates(); this.seaWarShips.push(collec); collec = this.fishShips.filter(API3.Filters.byMetadata(PlayerID, "sea", i)); collec.registerUpdates(); this.seaFishShips.push(collec); this.wantedTransportShips.push(0); this.wantedWarShips.push(0); if (availableFishes[i] && availableFishes[i] > 1000) this.wantedFishShips.push(this.Config.Economy.targetNumFishers); else this.wantedFishShips.push(0); this.neededTransportShips.push(0); this.neededWarShips.push(0); } } if (deserializing) return; // determination of the possible landing zones let width = gameState.getPassabilityMap().width; let length = width * gameState.getPassabilityMap().height; for (let i = 0; i < length; ++i) { let land = gameState.ai.accessibility.landPassMap[i]; if (land < 2) continue; let naval = gameState.ai.accessibility.navalPassMap[i]; if (naval < 2) continue; if (!this.landingZones[land]) this.landingZones[land] = {}; if (!this.landingZones[land][naval]) this.landingZones[land][naval] = new Set(); this.landingZones[land][naval].add(i); } // and keep only thoses with enough room around when possible for (let land in this.landingZones) { for (let sea in this.landingZones[land]) { let landing = this.landingZones[land][sea]; let nbaround = {}; let nbcut = 0; for (let i of landing) { let nb = 0; if (landing.has(i-1)) nb++; if (landing.has(i+1)) nb++; if (landing.has(i+width)) nb++; if (landing.has(i-width)) nb++; nbaround[i] = nb; nbcut = Math.max(nb, nbcut); } nbcut = Math.min(2, nbcut); for (let i of landing) { if (nbaround[i] < nbcut) landing.delete(i); } } } // Assign our initial docks and ships for (let ship of this.ships.values()) PETRA.setSeaAccess(gameState, ship); for (let dock of this.docks.values()) PETRA.setSeaAccess(gameState, dock); }; PETRA.NavalManager.prototype.updateFishingBoats = function(sea, num) { if (this.wantedFishShips[sea]) this.wantedFishShips[sea] = num; }; PETRA.NavalManager.prototype.resetFishingBoats = function(gameState, sea) { if (sea !== undefined) this.wantedFishShips[sea] = 0; else this.wantedFishShips.fill(0); }; /** Get the sea, cache it if not yet done and check if in opensea */ PETRA.NavalManager.prototype.getFishSea = function(gameState, fish) { let sea = fish.getMetadata(PlayerID, "sea"); if (sea) return sea; const ntry = 4; const around = [[-0.7, 0.7], [0, 1], [0.7, 0.7], [1, 0], [0.7, -0.7], [0, -1], [-0.7, -0.7], [-1, 0]]; let pos = gameState.ai.accessibility.gamePosToMapPos(fish.position()); let width = gameState.ai.accessibility.width; let k = pos[0] + pos[1]*width; sea = gameState.ai.accessibility.navalPassMap[k]; fish.setMetadata(PlayerID, "sea", sea); let radius = 120 / gameState.ai.accessibility.cellSize / ntry; if (around.every(a => { for (let t = 0; t < ntry; ++t) { let i = pos[0] + Math.round(a[0]*radius*(ntry-t)); let j = pos[1] + Math.round(a[1]*radius*(ntry-t)); if (i < 0 || i >= width || j < 0 || j >= width) continue; if (gameState.ai.accessibility.landPassMap[i + j*width] === 1) { let navalPass = gameState.ai.accessibility.navalPassMap[i + j*width]; if (navalPass == sea) return true; else if (navalPass == 1) // we could be outside the map continue; } return false; } return true; })) fish.setMetadata(PlayerID, "opensea", true); return sea; }; /** check if we can safely fish at the fish position */ PETRA.NavalManager.prototype.canFishSafely = function(gameState, fish) { if (fish.getMetadata(PlayerID, "opensea")) return true; const ntry = 2; const around = [[-0.7, 0.7], [0, 1], [0.7, 0.7], [1, 0], [0.7, -0.7], [0, -1], [-0.7, -0.7], [-1, 0]]; let territoryMap = gameState.ai.HQ.territoryMap; let width = territoryMap.width; let radius = 120 / territoryMap.cellSize / ntry; let pos = territoryMap.gamePosToMapPos(fish.position()); return around.every(a => { for (let t = 0; t < ntry; ++t) { let i = pos[0] + Math.round(a[0]*radius*(ntry-t)); let j = pos[1] + Math.round(a[1]*radius*(ntry-t)); if (i < 0 || i >= width || j < 0 || j >= width) continue; let owner = territoryMap.getOwnerIndex(i + j*width); if (owner != 0 && gameState.isPlayerEnemy(owner)) return false; } return true; }); }; /** get the list of seas (or lands) around this region not connected by a dock */ PETRA.NavalManager.prototype.getUnconnectedSeas = function(gameState, region) { let seas = gameState.ai.accessibility.regionLinks[region].slice(); this.docks.forEach(dock => { if (!dock.hasClass("Dock") || PETRA.getLandAccess(gameState, dock) != region) return; let i = seas.indexOf(PETRA.getSeaAccess(gameState, dock)); if (i != -1) seas.splice(i--, 1); }); return seas; }; PETRA.NavalManager.prototype.checkEvents = function(gameState, queues, events) { for (let evt of events.Create) { if (!evt.entity) continue; let ent = gameState.getEntityById(evt.entity); if (ent && ent.isOwn(PlayerID) && ent.foundationProgress() !== undefined && ent.hasClasses(["Dock", "Shipyard"])) PETRA.setSeaAccess(gameState, ent); } for (let evt of events.TrainingFinished) { if (!evt.entities) continue; for (let entId of evt.entities) { let ent = gameState.getEntityById(entId); if (!ent || !ent.hasClass("Ship") || !ent.isOwn(PlayerID)) continue; PETRA.setSeaAccess(gameState, ent); } } for (let evt of events.Destroy) { if (!evt.entityObj || evt.entityObj.owner() !== PlayerID || !evt.metadata || !evt.metadata[PlayerID]) continue; if (!evt.entityObj.hasClass("Ship") || !evt.metadata[PlayerID].transporter) continue; let plan = this.getPlan(evt.metadata[PlayerID].transporter); if (!plan) continue; let shipId = evt.entityObj.id(); if (this.Config.debug > 1) API3.warn("one ship " + shipId + " from plan " + plan.ID + " destroyed during " + plan.state); if (plan.state == "boarding") { // just reset the units onBoard metadata and wait for a new ship to be assigned to this plan plan.units.forEach(ent => { if (ent.getMetadata(PlayerID, "onBoard") == "onBoard" && ent.position() || ent.getMetadata(PlayerID, "onBoard") == shipId) ent.setMetadata(PlayerID, "onBoard", undefined); }); plan.needTransportShips = !plan.transportShips.hasEntities(); } else if (plan.state == "sailing") { let endIndex = plan.endIndex; for (let ent of plan.units.values()) { if (!ent.position()) // unit from another ship of this plan ... do nothing continue; let access = PETRA.getLandAccess(gameState, ent); let endPos = ent.getMetadata(PlayerID, "endPos"); ent.setMetadata(PlayerID, "transport", undefined); ent.setMetadata(PlayerID, "onBoard", undefined); ent.setMetadata(PlayerID, "endPos", undefined); // nothing else to do if access = endIndex as already at destination // otherwise, we should require another transport // TODO if attacking and no more ships available, remove the units from the attack // to avoid delaying it too much if (access != endIndex) this.requireTransport(gameState, ent, access, endIndex, endPos); } } } for (let evt of events.OwnershipChanged) // capture events { if (evt.to !== PlayerID) continue; let ent = gameState.getEntityById(evt.entity); if (ent && ent.hasClasses(["Dock", "Shipyard"])) PETRA.setSeaAccess(gameState, ent); } }; PETRA.NavalManager.prototype.getPlan = function(ID) { for (let plan of this.transportPlans) if (plan.ID === ID) return plan; return undefined; }; PETRA.NavalManager.prototype.addPlan = function(plan) { this.transportPlans.push(plan); }; /** * complete already existing plan or create a new one for this requirement * (many units can then call this separately and end up in the same plan) * TODO check garrison classes */ PETRA.NavalManager.prototype.requireTransport = function(gameState, ent, startIndex, endIndex, endPos) { if (!ent.canGarrison()) return false; if (ent.getMetadata(PlayerID, "transport") !== undefined) { if (this.Config.debug > 0) API3.warn("Petra naval manager error: unit " + ent.id() + " has already required a transport"); return false; } let plans = []; for (let plan of this.transportPlans) { if (plan.startIndex != startIndex || plan.endIndex != endIndex || plan.state != "boarding") continue; // Limit the number of siege units per transport to avoid problems when ungarrisoning if (PETRA.isSiegeUnit(ent) && plan.units.filter(unit => PETRA.isSiegeUnit(unit)).length > 3) continue; plans.push(plan); } if (plans.length) { plans.sort(plan => plan.units.length); plans[0].addUnit(ent, endPos); return true; } let plan = new PETRA.TransportPlan(gameState, [ent], startIndex, endIndex, endPos); if (plan.failed) { if (this.Config.debug > 1) API3.warn(">>>> transport plan aborted <<<<"); return false; } plan.init(gameState); this.transportPlans.push(plan); return true; }; /** split a transport plan in two, moving all entities not yet affected to a ship in the new plan */ PETRA.NavalManager.prototype.splitTransport = function(gameState, plan) { if (this.Config.debug > 1) API3.warn(">>>> split of transport plan started <<<<"); let newplan = new PETRA.TransportPlan(gameState, [], plan.startIndex, plan.endIndex, plan.endPos); if (newplan.failed) { if (this.Config.debug > 1) API3.warn(">>>> split of transport plan aborted <<<<"); return false; } newplan.init(gameState); for (let ent of plan.needSplit) { if (ent.getMetadata(PlayerID, "onBoard")) // Should never happen. continue; newplan.addUnit(ent, ent.getMetadata(PlayerID, "endPos")); plan.units.updateEnt(ent); } if (newplan.units.length) this.transportPlans.push(newplan); return newplan.units.length != 0; }; /** * create a transport from a garrisoned ship to a land location * needed at start game when starting with a garrisoned ship */ PETRA.NavalManager.prototype.createTransportIfNeeded = function(gameState, fromPos, toPos, toAccess) { let fromAccess = gameState.ai.accessibility.getAccessValue(fromPos); if (fromAccess !== 1) return; if (toAccess < 2) return; for (let ship of this.ships.values()) { if (!ship.isGarrisonHolder() || !ship.garrisoned().length) continue; if (ship.getMetadata(PlayerID, "transporter") !== undefined) continue; let units = []; for (let entId of ship.garrisoned()) units.push(gameState.getEntityById(entId)); // TODO check that the garrisoned units have not another purpose let plan = new PETRA.TransportPlan(gameState, units, fromAccess, toAccess, toPos, ship); if (plan.failed) continue; plan.init(gameState); this.transportPlans.push(plan); } }; // set minimal number of needed ships when a new event (new base or new attack plan) PETRA.NavalManager.prototype.setMinimalTransportShips = function(gameState, sea, number) { if (!sea) return; if (this.wantedTransportShips[sea] < number) this.wantedTransportShips[sea] = number; }; // bumps up the number of ships we want if we need more. PETRA.NavalManager.prototype.checkLevels = function(gameState, queues) { if (queues.ships.hasQueuedUnits()) return; for (let sea = 0; sea < this.neededTransportShips.length; sea++) this.neededTransportShips[sea] = 0; for (let plan of this.transportPlans) { if (!plan.needTransportShips || plan.units.length < 2) continue; let sea = plan.sea; if (gameState.countOwnQueuedEntitiesWithMetadata("sea", sea) > 0 || this.seaTransportShips[sea].length < this.wantedTransportShips[sea]) continue; ++this.neededTransportShips[sea]; if (this.wantedTransportShips[sea] === 0 || this.seaTransportShips[sea].length < plan.transportShips.length + 2) { ++this.wantedTransportShips[sea]; return; } } for (let sea = 0; sea < this.neededTransportShips.length; sea++) if (this.neededTransportShips[sea] > 2) ++this.wantedTransportShips[sea]; }; PETRA.NavalManager.prototype.maintainFleet = function(gameState, queues) { if (queues.ships.hasQueuedUnits()) return; if (!this.docks.filter(API3.Filters.isBuilt()).hasEntities()) return; // check if we have enough transport ships per region. for (let sea = 0; sea < this.seaShips.length; ++sea) { if (this.seaShips[sea] === undefined) continue; if (gameState.countOwnQueuedEntitiesWithMetadata("sea", sea) > 0) continue; if (this.seaTransportShips[sea].length < this.wantedTransportShips[sea]) { let template = this.getBestShip(gameState, sea, "transport"); if (template) { queues.ships.addPlan(new PETRA.TrainingPlan(gameState, template, { "sea": sea }, 1, 1)); continue; } } if (this.seaFishShips[sea].length < this.wantedFishShips[sea]) { let template = this.getBestShip(gameState, sea, "fishing"); if (template) { queues.ships.addPlan(new PETRA.TrainingPlan(gameState, template, { "base": 0, "role": "worker", "sea": sea }, 1, 1)); continue; } } } }; /** assigns free ships to plans that need some */ PETRA.NavalManager.prototype.assignShipsToPlans = function(gameState) { for (let plan of this.transportPlans) if (plan.needTransportShips) plan.assignShip(gameState); }; /** Return true if this ship is likeky (un)garrisoning units */ PETRA.NavalManager.prototype.isShipBoarding = function(ship) { if (!ship.position()) return false; let plan = this.getPlan(ship.getMetadata(PlayerID, "transporter")); if (!plan || !plan.boardingPos[ship.id()]) return false; return API3.SquareVectorDistance(plan.boardingPos[ship.id()], ship.position()) < plan.boardingRange; }; /** let blocking ships move apart from active ships (waiting for a better pathfinder) * TODO Ships entity collections are currently in two parts as the trader ships are dealt with * in the tradeManager. That should be modified to avoid dupplicating all the code here. */ PETRA.NavalManager.prototype.moveApart = function(gameState) { let blockedShips = []; let blockedIds = []; for (let ship of this.ships.values()) { let shipPosition = ship.position(); if (!shipPosition) continue; if (ship.getMetadata(PlayerID, "transporter") !== undefined && this.isShipBoarding(ship)) continue; let unitAIState = ship.unitAIState(); if (ship.getMetadata(PlayerID, "transporter") !== undefined || unitAIState == "INDIVIDUAL.GATHER.APPROACHING" || unitAIState == "INDIVIDUAL.GATHER.RETURNINGRESOURCE.APPROACHING") { let previousPosition = ship.getMetadata(PlayerID, "previousPosition"); if (!previousPosition || previousPosition[0] != shipPosition[0] || previousPosition[1] != shipPosition[1]) { ship.setMetadata(PlayerID, "previousPosition", shipPosition); ship.setMetadata(PlayerID, "turnPreviousPosition", gameState.ai.playedTurn); continue; } // New transport ships receive boarding commands only on the following turn. if (gameState.ai.playedTurn < ship.getMetadata(PlayerID, "turnPreviousPosition") + 2) continue; ship.moveToRange(shipPosition[0] + randFloat(-1, 1), shipPosition[1] + randFloat(-1, 1), 30, 35); blockedShips.push(ship); blockedIds.push(ship.id()); } else if (ship.isIdle()) { let previousIdlePosition = ship.getMetadata(PlayerID, "previousIdlePosition"); if (!previousIdlePosition || previousIdlePosition[0] != shipPosition[0] || previousIdlePosition[1] != shipPosition[1]) { ship.setMetadata(PlayerID, "previousIdlePosition", shipPosition); ship.setMetadata(PlayerID, "stationnary", undefined); continue; } if (ship.getMetadata(PlayerID, "stationnary")) continue; ship.setMetadata(PlayerID, "stationnary", true); // Check if there are some treasure around if (PETRA.gatherTreasure(gameState, ship, true)) continue; // Do not stay idle near a dock to not disturb other ships let sea = ship.getMetadata(PlayerID, "sea"); for (let dock of gameState.getAllyStructures().filter(API3.Filters.byClass("Dock")).values()) { if (PETRA.getSeaAccess(gameState, dock) != sea) continue; if (API3.SquareVectorDistance(shipPosition, dock.position()) > 4900) continue; ship.moveToRange(dock.position()[0], dock.position()[1], 70, 75); } } } for (let ship of gameState.ai.HQ.tradeManager.traders.filter(API3.Filters.byClass("Ship")).values()) { let shipPosition = ship.position(); if (!shipPosition) continue; let role = ship.getMetadata(PlayerID, "role"); if (!role || role != "trader") // already accounted before continue; let unitAIState = ship.unitAIState(); if (unitAIState == "INDIVIDUAL.TRADE.APPROACHINGMARKET") { let previousPosition = ship.getMetadata(PlayerID, "previousPosition"); if (!previousPosition || previousPosition[0] != shipPosition[0] || previousPosition[1] != shipPosition[1]) { ship.setMetadata(PlayerID, "previousPosition", shipPosition); ship.setMetadata(PlayerID, "turnPreviousPosition", gameState.ai.playedTurn); continue; } // New transport ships receives boarding commands only on the following turn. if (gameState.ai.playedTurn < ship.getMetadata(PlayerID, "turnPreviousPosition") + 2) continue; ship.moveToRange(shipPosition[0] + randFloat(-1, 1), shipPosition[1] + randFloat(-1, 1), 30, 35); blockedShips.push(ship); blockedIds.push(ship.id()); } else if (ship.isIdle()) { let previousIdlePosition = ship.getMetadata(PlayerID, "previousIdlePosition"); if (!previousIdlePosition || previousIdlePosition[0] != shipPosition[0] || previousIdlePosition[1] != shipPosition[1]) { ship.setMetadata(PlayerID, "previousIdlePosition", shipPosition); ship.setMetadata(PlayerID, "stationnary", undefined); continue; } if (ship.getMetadata(PlayerID, "stationnary")) continue; ship.setMetadata(PlayerID, "stationnary", true); // Check if there are some treasure around if (PETRA.gatherTreasure(gameState, ship, true)) continue; // Do not stay idle near a dock to not disturb other ships let sea = ship.getMetadata(PlayerID, "sea"); for (let dock of gameState.getAllyStructures().filter(API3.Filters.byClass("Dock")).values()) { if (PETRA.getSeaAccess(gameState, dock) != sea) continue; if (API3.SquareVectorDistance(shipPosition, dock.position()) > 4900) continue; ship.moveToRange(dock.position()[0], dock.position()[1], 70, 75); } } } for (let ship of blockedShips) { let shipPosition = ship.position(); let sea = ship.getMetadata(PlayerID, "sea"); for (let blockingShip of this.seaShips[sea].values()) { if (blockedIds.indexOf(blockingShip.id()) != -1 || !blockingShip.position()) continue; let distSquare = API3.SquareVectorDistance(shipPosition, blockingShip.position()); let unitAIState = blockingShip.unitAIState(); if (blockingShip.getMetadata(PlayerID, "transporter") === undefined && unitAIState != "INDIVIDUAL.GATHER.APPROACHING" && unitAIState != "INDIVIDUAL.GATHER.RETURNINGRESOURCE.APPROACHING") { if (distSquare < 1600) blockingShip.moveToRange(shipPosition[0], shipPosition[1], 40, 45); } else if (distSquare < 900) blockingShip.moveToRange(shipPosition[0], shipPosition[1], 30, 35); } for (let blockingShip of gameState.ai.HQ.tradeManager.traders.filter(API3.Filters.byClass("Ship")).values()) { if (blockingShip.getMetadata(PlayerID, "sea") != sea) continue; if (blockedIds.indexOf(blockingShip.id()) != -1 || !blockingShip.position()) continue; let role = blockingShip.getMetadata(PlayerID, "role"); if (!role || role != "trader") // already accounted before continue; let distSquare = API3.SquareVectorDistance(shipPosition, blockingShip.position()); let unitAIState = blockingShip.unitAIState(); if (unitAIState != "INDIVIDUAL.TRADE.APPROACHINGMARKET") { if (distSquare < 1600) blockingShip.moveToRange(shipPosition[0], shipPosition[1], 40, 45); } else if (distSquare < 900) blockingShip.moveToRange(shipPosition[0], shipPosition[1], 30, 35); } } }; PETRA.NavalManager.prototype.buildNavalStructures = function(gameState, queues) { - if (!gameState.ai.HQ.navalMap || !gameState.ai.HQ.baseManagers[1]) + if (!gameState.ai.HQ.navalMap || !gameState.ai.HQ.hasPotentialBase()) return; if (gameState.ai.HQ.getAccountedPopulation(gameState) > this.Config.Economy.popForDock) { if (queues.dock.countQueuedUnitsWithClass("Dock") === 0 && !gameState.getOwnStructures().filter(API3.Filters.and(API3.Filters.byClass("Dock"), API3.Filters.isFoundation())).hasEntities() && gameState.ai.HQ.canBuild(gameState, "structures/{civ}/dock")) { let dockStarted = false; - for (let base of gameState.ai.HQ.baseManagers) + for (const base of gameState.ai.HQ.baseManagers()) { if (dockStarted) break; if (!base.anchor || base.constructing) continue; let remaining = this.getUnconnectedSeas(gameState, base.accessIndex); for (let sea of remaining) { if (!gameState.ai.HQ.navalRegions[sea]) continue; let wantedLand = {}; wantedLand[base.accessIndex] = true; queues.dock.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/dock", { "land": wantedLand, "sea": sea })); dockStarted = true; break; } } } } if (gameState.currentPhase() < 2 || gameState.ai.HQ.getAccountedPopulation(gameState) < this.Config.Economy.popPhase2 + 15 || queues.militaryBuilding.hasQueuedUnits()) return; if (!this.docks.filter(API3.Filters.byClass("Dock")).hasEntities() || this.docks.filter(API3.Filters.byClass("Shipyard")).hasEntities()) return; // Use in priority resources to build a Market. if (!gameState.getOwnEntitiesByClass("Market", true).hasEntities() && gameState.ai.HQ.canBuild(gameState, "structures/{civ}/market")) return; let template; if (gameState.ai.HQ.canBuild(gameState, "structures/{civ}/super_dock")) template = "structures/{civ}/super_dock"; else if (gameState.ai.HQ.canBuild(gameState, "structures/{civ}/shipyard")) template = "structures/{civ}/shipyard"; else return; let wantedLand = {}; - for (let base of gameState.ai.HQ.baseManagers) + for (const base of gameState.ai.HQ.baseManagers()) if (base.anchor) wantedLand[base.accessIndex] = true; let sea = this.docks.toEntityArray()[0].getMetadata(PlayerID, "sea"); queues.militaryBuilding.addPlan(new PETRA.ConstructionPlan(gameState, template, { "land": wantedLand, "sea": sea })); }; /** goal can be either attack (choose ship with best arrowCount) or transport (choose ship with best capacity) */ PETRA.NavalManager.prototype.getBestShip = function(gameState, sea, goal) { let civ = gameState.getPlayerCiv(); let trainableShips = []; gameState.getOwnTrainingFacilities().filter(API3.Filters.byMetadata(PlayerID, "sea", sea)).forEach(function(ent) { let trainables = ent.trainableEntities(civ); for (let trainable of trainables) { if (gameState.isTemplateDisabled(trainable)) continue; let template = gameState.getTemplate(trainable); if (template && template.hasClass("Ship") && trainableShips.indexOf(trainable) === -1) trainableShips.push(trainable); } }); let best = 0; let bestShip; let limits = gameState.getEntityLimits(); let current = gameState.getEntityCounts(); for (let trainable of trainableShips) { let template = gameState.getTemplate(trainable); if (!template.available(gameState)) continue; let category = template.trainingCategory(); if (category && limits[category] && current[category] >= limits[category]) continue; let arrows = +(template.getDefaultArrow() || 0); if (goal === "attack") // choose the maximum default arrows { if (best > arrows) continue; best = arrows; } else if (goal === "transport") // choose the maximum capacity, with a bonus if arrows or if siege transport { let capacity = +(template.garrisonMax() || 0); if (capacity < 2) continue; capacity += 10*arrows; if (MatchesClassList(template.garrisonableClasses(), "Siege")) capacity += 50; if (best > capacity) continue; best = capacity; } else if (goal === "fishing") if (!template.hasClass("FishingBoat")) continue; bestShip = trainable; } return bestShip; }; PETRA.NavalManager.prototype.update = function(gameState, queues, events) { Engine.ProfileStart("Naval Manager update"); // close previous transport plans if finished for (let i = 0; i < this.transportPlans.length; ++i) { let remaining = this.transportPlans[i].update(gameState); if (remaining) continue; if (this.Config.debug > 1) API3.warn("no more units on transport plan " + this.transportPlans[i].ID); this.transportPlans[i].releaseAll(); this.transportPlans.splice(i--, 1); } // assign free ships to plans which need them this.assignShipsToPlans(gameState); // and require for more ships/structures if needed if (gameState.ai.playedTurn % 3 === 0) { this.checkLevels(gameState, queues); this.maintainFleet(gameState, queues); this.buildNavalStructures(gameState, queues); } // let inactive ships move apart from active ones (waiting for a better pathfinder) this.moveApart(gameState); Engine.ProfileStop(); }; PETRA.NavalManager.prototype.Serialize = function() { let properties = { "wantedTransportShips": this.wantedTransportShips, "wantedWarShips": this.wantedWarShips, "wantedFishShips": this.wantedFishShips, "neededTransportShips": this.neededTransportShips, "neededWarShips": this.neededWarShips, "landingZones": this.landingZones }; let transports = {}; for (let plan in this.transportPlans) transports[plan] = this.transportPlans[plan].Serialize(); return { "properties": properties, "transports": transports }; }; PETRA.NavalManager.prototype.Deserialize = function(gameState, data) { for (let key in data.properties) this[key] = data.properties[key]; this.transportPlans = []; for (let i in data.transports) { let dataPlan = data.transports[i]; let plan = new PETRA.TransportPlan(gameState, [], dataPlan.startIndex, dataPlan.endIndex, dataPlan.endPos); plan.Deserialize(dataPlan); plan.init(gameState); this.transportPlans.push(plan); } }; Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueManager.js (revision 25875) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueManager.js (revision 25876) @@ -1,595 +1,595 @@ /** * This takes the input queues and picks which items to fund with resources until no more resources are left to distribute. * * Currently this manager keeps accounts for each queue, split between the 4 main resources * * Each time resources are available (ie not in any account), it is split between the different queues * Mostly based on priority of the queue, and existing needs. * Each turn, the queue Manager checks if a queue can afford its next item, then it does. * * A consequence of the system it's not really revertible. Once a queue has an account of 500 food, it'll keep it * If for some reason the AI stops getting new food, and this queue lacks, say, wood, no other queues will * be able to benefit form the 500 food (even if they only needed food). * This is not to annoying as long as all goes well. If the AI loses many workers, it starts being problematic. * * It also has the effect of making the AI more or less always sit on a few hundreds resources since most queues * get some part of the total, and if all queues have 70% of their needs, nothing gets done * Particularly noticeable when phasing: the AI often overshoots by a good 200/300 resources before starting. * * This system should be improved. It's probably not flexible enough. */ PETRA.QueueManager = function(Config, queues) { this.Config = Config; this.queues = queues; this.priorities = {}; for (let i in Config.priorities) this.priorities[i] = Config.priorities[i]; this.accounts = {}; // the sorting is updated on priority change. this.queueArrays = []; for (let q in this.queues) { this.accounts[q] = new API3.Resources(); this.queueArrays.push([q, this.queues[q]]); } let priorities = this.priorities; this.queueArrays.sort((a, b) => priorities[b[0]] - priorities[a[0]]); }; PETRA.QueueManager.prototype.getAvailableResources = function(gameState) { let resources = gameState.getResources(); for (let key in this.queues) resources.subtract(this.accounts[key]); return resources; }; PETRA.QueueManager.prototype.getTotalAccountedResources = function() { let resources = new API3.Resources(); for (let key in this.queues) resources.add(this.accounts[key]); return resources; }; PETRA.QueueManager.prototype.currentNeeds = function(gameState) { let needed = new API3.Resources(); // queueArrays because it's faster. for (let q of this.queueArrays) { let queue = q[1]; if (!queue.hasQueuedUnits() || !queue.plans[0].isGo(gameState)) continue; let costs = queue.plans[0].getCost(); needed.add(costs); } // get out current resources, not removing accounts. let current = gameState.getResources(); for (let res of Resources.GetCodes()) needed[res] = Math.max(0, needed[res] - current[res]); return needed; }; // calculate the gather rates we'd want to be able to start all elements in our queues // TODO: many things. PETRA.QueueManager.prototype.wantedGatherRates = function(gameState) { // default values for first turn when we have not yet set our queues. if (gameState.ai.playedTurn === 0) { let ret = {}; for (let res of Resources.GetCodes()) ret[res] = this.Config.queues.firstTurn[res] || this.Config.queues.firstTurn.default; return ret; } // get out current resources, not removing accounts. let current = gameState.getResources(); // short queue is the first item of a queue, assumed to be ready in 30s // medium queue is the second item of a queue, assumed to be ready in 60s // long queue contains the isGo=false items, assumed to be ready in 300s let totalShort = {}; let totalMedium = {}; let totalLong = {}; for (let res of Resources.GetCodes()) { totalShort[res] = this.Config.queues.short[res] || this.Config.queues.short.default; totalMedium[res] = this.Config.queues.medium[res] || this.Config.queues.medium.default; totalLong[res] = this.Config.queues.long[res] || this.Config.queues.long.default; } let total; // queueArrays because it's faster. for (let q of this.queueArrays) { let queue = q[1]; if (queue.paused) continue; for (let j = 0; j < queue.length(); ++j) { if (j > 1) break; let cost = queue.plans[j].getCost(); if (queue.plans[j].isGo(gameState)) { if (j === 0) total = totalShort; else total = totalMedium; } else total = totalLong; for (let type in total) total[type] += cost[type]; if (!queue.plans[j].isGo(gameState)) break; } } // global rates let rates = {}; let diff; for (let res of Resources.GetCodes()) { if (current[res] > 0) { diff = Math.min(current[res], totalShort[res]); totalShort[res] -= diff; current[res] -= diff; if (current[res] > 0) { diff = Math.min(current[res], totalMedium[res]); totalMedium[res] -= diff; current[res] -= diff; if (current[res] > 0) totalLong[res] -= Math.min(current[res], totalLong[res]); } } rates[res] = totalShort[res]/30 + totalMedium[res]/60 + totalLong[res]/300; } return rates; }; PETRA.QueueManager.prototype.printQueues = function(gameState) { let numWorkers = 0; gameState.getOwnUnits().forEach(ent => { if (ent.getMetadata(PlayerID, "role") == "worker" && ent.getMetadata(PlayerID, "plan") === undefined) numWorkers++; }); API3.warn("---------- QUEUES ------------ with pop " + gameState.getPopulation() + " and workers " + numWorkers); for (let i in this.queues) { let q = this.queues[i]; if (q.hasQueuedUnits()) { API3.warn(i + ": ( with priority " + this.priorities[i] +" and accounts " + uneval(this.accounts[i]) +")"); API3.warn(" while maxAccountWanted(0.6) is " + uneval(q.maxAccountWanted(gameState, 0.6))); } for (let plan of q.plans) { let qStr = " " + plan.type + " "; if (plan.number) qStr += "x" + plan.number; qStr += " isGo " + plan.isGo(gameState); API3.warn(qStr); } } API3.warn("Accounts"); for (let p in this.accounts) API3.warn(p + ": " + uneval(this.accounts[p])); API3.warn("Current Resources: " + uneval(gameState.getResources())); API3.warn("Available Resources: " + uneval(this.getAvailableResources(gameState))); API3.warn("Wanted Gather Rates: " + uneval(gameState.ai.HQ.GetWantedGatherRates(gameState))); API3.warn("Current Gather Rates: " + uneval(gameState.ai.HQ.GetCurrentGatherRates(gameState))); API3.warn("Most needed resources: " + uneval(gameState.ai.HQ.pickMostNeededResources(gameState))); API3.warn("------------------------------------"); }; PETRA.QueueManager.prototype.clear = function() { for (let i in this.queues) this.queues[i].empty(); }; /** * set accounts of queue i from the unaccounted resources */ PETRA.QueueManager.prototype.setAccounts = function(gameState, cost, i) { let available = this.getAvailableResources(gameState); for (let res of Resources.GetCodes()) { if (this.accounts[i][res] >= cost[res]) continue; this.accounts[i][res] += Math.min(available[res], cost[res] - this.accounts[i][res]); } }; /** * transfer accounts from queue i to queue j */ PETRA.QueueManager.prototype.transferAccounts = function(cost, i, j) { for (let res of Resources.GetCodes()) { if (this.accounts[j][res] >= cost[res]) continue; let diff = Math.min(this.accounts[i][res], cost[res] - this.accounts[j][res]); this.accounts[i][res] -= diff; this.accounts[j][res] += diff; } }; /** * distribute the resources between the different queues according to their priorities */ PETRA.QueueManager.prototype.distributeResources = function(gameState) { let availableRes = this.getAvailableResources(gameState); for (let res of Resources.GetCodes()) { if (availableRes[res] < 0) // rescale the accounts if we've spent resources already accounted (e.g. by bartering) { let total = gameState.getResources()[res]; let scale = total / (total - availableRes[res]); availableRes[res] = total; for (let j in this.queues) { this.accounts[j][res] = Math.floor(scale * this.accounts[j][res]); availableRes[res] -= this.accounts[j][res]; } } if (!availableRes[res]) { this.switchResource(gameState, res); continue; } let totalPriority = 0; let tempPrio = {}; let maxNeed = {}; // Okay so this is where it gets complicated. // If a queue requires "res" for the next elements (in the queue) // And the account is not high enough for it. // Then we add it to the total priority. // To try and be clever, we don't want a long queue to hog all resources. So two things: // -if a queue has enough of resource X for the 1st element, its priority is decreased (factor 2). // -queues accounts are capped at "resources for the first + 60% of the next" // This avoids getting a high priority queue with many elements hogging all of one resource // uselessly while it awaits for other resources. for (let j in this.queues) { // returns exactly the correct amount, ie 0 if we're not go. let queueCost = this.queues[j].maxAccountWanted(gameState, 0.6); if (this.queues[j].hasQueuedUnits() && this.accounts[j][res] < queueCost[res] && !this.queues[j].paused) { // adding us to the list of queues that need an update. tempPrio[j] = this.priorities[j]; maxNeed[j] = queueCost[res] - this.accounts[j][res]; // if we have enough of that resource for our first item in the queue, diminish our priority. if (this.accounts[j][res] >= this.queues[j].getNext().getCost()[res]) tempPrio[j] /= 2; if (tempPrio[j]) totalPriority += tempPrio[j]; } else if (this.accounts[j][res] > queueCost[res]) { availableRes[res] += this.accounts[j][res] - queueCost[res]; this.accounts[j][res] = queueCost[res]; } } // Now we allow resources to the accounts. We can at most allow "TempPriority/totalpriority*available" // But we'll sometimes allow less if that would overflow. let available = availableRes[res]; let missing = false; for (let j in tempPrio) { // we'll add at much what can be allowed to this queue. let toAdd = Math.floor(availableRes[res] * tempPrio[j]/totalPriority); if (toAdd >= maxNeed[j]) toAdd = maxNeed[j]; else missing = true; this.accounts[j][res] += toAdd; maxNeed[j] -= toAdd; available -= toAdd; } if (missing && available > 0) // distribute the rest (due to floor) in any queue { for (let j in tempPrio) { let toAdd = Math.min(maxNeed[j], available); this.accounts[j][res] += toAdd; available -= toAdd; if (available <= 0) break; } } if (available < 0) API3.warn("Petra: problem with remaining " + res + " in queueManager " + available); } }; PETRA.QueueManager.prototype.switchResource = function(gameState, res) { // We have no available resources, see if we can't "compact" them in one queue. // compare queues 2 by 2, and if one with a higher priority could be completed by our amount, give it. // TODO: this isn't perfect compression. for (let j in this.queues) { if (!this.queues[j].hasQueuedUnits() || this.queues[j].paused) continue; let queue = this.queues[j]; let queueCost = queue.maxAccountWanted(gameState, 0); if (this.accounts[j][res] >= queueCost[res]) continue; for (let i in this.queues) { if (i === j) continue; let otherQueue = this.queues[i]; if (this.priorities[i] >= this.priorities[j] || otherQueue.switched !== 0) continue; if (this.accounts[j][res] + this.accounts[i][res] < queueCost[res]) continue; let diff = queueCost[res] - this.accounts[j][res]; this.accounts[j][res] += diff; this.accounts[i][res] -= diff; ++otherQueue.switched; if (this.Config.debug > 2) API3.warn ("switching queue " + res + " from " + i + " to " + j + " in amount " + diff); break; } } }; // Start the next item in the queue if we can afford it. PETRA.QueueManager.prototype.startNextItems = function(gameState) { for (let q of this.queueArrays) { let name = q[0]; let queue = q[1]; if (queue.hasQueuedUnits() && !queue.paused) { let item = queue.getNext(); if (this.accounts[name].canAfford(item.getCost()) && item.canStart(gameState)) { // canStart may update the cost because of the costMultiplier so we must check it again if (this.accounts[name].canAfford(item.getCost())) { this.finishingTime = gameState.ai.elapsedTime; this.accounts[name].subtract(item.getCost()); queue.startNext(gameState); queue.switched = 0; } } } else if (!queue.hasQueuedUnits()) { this.accounts[name].reset(); queue.switched = 0; } } }; PETRA.QueueManager.prototype.update = function(gameState) { Engine.ProfileStart("Queue Manager"); for (let i in this.queues) { this.queues[i].check(gameState); // do basic sanity checks on the queue if (this.priorities[i] > 0) continue; API3.warn("QueueManager received bad priorities, please report this error: " + uneval(this.priorities)); this.priorities[i] = 1; // TODO: make the Queue Manager not die when priorities are zero. } // Pause or unpause queues depending on the situation this.checkPausedQueues(gameState); // Let's assign resources to plans that need them this.distributeResources(gameState); // Start the next item in the queue if we can afford it. this.startNextItems(gameState); if (this.Config.debug > 1 && gameState.ai.playedTurn%50 === 0) this.printQueues(gameState); Engine.ProfileStop(); }; // Recovery system: if short of workers after an attack, pause (and reset) some queues to favor worker training PETRA.QueueManager.prototype.checkPausedQueues = function(gameState) { let numWorkers = gameState.countOwnEntitiesAndQueuedWithRole("worker"); let workersMin = Math.min(Math.max(12, 24 * this.Config.popScaling), this.Config.Economy.popPhase2); for (let q in this.queues) { let toBePaused = false; - if (gameState.ai.HQ.numPotentialBases() == 0) + if (!gameState.ai.HQ.hasPotentialBase()) toBePaused = q != "dock" && q != "civilCentre"; else if (numWorkers < workersMin / 3) toBePaused = q != "citizenSoldier" && q != "villager" && q != "emergency"; else if (numWorkers < workersMin * 2 / 3) toBePaused = q == "civilCentre" || q == "economicBuilding" || q == "militaryBuilding" || q == "defenseBuilding" || q == "healer" || q == "majorTech" || q == "minorTech" || q.indexOf("plan_") != -1; else if (numWorkers < workersMin) toBePaused = q == "civilCentre" || q == "defenseBuilding" || q == "majorTech" || q.indexOf("_siege") != -1 || q.indexOf("_champ") != -1; if (toBePaused) { if (q == "field" && gameState.ai.HQ.needFarm && !gameState.getOwnStructures().filter(API3.Filters.byClass("Field")).hasEntities()) toBePaused = false; if (q == "corral" && gameState.ai.HQ.needCorral && !gameState.getOwnStructures().filter(API3.Filters.byClass("Field")).hasEntities()) toBePaused = false; if (q == "dock" && gameState.ai.HQ.needFish && !gameState.getOwnStructures().filter(API3.Filters.byClass("Dock")).hasEntities()) toBePaused = false; if (q == "ships" && gameState.ai.HQ.needFish && !gameState.ai.HQ.navalManager.ships.filter(API3.Filters.byClass("FishingBoat")).hasEntities()) toBePaused = false; } let queue = this.queues[q]; if (!queue.paused && toBePaused) { queue.paused = true; this.accounts[q].reset(); } else if (queue.paused && !toBePaused) queue.paused = false; // And reduce the batch sizes of attack queues if (q.indexOf("plan_") != -1 && numWorkers < workersMin && queue.plans[0]) { queue.plans[0].number = 1; if (queue.plans[1]) queue.plans[1].number = 1; } } }; PETRA.QueueManager.prototype.canAfford = function(queue, cost) { if (!this.accounts[queue]) return false; return this.accounts[queue].canAfford(cost); }; PETRA.QueueManager.prototype.pauseQueue = function(queue, scrapAccounts) { if (!this.queues[queue]) return; this.queues[queue].paused = true; if (scrapAccounts) this.accounts[queue].reset(); }; PETRA.QueueManager.prototype.unpauseQueue = function(queue) { if (this.queues[queue]) this.queues[queue].paused = false; }; PETRA.QueueManager.prototype.pauseAll = function(scrapAccounts, but) { for (let q in this.queues) { if (q == but) continue; if (scrapAccounts) this.accounts[q].reset(); this.queues[q].paused = true; } }; PETRA.QueueManager.prototype.unpauseAll = function(but) { for (let q in this.queues) if (q != but) this.queues[q].paused = false; }; PETRA.QueueManager.prototype.addQueue = function(queueName, priority) { if (this.queues[queueName] !== undefined) return; this.queues[queueName] = new PETRA.Queue(); this.priorities[queueName] = priority; this.accounts[queueName] = new API3.Resources(); this.queueArrays = []; for (let q in this.queues) this.queueArrays.push([q, this.queues[q]]); let priorities = this.priorities; this.queueArrays.sort((a, b) => priorities[b[0]] - priorities[a[0]]); }; PETRA.QueueManager.prototype.removeQueue = function(queueName) { if (this.queues[queueName] === undefined) return; delete this.queues[queueName]; delete this.priorities[queueName]; delete this.accounts[queueName]; this.queueArrays = []; for (let q in this.queues) this.queueArrays.push([q, this.queues[q]]); let priorities = this.priorities; this.queueArrays.sort((a, b) => priorities[b[0]] - priorities[a[0]]); }; PETRA.QueueManager.prototype.getPriority = function(queueName) { return this.priorities[queueName]; }; PETRA.QueueManager.prototype.changePriority = function(queueName, newPriority) { if (this.Config.debug > 1) API3.warn(">>> Priority of queue " + queueName + " changed from " + this.priorities[queueName] + " to " + newPriority); if (this.queues[queueName] !== undefined) this.priorities[queueName] = newPriority; let priorities = this.priorities; this.queueArrays.sort((a, b) => priorities[b[0]] - priorities[a[0]]); }; PETRA.QueueManager.prototype.Serialize = function() { let accounts = {}; let queues = {}; for (let q in this.queues) { queues[q] = this.queues[q].Serialize(); accounts[q] = this.accounts[q].Serialize(); if (this.Config.debug == -100) API3.warn("queueManager serialization: queue " + q + " >>> " + uneval(queues[q]) + " with accounts " + uneval(accounts[q])); } return { "priorities": this.priorities, "queues": queues, "accounts": accounts }; }; PETRA.QueueManager.prototype.Deserialize = function(gameState, data) { this.priorities = data.priorities; this.queues = {}; this.accounts = {}; // the sorting is updated on priority change. this.queueArrays = []; for (let q in data.queues) { this.queues[q] = new PETRA.Queue(); this.queues[q].Deserialize(gameState, data.queues[q]); this.accounts[q] = new API3.Resources(); this.accounts[q].Deserialize(data.accounts[q]); this.queueArrays.push([q, this.queues[q]]); } this.queueArrays.sort((a, b) => data.priorities[b[0]] - data.priorities[a[0]]); }; Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplanBuilding.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplanBuilding.js (revision 25875) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/queueplanBuilding.js (revision 25876) @@ -1,945 +1,945 @@ /** * Defines a construction plan, ie a building. * We'll try to fing a good position if non has been provided */ PETRA.ConstructionPlan = function(gameState, type, metadata, position) { if (!PETRA.QueuePlan.call(this, gameState, type, metadata)) return false; this.position = position ? position : 0; this.category = "building"; return true; }; PETRA.ConstructionPlan.prototype = Object.create(PETRA.QueuePlan.prototype); PETRA.ConstructionPlan.prototype.canStart = function(gameState) { if (gameState.ai.HQ.turnCache.buildingBuilt) // do not start another building if already one this turn return false; if (!this.isGo(gameState)) return false; if (this.template.requiredTech() && !gameState.isResearched(this.template.requiredTech())) return false; return gameState.ai.HQ.buildManager.hasBuilder(this.type); }; PETRA.ConstructionPlan.prototype.start = function(gameState) { Engine.ProfileStart("Building construction start"); // We don't care which builder we assign, since they won't actually do // the building themselves - all we care about is that there is at least // one unit that can start the foundation (should always be the case here). let builder = gameState.findBuilder(this.type); if (!builder) { API3.warn("petra error: builder not found when starting construction."); Engine.ProfileStop(); return; } let pos = this.findGoodPosition(gameState); if (!pos) { gameState.ai.HQ.buildManager.setUnbuildable(gameState, this.type, 90, "room"); Engine.ProfileStop(); return; } if (this.metadata && this.metadata.expectedGain && (!this.template.hasClass("Market") || gameState.getOwnEntitiesByClass("Market", true).hasEntities())) { // Check if this Market is still worth building (others may have been built making it useless). let tradeManager = gameState.ai.HQ.tradeManager; tradeManager.checkRoutes(gameState); if (!tradeManager.isNewMarketWorth(this.metadata.expectedGain)) { Engine.ProfileStop(); return; } } gameState.ai.HQ.turnCache.buildingBuilt = true; if (this.metadata === undefined) this.metadata = { "base": pos.base }; else if (this.metadata.base === undefined) this.metadata.base = pos.base; if (pos.access) this.metadata.access = pos.access; // needed for Docks whose position is on water else this.metadata.access = gameState.ai.accessibility.getAccessValue([pos.x, pos.z]); if (this.template.buildPlacementType() == "shore") { // adjust a bit the position if needed let cosa = Math.cos(pos.angle); let sina = Math.sin(pos.angle); let shiftMax = gameState.ai.HQ.territoryMap.cellSize; for (let shift = 0; shift <= shiftMax; shift += 2) { builder.construct(this.type, pos.x-shift*sina, pos.z-shift*cosa, pos.angle, this.metadata); if (shift > 0) builder.construct(this.type, pos.x+shift*sina, pos.z+shift*cosa, pos.angle, this.metadata); } } else if (pos.xx === undefined || pos.x == pos.xx && pos.z == pos.zz) builder.construct(this.type, pos.x, pos.z, pos.angle, this.metadata); else // try with the lowest, move towards us unless we're same { for (let step = 0; step <= 1; step += 0.2) builder.construct(this.type, step*pos.x + (1-step)*pos.xx, step*pos.z + (1-step)*pos.zz, pos.angle, this.metadata); } this.onStart(gameState); Engine.ProfileStop(); if (this.metadata && this.metadata.proximity) gameState.ai.HQ.navalManager.createTransportIfNeeded(gameState, this.metadata.proximity, [pos.x, pos.z], this.metadata.access); }; PETRA.ConstructionPlan.prototype.findGoodPosition = function(gameState) { let template = this.template; if (template.buildPlacementType() == "shore") return this.findDockPosition(gameState); let HQ = gameState.ai.HQ; if (template.hasClass("Storehouse") && this.metadata && this.metadata.base) { // recompute the best dropsite location in case some conditions have changed let base = HQ.getBaseByID(this.metadata.base); let type = this.metadata.type ? this.metadata.type : "wood"; const newpos = base.findBestDropsiteLocation(gameState, type, template._templateName); if (newpos && newpos.quality > 0) { let pos = newpos.pos; return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": this.metadata.base }; } } if (!this.position) { if (template.hasClass("CivCentre")) { let pos; if (this.metadata && this.metadata.resource) { let proximity = this.metadata.proximity ? this.metadata.proximity : undefined; pos = HQ.findEconomicCCLocation(gameState, template, this.metadata.resource, proximity); } else pos = HQ.findStrategicCCLocation(gameState, template); if (pos) return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": 0 }; // No possible location, try to build instead a dock in a not-enemy island let templateName = gameState.applyCiv("structures/{civ}/dock"); if (gameState.ai.HQ.canBuild(gameState, templateName) && !gameState.isTemplateDisabled(templateName)) { template = gameState.getTemplate(templateName); if (template && gameState.getResources().canAfford(new API3.Resources(template.cost()))) this.buildOverseaDock(gameState, template); } return false; } else if (template.hasClasses(["Tower", "Fortress", "ArmyCamp"])) { let pos = HQ.findDefensiveLocation(gameState, template); if (pos) return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": pos[2] }; // if this fortress is our first one, just try the standard placement if (!template.hasClass("Fortress") || gameState.getOwnEntitiesByClass("Fortress", true).hasEntities()) return false; } else if (template.hasClass("Market")) // Docks are done before. { let pos = HQ.findMarketLocation(gameState, template); if (pos && pos[2] > 0) { if (!this.metadata) this.metadata = {}; this.metadata.expectedGain = pos[3]; return { "x": pos[0], "z": pos[1], "angle": 3*Math.PI/4, "base": pos[2] }; } else if (!pos) return false; } } // Compute each tile's closeness to friendly structures: let placement = new API3.Map(gameState.sharedScript, "territory"); let cellSize = placement.cellSize; // size of each tile let alreadyHasHouses = false; if (this.position) // If a position was specified then place the building as close to it as possible { let x = Math.floor(this.position[0] / cellSize); let z = Math.floor(this.position[1] / cellSize); placement.addInfluence(x, z, 255); } else // No position was specified so try and find a sensible place to build { // give a small > 0 level as the result of addInfluence is constrained to be > 0 // if we really need houses (i.e. Phasing without enough village building), do not apply these constraints if (this.metadata && this.metadata.base !== undefined) { let base = this.metadata.base; for (let j = 0; j < placement.map.length; ++j) - if (HQ.basesMap.map[j] == base) + if (HQ.baseAtIndex(j) == base) placement.set(j, 45); } else { for (let j = 0; j < placement.map.length; ++j) - if (HQ.basesMap.map[j] != 0) + if (HQ.baseAtIndex(j) != 0) placement.set(j, 45); } if (!HQ.requireHouses || !template.hasClass("House")) { gameState.getOwnStructures().forEach(function(ent) { let pos = ent.position(); let x = Math.round(pos[0] / cellSize); let z = Math.round(pos[1] / cellSize); let struct = PETRA.getBuiltEntity(gameState, ent); if (struct.resourceDropsiteTypes() && struct.resourceDropsiteTypes().indexOf("food") != -1) { if (template.hasClasses(["Field", "Corral"])) placement.addInfluence(x, z, 80 / cellSize, 50); else // If this is not a field add a negative influence because we want to leave this area for fields placement.addInfluence(x, z, 80 / cellSize, -20); } else if (template.hasClass("House")) { if (ent.hasClass("House")) { placement.addInfluence(x, z, 60 / cellSize, 40); // houses are close to other houses alreadyHasHouses = true; } else if (ent.hasClasses(["Gate", "!Wall"])) placement.addInfluence(x, z, 60 / cellSize, -40); // and further away from other stuffs } else if (template.hasClass("Farmstead") && !ent.hasClasses(["Field", "Corral"]) && ent.hasClasses(["Gate", "!Wall"])) placement.addInfluence(x, z, 100 / cellSize, -25); // move farmsteads away to make room (Wall test needed for iber) else if (template.hasClass("GarrisonFortress") && ent.hasClass("House")) placement.addInfluence(x, z, 120 / cellSize, -50); else if (template.hasClass("Military")) placement.addInfluence(x, z, 40 / cellSize, -40); else if (template.genericName() == "Rotary Mill" && ent.hasClass("Field")) placement.addInfluence(x, z, 60 / cellSize, 40); }); } if (template.hasClass("Farmstead")) { for (let j = 0; j < placement.map.length; ++j) { let value = placement.map[j] - gameState.sharedScript.resourceMaps.wood.map[j]/3; if (HQ.borderMap.map[j] & PETRA.fullBorder_Mask) value /= 2; // we need space around farmstead, so disfavor map border placement.set(j, value); } } } // Requires to be inside our territory, and inside our base territory if required // and if our first market, put it on border if possible to maximize distance with next Market. let favorBorder = template.hasClass("Market"); let disfavorBorder = gameState.currentPhase() > 1 && !template.hasDefensiveFire(); let favoredBase = this.metadata && (this.metadata.favoredBase || (this.metadata.militaryBase ? HQ.findBestBaseForMilitary(gameState) : undefined)); if (this.metadata && this.metadata.base !== undefined) { let base = this.metadata.base; for (let j = 0; j < placement.map.length; ++j) { - if (HQ.basesMap.map[j] != base) + if (HQ.baseAtIndex(j) != base) placement.map[j] = 0; else if (placement.map[j] > 0) { if (favorBorder && HQ.borderMap.map[j] & PETRA.border_Mask) placement.set(j, placement.map[j] + 50); else if (disfavorBorder && !(HQ.borderMap.map[j] & PETRA.fullBorder_Mask)) placement.set(j, placement.map[j] + 10); let x = (j % placement.width + 0.5) * cellSize; let z = (Math.floor(j / placement.width) + 0.5) * cellSize; if (HQ.isNearInvadingArmy([x, z])) placement.map[j] = 0; } } } else { for (let j = 0; j < placement.map.length; ++j) { - if (HQ.basesMap.map[j] == 0) + if (HQ.baseAtIndex(j) == 0) placement.map[j] = 0; else if (placement.map[j] > 0) { if (favorBorder && HQ.borderMap.map[j] & PETRA.border_Mask) placement.set(j, placement.map[j] + 50); else if (disfavorBorder && !(HQ.borderMap.map[j] & PETRA.fullBorder_Mask)) placement.set(j, placement.map[j] + 10); let x = (j % placement.width + 0.5) * cellSize; let z = (Math.floor(j / placement.width) + 0.5) * cellSize; if (HQ.isNearInvadingArmy([x, z])) placement.map[j] = 0; - else if (favoredBase && HQ.basesMap.map[j] == favoredBase) + else if (favoredBase && HQ.baseAtIndex(j) == favoredBase) placement.set(j, placement.map[j] + 100); } } } // Find the best non-obstructed: // Find target building's approximate obstruction radius, and expand by a bit to make sure we're not too close, // this allows room for units to walk between buildings. // note: not for houses and dropsites who ought to be closer to either each other or a resource. // also not for fields who can be stacked quite a bit let obstructions = PETRA.createObstructionMap(gameState, 0, template); // obstructions.dumpIm(template.buildPlacementType() + "_obstructions.png"); let radius = 0; if (template.hasClasses(["Fortress", "Arsenal"]) || this.type == gameState.applyCiv("structures/{civ}/elephant_stable")) radius = Math.floor((template.obstructionRadius().max + 8) / obstructions.cellSize); else if (template.resourceDropsiteTypes() === undefined && !template.hasClasses(["House", "Field", "Market"])) radius = Math.ceil((template.obstructionRadius().max + 4) / obstructions.cellSize); else radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); let bestTile; if (template.hasClass("House") && !alreadyHasHouses) { // try to get some space to place several houses first bestTile = placement.findBestTile(3*radius, obstructions); if (!bestTile.val) bestTile = undefined; } if (!bestTile) bestTile = placement.findBestTile(radius, obstructions); if (!bestTile.val) return false; let bestIdx = bestTile.idx; let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; let territorypos = placement.gamePosToMapPos([x, z]); let territoryIndex = territorypos[0] + territorypos[1]*placement.width; // default angle = 3*Math.PI/4; - return { "x": x, "z": z, "angle": 3*Math.PI/4, "base": HQ.basesMap.map[territoryIndex] }; + return { "x": x, "z": z, "angle": 3*Math.PI/4, "base": HQ.baseAtIndex(territoryIndex) }; }; /** * Placement of buildings with Dock build category * metadata.proximity is defined when first dock without any territory * => we try to minimize distance from our current point * metadata.oversea is defined for dock in oversea islands * => we try to maximize distance to our current docks (for trade) * otherwise standard dock on an island where we already have a cc * => we try not to be too far from our territory * In all cases, we add a bonus for nearby resources, and when a large extend of water in front ot it. */ PETRA.ConstructionPlan.prototype.findDockPosition = function(gameState) { let template = this.template; let territoryMap = gameState.ai.HQ.territoryMap; let obstructions = PETRA.createObstructionMap(gameState, 0, template); // obstructions.dumpIm(template.buildPlacementType() + "_obstructions.png"); let bestIdx; let bestJdx; let bestAngle; let bestLand; let bestWater; let bestVal = -1; let navalPassMap = gameState.ai.accessibility.navalPassMap; let width = gameState.ai.HQ.territoryMap.width; let cellSize = gameState.ai.HQ.territoryMap.cellSize; let nbShips = gameState.ai.HQ.navalManager.transportShips.length; let wantedLand = this.metadata && this.metadata.land ? this.metadata.land : null; let wantedSea = this.metadata && this.metadata.sea ? this.metadata.sea : null; let proxyAccess = this.metadata && this.metadata.proximity ? gameState.ai.accessibility.getAccessValue(this.metadata.proximity) : null; let oversea = this.metadata && this.metadata.oversea ? this.metadata.oversea : null; if (nbShips == 0 && proxyAccess && proxyAccess > 1) { wantedLand = {}; wantedLand[proxyAccess] = true; } let dropsiteTypes = template.resourceDropsiteTypes(); let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize); let halfSize = 0; // used for dock angle let halfDepth = 0; // used by checkPlacement let halfWidth = 0; // used by checkPlacement if (template.get("Footprint/Square")) { halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2; halfDepth = +template.get("Footprint/Square/@depth") / 2; halfWidth = +template.get("Footprint/Square/@width") / 2; } else if (template.get("Footprint/Circle")) { halfSize = +template.get("Footprint/Circle/@radius"); halfDepth = halfSize; halfWidth = halfSize; } // res is a measure of the amount of resources around, and maxRes is the max value taken into account // water is a measure of the water space around, and maxWater is the max value that can be returned by checkDockPlacement const maxRes = 10; const maxWater = 16; let ccEnts = oversea ? gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")) : null; let docks = oversea ? gameState.getOwnStructures().filter(API3.Filters.byClass("Dock")) : null; // Normalisation factors (only guessed, no attempt to optimize them) let factor = proxyAccess ? 1 : oversea ? 0.2 : 40; for (let j = 0; j < territoryMap.length; ++j) { if (!this.isDockLocation(gameState, j, halfDepth, wantedLand, wantedSea)) continue; let score = 0; if (!proxyAccess && !oversea) { // if not in our (or allied) territory, we do not want it too far to be able to defend it score = this.getFrontierProximity(gameState, j); if (score > 4) continue; score *= factor; } let i = territoryMap.getNonObstructedTile(j, radius, obstructions); if (i < 0) continue; if (wantedSea && navalPassMap[i] != wantedSea) continue; let res = dropsiteTypes ? Math.min(maxRes, this.getResourcesAround(gameState, dropsiteTypes, j, 80)) : maxRes; let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)]; // If proximity is given, we look for the nearest point if (proxyAccess) score = API3.VectorDistance(this.metadata.proximity, pos); // Bonus for resources score += 20 * (maxRes - res); if (oversea) { // Not much farther to one of our cc than to enemy ones let enemyDist; let ownDist; for (let cc of ccEnts.values()) { let owner = cc.owner(); if (owner != PlayerID && !gameState.isPlayerEnemy(owner)) continue; let dist = API3.SquareVectorDistance(pos, cc.position()); if (owner == PlayerID && (!ownDist || dist < ownDist)) ownDist = dist; if (gameState.isPlayerEnemy(owner) && (!enemyDist || dist < enemyDist)) enemyDist = dist; } if (ownDist && enemyDist && enemyDist < 0.5 * ownDist) continue; // And maximize distance for trade. let dockDist = 0; for (let dock of docks.values()) { if (PETRA.getSeaAccess(gameState, dock) != navalPassMap[i]) continue; let dist = API3.SquareVectorDistance(pos, dock.position()); if (dist > dockDist) dockDist = dist; } if (dockDist > 0) { dockDist = Math.sqrt(dockDist); if (dockDist > width * cellSize) // Could happen only on square maps, but anyway we don't want to be too far away continue; score += factor * (width * cellSize - dockDist); } } // Add a penalty if on the map border as ship movement will be difficult if (gameState.ai.HQ.borderMap.map[j] & PETRA.fullBorder_Mask) score += 20; // Do a pre-selection, supposing we will have the best possible water if (bestIdx !== undefined && score > bestVal + 5 * maxWater) continue; let x = (i % obstructions.width + 0.5) * obstructions.cellSize; let z = (Math.floor(i / obstructions.width) + 0.5) * obstructions.cellSize; let angle = this.getDockAngle(gameState, x, z, halfSize); if (angle == false) continue; let ret = this.checkDockPlacement(gameState, x, z, halfDepth, halfWidth, angle); if (!ret || !gameState.ai.HQ.landRegions[ret.land] || wantedLand && !wantedLand[ret.land]) continue; // Final selection now that the checkDockPlacement water is known if (bestIdx !== undefined && score + 5 * (maxWater - ret.water) > bestVal) continue; if (this.metadata.proximity && gameState.ai.accessibility.regionSize[ret.land] < 4000) continue; if (gameState.ai.HQ.isDangerousLocation(gameState, pos, halfSize)) continue; bestVal = score + maxWater - ret.water; bestIdx = i; bestJdx = j; bestAngle = angle; bestLand = ret.land; bestWater = ret.water; } if (bestVal < 0) return false; // if no good place with enough water around and still in first phase, wait for expansion at the next phase if (!this.metadata.proximity && bestWater < 10 && gameState.currentPhase() == 1) return false; let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize; let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize; // Assign this dock to a base - let baseIndex = gameState.ai.HQ.basesMap.map[bestJdx]; + let baseIndex = gameState.ai.HQ.baseAtIndex(bestJdx); if (!baseIndex) baseIndex = -2; // We'll do an anchorless base around it return { "x": x, "z": z, "angle": bestAngle, "base": baseIndex, "access": bestLand }; }; /** * Find a good island to build a dock. */ PETRA.ConstructionPlan.prototype.buildOverseaDock = function(gameState, template) { let docks = gameState.getOwnStructures().filter(API3.Filters.byClass("Dock")); if (!docks.hasEntities()) return; let passabilityMap = gameState.getPassabilityMap(); let cellArea = passabilityMap.cellSize * passabilityMap.cellSize; let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); let land = {}; let found; for (let i = 0; i < gameState.ai.accessibility.regionSize.length; ++i) { if (gameState.ai.accessibility.regionType[i] != "land" || cellArea * gameState.ai.accessibility.regionSize[i] < 3600) continue; let keep = true; for (let dock of docks.values()) { if (PETRA.getLandAccess(gameState, dock) != i) continue; keep = false; break; } if (!keep) continue; let sea; for (let cc of ccEnts.values()) { let ccAccess = PETRA.getLandAccess(gameState, cc); if (ccAccess != i) { if (cc.owner() == PlayerID && !sea) sea = gameState.ai.HQ.getSeaBetweenIndices(gameState, ccAccess, i); continue; } // Docks on island where we have a cc are already done elsewhere if (cc.owner() == PlayerID || gameState.isPlayerEnemy(cc.owner())) { keep = false; break; } } if (!keep || !sea) continue; land[i] = true; found = true; } if (!found) return; if (!gameState.ai.HQ.navalMap) API3.warn("petra.findOverseaLand on a non-naval map??? we should never go there "); let oldTemplate = this.template; let oldMetadata = this.metadata; this.template = template; let pos; this.metadata = { "land": land, "oversea": true }; pos = this.findDockPosition(gameState); if (pos) { let type = template.templateName(); let builder = gameState.findBuilder(type); this.metadata.base = pos.base; // Adjust a bit the position if needed let cosa = Math.cos(pos.angle); let sina = Math.sin(pos.angle); let shiftMax = gameState.ai.HQ.territoryMap.cellSize; for (let shift = 0; shift <= shiftMax; shift += 2) { builder.construct(type, pos.x-shift*sina, pos.z-shift*cosa, pos.angle, this.metadata); if (shift > 0) builder.construct(type, pos.x+shift*sina, pos.z+shift*cosa, pos.angle, this.metadata); } } this.template = oldTemplate; this.metadata = oldMetadata; }; /** Algorithm taken from the function GetDockAngle in simulation/helpers/Commands.js */ PETRA.ConstructionPlan.prototype.getDockAngle = function(gameState, x, z, size) { let pos = gameState.ai.accessibility.gamePosToMapPos([x, z]); let k = pos[0] + pos[1]*gameState.ai.accessibility.width; let seaRef = gameState.ai.accessibility.navalPassMap[k]; if (seaRef < 2) return false; const numPoints = 16; for (let dist = 0; dist < 4; ++dist) { let waterPoints = []; for (let i = 0; i < numPoints; ++i) { let angle = 2 * Math.PI * i / numPoints; pos = [x - (1+dist)*size*Math.sin(angle), z + (1+dist)*size*Math.cos(angle)]; pos = gameState.ai.accessibility.gamePosToMapPos(pos); if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width || pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height) continue; let j = pos[0] + pos[1]*gameState.ai.accessibility.width; if (gameState.ai.accessibility.navalPassMap[j] == seaRef) waterPoints.push(i); } let length = waterPoints.length; if (!length) continue; let consec = []; for (let i = 0; i < length; ++i) { let count = 0; for (let j = 0; j < length-1; ++j) { if ((waterPoints[(i + j) % length]+1) % numPoints == waterPoints[(i + j + 1) % length]) ++count; else break; } consec[i] = count; } let start = 0; let count = 0; for (let c in consec) { if (consec[c] > count) { start = c; count = consec[c]; } } // If we've found a shoreline, stop searching if (count != numPoints-1) return -((waterPoints[start] + consec[start]/2) % numPoints)/numPoints*2*Math.PI; } return false; }; /** * Algorithm taken from checkPlacement in simulation/components/BuildRestriction.js * to determine the special dock requirements * returns {"land": land index for this dock, "water": amount of water around this spot} */ PETRA.ConstructionPlan.prototype.checkDockPlacement = function(gameState, x, z, halfDepth, halfWidth, angle) { let sz = halfDepth * Math.sin(angle); let cz = halfDepth * Math.cos(angle); // center back position let pos = gameState.ai.accessibility.gamePosToMapPos([x - sz, z - cz]); let j = pos[0] + pos[1]*gameState.ai.accessibility.width; let land = gameState.ai.accessibility.landPassMap[j]; if (land < 2) return null; // center front position pos = gameState.ai.accessibility.gamePosToMapPos([x + sz, z + cz]); j = pos[0] + pos[1]*gameState.ai.accessibility.width; if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2) return null; // additional constraints compared to BuildRestriction.js to assure we have enough place to build let sw = halfWidth * Math.cos(angle) * 3 / 4; let cw = halfWidth * Math.sin(angle) * 3 / 4; pos = gameState.ai.accessibility.gamePosToMapPos([x - sz + sw, z - cz - cw]); j = pos[0] + pos[1]*gameState.ai.accessibility.width; if (gameState.ai.accessibility.landPassMap[j] != land) return null; pos = gameState.ai.accessibility.gamePosToMapPos([x - sz - sw, z - cz + cw]); j = pos[0] + pos[1]*gameState.ai.accessibility.width; if (gameState.ai.accessibility.landPassMap[j] != land) return null; let water = 0; let sp = 15 * Math.sin(angle); let cp = 15 * Math.cos(angle); for (let i = 1; i < 5; ++i) { pos = gameState.ai.accessibility.gamePosToMapPos([x + sz + i*(sp+sw), z + cz + i*(cp-cw)]); if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width || pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height) break; j = pos[0] + pos[1]*gameState.ai.accessibility.width; if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2) break; pos = gameState.ai.accessibility.gamePosToMapPos([x + sz + i*sp, z + cz + i*cp]); if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width || pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height) break; j = pos[0] + pos[1]*gameState.ai.accessibility.width; if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2) break; pos = gameState.ai.accessibility.gamePosToMapPos([x + sz + i*(sp-sw), z + cz + i*(cp+cw)]); if (pos[0] < 0 || pos[0] >= gameState.ai.accessibility.width || pos[1] < 0 || pos[1] >= gameState.ai.accessibility.height) break; j = pos[0] + pos[1]*gameState.ai.accessibility.width; if (gameState.ai.accessibility.landPassMap[j] > 1 || gameState.ai.accessibility.navalPassMap[j] < 2) break; water += 4; } return { "land": land, "water": water }; }; /** * fast check if we can build a dock: returns false if nearest land is farther than the dock dimension * if the (object) wantedLand is given, this nearest land should have one of these accessibility * if wantedSea is given, this tile should be inside this sea */ const around = [[ 1.0, 0.0], [ 0.87, 0.50], [ 0.50, 0.87], [ 0.0, 1.0], [-0.50, 0.87], [-0.87, 0.50], [-1.0, 0.0], [-0.87, -0.50], [-0.50, -0.87], [ 0.0, -1.0], [ 0.50, -0.87], [ 0.87, -0.50]]; PETRA.ConstructionPlan.prototype.isDockLocation = function(gameState, j, dimension, wantedLand, wantedSea) { let width = gameState.ai.HQ.territoryMap.width; let cellSize = gameState.ai.HQ.territoryMap.cellSize; let dimLand = dimension + 1.5 * cellSize; let dimSea = dimension + 2 * cellSize; let accessibility = gameState.ai.accessibility; let x = (j%width + 0.5) * cellSize; let z = (Math.floor(j/width) + 0.5) * cellSize; let pos = accessibility.gamePosToMapPos([x, z]); let k = pos[0] + pos[1]*accessibility.width; let landPass = accessibility.landPassMap[k]; if (landPass > 1 && wantedLand && !wantedLand[landPass] || landPass < 2 && accessibility.navalPassMap[k] < 2) return false; for (let a of around) { pos = accessibility.gamePosToMapPos([x + dimLand*a[0], z + dimLand*a[1]]); if (pos[0] < 0 || pos[0] >= accessibility.width) continue; if (pos[1] < 0 || pos[1] >= accessibility.height) continue; k = pos[0] + pos[1]*accessibility.width; landPass = accessibility.landPassMap[k]; if (landPass < 2 || wantedLand && !wantedLand[landPass]) continue; pos = accessibility.gamePosToMapPos([x - dimSea*a[0], z - dimSea*a[1]]); if (pos[0] < 0 || pos[0] >= accessibility.width) continue; if (pos[1] < 0 || pos[1] >= accessibility.height) continue; k = pos[0] + pos[1]*accessibility.width; if (wantedSea && accessibility.navalPassMap[k] != wantedSea || !wantedSea && accessibility.navalPassMap[k] < 2) continue; return true; } return false; }; /** * return a measure of the proximity to our frontier (including our allies) * 0=inside, 1=less than 24m, 2= less than 48m, 3= less than 72m, 4=less than 96m, 5=above 96m */ PETRA.ConstructionPlan.prototype.getFrontierProximity = function(gameState, j) { let alliedVictory = gameState.getAlliedVictory(); let territoryMap = gameState.ai.HQ.territoryMap; let territoryOwner = territoryMap.getOwnerIndex(j); if (territoryOwner == PlayerID || alliedVictory && gameState.isPlayerAlly(territoryOwner)) return 0; let borderMap = gameState.ai.HQ.borderMap; let width = territoryMap.width; let step = Math.round(24 / territoryMap.cellSize); let ix = j % width; let iz = Math.floor(j / width); let best = 5; for (let a of around) { for (let i = 1; i < 5; ++i) { let jx = ix + Math.round(i*step*a[0]); if (jx < 0 || jx >= width) continue; let jz = iz + Math.round(i*step*a[1]); if (jz < 0 || jz >= width) continue; if (borderMap.map[jx+width*jz] & PETRA.outside_Mask) continue; territoryOwner = territoryMap.getOwnerIndex(jx+width*jz); if (alliedVictory && gameState.isPlayerAlly(territoryOwner) || territoryOwner == PlayerID) { best = Math.min(best, i); break; } } if (best == 1) break; } return best; }; /** * get the sum of the resources (except food) around, inside a given radius * resources have a weight (1 if dist=0 and 0 if dist=size) doubled for wood */ PETRA.ConstructionPlan.prototype.getResourcesAround = function(gameState, types, i, radius) { let resourceMaps = gameState.sharedScript.resourceMaps; let w = resourceMaps.wood.width; let cellSize = resourceMaps.wood.cellSize; let size = Math.floor(radius / cellSize); let ix = i % w; let iy = Math.floor(i / w); let total = 0; let nbcell = 0; for (let k of types) { if (k == "food" || !resourceMaps[k]) continue; let weigh0 = k == "wood" ? 2 : 1; for (let dy = 0; dy <= size; ++dy) { let dxmax = size - dy; let ky = iy + dy; if (ky >= 0 && ky < w) { for (let dx = -dxmax; dx <= dxmax; ++dx) { let kx = ix + dx; if (kx < 0 || kx >= w) continue; let ddx = dx > 0 ? dx : -dx; let weight = weigh0 * (dxmax - ddx) / size; total += weight * resourceMaps[k].map[kx + w * ky]; nbcell += weight; } } if (dy == 0) continue; ky = iy - dy; if (ky >= 0 && ky < w) { for (let dx = -dxmax; dx <= dxmax; ++dx) { let kx = ix + dx; if (kx < 0 || kx >= w) continue; let ddx = dx > 0 ? dx : -dx; let weight = weigh0 * (dxmax - ddx) / size; total += weight * resourceMaps[k].map[kx + w * ky]; nbcell += weight; } } } } return nbcell ? total / nbcell : 0; }; PETRA.ConstructionPlan.prototype.isGo = function(gameState) { if (this.goRequirement && this.goRequirement == "houseNeeded") { if (!gameState.ai.HQ.canBuild(gameState, "structures/{civ}/house") && !gameState.ai.HQ.canBuild(gameState, "structures/{civ}/apartment")) return false; if (gameState.getPopulationMax() <= gameState.getPopulationLimit()) return false; let freeSlots = gameState.getPopulationLimit() - gameState.getPopulation(); for (let ent of gameState.getOwnFoundations().values()) { let template = gameState.getBuiltTemplate(ent.templateName()); if (template) freeSlots += template.getPopulationBonus(); } if (gameState.ai.HQ.saveResources) return freeSlots <= 10; if (gameState.getPopulation() > 55) return freeSlots <= 21; if (gameState.getPopulation() > 30) return freeSlots <= 15; return freeSlots <= 10; } return true; }; PETRA.ConstructionPlan.prototype.onStart = function(gameState) { if (this.queueToReset) gameState.ai.queueManager.changePriority(this.queueToReset, gameState.ai.Config.priorities[this.queueToReset]); }; PETRA.ConstructionPlan.prototype.Serialize = function() { return { "category": this.category, "type": this.type, "ID": this.ID, "metadata": this.metadata, "cost": this.cost.Serialize(), "number": this.number, "position": this.position, "goRequirement": this.goRequirement || undefined, "queueToReset": this.queueToReset || undefined }; }; PETRA.ConstructionPlan.prototype.Deserialize = function(gameState, data) { for (let key in data) this[key] = data[key]; this.cost = new API3.Resources(); this.cost.Deserialize(data.cost); }; Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/startingStrategy.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/startingStrategy.js (revision 25875) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/startingStrategy.js (revision 25876) @@ -1,572 +1,508 @@ /** * Determines the strategy to adopt when starting a new game, * depending on the initial conditions */ PETRA.HQ.prototype.gameAnalysis = function(gameState) { // Analysis of the terrain and the different access regions if (!this.regionAnalysis(gameState)) return; this.attackManager.init(gameState); this.buildManager.init(gameState); this.navalManager.init(gameState); this.tradeManager.init(gameState); this.diplomacyManager.init(gameState); // Make a list of buildable structures from the config file this.structureAnalysis(gameState); // Let's get our initial situation here. - let nobase = new PETRA.BaseManager(gameState, this.Config); - nobase.init(gameState); - nobase.accessIndex = 0; - this.baseManagers.push(nobase); // baseManagers[0] will deal with unit/structure without base - let ccEnts = gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")); - for (let cc of ccEnts.values()) - if (cc.foundationProgress() === undefined) - this.createBase(gameState, cc); - else - this.createBase(gameState, cc, "unconstructed"); + this.basesManager.init(gameState); this.updateTerritories(gameState); // Assign entities and resources in the different bases this.assignStartingEntities(gameState); // Sandbox difficulty should not try to expand this.canExpand = this.Config.difficulty != 0; // If no base yet, check if we can construct one. If not, dispatch our units to possible tasks/attacks this.canBuildUnits = true; if (!gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")).hasEntities()) { let template = gameState.applyCiv("structures/{civ}/civil_centre"); if (!gameState.isTemplateAvailable(template) || !gameState.getTemplate(template).available(gameState)) { if (this.Config.debug > 1) API3.warn(" this AI is unable to produce any units"); this.canBuildUnits = false; this.dispatchUnits(gameState); } else this.buildFirstBase(gameState); } // configure our first base strategy - if (this.baseManagers.length > 1) + if (!this.hasPotentialBase()) this.configFirstBase(gameState); }; /** * Assign the starting entities to the different bases */ PETRA.HQ.prototype.assignStartingEntities = function(gameState) { for (let ent of gameState.getOwnEntities().values()) { // do not affect merchant ship immediately to trade as they may-be useful for transport if (ent.hasClasses(["Trader+!Ship"])) this.tradeManager.assignTrader(ent); let pos = ent.position(); if (!pos) { // TODO should support recursive garrisoning. Make a warning for now if (ent.isGarrisonHolder() && ent.garrisoned().length) API3.warn("Petra warning: support for garrisoned units inside garrisoned holders not yet implemented"); continue; } // make sure we have not rejected small regions with units (TODO should probably also check with other non-gaia units) let gamepos = gameState.ai.accessibility.gamePosToMapPos(pos); let index = gamepos[0] + gamepos[1]*gameState.ai.accessibility.width; let land = gameState.ai.accessibility.landPassMap[index]; if (land > 1 && !this.landRegions[land]) this.landRegions[land] = true; let sea = gameState.ai.accessibility.navalPassMap[index]; if (sea > 1 && !this.navalRegions[sea]) this.navalRegions[sea] = true; // if garrisoned units inside, ungarrison them except if a ship in which case we will make a transport // when a construction will start (see createTransportIfNeeded) if (ent.isGarrisonHolder() && ent.garrisoned().length && !ent.hasClass("Ship")) for (let id of ent.garrisoned()) ent.unload(id); - let bestbase; let territorypos = this.territoryMap.gamePosToMapPos(pos); let territoryIndex = territorypos[0] + territorypos[1]*this.territoryMap.width; - for (let i = 1; i < this.baseManagers.length; ++i) - { - let base = this.baseManagers[i]; - if ((!ent.getMetadata(PlayerID, "base") || ent.getMetadata(PlayerID, "base") != base.ID) && - base.territoryIndices.indexOf(territoryIndex) == -1) - continue; - base.assignEntity(gameState, ent); - bestbase = base; - break; - } - if (!bestbase) // entity outside our territory - { - if (ent.hasClass("Structure") && !ent.decaying() && ent.resourceDropsiteTypes()) - bestbase = this.createBase(gameState, ent, "anchorless"); - else - bestbase = PETRA.getBestBase(gameState, ent) || this.baseManagers[0]; - bestbase.assignEntity(gameState, ent); - } - // now assign entities garrisoned inside this entity - if (ent.isGarrisonHolder() && ent.garrisoned().length) - for (let id of ent.garrisoned()) - bestbase.assignEntity(gameState, gameState.getEntityById(id)); - // and find something useful to do if we already have a base - if (pos && bestbase.ID !== this.baseManagers[0].ID) - { - bestbase.assignRolelessUnits(gameState, [ent]); - if (ent.getMetadata(PlayerID, "role") === "worker") - { - bestbase.reassignIdleWorkers(gameState, [ent]); - bestbase.workerObject.update(gameState, ent); - } - } + + this.basesManager.assignEntity(gameState, ent, territoryIndex); } }; /** * determine the main land Index (or water index if none) * as well as the list of allowed (land andf water) regions */ PETRA.HQ.prototype.regionAnalysis = function(gameState) { let accessibility = gameState.ai.accessibility; let landIndex; let seaIndex; let ccEnts = gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")); for (let cc of ccEnts.values()) { let land = accessibility.getAccessValue(cc.position()); if (land > 1) { landIndex = land; break; } } if (!landIndex) { let civ = gameState.getPlayerCiv(); for (let ent of gameState.getOwnEntities().values()) { if (!ent.position() || !ent.hasClass("Unit") && !ent.trainableEntities(civ)) continue; let land = accessibility.getAccessValue(ent.position()); if (land > 1) { landIndex = land; break; } let sea = accessibility.getAccessValue(ent.position(), true); if (!seaIndex && sea > 1) seaIndex = sea; } } if (!landIndex && !seaIndex) { API3.warn("Petra error: it does not know how to interpret this map"); return false; } let passabilityMap = gameState.getPassabilityMap(); let totalSize = passabilityMap.width * passabilityMap.width; let minLandSize = Math.floor(0.1*totalSize); let minWaterSize = Math.floor(0.2*totalSize); let cellArea = passabilityMap.cellSize * passabilityMap.cellSize; for (let i = 0; i < accessibility.regionSize.length; ++i) { if (landIndex && i == landIndex) this.landRegions[i] = true; else if (accessibility.regionType[i] === "land" && cellArea*accessibility.regionSize[i] > 320) { if (landIndex) { let sea = this.getSeaBetweenIndices(gameState, landIndex, i); if (sea && (accessibility.regionSize[i] > minLandSize || accessibility.regionSize[sea] > minWaterSize)) { this.navalMap = true; this.landRegions[i] = true; this.navalRegions[sea] = true; } } else { let traject = accessibility.getTrajectToIndex(seaIndex, i); if (traject && traject.length === 2) { this.navalMap = true; this.landRegions[i] = true; this.navalRegions[seaIndex] = true; } } } else if (accessibility.regionType[i] === "water" && accessibility.regionSize[i] > minWaterSize) { this.navalMap = true; this.navalRegions[i] = true; } else if (accessibility.regionType[i] === "water" && cellArea*accessibility.regionSize[i] > 3600) this.navalRegions[i] = true; } if (this.Config.debug < 3) return true; for (let region in this.landRegions) API3.warn(" >>> zone " + region + " taille " + cellArea*gameState.ai.accessibility.regionSize[region]); API3.warn(" navalMap " + this.navalMap); API3.warn(" landRegions " + uneval(this.landRegions)); API3.warn(" navalRegions " + uneval(this.navalRegions)); return true; }; /** * load units and buildings from the config files * TODO: change that to something dynamic */ PETRA.HQ.prototype.structureAnalysis = function(gameState) { let civref = gameState.playerData.civ; let civ = civref in this.Config.buildings ? civref : 'default'; this.bAdvanced = []; for (let building of this.Config.buildings[civ]) if (gameState.isTemplateAvailable(gameState.applyCiv(building))) this.bAdvanced.push(gameState.applyCiv(building)); }; /** * build our first base * if not enough resource, try first to do a dock */ PETRA.HQ.prototype.buildFirstBase = function(gameState) { if (gameState.ai.queues.civilCentre.hasQueuedUnits()) return; let templateName = gameState.applyCiv("structures/{civ}/civil_centre"); if (gameState.isTemplateDisabled(templateName)) return; let template = gameState.getTemplate(templateName); if (!template) return; let total = gameState.getResources(); let goal = "civil_centre"; if (!total.canAfford(new API3.Resources(template.cost()))) { let totalExpected = gameState.getResources(); // Check for treasures around available in some maps at startup for (let ent of gameState.getOwnUnits().values()) { if (!ent.position()) continue; // If we can get a treasure around, just do it if (ent.isIdle()) PETRA.gatherTreasure(gameState, ent); // Then count the resources from the treasures being collected let treasureId = ent.getMetadata(PlayerID, "treasure"); if (!treasureId) continue; let treasure = gameState.getEntityById(treasureId); if (!treasure) continue; let types = treasure.treasureResources(); for (let type in types) if (type in totalExpected) totalExpected[type] += types[type]; // If we can collect enough resources from these treasures, wait for them. if (totalExpected.canAfford(new API3.Resources(template.cost()))) return; } // not enough resource to build a cc, try with a dock to accumulate resources if none yet if (!this.navalManager.docks.filter(API3.Filters.byClass("Dock")).hasEntities()) { if (gameState.ai.queues.dock.hasQueuedUnits()) return; templateName = gameState.applyCiv("structures/{civ}/dock"); if (gameState.isTemplateDisabled(templateName)) return; template = gameState.getTemplate(templateName); if (!template || !total.canAfford(new API3.Resources(template.cost()))) return; goal = "dock"; } } if (!this.canBuild(gameState, templateName)) return; // We first choose as startingPoint the point where we have the more units let startingPoint = []; for (let ent of gameState.getOwnUnits().values()) { if (!ent.hasClass("Worker")) continue; if (PETRA.isFastMoving(ent)) continue; let pos = ent.position(); if (!pos) { let holder = PETRA.getHolder(gameState, ent); if (!holder || !holder.position()) continue; pos = holder.position(); } let gamepos = gameState.ai.accessibility.gamePosToMapPos(pos); let index = gamepos[0] + gamepos[1] * gameState.ai.accessibility.width; let land = gameState.ai.accessibility.landPassMap[index]; let sea = gameState.ai.accessibility.navalPassMap[index]; let found = false; for (let point of startingPoint) { if (land !== point.land || sea !== point.sea) continue; if (API3.SquareVectorDistance(point.pos, pos) > 2500) continue; point.weight += 1; found = true; break; } if (!found) startingPoint.push({ "pos": pos, "land": land, "sea": sea, "weight": 1 }); } if (!startingPoint.length) return; let imax = 0; for (let i = 1; i < startingPoint.length; ++i) if (startingPoint[i].weight > startingPoint[imax].weight) imax = i; if (goal == "dock") { let sea = startingPoint[imax].sea > 1 ? startingPoint[imax].sea : undefined; gameState.ai.queues.dock.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/dock", { "sea": sea, "proximity": startingPoint[imax].pos })); } else gameState.ai.queues.civilCentre.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}/civil_centre", { "base": -1, "resource": "wood", "proximity": startingPoint[imax].pos })); }; /** * set strategy if game without construction: * - if one of our allies has a cc, affect a small fraction of our army for his defense, the rest will attack * - otherwise all units will attack */ PETRA.HQ.prototype.dispatchUnits = function(gameState) { let allycc = gameState.getExclusiveAllyEntities().filter(API3.Filters.byClass("CivCentre")).toEntityArray(); if (allycc.length) { if (this.Config.debug > 1) API3.warn(" We have allied cc " + allycc.length + " and " + gameState.getOwnUnits().length + " units "); let units = gameState.getOwnUnits(); let num = Math.max(Math.min(Math.round(0.08*(1+this.Config.personality.cooperative)*units.length), 20), 5); let num1 = Math.floor(num / 2); let num2 = num1; // first pass to affect ranged infantry units.filter(API3.Filters.byClasses(["Infantry+Ranged"])).forEach(ent => { if (!num || !num1) return; if (ent.getMetadata(PlayerID, "allied")) return; let access = PETRA.getLandAccess(gameState, ent); for (let cc of allycc) { if (!cc.position() || PETRA.getLandAccess(gameState, cc) != access) continue; --num; --num1; ent.setMetadata(PlayerID, "allied", true); let range = 1.5 * cc.footprintRadius(); ent.moveToRange(cc.position()[0], cc.position()[1], range, range + 5); break; } }); // second pass to affect melee infantry units.filter(API3.Filters.byClasses(["Infantry+Melee"])).forEach(ent => { if (!num || !num2) return; if (ent.getMetadata(PlayerID, "allied")) return; let access = PETRA.getLandAccess(gameState, ent); for (let cc of allycc) { if (!cc.position() || PETRA.getLandAccess(gameState, cc) != access) continue; --num; --num2; ent.setMetadata(PlayerID, "allied", true); let range = 1.5 * cc.footprintRadius(); ent.moveToRange(cc.position()[0], cc.position()[1], range, range + 5); break; } }); // and now complete the affectation, including all support units units.forEach(ent => { if (!num && !ent.hasClass("Support")) return; if (ent.getMetadata(PlayerID, "allied")) return; let access = PETRA.getLandAccess(gameState, ent); for (let cc of allycc) { if (!cc.position() || PETRA.getLandAccess(gameState, cc) != access) continue; if (!ent.hasClass("Support")) --num; ent.setMetadata(PlayerID, "allied", true); let range = 1.5 * cc.footprintRadius(); ent.moveToRange(cc.position()[0], cc.position()[1], range, range + 5); break; } }); } }; /** * configure our first base expansion * - if on a small island, favor fishing * - count the available wood resource, and allow rushes only if enough (we should otherwise favor expansion) */ PETRA.HQ.prototype.configFirstBase = function(gameState) { - if (this.baseManagers.length < 2) + if (!this.hasPotentialBase()) return; this.firstBaseConfig = true; let startingSize = 0; let startingLand = []; for (let region in this.landRegions) { - for (let base of this.baseManagers) + for (const base of this.baseManagers()) { if (!base.anchor || base.accessIndex != +region) continue; startingSize += gameState.ai.accessibility.regionSize[region]; startingLand.push(base.accessIndex); break; } } let cell = gameState.getPassabilityMap().cellSize; startingSize = startingSize * cell * cell; if (this.Config.debug > 1) API3.warn("starting size " + startingSize + "(cut at 24000 for fish pushing)"); if (startingSize < 25000) { this.saveSpace = true; this.Config.Economy.popForDock = Math.min(this.Config.Economy.popForDock, 16); let num = Math.max(this.Config.Economy.targetNumFishers, 2); for (let land of startingLand) { for (let sea of gameState.ai.accessibility.regionLinks[land]) if (gameState.ai.HQ.navalRegions[sea]) this.navalManager.updateFishingBoats(sea, num); } this.maxFields = 1; this.needCorral = true; } else if (startingSize < 60000) this.maxFields = 2; else this.maxFields = false; // - count the available food resource, and react accordingly let startingFood = gameState.getResources().food; - let check = {}; - for (let proxim of ["nearby", "medium", "faraway"]) - { - for (let base of this.baseManagers) - { - for (let supply of base.dropsiteSupplies.food[proxim]) - { - if (check[supply.id]) // avoid double counting as same resource can appear several time - continue; - check[supply.id] = true; - startingFood += supply.ent.resourceSupplyAmount(); - } - } - } + startingFood += this.getTotalResourceLevel(gameState, ["food"], ["nearby", "medium", "faraway"]).food; + if (startingFood < 800) { if (startingSize < 25000) { this.needFish = true; this.Config.Economy.popForDock = 1; } else this.needFarm = true; } // - count the available wood resource, and allow rushes only if enough (we should otherwise favor expansion) let startingWood = gameState.getResources().wood; - check = {}; - for (let proxim of ["nearby", "medium", "faraway"]) - { - for (let base of this.baseManagers) - { - for (let supply of base.dropsiteSupplies.wood[proxim]) - { - if (check[supply.id]) // avoid double counting as same resource can appear several time - continue; - check[supply.id] = true; - startingWood += supply.ent.resourceSupplyAmount(); - } - } - } + startingWood += this.getTotalResourceLevel(gameState, ["wood"], ["nearby", "medium", "faraway"]).wood; + if (this.Config.debug > 1) API3.warn("startingWood: " + startingWood + " (cut at 8500 for no rush and 6000 for saveResources)"); if (startingWood < 6000) { this.saveResources = true; this.Config.Economy.popPhase2 = Math.floor(0.75 * this.Config.Economy.popPhase2); // Switch to town phase sooner to be able to expand if (startingWood < 2000 && this.needFarm) { this.needCorral = true; this.needFarm = false; } } if (startingWood > 8500 && this.canBuildUnits) { let allowed = Math.ceil((startingWood - 8500) / 3000); // Not useful to prepare rushing if too long ceasefire if (gameState.isCeasefireActive()) { if (gameState.ceasefireTimeRemaining > 900) allowed = 0; else if (gameState.ceasefireTimeRemaining > 600 && allowed > 1) allowed = 1; } this.attackManager.setRushes(allowed); } // immediatly build a wood dropsite if possible. if (!gameState.getOwnEntitiesByClass("DropsiteWood", true).hasEntities()) { - const newDP = this.baseManagers[1].findBestDropsiteAndLocation(gameState, "wood"); + const newDP = this.baseManagers()[0].findBestDropsiteAndLocation(gameState, "wood"); if (newDP.quality > 40 && this.canBuild(gameState, newDP.templateName)) { // if we start with enough workers, put our available resources in this first dropsite // same thing if our pop exceed the allowed one, as we will need several houses let numWorkers = gameState.getOwnUnits().filter(API3.Filters.byClass("Worker")).length; if (numWorkers > 12 && newDP.quality > 60 || gameState.getPopulation() > gameState.getPopulationLimit() + 20) { const cost = new API3.Resources(gameState.getTemplate(newDP.templateName).cost()); gameState.ai.queueManager.setAccounts(gameState, cost, "dropsites"); } - gameState.ai.queues.dropsites.addPlan(new PETRA.ConstructionPlan(gameState, newDP.templateName, { "base": this.baseManagers[1].ID }, newDP.pos)); + gameState.ai.queues.dropsites.addPlan(new PETRA.ConstructionPlan(gameState, newDP.templateName, { "base": this.baseManagers()[0].ID }, newDP.pos)); } } // and build immediately a corral if needed if (this.needCorral) { const template = gameState.applyCiv("structures/{civ}/corral"); if (!gameState.getOwnEntitiesByClass("Corral", true).hasEntities() && this.canBuild(gameState, template)) - gameState.ai.queues.corral.addPlan(new PETRA.ConstructionPlan(gameState, template, { "base": this.baseManagers[1].ID })); + gameState.ai.queues.corral.addPlan(new PETRA.ConstructionPlan(gameState, template, { "base": this.baseManagers()[0].ID })); } }; Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/transportPlan.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/transportPlan.js (revision 25875) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/transportPlan.js (revision 25876) @@ -1,723 +1,723 @@ /** * Describes a transport plan * Constructor assign units (units is an ID array), a destination (position). * The naval manager will try to deal with it accordingly. * * By this I mean that the naval manager will find how to go from access point 1 to access point 2 * and then carry units from there. * * Note: only assign it units currently over land, or it won't work. * Also: destination should probably be land, otherwise the units will be lost at sea. * * metadata for units: * transport = this.ID * onBoard = ship.id() when affected to a ship but not yet garrisoned * = "onBoard" when garrisoned in a ship * = undefined otherwise * endPos = position of destination * * metadata for ships * transporter = this.ID */ PETRA.TransportPlan = function(gameState, units, startIndex, endIndex, endPos, ship) { this.ID = gameState.ai.uniqueIDs.transports++; this.debug = gameState.ai.Config.debug; this.flotilla = false; // when false, only one ship per transport ... not yet tested when true this.endPos = endPos; this.endIndex = endIndex; this.startIndex = startIndex; // TODO only cases with land-sea-land are allowed for the moment // we could also have land-sea-land-sea-land if (startIndex == 1) { // special transport from already garrisoned ship if (!ship) { this.failed = true; return false; } this.sea = ship.getMetadata(PlayerID, "sea"); ship.setMetadata(PlayerID, "transporter", this.ID); ship.setStance("none"); for (let ent of units) ent.setMetadata(PlayerID, "onBoard", "onBoard"); } else { this.sea = gameState.ai.HQ.getSeaBetweenIndices(gameState, startIndex, endIndex); if (!this.sea) { this.failed = true; if (this.debug > 1) API3.warn("transport plan with bad path: startIndex " + startIndex + " endIndex " + endIndex); return false; } } for (let ent of units) { ent.setMetadata(PlayerID, "transport", this.ID); ent.setMetadata(PlayerID, "endPos", endPos); } if (this.debug > 1) API3.warn("Starting a new transport plan with ID " + this.ID + " to index " + endIndex + " with units length " + units.length); this.state = "boarding"; this.boardingPos = {}; this.needTransportShips = ship === undefined; this.nTry = {}; return true; }; PETRA.TransportPlan.prototype.init = function(gameState) { this.units = gameState.getOwnUnits().filter(API3.Filters.byMetadata(PlayerID, "transport", this.ID)); this.ships = gameState.ai.HQ.navalManager.ships.filter(API3.Filters.byMetadata(PlayerID, "transporter", this.ID)); this.transportShips = gameState.ai.HQ.navalManager.transportShips.filter(API3.Filters.byMetadata(PlayerID, "transporter", this.ID)); this.units.registerUpdates(); this.ships.registerUpdates(); this.transportShips.registerUpdates(); this.boardingRange = 18*18; // TODO compute it from the ship clearance and garrison range }; /** count available slots */ PETRA.TransportPlan.prototype.countFreeSlots = function() { let slots = 0; for (let ship of this.transportShips.values()) slots += this.countFreeSlotsOnShip(ship); return slots; }; PETRA.TransportPlan.prototype.countFreeSlotsOnShip = function(ship) { if (ship.hitpoints() < ship.garrisonEjectHealth() * ship.maxHitpoints()) return 0; let occupied = ship.garrisoned().length + this.units.filter(API3.Filters.byMetadata(PlayerID, "onBoard", ship.id())).length; return Math.max(ship.garrisonMax() - occupied, 0); }; PETRA.TransportPlan.prototype.assignUnitToShip = function(gameState, ent) { if (this.needTransportShips) return; for (let ship of this.transportShips.values()) { if (this.countFreeSlotsOnShip(ship) == 0) continue; ent.setMetadata(PlayerID, "onBoard", ship.id()); if (this.debug > 1) { if (ent.getMetadata(PlayerID, "role") == "attack") Engine.PostCommand(PlayerID, { "type": "set-shading-color", "entities": [ent.id()], "rgb": [2, 0, 0] }); else Engine.PostCommand(PlayerID, { "type": "set-shading-color", "entities": [ent.id()], "rgb": [0, 2, 0] }); } return; } if (this.flotilla) { this.needTransportShips = true; return; } if (!this.needSplit) this.needSplit = [ent]; else this.needSplit.push(ent); }; PETRA.TransportPlan.prototype.assignShip = function(gameState) { let pos; // choose a unit of this plan not yet assigned to a ship for (let ent of this.units.values()) { if (!ent.position() || ent.getMetadata(PlayerID, "onBoard") !== undefined) continue; pos = ent.position(); break; } // and choose the nearest available ship from this unit let distmin = Math.min(); let nearest; gameState.ai.HQ.navalManager.seaTransportShips[this.sea].forEach(ship => { if (ship.getMetadata(PlayerID, "transporter")) return; if (pos) { let dist = API3.SquareVectorDistance(pos, ship.position()); if (dist > distmin) return; distmin = dist; nearest = ship; } else if (!nearest) nearest = ship; }); if (!nearest) return false; nearest.setMetadata(PlayerID, "transporter", this.ID); nearest.setStance("none"); this.ships.updateEnt(nearest); this.transportShips.updateEnt(nearest); this.needTransportShips = false; return true; }; /** add a unit to this plan */ PETRA.TransportPlan.prototype.addUnit = function(unit, endPos) { unit.setMetadata(PlayerID, "transport", this.ID); unit.setMetadata(PlayerID, "endPos", endPos); this.units.updateEnt(unit); }; /** remove a unit from this plan, if not yet on board */ PETRA.TransportPlan.prototype.removeUnit = function(gameState, unit) { let shipId = unit.getMetadata(PlayerID, "onBoard"); if (shipId == "onBoard") return; // too late, already onBoard else if (shipId !== undefined) unit.stopMoving(); // cancel the garrison order unit.setMetadata(PlayerID, "transport", undefined); unit.setMetadata(PlayerID, "endPos", undefined); this.units.updateEnt(unit); if (shipId) { unit.setMetadata(PlayerID, "onBoard", undefined); let ship = gameState.getEntityById(shipId); if (ship && !ship.garrisoned().length && !this.units.filter(API3.Filters.byMetadata(PlayerID, "onBoard", shipId)).length) { this.releaseShip(ship); this.ships.updateEnt(ship); this.transportShips.updateEnt(ship); } } }; PETRA.TransportPlan.prototype.releaseShip = function(ship) { if (ship.getMetadata(PlayerID, "transporter") != this.ID) { API3.warn(" Petra: try removing a transporter ship with " + ship.getMetadata(PlayerID, "transporter") + " from " + this.ID + " and stance " + ship.getStance()); return; } let defaultStance = ship.get("UnitAI/DefaultStance"); if (defaultStance) ship.setStance(defaultStance); ship.setMetadata(PlayerID, "transporter", undefined); if (ship.getMetadata(PlayerID, "role") == "switchToTrader") ship.setMetadata(PlayerID, "role", "trader"); }; PETRA.TransportPlan.prototype.releaseAll = function() { for (let ship of this.ships.values()) this.releaseShip(ship); for (let ent of this.units.values()) { ent.setMetadata(PlayerID, "endPos", undefined); ent.setMetadata(PlayerID, "onBoard", undefined); ent.setMetadata(PlayerID, "transport", undefined); // TODO if the index of the endPos of the entity is !=, // require again another transport (we could need land-sea-land-sea-land) } this.transportShips.unregister(); this.ships.unregister(); this.units.unregister(); }; /** TODO not currently used ... to be fixed */ PETRA.TransportPlan.prototype.cancelTransport = function(gameState) { let ent = this.units.toEntityArray()[0]; let base = gameState.ai.HQ.getBaseByID(ent.getMetadata(PlayerID, "base")); if (!base.anchor || !base.anchor.position()) { - for (let newbase of gameState.ai.HQ.baseManagers) + for (const newbase of gameState.ai.HQ.baseManagers()) { if (!newbase.anchor || !newbase.anchor.position()) continue; ent.setMetadata(PlayerID, "base", newbase.ID); base = newbase; break; } if (!base.anchor || !base.anchor.position()) return false; this.units.forEach(unit => { unit.setMetadata(PlayerID, "base", base.ID); }); } this.endIndex = this.startIndex; this.endPos = base.anchor.position(); this.canceled = true; return true; }; /** * try to move on. There are two states: * - "boarding" means we're trying to board units onto our ships * - "sailing" means we're moving ships and eventually unload units * - then the plan is cleared */ PETRA.TransportPlan.prototype.update = function(gameState) { if (this.state == "boarding") this.onBoarding(gameState); else if (this.state == "sailing") this.onSailing(gameState); return this.units.length; }; PETRA.TransportPlan.prototype.onBoarding = function(gameState) { let ready = true; let time = gameState.ai.elapsedTime; let shipTested = {}; for (let ent of this.units.values()) { if (!ent.getMetadata(PlayerID, "onBoard")) { ready = false; this.assignUnitToShip(gameState, ent); if (ent.getMetadata(PlayerID, "onBoard")) { let shipId = ent.getMetadata(PlayerID, "onBoard"); let ship = gameState.getEntityById(shipId); if (!this.boardingPos[shipId]) { this.boardingPos[shipId] = this.getBoardingPos(gameState, ship, this.startIndex, this.sea, ent.position(), false); ship.move(this.boardingPos[shipId][0], this.boardingPos[shipId][1]); ship.setMetadata(PlayerID, "timeGarrison", time); } ent.garrison(ship); ent.setMetadata(PlayerID, "timeGarrison", time); ent.setMetadata(PlayerID, "posGarrison", ent.position()); } } else if (ent.getMetadata(PlayerID, "onBoard") != "onBoard" && !this.isOnBoard(ent)) { ready = false; let shipId = ent.getMetadata(PlayerID, "onBoard"); let ship = gameState.getEntityById(shipId); if (!ship) // the ship must have been destroyed { ent.setMetadata(PlayerID, "onBoard", undefined); continue; } let distShip = API3.SquareVectorDistance(this.boardingPos[shipId], ship.position()); if (!shipTested[shipId] && distShip > this.boardingRange) { shipTested[shipId] = true; let retry = false; let unitAIState = ship.unitAIState(); if (unitAIState == "INDIVIDUAL.WALKING" || unitAIState == "INDIVIDUAL.PICKUP.APPROACHING") { if (time - ship.getMetadata(PlayerID, "timeGarrison") > 2) { let oldPos = ent.getMetadata(PlayerID, "posGarrison"); let newPos = ent.position(); if (oldPos[0] == newPos[0] && oldPos[1] == newPos[1]) retry = true; ent.setMetadata(PlayerID, "posGarrison", newPos); ent.setMetadata(PlayerID, "timeGarrison", time); } } else if (unitAIState != "INDIVIDUAL.PICKUP.LOADING" && time - ship.getMetadata(PlayerID, "timeGarrison") > 5 || time - ship.getMetadata(PlayerID, "timeGarrison") > 8) { retry = true; ent.setMetadata(PlayerID, "timeGarrison", time); } if (retry) { if (!this.nTry[shipId]) this.nTry[shipId] = 1; else ++this.nTry[shipId]; if (this.nTry[shipId] > 1) // we must have been blocked by something ... try with another boarding point { this.nTry[shipId] = 0; if (this.debug > 1) API3.warn("ship " + shipId + " new attempt for a landing point "); this.boardingPos[shipId] = this.getBoardingPos(gameState, ship, this.startIndex, this.sea, undefined, false); } ship.move(this.boardingPos[shipId][0], this.boardingPos[shipId][1]); ship.setMetadata(PlayerID, "timeGarrison", time); } } if (time - ent.getMetadata(PlayerID, "timeGarrison") > 2) { let oldPos = ent.getMetadata(PlayerID, "posGarrison"); let newPos = ent.position(); if (oldPos[0] == newPos[0] && oldPos[1] == newPos[1]) { if (distShip < this.boardingRange) // looks like we are blocked ... try to go out of this trap { if (!this.nTry[ent.id()]) this.nTry[ent.id()] = 1; else ++this.nTry[ent.id()]; if (this.nTry[ent.id()] > 5) { if (this.debug > 1) API3.warn("unit blocked, but no ways out of the trap ... destroy it"); this.resetUnit(gameState, ent); ent.destroy(); continue; } if (this.nTry[ent.id()] > 1) ent.moveToRange(newPos[0], newPos[1], 30, 35); ent.garrison(ship, true); } else if (API3.SquareVectorDistance(this.boardingPos[shipId], newPos) > 225) ent.moveToRange(this.boardingPos[shipId][0], this.boardingPos[shipId][1], 0, 15); } else this.nTry[ent.id()] = 0; ent.setMetadata(PlayerID, "timeGarrison", time); ent.setMetadata(PlayerID, "posGarrison", ent.position()); } } } if (this.needSplit) { gameState.ai.HQ.navalManager.splitTransport(gameState, this); this.needSplit = undefined; } if (!ready) return; for (let ship of this.ships.values()) { this.boardingPos[ship.id()] = undefined; this.boardingPos[ship.id()] = this.getBoardingPos(gameState, ship, this.endIndex, this.sea, this.endPos, true); ship.move(this.boardingPos[ship.id()][0], this.boardingPos[ship.id()][1]); } this.state = "sailing"; this.nTry = {}; this.unloaded = []; this.recovered = []; }; /** tell if a unit is garrisoned in one of the ships of this plan, and update its metadata if yes */ PETRA.TransportPlan.prototype.isOnBoard = function(ent) { for (let ship of this.transportShips.values()) { if (ship.garrisoned().indexOf(ent.id()) == -1) continue; ent.setMetadata(PlayerID, "onBoard", "onBoard"); return true; } return false; }; /** when avoidEnnemy is true, we try to not board/unboard in ennemy territory */ PETRA.TransportPlan.prototype.getBoardingPos = function(gameState, ship, landIndex, seaIndex, destination, avoidEnnemy) { if (!gameState.ai.HQ.navalManager.landingZones[landIndex]) { API3.warn(" >>> no landing zone for land " + landIndex); return destination; } else if (!gameState.ai.HQ.navalManager.landingZones[landIndex][seaIndex]) { API3.warn(" >>> no landing zone for land " + landIndex + " and sea " + seaIndex); return destination; } let startPos = ship.position(); let distmin = Math.min(); let posmin = destination; let width = gameState.getPassabilityMap().width; let cell = gameState.getPassabilityMap().cellSize; let alliedDocks = gameState.getAllyStructures().filter(API3.Filters.and( API3.Filters.byClass("Dock"), API3.Filters.byMetadata(PlayerID, "sea", seaIndex))).toEntityArray(); for (let i of gameState.ai.HQ.navalManager.landingZones[landIndex][seaIndex]) { let pos = [i%width+0.5, Math.floor(i/width)+0.5]; pos = [cell*pos[0], cell*pos[1]]; let dist = API3.VectorDistance(startPos, pos); if (destination) dist += API3.VectorDistance(pos, destination); if (avoidEnnemy) { let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(pos); if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) dist += 100000000; } // require a small distance between all ships of the transport plan to avoid path finder problems // this is also used when the ship is blocked and we want to find a new boarding point for (let shipId in this.boardingPos) if (this.boardingPos[shipId] !== undefined && API3.SquareVectorDistance(this.boardingPos[shipId], pos) < this.boardingRange) dist += 1000000; // and not too near our allied docks to not disturb naval traffic let distSquare; for (let dock of alliedDocks) { if (dock.foundationProgress() !== undefined) distSquare = 900; else distSquare = 4900; let dockDist = API3.SquareVectorDistance(dock.position(), pos); if (dockDist < distSquare) dist += 100000 * (distSquare - dockDist) / distSquare; } if (dist > distmin) continue; distmin = dist; posmin = pos; } // We should always have either destination or the previous boardingPos defined // so let's return this value if everything failed if (!posmin && this.boardingPos[ship.id()]) posmin = this.boardingPos[ship.id()]; return posmin; }; PETRA.TransportPlan.prototype.onSailing = function(gameState) { // Check that the units recovered on the previous turn have been reloaded for (let recov of this.recovered) { let ent = gameState.getEntityById(recov.entId); if (!ent) // entity destroyed continue; if (!ent.position()) // reloading succeeded ... move a bit the ship before trying again { let ship = gameState.getEntityById(recov.shipId); if (ship) ship.moveApart(recov.entPos, 15); continue; } if (this.debug > 1) API3.warn(">>> transport " + this.ID + " reloading failed ... <<<"); // destroy the unit if inaccessible otherwise leave it there let index = PETRA.getLandAccess(gameState, ent); if (gameState.ai.HQ.landRegions[index]) { if (this.debug > 1) API3.warn(" recovered entity kept " + ent.id()); this.resetUnit(gameState, ent); // TODO we should not destroy it, but now the unit could still be reloaded on the next turn // and mess everything ent.destroy(); } else { if (this.debug > 1) API3.warn("recovered entity destroyed " + ent.id()); this.resetUnit(gameState, ent); ent.destroy(); } } this.recovered = []; // Check that the units unloaded on the previous turn have been really unloaded and in the right position let shipsToMove = {}; for (let entId of this.unloaded) { let ent = gameState.getEntityById(entId); if (!ent) // entity destroyed continue; else if (!ent.position()) // unloading failed { let ship = gameState.getEntityById(ent.getMetadata(PlayerID, "onBoard")); if (ship) { if (ship.garrisoned().indexOf(entId) != -1) ent.setMetadata(PlayerID, "onBoard", "onBoard"); else { API3.warn("Petra transportPlan problem: unit not on ship without position ???"); this.resetUnit(gameState, ent); ent.destroy(); } } else { API3.warn("Petra transportPlan problem: unit on ship, but no ship ???"); this.resetUnit(gameState, ent); ent.destroy(); } } else if (PETRA.getLandAccess(gameState, ent) != this.endIndex) { // unit unloaded on a wrong region - try to regarrison it and move a bit the ship if (this.debug > 1) API3.warn(">>> unit unloaded on a wrong region ! try to garrison it again <<<"); let ship = gameState.getEntityById(ent.getMetadata(PlayerID, "onBoard")); if (ship && !this.canceled) { shipsToMove[ship.id()] = ship; this.recovered.push({ "entId": ent.id(), "entPos": ent.position(), "shipId": ship.id() }); ent.garrison(ship); ent.setMetadata(PlayerID, "onBoard", "onBoard"); } else { if (this.debug > 1) API3.warn("no way ... we destroy it"); this.resetUnit(gameState, ent); ent.destroy(); } } else { // And make some room for other units let pos = ent.position(); let goal = ent.getMetadata(PlayerID, "endPos"); let dist = goal ? API3.VectorDistance(pos, goal) : 0; if (dist > 30) ent.moveToRange(goal[0], goal[1], dist-25, dist-20); else ent.moveToRange(pos[0], pos[1], 20, 25); ent.setMetadata(PlayerID, "transport", undefined); ent.setMetadata(PlayerID, "onBoard", undefined); ent.setMetadata(PlayerID, "endPos", undefined); } } for (let shipId in shipsToMove) { this.boardingPos[shipId] = this.getBoardingPos(gameState, shipsToMove[shipId], this.endIndex, this.sea, this.endPos, true); shipsToMove[shipId].move(this.boardingPos[shipId][0], this.boardingPos[shipId][1]); } this.unloaded = []; if (this.canceled) { for (let ship of this.ships.values()) { this.boardingPos[ship.id()] = undefined; this.boardingPos[ship.id()] = this.getBoardingPos(gameState, ship, this.endIndex, this.sea, this.endPos, true); ship.move(this.boardingPos[ship.id()][0], this.boardingPos[ship.id()][1]); } this.canceled = undefined; } for (let ship of this.transportShips.values()) { if (ship.unitAIState() == "INDIVIDUAL.WALKING") continue; let shipId = ship.id(); let dist = API3.SquareVectorDistance(ship.position(), this.boardingPos[shipId]); let remaining = 0; for (let entId of ship.garrisoned()) { let ent = gameState.getEntityById(entId); if (!ent.getMetadata(PlayerID, "transport")) continue; remaining++; if (dist < 625) { ship.unload(entId); this.unloaded.push(entId); ent.setMetadata(PlayerID, "onBoard", shipId); } } let recovering = 0; for (let recov of this.recovered) if (recov.shipId == shipId) recovering++; if (!remaining && !recovering) // when empty, release the ship and move apart to leave room for other ships. TODO fight { ship.moveApart(this.boardingPos[shipId], 30); this.releaseShip(ship); continue; } if (dist > this.boardingRange) { if (!this.nTry[shipId]) this.nTry[shipId] = 1; else ++this.nTry[shipId]; if (this.nTry[shipId] > 2) // we must have been blocked by something ... try with another boarding point { this.nTry[shipId] = 0; if (this.debug > 1) API3.warn(shipId + " new attempt for a landing point "); this.boardingPos[shipId] = this.getBoardingPos(gameState, ship, this.endIndex, this.sea, undefined, true); } ship.move(this.boardingPos[shipId][0], this.boardingPos[shipId][1]); } } }; PETRA.TransportPlan.prototype.resetUnit = function(gameState, ent) { ent.setMetadata(PlayerID, "transport", undefined); ent.setMetadata(PlayerID, "onBoard", undefined); ent.setMetadata(PlayerID, "endPos", undefined); // if from an army or attack, remove it if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") >= 0) { let attackPlan = gameState.ai.HQ.attackManager.getPlan(ent.getMetadata(PlayerID, "plan")); if (attackPlan) attackPlan.removeUnit(ent, true); } if (ent.getMetadata(PlayerID, "PartOfArmy")) { let army = gameState.ai.HQ.defenseManager.getArmy(ent.getMetadata(PlayerID, "PartOfArmy")); if (army) army.removeOwn(gameState, ent.id()); } }; PETRA.TransportPlan.prototype.Serialize = function() { return { "ID": this.ID, "flotilla": this.flotilla, "endPos": this.endPos, "endIndex": this.endIndex, "startIndex": this.startIndex, "sea": this.sea, "state": this.state, "boardingPos": this.boardingPos, "needTransportShips": this.needTransportShips, "nTry": this.nTry, "canceled": this.canceled, "unloaded": this.unloaded, "recovered": this.recovered }; }; PETRA.TransportPlan.prototype.Deserialize = function(data) { for (let key in data) this[key] = data[key]; this.failed = false; }; Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/worker.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/worker.js (revision 25875) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/worker.js (revision 25876) @@ -1,1103 +1,1104 @@ /** * This class makes a worker do as instructed by the economy manager */ PETRA.Worker = function(base) { this.ent = undefined; this.base = base; this.baseID = base.ID; }; PETRA.Worker.prototype.update = function(gameState, ent) { if (!ent.position() || ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3) return; let subrole = ent.getMetadata(PlayerID, "subrole"); // If we are waiting for a transport or we are sailing, just wait if (ent.getMetadata(PlayerID, "transport") !== undefined) { // Except if builder with their foundation destroyed, in which case cancel the transport if not yet on board if (subrole == "builder" && ent.getMetadata(PlayerID, "target-foundation") !== undefined) { let plan = gameState.ai.HQ.navalManager.getPlan(ent.getMetadata(PlayerID, "transport")); let target = gameState.getEntityById(ent.getMetadata(PlayerID, "target-foundation")); if (!target && plan && plan.state == "boarding" && ent.position()) plan.removeUnit(gameState, ent); } // and gatherer if there are no more dropsite accessible in the base the ent is going to if (subrole == "gatherer" || subrole == "hunter") { let plan = gameState.ai.HQ.navalManager.getPlan(ent.getMetadata(PlayerID, "transport")); if (plan.state == "boarding" && ent.position()) { let hasDropsite = false; let gatherType = ent.getMetadata(PlayerID, "gather-type") || "food"; for (let structure of gameState.getOwnStructures().values()) { if (PETRA.getLandAccess(gameState, structure) != plan.endIndex) continue; let resourceDropsiteTypes = PETRA.getBuiltEntity(gameState, structure).resourceDropsiteTypes(); if (!resourceDropsiteTypes || resourceDropsiteTypes.indexOf(gatherType) == -1) continue; hasDropsite = true; break; } if (!hasDropsite) { for (let unit of gameState.getOwnUnits().filter(API3.Filters.byClass("Support")).values()) { if (!unit.position() || PETRA.getLandAccess(gameState, unit) != plan.endIndex) continue; let resourceDropsiteTypes = unit.resourceDropsiteTypes(); if (!resourceDropsiteTypes || resourceDropsiteTypes.indexOf(gatherType) == -1) continue; hasDropsite = true; break; } } if (!hasDropsite) plan.removeUnit(gameState, ent); } } if (ent.getMetadata(PlayerID, "transport") !== undefined) return; } this.entAccess = PETRA.getLandAccess(gameState, ent); - // base 0 for unassigned entities has no accessIndex, so take the one from the entity - if (this.baseID == gameState.ai.HQ.baseManagers[0].ID) + // Base for unassigned entities has no accessIndex, so take the one from the entity. + if (this.baseID == gameState.ai.HQ.basesManager.baselessBase().ID) this.baseAccess = this.entAccess; else this.baseAccess = this.base.accessIndex; if (!subrole) // subrole may-be undefined after a transport, garrisoning, army, ... { ent.setMetadata(PlayerID, "subrole", "idle"); this.base.reassignIdleWorkers(gameState, [ent]); this.update(gameState, ent); return; } this.ent = ent; let unitAIState = ent.unitAIState(); if ((subrole == "hunter" || subrole == "gatherer") && (unitAIState == "INDIVIDUAL.GATHER.GATHERING" || unitAIState == "INDIVIDUAL.GATHER.APPROACHING" || unitAIState == "INDIVIDUAL.COMBAT.APPROACHING")) { if (this.isInaccessibleSupply(gameState)) { if (this.retryWorking(gameState, subrole)) return; ent.stopMoving(); } if (unitAIState == "INDIVIDUAL.COMBAT.APPROACHING" && ent.unitAIOrderData().length) { let orderData = ent.unitAIOrderData()[0]; if (orderData && orderData.target) { // Check that we have not drifted too far when hunting let target = gameState.getEntityById(orderData.target); if (target && target.resourceSupplyType() && target.resourceSupplyType().generic == "food") { let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(target.position()); if (gameState.isPlayerEnemy(territoryOwner)) { if (this.retryWorking(gameState, subrole)) return; ent.stopMoving(); } else if (!gameState.isPlayerAlly(territoryOwner)) { let distanceSquare = PETRA.isFastMoving(ent) ? 90000 : 30000; let targetAccess = PETRA.getLandAccess(gameState, target); let foodDropsites = gameState.playerData.hasSharedDropsites ? gameState.getAnyDropsites("food") : gameState.getOwnDropsites("food"); let hasFoodDropsiteWithinDistance = false; for (let dropsite of foodDropsites.values()) { if (!dropsite.position()) continue; let owner = dropsite.owner(); // owner != PlayerID can only happen when hasSharedDropsites == true, so no need to test it again if (owner != PlayerID && (!dropsite.isSharedDropsite() || !gameState.isPlayerMutualAlly(owner))) continue; if (targetAccess != PETRA.getLandAccess(gameState, dropsite)) continue; if (API3.SquareVectorDistance(target.position(), dropsite.position()) < distanceSquare) { hasFoodDropsiteWithinDistance = true; break; } } if (!hasFoodDropsiteWithinDistance) { if (this.retryWorking(gameState, subrole)) return; ent.stopMoving(); } } } } } } else if (ent.getMetadata(PlayerID, "approachingTarget")) { ent.setMetadata(PlayerID, "approachingTarget", undefined); ent.setMetadata(PlayerID, "alreadyTried", undefined); } let unitAIStateOrder = unitAIState.split(".")[1]; // If we're fighting or hunting, let's not start gathering except if inaccessible target // but for fishers where UnitAI must have made us target a moving whale. // Also, if we are attacking, do not capture if (unitAIStateOrder == "COMBAT") { if (subrole == "fisher") this.startFishing(gameState); else if (unitAIState == "INDIVIDUAL.COMBAT.APPROACHING" && ent.unitAIOrderData().length && !ent.getMetadata(PlayerID, "PartOfArmy")) { let orderData = ent.unitAIOrderData()[0]; if (orderData && orderData.target) { let target = gameState.getEntityById(orderData.target); if (target && (!target.position() || PETRA.getLandAccess(gameState, target) != this.entAccess)) { if (this.retryWorking(gameState, subrole)) return; ent.stopMoving(); } } } else if (unitAIState == "INDIVIDUAL.COMBAT.ATTACKING" && ent.unitAIOrderData().length && !ent.getMetadata(PlayerID, "PartOfArmy")) { let orderData = ent.unitAIOrderData()[0]; if (orderData && orderData.target && orderData.attackType && orderData.attackType == "Capture") { // If we are here, an enemy structure must have targeted one of our workers // and UnitAI sent it fight back with allowCapture=true let target = gameState.getEntityById(orderData.target); if (target && target.owner() > 0 && !gameState.isPlayerAlly(target.owner())) ent.attack(orderData.target, PETRA.allowCapture(gameState, ent, target)); } } return; } // Okay so we have a few tasks. // If we're gathering, we'll check that we haven't run idle. // And we'll also check that we're gathering a resource we want to gather. if (subrole == "gatherer") { if (ent.isIdle()) { // if we aren't storing resources or it's the same type as what we're about to gather, // let's just pick a new resource. // TODO if we already carry the max we can -> returnresources if (!ent.resourceCarrying() || !ent.resourceCarrying().length || ent.resourceCarrying()[0].type == ent.getMetadata(PlayerID, "gather-type")) { this.startGathering(gameState); } else if (!PETRA.returnResources(gameState, ent)) // try to deposit resources { // no dropsite, abandon old resources and start gathering new ones this.startGathering(gameState); } } else if (unitAIStateOrder == "GATHER") { // we're already gathering. But let's check if there is nothing better // in case UnitAI did something bad if (ent.unitAIOrderData().length) { let supplyId = ent.unitAIOrderData()[0].target; let supply = gameState.getEntityById(supplyId); if (supply && !supply.hasClasses(["Field", "Animal"]) && supplyId != ent.getMetadata(PlayerID, "supply")) { - let nbGatherers = supply.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(supplyId); + const nbGatherers = supply.resourceSupplyNumGatherers() + this.base.GetTCGatherer(supplyId); if (nbGatherers > 1 && supply.resourceSupplyAmount()/nbGatherers < 30) { - gameState.ai.HQ.RemoveTCGatherer(supplyId); + this.base.RemoveTCGatherer(supplyId); this.startGathering(gameState); } else { let gatherType = ent.getMetadata(PlayerID, "gather-type"); let nearby = this.base.dropsiteSupplies[gatherType].nearby; if (nearby.some(sup => sup.id == supplyId)) ent.setMetadata(PlayerID, "supply", supplyId); else if (nearby.length) { - gameState.ai.HQ.RemoveTCGatherer(supplyId); + this.base.RemoveTCGatherer(supplyId); this.startGathering(gameState); } else { let medium = this.base.dropsiteSupplies[gatherType].medium; if (medium.length && !medium.some(sup => sup.id == supplyId)) { - gameState.ai.HQ.RemoveTCGatherer(supplyId); + this.base.RemoveTCGatherer(supplyId); this.startGathering(gameState); } else ent.setMetadata(PlayerID, "supply", supplyId); } } } } if (unitAIState == "INDIVIDUAL.GATHER.RETURNINGRESOURCE.APPROACHING") { if (gameState.ai.playedTurn % 10 == 0) { // Check from time to time that UnitAI does not send us to an inaccessible dropsite let dropsite = gameState.getEntityById(ent.unitAIOrderData()[0].target); if (dropsite && dropsite.position() && this.entAccess != PETRA.getLandAccess(gameState, dropsite)) PETRA.returnResources(gameState, this.ent); } // If gathering a sparse resource, we may have been sent to a faraway resource if the one nearby was full. // Let's check if it is still the case. If so, we reset its metadata supplyId so that the unit will be // reordered to gather after having returned the resources (when comparing its supplyId with the UnitAI one). let gatherType = ent.getMetadata(PlayerID, "gather-type"); let influenceGroup = Resources.GetResource(gatherType).aiAnalysisInfluenceGroup; if (influenceGroup && influenceGroup == "sparse") { let supplyId = ent.getMetadata(PlayerID, "supply"); if (supplyId) { let nearby = this.base.dropsiteSupplies[gatherType].nearby; if (!nearby.some(sup => sup.id == supplyId)) { if (nearby.length) ent.setMetadata(PlayerID, "supply", undefined); else { let medium = this.base.dropsiteSupplies[gatherType].medium; if (!medium.some(sup => sup.id == supplyId) && medium.length) ent.setMetadata(PlayerID, "supply", undefined); } } } } } } } else if (subrole == "builder") { if (unitAIStateOrder == "REPAIR") { // Update our target in case UnitAI sent us to a different foundation because of autocontinue // and abandon it if UnitAI has sent us to build a field (as we build them only when needed) if (ent.unitAIOrderData()[0] && ent.unitAIOrderData()[0].target && ent.getMetadata(PlayerID, "target-foundation") != ent.unitAIOrderData()[0].target) { let targetId = ent.unitAIOrderData()[0].target; let target = gameState.getEntityById(targetId); if (target && !target.hasClass("Field")) { ent.setMetadata(PlayerID, "target-foundation", targetId); return; } ent.setMetadata(PlayerID, "target-foundation", undefined); ent.setMetadata(PlayerID, "subrole", "idle"); ent.stopMoving(); - if (this.baseID != gameState.ai.HQ.baseManagers[0].ID) + if (this.baseID != gameState.ai.HQ.basesManager.baselessBase().ID) { // reassign it to something useful this.base.reassignIdleWorkers(gameState, [ent]); this.update(gameState, ent); return; } } // Otherwise check that the target still exists (useful in REPAIR.APPROACHING) let targetId = ent.getMetadata(PlayerID, "target-foundation"); if (targetId && gameState.getEntityById(targetId)) return; ent.stopMoving(); } // okay so apparently we aren't working. // Unless we've been explicitely told to keep our role, make us idle. let target = gameState.getEntityById(ent.getMetadata(PlayerID, "target-foundation")); if (!target || target.foundationProgress() === undefined && target.needsRepair() === false) { ent.setMetadata(PlayerID, "subrole", "idle"); ent.setMetadata(PlayerID, "target-foundation", undefined); - if (this.baseID != gameState.ai.HQ.baseManagers[0].ID) + if (this.baseID != gameState.ai.HQ.basesManager.baselessBase().ID) { // reassign it to something useful this.base.reassignIdleWorkers(gameState, [ent]); this.update(gameState, ent); return; } } else { let goalAccess = PETRA.getLandAccess(gameState, target); let queued = PETRA.returnResources(gameState, ent); if (this.entAccess == goalAccess) ent.repair(target, target.hasClass("House"), queued); // autocontinue=true for houses else gameState.ai.HQ.navalManager.requireTransport(gameState, ent, this.entAccess, goalAccess, target.position()); } } else if (subrole == "hunter") { let lastHuntSearch = ent.getMetadata(PlayerID, "lastHuntSearch"); if (ent.isIdle() && (!lastHuntSearch || gameState.ai.elapsedTime - lastHuntSearch > 20)) { if (!this.startHunting(gameState)) { // nothing to hunt around. Try another region if any let nowhereToHunt = true; - for (let base of gameState.ai.HQ.baseManagers) + for (const base of gameState.ai.HQ.baseManagers()) { if (!base.anchor || !base.anchor.position()) continue; let basePos = base.anchor.position(); if (this.startHunting(gameState, basePos)) { ent.setMetadata(PlayerID, "base", base.ID); if (base.accessIndex == this.entAccess) ent.move(basePos[0], basePos[1]); else gameState.ai.HQ.navalManager.requireTransport(gameState, ent, this.entAccess, base.accessIndex, basePos); nowhereToHunt = false; break; } } if (nowhereToHunt) ent.setMetadata(PlayerID, "lastHuntSearch", gameState.ai.elapsedTime); } } else // Perform some sanity checks { if (unitAIStateOrder == "GATHER") { // we may have drifted towards ennemy territory during the hunt, if yes go home let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(ent.position()); if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) // player is its own ally this.startHunting(gameState); else if (unitAIState == "INDIVIDUAL.GATHER.RETURNINGRESOURCE.APPROACHING") { // Check that UnitAI does not send us to an inaccessible dropsite let dropsite = gameState.getEntityById(ent.unitAIOrderData()[0].target); if (dropsite && dropsite.position() && this.entAccess != PETRA.getLandAccess(gameState, dropsite)) PETRA.returnResources(gameState, ent); } } } } else if (subrole == "fisher") { if (ent.isIdle()) this.startFishing(gameState); else // if we have drifted towards ennemy territory during the fishing, go home { let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(ent.position()); if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) // player is its own ally this.startFishing(gameState); } } }; PETRA.Worker.prototype.retryWorking = function(gameState, subrole) { switch (subrole) { case "gatherer": return this.startGathering(gameState); case "hunter": return this.startHunting(gameState); case "fisher": return this.startFishing(gameState); case "builder": return this.startBuilding(gameState); default: return false; } }; PETRA.Worker.prototype.startBuilding = function(gameState) { let target = gameState.getEntityById(this.ent.getMetadata(PlayerID, "target-foundation")); if (!target || target.foundationProgress() === undefined && target.needsRepair() == false) return false; if (PETRA.getLandAccess(gameState, target) != this.entAccess) return false; this.ent.repair(target, target.hasClass("House")); // autocontinue=true for houses return true; }; PETRA.Worker.prototype.startGathering = function(gameState) { // First look for possible treasure if any if (PETRA.gatherTreasure(gameState, this.ent)) return true; let resource = this.ent.getMetadata(PlayerID, "gather-type"); // If we are gathering food, try to hunt first if (resource == "food" && this.startHunting(gameState)) return true; - let findSupply = function(ent, supplies) { + const findSupply = function(worker, supplies) { + const ent = worker.ent; let ret = false; let gatherRates = ent.resourceGatherRates(); for (let i = 0; i < supplies.length; ++i) { // exhausted resource, remove it from this list if (!supplies[i].ent || !gameState.getEntityById(supplies[i].id)) { supplies.splice(i--, 1); continue; } if (PETRA.IsSupplyFull(gameState, supplies[i].ent)) continue; let inaccessibleTime = supplies[i].ent.getMetadata(PlayerID, "inaccessibleTime"); if (inaccessibleTime && gameState.ai.elapsedTime < inaccessibleTime) continue; let supplyType = supplies[i].ent.get("ResourceSupply/Type"); if (!gatherRates[supplyType]) continue; // check if available resource is worth one additionnal gatherer (except for farms) - let nbGatherers = supplies[i].ent.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(supplies[i].id); + const nbGatherers = supplies[i].ent.resourceSupplyNumGatherers() + worker.base.GetTCGatherer(supplies[i].id); if (supplies[i].ent.resourceSupplyType().specific != "grain" && nbGatherers > 0 && supplies[i].ent.resourceSupplyAmount()/(1+nbGatherers) < 30) continue; // not in ennemy territory let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(supplies[i].ent.position()); if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) // player is its own ally continue; - gameState.ai.HQ.AddTCGatherer(supplies[i].id); + worker.base.AddTCGatherer(supplies[i].id); ent.setMetadata(PlayerID, "supply", supplies[i].id); ret = supplies[i].ent; break; } return ret; }; let navalManager = gameState.ai.HQ.navalManager; let supply; // first look in our own base if accessible from our present position if (this.baseAccess == this.entAccess) { - supply = findSupply(this.ent, this.base.dropsiteSupplies[resource].nearby); + supply = findSupply(this, this.base.dropsiteSupplies[resource].nearby); if (supply) { this.ent.gather(supply); return true; } // --> for food, try to gather from fields if any, otherwise build one if any if (resource == "food") { supply = this.gatherNearestField(gameState, this.baseID); if (supply) { this.ent.gather(supply); return true; } supply = this.buildAnyField(gameState, this.baseID); if (supply) { this.ent.repair(supply); return true; } } - supply = findSupply(this.ent, this.base.dropsiteSupplies[resource].medium); + supply = findSupply(this, this.base.dropsiteSupplies[resource].medium); if (supply) { this.ent.gather(supply); return true; } } // So if we're here we have checked our whole base for a proper resource (or it was not accessible) // --> check other bases directly accessible - for (let base of gameState.ai.HQ.baseManagers) + for (const base of gameState.ai.HQ.baseManagers()) { if (base.ID == this.baseID) continue; if (base.accessIndex != this.entAccess) continue; - supply = findSupply(this.ent, base.dropsiteSupplies[resource].nearby); + supply = findSupply(this, base.dropsiteSupplies[resource].nearby); if (supply) { this.ent.setMetadata(PlayerID, "base", base.ID); this.ent.gather(supply); return true; } } if (resource == "food") // --> for food, try to gather from fields if any, otherwise build one if any { - for (let base of gameState.ai.HQ.baseManagers) + for (const base of gameState.ai.HQ.baseManagers()) { if (base.ID == this.baseID) continue; if (base.accessIndex != this.entAccess) continue; supply = this.gatherNearestField(gameState, base.ID); if (supply) { this.ent.setMetadata(PlayerID, "base", base.ID); this.ent.gather(supply); return true; } supply = this.buildAnyField(gameState, base.ID); if (supply) { this.ent.setMetadata(PlayerID, "base", base.ID); this.ent.repair(supply); return true; } } } - for (let base of gameState.ai.HQ.baseManagers) + for (const base of gameState.ai.HQ.baseManagers()) { if (base.ID == this.baseID) continue; if (base.accessIndex != this.entAccess) continue; - supply = findSupply(this.ent, base.dropsiteSupplies[resource].medium); + supply = findSupply(this, base.dropsiteSupplies[resource].medium); if (supply) { this.ent.setMetadata(PlayerID, "base", base.ID); this.ent.gather(supply); return true; } } // Okay may-be we haven't found any appropriate dropsite anywhere. // Try to help building one if any accessible foundation available let foundations = gameState.getOwnFoundations().toEntityArray(); let shouldBuild = this.ent.isBuilder() && foundations.some(function(foundation) { if (!foundation || PETRA.getLandAccess(gameState, foundation) != this.entAccess) return false; let structure = gameState.getBuiltTemplate(foundation.templateName()); if (structure.resourceDropsiteTypes() && structure.resourceDropsiteTypes().indexOf(resource) != -1) { if (foundation.getMetadata(PlayerID, "base") != this.baseID) this.ent.setMetadata(PlayerID, "base", foundation.getMetadata(PlayerID, "base")); this.ent.setMetadata(PlayerID, "target-foundation", foundation.id()); this.ent.setMetadata(PlayerID, "subrole", "builder"); this.ent.repair(foundation); return true; } return false; }, this); if (shouldBuild) return true; // Still nothing ... try bases which need a transport - for (let base of gameState.ai.HQ.baseManagers) + for (const base of gameState.ai.HQ.baseManagers()) { if (base.accessIndex == this.entAccess) continue; - supply = findSupply(this.ent, base.dropsiteSupplies[resource].nearby); + supply = findSupply(this, base.dropsiteSupplies[resource].nearby); if (supply && navalManager.requireTransport(gameState, this.ent, this.entAccess, base.accessIndex, supply.position())) { if (base.ID != this.baseID) this.ent.setMetadata(PlayerID, "base", base.ID); return true; } } if (resource == "food") // --> for food, try to gather from fields if any, otherwise build one if any { - for (let base of gameState.ai.HQ.baseManagers) + for (const base of gameState.ai.HQ.baseManagers()) { if (base.accessIndex == this.entAccess) continue; supply = this.gatherNearestField(gameState, base.ID); if (supply && navalManager.requireTransport(gameState, this.ent, this.entAccess, base.accessIndex, supply.position())) { if (base.ID != this.baseID) this.ent.setMetadata(PlayerID, "base", base.ID); return true; } supply = this.buildAnyField(gameState, base.ID); if (supply && navalManager.requireTransport(gameState, this.ent, this.entAccess, base.accessIndex, supply.position())) { if (base.ID != this.baseID) this.ent.setMetadata(PlayerID, "base", base.ID); return true; } } } - for (let base of gameState.ai.HQ.baseManagers) + for (const base of gameState.ai.HQ.baseManagers()) { if (base.accessIndex == this.entAccess) continue; - supply = findSupply(this.ent, base.dropsiteSupplies[resource].medium); + supply = findSupply(this, base.dropsiteSupplies[resource].medium); if (supply && navalManager.requireTransport(gameState, this.ent, this.entAccess, base.accessIndex, supply.position())) { if (base.ID != this.baseID) this.ent.setMetadata(PlayerID, "base", base.ID); return true; } } // Okay so we haven't found any appropriate dropsite anywhere. // Try to help building one if any non-accessible foundation available shouldBuild = this.ent.isBuilder() && foundations.some(function(foundation) { if (!foundation || PETRA.getLandAccess(gameState, foundation) == this.entAccess) return false; let structure = gameState.getBuiltTemplate(foundation.templateName()); if (structure.resourceDropsiteTypes() && structure.resourceDropsiteTypes().indexOf(resource) != -1) { let foundationAccess = PETRA.getLandAccess(gameState, foundation); if (navalManager.requireTransport(gameState, this.ent, this.entAccess, foundationAccess, foundation.position())) { if (foundation.getMetadata(PlayerID, "base") != this.baseID) this.ent.setMetadata(PlayerID, "base", foundation.getMetadata(PlayerID, "base")); this.ent.setMetadata(PlayerID, "target-foundation", foundation.id()); this.ent.setMetadata(PlayerID, "subrole", "builder"); return true; } } return false; }, this); if (shouldBuild) return true; // Still nothing, we look now for faraway resources, first in the accessible ones, then in the others // except for food when farms or corrals can be used let allowDistant = true; if (resource == "food") { if (gameState.ai.HQ.turnCache.allowDistantFood === undefined) gameState.ai.HQ.turnCache.allowDistantFood = !gameState.ai.HQ.canBuild(gameState, "structures/{civ}/field") && !gameState.ai.HQ.canBuild(gameState, "structures/{civ}/corral"); allowDistant = gameState.ai.HQ.turnCache.allowDistantFood; } if (allowDistant) { if (this.baseAccess == this.entAccess) { - supply = findSupply(this.ent, this.base.dropsiteSupplies[resource].faraway); + supply = findSupply(this, this.base.dropsiteSupplies[resource].faraway); if (supply) { this.ent.gather(supply); return true; } } - for (let base of gameState.ai.HQ.baseManagers) + for (const base of gameState.ai.HQ.baseManagers()) { if (base.ID == this.baseID) continue; if (base.accessIndex != this.entAccess) continue; - supply = findSupply(this.ent, base.dropsiteSupplies[resource].faraway); + supply = findSupply(this, base.dropsiteSupplies[resource].faraway); if (supply) { this.ent.setMetadata(PlayerID, "base", base.ID); this.ent.gather(supply); return true; } } - for (let base of gameState.ai.HQ.baseManagers) + for (const base of gameState.ai.HQ.baseManagers()) { if (base.accessIndex == this.entAccess) continue; - supply = findSupply(this.ent, base.dropsiteSupplies[resource].faraway); + supply = findSupply(this, base.dropsiteSupplies[resource].faraway); if (supply && navalManager.requireTransport(gameState, this.ent, this.entAccess, base.accessIndex, supply.position())) { if (base.ID != this.baseID) this.ent.setMetadata(PlayerID, "base", base.ID); return true; } } } // If we are here, we have nothing left to gather ... certainly no more resources of this type gameState.ai.HQ.lastFailedGather[resource] = gameState.ai.elapsedTime; if (gameState.ai.Config.debug > 2) API3.warn(" >>>>> worker with gather-type " + resource + " with nothing to gather "); this.ent.setMetadata(PlayerID, "subrole", "idle"); return false; }; /** * if position is given, we only check if we could hunt from this position but do nothing * otherwise the position of the entity is taken, and if something is found, we directly start the hunt */ PETRA.Worker.prototype.startHunting = function(gameState, position) { // First look for possible treasure if any if (!position && PETRA.gatherTreasure(gameState, this.ent)) return true; let resources = gameState.getHuntableSupplies(); if (!resources.hasEntities()) return false; let nearestSupplyDist = Math.min(); let nearestSupply; let isFastMoving = PETRA.isFastMoving(this.ent); let isRanged = this.ent.hasClass("Ranged"); let entPosition = position ? position : this.ent.position(); let foodDropsites = gameState.playerData.hasSharedDropsites ? gameState.getAnyDropsites("food") : gameState.getOwnDropsites("food"); let hasFoodDropsiteWithinDistance = function(supplyPosition, supplyAccess, distSquare) { for (let dropsite of foodDropsites.values()) { if (!dropsite.position()) continue; let owner = dropsite.owner(); // owner != PlayerID can only happen when hasSharedDropsites == true, so no need to test it again if (owner != PlayerID && (!dropsite.isSharedDropsite() || !gameState.isPlayerMutualAlly(owner))) continue; if (supplyAccess != PETRA.getLandAccess(gameState, dropsite)) continue; if (API3.SquareVectorDistance(supplyPosition, dropsite.position()) < distSquare) return true; } return false; }; let gatherRates = this.ent.resourceGatherRates(); for (let supply of resources.values()) { if (!supply.position()) continue; let inaccessibleTime = supply.getMetadata(PlayerID, "inaccessibleTime"); if (inaccessibleTime && gameState.ai.elapsedTime < inaccessibleTime) continue; let supplyType = supply.get("ResourceSupply/Type"); if (!gatherRates[supplyType]) continue; if (PETRA.IsSupplyFull(gameState, supply)) continue; // Check if available resource is worth one additionnal gatherer (except for farms). - let nbGatherers = supply.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(supply.id()); + const nbGatherers = supply.resourceSupplyNumGatherers() + this.base.GetTCGatherer(supply.id()); if (nbGatherers > 0 && supply.resourceSupplyAmount()/(1+nbGatherers) < 30) continue; let canFlee = !supply.hasClass("Domestic") && supply.templateName().indexOf("resource|") == -1; // Only FastMoving and Ranged units should hunt fleeing animals. if (canFlee && !isFastMoving && !isRanged) continue; let supplyAccess = PETRA.getLandAccess(gameState, supply); if (supplyAccess != this.entAccess) continue; // measure the distance to the resource. let dist = API3.SquareVectorDistance(entPosition, supply.position()); if (dist > nearestSupplyDist) continue; // Only FastMoving should hunt faraway. if (!isFastMoving && dist > 25000) continue; // Avoid enemy territory. let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(supply.position()); if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) // Player is its own ally. continue; // And if in ally territory, don't hunt this ally's cattle. if (territoryOwner != 0 && territoryOwner != PlayerID && supply.owner() == territoryOwner) continue; // Only FastMoving should hunt far from dropsite (specially for non-Domestic animals which flee). if (!isFastMoving && canFlee && territoryOwner == 0) continue; let distanceSquare = isFastMoving ? 35000 : (canFlee ? 7000 : 12000); if (!hasFoodDropsiteWithinDistance(supply.position(), supplyAccess, distanceSquare)) continue; nearestSupplyDist = dist; nearestSupply = supply; } if (nearestSupply) { if (position) return true; - gameState.ai.HQ.AddTCGatherer(nearestSupply.id()); + this.base.AddTCGatherer(nearestSupply.id()); this.ent.gather(nearestSupply); this.ent.setMetadata(PlayerID, "supply", nearestSupply.id()); this.ent.setMetadata(PlayerID, "target-foundation", undefined); return true; } return false; }; PETRA.Worker.prototype.startFishing = function(gameState) { if (!this.ent.position()) return false; let resources = gameState.getFishableSupplies(); if (!resources.hasEntities()) { gameState.ai.HQ.navalManager.resetFishingBoats(gameState); this.ent.destroy(); return false; } let nearestSupplyDist = Math.min(); let nearestSupply; let fisherSea = PETRA.getSeaAccess(gameState, this.ent); let fishDropsites = (gameState.playerData.hasSharedDropsites ? gameState.getAnyDropsites("food") : gameState.getOwnDropsites("food")). filter(API3.Filters.byClass("Dock")).toEntityArray(); let nearestDropsiteDist = function(supply) { let distMin = 1000000; let pos = supply.position(); for (let dropsite of fishDropsites) { if (!dropsite.position()) continue; let owner = dropsite.owner(); // owner != PlayerID can only happen when hasSharedDropsites == true, so no need to test it again if (owner != PlayerID && (!dropsite.isSharedDropsite() || !gameState.isPlayerMutualAlly(owner))) continue; if (fisherSea != PETRA.getSeaAccess(gameState, dropsite)) continue; distMin = Math.min(distMin, API3.SquareVectorDistance(pos, dropsite.position())); } return distMin; }; let exhausted = true; let gatherRates = this.ent.resourceGatherRates(); resources.forEach(function(supply) { if (!supply.position()) return; // check that it is accessible if (gameState.ai.HQ.navalManager.getFishSea(gameState, supply) != fisherSea) return; exhausted = false; let supplyType = supply.get("ResourceSupply/Type"); if (!gatherRates[supplyType]) return; if (PETRA.IsSupplyFull(gameState, supply)) return; // check if available resource is worth one additionnal gatherer (except for farms) - let nbGatherers = supply.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(supply.id()); + const nbGatherers = supply.resourceSupplyNumGatherers() + this.base.GetTCGatherer(supply.id()); if (nbGatherers > 0 && supply.resourceSupplyAmount()/(1+nbGatherers) < 30) return; // Avoid ennemy territory if (!gameState.ai.HQ.navalManager.canFishSafely(gameState, supply)) return; // measure the distance from the resource to the nearest dropsite let dist = nearestDropsiteDist(supply); if (dist > nearestSupplyDist) return; nearestSupplyDist = dist; nearestSupply = supply; }); if (exhausted) { gameState.ai.HQ.navalManager.resetFishingBoats(gameState, fisherSea); this.ent.destroy(); return false; } if (nearestSupply) { - gameState.ai.HQ.AddTCGatherer(nearestSupply.id()); + this.base.AddTCGatherer(nearestSupply.id()); this.ent.gather(nearestSupply); this.ent.setMetadata(PlayerID, "supply", nearestSupply.id()); this.ent.setMetadata(PlayerID, "target-foundation", undefined); return true; } if (this.ent.getMetadata(PlayerID, "subrole") == "fisher") this.ent.setMetadata(PlayerID, "subrole", "idle"); return false; }; PETRA.Worker.prototype.gatherNearestField = function(gameState, baseID) { let ownFields = gameState.getOwnEntitiesByClass("Field", true).filter(API3.Filters.isBuilt()).filter(API3.Filters.byMetadata(PlayerID, "base", baseID)); let bestFarm; let gatherRates = this.ent.resourceGatherRates(); for (let field of ownFields.values()) { if (PETRA.IsSupplyFull(gameState, field)) continue; let supplyType = field.get("ResourceSupply/Type"); if (!gatherRates[supplyType]) continue; let rate = 1; let diminishing = field.getDiminishingReturns(); if (diminishing < 1) { - let num = field.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(field.id()); + const num = field.resourceSupplyNumGatherers() + this.base.GetTCGatherer(field.id()); if (num > 0) rate = Math.pow(diminishing, num); } // Add a penalty distance depending on rate let dist = API3.SquareVectorDistance(field.position(), this.ent.position()) + (1 - rate) * 160000; if (!bestFarm || dist < bestFarm.dist) bestFarm = { "ent": field, "dist": dist, "rate": rate }; } // If other field foundations available, better build them when rate becomes too small if (!bestFarm || bestFarm.rate < 0.70 && gameState.getOwnFoundations().filter(API3.Filters.byClass("Field")).filter(API3.Filters.byMetadata(PlayerID, "base", baseID)).hasEntities()) return false; - gameState.ai.HQ.AddTCGatherer(bestFarm.ent.id()); + this.base.AddTCGatherer(bestFarm.ent.id()); this.ent.setMetadata(PlayerID, "supply", bestFarm.ent.id()); return bestFarm.ent; }; /** * WARNING with the present options of AI orders, the unit will not gather after building the farm. * This is done by calling the gatherNearestField function when construction is completed. */ PETRA.Worker.prototype.buildAnyField = function(gameState, baseID) { if (!this.ent.isBuilder()) return false; let bestFarmEnt = false; let bestFarmDist = 10000000; let pos = this.ent.position(); for (let found of gameState.getOwnFoundations().values()) { if (found.getMetadata(PlayerID, "base") != baseID || !found.hasClass("Field")) continue; let current = found.getBuildersNb(); if (current === undefined || current >= gameState.getBuiltTemplate(found.templateName()).maxGatherers()) continue; let dist = API3.SquareVectorDistance(found.position(), pos); if (dist > bestFarmDist) continue; bestFarmEnt = found; bestFarmDist = dist; } return bestFarmEnt; }; /** * Workers elephant should move away from the buildings they've built to avoid being trapped in between constructions. * For the time being, we move towards the nearest gatherer (providing him a dropsite). * BaseManager does also use that function to deal with its mobile dropsites. */ PETRA.Worker.prototype.moveToGatherer = function(gameState, ent, forced) { let pos = ent.position(); if (!pos || ent.getMetadata(PlayerID, "target-foundation") !== undefined) return; if (!forced && gameState.ai.elapsedTime < (ent.getMetadata(PlayerID, "nextMoveToGatherer") || 5)) return; let gatherers = this.base.workersBySubrole(gameState, "gatherer"); let dist = Math.min(); let destination; let access = PETRA.getLandAccess(gameState, ent); let types = ent.resourceDropsiteTypes(); for (let gatherer of gatherers.values()) { let gathererType = gatherer.getMetadata(PlayerID, "gather-type"); if (!gathererType || types.indexOf(gathererType) == -1) continue; if (!gatherer.position() || gatherer.getMetadata(PlayerID, "transport") !== undefined || PETRA.getLandAccess(gameState, gatherer) != access || gatherer.isIdle()) continue; let distance = API3.SquareVectorDistance(pos, gatherer.position()); if (distance > dist) continue; dist = distance; destination = gatherer.position(); } ent.setMetadata(PlayerID, "nextMoveToGatherer", gameState.ai.elapsedTime + (destination ? 12 : 5)); if (destination && dist > 10) ent.move(destination[0], destination[1]); }; /** * Check accessibility of the target when in approach (in RMS maps, we quite often have chicken or bushes * inside obstruction of other entities). The resource will be flagged as inaccessible during 10 mn (in case * it will be cleared later). */ PETRA.Worker.prototype.isInaccessibleSupply = function(gameState) { if (!this.ent.unitAIOrderData()[0] || !this.ent.unitAIOrderData()[0].target) return false; let targetId = this.ent.unitAIOrderData()[0].target; let target = gameState.getEntityById(targetId); if (!target) return true; if (!target.resourceSupplyType()) return false; let approachingTarget = this.ent.getMetadata(PlayerID, "approachingTarget"); let carriedAmount = this.ent.resourceCarrying().length ? this.ent.resourceCarrying()[0].amount : 0; if (!approachingTarget || approachingTarget != targetId) { this.ent.setMetadata(PlayerID, "approachingTarget", targetId); this.ent.setMetadata(PlayerID, "approachingTime", undefined); this.ent.setMetadata(PlayerID, "approachingPos", undefined); this.ent.setMetadata(PlayerID, "carriedBefore", carriedAmount); let alreadyTried = this.ent.getMetadata(PlayerID, "alreadyTried"); if (alreadyTried && alreadyTried != targetId) this.ent.setMetadata(PlayerID, "alreadyTried", undefined); } let carriedBefore = this.ent.getMetadata(PlayerID, "carriedBefore"); if (carriedBefore != carriedAmount) { this.ent.setMetadata(PlayerID, "approachingTarget", undefined); this.ent.setMetadata(PlayerID, "alreadyTried", undefined); if (target.getMetadata(PlayerID, "inaccessibleTime")) target.setMetadata(PlayerID, "inaccessibleTime", 0); return false; } let inaccessibleTime = target.getMetadata(PlayerID, "inaccessibleTime"); if (inaccessibleTime && gameState.ai.elapsedTime < inaccessibleTime) return true; let approachingTime = this.ent.getMetadata(PlayerID, "approachingTime"); if (!approachingTime || gameState.ai.elapsedTime - approachingTime > 3) { let presentPos = this.ent.position(); let approachingPos = this.ent.getMetadata(PlayerID, "approachingPos"); if (!approachingPos || approachingPos[0] != presentPos[0] || approachingPos[1] != presentPos[1]) { this.ent.setMetadata(PlayerID, "approachingTime", gameState.ai.elapsedTime); this.ent.setMetadata(PlayerID, "approachingPos", presentPos); return false; } if (gameState.ai.elapsedTime - approachingTime > 10) { if (this.ent.getMetadata(PlayerID, "alreadyTried")) { target.setMetadata(PlayerID, "inaccessibleTime", gameState.ai.elapsedTime + 600); return true; } // let's try again to reach it this.ent.setMetadata(PlayerID, "alreadyTried", targetId); this.ent.setMetadata(PlayerID, "approachingTarget", undefined); this.ent.gather(target); return false; } } return false; };