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 // this is self-movable map object
20 class MapObject : MapEntity;
30 HANG, // for bats, spiders and monkeys
37 // constant states that the platform character may be
79 // megamouth, tomb lord
82 // player in transition level
84 STOPPED_TUNNEL, // tunnel man
121 bool hiddenTreasure; // true: cannot be seen without spectacles
125 transient SpriteImage spriteL;
126 transient SpriteImage spriteR; // can be empty
127 name spriteLName, spriteRName;
128 bool negateMirrorXOfs;
130 bool carryPlayer; // olmec
137 //MapObject alreadyHeld;
140 bool activeWhenHeld = false;
141 bool spectralWhenHeld = true;
143 bool sellingToShopAllowed = false;
144 bool fixedPrice = false;
145 bool sellOfferDone = false; // `true`, if selling was offered, next `PAY` will sell it
150 final MapObject heldBy { get { return mHeldBy; } }
153 #ifndef STANDALONE_MAP_ENTITY
154 // called after `heldBy` becomes `none` from something
155 // only for alive instances
156 void onHoldReset (MapObject oldholder) {
159 myGrav = myGravNorm; // stakes will reset gravity
160 spectral = false; // just in case
164 sellOfferDone = false;
166 //writeln("HOLD RESET FOR '", GetClassName(Class), "'");
167 if (mHeldBy) FatalError("WTF?!");
171 // called after `heldBy` becomes something `none`
172 // only for alive instances
173 void onHoldSet (MapObject newholder) {
174 //active = (self isa MapEnemy || armed);
175 active = activeWhenHeld;
176 spectral = spectralWhenHeld;
182 sellOfferDone = false;
183 //writeln("HOLD SET FOR '", GetClassName(Class), "' (activeWhenHeld=", activeWhenHeld, ")");
185 if (!mHeldBy) FatalError("WTF?!");
189 // called after `mHoldItem` becomes `none` from something
190 // only for alive instances
191 // note that `olditem` can be dead instance
192 // called after `onHoldReset()`
193 void onHoldItemReset (MapObject olditem) {
194 if (olditem) olditem.makeSafe();
198 // called after `mHoldItem` becomes something `none`
199 // only for alive instances
200 // called after `onHoldSet()`
201 // note that `newitem` can be dead instance in rare cases
202 void onHoldItemSet (MapObject newitem) {
203 if (newitem) newitem.makeSafe();
207 override void makeSafe () {
210 alarmDisarmSafe = 10;
218 // just in case: unhold dead instance
219 if (hi && !hi.isInstanceAlive) {
222 if (isInstanceAlive) onHoldItemReset(hi);
225 // just in case: don't hold dead instance
226 if (it && !it.isInstanceAlive) {
230 // already holding it?
233 if (it && it.mHeldBy != self) FatalError("something is VERY wrong with carrying item management");
236 // unheld current item
241 if (hi.isInstanceAlive) hi.onHoldReset(self);
242 if (isInstanceAlive) onHoldItemReset(hi);
249 if (it.isInstanceAlive) it.onHoldSet(self);
250 if (isInstanceAlive) onHoldItemSet(hi);
256 protected int hitboxX, hitboxY, hitboxW, hitboxH;
258 override int x0 () { return roundi(fltx)+hitboxX; }
259 override int y0 () { return roundi(flty)+hitboxY; }
260 override int width () { return hitboxW; }
261 override int height () { return hitboxH; }
265 float xDelta, yDelta;
266 float myGravNorm = 0.6;
267 float myGravWater = 0.2;
268 float bounceFactor = 0.5;
269 float frictionFactor = 0.3;
271 float xVelLimit = 16; // limits the xVel: default 15
272 float yVelLimit = 10; // limits the yVel
273 float xAccLimit = 9; // limits the xAcc
274 float yAccLimit = 6; // limits the yAcc
275 float runAcc = 3; // the running acceleration
281 int invincible; // counter
283 name shopType; // for items: in which shop it is placed? for player: last visited shop
299 bool canBeHitByBullet;
304 //bool damselDropped;
315 bool cannotBeCarriedOnNextLevel;
316 bool allowWaterProcessing = true;
322 //string carries = "";
330 int counter; // usually for stun
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.)
343 int bloodOfsX = int.min, bloodOfsY = int.min; // this means "center"
347 int damage = 1; // damage amount caused to player/enemy on touch
350 int deathTimer = 200; // how many steps after death until corpse is removed
355 //MapObject thrownBy; // "Yeti", "Hawkman", or "Shopkeeper" for stat tracking deaths by being thrown
356 name thrownBy; // name, 'cause this is the only thing we are interested in, and the object can die
359 const int whoaTimerMax = 30;
360 int distToNearestLightSource = int.max;
363 // ////////////////////////////////////////////////////////////////////////// //
364 #ifndef STANDALONE_MAP_ENTITY
365 override void onLoaded () {
367 if (spriteLName) spriteL = level.sprStore[spriteLName];
368 if (spriteRName) spriteR = level.sprStore[spriteRName];
372 void fixHoldCoords () {
376 final void forceFixHoldCoords (MapObject holder) {
378 if (mHeldBy == holder) return;
379 auto oldHolder = mHeldBy;
380 auto oldHolderItem = holder.mHoldItem;
382 holder.mHoldItem = self;
384 holder.mHoldItem = oldHolderItem;
390 // ////////////////////////////////////////////////////////////////////////// //
391 override void onDestroy () {
392 #ifndef STANDALONE_MAP_ENTITY
394 heldBy.holdItem = none;
401 #ifndef STANDALONE_MAP_ENTITY
402 // ////////////////////////////////////////////////////////////////////////// //
403 override void onOutOfLevel () {
404 if (canLiveOutsideOfLevel || heldBy) return;
405 if (yVel < -0.01) return; // it is flying up
406 // remove it only if it fallen down
407 if (y0 > level.tilesHeight*16+16) instanceRemove();
411 // ////////////////////////////////////////////////////////////////////////// //
412 // does only sprite collision, ignoring hitbox
413 private transient bool delegate (MapObject o) privNoDimsObjCheckerDG;
414 private transient bool delegate (MapTile t) privNoDimsTileCheckerDG;
416 final MapObject collideObjectsNoDims (optional scope bool delegate (MapObject o) dg) {
418 /*auto spf =*/ getSpriteFrame(default, x0, y0, x1, y1);
419 if (x1 <= x0 || y1 <= y0) return none;
421 bool delegate (MapObject o) olddg = privNoDimsObjCheckerDG;
422 privNoDimsObjCheckerDG = dg;
425 auto obj = level.isObjectInRect(ix+x0, iy+y0, x1-x0, y1-y0, delegate bool (MapObject o) {
426 if (o.spectral) return false;
427 if (!o.active) return false;
428 //if (o isa MapEnemy && o.dead) return false;
429 if (!o.collidesWith(self, ignoreDims:true)) return false;
430 if (!privNoDimsObjCheckerDG) return true;
431 return privNoDimsObjCheckerDG(o);
434 privNoDimsObjCheckerDG = olddg;
439 final MapTile collideTilesNoDims (optional scope bool delegate (MapTile o) dg) {
441 /*auto spf =*/ getSpriteFrame(default, x0, y0, x1, y1);
442 if (x1 <= x0 || y1 <= y0) return none;
444 bool delegate (MapTile t) olddg = privNoDimsTileCheckerDG;
445 privNoDimsTileCheckerDG = dg;
448 auto obj = level.checkTilesInRect(ix+x0, iy+y0, x1-x0, y1-y0, delegate bool (MapTile t) {
449 if (t.spectral) return false;
450 if (!t.collidesWith(self, ignoreDims:true)) return false;
451 if (!privNoDimsTileCheckerDG) return t.solid; // default
452 return privNoDimsTileCheckerDG(t);
455 privNoDimsTileCheckerDG = olddg;
461 // ////////////////////////////////////////////////////////////////////////// //
463 void generateCost () {
464 if (cost <= 0) cost = 100;
465 if (global.currLevel > 2) cost += (cost/100)*10*(global.currLevel-2);
469 override bool initialize () {
470 if (!::initialize()) return false;
471 if (sellingToShopAllowed) {
472 if (cost > 0 && !fixedPrice) generateCost();
477 // ////////////////////////////////////////////////////////////////////////// //
478 final void scrCreateBlood (int x, int y, int amount) {
479 //if (bloodless) return;
480 if (amount <= 0) return;
481 foreach (; 0..amount) {
482 auto blood = level.MakeMapObject(x, y, 'oBlood');
483 if (blood && xVel != 0) {
484 if ((xVel < 0 && blood.xVel > 0) || (xVel > 0 && blood.xVel < 0)) blood.xVel = -blood.xVel;
485 blood.xVel += fclamp(xVel, -1, 1);
491 final void scrCreateFlame (int x, int y, int amount) {
492 foreach (; 0..amount) {
493 level.MakeMapObject(x, y, 'oFlame');
498 final void scrCreateBloblets (int px, int py, int count) {
499 foreach (; 0..count) level.MakeMapObject(px, py, 'oBloblet');
504 // ////////////////////////////////////////////////////////////////////////// //
505 final void setCollisionBounds (int hx0, int hy0, int hx1, int hy1) {
508 hitboxW = max(0, hx1-hx0);
509 hitboxH = max(0, hy1-hy0);
513 final void setCollisionBoundsFromFrame () {
514 int fx0, fy0, fx1, fy1;
515 if (getSpriteFrame(default, out fx0, out fy0, out fx1, out fy1)) {
516 setCollisionBounds(fx0, fy0, fx1, fy1);
521 // ////////////////////////////////////////////////////////////////////////// //
522 void spillBlood (optional int amount, optional bool forced) {
523 if (!bloodless && (forced || bloodLeft > 0)) {
524 if (!specified_amount) amount = 3;
525 if (amount <= 0) return;
526 int x = (bloodOfsY != int.min ? ix+bloodOfsX : xCenter);
527 int y = (bloodOfsX != int.min ? iy+bloodOfsY : yCenter);
528 scrCreateBlood(x, y, amount);
529 if (hp <= 0) bloodLeft -= 1;
534 // ////////////////////////////////////////////////////////////////////////// //
535 bool checkAndPerformSacrifice (optional out bool onTheAltar) {
537 if (!heldBy && fabs(xVel) < 0.001 && fabs(yVel) < 0.001) {
538 auto myAltar = isCollisionAtPoint(ix+8, iy+16, &level.cbCollisionSacAltar);
539 onTheAltar = !!myAltar;
540 if (myAltar && canBeSacrificed(myAltar)) {
544 sacCount = default.sacCount;
545 if (onSacrificed(myAltar) || !isInstanceAlive) return true;
548 sacCount = default.sacCount;
551 sacCount = default.sacCount;
557 // ////////////////////////////////////////////////////////////////////////// //
559 void nudgeIt (int nx, int ny, optional bool forced) {
564 void onBulletHit (ObjBullet bullet) {
569 // called when player is trying to pick up something
570 // return `true` if this entity can be picekd up
571 // this is used in search loop, it is called before `onTryPickup()`
572 // DON'T do any actions here, this should be a pure checker!
573 // not called for inactive or spectral objects
574 bool onCanBePickedUp (PlayerPawn plr) { return false; }
578 // return `true` to stop player from holding it
579 bool onTryPickup (PlayerPawn plr) {
585 // various side effects
586 // called only if object was succesfully put into player hands
587 void onPickedUp (PlayerPawn plr) {
588 if (!wasCollected) { wasCollected = true; level.addCollect(objName); }
593 // return `true` to stop player from throwing it
594 bool onTryUseItem (PlayerPawn plr) {
600 void onBeforeThrowBy (PlayerPawn plr) {
604 #ifndef STANDALONE_MAP_ENTITY
605 // return `false` to prevent
606 // owner is usually a player
607 override bool onLostAsHeldItem (MapObject owner, LostCause cause, optional float xvel, optional float yvel) {
609 if (cause == LostCause.Whoa) {
610 xVel = (owner.dir == Dir.Left ? -2 : 2);
611 //!if (type == "Damsel") playSound(global.sndDamsel);
612 //!if (type == "Bow" and bowArmed) scrFireBow();
613 //!if (type == "Block Item") with (oBlockPreview) instance_destroy(); // YASM 1.8.1
615 if (type == pickupItemType || (type == "Shotgun" and pickupItemType == "Boomstick")) {
619 scrHoldItem(pickupItemType);
622 if (objName == 'GoldIdol') shiftY(-8); //FIXME: move to the respective class
624 } else if (cause == LostCause.Drop) {
627 //!if (bowArmed) scrFireBow();
629 if (pickupItemType != type) {
630 scrHoldItem(pickupItemType);
632 if (type == "Block Item") { with (oBlockPreview) instance_destroy(); }
637 } else if (cause == LostCause.Dead || cause == LostCause.Stunned) {
641 if (type == "Arrow") {
643 alarm[2] = 30; // prevent held arrow from hurting player as it flies out of hands
648 if (type == pickupItemType || (type == "Shotgun" and pickupItemType == "Boomstick")) {
652 scrHoldItem(pickupItemType);
656 if (specified_xvel) xVel = xvel;
657 if (specified_yvel) yVel = yvel;
658 if (objName == 'GoldIdol') shiftY(-8); //FIXME: move to the respective class
664 // ////////////////////////////////////////////////////////////////////////// //
665 override SpriteImage getSprite (optional out bool doMirror) {
667 if (spriteL || spriteR) {
668 SpriteImage spr = (dir == Dir.Left ? spriteL : spriteR);
670 spr = (dir != Dir.Left ? spriteL : spriteR);
671 if (spr && !disableMirror) doMirror = true;
680 override SpriteFrame getSpriteFrame (optional out bool doMirror, optional out int x0, optional out int y0, optional out int x1, optional out int y1) {
681 auto spr = getSprite(doMirror!optional);
682 if (!spr || spr.frames.length == 0) return none;
683 auto spf = spr.frames[trunci(imageFrame)%spr.frames.length];
684 if (!spf) return none;
685 if (specified_y0 || specified_y1) {
687 y1 = y0+spf.tex.height;
689 if (specified_x0 || specified_x1) {
690 if (!doMirror || !negateMirrorXOfs) {
692 x1 = x0+spf.tex.width;
695 x0 = x1-spf.tex.width;
702 override void clearSprite () {
712 #ifndef STANDALONE_MAP_ENTITY
713 override void setSprite (name sprNameL, optional name sprNameR) {
714 if (!sprNameL && !sprNameR) {
719 bool resetFrames = false;
721 if (sprNameL && sprNameR) {
722 if (!spriteL || spriteL.Name != sprNameL) {
723 spriteLName = sprNameL;
724 spriteL = level.sprStore[sprNameL];
725 resetFrames = (dir == Dir.Left);
727 if (!spriteR || spriteR.Name != sprNameR) {
728 spriteRName = sprNameR;
729 spriteR = level.sprStore[sprNameR];
730 resetFrames = (dir == Dir.Right);
732 } else if (sprNameL) {
733 if (!spriteL || spriteL.Name != sprNameL) {
734 spriteLName = sprNameL;
735 spriteL = level.sprStore[sprNameL];
736 resetFrames = (dir == Dir.Left);
741 resetFrames = (dir == Dir.Right);
743 } else if (sprNameR) {
744 if (!spriteR || spriteR.Name != sprNameR) {
745 spriteRName = sprNameR;
746 spriteR = level.sprStore[sprNameR];
747 resetFrames = (dir == Dir.Right);
752 resetFrames = (dir == Dir.Left);
764 // ////////////////////////////////////////////////////////////////////////// //
765 #ifndef STANDALONE_MAP_ENTITY
766 final void basicPhysicsStep () {
770 if (yVel > yVelLimit) yVel = yVelLimit;
771 if (!canLiveOutsideOfLevel && !heldBy && isOutsideOfLevel()) {
772 // oops, fallen out of level...
779 override void thinkFrame () {
780 if (!heldBy) basicPhysicsStep();
785 override void processAlarms () {
788 if (alarmNudge > 0) {
789 if (--alarmNudge == 0) nudged = false;
792 if (alarmDisarmSafe > 0) {
793 //writeln("DSF: ", alarmDisarmSafe);
794 if (--alarmDisarmSafe == 0) {
795 //writeln("DSF: ", alarmDisarmSafe);
802 // ////////////////////////////////////////////////////////////////////////// //
803 override void drawWithOfs (int xpos, int ypos, int scale, float currFrameDelta) {
805 getInterpCoords(currFrameDelta, scale, out xi, out yi);
808 int fx0, fy0, fx1, fy1;
809 auto spf = getSpriteFrame(out doMirror, out fx0, out fy0, out fx1, out fy1);
812 auto oclr = GLVideo.color;
813 GLVideo.color = oclr|(trunci(fclamp(255.0-255*imageAlpha, 0.0, 255.0))<<24);
815 fx0 = xi+fx0*scale-xpos;
816 fy0 = yi+fy0*scale-ypos;
817 fx1 = xi+fx1*scale-xpos;
818 fy1 = yi+fy1*scale-ypos;
820 spf.tex.blitExt(fx0, fy0, fx1, fy1, 0, 0, spf.width, spf.height, angle:imageAngle);
822 spf.tex.blitExt(fx0, fy0, fx1, fy1, spf.width, 0, 0, spf.height, angle:imageAngle);
824 GLVideo.color = oclr;
826 #ifndef STANDALONE_MAP_ENTITY
828 oclr = GLVideo.color;
829 GLVideo.color = 0xff_ff_00;
830 GLVideo.drawRect(x0*scale-xpos, y0*scale-ypos, width*scale, height*scale);
831 GLVideo.color = 0x00_ff_00;
832 GLVideo.drawRect(ix*scale-xpos, iy*scale-ypos, 2, 2);
835 GLVideo.color = 0x3f_ff_00_00;
836 GLVideo.fillRect(x0*scale-xpos, y0*scale-ypos, width*scale, height*scale);
839 GLVideo.color = oclr;