Line data Source code
1 : function Heal() {}
2 :
3 1 : Heal.prototype.Schema =
4 : "<a:help>Controls the healing abilities of the unit.</a:help>" +
5 : "<a:example>" +
6 : "<Range>20</Range>" +
7 : "<RangeOverlay>" +
8 : "<LineTexture>heal_overlay_range.png</LineTexture>" +
9 : "<LineTextureMask>heal_overlay_range_mask.png</LineTextureMask>" +
10 : "<LineThickness>0.35</LineThickness>" +
11 : "</RangeOverlay>" +
12 : "<Health>5</Health>" +
13 : "<Interval>2000</Interval>" +
14 : "<UnhealableClasses datatype=\"tokens\">Cavalry</UnhealableClasses>" +
15 : "<HealableClasses datatype=\"tokens\">Support Infantry</HealableClasses>" +
16 : "</a:example>" +
17 : "<element name='Range' a:help='Range (in metres) where healing is possible.'>" +
18 : "<ref name='nonNegativeDecimal'/>" +
19 : "</element>" +
20 : "<optional>" +
21 : "<element name='RangeOverlay'>" +
22 : "<interleave>" +
23 : "<element name='LineTexture'><text/></element>" +
24 : "<element name='LineTextureMask'><text/></element>" +
25 : "<element name='LineThickness'><ref name='nonNegativeDecimal'/></element>" +
26 : "</interleave>" +
27 : "</element>" +
28 : "</optional>" +
29 : "<element name='Health' a:help='Health healed per Interval.'>" +
30 : "<ref name='nonNegativeDecimal'/>" +
31 : "</element>" +
32 : "<element name='Interval' a:help='A heal is performed every Interval ms.'>" +
33 : "<ref name='nonNegativeDecimal'/>" +
34 : "</element>" +
35 : "<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).'>" +
36 : "<attribute name='datatype'>" +
37 : "<value>tokens</value>" +
38 : "</attribute>" +
39 : "<text/>" +
40 : "</element>" +
41 : "<element name='HealableClasses' a:help='The target must have one of these classes to be healable.'>" +
42 : "<attribute name='datatype'>" +
43 : "<value>tokens</value>" +
44 : "</attribute>" +
45 : "<text/>" +
46 : "</element>";
47 :
48 1 : Heal.prototype.Init = function()
49 : {
50 : };
51 :
52 1 : Heal.prototype.GetTimers = function()
53 : {
54 5 : return {
55 : "prepare": 1000,
56 : "repeat": this.GetInterval()
57 : };
58 : };
59 :
60 1 : Heal.prototype.GetHealth = function()
61 : {
62 5 : return ApplyValueModificationsToEntity("Heal/Health", +this.template.Health, this.entity);
63 : };
64 :
65 1 : Heal.prototype.GetInterval = function()
66 : {
67 6 : return ApplyValueModificationsToEntity("Heal/Interval", +this.template.Interval, this.entity);
68 : };
69 :
70 1 : Heal.prototype.GetRange = function()
71 : {
72 6 : return {
73 : "min": 0,
74 : "max": ApplyValueModificationsToEntity("Heal/Range", +this.template.Range, this.entity)
75 : };
76 : };
77 :
78 1 : Heal.prototype.GetUnhealableClasses = function()
79 : {
80 14 : return this.template.UnhealableClasses._string || "";
81 : };
82 :
83 1 : Heal.prototype.GetHealableClasses = function()
84 : {
85 12 : return this.template.HealableClasses._string || "";
86 : };
87 :
88 : /**
89 : * Whether this entity can heal the target.
90 : *
91 : * @param {number} target - The target's entity ID.
92 : * @return {boolean} - Whether the target can be healed.
93 : */
94 1 : Heal.prototype.CanHeal = function(target)
95 : {
96 16 : let cmpHealth = Engine.QueryInterface(target, IID_Health);
97 16 : if (!cmpHealth || cmpHealth.IsUnhealable() || !cmpHealth.IsInjured())
98 2 : return false;
99 :
100 14 : let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
101 14 : if (!cmpOwnership || !IsOwnedByAllyOfPlayer(cmpOwnership.GetOwner(), target))
102 1 : return false;
103 :
104 13 : let cmpIdentity = Engine.QueryInterface(target, IID_Identity);
105 13 : if (!cmpIdentity)
106 0 : return false;
107 :
108 13 : let targetClasses = cmpIdentity.GetClassesList();
109 13 : return !MatchesClassList(targetClasses, this.GetUnhealableClasses()) &&
110 : MatchesClassList(targetClasses, this.GetHealableClasses());
111 : };
112 :
113 1 : Heal.prototype.GetRangeOverlays = function()
114 : {
115 1 : if (!this.template.RangeOverlay)
116 0 : return [];
117 :
118 1 : return [{
119 : "radius": this.GetRange().max,
120 : "texture": this.template.RangeOverlay.LineTexture,
121 : "textureMask": this.template.RangeOverlay.LineTextureMask,
122 : "thickness": +this.template.RangeOverlay.LineThickness
123 : }];
124 : };
125 :
126 : /**
127 : * @param {number} target - The target to heal.
128 : * @param {number} callerIID - The IID to notify on specific events.
129 : * @return {boolean} - Whether we started healing.
130 : */
131 1 : Heal.prototype.StartHealing = function(target, callerIID)
132 : {
133 4 : if (this.target)
134 3 : this.StopHealing();
135 :
136 4 : if (!this.CanHeal(target))
137 0 : return false;
138 :
139 4 : let timings = this.GetTimers();
140 4 : let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
141 :
142 : // If the repeat time since the last heal hasn't elapsed,
143 : // delay the action to avoid healing too fast.
144 4 : let prepare = timings.prepare;
145 4 : if (this.lastHealed)
146 : {
147 3 : let repeatLeft = this.lastHealed + timings.repeat - cmpTimer.GetTime();
148 3 : prepare = Math.max(prepare, repeatLeft);
149 : }
150 :
151 4 : let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
152 4 : if (cmpVisual)
153 : {
154 0 : cmpVisual.SelectAnimation("heal", false, 1.0);
155 0 : cmpVisual.SetAnimationSyncRepeat(timings.repeat);
156 0 : cmpVisual.SetAnimationSyncOffset(prepare);
157 : }
158 :
159 : // If using a non-default prepare time, re-sync the animation when the timer runs.
160 4 : this.resyncAnimation = prepare != timings.prepare;
161 4 : this.target = target;
162 4 : this.callerIID = callerIID;
163 4 : this.timer = cmpTimer.SetInterval(this.entity, IID_Heal, "PerformHeal", prepare, timings.repeat, null);
164 :
165 4 : return true;
166 : };
167 :
168 : /**
169 : * @param {string} reason - The reason why we stopped healing.
170 : */
171 1 : Heal.prototype.StopHealing = function(reason)
172 : {
173 4 : if (!this.target)
174 0 : return;
175 :
176 4 : let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
177 4 : cmpTimer.CancelTimer(this.timer);
178 4 : delete this.timer;
179 :
180 4 : delete this.target;
181 :
182 4 : let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
183 4 : if (cmpVisual)
184 0 : cmpVisual.SelectAnimation("idle", false, 1.0);
185 :
186 : // The callerIID component may start again,
187 : // replacing the callerIID, hence save that.
188 4 : let callerIID = this.callerIID;
189 4 : delete this.callerIID;
190 :
191 4 : if (reason && callerIID)
192 : {
193 0 : let component = Engine.QueryInterface(this.entity, callerIID);
194 0 : if (component)
195 0 : component.ProcessMessage(reason, null);
196 : }
197 : };
198 :
199 : /**
200 : * Heal our target entity.
201 : * @param data - Unused.
202 : * @param {number} lateness - The offset of the actual call and when it was expected.
203 : */
204 1 : Heal.prototype.PerformHeal = function(data, lateness)
205 : {
206 5 : if (!this.CanHeal(this.target))
207 : {
208 1 : this.StopHealing("TargetInvalidated");
209 1 : return;
210 : }
211 4 : if (!this.IsTargetInRange(this.target))
212 : {
213 0 : this.StopHealing("OutOfRange");
214 0 : return;
215 : }
216 :
217 : // ToDo: Enable entities to keep facing a target.
218 4 : Engine.QueryInterface(this.entity, IID_UnitAI)?.FaceTowardsTarget(this.target);
219 :
220 4 : let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
221 4 : this.lastHealed = cmpTimer.GetTime() - lateness;
222 :
223 4 : let cmpHealth = Engine.QueryInterface(this.target, IID_Health);
224 4 : let targetState = cmpHealth.Increase(this.GetHealth());
225 :
226 : // Add experience.
227 4 : let cmpLoot = Engine.QueryInterface(this.target, IID_Loot);
228 4 : let cmpPromotion = Engine.QueryInterface(this.entity, IID_Promotion);
229 4 : if (targetState !== undefined && cmpLoot && cmpPromotion)
230 : // Health healed times experience per health.
231 2 : cmpPromotion.IncreaseXp((targetState.new - targetState.old) / cmpHealth.GetMaxHitpoints() * cmpLoot.GetXp());
232 :
233 : // TODO we need a sound file.
234 : // PlaySound("heal_impact", this.entity);
235 :
236 4 : if (!cmpHealth.IsInjured())
237 : {
238 0 : this.StopHealing("TargetInvalidated");
239 0 : return;
240 : }
241 :
242 4 : if (this.resyncAnimation)
243 : {
244 1 : let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
245 1 : if (cmpVisual)
246 : {
247 0 : let repeat = this.GetTimers().repeat;
248 0 : cmpVisual.SetAnimationSyncRepeat(repeat);
249 0 : cmpVisual.SetAnimationSyncOffset(repeat);
250 : }
251 1 : delete this.resyncAnimation;
252 : }
253 : };
254 :
255 : /**
256 : * @param {number} - The entity ID of the target to check.
257 : * @return {boolean} - Whether this entity is in range of its target.
258 : */
259 1 : Heal.prototype.IsTargetInRange = function(target)
260 : {
261 4 : let range = this.GetRange();
262 4 : let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
263 4 : return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, range.max, false);
264 : };
265 :
266 1 : Heal.prototype.OnValueModification = function(msg)
267 : {
268 2 : if (msg.component != "Heal" || msg.valueNames.indexOf("Heal/Range") === -1)
269 1 : return;
270 :
271 1 : let cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI);
272 1 : if (!cmpUnitAI)
273 0 : return;
274 :
275 1 : cmpUnitAI.UpdateRangeQueries();
276 : };
277 :
278 1 : Engine.RegisterComponentType(IID_Heal, "Heal", Heal);
|