1 /**********************************************************************************
2 * Copyright (c) 2008, 2009 Derek Yu and Mossmouth, LLC
3 * Copyright (c) 2018, Ketmar Dark
5 * This file is part of Spelunky.
7 * You can redistribute and/or modify Spelunky, including its source code, under
8 * the terms of the Spelunky User License.
10 * Spelunky is distributed in the hope that it will be entertaining and useful,
11 * but WITHOUT WARRANTY. Please see the Spelunky User License for more details.
13 * The Spelunky User License should be available in "Game Information", which
14 * can be found in the Resource Explorer, or as an external file called COPYING.
15 * If not, please obtain a new copy of Spelunky from <http://spelunkyworld.com/>
17 **********************************************************************************/
18 class EnemyMantrap['oManTrap'] : MapEnemy;
25 override bool initialize () {
26 if (!::initialize()) return false;
27 setSprite('sManTrapLeft');
28 auto spf = getSpriteFrame();
29 setCollisionBounds(2, 0, spf.width-3, spf.height);
30 if (global.config.bizarre) {
32 if (global.randOther(1, 3) == 1) veryHungry = true;
34 if (!veryHungry && global.randOther(1, 100) == 1) veryHungry = true;
36 if (isCollisionRight(0)) {
37 if (!isCollisionLeft(1)) {
38 while (isCollisionRight(0)) fltx -= 1;
40 } else if (isCollisionLeft(0)) {
41 if (!isCollisionRight(1)) {
42 while (isCollisionLeft(0)) fltx += 1;
45 //dir = (level.player.ix < ix+8 ? Dir.Left : Dir.Right);
50 override void onAnimationLooped () {
51 if (status == EATING) {
52 setSprite('sManTrapSleepL');
54 if (!ateRock) hp += 1;
56 } else if (spriteLName == 'sManTrapSleepL') {
57 setSprite('sManTrapStunL');
61 if (ateRock) ateRock = false;
66 // ////////////////////////////////////////////////////////////////////////// //
67 // for dead players too
68 // first, the code will call `onObjectTouched()` for player
69 // if it returned `false`, the code will call this handler
70 // note that player's handler is called *after* its frame thinker,
71 // but object handler is called *before* frame thinker for the object
72 // return `true` to skip thinker on this frame
73 override bool onTouchedByPlayer (PlayerPawn plr) {
74 if (plr.dead || status >= STUNNED || dead) return false;
78 if (abs(plr.ix-(x+8)) > 8) {
80 } else if ((global.hasSpikeShoes || status == EATING) && !plr.dead && !plr.stunned &&
81 (plr.status == JUMPING || plr.status == FALLING) && plr.iy < y+5 && !plr.swimming)
84 plr.yVel = -6-0.2*plr.yVel;
85 if (global.hasSpikeShoes) {
86 hp -= trunc(3*(floor(plr.fallTimer/16.0)+1));
87 if (!bloodless) scrCreateBlood(plr.ix, plr.iy+8, 1);
89 hp -= trunc(1*(floor(plr.fallTimer/16.0)+1));
96 if (plr.ix < x+8) xVel += 1; else xVel -= 1;
99 } else if (plr.visible && plr.invincible == 0) {
100 if (status != STUNNED && status != EATING && status != SLEEPY) {
103 dir = (plr.ix > x+8 ? Dir.Right : Dir.Left);
104 if (global.isDamsel) setSprite('sManTrapEatDamselL');
105 else if (global.isTunnelMan) setSprite('sManTrapEatTunnelL');
106 else setSprite('sManTrapEatL');
108 plr.invincible = 9999;
110 plr.canDropStuff = false;
113 //global.drawHUD = false; // in the original
114 if (global.plife <= 0 /*and isRealLevel()*/) level.addDeath(objName);
115 auto it = plr.holdItem;
123 return false; // don't skip thinker
127 // ////////////////////////////////////////////////////////////////////////// //
128 // return `false` to do standard weapon processing
129 override bool onTouchedByPlayerWeapon (PlayerPawn plr, PlayerWeapon wobj) {
130 wobj.hitEnemy = true;
131 if (status != STUNNED && global.plife > 0) {
132 hp -= wobj.damage*(wobj.slashing ? 2 : 1);
137 xVel = (wobj.ix < ix+8 ? 2 : -2);
139 level.MakeMapObject(ix+global.randOther(0, 16), iy-8+global.randOther(0, 16), 'oLeaf');
146 // ////////////////////////////////////////////////////////////////////////// //
147 override bool onItemHit (MapItem o) {
148 if (tryEatItem(o)) return true;
149 return ::onItemHit(o);
153 // return `true` to stop processing
154 bool tryEatItem (MapItem o) {
155 if (status == STUNNED || status == EATING || status == SLEEPY) return false;
156 if (o isa ItemRock && /*global.config.bizarre &&*/ veryHungry) {
159 dir = (o.ix > ix ? Dir.Right : Dir.Left);
160 setSprite('sManTrapEatRockL');
170 // return `true` to stop processing
171 bool tryEatEnemy (MapEnemy o) {
172 if (status == STUNNED || status == EATING || status == SLEEPY) return false;
173 if (o isa EnemyCaveman) {
176 dir = (o.ix > ix ? Dir.Right : Dir.Left);
177 setSprite('sManTrapEatCavemanL');
181 if (o isa EnemyVampire) {
184 dir = (o.ix > ix ? Dir.Right : Dir.Left);
185 setSprite('sManTrapEatVampireL');
189 if (o isa MonsterShopkeeper) {
192 dir = (o.ix > ix ? Dir.Right : Dir.Left);
193 setSprite('sManTrapEatShopkeeperL');
194 auto sc = MonsterShopkeeper(o);
195 if (o.hp > 0 && !sc.outlaw) ateShopkeeper = true;
198 auto obj = level.MakeMapObject(o.ix+8, o.iy+8, (sc.hisGunIsAsh ? 'oAshShotgun' : 'oShotgun'));
200 obj.yVel = global.randOther(4, 6);
201 obj.xVel = global.randOther(4, 6)*(o.xVel < 0 ? -1 : 1);
204 sc.hisGunIsAsh = false;
213 // return `true` to stop processing
214 bool tryEatCharacter (ObjCharacter o) {
215 if (status == STUNNED || status == EATING || status == SLEEPY) return false;
216 if (o isa MonsterDamsel) {
219 dir = (o.ix > ix ? Dir.Right : Dir.Left);
220 setSprite('sManTrapEatDamselL');
221 if (o.heldBy) o.heldBy.holdItem = none;
229 // return `true` to stop processing
230 bool tryEatObject (MapObject o) {
231 auto item = MapItem(o);
232 if (item) return tryEatItem(item);
233 auto enemy = MapEnemy(o);
234 if (enemy) return tryEatEnemy(enemy);
235 auto chr = ObjCharacter(o);
236 if (chr) return tryEatCharacter(chr);
241 // ////////////////////////////////////////////////////////////////////////// //
242 bool freeShopkeeper () {
244 if (heldBy) heldBy.holdItem = none;
246 auto obj = MonsterShopkeeper(level.MakeMapObject(x, y, 'oShopkeeper'));
251 foreach (; 0..3) level.MakeMapObject(x+global.randOther(0, 16), y-8+global.randOther(0, 16), 'oLeaf');
252 if (!bloodless && bloodLeft > 0) scrCreateBlood(x+8, y+8, 1);
253 playSound('sndSmallExplode');
261 override void thinkFrame () {
263 if (!isInstanceAlive) return;
265 //if (isCollision()) writeln("FUUUUU");
267 //writeln("*** MOVE 0: dir=", dir, "; xVel=", xVel, "; fltx=", fltx);
269 //writeln("*** MOVE 1: dir=", dir, "; xVel=", xVel, "; fltx=", fltx);
273 if (!heldBy) yVel += myGrav;
274 yVel = fmin(yVel, yVelLimit);
276 bool colLeft = !!isCollisionLeft(1);
277 bool colRight = !!isCollisionRight(1);
278 bool colBot = !!isCollisionBottom(1);
279 bool colTop = !!isCollisionTop(1);
281 inWeb = !!level.isObjectInRect(ix, iy, 17, 17, &level.cbIsObjectWeb);
284 if (countsAsKill) level.addKill(objName);
285 foreach (; 0..3) level.MakeMapObject(x+global.randOther(0, 16), y-8+global.randOther(0, 16), 'oLeaf');
286 if (!bloodless && bloodLeft > 0) scrCreateBlood(x+8, y+8, 1);
287 if (!freeShopkeeper()) instanceRemove();
291 if (isCollisionBottom(1) && status != STUNNED) yVel = 0;
293 if (status == IDLE) {
294 if (counter > 0) --counter;
296 if (colLeft) dir = Dir.Right;
297 else if (colRight) dir = Dir.Right;
298 else dir = global.randOther(0, 1);
300 //writeln("*** WALK! dir=", dir);
302 } else if (status == WALK) {
303 if (colLeft/*isCollisionLeft(1)*/) dir = Dir.Right;
304 else if (colRight/*isCollisionRight(1)*/) dir = Dir.Left;
305 //if (colLeft || colRight) writeln("COL: left=", colLeft, "; right=", colRight, "; xVel=", xVel);
307 if (dir == Dir.Left && !level.isSolidAtPoint(x-1, y) && !level.isSolidAtPoint(x-1, y+16)) {
308 //writeln("*** 000");
310 } else if (dir == Dir.Right && !level.isSolidAtPoint(x+16, y) && !level.isSolidAtPoint(x+16, y+16)) {
311 //writeln("*** 001");
315 if ((!level.isSolidAtPoint(x-1, y+16) || level.isSolidAtPoint(x-1, y)) &&
316 (!level.isSolidAtPoint(x+16, y+16) || level.isSolidAtPoint(x+16, y)))
318 //writeln("*** 002");
319 dir = (level.isSolidAtPoint(x-1, y) ? Dir.Right : Dir.Left);
321 } else if (dir == Dir.Left) {
327 if (global.randOther(1, 100) == 1) {
329 counter = global.randOther(20, 50);
331 //writeln("*** 003: counter=", counter);
333 //writeln("*** 004: dir=", dir, "; xVel=", xVel);
335 } else if (status == STUNNED) {
336 // xVel = 0; // in the original
337 //if (counter > 0) counter -= 1; else { status = IDLE; counter = rand(20, 50); } // in the original
338 setSprite('sManTrapStunL');
340 if (colBot && !bounced) {
342 if (!bloodless && bloodLeft > 0) scrCreateBlood(x+8, y+8, 1);
345 if (heldBy || colBot || inWeb) {
350 if (freeShopkeeper()) return;
353 counter = global.randOther(20, 50);
356 heldBy.holdItem = none;
357 // trap can get stuck in wall at this point:
358 if (level.isSolidAtPoint(ix+16, iy+8)) fltx = owner.ix-12;
359 else if (level.isSolidAtPoint(ix, iy+8)) fltx = owner.ix-4;
367 if (spriteLName == 'sManTrapStunL') {
369 if (counter > 0 && counter < 30) imageSpeed = 0.8;
375 if (status >= STUNNED) scrCheckCollisions();
379 if (fabs(xVel) < 0.1) xVel = 0;
380 else if (fabs(xVel) != 0) xVel *= 0.3;
384 // YASM 1.8: added boulder
385 if (!heldBy && level.checkTileAtPoint(x+8, y+8, delegate bool (MapTile t) { return (t isa ObjBoulder); })) {
386 foreach (; 0..3) level.MakeMapObject(x+global.randOther(0, 16), y-8+global.randOther(0, 16), 'oLeaf');
394 // imageFrame is fractional
395 if (status == EATING && round(imageFrame) == 8) {
396 if (!ateRock) scrCreateBlood(x+8, y, 1);
399 if (status == SLEEPY && round(imageFrame) == 6 && global.randOther(1, 8) == 1) {
400 if (dir == Dir.Left) {
401 auto bone = level.MakeMapObject(x+2, y+4, 'oBone');
402 if (bone) bone.xVel = -2;
404 auto bone = level.MakeMapObject(x+14, y+4, 'oBone');
405 if (bone) bone.xVel = 2;
409 if (status < SLEEPY) {
410 setSprite('sManTrapLeft');
414 // check for collisions
415 if (status != STUNNED && status != EATING && status != SLEEPY) {
416 level.isObjectInRect(x0, y0, width, height, delegate bool (MapObject o) {
417 if (o.spectral || !o.collidesWith(self)) return false;
418 return tryEatObject(o);
427 desc2 = "This slow-moving carnivorous plant will swallow a man whole.";
433 doBasicPhysics = false;
442 ateShopkeeper = false;
456 leavesBody = true; // we will do our own death sequence