0ad/binaries/data/mods/public/simulation/ai/petra/entityExtend.js
Freagarach 4e664dd712 [PetraAI] - Manage bases in a separate BasesManager.
The HQ should only care about high-level stuff, hence something like
managing/looping individual bases is now done in a `BasesManager`,
similar to the `AttackManager`.

Differential revision: https://code.wildfiregames.com/D4192
Comments by: @Angen
Fixes: #6185

This was SVN commit r25876.
2021-08-29 06:47:05 +00:00

435 lines
14 KiB
JavaScript

/** returns true if this unit should be considered as a siege unit */
PETRA.isSiegeUnit = function(ent)
{
return ent.hasClasses(["Siege", "Elephant+Melee"]);
};
/** returns true if this unit should be considered as "fast". */
PETRA.isFastMoving = function(ent)
{
// TODO: use clever logic based on walkspeed comparisons.
return ent.hasClass("FastMoving");
};
/** returns some sort of DPS * health factor. If you specify a class, it'll use the modifiers against that class too. */
PETRA.getMaxStrength = function(ent, debugLevel, DamageTypeImportance, againstClass)
{
let strength = 0;
let attackTypes = ent.attackTypes();
let damageTypes = Object.keys(DamageTypeImportance);
if (!attackTypes)
return strength;
for (let type of attackTypes)
{
if (type == "Slaughter")
continue;
let attackStrength = ent.attackStrengths(type);
for (let str in attackStrength)
{
let val = parseFloat(attackStrength[str]);
if (againstClass)
val *= ent.getMultiplierAgainst(type, againstClass);
if (DamageTypeImportance[str])
strength += DamageTypeImportance[str] * val / damageTypes.length;
else if (debugLevel > 0)
API3.warn("Petra: " + str + " unknown attackStrength in getMaxStrength (please add " + str + " to config.js).");
}
let attackRange = ent.attackRange(type);
if (attackRange)
strength += attackRange.max * 0.0125;
let attackTimes = ent.attackTimes(type);
for (let str in attackTimes)
{
let val = parseFloat(attackTimes[str]);
switch (str)
{
case "repeat":
strength += val / 100000;
break;
case "prepare":
strength -= val / 100000;
break;
default:
API3.warn("Petra: " + str + " unknown attackTimes in getMaxStrength");
}
}
}
let resistanceStrength = ent.resistanceStrengths();
if (resistanceStrength.Damage)
for (let str in resistanceStrength.Damage)
{
let val = +resistanceStrength.Damage[str];
if (DamageTypeImportance[str])
strength += DamageTypeImportance[str] * val / damageTypes.length;
else if (debugLevel > 0)
API3.warn("Petra: " + str + " unknown resistanceStrength in getMaxStrength (please add " + str + " to config.js).");
}
// ToDo: Add support for StatusEffects and Capture.
return strength * ent.maxHitpoints() / 100.0;
};
/** Get access and cache it (except for units as it can change) in metadata if not already done */
PETRA.getLandAccess = function(gameState, ent)
{
if (ent.hasClass("Unit"))
{
let pos = ent.position();
if (!pos)
{
let holder = PETRA.getHolder(gameState, ent);
if (holder)
return PETRA.getLandAccess(gameState, holder);
API3.warn("Petra error: entity without position, but not garrisoned");
PETRA.dumpEntity(ent);
return undefined;
}
return gameState.ai.accessibility.getAccessValue(pos);
}
let access = ent.getMetadata(PlayerID, "access");
if (!access)
{
access = gameState.ai.accessibility.getAccessValue(ent.position());
// Docks are sometimes not as expected
if (access < 2 && ent.buildPlacementType() == "shore")
{
let halfDepth = 0;
if (ent.get("Footprint/Square"))
halfDepth = +ent.get("Footprint/Square/@depth") / 2;
else if (ent.get("Footprint/Circle"))
halfDepth = +ent.get("Footprint/Circle/@radius");
let entPos = ent.position();
let cosa = Math.cos(ent.angle());
let sina = Math.sin(ent.angle());
for (let d = 3; d < halfDepth; d += 3)
{
let pos = [ entPos[0] - d * sina,
entPos[1] - d * cosa];
access = gameState.ai.accessibility.getAccessValue(pos);
if (access > 1)
break;
}
}
ent.setMetadata(PlayerID, "access", access);
}
return access;
};
/** Sea access always cached as it never changes */
PETRA.getSeaAccess = function(gameState, ent)
{
let sea = ent.getMetadata(PlayerID, "sea");
if (!sea)
{
sea = gameState.ai.accessibility.getAccessValue(ent.position(), true);
// Docks are sometimes not as expected
if (sea < 2 && ent.buildPlacementType() == "shore")
{
let entPos = ent.position();
let cosa = Math.cos(ent.angle());
let sina = Math.sin(ent.angle());
for (let d = 3; d < 15; d += 3)
{
let pos = [ entPos[0] + d * sina,
entPos[1] + d * cosa];
sea = gameState.ai.accessibility.getAccessValue(pos, true);
if (sea > 1)
break;
}
}
ent.setMetadata(PlayerID, "sea", sea);
}
return sea;
};
PETRA.setSeaAccess = function(gameState, ent)
{
PETRA.getSeaAccess(gameState, ent);
};
/** Decide if we should try to capture (returns true) or destroy (return false) */
PETRA.allowCapture = function(gameState, ent, target)
{
if (!target.isCapturable() || !ent.canCapture(target))
return false;
if (target.isInvulnerable())
return true;
// always try to recapture capture points from an allied, except if it's decaying
if (gameState.isPlayerAlly(target.owner()))
return !target.decaying();
let antiCapture = target.defaultRegenRate();
if (target.isGarrisonHolder() && target.garrisoned())
antiCapture += target.garrisonRegenRate() * target.garrisoned().length;
if (target.decaying())
antiCapture -= target.territoryDecayRate();
let capture;
let capturableTargets = gameState.ai.HQ.capturableTargets;
if (!capturableTargets.has(target.id()))
{
capture = ent.captureStrength() * PETRA.getAttackBonus(ent, target, "Capture");
capturableTargets.set(target.id(), { "strength": capture, "ents": new Set([ent.id()]) });
}
else
{
let capturable = capturableTargets.get(target.id());
if (!capturable.ents.has(ent.id()))
{
capturable.strength += ent.captureStrength() * PETRA.getAttackBonus(ent, target, "Capture");
capturable.ents.add(ent.id());
}
capture = capturable.strength;
}
capture *= 1 / (0.1 + 0.9*target.healthLevel());
let sumCapturePoints = target.capturePoints().reduce((a, b) => a + b);
if (target.hasDefensiveFire() && target.isGarrisonHolder() && target.garrisoned())
return capture > antiCapture + sumCapturePoints/50;
return capture > antiCapture + sumCapturePoints/80;
};
PETRA.getAttackBonus = function(ent, target, type)
{
let attackBonus = 1;
if (!ent.get("Attack/" + type) || !ent.get("Attack/" + type + "/Bonuses"))
return attackBonus;
let bonuses = ent.get("Attack/" + type + "/Bonuses");
for (let key in bonuses)
{
let bonus = bonuses[key];
if (bonus.Civ && bonus.Civ !== target.civ())
continue;
if (!bonus.Classes || target.hasClasses(bonus.Classes))
attackBonus *= bonus.Multiplier;
}
return attackBonus;
};
/** Makes the worker deposit the currently carried resources at the closest accessible dropsite */
PETRA.returnResources = function(gameState, ent)
{
if (!ent.resourceCarrying() || !ent.resourceCarrying().length || !ent.position())
return false;
let resource = ent.resourceCarrying()[0].type;
let closestDropsite;
let distmin = Math.min();
let access = PETRA.getLandAccess(gameState, ent);
let dropsiteCollection = gameState.playerData.hasSharedDropsites ?
gameState.getAnyDropsites(resource) : gameState.getOwnDropsites(resource);
for (let dropsite of dropsiteCollection.values())
{
if (!dropsite.position())
continue;
let owner = dropsite.owner();
// owner !== PlayerID can only happen when hasSharedDropsites === true, so no need to test it again
if (owner !== PlayerID && (!dropsite.isSharedDropsite() || !gameState.isPlayerMutualAlly(owner)))
continue;
if (PETRA.getLandAccess(gameState, dropsite) != access)
continue;
let dist = API3.SquareVectorDistance(ent.position(), dropsite.position());
if (dist > distmin)
continue;
distmin = dist;
closestDropsite = dropsite;
}
if (!closestDropsite)
return false;
ent.returnResources(closestDropsite);
return true;
};
/** is supply full taking into account gatherers affected during this turn */
PETRA.IsSupplyFull = function(gameState, ent)
{
return ent.isFull() === true ||
ent.resourceSupplyNumGatherers() + gameState.ai.HQ.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"));
};