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