restored seaweed in water
[k8vacspelynky.git] / mapent / MapObject.vc
blob117e3080fb2b6364231649f44a3113579c600495
1 /**********************************************************************************
2  * Copyright (c) 2008, 2009 Derek Yu and Mossmouth, LLC
3  * Copyright (c) 2018, Ketmar Dark
4  *
5  * This file is part of Spelunky.
6  *
7  * You can redistribute and/or modify Spelunky, including its source code, under
8  * the terms of the Spelunky User License.
9  *
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.
12  *
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/>
16  *
17  **********************************************************************************/
18 // this is self-movable map object
19 class MapObject : MapEntity;
21 enum {
22   // olmec
23   START2 = -2,
24   START1 = -1,
26   IDLE = 0,
27   WALK,
28   ATTACK,
29   HANG, // for bats, spiders and monkeys
30   BOUNCE,
31   RECOVER,
32   DROWNED,
33   CRAWL,
34   SQUIRT,
36   // constant states that the platform character may be
37   STANDING = 10,
38   RUNNING,
39   DUCKING,
40   LOOKING_UP,
41   CLIMBING,
42   JUMPING,
43   FALLING,
44   DYING,
45   LEFT,
46   RIGHT,
47   ON_GROUND,
48   IN_AIR,
49   ON_LADDER,
50   HANGING,
51   DUCKTOHANG,
53   // damsel states
54   RUN,
55   THROWN,
56   YELL,
57   EXIT,
58   SLAVE,
59   KISS,
61   // shopkeeper
62   THROW,
63   PATROL,
64   FOLLOW,
66   // vampire
67   FLY,
69   // green frog
70   SPIT1,
71   SPIT2,
72   SPIT3,
74   // piranha
75   PAUSE,
76   ATTACK_ENEMY,
78   // megamouth, tomb lord
79   TURN,
81   // player in transition level
82   STOPPED,
84   // monkey
85   CLIMB,
86   GRAB,
88   // spring trap
89   SPRUNG,
91   // ufo
92   SEARCH,
93   DESTROY,
94   BLAST,
96   // alien eject
97   EJECT,
98   DEPLOY,
99   FLOAT,
101   // olmec
102   PREPARE,
103   SLAM,
104   CREATE,
105   DROWNING,
107   // mantrap
108   SLEEPY = 96,
109   EATING = 97,
110   STUNNED = 98,
111   DEAD = 99,
113   // look
114   UP = 101,
115   DOWN = 102,
119 bool hiddenTreasure; // true: cannot be seen without spectacles
120 MapTile ownerTile;
123 transient SpriteImage spriteL;
124 transient SpriteImage spriteR; // can be empty
125 name spriteLName, spriteRName;
126 bool negateMirrorXOfs;
127 bool disableMirror;
128 bool walkableSolid; // hack!
129 bool carryPlayer; // olmec
132 MapObject mHeldBy;
133 MapObject mHoldItem;
134 //MapObject alreadyHeld;
135 int savedDepth;
136 int holdDepth = 1;
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
144 string shopDesc;
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) {
154   active = true;
155   visible = true;
156   myGrav = myGravNorm; // stakes will reset gravity
157   spectral = false; // just in case
158   imageAngle = 0;
159   // holding depth
160   depth = savedDepth;
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;
173   visible = true;
174   imageAngle = 0;
175   // holding depth
176   savedDepth = depth;
177   depth = 1;
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 () {
203   ::makeSafe();
204   safe = true;
205   alarmDisarmSafe = 10;
209 MapObject holdItem {
210   get mHoldItem;
211   set(it) {
212     auto hi = mHoldItem;
213     // just in case: unhold dead instance
214     if (hi && !hi.isInstanceAlive) {
215       hi.mHeldBy = none;
216       mHoldItem = none;
217       if (isInstanceAlive) onHoldItemReset(hi);
218       hi = none;
219     }
220     // just in case: don't hold dead instance
221     if (it && !it.isInstanceAlive) {
222       it.mHeldBy = none;
223       it = none;
224     }
225     // already holding it?
226     if (it == hi) {
227       // just in case
228       if (it && it.mHeldBy != self) FatalError("something is VERY wrong with carrying item management");
229       return;
230     }
231     // unheld current item
232     if (hi) {
233       hi.mHeldBy = none;
234       mHoldItem = none;
235       fixHoldCoords();
236       if (hi.isInstanceAlive) hi.onHoldReset(self);
237       if (isInstanceAlive) onHoldItemReset(hi);
238     }
239     // and take new one
240     mHoldItem = it;
241     if (it) {
242       it.mHeldBy = self;
243       fixHoldCoords();
244       if (it.isInstanceAlive) it.onHoldSet(self);
245       if (isInstanceAlive) onHoldItemSet(hi);
246     }
247   }
249 #endif
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; }
259 float xAcc, yAcc;
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
272 Dir dir = Dir.Left;
275 // object flags
276 int invincible; // counter
277 int alarmNudge;
278 name shopType;
280 bool persist = true;
281 bool bounced;
282 bool stunned;
283 bool dead;
284 //bool cleanDeath;
285 bool flying;
286 bool heavy;
287 bool bounce;
288 bool bounceTop;
289 bool collectible;
291 bool stuck;
292 bool nudged;
294 bool canBeHitByBullet;
295 bool canBeNudged;
297 bool inDiceHouse;
298 bool forSale;
299 //bool damselDropped;
300 bool isTreasure;
301 bool canCollect;
303 // items
304 //bool held;
305 bool armed;
306 bool trigger;
307 bool safe;
308 bool sticky;
309 bool canPickUp;
310 bool cannotBeCarriedOnNextLevel;
311 bool allowWaterProcessing = true;
313 int alarmDisarmSafe;
314 int cost;
315 int value;
316 int resaleValue;
317 //string carries = "";
318 //string holds = "";
319 int favor = 1;
320 int sacCount = 20;
322 // enemy and player
323 int hp;
324 int status;
325 int counter;
327 //int whipped;
329 //int shakeCounter;
330 //int shakeToggle;
332 bool bloodless;
333 bool swimming;
334 bool inWeb;
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.)
337 bool meGoldMonkey;
338 bool dying;
339 bool cursed;
340 bool wasCollected;
342 int bloodLeft = 4;
343 int burning;
344 int sightCounter;
345 int stunTime = 200;
346 int damage = 1; // damage amount caused to player/enemy on touch
347 int burnTimer;
349 int deathTimer = 200; // how many steps after death until corpse is removed
351 int fallTimer;
352 int stunTimer;
353 int wallHurt;
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
356 int pushTimer;
357 int whoaTimer;
358 const int whoaTimerMax = 30;
359 int distToNearestLightSource = int.max;
362 // ////////////////////////////////////////////////////////////////////////// //
363 #ifndef STANDALONE_MAP_ENTITY
364 override void onLoaded () {
365   ::onLoaded();
366   if (spriteLName) spriteL = level.sprStore[spriteLName];
367   if (spriteRName) spriteR = level.sprStore[spriteRName];
371 void fixHoldCoords () {
375 final void forceFixHoldCoords (MapObject holder) {
376   if (!holder) return;
377   if (mHeldBy == holder) return;
378   auto oldHolder = mHeldBy;
379   auto oldHolderItem = holder.mHoldItem;
380   mHeldBy = holder;
381   holder.mHoldItem = self;
382   fixHoldCoords();
383   holder.mHoldItem = oldHolderItem;
384   mHeldBy = oldHolder;
386 #endif
389 // ////////////////////////////////////////////////////////////////////////// //
390 override void onDestroy () {
391 #ifndef STANDALONE_MAP_ENTITY
392   if (heldBy) {
393     heldBy.holdItem = none;
394     mHeldBy = none;
395   }
396 #endif
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) {
416   int x0, y0, x1, y1;
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;
422   auto spc = spectral;
423   spectral = true;
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);
431   });
432   spectral = spc;
433   privNoDimsObjCheckerDG = olddg;
435   return obj;
438 final MapTile collideTilesNoDims (optional bool delegate (MapTile o) dg) {
439   int x0, y0, x1, y1;
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;
445   auto spc = spectral;
446   spectral = true;
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);
452   });
453   spectral = spc;
454   privNoDimsTileCheckerDG = olddg;
456   return obj;
460 // ////////////////////////////////////////////////////////////////////////// //
461 // create cost
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();
472   }
473   return true;
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');
481   }
485 final void scrCreateFlame (int x, int y, int amount) {
486   foreach (; 0..amount) {
487     level.MakeMapObject(x, y, 'oFlame');
488   }
492 final void scrCreateBloblets (int px, int py, int count) {
493   foreach (; 0..count) level.MakeMapObject(px, py, 'oBloblet');
495 #endif
498 // ////////////////////////////////////////////////////////////////////////// //
499 final void setCollisionBounds (int hx0, int hy0, int hx1, int hy1) {
500   hitboxX = hx0;
501   hitboxY = hy0;
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);
511   }
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;
524   }
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)) {
533       if (sacCount > 0) {
534         --sacCount;
535       } else {
536         sacCount = default.sacCount;
537         if (onSacrificed(myAltar) || !isInstanceAlive) return true;
538       }
539     } else {
540       sacCount = default.sacCount;
541     }
542   } else {
543     sacCount = default.sacCount;
544   }
545   return false;
549 // ////////////////////////////////////////////////////////////////////////// //
550 // virtual
551 void nudgeIt (int nx, int ny, optional bool forced) {
555 // virtual
556 void onBulletHit (ObjBullet bullet) {
560 // virtual
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; }
569 // virtual
570 // return `true` to stop player from holding it
571 bool onTryPickup (PlayerPawn plr) {
572   return false;
576 // virtual
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); }
584 // virtual
585 // return `true` to stop player from throwing it
586 bool onTryUseItem (PlayerPawn plr) {
587   return false;
591 // virtual
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) {
600   resaleValue = 0;
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
606     /+!
607     if (type == pickupItemType || (type == "Shotgun" and pickupItemType == "Boomstick")) {
608       holdItem = 0;
609       pickupItemType = "";
610     } else {
611       scrHoldItem(pickupItemType);
612     }
613     +/
614     if (objName == 'GoldIdol') shiftY(-8); //FIXME: move to the respective class
615     return true;
616   } else if (cause == LostCause.Drop) {
617     visible = true;
618     resaleValue = 0;
619     //!if (bowArmed) scrFireBow();
620     /*
621     if (pickupItemType != type) {
622       scrHoldItem(pickupItemType);
623     } else {
624       if (type == "Block Item") { with (oBlockPreview) instance_destroy(); }
625       holdItem = 0;
626       pickupItemType = "";
627     }
628     */
629   } else if (cause == LostCause.Dead || cause == LostCause.Stunned) {
630     visible = true;
632     /*!
633     if (type == "Arrow") {
634       safe = true;
635       alarm[2] = 30; // prevent held arrow from hurting player as it flies out of hands
636     }
637     */
639     /+!
640     if (type == pickupItemType || (type == "Shotgun" and pickupItemType == "Boomstick")) {
641       holdItem = 0;
642       pickupItemType = "";
643     } else {
644       scrHoldItem(pickupItemType);
645     }
646     +/
647   }
648   if (specified_xvel) xVel = xvel;
649   if (specified_yvel) yVel = yvel;
650   if (objName == 'GoldIdol') shiftY(-8); //FIXME: move to the respective class
651   return true;
653 #endif
656 // ////////////////////////////////////////////////////////////////////////// //
657 override SpriteImage getSprite (optional out bool doMirror) {
658   doMirror = false;
659   if (spriteL || spriteR) {
660     SpriteImage spr = (dir == Dir.Left ? spriteL : spriteR);
661     if (!spr) {
662       spr = (dir != Dir.Left ? spriteL : spriteR);
663       if (spr && !disableMirror) doMirror = true;
664     }
665     return spr;
666   }
667   return none;
671 // x1, y1: exclusive
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) {
678     y0 = -spf.yofs;
679     y1 = y0+spf.tex.height;
680   }
681   if (specified_x0 || specified_x1) {
682     if (!doMirror || !negateMirrorXOfs) {
683       x0 = -spf.xofs;
684       x1 = x0+spf.tex.width;
685     } else {
686       x1 = spf.xofs;
687       x0 = x1-spf.tex.width;
688     }
689   }
690   return spf;
694 override void clearSprite () {
695   spriteL = none;
696   spriteR = none;
697   spriteLName = '';
698   spriteRName = '';
699   imageFrame = 0;
700   imageLoopCount = 0;
704 #ifndef STANDALONE_MAP_ENTITY
705 override void setSprite (name sprNameL, optional name sprNameR) {
706   if (!sprNameL && !sprNameR) {
707     clearSprite();
708     return;
709   }
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);
718     }
719     if (!spriteR || spriteR.Name != sprNameR) {
720       spriteRName = sprNameR;
721       spriteR = level.sprStore[sprNameR];
722       resetFrames = (dir == Dir.Right);
723     }
724   } else if (sprNameL) {
725     if (!spriteL || spriteL.Name != sprNameL) {
726       spriteLName = sprNameL;
727       spriteL = level.sprStore[sprNameL];
728       resetFrames = (dir == Dir.Left);
729     }
730     if (spriteR) {
731       spriteRName = '';
732       spriteR = none;
733       resetFrames = (dir == Dir.Right);
734     }
735   } else if (sprNameR) {
736     if (!spriteR || spriteR.Name != sprNameR) {
737       spriteRName = sprNameR;
738       spriteR = level.sprStore[sprNameR];
739       resetFrames = (dir == Dir.Right);
740     }
741     if (spriteL) {
742       spriteLName = '';
743       spriteL = none;
744       resetFrames = (dir == Dir.Left);
745     }
746   }
748   if (resetFrames) {
749     imageFrame = 0;
750     imageLoopCount = 0;
751   }
753 #endif
756 // ////////////////////////////////////////////////////////////////////////// //
757 #ifndef STANDALONE_MAP_ENTITY
758 final void basicPhysicsStep () {
759   moveRel(xVel, yVel);
760   if (!flying) {
761     yVel += myGrav;
762     if (yVel > yVelLimit) yVel = yVelLimit;
763     if (!canLiveOutsideOfLevel && !heldBy && isOutsideOfLevel()) {
764       // oops, fallen out of level...
765       onOutOfLevel();
766     }
767   }
771 override void thinkFrame () {
772   if (!heldBy) basicPhysicsStep();
774 #endif
777 override void processAlarms () {
778   ::processAlarms();
779   // nudge
780   if (alarmNudge > 0) {
781     if (--alarmNudge == 0) nudged = false;
782   }
783   // safe
784   if (alarmDisarmSafe > 0) {
785     //writeln("DSF: ", alarmDisarmSafe);
786     if (--alarmDisarmSafe == 0) {
787       //writeln("DSF: ", alarmDisarmSafe);
788       safe = false;
789     }
790   }
794 // ////////////////////////////////////////////////////////////////////////// //
795 override void drawWithOfs (int xpos, int ypos, int scale, float currFrameDelta) {
796   int xi, yi;
797   getInterpCoords(currFrameDelta, scale, out xi, out yi);
799   bool doMirror;
800   int fx0, fy0, fx1, fy1;
801   auto spf = getSpriteFrame(out doMirror, out fx0, out fy0, out fx1, out fy1);
802   if (!spf) return;
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;
811   if (!doMirror) {
812     spf.tex.blitExt(fx0, fy0, fx1, fy1, 0, 0, spf.tex.width, spf.tex.height, angle:imageAngle);
813   } else {
814     spf.tex.blitExt(fx0, fy0, fx1, fy1, spf.tex.width, 0, 0, spf.tex.height, angle:imageAngle);
815   }
816   Video.color = oclr;
818 #ifndef STANDALONE_MAP_ENTITY
819   if (false) {
820     oclr = Video.color;
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);
826     if (isCollision()) {
827       Video.color = 0x3f_ff_00_00;
828       Video.fillRect(x0*scale-xpos, y0*scale-ypos, width*scale, height*scale);
829     }
831     Video.color = oclr;
832   }
833 #endif
837 defaultproperties {
838   active = true;
842 // ////////////////////////////////////////////////////////////////////////// //
843 class MapObjectSpearsBase : MapObject abstract;
845 int deltaX;
848 override void onAnimationLooped () {
849   instanceRemove();
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 () {
859   int x = ix, y = iy;
860   if (!level.checkTileAtPoint(x+deltaX, y, &cbIsSpearTrap)) { instanceRemove(); return; }
862   if (isHitFrame) {
863     spectral = true;
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);
868       return false;
869     }, precise:true);
870     spectral = false;
871   }
875 defaultproperties {
876   objName = 'Spears';
877   //depth = 999;
878   depth = 995;
879   imageSpeed = 1;
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');
890   return true;
893 defaultproperties {
894   deltaX = 16;
898 // ////////////////////////////////////////////////////////////////////////// //
899 class MapObjectSpearsRight['oSpearsRight'] : MapObjectSpearsBase;
901 override bool initialize () {
902   if (!::initialize()) return false;
903   setSprite(global.cityOfGold ? 'sSpearsRightGold' : 'sSpearsRight');
904   return true;
907 defaultproperties {
908   deltaX = -16;
912 // ////////////////////////////////////////////////////////////////////////// //
913 class MapObjectSpringTrap['oSpringTrap'] : MapObject;
915 override bool initialize () {
916   if (!::initialize()) return false;
917   setSprite('sSpringTrap');
918   return true;
922 override void onAnimationLooped () {
923   if (status == SPRUNG) {
924     status = IDLE;
925     setSprite('sSpringTrap');
926   }
930 override bool onExplosionTouch (MapObject xplo) {
931   if (invincible) return false;
932   if (heldBy) return false;
933   instanceRemove();
934   return true;
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) {
946     // player
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;
952           plr.shiftY(-16);
953           plr.yVel = -16;
954         }
955       }
956     }
958     // objects
959     spectral = 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;
964       // enemy
965       auto enemy = MapEnemy(o);
966       if (enemy) {
967         if (!o.flying && abs(o.ix-ix) < 6) {
968           o.shiftY(-16);
969           o.yVel = -16;
970           if (o.dir == Dir.Left) o.xVel -= 1; else o.xVel += 1;
971           activatedOnThisFrame = true;
972         }
973         return false;
974       }
975       // item
976       auto item = MapItem(o);
977       if (item) {
978         o.shiftY(-24);
979         o.yVel = -8;
980         activatedOnThisFrame = true;
981         return false;
982       }
983       // damsel
984       auto dms = MonsterDamsel(o);
985       if (dms) {
986         o.shiftY(-24);
987         o.yVel = -8;
988         if (o.dir == Dir.Left) o.xVel -= 1; else o.xVel += 1;
989         activatedOnThisFrame = true;
990         return false;
991       }
992       return false;
993     }, precise:true);
994     spectral = false;
995   }
997   if (activatedOnThisFrame) {
998     setSprite('sSpringTrapSprung');
999     playSound('sndBoing');
1000     status = SPRUNG;
1001     counter = 10;
1002   }
1006 defaultproperties {
1007   objName = 'Spring';
1008   desc = "Spring";
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);
1013   status = IDLE;
1014   counter = 0;