1 /**********************************************************************************
2 * Copyright (c) 2008, 2009 Derek Yu and Mossmouth, LLC
3 * Copyright (c) 2010, Moloch
4 * Copyright (c) 2018, Ketmar Dark
6 * This file is part of Spelunky.
8 * You can redistribute and/or modify Spelunky, including its source code, under
9 * the terms of the Spelunky User License.
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.
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/>
18 **********************************************************************************/
20 class MapEnemy : MapObject;
22 bool doBasicPhysics = true;
23 bool liveInWater; // so they won't splash
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;
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) {
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;
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) {
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));
82 hp -= trunc(1*(floor(plr.fallTimer/16.0)+1));
86 plr.playSound('sndHit');
87 } else if (!plr.dead && plr.invincible == 0) {
90 if (plr.status != CLIMBING) {
91 plr.xVel = (plrx < x ? -6 : 6);
93 if (global.plife > 0) {
94 global.plife -= damage;
95 if (global.plife < 0) level.addDeath(objName);
97 //plr.spillBlood(); // snakes should do that, for example
98 plr.playSound('sndHurt');
101 return false; // don't skip thinker
105 // ////////////////////////////////////////////////////////////////////////// //
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;
117 if (fabs(item.xVel) < ItemSpeedToHitX && fabs(item.yVel) < ItemSpeedToHitY) return false;
118 //writeln("item velocity: (", item.xVel, ",", item.yVel, ")");
120 if (heldBy) heldBy.holdItem = none;
125 auto proj = ItemProjectile(item);
126 if (proj) countsAsKill = proj.launchedByPlayer;
128 if (!dead) spillBlood(amount:2);
131 item.playSound('sndHit');
132 if (status != STUNNED) xVel = xVel*0.3;
134 if (self isa MonsterDamsel) {
136 MonsterDamsel(self).calm = false;
137 counter = MonsterDamsel(self).stunMax;
143 return true; // it hit us
147 // ////////////////////////////////////////////////////////////////////////// //
148 // return `true` if this entity can be sacrificed
149 override bool canBeSacrificed (MapTile altar) {
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;
162 if (!bloodless) scrCreateBlood(ix+bloodOffsetX, iy+bloodOffsetY, bloodAmount);
163 if (countsAsKill) level.addKill(objName);
169 override bool onFellInWater (MapTile water) {
170 if (!liveInWater && !heldBy) {
171 level.MakeMapObject(xCenter, iy, 'oSplash');
172 playSound('sndSplash');
174 myGrav = myGravWater;
179 override bool onOutOfWater () {
185 override bool onFellInLava (MapTile lava) {
186 if (heldBy) heldBy.holdItem = none;
188 countsAsKill = 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);
207 override bool onFellOnSpikes (MapTile spikes) {
208 if (heldBy) heldBy.holdItem = none;
209 hp -= global.config.scumSpikeDamage;
213 countsAsKill = false;
228 spikesRestoreGravity = true;
234 override bool onSacrificed (MapTile altar) {
235 if (heldBy) heldBy.holdItem = none;
236 level.performSacrifice(self, altar);
241 override void onStunnedHitEnemy (MapEnemy enemy) {
242 if (enemy isa EnemyMagmaMan) return;
243 if (enemy.status < STUNNED) enemy.xVel = xVel;
244 enemy.countsAsKill = true;
247 if (enemy.canBeStunned) {
248 if (enemy isa MonsterDamsel) {
249 enemy.status = THROWN;
250 MonsterDamsel(enemy).calm = false;
252 enemy.status = STUNNED;
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) {
267 if (other.type == "Magma Man") {
269 if (not isRoom("rOlmec2")) {
270 flame = instance_create(x+8, y-4, oMagma);
272 flame.yVel = -rand(1, 3);
273 flame = instance_create(x+8, y-4, oMagma);
275 flame.yVel = -rand(1, 3);
284 if (other.type == "Blob") {
286 scrCreateBloblets(x+sprite_width/2, y+sprite_height/2, 5);
289 if (invincible) return false;
291 if (heldBy) heldBy.holdItem = none;
292 hp -= global.config.explosionDmg;
294 if (xplo.fltx < fltx) xVel = global.randOther(4, 6); else xVel = -global.randOther(4, 6);
297 if (hp <= 0 && !leavesBody) {
298 if (countsAsKill) level.addKill(objName);
305 // ////////////////////////////////////////////////////////////////////////// //
306 override void onBulletHit (ObjBullet bullet) {
307 if (heldBy) heldBy.holdItem = none;
312 //writeln("'", objType, "': bullet hit before; hp=", hp, "; bloodless=", bloodless, "; bloodLeft=", bloodLeft);
316 //writeln("'", objType, "': bullet hit after; hp=", hp, "; bloodless=", bloodless, "; bloodLeft=", bloodLeft);
319 if ((type == "Caveman" or
322 type == "Shopkeeper") and
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;
343 // ////////////////////////////////////////////////////////////////////////// //
344 override void fixHoldCoords () {
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;
360 override void thinkFrame () {
361 //if (self isa EnemyCaveman) writeln("caveman; hp=", hp);
363 if (invincible > 0) invincible -= 1;
365 if (hp <= 0 && !leavesBody) {
366 if (countsAsKill) level.addKill(objName);
372 if (!heldBy.holdItem || status < STUNNED) heldBy.holdItem = none;
377 if (checkInsideBlock && checkInsideBlockOfsW > 0 && checkInsideBlockOfsH > 0) {
379 auto block = level.checkTilesInRect(x+checkInsideBlockOfsX, y+checkInsideBlockOfsY, checkInsideBlockOfsW, checkInsideBlockOfsH);
380 if (block && (onInsideBlock(block) || !isInstanceAlive)) return;
383 if (checkInsideBlock) {
384 auto block = level.checkTilesInRect(x0, y0, width, height);
385 if (block && (onInsideBlock(block) || !isInstanceAlive)) return;
388 auto spf = getSpriteFrame();
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);
396 if (onFellInWater(wtile) || !isInstanceAlive) return;
401 if (onOutOfWater() || !isInstanceAlive) return;
407 if (spf && global.randOther(1, 5) == 1) level.MakeMapObject(ix+global.randOther(0, spf.width), iy+global.randOther(0, spf.height), 'oBurn');
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;
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 >= 20 and trap.image_index < 24) {
425 if (type == "Caveman" or type == "ManTrap" or type == "Yeti" or type == "Hawkman" or type == "Shopkeeper") {
426 // if (status < 98)
428 hp -= global.spearDmg;
429 countsAsKill = false;
433 if (trap.x+8 < x+8) xVel = 4;
437 if (!bloodless) scrCreateBlood(x+sprite_width/2, y+sprite_height/2, 2);
440 hp -= global.spearDmg;
441 countsAsKill = false;
443 if (!bloodless) scrCreateBlood(x+sprite_width/2, y+sprite_height/2, 1);
451 auto spikes = isCollisionAtPoint(ix+8, iy+16, &level.cbCollisionSpikes);
453 //!?:spikes = instance_place(x+8, y+14, oSpikes);
454 if (!bloodless) spikes.makeBloody();
455 if (onFellOnSpikes(spikes) || !isInstanceAlive) return;
458 if (spikesRestoreGravity && fabs(yVel) < 0.01) myGrav = 0.6;
461 if (status >= STUNNED) {
462 if (checkAndPerformSacrifice()) return;
464 sacCount = default.sacCount;
468 if ((status == STUNNED || status == THROWN || status == DEAD || dead) && (fabs(xVel) > 2 || fabs(yVel) > 2) && !spectral) {
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));
478 }, precise:false, castClass:MapEnemy);
482 if (!heldBy && doBasicPhysics && isInstanceAlive) basicPhysicsStep();
486 if (countsAsKill) level.addKill(objName);
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) {
506 } else if (colRight) {
510 if (colLeft || colRight) xVel = -xVel*0.5;
512 if (colTop && !colBot) {
516 if (yVel > 1) yVel = -yVel*0.5;
517 else if (fabs(yVel) < 1) yVel = 0;
519 if (fabs(xVel) < 0.1) xVel = 0;
520 else if (fabs(xVel) != 0) xVel *= 0.3;
524 if (fabs(xVel) < 0.1) xVel = 0;
528 // ////////////////////////////////////////////////////////////////////////// //
533 // added so enemies can be carried with same code as items
548 activeWhenHeld = true;
549 spectralWhenHeld = false; // so they can work as meat shield
558 frictionFactor = 0.3;
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;
575 damage = 1; // damage amount caused to player on touch
577 deathTimer = 200; // how many steps after death until corpse is removed
582 //thrownBy = ""; // "Yeti", "Hawkman", or "Shopkeeper" for stat tracking deaths by being thrown
586 distToNearestLightSource = 999;
588 // make it immune to whip on spawn
592 canBeHitByBullet = true;