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