1 /* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */
2
3
4 #include "AirCAI.h"
5 #include "Game/GameHelper.h"
6 #include "Game/GlobalUnsynced.h"
7 #include "Game/SelectedUnitsHandler.h"
8 #include "Map/Ground.h"
9 #include "Sim/Misc/ModInfo.h"
10 #include "Sim/Misc/TeamHandler.h"
11 #include "Sim/MoveTypes/StrafeAirMoveType.h"
12 #include "Sim/Units/Unit.h"
13 #include "Sim/Units/UnitDef.h"
14 #include "Sim/Units/UnitHandler.h"
15 #include "Sim/Units/Groups/Group.h"
16 #include "Sim/Weapons/Weapon.h"
17 #include "Sim/Weapons/WeaponDefHandler.h"
18 #include "System/myMath.h"
19 #include "System/Log/ILog.h"
20 #include "System/Util.h"
21
22 #include <cassert>
23 #define AUTO_GENERATE_ATTACK_ORDERS 1
24
25 // AirCAI is always assigned to StrafeAirMoveType aircraft
GetStrafeAirMoveType(const CUnit * owner)26 static CStrafeAirMoveType* GetStrafeAirMoveType(const CUnit* owner) {
27 assert(owner->unitDef->IsAirUnit());
28
29 if (owner->UsingScriptMoveType()) {
30 return static_cast<CStrafeAirMoveType*>(owner->prevMoveType);
31 }
32
33 return static_cast<CStrafeAirMoveType*>(owner->moveType);
34 }
35
36
37
38 CR_BIND_DERIVED(CAirCAI, CMobileCAI, )
39 CR_REG_METADATA(CAirCAI, (
40 CR_MEMBER(basePos),
41 CR_MEMBER(baseDir),
42
43 CR_MEMBER(activeCommand),
44 CR_MEMBER(targetAge),
45
46 CR_MEMBER(lastPC1),
47 CR_MEMBER(lastPC2),
48 CR_RESERVED(16)
49 ))
50
CAirCAI()51 CAirCAI::CAirCAI()
52 : CMobileCAI()
53 , activeCommand(0)
54 , targetAge(0)
55 , lastPC1(-1)
56 , lastPC2(-1)
57 {}
58
CAirCAI(CUnit * owner)59 CAirCAI::CAirCAI(CUnit* owner)
60 : CMobileCAI(owner)
61 , activeCommand(0)
62 , targetAge(0)
63 , lastPC1(-1)
64 , lastPC2(-1)
65 {
66 cancelDistance = 16000;
67 CommandDescription c;
68
69 if (owner->unitDef->canAttack) {
70 c.id = CMD_AREA_ATTACK;
71 c.action = "areaattack";
72 c.type = CMDTYPE_ICON_AREA;
73 c.name = "Area attack";
74 c.mouseicon = c.name;
75 c.tooltip = "Sets the aircraft to attack enemy units within a circle";
76 possibleCommands.push_back(c);
77 }
78
79 basePos = owner->pos;
80 goalPos = owner->pos;
81 }
82
GiveCommandReal(const Command & c,bool fromSynced)83 void CAirCAI::GiveCommandReal(const Command& c, bool fromSynced)
84 {
85 // take care not to allow aircraft to be ordered to move out of the map
86 if ((c.GetID() != CMD_MOVE) && !AllowedCommand(c, true)) {
87 return;
88 } else if (c.GetID() == CMD_MOVE && c.params.size() >= 3 &&
89 (c.params[0] < 0.0f || c.params[2] < 0.0f
90 || c.params[0] > gs->mapx*SQUARE_SIZE
91 || c.params[2] > gs->mapy*SQUARE_SIZE))
92 {
93 return;
94 }
95
96 if (c.GetID() == CMD_SET_WANTED_MAX_SPEED) {
97 return;
98 }
99
100 {
101 CStrafeAirMoveType* airMT = GetStrafeAirMoveType(owner);
102
103 if (c.GetID() == CMD_AUTOREPAIRLEVEL) {
104 if (c.params.empty())
105 return;
106
107 switch ((int) c.params[0]) {
108 case 0: { airMT->SetRepairBelowHealth(0.0f); break; }
109 case 1: { airMT->SetRepairBelowHealth(0.3f); break; }
110 case 2: { airMT->SetRepairBelowHealth(0.5f); break; }
111 case 3: { airMT->SetRepairBelowHealth(0.8f); break; }
112 }
113
114 for (unsigned int n = 0; n < possibleCommands.size(); n++) {
115 if (possibleCommands[n].id != CMD_AUTOREPAIRLEVEL)
116 continue;
117
118 possibleCommands[n].params[0] = IntToString(int(c.params[0]), "%d");
119 break;
120 }
121
122 selectedUnitsHandler.PossibleCommandChange(owner);
123 return;
124 }
125
126 if (c.GetID() == CMD_IDLEMODE) {
127 if (c.params.empty())
128 return;
129
130 switch ((int) c.params[0]) {
131 case 0: { airMT->autoLand = false; break; }
132 case 1: { airMT->autoLand = true; break; }
133 }
134
135 for (unsigned int n = 0; n < possibleCommands.size(); n++) {
136 if (possibleCommands[n].id != CMD_IDLEMODE)
137 continue;
138
139 possibleCommands[n].params[0] = IntToString(int(c.params[0]), "%d");
140 break;
141 }
142
143 selectedUnitsHandler.PossibleCommandChange(owner);
144 return;
145 }
146 }
147
148 if (!(c.options & SHIFT_KEY)
149 && nonQueingCommands.find(c.GetID()) == nonQueingCommands.end())
150 {
151 activeCommand = 0;
152 tempOrder = false;
153 }
154
155 if (c.GetID() == CMD_AREA_ATTACK && c.params.size() < 4) {
156 Command c2(CMD_ATTACK, c.options);
157 c2.params = c.params;
158 CCommandAI::GiveAllowedCommand(c2);
159 return;
160 }
161
162 CCommandAI::GiveAllowedCommand(c);
163 }
164
SlowUpdate()165 void CAirCAI::SlowUpdate()
166 {
167 // Commands issued may invoke SlowUpdate when paused
168 if (gs->paused)
169 return;
170
171 if (!commandQue.empty() && (commandQue.front().timeOut < gs->frameNum)) {
172 FinishCommand();
173 return;
174 }
175
176 // avoid the invalid (CStrafeAirMoveType*) cast
177 if (owner->UsingScriptMoveType())
178 return;
179
180 const bool wantToRefuel = (LandRepairIfNeeded() || RefuelIfNeeded());
181
182 #if (AUTO_GENERATE_ATTACK_ORDERS == 1)
183 if (commandQue.empty()) {
184 if (!AirAutoGenerateTarget(GetStrafeAirMoveType(owner))) {
185 // if no target found, queue is still empty so bail now
186 return;
187 }
188 }
189 #endif
190
191 // FIXME: check owner->UsingScriptMoveType() and skip rest if true?
192 AAirMoveType* myPlane = GetStrafeAirMoveType(owner);
193 Command& c = commandQue.front();
194
195 if (c.GetID() == CMD_WAIT) {
196 if ((myPlane->aircraftState == AAirMoveType::AIRCRAFT_FLYING)
197 && !owner->unitDef->DontLand() && myPlane->autoLand)
198 {
199 StopMove();
200 }
201 return;
202 }
203
204 if (c.GetID() != CMD_STOP && c.GetID() != CMD_AUTOREPAIRLEVEL &&
205 c.GetID() != CMD_IDLEMODE && c.GetID() != CMD_SET_WANTED_MAX_SPEED)
206 {
207 myPlane->Takeoff();
208 }
209
210 if (wantToRefuel) {
211 switch (c.GetID()) {
212 case CMD_AREA_ATTACK:
213 case CMD_ATTACK:
214 case CMD_FIGHT:
215 return;
216 }
217 }
218
219 switch (c.GetID()) {
220 case CMD_AREA_ATTACK: {
221 ExecuteAreaAttack(c);
222 return;
223 }
224 default: {
225 CMobileCAI::Execute();
226 return;
227 }
228 }
229 }
230
AirAutoGenerateTarget(AAirMoveType * myPlane)231 bool CAirCAI::AirAutoGenerateTarget(AAirMoveType* myPlane) {
232 assert(commandQue.empty());
233 assert(myPlane->owner == owner);
234
235 const UnitDef* ownerDef = owner->unitDef;
236 const bool autoLand = !ownerDef->DontLand() && myPlane->autoLand;
237 const bool autoAttack = ((owner->fireState >= FIRESTATE_FIREATWILL) && (owner->moveState != MOVESTATE_HOLDPOS));
238
239 if (myPlane->aircraftState == AAirMoveType::AIRCRAFT_FLYING && autoLand) {
240 StopMove();
241 }
242
243 if (ownerDef->canAttack && autoAttack && owner->maxRange > 0) {
244 if (ownerDef->IsFighterAirUnit()) {
245 const float3 P = owner->pos + (owner->speed * 10.0);
246 const float R = 1000.0f * owner->moveState;
247 const CUnit* enemy = CGameHelper::GetClosestEnemyAircraft(NULL, P, R, owner->allyteam);
248
249 if (IsValidTarget(enemy)) {
250 Command nc(CMD_ATTACK, INTERNAL_ORDER, enemy->id);
251 commandQue.push_front(nc);
252 inCommand = false;
253 return true;
254 }
255 } else {
256 const float3 P = owner->pos + (owner->speed * 20.0f);
257 const float R = 500.0f * owner->moveState;
258 const CUnit* enemy = CGameHelper::GetClosestValidTarget(P, R, owner->allyteam, this);
259
260 if (enemy != NULL) {
261 Command nc(CMD_ATTACK, INTERNAL_ORDER, enemy->id);
262 commandQue.push_front(nc);
263 inCommand = false;
264 return true;
265 }
266 }
267 }
268
269 return false;
270 }
271
272
273
ExecuteFight(Command & c)274 void CAirCAI::ExecuteFight(Command& c)
275 {
276 assert((c.options & INTERNAL_ORDER) || owner->unitDef->canFight);
277
278 // FIXME: check owner->UsingScriptMoveType() and skip rest if true?
279 AAirMoveType* myPlane = GetStrafeAirMoveType(owner);
280
281 assert(owner == myPlane->owner);
282
283 if (tempOrder) {
284 tempOrder = false;
285 inCommand = true;
286 }
287
288 if (c.params.size() < 3) {
289 LOG_L(L_ERROR, "[ACAI::%s][f=%d][id=%d] CMD_FIGHT #params < 3", __FUNCTION__, gs->frameNum, owner->id);
290 return;
291 }
292
293 if (c.params.size() >= 6) {
294 if (!inCommand) {
295 commandPos1 = c.GetPos(3);
296 }
297 } else {
298 // HACK to make sure the line (commandPos1,commandPos2) is NOT
299 // rotated (only shortened) if we reach this because the previous return
300 // fight command finished by the 'if((curPos-pos).SqLength2D()<(127*127)){'
301 // condition, but is actually updated correctly if you click somewhere
302 // outside the area close to the line (for a new command).
303 commandPos1 = ClosestPointOnLine(commandPos1, commandPos2, owner->pos);
304
305 if ((owner->pos - commandPos1).SqLength2D() > (150 * 150)) {
306 commandPos1 = owner->pos;
307 }
308 }
309
310 goalPos = c.GetPos(0);
311
312 if (!inCommand) {
313 inCommand = true;
314 commandPos2 = goalPos;
315 }
316 if (c.params.size() >= 6) {
317 goalPos = ClosestPointOnLine(commandPos1, commandPos2, owner->pos);
318 }
319
320 // CMD_FIGHT is pretty useless if !canAttack, but we try to honour the modders wishes anyway...
321 if (owner->unitDef->canAttack && (owner->fireState >= FIRESTATE_FIREATWILL)
322 && (owner->moveState != MOVESTATE_HOLDPOS) && (owner->maxRange > 0))
323 {
324 CUnit* enemy = NULL;
325
326 if (owner->unitDef->IsFighterAirUnit()) {
327 const float3 P = ClosestPointOnLine(commandPos1, commandPos2, owner->pos + owner->speed*10);
328 const float R = 1000.0f * owner->moveState;
329
330 enemy = CGameHelper::GetClosestEnemyAircraft(NULL, P, R, owner->allyteam);
331 }
332 if (IsValidTarget(enemy) && (owner->moveState != MOVESTATE_MANEUVER
333 || LinePointDist(commandPos1, commandPos2, enemy->pos) < 1000))
334 {
335 // make the attack-command inherit <c>'s options
336 // (if <c> is internal, then so are the attacks)
337 //
338 // this is needed because CWeapon code will not
339 // fire on "internal" targets if the weapon has
340 // noAutoTarget set (although the <enemy> CUnit*
341 // is technically not a user-target, we treat it
342 // as such) even when explicitly told to fight
343 Command nc(CMD_ATTACK, c.options, enemy->id);
344 commandQue.push_front(nc);
345
346 tempOrder = true;
347 inCommand = false;
348
349 if (lastPC1 != gs->frameNum) { // avoid infinite loops
350 lastPC1 = gs->frameNum;
351 SlowUpdate();
352 }
353 return;
354 } else {
355 const float3 P = ClosestPointOnLine(commandPos1, commandPos2, owner->pos + owner->speed * 20);
356 const float R = 500.0f * owner->moveState;
357
358 enemy = CGameHelper::GetClosestValidTarget(P, R, owner->allyteam, this);
359
360 if (enemy != NULL) {
361 PushOrUpdateReturnFight();
362
363 // make the attack-command inherit <c>'s options
364 Command nc(CMD_ATTACK, c.options, enemy->id);
365 commandQue.push_front(nc);
366
367 tempOrder = true;
368 inCommand = false;
369
370 // avoid infinite loops (?)
371 if (lastPC2 != gs->frameNum) {
372 lastPC2 = gs->frameNum;
373 SlowUpdate();
374 }
375 return;
376 }
377 }
378 }
379
380 myPlane->goalPos = goalPos;
381
382 const CStrafeAirMoveType* airMT = (!owner->UsingScriptMoveType())? static_cast<const CStrafeAirMoveType*>(myPlane): NULL;
383 const float radius = (airMT != NULL)? std::max(airMT->turnRadius + 2*SQUARE_SIZE, 128.f) : 127.f;
384
385 // we're either circling or will get to the target in 8 frames
386 if ((owner->pos - goalPos).SqLength2D() < (radius * radius)
387 || (owner->pos + owner->speed*8 - goalPos).SqLength2D() < 127*127)
388 {
389 FinishCommand();
390 }
391 }
392
ExecuteAttack(Command & c)393 void CAirCAI::ExecuteAttack(Command& c)
394 {
395 assert(owner->unitDef->canAttack);
396 targetAge++;
397
398 if (tempOrder && owner->moveState == MOVESTATE_MANEUVER) {
399 // limit how far away we fly
400 if (orderTarget && LinePointDist(commandPos1, commandPos2, orderTarget->pos) > 1500) {
401 owner->AttackUnit(NULL, false, false);
402 FinishCommand();
403 return;
404 }
405 }
406
407 if (inCommand) {
408 if (targetDied || (c.params.size() == 1 && UpdateTargetLostTimer(int(c.params[0])) == 0)) {
409 FinishCommand();
410 return;
411 }
412 if (orderTarget != NULL) {
413 if (orderTarget->unitDef->canfly && orderTarget->IsCrashing()) {
414 owner->AttackUnit(NULL, false, false);
415 FinishCommand();
416 return;
417 }
418 if (!(c.options & ALT_KEY) && SkipParalyzeTarget(orderTarget)) {
419 owner->AttackUnit(NULL, false, false);
420 FinishCommand();
421 return;
422 }
423 }
424 } else {
425 targetAge = 0;
426
427 if (c.params.size() == 1) {
428 CUnit* targetUnit = unitHandler->GetUnit(c.params[0]);
429
430 if (targetUnit == NULL) { FinishCommand(); return; }
431 if (targetUnit == owner) { FinishCommand(); return; }
432 if (targetUnit->GetTransporter() != NULL && !modInfo.targetableTransportedUnits) {
433 FinishCommand(); return;
434 }
435
436 SetGoal(targetUnit->pos, owner->pos, cancelDistance);
437 SetOrderTarget(targetUnit);
438 owner->AttackUnit(targetUnit, (c.options & INTERNAL_ORDER) == 0, false);
439
440 inCommand = true;
441 } else {
442 SetGoal(c.GetPos(0), owner->pos, cancelDistance);
443 owner->AttackGround(c.GetPos(0), (c.options & INTERNAL_ORDER) == 0, false);
444
445 inCommand = true;
446 }
447 }
448 }
449
ExecuteAreaAttack(Command & c)450 void CAirCAI::ExecuteAreaAttack(Command& c)
451 {
452 assert(owner->unitDef->canAttack);
453
454 // FIXME: check owner->UsingScriptMoveType() and skip rest if true?
455 AAirMoveType* myPlane = GetStrafeAirMoveType(owner);
456
457 if (targetDied) {
458 targetDied = false;
459 inCommand = false;
460 }
461
462 const float3& pos = c.GetPos(0);
463 const float radius = c.params[3];
464
465 if (inCommand) {
466 if (myPlane->aircraftState == AAirMoveType::AIRCRAFT_LANDED)
467 inCommand = false;
468
469 if (orderTarget && orderTarget->pos.SqDistance2D(pos) > Square(radius)) {
470 inCommand = false;
471
472 // target wandered out of the attack-area
473 SetOrderTarget(NULL);
474 SelectNewAreaAttackTargetOrPos(c);
475 }
476 } else {
477 if (myPlane->aircraftState != AAirMoveType::AIRCRAFT_LANDED) {
478 inCommand = true;
479
480 SelectNewAreaAttackTargetOrPos(c);
481 }
482 }
483 }
484
ExecuteGuard(Command & c)485 void CAirCAI::ExecuteGuard(Command& c)
486 {
487 assert(owner->unitDef->canGuard);
488
489 const CUnit* guardee = unitHandler->GetUnit(c.params[0]);
490
491 if (guardee == NULL) { FinishCommand(); return; }
492 if (UpdateTargetLostTimer(guardee->id) == 0) { FinishCommand(); return; }
493 if (guardee->outOfMapTime > (GAME_SPEED * 5)) { FinishCommand(); return; }
494
495 const bool pushAttackCommand =
496 (owner->maxRange > 0.0f) &&
497 owner->unitDef->canAttack &&
498 ((guardee->lastAttackFrame + 40) < gs->frameNum) &&
499 IsValidTarget(guardee->lastAttacker);
500
501 if (pushAttackCommand) {
502 Command nc(CMD_ATTACK, c.options | INTERNAL_ORDER, guardee->lastAttacker->id);
503 commandQue.push_front(nc);
504 SlowUpdate();
505 } else {
506 Command c2(CMD_MOVE, c.options | INTERNAL_ORDER);
507 c2.timeOut = gs->frameNum + 60;
508
509 if (guardee->pos.IsInBounds()) {
510 c2.PushPos(guardee->pos);
511 } else {
512 float3 clampedGuardeePos = guardee->pos;
513
514 clampedGuardeePos.ClampInBounds();
515
516 c2.PushPos(clampedGuardeePos);
517 }
518
519 commandQue.push_front(c2);
520 }
521 }
522
GetDefaultCmd(const CUnit * pointed,const CFeature * feature)523 int CAirCAI::GetDefaultCmd(const CUnit* pointed, const CFeature* feature)
524 {
525 if (pointed) {
526 if (!teamHandler->Ally(gu->myAllyTeam, pointed->allyteam)) {
527 if (owner->unitDef->canAttack) {
528 return CMD_ATTACK;
529 }
530 } else {
531 if (owner->unitDef->canGuard) {
532 return CMD_GUARD;
533 }
534 }
535 }
536 return CMD_MOVE;
537 }
538
IsValidTarget(const CUnit * enemy) const539 bool CAirCAI::IsValidTarget(const CUnit* enemy) const {
540 if (!CMobileCAI::IsValidTarget(enemy)) return false;
541 if (enemy->IsCrashing()) return false;
542 return (GetStrafeAirMoveType(owner)->isFighter || !enemy->unitDef->canfly);
543 }
544
545
546
FinishCommand()547 void CAirCAI::FinishCommand()
548 {
549 targetAge = 0;
550 CCommandAI::FinishCommand();
551 }
552
BuggerOff(const float3 & pos,float radius)553 void CAirCAI::BuggerOff(const float3& pos, float radius)
554 {
555 if (!owner->UsingScriptMoveType()) {
556 static_cast<AAirMoveType*>(owner->moveType)->Takeoff();
557 } else {
558 CMobileCAI::BuggerOff(pos, radius);
559 }
560 }
561
SetGoal(const float3 & pos,const float3 & curPos,float goalRadius)562 void CAirCAI::SetGoal(const float3& pos, const float3& curPos, float goalRadius)
563 {
564 owner->moveType->SetGoal(pos);
565 CMobileCAI::SetGoal(pos, curPos, goalRadius);
566 }
567
SelectNewAreaAttackTargetOrPos(const Command & ac)568 bool CAirCAI::SelectNewAreaAttackTargetOrPos(const Command& ac) {
569 assert(ac.GetID() == CMD_AREA_ATTACK || (ac.GetID() == CMD_ATTACK && ac.GetParamsCount() >= 3));
570
571 if (ac.GetID() == CMD_ATTACK) {
572 FinishCommand();
573 return false;
574 }
575
576 const float3& pos = ac.GetPos(0);
577 const float radius = ac.params[3];
578
579 std::vector<int> enemyUnitIDs;
580 CGameHelper::GetEnemyUnits(pos, radius, owner->allyteam, enemyUnitIDs);
581
582 if (enemyUnitIDs.empty()) {
583 float3 attackPos = pos + (gs->randVector() * radius);
584 attackPos.y = CGround::GetHeightAboveWater(attackPos.x, attackPos.z);
585
586 owner->AttackGround(attackPos, (ac.options & INTERNAL_ORDER) == 0, false);
587 SetGoal(attackPos, owner->pos);
588 } else {
589 // note: the range of randFloat() is inclusive of 1.0f
590 const unsigned int unitIdx = std::min<int>(gs->randFloat() * enemyUnitIDs.size(), enemyUnitIDs.size() - 1);
591 const unsigned int unitID = enemyUnitIDs[unitIdx];
592
593 CUnit* targetUnit = unitHandler->GetUnitUnsafe(unitID);
594
595 SetOrderTarget(targetUnit);
596 owner->AttackUnit(targetUnit, (ac.options & INTERNAL_ORDER) == 0, false);
597 SetGoal(targetUnit->pos, owner->pos);
598 }
599
600 return true;
601 }
602
603