Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/defenseManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/defenseManager.js (revision 19221) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/defenseManager.js (revision 19222) @@ -1,711 +1,732 @@ var PETRA = function(m) { m.DefenseManager = function(Config) { this.armies = []; // array of "army" Objects this.Config = Config; this.targetList = []; this.armyMergeSize = this.Config.Defense.armyMergeSize; this.attackingArmies = {}; // stats on how many enemies are currently attacking our allies this.attackingUnits = {}; this.attackedAllies = {}; }; m.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(); }; m.DefenseManager.prototype.makeIntoArmy = function(gameState, entityID) { // Try to add it to an existing army. for (let army of this.armies) if (!army.isCapturing(gameState) && army.addFoe(gameState, entityID)) return; // over // Create a new army for it. let army = new m.DefenseArmy(gameState, [], [entityID]); this.armies.push(army); }; m.DefenseManager.prototype.getArmy = function(partOfArmy) { // Find the army corresponding to this ID partOfArmy for (let army of this.armies) if (army.ID === partOfArmy) return army; return undefined; }; m.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 (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); if (!target || !target.position()) // the enemy base is either destroyed or built 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); if (cooperation < 0.6 && cc.foundationProgress() !== undefined) continue; if (cooperation < 0.3) continue; if (API3.SquareVectorDistance(cc.position(), entity.position()) < dist2Min) return true; } let myBuildings = gameState.getOwnStructures(); for (let building of myBuildings.values()) { if (building.foundationProgress() === 0) continue; if (API3.SquareVectorDistance(building.position(), entity.position()) < dist2Min) return true; } // Update the number of enemies attacking this ally if (gameState.isPlayerMutualAlly(territoryOwner)) { 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; }; m.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()); break; } } return; } else if (!gameState.isPlayerEnemy(i)) return; // loop through enemy units 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.hasClass("Ship") || ent.hasClass("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.numActiveBase() === 0) 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()); } }; m.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); for (let breaker of breakaways) this.makeIntoArmy(gameState, breaker); // assume dangerosity if (army.getState() === 0) { army.clear(gameState); this.armies.splice(i--,1); continue; } } // 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.isCapturing(gameState)) continue; for (let j = i+1; j < this.armies.length; ++j) { let otherArmy = this.armies[j]; if (otherArmy.isCapturing(gameState) || 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; } else if (owner !== 0) // enemy army back in its territory { 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); 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; army.clear(gameState); this.armies.splice(i--,1); } }; m.DefenseManager.prototype.assignDefenders = function(gameState) { if (this.armies.length === 0) return; let armiesNeeding = []; // let's add defenders for (let army of this.armies) { let needsDef = army.needsDefenders(gameState); if (needsDef === false) continue; // Okay for now needsDef is the total needed strength. // we're dumb so we don't choose if we have a defender shortage. armiesNeeding.push( {"army": army, "need": needsDef} ); } if (armiesNeeding.length === 0) 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.hasClass("Siege") && !ent.hasClass("Melee")) return; if (ent.hasClass("FishingBoat") || ent.hasClass("Trader")) return; if (ent.getMetadata(PlayerID, "transport") !== undefined || ent.getMetadata(PlayerID, "transporter") !== undefined) return; if (gameState.ai.HQ.gameTypeManager.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 a = 0; a < armiesNeeding.length; ++a) armiesNeeding[a].army.recalculatePosition(gameState); for (let i = 0; i < potentialDefenders.length; ++i) { let ent = gameState.getEntityById(potentialDefenders[i]); if (!ent.position()) continue; let aMin; let distMin; for (let a = 0; a < armiesNeeding.length; ++a) { 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), keep some troops in backup if (i < 12 && this.territoryMap.getOwner(armiesNeeding[aMin].army.foePosition) !== PlayerID) continue; armiesNeeding[aMin].need -= m.getMaxStrength(ent); armiesNeeding[aMin].army.addOwn(gameState, potentialDefenders[i]); armiesNeeding[aMin].army.assignUnit(gameState, potentialDefenders[i]); if (armiesNeeding[aMin].need <= 0) armiesNeeding.splice(aMin, 1); if (!armiesNeeding.length) break; } if (!armiesNeeding.length) return; // If shortage of defenders, produce infantry garrisoned in nearest civil centre let armiesPos = []; for (let a = 0; a < armiesNeeding.length; ++a) armiesPos.push(armiesNeeding[a].army.foePosition); gameState.ai.HQ.trainEmergencyUnits(gameState, armiesPos); }; m.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 in regicide game mode, the gameTypeManager will handle it */ m.DefenseManager.prototype.checkEvents = function(gameState, events) { // must be called every turn for all armies for (let army of this.armies) army.checkEvents(gameState, events); for (let evt of events.Attacked) { let target = gameState.getEntityById(evt.target); - if (!target || !gameState.isEntityOwn(target) || !target.position()) + if (!target || !target.position()) continue; - // If attacked by one of our allies (he must trying to recover capture points), do not react + let attacker = gameState.getEntityById(evt.attacker); + if (attacker && gameState.isEntityOwn(attacker) && gameState.isEntityEnemy(target) && !attacker.hasClass("Ship")) + { + // If enemies are in range of one of our defensive structures, garrison it for arrow multiplier + if (attacker.position() && attacker.isGarrisonHolder() && attacker.getArrowMultiplier()) + this.garrisonRangedUnitsInside(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 (target.hasClass("Ship")) // TODO integrate ships later need to be sure it is accessible 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.hasClass("Structure") && !attacker.hasClass("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.isCapturing(gameState)) { 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 healthlevel if (target.hasClass("Support") && target.healthLevel() < 0.55 && !target.getMetadata(PlayerID, "transport") && plan !== -2 && plan !== -3) { this.garrisonAttackedUnit(gameState, target); continue; } // try to garrison any attacked range siege unit if (target.hasClass("Siege") && !target.hasClass("Melee") && !target.getMetadata(PlayerID, "transport") && plan !== -2 && plan !== -3) { this.garrisonSiegeUnit(gameState, target); continue; } if (!attacker || !attacker.position()) continue; if (target.isGarrisonHolder() && target.getArrowMultiplier()) this.garrisonRangedUnitsInside(gameState, target, {"attacker": attacker}); } }; m.DefenseManager.prototype.garrisonRangedUnitsInside = function(gameState, target, data) { let minGarrison = data.min ? data.min : target.garrisonMax(); let typeGarrison = data.type ? data.type : "protection"; if (gameState.ai.HQ.garrisonManager.numberOfGarrisonedUnits(target) >= minGarrison) return; if (target.hitpoints() < target.garrisonEjectHealth() * target.maxHitpoints()) return; if (data.attacker) { let attackTypes = target.attackTypes(); if (!attackTypes || attackTypes.indexOf("Ranged") === -1) return; let dist = API3.SquareVectorDistance(data.attacker.position(), target.position()); let range = target.attackRange("Ranged").max; if (dist >= range*range) return; } let index = gameState.ai.accessibility.getAccessValue(target.position()); let garrisonManager = gameState.ai.HQ.garrisonManager; let garrisonArrowClasses = target.getGarrisonArrowClasses(); let units = gameState.getOwnUnits().filter(ent => MatchesClassList(ent.classes(), garrisonArrowClasses)).filterNearest(target.position()); for (let ent of units.values()) { if (garrisonManager.numberOfGarrisonedUnits(target) >= minGarrison) break; if (!ent.position()) continue; if (ent.getMetadata(PlayerID, "transport") !== undefined) continue; - if (ent.getMetadata(PlayerID, "plan") === -2 || ent.getMetadata(PlayerID, "plan") === -3) + 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)) continue; - if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") !== -1) + if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") >= 0) { let subrole = ent.getMetadata(PlayerID, "subrole"); - if (subrole && (subrole === "completing" || subrole === "walking" || subrole === "attacking")) + // 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")) continue; } if (gameState.ai.accessibility.getAccessValue(ent.position()) !== index) continue; + 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 (army) + army.removeOwn(gameState, ent.id()); garrisonManager.garrison(gameState, ent, target, typeGarrison); } }; /** garrison a attacked siege ranged unit inside the nearest fortress */ m.DefenseManager.prototype.garrisonSiegeUnit = function(gameState, unit) { let distmin = Math.min(); let nearest; let unitAccess = gameState.ai.accessibility.getAccessValue(unit.position()); let garrisonManager = gameState.ai.HQ.garrisonManager; gameState.getAllyStructures().forEach(function(ent) { if (!MatchesClassList(unit.classes(), ent.garrisonableClasses())) return; if (garrisonManager.numberOfGarrisonedUnits(ent) >= ent.garrisonMax()) return; if (ent.hitpoints() < ent.garrisonEjectHealth() * ent.maxHitpoints()) return; if (m.getLandAccess(gameState, ent) !== unitAccess) return; let dist = API3.SquareVectorDistance(ent.position(), unit.position()); if (dist > distmin) return; distmin = dist; nearest = ent; }); if (nearest) garrisonManager.garrison(gameState, unit, nearest, "protection"); }; /** * 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 */ m.DefenseManager.prototype.garrisonAttackedUnit = function(gameState, unit, emergency = false) { let distmin = Math.min(); let nearest; let unitAccess = gameState.ai.accessibility.getAccessValue(unit.position()); let garrisonManager = gameState.ai.HQ.garrisonManager; for (let ent of gameState.getAllyStructures().values()) { if (!emergency && !ent.buffHeal()) continue; if (!MatchesClassList(unit.classes(), ent.garrisonableClasses())) continue; if (garrisonManager.numberOfGarrisonedUnits(ent) >= ent.garrisonMax() && (!emergency || !ent.garrisoned().length)) continue; if (ent.hitpoints() < ent.garrisonEjectHealth() * ent.maxHitpoints()) continue; if (m.getLandAccess(gameState, ent) !== unitAccess) continue; let dist = API3.SquareVectorDistance(ent.position(), unit.position()); if (dist > distmin) continue; distmin = dist; nearest = ent; } if (!nearest) return; if (!emergency) { garrisonManager.garrison(gameState, unit, nearest, "protection"); return; } if (garrisonManager.numberOfGarrisonedUnits(nearest) >= nearest.garrisonMax()) // make room for this ent nearest.unload(nearest.garrisoned()[0]); garrisonManager.garrison(gameState, unit, nearest, nearest.buffHeal() ? "protection" : "emergency"); }; /** * Be more inclined to help an ally attacked by several enemies */ m.DefenseManager.prototype.GetCooperationLevel = function(ent) { let ally = ent.owner(); let cooperation = this.Config.personality.cooperative; if (this.attackedAllies[ally] && this.attackedAllies[ally] > 1) cooperation += 0.2 * (this.attackedAllies[ally] - 1); return cooperation; }; m.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 }; }; m.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 m.DefenseArmy(gameState, [], []); army.Deserialize(dataArmy); this.armies.push(army); } }; return m; }(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/transportPlan.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/transportPlan.js (revision 19221) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/transportPlan.js (revision 19222) @@ -1,630 +1,630 @@ var PETRA = function(m) { /** * 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 */ m.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); 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; }; m.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 */ m.TransportPlan.prototype.countFreeSlots = function() { let self = this; let slots = 0; this.transportShips.forEach(function (ship) { slots += self.countFreeSlotsOnShip(ship); }); return slots; }; m.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); }; m.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; else gameState.ai.HQ.navalManager.splitTransport(gameState, this); }; m.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(function (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); this.ships.updateEnt(nearest); this.transportShips.updateEnt(nearest); this.needTransportShips = false; return true; }; /** add a unit to this plan */ m.TransportPlan.prototype.addUnit = function(unit, endPos) { unit.setMetadata(PlayerID, "transport", this.ID); unit.setMetadata(PlayerID, "endPos", endPos); this.units.updateEnt(unit); }; m.TransportPlan.prototype.releaseAll = function() { this.ships.forEach(function (ship) { ship.setMetadata(PlayerID, "transporter", undefined); if (ship.getMetadata(PlayerID, "role") === "switchToTrader") ship.setMetadata(PlayerID, "role", "trader"); }); this.units.forEach(function (ent) { 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(); }; m.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) { 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(function (ent) { ent.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 */ m.TransportPlan.prototype.update = function(gameState) { if (this.state === "boarding") this.onBoarding(gameState); else if (this.state === "sailing") this.onSailing(gameState); return this.units.length; }; m.TransportPlan.prototype.onBoarding = function(gameState) { let ready = true; let self = this; let time = gameState.ai.elapsedTime; this.units.forEach(function (ent) { if (!ent.getMetadata(PlayerID, "onBoard")) { ready = false; self.assignUnitToShip(gameState, ent); if (ent.getMetadata(PlayerID, "onBoard")) { let shipId = ent.getMetadata(PlayerID, "onBoard"); let ship = gameState.getEntityById(shipId); if (!self.boardingPos[shipId]) { self.boardingPos[shipId] = self.getBoardingPos(gameState, ship, self.startIndex, self.sea, ent.position(), false); ship.move(self.boardingPos[shipId][0], self.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" && !self.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); else { let distShip = API3.SquareVectorDistance(self.boardingPos[shipId], ship.position()); if (time - ship.getMetadata(PlayerID, "timeGarrison") > 8 && distShip > self.boardingRange) { if (!self.nTry[shipId]) self.nTry[shipId] = 1; else ++self.nTry[shipId]; if (self.nTry[shipId] > 1) // we must have been blocked by something ... try with another boarding point { self.nTry[shipId] = 0; if (self.debug > 1) API3.warn("ship " + shipId + " new attempt for a landing point "); self.boardingPos[shipId] = self.getBoardingPos(gameState, ship, self.startIndex, self.sea, undefined, false); } ship.move(self.boardingPos[shipId][0], self.boardingPos[shipId][1]); ship.setMetadata(PlayerID, "timeGarrison", time); } else 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 < self.boardingRange) // looks like we are blocked ... try to go out of this trap { if (!self.nTry[ent.id()]) self.nTry[ent.id()] = 1; else ++self.nTry[ent.id()]; if (self.nTry[ent.id()] > 5) { if (self.debug > 1) API3.warn("unit blocked, but no ways out of the trap ... destroy it"); self.resetUnit(gameState, ent); ent.destroy(); return; } if (self.nTry[ent.id()] > 1) ent.moveToRange(newPos[0], newPos[1], 30, 30); ent.garrison(ship, true); } else // wait for the ship ent.move(self.boardingPos[shipId][0], self.boardingPos[shipId][1]); } else self.nTry[ent.id()] = 0; ent.setMetadata(PlayerID, "timeGarrison", time); ent.setMetadata(PlayerID, "posGarrison", ent.position()); } } } }); if (!ready) return; this.ships.forEach(function (ship) { self.boardingPos[ship.id()] = undefined; self.boardingPos[ship.id()] = self.getBoardingPos(gameState, ship, self.endIndex, self.sea, self.endPos, true); ship.move(self.boardingPos[ship.id()][0], self.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 */ m.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 */ m.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.getMap().width; let cell = gameState.getMap().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.SquareVectorDistance(startPos, pos); if (destination) dist += API3.SquareVectorDistance(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 for (let dock of alliedDocks) { let dockDist = API3.SquareVectorDistance(dock.position(), pos); if (dockDist < 4900) dist += 100000 * (4900 - dockDist) / 4900; } 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; }; m.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 = gameState.ai.accessibility.getAccessValue(ent.position()); 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 (gameState.ai.accessibility.getAccessValue(ent.position()) !== 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 { 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], 15); ship.setMetadata(PlayerID, "transporter", undefined); if (ship.getMetadata(PlayerID, "role") === "switchToTrader") ship.setMetadata(PlayerID, "role", "trader"); 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]); } } }; m.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") >= 0) + 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()); } }; m.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 }; }; m.TransportPlan.prototype.Deserialize = function(data) { for (let key in data) this[key] = data[key]; this.failed = false; }; return m; }(PETRA);