source: ps/trunk/binaries/data/mods/public/simulation/components/BuildingAI.js@ 25123

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

Technically seperate Turrets from GarrisonHolder.

While they often look alike, their behaviour is totally different.
This split has some implications:

  • There are now separate auras for garrisoning and turrets.
  • Entities can now have both turret points and garrison slots, independent of eachother.

In general previous behaviour is maintained as much as possible.

Differential revision: D3150
Comments by: @Nescio, @wraitii
Tested by: @v32itas

  • Property svn:eol-style set to native
File size: 11.0 KB
Line 
1// Number of rounds of firing per 2 seconds.
2const roundCount = 10;
3const attackType = "Ranged";
4
5function BuildingAI() {}
6
7BuildingAI.prototype.Schema =
8 "<element name='DefaultArrowCount'>" +
9 "<data type='nonNegativeInteger'/>" +
10 "</element>" +
11 "<optional>" +
12 "<element name='MaxArrowCount' a:help='Limit the number of arrows to a certain amount'>" +
13 "<data type='nonNegativeInteger'/>" +
14 "</element>" +
15 "</optional>" +
16 "<element name='GarrisonArrowMultiplier'>" +
17 "<ref name='nonNegativeDecimal'/>" +
18 "</element>" +
19 "<element name='GarrisonArrowClasses' a:help='Add extra arrows for this class list'>" +
20 "<text/>" +
21 "</element>";
22
23BuildingAI.prototype.MAX_PREFERENCE_BONUS = 2;
24
25BuildingAI.prototype.Init = function()
26{
27 this.currentRound = 0;
28 this.archersGarrisoned = 0;
29 this.arrowsLeft = 0;
30 this.targetUnits = [];
31};
32
33BuildingAI.prototype.OnGarrisonedUnitsChanged = function(msg)
34{
35 let classes = this.template.GarrisonArrowClasses;
36 for (let ent of msg.added)
37 {
38 let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
39 if (cmpIdentity && MatchesClassList(cmpIdentity.GetClassesList(), classes))
40 ++this.archersGarrisoned;
41 }
42 for (let ent of msg.removed)
43 {
44 let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
45 if (cmpIdentity && MatchesClassList(cmpIdentity.GetClassesList(), classes))
46 --this.archersGarrisoned;
47 }
48};
49
50BuildingAI.prototype.OnOwnershipChanged = function(msg)
51{
52 this.targetUnits = [];
53 this.SetupRangeQuery();
54 this.SetupGaiaRangeQuery();
55};
56
57BuildingAI.prototype.OnDiplomacyChanged = function(msg)
58{
59 if (!IsOwnedByPlayer(msg.player, this.entity))
60 return;
61
62 // Remove maybe now allied/neutral units.
63 this.targetUnits = [];
64 this.SetupRangeQuery();
65 this.SetupGaiaRangeQuery();
66};
67
68BuildingAI.prototype.OnDestroy = function()
69{
70 if (this.timer)
71 {
72 let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
73 cmpTimer.CancelTimer(this.timer);
74 this.timer = undefined;
75 }
76
77 // Clean up range queries.
78 let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
79 if (this.enemyUnitsQuery)
80 cmpRangeManager.DestroyActiveQuery(this.enemyUnitsQuery);
81 if (this.gaiaUnitsQuery)
82 cmpRangeManager.DestroyActiveQuery(this.gaiaUnitsQuery);
83};
84
85/**
86 * React on Attack value modifications, as it might influence the range.
87 */
88BuildingAI.prototype.OnValueModification = function(msg)
89{
90 if (msg.component != "Attack")
91 return;
92
93 this.targetUnits = [];
94 this.SetupRangeQuery();
95 this.SetupGaiaRangeQuery();
96};
97
98/**
99 * Setup the Range Query to detect units coming in & out of range.
100 */
101BuildingAI.prototype.SetupRangeQuery = function()
102{
103 var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
104 if (!cmpAttack)
105 return;
106
107 var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
108 if (this.enemyUnitsQuery)
109 {
110 cmpRangeManager.DestroyActiveQuery(this.enemyUnitsQuery);
111 this.enemyUnitsQuery = undefined;
112 }
113
114 var cmpPlayer = QueryOwnerInterface(this.entity);
115 if (!cmpPlayer)
116 return;
117
118 var enemies = cmpPlayer.GetEnemies();
119 // Remove gaia.
120 if (enemies.length && enemies[0] == 0)
121 enemies.shift();
122
123 if (!enemies.length)
124 return;
125
126 var range = cmpAttack.GetRange(attackType);
127 // This takes entity sizes into accounts, so no need to compensate for structure size.
128 this.enemyUnitsQuery = cmpRangeManager.CreateActiveParabolicQuery(
129 this.entity, range.min, range.max, range.elevationBonus,
130 enemies, IID_Resistance, cmpRangeManager.GetEntityFlagMask("normal"));
131
132 cmpRangeManager.EnableActiveQuery(this.enemyUnitsQuery);
133};
134
135// Set up a range query for Gaia units within LOS range which can be attacked.
136// This should be called whenever our ownership changes.
137BuildingAI.prototype.SetupGaiaRangeQuery = function()
138{
139 var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
140 if (!cmpAttack)
141 return;
142
143 var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
144 if (this.gaiaUnitsQuery)
145 {
146 cmpRangeManager.DestroyActiveQuery(this.gaiaUnitsQuery);
147 this.gaiaUnitsQuery = undefined;
148 }
149
150 var cmpPlayer = QueryOwnerInterface(this.entity);
151 if (!cmpPlayer || !cmpPlayer.IsEnemy(0))
152 return;
153
154 var range = cmpAttack.GetRange(attackType);
155
156 // This query is only interested in Gaia entities that can attack.
157 // This takes entity sizes into accounts, so no need to compensate for structure size.
158 this.gaiaUnitsQuery = cmpRangeManager.CreateActiveParabolicQuery(
159 this.entity, range.min, range.max, range.elevationBonus,
160 [0], IID_Attack, cmpRangeManager.GetEntityFlagMask("normal"));
161
162 cmpRangeManager.EnableActiveQuery(this.gaiaUnitsQuery);
163};
164
165/**
166 * Called when units enter or leave range.
167 */
168BuildingAI.prototype.OnRangeUpdate = function(msg)
169{
170
171 var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
172 if (!cmpAttack)
173 return;
174
175 // Target enemy units except non-dangerous animals.
176 if (msg.tag == this.gaiaUnitsQuery)
177 {
178 msg.added = msg.added.filter(e => {
179 let cmpUnitAI = Engine.QueryInterface(e, IID_UnitAI);
180 return cmpUnitAI && (!cmpUnitAI.IsAnimal() || cmpUnitAI.IsDangerousAnimal());
181 });
182 }
183 else if (msg.tag != this.enemyUnitsQuery)
184 return;
185
186 // Add new targets.
187 for (let entity of msg.added)
188 if (cmpAttack.CanAttack(entity))
189 this.targetUnits.push(entity);
190
191 // Remove targets outside of vision-range.
192 for (let entity of msg.removed)
193 {
194 let index = this.targetUnits.indexOf(entity);
195 if (index > -1)
196 this.targetUnits.splice(index, 1);
197 }
198
199 if (this.targetUnits.length)
200 this.StartTimer();
201};
202
203BuildingAI.prototype.StartTimer = function()
204{
205 if (this.timer)
206 return;
207
208 var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
209 if (!cmpAttack)
210 return;
211
212 var cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
213 var attackTimers = cmpAttack.GetTimers(attackType);
214
215 this.timer = cmpTimer.SetInterval(this.entity, IID_BuildingAI, "FireArrows",
216 attackTimers.prepare, attackTimers.repeat / roundCount, null);
217};
218
219BuildingAI.prototype.GetDefaultArrowCount = function()
220{
221 var arrowCount = +this.template.DefaultArrowCount;
222 return Math.round(ApplyValueModificationsToEntity("BuildingAI/DefaultArrowCount", arrowCount, this.entity));
223};
224
225BuildingAI.prototype.GetMaxArrowCount = function()
226{
227 if (!this.template.MaxArrowCount)
228 return Infinity;
229
230 let maxArrowCount = +this.template.MaxArrowCount;
231 return Math.round(ApplyValueModificationsToEntity("BuildingAI/MaxArrowCount", maxArrowCount, this.entity));
232};
233
234BuildingAI.prototype.GetGarrisonArrowMultiplier = function()
235{
236 var arrowMult = +this.template.GarrisonArrowMultiplier;
237 return ApplyValueModificationsToEntity("BuildingAI/GarrisonArrowMultiplier", arrowMult, this.entity);
238};
239
240BuildingAI.prototype.GetGarrisonArrowClasses = function()
241{
242 var string = this.template.GarrisonArrowClasses;
243 if (string)
244 return string.split(/\s+/);
245 return [];
246};
247
248/**
249 * Returns the number of arrows which needs to be fired.
250 * DefaultArrowCount + Garrisoned Archers (i.e., any unit capable
251 * of shooting arrows from inside buildings).
252 */
253BuildingAI.prototype.GetArrowCount = function()
254{
255 let count = this.GetDefaultArrowCount() +
256 Math.round(this.archersGarrisoned * this.GetGarrisonArrowMultiplier());
257
258 return Math.min(count, this.GetMaxArrowCount());
259};
260
261BuildingAI.prototype.SetUnitAITarget = function(ent)
262{
263 this.unitAITarget = ent;
264 if (ent)
265 this.StartTimer();
266};
267
268/**
269 * Fire arrows with random temporal distribution on prefered targets.
270 * Called 'roundCount' times every 'RepeatTime' seconds when there are units in the range.
271 */
272BuildingAI.prototype.FireArrows = function()
273{
274 if (!this.targetUnits.length && !this.unitAITarget)
275 {
276 if (!this.timer)
277 return;
278
279 let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
280 cmpTimer.CancelTimer(this.timer);
281 this.timer = undefined;
282 return;
283 }
284
285 let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack);
286 if (!cmpAttack)
287 return;
288
289 if (this.currentRound > roundCount - 1)
290 this.currentRound = 0;
291
292 if (this.currentRound == 0)
293 this.arrowsLeft = this.GetArrowCount();
294
295 let arrowsToFire = 0;
296 if (this.currentRound == roundCount - 1)
297 arrowsToFire = this.arrowsLeft;
298 else
299 arrowsToFire = Math.min(
300 randIntInclusive(0, 2 * this.GetArrowCount() / roundCount),
301 this.arrowsLeft
302 );
303
304 if (arrowsToFire <= 0)
305 {
306 ++this.currentRound;
307 return;
308 }
309
310 // Add targets to a weighted list, to allow preferences.
311 let targets = new WeightedList();
312 let maxPreference = this.MAX_PREFERENCE_BONUS;
313 let addTarget = function(target)
314 {
315 let preference = cmpAttack.GetPreference(target);
316 let weight = 1;
317
318 if (preference !== null && preference !== undefined)
319 weight += maxPreference / (1 + preference);
320
321 targets.push(target, weight);
322 };
323
324 // Add the UnitAI target separately, as the UnitMotion and RangeManager implementations differ.
325 if (this.unitAITarget && this.targetUnits.indexOf(this.unitAITarget) == -1)
326 addTarget(this.unitAITarget);
327 for (let target of this.targetUnits)
328 addTarget(target);
329
330 // The obstruction manager performs approximate range checks.
331 // so we need to verify them here.
332 // TODO: perhaps an optional 'precise' mode to range queries would be more performant.
333 let cmpObstructionManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager);
334 let range = cmpAttack.GetRange(attackType);
335
336 let thisCmpPosition = Engine.QueryInterface(this.entity, IID_Position);
337 if (!thisCmpPosition.IsInWorld())
338 return;
339 let s = thisCmpPosition.GetPosition();
340
341 let firedArrows = 0;
342 while (firedArrows < arrowsToFire && targets.length())
343 {
344 let selectedIndex = targets.randomIndex();
345 let selectedTarget = targets.itemAt(selectedIndex);
346
347 let targetCmpPosition = Engine.QueryInterface(selectedTarget, IID_Position);
348 if (targetCmpPosition && targetCmpPosition.IsInWorld() && this.CheckTargetVisible(selectedTarget))
349 {
350 // Parabolic range compuation is the same as in UnitAI's MoveToTargetAttackRange.
351 // h is positive when I'm higher than the target.
352 let h = s.y - targetCmpPosition.GetPosition().y + range.elevationBonus;
353 if (h > -range.max / 2 && cmpObstructionManager.IsInTargetRange(
354 this.entity,
355 selectedTarget,
356 range.min,
357 Math.sqrt(Math.square(range.max) + 2 * range.max * h), false))
358 {
359 cmpAttack.PerformAttack(attackType, selectedTarget);
360 PlaySound("attack_" + attackType.toLowerCase(), this.entity);
361 ++firedArrows;
362 continue;
363 }
364 }
365
366 // Could not attack target, try a different target.
367 targets.removeAt(selectedIndex);
368 }
369
370 this.arrowsLeft -= firedArrows;
371 ++this.currentRound;
372};
373
374/**
375 * Returns true if the target entity is visible through the FoW/SoD.
376 */
377BuildingAI.prototype.CheckTargetVisible = function(target)
378{
379 var cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
380 if (!cmpOwnership)
381 return false;
382
383 // Entities that are hidden and miraged are considered visible.
384 var cmpFogging = Engine.QueryInterface(target, IID_Fogging);
385 if (cmpFogging && cmpFogging.IsMiraged(cmpOwnership.GetOwner()))
386 return true;
387
388 // Either visible directly, or visible in fog.
389 let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
390 return cmpRangeManager.GetLosVisibility(target, cmpOwnership.GetOwner()) != "hidden";
391};
392
393Engine.RegisterComponentType(IID_BuildingAI, "BuildingAI", BuildingAI);
Note: See TracBrowser for help on using the repository browser.