mirror of
https://gitea.wildfiregames.com/0ad/0ad.git
synced 2026-07-25 09:53:47 +00:00
fb1c0d2a82
Some units would retain an undue HeightOffset after renaming while being on a TurretPoint. Fixes #8651 Fixes #2004
477 lines
14 KiB
JavaScript
477 lines
14 KiB
JavaScript
/**
|
|
* This class holds the functions regarding entities being visible on
|
|
* another entity, but tied to their parents location.
|
|
*/
|
|
class TurretHolder
|
|
{
|
|
Init()
|
|
{
|
|
this.turretPoints = [];
|
|
|
|
const points = this.template.TurretPoints;
|
|
for (const point in points)
|
|
this.turretPoints.push({
|
|
"name": point,
|
|
"offset": {
|
|
"x": +points[point].X,
|
|
"y": +points[point].Y,
|
|
"z": +points[point].Z
|
|
},
|
|
"allowedClasses": points[point].AllowedClasses?._string,
|
|
"angle": points[point].Angle ? +points[point].Angle * Math.PI / 180 : null,
|
|
"entity": null,
|
|
"template": points[point].Template,
|
|
"ejectable": "Ejectable" in points[point] ? points[point].Ejectable == "true" : true
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Add a subunit as specified in the template.
|
|
* This function creates an entity and places it on the turret point.
|
|
*
|
|
* @param {Object} turretPoint - A turret point to (re)create the predefined subunit for.
|
|
*
|
|
* @return {boolean} - Whether the turret creation has succeeded.
|
|
*/
|
|
CreateSubunit(turretPointName)
|
|
{
|
|
const turretPoint = this.TurretPointByName(turretPointName);
|
|
if (!turretPoint || turretPoint.entity ||
|
|
this.initTurrets?.has(turretPointName) ||
|
|
this.reservedTurrets?.has(turretPointName))
|
|
return false;
|
|
|
|
const cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
|
|
|
|
const upgradedTemplate = GetUpgradedTemplate(cmpOwnership.GetOwner(), turretPoint.template);
|
|
const ent = Engine.AddEntity(upgradedTemplate);
|
|
|
|
const cmpEntOwnership = Engine.QueryInterface(ent, IID_Ownership);
|
|
cmpEntOwnership?.SetOwner(cmpOwnership.GetOwner());
|
|
|
|
const cmpTurretable = Engine.QueryInterface(ent, IID_Turretable);
|
|
return cmpTurretable?.OccupyTurret(this.entity, turretPoint.name, turretPoint.ejectable) || Engine.DestroyEntity(ent);
|
|
}
|
|
|
|
/**
|
|
* @param {string} name - The name of a turret point to reserve, e.g. for promotion.
|
|
*/
|
|
SetReservedTurretPoint(name)
|
|
{
|
|
if (!this.reservedTurrets)
|
|
this.reservedTurrets = new Set();
|
|
this.reservedTurrets.add(name);
|
|
}
|
|
|
|
/**
|
|
* @return {Object[]} - An array of the turret points this entity has.
|
|
*/
|
|
GetTurretPoints()
|
|
{
|
|
return this.turretPoints;
|
|
}
|
|
|
|
/**
|
|
* @param {number} entity - The entity to check for.
|
|
* @param {Object} turretPoint - The turret point to use.
|
|
* @param {boolean} [forReplacement=false] - Whether this check is for replacement
|
|
* (if true, occupied turret points are allowed).
|
|
*
|
|
* @return {boolean} - Whether the entity is allowed to occupy the specified turret point.
|
|
*/
|
|
AllowedToOccupyTurretPoint(entity, turretPoint, forReplacement = false)
|
|
{
|
|
if (!turretPoint || turretPoint.entity && !forReplacement)
|
|
return false;
|
|
|
|
if (!IsOwnedByMutualAllyOfEntity(entity, this.entity))
|
|
return false;
|
|
|
|
if (!turretPoint.allowedClasses)
|
|
return true;
|
|
|
|
const cmpIdentity = Engine.QueryInterface(entity, IID_Identity);
|
|
return cmpIdentity && MatchesClassList(cmpIdentity.GetClassesList(), turretPoint.allowedClasses);
|
|
}
|
|
|
|
/**
|
|
* @param {number} entity - The entity to check for.
|
|
* @return {boolean} - Whether the entity is allowed to occupy any turret point.
|
|
*/
|
|
CanOccupy(entity)
|
|
{
|
|
return !!this.turretPoints.find(turretPoint => this.AllowedToOccupyTurretPoint(entity, turretPoint));
|
|
}
|
|
|
|
/**
|
|
* Occupy a turret point with the given entity.
|
|
* @param {number} entity - The entity to use.
|
|
* @param {Object} requestedTurretPoint - Optionally the specific turret point to occupy.
|
|
*
|
|
* @return {boolean} - Whether the occupation was successful.
|
|
*/
|
|
OccupyTurretPoint(entity, requestedTurretPoint)
|
|
{
|
|
const cmpPositionOccupant = Engine.QueryInterface(entity, IID_Position);
|
|
if (!cmpPositionOccupant)
|
|
return false;
|
|
|
|
const cmpPositionSelf = Engine.QueryInterface(this.entity, IID_Position);
|
|
if (!cmpPositionSelf)
|
|
return false;
|
|
|
|
if (this.OccupiesTurretPoint(entity))
|
|
return false;
|
|
|
|
let turretPoint;
|
|
if (requestedTurretPoint)
|
|
{
|
|
if (this.AllowedToOccupyTurretPoint(entity, requestedTurretPoint))
|
|
turretPoint = requestedTurretPoint;
|
|
}
|
|
else
|
|
turretPoint = this.turretPoints.find(turret => !turret.entity && this.AllowedToOccupyTurretPoint(entity, turret));
|
|
|
|
if (!turretPoint)
|
|
return false;
|
|
|
|
turretPoint.entity = entity;
|
|
|
|
// Angle of turrets:
|
|
// Renamed entities (turretPoint != undefined) should keep their angle.
|
|
// Otherwise if an angle is given in the turretPoint, use it.
|
|
// If no such angle given (usually walls for which outside/inside not well defined), we keep
|
|
// the current angle as it was used for garrisoning and thus quite often was from inside to
|
|
// outside, except when garrisoning from outWorld where we take as default PI.
|
|
if (!turretPoint && turretPoint.angle != null)
|
|
cmpPositionOccupant.SetYRotation(cmpPositionSelf.GetRotation().y + turretPoint.angle);
|
|
else if (!turretPoint && !cmpPosition.IsInWorld())
|
|
cmpPositionOccupant.SetYRotation(cmpPositionSelf.GetRotation().y + Math.PI);
|
|
|
|
cmpPositionOccupant.SetTurretParent(this.entity, turretPoint.offset);
|
|
|
|
Engine.PostMessage(this.entity, MT_TurretsChanged, {
|
|
"added": [entity],
|
|
"removed": []
|
|
});
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param {number} entity - The entityID of the entity.
|
|
* @param {String} turretName - The name of the turret point to occupy.
|
|
* @return {boolean} - Whether the occupation has succeeded.
|
|
*/
|
|
OccupyNamedTurretPoint(entity, turretName)
|
|
{
|
|
return this.OccupyTurretPoint(entity, this.TurretPointByName(turretName));
|
|
}
|
|
|
|
/**
|
|
* @param {string} turretPointName - The name of the requested turret point.
|
|
* @return {Object} - The requested turret point.
|
|
*/
|
|
TurretPointByName(turretPointName)
|
|
{
|
|
return this.turretPoints.find(turret => turret.name == turretPointName);
|
|
}
|
|
|
|
/**
|
|
* Remove the entity from a turret.
|
|
* @param {number} entity - The specific entity to eject.
|
|
* @param {boolean} forced - Whether ejection is forced (e.g. due to death or renaming).
|
|
* @param {Object} turret - Optionally the turret to abandon.
|
|
*
|
|
* @return {boolean} - Whether the entity succesfully left us.
|
|
*/
|
|
LeaveTurretPoint(entity, forced, requestedTurretPoint)
|
|
{
|
|
let turretPoint;
|
|
if (requestedTurretPoint)
|
|
{
|
|
if (requestedTurretPoint.entity == entity)
|
|
turretPoint = requestedTurretPoint;
|
|
}
|
|
else
|
|
turretPoint = this.GetOccupiedTurretPoint(entity);
|
|
|
|
if (!turretPoint || (!turretPoint.ejectable && !forced))
|
|
return false;
|
|
|
|
turretPoint.entity = null;
|
|
|
|
Engine.PostMessage(this.entity, MT_TurretsChanged, {
|
|
"added": [],
|
|
"removed": [entity]
|
|
});
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param {number} entity - The entity's id.
|
|
* @param {Object} turret - Optionally the turret to check.
|
|
*
|
|
* @return {boolean} - Whether the entity is positioned on a turret of this entity.
|
|
*/
|
|
OccupiesTurretPoint(entity, requestedTurretPoint)
|
|
{
|
|
return requestedTurretPoint ? requestedTurretPoint.entity == entity :
|
|
!!this.GetOccupiedTurretPoint(entity);
|
|
}
|
|
|
|
/**
|
|
* @param {number} entity - The entity's id.
|
|
* @return {Object} - The turret this entity is positioned on, if applicable.
|
|
*/
|
|
GetOccupiedTurretPoint(entity)
|
|
{
|
|
return this.turretPoints.find(turretPoint => turretPoint.entity == entity);
|
|
}
|
|
|
|
/**
|
|
* @param {number} entity - The entity's id.
|
|
* @return {Object} - The turret this entity is positioned on, if applicable.
|
|
*/
|
|
GetOccupiedTurretPointName(entity)
|
|
{
|
|
const turret = this.GetOccupiedTurretPoint(entity);
|
|
return turret ? turret.name : "";
|
|
}
|
|
|
|
/**
|
|
* @return {number[]} - The turretted entityIDs.
|
|
*/
|
|
GetEntities()
|
|
{
|
|
const entities = [];
|
|
for (const turretPoint of this.turretPoints)
|
|
if (turretPoint.entity)
|
|
entities.push(turretPoint.entity);
|
|
return entities;
|
|
}
|
|
|
|
/**
|
|
* @return {boolean} - Whether all the turret points are occupied.
|
|
*/
|
|
IsFull()
|
|
{
|
|
return !!this.turretPoints.find(turretPoint => turretPoint.entity == null);
|
|
}
|
|
|
|
/**
|
|
* @return {Object} - Max and min ranges at which entities can occupy any turret.
|
|
*/
|
|
LoadingRange()
|
|
{
|
|
return { "min": 0, "max": +(this.template.LoadingRange || 2) };
|
|
}
|
|
|
|
/**
|
|
* @param {number} ent - The entity ID of the turret to be potentially picked up.
|
|
* @return {boolean} - Whether this entity can pick the specified entity up.
|
|
*/
|
|
CanPickup(ent)
|
|
{
|
|
if (!this.template.Pickup || this.IsFull())
|
|
return false;
|
|
const cmpOwner = Engine.QueryInterface(this.entity, IID_Ownership);
|
|
return !!cmpOwner && IsOwnedByPlayer(cmpOwner.GetOwner(), ent);
|
|
}
|
|
|
|
/**
|
|
* @param {number[]} entities - The entities to ask to leave or to kill.
|
|
*/
|
|
EjectOrKill(entities)
|
|
{
|
|
const removedEntities = [];
|
|
for (const entity of entities)
|
|
{
|
|
const cmpTurretable = Engine.QueryInterface(entity, IID_Turretable);
|
|
if (!cmpTurretable || !cmpTurretable.LeaveTurret(true))
|
|
{
|
|
const cmpHealth = Engine.QueryInterface(entity, IID_Health);
|
|
if (cmpHealth)
|
|
cmpHealth.Kill();
|
|
else
|
|
Engine.DestroyEntity(entity);
|
|
removedEntities.push(entity);
|
|
}
|
|
}
|
|
if (removedEntities.length)
|
|
Engine.PostMessage(this.entity, MT_TurretsChanged, {
|
|
"added": [],
|
|
"removed": removedEntities
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Sets an init turret, present from game start. (E.g. set in Atlas.)
|
|
* @param {String} turretName - The name of the turret point to be used.
|
|
* @param {number} entity - The entity-ID to be placed.
|
|
*/
|
|
SetInitEntity(turretName, entity)
|
|
{
|
|
if (!this.initTurrets)
|
|
this.initTurrets = new Map();
|
|
|
|
if (this.initTurrets.has(turretName))
|
|
warn("The turret position " + turretName + " of entity " +
|
|
this.entity + " is already set! Overwriting.");
|
|
|
|
this.initTurrets.set(turretName, entity);
|
|
}
|
|
|
|
/**
|
|
* Update list of turreted entities when a game inits.
|
|
*/
|
|
OnGlobalSkirmishReplacerReplaced(msg)
|
|
{
|
|
if (!this.initTurrets)
|
|
return;
|
|
|
|
if (msg.entity == this.entity)
|
|
{
|
|
const cmpTurretHolder = Engine.QueryInterface(msg.newentity, IID_TurretHolder);
|
|
if (cmpTurretHolder)
|
|
cmpTurretHolder.initTurrets = this.initTurrets;
|
|
}
|
|
else
|
|
{
|
|
const entityIndex = this.initTurrets.indexOf(msg.entity);
|
|
if (entityIndex != -1)
|
|
this.initTurrets[entityIndex] = msg.newentity;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialise turreted units.
|
|
*/
|
|
OnGlobalInitGame(msg)
|
|
{
|
|
if (!this.initTurrets)
|
|
return;
|
|
|
|
for (const [turretPointName, entity] of this.initTurrets)
|
|
{
|
|
const cmpTurretable = Engine.QueryInterface(entity, IID_Turretable);
|
|
if (!cmpTurretable || !cmpTurretable.OccupyTurret(this.entity, turretPointName, this.TurretPointByName(turretPointName).ejectable))
|
|
warn("Entity " + entity + " could not occupy the turret point " +
|
|
turretPointName + " of turret holder " + this.entity + ".");
|
|
}
|
|
|
|
delete this.initTurrets;
|
|
}
|
|
|
|
/**
|
|
* @param {Object} msg - { "entity": number, "newentity": number }.
|
|
*/
|
|
OnEntityRenamed(msg)
|
|
{
|
|
for (const entity of this.GetEntities())
|
|
{
|
|
const cmpTurretable = Engine.QueryInterface(entity, IID_Turretable);
|
|
if (!cmpTurretable)
|
|
continue;
|
|
const currentPoint = this.GetOccupiedTurretPointName(entity);
|
|
cmpTurretable.LeaveTurret(true);
|
|
cmpTurretable.OccupyTurret(msg.newentity, currentPoint);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {Object} msg - { "entity": number, "from": number, "to": number }.
|
|
*/
|
|
OnOwnershipChanged(msg)
|
|
{
|
|
if (msg.to === INVALID_PLAYER)
|
|
{
|
|
this.EjectOrKill(this.GetEntities());
|
|
return;
|
|
}
|
|
|
|
for (const point of this.turretPoints)
|
|
{
|
|
// If we were created, create any subunits now.
|
|
// This has to be done here (instead of on Init)
|
|
// for Ownership ought to be initialised.
|
|
if (point.template && msg.from === INVALID_PLAYER)
|
|
{
|
|
this.CreateSubunit(point.name);
|
|
continue;
|
|
}
|
|
if (!point.entity)
|
|
continue;
|
|
if (!point.ejectable)
|
|
{
|
|
const cmpTurretOwnership = Engine.QueryInterface(point.entity, IID_Ownership);
|
|
if (cmpTurretOwnership)
|
|
cmpTurretOwnership.SetOwner(msg.to);
|
|
}
|
|
else if (!IsOwnedByMutualAllyOfEntity(point.entity, this.entity))
|
|
{
|
|
const cmpTurretable = Engine.QueryInterface(point.entity, IID_Turretable);
|
|
if (cmpTurretable)
|
|
cmpTurretable.LeaveTurret();
|
|
}
|
|
}
|
|
delete this.reservedTurrets;
|
|
}
|
|
}
|
|
|
|
TurretHolder.prototype.Schema =
|
|
"<element name='TurretPoints' a:help='Points that will be used to visibly garrison a unit.'>" +
|
|
"<oneOrMore>" +
|
|
"<element a:help='Element containing the offset coordinates.'>" +
|
|
"<anyName/>" +
|
|
"<interleave>" +
|
|
"<element name='X'>" +
|
|
"<data type='decimal'/>" +
|
|
"</element>" +
|
|
"<element name='Y'>" +
|
|
"<data type='decimal'/>" +
|
|
"</element>" +
|
|
"<element name='Z'>" +
|
|
"<data type='decimal'/>" +
|
|
"</element>" +
|
|
"<optional>" +
|
|
"<interleave>" +
|
|
"<element name='Template'>" +
|
|
"<text/>" +
|
|
"</element>" +
|
|
"<element name='Ejectable' a:help='Whether this template is tied to the turret position (i.e. not allowed to leave the turret point).'>" +
|
|
"<data type='boolean'/>" +
|
|
"</element>" +
|
|
"</interleave>" +
|
|
"</optional>" +
|
|
"<optional>" +
|
|
"<element name='AllowedClasses' a:help='If specified, only entities matching the given classes will be able to use this turret.'>" +
|
|
"<attribute name='datatype'>" +
|
|
"<value>tokens</value>" +
|
|
"</attribute>" +
|
|
"<text/>" +
|
|
"</element>" +
|
|
"</optional>"+
|
|
"<optional>" +
|
|
"<element name='Angle' a:help='Angle in degrees relative to the turretHolder direction.'>" +
|
|
"<data type='decimal'/>" +
|
|
"</element>" +
|
|
"</optional>" +
|
|
"</interleave>" +
|
|
"</element>" +
|
|
"</oneOrMore>" +
|
|
"</element>" +
|
|
"<optional>" +
|
|
"<element name='LoadingRange' a:help='The maximum distance from this holder at which entities are allowed to occupy a turret point. Should be about 2.0 for land entities and preferably greater for ships.'>" +
|
|
"<ref name='nonNegativeDecimal'/>" +
|
|
"</element>" +
|
|
"</optional>" +
|
|
"<optional>" +
|
|
"<element name='Pickup' a:help='This entity will try to move to pick up units to be turreted.'>" +
|
|
"<data type='boolean'/>" +
|
|
"</element>" +
|
|
"</optional>";
|
|
|
|
Engine.RegisterComponentType(IID_TurretHolder, "TurretHolder", TurretHolder);
|