damsel fixes (dead/thrown states; stun from machete); slightly less blood for objects
[k8vacspelynky.git] / PlayerPawn.vc
blob2e7c0c1675966ba6b2f1682eee4e2c919910f418
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()*/) level.addDeath('explosion');
408     burning = 50;
409     if (global.config.scumExplosionStun) {
410       stunned = true;
411       stunTimer = 100;
412     }
413     spillBlood();
414   }
415   if (xplo.ix < ix) xVel = global.randOther(4, 6); else xVel = -global.randOther(4, 6);
416   yVel = -6;
417   return true;
421 // ////////////////////////////////////////////////////////////////////////// //
422 // start new game when exiting from title, and process other custom exits
423 void scrPlayerExit () {
424   level.playerExited = true;
425   status = STANDING;
426   characterSprite();
430 // ////////////////////////////////////////////////////////////////////////// //
431 bool scrHideItemToPocket (optional bool forBombOrRope) {
432   if (!holdItem) return true;
433   if (holdItem isa PlayerWeapon) return false;
434   if (holdItem.forSale) return false;
435   if (!forBombOrRope) {
436     if (holdItem isa ItemBall) return false;
437   }
439   // cannot hide armed bomb
440   ItemBomb bomb = ItemBomb(holdItem);
441   if (bomb && bomb.armed) return false;
442   if (bomb || holdItem isa ItemRopeThrow) {
443     holdItem.instanceRemove();
444     holdItem = none;
445     return true;
446   }
448   // cannot hide enemy
449   if (holdItem isa MapEnemy) return false;
450   //writeln("hiding: '", GetClassName(holdItem.Class), "'");
452   if (pickedItem) FatalError("we are already holding '%n'", GetClassName(pickedItem.Class));
453   pickedItem = holdItem;
454   holdItem = none;
455   pickedItem.active = false;
456   pickedItem.visible = false;
457   if (pickedItem.heldBy) FatalError("oooops (scrHideItemToPocket)");
458   return true;
462 bool scrSwitchToBombs () {
463   if (holdItem isa PlayerWeapon) return false;
465   if (global.bombs < 1) return false;
466   if (ItemBomb(holdItem)) return true;
467   if (!scrHideItemToPocket(forBombOrRope:true)) return false;
469   ItemBomb bomb = ItemBomb(level.MakeMapObject(ix, iy, 'oBomb'));
470   if (!bomb) return false;
471   bomb.setSticky(global.stickyBombsActive);
472   holdItem = bomb;
473   whoaTimer = whoaTimerMax;
474   return true;
478 bool scrSwitchToStickyBombs () {
479   if (holdItem isa PlayerWeapon) return false;
480   if (!global.hasStickyBombs) {
481     global.stickyBombsActive = false;
482     return false;
483   }
485   global.stickyBombsActive = !global.stickyBombsActive;
486   return true;
490 bool scrSwitchToRopes () {
491   if (holdItem isa PlayerWeapon) return false;
493   if (global.rope < 1) return false;
494   if (ItemRopeThrow(holdItem)) return true;
495   if (!scrHideItemToPocket(forBombOrRope:true)) return false;
497   ItemRopeThrow rope = ItemRopeThrow(level.MakeMapObject(ix, iy, 'oRopeThrow'));
498   if (!rope) return false;
499   holdItem = rope;
500   whoaTimer = whoaTimerMax;
501   return true;
505 bool isHoldingBombOrRope () {
506   auto hit = holdItem;
507   if (!hit) return false;
508   return (hit isa ItemBomb || hit isa ItemRopeThrow);
512 bool isHoldingBomb () {
513   auto hit = holdItem;
514   if (!hit) return false;
515   return (hit isa ItemBomb);
519 bool isHoldingArmedBomb () {
520   auto hit = ItemBomb(holdItem);
521   if (!hit) return false;
522   return hit.armed;
526 bool isHoldingRope () {
527   auto hit = holdItem;
528   if (!hit) return false;
529   return (hit isa ItemRopeThrow);
533 bool scrSwitchToPocketItem (bool forceIfEmpty) {
534   if (holdItem isa PlayerWeapon) return false;
535   if (holdItem && holdItem.forSale) return false;
537   if (holdItem == pickedItem) { pickedItem = none; whoaTimer = whoaTimerMax; return true; }
539   if (!forceIfEmpty && !pickedItem) return false;
541   // destroy currently holded item if it is a bomb or a rope
542   if (holdItem) {
543     // you cannot do it with an armed bomb
544     if (holdItem isa MapEnemy) return false; // cannot hide an enemy
545     ItemBomb bomb = ItemBomb(holdItem);
546     if (bomb && bomb.armed) return false;
547     if (bomb || holdItem isa ItemRopeThrow) {
548       //delete holdItem;
549       holdItem.instanceRemove();
550       holdItem = none;
551     } /*else {
552       if (pickedItem) {
553         writeln(va("cannot switch to pocket item while carrying '%n' ('%n' is in pocket, why?)", GetClassName(holdItem.Class), GetClassName(pickedItem.Class)));
554         return false;
555       }
556     }*/
557   }
559   auto oldHold = holdItem;
560   holdItem = pickedItem;
561   pickedItem = oldHold;
562   // all flag management is done in property handler
563   if (oldHold) {
564     oldHold.active = false;
565     oldHold.visible = false;
566   }
567   whoaTimer = whoaTimerMax;
568   return true;
572 bool scrSwitchToNextItem () {
573   if (holdItem isa PlayerWeapon) return false;
574   if (holdItem && holdItem.forSale) return false;
576   // holding a bomb?
577   if (ItemBomb(holdItem)) {
578     if (ItemBomb(holdItem).armed) return false; // cannot switch out of armed bomb
579     if (scrSwitchToRopes()) return true;
580     return scrSwitchToPocketItem(forceIfEmpty:true);
581   }
583   // holding a rope?
584   if (ItemRopeThrow(holdItem)) {
585     if (scrSwitchToPocketItem(forceIfEmpty:true)) return true;
586     if (scrSwitchToBombs()) return true;
587     return scrHideItemToPocket();
588   }
590   // either nothing, or normal item
591   bool tryPocket = !!holdItem;
592   if (scrSwitchToBombs()) return true;
593   if (scrSwitchToRopes()) return true;
594   if (holdItem isa ItemBall) return false;
595   if (tryPocket) return scrSwitchToPocketItem(forceIfEmpty:true);
596   return false;
600 // ////////////////////////////////////////////////////////////////////////// //
601 bool scrPickupItem (MapObject obj) {
602   if (holdItem isa PlayerWeapon) return false;
604   if (!obj) return false;
606   if (holdItem) {
607     if (pickedItem) return false;
608     if (isHoldingArmedBomb()) return false;
609     if (isHoldingBombOrRope()) {
610       if (!scrSwitchToPocketItem(forceIfEmpty:true)) return false;
611     }
612     if (holdItem) return false;
613   } else {
614     // just in case
615     if (pickedItem) return false;
616   }
618        if (obj isa ItemBomb && !ItemBomb(obj).armed) ++global.bombs;
619   else if (obj isa ItemRopeThrow) ++global.rope;
620   holdItem = obj;
621   whoaTimer = whoaTimerMax;
622   obj.onPickedUp(self);
623   return true;
627 // drop currently held item
628 bool scrDropItem (LostCause cause, optional float xVel, optional float yVel) {
629   if (holdItem isa PlayerWeapon) return false;
631   if (!holdItem) return false;
633   if (!onLoosingHeldItem(cause)) return false;
635   auto hi = holdItem;
636   holdItem = none;
638   if (!hi.onLostAsHeldItem(self, cause, xVel!optional, yVel!optional)) {
639     // oops, regain it
640     holdItem = hi;
641     return false;
642   }
644        if (hi isa ItemRopeThrow) global.rope = max(0, global.rope-1);
645   else if (hi isa ItemBomb && !ItemBomb(hi).armed) global.bombs = max(0, global.bombs-1);
647   madeOffer = false;
649   scrSwitchToPocketItem(forceIfEmpty:true);
650   return true;
654 // ////////////////////////////////////////////////////////////////////////// //
655 void scrUseThrowIt (MapObject it) {
656   if (!it) return;
658   it.onBeforeThrowBy(self);
660   it.resaleValue = 0;
661   it.makeSafe();
663   if (dir == Dir.Left) {
664     it.xVel = (it.heavy ? -4+xVel : -8+xVel);
665     //foreach (; 0..8) if (level.isSolidAtPoint(ix-8, iy)) it.shiftX(1);
666     //while (!level.isSolidAtPoint(ix-8, iy)) it.shiftX(1); // prevent getting stuck in wall
667   } else if (dir == Dir.Right) {
668     it.xVel = (it.heavy ? 4+xVel : 8+xVel);
669     //foreach (; 0..8) if (level.isSolidAtPoint(ix+8, iy)) it.shiftX(-1);
670     //while (!level.isSolidAtPoint(ix+8, iy)) it.shiftX(-1); // prevent getting stuck in wall
671   }
672   it.yVel = (it.heavy ? (kUp ? -4 : -2) : (kUp ? -9 : -3));
673   if (kDown || scrPlayerIsDucking()) {
674     if (platformCharacterIs(ON_GROUND)) {
675       it.shiftY(-2);
676       it.xVel *= 0.6;
677       it.yVel = 0.5;
678     } else {
679       it.yVel = 3;
680     }
681   } else if (!global.hasMitt) {
682     if (dir == Dir.Left) {
683       if (level.isSolidAtPoint(ix-8, iy-10)) {
684         it.yVel = 0;
685         it.xVel -= 1;
686       }
687     } else if (dir == Dir.Right) {
688       if (level.isSolidAtPoint(ix+8, iy-10)) {
689         it.yVel = 0;
690         it.xVel += 1;
691       }
692     }
693   }
695   if (global.hasMitt && !scrPlayerIsDucking()) {
696     it.xVel += (it.xVel < 0 ? -6 : 6);
697          if (!kUp && !kDown) it.yVel = -0.4;
698     else if (kDown) it.yVel = 6;
699     it.myGrav = 0.1;
700   }
702   // prevent getting stuck in a wall
703   if (it.isCollision()) {
704     //foreach (; 0..8) if (level.isSolidAtPoint(ix-8, iy)) it.shiftX(1);
705     if (it.xVel < 0) {
706       if (level.isSolidAtPoint(it.ix-8, it.iy)) it.shiftX(8);
707     } else if (it.xVel > 0) {
708       if (level.isSolidAtPoint(it.ix+8, it.iy)) it.shiftX(-8);
709     } else if (it.isCollision()) {
710       int dx = (it.isCollisionLeft(0) ? 1 : it.isCollisionRight(0) ? -1 : 0);
711       if (dx) {
712         foreach (; 0..8) {
713           it.shiftX(dx);
714           if (!it.isCollision()) break;
715         }
716       }
717     }
718     /*
719     int dx = 1;
720     while (dx > 8 && it.isCollisionLeft(dx)) ++dx;
721     if (dx < 8) it.shiftX(8);
722     else {
723       dx = 1;
724       while (dx > 8 && it.isCollisionRight(dx)) ++dx;
725       if (dx < 8) it.shiftX(-8);
726     }
727     */
728   }
730   /*
731   if (it.sprite_index == sBombBag ||
732       it.sprite_index == sBombBox ||
733       it.sprite_index == sRopePile)
734   {
735       // do nothing
736   } else*/ {
737     playSound('sndThrow');
738   }
740   auto proj = ItemProjectile(it);
741   if (proj) proj.launchedByPlayer = true;
745 bool scrUseThrowItem () {
746   if (holdItem isa PlayerWeapon) return false;
748   auto hitem = holdItem;
750   if (!hitem) return false;
751   if (!onLoosingHeldItem(LostCause.Unknown)) return false;
753   holdItem = none;
754   madeOffer = false;
756   scrUseThrowIt(hitem);
758   // if we throwing away armed bomb, get previous item back into our hands
759   //FIXME
760   if (/*ItemBomb(hitem)*/isHoldingBombOrRope()) scrSwitchToPocketItem(forceIfEmpty:false);
762   return true;
766 // ////////////////////////////////////////////////////////////////////////// //
767 bool scrPlayerIsDucking () {
768   if (dead) return false;
769   auto spr = getSprite();
770   //if (!spr) return false;
771   return
772     spr.Name == 'sDuckLeft' ||
773     spr.Name == 'sCrawlLeft' ||
774     spr.Name == 'sDamselDuckL' ||
775     spr.Name == 'sDamselCrawlL' ||
776     spr.Name == 'sTunnelDuckL' ||
777     spr.Name == 'sTunnelCrawlL';
781 bool scrFireBow () {
782   if (holdItem !isa ItemWeaponBow) return false;
783   sndStopSound('sndBowPull');
784   if (!bowArmed) return false;
785   if (!holdItem.onTryUseItem(self)) return false;
786   return true;
790 void scrUsePutItOnGroundHelper (MapObject it, optional float xVelMult, optional float yVelNew) {
791   if (!it) return;
793   if (!specified_xVelMult) xVelMult = 0.4;
794   if (!specified_yVelNew) yVelNew = 0.5;
796   //writeln("putting '", GetClassName(hi.Class), "'");
798   if (dir == Dir.Left) {
799     it.xVel = (it.heavy ? -4 : -8);
800   } else if (dir == Dir.Right) {
801     it.xVel = (it.heavy ? 4 : 8);
802   }
803   it.xVel += xVel;
804   it.xVel *= xVelMult;
805   it.yVel = yVelNew;
807   //hi.fltx = ix;
808   it.flty = iy+2;
809   if (ItemGoldIdol(it)) it.flty = iy;
811   foreach (; 0..16) {
812     if (it.isCollisionBottom(0) && !it.isCollisionTop(1)) {
813       it.flty -= 1;
814     } else {
815       break;
816     }
817   }
819   foreach (; 0..16) {
820     if (it.isCollisionLeft(0)) {
821       if (it.isCollisionRight(1)) break;
822       it.fltx += 1;
823     } else if (it.isCollisionRight(0)) {
824       if (it.isCollisionLeft(1)) break;
825       it.fltx -= 1;
826     } else {
827       break;
828     }
829   }
833 // put item which player holds in his hands on the ground if player is ducking
834 // return `true` if item was put
835 bool scrUsePutItemOnGround (optional float xVelMult, optional float yVelNew) {
836   if (holdItem isa PlayerWeapon) return false;
838   auto hi = holdItem;
839   if (!hi || !scrPlayerIsDucking()) return false;
841   if (!onLoosingHeldItem(LostCause.Unknown)) return false;
843   //writeln("putting '", GetClassName(hi.Class), "'");
845   if (global.bombs > 0) {
846     auto bomb = ItemBomb(hi);
847     if (bomb && !bomb.armed) global.bombs -= 1;
848   }
850   if (global.rope > 0) {
851     auto rope = ItemRopeThrow(hi);
852     if (rope) {
853       global.rope -= 1;
854       rope.falling = false;
855       rope.flying = false;
856     }
857   }
859   holdItem = none;
860   hi.resaleValue = 0;
861   madeOffer = false;
862   hi.makeSafe();
864   scrUsePutItOnGroundHelper(hi, xVelMult!optional, yVelNew!optional);
866   return true;
870 bool launchRope (bool goDown, bool doDrop) {
871   if (global.rope < 1) {
872     global.rope = 0;
873     if (ItemRopeThrow(holdItem)) scrSwitchToPocketItem(forceIfEmpty:false);
874     return false;
875   }
877   --global.rope;
879   bool wasHeld = false;
880   ItemRopeThrow rp = ItemRopeThrow(holdItem);
881   int xdelta = (doDrop ? 12 : 16)*(dir == Dir.Left ? -1 : 1);
882   if (rp) {
883     //FIXME: call handler
884     wasHeld = true;
885     holdItem = none;
886     rp.setXY(ix+xdelta, iy);
887   } else {
888     rp = ItemRopeThrow(level.MakeMapObject(ix+xdelta, iy, 'oRopeThrow'));
889   }
890   if (rp.heldBy) FatalError("PlayerPawn::launchRope: hold management fucked");
891   rp.armed = true;
892   rp.flying = false;
893   //rp.resaleValue = 0;
895   rp.px = ix;
896   rp.py = iy;
897   if (platformCharacterIs(ON_GROUND)) rp.startY = iy; // YASM 1.7
899   if (!goDown) {
900     // launch rope up
901     rp.setX(fltx);
902     rp.xVel = 0;
903     rp.yVel = -12;
904   } else {
905     // launch rope down
906     bool t = true;
907     rp.moveSnap(16, 1);
908     if (ix < rp.ix) {
909       if (!level.isSolidAtPoint(ix+(doDrop ? 2 : 8), iy)) { //2
910              if (!level.checkTilesInRect(rp.ix-8, rp.iy, 2, 17)) rp.shiftX(-8);
911         else if (!level.checkTilesInRect(rp.ix+7, rp.iy, 2, 17)) rp.shiftX(8);
912         else t = false;
913       } else {
914         t = false;
915       }
916     } else if (!level.isSolidAtPoint(ix-(doDrop ? 2 : 8), iy)) { //2
917            if (!level.checkTilesInRect(rp.ix+7, rp.iy, 2, 17)) rp.shiftX(8);
918       else if (!level.checkTilesInRect(rp.ix-8, rp.iy, 2, 17)) rp.shiftX(-8);
919       else t = false;
920     } else {
921       t = false;
922     }
923     //writeln("t=", t);
924     if (!t) {
925       // cannot launch rope
926       /* was commented in the original
927       if (oPlayer1.facing == 18) {
928         obj = instance_create(oPlayer1.x-4, oPlayer1.y+2, oRopeThrow);
929         obj.xVel = -3.2;
930       } else {
931         obj = instance_create(oPlayer1.x+4, oPlayer1.y+2, oRopeThrow);
932         obj.xVel = 3.2;
933       }
934       obj.yVel = 0.5;
935       */
936       //writeln("!!! goDown=", goDown, "; doDrop=", doDrop, "; wasHeld=", wasHeld);
937       rp.armed = false;
938       rp.flying = false;
939       if (!wasHeld) doDrop = true;
940       if (doDrop) {
941         /*
942         rp.setXY(ix, iy);
943         if (dir == Dir.Left) rp.xVel = -3.2; else rp.xVel = 3.2;
944         rp.yVel = 0.5;
945         */
946         rp.forceFixHoldCoords(self);
947         if (goDown) {
948           scrUsePutItOnGroundHelper(rp);
949         } else {
950           scrUseThrowIt(rp);
951         }
952         if (wasHeld) scrSwitchToPocketItem(forceIfEmpty:false);
953       } else {
954         //writeln("NO DROP!");
955         ++global.rope;
956         if (wasHeld) {
957           // regain it
958           //rp.resaleValue = 1; //k8:???
959           holdItem = rp;
960         } else {
961           rp.instanceRemove();
962           if (wasHeld) scrSwitchToPocketItem(forceIfEmpty:false);
963         }
964       }
965       return false;
966     } else {
967       level.MakeMapObject(rp.ix, rp.iy, 'oRopeTop');
968       rp.armed = false;
969       rp.falling = true;
970       rp.xVel = 0;
971       rp.yVel = 0;
972     }
973   }
974   if (wasHeld) scrSwitchToPocketItem(forceIfEmpty:false);
975   playSound('sndThrow');
976   return true;
980 bool scrLaunchBomb () {
981   if (whipping || global.bombs < 1) return false;
982   --global.bombs;
984   ItemBomb bomb = ItemBomb(level.MakeMapObject(ix, iy, 'oBomb'));
985   if (!bomb) return false;
986   bomb.forceFixHoldCoords(self);
987   bomb.setSticky(global.stickyBombsActive);
988   bomb.armIt(80);
989   bomb.resaleValue = 0;
991   if (kDown || scrPlayerIsDucking()) {
992     scrUsePutItOnGroundHelper(bomb);
993   } else {
994     scrUseThrowIt(bomb);
995   }
997   return true;
1001 bool scrUseItem () {
1002   auto it = holdItem;
1003   if (!it) return false;
1004   //writeln(GetClassName(holdItem.Class));
1006   //auto spr = holdItem.getSprite();
1007   /+
1008   } else if (holdItem.type == "Sceptre") {
1009     if (kDown) scrUsePutItemOnGround(0.4, 0.5);
1010     if (firing == 0 && !scrPlayerIsDucking()) {
1011       if (facing == LEFT) {
1012         asleft = true;
1013         xsgn = -1;
1014       } else {
1015         asleft = false;
1016         xsgn = 1;
1017       }
1018       xofs = 12*xsgn;
1019       repeat(3) {
1020         obj = instance_create(x+xofs, y+4, oPsychicCreateP);
1021         obj.xVel = xsgn*rand(1, 3);
1022         obj.yVel = -random(2);
1023       }
1024       obj = instance_create(x+xofs, y-2, oPsychicWaveP);
1025       obj.xVel = xsgn*6;
1026       playSound(global.sndPsychic);
1027       firing = firingPistolMax;
1028     }
1029   } else if (holdItem.type == "Teleporter II") {
1030     scrUseTeleporter2();
1031   } else if (holdItem.type == "Bow") {
1032     if (kDown) {
1033       scrUsePutItemOnGround(0.4, 0.5);
1034     } else if (firing == 0 && !scrPlayerIsDucking() && !bowArmed && global.arrows > 0) {
1035       bowArmed = true;
1036       playSound(global.sndBowPull);
1037     } else if (global.arrows <= 0) {
1038       global.message = "I'M OUT OF ARROWS!";
1039       global.message2 = "";
1040       global.messageTimer = 80;
1041     }
1042   } else {
1043   +/
1046   if (whipping) return false;
1048   if (kDown) {
1049     if (scrPlayerIsDucking()) scrUsePutItemOnGround();
1050     return true;
1051   }
1053   // you cannot throw away shop items, but can throw dices
1054   if (it.forSale && it !isa ItemDice) {
1055     if (!level.isInShop(ix/16, iy/16)) {
1056       it.forSale = false;
1057     } else {
1058       // allow throw/use shop items
1059       //return false;
1060     }
1061   }
1063   if (!it.onTryUseItem(self)) {
1064     // throw item
1065     scrUseThrowItem();
1066   }
1068   return true;
1072 // ////////////////////////////////////////////////////////////////////////// //
1073 // called by characterStepEvent
1074 // help player jump up through one block wide gaps by nudging them to one side so they don't hit their head
1075 void scrJumpHelper () {
1076   int d = 4; // max distance to nudge player
1077   int x = ix, y = iy;
1078   if (!level.checkTilesInRect(x, y-12, 1, 7)) {
1079     if (level.checkTilesInRect(x-5, y-12, 1, 7) &&
1080         level.checkTilesInRect(x+14, y-12, 1, 7))
1081     {
1082       while (d > 0 && level.checkTilesInRect(x-5, y-12, 1, 7)) { ++x; shiftX(1); --d; }
1083     } else if (level.checkTilesInRect(x+5, y-12, 1, 7) &&
1084                level.checkTilesInRect(x-14, y-12, 1, 7))
1085     {
1086       while (d > 0 && level.checkTilesInRect(x+5, y-12, 1, 7)) { --x; shiftX(-1); --d; }
1087     }
1088   }
1089   /+
1090   if (!collision_line(x, y-6, x, y-12, oSolid, 0, 0)) {
1091     if (collision_line(x-5, y-6, x-5, y-12, oSolid, 0, 0) &&
1092         collision_line(x+14, y-6, x+14, y-12, oSolid, 0, 0))
1093     {
1094       while (collision_line(x-5, y-6, x-5, y-12, oSolid, 0, 0) && d > 0) {
1095         x += 1;
1096         d -= 1;
1097       }
1098     }
1099     else if (collision_line(x+5, y-6, x+5, y-12, oSolid, 0, 0) and
1100              collision_line(x-14, y-6, x-14, y-12, oSolid, 0, 0))
1101     {
1102       while (collision_line(x+5, y-6, x+5, y-12, oSolid, 0, 0) && d > 0) {
1103         x -= 1;
1104         d -= 1;
1105       }
1106     }
1107   }
1108   +/
1112 // ////////////////////////////////////////////////////////////////////////// //
1114  * Returns whether a GENERAL trait about a character is true.
1115  * Only the platform character should run this script.
1117  * `tp` can be one of the following:
1118  *   ON_GROUND
1119  *   IN_AIR
1120  *   ON_LADDER
1121  */
1122 final bool platformCharacterIs (int tp) {
1123   if (tp == ON_GROUND && (status == RUNNING || status == STANDING || status == DUCKING || status == LOOKING_UP)) return true;
1124   if (tp == IN_AIR && (status == JUMPING || status == FALLING)) return true;
1125   if (tp == ON_LADDER && status == CLIMBING) return true;
1126   return false;
1130 // ////////////////////////////////////////////////////////////////////////// //
1131 // sets the sprite of the character depending on his/her status
1132 final void characterSprite () {
1133   if (status == STOPPED) {
1134          if (global.isDamsel) setSprite('sDamselLeft');
1135     else if (global.isTunnelMan) setSprite('sTunnelLeft');
1136     else setSprite('sStandLeft');
1137     return;
1138   }
1140   int x = ix, y = iy;
1141   if (global.isTunnelMan && !stunned && !whipping) {
1142     // Tunnel Man
1143     if (status == STANDING) {
1144       if (!level.isSolidAtPoint(x-2, y+9)) {
1145         imageSpeed = 0.6;
1146         setSprite('sTunnelWhoaL');
1147       } else {
1148         setSprite('sTunnelLeft');
1149       }
1150     }
1151     if (status == RUNNING) {
1152       if (kUp) setSprite('sTunnelLookRunL'); else setSprite('sTunnelRunL');
1153     }
1154     if (status == DUCKING) {
1155            if (xVel == 0) setSprite('sTunnelDuckL');
1156       else if (fabs(xVel) < 3) setSprite('sTunnelCrawlL');
1157       else setSprite('sTunnelRunL');
1158     }
1159     if (status == LOOKING_UP) {
1160       if (fabs(xVel) > 0) setSprite('sTunnelRunL'); else setSprite('sTunnelLookL');
1161     }
1162     if (status == JUMPING) setSprite('sTunnelJumpL');
1163     if (status == FALLING && statePrev == FALLING && statePrevPrev == FALLING) setSprite('sTunnelFallL');
1164     if (status == HANGING) setSprite('sTunnelHangL');
1165     if (pushTimer > 20) setSprite('sTunnelPushL');
1166     if (status == DUCKTOHANG) setSprite('sTunnelDtHL');
1167     if (status == CLIMBING) {
1168       if (level.isRopeAtPoint(x, y)) {
1169         if (kDown) setSprite('sTunnelClimb3'); else setSprite('sTunnelClimb2');
1170       } else {
1171         setSprite('sTunnelClimb');
1172       }
1173     }
1174   } else if (global.isDamsel && !stunned && !whipping) {
1175     // Damsel
1176     if (status == STANDING) {
1177       if (!level.isSolidAtPoint(x-2, y+9)) {
1178         imageSpeed = 0.6;
1179         setSprite('sDamselWhoaL');
1180         /* was commented out in the original
1181         if (holdItem && whoaTimer < 1) {
1182           holdItem.held = false;
1183           if (facing == LEFT) holdItem.xVel = -2; else holdItem.xVel = 2;
1184           if (holdItem.type == "Damsel") playSound('sndDamsel');
1185           if (holdItem.type == pickupItemType) { holdItem = 0; pickupItemType = ""; } else scrSwitchToPocketItem();
1186         }
1187         */
1188       } else {
1189         setSprite('sDamselLeft');
1190       }
1191     }
1192     if (status == RUNNING) {
1193       if (kUp) setSprite('sDamselRunL'); else setSprite('sDamselRunL');
1194     }
1195     if (status == DUCKING) {
1196            if (xVel == 0) setSprite('sDamselDuckL');
1197       else if (fabs(xVel) < 3) setSprite('sDamselCrawlL');
1198       else setSprite('sDamselRunL');
1199     }
1200     if (status == LOOKING_UP) {
1201       if (fabs(xVel) > 0) setSprite('sDamselRunL'); else setSprite('sDamselLookL');
1202     }
1203     if (status == JUMPING) setSprite('sDamselDieLR');
1204     if (status == FALLING && statePrev == FALLING && statePrevPrev == FALLING) setSprite('sDamselFallL');
1205     if (status == HANGING) setSprite('sDamselHangL');
1206     if (pushTimer > 20) setSprite('sDamselPushL');
1207     if (status == DUCKTOHANG) setSprite('sDamselDtHL');
1208     if (status == CLIMBING) {
1209       if (level.isRopeAtPoint(x, y)) {
1210         if (kDown) setSprite('sDamselClimb3'); else setSprite('sDamselClimb2');
1211       } else {
1212         setSprite('sDamselClimb');
1213       }
1214     }
1215   } else if (!stunned && !whipping) {
1216     // Spelunker
1217     if (status == STANDING) {
1218       if (!level.checkTileAtPoint(x-(dir == Dir.Left ? 2 : 0), y+9, &level.cbCollisionForWhoa)) {
1219         imageSpeed = 0.6;
1220         setSprite('sWhoaLeft');
1221         /* was commented out in the original
1222         if (holdItem && whoaTimer < 1) {
1223           holdItem.held = false;
1224           if (facing == LEFT) holdItem.xVel = -2; else holdItem.xVel = 2;
1225           if (holdItem.type == "Damsel") playSound('sndDamsel');
1226           if (holdItem.type == pickupItemType) { holdItem = 0; pickupItemType = ""; } else scrSwitchToPocketItem();
1227         }
1228         */
1229       } else {
1230         setSprite('sStandLeft');
1231       }
1232     }
1233     if (status == RUNNING) {
1234       if (kUp) setSprite('sLookRunL'); else setSprite('sRunLeft');
1235     }
1236     if (status == DUCKING) {
1237            if (xVel == 0) setSprite('sDuckLeft');
1238       else if (fabs(xVel) < 3) setSprite('sCrawlLeft');
1239       else setSprite('sRunLeft');
1240     }
1241     if (status == LOOKING_UP) {
1242       if (fabs(xVel) > 0) setSprite('sLookRunL'); else setSprite('sLookLeft');
1243     }
1244     if (status == JUMPING) setSprite('sJumpLeft');
1245     if (status == FALLING && statePrev == FALLING && statePrevPrev == FALLING) setSprite('sFallLeft');
1246     if (status == HANGING) setSprite('sHangLeft');
1247     if (pushTimer > 20) setSprite('sPushLeft');
1248     if (status == CLIMBING) {
1249       if (level.isRopeAtPoint(x, y)) {
1250         if (kDown) setSprite('sClimbUp3'); else setSprite('sClimbUp2');
1251       } else {
1252         setSprite('sClimbUp');
1253       }
1254     }
1255     if (status == DUCKTOHANG) setSprite('sDuckToHangL');
1256   }
1261 // ////////////////////////////////////////////////////////////////////////// //
1262 void addScore (int delta) {
1263   if (!level.isNormalLevel()) return;
1264   //score += delta;
1265   if (delta == 0) return;
1266   level.stats.addMoney(delta);
1267   if (delta > 0) {
1268     level.xmoney += delta;
1269     level.collectCounter = min(100, level.collectCounter+20);
1270   }
1274 // ////////////////////////////////////////////////////////////////////////// //
1275 // for dead players too
1276 // first, the code will call `onObjectTouched()` for player
1277 // if it returned `false`, the code will call `obj.onTouchedByPlayer()`
1278 // note that player's handler is called *after* its frame thinker,
1279 // but object handler is called *before* frame thinker for the object
1280 // i.e. return `true` to block calling `obj.onTouchedByPlayer()`,
1281 // (but NOT object thinker)
1282 bool onObjectTouched (MapObject obj) {
1283   // is player dead?
1284   if (dead || global.plife <= 0) return false; // player may be rendered dead, but not yet transited to dead state
1286   if (obj isa ItemProjectileArrow && holdItem isa ItemWeaponBow && !stunned && global.arrows < 99) {
1287     if (fabs(obj.xVel) < 1 && fabs(obj.yVel) < 1 && !obj.stuck) {
1288       ++global.arrows;
1289       playSound('sndPickup');
1290       obj.instanceRemove();
1291       return true;
1292     }
1293   }
1295   // collect treasure
1296   auto treasure = ItemTreasure(obj);
1297   if (treasure && treasure.canCollect) {
1298     if (treasure.value) addScore(treasure.value);
1299     treasure.onCollected(self); // various other effects
1300     playSound(treasure.soundName);
1301     treasure.instanceRemove();
1302     return true;
1303   }
1305   // collect blood
1306   if (global.hasKapala && obj isa MapObjBlood) {
1307     global.bloodLevel += 1;
1308     level.MakeMapObject(obj.ix, obj.iy, 'oBloodSpark');
1309     obj.instanceRemove();
1311     if (global.bloodLevel > 8) {
1312       global.bloodLevel = 0;
1313       global.plife += 1;
1314       level.MakeMapObject(ix, iy-8, 'oHeart');
1315       playSound('sndKiss');
1316     }
1318     if (redColor < 55) redColor += 5;
1319     redToggle = false;
1320   }
1322   // other objects will take care of themselves
1323   return false;
1327 // return `false` to prevent
1328 // holdItem is valid
1329 bool onLoosingHeldItem (LostCause cause) {
1330   if (level.inWinCutscene != 0) return false;
1331   return true;
1335 // ////////////////////////////////////////////////////////////////////////// //
1336 // k8: don't even ask me! the following mess is almost straightforward port of the original Derek's code!
1337 private final void closeCape () {
1338   auto pp = PPCape(findPowerup('Cape'));
1339   if (pp) pp.open = false;
1343 private final void switchCape () {
1344   auto pp = PPCape(findPowerup('Cape'));
1345   if (pp) pp.open = !pp.open;
1349 final bool isCapeActiveAndOpen () {
1350   auto pp = PPCape(findPowerup('Cape'));
1351   return (pp && pp.active && pp.open);
1355 final bool isParachuteActive () {
1356   auto pp = findPowerup('Parachute');
1357   return (pp && pp.active);
1361 // ////////////////////////////////////////////////////////////////////////// //
1362 // for cutscenes
1363 bool checkSkipCutScene () {
1364   if (skipCutscenePressed) {
1365     return level.isKeyReleased(GameConfig::Key.Pay);
1366   } else {
1367     skipCutscenePressed = level.isKeyPressed(GameConfig::Key.Pay);
1368     return false;
1369   }
1372 int transKissTimer;
1375 bool forcePlayerControls () {
1376   if (level.inWinCutscene) {
1377     unpressAllKeys();
1378     level.winCutscenePlayerControl(self);
1379     return true;
1380   } else if (level.inIntroCutscene) {
1381     unpressAllKeys();
1382     level.introCutscenePlayerControl(self);
1383     //return false;
1384     return true;
1385   } else if (level.levelKind == GameLevel::LevelKind.Transition) {
1386     unpressAllKeys();
1388     if (checkSkipCutScene()) {
1389       level.playerExited = true;
1390       return true;
1391     }
1393     auto door = level.checkTileAtPoint(ix, iy, &level.cbCollisionExitTile);
1394     if (door) {
1395       kExitPressed = true;
1396       return true;
1397     }
1399     if (status == STOPPED) {
1400       if (--transKissTimer > 0) return true;
1401       status = STANDING;
1402     }
1404     transKissTimer = 0;
1405     auto dms = MonsterDamselKiss(level.isObjectAtPoint(ix+8, iy+4, delegate bool (MapObject o) { return (o isa MonsterDamselKiss); }));
1406     if (dms && !dms.kissed) {
1407       status = STOPPED;
1408       xVel = 0;
1409       yVel = 0;
1410       dms.kiss();
1411       transKissTimer = 30;
1412       return true;
1413     }
1415     kRight = true;
1416     kRightPressed = true;
1417     return true;
1418   }
1419   return false;
1423 // ////////////////////////////////////////////////////////////////////////// //
1424 private final void checkControlKeys (SpriteImage spr) {
1425   if (forcePlayerControls()) {
1426     if (movementBlocked) unpressAllKeys();
1427     if (kLeft) kLeftPushedSteps += 1; else kLeftPushedSteps = 0;
1428     if (kRight) kRightPushedSteps += 1; else kRightPushedSteps = 0;
1429     return;
1430   }
1432   kLeft = level.isKeyDown(GameConfig::Key.Left);
1433   if (movementBlocked) kLeft = false;
1434   if (kLeft) kLeftPushedSteps += 1; else kLeftPushedSteps = 0;
1435   kLeftPressed = level.isKeyPressed(GameConfig::Key.Left);
1436   kLeftReleased = level.isKeyReleased(GameConfig::Key.Left);
1438   kRight = level.isKeyDown(GameConfig::Key.Right);
1439   if (movementBlocked) kRight = false;
1440   if (kRight) kRightPushedSteps += 1; else kRightPushedSteps = 0;
1441   kRightPressed = level.isKeyPressed(GameConfig::Key.Right);
1442   kRightReleased = level.isKeyReleased(GameConfig::Key.Right);
1444   kUp = level.isKeyDown(GameConfig::Key.Up);
1445   kDown = level.isKeyDown(GameConfig::Key.Down);
1447   kJump = level.isKeyDown(GameConfig::Key.Jump);
1448   kJumpPressed = level.isKeyPressed(GameConfig::Key.Jump);
1449   kJumpReleased = level.isKeyReleased(GameConfig::Key.Jump);
1451   if (movementBlocked) unpressAllKeys();
1453   if (cantJump > 0) {
1454     kJump = false;
1455     kJumpPressed = false;
1456     kJumpReleased = false;
1457     --cantJump;
1458   } else if (spr && global.isTunnelMan && spr.Name == 'sTunnelAttackL' && !holdItem) {
1459     kJump = false;
1460     kJumpPressed = false;
1461     kJumpReleased = false;
1462     cantJump = max(0, cantJump-1);
1463   }
1465   kAttack = level.isKeyDown(GameConfig::Key.Attack);
1466   kAttackPressed = level.isKeyPressed(GameConfig::Key.Attack);
1467   kAttackReleased = level.isKeyReleased(GameConfig::Key.Attack);
1469   kItemPressed = level.isKeyPressed(GameConfig::Key.Switch);
1470   kRopePressed = level.isKeyPressed(GameConfig::Key.Rope);
1471   kBombPressed = level.isKeyPressed(GameConfig::Key.Bomb);
1473   kPayPressed = level.isKeyPressed(GameConfig::Key.Pay);
1475   if (movementBlocked) unpressAllKeys();
1477   kExitPressed = false;
1478   if (global.config.useDoorWithButton) {
1479     if (kPayPressed) kExitPressed = true;
1480   } else {
1481     if (kUp) kExitPressed = true;
1482   }
1484   if (stunned || dead) {
1485     unpressAllKeys();
1486     //level.clearKeysPressRelease();
1487   }
1491 // ////////////////////////////////////////////////////////////////////////// //
1492 // knock off monkeys that grabbed you
1493 void knockOffMonkeys () {
1494   level.forEachObject(delegate bool (MapObject o) {
1495     auto mk = EnemyMonkey(o);
1496     if (mk && !mk.dead && mk.status == GRAB) {
1497       mk.xVel = global.randOther(0, 1)-global.randOther(0, 1);
1498       mk.yVel = -4;
1499       mk.status = BOUNCE;
1500       mk.vineCounter = 20;
1501       mk.grabCounter = 60;
1502     }
1503     return false;
1504   });
1508 // ////////////////////////////////////////////////////////////////////////// //
1509 // fix collision with boulder (bug with non-aligned boulder)
1510 void hackBoulderCollision () {
1511   auto bld = level.checkTilesInRect(x0, y0, width, height, delegate bool (MapTile o) { return (o isa ObjBoulder); });
1512   if (bld && fabs(bld.xVel) <= 1) {
1513     writeln("IN BOULDER!");
1514     if (x0 < bld.x0) {
1515       int dx = bld.x0-x0;
1516       writeln("  LEFT: dx=", dx);
1517       if (dx <= 2) fltx = x0-dx;
1518     } else if (x1 > bld.x1) {
1519       int dx = x1-bld.x1;
1520       writeln("  RIGHT: dx=", dx);
1521       if (dx <= 2) fltx = x1-dx;
1522     }
1523   }
1527 // ////////////////////////////////////////////////////////////////////////// //
1528 bool checkHangTileDG (MapTile t) { return (t.solid || t.tree); }
1531 void checkPerformHang (bool colLeft, bool colRight) {
1532   if (status == HANGING || platformCharacterIs(ON_GROUND)) return;
1533   if ((kLeft && kRight) || (!kLeft && !kRight)) return;
1534   if (kLeft && !colLeft) {
1535 #ifdef HANG_DEBUG
1536     writeln("checkPerformHang: no left solid");
1537 #endif
1538     return;
1539   }
1540   if (kRight && !colRight) {
1541 #ifdef HANG_DEBUG
1542     writeln("checkPerformHang: no right solid");
1543 #endif
1544     return;
1545   }
1546   if (hangCount != 0) {
1547 #ifdef HANG_DEBUG
1548     writeln("checkPerformHang: hangCount=", hangCount);
1549 #endif
1550     return;
1551   }
1552   if (iy <= 16) return;
1553   int dx = (kLeft ? -9 : 9);
1554 #ifdef HANG_DEBUG
1555   writeln("checkPerformHang: trying to hang at ", dx);
1556 #endif
1558   bool doHang = false;
1560   if (global.hasGloves) {
1561     doHang = (yVel > 0 && !!level.checkTilesInRect(ix+dx, iy-6, 1, 2, &checkHangTileDG));
1562   } else {
1563     // hang on tree?
1564     doHang = !!level.checkTilesInRect(ix+dx, iy-6, 1, 2, &level.cbCollisionAnyTree);
1565 #ifdef HANG_DEBUG
1566     writeln("  tree: ", doHang);
1567 #endif
1568     // hang on solid?
1569     if (!doHang) {
1570       doHang = level.checkTilesInRect(ix+dx, iy-6, 1, 2) &&
1571                !level.isSolidAtPoint(ix+dx, iy-9) && !level.isSolidAtPoint(ix, iy+9);
1572 #ifdef HANG_DEBUG
1573       writeln("  solid: ", doHang);
1574 #endif
1575     }
1576     if (!doHang) {
1577 #ifdef HANG_DEBUG
1578       writeln("    solid at dx, -6(1): ", !!level.checkTilesInRect(ix+dx, iy-6, 1, 2));
1579       writeln("    solid at dx, -9(0): ", !!level.isSolidAtPoint(ix+dx, iy-9));
1580       writeln("    solid at  0, +9(0): ", !!level.isSolidAtPoint(ix, iy-9));
1581 #endif
1582 #ifdef EASIER_HANG
1583       doHang = level.checkTilesInRect(ix+dx, iy-6, 1, 2) &&
1584                !level.isSolidAtPoint(ix+dx, iy-10) && !level.isSolidAtPoint(ix, iy+9);
1585 #ifdef HANG_DEBUG
1586       if (!doHang) writeln("    easier hang failed");
1587 #endif
1588       /*
1589       if (!level.isSolidAtPoint(ix, iy-9)) {
1590         foreach (int dy; 6..24) {
1591           writeln("    solid at dx:-", dy, "(0): ", !!level.isSolidAtPoint(ix+dx, iy-dy));
1592         }
1593         writeln("   ix=", ix, "; iy=", iy);
1594       }
1595       */
1596 #endif
1597     }
1598   }
1600   if (doHang) {
1601     status = HANGING;
1602     moveSnap(1, 8);
1603     yVel = 0;
1604     yAcc = 0;
1605     grav = 0;
1606   }
1610 // ////////////////////////////////////////////////////////////////////////// //
1611 final void characterStepEvent () {
1612   if (climbSoundTimer > 0) {
1613     if (--climbSoundTimer == 0) {
1614       playSound(climbSndToggle ? 'sndClimb2' : 'sndClimb1');
1615       climbSndToggle = !climbSndToggle;
1616     }
1617   }
1619   auto spr = getSprite();
1620   checkControlKeys(spr);
1622   float xPrev = fltx, yPrev = flty;
1623   int x = ix, y = iy;
1625   // check collisions in various directions
1626   bool colSolidLeft = !!getPushableLeft(1);
1627   bool colSolidRight = !!getPushableRight(1);
1628   bool colLeft = !!isCollisionLeft(1);
1629   bool colRight = !!isCollisionRight(1);
1630   bool colTop = !!isCollisionTop(1);
1631   bool colBot = !!isCollisionBottom(1);
1632   bool colLadder = !!isCollisionLadder();
1633   bool colPlatBot = !!isCollisionBottom(1, &level.cbCollisionPlatform);
1634   bool colPlat = !!isCollision(&level.cbCollisionPlatform);
1635   //bool colWaterTop = !!isCollisionTop(1, &level.cbCollisionWater);
1636   bool colWaterTop = !!level.checkTilesInRect(x0, y0-1, width, 3, &level.cbCollisionWater);
1637   bool colIceBot = !!level.isIceAtPoint(x, y+8);
1639   bool runKey = false;
1640   if (level.isKeyDown(GameConfig::Key.Run)) { runHeld = 100; runKey = true; }
1641   if (level.isKeyDown(GameConfig::Key.Attack) && !whipping) { runHeld += 1; runKey = true; }
1642   if (!runKey || (!kLeft && !kRight)) runHeld = 0;
1644   // allows the character to run left and right
1645   // if state!=DUCKING and state!=LOOKING_UP and state!=CLIMBING
1646   if (status != CLIMBING && status != HANGING) {
1647     if (kLeftReleased && fabs(xVel) < 0.0001) xAcc -= 0.5;
1648     if (kRightReleased && fabs(xVel) < 0.0001) xAcc += 0.5;
1649     if (kLeft && !kRight) {
1650       if (colSolidLeft) {
1651         //xVel = 3; // in orig
1652         if (platformCharacterIs(ON_GROUND) && status != DUCKING) {
1653           xAcc -= 1;
1654           pushTimer += 10;
1655           //playSound('sndPush', unique:true);
1656         }
1657       } else if (kLeftPushedSteps > 2 && (dir == Dir.Left || fabs(xVel) < 0.0001)) {
1658         xAcc -= runAcc;
1659       }
1660       dir = Dir.Left;
1661       //if (platformCharacterIs(ON_GROUND) && fabs(xVel) > 0 && alarm[3] < 1) alarm[3] = floor(16/-xVel);
1662     }
1663     if (kRight && !kLeft) {
1664       if (colSolidRight) {
1665         //xVel = 3; // in orig
1666         if (platformCharacterIs(ON_GROUND) && status != DUCKING) {
1667           xAcc += 1;
1668           pushTimer += 10;
1669           //playSound('sndPush', unique:true);
1670         }
1671       } else if ((kRightPushedSteps > 2 || colSolidLeft) && (dir == Dir.Right || fabs(xVel) < 0.0001)) {
1672         xAcc += runAcc;
1673       }
1674       dir = Dir.Right;
1675       //if (platformCharacterIs(ON_GROUND) && fabs(xVel) > 0 && alarm[3] < 1) alarm[3] = floor(16/xVel);
1676     }
1677   }
1679   // ladders
1680   if (status == CLIMBING) {
1681     closeCape();
1682     kJumped = false;
1683     ladderTimer = 10;
1684     auto ladder = level.isLadderAtPoint(x, y);
1685     if (ladder) { x = ladder.ix+8; setX(x); }
1686     if (kLeft) dir = Dir.Left; else if (kRight) dir = Dir.Right;
1687     if (kUp) {
1688       // checks both ladder and laddertop
1689       if (level.isAnyLadderAtPoint(x, y-8)) {
1690         //writeln("LADDER00! old yAcc=", yAcc, "; climbAcc=", climbAcc, "; new yAcc=", yAcc-climbAcc);
1691         yAcc -= climbAcc;
1692         if (climbSoundTimer < 1) climbSoundTimer = climbSndSpeed;
1693         //!if (alarm[2] < 1) alarm[2] = climbSndSpeed;
1694       } else {
1695         /*
1696         for (int dy = -6; dy > -12; --dy) {
1697           ladder = level.isAnyLadderAtPoint(x, y+dy);
1698           if (ladder) {
1699             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));
1700           }
1701         }
1702         */
1703         /*
1704         auto grid = level.miscTileGrid;
1705         foreach (MapTile t; grid.inCellPix(48, 96, grid.nextTag(), precise:false)) {
1706           writeln("at 48, 96: ", GetClassName(t.Class), "; pos=(", t.ix, ",", t.iy, ")");
1707         }
1708         foreach (MapTile t; grid.inCellPix(48, 94, grid.nextTag(), precise:false)) {
1709           writeln("at 48, 94: ", GetClassName(t.Class), "; pos=(", t.ix, ",", t.iy, ")");
1710         }
1711         foreach (int dy; 90..102) {
1712           ladder = level.isAnyLadderAtPoint(48, dy);
1713           if (ladder) {
1714             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));
1715           }
1716         }
1717         */
1718       }
1719     } else if (kDown) {
1720       // checks both ladder and laddertop
1721       if (level.isAnyLadderAtPoint(x, y+8)) {
1722         yAcc += climbAcc;
1723         //!if (alarm[2] < 1) alarm[2] = climbSndSpeed;
1724         if (climbSoundTimer < 1) climbSoundTimer = climbSndSpeed;
1725       } else {
1726         status = FALLING;
1727       }
1728       if (colBot) status = STANDING;
1729     }
1730     // jump from ladder
1731     if (kJumpPressed && !whipping) {
1732       if (kLeft) xVel = -departLadderXVel; else if (kRight) xVel = departLadderXVel; else xVel = 0;
1733       //yAcc += departLadderYVel;
1734       //k8: was `0.6`, but with `0.4` we can jump onto the wall above, and with `0.6` we cannot
1735       yAcc = 0.4+departLadderYVel; // YASM 1.8.1 Fix for extra air when jumping off ladders due to increased climb speed option
1736       status = JUMPING;
1737       jumpButtonReleased = false;
1738       jumpTime = 0;
1739       ladderTimer = 5;
1740     }
1741   } else {
1742     if (ladderTimer > 0) ladderTimer -= 1;
1743   }
1745   if (platformCharacterIs(IN_AIR) && status != HANGING) yAcc += gravityIntensity;
1747   // player has landed
1748   if ((colBot || colPlatBot) && platformCharacterIs(IN_AIR) && yVel >= 0) {
1749     if (!colPlat || colBot) {
1750       yVel = 0;
1751       yAcc = 0;
1752       status = RUNNING;
1753     }
1754     playSound('sndLand');
1755   }
1756   if ((colBot || colPlatBot) && !colPlat) yVel = 0;
1758   // player has just walked off of the edge of a solid
1759   if (colBot == 0 && (!colPlatBot || colPlat) && platformCharacterIs(ON_GROUND)) {
1760     status = FALLING;
1761     yAcc += grav;
1762     kJumped = true;
1763     if (global.hasGloves) hangCount = 5;
1764   }
1766   if (colTop) {
1767          if (dead || stunned) yVel = -yVel*0.8;
1768     else if (status == JUMPING) yVel = fabs(yVel*0.3);
1769   }
1771   if ((colLeft && dir == Dir.Left) || (colRight && dir == Dir.Right)) {
1772     if (dead || stunned) xVel = -xVel*0.5; else xVel = 0;
1773   }
1775   // jumping
1776   if (kJumpReleased && platformCharacterIs(IN_AIR)) {
1777     kJumped = true;
1778   } else if (platformCharacterIs(ON_GROUND)) {
1779     closeCape();
1780     kJumped = false;
1781   }
1783   MapObject oWeb = none, oBlob = none;
1784   if (kJumpPressed) {
1785     oWeb = level.isObjectAtPoint(x, y, &level.cbIsObjectWeb);
1786     if (!oWeb) oBlob = level.isObjectAtPoint(x, y, &level.cbIsObjectBlob);
1787   }
1789   bool invokeJumpHelper = false;
1791   if (kJumpPressed && oWeb) {
1792     ItemWeb(oWeb).tear(1);
1793     yAcc += initialJumpAcc*2;
1794     yVel -= 3;
1795     xAcc += xVel/2;
1797     status = JUMPING;
1798     jumpButtonReleased = false;
1799     jumpTime = 0;
1801     grav = gravNorm;
1802     invokeJumpHelper = true;
1803   } else if (kJumpPressed && oBlob) {
1804     oBlob.hp -= 5;
1805     scrCreateBloblets(oBlob.x0+8, oBlob.y0+8, 1);
1806     playSound('sndHit');
1807     yAcc += initialJumpAcc*2;
1808     yVel -= 2;
1809     xAcc += xVel/2;
1810     status = JUMPING;
1811     jumpButtonReleased = false; // k8: was `jumpButtonRelease`
1812     jumpTime = 0;
1813     invokeJumpHelper = true;
1814   } else if (kJumpPressed && colWaterTop) {
1815     yAcc += initialJumpAcc*2;
1816     yVel -= 3;
1817     xAcc += xVel/2;
1819     status = JUMPING;
1820     jumpButtonReleased = false;
1821     jumpTime = 0;
1823     grav = gravNorm;
1824     invokeJumpHelper = true;
1825   } else if (global.hasCape && kJumpPressed && kJumped && platformCharacterIs(IN_AIR)) {
1826     switchCape();
1827   } else if (global.hasJetpack && !swimming && kJump && kJumped && platformCharacterIs(IN_AIR) && jetpackFuel > 0) {
1828     yAcc += initialJumpAcc;
1829     yVel = -1;
1830     jetpackFuel -= 1;
1831     if (jetpackFlaresTime < 1) jetpackFlaresTime = 3;
1832     //!if (alarm[10] < 1) alarm[10] = 3; // jetpack flares
1833     fallTimer = 0;
1835     status = JUMPING;
1836     jumpButtonReleased = false;
1837     jumpTime = 0;
1839     grav = 0;
1840     invokeJumpHelper = true;
1841   } else if (platformCharacterIs(ON_GROUND) && kJumpPressed && fallTimer == 0) {
1842     if (fabs(xVel) > 3 /*xVel > 3 || xVel < -3*/) {
1843       yAcc += initialJumpAcc*2;
1844       xAcc += xVel*2;
1845     } else {
1846       yAcc += initialJumpAcc*2;
1847       xAcc += xVel/2;
1848       //scrJumpHelper(); // move to location where player doesn't have to be on ground
1849     }
1850     if (global.hasJordans) {
1851       yAcc *= 3;
1852       yAccLimit = 12;
1853       grav = 0.5;
1854     } else if (global.hasSpringShoes) {
1855       yAcc *= 1.5;
1856     } else {
1857       yAccLimit = 6;
1858       grav = gravNorm;
1859     }
1861     playSound('sndJump');
1863     pushTimer = 0;
1865     // the "state" gets changed to JUMPING later on in the code
1866     status = FALLING;
1867     // "variable jumping" states
1868     jumpButtonReleased = false;
1869     jumpTime = 0;
1870     invokeJumpHelper = true;
1871   }
1873   if (kJumpPressed && invokeJumpHelper) scrJumpHelper(); // YASM 1.8.1
1875   if (jumpTime < jumpTimeTotal) jumpTime += 1;
1876   // let the character continue to jump
1877   if (!kJump) jumpButtonReleased = true;
1878   if (jumpButtonReleased) jumpTime = jumpTimeTotal;
1880   gravityIntensity = (jumpTime/jumpTimeTotal)*grav;
1882   if (kUp && platformCharacterIs(ON_GROUND) && !colLadder) {
1883     //k8:!!!looking = UP;
1884     if (xVel == 0 && xAcc == 0) status = LOOKING_UP;
1885   } else {
1886     //k8:!!!looking = 0;
1887   }
1889   if (!kUp && status == LOOKING_UP) status = STANDING;
1891   // hanging
1892   if (!colTop) {
1893     checkPerformHang(colLeft, colRight);
1894     x = ix;
1895     y = iy;
1897     // hang on stuck arrow
1898     if (status == FALLING && hangCount == 0 && y > 16 && !platformCharacterIs(ON_GROUND) &&
1899         !level.isSolidAtPoint(x, y+12)) // from Spelunky Natural
1900     {
1901       auto arrow = level.isObjectInRect(ix, iy, 16, 16, delegate bool (MapObject o) {
1902         /*
1903         writeln("---");
1904         writeln(" ARROW : (", o.x0, ",", o.y0, ")-(", o.x1, ",", o.y1, "); coll=", o.collidesWith(self));
1905         writeln(" PLAYER: (", x0, ",", y0, ")-(", x1, ",", y1, "); coll=", self.collidesWith(o), "; dy=", iy-o.iy);
1906         */
1907         if (o.stuck && iy-o.iy >= -6 && iy-o.iy <= -5 && o.collidesWith(self)) {
1908           //writeln(" *** HANG IS POSSIBLE! p5=", !!level.isObjectAtPoint(ix, iy-5, &level.cbIsObjectArrow), "; p6=", !!level.isObjectAtPoint(ix, iy-6, &level.cbIsObjectArrow));
1909           return true;
1910         }
1911         return false;
1912       }, castClass:ItemProjectileArrow, precise:false);
1913       if (arrow) {
1914         status = HANGING;
1915         // move_snap(1, 8); // was commented out in the original
1916         yVel = 0;
1917         yAcc = 0;
1918         grav = 0;
1919       }
1920       /*
1921       writeln("TRYING ARROW HANG ALLOWED");
1922       writeln("  Z00: ", !level.isObjectAtPoint(x, y-9, &level.cbIsObjectArrow));
1923       writeln("  Z01: ", !level.isObjectAtPoint(x, y+9, &level.cbIsObjectArrow));
1924       writeln("  Z02: ", !!level.isObjectAtPoint(x, y-5, &level.cbIsObjectArrow));
1925       writeln("  Z03: ", !!level.isObjectAtPoint(x, y-6, &level.cbIsObjectArrow));
1926       level.isObjectInRect(ix, iy, 16, 16, delegate bool (MapObject o) {
1927         writeln("---");
1928         writeln(" ARROW : (", o.x0, ",", o.y0, ")-(", o.x1, ",", o.y1, "); coll=", o.collidesWith(self));
1929         writeln(" PLAYER: (", x0, ",", y0, ")-(", x1, ",", y1, "); coll=", self.collidesWith(o), "; dy=", iy-o.iy);
1930         if (iy-o.iy >= -6 && iy-o.iy <= -5 && o.collidesWith(self)) {
1931           writeln(" *** HANG IS POSSIBLE! p5=", !!level.isObjectAtPoint(ix, iy-5, &level.cbIsObjectArrow), "; p6=", !!level.isObjectAtPoint(ix, iy-6, &level.cbIsObjectArrow));
1932         }
1933         return false;
1934       }, castClass:ItemProjectileArrow, precise:false);
1935       */
1936     }
1938     // hang on stuck arrow
1939     /*k8: this is not working due to collision issues; see the fixed code above
1940     if (status == FALLING && hangCount == 0 && y > 16 && !platformCharacterIs(ON_GROUND) &&
1941         !level.isSolidAtPoint(x, y+12) && // from Spelunky Natural
1942         !level.isObjectAtPoint(x, y-9, &level.cbIsObjectArrow) && !level.isObjectAtPoint(x, y+9, &level.cbIsObjectArrow))
1943     {
1944       //obj = instance_nearest(x, y-5, oArrow);
1945       auto arr0 = level.isObjectAtPoint(x, y-5, &level.cbIsObjectArrow);
1946       auto arr1 = level.isObjectAtPoint(x, y-6, &level.cbIsObjectArrow);
1947       if (arr0 || arr1) {
1948         writeln("ARROW HANG!");
1949         // get nearest arrow
1950         MapObject arr;
1951         if (arr1 && arr0) {
1952           arr = (arr0.distanceToPoint(x, y-5) < arr1.distanceToPoint(x, y-5) ? arr0 : arr1);
1953         } else {
1954           arr = (arr0 ? arr0 : arr1);
1955         }
1956         if (arr.stuck) {
1957           status = HANGING;
1958           // move_snap(1, 8); // was commented out in the original
1959           yVel = 0;
1960           yAcc = 0;
1961           grav = 0;
1962         }
1963       }
1964     }
1965     */
1966     /* this was commented in the original
1967     if (hangCount == 0 && y > 16 && !platformCharacterIs(ON_GROUND) && state == FALLING &&
1968         (collision_point(x, y-5, oTreeBranch, 0, 0) || collision_point(x, y-6, oTreeBranch, 0, 0)) &&
1969         !collision_point(x, y-9, oTreeBranch, 0, 0) && !collision_point(x, y+9, oTreeBranch, 0, 0))
1970     {
1971       state = HANGING;
1972       // move_snap(1, 8); // was commented out in the original
1973       yVel = 0;
1974       yAcc = 0;
1975       grav = 0;
1976     }
1977     */
1978   }
1980   if (hangCount > 0) --hangCount;
1982   if (status == HANGING) {
1983     closeCape();
1984     kJumped = false;
1985     if (kJumpPressed) {
1986       if (kDown) {
1987         if (global.hasGloves) {
1988           if (hangCount == 0 && y > 16 && !platformCharacterIs(ON_GROUND)) {
1989             if (kRight && colRight &&
1990                 (level.isSolidAtPoint(x+9, y-5) || level.isSolidAtPoint(x+9, y-6)))
1991             {
1992               grav = gravNorm;
1993               status = FALLING;
1994               yAcc -= grav;
1995               hangCount = 10;
1996             } else if (kLeft && colLeft &&
1997                        (level.isSolidAtPoint(x-9, y-5) || level.isSolidAtPoint(x-9, y-6)))
1998             {
1999               grav = gravNorm;
2000               status = FALLING;
2001               yAcc -= grav;
2002               hangCount = 10;
2003             } else {
2004               grav = gravNorm;
2005               status = FALLING;
2006               yAcc -= grav;
2007               hangCount = 5;
2008             }
2009           }
2010         } else {
2011           grav = gravNorm;
2012           status = FALLING;
2013           yAcc -= grav;
2014           hangCount = 5;
2015         }
2016       } else {
2017         grav = gravNorm;
2018         status = JUMPING;
2019         yAcc += initialJumpAcc*2;
2020         shiftX(dir == Dir.Right ? -2 : 2);
2021         x = ix;
2022         cameraBlockX = 3;
2023         hangCount = hangCountMax;
2024         if (level.isObjectAtPoint(x, y-5, &level.cbIsObjectArrow) || level.isObjectAtPoint(x, y-6, &level.cbIsObjectArrow)) hangCount /= 2; //Spelunky Natural
2025       }
2026     }
2027     if ((dir == Dir.Left && !isCollisionLeft(2)) ||
2028         (dir == Dir.Right && !isCollisionRight(2)))
2029     {
2030       grav = gravNorm;
2031       status = FALLING;
2032       yAcc -= grav;
2033       hangCount = 4;
2034     }
2035   } else {
2036     grav = gravNorm;
2037   }
2039   // pressing down while standing
2040   if (kDown && platformCharacterIs(ON_GROUND) && !whipping) {
2041     if (colBot) {
2042       status = DUCKING;
2043     } else if (colPlatBot) {
2044       // climb down ladder if possible, else jump down
2045       fallTimer = 0;
2046       if (!colBot) {
2047         //ladder = instance_place(x, y+16, oLadder);
2049         // from Spelunky Natural
2050         /*
2051         ladder = collision_line(x-4, y+16, x+4, y+16, oLadder, 0, 0);
2052         if (!ladder) ladder = collision_line(x-4, y+16, x+4, y+16, oLadderTop, 0, 0);
2053         */
2054         auto ladder = level.checkTilesInRect(x-4, y+16, 9, 1, &level.cbCollisionAnyLadder);
2055         //writeln("DOWN; cpb=", colPlatBot, "; cb=", colBot, "; ladder=", !!ladder);
2057         if (ladder) {
2058           if (abs(x-(ladder.x0+8)) < 4) {
2059             x = ladder.ix+8;
2060             setX(x);
2061             xVel = 0;
2062             yVel = 0;
2063             xAcc = 0;
2064             yAcc = 0;
2065             status = CLIMBING;
2066           }
2067         } else {
2068           shiftY(1);
2069           y = iy;
2070           status = FALLING;
2071           yAcc += grav;
2072           kJumped = true; // Spelunky Natural
2073         }
2074       }
2075       else {
2076         // the character can't move down because there is a solid in the way
2077         status = RUNNING;
2078       }
2079     }
2080   }
2081   if (!kDown && status == DUCKING) {
2082     status = STANDING;
2083     xVel = 0;
2084     xAcc = 0;
2085   }
2086   if (xVel == 0 && xAcc == 0 && status == RUNNING) status = STANDING;
2087   if (xAcc != 0 && status == STANDING) status = RUNNING;
2088   if (yVel < 0 && platformCharacterIs(IN_AIR) && status != HANGING) status = JUMPING;
2089   if (yVel > 0 && platformCharacterIs(IN_AIR) && status != HANGING) {
2090     status = FALLING;
2091     setCollisionBounds(-5, -6, 5, 8);
2092   } else {
2093     setCollisionBounds(-5, -6, 5, 8);
2094   }
2096   // CLIMB LADDER
2097   bool colPointLadder = !!level.isAnyLadderAtPoint(x, y);
2099   /* this was commented in the original
2100   if ((kUp && platformCharacterIs(IN_AIR) && collision_point(x, y-8, oLadder, 0, 0) && ladderTimer == 0) ||
2101       (kUp && colPointLadder && ladderTimer == 0) ||
2102       (kDown && colPointLadder && ladderTimer == 0 && platformCharacterIs(ON_GROUND) && collision_point(x, y+9, oLadderTop, 0, 0) && xVel == 0))
2103   {
2104     ladder = 0;
2105     ladder = instance_place(x, y-8, oLadder);
2106     if (instance_exists(ladder)) {
2107       if (abs(x-(ladder.x0+8)) < 4) {
2108         x = ladder.ix+8;
2109         setX(x);
2110         if (!collision_point(x, y, oLadder, 0, 0) && !collision_point(x, y, oLadderTop, 0, 0)) { y = ladder.iy+14; setY(y); }
2111         xVel = 0;
2112         yVel = 0;
2113         xAcc = 0;
2114         yAcc = 0;
2115         state = CLIMBING;
2116       }
2117     }
2118   }*/
2120   // Spelunky Natural - Multiple changes to this big "if" condition
2121   if ((kUp && platformCharacterIs(IN_AIR) && ladderTimer == 0 && level.checkTilesInRect(x-2, y-8, 5, 1, &level.cbCollisionLadder)) ||
2122       (kUp && colPointLadder && ladderTimer == 0) ||
2123       (kDown && colPointLadder && ladderTimer == 0 && platformCharacterIs(ON_GROUND) && xVel == 0 && level.isLadderTopAtPoint(x, y+9)) ||
2124       ((kUp || kDown) && status == HANGING && level.checkTilesInRect(x-2, y, 5, 1, &level.cbCollisionLadder)))
2125   {
2126     //ladder = 0;
2127     //auto ladder = instance_place(x, y-8, oLadder);
2128     auto ladder = level.isLadderAtPoint(x, y-8);
2129     if (ladder) {
2130       //writeln("LADDER01! plrx=", x, "; ladder.x0=", ladder.x0, "; ladder.ix=", ladder.ix, "; ladder class=", GetClassName(ladder.Class));
2131       if (abs(x-(ladder.x0+8)) < 4) {
2132         x = ladder.ix+8;
2133         setX(x);
2134         if (!level.isAnyLadderAtPoint(x, y)) { y = ladder.y0+14; setY(y); }
2135         xVel = 0;
2136         yVel = 0;
2137         xAcc = 0;
2138         yAcc = 0;
2139         status = CLIMBING;
2140       }
2141     }
2142   }
2144   /* this was commented in the original
2145   if (sprite_index == sDuckToHangL || sprite_index == sDamselDtHL) {
2146     ladder = 0;
2147     if (facing == LEFT && collision_rectangle(x-8, y, x, y+16, oLadder, 0, 0) && !collision_point(x-4, y+16, oSolid, 0, 0)) {
2148       ladder = instance_nearest(x-4, y+16, oLadder);
2149     } else if (facing == RIGHT && collision_rectangle(x, y, x+8, y+16, oLadder, 0, 0) && !collision_point(x+4, y+16, oSolid, 0, 0)) {
2150       ladder = instance_nearest(x+4, y+16, oLadder);
2151     }
2152     if (ladder) {
2153       x = ladder.ix+8;
2154       setX(x);
2155       xVel = 0;
2156       yVel = 0;
2157       xAcc = 0;
2158       yAcc = 0;
2159       state = CLIMBING;
2160     }
2161   }
2163   if (colLadder && state == CLIMBING && kJumpPressed && !whipping) {
2164     if (kLeft) xVel = -departLadderXVel; else if (kRight) xVel = departLadderXVel; else xVel = 0;
2165     yAcc += departLadderYVel;
2166     state = JUMPING;
2167     jumpButtonReleased = false;
2168     jumpTime = 0;
2169     ladderTimer = 5;
2170   }
2171   */
2173   // calculate horizontal/vertical friction
2174   if (status == CLIMBING) {
2175     xFric = frictionClimbingX;
2176     yFric = frictionClimbingY;
2177   } else {
2178     //if (runKey && platformCharacterIs(ON_GROUND) && runHeld >= 10)
2179     if ((runKey && runHeld >= 10) && (platformCharacterIs(ON_GROUND) || global.config.toggleRunAnywhere)) {
2180       // YASM 1.8.1
2181       if (kLeft) {
2182         // run
2183         xVel -= 0.1;
2184         xVelLimit = 6;
2185         xFric = frictionRunningFastX;
2186       } else if (kRight) {
2187         xVel += 0.1;
2188         xVelLimit = 6;
2189         xFric = frictionRunningFastX;
2190       }
2191     } else if (status == DUCKING) {
2192       if (xVel < 2 && xVel > -2) {
2193         xFric = 0.2;
2194         xVelLimit = 3;
2195         imageSpeed = 0.8;
2196       } else if (kLeft && global.config.downToRun) {
2197         // run
2198         xVel -= 0.1;
2199         xVelLimit = 6;
2200         xFric = frictionRunningFastX;
2201       } else if (kRight && global.config.downToRun) {
2202         xVel += 0.1;
2203         xVelLimit = 6;
2204         xFric = frictionRunningFastX;
2205       } else {
2206         xVel *= 0.8;
2207         if (xVel < 0.5) xVel = 0;
2208         xFric = 0.2;
2209         xVelLimit = 3;
2210         imageSpeed = 0.8;
2211       }
2212     } else {
2213       // decrease the friction when the character is "flying"
2214       if (platformCharacterIs(IN_AIR)) {
2215         if (dead || stunned) xFric = 1.0; else xFric = 0.8;
2216       } else {
2217         xFric = frictionRunningX;
2218       }
2219     }
2221     /* // ORIGINAL RUN/WALK xVel/xFric code  this was commented in the original
2222     if (runKey && platformCharacterIs(ON_GROUND) && runHeld >= 10) {
2223       if (kLeft) {
2224         // run
2225         xVel -= 0.1;
2226         xVelLimit = 6;
2227         xFric = frictionRunningFastX;
2228       } else if (kRight) {
2229         xVel += 0.1;
2230         xVelLimit = 6;
2231         xFric = frictionRunningFastX;
2232       }
2233     } else if (state == DUCKING) {
2234       if (xVel < 2 && xVel > -2) {
2235         xFric = 0.2
2236         xVelLimit = 3;
2237         imageSpeed = 0.8;
2238       } else if (kLeft && global.downToRun) {
2239         // run
2240         xVel -= 0.1;
2241         xVelLimit = 6;
2242         xFric = frictionRunningFastX;
2243       } else if (kRight && global.downToRun) {
2244         xVel += 0.1;
2245         xVelLimit = 6;
2246         xFric = frictionRunningFastX;
2247       } else {
2248         xVel *= 0.8;
2249         if (xVel < 0.5) xVel = 0;
2250         xFric = 0.2
2251         xVelLimit = 3;
2252         imageSpeed = 0.8;
2253       }
2254     } else {
2255       // decrease the friction when the character is "flying"
2256       if (platformCharacterIs(IN_AIR)) {
2257         if (dead || stunned) xFric = 1.0; else xFric = 0.8;
2258       } else {
2259         xFric = frictionRunningX;
2260       }
2261     }
2262     */
2264     // stuck on web or underwater
2265     if (level.isObjectAtPoint(x, y, &level.cbIsObjectWeb)) {
2266       xFric = 0.2;
2267       yFric = 0.2;
2268       fallTimer = 0;
2269     } else if (level.isObjectAtPoint(x, y, &level.cbIsObjectBlob)) {
2270       // blob enemy
2271       //obj = instance_place(x, y, oBlob); this was commented in the original
2272       //xVel += obj.xVel; this was commented in the original
2273       xFric = 0.1;
2274       yFric = 0.3;
2275       fallTimer = 0;
2276     } else if (level.isWaterAtPoint(x, y/*, oWater, -1, -1*/)) {
2277       closeCape();
2278       //if (!runKey && global.toggleRunAnywhere) xFric = frictionRunningX; // YASM 1.8.1 this was commented in the original
2279       if (!platformCharacterIs(ON_GROUND)) xFric = frictionRunningX;
2280       if (status == FALLING && yVel > 0) {
2281         // Spelunky Natural
2282              if (global.config.naturalSwim && kUp) yFric = 0.2;
2283         else if (global.config.naturalSwim && kDown) yFric = 0.8;
2284         else yFric = 0.5;
2285       } else if (!level.isWaterAtPoint(x, y-9/*, oWater, -1, -1*/)) {
2286         yFric = 1;
2287       } else {
2288         yFric = 0.9;
2289       }
2290       if (yVel < -6 && global.config.noDolphin) {
2291         // Spelunky Natural (changed from -4 to -6)
2292         yVel = -6;
2293       }
2294     } else {
2295       swimming = false;
2296       yFric = 1;
2297     }
2298   }
2300   if (colIceBot && status != DUCKING && !global.hasSpikeShoes) {
2301     xFric = 0.98;
2302     yFric = 1;
2303   }
2305   // YASM 1.8.1
2306   if (global.config.toggleRunAnywhere) {
2307     if (!kJump && !kDown && !runKey) xVelLimit = 3;
2308   }
2310   // RUNNING
2311   if (platformCharacterIs(ON_GROUND)) {
2312          if (status == RUNNING && kLeft && colLeft) pushTimer += 1;
2313     else if (status == RUNNING && kRight && colRight) pushTimer += 1;
2314     else pushTimer = 0;
2316     //if (platformCharacterIs(ON_GROUND) && !kJump && !kDown && !runKey) this was commented in the original
2317     if (!kJump && !kDown && !runKey) xVelLimit = 3;
2319     /* this was commented in the original
2320     // ledge flip
2321     if (state == DUCKING && fabs(xVel) < 3 && facing == LEFT &&
2322         //collision_point(x, y+9, oSolid, 0, 0) && !collision_point(x-1, y+9, oSolid, 0, 0) && kLeft)
2323         collision_point(x, y+9, oSolid, 0, 0) && !collision_line(x-1, y+9, x-10, y+9, oSolid, 0, 0) && kLeft)
2324     */
2326     // ledge flip
2327     int dhdir = 0;
2328          if (kLeft && dir == Dir.Left) dhdir = -1;
2329     else if (kRight && dir == Dir.Right) dhdir = 1;
2331     if (dhdir && status == DUCKING && fabs(xVel) < 3+(dhdir < 0 ? 1 : 0) &&
2332         level.isSolidAtPoint(x, y+9) && !level.checkTilesInRect(x+(dhdir < 0 ? -8 : 1), y+9, 8, 8))
2333     {
2334       status = DUCKTOHANG;
2335       if (holdItem) {
2336         if (!global.config.scumFlipHold || holdItem.heavy) {
2337           /*
2338           holdItem.heldBy = none;
2339           if (holdItem.objName == 'GoldIdol') holdItem.shiftY(-8);
2340           */
2341           //else if (holdItem.type == "Block Item") { with (oBlockPreview) instance_destroy(); }
2342           scrDropItem(LostCause.Hang, (dir == Dir.Left ? -1 : 1), -4);
2343         }
2344       }
2345       knockOffMonkeys();
2346     }
2347   }
2349   if (status == DUCKTOHANG) {
2350     setXY(xPrev, yPrev);
2351     x = ix;
2352     y = iy;
2353     xVel = 0;
2354     yVel = 0;
2355     xAcc = 0;
2356     yAcc = 0;
2357     grav = 0;
2358   }
2360   // parachute and cape
2361   if (!level.inWinCutscene) {
2362     if (isParachuteActive() || isCapeActiveAndOpen()) yFric = 0.5;
2363   }
2365   if (pushTimer > 100) pushTimer = 100;
2367   // limits the acceleration if it is too extreme
2368   xAcc = fclamp(xAcc, -xAccLimit, xAccLimit);
2369   yAcc = fclamp(yAcc, -yAccLimit, yAccLimit);
2371   // applies the acceleration
2372   xVel += xAcc;
2373   if (dead || stunned) yVel += 0.6; else yVel += yAcc;
2375   // nullifies the acceleration
2376   xAcc = 0;
2377   yAcc = 0;
2379   // applies the friction to the velocity, now that the velocity has been calculated
2380   xVel *= xFric;
2381   yVel *= yFric;
2383   auto oBall = getMyBall();
2384   // apply ball and chain
2385   if (oBall) {
2386     int distsq = (ix-oBall.ix)*(ix-oBall.ix)+(iy-oBall.iy)*(iy-oBall.iy);
2387     if (distsq >= 24*24) {
2388       if (xVel > 0 && oBall.ix < ix && abs(oBall.ix-ix) > 24) xVel = 0;
2389       if (xVel < 0 && oBall.ix > ix && abs(oBall.ix-ix) > 24) xVel = 0;
2390       if (yVel > 0 && oBall.iy < iy && abs(oBall.iy-iy) > 24) {
2391         if (abs(oBall.ix-ix) < 1) {
2392           //teleportTo(destx:oBall.ix);
2393           fltx = oBall.fltx;
2394           prevFltX = oBall.prevFltX;
2395           x = ix;
2396         } else if (oBall.ix < ix && !kRight) {
2397                if (xVel > 0) xVel *= -0.25;
2398           else if (xVel == 0) xVel -= 1;
2399         } else if (oBall.ix > ix && !kLeft) {
2400                if (xVel < 0) xVel *= -0.25;
2401           else if (xVel == 0) xVel += 1;
2402         }
2403         yVel = 0;
2404         fallTimer = 0;
2405       }
2406       if (yVel < 0 && oBall.iy > iy && abs(oBall.iy-iy) > 24) yVel = 0;
2407     }
2408   }
2410   // apply the limits since the velocity may be too extreme
2411   if (!dead && !stunned) xVel = fclamp(xVel, -xVelLimit, xVelLimit);
2412   yVel = fclamp(yVel, -yVelLimit, yVelLimit);
2414   // approximates the "active" variables
2415   if (fabs(xVel) < 0.0001) xVel = 0;
2416   if (fabs(yVel) < 0.0001) yVel = 0;
2417   if (fabs(xAcc) < 0.0001) xAcc = 0;
2418   if (fabs(yAcc) < 0.0001) yAcc = 0;
2420   bool wasInWall = !!isCollision();
2421   moveRel(xVel, yVel);
2423   // don't go out of level (if we're not in ending sequence)
2424   if (!level.inWinCutscene && !level.inIntroCutscene) {
2425          if (ix < 0) fltx = 0;
2426     else if (ix > level.tilesWidth*16-16) fltx = level.tilesWidth*16-16;
2427     if (iy < 0) flty = 0;
2429     if (!dead) hackBoulderCollision();
2431     if (!wasInWall && isCollision()) {
2432       writeln("** FUUUU (XXX)");
2433       if (isCollisionBottom(0) && !isCollisionBottom(-2)) {
2434         flty = iy-2;
2435       }
2436       // we can stuck in the wall with this
2437       if (isCollisionLeft(0)) {
2438         writeln("** FUUUU (001: left)");
2439         while (isCollisionLeft(0) && !isCollisionRight(1)) shiftX(1);
2440       } else if (isCollisionRight(0)) {
2441         writeln("** FUUUU (001: right)");
2442         while (isCollisionRight(0) && !isCollisionLeft(1)) shiftX(-1);
2443       }
2444     }
2446     if (!dead) hackBoulderCollision();
2448     // move out of wall by 1 px, if possible
2449     if (!dead && isCollision()) {
2450       if (isCollisionBottom(0) && !isCollisionBottom(-1)) flty = iy-1;
2451       if (isCollisionTop(0) && !isCollisionTop(1)) flty = iy+1;
2452       if (isCollisionLeft(0) && !isCollisionLeft(1)) fltx = ix+1;
2453       if (isCollisionRight(0) && !isCollisionRight(-1)) fltx = ix-1;
2454     }
2456     if (!dead && isCollision()) {
2457       //k8:HACK: try to duck
2458       bool wallDeath = true;
2459       if (platformCharacterIs(ON_GROUND)) {
2460         setCollisionBounds(-5, -6, 5, 8);
2461         wallDeath = !!isCollision();
2462         if (wallDeath) {
2463           setCollisionBounds(-8, -6, 8, 8);
2464         } else {
2465           // force ducking
2466           status = DUCKING;
2467         }
2468       }
2470       if (wallDeath) {
2471         foreach (; 0..6) {
2472           if (isCollision()) {
2473                  if (isCollisionLeft(0) && !isCollisionRight(4)) fltx = ix+1;
2474             else if (isCollisionRight(0) && !isCollisionLeft(4)) fltx = ix-1;
2475             else if (isCollisionBottom(0) && !isCollisionTop(4)) flty = iy-1;
2476             else if (isCollisionTop(0) && !isCollisionBottom(4)) flty = iy+1;
2477             else break;
2478           }
2479         }
2481         if (wallDeath && isCollision()) {
2482           if (!dead) level.addDeath('wall');
2483           //visible = false;
2484           dead = true;
2485           writeln("PLAYER KILLED BY WALL");
2486           global.plife = 0; // oops
2487         }
2488       }
2489     }
2490   } else {
2491     // in cutscene
2492     //writeln("flty=", flty, "; iy=", iy);
2493     if (flty <= 0) {
2494       status = STANDING;
2495     }
2496   }
2498   // figures out what the sprite index of the character should be
2499   characterSprite();
2501   // sets the previous state and the previously previous state
2502   statePrevPrev = statePrev;
2503   statePrev = status;
2505   // calculates the imageSpeed based on the character's velocity
2506   if (status == RUNNING || status == DUCKING || status == LOOKING_UP) {
2507     if (status == RUNNING || status == LOOKING_UP) imageSpeed = fabs(xVel)*runAnimSpeed+0.1;
2508   }
2510   if (status == CLIMBING) imageSpeed = sqrt(xVel*xVel+yVel*yVel)*climbAnimSpeed;
2512   if (xVel >= 4 || xVel <= -4) {
2513     imageSpeed = 1;
2514     if (platformCharacterIs(ON_GROUND)) {
2515       setCollisionBounds(-8, -6, 8, 8);
2516     } else {
2517       setCollisionBounds(-5, -6, 5, 8);
2518     }
2519   } else {
2520     setCollisionBounds(-5, -6, 5, 8);
2521   }
2523   if (whipping) imageSpeed = 1;
2525   if (status == DUCKTOHANG) {
2526     imageFrame = 0;
2527     imageSpeed = 0.8;
2528   }
2530   // limit the imageSpeed at 1 so the animation always looks good
2531   if (imageSpeed > 1) imageSpeed = 1;
2533   //if (kItemPressed) writeln("ITEM! dead=", dead, "; stunned=", stunned, "; active=", active);
2534   if (dead || stunned || !active) {
2535     // do nothing
2536   } else if (/*inGame &&*/ kItemPressed && !whipping) {
2537     // SWITCH
2538     if (kUp) scrSwitchToStickyBombs(); else scrSwitchToNextItem();
2539   } else if (/*inGame &&*/ kRopePressed && global.rope > 0 && !whipping) {
2540     if (!kDown && colTop) {
2541       // do nothing
2542     } else {
2543       launchRope(kDown, doDrop:true);
2544     }
2545   } else if (/*inGame &&*/ kBombPressed && global.bombs > 0 && !whipping) {
2546     if (holdItem isa ItemWeaponBow && bowArmed) {
2547       if (holdArrow != ARROW_BOMB) {
2548         //writeln("set bow arrows to bomb");
2549         holdArrow = ARROW_BOMB;
2550       } else {
2551         //writeln("set bow arrows to normal");
2552         holdArrow = ARROW_NORM;
2553       }
2554     } else {
2555       scrLaunchBomb();
2556     }
2557   }
2560   // open chest/crate
2561   if (!dead && !stunned && kUp && kAttackPressed) {
2562     auto octr = ItemOpenableContainer(level.isObjectInRect(ix, iy, width, height, delegate bool (MapObject o) {
2563       return (o isa ItemOpenableContainer);
2564     }));
2565     if (octr) {
2566       if (octr.openMe()) kAttackPressed = false;
2567     }
2568   }
2571   // use weapon / attack
2572   if (!dead && !stunned && kAttackPressed && !holdItem /*&& !pickedItem*/) {
2573     bowArmed = false;
2574     bowStrength = 0;
2575     sndStopSound('sndBowPull');
2576     if (status != DUCKING && status != DUCKTOHANG && !whipping && !isExitingSprite()) {
2577       imageSpeed = 0.6;
2578       if (global.isTunnelMan) {
2579         if (platformCharacterIs(ON_GROUND) || platformCharacterIs(IN_AIR)) {
2580           setSprite('sTunnelAttackL');
2581           whipping = true;
2582         }
2583       } else if (global.isDamsel) {
2584         setSprite('sDamselAttackL');
2585         whipping = true;
2586       } else {
2587         setSprite('sAttackLeft');
2588         whipping = true;
2589       }
2590     } else if (kDown && !pickedItem) {
2591       // pick up item
2592       //HACK: always select dice to throw if there are two dices there
2593       MapObject diceToThrow = level.isObjectInRect(x-8, y, 9, 9, delegate bool (MapObject o) {
2594         if (o.spectral || !o.canPickUp) return false;
2595         if (ItemDice(o).isReadyToThrowForBet) return o.collidesWith(self);
2596         return false;
2597       }, precise:false, castClass:ItemDice);
2598       MapObject obj;
2599       if (diceToThrow) {
2600         obj = diceToThrow;
2601       } else {
2602         obj = level.isObjectInRect(x-8, y, 9, 9, delegate bool (MapObject o) {
2603           if (o.spectral || !o.canPickUp) return false;
2604           if (!o.collidesWith(self)) return false;
2605           return o.onCanBePickedUp(self);
2606           /*
2607           if (o isa MapItem) return (o.active && o.canPickUp && !o.spectral);
2608           if (o isa MapEnemy) return (o.active && o.canPickUp && !o.spectral && (o.dead || o.status >= MapObject::STUNNED || o.meGoldMonkey));
2609           if (o isa ObjCharacter) return (o.active && o.canPickUp && !o.spectral);
2610           */
2611           return false;
2612         }, precise:false);
2613       }
2614       if (!obj && diceToThrow) obj = diceToThrow;
2615       if (obj) {
2616         // `canPickUp` is checked in callback
2617         if (/*obj.canPickUp &&*/ true /*k8: do we really need this? !level.isSolidAtPoint(obj.ix+2, obj.iy)*/) {
2618           //pickupItemType = holdItem.type;
2619           //!if (isAshShotgun(holdItem)) pickupItemType = "Boomstick";
2620           //!if (isGoldMonkey(obj) and obj.status &lt; 98) obj.status = 0; // do not play walk animation while held
2622           if (!obj.onTryPickup(self)) {
2623             if (obj.isInstanceAlive) scrPickupItem(obj);
2624           }
2626           /+!
2627           if (holdItem.type == "Bow" and holdItem.new) {
2628             holdItem.new = false;
2629             global.arrows += 6;
2630             if (global.arrows &gt; 99) global.arrows = 99;
2631           }
2632           +/
2633         }
2634       }
2635     }
2636   } else if (!dead && !stunned) {
2637     if (holdItem isa ItemWeaponBow) {
2638       //writeln("BOW! kAttack=", kAttack, "; kAttackPressed=", kAttackPressed, "; bowArmed=", bowArmed, "; bowStrength=", bowStrength, "; holdArrow=", holdArrow);
2639       if (kAttackPressed) {
2640         if (scrPlayerIsDucking()) {
2641           scrUsePutItemOnGround();
2642         } else if (!bowArmed) {
2643           bowStrength = 0;
2644           ItemWeaponBow(holdItem).armBow(self);
2645         }
2646       }
2647       if (kAttack) {
2648         if (bowArmed && bowStrength < 12) {
2649           bowStrength += 0.2;
2650           //writeln("arming: ", bowStrength);
2651         } else {
2652           sndStopSound('sndBowPull');
2653         }
2654       } else {
2655         //writeln("   xxBOW!");
2656         // ...and shoot
2657         scrFireBow();
2658       }
2659       if (!holdArrow) holdArrow = ARROW_NORM;
2660     } else {
2661       if (kAttackPressed && holdItem) scrUseItem();
2662     }
2663   }
2665   // buy items
2666   if (!dead && !stunned && kPayPressed) {
2667       // find nearest shopkeeper
2668     auto sc = MonsterShopkeeper(level.findNearestObject(ix, iy, delegate bool (MapObject o) {
2669       auto sc = MonsterShopkeeper(o);
2670       if (!sc) return false;
2671       //if (needCraps && sc.stype != 'Craps') return false;
2672       if (sc.dead || sc.angered || sc.outlaw) return false;
2673       return sc.canSellItem(self, holdItem);
2674     }));
2675     if (level.isInShop(ix/16, iy/16)) {
2676       // if no shopkeepers found, just use it
2677       if (!sc) {
2678         if (holdItem) {
2679           holdItem.forSale = false;
2680           holdItem.onTryPickup(self);
2681         }
2682       } else if (global.thiefLevel == 0 && !global.murderer) {
2683         // only law-abiding players can buy/sell items or play games
2684         if (holdItem) writeln("shop item interaction: ", holdItem.objName, "; cost=", holdItem.cost);
2685         if (sc.doSellItem(self, holdItem)) {
2686           // use it
2687           if (holdItem) {
2688             holdItem.forSale = false;
2689             holdItem.onTryPickup(self);
2690           }
2691         }
2692         if (holdItem && !holdItem.isInstanceAlive) {
2693           holdItem = none;
2694           scrSwitchToPocketItem(forceIfEmpty:false); // just in case
2695         }
2696       }
2697     } else {
2698       // use pickup, if any
2699       if (holdItem isa ItemPickup) {
2700         // make nearest shopkeeper angry (an unlikely situation, but still...)
2701         if (sc && holdItem.forSale) level.scrShopkeeperAnger(GameLevel::SCAnger.ItemStolen);
2702         holdItem.forSale = false;
2703         holdItem.onTryPickup(self);
2704       } else {
2705         pickupsAround.clear();
2706         level.isObjectInRect(x0, y0, width, height, delegate bool (MapObject o) {
2707           auto pk = ItemPickup(o);
2708           if (pk && pk.collidesWith(self)) {
2709             bool found = false;
2710             foreach (auto opk; pickupsAround) if (opk == pk) { found = true; break; }
2711             if (!found) pickupsAround[$] = pk;
2712           }
2713           return false;
2714         }, precise:false);
2715         // now try to use all pickups
2716         foreach (ItemPickup pk; pickupsAround) {
2717           if (pk.isInstanceAlive) {
2718             if (sc && pk.forSale) level.scrShopkeeperAnger(GameLevel::SCAnger.ItemStolen);
2719             pk.forSale = false;
2720             pk.onTryPickup(self);
2721           }
2722         }
2723         pickupsAround.clear();
2724       }
2725     }
2726   }
2729 transient array!ItemPickup pickupsAround;
2732 // ////////////////////////////////////////////////////////////////////////// //
2733 override bool initialize () {
2734   if (!::initialize()) return false;
2736   powerups.length = 0;
2737   powerups[$] = SpawnObject(PPParachute);
2738   powerups[$] = SpawnObject(PPCape);
2740   foreach (PlayerPowerup pp; powerups) pp.owner = self;
2742   if (global.isDamsel) {
2743     desc = "Damsel";
2744     desc2 = "An athletic, unfittingly-dressed woman with extremely awkward running form.";
2745     setSprite('sDamselLeft');
2746   } else if (global.isTunnelMan) {
2747     desc = "Tunnel Man";
2748     desc2 = "A miner from the desert. His tools are a cut above the rest.";
2749     setSprite('sTunnelLeft');
2750   } else {
2751     desc = "Spelunker";
2752     desc2 = "A strange little man who spends his time exploring caverns. He wants to be just like Indiana Jones when he grows up.";
2753     setSprite('sStandLeft');
2754   }
2756   swimming = false;
2758   dir = Dir.Right;
2760   // scum ClimbSpeed
2761   switch (global.config.scumClimbSpeed) {
2762     case 2:
2763       climbAcc = 0.9;
2764       climbAnimSpeed = 0.4;
2765       climbSndSpeed = 6;
2766       break;
2767     case 3:
2768       climbAcc = 1.2;
2769       climbAnimSpeed = 0.45;
2770       climbSndSpeed = 5;
2771       break;
2772     case 4:
2773       climbAcc = 1.5;
2774       climbAnimSpeed = 0.5;
2775       climbSndSpeed = 4;
2776       break;
2777     case 5:
2778       climbAcc = 1.8;
2779       climbAnimSpeed = 0.5;
2780       climbSndSpeed = 3;
2781       break;
2782     default:
2783       climbAcc = 0.6;       // how fast the character will climb
2784       climbAnimSpeed = 0.4; // relates to how fast the climbing animation should go
2785       climbSndSpeed = 8;
2786       break;
2787   }
2789   // sets the collision bounds to fit the default sprites (you can edit the arguments of the script)
2790   setCollisionBounds(-5, -5, 5, 8); // setCollisionBounds(-5, -8, 5, 8);
2792   statePrev = status;
2793   statePrevPrev = statePrev;
2794   gravityIntensity = grav;  // this variable describes the current force due to gravity (this variable is altered for variable jumping)
2795   jumpTime = jumpTimeTotal; // current time of the jump (0=start of jump, jumpTimeTotal=end of jump)
2797   return true;
2801 // ////////////////////////////////////////////////////////////////////////// //
2802 override void onAnimationLooped () {
2803   auto spr = getSprite();
2804   if (spr.Name == 'sAttackLeft' || spr.Name == 'sDamselAttackL' || spr.Name == 'sTunnelAttackL') {
2805     whipping = false;
2806     if (holdItem) holdItem.visible = true;
2807   } else if (spr.Name == 'sDuckToHangL' || spr.Name == 'sDamselDtHL' || spr.Name == 'sTunnelDtHL') {
2808     shiftY(16);
2809     moveSnap(1, 8);
2810     int x = ix, y = iy;
2811     xVel = 0;
2812     yVel = 0;
2813     xAcc = 0;
2814     yAcc = 0;
2815     grav = 0;
2816     MapTile obj;
2817     if (dir == Dir.Left) {
2818       // left
2819       obj = level.isAnyLadderAtPoint(x-8, y);
2820     } else {
2821       // right
2822       obj = level.isAnyLadderAtPoint(x+8, y);
2823     }
2824     if (obj) {
2825       status = CLIMBING;
2826       setX(obj.ix+8);
2827     } else if (dir == Dir.Left) {
2828       status = HANGING;
2829       dir = Dir.Right;
2830       shiftX(-6);
2831       shiftX(1);
2832     } else {
2833       status = HANGING;
2834       dir = Dir.Left;
2835       shiftX(6);
2836     }
2837   } else if (isExitingSprite()) {
2838     scrPlayerExit();
2839     //!global.cleanSolids = true;
2840   }
2844 void activatePlayerWeapon () {
2845   if (dead) {
2846     if (holdItem isa PlayerWeapon) {
2847       auto wep = holdItem;
2848       holdItem = none;
2849       wep.instanceRemove();
2850       return;
2851     }
2852   }
2853   if (global.config.unarmed) return;
2855   if (holdItem isa PlayerWeapon) {
2856     if (!whipping) {
2857       /*
2858       writeln("000: !!!!!!!");
2859       if (holdItem) writeln("  H(", GetClassName(holdItem.Class), "): '", holdItem.objType, "'");
2860       if (pickedItem) writeln("  P(", GetClassName(pickedItem.Class), "): '", pickedItem.objType, "'");
2861       */
2862       auto wep = holdItem;
2863       holdItem = none;
2864       wep.instanceRemove();
2865       return;
2866     }
2867   }
2869   if (holdItem) return;
2871   auto spr = getSprite();
2872   if (spr.Name != 'sAttackLeft' && spr.Name != 'sDamselAttackL' && spr.Name != 'sTunnelAttackL') return;
2874   if (imageFrame > 4) {
2875     //bool hitEnemy = (PlayerWeapon(holdItem) ? PlayerWeapon(holdItem).hitEnemy : false);
2876     if (global.isTunnelMan || pickedItem isa ItemWeaponMattock) {
2877       holdItem = level.MakeMapObject(ix+(dir == Dir.Left ? -16 : 16), iy, 'oMattockHit');
2878       if (imageFrame < 7) playSound('sndWhip');
2879     } else if (pickedItem isa ItemWeaponMachete) {
2880       holdItem = level.MakeMapObject(ix+(dir == Dir.Left ? -16 : 16), iy, 'oSlash');
2881       playSound('sndWhip');
2882     } else {
2883       holdItem = level.MakeMapObject(ix+(dir == Dir.Left ? -16 : 16), iy, 'oWhip');
2884       playSound('sndWhip');
2885     }
2886     /+ not needed anymore
2887     if (holdItem) {
2888       holdItem.active = true;
2889       //if (PlayerWeapon(holdItem)) PlayerWeapon(holdItem).hitEnemy = hitEnemy;
2890     }
2891     +/
2892   } else if (imageFrame < 2) {
2893     if (global.isTunnelMan || pickedItem isa ItemWeaponMattock) {
2894       holdItem = level.MakeMapObject(ix+(dir == Dir.Left ? -16 : 16), iy, 'oMattockPre');
2895     } else if (pickedItem isa ItemWeaponMachete) {
2896       holdItem = level.MakeMapObject(ix+(dir == Dir.Left ? -16 : 16), iy, 'oMachetePre');
2897     } else {
2898       holdItem = level.MakeMapObject(ix+(dir == Dir.Left ? 16 : -16), iy, 'oWhipPre');
2899     }
2900     /+ not needed anymore
2901     if (holdItem) holdItem.active = true;
2902     +/
2903   }
2905   /*
2906   if (holdItem) {
2907     if (holdItem.type == "Machete") {
2908       obj = instance_create(x-16, y, oSlash);
2909       obj.sprite_index = sSlashLeft;
2910       playSound(global.sndWhip);
2911     } else if (holdItem.type == "Mattock") {
2912       obj = instance_create(x-16, y, oMattockHit);
2913       obj.sprite_index = sMattockHitL;
2914       if (image_index &lt; 7) playSound(global.sndWhip);
2915     }
2916   } else {
2917     if (global.isTunnelMan) {
2918       obj = instance_create(x-16, y, oMattockHit);
2919       obj.sprite_index = sMattockHitL;
2920       if (image_index &lt; 7) playSound(global.sndWhip);
2921     } else {
2922       obj = instance_create(x-16, y, oWhip);
2923       if (global.scumWhipUpgrade == 1) obj.sprite_index = sWhipLongLeft; else obj.sprite_index = sWhipLeft;
2924       playSound(global.sndWhip);
2925     }
2926   }
2927   */
2931 //bool webHit = false;
2933 bool doBreakWebsCB (MapObject o) {
2934   if (o isa ItemWeb) {
2935     writeln("IN WEB!");
2936     /*if (!webHit)*/ {
2937       if (fabs(xVel) > 1) {
2938         xVel = xVel*0.2;
2939         if (!o.dying) ItemWeb(o).life -= 5;
2940       } else {
2941         xVel = 0;
2942       }
2943       if (fabs(yVel) > 1) {
2944         yVel = yVel*0.2;
2945         if (!o.dying) ItemWeb(o).life -= 5;
2946       } else {
2947         yVel = 0;
2948       }
2949     }
2950   }
2951   return false;
2955 void initiateExitSequence () {
2956   writeln("exit sequence initiated...");
2957        if (global.isDamsel) setSprite('sDamselExit');
2958   else if (global.isTunnelMan) setSprite('sTunnelExit');
2959   else setSprite('sPExit');
2961   imageSpeed = 0.5;
2962   active = false;
2963   invincible = 999;
2964   depth = 999;
2966   /*k8: the following is done in `GameLevel`
2967   if (global.thiefLevel > 0) global.thiefLevel -= 1;
2968   //orig dbg:if (global.currLevel == 1) global.currLevel += firstLevelSkip; else global.currLevel += levelSkip;
2969   global.currLevel += 1;
2970   */
2971   playSound('sndSteps');
2975 void processLevelExit () {
2976   if (dead || stunned || whipping || level.playerExited) return;
2977   if (!platformCharacterIs(ON_GROUND)) return;
2978   if (isExitingSprite()) return; // just in case
2980   auto hld = holdItem;
2981   if (hld isa PlayerWeapon) return; // oops
2983   //if (!kExitPressed && !hld) return false;
2985   auto door = level.checkTileAtPoint(ix, iy, &level.cbCollisionExitTile);
2986   if (!door || !door.visible) return; // note that `invisible` doors still works
2988   // sell idol, or free damsel
2989   if (hld isa ItemGoldIdol) {
2990     //!if (isRealLevel()) global.idolsConverted += 1;
2991     //not thisglobal.money += hld.value*(global.levelType+1);
2992     ItemGoldIdol(hld).registerConverted();
2993     addScore(hld.value*(global.levelType+1));
2994     //!if (hld.sprite_index == sCrystalSkull) global.skulls += 1; else global.idols += 1;
2995     playSound('sndCoin');
2996     level.MakeMapObject(ix, iy-8, 'oBigCollect');
2997     holdItem = none;
2998     hld.instanceRemove();
2999     //!with (hld) instance_destroy();
3000     //!hld = 0;
3001     //!pickupItemType = "";
3002   } else if (hld isa MonsterDamsel) {
3003     holdItem = none;
3004     MonsterDamsel(hld).exitAtDoor(door);
3005   }
3007   if (!kExitPressed) {
3008     if (!door.invisible) {
3009       string msg = door.getExitMessage();
3010       if (msg.length == 0) {
3011         level.osdMessage(va("PRESS %s TO ENTER.", (global.config.useDoorWithButton ? "$PAY" : "$UP")), -666);
3012       } else if (msg[$-1] != '\n') {
3013         level.osdMessage(va("%s\nPRESS %s TO ENTER.", msg, (global.config.useDoorWithButton ? "$PAY" : "$UP")), -666);
3014       } else {
3015         level.osdMessage(msg, -666);
3016       }
3017     }
3018     return;
3019   }
3021   // exiting
3022   holdArrow = 0;
3023   bowArmed = false;
3025   // drop armed bomb
3026   if (isHoldingArmedBomb()) scrUseThrowItem();
3028   if (isHoldingBombOrRope()) scrSwitchToPocketItem(forceIfEmpty:true);
3030   wasHoldingBall = false;
3031   hld = holdItem;
3032   if (hld) {
3033     if (hld isa ItemGoldIdol) {
3034       //!if (isRealLevel()) global.idolsConverted += 1;
3035       //not thisglobal.money += hld.value*(global.levelType+1);
3036       ItemGoldIdol(hld).registerConverted();
3037       addScore(hld.value*(global.levelType+1));
3038       //!if (hld.sprite_index == sCrystalSkull) global.skulls += 1; else global.idols += 1;
3039       playSound('sndCoin');
3040       level.MakeMapObject(ix, iy-8, 'oBigCollect');
3041       holdItem = none;
3042       hld.instanceRemove();
3043       //!with (hld) instance_destroy();
3044       //!hld = 0;
3045       //!pickupItemType = "";
3046     } else if (hld isa MonsterDamsel) {
3047       holdItem = none;
3048       MonsterDamsel(hld).exitAtDoor(door);
3049     } else if (hld.heavy || hld isa MapEnemy || hld isa ObjCharacter) {
3050       // drop heavy items, characters and enemies (but not ball)
3051       if (hld !isa ItemBall) scrUseThrowItem();
3052     } else if (hld isa ItemBall) {
3053     } else {
3054       // other items are carried thru
3055       if (hld.cannotBeCarriedOnNextLevel) {
3056         scrUseThrowItem();
3057         holdItem = none; // just in case
3058       } else {
3059         scrHideItemToPocket();
3060       }
3061       /*
3062       global.pickupItem = hld.type;
3063       if (isAshShotgun(hld)) global.pickupItem = "Boomstick";
3064       with (hld) {
3065         breakPieces = false;
3066         instance_destroy();
3067       }
3068       */
3069       //scrHideItemToPocket();
3070     }
3071   }
3073   knockOffMonkeys();
3075   //door = instance_place(x, y, oExit); // done above
3076   door.snapToExit(self);
3078   initiateExitSequence();
3080   level.playerExitDoor = door;
3084 override bool onFellInWater (MapTile water) {
3085   level.MakeMapObject(ix, iy-8, 'oSplash');
3086   swimming = true;
3087   playSound('sndSplash');
3088   myGrav = 0.2; //k8:???
3089   return false;
3093 override bool onOutOfWater () {
3094   swimming = false;
3095   myGrav = 0.6;
3096   return false;
3100 // ////////////////////////////////////////////////////////////////////////// //
3101 override void thinkFrame () {
3102   // remove whip, etc. when dead
3103   if (dead && holdItem isa PlayerWeapon) {
3104     auto pw = holdItem;
3105     holdItem = none;
3106     pw.instanceRemove();
3107     scrSwitchToPocketItem(forceIfEmpty:false);
3108   }
3110   setPowerupState('Cape', global.hasCape);
3112   foreach (PlayerPowerup pp; powerups) if (pp.active) pp.onPreThink();
3114   // kapala
3115   if (redColor > 0) {
3116          if (redToggle) redColor -= 5;
3117     else if (redColor < 20) redColor += 5;
3118     else redToggle = true;
3119   } else {
3120     redColor = 0;
3121   }
3123   if (dead) justdied = false;
3125   if (!dead) {
3126     if (invincible > 0) --invincible;
3127   } else {
3128     invincible = 0;
3129   }
3131   if (blink > 0) {
3132     blinkHidden = !blinkHidden;
3133     --blink;
3134   } else {
3135     blinkHidden = false;
3136   }
3138   auto spr = getSprite();
3139   int x = ix, y = iy;
3141   cameraBlockX = max(0, cameraBlockX-1);
3142   cameraBlockY = max(0, cameraBlockY-1);
3144   // WHOA
3145   if (spr.Name == 'sWhoaLeft' || spr.Name == 'sDamselWhoaL' || spr.Name == 'sTunnelWhoaL') {
3146     if (whoaTimer > 0) {
3147       whoaTimer -= 1;
3148     } else if (holdItem && onLoosingHeldItem(LostCause.Whoa)) {
3149       auto hi = holdItem;
3150       holdItem = none;
3151       if (!hi.onLostAsHeldItem(self, LostCause.Whoa)) {
3152         // oops, regain it
3153         holdItem = hi;
3154       } else {
3155         scrSwitchToPocketItem(forceIfEmpty:true);
3156       }
3157     }
3158   } else {
3159     whoaTimer = whoaTimerMax;
3160   }
3162   // firing
3163   if (firing > 0) firing -= 1;
3165   // water
3166   auto wtile = level.isWaterAtPoint(x, y/*, oWaterSwim, -1, -1*/);
3167   if (wtile) {
3168     if (!swimming) {
3169       if (onFellInWater(wtile) || !isInstanceAlive) return;
3170     }
3171   } else {
3172     if (swimming) {
3173       if (onOutOfWater() || !isInstanceAlive) return;
3174     }
3175   }
3177   // burning
3178   if (burning > 0) {
3179     if (global.randOther(1, 5) == 1) level.MakeMapObject(x-8+global.randOther(4, 12), y-8+global.randOther(4, 12), 'oBurn');
3180     burning -= 1;
3181   }
3183   // lava
3184   if (!dead && level.isLavaAtPoint(x, y+6/*, oLava, 0, 0*/)) {
3185     //!if (isRealLevel()) global.miscDeaths[11] += 1;
3186     level.addDeath('lava');
3187     playSound('sndFlame');
3188     global.plife -= 99;
3189     dead = true;
3190     xVel = 0;
3191     yVel = 0.1;
3192     grav = 0;
3193     myGrav = 0;
3194     bounced = true;
3195     burning = 100;
3196     depth = 999;
3197   }
3200   // jetpack
3201   if (global.hasJetpack && platformCharacterIs(ON_GROUND)) {
3202     jetpackFuel = 50;
3203   }
3205   // fall off bottom of screen
3206   if (!level.inWinCutscene && !level.inIntroCutscene) {
3207     if (!dead && y > level.tilesHeight*16+16) {
3208       //!if (isRealLevel()) global.miscDeaths[10] += 1;
3209       level.addDeath('void');
3210       global.plife = -90; // spill blood
3211       xVel = 0;
3212       yVel = 0;
3213       grav = 0;
3214       myGrav = 0;
3215       bounced = true;
3216       scrDropItem(LostCause.Falloff);
3217       playSound('sndThud'); //???
3218       playSound('sndDie'); //???
3219     }
3221     if (dead && y > level.tilesHeight*16+16) {
3222       xVel = 0;
3223       yVel = 0;
3224       grav = 0;
3225       myGrav = 0;
3226     }
3227   }
3229   if (/*active*/true) {
3230     if (spr.Name == 'sStunL' || spr.Name == 'sDamselStunL' || spr.Name == 'sTunnelStunL') {
3231       if (stunTimer > 0) {
3232         imageSpeed = 0.4;
3233         stunTimer -= 1;
3234       }
3235       if (stunTimer < 1) {
3236         stunned = false;
3237         canDropStuff = true;
3238       }
3239     }
3241     if (!level.inWinCutscene) {
3242       if (isParachuteActive() || isCapeActiveAndOpen()) fallTimer = 0;
3243     }
3245     // changed to yVel > 1 from yVel > 0
3246     if (yVel > 1 && status != CLIMBING) {
3247       fallTimer += 1;
3248       if (fallTimer > 16) wallHurt = 0; // no sense in them taking extra damage from being thrown here
3249       int paraOpenHeight = (global.config.scumSpringShoesReduceFallDamage && (global.hasSpringShoes || global.hasJordans) ? 22 : 14);
3250       //paraOpenHeight = 4;
3251       if (global.hasParachute && !stunned && fallTimer > paraOpenHeight) {
3252         //if (not collision_point(x, y+32, oSolid, 0, 0)) // was commented in the original code
3253         //!*if (not collision_line(x, y+16, x, y+32, oSolid, 0, 0))
3254         if (!level.checkTilesInRect(x, y+16, 1, 17, &level.cbCollisionAnySolid)) {
3255           // drop parachute
3256           //!instance_create(x-8, y-16, oParachute);
3257           fallTimer = 0;
3258           global.hasParachute = false;
3259           activatePowerup('Parachute');
3260           //writeln("parachute state: ", isParachuteActive());
3261         }
3262       }
3263     } else if (fallTimer > 16 && platformCharacterIs(ON_GROUND) &&
3264                !level.checkTilesInRect(x-8, y-8, 17, 17, &level.cbCollisionSpringTrap) /* not onto springtrap */)
3265     {
3266       // long drop -- player has just landed
3267       bool reducedDamage = (global.config.scumSpringShoesReduceFallDamage && (global.hasSpringShoes || global.hasJordans));
3268       if (reducedDamage && fallTimer <= 24) {
3269         // land without taking damage
3270         fallTimer = 0;
3271       } else {
3272         stunned = true;
3273              if (fallTimer > (reducedDamage ? 72 : 48)) global.plife -= 10*global.config.scumFallDamage;
3274         else if (fallTimer > (reducedDamage ? 48 : 32)) global.plife -= 2*global.config.scumFallDamage;
3275         else global.plife -= 1*global.config.scumFallDamage;
3276         if (global.plife < 1) {
3277           if (!dead) level.addDeath('fall');
3278           spillBlood();
3279         }
3280         bounced = true;
3281         if (global.config.scumFallDamage > 0) stunTimer += 60;
3282         yVel = -3;
3283         fallTimer = 0;
3284         auto obj = level.MakeMapObject(x-4, y+6, 'oPoof');
3285         if (obj) obj.xVel = -0.4;
3286         obj = level.MakeMapObject(x+4, y+6, 'oPoof');
3287         if (obj) obj.xVel = 0.4;
3288         playSound('sndThud');
3289       }
3290     } else if (yVel <= 0) {
3291       fallTimer = 0;
3292       if (isParachuteActive()) {
3293         deactivatePowerup('Parachute');
3294         level.MakeMapObject(ix-8, iy-16-8, 'oParaUsed');
3295       }
3296     }
3298     // if (stunned) fallTimer = 0; // was commented in the original code
3300     if (swimming && !level.isLavaAtPoint(x, y/*, oLava, 0, 0*/)) {
3301       fallTimer = 0;
3302       if (bubbleTimer > 0) {
3303         bubbleTimer -= 1;
3304       } else {
3305         if (level.isWaterAtPoint(x, (y&~0x0f)-8)) level.MakeMapObject(x, y-4, 'oBubble');
3306         bubbleTimer = bubbleTimerMax;
3307       }
3308     } else {
3309       bubbleTimer = bubbleTimerMax;
3310     }
3312     //TODO: k8: move spear checking to spear handler
3313     if (!isExitingSprite()) {
3314       auto spear = MapObjectSpearsBase(level.isObjectInRect(ix-6, iy-6, 13, 14, delegate bool (MapObject o) {
3315         auto tt = MapObjectSpearsBase(o);
3316         if (!tt) return false;
3317         return tt.isHitFrame;
3318       }));
3319       if (spear) {
3320         // stunned = true;
3321         // bounced  = false;
3322         global.plife -= global.config.spearDmg; // 4
3323         if (!dead && global.plife <= 0 /*and isRealLevel()*/) level.addDeath('spear');
3324         xVel = global.randOther(4, 6)*(spear.isLeft ? -1 : 1);
3325         yVel = -6;
3326         flty -= 1;
3327         y = iy;
3328         // state = FALLING;
3329         spillBlood(); //1?
3330       }
3331     }
3333     if (status != DUCKTOHANG && !stunned && !dead && !isExitingSprite()) {
3334       bounced = false;
3335       characterStepEvent();
3336     } else {
3337       if (status != DUCKING && status != DUCKTOHANG) status = STANDING;
3338       checkControlKeys(getSprite());
3339     }
3340   }
3342   // if (dead or stunned)
3343   if (dead || stunned) {
3344     if (holdItem) {
3345       if (holdItem isa ItemWeaponBow && bowArmed) scrFireBow();
3346       scrDropItem(dead ? LostCause.Dead : LostCause.Stunned, xVel, -3);
3347     }
3349     yVel += (bounced ? 1.0 : 0.6);
3351     if (isCollisionTop(1) && yVel < 0) yVel = -yVel*0.8;
3352     if (isCollisionLeft(1) || isCollisionRight(1)) xVel = -xVel*0.5;
3354     bool collisionbottomcheck = !!isCollisionBottom(1);
3355     if (collisionbottomcheck || isCollisionBottom(1, &level.cbCollisionPlatform)) {
3356       // bounce
3357       if (collisionbottomcheck) {
3358         if (yVel > 2.5) yVel = -yVel*0.5; else yVel = 0;
3359       } else {
3360         // after falling onto a platform don't take extra damage after recovering from stunning
3361         fallTimer -= 1;
3362       }
3363       /* was commented in the original code
3364       if (isCollisionBottom(1)) {
3365         if (yVel &gt; 2.5) yVel = -yVel*0.5; else yVel = 0;
3366       } else {
3367         fallTimer -= 1;
3368       }
3369       */
3371       // friction
3372            if (fabs(xVel) < 0.1) xVel = 0;
3373       else if (fabs(xVel) != 0 && level.isIceAtPoint(x, y+16)) xVel *= 0.8;
3374       else if (fabs(xVel) != 0) xVel *= 0.3;
3376       bounced = true;
3377     }
3379     //webHit = false;
3380     //level.forEachObjectInRect(ix, iy, width, height, &doBreakWebsCB);
3382     // apply the limits since the velocity may be too extreme
3383     xVelLimit = 10;
3384     xVel = fclamp(xVel, -xVelLimit, xVelLimit);
3385     yVel = fclamp(yVel, -yVelLimit, yVelLimit);
3387     moveRel(xVel, yVel);
3388     x = ix;
3389     y = iy;
3391     // fix sprites, spawn blood from spikes
3392     if (isParachuteActive()) {
3393       deactivatePowerup('Parachute');
3394       level.MakeMapObject(ix-8, iy-16-8, 'oParaUsed');
3395     }
3397     if (whipping) {
3398       whipping = false;
3399       //!with (oWhip) instance_destroy();
3400     }
3402     if (global.isDamsel) {
3403       if (xVel == 0) {
3404              if (dead) setSprite('sDamselDieL');
3405         else if (stunned) setSprite('sDamselStunL');
3406       } else if (bounced) {
3407         if (yVel < 0) setSprite('sDamselBounceL'); else setSprite('sDamselFallL');
3408       } else {
3409         if (xVel < 0) setSprite('sDamselDieLL'); else setSprite('sDamselDieLR');
3410       }
3411     } else if (global.isTunnelMan) {
3412       if (xVel == 0) {
3413              if (dead) setSprite('sTunnelDieL');
3414         else if (stunned) setSprite('sTunnelStunL');
3415       } else if (bounced) {
3416         if (yVel < 0) setSprite('sTunnelLBounce'); else setSprite('sTunnelFallL');
3417       } else {
3418         if (xVel < 0) setSprite('sTunnelDieLL'); else setSprite('sTunnelDieLR');
3419       }
3420     } else {
3421       if (xVel == 0) {
3422              if (dead) setSprite('sDieL');
3423         else if (stunned) setSprite('sStunL');
3424       } else if (bounced) {
3425         if (yVel < 0) setSprite('sDieLBounce'); else setSprite('sDieLFall');
3426       } else {
3427         if (xVel < 0) setSprite('sDieLL'); else setSprite('sDieLR');
3428       }
3429     }
3431     x = ix;
3432     y = iy;
3434     auto colobj = isCollisionRight(1);
3435     if (!colobj) colobj = isCollisionLeft(1);
3436     if (!colobj) colobj = isCollisionBottom(1);
3437     if (colobj) {
3438       if (wallHurt > 0) {
3439         scrCreateBlood(colobj.x0, colobj.y0, 3);
3440         global.plife -= 1;
3441         if (!dead && global.plife <= 0 /*&& isRealLevel()*/) {
3442           if (thrownBy) {
3443             writeln("thrown to death by '", thrownBy, "'");
3444             level.addDeath(thrownBy);
3445           }
3446         }
3447         wallHurt -= 1;
3448         if (wallHurt <= 0) thrownBy = '';
3449         playSound('sndHurt'); //???
3450       }
3451     }
3453     colobj = isCollisionBottom(1);
3454     if (colobj && !bounced) {
3455       bounced = true;
3456       scrCreateBlood(colobj.x0, colobj.y0, 2);
3457       if (wallHurt > 0) {
3458         global.plife -= 1;
3459         if (!dead && global.plife <= 0 /*and isRealLevel()*/) {
3460           if (thrownBy) {
3461             writeln("thrown to death by '", thrownBy, "'");
3462             level.addDeath(thrownBy);
3463           }
3464         }
3465         wallHurt -= 1;
3466         if (wallHurt <= 0) thrownBy = '';
3467       }
3468     }
3469   } else {
3470     // look up and down
3471     bool kPay = level.isKeyDown(GameConfig::Key.Pay);
3472     if (kPay) {
3473       // gnounc's quick look
3474       if (!kRight && !kLeft && (platformCharacterIs(ON_GROUND) || status == HANGING)) {
3475              if (kDown) { if (viewCount <= 6) viewCount += 3; else viewOffset += 6; }
3476         else if (kUp) { if (viewCount <= 6) viewCount += 3; else viewOffset -= 6; }
3477         else viewCount = 0;
3478       } else {
3479         viewCount = 0;
3480       }
3481     } else {
3482       // default look up/down with delay if pay button not held
3483       if (!kRight && !kLeft && (platformCharacterIs(ON_GROUND) || status == HANGING)) {
3484              if (kDown) { if (viewCount <= 30) viewCount += 1; else viewOffset += 4; }
3485         else if (kUp) { if (viewCount <= 30) viewCount += 1; else viewOffset -= 4; }
3486         else viewCount = 0;
3487       } else {
3488         viewCount = 0;
3489       }
3490     }
3491   }
3492   if (viewCount == 0 && viewOffset) viewOffset = (viewOffset < 0 ? min(0, viewOffset+8) : max(0, viewOffset-8));
3493   viewOffset = clamp(viewOffset, -16*6, 16*6);
3495   if (!dead) activatePlayerWeapon();
3497   if (!dead) processLevelExit();
3499   // hurt too much
3500   if (global.plife < -99 && visible && justdied) spillBlood();
3502   if (global.plife < 1) {
3503     dead = true;
3504   }
3506   // spikes, and other shit
3507   if (global.plife >= -99 && visible && !isExitingSprite()) {
3508     auto colSpikes = level.checkTilesInRect(x-4, y-4, 9, 13, &level.cbCollisionSpikes);
3510     if (colSpikes && dead) {
3511       grav = 0;
3512       if (!level.isSolidAtPoint(x, y+9)) { shiftY(0.02); y = iy; } //0.05;
3513       //else myGrav = 0.6;
3514     } else {
3515       myGrav = 0.6;
3516     }
3518     if (colSpikes && yVel > 0 && (fallTimer > 3 || stunned)) { // originally fallTimer &gt; 4
3519       if (!dead) {
3520         spillBlood();
3521         // spikes will always instant-kill in Moon room
3522         /*if (isRoom("rMoon")) global.plife -= 99; else*/ global.plife -= global.config.scumSpikeDamage;
3523         if (/*isRealLevel() &&*/ global.plife <= 0) level.addDeath('spike');
3524         if (global.plife > 0) playSound('sndHurt');
3525         xVel = 0;
3526         yVel = 0;
3527         myGrav = 0;
3528       }
3529       colSpikes.makeBloody();
3530     }
3531     //else if (not dead) myGrav = 0.6;
3532   }
3535   // sacrifice
3536   if (visible && (status >= STUNNED || stunned || dead || status == DUCKING)) {
3537     bool onAltar;
3538     checkAndPerformSacrifice(out onAltar);
3539     // block looking down if we're trying to sacrifire ourselves
3540     if (onAltar) viewCount = max(0, viewCount-1);
3541   } else {
3542     sacCount = default.sacCount;
3543   }
3545   // activate ankh
3546   if (dead && global.hasAnkh) {
3547     writeln("*** ACTIVATED ANKH");
3548     global.hasAnkh = false;
3549     dead = false;
3550     int newLife = (global.isTunnelMan ? global.config.scumTMLife : global.config.scumStartLife);
3551     global.plife = max(global.plife, newLife);
3552     level.osdMessage("THE ANKH SHATTERS!\nYOU HAVE BEEN REVIVED!", 4);
3553     // find moai
3554     auto moai = level.forEachTile(delegate bool (MapTile t) { return (t.objType == 'oMoai'); });
3555     if (moai) {
3556       level.forEachTile(delegate bool (MapTile t) {
3557         if (t.objType == 'oMoaiInside') {
3558           teleportTo(t.ix+8, t.iy+8);
3559           t.instanceRemove();
3560         }
3561         return false;
3562       });
3563       //teleportTo(moai.ix+16+8, moai.iy+16+8);
3564     } else {
3565       if (level.allEnters.length) {
3566         teleportTo(level.allEnters[0].ix+8, level.allEnters[0].iy-8);
3567       }
3568     }
3569     level.centerViewAtPlayer();
3570     auto ball = getMyBall();
3571     if (ball) ball.teleportToPrisoner();
3572     //k8:???depth = 50;
3573     xVel = 0;
3574     yVel = 0;
3575     blink = 60;
3576     invincible = 60;
3577     fallTimer = 0;
3578     visible = true;
3579     active = true;
3580     dead = false;
3581     stunned = false;
3582     status = STANDING;
3583     burning = 0;
3584     //alarm[8] = 60; // this starts music; but we don't need it, 'cause we won't stop the music on player death
3585     playSound('sndTeleport');
3586   }
3589   if (dead) level.stats.gameOver();
3591   // step end
3592   if (status == DUCKTOHANG) {
3593     spr = getSprite();
3594     if (spr.Name != 'sDuckToHangL' && spr.Name != 'sDamselDtHL' && spr.Name != 'sTunnelDtHL') status = STANDING;
3595   }
3597   foreach (PlayerPowerup pp; powerups) if (pp.active) pp.onPostThink();
3599   if (jetpackFlaresTime > 0) {
3600     if (--jetpackFlaresTime == 0) {
3601       auto obj = level.MakeMapObject(ix+global.randOther(0, 3)-global.randOther(0, 3), iy+global.randOther(0, 3)-global.randOther(0, 3), 'oFlareSpark');
3602       if (obj) {
3603         obj.yVel = global.randOther(1, 3);
3604         obj.xVel = global.randOther(0, 3)-global.randOther(0, 3);
3605       }
3606       playSound('sndJetpack');
3607     }
3608   }
3612 // ////////////////////////////////////////////////////////////////////////// //
3613 void drawPrePrePowerupWithOfs (int xpos, int ypos, int scale, float currFrameDelta) {
3614   // so ducking player will have it's cape correctly rendered
3615   foreach (PlayerPowerup pp; powerups) {
3616     if (pp.active) pp.prePreDrawWithOfs(xpos, ypos, scale, currFrameDelta);
3617   }
3621 override void drawWithOfs (int xpos, int ypos, int scale, float currFrameDelta) {
3622   //if (heldBy) return; // owner will take care of this
3623   if (blinkHidden) return;
3625   bool renderJetpackBack = false;
3626   if (global.hasJetpack) {
3627     // render jetpack
3628     if ((status == CLIMBING || isExitingSprite()) && !whipping) {
3629       // later
3630       renderJetpackBack = true;
3631     } else {
3632       int xi, yi;
3633       getInterpCoords(currFrameDelta, scale, out xi, out yi);
3634       yi -= 1;
3635       SpriteImage spr;
3636       if (dir == Dir.Right) {
3637         spr = level.sprStore['sJetpackRight'];
3638         xi -= 4;
3639       } else {
3640         spr = level.sprStore['sJetpackLeft'];
3641         xi += 4;
3642       }
3643       if (spr) {
3644         auto spf = spr.frames[0];
3645         if (spf && spf.width > 0 && spf.height > 0) spf.tex.blitAt(xi-xpos-spf.xofs*scale, yi-ypos-spf.yofs*scale, scale);
3646       }
3647     }
3648   }
3650   bool ducking = (status == DUCKING);
3651   foreach (PlayerPowerup pp; powerups) {
3652     if (pp.active) pp.preDrawWithOfs(xpos, ypos, scale, currFrameDelta);
3653   }
3655   auto oldColor = Video.color;
3656   if (redColor > 0) Video.color = clamp(200+redColor, 0, 255)<<16;
3657   ::drawWithOfs(xpos, ypos, scale, currFrameDelta);
3658   Video.color = oldColor;
3660   if (renderJetpackBack) {
3661     int xi, yi;
3662     getInterpCoords(currFrameDelta, scale, out xi, out yi);
3663     SpriteImage spr = level.sprStore['sJetpackBack'];
3664     if (spr) {
3665       auto spf = spr.frames[0];
3666       if (spf && spf.width > 0 && spf.height > 0) spf.tex.blitAt(xi-xpos-spf.xofs*scale, yi-ypos-spf.yofs*scale, scale);
3667     }
3668   }
3670   foreach (PlayerPowerup pp; powerups) if (pp.active) pp.postDrawWithOfs(xpos, ypos, scale, currFrameDelta);
3674 void lastDrawWithOfs (int xpos, int ypos, int scale, float currFrameDelta) {
3675   foreach (PlayerPowerup pp; powerups) {
3676     if (pp.active) pp.lastDrawWithOfs(xpos, ypos, scale, currFrameDelta);
3677   }
3681 defaultproperties {
3682   objName = 'Player';
3683   objType = 'oPlayer';
3685   desc = "Spelunker";
3686   desc2 = "A strange little man who spends his time exploring caverns. He wants to be just like Indiana Jones when he grows up.";
3688   negateMirrorXOfs = true;
3690   status = FALLING; // the character state, must be one of the following: STANDING, RUNNING, DUCKING, LOOKING_UP, CLIMBING, JUMPING, or FALLING
3692   bloodless = false;
3694   stunned = false;
3695   bounced = false;
3697   fallTimer = 0;
3698   stunTimer = 0;
3699   wallHurt = 0;
3700   //thrownBy = ""; // "Yeti", "Hawkman", or "Shopkeeper" for stat tracking deaths by being thrown
3701   pushTimer = 0;
3702   whoaTimer = 0;
3703   //whoaTimerMax = 30;
3704   distToNearestLightSource = 999;
3706   sacCount = 60;
3708   flying = false;
3709   myGrav = 0.6;
3710   myGravNorm = 0.6;
3711   myGravWater = 0.2;
3712   yVelLimit = 10;
3713   bounceFactor = 0.5;
3714   frictionFactor = 0.3;
3716   xVelLimit = 16; // limits the xVel: default 15
3717   yVelLimit = 10; // limits the yVel
3718   xAccLimit = 9;  // limits the xAcc
3719   yAccLimit = 6;  // limits the yAcc
3720   runAcc = 3;     // the running acceleration
3722   grav = 1;
3724   bloodLeft = 999999;
3726   depth = 5;
3727   //lightRadius = 96; //???