1 /* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */
2
3
4 #include "MobileCAI.h"
5 #include "TransportCAI.h"
6 #include "ExternalAI/EngineOutHandler.h"
7 #include "Game/GameHelper.h"
8 #include "Game/GlobalUnsynced.h"
9 #include "Game/SelectedUnitsHandler.h"
10 #include "Map/Ground.h"
11 #include "Sim/Misc/AirBaseHandler.h"
12 #include "Sim/Misc/LosHandler.h"
13 #include "Sim/Misc/ModInfo.h"
14 #include "Sim/Misc/TeamHandler.h"
15 #include "Sim/MoveTypes/AAirMoveType.h"
16 #include "Sim/Units/UnitDef.h"
17 #include "Sim/Units/Unit.h"
18 #include "Sim/Units/UnitHandler.h"
19 #include "Sim/Units/Groups/Group.h"
20 #include "Sim/Units/UnitTypes/TransportUnit.h"
21 #include "Sim/Weapons/Weapon.h"
22 #include "Sim/Weapons/WeaponDef.h"
23 #include "System/Log/ILog.h"
24 #include "System/myMath.h"
25 #include "System/Util.h"
26 #include <assert.h>
27
28 #define AUTO_GENERATE_ATTACK_ORDERS 1
29 #define BUGGER_OFF_TTL 200
30 #define MAX_CLOSE_IN_RETRY_TICKS 30
31 #define MAX_USERGOAL_TOLERANCE_DIST 100.0f
32
33 // MobileCAI is not always assigned to aircraft
GetAirMoveType(const CUnit * owner)34 static AAirMoveType* GetAirMoveType(const CUnit* owner) {
35 assert(owner->unitDef->IsAirUnit());
36
37 if (owner->UsingScriptMoveType()) {
38 return static_cast<AAirMoveType*>(owner->prevMoveType);
39 }
40
41 return static_cast<AAirMoveType*>(owner->moveType);
42 }
43
44
45
46 CR_BIND_DERIVED(CMobileCAI ,CCommandAI , )
47 CR_REG_METADATA(CMobileCAI, (
48 CR_MEMBER(goalPos),
49 CR_MEMBER(goalRadius),
50 CR_MEMBER(lastBuggerGoalPos),
51 CR_MEMBER(lastUserGoal),
52
53 CR_MEMBER(lastIdleCheck),
54 CR_MEMBER(tempOrder),
55
56 CR_MEMBER(lastPC),
57
58 CR_MEMBER(lastBuggerOffTime),
59 CR_MEMBER(buggerOffPos),
60 CR_MEMBER(buggerOffRadius),
61
62 CR_MEMBER(commandPos1),
63 CR_MEMBER(commandPos2),
64
65 CR_MEMBER(lastCloseInTry),
66
67 CR_MEMBER(cancelDistance),
68 CR_MEMBER(slowGuard),
69 CR_MEMBER(moveDir),
70 CR_RESERVED(16)
71 ))
72
CMobileCAI()73 CMobileCAI::CMobileCAI():
74 CCommandAI(),
75 goalPos(-1,-1,-1),
76 goalRadius(0.0f),
77 lastBuggerGoalPos(-1,0,-1),
78 lastUserGoal(ZeroVector),
79 lastIdleCheck(0),
80 tempOrder(false),
81 lastPC(-1),
82 lastBuggerOffTime(-BUGGER_OFF_TTL),
83 buggerOffPos(ZeroVector),
84 buggerOffRadius(0),
85 commandPos1(ZeroVector),
86 commandPos2(ZeroVector),
87 cancelDistance(1024),
88 lastCloseInTry(-1),
89 slowGuard(false),
90 moveDir(gs->randFloat() > 0.5)
91 {}
92
93
CMobileCAI(CUnit * owner)94 CMobileCAI::CMobileCAI(CUnit* owner):
95 CCommandAI(owner),
96 goalPos(-1,-1,-1),
97 goalRadius(0.0f),
98 lastBuggerGoalPos(-1,0,-1),
99 lastUserGoal(owner->pos),
100 lastIdleCheck(0),
101 tempOrder(false),
102 lastPC(-1),
103 lastBuggerOffTime(-BUGGER_OFF_TTL),
104 buggerOffPos(ZeroVector),
105 buggerOffRadius(0),
106 commandPos1(ZeroVector),
107 commandPos2(ZeroVector),
108 cancelDistance(1024),
109 lastCloseInTry(-1),
110 slowGuard(false),
111 moveDir(gs->randFloat() > 0.5)
112 {
113 CalculateCancelDistance();
114
115 CommandDescription c;
116
117 c.id=CMD_LOAD_ONTO;
118 c.action="loadonto";
119 c.type=CMDTYPE_ICON_UNIT;
120 c.name="Load units";
121 c.mouseicon=c.name;
122 c.tooltip="Sets the unit to load itself onto a transport";
123 c.hidden = true;
124 possibleCommands.push_back(c);
125 c.hidden = false;
126
127 if (owner->unitDef->canmove) {
128 c.id=CMD_MOVE;
129 c.action="move";
130 c.type=CMDTYPE_ICON_FRONT;
131 c.name="Move";
132 c.mouseicon=c.name;
133 c.tooltip="Move: Order the unit to move to a position";
134 c.params.push_back("1000000"); // max distance
135 possibleCommands.push_back(c);
136 c.params.clear();
137 }
138
139 if (owner->unitDef->canPatrol) {
140 c.id=CMD_PATROL;
141 c.action="patrol";
142 c.type=CMDTYPE_ICON_MAP;
143 c.name="Patrol";
144 c.mouseicon=c.name;
145 c.tooltip="Patrol: Order the unit to patrol to one or more waypoints";
146 possibleCommands.push_back(c);
147 c.params.clear();
148 }
149
150 if (owner->unitDef->canFight) {
151 c.id = CMD_FIGHT;
152 c.action="fight";
153 c.type = CMDTYPE_ICON_FRONT;
154 c.name = "Fight";
155 c.mouseicon=c.name;
156 c.tooltip = "Fight: Order the unit to take action while moving to a position";
157 possibleCommands.push_back(c);
158 }
159
160 if (owner->unitDef->canGuard) {
161 c.id=CMD_GUARD;
162 c.action="guard";
163 c.type=CMDTYPE_ICON_UNIT;
164 c.name="Guard";
165 c.mouseicon=c.name;
166 c.tooltip="Guard: Order a unit to guard another unit and attack units attacking it";
167 possibleCommands.push_back(c);
168 }
169
170 if (owner->unitDef->canfly) {
171 c.params.clear();
172 c.id=CMD_AUTOREPAIRLEVEL;
173 c.action="autorepairlevel";
174 c.type=CMDTYPE_ICON_MODE;
175 c.name="Repair level";
176 c.mouseicon=c.name;
177 c.params.push_back("1");
178 c.params.push_back("LandAt 0");
179 c.params.push_back("LandAt 30");
180 c.params.push_back("LandAt 50");
181 c.params.push_back("LandAt 80");
182 c.tooltip=
183 "Repair level: Sets at which health level an aircraft will try to find a repair pad";
184 possibleCommands.push_back(c);
185 nonQueingCommands.insert(CMD_AUTOREPAIRLEVEL);
186
187 c.params.clear();
188 c.id=CMD_IDLEMODE;
189 c.action="idlemode";
190 c.type=CMDTYPE_ICON_MODE;
191 c.name="Land mode";
192 c.mouseicon=c.name;
193 c.params.push_back("1");
194 c.params.push_back(" Fly ");
195 c.params.push_back("Land");
196 c.tooltip="Land mode: Sets what aircraft will do on idle";
197 possibleCommands.push_back(c);
198 nonQueingCommands.insert(CMD_IDLEMODE);
199 }
200
201 nonQueingCommands.insert(CMD_SET_WANTED_MAX_SPEED);
202 }
203
204
205
GiveCommandReal(const Command & c,bool fromSynced)206 void CMobileCAI::GiveCommandReal(const Command& c, bool fromSynced)
207 {
208 if (!AllowedCommand(c, fromSynced))
209 return;
210
211 if (owner->unitDef->IsAirUnit()) {
212 AAirMoveType* airMT = GetAirMoveType(owner);
213
214 if (c.GetID() == CMD_AUTOREPAIRLEVEL) {
215 if (c.params.empty())
216 return;
217
218 switch ((int) c.params[0]) {
219 case 0: { airMT->SetRepairBelowHealth(0.0f); break; }
220 case 1: { airMT->SetRepairBelowHealth(0.3f); break; }
221 case 2: { airMT->SetRepairBelowHealth(0.5f); break; }
222 case 3: { airMT->SetRepairBelowHealth(0.8f); break; }
223 }
224
225 for (unsigned int n = 0; n < possibleCommands.size(); n++) {
226 if (possibleCommands[n].id != CMD_AUTOREPAIRLEVEL)
227 continue;
228
229 possibleCommands[n].params[0] = IntToString(int(c.params[0]), "%d");
230 break;
231 }
232
233 selectedUnitsHandler.PossibleCommandChange(owner);
234 return;
235 }
236
237 if (c.GetID() == CMD_IDLEMODE) {
238 if (c.params.empty())
239 return;
240
241 // toggle between the "land" and "fly" idle-modes
242 switch ((int) c.params[0]) {
243 case 0: { airMT->autoLand = false; break; }
244 case 1: { airMT->autoLand = true; break; }
245 }
246
247 if (!airMT->owner->beingBuilt) {
248 if (!airMT->autoLand)
249 airMT->Takeoff();
250 else
251 airMT->Land();
252 }
253
254 for (unsigned int n = 0; n < possibleCommands.size(); n++) {
255 if (possibleCommands[n].id != CMD_IDLEMODE)
256 continue;
257
258 possibleCommands[n].params[0] = IntToString(int(c.params[0]), "%d");
259 break;
260 }
261
262 selectedUnitsHandler.PossibleCommandChange(owner);
263 return;
264 }
265 }
266
267 if (!(c.options & SHIFT_KEY) && nonQueingCommands.find(c.GetID()) == nonQueingCommands.end()) {
268 tempOrder = false;
269 StopSlowGuard();
270 }
271
272 CCommandAI::GiveAllowedCommand(c);
273 }
274
275
276 /// returns true if the unit has to land
RefuelIfNeeded()277 bool CMobileCAI::RefuelIfNeeded()
278 {
279 if (owner->unitDef->maxFuel <= 0.0f)
280 return false;
281
282 if (owner->moveType->GetReservedPad() != NULL) {
283 // we already have a pad
284 return false;
285 }
286
287 if (owner->currentFuel <= 0.0f) {
288 // we're completely out of fuel
289 owner->AttackUnit(NULL, false, false);
290 inCommand = false;
291
292 CAirBaseHandler::LandingPad* lp =
293 airBaseHandler->FindAirBase(owner, owner->unitDef->minAirBasePower, true);
294
295 if (lp != NULL) {
296 // found a pad
297 owner->moveType->ReservePad(lp);
298 } else {
299 // no pads available, search for a landing
300 // spot near any that are currently occupied
301 const float3& landingPos = airBaseHandler->FindClosestAirBasePos(owner, owner->unitDef->minAirBasePower);
302
303 if (landingPos != ZeroVector) {
304 SetGoal(landingPos, owner->pos);
305 } else {
306 StopMove();
307 }
308 }
309
310 return true;
311 } else if (owner->moveType->WantsRefuel()) {
312 // current fuel level is below our bingo threshold
313 // note: force the refuel attempt (irrespective of
314 // what our currently active command is)
315
316 CAirBaseHandler::LandingPad* lp =
317 airBaseHandler->FindAirBase(owner, owner->unitDef->minAirBasePower, true);
318
319 if (lp != NULL) {
320 StopMove();
321 owner->AttackUnit(NULL, false, false);
322 owner->moveType->ReservePad(lp);
323 inCommand = false;
324 return true;
325 }
326 }
327
328 return false;
329 }
330
331 /// returns true if the unit has to land
LandRepairIfNeeded()332 bool CMobileCAI::LandRepairIfNeeded()
333 {
334 if (owner->moveType->GetReservedPad() != NULL)
335 return false;
336
337 if (!owner->moveType->WantsRepair())
338 return false;
339
340 // we're damaged, just seek a pad for repairs
341 CAirBaseHandler::LandingPad* lp =
342 airBaseHandler->FindAirBase(owner, owner->unitDef->minAirBasePower, true);
343
344 if (lp != NULL) {
345 owner->moveType->ReservePad(lp);
346 return true;
347 }
348
349 const float3& newGoal = airBaseHandler->FindClosestAirBasePos(owner, owner->unitDef->minAirBasePower);
350
351 if (newGoal != ZeroVector) {
352 SetGoal(newGoal, owner->pos);
353 return true;
354 }
355
356 return false;
357 }
358
SlowUpdate()359 void CMobileCAI::SlowUpdate()
360 {
361 if (gs->paused) // Commands issued may invoke SlowUpdate when paused
362 return;
363
364 if (!owner->UsingScriptMoveType() && owner->unitDef->IsAirUnit()) {
365 LandRepairIfNeeded() || RefuelIfNeeded();
366 }
367
368
369 if (!commandQue.empty() && commandQue.front().timeOut < gs->frameNum) {
370 StopMove();
371 FinishCommand();
372 return;
373 }
374
375 if (commandQue.empty()) {
376 MobileAutoGenerateTarget();
377
378 // the attack order could terminate directly and thus cause a loop
379 if (commandQue.empty() || (commandQue.front()).GetID() == CMD_ATTACK) {
380 return;
381 }
382 }
383
384 if (!slowGuard) {
385 // when slow-guarding, regulate speed through {Start,Stop}SlowGuard
386 SlowUpdateMaxSpeed();
387 }
388
389 Execute();
390 }
391
392 /**
393 * @brief Executes the first command in the commandQue
394 */
Execute()395 void CMobileCAI::Execute()
396 {
397 Command& c = commandQue.front();
398 switch (c.GetID()) {
399 case CMD_SET_WANTED_MAX_SPEED: { ExecuteSetWantedMaxSpeed(c); return; }
400 case CMD_MOVE: { ExecuteMove(c); return; }
401 case CMD_PATROL: { ExecutePatrol(c); return; }
402 case CMD_FIGHT: { ExecuteFight(c); return; }
403 case CMD_GUARD: { ExecuteGuard(c); return; }
404 case CMD_LOAD_ONTO: { ExecuteLoadUnits(c); return; }
405 default: {
406 CCommandAI::SlowUpdate();
407 return;
408 }
409 }
410 }
411
412 /**
413 * @brief executes the set wanted max speed command
414 */
ExecuteSetWantedMaxSpeed(Command & c)415 void CMobileCAI::ExecuteSetWantedMaxSpeed(Command &c)
416 {
417 if (repeatOrders && (commandQue.size() >= 2) &&
418 (commandQue.back().GetID() != CMD_SET_WANTED_MAX_SPEED)) {
419 commandQue.push_back(commandQue.front());
420 }
421 FinishCommand();
422 SlowUpdate();
423 return;
424 }
425
426 /**
427 * @brief executes the move command
428 */
ExecuteMove(Command & c)429 void CMobileCAI::ExecuteMove(Command &c)
430 {
431 const float3 cmdPos = c.GetPos(0);
432
433 if (cmdPos != goalPos) {
434 SetGoal(cmdPos, owner->pos);
435 }
436
437 if ((owner->pos - goalPos).SqLength2D() < cancelDistance || owner->moveType->progressState == AMoveType::Failed) {
438 FinishCommand();
439 }
440 return;
441 }
442
ExecuteLoadUnits(Command & c)443 void CMobileCAI::ExecuteLoadUnits(Command &c) {
444 CUnit* unit = unitHandler->GetUnit(c.params[0]);
445 CTransportUnit* tran = dynamic_cast<CTransportUnit*>(unit);
446
447 if (!tran) {
448 FinishCommand();
449 return;
450 }
451
452 if (!inCommand) {
453 inCommand = true;
454 Command newCommand(CMD_LOAD_UNITS, INTERNAL_ORDER | SHIFT_KEY, owner->id);
455 tran->commandAI->GiveCommandReal(newCommand);
456 }
457 if (owner->GetTransporter() != NULL) {
458 if (!commandQue.empty())
459 FinishCommand();
460 return;
461 }
462
463 if (unit == NULL)
464 return;
465
466 if ((unit->pos - goalPos).SqLength2D() > cancelDistance) {
467 SetGoal(unit->pos, owner->pos);
468 }
469 if ((owner->pos - goalPos).SqLength2D() < cancelDistance) {
470 StopMove();
471 }
472
473 return;
474 }
475
476 /**
477 * @brief Executes the Patrol command c
478 */
ExecutePatrol(Command & c)479 void CMobileCAI::ExecutePatrol(Command &c)
480 {
481 assert(owner->unitDef->canPatrol);
482 if (c.params.size() < 3) {
483 LOG_L(L_ERROR, "[MCAI::%s][f=%d][id=%d] CMD_FIGHT #params < 3", __FUNCTION__, gs->frameNum, owner->id);
484 return;
485 }
486 Command temp(CMD_FIGHT, c.options | INTERNAL_ORDER, c.GetPos(0));
487
488 commandQue.push_back(c);
489 commandQue.pop_front();
490 commandQue.push_front(temp);
491
492 Command tmpC(CMD_PATROL);
493 eoh->CommandFinished(*owner, tmpC);
494 ExecuteFight(temp);
495 }
496
497 /**
498 * @brief Executes the Fight command c
499 */
ExecuteFight(Command & c)500 void CMobileCAI::ExecuteFight(Command& c)
501 {
502 assert((c.options & INTERNAL_ORDER) || owner->unitDef->canFight);
503
504 if (c.params.size() == 1 && !owner->weapons.empty()) {
505 CWeapon* w = owner->weapons.front();
506
507 if ((orderTarget != NULL) && !w->AttackUnit(orderTarget, false)) {
508 CUnit* newTarget = CGameHelper::GetClosestValidTarget(owner->pos, owner->maxRange, owner->allyteam, this);
509
510 if ((newTarget != NULL) && w->AttackUnit(newTarget, false)) {
511 c.params[0] = newTarget->id;
512
513 inCommand = false;
514 } else {
515 w->AttackUnit(orderTarget, false);
516 }
517 }
518
519 ExecuteAttack(c);
520 return;
521 }
522
523 if (tempOrder) {
524 inCommand = true;
525 tempOrder = false;
526 }
527 if (c.params.size() < 3) {
528 LOG_L(L_ERROR, "[MCAI::%s][f=%d][id=%d] CMD_FIGHT #params < 3", __FUNCTION__, gs->frameNum, owner->id);
529 return;
530 }
531 if (c.params.size() >= 6) {
532 if (!inCommand) {
533 commandPos1 = c.GetPos(3);
534 }
535 } else {
536 // Some hackery to make sure the line (commandPos1,commandPos2) is NOT
537 // rotated (only shortened) if we reach this because the previous return
538 // fight command finished by the 'if((curPos-pos).SqLength2D()<(64*64)){'
539 // condition, but is actually updated correctly if you click somewhere
540 // outside the area close to the line (for a new command).
541 commandPos1 = ClosestPointOnLine(commandPos1, commandPos2, owner->pos);
542 if ((owner->pos - commandPos1).SqLength2D() > (96 * 96)) {
543 commandPos1 = owner->pos;
544 }
545 }
546
547 float3 pos = c.GetPos(0);
548
549 if (!inCommand) {
550 inCommand = true;
551 commandPos2 = pos;
552 lastUserGoal = commandPos2;
553 }
554 if (c.params.size() >= 6) {
555 pos = ClosestPointOnLine(commandPos1, commandPos2, owner->pos);
556 }
557 if (pos != goalPos) {
558 SetGoal(pos, owner->pos);
559 }
560
561 if (owner->unitDef->canAttack && owner->fireState >= FIRESTATE_FIREATWILL && !owner->weapons.empty()) {
562 const float3 curPosOnLine = ClosestPointOnLine(commandPos1, commandPos2, owner->pos);
563 const float searchRadius = owner->maxRange + 100 * owner->moveState * owner->moveState;
564 CUnit* enemy = CGameHelper::GetClosestValidTarget(curPosOnLine, searchRadius, owner->allyteam, this);
565
566 if (enemy != NULL) {
567 PushOrUpdateReturnFight();
568
569 // make the attack-command inherit <c>'s options
570 // NOTE: see AirCAI::ExecuteFight why we do not set INTERNAL_ORDER
571 Command c2(CMD_ATTACK, c.options, enemy->id);
572 commandQue.push_front(c2);
573
574 inCommand = false;
575 tempOrder = true;
576
577 // avoid infinite loops (?)
578 if (lastPC != gs->frameNum) {
579 lastPC = gs->frameNum;
580 SlowUpdate();
581 }
582 return;
583 }
584 }
585
586 if ((owner->pos - goalPos).SqLength2D() < (64 * 64)
587 || (owner->moveType->progressState == AMoveType::Failed)){
588 FinishCommand();
589 }
590 }
591
IsValidTarget(const CUnit * enemy) const592 bool CMobileCAI::IsValidTarget(const CUnit* enemy) const {
593 if (!enemy)
594 return false;
595
596 if (enemy == owner)
597 return false;
598
599 if (owner->unitDef->noChaseCategory & enemy->category)
600 return false;
601
602 // don't _auto_ chase neutrals
603 if (enemy->IsNeutral())
604 return false;
605
606 if (owner->weapons.empty())
607 return false;
608
609 // on "Hold pos", a target can not be valid if there exists no line of fire to it.
610 // FIXME: even if not on HOLDPOS there are situations where having LOF is not enough
611 if (owner->moveState == MOVESTATE_HOLDPOS && !owner->weapons.front()->TryTargetRotate(const_cast<CUnit*>(enemy), false))
612 return false;
613
614 // test if any weapon can target the enemy unit
615 for (std::vector<CWeapon*>::iterator it = owner->weapons.begin(); it != owner->weapons.end(); ++it) {
616 if ((*it)->TestTarget(enemy->pos, false, const_cast<CUnit*>(enemy))) {
617 return true;
618 }
619 }
620
621 return false;
622 }
623
624 /**
625 * @brief Executes the guard command c
626 */
ExecuteGuard(Command & c)627 void CMobileCAI::ExecuteGuard(Command &c)
628 {
629 assert(owner->unitDef->canGuard);
630 assert(!c.params.empty());
631
632 const CUnit* guardee = unitHandler->GetUnit(c.params[0]);
633
634 if (guardee == NULL) { FinishCommand(); return; }
635 if (UpdateTargetLostTimer(guardee->id) == 0) { FinishCommand(); return; }
636 if (guardee->outOfMapTime > (GAME_SPEED * 5)) { FinishCommand(); return; }
637
638 const bool pushAttackCommand =
639 owner->unitDef->canAttack &&
640 (guardee->lastAttackFrame + 40 < gs->frameNum) &&
641 IsValidTarget(guardee->lastAttacker);
642
643 if (pushAttackCommand) {
644 Command nc(CMD_ATTACK, c.options, guardee->lastAttacker->id);
645 commandQue.push_front(nc);
646
647 StopSlowGuard();
648 SlowUpdate();
649 } else {
650 const float3 dif = (guardee->pos - owner->pos).SafeNormalize();
651 const float3 goal = guardee->pos - dif * (guardee->radius + owner->radius + 64.0f);
652 const bool resetGoal =
653 ((goalPos - goal).SqLength2D() > 1600.0f) ||
654 (goalPos - owner->pos).SqLength2D() < Square(owner->moveType->GetMaxSpeed() * GAME_SPEED + 1 + SQUARE_SIZE * 2);
655
656 if (resetGoal) {
657 SetGoal(goal, owner->pos);
658 }
659
660 if ((goal - owner->pos).SqLength2D() < 6400.0f) {
661 StartSlowGuard(guardee->moveType->GetMaxSpeed());
662
663 if ((goal - owner->pos).SqLength2D() < 1800.0f) {
664 StopMove();
665 NonMoving();
666 }
667 } else {
668 StopSlowGuard();
669 }
670 }
671 }
672
673
ExecuteStop(Command & c)674 void CMobileCAI::ExecuteStop(Command &c)
675 {
676 StopMove();
677 return CCommandAI::ExecuteStop(c);
678 }
679
680
681
ExecuteAttack(Command & c)682 void CMobileCAI::ExecuteAttack(Command &c)
683 {
684 assert(owner->unitDef->canAttack);
685
686 // limit how far away we fly based on our movestate
687 if (tempOrder && orderTarget) {
688 const float3& closestPos = ClosestPointOnLine(commandPos1, commandPos2, owner->pos);
689 const float curTargetDist = LinePointDist(closestPos, commandPos2, orderTarget->pos);
690 const float maxTargetDist = (owner->moveType->GetManeuverLeash() * owner->moveState + owner->maxRange);
691
692 if (owner->moveState < MOVESTATE_ROAM && curTargetDist > maxTargetDist) {
693 StopMove();
694 FinishCommand();
695 return;
696 }
697 }
698
699 if (!inCommand) {
700 if (c.params.size() == 1) {
701 CUnit* targetUnit = unitHandler->GetUnit(c.params[0]);
702
703 // check if we have valid target parameter and that we aren't attacking ourselves
704 if (targetUnit == NULL) { StopMove(); FinishCommand(); return; }
705 if (targetUnit == owner) { StopMove(); FinishCommand(); return; }
706 if (targetUnit->GetTransporter() != NULL && !modInfo.targetableTransportedUnits) {
707 StopMove(); FinishCommand(); return;
708 }
709
710 const float3 tgtErrPos = targetUnit->pos + owner->posErrorVector * 128;
711 const float3 tgtPosDir = (tgtErrPos - owner->pos).Normalize();
712
713 // FIXME: don't call SetGoal() if target is already in range of some weapon?
714 SetGoal(tgtErrPos - tgtPosDir * targetUnit->radius, owner->pos);
715 SetOrderTarget(targetUnit);
716 owner->AttackUnit(targetUnit, (c.options & INTERNAL_ORDER) == 0, c.GetID() == CMD_MANUALFIRE);
717
718 inCommand = true;
719 }
720 else if (c.params.size() >= 3) {
721 // user gave force-fire attack command
722 SetGoal(c.GetPos(0), owner->pos);
723
724 inCommand = true;
725 }
726 }
727
728 // if our target is dead or we lost it then stop attacking
729 // NOTE: unit should actually just continue to target area!
730 if (targetDied || (c.params.size() == 1 && UpdateTargetLostTimer(int(c.params[0])) == 0)) {
731 // cancel keeppointingto
732 StopMove();
733 FinishCommand();
734 return;
735 }
736
737
738 // user clicked on enemy unit (note that we handle aircrafts slightly differently)
739 if (orderTarget != NULL) {
740 bool tryTargetRotate = false;
741 bool tryTargetHeading = false;
742
743 float edgeFactor = 0.0f; // percent offset to target center
744 const float3 targetMidPosVec = owner->midPos - orderTarget->midPos;
745
746 const float targetGoalDist = (orderTarget->pos + owner->posErrorVector * 128.0f).SqDistance2D(goalPos);
747 const float targetPosDist = Square(10.0f + orderTarget->pos.distance2D(owner->pos) * 0.2f);
748 const float minPointingDist = std::min(1.0f * owner->losRadius * losHandler->losDiv, owner->maxRange * 0.9f);
749
750 // FIXME? targetMidPosMaxDist is 3D, but compared with a 2D value
751 const float targetMidPosDist2D = targetMidPosVec.Length2D();
752 // const float targetMidPosMaxDist = owner->maxRange - (Square(orderTarget->speed.w) / owner->unitDef->maxAcc);
753
754 if (!owner->weapons.empty()) {
755 if (!(c.options & ALT_KEY) && SkipParalyzeTarget(orderTarget)) {
756 StopMove();
757 FinishCommand();
758 return;
759 }
760 }
761
762 for (unsigned int wNum = 0; wNum < owner->weapons.size(); wNum++) {
763 CWeapon* w = owner->weapons[wNum];
764
765 if (c.GetID() == CMD_MANUALFIRE) {
766 assert(owner->unitDef->canManualFire);
767
768 if (!w->weaponDef->manualfire) {
769 continue;
770 }
771 }
772
773 tryTargetRotate = w->TryTargetRotate(orderTarget, (c.options & INTERNAL_ORDER) == 0);
774 tryTargetHeading = w->TryTargetHeading(GetHeadingFromVector(-targetMidPosVec.x, -targetMidPosVec.z), orderTarget->pos, orderTarget != NULL, orderTarget);
775
776 if (tryTargetRotate || tryTargetHeading)
777 break;
778
779 edgeFactor = math::fabs(w->targetBorder);
780 }
781
782 // if w->AttackUnit() returned true then we are already
783 // in range with our biggest (?) weapon, so stop moving
784 // also make sure that we're not locked in close-in/in-range state
785 // loop due to rotates invoked by in-range or out-of-range states
786 if (tryTargetRotate) {
787 const bool canChaseTarget = (!tempOrder || owner->moveState != MOVESTATE_HOLDPOS);
788 const bool targetBehind = (targetMidPosVec.dot(orderTarget->speed) < 0.0f);
789
790 if (canChaseTarget && tryTargetHeading && targetBehind) {
791 SetGoal(owner->pos + (orderTarget->speed * 80), owner->pos, SQUARE_SIZE, orderTarget->speed.w * 1.1f);
792 } else {
793 StopMove();
794
795 if (gs->frameNum > lastCloseInTry + MAX_CLOSE_IN_RETRY_TICKS) {
796 owner->moveType->KeepPointingTo(orderTarget->midPos, minPointingDist, true);
797 }
798 }
799
800 owner->AttackUnit(orderTarget, (c.options & INTERNAL_ORDER) == 0, c.GetID() == CMD_MANUALFIRE);
801 }
802
803 // if we're on hold pos in a temporary order, then none of the close-in
804 // code below should run, and the attack command is cancelled.
805 else if (tempOrder && owner->moveState == MOVESTATE_HOLDPOS) {
806 StopMove();
807 FinishCommand();
808 return;
809 }
810
811 // if ((our movetype has type HoverAirMoveType and length of 2D vector from us to target
812 // less than 90% of our maximum range) OR squared length of 2D vector from us to target
813 // less than 1024) then we are close enough
814 else if (targetMidPosDist2D < (owner->maxRange * 0.9f)) {
815 if (owner->unitDef->IsHoveringAirUnit() || (targetMidPosVec.SqLength2D() < 1024)) {
816 StopMove();
817 owner->moveType->KeepPointingTo(orderTarget->midPos, minPointingDist, true);
818 }
819
820 // if (((first weapon range minus first weapon length greater than distance to target)
821 // and length of 2D vector from us to target less than 90% of our maximum range)
822 // then we are close enough, but need to move sideways to get a shot.
823 //assumption is flawed: The unit may be aiming or otherwise unable to shoot
824 else if (owner->unitDef->strafeToAttack && targetMidPosDist2D < (owner->maxRange * 0.9f)) {
825 moveDir ^= (owner->moveType->progressState == AMoveType::Failed);
826
827 const float sin = moveDir ? 3.0/5 : -3.0/5;
828 const float cos = 4.0 / 5;
829
830 float3 goalDiff;
831 goalDiff.x = targetMidPosVec.dot(float3(cos, 0, -sin));
832 goalDiff.z = targetMidPosVec.dot(float3(sin, 0, cos));
833 goalDiff *= (targetMidPosDist2D < (owner->maxRange * 0.3f)) ? 1/cos : cos;
834 goalDiff += orderTarget->pos;
835 SetGoal(goalDiff, owner->pos);
836 }
837 }
838
839 // if 2D distance of (target position plus attacker error vector times 128)
840 // to goal position greater than
841 // (10 plus 20% of 2D distance between attacker and target) then we need to close
842 // in on target more
843 else if (targetGoalDist > targetPosDist) {
844 // if the target isn't in LOS, go to its approximate position
845 // otherwise try to go precisely to the target
846 // this should fix issues with low range weapons (mainly melee)
847 const float3 errPos = ((orderTarget->losStatus[owner->allyteam] & LOS_INLOS)? ZeroVector: owner->posErrorVector * 128.0f);
848 const float3 tgtPos = orderTarget->pos + errPos;
849
850 const float3 norm = (tgtPos - owner->pos).Normalize();
851 const float3 goal = tgtPos - norm * (orderTarget->radius * edgeFactor * 0.8f);
852
853 SetGoal(goal, owner->pos);
854
855 if (lastCloseInTry < gs->frameNum + MAX_CLOSE_IN_RETRY_TICKS)
856 lastCloseInTry = gs->frameNum;
857 }
858 }
859
860 // user wants to attack the ground; cycle through our
861 // weapons until we find one that can accomodate him
862 else if (c.params.size() >= 3) {
863 const float3 attackPos = c.GetPos(0);
864 const float3 attackVec = attackPos - owner->pos;
865
866 bool foundWeapon = false;
867
868 for (unsigned int wNum = 0; wNum < owner->weapons.size(); wNum++) {
869 CWeapon* w = owner->weapons[wNum];
870
871 if (foundWeapon)
872 break;
873
874 // XXX HACK - special weapon overrides any checks
875 if (c.GetID() == CMD_MANUALFIRE) {
876 assert(owner->unitDef->canManualFire);
877
878 if (!w->weaponDef->manualfire)
879 continue;
880 if (attackVec.SqLength() >= (w->range * w->range))
881 continue;
882
883 // StopMoveAndKeepPointing calls StopMove before KeepPointingTo
884 // but we want to call it *after* KeepPointingTo to prevent 4131
885 owner->AttackGround(attackPos, (c.options & INTERNAL_ORDER) == 0, c.GetID() == CMD_MANUALFIRE);
886 owner->moveType->KeepPointingTo(attackPos, owner->maxRange * 0.9f, true);
887 StopMove();
888
889 foundWeapon = true;
890 } else {
891 // NOTE:
892 // we call TryTargetHeading which is less restrictive than TryTarget
893 // (eg. the former succeeds even if the unit has not already aligned
894 // itself with <attackVec>)
895 if (w->TryTargetHeading(GetHeadingFromVector(attackVec.x, attackVec.z), attackPos, (c.options & INTERNAL_ORDER) == 0, NULL)) {
896 if (w->TryTargetRotate(attackPos, (c.options & INTERNAL_ORDER) == 0)) {
897 StopMove();
898 owner->AttackGround(attackPos, (c.options & INTERNAL_ORDER) == 0, c.GetID() == CMD_MANUALFIRE);
899
900 foundWeapon = true;
901 }
902
903 // for gunships, this pitches the nose down such that
904 // TryTargetRotate (which also checks range for itself)
905 // has a bigger chance of succeeding
906 //
907 // hence it must be called as soon as we get in range
908 // and may not depend on what TryTargetRotate returns
909 // (otherwise we might never get a firing solution)
910 owner->moveType->KeepPointingTo(attackPos, owner->maxRange * 0.9f, true);
911 }
912 }
913 }
914
915 #if 0
916 // no weapons --> no need to stop at an arbitrary distance?
917 else if (diff.SqLength2D() < 1024) {
918 StopMove();
919 owner->moveType->KeepPointingTo(attackPos, owner->maxRange * 0.9f, true);
920 }
921 #endif
922
923 // if we are unarmed and more than 10 elmos distant
924 // from target position, then keeping moving closer
925 if (owner->weapons.empty() && attackPos.SqDistance2D(goalPos) > 100) {
926 SetGoal(attackPos, owner->pos);
927 }
928 }
929 }
930
931
932
933
GetDefaultCmd(const CUnit * pointed,const CFeature * feature)934 int CMobileCAI::GetDefaultCmd(const CUnit* pointed, const CFeature* feature)
935 {
936 if (pointed) {
937 if (!teamHandler->Ally(gu->myAllyTeam,pointed->allyteam)) {
938 if (owner->unitDef->canAttack) {
939 return CMD_ATTACK;
940 }
941 } else {
942 const CTransportCAI* tran = dynamic_cast<CTransportCAI*>(pointed->commandAI);
943
944 if (tran != NULL && tran->CanTransport(owner)) {
945 return CMD_LOAD_ONTO;
946 } else if (owner->unitDef->canGuard) {
947 return CMD_GUARD;
948 }
949 }
950 }
951 return CMD_MOVE;
952 }
953
SetGoal(const float3 & pos,const float3 &,float goalRadius)954 void CMobileCAI::SetGoal(const float3& pos, const float3& /*curPos*/, float goalRadius)
955 {
956 if (pos.SqDistance(goalPos) < Square(goalRadius))
957 return;
958
959 owner->moveType->StartMoving(goalPos = pos, this->goalRadius = goalRadius);
960 }
961
SetGoal(const float3 & pos,const float3 &,float goalRadius,float speed)962 void CMobileCAI::SetGoal(const float3& pos, const float3& /*curPos*/, float goalRadius, float speed)
963 {
964 if (pos.SqDistance(goalPos) < Square(goalRadius))
965 return;
966
967 owner->moveType->StartMoving(goalPos = pos, this->goalRadius = goalRadius, speed);
968 }
969
SetFrontMoveCommandPos(const float3 & pos)970 bool CMobileCAI::SetFrontMoveCommandPos(const float3& pos)
971 {
972 if (commandQue.empty())
973 return false;
974 if ((commandQue.front()).GetID() != CMD_MOVE)
975 return false;
976
977 (commandQue.front()).SetPos(0, pos);
978 return true;
979 }
980
StopMove()981 void CMobileCAI::StopMove()
982 {
983 owner->moveType->StopMoving();
984 goalPos = owner->pos;
985 goalRadius = 0.f;
986 }
987
StopMoveAndKeepPointing(const float3 & p,const float r,bool b)988 void CMobileCAI::StopMoveAndKeepPointing(const float3& p, const float r, bool b)
989 {
990 StopMove();
991 owner->moveType->KeepPointingTo(p, r, b);
992 }
993
BuggerOff(const float3 & pos,float radius)994 void CMobileCAI::BuggerOff(const float3& pos, float radius)
995 {
996 if (radius < 0.0f) {
997 lastBuggerOffTime = gs->frameNum - BUGGER_OFF_TTL;
998 return;
999 }
1000 lastBuggerOffTime = gs->frameNum;
1001 buggerOffPos = pos;
1002 buggerOffRadius = radius + owner->radius;
1003 }
1004
NonMoving()1005 void CMobileCAI::NonMoving()
1006 {
1007 if (owner->UsingScriptMoveType())
1008 return;
1009
1010 if (lastBuggerOffTime > gs->frameNum - BUGGER_OFF_TTL) {
1011 float3 dif = owner->pos-buggerOffPos;
1012 dif.y = 0.0f;
1013 float length=dif.Length();
1014 if (!length) {
1015 length = 0.1f;
1016 dif = float3(0.1f, 0.0f, 0.0f);
1017 }
1018 if (length < buggerOffRadius) {
1019 float3 goalPos = buggerOffPos + dif * ((buggerOffRadius + 128) / length);
1020 bool randomize = (goalPos.x == lastBuggerGoalPos.x) && (goalPos.z == lastBuggerGoalPos.z);
1021 lastBuggerGoalPos.x = goalPos.x;
1022 lastBuggerGoalPos.z = goalPos.z;
1023 if (randomize) {
1024 lastBuggerGoalPos.y += 32.0f; // gradually increase the amplitude of the random factor
1025 goalPos.x += (2.0f * lastBuggerGoalPos.y) * gs->randFloat() - lastBuggerGoalPos.y;
1026 goalPos.z += (2.0f * lastBuggerGoalPos.y) * gs->randFloat() - lastBuggerGoalPos.y;
1027 }
1028 else
1029 lastBuggerGoalPos.y = 0.0f;
1030
1031 Command c(CMD_MOVE, goalPos);
1032 //c.options = INTERNAL_ORDER;
1033 c.timeOut = gs->frameNum + 40;
1034 commandQue.push_front(c);
1035 unimportantMove = true;
1036 }
1037 }
1038 }
1039
FinishCommand()1040 void CMobileCAI::FinishCommand()
1041 {
1042 if (!((commandQue.front()).options & INTERNAL_ORDER)) {
1043 lastUserGoal = owner->pos;
1044 }
1045 StopSlowGuard();
1046 CCommandAI::FinishCommand();
1047 }
1048
MobileAutoGenerateTarget()1049 bool CMobileCAI::MobileAutoGenerateTarget()
1050 {
1051 //FIXME merge with CWeapon::AutoTarget()
1052 assert(commandQue.empty());
1053
1054 const bool canAttack = (owner->unitDef->canAttack && !owner->weapons.empty());
1055 const float extraRange = 200.0f * owner->moveState * owner->moveState;
1056 const float maxRangeSq = Square(owner->maxRange + extraRange);
1057
1058 #if (AUTO_GENERATE_ATTACK_ORDERS == 1)
1059 if (canAttack) {
1060 if (owner->attackTarget != NULL) {
1061 if (owner->fireState > FIRESTATE_HOLDFIRE) {
1062 if (owner->pos.SqDistance2D(owner->attackTarget->pos) < maxRangeSq) {
1063 Command c(CMD_ATTACK, INTERNAL_ORDER, owner->attackTarget->id);
1064 c.timeOut = gs->frameNum + GAME_SPEED * 5;
1065 commandQue.push_front(c);
1066
1067 commandPos1 = owner->pos;
1068 commandPos2 = owner->pos;
1069
1070 return (tempOrder = true);
1071 }
1072 }
1073 } else {
1074 if (owner->fireState > FIRESTATE_HOLDFIRE) {
1075 const bool haveLastAttacker = (owner->lastAttacker != NULL);
1076 const bool canAttackAttacker = (haveLastAttacker && (owner->lastAttackFrame + GAME_SPEED * 7) > gs->frameNum);
1077 const bool canChaseAttacker = (haveLastAttacker && !(owner->unitDef->noChaseCategory & owner->lastAttacker->category));
1078
1079 if (canAttackAttacker && canChaseAttacker) {
1080 const float3& P = owner->lastAttacker->pos;
1081 const float R = owner->pos.SqDistance2D(P);
1082
1083 if (R < maxRangeSq) {
1084 Command c(CMD_ATTACK, INTERNAL_ORDER, owner->lastAttacker->id);
1085 c.timeOut = gs->frameNum + GAME_SPEED * 5;
1086 commandQue.push_front(c);
1087
1088 commandPos1 = owner->pos;
1089 commandPos2 = owner->pos;
1090
1091 return (tempOrder = true);
1092 }
1093 }
1094 }
1095
1096 if (owner->fireState >= FIRESTATE_FIREATWILL && (gs->frameNum >= lastIdleCheck + 10)) {
1097 const float searchRadius = owner->maxRange + 150.0f * owner->moveState * owner->moveState;
1098 const CUnit* enemy = CGameHelper::GetClosestValidTarget(owner->pos, searchRadius, owner->allyteam, this);
1099
1100 if (enemy != NULL) {
1101 Command c(CMD_ATTACK, INTERNAL_ORDER, enemy->id);
1102 c.timeOut = gs->frameNum + GAME_SPEED * 5;
1103 commandQue.push_front(c);
1104
1105 commandPos1 = owner->pos;
1106 commandPos2 = owner->pos;
1107
1108 return (tempOrder = true);
1109 }
1110 }
1111 }
1112 }
1113 #endif
1114
1115 if (owner->UsingScriptMoveType())
1116 return false;
1117
1118 lastIdleCheck = gs->frameNum;
1119
1120 if (owner->haveTarget) {
1121 NonMoving(); return false;
1122 }
1123 if ((owner->pos - lastUserGoal).SqLength2D() <= (MAX_USERGOAL_TOLERANCE_DIST * MAX_USERGOAL_TOLERANCE_DIST)) {
1124 NonMoving(); return false;
1125 }
1126 if (owner->unitDef->IsHoveringAirUnit()) {
1127 NonMoving(); return false;
1128 }
1129
1130 unimportantMove = true;
1131 return false;
1132 }
1133
1134
1135
StopSlowGuard()1136 void CMobileCAI::StopSlowGuard() {
1137 if (!slowGuard)
1138 return;
1139
1140 slowGuard = false;
1141
1142 // restore maxWantedSpeed to our current maxSpeed
1143 // (StartSlowGuard modifies maxWantedSpeed, so we
1144 // do not know its old value here)
1145 owner->moveType->SetWantedMaxSpeed(owner->moveType->GetMaxSpeed());
1146 }
1147
StartSlowGuard(float speed)1148 void CMobileCAI::StartSlowGuard(float speed) {
1149 if (slowGuard)
1150 return;
1151
1152 slowGuard = true;
1153
1154 if (speed <= 0.0f) { return; }
1155 if (commandQue.empty()) { return; }
1156 if (owner->moveType->GetMaxSpeed() < speed) { return; }
1157
1158 const Command& c = (commandQue.size() > 1)? commandQue[1]: Command(CMD_STOP);
1159
1160 // when guarding, temporarily adopt the maximum
1161 // (forward) speed of the guardee unit as our own
1162 // WANTED maximum
1163 if (c.GetID() == CMD_SET_WANTED_MAX_SPEED) {
1164 owner->moveType->SetWantedMaxSpeed(speed);
1165 }
1166 }
1167
1168
1169
CalculateCancelDistance()1170 void CMobileCAI::CalculateCancelDistance()
1171 {
1172 const float tmp = owner->moveType->CalcStaticTurnRadius() + (SQUARE_SIZE << 1);
1173
1174 // clamp it a bit because the units don't have to turn at max speed
1175 cancelDistance = Clamp(tmp * tmp, 1024.0f, 2048.0f);
1176 }
1177
1178