1-- GunFu Deadlands 2-- Copyright 2009-2011 Christiaan Janssen, September 2009-October 2011 3-- 4-- This file is part of GunFu Deadlands. 5-- 6-- GunFu Deadlands 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-- GunFu Deadlands 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 GunFu Deadlands. If not, see <http://www.gnu.org/licenses/>. 18 19 20EnemyAI = {} 21 22-- ========================== UPDATE FUNCTIONS ================= 23 24function EnemyAI.update( dt ) 25 for i,enemy in ipairs(Level.enemies) do 26 EnemyAI.update_state( dt, enemy ) 27 EnemyAI.update_move( dt, enemy ) 28 EnemyAI.update_timers( dt, enemy ) 29 end 30end 31 32function EnemyAI.update_state( dt, enemy ) 33 local former_state = enemy.state 34 -- switch statement implemented as chain of ifs 35 if enemy.state == 1 then 36 EnemyAI.state_wandering( dt, enemy ) 37 elseif enemy.state == 2 then 38 EnemyAI.state_alert( dt, enemy ) 39 elseif enemy.state == 3 then 40 EnemyAI.state_engaging( dt, enemy ) 41 end 42 -- state 0 is dead (ignore) 43 enemy.former_state = former_state 44end 45 46function EnemyAI.update_move( dt, enemy ) 47--~ Movement.compute_predictions( enemy ) 48 Movement.update_character( dt, enemy ) 49end 50 51function EnemyAI.update_timers( dt, enemy ) 52 53 -- shooting 54 if enemy.shooting_timer > 0 then 55 enemy.shooting_timer = enemy.shooting_timer - dt 56 end 57 58 -- walking straight 59 if enemy.changedir_timer > 0 then 60 enemy.changedir_timer = enemy.changedir_timer - dt 61 end 62 63 -- blocked while walking 64 if enemy.blocked_timer > 0 then 65 enemy.blocked_timer = enemy.blocked_timer - dt 66 end 67 68 -- wandering 69 if enemy.wandering_timer > 0 then 70 enemy.wandering_timer = enemy.wandering_timer - dt 71 end 72 73 -- dodging 74 if enemy.dodging_timer > 0 then 75 enemy.dodging_timer = enemy.dodging_timer - dt 76 end 77 78 -- dying 79 if enemy.state == 0 and enemy.death_timer > 0 then 80 enemy.death_timer = enemy.death_timer - dt 81 end 82 83 -- suspicion 84 if enemy.suspicion_timer > 0 then 85 enemy.suspicion_timer = enemy.suspicion_timer - dt 86 end 87 88 -- scare 89 if enemy.scared_timer > 0 then 90 enemy.scared_timer = enemy.scared_timer - dt 91 end 92 93 local accel = 1 94 if BulletTime.active then 95 accel = accel / BulletTime.slowdown_enemies 96 end 97 98 -- sight 99 if enemy.see_player_timer > 0 then 100 enemy.see_player_timer = enemy.see_player_timer - dt*accel 101 end 102 if enemy.ninja_see_player_timer > 0 then 103 enemy.ninja_see_player_timer = enemy.ninja_see_player_timer - dt*accel 104 end 105 if enemy.see_bullet_timer > 0 then 106 enemy.see_bullet_timer = enemy.see_bullet_timer - dt*accel 107 end 108 109 110 -- prediction 111 if enemy.prediction_timer > 0 then 112 enemy.prediction_timer = enemy.prediction_timer - dt*accel 113 end 114 115 116 -- aiming 117 if enemy.target_dir[1]==0 and enemy.target_dir[2]==0 then 118 enemy.target_dir={1,0} 119 end 120 if enemy.shoot_dir[1]==0 and enemy.shoot_dir[2]==0 then 121 enemy.shoot_dir = {enemy.target_dir[1],enemy.target_dir[2]} 122 else 123 if Level.autoturns then 124 enemy.shoot_dir = {enemy.target_dir[1],enemy.target_dir[2]} 125 else 126 local turn = mymath.get_angle( enemy.shoot_dir, enemy.target_dir ) 127 if math.abs(turn) > enemy.aiming_angular_velocity * dt then 128 enemy.shoot_dir = mymath.rotate( enemy.shoot_dir, enemy.aiming_angular_velocity * dt * mymath.sign(turn) ) 129 else 130 enemy.shoot_dir = {enemy.target_dir[1],enemy.target_dir[2]} 131 end 132 end 133 end 134end 135 136-- ========================== END UPDATE FUNCTIONS ================= 137 138-- ========================== STATE MACHINE ================= 139 140 141function EnemyAI.state_wandering( dt, enemy ) 142 -- maybe change your mind 143 if enemy.wandering_timer <= 0 then 144 EnemyAI.choose_newpoint( enemy ) 145 enemy.wandering_timer = -math.log(1-math.random())*enemy.wandering_delay 146 end 147 148 local block_distance = 15 149 150 151 -- steer 152 if not EnemyAI.has_arrived( enemy ) then 153 -- blocked? poingg! 154 if math.abs(math.floor(enemy.pos[1]) - enemy.lastpos[1])<block_distance*dt and 155 math.abs(math.floor(enemy.pos[2]) - enemy.lastpos[2]) < block_distance*dt then 156 if enemy.blocked_timer <= 0 then 157 enemy.dir = {-enemy.dir[1], -enemy.dir[2]} 158 EnemyAI.choose_newpoint( enemy ) 159 end 160 else 161 enemy.blocked_timer = enemy.blocked_delay 162 end 163 164 165 -- first make sure that the direction vector is normalized 166 local dirmodsq = (enemy.dir[1]*enemy.dir[1] + enemy.dir[2]*enemy.dir[2]) 167 local destvector = mymath.get_dir_vector(enemy.pos[1], enemy.pos[2], enemy.destination[1], enemy.destination[2]) 168 if dirmodsq == 0 then 169 enemy.dir = {destvector[1], destvector[2]} 170 elseif dirmodsq ~= 1 then 171 local dirmod = math.sqrt(dirmodsq) 172 enemy.dir = {enemy.dir[1]/dirmod, enemy.dir[2]/dirmod} 173 end 174 175 -- check the angle 176 local angle = mymath.get_angle(enemy.dir, destvector) 177 if angle <= enemy.angular_velocity*dt then 178 enemy.dir = destvector 179 else 180 local steer_matrix = mymath.get_rotation_matrix( enemy.angular_velocity*dt ) 181 local newdir = { enemy.dir[1] * steer_matrix[1] + enemy.dir[2] * steer_matrix[2] , 182 enemy.dir[1] * steer_matrix[3] + enemy.dir[2] * steer_matrix[1] } 183 enemy.dir = { newdir[1], newdir[2] } 184 end 185 186 187 else -- arrived: wait 188 -- point to the open space 189 190 EnemyAI.aim_openspace(enemy) 191 192 if enemy.suspicion_timer>0 then 193 EnemyAI.choose_onemorestep( enemy ) 194 else 195 enemy.dir = {0,0} 196 end 197 198 end 199 200 201 -- if wandering then point in the direction you are walking 202 if enemy.dir[1]~=0 or enemy.dir[2]~=0 then 203 enemy.target_dir = {enemy.dir[1], enemy.dir[2]} 204 end 205 206 -- unless you are suspicious, then be prepared 207 if enemy.suspicion_timer > 0 then 208 EnemyAI.aim_lastseen_player(enemy) 209 end 210 211 enemy.lastpos = {math.floor(enemy.pos[1]), math.floor(enemy.pos[2]) } 212 213 -- state change: alarm 214 if EnemyAI.see_bullet( enemy ) then 215 enemy.state = 2 216 enemy.destination = { enemy.last_seen_bullet[1], enemy.last_seen_bullet[2] } 217 elseif EnemyAI.see_player( enemy ) then 218 enemy.state = 3 219 end 220 221end 222 223 224function EnemyAI.state_alert( dt, enemy ) 225 if enemy.former_state ~= enemy.state then 226 -- when entering this state... if the distance is long, approach it 227 -- if the distance was short, escape 228 if mymath.get_distanceSq( enemy.pos, enemy.destination ) < location_range*location_range*4 then 229 EnemyAI.choose_escape( enemy, enemy.destination ) 230 enemy.scared_timer = -math.log(1-math.random())*enemy.scared_delay 231 end 232 end 233 234 EnemyAI.walk_straight( dt, enemy ) 235 236 enemy.suspicion_timer = enemy.suspicion_delay 237 238 -- aim 239 enemy.target_dir = { enemy.dir[1], enemy.dir[2] } 240 241 if EnemyAI.see_player( enemy ) then 242 enemy.state = 3 243 -- if we get to the destination and still nothing has happened, then go idle 244 elseif EnemyAI.has_arrived( enemy ) then 245 if enemy.scared_timer <= 0 then 246 enemy.state = 1 247 enemy.dir = {0,0} 248 else 249 EnemyAI.choose_onemorestep( enemy ) 250 end 251 end 252end 253 254function EnemyAI.state_engaging( dt, enemy ) 255 -- first entry 256 if enemy.former_state ~= enemy.state then 257 -- freeze 258 enemy.dir = {0,0} 259 enemy.destination = { enemy.pos[1], enemy.pos[2] } 260 261 -- just to know where is the player now 262 enemy.lastplayerpos = { Player.pos[1], Player.pos[2] } 263 264 -- dodge if player infront of me 265 local player_sight = mymath.get_angle( enemy.target_dir, 266 mymath.get_dir_vector( enemy.pos[1], enemy.pos[2], Player.pos[1], Player.pos[2] ))/ math.pi 267 enemy.dodging_timer = math.abs( enemy.initial_reaction_time * player_sight * player_sight ) 268 end 269 270 enemy.suspicion_timer = enemy.suspicion_delay 271 272 -- move around (don't be a static target) TIMEIT 273 EnemyAI.walk_straight( dt, enemy ) 274 275 if enemy.dodging_timer <= 0 then 276 enemy.dodging_timer = -math.log(1-math.random())*enemy.dodging_delay 277 EnemyAI.choose_cover( enemy ) 278 enemy.dir = mymath.get_dir_vector( enemy.pos[1], enemy.pos[2], enemy.destination[1], enemy.destination[2] ) 279 end 280 281 if EnemyAI.has_arrived( enemy ) then 282 EnemyAI.choose_onemorestep( enemy ) 283 enemy.dir = mymath.get_dir_vector( enemy.pos[1], enemy.pos[2], enemy.destination[1], enemy.destination[2] ) 284 end 285 286 -- shoot the player 287 if EnemyAI.see_player( enemy ) then 288 enemy.lastplayerpos = { Player.pos[1], Player.pos[2] } 289 EnemyAI.aim_player( enemy ) 290 EnemyAI.keep_shooting( dt, enemy ) 291 else 292 -- if he disapears, pursue him 293 enemy.state = 2 -- alert works exactly as pursue, only that the destination is the last player pos 294 enemy.destination = { enemy.lastplayerpos[1], enemy.lastplayerpos[2] } 295 end 296end 297 298 299-- ========================== END STATE MACHINE ================= 300 301-- ========================== HELPER FUNCTIONS ================= 302 303function EnemyAI.walk_straight( dt, enemy ) 304 -- first entry 305 if enemy.former_state ~= enemy.state then 306 enemy.changedir_timer = 0 307 end 308 309 -- just straight go 310 if enemy.changedir_timer <= 0 then 311 enemy.dir = mymath.get_dir_vector( enemy.pos[1], enemy.pos[2], enemy.destination[1], enemy.destination[2] ) 312 enemy.changedir_timer = enemy.changedir_delay 313 end 314 315 -- if blocked for a while, start wandering 316 if math.floor(enemy.pos[1]) == enemy.lastpos[1] and math.floor(enemy.pos[2]) == enemy.lastpos[2] then 317 enemy.blocked = enemy.blocked_timer < enemy.blocked_delay / 2 318 if enemy.blocked_timer <= 0 then 319 enemy.state = 1 320 enemy.blocked = false 321 end 322 else 323 enemy.blocked = false 324 enemy.blocked_timer = enemy.blocked_delay 325 end 326 327 enemy.lastpos = {math.floor(enemy.pos[1]), math.floor(enemy.pos[2]) } 328end 329 330-- ================= CHOOSING DESTINATIONS 331 332-- checks if the enemy can get there walking a straight line 333function EnemyAI.check_walkable( enemy, newpoint ) 334 -- is it in the screen? 335 local hl,hu = enemy.spritesize[1]/2, enemy.spritesize[2]/2 336 337 if newpoint[1]-hl < 0 or newpoint[1]+hl > screensize[1] or 338 newpoint[2]-hu < 0 or newpoint[2]+hu >screensize[2] 339 then 340 return false 341 end 342 343 -- can i get there? 344 if enemy.free_box and mymath.check_pointinbox(newpoint, enemy.free_box) then 345 return true 346 end 347 348 local l = enemy.collision_buildings 349 while l do 350 if mymath.check_segmentinbuilding({enemy.pos[1], enemy.pos[2], newpoint[1], newpoint[2]}, l.value) then 351 enemy.collision_buildings = List.pushToFront(enemy.collision_buildings, l) 352 return false 353 end 354 l = l.next 355 end 356 357 return true 358end 359 360 361function EnemyAI.choose_newpoint( enemy ) 362 local distance 363 local mean_distance = 775 364 365 for i=1,16 do 366 distance = -math.log(1-math.random())*mean_distance 367 local angle = math.random(16)/16 * 2 * math.pi 368 local newpoint = { enemy.pos[1]+distance * math.cos(angle) , enemy.pos[2]+ distance * math.sin(angle) } 369 370 if EnemyAI.check_walkable(enemy, newpoint) then 371 enemy.destination = { newpoint[1], newpoint[2] } 372 return 373 end 374 end 375 376end 377 378function EnemyAI.choose_cover( enemy ) 379 local distance = 180 380 local order=mymath.permutation(4) 381 local myangle={60,120,240,300} 382 local player_dir = mymath.get_dir_vector( Player.pos[1], Player.pos[2], enemy.pos[1], enemy.pos[2] ) 383 local candidate_dest = {} 384 385 -- try progressively shorter distanc3es 386 for j=1,4 do 387 -- generate the points 388 for i=1,4 do 389 local angle = myangle[order[i]]*math.pi/180.0 390 local candidate_dir = mymath.rotate(player_dir,angle) 391 candidate_dest[i] = {enemy.pos[1]+candidate_dir[1]*distance, enemy.pos[2]+candidate_dir[2]*distance} 392 end 393 -- see if they cover 394 for i=1,4 do 395 if not EnemyAI.see_something( enemy, candidate_dest[i], Player) and 396 EnemyAI.check_walkable( enemy, candidate_dest[i]) 397 then 398 enemy.destination = { candidate_dest[i][1], candidate_dest[i][2] } 399 return 400 end 401 end 402 -- maybe they don't cover, but I can get there anyway 403 for i=1,4 do 404 if EnemyAI.check_walkable( enemy, candidate_dest[i]) then 405 enemy.destination = { candidate_dest[i][1], candidate_dest[i][2] } 406 return 407 end 408 end 409 -- ok, so let's try half of that distance 410 distance = distance/2 411 end 412end 413 414function EnemyAI.choose_nearlocation( enemy ) 415 local order = mymath.permutation(8) 416 417 for i=1,8 do 418 local angle=(order[i]*45 + 22.5)*math.pi/180.0 419 local distance = 75 420 local candidate_dir = {math.cos(angle),math.sin(angle)} 421 local candidate_dest = {enemy.pos[1]+candidate_dir[1]*distance, enemy.pos[2]+candidate_dir[2]*distance} 422 if EnemyAI.check_walkable( enemy, candidate_dest) 423 then 424 enemy.destination = { candidate_dest[1], candidate_dest[2] } 425 return 426 end 427 end 428end 429 430function EnemyAI.choose_onemorestep( enemy ) 431 local order = mymath.permutation(4) 432 433 if enemy.dir[1]==0 and enemy.dir[2]==0 then 434 return 435 end 436 437 for i=1,4 do 438 local angle=(order[i]*45 - 112.5)*math.pi/180.0 439 local distance = 85 440 local candidate_dir = mymath.rotate(enemy.dir,angle) 441 local candidate_dest = {enemy.pos[1]+candidate_dir[1]*distance , enemy.pos[2]+candidate_dir[2]*distance } 442 if EnemyAI.check_walkable( enemy, candidate_dest) 443 then 444 enemy.destination = { candidate_dest[1], candidate_dest[2] } 445 return 446 end 447 end 448end 449 450function EnemyAI.choose_escape( enemy, from ) 451 enemy.dir = mymath.get_dir_vector( from[1], from[2], enemy.pos[1], enemy.pos[2] ) 452 EnemyAI.choose_onemorestep( enemy ) 453end 454 455-- =============== MOVEMENT 456function EnemyAI.has_arrived( enemy ) 457 if mymath.get_distanceSq( enemy.pos, enemy.destination ) < location_range*location_range then 458 return true 459 end 460 return false 461end 462 463-- =============== PERCEPTION 464 465function EnemyAI.see_player( enemy ) 466 if not Player.alive and Player.death_timer <= 0 then 467 return false 468 end 469 470 if enemy.see_player_timer <= 0 then 471 enemy.player_was_seen = EnemyAI.see_something( enemy, Player.pos ) 472 enemy.see_player_timer = enemy.see_delay*math.random() 473 end 474 return enemy.player_was_seen 475 476end 477 478function EnemyAI.see_something( enemy, where ) 479 -- too far 480 if mymath.get_distanceSq( where, enemy.pos ) > enemy.sight_dist*enemy.sight_dist then 481 return false 482 end 483 484 if List.count(enemy.sight_buildings) == 0 then 485 return true 486 end 487 488 if enemy.free_box and mymath.check_pointinbox(where, enemy.free_box) then 489 return true 490 end 491 492 -- direct line of sight? 493 local l = enemy.sight_buildings 494 while l do 495 if mymath.check_segmentinbuilding({where[1], where[2], enemy.pos[1], enemy.pos[2]}, l.value) then 496 enemy.sight_buildings = List.pushToFront(enemy.sight_buildings, l) 497 return false 498 end 499 l = l.next 500 end 501 502 return true 503end 504 505function EnemyAI.see_bullet( enemy ) 506 507 if enemy.see_bullet_timer <= 0 then 508 enemy.bullet_was_seen = EnemyAI.compute_see_bullet( enemy ) 509 enemy.see_bullet_timer = enemy.bullet_see_delay*math.random() 510 end 511 return enemy.bullet_was_seen 512 513end 514 515function EnemyAI.compute_see_bullet( enemy ) 516 local l = Bullets 517 while l do 518 if l.value then 519 if EnemyAI.see_something( enemy, l.value.pos ) then 520 enemy.last_seen_bullet = { l.value.pos[1], l.value.pos[2] } 521 return true 522 end 523 end 524 l = l.next 525 end 526 return false 527end 528 529function EnemyAI.ninja_see_player_short( enemy ) 530 if enemy.ninja_see_player_timer <= 0 then 531 enemy.ninja_player_was_seen = EnemyAI.compute_ninja_see_player_short( enemy ) 532 enemy.ninja_see_player_timer = enemy.see_delay*math.random() 533 end 534 return enemy.ninja_player_was_seen 535end 536 537function EnemyAI.compute_ninja_see_player_short( enemy ) 538 if EnemyAI.see_player( enemy ) then 539 return true 540 end 541 542 local prediction_player = Player.get_prediction() 543 local prediction_enemy = EnemyAI.get_prediction( enemy ) 544 545 -- future 546 for i,building in ipairs(Level.buildings) do 547 if building.solid==1 and 548 mymath.check_segmentinbuilding( 549 {prediction_player[1], prediction_player[2], prediction_enemy[1], prediction_enemy[2]}, building) 550 551 then 552 return false 553 end 554 end 555 556 return true 557end 558 559function EnemyAI.aim_openspace(enemy) 560 -- first check if the current direction is open 561 local distance = 70 562 local view_pos = { enemy.pos[1]+enemy.target_dir[1]*distance, enemy.pos[2]+enemy.target_dir[2]*distance } 563 564 if EnemyAI.see_something( enemy, view_pos ) 565 then 566 -- it's ok 567 return 568 end 569 570 -- choose one from the 8 possible directions 571 local check_order = mymath.permutation(8) 572 for i=1,8 do 573 view_pos = { enemy.pos[1]+math.cos(math.pi/4*check_order[i])*distance, 574 enemy.pos[2]+math.sin(math.pi/4*check_order[i])*distance } 575 if EnemyAI.see_something( enemy, view_pos ) 576 then 577 enemy.target_dir = { math.cos(math.pi/4*i), math.sin(math.pi/4*i) } 578 return 579 end 580 end 581 582 -- still here? well, this means I am trapped and there is nothing to see... 583 return 584end 585 586function EnemyAI.aim_player( enemy ) 587 local direction = mymath.get_dir_vector( enemy.pos[1], enemy.pos[2], Player.pos[1], Player.pos[2] ) 588 enemy.target_dir = {direction[1],direction[2]} 589end 590 591function EnemyAI.aim_lastseen_player( enemy ) 592 local direction = mymath.get_dir_vector( enemy.pos[1], enemy.pos[2], enemy.lastplayerpos[1], enemy.lastplayerpos[2] ) 593 enemy.target_dir = {direction[1],direction[2]} 594end 595 596function EnemyAI.get_prediction( enemy ) 597 if enemy.prediction_timer <= 0 then 598 enemy.prediction_short = Movement.get_prediction( enemy, enemy.prediction_short_dt ) 599 enemy.prediction_timer = enemy.prediction_delay*math.random() 600 end 601 return enemy.prediction_short 602end 603 604-- ================== SHOOT 605function EnemyAI.keep_shooting( dt, enemy ) 606 if enemy.shooting_timer <= 0 then 607 if math.abs(mymath.get_angle(enemy.shoot_dir, enemy.target_dir)) > enemy.acceptable_arc then 608 return 609 end 610 611 enemy.shoot_dir = mymath.disturb_vector(enemy.shoot_dir, enemy.accuracy) 612 613 -- mode 1: 1 single bullet ( single gun ) 614 if enemy.firingmode == 1 then 615 616 Bullets = Movement.newbullet_enemy( enemy.pos, enemy.shoot_dir) 617 618 -- mode 2: 2 bullets ( dual gun, alternating ) 619 elseif enemy.firingmode == 2 then 620 local bpos = {enemy.pos[1] + enemy.spritesize[1]/2, enemy.pos[2]} 621 if enemy.alternatefire then 622 bpos[1] = enemy.pos[1] - enemy.spritesize[1]/2 623 end 624 enemy.alternatefire = not enemy.alternatefire 625 626 Bullets = Movement.newbullet_enemy( bpos, enemy.shoot_dir) 627 628 -- mode 3: 3 bullets ( shotgun ) 629 elseif enemy.firingmode == 3 then 630 local destup = mymath.rotate(enemy.shoot_dir, enemy.shotgun_arc*2*math.pi/360) 631 local destdn = mymath.rotate(enemy.shoot_dir, -enemy.shotgun_arc*2*math.pi/360) 632 633 Bullets = Movement.newbullet_enemy( enemy.pos, enemy.shoot_dir) 634 Bullets = Movement.newbullet_enemy( enemy.pos, destup) 635 Bullets = Movement.newbullet_enemy( enemy.pos, destdn) 636 end 637 638 enemy.shooting_timer = enemy.shooting_delay 639 Sounds.play_shot_enemy( enemy ) 640 end 641end 642 643-- ========================== END HELPER FUNCTIONS ================= 644 645function EnemyAI.load_buildingLists() 646 for i,e in ipairs(Level.enemies) do 647 e.collision_buildings = List.fromArray( Level.buildings ) 648 e.collision_buildings = List.applydel( e.collision_buildings, function(b) return b.solid<=3 end) 649 e.sight_buildings = List.fromArray( Level.buildings ) 650 e.sight_buildings = List.applydel( e.sight_buildings, function(b) return b.solid == 1 end) 651 end 652end 653