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