Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/defenseArmy.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/defenseArmy.js (revision 24314) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/defenseArmy.js (revision 24315) @@ -1,649 +1,649 @@ /** * Armies used by the defense manager. * An army is a collection of own entities and enemy entities. * * Types of armies: * "default": army to counter an invading army * "capturing": army set to capture a gaia building or recover capture points to one of its own structures * It must contain only one foe (the building to capture) and never be merged */ PETRA.DefenseArmy = function(gameState, foeEntities, type) { this.ID = gameState.ai.uniqueIDs.armies++; this.type = type || "default"; this.Config = gameState.ai.Config; this.compactSize = this.Config.Defense.armyCompactSize; this.breakawaySize = this.Config.Defense.armyBreakawaySize; // average this.foePosition = [0, 0]; this.positionLastUpdate = gameState.ai.elapsedTime; // Some caching // A list of our defenders that were tasked with attacking a particular unit // This doesn't mean that they actually are since they could move on to something else on their own. this.assignedAgainst = {}; // who we assigned against, for quick removal. this.assignedTo = {}; this.foeEntities = []; this.foeStrength = 0; this.ownEntities = []; this.ownStrength = 0; // actually add units for (let id of foeEntities) this.addFoe(gameState, id, true); this.recalculatePosition(gameState, true); return true; }; /** * add an entity to the enemy army * Will return true if the entity was added and false otherwise. * won't recalculate our position but will dirty it. * force is true at army creation or when merging armies, so in this case we should add it even if far */ PETRA.DefenseArmy.prototype.addFoe = function(gameState, enemyId, force) { if (this.foeEntities.indexOf(enemyId) !== -1) return false; let ent = gameState.getEntityById(enemyId); if (!ent || !ent.position()) return false; // check distance if (!force && API3.SquareVectorDistance(ent.position(), this.foePosition) > this.compactSize) return false; this.foeEntities.push(enemyId); this.assignedAgainst[enemyId] = []; this.positionLastUpdate = 0; this.evaluateStrength(ent); ent.setMetadata(PlayerID, "PartOfArmy", this.ID); return true; }; /** * returns true if the entity was removed and false otherwise. * TODO: when there is a technology update, we should probably recompute the strengths, or weird stuffs will happen. */ PETRA.DefenseArmy.prototype.removeFoe = function(gameState, enemyId, enemyEntity) { let idx = this.foeEntities.indexOf(enemyId); if (idx === -1) return false; this.foeEntities.splice(idx, 1); this.assignedAgainst[enemyId] = undefined; for (let to in this.assignedTo) if (this.assignedTo[to] == enemyId) this.assignedTo[to] = undefined; let ent = enemyEntity ? enemyEntity : gameState.getEntityById(enemyId); if (ent) // TODO recompute strength when no entities (could happen if capture+destroy) { this.evaluateStrength(ent, false, true); ent.setMetadata(PlayerID, "PartOfArmy", undefined); } return true; }; /** * adds a defender but doesn't assign him yet. * force is true when merging armies, so in this case we should add it even if no position as it can be in a ship */ PETRA.DefenseArmy.prototype.addOwn = function(gameState, id, force) { if (this.ownEntities.indexOf(id) !== -1) return false; let ent = gameState.getEntityById(id); if (!ent || !ent.position() && !force) return false; this.ownEntities.push(id); this.evaluateStrength(ent, true); ent.setMetadata(PlayerID, "PartOfArmy", this.ID); this.assignedTo[id] = 0; let plan = ent.getMetadata(PlayerID, "plan"); if (plan !== undefined) ent.setMetadata(PlayerID, "plan", -2); else ent.setMetadata(PlayerID, "plan", -3); let subrole = ent.getMetadata(PlayerID, "subrole"); if (subrole === undefined || subrole !== "defender") ent.setMetadata(PlayerID, "formerSubrole", subrole); ent.setMetadata(PlayerID, "subrole", "defender"); return true; }; PETRA.DefenseArmy.prototype.removeOwn = function(gameState, id, Entity) { let idx = this.ownEntities.indexOf(id); if (idx === -1) return false; this.ownEntities.splice(idx, 1); if (this.assignedTo[id] !== 0) { let temp = this.assignedAgainst[this.assignedTo[id]]; if (temp) temp.splice(temp.indexOf(id), 1); } this.assignedTo[id] = undefined; let ent = Entity ? Entity : gameState.getEntityById(id); if (!ent) return true; this.evaluateStrength(ent, true, true); ent.setMetadata(PlayerID, "PartOfArmy", undefined); if (ent.getMetadata(PlayerID, "plan") === -2) ent.setMetadata(PlayerID, "plan", -1); else ent.setMetadata(PlayerID, "plan", undefined); let formerSubrole = ent.getMetadata(PlayerID, "formerSubrole"); if (formerSubrole !== undefined) ent.setMetadata(PlayerID, "subrole", formerSubrole); else ent.setMetadata(PlayerID, "subrole", undefined); ent.setMetadata(PlayerID, "formerSubrole", undefined); // Remove from tranport plan if not yet on Board if (ent.getMetadata(PlayerID, "transport") !== undefined) { let plan = gameState.ai.HQ.navalManager.getPlan(ent.getMetadata(PlayerID, "transport")); if (plan && plan.state == "boarding" && ent.position()) plan.removeUnit(gameState, ent); } /* // TODO be sure that all units in the transport need the cancelation if (!ent.position()) // this unit must still be in a transport plan ... try to cancel it { let planID = ent.getMetadata(PlayerID, "transport"); // no plans must mean that the unit was in a ship which was destroyed, so do nothing if (planID) { if (gameState.ai.Config.debug > 0) warn("ent from army still in transport plan: plan " + planID + " canceled"); let plan = gameState.ai.HQ.navalManager.getPlan(planID); if (plan && !plan.canceled) plan.cancelTransport(gameState); } } */ return true; }; /** * resets the army properly. * assumes we already cleared dead units. */ PETRA.DefenseArmy.prototype.clear = function(gameState) { while (this.foeEntities.length > 0) this.removeFoe(gameState, this.foeEntities[0]); // Go back to our or allied territory if needed let posOwn = [0, 0]; let nOwn = 0; let posAlly = [0, 0]; let nAlly = 0; let posOther = [0, 0]; let nOther = 0; for (let entId of this.ownEntities) { let ent = gameState.getEntityById(entId); if (!ent || !ent.position()) continue; let pos = ent.position(); let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(pos); if (territoryOwner === PlayerID) { posOwn[0] += pos[0]; posOwn[1] += pos[1]; ++nOwn; } else if (gameState.isPlayerMutualAlly(territoryOwner)) { posAlly[0] += pos[0]; posAlly[1] += pos[1]; ++nAlly; } else { posOther[0] += pos[0]; posOther[1] += pos[1]; ++nOther; } } let destination; let defensiveFound; let distmin; let radius = 0; if (nOwn > 0) destination = [posOwn[0]/nOwn, posOwn[1]/nOwn]; else if (nAlly > 0) destination = [posAlly[0]/nAlly, posAlly[1]/nAlly]; else { posOther[0] /= nOther; posOther[1] /= nOther; let armyAccess = gameState.ai.accessibility.getAccessValue(posOther); for (let struct of gameState.getAllyStructures().values()) { let pos = struct.position(); if (!pos || !gameState.isPlayerMutualAlly(gameState.ai.HQ.territoryMap.getOwner(pos))) continue; if (PETRA.getLandAccess(gameState, struct) !== armyAccess) continue; let defensiveStruct = struct.hasDefensiveFire(); if (defensiveFound && !defensiveStruct) continue; let dist = API3.SquareVectorDistance(posOther, pos); if (distmin && dist > distmin && (defensiveFound || !defensiveStruct)) continue; if (defensiveStruct) defensiveFound = true; distmin = dist; destination = pos; radius = struct.obstructionRadius().max; } } while (this.ownEntities.length > 0) { let entId = this.ownEntities[0]; this.removeOwn(gameState, entId); let ent = gameState.getEntityById(entId); if (ent) { if (!ent.position() || ent.getMetadata(PlayerID, "transport") !== undefined || ent.getMetadata(PlayerID, "transporter") !== undefined) continue; if (ent.healthLevel() < this.Config.garrisonHealthLevel.low && gameState.ai.HQ.defenseManager.garrisonAttackedUnit(gameState, ent)) continue; if (destination && !gameState.isPlayerMutualAlly(gameState.ai.HQ.territoryMap.getOwner(ent.position()))) - ent.moveToRange(destination[0], destination[1], radius, radius+5); + ent.moveToRange(destination[0], destination[1], radius, radius + 5); else ent.stopMoving(); } } this.assignedAgainst = {}; this.assignedTo = {}; this.recalculateStrengths(gameState); this.recalculatePosition(gameState); }; PETRA.DefenseArmy.prototype.assignUnit = function(gameState, entID) { // we'll assume this defender is ours already. // we'll also override any previous assignment let ent = gameState.getEntityById(entID); if (!ent || !ent.position()) return false; // try to return its resources, and if any, the attack order will be queued let queued = PETRA.returnResources(gameState, ent); let idMin; let distMin; let idMinAll; let distMinAll; for (let id of this.foeEntities) { let eEnt = gameState.getEntityById(id); if (!eEnt || !eEnt.position()) // probably can't happen. continue; if (!ent.canAttackTarget(eEnt, PETRA.allowCapture(gameState, ent, eEnt))) continue; if (eEnt.hasClass("Unit") && eEnt.unitAIOrderData() && eEnt.unitAIOrderData().length && eEnt.unitAIOrderData()[0].target && eEnt.unitAIOrderData()[0].target == entID) { // being attacked >>> target the unit idMin = id; break; } // already enough units against it if (this.assignedAgainst[id].length > 8 || this.assignedAgainst[id].length > 5 && !eEnt.hasClass("Hero") && !PETRA.isSiegeUnit(eEnt)) continue; let dist = API3.SquareVectorDistance(ent.position(), eEnt.position()); if (idMinAll === undefined || dist < distMinAll) { idMinAll = id; distMinAll = dist; } if (this.assignedAgainst[id].length > 2) continue; if (idMin === undefined || dist < distMin) { idMin = id; distMin = dist; } } let idFoe; if (idMin !== undefined) idFoe = idMin; else if (idMinAll !== undefined) idFoe = idMinAll; else return false; let ownIndex = PETRA.getLandAccess(gameState, ent); let foeEnt = gameState.getEntityById(idFoe); let foePosition = foeEnt.position(); let foeIndex = gameState.ai.accessibility.getAccessValue(foePosition); if (ownIndex == foeIndex || ent.hasClass("Ship")) { this.assignedTo[entID] = idFoe; this.assignedAgainst[idFoe].push(entID); ent.attack(idFoe, PETRA.allowCapture(gameState, ent, foeEnt), queued); } else gameState.ai.HQ.navalManager.requireTransport(gameState, ent, ownIndex, foeIndex, foePosition); return true; }; PETRA.DefenseArmy.prototype.getType = function() { return this.type; }; PETRA.DefenseArmy.prototype.getState = function() { if (!this.foeEntities.length) return 0; return 1; }; /** * merge this army with another properly. * assumes units are in only one army. * also assumes that all have been properly cleaned up (no dead units). */ PETRA.DefenseArmy.prototype.merge = function(gameState, otherArmy) { // copy over all parameters. for (let i in otherArmy.assignedAgainst) { if (this.assignedAgainst[i] === undefined) this.assignedAgainst[i] = otherArmy.assignedAgainst[i]; else this.assignedAgainst[i] = this.assignedAgainst[i].concat(otherArmy.assignedAgainst[i]); } for (let i in otherArmy.assignedTo) this.assignedTo[i] = otherArmy.assignedTo[i]; for (let id of otherArmy.foeEntities) this.addFoe(gameState, id, true); // TODO: reassign those ? for (let id of otherArmy.ownEntities) this.addOwn(gameState, id, true); this.recalculatePosition(gameState, true); this.recalculateStrengths(gameState); return true; }; PETRA.DefenseArmy.prototype.needsDefenders = function(gameState) { let defenseRatio; let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(this.foePosition); if (territoryOwner == PlayerID) defenseRatio = this.Config.Defense.defenseRatio.own; else if (gameState.isPlayerAlly(territoryOwner)) { defenseRatio = this.Config.Defense.defenseRatio.ally; let numExclusiveAllies = 0; for (let p = 1; p < gameState.sharedScript.playersData.length; ++p) if (p != territoryOwner && gameState.sharedScript.playersData[p].isAlly[territoryOwner]) ++numExclusiveAllies; defenseRatio /= 1 + 0.5*Math.max(0, numExclusiveAllies-1); } else defenseRatio = this.Config.Defense.defenseRatio.neutral; // some preliminary checks because we don't update for tech so entStrength removed can be > entStrength added if (this.foeStrength <= 0 || this.ownStrength <= 0) this.recalculateStrengths(gameState); if (this.foeStrength * defenseRatio <= this.ownStrength) return false; return this.foeStrength * defenseRatio - this.ownStrength; }; /** if not forced, will only recalculate if on a different turn. */ PETRA.DefenseArmy.prototype.recalculatePosition = function(gameState, force) { if (!force && this.positionLastUpdate === gameState.ai.elapsedTime) return; let npos = 0; let pos = [0, 0]; for (let id of this.foeEntities) { let ent = gameState.getEntityById(id); if (!ent || !ent.position()) continue; npos++; let epos = ent.position(); pos[0] += epos[0]; pos[1] += epos[1]; } // if npos = 0, the army must have been destroyed and will be removed next turn. keep previous position if (npos > 0) { this.foePosition[0] = pos[0]/npos; this.foePosition[1] = pos[1]/npos; } this.positionLastUpdate = gameState.ai.elapsedTime; }; PETRA.DefenseArmy.prototype.recalculateStrengths = function(gameState) { this.ownStrength = 0; this.foeStrength = 0; for (let id of this.foeEntities) this.evaluateStrength(gameState.getEntityById(id)); for (let id of this.ownEntities) this.evaluateStrength(gameState.getEntityById(id), true); }; /** adds or remove the strength of the entity either to the enemy or to our units. */ PETRA.DefenseArmy.prototype.evaluateStrength = function(ent, isOwn, remove) { if (!ent) return; let entStrength; if (ent.hasClass("Structure")) { if (ent.owner() !== PlayerID) entStrength = ent.getDefaultArrow() ? 6*ent.getDefaultArrow() : 4; else // small strength used only when we try to recover capture points entStrength = 2; } else entStrength = PETRA.getMaxStrength(ent, this.Config.debug, this.Config.DamageTypeImportance); // TODO adapt the getMaxStrength function for animals. // For the time being, just increase it for elephants as the returned value is too small. if (ent.hasClass("Animal") && ent.hasClass("Elephant")) entStrength *= 3; if (remove) entStrength *= -1; if (isOwn) this.ownStrength += entStrength; else this.foeStrength += entStrength; }; PETRA.DefenseArmy.prototype.checkEvents = function(gameState, events) { // Warning the metadata is already cloned in shared.js. Futhermore, changes should be done before destroyEvents // otherwise it would remove the old entity from this army list // TODO we should may-be reevaluate the strength for (let evt of events.EntityRenamed) // take care of promoted and packed units { if (this.foeEntities.indexOf(evt.entity) !== -1) { let ent = gameState.getEntityById(evt.newentity); if (ent && ent.templateName().indexOf("resource|") !== -1) // corpse of animal killed continue; let idx = this.foeEntities.indexOf(evt.entity); this.foeEntities[idx] = evt.newentity; this.assignedAgainst[evt.newentity] = this.assignedAgainst[evt.entity]; this.assignedAgainst[evt.entity] = undefined; for (let to in this.assignedTo) if (this.assignedTo[to] === evt.entity) this.assignedTo[to] = evt.newentity; } else if (this.ownEntities.indexOf(evt.entity) !== -1) { let idx = this.ownEntities.indexOf(evt.entity); this.ownEntities[idx] = evt.newentity; this.assignedTo[evt.newentity] = this.assignedTo[evt.entity]; this.assignedTo[evt.entity] = undefined; for (let against in this.assignedAgainst) { if (!this.assignedAgainst[against]) continue; if (this.assignedAgainst[against].indexOf(evt.entity) !== -1) this.assignedAgainst[against][this.assignedAgainst[against].indexOf(evt.entity)] = evt.newentity; } } } for (let evt of events.Garrison) this.removeFoe(gameState, evt.entity); for (let evt of events.OwnershipChanged) // captured { if (!gameState.isPlayerEnemy(evt.to)) this.removeFoe(gameState, evt.entity); else if (evt.from === PlayerID) this.removeOwn(gameState, evt.entity); } for (let evt of events.Destroy) { let entityObj = evt.entityObj || undefined; // we may have capture+destroy, so do not trust owner and check all possibilities this.removeOwn(gameState, evt.entity, entityObj); this.removeFoe(gameState, evt.entity, entityObj); } }; PETRA.DefenseArmy.prototype.update = function(gameState) { for (let entId of this.ownEntities) { let ent = gameState.getEntityById(entId); if (!ent) continue; let orderData = ent.unitAIOrderData(); if (!orderData.length && !ent.getMetadata(PlayerID, "transport")) this.assignUnit(gameState, entId); else if (orderData.length && orderData[0].target && orderData[0].attackType && orderData[0].attackType === "Capture") { let target = gameState.getEntityById(orderData[0].target); if (target && !PETRA.allowCapture(gameState, ent, target)) ent.attack(orderData[0].target, false); } } if (this.type == "capturing") { if (this.foeEntities.length && gameState.getEntityById(this.foeEntities[0])) { // Check if we still still some capturePoints to recover // and if not, remove this foe from the list (capture army have only one foe) let capture = gameState.getEntityById(this.foeEntities[0]).capturePoints(); if (capture) for (let j = 0; j < capture.length; ++j) if (gameState.isPlayerEnemy(j) && capture[j] > 0) return []; this.removeFoe(gameState, this.foeEntities[0]); } return []; } let breakaways = []; // TODO: assign unassigned defenders, cleanup of a few things. // perhaps occasional strength recomputation // occasional update or breakaways, positions… if (gameState.ai.elapsedTime - this.positionLastUpdate > 5) { this.recalculatePosition(gameState); this.positionLastUpdate = gameState.ai.elapsedTime; // Check for breakaways. for (let i = 0; i < this.foeEntities.length; ++i) { let id = this.foeEntities[i]; let ent = gameState.getEntityById(id); if (!ent || !ent.position()) continue; if (API3.SquareVectorDistance(ent.position(), this.foePosition) > this.breakawaySize) { breakaways.push(id); if (this.removeFoe(gameState, id)) i--; } } this.recalculatePosition(gameState); } return breakaways; }; PETRA.DefenseArmy.prototype.Serialize = function() { return { "ID": this.ID, "type": this.type, "foePosition": this.foePosition, "positionLastUpdate": this.positionLastUpdate, "assignedAgainst": this.assignedAgainst, "assignedTo": this.assignedTo, "foeEntities": this.foeEntities, "foeStrength": this.foeStrength, "ownEntities": this.ownEntities, "ownStrength": this.ownStrength }; }; PETRA.DefenseArmy.prototype.Deserialize = function(data) { for (let key in data) this[key] = data[key]; }; Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/defenseManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/defenseManager.js (revision 24314) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/defenseManager.js (revision 24315) @@ -1,964 +1,964 @@ 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") { // Try to add it to an existing army. for (let army of this.armies) if (army.getType() == type && army.addFoe(gameState, entityID)) return; // over } // Create a new army for it. let army = new PETRA.DefenseArmy(gameState, [entityID], type); this.armies.push(army); }; PETRA.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; }; 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.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.numActiveBases() == 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(), "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.hasClass("StoneThrower")) 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.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); + 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)) { 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.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.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.hasClass("Structure") && !currentTarget.hasClass("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.numberOfGarrisonedUnits(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 (!MatchesClassList(ent.classes(), 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.numberOfGarrisonedUnits(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 (!MatchesClassList(unit.classes(), ent.garrisonableClasses())) continue; if (garrisonManager.numberOfGarrisonedUnits(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 (!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 (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.numberOfGarrisonedUnits(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/navalManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/navalManager.js (revision 24314) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/navalManager.js (revision 24315) @@ -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.byClassesOr(["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.hasClass("Dock") || ent.hasClass("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.hasClass("Dock") || ent.hasClass("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.RETURNRESOURCE.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, 30); + 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, 70); + 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, 30); + 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, 70); + 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.RETURNRESOURCE.APPROACHING") { if (distSquare < 1600) - blockingShip.moveToRange(shipPosition[0], shipPosition[1], 40, 40); + blockingShip.moveToRange(shipPosition[0], shipPosition[1], 40, 45); } else if (distSquare < 900) - blockingShip.moveToRange(shipPosition[0], shipPosition[1], 30, 30); + 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, 40); + blockingShip.moveToRange(shipPosition[0], shipPosition[1], 40, 45); } else if (distSquare < 900) - blockingShip.moveToRange(shipPosition[0], shipPosition[1], 30, 30); + 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]) 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) { 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) 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/startingStrategy.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/startingStrategy.js (revision 24314) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/startingStrategy.js (revision 24315) @@ -1,573 +1,573 @@ /** * 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.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) 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.hasClass("Trader") && !ent.hasClass("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); } } } }; /** * 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 supplyId = ent.getMetadata(PlayerID, "supply"); if (!supplyId) continue; let supply = gameState.getEntityById(supplyId); if (!supply || supply.resourceSupplyType().generic != "treasure") continue; let type = supply.resourceSupplyType().specific; if (!(type in totalExpected)) continue; totalExpected[type] += supply.resourceSupplyMax(); // 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.byClassesAnd(["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); + ent.moveToRange(cc.position()[0], cc.position()[1], range, range + 5); break; } }); // second pass to affect melee infantry units.filter(API3.Filters.byClassesAnd(["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); + 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); + 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) return; this.firstBaseConfig = true; let startingSize = 0; let startingLand = []; for (let region in this.landRegions) { for (let 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(); } } } 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(); } } } 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. let template = gameState.applyCiv("structures/{civ}/storehouse"); if (!gameState.getOwnEntitiesByClass("Storehouse", true).hasEntities() && this.canBuild(gameState, template)) { let newDP = this.baseManagers[1].findBestDropsiteLocation(gameState, "wood"); if (newDP.quality > 40) { // 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) { let cost = new API3.Resources(gameState.getTemplate(template).cost()); gameState.ai.queueManager.setAccounts(gameState, cost, "dropsites"); } gameState.ai.queues.dropsites.addPlan(new PETRA.ConstructionPlan(gameState, template, { "base": this.baseManagers[1].ID }, newDP.pos)); } } // and build immediately a corral if needed if (this.needCorral) { 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 })); } }; Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/transportPlan.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/transportPlan.js (revision 24314) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/transportPlan.js (revision 24315) @@ -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) { 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, 30); + 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-20, dist-20); + ent.moveToRange(goal[0], goal[1], dist-25, dist-20); else - ent.moveToRange(pos[0], pos[1], 20, 20); + 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/source/simulation2/components/CCmpUnitMotion.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpUnitMotion.cpp (revision 24314) +++ ps/trunk/source/simulation2/components/CCmpUnitMotion.cpp (revision 24315) @@ -1,1579 +1,1582 @@ /* Copyright (C) 2020 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "simulation2/system/Component.h" #include "ICmpUnitMotion.h" #include "simulation2/components/ICmpObstruction.h" #include "simulation2/components/ICmpObstructionManager.h" #include "simulation2/components/ICmpOwnership.h" #include "simulation2/components/ICmpPosition.h" #include "simulation2/components/ICmpPathfinder.h" #include "simulation2/components/ICmpRangeManager.h" #include "simulation2/components/ICmpValueModificationManager.h" #include "simulation2/components/ICmpVisual.h" #include "simulation2/helpers/Geometry.h" #include "simulation2/helpers/Render.h" #include "simulation2/MessageTypes.h" #include "simulation2/serialization/SerializeTemplates.h" #include "graphics/Overlay.h" #include "graphics/Terrain.h" #include "maths/FixedVector2D.h" #include "ps/CLogger.h" #include "ps/Profile.h" #include "renderer/Scene.h" // For debugging; units will start going straight to the target // instead of calling the pathfinder #define DISABLE_PATHFINDER 0 /** * Min/Max range to restrict short path queries to. (Larger ranges are slower, * Min/Max range to restrict short path queries to. (Larger ranges are (much) slower, * smaller ranges might miss some legitimate routes around large obstacles.) */ static const entity_pos_t SHORT_PATH_MIN_SEARCH_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*3)/2; static const entity_pos_t SHORT_PATH_MAX_SEARCH_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*14); static const entity_pos_t SHORT_PATH_SEARCH_RANGE_INCREMENT = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*2); /** * When using the short-pathfinder to rejoin a long-path waypoint, aim for a circle of this radius around the waypoint. */ static const entity_pos_t SHORT_PATH_LONG_WAYPOINT_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*1); /** * Minimum distance to goal for a long path request */ static const entity_pos_t LONG_PATH_MIN_DIST = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*4); /** * If we are this close to our target entity/point, then think about heading * for it in a straight line instead of pathfinding. */ static const entity_pos_t DIRECT_PATH_RANGE = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*4); /** * To avoid recomputing paths too often, have some leeway for target range checks * based on our distance to the target. Increase that incertainty by one navcell * for every this many tiles of distance. */ static const entity_pos_t TARGET_UNCERTAINTY_MULTIPLIER = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*2); /** * When following a known imperfect path (i.e. a path that won't take us in range of our goal * we still recompute a new path every N turn to adapt to moving targets (for example, ships that must pickup * units may easily end up in this state, they still need to adjust to moving units). * This is rather arbitrary and mostly for simplicity & optimisation (a better recomputing algorithm * would not need this). * Keep in mind that MP turns are currently 500ms. */ static const u8 KNOWN_IMPERFECT_PATH_RESET_COUNTDOWN = 12; /** * When we fail more than this many path computations in a row, inform other components that the move will fail. * Experimentally, this number needs to be somewhat high or moving groups of units will lead to stuck units. * However, too high means units will look idle for a long time when they are failing to move. * TODO: if UnitMotion could send differentiated "unreachable" and "currently stuck" failing messages, * this could probably be lowered. * TODO: when unit pushing is implemented, this number can probably be lowered. */ static const u8 MAX_FAILED_PATH_COMPUTATIONS = 15; /** * If we have failed path computations this many times and ComputePathToGoal is called, * always run a long-path, to avoid getting stuck sometimes (see D1424). */ static const u8 MAX_FAILED_PATH_COMPUTATIONS_BEFORE_LONG_PATH = 3; static const CColor OVERLAY_COLOR_LONG_PATH(1, 1, 1, 1); static const CColor OVERLAY_COLOR_SHORT_PATH(1, 0, 0, 1); class CCmpUnitMotion : public ICmpUnitMotion { public: static void ClassInit(CComponentManager& componentManager) { componentManager.SubscribeToMessageType(MT_Update_MotionFormation); componentManager.SubscribeToMessageType(MT_Update_MotionUnit); componentManager.SubscribeToMessageType(MT_PathResult); componentManager.SubscribeToMessageType(MT_OwnershipChanged); componentManager.SubscribeToMessageType(MT_ValueModification); componentManager.SubscribeToMessageType(MT_Deserialized); } DEFAULT_COMPONENT_ALLOCATOR(UnitMotion) bool m_DebugOverlayEnabled; std::vector m_DebugOverlayLongPathLines; std::vector m_DebugOverlayShortPathLines; // Template state: bool m_FormationController; fixed m_TemplateWalkSpeed, m_TemplateRunMultiplier; pass_class_t m_PassClass; std::string m_PassClassName; // Dynamic state: entity_pos_t m_Clearance; // cached for efficiency fixed m_WalkSpeed, m_RunMultiplier; bool m_FacePointAfterMove; // Number of path computations that failed (in a row). // When this gets above MAX_FAILED_PATH_COMPUTATIONS, inform other components // that the move will likely fail. u8 m_FailedPathComputations = 0; // If > 0, PathingUpdateNeeded returns false always. // This exists because the goal may be unreachable to the short/long pathfinder. // In such cases, we would compute inacceptable paths and PathingUpdateNeeded would trigger every turn, // which would be quite bad for performance. // To avoid that, when we know the new path is imperfect, treat it as OK and follow it anyways. // When reaching the end, we'll go through HandleObstructedMove and reset regardless. // To still recompute now and then (the target may be moving), this is a countdown decremented on each frame. u8 m_FollowKnownImperfectPathCountdown = 0; struct Ticket { u32 m_Ticket = 0; // asynchronous request ID we're waiting for, or 0 if none enum Type { SHORT_PATH, LONG_PATH } m_Type = SHORT_PATH; // Pick some default value to avoid UB. void clear() { m_Ticket = 0; } } m_ExpectedPathTicket; struct MoveRequest { enum Type { NONE, POINT, ENTITY, OFFSET } m_Type = NONE; entity_id_t m_Entity = INVALID_ENTITY; CFixedVector2D m_Position; entity_pos_t m_MinRange, m_MaxRange; // For readability CFixedVector2D GetOffset() const { return m_Position; }; MoveRequest() = default; MoveRequest(CFixedVector2D pos, entity_pos_t minRange, entity_pos_t maxRange) : m_Type(POINT), m_Position(pos), m_MinRange(minRange), m_MaxRange(maxRange) {}; MoveRequest(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) : m_Type(ENTITY), m_Entity(target), m_MinRange(minRange), m_MaxRange(maxRange) {}; MoveRequest(entity_id_t target, CFixedVector2D offset) : m_Type(OFFSET), m_Entity(target), m_Position(offset) {}; } m_MoveRequest; // If the entity moves, it will do so at m_WalkSpeed * m_SpeedMultiplier. fixed m_SpeedMultiplier; // This caches the resulting speed from m_WalkSpeed * m_SpeedMultiplier for convenience. fixed m_Speed; // Current mean speed (over the last turn). fixed m_CurSpeed; // Currently active paths (storing waypoints in reverse order). // The last item in each path is the point we're currently heading towards. WaypointPath m_LongPath; WaypointPath m_ShortPath; static std::string GetSchema() { return "Provides the unit with the ability to move around the world by itself." "" "7.0" "default" "" "" "" "" "" "" "" "" "" "" "" "" "" "" ""; } virtual void Init(const CParamNode& paramNode) { m_FormationController = paramNode.GetChild("FormationController").ToBool(); m_FacePointAfterMove = true; m_WalkSpeed = m_TemplateWalkSpeed = m_Speed = paramNode.GetChild("WalkSpeed").ToFixed(); m_SpeedMultiplier = fixed::FromInt(1); m_CurSpeed = fixed::Zero(); m_RunMultiplier = m_TemplateRunMultiplier = fixed::FromInt(1); if (paramNode.GetChild("RunMultiplier").IsOk()) m_RunMultiplier = m_TemplateRunMultiplier = paramNode.GetChild("RunMultiplier").ToFixed(); CmpPtr cmpPathfinder(GetSystemEntity()); if (cmpPathfinder) { m_PassClassName = paramNode.GetChild("PassabilityClass").ToUTF8(); m_PassClass = cmpPathfinder->GetPassabilityClass(m_PassClassName); m_Clearance = cmpPathfinder->GetClearance(m_PassClass); CmpPtr cmpObstruction(GetEntityHandle()); if (cmpObstruction) cmpObstruction->SetUnitClearance(m_Clearance); } m_DebugOverlayEnabled = false; } virtual void Deinit() { } template void SerializeCommon(S& serialize) { serialize.StringASCII("pass class", m_PassClassName, 0, 64); serialize.NumberU32_Unbounded("ticket", m_ExpectedPathTicket.m_Ticket); SerializeU8_Enum()(serialize, "ticket type", m_ExpectedPathTicket.m_Type); serialize.NumberU8_Unbounded("failed path computations", m_FailedPathComputations); serialize.NumberU8_Unbounded("followknownimperfectpath", m_FollowKnownImperfectPathCountdown); SerializeU8_Enum()(serialize, "target type", m_MoveRequest.m_Type); serialize.NumberU32_Unbounded("target entity", m_MoveRequest.m_Entity); serialize.NumberFixed_Unbounded("target pos x", m_MoveRequest.m_Position.X); serialize.NumberFixed_Unbounded("target pos y", m_MoveRequest.m_Position.Y); serialize.NumberFixed_Unbounded("target min range", m_MoveRequest.m_MinRange); serialize.NumberFixed_Unbounded("target max range", m_MoveRequest.m_MaxRange); serialize.NumberFixed_Unbounded("speed multiplier", m_SpeedMultiplier); serialize.NumberFixed_Unbounded("current speed", m_CurSpeed); serialize.Bool("facePointAfterMove", m_FacePointAfterMove); SerializeVector()(serialize, "long path", m_LongPath.m_Waypoints); SerializeVector()(serialize, "short path", m_ShortPath.m_Waypoints); } virtual void Serialize(ISerializer& serialize) { SerializeCommon(serialize); } virtual void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize) { Init(paramNode); SerializeCommon(deserialize); CmpPtr cmpPathfinder(GetSystemEntity()); if (cmpPathfinder) m_PassClass = cmpPathfinder->GetPassabilityClass(m_PassClassName); } virtual void HandleMessage(const CMessage& msg, bool UNUSED(global)) { switch (msg.GetType()) { case MT_Update_MotionFormation: { if (m_FormationController) { fixed dt = static_cast (msg).turnLength; Move(dt); } break; } case MT_Update_MotionUnit: { if (!m_FormationController) { fixed dt = static_cast (msg).turnLength; Move(dt); } break; } case MT_RenderSubmit: { PROFILE("UnitMotion::RenderSubmit"); const CMessageRenderSubmit& msgData = static_cast (msg); RenderSubmit(msgData.collector); break; } case MT_PathResult: { const CMessagePathResult& msgData = static_cast (msg); PathResult(msgData.ticket, msgData.path); break; } case MT_ValueModification: { const CMessageValueModification& msgData = static_cast (msg); if (msgData.component != L"UnitMotion") break; FALLTHROUGH; } case MT_OwnershipChanged: case MT_Deserialized: { CmpPtr cmpValueModificationManager(GetSystemEntity()); if (!cmpValueModificationManager) break; m_WalkSpeed = cmpValueModificationManager->ApplyModifications(L"UnitMotion/WalkSpeed", m_TemplateWalkSpeed, GetEntityId()); m_RunMultiplier = cmpValueModificationManager->ApplyModifications(L"UnitMotion/RunMultiplier", m_TemplateRunMultiplier, GetEntityId()); // For MT_Deserialize compute m_Speed from the serialized m_SpeedMultiplier. // For MT_ValueModification and MT_OwnershipChanged, adjust m_SpeedMultiplier if needed // (in case then new m_RunMultiplier value is lower than the old). SetSpeedMultiplier(m_SpeedMultiplier); break; } } } void UpdateMessageSubscriptions() { bool needRender = m_DebugOverlayEnabled; GetSimContext().GetComponentManager().DynamicSubscriptionNonsync(MT_RenderSubmit, this, needRender); } virtual bool IsMoveRequested() const { return m_MoveRequest.m_Type != MoveRequest::NONE; } virtual fixed GetSpeedMultiplier() const { return m_SpeedMultiplier; } virtual void SetSpeedMultiplier(fixed multiplier) { m_SpeedMultiplier = std::min(multiplier, m_RunMultiplier); m_Speed = m_SpeedMultiplier.Multiply(GetWalkSpeed()); } virtual fixed GetSpeed() const { return m_Speed; } virtual fixed GetWalkSpeed() const { return m_WalkSpeed; } virtual fixed GetRunMultiplier() const { return m_RunMultiplier; } virtual pass_class_t GetPassabilityClass() const { return m_PassClass; } virtual std::string GetPassabilityClassName() const { return m_PassClassName; } virtual void SetPassabilityClassName(const std::string& passClassName) { m_PassClassName = passClassName; CmpPtr cmpPathfinder(GetSystemEntity()); if (cmpPathfinder) m_PassClass = cmpPathfinder->GetPassabilityClass(passClassName); } virtual fixed GetCurrentSpeed() const { return m_CurSpeed; } virtual void SetFacePointAfterMove(bool facePointAfterMove) { m_FacePointAfterMove = facePointAfterMove; } virtual bool GetFacePointAfterMove() const { return m_FacePointAfterMove; } virtual void SetDebugOverlay(bool enabled) { m_DebugOverlayEnabled = enabled; UpdateMessageSubscriptions(); } virtual bool MoveToPointRange(entity_pos_t x, entity_pos_t z, entity_pos_t minRange, entity_pos_t maxRange) { return MoveTo(MoveRequest(CFixedVector2D(x, z), minRange, maxRange)); } virtual bool MoveToTargetRange(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) { return MoveTo(MoveRequest(target, minRange, maxRange)); } virtual void MoveToFormationOffset(entity_id_t target, entity_pos_t x, entity_pos_t z) { MoveTo(MoveRequest(target, CFixedVector2D(x, z))); } virtual bool IsTargetRangeReachable(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange); virtual void FaceTowardsPoint(entity_pos_t x, entity_pos_t z); /** * Clears the current MoveRequest - the unit will stop and no longer try and move. * This should never be called from UnitMotion, since MoveToX orders are given * by other components - these components should also decide when to stop. */ virtual void StopMoving() { if (m_FacePointAfterMove) { CmpPtr cmpPosition(GetEntityHandle()); if (cmpPosition && cmpPosition->IsInWorld()) { CFixedVector2D targetPos; if (ComputeTargetPosition(targetPos)) FaceTowardsPointFromPos(cmpPosition->GetPosition2D(), targetPos.X, targetPos.Y); } } m_MoveRequest = MoveRequest(); m_ExpectedPathTicket.clear(); m_LongPath.m_Waypoints.clear(); m_ShortPath.m_Waypoints.clear(); } virtual entity_pos_t GetUnitClearance() const { return m_Clearance; } private: bool ShouldAvoidMovingUnits() const { return !m_FormationController; } bool IsFormationMember() const { // TODO: this really shouldn't be what we are checking for. return m_MoveRequest.m_Type == MoveRequest::OFFSET; } bool IsFormationControllerMoving() const { CmpPtr cmpControllerMotion(GetSimContext(), m_MoveRequest.m_Entity); return cmpControllerMotion && cmpControllerMotion->IsMoveRequested(); } entity_id_t GetGroup() const { return IsFormationMember() ? m_MoveRequest.m_Entity : GetEntityId(); } /** * Warns other components that our current movement will likely fail (e.g. we won't be able to reach our target) * This should only be called before the actual movement in a given turn, or units might both move and try to do things * on the same turn, leading to gliding units. */ void MoveFailed() { // Don't notify if we are a formation member in a moving formation - we can occasionally be stuck for a long time // if our current offset is unreachable, but we don't want to end up stuck. // (If the formation controller has stopped moving however, we can safely message). if (IsFormationMember() && IsFormationControllerMoving()) return; CMessageMotionUpdate msg(CMessageMotionUpdate::LIKELY_FAILURE); GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); } /** * Warns other components that our current movement is likely over (i.e. we probably reached our destination) * This should only be called before the actual movement in a given turn, or units might both move and try to do things * on the same turn, leading to gliding units. */ void MoveSucceeded() { // Don't notify if we are a formation member in a moving formation - we can occasionally be stuck for a long time // if our current offset is unreachable, but we don't want to end up stuck. // (If the formation controller has stopped moving however, we can safely message). if (IsFormationMember() && IsFormationControllerMoving()) return; CMessageMotionUpdate msg(CMessageMotionUpdate::LIKELY_SUCCESS); GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); } /** * Warns other components that our current movement was obstructed (i.e. we failed to move this turn). * This should only be called before the actual movement in a given turn, or units might both move and try to do things * on the same turn, leading to gliding units. */ void MoveObstructed() { // Don't notify if we are a formation member in a moving formation - we can occasionally be stuck for a long time // if our current offset is unreachable, but we don't want to end up stuck. // (If the formation controller has stopped moving however, we can safely message). if (IsFormationMember() && IsFormationControllerMoving()) return; CMessageMotionUpdate msg(CMessageMotionUpdate::OBSTRUCTED); GetSimContext().GetComponentManager().PostMessage(GetEntityId(), msg); } /** * Increment the number of failed path computations and notify other components if required. * @returns true if the failure was notified, false otherwise. */ bool IncrementFailedPathComputationAndMaybeNotify() { m_FailedPathComputations++; if (m_FailedPathComputations >= MAX_FAILED_PATH_COMPUTATIONS) { MoveFailed(); m_FailedPathComputations = 0; return true; } return false; } /** * If path would take us farther away from the goal than pos currently is, return false, else return true. */ bool RejectFartherPaths(const PathGoal& goal, const WaypointPath& path, const CFixedVector2D& pos) const; /** * If there are 2 waypoints of more remaining in longPath, return SHORT_PATH_LONG_WAYPOINT_RANGE. * Otherwise the pathing should be exact. */ entity_pos_t ShortPathWaypointRange(const WaypointPath& longPath) const { return longPath.m_Waypoints.size() >= 2 ? SHORT_PATH_LONG_WAYPOINT_RANGE : entity_pos_t::Zero(); } bool InShortPathRange(const PathGoal& goal, const CFixedVector2D& pos) const { return goal.DistanceToPoint(pos) < LONG_PATH_MIN_DIST; } /** * Handle the result of an asynchronous path query. */ void PathResult(u32 ticket, const WaypointPath& path); /** * Do the per-turn movement and other updates. */ void Move(fixed dt); /** * Returns true if we are possibly at our destination. * Since the concept of being at destination is dependent on why the move was requested, * UnitMotion can only ever hint about this, hence the conditional tone. */ bool PossiblyAtDestination() const; /** * Process the move the unit will do this turn. * This does not send actually change the position. * @returns true if the move was obstructed. */ bool PerformMove(fixed dt, WaypointPath& shortPath, WaypointPath& longPath, CFixedVector2D& pos) const; /** * Update other components on our speed. * (For performance, this should try to avoid sending messages). */ void UpdateMovementState(entity_pos_t speed); /** * React if our move was obstructed. * @returns true if the obstruction required handling, false otherwise. */ bool HandleObstructedMove(); /** * Returns true if the target position is valid. False otherwise. * (this may indicate that the target is e.g. out of the world/dead). * NB: for code-writing convenience, if we have no target, this returns true. */ bool TargetHasValidPosition(const MoveRequest& moveRequest) const; bool TargetHasValidPosition() const { return TargetHasValidPosition(m_MoveRequest); } /** * Computes the current location of our target entity (plus offset). * Returns false if no target entity or no valid position. */ bool ComputeTargetPosition(CFixedVector2D& out, const MoveRequest& moveRequest) const; bool ComputeTargetPosition(CFixedVector2D& out) const { return ComputeTargetPosition(out, m_MoveRequest); } /** * Attempts to replace the current path with a straight line to the target, * if it's close enough and the route is not obstructed. */ bool TryGoingStraightToTarget(const CFixedVector2D& from); /** * Returns whether our we need to recompute a path to reach our target. */ bool PathingUpdateNeeded(const CFixedVector2D& from) const; /** * Rotate to face towards the target point, given the current pos */ void FaceTowardsPointFromPos(const CFixedVector2D& pos, entity_pos_t x, entity_pos_t z); /** * Returns an appropriate obstruction filter for use with path requests. */ ControlGroupMovementObstructionFilter GetObstructionFilter() const; /** * Decide whether to approximate the given range from a square target as a circle, * rather than as a square. */ bool ShouldTreatTargetAsCircle(entity_pos_t range, entity_pos_t circleRadius) const; /** * Create a PathGoal from a move request. * @returns true if the goal was successfully created. */ bool ComputeGoal(PathGoal& out, const MoveRequest& moveRequest) const; /** * Compute a path to the given goal from the given position. * Might go in a straight line immediately, or might start an asynchronous path request. */ void ComputePathToGoal(const CFixedVector2D& from, const PathGoal& goal); /** * Start an asynchronous long path query. */ void RequestLongPath(const CFixedVector2D& from, const PathGoal& goal); /** * Start an asynchronous short path query. */ void RequestShortPath(const CFixedVector2D& from, const PathGoal& goal, bool avoidMovingUnits); /** * General handler for MoveTo interface functions. */ bool MoveTo(MoveRequest request); /** * Convert a path into a renderable list of lines */ void RenderPath(const WaypointPath& path, std::vector& lines, CColor color); void RenderSubmit(SceneCollector& collector); }; REGISTER_COMPONENT_TYPE(UnitMotion) bool CCmpUnitMotion::RejectFartherPaths(const PathGoal& goal, const WaypointPath& path, const CFixedVector2D& pos) const { if (path.m_Waypoints.empty()) return false; // Reject the new path if it does not lead us closer to the target's position. if (goal.DistanceToPoint(pos) <= goal.DistanceToPoint(CFixedVector2D(path.m_Waypoints.front().x, path.m_Waypoints.front().z))) return true; return false; } void CCmpUnitMotion::PathResult(u32 ticket, const WaypointPath& path) { // Ignore obsolete path requests if (ticket != m_ExpectedPathTicket.m_Ticket || m_MoveRequest.m_Type == MoveRequest::NONE) return; Ticket::Type ticketType = m_ExpectedPathTicket.m_Type; m_ExpectedPathTicket.clear(); // Check that we are still able to do something with that path CmpPtr cmpPosition(GetEntityHandle()); if (!cmpPosition || !cmpPosition->IsInWorld()) { // We will probably fail to move so inform components but keep on trying anyways. MoveFailed(); return; } CFixedVector2D pos = cmpPosition->GetPosition2D(); PathGoal goal; // If we can't compute a goal, we'll fail in the next Move() call so do nothing special. if (!ComputeGoal(goal, m_MoveRequest)) return; if (ticketType == Ticket::LONG_PATH) { if (RejectFartherPaths(goal, path, pos)) { IncrementFailedPathComputationAndMaybeNotify(); return; } m_LongPath = path; m_FollowKnownImperfectPathCountdown = 0; // If there's no waypoints then we couldn't get near the target. // Sort of hack: Just try going directly to the goal point instead // (via the short pathfinder over the next turns), so if we're stuck and the user clicks // close enough to the unit then we can probably get unstuck // NB: this relies on HandleObstructedMove requesting short paths if we still have long waypoints. if (m_LongPath.m_Waypoints.empty()) { IncrementFailedPathComputationAndMaybeNotify(); CFixedVector2D targetPos; if (ComputeTargetPosition(targetPos)) m_LongPath.m_Waypoints.emplace_back(Waypoint{ targetPos.X, targetPos.Y }); } // If this new path won't put us in range, it's highly likely that we are going somewhere unreachable. // This means we will try to recompute the path every turn. // To avoid this, act as if our current path leads us to the correct destination. // (we will still fail the move when we arrive to the best possible position, and if we were blocked by // an obstruction and it goes away we will notice when getting there as having no waypoint goes through // HandleObstructedMove, so this is safe). else if (PathingUpdateNeeded(pos)) { // Inform other components early, as they might have better behaviour than waiting for the path to carry out. // Send OBSTRUCTED at first - moveFailed is likely to trigger path recomputation and we might end up // recomputing too often for nothing. if (!IncrementFailedPathComputationAndMaybeNotify()) MoveObstructed(); m_FollowKnownImperfectPathCountdown = KNOWN_IMPERFECT_PATH_RESET_COUNTDOWN; } return; } // Reject new short paths if they were aiming at the goal directly (i.e. no long waypoints still exists). if (m_LongPath.m_Waypoints.empty() && RejectFartherPaths(goal, path, pos)) { IncrementFailedPathComputationAndMaybeNotify(); return; } m_ShortPath = path; m_FollowKnownImperfectPathCountdown = 0; if (!m_ShortPath.m_Waypoints.empty()) { if (PathingUpdateNeeded(pos)) { // Inform other components early, as they might have better behaviour than waiting for the path to carry out. // Send OBSTRUCTED at first - moveFailed is likely to trigger path recomputation and we might end up // recomputing too often for nothing. if (!IncrementFailedPathComputationAndMaybeNotify()) MoveObstructed(); m_FollowKnownImperfectPathCountdown = KNOWN_IMPERFECT_PATH_RESET_COUNTDOWN; } return; } if (m_FailedPathComputations >= 1) // Inform other components - we might be ordered to stop, and computeGoal will then fail and return early. MoveObstructed(); IncrementFailedPathComputationAndMaybeNotify(); // If there's no waypoints then we couldn't get near the target // If we're globally following a long path, try to remove the next waypoint, // it might be obstructed (e.g. by idle entities which the long-range pathfinder doesn't see). if (!m_LongPath.m_Waypoints.empty()) { m_LongPath.m_Waypoints.pop_back(); if (!m_LongPath.m_Waypoints.empty()) { // Get close enough - this will likely help the short path efficiency, and if we end up taking a wrong way // we'll easily be able to revert it using a long path. goal = { PathGoal::CIRCLE, m_LongPath.m_Waypoints.back().x, m_LongPath.m_Waypoints.back().z, ShortPathWaypointRange(m_LongPath) }; RequestShortPath(pos, goal, true); return; } } ComputePathToGoal(pos, goal); } void CCmpUnitMotion::Move(fixed dt) { PROFILE("Move"); // If we were idle and will still be, we can return. // TODO: this will need to be removed if pushing is implemented. if (m_CurSpeed == fixed::Zero() && m_MoveRequest.m_Type == MoveRequest::NONE) return; if (PossiblyAtDestination()) MoveSucceeded(); else if (!TargetHasValidPosition()) { // Scrap waypoints - we don't know where to go. // If the move request remains unchanged and the target again has a valid position later on, // moving will be resumed. // Units may want to move to move to the target's last known position, // but that should be decided by UnitAI (handling MoveFailed), not UnitMotion. m_LongPath.m_Waypoints.clear(); m_ShortPath.m_Waypoints.clear(); MoveFailed(); } CmpPtr cmpPosition(GetEntityHandle()); if (!cmpPosition || !cmpPosition->IsInWorld()) return; CFixedVector2D initialPos = cmpPosition->GetPosition2D(); // Keep track of the current unit's position during the update CFixedVector2D pos = initialPos; // If we're chasing a potentially-moving unit and are currently close // enough to its current position, and we can head in a straight line // to it, then throw away our current path and go straight to it bool wentStraight = TryGoingStraightToTarget(initialPos); bool wasObstructed = PerformMove(dt, m_ShortPath, m_LongPath, pos); // Update our speed over this turn so that the visual actor shows the correct animation. if (pos == initialPos) UpdateMovementState(fixed::Zero()); else { // Update the Position component after our movement (if we actually moved anywhere) CFixedVector2D offset = pos - initialPos; // Face towards the target entity_angle_t angle = atan2_approx(offset.X, offset.Y); cmpPosition->MoveAndTurnTo(pos.X,pos.Y, angle); // Calculate the mean speed over this past turn. UpdateMovementState(offset.Length() / dt); } if (wasObstructed && HandleObstructedMove()) return; else if (!wasObstructed) m_FailedPathComputations = 0; // We may need to recompute our path sometimes (e.g. if our target moves). // Since we request paths asynchronously anyways, this does not need to be done before moving. if (!wentStraight && PathingUpdateNeeded(pos)) { PathGoal goal; if (ComputeGoal(goal, m_MoveRequest)) ComputePathToGoal(pos, goal); } else if (m_FollowKnownImperfectPathCountdown > 0) --m_FollowKnownImperfectPathCountdown; } bool CCmpUnitMotion::PossiblyAtDestination() const { if (m_MoveRequest.m_Type == MoveRequest::NONE) return false; CmpPtr cmpObstructionManager(GetSystemEntity()); ENSURE(cmpObstructionManager); if (m_MoveRequest.m_Type == MoveRequest::POINT) return cmpObstructionManager->IsInPointRange(GetEntityId(), m_MoveRequest.m_Position.X, m_MoveRequest.m_Position.Y, m_MoveRequest.m_MinRange, m_MoveRequest.m_MaxRange, false); if (m_MoveRequest.m_Type == MoveRequest::ENTITY) return cmpObstructionManager->IsInTargetRange(GetEntityId(), m_MoveRequest.m_Entity, m_MoveRequest.m_MinRange, m_MoveRequest.m_MaxRange, false); if (m_MoveRequest.m_Type == MoveRequest::OFFSET) { CmpPtr cmpControllerMotion(GetSimContext(), m_MoveRequest.m_Entity); if (cmpControllerMotion && cmpControllerMotion->IsMoveRequested()) return false; CFixedVector2D targetPos; ComputeTargetPosition(targetPos); CmpPtr cmpPosition(GetEntityHandle()); return cmpObstructionManager->IsInPointRange(GetEntityId(), targetPos.X, targetPos.Y, m_MoveRequest.m_MinRange, m_MoveRequest.m_MaxRange, false); } return false; } bool CCmpUnitMotion::PerformMove(fixed dt, WaypointPath& shortPath, WaypointPath& longPath, CFixedVector2D& pos) const { // If there are no waypoint, behave as though we were obstructed and let HandleObstructedMove handle it. if (shortPath.m_Waypoints.empty() && longPath.m_Waypoints.empty()) return true; // TODO: there's some asymmetry here when units look at other // units' positions - the result will depend on the order of execution. // Maybe we should split the updates into multiple phases to minimise // that problem. CmpPtr cmpPathfinder(GetSystemEntity()); ENSURE(cmpPathfinder); fixed basicSpeed = m_Speed; // If in formation, run to keep up; otherwise just walk if (IsFormationMember()) basicSpeed = m_Speed.Multiply(m_RunMultiplier); // Find the speed factor of the underlying terrain // (We only care about the tile we start on - it doesn't matter if we're moving // partially onto a much slower/faster tile) // TODO: Terrain-dependent speeds are not currently supported fixed terrainSpeed = fixed::FromInt(1); fixed maxSpeed = basicSpeed.Multiply(terrainSpeed); // We want to move (at most) maxSpeed*dt units from pos towards the next waypoint fixed timeLeft = dt; fixed zero = fixed::Zero(); while (timeLeft > zero) { // If we ran out of path, we have to stop if (shortPath.m_Waypoints.empty() && longPath.m_Waypoints.empty()) break; CFixedVector2D target; if (shortPath.m_Waypoints.empty()) target = CFixedVector2D(longPath.m_Waypoints.back().x, longPath.m_Waypoints.back().z); else target = CFixedVector2D(shortPath.m_Waypoints.back().x, shortPath.m_Waypoints.back().z); CFixedVector2D offset = target - pos; // Work out how far we can travel in timeLeft fixed maxdist = maxSpeed.Multiply(timeLeft); // If the target is close, we can move there directly fixed offsetLength = offset.Length(); if (offsetLength <= maxdist) { if (cmpPathfinder->CheckMovement(GetObstructionFilter(), pos.X, pos.Y, target.X, target.Y, m_Clearance, m_PassClass)) { pos = target; // Spend the rest of the time heading towards the next waypoint timeLeft = (maxdist - offsetLength) / maxSpeed; if (shortPath.m_Waypoints.empty()) longPath.m_Waypoints.pop_back(); else shortPath.m_Waypoints.pop_back(); continue; } else { // Error - path was obstructed return true; } } else { // Not close enough, so just move in the right direction offset.Normalize(maxdist); target = pos + offset; if (cmpPathfinder->CheckMovement(GetObstructionFilter(), pos.X, pos.Y, target.X, target.Y, m_Clearance, m_PassClass)) pos = target; else return true; break; } } return false; } void CCmpUnitMotion::UpdateMovementState(entity_pos_t speed) { CmpPtr cmpObstruction(GetEntityHandle()); CmpPtr cmpVisual(GetEntityHandle()); // Moved last turn, didn't this turn. if (speed == fixed::Zero() && m_CurSpeed > fixed::Zero()) { if (cmpObstruction) cmpObstruction->SetMovingFlag(false); if (cmpVisual) cmpVisual->SelectMovementAnimation("idle", fixed::FromInt(1)); } // Moved this turn, didn't last turn else if (speed > fixed::Zero() && m_CurSpeed == fixed::Zero()) { if (cmpObstruction) cmpObstruction->SetMovingFlag(true); if (cmpVisual) cmpVisual->SelectMovementAnimation(m_Speed > m_WalkSpeed ? "run" : "walk", m_Speed); } // Speed change, update the visual actor if necessary. else if (speed != m_CurSpeed && cmpVisual) cmpVisual->SelectMovementAnimation(m_Speed > m_WalkSpeed ? "run" : "walk", m_Speed); m_CurSpeed = speed; } bool CCmpUnitMotion::HandleObstructedMove() { CmpPtr cmpPosition(GetEntityHandle()); if (!cmpPosition || !cmpPosition->IsInWorld()) return false; if (m_FailedPathComputations >= 1) // Inform other components - we might be ordered to stop, and computeGoal will then fail and return early. MoveObstructed(); PathGoal goal; if (!ComputeGoal(goal, m_MoveRequest)) return false; // At this point we have a position in the world since ComputeGoal checked for that. CFixedVector2D pos = cmpPosition->GetPosition2D(); if (!InShortPathRange(goal, pos)) { // If we still have long waypoints, try and compute a short path to our next long waypoint. // Assume the next waypoint is impassable and pop it. This helps unstuck entities in some cases, and we'll just // end up recomputing a long path if we pop all of them, so it's safe. if (m_LongPath.m_Waypoints.size() >= 1) m_LongPath.m_Waypoints.pop_back(); if (!m_LongPath.m_Waypoints.empty()) { // Get close enough - this will likely help the short path efficiency, and if we end up taking a wrong way // we'll easily be able to revert it using a long path. goal = { PathGoal::CIRCLE, m_LongPath.m_Waypoints.back().x, m_LongPath.m_Waypoints.back().z, ShortPathWaypointRange(m_LongPath) }; RequestShortPath(pos, goal, true); return true; } } // Else, just entirely recompute. This will ensure we occasionally run a long path so avoid getting stuck // in the short pathfinder, which can happen when an entity is right ober an obstruction's edge. ComputePathToGoal(pos, goal); // potential TODO: We could switch the short-range pathfinder for something else entirely. return true; } bool CCmpUnitMotion::TargetHasValidPosition(const MoveRequest& moveRequest) const { if (moveRequest.m_Type != MoveRequest::ENTITY) return true; CmpPtr cmpPosition(GetSimContext(), moveRequest.m_Entity); return cmpPosition && cmpPosition->IsInWorld(); } bool CCmpUnitMotion::ComputeTargetPosition(CFixedVector2D& out, const MoveRequest& moveRequest) const { if (moveRequest.m_Type == MoveRequest::POINT) { out = moveRequest.m_Position; return true; } CmpPtr cmpTargetPosition(GetSimContext(), moveRequest.m_Entity); if (!cmpTargetPosition || !cmpTargetPosition->IsInWorld()) return false; if (moveRequest.m_Type == MoveRequest::OFFSET) { // There is an offset, so compute it relative to orientation entity_angle_t angle = cmpTargetPosition->GetRotation().Y; CFixedVector2D offset = moveRequest.GetOffset().Rotate(angle); out = cmpTargetPosition->GetPosition2D() + offset; } else { out = cmpTargetPosition->GetPosition2D(); // If the target is moving, we might never get in range if we just try to reach its current position, // so we have to try and move to a position where we will be in-range, including their movement. // Since we request paths asynchronously a the end of our turn and the order in which two units move is uncertain, // we need to account for twice the movement speed to be sure that we're targeting the correct point. // TODO: be cleverer about this. It fixes fleeing nicely currently, but orthogonal movement should be considered, // and the overall logic could be improved upon. CmpPtr cmpUnitMotion(GetSimContext(), moveRequest.m_Entity); if (cmpUnitMotion && cmpUnitMotion->IsMoveRequested()) { CmpPtr cmpPosition(GetEntityHandle()); if (!cmpPosition || !cmpPosition->IsInWorld()) return true; // Still return true since we don't need a position for the target to have one. CFixedVector2D tempPos = out + (out - cmpTargetPosition->GetPreviousPosition2D()) * 2; // Check if we anticipate the target to go through us, in which case we shouldn't anticipate // (or e.g. units fleeing might suddenly turn around towards their attacker). if ((out - cmpPosition->GetPosition2D()).RelativeOrientation(tempPos - cmpPosition->GetPosition2D()) >= 0) out = tempPos; } } return true; } bool CCmpUnitMotion::TryGoingStraightToTarget(const CFixedVector2D& from) { CFixedVector2D targetPos; if (!ComputeTargetPosition(targetPos)) return false; // Fail if the target is too far away if ((targetPos - from).CompareLength(DIRECT_PATH_RANGE) > 0) return false; CmpPtr cmpPathfinder(GetSystemEntity()); if (!cmpPathfinder) return false; // Move the goal to match the target entity's new position PathGoal goal; if (!ComputeGoal(goal, m_MoveRequest)) return false; goal.x = targetPos.X; goal.z = targetPos.Y; // (we ignore changes to the target's rotation, since only buildings are // square and buildings don't move) // Find the point on the goal shape that we should head towards CFixedVector2D goalPos = goal.NearestPointOnGoal(from); // Check if there's any collisions on that route. // For entity goals, skip only the specific obstruction tag or with e.g. walls we might ignore too many entities. ICmpObstructionManager::tag_t specificIgnore; if (m_MoveRequest.m_Type == MoveRequest::ENTITY) { CmpPtr cmpTargetObstruction(GetSimContext(), m_MoveRequest.m_Entity); if (cmpTargetObstruction) specificIgnore = cmpTargetObstruction->GetObstruction(); } if (specificIgnore.valid()) { if (!cmpPathfinder->CheckMovement(SkipTagObstructionFilter(specificIgnore), from.X, from.Y, goalPos.X, goalPos.Y, m_Clearance, m_PassClass)) return false; } else if (!cmpPathfinder->CheckMovement(GetObstructionFilter(), from.X, from.Y, goalPos.X, goalPos.Y, m_Clearance, m_PassClass)) return false; // That route is okay, so update our path m_LongPath.m_Waypoints.clear(); m_ShortPath.m_Waypoints.clear(); m_ShortPath.m_Waypoints.emplace_back(Waypoint{ goalPos.X, goalPos.Y }); return true; } bool CCmpUnitMotion::PathingUpdateNeeded(const CFixedVector2D& from) const { if (m_MoveRequest.m_Type == MoveRequest::NONE) return false; CFixedVector2D targetPos; if (!ComputeTargetPosition(targetPos)) return false; if (m_FollowKnownImperfectPathCountdown > 0) return false; if (PossiblyAtDestination()) return false; // Get the obstruction shape and translate it where we estimate the target to be. ICmpObstructionManager::ObstructionSquare estimatedTargetShape; if (m_MoveRequest.m_Type == MoveRequest::ENTITY) { CmpPtr cmpTargetObstruction(GetSimContext(), m_MoveRequest.m_Entity); if (cmpTargetObstruction) cmpTargetObstruction->GetObstructionSquare(estimatedTargetShape); } estimatedTargetShape.x = targetPos.X; estimatedTargetShape.z = targetPos.Y; CmpPtr cmpObstruction(GetEntityHandle()); ICmpObstructionManager::ObstructionSquare shape; if (cmpObstruction) cmpObstruction->GetObstructionSquare(shape); // Translate our own obstruction shape to our last waypoint or our current position, lacking that. if (m_LongPath.m_Waypoints.empty() && m_ShortPath.m_Waypoints.empty()) { shape.x = from.X; shape.z = from.Y; } else { const Waypoint& lastWaypoint = m_LongPath.m_Waypoints.empty() ? m_ShortPath.m_Waypoints.front() : m_LongPath.m_Waypoints.front(); shape.x = lastWaypoint.x; shape.z = lastWaypoint.z; } CmpPtr cmpObstructionManager(GetSystemEntity()); ENSURE(cmpObstructionManager); // Increase the ranges with distance, to avoid recomputing every turn against units that are moving and far-away for example. entity_pos_t distance = (from - CFixedVector2D(estimatedTargetShape.x, estimatedTargetShape.z)).Length(); // When in straight-path distance, we want perfect detection. distance = std::max(distance - DIRECT_PATH_RANGE, entity_pos_t::Zero()); // TODO: it could be worth computing this based on time to collision instead of linear distance. entity_pos_t minRange = std::max(m_MoveRequest.m_MinRange - distance / TARGET_UNCERTAINTY_MULTIPLIER, entity_pos_t::Zero()); entity_pos_t maxRange = m_MoveRequest.m_MaxRange < entity_pos_t::Zero() ? m_MoveRequest.m_MaxRange : m_MoveRequest.m_MaxRange + distance / TARGET_UNCERTAINTY_MULTIPLIER; if (cmpObstructionManager->AreShapesInRange(shape, estimatedTargetShape, minRange, maxRange, false)) return false; return true; } void CCmpUnitMotion::FaceTowardsPoint(entity_pos_t x, entity_pos_t z) { CmpPtr cmpPosition(GetEntityHandle()); if (!cmpPosition || !cmpPosition->IsInWorld()) return; CFixedVector2D pos = cmpPosition->GetPosition2D(); FaceTowardsPointFromPos(pos, x, z); } void CCmpUnitMotion::FaceTowardsPointFromPos(const CFixedVector2D& pos, entity_pos_t x, entity_pos_t z) { CFixedVector2D target(x, z); CFixedVector2D offset = target - pos; if (!offset.IsZero()) { entity_angle_t angle = atan2_approx(offset.X, offset.Y); CmpPtr cmpPosition(GetEntityHandle()); if (!cmpPosition) return; cmpPosition->TurnTo(angle); } } ControlGroupMovementObstructionFilter CCmpUnitMotion::GetObstructionFilter() const { return ControlGroupMovementObstructionFilter(ShouldAvoidMovingUnits(), GetGroup()); } // The pathfinder cannot go to "rounded rectangles" goals, which are what happens with square targets and a non-null range. // Depending on what the best approximation is, we either pretend the target is a circle or a square. // One needs to be careful that the approximated geometry will be in the range. bool CCmpUnitMotion::ShouldTreatTargetAsCircle(entity_pos_t range, entity_pos_t circleRadius) const { // Given a square, plus a target range we should reach, the shape at that distance // is a round-cornered square which we can approximate as either a circle or as a square. // Previously, we used the shape that minimized the worst-case error. // However that is unsage in some situations. So let's be less clever and // just check if our range is at least three times bigger than the circleradius return (range > circleRadius*3); } bool CCmpUnitMotion::ComputeGoal(PathGoal& out, const MoveRequest& moveRequest) const { if (moveRequest.m_Type == MoveRequest::NONE) return false; CmpPtr cmpPosition(GetEntityHandle()); if (!cmpPosition || !cmpPosition->IsInWorld()) return false; CFixedVector2D pos = cmpPosition->GetPosition2D(); CFixedVector2D targetPosition; if (!ComputeTargetPosition(targetPosition, moveRequest)) return false; ICmpObstructionManager::ObstructionSquare targetObstruction; if (moveRequest.m_Type == MoveRequest::ENTITY) { CmpPtr cmpTargetObstruction(GetSimContext(), moveRequest.m_Entity); if (cmpTargetObstruction) cmpTargetObstruction->GetObstructionSquare(targetObstruction); } targetObstruction.x = targetPosition.X; targetObstruction.z = targetPosition.Y; ICmpObstructionManager::ObstructionSquare obstruction; CmpPtr cmpObstruction(GetEntityHandle()); if (cmpObstruction) cmpObstruction->GetObstructionSquare(obstruction); else { obstruction.x = pos.X; obstruction.z = pos.Y; } CmpPtr cmpObstructionManager(GetSystemEntity()); ENSURE(cmpObstructionManager); entity_pos_t distance = cmpObstructionManager->DistanceBetweenShapes(obstruction, targetObstruction); out.x = targetObstruction.x; out.z = targetObstruction.z; out.hw = targetObstruction.hw; out.hh = targetObstruction.hh; out.u = targetObstruction.u; out.v = targetObstruction.v; if (moveRequest.m_MinRange > fixed::Zero() || moveRequest.m_MaxRange > fixed::Zero() || targetObstruction.hw > fixed::Zero()) out.type = PathGoal::SQUARE; else { out.type = PathGoal::POINT; return true; } entity_pos_t circleRadius = CFixedVector2D(targetObstruction.hw, targetObstruction.hh).Length(); // TODO: because we cannot move to rounded rectangles, we have to make conservative approximations. // This means we might end up in a situation where cons(max-range) < min range < max range < cons(min-range) // When going outside of the min-range or inside the max-range, the unit will still go through the correct range // but if it moves fast enough, this might not be picked up by PossiblyAtDestination(). // Fixing this involves moving to rounded rectangles, or checking more often in PerformMove(). // In the meantime, one should avoid that 'Speed over a turn' > MaxRange - MinRange, in case where // min-range is not 0 and max-range is not infinity. if (distance < moveRequest.m_MinRange) { // Distance checks are nearest edge to nearest edge, so we need to account for our clearance // and we must make sure diagonals also fit so multiply by slightly more than sqrt(2) entity_pos_t goalDistance = moveRequest.m_MinRange + m_Clearance * 3 / 2; if (ShouldTreatTargetAsCircle(moveRequest.m_MinRange, circleRadius)) { // We are safely away from the obstruction itself if we are away from the circumscribing circle out.type = PathGoal::INVERTED_CIRCLE; out.hw = circleRadius + goalDistance; } else { out.type = PathGoal::INVERTED_SQUARE; out.hw = targetObstruction.hw + goalDistance; out.hh = targetObstruction.hh + goalDistance; } } else if (moveRequest.m_MaxRange >= fixed::Zero() && distance > moveRequest.m_MaxRange) { if (ShouldTreatTargetAsCircle(moveRequest.m_MaxRange, circleRadius)) { entity_pos_t goalDistance = moveRequest.m_MaxRange; // We must go in-range of the inscribed circle, not the circumscribing circle. circleRadius = std::min(targetObstruction.hw, targetObstruction.hh); out.type = PathGoal::CIRCLE; out.hw = circleRadius + goalDistance; } else { // The target is large relative to our range, so treat it as a square and // get close enough that the diagonals come within range entity_pos_t goalDistance = moveRequest.m_MaxRange * 2 / 3; // multiply by slightly less than 1/sqrt(2) out.type = PathGoal::SQUARE; entity_pos_t delta = std::max(goalDistance, m_Clearance + entity_pos_t::FromInt(TERRAIN_TILE_SIZE)/16); // ensure it's far enough to not intersect the building itself out.hw = targetObstruction.hw + delta; out.hh = targetObstruction.hh + delta; } } // Do nothing in particular in case we are already in range. return true; } void CCmpUnitMotion::ComputePathToGoal(const CFixedVector2D& from, const PathGoal& goal) { #if DISABLE_PATHFINDER { CmpPtr cmpPathfinder (GetSimContext(), SYSTEM_ENTITY); CFixedVector2D goalPos = m_FinalGoal.NearestPointOnGoal(from); m_LongPath.m_Waypoints.clear(); m_ShortPath.m_Waypoints.clear(); m_ShortPath.m_Waypoints.emplace_back(Waypoint{ goalPos.X, goalPos.Y }); return; } #endif // If the target is close and we can reach it in a straight line, // then we'll just go along the straight line instead of computing a path. if (m_FailedPathComputations != MAX_FAILED_PATH_COMPUTATIONS_BEFORE_LONG_PATH && TryGoingStraightToTarget(from)) return; // Otherwise we need to compute a path. // If it's close then just do a short path, not a long path // TODO: If it's close on the opposite side of a river then we really // need a long path, so we shouldn't simply check linear distance // the check is arbitrary but should be a reasonably small distance. // To avoid getting stuck because the short-range pathfinder is bounded, occasionally compute a long path instead. if (m_FailedPathComputations != MAX_FAILED_PATH_COMPUTATIONS_BEFORE_LONG_PATH && InShortPathRange(goal, from)) { m_LongPath.m_Waypoints.clear(); RequestShortPath(from, goal, true); } else { if (m_FailedPathComputations == MAX_FAILED_PATH_COMPUTATIONS_BEFORE_LONG_PATH) m_FailedPathComputations++; // This makes sure we don't end up stuck in this special state which can break pathing. m_ShortPath.m_Waypoints.clear(); RequestLongPath(from, goal); } } void CCmpUnitMotion::RequestLongPath(const CFixedVector2D& from, const PathGoal& goal) { CmpPtr cmpPathfinder(GetSystemEntity()); if (!cmpPathfinder) return; // this is by how much our waypoints will be apart at most. // this value here seems sensible enough. PathGoal improvedGoal = goal; improvedGoal.maxdist = SHORT_PATH_MIN_SEARCH_RANGE - entity_pos_t::FromInt(1); cmpPathfinder->SetDebugPath(from.X, from.Y, improvedGoal, m_PassClass); m_ExpectedPathTicket.m_Type = Ticket::LONG_PATH; m_ExpectedPathTicket.m_Ticket = cmpPathfinder->ComputePathAsync(from.X, from.Y, improvedGoal, m_PassClass, GetEntityId()); } void CCmpUnitMotion::RequestShortPath(const CFixedVector2D &from, const PathGoal& goal, bool avoidMovingUnits) { CmpPtr cmpPathfinder(GetSystemEntity()); if (!cmpPathfinder) return; fixed searchRange = SHORT_PATH_MIN_SEARCH_RANGE + SHORT_PATH_SEARCH_RANGE_INCREMENT * m_FailedPathComputations; if (searchRange > SHORT_PATH_MAX_SEARCH_RANGE) searchRange = SHORT_PATH_MAX_SEARCH_RANGE; m_ExpectedPathTicket.m_Type = Ticket::SHORT_PATH; m_ExpectedPathTicket.m_Ticket = cmpPathfinder->ComputeShortPathAsync(from.X, from.Y, m_Clearance, searchRange, goal, m_PassClass, avoidMovingUnits, GetGroup(), GetEntityId()); } bool CCmpUnitMotion::MoveTo(MoveRequest request) { PROFILE("MoveTo"); + if (request.m_MinRange == request.m_MaxRange && !request.m_MinRange.IsZero()) + LOGWARNING("MaxRange must be larger than MinRange; See CCmpUnitMotion.cpp for more information"); + CmpPtr cmpPosition(GetEntityHandle()); if (!cmpPosition || !cmpPosition->IsInWorld()) return false; PathGoal goal; if (!ComputeGoal(goal, request)) return false; m_MoveRequest = request; m_FailedPathComputations = 0; m_FollowKnownImperfectPathCountdown = 0; ComputePathToGoal(cmpPosition->GetPosition2D(), goal); return true; } bool CCmpUnitMotion::IsTargetRangeReachable(entity_id_t target, entity_pos_t minRange, entity_pos_t maxRange) { CmpPtr cmpPosition(GetEntityHandle()); if (!cmpPosition || !cmpPosition->IsInWorld()) return false; MoveRequest request(target, minRange, maxRange); PathGoal goal; if (!ComputeGoal(goal, request)) return false; CmpPtr cmpPathfinder(GetSimContext(), SYSTEM_ENTITY); CFixedVector2D pos = cmpPosition->GetPosition2D(); return cmpPathfinder->IsGoalReachable(pos.X, pos.Y, goal, m_PassClass); } void CCmpUnitMotion::RenderPath(const WaypointPath& path, std::vector& lines, CColor color) { bool floating = false; CmpPtr cmpPosition(GetEntityHandle()); if (cmpPosition) floating = cmpPosition->CanFloat(); lines.clear(); std::vector waypointCoords; for (size_t i = 0; i < path.m_Waypoints.size(); ++i) { float x = path.m_Waypoints[i].x.ToFloat(); float z = path.m_Waypoints[i].z.ToFloat(); waypointCoords.push_back(x); waypointCoords.push_back(z); lines.push_back(SOverlayLine()); lines.back().m_Color = color; SimRender::ConstructSquareOnGround(GetSimContext(), x, z, 1.0f, 1.0f, 0.0f, lines.back(), floating); } float x = cmpPosition->GetPosition2D().X.ToFloat(); float z = cmpPosition->GetPosition2D().Y.ToFloat(); waypointCoords.push_back(x); waypointCoords.push_back(z); lines.push_back(SOverlayLine()); lines.back().m_Color = color; SimRender::ConstructLineOnGround(GetSimContext(), waypointCoords, lines.back(), floating); } void CCmpUnitMotion::RenderSubmit(SceneCollector& collector) { if (!m_DebugOverlayEnabled) return; RenderPath(m_LongPath, m_DebugOverlayLongPathLines, OVERLAY_COLOR_LONG_PATH); RenderPath(m_ShortPath, m_DebugOverlayShortPathLines, OVERLAY_COLOR_SHORT_PATH); for (size_t i = 0; i < m_DebugOverlayLongPathLines.size(); ++i) collector.Submit(&m_DebugOverlayLongPathLines[i]); for (size_t i = 0; i < m_DebugOverlayShortPathLines.size(); ++i) collector.Submit(&m_DebugOverlayShortPathLines[i]); }