Line data Source code
1 : function StatisticsTracker() {}
2 :
3 1 : StatisticsTracker.prototype.Schema =
4 : "<a:help>This component records statistics over the course of the match, such as the number of trained, lost, captured and destroyed units and buildings The statistics are consumed by the summary screen and lobby rankings.</a:help>" +
5 : "<a:example>" +
6 : "<UnitClasses>Infantry FemaleCitizen</UnitClasses>" +
7 : "<StructureClasses>House Wonder</StructureClasses>" +
8 : "</a:example>" +
9 : "<element name='UnitClasses' a:help='The tracker records trained, lost, killed and captured units of entities that match any of these Identity classes.'>" +
10 : "<attribute name='datatype'>" +
11 : "<value>tokens</value>" +
12 : "</attribute>" +
13 : "<text/>" +
14 : "</element>" +
15 : "<element name='StructureClasses' a:help='The tracker records constructed, lost, destroyed and captured structures of entities that match any of these Identity classes.'>" +
16 : "<attribute name='datatype'>" +
17 : "<value>tokens</value>" +
18 : "</attribute>" +
19 : "<text/>" +
20 : "</element>";
21 :
22 : /**
23 : * This number specifies the time in milliseconds between consecutive statistics snapshots recorded.
24 : */
25 1 : StatisticsTracker.prototype.UpdateSequenceInterval = 30 * 1000;
26 :
27 1 : StatisticsTracker.prototype.Init = function()
28 : {
29 1 : this.unitsClasses = this.template.UnitClasses._string.split(/\s+/);
30 1 : this.buildingsClasses = this.template.StructureClasses._string.split(/\s+/);
31 :
32 1 : this.unitsTrained = {};
33 1 : this.unitsLost = {};
34 1 : this.enemyUnitsKilled = {};
35 1 : this.unitsCaptured = {};
36 :
37 1 : this.unitsLostValue = 0;
38 1 : this.enemyUnitsKilledValue = 0;
39 1 : this.unitsCapturedValue = 0;
40 :
41 1 : for (let counterName of ["unitsTrained", "unitsLost", "enemyUnitsKilled", "unitsCaptured"])
42 : {
43 4 : this[counterName].total = 0;
44 4 : for (let unitClass of this.unitsClasses)
45 : // Domestic units are only counted for training
46 8 : if (unitClass != "Domestic" || counterName == "unitsTrained")
47 8 : this[counterName][unitClass] = 0;
48 : }
49 :
50 1 : this.buildingsConstructed = {};
51 1 : this.buildingsLost = {};
52 1 : this.enemyBuildingsDestroyed = {};
53 1 : this.buildingsCaptured = {};
54 :
55 1 : this.buildingsLostValue = 0;
56 1 : this.enemyBuildingsDestroyedValue = 0;
57 1 : this.buildingsCapturedValue = 0;
58 :
59 1 : for (let counterName of ["buildingsConstructed", "buildingsLost", "enemyBuildingsDestroyed", "buildingsCaptured"])
60 : {
61 4 : this[counterName].total = 0;
62 4 : for (let unitClass of this.buildingsClasses)
63 8 : this[counterName][unitClass] = 0;
64 : }
65 :
66 1 : this.resourcesGathered = {
67 : "vegetarianFood": 0
68 : };
69 1 : this.resourcesUsed = {};
70 1 : this.resourcesSold = {};
71 1 : this.resourcesBought = {};
72 1 : for (let res of Resources.GetCodes())
73 : {
74 4 : this.resourcesGathered[res] = 0;
75 4 : this.resourcesUsed[res] = 0;
76 4 : this.resourcesSold[res] = 0;
77 4 : this.resourcesBought[res] = 0;
78 : }
79 :
80 1 : this.tributesSent = 0;
81 1 : this.tributesReceived = 0;
82 1 : this.tradeIncome = 0;
83 1 : this.treasuresCollected = 0;
84 1 : this.lootCollected = 0;
85 1 : this.peakPercentMapControlled = 0;
86 1 : this.teamPeakPercentMapControlled = 0;
87 1 : this.successfulBribes = 0;
88 1 : this.failedBribes = 0;
89 :
90 1 : let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
91 1 : this.updateTimer = cmpTimer.SetInterval(
92 : this.entity, IID_StatisticsTracker, "UpdateSequences", 0, this.UpdateSequenceInterval);
93 : };
94 :
95 1 : StatisticsTracker.prototype.OnGlobalInitGame = function()
96 : {
97 0 : this.sequences = clone(this.GetStatistics());
98 0 : this.sequences.time = [];
99 : };
100 :
101 : /**
102 : * Returns a subset of statistics that will be added to the simulation state,
103 : * thus called each turn. Basic statistics should not contain data that would
104 : * be expensive to compute.
105 : *
106 : * Note: as of now, nothing in the game needs that, but some AIs developed by
107 : * modders need it in the API.
108 : */
109 1 : StatisticsTracker.prototype.GetBasicStatistics = function()
110 : {
111 0 : return {
112 : "resourcesGathered": this.resourcesGathered,
113 : "percentMapExplored": this.GetPercentMapExplored()
114 : };
115 : };
116 :
117 1 : StatisticsTracker.prototype.GetStatistics = function()
118 : {
119 0 : return {
120 : "unitsTrained": this.unitsTrained,
121 : "unitsLost": this.unitsLost,
122 : "unitsLostValue": this.unitsLostValue,
123 : "enemyUnitsKilled": this.enemyUnitsKilled,
124 : "enemyUnitsKilledValue": this.enemyUnitsKilledValue,
125 : "unitsCaptured": this.unitsCaptured,
126 : "unitsCapturedValue": this.unitsCapturedValue,
127 : "buildingsConstructed": this.buildingsConstructed,
128 : "buildingsLost": this.buildingsLost,
129 : "buildingsLostValue": this.buildingsLostValue,
130 : "enemyBuildingsDestroyed": this.enemyBuildingsDestroyed,
131 : "enemyBuildingsDestroyedValue": this.enemyBuildingsDestroyedValue,
132 : "buildingsCaptured": this.buildingsCaptured,
133 : "buildingsCapturedValue": this.buildingsCapturedValue,
134 : "resourcesCount": this.GetResourceCounts(),
135 : "resourcesGathered": this.resourcesGathered,
136 : "resourcesUsed": this.resourcesUsed,
137 : "resourcesSold": this.resourcesSold,
138 : "resourcesBought": this.resourcesBought,
139 : "tributesSent": this.tributesSent,
140 : "tributesReceived": this.tributesReceived,
141 : "tradeIncome": this.tradeIncome,
142 : "treasuresCollected": this.treasuresCollected,
143 : "lootCollected": this.lootCollected,
144 : "populationCount": this.GetPopulationCount(),
145 : "percentMapExplored": this.GetPercentMapExplored(),
146 : "teamPercentMapExplored": this.GetTeamPercentMapExplored(),
147 : "percentMapControlled": this.GetPercentMapControlled(),
148 : "teamPercentMapControlled": this.GetTeamPercentMapControlled(),
149 : "peakPercentMapControlled": this.peakPercentMapControlled,
150 : "teamPeakPercentMapControlled": this.teamPeakPercentMapControlled,
151 : "successfulBribes": this.successfulBribes,
152 : "failedBribes": this.failedBribes
153 : };
154 : };
155 :
156 1 : StatisticsTracker.prototype.GetSequences = function()
157 : {
158 0 : let ret = clone(this.sequences);
159 0 : let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
160 :
161 0 : ret.time.push(cmpTimer.GetTime() / 1000);
162 0 : this.PushValue(this.GetStatistics(), ret);
163 0 : return ret;
164 : };
165 :
166 : /**
167 : * Used to print statistics for non-visual autostart games.
168 : * @return The player's statistics as a JSON string beautified with some indentations.
169 : */
170 1 : StatisticsTracker.prototype.GetStatisticsJSON = function()
171 : {
172 0 : let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player);
173 :
174 0 : let playerStatistics = {
175 : "playerID": cmpPlayer.GetPlayerID(),
176 : "playerState": cmpPlayer.GetState(),
177 : "statistics": this.GetStatistics()
178 : };
179 :
180 0 : return JSON.stringify(playerStatistics, null, "\t");
181 : };
182 :
183 : /**
184 : * Increments counter associated with certain entity/counter and type of given entity.
185 : * @param classes - The classes an entity has.
186 : * @param counter - the name of the counter to increment (e.g. "unitsTrained").
187 : * @param type - the type of the counter (e.g. "workers").
188 : */
189 1 : StatisticsTracker.prototype.CounterIncrement = function(classes, counter, type)
190 : {
191 0 : if (!classes)
192 0 : return;
193 :
194 0 : if (classes.includes(type))
195 0 : ++this[counter][type];
196 : };
197 :
198 : /**
199 : * Counts the total number of units trained as well as an individual count for
200 : * each unit type. Based on templates.
201 : */
202 1 : StatisticsTracker.prototype.IncreaseTrainedUnitsCounter = function(trainedUnit)
203 : {
204 0 : const classes = Engine.QueryInterface(trainedUnit, IID_Identity)?.GetClassesList();
205 0 : if (!classes)
206 0 : return;
207 :
208 0 : for (const type of this.unitsClasses)
209 0 : this.CounterIncrement(classes, "unitsTrained", type);
210 :
211 0 : if (!classes.includes("Domestic"))
212 0 : ++this.unitsTrained.total;
213 : };
214 :
215 : /**
216 : * Counts the total number of buildings constructed as well as an individual count for
217 : * each building type. Based on templates.
218 : */
219 1 : StatisticsTracker.prototype.IncreaseConstructedBuildingsCounter = function(constructedBuilding)
220 : {
221 0 : const classes = Engine.QueryInterface(constructedBuilding, IID_Identity)?.GetClassesList();
222 0 : if (!classes)
223 0 : return;
224 :
225 0 : for (const type of this.buildingsClasses)
226 0 : this.CounterIncrement(classes, "buildingsConstructed", type);
227 :
228 0 : ++this.buildingsConstructed.total;
229 : };
230 :
231 1 : StatisticsTracker.prototype.KilledEntity = function(targetEntity)
232 : {
233 0 : const cmpTargetEntityIdentity = Engine.QueryInterface(targetEntity, IID_Identity);
234 0 : if (!cmpTargetEntityIdentity)
235 0 : return;
236 :
237 0 : const classes = cmpTargetEntityIdentity.GetClassesList();
238 0 : const costs = Engine.QueryInterface(targetEntity, IID_Cost)?.GetResourceCosts();
239 :
240 0 : if (cmpTargetEntityIdentity.HasClass("Unit") && !cmpTargetEntityIdentity.HasClass("Animal"))
241 : {
242 0 : for (const type of this.unitsClasses)
243 0 : this.CounterIncrement(classes, "enemyUnitsKilled", type);
244 :
245 0 : if (costs)
246 0 : for (const type in costs)
247 0 : this.enemyUnitsKilledValue += costs[type];
248 : }
249 :
250 0 : if (cmpTargetEntityIdentity.HasClass("Structure") && !Engine.QueryInterface(targetEntity, IID_Foundation))
251 : {
252 0 : for (const type of this.buildingsClasses)
253 0 : this.CounterIncrement(classes, "enemyBuildingsDestroyed", type);
254 :
255 0 : if (costs)
256 0 : for (const type in costs)
257 0 : this.enemyBuildingsDestroyedValue += costs[type];
258 : }
259 : };
260 :
261 1 : StatisticsTracker.prototype.LostEntity = function(lostEntity)
262 : {
263 0 : const cmpLostEntityIdentity = Engine.QueryInterface(lostEntity, IID_Identity);
264 0 : if (!cmpLostEntityIdentity)
265 0 : return;
266 :
267 0 : const classes = cmpLostEntityIdentity.GetClassesList();
268 0 : const costs = Engine.QueryInterface(lostEntity, IID_Cost)?.GetResourceCosts();
269 :
270 0 : if (cmpLostEntityIdentity.HasClass("Unit") && !cmpLostEntityIdentity.HasClass("Domestic"))
271 : {
272 0 : for (const type of this.unitsClasses)
273 0 : this.CounterIncrement(classes, "unitsLost", type);
274 :
275 0 : if (costs)
276 0 : for (const type in costs)
277 0 : this.unitsLostValue += costs[type];
278 : }
279 :
280 0 : if (cmpLostEntityIdentity.HasClass("Structure") && !Engine.QueryInterface(lostEntity, IID_Foundation))
281 : {
282 0 : for (const type of this.buildingsClasses)
283 0 : this.CounterIncrement(classes, "buildingsLost", type);
284 :
285 0 : if (costs)
286 0 : for (const type in costs)
287 0 : this.buildingsLostValue += costs[type];
288 : }
289 : };
290 :
291 1 : StatisticsTracker.prototype.CapturedEntity = function(capturedEntity)
292 : {
293 0 : const cmpCapturedEntityIdentity = Engine.QueryInterface(capturedEntity, IID_Identity);
294 0 : if (!cmpCapturedEntityIdentity)
295 0 : return;
296 :
297 0 : const classes = cmpCapturedEntityIdentity.GetClassesList();
298 0 : const costs = Engine.QueryInterface(capturedEntity, IID_Cost)?.GetResourceCosts();
299 :
300 0 : if (cmpCapturedEntityIdentity.HasClass("Unit"))
301 : {
302 0 : for (const type of this.unitsClasses)
303 0 : this.CounterIncrement(classes, "unitsCaptured", type);
304 :
305 0 : if (costs)
306 0 : for (const type in costs)
307 0 : this.unitsCapturedValue += costs[type];
308 : }
309 :
310 0 : if (cmpCapturedEntityIdentity.HasClass("Structure"))
311 : {
312 0 : for (const type of this.buildingsClasses)
313 0 : this.CounterIncrement(classes, "buildingsCaptured", type);
314 :
315 0 : if (costs)
316 0 : for (const type in costs)
317 0 : this.buildingsCapturedValue += costs[type];
318 : }
319 : };
320 :
321 : /**
322 : * @return {Object} - The amount of available resources.
323 : */
324 1 : StatisticsTracker.prototype.GetResourceCounts = function()
325 : {
326 0 : return Engine.QueryInterface(this.entity, IID_Player)?.GetResourceCounts() ??
327 0 : Object.fromEntries(Resources.GetCodes().map(res => [res, 0]));
328 : };
329 :
330 : /**
331 : * @param {string} type - generic type of resource.
332 : * @param {number} amount - amount of resource, whick should be added.
333 : * @param {string} specificType - specific type of resource.
334 : */
335 1 : StatisticsTracker.prototype.IncreaseResourceGatheredCounter = function(type, amount, specificType)
336 : {
337 0 : this.resourcesGathered[type] += amount;
338 :
339 0 : if (type == "food" && (specificType == "fruit" || specificType == "grain"))
340 0 : this.resourcesGathered.vegetarianFood += amount;
341 : };
342 :
343 : /**
344 : * @param {string} type - generic type of resource.
345 : * @param {number} amount - amount of resource, which should be added.
346 : */
347 1 : StatisticsTracker.prototype.IncreaseResourceUsedCounter = function(type, amount)
348 : {
349 0 : this.resourcesUsed[type] += amount;
350 : };
351 :
352 1 : StatisticsTracker.prototype.IncreaseTreasuresCollectedCounter = function()
353 : {
354 0 : ++this.treasuresCollected;
355 : };
356 :
357 1 : StatisticsTracker.prototype.IncreaseLootCollectedCounter = function(amount)
358 : {
359 0 : for (let type in amount)
360 0 : this.lootCollected += amount[type];
361 : };
362 :
363 1 : StatisticsTracker.prototype.IncreaseResourcesSoldCounter = function(type, amount)
364 : {
365 0 : this.resourcesSold[type] += amount;
366 : };
367 :
368 1 : StatisticsTracker.prototype.IncreaseResourcesBoughtCounter = function(type, amount)
369 : {
370 0 : this.resourcesBought[type] += amount;
371 : };
372 :
373 1 : StatisticsTracker.prototype.IncreaseTributesSentCounter = function(amount)
374 : {
375 0 : this.tributesSent += amount;
376 : };
377 :
378 1 : StatisticsTracker.prototype.IncreaseTributesReceivedCounter = function(amount)
379 : {
380 0 : this.tributesReceived += amount;
381 : };
382 :
383 1 : StatisticsTracker.prototype.IncreaseTradeIncomeCounter = function(amount)
384 : {
385 0 : this.tradeIncome += amount;
386 : };
387 :
388 1 : StatisticsTracker.prototype.GetPopulationCount = function()
389 : {
390 0 : let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player);
391 0 : return cmpPlayer ? cmpPlayer.GetPopulationCount() : 0;
392 : };
393 :
394 1 : StatisticsTracker.prototype.IncreaseSuccessfulBribesCounter = function()
395 : {
396 0 : ++this.successfulBribes;
397 : };
398 :
399 1 : StatisticsTracker.prototype.IncreaseFailedBribesCounter = function()
400 : {
401 0 : ++this.failedBribes;
402 : };
403 :
404 1 : StatisticsTracker.prototype.GetPercentMapExplored = function()
405 : {
406 0 : let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player);
407 0 : if (!cmpPlayer)
408 0 : return 0;
409 :
410 0 : return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetPercentMapExplored(cmpPlayer.GetPlayerID());
411 : };
412 :
413 : /**
414 : * Note: cmpRangeManager.GetUnionPercentMapExplored computes statistics from scratch!
415 : * As a consequence, this function should not be called too often.
416 : */
417 1 : StatisticsTracker.prototype.GetTeamPercentMapExplored = function()
418 : {
419 0 : let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player);
420 0 : if (!cmpPlayer)
421 0 : return 0;
422 :
423 0 : let team = cmpPlayer.GetTeam();
424 0 : let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
425 : // If teams are not locked, this statistic won't be displayed, so don't bother computing
426 0 : if (team == -1 || !cmpPlayer.GetLockTeams())
427 0 : return cmpRangeManager.GetPercentMapExplored(cmpPlayer.GetPlayerID());
428 :
429 0 : let teamPlayers = [];
430 0 : let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
431 0 : for (let i = 1; i < numPlayers; ++i)
432 : {
433 0 : let cmpOtherPlayer = QueryPlayerIDInterface(i);
434 0 : if (cmpOtherPlayer && cmpOtherPlayer.GetTeam() == team)
435 0 : teamPlayers.push(i);
436 : }
437 :
438 0 : return cmpRangeManager.GetUnionPercentMapExplored(teamPlayers);
439 : };
440 :
441 1 : StatisticsTracker.prototype.GetPercentMapControlled = function()
442 : {
443 0 : let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player);
444 0 : if (!cmpPlayer)
445 0 : return 0;
446 :
447 0 : return Engine.QueryInterface(SYSTEM_ENTITY, IID_TerritoryManager).GetTerritoryPercentage(cmpPlayer.GetPlayerID());
448 : };
449 :
450 1 : StatisticsTracker.prototype.GetTeamPercentMapControlled = function()
451 : {
452 0 : let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player);
453 0 : if (!cmpPlayer)
454 0 : return 0;
455 :
456 0 : let team = cmpPlayer.GetTeam();
457 0 : let cmpTerritoryManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TerritoryManager);
458 0 : if (team == -1 || !cmpPlayer.GetLockTeams())
459 0 : return cmpTerritoryManager.GetTerritoryPercentage(cmpPlayer.GetPlayerID());
460 :
461 0 : let teamPercent = 0;
462 0 : let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
463 0 : for (let i = 1; i < numPlayers; ++i)
464 : {
465 0 : let cmpOtherPlayer = QueryPlayerIDInterface(i);
466 0 : if (cmpOtherPlayer && cmpOtherPlayer.GetTeam() == team)
467 0 : teamPercent += cmpTerritoryManager.GetTerritoryPercentage(i);
468 : }
469 :
470 0 : return teamPercent;
471 : };
472 :
473 1 : StatisticsTracker.prototype.OnTerritoriesChanged = function(msg)
474 : {
475 0 : this.UpdatePeakPercentages();
476 : };
477 :
478 1 : StatisticsTracker.prototype.OnGlobalPlayerDefeated = function(msg)
479 : {
480 0 : this.UpdatePeakPercentages();
481 : };
482 :
483 1 : StatisticsTracker.prototype.OnGlobalPlayerWon = function(msg)
484 : {
485 0 : this.UpdatePeakPercentages();
486 : };
487 :
488 1 : StatisticsTracker.prototype.UpdatePeakPercentages = function()
489 : {
490 0 : this.peakPercentMapControlled = Math.max(this.peakPercentMapControlled, this.GetPercentMapControlled());
491 0 : this.teamPeakPercentMapControlled = Math.max(this.teamPeakPercentMapControlled, this.GetTeamPercentMapControlled());
492 : };
493 :
494 : /**
495 : * Adds the values of fromData to the end of the arrays of toData.
496 : * If toData misses the needed array, one will be created.
497 : *
498 : * @param fromData - an object of values or a value.
499 : * @param toData - an object of arrays or an array.
500 : **/
501 1 : StatisticsTracker.prototype.PushValue = function(fromData, toData)
502 : {
503 5 : if (typeof fromData == "object")
504 2 : for (let prop in fromData)
505 : {
506 4 : if (typeof toData[prop] != "object")
507 0 : toData[prop] = [fromData[prop]];
508 : else
509 4 : this.PushValue(fromData[prop], toData[prop]);
510 : }
511 : else
512 3 : toData.push(fromData);
513 : };
514 :
515 1 : StatisticsTracker.prototype.UpdateSequences = function()
516 : {
517 0 : let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
518 0 : this.sequences.time.push(cmpTimer.GetTime() / 1000);
519 0 : this.PushValue(this.GetStatistics(), this.sequences);
520 : };
521 :
522 1 : Engine.RegisterComponentType(IID_StatisticsTracker, "StatisticsTracker", StatisticsTracker);
|