1 /*
2 * Copyright 2010-2014 OpenXcom Developers.
3 *
4 * This file is part of OpenXcom.
5 *
6 * OpenXcom is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
10 *
11 * OpenXcom is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with OpenXcom. If not, see <http://www.gnu.org/licenses/>.
18 */
19 #define _USE_MATH_DEFINES
20 #include <cmath>
21 #include <climits>
22 #include <algorithm>
23 #include "AlienBAIState.h"
24 #include "ProjectileFlyBState.h"
25 #include "../Savegame/BattleUnit.h"
26 #include "../Savegame/BattleItem.h"
27 #include "../Savegame/Node.h"
28 #include "../Savegame/SavedBattleGame.h"
29 #include "../Savegame/SavedGame.h"
30 #include "../Battlescape/TileEngine.h"
31 #include "../Battlescape/Map.h"
32 #include "../Battlescape/BattlescapeState.h"
33 #include "../Savegame/Tile.h"
34 #include "../Battlescape/Pathfinding.h"
35 #include "../Engine/RNG.h"
36 #include "../Engine/Logger.h"
37 #include "../Engine/Game.h"
38 #include "../Ruleset/Armor.h"
39 #include "../Resource/ResourcePack.h"
40 #include "../Ruleset/Ruleset.h"
41 #include "../Ruleset/RuleItem.h"
42
43 namespace OpenXcom
44 {
45
46
47 /**
48 * Sets up a BattleAIState.
49 * @param save Pointer to the battle game.
50 * @param unit Pointer to the unit.
51 * @param node Pointer to the node the unit originates from.
52 */
AlienBAIState(SavedBattleGame * save,BattleUnit * unit,Node * node)53 AlienBAIState::AlienBAIState(SavedBattleGame *save, BattleUnit *unit, Node *node) : BattleAIState(save, unit), _aggroTarget(0), _knownEnemies(0), _visibleEnemies(0), _spottingEnemies(0),
54 _escapeTUs(0), _ambushTUs(0), _reserveTUs(0), _rifle(false), _melee(false), _blaster(false),
55 _wasHit(false), _didPsi(false), _AIMode(AI_PATROL), _closestDist(100), _fromNode(node), _toNode(0)
56 {
57 _traceAI = Options::traceAI;
58
59 _intelligence = _unit->getIntelligence();
60 _escapeAction = new BattleAction();
61 _ambushAction = new BattleAction();
62 _attackAction = new BattleAction();
63 _patrolAction = new BattleAction();
64 _psiAction = new BattleAction();
65 }
66
67 /**
68 * Deletes the BattleAIState.
69 */
~AlienBAIState()70 AlienBAIState::~AlienBAIState()
71 {
72 delete _escapeAction;
73 delete _ambushAction;
74 delete _attackAction;
75 delete _patrolAction;
76 delete _psiAction;
77 }
78
79 /**
80 * Loads the AI state from a YAML file.
81 * @param node YAML node.
82 */
load(const YAML::Node & node)83 void AlienBAIState::load(const YAML::Node &node)
84 {
85 int fromNodeID, toNodeID;
86 fromNodeID = node["fromNode"].as<int>(-1);
87 toNodeID = node["toNode"].as<int>(-1);
88 _AIMode = node["AIMode"].as<int>(0);
89 _wasHit = node["wasHit"].as<bool>(false);
90 if (fromNodeID != -1)
91 {
92 _fromNode = _save->getNodes()->at(fromNodeID);
93 }
94 if (toNodeID != -1)
95 {
96 _toNode = _save->getNodes()->at(toNodeID);
97 }
98 }
99
100 /**
101 * Saves the AI state to a YAML file.
102 * @return YAML node.
103 */
save() const104 YAML::Node AlienBAIState::save() const
105 {
106 int fromNodeID = -1, toNodeID = -1;
107 if (_fromNode)
108 fromNodeID = _fromNode->getID();
109 if (_toNode)
110 toNodeID = _toNode->getID();
111
112 YAML::Node node;
113 node["fromNode"] = fromNodeID;
114 node["toNode"] = toNodeID;
115 node["AIMode"] = _AIMode;
116 node["wasHit"] = _wasHit;
117 return node;
118 }
119
120 /**
121 * Enters the current AI state.
122 */
enter()123 void AlienBAIState::enter()
124 {
125 // ROOOAARR !
126
127 }
128
129
130 /**
131 * Exits the current AI state.
132 */
exit()133 void AlienBAIState::exit()
134 {
135
136 }
137
138 /**
139 * Runs any code the state needs to keep updating every AI cycle.
140 * @param action (possible) AI action to execute after thinking is done.
141 */
think(BattleAction * action)142 void AlienBAIState::think(BattleAction *action)
143 {
144 action->type = BA_RETHINK;
145 action->actor = _unit;
146 action->weapon = _unit->getMainHandWeapon();
147 _attackAction->diff = (int)(_save->getBattleState()->getGame()->getSavedGame()->getDifficulty());
148 _attackAction->actor = _unit;
149 _attackAction->weapon = action->weapon;
150 _attackAction->number = action->number;
151 _escapeAction->number = action->number;
152 _knownEnemies = countKnownTargets();
153 _visibleEnemies = selectNearestTarget();
154 _spottingEnemies = getSpottingUnits(_unit->getPosition());
155 _melee = false;
156 _rifle = false;
157 _blaster = false;
158 _reachable = _save->getPathfinding()->findReachable(_unit, _unit->getTimeUnits());
159 if(_unit->getCharging() && _unit->getCharging()->isOut())
160 {
161 _unit->setCharging(0);
162 }
163
164 if (_traceAI)
165 {
166 Log(LOG_INFO) << "Unit has " << _visibleEnemies << "/" << _knownEnemies << " known enemies visible, " << _spottingEnemies << " of whom are spotting him. ";
167 std::string AIMode;
168 switch (_AIMode)
169 {
170 case 0:
171 AIMode = "Patrol";
172 break;
173 case 1:
174 AIMode = "Ambush";
175 break;
176 case 2:
177 AIMode = "Combat";
178 break;
179 case 3:
180 AIMode = "Escape";
181 break;
182 }
183 Log(LOG_INFO) << "Currently using " << AIMode << " behaviour";
184 }
185
186 if (action->weapon)
187 {
188 RuleItem *rule = action->weapon->getRules();
189 if (rule->getBattleType() == BT_FIREARM)
190 {
191 if (!rule->isWaypoint())
192 {
193 _rifle = true;
194 _reachableWithAttack = _save->getPathfinding()->findReachable(_unit, _unit->getTimeUnits() - _unit->getActionTUs(BA_SNAPSHOT, action->weapon));
195 }
196 else
197 {
198 _blaster = true;
199 _reachableWithAttack = _save->getPathfinding()->findReachable(_unit, _unit->getTimeUnits() - _unit->getActionTUs(BA_AIMEDSHOT, action->weapon));
200 }
201 }
202 else if (rule->getBattleType() == BT_MELEE)
203 {
204 _melee = true;
205 _reachableWithAttack = _save->getPathfinding()->findReachable(_unit, _unit->getTimeUnits() - _unit->getActionTUs(BA_HIT, action->weapon));
206 }
207 }
208
209 if (_spottingEnemies && !_escapeTUs)
210 {
211 setupEscape();
212 }
213
214 if (_knownEnemies && !_melee && !_ambushTUs)
215 {
216 setupAmbush();
217 }
218
219 setupAttack();
220 setupPatrol();
221
222 if (_psiAction->type != BA_NONE && !_didPsi)
223 {
224 _didPsi = true;
225 action->type = _psiAction->type;
226 action->target = _psiAction->target;
227 action->number -= 1;
228 return;
229 }
230 else
231 {
232 _didPsi = false;
233 }
234
235 bool evaluate = false;
236
237 if (_AIMode == AI_ESCAPE)
238 {
239 if (!_spottingEnemies || !_knownEnemies)
240 {
241 evaluate = true;
242 }
243 }
244 else if (_AIMode == AI_PATROL)
245 {
246 if (_spottingEnemies || _visibleEnemies || _knownEnemies || RNG::percent(10))
247 {
248 evaluate = true;
249 }
250 }
251 else if (_AIMode == AI_AMBUSH)
252 {
253 if (!_rifle || !_ambushTUs || _visibleEnemies)
254 {
255 evaluate = true;
256 }
257 }
258
259 if (_AIMode == AI_COMBAT)
260 {
261 if (_attackAction->type == BA_RETHINK)
262 {
263 evaluate = true;
264 }
265 }
266 if (_spottingEnemies > 2
267 || _unit->getHealth() < 2 * _unit->getStats()->health / 3
268 || (_aggroTarget && _aggroTarget->getTurnsSinceSpotted() > _intelligence))
269 {
270 evaluate = true;
271 }
272
273
274 if (_save->isCheating() && _AIMode != AI_COMBAT)
275 {
276 evaluate = true;
277 }
278
279 if (evaluate)
280 {
281 evaluateAIMode();
282 if (_traceAI)
283 {
284 std::string AIMode;
285 switch (_AIMode)
286 {
287 case 0:
288 AIMode = "Patrol";
289 break;
290 case 1:
291 AIMode = "Ambush";
292 break;
293 case 2:
294 AIMode = "Combat";
295 break;
296 case 3:
297 AIMode = "Escape";
298 break;
299 }
300 Log(LOG_INFO) << "Re-Evaluated, now using " << AIMode << " behaviour";
301 }
302 }
303
304 switch (_AIMode)
305 {
306 case AI_ESCAPE:
307 _unit->setCharging(0);
308 action->type = _escapeAction->type;
309 action->target = _escapeAction->target;
310 // end this unit's turn.
311 action->finalAction = true;
312 // ignore new targets.
313 action->desperate = true;
314 // spin 180 at the end of your route.
315 _unit->_hidingForTurn = true;
316 break;
317 case AI_PATROL:
318 _unit->setCharging(0);
319 if (action->weapon && action->weapon->getRules()->getBattleType() == BT_FIREARM)
320 {
321 switch (_unit->getAggression())
322 {
323 case 0:
324 _save->getBattleGame()->setTUReserved(BA_AIMEDSHOT, false);
325 break;
326 case 1:
327 _save->getBattleGame()->setTUReserved(BA_AUTOSHOT, false);
328 break;
329 case 2:
330 _save->getBattleGame()->setTUReserved(BA_SNAPSHOT, false);
331 default:
332 break;
333 }
334 }
335 action->type = _patrolAction->type;
336 action->target = _patrolAction->target;
337 break;
338 case AI_COMBAT:
339 action->type = _attackAction->type;
340 action->target = _attackAction->target;
341 // this may have changed to a grenade.
342 action->weapon = _attackAction->weapon;
343 if (action->weapon && action->type == BA_THROW && action->weapon->getRules()->getBattleType() == BT_GRENADE)
344 {
345 _unit->spendTimeUnits(_unit->getActionTUs(BA_PRIME, action->weapon));
346 }
347 // if this is a firepoint action, set our facing.
348 action->finalFacing = _attackAction->finalFacing;
349 action->TU = _unit->getActionTUs(_attackAction->type, _attackAction->weapon);
350 // don't worry about reserving TUs, we've factored that in already.
351 _save->getBattleGame()->setTUReserved(BA_NONE, false);
352 // if this is a "find fire point" action, don't increment the AI counter.
353 if (action->type == BA_WALK && _rifle
354 // so long as we can take a shot afterwards.
355 && _unit->getTimeUnits() > _unit->getActionTUs(BA_SNAPSHOT, action->weapon))
356 {
357 action->number -= 1;
358 }
359 else if (action->type == BA_LAUNCH)
360 {
361 action->waypoints = _attackAction->waypoints;
362 }
363 break;
364 case AI_AMBUSH:
365 _unit->setCharging(0);
366 action->type = _ambushAction->type;
367 action->target = _ambushAction->target;
368 // face where we think our target will appear.
369 action->finalFacing = _ambushAction->finalFacing;
370 // end this unit's turn.
371 action->finalAction = true;
372 break;
373 default:
374 break;
375 }
376
377 if (action->type == BA_WALK)
378 {
379 // if we're moving, we'll have to re-evaluate our escape/ambush position.
380 if (action->target != _unit->getPosition())
381 {
382 _escapeTUs = 0;
383 _ambushTUs = 0;
384 }
385 else
386 {
387 action->type = BA_NONE;
388 }
389 }
390 }
391
392 /*
393 * sets the "was hit" flag to true.
394 */
setWasHit()395 void AlienBAIState::setWasHit()
396 {
397 _wasHit = true;
398 }
399
400 /*
401 * Gets whether the unit was hit.
402 * @return if it was hit.
403 */
getWasHit() const404 bool AlienBAIState::getWasHit() const
405 {
406 return _wasHit;
407 }
408 /*
409 * Sets up a patrol action.
410 * this is mainly going from node to node, moving about the map.
411 * handles node selection, and fills out the _patrolAction with useful data.
412 */
setupPatrol()413 void AlienBAIState::setupPatrol()
414 {
415 Node *node;
416 _patrolAction->TU = 0;
417 if (_toNode != 0 && _unit->getPosition() == _toNode->getPosition())
418 {
419 if (_traceAI)
420 {
421 Log(LOG_INFO) << "Patrol destination reached!";
422 }
423 // destination reached
424 // head off to next patrol node
425 _fromNode = _toNode;
426 _toNode->freeNode();
427 _toNode = 0;
428 // take a peek through window before walking to the next node
429 int dir = _save->getTileEngine()->faceWindow(_unit->getPosition());
430 if (dir != -1 && dir != _unit->getDirection())
431 {
432 _unit->lookAt(dir);
433 while (_unit->getStatus() == STATUS_TURNING)
434 {
435 _unit->turn();
436 }
437 }
438 }
439
440 if (_fromNode == 0)
441 {
442 // assume closest node as "from node"
443 // on same level to avoid strange things, and the node has to match unit size or it will freeze
444 int closest = 1000000;
445 for (std::vector<Node*>::iterator i = _save->getNodes()->begin(); i != _save->getNodes()->end(); ++i)
446 {
447 node = *i;
448 int d = _save->getTileEngine()->distanceSq(_unit->getPosition(), node->getPosition());
449 if (_unit->getPosition().z == node->getPosition().z
450 && d < closest
451 && (!(node->getType() & Node::TYPE_SMALL) || _unit->getArmor()->getSize() == 1))
452 {
453 _fromNode = node;
454 closest = d;
455 }
456 }
457 }
458 int triesLeft = 5;
459
460 while (_toNode == 0 && triesLeft)
461 {
462 triesLeft--;
463 // look for a new node to walk towards
464 bool scout = true;
465 if (_save->getMissionType() != "STR_BASE_DEFENSE")
466 {
467 // after turn 20 or if the morale is low, everyone moves out the UFO and scout
468 // also anyone standing in fire should also probably move
469 if (_save->isCheating() || !_fromNode || _fromNode->getRank() == 0 ||
470 (_save->getTile(_unit->getPosition()) && _save->getTile(_unit->getPosition())->getFire()))
471 {
472 scout = true;
473 }
474 else
475 {
476 scout = false;
477 }
478 }
479
480 // in base defense missions, the smaller aliens walk towards target nodes - or if there, shoot objects around them
481 else if (_unit->getArmor()->getSize() == 1)
482 {
483 // can i shoot an object?
484 if (_fromNode->isTarget() &&
485 _unit->getMainHandWeapon() &&
486 _unit->getMainHandWeapon()->getAmmoItem()->getRules()->getDamageType() != DT_HE &&
487 _save->getModuleMap()[_fromNode->getPosition().x / 10][_fromNode->getPosition().y / 10].second > 0)
488 {
489 // scan this room for objects to destroy
490 int x = (_unit->getPosition().x/10)*10;
491 int y = (_unit->getPosition().y/10)*10;
492 for (int i = x; i < x+9; i++)
493 for (int j = y; j < y+9; j++)
494 {
495 MapData *md = _save->getTile(Position(i, j, 1))->getMapData(MapData::O_OBJECT);
496 if (md && md->isBaseModule())
497 {
498 _patrolAction->actor = _unit;
499 _patrolAction->target = Position(i, j, 1);
500 _patrolAction->weapon = _patrolAction->actor->getMainHandWeapon();
501 _patrolAction->type = BA_SNAPSHOT;
502 _patrolAction->TU = _patrolAction->actor->getActionTUs(_patrolAction->type, _patrolAction->weapon);
503 return;
504 }
505 }
506 }
507 else
508 {
509 // find closest high value target which is not already allocated
510 int closest = 1000000;
511 for (std::vector<Node*>::iterator i = _save->getNodes()->begin(); i != _save->getNodes()->end(); ++i)
512 {
513 if ((*i)->isTarget() && !(*i)->isAllocated())
514 {
515 node = *i;
516 int d = _save->getTileEngine()->distanceSq(_unit->getPosition(), node->getPosition());
517 if (!_toNode || (d < closest && node != _fromNode))
518 {
519 _toNode = node;
520 closest = d;
521 }
522 }
523 }
524 }
525 }
526
527 if (_toNode == 0)
528 {
529 _toNode = _save->getPatrolNode(scout, _unit, _fromNode);
530 if (_toNode == 0)
531 {
532 _toNode = _save->getPatrolNode(!scout, _unit, _fromNode);
533 }
534 }
535
536 if (_toNode != 0)
537 {
538 _save->getPathfinding()->calculate(_unit, _toNode->getPosition());
539 if (_save->getPathfinding()->getStartDirection() == -1)
540 {
541 _toNode = 0;
542 }
543 _save->getPathfinding()->abortPath();
544 }
545 }
546
547 if (_toNode != 0)
548 {
549 _toNode->allocateNode();
550 _patrolAction->actor = _unit;
551 _patrolAction->type = BA_WALK;
552 _patrolAction->target = _toNode->getPosition();
553 }
554 else
555 {
556 _patrolAction->type = BA_RETHINK;
557 }
558 }
559
560 /**
561 * Try to set up an ambush action
562 * The idea is to check within a 11x11 tile square for a tile which is not seen by our aggroTarget,
563 * but that can be reached by him. we then intuit where we will see the target first from our covered
564 * position, and set that as our final facing.
565 * Fills out the _ambushAction with useful data.
566 */
setupAmbush()567 void AlienBAIState::setupAmbush()
568 {
569 _ambushAction->type = BA_RETHINK;
570 int bestScore = 0;
571 _ambushTUs = 0;
572 std::vector<int> path;
573
574 if (selectClosestKnownEnemy())
575 {
576 Position target;
577 const int BASE_SYSTEMATIC_SUCCESS = 100;
578 const int COVER_BONUS = 25;
579 const int FAST_PASS_THRESHOLD = 80;
580 Position origin = _save->getTileEngine()->getSightOriginVoxel(_aggroTarget);
581
582 // we'll use node positions for this, as it gives map makers a good degree of control over how the units will use the environment.
583 for (std::vector<Node*>::const_iterator i = _save->getNodes()->begin(); i != _save->getNodes()->end(); ++i)
584 {
585 Position pos = (*i)->getPosition();
586 Tile *tile = _save->getTile(pos);
587 if (tile == 0 || _save->getTileEngine()->distance(pos, _unit->getPosition()) > 10 || pos.z != _unit->getPosition().z || tile->getDangerous() ||
588 std::find(_reachableWithAttack.begin(), _reachableWithAttack.end(), _save->getTileIndex(pos)) == _reachableWithAttack.end())
589 continue; // just ignore unreachable tiles
590
591 if (_traceAI)
592 {
593 // colour all the nodes in range purple.
594 tile->setPreview(10);
595 tile->setMarkerColor(13);
596 }
597
598 // make sure we can't be seen here.
599 if (!_save->getTileEngine()->canTargetUnit(&origin, tile, &target, _aggroTarget, _unit) && !getSpottingUnits(pos))
600 {
601 _save->getPathfinding()->calculate(_unit, pos);
602 int ambushTUs = _save->getPathfinding()->getTotalTUCost();
603 // make sure we can move here
604 if (_save->getPathfinding()->getStartDirection() != -1)
605 {
606 int score = BASE_SYSTEMATIC_SUCCESS;
607 score -= ambushTUs;
608
609 // make sure our enemy can reach here too.
610 _save->getPathfinding()->calculate(_aggroTarget, pos);
611
612 if (_save->getPathfinding()->getStartDirection() != -1)
613 {
614 // ideally we'd like to be behind some cover, like say a window or a low wall.
615 if (_save->getTileEngine()->faceWindow(pos) != -1)
616 {
617 score += COVER_BONUS;
618 }
619 if (score > bestScore)
620 {
621 path = _save->getPathfinding()->copyPath();
622 bestScore = score;
623 _ambushTUs = (pos == _unit->getPosition()) ? 1 : ambushTUs;
624 _ambushAction->target = pos;
625 if (bestScore > FAST_PASS_THRESHOLD)
626 {
627 break;
628 }
629 }
630 }
631 }
632 }
633 }
634
635 if (bestScore > 0)
636 {
637 _ambushAction->type = BA_WALK;
638 // i should really make a function for this
639 origin = (_ambushAction->target * Position(16,16,24)) +
640 // 4 because -2 is eyes and 2 below that is the rifle (or at least that's my understanding)
641 Position(8,8, _unit->getHeight() + _unit->getFloatHeight() - _save->getTile(_ambushAction->target)->getTerrainLevel() - 4);
642 Position currentPos = _aggroTarget->getPosition();
643 _save->getPathfinding()->setUnit(_aggroTarget);
644 Position nextPos;
645 size_t tries = path.size();
646 // hypothetically walk the target through the path.
647 while (tries > 0)
648 {
649 _save->getPathfinding()->getTUCost(currentPos, path.back(), &nextPos, _aggroTarget, 0, false);
650 path.pop_back();
651 currentPos = nextPos;
652 Tile *tile = _save->getTile(currentPos);
653 Position target;
654 // do a virtual fire calculation
655 if (_save->getTileEngine()->canTargetUnit(&origin, tile, &target, _unit, _aggroTarget))
656 {
657 // if we can virtually fire at the hypothetical target, we know which way to face.
658 _ambushAction->finalFacing = _save->getTileEngine()->getDirectionTo(_ambushAction->target, currentPos);
659 break;
660 }
661 --tries;
662 }
663 if (_traceAI)
664 {
665 Log(LOG_INFO) << "Ambush estimation will move to " << _ambushAction->target;
666 }
667 return;
668 }
669 }
670 if (_traceAI)
671 {
672 Log(LOG_INFO) << "Ambush estimation failed";
673 }
674 }
675
676 /**
677 * Try to set up a combat action
678 * This will either be a psionic, grenade, or weapon attack,
679 * or potentially just moving to get a line of sight to a target.
680 * Fills out the _attackAction with useful data.
681 */
setupAttack()682 void AlienBAIState::setupAttack()
683 {
684 _attackAction->type = BA_RETHINK;
685 _psiAction->type = BA_NONE;
686
687 // if enemies are known to us but not necessarily visible, we can attack them with a blaster launcher or psi.
688 if (_knownEnemies)
689 {
690 if (_unit->getStats()->psiSkill && psiAction())
691 {
692 // at this point we can save some time with other calculations - the unit WILL make a psionic attack this turn.
693 return;
694 }
695 if (_blaster)
696 {
697 wayPointAction();
698 }
699 }
700
701 // if we CAN see someone, that makes them a viable target for "regular" attacks.
702 if (selectNearestTarget())
703 {
704 if (_unit->getGrenadeFromBelt())
705 {
706 grenadeAction();
707 }
708 if (_melee)
709 {
710 meleeAction();
711 }
712 if (_rifle)
713 {
714 projectileAction();
715 }
716 }
717
718 if (_attackAction->type != BA_RETHINK)
719 {
720 if (_traceAI)
721 {
722 if (_attackAction->type != BA_WALK)
723 {
724 Log(LOG_INFO) << "Attack estimation desires to shoot at " << _attackAction->target;
725 }
726 else
727 {
728 Log(LOG_INFO) << "Attack estimation desires to move to " << _attackAction->target;
729 }
730 }
731 return;
732 }
733 else if (_spottingEnemies || _unit->getAggression() < RNG::generate(0, 3))
734 {
735 // if enemies can see us, or if we're feeling lucky, we can try to spot the enemy.
736 if (findFirePoint())
737 {
738 if (_traceAI)
739 {
740 Log(LOG_INFO) << "Attack estimation desires to move to " << _attackAction->target;
741 }
742 return;
743 }
744 }
745 if (_traceAI)
746 {
747 Log(LOG_INFO) << "Attack estimation failed";
748 }
749 }
750
751 /**
752 * Attempts to find cover, and move toward it.
753 * The idea is to check within a 11x11 tile square for a tile which is not seen by our aggroTarget.
754 * If there is no such tile, we run away from the target.
755 * Fills out the _escapeAction with useful data.
756 */
setupEscape()757 void AlienBAIState::setupEscape()
758 {
759 int unitsSpottingMe = getSpottingUnits(_unit->getPosition());
760 int currentTilePreference = 15;
761 int tries = -1;
762 bool coverFound = false;
763 selectNearestTarget();
764 _escapeTUs = 0;
765
766 int dist = _aggroTarget ? _save->getTileEngine()->distance(_unit->getPosition(), _aggroTarget->getPosition()) : 0;
767
768 int bestTileScore = -100000;
769 int score = -100000;
770 Position bestTile(0, 0, 0);
771
772 Tile *tile = 0;
773
774 // weights of various factors in choosing a tile to which to withdraw
775 const int EXPOSURE_PENALTY = 10;
776 const int FIRE_PENALTY = 40;
777 const int BASE_SYSTEMATIC_SUCCESS = 100;
778 const int BASE_DESPERATE_SUCCESS = 110;
779 const int FAST_PASS_THRESHOLD = 100; // a score that's good engouh to quit the while loop early; it's subjective, hand-tuned and may need tweaking
780
781 std::vector<Position> randomTileSearch = _save->getTileSearch();
782 RNG::shuffle(randomTileSearch);
783
784 while (tries < 150 && !coverFound)
785 {
786 _escapeAction->target = _unit->getPosition(); // start looking in a direction away from the enemy
787
788 if (!_save->getTile(_escapeAction->target))
789 {
790 _escapeAction->target = _unit->getPosition(); // cornered at the edge of the map perhaps?
791 }
792
793 score = 0;
794
795 if (tries == -1)
796 {
797 // you know, maybe we should just stay where we are and not risk reaction fire...
798 // or maybe continue to wherever we were running to and not risk looking stupid
799 if (_save->getTile(_unit->lastCover) != 0)
800 {
801 _escapeAction->target = _unit->lastCover;
802 }
803 }
804 else if (tries < 121)
805 {
806 // looking for cover
807 _escapeAction->target.x += randomTileSearch[tries].x;
808 _escapeAction->target.y += randomTileSearch[tries].y;
809 score = BASE_SYSTEMATIC_SUCCESS;
810 if (_escapeAction->target == _unit->getPosition())
811 {
812 if (unitsSpottingMe > 0)
813 {
814 // maybe don't stay in the same spot? move or something if there's any point to it?
815 _escapeAction->target.x += RNG::generate(-20,20);
816 _escapeAction->target.y += RNG::generate(-20,20);
817 }
818 else
819 {
820 score += currentTilePreference;
821 }
822 }
823 }
824 else
825 {
826
827 if (tries == 121)
828 {
829 if (_traceAI)
830 {
831 Log(LOG_INFO) << "best score after systematic search was: " << bestTileScore;
832 }
833 }
834
835 score = BASE_DESPERATE_SUCCESS; // ruuuuuuun
836 _escapeAction->target = _unit->getPosition();
837 _escapeAction->target.x += RNG::generate(-10,10);
838 _escapeAction->target.y += RNG::generate(-10,10);
839 _escapeAction->target.z = _unit->getPosition().z + RNG::generate(-1,1);
840 if (_escapeAction->target.z < 0)
841 {
842 _escapeAction->target.z = 0;
843 }
844 else if (_escapeAction->target.z >= _save->getMapSizeZ())
845 {
846 _escapeAction->target.z = _unit->getPosition().z;
847 }
848 }
849
850 tries++;
851
852 // THINK, DAMN YOU
853 tile = _save->getTile(_escapeAction->target);
854 int distanceFromTarget = _aggroTarget ? _save->getTileEngine()->distance(_aggroTarget->getPosition(), _escapeAction->target) : 0;
855 if (dist >= distanceFromTarget)
856 {
857 score -= (distanceFromTarget - dist) * 10;
858 }
859 else
860 {
861 score += (distanceFromTarget - dist) * 10;
862 }
863 int spotters = 0;
864 if (!tile)
865 {
866 score = -100001; // no you can't quit the battlefield by running off the map.
867 }
868 else
869 {
870 spotters = getSpottingUnits(_escapeAction->target);
871 if (std::find(_reachable.begin(), _reachable.end(), _save->getTileIndex(_escapeAction->target)) == _reachable.end())
872 continue; // just ignore unreachable tiles
873
874 if (_spottingEnemies || spotters)
875 {
876 if (_spottingEnemies <= spotters)
877 {
878 score -= (1 + spotters - _spottingEnemies) * EXPOSURE_PENALTY; // that's for giving away our position
879 }
880 else
881 {
882 score += (_spottingEnemies - spotters) * EXPOSURE_PENALTY;
883 }
884 }
885 if (tile->getFire())
886 {
887 score -= FIRE_PENALTY;
888 }
889 if (tile->getDangerous())
890 {
891 score -= BASE_SYSTEMATIC_SUCCESS;
892 }
893
894 if (_traceAI)
895 {
896 tile->setMarkerColor(score < 0 ? 3 : (score < FAST_PASS_THRESHOLD/2 ? 8 : (score < FAST_PASS_THRESHOLD ? 9 : 5)));
897 tile->setPreview(10);
898 tile->setTUMarker(score);
899 }
900
901 }
902
903 if (tile && score > bestTileScore)
904 {
905 // calculate TUs to tile; we could be getting this from findReachable() somehow but that would break something for sure...
906 _save->getPathfinding()->calculate(_unit, _escapeAction->target);
907 if (_escapeAction->target == _unit->getPosition() || _save->getPathfinding()->getStartDirection() != -1)
908 {
909 bestTileScore = score;
910 bestTile = _escapeAction->target;
911 _escapeTUs = _save->getPathfinding()->getTotalTUCost();
912 if (_escapeAction->target == _unit->getPosition())
913 {
914 _escapeTUs = 1;
915 }
916 if (_traceAI)
917 {
918 tile->setMarkerColor(score < 0 ? 7 : (score < FAST_PASS_THRESHOLD/2 ? 10 : (score < FAST_PASS_THRESHOLD ? 4 : 5)));
919 tile->setPreview(10);
920 tile->setTUMarker(score);
921 }
922 }
923 _save->getPathfinding()->abortPath();
924 if (bestTileScore > FAST_PASS_THRESHOLD) coverFound = true; // good enough, gogogo
925 }
926 }
927 _escapeAction->target = bestTile;
928 if (_traceAI)
929 {
930 _save->getTile(_escapeAction->target)->setMarkerColor(13);
931 }
932
933 if (bestTileScore <= -100000)
934 {
935 if (_traceAI)
936 {
937 Log(LOG_INFO) << "Escape estimation failed.";
938 }
939 _escapeAction->type = BA_RETHINK; // do something, just don't look dumbstruck :P
940 return;
941 }
942 else
943 {
944 if (_traceAI)
945 {
946 Log(LOG_INFO) << "Escape estimation completed after " << tries << " tries, " << _save->getTileEngine()->distance(_unit->getPosition(), bestTile) << " squares or so away.";
947 }
948 _escapeAction->type = BA_WALK;
949 }
950 }
951
952 /**
953 * Counts how many targets, both xcom and civilian are known to this unit
954 * @return how many targets are known to us.
955 */
countKnownTargets() const956 int AlienBAIState::countKnownTargets() const
957 {
958 int knownEnemies = 0;
959 for (std::vector<BattleUnit*>::const_iterator i = _save->getUnits()->begin(); i != _save->getUnits()->end(); ++i)
960 {
961 if (validTarget(*i, true, true))
962 {
963 ++knownEnemies;
964 }
965 }
966 return knownEnemies;
967 }
968
969 /*
970 * counts how many enemies (xcom only) are spotting any given position.
971 * @param pos the Position to check for spotters.
972 * @return spotters.
973 */
getSpottingUnits(Position pos) const974 int AlienBAIState::getSpottingUnits(Position pos) const
975 {
976 // if we don't actually occupy the position being checked, we need to do a virtual LOF check.
977 bool checking = pos != _unit->getPosition();
978 int tally = 0;
979 for (std::vector<BattleUnit*>::const_iterator i = _save->getUnits()->begin(); i != _save->getUnits()->end(); ++i)
980 {
981 if (validTarget(*i, false, false))
982 {
983 int dist = _save->getTileEngine()->distance(pos, (*i)->getPosition());
984 if (dist > 20) continue;
985 Position originVoxel = _save->getTileEngine()->getSightOriginVoxel(*i);
986 originVoxel.z -= 2;
987 Position targetVoxel;
988 if (checking)
989 {
990 if (_save->getTileEngine()->canTargetUnit(&originVoxel, _save->getTile(pos), &targetVoxel, *i, _unit))
991 {
992 tally++;
993 }
994 }
995 else
996 {
997 if (_save->getTileEngine()->canTargetUnit(&originVoxel, _save->getTile(pos), &targetVoxel, *i))
998 {
999 tally++;
1000 }
1001 }
1002 }
1003 }
1004 return tally;
1005 }
1006
1007 /**
1008 * Selects the nearest known living target we can see/reach and returns the number of visible enemies.
1009 * This function includes civilians as viable targets.
1010 * @return viable targets.
1011 */
selectNearestTarget()1012 int AlienBAIState::selectNearestTarget()
1013 {
1014 int tally = 0;
1015 _closestDist= 100;
1016 _aggroTarget = 0;
1017 Position origin = _save->getTileEngine()->getSightOriginVoxel(_unit);
1018 origin.z -= 2;
1019 Position target;
1020 for (std::vector<BattleUnit*>::const_iterator i = _save->getUnits()->begin(); i != _save->getUnits()->end(); ++i)
1021 {
1022 if (validTarget(*i, true, true) &&
1023 _save->getTileEngine()->visible(_unit, (*i)->getTile()))
1024 {
1025 tally++;
1026 int dist = _save->getTileEngine()->distance(_unit->getPosition(), (*i)->getPosition());
1027 if (dist < _closestDist)
1028 {
1029 bool valid = false;
1030 if (_rifle || !_melee)
1031 {
1032 valid = _save->getTileEngine()->canTargetUnit(&origin, (*i)->getTile(), &target, _unit);
1033 }
1034 else
1035 {
1036 if (selectPointNearTarget(*i, _unit->getTimeUnits()))
1037 {
1038 int dir = _save->getTileEngine()->getDirectionTo(_attackAction->target, (*i)->getPosition());
1039 valid = _save->getTileEngine()->validMeleeRange(_attackAction->target, dir, _unit, *i, 0);
1040 }
1041 }
1042 if (valid)
1043 {
1044 _closestDist = dist;
1045 _aggroTarget = *i;
1046 }
1047 }
1048 }
1049 }
1050 if (_aggroTarget)
1051 {
1052 return tally;
1053 }
1054
1055 return 0;
1056 }
1057
1058 /**
1059 * Selects the nearest known living Xcom unit.
1060 * used for ambush calculations
1061 * @return if we found one.
1062 */
selectClosestKnownEnemy()1063 bool AlienBAIState::selectClosestKnownEnemy()
1064 {
1065 _aggroTarget = 0;
1066 int minDist = 255;
1067 for (std::vector<BattleUnit*>::iterator i = _save->getUnits()->begin(); i != _save->getUnits()->end(); ++i)
1068 {
1069 if (validTarget(*i, true, false))
1070 {
1071 int dist = _save->getTileEngine()->distance((*i)->getPosition(), _unit->getPosition());
1072 if (dist < minDist)
1073 {
1074 minDist = dist;
1075 _aggroTarget = *i;
1076 }
1077 }
1078 }
1079 return _aggroTarget != 0;
1080 }
1081
1082 /**
1083 * Selects a random known living Xcom or civilian unit.
1084 * @return if we found one.
1085 */
selectRandomTarget()1086 bool AlienBAIState::selectRandomTarget()
1087 {
1088 int farthest = -100;
1089 _aggroTarget = 0;
1090
1091 for (std::vector<BattleUnit*>::const_iterator i = _save->getUnits()->begin(); i != _save->getUnits()->end(); ++i)
1092 {
1093 if (validTarget(*i, true, true))
1094 {
1095 int dist = RNG::generate(0,20) - _save->getTileEngine()->distance(_unit->getPosition(), (*i)->getPosition());
1096 if (dist > farthest)
1097 {
1098 farthest = dist;
1099 _aggroTarget = *i;
1100 }
1101 }
1102 }
1103 return _aggroTarget != 0;
1104 }
1105
1106 /**
1107 * Selects a point near enough to our target to perform a melee attack.
1108 * @param target Pointer to a target.
1109 * @param maxTUs Maximum time units the path to the target can cost.
1110 * @return True if a point was found.
1111 */
selectPointNearTarget(BattleUnit * target,int maxTUs) const1112 bool AlienBAIState::selectPointNearTarget(BattleUnit *target, int maxTUs) const
1113 {
1114 int size = _unit->getArmor()->getSize();
1115 int targetsize = target->getArmor()->getSize();
1116 bool returnValue = false;
1117 int distance = 1000;
1118 for (int z = -1; z <= 1; ++z)
1119 {
1120 for (int x = -size; x <= targetsize; ++x)
1121 {
1122 for (int y = -size; y <= targetsize; ++y)
1123 {
1124 if (x || y) // skip the unit itself
1125 {
1126 Position checkPath = target->getPosition() + Position (x, y, z);
1127 if (_save->getTile(checkPath) == 0 || std::find(_reachable.begin(), _reachable.end(), _save->getTileIndex(checkPath)) == _reachable.end())
1128 continue;
1129 int dir = _save->getTileEngine()->getDirectionTo(checkPath, target->getPosition());
1130 bool valid = _save->getTileEngine()->validMeleeRange(checkPath, dir, _unit, target, 0);
1131 bool fitHere = _save->setUnitPosition(_unit, checkPath, true);
1132
1133 if (valid && fitHere && !_save->getTile(checkPath)->getDangerous())
1134 {
1135 _save->getPathfinding()->calculate(_unit, checkPath, 0, maxTUs);
1136 if (_save->getPathfinding()->getStartDirection() != -1 && _save->getPathfinding()->getPath().size() < distance)
1137 {
1138 _attackAction->target = checkPath;
1139 returnValue = true;
1140 distance = _save->getPathfinding()->getPath().size();
1141 }
1142 _save->getPathfinding()->abortPath();
1143 }
1144 }
1145 }
1146 }
1147 }
1148 return returnValue;
1149 }
1150
1151 /**
1152 * Selects an AI mode based on a number of factors, some RNG and the results of the rest of the determinations.
1153 */
evaluateAIMode()1154 void AlienBAIState::evaluateAIMode()
1155 {
1156 if (_unit->getCharging() && _attackAction->type != BA_RETHINK)
1157 {
1158 _AIMode = AI_COMBAT;
1159 return;
1160 }
1161 // don't try to run away as often if we're a melee type, and really don't try to run away if we have a viable melee target, or we still have 50% or more TUs remaining.
1162 int escapeOdds = 15;
1163 if (_melee)
1164 {
1165 escapeOdds = 12;
1166 }
1167 if (_unit->getTimeUnits() > _unit->getStats()->tu / 2 || _unit->getCharging())
1168 {
1169 escapeOdds = 5;
1170 }
1171 int ambushOdds = 12;
1172 int combatOdds = 20;
1173 // we're less likely to patrol if we see enemies.
1174 int patrolOdds = _visibleEnemies ? 15 : 30;
1175
1176 // the enemy sees us, we should take retreat into consideration, and forget about patrolling for now.
1177 if (_spottingEnemies)
1178 {
1179 patrolOdds = 0;
1180 if (_escapeTUs == 0)
1181 {
1182 setupEscape();
1183 }
1184 }
1185
1186 // melee/blaster units shouldn't consider ambush
1187 if (!_rifle || _ambushTUs == 0)
1188 {
1189 ambushOdds = 0;
1190 if (_melee)
1191 {
1192 combatOdds *= 1.3;
1193 }
1194 }
1195
1196 // if we KNOW there are enemies around...
1197 if (_knownEnemies)
1198 {
1199 if (_knownEnemies == 1)
1200 {
1201 combatOdds *= 1.2;
1202 }
1203
1204 if (_escapeTUs == 0)
1205 {
1206 if (selectClosestKnownEnemy())
1207 {
1208 setupEscape();
1209 }
1210 else
1211 {
1212 escapeOdds = 0;
1213 }
1214 }
1215 }
1216 else
1217 {
1218 combatOdds = 0;
1219 escapeOdds = 0;
1220 }
1221
1222 // take our current mode into consideration
1223 switch (_AIMode)
1224 {
1225 case AI_PATROL:
1226 patrolOdds *= 1.1;
1227 break;
1228 case AI_AMBUSH:
1229 ambushOdds *= 1.1;
1230 break;
1231 case AI_COMBAT:
1232 combatOdds *= 1.1;
1233 break;
1234 case AI_ESCAPE:
1235 escapeOdds *= 1.1;
1236 break;
1237 }
1238
1239 // take our overall health into consideration
1240 if (_unit->getHealth() < _unit->getStats()->health / 3)
1241 {
1242 escapeOdds *= 1.7;
1243 combatOdds *= 0.6;
1244 ambushOdds *= 0.75;
1245 }
1246 else if (_unit->getHealth() < 2 * (_unit->getStats()->health / 3))
1247 {
1248 escapeOdds *= 1.4;
1249 combatOdds *= 0.8;
1250 ambushOdds *= 0.8;
1251 }
1252 else if (_unit->getHealth() < _unit->getStats()->health)
1253 {
1254 escapeOdds *= 1.1;
1255 }
1256
1257 // take our aggression into consideration
1258 switch (_unit->getAggression())
1259 {
1260 case 0:
1261 escapeOdds *= 1.4;
1262 combatOdds *= 0.7;
1263 break;
1264 case 1:
1265 ambushOdds *= 1.1;
1266 break;
1267 case 2:
1268 combatOdds *= 1.4;
1269 escapeOdds *= 0.7;
1270 break;
1271 default:
1272 combatOdds *= std::max(0.1, std::min(2.0, 1.2 + (_unit->getAggression() / 10)));
1273 escapeOdds *= std::min(2.0, std::max(0.1, 0.9 - (_unit->getAggression() / 10)));
1274 break;
1275 }
1276
1277 if (_AIMode == AI_COMBAT)
1278 {
1279 ambushOdds *= 1.5;
1280 }
1281
1282 // factor in the spotters.
1283 if (_spottingEnemies)
1284 {
1285 escapeOdds = 10 * escapeOdds * (_spottingEnemies + 10) / 100;
1286 combatOdds = 5 * combatOdds * (_spottingEnemies + 20) / 100;
1287 }
1288 else
1289 {
1290 escapeOdds /= 2;
1291 }
1292
1293 // factor in visible enemies.
1294 if (_visibleEnemies)
1295 {
1296 combatOdds = 10 * combatOdds * (_visibleEnemies + 10) /100;
1297 if (_closestDist < 5)
1298 {
1299 ambushOdds = 0;
1300 }
1301 }
1302 // make sure we have an ambush lined up, or don't even consider it.
1303 if (_ambushTUs)
1304 {
1305 ambushOdds *= 1.7;
1306 }
1307 else
1308 {
1309 ambushOdds = 0;
1310 }
1311
1312 // factor in mission type
1313 if (_save->getMissionType() == "STR_BASE_DEFENSE")
1314 {
1315 escapeOdds *= 0.75;
1316 ambushOdds *= 0.6;
1317 }
1318
1319 // generate a random number to represent our decision.
1320 int decision = RNG::generate(1, patrolOdds + ambushOdds + escapeOdds + combatOdds);
1321
1322 if (decision > escapeOdds)
1323 {
1324 if (decision > escapeOdds + ambushOdds)
1325 {
1326 if (decision > escapeOdds + ambushOdds + combatOdds)
1327 {
1328 _AIMode = AI_PATROL;
1329 }
1330 else
1331 {
1332 _AIMode = AI_COMBAT;
1333 }
1334 }
1335 else
1336 {
1337 _AIMode = AI_AMBUSH;
1338 }
1339 }
1340 else
1341 {
1342 _AIMode = AI_ESCAPE;
1343 }
1344
1345 // if the aliens are cheating, or the unit is charging, enforce combat as a priority.
1346 if (_save->isCheating() || _unit->getCharging() != 0)
1347 {
1348 _AIMode = AI_COMBAT;
1349 }
1350
1351
1352 // enforce the validity of our decision, and try fallback behaviour according to priority.
1353 if (_AIMode == AI_COMBAT)
1354 {
1355 if (_save->getTile(_attackAction->target) && _save->getTile(_attackAction->target)->getUnit())
1356 {
1357 if (_attackAction->type != BA_RETHINK)
1358 {
1359 return;
1360 }
1361 if (findFirePoint())
1362 {
1363 return;
1364 }
1365 }
1366 else if (selectRandomTarget() && findFirePoint())
1367 {
1368 return;
1369 }
1370 _AIMode = AI_PATROL;
1371 }
1372
1373 if (_AIMode == AI_PATROL)
1374 {
1375 if (_toNode)
1376 {
1377 return;
1378 }
1379 _AIMode = AI_AMBUSH;
1380 }
1381
1382 if (_AIMode == AI_AMBUSH)
1383 {
1384 if (_ambushTUs != 0)
1385 {
1386 return;
1387 }
1388 _AIMode = AI_ESCAPE;
1389 }
1390 }
1391
1392 /**
1393 * Find a position where we can see our target, and move there.
1394 * check the 11x11 grid for a position nearby where we can potentially target him.
1395 * @return True if a possible position was found.
1396 */
findFirePoint()1397 bool AlienBAIState::findFirePoint()
1398 {
1399 if (!selectClosestKnownEnemy())
1400 return false;
1401 std::vector<Position> randomTileSearch = _save->getTileSearch();
1402 RNG::shuffle(randomTileSearch);
1403 Position target;
1404 const int BASE_SYSTEMATIC_SUCCESS = 100;
1405 const int FAST_PASS_THRESHOLD = 125;
1406 int bestScore = 0;
1407 _attackAction->type = BA_RETHINK;
1408 for (std::vector<Position>::const_iterator i = randomTileSearch.begin(); i != randomTileSearch.end(); ++i)
1409 {
1410 Position pos = _unit->getPosition() + *i;
1411 Tile *tile = _save->getTile(pos);
1412 if (tile == 0 ||
1413 std::find(_reachableWithAttack.begin(), _reachableWithAttack.end(), _save->getTileIndex(pos)) == _reachableWithAttack.end())
1414 continue;
1415 int score = 0;
1416 // i should really make a function for this
1417 Position origin = (pos * Position(16,16,24)) +
1418 // 4 because -2 is eyes and 2 below that is the rifle (or at least that's my understanding)
1419 Position(8,8, _unit->getHeight() + _unit->getFloatHeight() - tile->getTerrainLevel() - 4);
1420
1421 if (_save->getTileEngine()->canTargetUnit(&origin, _aggroTarget->getTile(), &target, _unit))
1422 {
1423 _save->getPathfinding()->calculate(_unit, pos);
1424 // can move here
1425 if (_save->getPathfinding()->getStartDirection() != -1)
1426 {
1427 score = BASE_SYSTEMATIC_SUCCESS - getSpottingUnits(pos) * 10;
1428 score += _unit->getTimeUnits() - _save->getPathfinding()->getTotalTUCost();
1429 if (!_aggroTarget->checkViewSector(pos))
1430 {
1431 score += 10;
1432 }
1433 if (score > bestScore)
1434 {
1435 bestScore = score;
1436 _attackAction->target = pos;
1437 _attackAction->finalFacing = _save->getTileEngine()->getDirectionTo(pos, _aggroTarget->getPosition());
1438 if (score > FAST_PASS_THRESHOLD)
1439 {
1440 break;
1441 }
1442 }
1443 }
1444 }
1445 }
1446
1447 if (bestScore > 70)
1448 {
1449 _attackAction->type = BA_WALK;
1450 if (_traceAI)
1451 {
1452 Log(LOG_INFO) << "Firepoint found at " << _attackAction->target << ", with a score of: " << bestScore;
1453 }
1454 return true;
1455 }
1456 if (_traceAI)
1457 {
1458 Log(LOG_INFO) << "Firepoint failed, best estimation was: " << _attackAction->target << ", with a score of: " << bestScore;
1459 }
1460
1461 return false;
1462 }
1463
1464 /**
1465 * Decides if it worth our while to create an explosion here.
1466 * @param targetPos The target's position.
1467 * @param attackingUnit The attacking unit.
1468 * @param radius How big the explosion will be.
1469 * @param diff Game difficulty.
1470 * @param grenade Is the explosion coming from a grenade?
1471 * @return True if it is worthwhile creating an explosion in the target position.
1472 */
explosiveEfficacy(Position targetPos,BattleUnit * attackingUnit,int radius,int diff,bool grenade) const1473 bool AlienBAIState::explosiveEfficacy(Position targetPos, BattleUnit *attackingUnit, int radius, int diff, bool grenade) const
1474 {
1475 // i hate the player and i want him dead, but i don't want to piss him off.
1476 if (_save->getTurn() < 3)
1477 return false;
1478 if (diff == -1)
1479 {
1480 diff = (int)(_save->getBattleState()->getGame()->getSavedGame()->getDifficulty());
1481 }
1482 int distance = _save->getTileEngine()->distance(attackingUnit->getPosition(), targetPos);
1483 int injurylevel = attackingUnit->getStats()->health - attackingUnit->getHealth();
1484 int desperation = (100 - attackingUnit->getMorale()) / 10;
1485 int enemiesAffected = 0;
1486 // if we're below 1/3 health, let's assume things are dire, and increase desperation.
1487 if (injurylevel > (attackingUnit->getStats()->health / 3) * 2)
1488 desperation += 3;
1489
1490 int efficacy = desperation;
1491
1492 // don't go kamikaze unless we're already doomed.
1493 if (abs(attackingUnit->getPosition().z - targetPos.z) <= Options::battleExplosionHeight && distance <= radius)
1494 {
1495 efficacy -= 4;
1496 }
1497
1498 // we don't want to ruin our own base, but we do want to ruin XCom's day.
1499 if (_save->getMissionType() == "STR_ALIEN_BASE_ASSAULT") efficacy -= 3;
1500 else if (_save->getMissionType() == "STR_BASE_DEFENSE" || _save->getMissionType() == "STR_TERROR_MISSION") efficacy += 3;
1501
1502 // allow difficulty to have its influence
1503 efficacy += diff/2;
1504
1505 // account for the unit we're targetting
1506 BattleUnit *target = _save->getTile(targetPos)->getUnit();
1507 if (target)
1508 {
1509 ++enemiesAffected;
1510 ++efficacy;
1511 }
1512
1513 for (std::vector<BattleUnit*>::iterator i = _save->getUnits()->begin(); i != _save->getUnits()->end(); ++i)
1514 {
1515 // don't grenade dead guys
1516 if (!(*i)->isOut() &&
1517 // don't count ourself twice
1518 (*i) != attackingUnit &&
1519 // don't count the target twice
1520 (*i) != target &&
1521 // don't count units that probably won't be affected cause they're out of range
1522 abs((*i)->getPosition().z - targetPos.z) <= Options::battleExplosionHeight &&
1523 _save->getTileEngine()->distance((*i)->getPosition(), targetPos) <= radius)
1524 {
1525 // don't count people who were already grenaded this turn
1526 if ((*i)->getTile()->getDangerous() ||
1527 // don't count units we don't know about
1528 ((*i)->getFaction() == FACTION_PLAYER && (*i)->getTurnsSinceSpotted() > _intelligence))
1529 continue;
1530
1531 // trace a line from the grenade origin to the unit we're checking against
1532 Position voxelPosA = Position ((targetPos.x * 16)+8, (targetPos.y * 16)+8, (targetPos.z * 24)+12);
1533 Position voxelPosB = Position (((*i)->getPosition().x * 16)+8, ((*i)->getPosition().y * 16)+8, ((*i)->getPosition().z * 24)+12);
1534 std::vector<Position> traj;
1535 int collidesWith = _save->getTileEngine()->calculateLine(voxelPosA, voxelPosB, false, &traj, target, true, false, *i);
1536
1537 if (collidesWith == V_UNIT && traj.front() / Position(16,16,24) == (*i)->getPosition())
1538 {
1539 if ((*i)->getFaction() == FACTION_PLAYER)
1540 {
1541 ++enemiesAffected;
1542 ++efficacy;
1543 }
1544 else if ((*i)->getFaction() == attackingUnit->getFaction())
1545 efficacy -= 2; // friendlies count double
1546 }
1547 }
1548 }
1549 // don't throw grenades at single targets, unless morale is in the danger zone
1550 // or we're halfway towards panicking while bleeding to death.
1551 if (grenade && desperation < 6 && enemiesAffected < 2)
1552 {
1553 return false;
1554 }
1555 return (efficacy > 0 || enemiesAffected >= 10);
1556 }
1557
1558 /**
1559 * Attempts to take a melee attack/charge an enemy we can see.
1560 * Melee targetting: we can see an enemy, we can move to it so we're charging blindly toward an enemy.
1561 */
meleeAction()1562 void AlienBAIState::meleeAction()
1563 {
1564 if (_aggroTarget != 0 && !_aggroTarget->isOut())
1565 {
1566 if (_save->getTileEngine()->validMeleeRange(_unit, _aggroTarget, _save->getTileEngine()->getDirectionTo(_unit->getPosition(), _aggroTarget->getPosition())))
1567 {
1568 meleeAttack();
1569 return;
1570 }
1571 }
1572 int attackCost = _unit->getActionTUs(BA_HIT, _unit->getMainHandWeapon());
1573 int chargeReserve = _unit->getTimeUnits() - attackCost;
1574 int distance = (chargeReserve / 4) + 1;
1575 _aggroTarget = 0;
1576 for (std::vector<BattleUnit*>::const_iterator i = _save->getUnits()->begin(); i != _save->getUnits()->end(); ++i)
1577 {
1578 int newDistance = _save->getTileEngine()->distance(_unit->getPosition(), (*i)->getPosition());
1579 if (newDistance > 20 ||
1580 !validTarget(*i, true, true))
1581 continue;
1582 //pick closest living unit that we can move to
1583 if ((newDistance < distance || newDistance == 1) && !(*i)->isOut())
1584 {
1585 if (newDistance == 1 || selectPointNearTarget(*i, chargeReserve))
1586 {
1587 _aggroTarget = (*i);
1588 _attackAction->type = BA_WALK;
1589 _unit->setCharging(_aggroTarget);
1590 distance = newDistance;
1591 }
1592
1593 }
1594 }
1595 if (_aggroTarget != 0)
1596 {
1597 if (_save->getTileEngine()->validMeleeRange(_unit, _aggroTarget, _save->getTileEngine()->getDirectionTo(_unit->getPosition(), _aggroTarget->getPosition())))
1598 {
1599 meleeAttack();
1600 }
1601 }
1602 if (_traceAI && _aggroTarget) { Log(LOG_INFO) << "AlienBAIState::meleeAction:" << " [target]: " << (_aggroTarget->getId()) << " at: " << _attackAction->target; }
1603 if (_traceAI && _aggroTarget) { Log(LOG_INFO) << "CHARGE!"; }
1604 }
1605
1606 /**
1607 * Attempts to fire a waypoint projectile at an enemy we, or one of our teammates sees.
1608 *
1609 * Waypoint targeting: pick from any units currently spotted by our allies.
1610 */
wayPointAction()1611 void AlienBAIState::wayPointAction()
1612 {
1613 _aggroTarget = 0;
1614 for (std::vector<BattleUnit*>::const_iterator i = _save->getUnits()->begin(); i != _save->getUnits()->end() && _aggroTarget == 0; ++i)
1615 {
1616 if (!validTarget(*i, true, true))
1617 continue;
1618 _save->getPathfinding()->calculate(_unit, (*i)->getPosition(), *i, -1);
1619 if (_save->getPathfinding()->getStartDirection() != -1 &&
1620 explosiveEfficacy((*i)->getPosition(), _unit, (_unit->getMainHandWeapon()->getAmmoItem()->getRules()->getPower()/20)+1, _attackAction->diff))
1621 {
1622 _aggroTarget = *i;
1623 }
1624 _save->getPathfinding()->abortPath();
1625 }
1626
1627 if (_aggroTarget != 0)
1628 {
1629 _attackAction->type = BA_LAUNCH;
1630 _attackAction->TU = _unit->getActionTUs(BA_LAUNCH, _attackAction->weapon);
1631 if (_attackAction->TU > _unit->getTimeUnits())
1632 {
1633 _attackAction->type = BA_RETHINK;
1634 return;
1635 }
1636 _attackAction->waypoints.clear();
1637
1638 int PathDirection;
1639 int CollidesWith;
1640 Position LastWayPoint = _unit->getPosition();
1641 Position LastPosition = _unit->getPosition();
1642 Position CurrentPosition = _unit->getPosition();
1643 Position DirectionVector;
1644
1645 _save->getPathfinding()->calculate(_unit, _aggroTarget->getPosition(), _aggroTarget, -1);
1646 PathDirection = _save->getPathfinding()->dequeuePath();
1647 while (PathDirection != -1)
1648 {
1649 LastPosition = CurrentPosition;
1650 _save->getPathfinding()->directionToVector(PathDirection, &DirectionVector);
1651 CurrentPosition = CurrentPosition + DirectionVector;
1652 Position voxelPosA ((CurrentPosition.x * 16)+8, (CurrentPosition.y * 16)+8, (CurrentPosition.z * 24)+16);
1653 Position voxelPosb ((LastWayPoint.x * 16)+8, (LastWayPoint.y * 16)+8, (LastWayPoint.z * 24)+16);
1654 CollidesWith = _save->getTileEngine()->calculateLine(voxelPosA, voxelPosb, false, 0, _unit, true);
1655 if (CollidesWith > V_EMPTY && CollidesWith < V_UNIT)
1656 {
1657 _attackAction->waypoints.push_back(LastPosition);
1658 LastWayPoint = LastPosition;
1659 }
1660 else if (CollidesWith == V_UNIT)
1661 {
1662 BattleUnit* target = _save->getTile(CurrentPosition)->getUnit();
1663 if (target == _aggroTarget)
1664 {
1665 _attackAction->waypoints.push_back(CurrentPosition);
1666 LastWayPoint = CurrentPosition;
1667 }
1668 }
1669
1670 PathDirection = _save->getPathfinding()->dequeuePath();
1671 }
1672 _attackAction->target = _attackAction->waypoints.front();
1673 if ((int) _attackAction->waypoints.size() > 6 + (_attackAction->diff * 2) || LastWayPoint != _aggroTarget->getPosition())
1674 {
1675 _attackAction->type = BA_RETHINK;
1676 }
1677 }
1678 }
1679
1680 /**
1681 * Attempts to fire at an enemy we can see.
1682 *
1683 * Regular targeting: we can see an enemy, we have a gun, let's try to shoot.
1684 */
projectileAction()1685 void AlienBAIState::projectileAction()
1686 {
1687 _attackAction->target = _aggroTarget->getPosition();
1688 if (!_attackAction->weapon->getAmmoItem()->getRules()->getExplosionRadius() ||
1689 explosiveEfficacy(_aggroTarget->getPosition(), _unit, _attackAction->weapon->getAmmoItem()->getRules()->getExplosionRadius(), _attackAction->diff))
1690 {
1691 selectFireMethod();
1692 }
1693 }
1694
1695 /**
1696 * Selects a fire method based on range, time units, and time units reserved for cover.
1697 */
selectFireMethod()1698 void AlienBAIState::selectFireMethod()
1699 {
1700 int distance = _save->getTileEngine()->distance(_unit->getPosition(), _attackAction->target);
1701 _attackAction->type = BA_RETHINK;
1702 int tuAuto = _attackAction->weapon->getRules()->getTUAuto();
1703 int tuSnap = _attackAction->weapon->getRules()->getTUSnap();
1704 int tuAimed = _attackAction->weapon->getRules()->getTUAimed();
1705 int currentTU = _unit->getTimeUnits();
1706
1707 if (distance < 4)
1708 {
1709 if ( tuAuto && currentTU >= _unit->getActionTUs(BA_AUTOSHOT, _attackAction->weapon) )
1710 {
1711 _attackAction->type = BA_AUTOSHOT;
1712 return;
1713 }
1714 if ( !tuSnap || currentTU < _unit->getActionTUs(BA_SNAPSHOT, _attackAction->weapon) )
1715 {
1716 if ( tuAimed && currentTU >= _unit->getActionTUs(BA_AIMEDSHOT, _attackAction->weapon) )
1717 {
1718 _attackAction->type = BA_AIMEDSHOT;
1719 }
1720 return;
1721 }
1722 _attackAction->type = BA_SNAPSHOT;
1723 return;
1724 }
1725
1726
1727 if ( distance > 12 )
1728 {
1729 if ( tuAimed && currentTU >= _unit->getActionTUs(BA_AIMEDSHOT, _attackAction->weapon) )
1730 {
1731 _attackAction->type = BA_AIMEDSHOT;
1732 return;
1733 }
1734 if ( distance < 20
1735 && tuSnap
1736 && currentTU >= _unit->getActionTUs(BA_SNAPSHOT, _attackAction->weapon) )
1737 {
1738 _attackAction->type = BA_SNAPSHOT;
1739 return;
1740 }
1741 }
1742
1743 if ( tuSnap && currentTU >= _unit->getActionTUs(BA_SNAPSHOT, _attackAction->weapon) )
1744 {
1745 _attackAction->type = BA_SNAPSHOT;
1746 return;
1747 }
1748 if ( tuAimed && currentTU >= _unit->getActionTUs(BA_AIMEDSHOT, _attackAction->weapon) )
1749 {
1750 _attackAction->type = BA_AIMEDSHOT;
1751 return;
1752 }
1753 if ( tuAuto && currentTU >= _unit->getActionTUs(BA_AUTOSHOT, _attackAction->weapon) )
1754 {
1755 _attackAction->type = BA_AUTOSHOT;
1756 }
1757 }
1758
1759 /**
1760 * Evaluates whether to throw a grenade at an enemy (or group of enemies) we can see.
1761 */
grenadeAction()1762 void AlienBAIState::grenadeAction()
1763 {
1764 // do we have a grenade on our belt?
1765 BattleItem *grenade = _unit->getGrenadeFromBelt();
1766 // distance must be more than X tiles, otherwise it's too dangerous to play with explosives
1767 if (explosiveEfficacy(_aggroTarget->getPosition(), _unit, grenade->getRules()->getExplosionRadius(), _attackAction->diff, true))
1768 {
1769 int tu = 4; // 4TUs for picking up the grenade
1770 tu += _unit->getActionTUs(BA_PRIME, grenade);
1771 tu += _unit->getActionTUs(BA_THROW, grenade);
1772 // do we have enough TUs to prime and throw the grenade?
1773 if (tu <= _unit->getStats()->tu)
1774 {
1775 BattleAction action;
1776 action.weapon = grenade;
1777 action.target = _aggroTarget->getPosition();
1778 action.type = BA_THROW;
1779 action.actor = _unit;
1780 Position originVoxel = _save->getTileEngine()->getOriginVoxel(action, 0);
1781 Position targetVoxel = action.target * Position (16,16,24) + Position (8,8, (2 + -_save->getTile(action.target)->getTerrainLevel()));
1782 // are we within range?
1783 if (_save->getTileEngine()->validateThrow(action, originVoxel, targetVoxel))
1784 {
1785 _attackAction->weapon = grenade;
1786 _attackAction->target = action.target;
1787 _attackAction->type = BA_THROW;
1788 _rifle = false;
1789 _melee = false;
1790 }
1791 }
1792 }
1793 }
1794
1795 /**
1796 * Attempts a psionic attack on an enemy we "know of".
1797 *
1798 * Psionic targetting: pick from any of the "exposed" units.
1799 * Exposed means they have been previously spotted, and are therefore "known" to the AI,
1800 * regardless of whether we can see them or not, because we're psychic.
1801 * @return True if a psionic attack is performed.
1802 */
psiAction()1803 bool AlienBAIState::psiAction()
1804 {
1805 RuleItem *psiWeaponRules = _save->getBattleGame()->getRuleset()->getItem("ALIEN_PSI_WEAPON");
1806 int cost = psiWeaponRules->getTUUse();
1807 if (!psiWeaponRules->getFlatRate())
1808 {
1809 cost = (int)floor(_unit->getStats()->tu * cost / 100.0f);
1810 }
1811 bool LOSRequired = psiWeaponRules->isLOSRequired();
1812
1813 _aggroTarget = 0;
1814 // don't let mind controlled soldiers mind control other soldiers.
1815 if (_unit->getOriginalFaction() != FACTION_PLAYER
1816 // and we have the required 25 TUs and can still make it to cover
1817 && _unit->getTimeUnits() > _escapeTUs + cost
1818 // and we didn't already do a psi action this round
1819 && !_didPsi)
1820 {
1821 int psiAttackStrength = _unit->getStats()->psiSkill * _unit->getStats()->psiStrength / 50;
1822 int chanceToAttack = 0;
1823
1824 for (std::vector<BattleUnit*>::const_iterator i = _save->getUnits()->begin(); i != _save->getUnits()->end(); ++i)
1825 {
1826 // don't target tanks
1827 if ((*i)->getArmor()->getSize() == 1 &&
1828 validTarget(*i, true, false) &&
1829 // they must be player units
1830 (*i)->getOriginalFaction() == FACTION_PLAYER &&
1831 (!LOSRequired ||
1832 std::find(_unit->getVisibleUnits()->begin(), _unit->getVisibleUnits()->end(), *i) != _unit->getVisibleUnits()->end()))
1833 {
1834 int chanceToAttackMe = psiAttackStrength
1835 + (((*i)->getStats()->psiSkill > 0) ? (*i)->getStats()->psiSkill * -0.4 : 0)
1836 - _save->getTileEngine()->distance((*i)->getPosition(), _unit->getPosition())
1837 - ((*i)->getStats()->psiStrength)
1838 + RNG::generate(55, 105);
1839
1840 if (chanceToAttackMe > chanceToAttack)
1841 {
1842 chanceToAttack = chanceToAttackMe;
1843 _aggroTarget = *i;
1844 }
1845 }
1846 }
1847
1848 if (!_aggroTarget || !chanceToAttack) return false;
1849
1850 if (_visibleEnemies && _attackAction->weapon && _attackAction->weapon->getAmmoItem())
1851 {
1852 if (_attackAction->weapon->getAmmoItem()->getRules()->getPower() >= chanceToAttack)
1853 {
1854 return false;
1855 }
1856 }
1857 else if (RNG::generate(35, 155) >= chanceToAttack)
1858 {
1859 return false;
1860 }
1861
1862 if (_traceAI)
1863 {
1864 Log(LOG_INFO) << "making a psionic attack this turn";
1865 }
1866
1867 if (chanceToAttack >= 30)
1868 {
1869 int controlOdds = 40;
1870 int morale = _aggroTarget->getMorale();
1871 int bravery = (110 - _aggroTarget->getStats()->bravery) / 10;
1872 if (bravery > 6)
1873 controlOdds -= 15;
1874 if (bravery < 4)
1875 controlOdds += 15;
1876 if (morale >= 40)
1877 {
1878 if (morale - 10 * bravery < 50)
1879 controlOdds -= 15;
1880 }
1881 else
1882 {
1883 controlOdds += 15;
1884 }
1885 if (!morale)
1886 {
1887 controlOdds = 100;
1888 }
1889 if (RNG::percent(controlOdds))
1890 {
1891 _psiAction->type = BA_MINDCONTROL;
1892 _psiAction->target = _aggroTarget->getPosition();
1893 return true;
1894 }
1895 }
1896 _psiAction->type = BA_PANIC;
1897 _psiAction->target = _aggroTarget->getPosition();
1898 return true;
1899 }
1900 return false;
1901 }
1902
1903 /**
1904 * Performs a melee attack action.
1905 */
meleeAttack()1906 void AlienBAIState::meleeAttack()
1907 {
1908 _unit->lookAt(_aggroTarget->getPosition() + Position(_unit->getArmor()->getSize()-1, _unit->getArmor()->getSize()-1, 0), false);
1909 while (_unit->getStatus() == STATUS_TURNING)
1910 _unit->turn();
1911 if (_traceAI) { Log(LOG_INFO) << "Attack unit: " << _aggroTarget->getId(); }
1912 _attackAction->target = _aggroTarget->getPosition();
1913 _attackAction->type = BA_HIT;
1914 }
1915
validTarget(BattleUnit * unit,bool assessDanger,bool includeCivs) const1916 bool AlienBAIState::validTarget(BattleUnit *unit, bool assessDanger, bool includeCivs) const
1917 {
1918 // ignore units that are dead/unconscious
1919 if (unit->isOut() ||
1920 // they must be units that we "know" about
1921 _intelligence < unit->getTurnsSinceSpotted() ||
1922 // they haven't been grenaded
1923 (assessDanger && unit->getTile()->getDangerous()) ||
1924 // and they mustn't be on our side
1925 unit->getFaction() == FACTION_HOSTILE)
1926 {
1927 return false;
1928 }
1929
1930 if (includeCivs)
1931 {
1932 return true;
1933 }
1934
1935 return unit->getFaction() == FACTION_PLAYER;
1936 }
1937 }
1938