Line data Source code
1 : function Capturable() {}
2 :
3 1 : Capturable.prototype.Schema =
4 : "<element name='CapturePoints' a:help='Maximum capture points.'>" +
5 : "<ref name='positiveDecimal'/>" +
6 : "</element>" +
7 : "<element name='RegenRate' a:help='Number of capture points that are regenerated per second in favour of the owner.'>" +
8 : "<ref name='nonNegativeDecimal'/>" +
9 : "</element>" +
10 : "<element name='GarrisonRegenRate' a:help='Factor how much each garrisoned entity will add capture points to the regeneration per second in favour of the owner.'>" +
11 : "<ref name='nonNegativeDecimal'/>" +
12 : "</element>";
13 :
14 1 : Capturable.prototype.Init = function()
15 : {
16 19 : this.maxCapturePoints = +this.template.CapturePoints;
17 19 : this.garrisonRegenRate = +this.template.GarrisonRegenRate;
18 19 : this.regenRate = +this.template.RegenRate;
19 19 : this.capturePoints = [];
20 : };
21 :
22 : // Interface functions
23 :
24 : /**
25 : * Returns the current capture points array.
26 : */
27 1 : Capturable.prototype.GetCapturePoints = function()
28 : {
29 10 : return this.capturePoints;
30 : };
31 :
32 1 : Capturable.prototype.GetMaxCapturePoints = function()
33 : {
34 0 : return this.maxCapturePoints;
35 : };
36 :
37 1 : Capturable.prototype.GetGarrisonRegenRate = function()
38 : {
39 57 : return this.garrisonRegenRate;
40 : };
41 :
42 : /**
43 : * Set the new capture points, used for cloning entities.
44 : * The caller should assure that the sum of capture points
45 : * matches the max.
46 : * @param {number[]} - Array with for all players the new value.
47 : */
48 1 : Capturable.prototype.SetCapturePoints = function(capturePointsArray)
49 : {
50 18 : this.capturePoints = capturePointsArray;
51 : };
52 :
53 : /**
54 : * Compute the amount of capture points to be reduced and reduce them.
55 : * @param {number} amount - Number of capture points to be taken.
56 : * @param {number} captor - The entity capturing us.
57 : * @param {number} captorOwner - Owner of the captor.
58 : * @return {Object} - Object of the form { "captureChange": number }, where number indicates the actual amount of capture points taken.
59 : */
60 1 : Capturable.prototype.Capture = function(amount, captor, captorOwner)
61 : {
62 0 : if (captorOwner == INVALID_PLAYER || !this.CanCapture(captorOwner))
63 0 : return {};
64 :
65 : // TODO: implement loot
66 :
67 0 : return { "captureChange": this.Reduce(amount, captorOwner) };
68 : };
69 :
70 : /**
71 : * Reduces the amount of capture points of an entity,
72 : * in favour of the player of the source.
73 : * @param {number} amount - Number of capture points to be taken.
74 : * @param {number} playerID - ID of player the capture points should be awarded to.
75 : * @return {number} - The number of capture points actually taken.
76 : */
77 1 : Capturable.prototype.Reduce = function(amount, playerID)
78 : {
79 16 : if (amount <= 0)
80 2 : return 0;
81 :
82 14 : let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
83 14 : if (!cmpOwnership || cmpOwnership.GetOwner() == INVALID_PLAYER)
84 0 : return 0;
85 :
86 14 : let cmpPlayerSource = QueryPlayerIDInterface(playerID);
87 14 : if (!cmpPlayerSource)
88 0 : return 0;
89 :
90 : // Before changing the value, activate Fogging if necessary to hide changes.
91 14 : let cmpFogging = Engine.QueryInterface(this.entity, IID_Fogging);
92 14 : if (cmpFogging)
93 14 : cmpFogging.Activate();
94 :
95 56 : let numberOfEnemies = this.capturePoints.filter((v, i) => v > 0 && cmpPlayerSource.IsEnemy(i)).length;
96 :
97 14 : if (numberOfEnemies == 0)
98 1 : return 0;
99 :
100 : // Distribute the capture points over all enemies.
101 13 : let distributedAmount = amount / numberOfEnemies;
102 13 : let removedAmount = 0;
103 13 : while (distributedAmount > 0.0001)
104 : {
105 13 : numberOfEnemies = 0;
106 13 : for (let i in this.capturePoints)
107 : {
108 52 : if (!this.capturePoints[i] || !cmpPlayerSource.IsEnemy(i))
109 34 : continue;
110 18 : if (this.capturePoints[i] > distributedAmount)
111 : {
112 14 : removedAmount += distributedAmount;
113 14 : this.capturePoints[i] -= distributedAmount;
114 14 : ++numberOfEnemies;
115 : }
116 : else
117 : {
118 4 : removedAmount += this.capturePoints[i];
119 4 : this.capturePoints[i] = 0;
120 : }
121 : }
122 13 : distributedAmount = numberOfEnemies ? (amount - removedAmount) / numberOfEnemies : 0;
123 : }
124 :
125 : // Give all capture points taken to the player.
126 39 : let takenCapturePoints = this.maxCapturePoints - this.capturePoints.reduce((a, b) => a + b);
127 13 : this.capturePoints[playerID] += takenCapturePoints;
128 :
129 13 : this.CheckTimer();
130 13 : this.RegisterCapturePointsChanged();
131 13 : return takenCapturePoints;
132 : };
133 :
134 : /**
135 : * Check if the source can (re)capture points from this building.
136 : * @param {number} playerID - PlayerID of the source.
137 : * @return {boolean} - Whether the source can (re)capture points from this building.
138 : */
139 1 : Capturable.prototype.CanCapture = function(playerID)
140 : {
141 0 : let cmpPlayerSource = QueryPlayerIDInterface(playerID);
142 :
143 0 : if (!cmpPlayerSource)
144 0 : warn(playerID + " has no player component defined on its id.");
145 0 : let capturePoints = this.GetCapturePoints();
146 0 : let sourceEnemyCapturePoints = 0;
147 0 : for (let i in this.GetCapturePoints())
148 0 : if (cmpPlayerSource.IsEnemy(i))
149 0 : sourceEnemyCapturePoints += capturePoints[i];
150 0 : return sourceEnemyCapturePoints > 0;
151 : };
152 :
153 : // Private functions
154 :
155 : /**
156 : * This has to be called whenever the capture points are changed.
157 : * It notifies other components of the change, and switches ownership when needed.
158 : */
159 1 : Capturable.prototype.RegisterCapturePointsChanged = function()
160 : {
161 16 : let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
162 16 : if (!cmpOwnership)
163 0 : return;
164 :
165 16 : Engine.PostMessage(this.entity, MT_CapturePointsChanged, { "capturePoints": this.capturePoints });
166 :
167 16 : let owner = cmpOwnership.GetOwner();
168 16 : if (owner == INVALID_PLAYER || this.capturePoints[owner] > 0)
169 13 : return;
170 :
171 : // If all capture points have been taken from the owner, convert it to player with the most capture points.
172 3 : let cmpLostPlayerStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker);
173 3 : if (cmpLostPlayerStatisticsTracker)
174 0 : cmpLostPlayerStatisticsTracker.LostEntity(this.entity);
175 :
176 12 : cmpOwnership.SetOwner(this.capturePoints.reduce((bestPlayer, playerCapturePoints, player, capturePoints) => playerCapturePoints > capturePoints[bestPlayer] ? player : bestPlayer, 0));
177 :
178 3 : let cmpCapturedPlayerStatisticsTracker = QueryOwnerInterface(this.entity, IID_StatisticsTracker);
179 3 : if (cmpCapturedPlayerStatisticsTracker)
180 0 : cmpCapturedPlayerStatisticsTracker.CapturedEntity(this.entity);
181 : };
182 :
183 1 : Capturable.prototype.GetRegenRate = function()
184 : {
185 57 : const cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder);
186 57 : if (!cmpGarrisonHolder)
187 0 : return this.regenRate;
188 :
189 57 : let total = this.regenRate;
190 57 : const garrisonRegenRate = this.GetGarrisonRegenRate();
191 57 : for (const entity of cmpGarrisonHolder.GetEntities())
192 : {
193 228 : const captureStrength = Engine.QueryInterface(entity, IID_Attack)?.GetAttackEffectsData("Capture")?.Capture;
194 228 : if (!captureStrength)
195 114 : continue;
196 :
197 114 : total += captureStrength * garrisonRegenRate;
198 : }
199 :
200 57 : return total;
201 : };
202 :
203 1 : Capturable.prototype.TimerTick = function()
204 : {
205 5 : let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
206 5 : if (!cmpOwnership || cmpOwnership.GetOwner() == INVALID_PLAYER)
207 0 : return;
208 :
209 5 : let owner = cmpOwnership.GetOwner();
210 5 : let modifiedCapturePoints = 0;
211 :
212 : // Special handle for the territory decay.
213 : // Reduce capture points from the owner in favour of all neighbours (also allies).
214 5 : let cmpTerritoryDecay = Engine.QueryInterface(this.entity, IID_TerritoryDecay);
215 5 : if (cmpTerritoryDecay && cmpTerritoryDecay.IsDecaying())
216 : {
217 1 : let neighbours = cmpTerritoryDecay.GetConnectedNeighbours();
218 3 : let totalNeighbours = neighbours.reduce((a, b) => a + b);
219 1 : let decay = Math.min(cmpTerritoryDecay.GetDecayRate(), this.capturePoints[owner]);
220 1 : this.capturePoints[owner] -= decay;
221 :
222 1 : if (totalNeighbours)
223 1 : for (let p in neighbours)
224 4 : this.capturePoints[p] += decay * neighbours[p] / totalNeighbours;
225 : // Decay to gaia as default.
226 : else
227 0 : this.capturePoints[0] += decay;
228 :
229 1 : modifiedCapturePoints += decay;
230 1 : this.RegisterCapturePointsChanged();
231 : }
232 :
233 5 : let regenRate = this.GetRegenRate();
234 5 : if (regenRate < 0)
235 1 : modifiedCapturePoints += this.Reduce(-regenRate, 0);
236 4 : else if (regenRate > 0)
237 4 : modifiedCapturePoints += this.Reduce(regenRate, owner);
238 :
239 5 : if (modifiedCapturePoints)
240 4 : return;
241 :
242 : // Nothing changed, stop the timer.
243 1 : let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
244 1 : cmpTimer.CancelTimer(this.timer);
245 1 : delete this.timer;
246 1 : Engine.PostMessage(this.entity, MT_CaptureRegenStateChanged, { "regenerating": false, "regenRate": 0, "territoryDecay": 0 });
247 : };
248 :
249 : /**
250 : * Start the regeneration timer when no timer exists.
251 : * When nothing can be modified (f.e. because it is fully regenerated), the
252 : * timer stops automatically after one execution.
253 : */
254 1 : Capturable.prototype.CheckTimer = function()
255 : {
256 31 : if (this.timer)
257 0 : return;
258 :
259 31 : let regenRate = this.GetRegenRate();
260 31 : let cmpDecay = Engine.QueryInterface(this.entity, IID_TerritoryDecay);
261 31 : let decay = cmpDecay && cmpDecay.IsDecaying() ? cmpDecay.GetDecayRate() : 0;
262 31 : if (regenRate == 0 && decay == 0)
263 0 : return;
264 :
265 31 : let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
266 31 : this.timer = cmpTimer.SetInterval(this.entity, IID_Capturable, "TimerTick", 1000, 1000, null);
267 31 : Engine.PostMessage(this.entity, MT_CaptureRegenStateChanged, { "regenerating": true, "regenRate": regenRate, "territoryDecay": decay });
268 : };
269 :
270 : /**
271 : * Update all chached values that could be affected by modifications.
272 : */
273 1 : Capturable.prototype.UpdateCachedValues = function()
274 : {
275 1 : this.garrisonRegenRate = ApplyValueModificationsToEntity("Capturable/GarrisonRegenRate", +this.template.GarrisonRegenRate, this.entity);
276 1 : this.regenRate = ApplyValueModificationsToEntity("Capturable/RegenRate", +this.template.RegenRate, this.entity);
277 1 : this.maxCapturePoints = ApplyValueModificationsToEntity("Capturable/CapturePoints", +this.template.CapturePoints, this.entity);
278 : };
279 :
280 : /**
281 : * Update all chached values that could be affected by modifications.
282 : * Check timer and send changed messages when required.
283 : * @param {boolean} message - Whether not to send a CapturePointsChanged message. When false, caller should take care of sending that message.
284 : */
285 1 : Capturable.prototype.UpdateCachedValuesAndNotify = function(sendMessage = true)
286 : {
287 0 : let oldMaxCapturePoints = this.maxCapturePoints;
288 0 : let oldGarrisonRegenRate = this.garrisonRegenRate;
289 0 : let oldRegenRate = this.regenRate;
290 :
291 0 : this.UpdateCachedValues();
292 :
293 0 : if (oldMaxCapturePoints != this.maxCapturePoints)
294 : {
295 0 : let scale = this.maxCapturePoints / oldMaxCapturePoints;
296 0 : for (let i in this.capturePoints)
297 0 : this.capturePoints[i] *= scale;
298 0 : if (sendMessage)
299 0 : Engine.PostMessage(this.entity, MT_CapturePointsChanged, { "capturePoints": this.capturePoints });
300 : }
301 :
302 0 : if (oldGarrisonRegenRate != this.garrisonRegenRate || oldRegenRate != this.regenRate)
303 0 : this.CheckTimer();
304 : };
305 :
306 : // Message Listeners
307 :
308 1 : Capturable.prototype.OnValueModification = function(msg)
309 : {
310 0 : if (msg.component == "Capturable")
311 0 : this.UpdateCachedValuesAndNotify();
312 : };
313 :
314 1 : Capturable.prototype.OnGarrisonedUnitsChanged = function(msg)
315 : {
316 0 : this.CheckTimer();
317 : };
318 :
319 1 : Capturable.prototype.OnTerritoryDecayChanged = function(msg)
320 : {
321 0 : if (msg.to)
322 0 : this.CheckTimer();
323 : };
324 :
325 1 : Capturable.prototype.OnDiplomacyChanged = function(msg)
326 : {
327 0 : this.CheckTimer();
328 : };
329 :
330 1 : Capturable.prototype.OnOwnershipChanged = function(msg)
331 : {
332 1 : if (msg.to == INVALID_PLAYER)
333 0 : return;
334 :
335 : // Initialise the capture points when created.
336 1 : if (!this.capturePoints.length)
337 : {
338 1 : this.UpdateCachedValues();
339 1 : let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
340 1 : for (let i = 0; i < numPlayers; ++i)
341 : {
342 4 : if (i == msg.to)
343 1 : this.capturePoints[i] = this.maxCapturePoints;
344 : else
345 3 : this.capturePoints[i] = 0;
346 : }
347 1 : this.CheckTimer();
348 1 : return;
349 : }
350 :
351 : // When already initialised, this happens on defeat or wololo,
352 : // transfer the points of the old owner to the new one.
353 0 : if (this.capturePoints[msg.from])
354 : {
355 0 : this.capturePoints[msg.to] += this.capturePoints[msg.from];
356 0 : this.capturePoints[msg.from] = 0;
357 0 : this.UpdateCachedValuesAndNotify(false);
358 0 : this.RegisterCapturePointsChanged();
359 0 : return;
360 : }
361 :
362 0 : this.UpdateCachedValuesAndNotify();
363 : };
364 :
365 : /**
366 : * When a player is defeated, reassign the capture points of non-owned entities to gaia.
367 : * Those owned by the defeated player are dealt with onOwnershipChanged.
368 : */
369 1 : Capturable.prototype.OnGlobalPlayerDefeated = function(msg)
370 : {
371 1 : if (!this.capturePoints[msg.playerId])
372 0 : return;
373 1 : let cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
374 1 : if (cmpOwnership && (cmpOwnership.GetOwner() == INVALID_PLAYER ||
375 : cmpOwnership.GetOwner() == msg.playerId))
376 0 : return;
377 1 : this.capturePoints[0] += this.capturePoints[msg.playerId];
378 1 : this.capturePoints[msg.playerId] = 0;
379 1 : this.RegisterCapturePointsChanged();
380 1 : this.CheckTimer();
381 : };
382 :
383 : function CapturableMirage() {}
384 1 : CapturableMirage.prototype.Init = function(cmpCapturable)
385 : {
386 0 : this.capturePoints = clone(cmpCapturable.GetCapturePoints());
387 0 : this.maxCapturePoints = cmpCapturable.GetMaxCapturePoints();
388 : };
389 :
390 1 : CapturableMirage.prototype.GetCapturePoints = function() { return this.capturePoints; };
391 1 : CapturableMirage.prototype.GetMaxCapturePoints = function() { return this.maxCapturePoints; };
392 1 : CapturableMirage.prototype.CanCapture = Capturable.prototype.CanCapture;
393 :
394 1 : Engine.RegisterGlobal("CapturableMirage", CapturableMirage);
395 :
396 1 : Capturable.prototype.Mirage = function()
397 : {
398 0 : let mirage = new CapturableMirage();
399 0 : mirage.Init(this);
400 0 : return mirage;
401 : };
402 :
403 1 : Engine.RegisterComponentType(IID_Capturable, "Capturable", Capturable);
|