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