1 /*
2 Copyright (C) 2009-2021 Parallel Realities
3 
4 This program is free software; you can redistribute it and/or
5 modify it under the terms of the GNU General Public License
6 as published by the Free Software Foundation; either version 2
7 of the License, or (at your option) any later version.
8 
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12 
13 See the 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, 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
18 */
19 
20 #include "../headers.h"
21 
22 #include "../audio/audio.h"
23 #include "../collisions.h"
24 #include "../custom_actions.h"
25 #include "../entity.h"
26 #include "../event/global_trigger.h"
27 #include "../event/trigger.h"
28 #include "../game.h"
29 #include "../geometry.h"
30 #include "../graphics/animation.h"
31 #include "../graphics/decoration.h"
32 #include "../item/item.h"
33 #include "../player.h"
34 #include "../system/error.h"
35 #include "../system/properties.h"
36 #include "../system/random.h"
37 #include "../world/explosion.h"
38 
39 extern Entity *self, player;
40 
41 static void lookForPlayer(void);
42 static void stompAttackInit(void);
43 static void stompAttack(void);
44 static void stompAttackFinish(void);
45 static void attacking(void);
46 static void die(void);
47 static void redDie(void);
48 static void reform(void);
49 static void dieWait(void);
50 static void pieceWait(void);
51 static void pieceReform(void);
52 static void pieceFallout(void);
53 static void reformFinish(void);
54 static void explosionAttackInit(void);
55 static void explosionAttack(void);
56 static void explosionAttackFinish(void);
57 static void explosionMove(void);
58 static void explode(void);
59 static void explosionTouch(Entity *);
60 static void redTakeDamage(Entity *, int);
61 static void creditsMove(void);
62 static void redCreditsMove(void);
63 static void creditsReformFinish(void);
64 
addCenturion(int x,int y,char * name)65 Entity *addCenturion(int x, int y, char *name)
66 {
67 	Entity *e = getFreeEntity();
68 
69 	if (e == NULL)
70 	{
71 		showErrorAndExit("No free slots to add a Centurion");
72 	}
73 
74 	loadProperties(name, e);
75 
76 	e->x = x;
77 	e->y = y;
78 
79 	e->action = &lookForPlayer;
80 	e->die = strcmpignorecase(name, "enemy/red_centurion") == 0 ? &redDie : ¨
81 	e->draw = &drawLoopingAnimationToMap;
82 	e->touch = &entityTouch;
83 	e->takeDamage = strcmpignorecase(name, "enemy/red_centurion") == 0 ? &redTakeDamage : &entityTakeDamageNoFlinch;
84 	e->reactToBlock = &changeDirection;
85 
86 	e->creditsAction = strcmpignorecase(name, "enemy/red_centurion") == 0 ? &redCreditsMove : &creditsMove;;
87 
88 	e->type = ENEMY;
89 
90 	setEntityAnimation(e, "STAND");
91 
92 	return e;
93 }
94 
lookForPlayer()95 static void lookForPlayer()
96 {
97 	if (self->maxThinkTime == 1)
98 	{
99 		if (!(self->flags & ATTRACTED))
100 		{
101 			self->dirX = (self->face == RIGHT ? self->speed : -self->speed);
102 		}
103 	}
104 
105 	if (self->offsetX != 0)
106 	{
107 		if (self->maxThinkTime == 0)
108 		{
109 			if (self->flags & ON_GROUND)
110 			{
111 				playSoundToMap("sound/enemy/centurion/walk", -1, self->x, self->y, 0);
112 			}
113 
114 			self->maxThinkTime = 1;
115 		}
116 
117 		if (!(self->flags & ATTRACTED))
118 		{
119 			self->dirX = self->standingOn == NULL ? 0 : self->standingOn->dirX;
120 		}
121 	}
122 
123 	else
124 	{
125 		self->maxThinkTime = 0;
126 	}
127 
128 	if (self->maxThinkTime == 0 && (self->dirX == 0 || isAtEdge(self) == TRUE))
129 	{
130 		self->dirX = (self->face == RIGHT ? -self->speed : self->speed);
131 
132 		self->face = (self->face == RIGHT ? LEFT : RIGHT);
133 	}
134 
135 	checkToMap(self);
136 
137 	self->thinkTime--;
138 
139 	if (self->thinkTime <= 0)
140 	{
141 		self->thinkTime = 0;
142 	}
143 
144 	if (strcmpignorecase(self->name, "enemy/red_centurion") == 0)
145 	{
146 		if (self->thinkTime == 0 && !(player.flags & TELEPORTING) && player.health > 0 && prand() % 10 == 0)
147 		{
148 			if (collision(self->x + (self->face == RIGHT ? self->w : -240), self->y, 240, self->h, player.x, player.y, player.w, player.h) == 1)
149 			{
150 				self->action = &explosionAttackInit;
151 
152 				if (!(self->flags & ATTRACTED))
153 				{
154 					self->dirX = 0;
155 				}
156 			}
157 		}
158 	}
159 
160 	else
161 	{
162 		if (self->thinkTime == 0 && !(player.flags & TELEPORTING) && player.health > 0 && prand() % 15 == 0)
163 		{
164 			if (collision(self->x + (self->face == RIGHT ? self->w : -160), self->y, 160, self->h, player.x, player.y, player.w, player.h) == 1)
165 			{
166 				self->action = &stompAttackInit;
167 
168 				if (!(self->flags & ATTRACTED))
169 				{
170 					self->dirX = 0;
171 				}
172 			}
173 		}
174 	}
175 }
176 
stompAttackInit()177 static void stompAttackInit()
178 {
179 	setEntityAnimation(self, "ATTACK_1");
180 
181 	self->animationCallback = &stompAttack;
182 
183 	self->action = &attacking;
184 
185 	checkToMap(self);
186 }
187 
stompAttack()188 static void stompAttack()
189 {
190 	setEntityAnimation(self, "ATTACK_2");
191 
192 	playSoundToMap("sound/common/crash", -1, self->x, self->y, 0);
193 
194 	shakeScreen(MEDIUM, 60);
195 
196 	self->thinkTime = 60;
197 
198 	if (player.flags & ON_GROUND)
199 	{
200 		setPlayerStunned(120);
201 	}
202 
203 	self->action = &stompAttackFinish;
204 }
205 
stompAttackFinish()206 static void stompAttackFinish()
207 {
208 	self->thinkTime--;
209 
210 	if (self->thinkTime <= 0)
211 	{
212 		setEntityAnimation(self, "STAND");
213 
214 		self->action = &lookForPlayer;
215 
216 		self->thinkTime = 60;
217 
218 		self->dirX = (self->face == RIGHT ? self->speed : -self->speed);
219 	}
220 }
221 
explosionAttackInit()222 static void explosionAttackInit()
223 {
224 	setEntityAnimation(self, "ATTACK_1");
225 
226 	self->animationCallback = &explosionAttack;
227 
228 	self->action = &attacking;
229 
230 	checkToMap(self);
231 }
232 
explosionAttack()233 static void explosionAttack()
234 {
235 	Entity *e = getFreeEntity();
236 
237 	if (e == NULL)
238 	{
239 		showErrorAndExit("No free slots to add a moving explosion");
240 	}
241 
242 	loadProperties("common/explosion", e);
243 
244 	setEntityAnimation(e, "STAND");
245 
246 	e->x = self->x + self->w / 2 - e->w / 2;
247 	e->y = self->y + self->h - e->h;
248 
249 	e->face = self->face;
250 
251 	e->dirX = e->face == LEFT ? -6 : 6;
252 
253 	e->damage = 1;
254 
255 	e->action = &explosionMove;
256 	e->draw = &drawLoopingAnimationToMap;
257 	e->touch = &explosionTouch;
258 
259 	e->type = ENEMY;
260 
261 	e->flags |= NO_DRAW|DO_NOT_PERSIST;
262 
263 	e->startX = playSoundToMap("sound/boss/ant_lion/earthquake", -1, self->x, self->y, -1);
264 
265 	setEntityAnimation(self, "ATTACK_2");
266 
267 	self->thinkTime = 60;
268 
269 	self->action = &explosionAttackFinish;
270 
271 	checkToMap(self);
272 }
273 
explosionAttackFinish()274 static void explosionAttackFinish()
275 {
276 	self->thinkTime--;
277 
278 	if (self->thinkTime <= 0)
279 	{
280 		setEntityAnimation(self, "STAND");
281 
282 		self->action = &lookForPlayer;
283 
284 		self->thinkTime = 60;
285 
286 		self->dirX = (self->face == RIGHT ? self->speed : -self->speed);
287 	}
288 
289 	checkToMap(self);
290 }
291 
explosionMove()292 static void explosionMove()
293 {
294 	Entity *e;
295 
296 	e = addSmoke(self->x + (prand() % self->w), self->y + self->h, "decoration/dust");
297 
298 	if (e != NULL)
299 	{
300 		e->y -= prand() % e->h;
301 	}
302 
303 	checkToMap(self);
304 
305 	if (isAtEdge(self) == TRUE || self->dirX == 0)
306 	{
307 		self->touch = NULL;
308 
309 		self->dirX = 0;
310 
311 		self->mental = 5;
312 
313 		self->action = &explode;
314 	}
315 }
316 
explosionTouch(Entity * other)317 static void explosionTouch(Entity *other)
318 {
319 	entityTouch(other);
320 
321 	if (other->type == PLAYER)
322 	{
323 		self->touch = NULL;
324 
325 		self->dirX = 0;
326 
327 		self->mental = 5;
328 
329 		self->action = &explode;
330 	}
331 }
332 
explode()333 static void explode()
334 {
335 	int x, y;
336 	Entity *e;
337 
338 	self->thinkTime--;
339 
340 	stopSound(self->startX);
341 
342 	self->startX = -1;
343 
344 	if (self->thinkTime <= 0)
345 	{
346 		x = self->x + self->w / 2;
347 		y = self->y + self->h / 2;
348 
349 		x += (prand() % 32) * (prand() % 2 == 0 ? 1 : -1);
350 		y += (prand() % 32) * (prand() % 2 == 0 ? 1 : -1);
351 
352 		e = addExplosion(x, y);
353 
354 		e->type = ENEMY;
355 
356 		e->damage = 1;
357 
358 		self->mental--;
359 
360 		self->thinkTime = 10;
361 
362 		if (self->mental == 0)
363 		{
364 			self->inUse = FALSE;
365 		}
366 	}
367 }
368 
attacking()369 static void attacking()
370 {
371 	checkToMap(self);
372 }
373 
die()374 static void die()
375 {
376 	int i;
377 	Entity *e;
378 	char name[MAX_VALUE_LENGTH];
379 
380 	SNPRINTF(name, sizeof(name), "%s_piece", self->name);
381 
382 	fireTrigger(self->objectiveName);
383 
384 	fireGlobalTrigger(self->objectiveName);
385 
386 	playSoundToMap("sound/enemy/centurion/centurion_die", -1, self->x, self->y, 0);
387 
388 	for (i=0;i<9;i++)
389 	{
390 		e = addTemporaryItem(name, self->x, self->y, self->face, 0, 0);
391 
392 		e->x += (self->w - e->w) / 2;
393 		e->y += (self->w - e->w) / 2;
394 
395 		e->dirX = (prand() % 5) * (prand() % 2 == 0 ? -1 : 1);
396 		e->dirY = ITEM_JUMP_HEIGHT + (prand() % ITEM_JUMP_HEIGHT);
397 
398 		setEntityAnimationByID(e, i);
399 
400 		e->thinkTime = 60 + (prand() % 180);
401 	}
402 
403 	self->damage = 0;
404 
405 	if (!(self->flags & INVULNERABLE))
406 	{
407 		self->flags &= ~FLY;
408 
409 		self->flags |= (DO_NOT_PERSIST|NO_DRAW);
410 
411 		self->thinkTime = 60;
412 
413 		setCustomAction(self, &invulnerableNoFlash, 240, 0, 0);
414 
415 		self->frameSpeed = 0;
416 
417 		self->action = &standardDie;
418 
419 		self->damage = 0;
420 	}
421 }
422 
redDie()423 static void redDie()
424 {
425 	int i;
426 	Entity *e;
427 	char name[MAX_VALUE_LENGTH];
428 
429 	playSoundToMap("sound/enemy/centurion/centurion_die", -1, self->x, self->y, 0);
430 
431 	SNPRINTF(name, sizeof(name), "%s_piece", self->name);
432 
433 	for (i=0;i<9;i++)
434 	{
435 		e = addTemporaryItem(name, self->x, self->y, self->face, 0, 0);
436 
437 		e->action = &pieceWait;
438 
439 		e->creditsAction = &pieceWait;
440 
441 		e->fallout = &pieceFallout;
442 
443 		e->x += (self->w - e->w) / 2;
444 		e->y += (self->w - e->w) / 2;
445 
446 		e->dirX = (prand() % 5) * (prand() % 2 == 0 ? -1 : 1);
447 		e->dirY = ITEM_JUMP_HEIGHT + (prand() % ITEM_JUMP_HEIGHT);
448 
449 		setEntityAnimationByID(e, i);
450 
451 		e->head = self;
452 	}
453 
454 	self->endX = self->damage;
455 
456 	self->damage = 0;
457 
458 	self->health = 0;
459 
460 	self->dirX = 0;
461 
462 	self->mental = 1;
463 
464 	self->flags &= ~FLY;
465 
466 	self->flags |= NO_DRAW;
467 
468 	self->takeDamage = NULL;
469 
470 	self->action = &dieWait;
471 
472 	self->creditsAction = &dieWait;
473 
474 	self->thinkTime = 120;
475 }
476 
dieWait()477 static void dieWait()
478 {
479 	self->thinkTime--;
480 
481 	if (self->thinkTime <= 0)
482 	{
483 		self->mental = 0;
484 
485 		self->thinkTime = 9;
486 
487 		self->action = &reform;
488 
489 		self->creditsAction = &reform;
490 	}
491 }
492 
reform()493 static void reform()
494 {
495 	if (self->health == -1)
496 	{
497 		increaseKillCount();
498 
499 		dropRandomItem(self->x + self->w / 2, self->y);
500 
501 		self->action = &entityDieVanish;
502 	}
503 
504 	if (self->thinkTime == 0)
505 	{
506 		self->thinkTime = 30;
507 
508 		self->action = &reformFinish;
509 
510 		self->creditsAction = &creditsReformFinish;
511 	}
512 }
513 
reformFinish()514 static void reformFinish()
515 {
516 	self->thinkTime--;
517 
518 	if (self->thinkTime <= 0)
519 	{
520 		self->health = self->maxHealth;
521 
522 		self->flags &= ~NO_DRAW;
523 
524 		self->action = &lookForPlayer;
525 
526 		facePlayer();
527 
528 		self->dirX = self->face == LEFT ? -self->speed : self->speed;
529 
530 		self->damage = 1;
531 
532 		self->takeDamage = &redTakeDamage;
533 
534 		setEntityAnimation(self, "STAND");
535 
536 		self->creditsAction = &creditsMove;
537 	}
538 }
539 
pieceWait()540 static void pieceWait()
541 {
542 	if (self->head->health == -1)
543 	{
544 		self->action = &entityDieNoDrop;
545 	}
546 
547 	else if (self->head->mental == 0)
548 	{
549 		self->action = &pieceReform;
550 
551 		self->creditsAction = &pieceReform;
552 
553 		if (self->face == LEFT)
554 		{
555 			self->targetX = self->head->x + self->head->w - self->w - self->offsetX;
556 		}
557 
558 		else
559 		{
560 			self->targetX = self->head->x + self->offsetX;
561 		}
562 
563 		self->targetY = self->head->y + self->offsetY;
564 
565 		calculatePath(self->x, self->y, self->targetX, self->targetY, &self->dirX, &self->dirY);
566 
567 		self->dirX *= 4;
568 		self->dirY *= 4;
569 
570 		self->flags |= FLY;
571 
572 		self->touch = NULL;
573 	}
574 
575 	if ((self->flags & ON_GROUND) && !(self->flags & FLY))
576 	{
577 		self->dirX = 0;
578 	}
579 
580 	checkToMap(self);
581 }
582 
pieceReform()583 static void pieceReform()
584 {
585 	if (atTarget())
586 	{
587 		if (self->mental == 0)
588 		{
589 			self->mental = 1;
590 
591 			self->head->thinkTime--;
592 		}
593 
594 		else
595 		{
596 			if (self->head->health == self->head->maxHealth)
597 			{
598 				self->inUse = FALSE;
599 			}
600 		}
601 	}
602 
603 	else
604 	{
605 		self->x += self->dirX;
606 		self->y += self->dirY;
607 	}
608 }
609 
pieceFallout()610 static void pieceFallout()
611 {
612 	self->head->health = -1;
613 
614 	self->inUse = FALSE;
615 }
616 
redTakeDamage(Entity * other,int damage)617 static void redTakeDamage(Entity *other, int damage)
618 {
619 	Entity *temp;
620 
621 	if (self->flags & INVULNERABLE)
622 	{
623 		return;
624 	}
625 
626 	if (damage != 0)
627 	{
628 		self->health -= damage;
629 
630 		if (other->type == PROJECTILE)
631 		{
632 			temp = self;
633 
634 			self = other;
635 
636 			self->die();
637 
638 			self = temp;
639 		}
640 
641 		if (self->health > 0)
642 		{
643 			setCustomAction(self, &flashWhite, 6, 0, 0);
644 
645 			/* Don't make an enemy invulnerable from a projectile hit, allows multiple hits */
646 
647 			if (other->type != PROJECTILE)
648 			{
649 				setCustomAction(self, &invulnerableNoFlash, HIT_INVULNERABLE_TIME, 0, 0);
650 			}
651 
652 			if (self->pain != NULL)
653 			{
654 				self->pain();
655 			}
656 		}
657 
658 		else
659 		{
660 			self->animationCallback = NULL;
661 
662 			self->damage = 0;
663 
664 			self->die();
665 		}
666 	}
667 }
668 
creditsMove()669 static void creditsMove()
670 {
671 	float dirX;
672 
673 	self->face = RIGHT;
674 
675 	if (self->maxThinkTime == 1)
676 	{
677 		self->dirX = self->speed;
678 	}
679 
680 	if (self->offsetX != 0)
681 	{
682 		if (self->maxThinkTime == 0)
683 		{
684 			self->maxThinkTime = 1;
685 
686 			playSoundToMap("sound/enemy/centurion/walk", -1, self->x, self->y, 0);
687 		}
688 
689 		self->dirX = 0;
690 	}
691 
692 	else
693 	{
694 		self->maxThinkTime = 0;
695 	}
696 
697 	dirX = self->dirX;
698 
699 	checkToMap(self);
700 
701 	if (self->dirX == 0 && dirX != 0)
702 	{
703 		self->inUse = FALSE;
704 	}
705 }
706 
redCreditsMove()707 static void redCreditsMove()
708 {
709 	float dirX;
710 
711 	self->face = RIGHT;
712 
713 	if (self->maxThinkTime == 1)
714 	{
715 		self->dirX = self->speed;
716 	}
717 
718 	if (self->offsetX != 0)
719 	{
720 		if (self->maxThinkTime == 0)
721 		{
722 			self->thinkTime++;
723 
724 			self->maxThinkTime = 1;
725 
726 			playSoundToMap("sound/enemy/centurion/walk", -1, self->x, self->y, 0);
727 		}
728 
729 		self->dirX = 0;
730 	}
731 
732 	else
733 	{
734 		self->maxThinkTime = 0;
735 	}
736 
737 	dirX = self->dirX;
738 
739 	checkToMap(self);
740 
741 	if (self->dirX == 0 && dirX != 0)
742 	{
743 		self->inUse = FALSE;
744 	}
745 
746 	if (self->thinkTime >= 10)
747 	{
748 		self->creditsAction = &redDie;
749 	}
750 }
751 
creditsReformFinish()752 static void creditsReformFinish()
753 {
754 	self->thinkTime--;
755 
756 	if (self->thinkTime <= 0)
757 	{
758 		self->health = self->maxHealth;
759 
760 		self->flags &= ~NO_DRAW;
761 
762 		self->face = RIGHT;
763 
764 		self->dirX = self->face == LEFT ? -self->speed : self->speed;
765 
766 		setEntityAnimation(self, "STAND");
767 
768 		self->creditsAction = &creditsMove;
769 	}
770 }
771