Line data Source code
1 : // Setting this to true will display some warnings when commands
2 : // are likely to fail, which may be useful for debugging AIs
3 1 : var g_DebugCommands = false;
4 :
5 : function ProcessCommand(player, cmd)
6 : {
7 0 : let cmpPlayer = QueryPlayerIDInterface(player);
8 0 : if (!cmpPlayer)
9 0 : return;
10 :
11 0 : let data = {
12 : "cmpPlayer": cmpPlayer,
13 : "controlAllUnits": cmpPlayer.CanControlAllUnits()
14 : };
15 :
16 0 : if (cmd.entities)
17 0 : data.entities = FilterEntityList(cmd.entities, player, data.controlAllUnits);
18 :
19 : // TODO: queuing order and forcing formations doesn't really work.
20 : // To play nice, we'll still no-formation queued order if units are in formation
21 : // but the opposite perhaps ought to be implemented.
22 0 : if (!cmd.queued || cmd.formation == NULL_FORMATION)
23 0 : data.formation = cmd.formation || undefined;
24 :
25 : // Allow focusing the camera on recent commands
26 0 : let commandData = {
27 : "type": "playercommand",
28 : "players": [player],
29 : "cmd": cmd
30 : };
31 :
32 : // Save the position, since the GUI event is received after the unit died
33 0 : if (cmd.type == "delete-entities")
34 : {
35 0 : let cmpPosition = cmd.entities[0] && Engine.QueryInterface(cmd.entities[0], IID_Position);
36 0 : commandData.position = cmpPosition && cmpPosition.IsInWorld() && cmpPosition.GetPosition2D();
37 : }
38 :
39 0 : let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
40 0 : cmpGuiInterface.PushNotification(commandData);
41 :
42 : // Note: checks of UnitAI targets are not robust enough here, as ownership
43 : // can change after the order is issued, they should be checked by UnitAI
44 : // when the specific behavior (e.g. attack, garrison) is performed.
45 : // (Also it's not ideal if a command silently fails, it's nicer if UnitAI
46 : // moves the entities closer to the target before giving up.)
47 :
48 : // Now handle various commands
49 0 : if (g_Commands[cmd.type])
50 : {
51 0 : var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
52 0 : cmpTrigger.CallEvent("OnPlayerCommand", { "player": player, "cmd": cmd });
53 0 : g_Commands[cmd.type](player, cmd, data);
54 : }
55 : else
56 0 : error("Invalid command: unknown command type: "+uneval(cmd));
57 : }
58 :
59 1 : var g_Commands = {
60 :
61 : "aichat": function(player, cmd, data)
62 : {
63 0 : var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
64 0 : var notification = { "players": [player] };
65 0 : for (var key in cmd)
66 0 : notification[key] = cmd[key];
67 0 : cmpGuiInterface.PushNotification(notification);
68 : },
69 :
70 : "cheat": function(player, cmd, data)
71 : {
72 0 : Cheat(cmd);
73 : },
74 :
75 : "collect-treasure": function(player, cmd, data)
76 : {
77 0 : GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
78 0 : cmpUnitAI.CollectTreasure(cmd.target, cmd.queued);
79 : });
80 : },
81 :
82 : "collect-treasure-near-position": function(player, cmd, data)
83 : {
84 0 : GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
85 0 : cmpUnitAI.CollectTreasureNearPosition(cmd.x, cmd.z, cmd.queued);
86 : });
87 : },
88 :
89 : "diplomacy": function(player, cmd, data)
90 : {
91 0 : let cmpCeasefireManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager);
92 0 : if (data.cmpPlayer.GetLockTeams() ||
93 : cmpCeasefireManager && cmpCeasefireManager.IsCeasefireActive())
94 0 : return;
95 :
96 0 : switch(cmd.to)
97 : {
98 : case "ally":
99 0 : data.cmpPlayer.SetAlly(cmd.player);
100 0 : break;
101 : case "neutral":
102 0 : data.cmpPlayer.SetNeutral(cmd.player);
103 0 : break;
104 : case "enemy":
105 0 : data.cmpPlayer.SetEnemy(cmd.player);
106 0 : break;
107 : default:
108 0 : warn("Invalid command: Could not set "+player+" diplomacy status of player "+cmd.player+" to "+cmd.to);
109 : }
110 :
111 0 : var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
112 0 : cmpGuiInterface.PushNotification({
113 : "type": "diplomacy",
114 : "players": [player],
115 : "targetPlayer": cmd.player,
116 : "status": cmd.to
117 : });
118 : },
119 :
120 : "tribute": function(player, cmd, data)
121 : {
122 0 : data.cmpPlayer.TributeResource(cmd.player, cmd.amounts);
123 : },
124 :
125 : "control-all": function(player, cmd, data)
126 : {
127 0 : if (!data.cmpPlayer.GetCheatsEnabled())
128 0 : return;
129 :
130 0 : var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
131 0 : cmpGuiInterface.PushNotification({
132 : "type": "aichat",
133 : "players": [player],
134 : "message": markForTranslation("(Cheat - control all units)")
135 : });
136 :
137 0 : data.cmpPlayer.SetControlAllUnits(cmd.flag);
138 : },
139 :
140 : "reveal-map": function(player, cmd, data)
141 : {
142 0 : if (!data.cmpPlayer.GetCheatsEnabled())
143 0 : return;
144 :
145 0 : var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
146 0 : cmpGuiInterface.PushNotification({
147 : "type": "aichat",
148 : "players": [player],
149 : "message": markForTranslation("(Cheat - reveal map)")
150 : });
151 :
152 : // Reveal the map for all players, not just the current player,
153 : // primarily to make it obvious to everyone that the player is cheating
154 0 : var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
155 0 : cmpRangeManager.SetLosRevealAll(-1, cmd.enable);
156 : },
157 :
158 : "walk": function(player, cmd, data)
159 : {
160 0 : GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
161 0 : cmpUnitAI.Walk(cmd.x, cmd.z, cmd.queued, cmd.pushFront);
162 : });
163 : },
164 :
165 : "walk-custom": function(player, cmd, data)
166 : {
167 0 : for (let ent in data.entities)
168 0 : GetFormationUnitAIs([data.entities[ent]], player, cmd, data.formation).forEach(cmpUnitAI => {
169 0 : cmpUnitAI.Walk(cmd.targetPositions[ent].x, cmd.targetPositions[ent].y, cmd.queued, cmd.pushFront);
170 : });
171 : },
172 :
173 : "walk-to-range": function(player, cmd, data)
174 : {
175 : // Only used by the AI
176 0 : for (let ent of data.entities)
177 : {
178 0 : var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
179 0 : if (cmpUnitAI)
180 0 : cmpUnitAI.WalkToPointRange(cmd.x, cmd.z, cmd.min, cmd.max, cmd.queued, cmd.pushFront);
181 : }
182 : },
183 :
184 : "attack-walk": function(player, cmd, data)
185 : {
186 0 : GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
187 0 : cmpUnitAI.WalkAndFight(cmd.x, cmd.z, cmd.targetClasses, cmd.allowCapture, cmd.queued, cmd.pushFront);
188 : });
189 : },
190 :
191 : "attack-walk-custom": function(player, cmd, data)
192 : {
193 0 : for (let ent in data.entities)
194 0 : GetFormationUnitAIs([data.entities[ent]], player, cmd, data.formation).forEach(cmpUnitAI => {
195 0 : cmpUnitAI.WalkAndFight(cmd.targetPositions[ent].x, cmd.targetPositions[ent].y, cmd.targetClasses, cmd.allowCapture, cmd.queued, cmd.pushFront);
196 : });
197 : },
198 :
199 : "attack": function(player, cmd, data)
200 : {
201 0 : GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
202 0 : cmpUnitAI.Attack(cmd.target, cmd.allowCapture, cmd.queued, cmd.pushFront);
203 : });
204 : },
205 :
206 : "patrol": function(player, cmd, data)
207 : {
208 0 : GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI =>
209 0 : cmpUnitAI.Patrol(cmd.x, cmd.z, cmd.targetClasses, cmd.allowCapture, cmd.queued)
210 : );
211 : },
212 :
213 : "heal": function(player, cmd, data)
214 : {
215 0 : if (g_DebugCommands && !(IsOwnedByPlayer(player, cmd.target) || IsOwnedByAllyOfPlayer(player, cmd.target)))
216 0 : warn("Invalid command: heal target is not owned by player "+player+" or their ally: "+uneval(cmd));
217 :
218 0 : GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
219 0 : cmpUnitAI.Heal(cmd.target, cmd.queued, cmd.pushFront);
220 : });
221 : },
222 :
223 : "repair": function(player, cmd, data)
224 : {
225 : // This covers both repairing damaged buildings, and constructing unfinished foundations
226 0 : if (g_DebugCommands && !IsOwnedByAllyOfPlayer(player, cmd.target))
227 0 : warn("Invalid command: repair target is not owned by ally of player "+player+": "+uneval(cmd));
228 :
229 0 : GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
230 0 : cmpUnitAI.Repair(cmd.target, cmd.autocontinue, cmd.queued, cmd.pushFront);
231 : });
232 : },
233 :
234 : "gather": function(player, cmd, data)
235 : {
236 0 : if (g_DebugCommands && !(IsOwnedByPlayer(player, cmd.target) || IsOwnedByGaia(cmd.target)))
237 0 : warn("Invalid command: resource is not owned by gaia or player "+player+": "+uneval(cmd));
238 :
239 0 : GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
240 0 : cmpUnitAI.Gather(cmd.target, cmd.queued, cmd.pushFront);
241 : });
242 : },
243 :
244 : "gather-near-position": function(player, cmd, data)
245 : {
246 0 : GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
247 0 : cmpUnitAI.GatherNearPosition(cmd.x, cmd.z, cmd.resourceType, cmd.resourceTemplate, cmd.queued, cmd.pushFront);
248 : });
249 : },
250 :
251 : "returnresource": function(player, cmd, data)
252 : {
253 0 : if (g_DebugCommands && !IsOwnedByPlayer(player, cmd.target))
254 0 : warn("Invalid command: dropsite is not owned by player "+player+": "+uneval(cmd));
255 :
256 0 : GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
257 0 : cmpUnitAI.ReturnResource(cmd.target, cmd.queued, cmd.pushFront);
258 : });
259 : },
260 :
261 : "back-to-work": function(player, cmd, data)
262 : {
263 0 : for (let ent of data.entities)
264 : {
265 0 : var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
266 0 : if(!cmpUnitAI || !cmpUnitAI.BackToWork())
267 0 : notifyBackToWorkFailure(player);
268 : }
269 : },
270 :
271 : "call-to-arms": function(player, cmd, data)
272 : {
273 0 : const unitsToMove = data.entities.filter(ent =>
274 0 : MatchesClassList(Engine.QueryInterface(ent, IID_Identity).GetClassesList(),
275 : ["Soldier", "Warship", "Siege", "Healer"])
276 : );
277 0 : GetFormationUnitAIs(unitsToMove, player, cmd, data.formation).forEach(cmpUnitAI => {
278 0 : if (cmd.pushFront)
279 : {
280 0 : cmpUnitAI.WalkAndFight(cmd.position.x, cmd.position.z, cmd.targetClasses, cmd.allowCapture, false, cmd.pushFront);
281 0 : cmpUnitAI.DropAtNearestDropSite(false, cmd.pushFront);
282 : }
283 : else
284 : {
285 0 : cmpUnitAI.DropAtNearestDropSite(cmd.queued, false)
286 0 : cmpUnitAI.WalkAndFight(cmd.position.x, cmd.position.z, cmd.targetClasses, cmd.allowCapture, true, false);
287 : }
288 : });
289 : },
290 :
291 : "remove-guard": function(player, cmd, data)
292 : {
293 0 : for (let ent of data.entities)
294 : {
295 0 : var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
296 0 : if (cmpUnitAI)
297 0 : cmpUnitAI.RemoveGuard();
298 : }
299 : },
300 :
301 : "train": function(player, cmd, data)
302 : {
303 0 : if (!Number.isInteger(cmd.count) || cmd.count <= 0)
304 : {
305 0 : warn("Invalid command: can't train " + uneval(cmd.count) + " units");
306 0 : return;
307 : }
308 :
309 : // Check entity limits
310 0 : var template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(cmd.template);
311 0 : var unitCategory = null;
312 0 : if (template.TrainingRestrictions)
313 0 : unitCategory = template.TrainingRestrictions.Category;
314 :
315 : // Verify that the building(s) can be controlled by the player
316 0 : if (data.entities.length <= 0)
317 : {
318 0 : if (g_DebugCommands)
319 0 : warn("Invalid command: training building(s) cannot be controlled by player "+player+": "+uneval(cmd));
320 0 : return;
321 : }
322 :
323 0 : for (let ent of data.entities)
324 : {
325 0 : if (unitCategory)
326 : {
327 0 : var cmpPlayerEntityLimits = QueryOwnerInterface(ent, IID_EntityLimits);
328 0 : if (cmpPlayerEntityLimits && !cmpPlayerEntityLimits.AllowedToTrain(unitCategory, cmd.count, cmd.template, template.TrainingRestrictions.MatchLimit))
329 : {
330 0 : if (g_DebugCommands)
331 0 : warn(unitCategory + " train limit is reached: " + uneval(cmd));
332 0 : continue;
333 : }
334 : }
335 :
336 0 : var cmpTechnologyManager = QueryOwnerInterface(ent, IID_TechnologyManager);
337 0 : if (cmpTechnologyManager && !cmpTechnologyManager.CanProduce(cmd.template))
338 : {
339 0 : if (g_DebugCommands)
340 0 : warn("Invalid command: training requires unresearched technology: " + uneval(cmd));
341 0 : continue;
342 : }
343 :
344 0 : const cmpTrainer = Engine.QueryInterface(ent, IID_Trainer);
345 0 : if (!cmpTrainer)
346 0 : continue;
347 :
348 0 : let templateName = cmd.template;
349 : // Check if the building can train the unit
350 : // TODO: the AI API does not take promotion technologies into account for the list
351 : // of trainable units (taken directly from the unit template). Here is a temporary fix.
352 0 : if (data.cmpPlayer.IsAI())
353 0 : templateName = cmpTrainer.GetUpgradedTemplate(cmd.template);
354 :
355 0 : if (cmpTrainer.CanTrain(templateName))
356 0 : Engine.QueryInterface(ent, IID_ProductionQueue)?.AddItem(templateName, "unit", +cmd.count, cmd.metadata, cmd.pushFront);
357 : }
358 : },
359 :
360 : "research": function(player, cmd, data)
361 : {
362 0 : var cmpTechnologyManager = QueryOwnerInterface(cmd.entity, IID_TechnologyManager);
363 0 : if (cmpTechnologyManager && !cmpTechnologyManager.CanResearch(cmd.template))
364 : {
365 0 : if (g_DebugCommands)
366 0 : warn("Invalid command: Requirements to research technology are not met: " + uneval(cmd));
367 0 : return;
368 : }
369 :
370 0 : var queue = Engine.QueryInterface(cmd.entity, IID_ProductionQueue);
371 0 : if (queue)
372 0 : queue.AddItem(cmd.template, "technology", undefined, cmd.metadata, cmd.pushFront);
373 : },
374 :
375 : "stop-production": function(player, cmd, data)
376 : {
377 0 : let cmpProductionQueue = Engine.QueryInterface(cmd.entity, IID_ProductionQueue);
378 0 : if (cmpProductionQueue)
379 0 : cmpProductionQueue.RemoveItem(cmd.id);
380 : },
381 :
382 : "construct": function(player, cmd, data)
383 : {
384 0 : TryConstructBuilding(player, data.cmpPlayer, data.controlAllUnits, cmd);
385 : },
386 :
387 : "construct-wall": function(player, cmd, data)
388 : {
389 0 : TryConstructWall(player, data.cmpPlayer, data.controlAllUnits, cmd);
390 : },
391 :
392 : "delete-entities": function(player, cmd, data)
393 : {
394 0 : for (let ent of data.entities)
395 : {
396 0 : if (!data.controlAllUnits)
397 : {
398 0 : let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
399 0 : if (cmpIdentity && cmpIdentity.IsUndeletable())
400 0 : continue;
401 :
402 0 : let cmpCapturable = QueryMiragedInterface(ent, IID_Capturable);
403 0 : if (cmpCapturable &&
404 : cmpCapturable.GetCapturePoints()[player] < cmpCapturable.GetMaxCapturePoints() / 2)
405 0 : continue;
406 :
407 0 : let cmpResourceSupply = QueryMiragedInterface(ent, IID_ResourceSupply);
408 0 : if (cmpResourceSupply && cmpResourceSupply.GetKillBeforeGather())
409 0 : continue;
410 : }
411 :
412 0 : let cmpMirage = Engine.QueryInterface(ent, IID_Mirage);
413 0 : if (cmpMirage)
414 : {
415 0 : let cmpMiragedHealth = Engine.QueryInterface(cmpMirage.parent, IID_Health);
416 0 : if (cmpMiragedHealth)
417 0 : cmpMiragedHealth.Kill();
418 : else
419 0 : Engine.DestroyEntity(cmpMirage.parent);
420 :
421 0 : Engine.DestroyEntity(ent);
422 0 : continue;
423 : }
424 :
425 0 : let cmpHealth = Engine.QueryInterface(ent, IID_Health);
426 0 : if (cmpHealth)
427 0 : cmpHealth.Kill();
428 : else
429 0 : Engine.DestroyEntity(ent);
430 : }
431 : },
432 :
433 : "set-rallypoint": function(player, cmd, data)
434 : {
435 0 : for (let ent of data.entities)
436 : {
437 0 : var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
438 0 : if (cmpRallyPoint)
439 : {
440 0 : if (!cmd.queued)
441 0 : cmpRallyPoint.Unset();
442 :
443 0 : cmpRallyPoint.AddPosition(cmd.x, cmd.z);
444 0 : cmpRallyPoint.AddData(clone(cmd.data));
445 : }
446 : }
447 : },
448 :
449 : "unset-rallypoint": function(player, cmd, data)
450 : {
451 0 : for (let ent of data.entities)
452 : {
453 0 : var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
454 0 : if (cmpRallyPoint)
455 0 : cmpRallyPoint.Reset();
456 : }
457 : },
458 :
459 : "resign": function(player, cmd, data)
460 : {
461 0 : data.cmpPlayer.Defeat(markForTranslation("%(player)s has resigned."));
462 : },
463 :
464 : "occupy-turret": function(player, cmd, data)
465 : {
466 0 : GetFormationUnitAIs(data.entities, player).forEach(cmpUnitAI => {
467 0 : cmpUnitAI.OccupyTurret(cmd.target, cmd.queued);
468 : });
469 : },
470 :
471 : "garrison": function(player, cmd, data)
472 : {
473 0 : if (!CanPlayerOrAllyControlUnit(cmd.target, player, data.controlAllUnits))
474 : {
475 0 : if (g_DebugCommands)
476 0 : warn("Invalid command: garrison target cannot be controlled by player "+player+" (or ally): "+uneval(cmd));
477 0 : return;
478 : }
479 :
480 0 : GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
481 0 : cmpUnitAI.Garrison(cmd.target, cmd.queued, cmd.pushFront);
482 : });
483 : },
484 :
485 : "guard": function(player, cmd, data)
486 : {
487 0 : if (!IsOwnedByPlayerOrMutualAlly(cmd.target, player, data.controlAllUnits))
488 : {
489 0 : if (g_DebugCommands)
490 0 : warn("Invalid command: Guard/escort target is not owned by player " + player + " or ally thereof: " + uneval(cmd));
491 0 : return;
492 : }
493 :
494 0 : GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
495 0 : cmpUnitAI.Guard(cmd.target, cmd.queued, cmd.pushFront);
496 : });
497 : },
498 :
499 : "stop": function(player, cmd, data)
500 : {
501 0 : GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
502 0 : cmpUnitAI.Stop(cmd.queued);
503 : });
504 : },
505 :
506 : "leave-turret": function(player, cmd, data)
507 : {
508 0 : let notUnloaded = 0;
509 0 : for (let ent of data.entities)
510 : {
511 0 : let cmpTurretable = Engine.QueryInterface(ent, IID_Turretable);
512 0 : if (!cmpTurretable || !cmpTurretable.LeaveTurret())
513 0 : ++notUnloaded;
514 : }
515 :
516 0 : if (notUnloaded)
517 0 : notifyUnloadFailure(player);
518 : },
519 :
520 : "unload-turrets": function(player, cmd, data)
521 : {
522 0 : let notUnloaded = 0;
523 0 : for (let ent of data.entities)
524 : {
525 0 : let cmpTurretHolder = Engine.QueryInterface(ent, IID_TurretHolder);
526 0 : for (let turret of cmpTurretHolder.GetEntities())
527 : {
528 0 : let cmpTurretable = Engine.QueryInterface(turret, IID_Turretable);
529 0 : if (!cmpTurretable || !cmpTurretable.LeaveTurret())
530 0 : ++notUnloaded;
531 : }
532 : }
533 :
534 0 : if (notUnloaded)
535 0 : notifyUnloadFailure(player);
536 : },
537 :
538 : "unload": function(player, cmd, data)
539 : {
540 0 : if (!CanPlayerOrAllyControlUnit(cmd.garrisonHolder, player, data.controlAllUnits))
541 : {
542 0 : if (g_DebugCommands)
543 0 : warn("Invalid command: unload target cannot be controlled by player "+player+" (or ally): "+uneval(cmd));
544 0 : return;
545 : }
546 :
547 0 : var cmpGarrisonHolder = Engine.QueryInterface(cmd.garrisonHolder, IID_GarrisonHolder);
548 0 : var notUngarrisoned = 0;
549 :
550 : // The owner can ungarrison every garrisoned unit
551 0 : if (IsOwnedByPlayer(player, cmd.garrisonHolder))
552 0 : data.entities = cmd.entities;
553 :
554 0 : for (let ent of data.entities)
555 0 : if (!cmpGarrisonHolder || !cmpGarrisonHolder.Unload(ent))
556 0 : ++notUngarrisoned;
557 :
558 0 : if (notUngarrisoned != 0)
559 0 : notifyUnloadFailure(player, cmd.garrisonHolder);
560 : },
561 :
562 : "unload-template": function(player, cmd, data)
563 : {
564 0 : var entities = FilterEntityListWithAllies(cmd.garrisonHolders, player, data.controlAllUnits);
565 0 : for (let garrisonHolder of entities)
566 : {
567 0 : var cmpGarrisonHolder = Engine.QueryInterface(garrisonHolder, IID_GarrisonHolder);
568 0 : if (cmpGarrisonHolder)
569 : {
570 : // Only the owner of the garrisonHolder may unload entities from any owners
571 0 : if (!IsOwnedByPlayer(player, garrisonHolder) && !data.controlAllUnits
572 : && player != +cmd.owner)
573 0 : continue;
574 :
575 0 : if (!cmpGarrisonHolder.UnloadTemplate(cmd.template, cmd.owner, cmd.all))
576 0 : notifyUnloadFailure(player, garrisonHolder);
577 : }
578 : }
579 : },
580 :
581 : "unload-all-by-owner": function(player, cmd, data)
582 : {
583 0 : var entities = FilterEntityListWithAllies(cmd.garrisonHolders, player, data.controlAllUnits);
584 0 : for (let garrisonHolder of entities)
585 : {
586 0 : var cmpGarrisonHolder = Engine.QueryInterface(garrisonHolder, IID_GarrisonHolder);
587 0 : if (!cmpGarrisonHolder || !cmpGarrisonHolder.UnloadAllByOwner(player))
588 0 : notifyUnloadFailure(player, garrisonHolder);
589 : }
590 : },
591 :
592 : "unload-all": function(player, cmd, data)
593 : {
594 0 : var entities = FilterEntityList(cmd.garrisonHolders, player, data.controlAllUnits);
595 0 : for (let garrisonHolder of entities)
596 : {
597 0 : var cmpGarrisonHolder = Engine.QueryInterface(garrisonHolder, IID_GarrisonHolder);
598 0 : if (!cmpGarrisonHolder || !cmpGarrisonHolder.UnloadAll())
599 0 : notifyUnloadFailure(player, garrisonHolder);
600 : }
601 : },
602 :
603 : "alert-raise": function(player, cmd, data)
604 : {
605 0 : for (let ent of data.entities)
606 : {
607 0 : var cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser);
608 0 : if (cmpAlertRaiser)
609 0 : cmpAlertRaiser.RaiseAlert();
610 : }
611 : },
612 :
613 : "alert-end": function(player, cmd, data)
614 : {
615 0 : for (let ent of data.entities)
616 : {
617 0 : var cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser);
618 0 : if (cmpAlertRaiser)
619 0 : cmpAlertRaiser.EndOfAlert();
620 : }
621 : },
622 :
623 : "formation": function(player, cmd, data)
624 : {
625 0 : GetFormationUnitAIs(data.entities, player, cmd, data.formation, true).forEach(cmpUnitAI => {
626 0 : cmpUnitAI.MoveIntoFormation(cmd);
627 : });
628 : },
629 :
630 : "promote": function(player, cmd, data)
631 : {
632 0 : if (!data.cmpPlayer.GetCheatsEnabled())
633 0 : return;
634 :
635 0 : var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
636 0 : cmpGuiInterface.PushNotification({
637 : "type": "aichat",
638 : "players": [player],
639 : "message": markForTranslation("(Cheat - promoted units)"),
640 : "translateMessage": true
641 : });
642 :
643 0 : for (let ent of cmd.entities)
644 : {
645 0 : var cmpPromotion = Engine.QueryInterface(ent, IID_Promotion);
646 0 : if (cmpPromotion)
647 0 : cmpPromotion.IncreaseXp(cmpPromotion.GetRequiredXp() - cmpPromotion.GetCurrentXp());
648 : }
649 : },
650 :
651 : "stance": function(player, cmd, data)
652 : {
653 0 : for (let ent of data.entities)
654 : {
655 0 : var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
656 0 : if (cmpUnitAI && !cmpUnitAI.IsTurret())
657 0 : cmpUnitAI.SwitchToStance(cmd.name);
658 : }
659 : },
660 :
661 : "lock-gate": function(player, cmd, data)
662 : {
663 0 : for (let ent of data.entities)
664 : {
665 0 : var cmpGate = Engine.QueryInterface(ent, IID_Gate);
666 0 : if (!cmpGate)
667 0 : continue;
668 :
669 0 : if (cmd.lock)
670 0 : cmpGate.LockGate();
671 : else
672 0 : cmpGate.UnlockGate();
673 : }
674 : },
675 :
676 : "setup-trade-route": function(player, cmd, data)
677 : {
678 0 : GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
679 0 : cmpUnitAI.SetupTradeRoute(cmd.target, cmd.source, cmd.route, cmd.queued);
680 : });
681 : },
682 :
683 : "cancel-setup-trade-route": function(player, cmd, data)
684 : {
685 0 : GetFormationUnitAIs(data.entities, player, cmd, data.formation).forEach(cmpUnitAI => {
686 0 : cmpUnitAI.CancelSetupTradeRoute(cmd.target);
687 : });
688 : },
689 :
690 : "set-trading-goods": function(player, cmd, data)
691 : {
692 0 : data.cmpPlayer.SetTradingGoods(cmd.tradingGoods);
693 : },
694 :
695 : "barter": function(player, cmd, data)
696 : {
697 0 : var cmpBarter = Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter);
698 0 : cmpBarter.ExchangeResources(player, cmd.sell, cmd.buy, cmd.amount);
699 : },
700 :
701 : "set-shading-color": function(player, cmd, data)
702 : {
703 : // Prevent multiplayer abuse
704 0 : if (!data.cmpPlayer.IsAI())
705 0 : return;
706 :
707 : // Debug command to make an entity brightly colored
708 0 : for (let ent of cmd.entities)
709 : {
710 0 : var cmpVisual = Engine.QueryInterface(ent, IID_Visual);
711 0 : if (cmpVisual)
712 0 : cmpVisual.SetShadingColor(cmd.rgb[0], cmd.rgb[1], cmd.rgb[2], 0); // alpha isn't used so just send 0
713 : }
714 : },
715 :
716 : "pack": function(player, cmd, data)
717 : {
718 0 : for (let ent of data.entities)
719 : {
720 0 : var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
721 0 : if (!cmpUnitAI)
722 0 : continue;
723 :
724 0 : if (cmd.pack)
725 0 : cmpUnitAI.Pack(cmd.queued, cmd.pushFront);
726 : else
727 0 : cmpUnitAI.Unpack(cmd.queued, cmd.pushFront);
728 : }
729 : },
730 :
731 : "cancel-pack": function(player, cmd, data)
732 : {
733 0 : for (let ent of data.entities)
734 : {
735 0 : var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
736 0 : if (!cmpUnitAI)
737 0 : continue;
738 :
739 0 : if (cmd.pack)
740 0 : cmpUnitAI.CancelPack(cmd.queued, cmd.pushFront);
741 : else
742 0 : cmpUnitAI.CancelUnpack(cmd.queued, cmd.pushFront);
743 : }
744 : },
745 :
746 : "upgrade": function(player, cmd, data)
747 : {
748 0 : for (let ent of data.entities)
749 : {
750 0 : var cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade);
751 :
752 0 : if (!cmpUpgrade || !cmpUpgrade.CanUpgradeTo(cmd.template))
753 0 : continue;
754 :
755 0 : if (cmpUpgrade.WillCheckPlacementRestrictions(cmd.template) && ObstructionsBlockingTemplateChange(ent, cmd.template))
756 : {
757 0 : var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
758 0 : cmpGUIInterface.PushNotification({
759 : "players": [player],
760 : "message": markForTranslation("Cannot upgrade as distance requirements are not verified or terrain is obstructed.")
761 : });
762 0 : continue;
763 : }
764 :
765 : // Check entity limits
766 0 : var cmpEntityLimits = QueryPlayerIDInterface(player, IID_EntityLimits);
767 0 : if (cmpEntityLimits && !cmpEntityLimits.AllowedToReplace(ent, cmd.template))
768 : {
769 0 : if (g_DebugCommands)
770 0 : warn("Invalid command: build limits check failed for player " + player + ": " + uneval(cmd));
771 0 : continue;
772 : }
773 :
774 0 : if (!RequirementsHelper.AreRequirementsMet(cmpUpgrade.GetRequirements(cmd.template), player))
775 : {
776 0 : if (g_DebugCommands)
777 0 : warn("Invalid command: upgrading is not possible for this player or requires unresearched technology: " + uneval(cmd));
778 0 : continue;
779 : }
780 :
781 0 : cmpUpgrade.Upgrade(cmd.template, data.cmpPlayer);
782 : }
783 : },
784 :
785 : "cancel-upgrade": function(player, cmd, data)
786 : {
787 0 : for (let ent of data.entities)
788 : {
789 0 : let cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade);
790 0 : if (cmpUpgrade)
791 0 : cmpUpgrade.CancelUpgrade(player);
792 : }
793 : },
794 :
795 : "attack-request": function(player, cmd, data)
796 : {
797 : // Send a chat message to human players
798 0 : var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
799 0 : cmpGuiInterface.PushNotification({
800 : "type": "aichat",
801 : "players": [player],
802 : "message": "/allies " + markForTranslation("Attack against %(_player_)s requested."),
803 : "translateParameters": ["_player_"],
804 : "parameters": { "_player_": cmd.player }
805 : });
806 :
807 : // And send an attackRequest event to the AIs
808 0 : let cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface);
809 0 : if (cmpAIInterface)
810 0 : cmpAIInterface.PushEvent("AttackRequest", cmd);
811 : },
812 :
813 : "spy-request": function(player, cmd, data)
814 : {
815 0 : let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
816 0 : let ent = pickRandom(cmpRangeManager.GetEntitiesByPlayer(cmd.player).filter(ent => {
817 0 : let cmpVisionSharing = Engine.QueryInterface(ent, IID_VisionSharing);
818 0 : return cmpVisionSharing && cmpVisionSharing.IsBribable() && !cmpVisionSharing.ShareVisionWith(player);
819 : }));
820 :
821 0 : let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
822 0 : cmpGUIInterface.PushNotification({
823 : "type": "spy-response",
824 : "players": [player],
825 : "target": cmd.player,
826 : "entity": ent
827 : });
828 0 : if (ent)
829 0 : Engine.QueryInterface(ent, IID_VisionSharing).AddSpy(cmd.source);
830 : else
831 : {
832 0 : let template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate("special/spy");
833 0 : IncurBribeCost(template, player, cmd.player, true);
834 : // update statistics for failed bribes
835 0 : let cmpBribesStatisticsTracker = QueryPlayerIDInterface(player, IID_StatisticsTracker);
836 0 : if (cmpBribesStatisticsTracker)
837 0 : cmpBribesStatisticsTracker.IncreaseFailedBribesCounter();
838 0 : cmpGUIInterface.PushNotification({
839 : "type": "text",
840 : "players": [player],
841 : "message": markForTranslation("There are no bribable units"),
842 : "translateMessage": true
843 : });
844 : }
845 : },
846 :
847 : "diplomacy-request": function(player, cmd, data)
848 : {
849 0 : let cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface);
850 0 : if (cmpAIInterface)
851 0 : cmpAIInterface.PushEvent("DiplomacyRequest", cmd);
852 : },
853 :
854 : "tribute-request": function(player, cmd, data)
855 : {
856 0 : let cmpAIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIInterface);
857 0 : if (cmpAIInterface)
858 0 : cmpAIInterface.PushEvent("TributeRequest", cmd);
859 : },
860 :
861 : "dialog-answer": function(player, cmd, data)
862 : {
863 : // Currently nothing. Triggers can read it anyway, and send this
864 : // message to any component you like.
865 : },
866 :
867 : "set-dropsite-sharing": function(player, cmd, data)
868 : {
869 0 : for (let ent of data.entities)
870 : {
871 0 : let cmpResourceDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite);
872 0 : if (cmpResourceDropsite && cmpResourceDropsite.IsSharable())
873 0 : cmpResourceDropsite.SetSharing(cmd.shared);
874 : }
875 : },
876 :
877 : "map-flare": function(player, cmd, data)
878 : {
879 0 : let cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
880 0 : cmpGuiInterface.PushNotification({
881 : "type": "map-flare",
882 : "players": [player],
883 : "position": cmd.position
884 : });
885 : },
886 :
887 : "autoqueue-on": function(player, cmd, data)
888 : {
889 0 : for (let ent of data.entities)
890 : {
891 0 : let cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue);
892 0 : if (cmpProductionQueue)
893 0 : cmpProductionQueue.EnableAutoQueue();
894 : }
895 : },
896 :
897 : "autoqueue-off": function(player, cmd, data)
898 : {
899 0 : for (let ent of data.entities)
900 : {
901 0 : let cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue);
902 0 : if (cmpProductionQueue)
903 0 : cmpProductionQueue.DisableAutoQueue();
904 : }
905 : },
906 :
907 : };
908 :
909 : /**
910 : * Sends a GUI notification about unit(s) that failed to ungarrison.
911 : */
912 : function notifyUnloadFailure(player)
913 : {
914 0 : let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
915 0 : cmpGUIInterface.PushNotification({
916 : "type": "text",
917 : "players": [player],
918 : "message": markForTranslation("Unable to unload unit(s)."),
919 : "translateMessage": true
920 : });
921 : }
922 :
923 : /**
924 : * Sends a GUI notification about worker(s) that failed to go back to work.
925 : */
926 : function notifyBackToWorkFailure(player)
927 : {
928 0 : var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
929 0 : cmpGUIInterface.PushNotification({
930 : "type": "text",
931 : "players": [player],
932 : "message": markForTranslation("Some unit(s) can't go back to work"),
933 : "translateMessage": true
934 : });
935 : }
936 :
937 : /**
938 : * Sends a GUI notification about entities that can't be controlled.
939 : * @param {number} player - The player-ID of the player that needs to receive this message.
940 : */
941 : function notifyOrderFailure(entity, player)
942 : {
943 0 : let cmpIdentity = Engine.QueryInterface(entity, IID_Identity);
944 0 : if (!cmpIdentity)
945 0 : return;
946 :
947 0 : let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
948 0 : cmpGUIInterface.PushNotification({
949 : "type": "text",
950 : "players": [player],
951 : "message": markForTranslation("%(unit)s can't be controlled."),
952 : "parameters": { "unit": cmpIdentity.GetGenericName() },
953 : "translateParameters": ["unit"],
954 : "translateMessage": true
955 : });
956 : }
957 :
958 : /**
959 : * Get some information about the formations used by entities.
960 : */
961 : function ExtractFormations(ents)
962 : {
963 0 : let entities = []; // Entities with UnitAI.
964 0 : let members = {}; // { formationentity: [ent, ent, ...], ... }
965 0 : let templates = {}; // { formationentity: template }
966 0 : for (let ent of ents)
967 : {
968 0 : let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
969 0 : if (!cmpUnitAI)
970 0 : continue;
971 :
972 0 : entities.push(ent);
973 :
974 0 : let fid = cmpUnitAI.GetFormationController();
975 0 : if (fid == INVALID_ENTITY)
976 0 : continue;
977 :
978 0 : if (!members[fid])
979 : {
980 0 : members[fid] = [];
981 0 : templates[fid] = cmpUnitAI.GetFormationTemplate();
982 : }
983 0 : members[fid].push(ent);
984 : }
985 :
986 0 : return {
987 : "entities": entities,
988 : "members": members,
989 : "templates": templates
990 : };
991 : }
992 :
993 : /**
994 : * Tries to find the best angle to put a dock at a given position
995 : * Taken from GuiInterface.js
996 : */
997 : function GetDockAngle(template, x, z)
998 : {
999 0 : var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
1000 0 : var cmpWaterManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_WaterManager);
1001 0 : if (!cmpTerrain || !cmpWaterManager)
1002 0 : return undefined;
1003 :
1004 : // Get footprint size
1005 0 : var halfSize = 0;
1006 0 : if (template.Footprint.Square)
1007 0 : halfSize = Math.max(template.Footprint.Square["@depth"], template.Footprint.Square["@width"])/2;
1008 0 : else if (template.Footprint.Circle)
1009 0 : halfSize = template.Footprint.Circle["@radius"];
1010 :
1011 : /* Find direction of most open water, algorithm:
1012 : * 1. Pick points in a circle around dock
1013 : * 2. If point is in water, add to array
1014 : * 3. Scan array looking for consecutive points
1015 : * 4. Find longest sequence of consecutive points
1016 : * 5. If sequence equals all points, no direction can be determined,
1017 : * expand search outward and try (1) again
1018 : * 6. Calculate angle using average of sequence
1019 : */
1020 0 : const numPoints = 16;
1021 0 : for (var dist = 0; dist < 4; ++dist)
1022 : {
1023 0 : var waterPoints = [];
1024 0 : for (var i = 0; i < numPoints; ++i)
1025 : {
1026 0 : var angle = (i/numPoints)*2*Math.PI;
1027 0 : var d = halfSize*(dist+1);
1028 0 : var nx = x - d*Math.sin(angle);
1029 0 : var nz = z + d*Math.cos(angle);
1030 :
1031 0 : if (cmpTerrain.GetGroundLevel(nx, nz) < cmpWaterManager.GetWaterLevel(nx, nz))
1032 0 : waterPoints.push(i);
1033 : }
1034 0 : var consec = [];
1035 0 : var length = waterPoints.length;
1036 0 : if (!length)
1037 0 : continue;
1038 0 : for (var i = 0; i < length; ++i)
1039 : {
1040 0 : var count = 0;
1041 0 : for (let j = 0; j < length - 1; ++j)
1042 : {
1043 0 : if ((waterPoints[(i + j) % length] + 1) % numPoints == waterPoints[(i + j + 1) % length])
1044 0 : ++count;
1045 : else
1046 0 : break;
1047 : }
1048 0 : consec[i] = count;
1049 : }
1050 0 : var start = 0;
1051 0 : var count = 0;
1052 0 : for (var c in consec)
1053 : {
1054 0 : if (consec[c] > count)
1055 : {
1056 0 : start = c;
1057 0 : count = consec[c];
1058 : }
1059 : }
1060 :
1061 : // If we've found a shoreline, stop searching
1062 0 : if (count != numPoints-1)
1063 0 : return -((waterPoints[start] + consec[start]/2) % numPoints) / numPoints * 2 * Math.PI;
1064 : }
1065 0 : return undefined;
1066 : }
1067 :
1068 : /**
1069 : * Attempts to construct a building using the specified parameters.
1070 : * Returns true on success, false on failure.
1071 : */
1072 : function TryConstructBuilding(player, cmpPlayer, controlAllUnits, cmd)
1073 : {
1074 : // Message structure:
1075 : // {
1076 : // "type": "construct",
1077 : // "entities": [...], // entities that will be ordered to construct the building (if applicable)
1078 : // "template": "...", // template name of the entity being constructed
1079 : // "x": ...,
1080 : // "z": ...,
1081 : // "angle": ...,
1082 : // "metadata": "...", // AI metadata of the building
1083 : // "actorSeed": ...,
1084 : // "autorepair": true, // whether to automatically start constructing/repairing the new foundation
1085 : // "autocontinue": true, // whether to automatically gather/build/etc after finishing this
1086 : // "queued": true, // whether to add the construction/repairing of this foundation to entities' queue (if applicable)
1087 : // "obstructionControlGroup": ..., // Optional; the obstruction control group ID that should be set for this building prior to obstruction
1088 : // // testing to determine placement validity. If specified, must be a valid control group ID (> 0).
1089 : // "obstructionControlGroup2": ..., // Optional; secondary obstruction control group ID that should be set for this building prior to obstruction
1090 : // // testing to determine placement validity. May be INVALID_ENTITY.
1091 : // }
1092 :
1093 : /*
1094 : * Construction process:
1095 : * . Take resources away immediately.
1096 : * . Create a foundation entity with 1hp, 0% build progress.
1097 : * . Increase hp and build progress up to 100% when people work on it.
1098 : * . If it's destroyed, an appropriate fraction of the resource cost is refunded.
1099 : * . If it's completed, it gets replaced with the real building.
1100 : */
1101 :
1102 : // Check whether we can control these units
1103 0 : var entities = FilterEntityList(cmd.entities, player, controlAllUnits);
1104 0 : if (!entities.length)
1105 0 : return false;
1106 :
1107 0 : var foundationTemplate = "foundation|" + cmd.template;
1108 :
1109 : // Tentatively create the foundation (we might find later that it's a invalid build command)
1110 0 : var ent = Engine.AddEntity(foundationTemplate);
1111 0 : if (ent == INVALID_ENTITY)
1112 : {
1113 : // Error (e.g. invalid template names)
1114 0 : error("Error creating foundation entity for '" + cmd.template + "'");
1115 0 : return false;
1116 : }
1117 :
1118 : // If it's a dock, get the right angle.
1119 0 : var template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(cmd.template);
1120 0 : var angle = cmd.angle;
1121 0 : if (template.BuildRestrictions.PlacementType === "shore")
1122 : {
1123 0 : let angleDock = GetDockAngle(template, cmd.x, cmd.z);
1124 0 : if (angleDock !== undefined)
1125 0 : angle = angleDock;
1126 : }
1127 :
1128 : // Move the foundation to the right place
1129 0 : var cmpPosition = Engine.QueryInterface(ent, IID_Position);
1130 0 : cmpPosition.JumpTo(cmd.x, cmd.z);
1131 0 : cmpPosition.SetYRotation(angle);
1132 :
1133 : // Set the obstruction control group if needed
1134 0 : if (cmd.obstructionControlGroup || cmd.obstructionControlGroup2)
1135 : {
1136 0 : var cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction);
1137 :
1138 : // primary control group must always be valid
1139 0 : if (cmd.obstructionControlGroup)
1140 : {
1141 0 : if (cmd.obstructionControlGroup <= 0)
1142 0 : warn("[TryConstructBuilding] Invalid primary obstruction control group " + cmd.obstructionControlGroup + " received; must be > 0");
1143 :
1144 0 : cmpObstruction.SetControlGroup(cmd.obstructionControlGroup);
1145 : }
1146 :
1147 0 : if (cmd.obstructionControlGroup2)
1148 0 : cmpObstruction.SetControlGroup2(cmd.obstructionControlGroup2);
1149 : }
1150 :
1151 : // Make it owned by the current player
1152 0 : var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
1153 0 : cmpOwnership.SetOwner(player);
1154 :
1155 : // Check whether building placement is valid
1156 0 : var cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
1157 0 : if (cmpBuildRestrictions)
1158 : {
1159 0 : var ret = cmpBuildRestrictions.CheckPlacement();
1160 0 : if (!ret.success)
1161 : {
1162 0 : if (g_DebugCommands)
1163 0 : warn("Invalid command: build restrictions check failed with '"+ret.message+"' for player "+player+": "+uneval(cmd));
1164 :
1165 0 : var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
1166 0 : ret.players = [player];
1167 0 : cmpGuiInterface.PushNotification(ret);
1168 :
1169 : // Remove the foundation because the construction was aborted
1170 : // move it out of world because it's not destroyed immediately.
1171 0 : cmpPosition.MoveOutOfWorld();
1172 0 : Engine.DestroyEntity(ent);
1173 0 : return false;
1174 : }
1175 : }
1176 : else
1177 0 : error("cmpBuildRestrictions not defined");
1178 :
1179 : // Check entity limits
1180 0 : var cmpEntityLimits = QueryPlayerIDInterface(player, IID_EntityLimits);
1181 0 : if (cmpEntityLimits && !cmpEntityLimits.AllowedToBuild(cmpBuildRestrictions.GetCategory()))
1182 : {
1183 0 : if (g_DebugCommands)
1184 0 : warn("Invalid command: build limits check failed for player "+player+": "+uneval(cmd));
1185 :
1186 : // Remove the foundation because the construction was aborted
1187 0 : cmpPosition.MoveOutOfWorld();
1188 0 : Engine.DestroyEntity(ent);
1189 0 : return false;
1190 : }
1191 :
1192 0 : var cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager);
1193 0 : if (cmpTechnologyManager && !cmpTechnologyManager.CanProduce(cmd.template))
1194 : {
1195 0 : if (g_DebugCommands)
1196 0 : warn("Invalid command: required technology check failed for player "+player+": "+uneval(cmd));
1197 :
1198 0 : var cmpGuiInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface);
1199 0 : cmpGuiInterface.PushNotification({
1200 : "type": "text",
1201 : "players": [player],
1202 : "message": markForTranslation("The building's technology requirements are not met."),
1203 : "translateMessage": true
1204 : });
1205 :
1206 : // Remove the foundation because the construction was aborted
1207 0 : cmpPosition.MoveOutOfWorld();
1208 0 : Engine.DestroyEntity(ent);
1209 : }
1210 :
1211 : // We need the cost after tech and aura modifications.
1212 0 : let cmpCost = Engine.QueryInterface(ent, IID_Cost);
1213 0 : let costs = cmpCost.GetResourceCosts();
1214 :
1215 0 : if (!cmpPlayer.TrySubtractResources(costs))
1216 : {
1217 0 : if (g_DebugCommands)
1218 0 : warn("Invalid command: building cost check failed for player "+player+": "+uneval(cmd));
1219 :
1220 0 : Engine.DestroyEntity(ent);
1221 0 : cmpPosition.MoveOutOfWorld();
1222 0 : return false;
1223 : }
1224 :
1225 0 : var cmpVisual = Engine.QueryInterface(ent, IID_Visual);
1226 0 : if (cmpVisual && cmd.actorSeed !== undefined)
1227 0 : cmpVisual.SetActorSeed(cmd.actorSeed);
1228 :
1229 : // Initialise the foundation
1230 0 : var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation);
1231 0 : cmpFoundation.InitialiseConstruction(cmd.template);
1232 :
1233 : // send Metadata info if any
1234 0 : if (cmd.metadata)
1235 0 : Engine.PostMessage(ent, MT_AIMetadata, { "id": ent, "metadata" : cmd.metadata, "owner" : player } );
1236 :
1237 : // Tell the units to start building this new entity
1238 0 : if (cmd.autorepair)
1239 : {
1240 0 : ProcessCommand(player, {
1241 : "type": "repair",
1242 : "entities": entities,
1243 : "target": ent,
1244 : "autocontinue": cmd.autocontinue,
1245 : "queued": cmd.queued,
1246 : "pushFront": cmd.pushFront,
1247 : "formation": cmd.formation || undefined
1248 : });
1249 : }
1250 :
1251 0 : return ent;
1252 : }
1253 :
1254 : function TryConstructWall(player, cmpPlayer, controlAllUnits, cmd)
1255 : {
1256 : // 'cmd' message structure:
1257 : // {
1258 : // "type": "construct-wall",
1259 : // "entities": [...], // entities that will be ordered to construct the wall (if applicable)
1260 : // "pieces": [ // ordered list of information about the pieces making up the wall (towers, wall segments, ...)
1261 : // {
1262 : // "template": "...", // one of the templates from the wallset
1263 : // "x": ...,
1264 : // "z": ...,
1265 : // "angle": ...,
1266 : // },
1267 : // ...
1268 : // ],
1269 : // "wallSet": {
1270 : // "templates": {
1271 : // "tower": // tower template name
1272 : // "long": // long wall segment template name
1273 : // ... // etc.
1274 : // },
1275 : // "maxTowerOverlap": ...,
1276 : // "minTowerOverlap": ...,
1277 : // },
1278 : // "startSnappedEntity": // optional; entity ID of tower being snapped to at the starting side of the wall
1279 : // "endSnappedEntity": // optional; entity ID of tower being snapped to at the ending side of the wall
1280 : // "autorepair": true, // whether to automatically start constructing/repairing the new foundation
1281 : // "autocontinue": true, // whether to automatically gather/build/etc after finishing this
1282 : // "queued": true, // whether to add the construction/repairing of this wall's pieces to entities' queue (if applicable)
1283 : // }
1284 :
1285 0 : if (cmd.pieces.length <= 0)
1286 0 : return;
1287 :
1288 0 : if (cmd.startSnappedEntity && cmd.pieces[0].template == cmd.wallSet.templates.tower)
1289 : {
1290 0 : error("[TryConstructWall] Starting wall piece cannot be a tower (" + cmd.wallSet.templates.tower + ") when snapping at the starting side");
1291 0 : return;
1292 : }
1293 :
1294 0 : if (cmd.endSnappedEntity && cmd.pieces[cmd.pieces.length - 1].template == cmd.wallSet.templates.tower)
1295 : {
1296 0 : error("[TryConstructWall] Ending wall piece cannot be a tower (" + cmd.wallSet.templates.tower + ") when snapping at the ending side");
1297 0 : return;
1298 : }
1299 :
1300 : // Assign obstruction control groups to allow the wall pieces to mutually overlap during foundation placement
1301 : // and during construction. The scheme here is that whatever wall pieces are inbetween two towers inherit the control
1302 : // groups of both of the towers they are connected to (either newly constructed ones as part of the wall, or existing
1303 : // towers in the case of snapping). The towers themselves all keep their default unique control groups.
1304 :
1305 : // To support this, every non-tower piece registers the entity ID of the towers (or foundations thereof) that neighbour
1306 : // it on either side. Specifically, each non-tower wall piece has its primary control group set equal to that of the
1307 : // first tower encountered towards the starting side of the wall, and its secondary control group set equal to that of
1308 : // the first tower encountered towards the ending side of the wall (if any).
1309 :
1310 : // We can't build the whole wall at once by linearly stepping through the wall pieces and build them, because the
1311 : // wall segments may/will need the entity IDs of towers that come afterwards. So, build it in two passes:
1312 : //
1313 : // FIRST PASS:
1314 : // - Go from start to end and construct wall piece foundations as far as we can without running into a piece that
1315 : // cannot be built (e.g. because it is obstructed). At each non-tower, set the most recently built tower's ID
1316 : // as the primary control group, thus allowing it to be built overlapping the previous piece.
1317 : // - If we encounter a new tower along the way (which will gain its own control group), do the following:
1318 : // o First build it using temporarily the same control group of the previous (non-tower) piece
1319 : // o Set the previous piece's secondary control group to the tower's entity ID
1320 : // o Restore the primary control group of the constructed tower back its original (unique) value.
1321 : // The temporary control group is necessary to allow the newer tower with its unique control group ID to be able
1322 : // to be placed while overlapping the previous piece.
1323 : //
1324 : // SECOND PASS:
1325 : // - Go end to start from the last successfully placed wall piece (which might be a tower we backtracked to), this
1326 : // time registering the right neighbouring tower in each non-tower piece.
1327 :
1328 : // first pass; L -> R
1329 :
1330 0 : var lastTowerIndex = -1; // index of the last tower we've encountered in cmd.pieces
1331 0 : var lastTowerControlGroup = null; // control group of the last tower we've encountered, to assign to non-tower pieces
1332 :
1333 : // If we're snapping to an existing entity at the starting end, set lastTowerControlGroup to its control group ID so that
1334 : // the first wall piece can be built while overlapping it.
1335 0 : if (cmd.startSnappedEntity)
1336 : {
1337 0 : var cmpSnappedStartObstruction = Engine.QueryInterface(cmd.startSnappedEntity, IID_Obstruction);
1338 0 : if (!cmpSnappedStartObstruction)
1339 : {
1340 0 : error("[TryConstructWall] Snapped entity on starting side does not have an obstruction component");
1341 0 : return;
1342 : }
1343 :
1344 0 : lastTowerControlGroup = cmpSnappedStartObstruction.GetControlGroup();
1345 : //warn("setting lastTowerControlGroup to control group of start snapped entity " + cmd.startSnappedEntity + ": " + lastTowerControlGroup);
1346 : }
1347 :
1348 0 : var i = 0;
1349 0 : var queued = cmd.queued;
1350 0 : var pieces = clone(cmd.pieces);
1351 0 : for (; i < pieces.length; ++i)
1352 : {
1353 0 : var piece = pieces[i];
1354 :
1355 : // All wall pieces after the first must be queued.
1356 0 : if (i > 0 && !queued)
1357 0 : queued = true;
1358 :
1359 : // 'lastTowerControlGroup' must always be defined and valid here, except if we're at the first piece and we didn't do
1360 : // start position snapping (implying that the first entity we build must be a tower)
1361 0 : if (lastTowerControlGroup === null || lastTowerControlGroup == INVALID_ENTITY)
1362 : {
1363 0 : if (!(i == 0 && piece.template == cmd.wallSet.templates.tower && !cmd.startSnappedEntity))
1364 : {
1365 0 : error("[TryConstructWall] Expected last tower control group to be available, none found (1st pass, iteration " + i + ")");
1366 0 : break;
1367 : }
1368 : }
1369 :
1370 0 : var constructPieceCmd = {
1371 : "type": "construct",
1372 : "entities": cmd.entities,
1373 : "template": piece.template,
1374 : "x": piece.x,
1375 : "z": piece.z,
1376 : "angle": piece.angle,
1377 : "autorepair": cmd.autorepair,
1378 : "autocontinue": cmd.autocontinue,
1379 : "queued": queued,
1380 : // Regardless of whether we're building a tower or an intermediate wall piece, it is always (first) constructed
1381 : // using the control group of the last tower (see comments above).
1382 : "obstructionControlGroup": lastTowerControlGroup,
1383 : };
1384 :
1385 : // If we're building the last piece and we're attaching to a snapped entity, we need to add in the snapped entity's
1386 : // control group directly at construction time (instead of setting it in the second pass) to allow it to be built
1387 : // while overlapping the snapped entity.
1388 0 : if (i == pieces.length - 1 && cmd.endSnappedEntity)
1389 : {
1390 0 : var cmpEndSnappedObstruction = Engine.QueryInterface(cmd.endSnappedEntity, IID_Obstruction);
1391 0 : if (cmpEndSnappedObstruction)
1392 0 : constructPieceCmd.obstructionControlGroup2 = cmpEndSnappedObstruction.GetControlGroup();
1393 : }
1394 :
1395 0 : var pieceEntityId = TryConstructBuilding(player, cmpPlayer, controlAllUnits, constructPieceCmd);
1396 0 : if (pieceEntityId)
1397 : {
1398 : // wall piece foundation successfully built, save the entity ID in the piece info object so we can reference it later
1399 0 : piece.ent = pieceEntityId;
1400 :
1401 : // if we built a tower, do the control group dance (see outline above) and update lastTowerControlGroup and lastTowerIndex
1402 0 : if (piece.template == cmd.wallSet.templates.tower)
1403 : {
1404 0 : var cmpTowerObstruction = Engine.QueryInterface(pieceEntityId, IID_Obstruction);
1405 0 : var newTowerControlGroup = pieceEntityId;
1406 :
1407 0 : if (i > 0)
1408 : {
1409 : //warn(" updating previous wall piece's secondary control group to " + newTowerControlGroup);
1410 0 : var cmpPreviousObstruction = Engine.QueryInterface(pieces[i-1].ent, IID_Obstruction);
1411 : // TODO: ensure that cmpPreviousObstruction exists
1412 : // TODO: ensure that the previous obstruction does not yet have a secondary control group set
1413 0 : cmpPreviousObstruction.SetControlGroup2(newTowerControlGroup);
1414 : }
1415 :
1416 : // TODO: ensure that cmpTowerObstruction exists
1417 0 : cmpTowerObstruction.SetControlGroup(newTowerControlGroup); // give the tower its own unique control group
1418 :
1419 0 : lastTowerIndex = i;
1420 0 : lastTowerControlGroup = newTowerControlGroup;
1421 : }
1422 : }
1423 : else // failed to build wall piece, abort
1424 0 : break;
1425 : }
1426 :
1427 0 : var lastBuiltPieceIndex = i - 1;
1428 0 : var wallComplete = (lastBuiltPieceIndex == pieces.length - 1);
1429 :
1430 : // At this point, 'i' is the index of the last wall piece that was successfully constructed (which may or may not be a tower).
1431 : // Now do the second pass going right-to-left, registering the control groups of the towers to the right of each piece (if any)
1432 : // as their secondary control groups.
1433 :
1434 0 : lastTowerControlGroup = null; // control group of the last tower we've encountered, to assign to non-tower pieces
1435 :
1436 : // only start off with the ending side's snapped tower's control group if we were able to build the entire wall
1437 0 : if (cmd.endSnappedEntity && wallComplete)
1438 : {
1439 0 : var cmpSnappedEndObstruction = Engine.QueryInterface(cmd.endSnappedEntity, IID_Obstruction);
1440 0 : if (!cmpSnappedEndObstruction)
1441 : {
1442 0 : error("[TryConstructWall] Snapped entity on ending side does not have an obstruction component");
1443 0 : return;
1444 : }
1445 :
1446 0 : lastTowerControlGroup = cmpSnappedEndObstruction.GetControlGroup();
1447 : }
1448 :
1449 0 : for (var j = lastBuiltPieceIndex; j >= 0; --j)
1450 : {
1451 0 : var piece = pieces[j];
1452 :
1453 0 : if (!piece.ent)
1454 : {
1455 0 : error("[TryConstructWall] No entity ID set for constructed entity of template '" + piece.template + "'");
1456 0 : continue;
1457 : }
1458 :
1459 0 : var cmpPieceObstruction = Engine.QueryInterface(piece.ent, IID_Obstruction);
1460 0 : if (!cmpPieceObstruction)
1461 : {
1462 0 : error("[TryConstructWall] Wall piece of template '" + piece.template + "' has no Obstruction component");
1463 0 : continue;
1464 : }
1465 :
1466 0 : if (piece.template == cmd.wallSet.templates.tower)
1467 : {
1468 : // encountered a tower entity, update the last tower control group
1469 0 : lastTowerControlGroup = cmpPieceObstruction.GetControlGroup();
1470 : }
1471 : else
1472 : {
1473 : // Encountered a non-tower entity, update its secondary control group to 'lastTowerControlGroup'.
1474 : // Note that the wall piece may already have its secondary control group set to the tower's entity ID from a control group
1475 : // dance during the first pass, in which case we should validate it against 'lastTowerControlGroup'.
1476 :
1477 0 : var existingSecondaryControlGroup = cmpPieceObstruction.GetControlGroup2();
1478 0 : if (existingSecondaryControlGroup == INVALID_ENTITY)
1479 : {
1480 0 : if (lastTowerControlGroup != null && lastTowerControlGroup != INVALID_ENTITY)
1481 : {
1482 0 : cmpPieceObstruction.SetControlGroup2(lastTowerControlGroup);
1483 : }
1484 : }
1485 0 : else if (existingSecondaryControlGroup != lastTowerControlGroup)
1486 : {
1487 0 : error("[TryConstructWall] Existing secondary control group of non-tower entity does not match expected value (2nd pass, iteration " + j + ")");
1488 0 : break;
1489 : }
1490 : }
1491 : }
1492 : }
1493 :
1494 : /**
1495 : * Remove the given list of entities from their current formations.
1496 : */
1497 : function RemoveFromFormation(ents)
1498 : {
1499 0 : let formation = ExtractFormations(ents);
1500 0 : for (let fid in formation.members)
1501 : {
1502 0 : let cmpFormation = Engine.QueryInterface(+fid, IID_Formation);
1503 0 : if (cmpFormation)
1504 0 : cmpFormation.RemoveMembers(formation.members[fid]);
1505 : }
1506 : }
1507 :
1508 : /**
1509 : * Returns a list of UnitAI components, each belonging either to a
1510 : * selected unit or to a formation entity for groups of the selected units.
1511 : */
1512 : function GetFormationUnitAIs(ents, player, cmd, formationTemplate, forceTemplate)
1513 : {
1514 : // If an individual was selected, remove it from any formation
1515 : // and command it individually.
1516 0 : if (ents.length == 1)
1517 : {
1518 0 : let cmpUnitAI = Engine.QueryInterface(ents[0], IID_UnitAI);
1519 0 : if (!cmpUnitAI)
1520 0 : return [];
1521 :
1522 0 : RemoveFromFormation(ents);
1523 :
1524 0 : return [ cmpUnitAI ];
1525 : }
1526 :
1527 0 : let formationUnitAIs = [];
1528 : // Find what formations the selected entities are currently in,
1529 : // and default to that unless the formation is forced or it's the null formation
1530 : // (we want that to reset whatever formations units are in).
1531 0 : if (formationTemplate != NULL_FORMATION)
1532 : {
1533 0 : let formation = ExtractFormations(ents);
1534 0 : let formationIds = Object.keys(formation.members);
1535 0 : if (formationIds.length == 1)
1536 : {
1537 : // Selected units either belong to this formation or have no formation.
1538 0 : let fid = formationIds[0];
1539 0 : let cmpFormation = Engine.QueryInterface(+fid, IID_Formation);
1540 0 : if (cmpFormation && cmpFormation.GetMemberCount() == formation.members[fid].length &&
1541 : cmpFormation.GetMemberCount() == formation.entities.length)
1542 : {
1543 0 : cmpFormation.DeleteTwinFormations();
1544 :
1545 : // The whole formation was selected, so reuse its controller for this command.
1546 0 : if (!forceTemplate || formationTemplate == formation.templates[fid])
1547 : {
1548 0 : formationTemplate = formation.templates[fid];
1549 0 : formationUnitAIs = [Engine.QueryInterface(+fid, IID_UnitAI)];
1550 : }
1551 0 : else if (formationTemplate && CanMoveEntsIntoFormation(formation.entities, formationTemplate))
1552 0 : formationUnitAIs = [cmpFormation.LoadFormation(formationTemplate)];
1553 : }
1554 0 : else if (cmpFormation && !forceTemplate)
1555 : {
1556 : // Just reuse the template.
1557 0 : formationTemplate = formation.templates[fid];
1558 : }
1559 : }
1560 0 : else if (formationIds.length)
1561 : {
1562 : // Check if all entities share a common formation, if so reuse this template.
1563 0 : let template = formation.templates[formationIds[0]];
1564 0 : for (let i = 1; i < formationIds.length; ++i)
1565 0 : if (formation.templates[formationIds[i]] != template)
1566 : {
1567 0 : template = null;
1568 0 : break;
1569 : }
1570 0 : if (template && !forceTemplate)
1571 0 : formationTemplate = template;
1572 : }
1573 : }
1574 :
1575 : // Separate out the units that don't support the chosen formation.
1576 0 : let formedUnits = [];
1577 0 : let nonformedUnitAIs = [];
1578 0 : for (let ent of ents)
1579 : {
1580 0 : let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
1581 0 : let cmpPosition = Engine.QueryInterface(ent, IID_Position);
1582 0 : if (!cmpUnitAI || !cmpPosition || !cmpPosition.IsInWorld())
1583 0 : continue;
1584 :
1585 : // TODO: We only check if the formation is usable by some units
1586 : // if we move them to it. We should check if we can use formations
1587 : // for the other cases.
1588 0 : let nullFormation = (formationTemplate || cmpUnitAI.GetFormationTemplate()) == NULL_FORMATION;
1589 0 : if (nullFormation || !cmpUnitAI.CanUseFormation(formationTemplate || NULL_FORMATION))
1590 : {
1591 0 : if (nullFormation && cmpUnitAI.GetFormationController())
1592 0 : cmpUnitAI.LeaveFormation(cmd.queued || false);
1593 0 : nonformedUnitAIs.push(cmpUnitAI);
1594 : }
1595 : else
1596 0 : formedUnits.push(ent);
1597 : }
1598 0 : if (nonformedUnitAIs.length == ents.length)
1599 : {
1600 : // No units support the formation.
1601 0 : return nonformedUnitAIs;
1602 : }
1603 :
1604 0 : if (!formationUnitAIs.length)
1605 : {
1606 : // We need to give the selected units a new formation controller.
1607 :
1608 : // TODO replace the fixed 60 with something sensible, based on vision range f.e.
1609 0 : let formationSeparation = 60;
1610 0 : let clusters = ClusterEntities(formedUnits, formationSeparation);
1611 0 : let formationEnts = [];
1612 0 : for (let cluster of clusters)
1613 : {
1614 0 : RemoveFromFormation(cluster);
1615 :
1616 0 : if (!formationTemplate || !CanMoveEntsIntoFormation(cluster, formationTemplate))
1617 : {
1618 0 : for (let ent of cluster)
1619 0 : nonformedUnitAIs.push(Engine.QueryInterface(ent, IID_UnitAI));
1620 :
1621 0 : continue;
1622 : }
1623 :
1624 : // Create the new controller.
1625 0 : let formationEnt = Engine.AddEntity(formationTemplate);
1626 0 : let cmpFormation = Engine.QueryInterface(formationEnt, IID_Formation);
1627 0 : formationUnitAIs.push(Engine.QueryInterface(formationEnt, IID_UnitAI));
1628 0 : cmpFormation.SetFormationSeparation(formationSeparation);
1629 0 : cmpFormation.SetMembers(cluster);
1630 :
1631 0 : for (let ent of formationEnts)
1632 0 : cmpFormation.RegisterTwinFormation(ent);
1633 :
1634 0 : formationEnts.push(formationEnt);
1635 0 : let cmpOwnership = Engine.QueryInterface(formationEnt, IID_Ownership);
1636 0 : cmpOwnership.SetOwner(player);
1637 : }
1638 : }
1639 :
1640 0 : return nonformedUnitAIs.concat(formationUnitAIs);
1641 : }
1642 :
1643 : /**
1644 : * Group a list of entities in clusters via single-links
1645 : */
1646 : function ClusterEntities(ents, separationDistance)
1647 : {
1648 0 : let clusters = [];
1649 0 : if (!ents.length)
1650 0 : return clusters;
1651 :
1652 0 : let distSq = separationDistance * separationDistance;
1653 0 : let positions = [];
1654 : // triangular matrix with the (squared) distances between the different clusters
1655 : // the other half is not initialised
1656 0 : let matrix = [];
1657 0 : for (let i = 0; i < ents.length; ++i)
1658 : {
1659 0 : matrix[i] = [];
1660 0 : clusters.push([ents[i]]);
1661 0 : let cmpPosition = Engine.QueryInterface(ents[i], IID_Position);
1662 0 : positions.push(cmpPosition.GetPosition2D());
1663 0 : for (let j = 0; j < i; ++j)
1664 0 : matrix[i][j] = positions[i].distanceToSquared(positions[j]);
1665 : }
1666 0 : while (clusters.length > 1)
1667 : {
1668 : // search two clusters that are closer than the required distance
1669 0 : let closeClusters = undefined;
1670 :
1671 0 : for (let i = matrix.length - 1; i >= 0 && !closeClusters; --i)
1672 0 : for (let j = i - 1; j >= 0 && !closeClusters; --j)
1673 0 : if (matrix[i][j] < distSq)
1674 0 : closeClusters = [i,j];
1675 :
1676 : // if no more close clusters found, just return all found clusters so far
1677 0 : if (!closeClusters)
1678 0 : return clusters;
1679 :
1680 : // make a new cluster with the entities from the two found clusters
1681 0 : let newCluster = clusters[closeClusters[0]].concat(clusters[closeClusters[1]]);
1682 :
1683 : // calculate the minimum distance between the new cluster and all other remaining
1684 : // clusters by taking the minimum of the two distances.
1685 0 : let distances = [];
1686 0 : for (let i = 0; i < clusters.length; ++i)
1687 : {
1688 0 : let a = closeClusters[1];
1689 0 : let b = closeClusters[0];
1690 0 : if (i == a || i == b)
1691 0 : continue;
1692 0 : let dist1 = matrix[a][i] !== undefined ? matrix[a][i] : matrix[i][a];
1693 0 : let dist2 = matrix[b][i] !== undefined ? matrix[b][i] : matrix[i][b];
1694 0 : distances.push(Math.min(dist1, dist2));
1695 : }
1696 : // remove the rows and columns in the matrix for the merged clusters,
1697 : // and the clusters themselves from the cluster list
1698 0 : clusters.splice(closeClusters[0],1);
1699 0 : clusters.splice(closeClusters[1],1);
1700 0 : matrix.splice(closeClusters[0],1);
1701 0 : matrix.splice(closeClusters[1],1);
1702 0 : for (let i = 0; i < matrix.length; ++i)
1703 : {
1704 0 : if (matrix[i].length > closeClusters[0])
1705 0 : matrix[i].splice(closeClusters[0],1);
1706 0 : if (matrix[i].length > closeClusters[1])
1707 0 : matrix[i].splice(closeClusters[1],1);
1708 : }
1709 : // add a new row of distances to the matrix and the new cluster
1710 0 : clusters.push(newCluster);
1711 0 : matrix.push(distances);
1712 : }
1713 0 : return clusters;
1714 : }
1715 :
1716 : function GetFormationRequirements(formationTemplate)
1717 : {
1718 0 : var template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(formationTemplate);
1719 0 : if (!template.Formation)
1720 0 : return false;
1721 :
1722 0 : return { "minCount": +template.Formation.RequiredMemberCount };
1723 : }
1724 :
1725 :
1726 : function CanMoveEntsIntoFormation(ents, formationTemplate)
1727 : {
1728 : // TODO: should check the player's civ is allowed to use this formation
1729 : // See simulation/components/Player.js GetFormations() for a list of all allowed formations
1730 :
1731 0 : const requirements = GetFormationRequirements(formationTemplate);
1732 0 : if (!requirements)
1733 0 : return false;
1734 :
1735 0 : let count = 0;
1736 0 : for (const ent of ents)
1737 0 : if (Engine.QueryInterface(ent, IID_UnitAI)?.CanUseFormation(formationTemplate))
1738 0 : ++count;
1739 :
1740 0 : return count >= requirements.minCount;
1741 : }
1742 :
1743 : /**
1744 : * Check if player can control this entity
1745 : * returns: true if the entity is owned by the player and controllable
1746 : * or control all units is activated, else false
1747 : */
1748 : function CanControlUnit(entity, player, controlAll)
1749 : {
1750 0 : let cmpIdentity = Engine.QueryInterface(entity, IID_Identity);
1751 0 : let canBeControlled = IsOwnedByPlayer(player, entity) &&
1752 : (!cmpIdentity || cmpIdentity.IsControllable()) ||
1753 : controlAll;
1754 :
1755 0 : if (!canBeControlled)
1756 0 : notifyOrderFailure(entity, player);
1757 :
1758 0 : return canBeControlled;
1759 : }
1760 :
1761 : /**
1762 : * @param {number} entity - The entityID to verify.
1763 : * @param {number} player - The playerID to check against.
1764 : * @return {boolean}.
1765 : */
1766 : function IsOwnedByPlayerOrMutualAlly(entity, player)
1767 : {
1768 0 : return IsOwnedByPlayer(player, entity) || IsOwnedByMutualAllyOfPlayer(player, entity);
1769 : }
1770 :
1771 : /**
1772 : * Check if player can control this entity
1773 : * @return {boolean} - True if the entity is valid and controlled by the player
1774 : * or the entity is owned by an mutualAlly and can be controlled
1775 : * or control all units is activated, else false.
1776 : */
1777 : function CanPlayerOrAllyControlUnit(entity, player, controlAll)
1778 : {
1779 0 : return CanControlUnit(player, entity, controlAll) ||
1780 : IsOwnedByMutualAllyOfPlayer(player, entity) && CanOwnerControlEntity(entity);
1781 : }
1782 :
1783 : /**
1784 : * @return {boolean} - Whether the owner of this entity can control the entity.
1785 : */
1786 : function CanOwnerControlEntity(entity)
1787 : {
1788 0 : let cmpOwner = QueryOwnerInterface(entity);
1789 0 : return cmpOwner && CanControlUnit(entity, cmpOwner.GetPlayerID());
1790 : }
1791 :
1792 : /**
1793 : * Filter entities which the player can control.
1794 : */
1795 : function FilterEntityList(entities, player, controlAll)
1796 : {
1797 0 : return entities.filter(ent => CanControlUnit(ent, player, controlAll));
1798 : }
1799 :
1800 : /**
1801 : * Filter entities which the player can control or are mutualAlly
1802 : */
1803 : function FilterEntityListWithAllies(entities, player, controlAll)
1804 : {
1805 0 : return entities.filter(ent => CanPlayerOrAllyControlUnit(ent, player, controlAll));
1806 : }
1807 :
1808 : /**
1809 : * Incur the player with the cost of a bribe, optionally multiply the cost with
1810 : * the additionalMultiplier
1811 : */
1812 : function IncurBribeCost(template, player, playerBribed, failedBribe)
1813 : {
1814 3 : let cmpPlayerBribed = QueryPlayerIDInterface(playerBribed);
1815 3 : if (!cmpPlayerBribed)
1816 0 : return false;
1817 :
1818 3 : let costs = {};
1819 : // Additional cost for this owner
1820 3 : let multiplier = cmpPlayerBribed.GetSpyCostMultiplier();
1821 3 : if (failedBribe)
1822 0 : multiplier *= template.VisionSharing.FailureCostRatio;
1823 :
1824 3 : for (let res in template.Cost.Resources)
1825 3 : costs[res] = Math.floor(multiplier * ApplyValueModificationsToTemplate("Cost/Resources/" + res, +template.Cost.Resources[res], player, template));
1826 :
1827 3 : let cmpPlayer = QueryPlayerIDInterface(player);
1828 3 : return cmpPlayer && cmpPlayer.TrySubtractResources(costs);
1829 : }
1830 :
1831 1 : Engine.RegisterGlobal("GetFormationRequirements", GetFormationRequirements);
1832 1 : Engine.RegisterGlobal("CanMoveEntsIntoFormation", CanMoveEntsIntoFormation);
1833 1 : Engine.RegisterGlobal("GetDockAngle", GetDockAngle);
1834 1 : Engine.RegisterGlobal("ProcessCommand", ProcessCommand);
1835 1 : Engine.RegisterGlobal("g_Commands", g_Commands);
1836 1 : Engine.RegisterGlobal("IncurBribeCost", IncurBribeCost);
|