1 | | function GuiInterface() {} |
2 | | |
3 | | GuiInterface.prototype.Schema = |
4 | | "<a:component type='system'/><empty/>"; |
5 | | |
6 | | GuiInterface.prototype.Serialize = function() |
7 | | { |
8 | | // This component isn't network-synchronised so we mustn't serialise |
9 | | // its non-deterministic data. Instead just return an empty object. |
10 | | return {}; |
11 | | }; |
12 | | |
13 | | GuiInterface.prototype.Deserialize = function(obj) |
14 | | { |
15 | | this.Init(); |
16 | | }; |
17 | | |
18 | | GuiInterface.prototype.Init = function() |
19 | | { |
20 | | this.placementEntity = undefined; // = undefined or [templateName, entityID] |
21 | | this.rallyPoints = undefined; |
22 | | this.notifications = []; |
23 | | this.renamedEntities = []; |
24 | | }; |
25 | | |
26 | | /* |
27 | | * All of the functions defined below are called via Engine.GuiInterfaceCall(name, arg) |
28 | | * from GUI scripts, and executed here with arguments (player, arg). |
29 | | * |
30 | | * CAUTION: The input to the functions in this module is not network-synchronised, so it |
31 | | * mustn't affect the simulation state (i.e. the data that is serialised and can affect |
32 | | * the behaviour of the rest of the simulation) else it'll cause out-of-sync errors. |
33 | | */ |
34 | | |
35 | | /** |
36 | | * Returns global information about the current game state. |
37 | | * This is used by the GUI and also by AI scripts. |
38 | | */ |
39 | | GuiInterface.prototype.GetSimulationState = function(player) |
40 | | { |
41 | | var ret = { |
42 | | "players": [] |
43 | | }; |
44 | | |
45 | | var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); |
46 | | var n = cmpPlayerMan.GetNumPlayers(); |
47 | | for (var i = 0; i < n; ++i) |
48 | | { |
49 | | var playerEnt = cmpPlayerMan.GetPlayerByID(i); |
50 | | var cmpPlayerBuildLimits = Engine.QueryInterface(playerEnt, IID_BuildLimits); |
51 | | var cmpPlayer = Engine.QueryInterface(playerEnt, IID_Player); |
52 | | |
53 | | // store player ally/enemy data as arrays |
54 | | var allies = []; |
55 | | var enemies = []; |
56 | | for (var j = 0; j <= n; ++j) |
57 | | { |
58 | | allies[j] = cmpPlayer.IsAlly(j); |
59 | | enemies[j] = cmpPlayer.IsEnemy(j); |
60 | | } |
61 | | var playerData = { |
62 | | "name": cmpPlayer.GetName(), |
63 | | "civ": cmpPlayer.GetCiv(), |
64 | | "colour": cmpPlayer.GetColour(), |
65 | | "popCount": cmpPlayer.GetPopulationCount(), |
66 | | "popLimit": cmpPlayer.GetPopulationLimit(), |
67 | | "popMax": cmpPlayer.GetMaxPopulation(), |
68 | | "resourceCounts": cmpPlayer.GetResourceCounts(), |
69 | | "trainingQueueBlocked": cmpPlayer.IsTrainingQueueBlocked(), |
70 | | "state": cmpPlayer.GetState(), |
71 | | "team": cmpPlayer.GetTeam(), |
72 | | "phase": cmpPlayer.GetPhase(), |
73 | | "isAlly": allies, |
74 | | "isEnemy": enemies, |
75 | | "buildLimits": cmpPlayerBuildLimits.GetLimits(), |
76 | | "buildCounts": cmpPlayerBuildLimits.GetCounts() |
77 | | }; |
78 | | ret.players.push(playerData); |
79 | | } |
80 | | |
81 | | var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); |
82 | | if (cmpRangeManager) |
83 | | { |
84 | | ret.circularMap = cmpRangeManager.GetLosCircular(); |
85 | | } |
86 | | |
87 | | // Add timeElapsed |
88 | | var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); |
89 | | ret.timeElapsed = cmpTimer.GetTime(); |
90 | | |
91 | | return ret; |
92 | | }; |
93 | | |
94 | | GuiInterface.prototype.GetExtendedSimulationState = function(player) |
95 | | { |
96 | | // Get basic simulation info |
97 | | var ret = this.GetSimulationState(); |
98 | | |
99 | | // Add statistics to each player |
100 | | var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); |
101 | | var n = cmpPlayerMan.GetNumPlayers(); |
102 | | for (var i = 0; i < n; ++i) |
103 | | { |
104 | | var playerEnt = cmpPlayerMan.GetPlayerByID(i); |
105 | | var cmpPlayerStatisticsTracker = Engine.QueryInterface(playerEnt, IID_StatisticsTracker); |
106 | | ret.players[i].statistics = cmpPlayerStatisticsTracker.GetStatistics(); |
107 | | } |
108 | | |
109 | | return ret; |
110 | | }; |
111 | | |
112 | | GuiInterface.prototype.GetRenamedEntities = function(player) |
113 | | { |
114 | | return this.renamedEntities; |
115 | | }; |
116 | | |
117 | | GuiInterface.prototype.ClearRenamedEntities = function(player) |
118 | | { |
119 | | this.renamedEntities = []; |
120 | | }; |
121 | | |
122 | | GuiInterface.prototype.GetEntityState = function(player, ent) |
123 | | { |
124 | | var cmpTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); |
125 | | |
126 | | // All units must have a template; if not then it's a nonexistent entity id |
127 | | var template = cmpTempMan.GetCurrentTemplateName(ent); |
128 | | if (!template) |
129 | | return null; |
130 | | |
131 | | var ret = { |
132 | | "id": ent, |
133 | | "template": template |
134 | | } |
135 | | |
136 | | var cmpIdentity = Engine.QueryInterface(ent, IID_Identity); |
137 | | if (cmpIdentity) |
138 | | { |
139 | | ret.identity = { |
140 | | "rank": cmpIdentity.GetRank(), |
141 | | "classes": cmpIdentity.GetClassesList(), |
142 | | "selectionGroupName": cmpIdentity.GetSelectionGroupName() |
143 | | }; |
144 | | } |
145 | | |
146 | | var cmpPosition = Engine.QueryInterface(ent, IID_Position); |
147 | | if (cmpPosition && cmpPosition.IsInWorld()) |
148 | | { |
149 | | ret.position = cmpPosition.GetPosition(); |
150 | | } |
151 | | |
152 | | var cmpHealth = Engine.QueryInterface(ent, IID_Health); |
153 | | if (cmpHealth) |
154 | | { |
155 | | ret.hitpoints = cmpHealth.GetHitpoints(); |
156 | | ret.maxHitpoints = cmpHealth.GetMaxHitpoints(); |
157 | | ret.needsRepair = cmpHealth.IsRepairable() && (cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints()); |
158 | | } |
159 | | |
160 | | var cmpAttack = Engine.QueryInterface(ent, IID_Attack); |
161 | | if (cmpAttack) |
162 | | { |
163 | | var type = cmpAttack.GetBestAttack(); // TODO: how should we decide which attack to show? |
164 | | ret.attack = cmpAttack.GetAttackStrengths(type); |
165 | | } |
166 | | |
167 | | var cmpArmour = Engine.QueryInterface(ent, IID_DamageReceiver); |
168 | | if (cmpArmour) |
169 | | { |
170 | | ret.armour = cmpArmour.GetArmourStrengths(); |
171 | | } |
172 | | |
173 | | var cmpBuilder = Engine.QueryInterface(ent, IID_Builder); |
174 | | if (cmpBuilder) |
175 | | { |
176 | | ret.buildEntities = cmpBuilder.GetEntitiesList(); |
177 | | } |
178 | | |
179 | | var cmpTrainingQueue = Engine.QueryInterface(ent, IID_TrainingQueue); |
180 | | if (cmpTrainingQueue) |
181 | | { |
182 | | ret.training = { |
183 | | "entities": cmpTrainingQueue.GetEntitiesList(), |
184 | | "queue": cmpTrainingQueue.GetQueue(), |
185 | | }; |
186 | | } |
187 | | |
188 | | var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation); |
189 | | if (cmpFoundation) |
190 | | { |
191 | | ret.foundation = { |
192 | | "progress": cmpFoundation.GetBuildPercentage() |
193 | | }; |
194 | | } |
195 | | |
196 | | var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); |
197 | | if (cmpOwnership) |
198 | | { |
199 | | ret.player = cmpOwnership.GetOwner(); |
200 | | } |
201 | | |
202 | | var cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply); |
203 | | if (cmpResourceSupply) |
204 | | { |
205 | | ret.resourceSupply = { |
206 | | "max": cmpResourceSupply.GetMaxAmount(), |
207 | | "amount": cmpResourceSupply.GetCurrentAmount(), |
208 | | "type": cmpResourceSupply.GetType() |
209 | | }; |
210 | | } |
211 | | |
212 | | var cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer); |
213 | | if (cmpResourceGatherer) |
214 | | { |
215 | | ret.resourceGatherRates = cmpResourceGatherer.GetGatherRates(); |
216 | | ret.resourceCarrying = cmpResourceGatherer.GetCarryingStatus(); |
217 | | } |
218 | | |
219 | | var cmpResourceDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite); |
220 | | if (cmpResourceDropsite) |
221 | | { |
222 | | ret.resourceDropsite = { |
223 | | "types": cmpResourceDropsite.GetTypes() |
224 | | }; |
225 | | } |
226 | | |
227 | | var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint); |
228 | | if (cmpRallyPoint) |
229 | | { |
230 | | ret.rallyPoint = {'position': cmpRallyPoint.GetPosition()}; // undefined or {x,z} object |
231 | | } |
232 | | |
233 | | var cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder); |
234 | | if (cmpGarrisonHolder) |
235 | | { |
236 | | ret.garrisonHolder = { |
237 | | "entities": cmpGarrisonHolder.GetEntities(), |
238 | | "allowedClasses": cmpGarrisonHolder.GetAllowedClassesList() |
239 | | }; |
240 | | } |
241 | | |
242 | | var cmpPromotion = Engine.QueryInterface(ent, IID_Promotion); |
243 | | if (cmpPromotion) |
244 | | { |
245 | | ret.promotion = { |
246 | | "curr": cmpPromotion.GetCurrentXp(), |
247 | | "req": cmpPromotion.GetRequiredXp() |
248 | | }; |
249 | | } |
250 | | |
251 | | var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); |
252 | | if (cmpUnitAI) |
253 | | { |
254 | | ret.unitAI = { |
255 | | // TODO: reading properties directly is kind of violating abstraction |
256 | | "state": cmpUnitAI.fsmStateName, |
257 | | "orders": cmpUnitAI.orderQueue, |
258 | | }; |
259 | | } |
260 | | |
261 | | if (!cmpFoundation && cmpIdentity && cmpIdentity.HasClass("BarterMarket")) |
262 | | { |
263 | | var cmpBarter = Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter); |
264 | | ret.barterMarket = { "prices": cmpBarter.GetPrices() }; |
265 | | } |
266 | | |
267 | | var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); |
268 | | ret.visibility = cmpRangeManager.GetLosVisibility(ent, player, false); |
269 | | |
270 | | return ret; |
271 | | }; |
272 | | |
273 | | GuiInterface.prototype.GetTemplateData = function(player, name) |
274 | | { |
275 | | var cmpTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); |
276 | | var template = cmpTempMan.GetTemplate(name); |
277 | | |
278 | | if (!template) |
279 | | return null; |
280 | | |
281 | | var ret = {}; |
282 | | |
283 | | if (template.Armour) |
284 | | { |
285 | | ret.armour = { |
286 | | "hack": +template.Armour.Hack, |
287 | | "pierce": +template.Armour.Pierce, |
288 | | "crush": +template.Armour.Crush, |
289 | | }; |
290 | | } |
291 | | |
292 | | if (template.Attack) |
293 | | { |
294 | | ret.attack = {}; |
295 | | for (var type in template.Attack) |
296 | | { |
297 | | ret.attack[type] = { |
298 | | "hack": (+template.Attack[type].Hack || 0), |
299 | | "pierce": (+template.Attack[type].Pierce || 0), |
300 | | "crush": (+template.Attack[type].Crush || 0), |
301 | | }; |
302 | | } |
303 | | } |
304 | | |
305 | | if (template.Cost) |
306 | | { |
307 | | ret.cost = {}; |
308 | | if (template.Cost.Resources.food) ret.cost.food = +template.Cost.Resources.food; |
309 | | if (template.Cost.Resources.wood) ret.cost.wood = +template.Cost.Resources.wood; |
310 | | if (template.Cost.Resources.stone) ret.cost.stone = +template.Cost.Resources.stone; |
311 | | if (template.Cost.Resources.metal) ret.cost.metal = +template.Cost.Resources.metal; |
312 | | if (template.Cost.Population) ret.cost.population = +template.Cost.Population; |
313 | | if (template.Cost.PopulationBonus) ret.cost.populationBonus = +template.Cost.PopulationBonus; |
314 | | } |
315 | | |
316 | | if (template.Health) |
317 | | { |
318 | | ret.health = +template.Health.Max; |
319 | | } |
320 | | |
321 | | if (template.Identity) |
322 | | { |
323 | | ret.selectionGroupName = template.Identity.SelectionGroupName; |
324 | | ret.name = { |
325 | | "specific": (template.Identity.SpecificName || template.Identity.GenericName), |
326 | | "generic": template.Identity.GenericName |
327 | | }; |
328 | | ret.icon = template.Identity.Icon; |
329 | | ret.tooltip = template.Identity.Tooltip; |
330 | | } |
331 | | |
332 | | if (template.UnitMotion) |
333 | | { |
334 | | ret.speed = { |
335 | | "walk": +template.UnitMotion.WalkSpeed, |
336 | | }; |
337 | | if (template.UnitMotion.Run) ret.speed.run = +template.UnitMotion.Run.Speed; |
338 | | } |
339 | | |
340 | | return ret; |
341 | | }; |
342 | | |
343 | | GuiInterface.prototype.PushNotification = function(notification) |
344 | | { |
345 | | this.notifications.push(notification); |
346 | | }; |
347 | | |
348 | | GuiInterface.prototype.GetNextNotification = function() |
349 | | { |
350 | | if (this.notifications.length) |
351 | | return this.notifications.pop(); |
352 | | else |
353 | | return ""; |
354 | | }; |
355 | | |
356 | | GuiInterface.prototype.CanMoveEntsIntoFormation = function(player, data) |
357 | | { |
358 | | return CanMoveEntsIntoFormation(data.ents, data.formationName); |
359 | | }; |
360 | | |
361 | | GuiInterface.prototype.IsStanceSelected = function(player, data) |
362 | | { |
363 | | for each (var ent in data.ents) |
364 | | { |
365 | | var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); |
366 | | if (cmpUnitAI) |
367 | | { |
368 | | if (cmpUnitAI.GetStanceName() == data.stance) |
369 | | return true; |
370 | | } |
371 | | } |
372 | | return false; |
373 | | }; |
374 | | |
375 | | GuiInterface.prototype.SetSelectionHighlight = function(player, cmd) |
376 | | { |
377 | | var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); |
378 | | |
379 | | var playerColours = {}; // cache of owner -> colour map |
380 | | |
381 | | for each (var ent in cmd.entities) |
382 | | { |
383 | | var cmpSelectable = Engine.QueryInterface(ent, IID_Selectable); |
384 | | if (!cmpSelectable) |
385 | | continue; |
386 | | |
387 | | if (cmd.alpha == 0) |
388 | | { |
389 | | cmpSelectable.SetSelectionHighlight({"r":0, "g":0, "b":0, "a":0}); |
390 | | continue; |
391 | | } |
392 | | |
393 | | // Find the entity's owner's colour: |
394 | | |
395 | | var owner = -1; |
396 | | var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); |
397 | | if (cmpOwnership) |
398 | | owner = cmpOwnership.GetOwner(); |
399 | | |
400 | | var colour = playerColours[owner]; |
401 | | if (!colour) |
402 | | { |
403 | | colour = {"r":1, "g":1, "b":1}; |
404 | | var cmpPlayer = Engine.QueryInterface(cmpPlayerMan.GetPlayerByID(owner), IID_Player); |
405 | | if (cmpPlayer) |
406 | | colour = cmpPlayer.GetColour(); |
407 | | playerColours[owner] = colour; |
408 | | } |
409 | | |
410 | | cmpSelectable.SetSelectionHighlight({"r":colour.r, "g":colour.g, "b":colour.b, "a":cmd.alpha}); |
411 | | } |
412 | | }; |
413 | | |
414 | | GuiInterface.prototype.SetStatusBars = function(player, cmd) |
415 | | { |
416 | | for each (var ent in cmd.entities) |
417 | | { |
418 | | var cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars); |
419 | | if (cmpStatusBars) |
420 | | cmpStatusBars.SetEnabled(cmd.enabled); |
421 | | } |
422 | | }; |
423 | | |
424 | | /** |
425 | | * Displays the rally point of a given list of entities (carried in cmd.entities). |
426 | | * |
427 | | * The 'cmd' object may carry its own x/z coordinate pair indicating the location where the rally point should |
428 | | * be rendered, in order to support instantaneously rendering a rally point marker at a specified location |
429 | | * instead of incurring a delay while PostNetworkCommand processes the set-rallypoint command (see input.js). |
430 | | * If cmd doesn't carry a custom location, then the position to render the marker at will be read from the |
431 | | * RallyPoint component. |
432 | | */ |
433 | | GuiInterface.prototype.DisplayRallyPoint = function(player, cmd) |
434 | | { |
435 | | var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); |
436 | | var cmpPlayer = Engine.QueryInterface(cmpPlayerMan.GetPlayerByID(player), IID_Player); |
437 | | |
438 | | // If there are some rally points already displayed, first hide them |
439 | | for each (var ent in this.entsRallyPointsDisplayed) |
440 | | { |
441 | | var cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer); |
442 | | if (cmpRallyPointRenderer) |
443 | | cmpRallyPointRenderer.SetDisplayed(false); |
444 | | } |
445 | | |
446 | | this.entsRallyPointsDisplayed = []; |
447 | | |
448 | | // Show the rally points for the passed entities |
449 | | for each (var ent in cmd.entities) |
450 | | { |
451 | | var cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer); |
452 | | if (!cmpRallyPointRenderer) |
453 | | continue; |
454 | | |
455 | | // entity must have a rally point component to display a rally point marker |
456 | | // (regardless of whether cmd specifies a custom location) |
457 | | var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint); |
458 | | if (!cmpRallyPoint) |
459 | | continue; |
460 | | |
461 | | // Verify the owner |
462 | | var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); |
463 | | if (!(cmpPlayer && cmpPlayer.CanControlAllUnits())) |
464 | | if (!cmpOwnership || cmpOwnership.GetOwner() != player) |
465 | | continue; |
466 | | |
467 | | // If the command was passed an explicit position, use that and |
468 | | // override the real rally point position; otherwise use the real position |
469 | | var pos; |
470 | | if (cmd.x && cmd.z) |
471 | | pos = cmd; |
472 | | else |
473 | | pos = cmpRallyPoint.GetPosition(); // may return undefined if no rally point is set |
474 | | |
475 | | if (pos) |
476 | | { |
477 | | cmpRallyPointRenderer.SetPosition({'x': pos.x, 'y': pos.z}); // SetPosition takes a CFixedVector2D which has X/Y components, not X/Z |
478 | | cmpRallyPointRenderer.SetDisplayed(true); |
479 | | |
480 | | // remember which entities have their rally points displayed so we can hide them again |
481 | | this.entsRallyPointsDisplayed.push(ent); |
482 | | } |
483 | | } |
484 | | }; |
485 | | |
486 | | /** |
487 | | * Display the building placement preview. |
488 | | * cmd.template is the name of the entity template, or "" to disable the preview. |
489 | | * cmd.x, cmd.z, cmd.angle give the location. |
490 | | * Returns true if the placement is okay (everything is valid and the entity is not obstructed by others). |
491 | | */ |
492 | | GuiInterface.prototype.SetBuildingPlacementPreview = function(player, cmd) |
493 | | { |
494 | | // See if we're changing template |
495 | | if (!this.placementEntity || this.placementEntity[0] != cmd.template) |
496 | | { |
497 | | // Destroy the old preview if there was one |
498 | | if (this.placementEntity) |
499 | | Engine.DestroyEntity(this.placementEntity[1]); |
500 | | |
501 | | // Load the new template |
502 | | if (cmd.template == "") |
503 | | { |
504 | | this.placementEntity = undefined; |
505 | | } |
506 | | else |
507 | | { |
508 | | this.placementEntity = [cmd.template, Engine.AddLocalEntity("preview|" + cmd.template)]; |
509 | | } |
510 | | } |
511 | | |
512 | | if (this.placementEntity) |
513 | | { |
514 | | var ent = this.placementEntity[1]; |
515 | | |
516 | | // Move the preview into the right location |
517 | | var pos = Engine.QueryInterface(ent, IID_Position); |
518 | | if (pos) |
519 | | { |
520 | | pos.JumpTo(cmd.x, cmd.z); |
521 | | pos.SetYRotation(cmd.angle); |
522 | | } |
523 | | |
524 | | // Check whether it's in a visible or fogged region |
525 | | // tell GetLosVisibility to force RetainInFog because preview entities set this to false, |
526 | | // which would show them as hidden instead of fogged |
527 | | var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); |
528 | | var visible = (cmpRangeManager && cmpRangeManager.GetLosVisibility(ent, player, true) != "hidden"); |
529 | | var validPlacement = false; |
530 | | |
531 | | if (visible) |
532 | | { // Check whether it's obstructed by other entities or invalid terrain |
533 | | var cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions); |
534 | | if (!cmpBuildRestrictions) |
535 | | error("cmpBuildRestrictions not defined"); |
536 | | |
537 | | validPlacement = (cmpBuildRestrictions && cmpBuildRestrictions.CheckPlacement(player)); |
538 | | } |
539 | | |
540 | | var ok = (visible && validPlacement); |
541 | | |
542 | | // Set it to a red shade if this is an invalid location |
543 | | var cmpVisual = Engine.QueryInterface(ent, IID_Visual); |
544 | | if (cmpVisual) |
545 | | { |
546 | | if (!ok) |
547 | | cmpVisual.SetShadingColour(1.4, 0.4, 0.4, 1); |
548 | | else |
549 | | cmpVisual.SetShadingColour(1, 1, 1, 1); |
550 | | } |
551 | | |
552 | | return ok; |
553 | | } |
554 | | |
555 | | return false; |
556 | | }; |
557 | | |
558 | | GuiInterface.prototype.GetFoundationSnapData = function(player, data) |
559 | | { |
560 | | var cmpTemplateMgr = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); |
561 | | var template = cmpTemplateMgr.GetTemplate(data.template); |
562 | | |
563 | | if (template.BuildRestrictions.Category == "Dock") |
564 | | { |
565 | | var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); |
566 | | var cmpWaterManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_WaterManager); |
567 | | if (!cmpTerrain || !cmpWaterManager) |
568 | | { |
569 | | return false; |
570 | | } |
571 | | |
572 | | // Get footprint size |
573 | | var halfSize = 0; |
574 | | if (template.Footprint.Square) |
575 | | { |
576 | | halfSize = Math.max(template.Footprint.Square["@depth"], template.Footprint.Square["@width"])/2; |
577 | | } |
578 | | else if (template.Footprint.Circle) |
579 | | { |
580 | | halfSize = template.Footprint.Circle["@radius"]; |
581 | | } |
582 | | |
583 | | /* Find direction of most open water, algorithm: |
584 | | * 1. Pick points in a circle around dock |
585 | | * 2. If point is in water, add to array |
586 | | * 3. Scan array looking for consecutive points |
587 | | * 4. Find longest sequence of consecutive points |
588 | | * 5. If sequence equals all points, no direction can be determined, |
589 | | * expand search outward and try (1) again |
590 | | * 6. Calculate angle using average of sequence |
591 | | */ |
592 | | const numPoints = 16; |
593 | | for (var dist = 0; dist < 4; ++dist) |
594 | | { |
595 | | var waterPoints = []; |
596 | | for (var i = 0; i < numPoints; ++i) |
597 | | { |
598 | | var angle = (i/numPoints)*2*Math.PI; |
599 | | var d = halfSize*(dist+1); |
600 | | var nx = data.x - d*Math.sin(angle); |
601 | | var nz = data.z + d*Math.cos(angle); |
602 | | |
603 | | if (cmpTerrain.GetGroundLevel(nx, nz) < cmpWaterManager.GetWaterLevel(nx, nz)) |
604 | | { |
605 | | waterPoints.push(i); |
606 | | } |
607 | | } |
608 | | var consec = []; |
609 | | var length = waterPoints.length; |
610 | | for (var i = 0; i < length; ++i) |
611 | | { |
612 | | var count = 0; |
613 | | for (var j = 0; j < (length-1); ++j) |
614 | | { |
615 | | if (((waterPoints[(i + j) % length]+1) % numPoints) == waterPoints[(i + j + 1) % length]) |
616 | | { |
617 | | ++count; |
618 | | } |
619 | | else |
620 | | { |
621 | | break; |
622 | | } |
623 | | } |
624 | | consec[i] = count; |
625 | | } |
626 | | var start = 0; |
627 | | var count = 0; |
628 | | for (var c in consec) |
629 | | { |
630 | | if (consec[c] > count) |
631 | | { |
632 | | start = c; |
633 | | count = consec[c]; |
634 | | } |
635 | | } |
636 | | |
637 | | // If we've found a shoreline, stop searching |
638 | | if (count != numPoints-1) |
639 | | { |
640 | | return {"x": data.x, "z": data.z, "angle": -(((waterPoints[start] + consec[start]/2) % numPoints)/numPoints*2*Math.PI)}; |
641 | | } |
642 | | } |
643 | | } |
644 | | |
645 | | return false; |
646 | | }; |
647 | | |
648 | | GuiInterface.prototype.PlaySound = function(player, data) |
649 | | { |
650 | | // Ignore if no entity was passed |
651 | | if (!data.entity) |
652 | | return; |
653 | | |
654 | | PlaySound(data.name, data.entity); |
655 | | }; |
656 | | |
657 | | function isIdleUnit(ent, idleClass) |
658 | | { |
659 | | var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); |
660 | | var cmpIdentity = Engine.QueryInterface(ent, IID_Identity); |
661 | | |
662 | | // TODO: Do something with garrisoned idle units |
663 | | return (cmpUnitAI && cmpIdentity && cmpUnitAI.IsIdle() && !cmpUnitAI.IsGarrisoned() && idleClass && cmpIdentity.HasClass(idleClass)); |
664 | | } |
665 | | |
666 | | GuiInterface.prototype.FindIdleUnit = function(player, data) |
667 | | { |
668 | | var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); |
669 | | var playerEntities = rangeMan.GetEntitiesByPlayer(player); |
670 | | |
671 | | // Find the first matching entity that is after the previous selection, |
672 | | // so that we cycle around in a predictable order |
673 | | for each (var ent in playerEntities) |
674 | | { |
675 | | if (ent > data.prevUnit && isIdleUnit(ent, data.idleClass)) |
676 | | return ent; |
677 | | } |
678 | | |
679 | | // No idle entities left in the class |
680 | | return 0; |
681 | | }; |
682 | | |
683 | | GuiInterface.prototype.SetPathfinderDebugOverlay = function(player, enabled) |
684 | | { |
685 | | var cmpPathfinder = Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder); |
686 | | cmpPathfinder.SetDebugOverlay(enabled); |
687 | | }; |
688 | | |
689 | | GuiInterface.prototype.SetObstructionDebugOverlay = function(player, enabled) |
690 | | { |
691 | | var cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); |
692 | | cmpObstructionManager.SetDebugOverlay(enabled); |
693 | | }; |
694 | | |
695 | | GuiInterface.prototype.SetMotionDebugOverlay = function(player, data) |
696 | | { |
697 | | for each (var ent in data.entities) |
698 | | { |
699 | | var cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion); |
700 | | if (cmpUnitMotion) |
701 | | cmpUnitMotion.SetDebugOverlay(data.enabled); |
702 | | } |
703 | | }; |
704 | | |
705 | | GuiInterface.prototype.SetRangeDebugOverlay = function(player, enabled) |
706 | | { |
707 | | var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); |
708 | | cmpRangeManager.SetDebugOverlay(enabled); |
709 | | }; |
710 | | |
711 | | GuiInterface.prototype.OnGlobalEntityRenamed = function(msg) |
712 | | { |
713 | | this.renamedEntities.push(msg); |
714 | | } |
715 | | |
716 | | // List the GuiInterface functions that can be safely called by GUI scripts. |
717 | | // (GUI scripts are non-deterministic and untrusted, so these functions must be |
718 | | // appropriately careful. They are called with a first argument "player", which is |
719 | | // trusted and indicates the player associated with the current client; no data should |
720 | | // be returned unless this player is meant to be able to see it.) |
721 | | var exposedFunctions = { |
722 | | |
723 | | "GetSimulationState": 1, |
724 | | "GetExtendedSimulationState": 1, |
725 | | "GetRenamedEntities": 1, |
726 | | "ClearRenamedEntities": 1, |
727 | | "GetEntityState": 1, |
728 | | "GetTemplateData": 1, |
729 | | "GetNextNotification": 1, |
730 | | |
731 | | "CanMoveEntsIntoFormation": 1, |
732 | | "IsStanceSelected": 1, |
733 | | |
734 | | "SetSelectionHighlight": 1, |
735 | | "SetStatusBars": 1, |
736 | | "DisplayRallyPoint": 1, |
737 | | "SetBuildingPlacementPreview": 1, |
738 | | "GetFoundationSnapData": 1, |
739 | | "PlaySound": 1, |
740 | | "FindIdleUnit": 1, |
741 | | |
742 | | "SetPathfinderDebugOverlay": 1, |
743 | | "SetObstructionDebugOverlay": 1, |
744 | | "SetMotionDebugOverlay": 1, |
745 | | "SetRangeDebugOverlay": 1, |
746 | | }; |
747 | | |
748 | | GuiInterface.prototype.ScriptCall = function(player, name, args) |
749 | | { |
750 | | if (exposedFunctions[name]) |
751 | | return this[name](player, args); |
752 | | else |
753 | | throw new Error("Invalid GuiInterface Call name \""+name+"\""); |
754 | | }; |
755 | | |
756 | | Engine.RegisterComponentType(IID_GuiInterface, "GuiInterface", GuiInterface); |
| 1 | function GuiInterface() {} |
| 2 | |
| 3 | GuiInterface.prototype.Schema = |
| 4 | "<a:component type='system'/><empty/>"; |
| 5 | |
| 6 | GuiInterface.prototype.Serialize = function() |
| 7 | { |
| 8 | // This component isn't network-synchronised so we mustn't serialise |
| 9 | // its non-deterministic data. Instead just return an empty object. |
| 10 | return {}; |
| 11 | }; |
| 12 | |
| 13 | GuiInterface.prototype.Deserialize = function(obj) |
| 14 | { |
| 15 | this.Init(); |
| 16 | }; |
| 17 | |
| 18 | GuiInterface.prototype.Init = function() |
| 19 | { |
| 20 | this.placementEntity = undefined; // = undefined or [templateName, entityID] |
| 21 | this.rallyPoints = undefined; |
| 22 | this.notifications = []; |
| 23 | this.renamedEntities = []; |
| 24 | }; |
| 25 | |
| 26 | /* |
| 27 | * All of the functions defined below are called via Engine.GuiInterfaceCall(name, arg) |
| 28 | * from GUI scripts, and executed here with arguments (player, arg). |
| 29 | * |
| 30 | * CAUTION: The input to the functions in this module is not network-synchronised, so it |
| 31 | * mustn't affect the simulation state (i.e. the data that is serialised and can affect |
| 32 | * the behaviour of the rest of the simulation) else it'll cause out-of-sync errors. |
| 33 | */ |
| 34 | |
| 35 | /** |
| 36 | * Returns global information about the current game state. |
| 37 | * This is used by the GUI and also by AI scripts. |
| 38 | */ |
| 39 | GuiInterface.prototype.GetSimulationState = function(player) |
| 40 | { |
| 41 | var ret = { |
| 42 | "players": [] |
| 43 | }; |
| 44 | |
| 45 | var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); |
| 46 | var n = cmpPlayerMan.GetNumPlayers(); |
| 47 | for (var i = 0; i < n; ++i) |
| 48 | { |
| 49 | var playerEnt = cmpPlayerMan.GetPlayerByID(i); |
| 50 | var cmpPlayerBuildLimits = Engine.QueryInterface(playerEnt, IID_BuildLimits); |
| 51 | var cmpPlayer = Engine.QueryInterface(playerEnt, IID_Player); |
| 52 | |
| 53 | // store player ally/enemy data as arrays |
| 54 | var allies = []; |
| 55 | var enemies = []; |
| 56 | for (var j = 0; j <= n; ++j) |
| 57 | { |
| 58 | allies[j] = cmpPlayer.IsAlly(j); |
| 59 | enemies[j] = cmpPlayer.IsEnemy(j); |
| 60 | } |
| 61 | var playerData = { |
| 62 | "name": cmpPlayer.GetName(), |
| 63 | "civ": cmpPlayer.GetCiv(), |
| 64 | "colour": cmpPlayer.GetColour(), |
| 65 | "popCount": cmpPlayer.GetPopulationCount(), |
| 66 | "popLimit": cmpPlayer.GetPopulationLimit(), |
| 67 | "popMax": cmpPlayer.GetMaxPopulation(), |
| 68 | "resourceCounts": cmpPlayer.GetResourceCounts(), |
| 69 | "trainingQueueBlocked": cmpPlayer.IsTrainingQueueBlocked(), |
| 70 | "state": cmpPlayer.GetState(), |
| 71 | "team": cmpPlayer.GetTeam(), |
| 72 | "phase": cmpPlayer.GetPhase(), |
| 73 | "isAlly": allies, |
| 74 | "isEnemy": enemies, |
| 75 | "buildLimits": cmpPlayerBuildLimits.GetLimits(), |
| 76 | "buildCounts": cmpPlayerBuildLimits.GetCounts() |
| 77 | }; |
| 78 | ret.players.push(playerData); |
| 79 | } |
| 80 | |
| 81 | var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); |
| 82 | if (cmpRangeManager) |
| 83 | { |
| 84 | ret.circularMap = cmpRangeManager.GetLosCircular(); |
| 85 | } |
| 86 | |
| 87 | // Add timeElapsed |
| 88 | var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); |
| 89 | ret.timeElapsed = cmpTimer.GetTime(); |
| 90 | |
| 91 | return ret; |
| 92 | }; |
| 93 | |
| 94 | GuiInterface.prototype.GetExtendedSimulationState = function(player) |
| 95 | { |
| 96 | // Get basic simulation info |
| 97 | var ret = this.GetSimulationState(); |
| 98 | |
| 99 | // Add statistics to each player |
| 100 | var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); |
| 101 | var n = cmpPlayerMan.GetNumPlayers(); |
| 102 | for (var i = 0; i < n; ++i) |
| 103 | { |
| 104 | var playerEnt = cmpPlayerMan.GetPlayerByID(i); |
| 105 | var cmpPlayerStatisticsTracker = Engine.QueryInterface(playerEnt, IID_StatisticsTracker); |
| 106 | ret.players[i].statistics = cmpPlayerStatisticsTracker.GetStatistics(); |
| 107 | } |
| 108 | |
| 109 | return ret; |
| 110 | }; |
| 111 | |
| 112 | GuiInterface.prototype.GetRenamedEntities = function(player) |
| 113 | { |
| 114 | return this.renamedEntities; |
| 115 | }; |
| 116 | |
| 117 | GuiInterface.prototype.ClearRenamedEntities = function(player) |
| 118 | { |
| 119 | this.renamedEntities = []; |
| 120 | }; |
| 121 | |
| 122 | GuiInterface.prototype.GetEntityState = function(player, ent) |
| 123 | { |
| 124 | var cmpTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); |
| 125 | |
| 126 | // All units must have a template; if not then it's a nonexistent entity id |
| 127 | var template = cmpTempMan.GetCurrentTemplateName(ent); |
| 128 | if (!template) |
| 129 | return null; |
| 130 | |
| 131 | var ret = { |
| 132 | "id": ent, |
| 133 | "template": template |
| 134 | } |
| 135 | |
| 136 | var cmpIdentity = Engine.QueryInterface(ent, IID_Identity); |
| 137 | if (cmpIdentity) |
| 138 | { |
| 139 | ret.identity = { |
| 140 | "rank": cmpIdentity.GetRank(), |
| 141 | "classes": cmpIdentity.GetClassesList(), |
| 142 | "selectionGroupName": cmpIdentity.GetSelectionGroupName() |
| 143 | }; |
| 144 | } |
| 145 | |
| 146 | var cmpPosition = Engine.QueryInterface(ent, IID_Position); |
| 147 | if (cmpPosition && cmpPosition.IsInWorld()) |
| 148 | { |
| 149 | ret.position = cmpPosition.GetPosition(); |
| 150 | } |
| 151 | |
| 152 | var cmpHealth = Engine.QueryInterface(ent, IID_Health); |
| 153 | if (cmpHealth) |
| 154 | { |
| 155 | ret.hitpoints = cmpHealth.GetHitpoints(); |
| 156 | ret.maxHitpoints = cmpHealth.GetMaxHitpoints(); |
| 157 | ret.needsRepair = cmpHealth.IsRepairable() && (cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints()); |
| 158 | } |
| 159 | |
| 160 | var cmpAttack = Engine.QueryInterface(ent, IID_Attack); |
| 161 | if (cmpAttack) |
| 162 | { |
| 163 | var type = cmpAttack.GetBestAttack(); // TODO: how should we decide which attack to show? |
| 164 | ret.attack = cmpAttack.GetAttackStrengths(type); |
| 165 | } |
| 166 | |
| 167 | var cmpArmour = Engine.QueryInterface(ent, IID_DamageReceiver); |
| 168 | if (cmpArmour) |
| 169 | { |
| 170 | ret.armour = cmpArmour.GetArmourStrengths(); |
| 171 | } |
| 172 | |
| 173 | var cmpBuilder = Engine.QueryInterface(ent, IID_Builder); |
| 174 | if (cmpBuilder) |
| 175 | { |
| 176 | ret.buildEntities = cmpBuilder.GetEntitiesList(); |
| 177 | } |
| 178 | |
| 179 | var cmpTrainingQueue = Engine.QueryInterface(ent, IID_TrainingQueue); |
| 180 | if (cmpTrainingQueue) |
| 181 | { |
| 182 | ret.training = { |
| 183 | "entities": cmpTrainingQueue.GetEntitiesList(), |
| 184 | "queue": cmpTrainingQueue.GetQueue(), |
| 185 | }; |
| 186 | } |
| 187 | |
| 188 | var cmpFoundation = Engine.QueryInterface(ent, IID_Foundation); |
| 189 | if (cmpFoundation) |
| 190 | { |
| 191 | ret.foundation = { |
| 192 | "progress": cmpFoundation.GetBuildPercentage() |
| 193 | }; |
| 194 | } |
| 195 | |
| 196 | var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); |
| 197 | if (cmpOwnership) |
| 198 | { |
| 199 | ret.player = cmpOwnership.GetOwner(); |
| 200 | } |
| 201 | |
| 202 | var cmpResourceSupply = Engine.QueryInterface(ent, IID_ResourceSupply); |
| 203 | if (cmpResourceSupply) |
| 204 | { |
| 205 | ret.resourceSupply = { |
| 206 | "max": cmpResourceSupply.GetMaxAmount(), |
| 207 | "amount": cmpResourceSupply.GetCurrentAmount(), |
| 208 | "type": cmpResourceSupply.GetType() |
| 209 | }; |
| 210 | } |
| 211 | |
| 212 | var cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer); |
| 213 | if (cmpResourceGatherer) |
| 214 | { |
| 215 | ret.resourceGatherRates = cmpResourceGatherer.GetGatherRates(); |
| 216 | ret.resourceCarrying = cmpResourceGatherer.GetCarryingStatus(); |
| 217 | } |
| 218 | |
| 219 | var cmpResourceDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite); |
| 220 | if (cmpResourceDropsite) |
| 221 | { |
| 222 | ret.resourceDropsite = { |
| 223 | "types": cmpResourceDropsite.GetTypes() |
| 224 | }; |
| 225 | } |
| 226 | |
| 227 | var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint); |
| 228 | if (cmpRallyPoint) |
| 229 | { |
| 230 | ret.rallyPoint = {'position': cmpRallyPoint.GetPosition()}; // undefined or {x,z} object |
| 231 | } |
| 232 | |
| 233 | var cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder); |
| 234 | if (cmpGarrisonHolder) |
| 235 | { |
| 236 | ret.garrisonHolder = { |
| 237 | "entities": cmpGarrisonHolder.GetEntities(), |
| 238 | "allowedClasses": cmpGarrisonHolder.GetAllowedClassesList() |
| 239 | }; |
| 240 | } |
| 241 | |
| 242 | var cmpPromotion = Engine.QueryInterface(ent, IID_Promotion); |
| 243 | if (cmpPromotion) |
| 244 | { |
| 245 | ret.promotion = { |
| 246 | "curr": cmpPromotion.GetCurrentXp(), |
| 247 | "req": cmpPromotion.GetRequiredXp() |
| 248 | }; |
| 249 | } |
| 250 | |
| 251 | var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); |
| 252 | if (cmpUnitAI) |
| 253 | { |
| 254 | ret.unitAI = { |
| 255 | // TODO: reading properties directly is kind of violating abstraction |
| 256 | "state": cmpUnitAI.fsmStateName, |
| 257 | "orders": cmpUnitAI.orderQueue, |
| 258 | }; |
| 259 | } |
| 260 | |
| 261 | if (!cmpFoundation && cmpIdentity && cmpIdentity.HasClass("BarterMarket")) |
| 262 | { |
| 263 | var cmpBarter = Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter); |
| 264 | ret.barterMarket = { "prices": cmpBarter.GetPrices() }; |
| 265 | } |
| 266 | |
| 267 | var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); |
| 268 | ret.visibility = cmpRangeManager.GetLosVisibility(ent, player, false); |
| 269 | |
| 270 | return ret; |
| 271 | }; |
| 272 | |
| 273 | GuiInterface.prototype.GetTemplateData = function(player, name) |
| 274 | { |
| 275 | var cmpTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); |
| 276 | var template = cmpTempMan.GetTemplate(name); |
| 277 | |
| 278 | if (!template) |
| 279 | return null; |
| 280 | |
| 281 | var ret = {}; |
| 282 | |
| 283 | if (template.Armour) |
| 284 | { |
| 285 | ret.armour = { |
| 286 | "hack": +template.Armour.Hack, |
| 287 | "pierce": +template.Armour.Pierce, |
| 288 | "crush": +template.Armour.Crush, |
| 289 | }; |
| 290 | } |
| 291 | |
| 292 | if (template.Attack) |
| 293 | { |
| 294 | ret.attack = {}; |
| 295 | for (var type in template.Attack) |
| 296 | { |
| 297 | ret.attack[type] = { |
| 298 | "hack": (+template.Attack[type].Hack || 0), |
| 299 | "pierce": (+template.Attack[type].Pierce || 0), |
| 300 | "crush": (+template.Attack[type].Crush || 0), |
| 301 | }; |
| 302 | } |
| 303 | } |
| 304 | |
| 305 | if (template.Cost) |
| 306 | { |
| 307 | ret.cost = {}; |
| 308 | if (template.Cost.Resources.food) ret.cost.food = +template.Cost.Resources.food; |
| 309 | if (template.Cost.Resources.wood) ret.cost.wood = +template.Cost.Resources.wood; |
| 310 | if (template.Cost.Resources.stone) ret.cost.stone = +template.Cost.Resources.stone; |
| 311 | if (template.Cost.Resources.metal) ret.cost.metal = +template.Cost.Resources.metal; |
| 312 | if (template.Cost.Population) ret.cost.population = +template.Cost.Population; |
| 313 | if (template.Cost.PopulationBonus) ret.cost.populationBonus = +template.Cost.PopulationBonus; |
| 314 | } |
| 315 | |
| 316 | if (template.Health) |
| 317 | { |
| 318 | ret.health = +template.Health.Max; |
| 319 | } |
| 320 | |
| 321 | if (template.Identity) |
| 322 | { |
| 323 | ret.selectionGroupName = template.Identity.SelectionGroupName; |
| 324 | ret.name = { |
| 325 | "specific": (template.Identity.SpecificName || template.Identity.GenericName), |
| 326 | "generic": template.Identity.GenericName |
| 327 | }; |
| 328 | ret.icon = template.Identity.Icon; |
| 329 | ret.tooltip = template.Identity.Tooltip; |
| 330 | } |
| 331 | |
| 332 | if (template.UnitMotion) |
| 333 | { |
| 334 | ret.speed = { |
| 335 | "walk": +template.UnitMotion.WalkSpeed, |
| 336 | }; |
| 337 | if (template.UnitMotion.Run) ret.speed.run = +template.UnitMotion.Run.Speed; |
| 338 | } |
| 339 | |
| 340 | return ret; |
| 341 | }; |
| 342 | |
| 343 | GuiInterface.prototype.PushNotification = function(notification) |
| 344 | { |
| 345 | this.notifications.push(notification); |
| 346 | }; |
| 347 | |
| 348 | GuiInterface.prototype.GetNextNotification = function() |
| 349 | { |
| 350 | if (this.notifications.length) |
| 351 | return this.notifications.pop(); |
| 352 | else |
| 353 | return ""; |
| 354 | }; |
| 355 | |
| 356 | GuiInterface.prototype.CanMoveEntsIntoFormation = function(player, data) |
| 357 | { |
| 358 | return CanMoveEntsIntoFormation(data.ents, data.formationName); |
| 359 | }; |
| 360 | |
| 361 | GuiInterface.prototype.IsStanceSelected = function(player, data) |
| 362 | { |
| 363 | for each (var ent in data.ents) |
| 364 | { |
| 365 | var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); |
| 366 | if (cmpUnitAI) |
| 367 | { |
| 368 | if (cmpUnitAI.GetStanceName() == data.stance) |
| 369 | return true; |
| 370 | } |
| 371 | } |
| 372 | return false; |
| 373 | }; |
| 374 | |
| 375 | GuiInterface.prototype.SetSelectionHighlight = function(player, cmd) |
| 376 | { |
| 377 | var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); |
| 378 | |
| 379 | var playerColours = {}; // cache of owner -> colour map |
| 380 | |
| 381 | for each (var ent in cmd.entities) |
| 382 | { |
| 383 | var cmpSelectable = Engine.QueryInterface(ent, IID_Selectable); |
| 384 | if (!cmpSelectable) |
| 385 | continue; |
| 386 | |
| 387 | if (cmd.alpha == 0) |
| 388 | { |
| 389 | cmpSelectable.SetSelectionHighlight({"r":0, "g":0, "b":0, "a":0}); |
| 390 | continue; |
| 391 | } |
| 392 | |
| 393 | // Find the entity's owner's colour: |
| 394 | |
| 395 | var owner = -1; |
| 396 | var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); |
| 397 | if (cmpOwnership) |
| 398 | owner = cmpOwnership.GetOwner(); |
| 399 | |
| 400 | var colour = playerColours[owner]; |
| 401 | if (!colour) |
| 402 | { |
| 403 | colour = {"r":1, "g":1, "b":1}; |
| 404 | var cmpPlayer = Engine.QueryInterface(cmpPlayerMan.GetPlayerByID(owner), IID_Player); |
| 405 | if (cmpPlayer) |
| 406 | colour = cmpPlayer.GetColour(); |
| 407 | playerColours[owner] = colour; |
| 408 | } |
| 409 | |
| 410 | cmpSelectable.SetSelectionHighlight({"r":colour.r, "g":colour.g, "b":colour.b, "a":cmd.alpha}); |
| 411 | } |
| 412 | }; |
| 413 | |
| 414 | GuiInterface.prototype.SetStatusBars = function(player, cmd) |
| 415 | { |
| 416 | for each (var ent in cmd.entities) |
| 417 | { |
| 418 | var cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars); |
| 419 | if (cmpStatusBars) |
| 420 | cmpStatusBars.SetEnabled(cmd.enabled); |
| 421 | } |
| 422 | }; |
| 423 | |
| 424 | /** |
| 425 | * Displays the rally point of a given list of entities (carried in cmd.entities). |
| 426 | * |
| 427 | * The 'cmd' object may carry its own x/z coordinate pair indicating the location where the rally point should |
| 428 | * be rendered, in order to support instantaneously rendering a rally point marker at a specified location |
| 429 | * instead of incurring a delay while PostNetworkCommand processes the set-rallypoint command (see input.js). |
| 430 | * If cmd doesn't carry a custom location, then the position to render the marker at will be read from the |
| 431 | * RallyPoint component. |
| 432 | */ |
| 433 | GuiInterface.prototype.DisplayRallyPoint = function(player, cmd) |
| 434 | { |
| 435 | var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); |
| 436 | var cmpPlayer = Engine.QueryInterface(cmpPlayerMan.GetPlayerByID(player), IID_Player); |
| 437 | |
| 438 | // If there are some rally points already displayed, first hide them |
| 439 | for each (var ent in this.entsRallyPointsDisplayed) |
| 440 | { |
| 441 | var cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer); |
| 442 | if (cmpRallyPointRenderer) |
| 443 | cmpRallyPointRenderer.SetDisplayed(false); |
| 444 | } |
| 445 | |
| 446 | this.entsRallyPointsDisplayed = []; |
| 447 | |
| 448 | // Show the rally points for the passed entities |
| 449 | for each (var ent in cmd.entities) |
| 450 | { |
| 451 | var cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer); |
| 452 | if (!cmpRallyPointRenderer) |
| 453 | continue; |
| 454 | |
| 455 | // entity must have a rally point component to display a rally point marker |
| 456 | // (regardless of whether cmd specifies a custom location) |
| 457 | var cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint); |
| 458 | if (!cmpRallyPoint) |
| 459 | continue; |
| 460 | |
| 461 | // Verify the owner |
| 462 | var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership); |
| 463 | if (!(cmpPlayer && cmpPlayer.CanControlAllUnits())) |
| 464 | if (!cmpOwnership || cmpOwnership.GetOwner() != player) |
| 465 | continue; |
| 466 | |
| 467 | // If the command was passed an explicit position, use that and |
| 468 | // override the real rally point position; otherwise use the real position |
| 469 | var pos; |
| 470 | if (cmd.x && cmd.z) |
| 471 | pos = cmd; |
| 472 | else |
| 473 | pos = cmpRallyPoint.GetPosition(); // may return undefined if no rally point is set |
| 474 | |
| 475 | if (pos) |
| 476 | { |
| 477 | cmpRallyPointRenderer.SetPosition({'x': pos.x, 'y': pos.z}); // SetPosition takes a CFixedVector2D which has X/Y components, not X/Z |
| 478 | cmpRallyPointRenderer.SetDisplayed(true); |
| 479 | |
| 480 | // remember which entities have their rally points displayed so we can hide them again |
| 481 | this.entsRallyPointsDisplayed.push(ent); |
| 482 | } |
| 483 | } |
| 484 | }; |
| 485 | |
| 486 | /** |
| 487 | * Display the building placement preview. |
| 488 | * cmd.template is the name of the entity template, or "" to disable the preview. |
| 489 | * cmd.x, cmd.z, cmd.angle give the location. |
| 490 | * Returns true if the placement is okay (everything is valid and the entity is not obstructed by others). |
| 491 | */ |
| 492 | GuiInterface.prototype.SetBuildingPlacementPreview = function(player, cmd) |
| 493 | { |
| 494 | // See if we're changing template |
| 495 | if (!this.placementEntity || this.placementEntity[0] != cmd.template) |
| 496 | { |
| 497 | // Destroy the old preview if there was one |
| 498 | if (this.placementEntity) |
| 499 | Engine.DestroyEntity(this.placementEntity[1]); |
| 500 | |
| 501 | // Load the new template |
| 502 | if (cmd.template == "") |
| 503 | { |
| 504 | this.placementEntity = undefined; |
| 505 | } |
| 506 | else |
| 507 | { |
| 508 | this.placementEntity = [cmd.template, Engine.AddLocalEntity("preview|" + cmd.template)]; |
| 509 | } |
| 510 | } |
| 511 | |
| 512 | if (this.placementEntity) |
| 513 | { |
| 514 | var ent = this.placementEntity[1]; |
| 515 | |
| 516 | // Move the preview into the right location |
| 517 | var pos = Engine.QueryInterface(ent, IID_Position); |
| 518 | if (pos) |
| 519 | { |
| 520 | pos.JumpTo(cmd.x, cmd.z); |
| 521 | pos.SetYRotation(cmd.angle); |
| 522 | } |
| 523 | |
| 524 | // Check whether it's in a visible or fogged region |
| 525 | // tell GetLosVisibility to force RetainInFog because preview entities set this to false, |
| 526 | // which would show them as hidden instead of fogged |
| 527 | var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); |
| 528 | var visible = (cmpRangeManager && cmpRangeManager.GetLosVisibility(ent, player, true) != "hidden"); |
| 529 | var validPlacement = false; |
| 530 | |
| 531 | if (visible) |
| 532 | { // Check whether it's obstructed by other entities or invalid terrain |
| 533 | var cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions); |
| 534 | if (!cmpBuildRestrictions) |
| 535 | error("cmpBuildRestrictions not defined"); |
| 536 | |
| 537 | validPlacement = (cmpBuildRestrictions && cmpBuildRestrictions.CheckPlacement(player)); |
| 538 | } |
| 539 | |
| 540 | var ok = (visible && validPlacement); |
| 541 | |
| 542 | // Set it to a red shade if this is an invalid location |
| 543 | var cmpVisual = Engine.QueryInterface(ent, IID_Visual); |
| 544 | if (cmpVisual) |
| 545 | { |
| 546 | if (!ok) |
| 547 | cmpVisual.SetShadingColour(1.4, 0.4, 0.4, 1); |
| 548 | else |
| 549 | cmpVisual.SetShadingColour(1, 1, 1, 1); |
| 550 | } |
| 551 | |
| 552 | return ok; |
| 553 | } |
| 554 | |
| 555 | return false; |
| 556 | }; |
| 557 | |
| 558 | GuiInterface.prototype.GetFoundationSnapData = function(player, data) |
| 559 | { |
| 560 | var cmpTemplateMgr = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); |
| 561 | var template = cmpTemplateMgr.GetTemplate(data.template); |
| 562 | |
| 563 | if (template.BuildRestrictions.Category == "Dock") |
| 564 | { |
| 565 | var cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain); |
| 566 | var cmpWaterManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_WaterManager); |
| 567 | if (!cmpTerrain || !cmpWaterManager) |
| 568 | { |
| 569 | return false; |
| 570 | } |
| 571 | |
| 572 | // Get footprint size |
| 573 | var halfSize = 0; |
| 574 | if (template.Footprint.Square) |
| 575 | { |
| 576 | halfSize = Math.max(template.Footprint.Square["@depth"], template.Footprint.Square["@width"])/2; |
| 577 | } |
| 578 | else if (template.Footprint.Circle) |
| 579 | { |
| 580 | halfSize = template.Footprint.Circle["@radius"]; |
| 581 | } |
| 582 | |
| 583 | /* Find direction of most open water, algorithm: |
| 584 | * 1. Pick points in a circle around dock |
| 585 | * 2. If point is in water, add to array |
| 586 | * 3. Scan array looking for consecutive points |
| 587 | * 4. Find longest sequence of consecutive points |
| 588 | * 5. If sequence equals all points, no direction can be determined, |
| 589 | * expand search outward and try (1) again |
| 590 | * 6. Calculate angle using average of sequence |
| 591 | */ |
| 592 | const numPoints = 16; |
| 593 | for (var dist = 0; dist < 4; ++dist) |
| 594 | { |
| 595 | var waterPoints = []; |
| 596 | for (var i = 0; i < numPoints; ++i) |
| 597 | { |
| 598 | var angle = (i/numPoints)*2*Math.PI; |
| 599 | var d = halfSize*(dist+1); |
| 600 | var nx = data.x - d*Math.sin(angle); |
| 601 | var nz = data.z + d*Math.cos(angle); |
| 602 | |
| 603 | if (cmpTerrain.GetGroundLevel(nx, nz) < cmpWaterManager.GetWaterLevel(nx, nz)) |
| 604 | { |
| 605 | waterPoints.push(i); |
| 606 | } |
| 607 | } |
| 608 | var consec = []; |
| 609 | var length = waterPoints.length; |
| 610 | for (var i = 0; i < length; ++i) |
| 611 | { |
| 612 | var count = 0; |
| 613 | for (var j = 0; j < (length-1); ++j) |
| 614 | { |
| 615 | if (((waterPoints[(i + j) % length]+1) % numPoints) == waterPoints[(i + j + 1) % length]) |
| 616 | { |
| 617 | ++count; |
| 618 | } |
| 619 | else |
| 620 | { |
| 621 | break; |
| 622 | } |
| 623 | } |
| 624 | consec[i] = count; |
| 625 | } |
| 626 | var start = 0; |
| 627 | var count = 0; |
| 628 | for (var c in consec) |
| 629 | { |
| 630 | if (consec[c] > count) |
| 631 | { |
| 632 | start = c; |
| 633 | count = consec[c]; |
| 634 | } |
| 635 | } |
| 636 | |
| 637 | // If we've found a shoreline, stop searching |
| 638 | if (count != numPoints-1) |
| 639 | { |
| 640 | return {"x": data.x, "z": data.z, "angle": -(((waterPoints[start] + consec[start]/2) % numPoints)/numPoints*2*Math.PI)}; |
| 641 | } |
| 642 | } |
| 643 | } |
| 644 | |
| 645 | return false; |
| 646 | }; |
| 647 | |
| 648 | GuiInterface.prototype.PlaySound = function(player, data) |
| 649 | { |
| 650 | // Ignore if no entity was passed |
| 651 | if (!data.entity) |
| 652 | return; |
| 653 | |
| 654 | PlaySound(data.name, data.entity); |
| 655 | }; |
| 656 | |
| 657 | function isIdleUnit(ent, idleClass) |
| 658 | { |
| 659 | var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); |
| 660 | var cmpIdentity = Engine.QueryInterface(ent, IID_Identity); |
| 661 | |
| 662 | // TODO: Do something with garrisoned idle units |
| 663 | return (cmpUnitAI && cmpIdentity && cmpUnitAI.IsIdle() && !cmpUnitAI.IsGarrisoned() && idleClass && cmpIdentity.HasClass(idleClass)); |
| 664 | } |
| 665 | |
| 666 | GuiInterface.prototype.FindIdleUnit = function(player, data) |
| 667 | { |
| 668 | var rangeMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); |
| 669 | var playerEntities = rangeMan.GetEntitiesByPlayer(player); |
| 670 | |
| 671 | // Find the first matching entity that is after the previous selection, |
| 672 | // so that we cycle around in a predictable order |
| 673 | for each (var ent in playerEntities) |
| 674 | { |
| 675 | if (ent > data.prevUnit && isIdleUnit(ent, data.idleClass)) |
| 676 | return ent; |
| 677 | } |
| 678 | |
| 679 | // No idle entities left in the class |
| 680 | return 0; |
| 681 | }; |
| 682 | |
| 683 | GuiInterface.prototype.SetPathfinderDebugOverlay = function(player, enabled) |
| 684 | { |
| 685 | var cmpPathfinder = Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder); |
| 686 | cmpPathfinder.SetDebugOverlay(enabled); |
| 687 | }; |
| 688 | |
| 689 | GuiInterface.prototype.SetObstructionDebugOverlay = function(player, enabled) |
| 690 | { |
| 691 | var cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager); |
| 692 | cmpObstructionManager.SetDebugOverlay(enabled); |
| 693 | }; |
| 694 | |
| 695 | GuiInterface.prototype.SetMotionDebugOverlay = function(player, data) |
| 696 | { |
| 697 | for each (var ent in data.entities) |
| 698 | { |
| 699 | var cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion); |
| 700 | if (cmpUnitMotion) |
| 701 | cmpUnitMotion.SetDebugOverlay(data.enabled); |
| 702 | } |
| 703 | }; |
| 704 | |
| 705 | GuiInterface.prototype.SetRangeDebugOverlay = function(player, enabled) |
| 706 | { |
| 707 | var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); |
| 708 | cmpRangeManager.SetDebugOverlay(enabled); |
| 709 | }; |
| 710 | |
| 711 | GuiInterface.prototype.OnGlobalEntityRenamed = function(msg) |
| 712 | { |
| 713 | this.renamedEntities.push(msg); |
| 714 | } |
| 715 | |
| 716 | // List the GuiInterface functions that can be safely called by GUI scripts. |
| 717 | // (GUI scripts are non-deterministic and untrusted, so these functions must be |
| 718 | // appropriately careful. They are called with a first argument "player", which is |
| 719 | // trusted and indicates the player associated with the current client; no data should |
| 720 | // be returned unless this player is meant to be able to see it.) |
| 721 | var exposedFunctions = { |
| 722 | |
| 723 | "GetSimulationState": 1, |
| 724 | "GetExtendedSimulationState": 1, |
| 725 | "GetRenamedEntities": 1, |
| 726 | "ClearRenamedEntities": 1, |
| 727 | "GetEntityState": 1, |
| 728 | "GetTemplateData": 1, |
| 729 | "GetNextNotification": 1, |
| 730 | |
| 731 | "CanMoveEntsIntoFormation": 1, |
| 732 | "IsStanceSelected": 1, |
| 733 | |
| 734 | "SetSelectionHighlight": 1, |
| 735 | "SetStatusBars": 1, |
| 736 | "DisplayRallyPoint": 1, |
| 737 | "SetBuildingPlacementPreview": 1, |
| 738 | "GetFoundationSnapData": 1, |
| 739 | "PlaySound": 1, |
| 740 | "FindIdleUnit": 1, |
| 741 | |
| 742 | "SetPathfinderDebugOverlay": 1, |
| 743 | "SetObstructionDebugOverlay": 1, |
| 744 | "SetMotionDebugOverlay": 1, |
| 745 | "SetRangeDebugOverlay": 1, |
| 746 | }; |
| 747 | |
| 748 | GuiInterface.prototype.ScriptCall = function(player, name, args) |
| 749 | { |
| 750 | if (exposedFunctions[name]) |
| 751 | return this[name](player, args); |
| 752 | else |
| 753 | throw new Error("Invalid GuiInterface Call name \""+name+"\""); |
| 754 | }; |
| 755 | |
| 756 | Engine.RegisterComponentType(IID_GuiInterface, "GuiInterface", GuiInterface); |