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