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 **********************************************************************************/
19 class MapItem : MapObject;
21 bool breakPieces = true;
22 bool dropContents = true;
23 name contents; // to use in `level.MakeMapObject()`
24 int contOfsX, contOfsY;
27 bool breaksOnCollision = false; // jars and skulls will do, rocks will not
28 bool canHitEnemies = false;
31 float breakYVUp = -3, breakXV = 4, breakYV = 4;
33 int holdXOfs, holdYOfs;
38 override bool initialize () {
39 if (!::initialize()) return false;
40 forSaleFrame = global.randOther(0, 9);
46 final void setCollisionBoundsKill (int hx0, int hy0, int hx1, int hy1) {
47 setCollisionBounds(hx0, hy0, hx1, hy1);
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
62 if (obj && obj.isCollision()) {
63 writeln("***STUCK! (", obj.objType, ")");
65 auto ox = obj.fltx, oy = obj.flty;
68 if (!obj.isCollisionBottom(1)) {
69 writeln(" UNSTUCK: go bottom!");
71 } else if (!obj.isCollisionTop(1)) {
72 writeln(" UNSTUCK: go top!");
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);
92 writeln(" UNSTUCK: horizontal");
93 foreach (int dy; 0..9*3) {
94 foreach (int dx; 0..9*3) {
97 if (!obj.isCollision()) { didit = true; break; }
100 if (!obj.isCollision()) { didit = true; break; }
103 if (!obj.isCollision()) { didit = true; break; }
106 if (!obj.isCollision()) { didit = true; break; }
111 //obj.active = false;
116 obj.saveInterpData();
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);
129 auto piece = level.MakeMapObject(ix-2, iy-2, 'oRubbleSmall');
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);
143 // ////////////////////////////////////////////////////////////////////////// //
144 void drawSignsWithOfs (int xpos, int ypos, int scale, float currFrameDelta) {
145 if (!forSale && !sellOfferDone) return;
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);
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 () {
168 if (forSale || sellOfferDone) {
169 if (++forSaleFrame < 0) forSaleFrame = 0;
170 onCheckItemStolen(level.player);
175 // ////////////////////////////////////////////////////////////////////////// //
176 override bool onCanBePickedUp (PlayerPawn plr) {
181 // ////////////////////////////////////////////////////////////////////////// //
182 protected transient bool onExploAffected = true;
184 override bool onExplosionTouch (MapObject xplo) {
185 if (invincible) return false;
188 auto plr = PlayerPawn(heldBy);
189 if (plr) plr.scrDropItem(LostCause.Throw, 0, 0); else heldBy.holdItem = none;
190 // drop item from pocket
192 plr.scrSwitchToPocketItem(forceIfEmpty:false);
193 auto hi = plr.holdItem;
194 plr.scrDropItem(LostCause.Throw, 0, 0);
195 if (hi) hi.onExplosionTouch(xplo);
199 if (onExploAffected) {
200 if (breaksOnCollision) {
203 return true; // stop it, we are dead anyway
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);
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") {
214 sprite_index = sBombArmed;
216 alarm[1] = rand(4, 8);
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);
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);
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);
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;
253 plr.CreateBlood(plr.ix, plr.iy, 1);
255 plr.stunTimer = 120; // 200?
257 plr.playSound('sndHit');
259 if (breaksOnCollision) {
266 if (canPickUp && global.hasMitt && !plr.holdItem && (fabs(xVel) > 4 || fabs(yVel) >= 6) &&
267 !safe && !plr.stunned && !plr.dead)
271 return true; // no more actions
275 return false; // go on
279 // ////////////////////////////////////////////////////////////////////////// //
281 // return `true` to skip normal item processing
282 // if skipped, engine will call `onAfterSomethingHit()`
283 bool onEnemyHit (MapEnemy e) {
285 if (enemy.status != STUNNED) enemy.xVel = xVel*0.3; //k8: *0.3 is mine
286 switch (enemy.objName) {
292 if (enemy.status != STUNNED) {
293 switch (enemy.objName) {
298 level.MakeMapObject(enemy.ix, enemy.iy, 'oBlood');
301 enemy.status = STUNNED;
302 enemy.counter = stunTime;
309 level.MakeMapObject(enemy.ix, enemy.iy, 'oBlood');
311 if (enemy.heldBy) enemy.heldBy.holdItem = none;
314 enemy.status = 2; //???
316 enemy.damselDropped = true;
317 enemy.xVel = xVel*0.3;
321 level.MakeMapObject(enemy.ix+8, enemy.iy+8, 'oBlood');
323 //!enemy.origX = enemy.x;
324 //!enemy.origY = enemy.y;
325 enemy.shakeCounter = 10;
332 if (enemy.status != STUNNED) enemy.xVel = xVel*0.3;
333 //if (objType == 'Arrow' || objType == 'Fish Bone') instance_destroy();
340 // return `false` to do standard weapon processing
341 override bool onTouchedByPlayerWeapon (PlayerPawn plr, PlayerWeapon wpn) {
342 if (!wpn.prestrike && breaksOnCollision) {
353 override void onBulletHit (ObjBullet bullet) {
354 if (breaksOnCollision) {
363 // return `true` to stop further processing
364 bool onAfterSomethingHit () {
365 if (breaksOnCollision) {
368 return true; // stop it, we are dead anyway
376 transient bool wasObjectCollision;
378 bool doObjectColAction (MapObject o) {
379 if (!isInstanceAlive) return true; // stop it, we are dead anyway
383 MapEnemy enemy = MapEnemy(o);
386 if (enemy.isInstanceAlive && onEnemyHit(enemy)) {
387 wasObjectCollision = true;
388 if (onAfterSomethingHit()) return true; // anyway
389 return !isInstanceAlive;
392 if (enemy.onHitByItem(self)) {
393 wasObjectCollision = true;
394 if (onAfterSomethingHit()) return true; // anyway
395 return !isInstanceAlive;
397 return !isInstanceAlive;
402 return !isInstanceAlive;
406 override void fixHoldCoords () {
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);
417 setXY(heldBy.fltx+dx, heldBy.flty+dy);
418 prevFltX = heldBy.prevFltX+dx;
419 prevFltY = heldBy.prevFltY+dy;
421 if (spriteRName) dir = heldBy.dir;
426 // Nudge with melee weapon
427 override void nudgeIt (int nx, int ny, optional bool forced) {
429 if (level.isSolidAtPoint(ix, iy)) return;
430 if (!forced && !global.config.nudge) return;
433 if (forSale || /*forVending ||*/ trigger) {
434 if (!trigger) 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);
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;
450 if (type == "Basketball") {
451 if (abs(yVel) < 4) yVel -= 5;
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);
471 override bool onFellInWater (MapTile water) {
472 level.MakeMapObject(xCenter, iy, 'oSplash');
473 playSound('sndSplash');
474 //!myGrav = myGravWater;
479 override bool onOutOfWater () {
480 //!myGrav = myGravNorm;
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));
493 bool doBreak = false;
495 //basicPhysicsStep();
496 //if (!isInstanceAlive) return;
497 //if (!heldBy && self isa ItemBomb) writeln("yVel=", yVel, "; myGrav=", myGrav);
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);
506 if (onFellInWater(wtile) || !isInstanceAlive) return;
511 if (onOutOfWater() || !isInstanceAlive) return;
518 if (!isCollisionAtPoint(ix, iy)) {
519 //if (self isa ItemProjectileArrow) writeln("xVel=", xVel, "; yVel=", 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
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;
543 if (colLeft || colRight) {
544 if (breaksOnCollision && fabs(xVel) > breakXV) doBreak = true;
545 xVel = (bounce ? -xVel*0.5 : 0.0);
550 if (breaksOnCollision && yVel > breakYV) doBreak = true;
553 if (yVel > 1) ++bounceCount;
554 yVel = (yVel > 1 && bounce ? -yVel*bounceFactor : 0.0);
556 if (fabs(xVel) < 0.1) xVel = 0;
557 else if (fabs(xVel) != 0) xVel *= frictionFactor;
558 if (fabs(yVel) < 1) {
560 if (!isCollisionBottom(1)) flty = iy+1;
565 if (sticky && self isa ItemBomb && self.armed) {
566 if (colLeft || colRight || colTop || colBot) {
569 if (colBot && fabs(yVel) < 1) flty = iy+1;
571 } else if (self isa ItemProjectileArrow && fabs(xVel) > 6) {
576 } else if (colRight) {
582 } else if (colLeft && !stuck) {
583 if (!colRight) fltx = ix+1;
584 //yVel = 0; // in the original
585 } else if (colRight && !stuck) {
587 //yVel = 0; // in the original
590 if (sticky && self isa ItemBomb && self.armed) {
592 } else if (isCollisionTop(1)) {
593 if (breaksOnCollision && yVel < breakYVUp) doBreak = true;
594 if (yVel < 0) yVel = -yVel*0.8; else flty = iy+1;
598 if (isCollisionInRect(ix-3, iy-3, 7, 7, &level.cbCollisionLava)) {
607 if (self !isa ItemWeaponSceptre && isCollisionAtPoint(ix, iy-5, &level.cbCollisionLava)) {
608 auto bomb = ItemBomb(self);
609 if (bomb) bomb.explode();
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);
620 if (breaksOnCollision) {
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; }
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) {
637 level.forEachObjectInRect(x0, y0, width, height, &doObjectColAction);
640 if (isInstanceAlive && plr.isRectCollision(x0, y0, width, height)) {
641 doPlayerColAction(plr);
642 wasObjectCollision = true;
644 // this is done in `onHitByItem`
645 //if (wasObjectCollision) obj.xVel = xVel*0.3;
649 if (doBreak && breaksOnCollision) {
657 depth = 101; // behind enemies (60)
660 setCollisionBounds(0, 0, 16, 16);
665 canBeHitByBullet = false;