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 // this is self-movable map object
19 class MapObject : MapEntity;
29 HANG, // for bats, spiders and monkeys
36 // constant states that the platform character may be
78 // megamouth, tomb lord
81 // player in transition level
119 bool hiddenTreasure; // true: cannot be seen without spectacles
123 transient SpriteImage spriteL;
124 transient SpriteImage spriteR; // can be empty
125 name spriteLName, spriteRName;
126 bool negateMirrorXOfs;
128 bool walkableSolid; // hack!
129 bool carryPlayer; // olmec
134 //MapObject alreadyHeld;
137 bool activeWhenHeld = false;
138 bool spectralWhenHeld = true;
140 bool sellingToShopAllowed = false;
141 bool fixedPrice = false;
142 bool sellOfferDone = false; // `true`, if selling was offered, next `PAY` will sell it
147 final MapObject heldBy { get { return mHeldBy; } }
150 #ifndef STANDALONE_MAP_ENTITY
151 // called after `heldBy` becomes `none` from something
152 // only for alive instances
153 void onHoldReset (MapObject oldholder) {
156 myGrav = myGravNorm; // stakes will reset gravity
157 spectral = false; // just in case
161 sellOfferDone = false;
162 //writeln("HOLD RESET FOR '", GetClassName(Class), "'");
163 if (mHeldBy) FatalError("WTF?!");
167 // called after `heldBy` becomes something `none`
168 // only for alive instances
169 void onHoldSet (MapObject newholder) {
170 //active = (self isa MapEnemy || armed);
171 active = activeWhenHeld;
172 spectral = spectralWhenHeld;
178 sellOfferDone = false;
179 //writeln("HOLD SET FOR '", GetClassName(Class), "' (activeWhenHeld=", activeWhenHeld, ")");
180 if (!mHeldBy) FatalError("WTF?!");
184 // called after `mHoldItem` becomes `none` from something
185 // only for alive instances
186 // note that `olditem` can be dead instance
187 // called after `onHoldReset()`
188 void onHoldItemReset (MapObject olditem) {
189 if (olditem) olditem.makeSafe();
193 // called after `mHoldItem` becomes something `none`
194 // only for alive instances
195 // called after `onHoldSet()`
196 // note that `newitem` can be dead instance in rare cases
197 void onHoldItemSet (MapObject newitem) {
198 if (newitem) newitem.makeSafe();
202 override void makeSafe () {
205 alarmDisarmSafe = 10;
213 // just in case: unhold dead instance
214 if (hi && !hi.isInstanceAlive) {
217 if (isInstanceAlive) onHoldItemReset(hi);
220 // just in case: don't hold dead instance
221 if (it && !it.isInstanceAlive) {
225 // already holding it?
228 if (it && it.mHeldBy != self) FatalError("something is VERY wrong with carrying item management");
231 // unheld current item
236 if (hi.isInstanceAlive) hi.onHoldReset(self);
237 if (isInstanceAlive) onHoldItemReset(hi);
244 if (it.isInstanceAlive) it.onHoldSet(self);
245 if (isInstanceAlive) onHoldItemSet(hi);
251 protected int hitboxX, hitboxY, hitboxW, hitboxH;
253 override int x0 () { return round(fltx)+hitboxX; }
254 override int y0 () { return round(flty)+hitboxY; }
255 override int width () { return hitboxW; }
256 override int height () { return hitboxH; }
260 float xDelta, yDelta;
261 float myGravNorm = 0.6;
262 float myGravWater = 0.2;
263 float bounceFactor = 0.5;
264 float frictionFactor = 0.3;
266 float xVelLimit = 16; // limits the xVel: default 15
267 float yVelLimit = 10; // limits the yVel
268 float xAccLimit = 9; // limits the xAcc
269 float yAccLimit = 6; // limits the yAcc
270 float runAcc = 3; // the running acceleration
276 int invincible; // counter
294 bool canBeHitByBullet;
299 //bool damselDropped;
310 bool cannotBeCarriedOnNextLevel;
311 bool allowWaterProcessing = true;
317 //string carries = "";
335 bool countsAsKill = true; // sometimes it's not the player's fault!
336 bool removeCorpse; // only applies to enemies that have corpses (Caveman, Yeti, etc.)
346 int damage = 1; // damage amount caused to player/enemy on touch
349 int deathTimer = 200; // how many steps after death until corpse is removed
354 //MapObject thrownBy; // "Yeti", "Hawkman", or "Shopkeeper" for stat tracking deaths by being thrown
355 name thrownBy; // name, 'cause this is the only thing we are interested in, and the object can die
358 const int whoaTimerMax = 30;
359 int distToNearestLightSource = int.max;
362 // ////////////////////////////////////////////////////////////////////////// //
363 #ifndef STANDALONE_MAP_ENTITY
364 override void onLoaded () {
366 if (spriteLName) spriteL = level.sprStore[spriteLName];
367 if (spriteRName) spriteR = level.sprStore[spriteRName];
371 void fixHoldCoords () {
375 final void forceFixHoldCoords (MapObject holder) {
377 if (mHeldBy == holder) return;
378 auto oldHolder = mHeldBy;
379 auto oldHolderItem = holder.mHoldItem;
381 holder.mHoldItem = self;
383 holder.mHoldItem = oldHolderItem;
389 // ////////////////////////////////////////////////////////////////////////// //
390 override void onDestroy () {
391 #ifndef STANDALONE_MAP_ENTITY
393 heldBy.holdItem = none;
400 #ifndef STANDALONE_MAP_ENTITY
401 // ////////////////////////////////////////////////////////////////////////// //
402 override void onOutOfLevel () {
403 if (canLiveOutsideOfLevel || heldBy) return;
404 if (yVel < -0.01) return; // it is flying up
405 // remove it only if it fallen down
406 if (y0 > level.tilesHeight*16+16) instanceRemove();
410 // ////////////////////////////////////////////////////////////////////////// //
411 // does only sprite collision, ignoring hitbox
412 private transient bool delegate (MapObject o) privNoDimsObjCheckerDG;
413 private transient bool delegate (MapTile t) privNoDimsTileCheckerDG;
415 final MapObject collideObjectsNoDims (optional bool delegate (MapObject o) dg) {
417 auto spf = getSpriteFrame(default, x0, y0, x1, y1);
418 if (x1 <= x0 || y1 <= y0) return none;
420 bool delegate (MapObject o) olddg = privNoDimsObjCheckerDG;
421 privNoDimsObjCheckerDG = dg;
424 auto obj = level.isObjectInRect(ix+x0, iy+y0, x1-x0, y1-y0, delegate bool (MapObject o) {
425 if (o.spectral) return false;
426 if (!o.active) return false;
427 if (o isa MapEnemy && o.dead) return false;
428 if (!o.collidesWith(self, ignoreDims:true)) return false;
429 if (!privNoDimsObjCheckerDG) return true;
430 return privNoDimsObjCheckerDG(o);
433 privNoDimsObjCheckerDG = olddg;
438 final MapTile collideTilesNoDims (optional bool delegate (MapTile o) dg) {
440 auto spf = getSpriteFrame(default, x0, y0, x1, y1);
441 if (x1 <= x0 || y1 <= y0) return none;
443 bool delegate (MapTile t) olddg = privNoDimsTileCheckerDG;
444 privNoDimsTileCheckerDG = dg;
447 auto obj = level.checkTilesInRect(ix+x0, iy+y0, x1-x0, y1-y0, delegate bool (MapTile t) {
448 if (t.spectral) return false;
449 if (!t.collidesWith(self, ignoreDims:true)) return false;
450 if (!privNoDimsTileCheckerDG) return t.solid; // default
451 return privNoDimsTileCheckerDG(t);
454 privNoDimsTileCheckerDG = olddg;
460 // ////////////////////////////////////////////////////////////////////////// //
462 void generateCost () {
463 if (cost <= 0) cost = 100;
464 if (global.currLevel > 2) cost += (cost/100)*10*(global.currLevel-2);
468 override bool initialize () {
469 if (!::initialize()) return false;
470 if (sellingToShopAllowed) {
471 if (cost <= 0 || !fixedPrice) generateCost();
476 // ////////////////////////////////////////////////////////////////////////// //
477 final void scrCreateBlood (int x, int y, int amount) {
478 //if (bloodless) return;
479 foreach (; 0..amount) {
480 level.MakeMapObject(x, y, 'oBlood');
485 final void scrCreateFlame (int x, int y, int amount) {
486 foreach (; 0..amount) {
487 level.MakeMapObject(x, y, 'oFlame');
492 final void scrCreateBloblets (int px, int py, int count) {
493 foreach (; 0..count) level.MakeMapObject(px, py, 'oBloblet');
498 // ////////////////////////////////////////////////////////////////////////// //
499 final void setCollisionBounds (int hx0, int hy0, int hx1, int hy1) {
502 hitboxW = max(0, hx1-hx0);
503 hitboxH = max(0, hy1-hy0);
507 final void setCollisionBoundsFromFrame () {
508 int fx0, fy0, fx1, fy1;
509 if (getSpriteFrame(default, out fx0, out fy0, out fx1, out fy1)) {
510 setCollisionBounds(fx0, fy0, fx1, fy1);
515 // ////////////////////////////////////////////////////////////////////////// //
516 void spillBlood (optional int amount) {
517 if (!bloodless && bloodLeft > 0) {
518 if (!specified_amount) amount = 1;
519 if (amount < 0) return;
520 //auto spf = getSpriteFrame();
521 //scrCreateBlood(ix+(spf ? spf.width/2 : 0), iy+(spf ? spf.height/2 : 0), amount);
522 scrCreateBlood(xCenter, yCenter, amount);
523 if (hp <= 0) bloodLeft -= 1;
528 // ////////////////////////////////////////////////////////////////////////// //
529 bool checkAndPerformSacrifice () {
530 if (!heldBy && fabs(xVel) < 0.001 && fabs(yVel) < 0.001) {
531 auto myAltar = isCollisionAtPoint(ix+8, iy+16, &level.cbCollisionSacAltar);
532 if (myAltar && canBeSacrificed(myAltar)) {
536 sacCount = default.sacCount;
537 if (onSacrificed(myAltar) || !isInstanceAlive) return true;
540 sacCount = default.sacCount;
543 sacCount = default.sacCount;
549 // ////////////////////////////////////////////////////////////////////////// //
551 void nudgeIt (int nx, int ny, optional bool forced) {
556 void onBulletHit (ObjBullet bullet) {
561 // called when player is trying to pick up something
562 // return `true` if this entity can be picekd up
563 // this is used in search loop, it is called before `onTryPickup()`
564 // DON'T do any actions here, this should be a pure checker!
565 // not called for inactive or spectral objects
566 bool onCanBePickedUp (PlayerPawn plr) { return false; }
570 // return `true` to stop player from holding it
571 bool onTryPickup (PlayerPawn plr) {
577 // various side effects
578 // called only if object was succesfully put into player hands
579 void onPickedUp (PlayerPawn plr) {
580 if (!wasCollected) { wasCollected = true; level.addCollect(objName); }
585 // return `true` to stop player from throwing it
586 bool onTryUseItem (PlayerPawn plr) {
592 void onBeforeThrowBy (PlayerPawn plr) {
596 #ifndef STANDALONE_MAP_ENTITY
597 // return `false` to prevent
598 // owner is usually a player
599 override bool onLostAsHeldItem (MapObject owner, LostCause cause, optional float xvel, optional float yvel) {
601 if (cause == LostCause.Whoa) {
602 xVel = (owner.dir == Dir.Left ? -2 : 2);
603 //!if (type == "Damsel") playSound(global.sndDamsel);
604 //!if (type == "Bow" and bowArmed) scrFireBow();
605 //!if (type == "Block Item") with (oBlockPreview) instance_destroy(); // YASM 1.8.1
607 if (type == pickupItemType || (type == "Shotgun" and pickupItemType == "Boomstick")) {
611 scrHoldItem(pickupItemType);
614 if (objName == 'GoldIdol') shiftY(-8); //FIXME: move to the respective class
616 } else if (cause == LostCause.Drop) {
619 //!if (bowArmed) scrFireBow();
621 if (pickupItemType != type) {
622 scrHoldItem(pickupItemType);
624 if (type == "Block Item") { with (oBlockPreview) instance_destroy(); }
629 } else if (cause == LostCause.Dead || cause == LostCause.Stunned) {
633 if (type == "Arrow") {
635 alarm[2] = 30; // prevent held arrow from hurting player as it flies out of hands
640 if (type == pickupItemType || (type == "Shotgun" and pickupItemType == "Boomstick")) {
644 scrHoldItem(pickupItemType);
648 if (specified_xvel) xVel = xvel;
649 if (specified_yvel) yVel = yvel;
650 if (objName == 'GoldIdol') shiftY(-8); //FIXME: move to the respective class
656 // ////////////////////////////////////////////////////////////////////////// //
657 override SpriteImage getSprite (optional out bool doMirror) {
659 if (spriteL || spriteR) {
660 SpriteImage spr = (dir == Dir.Left ? spriteL : spriteR);
662 spr = (dir != Dir.Left ? spriteL : spriteR);
663 if (spr && !disableMirror) doMirror = true;
672 override SpriteFrame getSpriteFrame (optional out bool doMirror, optional out int x0, optional out int y0, optional out int x1, optional out int y1) {
673 auto spr = getSprite(doMirror!optional);
674 if (!spr || spr.frames.length == 0) return none;
675 auto spf = spr.frames[trunc(imageFrame)%spr.frames.length];
676 if (!spf) return none;
677 if (specified_y0 || specified_y1) {
679 y1 = y0+spf.tex.height;
681 if (specified_x0 || specified_x1) {
682 if (!doMirror || !negateMirrorXOfs) {
684 x1 = x0+spf.tex.width;
687 x0 = x1-spf.tex.width;
694 override void clearSprite () {
704 #ifndef STANDALONE_MAP_ENTITY
705 override void setSprite (name sprNameL, optional name sprNameR) {
706 if (!sprNameL && !sprNameR) {
711 bool resetFrames = false;
713 if (sprNameL && sprNameR) {
714 if (!spriteL || spriteL.Name != sprNameL) {
715 spriteLName = sprNameL;
716 spriteL = level.sprStore[sprNameL];
717 resetFrames = (dir == Dir.Left);
719 if (!spriteR || spriteR.Name != sprNameR) {
720 spriteRName = sprNameR;
721 spriteR = level.sprStore[sprNameR];
722 resetFrames = (dir == Dir.Right);
724 } else if (sprNameL) {
725 if (!spriteL || spriteL.Name != sprNameL) {
726 spriteLName = sprNameL;
727 spriteL = level.sprStore[sprNameL];
728 resetFrames = (dir == Dir.Left);
733 resetFrames = (dir == Dir.Right);
735 } else if (sprNameR) {
736 if (!spriteR || spriteR.Name != sprNameR) {
737 spriteRName = sprNameR;
738 spriteR = level.sprStore[sprNameR];
739 resetFrames = (dir == Dir.Right);
744 resetFrames = (dir == Dir.Left);
756 // ////////////////////////////////////////////////////////////////////////// //
757 #ifndef STANDALONE_MAP_ENTITY
758 final void basicPhysicsStep () {
762 if (yVel > yVelLimit) yVel = yVelLimit;
763 if (!canLiveOutsideOfLevel && !heldBy && isOutsideOfLevel()) {
764 // oops, fallen out of level...
771 override void thinkFrame () {
772 if (!heldBy) basicPhysicsStep();
777 override void processAlarms () {
780 if (alarmNudge > 0) {
781 if (--alarmNudge == 0) nudged = false;
784 if (alarmDisarmSafe > 0) {
785 //writeln("DSF: ", alarmDisarmSafe);
786 if (--alarmDisarmSafe == 0) {
787 //writeln("DSF: ", alarmDisarmSafe);
794 // ////////////////////////////////////////////////////////////////////////// //
795 override void drawWithOfs (int xpos, int ypos, int scale, float currFrameDelta) {
797 getInterpCoords(currFrameDelta, scale, out xi, out yi);
800 int fx0, fy0, fx1, fy1;
801 auto spf = getSpriteFrame(out doMirror, out fx0, out fy0, out fx1, out fy1);
804 auto oclr = Video.color;
805 Video.color = oclr|(trunc(fclamp(255.0-255*imageAlpha, 0.0, 255.0))<<24);
807 fx0 = xi+fx0*scale-xpos;
808 fy0 = yi+fy0*scale-ypos;
809 fx1 = xi+fx1*scale-xpos;
810 fy1 = yi+fy1*scale-ypos;
812 spf.tex.blitExt(fx0, fy0, fx1, fy1, 0, 0, spf.tex.width, spf.tex.height, angle:imageAngle);
814 spf.tex.blitExt(fx0, fy0, fx1, fy1, spf.tex.width, 0, 0, spf.tex.height, angle:imageAngle);
818 #ifndef STANDALONE_MAP_ENTITY
821 Video.color = 0xff_ff_00;
822 Video.drawRect(x0*scale-xpos, y0*scale-ypos, width*scale, height*scale);
823 Video.color = 0x00_ff_00;
824 Video.drawRect(ix*scale-xpos, iy*scale-ypos, 2, 2);
827 Video.color = 0x3f_ff_00_00;
828 Video.fillRect(x0*scale-xpos, y0*scale-ypos, width*scale, height*scale);
842 // ////////////////////////////////////////////////////////////////////////// //
843 class MapObjectSpearsBase : MapObject abstract;
848 override void onAnimationLooped () {
853 final bool cbIsSpearTrap (MapTile t) { return (t isa MapTileSpearTrapBase); }
854 final bool isHitFrame () { return (imageFrame >= 20 && imageFrame < 24); }
855 final bool isLeft () { return (deltaX > 0); }
858 override void thinkFrame () {
860 if (!level.checkTileAtPoint(x+deltaX, y, &cbIsSpearTrap)) { instanceRemove(); return; }
864 level.isObjectInRect(ix+2, iy+2, 13, 13, delegate bool (MapObject o) {
865 if (!self.isInstanceAlive) return true;
866 //writeln("spear hit: '", GetClassName(o.Class), "'");
867 o.onSpearTrapHit(self);
880 setCollisionBounds(0, 0, 15, 15);
884 // ////////////////////////////////////////////////////////////////////////// //
885 class MapObjectSpearsLeft['oSpearsLeft'] : MapObjectSpearsBase;
887 override bool initialize () {
888 if (!::initialize()) return false;
889 setSprite(global.cityOfGold ? 'sSpearsLeftGold' : 'sSpearsLeft');
898 // ////////////////////////////////////////////////////////////////////////// //
899 class MapObjectSpearsRight['oSpearsRight'] : MapObjectSpearsBase;
901 override bool initialize () {
902 if (!::initialize()) return false;
903 setSprite(global.cityOfGold ? 'sSpearsRightGold' : 'sSpearsRight');
912 // ////////////////////////////////////////////////////////////////////////// //
913 class MapObjectSpringTrap['oSpringTrap'] : MapObject;
915 override bool initialize () {
916 if (!::initialize()) return false;
917 setSprite('sSpringTrap');
922 override void onAnimationLooped () {
923 if (status == SPRUNG) {
925 setSprite('sSpringTrap');
930 override bool onExplosionTouch (MapObject xplo) {
931 if (invincible) return false;
932 if (heldBy) return false;
938 bool activatedOnThisFrame;
940 override void thinkFrame () {
941 if (counter > 0) --counter;
942 if (!level.isSolidAtPoint(ix, iy+16)) { instanceRemove(); return; }
944 activatedOnThisFrame = false;
945 if (status == IDLE && counter == 0) {
947 auto plr = level.player;
948 if (abs(plr.ix-(ix+8)) < 6) {
949 if (plr.status <= LOOKING_UP && !plr.isExitingSprite()) {
950 if (plr.collidesWith(self)) {
951 activatedOnThisFrame = true;
960 level.isObjectInRect(ix, iy, 16, 16, delegate bool (MapObject o) {
961 if (!self.isInstanceAlive) return true;
962 if (o.heldBy) return false;
963 if (!o.collidesWith(self)) return false;
965 auto enemy = MapEnemy(o);
967 if (!o.flying && abs(o.ix-ix) < 6) {
970 if (o.dir == Dir.Left) o.xVel -= 1; else o.xVel += 1;
971 activatedOnThisFrame = true;
976 auto item = MapItem(o);
980 activatedOnThisFrame = true;
984 auto dms = MonsterDamsel(o);
988 if (o.dir == Dir.Left) o.xVel -= 1; else o.xVel += 1;
989 activatedOnThisFrame = true;
997 if (activatedOnThisFrame) {
998 setSprite('sSpringTrapSprung');
999 playSound('sndBoing');
1009 desc2 = "Anything that steps on it will be flung high into the air.";
1010 depth = 98; // before tiles
1012 setCollisionBounds(0, 0, 15, 15);