1 
2 /* Battle Tanks Game
3  * Copyright (C) 2006-2009 Battle Tanks team
4  *
5  * This program is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU General Public License
7  * as published by the Free Software Foundation; either version 2
8  * of the License, or (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, write to the Free Software
17  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
18  */
19 
20 /*
21  * Additional rights can be granted beyond the GNU General Public License
22  * on the terms provided in the Exception. If you modify this file,
23  * you may extend this exception to your version of the file,
24  * but you are not obligated to do so. If you do not wish to provide this
25  * exception without modification, you must delete this exception statement
26  * from your version and license this file solely under the GPL without exception.
27 */
28 #include "buratino.h"
29 #include <assert.h>
30 #include "world.h"
31 #include "config.h"
32 
33 #include "math/unary.h"
34 #include "player_manager.h"
35 #include "game_monitor.h"
36 #include "mrt/random.h"
37 #include "math/binary.h"
38 #include "vehicle_traits.h"
39 #include "tmx/map.h"
40 #include "special_zone.h"
41 #include "team.h"
42 
43 using namespace ai;
44 
Buratino()45 Buratino::Buratino() : _reaction_time(true), _refresh_path(false), _target_id(-1), _target_dir(-1) {}
46 
active() const47 const bool Buratino::active() const {
48 	return !PlayerManager->is_client();
49 }
50 
~Buratino()51 Buratino::~Buratino() {
52 	if (!active())
53 		return;
54 
55 	if (!_traits.empty())
56 		LOG_DEBUG(("traits: \n%s", _traits.save().c_str()));
57 }
58 
addEnemyClass(const std::string & classname)59 void Buratino::addEnemyClass(const std::string &classname) {
60 	_enemies.insert(classname);
61 }
62 
addBonusName(const std::string & rname)63 void Buratino::addBonusName(const std::string &rname) {
64 	_bonuses.insert(rname);
65 }
66 
processPF(Object * object)67 void Buratino::processPF(Object *object) {
68 	if (object->calculating_path()) {
69 		Way way;
70 		int n = 1;
71 		bool found;
72 		while(! (found = object->find_path_done(way)) && n < _pf_slice)
73 			++n;
74 
75 		if (found) {
76 			//LOG_DEBUG(("n = %d", n));
77 			if (!way.empty()) {
78 				object->set_way(way);
79 				_skip_objects.clear();
80 			} else {
81 				LOG_DEBUG(("no path, adding %d to targets black list ", _target_id));
82 				_skip_objects.insert(_target_id);
83 			}
84 		}
85 	} else  {
86 		//LOG_DEBUG(("idle"));
87 
88 	}
89 }
90 
91 
on_spawn(const Object * object)92 void Buratino::on_spawn(const Object *object) {
93 	if (!active())
94 		return;
95 
96 	const std::string vehicle = object->getType();
97 	if (vehicle.empty())
98 		throw_ex(("vehicle MUST provide its type"));
99 
100 	LOG_DEBUG(("spawning as '%s'", vehicle.c_str()));
101 	if (_enemies.empty() && _bonuses.empty())
102 		throw_ex(("vehicle had not provided enemies/bonuses"));
103 
104 	float rt;
105 	Config->get("objects.ai-" + object->registered_name + ".reaction-time", rt, 0.1f);
106 	float rpi = 2.0f;
107 
108 	mrt::randomize(rt, rt/10);
109 	_reaction_time.set(rt);
110 
111 	mrt::randomize(rpi, rpi/10);
112 	_refresh_path.set(rpi);
113 
114 	Config->get("objects.ai-" + vehicle + ".pathfinding-slice", _pf_slice, 10);
115 }
116 
isEnemy(const Object * o) const117 const bool Buratino::isEnemy(const Object *o) const {
118 	return _enemies.find(o->classname) != _enemies.end();
119 }
120 
convertName(const std::string & weapon)121 const std::string Buratino::convertName(const std::string &weapon) {
122 	std::string wc, wt;
123 	std::string::size_type p;
124 	if ((p = weapon.rfind(':')) != std::string::npos) {
125 		wc = weapon.substr(0, p);
126 		wt = weapon.substr(p + 1);
127 	} else {
128 		wt = weapon;
129 	}
130 	if (wc.empty())
131 		return wt;
132 	return wt + "-" + wc.substr(0, wc.size() - 1);
133 }
134 
135 
checkTarget(const Object * object,const Object * target,const std::string & weapon) const136 const bool Buratino::checkTarget(const Object *object, const Object * target, const std::string &weapon) const {
137 	if (!isEnemy(target))
138 		return false;
139 
140 	if (object->registered_name == "shilka" || object->registered_name == "static-shilka") {
141 		return true;
142 	}
143 
144 	v2<float> pos = object->get_relative_position(target);
145 
146 	std::string wc, wt;
147 	{
148 		std::string::size_type p;
149 		if ((p = weapon.rfind(':')) != std::string::npos) {
150 			wc = weapon.substr(0, p);
151 			wt = weapon.substr(p + 1);
152 		} else {
153 			wc = weapon;
154 		}
155 	}
156 
157 	bool codir, codir1;
158 	{
159 		v2<float> d(pos);
160 		d.normalize();
161 		int dir = d.get_direction(object->get_directions_number()) - 1;
162 		codir = dir == object->get_direction();
163 		int dd = math::abs(dir - object->get_direction());
164 		codir1 = codir || dd == 1 || dd == (object->get_directions_number() - 1);
165 	}
166 
167 	//LOG_DEBUG(("checking target(%s/%s): %g %g codir: %c, codir1: %c", wc.c_str(), wt.c_str(), pos.x, pos.y, codir?'+':'-', codir1?'+':'-'));
168 
169 	if (wc == "missiles" || wc == "bullets" || wc == "bullet") {
170 		if (codir)
171 			return true;
172 		if ((wt == "guided" && codir1) || wt == "dispersion")
173 			return true;
174 		if (wt == "boomerang")
175 			return true;
176 	} else if (wc == "mines") {
177 		if (!object->_velocity.is0())
178 			return true;
179 	}
180 	return false;
181 }
182 
calculateCloseCombat(Object * object,const Object * target,const float range,const bool dumb)183 void Buratino::calculateCloseCombat(Object *object, const Object *target, const float range, const bool dumb) {
184 	assert(object != NULL);
185 	assert(target != NULL);
186 
187 	//LOG_DEBUG(("close combat with %s, range: %g, dumb: %c", target->animation.c_str(), range, dumb?'+':'-'));
188 
189 	if (!dumb) {
190 		_target_dir = object->get_target_position(_target_position, object->get_relative_position(target), range);
191 		if (_target_dir >= 0)
192 			Map->add(_target_position, object->get_center_position());
193 	}
194 
195 	object->_velocity = Map->distance(object->get_center_position(), _target_position);
196 
197 	//LOG_DEBUG(("object velocity: %g,%g, target dir: %d", object->_velocity.x, object->_velocity.y, _target_dir));
198 
199 	if (_target_dir >= 0) {
200 		int dirs = object->get_directions_number();
201 		if (object->_velocity.length() >= 9) {
202 			object->quantize_velocity();
203 			object->_direction.fromDirection(object->get_direction(), dirs);
204 		} else {
205 			object->_velocity.clear();
206 			object->set_direction(_target_dir);
207 			//LOG_DEBUG(("%d", _target_dir));
208 			object->_direction.fromDirection(_target_dir, dirs);
209 			std::string weapon1 = getWeapon(0), weapon2 = getWeapon(1);
210 			object->_state.fire = checkTarget(object, target, weapon1);
211 			object->_state.alt_fire = checkTarget(object, target, weapon2);
212 			//LOG_DEBUG(("firing: %c%c", object->_state.fire?'+':'-', object->_state.alt_fire?'+':'-'));
213 		}
214 	} else {
215 		object->_velocity.clear();
216 	}
217 }
218 
getWeaponRange(const Object * object) const219 const float Buratino::getWeaponRange(const Object *object) const {
220 	std::string weapon1 = getWeapon(0), weapon2 = getWeapon(1);
221 
222 	float range = 0;
223 	if (!weapon1.empty()) {
224 		range = math::max(range, object->getWeaponRange(convertName(weapon1)));
225 	}
226 	if (!weapon2.empty()) {
227 		range = math::max(range, object->getWeaponRange(convertName(weapon2)));
228 	}
229 	return range;
230 }
231 
calculate(Object * object,const float dt)232 void Buratino::calculate(Object *object, const float dt) {
233 	if (object->ai_disabled()) {
234 		return;
235 	}
236 
237 	if (!active()) {
238 		if (object->is_driven())
239 			object->calculate_way_velocity();
240 		else
241 			object->Object::calculate(dt);
242 		object->update_state_from_velocity();
243 		return;
244 	}
245 
246 	const bool racing = object->get_variants().has("racing");
247 	const bool refresh_path = !racing && _refresh_path.tick(dt) && object->is_driven();
248 	const bool dumb = !_reaction_time.tick(dt);
249 	const Object *target = NULL;
250 
251 	std::string weapon1, weapon2;
252 	int amount1, amount2;
253 
254 	if (dumb) {
255 		if (_target_dir >= 0) {
256 			if (target == NULL)
257 				target = World->getObjectByID(_target_id);
258 			if (target == NULL)
259 				goto gogogo;
260 
261 			//processPF(object);
262 			calculateCloseCombat(object, target, getWeaponRange(object), true);
263 			goto skip_calculations;
264 		}
265 		goto gogogo;
266 	}
267 
268 
269 	weapon1 = getWeapon(0), weapon2 = getWeapon(1);
270 	amount1 = getWeaponAmount(0), amount2 = getWeaponAmount(1);
271 
272 	if (target == NULL)
273 		target = World->getObjectByID(_target_id);
274 
275 	if (amount1 < 0)
276 		amount1 = 50;
277 	if (amount2 < 0)
278 		amount2 = 50; //infinite amount
279 
280 	if (target != NULL) {
281 		if (!weapon1.empty())
282 			object->_state.fire = checkTarget(object, target, weapon1);
283 		if (!weapon2.empty())
284 			object->_state.alt_fire = checkTarget(object, target, weapon2);
285 
286 		float range = getWeaponRange(object);
287 
288 		v2<float> dpos = object->get_relative_position(target);
289 		if (_enemy && dpos.length() <= range) {
290 			//processPF(object);
291 			calculateCloseCombat(object, target, range, false);
292 
293 			if (_target_dir >= 0) {
294 				if (object->is_driven())
295 					object->set_way(Way());
296 			}
297 		} else {
298 			_target_dir = -1;
299 		}
300 	}
301 
302 	if (racing) {
303 		Way way;
304 		if (object->calculating_path()) {
305 			goto gogogo;
306 		}
307 		if (!object->is_driven()) {
308 			int slot_id = PlayerManager->get_slot_id(object->get_id());
309 			if (slot_id <= 0)
310 				throw_ex(("ai in racing mode cannot operate without slot."));
311 
312 			PlayerSlot &slot = PlayerManager->get_slot(slot_id);
313 			const SpecialZone &zone = PlayerManager->get_next_checkpoint(slot);
314 			v3<int> position = zone.getPlayerPosition(slot_id);
315 
316 			object->find_path(v2<int>(position.x, position.y), 24);
317 		}
318 	}
319 
320 	target = findTarget(object,
321 		(amount1 > 0 || amount2 > 0)?_enemies:std::set<std::string>(),
322 		object->get_variants().has("no-bonuses")?std::set<std::string>():_bonuses,
323 		_traits, _skip_objects);
324 
325 	if (target != NULL) {
326 		if ( ((refresh_path && isEnemy(target)) || target->get_id() != _target_id)) {
327 			_target_id = target->get_id();
328 			_enemy = isEnemy(target);
329 			v2<int> target_position;
330 			target->get_center_position(target_position);
331 /*
332 			if (_enemy && !weapon1.empty()) {
333 				v2<float> r;
334 				if (object->get_target_position(r, target->get_position(), convertName(weapon1)))
335 					_target_position = r.convert<int>();
336 			}
337 */
338 			LOG_DEBUG(("%d: %s: next target: %s at %d,%d",
339 				object->get_id(), object->animation.c_str(), target->animation.c_str(), target_position.x, target_position.y));
340 			object->find_path(target_position, 24);
341 			_refresh_path.reset();
342 
343 			//Way way;
344 			//if (!old_find_path(target, way))
345 			//	LOG_WARN(("no way"));
346 			//else set_way(way);
347 		}
348 	} else if (!object->is_driven()) {
349 		//LOG_DEBUG(("%d:%s idle", object->_id, object->animation.c_str()));
350 		object->_velocity.clear();
351 	}
352 
353 	//2 fire or not 2 fire.
354 
355 
356 
357 	//LOG_DEBUG(("w1: %s", getWeapon(0).c_str()));
358 	//LOG_DEBUG(("w2: %s", getWeapon(1).c_str()));
359 
360 	//bool driven = is_driven();
361 
362 	//LOG_DEBUG(("calculating: %c, driven: %c", calculating?'+':'-', driven?'+':'-'));
363 	gogogo:
364 
365 	processPF(object);
366 
367 	object->calculate_way_velocity();
368 
369 skip_calculations:
370 	if (target != NULL) {
371 		if (!weapon1.empty() && !object->_state.fire)
372 			object->_state.fire = checkTarget(object, target, weapon1);
373 		if (!weapon2.empty() && !object->_state.alt_fire)
374 			object->_state.alt_fire = checkTarget(object, target, weapon2);
375 	}
376 	object->update_state_from_velocity();
377 }
378 
findTarget(const Object * src,const std::set<std::string> & enemies,const std::set<std::string> & bonuses,ai::Traits & traits,const std::set<int> & skip_objects) const379 const Object * Buratino::findTarget(const Object *src, const std::set<std::string> &enemies, const std::set<std::string> &bonuses, ai::Traits &traits, const std::set<int> &skip_objects) const {
380 	if (src->get_variants().has("racing"))
381 		return NULL;
382 
383 	if (src->has("#ctf-flag")) {
384 		Team::ID team = Team::get_team(src->get("#ctf-flag"));
385 		if (team != Team::Red && team != Team::Green)
386 			throw_ex(("flag team must be red or green"));
387 		int base_id = GameMonitor->getBase(team == Team::Red? Team::Green: Team::Red);
388 		Object *base = World->getObjectByID(base_id);
389 		if (base != NULL && !base->has_effect("abandoned")) {
390 			return base;
391 		}
392 	}
393 
394 	if (src->getType().empty())
395 		throw_ex(("findTarget source must always provide its type"));
396 
397 	const Object *result = NULL;
398 	float result_value = 0;
399 	std::set<const Object *> objects;
400 	{
401 		float range;
402 		Config->get("objects." + src->registered_name + ".range", range, 640.0f);
403 		World->enumerate_objects(objects, src, range, NULL);
404 	}
405 
406 	for(std::set<const Object *>::const_iterator i = objects.begin(); i != objects.end(); ++i) {
407 		const Object *o = *i;
408 		if (o->impassability == 0 || o->_id == src->_id || o->hp <= 0 ||
409 			!ZBox::sameBox(src->get_z(), o->get_z()) ||
410 			o->has_same_owner(src) ||
411 			o->has_effect("invulnerability") ||
412 			skip_objects.find(o->get_id()) != skip_objects.end()
413 			)
414 			continue;
415 
416 		const bool enemy = enemies.find(o->classname) != enemies.end();
417 		if (enemy && o->hp < 0)
418 			continue; //invulnerable enemy
419 
420 		const bool bonus = bonuses.find(o->registered_name) != bonuses.end() || bonuses.find(o->classname) != bonuses.end();
421 		//LOG_DEBUG(("object: %s", o->registered_name.c_str()));
422 		if (!enemy && !bonus)
423 			continue;
424 
425 		//bonus!
426 		int min = 0, max = 0;
427 		float multiplier = 1;
428 
429 		std::string mod_type = o->classname;
430 		if (mod_type == "teleport") {
431 			v2<float> dpos = src->get_relative_position(o);
432 			if (dpos.quick_length() < ((o->size.x + o->size.y) * 141 / 100))
433 				continue;
434 		}
435 		if (!o->getType().empty())
436 			mod_type += ":" + o->getType();
437 		if (o->has_effect("invulnerability"))
438 			continue;
439 
440 		if (o->classname == "missiles" || o->classname == "mines") {
441 			if (src->has("mod")) {
442 				const Object *mod = src->get("mod");
443 				if (mod->getType() == mod_type)
444 					min = mod->getCount();
445 			}
446 
447 			if (src->has("alt-mod")) {
448 				const Object *mod = src->get("alt-mod");
449 				if (mod->getType() == mod_type)
450 					min = mod->getCount();
451 			}
452 			assert(min >= 0);
453 			int max_v;
454 			VehicleTraits::getWeaponCapacity(max, max_v, src->getType(), o->classname, o->getType());
455 			if (min > max)
456 				min = max;
457 		} else if (o->classname == "heal") {
458 			min = src->hp;
459 			max = src->max_hp;
460 		} else if (o->classname == "effects" || o->classname == "mod") {
461 			max = 1;
462 		} else if (o->classname == "teleport") {
463 			max = 1;
464 		} else if (o->classname == "vehicle") {
465 			max = 1;
466 		} else if (o->classname == "ctf-flag") {
467 			PlayerSlot *slot = PlayerManager->get_slot_by_id(src->get_id());
468 			if (slot == NULL)
469 				continue;
470 			Team::ID flag_team = Team::get_team(o);
471 			if (flag_team == slot->team) {
472 				const Object *base = World->getObjectByID(o->get_summoner());
473 				if (base == NULL) {
474 					LOG_WARN(("could not find base #%d for %s", o->get_summoner(), o->animation.c_str()));
475 					continue;
476 				}
477 
478 				v2<float> dpos = o->get_relative_position(base);
479 				if (dpos.quick_length() < o->size.x * o->size.y / 4) {
480 					//my flag is on the base
481 					continue;
482 				}
483 				multiplier = traits.get("value", "ctf-flag-multiplier", 2, 4);
484 			}
485 			min = 0;
486 			max = 1;
487 		}
488 		float value = 0;
489 		const std::string type = o->getType();
490 
491 		if (enemy) {
492 			value = traits.get("enemy", type.empty()?o->classname:type, 1000.0, 1000.0) * multiplier;
493 		} else if (bonus) {
494 			if (max == 0) {
495 				LOG_WARN(("cannot determine bonus for %s", o->registered_name.c_str()));
496 				continue;
497 			}
498 			value = traits.get("value", mod_type, 1.0, 1000.0) * (max - min) * multiplier / max;
499 		} else assert(0);
500 
501 		if (enemy) {
502 			value *= (getFirePower(src, traits) + 1) / (getFirePower(o, traits) + 1);
503 			if (o->has("#ctf-flag"))
504 				value *= traits.get("value", "pwned-ctf-flag", 2, 4);
505 		}
506 		value /= (src->_position.distance(o->_position));
507 		//LOG_DEBUG(("item: %s, value: %g", o->registered_name.c_str(), value));
508 		//find most valuable item.
509 		if (value > result_value) {
510 			result = o;
511 			result_value = value;
512 		}
513 	}
514 	return result;
515 }
516 
getFirePower(const Object * o,ai::Traits & traits)517 const float Buratino::getFirePower(const Object *o, ai::Traits &traits) {
518 	float value = 0;
519 	if (o->has("mod")) {
520 		const Object *mod = o->get("mod");
521 		int c = mod->getCount();
522 		const std::string& type = mod->getType();
523 		if (c > 0 && !type.empty())
524 			value += traits.get("value", type, 1.0, 1000.0) * c;
525 	}
526 	if (o->has("alt-mod")) {
527 		const Object *mod = o->get("alt-mod");
528 		int c = mod->getCount();
529 		const std::string& type = mod->getType();
530 		if (c > 0 && !type.empty())
531 			value += traits.get("value", type, 1.0, 1000.0) * c;
532 	}
533 	return value;
534 }
535