1 // WL_ACT2.C
2 
3 #include <stdio.h>
4 #include <climits>
5 #include <math.h>
6 #include "actor.h"
7 #include "m_random.h"
8 #include "wl_act.h"
9 #include "wl_def.h"
10 #include "wl_menu.h"
11 #include "id_ca.h"
12 #include "id_sd.h"
13 #include "id_vl.h"
14 #include "id_vh.h"
15 #include "id_us.h"
16 #include "language.h"
17 #include "thingdef/thingdef.h"
18 #include "thingdef/thingdef_expression.h"
19 #include "wl_agent.h"
20 #include "wl_draw.h"
21 #include "wl_game.h"
22 #include "wl_state.h"
23 
24 static const angle_t dirangle[9] = {0,ANGLE_45,2*ANGLE_45,3*ANGLE_45,4*ANGLE_45,
25 					5*ANGLE_45,6*ANGLE_45,7*ANGLE_45,0};
26 
CheckDoorMovement(AActor * actor)27 static inline bool CheckDoorMovement(AActor *actor)
28 {
29 	MapTile::Side direction;
30 	switch(actor->dir)
31 	{
32 		default:
33 			return false;
34 		case north:
35 			direction = MapTile::South;
36 			break;
37 		case south:
38 			direction = MapTile::North;
39 			break;
40 		case east:
41 			direction = MapTile::West;
42 			break;
43 		case west:
44 			direction = MapTile::East;
45 			break;
46 	}
47 
48 	if(actor->distance < 0) // Waiting for door?
49 	{
50 		MapSpot spot = map->GetSpot(actor->tilex, actor->tiley, 0);
51 		spot = spot->GetAdjacent(direction, true);
52 		if(spot->slideAmount[direction] != 0xffff)
53 			return true;
54 		// Door is open, go on
55 		actor->distance = TILEGLOBAL;
56 		TryWalk(actor);
57 	}
58 	return false;
59 }
60 
A_Face(AActor * self,AActor * target,angle_t maxturn)61 void A_Face(AActor *self, AActor *target, angle_t maxturn)
62 {
63 	double angle = atan2 ((double) (target->x - self->x), (double) (target->y - self->y));
64 	if (angle<0)
65 		angle = (M_PI*2+angle);
66 	angle_t iangle = (angle_t) (angle*ANGLE_180/M_PI) - ANGLE_90;
67 
68 	if(maxturn > 0 && maxturn < self->angle - iangle)
69 	{
70 		if(self->angle > iangle)
71 		{
72 			if(self->angle - iangle < ANGLE_180)
73 				self->angle -= maxturn;
74 			else
75 				self->angle += maxturn;
76 		}
77 		else
78 		{
79 			if(iangle - self->angle < ANGLE_180)
80 				self->angle += maxturn;
81 			else
82 				self->angle -= maxturn;
83 		}
84 	}
85 	else
86 		self->angle = iangle;
87 }
88 
89 //==========================================================================
90 //
91 // P_AproxDistance
92 //
93 // Gives an estimation of distance (not exact)
94 // From Doom.
95 //
96 //==========================================================================
97 
P_AproxDistance(fixed dx,fixed dy)98 fixed P_AproxDistance (fixed dx, fixed dy)
99 {
100 	dx = abs(dx);
101 	dy = abs(dy);
102 	return (dx < dy) ? dx+dy-(dx>>1) : dx+dy-(dy>>1);
103 }
104 
105 /*
106 =============================================================================
107 
108 							LOCAL CONSTANTS
109 
110 =============================================================================
111 */
112 
113 #define BJRUNSPEED      2048
114 #define BJJUMPSPEED     680
115 
116 /*
117 =============================================================================
118 
119 							LOCAL VARIABLES
120 
121 =============================================================================
122 */
123 
124 
125 dirtype dirtable[9] = {northwest,north,northeast,west,nodir,east,
126 	southwest,south,southeast};
127 
128 /*
129 ===================
130 =
131 = ProjectileTryMove
132 =
133 = returns true if move ok
134 ===================
135 */
136 
ProjectileTryMove(AActor * ob)137 bool ProjectileTryMove (AActor *ob)
138 {
139 	int xl,yl,xh,yh,x,y;
140 	MapSpot check;
141 
142 	xl = (ob->x-ob->radius) >> TILESHIFT;
143 	yl = (ob->y-ob->radius) >> TILESHIFT;
144 
145 	xh = (ob->x+ob->radius) >> TILESHIFT;
146 	yh = (ob->y+ob->radius) >> TILESHIFT;
147 
148 	//
149 	// check for solid walls
150 	//
151 	for (y=yl;y<=yh;y++)
152 		for (x=xl;x<=xh;x++)
153 		{
154 			const bool checkLines[4] =
155 			{
156 				(ob->x+ob->radius) > ((x+1)<<TILESHIFT),
157 				(ob->y-ob->radius) < (y<<TILESHIFT),
158 				(ob->x-ob->radius) < (x<<TILESHIFT),
159 				(ob->y+ob->radius) > ((y+1)<<TILESHIFT)
160 			};
161 			check = map->GetSpot(x, y, 0);
162 			if (check->tile)
163 			{
164 				for(unsigned short i = 0;i < 4;++i)
165 				{
166 					if(check->slideAmount[i] != 0xFFFF && checkLines[i])
167 						return false;
168 				}
169 			}
170 		}
171 
172 	return true;
173 }
174 
175 /*
176 =================
177 =
178 = T_Projectile
179 =
180 =================
181 */
182 
183 static FRandom pr_explodemissile("ExplodeMissile");
T_ExplodeProjectile(AActor * self,AActor * target)184 void T_ExplodeProjectile(AActor *self, AActor *target)
185 {
186 	PlaySoundLocActor(self->deathsound, self);
187 
188 	const Frame *deathstate = NULL;
189 	if(target && (target->flags & FL_SHOOTABLE)) // Fleshy!
190 		deathstate = self->FindState(NAME_XDeath);
191 	if(!deathstate)
192 		deathstate = self->FindState(NAME_Death);
193 
194 	if(deathstate)
195 	{
196 		self->flags &= ~FL_MISSILE;
197 		self->SetState(deathstate);
198 
199 		if((self->flags & FL_RANDOMIZE) && self->ticcount > 0)
200 		{
201 			self->ticcount -= pr_explodemissile() & 7;
202 			if(self->ticcount < 1)
203 				self->ticcount = 1;
204 		}
205 	}
206 	else
207 		self->Destroy();
208 }
209 
T_Projectile(AActor * self)210 void T_Projectile (AActor *self)
211 {
212 	int steps = 1;
213 	fixed movex = self->velx;
214 	fixed movey = self->vely;
215 
216 	// Projectiles can't move faster than their radius in a tic or collision
217 	// detection can be off.
218 	{
219 		fixed maxmove = self->radius - FRACUNIT/64;
220 		if(maxmove <= 0) // Really small projectile? Prevent problems with division
221 			maxmove = FRACUNIT/2;
222 
223 		fixed vel = MAX(abs(movex), abs(movey));
224 		if(vel > maxmove)
225 			steps = 1 + vel / maxmove;
226 
227 		movex /= steps;
228 		movey /= steps;
229 	}
230 
231 	AActor *lastHit = NULL; // For ripping, so we only hit an actor once per tic
232 	do
233 	{
234 		self->x += movex;
235 		self->y += movey;
236 
237 		if (!ProjectileTryMove (self))
238 		{
239 			T_ExplodeProjectile(self, NULL);
240 			return;
241 		}
242 
243 		const bool playermissile = !!(self->flags & FL_PLAYERMISSILE);
244 		AActor::Iterator iter = AActor::GetIterator();
245 		while(iter.Next())
246 		{
247 			AActor *check = iter;
248 			if(check == self)
249 				continue;
250 
251 			// Pass through allies
252 			if(playermissile)
253 			{
254 				if(check == players[0].mo)
255 					continue;
256 			}
257 			else
258 			{
259 				if(check->flags & FL_ISMONSTER)
260 					continue;
261 			}
262 
263 			if((check->flags & (FL_SHOOTABLE|FL_SOLID)) && lastHit != check)
264 			{
265 				fixed deltax = abs(self->x - check->x);
266 				fixed deltay = abs(self->y - check->y);
267 				fixed radius = check->radius + self->radius;
268 				if(deltax < radius && deltay < radius)
269 				{
270 					lastHit = check;
271 					if(check->flags & FL_SHOOTABLE)
272 					{
273 						if(check != players[0].mo)
274 							DamageActor(check, self->GetDamage());
275 						else
276 							TakeDamage(self->GetDamage(), self);
277 
278 						if(!(self->flags & FL_RIPPER) || (check->flags & FL_DONTRIP))
279 						{
280 							T_ExplodeProjectile(self, check);
281 							return;
282 						}
283 					}
284 					// Eventually this will need an actual height check.
285 					else if(check->projectilepassheight != 0)
286 					{
287 						T_ExplodeProjectile(self, check);
288 						return;
289 					}
290 				}
291 			}
292 		}
293 	}
294 	while(--steps);
295 }
296 
297 /*
298 ==================
299 =
300 = A_Scream
301 =
302 ==================
303 */
304 
ACTION_FUNCTION(A_Scream)305 ACTION_FUNCTION(A_Scream)
306 {
307 	PlaySoundLocActor(self->deathsound, self);
308 	return true;
309 }
310 
311 /*
312 ==================
313 =
314 = A_CustomMissile
315 =
316 ==================
317 */
318 
ACTION_FUNCTION(A_CustomMissile)319 ACTION_FUNCTION(A_CustomMissile)
320 {
321 	enum
322 	{
323 		CMF_AIMOFFSET = 1
324 	};
325 
326 	ACTION_PARAM_STRING(missiletype, 0);
327 	ACTION_PARAM_DOUBLE(spawnheight, 1);
328 	ACTION_PARAM_INT(spawnoffset, 2);
329 	ACTION_PARAM_DOUBLE(angleOffset, 3);
330 	ACTION_PARAM_INT(flags, 4);
331 
332 	fixed newx = self->x + spawnoffset*finesine[self->angle>>ANGLETOFINESHIFT]/64;
333 	fixed newy = self->y + spawnoffset*finecosine[self->angle>>ANGLETOFINESHIFT]/64;
334 
335 	double angle = (flags & CMF_AIMOFFSET) ?
336 		atan2 ((double) (self->y - players[0].mo->y), (double) (players[0].mo->x - self->x)) :
337 		atan2 ((double) (newy - players[0].mo->y), (double) (players[0].mo->x - newx));
338 	if (angle<0)
339 		angle = (M_PI*2+angle);
340 	angle_t iangle = (angle_t) (angle*ANGLE_180/M_PI) + (angle_t) ((angleOffset*ANGLE_45)/45);
341 
342 	const ClassDef *cls = ClassDef::FindClass(missiletype);
343 	if(!cls)
344 		return false;
345 	AActor *newobj = AActor::Spawn(cls, newx, newy, 0, SPAWN_AllowReplacement);
346 	newobj->angle = iangle;
347 
348 	newobj->velx = FixedMul(newobj->speed,finecosine[iangle>>ANGLETOFINESHIFT]);
349 	newobj->vely = -FixedMul(newobj->speed,finesine[iangle>>ANGLETOFINESHIFT]);
350 	return true;
351 }
352 
353 //
354 // spectre
355 //
356 
357 
358 /*
359 ===============
360 =
361 = A_Dormant
362 =
363 ===============
364 */
365 
ACTION_FUNCTION(A_Dormant)366 ACTION_FUNCTION(A_Dormant)
367 {
368 	AActor::Iterator iter = AActor::GetIterator();
369 	while(iter.Next())
370 	{
371 		AActor *actor = iter;
372 		if(actor == self || !(actor->flags&(FL_SHOOTABLE|FL_SOLID)))
373 			continue;
374 
375 		fixed radius = self->radius + actor->radius;
376 		if(abs(self->x - actor->x) < radius &&
377 			abs(self->y - actor->y) < radius)
378 			return false;
379 	}
380 
381 	self->flags |= FL_AMBUSH | FL_SHOOTABLE | FL_SOLID;
382 	self->flags &= ~(FL_ATTACKMODE|FL_COUNTKILL);
383 	self->dir = nodir;
384 	self->SetState(self->SeeState);
385 	return true;
386 }
387 
388 /*
389 ============================================================================
390 
391 STAND
392 
393 ============================================================================
394 */
395 
396 
397 /*
398 ===============
399 =
400 = A_Look
401 =
402 ===============
403 */
404 
ACTION_FUNCTION(A_Look)405 ACTION_FUNCTION(A_Look)
406 {
407 	ACTION_PARAM_INT(flags, 0);
408 	ACTION_PARAM_DOUBLE(minseedist, 1);
409 	ACTION_PARAM_DOUBLE(maxseedist, 2);
410 	ACTION_PARAM_DOUBLE(maxheardist, 3);
411 	ACTION_PARAM_DOUBLE(fov, 4);
412 
413 	// FOV of 0 indicates default
414 	if(fov < 0.00001)
415 		fov = 180;
416 
417 	SightPlayer(self, minseedist, maxseedist, maxheardist, fov);
418 	return true;
419 }
420 // Create A_LookEx as an alias to A_Look since we're technically emulating this
421 // ZDoom function with A_Look.
ACTION_ALIAS(A_Look,A_LookEx)422 ACTION_ALIAS(A_Look, A_LookEx)
423 
424 
425 /*
426 ============================================================================
427 
428 CHASE
429 
430 ============================================================================
431 */
432 
433 /*
434 =================
435 =
436 = T_Chase
437 =
438 =================
439 */
440 
441 bool CheckMeleeRange(AActor *inflictor, AActor *inflictee, fixed range)
442 {
443 	fixed r = inflictor->meleerange + inflictee->radius + range;
444 	return abs(inflictee->x - inflictor->x) <= r && abs(inflictee->y - inflictor->y) <= r;
445 }
446 
447 /*
448 ===============
449 =
450 = SelectPathDir
451 =
452 ===============
453 */
454 
SelectPathDir(AActor * ob)455 void SelectPathDir (AActor *ob)
456 {
457 	if (!TryWalk (ob))
458 		ob->dir = nodir;
459 }
460 
461 FRandom pr_chase("Chase");
ACTION_FUNCTION(A_Chase)462 ACTION_FUNCTION(A_Chase)
463 {
464 	enum
465 	{
466 		CHF_DONTDODGE = 1,
467 		CHF_BACKOFF = 2,
468 		CHF_NOSIGHTCHECK = 4,
469 		CHF_NOPLAYACTIVE = 8
470 	};
471 
472 	ACTION_PARAM_STATE(melee, 0, self->MeleeState);
473 	ACTION_PARAM_STATE(missile, 1, self->MissileState);
474 	ACTION_PARAM_INT(flags, 2);
475 
476 	int32_t	move,target;
477 	int		dx,dy,dist = INT_MAX,chance;
478 	bool	dodge = !(flags & CHF_DONTDODGE);
479 	bool	pathing = (self->flags & FL_PATHING) ? true : false;
480 
481 	if (self->dir == nodir)
482 	{
483 		if (pathing)
484 			SelectPathDir (self);
485 		else if (dodge)
486 			SelectDodgeDir (self);
487 		else
488 			SelectChaseDir (self);
489 
490 		self->movecount = pr_chase.RandomOld(false) & 15;
491 	}
492 	// Movecount is an approximation of Doom's movecount which would keep the
493 	// monster moving in some direction for a random amount of time (contrarily
494 	// to wolfensteins block based movement). This is simulated since it also
495 	// determines when a monster attempts to attack
496 	else if(--self->movecount < 0)
497 		self->movecount = pr_chase.RandomOld(false) & 15;
498 
499 	if(!pathing)
500 	{
501 		bool inMeleeRange = melee ? CheckMeleeRange(self, players[0].mo, self->speed) : false;
502 
503 		if(!inMeleeRange && missile)
504 		{
505 			dodge = false;
506 			if ((self->IsFast() || self->movecount == 0) && CheckLine(self)) // got a shot at players[0].mo?
507 			{
508 				self->hidden = false;
509 				dx = abs(self->tilex + dirdeltax[self->dir] - players[0].mo->tilex);
510 				dy = abs(self->tiley + dirdeltay[self->dir] - players[0].mo->tiley);
511 				dist = dx>dy ? dx : dy;
512 				// If we only do ranged attacks, be more aggressive
513 				if(!melee)
514 				{
515 					if(self->missilefrequency >= FRACUNIT)
516 						dist -= 2;
517 					else
518 						// For frequencies less than 1.0 scale back the boost
519 						// in aggressiveness. Through the magic that is integer
520 						// math, this will become 0 at wolfensteins's frequency
521 						// This allows us to approximate Doom's aggressiveness
522 						// while not tampering the Wolf probability
523 						dist -= (2*self->missilefrequency)>>FRACBITS;
524 				}
525 
526 				if(!(flags & CHF_BACKOFF))
527 				{
528 					if (dist > 0)
529 						chance = (208*self->missilefrequency/dist)>>FRACBITS;
530 					else
531 						chance = 256;
532 
533 					// If we have a combo attack monster, we want to skip this
534 					// check as the monster should try to get melee in.
535 					if (dist == 1 && !melee)
536 					{
537 						target = abs(self->x - players[0].mo->x);
538 						if (target < 0x14000l) //  < 1.25 tiles or 80 units
539 						{
540 							target = abs(self->y - players[0].mo->y);
541 							if (target < 0x14000l)
542 								chance = 256;
543 						}
544 					}
545 				}
546 				else
547 					chance = (208*self->missilefrequency)>>FRACBITS;
548 
549 				if ( pr_chase.RandomOld(!!(self->flags & FL_OLDRANDOMCHASE)) < MAX<int>(chance, self->minmissilechance))
550 				{
551 					self->SetState(missile);
552 					return true;
553 				}
554 				dodge = !(flags & CHF_DONTDODGE);
555 			}
556 			else
557 				self->hidden = true;
558 		}
559 		else
560 			self->hidden = !inMeleeRange;
561 	}
562 	else
563 	{
564 		if (!(flags & CHF_NOSIGHTCHECK) && SightPlayer (self, 0, 0, 0, 180))
565 			return true;
566 	}
567 
568 	if(self->dir == nodir)
569 		return false; // object is blocked in
570 
571 	self->angle = dirangle[self->dir];
572 	move = self->speed;
573 
574 	do
575 	{
576 		if (CheckDoorMovement(self))
577 			return true;
578 
579 		if(!pathing)
580 		{
581 			//
582 			// check for melee range
583 			//
584 			if(melee && CheckMeleeRange(self, players[0].mo, self->speed))
585 			{
586 				PlaySoundLocActor(self->attacksound, self);
587 				self->SetState(melee);
588 				return true;
589 			}
590 		}
591 
592 		if (move < self->distance)
593 		{
594 			if(!MoveObj (self,move) && !(flags & CHF_DONTDODGE) && !(self->flags & FL_PATHING))
595 			{
596 				// Touched the player so turn around!
597 				self->dir = dirtype((self->dir+4)%8);
598 				self->distance = FRACUNIT-self->distance;
599 			}
600 			break;
601 		}
602 
603 		//
604 		// reached goal tile, so select another one
605 		//
606 
607 		//
608 		// fix position to account for round off during moving
609 		//
610 		self->x = ((int32_t)self->tilex<<TILESHIFT)+TILEGLOBAL/2;
611 		self->y = ((int32_t)self->tiley<<TILESHIFT)+TILEGLOBAL/2;
612 
613 		move -= self->distance;
614 
615 		dx = abs(self->tilex - players[0].mo->tilex);
616 		dy = abs(self->tiley - players[0].mo->tiley);
617 		dist = dx>dy ? dx : dy;
618 		if (pathing)
619 			SelectPathDir (self);
620 		else if ((flags & CHF_BACKOFF) && dist < 4)
621 			SelectRunDir (self);
622 		else if (dodge)
623 			SelectDodgeDir (self);
624 		else
625 			SelectChaseDir (self);
626 
627 		if (self->dir == nodir)
628 			return false; // object is blocked in
629 	}
630 	while(move);
631 
632 	if(!(flags & CHF_NOPLAYACTIVE) &&
633 		self->activesound != NAME_None && pr_chase.RandomOld(false) < 3)
634 	{
635 		PlaySoundLocActor(self->activesound, self);
636 	}
637 	return true;
638 }
639 
640 /*
641 =============================================================================
642 
643 									FIGHT
644 
645 =============================================================================
646 */
647 
648 
649 /*
650 ===============
651 =
652 = A_WolfAttack
653 =
654 = Try to damage the players[0].mo, based on skill level and players[0].mo's speed
655 =
656 ===============
657 */
658 
659 static FRandom pr_cabullet("CustomBullet");
ACTION_FUNCTION(A_WolfAttack)660 ACTION_FUNCTION(A_WolfAttack)
661 {
662 	enum
663 	{
664 		WAF_NORANDOM = 1
665 	};
666 
667 	ACTION_PARAM_INT(flags, 0);
668 	ACTION_PARAM_STRING(sound, 1);
669 	ACTION_PARAM_FIXED(snipe, 2);
670 	ACTION_PARAM_INT(maxdamage, 3);
671 	ACTION_PARAM_INT(blocksize, 4);
672 	ACTION_PARAM_INT(pointblank, 5);
673 	ACTION_PARAM_INT(longrange, 6);
674 	ACTION_PARAM_DOUBLE(runspeed, 7);
675 
676 	int     dx,dy,dist;
677 	int     hitchance;
678 
679 	runspeed *= 37.5;
680 
681 	A_Face(self, players[0].mo);
682 
683 	if (CheckLine (self))                    // players[0].mo is not behind a wall
684 	{
685 		dx = abs(self->x - players[0].mo->x);
686 		dy = abs(self->y - players[0].mo->y);
687 		dist = dx>dy ? dx:dy;
688 
689 		dist = FixedMul(dist, snipe);
690 		dist /= blocksize<<9;
691 
692 		if (thrustspeed >= runspeed)
693 		{
694 			if (self->flags&FL_VISABLE)
695 				hitchance = 160-dist*16;                // players[0].mo can see to dodge
696 			else
697 				hitchance = 160-dist*8;
698 		}
699 		else
700 		{
701 			if (self->flags&FL_VISABLE)
702 				hitchance = 256-dist*16;                // players[0].mo can see to dodge
703 			else
704 				hitchance = 256-dist*8;
705 		}
706 
707 		// see if the shot was a hit
708 
709 		if (pr_cabullet()<hitchance)
710 		{
711 			int damage = flags & WAF_NORANDOM ? maxdamage : (1 + (pr_cabullet()%maxdamage));
712 			if (dist>=pointblank)
713 				damage >>= 1;
714 			if (dist>=longrange)
715 				damage >>= 1;
716 
717 			TakeDamage (damage,self);
718 		}
719 	}
720 
721 	if(sound.Len() == 1 && sound[0] == '*')
722 		PlaySoundLocActor(self->attacksound, self);
723 	else
724 		PlaySoundLocActor(sound, self);
725 
726 	return true;
727 }
728