0ad/binaries/data/mods/public/simulation/components/GarrisonHolder.js
2015-02-11 18:34:06 +00:00

746 lines
21 KiB
JavaScript

function GarrisonHolder() {}
GarrisonHolder.prototype.Schema =
"<element name='Max' a:help='Maximum number of entities which can be garrisoned inside this holder'>" +
"<data type='positiveInteger'/>" +
"</element>" +
"<element name='List' a:help='Classes of entities which are allowed to garrison inside this holder (from Identity)'>" +
"<attribute name='datatype'>" +
"<value>tokens</value>" +
"</attribute>" +
"<text/>" +
"</element>" +
"<element name='EjectHealth' a:help='Percentage of maximum health below which this holder no longer allows garrisoning'>" +
"<ref name='nonNegativeDecimal'/>" +
"</element>" +
"<element name='EjectClassesOnDestroy' a:help='Classes of entities to be ejected on destroy. Others are killed'>" +
"<attribute name='datatype'>" +
"<value>tokens</value>" +
"</attribute>" +
"<text/>" +
"</element>" +
"<element name='BuffHeal' a:help='Number of hit points that will be restored to this holder&apos;s garrisoned units each second'>" +
"<ref name='nonNegativeDecimal'/>" +
"</element>" +
"<element name='LoadingRange' a:help='The maximum distance from this holder at which entities are allowed to garrison. Should be about 2.0 for land entities and preferably greater for ships'>" +
"<ref name='nonNegativeDecimal'/>" +
"</element>" +
"<optional>" +
"<element name='Pickup' a:help='This garrisonHolder will move to pick up units to be garrisoned'>" +
"<data type='boolean'/>" +
"</element>" +
"</optional>" +
"<optional>" +
"<element name='VisibleGarrisonPoints' a:help='Points that will be used to visibly garrison a unit'>" +
"<zeroOrMore>" +
"<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>" +
"</interleave>" +
"</element>" +
"</zeroOrMore>" +
"</element>" +
"</optional>";
/**
* Initialize GarrisonHolder Component
*
* Garrisoning when loading a map is set in the script of the map, by setting initGarrison
* which should contain the array of garrisoned entities
*/
GarrisonHolder.prototype.Init = function()
{
// Garrisoned Units
this.entities = [];
this.timer = undefined;
this.allowGarrisoning = {};
this.visibleGarrisonPoints = [];
if (this.template.VisibleGarrisonPoints)
{
for each (var offset in this.template.VisibleGarrisonPoints)
{
var o = {};
o.x = +offset.X;
o.y = +offset.Y;
o.z = +offset.Z;
this.visibleGarrisonPoints.push({"offset":o, "entity": null});
}
}
};
/**
* Return range at which entities can garrison here
*/
GarrisonHolder.prototype.GetLoadingRange = function()
{
var max = +this.template.LoadingRange;
return { "max": max, "min": 0 };
};
/**
* Return true if this garrisonHolder can pickup ent
*/
GarrisonHolder.prototype.CanPickup = function(ent)
{
if (!this.template.Pickup || this.IsFull())
return false;
var cmpOwner = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwner)
return false;
var player = cmpOwner.GetOwner();
return IsOwnedByPlayer(player, ent);
};
/**
* Return the list of entities garrisoned inside
*/
GarrisonHolder.prototype.GetEntities = function()
{
return this.entities;
};
/**
* Returns an array of unit classes which can be garrisoned inside this
* particualar entity. Obtained from the entity's template
*/
GarrisonHolder.prototype.GetAllowedClasses = function()
{
return this.template.List._string;
};
/**
* Get Maximum pop which can be garrisoned
*/
GarrisonHolder.prototype.GetCapacity = function()
{
return ApplyValueModificationsToEntity("GarrisonHolder/Max", +this.template.Max, this.entity);
};
/**
* Return true if this garrisonHolder is full
*/
GarrisonHolder.prototype.IsFull = function()
{
return this.GetGarrisonedEntitiesCount() >= this.GetCapacity();
};
/**
* Get the heal rate with which garrisoned units will be healed
*/
GarrisonHolder.prototype.GetHealRate = function()
{
return ApplyValueModificationsToEntity("GarrisonHolder/BuffHeal", +this.template.BuffHeal, this.entity);
};
/**
* Set this entity to allow or disallow garrisoning in
* Every component calling this function should do it with its own ID, and as long as one
* component doesn't allow this entity to garrison, it can't be garrisoned
* When this entity already contains garrisoned soldiers,
* these will not be able to ungarrison until the flag is set to true again.
*
* This more useful for modern-day features. For example you can't garrison or ungarrison
* a driving vehicle or plane.
*/
GarrisonHolder.prototype.AllowGarrisoning = function(allow, callerID)
{
this.allowGarrisoning[callerID] = allow;
};
/**
* Check if no component of this entity blocks garrisoning
* (f.e. because the vehicle is moving too fast)
*/
GarrisonHolder.prototype.IsGarrisoningAllowed = function()
{
for each (var allow in this.allowGarrisoning)
{
if (!allow)
return false;
}
return true;
};
/**
* Return the number of recursively garrisoned units
*/
GarrisonHolder.prototype.GetGarrisonedEntitiesCount = function()
{
var count = 0;
for each (var ent in this.entities)
{
count++;
var cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder);
if (cmpGarrisonHolder)
count += cmpGarrisonHolder.GetGarrisonedEntitiesCount();
}
return count;
};
/**
* Get number of garrisoned units capable of shooting arrows
* Not necessarily archers
*/
GarrisonHolder.prototype.GetGarrisonedArcherCount = function(garrisonArrowClasses)
{
var count = 0;
for each (var entity in this.entities)
{
var cmpIdentity = Engine.QueryInterface(entity, IID_Identity);
var classes = cmpIdentity.GetClassesList();
if (classes.some(function(c){return garrisonArrowClasses.indexOf(c) > -1;}))
count++;
}
return count;
};
/**
* Checks if an entity can be allowed to garrison in the building
* based on its class
*/
GarrisonHolder.prototype.AllowedToGarrison = function(entity)
{
if (!this.IsGarrisoningAllowed())
return false;
var cmpIdentity = Engine.QueryInterface(entity, IID_Identity);
if (!cmpIdentity)
return false;
var entityClasses = cmpIdentity.GetClassesList();
return MatchesClassList(entityClasses, this.template.List._string);
};
/**
* Garrison a unit inside.
* Returns true if successful, false if not
* The timer for AutoHeal is started here
*/
GarrisonHolder.prototype.Garrison = function(entity)
{
var cmpPosition = Engine.QueryInterface(entity, IID_Position);
if (!cmpPosition)
return false;
if (!this.PerformGarrison(entity))
return false;
var visiblyGarrisoned = false;
for (var vgp of this.visibleGarrisonPoints)
{
if (vgp.entity)
continue;
vgp.entity = entity;
cmpPosition.SetTurretParent(this.entity, vgp.offset);
visiblyGarrisoned = true;
var cmpUnitAI = Engine.QueryInterface(entity, IID_UnitAI);
if (cmpUnitAI)
cmpUnitAI.SetTurretStance();
break;
}
if (!visiblyGarrisoned)
cmpPosition.MoveOutOfWorld();
return true;
};
GarrisonHolder.prototype.PerformGarrison = function(entity)
{
if (!this.HasEnoughHealth())
return false;
// Check if the unit is allowed to be garrisoned inside the building
if(!this.AllowedToGarrison(entity))
return false;
// check the capacity
var extraCount = 0;
var cmpGarrisonHolder = Engine.QueryInterface(entity, IID_GarrisonHolder);
if (cmpGarrisonHolder)
extraCount += cmpGarrisonHolder.GetGarrisonedEntitiesCount();
if (this.GetGarrisonedEntitiesCount() + extraCount >= this.GetCapacity())
return false;
if (!this.timer && this.GetHealRate() > 0)
{
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.timer = cmpTimer.SetTimeout(this.entity, IID_GarrisonHolder, "HealTimeout", 1000, {});
}
// Actual garrisoning happens here
this.entities.push(entity);
this.UpdateGarrisonFlag();
var cmpProductionQueue = Engine.QueryInterface(entity, IID_ProductionQueue);
if (cmpProductionQueue)
cmpProductionQueue.PauseProduction();
var cmpAura = Engine.QueryInterface(entity, IID_Auras);
if (cmpAura && cmpAura.HasGarrisonAura())
cmpAura.ApplyGarrisonBonus(this.entity);
Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, { "added" : [entity], "removed": [] });
var cmpUnitAI = Engine.QueryInterface(entity, IID_UnitAI);
if (cmpUnitAI && cmpUnitAI.IsUnderAlert())
Engine.PostMessage(cmpUnitAI.GetAlertRaiser(), MT_UnitGarrisonedAfterAlert, {"holder": this.entity, "unit": entity});
return true;
};
/**
* Simply eject the unit from the garrisoning entity without
* moving it
* Returns true if successful, false if not
*/
GarrisonHolder.prototype.Eject = function(entity, forced)
{
var entityIndex = this.entities.indexOf(entity);
// Error: invalid entity ID, usually it's already been ejected
if (entityIndex == -1)
return false; // Fail
// Find spawning location
var cmpFootprint = Engine.QueryInterface(this.entity, IID_Footprint);
var cmpHealth = Engine.QueryInterface(this.entity, IID_Health);
var cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
// If the garrisonHolder is a sinking ship, restrict the location to the intersection of both passabilities
// TODO: should use passability classes to be more generic
if ((!cmpHealth || cmpHealth.GetHitpoints() == 0) && cmpIdentity && cmpIdentity.HasClass("Ship"))
var pos = cmpFootprint.PickSpawnPointBothPass(entity);
else
var pos = cmpFootprint.PickSpawnPoint(entity);
if (pos.y < 0)
{
// Error: couldn't find space satisfying the unit's passability criteria
if (forced)
{ // If ejection is forced, we need to continue, so use center of the building
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
pos = cmpPosition.GetPosition();
}
else
{ // Fail
return false;
}
}
var cmpNewPosition = Engine.QueryInterface(entity, IID_Position);
this.entities.splice(entityIndex, 1);
for (var vgp of this.visibleGarrisonPoints)
{
if (vgp.entity != entity)
continue;
cmpNewPosition.SetTurretParent(INVALID_ENTITY, new Vector3D());
var cmpUnitAI = Engine.QueryInterface(entity, IID_UnitAI);
if (cmpUnitAI)
cmpUnitAI.ResetTurretStance();
vgp.entity = null;
break;
}
var cmpUnitAI = Engine.QueryInterface(entity, IID_UnitAI);
if (cmpUnitAI)
cmpUnitAI.Ungarrison();
var cmpProductionQueue = Engine.QueryInterface(entity, IID_ProductionQueue);
if (cmpProductionQueue)
cmpProductionQueue.UnpauseProduction();
var cmpAura = Engine.QueryInterface(entity, IID_Auras);
if (cmpAura && cmpAura.HasGarrisonAura())
cmpAura.RemoveGarrisonBonus(this.entity);
cmpNewPosition.JumpTo(pos.x, pos.z);
cmpNewPosition.SetHeightOffset(0);
// TODO: what direction should they face in?
Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, { "added" : [], "removed": [entity] });
return true;
};
/**
* Order entities to walk to the Rally Point
*/
GarrisonHolder.prototype.OrderWalkToRallyPoint = function(entities)
{
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
var cmpRallyPoint = Engine.QueryInterface(this.entity, IID_RallyPoint);
if (cmpRallyPoint)
{
var rallyPos = cmpRallyPoint.GetPositions()[0];
if (rallyPos)
{
var commands = GetRallyPointCommands(cmpRallyPoint, entities);
// ignore the rally point if it is autogarrison
if (commands[0].type == "garrison" && commands[0].target == this.entity)
return;
for each (var com in commands)
{
ProcessCommand(cmpOwnership.GetOwner(), com);
}
}
}
};
/**
* Ejects units and orders them to move to the Rally Point.
* Returns true if successful, false if not
*/
GarrisonHolder.prototype.PerformEject = function(entities, forced)
{
if (!this.IsGarrisoningAllowed() && !forced)
return false;
var ejectedEntities = [];
var success = true;
for each (var entity in entities)
{
if (this.Eject(entity, forced))
{
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
var cmpEntOwnership = Engine.QueryInterface(entity, IID_Ownership);
if (cmpOwnership && cmpEntOwnership && cmpOwnership.GetOwner() == cmpEntOwnership.GetOwner())
ejectedEntities.push(entity);
}
else
success = false;
}
this.OrderWalkToRallyPoint(ejectedEntities);
this.UpdateGarrisonFlag();
return success;
};
/**
* Unload unit from the garrisoning entity and order them
* to move to the Rally Point
* Returns true if successful, false if not
*/
GarrisonHolder.prototype.Unload = function(entity, forced)
{
return this.PerformEject([entity], forced);
};
/**
* Unload one or all units that match a template and owner from
* the garrisoning entity and order them to move to the Rally Point
* Returns true if successful, false if not
*
* extendedTemplate has the format "p"+ownerid+"&"+template
*/
GarrisonHolder.prototype.UnloadTemplate = function(extendedTemplate, all, forced)
{
var index = extendedTemplate.indexOf("&");
if (index == -1)
return false;
var owner = +extendedTemplate.slice(1,index);
var template = extendedTemplate.slice(index+1);
var entities = [];
var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
for each (var entity in this.entities)
{
var cmpIdentity = Engine.QueryInterface(entity, IID_Identity);
// Units with multiple ranks are grouped together.
var name = cmpIdentity.GetSelectionGroupName()
|| cmpTemplateManager.GetCurrentTemplateName(entity);
if (name != template)
continue;
if (owner != Engine.QueryInterface(entity, IID_Ownership).GetOwner())
continue;
entities.push(entity);
// If 'all' is false, only ungarrison the first matched unit.
if (!all)
break;
}
return this.PerformEject(entities, forced);
};
/**
* Unload all units with same owner as the entity
* and order them to move to the Rally Point
* Returns true if all successful, false if not
*/
GarrisonHolder.prototype.UnloadAllOwn = function(forced)
{
var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership)
return false;
var owner = cmpOwnership.GetOwner();
// Make copy of entity list
var entities = [];
for each (var entity in this.entities)
{
var cmpOwnership = Engine.QueryInterface(entity, IID_Ownership);
if (cmpOwnership && cmpOwnership.GetOwner() == owner)
entities.push(entity);
}
return this.PerformEject(entities, forced);
};
/**
* Unload all units from the entity
* and order them to move to the Rally Point
* Returns true if all successful, false if not
*/
GarrisonHolder.prototype.UnloadAll = function(forced)
{
var entities = this.entities.slice();
return this.PerformEject(entities, forced);
};
/**
* Used to check if the garrisoning entity's health has fallen below
* a certain limit after which all garrisoned units are unloaded
*/
GarrisonHolder.prototype.OnHealthChanged = function(msg)
{
if (!this.HasEnoughHealth() && this.entities.length)
{
var entities = this.entities.slice();
this.EjectOrKill(entities);
}
};
/**
* Check if this entity has enough health to garrison units inside it
*/
GarrisonHolder.prototype.HasEnoughHealth = function()
{
var cmpHealth = Engine.QueryInterface(this.entity, IID_Health)
var hitpoints = cmpHealth.GetHitpoints();
var maxHitpoints = cmpHealth.GetMaxHitpoints();
var ejectHitpoints = Math.floor((+this.template.EjectHealth) * maxHitpoints);
return hitpoints > ejectHitpoints;
};
/**
* Called every second. Heals garrisoned units
*/
GarrisonHolder.prototype.HealTimeout = function(data)
{
if (this.entities.length == 0)
{
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(this.timer);
this.timer = undefined;
}
else
{
for each (var entity in this.entities)
{
var cmpHealth = Engine.QueryInterface(entity, IID_Health);
if (cmpHealth)
{
// We do not want to heal unhealable units
if (!cmpHealth.IsUnhealable())
cmpHealth.Increase(this.GetHealRate());
}
}
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.timer = cmpTimer.SetTimeout(this.entity, IID_GarrisonHolder, "HealTimeout", 1000, {});
}
};
GarrisonHolder.prototype.UpdateGarrisonFlag = function()
{
var cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (!cmpVisual)
return;
cmpVisual.SelectAnimation("garrisoned", true, 0, "");
// TODO: ought to extend ICmpVisual to let us just select variant
// keywords without changing the animation too
if (this.entities.length)
cmpVisual.SelectAnimation("garrisoned", false, 1.0, "");
else
cmpVisual.SelectAnimation("idle", false, 1.0, "");
};
/**
* Cancel timer when destroyed
*/
GarrisonHolder.prototype.OnDestroy = function()
{
if (this.timer)
{
var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(this.timer);
}
};
/**
* If a garrisoned entity is captured, or about to be killed (so its owner
* changes to '-1'), remove it from the building so we only ever contain valid
* entities
*/
GarrisonHolder.prototype.OnGlobalOwnershipChanged = function(msg)
{
// the ownership change may be on the garrisonholder
if (this.entity == msg.entity)
{
var entities = [];
for each (var entity in this.entities)
{
if (msg.to == -1 || !IsOwnedByMutualAllyOfEntity(this.entity, entity))
entities.push(entity);
}
if (entities.length)
this.EjectOrKill(entities);
return;
}
// or on some of its garrisoned units
var entityIndex = this.entities.indexOf(msg.entity);
if (entityIndex != -1)
{
// If the entity is dead, remove it directly instead of ejecting the corpse
var cmpHealth = Engine.QueryInterface(msg.entity, IID_Health);
if (cmpHealth && cmpHealth.GetHitpoints() == 0)
{
this.entities.splice(entityIndex, 1);
Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, { "added" : [], "removed": [msg.entity] });
this.UpdateGarrisonFlag();
for (var pt of this.visibleGarrisonPoints)
if (pt.entity == msg.entity)
pt.entity = null;
}
else if (msg.to == -1 || !IsOwnedByMutualAllyOfEntity(this.entity, msg.entity))
this.EjectOrKill([msg.entity]);
}
};
/**
* Update list of garrisoned entities if one gets renamed (e.g. by promotion)
*/
GarrisonHolder.prototype.OnGlobalEntityRenamed = function(msg)
{
var entityIndex = this.entities.indexOf(msg.entity);
if (entityIndex != -1)
{
this.Eject(msg.entity);
this.Garrison(msg.newentity);
}
if (!this.initGarrison)
return;
// update the pre-game garrison because of SkirmishReplacement
if (msg.entity == this.entity)
{
let cmpGarrisonHolder = Engine.QueryInterface(msg.newentity, IID_GarrisonHolder);
if (cmpGarrisonHolder)
cmpGarrisonHolder.initGarrison = this.initGarrison;
}
else
{
let entityIndex = this.initGarrison.indexOf(msg.entity);
if (entityIndex != -1)
this.initGarrison[entityIndex] = msg.newentity;
}
};
/**
* Eject all foreign garrisoned entities which are no more allied
*/
GarrisonHolder.prototype.OnDiplomacyChanged = function()
{
var entities = []
for each (var entity in this.entities)
{
if (!IsOwnedByMutualAllyOfEntity(this.entity, entity))
entities.push(entity);
}
this.EjectOrKill(entities);
};
/**
* Eject or kill a garrisoned unit which can no more be garrisoned
* (garrisonholder's health too small or ownership changed)
*/
GarrisonHolder.prototype.EjectOrKill = function(entities)
{
var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
// Eject the units which can be ejected (if not in world, it generally means this holder
// is inside a holder which kills its entities, so do not eject)
if (cmpPosition.IsInWorld())
{
var cmpGarrisonHolder = this;
var ejectables = entities.filter(function(ent) { return cmpGarrisonHolder.IsEjectable(ent) });
if (ejectables.length)
this.PerformEject(ejectables, false);
}
// And destroy all remaining entities
for each (var entity in entities)
{
var entityIndex = this.entities.indexOf(entity);
if (entityIndex == -1)
continue;
var cmpHealth = Engine.QueryInterface(entity, IID_Health);
if (cmpHealth)
cmpHealth.Kill();
this.entities.splice(entityIndex, 1);
}
Engine.PostMessage(this.entity, MT_GarrisonedUnitsChanged, { "added" : [], "removed" : entities });
this.UpdateGarrisonFlag();
};
/**
* Checks if an entity is ejectable on destroy if possible
*/
GarrisonHolder.prototype.IsEjectable = function(entity)
{
var ejectableClasses = this.template.EjectClassesOnDestroy._string;
ejectableClasses = ejectableClasses ? ejectableClasses.split(/\s+/) : [];
var entityClasses = (Engine.QueryInterface(entity, IID_Identity)).GetClassesList();
for each (var ejectableClass in ejectableClasses)
if (entityClasses.indexOf(ejectableClass) != -1)
return true;
return false;
};
/**
* Initialise the garrisoned units
*/
GarrisonHolder.prototype.OnGlobalInitGame = function(msg)
{
if (!this.initGarrison)
return;
for (let ent of this.initGarrison)
{
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (!cmpUnitAI || !cmpUnitAI.CanGarrison(this.entity))
continue;
this.Garrison(ent);
}
this.initGarrison = undefined;
};
Engine.RegisterComponentType(IID_GarrisonHolder, "GarrisonHolder", GarrisonHolder);