Changeset View
Changeset View
Standalone View
Standalone View
ps/trunk/binaries/data/mods/public/simulation/components/Heal.js
Show First 20 Lines • Show All 43 Lines • ▼ Show 20 Lines | "<element name='HealableClasses' a:help='The target must have one of these classes to be healable.'>" + | ||||
"</attribute>" + | "</attribute>" + | ||||
"<text/>" + | "<text/>" + | ||||
"</element>"; | "</element>"; | ||||
Heal.prototype.Init = function() | Heal.prototype.Init = function() | ||||
{ | { | ||||
}; | }; | ||||
// We have no dynamic state to save. | |||||
Heal.prototype.Serialize = null; | |||||
Heal.prototype.GetTimers = function() | Heal.prototype.GetTimers = function() | ||||
{ | { | ||||
return { | return { | ||||
"prepare": 1000, | "prepare": 1000, | ||||
"repeat": this.GetInterval() | "repeat": this.GetInterval() | ||||
}; | }; | ||||
}; | }; | ||||
Show All 29 Lines | |||||
* Whether this entity can heal the target. | * Whether this entity can heal the target. | ||||
* | * | ||||
* @param {number} target - The target's entity ID. | * @param {number} target - The target's entity ID. | ||||
* @return {boolean} - Whether the target can be healed. | * @return {boolean} - Whether the target can be healed. | ||||
*/ | */ | ||||
Heal.prototype.CanHeal = function(target) | Heal.prototype.CanHeal = function(target) | ||||
{ | { | ||||
let cmpHealth = Engine.QueryInterface(target, IID_Health); | let cmpHealth = Engine.QueryInterface(target, IID_Health); | ||||
if (!cmpHealth || cmpHealth.IsUnhealable()) | if (!cmpHealth || cmpHealth.IsUnhealable() || !cmpHealth.IsInjured()) | ||||
return false; | return false; | ||||
// Verify that the target is owned by an ally or the player self. | |||||
let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); | let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership); | ||||
if (!cmpOwnership || !IsOwnedByAllyOfPlayer(cmpOwnership.GetOwner(), target)) | if (!cmpOwnership || !IsOwnedByAllyOfPlayer(cmpOwnership.GetOwner(), target)) | ||||
return false; | return false; | ||||
// Verify that the target has the right class. | |||||
let cmpIdentity = Engine.QueryInterface(target, IID_Identity); | let cmpIdentity = Engine.QueryInterface(target, IID_Identity); | ||||
if (!cmpIdentity) | if (!cmpIdentity) | ||||
return false; | return false; | ||||
let targetClasses = cmpIdentity.GetClassesList(); | let targetClasses = cmpIdentity.GetClassesList(); | ||||
return !MatchesClassList(targetClasses, this.GetUnhealableClasses()) && | return !MatchesClassList(targetClasses, this.GetUnhealableClasses()) && | ||||
MatchesClassList(targetClasses, this.GetHealableClasses()); | MatchesClassList(targetClasses, this.GetHealableClasses()); | ||||
}; | }; | ||||
Heal.prototype.GetRangeOverlays = function() | Heal.prototype.GetRangeOverlays = function() | ||||
{ | { | ||||
if (!this.template.RangeOverlay) | if (!this.template.RangeOverlay) | ||||
return []; | return []; | ||||
return [{ | return [{ | ||||
"radius": this.GetRange().max, | "radius": this.GetRange().max, | ||||
"texture": this.template.RangeOverlay.LineTexture, | "texture": this.template.RangeOverlay.LineTexture, | ||||
"textureMask": this.template.RangeOverlay.LineTextureMask, | "textureMask": this.template.RangeOverlay.LineTextureMask, | ||||
"thickness": +this.template.RangeOverlay.LineThickness | "thickness": +this.template.RangeOverlay.LineThickness | ||||
}]; | }]; | ||||
}; | }; | ||||
/** | /** | ||||
* Heal the target entity. This should only be called after a successful range | * @param {number} target - The target to heal. | ||||
* check, and should only be called after GetTimers().repeat msec has passed | * @param {number} callerIID - The IID to notify on specific events. | ||||
* since the last call to PerformHeal. | * @return {boolean} - Whether we started healing. | ||||
*/ | */ | ||||
Heal.prototype.PerformHeal = function(target) | Heal.prototype.StartHealing = function(target, callerIID) | ||||
{ | { | ||||
let cmpHealth = Engine.QueryInterface(target, IID_Health); | if (this.target) | ||||
if (!cmpHealth) | 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. Currently implemented are: | |||||
* "outOfRange", "targetInvalidated". | |||||
*/ | |||||
Heal.prototype.StopHealing = function(reason) | |||||
{ | |||||
if (this.timer) | |||||
{ | |||||
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); | |||||
cmpTimer.CancelTimer(this.timer); | |||||
delete this.timer; | |||||
} | |||||
let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual); | |||||
if (cmpVisual) | |||||
cmpVisual.SelectAnimation("idle", false, 1.0); | |||||
delete this.target; | |||||
// The callerIID component may start healing 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. | |||||
* @params - data and lateness are unused. | |||||
*/ | |||||
Heal.prototype.PerformHeal = function(data, lateness) | |||||
{ | |||||
if (!this.CanHeal(this.target)) | |||||
{ | |||||
this.StopHealing("TargetInvalidated"); | |||||
return; | 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()); | let targetState = cmpHealth.Increase(this.GetHealth()); | ||||
// Add experience. | // Add experience. | ||||
let cmpLoot = Engine.QueryInterface(target, IID_Loot); | let cmpLoot = Engine.QueryInterface(this.target, IID_Loot); | ||||
let cmpPromotion = Engine.QueryInterface(this.entity, IID_Promotion); | let cmpPromotion = Engine.QueryInterface(this.entity, IID_Promotion); | ||||
if (targetState !== undefined && cmpLoot && cmpPromotion) | if (targetState !== undefined && cmpLoot && cmpPromotion) | ||||
{ | |||||
// Health healed times experience per health. | // Health healed times experience per health. | ||||
cmpPromotion.IncreaseXp((targetState.new - targetState.old) / cmpHealth.GetMaxHitpoints() * cmpLoot.GetXp()); | cmpPromotion.IncreaseXp((targetState.new - targetState.old) / cmpHealth.GetMaxHitpoints() * cmpLoot.GetXp()); | ||||
} | |||||
// TODO we need a sound file | // TODO we need a sound file. | ||||
// PlaySound("heal_impact", this.entity); | // 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) | Heal.prototype.OnValueModification = function(msg) | ||||
{ | { | ||||
if (msg.component != "Heal" || msg.valueNames.indexOf("Heal/Range") === -1) | if (msg.component != "Heal" || msg.valueNames.indexOf("Heal/Range") === -1) | ||||
return; | return; | ||||
let cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI); | let cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI); | ||||
if (!cmpUnitAI) | if (!cmpUnitAI) | ||||
return; | return; | ||||
cmpUnitAI.UpdateRangeQueries(); | cmpUnitAI.UpdateRangeQueries(); | ||||
}; | }; | ||||
Engine.RegisterComponentType(IID_Heal, "Heal", Heal); | Engine.RegisterComponentType(IID_Heal, "Heal", Heal); |
Wildfire Games · Phabricator