thrown enemies now does damage
[k8vacspelynky.git] / mapent / enemies / mantrap.vc
blob0c233fa4d7faac68605abb663357ca9ceee3a22e
1 /**********************************************************************************
2  * Copyright (c) 2008, 2009 Derek Yu and Mossmouth, LLC
3  * Copyright (c) 2018, Ketmar Dark
4  *
5  * This file is part of Spelunky.
6  *
7  * You can redistribute and/or modify Spelunky, including its source code, under
8  * the terms of the Spelunky User License.
9  *
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.
12  *
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/>
16  *
17  **********************************************************************************/
18 class EnemyMantrap['oManTrap'] : MapEnemy;
20 bool ateShopkeeper;
21 bool ateRock;
22 bool veryHungry;
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) {
31     stunTime = 120;
32     if (global.randOther(1, 3) == 1) veryHungry = true;
33   }
34   if (!veryHungry && global.randOther(1, 100) == 1) veryHungry = true;
35   //veryHungry = true;
36   if (isCollisionRight(0)) {
37     if (!isCollisionLeft(1)) {
38       while (isCollisionRight(0)) fltx -= 1;
39     }
40   } else if (isCollisionLeft(0)) {
41     if (!isCollisionRight(1)) {
42       while (isCollisionLeft(0)) fltx += 1;
43     }
44   }
45   //dir = (level.player.ix < ix+8 ? Dir.Left : Dir.Right);
46   return true;
50 override void onAnimationLooped () {
51   if (status == EATING) {
52     setSprite('sManTrapSleepL');
53     imageSpeed = 0.5;
54     if (!ateRock) hp += 1;
55     status = SLEEPY;
56   } else if (spriteLName == 'sManTrapSleepL') {
57     setSprite('sManTrapStunL');
58     imageSpeed = 0.5;
59     status = STUNNED;
60     counter = stunTime*2;
61     if (ateRock) ateRock = false;
62   }
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;
76   int x = ix, y = iy;
78   if (abs(plr.ix-(x+8)) > 8) {
79     // do nothing
80   } else if ((global.hasSpikeShoes || status == EATING) && !plr.dead && !plr.stunned &&
81              (plr.status == JUMPING || plr.status == FALLING) && plr.iy < y+5 && !plr.swimming)
82   {
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);
88     } else {
89       hp -= trunc(1*(floor(plr.fallTimer/16.0)+1));
90     }
91     plr.fallTimer = 0;
92     countsAsKill = true;
93     status = STUNNED;
94     counter = stunTime;
95     yVel = -6;
96     if (plr.ix < x+8) xVel += 1; else xVel -= 1;
97     imageSpeed = 0.5;
98     playSound('sndHit');
99   } else if (plr.visible && plr.invincible == 0) {
100     if (status != STUNNED && status != EATING && status != SLEEPY) {
101       xVel = 0;
102       status = EATING;
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');
107       plr.visible = false;
108       plr.invincible = 9999;
109       plr.bounced = true;
110       plr.canDropStuff = false;
111       global.plife = -99;
112       playSound('sndDie');
113       //global.drawHUD = false; // in the original
114       if (global.plife <= 0 /*and isRealLevel()*/) level.addDeath(objName);
115       auto it = plr.holdItem;
116       if (it) {
117         plr.holdItem = none;
118         it.instanceRemove();
119       }
120     }
121   }
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);
133     countsAsKill = true;
134     status = STUNNED;
135     counter = stunTime;
136     yVel = -3;
137     xVel = (wobj.ix < ix+8 ? 2 : -2);
138     imageSpeed = 0.5;
139     level.MakeMapObject(ix+global.randOther(0, 16), iy-8+global.randOther(0, 16), 'oLeaf');
140     playSound('sndHit');
141   }
142   return true;
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) {
157     xVel = 0;
158     status = EATING;
159     dir = (o.ix > ix ? Dir.Right : Dir.Left);
160     setSprite('sManTrapEatRockL');
161     o.instanceRemove();
162     ateRock = true;
163     veryHungry = false;
164     return true;
165   }
166   return false;
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) {
174     xVel = 0;
175     status = EATING;
176     dir = (o.ix > ix ? Dir.Right : Dir.Left);
177     setSprite('sManTrapEatCavemanL');
178     o.instanceRemove();
179     return true;
180   }
181   if (o isa EnemyVampire) {
182     xVel = 0;
183     status = EATING;
184     dir = (o.ix > ix ? Dir.Right : Dir.Left);
185     setSprite('sManTrapEatVampireL');
186     o.instanceRemove();
187     return true;
188   }
189   if (o isa MonsterShopkeeper) {
190     xVel = 0;
191     status = EATING;
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;
196     // drop a gun
197     if (sc.hasGun) {
198       auto obj = level.MakeMapObject(o.ix+8, o.iy+8, (sc.hisGunIsAsh ? 'oAshShotgun' : 'oShotgun'));
199       if (obj) {
200         obj.yVel = global.randOther(4, 6);
201         obj.xVel = global.randOther(4, 6)*(o.xVel < 0 ? -1 : 1);
202       }
203       sc.hasGun = false;
204       sc.hisGunIsAsh = false;
205     }
206     o.instanceRemove();
207     return true;
208   }
209   return 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) {
217     xVel = 0;
218     status = EATING;
219     dir = (o.ix > ix ? Dir.Right : Dir.Left);
220     setSprite('sManTrapEatDamselL');
221     if (o.heldBy) o.heldBy.holdItem = none;
222     o.instanceRemove();
223     return true;
224   }
225   return false;
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);
237   return false;
241 // ////////////////////////////////////////////////////////////////////////// //
242 bool freeShopkeeper () {
243   if (ateShopkeeper) {
244     if (heldBy) heldBy.holdItem = none;
245     int x = ix, y = iy;
246     auto obj = MonsterShopkeeper(level.MakeMapObject(x, y, 'oShopkeeper'));
247     if (obj) {
248       obj.status = ATTACK;
249       obj.hasGun = false;
250     }
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');
254     instanceRemove();
255     return true;
256   }
257   return false;
261 override void thinkFrame () {
262   ::thinkFrame();
263   if (!isInstanceAlive) return;
265   //if (isCollision()) writeln("FUUUUU");
267   //writeln("*** MOVE 0: dir=", dir, "; xVel=", xVel, "; fltx=", fltx);
268   moveRel(xVel, yVel);
269   //writeln("*** MOVE 1: dir=", dir, "; xVel=", xVel, "; fltx=", fltx);
271   int x = ix, y = iy;
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);
283   if (hp < 1) {
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();
288     return;
289   }
291   if (isCollisionBottom(1) && status != STUNNED) yVel = 0;
293   if (status == IDLE) {
294     if (counter > 0) --counter;
295     if (counter == 0) {
296            if (colLeft) dir = Dir.Right;
297       else if (colRight) dir = Dir.Right;
298       else dir = global.randOther(0, 1);
299       status = WALK;
300       //writeln("*** WALK! dir=", dir);
301     }
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");
309       dir = Dir.Right;
310     } else if (dir == Dir.Right && !level.isSolidAtPoint(x+16, y) && !level.isSolidAtPoint(x+16, y+16)) {
311       //writeln("*** 001");
312       dir = Dir.Left;
313     }
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)))
317     {
318       //writeln("*** 002");
319       dir = (level.isSolidAtPoint(x-1, y) ? Dir.Right : Dir.Left);
320       xVel = 0;
321     } else if (dir == Dir.Left) {
322       xVel = -1;
323     } else {
324       xVel = 1;
325     }
327     if (global.randOther(1, 100) == 1) {
328       status = IDLE;
329       counter = global.randOther(20, 50);
330       xVel = 0;
331       //writeln("*** 003: counter=", counter);
332     } else {
333       //writeln("*** 004: dir=", dir, "; xVel=", xVel);
334     }
335   } else if (status == STUNNED) {
336     // xVel = 0; // in the original
337     //if (counter &gt; 0) counter -= 1; else { status = IDLE; counter = rand(20, 50); } // in the original
338     setSprite('sManTrapStunL');
340     if (colBot && !bounced) {
341       bounced = true;
342       if (!bloodless && bloodLeft > 0) scrCreateBlood(x+8, y+8, 1);
343     }
345     if (heldBy || colBot || inWeb) {
346       if (counter > 0) {
347         --counter;
348       } else {
349         // free shopkeeper
350         if (freeShopkeeper()) return;
351         if (hp > 0) {
352           status = IDLE;
353           counter = global.randOther(20, 50);
354           if (heldBy) {
355             auto owner = heldBy;
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;
360             flty = owner.iy-8;
361             x = ix;
362             y = iy;
363           }
364         }
365       }
366     }
367     if (spriteLName == 'sManTrapStunL') {
368       // YASM 1.8.1
369       if (counter > 0 && counter < 30) imageSpeed = 0.8;
370     } else {
371       imageSpeed = 0.5;
372     }
373   }
375   if (status >= STUNNED) scrCheckCollisions();
377   // friction
378   if (colBot) {
379          if (fabs(xVel) < 0.1) xVel = 0;
380     else if (fabs(xVel) != 0) xVel *= 0.3;
381   }
383   if (isCollision()) {
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');
387       instanceRemove();
388     } else {
389       flty -= 2;
390       y = iy;
391     }
392   }
394   // imageFrame is fractional
395   if (status == EATING && round(imageFrame) == 8) {
396     if (!ateRock) scrCreateBlood(x+8, y, 1);
397   }
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;
403     } else {
404       auto bone = level.MakeMapObject(x+14, y+4, 'oBone');
405       if (bone) bone.xVel = 2;
406     }
407   }
409   if (status < SLEEPY) {
410     setSprite('sManTrapLeft');
411     imageSpeed = 0.5;
412   }
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);
419     }, precise:false);
420   }
424 defaultproperties {
425   objName = 'ManTrap';
426   desc = "ManTrap";
427   desc2 = "This slow-moving carnivorous plant will swallow a man whole.";
428   status = IDLE;
430   xVel = 2.5;
431   imageSpeed = 0.5;
433   doBasicPhysics = false;
434   flying = false;
436   // stats
437   hp = 3;
438   invincible = 0;
439   favor = 2;
440   dir = Dir.Right;
442   ateShopkeeper = false;
443   ateRock = false;
445   bounced = false;
446   dead = false;
447   counter = 0;
448   inWeb = false;
449   veryHungry = false;
451   bloodless = false;
452   //bounced = false;
453   //dead = false;
454   //counter = 20;
455   countsAsKill = true;
456   leavesBody = true; // we will do our own death sequence
457   canBeStunned = true;
459   depth = 60;