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