made dead items list non-transient, 'cause game can be saved inter-frame
[k8vacspelynky.git] / PlayerPawn.vc
blob45e85be066245ea1a509351a3a095e3202dd1c22
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
69 int greenColor;
70 bool greenToggle;
71 //!!!global.poisonStrength = max(global.poisonStrength-0.5, 1);
73 //string holdItemType = "";
74 //string pickupItemType = "";
76 // this is what we had picked up
77 // picked item will be stored here by bomb/rope/item switcher
78 MapObject pickedItem;
80 bool canDropStuff = true;
81 bool kItemPressed;
82 //bool kItemReleased;
83 bool kRopePressed;
84 bool kBombPressed;
85 bool kPayPressed;
86 //bool kRope;
87 //bool kBomb;
88 //bool kPay;
90 int holdArrow;
91 bool holdArrowToggle;
92 int bombArrowCounter = 80;
94 int hangCount;
95 int runHeld;
97 // other
98 int blink;
99 bool blinkHidden;
101 // the keys that the platform character will use (don't edit)
102 bool kLeft;
103 bool kLeftPressed;
104 bool kLeftReleased;
105 bool kRight;
106 bool kRightPressed;
107 bool kRightReleased;
108 bool kUp;
109 bool kDown;
110 bool kJump;
111 bool kJumpPressed;
112 bool kJumpReleased;
113 bool jumpButtonReleased; // whether the jump button was released. (Stops the user from pressing the jump button many times to get extra jumps)
114 bool kAttack;
115 bool kAttackPressed;
116 bool kAttackReleased;
118 const float gravNorm = 1;
119 //float grav = 1; // the gravity
121 const float initialJumpAcc = -2; // relates to how high the character will jump
122 const int jumpTimeTotal = 10;  // how long the user must hold the jump button to get the maximum jump height
124 float climbAcc = 0.6; // how fast the character will climb
125 float climbAnimSpeed = 0.4; // relates to how fast the climbing animation should go
126 int climbSndSpeed = 8;
127 int climbSoundTimer;
128 bool climbSndToggle;
131 const float departLadderXVel = 4;  // how fast character should be moving horizontally when he leaves the ladder
132 const float departLadderYVel = -4; // how fast the character should be moving vertically when he leaves the ladder
134 const float frictionRunningX = 0.6;      // friction obtained while running
135 const float frictionRunningFastX = 0.98; // friction obtained while holding the shift button for some time while running
136 const float frictionClimbingX = 0.6;     // friction obtained while climbing
137 const float frictionClimbingY = 0.6;     // friction obtained while climbing
138 const float frictionDuckingX = 0.8;      // friction obtained while ducking
139 const float frictionFlyingX = 0.99;      // friction obtained while "flying"
141 const float runAnimSpeed = 0.1; // relates to the how fast the running animation should go
143 // hidden variables (don't edit)
144 protected int statePrev;
145 protected int statePrevPrev;
146 protected float gravityIntensity = grav; // this variable describes the current force due to gravity (this variable is altered for variable jumping)
147 protected float jumpTime = jumpTimeTotal; // current time of the jump (0=start of jump, jumpTimeTotal=end of jump)
148 protected int ladderTimer; // relates to whether the character can climb a ladder
149 protected int kLeftPushedSteps;
150 protected int kRightPushedSteps;
152 transient protected bool skipCutscenePressed;
155 //int score;
157 //PlayerWeapon actWeapon; // active weapon object
160 enum {
161   ARROW_NORM = 1,
162   ARROW_BOMB = 2,
165 int viewOffset;
166 int viewCount;
167 int lookOff0;
169 //int pExit0;
173 enum {
174   xSTART,
175   xTUTORIAL,
176   xSCORES,
177   xTITLE,
178   xEND,
179   xSHORTCUT5,
180   xSHORTCUT9,
181   xSUN,
182   xMOON,
183   xSTARS,
184   xCHANGE,
185   xSHORTCUT13,
186   xCHANGE2,
187   xLAVA1,
188   xTEST,
189   xBIZARRE,
190   xTEST2,
191   xDEBUG,
192   xMODE,
193   xTUTORIAL2,
198 // ////////////////////////////////////////////////////////////////////////// //
199 bool mustBeChained;
200 bool wasHoldingBall;
202 ItemBall myBall;
204 final ItemBall getMyBall () {
205   ItemBall res = myBall;
206   if (res && !res.isInstanceAlive) { res = none; myBall = none; }
207   return res;
211 void spawnBallAndChain () {
212   mustBeChained = true;
213   auto ball = getMyBall();
214   if (!ball) {
215     ball = ItemBall(level.MakeMapObject(ix, iy, 'oBall'));
216     if (ball) ball.attachTo(self);
217   }
218   if (ball) {
219     if (wasHoldingBall) {
220       if (pickedItem) {
221         pickedItem.instanceRemove();
222         pickedItem = none;
223       }
224       if (holdItem && holdItem != ball) {
225         holdItem.instanceRemove();
226         holdItem = none;
227       }
228       holdItem = ball;
229     }
230   } else {
231     mustBeChained = false;
232   }
233   wasHoldingBall = false;
237 void removeBallAndChain (optional bool temp) {
238   auto ball = getMyBall();
239   if (ball) {
240     wasHoldingBall = (holdItem == ball);
241     mustBeChained = true;
242     ball.attachTo(none);
243     ball.instanceRemove();
244   }
245   if (specified_temp && temp) return;
246   wasHoldingBall = false;
247   mustBeChained = false;
251 // ////////////////////////////////////////////////////////////////////////// //
252 final PlayerPowerup findPowerup (name id) {
253   foreach (PlayerPowerup pp; powerups) if (pp.id == id) return pp;
254   return none;
258 final bool setPowerupState (name id, bool active) {
259   auto pp = findPowerup(id);
260   if (!pp) return false;
261   return (active ? pp.onActivate() : pp.onDeactivate());
265 final bool togglePowerupState (name id) {
266   auto pp = findPowerup(id);
267   if (!pp) return false;
268   return (pp.active ? pp.onDeactivate() : pp.onActivate());
272 final bool activatePowerup (name id) { return setPowerupState(id, true); }
273 final bool deactivatePowerup (name id) { return setPowerupState(id, false); }
276 final bool isActivePowerup (name id) {
277   auto pp = findPowerup(id);
278   return (pp && pp.active);
282 // ////////////////////////////////////////////////////////////////////////// //
283 override void Destroy () {
284   foreach (PlayerPowerup pp; powerups) delete pp;
285   powerups.length = 0;
289 void unpressAllKeys () {
290   kLeft = false;
291   kLeftPressed = false;
292   kLeftReleased = false;
293   kRight = false;
294   kRightPressed = false;
295   kRightReleased = false;
296   kUp = false;
297   kDown = false;
298   kJump = false;
299   kJumpPressed = false;
300   kJumpReleased = false;
301   kAttack = false;
302   kAttackPressed = false;
303   kAttackReleased = false;
304   kItemPressed = false;
305   kRopePressed = false;
306   kBombPressed = false;
307   kPayPressed = false;
308   kExitPressed = false;
312 // ////////////////////////////////////////////////////////////////////////// //
313 // called on level start too
314 void resurrect () {
315   justSpawned = true;
316   holdArrow = 0;
317   bowStrength = 0;
318   bowArmed = false;
319   skipCutscenePressed = false;
320   movementBlocked = false;
321   if (global.plife < 1) global.plife = max(1, global.config.scumStartLife);
322   dead = false;
323   xVel = 0;
324   yVel = 0;
325   grav = default.grav;
326   myGrav = default.myGrav;
327   bounced = false;
328   stunned = false;
329   burning = 0;
330   depth = default.depth;
331   status = default.status;
332   fallTimer = 0;
333   stunTimer = 0;
334   wallHurt = 0;
335   pushTimer = 0;
336   whoaTimer = 0;
337   distToNearestLightSource = 999;
338   flying = false;
339   justdied = default.justdied;
340   whipping = false;
341   if (holdItem isa PlayerWeapon) {
342     auto w = holdItem;
343     holdItem = none;
344     w.instanceRemove();
345   }
346   invincible = 0;
347   blink = default.blink;
348   blinkHidden = default.blinkHidden;
349   status = STANDING;
350   characterSprite();
351   active = true;
352   visible = true;
353   unpressAllKeys();
354   level.clearKeysPressRelease();
355   climbSoundTimer = 0;
356   bet = 0;
357   //scrSwitchToPocketItem(forceIfEmpty:false);
361 // ////////////////////////////////////////////////////////////////////////// //
362 bool isExitingSprite () {
363   auto spr = getSprite();
364   return (spr.Name == 'sPExit' || spr.Name == 'sDamselExit' || spr.Name == 'sTunnelExit');
368 // ////////////////////////////////////////////////////////////////////////// //
369 override void playSound (name aname, optional bool unique) {
370   if (unique && global.sndIsPlaying(aname)) return;
371   global.playSound(xCenter, yCenter, aname, relative:true);
375 override bool sndIsPlaying (name aname) {
376   return global.sndIsPlaying(aname, relative:true);
380 override void sndStopSound (name aname) {
381   global.sndStopSound(aname, relative:true);
385 // ////////////////////////////////////////////////////////////////////////// //
386 transient ItemDice currDie;
388 void onDieRolled (ItemDice die) {
389   if (!die.forSale) return;
390   // only law-abiding players can play
391   if (global.thiefLevel > 0 || global.murderer) return;
392   if (bet == 0) return;
393   auto odie = currDie;
394   currDie = die;
395   level.forEachObject(delegate bool (MapObject o) {
396     MonsterShopkeeper sc = MonsterShopkeeper(o);
397     if (sc && !sc.dead && !sc.angered) return sc.onDiePlayed(self, currDie);
398     return false;
399   });
400   currDie = odie;
404 // ////////////////////////////////////////////////////////////////////////// //
405 override bool onExplosionTouch (MapObject xplo) {
406   //writeln("PlayerPawn: on explo touch! ", invincible);
407   if (invincible) return false;
408   if (global.config.scumExplosionHurt) {
409     global.plife -= global.config.explosionDmg;
410     if (!dead && global.plife <= 0 /*&& isRealLevel()*/) level.addDeath('explosion');
411     burning = 50;
412     if (global.config.scumExplosionStun) {
413       stunned = true;
414       stunTimer = 100;
415     }
416     scrCreateBlood(ix, iy, 1);
417   }
418   if (xplo.ix < ix) xVel = global.randOther(4, 6); else xVel = -global.randOther(4, 6);
419   yVel = -6;
420   return true;
424 // ////////////////////////////////////////////////////////////////////////// //
425 // start new game when exiting from title, and process other custom exits
426 void scrPlayerExit () {
427   level.playerExited = true;
428   status = STANDING;
429   characterSprite();
433 // ////////////////////////////////////////////////////////////////////////// //
434 bool scrHideItemToPocket (optional bool forBombOrRope) {
435   if (!holdItem) return true;
436   if (holdItem isa PlayerWeapon) return false;
437   if (holdItem.forSale) return false;
438   if (!forBombOrRope) {
439     if (holdItem isa ItemBall) return false;
440   }
442   // cannot hide armed bomb
443   ItemBomb bomb = ItemBomb(holdItem);
444   if (bomb && bomb.armed) return false;
445   if (bomb || holdItem isa ItemRopeThrow) {
446     holdItem.instanceRemove();
447     holdItem = none;
448     return true;
449   }
451   // cannot hide enemy
452   if (holdItem isa MapEnemy) return false;
453   //writeln("hiding: '", GetClassName(holdItem.Class), "'");
455   if (pickedItem) FatalError("we are already holding '%n'", GetClassName(pickedItem.Class));
456   pickedItem = holdItem;
457   holdItem = none;
458   pickedItem.active = false;
459   pickedItem.visible = false;
460   if (pickedItem.heldBy) FatalError("oooops (scrHideItemToPocket)");
461   return true;
465 bool scrSwitchToBombs () {
466   if (holdItem isa PlayerWeapon) return false;
468   if (global.bombs < 1) return false;
469   if (ItemBomb(holdItem)) return true;
470   if (!scrHideItemToPocket(forBombOrRope:true)) return false;
472   ItemBomb bomb = ItemBomb(level.MakeMapObject(ix, iy, 'oBomb'));
473   if (!bomb) return false;
474   bomb.setSticky(global.stickyBombsActive);
475   holdItem = bomb;
476   whoaTimer = whoaTimerMax;
477   return true;
481 bool scrSwitchToStickyBombs () {
482   if (holdItem isa PlayerWeapon) return false;
483   if (!global.hasStickyBombs) {
484     global.stickyBombsActive = false;
485     return false;
486   }
488   global.stickyBombsActive = !global.stickyBombsActive;
489   return true;
493 bool scrSwitchToRopes () {
494   if (holdItem isa PlayerWeapon) return false;
496   if (global.rope < 1) return false;
497   if (ItemRopeThrow(holdItem)) return true;
498   if (!scrHideItemToPocket(forBombOrRope:true)) return false;
500   ItemRopeThrow rope = ItemRopeThrow(level.MakeMapObject(ix, iy, 'oRopeThrow'));
501   if (!rope) return false;
502   holdItem = rope;
503   whoaTimer = whoaTimerMax;
504   return true;
508 bool isHoldingBombOrRope () {
509   auto hit = holdItem;
510   if (!hit) return false;
511   return (hit isa ItemBomb || hit isa ItemRopeThrow);
515 bool isHoldingBomb () {
516   auto hit = holdItem;
517   if (!hit) return false;
518   return (hit isa ItemBomb);
522 bool isHoldingArmedBomb () {
523   auto hit = ItemBomb(holdItem);
524   if (!hit) return false;
525   return hit.armed;
529 bool isHoldingRope () {
530   auto hit = holdItem;
531   if (!hit) return false;
532   return (hit isa ItemRopeThrow);
536 bool scrSwitchToPocketItem (bool forceIfEmpty) {
537   if (holdItem isa PlayerWeapon) return false;
538   if (holdItem && holdItem.forSale) return false;
540   if (holdItem == pickedItem) { pickedItem = none; whoaTimer = whoaTimerMax; return true; }
542   if (!forceIfEmpty && !pickedItem) return false;
544   // destroy currently holded item if it is a bomb or a rope
545   if (holdItem) {
546     // you cannot do it with an armed bomb
547     if (holdItem isa MapEnemy) return false; // cannot hide an enemy
548     ItemBomb bomb = ItemBomb(holdItem);
549     if (bomb && bomb.armed) return false;
550     if (bomb || holdItem isa ItemRopeThrow) {
551       //delete holdItem;
552       holdItem.instanceRemove();
553       holdItem = none;
554     } /*else {
555       if (pickedItem) {
556         writeln(va("cannot switch to pocket item while carrying '%n' ('%n' is in pocket, why?)", GetClassName(holdItem.Class), GetClassName(pickedItem.Class)));
557         return false;
558       }
559     }*/
560   }
562   auto oldHold = holdItem;
563   holdItem = pickedItem;
564   pickedItem = oldHold;
565   // all flag management is done in property handler
566   if (oldHold) {
567     oldHold.active = false;
568     oldHold.visible = false;
569   }
570   whoaTimer = whoaTimerMax;
571   return true;
575 bool scrSwitchToNextItem () {
576   if (holdItem isa PlayerWeapon) return false;
577   if (holdItem && holdItem.forSale) return false;
579   // holding a bomb?
580   if (ItemBomb(holdItem)) {
581     if (ItemBomb(holdItem).armed) return false; // cannot switch out of armed bomb
582     if (scrSwitchToRopes()) return true;
583     return scrSwitchToPocketItem(forceIfEmpty:true);
584   }
586   // holding a rope?
587   if (ItemRopeThrow(holdItem)) {
588     if (scrSwitchToPocketItem(forceIfEmpty:true)) return true;
589     if (scrSwitchToBombs()) return true;
590     return scrHideItemToPocket();
591   }
593   // either nothing, or normal item
594   bool tryPocket = !!holdItem;
595   if (scrSwitchToBombs()) return true;
596   if (scrSwitchToRopes()) return true;
597   if (holdItem isa ItemBall) return false;
598   if (tryPocket) return scrSwitchToPocketItem(forceIfEmpty:true);
599   return false;
603 // ////////////////////////////////////////////////////////////////////////// //
604 bool scrPickupItem (MapObject obj) {
605   if (holdItem isa PlayerWeapon) return false;
607   if (!obj) return false;
609   if (holdItem) {
610     if (pickedItem) return false;
611     if (isHoldingArmedBomb()) return false;
612     if (isHoldingBombOrRope()) {
613       if (!scrSwitchToPocketItem(forceIfEmpty:true)) return false;
614     }
615     if (holdItem) return false;
616   } else {
617     // just in case
618     if (pickedItem) return false;
619   }
621        if (obj isa ItemBomb && !ItemBomb(obj).armed) ++global.bombs;
622   else if (obj isa ItemRopeThrow) ++global.rope;
623   holdItem = obj;
624   whoaTimer = whoaTimerMax;
625   obj.onPickedUp(self);
626   return true;
630 // drop currently held item
631 bool scrDropItem (LostCause cause, optional float xVel, optional float yVel) {
632   if (holdItem isa PlayerWeapon) return false;
634   if (!holdItem) return false;
636   if (!onLoosingHeldItem(cause)) return false;
638   auto hi = holdItem;
639   holdItem = none;
641   if (!hi.onLostAsHeldItem(self, cause, xVel!optional, yVel!optional)) {
642     // oops, regain it
643     holdItem = hi;
644     return false;
645   }
647        if (hi isa ItemRopeThrow) global.rope = max(0, global.rope-1);
648   else if (hi isa ItemBomb && !ItemBomb(hi).armed) global.bombs = max(0, global.bombs-1);
650   madeOffer = false;
652   scrSwitchToPocketItem(forceIfEmpty:true);
653   return true;
657 // ////////////////////////////////////////////////////////////////////////// //
658 void scrUseThrowIt (MapObject it) {
659   if (!it) return;
661   it.onBeforeThrowBy(self);
663   it.resaleValue = 0;
664   it.makeSafe();
666   if (dir == Dir.Left) {
667     it.xVel = (it.heavy ? -4+xVel : -8+xVel);
668     //foreach (; 0..8) if (level.isSolidAtPoint(ix-8, iy)) it.shiftX(1);
669     //while (!level.isSolidAtPoint(ix-8, iy)) it.shiftX(1); // prevent getting stuck in wall
670   } else if (dir == Dir.Right) {
671     it.xVel = (it.heavy ? 4+xVel : 8+xVel);
672     //foreach (; 0..8) if (level.isSolidAtPoint(ix+8, iy)) it.shiftX(-1);
673     //while (!level.isSolidAtPoint(ix+8, iy)) it.shiftX(-1); // prevent getting stuck in wall
674   }
675   it.yVel = (it.heavy ? (kUp ? -4 : -2) : (kUp ? -9 : -3));
676   if (kDown || scrPlayerIsDucking()) {
677     if (platformCharacterIs(ON_GROUND)) {
678       it.shiftY(-2);
679       it.xVel *= 0.6;
680       it.yVel = 0.5;
681     } else {
682       it.yVel = 3;
683     }
684   } else if (!global.hasMitt) {
685     if (dir == Dir.Left) {
686       if (level.isSolidAtPoint(ix-8, iy-10)) {
687         it.yVel = 0;
688         it.xVel -= 1;
689       }
690     } else if (dir == Dir.Right) {
691       if (level.isSolidAtPoint(ix+8, iy-10)) {
692         it.yVel = 0;
693         it.xVel += 1;
694       }
695     }
696   }
698   if (global.hasMitt && !scrPlayerIsDucking()) {
699     it.xVel += (it.xVel < 0 ? -6 : 6);
700          if (!kUp && !kDown) it.yVel = -0.4;
701     else if (kDown) it.yVel = 6;
702     it.myGrav = 0.1;
703   }
705   // prevent getting stuck in a wall
706   if (it.isCollision()) {
707     //foreach (; 0..8) if (level.isSolidAtPoint(ix-8, iy)) it.shiftX(1);
708     if (it.xVel < 0) {
709       if (level.isSolidAtPoint(it.ix-8, it.iy)) it.shiftX(8);
710     } else if (it.xVel > 0) {
711       if (level.isSolidAtPoint(it.ix+8, it.iy)) it.shiftX(-8);
712     } else if (it.isCollision()) {
713       int dx = (it.isCollisionLeft(0) ? 1 : it.isCollisionRight(0) ? -1 : 0);
714       if (dx) {
715         foreach (; 0..8) {
716           it.shiftX(dx);
717           if (!it.isCollision()) break;
718         }
719       }
720     }
721     /*
722     int dx = 1;
723     while (dx > 8 && it.isCollisionLeft(dx)) ++dx;
724     if (dx < 8) it.shiftX(8);
725     else {
726       dx = 1;
727       while (dx > 8 && it.isCollisionRight(dx)) ++dx;
728       if (dx < 8) it.shiftX(-8);
729     }
730     */
731   }
733   /*
734   if (it.sprite_index == sBombBag ||
735       it.sprite_index == sBombBox ||
736       it.sprite_index == sRopePile)
737   {
738       // do nothing
739   } else*/ {
740     playSound('sndThrow');
741   }
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) return false;
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.levelKind == GameLevel::LevelKind.Transition) {
1381     unpressAllKeys();
1383     if (checkSkipCutScene()) {
1384       level.playerExited = true;
1385       return true;
1386     }
1388     auto door = level.checkTileAtPoint(ix, iy, &level.cbCollisionExitTile);
1389     if (door) {
1390       kExitPressed = true;
1391       return true;
1392     }
1394     if (status == STOPPED) {
1395       if (--transKissTimer > 0) return true;
1396       status = STANDING;
1397     }
1399     transKissTimer = 0;
1400     auto dms = MonsterDamselKiss(level.isObjectAtPoint(ix+8, iy+4, delegate bool (MapObject o) { return (o isa MonsterDamselKiss); }));
1401     if (dms && !dms.kissed) {
1402       status = STOPPED;
1403       xVel = 0;
1404       yVel = 0;
1405       dms.kiss();
1406       transKissTimer = 30;
1407       return true;
1408     }
1410     kRight = true;
1411     kRightPressed = true;
1412     return true;
1413   }
1414   return false;
1418 // ////////////////////////////////////////////////////////////////////////// //
1419 private final void checkControlKeys (SpriteImage spr) {
1420   if (forcePlayerControls()) {
1421     if (movementBlocked) unpressAllKeys();
1422     if (kLeft) kLeftPushedSteps += 1; else kLeftPushedSteps = 0;
1423     if (kRight) kRightPushedSteps += 1; else kRightPushedSteps = 0;
1424     return;
1425   }
1427   kLeft = level.isKeyDown(GameConfig::Key.Left);
1428   if (movementBlocked) kLeft = false;
1429   if (kLeft) kLeftPushedSteps += 1; else kLeftPushedSteps = 0;
1430   kLeftPressed = level.isKeyPressed(GameConfig::Key.Left);
1431   kLeftReleased = level.isKeyReleased(GameConfig::Key.Left);
1433   kRight = level.isKeyDown(GameConfig::Key.Right);
1434   if (movementBlocked) kRight = false;
1435   if (kRight) kRightPushedSteps += 1; else kRightPushedSteps = 0;
1436   kRightPressed = level.isKeyPressed(GameConfig::Key.Right);
1437   kRightReleased = level.isKeyReleased(GameConfig::Key.Right);
1439   kUp = level.isKeyDown(GameConfig::Key.Up);
1440   kDown = level.isKeyDown(GameConfig::Key.Down);
1442   kJump = level.isKeyDown(GameConfig::Key.Jump);
1443   kJumpPressed = level.isKeyPressed(GameConfig::Key.Jump);
1444   kJumpReleased = level.isKeyReleased(GameConfig::Key.Jump);
1446   if (movementBlocked) unpressAllKeys();
1448   if (cantJump > 0) {
1449     kJump = false;
1450     kJumpPressed = false;
1451     kJumpReleased = false;
1452     --cantJump;
1453   } else if (spr && global.isTunnelMan && spr.Name == 'sTunnelAttackL' && !holdItem) {
1454     kJump = false;
1455     kJumpPressed = false;
1456     kJumpReleased = false;
1457     cantJump = max(0, cantJump-1);
1458   }
1460   kAttack = level.isKeyDown(GameConfig::Key.Attack);
1461   kAttackPressed = level.isKeyPressed(GameConfig::Key.Attack);
1462   kAttackReleased = level.isKeyReleased(GameConfig::Key.Attack);
1464   kItemPressed = level.isKeyPressed(GameConfig::Key.Switch);
1465   kRopePressed = level.isKeyPressed(GameConfig::Key.Rope);
1466   kBombPressed = level.isKeyPressed(GameConfig::Key.Bomb);
1468   kPayPressed = level.isKeyPressed(GameConfig::Key.Pay);
1470   if (movementBlocked) unpressAllKeys();
1472   kExitPressed = false;
1473   if (global.config.useDoorWithButton) {
1474     if (kPayPressed) kExitPressed = true;
1475   } else {
1476     if (kUp) kExitPressed = true;
1477   }
1479   if (stunned || dead) {
1480     unpressAllKeys();
1481     //level.clearKeysPressRelease();
1482   }
1486 // ////////////////////////////////////////////////////////////////////////// //
1487 // knock off monkeys that grabbed you
1488 void knockOffMonkeys () {
1489   level.forEachObject(delegate bool (MapObject o) {
1490     auto mk = EnemyMonkey(o);
1491     if (mk && !mk.dead && mk.status == GRAB) {
1492       mk.xVel = global.randOther(0, 1)-global.randOther(0, 1);
1493       mk.yVel = -4;
1494       mk.status = BOUNCE;
1495       mk.vineCounter = 20;
1496       mk.grabCounter = 60;
1497     }
1498     return false;
1499   });
1503 // ////////////////////////////////////////////////////////////////////////// //
1504 // fix collision with boulder (bug with non-aligned boulder)
1505 void hackBoulderCollision () {
1506   auto bld = level.checkTilesInRect(x0, y0, width, height, delegate bool (MapTile o) { return (o isa ObjBoulder); });
1507   if (bld && fabs(bld.xVel) <= 1) {
1508     writeln("IN BOULDER!");
1509     if (x0 < bld.x0) {
1510       int dx = bld.x0-x0;
1511       writeln("  LEFT: dx=", dx);
1512       if (dx <= 2) fltx = x0-dx;
1513     } else if (x1 > bld.x1) {
1514       int dx = x1-bld.x1;
1515       writeln("  RIGHT: dx=", dx);
1516       if (dx <= 2) fltx = x1-dx;
1517     }
1518   }
1522 // ////////////////////////////////////////////////////////////////////////// //
1523 bool checkHangTileDG (MapTile t) { return (t.solid || t.tree); }
1526 void checkPerformHang (bool colLeft, bool colRight) {
1527   if (status == HANGING || platformCharacterIs(ON_GROUND)) return;
1528   if ((kLeft && kRight) || (!kLeft && !kRight)) return;
1529   if (kLeft && !colLeft) {
1530 #ifdef HANG_DEBUG
1531     writeln("checkPerformHang: no left solid");
1532 #endif
1533     return;
1534   }
1535   if (kRight && !colRight) {
1536 #ifdef HANG_DEBUG
1537     writeln("checkPerformHang: no right solid");
1538 #endif
1539     return;
1540   }
1541   if (hangCount != 0) {
1542 #ifdef HANG_DEBUG
1543     writeln("checkPerformHang: hangCount=", hangCount);
1544 #endif
1545     return;
1546   }
1547   if (iy <= 16) return;
1548   int dx = (kLeft ? -9 : 9);
1549 #ifdef HANG_DEBUG
1550   writeln("checkPerformHang: trying to hang at ", dx);
1551 #endif
1553   bool doHang = false;
1555   if (global.hasGloves) {
1556     doHang = (yVel > 0 && !!level.checkTilesInRect(ix+dx, iy-6, 1, 2, &checkHangTileDG));
1557   } else {
1558     // hang on tree?
1559     doHang = !!level.checkTilesInRect(ix+dx, iy-6, 1, 2, &level.cbCollisionAnyTree);
1560 #ifdef HANG_DEBUG
1561     writeln("  tree: ", doHang);
1562 #endif
1563     // hang on solid?
1564     if (!doHang) {
1565       doHang = level.checkTilesInRect(ix+dx, iy-6, 1, 2) &&
1566                !level.isSolidAtPoint(ix+dx, iy-9) && !level.isSolidAtPoint(ix, iy+9);
1567 #ifdef HANG_DEBUG
1568       writeln("  solid: ", doHang);
1569 #endif
1570     }
1571     if (!doHang) {
1572 #ifdef HANG_DEBUG
1573       writeln("    solid at dx, -6(1): ", !!level.checkTilesInRect(ix+dx, iy-6, 1, 2));
1574       writeln("    solid at dx, -9(0): ", !!level.isSolidAtPoint(ix+dx, iy-9));
1575       writeln("    solid at  0, +9(0): ", !!level.isSolidAtPoint(ix, iy-9));
1576 #endif
1577 #ifdef EASIER_HANG
1578       doHang = level.checkTilesInRect(ix+dx, iy-6, 1, 2) &&
1579                !level.isSolidAtPoint(ix+dx, iy-10) && !level.isSolidAtPoint(ix, iy+9);
1580 #ifdef HANG_DEBUG
1581       if (!doHang) writeln("    easier hang failed");
1582 #endif
1583       /*
1584       if (!level.isSolidAtPoint(ix, iy-9)) {
1585         foreach (int dy; 6..24) {
1586           writeln("    solid at dx:-", dy, "(0): ", !!level.isSolidAtPoint(ix+dx, iy-dy));
1587         }
1588         writeln("   ix=", ix, "; iy=", iy);
1589       }
1590       */
1591 #endif
1592     }
1593   }
1595   if (doHang) {
1596     status = HANGING;
1597     moveSnap(1, 8);
1598     yVel = 0;
1599     yAcc = 0;
1600     grav = 0;
1601   }
1605 // ////////////////////////////////////////////////////////////////////////// //
1606 final void characterStepEvent () {
1607   if (climbSoundTimer > 0) {
1608     if (--climbSoundTimer == 0) {
1609       playSound(climbSndToggle ? 'sndClimb2' : 'sndClimb1');
1610       climbSndToggle = !climbSndToggle;
1611     }
1612   }
1614   auto spr = getSprite();
1615   checkControlKeys(spr);
1617   float xPrev = fltx, yPrev = flty;
1618   int x = ix, y = iy;
1620   // check collisions in various directions
1621   bool colSolidLeft = !!getPushableLeft(1);
1622   bool colSolidRight = !!getPushableRight(1);
1623   bool colLeft = !!isCollisionLeft(1);
1624   bool colRight = !!isCollisionRight(1);
1625   bool colTop = !!isCollisionTop(1);
1626   bool colBot = !!isCollisionBottom(1);
1627   bool colLadder = !!isCollisionLadder();
1628   bool colPlatBot = !!isCollisionBottom(1, &level.cbCollisionPlatform);
1629   bool colPlat = !!isCollision(&level.cbCollisionPlatform);
1630   bool colWaterTop = !!isCollisionTop(1, &level.cbCollisionWater);
1631   bool colIceBot = !!level.isIceAtPoint(x, y+8);
1633   bool runKey = false;
1634   if (level.isKeyDown(GameConfig::Key.Run)) { runHeld = 100; runKey = true; }
1635   if (level.isKeyDown(GameConfig::Key.Attack) && !whipping) { runHeld += 1; runKey = true; }
1636   if (!runKey || (!kLeft && !kRight)) runHeld = 0;
1638   // allows the character to run left and right
1639   // if state!=DUCKING and state!=LOOKING_UP and state!=CLIMBING
1640   if (status != CLIMBING && status != HANGING) {
1641     if (kLeftReleased && fabs(xVel) < 0.0001) xAcc -= 0.5;
1642     if (kRightReleased && fabs(xVel) < 0.0001) xAcc += 0.5;
1643     if (kLeft && !kRight) {
1644       if (colSolidLeft) {
1645         //xVel = 3; // in orig
1646         if (platformCharacterIs(ON_GROUND) && status != DUCKING) {
1647           xAcc -= 1;
1648           pushTimer += 10;
1649           //playSound('sndPush', unique:true);
1650         }
1651       } else if (kLeftPushedSteps > 2 && (dir == Dir.Left || fabs(xVel) < 0.0001)) {
1652         xAcc -= runAcc;
1653       }
1654       dir = Dir.Left;
1655       //if (platformCharacterIs(ON_GROUND) && fabs(xVel) > 0 && alarm[3] < 1) alarm[3] = floor(16/-xVel);
1656     }
1657     if (kRight && !kLeft) {
1658       if (colSolidRight) {
1659         //xVel = 3; // in orig
1660         if (platformCharacterIs(ON_GROUND) && status != DUCKING) {
1661           xAcc += 1;
1662           pushTimer += 10;
1663           //playSound('sndPush', unique:true);
1664         }
1665       } else if ((kRightPushedSteps > 2 || colSolidLeft) && (dir == Dir.Right || fabs(xVel) < 0.0001)) {
1666         xAcc += runAcc;
1667       }
1668       dir = Dir.Right;
1669       //if (platformCharacterIs(ON_GROUND) && fabs(xVel) > 0 && alarm[3] < 1) alarm[3] = floor(16/xVel);
1670     }
1671   }
1673   // ladders
1674   if (status == CLIMBING) {
1675     closeCape();
1676     kJumped = false;
1677     ladderTimer = 10;
1678     auto ladder = level.isLadderAtPoint(x, y);
1679     if (ladder) { x = ladder.ix+8; setX(x); }
1680     if (kLeft) dir = Dir.Left; else if (kRight) dir = Dir.Right;
1681     if (kUp) {
1682       // checks both ladder and laddertop
1683       if (level.isAnyLadderAtPoint(x, y-8)) {
1684         //writeln("LADDER00! old yAcc=", yAcc, "; climbAcc=", climbAcc, "; new yAcc=", yAcc-climbAcc);
1685         yAcc -= climbAcc;
1686         if (climbSoundTimer < 1) climbSoundTimer = climbSndSpeed;
1687         //!if (alarm[2] < 1) alarm[2] = climbSndSpeed;
1688       } else {
1689         /*
1690         for (int dy = -6; dy > -12; --dy) {
1691           ladder = level.isAnyLadderAtPoint(x, y+dy);
1692           if (ladder) {
1693             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));
1694           }
1695         }
1696         */
1697         /*
1698         auto grid = level.miscTileGrid;
1699         foreach (MapTile t; grid.inCellPix(48, 96, grid.nextTag(), precise:false)) {
1700           writeln("at 48, 96: ", GetClassName(t.Class), "; pos=(", t.ix, ",", t.iy, ")");
1701         }
1702         foreach (MapTile t; grid.inCellPix(48, 94, grid.nextTag(), precise:false)) {
1703           writeln("at 48, 94: ", GetClassName(t.Class), "; pos=(", t.ix, ",", t.iy, ")");
1704         }
1705         foreach (int dy; 90..102) {
1706           ladder = level.isAnyLadderAtPoint(48, dy);
1707           if (ladder) {
1708             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));
1709           }
1710         }
1711         */
1712       }
1713     } else if (kDown) {
1714       // checks both ladder and laddertop
1715       if (level.isAnyLadderAtPoint(x, y+8)) {
1716         yAcc += climbAcc;
1717         //!if (alarm[2] < 1) alarm[2] = climbSndSpeed;
1718         if (climbSoundTimer < 1) climbSoundTimer = climbSndSpeed;
1719       } else {
1720         status = FALLING;
1721       }
1722       if (colBot) status = STANDING;
1723     }
1724     // jump from ladder
1725     if (kJumpPressed && !whipping) {
1726       if (kLeft) xVel = -departLadderXVel; else if (kRight) xVel = departLadderXVel; else xVel = 0;
1727       //yAcc += departLadderYVel;
1728       //k8: was `0.6`, but with `0.4` we can jump onto the wall above, and with `0.6` we cannot
1729       yAcc = 0.4+departLadderYVel; // YASM 1.8.1 Fix for extra air when jumping off ladders due to increased climb speed option
1730       status = JUMPING;
1731       jumpButtonReleased = false;
1732       jumpTime = 0;
1733       ladderTimer = 5;
1734     }
1735   } else {
1736     if (ladderTimer > 0) ladderTimer -= 1;
1737   }
1739   if (platformCharacterIs(IN_AIR) && status != HANGING) yAcc += gravityIntensity;
1741   // player has landed
1742   if ((colBot || colPlatBot) && platformCharacterIs(IN_AIR) && yVel >= 0) {
1743     if (!colPlat || colBot) {
1744       yVel = 0;
1745       yAcc = 0;
1746       status = RUNNING;
1747     }
1748     playSound('sndLand');
1749   }
1750   if ((colBot || colPlatBot) && !colPlat) yVel = 0;
1752   // player has just walked off of the edge of a solid
1753   if (colBot == 0 && (!colPlatBot || colPlat) && platformCharacterIs(ON_GROUND)) {
1754     status = FALLING;
1755     yAcc += grav;
1756     kJumped = true;
1757     if (global.hasGloves) hangCount = 5;
1758   }
1760   if (colTop) {
1761          if (dead || stunned) yVel = -yVel*0.8;
1762     else if (status == JUMPING) yVel = fabs(yVel*0.3);
1763   }
1765   if ((colLeft && dir == Dir.Left) || (colRight && dir == Dir.Right)) {
1766     if (dead || stunned) xVel = -xVel*0.5; else xVel = 0;
1767   }
1769   // jumping
1770   if (kJumpReleased && platformCharacterIs(IN_AIR)) {
1771     kJumped = true;
1772   } else if (platformCharacterIs(ON_GROUND)) {
1773     closeCape();
1774     kJumped = false;
1775   }
1777   MapObject oWeb = none, oBlob = none;
1778   if (kJumpPressed) {
1779     oWeb = level.isObjectAtPoint(x, y, &level.cbIsObjectWeb);
1780     if (!oWeb) oBlob = level.isObjectAtPoint(x, y, &level.cbIsObjectBlob);
1781   }
1783   bool invokeJumpHelper = false;
1785   if (kJumpPressed && oWeb) {
1786     ItemWeb(oWeb).tear(1);
1787     yAcc += initialJumpAcc*2;
1788     yVel -= 3;
1789     xAcc += xVel/2;
1791     status = JUMPING;
1792     jumpButtonReleased = false;
1793     jumpTime = 0;
1795     grav = gravNorm;
1796     invokeJumpHelper = true;
1797   } else if (kJumpPressed && oBlob) {
1798     oBlob.hp -= 5;
1799     scrCreateBloblets(oBlob.x0+8, oBlob.y0+8, 1);
1800     playSound('sndHit');
1801     yAcc += initialJumpAcc*2;
1802     yVel -= 2;
1803     xAcc += xVel/2;
1804     status = JUMPING;
1805     jumpButtonReleased = false; // k8: was `jumpButtonRelease`
1806     jumpTime = 0;
1807     invokeJumpHelper = true;
1808   } else if (kJumpPressed && colWaterTop) {
1809     yAcc += initialJumpAcc*2;
1810     yVel -= 3;
1811     xAcc += xVel/2;
1813     status = JUMPING;
1814     jumpButtonReleased = false;
1815     jumpTime = 0;
1817     grav = gravNorm;
1818     invokeJumpHelper = true;
1819   } else if (global.hasCape && kJumpPressed && kJumped && platformCharacterIs(IN_AIR)) {
1820     switchCape();
1821   } else if (global.hasJetpack && !swimming && kJump && kJumped && platformCharacterIs(IN_AIR) && jetpackFuel > 0) {
1822     yAcc += initialJumpAcc;
1823     yVel = -1;
1824     jetpackFuel -= 1;
1825     if (jetpackFlaresTime < 1) jetpackFlaresTime = 3;
1826     //!if (alarm[10] < 1) alarm[10] = 3; // jetpack flares
1827     fallTimer = 0;
1829     status = JUMPING;
1830     jumpButtonReleased = false;
1831     jumpTime = 0;
1833     grav = 0;
1834     invokeJumpHelper = true;
1835   } else if (platformCharacterIs(ON_GROUND) && kJumpPressed && fallTimer == 0) {
1836     if (fabs(xVel) > 3 /*xVel > 3 || xVel < -3*/) {
1837       yAcc += initialJumpAcc*2;
1838       xAcc += xVel*2;
1839     } else {
1840       yAcc += initialJumpAcc*2;
1841       xAcc += xVel/2;
1842       //scrJumpHelper(); // move to location where player doesn't have to be on ground
1843     }
1844     if (global.hasJordans) {
1845       yAcc *= 3;
1846       yAccLimit = 12;
1847       grav = 0.5;
1848     } else if (global.hasSpringShoes) {
1849       yAcc *= 1.5;
1850     } else {
1851       yAccLimit = 6;
1852       grav = gravNorm;
1853     }
1855     playSound('sndJump');
1857     pushTimer = 0;
1859     // the "state" gets changed to JUMPING later on in the code
1860     status = FALLING;
1861     // "variable jumping" states
1862     jumpButtonReleased = false;
1863     jumpTime = 0;
1864     invokeJumpHelper = true;
1865   }
1867   if (kJumpPressed && invokeJumpHelper) scrJumpHelper(); // YASM 1.8.1
1869   if (jumpTime < jumpTimeTotal) jumpTime += 1;
1870   // let the character continue to jump
1871   if (!kJump) jumpButtonReleased = true;
1872   if (jumpButtonReleased) jumpTime = jumpTimeTotal;
1874   gravityIntensity = (jumpTime/jumpTimeTotal)*grav;
1876   if (kUp && platformCharacterIs(ON_GROUND) && !colLadder) {
1877     //k8:!!!looking = UP;
1878     if (xVel == 0 && xAcc == 0) status = LOOKING_UP;
1879   } else {
1880     //k8:!!!looking = 0;
1881   }
1883   if (!kUp && status == LOOKING_UP) status = STANDING;
1885   // hanging
1886   if (!colTop) {
1887     checkPerformHang(colLeft, colRight);
1888     x = ix;
1889     y = iy;
1891     // hang on stuck arrow
1892     if (status == FALLING && hangCount == 0 && y > 16 && !platformCharacterIs(ON_GROUND) &&
1893         !level.isSolidAtPoint(x, y+12) && // from Spelunky Natural
1894         !level.isObjectAtPoint(x, y-9, &level.cbIsObjectArrow) && !level.isObjectAtPoint(x, y+9, &level.cbIsObjectArrow))
1895     {
1896       //obj = instance_nearest(x, y-5, oArrow);
1897       auto arr0 = level.isObjectAtPoint(x, y-5, &level.cbIsObjectArrow);
1898       auto arr1 = level.isObjectAtPoint(x, y-6, &level.cbIsObjectArrow);
1899       if (arr0 || arr1) {
1900         writeln("ARROW HANG!");
1901         // get nearest arrow
1902         MapObject arr;
1903         if (arr1 && arr0) {
1904           arr = (arr0.distanceToPoint(x, y-5) < arr1.distanceToPoint(x, y-5) ? arr0 : arr1);
1905         } else {
1906           arr = (arr0 ? arr0 : arr1);
1907         }
1908         if (arr.stuck) {
1909           status = HANGING;
1910           // move_snap(1, 8); // was commented out in the original
1911           yVel = 0;
1912           yAcc = 0;
1913           grav = 0;
1914         }
1915       }
1916     }
1917     /* this was commented in the original
1918     if (hangCount == 0 && y > 16 && !platformCharacterIs(ON_GROUND) && state == FALLING &&
1919         (collision_point(x, y-5, oTreeBranch, 0, 0) || collision_point(x, y-6, oTreeBranch, 0, 0)) &&
1920         !collision_point(x, y-9, oTreeBranch, 0, 0) && !collision_point(x, y+9, oTreeBranch, 0, 0))
1921     {
1922       state = HANGING;
1923       // move_snap(1, 8); // was commented out in the original
1924       yVel = 0;
1925       yAcc = 0;
1926       grav = 0;
1927     }
1928     */
1929   }
1931   if (hangCount > 0) --hangCount;
1933   if (status == HANGING) {
1934     closeCape();
1935     kJumped = false;
1936     if (kJumpPressed) {
1937       if (kDown) {
1938         if (global.hasGloves) {
1939           if (hangCount == 0 && y > 16 && !platformCharacterIs(ON_GROUND)) {
1940             if (kRight && colRight &&
1941                 (level.isSolidAtPoint(x+9, y-5) || level.isSolidAtPoint(x+9, y-6)))
1942             {
1943               grav = gravNorm;
1944               status = FALLING;
1945               yAcc -= grav;
1946               hangCount = 10;
1947             } else if (kLeft && colLeft &&
1948                        (level.isSolidAtPoint(x-9, y-5) || level.isSolidAtPoint(x-9, y-6)))
1949             {
1950               grav = gravNorm;
1951               status = FALLING;
1952               yAcc -= grav;
1953               hangCount = 10;
1954             } else {
1955               grav = gravNorm;
1956               status = FALLING;
1957               yAcc -= grav;
1958               hangCount = 5;
1959             }
1960           }
1961         } else {
1962           grav = gravNorm;
1963           status = FALLING;
1964           yAcc -= grav;
1965           hangCount = 5;
1966         }
1967       } else {
1968         grav = gravNorm;
1969         status = JUMPING;
1970         yAcc += initialJumpAcc*2;
1971         shiftX(dir == Dir.Right ? -2 : 2);
1972         x = ix;
1973         cameraBlockX = 3;
1974         hangCount = hangCountMax;
1975         if (level.isObjectAtPoint(x, y-5, &level.cbIsObjectArrow) || level.isObjectAtPoint(x, y-6, &level.cbIsObjectArrow)) hangCount /= 2; //Spelunky Natural
1976       }
1977     }
1978     if ((dir == Dir.Left && !isCollisionLeft(2)) ||
1979         (dir == Dir.Right && !isCollisionRight(2)))
1980     {
1981       grav = gravNorm;
1982       status = FALLING;
1983       yAcc -= grav;
1984       hangCount = 4;
1985     }
1986   } else {
1987     grav = gravNorm;
1988   }
1990   // pressing down while standing
1991   if (kDown && platformCharacterIs(ON_GROUND) && !whipping) {
1992     if (colBot) {
1993       status = DUCKING;
1994     } else if (colPlatBot) {
1995       // climb down ladder if possible, else jump down
1996       fallTimer = 0;
1997       if (!colBot) {
1998         //ladder = instance_place(x, y+16, oLadder);
2000         // from Spelunky Natural
2001         /*
2002         ladder = collision_line(x-4, y+16, x+4, y+16, oLadder, 0, 0);
2003         if (!ladder) ladder = collision_line(x-4, y+16, x+4, y+16, oLadderTop, 0, 0);
2004         */
2005         auto ladder = level.checkTilesInRect(x-4, y+16, 9, 1, &level.cbCollisionAnyLadder);
2006         //writeln("DOWN; cpb=", colPlatBot, "; cb=", colBot, "; ladder=", !!ladder);
2008         if (ladder) {
2009           if (abs(x-(ladder.x0+8)) < 4) {
2010             x = ladder.ix+8;
2011             setX(x);
2012             xVel = 0;
2013             yVel = 0;
2014             xAcc = 0;
2015             yAcc = 0;
2016             status = CLIMBING;
2017           }
2018         } else {
2019           shiftY(1);
2020           y = iy;
2021           status = FALLING;
2022           yAcc += grav;
2023           kJumped = true; // Spelunky Natural
2024         }
2025       }
2026       else {
2027         // the character can't move down because there is a solid in the way
2028         status = RUNNING;
2029       }
2030     }
2031   }
2032   if (!kDown && status == DUCKING) {
2033     status = STANDING;
2034     xVel = 0;
2035     xAcc = 0;
2036   }
2037   if (xVel == 0 && xAcc == 0 && status == RUNNING) status = STANDING;
2038   if (xAcc != 0 && status == STANDING) status = RUNNING;
2039   if (yVel < 0 && platformCharacterIs(IN_AIR) && status != HANGING) status = JUMPING;
2040   if (yVel > 0 && platformCharacterIs(IN_AIR) && status != HANGING) {
2041     status = FALLING;
2042     setCollisionBounds(-5, -6, 5, 8);
2043   } else {
2044     setCollisionBounds(-5, -6, 5, 8);
2045   }
2047   // CLIMB LADDER
2048   bool colPointLadder = !!level.isAnyLadderAtPoint(x, y);
2050   /* this was commented in the original
2051   if ((kUp && platformCharacterIs(IN_AIR) && collision_point(x, y-8, oLadder, 0, 0) && ladderTimer == 0) ||
2052       (kUp && colPointLadder && ladderTimer == 0) ||
2053       (kDown && colPointLadder && ladderTimer == 0 && platformCharacterIs(ON_GROUND) && collision_point(x, y+9, oLadderTop, 0, 0) && xVel == 0))
2054   {
2055     ladder = 0;
2056     ladder = instance_place(x, y-8, oLadder);
2057     if (instance_exists(ladder)) {
2058       if (abs(x-(ladder.x0+8)) < 4) {
2059         x = ladder.ix+8;
2060         setX(x);
2061         if (!collision_point(x, y, oLadder, 0, 0) && !collision_point(x, y, oLadderTop, 0, 0)) { y = ladder.iy+14; setY(y); }
2062         xVel = 0;
2063         yVel = 0;
2064         xAcc = 0;
2065         yAcc = 0;
2066         state = CLIMBING;
2067       }
2068     }
2069   }*/
2071   // Spelunky Natural - Multiple changes to this big "if" condition
2072   if ((kUp && platformCharacterIs(IN_AIR) && ladderTimer == 0 && level.checkTilesInRect(x-2, y-8, 5, 1, &level.cbCollisionLadder)) ||
2073       (kUp && colPointLadder && ladderTimer == 0) ||
2074       (kDown && colPointLadder && ladderTimer == 0 && platformCharacterIs(ON_GROUND) && xVel == 0 && level.isLadderTopAtPoint(x, y+9)) ||
2075       ((kUp || kDown) && status == HANGING && level.checkTilesInRect(x-2, y, 5, 1, &level.cbCollisionLadder)))
2076   {
2077     //ladder = 0;
2078     //auto ladder = instance_place(x, y-8, oLadder);
2079     auto ladder = level.isLadderAtPoint(x, y-8);
2080     if (ladder) {
2081       //writeln("LADDER01! plrx=", x, "; ladder.x0=", ladder.x0, "; ladder.ix=", ladder.ix, "; ladder class=", GetClassName(ladder.Class));
2082       if (abs(x-(ladder.x0+8)) < 4) {
2083         x = ladder.ix+8;
2084         setX(x);
2085         if (!level.isAnyLadderAtPoint(x, y)) { y = ladder.y0+14; setY(y); }
2086         xVel = 0;
2087         yVel = 0;
2088         xAcc = 0;
2089         yAcc = 0;
2090         status = CLIMBING;
2091       }
2092     }
2093   }
2095   /* this was commented in the original
2096   if (sprite_index == sDuckToHangL || sprite_index == sDamselDtHL) {
2097     ladder = 0;
2098     if (facing == LEFT && collision_rectangle(x-8, y, x, y+16, oLadder, 0, 0) && !collision_point(x-4, y+16, oSolid, 0, 0)) {
2099       ladder = instance_nearest(x-4, y+16, oLadder);
2100     } else if (facing == RIGHT && collision_rectangle(x, y, x+8, y+16, oLadder, 0, 0) && !collision_point(x+4, y+16, oSolid, 0, 0)) {
2101       ladder = instance_nearest(x+4, y+16, oLadder);
2102     }
2103     if (ladder) {
2104       x = ladder.ix+8;
2105       setX(x);
2106       xVel = 0;
2107       yVel = 0;
2108       xAcc = 0;
2109       yAcc = 0;
2110       state = CLIMBING;
2111     }
2112   }
2114   if (colLadder && state == CLIMBING && kJumpPressed && !whipping) {
2115     if (kLeft) xVel = -departLadderXVel; else if (kRight) xVel = departLadderXVel; else xVel = 0;
2116     yAcc += departLadderYVel;
2117     state = JUMPING;
2118     jumpButtonReleased = false;
2119     jumpTime = 0;
2120     ladderTimer = 5;
2121   }
2122   */
2124   // calculate horizontal/vertical friction
2125   if (status == CLIMBING) {
2126     xFric = frictionClimbingX;
2127     yFric = frictionClimbingY;
2128   } else {
2129     //if (runKey && platformCharacterIs(ON_GROUND) && runHeld >= 10)
2130     if ((runKey && runHeld >= 10) && (platformCharacterIs(ON_GROUND) || global.config.toggleRunAnywhere)) {
2131       // YASM 1.8.1
2132       if (kLeft) {
2133         // run
2134         xVel -= 0.1;
2135         xVelLimit = 6;
2136         xFric = frictionRunningFastX;
2137       } else if (kRight) {
2138         xVel += 0.1;
2139         xVelLimit = 6;
2140         xFric = frictionRunningFastX;
2141       }
2142     } else if (status == DUCKING) {
2143       if (xVel < 2 && xVel > -2) {
2144         xFric = 0.2;
2145         xVelLimit = 3;
2146         imageSpeed = 0.8;
2147       } else if (kLeft && global.config.downToRun) {
2148         // run
2149         xVel -= 0.1;
2150         xVelLimit = 6;
2151         xFric = frictionRunningFastX;
2152       } else if (kRight && global.config.downToRun) {
2153         xVel += 0.1;
2154         xVelLimit = 6;
2155         xFric = frictionRunningFastX;
2156       } else {
2157         xVel *= 0.8;
2158         if (xVel < 0.5) xVel = 0;
2159         xFric = 0.2;
2160         xVelLimit = 3;
2161         imageSpeed = 0.8;
2162       }
2163     } else {
2164       // decrease the friction when the character is "flying"
2165       if (platformCharacterIs(IN_AIR)) {
2166         if (dead || stunned) xFric = 1.0; else xFric = 0.8;
2167       } else {
2168         xFric = frictionRunningX;
2169       }
2170     }
2172     /* // ORIGINAL RUN/WALK xVel/xFric code  this was commented in the original
2173     if (runKey && platformCharacterIs(ON_GROUND) && runHeld >= 10) {
2174       if (kLeft) {
2175         // run
2176         xVel -= 0.1;
2177         xVelLimit = 6;
2178         xFric = frictionRunningFastX;
2179       } else if (kRight) {
2180         xVel += 0.1;
2181         xVelLimit = 6;
2182         xFric = frictionRunningFastX;
2183       }
2184     } else if (state == DUCKING) {
2185       if (xVel < 2 && xVel > -2) {
2186         xFric = 0.2
2187         xVelLimit = 3;
2188         imageSpeed = 0.8;
2189       } else if (kLeft && global.downToRun) {
2190         // run
2191         xVel -= 0.1;
2192         xVelLimit = 6;
2193         xFric = frictionRunningFastX;
2194       } else if (kRight && global.downToRun) {
2195         xVel += 0.1;
2196         xVelLimit = 6;
2197         xFric = frictionRunningFastX;
2198       } else {
2199         xVel *= 0.8;
2200         if (xVel < 0.5) xVel = 0;
2201         xFric = 0.2
2202         xVelLimit = 3;
2203         imageSpeed = 0.8;
2204       }
2205     } else {
2206       // decrease the friction when the character is "flying"
2207       if (platformCharacterIs(IN_AIR)) {
2208         if (dead || stunned) xFric = 1.0; else xFric = 0.8;
2209       } else {
2210         xFric = frictionRunningX;
2211       }
2212     }
2213     */
2215     // stuck on web or underwater
2216     if (level.isObjectAtPoint(x, y, &level.cbIsObjectWeb)) {
2217       xFric = 0.2;
2218       yFric = 0.2;
2219       fallTimer = 0;
2220     } else if (level.isObjectAtPoint(x, y, &level.cbIsObjectBlob)) {
2221       // blob enemy
2222       //obj = instance_place(x, y, oBlob); this was commented in the original
2223       //xVel += obj.xVel; this was commented in the original
2224       xFric = 0.1;
2225       yFric = 0.3;
2226       fallTimer = 0;
2227     } else if (level.isWaterAtPoint(x, y/*, oWater, -1, -1*/)) {
2228       closeCape();
2229       //if (!runKey && global.toggleRunAnywhere) xFric = frictionRunningX; // YASM 1.8.1 this was commented in the original
2230       if (!platformCharacterIs(ON_GROUND)) xFric = frictionRunningX;
2231       if (status == FALLING && yVel > 0) {
2232         // Spelunky Natural
2233              if (global.config.naturalSwim && kUp) yFric = 0.2;
2234         else if (global.config.naturalSwim && kDown) yFric = 0.8;
2235         else yFric = 0.5;
2236       } else if (!level.isWaterAtPoint(x, y-9/*, oWater, -1, -1*/)) {
2237         yFric = 1;
2238       } else {
2239         yFric = 0.9;
2240       }
2241       if (yVel < -6 && global.config.noDolphin) {
2242         // Spelunky Natural (changed from -4 to -6)
2243         yVel = -6;
2244       }
2245     } else {
2246       swimming = false;
2247       yFric = 1;
2248     }
2249   }
2251   if (colIceBot && status != DUCKING && !global.hasSpikeShoes) {
2252     xFric = 0.98;
2253     yFric = 1;
2254   }
2256   // YASM 1.8.1
2257   if (global.config.toggleRunAnywhere) {
2258     if (!kJump && !kDown && !runKey) xVelLimit = 3;
2259   }
2261   // RUNNING
2262   if (platformCharacterIs(ON_GROUND)) {
2263          if (status == RUNNING && kLeft && colLeft) pushTimer += 1;
2264     else if (status == RUNNING && kRight && colRight) pushTimer += 1;
2265     else pushTimer = 0;
2267     //if (platformCharacterIs(ON_GROUND) && !kJump && !kDown && !runKey) this was commented in the original
2268     if (!kJump && !kDown && !runKey) xVelLimit = 3;
2270     /* this was commented in the original
2271     // ledge flip
2272     if (state == DUCKING && fabs(xVel) < 3 && facing == LEFT &&
2273         //collision_point(x, y+9, oSolid, 0, 0) && !collision_point(x-1, y+9, oSolid, 0, 0) && kLeft)
2274         collision_point(x, y+9, oSolid, 0, 0) && !collision_line(x-1, y+9, x-10, y+9, oSolid, 0, 0) && kLeft)
2275     */
2277     // ledge flip
2278     int dhdir = 0;
2279          if (kLeft && dir == Dir.Left) dhdir = -1;
2280     else if (kRight && dir == Dir.Right) dhdir = 1;
2282     if (dhdir && status == DUCKING && fabs(xVel) < 3+(dhdir < 0 ? 1 : 0) &&
2283         level.isSolidAtPoint(x, y+9) && !level.checkTilesInRect(x+(dhdir < 0 ? -8 : 1), y+9, 8, 8))
2284     {
2285       status = DUCKTOHANG;
2286       if (holdItem) {
2287         if (!global.config.scumFlipHold || holdItem.heavy) {
2288           /*
2289           holdItem.heldBy = none;
2290           if (holdItem.objName == 'GoldIdol') holdItem.shiftY(-8);
2291           */
2292           //else if (holdItem.type == "Block Item") { with (oBlockPreview) instance_destroy(); }
2293           scrDropItem(LostCause.Hang, (dir == Dir.Left ? -1 : 1), -4);
2294         }
2295       }
2296       knockOffMonkeys();
2297     }
2298   }
2300   if (status == DUCKTOHANG) {
2301     setXY(xPrev, yPrev);
2302     x = ix;
2303     y = iy;
2304     xVel = 0;
2305     yVel = 0;
2306     xAcc = 0;
2307     yAcc = 0;
2308     grav = 0;
2309   }
2311   // parachute and cape
2312   if (!level.inWinCutscene) {
2313     if (isParachuteActive() || isCapeActiveAndOpen()) yFric = 0.5;
2314   }
2316   if (pushTimer > 100) pushTimer = 100;
2318   // limits the acceleration if it is too extreme
2319   xAcc = fclamp(xAcc, -xAccLimit, xAccLimit);
2320   yAcc = fclamp(yAcc, -yAccLimit, yAccLimit);
2322   // applies the acceleration
2323   xVel += xAcc;
2324   if (dead || stunned) yVel += 0.6; else yVel += yAcc;
2326   // nullifies the acceleration
2327   xAcc = 0;
2328   yAcc = 0;
2330   // applies the friction to the velocity, now that the velocity has been calculated
2331   xVel *= xFric;
2332   yVel *= yFric;
2334   auto oBall = getMyBall();
2335   // apply ball and chain
2336   if (oBall) {
2337     int distsq = (ix-oBall.ix)*(ix-oBall.ix)+(iy-oBall.iy)*(iy-oBall.iy);
2338     if (distsq >= 24*24) {
2339       if (xVel > 0 && oBall.ix < ix && abs(oBall.ix-ix) > 24) xVel = 0;
2340       if (xVel < 0 && oBall.ix > ix && abs(oBall.ix-ix) > 24) xVel = 0;
2341       if (yVel > 0 && oBall.iy < iy && abs(oBall.iy-iy) > 24) {
2342         if (abs(oBall.ix-ix) < 1) {
2343           //teleportTo(destx:oBall.ix);
2344           fltx = oBall.fltx;
2345           prevFltX = oBall.prevFltX;
2346           x = ix;
2347         } else if (oBall.ix < ix && !kRight) {
2348                if (xVel > 0) xVel *= -0.25;
2349           else if (xVel == 0) xVel -= 1;
2350         } else if (oBall.ix > ix && !kLeft) {
2351                if (xVel < 0) xVel *= -0.25;
2352           else if (xVel == 0) xVel += 1;
2353         }
2354         yVel = 0;
2355         fallTimer = 0;
2356       }
2357       if (yVel < 0 && oBall.iy > iy && abs(oBall.iy-iy) > 24) yVel = 0;
2358     }
2359   }
2361   // apply the limits since the velocity may be too extreme
2362   if (!dead && !stunned) xVel = fclamp(xVel, -xVelLimit, xVelLimit);
2363   yVel = fclamp(yVel, -yVelLimit, yVelLimit);
2365   // approximates the "active" variables
2366   if (fabs(xVel) < 0.0001) xVel = 0;
2367   if (fabs(yVel) < 0.0001) yVel = 0;
2368   if (fabs(xAcc) < 0.0001) xAcc = 0;
2369   if (fabs(yAcc) < 0.0001) yAcc = 0;
2371   bool wasInWall = !!isCollision();
2372   moveRel(xVel, yVel);
2374   // don't go out of level (if we're not in ending sequence)
2375   if (!level.inWinCutscene) {
2376          if (ix < 0) fltx = 0;
2377     else if (ix > level.tilesWidth*16-16) fltx = level.tilesWidth*16-16;
2378     if (iy < 0) flty = 0;
2380     if (!dead) hackBoulderCollision();
2382     if (!wasInWall && isCollision()) {
2383       writeln("** FUUUU (XXX)");
2384       if (isCollisionBottom(0) && !isCollisionBottom(-2)) {
2385         flty = iy-2;
2386       }
2387       // we can stuck in the wall with this
2388       if (isCollisionLeft(0)) {
2389         writeln("** FUUUU (001: left)");
2390         while (isCollisionLeft(0) && !isCollisionRight(1)) shiftX(1);
2391       } else if (isCollisionRight(0)) {
2392         writeln("** FUUUU (001: right)");
2393         while (isCollisionRight(0) && !isCollisionLeft(1)) shiftX(-1);
2394       }
2395     }
2397     if (!dead) hackBoulderCollision();
2399     // move out of wall by 1 px, if possible
2400     if (!dead && isCollision()) {
2401       if (isCollisionBottom(0) && !isCollisionBottom(-1)) flty = iy-1;
2402       if (isCollisionTop(0) && !isCollisionTop(1)) flty = iy+1;
2403       if (isCollisionLeft(0) && !isCollisionLeft(1)) fltx = ix+1;
2404       if (isCollisionRight(0) && !isCollisionRight(-1)) fltx = ix-1;
2405     }
2407     if (!dead && isCollision()) {
2408       //k8:HACK: try to duck
2409       bool wallDeath = true;
2410       if (platformCharacterIs(ON_GROUND)) {
2411         setCollisionBounds(-5, -6, 5, 8);
2412         wallDeath = !!isCollision();
2413         if (wallDeath) {
2414           setCollisionBounds(-8, -6, 8, 8);
2415         } else {
2416           // force ducking
2417           status = DUCKING;
2418         }
2419       }
2421       if (wallDeath) {
2422         foreach (; 0..6) {
2423           if (isCollision()) {
2424                  if (isCollisionLeft(0) && !isCollisionRight(4)) fltx = ix+1;
2425             else if (isCollisionRight(0) && !isCollisionLeft(4)) fltx = ix-1;
2426             else if (isCollisionBottom(0) && !isCollisionTop(4)) flty = iy-1;
2427             else if (isCollisionTop(0) && !isCollisionBottom(4)) flty = iy+1;
2428             else break;
2429           }
2430         }
2432         if (wallDeath && isCollision()) {
2433           if (!dead) level.addDeath('wall');
2434           //visible = false;
2435           dead = true;
2436           writeln("PLAYER KILLED BY WALL");
2437           global.plife = 0; // oops
2438         }
2439       }
2440     }
2441   } else {
2442     // in cutscene
2443     //writeln("flty=", flty, "; iy=", iy);
2444     if (flty <= 0) {
2445       status = STANDING;
2446     }
2447   }
2449   // figures out what the sprite index of the character should be
2450   characterSprite();
2452   // sets the previous state and the previously previous state
2453   statePrevPrev = statePrev;
2454   statePrev = status;
2456   // calculates the imageSpeed based on the character's velocity
2457   if (status == RUNNING || status == DUCKING || status == LOOKING_UP) {
2458     if (status == RUNNING || status == LOOKING_UP) imageSpeed = fabs(xVel)*runAnimSpeed+0.1;
2459   }
2461   if (status == CLIMBING) imageSpeed = sqrt(xVel*xVel+yVel*yVel)*climbAnimSpeed;
2463   if (xVel >= 4 || xVel <= -4) {
2464     imageSpeed = 1;
2465     if (platformCharacterIs(ON_GROUND)) {
2466       setCollisionBounds(-8, -6, 8, 8);
2467     } else {
2468       setCollisionBounds(-5, -6, 5, 8);
2469     }
2470   } else {
2471     setCollisionBounds(-5, -6, 5, 8);
2472   }
2474   if (whipping) imageSpeed = 1;
2476   if (status == DUCKTOHANG) {
2477     imageFrame = 0;
2478     imageSpeed = 0.8;
2479   }
2481   // limit the imageSpeed at 1 so the animation always looks good
2482   if (imageSpeed > 1) imageSpeed = 1;
2484   //if (kItemPressed) writeln("ITEM! dead=", dead, "; stunned=", stunned, "; active=", active);
2485   if (dead || stunned || !active) {
2486     // do nothing
2487   } else if (/*inGame &&*/ kItemPressed && !whipping) {
2488     // SWITCH
2489     if (kUp) scrSwitchToStickyBombs(); else scrSwitchToNextItem();
2490   } else if (/*inGame &&*/ kRopePressed && global.rope > 0 && !whipping) {
2491     if (!kDown && colTop) {
2492       // do nothing
2493     } else {
2494       launchRope(kDown, doDrop:true);
2495     }
2496   } else if (/*inGame &&*/ kBombPressed && global.bombs > 0 && !whipping) {
2497     if (holdItem isa ItemWeaponBow && bowArmed) {
2498       if (holdArrow != ARROW_BOMB) {
2499         //writeln("set bow arrows to bomb");
2500         holdArrow = ARROW_BOMB;
2501       } else {
2502         //writeln("set bow arrows to normal");
2503         holdArrow = ARROW_NORM;
2504       }
2505     } else {
2506       scrLaunchBomb();
2507     }
2508   }
2511   // open chest/crate
2512   if (!dead && !stunned && kUp && kAttackPressed) {
2513     auto octr = ItemOpenableContainer(level.isObjectInRect(ix, iy, width, height, delegate bool (MapObject o) {
2514       return (o isa ItemOpenableContainer);
2515     }));
2516     if (octr) {
2517       if (octr.openMe()) kAttackPressed = false;
2518     }
2519   }
2522   // use weapon / attack
2523   if (!dead && !stunned && kAttackPressed && !holdItem /*&& !pickedItem*/) {
2524     bowArmed = false;
2525     bowStrength = 0;
2526     sndStopSound('sndBowPull');
2527     if (status != DUCKING && status != DUCKTOHANG && !whipping && !isExitingSprite()) {
2528       imageSpeed = 0.6;
2529       if (global.isTunnelMan) {
2530         if (platformCharacterIs(ON_GROUND) || platformCharacterIs(IN_AIR)) {
2531           setSprite('sTunnelAttackL');
2532           whipping = true;
2533         }
2534       } else if (global.isDamsel) {
2535         setSprite('sDamselAttackL');
2536         whipping = true;
2537       } else {
2538         setSprite('sAttackLeft');
2539         whipping = true;
2540       }
2541     } else if (kDown && !pickedItem) {
2542       // pick up item
2543       MapObject obj = level.isObjectInRect(x-8, y, 9, 9, delegate bool (MapObject o) {
2544         if (o.spectral || !o.canPickUp) return false;
2545         if (!o.collidesWith(self)) return false;
2546         return o.onCanBePickedUp(self);
2547         /*
2548         if (o isa MapItem) return (o.active && o.canPickUp && !o.spectral);
2549         if (o isa MapEnemy) return (o.active && o.canPickUp && !o.spectral && (o.dead || o.status >= MapObject::STUNNED || o.meGoldMonkey));
2550         if (o isa ObjCharacter) return (o.active && o.canPickUp && !o.spectral);
2551         */
2552         return false;
2553       }, precise:false);
2554       if (obj) {
2555         // `canPickUp` is checked in callback
2556         if (/*obj.canPickUp &&*/ true /*k8: do we really need this? !level.isSolidAtPoint(obj.ix+2, obj.iy)*/) {
2557           //pickupItemType = holdItem.type;
2558           //!if (isAshShotgun(holdItem)) pickupItemType = "Boomstick";
2559           //!if (isGoldMonkey(obj) and obj.status &lt; 98) obj.status = 0; // do not play walk animation while held
2561           if (!obj.onTryPickup(self)) {
2562             if (obj.isInstanceAlive) scrPickupItem(obj);
2563           }
2565           /+!
2566           if (holdItem.type == "Bow" and holdItem.new) {
2567             holdItem.new = false;
2568             global.arrows += 6;
2569             if (global.arrows &gt; 99) global.arrows = 99;
2570           }
2571           +/
2572         }
2573       }
2574     }
2575   } else if (!dead && !stunned) {
2576     if (holdItem isa ItemWeaponBow) {
2577       //writeln("BOW! kAttack=", kAttack, "; kAttackPressed=", kAttackPressed, "; bowArmed=", bowArmed, "; bowStrength=", bowStrength, "; holdArrow=", holdArrow);
2578       if (kAttackPressed) {
2579         if (scrPlayerIsDucking()) {
2580           scrUsePutItemOnGround();
2581         } else if (!bowArmed) {
2582           bowStrength = 0;
2583           ItemWeaponBow(holdItem).armBow(self);
2584         }
2585       }
2586       if (kAttack) {
2587         if (bowArmed && bowStrength < 12) {
2588           bowStrength += 0.2;
2589           //writeln("arming: ", bowStrength);
2590         } else {
2591           sndStopSound('sndBowPull');
2592         }
2593       } else {
2594         //writeln("   xxBOW!");
2595         // ...and shoot
2596         scrFireBow();
2597       }
2598       if (!holdArrow) holdArrow = ARROW_NORM;
2599     } else {
2600       if (kAttackPressed && holdItem) scrUseItem();
2601     }
2602   }
2604   // buy items
2605   if (!dead && !stunned && kPayPressed) {
2606       // find nearest shopkeeper
2607     auto sc = MonsterShopkeeper(level.findNearestObject(ix, iy, delegate bool (MapObject o) {
2608       auto sc = MonsterShopkeeper(o);
2609       if (!sc) return false;
2610       //if (needCraps && sc.stype != 'Craps') return false;
2611       if (sc.dead || sc.angered || sc.outlaw) return false;
2612       return sc.canSellItem(self, holdItem);
2613     }));
2614     if (level.isInShop(ix/16, iy/16)) {
2615       // if no shopkeepers found, just use it
2616       if (!sc) {
2617         if (holdItem) {
2618           holdItem.forSale = false;
2619           holdItem.onTryPickup(self);
2620         }
2621       } else if (global.thiefLevel == 0 && !global.murderer) {
2622         // only law-abiding players can buy/sell items or play games
2623         writeln("shop item interaction: ", holdItem.objName, "; cost=", holdItem.cost);
2624         if (sc.doSellItem(self, holdItem)) {
2625           // use it
2626           if (holdItem) {
2627             holdItem.forSale = false;
2628             holdItem.onTryPickup(self);
2629           }
2630         }
2631         if (holdItem && !holdItem.isInstanceAlive) {
2632           holdItem = none;
2633           scrSwitchToPocketItem(forceIfEmpty:false); // just in case
2634         }
2635       }
2636     } else {
2637       // use pickup, if any
2638       if (holdItem isa ItemPickup) {
2639         // make nearest shopkeeper angry (an unlikely situation, but still...)
2640         if (sc && holdItem.forSale) level.scrShopkeeperAnger(GameLevel::SCAnger.ItemStolen);
2641         holdItem.forSale = false;
2642         holdItem.onTryPickup(self);
2643       } else {
2644         pickupsAround.clear();
2645         level.isObjectInRect(x0, y0, width, height, delegate bool (MapObject o) {
2646           auto pk = ItemPickup(o);
2647           if (pk && pk.collidesWith(self)) {
2648             bool found = false;
2649             foreach (auto opk; pickupsAround) if (opk == pk) { found = true; break; }
2650             if (!found) pickupsAround[$] = pk;
2651           }
2652           return false;
2653         }, precise:false);
2654         // now try to use all pickups
2655         foreach (ItemPickup pk; pickupsAround) {
2656           if (pk.isInstanceAlive) {
2657             if (sc && pk.forSale) level.scrShopkeeperAnger(GameLevel::SCAnger.ItemStolen);
2658             pk.forSale = false;
2659             pk.onTryPickup(self);
2660           }
2661         }
2662         pickupsAround.clear();
2663       }
2664     }
2665   }
2668 transient array!ItemPickup pickupsAround;
2671 // ////////////////////////////////////////////////////////////////////////// //
2672 override bool initialize () {
2673   if (!::initialize()) return false;
2675   powerups.length = 0;
2676   powerups[$] = SpawnObject(PPParachute);
2677   powerups[$] = SpawnObject(PPCape);
2679   foreach (PlayerPowerup pp; powerups) pp.owner = self;
2681   if (global.isDamsel) {
2682     desc = "Damsel";
2683     desc2 = "An athletic, unfittingly-dressed woman with extremely awkward running form.";
2684     setSprite('sDamselLeft');
2685   } else if (global.isTunnelMan) {
2686     desc = "Tunnel Man";
2687     desc2 = "A miner from the desert. His tools are a cut above the rest.";
2688     setSprite('sTunnelLeft');
2689   } else {
2690     desc = "Spelunker";
2691     desc2 = "A strange little man who spends his time exploring caverns. He wants to be just like Indiana Jones when he grows up.";
2692     setSprite('sStandLeft');
2693   }
2695   swimming = false;
2697   dir = Dir.Right;
2699   // scum ClimbSpeed
2700   switch (global.config.scumClimbSpeed) {
2701     case 2:
2702       climbAcc = 0.9;
2703       climbAnimSpeed = 0.4;
2704       climbSndSpeed = 6;
2705       break;
2706     case 3:
2707       climbAcc = 1.2;
2708       climbAnimSpeed = 0.45;
2709       climbSndSpeed = 5;
2710       break;
2711     case 4:
2712       climbAcc = 1.5;
2713       climbAnimSpeed = 0.5;
2714       climbSndSpeed = 4;
2715       break;
2716     case 5:
2717       climbAcc = 1.8;
2718       climbAnimSpeed = 0.5;
2719       climbSndSpeed = 3;
2720       break;
2721     default:
2722       climbAcc = 0.6;       // how fast the character will climb
2723       climbAnimSpeed = 0.4; // relates to how fast the climbing animation should go
2724       climbSndSpeed = 8;
2725       break;
2726   }
2728   // sets the collision bounds to fit the default sprites (you can edit the arguments of the script)
2729   setCollisionBounds(-5, -5, 5, 8); // setCollisionBounds(-5, -8, 5, 8);
2731   statePrev = status;
2732   statePrevPrev = statePrev;
2733   gravityIntensity = grav;  // this variable describes the current force due to gravity (this variable is altered for variable jumping)
2734   jumpTime = jumpTimeTotal; // current time of the jump (0=start of jump, jumpTimeTotal=end of jump)
2736   return true;
2740 // ////////////////////////////////////////////////////////////////////////// //
2741 override void onAnimationLooped () {
2742   auto spr = getSprite();
2743   if (spr.Name == 'sAttackLeft' || spr.Name == 'sDamselAttackL' || spr.Name == 'sTunnelAttackL') {
2744     whipping = false;
2745     if (holdItem) holdItem.visible = true;
2746   } else if (spr.Name == 'sDuckToHangL' || spr.Name == 'sDamselDtHL' || spr.Name == 'sTunnelDtHL') {
2747     shiftY(16);
2748     moveSnap(1, 8);
2749     int x = ix, y = iy;
2750     xVel = 0;
2751     yVel = 0;
2752     xAcc = 0;
2753     yAcc = 0;
2754     grav = 0;
2755     MapTile obj;
2756     if (dir == Dir.Left) {
2757       // left
2758       obj = level.isAnyLadderAtPoint(x-8, y);
2759     } else {
2760       // right
2761       obj = level.isAnyLadderAtPoint(x+8, y);
2762     }
2763     if (obj) {
2764       status = CLIMBING;
2765       setX(obj.ix+8);
2766     } else if (dir == Dir.Left) {
2767       status = HANGING;
2768       dir = Dir.Right;
2769       shiftX(-6);
2770       shiftX(1);
2771     } else {
2772       status = HANGING;
2773       dir = Dir.Left;
2774       shiftX(6);
2775     }
2776   } else if (isExitingSprite()) {
2777     scrPlayerExit();
2778     //!global.cleanSolids = true;
2779   }
2783 void activatePlayerWeapon () {
2784   if (dead) {
2785     if (holdItem isa PlayerWeapon) {
2786       auto wep = holdItem;
2787       holdItem = none;
2788       wep.instanceRemove();
2789       return;
2790     }
2791   }
2792   if (global.config.unarmed) return;
2794   if (holdItem isa PlayerWeapon) {
2795     if (!whipping) {
2796       /*
2797       writeln("000: !!!!!!!");
2798       if (holdItem) writeln("  H(", GetClassName(holdItem.Class), "): '", holdItem.objType, "'");
2799       if (pickedItem) writeln("  P(", GetClassName(pickedItem.Class), "): '", pickedItem.objType, "'");
2800       */
2801       auto wep = holdItem;
2802       holdItem = none;
2803       wep.instanceRemove();
2804       return;
2805     }
2806   }
2808   if (holdItem) return;
2810   auto spr = getSprite();
2811   if (spr.Name != 'sAttackLeft' && spr.Name != 'sDamselAttackL' && spr.Name != 'sTunnelAttackL') return;
2813   if (imageFrame > 4) {
2814     //bool hitEnemy = (PlayerWeapon(holdItem) ? PlayerWeapon(holdItem).hitEnemy : false);
2815     if (global.isTunnelMan || pickedItem isa ItemWeaponMattock) {
2816       holdItem = level.MakeMapObject(ix+(dir == Dir.Left ? -16 : 16), iy, 'oMattockHit');
2817       if (imageFrame < 7) playSound('sndWhip');
2818     } else if (pickedItem isa ItemWeaponMachete) {
2819       holdItem = level.MakeMapObject(ix+(dir == Dir.Left ? -16 : 16), iy, 'oSlash');
2820       playSound('sndWhip');
2821     } else {
2822       holdItem = level.MakeMapObject(ix+(dir == Dir.Left ? -16 : 16), iy, 'oWhip');
2823       playSound('sndWhip');
2824     }
2825     /+ not needed anymore
2826     if (holdItem) {
2827       holdItem.active = true;
2828       //if (PlayerWeapon(holdItem)) PlayerWeapon(holdItem).hitEnemy = hitEnemy;
2829     }
2830     +/
2831   } else if (imageFrame < 2) {
2832     if (global.isTunnelMan || pickedItem isa ItemWeaponMattock) {
2833       holdItem = level.MakeMapObject(ix+(dir == Dir.Left ? -16 : 16), iy, 'oMattockPre');
2834     } else if (pickedItem isa ItemWeaponMachete) {
2835       holdItem = level.MakeMapObject(ix+(dir == Dir.Left ? -16 : 16), iy, 'oMachetePre');
2836     } else {
2837       holdItem = level.MakeMapObject(ix+(dir == Dir.Left ? 16 : -16), iy, 'oWhipPre');
2838     }
2839     /+ not needed anymore
2840     if (holdItem) holdItem.active = true;
2841     +/
2842   }
2844   /*
2845   if (holdItem) {
2846     if (holdItem.type == "Machete") {
2847       obj = instance_create(x-16, y, oSlash);
2848       obj.sprite_index = sSlashLeft;
2849       playSound(global.sndWhip);
2850     } else if (holdItem.type == "Mattock") {
2851       obj = instance_create(x-16, y, oMattockHit);
2852       obj.sprite_index = sMattockHitL;
2853       if (image_index &lt; 7) playSound(global.sndWhip);
2854     }
2855   } else {
2856     if (global.isTunnelMan) {
2857       obj = instance_create(x-16, y, oMattockHit);
2858       obj.sprite_index = sMattockHitL;
2859       if (image_index &lt; 7) playSound(global.sndWhip);
2860     } else {
2861       obj = instance_create(x-16, y, oWhip);
2862       if (global.scumWhipUpgrade == 1) obj.sprite_index = sWhipLongLeft; else obj.sprite_index = sWhipLeft;
2863       playSound(global.sndWhip);
2864     }
2865   }
2866   */
2870 //bool webHit = false;
2872 bool doBreakWebsCB (MapObject o) {
2873   if (o isa ItemWeb) {
2874     writeln("IN WEB!");
2875     /*if (!webHit)*/ {
2876       if (fabs(xVel) > 1) {
2877         xVel = xVel*0.2;
2878         if (!o.dying) ItemWeb(o).life -= 5;
2879       } else {
2880         xVel = 0;
2881       }
2882       if (fabs(yVel) > 1) {
2883         yVel = yVel*0.2;
2884         if (!o.dying) ItemWeb(o).life -= 5;
2885       } else {
2886         yVel = 0;
2887       }
2888     }
2889   }
2890   return false;
2894 void initiateExitSequence () {
2895        if (global.isDamsel) setSprite('sDamselExit');
2896   else if (global.isTunnelMan) setSprite('sTunnelExit');
2897   else setSprite('sPExit');
2899   imageSpeed = 0.5;
2900   active = false;
2901   invincible = 999;
2902   depth = 999;
2904   /*k8: the following is done in `GameLevel`
2905   if (global.thiefLevel > 0) global.thiefLevel -= 1;
2906   //orig dbg:if (global.currLevel == 1) global.currLevel += firstLevelSkip; else global.currLevel += levelSkip;
2907   global.currLevel += 1;
2908   */
2909   playSound('sndSteps');
2913 void processLevelExit () {
2914   if (dead || stunned || whipping) return;
2915   if (!platformCharacterIs(ON_GROUND)) return;
2916   if (isExitingSprite()) return; // just in case
2918   auto hld = holdItem;
2919   if (hld isa PlayerWeapon) return; // oops
2921   //if (!kExitPressed && !hld) return false;
2923   auto door = level.checkTileAtPoint(ix, iy, &level.cbCollisionExitTile);
2924   if (!door || !door.visible) return; // note that `invisible` doors still works
2926   // sell idol, or free damsel
2927   if (hld isa ItemGoldIdol) {
2928     //!if (isRealLevel()) global.idolsConverted += 1;
2929     //not thisglobal.money += hld.value*(global.levelType+1);
2930     ItemGoldIdol(hld).registerConverted();
2931     addScore(hld.value*(global.levelType+1));
2932     //!if (hld.sprite_index == sCrystalSkull) global.skulls += 1; else global.idols += 1;
2933     playSound('sndCoin');
2934     level.MakeMapObject(ix, iy-8, 'oBigCollect');
2935     holdItem = none;
2936     hld.instanceRemove();
2937     //!with (hld) instance_destroy();
2938     //!hld = 0;
2939     //!pickupItemType = "";
2940   } else if (hld isa MonsterDamsel) {
2941     holdItem = none;
2942     MonsterDamsel(hld).exitAtDoor(door);
2943   }
2945   if (!kExitPressed) {
2946     if (!door.invisible) {
2947       string msg = door.getExitMessage();
2948       if (msg.length == 0) {
2949         level.osdMessage(va("PRESS %s TO ENTER.", (global.config.useDoorWithButton ? "$PAY" : "$UP")), -666);
2950       } else if (msg[$-1] != '\n') {
2951         level.osdMessage(va("%s\nPRESS %s TO ENTER.", msg, (global.config.useDoorWithButton ? "$PAY" : "$UP")), -666);
2952       } else {
2953         level.osdMessage(msg, -666);
2954       }
2955     }
2956     return;
2957   }
2959   // exiting
2960   holdArrow = 0;
2961   bowArmed = false;
2963   // drop armed bomb
2964   if (isHoldingArmedBomb()) scrUseThrowItem();
2966   if (isHoldingBombOrRope()) scrSwitchToPocketItem(forceIfEmpty:true);
2968   wasHoldingBall = false;
2969   hld = holdItem;
2970   if (hld) {
2971     if (hld isa ItemGoldIdol) {
2972       //!if (isRealLevel()) global.idolsConverted += 1;
2973       //not thisglobal.money += hld.value*(global.levelType+1);
2974       ItemGoldIdol(hld).registerConverted();
2975       addScore(hld.value*(global.levelType+1));
2976       //!if (hld.sprite_index == sCrystalSkull) global.skulls += 1; else global.idols += 1;
2977       playSound('sndCoin');
2978       level.MakeMapObject(ix, iy-8, 'oBigCollect');
2979       holdItem = none;
2980       hld.instanceRemove();
2981       //!with (hld) instance_destroy();
2982       //!hld = 0;
2983       //!pickupItemType = "";
2984     } else if (hld isa MonsterDamsel) {
2985       holdItem = none;
2986       MonsterDamsel(hld).exitAtDoor(door);
2987     } else if (hld.heavy || hld isa MapEnemy || hld isa ObjCharacter) {
2988       // drop heavy items, characters and enemies (but not ball)
2989       if (hld isa ItemBall) {
2990         wasHoldingBall = true;
2991         removeBallAndChain(temp:true);
2992       } else {
2993         scrUseThrowItem();
2994       }
2995     } else {
2996       // other items are carried thru
2997       if (hld.cannotBeCarriedOnNextLevel) {
2998         scrUseThrowItem();
2999         holdItem = none; // just in case
3000       } else {
3001         scrHideItemToPocket();
3002       }
3003       /*
3004       global.pickupItem = hld.type;
3005       if (isAshShotgun(hld)) global.pickupItem = "Boomstick";
3006       with (hld) {
3007         breakPieces = false;
3008         instance_destroy();
3009       }
3010       */
3011       //scrHideItemToPocket();
3012     }
3013   }
3015   knockOffMonkeys();
3017   //door = instance_place(x, y, oExit); // done above
3018   door.snapToExit(self);
3020   initiateExitSequence();
3022   level.playerExitDoor = door;
3026 override bool onFellInWater (MapTile water) {
3027   level.MakeMapObject(ix, iy-8, 'oSplash');
3028   swimming = true;
3029   playSound('sndSplash');
3030   myGrav = 0.2; //k8:???
3031   return false;
3034 override bool onOutOfWater () {
3035   swimming = false;
3036   myGrav = 0.6;
3037   return false;
3041 // ////////////////////////////////////////////////////////////////////////// //
3042 override void thinkFrame () {
3043   // remove whip, etc. when dead
3044   if (dead && holdItem isa PlayerWeapon) {
3045     auto pw = holdItem;
3046     holdItem = none;
3047     pw.instanceRemove();
3048     scrSwitchToPocketItem(forceIfEmpty:false);
3049   }
3051   setPowerupState('Cape', global.hasCape);
3053   foreach (PlayerPowerup pp; powerups) if (pp.active) pp.onPreThink();
3055   // kapala
3056   if (redColor > 0) {
3057          if (redToggle) redColor -= 5;
3058     else if (redColor < 20) redColor += 5;
3059     else redToggle = true;
3060   } else {
3061     redColor = 0;
3062   }
3064   if (dead) justdied = false;
3066   if (!dead) {
3067     if (invincible > 0) --invincible;
3068   } else {
3069     invincible = 0;
3070   }
3072   if (blink > 0) {
3073     blinkHidden = !blinkHidden;
3074     --blink;
3075   } else {
3076     blinkHidden = false;
3077   }
3079   auto spr = getSprite();
3080   int x = ix, y = iy;
3082   cameraBlockX = max(0, cameraBlockX-1);
3083   cameraBlockY = max(0, cameraBlockY-1);
3085   // WHOA
3086   if (spr.Name == 'sWhoaLeft' || spr.Name == 'sDamselWhoaL' || spr.Name == 'sTunnelWhoaL') {
3087     if (whoaTimer > 0) {
3088       whoaTimer -= 1;
3089     } else if (holdItem && onLoosingHeldItem(LostCause.Whoa)) {
3090       auto hi = holdItem;
3091       holdItem = none;
3092       if (!hi.onLostAsHeldItem(self, LostCause.Whoa)) {
3093         // oops, regain it
3094         holdItem = hi;
3095       } else {
3096         scrSwitchToPocketItem(forceIfEmpty:true);
3097       }
3098     }
3099   } else {
3100     whoaTimer = whoaTimerMax;
3101   }
3103   // firing
3104   if (firing > 0) firing -= 1;
3106   // water
3107   auto wtile = level.isWaterAtPoint(x, y/*, oWaterSwim, -1, -1*/);
3108   if (wtile) {
3109     if (!swimming) {
3110       if (onFellInWater(wtile) || !isInstanceAlive) return;
3111     }
3112   } else {
3113     if (swimming) {
3114       if (onOutOfWater() || !isInstanceAlive) return;
3115     }
3116   }
3118   // burning
3119   if (burning > 0) {
3120     if (global.randOther(1, 5) == 1) level.MakeMapObject(x-8+global.randOther(4, 12), y-8+global.randOther(4, 12), 'oBurn');
3121     burning -= 1;
3122   }
3124   // lava
3125   if (!dead && level.isLavaAtPoint(x, y+6/*, oLava, 0, 0*/)) {
3126     //!if (isRealLevel()) global.miscDeaths[11] += 1;
3127     level.addDeath('lava');
3128     playSound('sndFlame');
3129     global.plife -= 99;
3130     dead = true;
3131     xVel = 0;
3132     yVel = 0.1;
3133     grav = 0;
3134     myGrav = 0;
3135     bounced = true;
3136     burning = 100;
3137     depth = 999;
3138   }
3141   // jetpack
3142   if (global.hasJetpack && platformCharacterIs(ON_GROUND)) {
3143     jetpackFuel = 50;
3144   }
3146   // fall off bottom of screen
3147   if (!dead && y > level.tilesHeight*16+16) {
3148     //!if (isRealLevel()) global.miscDeaths[10] += 1;
3149     level.addDeath('void');
3150     global.plife -= 99; // spill blood
3151     xVel = 0;
3152     yVel = 0;
3153     grav = 0;
3154     myGrav = 0;
3155     bounced = true;
3156     scrDropItem(LostCause.Falloff);
3157     playSound('sndThud'); //???
3158     playSound('sndDie'); //???
3159   }
3161   if (dead && y > level.tilesHeight*16+16) {
3162     xVel = 0;
3163     yVel = 0;
3164     grav = 0;
3165     myGrav = 0;
3166   }
3168   if (/*active*/true) {
3169     if (spr.Name == 'sStunL' || spr.Name == 'sDamselStunL' || spr.Name == 'sTunnelStunL') {
3170       if (stunTimer > 0) {
3171         imageSpeed = 0.4;
3172         stunTimer -= 1;
3173       }
3174       if (stunTimer < 1) {
3175         stunned = false;
3176         canDropStuff = true;
3177       }
3178     }
3180     if (!level.inWinCutscene) {
3181       if (isParachuteActive() || isCapeActiveAndOpen()) fallTimer = 0;
3182     }
3184     // changed to yVel > 1 from yVel > 0
3185     if (yVel > 1 && status != CLIMBING) {
3186       fallTimer += 1;
3187       if (fallTimer > 16) wallHurt = 0; // no sense in them taking extra damage from being thrown here
3188       int paraOpenHeight = (global.config.scumSpringShoesReduceFallDamage && (global.hasSpringShoes || global.hasJordans) ? 22 : 14);
3189       //paraOpenHeight = 4;
3190       if (global.hasParachute && !stunned && fallTimer > paraOpenHeight) {
3191         //if (not collision_point(x, y+32, oSolid, 0, 0)) // was commented in the original code
3192         //!*if (not collision_line(x, y+16, x, y+32, oSolid, 0, 0))
3193         if (!level.checkTilesInRect(x, y+16, 1, 17, &level.cbCollisionAnySolid)) {
3194           // drop parachute
3195           //!instance_create(x-8, y-16, oParachute);
3196           fallTimer = 0;
3197           global.hasParachute = false;
3198           activatePowerup('Parachute');
3199           //writeln("parachute state: ", isParachuteActive());
3200         }
3201       }
3202     } else if (fallTimer > 16 && platformCharacterIs(ON_GROUND) &&
3203                !level.checkTilesInRect(x-8, y-8, 17, 17, &level.cbCollisionSpringTrap) /* not onto springtrap */)
3204     {
3205       // long drop -- player has just landed
3206       bool reducedDamage = (global.config.scumSpringShoesReduceFallDamage && (global.hasSpringShoes || global.hasJordans));
3207       if (reducedDamage && fallTimer <= 24) {
3208         // land without taking damage
3209         fallTimer = 0;
3210       } else {
3211         stunned = true;
3212              if (fallTimer > (reducedDamage ? 72 : 48)) global.plife -= 10*global.config.scumFallDamage;
3213         else if (fallTimer > (reducedDamage ? 48 : 32)) global.plife -= 2*global.config.scumFallDamage;
3214         else global.plife -= 1*global.config.scumFallDamage;
3215         if (global.plife < 1) {
3216           if (!dead) level.addDeath('fall');
3217           scrCreateBlood(x, y, 3);
3218           //!if (isRealLevel()) global.miscDeaths[3] += 1;
3219         }
3220         bounced = true;
3221         if (global.config.scumFallDamage > 0) stunTimer += 60;
3222         yVel = -3;
3223         fallTimer = 0;
3224         auto obj = level.MakeMapObject(x-4, y+6, 'oPoof');
3225         if (obj) obj.xVel = -0.4;
3226         obj = level.MakeMapObject(x+4, y+6, 'oPoof');
3227         if (obj) obj.xVel = 0.4;
3228         playSound('sndThud');
3229       }
3230     } else if (yVel <= 0) {
3231       fallTimer = 0;
3232       if (isParachuteActive()) {
3233         deactivatePowerup('Parachute');
3234         level.MakeMapObject(ix-8, iy-16-8, 'oParaUsed');
3235       }
3236     }
3238     // if (stunned) fallTimer = 0; // was commented in the original code
3240     if (swimming && !level.isLavaAtPoint(x, y/*, oLava, 0, 0*/)) {
3241       fallTimer = 0;
3242       if (bubbleTimer > 0) {
3243         bubbleTimer -= 1;
3244       } else {
3245         level.MakeMapObject(x, y-4, 'oBubble');
3246         bubbleTimer = bubbleTimerMax;
3247       }
3248     } else {
3249       bubbleTimer = bubbleTimerMax;
3250     }
3252     //TODO: k8: move spear checking to spear handler
3253     if (!isExitingSprite()) {
3254       auto spear = MapObjectSpearsBase(level.isObjectInRect(ix-6, iy-6, 13, 14, delegate bool (MapObject o) {
3255         auto tt = MapObjectSpearsBase(o);
3256         if (!tt) return false;
3257         return tt.isHitFrame;
3258       }));
3259       if (spear) {
3260         // stunned = true;
3261         // bounced  = false;
3262         global.plife -= global.config.spearDmg; // 4
3263         if (!dead && global.plife <= 0 /*and isRealLevel()*/) level.addDeath('spear');
3264         xVel = global.randOther(4, 6)*(spear.isLeft ? -1 : 1);
3265         yVel = -6;
3266         flty -= 1;
3267         y = iy;
3268         // state = FALLING;
3269         scrCreateBlood(x, y, 1);
3270       }
3271     }
3273     if (status != DUCKTOHANG && !stunned && !dead && !isExitingSprite()) {
3274       bounced = false;
3275       characterStepEvent();
3276     } else {
3277       if (status != DUCKING && status != DUCKTOHANG) status = STANDING;
3278       checkControlKeys(getSprite());
3279     }
3280   }
3282   // if (dead or stunned)
3283   if (dead || stunned) {
3284     if (holdItem) {
3285       if (holdItem isa ItemWeaponBow && bowArmed) scrFireBow();
3286       scrDropItem(dead ? LostCause.Dead : LostCause.Stunned, xVel, -3);
3287     }
3289     yVel += (bounced ? 1.0 : 0.6);
3291     if (isCollisionTop(1) && yVel < 0) yVel = -yVel*0.8;
3292     if (isCollisionLeft(1) || isCollisionRight(1)) xVel = -xVel*0.5;
3294     bool collisionbottomcheck = !!isCollisionBottom(1);
3295     if (collisionbottomcheck || isCollisionBottom(1, &level.cbCollisionPlatform)) {
3296       // bounce
3297       if (collisionbottomcheck) {
3298         if (yVel > 2.5) yVel = -yVel*0.5; else yVel = 0;
3299       } else {
3300         // after falling onto a platform don't take extra damage after recovering from stunning
3301         fallTimer -= 1;
3302       }
3303       /* was commented in the original code
3304       if (isCollisionBottom(1)) {
3305         if (yVel &gt; 2.5) yVel = -yVel*0.5; else yVel = 0;
3306       } else {
3307         fallTimer -= 1;
3308       }
3309       */
3311       // friction
3312            if (fabs(xVel) < 0.1) xVel = 0;
3313       else if (fabs(xVel) != 0 && level.isIceAtPoint(x, y+16)) xVel *= 0.8;
3314       else if (fabs(xVel) != 0) xVel *= 0.3;
3316       bounced = true;
3317     }
3319     //webHit = false;
3320     //level.forEachObjectInRect(ix, iy, width, height, &doBreakWebsCB);
3322     // apply the limits since the velocity may be too extreme
3323     xVelLimit = 10;
3324     xVel = fclamp(xVel, -xVelLimit, xVelLimit);
3325     yVel = fclamp(yVel, -yVelLimit, yVelLimit);
3327     moveRel(xVel, yVel);
3328     x = ix;
3329     y = iy;
3331     // fix sprites, spawn blood from spikes
3332     if (isParachuteActive()) {
3333       deactivatePowerup('Parachute');
3334       level.MakeMapObject(ix-8, iy-16-8, 'oParaUsed');
3335     }
3337     if (whipping) {
3338       whipping = false;
3339       //!with (oWhip) instance_destroy();
3340     }
3342     if (global.isDamsel) {
3343       if (xVel == 0) {
3344              if (dead) setSprite('sDamselDieL');
3345         else if (stunned) setSprite('sDamselStunL');
3346       } else if (bounced) {
3347         if (yVel < 0) setSprite('sDamselBounceL'); else setSprite('sDamselFallL');
3348       } else {
3349         if (xVel < 0) setSprite('sDamselDieLL'); else setSprite('sDamselDieLR');
3350       }
3351     } else if (global.isTunnelMan) {
3352       if (xVel == 0) {
3353              if (dead) setSprite('sTunnelDieL');
3354         else if (stunned) setSprite('sTunnelStunL');
3355       } else if (bounced) {
3356         if (yVel < 0) setSprite('sTunnelLBounce'); else setSprite('sTunnelFallL');
3357       } else {
3358         if (xVel < 0) setSprite('sTunnelDieLL'); else setSprite('sTunnelDieLR');
3359       }
3360     } else {
3361       if (xVel == 0) {
3362              if (dead) setSprite('sDieL');
3363         else if (stunned) setSprite('sStunL');
3364       } else if (bounced) {
3365         if (yVel < 0) setSprite('sDieLBounce'); else setSprite('sDieLFall');
3366       } else {
3367         if (xVel < 0) setSprite('sDieLL'); else setSprite('sDieLR');
3368       }
3369     }
3371     x = ix;
3372     y = iy;
3374     /*
3375     if (dead && justdied && yVel != 0) {
3376       auto spk = level.checkTileAtPoint(x, y, &level.cbCollisionSpikes);
3377       if (spk && global.randOther(1, 8) == 1) scrCreateBlood(spk.x0, spk.y0, 1);
3378     }
3379     */
3381     auto colobj = isCollisionRight(1);
3382     if (!colobj) colobj = isCollisionLeft(1);
3383     if (!colobj) colobj = isCollisionBottom(1);
3384     if (colobj) {
3385       if (wallHurt > 0) {
3386         foreach (; 0..3) level.MakeMapObject(colobj.x0, colobj.y0, 'oBlood');
3387         global.plife -= 1;
3388         if (!dead && global.plife <= 0 /*&& isRealLevel()*/) {
3389           if (thrownBy) {
3390             writeln("thrown to death by '", thrownBy, "'");
3391             level.addDeath(thrownBy);
3392           }
3393         }
3394         wallHurt -= 1;
3395         if (wallHurt <= 0) thrownBy = '';
3396         playSound('sndHurt'); //???
3397       }
3398     }
3400     colobj = isCollisionBottom(1);
3401     if (colobj && !bounced) {
3402       bounced = true;
3403       foreach (; 0..3) level.MakeMapObject(colobj.x0, colobj.y0, 'oBlood');
3404       if (wallHurt > 0) {
3405         global.plife -= 1;
3406         if (!dead && global.plife <= 0 /*and isRealLevel()*/) {
3407           if (thrownBy) {
3408             writeln("thrown to death by '", thrownBy, "'");
3409             level.addDeath(thrownBy);
3410           }
3411         }
3412         wallHurt -= 1;
3413         if (wallHurt <= 0) thrownBy = '';
3414       }
3415     }
3416   } else {
3417     // look up and down
3418     bool kPay = level.isKeyDown(GameConfig::Key.Pay);
3419     if (kPay) {
3420       // gnounc's quick look
3421       if (!kRight && !kLeft && (platformCharacterIs(ON_GROUND) || status == HANGING)) {
3422              if (kDown) { if (viewCount <= 6) viewCount += 3; else viewOffset += 6; }
3423         else if (kUp) { if (viewCount <= 6) viewCount += 3; else viewOffset -= 6; }
3424         else viewCount = 0;
3425       } else {
3426         viewCount = 0;
3427       }
3428     } else {
3429       // default look up/down with delay if pay button not held
3430       if (!kRight && !kLeft && (platformCharacterIs(ON_GROUND) || status == HANGING)) {
3431              if (kDown) { if (viewCount <= 30) viewCount += 1; else viewOffset += 4; }
3432         else if (kUp) { if (viewCount <= 30) viewCount += 1; else viewOffset -= 4; }
3433         else viewCount = 0;
3434       } else {
3435         viewCount = 0;
3436       }
3437     }
3438   }
3439   if (viewCount == 0 && viewOffset) viewOffset = (viewOffset < 0 ? min(0, viewOffset+8) : max(0, viewOffset-8));
3440   viewOffset = clamp(viewOffset, -16*6, 16*6);
3442   if (!dead) activatePlayerWeapon();
3444   if (!dead) processLevelExit();
3446   // hurt too much
3447   if (global.plife < -99 && visible && justdied) scrCreateBlood(x, y, 3);
3449   if (global.plife < 1) {
3450     dead = true;
3451   }
3453   // spikes, and other shit
3454   if (global.plife >= -99 && visible && !isExitingSprite()) {
3455     auto colSpikes = level.checkTilesInRect(x-4, y-4, 9, 13, &level.cbCollisionSpikes);
3457     if (colSpikes && dead) {
3458       grav = 0;
3459       if (!level.isSolidAtPoint(x, y+9)) { shiftY(0.02); y = iy; } //0.05;
3460       //else myGrav = 0.6;
3461     } else {
3462       myGrav = 0.6;
3463     }
3465     if (colSpikes && yVel > 0 && (fallTimer > 3 || stunned)) { // originally fallTimer &gt; 4
3466       if (!dead) {
3467         scrCreateBlood(ix, iy, 3);
3468         // spikes will always instant-kill in Moon room
3469         /*if (isRoom("rMoon")) global.plife -= 99; else*/ global.plife -= global.config.scumSpikeDamage;
3470         if (/*isRealLevel() &&*/ global.plife <= 0) level.addDeath('spike');
3471         if (global.plife > 0) playSound('sndHurt');
3472         xVel = 0;
3473         yVel = 0;
3474         myGrav = 0;
3475       }
3476       colSpikes.makeBloody();
3477     }
3478     //else if (not dead) myGrav = 0.6;
3479   }
3482   // sacrifice
3483   if (visible && (status >= STUNNED || stunned || dead || status == DUCKING)) {
3484     /*if (*/checkAndPerformSacrifice()/*) return*/;
3485   } else {
3486     sacCount = default.sacCount;
3487   }
3489   // activate ankh
3490   if (dead && global.hasAnkh) {
3491     writeln("*** ACTIVATED ANKH");
3492     global.hasAnkh = false;
3493     dead = false;
3494     int newLife = (global.isTunnelMan ? global.config.scumTMLife : global.config.scumStartLife);
3495     global.plife = max(global.plife, newLife);
3496     level.osdMessage("THE ANKH SHATTERS!\nYOU HAVE BEEN REVIVED!", 4);
3497     // find moai
3498     auto moai = level.forEachTile(delegate bool (MapTile t) { return (t.objType == 'oMoai'); });
3499     if (moai) {
3500       level.forEachTile(delegate bool (MapTile t) {
3501         if (t.objType == 'oMoaiInside') {
3502           teleportTo(t.ix+8, t.iy+8);
3503           t.instanceRemove();
3504         }
3505         return false;
3506       });
3507       //teleportTo(moai.ix+16+8, moai.iy+16+8);
3508     } else {
3509       if (level.allEnters.length) {
3510         teleportTo(level.allEnters[0].ix+8, level.allEnters[0].iy-8);
3511       }
3512     }
3513     level.centerViewAtPlayer();
3514     auto ball = getMyBall();
3515     if (ball) ball.teleportToPrisoner();
3516     //k8:???depth = 50;
3517     xVel = 0;
3518     yVel = 0;
3519     blink = 60;
3520     invincible = 60;
3521     fallTimer = 0;
3522     visible = true;
3523     active = true;
3524     dead = false;
3525     stunned = false;
3526     status = STANDING;
3527     burning = 0;
3528     //alarm[8] = 60; // this starts music; but we don't need it, 'cause we won't stop the music on player death
3529     playSound('sndTeleport');
3530   }
3533   if (dead) level.stats.gameOver();
3535   // step end
3536   if (status == DUCKTOHANG) {
3537     spr = getSprite();
3538     if (spr.Name != 'sDuckToHangL' && spr.Name != 'sDamselDtHL' && spr.Name != 'sTunnelDtHL') status = STANDING;
3539   }
3541   foreach (PlayerPowerup pp; powerups) if (pp.active) pp.onPostThink();
3543   if (jetpackFlaresTime > 0) {
3544     if (--jetpackFlaresTime == 0) {
3545       auto obj = level.MakeMapObject(ix+global.randOther(0, 3)-global.randOther(0, 3), iy+global.randOther(0, 3)-global.randOther(0, 3), 'oFlareSpark');
3546       if (obj) {
3547         obj.yVel = global.randOther(1, 3);
3548         obj.xVel = global.randOther(0, 3)-global.randOther(0, 3);
3549       }
3550       playSound('sndJetpack');
3551     }
3552   }
3556 // ////////////////////////////////////////////////////////////////////////// //
3557 override void drawWithOfs (int xpos, int ypos, int scale, float currFrameDelta) {
3558   //if (heldBy) return; // owner will take care of this
3559   if (blinkHidden) return;
3561   bool renderJetpackBack = false;
3562   if (global.hasJetpack) {
3563     // render jetpack
3564     if ((status == CLIMBING || isExitingSprite()) && !whipping) {
3565       // later
3566       renderJetpackBack = true;
3567     } else {
3568       int xi, yi;
3569       getInterpCoords(currFrameDelta, scale, out xi, out yi);
3570       yi -= 1;
3571       SpriteImage spr;
3572       if (dir == Dir.Right) {
3573         spr = level.sprStore['sJetpackRight'];
3574         xi -= 4;
3575       } else {
3576         spr = level.sprStore['sJetpackLeft'];
3577         xi += 4;
3578       }
3579       if (spr) {
3580         auto spf = spr.frames[0];
3581         if (spf && spf.width > 0 && spf.height > 0) spf.tex.blitAt(xi-xpos-spf.xofs*scale, yi-ypos-spf.yofs*scale, scale);
3582       }
3583     }
3584   }
3586   foreach (PlayerPowerup pp; powerups) if (pp.active) pp.preDrawWithOfs(xpos, ypos, scale, currFrameDelta);
3588   auto oldColor = Video.color;
3589   if (redColor > 0) Video.color = clamp(200+redColor, 0, 255)<<16;
3590   ::drawWithOfs(xpos, ypos, scale, currFrameDelta);
3591   Video.color = oldColor;
3593   if (renderJetpackBack) {
3594     int xi, yi;
3595     getInterpCoords(currFrameDelta, scale, out xi, out yi);
3596     SpriteImage spr = level.sprStore['sJetpackBack'];
3597     if (spr) {
3598       auto spf = spr.frames[0];
3599       if (spf && spf.width > 0 && spf.height > 0) spf.tex.blitAt(xi-xpos-spf.xofs*scale, yi-ypos-spf.yofs*scale, scale);
3600     }
3601   }
3603   foreach (PlayerPowerup pp; powerups) if (pp.active) pp.postDrawWithOfs(xpos, ypos, scale, currFrameDelta);
3606   int xi, yi;
3607   getInterpCoords(currFrameDelta, scale, out xi, out yi);
3609   bool doMirror;
3610   int frmXOfs;
3611   auto spf = getSpriteFrame(out doMirror);
3612   int spry0 = yi-spf.yofs*scale-ypos;
3613   if (!doMirror) {
3614     int sprx0 = xi-spf.xofs*scale-xpos;
3615     spf.tex.blitAt(sprx0, spry0, scale);
3616   } else {
3617     // mirror it
3618     int sprx0 = xi+spf.xofs*scale-xpos;
3619     spf.tex.blitExt(sprx0, spry0, sprx0-spf.tex.width*scale, spry0+spf.tex.height*scale, 0, 0, spf.tex.width, spf.tex.height);
3620   }
3622   if (false) {
3623     auto oclr = Video.color;
3624     Video.color = 0xff_ff_00;
3625     Video.drawRect(x0*scale-xpos, y0*scale-ypos, width*scale, height*scale);
3626     Video.color = 0x00_ff_00;
3627     Video.drawRect(ix*scale-xpos, iy*scale-ypos, 2, 2);
3629     if (isCollision()) {
3630       Video.color = 0x3f_ff_00_00;
3631       Video.fillRect(x0*scale-xpos, y0*scale-ypos, width*scale, height*scale);
3632     }
3634     Video.color = oclr;
3635   }
3640 void lastDrawWithOfs (int xpos, int ypos, int scale, float currFrameDelta) {
3641   foreach (PlayerPowerup pp; powerups) if (pp.active) pp.lastDrawWithOfs(xpos, ypos, scale, currFrameDelta);
3646   if (draw) {
3647          if (redColor > 0) draw_sprite_ext(sprite_index, -1, x, y, image_xscale, image_yscale, image_angle, make_color_rgb(200+redColor, 0, 0), image_alpha);
3648     else if (greenColor > 0) draw_sprite_ext(sprite_index, -1, x, y, image_xscale, image_yscale, image_angle, make_color_rgb(round(103+greenColor), round(161+greenColor), 0), image_alpha);
3649     else draw_sprite_ext(sprite_index, -1, x, y, image_xscale, image_yscale, image_angle, image_blend, image_alpha);
3650   }
3651   if (facing == RIGHT) {
3652     if (holdArrow == ARROW_NORM) {
3653       draw_sprite(sArrowRight, -1, x+4, y+1);
3654     } else if (holdArrow == ARROW_BOMB) {
3655       if (holdArrowToggle) draw_sprite(sBombArrowRight, 0, x+4, y+2); else draw_sprite(sBombArrowRight, 1, x+4, y+2);
3656     }
3657   } else if (facing == LEFT) {
3658     if (holdArrow == ARROW_NORM) {
3659       draw_sprite(sArrowLeft, -1, x-4, y+1);
3660     } else if (holdArrow == ARROW_BOMB) {
3661       if (holdArrowToggle) draw_sprite(sBombArrowLeft, 0, x-4, y+2); else draw_sprite(sBombArrowLeft, 1, x-4, y+2);
3662     }
3663   }
3667 defaultproperties {
3668   objName = 'Player';
3669   objType = 'oPlayer';
3671   desc = "Spelunker";
3672   desc2 = "A strange little man who spends his time exploring caverns. He wants to be just like Indiana Jones when he grows up.";
3674   negateMirrorXOfs = true;
3676   status = FALLING; // the character state, must be one of the following: STANDING, RUNNING, DUCKING, LOOKING_UP, CLIMBING, JUMPING, or FALLING
3678   bloodless = false;
3680   stunned = false;
3681   bounced = false;
3683   fallTimer = 0;
3684   stunTimer = 0;
3685   wallHurt = 0;
3686   //thrownBy = ""; // "Yeti", "Hawkman", or "Shopkeeper" for stat tracking deaths by being thrown
3687   pushTimer = 0;
3688   whoaTimer = 0;
3689   //whoaTimerMax = 30;
3690   distToNearestLightSource = 999;
3692   sacCount = 60;
3694   flying = false;
3695   myGrav = 0.6;
3696   myGravNorm = 0.6;
3697   myGravWater = 0.2;
3698   yVelLimit = 10;
3699   bounceFactor = 0.5;
3700   frictionFactor = 0.3;
3702   xVelLimit = 16; // limits the xVel: default 15
3703   yVelLimit = 10; // limits the yVel
3704   xAccLimit = 9;  // limits the xAcc
3705   yAccLimit = 6;  // limits the yAcc
3706   runAcc = 3;     // the running acceleration
3708   grav = 1;
3710   depth = 5;
3711   //lightRadius = 96; //???