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