mattock fixes: web blocks brick crushing; don't make hit sound if mattock hits no...
[k8vacspelynky.git] / mapent / MapItem.vc
blob5f4a592e47d70d38dcab40c92bb1d5cb0a74545e
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 MapItem : MapObject;
21 bool breakPieces = true;
22 bool dropContents = true;
23 name contents; // to use in `level.MakeMapObject()`
24 int contOfsX, contOfsY;
25 int life;
27 bool breaksOnCollision = false; // jars and skulls will do, rocks will not
28 bool canHitEnemies = false;
29 bool collBroken;
31 float breakYVUp = -3, breakXV = 4, breakYV = 4;
33 int holdXOfs, holdYOfs;
35 int forSaleFrame;
38 override bool initialize () {
39   if (!::initialize()) return false;
40   forSaleFrame = global.randOther(0, 9);
41   return true;
46 final void setCollisionBoundsKill (int hx0, int hy0, int hx1, int hy1) {
47   setCollisionBounds(hx0, hy0, hx1, hy1);
48   enemyColX = hitboxX;
49   enemyColY = hitboxY;
50   enemyColW = hitboxW;
51   enemyColH = hitboxH;
56 override void onDestroy () {
57   if (dropContents && contents) {
58     //level.MakeMapObjectByClass(contents, contOfsX, contOfsY);
59     auto obj = level.MakeMapObject(ix+contOfsX, iy+contOfsY, contents);
60     spectral = true; // just in case
61     //obj.active = false;
62     if (obj && obj.isCollision()) {
63       writeln("***STUCK! (", obj.objType, ")");
64       // unstuck it
65       auto ox = obj.fltx, oy = obj.flty;
66       bool didit = false;
67       int ymove = 0;
68       if (!obj.isCollisionBottom(1)) {
69         writeln(" UNSTUCK: go bottom!");
70         ymove = 1;
71       } else if (!obj.isCollisionTop(1)) {
72         writeln(" UNSTUCK: go top!");
73         ymove = -1;
74       }
75       if (ymove != 0) {
76         int xmove = (obj.isCollisionLeft(1) ? 1 : obj.isCollisionRight(1) ? -1 : 0);
77         //if (!xmove) xmove = (obj.isCollisionLeft(1) ? 1 : obj.isCollisionRight(1) ? -1 : 0);
78         //writeln(" xmove=", xmove);
79         foreach (int dy; 0..9*3) {
80           foreach (int dx; 0..9*3) {
81             obj.fltx = ox+dx*xmove;
82             obj.flty = oy+dy*ymove;
83             if (!obj.isCollision()) {
84               //writeln("***UNSTUCK! dy=", dy);
85               didit = true;
86               break;
87             }
88           }
89           if (didit) break;
90         }
91       } else {
92         writeln(" UNSTUCK: horizontal");
93         foreach (int dy; 0..9*3) {
94           foreach (int dx; 0..9*3) {
95             obj.fltx = ox+dx;
96             obj.flty = oy+dy;
97             if (!obj.isCollision()) { didit = true; break; }
98             obj.fltx = ox-dx;
99             obj.flty = oy+dy;
100             if (!obj.isCollision()) { didit = true; break; }
101             obj.fltx = ox+dx;
102             obj.flty = oy-dy;
103             if (!obj.isCollision()) { didit = true; break; }
104             obj.fltx = ox-dx;
105             obj.flty = oy-dy;
106             if (!obj.isCollision()) { didit = true; break; }
107           }
108           if (didit) break;
109         }
110       }
111       //obj.active = false;
112       if (!didit) {
113         obj.fltx = ox;
114         obj.flty = oy;
115       }
116       obj.saveInterpData();
117       obj.updateGrid();
118     }
119   }
120   if (collBroken) {
121     playSound('sndBreak');
122     level.MakeMapObject(ix, iy, 'oSmokePuff');
123     bool colTop = !!isCollisionTop(1);
124     bool colLeft = !!isCollisionLeft(1);
125     bool colRight = !!isCollisionRight(1);
126     //bool colBot = !!isCollisionBottom(1);
127     if (breakPieces) {
128       foreach (; 0..3) {
129         auto piece = level.MakeMapObject(ix-2, iy-2, 'oRubbleSmall');
130         if (piece) {
131                if (colLeft) piece.xVel = global.randOther(1, 3);
132           else if (colRight) piece.xVel = -global.randOther(1, 3);
133           else piece.xVel = global.randOther(1, 3)-global.randOther(1, 3);
134           if (colTop) piece.yVel = global.randOther(0, 3); else piece.yVel = -global.randOther(0, 3);
135         }
136       }
137     }
138   }
139   ::onDestroy();
143 // ////////////////////////////////////////////////////////////////////////// //
144 void drawSignsWithOfs (int xpos, int ypos, int scale, float currFrameDelta) {
145   if (!forSale && !sellOfferDone) return;
146   if (!cost) return;
148   int xi, yi;
149   getInterpCoords(currFrameDelta, scale, out xi, out yi);
150   auto spr = level.sprStore[sellOfferDone ? 'sSmallCollectGreen' : 'sSmallCollect']; //sSmallCollectGreen for resale
151   if (spr && spr.frames.length) {
152     forSaleFrame %= spr.frames.length;
153     auto spf = spr.frames[forSaleFrame];
154     spf.tex.blitAt(xi-xpos-spf.xofs*scale, yi-ypos-(12+spf.yofs)*scale, scale);
155   }
159 override void drawWithOfs (int xpos, int ypos, int scale, float currFrameDelta) {
160   ::drawWithOfs(xpos, ypos, scale, currFrameDelta);
161   drawSignsWithOfs(xpos, ypos, scale, currFrameDelta);
165 // ////////////////////////////////////////////////////////////////////////// //
166 override void processAlarms () {
167   ::processAlarms();
168   if (forSale || sellOfferDone) {
169     if (++forSaleFrame < 0) forSaleFrame = 0;
170     onCheckItemStolen(level.player);
171   }
175 // ////////////////////////////////////////////////////////////////////////// //
176 override bool onCanBePickedUp (PlayerPawn plr) {
177   return true;
181 // ////////////////////////////////////////////////////////////////////////// //
182 protected transient bool onExploAffected = true;
184 override bool onExplosionTouch (MapObject xplo) {
185   if (invincible) return false;
187   if (heldBy) {
188     auto plr = PlayerPawn(heldBy);
189     if (plr) plr.scrDropItem(LostCause.Throw, 0, 0); else heldBy.holdItem = none;
190     // drop item from pocket
191     if (plr) {
192       plr.scrSwitchToPocketItem(forceIfEmpty:false);
193       auto hi = plr.holdItem;
194       plr.scrDropItem(LostCause.Throw, 0, 0);
195       if (hi) hi.onExplosionTouch(xplo);
196     }
197   }
199   if (onExploAffected) {
200     if (breaksOnCollision) {
201       collBroken = true;
202       instanceRemove();
203       return true; // stop it, we are dead anyway
204     }
205     if (flty < xplo.flty) yVel -= 6; else yVel += 6;
206     if (xplo.fltx > fltx) xVel -= global.randOther(4, 6); else xVel += global.randOther(4, 6);
207   }
209   /+
210   if (other.type == "Arrow" or other.type == "Fish Bone" or other.type == "Jar" or other.type == "Skull") {
211     with (other) instance_destroy();
212   } else if (other.type == "Bomb") {
213     with (other) {
214       sprite_index = sBombArmed;
215       image_speed = 1;
216       alarm[1] = rand(4, 8);
217       enemyID = 0;
218     }
219     if (other.y < y) other.yVel = -rand(2, 4);
220     if (other.x < x) other.xVel = -rand(2, 4); else other.xVel = rand(2, 4);
221   } else if (other.type == "Rope") {
222     if (not other.falling) {
223       if (other.y < y) other.yVel -= 6; else other.yVel += 6;
224       if (x > other.x) other.xVel -= rand(4, 6); else other.xVel += rand(4, 6);
225     }
226   } else {
227     if (other.y < y) other.yVel -= 6; else other.yVel += 6;
228     if (x > other.x) other.xVel -= rand(4, 6); else other.xVel += rand(4, 6);
229   }
230   +/
232   return true;
236 // ////////////////////////////////////////////////////////////////////////// //
237 //override bool onTouchedByPlayer (PlayerPawn plr)
239 bool doPlayerColAction (PlayerPawn plr) {
240   //if (safe) return false;
242   if (collision_rectangle(x-8, y-8, x+8, y+8, oRock, 0, 0)) {
243     obj = instance_nearest(x, y, oRock);
244   }
246   /*
247   if (enemyColW < 1 || enemyColH < 1 || self isa ItemBomb) return false;
248   if (fabs(xVel) > 2 || fabs(yVel) > 2) {
249     if (!isInstanceAlive) return false; // stop it, we are dead anyway
250     if (heldBy) return false;
251     if (plr.dead || plr.invincible || plr.status == STUNNED || plr.stunned) return false;
252     global.plife -= 1;
253     plr.CreateBlood(plr.ix, plr.iy, 1);
254     plr.stunned = true;
255     plr.stunTimer = 120; // 200?
256     plr.yVel = -6;
257     plr.playSound('sndHit');
258     plr.xVel *= 0.3;
259     if (breaksOnCollision) {
260       collBroken = true;
261       instanceRemove();
262     }
263   }
264   */
266   if (canPickUp && global.hasMitt && !plr.holdItem && (fabs(xVel) > 4 || fabs(yVel) >= 6) &&
267       !safe && !plr.stunned && !plr.dead)
268   {
269     // catch ya!
270     plr.holdItem = self;
271     return true; // no more actions
272   }
275   return false; // go on
279 // ////////////////////////////////////////////////////////////////////////// //
280 // virtual
281 // return `true` to skip normal item processing
282 // if skipped, engine will call `onAfterSomethingHit()`
283 bool onEnemyHit (MapEnemy e) {
284   /+
285   if (enemy.status != STUNNED) enemy.xVel = xVel*0.3; //k8: *0.3 is mine
286   switch (enemy.objName) {
287     case 'Caveman':
288     case 'ManTrap':
289     case 'Yeti':
290     case 'Hawkman':
291     case 'Shopkeeper':
292       if (enemy.status != STUNNED) {
293         switch (enemy.objName) {
294           case 'Caveman':
295           case 'Yeti':
296           case 'Hawkman':
297           case 'Shopkeeper':
298             level.MakeMapObject(enemy.ix, enemy.iy, 'oBlood');
299             break;
300         }
301         enemy.status = STUNNED;
302         enemy.counter = stunTime;
303         enemy.yVel = -6;
304         enemy.hp -= damage;
305       }
306       break;
307     /*
308     case 'oDamsel':
309       level.MakeMapObject(enemy.ix, enemy.iy, 'oBlood');
310       // drop damsel
311       if (enemy.heldBy) enemy.heldBy.holdItem = none;
312       enemy.hp -= damage;
313       enemy.yVel = -6;
314       enemy.status = 2; //???
315       enemy.counter = 120;
316       enemy.damselDropped = true;
317       enemy.xVel = xVel*0.3;
318       break;
319     */
320     default:
321       level.MakeMapObject(enemy.ix+8, enemy.iy+8, 'oBlood');
322       enemy.hp -= damage;
323       //!enemy.origX = enemy.x;
324       //!enemy.origY = enemy.y;
325       enemy.shakeCounter = 10;
326       break;
327   }
328   +/
330   /+
331   playSound('sndHit');
332   if (enemy.status != STUNNED) enemy.xVel = xVel*0.3;
333   //if (objType == 'Arrow' || objType == 'Fish Bone') instance_destroy();
334   +/
336   return false;
340 // return `false` to do standard weapon processing
341 override bool onTouchedByPlayerWeapon (PlayerPawn plr, PlayerWeapon wpn) {
342   if (!wpn.prestrike && breaksOnCollision) {
343     wpn.hitEnemy = true;
344     dropContents = true;
345     collBroken = true;
346     instanceRemove();
347     return true;
348   }
349   return false;
353 override void onBulletHit (ObjBullet bullet) {
354   if (breaksOnCollision) {
355     dropContents = true;
356     collBroken = true;
357     instanceRemove();
358   }
362 // virtual
363 // return `true` to stop further processing
364 bool onAfterSomethingHit () {
365   if (breaksOnCollision) {
366     collBroken = true;
367     instanceRemove();
368     return true; // stop it, we are dead anyway
369   }
370   collBroken = false;
371   //return true;
372   return false;
376 transient bool wasObjectCollision;
378 bool doObjectColAction (MapObject o) {
379   if (!isInstanceAlive) return true; // stop it, we are dead anyway
381   collBroken = true;
383   MapEnemy enemy = MapEnemy(o);
384   if (enemy) {
385     // our handler
386     if (enemy.isInstanceAlive && onEnemyHit(enemy)) {
387       wasObjectCollision = true;
388       if (onAfterSomethingHit()) return true; // anyway
389       return !isInstanceAlive;
390     }
391     // their handler
392     if (enemy.onHitByItem(self)) {
393       wasObjectCollision = true;
394       if (onAfterSomethingHit()) return true; // anyway
395       return !isInstanceAlive;
396     }
397     return !isInstanceAlive;
398   }
400   collBroken = false;
402   return !isInstanceAlive;
406 override void fixHoldCoords () {
407   if (heldBy) {
408     xVel = 0;
409     yVel = 0;
410     xAcc = 0;
411     yAcc = 0;
412     imageAngle = 0;
413     int dx = (heldBy.dir == Dir.Left ? -4 : 4);
414     int dy = ((heldBy.status == DUCKING || heldBy.status == STUNNED || heldBy.stunned) && fabs(heldBy.xVel) < 2 ? 4 : 0);
415     dx += holdXOfs;
416     dy += holdYOfs;
417     setXY(heldBy.fltx+dx, heldBy.flty+dy);
418     prevFltX = heldBy.prevFltX+dx;
419     prevFltY = heldBy.prevFltY+dy;
420     updateGrid();
421     if (spriteRName) dir = heldBy.dir;
422   }
426 // Nudge with melee weapon
427 override void nudgeIt (int nx, int ny, optional bool forced) {
428   if (heldBy) return;
429   if (level.isSolidAtPoint(ix, iy)) return;
430   if (!forced && !global.config.nudge) return;
431   if (nudged) return;
433   if (forSale || /*forVending ||*/ trigger) {
434     if (!trigger) yVel = -1;
435   } else {
436     if (heavy) {
437       yVel -= 1;
438            if (nx < ix) xVel += global.randOther(5, 8)*0.1;
439       else if (nx > ix) xVel -= global.randOther(5, 8)*0.1;
440     } else if (self isa ItemProjectileArrow && fabs(xVel) > 0) {
441              if (fabs(xVel) < 4) xVel = -xVel;
442         else if (xVel < 0) xVel = global.randOther(3, 5);
443         else if (xVel > 0) xVel = -global.randOther(3, 5);
444         yVel = -yVel;
445     } else if (!stuck && !sticky) {
446       yVel -= (global.randOther(0, 1) ? 2.0 : 1.5);
447            if (nx < ix) xVel += global.randOther(10, 15)*0.1;
448       else if (nx > ix) xVel -= global.randOther(10, 15)*0.1;
449       /*
450       if (type == "Basketball") {
451         if (abs(yVel) < 4) yVel -= 5;
452         xVel = xVel*4;
453       }
454       */
455     }
456   }
458   nudged = true;
459   alarmNudge = 10;
463 void onCheckItemStolen (PlayerPawn plr) {
464   // check if it is stolen
465   if (forSale && cost > 0 && !level.isInShop(ix/16, iy/16)) {
466     level.scrShopkeeperAnger(GameLevel::SCAnger.ItemStolen);
467   }
471 override bool onFellInWater (MapTile water) {
472   level.MakeMapObject(xCenter, iy, 'oSplash');
473   playSound('sndSplash');
474   //!myGrav = myGravWater;
475   return false;
479 override bool onOutOfWater () {
480   //!myGrav = myGravNorm;
481   return false;
485 int origLightRadius = -666;
487 override void thinkFrame () {
488   if (origLightRadius == -666) {
489     origLightRadius = lightRadius;
490     lightRadius = max(origLightRadius, (forSale && level.isInShop(ix/16, iy/16) ? 64 : 0));
491   }
493   bool doBreak = false;
495   //basicPhysicsStep();
496   //if (!isInstanceAlive) return;
497   //if (!heldBy && self isa ItemBomb) writeln("yVel=", yVel, "; myGrav=", myGrav);
499   // water
500   if (allowWaterProcessing) {
501     //auto wtile = level.isWaterAtPoint(ix+spf.width/2, iy+spf.height/2/*, -1, -1*/);
502     auto wtile = level.isWaterAtPoint(ix+8, iy+8);
503     if (wtile) {
504       if (!swimming) {
505         swimming = true;
506         if (onFellInWater(wtile) || !isInstanceAlive) return;
507       }
508     } else {
509       if (swimming) {
510         swimming = false;
511         if (onOutOfWater() || !isInstanceAlive) return;
512       }
513     }
514   }
516   if (heldBy) return;
518   if (!isCollisionAtPoint(ix, iy)) {
519     //if (self isa ItemProjectileArrow) writeln("xVel=", xVel, "; yVel=", yVel);
520     moveRel(xVel, yVel);
522     bool colTop = !!isCollisionTop(1);
523     bool colLeft = !!isCollisionLeft(1);
524     bool colRight = !!isCollisionRight(1);
525     bool colBot = !!isCollisionBottom(1);
527     if (!colLeft && !colRight) stuck = false;
529     if (!flying && !colBot && !stuck) yVel += myGrav;
530     //if (yVel > 8) yVel = 8;
531     //yVel = fmin(yVelLimit, yVel);
532     yVel = fmin(8, yVel);
534     // not in the original
535     /+
536     if (colTop && yVel < 0) {
537       if (breaksOnCollision && yVel < breakYVUp) doBreak = true;
538            if (bounceTop) yVel = -yVel*0.8;
539       else if (fabs(xVel) < 0.0001) yVel = 0;
540     }
541     +/
543     if (colLeft || colRight) {
544       if (breaksOnCollision && fabs(xVel) > breakXV) doBreak = true;
545       xVel = (bounce ? -xVel*0.5 : 0.0);
546       myGrav = 0.6;
547     }
549     if (colBot) {
550       if (breaksOnCollision && yVel > breakYV) doBreak = true;
551       myGrav = 0.6;
552       // bounce
553       if (yVel > 1) ++bounceCount;
554       yVel = (yVel > 1 && bounce ? -yVel*bounceFactor : 0.0);
555       // friction
556            if (fabs(xVel) < 0.1) xVel = 0;
557       else if (fabs(xVel) != 0) xVel *= frictionFactor;
558       if (fabs(yVel) < 1) {
559         flty = iy-1;
560         if (!isCollisionBottom(1)) flty = iy+1;
561         yVel = 0;
562       }
563     }
565     if (sticky && self isa ItemBomb && self.armed) {
566       if (colLeft || colRight || colTop || colBot) {
567         xVel = 0;
568         yVel = 0;
569         if (colBot && fabs(yVel) < 1) flty = iy+1;
570       }
571     } else if (self isa ItemProjectileArrow && fabs(xVel) > 6) {
572       if (colLeft) {
573         fltx = ix-2;
574         xVel = 0;
575         yVel = 0;
576       } else if (colRight) {
577         fltx = ix+2;
578         xVel = 0;
579         yVel = 0;
580       }
581       stuck = true;
582     } else if (colLeft && !stuck) {
583       if (!colRight) fltx = ix+1;
584       //yVel = 0; // in the original
585     } else if (colRight && !stuck) {
586       fltx = ix-1;
587       //yVel = 0; // in the original
588     }
590     if (sticky && self isa ItemBomb && self.armed) {
591       // do nothing
592     } else if (isCollisionTop(1)) {
593       if (breaksOnCollision && yVel < breakYVUp) doBreak = true;
594       if (yVel < 0) yVel = -yVel*0.8; else flty = iy+1;
595       myGrav = 0.6;
596     }
598     if (isCollisionInRect(ix-3, iy-3, 7, 7, &level.cbCollisionLava)) {
599       myGrav = 0;
600       xVel = 0;
601       yVel = 0;
602       flty += 0.05;
603     } else {
604       myGrav = 0.6;
605     }
607     if (self !isa ItemWeaponSceptre && isCollisionAtPoint(ix, iy-5, &level.cbCollisionLava)) {
608       auto bomb = ItemBomb(self);
609       if (bomb) bomb.explode();
610       instanceRemove();
611       return;
612     }
613   } else {
614     //if (self isa ItemProjectileArrow) writeln("CLD: xVel=", xVel, "; yVel=", yVel);
615     bool colTop = !!isCollisionTop(1);
616     bool colLeft = !!isCollisionLeft(1);
617     bool colRight = !!isCollisionRight(1);
618     bool colBot = !!isCollisionBottom(1);
619     // collided
620     if (breaksOnCollision) {
621       collBroken = true;
622       instanceRemove();
623     } else {
624            if (colTop && !colBot) flty = iy+1;
625       else if (colLeft && !colRight) fltx = ix+1;
626       else if (colRight && !colLeft) fltx = ix-1;
627       else { xVel = 0; yVel = 0; }
628     }
629   }
631   if (canHitEnemies && width > 0 && height > 0 && isInstanceAlive) {
632     if (fabs(xVel) > 2 || fabs(yVel) > 2) {
633       wasObjectCollision = false;
634       auto plr = level.player;
635       if (isInstanceAlive && !spectral) {
636         spectral = true;
637         level.forEachObjectInRect(x0, y0, width, height, &doObjectColAction);
638         spectral = false;
639       }
640       if (isInstanceAlive && plr.isRectCollision(x0, y0, width, height)) {
641         doPlayerColAction(plr);
642         wasObjectCollision = true;
643       }
644       // this is done in `onHitByItem`
645       //if (wasObjectCollision) obj.xVel = xVel*0.3;
646     }
647   }
649   if (doBreak && breaksOnCollision) {
650     collBroken = true;
651     instanceRemove();
652   }
656 defaultproperties {
657   depth = 101; // behind enemies (60)
659   objType = 'oItem';
660   setCollisionBounds(0, 0, 16, 16);
661   yVelLimit = 8;
662   bounce = true;
663   bounceTop = true;
665   canBeHitByBullet = false;
666   canBeNudged = true;
668   holdYOfs = 2;