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 MapItem : MapObject;
20 bool breakPieces = true;
21 bool dropContents = true;
22 name contents; // to use in `level.MakeMapObject()`
23 int contOfsX, contOfsY;
26 bool breaksOnCollision = false; // jars and skulls will do, rocks will not
27 bool canHitEnemies = false;
30 float breakYVUp = -3, breakXV = 4, breakYV = 4;
32 int enemyColX, enemyColY;
33 int enemyColW, enemyColH;
36 int holdXOfs, holdYOfs;
41 override bool initialize () {
42 if (!::initialize()) return false;
43 forSaleFrame = global.randOther(0, 9);
49 final void setCollisionBoundsKill (int hx0, int hy0, int hx1, int hy1) {
50 setCollisionBounds(hx0, hy0, hx1, hy1);
59 override void onDestroy () {
60 if (dropContents && contents) {
61 //level.MakeMapObjectByClass(contents, contOfsX, contOfsY);
62 auto obj = level.MakeMapObject(ix+contOfsX, iy+contOfsY, contents);
63 spectral = true; // just in case
65 if (obj && obj.isCollision()) {
66 writeln("***STUCK! (", obj.objType, ")");
68 auto ox = obj.fltx, oy = obj.flty;
71 if (!obj.isCollisionBottom(1)) {
72 writeln(" UNSTUCK: go bottom!");
74 } else if (!obj.isCollisionTop(1)) {
75 writeln(" UNSTUCK: go top!");
79 int xmove = (obj.isCollisionLeft(1) ? 1 : obj.isCollisionRight(1) ? -1 : 0);
80 //if (!xmove) xmove = (obj.isCollisionLeft(1) ? 1 : obj.isCollisionRight(1) ? -1 : 0);
81 //writeln(" xmove=", xmove);
82 foreach (int dy; 0..9*3) {
83 foreach (int dx; 0..9*3) {
84 obj.fltx = ox+dx*xmove;
85 obj.flty = oy+dy*ymove;
86 if (!obj.isCollision()) {
87 //writeln("***UNSTUCK! dy=", dy);
95 writeln(" UNSTUCK: horizontal");
96 foreach (int dy; 0..9*3) {
97 foreach (int dx; 0..9*3) {
100 if (!obj.isCollision()) { didit = true; break; }
103 if (!obj.isCollision()) { didit = true; break; }
106 if (!obj.isCollision()) { didit = true; break; }
109 if (!obj.isCollision()) { didit = true; break; }
114 //obj.active = false;
119 obj.saveInterpData();
124 playSound('sndBreak');
125 level.MakeMapObject(ix, iy, 'oSmokePuff');
126 bool colTop = !!isCollisionTop(1);
127 bool colLeft = !!isCollisionLeft(1);
128 bool colRight = !!isCollisionRight(1);
129 //bool colBot = !!isCollisionBottom(1);
132 auto piece = level.MakeMapObject(ix-2, iy-2, 'oRubbleSmall');
134 if (colLeft) piece.xVel = global.randOther(1, 3);
135 else if (colRight) piece.xVel = -global.randOther(1, 3);
136 else piece.xVel = global.randOther(1, 3)-global.randOther(1, 3);
137 if (colTop) piece.yVel = global.randOther(0, 3); else piece.yVel = -global.randOther(0, 3);
146 // ////////////////////////////////////////////////////////////////////////// //
147 void drawSignsWithOfs (int xpos, int ypos, int scale, float currFrameDelta) {
148 if (!forSale && !sellOfferDone) return;
152 getInterpCoords(currFrameDelta, scale, out xi, out yi);
153 auto spr = level.sprStore[sellOfferDone ? 'sSmallCollectGreen' : 'sSmallCollect']; //sSmallCollectGreen for resale
154 if (spr && spr.frames.length) {
155 forSaleFrame %= spr.frames.length;
156 auto spf = spr.frames[forSaleFrame];
157 spf.tex.blitAt(xi-xpos-spf.xofs*scale, yi-ypos-(12+spf.yofs)*scale, scale);
162 override void drawWithOfs (int xpos, int ypos, int scale, float currFrameDelta) {
163 ::drawWithOfs(xpos, ypos, scale, currFrameDelta);
164 drawSignsWithOfs(xpos, ypos, scale, currFrameDelta);
168 // ////////////////////////////////////////////////////////////////////////// //
169 override void processAlarms () {
171 if (forSale || sellOfferDone) {
172 if (++forSaleFrame < 0) forSaleFrame = 0;
173 onCheckItemStolen(level.player);
178 // ////////////////////////////////////////////////////////////////////////// //
179 override bool onCanBePickedUp (PlayerPawn plr) {
184 // ////////////////////////////////////////////////////////////////////////// //
185 protected transient bool onExploAffected = true;
187 override bool onExplosionTouch (MapObject xplo) {
188 if (invincible) return false;
191 heldBy.holdItem = none;
192 // drop item from pocket
193 auto plr = PlayerPawn(heldBy);
195 plr.scrSwitchToPocketItem(forceIfEmpty:false);
196 auto hi = plr.holdItem;
198 if (hi) hi.onExplosionTouch(xplo);
202 if (onExploAffected) {
203 if (breaksOnCollision) {
206 return true; // stop it, we are dead anyway
208 if (flty < xplo.flty) yVel -= 6; else yVel += 6;
209 if (xplo.fltx > fltx) xVel -= global.randOther(4, 6); else xVel += global.randOther(4, 6);
213 if (other.type == "Arrow" or other.type == "Fish Bone" or other.type == "Jar" or other.type == "Skull") {
214 with (other) instance_destroy();
215 } else if (other.type == "Bomb") {
217 sprite_index = sBombArmed;
219 alarm[1] = rand(4, 8);
222 if (other.y < y) other.yVel = -rand(2, 4);
223 if (other.x < x) other.xVel = -rand(2, 4); else other.xVel = rand(2, 4);
224 } else if (other.type == "Rope") {
225 if (not other.falling) {
226 if (other.y < y) other.yVel -= 6; else other.yVel += 6;
227 if (x > other.x) other.xVel -= rand(4, 6); else other.xVel += rand(4, 6);
230 if (other.y < y) other.yVel -= 6; else other.yVel += 6;
231 if (x > other.x) other.xVel -= rand(4, 6); else other.xVel += rand(4, 6);
239 // ////////////////////////////////////////////////////////////////////////// //
240 //override bool onTouchedByPlayer (PlayerPawn plr)
242 bool doPlayerColAction (PlayerPawn plr) {
243 //if (safe) return false;
245 if (collision_rectangle(x-8, y-8, x+8, y+8, oRock, 0, 0)) {
246 obj = instance_nearest(x, y, oRock);
250 if (enemyColW < 1 || enemyColH < 1 || self isa ItemBomb) return false;
251 if (fabs(xVel) > 2 || fabs(yVel) > 2) {
252 if (!isInstanceAlive) return false; // stop it, we are dead anyway
253 if (heldBy) return false;
254 if (plr.dead || plr.invincible || plr.status == STUNNED || plr.stunned) return false;
256 plr.CreateBlood(plr.ix, plr.iy, 1);
258 plr.stunTimer = 120; // 200?
260 plr.playSound('sndHit');
262 if (breaksOnCollision) {
269 if (canPickUp && global.hasMitt && !plr.holdItem && (fabs(xVel) > 4 || fabs(yVel) >= 6) &&
270 !safe && !plr.stunned && !plr.dead)
274 return true; // no more actions
278 return false; // go on
282 // ////////////////////////////////////////////////////////////////////////// //
284 // return `true` to skip normal item processing
285 // if skipped, engine will call `onAfterSomethingHit()`
286 bool onEnemyHit (MapEnemy e) {
288 if (enemy.status != STUNNED) enemy.xVel = xVel*0.3; //k8: *0.3 is mine
289 switch (enemy.objName) {
295 if (enemy.status != STUNNED) {
296 switch (enemy.objName) {
301 level.MakeMapObject(enemy.ix, enemy.iy, 'oBlood');
304 enemy.status = STUNNED;
305 enemy.counter = stunTime;
312 level.MakeMapObject(enemy.ix, enemy.iy, 'oBlood');
314 if (enemy.heldBy) enemy.heldBy.holdItem = none;
317 enemy.status = 2; //???
319 enemy.damselDropped = true;
320 enemy.xVel = xVel*0.3;
324 level.MakeMapObject(enemy.ix+8, enemy.iy+8, 'oBlood');
326 //!enemy.origX = enemy.x;
327 //!enemy.origY = enemy.y;
328 enemy.shakeCounter = 10;
335 if (enemy.status != STUNNED) enemy.xVel = xVel*0.3;
336 //if (objType == 'Arrow' || objType == 'Fish Bone') instance_destroy();
343 // return `false` to do standard weapon processing
344 override bool onTouchedByPlayerWeapon (PlayerPawn plr, PlayerWeapon wpn) {
345 if (!wpn.prestrike && breaksOnCollision) {
356 override void onBulletHit (ObjBullet bullet) {
357 if (breaksOnCollision) {
366 // return `true` to stop further processing
367 bool onAfterSomethingHit () {
368 if (breaksOnCollision) {
371 return true; // stop it, we are dead anyway
379 transient bool wasObjectCollision;
381 bool doObjectColAction (MapObject o) {
382 if (!isInstanceAlive) return true; // stop it, we are dead anyway
386 MapEnemy enemy = MapEnemy(o);
389 if (enemy.isInstanceAlive && onEnemyHit(enemy)) {
390 wasObjectCollision = true;
391 if (onAfterSomethingHit()) return true; // anyway
392 return !isInstanceAlive;
395 if (enemy.onHitByItem(self)) {
396 wasObjectCollision = true;
397 if (onAfterSomethingHit()) return true; // anyway
398 return !isInstanceAlive;
400 return !isInstanceAlive;
405 return !isInstanceAlive;
409 override void fixHoldCoords () {
416 int dx = (heldBy.dir == Dir.Left ? -4 : 4);
417 int dy = ((heldBy.status == DUCKING || heldBy.status == STUNNED || heldBy.stunned) && fabs(heldBy.xVel) < 2 ? 4 : 0);
420 setXY(heldBy.fltx+dx, heldBy.flty+dy);
421 prevFltX = heldBy.prevFltX+dx;
422 prevFltY = heldBy.prevFltY+dy;
424 if (spriteRName) dir = heldBy.dir;
429 // Nudge with melee weapon
430 override void nudgeIt (int nx, int ny, optional bool forced) {
432 if (level.isSolidAtPoint(ix, iy)) return;
433 if (!forced && !global.config.nudge) return;
436 if (forSale || /*forVending ||*/ trigger) {
437 if (!trigger) yVel = -1;
441 if (nx < ix) xVel += global.randOther(5, 8)*0.1;
442 else if (nx > ix) xVel -= global.randOther(5, 8)*0.1;
443 } else if (self isa ItemProjectileArrow && fabs(xVel) > 0) {
444 if (fabs(xVel) < 4) xVel = -xVel;
445 else if (xVel < 0) xVel = global.randOther(3, 5);
446 else if (xVel > 0) xVel = -global.randOther(3, 5);
448 } else if (!stuck && !sticky) {
449 yVel -= (global.randOther(0, 1) ? 2.0 : 1.5);
450 if (nx < ix) xVel += global.randOther(10, 15)*0.1;
451 else if (nx > ix) xVel -= global.randOther(10, 15)*0.1;
453 if (type == "Basketball") {
454 if (abs(yVel) < 4) yVel -= 5;
466 void onCheckItemStolen (PlayerPawn plr) {
467 // check if it is stolen
468 if (forSale && cost > 0 && !level.isInShop(ix/16, iy/16)) {
469 level.scrShopkeeperAnger(GameLevel::SCAnger.ItemStolen);
474 override bool onFellInWater (MapTile water) {
475 level.MakeMapObject(xCenter, iy, 'oSplash');
476 playSound('sndSplash');
477 //!myGrav = myGravWater;
482 override bool onOutOfWater () {
483 //!myGrav = myGravNorm;
488 int origLightRadius = -666;
490 override void thinkFrame () {
491 if (origLightRadius == -666) {
492 origLightRadius = lightRadius;
493 lightRadius = max(origLightRadius, (forSale && level.isInShop(ix/16, iy/16) ? 64 : 0));
496 bool doBreak = false;
498 //basicPhysicsStep();
499 //if (!isInstanceAlive) return;
500 //if (!heldBy && self isa ItemBomb) writeln("yVel=", yVel, "; myGrav=", myGrav);
503 if (allowWaterProcessing) {
504 //auto wtile = level.isWaterAtPoint(ix+spf.width/2, iy+spf.height/2/*, -1, -1*/);
505 auto wtile = level.isWaterAtPoint(ix+8, iy+8);
509 if (onFellInWater(wtile) || !isInstanceAlive) return;
514 if (onOutOfWater() || !isInstanceAlive) return;
522 if (!isCollisionAtPoint(ix, iy)) {
523 //if (self isa ItemProjectileArrow) writeln("xVel=", xVel, "; yVel=", yVel);
526 bool colTop = !!isCollisionTop(1);
527 bool colLeft = !!isCollisionLeft(1);
528 bool colRight = !!isCollisionRight(1);
529 bool colBot = !!isCollisionBottom(1);
531 if (!colLeft && !colRight) stuck = false;
533 if (!flying && !colBot && !stuck) yVel += myGrav;
534 //if (yVel > 8) yVel = 8;
535 //yVel = fmin(yVelLimit, yVel);
536 yVel = fmin(8, yVel);
538 // not in the original
540 if (colTop && yVel < 0) {
541 if (breaksOnCollision && yVel < breakYVUp) doBreak = true;
542 if (bounceTop) yVel = -yVel*0.8;
543 else if (fabs(xVel) < 0.0001) yVel = 0;
547 if (colLeft || colRight) {
548 if (breaksOnCollision && fabs(xVel) > breakXV) doBreak = true;
549 xVel = (bounce ? -xVel*0.5 : 0.0);
554 if (breaksOnCollision && yVel > breakYV) doBreak = true;
557 yVel = (yVel > 1 && bounce ? -yVel*bounceFactor : 0.0);
559 if (fabs(xVel) < 0.1) xVel = 0;
560 else if (fabs(xVel) != 0) xVel *= frictionFactor;
561 if (fabs(yVel) < 1) {
563 if (!isCollisionBottom(1)) flty = iy+1;
568 if (sticky && self isa ItemBomb && self.armed) {
569 if (colLeft || colRight || colTop || colBot) {
572 if (colBot && fabs(yVel) < 1) flty = iy+1;
574 } else if (self isa ItemProjectileArrow && fabs(xVel) > 6) {
579 } else if (colRight) {
585 } else if (colLeft && !stuck) {
586 if (!colRight) fltx = ix+1;
587 //yVel = 0; // in the original
588 } else if (colRight && !stuck) {
590 //yVel = 0; // in the original
593 if (sticky && self isa ItemBomb && self.armed) {
595 } else if (isCollisionTop(1)) {
596 if (breaksOnCollision && yVel < breakYVUp) doBreak = true;
597 if (yVel < 0) yVel = -yVel*0.8; else flty = iy+1;
601 if (isCollisionInRect(ix-3, iy-3, 7, 7, &level.cbCollisionLava)) {
610 if (self !isa ItemWeaponSceptre && isCollisionAtPoint(ix, iy-5, &level.cbCollisionLava)) {
611 auto bomb = ItemBomb(self);
612 if (bomb) bomb.explode();
617 //if (self isa ItemProjectileArrow) writeln("CLD: xVel=", xVel, "; yVel=", yVel);
618 bool colTop = !!isCollisionTop(1);
619 bool colLeft = !!isCollisionLeft(1);
620 bool colRight = !!isCollisionRight(1);
621 bool colBot = !!isCollisionBottom(1);
623 if (breaksOnCollision) {
627 if (colTop && !colBot) flty = iy+1;
628 else if (colLeft && !colRight) fltx = ix+1;
629 else if (colRight && !colLeft) fltx = ix-1;
630 else { xVel = 0; yVel = 0; }
634 if (canHitEnemies && width > 0 && height > 0 && isInstanceAlive) {
635 if (fabs(xVel) > 2 || fabs(yVel) > 2) {
636 wasObjectCollision = false;
637 auto plr = level.player;
638 if (isInstanceAlive && !spectral) {
640 level.forEachObjectInRect(x0, y0, width, height, &doObjectColAction);
643 if (isInstanceAlive && plr.isRectCollision(x0, y0, width, height)) {
644 doPlayerColAction(plr);
645 wasObjectCollision = true;
647 // this is done in `onHitByItem`
648 //if (wasObjectCollision) obj.xVel = xVel*0.3;
652 if (doBreak && breaksOnCollision) {
660 depth = 101; // behind enemies (60)
663 setCollisionBounds(0, 0, 16, 16);
668 canBeHitByBullet = false;
675 // ////////////////////////////////////////////////////////////////////////// //
676 class ItemDice['oDice'] : MapItem;
680 None, // ready to roll
682 Finished, // landed in shop
683 Failed, // landed outside of the shop
687 bool pickedOutsideOfAShop;
690 override bool initialize () {
691 if (!::initialize()) return false;
693 value = global.randOther(1, 6);
699 final int getRollNumber () {
700 if (rollState != RollState.Finished) return 0;
705 final void resetRollState () {
706 rollState = RollState.None;
710 bool isReadyToThrowForBet () {
711 if (!forSale) return false;
712 return ((rollState == RollState.Failed || rollState == RollState.None) && level.player.bet);
716 // various side effects
717 // called only if object was succesfully put into player hands
718 override void onPickedUp (PlayerPawn plr) {
719 pickedOutsideOfAShop = !level.isInShop(ix/16, iy/16);
720 if (rollState != RollState.Finished) rollState = RollState.None;
724 override void drawSignsWithOfs (int xpos, int ypos, int scale, float currFrameDelta) {
725 if (!forSale) return;
726 if ((rollState == RollState.Failed || rollState == RollState.None) && level.player.bet) {
728 getInterpCoords(currFrameDelta, scale, out xi, out yi);
729 auto spr = level.sprStore['sRedArrowDown'];
730 if (spr && spr.frames.length) {
731 auto spf = spr.frames[0];
732 spf.tex.blitAt(xi-xpos-spf.xofs*scale, yi-ypos-(12+spf.yofs)*scale, scale);
738 override void onCheckItemStolen (PlayerPawn plr) {
739 if (!heldBy || pickedOutsideOfAShop) return;
741 if (forSale && !level.player.bet && rollState != RollState.Failed) {
742 bool inShop = level.isInShop(ix/16, iy/16);
744 level.scrShopkeeperAnger(GameLevel::SCAnger.ItemStolen); // don't steal it!
750 override void thinkFrame () {
751 if (forSale && /*!forVending && cost > 0 &&*/ !level.hasAliveShopkeepers(skipAngry:true)) {
756 lightRadius = max(origLightRadius, (forSale && level.isInShop(ix/16, iy/16) ? 64 : 0));
760 if (oCharacter.facing == LEFT) x = oCharacter.x - 4;
761 else if (oCharacter.facing == RIGHT) x = oCharacter.x + 4;
764 if (oCharacter.state == DUCKING and abs(oCharacter.xVel) < 2) y = oCharacter.y; else y = oCharacter.y-2;
766 if (oCharacter.state == DUCKING and abs(oCharacter.xVel) < 2) y = oCharacter.y+4; else y = oCharacter.y+2;
770 if (oCharacter.holdItem == 0) held = false;
772 // stealing makes shopkeeper angry
773 //writeln("!!! fs=", forSale, "; bet=", level.player.bet, "; st=", rollState);
777 bool colLeft = !!isCollisionLeft(1);
778 bool colRight = !!isCollisionRight(1);
779 bool colBot = !!isCollisionBottom(1);
780 bool colTop = !!isCollisionTop(1);
782 if (!colBot && yVel < 6) yVel += myGrav;
784 if (fabs(xVel) < 0.1) xVel = 0;
785 else if (colLeft || colRight) xVel = -xVel*0.5;
789 if (yVel > 1) yVel = -yVel*bounceFactor; else yVel = 0;
791 if (fabs(xVel) < 0.1) xVel = 0;
792 else if (fabs(xVel) != 0) xVel *= frictionFactor;
793 if (fabs(yVel) < 1) {
795 if (!isCollisionBottom(1)) flty += 1;
801 if (!colRight) fltx += 1;
803 } else if (colRight) {
808 if (isCollisionTop(1)) {
809 if (yVel < 0) yVel = -yVel*0.8; else flty += 1;
812 //!depth = (global.hasSpectacles ? 0 : 101); //???
814 if (isCollisionInRect(ix-3, iy-3, 7, 7, &level.cbCollisionLava)) {
823 if (isCollisionAtPoint(ix, iy-5, &level.cbCollisionLava)) {
829 if (!isInstanceAlive || spectral) return;
831 if (fabs(xVel) > 3 || fabs(yVel) > 3) {
833 auto plr = level.player;
834 if (plr.isRectCollision(ix+enemyColX, iy+enemyColY, enemyColW, enemyColH)) {
835 doPlayerColAction(plr);
839 level.forEachObjectInRect(ix-2, iy-2, 5, 5, &doObjectColAction);
844 if (fabs(yVel) > 2 || fabs(xVel) > 2) {
845 setSprite('sDiceRoll');
846 value = global.randOther(1, 6);
848 case RollState.Finished:
850 if (level.player.bet > 0) level.scrShopkeeperAnger(GameLevel::SCAnger.CrapsCheated);
853 rollState = RollState.Rolling;
856 } else if (yVel == 0 && fabs(xVel <= 2) && isCollisionBottom(1)) {
859 case RollState.Rolling:
860 rollState = (level.isInShop(ix/16, iy/16) ? RollState.Finished : RollState.Failed);
861 if (rollState == RollState.Finished) level.player.onDieRolled(self);
863 case RollState.Finished:
864 case RollState.Failed:
867 default: rollState = RollState.None; break;
872 case 1: setSprite('sDice1'); break;
873 case 2: setSprite('sDice2'); break;
874 case 3: setSprite('sDice3'); break;
875 case 4: setSprite('sDice4'); break;
876 case 5: setSprite('sDice5'); break;
877 default: setSprite('sDice6'); break;
880 canPickUp = (rollState != RollState.Rolling);
887 desc2 = "A six-sided die. The storeowner talks to it every night before he goes to sleep.";
888 setCollisionBounds(-6, 0, 6, 8);
895 rollState = RollState.None;
896 bloodless = true; // just in case, lol
897 canHitEnemies = true;