Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/entityExtend.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/entityExtend.js (revision 27321) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/entityExtend.js (revision 27322) @@ -1,435 +1,440 @@ /** returns true if this unit should be considered as a siege unit */ PETRA.isSiegeUnit = function(ent) { return ent.hasClasses(["Siege", "Elephant+Melee"]); }; /** returns true if this unit should be considered as "fast". */ PETRA.isFastMoving = function(ent) { // TODO: use clever logic based on walkspeed comparisons. return ent.hasClass("FastMoving"); }; /** returns some sort of DPS * health factor. If you specify a class, it'll use the modifiers against that class too. */ PETRA.getMaxStrength = function(ent, debugLevel, DamageTypeImportance, againstClass) { let strength = 0; let attackTypes = ent.attackTypes(); let damageTypes = Object.keys(DamageTypeImportance); if (!attackTypes) return strength; for (let type of attackTypes) { if (type == "Slaughter") continue; let attackStrength = ent.attackStrengths(type); for (let str in attackStrength) { let val = parseFloat(attackStrength[str]); if (againstClass) val *= ent.getMultiplierAgainst(type, againstClass); if (DamageTypeImportance[str]) strength += DamageTypeImportance[str] * val / damageTypes.length; else if (debugLevel > 0) API3.warn("Petra: " + str + " unknown attackStrength in getMaxStrength (please add " + str + " to config.js)."); } let attackRange = ent.attackRange(type); if (attackRange) strength += attackRange.max * 0.0125; let attackTimes = ent.attackTimes(type); for (let str in attackTimes) { let val = parseFloat(attackTimes[str]); switch (str) { case "repeat": strength += val / 100000; break; case "prepare": strength -= val / 100000; break; default: API3.warn("Petra: " + str + " unknown attackTimes in getMaxStrength"); } } } let resistanceStrength = ent.resistanceStrengths(); if (resistanceStrength.Damage) for (let str in resistanceStrength.Damage) { let val = +resistanceStrength.Damage[str]; if (DamageTypeImportance[str]) strength += DamageTypeImportance[str] * val / damageTypes.length; else if (debugLevel > 0) API3.warn("Petra: " + str + " unknown resistanceStrength in getMaxStrength (please add " + str + " to config.js)."); } // ToDo: Add support for StatusEffects and Capture. return strength * ent.maxHitpoints() / 100.0; }; /** Get access and cache it (except for units as it can change) in metadata if not already done */ PETRA.getLandAccess = function(gameState, ent) { if (ent.hasClass("Unit")) { let pos = ent.position(); if (!pos) { let holder = PETRA.getHolder(gameState, ent); if (holder) return PETRA.getLandAccess(gameState, holder); API3.warn("Petra error: entity without position, but not garrisoned"); PETRA.dumpEntity(ent); return undefined; } return gameState.ai.accessibility.getAccessValue(pos); } let access = ent.getMetadata(PlayerID, "access"); if (!access) { access = gameState.ai.accessibility.getAccessValue(ent.position()); // Docks are sometimes not as expected if (access < 2 && ent.buildPlacementType() == "shore") { let halfDepth = 0; if (ent.get("Footprint/Square")) halfDepth = +ent.get("Footprint/Square/@depth") / 2; else if (ent.get("Footprint/Circle")) halfDepth = +ent.get("Footprint/Circle/@radius"); let entPos = ent.position(); let cosa = Math.cos(ent.angle()); let sina = Math.sin(ent.angle()); for (let d = 3; d < halfDepth; d += 3) { let pos = [ entPos[0] - d * sina, entPos[1] - d * cosa]; access = gameState.ai.accessibility.getAccessValue(pos); if (access > 1) break; } } ent.setMetadata(PlayerID, "access", access); } return access; }; /** Sea access always cached as it never changes */ PETRA.getSeaAccess = function(gameState, ent) { let sea = ent.getMetadata(PlayerID, "sea"); if (!sea) { sea = gameState.ai.accessibility.getAccessValue(ent.position(), true); // Docks are sometimes not as expected if (sea < 2 && ent.buildPlacementType() == "shore") { let entPos = ent.position(); let cosa = Math.cos(ent.angle()); let sina = Math.sin(ent.angle()); for (let d = 3; d < 15; d += 3) { let pos = [ entPos[0] + d * sina, entPos[1] + d * cosa]; sea = gameState.ai.accessibility.getAccessValue(pos, true); if (sea > 1) break; } } ent.setMetadata(PlayerID, "sea", sea); } return sea; }; PETRA.setSeaAccess = function(gameState, ent) { PETRA.getSeaAccess(gameState, ent); }; /** Decide if we should try to capture (returns true) or destroy (return false) */ PETRA.allowCapture = function(gameState, ent, target) { if (!target.isCapturable() || !ent.canCapture(target)) return false; if (target.isInvulnerable()) return true; // always try to recapture capture points from an allied, except if it's decaying if (gameState.isPlayerAlly(target.owner())) return !target.decaying(); let antiCapture = target.defaultRegenRate(); - if (target.isGarrisonHolder() && target.garrisoned()) - antiCapture += target.garrisonRegenRate() * target.garrisoned().length; + if (target.isGarrisonHolder()) + { + const garrisonRegenRate = target.garrisonRegenRate(); + for (const garrisonedEntity of target.garrisoned()) + antiCapture += garrisonRegenRate * (gameState.getEntityById(garrisonedEntity)?.captureStrength() || 0); + } + if (target.decaying()) antiCapture -= target.territoryDecayRate(); let capture; let capturableTargets = gameState.ai.HQ.capturableTargets; if (!capturableTargets.has(target.id())) { capture = ent.captureStrength() * PETRA.getAttackBonus(ent, target, "Capture"); capturableTargets.set(target.id(), { "strength": capture, "ents": new Set([ent.id()]) }); } else { let capturable = capturableTargets.get(target.id()); if (!capturable.ents.has(ent.id())) { capturable.strength += ent.captureStrength() * PETRA.getAttackBonus(ent, target, "Capture"); capturable.ents.add(ent.id()); } capture = capturable.strength; } capture *= 1 / (0.1 + 0.9*target.healthLevel()); let sumCapturePoints = target.capturePoints().reduce((a, b) => a + b); if (target.hasDefensiveFire() && target.isGarrisonHolder() && target.garrisoned()) return capture > antiCapture + sumCapturePoints/50; return capture > antiCapture + sumCapturePoints/80; }; PETRA.getAttackBonus = function(ent, target, type) { let attackBonus = 1; if (!ent.get("Attack/" + type) || !ent.get("Attack/" + type + "/Bonuses")) return attackBonus; let bonuses = ent.get("Attack/" + type + "/Bonuses"); for (let key in bonuses) { let bonus = bonuses[key]; if (bonus.Civ && bonus.Civ !== target.civ()) continue; if (!bonus.Classes || target.hasClasses(bonus.Classes)) attackBonus *= bonus.Multiplier; } return attackBonus; }; /** Makes the worker deposit the currently carried resources at the closest accessible dropsite */ PETRA.returnResources = function(gameState, ent) { if (!ent.resourceCarrying() || !ent.resourceCarrying().length || !ent.position()) return false; let resource = ent.resourceCarrying()[0].type; let closestDropsite; let distmin = Math.min(); let access = PETRA.getLandAccess(gameState, ent); let dropsiteCollection = gameState.playerData.hasSharedDropsites ? gameState.getAnyDropsites(resource) : gameState.getOwnDropsites(resource); for (let dropsite of dropsiteCollection.values()) { if (!dropsite.position()) continue; let owner = dropsite.owner(); // owner !== PlayerID can only happen when hasSharedDropsites === true, so no need to test it again if (owner !== PlayerID && (!dropsite.isSharedDropsite() || !gameState.isPlayerMutualAlly(owner))) continue; if (PETRA.getLandAccess(gameState, dropsite) != access) continue; let dist = API3.SquareVectorDistance(ent.position(), dropsite.position()); if (dist > distmin) continue; distmin = dist; closestDropsite = dropsite; } if (!closestDropsite) return false; ent.returnResources(closestDropsite); return true; }; /** is supply full taking into account gatherers affected during this turn */ PETRA.IsSupplyFull = function(gameState, ent) { return ent.isFull() === true || ent.resourceSupplyNumGatherers() + gameState.ai.HQ.basesManager.GetTCGatherer(ent.id()) >= ent.maxGatherers(); }; /** * Get the best base (in terms of distance and accessIndex) for an entity. * It should be on the same accessIndex for structures. * If nothing found, return the noBase for units and undefined for structures. * If exclude is given, we exclude the base with ID = exclude. */ PETRA.getBestBase = function(gameState, ent, onlyConstructedBase = false, exclude = false) { let pos = ent.position(); let accessIndex; if (!pos) { let holder = PETRA.getHolder(gameState, ent); if (!holder || !holder.position()) { API3.warn("Petra error: entity without position, but not garrisoned"); PETRA.dumpEntity(ent); return gameState.ai.HQ.basesManager.baselessBase(); } pos = holder.position(); accessIndex = PETRA.getLandAccess(gameState, holder); } else accessIndex = PETRA.getLandAccess(gameState, ent); let distmin = Math.min(); let dist; let bestbase; for (const base of gameState.ai.HQ.baseManagers()) { if (base.ID == gameState.ai.HQ.basesManager.baselessBase().ID || exclude && base.ID == exclude) continue; if (onlyConstructedBase && (!base.anchor || base.anchor.foundationProgress() !== undefined)) continue; if (ent.hasClass("Structure") && base.accessIndex != accessIndex) continue; if (base.anchor && base.anchor.position()) dist = API3.SquareVectorDistance(base.anchor.position(), pos); else { let found = false; for (let structure of base.buildings.values()) { if (!structure.position()) continue; dist = API3.SquareVectorDistance(structure.position(), pos); found = true; break; } if (!found) continue; } if (base.accessIndex != accessIndex) dist += 50000000; if (!base.anchor) dist += 50000000; if (dist > distmin) continue; distmin = dist; bestbase = base; } if (!bestbase && !ent.hasClass("Structure")) bestbase = gameState.ai.HQ.basesManager.baselessBase(); return bestbase; }; PETRA.getHolder = function(gameState, ent) { for (let holder of gameState.getEntities().values()) { if (holder.isGarrisonHolder() && holder.garrisoned().indexOf(ent.id()) !== -1) return holder; } return undefined; }; /** return the template of the built foundation if a foundation, otherwise return the entity itself */ PETRA.getBuiltEntity = function(gameState, ent) { if (ent.foundationProgress() !== undefined) return gameState.getBuiltTemplate(ent.templateName()); return ent; }; /** * return true if it is not worth finishing this building (it would surely decay) * TODO implement the other conditions */ PETRA.isNotWorthBuilding = function(gameState, ent) { if (gameState.ai.HQ.territoryMap.getOwner(ent.position()) !== PlayerID) { let buildTerritories = ent.buildTerritories(); if (buildTerritories && (!buildTerritories.length || buildTerritories.length === 1 && buildTerritories[0] === "own")) return true; } return false; }; /** * Check if the straight line between the two positions crosses an enemy territory */ PETRA.isLineInsideEnemyTerritory = function(gameState, pos1, pos2, step=70) { let n = Math.floor(Math.sqrt(API3.SquareVectorDistance(pos1, pos2))/step) + 1; let stepx = (pos2[0] - pos1[0]) / n; let stepy = (pos2[1] - pos1[1]) / n; for (let i = 1; i < n; ++i) { let pos = [pos1[0]+i*stepx, pos1[1]+i*stepy]; let owner = gameState.ai.HQ.territoryMap.getOwner(pos); if (owner && gameState.isPlayerEnemy(owner)) return true; } return false; }; PETRA.gatherTreasure = function(gameState, ent, water = false) { if (!gameState.ai.HQ.treasures.hasEntities()) return false; if (!ent || !ent.position()) return false; if (!ent.isTreasureCollector()) return false; let treasureFound; let distmin = Math.min(); let access = water ? PETRA.getSeaAccess(gameState, ent) : PETRA.getLandAccess(gameState, ent); for (let treasure of gameState.ai.HQ.treasures.values()) { // let some time for the previous gatherer to reach the treasure before trying again let lastGathered = treasure.getMetadata(PlayerID, "lastGathered"); if (lastGathered && gameState.ai.elapsedTime - lastGathered < 20) continue; if (!water && access != PETRA.getLandAccess(gameState, treasure)) continue; if (water && access != PETRA.getSeaAccess(gameState, treasure)) continue; let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(treasure.position()); if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) continue; let dist = API3.SquareVectorDistance(ent.position(), treasure.position()); if (dist > 120000 || territoryOwner != PlayerID && dist > 14000) // AI has no LOS, so restrict it a bit continue; if (dist > distmin) continue; distmin = dist; treasureFound = treasure; } if (!treasureFound) return false; treasureFound.setMetadata(PlayerID, "lastGathered", gameState.ai.elapsedTime); ent.collectTreasure(treasureFound); ent.setMetadata(PlayerID, "treasure", treasureFound.id()); return true; }; PETRA.dumpEntity = function(ent) { if (!ent) return; API3.warn(" >>> id " + ent.id() + " name " + ent.genericName() + " pos " + ent.position() + " state " + ent.unitAIState()); API3.warn(" base " + ent.getMetadata(PlayerID, "base") + " >>> role " + ent.getMetadata(PlayerID, "role") + " subrole " + ent.getMetadata(PlayerID, "subrole")); API3.warn("owner " + ent.owner() + " health " + ent.hitpoints() + " healthMax " + ent.maxHitpoints() + " foundationProgress " + ent.foundationProgress()); API3.warn(" garrisoning " + ent.getMetadata(PlayerID, "garrisoning") + " garrisonHolder " + ent.getMetadata(PlayerID, "garrisonHolder") + " plan " + ent.getMetadata(PlayerID, "plan") + " transport " + ent.getMetadata(PlayerID, "transport")); API3.warn(" stance " + ent.getStance() + " transporter " + ent.getMetadata(PlayerID, "transporter") + " gather-type " + ent.getMetadata(PlayerID, "gather-type") + " target-foundation " + ent.getMetadata(PlayerID, "target-foundation") + " PartOfArmy " + ent.getMetadata(PlayerID, "PartOfArmy")); }; Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/garrisonManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/garrisonManager.js (revision 27321) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/garrisonManager.js (revision 27322) @@ -1,382 +1,382 @@ /** * Manage the garrisonHolders * When a unit is ordered to garrison, it must be done through this.garrison() function so that * an object in this.holders is created. This object contains an array with the entities * in the process of being garrisoned. To have all garrisoned units, we must add those in holder.garrisoned(). * Futhermore garrison units have a metadata garrisonType describing its reason (protection, transport, ...) */ PETRA.GarrisonManager = function(Config) { this.Config = Config; this.holders = new Map(); this.decayingStructures = new Map(); }; PETRA.GarrisonManager.TYPE_FORCE = "force"; PETRA.GarrisonManager.TYPE_TRADE = "trade"; PETRA.GarrisonManager.TYPE_PROTECTION = "protection"; PETRA.GarrisonManager.TYPE_DECAY = "decay"; PETRA.GarrisonManager.TYPE_EMERGENCY = "emergency"; PETRA.GarrisonManager.prototype.update = function(gameState, events) { // First check for possible upgrade of a structure for (let evt of events.EntityRenamed) { for (let id of this.holders.keys()) { if (id != evt.entity) continue; let data = this.holders.get(id); let newHolder = gameState.getEntityById(evt.newentity); if (newHolder && newHolder.isGarrisonHolder()) { this.holders.delete(id); this.holders.set(evt.newentity, data); } else { for (let entId of data.list) { let ent = gameState.getEntityById(entId); if (!ent || ent.getMetadata(PlayerID, "garrisonHolder") != id) continue; this.leaveGarrison(ent); ent.stopMoving(); } this.holders.delete(id); } } for (let id of this.decayingStructures.keys()) { if (id !== evt.entity) continue; this.decayingStructures.delete(id); if (this.decayingStructures.has(evt.newentity)) continue; let ent = gameState.getEntityById(evt.newentity); if (!ent || !ent.territoryDecayRate() || !ent.garrisonRegenRate()) continue; let gmin = Math.ceil((ent.territoryDecayRate() - ent.defaultRegenRate()) / ent.garrisonRegenRate()); this.decayingStructures.set(evt.newentity, gmin); } } for (let [id, data] of this.holders.entries()) { let list = data.list; let holder = gameState.getEntityById(id); if (!holder || !gameState.isPlayerAlly(holder.owner())) { // this holder was certainly destroyed or captured. Let's remove it for (let entId of list) { let ent = gameState.getEntityById(entId); if (!ent || ent.getMetadata(PlayerID, "garrisonHolder") != id) continue; this.leaveGarrison(ent); ent.stopMoving(); } this.holders.delete(id); continue; } // Update the list of garrisoned units for (let j = 0; j < list.length; ++j) { for (let evt of events.EntityRenamed) if (evt.entity === list[j]) list[j] = evt.newentity; let ent = gameState.getEntityById(list[j]); if (!ent) // unit must have been killed while garrisoning list.splice(j--, 1); else if (holder.garrisoned().indexOf(list[j]) !== -1) // unit is garrisoned { this.leaveGarrison(ent); list.splice(j--, 1); } else { if (ent.unitAIOrderData().some(order => order.target && order.target == id)) continue; if (ent.getMetadata(PlayerID, "garrisonHolder") == id) { // The garrison order must have failed this.leaveGarrison(ent); list.splice(j--, 1); } else { if (gameState.ai.Config.debug > 0) { API3.warn("Petra garrison error: unit " + ent.id() + " (" + ent.genericName() + ") is expected to garrison in " + id + " (" + holder.genericName() + "), but has no such garrison order " + uneval(ent.unitAIOrderData())); PETRA.dumpEntity(ent); } list.splice(j--, 1); } } } if (!holder.position()) // could happen with siege unit inside a ship continue; if (gameState.ai.elapsedTime - holder.getMetadata(PlayerID, "holderTimeUpdate") > 3) { let range = holder.attackRange("Ranged") ? holder.attackRange("Ranged").max : 80; let around = { "defenseStructure": false, "meleeSiege": false, "rangeSiege": false, "unit": false }; for (let ent of gameState.getEnemyEntities().values()) { if (ent.hasClass("Structure")) { if (!ent.attackRange("Ranged")) continue; } else if (ent.hasClass("Unit")) { if (ent.owner() == 0 && (!ent.unitAIState() || ent.unitAIState().split(".")[1] != "COMBAT")) continue; } else continue; if (!ent.position()) continue; let dist = API3.SquareVectorDistance(ent.position(), holder.position()); if (dist > range*range) continue; if (ent.hasClass("Structure")) around.defenseStructure = true; else if (PETRA.isSiegeUnit(ent)) { if (ent.attackTypes().indexOf("Melee") !== -1) around.meleeSiege = true; else around.rangeSiege = true; } else { around.unit = true; break; } } // Keep defenseManager.garrisonUnitsInside in sync to avoid garrisoning-ungarrisoning some units data.allowMelee = around.defenseStructure || around.unit; for (let entId of holder.garrisoned()) { let ent = gameState.getEntityById(entId); if (ent.owner() === PlayerID && !this.keepGarrisoned(ent, holder, around)) holder.unload(entId); } for (let j = 0; j < list.length; ++j) { let ent = gameState.getEntityById(list[j]); if (this.keepGarrisoned(ent, holder, around)) continue; if (ent.getMetadata(PlayerID, "garrisonHolder") == id) { this.leaveGarrison(ent); ent.stopMoving(); } list.splice(j--, 1); } if (this.numberOfGarrisonedSlots(holder) === 0) this.holders.delete(id); else holder.setMetadata(PlayerID, "holderTimeUpdate", gameState.ai.elapsedTime); } } // Warning new garrison orders (as in the following lines) should be done after having updated the holders // (or TODO we should add a test that the garrison order is from a previous turn when updating) for (let [id, gmin] of this.decayingStructures.entries()) { let ent = gameState.getEntityById(id); if (!ent || ent.owner() !== PlayerID) this.decayingStructures.delete(id); else if (this.numberOfGarrisonedSlots(ent) < gmin) gameState.ai.HQ.defenseManager.garrisonUnitsInside(gameState, ent, { "min": gmin, "type": PETRA.GarrisonManager.TYPE_DECAY }); } }; /** TODO should add the units garrisoned inside garrisoned units */ PETRA.GarrisonManager.prototype.numberOfGarrisonedUnits = function(holder) { if (!this.holders.has(holder.id())) return holder.garrisoned().length; return holder.garrisoned().length + this.holders.get(holder.id()).list.length; }; /** TODO should add the units garrisoned inside garrisoned units */ PETRA.GarrisonManager.prototype.numberOfGarrisonedSlots = function(holder) { if (!this.holders.has(holder.id())) return holder.garrisonedSlots(); return holder.garrisonedSlots() + this.holders.get(holder.id()).list.length; }; PETRA.GarrisonManager.prototype.allowMelee = function(holder) { if (!this.holders.has(holder.id())) return undefined; return this.holders.get(holder.id()).allowMelee; }; /** This is just a pre-garrison state, while the entity walk to the garrison holder */ PETRA.GarrisonManager.prototype.garrison = function(gameState, ent, holder, type) { if (this.numberOfGarrisonedSlots(holder) >= holder.garrisonMax() || !ent.canGarrison()) return; this.registerHolder(gameState, holder); this.holders.get(holder.id()).list.push(ent.id()); if (gameState.ai.Config.debug > 2) { warn("garrison unit " + ent.genericName() + " in " + holder.genericName() + " with type " + type); warn(" we try to garrison a unit with plan " + ent.getMetadata(PlayerID, "plan") + " and role " + ent.getMetadata(PlayerID, "role") + " and subrole " + ent.getMetadata(PlayerID, "subrole") + " and transport " + ent.getMetadata(PlayerID, "transport")); } if (ent.getMetadata(PlayerID, "plan") !== undefined) ent.setMetadata(PlayerID, "plan", -2); else ent.setMetadata(PlayerID, "plan", -3); ent.setMetadata(PlayerID, "subrole", PETRA.Worker.SUBROLE_GARRISONING); ent.setMetadata(PlayerID, "garrisonHolder", holder.id()); ent.setMetadata(PlayerID, "garrisonType", type); ent.garrison(holder); }; /** This is the end of the pre-garrison state, either because the entity is really garrisoned or because it has changed its order (i.e. because the garrisonHolder was destroyed) This function is for internal use inside garrisonManager. From outside, you should also update the holder and then using cancelGarrison should be the preferred solution */ PETRA.GarrisonManager.prototype.leaveGarrison = function(ent) { ent.setMetadata(PlayerID, "subrole", undefined); if (ent.getMetadata(PlayerID, "plan") === -2) ent.setMetadata(PlayerID, "plan", -1); else ent.setMetadata(PlayerID, "plan", undefined); ent.setMetadata(PlayerID, "garrisonHolder", undefined); }; /** Cancel a pre-garrison state */ PETRA.GarrisonManager.prototype.cancelGarrison = function(ent) { ent.stopMoving(); this.leaveGarrison(ent); let holderId = ent.getMetadata(PlayerID, "garrisonHolder"); if (!holderId || !this.holders.has(holderId)) return; let list = this.holders.get(holderId).list; let index = list.indexOf(ent.id()); if (index !== -1) list.splice(index, 1); }; PETRA.GarrisonManager.prototype.keepGarrisoned = function(ent, holder, around) { switch (ent.getMetadata(PlayerID, "garrisonType")) { case PETRA.GarrisonManager.TYPE_FORCE: // force the ungarrisoning return false; case PETRA.GarrisonManager.TYPE_TRADE: // trader garrisoned in ship return true; case PETRA.GarrisonManager.TYPE_PROTECTION: // hurt unit for healing or infantry for defense if (holder.buffHeal() && ent.isHealable() && ent.healthLevel() < this.Config.garrisonHealthLevel.high) return true; let capture = ent.capturePoints(); if (capture && capture[PlayerID] / capture.reduce((a, b) => a + b) < 0.8) return true; if (ent.hasClasses(holder.getGarrisonArrowClasses())) { if (around.unit || around.defenseStructure) return true; if (around.meleeSiege || around.rangeSiege) return ent.attackTypes().indexOf("Melee") === -1 || ent.healthLevel() < this.Config.garrisonHealthLevel.low; return false; } if (ent.attackTypes() && ent.attackTypes().indexOf("Melee") !== -1) return false; if (around.unit) return ent.hasClass("Support") || PETRA.isSiegeUnit(ent); // only ranged siege here and below as melee siege already released above if (PETRA.isSiegeUnit(ent)) return around.meleeSiege; return holder.buffHeal() && ent.needsHeal(); case PETRA.GarrisonManager.TYPE_DECAY: - return this.decayingStructures.has(holder.id()); + return ent.captureStrength() && this.decayingStructures.has(holder.id()); case PETRA.GarrisonManager.TYPE_EMERGENCY: // f.e. hero in regicide mode if (holder.buffHeal() && ent.isHealable() && ent.healthLevel() < this.Config.garrisonHealthLevel.high) return true; if (around.unit || around.defenseStructure || around.meleeSiege || around.rangeSiege && ent.healthLevel() < this.Config.garrisonHealthLevel.high) return true; return holder.buffHeal() && ent.needsHeal(); default: if (ent.getMetadata(PlayerID, "onBoard") === "onBoard") // transport is not (yet ?) managed by garrisonManager return true; API3.warn("unknown type in garrisonManager " + ent.getMetadata(PlayerID, "garrisonType") + " for " + ent.genericName() + " id " + ent.id() + " inside " + holder.genericName() + " id " + holder.id()); ent.setMetadata(PlayerID, "garrisonType", PETRA.GarrisonManager.TYPE_PROTECTION); return true; } }; /** Add this holder in the list managed by the garrisonManager */ PETRA.GarrisonManager.prototype.registerHolder = function(gameState, holder) { if (this.holders.has(holder.id())) // already registered return; this.holders.set(holder.id(), { "list": [], "allowMelee": true }); holder.setMetadata(PlayerID, "holderTimeUpdate", gameState.ai.elapsedTime); }; /** * Garrison units in decaying structures to stop their decay * do it only for structures useful for defense, except if we are expanding (justCaptured=true) * in which case we also do it for structures useful for unit trainings (TODO only Barracks are done) */ PETRA.GarrisonManager.prototype.addDecayingStructure = function(gameState, entId, justCaptured) { if (this.decayingStructures.has(entId)) return true; let ent = gameState.getEntityById(entId); if (!ent || !(ent.hasClass("Barracks") && justCaptured) && !ent.hasDefensiveFire()) return false; if (!ent.territoryDecayRate() || !ent.garrisonRegenRate()) return false; let gmin = Math.ceil((ent.territoryDecayRate() - ent.defaultRegenRate()) / ent.garrisonRegenRate()); this.decayingStructures.set(entId, gmin); return true; }; PETRA.GarrisonManager.prototype.removeDecayingStructure = function(entId) { if (!this.decayingStructures.has(entId)) return; this.decayingStructures.delete(entId); }; PETRA.GarrisonManager.prototype.Serialize = function() { return { "holders": this.holders, "decayingStructures": this.decayingStructures }; }; PETRA.GarrisonManager.prototype.Deserialize = function(data) { for (let key in data) this[key] = data[key]; };