source: ps/trunk/binaries/data/mods/public/simulation/components/Heal.js@ 25235

Last change on this file since 25235 was 25235, checked in by Freagarach, 3 years ago

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

  • Property svn:eol-style set to native
File size: 7.8 KB
Line 
1function Heal() {}
2
3Heal.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
48Heal.prototype.Init = function()
49{
50};
51
52Heal.prototype.GetTimers = function()
53{
54 return {
55 "prepare": 1000,
56 "repeat": this.GetInterval()
57 };
58};
59
60Heal.prototype.GetHealth = function()
61{
62 return ApplyValueModificationsToEntity("Heal/Health", +this.template.Health, this.entity);
63};
64
65Heal.prototype.GetInterval = function()
66{
67 return ApplyValueModificationsToEntity("Heal/Interval", +this.template.Interval, this.entity);
68};
69
70Heal.prototype.GetRange = function()
71{
72 return {
73 "min": 0,
74 "max": ApplyValueModificationsToEntity("Heal/Range", +this.template.Range, this.entity)
75 };
76};
77
78Heal.prototype.GetUnhealableClasses = function()
79{
80 return this.template.UnhealableClasses._string || "";
81};
82
83Heal.prototype.GetHealableClasses = function()
84{
85 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 */
94Heal.prototype.CanHeal = function(target)
95{
96 let cmpHealth = Engine.QueryInterface(target, IID_Health);
97 if (!cmpHealth || cmpHealth.IsUnhealable() || !cmpHealth.IsInjured())
98 return false;
99
100 let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
101 if (!cmpOwnership || !IsOwnedByAllyOfPlayer(cmpOwnership.GetOwner(), target))
102 return false;
103
104 let cmpIdentity = Engine.QueryInterface(target, IID_Identity);
105 if (!cmpIdentity)
106 return false;
107
108 let targetClasses = cmpIdentity.GetClassesList();
109 return !MatchesClassList(targetClasses, this.GetUnhealableClasses()) &&
110 MatchesClassList(targetClasses, this.GetHealableClasses());
111};
112
113Heal.prototype.GetRangeOverlays = function()
114{
115 if (!this.template.RangeOverlay)
116 return [];
117
118 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 */
131Heal.prototype.StartHealing = function(target, callerIID)
132{
133 if (this.target)
134 this.StopHealing();
135
136 if (!this.CanHeal(target))
137 return false;
138
139 let timings = this.GetTimers();
140 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 let prepare = timings.prepare;
145 if (this.lastHealed)
146 {
147 let repeatLeft = this.lastHealed + timings.repeat - cmpTimer.GetTime();
148 prepare = Math.max(prepare, repeatLeft);
149 }
150
151 let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
152 if (cmpVisual)
153 {
154 cmpVisual.SelectAnimation("heal", false, 1.0);
155 cmpVisual.SetAnimationSyncRepeat(timings.repeat);
156 cmpVisual.SetAnimationSyncOffset(prepare);
157 }
158
159 // If using a non-default prepare time, re-sync the animation when the timer runs.
160 this.resyncAnimation = prepare != timings.prepare;
161 this.target = target;
162 this.callerIID = callerIID;
163 this.timer = cmpTimer.SetInterval(this.entity, IID_Heal, "PerformHeal", prepare, timings.repeat, null);
164
165 return true;
166};
167
168/**
169 * @param {string} reason - The reason why we stopped healing.
170 */
171Heal.prototype.StopHealing = function(reason)
172{
173 if (!this.target)
174 return;
175
176 let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
177 cmpTimer.CancelTimer(this.timer);
178 delete this.timer;
179
180 delete this.target;
181
182 let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
183 if (cmpVisual)
184 cmpVisual.SelectAnimation("idle", false, 1.0);
185
186 // The callerIID component may start again,
187 // replacing the callerIID, hence save that.
188 let callerIID = this.callerIID;
189 delete this.callerIID;
190
191 if (reason && callerIID)
192 {
193 let component = Engine.QueryInterface(this.entity, callerIID);
194 if (component)
195 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 */
204Heal.prototype.PerformHeal = function(data, lateness)
205{
206 if (!this.CanHeal(this.target))
207 {
208 this.StopHealing("TargetInvalidated");
209 return;
210 }
211 if (!this.IsTargetInRange(this.target))
212 {
213 this.StopHealing("OutOfRange");
214 return;
215 }
216
217 // ToDo: Enable entities to keep facing a target.
218 Engine.QueryInterface(this.entity, IID_UnitAI)?.FaceTowardsTarget(this.target);
219
220 let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
221 this.lastHealed = cmpTimer.GetTime() - lateness;
222
223 let cmpHealth = Engine.QueryInterface(this.target, IID_Health);
224 let targetState = cmpHealth.Increase(this.GetHealth());
225
226 // Add experience.
227 let cmpLoot = Engine.QueryInterface(this.target, IID_Loot);
228 let cmpPromotion = Engine.QueryInterface(this.entity, IID_Promotion);
229 if (targetState !== undefined && cmpLoot && cmpPromotion)
230 // Health healed times experience per health.
231 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 if (!cmpHealth.IsInjured())
237 {
238 this.StopHealing("TargetInvalidated");
239 return;
240 }
241
242 if (this.resyncAnimation)
243 {
244 let cmpVisual = Engine.QueryInterface(this.entity, IID_Visual);
245 if (cmpVisual)
246 {
247 let repeat = this.GetTimers().repeat;
248 cmpVisual.SetAnimationSyncRepeat(repeat);
249 cmpVisual.SetAnimationSyncOffset(repeat);
250 }
251 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 */
259Heal.prototype.IsTargetInRange = function(target)
260{
261 let range = this.GetRange();
262 let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
263 return cmpObstructionManager.IsInTargetRange(this.entity, target, range.min, range.max, false);
264};
265
266Heal.prototype.OnValueModification = function(msg)
267{
268 if (msg.component != "Heal" || msg.valueNames.indexOf("Heal/Range") === -1)
269 return;
270
271 let cmpUnitAI = Engine.QueryInterface(this.entity, IID_UnitAI);
272 if (!cmpUnitAI)
273 return;
274
275 cmpUnitAI.UpdateRangeQueries();
276};
277
278Engine.RegisterComponentType(IID_Heal, "Heal", Heal);
Note: See TracBrowser for help on using the repository browser.