1 /******************************************************************************
2 * Warmux is a convivial mass murder game.
3 * Copyright (C) 2001-2011 Warmux Team.
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (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 * A factory for AI strategies. It contains no turn specfic data.
20 *****************************************************************************/
21
22 #include "ai/ai_idea.h"
23 #include "ai/trajectory.h"
24 #include "character/character.h"
25 #include "game/game.h"
26 #include "game/game_mode.h"
27 #include "map/map.h"
28 #include "map/wind.h"
29 #include "object/objects_list.h"
30 #include "team/team.h"
31 #include "team/macro.h"
32 #include "tool/math_tools.h"
33 #include "weapon/explosion.h"
34 #include "weapon/weapon_launcher.h"
35 #include "weapon/weapons_list.h"
36 #include "weapon/shotgun.h"
37
38 // This constant defines how how much damage is worth killing one character?
39 // e.g. Killing one Character with 20 health is about the same worth like doing a sum of 120 damage (60 each) to two characters without killing them. Both cases would get a rating of 120 when this constant is 100.
40 const float BONUS_FOR_KILLING_CHARACTER = 100;
41 const float MALUS_PER_UNUSED_DAMGE_POINT = 0.1f;
42 const float MIN_GROUND_BONUS = 0.1f;
43 const float MAX_GROUND_BONUS = 1.0f;
44 const float GROUND_BONUS_RANGE = 2000.0f;
45 // At the time this code has been written the
46 // bazooka did about 30-60 additional damage at 2500 force
47 const float MIN_DAMAGE_PER_FORCE_UNIT = 30.0f/2500.0f;
48 const float MAX_DAMAGE_PER_FORCE_UNIT = 60.0f/2500.0f;
49
CanUseWeapon(const Weapon * weapon)50 bool AIIdea::CanUseWeapon(const Weapon * weapon)
51 {
52 bool correct_weapon = weapon == &(ActiveTeam().GetWeapon());
53 bool can_change_weapon = ActiveTeam().GetWeapon().CanChangeWeapon()
54 && (Game::GetInstance()->ReadState() == Game::PLAYING);
55 return correct_weapon || (can_change_weapon && weapon->EnoughAmmo());
56 }
57
CanUseCharacter(const Character & character)58 bool AIIdea::CanUseCharacter(const Character & character)
59 {
60 if (character.IsDead())
61 return false;
62
63 bool can_change_character = GameMode::GetInstance()->AllowCharacterSelection()
64 && (Game::GetInstance()->ReadState() == Game::PLAYING)
65 && !Game::GetInstance()->IsCharacterAlreadyChosen();
66 return (character.IsActiveCharacter() || can_change_character);
67 }
68
GetDirectionRelativeAngle(LRDirection direction,float angle)69 float AIIdea::GetDirectionRelativeAngle(LRDirection direction, float angle)
70 {
71 return (direction == DIRECTION_LEFT) ? InverseAngleRad(angle) : angle;
72 }
73
RateDamageDoneToEnemy(int damage,const Character & enemy)74 float AIIdea::RateDamageDoneToEnemy(int damage, const Character & enemy)
75 {
76 float rating = std::min(damage, enemy.GetEnergy());
77 if (damage >= enemy.GetEnergy()) {
78 rating += BONUS_FOR_KILLING_CHARACTER;
79 float unused_damage = damage - enemy.GetEnergy();
80 rating -= MALUS_PER_UNUSED_DAMGE_POINT * unused_damage;
81 }
82 return rating;
83 }
84
RateDamageDoneToEnemy(int min_damage,int max_damage,const Character & enemy)85 float AIIdea::RateDamageDoneToEnemy(int min_damage, int max_damage, const Character & enemy)
86 {
87 float min_rating = RateDamageDoneToEnemy(min_damage, enemy);
88 float max_rating = RateDamageDoneToEnemy(max_damage, enemy);
89 return (min_rating + max_rating)* 0.5f;
90 }
91
RateExplosion(const Character & shooter,const Point2i & position,const ExplosiveWeaponConfig & config,const float & expected_additional_distance)92 float AIIdea::RateExplosion(const Character & shooter, const Point2i& position,
93 const ExplosiveWeaponConfig& config,
94 const float& expected_additional_distance)
95 {
96 float rating = 0.0f;
97
98 FOR_ALL_LIVING_CHARACTERS(team, character) {
99 float distance = position.Distance(character->GetCenter());
100 distance += expected_additional_distance;
101 if (distance < 1.0f)
102 distance = 1.0f;
103 Double Dist = distance;
104 float min_damage = GetDamageFromExplosion(config, Dist);
105 float max_damage = min_damage;
106 if (Dist <= config.blast_range) {
107 float force = GetForceFromExplosion(config, Dist).tofloat();
108 min_damage += MIN_DAMAGE_PER_FORCE_UNIT * force;
109 max_damage += MAX_DAMAGE_PER_FORCE_UNIT * force;
110 }
111 bool is_friend = shooter.GetTeamIndex() == character->GetTeamIndex();
112 if (is_friend) {
113 rating -= RateDamageDoneToEnemy(min_damage, max_damage, *character);
114 } else {
115 rating += RateDamageDoneToEnemy(min_damage, max_damage, *character);
116 }
117 }
118 return rating;
119 }
120
CreateStrategy() const121 AIStrategy * SkipTurnIdea::CreateStrategy() const
122 {
123 const WeaponsList * weapons_list = Game::GetInstance()->GetWeaponsList();
124 const Weapon * weapon = weapons_list->GetWeapon(Weapon::WEAPON_SKIP_TURN);
125 if (!CanUseWeapon(weapon))
126 return NULL;
127 return new SkipTurnStrategy();
128 }
129
CreateStrategy() const130 AIStrategy * WasteAmmoUnitsIdea::CreateStrategy() const
131 {
132 if (ActiveTeam().GetWeapon().CanChangeWeapon())
133 return NULL;
134 Weapon::Weapon_type weapon_type = ActiveTeam().GetWeapon().GetType();
135 int used_ammo_units = ActiveTeam().ReadNbUnits(weapon_type);
136 float max_angle = -ActiveTeam().GetWeapon().GetMinAngle().tofloat();
137 return new ShootWithGunStrategy(-0.1f, ActiveCharacter(), weapon_type,
138 ActiveCharacter().GetDirection(), max_angle, used_ammo_units);
139 }
140
NoLongerPossible() const141 bool AIShootIdea::NoLongerPossible() const
142 {
143 return shooter.IsDead() || enemy.IsDead();
144 }
145
GetMaxRating(bool one_shot) const146 float AIShootIdea::GetMaxRating(bool one_shot) const
147 {
148 const WeaponsList *weapons_list = Game::GetInstance()->GetWeaponsList();
149 const WeaponLauncher *weapon = weapons_list->GetWeaponLauncher(weapon_type);
150 int damage = weapon->GetDamage();
151 int units = (one_shot) ? 1 : weapon->ReadInitialNbUnit();
152
153 if (weapon_type == Weapon::WEAPON_SHOTGUN)
154 damage *= SHOTGUN_BULLETS;
155
156 return weapons_weighting.GetFactor(weapon_type)*damage*units;
157 }
158
ShootDirectlyAtEnemyIdea(const WeaponsWeighting & weapons_weighting,const Character & shooter,const Character & enemy,Weapon::Weapon_type weapon_type,int max_distance)159 ShootDirectlyAtEnemyIdea::ShootDirectlyAtEnemyIdea(const WeaponsWeighting & weapons_weighting,
160 const Character & shooter, const Character & enemy,
161 Weapon::Weapon_type weapon_type,
162 int max_distance)
163 : AIShootIdea(weapons_weighting, shooter, enemy, weapon_type)
164 , max_sq_distance(max_distance*max_distance)
165 {
166 // do nothing
167 }
168
GetObjectAt(const Point2i & pos)169 static const PhysicalObj* GetObjectAt(const Point2i & pos)
170 {
171 const ObjectsList * objects = ObjectsList::GetConstInstance();
172 ObjectsList::const_iterator it = objects->begin();
173 while(it != objects->end()) {
174 const PhysicalObj* object = *it;
175 if (object->GetTestRect().Contains(pos) && !object->IsDead())
176 return object;
177 it++;
178 }
179
180 FOR_ALL_CHARACTERS(team, character) {
181 if (character->GetTestRect().Contains(pos) && !character->IsDead())
182 return &(*character);
183 }
184
185 return NULL;
186 }
187
ObjectLiesOnSegment(const PhysicalObj * object,const Point2i & from,const Point2i & to)188 static bool ObjectLiesOnSegment(const PhysicalObj* object,
189 const Point2i& from, const Point2i& to)
190 {
191 const Rectanglei& r = object->GetTestRect();
192 const Point2i& center = object->GetCenter();
193
194 if (from.y == to.y) {
195 // Horizontal shot
196 return r.Contains(Point2i(center.x, to.y));
197 } else if (from.x == to.x) {
198 // Vertical shot
199 return r.Contains(Point2i(to.x, center.y));
200 }
201
202 // Are the ordinates within the segment
203 if ((center.x<from.x && center.x<to.x) || (center.x>from.x && center.x>to.x) ||
204 (center.y<from.y && center.y<to.y) || (center.y>from.y && center.y>to.y))
205 return false;
206
207 // Find point on corresponding line
208 int y = from.y + ((center.x-from.x)*(to.y-from.y))/(to.x-from.x);
209 return r.Contains(Point2i(center.x, y));
210 }
211
212
213 /* Returns the object the missile has collided with or NULL if the missile has collided with the ground. */
ShotMisses(const Character * shooter,const Character * enemy,const Point2i & from,const Point2i & to)214 static bool ShotMisses(const Character *shooter, const Character *enemy,
215 const Point2i& from, const Point2i& to) {
216 Point2i pos = from;
217 Point2i delta = to - from;
218
219 const ObjectsList * objects = ObjectsList::GetConstInstance();
220 ObjectsList::const_iterator it = objects->begin();
221 while(it != objects->end()) {
222 const PhysicalObj* object = *it;
223 if (!object->IsDead() && ObjectLiesOnSegment(object, from, to))
224 return true;
225 it++;
226 }
227
228 FOR_ALL_CHARACTERS(team, character) {
229 const PhysicalObj* object = &(*character);
230 if (object!=shooter && object!=enemy && !object->IsDead() &&
231 ObjectLiesOnSegment(object, from, to))
232 return true;
233 }
234
235 int steps_x = abs(delta.x);
236 int steps_y = abs(delta.y);
237 int step_x = delta.x < 0 ? -1 : 1;
238 int step_y = delta.y < 0 ? -1 : 1;
239 int done_x_mul_steps_y = 0;
240 int done_y_mul_steps_x = 0;
241 // explanation of done_x_mul_steps_y:
242 // done_x = how often has step_x been added to pos.x
243 // done_x_mul_steps_y = done_x * steps_y
244 // example of algorithm:
245 // given: departure = (0,0) arrival = (-2,7);
246 // => steps_x = 2; steps_y = 7; step_x = -1; step_y = 1
247 // progress:
248 // pos.x | pos.y | done_x_mul_steps_y | done_y_mul_steps_x
249 // -------------------------------------------------------
250 // 0 | 0 | 0 | 0
251 // 0 | 1 | 0 | 2
252 // -1 | 2 | 7 | 4
253 // -1 | 3 | 7 | 6
254 // -1 | 4 | 7 | 8
255 // -1 | 5 | 7 | 10
256 // -2 | 6 | 14 | 12
257 // -2 | 7 | 14 | 14
258 // The algorithm tries to keep the difference small between:
259 // done_x_mul_steps_y and done_y_mul_steps_x.
260 // By doing so it gets assured that all intermediate positions pos form a straight line.
261 // (Or at least something close to a straight line)
262 while (pos != to) {
263 int new_done_x_mul_steps_y = done_x_mul_steps_y + steps_y;
264 int new_done_y_mul_steps_x = done_y_mul_steps_x + steps_x;
265 int diff_after_step_x = abs(done_y_mul_steps_x - new_done_x_mul_steps_y);
266 int diff_after_step_y = abs(new_done_y_mul_steps_x - done_x_mul_steps_y);
267
268 if (diff_after_step_x <= diff_after_step_y) {
269 pos.x += step_x;
270 done_x_mul_steps_y = new_done_x_mul_steps_y;
271 }
272 if (diff_after_step_y <= diff_after_step_x) {
273 pos.y += step_y;
274 done_y_mul_steps_x = new_done_y_mul_steps_x;
275 }
276
277 if (GetWorld().IsOutsideWorld(pos))
278 return true;
279
280 if (!GetWorld().IsInVacuum(pos))
281 return true;
282 }
283 return false;
284 }
285
CreateStrategy() const286 AIStrategy * ShootDirectlyAtEnemyIdea::CreateStrategy() const {
287 if (enemy.IsDead())
288 return NULL;
289
290 if (!CanUseCharacter(shooter))
291 return NULL;
292
293 const WeaponsList *weapons_list = Game::GetInstance()->GetWeaponsList();
294 const WeaponLauncher *weapon = weapons_list->GetWeaponLauncher(weapon_type);
295
296 if (!CanUseWeapon(weapon))
297 return NULL;
298
299 // We need to use center point, because gunholePosition is location
300 // of last weapon of the ActiveTeam() and not the future gunholePos
301 // which will be select.
302 // TODO: Please find an alternative to solve this tempory solution
303 Point2i departure = shooter.GetCenter();
304 Point2i arrival = enemy.GetCenter();
305
306 if (departure.SquareDistance(arrival) > max_sq_distance)
307 return NULL;
308
309 float original_angle = departure.ComputeAngleFloat(arrival);
310
311 LRDirection direction = XDeltaToDirection(arrival.x - departure.x);
312 float shoot_angle = GetDirectionRelativeAngle(direction, original_angle);
313
314 if (!weapon->IsAngleValid(shoot_angle))
315 return NULL;
316
317 if (ShotMisses(&shooter, &enemy, departure, arrival))
318 return NULL;
319
320 int available_ammo_units = ActiveTeam().ReadNbUnits(weapon_type);
321
322 int damage_per_ammo_unit = weapon->GetDamage();
323 if (weapon_type == Weapon::WEAPON_SHOTGUN) {
324 damage_per_ammo_unit *= SHOTGUN_BULLETS;
325 }
326 int required_ammo_units = (enemy.GetEnergy() + damage_per_ammo_unit -1)
327 / damage_per_ammo_unit;
328 int used_ammo_units = std::min(required_ammo_units, available_ammo_units);
329 int damage = used_ammo_units * damage_per_ammo_unit;
330
331 float rating = RateDamageDoneToEnemy(damage, enemy);
332 rating = rating * weapons_weighting.GetFactor(weapon_type);
333 return new ShootWithGunStrategy(rating, shooter, weapon_type, direction,
334 shoot_angle, used_ammo_units);
335 }
336
FireMissileWithFixedDurationIdea(const WeaponsWeighting & weapons_weighting,const Character & shooter,const Character & enemy,Weapon::Weapon_type weapon_type,float duration,int timeout)337 FireMissileWithFixedDurationIdea::FireMissileWithFixedDurationIdea(const WeaponsWeighting & weapons_weighting,
338 const Character & shooter, const Character & enemy,
339 Weapon::Weapon_type weapon_type,
340 float duration, int timeout)
341 : AIShootIdea(weapons_weighting, shooter, enemy, weapon_type)
342 , duration(duration)
343 , timeout(timeout)
344 {
345 // do nothing
346 }
347
IsPositionEmpty(const Character & character_to_ignore,const Point2i & pos,const PhysicalObj ** object)348 static bool IsPositionEmpty(const Character & character_to_ignore,
349 const Point2i& pos, const PhysicalObj** object)
350 {
351 *object = NULL;
352 if (GetWorld().IsOutsideWorld(pos))
353 return false;
354
355 if (!GetWorld().IsInVacuum(pos))
356 return false;
357
358 *object = GetObjectAt(pos);
359 if (*object != NULL && *object != &character_to_ignore)
360 return false;
361 *object = NULL;
362 return true;
363 }
364
365 #define STEP_IN_PIXEL 2
366
GetFirstContact(const Character & character_to_ignore,const Trajectory & trajectory,const PhysicalObj ** object)367 static const Point2i GetFirstContact(const Character & character_to_ignore,
368 const Trajectory & trajectory,
369 const PhysicalObj** object)
370 {
371 float time = 0;
372 Point2i pos;
373 do {
374 pos = trajectory.GetPositionAt(time);
375 float pixel_per_second = trajectory.GetSpeedAt(time);
376 time += STEP_IN_PIXEL / pixel_per_second;
377 } while (IsPositionEmpty(character_to_ignore, pos, object));
378 return pos;
379 }
380
CreateStrategy() const381 AIStrategy * FireMissileWithFixedDurationIdea::CreateStrategy() const
382 {
383 if (enemy.IsDead())
384 return NULL;
385
386 if (!CanUseCharacter(shooter))
387 return NULL;
388
389 const WeaponsList * weapons_list = Game::GetInstance()->GetWeaponsList();
390 const WeaponLauncher * weapon = weapons_list->GetWeaponLauncher(weapon_type);
391
392 if (!CanUseWeapon(weapon))
393 return NULL;
394 float g = GameMode::GetInstance()->gravity.tofloat();
395 float wind_factor = weapon->GetWindFactor().tofloat();
396 float mass = weapon->GetMass().tofloat();
397 Point2f f(Wind::GetRef().GetStrength().tofloat() * wind_factor, g * mass);
398 Point2f a = f / mass * PIXEL_PER_METER;
399 const Point2f pos_0 = shooter.GetCenter();
400 const Point2f pos_t = enemy.GetCenter();
401 float t = duration;
402 // Calculate v_0 using "pos_t = 1/2 * a_x * t*t + v_0*t + pos_0":
403 Point2f v_0 = (pos_t - pos_0)/t - a/2 * t;
404
405 float strength = v_0.Norm() / PIXEL_PER_METER;
406 float angle = v_0.ComputeAngleFloat();
407 LRDirection direction = XDeltaToDirection(v_0.x<0);
408 float shoot_angle = GetDirectionRelativeAngle(direction, angle);
409 if (!weapon->IsAngleValid(shoot_angle))
410 return NULL;
411
412 if (strength > weapon->GetMaxStrength().tofloat())
413 return NULL;
414
415 Trajectory trajectory(pos_0, v_0, a);
416 const PhysicalObj * aim;
417 Point2i explosion_pos = GetFirstContact(shooter, trajectory, &aim);
418 float rating;
419 bool explodes_on_contact = weapon_type == Weapon::WEAPON_BAZOOKA;
420 if (aim == &enemy || explodes_on_contact) {
421 float expected_additional_distance = explodes_on_contact ? 0.0f : 30.0f;
422 rating = RateExplosion(shooter, explosion_pos, weapon->cfg(), expected_additional_distance);
423
424 // Explosions remove ground and make it possible to hit the characters behind the ground.
425 // That is why ground hits get rewared with a small positive rating.
426 if (explodes_on_contact) {
427 float distance = explosion_pos.Distance(enemy.GetCenter());
428 // Give more bonus if the explosion is near the target.
429 // This will make the AI focus on one character
430 float ground_bonus = max(MIN_GROUND_BONUS, MAX_GROUND_BONUS - distance/GROUND_BONUS_RANGE);
431 rating += ground_bonus;
432 }
433 } else {
434 return NULL;
435 }
436 rating = rating * weapons_weighting.GetFactor(weapon_type);
437 return new LoadAndFireStrategy(rating, shooter, weapon_type, direction, shoot_angle, strength, timeout);
438 }
439