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