added Moloch (author of YASM mod) to copyrights in source files
[k8vacspelynky.git] / mapent / MapEnemy.vc
blob95523f3fb50ebad0587c6c9c3844c1b611cfc9a9
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 // enemies
20 class MapEnemy : MapObject;
22 bool doBasicPhysics = true;
23 bool liveInWater; // so they won't splash
24 bool leavesBody;
26 // `true`: check hitbox
27 bool checkInsideBlock = true;
28 bool canBeStunned = false;
29 bool spikesRestoreGravity = true;
31 int checkInsideBlockOfsX = 8;
32 int checkInsideBlockOfsY = 8;
33 int checkInsideBlockOfsW = 1;
34 int checkInsideBlockOfsH = 1;
37 int bloodOffsetX = 8;
38 int bloodOffsetY = 8;
39 int bloodAmount = 3;
42 // ////////////////////////////////////////////////////////////////////////// //
43 override bool onCanBePickedUp (PlayerPawn plr) {
44   return (dead || status >= STUNNED || meGoldMonkey);
48 // block "collected" stats
49 override void onPickedUp (PlayerPawn plr) {
53 // ////////////////////////////////////////////////////////////////////////// //
54 // return `false` to do standard weapon processing
55 override bool onTouchedByPlayerWeapon (PlayerPawn plr, PlayerWeapon wobj) {
56   if (hp > 0 && !dead && !stunned && status < STUNNED) wobj.hitEnemy = true;
57   if (invincible <= 0) {
58     hp -= wobj.damage;
59     spillBlood();
60     countsAsKill = true;
61   }
62   playSound('sndHit');
63   return true;
67 // ////////////////////////////////////////////////////////////////////////// //
68 override bool onTouchedByPlayer (PlayerPawn plr) {
69   if (dead || status == DEAD || plr.dead) return false;
70   if ((stunned || status == STUNNED) && !global.hasSpikeShoes) return false;
71   int x = ix, y = iy;
72   int plrx = plr.ix, plry = plr.iy;
74   // jumped on - oCaveman, oManTrap replaces this script with its own
75   if (abs(plrx-(x+8)) > 12) {
76     // do nothing
77   } else if (!plr.dead && (plr.status == JUMPING || plr.status == FALLING) && plry < y+8 && !plr.swimming) {
78     plr.yVel = -6-0.2*plr.yVel;
79     if (global.hasSpikeShoes) {
80       hp -= trunc(3*(floor(plr.fallTimer/16.0)+1));
81     } else {
82       hp -= trunc(1*(floor(plr.fallTimer/16.0)+1));
83     }
84     spillBlood(amount:1);
85     plr.fallTimer = 0;
86     plr.playSound('sndHit');
87   } else if (!plr.dead && plr.invincible == 0) {
88     plr.blink = 30;
89     plr.invincible = 30;
90     if (plr.status != CLIMBING) {
91       plr.xVel = (plrx < x ? -6 : 6);
92     }
93     if (global.plife > 0) {
94       global.plife -= damage;
95       if (global.plife < 0) level.addDeath(objName);
96     }
97     //plr.spillBlood(); // snakes should do that, for example
98     plr.playSound('sndHurt');
99   }
101   return false; // don't skip thinker
105 // ////////////////////////////////////////////////////////////////////////// //
106 // virtual
107 // return `true` if item hits the character
108 // this is called after item's event
109 bool onHitByItem (MapItem item) {
110   // are we invincible?
111   if (invincible) return false;
112   // any items will fly thru stunned enemies (but not projectiles)
113   if (status >= STUNNED && item !isa ItemProjectile) return false;
114   // item does no damage?
115   if (!item.damage) return false;
116   // speed check
117   if (fabs(item.xVel) < ItemSpeedToHitX && fabs(item.yVel) < ItemSpeedToHitY) return false;
118   //writeln("item velocity: (", item.xVel, ",", item.yVel, ")");
119   // drop us
120   if (heldBy) heldBy.holdItem = none;
122   if (!dead) {
123     hp -= damage;
124     if (hp <= 0) {
125       auto proj = ItemProjectile(item);
126       if (proj) countsAsKill = proj.launchedByPlayer;
127     }
128     if (!dead) spillBlood(amount:2);
129   }
131   item.playSound('sndHit');
132   if (status != STUNNED) xVel = xVel*0.3;
134   if (self isa MonsterDamsel) {
135     status = THROWN;
136     MonsterDamsel(self).calm = false;
137     counter = MonsterDamsel(self).stunMax;
138   } else {
139     status = STUNNED;
140     counter = stunTime;
141   }
143   return true; // it hit us
147 // ////////////////////////////////////////////////////////////////////////// //
148 // return `true` if this entity can be sacrificed
149 override bool canBeSacrificed (MapTile altar) {
150   return !bloodless;
154 // ////////////////////////////////////////////////////////////////////////// //
155 // return `true` from any handler to stop further processing
157 override bool onInsideBlock (MapTile block) {
158   // if we are inside a block, die
159   if (heldBy) return false;
160   if (hp < 1) return false;
161   hp = 0;
162   if (!bloodless) scrCreateBlood(ix+bloodOffsetX, iy+bloodOffsetY, bloodAmount);
163   if (countsAsKill) level.addKill(objName);
164   instanceRemove();
165   return true;
169 override bool onFellInWater (MapTile water) {
170   if (!liveInWater && !heldBy) {
171     level.MakeMapObject(xCenter, iy, 'oSplash');
172     playSound('sndSplash');
173   }
174   myGrav = myGravWater;
175   return false;
179 override bool onOutOfWater () {
180   myGrav = myGravNorm;
181   return false;
185 override bool onFellInLava (MapTile lava) {
186   if (heldBy) heldBy.holdItem = none;
187   hp = 0;
188   countsAsKill = false;
189   burning = 1;
190   myGrav = 0;
191   xVel = 0;
192   yVel = 0.1;
193   depth = 999;
194   return false;
198 // it is deeply inside a lava
199 override bool onDipInLava (MapTile lava) {
200   if (heldBy) heldBy.holdItem = none;
201   if (countsAsKill) level.addKill(objName);
202   instanceRemove();
203   return true;
207 override bool onFellOnSpikes (MapTile spikes) {
208   if (heldBy) heldBy.holdItem = none;
209   hp -= global.config.scumSpikeDamage;
210   spillBlood();
211   if (hp <= 0) {
212     hp = 0;
213     countsAsKill = false;
214     /+
215     switch (objType) {
216       case 'Caveman':
217       case 'ManTrap':
218       case 'Yeti':
219       case 'Hawkman':
220       case 'Shopkeeper':
221         status = DEAD;
222         break;
223     }
224     +/
225     myGrav = 0;
226     xVel = 0;
227     yVel = 0.2;
228     spikesRestoreGravity = true;
229   }
230   return false;
234 override bool onSacrificed (MapTile altar) {
235   if (heldBy) heldBy.holdItem = none;
236   level.performSacrifice(self, altar);
237   return true;
241 override void onStunnedHitEnemy (MapEnemy enemy) {
242   if (enemy isa EnemyMagmaMan) return;
243   if (enemy.status < STUNNED) enemy.xVel = xVel;
244   enemy.countsAsKill = true;
245   enemy.hp -= 1;
246   enemy.spillBlood();
247   if (enemy.canBeStunned) {
248     if (enemy isa MonsterDamsel) {
249       enemy.status = THROWN;
250       MonsterDamsel(enemy).calm = false;
251     } else {
252       enemy.status = STUNNED;
253     }
254     enemy.counter = 50;
255   }
256   //!enemy.origX = x;
257   //!enemy.origY = y;
258   playSound('sndHit');
259   //enemy.xVel = xVel*0.3; // was commented in the original
260   //!if (type == "Arrow" or type == "Fish Bone") instance_destroy();
264 // ////////////////////////////////////////////////////////////////////////// //
265 override bool onExplosionTouch (MapObject xplo) {
266   /+!
267   if (other.type == "Magma Man") {
268     with (other) {
269       if (not isRoom("rOlmec2")) {
270         flame = instance_create(x+8, y-4, oMagma);
271         flame.hp = 200;
272         flame.yVel = -rand(1, 3);
273         flame = instance_create(x+8, y-4, oMagma);
274         flame.hp = 200;
275         flame.yVel = -rand(1, 3);
276         instance_destroy();
277       }
278     }
279     return;
280   }
281   +/
283   /+!
284   if (other.type == "Blob") {
285     other.hp = 0;
286     scrCreateBloblets(x+sprite_width/2, y+sprite_height/2, 5);
287   }
288   +/
289   if (invincible) return false;
291   if (heldBy) heldBy.holdItem = none;
292   hp -= global.config.explosionDmg;
293   spillBlood();
294   if (xplo.fltx < fltx) xVel = global.randOther(4, 6); else xVel = -global.randOther(4, 6);
295   yVel = -6;
296   burning = 50;
297   if (hp <= 0 && !leavesBody) {
298     if (countsAsKill) level.addKill(objName);
299     instanceRemove();
300   }
301   return true;
305 // ////////////////////////////////////////////////////////////////////////// //
306 override void onBulletHit (ObjBullet bullet) {
307   if (heldBy) heldBy.holdItem = none;
309   xVel = bullet.xVel;
310   yVel = -4;
312   //writeln("'", objType, "': bullet hit before; hp=", hp, "; bloodless=", bloodless, "; bloodLeft=", bloodLeft);
313   hp -= bullet.damage;
314   spillBlood();
315   countsAsKill = true;
316   //writeln("'", objType, "': bullet hit after; hp=", hp, "; bloodless=", bloodless, "; bloodLeft=", bloodLeft);
318   /*
319   if ((type == "Caveman" or
320        type == "Yeti" or
321        type == "Hawkman" or
322        type == "Shopkeeper") and
323       status != 99)
324   {
325     status = 98;
326     counter = 20;
327   }
328   */
332 // ////////////////////////////////////////////////////////////////////////// //
333 override bool onSpearTrapHit (MapObject spear) {
334   if (heldBy) heldBy.holdItem = none;
335   countsAsKill = false;
336   spear.playSound('sndHit');
337   hp -= global.config.spearDmg;
338   spillBlood();
339   return false;
343 // ////////////////////////////////////////////////////////////////////////// //
344 override void fixHoldCoords () {
345   if (heldBy) {
346     xVel = 0;
347     yVel = 0;
348     int dx = 0;
349          if (heldBy.dir == Dir.Left) { dx = -12; dir = Dir.Left; }
350     else if (heldBy.dir == Dir.Right) { dx = -4; dir = Dir.Right; }
351     int dy = -(heldBy.status == DUCKING && fabs(heldBy.xVel) < 2 ? 10 : 12);
352     setXY(heldBy.fltx+dx, heldBy.flty+dy);
353     prevFltX = heldBy.prevFltX+dx;
354     prevFltY = heldBy.prevFltY+dy;
355     updateGrid();
356   }
360 override void thinkFrame () {
361   //if (self isa EnemyCaveman) writeln("caveman; hp=", hp);
362   //active = true;
363   if (invincible > 0) invincible -= 1;
365   if (hp <= 0 && !leavesBody) {
366     if (countsAsKill) level.addKill(objName);
367     instanceRemove();
368     return;
369   }
371   if (heldBy) {
372     if (!heldBy.holdItem || status < STUNNED) heldBy.holdItem = none;
373     if (heldBy) return;
374   }
376   /*
377   if (checkInsideBlock && checkInsideBlockOfsW > 0 && checkInsideBlockOfsH > 0) {
378     int x = ix, y = iy;
379     auto block = level.checkTilesInRect(x+checkInsideBlockOfsX, y+checkInsideBlockOfsY, checkInsideBlockOfsW, checkInsideBlockOfsH);
380     if (block && (onInsideBlock(block) || !isInstanceAlive)) return;
381   }
382   */
383   if (checkInsideBlock) {
384     auto block = level.checkTilesInRect(x0, y0, width, height);
385     if (block && (onInsideBlock(block) || !isInstanceAlive)) return;
386   }
388   auto spf = getSpriteFrame();
389   // water
390   if (allowWaterProcessing) {
391     //auto wtile = level.isWaterAtPoint(ix+spf.width/2, iy+spf.height/2/*, -1, -1*/);
392     auto wtile = level.isWaterAtPoint(ix+8, iy+8);
393     if (wtile) {
394       if (!swimming) {
395         swimming = true;
396         if (onFellInWater(wtile) || !isInstanceAlive) return;
397       }
398     } else {
399       if (swimming) {
400         swimming = false;
401         if (onOutOfWater() || !isInstanceAlive) return;
402       }
403     }
404   }
406   if (burning > 0) {
407     if (spf && global.randOther(1, 5) == 1) level.MakeMapObject(ix+global.randOther(0, spf.width), iy+global.randOther(0, spf.height), 'oBurn');
408     burning -= 1;
409   }
411   if (spf) {
412     // felt in lava?
413     auto lava = isCollisionAtPoint(ix+spf.width/2, iy-1, &level.cbCollisionLava);
414     if (lava && (onDipInLava(lava) || !isInstanceAlive)) return;
416     lava = isCollisionAtPoint(ix+spf.width/2, iy+spf.height-2, &level.cbCollisionLava);
417     if (lava && (onFellInLava(lava) || !isInstanceAlive)) return;
418   }
420   // spear
421   /+
422   if (collision_rectangle(x+2, y+2, x+14, y+14, oSpearsLeft, 0, 0)) {
423     trap = instance_nearest(x, y, oSpearsLeft);
424     if (trap.image_index &gt;= 20 and trap.image_index &lt; 24) {
425       if (type == "Caveman" or type == "ManTrap" or type == "Yeti" or type == "Hawkman" or type == "Shopkeeper") {
426         // if (status &lt; 98)
427         if (hp > 0) {
428           hp -= global.spearDmg;
429           countsAsKill = false;
430           status = 98;
431           counter = stunTime;
432           yVel = -6;
433           if (trap.x+8 &lt; x+8) xVel = 4;
434           else xVel = -4;
435           image_speed = 0.5;
436           playSound('sndHit');
437           if (!bloodless) scrCreateBlood(x+sprite_width/2, y+sprite_height/2, 2);
438         }
439       } else {
440         hp -= global.spearDmg;
441         countsAsKill = false;
442         playSound('sndHit');
443         if (!bloodless) scrCreateBlood(x+sprite_width/2, y+sprite_height/2, 1);
444       }
445     }
446   }
447   +/
449   // spikes
450   if (yVel > 2) {
451     auto spikes = isCollisionAtPoint(ix+8, iy+16, &level.cbCollisionSpikes);
452     if (spikes) {
453       //!?:spikes = instance_place(x+8, y+14, oSpikes);
454       if (!bloodless) spikes.makeBloody();
455       if (onFellOnSpikes(spikes) || !isInstanceAlive) return;
456     }
457   }
458   if (spikesRestoreGravity && fabs(yVel) < 0.01) myGrav = 0.6;
460   // sacrifice
461   if (status >= STUNNED) {
462     if (checkAndPerformSacrifice()) return;
463   } else {
464     sacCount = default.sacCount;
465   }
467   // moving projectile
468   if ((status == STUNNED || status == THROWN || status == DEAD || dead) && (fabs(xVel) > 2 || fabs(yVel) > 2) && !spectral) {
469     spectral = true;
470     level.isObjectInRect(x0, y0, width, height, delegate bool (MapObject o) {
471       if (o == self) return false;
472       if (!o.dead && !o.stunned && o.status < STUNNED && o.status != THROWN) {
473         if (!o.collidesWith(self)) return false;
474         onStunnedHitEnemy(MapEnemy(o));
475         return false;
476       }
477       return false;
478     }, precise:false, castClass:MapEnemy);
479     spectral = false;
480   }
482   if (!heldBy && doBasicPhysics && isInstanceAlive) basicPhysicsStep();
484   if (hp <= 0) {
485     if (!leavesBody) {
486       if (countsAsKill) level.addKill(objName);
487       instanceRemove();
488       return;
489     }
490   }
494 // ////////////////////////////////////////////////////////////////////////// //
495 // generic collision adjustments made by enemies
496 final void scrCheckCollisions () {
497   setCollisionBounds(2, 6, 14, 16);
499   bool colLeft = !!isCollisionLeft(1);
500   bool colRight = !!isCollisionRight(1);
501   bool colBot = !!isCollisionBottom(1);
502   bool colTop = !!isCollisionTop(1);
504   if (colLeft && !colRight) {
505     fltx = ix+1;
506   } else if (colRight) {
507     fltx = ix-1;
508   }
510   if (colLeft || colRight) xVel = -xVel*0.5;
512   if (colTop && !colBot) {
513     flty = iy+1;
514   } else if (colBot) {
515     // bounce
516          if (yVel > 1) yVel = -yVel*0.5;
517     else if (fabs(yVel) < 1) yVel = 0;
518     // friction
519          if (fabs(xVel) < 0.1) xVel = 0;
520     else if (fabs(xVel) != 0) xVel *= 0.3;
521   }
523   //k8
524   if (fabs(xVel) < 0.1) xVel = 0;
528 // ////////////////////////////////////////////////////////////////////////// //
529 defaultproperties {
530   depth = 60;
532   objType = 'oEnemy';
533   // added so enemies can be carried with same code as items
534   //held = false;
535   armed = false;
536   trigger = false;
537   safe = false;
538   sticky = false;
539   canPickUp = true;
540   cost = 0;
541   forSale = false;
542   //carries = "";
543   //holds = "";
544   favor = 1;
545   sacCount = 20;
546   invincible = 0;
548   activeWhenHeld = true;
549   spectralWhenHeld = false; // so they can work as meat shield
551   heavy = true;
553   myGrav = 0.6;
554   myGravNorm = 0.6;
555   myGravWater = 0.2;
556   yVelLimit = 10;
557   bounceFactor = 0.5;
558   frictionFactor = 0.3;
560   //shakeCounter = 0;
561   //shakeToggle = 0;
563   bloodless = false;
564   swimming = false;
565   flying = false;
566   inWeb = false;
567   countsAsKill = true; // sometimes it's not the player's fault!
568   removeCorpse = false; // only applies to enemies that have corpses (Caveman, Yeti, etc.)
569   meGoldMonkey = false;
571   bloodLeft = 4;
572   burning = 0;
573   sightCounter = 0;
574   stunTime = 200;
575   damage = 1; // damage amount caused to player on touch
577   deathTimer = 200; // how many steps after death until corpse is removed
579   fallTimer = 0;
580   stunTimer = 0;
581   wallHurt = 0;
582   //thrownBy = ""; // "Yeti", "Hawkman", or "Shopkeeper" for stat tracking deaths by being thrown
583   pushTimer = 0;
584   whoaTimer = 0;
585   //whoaTimerMax = 30;
586   distToNearestLightSource = 999;
588   // make it immune to whip on spawn
589   whipTimer = 15;
591   canBeNudged = false;
592   canBeHitByBullet = true;