Files
0ad/binaries/data/mods/public/simulation/components/Heal.js
T
Freagarach 3097a7c302 Attack using cmpAttack instead of UnitAI.
Keep responsibilities separated, allow easier modding of attacking
behaviour.
Ideally the BuildingAI attacking routine would be merged in here as
well, but not done for now.

Part of #5810
Differential revision: D3816
Comments by: @smiley, @Stan
This was SVN commit r25235.
2021-04-12 06:51:39 +00:00

279 lines
7.8 KiB
JavaScript

function Heal() {}
Heal.prototype.Schema =
"<a:help>Controls the healing abilities of the unit.</a:help>" +
"<a:example>" +
"<Range>20</Range>" +
"<RangeOverlay>" +
"<LineTexture>heal_overlay_range.png</LineTexture>" +
"<LineTextureMask>heal_overlay_range_mask.png</LineTextureMask>" +
"<LineThickness>0.35</LineThickness>" +
"</RangeOverlay>" +
"<Health>5</Health>" +
"<Interval>2000</Interval>" +
"<UnhealableClasses datatype=\"tokens\">Cavalry</UnhealableClasses>" +
"<HealableClasses datatype=\"tokens\">Support Infantry</HealableClasses>" +
"</a:example>" +
"<element name='Range' a:help='Range (in metres) where healing is possible.'>" +
"<ref name='nonNegativeDecimal'/>" +
"</element>" +
"<optional>" +
"<element name='RangeOverlay'>" +
"<interleave>" +
"<element name='LineTexture'><text/></element>" +
"<element name='LineTextureMask'><text/></element>" +
"<element name='LineThickness'><ref name='nonNegativeDecimal'/></element>" +
"</interleave>" +
"</element>" +
"</optional>" +
"<element name='Health' a:help='Health healed per Interval.'>" +
"<ref name='nonNegativeDecimal'/>" +
"</element>" +
"<element name='Interval' a:help='A heal is performed every Interval ms.'>" +
"<ref name='nonNegativeDecimal'/>" +
"</element>" +
"<element name='UnhealableClasses' a:help='If the target has any of these classes it can not be healed (even if it has a class from HealableClasses).'>" +
"<attribute name='datatype'>" +
"<value>tokens</value>" +
"</attribute>" +
"<text/>" +
"</element>" +
"<element name='HealableClasses' a:help='The target must have one of these classes to be healable.'>" +
"<attribute name='datatype'>" +
"<value>tokens</value>" +
"</attribute>" +
"<text/>" +
"</element>";
Heal.prototype.Init = function()
{
};
Heal.prototype.GetTimers = function()
{
return {
"prepare": 1000,
"repeat": this.GetInterval()
};
};
Heal.prototype.GetHealth = function()
{
return ApplyValueModificationsToEntity("Heal/Health", +this.template.Health, this.entity);
};
Heal.prototype.GetInterval = function()
{
return ApplyValueModificationsToEntity("Heal/Interval", +this.template.Interval, this.entity);
};
Heal.prototype.GetRange = function()
{
return {
"min": 0,
"max": ApplyValueModificationsToEntity("Heal/Range", +this.template.Range, this.entity)
};
};
Heal.prototype.GetUnhealableClasses = function()
{
return this.template.UnhealableClasses._string || "";
};
Heal.prototype.GetHealableClasses = function()
{
return this.template.HealableClasses._string || "";
};
/**
* Whether this entity can heal the target.
*
* @param {number} target - The target's entity ID.
* @return {boolean} - Whether the target can be healed.
*/
Heal.prototype.CanHeal = function(target)
{
let cmpHealth = Engine.QueryInterface(target, IID_Health);
if (!cmpHealth || cmpHealth.IsUnhealable() || !cmpHealth.IsInjured())
return false;
let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
if (!cmpOwnership || !IsOwnedByAllyOfPlayer(cmpOwnership.GetOwner(), target))
return false;
let cmpIdentity = Engine.QueryInterface(target, IID_Identity);
if (!cmpIdentity)
return false;
let targetClasses = cmpIdentity.GetClassesList();
return !MatchesClassList(targetClasses, this.GetUnhealableClasses()) &&
MatchesClassList(targetClasses, this.GetHealableClasses());
};
Heal.prototype.GetRangeOverlays = function()
{
if (!this.template.RangeOverlay)
return [];
return [{
"radius": this.GetRange().max,
"texture": this.template.RangeOverlay.LineTexture,
"textureMask": this.template.RangeOverlay.LineTextureMask,
"thickness": +this.template.RangeOverlay.LineThickness
}];
};
/**
* @param {number} target - The target to heal.
* @param {number} callerIID - The IID to notify on specific events.
* @return {boolean} - Whether we started healing.
*/
Heal.prototype.StartHealing = function(target, callerIID)
{
if (this.target)
this.StopHealing();
if (!this.CanHeal(target))
return false;
let timings = this.GetTimers();
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
// If the repeat time since the last heal hasn't elapsed,
// delay the action to avoid healing too fast.
let prepare = timings.prepare;
if (this.lastHealed)
{
let repeatLeft = this.lastHealed + timings.repeat - cmpTimer.GetTime();
prepare = Math.max(prepare, repeatLeft);
}
let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (cmpVisual)
{
cmpVisual.SelectAnimation("heal", false, 1.0);
cmpVisual.SetAnimationSyncRepeat(timings.repeat);
cmpVisual.SetAnimationSyncOffset(prepare);
}
// If using a non-default prepare time, re-sync the animation when the timer runs.
this.resyncAnimation = prepare != timings.prepare;
this.target = target;
this.callerIID = callerIID;
this.timer = cmpTimer.SetInterval(this.entity, IID_Heal, "PerformHeal", prepare, timings.repeat, null);
return true;
};
/**
* @param {string} reason - The reason why we stopped healing.
*/
Heal.prototype.StopHealing = function(reason)
{
if (!this.target)
return;
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
cmpTimer.CancelTimer(this.timer);
delete this.timer;
delete this.target;
let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (cmpVisual)
cmpVisual.SelectAnimation("idle", false, 1.0);
// The callerIID component may start again,
// replacing the callerIID, hence save that.
let callerIID = this.callerIID;
delete this.callerIID;
if (reason && callerIID)
{
let component = Engine.QueryInterface(this.entity, callerIID);
if (component)
component.ProcessMessage(reason, null);
}
};
/**
* Heal our target entity.
* @param data - Unused.
* @param {number} lateness - The offset of the actual call and when it was expected.
*/
Heal.prototype.PerformHeal = function(data, lateness)
{
if (!this.CanHeal(this.target))
{
this.StopHealing("TargetInvalidated");
return;
}
if (!this.IsTargetInRange(this.target))
{
this.StopHealing("OutOfRange");
return;
}
// ToDo: Enable entities to keep facing a target.
Engine.QueryInterface(this.entity, IID_UnitAI)?.FaceTowardsTarget(this.target);
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
this.lastHealed = cmpTimer.GetTime() - lateness;
let cmpHealth = Engine.QueryInterface(this.target, IID_Health);
let targetState = cmpHealth.Increase(this.GetHealth());
// Add experience.
let cmpLoot = Engine.QueryInterface(this.target, IID_Loot);
let cmpPromotion = Engine.QueryInterface(this.entity, IID_Promotion);
if (targetState !== undefined && cmpLoot && cmpPromotion)
// Health healed times experience per health.
cmpPromotion.IncreaseXp((targetState.new - targetState.old) / cmpHealth.GetMaxHitpoints() * cmpLoot.GetXp());
// TODO we need a sound file.
// PlaySound("heal_impact", this.entity);
if (!cmpHealth.IsInjured())
{
this.StopHealing("TargetInvalidated");
return;
}
if (this.resyncAnimation)
{
let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
if (cmpVisual)
{
let repeat = this.GetTimers().repeat;
cmpVisual.SetAnimationSyncRepeat(repeat);
cmpVisual.SetAnimationSyncOffset(repeat);
}
delete this.resyncAnimation;
}
};
/**
* @param {number} - The entity ID of the target to check.
* @return {boolean} - Whether this entity is in range of its target.
*/
Heal.prototype.IsTargetInRange = function(target)
{
let range = this.GetRange();
let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, range.max, false);
};
Heal.prototype.OnValueModification = function(msg)
{
if (msg.component != "Heal" || msg.valueNames.indexOf("Heal/Range") === -1)
return;
let cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI);
if (!cmpUnitAI)
return;
cmpUnitAI.UpdateRangeQueries();
};
Engine.RegisterComponentType(IID_Heal, "Heal", Heal);