renamed `onHitItem()` to `onHitByItem()`
[k8vacspelynky.git] / PlayerPawn.vc
blob74e65efaf39fb4ffc196054c2e0a356852420c24
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 // recoded by Ketmar // Invisible Vector
19 class PlayerPawn : MapEnemy;
21 //#define HANG_DEBUG
22 #define EASIER_HANG
24 const int hangCountMax = 3;
26 array!PlayerPowerup powerups; // created in initializer
28 int cameraBlockX; // >0: don't center camera
29 int cameraBlockY; // >0: don't center camera
31 float xFric, yFric;
33 // swimming
34 int bubbleTimer;
35 const int bubbleTimerMax = 20;
36 int jetpackFlaresTime;
38 // gambling
39 int bet;
40 bool point;
42 //bool walkSndToggle;
44 bool damsel;
45 bool tunnelMan;
47 bool justdied = true; // so dead body won't spill blood endlessly
48 bool madeOffer;
49 bool whipping;
50 bool kJumped;
51 bool bowArmed;
52 bool movementBlocked;
53 int cantJump;
54 int firing;
55 const int firingMax = 20;
56 const int firingPistolMax = 20;
57 const int firingShotgunMax = 40;
58 float bowStrength;
59 int jetpackFuel;
61 int hotkeyPressed = -1;
62 bool kExitPressed;
64 // used with Kapala
65 int redColor;
66 bool redToggle;
68 // poison
70 int greenColor;
71 bool greenToggle;
73 //!!!global.poisonStrength = max(global.poisonStrength-0.5, 1);
75 //string holdItemType = "";
76 //string pickupItemType = "";
78 // this is what we had picked up
79 // picked item will be stored here by bomb/rope/item switcher
80 MapObject pickedItem;
82 bool canDropStuff = true;
83 bool kItemPressed;
84 //bool kItemReleased;
85 bool kRopePressed;
86 bool kBombPressed;
87 bool kPayPressed;
88 //bool kRope;
89 //bool kBomb;
90 //bool kPay;
92 int holdArrow;
93 bool holdArrowToggle;
94 int bombArrowCounter = 80;
96 int hangCount;
97 int runHeld;
99 // other
100 int blink;
101 bool blinkHidden;
103 // the keys that the platform character will use (don't edit)
104 bool kLeft;
105 bool kLeftPressed;
106 bool kLeftReleased;
107 bool kRight;
108 bool kRightPressed;
109 bool kRightReleased;
110 bool kUp;
111 bool kDown;
112 bool kJump;
113 bool kJumpPressed;
114 bool kJumpReleased;
115 bool jumpButtonReleased; // whether the jump button was released. (Stops the user from pressing the jump button many times to get extra jumps)
116 bool kAttack;
117 bool kAttackPressed;
118 bool kAttackReleased;
120 const float gravNorm = 1;
121 //float grav = 1; // the gravity
123 const float initialJumpAcc = -2; // relates to how high the character will jump
124 const int jumpTimeTotal = 10;  // how long the user must hold the jump button to get the maximum jump height
126 float climbAcc = 0.6; // how fast the character will climb
127 float climbAnimSpeed = 0.4; // relates to how fast the climbing animation should go
128 int climbSndSpeed = 8;
129 int climbSoundTimer;
130 bool climbSndToggle;
132 // these flags are used to recreate ball and chain when player is moved at a new level
133 //bool chained;
134 //bool holdingBall;
136 const float departLadderXVel = 4;  // how fast character should be moving horizontally when he leaves the ladder
137 const float departLadderYVel = -4; // how fast the character should be moving vertically when he leaves the ladder
139 const float frictionRunningX = 0.6;      // friction obtained while running
140 const float frictionRunningFastX = 0.98; // friction obtained while holding the shift button for some time while running
141 const float frictionClimbingX = 0.6;     // friction obtained while climbing
142 const float frictionClimbingY = 0.6;     // friction obtained while climbing
143 const float frictionDuckingX = 0.8;      // friction obtained while ducking
144 const float frictionFlyingX = 0.99;      // friction obtained while "flying"
146 const float runAnimSpeed = 0.1; // relates to the how fast the running animation should go
148 // hidden variables (don't edit)
149 protected int statePrev;
150 protected int statePrevPrev;
151 protected float gravityIntensity = grav; // this variable describes the current force due to gravity (this variable is altered for variable jumping)
152 protected float jumpTime = jumpTimeTotal; // current time of the jump (0=start of jump, jumpTimeTotal=end of jump)
153 protected int ladderTimer; // relates to whether the character can climb a ladder
154 protected int kLeftPushedSteps;
155 protected int kRightPushedSteps;
157 transient protected bool skipCutscenePressed;
160 //int score;
162 //PlayerWeapon actWeapon; // active weapon object
165 enum {
166   ARROW_NORM = 1,
167   ARROW_BOMB = 2,
170 int viewOffset;
171 int viewCount;
172 int lookOff0;
175 // ////////////////////////////////////////////////////////////////////////// //
176 bool mustBeChained;
177 bool wasHoldingBall;
179 ItemBall myBall;
181 final ItemBall getMyBall () {
182   ItemBall res = myBall;
183   if (res && !res.isInstanceAlive) { res = none; myBall = none; }
184   return res;
188 void spawnBallAndChain (optional bool levelStart) {
189   if (levelStart) {
190     auto owh = wasHoldingBall;
191     removeBallAndChain();
192     wasHoldingBall = owh;
193   }
194   mustBeChained = true;
195   auto ball = getMyBall();
196   if (!ball) {
197     if (levelStart) writeln("::: respawning ball; old ball is missing (it is ok)");
198     writeln("creating new ball");
199     ball = ItemBall(level.MakeMapObject(ix, iy, 'oBall'));
200     if (ball) {
201       ball.attachTo(self, levelStart);
202       writeln("ball created");
203     }
204   }
205   if (ball) {
206     if (levelStart) writeln("::: attaching ball to player");
207     ball.attachTo(self, levelStart);
208     if (wasHoldingBall) {
209       if (levelStart) writeln("::: picking ball");
210       if (pickedItem) {
211         pickedItem.instanceRemove();
212         pickedItem = none;
213       }
214       if (holdItem && holdItem != ball) {
215         holdItem.instanceRemove();
216         holdItem = none;
217       }
218       holdItem = none;
219       holdItem = ball;
220     }
221     if (myBall != ball) FatalError("error in ball management");
222     if (levelStart) writeln("ballpos=(", ball.ix, ",", ball.iy, "); plrpos=(", ix, ",", iy, "); ballalive=", ball.isInstanceAlive);
223   } else {
224     writeln("failed to create a new ball");
225     mustBeChained = false;
226   }
227   wasHoldingBall = false;
231 void removeBallAndChain (optional bool temp) {
232   auto ball = getMyBall();
233   if (ball) {
234     writeln("removing ball and chain...", (temp ? " (temporarily)" : ""));
235     wasHoldingBall = (holdItem == ball);
236     writeln("  has ball, holding=", wasHoldingBall);
237     mustBeChained = true;
238     ball.attachTo(none);
239     ball.instanceRemove();
240     myBall = none;
241   }
242   if (temp) return;
243   wasHoldingBall = false;
244   mustBeChained = false;
248 // ////////////////////////////////////////////////////////////////////////// //
249 final PlayerPowerup findPowerup (name id) {
250   foreach (PlayerPowerup pp; powerups) if (pp.id == id) return pp;
251   return none;
255 final bool setPowerupState (name id, bool active) {
256   auto pp = findPowerup(id);
257   if (!pp) return false;
258   return (active ? pp.onActivate() : pp.onDeactivate());
262 final bool togglePowerupState (name id) {
263   auto pp = findPowerup(id);
264   if (!pp) return false;
265   return (pp.active ? pp.onDeactivate() : pp.onActivate());
269 final bool activatePowerup (name id) { return setPowerupState(id, true); }
270 final bool deactivatePowerup (name id) { return setPowerupState(id, false); }
273 final bool isActivePowerup (name id) {
274   auto pp = findPowerup(id);
275   return (pp && pp.active);
279 // ////////////////////////////////////////////////////////////////////////// //
280 override void Destroy () {
281   foreach (PlayerPowerup pp; powerups) delete pp;
282   powerups.length = 0;
286 void unpressAllKeys () {
287   kLeft = false;
288   kLeftPressed = false;
289   kLeftReleased = false;
290   kRight = false;
291   kRightPressed = false;
292   kRightReleased = false;
293   kUp = false;
294   kDown = false;
295   kJump = false;
296   kJumpPressed = false;
297   kJumpReleased = false;
298   kAttack = false;
299   kAttackPressed = false;
300   kAttackReleased = false;
301   kItemPressed = false;
302   kRopePressed = false;
303   kBombPressed = false;
304   kPayPressed = false;
305   kExitPressed = false;
309 // ////////////////////////////////////////////////////////////////////////// //
310 // called on level start too
311 void resurrect () {
312   justSpawned = true;
313   holdArrow = 0;
314   bowStrength = 0;
315   bowArmed = false;
316   skipCutscenePressed = false;
317   movementBlocked = false;
318   if (global.plife < 1) global.plife = max(1, global.config.scumStartLife);
319   dead = false;
320   xVel = 0;
321   yVel = 0;
322   grav = default.grav;
323   myGrav = default.myGrav;
324   bounced = false;
325   stunned = false;
326   burning = 0;
327   depth = default.depth;
328   status = default.status;
329   fallTimer = 0;
330   stunTimer = 0;
331   wallHurt = 0;
332   pushTimer = 0;
333   whoaTimer = 0;
334   distToNearestLightSource = 999;
335   flying = false;
336   justdied = default.justdied;
337   whipping = false;
338   if (holdItem isa PlayerWeapon) {
339     auto w = holdItem;
340     holdItem = none;
341     w.instanceRemove();
342   }
343   invincible = 0;
344   blink = default.blink;
345   blinkHidden = default.blinkHidden;
346   status = STANDING;
347   characterSprite();
348   active = true;
349   visible = true;
350   unpressAllKeys();
351   level.clearKeysPressRelease();
352   climbSoundTimer = 0;
353   bet = 0;
354   //scrSwitchToPocketItem(forceIfEmpty:false);
358 // ////////////////////////////////////////////////////////////////////////// //
359 bool isExitingSprite () {
360   auto spr = getSprite();
361   return (spr.Name == 'sPExit' || spr.Name == 'sDamselExit' || spr.Name == 'sTunnelExit');
365 // ////////////////////////////////////////////////////////////////////////// //
366 override void playSound (name aname, optional bool unique) {
367   if (unique && global.sndIsPlaying(0, aname)) return;
368   global.playSound(0, 0, 0, aname); // it is local
372 override bool sndIsPlaying (name aname) {
373   return global.sndIsPlaying(0, aname);
377 override void sndStopSound (name aname) {
378   global.sndStopSound(0, aname);
382 // ////////////////////////////////////////////////////////////////////////// //
383 transient ItemDice currDie;
385 void onDieRolled (ItemDice die) {
386   if (!die.forSale) return;
387   // only law-abiding players can play
388   if (global.thiefLevel > 0 || global.murderer) return;
389   if (bet == 0) return;
390   auto odie = currDie;
391   currDie = die;
392   level.forEachObject(delegate bool (MapObject o) {
393     MonsterShopkeeper sc = MonsterShopkeeper(o);
394     if (sc && !sc.dead && !sc.angered) return sc.onDiePlayed(self, currDie);
395     return false;
396   });
397   currDie = odie;
401 // ////////////////////////////////////////////////////////////////////////// //
402 override bool onExplosionTouch (MapObject xplo) {
403   //writeln("PlayerPawn: on explo touch! ", invincible);
404   if (invincible) return false;
405   if (global.config.scumExplosionHurt) {
406     global.plife -= global.config.explosionDmg;
407     if (!dead && global.plife <= 0 /*&& isRealLevel()*/) {
408       auto xp = MapObjExplosion(xplo);
409       if (xp && xp.suicide) level.addDeath('suicide'); else level.addDeath('explosion');
410     }
411     burning = 50;
412     if (global.config.scumExplosionStun) {
413       stunned = true;
414       stunTimer = 100;
415     }
416     spillBlood();
417   }
418   if (xplo.ix < ix) xVel = global.randOther(4, 6); else xVel = -global.randOther(4, 6);
419   yVel = -6;
420   return true;
424 // ////////////////////////////////////////////////////////////////////////// //
425 // start new game when exiting from title, and process other custom exits
426 void scrPlayerExit () {
427   level.playerExited = true;
428   status = STANDING;
429   characterSprite();
433 // ////////////////////////////////////////////////////////////////////////// //
434 bool scrHideItemToPocket (optional bool forBombOrRope) {
435   if (!holdItem) return true;
436   if (holdItem isa PlayerWeapon) return false;
437   if (holdItem.forSale) return false;
438   if (!forBombOrRope) {
439     if (holdItem isa ItemBall) return false;
440   }
442   // cannot hide armed bomb
443   ItemBomb bomb = ItemBomb(holdItem);
444   if (bomb && bomb.armed) return false;
445   if (bomb || holdItem isa ItemRopeThrow) {
446     holdItem.instanceRemove();
447     holdItem = none;
448     return true;
449   }
451   // cannot hide enemy
452   if (holdItem isa MapEnemy) return false;
453   //writeln("hiding: '", GetClassName(holdItem.Class), "'");
455   if (pickedItem) FatalError("we are already holding '%n'", GetClassName(pickedItem.Class));
456   pickedItem = holdItem;
457   holdItem = none;
458   pickedItem.active = false;
459   pickedItem.visible = false;
460   if (pickedItem.heldBy) FatalError("oooops (scrHideItemToPocket)");
461   return true;
465 bool scrSwitchToBombs () {
466   if (holdItem isa PlayerWeapon) return false;
468   if (global.bombs < 1) return false;
469   if (ItemBomb(holdItem)) return true;
470   if (!scrHideItemToPocket(forBombOrRope:true)) return false;
472   ItemBomb bomb = ItemBomb(level.MakeMapObject(ix, iy, 'oBomb'));
473   if (!bomb) return false;
474   bomb.setSticky(global.stickyBombsActive);
475   holdItem = bomb;
476   whoaTimer = whoaTimerMax;
477   return true;
481 bool scrSwitchToStickyBombs () {
482   if (holdItem isa PlayerWeapon) return false;
483   if (!global.hasStickyBombs) {
484     global.stickyBombsActive = false;
485     return false;
486   }
488   global.stickyBombsActive = !global.stickyBombsActive;
489   return true;
493 bool scrSwitchToRopes () {
494   if (holdItem isa PlayerWeapon) return false;
496   if (global.rope < 1) return false;
497   if (ItemRopeThrow(holdItem)) return true;
498   if (!scrHideItemToPocket(forBombOrRope:true)) return false;
500   ItemRopeThrow rope = ItemRopeThrow(level.MakeMapObject(ix, iy, 'oRopeThrow'));
501   if (!rope) return false;
502   holdItem = rope;
503   whoaTimer = whoaTimerMax;
504   return true;
508 bool isHoldingBombOrRope () {
509   auto hit = holdItem;
510   if (!hit) return false;
511   return (hit isa ItemBomb || hit isa ItemRopeThrow);
515 bool isHoldingBomb () {
516   auto hit = holdItem;
517   if (!hit) return false;
518   return (hit isa ItemBomb);
522 bool isHoldingArmedBomb () {
523   auto hit = ItemBomb(holdItem);
524   if (!hit) return false;
525   return hit.armed;
529 bool isHoldingRope () {
530   auto hit = holdItem;
531   if (!hit) return false;
532   return (hit isa ItemRopeThrow);
536 bool scrSwitchToPocketItem (bool forceIfEmpty) {
537   if (holdItem isa PlayerWeapon) return false;
538   if (holdItem && holdItem.forSale) return false;
540   if (holdItem == pickedItem) { pickedItem = none; whoaTimer = whoaTimerMax; return true; }
542   if (!forceIfEmpty && !pickedItem) return false;
544   // destroy currently holded item if it is a bomb or a rope
545   if (holdItem) {
546     // you cannot do it with an armed bomb
547     if (holdItem isa MapEnemy) return false; // cannot hide an enemy
548     ItemBomb bomb = ItemBomb(holdItem);
549     if (bomb && bomb.armed) return false;
550     if (bomb || holdItem isa ItemRopeThrow) {
551       //delete holdItem;
552       holdItem.instanceRemove();
553       holdItem = none;
554     } /*else {
555       if (pickedItem) {
556         writeln(va("cannot switch to pocket item while carrying '%n' ('%n' is in pocket, why?)", GetClassName(holdItem.Class), GetClassName(pickedItem.Class)));
557         return false;
558       }
559     }*/
560   }
562   auto oldHold = holdItem;
563   holdItem = pickedItem;
564   pickedItem = oldHold;
565   // all flag management is done in property handler
566   if (oldHold) {
567     oldHold.active = false;
568     oldHold.visible = false;
569   }
570   whoaTimer = whoaTimerMax;
571   return true;
575 bool scrSwitchToNextItem () {
576   if (holdItem isa PlayerWeapon) return false;
577   if (holdItem && holdItem.forSale) return false;
579   // holding a bomb?
580   if (ItemBomb(holdItem)) {
581     if (ItemBomb(holdItem).armed) return false; // cannot switch out of armed bomb
582     if (scrSwitchToRopes()) return true;
583     return scrSwitchToPocketItem(forceIfEmpty:true);
584   }
586   // holding a rope?
587   if (ItemRopeThrow(holdItem)) {
588     if (scrSwitchToPocketItem(forceIfEmpty:true)) return true;
589     if (scrSwitchToBombs()) return true;
590     return scrHideItemToPocket();
591   }
593   // either nothing, or normal item
594   bool tryPocket = !!holdItem;
595   if (scrSwitchToBombs()) return true;
596   if (scrSwitchToRopes()) return true;
597   if (holdItem isa ItemBall) return false;
598   if (tryPocket) return scrSwitchToPocketItem(forceIfEmpty:true);
599   return false;
603 // ////////////////////////////////////////////////////////////////////////// //
604 bool scrPickupItem (MapObject obj) {
605   if (holdItem isa PlayerWeapon) return false;
607   if (!obj) return false;
609   if (holdItem) {
610     if (pickedItem) return false;
611     if (isHoldingArmedBomb()) return false;
612     if (isHoldingBombOrRope()) {
613       if (!scrSwitchToPocketItem(forceIfEmpty:true)) return false;
614     }
615     if (holdItem) return false;
616   } else {
617     // just in case
618     if (pickedItem) return false;
619   }
621        if (obj isa ItemBomb && !ItemBomb(obj).armed) ++global.bombs;
622   else if (obj isa ItemRopeThrow) ++global.rope;
623   holdItem = obj;
624   whoaTimer = whoaTimerMax;
625   obj.onPickedUp(self);
626   return true;
630 // drop currently held item
631 bool scrDropItem (LostCause cause, optional float xVel, optional float yVel) {
632   if (holdItem isa PlayerWeapon) return false;
634   if (!holdItem) return false;
636   if (!onLoosingHeldItem(cause)) return false;
638   auto hi = holdItem;
639   holdItem = none;
641   if (!hi.onLostAsHeldItem(self, cause, xVel!optional, yVel!optional)) {
642     // oops, regain it
643     holdItem = hi;
644     return false;
645   }
647        if (hi isa ItemRopeThrow) global.rope = max(0, global.rope-1);
648   else if (hi isa ItemBomb && !ItemBomb(hi).armed) global.bombs = max(0, global.bombs-1);
650   madeOffer = false;
652   scrSwitchToPocketItem(forceIfEmpty:true);
653   return true;
657 // ////////////////////////////////////////////////////////////////////////// //
658 void scrUseThrowIt (MapObject it) {
659   if (!it) return;
661   it.onBeforeThrowBy(self);
663   it.resaleValue = 0;
664   it.makeSafe();
666   if (dir == Dir.Left) {
667     it.xVel = (it.heavy ? -4+xVel : -8+xVel);
668     //foreach (; 0..8) if (level.isSolidAtPoint(ix-8, iy)) it.shiftX(1);
669     //while (!level.isSolidAtPoint(ix-8, iy)) it.shiftX(1); // prevent getting stuck in wall
670   } else if (dir == Dir.Right) {
671     it.xVel = (it.heavy ? 4+xVel : 8+xVel);
672     //foreach (; 0..8) if (level.isSolidAtPoint(ix+8, iy)) it.shiftX(-1);
673     //while (!level.isSolidAtPoint(ix+8, iy)) it.shiftX(-1); // prevent getting stuck in wall
674   }
675   it.yVel = (it.heavy ? (kUp ? -4 : -2) : (kUp ? -9 : -3));
676   if (kDown || scrPlayerIsDucking()) {
677     if (platformCharacterIs(ON_GROUND)) {
678       it.shiftY(-2);
679       it.xVel *= 0.6;
680       it.yVel = 0.5;
681     } else {
682       it.yVel = 3;
683     }
684   } else if (!global.hasMitt) {
685     if (dir == Dir.Left) {
686       if (level.isSolidAtPoint(ix-8, iy-10)) {
687         it.yVel = 0;
688         it.xVel -= 1;
689       }
690     } else if (dir == Dir.Right) {
691       if (level.isSolidAtPoint(ix+8, iy-10)) {
692         it.yVel = 0;
693         it.xVel += 1;
694       }
695     }
696   }
698   if (global.hasMitt && !scrPlayerIsDucking()) {
699     it.xVel += (it.xVel < 0 ? -6 : 6);
700          if (!kUp && !kDown) it.yVel = -0.4;
701     else if (kDown) it.yVel = 6;
702     it.myGrav = 0.1;
703   }
705   // prevent getting stuck in a wall
706   if (it.isCollision()) {
707     //foreach (; 0..8) if (level.isSolidAtPoint(ix-8, iy)) it.shiftX(1);
708     if (it.xVel < 0) {
709       if (level.isSolidAtPoint(it.ix-8, it.iy)) it.shiftX(8);
710     } else if (it.xVel > 0) {
711       if (level.isSolidAtPoint(it.ix+8, it.iy)) it.shiftX(-8);
712     } else if (it.isCollision()) {
713       int dx = (it.isCollisionLeft(0) ? 1 : it.isCollisionRight(0) ? -1 : 0);
714       if (dx) {
715         foreach (; 0..8) {
716           it.shiftX(dx);
717           if (!it.isCollision()) break;
718         }
719       }
720     }
721     /*
722     int dx = 1;
723     while (dx > 8 && it.isCollisionLeft(dx)) ++dx;
724     if (dx < 8) it.shiftX(8);
725     else {
726       dx = 1;
727       while (dx > 8 && it.isCollisionRight(dx)) ++dx;
728       if (dx < 8) it.shiftX(-8);
729     }
730     */
731   }
733   /*
734   if (it.sprite_index == sBombBag ||
735       it.sprite_index == sBombBox ||
736       it.sprite_index == sRopePile)
737   {
738       // do nothing
739   } else*/ {
740     playSound('sndThrow');
741   }
743   auto proj = ItemProjectile(it);
744   if (proj) proj.launchedByPlayer = true;
748 bool scrUseThrowItem () {
749   if (holdItem isa PlayerWeapon) return false;
751   auto hitem = holdItem;
753   if (!hitem) return false;
754   if (!onLoosingHeldItem(LostCause.Unknown)) return false;
756   holdItem = none;
757   madeOffer = false;
759   scrUseThrowIt(hitem);
761   // if we throwing away armed bomb, get previous item back into our hands
762   //FIXME
763   if (/*ItemBomb(hitem)*/isHoldingBombOrRope()) scrSwitchToPocketItem(forceIfEmpty:false);
765   return true;
769 // ////////////////////////////////////////////////////////////////////////// //
770 bool scrPlayerIsDucking () {
771   if (dead) return false;
772   auto spr = getSprite();
773   //if (!spr) return false;
774   return
775     spr.Name == 'sDuckLeft' ||
776     spr.Name == 'sCrawlLeft' ||
777     spr.Name == 'sDamselDuckL' ||
778     spr.Name == 'sDamselCrawlL' ||
779     spr.Name == 'sTunnelDuckL' ||
780     spr.Name == 'sTunnelCrawlL';
784 bool scrFireBow () {
785   if (holdItem !isa ItemWeaponBow) return false;
786   sndStopSound('sndBowPull');
787   if (!bowArmed) return false;
788   if (!holdItem.onTryUseItem(self)) return false;
789   return true;
793 void scrUsePutItOnGroundHelper (MapObject it, optional float xVelMult, optional float yVelNew) {
794   if (!it) return;
796   if (!specified_xVelMult) xVelMult = 0.4;
797   if (!specified_yVelNew) yVelNew = 0.5;
799   //writeln("putting '", GetClassName(hi.Class), "'");
801   if (dir == Dir.Left) {
802     it.xVel = (it.heavy ? -4 : -8);
803   } else if (dir == Dir.Right) {
804     it.xVel = (it.heavy ? 4 : 8);
805   }
806   it.xVel += xVel;
807   it.xVel *= xVelMult;
808   it.yVel = yVelNew;
810   //hi.fltx = ix;
811   it.flty = iy+2;
812   if (ItemGoldIdol(it)) it.flty = iy;
814   foreach (; 0..16) {
815     if (it.isCollisionBottom(0) && !it.isCollisionTop(1)) {
816       it.flty -= 1;
817     } else {
818       break;
819     }
820   }
822   foreach (; 0..16) {
823     if (it.isCollisionLeft(0)) {
824       if (it.isCollisionRight(1)) break;
825       it.fltx += 1;
826     } else if (it.isCollisionRight(0)) {
827       if (it.isCollisionLeft(1)) break;
828       it.fltx -= 1;
829     } else {
830       break;
831     }
832   }
836 // put item which player holds in his hands on the ground if player is ducking
837 // return `true` if item was put
838 bool scrUsePutItemOnGround (optional float xVelMult, optional float yVelNew) {
839   if (holdItem isa PlayerWeapon) return false;
841   auto hi = holdItem;
842   if (!hi || !scrPlayerIsDucking()) return false;
844   if (!onLoosingHeldItem(LostCause.Unknown)) return false;
846   //writeln("putting '", GetClassName(hi.Class), "'");
848   if (global.bombs > 0) {
849     auto bomb = ItemBomb(hi);
850     if (bomb && !bomb.armed) global.bombs -= 1;
851   }
853   if (global.rope > 0) {
854     auto rope = ItemRopeThrow(hi);
855     if (rope) {
856       global.rope -= 1;
857       rope.falling = false;
858       rope.flying = false;
859     }
860   }
862   holdItem = none;
863   hi.resaleValue = 0;
864   madeOffer = false;
865   hi.makeSafe();
867   scrUsePutItOnGroundHelper(hi, xVelMult!optional, yVelNew!optional);
869   return true;
873 bool launchRope (bool goDown, bool doDrop) {
874   if (global.rope < 1) {
875     global.rope = 0;
876     if (ItemRopeThrow(holdItem)) scrSwitchToPocketItem(forceIfEmpty:false);
877     return false;
878   }
880   --global.rope;
882   bool wasHeld = false;
883   ItemRopeThrow rp = ItemRopeThrow(holdItem);
884   int xdelta = (doDrop ? 12 : 16)*(dir == Dir.Left ? -1 : 1);
885   if (rp) {
886     //FIXME: call handler
887     wasHeld = true;
888     holdItem = none;
889     rp.setXY(ix+xdelta, iy);
890   } else {
891     rp = ItemRopeThrow(level.MakeMapObject(ix+xdelta, iy, 'oRopeThrow'));
892   }
893   if (rp.heldBy) FatalError("PlayerPawn::launchRope: hold management fucked");
894   rp.armed = true;
895   rp.flying = false;
896   //rp.resaleValue = 0;
898   rp.px = ix;
899   rp.py = iy;
900   if (platformCharacterIs(ON_GROUND)) rp.startY = iy; // YASM 1.7
902   if (!goDown) {
903     // launch rope up
904     rp.setX(fltx);
905     rp.xVel = 0;
906     rp.yVel = -12;
907   } else {
908     // launch rope down
909     bool t = true;
910     rp.moveSnap(16, 1);
911     if (ix < rp.ix) {
912       if (!level.isSolidAtPoint(ix+(doDrop ? 2 : 8), iy)) { //2
913              if (!level.checkTilesInRect(rp.ix-8, rp.iy, 2, 17)) rp.shiftX(-8);
914         else if (!level.checkTilesInRect(rp.ix+7, rp.iy, 2, 17)) rp.shiftX(8);
915         else t = false;
916       } else {
917         t = false;
918       }
919     } else if (!level.isSolidAtPoint(ix-(doDrop ? 2 : 8), iy)) { //2
920            if (!level.checkTilesInRect(rp.ix+7, rp.iy, 2, 17)) rp.shiftX(8);
921       else if (!level.checkTilesInRect(rp.ix-8, rp.iy, 2, 17)) rp.shiftX(-8);
922       else t = false;
923     } else {
924       t = false;
925     }
926     //writeln("t=", t);
927     if (!t) {
928       // cannot launch rope
929       /* was commented in the original
930       if (oPlayer1.facing == 18) {
931         obj = instance_create(oPlayer1.x-4, oPlayer1.y+2, oRopeThrow);
932         obj.xVel = -3.2;
933       } else {
934         obj = instance_create(oPlayer1.x+4, oPlayer1.y+2, oRopeThrow);
935         obj.xVel = 3.2;
936       }
937       obj.yVel = 0.5;
938       */
939       //writeln("!!! goDown=", goDown, "; doDrop=", doDrop, "; wasHeld=", wasHeld);
940       rp.armed = false;
941       rp.flying = false;
942       if (!wasHeld) doDrop = true;
943       if (doDrop) {
944         /*
945         rp.setXY(ix, iy);
946         if (dir == Dir.Left) rp.xVel = -3.2; else rp.xVel = 3.2;
947         rp.yVel = 0.5;
948         */
949         rp.forceFixHoldCoords(self);
950         if (goDown) {
951           scrUsePutItOnGroundHelper(rp);
952         } else {
953           scrUseThrowIt(rp);
954         }
955         if (wasHeld) scrSwitchToPocketItem(forceIfEmpty:false);
956       } else {
957         //writeln("NO DROP!");
958         ++global.rope;
959         if (wasHeld) {
960           // regain it
961           //rp.resaleValue = 1; //k8:???
962           holdItem = rp;
963         } else {
964           rp.instanceRemove();
965           if (wasHeld) scrSwitchToPocketItem(forceIfEmpty:false);
966         }
967       }
968       return false;
969     } else {
970       level.MakeMapObject(rp.ix, rp.iy, 'oRopeTop');
971       rp.armed = false;
972       rp.falling = true;
973       rp.xVel = 0;
974       rp.yVel = 0;
975     }
976   }
977   if (wasHeld) scrSwitchToPocketItem(forceIfEmpty:false);
978   playSound('sndThrow');
979   return true;
983 bool scrLaunchBomb () {
984   if (whipping || global.bombs < 1) return false;
985   --global.bombs;
987   ItemBomb bomb = ItemBomb(level.MakeMapObject(ix, iy, 'oBomb'));
988   if (!bomb) return false;
989   bomb.forceFixHoldCoords(self);
990   bomb.setSticky(global.stickyBombsActive);
991   bomb.armIt(80);
992   bomb.resaleValue = 0;
994   if (kDown || scrPlayerIsDucking()) {
995     scrUsePutItOnGroundHelper(bomb);
996   } else {
997     scrUseThrowIt(bomb);
998   }
1000   return true;
1004 bool scrUseItem () {
1005   auto it = holdItem;
1006   if (!it) return false;
1007   //writeln(GetClassName(holdItem.Class));
1009   //auto spr = holdItem.getSprite();
1010   /+
1011   } else if (holdItem.type == "Sceptre") {
1012     if (kDown) scrUsePutItemOnGround(0.4, 0.5);
1013     if (firing == 0 && !scrPlayerIsDucking()) {
1014       if (facing == LEFT) {
1015         asleft = true;
1016         xsgn = -1;
1017       } else {
1018         asleft = false;
1019         xsgn = 1;
1020       }
1021       xofs = 12*xsgn;
1022       repeat(3) {
1023         obj = instance_create(x+xofs, y+4, oPsychicCreateP);
1024         obj.xVel = xsgn*rand(1, 3);
1025         obj.yVel = -random(2);
1026       }
1027       obj = instance_create(x+xofs, y-2, oPsychicWaveP);
1028       obj.xVel = xsgn*6;
1029       playSound(global.sndPsychic);
1030       firing = firingPistolMax;
1031     }
1032   } else if (holdItem.type == "Teleporter II") {
1033     scrUseTeleporter2();
1034   } else if (holdItem.type == "Bow") {
1035     if (kDown) {
1036       scrUsePutItemOnGround(0.4, 0.5);
1037     } else if (firing == 0 && !scrPlayerIsDucking() && !bowArmed && global.arrows > 0) {
1038       bowArmed = true;
1039       playSound(global.sndBowPull);
1040     } else if (global.arrows <= 0) {
1041       global.message = "I'M OUT OF ARROWS!";
1042       global.message2 = "";
1043       global.messageTimer = 80;
1044     }
1045   } else {
1046   +/
1049   if (whipping) return false;
1051   if (kDown) {
1052     if (scrPlayerIsDucking()) scrUsePutItemOnGround();
1053     return true;
1054   }
1056   // you cannot throw away shop items, but can throw dices
1057   if (it.forSale && it !isa ItemDice) {
1058     if (!level.isInShop(ix/16, iy/16)) {
1059       it.forSale = false;
1060     } else {
1061       // allow throw/use shop items
1062       //return false;
1063     }
1064   }
1066   if (!it.onTryUseItem(self)) {
1067     // throw item
1068     scrUseThrowItem();
1069   }
1071   return true;
1075 // ////////////////////////////////////////////////////////////////////////// //
1076 // called by characterStepEvent
1077 // help player jump up through one block wide gaps by nudging them to one side so they don't hit their head
1078 void scrJumpHelper () {
1079   int d = 4; // max distance to nudge player
1080   int x = ix, y = iy;
1081   if (!level.checkTilesInRect(x, y-12, 1, 7)) {
1082     if (level.checkTilesInRect(x-5, y-12, 1, 7) &&
1083         level.checkTilesInRect(x+14, y-12, 1, 7))
1084     {
1085       while (d > 0 && level.checkTilesInRect(x-5, y-12, 1, 7)) { ++x; shiftX(1); --d; }
1086     } else if (level.checkTilesInRect(x+5, y-12, 1, 7) &&
1087                level.checkTilesInRect(x-14, y-12, 1, 7))
1088     {
1089       while (d > 0 && level.checkTilesInRect(x+5, y-12, 1, 7)) { --x; shiftX(-1); --d; }
1090     }
1091   }
1092   /+
1093   if (!collision_line(x, y-6, x, y-12, oSolid, 0, 0)) {
1094     if (collision_line(x-5, y-6, x-5, y-12, oSolid, 0, 0) &&
1095         collision_line(x+14, y-6, x+14, y-12, oSolid, 0, 0))
1096     {
1097       while (collision_line(x-5, y-6, x-5, y-12, oSolid, 0, 0) && d > 0) {
1098         x += 1;
1099         d -= 1;
1100       }
1101     }
1102     else if (collision_line(x+5, y-6, x+5, y-12, oSolid, 0, 0) and
1103              collision_line(x-14, y-6, x-14, y-12, oSolid, 0, 0))
1104     {
1105       while (collision_line(x+5, y-6, x+5, y-12, oSolid, 0, 0) && d > 0) {
1106         x -= 1;
1107         d -= 1;
1108       }
1109     }
1110   }
1111   +/
1115 // ////////////////////////////////////////////////////////////////////////// //
1117  * Returns whether a GENERAL trait about a character is true.
1118  * Only the platform character should run this script.
1120  * `tp` can be one of the following:
1121  *   ON_GROUND
1122  *   IN_AIR
1123  *   ON_LADDER
1124  */
1125 final bool platformCharacterIs (int tp) {
1126   if (tp == ON_GROUND && (status == RUNNING || status == STANDING || status == DUCKING || status == LOOKING_UP)) return true;
1127   if (tp == IN_AIR && (status == JUMPING || status == FALLING)) return true;
1128   if (tp == ON_LADDER && status == CLIMBING) return true;
1129   return false;
1133 // ////////////////////////////////////////////////////////////////////////// //
1134 // sets the sprite of the character depending on his/her status
1135 final void characterSprite () {
1136   if (status == STOPPED) {
1137          if (global.isDamsel) setSprite('sDamselLeft');
1138     else if (global.isTunnelMan) setSprite('sTunnelLeft');
1139     else setSprite('sStandLeft');
1140     return;
1141   }
1143   int x = ix, y = iy;
1144   if (global.isTunnelMan && !stunned && !whipping) {
1145     // Tunnel Man
1146     if (status == STANDING) {
1147       if (!level.isSolidAtPoint(x-2, y+9)) {
1148         imageSpeed = 0.6;
1149         setSprite('sTunnelWhoaL');
1150       } else {
1151         setSprite('sTunnelLeft');
1152       }
1153     }
1154     if (status == RUNNING) {
1155       if (kUp) setSprite('sTunnelLookRunL'); else setSprite('sTunnelRunL');
1156     }
1157     if (status == DUCKING) {
1158            if (xVel == 0) setSprite('sTunnelDuckL');
1159       else if (fabs(xVel) < 3) setSprite('sTunnelCrawlL');
1160       else setSprite('sTunnelRunL');
1161     }
1162     if (status == LOOKING_UP) {
1163       if (fabs(xVel) > 0) setSprite('sTunnelRunL'); else setSprite('sTunnelLookL');
1164     }
1165     if (status == JUMPING) setSprite('sTunnelJumpL');
1166     if (status == FALLING && statePrev == FALLING && statePrevPrev == FALLING) setSprite('sTunnelFallL');
1167     if (status == HANGING) setSprite('sTunnelHangL');
1168     if (pushTimer > 20) setSprite('sTunnelPushL');
1169     if (status == DUCKTOHANG) setSprite('sTunnelDtHL');
1170     if (status == CLIMBING) {
1171       if (level.isRopeAtPoint(x, y)) {
1172         if (kDown) setSprite('sTunnelClimb3'); else setSprite('sTunnelClimb2');
1173       } else {
1174         setSprite('sTunnelClimb');
1175       }
1176     }
1177   } else if (global.isDamsel && !stunned && !whipping) {
1178     // Damsel
1179     if (status == STANDING) {
1180       if (!level.isSolidAtPoint(x-2, y+9)) {
1181         imageSpeed = 0.6;
1182         setSprite('sDamselWhoaL');
1183         /* was commented out in the original
1184         if (holdItem && whoaTimer < 1) {
1185           holdItem.held = false;
1186           if (facing == LEFT) holdItem.xVel = -2; else holdItem.xVel = 2;
1187           if (holdItem.type == "Damsel") playSound('sndDamsel');
1188           if (holdItem.type == pickupItemType) { holdItem = 0; pickupItemType = ""; } else scrSwitchToPocketItem();
1189         }
1190         */
1191       } else {
1192         setSprite('sDamselLeft');
1193       }
1194     }
1195     if (status == RUNNING) {
1196       if (kUp) setSprite('sDamselRunL'); else setSprite('sDamselRunL');
1197     }
1198     if (status == DUCKING) {
1199            if (xVel == 0) setSprite('sDamselDuckL');
1200       else if (fabs(xVel) < 3) setSprite('sDamselCrawlL');
1201       else setSprite('sDamselRunL');
1202     }
1203     if (status == LOOKING_UP) {
1204       if (fabs(xVel) > 0) setSprite('sDamselRunL'); else setSprite('sDamselLookL');
1205     }
1206     if (status == JUMPING) setSprite('sDamselDieLR');
1207     if (status == FALLING && statePrev == FALLING && statePrevPrev == FALLING) setSprite('sDamselFallL');
1208     if (status == HANGING) setSprite('sDamselHangL');
1209     if (pushTimer > 20) setSprite('sDamselPushL');
1210     if (status == DUCKTOHANG) setSprite('sDamselDtHL');
1211     if (status == CLIMBING) {
1212       if (level.isRopeAtPoint(x, y)) {
1213         if (kDown) setSprite('sDamselClimb3'); else setSprite('sDamselClimb2');
1214       } else {
1215         setSprite('sDamselClimb');
1216       }
1217     }
1218   } else if (!stunned && !whipping) {
1219     // Spelunker
1220     if (status == STANDING) {
1221       if (!level.checkTileAtPoint(x-(dir == Dir.Left ? 2 : 0), y+9, &level.cbCollisionForWhoa)) {
1222         imageSpeed = 0.6;
1223         setSprite('sWhoaLeft');
1224         /* was commented out in the original
1225         if (holdItem && whoaTimer < 1) {
1226           holdItem.held = false;
1227           if (facing == LEFT) holdItem.xVel = -2; else holdItem.xVel = 2;
1228           if (holdItem.type == "Damsel") playSound('sndDamsel');
1229           if (holdItem.type == pickupItemType) { holdItem = 0; pickupItemType = ""; } else scrSwitchToPocketItem();
1230         }
1231         */
1232       } else {
1233         setSprite('sStandLeft');
1234       }
1235     }
1236     if (status == RUNNING) {
1237       if (kUp) setSprite('sLookRunL'); else setSprite('sRunLeft');
1238     }
1239     if (status == DUCKING) {
1240            if (xVel == 0) setSprite('sDuckLeft');
1241       else if (fabs(xVel) < 3) setSprite('sCrawlLeft');
1242       else setSprite('sRunLeft');
1243     }
1244     if (status == LOOKING_UP) {
1245       if (fabs(xVel) > 0) setSprite('sLookRunL'); else setSprite('sLookLeft');
1246     }
1247     if (status == JUMPING) setSprite('sJumpLeft');
1248     if (status == FALLING && statePrev == FALLING && statePrevPrev == FALLING) setSprite('sFallLeft');
1249     if (status == HANGING) setSprite('sHangLeft');
1250     if (pushTimer > 20) setSprite('sPushLeft');
1251     if (status == CLIMBING) {
1252       if (level.isRopeAtPoint(x, y)) {
1253         if (kDown) setSprite('sClimbUp3'); else setSprite('sClimbUp2');
1254       } else {
1255         setSprite('sClimbUp');
1256       }
1257     }
1258     if (status == DUCKTOHANG) setSprite('sDuckToHangL');
1259   }
1264 // ////////////////////////////////////////////////////////////////////////// //
1265 void addScore (int delta) {
1266   if (!level.isNormalLevel()) return;
1267   //score += delta;
1268   if (delta == 0) return;
1269   level.stats.addMoney(delta);
1270   if (delta > 0) {
1271     level.xmoney += delta;
1272     level.collectCounter = min(100, level.collectCounter+20);
1273   }
1277 // ////////////////////////////////////////////////////////////////////////// //
1278 // for dead players too
1279 // first, the code will call `onObjectTouched()` for player
1280 // if it returned `false`, the code will call `obj.onTouchedByPlayer()`
1281 // note that player's handler is called *after* its frame thinker,
1282 // but object handler is called *before* frame thinker for the object
1283 // i.e. return `true` to block calling `obj.onTouchedByPlayer()`,
1284 // (but NOT object thinker)
1285 bool onObjectTouched (MapObject obj) {
1286   // is player dead?
1287   if (dead || global.plife <= 0) return false; // player may be rendered dead, but not yet transited to dead state
1289   if (obj isa ItemProjectileArrow && holdItem isa ItemWeaponBow && !stunned && global.arrows < 99) {
1290     if (fabs(obj.xVel) < 1 && fabs(obj.yVel) < 1 && !obj.stuck) {
1291       ++global.arrows;
1292       playSound('sndPickup');
1293       obj.instanceRemove();
1294       return true;
1295     }
1296   }
1298   // collect treasure
1299   auto treasure = ItemTreasure(obj);
1300   if (treasure && treasure.canCollect) {
1301     if (treasure.value) addScore(treasure.value);
1302     treasure.onCollected(self); // various other effects
1303     playSound(treasure.soundName);
1304     treasure.instanceRemove();
1305     return true;
1306   }
1308   // collect blood
1309   if (global.hasKapala && obj isa MapObjBlood) {
1310     global.bloodLevel += 1;
1311     level.MakeMapObject(obj.ix, obj.iy, 'oBloodSpark');
1312     obj.instanceRemove();
1314     if (global.bloodLevel > 8) {
1315       global.bloodLevel = 0;
1316       global.plife += 1;
1317       level.MakeMapObject(ix, iy-8, 'oHeart');
1318       playSound('sndKiss');
1319     }
1321     if (redColor < 55) redColor += 5;
1322     redToggle = false;
1323   }
1325   // other objects will take care of themselves
1326   return false;
1330 // return `false` to prevent
1331 // holdItem is valid
1332 bool onLoosingHeldItem (LostCause cause) {
1333   if (level.inWinCutscene != 0) return false;
1334   return true;
1338 // ////////////////////////////////////////////////////////////////////////// //
1339 // k8: don't even ask me! the following mess is almost straightforward port of the original Derek's code!
1340 private final void closeCape () {
1341   auto pp = PPCape(findPowerup('Cape'));
1342   if (pp) pp.open = false;
1346 private final void switchCape () {
1347   auto pp = PPCape(findPowerup('Cape'));
1348   if (pp) pp.open = !pp.open;
1352 final bool isCapeActiveAndOpen () {
1353   auto pp = PPCape(findPowerup('Cape'));
1354   return (pp && pp.active && pp.open);
1358 final bool isParachuteActive () {
1359   auto pp = findPowerup('Parachute');
1360   return (pp && pp.active);
1364 // ////////////////////////////////////////////////////////////////////////// //
1365 // for cutscenes
1366 bool checkSkipCutScene () {
1367   if (skipCutscenePressed) {
1368     return level.isKeyReleased(GameConfig::Key.Pay);
1369   } else {
1370     skipCutscenePressed = level.isKeyPressed(GameConfig::Key.Pay);
1371     return false;
1372   }
1375 int transKissTimer;
1378 bool forcePlayerControls () {
1379   if (level.inWinCutscene) {
1380     unpressAllKeys();
1381     level.winCutscenePlayerControl(self);
1382     return true;
1383   } else if (level.inIntroCutscene) {
1384     unpressAllKeys();
1385     level.introCutscenePlayerControl(self);
1386     //return false;
1387     return true;
1388   } else if (level.levelKind == GameLevel::LevelKind.Transition) {
1389     unpressAllKeys();
1391     if (checkSkipCutScene()) {
1392       level.playerExited = true;
1393       return true;
1394     }
1396     auto door = level.checkTileAtPoint(ix, iy, &level.cbCollisionExitTile);
1397     if (door) {
1398       kExitPressed = true;
1399       return true;
1400     }
1402     if (status == STOPPED) {
1403       if (--transKissTimer > 0) return true;
1404       status = STANDING;
1405     }
1407     transKissTimer = 0;
1408     auto dms = MonsterDamselKiss(level.isObjectAtPoint(ix+8, iy+4, delegate bool (MapObject o) { return (o isa MonsterDamselKiss); }));
1409     if (dms && !dms.kissed) {
1410       status = STOPPED;
1411       xVel = 0;
1412       yVel = 0;
1413       dms.kiss();
1414       transKissTimer = 30;
1415       return true;
1416     }
1418     kRight = true;
1419     kRightPressed = true;
1420     return true;
1421   }
1422   return false;
1426 // ////////////////////////////////////////////////////////////////////////// //
1427 private final void checkControlKeys (SpriteImage spr) {
1428   if (forcePlayerControls()) {
1429     if (movementBlocked) unpressAllKeys();
1430     if (kLeft) kLeftPushedSteps += 1; else kLeftPushedSteps = 0;
1431     if (kRight) kRightPushedSteps += 1; else kRightPushedSteps = 0;
1432     return;
1433   }
1435   kLeft = level.isKeyDown(GameConfig::Key.Left);
1436   if (movementBlocked) kLeft = false;
1437   if (kLeft) kLeftPushedSteps += 1; else kLeftPushedSteps = 0;
1438   kLeftPressed = level.isKeyPressed(GameConfig::Key.Left);
1439   kLeftReleased = level.isKeyReleased(GameConfig::Key.Left);
1441   kRight = level.isKeyDown(GameConfig::Key.Right);
1442   if (movementBlocked) kRight = false;
1443   if (kRight) kRightPushedSteps += 1; else kRightPushedSteps = 0;
1444   kRightPressed = level.isKeyPressed(GameConfig::Key.Right);
1445   kRightReleased = level.isKeyReleased(GameConfig::Key.Right);
1447   kUp = level.isKeyDown(GameConfig::Key.Up);
1448   kDown = level.isKeyDown(GameConfig::Key.Down);
1450   kJump = level.isKeyDown(GameConfig::Key.Jump);
1451   kJumpPressed = level.isKeyPressed(GameConfig::Key.Jump);
1452   kJumpReleased = level.isKeyReleased(GameConfig::Key.Jump);
1454   if (movementBlocked) unpressAllKeys();
1456   if (cantJump > 0) {
1457     kJump = false;
1458     kJumpPressed = false;
1459     kJumpReleased = false;
1460     --cantJump;
1461   } else if (spr && global.isTunnelMan && spr.Name == 'sTunnelAttackL' && !holdItem) {
1462     kJump = false;
1463     kJumpPressed = false;
1464     kJumpReleased = false;
1465     cantJump = max(0, cantJump-1);
1466   }
1468   kAttack = level.isKeyDown(GameConfig::Key.Attack);
1469   kAttackPressed = level.isKeyPressed(GameConfig::Key.Attack);
1470   kAttackReleased = level.isKeyReleased(GameConfig::Key.Attack);
1472   kItemPressed = level.isKeyPressed(GameConfig::Key.Switch);
1473   kRopePressed = level.isKeyPressed(GameConfig::Key.Rope);
1474   kBombPressed = level.isKeyPressed(GameConfig::Key.Bomb);
1476   kPayPressed = level.isKeyPressed(GameConfig::Key.Pay);
1478   if (movementBlocked) unpressAllKeys();
1480   kExitPressed = false;
1481   if (global.config.useDoorWithButton) {
1482     if (kPayPressed) kExitPressed = true;
1483   } else {
1484     if (kUp) kExitPressed = true;
1485   }
1487   if (stunned || dead) {
1488     unpressAllKeys();
1489     //level.clearKeysPressRelease();
1490   }
1494 // ////////////////////////////////////////////////////////////////////////// //
1495 // knock off monkeys that grabbed you
1496 void knockOffMonkeys () {
1497   level.forEachObject(delegate bool (MapObject o) {
1498     auto mk = EnemyMonkey(o);
1499     if (mk && !mk.dead && mk.status == GRAB) {
1500       mk.xVel = global.randOther(0, 1)-global.randOther(0, 1);
1501       mk.yVel = -4;
1502       mk.status = BOUNCE;
1503       mk.vineCounter = 20;
1504       mk.grabCounter = 60;
1505     }
1506     return false;
1507   });
1511 // ////////////////////////////////////////////////////////////////////////// //
1512 // fix collision with boulder (bug with non-aligned boulder)
1513 void hackBoulderCollision () {
1514   auto bld = level.checkTilesInRect(x0, y0, width, height, delegate bool (MapTile o) { return (o isa ObjBoulder); });
1515   if (bld && fabs(bld.xVel) <= 1) {
1516     writeln("IN BOULDER!");
1517     if (x0 < bld.x0) {
1518       int dx = bld.x0-x0;
1519       writeln("  LEFT: dx=", dx);
1520       if (dx <= 2) fltx = x0-dx;
1521     } else if (x1 > bld.x1) {
1522       int dx = x1-bld.x1;
1523       writeln("  RIGHT: dx=", dx);
1524       if (dx <= 2) fltx = x1-dx;
1525     }
1526   }
1530 // ////////////////////////////////////////////////////////////////////////// //
1531 bool checkHangTileDG (MapTile t) { return (t.solid || t.tree); }
1534 void checkPerformHang (bool colLeft, bool colRight) {
1535   if (status == HANGING || platformCharacterIs(ON_GROUND)) return;
1536   if ((kLeft && kRight) || (!kLeft && !kRight)) return;
1537   if (kLeft && !colLeft) {
1538 #ifdef HANG_DEBUG
1539     writeln("checkPerformHang: no left solid");
1540 #endif
1541     return;
1542   }
1543   if (kRight && !colRight) {
1544 #ifdef HANG_DEBUG
1545     writeln("checkPerformHang: no right solid");
1546 #endif
1547     return;
1548   }
1549   if (hangCount != 0) {
1550 #ifdef HANG_DEBUG
1551     writeln("checkPerformHang: hangCount=", hangCount);
1552 #endif
1553     return;
1554   }
1555   if (iy <= 16) return;
1556   int dx = (kLeft ? -9 : 9);
1557 #ifdef HANG_DEBUG
1558   writeln("checkPerformHang: trying to hang at ", dx);
1559 #endif
1561   bool doHang = false;
1563   if (global.hasGloves) {
1564     doHang = (yVel > 0 && !!level.checkTilesInRect(ix+dx, iy-6, 1, 2, &checkHangTileDG));
1565   } else {
1566     // hang on tree?
1567     doHang = !!level.checkTilesInRect(ix+dx, iy-6, 1, 2, &level.cbCollisionAnyTree);
1568 #ifdef HANG_DEBUG
1569     writeln("  tree: ", doHang);
1570 #endif
1571     // hang on solid?
1572     if (!doHang) {
1573       doHang = level.checkTilesInRect(ix+dx, iy-6, 1, 2) &&
1574                !level.isSolidAtPoint(ix+dx, iy-9) && !level.isSolidAtPoint(ix, iy+9);
1575 #ifdef HANG_DEBUG
1576       writeln("  solid: ", doHang);
1577 #endif
1578     }
1579     if (!doHang) {
1580 #ifdef HANG_DEBUG
1581       writeln("    solid at dx, -6(1): ", !!level.checkTilesInRect(ix+dx, iy-6, 1, 2));
1582       writeln("    solid at dx, -9(0): ", !!level.isSolidAtPoint(ix+dx, iy-9));
1583       writeln("    solid at  0, +9(0): ", !!level.isSolidAtPoint(ix, iy-9));
1584 #endif
1585 #ifdef EASIER_HANG
1586       doHang = level.checkTilesInRect(ix+dx, iy-6, 1, 2) &&
1587                !level.isSolidAtPoint(ix+dx, iy-10) && !level.isSolidAtPoint(ix, iy+9);
1588 #ifdef HANG_DEBUG
1589       if (!doHang) writeln("    easier hang failed");
1590 #endif
1591       /*
1592       if (!level.isSolidAtPoint(ix, iy-9)) {
1593         foreach (int dy; 6..24) {
1594           writeln("    solid at dx:-", dy, "(0): ", !!level.isSolidAtPoint(ix+dx, iy-dy));
1595         }
1596         writeln("   ix=", ix, "; iy=", iy);
1597       }
1598       */
1599 #endif
1600     }
1601   }
1603   if (doHang) {
1604     status = HANGING;
1605     moveSnap(1, 8);
1606     yVel = 0;
1607     yAcc = 0;
1608     grav = 0;
1609   }
1613 // ////////////////////////////////////////////////////////////////////////// //
1614 final void characterStepEvent () {
1615   if (climbSoundTimer > 0) {
1616     if (--climbSoundTimer == 0) {
1617       playSound(climbSndToggle ? 'sndClimb2' : 'sndClimb1');
1618       climbSndToggle = !climbSndToggle;
1619     }
1620   }
1622   auto spr = getSprite();
1623   checkControlKeys(spr);
1625   float xPrev = fltx, yPrev = flty;
1626   int x = ix, y = iy;
1628   // check collisions in various directions
1629   bool colSolidLeft = !!getPushableLeft(1);
1630   bool colSolidRight = !!getPushableRight(1);
1631   bool colLeft = !!isCollisionLeft(1);
1632   bool colRight = !!isCollisionRight(1);
1633   bool colTop = !!isCollisionTop(1);
1634   bool colBot = !!isCollisionBottom(1);
1635   bool colLadder = !!isCollisionLadder();
1636   bool colPlatBot = !!isCollisionBottom(1, &level.cbCollisionPlatform);
1637   bool colPlat = !!isCollision(&level.cbCollisionPlatform);
1638   //bool colWaterTop = !!isCollisionTop(1, &level.cbCollisionWater);
1639   bool colWaterTop = !!level.checkTilesInRect(x0, y0-1, width, 3, &level.cbCollisionWater);
1640   bool colIceBot = !!level.isIceAtPoint(x, y+8);
1642   bool runKey = false;
1643   if (level.isKeyDown(GameConfig::Key.Run)) { runHeld = 100; runKey = true; }
1644   if (level.isKeyDown(GameConfig::Key.Attack) && !whipping) { runHeld += 1; runKey = true; }
1645   if (!runKey || (!kLeft && !kRight)) runHeld = 0;
1647   // allows the character to run left and right
1648   // if state!=DUCKING and state!=LOOKING_UP and state!=CLIMBING
1649   if (status != CLIMBING && status != HANGING) {
1650     if (kLeftReleased && fabs(xVel) < 0.0001) xAcc -= 0.5;
1651     if (kRightReleased && fabs(xVel) < 0.0001) xAcc += 0.5;
1652     if (kLeft && !kRight) {
1653       if (colSolidLeft) {
1654         //xVel = 3; // in orig
1655         if (platformCharacterIs(ON_GROUND) && status != DUCKING) {
1656           xAcc -= 1;
1657           pushTimer += 10;
1658           //playSound('sndPush', unique:true);
1659         }
1660       } else if (kLeftPushedSteps > 2 && (dir == Dir.Left || fabs(xVel) < 0.0001)) {
1661         xAcc -= runAcc;
1662       }
1663       dir = Dir.Left;
1664       //if (platformCharacterIs(ON_GROUND) && fabs(xVel) > 0 && alarm[3] < 1) alarm[3] = floor(16/-xVel);
1665     }
1666     if (kRight && !kLeft) {
1667       if (colSolidRight) {
1668         //xVel = 3; // in orig
1669         if (platformCharacterIs(ON_GROUND) && status != DUCKING) {
1670           xAcc += 1;
1671           pushTimer += 10;
1672           //playSound('sndPush', unique:true);
1673         }
1674       } else if ((kRightPushedSteps > 2 || colSolidLeft) && (dir == Dir.Right || fabs(xVel) < 0.0001)) {
1675         xAcc += runAcc;
1676       }
1677       dir = Dir.Right;
1678       //if (platformCharacterIs(ON_GROUND) && fabs(xVel) > 0 && alarm[3] < 1) alarm[3] = floor(16/xVel);
1679     }
1680   }
1682   // ladders
1683   if (status == CLIMBING) {
1684     closeCape();
1685     kJumped = false;
1686     ladderTimer = 10;
1687     auto ladder = level.isLadderAtPoint(x, y);
1688     if (ladder) { x = ladder.ix+8; setX(x); }
1689     if (kLeft) dir = Dir.Left; else if (kRight) dir = Dir.Right;
1690     if (kUp) {
1691       // checks both ladder and laddertop
1692       if (level.isAnyLadderAtPoint(x, y-8)) {
1693         //writeln("LADDER00! old yAcc=", yAcc, "; climbAcc=", climbAcc, "; new yAcc=", yAcc-climbAcc);
1694         yAcc -= climbAcc;
1695         if (climbSoundTimer < 1) climbSoundTimer = climbSndSpeed;
1696         //!if (alarm[2] < 1) alarm[2] = climbSndSpeed;
1697       } else {
1698         /*
1699         for (int dy = -6; dy > -12; --dy) {
1700           ladder = level.isAnyLadderAtPoint(x, y+dy);
1701           if (ladder) {
1702             writeln("::: ", dy, ": plrx=", x, "; ladder.xy0=(", ladder.x0, ",", ladder.y0, "); ladder.ixy=(", ladder.ix, ",", ladder.iy, "); wdt=", ladder.width, "; hgt=", ladder.height, "; ladder class=", GetClassName(ladder.Class));
1703           }
1704         }
1705         */
1706         /*
1707         auto grid = level.miscTileGrid;
1708         foreach (MapTile t; grid.inCellPix(48, 96, grid.nextTag(), precise:false)) {
1709           writeln("at 48, 96: ", GetClassName(t.Class), "; pos=(", t.ix, ",", t.iy, ")");
1710         }
1711         foreach (MapTile t; grid.inCellPix(48, 94, grid.nextTag(), precise:false)) {
1712           writeln("at 48, 94: ", GetClassName(t.Class), "; pos=(", t.ix, ",", t.iy, ")");
1713         }
1714         foreach (int dy; 90..102) {
1715           ladder = level.isAnyLadderAtPoint(48, dy);
1716           if (ladder) {
1717             writeln("::: ", dy, ": plrx=", x, "; ladder.xy0=(", ladder.x0, ",", ladder.y0, "); ladder.ixy=(", ladder.ix, ",", ladder.iy, "); wdt=", ladder.width, "; hgt=", ladder.height, "; ladder class=", GetClassName(ladder.Class));
1718           }
1719         }
1720         */
1721       }
1722     } else if (kDown) {
1723       // checks both ladder and laddertop
1724       if (level.isAnyLadderAtPoint(x, y+8)) {
1725         yAcc += climbAcc;
1726         //!if (alarm[2] < 1) alarm[2] = climbSndSpeed;
1727         if (climbSoundTimer < 1) climbSoundTimer = climbSndSpeed;
1728       } else {
1729         status = FALLING;
1730       }
1731       if (colBot) status = STANDING;
1732     }
1733     // jump from ladder
1734     if (kJumpPressed && !whipping) {
1735       if (kLeft) xVel = -departLadderXVel; else if (kRight) xVel = departLadderXVel; else xVel = 0;
1736       //yAcc += departLadderYVel;
1737       //k8: was `0.6`, but with `0.4` we can jump onto the wall above, and with `0.6` we cannot
1738       yAcc = 0.4+departLadderYVel; // YASM 1.8.1 Fix for extra air when jumping off ladders due to increased climb speed option
1739       status = JUMPING;
1740       jumpButtonReleased = false;
1741       jumpTime = 0;
1742       ladderTimer = 5;
1743     }
1744   } else {
1745     if (ladderTimer > 0) ladderTimer -= 1;
1746   }
1748   if (platformCharacterIs(IN_AIR) && status != HANGING) yAcc += gravityIntensity;
1750   // player has landed
1751   if ((colBot || colPlatBot) && platformCharacterIs(IN_AIR) && yVel >= 0) {
1752     if (!colPlat || colBot) {
1753       yVel = 0;
1754       yAcc = 0;
1755       status = RUNNING;
1756     }
1757     playSound('sndLand');
1758   }
1759   if ((colBot || colPlatBot) && !colPlat) yVel = 0;
1761   // player has just walked off of the edge of a solid
1762   if (colBot == 0 && (!colPlatBot || colPlat) && platformCharacterIs(ON_GROUND)) {
1763     status = FALLING;
1764     yAcc += grav;
1765     kJumped = true;
1766     if (global.hasGloves) hangCount = 5;
1767   }
1769   if (colTop) {
1770          if (dead || stunned) yVel = -yVel*0.8;
1771     else if (status == JUMPING) yVel = fabs(yVel*0.3);
1772   }
1774   if ((colLeft && dir == Dir.Left) || (colRight && dir == Dir.Right)) {
1775     if (dead || stunned) xVel = -xVel*0.5; else xVel = 0;
1776   }
1778   // jumping
1779   if (kJumpReleased && platformCharacterIs(IN_AIR)) {
1780     kJumped = true;
1781   } else if (platformCharacterIs(ON_GROUND)) {
1782     closeCape();
1783     kJumped = false;
1784   }
1786   MapObject oWeb = none, oBlob = none;
1787   if (kJumpPressed) {
1788     oWeb = level.isObjectAtPoint(x, y, &level.cbIsObjectWeb);
1789     if (!oWeb) oBlob = level.isObjectAtPoint(x, y, &level.cbIsObjectBlob);
1790   }
1792   bool invokeJumpHelper = false;
1794   if (kJumpPressed && oWeb) {
1795     ItemWeb(oWeb).tear(1);
1796     yAcc += initialJumpAcc*2;
1797     yVel -= 3;
1798     xAcc += xVel/2;
1800     status = JUMPING;
1801     jumpButtonReleased = false;
1802     jumpTime = 0;
1804     grav = gravNorm;
1805     invokeJumpHelper = true;
1806   } else if (kJumpPressed && oBlob) {
1807     oBlob.hp -= 5;
1808     scrCreateBloblets(oBlob.x0+8, oBlob.y0+8, 1);
1809     playSound('sndHit');
1810     yAcc += initialJumpAcc*2;
1811     yVel -= 2;
1812     xAcc += xVel/2;
1813     status = JUMPING;
1814     jumpButtonReleased = false; // k8: was `jumpButtonRelease`
1815     jumpTime = 0;
1816     invokeJumpHelper = true;
1817   } else if (kJumpPressed && colWaterTop) {
1818     yAcc += initialJumpAcc*2;
1819     yVel -= 3;
1820     xAcc += xVel/2;
1822     status = JUMPING;
1823     jumpButtonReleased = false;
1824     jumpTime = 0;
1826     grav = gravNorm;
1827     invokeJumpHelper = true;
1828   } else if (global.hasCape && kJumpPressed && kJumped && platformCharacterIs(IN_AIR)) {
1829     switchCape();
1830   } else if (global.hasJetpack && !swimming && kJump && kJumped && platformCharacterIs(IN_AIR) && jetpackFuel > 0) {
1831     yAcc += initialJumpAcc;
1832     yVel = -1;
1833     jetpackFuel -= 1;
1834     if (jetpackFlaresTime < 1) jetpackFlaresTime = 3;
1835     //!if (alarm[10] < 1) alarm[10] = 3; // jetpack flares
1836     fallTimer = 0;
1838     status = JUMPING;
1839     jumpButtonReleased = false;
1840     jumpTime = 0;
1842     grav = 0;
1843     invokeJumpHelper = true;
1844   } else if (platformCharacterIs(ON_GROUND) && kJumpPressed && fallTimer == 0) {
1845     if (fabs(xVel) > 3 /*xVel > 3 || xVel < -3*/) {
1846       yAcc += initialJumpAcc*2;
1847       xAcc += xVel*2;
1848     } else {
1849       yAcc += initialJumpAcc*2;
1850       xAcc += xVel/2;
1851       //scrJumpHelper(); // move to location where player doesn't have to be on ground
1852     }
1853     if (global.hasJordans) {
1854       yAcc *= 3;
1855       yAccLimit = 12;
1856       grav = 0.5;
1857     } else if (global.hasSpringShoes) {
1858       yAcc *= 1.5;
1859     } else {
1860       yAccLimit = 6;
1861       grav = gravNorm;
1862     }
1864     playSound('sndJump');
1866     pushTimer = 0;
1868     // the "state" gets changed to JUMPING later on in the code
1869     status = FALLING;
1870     // "variable jumping" states
1871     jumpButtonReleased = false;
1872     jumpTime = 0;
1873     invokeJumpHelper = true;
1874   }
1876   if (kJumpPressed && invokeJumpHelper) scrJumpHelper(); // YASM 1.8.1
1878   if (jumpTime < jumpTimeTotal) jumpTime += 1;
1879   // let the character continue to jump
1880   if (!kJump) jumpButtonReleased = true;
1881   if (jumpButtonReleased) jumpTime = jumpTimeTotal;
1883   gravityIntensity = (jumpTime/jumpTimeTotal)*grav;
1885   if (kUp && platformCharacterIs(ON_GROUND) && !colLadder) {
1886     //k8:!!!looking = UP;
1887     if (xVel == 0 && xAcc == 0) status = LOOKING_UP;
1888   } else {
1889     //k8:!!!looking = 0;
1890   }
1892   if (!kUp && status == LOOKING_UP) status = STANDING;
1894   // hanging
1895   if (!colTop) {
1896     checkPerformHang(colLeft, colRight);
1897     x = ix;
1898     y = iy;
1900     // hang on stuck arrow
1901     if (status == FALLING && hangCount == 0 && y > 16 && !platformCharacterIs(ON_GROUND) &&
1902         !level.isSolidAtPoint(x, y+12)) // from Spelunky Natural
1903     {
1904       auto arrow = level.isObjectInRect(ix, iy, 16, 16, delegate bool (MapObject o) {
1905         /*
1906         writeln("---");
1907         writeln(" ARROW : (", o.x0, ",", o.y0, ")-(", o.x1, ",", o.y1, "); coll=", o.collidesWith(self));
1908         writeln(" PLAYER: (", x0, ",", y0, ")-(", x1, ",", y1, "); coll=", self.collidesWith(o), "; dy=", iy-o.iy);
1909         */
1910         if (o.stuck && iy-o.iy >= -6 && iy-o.iy <= -5 && o.collidesWith(self)) {
1911           //writeln(" *** HANG IS POSSIBLE! p5=", !!level.isObjectAtPoint(ix, iy-5, &level.cbIsObjectArrow), "; p6=", !!level.isObjectAtPoint(ix, iy-6, &level.cbIsObjectArrow));
1912           return true;
1913         }
1914         return false;
1915       }, castClass:ItemProjectileArrow, precise:false);
1916       if (arrow) {
1917         status = HANGING;
1918         // move_snap(1, 8); // was commented out in the original
1919         yVel = 0;
1920         yAcc = 0;
1921         grav = 0;
1922       }
1923       /*
1924       writeln("TRYING ARROW HANG ALLOWED");
1925       writeln("  Z00: ", !level.isObjectAtPoint(x, y-9, &level.cbIsObjectArrow));
1926       writeln("  Z01: ", !level.isObjectAtPoint(x, y+9, &level.cbIsObjectArrow));
1927       writeln("  Z02: ", !!level.isObjectAtPoint(x, y-5, &level.cbIsObjectArrow));
1928       writeln("  Z03: ", !!level.isObjectAtPoint(x, y-6, &level.cbIsObjectArrow));
1929       level.isObjectInRect(ix, iy, 16, 16, delegate bool (MapObject o) {
1930         writeln("---");
1931         writeln(" ARROW : (", o.x0, ",", o.y0, ")-(", o.x1, ",", o.y1, "); coll=", o.collidesWith(self));
1932         writeln(" PLAYER: (", x0, ",", y0, ")-(", x1, ",", y1, "); coll=", self.collidesWith(o), "; dy=", iy-o.iy);
1933         if (iy-o.iy >= -6 && iy-o.iy <= -5 && o.collidesWith(self)) {
1934           writeln(" *** HANG IS POSSIBLE! p5=", !!level.isObjectAtPoint(ix, iy-5, &level.cbIsObjectArrow), "; p6=", !!level.isObjectAtPoint(ix, iy-6, &level.cbIsObjectArrow));
1935         }
1936         return false;
1937       }, castClass:ItemProjectileArrow, precise:false);
1938       */
1939     }
1941     // hang on stuck arrow
1942     /*k8: this is not working due to collision issues; see the fixed code above
1943     if (status == FALLING && hangCount == 0 && y > 16 && !platformCharacterIs(ON_GROUND) &&
1944         !level.isSolidAtPoint(x, y+12) && // from Spelunky Natural
1945         !level.isObjectAtPoint(x, y-9, &level.cbIsObjectArrow) && !level.isObjectAtPoint(x, y+9, &level.cbIsObjectArrow))
1946     {
1947       //obj = instance_nearest(x, y-5, oArrow);
1948       auto arr0 = level.isObjectAtPoint(x, y-5, &level.cbIsObjectArrow);
1949       auto arr1 = level.isObjectAtPoint(x, y-6, &level.cbIsObjectArrow);
1950       if (arr0 || arr1) {
1951         writeln("ARROW HANG!");
1952         // get nearest arrow
1953         MapObject arr;
1954         if (arr1 && arr0) {
1955           arr = (arr0.distanceToPoint(x, y-5) < arr1.distanceToPoint(x, y-5) ? arr0 : arr1);
1956         } else {
1957           arr = (arr0 ? arr0 : arr1);
1958         }
1959         if (arr.stuck) {
1960           status = HANGING;
1961           // move_snap(1, 8); // was commented out in the original
1962           yVel = 0;
1963           yAcc = 0;
1964           grav = 0;
1965         }
1966       }
1967     }
1968     */
1969     /* this was commented in the original
1970     if (hangCount == 0 && y > 16 && !platformCharacterIs(ON_GROUND) && state == FALLING &&
1971         (collision_point(x, y-5, oTreeBranch, 0, 0) || collision_point(x, y-6, oTreeBranch, 0, 0)) &&
1972         !collision_point(x, y-9, oTreeBranch, 0, 0) && !collision_point(x, y+9, oTreeBranch, 0, 0))
1973     {
1974       state = HANGING;
1975       // move_snap(1, 8); // was commented out in the original
1976       yVel = 0;
1977       yAcc = 0;
1978       grav = 0;
1979     }
1980     */
1981   }
1983   if (hangCount > 0) --hangCount;
1985   if (status == HANGING) {
1986     closeCape();
1987     kJumped = false;
1988     if (kJumpPressed) {
1989       if (kDown) {
1990         if (global.hasGloves) {
1991           if (hangCount == 0 && y > 16 && !platformCharacterIs(ON_GROUND)) {
1992             if (kRight && colRight &&
1993                 (level.isSolidAtPoint(x+9, y-5) || level.isSolidAtPoint(x+9, y-6)))
1994             {
1995               grav = gravNorm;
1996               status = FALLING;
1997               yAcc -= grav;
1998               hangCount = 10;
1999             } else if (kLeft && colLeft &&
2000                        (level.isSolidAtPoint(x-9, y-5) || level.isSolidAtPoint(x-9, y-6)))
2001             {
2002               grav = gravNorm;
2003               status = FALLING;
2004               yAcc -= grav;
2005               hangCount = 10;
2006             } else {
2007               grav = gravNorm;
2008               status = FALLING;
2009               yAcc -= grav;
2010               hangCount = 5;
2011             }
2012           }
2013         } else {
2014           grav = gravNorm;
2015           status = FALLING;
2016           yAcc -= grav;
2017           hangCount = 5;
2018         }
2019       } else {
2020         grav = gravNorm;
2021         status = JUMPING;
2022         yAcc += initialJumpAcc*2;
2023         shiftX(dir == Dir.Right ? -2 : 2);
2024         x = ix;
2025         cameraBlockX = 3;
2026         hangCount = hangCountMax;
2027         if (level.isObjectAtPoint(x, y-5, &level.cbIsObjectArrow) || level.isObjectAtPoint(x, y-6, &level.cbIsObjectArrow)) hangCount /= 2; //Spelunky Natural
2028       }
2029     }
2030     if ((dir == Dir.Left && !isCollisionLeft(2)) ||
2031         (dir == Dir.Right && !isCollisionRight(2)))
2032     {
2033       grav = gravNorm;
2034       status = FALLING;
2035       yAcc -= grav;
2036       hangCount = 4;
2037     }
2038   } else {
2039     grav = gravNorm;
2040   }
2042   // pressing down while standing
2043   if (kDown && platformCharacterIs(ON_GROUND) && !whipping) {
2044     if (colBot) {
2045       status = DUCKING;
2046     } else if (colPlatBot) {
2047       // climb down ladder if possible, else jump down
2048       fallTimer = 0;
2049       if (!colBot) {
2050         //ladder = instance_place(x, y+16, oLadder);
2052         // from Spelunky Natural
2053         /*
2054         ladder = collision_line(x-4, y+16, x+4, y+16, oLadder, 0, 0);
2055         if (!ladder) ladder = collision_line(x-4, y+16, x+4, y+16, oLadderTop, 0, 0);
2056         */
2057         auto ladder = level.checkTilesInRect(x-4, y+16, 9, 1, &level.cbCollisionAnyLadder);
2058         //writeln("DOWN; cpb=", colPlatBot, "; cb=", colBot, "; ladder=", !!ladder);
2060         if (ladder) {
2061           if (abs(x-(ladder.x0+8)) < 4) {
2062             x = ladder.ix+8;
2063             setX(x);
2064             xVel = 0;
2065             yVel = 0;
2066             xAcc = 0;
2067             yAcc = 0;
2068             status = CLIMBING;
2069           }
2070         } else {
2071           shiftY(1);
2072           y = iy;
2073           status = FALLING;
2074           yAcc += grav;
2075           kJumped = true; // Spelunky Natural
2076         }
2077       }
2078       else {
2079         // the character can't move down because there is a solid in the way
2080         status = RUNNING;
2081       }
2082     }
2083   }
2084   if (!kDown && status == DUCKING) {
2085     status = STANDING;
2086     xVel = 0;
2087     xAcc = 0;
2088   }
2089   if (xVel == 0 && xAcc == 0 && status == RUNNING) status = STANDING;
2090   if (xAcc != 0 && status == STANDING) status = RUNNING;
2091   if (yVel < 0 && platformCharacterIs(IN_AIR) && status != HANGING) status = JUMPING;
2092   if (yVel > 0 && platformCharacterIs(IN_AIR) && status != HANGING) {
2093     status = FALLING;
2094     setCollisionBounds(-5, -6, 5, 8);
2095   } else {
2096     setCollisionBounds(-5, -6, 5, 8);
2097   }
2099   // CLIMB LADDER
2100   bool colPointLadder = !!level.isAnyLadderAtPoint(x, y);
2102   /* this was commented in the original
2103   if ((kUp && platformCharacterIs(IN_AIR) && collision_point(x, y-8, oLadder, 0, 0) && ladderTimer == 0) ||
2104       (kUp && colPointLadder && ladderTimer == 0) ||
2105       (kDown && colPointLadder && ladderTimer == 0 && platformCharacterIs(ON_GROUND) && collision_point(x, y+9, oLadderTop, 0, 0) && xVel == 0))
2106   {
2107     ladder = 0;
2108     ladder = instance_place(x, y-8, oLadder);
2109     if (instance_exists(ladder)) {
2110       if (abs(x-(ladder.x0+8)) < 4) {
2111         x = ladder.ix+8;
2112         setX(x);
2113         if (!collision_point(x, y, oLadder, 0, 0) && !collision_point(x, y, oLadderTop, 0, 0)) { y = ladder.iy+14; setY(y); }
2114         xVel = 0;
2115         yVel = 0;
2116         xAcc = 0;
2117         yAcc = 0;
2118         state = CLIMBING;
2119       }
2120     }
2121   }*/
2123   // Spelunky Natural - Multiple changes to this big "if" condition
2124   if ((kUp && platformCharacterIs(IN_AIR) && ladderTimer == 0 && level.checkTilesInRect(x-2, y-8, 5, 1, &level.cbCollisionLadder)) ||
2125       (kUp && colPointLadder && ladderTimer == 0) ||
2126       (kDown && colPointLadder && ladderTimer == 0 && platformCharacterIs(ON_GROUND) && xVel == 0 && level.isLadderTopAtPoint(x, y+9)) ||
2127       ((kUp || kDown) && status == HANGING && level.checkTilesInRect(x-2, y, 5, 1, &level.cbCollisionLadder)))
2128   {
2129     //ladder = 0;
2130     //auto ladder = instance_place(x, y-8, oLadder);
2131     auto ladder = level.isLadderAtPoint(x, y-8);
2132     if (ladder) {
2133       //writeln("LADDER01! plrx=", x, "; ladder.x0=", ladder.x0, "; ladder.ix=", ladder.ix, "; ladder class=", GetClassName(ladder.Class));
2134       if (abs(x-(ladder.x0+8)) < 4) {
2135         x = ladder.ix+8;
2136         setX(x);
2137         if (!level.isAnyLadderAtPoint(x, y)) { y = ladder.y0+14; setY(y); }
2138         xVel = 0;
2139         yVel = 0;
2140         xAcc = 0;
2141         yAcc = 0;
2142         status = CLIMBING;
2143       }
2144     }
2145   }
2147   /* this was commented in the original
2148   if (sprite_index == sDuckToHangL || sprite_index == sDamselDtHL) {
2149     ladder = 0;
2150     if (facing == LEFT && collision_rectangle(x-8, y, x, y+16, oLadder, 0, 0) && !collision_point(x-4, y+16, oSolid, 0, 0)) {
2151       ladder = instance_nearest(x-4, y+16, oLadder);
2152     } else if (facing == RIGHT && collision_rectangle(x, y, x+8, y+16, oLadder, 0, 0) && !collision_point(x+4, y+16, oSolid, 0, 0)) {
2153       ladder = instance_nearest(x+4, y+16, oLadder);
2154     }
2155     if (ladder) {
2156       x = ladder.ix+8;
2157       setX(x);
2158       xVel = 0;
2159       yVel = 0;
2160       xAcc = 0;
2161       yAcc = 0;
2162       state = CLIMBING;
2163     }
2164   }
2166   if (colLadder && state == CLIMBING && kJumpPressed && !whipping) {
2167     if (kLeft) xVel = -departLadderXVel; else if (kRight) xVel = departLadderXVel; else xVel = 0;
2168     yAcc += departLadderYVel;
2169     state = JUMPING;
2170     jumpButtonReleased = false;
2171     jumpTime = 0;
2172     ladderTimer = 5;
2173   }
2174   */
2176   // calculate horizontal/vertical friction
2177   if (status == CLIMBING) {
2178     xFric = frictionClimbingX;
2179     yFric = frictionClimbingY;
2180   } else {
2181     //if (runKey && platformCharacterIs(ON_GROUND) && runHeld >= 10)
2182     if ((runKey && runHeld >= 10) && (platformCharacterIs(ON_GROUND) || global.config.toggleRunAnywhere)) {
2183       // YASM 1.8.1
2184       if (kLeft) {
2185         // run
2186         xVel -= 0.1;
2187         xVelLimit = 6;
2188         xFric = frictionRunningFastX;
2189       } else if (kRight) {
2190         xVel += 0.1;
2191         xVelLimit = 6;
2192         xFric = frictionRunningFastX;
2193       }
2194     } else if (status == DUCKING) {
2195       if (xVel < 2 && xVel > -2) {
2196         xFric = 0.2;
2197         xVelLimit = 3;
2198         imageSpeed = 0.8;
2199       } else if (kLeft && global.config.downToRun) {
2200         // run
2201         xVel -= 0.1;
2202         xVelLimit = 6;
2203         xFric = frictionRunningFastX;
2204       } else if (kRight && global.config.downToRun) {
2205         xVel += 0.1;
2206         xVelLimit = 6;
2207         xFric = frictionRunningFastX;
2208       } else {
2209         xVel *= 0.8;
2210         if (xVel < 0.5) xVel = 0;
2211         xFric = 0.2;
2212         xVelLimit = 3;
2213         imageSpeed = 0.8;
2214       }
2215     } else {
2216       // decrease the friction when the character is "flying"
2217       if (platformCharacterIs(IN_AIR)) {
2218         if (dead || stunned) xFric = 1.0; else xFric = 0.8;
2219       } else {
2220         xFric = frictionRunningX;
2221       }
2222     }
2224     /* // ORIGINAL RUN/WALK xVel/xFric code  this was commented in the original
2225     if (runKey && platformCharacterIs(ON_GROUND) && runHeld >= 10) {
2226       if (kLeft) {
2227         // run
2228         xVel -= 0.1;
2229         xVelLimit = 6;
2230         xFric = frictionRunningFastX;
2231       } else if (kRight) {
2232         xVel += 0.1;
2233         xVelLimit = 6;
2234         xFric = frictionRunningFastX;
2235       }
2236     } else if (state == DUCKING) {
2237       if (xVel < 2 && xVel > -2) {
2238         xFric = 0.2
2239         xVelLimit = 3;
2240         imageSpeed = 0.8;
2241       } else if (kLeft && global.downToRun) {
2242         // run
2243         xVel -= 0.1;
2244         xVelLimit = 6;
2245         xFric = frictionRunningFastX;
2246       } else if (kRight && global.downToRun) {
2247         xVel += 0.1;
2248         xVelLimit = 6;
2249         xFric = frictionRunningFastX;
2250       } else {
2251         xVel *= 0.8;
2252         if (xVel < 0.5) xVel = 0;
2253         xFric = 0.2
2254         xVelLimit = 3;
2255         imageSpeed = 0.8;
2256       }
2257     } else {
2258       // decrease the friction when the character is "flying"
2259       if (platformCharacterIs(IN_AIR)) {
2260         if (dead || stunned) xFric = 1.0; else xFric = 0.8;
2261       } else {
2262         xFric = frictionRunningX;
2263       }
2264     }
2265     */
2267     // stuck on web or underwater
2268     if (level.isObjectAtPoint(x, y, &level.cbIsObjectWeb)) {
2269       xFric = 0.2;
2270       yFric = 0.2;
2271       fallTimer = 0;
2272     } else if (level.isObjectAtPoint(x, y, &level.cbIsObjectBlob)) {
2273       // blob enemy
2274       //obj = instance_place(x, y, oBlob); this was commented in the original
2275       //xVel += obj.xVel; this was commented in the original
2276       xFric = 0.1;
2277       yFric = 0.3;
2278       fallTimer = 0;
2279     } else if (level.isWaterAtPoint(x, y/*, oWater, -1, -1*/)) {
2280       closeCape();
2281       //if (!runKey && global.toggleRunAnywhere) xFric = frictionRunningX; // YASM 1.8.1 this was commented in the original
2282       if (!platformCharacterIs(ON_GROUND)) xFric = frictionRunningX;
2283       if (status == FALLING && yVel > 0) {
2284         // Spelunky Natural
2285              if (global.config.naturalSwim && kUp) yFric = 0.2;
2286         else if (global.config.naturalSwim && kDown) yFric = 0.8;
2287         else yFric = 0.5;
2288       } else if (!level.isWaterAtPoint(x, y-9/*, oWater, -1, -1*/)) {
2289         yFric = 1;
2290       } else {
2291         yFric = 0.9;
2292       }
2293       if (yVel < -6 && global.config.noDolphin) {
2294         // Spelunky Natural (changed from -4 to -6)
2295         yVel = -6;
2296       }
2297     } else {
2298       swimming = false;
2299       yFric = 1;
2300     }
2301   }
2303   if (colIceBot && status != DUCKING && !global.hasSpikeShoes) {
2304     xFric = 0.98;
2305     yFric = 1;
2306   }
2308   // YASM 1.8.1
2309   if (global.config.toggleRunAnywhere) {
2310     if (!kJump && !kDown && !runKey) xVelLimit = 3;
2311   }
2313   // RUNNING
2314   if (platformCharacterIs(ON_GROUND)) {
2315          if (status == RUNNING && kLeft && colLeft) pushTimer += 1;
2316     else if (status == RUNNING && kRight && colRight) pushTimer += 1;
2317     else pushTimer = 0;
2319     //if (platformCharacterIs(ON_GROUND) && !kJump && !kDown && !runKey) this was commented in the original
2320     if (!kJump && !kDown && !runKey) xVelLimit = 3;
2322     /* this was commented in the original
2323     // ledge flip
2324     if (state == DUCKING && fabs(xVel) < 3 && facing == LEFT &&
2325         //collision_point(x, y+9, oSolid, 0, 0) && !collision_point(x-1, y+9, oSolid, 0, 0) && kLeft)
2326         collision_point(x, y+9, oSolid, 0, 0) && !collision_line(x-1, y+9, x-10, y+9, oSolid, 0, 0) && kLeft)
2327     */
2329     // ledge flip
2330     int dhdir = 0;
2331          if (kLeft && dir == Dir.Left) dhdir = -1;
2332     else if (kRight && dir == Dir.Right) dhdir = 1;
2334     if (dhdir && status == DUCKING && fabs(xVel) < 3+(dhdir < 0 ? 1 : 0) &&
2335         level.isSolidAtPoint(x, y+9) && !level.checkTilesInRect(x+(dhdir < 0 ? -8 : 1), y+9, 8, 8))
2336     {
2337       status = DUCKTOHANG;
2338       if (holdItem) {
2339         if (!global.config.scumFlipHold || holdItem.heavy) {
2340           /*
2341           holdItem.heldBy = none;
2342           if (holdItem.objName == 'GoldIdol') holdItem.shiftY(-8);
2343           */
2344           //else if (holdItem.type == "Block Item") { with (oBlockPreview) instance_destroy(); }
2345           scrDropItem(LostCause.Hang, (dir == Dir.Left ? -1 : 1), -4);
2346         }
2347       }
2348       knockOffMonkeys();
2349     }
2350   }
2352   if (status == DUCKTOHANG) {
2353     setXY(xPrev, yPrev);
2354     x = ix;
2355     y = iy;
2356     xVel = 0;
2357     yVel = 0;
2358     xAcc = 0;
2359     yAcc = 0;
2360     grav = 0;
2361   }
2363   // parachute and cape
2364   if (!level.inWinCutscene) {
2365     if (isParachuteActive() || isCapeActiveAndOpen()) yFric = 0.5;
2366   }
2368   if (pushTimer > 100) pushTimer = 100;
2370   // limits the acceleration if it is too extreme
2371   xAcc = fclamp(xAcc, -xAccLimit, xAccLimit);
2372   yAcc = fclamp(yAcc, -yAccLimit, yAccLimit);
2374   // applies the acceleration
2375   xVel += xAcc;
2376   if (dead || stunned) yVel += 0.6; else yVel += yAcc;
2378   // nullifies the acceleration
2379   xAcc = 0;
2380   yAcc = 0;
2382   // applies the friction to the velocity, now that the velocity has been calculated
2383   xVel *= xFric;
2384   yVel *= yFric;
2386   auto oBall = getMyBall();
2387   // apply ball and chain
2388   if (oBall) {
2389     int distsq = (ix-oBall.ix)*(ix-oBall.ix)+(iy-oBall.iy)*(iy-oBall.iy);
2390     if (distsq >= 24*24) {
2391       if (xVel > 0 && oBall.ix < ix && abs(oBall.ix-ix) > 24) xVel = 0;
2392       if (xVel < 0 && oBall.ix > ix && abs(oBall.ix-ix) > 24) xVel = 0;
2393       if (yVel > 0 && oBall.iy < iy && abs(oBall.iy-iy) > 24) {
2394         if (abs(oBall.ix-ix) < 1) {
2395           //teleportTo(destx:oBall.ix);
2396           fltx = oBall.fltx;
2397           prevFltX = oBall.prevFltX;
2398           x = ix;
2399         } else if (oBall.ix < ix && !kRight) {
2400                if (xVel > 0) xVel *= -0.25;
2401           else if (xVel == 0) xVel -= 1;
2402         } else if (oBall.ix > ix && !kLeft) {
2403                if (xVel < 0) xVel *= -0.25;
2404           else if (xVel == 0) xVel += 1;
2405         }
2406         yVel = 0;
2407         fallTimer = 0;
2408       }
2409       if (yVel < 0 && oBall.iy > iy && abs(oBall.iy-iy) > 24) yVel = 0;
2410     }
2411   }
2413   // apply the limits since the velocity may be too extreme
2414   if (!dead && !stunned) xVel = fclamp(xVel, -xVelLimit, xVelLimit);
2415   yVel = fclamp(yVel, -yVelLimit, yVelLimit);
2417   // approximates the "active" variables
2418   if (fabs(xVel) < 0.0001) xVel = 0;
2419   if (fabs(yVel) < 0.0001) yVel = 0;
2420   if (fabs(xAcc) < 0.0001) xAcc = 0;
2421   if (fabs(yAcc) < 0.0001) yAcc = 0;
2423   bool wasInWall = !!isCollision();
2424   moveRel(xVel, yVel);
2426   // don't go out of level (if we're not in ending sequence)
2427   if (!level.inWinCutscene && !level.inIntroCutscene) {
2428          if (ix < 0) fltx = 0;
2429     else if (ix > level.tilesWidth*16-16) fltx = level.tilesWidth*16-16;
2430     if (iy < 0) flty = 0;
2432     if (!dead) hackBoulderCollision();
2434     if (!wasInWall && isCollision()) {
2435       writeln("** FUUUU (XXX)");
2436       if (isCollisionBottom(0) && !isCollisionBottom(-2)) {
2437         flty = iy-2;
2438       }
2439       // we can stuck in the wall with this
2440       if (isCollisionLeft(0)) {
2441         writeln("** FUUUU (001: left)");
2442         while (isCollisionLeft(0) && !isCollisionRight(1)) shiftX(1);
2443       } else if (isCollisionRight(0)) {
2444         writeln("** FUUUU (001: right)");
2445         while (isCollisionRight(0) && !isCollisionLeft(1)) shiftX(-1);
2446       }
2447     }
2449     if (!dead) hackBoulderCollision();
2451     // move out of wall by 1 px, if possible
2452     if (!dead && isCollision()) {
2453       if (isCollisionBottom(0) && !isCollisionBottom(-1)) flty = iy-1;
2454       if (isCollisionTop(0) && !isCollisionTop(1)) flty = iy+1;
2455       if (isCollisionLeft(0) && !isCollisionLeft(1)) fltx = ix+1;
2456       if (isCollisionRight(0) && !isCollisionRight(-1)) fltx = ix-1;
2457     }
2459     if (!dead && isCollision()) {
2460       //k8:HACK: try to duck
2461       bool wallDeath = true;
2462       if (platformCharacterIs(ON_GROUND)) {
2463         setCollisionBounds(-5, -6, 5, 8);
2464         wallDeath = !!isCollision();
2465         if (wallDeath) {
2466           setCollisionBounds(-8, -6, 8, 8);
2467         } else {
2468           // force ducking
2469           status = DUCKING;
2470         }
2471       }
2473       if (wallDeath) {
2474         foreach (; 0..6) {
2475           if (isCollision()) {
2476                  if (isCollisionLeft(0) && !isCollisionRight(4)) fltx = ix+1;
2477             else if (isCollisionRight(0) && !isCollisionLeft(4)) fltx = ix-1;
2478             else if (isCollisionBottom(0) && !isCollisionTop(4)) flty = iy-1;
2479             else if (isCollisionTop(0) && !isCollisionBottom(4)) flty = iy+1;
2480             else break;
2481           }
2482         }
2484         if (wallDeath && isCollision()) {
2485           if (!dead) level.addDeath('wall');
2486           //visible = false;
2487           dead = true;
2488           writeln("PLAYER KILLED BY WALL");
2489           global.plife = 0; // oops
2490         }
2491       }
2492     }
2493   } else {
2494     // in cutscene
2495     //writeln("flty=", flty, "; iy=", iy);
2496     if (flty <= 0) {
2497       status = STANDING;
2498     }
2499   }
2501   // figures out what the sprite index of the character should be
2502   characterSprite();
2504   // sets the previous state and the previously previous state
2505   statePrevPrev = statePrev;
2506   statePrev = status;
2508   // calculates the imageSpeed based on the character's velocity
2509   if (status == RUNNING || status == DUCKING || status == LOOKING_UP) {
2510     if (status == RUNNING || status == LOOKING_UP) imageSpeed = fabs(xVel)*runAnimSpeed+0.1;
2511   }
2513   if (status == CLIMBING) imageSpeed = sqrt(xVel*xVel+yVel*yVel)*climbAnimSpeed;
2515   if (xVel >= 4 || xVel <= -4) {
2516     imageSpeed = 1;
2517     if (platformCharacterIs(ON_GROUND)) {
2518       setCollisionBounds(-8, -6, 8, 8);
2519     } else {
2520       setCollisionBounds(-5, -6, 5, 8);
2521     }
2522   } else {
2523     setCollisionBounds(-5, -6, 5, 8);
2524   }
2526   if (whipping) imageSpeed = 1;
2528   if (status == DUCKTOHANG) {
2529     imageFrame = 0;
2530     imageSpeed = 0.8;
2531   }
2533   // limit the imageSpeed at 1 so the animation always looks good
2534   if (imageSpeed > 1) imageSpeed = 1;
2536   //if (kItemPressed) writeln("ITEM! dead=", dead, "; stunned=", stunned, "; active=", active);
2537   if (dead || stunned || !active) {
2538     // do nothing
2539   } else if (/*inGame &&*/ kItemPressed && !whipping) {
2540     // SWITCH
2541     if (kUp) scrSwitchToStickyBombs(); else scrSwitchToNextItem();
2542   } else if (/*inGame &&*/ kRopePressed && global.rope > 0 && !whipping) {
2543     if (!kDown && colTop) {
2544       // do nothing
2545     } else {
2546       launchRope(kDown, doDrop:true);
2547     }
2548   } else if (/*inGame &&*/ kBombPressed && global.bombs > 0 && !whipping) {
2549     if (holdItem isa ItemWeaponBow && bowArmed) {
2550       if (holdArrow != ARROW_BOMB) {
2551         //writeln("set bow arrows to bomb");
2552         holdArrow = ARROW_BOMB;
2553       } else {
2554         //writeln("set bow arrows to normal");
2555         holdArrow = ARROW_NORM;
2556       }
2557     } else {
2558       scrLaunchBomb();
2559     }
2560   }
2563   // open chest/crate
2564   if (!dead && !stunned && kUp && kAttackPressed) {
2565     auto octr = ItemOpenableContainer(level.isObjectInRect(ix, iy, width, height, delegate bool (MapObject o) {
2566       return (o isa ItemOpenableContainer);
2567     }));
2568     if (octr) {
2569       if (octr.openMe()) kAttackPressed = false;
2570     }
2571   }
2574   // use weapon / attack
2575   if (!dead && !stunned && kAttackPressed && !holdItem /*&& !pickedItem*/) {
2576     bowArmed = false;
2577     bowStrength = 0;
2578     sndStopSound('sndBowPull');
2579     if (status != DUCKING && status != DUCKTOHANG && !whipping && !isExitingSprite()) {
2580       imageSpeed = 0.6;
2581       if (global.isTunnelMan) {
2582         if (platformCharacterIs(ON_GROUND) || platformCharacterIs(IN_AIR)) {
2583           setSprite('sTunnelAttackL');
2584           whipping = true;
2585         }
2586       } else if (global.isDamsel) {
2587         setSprite('sDamselAttackL');
2588         whipping = true;
2589       } else {
2590         setSprite('sAttackLeft');
2591         whipping = true;
2592       }
2593     } else if (kDown && !pickedItem) {
2594       // pick up item
2595       //HACK: always select dice to throw if there are two dices there
2596       MapObject diceToThrow = level.isObjectInRect(x-8, y, 9, 9, delegate bool (MapObject o) {
2597         if (o.spectral || !o.canPickUp) return false;
2598         if (ItemDice(o).isReadyToThrowForBet) return o.collidesWith(self);
2599         return false;
2600       }, precise:false, castClass:ItemDice);
2601       MapObject obj;
2602       if (diceToThrow) {
2603         obj = diceToThrow;
2604       } else {
2605         obj = level.isObjectInRect(x-8, y, 9, 9, delegate bool (MapObject o) {
2606           if (o.spectral || !o.canPickUp) return false;
2607           if (!o.collidesWith(self)) return false;
2608           return o.onCanBePickedUp(self);
2609           /*
2610           if (o isa MapItem) return (o.active && o.canPickUp && !o.spectral);
2611           if (o isa MapEnemy) return (o.active && o.canPickUp && !o.spectral && (o.dead || o.status >= MapObject::STUNNED || o.meGoldMonkey));
2612           */
2613           return false;
2614         }, precise:false);
2615       }
2616       if (!obj && diceToThrow) obj = diceToThrow;
2617       if (obj) {
2618         // `canPickUp` is checked in callback
2619         if (/*obj.canPickUp &&*/ true /*k8: do we really need this? !level.isSolidAtPoint(obj.ix+2, obj.iy)*/) {
2620           //pickupItemType = holdItem.type;
2621           //!if (isAshShotgun(holdItem)) pickupItemType = "Boomstick";
2622           //!if (isGoldMonkey(obj) and obj.status &lt; 98) obj.status = 0; // do not play walk animation while held
2624           if (!obj.onTryPickup(self)) {
2625             if (obj.isInstanceAlive) scrPickupItem(obj);
2626           }
2628           /+!
2629           if (holdItem.type == "Bow" and holdItem.new) {
2630             holdItem.new = false;
2631             global.arrows += 6;
2632             if (global.arrows &gt; 99) global.arrows = 99;
2633           }
2634           +/
2635         }
2636       }
2637     }
2638   } else if (!dead && !stunned) {
2639     if (holdItem isa ItemWeaponBow) {
2640       //writeln("BOW! kAttack=", kAttack, "; kAttackPressed=", kAttackPressed, "; bowArmed=", bowArmed, "; bowStrength=", bowStrength, "; holdArrow=", holdArrow);
2641       if (kAttackPressed) {
2642         if (scrPlayerIsDucking()) {
2643           scrUsePutItemOnGround();
2644         } else if (!bowArmed) {
2645           bowStrength = 0;
2646           ItemWeaponBow(holdItem).armBow(self);
2647         }
2648       }
2649       if (kAttack) {
2650         if (bowArmed && bowStrength < 12) {
2651           bowStrength += 0.2;
2652           //writeln("arming: ", bowStrength);
2653         } else {
2654           sndStopSound('sndBowPull');
2655         }
2656       } else {
2657         //writeln("   xxBOW!");
2658         // ...and shoot
2659         scrFireBow();
2660       }
2661       if (!holdArrow) holdArrow = ARROW_NORM;
2662     } else {
2663       if (kAttackPressed && holdItem) scrUseItem();
2664     }
2665   }
2667   // buy items
2668   if (!dead && !stunned && kPayPressed) {
2669       // find nearest shopkeeper
2670     auto sc = MonsterShopkeeper(level.findNearestObject(ix, iy, delegate bool (MapObject o) {
2671       auto sc = MonsterShopkeeper(o);
2672       if (!sc) return false;
2673       //if (needCraps && sc.stype != 'Craps') return false;
2674       if (sc.dead || sc.angered || sc.outlaw) return false;
2675       return sc.canSellItem(self, holdItem);
2676     }));
2677     if (level.isInShop(ix/16, iy/16)) {
2678       // if no shopkeepers found, just use it
2679       if (!sc) {
2680         if (holdItem) {
2681           holdItem.forSale = false;
2682           holdItem.onTryPickup(self);
2683         }
2684       } else if (global.thiefLevel == 0 && !global.murderer) {
2685         // only law-abiding players can buy/sell items or play games
2686         if (holdItem) writeln("shop item interaction: ", holdItem.objName, "; cost=", holdItem.cost);
2687         if (sc.doSellItem(self, holdItem)) {
2688           // use it
2689           if (holdItem) {
2690             holdItem.forSale = false;
2691             holdItem.onTryPickup(self);
2692           }
2693         }
2694         if (holdItem && !holdItem.isInstanceAlive) {
2695           holdItem = none;
2696           scrSwitchToPocketItem(forceIfEmpty:false); // just in case
2697         }
2698       }
2699     } else {
2700       // use pickup, if any
2701       if (holdItem isa ItemPickup) {
2702         // make nearest shopkeeper angry (an unlikely situation, but still...)
2703         if (sc && holdItem.forSale) level.scrShopkeeperAnger(GameLevel::SCAnger.ItemStolen);
2704         holdItem.forSale = false;
2705         holdItem.onTryPickup(self);
2706       } else {
2707         pickupsAround.clear();
2708         level.isObjectInRect(x0, y0, width, height, delegate bool (MapObject o) {
2709           auto pk = ItemPickup(o);
2710           if (pk && pk.collidesWith(self)) {
2711             bool found = false;
2712             foreach (auto opk; pickupsAround) if (opk == pk) { found = true; break; }
2713             if (!found) pickupsAround[$] = pk;
2714           }
2715           return false;
2716         }, precise:false);
2717         // now try to use all pickups
2718         foreach (ItemPickup pk; pickupsAround) {
2719           if (pk.isInstanceAlive) {
2720             if (sc && pk.forSale) level.scrShopkeeperAnger(GameLevel::SCAnger.ItemStolen);
2721             pk.forSale = false;
2722             pk.onTryPickup(self);
2723           }
2724         }
2725         pickupsAround.clear();
2726       }
2727     }
2728   }
2731 transient array!ItemPickup pickupsAround;
2734 // ////////////////////////////////////////////////////////////////////////// //
2735 override bool initialize () {
2736   if (!::initialize()) return false;
2738   powerups.length = 0;
2739   powerups[$] = SpawnObject(PPParachute);
2740   powerups[$] = SpawnObject(PPCape);
2742   foreach (PlayerPowerup pp; powerups) pp.owner = self;
2744   if (global.isDamsel) {
2745     desc = "Damsel";
2746     desc2 = "An athletic, unfittingly-dressed woman with extremely awkward running form.";
2747     setSprite('sDamselLeft');
2748   } else if (global.isTunnelMan) {
2749     desc = "Tunnel Man";
2750     desc2 = "A miner from the desert. His tools are a cut above the rest.";
2751     setSprite('sTunnelLeft');
2752   } else {
2753     desc = "Spelunker";
2754     desc2 = "A strange little man who spends his time exploring caverns. He wants to be just like Indiana Jones when he grows up.";
2755     setSprite('sStandLeft');
2756   }
2758   swimming = false;
2760   dir = Dir.Right;
2762   // scum ClimbSpeed
2763   switch (global.config.scumClimbSpeed) {
2764     case 2:
2765       climbAcc = 0.9;
2766       climbAnimSpeed = 0.4;
2767       climbSndSpeed = 6;
2768       break;
2769     case 3:
2770       climbAcc = 1.2;
2771       climbAnimSpeed = 0.45;
2772       climbSndSpeed = 5;
2773       break;
2774     case 4:
2775       climbAcc = 1.5;
2776       climbAnimSpeed = 0.5;
2777       climbSndSpeed = 4;
2778       break;
2779     case 5:
2780       climbAcc = 1.8;
2781       climbAnimSpeed = 0.5;
2782       climbSndSpeed = 3;
2783       break;
2784     default:
2785       climbAcc = 0.6;       // how fast the character will climb
2786       climbAnimSpeed = 0.4; // relates to how fast the climbing animation should go
2787       climbSndSpeed = 8;
2788       break;
2789   }
2791   // sets the collision bounds to fit the default sprites (you can edit the arguments of the script)
2792   setCollisionBounds(-5, -5, 5, 8); // setCollisionBounds(-5, -8, 5, 8);
2794   statePrev = status;
2795   statePrevPrev = statePrev;
2796   gravityIntensity = grav;  // this variable describes the current force due to gravity (this variable is altered for variable jumping)
2797   jumpTime = jumpTimeTotal; // current time of the jump (0=start of jump, jumpTimeTotal=end of jump)
2799   return true;
2803 // ////////////////////////////////////////////////////////////////////////// //
2804 override void onAnimationLooped () {
2805   auto spr = getSprite();
2806   if (spr.Name == 'sAttackLeft' || spr.Name == 'sDamselAttackL' || spr.Name == 'sTunnelAttackL') {
2807     whipping = false;
2808     if (holdItem) holdItem.visible = true;
2809   } else if (spr.Name == 'sDuckToHangL' || spr.Name == 'sDamselDtHL' || spr.Name == 'sTunnelDtHL') {
2810     shiftY(16);
2811     moveSnap(1, 8);
2812     int x = ix, y = iy;
2813     xVel = 0;
2814     yVel = 0;
2815     xAcc = 0;
2816     yAcc = 0;
2817     grav = 0;
2818     MapTile obj;
2819     if (dir == Dir.Left) {
2820       // left
2821       obj = level.isAnyLadderAtPoint(x-8, y);
2822     } else {
2823       // right
2824       obj = level.isAnyLadderAtPoint(x+8, y);
2825     }
2826     if (obj) {
2827       status = CLIMBING;
2828       setX(obj.ix+8);
2829     } else if (dir == Dir.Left) {
2830       status = HANGING;
2831       dir = Dir.Right;
2832       shiftX(-6);
2833       shiftX(1);
2834     } else {
2835       status = HANGING;
2836       dir = Dir.Left;
2837       shiftX(6);
2838     }
2839   } else if (isExitingSprite()) {
2840     scrPlayerExit();
2841     //!global.cleanSolids = true;
2842   }
2846 void activatePlayerWeapon () {
2847   if (dead) {
2848     if (holdItem isa PlayerWeapon) {
2849       auto wep = holdItem;
2850       holdItem = none;
2851       wep.instanceRemove();
2852       return;
2853     }
2854   }
2855   if (global.config.unarmed) return;
2857   if (holdItem isa PlayerWeapon) {
2858     if (!whipping) {
2859       /*
2860       writeln("000: !!!!!!!");
2861       if (holdItem) writeln("  H(", GetClassName(holdItem.Class), "): '", holdItem.objType, "'");
2862       if (pickedItem) writeln("  P(", GetClassName(pickedItem.Class), "): '", pickedItem.objType, "'");
2863       */
2864       auto wep = holdItem;
2865       holdItem = none;
2866       wep.instanceRemove();
2867       return;
2868     }
2869   }
2871   if (holdItem) return;
2873   auto spr = getSprite();
2874   if (spr.Name != 'sAttackLeft' && spr.Name != 'sDamselAttackL' && spr.Name != 'sTunnelAttackL') return;
2876   if (imageFrame > 4) {
2877     //bool hitEnemy = (PlayerWeapon(holdItem) ? PlayerWeapon(holdItem).hitEnemy : false);
2878     if (global.isTunnelMan || pickedItem isa ItemWeaponMattock) {
2879       holdItem = level.MakeMapObject(ix+(dir == Dir.Left ? -16 : 16), iy, 'oMattockHit');
2880       if (imageFrame < 7) playSound('sndWhip');
2881     } else if (pickedItem isa ItemWeaponMachete) {
2882       holdItem = level.MakeMapObject(ix+(dir == Dir.Left ? -16 : 16), iy, 'oSlash');
2883       playSound('sndWhip');
2884     } else {
2885       holdItem = level.MakeMapObject(ix+(dir == Dir.Left ? -16 : 16), iy, 'oWhip');
2886       playSound('sndWhip');
2887     }
2888     /+ not needed anymore
2889     if (holdItem) {
2890       holdItem.active = true;
2891       //if (PlayerWeapon(holdItem)) PlayerWeapon(holdItem).hitEnemy = hitEnemy;
2892     }
2893     +/
2894   } else if (imageFrame < 2) {
2895     if (global.isTunnelMan || pickedItem isa ItemWeaponMattock) {
2896       holdItem = level.MakeMapObject(ix+(dir == Dir.Left ? -16 : 16), iy, 'oMattockPre');
2897     } else if (pickedItem isa ItemWeaponMachete) {
2898       holdItem = level.MakeMapObject(ix+(dir == Dir.Left ? -16 : 16), iy, 'oMachetePre');
2899     } else {
2900       holdItem = level.MakeMapObject(ix+(dir == Dir.Left ? 16 : -16), iy, 'oWhipPre');
2901     }
2902     /+ not needed anymore
2903     if (holdItem) holdItem.active = true;
2904     +/
2905   }
2907   /*
2908   if (holdItem) {
2909     if (holdItem.type == "Machete") {
2910       obj = instance_create(x-16, y, oSlash);
2911       obj.sprite_index = sSlashLeft;
2912       playSound(global.sndWhip);
2913     } else if (holdItem.type == "Mattock") {
2914       obj = instance_create(x-16, y, oMattockHit);
2915       obj.sprite_index = sMattockHitL;
2916       if (image_index &lt; 7) playSound(global.sndWhip);
2917     }
2918   } else {
2919     if (global.isTunnelMan) {
2920       obj = instance_create(x-16, y, oMattockHit);
2921       obj.sprite_index = sMattockHitL;
2922       if (image_index &lt; 7) playSound(global.sndWhip);
2923     } else {
2924       obj = instance_create(x-16, y, oWhip);
2925       if (global.scumWhipUpgrade == 1) obj.sprite_index = sWhipLongLeft; else obj.sprite_index = sWhipLeft;
2926       playSound(global.sndWhip);
2927     }
2928   }
2929   */
2933 //bool webHit = false;
2935 bool doBreakWebsCB (MapObject o) {
2936   if (o isa ItemWeb) {
2937     writeln("IN WEB!");
2938     /*if (!webHit)*/ {
2939       if (fabs(xVel) > 1) {
2940         xVel = xVel*0.2;
2941         if (!o.dying) ItemWeb(o).life -= 5;
2942       } else {
2943         xVel = 0;
2944       }
2945       if (fabs(yVel) > 1) {
2946         yVel = yVel*0.2;
2947         if (!o.dying) ItemWeb(o).life -= 5;
2948       } else {
2949         yVel = 0;
2950       }
2951     }
2952   }
2953   return false;
2957 void initiateExitSequence () {
2958   writeln("exit sequence initiated...");
2959        if (global.isDamsel) setSprite('sDamselExit');
2960   else if (global.isTunnelMan) setSprite('sTunnelExit');
2961   else setSprite('sPExit');
2963   imageSpeed = 0.5;
2964   active = false;
2965   invincible = 999;
2966   depth = 999;
2968   /*k8: the following is done in `GameLevel`
2969   if (global.thiefLevel > 0) global.thiefLevel -= 1;
2970   //orig dbg:if (global.currLevel == 1) global.currLevel += firstLevelSkip; else global.currLevel += levelSkip;
2971   global.currLevel += 1;
2972   */
2973   playSound('sndSteps');
2977 void processLevelExit () {
2978   if (dead || stunned || whipping || level.playerExited) return;
2979   if (!platformCharacterIs(ON_GROUND)) return;
2980   if (isExitingSprite()) return; // just in case
2982   auto hld = holdItem;
2983   if (hld isa PlayerWeapon) return; // oops
2985   //if (!kExitPressed && !hld) return false;
2987   auto door = level.checkTileAtPoint(ix, iy, &level.cbCollisionExitTile);
2988   if (!door || !door.visible) return; // note that `invisible` doors still works
2990   // sell idol, or free damsel
2991   if (hld isa ItemGoldIdol) {
2992     //!if (isRealLevel()) global.idolsConverted += 1;
2993     //not thisglobal.money += hld.value*(global.levelType+1);
2994     ItemGoldIdol(hld).registerConverted();
2995     addScore(hld.value*(global.levelType+1));
2996     //!if (hld.sprite_index == sCrystalSkull) global.skulls += 1; else global.idols += 1;
2997     playSound('sndCoin');
2998     level.MakeMapObject(ix, iy-8, 'oBigCollect');
2999     holdItem = none;
3000     hld.instanceRemove();
3001     //!with (hld) instance_destroy();
3002     //!hld = 0;
3003     //!pickupItemType = "";
3004   } else if (hld isa MonsterDamsel) {
3005     holdItem = none;
3006     MonsterDamsel(hld).exitAtDoor(door);
3007   }
3009   if (!kExitPressed) {
3010     if (!door.invisible) {
3011       string msg = door.getExitMessage();
3012       if (msg.length == 0) {
3013         level.osdMessage(va("PRESS %s TO ENTER.", (global.config.useDoorWithButton ? "$PAY" : "$UP")), -666);
3014       } else if (msg[$-1] != '\n') {
3015         level.osdMessage(va("%s\nPRESS %s TO ENTER.", msg, (global.config.useDoorWithButton ? "$PAY" : "$UP")), -666);
3016       } else {
3017         level.osdMessage(msg, -666);
3018       }
3019     }
3020     return;
3021   }
3023   // exiting
3024   holdArrow = 0;
3025   bowArmed = false;
3027   // drop armed bomb
3028   if (isHoldingArmedBomb()) scrUseThrowItem();
3030   if (isHoldingBombOrRope()) scrSwitchToPocketItem(forceIfEmpty:true);
3032   wasHoldingBall = false;
3033   hld = holdItem;
3034   if (hld) {
3035     if (hld isa ItemGoldIdol) {
3036       //!if (isRealLevel()) global.idolsConverted += 1;
3037       //not thisglobal.money += hld.value*(global.levelType+1);
3038       ItemGoldIdol(hld).registerConverted();
3039       addScore(hld.value*(global.levelType+1));
3040       //!if (hld.sprite_index == sCrystalSkull) global.skulls += 1; else global.idols += 1;
3041       playSound('sndCoin');
3042       level.MakeMapObject(ix, iy-8, 'oBigCollect');
3043       holdItem = none;
3044       hld.instanceRemove();
3045       //!with (hld) instance_destroy();
3046       //!hld = 0;
3047       //!pickupItemType = "";
3048     } else if (hld isa MonsterDamsel) {
3049       holdItem = none;
3050       MonsterDamsel(hld).exitAtDoor(door);
3051     } else if (hld.heavy || hld isa MapEnemy) {
3052       // drop heavy items, characters and enemies (but not ball)
3053       if (hld !isa ItemBall) scrUseThrowItem();
3054     } else if (hld isa ItemBall) {
3055     } else {
3056       // other items are carried thru
3057       if (hld.cannotBeCarriedOnNextLevel) {
3058         scrUseThrowItem();
3059         holdItem = none; // just in case
3060       } else {
3061         scrHideItemToPocket();
3062       }
3063       /*
3064       global.pickupItem = hld.type;
3065       if (isAshShotgun(hld)) global.pickupItem = "Boomstick";
3066       with (hld) {
3067         breakPieces = false;
3068         instance_destroy();
3069       }
3070       */
3071       //scrHideItemToPocket();
3072     }
3073   }
3075   knockOffMonkeys();
3077   //door = instance_place(x, y, oExit); // done above
3078   door.snapToExit(self);
3080   initiateExitSequence();
3082   level.playerExitDoor = door;
3086 override bool onFellInWater (MapTile water) {
3087   level.MakeMapObject(ix, iy-8, 'oSplash');
3088   swimming = true;
3089   playSound('sndSplash');
3090   myGrav = 0.2; //k8:???
3091   return false;
3095 override bool onOutOfWater () {
3096   swimming = false;
3097   myGrav = 0.6;
3098   return false;
3102 // ////////////////////////////////////////////////////////////////////////// //
3103 override void thinkFrame () {
3104   // remove whip, etc. when dead
3105   if (dead && holdItem isa PlayerWeapon) {
3106     auto pw = holdItem;
3107     holdItem = none;
3108     pw.instanceRemove();
3109     scrSwitchToPocketItem(forceIfEmpty:false);
3110   }
3112   setPowerupState('Cape', global.hasCape);
3114   foreach (PlayerPowerup pp; powerups) if (pp.active) pp.onPreThink();
3116   // kapala
3117   if (redColor > 0) {
3118          if (redToggle) redColor -= 5;
3119     else if (redColor < 20) redColor += 5;
3120     else redToggle = true;
3121   } else {
3122     redColor = 0;
3123   }
3125   if (dead) justdied = false;
3127   if (!dead) {
3128     if (invincible > 0) --invincible;
3129   } else {
3130     invincible = 0;
3131   }
3133   if (blink > 0) {
3134     blinkHidden = !blinkHidden;
3135     --blink;
3136   } else {
3137     blinkHidden = false;
3138   }
3140   auto spr = getSprite();
3141   int x = ix, y = iy;
3143   cameraBlockX = max(0, cameraBlockX-1);
3144   cameraBlockY = max(0, cameraBlockY-1);
3146   // WHOA
3147   if (spr.Name == 'sWhoaLeft' || spr.Name == 'sDamselWhoaL' || spr.Name == 'sTunnelWhoaL') {
3148     if (whoaTimer > 0) {
3149       whoaTimer -= 1;
3150     } else if (holdItem && onLoosingHeldItem(LostCause.Whoa)) {
3151       auto hi = holdItem;
3152       holdItem = none;
3153       if (!hi.onLostAsHeldItem(self, LostCause.Whoa)) {
3154         // oops, regain it
3155         holdItem = hi;
3156       } else {
3157         scrSwitchToPocketItem(forceIfEmpty:true);
3158       }
3159     }
3160   } else {
3161     whoaTimer = whoaTimerMax;
3162   }
3164   // firing
3165   if (firing > 0) firing -= 1;
3167   // water
3168   auto wtile = level.isWaterAtPoint(x, y/*, oWaterSwim, -1, -1*/);
3169   if (wtile) {
3170     if (!swimming) {
3171       if (onFellInWater(wtile) || !isInstanceAlive) return;
3172     }
3173   } else {
3174     if (swimming) {
3175       if (onOutOfWater() || !isInstanceAlive) return;
3176     }
3177   }
3179   // burning
3180   if (burning > 0) {
3181     if (global.randOther(1, 5) == 1) level.MakeMapObject(x-8+global.randOther(4, 12), y-8+global.randOther(4, 12), 'oBurn');
3182     burning -= 1;
3183   }
3185   // lava
3186   if (!dead && level.isLavaAtPoint(x, y+6/*, oLava, 0, 0*/)) {
3187     //!if (isRealLevel()) global.miscDeaths[11] += 1;
3188     level.addDeath('lava');
3189     playSound('sndFlame');
3190     global.plife -= 99;
3191     dead = true;
3192     xVel = 0;
3193     yVel = 0.1;
3194     grav = 0;
3195     myGrav = 0;
3196     bounced = true;
3197     burning = 100;
3198     depth = 999;
3199   }
3202   // jetpack
3203   if (global.hasJetpack && platformCharacterIs(ON_GROUND)) {
3204     jetpackFuel = 50;
3205   }
3207   // fall off bottom of screen
3208   if (!level.inWinCutscene && !level.inIntroCutscene) {
3209     if (!dead && y > level.tilesHeight*16+16) {
3210       //!if (isRealLevel()) global.miscDeaths[10] += 1;
3211       level.addDeath('void');
3212       global.plife = -90; // spill blood
3213       xVel = 0;
3214       yVel = 0;
3215       grav = 0;
3216       myGrav = 0;
3217       bounced = true;
3218       scrDropItem(LostCause.Falloff);
3219       playSound('sndThud'); //???
3220       playSound('sndDie'); //???
3221     }
3223     if (dead && y > level.tilesHeight*16+16) {
3224       xVel = 0;
3225       yVel = 0;
3226       grav = 0;
3227       myGrav = 0;
3228     }
3229   }
3231   if (/*active*/true) {
3232     if (spr.Name == 'sStunL' || spr.Name == 'sDamselStunL' || spr.Name == 'sTunnelStunL') {
3233       if (stunTimer > 0) {
3234         imageSpeed = 0.4;
3235         stunTimer -= 1;
3236       }
3237       if (stunTimer < 1) {
3238         stunned = false;
3239         canDropStuff = true;
3240       }
3241     }
3243     if (!level.inWinCutscene) {
3244       if (isParachuteActive() || isCapeActiveAndOpen()) fallTimer = 0;
3245     }
3247     // changed to yVel > 1 from yVel > 0
3248     if (yVel > 1 && status != CLIMBING) {
3249       fallTimer += 1;
3250       if (fallTimer > 16) wallHurt = 0; // no sense in them taking extra damage from being thrown here
3251       int paraOpenHeight = (global.config.scumSpringShoesReduceFallDamage && (global.hasSpringShoes || global.hasJordans) ? 22 : 14);
3252       //paraOpenHeight = 4;
3253       if (global.hasParachute && !stunned && fallTimer > paraOpenHeight) {
3254         //if (not collision_point(x, y+32, oSolid, 0, 0)) // was commented in the original code
3255         //!*if (not collision_line(x, y+16, x, y+32, oSolid, 0, 0))
3256         if (!level.checkTilesInRect(x, y+16, 1, 17, &level.cbCollisionAnySolid)) {
3257           // drop parachute
3258           //!instance_create(x-8, y-16, oParachute);
3259           fallTimer = 0;
3260           global.hasParachute = false;
3261           activatePowerup('Parachute');
3262           //writeln("parachute state: ", isParachuteActive());
3263         }
3264       }
3265     } else if (fallTimer > 16 && platformCharacterIs(ON_GROUND) &&
3266                !level.checkTilesInRect(x-8, y-8, 17, 17, &level.cbCollisionSpringTrap) /* not onto springtrap */)
3267     {
3268       // long drop -- player has just landed
3269       bool reducedDamage = (global.config.scumSpringShoesReduceFallDamage && (global.hasSpringShoes || global.hasJordans));
3270       if (reducedDamage && fallTimer <= 24) {
3271         // land without taking damage
3272         fallTimer = 0;
3273       } else {
3274         stunned = true;
3275              if (fallTimer > (reducedDamage ? 72 : 48)) global.plife -= 10*global.config.scumFallDamage;
3276         else if (fallTimer > (reducedDamage ? 48 : 32)) global.plife -= 2*global.config.scumFallDamage;
3277         else global.plife -= 1*global.config.scumFallDamage;
3278         if (global.plife < 1) {
3279           if (!dead) level.addDeath('fall');
3280           spillBlood();
3281         }
3282         bounced = true;
3283         if (global.config.scumFallDamage > 0) stunTimer += 60;
3284         yVel = -3;
3285         fallTimer = 0;
3286         auto obj = level.MakeMapObject(x-4, y+6, 'oPoof');
3287         if (obj) obj.xVel = -0.4;
3288         obj = level.MakeMapObject(x+4, y+6, 'oPoof');
3289         if (obj) obj.xVel = 0.4;
3290         playSound('sndThud');
3291       }
3292     } else if (yVel <= 0) {
3293       fallTimer = 0;
3294       if (isParachuteActive()) {
3295         deactivatePowerup('Parachute');
3296         level.MakeMapObject(ix-8, iy-16-8, 'oParaUsed');
3297       }
3298     }
3300     // if (stunned) fallTimer = 0; // was commented in the original code
3302     if (swimming && !level.isLavaAtPoint(x, y/*, oLava, 0, 0*/)) {
3303       fallTimer = 0;
3304       if (bubbleTimer > 0) {
3305         bubbleTimer -= 1;
3306       } else {
3307         if (level.isWaterAtPoint(x, (y&~0x0f)-8)) level.MakeMapObject(x, y-4, 'oBubble');
3308         bubbleTimer = bubbleTimerMax;
3309       }
3310     } else {
3311       bubbleTimer = bubbleTimerMax;
3312     }
3314     //TODO: k8: move spear checking to spear handler
3315     if (!isExitingSprite()) {
3316       auto spear = MapObjectSpearsBase(level.isObjectInRect(ix-6, iy-6, 13, 14, delegate bool (MapObject o) {
3317         auto tt = MapObjectSpearsBase(o);
3318         if (!tt) return false;
3319         return tt.isHitFrame;
3320       }));
3321       if (spear) {
3322         // stunned = true;
3323         // bounced  = false;
3324         global.plife -= global.config.spearDmg; // 4
3325         if (!dead && global.plife <= 0 /*and isRealLevel()*/) level.addDeath('spear');
3326         xVel = global.randOther(4, 6)*(spear.isLeft ? -1 : 1);
3327         yVel = -6;
3328         flty -= 1;
3329         y = iy;
3330         // state = FALLING;
3331         spillBlood(); //1?
3332       }
3333     }
3335     if (status != DUCKTOHANG && !stunned && !dead && !isExitingSprite()) {
3336       bounced = false;
3337       characterStepEvent();
3338     } else {
3339       if (status != DUCKING && status != DUCKTOHANG) status = STANDING;
3340       checkControlKeys(getSprite());
3341     }
3342   }
3344   // if (dead or stunned)
3345   if (dead || stunned) {
3346     if (holdItem) {
3347       if (holdItem isa ItemWeaponBow && bowArmed) scrFireBow();
3348       scrDropItem(dead ? LostCause.Dead : LostCause.Stunned, xVel, -3);
3349     }
3351     yVel += (bounced ? 1.0 : 0.6);
3353     if (isCollisionTop(1) && yVel < 0) yVel = -yVel*0.8;
3354     if (isCollisionLeft(1) || isCollisionRight(1)) xVel = -xVel*0.5;
3356     bool collisionbottomcheck = !!isCollisionBottom(1);
3357     if (collisionbottomcheck || isCollisionBottom(1, &level.cbCollisionPlatform)) {
3358       // bounce
3359       if (collisionbottomcheck) {
3360         if (yVel > 2.5) yVel = -yVel*0.5; else yVel = 0;
3361       } else {
3362         // after falling onto a platform don't take extra damage after recovering from stunning
3363         fallTimer -= 1;
3364       }
3365       /* was commented in the original code
3366       if (isCollisionBottom(1)) {
3367         if (yVel &gt; 2.5) yVel = -yVel*0.5; else yVel = 0;
3368       } else {
3369         fallTimer -= 1;
3370       }
3371       */
3373       // friction
3374            if (fabs(xVel) < 0.1) xVel = 0;
3375       else if (fabs(xVel) != 0 && level.isIceAtPoint(x, y+16)) xVel *= 0.8;
3376       else if (fabs(xVel) != 0) xVel *= 0.3;
3378       bounced = true;
3379     }
3381     //webHit = false;
3382     //level.forEachObjectInRect(ix, iy, width, height, &doBreakWebsCB);
3384     // apply the limits since the velocity may be too extreme
3385     xVelLimit = 10;
3386     xVel = fclamp(xVel, -xVelLimit, xVelLimit);
3387     yVel = fclamp(yVel, -yVelLimit, yVelLimit);
3389     moveRel(xVel, yVel);
3390     x = ix;
3391     y = iy;
3393     // fix sprites, spawn blood from spikes
3394     if (isParachuteActive()) {
3395       deactivatePowerup('Parachute');
3396       level.MakeMapObject(ix-8, iy-16-8, 'oParaUsed');
3397     }
3399     if (whipping) {
3400       whipping = false;
3401       //!with (oWhip) instance_destroy();
3402     }
3404     if (global.isDamsel) {
3405       if (xVel == 0) {
3406              if (dead) setSprite('sDamselDieL');
3407         else if (stunned) setSprite('sDamselStunL');
3408       } else if (bounced) {
3409         if (yVel < 0) setSprite('sDamselBounceL'); else setSprite('sDamselFallL');
3410       } else {
3411         if (xVel < 0) setSprite('sDamselDieLL'); else setSprite('sDamselDieLR');
3412       }
3413     } else if (global.isTunnelMan) {
3414       if (xVel == 0) {
3415              if (dead) setSprite('sTunnelDieL');
3416         else if (stunned) setSprite('sTunnelStunL');
3417       } else if (bounced) {
3418         if (yVel < 0) setSprite('sTunnelLBounce'); else setSprite('sTunnelFallL');
3419       } else {
3420         if (xVel < 0) setSprite('sTunnelDieLL'); else setSprite('sTunnelDieLR');
3421       }
3422     } else {
3423       if (xVel == 0) {
3424              if (dead) setSprite('sDieL');
3425         else if (stunned) setSprite('sStunL');
3426       } else if (bounced) {
3427         if (yVel < 0) setSprite('sDieLBounce'); else setSprite('sDieLFall');
3428       } else {
3429         if (xVel < 0) setSprite('sDieLL'); else setSprite('sDieLR');
3430       }
3431     }
3433     x = ix;
3434     y = iy;
3436     auto colobj = isCollisionRight(1);
3437     if (!colobj) colobj = isCollisionLeft(1);
3438     if (!colobj) colobj = isCollisionBottom(1);
3439     if (colobj) {
3440       if (wallHurt > 0) {
3441         scrCreateBlood(colobj.x0, colobj.y0, 3);
3442         global.plife -= 1;
3443         if (!dead && global.plife <= 0 /*&& isRealLevel()*/) {
3444           if (thrownBy) {
3445             writeln("thrown to death by '", thrownBy, "'");
3446             level.addDeath(thrownBy);
3447           }
3448         }
3449         wallHurt -= 1;
3450         if (wallHurt <= 0) thrownBy = '';
3451         playSound('sndHurt'); //???
3452       }
3453     }
3455     colobj = isCollisionBottom(1);
3456     if (colobj && !bounced) {
3457       bounced = true;
3458       scrCreateBlood(colobj.x0, colobj.y0, 2);
3459       if (wallHurt > 0) {
3460         global.plife -= 1;
3461         if (!dead && global.plife <= 0 /*and isRealLevel()*/) {
3462           if (thrownBy) {
3463             writeln("thrown to death by '", thrownBy, "'");
3464             level.addDeath(thrownBy);
3465           }
3466         }
3467         wallHurt -= 1;
3468         if (wallHurt <= 0) thrownBy = '';
3469       }
3470     }
3471   } else {
3472     // look up and down
3473     bool kPay = level.isKeyDown(GameConfig::Key.Pay);
3474     if (kPay) {
3475       // gnounc's quick look
3476       if (!kRight && !kLeft && (platformCharacterIs(ON_GROUND) || status == HANGING)) {
3477              if (kDown) { if (viewCount <= 6) viewCount += 3; else viewOffset += 6; }
3478         else if (kUp) { if (viewCount <= 6) viewCount += 3; else viewOffset -= 6; }
3479         else viewCount = 0;
3480       } else {
3481         viewCount = 0;
3482       }
3483     } else {
3484       // default look up/down with delay if pay button not held
3485       if (!kRight && !kLeft && (platformCharacterIs(ON_GROUND) || status == HANGING)) {
3486              if (kDown) { if (viewCount <= 30) viewCount += 1; else viewOffset += 4; }
3487         else if (kUp) { if (viewCount <= 30) viewCount += 1; else viewOffset -= 4; }
3488         else viewCount = 0;
3489       } else {
3490         viewCount = 0;
3491       }
3492     }
3493   }
3494   if (viewCount == 0 && viewOffset) viewOffset = (viewOffset < 0 ? min(0, viewOffset+8) : max(0, viewOffset-8));
3495   viewOffset = clamp(viewOffset, -16*6, 16*6);
3497   if (!dead) activatePlayerWeapon();
3499   if (!dead) processLevelExit();
3501   // hurt too much
3502   if (global.plife < -99 && visible && justdied) spillBlood();
3504   if (global.plife < 1) {
3505     dead = true;
3506   }
3508   // spikes, and other shit
3509   if (global.plife >= -99 && visible && !isExitingSprite()) {
3510     auto colSpikes = level.checkTilesInRect(x-4, y-4, 9, 13, &level.cbCollisionSpikes);
3512     if (colSpikes && dead) {
3513       grav = 0;
3514       if (!level.isSolidAtPoint(x, y+9)) { shiftY(0.02); y = iy; } //0.05;
3515       //else myGrav = 0.6;
3516     } else {
3517       myGrav = 0.6;
3518     }
3520     if (colSpikes && yVel > 0 && (fallTimer > 3 || stunned)) { // originally fallTimer &gt; 4
3521       if (!dead) {
3522         // spikes will always instant-kill in Moon room
3523         /*if (isRoom("rMoon")) global.plife -= 99; else*/ global.plife -= global.config.scumSpikeDamage;
3524         if (/*isRealLevel() &&*/ global.plife <= 0) level.addDeath('spike');
3525         if (global.plife > 0) playSound('sndHurt');
3526         spillBlood();
3527         xVel = 0;
3528         yVel = 0;
3529         myGrav = 0;
3530       }
3531       colSpikes.makeBloody();
3532     }
3533     //else if (not dead) myGrav = 0.6;
3534   }
3537   // sacrifice
3538   if (visible && (status >= STUNNED || stunned || dead || status == DUCKING)) {
3539     bool onAltar;
3540     checkAndPerformSacrifice(out onAltar);
3541     // block looking down if we're trying to sacrifire ourselves
3542     if (onAltar) viewCount = max(0, viewCount-1);
3543   } else {
3544     sacCount = default.sacCount;
3545   }
3547   // activate ankh
3548   if (dead && global.hasAnkh) {
3549     writeln("*** ACTIVATED ANKH");
3550     global.hasAnkh = false;
3551     dead = false;
3552     int newLife = (global.isTunnelMan ? global.config.scumTMLife : global.config.scumStartLife);
3553     global.plife = max(global.plife, newLife);
3554     level.osdMessage("THE ANKH SHATTERS!\nYOU HAVE BEEN REVIVED!", 4);
3555     // find moai
3556     auto moai = level.forEachTile(delegate bool (MapTile t) { return (t.objType == 'oMoai'); });
3557     if (moai) {
3558       level.forEachTile(delegate bool (MapTile t) {
3559         if (t.objType == 'oMoaiInside') {
3560           teleportTo(t.ix+8, t.iy+8);
3561           t.instanceRemove();
3562         }
3563         return false;
3564       });
3565       //teleportTo(moai.ix+16+8, moai.iy+16+8);
3566     } else {
3567       if (level.allEnters.length) {
3568         teleportTo(level.allEnters[0].ix+8, level.allEnters[0].iy-8);
3569       }
3570     }
3571     level.centerViewAtPlayer();
3572     auto ball = getMyBall();
3573     if (ball) ball.teleportToPrisoner();
3574     //k8:???depth = 50;
3575     xVel = 0;
3576     yVel = 0;
3577     blink = 60;
3578     invincible = 60;
3579     fallTimer = 0;
3580     visible = true;
3581     active = true;
3582     dead = false;
3583     stunned = false;
3584     status = STANDING;
3585     burning = 0;
3586     //alarm[8] = 60; // this starts music; but we don't need it, 'cause we won't stop the music on player death
3587     playSound('sndTeleport');
3588   }
3591   if (dead) level.stats.gameOver();
3593   // step end
3594   if (status == DUCKTOHANG) {
3595     spr = getSprite();
3596     if (spr.Name != 'sDuckToHangL' && spr.Name != 'sDamselDtHL' && spr.Name != 'sTunnelDtHL') status = STANDING;
3597   }
3599   foreach (PlayerPowerup pp; powerups) if (pp.active) pp.onPostThink();
3601   if (jetpackFlaresTime > 0) {
3602     if (--jetpackFlaresTime == 0) {
3603       auto obj = level.MakeMapObject(ix+global.randOther(0, 3)-global.randOther(0, 3), iy+global.randOther(0, 3)-global.randOther(0, 3), 'oFlareSpark');
3604       if (obj) {
3605         obj.yVel = global.randOther(1, 3);
3606         obj.xVel = global.randOther(0, 3)-global.randOther(0, 3);
3607       }
3608       playSound('sndJetpack');
3609     }
3610   }
3614 // ////////////////////////////////////////////////////////////////////////// //
3615 void drawPrePrePowerupWithOfs (int xpos, int ypos, int scale, float currFrameDelta) {
3616   // so ducking player will have it's cape correctly rendered
3617   foreach (PlayerPowerup pp; powerups) {
3618     if (pp.active) pp.prePreDrawWithOfs(xpos, ypos, scale, currFrameDelta);
3619   }
3623 override void drawWithOfs (int xpos, int ypos, int scale, float currFrameDelta) {
3624   //if (heldBy) return; // owner will take care of this
3625   if (blinkHidden) return;
3627   bool renderJetpackBack = false;
3628   if (global.hasJetpack) {
3629     // render jetpack
3630     if ((status == CLIMBING || isExitingSprite()) && !whipping) {
3631       // later
3632       renderJetpackBack = true;
3633     } else {
3634       int xi, yi;
3635       getInterpCoords(currFrameDelta, scale, out xi, out yi);
3636       yi -= 1;
3637       SpriteImage spr;
3638       if (dir == Dir.Right) {
3639         spr = level.sprStore['sJetpackRight'];
3640         xi -= 4;
3641       } else {
3642         spr = level.sprStore['sJetpackLeft'];
3643         xi += 4;
3644       }
3645       if (spr) {
3646         auto spf = spr.frames[0];
3647         if (spf && spf.width > 0 && spf.height > 0) spf.tex.blitAt(xi-xpos-spf.xofs*scale, yi-ypos-spf.yofs*scale, scale);
3648       }
3649     }
3650   }
3652   bool ducking = (status == DUCKING);
3653   foreach (PlayerPowerup pp; powerups) {
3654     if (pp.active) pp.preDrawWithOfs(xpos, ypos, scale, currFrameDelta);
3655   }
3657   auto oldColor = Video.color;
3658   if (redColor > 0) Video.color = clamp(200+redColor, 0, 255)<<16;
3659   ::drawWithOfs(xpos, ypos, scale, currFrameDelta);
3660   Video.color = oldColor;
3662   if (renderJetpackBack) {
3663     int xi, yi;
3664     getInterpCoords(currFrameDelta, scale, out xi, out yi);
3665     SpriteImage spr = level.sprStore['sJetpackBack'];
3666     if (spr) {
3667       auto spf = spr.frames[0];
3668       if (spf && spf.width > 0 && spf.height > 0) spf.tex.blitAt(xi-xpos-spf.xofs*scale, yi-ypos-spf.yofs*scale, scale);
3669     }
3670   }
3672   foreach (PlayerPowerup pp; powerups) if (pp.active) pp.postDrawWithOfs(xpos, ypos, scale, currFrameDelta);
3676 void lastDrawWithOfs (int xpos, int ypos, int scale, float currFrameDelta) {
3677   foreach (PlayerPowerup pp; powerups) {
3678     if (pp.active) pp.lastDrawWithOfs(xpos, ypos, scale, currFrameDelta);
3679   }
3683 defaultproperties {
3684   objName = 'Player';
3685   objType = 'oPlayer';
3687   desc = "Spelunker";
3688   desc2 = "A strange little man who spends his time exploring caverns. He wants to be just like Indiana Jones when he grows up.";
3690   negateMirrorXOfs = true;
3692   status = FALLING; // the character state, must be one of the following: STANDING, RUNNING, DUCKING, LOOKING_UP, CLIMBING, JUMPING, or FALLING
3694   bloodless = false;
3696   stunned = false;
3697   bounced = false;
3699   fallTimer = 0;
3700   stunTimer = 0;
3701   wallHurt = 0;
3702   //thrownBy = ""; // "Yeti", "Hawkman", or "Shopkeeper" for stat tracking deaths by being thrown
3703   pushTimer = 0;
3704   whoaTimer = 0;
3705   //whoaTimerMax = 30;
3706   distToNearestLightSource = 999;
3708   sacCount = 60;
3710   flying = false;
3711   myGrav = 0.6;
3712   myGravNorm = 0.6;
3713   myGravWater = 0.2;
3714   yVelLimit = 10;
3715   bounceFactor = 0.5;
3716   frictionFactor = 0.3;
3718   xVelLimit = 16; // limits the xVel: default 15
3719   yVelLimit = 10; // limits the yVel
3720   xAccLimit = 9;  // limits the xAcc
3721   yAccLimit = 6;  // limits the yAcc
3722   runAcc = 3;     // the running acceleration
3724   grav = 1;
3726   bloodLeft = 999999;
3728   depth = 5;
3729   //lightRadius = 96; //???