stars challenge (empty for now); shotgun will not use ammo in stars room
[k8vacspelynky.git] / PlayerPawn.vc
blobb4321be9f6ac42c39fb99ca4aa2964d8df6cd190
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 const int hangCountMax = 3;
23 array!PlayerPowerup powerups; // created in initializer
25 int cameraBlockX; // >0: don't center camera
26 int cameraBlockY; // >0: don't center camera
28 float xFric, yFric;
30 // swimming
31 int bubbleTimer;
32 const int bubbleTimerMax = 20;
33 int jetpackFlaresTime;
35 // gambling
36 int bet;
37 bool point;
39 //bool walkSndToggle;
41 bool damsel;
42 bool tunnelMan;
44 bool justdied = true; // so dead body won't spill blood endlessly
45 bool madeOffer;
46 bool whipping;
47 bool kJumped;
48 bool bowArmed;
49 bool movementBlocked;
50 int cantJump;
51 int firing;
52 const int firingMax = 20;
53 const int firingPistolMax = 20;
54 const int firingShotgunMax = 40;
55 float bowStrength;
56 int jetpackFuel;
58 int hotkeyPressed = -1;
59 bool kExitPressed;
61 // used with Kapala
62 int redColor;
63 bool redToggle;
65 // poison
66 int greenColor;
67 bool greenToggle;
68 //!!!global.poisonStrength = max(global.poisonStrength-0.5, 1);
70 //string holdItemType = "";
71 //string pickupItemType = "";
73 // this is what we had picked up
74 // picked item will be stored here by bomb/rope/item switcher
75 MapObject pickedItem;
77 bool canDropStuff = true;
78 bool kItemPressed;
79 //bool kItemReleased;
80 bool kRopePressed;
81 bool kBombPressed;
82 bool kPayPressed;
83 //bool kRope;
84 //bool kBomb;
85 //bool kPay;
87 int holdArrow;
88 bool holdArrowToggle;
89 int bombArrowCounter = 80;
91 int hangCount;
92 int runHeld;
94 // other
95 int blink;
96 bool blinkHidden;
98 // the keys that the platform character will use (don't edit)
99 bool kLeft;
100 bool kLeftPressed;
101 bool kLeftReleased;
102 bool kRight;
103 bool kRightPressed;
104 bool kRightReleased;
105 bool kUp;
106 bool kDown;
107 bool kJump;
108 bool kJumpPressed;
109 bool kJumpReleased;
110 bool jumpButtonReleased; // whether the jump button was released. (Stops the user from pressing the jump button many times to get extra jumps)
111 bool kAttack;
112 bool kAttackPressed;
113 bool kAttackReleased;
115 const float gravNorm = 1;
116 //float grav = 1; // the gravity
118 const float initialJumpAcc = -2; // relates to how high the character will jump
119 const int jumpTimeTotal = 10;  // how long the user must hold the jump button to get the maximum jump height
121 float climbAcc = 0.6; // how fast the character will climb
122 float climbAnimSpeed = 0.4; // relates to how fast the climbing animation should go
123 int climbSndSpeed = 8;
124 int climbSoundTimer;
125 bool climbSndToggle;
128 const float departLadderXVel = 4;  // how fast character should be moving horizontally when he leaves the ladder
129 const float departLadderYVel = -4; // how fast the character should be moving vertically when he leaves the ladder
131 const float frictionRunningX = 0.6;      // friction obtained while running
132 const float frictionRunningFastX = 0.98; // friction obtained while holding the shift button for some time while running
133 const float frictionClimbingX = 0.6;     // friction obtained while climbing
134 const float frictionClimbingY = 0.6;     // friction obtained while climbing
135 const float frictionDuckingX = 0.8;      // friction obtained while ducking
136 const float frictionFlyingX = 0.99;      // friction obtained while "flying"
138 const float runAnimSpeed = 0.1; // relates to the how fast the running animation should go
140 // hidden variables (don't edit)
141 protected int statePrev;
142 protected int statePrevPrev;
143 protected float gravityIntensity = grav; // this variable describes the current force due to gravity (this variable is altered for variable jumping)
144 protected float jumpTime = jumpTimeTotal; // current time of the jump (0=start of jump, jumpTimeTotal=end of jump)
145 protected int ladderTimer; // relates to whether the character can climb a ladder
146 protected int kLeftPushedSteps;
147 protected int kRightPushedSteps;
149 transient protected bool skipCutscenePressed;
152 //int score;
154 //PlayerWeapon actWeapon; // active weapon object
157 enum {
158   ARROW_NORM = 1,
159   ARROW_BOMB = 2,
162 int viewOffset;
163 int viewCount;
164 int lookOff0;
166 //int pExit0;
170 enum {
171   xSTART,
172   xTUTORIAL,
173   xSCORES,
174   xTITLE,
175   xEND,
176   xSHORTCUT5,
177   xSHORTCUT9,
178   xSUN,
179   xMOON,
180   xSTARS,
181   xCHANGE,
182   xSHORTCUT13,
183   xCHANGE2,
184   xLAVA1,
185   xTEST,
186   xBIZARRE,
187   xTEST2,
188   xDEBUG,
189   xMODE,
190   xTUTORIAL2,
195 // ////////////////////////////////////////////////////////////////////////// //
196 bool mustBeChained;
197 bool wasHoldingBall;
200 final ItemBall getMyBall () {
201   ItemBall res = none; //level.findNearestBall(x, y);
202   foreach (MapObject o; level.ballObjects) {
203     auto ball = ItemBall(o);
204     if (ball && ball.isInstanceAlive && ball.prisoner == self) {
205       res = ball;
206       break;
207     }
208   }
209   return res;
213 void spawnBallAndChain () {
214   mustBeChained = true;
215   auto ball = getMyBall();
216   if (!ball) {
217     ball = ItemBall(level.MakeMapObject(ix, iy, 'oBall'));
218     if (ball) ball.attachTo(self);
219   }
220   if (ball) {
221     if (wasHoldingBall) {
222       if (pickedItem) {
223         pickedItem.instanceRemove();
224         pickedItem = none;
225       }
226       if (holdItem && holdItem != ball) {
227         holdItem.instanceRemove();
228         holdItem = none;
229       }
230       holdItem = ball;
231     }
232   } else {
233     mustBeChained = false;
234   }
235   wasHoldingBall = false;
239 void removeBallAndChain (optional bool temp) {
240   auto ball = getMyBall();
241   if (ball) {
242     wasHoldingBall = (holdItem == ball);
243     mustBeChained = true;
244     ball.attachTo(none);
245     ball.instanceRemove();
246   }
247   if (specified_temp && temp) return;
248   wasHoldingBall = false;
249   mustBeChained = false;
253 // ////////////////////////////////////////////////////////////////////////// //
254 final PlayerPowerup findPowerup (name id) {
255   foreach (PlayerPowerup pp; powerups) if (pp.id == id) return pp;
256   return none;
260 final bool setPowerupState (name id, bool active) {
261   auto pp = findPowerup(id);
262   if (!pp) return false;
263   return (active ? pp.onActivate() : pp.onDeactivate());
267 final bool togglePowerupState (name id) {
268   auto pp = findPowerup(id);
269   if (!pp) return false;
270   return (pp.active ? pp.onDeactivate() : pp.onActivate());
274 final bool activatePowerup (name id) { return setPowerupState(id, true); }
275 final bool deactivatePowerup (name id) { return setPowerupState(id, false); }
278 final bool isActivePowerup (name id) {
279   auto pp = findPowerup(id);
280   return (pp && pp.active);
284 // ////////////////////////////////////////////////////////////////////////// //
285 override void Destroy () {
286   foreach (PlayerPowerup pp; powerups) delete pp;
287   powerups.length = 0;
291 void unpressAllKeys () {
292   kLeft = false;
293   kLeftPressed = false;
294   kLeftReleased = false;
295   kRight = false;
296   kRightPressed = false;
297   kRightReleased = false;
298   kUp = false;
299   kDown = false;
300   kJump = false;
301   kJumpPressed = false;
302   kJumpReleased = false;
303   kAttack = false;
304   kAttackPressed = false;
305   kAttackReleased = false;
306   kItemPressed = false;
307   kRopePressed = false;
308   kBombPressed = false;
309   kPayPressed = false;
310   kExitPressed = false;
314 // ////////////////////////////////////////////////////////////////////////// //
315 // called on level start too
316 void resurrect () {
317   justSpawned = true;
318   holdArrow = 0;
319   bowStrength = 0;
320   bowArmed = 0;
321   skipCutscenePressed = false;
322   movementBlocked = false;
323   if (global.plife < 1) global.plife = max(1, global.config.scumStartLife);
324   dead = false;
325   xVel = 0;
326   yVel = 0;
327   grav = default.grav;
328   myGrav = default.myGrav;
329   bounced = false;
330   stunned = false;
331   burning = 0;
332   depth = default.depth;
333   status = default.status;
334   fallTimer = 0;
335   stunTimer = 0;
336   wallHurt = 0;
337   pushTimer = 0;
338   whoaTimer = 0;
339   distToNearestLightSource = 999;
340   flying = false;
341   justdied = default.justdied;
342   whipping = false;
343   if (holdItem isa PlayerWeapon) {
344     auto w = holdItem;
345     holdItem = none;
346     w.instanceRemove();
347   }
348   invincible = 0;
349   blink = default.blink;
350   blinkHidden = default.blinkHidden;
351   status = STANDING;
352   characterSprite();
353   active = true;
354   visible = true;
355   unpressAllKeys();
356   level.clearKeysPressRelease();
357   climbSoundTimer = 0;
358   bet = 0;
359   //scrSwitchToPocketItem(forceIfEmpty:false);
363 // ////////////////////////////////////////////////////////////////////////// //
364 bool isExitingSprite () {
365   auto spr = getSprite();
366   return (spr.Name == 'sPExit' || spr.Name == 'sDamselExit' || spr.Name == 'sTunnelExit');
370 // ////////////////////////////////////////////////////////////////////////// //
371 override void playSound (name aname, optional bool unique) {
372   if (unique && global.sndIsPlaying(aname)) return;
373   global.playSound(xCenter, yCenter, aname, relative:true);
377 override bool sndIsPlaying (name aname) {
378   return global.sndIsPlaying(aname, relative:true);
382 override void sndStopSound (name aname) {
383   global.sndStopSound(aname, relative:true);
387 // ////////////////////////////////////////////////////////////////////////// //
388 transient ItemDice currDie;
390 void onDieRolled (ItemDice die) {
391   if (!die.forSale) return;
392   // only law-abiding players can play
393   if (global.thiefLevel > 0 || global.murderer) return;
394   if (bet == 0) return;
395   auto odie = currDie;
396   currDie = die;
397   level.forEachObject(delegate bool (MapObject o) {
398     MonsterShopkeeper sc = MonsterShopkeeper(o);
399     if (sc && !sc.dead && !sc.angered) return sc.onDiePlayed(self, currDie);
400     return false;
401   });
402   currDie = odie;
406 // ////////////////////////////////////////////////////////////////////////// //
407 override bool onExplosionTouch (MapObject xplo) {
408   //writeln("PlayerPawn: on explo touch! ", invincible);
409   if (invincible) return false;
410   if (global.config.scumExplosionHurt) {
411     global.plife -= global.config.explosionDmg;
412     if (!dead && global.plife <= 0 /*&& isRealLevel()*/) level.addDeath('explosion');
413     burning = 50;
414     if (global.config.scumExplosionStun) {
415       stunned = true;
416       stunTimer = 100;
417     }
418     scrCreateBlood(ix, iy, 1);
419   }
420   if (xplo.ix < ix) xVel = global.randOther(4, 6); else xVel = -global.randOther(4, 6);
421   yVel = -6;
422   return true;
426 // ////////////////////////////////////////////////////////////////////////// //
427 // start new game when exiting from title, and process other custom exits
428 void scrPlayerExit () {
429   level.playerExited = true;
430   status = STANDING;
431   characterSprite();
435 // ////////////////////////////////////////////////////////////////////////// //
436 bool scrHideItemToPocket (optional bool forBombOrRope) {
437   if (!holdItem) return true;
438   if (holdItem isa PlayerWeapon) return false;
439   if (holdItem.forSale) return false;
440   if (!forBombOrRope) {
441     if (holdItem isa ItemBall) return false;
442   }
444   // cannot hide armed bomb
445   ItemBomb bomb = ItemBomb(holdItem);
446   if (bomb && bomb.armed) return false;
447   if (bomb || holdItem isa ItemRopeThrow) {
448     holdItem.instanceRemove();
449     holdItem = none;
450     return true;
451   }
453   // cannot hide enemy
454   if (holdItem isa MapEnemy) return false;
455   //writeln("hiding: '", GetClassName(holdItem.Class), "'");
457   if (pickedItem) FatalError("we are already holding '%n'", GetClassName(pickedItem.Class));
458   pickedItem = holdItem;
459   holdItem = none;
460   pickedItem.active = false;
461   pickedItem.visible = false;
462   if (pickedItem.heldBy) FatalError("oooops (scrHideItemToPocket)");
463   return true;
467 bool scrSwitchToBombs () {
468   if (holdItem isa PlayerWeapon) return false;
470   if (global.bombs < 1) return false;
471   if (ItemBomb(holdItem)) return true;
472   if (!scrHideItemToPocket(forBombOrRope:true)) return false;
474   ItemBomb bomb = ItemBomb(level.MakeMapObject(ix, iy, 'oBomb'));
475   if (!bomb) return false;
476   bomb.setSticky(global.stickyBombsActive);
477   holdItem = bomb;
478   whoaTimer = whoaTimerMax;
479   return true;
483 bool scrSwitchToStickyBombs () {
484   if (holdItem isa PlayerWeapon) return false;
485   if (!global.hasStickyBombs) {
486     global.stickyBombsActive = false;
487     return false;
488   }
490   global.stickyBombsActive = !global.stickyBombsActive;
491   return true;
495 bool scrSwitchToRopes () {
496   if (holdItem isa PlayerWeapon) return false;
498   if (global.rope < 1) return false;
499   if (ItemRopeThrow(holdItem)) return true;
500   if (!scrHideItemToPocket(forBombOrRope:true)) return false;
502   ItemRopeThrow rope = ItemRopeThrow(level.MakeMapObject(ix, iy, 'oRopeThrow'));
503   if (!rope) return false;
504   holdItem = rope;
505   whoaTimer = whoaTimerMax;
506   return true;
510 bool isHoldingBombOrRope () {
511   auto hit = holdItem;
512   if (!hit) return false;
513   return (hit isa ItemBomb || hit isa ItemRopeThrow);
517 bool isHoldingBomb () {
518   auto hit = holdItem;
519   if (!hit) return false;
520   return (hit isa ItemBomb);
524 bool isHoldingArmedBomb () {
525   auto hit = ItemBomb(holdItem);
526   if (!hit) return false;
527   return hit.armed;
531 bool isHoldingRope () {
532   auto hit = holdItem;
533   if (!hit) return false;
534   return (hit isa ItemRopeThrow);
538 bool scrSwitchToPocketItem (bool forceIfEmpty) {
539   if (holdItem isa PlayerWeapon) return false;
540   if (holdItem && holdItem.forSale) return false;
542   if (holdItem == pickedItem) { pickedItem = none; whoaTimer = whoaTimerMax; return true; }
544   if (!forceIfEmpty && !pickedItem) return false;
546   // destroy currently holded item if it is a bomb or a rope
547   if (holdItem) {
548     // you cannot do it with an armed bomb
549     if (holdItem isa MapEnemy) return false; // cannot hide an enemy
550     ItemBomb bomb = ItemBomb(holdItem);
551     if (bomb && bomb.armed) return false;
552     if (bomb || holdItem isa ItemRopeThrow) {
553       //delete holdItem;
554       holdItem.instanceRemove();
555       holdItem = none;
556     } /*else {
557       if (pickedItem) {
558         writeln(va("cannot switch to pocket item while carrying '%n' ('%n' is in pocket, why?)", GetClassName(holdItem.Class), GetClassName(pickedItem.Class)));
559         return false;
560       }
561     }*/
562   }
564   auto oldHold = holdItem;
565   holdItem = pickedItem;
566   pickedItem = oldHold;
567   // all flag management is done in property handler
568   if (oldHold) {
569     oldHold.active = false;
570     oldHold.visible = false;
571   }
572   whoaTimer = whoaTimerMax;
573   return true;
577 bool scrSwitchToNextItem () {
578   if (holdItem isa PlayerWeapon) return false;
579   if (holdItem && holdItem.forSale) return false;
581   // holding a bomb?
582   if (ItemBomb(holdItem)) {
583     if (ItemBomb(holdItem).armed) return false; // cannot switch out of armed bomb
584     if (scrSwitchToRopes()) return true;
585     return scrSwitchToPocketItem(forceIfEmpty:true);
586   }
588   // holding a rope?
589   if (ItemRopeThrow(holdItem)) {
590     if (scrSwitchToPocketItem(forceIfEmpty:true)) return true;
591     if (scrSwitchToBombs()) return true;
592     return scrHideItemToPocket();
593   }
595   // either nothing, or normal item
596   bool tryPocket = !!holdItem;
597   if (scrSwitchToBombs()) return true;
598   if (scrSwitchToRopes()) return true;
599   if (holdItem isa ItemBall) return false;
600   if (tryPocket) return scrSwitchToPocketItem(forceIfEmpty:true);
601   return false;
605 // ////////////////////////////////////////////////////////////////////////// //
606 bool scrPickupItem (MapObject obj) {
607   if (holdItem isa PlayerWeapon) return false;
609   if (!obj) return false;
611   if (holdItem) {
612     if (pickedItem) return false;
613     if (isHoldingArmedBomb()) return false;
614     if (isHoldingBombOrRope()) {
615       if (!scrSwitchToPocketItem(forceIfEmpty:true)) return false;
616     }
617     if (holdItem) return false;
618   } else {
619     // just in case
620     if (pickedItem) return false;
621   }
623        if (obj isa ItemBomb && !ItemBomb(obj).armed) ++global.bombs;
624   else if (obj isa ItemRopeThrow) ++global.rope;
625   holdItem = obj;
626   whoaTimer = whoaTimerMax;
627   obj.onPickedUp(self);
628   return true;
632 // drop currently held item
633 bool scrDropItem (LostCause cause, optional float xVel, optional float yVel) {
634   if (holdItem isa PlayerWeapon) return false;
636   if (!holdItem) return false;
638   if (!onLoosingHeldItem(cause)) return false;
640   auto hi = holdItem;
641   holdItem = none;
643   if (!hi.onLostAsHeldItem(self, cause, xVel!optional, yVel!optional)) {
644     // oops, regain it
645     holdItem = hi;
646     return false;
647   }
649        if (hi isa ItemRopeThrow) global.rope = max(0, global.rope-1);
650   else if (hi isa ItemBomb && !ItemBomb(hi).armed) global.bombs = max(0, global.bombs-1);
652   madeOffer = false;
654   scrSwitchToPocketItem(forceIfEmpty:true);
655   return true;
659 // ////////////////////////////////////////////////////////////////////////// //
660 void scrUseThrowIt (MapObject it) {
661   if (!it) return;
663   it.onBeforeThrowBy(self);
665   it.resaleValue = 0;
666   it.makeSafe();
668   if (dir == Dir.Left) {
669     it.xVel = (it.heavy ? -4+xVel : -8+xVel);
670     //foreach (; 0..8) if (level.isSolidAtPoint(ix-8, iy)) it.shiftX(1);
671     //while (!level.isSolidAtPoint(ix-8, iy)) it.shiftX(1); // prevent getting stuck in wall
672   } else if (dir == Dir.Right) {
673     it.xVel = (it.heavy ? 4+xVel : 8+xVel);
674     //foreach (; 0..8) if (level.isSolidAtPoint(ix+8, iy)) it.shiftX(-1);
675     //while (!level.isSolidAtPoint(ix+8, iy)) it.shiftX(-1); // prevent getting stuck in wall
676   }
677   it.yVel = (it.heavy ? (kUp ? -4 : -2) : (kUp ? -9 : -3));
678   if (kDown || scrPlayerIsDucking()) {
679     if (platformCharacterIs(ON_GROUND)) {
680       it.shiftY(-2);
681       it.xVel *= 0.6;
682       it.yVel = 0.5;
683     } else {
684       it.yVel = 3;
685     }
686   } else if (!global.hasMitt) {
687     if (dir == Dir.Left) {
688       if (level.isSolidAtPoint(ix-8, iy-10)) {
689         it.yVel = 0;
690         it.xVel -= 1;
691       }
692     } else if (dir == Dir.Right) {
693       if (level.isSolidAtPoint(ix+8, iy-10)) {
694         it.yVel = 0;
695         it.xVel += 1;
696       }
697     }
698   }
700   if (global.hasMitt && !scrPlayerIsDucking()) {
701     it.xVel += (it.xVel < 0 ? -6 : 6);
702          if (!kUp && !kDown) it.yVel = -0.4;
703     else if (kDown) it.yVel = 6;
704     it.myGrav = 0.1;
705   }
707   // prevent getting stuck in a wall
708   if (it.isCollision()) {
709     //foreach (; 0..8) if (level.isSolidAtPoint(ix-8, iy)) it.shiftX(1);
710     if (it.xVel < 0) {
711       if (level.isSolidAtPoint(it.ix-8, it.iy)) it.shiftX(8);
712     } else if (it.xVel > 0) {
713       if (level.isSolidAtPoint(it.ix+8, it.iy)) it.shiftX(-8);
714     } else if (it.isCollision()) {
715       int dx = (it.isCollisionLeft(0) ? 1 : it.isCollisionRight(0) ? -1 : 0);
716       if (dx) {
717         foreach (; 0..8) {
718           it.shiftX(dx);
719           if (!it.isCollision()) break;
720         }
721       }
722     }
723     /*
724     int dx = 1;
725     while (dx > 8 && it.isCollisionLeft(dx)) ++dx;
726     if (dx < 8) it.shiftX(8);
727     else {
728       dx = 1;
729       while (dx > 8 && it.isCollisionRight(dx)) ++dx;
730       if (dx < 8) it.shiftX(-8);
731     }
732     */
733   }
735   /*
736   if (it.sprite_index == sBombBag ||
737       it.sprite_index == sBombBox ||
738       it.sprite_index == sRopePile)
739   {
740       // do nothing
741   } else*/ {
742     playSound('sndThrow');
743   }
747 bool scrUseThrowItem () {
748   if (holdItem isa PlayerWeapon) return false;
750   auto hitem = holdItem;
752   if (!hitem) return false;
753   if (!onLoosingHeldItem(LostCause.Unknown)) return false;
755   holdItem = none;
756   madeOffer = false;
758   scrUseThrowIt(hitem);
760   // if we throwing away armed bomb, get previous item back into our hands
761   //FIXME
762   if (/*ItemBomb(hitem)*/isHoldingBombOrRope()) scrSwitchToPocketItem(forceIfEmpty:false);
764   return true;
768 // ////////////////////////////////////////////////////////////////////////// //
769 bool scrPlayerIsDucking () {
770   if (dead) return false;
771   auto spr = getSprite();
772   //if (!spr) return false;
773   return
774     spr.Name == 'sDuckLeft' ||
775     spr.Name == 'sCrawlLeft' ||
776     spr.Name == 'sDamselDuckL' ||
777     spr.Name == 'sDamselCrawlL' ||
778     spr.Name == 'sTunnelDuckL' ||
779     spr.Name == 'sTunnelCrawlL';
783 bool scrFireBow () {
784   if (holdItem !isa ItemWeaponBow) return false;
785   if (!bowArmed) return false;
786   if (!holdItem.onTryUseItem(self)) return false;
787   return true;
791 void scrUsePutItOnGroundHelper (MapObject it, optional float xVelMult, optional float yVelNew) {
792   if (!it) return;
794   if (!specified_xVelMult) xVelMult = 0.4;
795   if (!specified_yVelNew) yVelNew = 0.5;
797   //writeln("putting '", GetClassName(hi.Class), "'");
799   if (dir == Dir.Left) {
800     it.xVel = (it.heavy ? -4 : -8);
801   } else if (dir == Dir.Right) {
802     it.xVel = (it.heavy ? 4 : 8);
803   }
804   it.xVel += xVel;
805   it.xVel *= xVelMult;
806   it.yVel = yVelNew;
808   //hi.fltx = ix;
809   it.flty = iy+2;
810   if (ItemGoldIdol(it)) it.flty = iy;
812   foreach (; 0..16) {
813     if (it.isCollisionBottom(0) && !it.isCollisionTop(1)) {
814       it.flty -= 1;
815     } else {
816       break;
817     }
818   }
820   foreach (; 0..16) {
821     if (it.isCollisionLeft(0)) {
822       if (it.isCollisionRight(1)) break;
823       it.fltx += 1;
824     } else if (it.isCollisionRight(0)) {
825       if (it.isCollisionLeft(1)) break;
826       it.fltx -= 1;
827     } else {
828       break;
829     }
830   }
834 // put item which player holds in his hands on the ground if player is ducking
835 // return `true` if item was put
836 bool scrUsePutItemOnGround (optional float xVelMult, optional float yVelNew) {
837   if (holdItem isa PlayerWeapon) return false;
839   auto hi = holdItem;
840   if (!hi || !scrPlayerIsDucking()) return false;
842   if (!onLoosingHeldItem(LostCause.Unknown)) return false;
844   //writeln("putting '", GetClassName(hi.Class), "'");
846   if (global.bombs > 0) {
847     auto bomb = ItemBomb(hi);
848     if (bomb && !bomb.armed) global.bombs -= 1;
849   }
851   if (global.rope > 0) {
852     auto rope = ItemRopeThrow(hi);
853     if (rope) {
854       global.rope -= 1;
855       rope.falling = false;
856       rope.flying = false;
857     }
858   }
860   holdItem = none;
861   hi.resaleValue = 0;
862   madeOffer = false;
863   hi.makeSafe();
865   scrUsePutItOnGroundHelper(hi, xVelMult!optional, yVelNew!optional);
867   return true;
871 bool launchRope (bool goDown, bool doDrop) {
872   if (global.rope < 1) {
873     global.rope = 0;
874     if (ItemRopeThrow(holdItem)) scrSwitchToPocketItem(forceIfEmpty:false);
875     return false;
876   }
878   --global.rope;
880   bool wasHeld = false;
881   ItemRopeThrow rp = ItemRopeThrow(holdItem);
882   int xdelta = (doDrop ? 12 : 16)*(dir == Dir.Left ? -1 : 1);
883   if (rp) {
884     //FIXME: call handler
885     wasHeld = true;
886     holdItem = none;
887     rp.setXY(ix+xdelta, iy);
888   } else {
889     rp = ItemRopeThrow(level.MakeMapObject(ix+xdelta, iy, 'oRopeThrow'));
890   }
891   if (rp.heldBy) FatalError("PlayerPawn::launchRope: hold management fucked");
892   rp.armed = true;
893   rp.flying = false;
894   //rp.resaleValue = 0;
896   rp.px = ix;
897   rp.py = iy;
898   if (platformCharacterIs(ON_GROUND)) rp.startY = iy; // YASM 1.7
900   if (!goDown) {
901     // launch rope up
902     rp.setX(fltx);
903     rp.xVel = 0;
904     rp.yVel = -12;
905   } else {
906     // launch rope down
907     bool t = true;
908     rp.moveSnap(16, 1);
909     if (ix < rp.ix) {
910       if (!level.isSolidAtPoint(ix+(doDrop ? 2 : 8), iy)) { //2
911              if (!level.checkTilesInRect(rp.ix-8, rp.iy, 2, 17)) rp.shiftX(-8);
912         else if (!level.checkTilesInRect(rp.ix+7, rp.iy, 2, 17)) rp.shiftX(8);
913         else t = false;
914       } else {
915         t = false;
916       }
917     } else if (!level.isSolidAtPoint(ix-(doDrop ? 2 : 8), iy)) { //2
918            if (!level.checkTilesInRect(rp.ix+7, rp.iy, 2, 17)) rp.shiftX(8);
919       else if (!level.checkTilesInRect(rp.ix-8, rp.iy, 2, 17)) rp.shiftX(-8);
920       else t = false;
921     } else {
922       t = false;
923     }
924     //writeln("t=", t);
925     if (!t) {
926       // cannot launch rope
927       /* was commented in the original
928       if (oPlayer1.facing == 18) {
929         obj = instance_create(oPlayer1.x-4, oPlayer1.y+2, oRopeThrow);
930         obj.xVel = -3.2;
931       } else {
932         obj = instance_create(oPlayer1.x+4, oPlayer1.y+2, oRopeThrow);
933         obj.xVel = 3.2;
934       }
935       obj.yVel = 0.5;
936       */
937       //writeln("!!! goDown=", goDown, "; doDrop=", doDrop, "; wasHeld=", wasHeld);
938       rp.armed = false;
939       rp.flying = false;
940       if (!wasHeld) doDrop = true;
941       if (doDrop) {
942         /*
943         rp.setXY(ix, iy);
944         if (dir == Dir.Left) rp.xVel = -3.2; else rp.xVel = 3.2;
945         rp.yVel = 0.5;
946         */
947         rp.forceFixHoldCoords(self);
948         if (goDown) {
949           scrUsePutItOnGroundHelper(rp);
950         } else {
951           scrUseThrowIt(rp);
952         }
953         if (wasHeld) scrSwitchToPocketItem(forceIfEmpty:false);
954       } else {
955         //writeln("NO DROP!");
956         ++global.rope;
957         if (wasHeld) {
958           // regain it
959           //rp.resaleValue = 1; //k8:???
960           holdItem = rp;
961         } else {
962           rp.instanceRemove();
963           if (wasHeld) scrSwitchToPocketItem(forceIfEmpty:false);
964         }
965       }
966       return false;
967     } else {
968       level.MakeMapObject(rp.ix, rp.iy, 'oRopeTop');
969       rp.armed = false;
970       rp.falling = true;
971       rp.xVel = 0;
972       rp.yVel = 0;
973     }
974   }
975   if (wasHeld) scrSwitchToPocketItem(forceIfEmpty:false);
976   playSound('sndThrow');
977   return true;
981 bool scrLaunchBomb () {
982   if (whipping || global.bombs < 1) return false;
983   --global.bombs;
985   ItemBomb bomb = ItemBomb(level.MakeMapObject(ix, iy, 'oBomb'));
986   if (!bomb) return false;
987   bomb.forceFixHoldCoords(self);
988   bomb.setSticky(global.stickyBombsActive);
989   bomb.armIt(80);
990   bomb.resaleValue = 0;
992   if (kDown || scrPlayerIsDucking()) {
993     scrUsePutItOnGroundHelper(bomb);
994   } else {
995     scrUseThrowIt(bomb);
996   }
998   return true;
1002 bool scrUseItem () {
1003   auto it = holdItem;
1004   if (!it) return false;
1005   //writeln(GetClassName(holdItem.Class));
1007   //auto spr = holdItem.getSprite();
1008   /+
1009   } else if (holdItem.type == "Sceptre") {
1010     if (kDown) scrUsePutItemOnGround(0.4, 0.5);
1011     if (firing == 0 && !scrPlayerIsDucking()) {
1012       if (facing == LEFT) {
1013         asleft = true;
1014         xsgn = -1;
1015       } else {
1016         asleft = false;
1017         xsgn = 1;
1018       }
1019       xofs = 12*xsgn;
1020       repeat(3) {
1021         obj = instance_create(x+xofs, y+4, oPsychicCreateP);
1022         obj.xVel = xsgn*rand(1, 3);
1023         obj.yVel = -random(2);
1024       }
1025       obj = instance_create(x+xofs, y-2, oPsychicWaveP);
1026       obj.xVel = xsgn*6;
1027       playSound(global.sndPsychic);
1028       firing = firingPistolMax;
1029     }
1030   } else if (holdItem.type == "Teleporter II") {
1031     scrUseTeleporter2();
1032   } else if (holdItem.type == "Bow") {
1033     if (kDown) {
1034       scrUsePutItemOnGround(0.4, 0.5);
1035     } else if (firing == 0 && !scrPlayerIsDucking() && !bowArmed && global.arrows > 0) {
1036       bowArmed = true;
1037       playSound(global.sndBowPull);
1038     } else if (global.arrows <= 0) {
1039       global.message = "I'M OUT OF ARROWS!";
1040       global.message2 = "";
1041       global.messageTimer = 80;
1042     }
1043   } else {
1044   +/
1047   if (whipping) return false;
1049   if (kDown) {
1050     scrUsePutItemOnGround();
1051     return true;
1052   }
1054   // you cannot throw away shop items, but can throw dices
1055   if (it.forSale && it !isa ItemDice) {
1056     if (!level.isInShop(ix/16, iy/16)) {
1057       it.forSale = false;
1058     } else {
1059       // allow throw/use shop items
1060       //return false;
1061     }
1062   }
1064   if (!it.onTryUseItem(self)) {
1065     // throw item
1066     scrUseThrowItem();
1067   }
1069   return true;
1073 // ////////////////////////////////////////////////////////////////////////// //
1074 // called by characterStepEvent
1075 // help player jump up through one block wide gaps by nudging them to one side so they don't hit their head
1076 void scrJumpHelper () {
1077   int d = 4; // max distance to nudge player
1078   int x = ix, y = iy;
1079   if (!level.checkTilesInRect(x, y-12, 1, 7)) {
1080     if (level.checkTilesInRect(x-5, y-12, 1, 7) &&
1081         level.checkTilesInRect(x+14, y-12, 1, 7))
1082     {
1083       while (d > 0 && level.checkTilesInRect(x-5, y-12, 1, 7)) { ++x; shiftX(1); --d; }
1084     } else if (level.checkTilesInRect(x+5, y-12, 1, 7) &&
1085                level.checkTilesInRect(x-14, y-12, 1, 7))
1086     {
1087       while (d > 0 && level.checkTilesInRect(x+5, y-12, 1, 7)) { --x; shiftX(-1); --d; }
1088     }
1089   }
1090   /+
1091   if (!collision_line(x, y-6, x, y-12, oSolid, 0, 0)) {
1092     if (collision_line(x-5, y-6, x-5, y-12, oSolid, 0, 0) &&
1093         collision_line(x+14, y-6, x+14, y-12, oSolid, 0, 0))
1094     {
1095       while (collision_line(x-5, y-6, x-5, y-12, oSolid, 0, 0) && d > 0) {
1096         x += 1;
1097         d -= 1;
1098       }
1099     }
1100     else if (collision_line(x+5, y-6, x+5, y-12, oSolid, 0, 0) and
1101              collision_line(x-14, y-6, x-14, y-12, oSolid, 0, 0))
1102     {
1103       while (collision_line(x+5, y-6, x+5, y-12, oSolid, 0, 0) && d > 0) {
1104         x -= 1;
1105         d -= 1;
1106       }
1107     }
1108   }
1109   +/
1113 // ////////////////////////////////////////////////////////////////////////// //
1115  * Returns whether a GENERAL trait about a character is true.
1116  * Only the platform character should run this script.
1118  * `tp` can be one of the following:
1119  *   ON_GROUND
1120  *   IN_AIR
1121  *   ON_LADDER
1122  */
1123 final bool platformCharacterIs (int tp) {
1124   if (tp == ON_GROUND && (status == RUNNING || status == STANDING || status == DUCKING || status == LOOKING_UP)) return true;
1125   if (tp == IN_AIR && (status == JUMPING || status == FALLING)) return true;
1126   if (tp == ON_LADDER && status == CLIMBING) return true;
1127   return false;
1131 // ////////////////////////////////////////////////////////////////////////// //
1132 // sets the sprite of the character depending on his/her status
1133 final void characterSprite () {
1134   if (status == STOPPED) {
1135          if (global.isDamsel) setSprite('sDamselLeft');
1136     else if (global.isTunnelMan) setSprite('sTunnelLeft');
1137     else setSprite('sStandLeft');
1138     return;
1139   }
1141   int x = ix, y = iy;
1142   if (global.isTunnelMan && !stunned && !whipping) {
1143     // Tunnel Man
1144     if (status == STANDING) {
1145       if (!level.isSolidAtPoint(x-2, y+9)) {
1146         imageSpeed = 0.6;
1147         setSprite('sTunnelWhoaL');
1148       } else {
1149         setSprite('sTunnelLeft');
1150       }
1151     }
1152     if (status == RUNNING) {
1153       if (kUp) setSprite('sTunnelLookRunL'); else setSprite('sTunnelRunL');
1154     }
1155     if (status == DUCKING) {
1156            if (xVel == 0) setSprite('sTunnelDuckL');
1157       else if (fabs(xVel) < 3) setSprite('sTunnelCrawlL');
1158       else setSprite('sTunnelRunL');
1159     }
1160     if (status == LOOKING_UP) {
1161       if (fabs(xVel) > 0) setSprite('sTunnelRunL'); else setSprite('sTunnelLookL');
1162     }
1163     if (status == JUMPING) setSprite('sTunnelJumpL');
1164     if (status == FALLING && statePrev == FALLING && statePrevPrev == FALLING) setSprite('sTunnelFallL');
1165     if (status == HANGING) setSprite('sTunnelHangL');
1166     if (pushTimer > 20) setSprite('sTunnelPushL');
1167     if (status == DUCKTOHANG) setSprite('sTunnelDtHL');
1168     if (status == CLIMBING) {
1169       if (level.isRopeAtPoint(x, y)) {
1170         if (kDown) setSprite('sTunnelClimb3'); else setSprite('sTunnelClimb2');
1171       } else {
1172         setSprite('sTunnelClimb');
1173       }
1174     }
1175   } else if (global.isDamsel && !stunned && !whipping) {
1176     // Damsel
1177     if (status == STANDING) {
1178       if (!level.isSolidAtPoint(x-2, y+9)) {
1179         imageSpeed = 0.6;
1180         setSprite('sDamselWhoaL');
1181         /* was commented out in the original
1182         if (holdItem && whoaTimer < 1) {
1183           holdItem.held = false;
1184           if (facing == LEFT) holdItem.xVel = -2; else holdItem.xVel = 2;
1185           if (holdItem.type == "Damsel") playSound('sndDamsel');
1186           if (holdItem.type == pickupItemType) { holdItem = 0; pickupItemType = ""; } else scrSwitchToPocketItem();
1187         }
1188         */
1189       } else {
1190         setSprite('sDamselLeft');
1191       }
1192     }
1193     if (status == RUNNING) {
1194       if (kUp) setSprite('sDamselRunL'); else setSprite('sDamselRunL');
1195     }
1196     if (status == DUCKING) {
1197            if (xVel == 0) setSprite('sDamselDuckL');
1198       else if (fabs(xVel) < 3) setSprite('sDamselCrawlL');
1199       else setSprite('sDamselRunL');
1200     }
1201     if (status == LOOKING_UP) {
1202       if (fabs(xVel) > 0) setSprite('sDamselRunL'); else setSprite('sDamselLookL');
1203     }
1204     if (status == JUMPING) setSprite('sDamselDieLR');
1205     if (status == FALLING && statePrev == FALLING && statePrevPrev == FALLING) setSprite('sDamselFallL');
1206     if (status == HANGING) setSprite('sDamselHangL');
1207     if (pushTimer > 20) setSprite('sDamselPushL');
1208     if (status == DUCKTOHANG) setSprite('sDamselDtHL');
1209     if (status == CLIMBING) {
1210       if (level.isRopeAtPoint(x, y)) {
1211         if (kDown) setSprite('sDamselClimb3'); else setSprite('sDamselClimb2');
1212       } else {
1213         setSprite('sDamselClimb');
1214       }
1215     }
1216   } else if (!stunned && !whipping) {
1217     // Spelunker
1218     if (status == STANDING) {
1219       if (!level.checkTileAtPoint(x-(dir == Dir.Left ? 2 : 0), y+9, &level.cbCollisionForWhoa)) {
1220         imageSpeed = 0.6;
1221         setSprite('sWhoaLeft');
1222         /* was commented out in the original
1223         if (holdItem && whoaTimer < 1) {
1224           holdItem.held = false;
1225           if (facing == LEFT) holdItem.xVel = -2; else holdItem.xVel = 2;
1226           if (holdItem.type == "Damsel") playSound('sndDamsel');
1227           if (holdItem.type == pickupItemType) { holdItem = 0; pickupItemType = ""; } else scrSwitchToPocketItem();
1228         }
1229         */
1230       } else {
1231         setSprite('sStandLeft');
1232       }
1233     }
1234     if (status == RUNNING) {
1235       if (kUp) setSprite('sLookRunL'); else setSprite('sRunLeft');
1236     }
1237     if (status == DUCKING) {
1238            if (xVel == 0) setSprite('sDuckLeft');
1239       else if (fabs(xVel) < 3) setSprite('sCrawlLeft');
1240       else setSprite('sRunLeft');
1241     }
1242     if (status == LOOKING_UP) {
1243       if (fabs(xVel) > 0) setSprite('sLookRunL'); else setSprite('sLookLeft');
1244     }
1245     if (status == JUMPING) setSprite('sJumpLeft');
1246     if (status == FALLING && statePrev == FALLING && statePrevPrev == FALLING) setSprite('sFallLeft');
1247     if (status == HANGING) setSprite('sHangLeft');
1248     if (pushTimer > 20) setSprite('sPushLeft');
1249     if (status == CLIMBING) {
1250       if (level.isRopeAtPoint(x, y)) {
1251         if (kDown) setSprite('sClimbUp3'); else setSprite('sClimbUp2');
1252       } else {
1253         setSprite('sClimbUp');
1254       }
1255     }
1256     if (status == DUCKTOHANG) setSprite('sDuckToHangL');
1257   }
1262 // ////////////////////////////////////////////////////////////////////////// //
1263 void addScore (int delta) {
1264   if (!level.isNormalLevel()) return;
1265   //score += delta;
1266   if (delta == 0) return;
1267   level.stats.addMoney(delta);
1268   if (delta > 0) {
1269     level.xmoney += delta;
1270     level.collectCounter = min(100, level.collectCounter+20);
1271   }
1275 // ////////////////////////////////////////////////////////////////////////// //
1276 // for dead players too
1277 // first, the code will call `onObjectTouched()` for player
1278 // if it returned `false`, the code will call `obj.onTouchedByPlayer()`
1279 // note that player's handler is called *after* its frame thinker,
1280 // but object handler is called *before* frame thinker for the object
1281 // i.e. return `true` to block calling `obj.onTouchedByPlayer()`,
1282 // (but NOT object thinker)
1283 bool onObjectTouched (MapObject obj) {
1284   // is player dead?
1285   if (dead) return false;
1287   if (obj isa ItemProjectileArrow && holdItem isa ItemWeaponBow && !stunned && global.arrows < 99) {
1288     if (fabs(obj.xVel) < 1 && fabs(obj.yVel) < 1 && !obj.stuck) {
1289       ++global.arrows;
1290       playSound('sndPickup');
1291       obj.instanceRemove();
1292       return true;
1293     }
1294   }
1296   // collect treasure
1297   auto treasure = ItemTreasure(obj);
1298   if (treasure && treasure.canCollect) {
1299     if (treasure.value) addScore(treasure.value);
1300     treasure.onCollected(self); // various other effects
1301     playSound(treasure.soundName);
1302     treasure.instanceRemove();
1303     return true;
1304   }
1306   // collect blood
1307   if (global.hasKapala && obj isa MapObjBlood) {
1308     global.bloodLevel += 1;
1309     level.MakeMapObject(obj.ix, obj.iy, 'oBloodSpark');
1310     obj.instanceRemove();
1312     if (global.bloodLevel > 8) {
1313       global.bloodLevel = 0;
1314       global.plife += 1;
1315       level.MakeMapObject(ix, iy-8, 'oHeart');
1316       playSound('sndKiss');
1317     }
1319     if (redColor < 55) redColor += 5;
1320     redToggle = false;
1321   }
1323   // other objects will take care of themselves
1324   return false;
1328 // return `false` to prevent
1329 // holdItem is valid
1330 bool onLoosingHeldItem (LostCause cause) {
1331   if (level.inWinCutscene != 0) return false;
1332   return true;
1336 // ////////////////////////////////////////////////////////////////////////// //
1337 // k8: don't even ask me! the following mess is almost straightforward port of the original Derek's code!
1338 private final void closeCape () {
1339   auto pp = PPCape(findPowerup('Cape'));
1340   if (pp) pp.open = false;
1344 private final void switchCape () {
1345   auto pp = PPCape(findPowerup('Cape'));
1346   if (pp) pp.open = !pp.open;
1350 final bool isCapeActiveAndOpen () {
1351   auto pp = PPCape(findPowerup('Cape'));
1352   return (pp && pp.active && pp.open);
1356 final bool isParachuteActive () {
1357   auto pp = findPowerup('Parachute');
1358   return (pp && pp.active);
1362 // ////////////////////////////////////////////////////////////////////////// //
1363 // for cutscenes
1364 bool checkSkipCutScene () {
1365   if (skipCutscenePressed) {
1366     return level.isKeyReleased(GameConfig::Key.Pay);
1367   } else {
1368     skipCutscenePressed = level.isKeyPressed(GameConfig::Key.Pay);
1369     return false;
1370   }
1373 int transKissTimer;
1376 bool forcePlayerControls () {
1377   if (level.inWinCutscene) {
1378     unpressAllKeys();
1379     level.winCutscenePlayerControl(self);
1380     return true;
1381   } else if (level.levelKind == GameLevel::LevelKind.Transition) {
1382     unpressAllKeys();
1384     if (checkSkipCutScene()) {
1385       level.playerExited = true;
1386       return true;
1387     }
1389     auto door = level.checkTileAtPoint(ix, iy, &level.cbCollisionExitTile);
1390     if (door) {
1391       kExitPressed = true;
1392       return true;
1393     }
1395     if (status == STOPPED) {
1396       if (--transKissTimer > 0) return true;
1397       status = STANDING;
1398     }
1400     transKissTimer = 0;
1401     auto dms = MonsterDamselKiss(level.isObjectAtPoint(ix+8, iy+4, delegate bool (MapObject o) { return (o isa MonsterDamselKiss); }));
1402     if (dms && !dms.kissed) {
1403       status = STOPPED;
1404       xVel = 0;
1405       yVel = 0;
1406       dms.kiss();
1407       transKissTimer = 30;
1408       return true;
1409     }
1411     kRight = true;
1412     kRightPressed = true;
1413     return true;
1414   }
1415   return false;
1419 // ////////////////////////////////////////////////////////////////////////// //
1420 private final void checkControlKeys (SpriteImage spr) {
1421   if (forcePlayerControls()) {
1422     if (movementBlocked) unpressAllKeys();
1423     if (kLeft) kLeftPushedSteps += 1; else kLeftPushedSteps = 0;
1424     if (kRight) kRightPushedSteps += 1; else kRightPushedSteps = 0;
1425     return;
1426   }
1428   kLeft = level.isKeyDown(GameConfig::Key.Left);
1429   if (movementBlocked) kLeft = false;
1430   if (kLeft) kLeftPushedSteps += 1; else kLeftPushedSteps = 0;
1431   kLeftPressed = level.isKeyPressed(GameConfig::Key.Left);
1432   kLeftReleased = level.isKeyReleased(GameConfig::Key.Left);
1434   kRight = level.isKeyDown(GameConfig::Key.Right);
1435   if (movementBlocked) kRight = false;
1436   if (kRight) kRightPushedSteps += 1; else kRightPushedSteps = 0;
1437   kRightPressed = level.isKeyPressed(GameConfig::Key.Right);
1438   kRightReleased = level.isKeyReleased(GameConfig::Key.Right);
1440   kUp = level.isKeyDown(GameConfig::Key.Up);
1441   kDown = level.isKeyDown(GameConfig::Key.Down);
1443   kJump = level.isKeyDown(GameConfig::Key.Jump);
1444   kJumpPressed = level.isKeyPressed(GameConfig::Key.Jump);
1445   kJumpReleased = level.isKeyReleased(GameConfig::Key.Jump);
1447   if (movementBlocked) unpressAllKeys();
1449   if (cantJump > 0) {
1450     kJump = false;
1451     kJumpPressed = false;
1452     kJumpReleased = false;
1453     --cantJump;
1454   } else if (spr && global.isTunnelMan && spr.Name == 'sTunnelAttackL' && !holdItem) {
1455     kJump = false;
1456     kJumpPressed = false;
1457     kJumpReleased = false;
1458     cantJump = max(0, cantJump-1);
1459   }
1461   kAttack = level.isKeyDown(GameConfig::Key.Attack);
1462   kAttackPressed = level.isKeyPressed(GameConfig::Key.Attack);
1463   kAttackReleased = level.isKeyReleased(GameConfig::Key.Attack);
1465   kItemPressed = level.isKeyPressed(GameConfig::Key.Switch);
1466   kRopePressed = level.isKeyPressed(GameConfig::Key.Rope);
1467   kBombPressed = level.isKeyPressed(GameConfig::Key.Bomb);
1469   kPayPressed = level.isKeyPressed(GameConfig::Key.Pay);
1471   if (movementBlocked) unpressAllKeys();
1473   kExitPressed = false;
1474   if (global.config.useDoorWithButton) {
1475     if (kPayPressed) kExitPressed = true;
1476   } else {
1477     if (kUp) kExitPressed = true;
1478   }
1480   if (stunned || dead) {
1481     unpressAllKeys();
1482     //level.clearKeysPressRelease();
1483   }
1487 // ////////////////////////////////////////////////////////////////////////// //
1488 // knock off monkeys that grabbed you
1489 void knockOffMonkeys () {
1490   level.forEachObject(delegate bool (MapObject o) {
1491     auto mk = EnemyMonkey(o);
1492     if (mk && !mk.dead && mk.status == GRAB) {
1493       mk.xVel = global.randOther(0, 1)-global.randOther(0, 1);
1494       mk.yVel = -4;
1495       mk.status = BOUNCE;
1496       mk.vineCounter = 20;
1497       mk.grabCounter = 60;
1498     }
1499     return false;
1500   });
1504 // ////////////////////////////////////////////////////////////////////////// //
1505 final void characterStepEvent () {
1506   if (climbSoundTimer > 0) {
1507     if (--climbSoundTimer == 0) {
1508       playSound(climbSndToggle ? 'sndClimb2' : 'sndClimb1');
1509       climbSndToggle = !climbSndToggle;
1510     }
1511   }
1513   auto spr = getSprite();
1514   checkControlKeys(spr);
1516   float xPrev = fltx, yPrev = flty;
1517   int x = ix, y = iy;
1519   // check collisions in various directions
1520   bool colSolidLeft = !!getPushableLeft(1);
1521   bool colSolidRight = !!getPushableRight(1);
1522   bool colLeft = !!isCollisionLeft(1);
1523   bool colRight = !!isCollisionRight(1);
1524   bool colTop = !!isCollisionTop(1);
1525   bool colBot = !!isCollisionBottom(1);
1526   bool colLadder = !!isCollisionLadder();
1527   bool colPlatBot = !!isCollisionBottom(1, &level.cbCollisionPlatform);
1528   bool colPlat = !!isCollision(&level.cbCollisionPlatform);
1529   bool colWaterTop = !!isCollisionTop(1, &level.cbCollisionWater);
1530   bool colIceBot = !!level.isIceAtPoint(x, y+8);
1532   bool runKey = false;
1533   if (level.isKeyDown(GameConfig::Key.Run)) { runHeld = 100; runKey = true; }
1534   if (level.isKeyDown(GameConfig::Key.Attack) && !whipping) { runHeld += 1; runKey = true; }
1535   if (!runKey || (!kLeft && !kRight)) runHeld = 0;
1537   // allows the character to run left and right
1538   // if state!=DUCKING and state!=LOOKING_UP and state!=CLIMBING
1539   if (status != CLIMBING && status != HANGING) {
1540     if (kLeftReleased && fabs(xVel) < 0.0001) xAcc -= 0.5;
1541     if (kRightReleased && fabs(xVel) < 0.0001) xAcc += 0.5;
1542     if (kLeft && !kRight) {
1543       if (colSolidLeft) {
1544         //xVel = 3; // in orig
1545         if (platformCharacterIs(ON_GROUND) && status != DUCKING) {
1546           xAcc -= 1;
1547           pushTimer += 10;
1548           //playSound('sndPush', unique:true);
1549         }
1550       } else if (kLeftPushedSteps > 2 && (dir == Dir.Left || fabs(xVel) < 0.0001)) {
1551         xAcc -= runAcc;
1552       }
1553       dir = Dir.Left;
1554       //if (platformCharacterIs(ON_GROUND) && fabs(xVel) > 0 && alarm[3] < 1) alarm[3] = floor(16/-xVel);
1555     }
1556     if (kRight && !kLeft) {
1557       if (colSolidRight) {
1558         //xVel = 3; // in orig
1559         if (platformCharacterIs(ON_GROUND) && status != DUCKING) {
1560           xAcc += 1;
1561           pushTimer += 10;
1562           //playSound('sndPush', unique:true);
1563         }
1564       } else if ((kRightPushedSteps > 2 || colSolidLeft) && (dir == Dir.Right || fabs(xVel) < 0.0001)) {
1565         xAcc += runAcc;
1566       }
1567       dir = Dir.Right;
1568       //if (platformCharacterIs(ON_GROUND) && fabs(xVel) > 0 && alarm[3] < 1) alarm[3] = floor(16/xVel);
1569     }
1570   }
1572   // ladders
1573   if (status == CLIMBING) {
1574     closeCape();
1575     kJumped = false;
1576     ladderTimer = 10;
1577     auto ladder = level.isLadderAtPoint(x, y);
1578     if (ladder) { x = ladder.ix+8; setX(x); }
1579     if (kLeft) dir = Dir.Left; else if (kRight) dir = Dir.Right;
1580     if (kUp) {
1581       // checks both ladder and laddertop
1582       if (level.isAnyLadderAtPoint(x, y-8)) {
1583         //writeln("LADDER00! old yAcc=", yAcc, "; climbAcc=", climbAcc, "; new yAcc=", yAcc-climbAcc);
1584         yAcc -= climbAcc;
1585         if (climbSoundTimer < 1) climbSoundTimer = climbSndSpeed;
1586         //!if (alarm[2] < 1) alarm[2] = climbSndSpeed;
1587       } else {
1588         /*
1589         for (int dy = -6; dy > -12; --dy) {
1590           ladder = level.isAnyLadderAtPoint(x, y+dy);
1591           if (ladder) {
1592             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));
1593           }
1594         }
1595         */
1596         /*
1597         auto grid = level.miscTileGrid;
1598         foreach (MapTile t; grid.inCellPix(48, 96, grid.nextTag(), precise:false)) {
1599           writeln("at 48, 96: ", GetClassName(t.Class), "; pos=(", t.ix, ",", t.iy, ")");
1600         }
1601         foreach (MapTile t; grid.inCellPix(48, 94, grid.nextTag(), precise:false)) {
1602           writeln("at 48, 94: ", GetClassName(t.Class), "; pos=(", t.ix, ",", t.iy, ")");
1603         }
1604         foreach (int dy; 90..102) {
1605           ladder = level.isAnyLadderAtPoint(48, dy);
1606           if (ladder) {
1607             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));
1608           }
1609         }
1610         */
1611       }
1612     } else if (kDown) {
1613       // checks both ladder and laddertop
1614       if (level.isAnyLadderAtPoint(x, y+8)) {
1615         yAcc += climbAcc;
1616         //!if (alarm[2] < 1) alarm[2] = climbSndSpeed;
1617         if (climbSoundTimer < 1) climbSoundTimer = climbSndSpeed;
1618       } else {
1619         status = FALLING;
1620       }
1621       if (colBot) status = STANDING;
1622     }
1623     // jump from ladder
1624     if (kJumpPressed && !whipping) {
1625       if (kLeft) xVel = -departLadderXVel; else if (kRight) xVel = departLadderXVel; else xVel = 0;
1626       //yAcc += departLadderYVel;
1627       //k8: was `0.6`, but with `0.4` we can jump onto the wall above, and with `0.6` we cannot
1628       yAcc = 0.4+departLadderYVel; // YASM 1.8.1 Fix for extra air when jumping off ladders due to increased climb speed option
1629       status = JUMPING;
1630       jumpButtonReleased = false;
1631       jumpTime = 0;
1632       ladderTimer = 5;
1633     }
1634   } else {
1635     if (ladderTimer > 0) ladderTimer -= 1;
1636   }
1638   if (platformCharacterIs(IN_AIR) && status != HANGING) yAcc += gravityIntensity;
1640   // player has landed
1641   if ((colBot || colPlatBot) && platformCharacterIs(IN_AIR) && yVel >= 0) {
1642     if (!colPlat || colBot) {
1643       yVel = 0;
1644       yAcc = 0;
1645       status = RUNNING;
1646     }
1647     playSound('sndLand');
1648   }
1649   if ((colBot || colPlatBot) && !colPlat) yVel = 0;
1651   // player has just walked off of the edge of a solid
1652   if (colBot == 0 && (!colPlatBot || colPlat) && platformCharacterIs(ON_GROUND)) {
1653     status = FALLING;
1654     yAcc += grav;
1655     kJumped = true;
1656     if (global.hasGloves) hangCount = 5;
1657   }
1659   if (colTop) {
1660          if (dead || stunned) yVel = -yVel*0.8;
1661     else if (status == JUMPING) yVel = fabs(yVel*0.3);
1662   }
1664   if ((colLeft && dir == Dir.Left) || (colRight && dir == Dir.Right)) {
1665     if (dead || stunned) xVel = -xVel*0.5; else xVel = 0;
1666   }
1668   // jumping
1669   if (kJumpReleased && platformCharacterIs(IN_AIR)) {
1670     kJumped = true;
1671   } else if (platformCharacterIs(ON_GROUND)) {
1672     closeCape();
1673     kJumped = false;
1674   }
1676   MapObject oWeb = none, oBlob = none;
1677   if (kJumpPressed) {
1678     oWeb = level.isObjectAtPoint(x, y, &level.cbIsObjectWeb);
1679     if (!oWeb) oBlob = level.isObjectAtPoint(x, y, &level.cbIsObjectBlob);
1680   }
1682   bool invokeJumpHelper = false;
1684   if (kJumpPressed && oWeb) {
1685     ItemWeb(oWeb).tear(1);
1686     yAcc += initialJumpAcc*2;
1687     yVel -= 3;
1688     xAcc += xVel/2;
1690     status = JUMPING;
1691     jumpButtonReleased = false;
1692     jumpTime = 0;
1694     grav = gravNorm;
1695     invokeJumpHelper = true;
1696   } else if (kJumpPressed && oBlob) {
1697     oBlob.hp -= 5;
1698     scrCreateBloblets(oBlob.x0+8, oBlob.y0+8, 1);
1699     playSound('sndHit');
1700     yAcc += initialJumpAcc*2;
1701     yVel -= 2;
1702     xAcc += xVel/2;
1703     status = JUMPING;
1704     jumpButtonReleased = false; // k8: was `jumpButtonRelease`
1705     jumpTime = 0;
1706     invokeJumpHelper = true;
1707   } else if (kJumpPressed && colWaterTop) {
1708     yAcc += initialJumpAcc*2;
1709     yVel -= 3;
1710     xAcc += xVel/2;
1712     status = JUMPING;
1713     jumpButtonReleased = false;
1714     jumpTime = 0;
1716     grav = gravNorm;
1717     invokeJumpHelper = true;
1718   } else if (global.hasCape && kJumpPressed && kJumped && platformCharacterIs(IN_AIR)) {
1719     switchCape();
1720   } else if (global.hasJetpack && !swimming && kJump && kJumped && platformCharacterIs(IN_AIR) && jetpackFuel > 0) {
1721     yAcc += initialJumpAcc;
1722     yVel = -1;
1723     jetpackFuel -= 1;
1724     if (jetpackFlaresTime < 1) jetpackFlaresTime = 3;
1725     //!if (alarm[10] < 1) alarm[10] = 3; // jetpack flares
1726     fallTimer = 0;
1728     status = JUMPING;
1729     jumpButtonReleased = false;
1730     jumpTime = 0;
1732     grav = 0;
1733     invokeJumpHelper = true;
1734   } else if (platformCharacterIs(ON_GROUND) && kJumpPressed && fallTimer == 0) {
1735     if (fabs(xVel) > 3 /*xVel > 3 || xVel < -3*/) {
1736       yAcc += initialJumpAcc*2;
1737       xAcc += xVel*2;
1738     } else {
1739       yAcc += initialJumpAcc*2;
1740       xAcc += xVel/2;
1741       //scrJumpHelper(); // move to location where player doesn't have to be on ground
1742     }
1743     if (global.hasJordans) {
1744       yAcc *= 3;
1745       yAccLimit = 12;
1746       grav = 0.5;
1747     } else if (global.hasSpringShoes) {
1748       yAcc *= 1.5;
1749     } else {
1750       yAccLimit = 6;
1751       grav = gravNorm;
1752     }
1754     playSound('sndJump');
1756     pushTimer = 0;
1758     // the "state" gets changed to JUMPING later on in the code
1759     status = FALLING;
1760     // "variable jumping" states
1761     jumpButtonReleased = false;
1762     jumpTime = 0;
1763     invokeJumpHelper = true;
1764   }
1766   if (kJumpPressed && invokeJumpHelper) scrJumpHelper(); // YASM 1.8.1
1768   if (jumpTime < jumpTimeTotal) jumpTime += 1;
1769   // let the character continue to jump
1770   if (!kJump) jumpButtonReleased = true;
1771   if (jumpButtonReleased) jumpTime = jumpTimeTotal;
1773   gravityIntensity = (jumpTime/jumpTimeTotal)*grav;
1775   if (kUp && platformCharacterIs(ON_GROUND) && !colLadder) {
1776     //k8:!!!looking = UP;
1777     if (xVel == 0 && xAcc == 0) status = LOOKING_UP;
1778   } else {
1779     //k8:!!!looking = 0;
1780   }
1782   if (!kUp && status == LOOKING_UP) status = STANDING;
1784   // hanging
1785   if (!colTop) {
1786     if (global.hasGloves) {
1787       if (yVel > 0) {
1788         if (hangCount == 0 && y > 16 && !platformCharacterIs(ON_GROUND) && kRight && colRight &&
1789             (level.isSolidAtPoint(x+9, y-5) || level.isSolidAtPoint(x+9, y-6)))
1790         {
1791           status = HANGING;
1792           if (moveSnap(1, 8)) { x = ix; y = iy; }
1793           yVel = 0;
1794           yAcc = 0;
1795           grav = 0;
1796         } else if (hangCount == 0 && y > 16 && !platformCharacterIs(ON_GROUND) && kLeft && colLeft &&
1797                    (level.isSolidAtPoint(x-9, y-5) || level.isSolidAtPoint(x-9, y-6)))
1798         {
1799           status = HANGING;
1800           if (moveSnap(1, 8)) { x = ix; y = iy; }
1801           yVel = 0;
1802           yAcc = 0;
1803           grav = 0;
1804         }
1805       }
1806     } else if (hangCount == 0 && y > 16 && !platformCharacterIs(ON_GROUND) && kRight && colRight &&
1807                (level.isTreeAtPoint(x+9, y-5) || level.isTreeAtPoint(x+9, y-6)))
1808     {
1809       status = HANGING;
1810       if (moveSnap(1, 8)) { x = ix; y = iy; }
1811       yVel = 0;
1812       yAcc = 0;
1813       grav = 0;
1814     } else if (hangCount == 0 && y > 16 && !platformCharacterIs(ON_GROUND) && kLeft && colLeft &&
1815                (level.isTreeAtPoint(x-9, y-5) || level.isTreeAtPoint(x-9, y-6)))
1816     {
1817       status = HANGING;
1818       if (moveSnap(1, 8)) { x = ix; y = iy; }
1819       yVel = 0;
1820       yAcc = 0;
1821       grav = 0;
1822     } else if (hangCount == 0 && y > 16 && !platformCharacterIs(ON_GROUND) && kRight && colRight &&
1823                (level.isSolidAtPoint(x+9, y-5) || level.isSolidAtPoint(x+9, y-6)) &&
1824                !level.isSolidAtPoint(x+9, y-9) && !level.isSolidAtPoint(x, y+9))
1825     {
1826       status = HANGING;
1827       if (moveSnap(1, 8)) { x = ix; y = iy; }
1828       yVel = 0;
1829       yAcc = 0;
1830       grav = 0;
1831     } else if (hangCount == 0 && y > 16 && !platformCharacterIs(ON_GROUND) && kLeft && colLeft &&
1832                (level.isSolidAtPoint(x-9, y-5) || level.isSolidAtPoint(x-9, y-6)) &&
1833                !level.isSolidAtPoint(x-9, y-9) && !level.isSolidAtPoint(x, y+9))
1834     {
1835       status = HANGING;
1836       if (moveSnap(1, 8)) { x = ix; y = iy; }
1837       yVel = 0;
1838       yAcc = 0;
1839       grav = 0;
1840     }
1842     if (hangCount == 0 && y > 16 && !platformCharacterIs(ON_GROUND) &&
1843         !level.isSolidAtPoint(x, y+12) && // from Spelunky Natural
1844         status == FALLING &&
1845         !level.isObjectAtPoint(x, y-9, &level.cbIsObjectArrow) && !level.isObjectAtPoint(x, y+9, &level.cbIsObjectArrow))
1846     {
1847       auto obj0 = level.isObjectAtPoint(x, y-5, &level.cbIsObjectArrow);
1848       auto obj1 = level.isObjectAtPoint(x, y-6, &level.cbIsObjectArrow);
1849       if (obj0 || obj1) {
1850         // get nearest arrow
1851         MapObject obj;
1852         if (obj1 && obj0) obj = (obj0.distanceToPoint(x, y-5) < obj1.distanceToPoint(x, y-5) ? obj0 : obj1);
1853         else obj = (obj0 ? obj0 : obj1);
1854         //obj = instance_nearest(x, y-5, oArrow);
1855         if (obj.stuck) {
1856           status = HANGING;
1857           // move_snap(1, 8); // was commented out in the original
1858           yVel = 0;
1859           yAcc = 0;
1860           grav = 0;
1861         }
1862       }
1863     }
1864     /* this was commented in the original
1865     if (hangCount == 0 && y > 16 && !platformCharacterIs(ON_GROUND) && state == FALLING &&
1866         (collision_point(x, y-5, oTreeBranch, 0, 0) || collision_point(x, y-6, oTreeBranch, 0, 0)) &&
1867         !collision_point(x, y-9, oTreeBranch, 0, 0) && !collision_point(x, y+9, oTreeBranch, 0, 0))
1868     {
1869       state = HANGING;
1870       // move_snap(1, 8); // was commented out in the original
1871       yVel = 0;
1872       yAcc = 0;
1873       grav = 0;
1874     }
1875     */
1876   }
1878   if (hangCount > 0) hangCount -= 1;
1880   if (status == HANGING) {
1881     closeCape();
1882     kJumped = false;
1883     if (kJumpPressed) {
1884       if (kDown) {
1885         if (global.hasGloves) {
1886           if (hangCount == 0 && y > 16 && !platformCharacterIs(ON_GROUND)) {
1887             if (kRight && colRight &&
1888                 (level.isSolidAtPoint(x+9, y-5) || level.isSolidAtPoint(x+9, y-6)))
1889             {
1890               grav = gravNorm;
1891               status = FALLING;
1892               yAcc -= grav;
1893               hangCount = 10;
1894             } else if (kLeft && colLeft &&
1895                        (level.isSolidAtPoint(x-9, y-5) || level.isSolidAtPoint(x-9, y-6)))
1896             {
1897               grav = gravNorm;
1898               status = FALLING;
1899               yAcc -= grav;
1900               hangCount = 10;
1901             } else {
1902               grav = gravNorm;
1903               status = FALLING;
1904               yAcc -= grav;
1905               hangCount = 5;
1906             }
1907           }
1908         } else {
1909           grav = gravNorm;
1910           status = FALLING;
1911           yAcc -= grav;
1912           hangCount = 5;
1913         }
1914       } else {
1915         grav = gravNorm;
1916         status = JUMPING;
1917         yAcc += initialJumpAcc*2;
1918         shiftX(dir == Dir.Right ? -2 : 2);
1919         x = ix;
1920         cameraBlockX = 3;
1921         hangCount = hangCountMax;
1922         if (level.isObjectAtPoint(x, y-5, &level.cbIsObjectArrow) || level.isObjectAtPoint(x, y-6, &level.cbIsObjectArrow)) hangCount /= 2; //Spelunky Natural
1923       }
1924     }
1925     if ((dir == Dir.Left && !isCollisionLeft(2)) ||
1926         (dir == Dir.Right && !isCollisionRight(2)))
1927     {
1928       grav = gravNorm;
1929       status = FALLING;
1930       yAcc -= grav;
1931       hangCount = 4;
1932     }
1933   } else {
1934     grav = gravNorm;
1935   }
1937   // pressing down while standing
1938   if (kDown && platformCharacterIs(ON_GROUND) && !whipping) {
1939     if (colBot) {
1940       status = DUCKING;
1941     } else if (colPlatBot) {
1942       // climb down ladder if possible, else jump down
1943       fallTimer = 0;
1944       if (!colBot) {
1945         //ladder = instance_place(x, y+16, oLadder);
1947         // from Spelunky Natural
1948         /*
1949         ladder = collision_line(x-4, y+16, x+4, y+16, oLadder, 0, 0);
1950         if (!ladder) ladder = collision_line(x-4, y+16, x+4, y+16, oLadderTop, 0, 0);
1951         */
1952         auto ladder = level.checkTilesInRect(x-4, y+16, 9, 1, &level.cbCollisionAnyLadder);
1953         //writeln("DOWN; cpb=", colPlatBot, "; cb=", colBot, "; ladder=", !!ladder);
1955         if (ladder) {
1956           if (abs(x-(ladder.x0+8)) < 4) {
1957             x = ladder.ix+8;
1958             setX(x);
1959             xVel = 0;
1960             yVel = 0;
1961             xAcc = 0;
1962             yAcc = 0;
1963             status = CLIMBING;
1964           }
1965         } else {
1966           shiftY(1);
1967           y = iy;
1968           status = FALLING;
1969           yAcc += grav;
1970           kJumped = true; // Spelunky Natural
1971         }
1972       }
1973       else {
1974         // the character can't move down because there is a solid in the way
1975         status = RUNNING;
1976       }
1977     }
1978   }
1979   if (!kDown && status == DUCKING) {
1980     status = STANDING;
1981     xVel = 0;
1982     xAcc = 0;
1983   }
1984   if (xVel == 0 && xAcc == 0 && status == RUNNING) status = STANDING;
1985   if (xAcc != 0 && status == STANDING) status = RUNNING;
1986   if (yVel < 0 && platformCharacterIs(IN_AIR) && status != HANGING) status = JUMPING;
1987   if (yVel > 0 && platformCharacterIs(IN_AIR) && status != HANGING) {
1988     status = FALLING;
1989     setCollisionBounds(-5, -6, 5, 8);
1990   } else {
1991     setCollisionBounds(-5, -6, 5, 8);
1992   }
1994   // CLIMB LADDER
1995   bool colPointLadder = !!level.isAnyLadderAtPoint(x, y);
1997   /* this was commented in the original
1998   if ((kUp && platformCharacterIs(IN_AIR) && collision_point(x, y-8, oLadder, 0, 0) && ladderTimer == 0) ||
1999       (kUp && colPointLadder && ladderTimer == 0) ||
2000       (kDown && colPointLadder && ladderTimer == 0 && platformCharacterIs(ON_GROUND) && collision_point(x, y+9, oLadderTop, 0, 0) && xVel == 0))
2001   {
2002     ladder = 0;
2003     ladder = instance_place(x, y-8, oLadder);
2004     if (instance_exists(ladder)) {
2005       if (abs(x-(ladder.x0+8)) < 4) {
2006         x = ladder.ix+8;
2007         setX(x);
2008         if (!collision_point(x, y, oLadder, 0, 0) && !collision_point(x, y, oLadderTop, 0, 0)) { y = ladder.iy+14; setY(y); }
2009         xVel = 0;
2010         yVel = 0;
2011         xAcc = 0;
2012         yAcc = 0;
2013         state = CLIMBING;
2014       }
2015     }
2016   }*/
2018   // Spelunky Natural - Multiple changes to this big "if" condition
2019   if ((kUp && platformCharacterIs(IN_AIR) && ladderTimer == 0 && level.checkTilesInRect(x-2, y-8, 5, 1, &level.cbCollisionLadder)) ||
2020       (kUp && colPointLadder && ladderTimer == 0) ||
2021       (kDown && colPointLadder && ladderTimer == 0 && platformCharacterIs(ON_GROUND) && xVel == 0 && level.isLadderTopAtPoint(x, y+9)) ||
2022       ((kUp || kDown) && status == HANGING && level.checkTilesInRect(x-2, y, 5, 1, &level.cbCollisionLadder)))
2023   {
2024     //ladder = 0;
2025     //auto ladder = instance_place(x, y-8, oLadder);
2026     auto ladder = level.isLadderAtPoint(x, y-8);
2027     if (ladder) {
2028       //writeln("LADDER01! plrx=", x, "; ladder.x0=", ladder.x0, "; ladder.ix=", ladder.ix, "; ladder class=", GetClassName(ladder.Class));
2029       if (abs(x-(ladder.x0+8)) < 4) {
2030         x = ladder.ix+8;
2031         setX(x);
2032         if (!level.isAnyLadderAtPoint(x, y)) { y = ladder.y0+14; setY(y); }
2033         xVel = 0;
2034         yVel = 0;
2035         xAcc = 0;
2036         yAcc = 0;
2037         status = CLIMBING;
2038       }
2039     }
2040   }
2042   /* this was commented in the original
2043   if (sprite_index == sDuckToHangL || sprite_index == sDamselDtHL) {
2044     ladder = 0;
2045     if (facing == LEFT && collision_rectangle(x-8, y, x, y+16, oLadder, 0, 0) && !collision_point(x-4, y+16, oSolid, 0, 0)) {
2046       ladder = instance_nearest(x-4, y+16, oLadder);
2047     } else if (facing == RIGHT && collision_rectangle(x, y, x+8, y+16, oLadder, 0, 0) && !collision_point(x+4, y+16, oSolid, 0, 0)) {
2048       ladder = instance_nearest(x+4, y+16, oLadder);
2049     }
2050     if (ladder) {
2051       x = ladder.ix+8;
2052       setX(x);
2053       xVel = 0;
2054       yVel = 0;
2055       xAcc = 0;
2056       yAcc = 0;
2057       state = CLIMBING;
2058     }
2059   }
2061   if (colLadder && state == CLIMBING && kJumpPressed && !whipping) {
2062     if (kLeft) xVel = -departLadderXVel; else if (kRight) xVel = departLadderXVel; else xVel = 0;
2063     yAcc += departLadderYVel;
2064     state = JUMPING;
2065     jumpButtonReleased = false;
2066     jumpTime = 0;
2067     ladderTimer = 5;
2068   }
2069   */
2071   // calculate horizontal/vertical friction
2072   if (status == CLIMBING) {
2073     xFric = frictionClimbingX;
2074     yFric = frictionClimbingY;
2075   } else {
2076     //if (runKey && platformCharacterIs(ON_GROUND) && runHeld >= 10)
2077     if ((runKey && runHeld >= 10) && (platformCharacterIs(ON_GROUND) || global.config.toggleRunAnywhere)) {
2078       // YASM 1.8.1
2079       if (kLeft) {
2080         // run
2081         xVel -= 0.1;
2082         xVelLimit = 6;
2083         xFric = frictionRunningFastX;
2084       } else if (kRight) {
2085         xVel += 0.1;
2086         xVelLimit = 6;
2087         xFric = frictionRunningFastX;
2088       }
2089     } else if (status == DUCKING) {
2090       if (xVel < 2 && xVel > -2) {
2091         xFric = 0.2;
2092         xVelLimit = 3;
2093         imageSpeed = 0.8;
2094       } else if (kLeft && global.config.downToRun) {
2095         // run
2096         xVel -= 0.1;
2097         xVelLimit = 6;
2098         xFric = frictionRunningFastX;
2099       } else if (kRight && global.config.downToRun) {
2100         xVel += 0.1;
2101         xVelLimit = 6;
2102         xFric = frictionRunningFastX;
2103       } else {
2104         xVel *= 0.8;
2105         if (xVel < 0.5) xVel = 0;
2106         xFric = 0.2;
2107         xVelLimit = 3;
2108         imageSpeed = 0.8;
2109       }
2110     } else {
2111       // decrease the friction when the character is "flying"
2112       if (platformCharacterIs(IN_AIR)) {
2113         if (dead || stunned) xFric = 1.0; else xFric = 0.8;
2114       } else {
2115         xFric = frictionRunningX;
2116       }
2117     }
2119     /* // ORIGINAL RUN/WALK xVel/xFric code  this was commented in the original
2120     if (runKey && platformCharacterIs(ON_GROUND) && runHeld >= 10) {
2121       if (kLeft) {
2122         // run
2123         xVel -= 0.1;
2124         xVelLimit = 6;
2125         xFric = frictionRunningFastX;
2126       } else if (kRight) {
2127         xVel += 0.1;
2128         xVelLimit = 6;
2129         xFric = frictionRunningFastX;
2130       }
2131     } else if (state == DUCKING) {
2132       if (xVel < 2 && xVel > -2) {
2133         xFric = 0.2
2134         xVelLimit = 3;
2135         imageSpeed = 0.8;
2136       } else if (kLeft && global.downToRun) {
2137         // run
2138         xVel -= 0.1;
2139         xVelLimit = 6;
2140         xFric = frictionRunningFastX;
2141       } else if (kRight && global.downToRun) {
2142         xVel += 0.1;
2143         xVelLimit = 6;
2144         xFric = frictionRunningFastX;
2145       } else {
2146         xVel *= 0.8;
2147         if (xVel < 0.5) xVel = 0;
2148         xFric = 0.2
2149         xVelLimit = 3;
2150         imageSpeed = 0.8;
2151       }
2152     } else {
2153       // decrease the friction when the character is "flying"
2154       if (platformCharacterIs(IN_AIR)) {
2155         if (dead || stunned) xFric = 1.0; else xFric = 0.8;
2156       } else {
2157         xFric = frictionRunningX;
2158       }
2159     }
2160     */
2162     // stuck on web or underwater
2163     if (level.isObjectAtPoint(x, y, &level.cbIsObjectWeb)) {
2164       xFric = 0.2;
2165       yFric = 0.2;
2166       fallTimer = 0;
2167     } else if (level.isObjectAtPoint(x, y, &level.cbIsObjectBlob)) {
2168       // blob enemy
2169       //obj = instance_place(x, y, oBlob); this was commented in the original
2170       //xVel += obj.xVel; this was commented in the original
2171       xFric = 0.1;
2172       yFric = 0.3;
2173       fallTimer = 0;
2174     } else if (level.isWaterAtPoint(x, y/*, oWater, -1, -1*/)) {
2175       closeCape();
2176       //if (!runKey && global.toggleRunAnywhere) xFric = frictionRunningX; // YASM 1.8.1 this was commented in the original
2177       if (!platformCharacterIs(ON_GROUND)) xFric = frictionRunningX;
2178       if (status == FALLING && yVel > 0) {
2179         // Spelunky Natural
2180              if (global.config.naturalSwim && kUp) yFric = 0.2;
2181         else if (global.config.naturalSwim && kDown) yFric = 0.8;
2182         else yFric = 0.5;
2183       } else if (!level.isWaterAtPoint(x, y-9/*, oWater, -1, -1*/)) {
2184         yFric = 1;
2185       } else {
2186         yFric = 0.9;
2187       }
2188       if (yVel < -6 && global.config.noDolphin) {
2189         // Spelunky Natural (changed from -4 to -6)
2190         yVel = -6;
2191       }
2192     } else {
2193       swimming = false;
2194       yFric = 1;
2195     }
2196   }
2198   if (colIceBot && status != DUCKING && !global.hasSpikeShoes) {
2199     xFric = 0.98;
2200     yFric = 1;
2201   }
2203   // YASM 1.8.1
2204   if (global.config.toggleRunAnywhere) {
2205     if (!kJump && !kDown && !runKey) xVelLimit = 3;
2206   }
2208   // RUNNING
2209   if (platformCharacterIs(ON_GROUND)) {
2210          if (status == RUNNING && kLeft && colLeft) pushTimer += 1;
2211     else if (status == RUNNING && kRight && colRight) pushTimer += 1;
2212     else pushTimer = 0;
2214     //if (platformCharacterIs(ON_GROUND) && !kJump && !kDown && !runKey) this was commented in the original
2215     if (!kJump && !kDown && !runKey) xVelLimit = 3;
2217     /* this was commented in the original
2218     // ledge flip
2219     if (state == DUCKING && fabs(xVel) < 3 && facing == LEFT &&
2220         //collision_point(x, y+9, oSolid, 0, 0) && !collision_point(x-1, y+9, oSolid, 0, 0) && kLeft)
2221         collision_point(x, y+9, oSolid, 0, 0) && !collision_line(x-1, y+9, x-10, y+9, oSolid, 0, 0) && kLeft)
2222     */
2224     // ledge flip
2225     int dhdir = 0;
2226          if (kLeft && dir == Dir.Left) dhdir = -1;
2227     else if (kRight && dir == Dir.Right) dhdir = 1;
2229     if (dhdir && status == DUCKING && fabs(xVel) < 3+(dhdir < 0 ? 1 : 0) &&
2230         level.isSolidAtPoint(x, y+9) && !level.checkTilesInRect(x+(dhdir < 0 ? -8 : 1), y+9, 8, 8))
2231     {
2232       status = DUCKTOHANG;
2233       if (holdItem) {
2234         if (!global.config.scumFlipHold || holdItem.heavy) {
2235           /*
2236           holdItem.heldBy = none;
2237           if (holdItem.objName == 'GoldIdol') holdItem.shiftY(-8);
2238           */
2239           //else if (holdItem.type == "Block Item") { with (oBlockPreview) instance_destroy(); }
2240           scrDropItem(LostCause.Hang, (dir == Dir.Left ? -1 : 1), -4);
2241         }
2242       }
2243       knockOffMonkeys();
2244     }
2245   }
2247   if (status == DUCKTOHANG) {
2248     setXY(xPrev, yPrev);
2249     x = ix;
2250     y = iy;
2251     xVel = 0;
2252     yVel = 0;
2253     xAcc = 0;
2254     yAcc = 0;
2255     grav = 0;
2256   }
2258   // parachute and cape
2259   if (!level.inWinCutscene) {
2260     if (isParachuteActive() || isCapeActiveAndOpen()) yFric = 0.5;
2261   }
2263   if (pushTimer > 100) pushTimer = 100;
2265   // limits the acceleration if it is too extreme
2266   xAcc = fclamp(xAcc, -xAccLimit, xAccLimit);
2267   yAcc = fclamp(yAcc, -yAccLimit, yAccLimit);
2269   // applies the acceleration
2270   xVel += xAcc;
2271   if (dead || stunned) yVel += 0.6; else yVel += yAcc;
2273   // nullifies the acceleration
2274   xAcc = 0;
2275   yAcc = 0;
2277   // applies the friction to the velocity, now that the velocity has been calculated
2278   xVel *= xFric;
2279   yVel *= yFric;
2281   auto oBall = getMyBall();
2282   // apply ball and chain
2283   if (oBall) {
2284     int distsq = (ix-oBall.ix)*(ix-oBall.ix)+(iy-oBall.iy)*(iy-oBall.iy);
2285     if (distsq >= 24*24) {
2286       if (xVel > 0 && oBall.ix < ix && abs(oBall.ix-ix) > 24) xVel = 0;
2287       if (xVel < 0 && oBall.ix > ix && abs(oBall.ix-ix) > 24) xVel = 0;
2288       if (yVel > 0 && oBall.iy < iy && abs(oBall.iy-iy) > 24) {
2289         if (abs(oBall.ix-ix) < 1) {
2290           //teleportTo(destx:oBall.ix);
2291           fltx = oBall.fltx;
2292           prevFltX = oBall.prevFltX;
2293           x = ix;
2294         } else if (oBall.ix < ix && !kRight) {
2295                if (xVel > 0) xVel *= -0.25;
2296           else if (xVel == 0) xVel -= 1;
2297         } else if (oBall.ix > ix && !kLeft) {
2298                if (xVel < 0) xVel *= -0.25;
2299           else if (xVel == 0) xVel += 1;
2300         }
2301         yVel = 0;
2302         fallTimer = 0;
2303       }
2304       if (yVel < 0 && oBall.iy > iy && abs(oBall.iy-iy) > 24) yVel = 0;
2305     }
2306   }
2308   // apply the limits since the velocity may be too extreme
2309   if (!dead && !stunned) xVel = fclamp(xVel, -xVelLimit, xVelLimit);
2310   yVel = fclamp(yVel, -yVelLimit, yVelLimit);
2312   // approximates the "active" variables
2313   if (fabs(xVel) < 0.0001) xVel = 0;
2314   if (fabs(yVel) < 0.0001) yVel = 0;
2315   if (fabs(xAcc) < 0.0001) xAcc = 0;
2316   if (fabs(yAcc) < 0.0001) yAcc = 0;
2318   bool wasInWall = !!isCollision();
2319   moveRel(xVel, yVel);
2321   // don't go out of level (if we're not in ending sequence)
2322   if (!level.inWinCutscene) {
2323          if (ix < 0) fltx = 0;
2324     else if (ix > level.tilesWidth*16-16) fltx = level.tilesWidth*16-16;
2325     if (iy < 0) flty = 0;
2327     if (!wasInWall && isCollision()) {
2328       writeln("** FUUUU (XXX)");
2329       if (isCollisionBottom(0) && !isCollisionBottom(-2)) {
2330         flty = iy-2;
2331       }
2332       // we can stuck in the wall with this
2333       if (isCollisionLeft(0)) {
2334         writeln("** FUUUU (001: left)");
2335         while (isCollisionLeft(0) && !isCollisionRight(1)) shiftX(1);
2336       } else if (isCollisionRight(0)) {
2337         writeln("** FUUUU (001: right)");
2338         while (isCollisionRight(0) && !isCollisionLeft(1)) shiftX(-1);
2339       }
2340     }
2342     // move out of wall by 1 px, if possible
2343     if (!dead && isCollision()) {
2344       if (isCollisionBottom(0) && !isCollisionBottom(-1)) flty = iy-1;
2345       if (isCollisionTop(0) && !isCollisionTop(1)) flty = iy+1;
2346       if (isCollisionLeft(0) && !isCollisionLeft(1)) fltx = ix+1;
2347       if (isCollisionRight(0) && !isCollisionRight(-1)) fltx = ix-1;
2348     }
2350     if (isCollision()) {
2351       //k8:HACK: try to duck
2352       bool wallDeath = true;
2353       if (platformCharacterIs(ON_GROUND)) {
2354         setCollisionBounds(-5, -6, 5, 8);
2355         wallDeath = !!isCollision();
2356         if (wallDeath) {
2357           setCollisionBounds(-8, -6, 8, 8);
2358         } else {
2359           // force ducking
2360           status = DUCKING;
2361         }
2362       }
2364       if (wallDeath) {
2365         foreach (; 0..6) {
2366           if (isCollision()) {
2367                  if (isCollisionLeft(0) && !isCollisionRight(4)) fltx = ix+1;
2368             else if (isCollisionRight(0) && !isCollisionLeft(4)) fltx = ix-1;
2369             else if (isCollisionBottom(0) && !isCollisionTop(4)) flty = iy-1;
2370             else if (isCollisionTop(0) && !isCollisionBottom(4)) flty = iy+1;
2371             else break;
2372           }
2373         }
2375         if (wallDeath && isCollision()) {
2376           if (!dead) level.addDeath('wall');
2377           //visible = false;
2378           dead = true;
2379           writeln("PLAYER KILLED BY WALL");
2380           global.plife = 0; // oops
2381         }
2382       }
2383     }
2384   } else {
2385     // in cutscene
2386     //writeln("flty=", flty, "; iy=", iy);
2387     if (flty <= 0) {
2388       status = STANDING;
2389     }
2390   }
2392   // figures out what the sprite index of the character should be
2393   characterSprite();
2395   // sets the previous state and the previously previous state
2396   statePrevPrev = statePrev;
2397   statePrev = status;
2399   // calculates the imageSpeed based on the character's velocity
2400   if (status == RUNNING || status == DUCKING || status == LOOKING_UP) {
2401     if (status == RUNNING || status == LOOKING_UP) imageSpeed = fabs(xVel)*runAnimSpeed+0.1;
2402   }
2404   if (status == CLIMBING) imageSpeed = sqrt(xVel*xVel+yVel*yVel)*climbAnimSpeed;
2406   if (xVel >= 4 || xVel <= -4) {
2407     imageSpeed = 1;
2408     if (platformCharacterIs(ON_GROUND)) {
2409       setCollisionBounds(-8, -6, 8, 8);
2410     } else {
2411       setCollisionBounds(-5, -6, 5, 8);
2412     }
2413   } else {
2414     setCollisionBounds(-5, -6, 5, 8);
2415   }
2417   if (whipping) imageSpeed = 1;
2419   if (status == DUCKTOHANG) {
2420     imageFrame = 0;
2421     imageSpeed = 0.8;
2422   }
2424   // limit the imageSpeed at 1 so the animation always looks good
2425   if (imageSpeed > 1) imageSpeed = 1;
2427   //if (kItemPressed) writeln("ITEM! dead=", dead, "; stunned=", stunned, "; active=", active);
2428   if (dead || stunned || !active) {
2429     // do nothing
2430   } else if (/*inGame &&*/ kItemPressed && !whipping) {
2431     // SWITCH
2432     if (kUp) scrSwitchToStickyBombs(); else scrSwitchToNextItem();
2433   } else if (/*inGame &&*/ kRopePressed && global.rope > 0 && !whipping) {
2434     if (!kDown && colTop) {
2435       // do nothing
2436     } else {
2437       launchRope(kDown, doDrop:true);
2438     }
2439   } else if (/*inGame &&*/ kBombPressed && global.bombs > 0 && !whipping) {
2440     if (holdItem isa ItemWeaponBow && bowArmed) {
2441       if (holdArrow != ARROW_BOMB) {
2442         //writeln("set bow arrows to bomb");
2443         holdArrow = ARROW_BOMB;
2444       } else {
2445         //writeln("set bow arrows to normal");
2446         holdArrow = ARROW_NORM;
2447       }
2448     } else {
2449       scrLaunchBomb();
2450     }
2451   }
2454   // open chest/crate
2455   if (!dead && !stunned && kUp && kAttackPressed) {
2456     auto octr = ItemOpenableContainer(level.isObjectInRect(ix, iy, width, height, delegate bool (MapObject o) {
2457       return (o isa ItemOpenableContainer);
2458     }));
2459     if (octr) {
2460       if (octr.openMe()) kAttackPressed = false;
2461     }
2462   }
2465   // use weapon / attack
2466   if (!dead && !stunned && kAttackPressed && !holdItem /*&& !pickedItem*/) {
2467     bowArmed = false;
2468     bowStrength = 0;
2469     sndStopSound('sndBowPull');
2470     if (status != DUCKING && status != DUCKTOHANG && !whipping && !isExitingSprite()) {
2471       imageSpeed = 0.6;
2472       if (global.isTunnelMan) {
2473         if (platformCharacterIs(ON_GROUND) || platformCharacterIs(IN_AIR)) {
2474           setSprite('sTunnelAttackL');
2475           whipping = true;
2476         }
2477       } else if (global.isDamsel) {
2478         setSprite('sDamselAttackL');
2479         whipping = true;
2480       } else {
2481         setSprite('sAttackLeft');
2482         whipping = true;
2483       }
2484     } else if (kDown && !pickedItem) {
2485       // pick up item
2486       MapObject obj = level.isObjectInRect(x-8, y, 9, 9, delegate bool (MapObject o) {
2487         if (o.spectral || !o.canPickUp) return false;
2488         if (!o.collidesWith(self)) return false;
2489         return o.onCanBePickedUp(self);
2490         /*
2491         if (o isa MapItem) return (o.active && o.canPickUp && !o.spectral);
2492         if (o isa MapEnemy) return (o.active && o.canPickUp && !o.spectral && (o.dead || o.status >= MapObject::STUNNED || o.meGoldMonkey));
2493         if (o isa ObjCharacter) return (o.active && o.canPickUp && !o.spectral);
2494         */
2495         return false;
2496       }, precise:false);
2497       if (obj) {
2498         // `canPickUp` is checked in callback
2499         if (/*obj.canPickUp &&*/ true /*k8: do we really need this? !level.isSolidAtPoint(obj.ix+2, obj.iy)*/) {
2500           //pickupItemType = holdItem.type;
2501           //!if (isAshShotgun(holdItem)) pickupItemType = "Boomstick";
2502           //!if (isGoldMonkey(obj) and obj.status &lt; 98) obj.status = 0; // do not play walk animation while held
2504           if (!obj.onTryPickup(self)) {
2505             if (obj.isInstanceAlive) scrPickupItem(obj);
2506           }
2508           /+!
2509           if (holdItem.type == "Bow" and holdItem.new) {
2510             holdItem.new = false;
2511             global.arrows += 6;
2512             if (global.arrows &gt; 99) global.arrows = 99;
2513           }
2514           +/
2515         }
2516       }
2517     }
2518   } else if (!dead && !stunned) {
2519     if (holdItem isa ItemWeaponBow) {
2520       if (kAttack && bowArmed && bowStrength < 12) {
2521         bowStrength += 0.2;
2522         //writeln("arming: ", bowStrength);
2523       } else {
2524         //!!!if (sndIsPlaying('sndBowPull')) sndStopSound('sndBowPull');
2525         sndStopSound('sndBowPull');
2526         // ...and shoot
2527         if (bowArmed && !kAttack) scrUseItem();
2528       }
2529       if (!holdArrow) holdArrow = ARROW_NORM;
2530     }
2531     if (kAttackPressed && holdItem) scrUseItem();
2532   }
2534   // buy items
2535   if (!dead && !stunned && kPayPressed) {
2536       // find nearest shopkeeper
2537     auto sc = MonsterShopkeeper(level.findNearestObject(ix, iy, delegate bool (MapObject o) {
2538       auto sc = MonsterShopkeeper(o);
2539       if (!sc) return false;
2540       //if (needCraps && sc.stype != 'Craps') return false;
2541       if (sc.dead || sc.angered) return false;
2542       return sc.canSellItem(self, holdItem);
2543     }));
2544     if (level.isInShop(ix/16, iy/16)) {
2545       // if no shopkeepers found, just use it
2546       if (!sc) {
2547         if (holdItem) {
2548           holdItem.forSale = false;
2549           holdItem.onTryPickup(self);
2550         }
2551       } else if (global.thiefLevel == 0 && !global.murderer) {
2552         // only law-abiding players can buy items or play games
2553         if (sc.doSellItem(self, holdItem)) {
2554           // use it
2555           if (holdItem) {
2556             holdItem.forSale = false;
2557             holdItem.onTryPickup(self);
2558           }
2559         }
2560         if (holdItem && !holdItem.isInstanceAlive) {
2561           holdItem = none;
2562           scrSwitchToPocketItem(forceIfEmpty:false); // just in case
2563         }
2564       }
2565     } else {
2566       // use pickup, if any
2567       if (holdItem isa ItemPickup) {
2568         // make nearest shopkeeper angry (an unlikely situation, but still...)
2569         if (sc && holdItem.forSale) level.scrShopkeeperAnger(GameLevel::SCAnger.ItemStolen);
2570         holdItem.forSale = false;
2571         holdItem.onTryPickup(self);
2572       } else {
2573         pickupsAround.clear();
2574         level.isObjectInRect(x0, y0, width, height, delegate bool (MapObject o) {
2575           auto pk = ItemPickup(o);
2576           if (pk && pk.collidesWith(self)) {
2577             bool found = false;
2578             foreach (auto opk; pickupsAround) if (opk == pk) { found = true; break; }
2579             if (!found) pickupsAround[$] = pk;
2580           }
2581           return false;
2582         }, precise:false);
2583         // now try to use all pickups
2584         foreach (ItemPickup pk; pickupsAround) {
2585           if (pk.isInstanceAlive) {
2586             if (sc && pk.forSale) level.scrShopkeeperAnger(GameLevel::SCAnger.ItemStolen);
2587             pk.forSale = false;
2588             pk.onTryPickup(self);
2589           }
2590         }
2591         pickupsAround.clear();
2592       }
2593     }
2594   }
2597 transient array!ItemPickup pickupsAround;
2600 // ////////////////////////////////////////////////////////////////////////// //
2601 override bool initialize () {
2602   if (!::initialize()) return false;
2604   powerups.length = 0;
2605   powerups[$] = SpawnObject(PPParachute);
2606   powerups[$] = SpawnObject(PPCape);
2608   foreach (PlayerPowerup pp; powerups) pp.owner = self;
2610   if (global.isDamsel) {
2611     desc = "Damsel";
2612     desc2 = "An athletic, unfittingly-dressed woman with extremely awkward running form.";
2613     setSprite('sDamselLeft');
2614   } else if (global.isTunnelMan) {
2615     desc = "Tunnel Man";
2616     desc2 = "A miner from the desert. His tools are a cut above the rest.";
2617     setSprite('sTunnelLeft');
2618   } else {
2619     desc = "Spelunker";
2620     desc2 = "A strange little man who spends his time exploring caverns. He wants to be just like Indiana Jones when he grows up.";
2621     setSprite('sStandLeft');
2622   }
2624   swimming = false;
2626   dir = Dir.Right;
2628   // scum ClimbSpeed
2629   switch (global.config.scumClimbSpeed) {
2630     case 2:
2631       climbAcc = 0.9;
2632       climbAnimSpeed = 0.4;
2633       climbSndSpeed = 6;
2634       break;
2635     case 3:
2636       climbAcc = 1.2;
2637       climbAnimSpeed = 0.45;
2638       climbSndSpeed = 5;
2639       break;
2640     case 4:
2641       climbAcc = 1.5;
2642       climbAnimSpeed = 0.5;
2643       climbSndSpeed = 4;
2644       break;
2645     case 5:
2646       climbAcc = 1.8;
2647       climbAnimSpeed = 0.5;
2648       climbSndSpeed = 3;
2649       break;
2650     default:
2651       climbAcc = 0.6;       // how fast the character will climb
2652       climbAnimSpeed = 0.4; // relates to how fast the climbing animation should go
2653       climbSndSpeed = 8;
2654       break;
2655   }
2657   // sets the collision bounds to fit the default sprites (you can edit the arguments of the script)
2658   setCollisionBounds(-5, -5, 5, 8); // setCollisionBounds(-5, -8, 5, 8);
2660   statePrev = status;
2661   statePrevPrev = statePrev;
2662   gravityIntensity = grav;  // this variable describes the current force due to gravity (this variable is altered for variable jumping)
2663   jumpTime = jumpTimeTotal; // current time of the jump (0=start of jump, jumpTimeTotal=end of jump)
2665   return true;
2669 // ////////////////////////////////////////////////////////////////////////// //
2670 override void onAnimationLooped () {
2671   auto spr = getSprite();
2672   if (spr.Name == 'sAttackLeft' || spr.Name == 'sDamselAttackL' || spr.Name == 'sTunnelAttackL') {
2673     whipping = false;
2674     if (holdItem) holdItem.visible = true;
2675   } else if (spr.Name == 'sDuckToHangL' || spr.Name == 'sDamselDtHL' || spr.Name == 'sTunnelDtHL') {
2676     shiftY(16);
2677     moveSnap(1, 8);
2678     int x = ix, y = iy;
2679     xVel = 0;
2680     yVel = 0;
2681     xAcc = 0;
2682     yAcc = 0;
2683     grav = 0;
2684     MapTile obj;
2685     if (dir == Dir.Left) {
2686       // left
2687       obj = level.isAnyLadderAtPoint(x-8, y);
2688     } else {
2689       // right
2690       obj = level.isAnyLadderAtPoint(x+8, y);
2691     }
2692     if (obj) {
2693       status = CLIMBING;
2694       setX(obj.ix+8);
2695     } else if (dir == Dir.Left) {
2696       status = HANGING;
2697       dir = Dir.Right;
2698       shiftX(-6);
2699       shiftX(1);
2700     } else {
2701       status = HANGING;
2702       dir = Dir.Left;
2703       shiftX(6);
2704     }
2705   } else if (isExitingSprite()) {
2706     scrPlayerExit();
2707     //!global.cleanSolids = true;
2708   }
2712 void activatePlayerWeapon () {
2713   if (dead) {
2714     if (holdItem isa PlayerWeapon) {
2715       auto wep = holdItem;
2716       holdItem = none;
2717       wep.instanceRemove();
2718       return;
2719     }
2720   }
2721   if (global.config.unarmed) return;
2723   //TODO: Mattock and Machete
2724   if (holdItem isa PlayerWeapon) {
2725     if (!whipping /+&& holdItem isa /*WeaponWhip*/PlayerWeapon+/) {
2726       auto wep = holdItem;
2727       holdItem = none;
2728       wep.instanceRemove();
2729       return;
2730     }
2731   }
2733   if (holdItem) return;
2735   auto spr = getSprite();
2736   if (spr.Name != 'sAttackLeft' && spr.Name != 'sDamselAttackL' && spr.Name != 'sTunnelAttackL') return;
2737   if (imageFrame > 4) {
2738     //bool hitEnemy = (PlayerWeapon(holdItem) ? PlayerWeapon(holdItem).hitEnemy : false);
2739     if (global.isTunnelMan || pickedItem isa ItemWeaponMattock) {
2740       holdItem = level.MakeMapObject(ix+(dir == Dir.Left ? -16 : 16), iy, 'oMattockHit');
2741       if (imageFrame < 7) playSound('sndWhip');
2742     } else if (pickedItem isa ItemWeaponMachete) {
2743       holdItem = level.MakeMapObject(ix+(dir == Dir.Left ? -16 : 16), iy, 'oSlash');
2744       playSound('sndWhip');
2745     } else {
2746       holdItem = level.MakeMapObject(ix+(dir == Dir.Left ? -16 : 16), iy, 'oWhip');
2747       playSound('sndWhip');
2748     }
2749     /+ not needed anymore
2750     if (holdItem) {
2751       holdItem.active = true;
2752       //if (PlayerWeapon(holdItem)) PlayerWeapon(holdItem).hitEnemy = hitEnemy;
2753     }
2754     +/
2755   } else if (imageFrame < 2) {
2756     if (global.isTunnelMan || pickedItem isa ItemWeaponMattock) {
2757       holdItem = level.MakeMapObject(ix+(dir == Dir.Left ? -16 : 16), iy, 'oMattockPre');
2758     } else if (pickedItem isa ItemWeaponMachete) {
2759       holdItem = level.MakeMapObject(ix+(dir == Dir.Left ? -16 : 16), iy, 'oMachetePre');
2760     } else {
2761       holdItem = level.MakeMapObject(ix+(dir == Dir.Left ? 16 : -16), iy, 'oWhipPre');
2762     }
2763     /+ not needed anymore
2764     if (holdItem) holdItem.active = true;
2765     +/
2766   }
2768   /*
2769   if (holdItem) {
2770     if (holdItem.type == "Machete") {
2771       obj = instance_create(x-16, y, oSlash);
2772       obj.sprite_index = sSlashLeft;
2773       playSound(global.sndWhip);
2774     } else if (holdItem.type == "Mattock") {
2775       obj = instance_create(x-16, y, oMattockHit);
2776       obj.sprite_index = sMattockHitL;
2777       if (image_index &lt; 7) playSound(global.sndWhip);
2778     }
2779   } else {
2780     if (global.isTunnelMan) {
2781       obj = instance_create(x-16, y, oMattockHit);
2782       obj.sprite_index = sMattockHitL;
2783       if (image_index &lt; 7) playSound(global.sndWhip);
2784     } else {
2785       obj = instance_create(x-16, y, oWhip);
2786       if (global.scumWhipUpgrade == 1) obj.sprite_index = sWhipLongLeft; else obj.sprite_index = sWhipLeft;
2787       playSound(global.sndWhip);
2788     }
2789   }
2790   */
2794 //bool webHit = false;
2796 bool doBreakWebsCB (MapObject o) {
2797   if (o isa ItemWeb) {
2798     writeln("IN WEB!");
2799     /*if (!webHit)*/ {
2800       if (fabs(xVel) > 1) {
2801         xVel = xVel*0.2;
2802         if (!o.dying) ItemWeb(o).life -= 5;
2803       } else {
2804         xVel = 0;
2805       }
2806       if (fabs(yVel) > 1) {
2807         yVel = yVel*0.2;
2808         if (!o.dying) ItemWeb(o).life -= 5;
2809       } else {
2810         yVel = 0;
2811       }
2812     }
2813   }
2814   return false;
2818 void initiateExitSequence () {
2819        if (global.isDamsel) setSprite('sDamselExit');
2820   else if (global.isTunnelMan) setSprite('sTunnelExit');
2821   else setSprite('sPExit');
2823   imageSpeed = 0.5;
2824   active = false;
2825   invincible = 999;
2826   depth = 999;
2828   /*k8: the following is done in `GameLevel`
2829   if (global.thiefLevel > 0) global.thiefLevel -= 1;
2830   //orig dbg:if (global.currLevel == 1) global.currLevel += firstLevelSkip; else global.currLevel += levelSkip;
2831   global.currLevel += 1;
2832   */
2833   playSound('sndSteps');
2837 void processLevelExit () {
2838   if (dead || stunned || whipping) return;
2839   if (!platformCharacterIs(ON_GROUND)) return;
2840   if (isExitingSprite()) return; // just in case
2842   auto hld = holdItem;
2843   if (hld isa PlayerWeapon) return; // oops
2845   //if (!kExitPressed && !hld) return false;
2847   auto door = level.checkTileAtPoint(ix, iy, &level.cbCollisionExitTile);
2848   if (!door || !door.visible) return; // note that `invisible` doors still works
2850   // sell idol, or free damsel
2851   if (hld isa ItemGoldIdol) {
2852     //!if (isRealLevel()) global.idolsConverted += 1;
2853     //not thisglobal.money += hld.value*(global.levelType+1);
2854     ItemGoldIdol(hld).registerConverted();
2855     addScore(hld.value*(global.levelType+1));
2856     //!if (hld.sprite_index == sCrystalSkull) global.skulls += 1; else global.idols += 1;
2857     playSound('sndCoin');
2858     level.MakeMapObject(ix, iy-8, 'oBigCollect');
2859     holdItem = none;
2860     hld.instanceRemove();
2861     //!with (hld) instance_destroy();
2862     //!hld = 0;
2863     //!pickupItemType = "";
2864   } else if (hld isa MonsterDamsel) {
2865     holdItem = none;
2866     MonsterDamsel(hld).exitAtDoor(door);
2867   }
2869   if (!kExitPressed) {
2870     if (!door.invisible) {
2871       string msg = door.getExitMessage();
2872       if (msg.length == 0) {
2873         level.osdMessage(va("PRESS %s TO ENTER.", (global.config.useDoorWithButton ? "$PAY" : "$UP")), -666);
2874       } else if (msg[$-1] != '\n') {
2875         level.osdMessage(va("%s\nPRESS %s TO ENTER.", msg, (global.config.useDoorWithButton ? "$PAY" : "$UP")), -666);
2876       } else {
2877         level.osdMessage(msg, -666);
2878       }
2879     }
2880     return;
2881   }
2883   // exiting
2884   holdArrow = 0;
2885   bowArmed = false;
2887   // drop armed bomb
2888   if (isHoldingArmedBomb()) scrUseThrowItem();
2890   if (isHoldingBombOrRope()) scrSwitchToPocketItem(forceIfEmpty:true);
2892   wasHoldingBall = false;
2893   hld = holdItem;
2894   if (hld) {
2895     if (hld isa ItemGoldIdol) {
2896       //!if (isRealLevel()) global.idolsConverted += 1;
2897       //not thisglobal.money += hld.value*(global.levelType+1);
2898       ItemGoldIdol(hld).registerConverted();
2899       addScore(hld.value*(global.levelType+1));
2900       //!if (hld.sprite_index == sCrystalSkull) global.skulls += 1; else global.idols += 1;
2901       playSound('sndCoin');
2902       level.MakeMapObject(ix, iy-8, 'oBigCollect');
2903       holdItem = none;
2904       hld.instanceRemove();
2905       //!with (hld) instance_destroy();
2906       //!hld = 0;
2907       //!pickupItemType = "";
2908     } else if (hld isa MonsterDamsel) {
2909       holdItem = none;
2910       MonsterDamsel(hld).exitAtDoor(door);
2911     } else if (hld.heavy || hld isa MapEnemy || hld isa ObjCharacter) {
2912       // drop heavy items, characters and enemies (but not ball)
2913       if (hld isa ItemBall) {
2914         wasHoldingBall = true;
2915         removeBallAndChain(temp:true);
2916       } else {
2917         scrUseThrowItem();
2918       }
2919     } else {
2920       // other items are carried thru
2921       if (hld.cannotBeCarriedOnNextLevel) {
2922         scrUseThrowItem();
2923         holdItem = none; // just in case
2924       } else {
2925         scrHideItemToPocket();
2926       }
2927       /*
2928       global.pickupItem = hld.type;
2929       if (isAshShotgun(hld)) global.pickupItem = "Boomstick";
2930       with (hld) {
2931         breakPieces = false;
2932         instance_destroy();
2933       }
2934       */
2935       //scrHideItemToPocket();
2936     }
2937   }
2939   knockOffMonkeys();
2941   //door = instance_place(x, y, oExit); // done above
2942   door.snapToExit(self);
2944   initiateExitSequence();
2946   level.playerExitDoor = door;
2950 override bool onFellInWater (MapTile water) {
2951   level.MakeMapObject(ix, iy-8, 'oSplash');
2952   swimming = true;
2953   playSound('sndSplash');
2954   myGrav = 0.2; //k8:???
2955   return false;
2958 override bool onOutOfWater () {
2959   swimming = false;
2960   myGrav = 0.6;
2961   return false;
2965 // ////////////////////////////////////////////////////////////////////////// //
2966 override void thinkFrame () {
2967   // remove whip, etc. when dead
2968   if (dead && holdItem isa PlayerWeapon) {
2969     auto pw = holdItem;
2970     holdItem = none;
2971     pw.instanceRemove();
2972     scrSwitchToPocketItem(forceIfEmpty:false);
2973   }
2975   setPowerupState('Cape', global.hasCape);
2977   foreach (PlayerPowerup pp; powerups) if (pp.active) pp.onPreThink();
2979   // kapala
2980   if (redColor > 0) {
2981          if (redToggle) redColor -= 5;
2982     else if (redColor < 20) redColor += 5;
2983     else redToggle = true;
2984   } else {
2985     redColor = 0;
2986   }
2988   if (dead) justdied = false;
2990   if (!dead) {
2991     if (invincible > 0) --invincible;
2992   } else {
2993     invincible = 0;
2994   }
2996   if (blink > 0) {
2997     blinkHidden = !blinkHidden;
2998     --blink;
2999   } else {
3000     blinkHidden = false;
3001   }
3003   auto spr = getSprite();
3004   int x = ix, y = iy;
3006   cameraBlockX = max(0, cameraBlockX-1);
3007   cameraBlockY = max(0, cameraBlockY-1);
3009   // WHOA
3010   if (spr.Name == 'sWhoaLeft' || spr.Name == 'sDamselWhoaL' || spr.Name == 'sTunnelWhoaL') {
3011     if (whoaTimer > 0) {
3012       whoaTimer -= 1;
3013     } else if (holdItem && onLoosingHeldItem(LostCause.Whoa)) {
3014       auto hi = holdItem;
3015       holdItem = none;
3016       if (!hi.onLostAsHeldItem(self, LostCause.Whoa)) {
3017         // oops, regain it
3018         holdItem = hi;
3019       } else {
3020         scrSwitchToPocketItem(forceIfEmpty:true);
3021       }
3022     }
3023   } else {
3024     whoaTimer = whoaTimerMax;
3025   }
3027   // firing
3028   if (firing > 0) firing -= 1;
3030   // water
3031   auto wtile = level.isWaterAtPoint(x, y/*, oWaterSwim, -1, -1*/);
3032   if (wtile) {
3033     if (!swimming) {
3034       if (onFellInWater(wtile) || !isInstanceAlive) return;
3035     }
3036   } else {
3037     if (swimming) {
3038       if (onOutOfWater() || !isInstanceAlive) return;
3039     }
3040   }
3042   // burning
3043   if (burning > 0) {
3044     if (global.randOther(1, 5) == 1) level.MakeMapObject(x-8+global.randOther(4, 12), y-8+global.randOther(4, 12), 'oBurn');
3045     burning -= 1;
3046   }
3048   // lava
3049   if (!dead && level.isLavaAtPoint(x, y+6/*, oLava, 0, 0*/)) {
3050     //!if (isRealLevel()) global.miscDeaths[11] += 1;
3051     level.addDeath('lava');
3052     playSound('sndFlame');
3053     global.plife -= 99;
3054     dead = true;
3055     xVel = 0;
3056     yVel = 0.1;
3057     grav = 0;
3058     myGrav = 0;
3059     bounced = true;
3060     burning = 100;
3061     depth = 999;
3062   }
3065   // jetpack
3066   if (global.hasJetpack && platformCharacterIs(ON_GROUND)) {
3067     jetpackFuel = 50;
3068   }
3070   // fall off bottom of screen
3071   if (!dead && y > level.tilesHeight*16+16) {
3072     //!if (isRealLevel()) global.miscDeaths[10] += 1;
3073     level.addDeath('void');
3074     global.plife -= 99; // spill blood
3075     xVel = 0;
3076     yVel = 0;
3077     grav = 0;
3078     myGrav = 0;
3079     bounced = true;
3080     scrDropItem(LostCause.Falloff);
3081     playSound('sndThud'); //???
3082     playSound('sndDie'); //???
3083   }
3085   if (dead && y > level.tilesHeight*16+16) {
3086     xVel = 0;
3087     yVel = 0;
3088     grav = 0;
3089     myGrav = 0;
3090   }
3092   if (/*active*/true) {
3093     if (spr.Name == 'sStunL' || spr.Name == 'sDamselStunL' || spr.Name == 'sTunnelStunL') {
3094       if (stunTimer > 0) {
3095         imageSpeed = 0.4;
3096         stunTimer -= 1;
3097       }
3098       if (stunTimer < 1) {
3099         stunned = false;
3100         canDropStuff = true;
3101       }
3102     }
3104     if (!level.inWinCutscene) {
3105       if (isParachuteActive() || isCapeActiveAndOpen()) fallTimer = 0;
3106     }
3108     // changed to yVel > 1 from yVel > 0
3109     if (yVel > 1 && status != CLIMBING) {
3110       fallTimer += 1;
3111       if (fallTimer > 16) wallHurt = 0; // no sense in them taking extra damage from being thrown here
3112       int paraOpenHeight = (global.config.scumSpringShoesReduceFallDamage && (global.hasSpringShoes || global.hasJordans) ? 22 : 14);
3113       //paraOpenHeight = 4;
3114       if (global.hasParachute && !stunned && fallTimer > paraOpenHeight) {
3115         //if (not collision_point(x, y+32, oSolid, 0, 0)) // was commented in the original code
3116         //!*if (not collision_line(x, y+16, x, y+32, oSolid, 0, 0))
3117         if (!level.checkTilesInRect(x, y+16, 1, 17, &level.cbCollisionAnySolid)) {
3118           // drop parachute
3119           //!instance_create(x-8, y-16, oParachute);
3120           fallTimer = 0;
3121           global.hasParachute = false;
3122           activatePowerup('Parachute');
3123           //writeln("parachute state: ", isParachuteActive());
3124         }
3125       }
3126     } else if (fallTimer > 16 && platformCharacterIs(ON_GROUND) &&
3127                !level.checkTilesInRect(x-8, y-8, 17, 17, &level.cbCollisionSpringTrap) /* not onto springtrap */)
3128     {
3129       // long drop -- player has just landed
3130       bool reducedDamage = (global.config.scumSpringShoesReduceFallDamage && (global.hasSpringShoes || global.hasJordans));
3131       if (reducedDamage && fallTimer <= 24) {
3132         // land without taking damage
3133         fallTimer = 0;
3134       } else {
3135         stunned = true;
3136              if (fallTimer > (reducedDamage ? 72 : 48)) global.plife -= 10*global.config.scumFallDamage;
3137         else if (fallTimer > (reducedDamage ? 48 : 32)) global.plife -= 2*global.config.scumFallDamage;
3138         else global.plife -= 1*global.config.scumFallDamage;
3139         if (global.plife < 1) {
3140           if (!dead) level.addDeath('fall');
3141           scrCreateBlood(x, y, 3);
3142           //!if (isRealLevel()) global.miscDeaths[3] += 1;
3143         }
3144         bounced = true;
3145         if (global.config.scumFallDamage > 0) stunTimer += 60;
3146         yVel = -3;
3147         fallTimer = 0;
3148         auto obj = level.MakeMapObject(x-4, y+6, 'oPoof');
3149         if (obj) obj.xVel = -0.4;
3150         obj = level.MakeMapObject(x+4, y+6, 'oPoof');
3151         if (obj) obj.xVel = 0.4;
3152         playSound('sndThud');
3153       }
3154     } else if (yVel <= 0) {
3155       fallTimer = 0;
3156       if (isParachuteActive()) {
3157         deactivatePowerup('Parachute');
3158         level.MakeMapObject(ix-8, iy-16-8, 'oParaUsed');
3159       }
3160     }
3162     // if (stunned) fallTimer = 0; // was commented in the original code
3164     if (swimming && !level.isLavaAtPoint(x, y/*, oLava, 0, 0*/)) {
3165       fallTimer = 0;
3166       if (bubbleTimer > 0) {
3167         bubbleTimer -= 1;
3168       } else {
3169         level.MakeMapObject(x, y-4, 'oBubble');
3170         bubbleTimer = bubbleTimerMax;
3171       }
3172     } else {
3173       bubbleTimer = bubbleTimerMax;
3174     }
3176     //TODO: k8: move spear checking to spear handler
3177     if (!isExitingSprite()) {
3178       auto spear = MapObjectSpearsBase(level.isObjectInRect(ix-6, iy-6, 13, 14, delegate bool (MapObject o) {
3179         auto tt = MapObjectSpearsBase(o);
3180         if (!tt) return false;
3181         return tt.isHitFrame;
3182       }));
3183       if (spear) {
3184         // stunned = true;
3185         // bounced  = false;
3186         global.plife -= global.config.spearDmg; // 4
3187         if (!dead && global.plife <= 0 /*and isRealLevel()*/) level.addDeath('spear');
3188         xVel = global.randOther(4, 6)*(spear.isLeft ? -1 : 1);
3189         yVel = -6;
3190         flty -= 1;
3191         y = iy;
3192         // state = FALLING;
3193         scrCreateBlood(x, y, 1);
3194       }
3195     }
3197     if (status != DUCKTOHANG && !stunned && !dead && !isExitingSprite()) {
3198       bounced = false;
3199       characterStepEvent();
3200     } else {
3201       if (status != DUCKING && status != DUCKTOHANG) status = STANDING;
3202       checkControlKeys(getSprite());
3203     }
3204   }
3206   // if (dead or stunned)
3207   if (dead || stunned) {
3208     if (holdItem) {
3209       if (holdItem isa ItemWeaponBow && bowArmed) scrFireBow();
3210       scrDropItem(dead ? LostCause.Dead : LostCause.Stunned, xVel, -3);
3211     }
3213     yVel += (bounced ? 1.0 : 0.6);
3215     if (isCollisionTop(1) && yVel < 0) yVel = -yVel*0.8;
3216     if (isCollisionLeft(1) || isCollisionRight(1)) xVel = -xVel*0.5;
3218     bool collisionbottomcheck = !!isCollisionBottom(1);
3219     if (collisionbottomcheck || isCollisionBottom(1, &level.cbCollisionPlatform)) {
3220       // bounce
3221       if (collisionbottomcheck) {
3222         if (yVel > 2.5) yVel = -yVel*0.5; else yVel = 0;
3223       } else {
3224         // after falling onto a platform don't take extra damage after recovering from stunning
3225         fallTimer -= 1;
3226       }
3227       /* was commented in the original code
3228       if (isCollisionBottom(1)) {
3229         if (yVel &gt; 2.5) yVel = -yVel*0.5; else yVel = 0;
3230       } else {
3231         fallTimer -= 1;
3232       }
3233       */
3235       // friction
3236            if (fabs(xVel) < 0.1) xVel = 0;
3237       else if (fabs(xVel) != 0 && level.isIceAtPoint(x, y+16)) xVel *= 0.8;
3238       else if (fabs(xVel) != 0) xVel *= 0.3;
3240       bounced = true;
3241     }
3243     //webHit = false;
3244     //level.forEachObjectInRect(ix, iy, width, height, &doBreakWebsCB);
3246     // apply the limits since the velocity may be too extreme
3247     xVelLimit = 10;
3248     xVel = fclamp(xVel, -xVelLimit, xVelLimit);
3249     yVel = fclamp(yVel, -yVelLimit, yVelLimit);
3251     moveRel(xVel, yVel);
3252     x = ix;
3253     y = iy;
3255     // fix sprites, spawn blood from spikes
3256     if (isParachuteActive()) {
3257       deactivatePowerup('Parachute');
3258       level.MakeMapObject(ix-8, iy-16-8, 'oParaUsed');
3259     }
3261     if (whipping) {
3262       whipping = false;
3263       //!with (oWhip) instance_destroy();
3264     }
3266     if (global.isDamsel) {
3267       if (xVel == 0) {
3268              if (dead) setSprite('sDamselDieL');
3269         else if (stunned) setSprite('sDamselStunL');
3270       } else if (bounced) {
3271         if (yVel < 0) setSprite('sDamselBounceL'); else setSprite('sDamselFallL');
3272       } else {
3273         if (xVel < 0) setSprite('sDamselDieLL'); else setSprite('sDamselDieLR');
3274       }
3275     } else if (global.isTunnelMan) {
3276       if (xVel == 0) {
3277              if (dead) setSprite('sTunnelDieL');
3278         else if (stunned) setSprite('sTunnelStunL');
3279       } else if (bounced) {
3280         if (yVel < 0) setSprite('sTunnelLBounce'); else setSprite('sTunnelFallL');
3281       } else {
3282         if (xVel < 0) setSprite('sTunnelDieLL'); else setSprite('sTunnelDieLR');
3283       }
3284     } else {
3285       if (xVel == 0) {
3286              if (dead) setSprite('sDieL');
3287         else if (stunned) setSprite('sStunL');
3288       } else if (bounced) {
3289         if (yVel < 0) setSprite('sDieLBounce'); else setSprite('sDieLFall');
3290       } else {
3291         if (xVel < 0) setSprite('sDieLL'); else setSprite('sDieLR');
3292       }
3293     }
3295     x = ix;
3296     y = iy;
3298     /*
3299     if (dead && justdied && yVel != 0) {
3300       auto spk = level.checkTileAtPoint(x, y, &level.cbCollisionSpikes);
3301       if (spk && global.randOther(1, 8) == 1) scrCreateBlood(spk.x0, spk.y0, 1);
3302     }
3303     */
3305     auto colobj = isCollisionRight(1);
3306     if (!colobj) colobj = isCollisionLeft(1);
3307     if (!colobj) colobj = isCollisionBottom(1);
3308     if (colobj) {
3309       if (wallHurt > 0) {
3310         foreach (; 0..3) level.MakeMapObject(colobj.x0, colobj.y0, 'oBlood');
3311         global.plife -= 1;
3312         if (!dead && global.plife <= 0 /*&& isRealLevel()*/) {
3313           if (thrownBy) {
3314             writeln("thrown to death by '", thrownBy, "'");
3315             level.addDeath(thrownBy);
3316           }
3317         }
3318         wallHurt -= 1;
3319         if (wallHurt <= 0) thrownBy = '';
3320         playSound('sndHurt'); //???
3321       }
3322     }
3324     colobj = isCollisionBottom(1);
3325     if (colobj && !bounced) {
3326       bounced = true;
3327       foreach (; 0..3) level.MakeMapObject(colobj.x0, colobj.y0, 'oBlood');
3328       if (wallHurt > 0) {
3329         global.plife -= 1;
3330         if (!dead && global.plife <= 0 /*and isRealLevel()*/) {
3331           if (thrownBy) {
3332             writeln("thrown to death by '", thrownBy, "'");
3333             level.addDeath(thrownBy);
3334           }
3335         }
3336         wallHurt -= 1;
3337         if (wallHurt <= 0) thrownBy = '';
3338       }
3339     }
3340   } else {
3341     // look up and down
3342     bool kPay = level.isKeyDown(GameConfig::Key.Pay);
3343     if (kPay) {
3344       // gnounc's quick look
3345       if (!kRight && !kLeft && (platformCharacterIs(ON_GROUND) || status == HANGING)) {
3346              if (kDown) { if (viewCount <= 6) viewCount += 3; else viewOffset += 6; }
3347         else if (kUp) { if (viewCount <= 6) viewCount += 3; else viewOffset -= 6; }
3348         else viewCount = 0;
3349       } else {
3350         viewCount = 0;
3351       }
3352     } else {
3353       // default look up/down with delay if pay button not held
3354       if (!kRight && !kLeft && (platformCharacterIs(ON_GROUND) || status == HANGING)) {
3355              if (kDown) { if (viewCount <= 30) viewCount += 1; else viewOffset += 4; }
3356         else if (kUp) { if (viewCount <= 30) viewCount += 1; else viewOffset -= 4; }
3357         else viewCount = 0;
3358       } else {
3359         viewCount = 0;
3360       }
3361     }
3362   }
3363   if (viewCount == 0 && viewOffset) viewOffset = (viewOffset < 0 ? min(0, viewOffset+8) : max(0, viewOffset-8));
3364   viewOffset = clamp(viewOffset, -16*6, 16*6);
3366   if (!dead) activatePlayerWeapon();
3368   if (!dead) processLevelExit();
3370   // hurt too much
3371   if (global.plife < -99 && visible && justdied) scrCreateBlood(x, y, 3);
3373   if (global.plife < 1) {
3374     dead = true;
3375   }
3377   // spikes, and other shit
3378   if (global.plife >= -99 && visible && !isExitingSprite()) {
3379     auto colSpikes = level.checkTilesInRect(x-4, y-4, 9, 13, &level.cbCollisionSpikes);
3381     if (colSpikes && dead) {
3382       grav = 0;
3383       if (!level.isSolidAtPoint(x, y+9)) { shiftY(0.02); y = iy; } //0.05;
3384       //else myGrav = 0.6;
3385     } else {
3386       myGrav = 0.6;
3387     }
3389     if (colSpikes && yVel > 0 && (fallTimer > 3 || stunned)) { // originally fallTimer &gt; 4
3390       if (!dead) {
3391         scrCreateBlood(ix, iy, 3);
3392         // spikes will always instant-kill in Moon room
3393         /*if (isRoom("rMoon")) global.plife -= 99; else*/ global.plife -= global.config.scumSpikeDamage;
3394         if (/*isRealLevel() &&*/ global.plife <= 0) level.addDeath('spike');
3395         if (global.plife > 0) playSound('sndHurt');
3396         xVel = 0;
3397         yVel = 0;
3398         myGrav = 0;
3399       }
3400       colSpikes.makeBloody();
3401     }
3402     //else if (not dead) myGrav = 0.6;
3403   }
3406   // sacrifice
3407   if (visible && (status >= STUNNED || stunned || dead || status == DUCKING)) {
3408     /*if (*/checkAndPerformSacrifice()/*) return*/;
3409   } else {
3410     sacCount = default.sacCount;
3411   }
3413   // activate ankh
3414   if (dead && global.hasAnkh) {
3415     writeln("*** ACTIVATED ANKH");
3416     global.hasAnkh = false;
3417     dead = false;
3418     int newLife = (global.isTunnelMan ? global.config.scumTMLife : global.config.scumStartLife);
3419     global.plife = max(global.plife, newLife);
3420     level.osdMessage("THE ANKH SHATTERS!\nYOU HAVE BEEN REVIVED!", 4);
3421     // find moai
3422     auto moai = level.forEachTile(delegate bool (MapTile t) { return (t.objType == 'oMoai'); });
3423     if (moai) {
3424       level.forEachTile(delegate bool (MapTile t) {
3425         if (t.objType == 'oMoaiInside') {
3426           teleportTo(t.ix+8, t.iy+8);
3427           t.instanceRemove();
3428         }
3429         return false;
3430       });
3431       //teleportTo(moai.ix+16+8, moai.iy+16+8);
3432     } else {
3433       if (level.allEnters.length) {
3434         teleportTo(level.allEnters[0].ix+8, level.allEnters[0].iy-8);
3435       }
3436     }
3437     level.centerViewAtPlayer();
3438     auto ball = getMyBall();
3439     if (ball) ball.teleportToPrisoner();
3440     //k8:???depth = 50;
3441     xVel = 0;
3442     yVel = 0;
3443     blink = 60;
3444     invincible = 60;
3445     fallTimer = 0;
3446     visible = true;
3447     active = true;
3448     dead = false;
3449     stunned = false;
3450     status = STANDING;
3451     burning = 0;
3452     //alarm[8] = 60; // this starts music; but we don't need it, 'cause we won't stop the music on player death
3453     playSound('sndTeleport');
3454   }
3457   if (dead) level.stats.gameOver();
3459   // step end
3460   if (status == DUCKTOHANG) {
3461     spr = getSprite();
3462     if (spr.Name != 'sDuckToHangL' && spr.Name != 'sDamselDtHL' && spr.Name != 'sTunnelDtHL') status = STANDING;
3463   }
3465   foreach (PlayerPowerup pp; powerups) if (pp.active) pp.onPostThink();
3467   if (jetpackFlaresTime > 0) {
3468     if (--jetpackFlaresTime == 0) {
3469       auto obj = level.MakeMapObject(ix+global.randOther(0, 3)-global.randOther(0, 3), iy+global.randOther(0, 3)-global.randOther(0, 3), 'oFlareSpark');
3470       if (obj) {
3471         obj.yVel = global.randOther(1, 3);
3472         obj.xVel = global.randOther(0, 3)-global.randOther(0, 3);
3473       }
3474       playSound('sndJetpack');
3475     }
3476   }
3480 // ////////////////////////////////////////////////////////////////////////// //
3481 override void drawWithOfs (int xpos, int ypos, int scale, float currFrameDelta) {
3482   //if (heldBy) return; // owner will take care of this
3483   if (blinkHidden) return;
3485   bool renderJetpackBack = false;
3486   if (global.hasJetpack) {
3487     // render jetpack
3488     if ((status == CLIMBING || isExitingSprite()) && !whipping) {
3489       // later
3490       renderJetpackBack = true;
3491     } else {
3492       int xi, yi;
3493       getInterpCoords(currFrameDelta, scale, out xi, out yi);
3494       yi -= 1;
3495       SpriteImage spr;
3496       if (dir == Dir.Right) {
3497         spr = level.sprStore['sJetpackRight'];
3498         xi -= 4;
3499       } else {
3500         spr = level.sprStore['sJetpackLeft'];
3501         xi += 4;
3502       }
3503       if (spr) {
3504         auto spf = spr.frames[0];
3505         if (spf && spf.width > 0 && spf.height > 0) spf.tex.blitAt(xi-xpos-spf.xofs*scale, yi-ypos-spf.yofs*scale, scale);
3506       }
3507     }
3508   }
3510   foreach (PlayerPowerup pp; powerups) if (pp.active) pp.preDrawWithOfs(xpos, ypos, scale, currFrameDelta);
3512   auto oldColor = Video.color;
3513   if (redColor > 0) Video.color = clamp(200+redColor, 0, 255)<<16;
3514   ::drawWithOfs(xpos, ypos, scale, currFrameDelta);
3515   Video.color = oldColor;
3517   if (renderJetpackBack) {
3518     int xi, yi;
3519     getInterpCoords(currFrameDelta, scale, out xi, out yi);
3520     SpriteImage spr = level.sprStore['sJetpackBack'];
3521     if (spr) {
3522       auto spf = spr.frames[0];
3523       if (spf && spf.width > 0 && spf.height > 0) spf.tex.blitAt(xi-xpos-spf.xofs*scale, yi-ypos-spf.yofs*scale, scale);
3524     }
3525   }
3527   foreach (PlayerPowerup pp; powerups) if (pp.active) pp.postDrawWithOfs(xpos, ypos, scale, currFrameDelta);
3530   int xi, yi;
3531   getInterpCoords(currFrameDelta, scale, out xi, out yi);
3533   bool doMirror;
3534   int frmXOfs;
3535   auto spf = getSpriteFrame(out doMirror);
3536   int spry0 = yi-spf.yofs*scale-ypos;
3537   if (!doMirror) {
3538     int sprx0 = xi-spf.xofs*scale-xpos;
3539     spf.tex.blitAt(sprx0, spry0, scale);
3540   } else {
3541     // mirror it
3542     int sprx0 = xi+spf.xofs*scale-xpos;
3543     spf.tex.blitExt(sprx0, spry0, sprx0-spf.tex.width*scale, spry0+spf.tex.height*scale, 0, 0, spf.tex.width, spf.tex.height);
3544   }
3546   if (false) {
3547     auto oclr = Video.color;
3548     Video.color = 0xff_ff_00;
3549     Video.drawRect(x0*scale-xpos, y0*scale-ypos, width*scale, height*scale);
3550     Video.color = 0x00_ff_00;
3551     Video.drawRect(ix*scale-xpos, iy*scale-ypos, 2, 2);
3553     if (isCollision()) {
3554       Video.color = 0x3f_ff_00_00;
3555       Video.fillRect(x0*scale-xpos, y0*scale-ypos, width*scale, height*scale);
3556     }
3558     Video.color = oclr;
3559   }
3564 void lastDrawWithOfs (int xpos, int ypos, int scale, float currFrameDelta) {
3565   foreach (PlayerPowerup pp; powerups) if (pp.active) pp.lastDrawWithOfs(xpos, ypos, scale, currFrameDelta);
3570   if (draw) {
3571          if (redColor > 0) draw_sprite_ext(sprite_index, -1, x, y, image_xscale, image_yscale, image_angle, make_color_rgb(200+redColor, 0, 0), image_alpha);
3572     else if (greenColor > 0) draw_sprite_ext(sprite_index, -1, x, y, image_xscale, image_yscale, image_angle, make_color_rgb(round(103+greenColor), round(161+greenColor), 0), image_alpha);
3573     else draw_sprite_ext(sprite_index, -1, x, y, image_xscale, image_yscale, image_angle, image_blend, image_alpha);
3574   }
3575   if (facing == RIGHT) {
3576     if (holdArrow == ARROW_NORM) {
3577       draw_sprite(sArrowRight, -1, x+4, y+1);
3578     } else if (holdArrow == ARROW_BOMB) {
3579       if (holdArrowToggle) draw_sprite(sBombArrowRight, 0, x+4, y+2); else draw_sprite(sBombArrowRight, 1, x+4, y+2);
3580     }
3581   } else if (facing == LEFT) {
3582     if (holdArrow == ARROW_NORM) {
3583       draw_sprite(sArrowLeft, -1, x-4, y+1);
3584     } else if (holdArrow == ARROW_BOMB) {
3585       if (holdArrowToggle) draw_sprite(sBombArrowLeft, 0, x-4, y+2); else draw_sprite(sBombArrowLeft, 1, x-4, y+2);
3586     }
3587   }
3591 defaultproperties {
3592   objName = 'Player';
3593   objType = 'oPlayer';
3595   desc = "Spelunker";
3596   desc2 = "A strange little man who spends his time exploring caverns. He wants to be just like Indiana Jones when he grows up.";
3598   negateMirrorXOfs = true;
3600   status = FALLING; // the character state, must be one of the following: STANDING, RUNNING, DUCKING, LOOKING_UP, CLIMBING, JUMPING, or FALLING
3602   bloodless = false;
3604   stunned = false;
3605   bounced = false;
3607   fallTimer = 0;
3608   stunTimer = 0;
3609   wallHurt = 0;
3610   //thrownBy = ""; // "Yeti", "Hawkman", or "Shopkeeper" for stat tracking deaths by being thrown
3611   pushTimer = 0;
3612   whoaTimer = 0;
3613   //whoaTimerMax = 30;
3614   distToNearestLightSource = 999;
3616   sacCount = 60;
3618   flying = false;
3619   myGrav = 0.6;
3620   myGravNorm = 0.6;
3621   myGravWater = 0.2;
3622   yVelLimit = 10;
3623   bounceFactor = 0.5;
3624   frictionFactor = 0.3;
3626   xVelLimit = 16; // limits the xVel: default 15
3627   yVelLimit = 10; // limits the yVel
3628   xAccLimit = 9;  // limits the xAcc
3629   yAccLimit = 6;  // limits the yAcc
3630   runAcc = 3;     // the running acceleration
3632   grav = 1;
3634   depth = 5;
3635   //lightRadius = 96; //???