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) return;
152 getInterpCoords(currFrameDelta, scale, out xi, out yi);
153 auto spr = level.sprStore['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 () {
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.scrCreateBlood(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 // return `true` to skip normal item processing
380 // if skipped, engine will call `onAfterSomethingHit()`
381 bool onCharacterHit (ObjCharacter chr) {
386 transient bool wasObjectCollision;
388 bool doObjectColAction (MapObject o) {
389 if (!isInstanceAlive) return true; // stop it, we are dead anyway
393 MapEnemy enemy = MapEnemy(o);
396 if (enemy.isInstanceAlive && onEnemyHit(enemy)) {
397 wasObjectCollision = true;
398 if (onAfterSomethingHit()) return true; // anyway
399 return !isInstanceAlive;
402 if (enemy.onItemHit(self)) {
403 wasObjectCollision = true;
404 if (onAfterSomethingHit()) return true; // anyway
405 return !isInstanceAlive;
407 return !isInstanceAlive;
411 ObjCharacter chr = ObjCharacter(o);
414 if (chr.isInstanceAlive && onCharacterHit(chr)) {
415 wasObjectCollision = true;
416 if (onAfterSomethingHit()) return true; // anyway
417 return !isInstanceAlive;
420 if (chr.onItemHit(self)) {
421 wasObjectCollision = true;
422 if (onAfterSomethingHit()) return true; // anyway
423 return !isInstanceAlive;
429 return !isInstanceAlive;
433 override void fixHoldCoords () {
440 int dx = (heldBy.dir == Dir.Left ? -4 : 4);
441 int dy = ((heldBy.status == DUCKING || heldBy.status == STUNNED || heldBy.stunned) && fabs(heldBy.xVel) < 2 ? 4 : 0);
444 setXY(heldBy.fltx+dx, heldBy.flty+dy);
445 prevFltX = heldBy.prevFltX+dx;
446 prevFltY = heldBy.prevFltY+dy;
448 if (spriteRName) dir = heldBy.dir;
453 // Nudge with melee weapon
454 override void nudgeIt (int nx, int ny, optional bool forced) {
456 if (level.isSolidAtPoint(ix, iy)) return;
457 if (!forced && !global.config.nudge) return;
460 if (forSale || /*forVending ||*/ trigger) {
461 if (!trigger) yVel = -1;
465 if (nx < ix) xVel += global.randOther(5, 8)*0.1;
466 else if (nx > ix) xVel -= global.randOther(5, 8)*0.1;
467 } else if (self isa ItemProjectileArrow && fabs(xVel) > 0) {
468 if (fabs(xVel) < 4) xVel = -xVel;
469 else if (xVel < 0) xVel = global.randOther(3, 5);
470 else if (xVel > 0) xVel = -global.randOther(3, 5);
472 } else if (!stuck && !sticky) {
473 yVel -= (global.randOther(0, 1) ? 2.0 : 1.5);
474 if (nx < ix) xVel += global.randOther(10, 15)*0.1;
475 else if (nx > ix) xVel -= global.randOther(10, 15)*0.1;
477 if (type == "Basketball") {
478 if (abs(yVel) < 4) yVel -= 5;
490 void onCheckItemStolen (PlayerPawn plr) {
491 // check if it is stolen
492 if (forSale && cost > 0 && !level.isInShop(ix/16, iy/16)) {
493 level.scrShopkeeperAnger(GameLevel::SCAnger.ItemStolen);
498 override bool onFellInWater (MapTile water) {
499 level.MakeMapObject(xCenter, iy, 'oSplash');
500 playSound('sndSplash');
501 //!myGrav = myGravWater;
506 override bool onOutOfWater () {
507 //!myGrav = myGravNorm;
512 int origLightRadius = -666;
514 override void thinkFrame () {
515 if (origLightRadius == -666) {
516 origLightRadius = lightRadius;
517 lightRadius = max(origLightRadius, (forSale && level.isInShop(ix/16, iy/16) ? 64 : 0));
520 bool doBreak = false;
522 //basicPhysicsStep();
523 //if (!isInstanceAlive) return;
524 //if (!heldBy && self isa ItemBomb) writeln("yVel=", yVel, "; myGrav=", myGrav);
527 if (allowWaterProcessing) {
528 //auto wtile = level.isWaterAtPoint(ix+spf.width/2, iy+spf.height/2/*, -1, -1*/);
529 auto wtile = level.isWaterAtPoint(ix+8, iy+8);
533 if (onFellInWater(wtile) || !isInstanceAlive) return;
538 if (onOutOfWater() || !isInstanceAlive) return;
546 if (!isCollisionAtPoint(ix, iy)) {
547 //if (self isa ItemProjectileArrow) writeln("xVel=", xVel, "; yVel=", yVel);
550 bool colTop = !!isCollisionTop(1);
551 bool colLeft = !!isCollisionLeft(1);
552 bool colRight = !!isCollisionRight(1);
553 bool colBot = !!isCollisionBottom(1);
555 if (!colLeft && !colRight) stuck = false;
557 if (!flying && !colBot && !stuck) yVel += myGrav;
558 //if (yVel > 8) yVel = 8;
559 //yVel = fmin(yVelLimit, yVel);
560 yVel = fmin(8, yVel);
562 // not in the original
564 if (colTop && yVel < 0) {
565 if (breaksOnCollision && yVel < breakYVUp) doBreak = true;
566 if (bounceTop) yVel = -yVel*0.8;
567 else if (fabs(xVel) < 0.0001) yVel = 0;
571 if (colLeft || colRight) {
572 if (breaksOnCollision && fabs(xVel) > breakXV) doBreak = true;
573 xVel = (bounce ? -xVel*0.5 : 0.0);
578 if (breaksOnCollision && yVel > breakYV) doBreak = true;
581 yVel = (yVel > 1 && bounce ? -yVel*bounceFactor : 0.0);
583 if (fabs(xVel) < 0.1) xVel = 0;
584 else if (fabs(xVel) != 0) xVel *= frictionFactor;
585 if (fabs(yVel) < 1) {
587 if (!isCollisionBottom(1)) flty = iy+1;
592 if (sticky && self isa ItemBomb && self.armed) {
593 if (colLeft || colRight || colTop || colBot) {
596 if (colBot && fabs(yVel) < 1) flty = iy+1;
598 } else if (self isa ItemProjectileArrow && fabs(xVel) > 6) {
603 } else if (colRight) {
609 } else if (colLeft && !stuck) {
610 if (!colRight) fltx = ix+1;
611 //yVel = 0; // in the original
612 } else if (colRight && !stuck) {
614 //yVel = 0; // in the original
617 if (sticky && self isa ItemBomb && self.armed) {
619 } else if (isCollisionTop(1)) {
620 if (breaksOnCollision && yVel < breakYVUp) doBreak = true;
621 if (yVel < 0) yVel = -yVel*0.8; else flty = iy+1;
625 if (isCollisionInRect(ix-3, iy-3, 7, 7, &level.cbCollisionLava)) {
634 if (self !isa ItemWeaponSceptre && isCollisionAtPoint(ix, iy-5, &level.cbCollisionLava)) {
635 auto bomb = ItemBomb(self);
636 if (bomb) bomb.explode();
641 //if (self isa ItemProjectileArrow) writeln("CLD: xVel=", xVel, "; yVel=", yVel);
642 bool colTop = !!isCollisionTop(1);
643 bool colLeft = !!isCollisionLeft(1);
644 bool colRight = !!isCollisionRight(1);
645 bool colBot = !!isCollisionBottom(1);
647 if (breaksOnCollision) {
651 if (colTop && !colBot) flty = iy+1;
652 else if (colLeft && !colRight) fltx = ix+1;
653 else if (colRight && !colLeft) fltx = ix-1;
654 else { xVel = 0; yVel = 0; }
658 if (canHitEnemies && width > 0 && height > 0 && isInstanceAlive) {
659 if (fabs(xVel) > 2 || fabs(yVel) > 2) {
660 wasObjectCollision = false;
661 auto plr = level.player;
662 if (isInstanceAlive && !spectral) {
664 level.forEachObjectInRect(x0, y0, width, height, &doObjectColAction);
667 if (isInstanceAlive && plr.isRectCollision(x0, y0, width, height)) {
668 doPlayerColAction(plr);
669 wasObjectCollision = true;
671 // this is done in `onItemHit`
672 //if (wasObjectCollision) obj.xVel = xVel*0.3;
676 if (doBreak && breaksOnCollision) {
684 depth = 101; // behind enemies (60)
687 setCollisionBounds(0, 0, 16, 16);
692 canBeHitByBullet = false;
699 // ////////////////////////////////////////////////////////////////////////// //
700 class ItemDice['oDice'] : MapItem;
704 None, // ready to roll
706 Finished, // landed in shop
707 Failed, // landed outside of the shop
711 bool pickedOutsideOfAShop;
714 override bool initialize () {
715 if (!::initialize()) return false;
717 value = global.randOther(1, 6);
723 final int getRollNumber () {
724 if (rollState != RollState.Finished) return 0;
729 final void resetRollState () {
730 rollState = RollState.None;
734 // various side effects
735 // called only if object was succesfully put into player hands
736 override void onPickedUp (PlayerPawn plr) {
737 pickedOutsideOfAShop = !level.isInShop(ix/16, iy/16);
738 if (rollState != RollState.Finished) rollState = RollState.None;
742 override void drawSignsWithOfs (int xpos, int ypos, int scale, float currFrameDelta) {
743 if (!forSale) return;
744 if ((rollState == RollState.Failed || rollState == RollState.None) && level.player.bet) {
746 getInterpCoords(currFrameDelta, scale, out xi, out yi);
747 auto spr = level.sprStore['sRedArrowDown'];
748 if (spr && spr.frames.length) {
749 auto spf = spr.frames[0];
750 spf.tex.blitAt(xi-xpos-spf.xofs*scale, yi-ypos-(12+spf.yofs)*scale, scale);
756 override void onCheckItemStolen (PlayerPawn plr) {
757 if (!heldBy || pickedOutsideOfAShop) return;
759 if (forSale && !level.player.bet && rollState != RollState.Failed) {
760 bool inShop = level.isInShop(ix/16, iy/16);
762 level.scrShopkeeperAnger(GameLevel::SCAnger.ItemStolen); // don't steal it!
768 override void thinkFrame () {
769 if (forSale && /*!forVending && cost > 0 &&*/ !level.hasAliveShopkeepers(skipAngry:true)) {
774 lightRadius = max(origLightRadius, (forSale && level.isInShop(ix/16, iy/16) ? 64 : 0));
778 if (oCharacter.facing == LEFT) x = oCharacter.x - 4;
779 else if (oCharacter.facing == RIGHT) x = oCharacter.x + 4;
782 if (oCharacter.state == DUCKING and abs(oCharacter.xVel) < 2) y = oCharacter.y; else y = oCharacter.y-2;
784 if (oCharacter.state == DUCKING and abs(oCharacter.xVel) < 2) y = oCharacter.y+4; else y = oCharacter.y+2;
788 if (oCharacter.holdItem == 0) held = false;
790 // stealing makes shopkeeper angry
791 //writeln("!!! fs=", forSale, "; bet=", level.player.bet, "; st=", rollState);
795 bool colLeft = !!isCollisionLeft(1);
796 bool colRight = !!isCollisionRight(1);
797 bool colBot = !!isCollisionBottom(1);
798 bool colTop = !!isCollisionTop(1);
800 if (!colBot && yVel < 6) yVel += myGrav;
802 if (fabs(xVel) < 0.1) xVel = 0;
803 else if (colLeft || colRight) xVel = -xVel*0.5;
807 if (yVel > 1) yVel = -yVel*bounceFactor; else yVel = 0;
809 if (fabs(xVel) < 0.1) xVel = 0;
810 else if (fabs(xVel) != 0) xVel *= frictionFactor;
811 if (fabs(yVel) < 1) {
813 if (!isCollisionBottom(1)) flty += 1;
819 if (!colRight) fltx += 1;
821 } else if (colRight) {
826 if (isCollisionTop(1)) {
827 if (yVel < 0) yVel = -yVel*0.8; else flty += 1;
830 //!depth = (global.hasSpectacles ? 0 : 101); //???
832 if (isCollisionInRect(ix-3, iy-3, 7, 7, &level.cbCollisionLava)) {
841 if (isCollisionAtPoint(ix, iy-5, &level.cbCollisionLava)) {
847 if (!isInstanceAlive || spectral) return;
849 if (fabs(xVel) > 3 || fabs(yVel) > 3) {
851 auto plr = level.player;
852 if (plr.isRectCollision(ix+enemyColX, iy+enemyColY, enemyColW, enemyColH)) {
853 doPlayerColAction(plr);
857 level.forEachObjectInRect(ix-2, iy-2, 5, 5, &doObjectColAction);
862 if (fabs(yVel) > 2 || fabs(xVel) > 2) {
863 setSprite('sDiceRoll');
864 value = global.randOther(1, 6);
866 case RollState.Finished:
868 if (level.player.bet > 0) level.scrShopkeeperAnger(GameLevel::SCAnger.CrapsCheated);
871 rollState = RollState.Rolling;
874 } else if (yVel == 0 && fabs(xVel <= 2) && isCollisionBottom(1)) {
877 case RollState.Rolling:
878 rollState = (level.isInShop(ix/16, iy/16) ? RollState.Finished : RollState.Failed);
879 if (rollState == RollState.Finished) level.player.onDieRolled(self);
881 case RollState.Finished:
882 case RollState.Failed:
885 default: rollState = RollState.None; break;
890 case 1: setSprite('sDice1'); break;
891 case 2: setSprite('sDice2'); break;
892 case 3: setSprite('sDice3'); break;
893 case 4: setSprite('sDice4'); break;
894 case 5: setSprite('sDice5'); break;
895 default: setSprite('sDice6'); break;
898 canPickUp = (rollState != RollState.Rolling);
905 desc2 = "A six-sided die. The storeowner talks to it every night before he goes to sleep.";
906 setCollisionBounds(-6, 0, 6, 8);
913 rollState = RollState.None;
914 bloodless = true; // just in case, lol
915 canHitEnemies = true;