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 "CivilianBAIState.h"
22 #include "TileEngine.h"
23 #include "Pathfinding.h"
24 #include "BattlescapeState.h"
25 #include "../Savegame/BattleUnit.h"
26 #include "../Savegame/SavedBattleGame.h"
27 #include "../Savegame/Node.h"
28 #include "../Engine/RNG.h"
29 #include "../Engine/Logger.h"
30 #include "../Engine/Options.h"
31 #include "../Ruleset/Armor.h"
32 #include "../Savegame/Tile.h"
33
34 namespace OpenXcom
35 {
36
37 /**
38 * Sets up a CivilianBAIState.
39 * @param save Pointer to the battle game.
40 * @param unit Pointer to the unit.
41 * @param node Pointer to the node the unit originates from.
42 */
CivilianBAIState(SavedBattleGame * save,BattleUnit * unit,Node * node)43 CivilianBAIState::CivilianBAIState(SavedBattleGame *save, BattleUnit *unit, Node *node) : BattleAIState(save, unit), _aggroTarget(0), _escapeTUs(0), _AIMode(0), _visibleEnemies(0), _spottingEnemies(0), _fromNode(node), _toNode(0)
44 {
45 _traceAI = Options::traceAI;
46 _escapeAction = new BattleAction();
47 _patrolAction = new BattleAction();
48 }
49
50 /**
51 * Deletes the CivilianBAIState.
52 */
~CivilianBAIState()53 CivilianBAIState::~CivilianBAIState()
54 {
55 delete _escapeAction;
56 delete _patrolAction;
57 }
58
59 /**
60 * Loads the AI state from a YAML file.
61 * @param node YAML node.
62 */
load(const YAML::Node & node)63 void CivilianBAIState::load(const YAML::Node &node)
64 {
65 int fromNodeID, toNodeID;
66 fromNodeID = node["fromNode"].as<int>(-1);
67 toNodeID = node["toNode"].as<int>(-1);
68 _AIMode = node["AIMode"].as<int>(0);
69 if (fromNodeID != -1)
70 {
71 _fromNode = _save->getNodes()->at(fromNodeID);
72 }
73 if (toNodeID != -1)
74 {
75 _toNode = _save->getNodes()->at(toNodeID);
76 }
77 }
78
79 /**
80 * Saves the AI state to a YAML file.
81 * @return YAML node.
82 */
save() const83 YAML::Node CivilianBAIState::save() const
84 {
85 int fromNodeID = -1, toNodeID = -1;
86 if (_fromNode)
87 fromNodeID = _fromNode->getID();
88 if (_toNode)
89 toNodeID = _toNode->getID();
90
91 YAML::Node node;
92 node["fromNode"] = fromNodeID;
93 node["toNode"] = toNodeID;
94 node["AIMode"] = _AIMode;
95 return node;
96 }
97
98 /**
99 * Enters the current AI state.
100 */
enter()101 void CivilianBAIState::enter()
102 {
103
104 }
105
106 /**
107 * Exits the current AI state.
108 */
exit()109 void CivilianBAIState::exit()
110 {
111 if (_toNode) _toNode->freeNode();
112 }
113
114 /**
115 * Runs any code the state needs to keep updating every AI cycle.
116 * @param action (possible) AI action to execute after thinking is done.
117 */
think(BattleAction * action)118 void CivilianBAIState::think(BattleAction *action)
119 {
120 action->type = BA_RETHINK;
121 action->actor = _unit;
122 _escapeAction->number = action->number;
123 _visibleEnemies = selectNearestTarget();
124 _spottingEnemies = getSpottingUnits(_unit->getPosition());
125
126
127 if (_traceAI)
128 {
129 Log(LOG_INFO) << "Civilian Unit has " << _visibleEnemies << " enemies visible, " << _spottingEnemies << " of whom are spotting him. ";
130 std::string AIMode;
131 switch (_AIMode)
132 {
133 case 0:
134 AIMode = "Patrol";
135 break;
136 case 3:
137 AIMode = "Escape";
138 break;
139 }
140 Log(LOG_INFO) << "Currently using " << AIMode << " behaviour";
141 }
142
143 if (_spottingEnemies && !_escapeTUs)
144 {
145 setupEscape();
146 }
147
148 setupPatrol();
149
150 bool evaluate = false;
151 if (_AIMode == AI_ESCAPE)
152 {
153 if (!_spottingEnemies)
154 {
155 evaluate = true;
156 }
157 }
158 else if (_AIMode == AI_PATROL)
159 {
160 if (_spottingEnemies || _visibleEnemies || RNG::percent(10))
161 {
162 evaluate = true;
163 }
164 }
165 if (_spottingEnemies > 2
166 || _unit->getHealth() < 2 * _unit->getStats()->health / 3)
167 {
168 evaluate = true;
169 }
170
171 if (evaluate)
172 {
173 evaluateAIMode();
174 if (_traceAI)
175 {
176 std::string AIMode;
177 switch (_AIMode)
178 {
179 case 0:
180 AIMode = "Patrol";
181 break;
182 case 3:
183 AIMode = "Escape";
184 break;
185 }
186 Log(LOG_INFO) << "Re-Evaluated, now using " << AIMode << " behaviour";
187 }
188 }
189
190 switch (_AIMode)
191 {
192 case AI_ESCAPE:
193 action->type = _escapeAction->type;
194 action->target = _escapeAction->target;
195 action->number = 3;
196 _unit->dontReselect();
197 action->desperate = true;
198 _save->getBattleGame()->setTUReserved(BA_NONE, false);
199 break;
200 case AI_PATROL:
201 action->type = _patrolAction->type;
202 action->target = _patrolAction->target;
203 break;
204 default:
205 break;
206 }
207 if (action->type == BA_WALK && action->target != _unit->getPosition())
208 {
209 _escapeTUs = 0;
210 }
211 }
212
selectNearestTarget()213 int CivilianBAIState::selectNearestTarget()
214 {
215 int tally = 0;
216 int closest = 100;
217 _aggroTarget = 0;
218 Position origin = _save->getTileEngine()->getSightOriginVoxel(_unit);
219 origin.z -= 4;
220 Position target;
221 for (std::vector<BattleUnit*>::const_iterator i = _save->getUnits()->begin(); i != _save->getUnits()->end(); ++i)
222 {
223 if (!(*i)->isOut() && (*i)->getFaction() == FACTION_HOSTILE)
224 {
225 if (_save->getTileEngine()->visible(_unit, (*i)->getTile()))
226 {
227 tally++;
228 int dist = _save->getTileEngine()->distance(_unit->getPosition(), (*i)->getPosition());
229 if (dist < closest)
230 {
231 bool valid = _save->getTileEngine()->canTargetUnit(&origin, (*i)->getTile(), &target, _unit);
232 if (valid)
233 {
234 closest = dist;
235 _aggroTarget = *i;
236 }
237 }
238 }
239 }
240 }
241 if (_aggroTarget)
242 return tally;
243
244 return 0;
245 }
246
getSpottingUnits(Position pos) const247 int CivilianBAIState::getSpottingUnits(Position pos) const
248 {
249 bool checking = pos != _unit->getPosition();
250 int tally = 0;
251 for (std::vector<BattleUnit*>::const_iterator i = _save->getUnits()->begin(); i != _save->getUnits()->end(); ++i)
252 {
253 if (!(*i)->isOut() && (*i)->getFaction() == FACTION_HOSTILE)
254 {
255 int dist = _save->getTileEngine()->distance(pos, (*i)->getPosition());
256 if (dist > 20) continue;
257 Position originVoxel = _save->getTileEngine()->getSightOriginVoxel(*i);
258 originVoxel.z -= 4;
259 Position targetVoxel;
260 if (checking)
261 {
262 if (_save->getTileEngine()->canTargetUnit(&originVoxel, _save->getTile(pos), &targetVoxel, *i, _unit))
263 {
264 tally++;
265 }
266 }
267 else
268 {
269 if (_save->getTileEngine()->canTargetUnit(&originVoxel, _save->getTile(pos), &targetVoxel, *i))
270 {
271 tally++;
272 }
273 }
274 }
275 }
276 return tally;
277 }
278
setupEscape()279 void CivilianBAIState::setupEscape()
280 {
281 int unitsSpottingMe = getSpottingUnits(_unit->getPosition());
282 int currentTilePreference = 15;
283 int tries = 0;
284 bool coverFound = false;
285 selectNearestTarget();
286
287 int dist = _aggroTarget ? _save->getTileEngine()->distance(_unit->getPosition(), _aggroTarget->getPosition()) : 0;
288
289 int bestTileScore = -100000;
290 int score = -100000;
291 Position bestTile(0, 0, 0);
292
293 Tile *tile = 0;
294
295 // weights of various factors in choosing a tile to which to withdraw
296 const int EXPOSURE_PENALTY = 10;
297 const int FIRE_PENALTY = 40;
298 const int BASE_SYSTEMATIC_SUCCESS = 100;
299 const int BASE_DESPERATE_SUCCESS = 110;
300 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
301
302 int tu = _unit->getTimeUnits() / 2;
303
304 std::vector<int> reachable = _save->getPathfinding()->findReachable(_unit, tu);
305 std::vector<Position> randomTileSearch = _save->getTileSearch();
306 RNG::shuffle(randomTileSearch);
307
308 while (tries < 150 && !coverFound)
309 {
310 _escapeAction->target = _unit->getPosition(); // start looking in a direction away from the enemy
311
312 if (!_save->getTile(_escapeAction->target))
313 {
314 _escapeAction->target = _unit->getPosition(); // cornered at the edge of the map perhaps?
315 }
316
317 score = 0;
318
319 if (tries < 121)
320 {
321 // looking for cover
322 _escapeAction->target.x += randomTileSearch[tries].x;
323 _escapeAction->target.y += randomTileSearch[tries].y;
324 score = BASE_SYSTEMATIC_SUCCESS;
325 if (_escapeAction->target == _unit->getPosition())
326 {
327 if (unitsSpottingMe > 0)
328 {
329 // maybe don't stay in the same spot? move or something if there's any point to it?
330 _escapeAction->target.x += RNG::generate(-20,20);
331 _escapeAction->target.y += RNG::generate(-20,20);
332 } else
333 {
334 score += currentTilePreference;
335 }
336 }
337 }
338 else
339 {
340
341 if (tries == 121)
342 {
343 if (_traceAI)
344 {
345 Log(LOG_INFO) << "best score after systematic search was: " << bestTileScore;
346 }
347 }
348
349 score = BASE_DESPERATE_SUCCESS; // ruuuuuuun
350 _escapeAction->target = _unit->getPosition();
351 _escapeAction->target.x += RNG::generate(-10,10);
352 _escapeAction->target.y += RNG::generate(-10,10);
353 _escapeAction->target.z = _unit->getPosition().z + RNG::generate(-1,1);
354 if (_escapeAction->target.z < 0)
355 {
356 _escapeAction->target.z = 0;
357 }
358 else if (_escapeAction->target.z >= _save->getMapSizeZ())
359 {
360 _escapeAction->target.z = _unit->getPosition().z;
361 }
362 }
363
364 tries += 10; // civilians shouldn't have any tactical sense anyway so save some CPU cycles here
365
366 // THINK, DAMN YOU
367 tile = _save->getTile(_escapeAction->target);
368 int distanceFromTarget = _aggroTarget ? _save->getTileEngine()->distance(_aggroTarget->getPosition(), _escapeAction->target) : 0;
369 if (dist >= distanceFromTarget)
370 {
371 score -= (distanceFromTarget - dist) * 10;
372 }
373 else
374 {
375 score += (distanceFromTarget - dist) * 10;
376 }
377 int spotters = 0;
378 if (!tile)
379 {
380 score = -100001; // no you can't quit the battlefield by running off the map.
381 }
382 else
383 {
384 spotters = getSpottingUnits(_escapeAction->target);
385 if (std::find(reachable.begin(), reachable.end(), _save->getTileIndex(tile->getPosition())) == reachable.end()) continue; // just ignore unreachable tiles
386
387 if (_spottingEnemies || spotters)
388 {
389 if (_spottingEnemies <= spotters)
390 {
391 score -= (1 + spotters - _spottingEnemies) * EXPOSURE_PENALTY; // that's for giving away our position
392 }
393 else
394 {
395 score += (_spottingEnemies - spotters) * EXPOSURE_PENALTY;
396 }
397 }
398 if (tile->getFire())
399 {
400 score -= FIRE_PENALTY;
401 }
402 if (_traceAI)
403 {
404 tile->setMarkerColor(score < 0 ? 3 : (score < FAST_PASS_THRESHOLD/2 ? 8 : (score < FAST_PASS_THRESHOLD ? 9 : 5)));
405 tile->setPreview(10);
406 tile->setTUMarker(score);
407 }
408
409 }
410
411 if (tile && score > bestTileScore)
412 {
413 // calculate TUs to tile; we could be getting this from findReachable() somehow but that would break something for sure...
414 _save->getPathfinding()->calculate(_unit, _escapeAction->target, 0, tu);
415 if (_escapeAction->target == _unit->getPosition() || _save->getPathfinding()->getStartDirection() != -1)
416 {
417 bestTileScore = score;
418 bestTile = _escapeAction->target;
419 _escapeTUs = _save->getPathfinding()->getTotalTUCost();
420 if (_escapeAction->target == _unit->getPosition())
421 {
422 _escapeTUs = 1;
423 }
424 if (_traceAI)
425 {
426 tile->setMarkerColor(score < 0 ? 7 : (score < FAST_PASS_THRESHOLD/2 ? 10 : (score < FAST_PASS_THRESHOLD ? 4 : 5)));
427 tile->setPreview(10);
428 tile->setTUMarker(score);
429 }
430 }
431 _save->getPathfinding()->abortPath();
432 if (bestTileScore > FAST_PASS_THRESHOLD) coverFound = true; // good enough, gogogo
433 }
434 }
435 _escapeAction->target = bestTile;
436 if (_traceAI)
437 {
438 _save->getTile(_escapeAction->target)->setMarkerColor(13);
439 }
440
441 if (bestTileScore <= -100000)
442 {
443 if (_traceAI)
444 {
445 Log(LOG_INFO) << "Escape estimation failed.";
446 }
447 _escapeAction->type = BA_RETHINK; // do something, just don't look dumbstruck :P
448 return;
449 }
450 else
451 {
452 if (_traceAI)
453 {
454 Log(LOG_INFO) << "Escape estimation completed after " << tries << " tries, " << _save->getTileEngine()->distance(_unit->getPosition(), bestTile) << " squares or so away.";
455 }
456 _escapeAction->type = BA_WALK;
457 }
458 }
459
setupPatrol()460 void CivilianBAIState::setupPatrol()
461 {
462 Node *node;
463 if (_toNode != 0 && _unit->getPosition() == _toNode->getPosition())
464 {
465 if (_traceAI)
466 {
467 Log(LOG_INFO) << "Patrol destination reached!";
468 }
469 // destination reached
470 // head off to next patrol node
471 _fromNode = _toNode;
472 _toNode = 0;
473 }
474
475 if (_fromNode == 0)
476 {
477 // assume closest node as "from node"
478 // on same level to avoid strange things, and the node has to match unit size or it will freeze
479 int closest = 1000000;
480 for (std::vector<Node*>::iterator i = _save->getNodes()->begin(); i != _save->getNodes()->end(); ++i)
481 {
482 node = *i;
483 int d = _save->getTileEngine()->distanceSq(_unit->getPosition(), node->getPosition());
484 if (_unit->getPosition().z == node->getPosition().z
485 && d < closest
486 && node->getType() & Node::TYPE_SMALL)
487 {
488 _fromNode = node;
489 closest = d;
490 }
491 }
492 }
493
494 // look for a new node to walk towards
495
496 int triesLeft = 5;
497
498 while (_toNode == 0 && triesLeft)
499 {
500 triesLeft--;
501
502 _toNode = _save->getPatrolNode(true, _unit, _fromNode);
503 if (_toNode == 0)
504 {
505 _toNode = _save->getPatrolNode(false, _unit, _fromNode);
506 }
507
508 if (_toNode != 0)
509 {
510 _save->getPathfinding()->calculate(_unit, _toNode->getPosition());
511 if (_save->getPathfinding()->getStartDirection() == -1)
512 {
513 _toNode = 0;
514 }
515 _save->getPathfinding()->abortPath();
516 }
517 }
518
519 if (_toNode != 0)
520 {
521 _patrolAction->actor = _unit;
522 _patrolAction->type = BA_WALK;
523 _patrolAction->target = _toNode->getPosition();
524 }
525 else
526 {
527 _patrolAction->type = BA_RETHINK;
528 }
529 }
530
evaluateAIMode()531 void CivilianBAIState::evaluateAIMode()
532 {
533 int escapeOdds = _visibleEnemies ? 15 : 0;
534 int patrolOdds = _visibleEnemies ? 15 : 30;
535 if (_spottingEnemies)
536 {
537 patrolOdds = 0;
538 if (_escapeTUs == 0)
539 {
540 setupEscape();
541 }
542 }
543
544 switch (_AIMode)
545 {
546 case 0:
547 patrolOdds *= 1.1;
548 break;
549 case 3:
550 escapeOdds *= 1.1;
551 break;
552 }
553
554 if (_unit->getHealth() < _unit->getStats()->health / 3)
555 {
556 escapeOdds *= 1.7;
557 }
558 else if (_unit->getHealth() < 2 * (_unit->getStats()->health / 3))
559 {
560 escapeOdds *= 1.4;
561 }
562 else if (_unit->getHealth() < _unit->getStats()->health)
563 {
564 escapeOdds *= 1.1;
565 }
566
567 switch (_unit->getAggression())
568 {
569 case 0:
570 escapeOdds *= 1.4;
571 break;
572 case 2:
573 escapeOdds *= 0.7;
574 break;
575 }
576
577 if (_spottingEnemies)
578 {
579 escapeOdds = 10 * escapeOdds * (_spottingEnemies + 10) / 100;
580 }
581 else
582 {
583 escapeOdds /= 2;
584 }
585
586 int decision = 1 + RNG::generate(0, patrolOdds + escapeOdds);
587 if (decision > escapeOdds)
588 {
589 _AIMode = AI_PATROL;
590 }
591 else
592 {
593 _AIMode = AI_ESCAPE;
594 }
595
596 if (_AIMode == AI_PATROL)
597 {
598 if (_toNode)
599 {
600 return;
601 }
602 }
603
604 _AIMode = AI_ESCAPE;
605 }
606 }
607