1void() movetarget_f;
2void() t_movetarget;
3void() knight_walk1;
4void() knight_bow6;
5void() knight_bow1;
6void() FoundTarget;
7
8float MONSTER_WANDER = 32; // disable wandering around
9float MONSTER_APPEAR = 64; // spawn invisible, and appear when triggered
10
11.float ismonster;
12.float monsterawaitingteleport; // avoid awaking monsters in teleport rooms
13
14// when a monster becomes angry at a player, that monster will be used
15// as the sight target the next frame so that monsters near that one
16// will wake up even if they wouldn't have noticed the player
17//
18entity sight_entity;
19float sight_entity_time;
20
21/*
22
23.enemy
24Will be world if not currently angry at anyone.
25
26.movetarget
27The next path spot to walk toward.  If .enemy, ignore .movetarget.
28When an enemy is killed, the monster will try to return to it's path.
29
30.huntt_ime
31Set to time + something when the player is in sight, but movement straight for
32him is blocked.  This causes the monster to use wall following code for
33movement direction instead of sighting on the player.
34
35.ideal_yaw
36A yaw angle of the intended direction, which will be turned towards at up
37to 45 deg / state.  If the enemy is in view and hunt_time is not active,
38this will be the exact line towards the enemy.
39
40.pausetime
41A monster will leave it's stand state and head towards it's .movetarget when
42time > .pausetime.
43
44walkmove(angle, speed) primitive is all or nothing
45*/
46
47
48//
49// globals
50//
51//float current_yaw;
52
53float(float v) anglemod =
54{
55	v = v - 360 * floor(v / 360);
56	return v;
57};
58
59/*
60==============================================================================
61
62MOVETARGET CODE
63
64The angle of the movetarget effects standing and bowing direction, but has no effect on movement, which allways heads to the next target.
65
66targetname
67must be present.  The name of this movetarget.
68
69target
70the next spot to move to.  If not present, stop here for good.
71
72pausetime
73The number of seconds to spend standing or bowing for path_stand or path_bow
74
75==============================================================================
76*/
77
78
79void() movetarget_f =
80{
81	if (!self.targetname)
82		objerror ("monster_movetarget: no targetname");
83
84	self.solid = SOLID_TRIGGER;
85	self.touch = t_movetarget;
86	setsize (self, '-8 -8 -8', '8 8 8');
87};
88
89/*QUAKED path_corner (0.5 0.3 0) (-8 -8 -8) (8 8 8)
90Monsters will continue walking towards the next target corner.
91*/
92void() path_corner =
93{
94	movetarget_f ();
95};
96
97/*
98=============
99t_movetarget
100
101Something has bumped into a movetarget.  If it is a monster
102moving towards it, change the next destination and continue.
103==============
104*/
105void() t_movetarget =
106{
107	local entity temp;
108
109	if (other.health < 1)
110		return;
111	if (other.movetarget != self)
112		return;
113
114	if (other.enemy)
115		return;		// fighting, not following a path
116
117	temp = self;
118	self = other;
119	other = temp;
120
121	if (self.classname == "monster_ogre")
122		sound (self, CHAN_VOICE, "ogre/ogdrag.wav", 1, ATTN_IDLE);// play chainsaw drag sound
123
124//dprint ("t_movetarget\n");
125	self.goalentity = self.movetarget = find (world, targetname, other.target);
126	self.ideal_yaw = vectoyaw(self.goalentity.origin - self.origin);
127	if (!self.movetarget)
128	{
129		self.pausetime = time + 999999;
130		self.th_stand ();
131		return;
132	}
133};
134
135void() monster_wanderpaththink =
136{
137	local vector v, v1;
138	local float b, c;
139	self.nextthink = time + random() * 10 + 1;
140	if (self.owner.health < 1) // dead, also handled in death code
141	{
142		self.owner.movetarget = world;
143		remove(self);
144		return;
145	}
146	b = -1;
147	c = 10;
148	while (c > 0)
149	{
150		c = c - 1;
151		v = randomvec();
152		traceline(self.owner.origin, v * 1024 + self.owner.origin, FALSE, self);
153		v = trace_endpos - (normalize(v) * 16) - self.owner.origin;
154		if (vlen(v) > b)
155		{
156			b = vlen(v);
157			v1 = v;
158		}
159	}
160	setorigin(self, v1 + self.owner.origin);
161	self.owner.ideal_yaw = vectoyaw(self.origin - self.owner.origin);
162};
163
164void() monster_wanderpathtouch =
165{
166	if (other.health < 1)
167		return;
168	if (other.movetarget != self)
169		return;
170
171	if (other.enemy)
172		return;		// fighting, not following a path
173
174	if (other.classname == "monster_ogre")
175		sound (other, CHAN_VOICE, "ogre/ogdrag.wav", 1, ATTN_IDLE);// play chainsaw drag sound
176	monster_wanderpaththink();
177};
178
179void() monster_spawnwanderpath =
180{
181	newmis = spawn();
182	newmis.classname = "monster_wanderpath";
183	newmis.solid = SOLID_TRIGGER;
184	newmis.touch = monster_wanderpathtouch;
185	setsize (newmis, '-8 -8 -8', '8 8 8');
186	newmis.think = monster_wanderpaththink;
187	newmis.nextthink = time + random() * 10 + 1;
188	newmis.owner = self;
189	self.goalentity = self.movetarget = newmis;
190};
191
192void() monster_checkbossflag =
193{
194	local float healthboost;
195	local float r;
196	// monsterbosses cvar or spawnflag 64 causes a monster to be a miniboss
197	if ((self.spawnflags & 64) || (random() * 100 < cvar("monsterbosspercent")))
198	{
199		self.radsuit_finished = time + 1000000000;
200		r = random() * 4;
201		if (r < 2)
202		{
203			self.super_damage_finished = time + 1000000000;
204			healthboost = 30 + self.health * 0.5;
205			self.effects = self.effects | (EF_FULLBRIGHT | EF_BLUE);
206		}
207		if (r >= 1)
208		{
209			healthboost = 30 + self.health * bound(0.5, skill * 0.5, 1.5);
210			self.effects = self.effects | (EF_FULLBRIGHT | EF_RED);
211			self.healthregen = max(self.healthregen, min(skill * 10, 30));
212		}
213		self.health = self.health + healthboost;
214		self.max_health = self.health;
215		self.bodyhealth = self.bodyhealth * 2 + healthboost;
216		do
217		{
218			self.colormod_x = random();
219			self.colormod_y = random();
220			self.colormod_z = random();
221			self.colormod =  normalize(self.colormod);
222		}
223		while (self.colormod_x > 0.6 && self.colormod_y > 0.6 && self.colormod_z > 0.6);
224	}
225};
226
227
228//============================================================================
229
230/*
231=============
232range
233
234returns the range catagorization of an entity reletive to self
2350	melee range, will become hostile even if back is turned
2361	visibility and infront, or visibility and show hostile
2372	infront and show hostile
2383	only triggered by damage
239=============
240*/
241float(entity targ) range =
242{
243	local float r;
244	r = vlen ((self.origin + self.view_ofs) - (targ.origin + targ.view_ofs));
245	if (r < 120)
246		return RANGE_MELEE;
247	if (r < 500)
248		return RANGE_NEAR;
249	if (r < 2000) // increased from 1000 for DP
250		return RANGE_MID;
251	return RANGE_FAR;
252};
253
254/*
255=============
256visible
257
258returns 1 if the entity is visible to self, even if not infront ()
259=============
260*/
261float (entity targ) visible =
262{
263	if (vlen(targ.origin - self.origin) > 5000) // long traces are slow
264		return FALSE;
265
266	traceline ((self.origin + self.view_ofs), (targ.origin + targ.view_ofs), TRUE, self);   // see through other monsters
267
268	if (trace_inopen && trace_inwater)
269		return FALSE;			// sight line crossed contents
270
271	if (trace_fraction == 1)
272		return TRUE;
273	return FALSE;
274};
275
276
277/*
278=============
279infront
280
281returns 1 if the entity is in front (in sight) of self
282=============
283*/
284float(entity targ) infront =
285{
286	local float dot;
287
288	makevectors (self.angles);
289	dot = normalize (targ.origin - self.origin) * v_forward;
290
291	return (dot > 0.3);
292};
293// returns 0 if not infront, or the dotproduct if infront
294float(vector dir, entity targ) infront2 =
295{
296	local float dot;
297
298	dir = normalize(dir);
299	dot = normalize (targ.origin - self.origin) * dir;
300
301	if (dot >= 0.3) return dot; // infront
302	return 0;
303};
304
305
306//============================================================================
307
308/*
309===========
310ChangeYaw
311
312Turns towards self.ideal_yaw at self.yaw_speed
313Sets the global variable current_yaw
314Called every 0.1 sec by monsters
315============
316*/
317/*
318
319void() ChangeYaw =
320{
321	local float ideal, move;
322
323//current_yaw = self.ideal_yaw;
324// mod down the current angle
325	current_yaw = anglemod( self.angles_y );
326	ideal = self.ideal_yaw;
327
328	if (current_yaw == ideal)
329		return;
330
331	move = ideal - current_yaw;
332	if (ideal > current_yaw)
333	{
334		if (move > 180)
335			move = move - 360;
336	}
337	else
338	{
339		if (move < -180)
340			move = move + 360;
341	}
342
343	if (move > 0)
344	{
345		if (move > self.yaw_speed)
346			move = self.yaw_speed;
347	}
348	else
349	{
350		if (move < 0-self.yaw_speed )
351			move = 0-self.yaw_speed;
352	}
353
354	current_yaw = anglemod (current_yaw + move);
355
356	self.angles_y = current_yaw;
357};
358
359*/
360
361
362//============================================================================
363
364void() HuntTarget =
365{
366	self.goalentity = self.enemy;
367	self.think = self.th_run;
368	self.ideal_yaw = vectoyaw(self.enemy.origin - self.origin);
369	self.nextthink = time + 0.1;
370	SUB_AttackFinished (1);	// wait a while before first attack
371};
372
373.void() th_sightsound;
374
375void() SightSound =
376{
377	if (self.health < 1)
378		return;
379	// skill 5 does not play sight sounds, instead you only hear the appear sound as they are about to attack
380	if (skill >= 5)
381	if (self.classname != "monster_hellfish")
382		return;
383
384	if (self.th_sightsound)
385		self.th_sightsound();
386};
387
388void() FoundTarget =
389{
390	if (self.health < 1 || !self.th_run)
391		return;
392	if (self.enemy.health < 1 || !self.enemy.takedamage)
393		return;
394	if (self.enemy.classname == "player")
395	{
396		// let other monsters see this monster for a while
397		sight_entity = self;
398		sight_entity_time = time + 0.1;
399	}
400
401	self.show_hostile = time + 1;		// wake up other monsters
402
403	SightSound ();
404	HuntTarget ();
405};
406
407/*
408//float checkplayertime;
409entity lastcheckplayer;
410entity havocbot_list;
411
412
413entity() checkplayer =
414{
415	local entity check;
416	local float worldcount;
417	// we can just fallback on checkclient if there are no bots
418	if (!havocbot_list)
419		return checkclient();
420*/
421	/*
422	if (time < checkplayertime)
423	{
424		traceline(self.origin + self.view_ofs, lastcheckplayer.origin + lastcheckplayer.view_ofs, TRUE, self);
425		if (trace_fraction == 1)
426			return lastcheckplayer;
427		if (trace_ent == lastcheckplayer)
428			return lastcheckplayer;
429	}
430	checkplayertime = time + 0.1;
431	*/
432/*
433	check = lastcheckplayer;
434	worldcount = 0;
435	c = 0;
436	do
437	{
438		c = c + 1;
439		check = findfloat(check, havocattack, TRUE);
440		if (check.classname == "player" || check.classname == "turretbase")
441		{
442			traceline(self.origin + self.view_ofs, check.origin + check.view_ofs, TRUE, self);
443			if (trace_fraction == 1)
444				return lastcheckplayer = check;
445			if (trace_ent == check)
446				return lastcheckplayer = check;
447		}
448		else if (check == world)
449		{
450			worldcount = worldcount + 1;
451			if (worldcount >= 2)
452				return lastcheckplayer = check;
453		}
454	}
455	while(check != lastcheckplayer && c < 100);
456	return world;
457};
458*/
459
460/*
461===========
462FindTarget
463
464Self is currently not attacking anything, so try to find a target
465
466Returns TRUE if an enemy was sighted
467
468When a player fires a missile, the point of impact becomes a fakeplayer so
469that monsters that see the impact will respond as if they had seen the
470player.
471
472To avoid spending too much time, only a single client (or fakeclient) is
473checked each frame.  This means multi player games will have slightly
474slower noticing monsters.
475============
476*/
477.float findtarget;
478float() FindTarget =
479{
480	local entity client;
481	local float r;
482
483	if (self.health < 1)
484		return FALSE;
485
486	// if the first or second spawnflag bit is set, the monster will only
487	// wake up on really seeing the player, not another monster getting angry
488
489	if (self.spawnflags & 3)
490	{
491		// don't wake up on seeing another monster getting angry
492		client = checkclient ();
493		if (!client)
494			return FALSE;   // current check entity isn't in PVS
495	}
496	else
497	{
498		if (sight_entity_time >= time)
499		{
500			client = sight_entity;
501			if (client.enemy == self.enemy)
502				return TRUE;
503		}
504		else
505		{
506			client = checkclient ();
507			if (!client)
508				return FALSE;   // current check entity isn't in PVS
509		}
510	}
511
512	if (client == self.enemy)
513		return FALSE;
514
515	if (client.flags & FL_NOTARGET)
516		return FALSE;
517
518	if (client.items & IT_INVISIBILITY)
519		return FALSE;
520
521	// on skill 5 the monsters usually ignore the player and remain ghostlike
522	if (skill >= 5)
523	if (self.classname != "monster_hellfish")
524	if (random() < 0.99)
525		return FALSE;
526
527	r = range(client);
528	if (r == RANGE_FAR)
529		return FALSE;
530
531	if (!visible (client))
532		return FALSE;
533
534	if (r == RANGE_NEAR)
535	{
536		if (client.show_hostile < time && !infront (client))
537			return FALSE;
538	}
539	else if (r == RANGE_MID)
540	{
541		// LordHavoc: was if ( /* client.show_hostile < time || */ !infront (client))
542		if (client.show_hostile < time && !infront (client))
543			return FALSE;
544	}
545
546	//
547	// got one
548	//
549
550	if (client.model == "")
551		return FALSE;
552	self.enemy = client;
553	if (self.enemy.classname != "player" && self.enemy.classname != "turretbase")
554	{
555		self.enemy = self.enemy.enemy;
556		if (self.enemy.classname != "player" && self.enemy.classname != "turretbase")
557		{
558			self.enemy = world;
559			return FALSE;
560		}
561	}
562
563	FoundTarget ();
564
565	return TRUE;
566};
567
568
569//=============================================================================
570
571void(float dist) ai_forward =
572{
573	walkmove (self.angles_y, dist);
574};
575
576void(float dist) ai_back =
577{
578	walkmove ( (self.angles_y+180), dist);
579};
580
581
582void(float a) monster_setalpha;
583
584/*
585=============
586ai_pain
587
588stagger back a bit
589=============
590*/
591void(float dist) ai_pain =
592{
593	if (self.health < 1)
594		return;
595	ai_back (dist);
596};
597
598/*
599=============
600ai_painforward
601
602stagger back a bit
603=============
604*/
605void(float dist) ai_painforward =
606{
607	if (self.health < 1)
608		return;
609	walkmove (self.ideal_yaw, dist);
610};
611
612/*
613=============
614ai_walk
615
616The monster is walking it's beat
617=============
618*/
619void(float dist) ai_walk =
620{
621	if (self.health < 1)
622		return;
623
624	movedist = dist;
625
626	// check for noticing a player
627	if (self.oldenemy.takedamage)
628	if (self.oldenemy.health >= 1)
629	{
630		self.enemy = self.oldenemy;
631		self.oldenemy = world;
632		FoundTarget();
633		monster_setalpha(0);
634		return;
635	}
636	if (self.enemy)
637	{
638		if (self.enemy.takedamage)
639		{
640			if (self.enemy.health >= 1)
641			{
642				FoundTarget();
643				monster_setalpha(0);
644				return;
645			}
646			else
647				self.enemy = world;
648		}
649		else
650			self.enemy = world;
651	}
652
653	self.findtarget = TRUE;
654
655	movetogoal (dist);
656	monster_setalpha(0);
657};
658
659
660/*
661=============
662ai_stand
663
664The monster is staying in one place for a while, with slight angle turns
665=============
666*/
667void() ai_stand =
668{
669	if (self.health < 1)
670		return;
671	if (self.enemy)
672	{
673		if (self.enemy.takedamage)
674		{
675			if (self.enemy.health >= 1)
676			{
677				FoundTarget();
678				monster_setalpha(0);
679				return;
680			}
681			else
682				self.enemy = world;
683		}
684		else
685			self.enemy = world;
686	}
687	self.findtarget = TRUE;
688
689	if (time > self.pausetime)
690	{
691		self.th_walk ();
692		monster_setalpha(0);
693		return;
694	}
695
696// change angle slightly
697
698	monster_setalpha(0);
699};
700
701/*
702=============
703ai_turn
704
705don't move, but turn towards ideal_yaw
706=============
707*/
708void() ai_turn =
709{
710	if (self.enemy)
711	{
712		if (self.enemy.takedamage)
713		{
714			if (self.enemy.health >= 1)
715			{
716				FoundTarget();
717				monster_setalpha(0);
718				return;
719			}
720			else
721				self.enemy = world;
722		}
723		else
724			self.enemy = world;
725	}
726	self.findtarget = TRUE;
727
728	ChangeYaw ();
729	monster_setalpha(0);
730};
731
732//=============================================================================
733
734/*
735=============
736ChooseTurn
737=============
738*/
739void(vector destvec) ChooseTurn =
740{
741	local vector dir, newdir;
742
743	dir = self.origin - destvec;
744
745	newdir_x = trace_plane_normal_y;
746	newdir_y = 0 - trace_plane_normal_x;
747	newdir_z = 0;
748
749	if (dir * newdir > 0)
750	{
751		dir_x = 0 - trace_plane_normal_y;
752		dir_y = trace_plane_normal_x;
753	}
754	else
755	{
756		dir_x = trace_plane_normal_y;
757		dir_y = 0 - trace_plane_normal_x;
758	}
759
760	dir_z = 0;
761	self.ideal_yaw = vectoyaw(dir);
762};
763
764/*
765============
766FacingIdeal
767
768============
769*/
770float() FacingIdeal =
771{
772	local float delta;
773
774	delta = anglemod(self.angles_y - self.ideal_yaw);
775	if (delta > 45 && delta < 315)
776		return FALSE;
777	return TRUE;
778};
779
780
781//=============================================================================
782
783.float() th_checkattack;
784
785
786
787/*
788=============
789ai_run
790
791The monster has an enemy it is trying to kill
792=============
793*/
794void(float dist) ai_run =
795{
796	local float ofs;
797	if (self.health < 1)
798		return;
799	movedist = dist;
800	// see if the enemy is dead
801	if (self.enemy.health < 1 || self.enemy.takedamage == DAMAGE_NO)
802	{
803		self.enemy = world;
804		// FIXME: look all around for other targets
805		if (self.oldenemy.health >= 1 && self.oldenemy.takedamage)
806		{
807			self.enemy = self.oldenemy;
808			self.oldenemy = world;
809			HuntTarget ();
810		}
811		else
812		{
813			if (self.movetarget)
814				self.th_walk ();
815			else
816				self.th_stand ();
817			return;
818		}
819	}
820
821	// wake up other monsters
822	self.show_hostile = time + 1;
823
824	// check knowledge of enemy
825	enemy_range = range(self.enemy);
826
827	self.ideal_yaw = vectoyaw(self.enemy.origin - self.origin);
828	ChangeYaw ();
829
830	if (self.attack_state == AS_MELEE)
831	{
832		//dprint ("ai_run_melee\n");
833		//Turn and close until within an angle to launch a melee attack
834		if (FacingIdeal())
835		{
836			self.th_melee ();
837			self.attack_state = AS_STRAIGHT;
838		}
839		return;
840	}
841	else if (self.attack_state == AS_MISSILE)
842	{
843		//dprint ("ai_run_missile\n");
844		//Turn in place until within an angle to launch a missile attack
845		if (FacingIdeal())
846		if (self.th_missile ())
847			self.attack_state = AS_STRAIGHT;
848		return;
849	}
850
851	if (self.th_checkattack())
852		return;					// beginning an attack
853
854	if (visible(self.enemy))
855		self.search_time = time + 5;
856	else if (coop)
857	{
858		// look for other coop players
859		if (self.search_time < time)
860			self.findtarget = TRUE;
861	}
862
863	if (self.attack_state == AS_SLIDING)
864	{
865		//dprint ("ai_run_slide\n");
866		//Strafe sideways, but stay at aproximately the same range
867		if (self.lefty)
868			ofs = 90;
869		else
870			ofs = -90;
871
872		if (walkmove (self.ideal_yaw + ofs, movedist))
873			return;
874
875		self.lefty = !self.lefty;
876
877		walkmove (self.ideal_yaw - ofs, movedist);
878	}
879
880	// head straight in
881	movetogoal (dist);		// done in C code...
882};
883
884