sceptre and alienboss psychic waves now makes shopkeepers angry
[k8vacspelynky.git] / mapent / enemies / alienboss.vc
blobc79d8d4abc891faba14e65cf6f7d158a5ade998e
1 /**********************************************************************************
2  * Copyright (c) 2008, 2009 Derek Yu and Mossmouth, LLC
3  * Copyright (c) 2010, Moloch
4  * Copyright (c) 2018, Ketmar Dark
5  *
6  * This file is part of Spelunky.
7  *
8  * You can redistribute and/or modify Spelunky, including its source code, under
9  * the terms of the Spelunky User License.
10  *
11  * Spelunky is distributed in the hope that it will be entertaining and useful,
12  * but WITHOUT WARRANTY.  Please see the Spelunky User License for more details.
13  *
14  * The Spelunky User License should be available in "Game Information", which
15  * can be found in the Resource Explorer, or as an external file called COPYING.
16  * If not, please obtain a new copy of Spelunky from <http://spelunkyworld.com/>
17  *
18  **********************************************************************************/
19 class ObjBarrier['oBarrier'] : MapObject;
22 override bool initialize () {
23   if (!::initialize()) return false;
24   setSprite('sBarrier');
25   return true;
29 override bool collidesWith (MapEntity e, optional bool ignoreDims) {
30   if (!e || width < 1 || height < 1) return false;
31   if (e == self) return false; // never
32   return e.isRectHitSimple(x0, y0, width, height);
36 override void onBulletHit (ObjBullet bullet) {
37   level.MakeMapObject(bullet.ix, bullet.iy, 'oSmokePuff');
38   return;
42 transient array!MapObject objhits;
44 override void thinkFrame () {
45   setCollisionBoundsFromFrame();
46   //writeln("barrier hitbox: (", hitboxX, ",", hitboxY, "); size:(", hitboxW, "x", hitboxH, ")");
48   // player
49   auto plr = level.player;
50   //writeln("C0:", plr.isRectHitSimple(x0, y0, width, height), "; C1:", plr.collidesWith(self));
51   if (plr.collidesWith(self)) {
52     //writeln("barrier with player");
53     bool wasInvi = (plr.invincible == 0);
54     plr.blink = 30;
55     plr.invincible = 30;
56     plr.yVel = -2;
57     if (plr.ix < ix) plr.xVel = -6; else plr.xVel = 6;
58     if (!wasInvi && global.plife > 0) global.plife -= damage;
59     // drop holding item
60     if (!plr.isHoldingBombOrRope() && plr.holdItem !isa PlayerWeapon) {
61       plr.scrDropItem(LostCause.Throw, -global.randOther(4, 6), -2);
62       /*
63       auto it = plr.holdItem;
64       if (it && it !isa PlayerWeapon) {
65         plr.holdItem = none;
66         it.xVel = -global.randOther(4, 6);
67         it.yVel = -2;
68       }
69       */
70     }
71     plr.playSound('sndHurt');
72   }
74   // objects
75   objhits.clear();
76   level.isObjectInRect(x0, y0, width, height, delegate bool (MapObject o) {
77     if (o.heldBy) return false;
78     if (o isa ItemWebBall || o isa MapItem) {
79       if (o.collidesWith(self)) {
80         bool found = false;
81         foreach (MapObject xo; objhits) if (xo == o) { found = true; break; }
82         if (!found) objhits[$] = o;
83       }
84     } else if (o isa MapEnemy) {
85       o.xVel = -global.randOther(4, 6);
86       o.yVel = -2;
87     }
88     return false;
89   });
91   foreach (MapObject o; objhits) {
92     if (!o || !o.isInstanceAlive) continue;
93     if (o isa ItemWebBall) { o.instanceRemove(); continue; }
94     auto bomb = ItemBomb(o);
95     if (bomb) bomb.armIt(global.randOther(4, 8));
96     o.xVel = -global.randOther(4, 6);
97     o.yVel = -2;
98   }
99   objhits.clear();
103 defaultproperties {
104   objName = 'Barrier';
105   desc = "Laser Beam";
106   desc2 = "The business end of a beam turret.";
108   damage = 1; // damage amount to player on contact
109   setCollisionBounds(0, 0, 10, 10);
111   canBeHitByBullet = true;
113   depth = 108;
117 // ////////////////////////////////////////////////////////////////////////// //
118 class ObjBarrierEmitter['oBarrierEmitter'] : MapObject;
120 bool barrierChecked;
123 override bool initialize () {
124   if (!::initialize()) return false;
125   setSprite('sBarrierEmitter');
126   return true;
130 override bool collidesWith (MapEntity e, optional bool ignoreDims) {
131   if (!e || width < 1 || height < 1) return false;
132   if (e == self) return false; // never
133   return e.isRectHitSimple(x0, y0, width, height);
137 void destroyBarrier () {
138   level.forEachObject(delegate bool (MapObject o) {
139     if (o isa ObjBarrier) o.instanceRemove();
140     return false;
141   });
145 override void onDestroy () {
146   foreach (; 0..6) {
147     auto obj = level.MakeMapObject(ix+2+global.randOther(0, 14), iy+2+global.randOther(0, 14), 'oFlareSpark');
148     if (obj) obj.yVel = global.randOther(1, 3);
149   }
150   level.scrShake(10);
151   playSound('sndSmallExplode');
152   destroyBarrier();
153   ::onDestroy();
157 override void onBulletHit (ObjBullet bullet) {
158   instanceRemove();
162 // return `true` if item hits the character
163 // this is called after item's event
165 bool onHitByItem (MapItem item) {
166   // are we invincible?
167   if (!barrierChecked) return false;
168   //if (invincible) return false;
169   if (fabs(item.xVel) > 2 || fabs(item.yVel) > 2) {
170     // it hit us
171     writeln("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
172     instanceRemove();
173     item.playSound('sndHit');
174     return true;
175   }
176   item.moveRel(-item.xVel, -item.yVel);
177   item.xVel = -item.xVel;
178   return false;
183 void moveObjectAwayHoriz (MapObject o) {
184   if (!o || !o.isInstanceAlive) return;
185   if (o.x0 > xCenter) {
186     o.setX(x1+1);
187   } else {
188     o.shiftX(x0-o.x1);
189   }
193 transient array!MapObject objhits;
195 override void thinkFrame () {
196   if (!barrierChecked) {
197     barrierChecked = true;
198     auto bar = level.forEachObject(delegate bool (MapObject o) { return (o isa ObjBarrier); });
199     if (!bar) level.MakeMapObject(ix, iy+16, 'oBarrier');
200   }
202   setCollisionBoundsFromFrame();
203   //writeln("barrier hitbox: (", hitboxX, ",", hitboxY, "); size:(", hitboxW, "x", hitboxH, ")");
205   /*
206   level.isObjectInRect(x0, y0, width, height, delegate bool (MapObject o) {
207     if (o isa MapItem && fabs(o.xVel) > 2 || fabs(o.yVel) > 2) {
208       if (o.collidesWith(self)) {
209         writeln("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
210         // it hit us
211         instanceRemove();
212         o.playSound('sndHit');
213         return true;
214       }
215     }
216     return false;
217   });
218   */
220   if (!level.isSolidAtPoint(ix, iy-16)) { instanceRemove(); return; }
222   // player
223   auto plr = level.player;
224   //writeln("C0:", plr.isRectHitSimple(x0, y0, width, height), "; C1:", plr.collidesWith(self));
225   if (plr.collidesWith(self)) {
226     //writeln("barrier with player");
227     //plr.yVel = -2;
228     /*
229     plr.xVel = fabs(plr.xVel);
230     if (plr.ix >= ix) plr.xVel = -plr.xVel;
231     */
232     moveObjectAwayHoriz(plr);
233   }
235   // objects
236   objhits.clear();
237   level.isObjectInRect(x0, y0, width, height, delegate bool (MapObject o) {
238     if (o.heldBy) return false;
239     if (o isa ItemWebBall) return false;
240     if (o isa MapItem) {
241       if (o.collidesWith(self)) {
242         bool found = false;
243         foreach (MapObject xo; objhits) if (xo == o) { found = true; break; }
244         if (!found) objhits[$] = o;
245       }
246     } else if (o isa MapEnemy) {
247       //o.xVel = fabs(o.xVel);
248       //if (o.ix >= ix) o.xVel = -o.xVel;
249       moveObjectAwayHoriz(o);
250     }
251     return false;
252   });
254   foreach (MapObject o; objhits) {
255     if (!o || !o.isInstanceAlive) continue;
256     //if (invincible) return false;
257     if (fabs(o.xVel) > 2 || fabs(o.yVel) > 2) {
258       // it hit us
259       //writeln("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
260       instanceRemove();
261       o.playSound('sndHit');
262       objhits.clear();
263       return;
264     }
265     //o.xVel = fabs(o.xVel);
266     //if (o.ix >= ix) o.xVel = -o.xVel;
267     moveObjectAwayHoriz(o);
268   }
269   objhits.clear();
273 defaultproperties {
274   objName = 'Barrier Emitter';
275   desc = "Beam Turret";
276   desc2 = "An alien defense mechanism that emits a short-range laser beam.";
278   imageSpeed = 0.5;
279   canBeHitByBullet = true;
280   setCollisionBounds(0, 0, 10, 10);
282   depth = 100;
286 // ////////////////////////////////////////////////////////////////////////// //
287 class EnemyAlienBoss['oAlienBoss'] : MapEnemy;
289 int psychicRecover;
290 int attackRange = 96;
293 // ////////////////////////////////////////////////////////////////////////// //
294 override bool initialize () {
295   if (!::initialize()) return false;
296   setSprite('sAlienBoss');
297   //!if (isRoom("rOlmec2")) attackRange = 128; else attackRange = 96;
298   //dir = global.randOther(0, 1);
299   return true;
303 // ////////////////////////////////////////////////////////////////////////// //
304 void destroyBarrier () {
305   level.forEachObject(delegate bool (MapObject o) {
306     if (o isa ObjBarrierEmitter) o.instanceRemove();
307     return false;
308   });
312 // ////////////////////////////////////////////////////////////////////////// //
313 override void onAnimationLooped () {
314   if (spriteLName == 'sAlienBossDie') {
315     setSprite('sAlienBossDead');
316     destroyBarrier();
317   } else if (spriteLName == 'sAlienBossHurt') {
318     setSprite('sAlienBoss');
319   }
323 // ////////////////////////////////////////////////////////////////////////// //
324 override bool onTouchedByPlayer (PlayerPawn plr) {
325   if (plr.dead || dead || status == DEAD) return false;
326   if ((stunned || status == STUNNED) && !global.hasSpikeShoes) return false;
328   int x = ix, y = iy;
329   int plrx = plr.ix, plry = plr.iy;
331   // jumped on
332   if ((plr.status == JUMPING || plr.status == FALLING) && plry < y+8 && !plr.swimming) {
333     if (status < STUNNED || global.hasSpikeShoes) {
334       plr.yVel = -6-0.2*plr.yVel;
335       int bam = 1;
336       if (global.hasSpikeShoes) {
337         hp -= 3*trunc(floor(plr.fallTimer/16)+1);
338         bam = 3;
339       } else {
340         hp -= 1*trunc(floor(plr.fallTimer/16)+1);
341       }
342       plr.fallTimer = 0;
343       countsAsKill = true;
344       spillBlood(amount:bam);
345       plr.playSound('sndHit');
346     }
347   } else if (plr.invincible == 0 && status != DEAD) {
348     plr.blink = 30;
349     plr.invincible = 30;
350     if (plry < y) plr.yVel = -6;
351     if (plrx < x) plr.xVel = -6; else plr.xVel = 6;
352     //instance_create(plr.x, plr.y, oBlood);
353     if (global.plife > 0) {
354       global.plife -= damage;
355       if (global.plife <= 0 /*&& isRealLevel()*/) level.addDeath(objName);
356     }
357     plr.spillBlood();
358     plr.playSound('sndHurt');
359   }
361   return false; // don't skip thinker
365 // return `false` to do standard weapon processing
366 override bool onTouchedByPlayerWeapon (PlayerPawn plr, PlayerWeapon wpn) {
367   if (heldBy) return true;
368   if (status < STUNNED) {
369     wpn.hitEnemy = true;
370     hp -= wpn.damage;
371     countsAsKill = true;
372     spillBlood(amount:1);
373     plr.playSound('sndHit');
374   }
375   return true;
379 override void onBulletHit (ObjBullet bullet) {
380   if (status == DEAD) return;
381   countsAsKill = true;
382   ::onBulletHit(bullet);
386 // ////////////////////////////////////////////////////////////////////////// //
387 override void thinkFrame () {
388   int x = ix, y = iy;
390   if (level.isSolidAtPoint(x+8, y+8)) hp = 0;
392   if (hp < 1 && status != DEAD) {
393     status = DEAD;
394     setSprite('sAlienBossDie');
395     depth = 101;
396     if (countsAsKill) level.addKill(objName);
397     if (false /*carries != ""*/) {
398       //!!!if (holds != "") scrMakeItem(carries, x+16, y+16, 1); else scrMakeItem(carries, x+16, y+16);
399     } else {
400       foreach (; 0..4) {
401         MapObject gem = none;
402         switch (global.randOther(1, 3)) {
403           case 1: gem = level.MakeMapObject(x+16, y+16, 'oEmeraldBig'); break;
404           case 2: gem = level.MakeMapObject(x+16, y+16, 'oSapphireBig'); break;
405           case 3: gem = level.MakeMapObject(x+16, y+16, 'oRubyBig'); break;
406         }
407         if (gem) {
408           gem.xVel = global.randOther(0, 3)-global.randOther(0, 3);
409           gem.yVel = -2;
410         }
411       }
412     }
413     //!with (oBossBlock) dying = true;
414   }
416   if (spriteLName == 'sAlienBossDie') {
417     if (global.randOther(1, 2) == 1) {
418       foreach (; 0..2) scrCreateBlood(ix+8, iy+global.randOther(14, 18), 2);
419     }
420     if (imageFrame >= 8) {
421       setSprite('sAlienBossDead');
422       destroyBarrier();
423     }
424   }
426   yVel = fmin(yVel+myGrav, yVelLimit);
428   if (xVel > 0) xVel -= 0.1;
429   if (xVel < 0) xVel += 0.1;
430   if (fabs(xVel) < 0.5) xVel = 0;
432   if (isCollisionBottom(1) && status != STUNNED) yVel = 0;
434   if (status == IDLE) xVel = 0;
436   moveRel(xVel, yVel);
438   x = ix;
439   y = iy;
441   if (isCollision()) { flty = y-2; y = iy; }
443   auto plr = level.player;
444   auto dist = distanceToEntity(plr);
446   if (psychicRecover > 0) {
447     --psychicRecover;
448   } else if (dist < attackRange && status != DEAD && !plr.dead && !plr.stunned && plr.invincible == 0) {
449     foreach (; 0..6) {
450       level.MakeMapObject(x+16+global.randOther(0, 32)-global.randOther(0, 32), y+16+global.randOther(0, 32)-global.randOther(0, 32), 'oPsychicCreate');
451     }
452     level.MakeMapObject(x+16, y+16, 'oPsychicWave');
453     //!if (isRoom("rOlmec2")) psychicRecover = 30; else psychicRecover = 100;
454     psychicRecover = 100;
455     playSound('sndPsychic');
456   }
458   if (spriteLName != 'sAlienBossHurt') imageSpeed = 0.25;
460   /*
461   if (status != DEAD && spriteLName != 'sAlienBossHurt' && facing == LEFT) sprite_index = sAlienBoss;
462   if (status != DEAD && spriteLName != 'sAlienBossHurt' && facing == RIGHT) sprite_index = sAlienBoss;
463   */
464   if (status != DEAD && spriteLName != 'sAlienBossHurt') setSprite('sAlienBoss');
468 defaultproperties {
469   objName = 'Alien Boss';
470   desc = "Alien Boss";
471   desc2 = "This psychic alien subspecies operates the more complex mechanics of their starship and holds sway over the rest of the crew.";
473   setCollisionBounds(0, 0, 32, 32);
474   xVel = 2.5;
475   imageSpeed = 0.25;
477   yVelLimit = 6; // YASM 1.7
479   // stats
480   hp = 10;
481   invincible = 0;
483   // status
484   status = IDLE;
486   //dir = Dir.Right;
488   bloodless = false;
489   //bloodLeft = 1;
491   canPickUp = false;
492   bounced = false;
493   canBeHitByBullet = true;
495   psychicRecover = 100;
496   whipTimerValue = 5;
498   doBasicPhysics = false;
499   countsAsKill = true;
500   leavesBody = false; // default processing
501   checkInsideBlock = true;
502   allowWaterProcessing = true;
504   bloodOfsX = 16;
505   bloodOfsY = 25;
507   depth = 60;
511 // ////////////////////////////////////////////////////////////////////////// //
512 class ItemSfxPsychicCreateAlienBoss['oPsychicCreate'] : MapObject;
515 float dirAngle;
516 bool dirAngleSet;
519 override bool initialize () {
520   if (!::initialize()) return false;
521   setSprite('sPsychicCreate');
522   return true;
526 override void onAnimationLooped () {
527   instanceRemove();
531 override void thinkFrame () {
532   if (!dirAngleSet) {
533     dirAngleSet = true;
534     auto aboss = level.forEachObject(delegate bool (MapObject o) { return (o isa EnemyAlienBoss); });
535     if (aboss) dirAngle = pointDirection(ix, iy, aboss.ix+16, aboss.iy+16);
536   }
537   shiftXY(2*cos(dirAngle), -2*sin(dirAngle));
541 defaultproperties {
542   objName = 'Psychic Wave';
543   yAcc = 0.6;
544   imageSpeed = 0.4;
545   grav = 0;
546   spectral = true;
547   depth = 1;
551 // ////////////////////////////////////////////////////////////////////////// //
552 class ItemProjPsychicWaveAlienBoss['oPsychicWave'] : MapObject;
554 float dm;
557 override bool initialize () {
558   if (!::initialize()) return false;
559   setSprite('sPsychicWave');
560   if (false /*isRoom("rOlmec2")*/) {
561     imageSpeed = 0.3;
562     dm = 2.5;
563   } else {
564     imageSpeed = 0.25;
565     dm = 2;
566   }
567   return true;
571 override void onAnimationLooped () {
572   instanceRemove();
576 override void thinkFrame () {
577   auto plr = level.player;
579   auto dirAngle = pointDirection(ix, iy, plr.ix, plr.iy);
580   shiftXY(dm*cos(dirAngle), -dm*sin(dirAngle));
582   if (isOutsideOfLevel()) { instanceRemove(); return; }
584   setCollisionBoundsFromFrame();
586   if (plr.visible && plr.invincible == 0 && plr.collidesWith(self)) {
587     plr.blink = 30;
588     plr.invincible = 30;
589     //if (plry < y) plr.yVel = -6;
590     //if (plrx < x) plr.xVel = -6; else plr.xVel = 6;
591     //instance_create(plr.x, plr.y, oBlood);
592     plr.xVel = global.randOther(0, 2)-global.randOther(1, 2);
593     plr.xVel = -1;
594     plr.yVel = -6;
595     if (global.plife > 0) {
596       global.plife -= damage;
597       if (global.plife <= 0 /*&& isRealLevel()*/) level.addDeath(objName);
598     }
599     plr.spillBlood();
600     playSound('sndHurt');
601   }
603   level.isObjectInRect(x0, y0, width, height, delegate bool (MapObject o) {
604     if (o isa EnemyAlienBoss) return false;
605     if (/*o.dead ||*/ o.invincible) return false;
606     // shopkeeper
607     auto scp = MonsterShopkeeper(o);
608     if (scp && !scp.angered) scp.status = ATTACK;
609     // damsel
610     auto dms = MonsterDamsel(o);
611     if (dms) {
612       if (dms.status == DEAD) return false;
613       dms.xVel = global.randOther(0, 2)-global.randOther(1, 2);
614       dms.xVel = -1;
615       dms.yVel = -6;
616       if (dms.hp > 0) {
617         dms.hp -= damage;
618         dms.spillBlood();
619         dms.status = THROWN;
620         dms.counter = 120;
621         //dms.kissCount = min(1, dms.kissCount);
622         dms.calm = false;
623         //playSound('sndDamsel');
624       }
625       instanceRemove(); //???
626       return true;
627     }
628     // enemy
629     auto enemy = MapEnemy(o);
630     if (enemy) {
631       //if (enemy.status == DEAD) return false;
632       if (enemy.hp > 0) {
633         enemy.hp -= damage;
634         enemy.spillBlood();
635       }
636       enemy.xVel = global.randOther(0, 2)-global.randOther(1, 2);
637       enemy.xVel = -1;
638       enemy.yVel = -6;
639       return false;
640     }
641     return false;
642   }, castClass:MapEnemy);
646 defaultproperties {
647   objName = 'Psychic Wave';
648   damage = 1;
649   yVel = 0;
650   yAcc = 0.6;
651   imageSpeed = 0.25;
652   //counter = 5;
653   //dirAngle = 0;
654   //dir = Dir.Left;
655   depth = 1;