1 /**********************************************************************************
2 * Copyright (c) 2008, 2009 Derek Yu and Mossmouth, LLC
3 * Copyright (c) 2010, Moloch
4 * Copyright (c) 2018, Ketmar Dark
6 * This file is part of Spelunky.
8 * You can redistribute and/or modify Spelunky, including its source code, under
9 * the terms of the Spelunky User License.
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.
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/>
18 **********************************************************************************/
19 // recoded by Ketmar // Invisible Vector
20 class PlayerPawn : MapEnemy;
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
36 const int bubbleTimerMax = 20;
37 int jetpackFlaresTime;
48 bool justdied = true; // so dead body won't spill blood endlessly
56 const int firingMax = 20;
57 const int firingPistolMax = 20;
58 const int firingShotgunMax = 40;
62 int hotkeyPressed = -1;
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
83 bool canDropStuff = true;
95 int bombArrowCounter = 80;
104 // the keys that the platform character will use (don't edit)
116 bool jumpButtonReleased; // whether the jump button was released. (Stops the user from pressing the jump button many times to get extra jumps)
119 bool kAttackReleased;
122 const float gravNorm = 1;
123 //float grav = 1; // the gravity
125 const float initialJumpAcc = -2; // relates to how high the character will jump
126 const int jumpTimeTotal = 10; // how long the user must hold the jump button to get the maximum jump height
128 float climbAcc = 0.6; // how fast the character will climb
129 float climbAnimSpeed = 0.4; // relates to how fast the climbing animation should go
130 int climbSndSpeed = 8;
134 // these flags are used to recreate ball and chain when player is moved at a new level
138 const float departLadderXVel = 4; // how fast character should be moving horizontally when he leaves the ladder
139 const float departLadderYVel = -4; // how fast the character should be moving vertically when he leaves the ladder
141 const float frictionRunningX = 0.6; // friction obtained while running
142 const float frictionRunningFastX = 0.98; // friction obtained while holding the shift button for some time while running
143 const float frictionClimbingX = 0.6; // friction obtained while climbing
144 const float frictionClimbingY = 0.6; // friction obtained while climbing
145 const float frictionDuckingX = 0.8; // friction obtained while ducking
146 const float frictionFlyingX = 0.99; // friction obtained while "flying"
148 const float runAnimSpeed = 0.1; // relates to the how fast the running animation should go
150 // hidden variables (don't edit)
151 protected int statePrev;
152 protected int statePrevPrev;
153 protected float gravityIntensity = grav; // this variable describes the current force due to gravity (this variable is altered for variable jumping)
154 protected float jumpTime = jumpTimeTotal; // current time of the jump (0=start of jump, jumpTimeTotal=end of jump)
155 protected int ladderTimer; // relates to whether the character can climb a ladder
156 protected int kLeftPushedSteps;
157 protected int kRightPushedSteps;
159 transient protected bool skipCutscenePressed;
172 // ////////////////////////////////////////////////////////////////////////// //
178 final ItemBall getMyBall () {
179 ItemBall res = myBall;
180 if (res && !res.isInstanceAlive) { res = none; myBall = none; }
185 void spawnBallAndChain (optional bool levelStart) {
187 auto owh = wasHoldingBall;
188 removeBallAndChain();
189 wasHoldingBall = owh;
191 mustBeChained = true;
192 auto ball = getMyBall();
194 if (levelStart) writeln("::: respawning ball; old ball is missing (it is ok)");
195 writeln("creating new ball");
196 ball = ItemBall(level.MakeMapObject(ix, iy, 'oBall'));
198 ball.attachTo(self, levelStart);
199 writeln("ball created");
203 if (levelStart) writeln("::: attaching ball to player");
204 ball.attachTo(self, levelStart);
205 if (wasHoldingBall) {
206 if (levelStart) writeln("::: picking ball");
208 pickedItem.instanceRemove();
211 if (holdItem && holdItem != ball) {
212 holdItem.instanceRemove();
218 if (myBall != ball) FatalError("error in ball management");
219 if (levelStart) writeln("ballpos=(", ball.ix, ",", ball.iy, "); plrpos=(", ix, ",", iy, "); ballalive=", ball.isInstanceAlive);
221 writeln("failed to create a new ball");
222 mustBeChained = false;
224 wasHoldingBall = false;
228 void removeBallAndChain (optional bool temp) {
229 auto ball = getMyBall();
231 writeln("removing ball and chain...", (temp ? " (temporarily)" : ""));
232 wasHoldingBall = (holdItem == ball);
233 writeln(" has ball, holding=", wasHoldingBall);
234 mustBeChained = true;
236 ball.instanceRemove();
240 wasHoldingBall = false;
241 mustBeChained = false;
245 // ////////////////////////////////////////////////////////////////////////// //
246 final PlayerPowerup findPowerup (name id) {
247 foreach (PlayerPowerup pp; powerups) if (pp.id == id) return pp;
252 final bool setPowerupState (name id, bool active) {
253 auto pp = findPowerup(id);
254 if (!pp) return false;
255 return (active ? pp.onActivate() : pp.onDeactivate());
259 final bool togglePowerupState (name id) {
260 auto pp = findPowerup(id);
261 if (!pp) return false;
262 return (pp.active ? pp.onDeactivate() : pp.onActivate());
266 final bool activatePowerup (name id) { return setPowerupState(id, true); }
267 final bool deactivatePowerup (name id) { return setPowerupState(id, false); }
270 final bool isActivePowerup (name id) {
271 auto pp = findPowerup(id);
272 return (pp && pp.active);
276 // ////////////////////////////////////////////////////////////////////////// //
277 override void Destroy () {
278 foreach (PlayerPowerup pp; powerups) delete pp;
283 void unpressAllKeys () {
285 kLeftPressed = false;
286 kLeftReleased = false;
288 kRightPressed = false;
289 kRightReleased = false;
293 kJumpPressed = false;
294 kJumpReleased = false;
296 kAttackPressed = false;
297 kAttackReleased = false;
298 kItemPressed = false;
299 kRopePressed = false;
300 kBombPressed = false;
302 kExitPressed = false;
307 void removeActivatedPlayerWeapon () {
309 if (holdItem isa PlayerWeapon) {
313 if (pickedItem) scrSwitchToPocketItem(forceIfEmpty:false);
318 // ////////////////////////////////////////////////////////////////////////// //
319 // called on level start too
325 skipCutscenePressed = false;
326 movementBlocked = false;
327 if (global.plife < 1) global.plife = max(1, global.config.scumStartLife);
332 myGrav = default.myGrav;
336 depth = default.depth;
337 status = default.status;
343 distToNearestLightSource = 999;
345 justdied = default.justdied;
346 removeActivatedPlayerWeapon();
348 blink = default.blink;
349 blinkHidden = default.blinkHidden;
355 level.clearKeysPressRelease();
358 //scrSwitchToPocketItem(forceIfEmpty:false);
362 // ////////////////////////////////////////////////////////////////////////// //
363 bool isExitingSprite () {
364 auto spr = getSprite();
365 return (spr.Name == 'sPExit' || spr.Name == 'sDamselExit' || spr.Name == 'sTunnelExit');
369 // ////////////////////////////////////////////////////////////////////////// //
370 override void playSound (name aname, optional bool unique) {
371 if (unique && global.sndIsPlaying(0, aname)) return;
372 global.playSound(0, 0, 0, aname); // it is local
376 override bool sndIsPlaying (name aname) {
377 return global.sndIsPlaying(0, aname);
381 override void sndStopSound (name aname) {
382 global.sndStopSound(0, aname);
386 // ////////////////////////////////////////////////////////////////////////// //
387 transient ItemDice currDie;
389 void onDieRolled (ItemDice die) {
390 if (!die.forSale) return;
391 // only law-abiding players can play
392 if (global.thiefLevel > 0 || global.murderer) return;
393 if (bet == 0) return;
396 level.forEachObject(delegate bool (MapObject o) {
397 MonsterShopkeeper sc = MonsterShopkeeper(o);
398 if (sc && !sc.dead && !sc.angered) return sc.onDiePlayed(self, currDie);
405 // ////////////////////////////////////////////////////////////////////////// //
406 override bool onExplosionTouch (MapObject xplo) {
407 //writeln("PlayerPawn: on explo touch! ", invincible);
408 if (invincible) return false;
409 if (global.config.scumExplosionHurt) {
410 global.plife -= global.config.explosionDmg;
411 if (!dead && global.plife <= 0 /*&& isRealLevel()*/) {
412 auto xp = MapObjExplosion(xplo);
413 if (xp && xp.suicide) level.addDeath('suicide'); else level.addDeath('explosion');
416 if (global.config.scumExplosionStun) {
422 if (xplo.ix < ix) xVel = global.randOther(4, 6); else xVel = -global.randOther(4, 6);
428 // ////////////////////////////////////////////////////////////////////////// //
429 // start new game when exiting from title, and process other custom exits
430 void scrPlayerExit () {
431 level.playerExited = true;
437 // ////////////////////////////////////////////////////////////////////////// //
438 bool scrHideItemToPocket (optional bool forBombOrRope) {
439 if (!holdItem) return true;
440 if (holdItem isa PlayerWeapon) return false;
441 if (holdItem.forSale) return false;
442 if (!forBombOrRope) {
443 if (holdItem isa ItemBall) return false;
446 // cannot hide armed bomb
447 ItemBomb bomb = ItemBomb(holdItem);
448 if (bomb && bomb.armed) return false;
449 if (bomb || holdItem isa ItemRopeThrow) {
450 holdItem.instanceRemove();
456 if (holdItem isa MapEnemy) return false;
457 //writeln("hiding: '", GetClassName(holdItem.Class), "'");
459 if (pickedItem) FatalError("we are already holding '%n'", GetClassName(pickedItem.Class));
460 pickedItem = holdItem;
462 pickedItem.active = false;
463 pickedItem.visible = false;
464 pickedItem.sellOfferDone = false;
465 if (pickedItem.heldBy) FatalError("oooops (scrHideItemToPocket)");
470 bool scrSwitchToBombs () {
471 if (holdItem isa PlayerWeapon) return false;
473 if (global.bombs < 1) return false;
474 if (ItemBomb(holdItem)) return true;
475 if (!scrHideItemToPocket(forBombOrRope:true)) return false;
477 ItemBomb bomb = ItemBomb(level.MakeMapObject(ix, iy, 'oBomb'));
478 if (!bomb) return false;
479 bomb.setSticky(global.stickyBombsActive);
481 whoaTimer = whoaTimerMax;
486 bool scrSwitchToStickyBombs () {
487 if (holdItem isa PlayerWeapon) return false;
488 if (!global.hasStickyBombs) {
489 global.stickyBombsActive = false;
493 global.stickyBombsActive = !global.stickyBombsActive;
498 bool scrSwitchToRopes () {
499 if (holdItem isa PlayerWeapon) return false;
501 if (global.rope < 1) return false;
502 if (ItemRopeThrow(holdItem)) return true;
503 if (!scrHideItemToPocket(forBombOrRope:true)) return false;
505 ItemRopeThrow rope = ItemRopeThrow(level.MakeMapObject(ix, iy, 'oRopeThrow'));
506 if (!rope) return false;
508 whoaTimer = whoaTimerMax;
513 bool isHoldingBombOrRope () {
515 if (!hit) return false;
516 return (hit isa ItemBomb || hit isa ItemRopeThrow);
520 bool isHoldingBomb () {
522 if (!hit) return false;
523 return (hit isa ItemBomb);
527 bool isHoldingArmedBomb () {
528 auto hit = ItemBomb(holdItem);
529 if (!hit) return false;
534 bool isHoldingRope () {
536 if (!hit) return false;
537 return (hit isa ItemRopeThrow);
541 bool scrSwitchToPocketItem (bool forceIfEmpty) {
542 if (holdItem isa PlayerWeapon) return false;
543 if (holdItem && holdItem.forSale) return false;
545 if (holdItem == pickedItem) {
547 whoaTimer = whoaTimerMax;
548 if (holdItem) holdItem.sellOfferDone = false;
552 if (!forceIfEmpty && !pickedItem) return false;
554 // destroy currently holded item if it is a bomb or a rope
556 // you cannot do it with an armed bomb
557 if (holdItem isa MapEnemy) return false; // cannot hide an enemy
558 ItemBomb bomb = ItemBomb(holdItem);
559 if (bomb && bomb.armed) return false;
560 if (bomb || holdItem isa ItemRopeThrow) {
562 holdItem.instanceRemove();
566 writeln(va("cannot switch to pocket item while carrying '%n' ('%n' is in pocket, why?)", GetClassName(holdItem.Class), GetClassName(pickedItem.Class)));
572 auto oldHold = holdItem;
573 holdItem = pickedItem;
574 pickedItem = oldHold;
575 // all flag management is done in property handler
577 oldHold.active = false;
578 oldHold.visible = false;
579 oldHold.sellOfferDone = false;
581 if (holdItem) holdItem.sellOfferDone = false;
582 whoaTimer = whoaTimerMax;
587 bool scrSwitchToNextItem () {
588 if (holdItem isa PlayerWeapon) return false;
589 if (holdItem && holdItem.forSale) return false;
592 if (ItemBomb(holdItem)) {
593 if (ItemBomb(holdItem).armed) return false; // cannot switch out of armed bomb
594 if (scrSwitchToRopes()) return true;
595 return scrSwitchToPocketItem(forceIfEmpty:true);
599 if (ItemRopeThrow(holdItem)) {
600 if (scrSwitchToPocketItem(forceIfEmpty:true)) return true;
601 if (scrSwitchToBombs()) return true;
602 return scrHideItemToPocket();
605 // either nothing, or normal item
606 bool tryPocket = !!holdItem;
607 if (scrSwitchToBombs()) return true;
608 if (scrSwitchToRopes()) return true;
609 if (holdItem isa ItemBall) return false;
610 if (tryPocket) return scrSwitchToPocketItem(forceIfEmpty:true);
615 // ////////////////////////////////////////////////////////////////////////// //
616 bool scrPickupItem (MapObject obj) {
617 if (holdItem isa PlayerWeapon) return false;
619 if (!obj) return false;
622 if (pickedItem) return false;
623 if (isHoldingArmedBomb()) return false;
624 if (isHoldingBombOrRope()) {
625 if (!scrSwitchToPocketItem(forceIfEmpty:true)) return false;
627 if (holdItem) return false;
630 if (pickedItem) return false;
633 if (obj isa ItemBomb && !ItemBomb(obj).armed) ++global.bombs;
634 else if (obj isa ItemRopeThrow) ++global.rope;
636 whoaTimer = whoaTimerMax;
637 obj.onPickedUp(self);
642 // ////////////////////////////////////////////////////////////////////////// //
643 //transient MapObject itck;
644 enum UnstickDebug = 0;
646 void unstuckDroppedObject (MapObject it) {
647 if (!it || !it.isInstanceAlive || it.width < 1 || it.height < 1) return;
649 if (it isa MapEnemy) {
650 it.ix = ix+(dir == Dir.Left ? -8 : -4);
651 if (it isa MonsterDamsel) it.iy = iy-1; else it.iy = iy-12;
654 // prevent getting stuck in a wall
655 if (UnstickDebug) writeln("???STUCK: (", it.objType, "), hitbox=(", it.hitboxX, ",", it.hitboxY, ")-(", it.hitboxW, "x", it.hitboxH, ")");
656 auto ospec = it.spectral;
658 if (!it.isCollision()) { it.spectral = ospec; return; }
659 if (UnstickDebug) writeln("***STUCK! (", it.objType, ")");
661 auto ox = it.ix, oy = it.iy;
664 if (!it.isCollision()) { it.spectral = ospec; it.saveInterpData(); it.updateGrid(); return; }
667 level.checkTilesInRect(it.x0, it.y0, width, height, delegate bool (MapTile t) {
669 writeln("mypos=(", itck.x0, ",", itck.y0, ")-(", itck.x1, ",", itck.y1, "); tpos=(", t.x0, ",", t.y0, ")-(", t.x1, ",", t.y1, ")");
675 foreach (int dy; 0..16) {
677 int dx = (dir == Dir.Left ? 1 : -1);
678 if (UnstickDebug) writeln(" only horizontal; dir=", dx);
680 foreach (auto step; 1..16) {
682 if (!it.isCollision()) { if (UnstickDebug) writeln(" OK at dx=", dx); break; }
684 if (!it.isCollision()) { if (UnstickDebug) writeln(" OK at dx=-", dx); break; }
686 if (!it.isCollision()) break;
687 if (it.isCollisionBottom(0)) {
688 if (UnstickDebug) writeln(" slide up");
690 } else if (it.isCollisionTop(0)) {
691 if (UnstickDebug) writeln(" slide down");
695 if (it.isCollision()) {
696 if (UnstickDebug) writeln(" CANNOT UNSTUCK!");
700 if (UnstickDebug) writeln(" MOVED BY (", it.ix-ox, ",", it.iy-oy, ")");
701 //if (it.isCollision()) FatalError("FUCK?!");
709 // ////////////////////////////////////////////////////////////////////////// //
710 // drop currently held item
711 bool scrDropItem (LostCause cause, optional float xVel, optional float yVel) {
712 if (holdItem isa PlayerWeapon) return false;
714 if (!holdItem) return false;
716 if (!onLoosingHeldItem(cause)) return false;
721 if (!hi.onLostAsHeldItem(self, cause, xVel!optional, yVel!optional)) {
727 if (hi isa ItemRopeThrow) global.rope = max(0, global.rope-1);
728 else if (hi isa ItemBomb && !ItemBomb(hi).armed) global.bombs = max(0, global.bombs-1);
732 unstuckDroppedObject(hi);
734 scrSwitchToPocketItem(forceIfEmpty:true);
739 // ////////////////////////////////////////////////////////////////////////// //
740 void scrUseThrowIt (MapObject it) {
743 it.onBeforeThrowBy(self);
748 if (dir == Dir.Left) {
749 it.xVel = (it.heavy ? -4+xVel : -8+xVel);
750 //foreach (; 0..8) if (level.isSolidAtPoint(ix-8, iy)) it.shiftX(1);
751 //while (!level.isSolidAtPoint(ix-8, iy)) it.shiftX(1); // prevent getting stuck in wall
752 } else if (dir == Dir.Right) {
753 it.xVel = (it.heavy ? 4+xVel : 8+xVel);
754 //foreach (; 0..8) if (level.isSolidAtPoint(ix+8, iy)) it.shiftX(-1);
755 //while (!level.isSolidAtPoint(ix+8, iy)) it.shiftX(-1); // prevent getting stuck in wall
757 it.yVel = (it.heavy ? (kUp ? -4 : -2) : (kUp ? -9 : -3));
758 if (kDown || scrPlayerIsDucking()) {
759 if (platformCharacterIs(ON_GROUND)) {
766 } else if (!global.hasMitt) {
767 if (dir == Dir.Left) {
768 if (level.isSolidAtPoint(ix-8, iy-10)) {
772 } else if (dir == Dir.Right) {
773 if (level.isSolidAtPoint(ix+8, iy-10)) {
780 if (global.hasMitt && !scrPlayerIsDucking()) {
781 it.xVel += (it.xVel < 0 ? -6 : 6);
782 if (!kUp && !kDown) it.yVel = -0.4;
783 else if (kDown) it.yVel = 6;
787 //unstuckDroppedObject(it);
788 if (it.isCollision()) {
790 if (level.isSolidAtPoint(it.ix-8, it.iy)) it.shiftX(8);
791 } else if (it.xVel > 0) {
792 if (level.isSolidAtPoint(it.ix+8, it.iy)) it.shiftX(-8);
793 } else if (it.isCollision()) {
794 int dx = (it.isCollisionLeft(0) ? 1 : it.isCollisionRight(0) ? -1 : 0);
798 if (!it.isCollision()) break;
805 if (it.sprite_index == sBombBag ||
806 it.sprite_index == sBombBox ||
807 it.sprite_index == sRopePile)
811 playSound('sndThrow');
814 auto proj = ItemProjectile(it);
815 if (proj) proj.launchedByPlayer = true;
819 bool scrUseThrowItem () {
820 if (holdItem isa PlayerWeapon) return false;
822 auto hitem = holdItem;
824 if (!hitem) return false;
825 if (!onLoosingHeldItem(LostCause.Unknown)) return false;
830 scrUseThrowIt(hitem);
832 // if we throwing away armed bomb, get previous item back into our hands
835 if (/*ItemBomb(hitem)*/isHoldingBombOrRope()) scrSwitchToPocketItem(forceIfEmpty:false);
837 if (!holdItem) scrSwitchToPocketItem(forceIfEmpty:false);
843 // ////////////////////////////////////////////////////////////////////////// //
844 bool scrPlayerIsDucking () {
845 if (dead) return false;
846 auto spr = getSprite();
847 //if (!spr) return false;
849 spr.Name == 'sDuckLeft' ||
850 spr.Name == 'sCrawlLeft' ||
851 spr.Name == 'sDamselDuckL' ||
852 spr.Name == 'sDamselCrawlL' ||
853 spr.Name == 'sTunnelDuckL' ||
854 spr.Name == 'sTunnelCrawlL';
859 if (holdItem !isa ItemWeaponBow) return false;
860 sndStopSound('sndBowPull');
861 if (!bowArmed) return false;
862 if (!holdItem.onTryUseItem(self)) return false;
867 void scrUsePutItOnGroundHelper (MapObject it, optional float xVelMult, optional float yVelNew) {
870 if (!specified_xVelMult) xVelMult = 0.4;
871 if (!specified_yVelNew) yVelNew = 0.5;
873 //writeln("putting '", GetClassName(hi.Class), "'");
875 if (dir == Dir.Left) {
876 it.xVel = (it.heavy ? -4 : -8);
877 } else if (dir == Dir.Right) {
878 it.xVel = (it.heavy ? 4 : 8);
886 if (ItemGoldIdol(it)) it.flty = iy;
890 if (it.isCollisionBottom(0) && !it.isCollisionTop(1)) {
898 if (it.isCollisionLeft(0)) {
899 if (it.isCollisionRight(1)) break;
901 } else if (it.isCollisionRight(0)) {
902 if (it.isCollisionLeft(1)) break;
910 unstuckDroppedObject(it);
914 // put item which player holds in his hands on the ground if player is ducking
915 // return `true` if item was put
916 bool scrUsePutItemOnGround (optional float xVelMult, optional float yVelNew) {
917 if (holdItem isa PlayerWeapon) return false;
920 if (!hi || !scrPlayerIsDucking()) return false;
922 if (!onLoosingHeldItem(LostCause.Unknown)) return false;
924 //writeln("putting '", GetClassName(hi.Class), "'");
926 if (global.bombs > 0) {
927 auto bomb = ItemBomb(hi);
928 if (bomb && !bomb.armed) global.bombs -= 1;
931 if (global.rope > 0) {
932 auto rope = ItemRopeThrow(hi);
935 rope.falling = false;
945 scrUsePutItOnGroundHelper(hi, xVelMult!optional, yVelNew!optional);
951 bool launchRope (bool goDown, bool doDrop) {
952 if (global.rope < 1) {
954 if (ItemRopeThrow(holdItem)) scrSwitchToPocketItem(forceIfEmpty:false);
960 bool wasHeld = false;
961 ItemRopeThrow rp = ItemRopeThrow(holdItem);
962 int xdelta = (doDrop ? 12 : 16)*(dir == Dir.Left ? -1 : 1);
964 //FIXME: call handler
967 rp.setXY(ix+xdelta, iy);
969 rp = ItemRopeThrow(level.MakeMapObject(ix+xdelta, iy, 'oRopeThrow'));
971 if (rp.heldBy) FatalError("PlayerPawn::launchRope: hold management fucked");
974 //rp.resaleValue = 0;
978 if (platformCharacterIs(ON_GROUND)) rp.startY = iy; // YASM 1.7
990 if (!level.isSolidAtPoint(ix+(doDrop ? 2 : 8), iy)) { //2
991 if (!level.checkTilesInRect(rp.ix-8, rp.iy, 2, 17)) rp.shiftX(-8);
992 else if (!level.checkTilesInRect(rp.ix+7, rp.iy, 2, 17)) rp.shiftX(8);
997 } else if (!level.isSolidAtPoint(ix-(doDrop ? 2 : 8), iy)) { //2
998 if (!level.checkTilesInRect(rp.ix+7, rp.iy, 2, 17)) rp.shiftX(8);
999 else if (!level.checkTilesInRect(rp.ix-8, rp.iy, 2, 17)) rp.shiftX(-8);
1006 // cannot launch rope
1007 /* was commented in the original
1008 if (oPlayer1.facing == 18) {
1009 obj = instance_create(oPlayer1.x-4, oPlayer1.y+2, oRopeThrow);
1012 obj = instance_create(oPlayer1.x+4, oPlayer1.y+2, oRopeThrow);
1017 //writeln("!!! goDown=", goDown, "; doDrop=", doDrop, "; wasHeld=", wasHeld);
1020 if (!wasHeld) doDrop = true;
1024 if (dir == Dir.Left) rp.xVel = -3.2; else rp.xVel = 3.2;
1027 rp.forceFixHoldCoords(self);
1029 scrUsePutItOnGroundHelper(rp);
1033 if (wasHeld) scrSwitchToPocketItem(forceIfEmpty:false);
1035 //writeln("NO DROP!");
1039 //rp.resaleValue = 1; //k8:???
1042 rp.instanceRemove();
1043 if (wasHeld) scrSwitchToPocketItem(forceIfEmpty:false);
1048 level.MakeMapObject(rp.ix, rp.iy, 'oRopeTop');
1055 if (wasHeld) scrSwitchToPocketItem(forceIfEmpty:false);
1056 playSound('sndThrow');
1061 bool scrLaunchBomb () {
1062 if (whipping || global.bombs < 1) return false;
1065 ItemBomb bomb = ItemBomb(level.MakeMapObject(ix, iy, 'oBomb'));
1066 if (!bomb) return false;
1067 bomb.forceFixHoldCoords(self);
1068 bomb.setSticky(global.stickyBombsActive);
1070 bomb.resaleValue = 0;
1072 if (kDown || scrPlayerIsDucking()) {
1073 scrUsePutItOnGroundHelper(bomb);
1075 scrUseThrowIt(bomb);
1082 bool scrUseItem () {
1084 if (!it) return false;
1085 //writeln(GetClassName(holdItem.Class));
1087 //auto spr = holdItem.getSprite();
1089 } else if (holdItem.type == "Sceptre") {
1090 if (kDown) scrUsePutItemOnGround(0.4, 0.5);
1091 if (firing == 0 && !scrPlayerIsDucking()) {
1092 if (facing == LEFT) {
1101 obj = instance_create(x+xofs, y+4, oPsychicCreateP);
1102 obj.xVel = xsgn*rand(1, 3);
1103 obj.yVel = -random(2);
1105 obj = instance_create(x+xofs, y-2, oPsychicWaveP);
1107 playSound(global.sndPsychic);
1108 firing = firingPistolMax;
1110 } else if (holdItem.type == "Teleporter II") {
1111 scrUseTeleporter2();
1112 } else if (holdItem.type == "Bow") {
1114 scrUsePutItemOnGround(0.4, 0.5);
1115 } else if (firing == 0 && !scrPlayerIsDucking() && !bowArmed && global.arrows > 0) {
1117 playSound(global.sndBowPull);
1118 } else if (global.arrows <= 0) {
1119 global.message = "I'M OUT OF ARROWS!";
1120 global.message2 = "";
1121 global.messageTimer = 80;
1127 if (whipping) return false;
1130 if (scrPlayerIsDucking()) scrUsePutItemOnGround();
1134 // you cannot throw away shop items, but can throw dices
1135 if (it.forSale && it !isa ItemDice) {
1136 if (!level.isInShop(ix/16, iy/16)) {
1139 // allow throw/use shop items
1144 //if (it.forSale) writeln(":::FORSALE 000: '", GetClassName(it.Class), "'");
1145 if (!it.onTryUseItem(self)) {
1146 //if (it.forSale) writeln(":::FORSALE 001: '", GetClassName(it.Class), "'");
1155 // ////////////////////////////////////////////////////////////////////////// //
1156 // called by characterStepEvent
1157 // help player jump up through one block wide gaps by nudging them to one side so they don't hit their head
1158 void scrJumpHelper () {
1159 int d = 4; // max distance to nudge player
1161 if (!level.checkTilesInRect(x, y-12, 1, 7)) {
1162 if (level.checkTilesInRect(x-5, y-12, 1, 7) &&
1163 level.checkTilesInRect(x+14, y-12, 1, 7))
1165 while (d > 0 && level.checkTilesInRect(x-5, y-12, 1, 7)) { ++x; shiftX(1); --d; }
1166 } else if (level.checkTilesInRect(x+5, y-12, 1, 7) &&
1167 level.checkTilesInRect(x-14, y-12, 1, 7))
1169 while (d > 0 && level.checkTilesInRect(x+5, y-12, 1, 7)) { --x; shiftX(-1); --d; }
1173 if (!collision_line(x, y-6, x, y-12, oSolid, 0, 0)) {
1174 if (collision_line(x-5, y-6, x-5, y-12, oSolid, 0, 0) &&
1175 collision_line(x+14, y-6, x+14, y-12, oSolid, 0, 0))
1177 while (collision_line(x-5, y-6, x-5, y-12, oSolid, 0, 0) && d > 0) {
1182 else if (collision_line(x+5, y-6, x+5, y-12, oSolid, 0, 0) and
1183 collision_line(x-14, y-6, x-14, y-12, oSolid, 0, 0))
1185 while (collision_line(x+5, y-6, x+5, y-12, oSolid, 0, 0) && d > 0) {
1195 // ////////////////////////////////////////////////////////////////////////// //
1197 * Returns whether a GENERAL trait about a character is true.
1198 * Only the platform character should run this script.
1200 * `tp` can be one of the following:
1205 final bool platformCharacterIs (int tp) {
1206 if (tp == ON_GROUND && (status == RUNNING || status == STANDING || status == DUCKING || status == LOOKING_UP)) return true;
1207 if (tp == IN_AIR && (status == JUMPING || status == FALLING)) return true;
1208 if (tp == ON_LADDER && status == CLIMBING) return true;
1213 // ////////////////////////////////////////////////////////////////////////// //
1214 // sets the sprite of the character depending on his/her status
1215 final void characterSprite () {
1216 if (status == STOPPED || status == STOPPED_TUNNEL) {
1217 if (global.isDamsel) setSprite('sDamselLeft');
1218 else if (global.isTunnelMan) setSprite('sTunnelLeft');
1219 else setSprite('sStandLeft');
1224 if (global.isTunnelMan && !stunned && !whipping) {
1226 if (status == STANDING) {
1227 if (!level.isSolidAtPoint(x-2, y+9)) {
1229 setSprite('sTunnelWhoaL');
1231 setSprite('sTunnelLeft');
1234 if (status == RUNNING) {
1235 if (kUp) setSprite('sTunnelLookRunL'); else setSprite('sTunnelRunL');
1237 if (status == DUCKING) {
1238 if (xVel == 0) setSprite('sTunnelDuckL');
1239 else if (fabs(xVel) < 3) setSprite('sTunnelCrawlL');
1240 else setSprite('sTunnelRunL');
1242 if (status == LOOKING_UP) {
1243 if (fabs(xVel) > 0) setSprite('sTunnelRunL'); else setSprite('sTunnelLookL');
1245 if (status == JUMPING) setSprite('sTunnelJumpL');
1246 if (status == FALLING && statePrev == FALLING && statePrevPrev == FALLING) setSprite('sTunnelFallL');
1247 if (status == HANGING) setSprite('sTunnelHangL');
1248 if (pushTimer > 20) setSprite('sTunnelPushL');
1249 if (status == DUCKTOHANG) setSprite('sTunnelDtHL');
1250 if (status == CLIMBING) {
1251 if (level.isRopeAtPoint(x, y)) {
1252 if (kDown) setSprite('sTunnelClimb3'); else setSprite('sTunnelClimb2');
1254 setSprite('sTunnelClimb');
1257 } else if (global.isDamsel && !stunned && !whipping) {
1259 if (status == STANDING) {
1260 if (!level.isSolidAtPoint(x-2, y+9)) {
1262 setSprite('sDamselWhoaL');
1263 /* was commented out in the original
1264 if (holdItem && whoaTimer < 1) {
1265 holdItem.held = false;
1266 if (facing == LEFT) holdItem.xVel = -2; else holdItem.xVel = 2;
1267 if (holdItem.type == "Damsel") playSound('sndDamsel');
1268 if (holdItem.type == pickupItemType) { holdItem = 0; pickupItemType = ""; } else scrSwitchToPocketItem();
1272 setSprite('sDamselLeft');
1275 if (status == RUNNING) {
1276 if (kUp) setSprite('sDamselRunL'); else setSprite('sDamselRunL');
1278 if (status == DUCKING) {
1279 if (xVel == 0) setSprite('sDamselDuckL');
1280 else if (fabs(xVel) < 3) setSprite('sDamselCrawlL');
1281 else setSprite('sDamselRunL');
1283 if (status == LOOKING_UP) {
1284 if (fabs(xVel) > 0) setSprite('sDamselRunL'); else setSprite('sDamselLookL');
1286 if (status == JUMPING) setSprite('sDamselDieLR');
1287 if (status == FALLING && statePrev == FALLING && statePrevPrev == FALLING) setSprite('sDamselFallL');
1288 if (status == HANGING) setSprite('sDamselHangL');
1289 if (pushTimer > 20) setSprite('sDamselPushL');
1290 if (status == DUCKTOHANG) setSprite('sDamselDtHL');
1291 if (status == CLIMBING) {
1292 if (level.isRopeAtPoint(x, y)) {
1293 if (kDown) setSprite('sDamselClimb3'); else setSprite('sDamselClimb2');
1295 setSprite('sDamselClimb');
1298 } else if (!stunned && !whipping) {
1300 if (status == STANDING) {
1301 if (!level.checkTileAtPoint(x-(dir == Dir.Left ? 2 : 0), y+9, &level.cbCollisionForWhoa)) {
1303 setSprite('sWhoaLeft');
1304 /* was commented out in the original
1305 if (holdItem && whoaTimer < 1) {
1306 holdItem.held = false;
1307 if (facing == LEFT) holdItem.xVel = -2; else holdItem.xVel = 2;
1308 if (holdItem.type == "Damsel") playSound('sndDamsel');
1309 if (holdItem.type == pickupItemType) { holdItem = 0; pickupItemType = ""; } else scrSwitchToPocketItem();
1313 setSprite('sStandLeft');
1316 if (status == RUNNING) {
1317 if (kUp) setSprite('sLookRunL'); else setSprite('sRunLeft');
1319 if (status == DUCKING) {
1320 if (xVel == 0) setSprite('sDuckLeft');
1321 else if (fabs(xVel) < 3) setSprite('sCrawlLeft');
1322 else setSprite('sRunLeft');
1324 if (status == LOOKING_UP) {
1325 if (fabs(xVel) > 0) setSprite('sLookRunL'); else setSprite('sLookLeft');
1327 if (status == JUMPING) setSprite('sJumpLeft');
1328 if (status == FALLING && statePrev == FALLING && statePrevPrev == FALLING) setSprite('sFallLeft');
1329 if (status == HANGING) setSprite('sHangLeft');
1330 if (pushTimer > 20) setSprite('sPushLeft');
1331 if (status == CLIMBING) {
1332 if (level.isRopeAtPoint(x, y)) {
1333 if (kDown) setSprite('sClimbUp3'); else setSprite('sClimbUp2');
1335 setSprite('sClimbUp');
1338 if (status == DUCKTOHANG) setSprite('sDuckToHangL');
1344 // ////////////////////////////////////////////////////////////////////////// //
1345 void addScore (int delta) {
1346 if (!level.isNormalLevel()) return;
1348 if (delta == 0) return;
1349 level.stats.addMoney(delta);
1351 level.xmoney += delta;
1352 level.collectCounter = min(100, level.collectCounter+20);
1357 // ////////////////////////////////////////////////////////////////////////// //
1358 // for dead players too
1359 // first, the code will call `onObjectTouched()` for player
1360 // if it returned `false`, the code will call `obj.onTouchedByPlayer()`
1361 // note that player's handler is called *after* its frame thinker,
1362 // but object handler is called *before* frame thinker for the object
1363 // i.e. return `true` to block calling `obj.onTouchedByPlayer()`,
1364 // (but NOT object thinker)
1365 bool onObjectTouched (MapObject obj) {
1367 if (dead || global.plife <= 0) return false; // player may be rendered dead, but not yet transited to dead state
1369 if (obj isa ItemProjectileArrow && holdItem isa ItemWeaponBow && !stunned && global.arrows < 99) {
1370 if (fabs(obj.xVel) < 1 && fabs(obj.yVel) < 1 && !obj.stuck) {
1372 playSound('sndPickup');
1373 obj.instanceRemove();
1379 auto treasure = ItemTreasure(obj);
1380 if (treasure && treasure.canCollect) {
1381 if (treasure.value) addScore(treasure.value);
1382 treasure.onCollected(self); // various other effects
1383 playSound(treasure.soundName);
1384 treasure.instanceRemove();
1389 if (global.hasKapala && obj isa MapObjBlood) {
1390 global.bloodLevel += 1;
1391 level.MakeMapObject(obj.ix, obj.iy, 'oBloodSpark');
1392 obj.instanceRemove();
1394 if (global.bloodLevel > 8) {
1395 global.bloodLevel = 0;
1397 level.MakeMapObject(ix, iy-8, 'oHeart');
1398 playSound('sndKiss');
1401 if (redColor < 55) redColor += 5;
1405 // other objects will take care of themselves
1410 // return `false` to prevent
1411 // holdItem is valid
1412 bool onLoosingHeldItem (LostCause cause) {
1413 if (level.inWinCutscene != 0) return false;
1418 // ////////////////////////////////////////////////////////////////////////// //
1419 // k8: don't even ask me! the following mess is almost straightforward port of the original Derek's code!
1420 private final void closeCape () {
1421 auto pp = PPCape(findPowerup('Cape'));
1422 if (pp) pp.open = false;
1426 private final void switchCape () {
1427 auto pp = PPCape(findPowerup('Cape'));
1428 if (pp) pp.open = !pp.open;
1432 final bool isCapeActiveAndOpen () {
1433 auto pp = PPCape(findPowerup('Cape'));
1434 return (pp && pp.active && pp.open);
1438 final bool isParachuteActive () {
1439 auto pp = findPowerup('Parachute');
1440 return (pp && pp.active);
1444 // ////////////////////////////////////////////////////////////////////////// //
1446 bool checkSkipCutScene () {
1447 if (skipCutscenePressed) {
1448 return level.isKeyReleased(GameConfig::Key.Pay);
1450 skipCutscenePressed = level.isKeyPressed(GameConfig::Key.Pay);
1458 bool forcePlayerControls () {
1459 if (level.inWinCutscene) {
1461 level.winCutscenePlayerControl(self);
1463 } else if (level.inIntroCutscene) {
1465 level.introCutscenePlayerControl(self);
1468 } else if (level.levelKind == GameLevel::LevelKind.Transition) {
1469 if (status == STOPPED_TUNNEL) {
1470 auto tman = MonsterTunnelMan(level.isObjectAtPoint(ix+8, iy+4, delegate bool (MapObject o) { return (o isa MonsterTunnelMan); }));
1471 if (tman && !tman.playerGoAway) {
1473 return false; // allow movement
1475 status = STANDING; // go on
1476 foreach (MonsterTunnelMan tm; level.objGrid.allObjects(MonsterTunnelMan)) tm.playerMovedAway();
1479 foreach (MonsterTunnelMan tm; level.objGrid.allObjects(MonsterTunnelMan)) tm.doTalk();
1483 if (checkSkipCutScene()) {
1484 level.playerExited = true;
1488 auto door = level.checkTileAtPoint(ix, iy, &level.cbCollisionExitTile);
1490 kExitPressed = true;
1494 if (status == STOPPED) {
1495 if (--transKissTimer > 0) return true;
1500 auto dms = MonsterDamselKiss(level.isObjectAtPoint(ix+8, iy+4, delegate bool (MapObject o) { return (o isa MonsterDamselKiss); }));
1501 if (dms && !dms.kissed) {
1506 transKissTimer = 30;
1510 if (level.stats.money > 0) {
1511 auto tm = MonsterTunnelMan(level.isObjectAtPoint(ix+8, iy+4, delegate bool (MapObject o) { return (o isa MonsterTunnelMan); }));
1512 if (tm && !tm.playerGoAway) {
1514 status = STOPPED_TUNNEL;
1522 kRightPressed = true;
1529 // ////////////////////////////////////////////////////////////////////////// //
1530 private final void checkControlKeys (SpriteImage spr) {
1531 if (forcePlayerControls()) {
1532 if (movementBlocked) unpressAllKeys();
1533 if (kLeft) kLeftPushedSteps += 1; else kLeftPushedSteps = 0;
1534 if (kRight) kRightPushedSteps += 1; else kRightPushedSteps = 0;
1538 kLeft = level.isKeyDown(GameConfig::Key.Left);
1539 if (movementBlocked) kLeft = false;
1540 if (kLeft) kLeftPushedSteps += 1; else kLeftPushedSteps = 0;
1541 kLeftPressed = level.isKeyPressed(GameConfig::Key.Left);
1542 kLeftReleased = level.isKeyReleased(GameConfig::Key.Left);
1544 kRight = level.isKeyDown(GameConfig::Key.Right);
1545 if (movementBlocked) kRight = false;
1546 if (kRight) kRightPushedSteps += 1; else kRightPushedSteps = 0;
1547 kRightPressed = level.isKeyPressed(GameConfig::Key.Right);
1548 kRightReleased = level.isKeyReleased(GameConfig::Key.Right);
1550 kUp = level.isKeyDown(GameConfig::Key.Up);
1551 kDown = level.isKeyDown(GameConfig::Key.Down);
1552 kRun = level.isKeyDown(GameConfig::Key.Run);
1554 kJump = level.isKeyDown(GameConfig::Key.Jump);
1555 kJumpPressed = level.isKeyPressed(GameConfig::Key.Jump);
1556 kJumpReleased = level.isKeyReleased(GameConfig::Key.Jump);
1558 if (movementBlocked) unpressAllKeys();
1562 kJumpPressed = false;
1563 kJumpReleased = false;
1565 } else if (spr && global.isTunnelMan && spr.Name == 'sTunnelAttackL' && !holdItem) {
1567 kJumpPressed = false;
1568 kJumpReleased = false;
1569 cantJump = max(0, cantJump-1);
1572 kAttack = level.isKeyDown(GameConfig::Key.Attack);
1573 kAttackPressed = level.isKeyPressed(GameConfig::Key.Attack);
1574 kAttackReleased = level.isKeyReleased(GameConfig::Key.Attack);
1576 kItemPressed = level.isKeyPressed(GameConfig::Key.Switch);
1577 kRopePressed = level.isKeyPressed(GameConfig::Key.Rope);
1578 kBombPressed = level.isKeyPressed(GameConfig::Key.Bomb);
1580 kPayPressed = level.isKeyPressed(GameConfig::Key.Pay);
1582 if (movementBlocked) unpressAllKeys();
1584 // unpress "dangerous" keys
1585 if (status == STOPPED_TUNNEL) {
1587 kAttackPressed = false;
1588 kAttackReleased = false;
1589 kItemPressed = false;
1590 kRopePressed = false;
1591 kBombPressed = false;
1593 foreach (MonsterTunnelMan tm; level.objGrid.allObjects(MonsterTunnelMan)) {
1594 tm.upPressed = level.isKeyPressed(GameConfig::Key.Up);
1595 tm.downPressed = level.isKeyPressed(GameConfig::Key.Down);
1596 tm.payPressed = kPayPressed;
1598 tm.downHold = kDown;
1602 kExitPressed = false;
1603 if (global.config.useDoorWithButton) {
1604 if (kPayPressed) kExitPressed = true;
1606 if (kUp) kExitPressed = true;
1609 if (stunned || dead) {
1611 //level.clearKeysPressRelease();
1616 // ////////////////////////////////////////////////////////////////////////// //
1617 // knock off monkeys that grabbed you
1618 void knockOffMonkeys () {
1619 level.forEachObject(delegate bool (MapObject o) {
1620 auto mk = EnemyMonkey(o);
1621 if (mk && !mk.dead && mk.status == GRAB) {
1622 mk.xVel = global.randOther(0, 1)-global.randOther(0, 1);
1625 mk.vineCounter = 20;
1626 mk.grabCounter = 60;
1633 // ////////////////////////////////////////////////////////////////////////// //
1634 // fix collision with boulder (bug with non-aligned boulder)
1635 void hackBoulderCollision () {
1636 auto bld = level.checkTilesInRect(x0, y0, width, height, delegate bool (MapTile o) { return (o isa ObjBoulder); });
1637 if (bld && fabs(bld.xVel) <= 1) {
1638 writeln("IN BOULDER!");
1641 writeln(" LEFT: dx=", dx);
1642 if (dx <= 2) fltx = x0-dx;
1643 } else if (x1 > bld.x1) {
1645 writeln(" RIGHT: dx=", dx);
1646 if (dx <= 2) fltx = x1-dx;
1652 // ////////////////////////////////////////////////////////////////////////// //
1653 bool checkHangTileDG (MapTile t) { return (t.solid || t.tree); }
1656 void checkPerformHang (bool colLeft, bool colRight) {
1657 if (status == HANGING || platformCharacterIs(ON_GROUND)) return;
1658 if ((kLeft && kRight) || (!kLeft && !kRight)) return;
1659 if (kLeft && !colLeft) {
1661 writeln("checkPerformHang: no left solid");
1665 if (kRight && !colRight) {
1667 writeln("checkPerformHang: no right solid");
1671 if (hangCount != 0) {
1673 writeln("checkPerformHang: hangCount=", hangCount);
1677 if (iy <= 16) return;
1678 int dx = (kLeft ? -9 : 9);
1680 writeln("checkPerformHang: trying to hang at ", dx);
1683 bool doHang = false;
1685 if (global.hasGloves) {
1686 doHang = (yVel > 0 && !!level.checkTilesInRect(ix+dx, iy-6, 1, 2, &checkHangTileDG));
1689 doHang = !!level.checkTilesInRect(ix+dx, iy-6, 1, 2, &level.cbCollisionAnyTree);
1691 writeln(" tree: ", doHang);
1695 doHang = level.checkTilesInRect(ix+dx, iy-6, 1, 2) &&
1696 !level.isSolidAtPoint(ix+dx, iy-9) && !level.isSolidAtPoint(ix, iy+9);
1698 writeln(" solid: ", doHang);
1703 writeln(" solid at dx, -6(1): ", !!level.checkTilesInRect(ix+dx, iy-6, 1, 2));
1704 writeln(" solid at dx, -9(0): ", !!level.isSolidAtPoint(ix+dx, iy-9));
1705 writeln(" solid at 0, +9(0): ", !!level.isSolidAtPoint(ix, iy-9));
1708 doHang = level.checkTilesInRect(ix+dx, iy-6, 1, 2) &&
1709 !level.isSolidAtPoint(ix+dx, iy-10) && !level.isSolidAtPoint(ix, iy+9);
1711 if (!doHang) writeln(" easier hang failed");
1714 if (!level.isSolidAtPoint(ix, iy-9)) {
1715 foreach (int dy; 6..24) {
1716 writeln(" solid at dx:-", dy, "(0): ", !!level.isSolidAtPoint(ix+dx, iy-dy));
1718 writeln(" ix=", ix, "; iy=", iy);
1735 // ////////////////////////////////////////////////////////////////////////// //
1736 final void characterStepEvent () {
1737 if (climbSoundTimer > 0) {
1738 if (--climbSoundTimer == 0) {
1739 playSound(climbSndToggle ? 'sndClimb2' : 'sndClimb1');
1740 climbSndToggle = !climbSndToggle;
1744 auto spr = getSprite();
1745 checkControlKeys(spr);
1747 float xPrev = fltx, yPrev = flty;
1750 // check collisions in various directions
1751 bool colSolidLeft = !!getPushableLeft(1);
1752 bool colSolidRight = !!getPushableRight(1);
1753 bool colLeft = !!isCollisionLeft(1);
1754 bool colRight = !!isCollisionRight(1);
1755 bool colTop = !!isCollisionTop(1);
1756 bool colBot = !!isCollisionBottom(1);
1757 bool colLadder = !!isCollisionLadder();
1758 bool colPlatBot = !!isCollisionBottom(1, &level.cbCollisionPlatform);
1759 bool colPlat = !!isCollision(&level.cbCollisionPlatform);
1760 //bool colWaterTop = !!isCollisionTop(1, &level.cbCollisionWater);
1761 bool colWaterTop = !!level.checkTilesInRect(x0, y0-1, width, 3, &level.cbCollisionWater);
1762 bool colIceBot = !!level.isIceAtPoint(x, y+8);
1764 bool runKey = false;
1766 if (level.isKeyDown(GameConfig::Key.Run)) { runHeld = 100; runKey = true; }
1767 if (level.isKeyDown(GameConfig::Key.Attack) && !whipping) { runHeld += 1; runKey = true; }
1769 if (kRun) { runHeld = 100; runKey = true; }
1770 if (kAttack && !whipping) { runHeld += 1; runKey = true; }
1771 if (!runKey || (!kLeft && !kRight)) runHeld = 0;
1773 // allows the character to run left and right
1774 // if state!=DUCKING and state!=LOOKING_UP and state!=CLIMBING
1775 if (status != CLIMBING && status != HANGING) {
1776 if (kLeftReleased && fabs(xVel) < 0.0001) xAcc -= 0.5;
1777 if (kRightReleased && fabs(xVel) < 0.0001) xAcc += 0.5;
1778 if (kLeft && !kRight) {
1780 //xVel = 3; // in orig
1781 if (platformCharacterIs(ON_GROUND) && status != DUCKING) {
1784 //playSound('sndPush', unique:true);
1786 } else if (kLeftPushedSteps > 2 && (dir == Dir.Left || fabs(xVel) < 0.0001)) {
1790 //if (platformCharacterIs(ON_GROUND) && fabs(xVel) > 0 && alarm[3] < 1) alarm[3] = floor(16/-xVel);
1792 if (kRight && !kLeft) {
1793 if (colSolidRight) {
1794 //xVel = 3; // in orig
1795 if (platformCharacterIs(ON_GROUND) && status != DUCKING) {
1798 //playSound('sndPush', unique:true);
1800 } else if ((kRightPushedSteps > 2 || colSolidLeft) && (dir == Dir.Right || fabs(xVel) < 0.0001)) {
1804 //if (platformCharacterIs(ON_GROUND) && fabs(xVel) > 0 && alarm[3] < 1) alarm[3] = floor(16/xVel);
1809 if (status == CLIMBING) {
1813 auto ladder = level.isLadderAtPoint(x, y);
1814 if (ladder) { x = ladder.ix+8; setX(x); }
1815 if (kLeft) dir = Dir.Left; else if (kRight) dir = Dir.Right;
1817 // checks both ladder and laddertop
1818 if (level.isAnyLadderAtPoint(x, y-8)) {
1819 //writeln("LADDER00! old yAcc=", yAcc, "; climbAcc=", climbAcc, "; new yAcc=", yAcc-climbAcc);
1821 if (climbSoundTimer < 1) climbSoundTimer = climbSndSpeed;
1822 //!if (alarm[2] < 1) alarm[2] = climbSndSpeed;
1825 for (int dy = -6; dy > -12; --dy) {
1826 ladder = level.isAnyLadderAtPoint(x, y+dy);
1828 writeln("::: ", dy, ": plrx=", x, "; ladder.xy0=(", ladder.x0, ",", ladder.y0, "); ladder.ixy=(", ladder.ix, ",", ladder.iy, "); wdt=", ladder.width, "; hgt=", ladder.height, "; ladder class=", GetClassName(ladder.Class));
1833 auto grid = level.miscTileGrid;
1834 foreach (MapTile t; grid.inCellPix(48, 96, grid.nextTag(), precise:false)) {
1835 writeln("at 48, 96: ", GetClassName(t.Class), "; pos=(", t.ix, ",", t.iy, ")");
1837 foreach (MapTile t; grid.inCellPix(48, 94, grid.nextTag(), precise:false)) {
1838 writeln("at 48, 94: ", GetClassName(t.Class), "; pos=(", t.ix, ",", t.iy, ")");
1840 foreach (int dy; 90..102) {
1841 ladder = level.isAnyLadderAtPoint(48, dy);
1843 writeln("::: ", dy, ": plrx=", x, "; ladder.xy0=(", ladder.x0, ",", ladder.y0, "); ladder.ixy=(", ladder.ix, ",", ladder.iy, "); wdt=", ladder.width, "; hgt=", ladder.height, "; ladder class=", GetClassName(ladder.Class));
1849 // checks both ladder and laddertop
1850 if (level.isAnyLadderAtPoint(x, y+8)) {
1852 //!if (alarm[2] < 1) alarm[2] = climbSndSpeed;
1853 if (climbSoundTimer < 1) climbSoundTimer = climbSndSpeed;
1857 if (colBot) status = STANDING;
1860 if (kJumpPressed && !whipping) {
1861 if (kLeft) xVel = -departLadderXVel; else if (kRight) xVel = departLadderXVel; else xVel = 0;
1862 //yAcc += departLadderYVel;
1863 //k8: was `0.6`, but with `0.4` we can jump onto the wall above, and with `0.6` we cannot
1864 yAcc = 0.4+departLadderYVel; // YASM 1.8.1 Fix for extra air when jumping off ladders due to increased climb speed option
1866 jumpButtonReleased = false;
1871 if (ladderTimer > 0) ladderTimer -= 1;
1874 if (platformCharacterIs(IN_AIR) && status != HANGING) yAcc += gravityIntensity;
1876 // player has landed
1877 if ((colBot || colPlatBot) && platformCharacterIs(IN_AIR) && yVel >= 0) {
1878 if (!colPlat || colBot) {
1883 playSound('sndLand');
1885 if ((colBot || colPlatBot) && !colPlat) yVel = 0;
1887 // player has just walked off of the edge of a solid
1888 if (colBot == 0 && (!colPlatBot || colPlat) && platformCharacterIs(ON_GROUND)) {
1892 if (global.hasGloves) hangCount = 5;
1896 if (dead || stunned) yVel = -yVel*0.8;
1897 else if (status == JUMPING) yVel = fabs(yVel*0.3);
1900 if ((colLeft && dir == Dir.Left) || (colRight && dir == Dir.Right)) {
1901 if (dead || stunned) xVel = -xVel*0.5; else xVel = 0;
1905 if (kJumpReleased && platformCharacterIs(IN_AIR)) {
1907 } else if (platformCharacterIs(ON_GROUND)) {
1912 MapObject oWeb = none, oBlob = none;
1914 oWeb = level.isObjectAtPoint(x, y, &level.cbIsObjectWeb);
1915 if (!oWeb) oBlob = level.isObjectAtPoint(x, y, &level.cbIsObjectBlob);
1918 bool invokeJumpHelper = false;
1920 if (kJumpPressed && oWeb) {
1921 ItemWeb(oWeb).tear(1);
1922 yAcc += initialJumpAcc*2;
1927 jumpButtonReleased = false;
1931 invokeJumpHelper = true;
1932 } else if (kJumpPressed && oBlob) {
1934 scrCreateBloblets(oBlob.x0+8, oBlob.y0+8, 1);
1935 playSound('sndHit');
1936 yAcc += initialJumpAcc*2;
1940 jumpButtonReleased = false; // k8: was `jumpButtonRelease`
1942 invokeJumpHelper = true;
1943 } else if (kJumpPressed && colWaterTop) {
1944 yAcc += initialJumpAcc*2;
1949 jumpButtonReleased = false;
1953 invokeJumpHelper = true;
1954 } else if (global.hasCape && kJumpPressed && kJumped && platformCharacterIs(IN_AIR)) {
1956 } else if (global.hasJetpack && !swimming && kJump && kJumped && platformCharacterIs(IN_AIR) && jetpackFuel > 0) {
1957 yAcc += initialJumpAcc;
1960 if (jetpackFlaresTime < 1) jetpackFlaresTime = 3;
1961 //!if (alarm[10] < 1) alarm[10] = 3; // jetpack flares
1965 jumpButtonReleased = false;
1969 invokeJumpHelper = true;
1970 } else if (platformCharacterIs(ON_GROUND) && kJumpPressed && fallTimer == 0) {
1971 if (fabs(xVel) > 3 /*xVel > 3 || xVel < -3*/) {
1972 yAcc += initialJumpAcc*2;
1975 yAcc += initialJumpAcc*2;
1977 //scrJumpHelper(); // move to location where player doesn't have to be on ground
1979 if (global.hasJordans) {
1983 } else if (global.hasSpringShoes) {
1990 playSound('sndJump');
1994 // the "state" gets changed to JUMPING later on in the code
1996 // "variable jumping" states
1997 jumpButtonReleased = false;
1999 invokeJumpHelper = true;
2002 if (kJumpPressed && invokeJumpHelper) scrJumpHelper(); // YASM 1.8.1
2004 if (jumpTime < jumpTimeTotal) jumpTime += 1;
2005 // let the character continue to jump
2006 if (!kJump) jumpButtonReleased = true;
2007 if (jumpButtonReleased) jumpTime = jumpTimeTotal;
2009 gravityIntensity = (jumpTime/jumpTimeTotal)*grav;
2011 if (kUp && platformCharacterIs(ON_GROUND) && !colLadder) {
2012 //k8:!!!looking = UP;
2013 if (xVel == 0 && xAcc == 0) status = LOOKING_UP;
2015 //k8:!!!looking = 0;
2018 if (!kUp && status == LOOKING_UP) status = STANDING;
2022 checkPerformHang(colLeft, colRight);
2026 // hang on stuck arrow
2027 if (status == FALLING && hangCount == 0 && y > 16 && !platformCharacterIs(ON_GROUND) &&
2028 !level.isSolidAtPoint(x, y+12)) // from Spelunky Natural
2030 auto arrow = level.isObjectInRect(ix, iy, 16, 16, delegate bool (MapObject o) {
2033 writeln(" ARROW : (", o.x0, ",", o.y0, ")-(", o.x1, ",", o.y1, "); coll=", o.collidesWith(self));
2034 writeln(" PLAYER: (", x0, ",", y0, ")-(", x1, ",", y1, "); coll=", self.collidesWith(o), "; dy=", iy-o.iy);
2036 if (o.stuck && iy-o.iy >= -6 && iy-o.iy <= -5 && o.collidesWith(self)) {
2037 //writeln(" *** HANG IS POSSIBLE! p5=", !!level.isObjectAtPoint(ix, iy-5, &level.cbIsObjectArrow), "; p6=", !!level.isObjectAtPoint(ix, iy-6, &level.cbIsObjectArrow));
2041 }, castClass:ItemProjectileArrow, precise:false);
2044 // move_snap(1, 8); // was commented out in the original
2050 writeln("TRYING ARROW HANG ALLOWED");
2051 writeln(" Z00: ", !level.isObjectAtPoint(x, y-9, &level.cbIsObjectArrow));
2052 writeln(" Z01: ", !level.isObjectAtPoint(x, y+9, &level.cbIsObjectArrow));
2053 writeln(" Z02: ", !!level.isObjectAtPoint(x, y-5, &level.cbIsObjectArrow));
2054 writeln(" Z03: ", !!level.isObjectAtPoint(x, y-6, &level.cbIsObjectArrow));
2055 level.isObjectInRect(ix, iy, 16, 16, delegate bool (MapObject o) {
2057 writeln(" ARROW : (", o.x0, ",", o.y0, ")-(", o.x1, ",", o.y1, "); coll=", o.collidesWith(self));
2058 writeln(" PLAYER: (", x0, ",", y0, ")-(", x1, ",", y1, "); coll=", self.collidesWith(o), "; dy=", iy-o.iy);
2059 if (iy-o.iy >= -6 && iy-o.iy <= -5 && o.collidesWith(self)) {
2060 writeln(" *** HANG IS POSSIBLE! p5=", !!level.isObjectAtPoint(ix, iy-5, &level.cbIsObjectArrow), "; p6=", !!level.isObjectAtPoint(ix, iy-6, &level.cbIsObjectArrow));
2063 }, castClass:ItemProjectileArrow, precise:false);
2067 // hang on stuck arrow
2068 /*k8: this is not working due to collision issues; see the fixed code above
2069 if (status == FALLING && hangCount == 0 && y > 16 && !platformCharacterIs(ON_GROUND) &&
2070 !level.isSolidAtPoint(x, y+12) && // from Spelunky Natural
2071 !level.isObjectAtPoint(x, y-9, &level.cbIsObjectArrow) && !level.isObjectAtPoint(x, y+9, &level.cbIsObjectArrow))
2073 //obj = instance_nearest(x, y-5, oArrow);
2074 auto arr0 = level.isObjectAtPoint(x, y-5, &level.cbIsObjectArrow);
2075 auto arr1 = level.isObjectAtPoint(x, y-6, &level.cbIsObjectArrow);
2077 writeln("ARROW HANG!");
2078 // get nearest arrow
2081 arr = (arr0.distanceToPoint(x, y-5) < arr1.distanceToPoint(x, y-5) ? arr0 : arr1);
2083 arr = (arr0 ? arr0 : arr1);
2087 // move_snap(1, 8); // was commented out in the original
2095 /* this was commented in the original
2096 if (hangCount == 0 && y > 16 && !platformCharacterIs(ON_GROUND) && state == FALLING &&
2097 (collision_point(x, y-5, oTreeBranch, 0, 0) || collision_point(x, y-6, oTreeBranch, 0, 0)) &&
2098 !collision_point(x, y-9, oTreeBranch, 0, 0) && !collision_point(x, y+9, oTreeBranch, 0, 0))
2101 // move_snap(1, 8); // was commented out in the original
2109 if (hangCount > 0) --hangCount;
2111 if (status == HANGING) {
2116 if (global.hasGloves) {
2117 if (hangCount == 0 && y > 16 && !platformCharacterIs(ON_GROUND)) {
2118 if (kRight && colRight &&
2119 (level.isSolidAtPoint(x+9, y-5) || level.isSolidAtPoint(x+9, y-6)))
2125 } else if (kLeft && colLeft &&
2126 (level.isSolidAtPoint(x-9, y-5) || level.isSolidAtPoint(x-9, y-6)))
2148 yAcc += initialJumpAcc*2;
2149 shiftX(dir == Dir.Right ? -2 : 2);
2152 hangCount = hangCountMax;
2153 if (level.isObjectAtPoint(x, y-5, &level.cbIsObjectArrow) || level.isObjectAtPoint(x, y-6, &level.cbIsObjectArrow)) hangCount /= 2; //Spelunky Natural
2156 if ((dir == Dir.Left && !isCollisionLeft(2)) ||
2157 (dir == Dir.Right && !isCollisionRight(2)))
2168 // pressing down while standing
2169 if (kDown && platformCharacterIs(ON_GROUND) && !whipping) {
2172 } else if (colPlatBot) {
2173 // climb down ladder if possible, else jump down
2176 //ladder = instance_place(x, y+16, oLadder);
2178 // from Spelunky Natural
2180 ladder = collision_line(x-4, y+16, x+4, y+16, oLadder, 0, 0);
2181 if (!ladder) ladder = collision_line(x-4, y+16, x+4, y+16, oLadderTop, 0, 0);
2183 auto ladder = level.checkTilesInRect(x-4, y+16, 9, 1, &level.cbCollisionAnyLadder);
2184 //writeln("DOWN; cpb=", colPlatBot, "; cb=", colBot, "; ladder=", !!ladder);
2187 if (abs(x-(ladder.x0+8)) < 4) {
2201 kJumped = true; // Spelunky Natural
2205 // the character can't move down because there is a solid in the way
2210 if (!kDown && status == DUCKING) {
2215 if (xVel == 0 && xAcc == 0 && status == RUNNING) status = STANDING;
2216 if (xAcc != 0 && status == STANDING) status = RUNNING;
2217 if (yVel < 0 && platformCharacterIs(IN_AIR) && status != HANGING) status = JUMPING;
2218 if (yVel > 0 && platformCharacterIs(IN_AIR) && status != HANGING) status = FALLING;
2219 setCollisionBounds(-5, -6, 5, 8);
2222 bool colPointLadder = !!level.isAnyLadderAtPoint(x, y);
2224 /* this was commented in the original
2225 if ((kUp && platformCharacterIs(IN_AIR) && collision_point(x, y-8, oLadder, 0, 0) && ladderTimer == 0) ||
2226 (kUp && colPointLadder && ladderTimer == 0) ||
2227 (kDown && colPointLadder && ladderTimer == 0 && platformCharacterIs(ON_GROUND) && collision_point(x, y+9, oLadderTop, 0, 0) && xVel == 0))
2230 ladder = instance_place(x, y-8, oLadder);
2231 if (instance_exists(ladder)) {
2232 if (abs(x-(ladder.x0+8)) < 4) {
2235 if (!collision_point(x, y, oLadder, 0, 0) && !collision_point(x, y, oLadderTop, 0, 0)) { y = ladder.iy+14; setY(y); }
2245 // Spelunky Natural - Multiple changes to this big "if" condition
2246 if ((kUp && platformCharacterIs(IN_AIR) && ladderTimer == 0 && level.checkTilesInRect(x-2, y-8, 5, 1, &level.cbCollisionLadder)) ||
2247 (kUp && colPointLadder && ladderTimer == 0) ||
2248 (kDown && colPointLadder && ladderTimer == 0 && platformCharacterIs(ON_GROUND) && xVel == 0 && level.isLadderTopAtPoint(x, y+9)) ||
2249 ((kUp || kDown) && status == HANGING && level.checkTilesInRect(x-2, y, 5, 1, &level.cbCollisionLadder)))
2252 //auto ladder = instance_place(x, y-8, oLadder);
2253 auto ladder = level.isLadderAtPoint(x, y-8);
2255 //writeln("LADDER01! plrx=", x, "; ladder.x0=", ladder.x0, "; ladder.ix=", ladder.ix, "; ladder class=", GetClassName(ladder.Class));
2256 if (abs(x-(ladder.x0+8)) < 4) {
2259 if (!level.isAnyLadderAtPoint(x, y)) { y = ladder.y0+14; setY(y); }
2269 /* this was commented in the original
2270 if (sprite_index == sDuckToHangL || sprite_index == sDamselDtHL) {
2272 if (facing == LEFT && collision_rectangle(x-8, y, x, y+16, oLadder, 0, 0) && !collision_point(x-4, y+16, oSolid, 0, 0)) {
2273 ladder = instance_nearest(x-4, y+16, oLadder);
2274 } else if (facing == RIGHT && collision_rectangle(x, y, x+8, y+16, oLadder, 0, 0) && !collision_point(x+4, y+16, oSolid, 0, 0)) {
2275 ladder = instance_nearest(x+4, y+16, oLadder);
2288 if (colLadder && state == CLIMBING && kJumpPressed && !whipping) {
2289 if (kLeft) xVel = -departLadderXVel; else if (kRight) xVel = departLadderXVel; else xVel = 0;
2290 yAcc += departLadderYVel;
2292 jumpButtonReleased = false;
2298 // calculate horizontal/vertical friction
2299 if (status == CLIMBING) {
2300 xFric = frictionClimbingX;
2301 yFric = frictionClimbingY;
2303 //if (runKey && platformCharacterIs(ON_GROUND) && runHeld >= 10)
2304 if ((runKey && runHeld >= 10) && (platformCharacterIs(ON_GROUND) || global.config.toggleRunAnywhere)) {
2310 xFric = frictionRunningFastX;
2311 } else if (kRight) {
2314 xFric = frictionRunningFastX;
2316 } else if (status == DUCKING) {
2317 if (xVel < 2 && xVel > -2) {
2321 } else if (kLeft && global.config.downToRun) {
2325 xFric = frictionRunningFastX;
2326 } else if (kRight && global.config.downToRun) {
2329 xFric = frictionRunningFastX;
2332 if (xVel < 0.5) xVel = 0;
2338 // decrease the friction when the character is "flying"
2339 if (platformCharacterIs(IN_AIR)) {
2340 if (dead || stunned) xFric = 1.0; else xFric = 0.8;
2342 xFric = frictionRunningX;
2346 /* // ORIGINAL RUN/WALK xVel/xFric code this was commented in the original
2347 if (runKey && platformCharacterIs(ON_GROUND) && runHeld >= 10) {
2352 xFric = frictionRunningFastX;
2353 } else if (kRight) {
2356 xFric = frictionRunningFastX;
2358 } else if (state == DUCKING) {
2359 if (xVel < 2 && xVel > -2) {
2363 } else if (kLeft && global.downToRun) {
2367 xFric = frictionRunningFastX;
2368 } else if (kRight && global.downToRun) {
2371 xFric = frictionRunningFastX;
2374 if (xVel < 0.5) xVel = 0;
2380 // decrease the friction when the character is "flying"
2381 if (platformCharacterIs(IN_AIR)) {
2382 if (dead || stunned) xFric = 1.0; else xFric = 0.8;
2384 xFric = frictionRunningX;
2389 // stuck on web or underwater
2390 if (level.isObjectAtPoint(x, y, &level.cbIsObjectWeb)) {
2394 } else if (level.isObjectAtPoint(x, y, &level.cbIsObjectBlob)) {
2396 //obj = instance_place(x, y, oBlob); this was commented in the original
2397 //xVel += obj.xVel; this was commented in the original
2401 } else if (level.isWaterAtPoint(x, y/*, oWater, -1, -1*/)) {
2403 //if (!runKey && global.toggleRunAnywhere) xFric = frictionRunningX; // YASM 1.8.1 this was commented in the original
2404 if (!platformCharacterIs(ON_GROUND)) xFric = frictionRunningX;
2405 if (status == FALLING && yVel > 0) {
2407 if (global.config.naturalSwim && kUp) yFric = 0.2;
2408 else if (global.config.naturalSwim && kDown) yFric = 0.8;
2410 } else if (!level.isWaterAtPoint(x, y-9/*, oWater, -1, -1*/)) {
2415 if (yVel < -6 && global.config.noDolphin) {
2416 // Spelunky Natural (changed from -4 to -6)
2425 if (colIceBot && status != DUCKING && !global.hasSpikeShoes) {
2431 if (global.config.toggleRunAnywhere) {
2432 if (!kJump && !kDown && !runKey) xVelLimit = 3;
2436 if (platformCharacterIs(ON_GROUND)) {
2437 if (status == RUNNING && kLeft && colLeft) pushTimer += 1;
2438 else if (status == RUNNING && kRight && colRight) pushTimer += 1;
2441 //if (platformCharacterIs(ON_GROUND) && !kJump && !kDown && !runKey) this was commented in the original
2442 if (!kJump && !kDown && !runKey) xVelLimit = 3;
2444 /* this was commented in the original
2446 if (state == DUCKING && fabs(xVel) < 3 && facing == LEFT &&
2447 //collision_point(x, y+9, oSolid, 0, 0) && !collision_point(x-1, y+9, oSolid, 0, 0) && kLeft)
2448 collision_point(x, y+9, oSolid, 0, 0) && !collision_line(x-1, y+9, x-10, y+9, oSolid, 0, 0) && kLeft)
2453 if (kLeft && dir == Dir.Left) dhdir = -1;
2454 else if (kRight && dir == Dir.Right) dhdir = 1;
2456 if (dhdir && status == DUCKING && fabs(xVel) < 3+(dhdir < 0 ? 1 : 0) &&
2457 level.isSolidAtPoint(x, y+9) && !level.checkTilesInRect(x+(dhdir < 0 ? -8 : 1), y+9, 8, 8))
2459 status = DUCKTOHANG;
2461 if (!global.config.scumFlipHold || holdItem.heavy) {
2463 holdItem.heldBy = none;
2464 if (holdItem.objName == 'GoldIdol') holdItem.shiftY(-8);
2466 //else if (holdItem.type == "Block Item") { with (oBlockPreview) instance_destroy(); }
2467 scrDropItem(LostCause.Hang, (dir == Dir.Left ? -1 : 1), -4);
2474 if (status == DUCKTOHANG) {
2475 setXY(xPrev, yPrev);
2485 // parachute and cape
2486 if (!level.inWinCutscene) {
2487 if (isParachuteActive() || isCapeActiveAndOpen()) yFric = 0.5;
2490 if (pushTimer > 100) pushTimer = 100;
2492 // limits the acceleration if it is too extreme
2493 xAcc = fclamp(xAcc, -xAccLimit, xAccLimit);
2494 yAcc = fclamp(yAcc, -yAccLimit, yAccLimit);
2496 // applies the acceleration
2498 if (dead || stunned) yVel += 0.6; else yVel += yAcc;
2500 // nullifies the acceleration
2504 // applies the friction to the velocity, now that the velocity has been calculated
2508 auto oBall = getMyBall();
2509 // apply ball and chain
2511 int distsq = (ix-oBall.ix)*(ix-oBall.ix)+(iy-oBall.iy)*(iy-oBall.iy);
2512 if (distsq >= 24*24) {
2513 if (xVel > 0 && oBall.ix < ix && abs(oBall.ix-ix) > 24) xVel = 0;
2514 if (xVel < 0 && oBall.ix > ix && abs(oBall.ix-ix) > 24) xVel = 0;
2515 if (yVel > 0 && oBall.iy < iy && abs(oBall.iy-iy) > 24) {
2516 if (abs(oBall.ix-ix) < 1) {
2517 //teleportTo(destx:oBall.ix);
2519 prevFltX = oBall.prevFltX;
2521 } else if (oBall.ix < ix && !kRight) {
2522 if (xVel > 0) xVel *= -0.25;
2523 else if (xVel == 0) xVel -= 1;
2524 } else if (oBall.ix > ix && !kLeft) {
2525 if (xVel < 0) xVel *= -0.25;
2526 else if (xVel == 0) xVel += 1;
2531 if (yVel < 0 && oBall.iy > iy && abs(oBall.iy-iy) > 24) yVel = 0;
2535 // apply the limits since the velocity may be too extreme
2536 if (!dead && !stunned) xVel = fclamp(xVel, -xVelLimit, xVelLimit);
2537 yVel = fclamp(yVel, -yVelLimit, yVelLimit);
2539 // approximates the "active" variables
2540 if (fabs(xVel) < 0.0001) xVel = 0;
2541 if (fabs(yVel) < 0.0001) yVel = 0;
2542 if (fabs(xAcc) < 0.0001) xAcc = 0;
2543 if (fabs(yAcc) < 0.0001) yAcc = 0;
2545 bool wasInWall = !!isCollision();
2546 moveRel(xVel, yVel);
2548 // don't go out of level (if we're not in ending sequence)
2549 if (!level.inWinCutscene && !level.inIntroCutscene) {
2550 if (ix < 0) fltx = 0;
2551 else if (ix > level.tilesWidth*16-16) fltx = level.tilesWidth*16-16;
2552 if (iy < 0) flty = 0;
2554 if (!dead) hackBoulderCollision();
2556 if (!wasInWall && isCollision()) {
2557 writeln("** FUUUU (XXX)");
2558 if (isCollisionBottom(0) && !isCollisionBottom(-2)) {
2561 // we can stuck in the wall with this
2562 if (isCollisionLeft(0)) {
2563 writeln("** FUUUU (001: left)");
2564 while (isCollisionLeft(0) && !isCollisionRight(1)) shiftX(1);
2565 } else if (isCollisionRight(0)) {
2566 writeln("** FUUUU (001: right)");
2567 while (isCollisionRight(0) && !isCollisionLeft(1)) shiftX(-1);
2571 if (!dead) hackBoulderCollision();
2573 // move out of wall by at most 2 px, if possible
2574 if (!dead && isCollision()) {
2575 foreach (; 0..2) if (isCollisionBottom(0) && !isCollisionBottom(-4)) flty = iy-1;
2576 foreach (; 0..2) if (isCollisionTop(0) && !isCollisionTop(-4)) flty = iy+1;
2577 foreach (; 0..2) if (isCollisionLeft(0) && !isCollisionLeft(-4)) fltx = ix+1;
2578 foreach (; 0..2) if (isCollisionRight(0) && !isCollisionRight(-4)) fltx = ix-1;
2581 if (!dead && isCollision()) {
2582 //k8:HACK: try to duck
2583 bool wallDeath = true;
2587 if (isCollision()) {
2588 if (isCollisionLeft(0) && !isCollisionRight(4)) ix = ix+1;
2589 else if (isCollisionRight(0) && !isCollisionLeft(4)) ix = ix-1;
2590 else if (isCollisionBottom(0) && !isCollisionTop(4)) iy = iy-1;
2591 else if (isCollisionTop(0) && !isCollisionBottom(4)) iy = iy+1;
2596 if (wallDeath && isCollision()) {
2597 level.checkTilesInRect(x0, y0, width, height, delegate bool (MapTile t) {
2599 writeln("mypos=(", x0, ",", y0, ")-(", x1, ",", y1, "); tpos=(", t.x0, ",", t.y0, ")-(", t.x1, ",", t.y1, ")");
2603 if (!dead) level.addDeath('wall');
2606 writeln("PLAYER KILLED BY WALL");
2607 global.plife = 0; // oops
2613 //writeln("flty=", flty, "; iy=", iy);
2619 // figures out what the sprite index of the character should be
2622 // sets the previous state and the previously previous state
2623 statePrevPrev = statePrev;
2626 // calculates the imageSpeed based on the character's velocity
2627 if (status == RUNNING || status == DUCKING || status == LOOKING_UP) {
2628 if (status == RUNNING || status == LOOKING_UP) imageSpeed = fabs(xVel)*runAnimSpeed+0.1;
2631 if (status == CLIMBING) imageSpeed = sqrt(xVel*xVel+yVel*yVel)*climbAnimSpeed;
2633 if (xVel >= 4 || xVel <= -4) {
2635 if (platformCharacterIs(ON_GROUND)) {
2636 setCollisionBounds(-8, -6, 8, 8);
2638 setCollisionBounds(-5, -6, 5, 8);
2641 setCollisionBounds(-5, -6, 5, 8);
2644 if (whipping) imageSpeed = 1;
2646 if (status == DUCKTOHANG) {
2651 // limit the imageSpeed at 1 so the animation always looks good
2652 if (imageSpeed > 1) imageSpeed = 1;
2654 //if (kItemPressed) writeln("ITEM! dead=", dead, "; stunned=", stunned, "; active=", active);
2655 if (dead || stunned || !active) {
2657 } else if (/*inGame &&*/ kItemPressed && !whipping) {
2659 if (kUp) scrSwitchToStickyBombs(); else scrSwitchToNextItem();
2660 } else if (/*inGame &&*/ kRopePressed && global.rope > 0 && !whipping) {
2661 if (!kDown && colTop) {
2664 launchRope(kDown, doDrop:true);
2666 } else if (/*inGame &&*/ kBombPressed && global.bombs > 0 && !whipping) {
2667 if (holdItem isa ItemWeaponBow && bowArmed) {
2668 if (holdArrow != ARROW_BOMB) {
2669 //writeln("set bow arrows to bomb");
2670 holdArrow = ARROW_BOMB;
2672 //writeln("set bow arrows to normal");
2673 holdArrow = ARROW_NORM;
2682 if (!dead && !stunned && kUp && kAttackPressed) {
2683 auto octr = ItemOpenableContainer(level.isObjectInRect(ix, iy, width, height, delegate bool (MapObject o) {
2684 return (o isa ItemOpenableContainer);
2687 if (octr.openMe()) kAttackPressed = false;
2692 // use weapon / attack
2693 if (!dead && !stunned && kAttackPressed && !holdItem /*&& !pickedItem*/) {
2696 sndStopSound('sndBowPull');
2697 if (!global.config.unarmed && status != DUCKING && status != DUCKTOHANG && !whipping && !isExitingSprite()) {
2699 if (global.isTunnelMan) {
2700 if (platformCharacterIs(ON_GROUND) || platformCharacterIs(IN_AIR)) {
2701 setSprite('sTunnelAttackL');
2704 } else if (global.isDamsel) {
2705 setSprite('sDamselAttackL');
2708 setSprite('sAttackLeft');
2711 } else if (kDown && !pickedItem) {
2713 //HACK: always select dice to throw if there are two dices there
2714 MapObject diceToThrow = level.isObjectInRect(x-8, y, 9, 9, delegate bool (MapObject o) {
2715 if (o.spectral || !o.canPickUp) return false;
2716 if (ItemDice(o).isReadyToThrowForBet) return o.collidesWith(self);
2718 }, precise:false, castClass:ItemDice);
2723 obj = level.isObjectInRect(x-8, y, 9, 9, delegate bool (MapObject o) {
2724 if (o.spectral || !o.canPickUp) return false;
2725 if (!o.collidesWith(self)) return false;
2726 return o.onCanBePickedUp(self);
2728 if (o isa MapItem) return (o.active && o.canPickUp && !o.spectral);
2729 if (o isa MapEnemy) return (o.active && o.canPickUp && !o.spectral && (o.dead || o.status >= MapObject::STUNNED || o.meGoldMonkey));
2734 if (!obj && diceToThrow) obj = diceToThrow;
2736 // `canPickUp` is checked in callback
2737 if (/*obj.canPickUp &&*/ true /*k8: do we really need this? !level.isSolidAtPoint(obj.ix+2, obj.iy)*/) {
2738 //pickupItemType = holdItem.type;
2739 //!if (isAshShotgun(holdItem)) pickupItemType = "Boomstick";
2740 //!if (isGoldMonkey(obj) and obj.status < 98) obj.status = 0; // do not play walk animation while held
2742 if (!obj.onTryPickup(self)) {
2743 if (obj.isInstanceAlive) scrPickupItem(obj);
2747 if (holdItem.type == "Bow" and holdItem.new) {
2748 holdItem.new = false;
2750 if (global.arrows > 99) global.arrows = 99;
2756 } else if (!dead && !stunned) {
2757 if (holdItem isa ItemWeaponBow) {
2758 //writeln("BOW! kAttack=", kAttack, "; kAttackPressed=", kAttackPressed, "; bowArmed=", bowArmed, "; bowStrength=", bowStrength, "; holdArrow=", holdArrow);
2759 if (kAttackPressed) {
2760 if (scrPlayerIsDucking()) {
2761 scrUsePutItemOnGround();
2762 } else if (!bowArmed) {
2764 ItemWeaponBow(holdItem).armBow(self);
2768 if (bowArmed && bowStrength < 12) {
2770 //writeln("arming: ", bowStrength);
2772 sndStopSound('sndBowPull');
2775 //writeln(" xxBOW!");
2779 if (!holdArrow) holdArrow = ARROW_NORM;
2781 if (kAttackPressed && holdItem) scrUseItem();
2785 // remove held item offer
2786 if (!level.isInShop(ix/16, iy/16)) {
2787 if (holdItem) holdItem.sellOfferDone = false;
2788 if (pickedItem) pickedItem.sellOfferDone = false;
2792 if (!dead && !stunned && kPayPressed) {
2793 // find nearest shopkeeper
2794 auto sc = MonsterShopkeeper(level.findNearestObject(ix, iy, delegate bool (MapObject o) {
2795 auto sc = MonsterShopkeeper(o);
2796 if (!sc) return false;
2797 //if (needCraps && sc.stype != 'Craps') return false;
2798 if (sc.dead || sc.angered || sc.outlaw) return false;
2799 return sc.canSellItem(self, holdItem);
2801 if (level.isInShop(ix/16, iy/16)) {
2802 // if no shopkeepers found, just use it
2805 holdItem.forSale = false;
2806 holdItem.onTryPickup(self);
2808 } else if (global.thiefLevel == 0 && !global.murderer) {
2809 // only law-abiding players can buy/sell items or play games
2810 if (holdItem) writeln("shop item interaction: ", holdItem.objName, "; cost=", holdItem.cost);
2811 if (sc.doSellItem(self, holdItem)) {
2814 holdItem.forSale = false;
2815 holdItem.onTryPickup(self);
2818 if (holdItem && !holdItem.isInstanceAlive) {
2820 scrSwitchToPocketItem(forceIfEmpty:false); // just in case
2824 // use pickup, if any
2825 if (holdItem isa ItemPickup) {
2826 // make nearest shopkeeper angry (an unlikely situation, but still...)
2827 if (sc && holdItem.forSale) level.scrShopkeeperAnger(GameLevel::SCAnger.ItemStolen);
2828 holdItem.forSale = false;
2829 holdItem.onTryPickup(self);
2831 pickupsAround.clear();
2832 level.isObjectInRect(x0, y0, width, height, delegate bool (MapObject o) {
2833 auto pk = ItemPickup(o);
2834 if (pk && pk.collidesWith(self)) {
2836 foreach (auto opk; pickupsAround) if (opk == pk) { found = true; break; }
2837 if (!found) pickupsAround[$] = pk;
2841 // now try to use all pickups
2842 foreach (ItemPickup pk; pickupsAround) {
2843 if (pk.isInstanceAlive) {
2844 if (sc && pk.forSale) level.scrShopkeeperAnger(GameLevel::SCAnger.ItemStolen);
2846 pk.onTryPickup(self);
2849 pickupsAround.clear();
2855 transient array!ItemPickup pickupsAround;
2858 // ////////////////////////////////////////////////////////////////////////// //
2859 override bool initialize () {
2860 if (!::initialize()) return false;
2862 powerups.length = 0;
2863 powerups[$] = SpawnObject(PPParachute);
2864 powerups[$] = SpawnObject(PPCape);
2866 foreach (PlayerPowerup pp; powerups) pp.owner = self;
2868 if (global.isDamsel) {
2870 desc2 = "An athletic, unfittingly-dressed woman with extremely awkward running form.";
2871 setSprite('sDamselLeft');
2872 } else if (global.isTunnelMan) {
2873 desc = "Tunnel Man";
2874 desc2 = "A miner from the desert. His tools are a cut above the rest.";
2875 setSprite('sTunnelLeft');
2878 desc2 = "A strange little man who spends his time exploring caverns. He wants to be just like Indiana Jones when he grows up.";
2879 setSprite('sStandLeft');
2887 switch (global.config.scumClimbSpeed) {
2890 climbAnimSpeed = 0.4;
2895 climbAnimSpeed = 0.45;
2900 climbAnimSpeed = 0.5;
2905 climbAnimSpeed = 0.5;
2909 climbAcc = 0.6; // how fast the character will climb
2910 climbAnimSpeed = 0.4; // relates to how fast the climbing animation should go
2915 // sets the collision bounds to fit the default sprites (you can edit the arguments of the script)
2916 //setCollisionBounds(-5, -5, 5, 8); // setCollisionBounds(-5, -8, 5, 8);
2917 setCollisionBounds(-5, -6, 5, 8);
2920 statePrevPrev = statePrev;
2921 gravityIntensity = grav; // this variable describes the current force due to gravity (this variable is altered for variable jumping)
2922 jumpTime = jumpTimeTotal; // current time of the jump (0=start of jump, jumpTimeTotal=end of jump)
2928 // ////////////////////////////////////////////////////////////////////////// //
2929 override void onAnimationLooped () {
2930 auto spr = getSprite();
2931 if (spr.Name == 'sAttackLeft' || spr.Name == 'sDamselAttackL' || spr.Name == 'sTunnelAttackL') {
2932 removeActivatedPlayerWeapon();
2933 } else if (spr.Name == 'sDuckToHangL' || spr.Name == 'sDamselDtHL' || spr.Name == 'sTunnelDtHL') {
2943 if (dir == Dir.Left) {
2945 obj = level.isAnyLadderAtPoint(x-8, y);
2948 obj = level.isAnyLadderAtPoint(x+8, y);
2953 } else if (dir == Dir.Left) {
2963 } else if (isExitingSprite()) {
2965 //!global.cleanSolids = true;
2970 void activatePlayerWeapon () {
2972 if (holdItem isa PlayerWeapon) {
2973 auto wep = holdItem;
2975 wep.instanceRemove();
2980 if (holdItem isa PlayerWeapon) {
2982 removeActivatedPlayerWeapon();
2989 auto spr = getSprite();
2990 if (spr.Name != 'sAttackLeft' && spr.Name != 'sDamselAttackL' && spr.Name != 'sTunnelAttackL') {
2991 writeln("PLR ATTACK DONE; holdItem=", (holdItem ? GetClassName(holdItem.Class) : '<none>'), "; frm=", imageFrame);
2993 writeln("PLR ATTACK; holdItem=", (holdItem ? GetClassName(holdItem.Class) : '<none>'), "; frm=", imageFrame);
2998 if (global.config.unarmed && !holdItem) return; // no whip when unarmed
3000 auto spr = getSprite();
3001 if (spr.Name != 'sAttackLeft' && spr.Name != 'sDamselAttackL' && spr.Name != 'sTunnelAttackL') {
3002 //writeln("PLR ATTACK DONE; holdItem=", (holdItem ? GetClassName(holdItem.Class) : '<none>'), "; frm=", imageFrame);
3005 //writeln("PLR ATTACK; holdItem=", (holdItem ? GetClassName(holdItem.Class) : '<none>'), "; frm=", imageFrame);
3007 if (imageFrame > 4) {
3008 //bool hitEnemy = (PlayerWeapon(holdItem) ? PlayerWeapon(holdItem).hitEnemy : false);
3009 if (global.isTunnelMan || pickedItem isa ItemWeaponMattock) {
3010 holdItem = level.MakeMapObject(ix+(dir == Dir.Left ? -16 : 16), iy, 'oMattockHit');
3011 if (imageFrame < 7) playSound('sndWhip');
3012 } else if (pickedItem isa ItemWeaponMachete) {
3013 holdItem = level.MakeMapObject(ix+(dir == Dir.Left ? -16 : 16), iy, 'oSlash');
3014 playSound('sndWhip');
3016 holdItem = level.MakeMapObject(ix+(dir == Dir.Left ? -16 : 16), iy, 'oWhip');
3017 playSound('sndWhip');
3019 } else if (imageFrame < 2) {
3020 if (global.isTunnelMan || pickedItem isa ItemWeaponMattock) {
3021 holdItem = level.MakeMapObject(ix+(dir == Dir.Left ? -16 : 16), iy, 'oMattockPre');
3022 } else if (pickedItem isa ItemWeaponMachete) {
3023 holdItem = level.MakeMapObject(ix+(dir == Dir.Left ? -16 : 16), iy, 'oMachetePre');
3025 holdItem = level.MakeMapObject(ix+(dir == Dir.Left ? 16 : -16), iy, 'oWhipPre');
3031 //bool webHit = false;
3033 bool doBreakWebsCB (MapObject o) {
3034 if (o isa ItemWeb) {
3037 if (fabs(xVel) > 1) {
3039 if (!o.dying) ItemWeb(o).life -= 5;
3043 if (fabs(yVel) > 1) {
3045 if (!o.dying) ItemWeb(o).life -= 5;
3055 void initiateExitSequence () {
3056 writeln("exit sequence initiated...");
3057 if (global.isDamsel) setSprite('sDamselExit');
3058 else if (global.isTunnelMan) setSprite('sTunnelExit');
3059 else setSprite('sPExit');
3066 /*k8: the following is done in `GameLevel`
3067 if (global.thiefLevel > 0) global.thiefLevel -= 1;
3068 //orig dbg:if (global.currLevel == 1) global.currLevel += firstLevelSkip; else global.currLevel += levelSkip;
3069 global.currLevel += 1;
3071 playSound('sndSteps');
3075 void processLevelExit () {
3076 if (dead || stunned || whipping || level.playerExited) return;
3077 if (!platformCharacterIs(ON_GROUND)) return;
3078 if (isExitingSprite()) return; // just in case
3080 auto hld = holdItem;
3081 if (hld isa PlayerWeapon) return; // oops
3083 //if (!kExitPressed && !hld) return false;
3085 auto door = level.checkTileAtPoint(ix, iy, &level.cbCollisionExitTile);
3086 if (!door || !door.visible) return; // note that `invisible` doors still works
3088 // sell idol, or free damsel
3089 if (hld isa ItemGoldIdol) {
3090 //!if (isRealLevel()) global.idolsConverted += 1;
3091 //not thisglobal.money += hld.value*(global.levelType+1);
3092 ItemGoldIdol(hld).registerConverted();
3093 addScore(hld.value*(global.levelType+1));
3094 //!if (hld.sprite_index == sCrystalSkull) global.skulls += 1; else global.idols += 1;
3095 playSound('sndCoin');
3096 level.MakeMapObject(ix, iy-8, 'oBigCollect');
3098 hld.instanceRemove();
3099 //!with (hld) instance_destroy();
3101 //!pickupItemType = "";
3102 } else if (hld isa MonsterDamsel) {
3104 MonsterDamsel(hld).exitAtDoor(door);
3107 if (!kExitPressed) {
3108 if (!door.invisible) {
3109 string msg = door.getExitMessage();
3110 if (msg.length == 0) {
3111 level.osdMessage(va("PRESS %s TO ENTER.", (global.config.useDoorWithButton ? "$PAY" : "$UP")), -666);
3112 } else if (msg[$-1] != '\n') {
3113 level.osdMessage(va("%s\nPRESS %s TO ENTER.", msg, (global.config.useDoorWithButton ? "$PAY" : "$UP")), -666);
3115 level.osdMessage(msg, -666);
3126 if (isHoldingArmedBomb()) scrUseThrowItem();
3128 if (isHoldingBombOrRope()) scrSwitchToPocketItem(forceIfEmpty:true);
3130 wasHoldingBall = false;
3133 if (hld isa ItemGoldIdol) {
3134 //!if (isRealLevel()) global.idolsConverted += 1;
3135 //not thisglobal.money += hld.value*(global.levelType+1);
3136 ItemGoldIdol(hld).registerConverted();
3137 addScore(hld.value*(global.levelType+1));
3138 //!if (hld.sprite_index == sCrystalSkull) global.skulls += 1; else global.idols += 1;
3139 playSound('sndCoin');
3140 level.MakeMapObject(ix, iy-8, 'oBigCollect');
3142 hld.instanceRemove();
3143 //!with (hld) instance_destroy();
3145 //!pickupItemType = "";
3146 } else if (hld isa MonsterDamsel) {
3148 MonsterDamsel(hld).exitAtDoor(door);
3149 } else if (hld.heavy || hld isa MapEnemy) {
3150 // drop heavy items, characters and enemies (but not ball)
3151 if (hld !isa ItemBall) scrUseThrowItem();
3152 } else if (hld isa ItemBall) {
3154 // other items are carried thru
3155 if (hld.cannotBeCarriedOnNextLevel) {
3157 holdItem = none; // just in case
3159 scrHideItemToPocket();
3162 global.pickupItem = hld.type;
3163 if (isAshShotgun(hld)) global.pickupItem = "Boomstick";
3165 breakPieces = false;
3169 //scrHideItemToPocket();
3175 //door = instance_place(x, y, oExit); // done above
3176 door.snapToExit(self);
3178 initiateExitSequence();
3180 level.playerExitDoor = door;
3184 override bool onFellInWater (MapTile water) {
3185 level.MakeMapObject(ix, iy-8, 'oSplash');
3187 playSound('sndSplash');
3188 myGrav = 0.2; //k8:???
3193 override bool onOutOfWater () {
3200 // ////////////////////////////////////////////////////////////////////////// //
3201 override void thinkFrame () {
3202 // remove whip, etc. when dead
3203 if (dead && holdItem isa PlayerWeapon) {
3204 removeActivatedPlayerWeapon();
3207 setPowerupState('Cape', global.hasCape);
3209 foreach (PlayerPowerup pp; powerups) if (pp.active) pp.onPreThink();
3213 if (redToggle) redColor -= 5;
3214 else if (redColor < 20) redColor += 5;
3215 else redToggle = true;
3220 if (dead) justdied = false;
3223 if (invincible > 0) --invincible;
3229 blinkHidden = !blinkHidden;
3232 blinkHidden = false;
3235 auto spr = getSprite();
3238 if (level.lg && level.isInShop(x/16, y/16)) {
3239 shopType = level.lg.roomShopType(x/16, y/16);
3244 cameraBlockX = max(0, cameraBlockX-1);
3245 cameraBlockY = max(0, cameraBlockY-1);
3248 if (spr.Name == 'sWhoaLeft' || spr.Name == 'sDamselWhoaL' || spr.Name == 'sTunnelWhoaL') {
3249 if (whoaTimer > 0) {
3251 } else if (holdItem && onLoosingHeldItem(LostCause.Whoa)) {
3254 if (!hi.onLostAsHeldItem(self, LostCause.Whoa)) {
3258 scrSwitchToPocketItem(forceIfEmpty:true);
3262 whoaTimer = whoaTimerMax;
3266 if (firing > 0) firing -= 1;
3269 auto wtile = level.isWaterAtPoint(x, y/*, oWaterSwim, -1, -1*/);
3272 if (onFellInWater(wtile) || !isInstanceAlive) return;
3276 if (onOutOfWater() || !isInstanceAlive) return;
3282 if (global.randOther(1, 5) == 1) level.MakeMapObject(x-8+global.randOther(4, 12), y-8+global.randOther(4, 12), 'oBurn');
3287 if (!dead && level.isLavaAtPoint(x, y+6/*, oLava, 0, 0*/)) {
3288 //!if (isRealLevel()) global.miscDeaths[11] += 1;
3289 level.addDeath('lava');
3290 playSound('sndFlame');
3304 if (global.hasJetpack && platformCharacterIs(ON_GROUND)) {
3308 // fall off bottom of screen
3309 if (!level.inWinCutscene && !level.inIntroCutscene) {
3310 if (!dead && y > level.tilesHeight*16+16) {
3311 //!if (isRealLevel()) global.miscDeaths[10] += 1;
3312 level.addDeath('void');
3313 global.plife = -90; // spill blood
3319 scrDropItem(LostCause.Falloff);
3320 playSound('sndThud'); //???
3321 playSound('sndDie'); //???
3324 if (dead && y > level.tilesHeight*16+16) {
3334 if (/*active*/true) {
3335 if (spr.Name == 'sStunL' || spr.Name == 'sDamselStunL' || spr.Name == 'sTunnelStunL') {
3336 if (stunTimer > 0) {
3340 if (stunTimer < 1) {
3342 canDropStuff = true;
3346 if (!level.inWinCutscene) {
3347 if (isParachuteActive() || isCapeActiveAndOpen()) fallTimer = 0;
3350 // changed to yVel > 1 from yVel > 0
3351 if (yVel > 1 && status != CLIMBING) {
3353 if (fallTimer > 16) wallHurt = 0; // no sense in them taking extra damage from being thrown here
3354 int paraOpenHeight = (global.config.scumSpringShoesReduceFallDamage && (global.hasSpringShoes || global.hasJordans) ? 22 : 14);
3355 //paraOpenHeight = 4;
3356 if (global.hasParachute && !stunned && fallTimer > paraOpenHeight) {
3357 //if (not collision_point(x, y+32, oSolid, 0, 0)) // was commented in the original code
3358 //!*if (not collision_line(x, y+16, x, y+32, oSolid, 0, 0))
3359 if (!level.checkTilesInRect(x, y+16, 1, 17, &level.cbCollisionAnySolid)) {
3361 //!instance_create(x-8, y-16, oParachute);
3363 global.hasParachute = false;
3364 activatePowerup('Parachute');
3365 //writeln("parachute state: ", isParachuteActive());
3368 } else if (fallTimer > 16 && platformCharacterIs(ON_GROUND) &&
3369 !level.checkTilesInRect(x-8, y-8, 17, 17, &level.cbCollisionSpringTrap) /* not onto springtrap */)
3371 // long drop -- player has just landed
3372 bool reducedDamage = (global.config.scumSpringShoesReduceFallDamage && (global.hasSpringShoes || global.hasJordans));
3373 if (reducedDamage && fallTimer <= 24) {
3374 // land without taking damage
3378 if (fallTimer > (reducedDamage ? 72 : 48)) global.plife -= 10*global.config.scumFallDamage;
3379 else if (fallTimer > (reducedDamage ? 48 : 32)) global.plife -= 2*global.config.scumFallDamage;
3380 else global.plife -= 1*global.config.scumFallDamage;
3381 if (global.plife < 1) {
3382 if (!dead) level.addDeath('fall');
3386 if (global.config.scumFallDamage > 0) stunTimer += 60;
3389 auto obj = level.MakeMapObject(x-4, y+6, 'oPoof');
3390 if (obj) obj.xVel = -0.4;
3391 obj = level.MakeMapObject(x+4, y+6, 'oPoof');
3392 if (obj) obj.xVel = 0.4;
3393 playSound('sndThud');
3395 } else if (yVel <= 0) {
3397 if (isParachuteActive()) {
3398 deactivatePowerup('Parachute');
3399 level.MakeMapObject(ix-8, iy-16-8, 'oParaUsed');
3403 // if (stunned) fallTimer = 0; // was commented in the original code
3405 if (swimming && !level.isLavaAtPoint(x, y/*, oLava, 0, 0*/)) {
3407 if (bubbleTimer > 0) {
3410 if (level.isWaterAtPoint(x, (y&~0x0f)-8)) level.MakeMapObject(x, y-4, 'oBubble');
3411 bubbleTimer = bubbleTimerMax;
3414 bubbleTimer = bubbleTimerMax;
3417 //TODO: k8: move spear checking to spear handler
3418 if (!isExitingSprite()) {
3419 auto spear = MapObjectSpearsBase(level.isObjectInRect(ix-6, iy-6, 13, 14, delegate bool (MapObject o) {
3420 auto tt = MapObjectSpearsBase(o);
3421 if (!tt) return false;
3422 return tt.isHitFrame;
3427 global.plife -= global.config.spearDmg; // 4
3428 if (!dead && global.plife <= 0 /*and isRealLevel()*/) level.addDeath('spear');
3429 xVel = global.randOther(4, 6)*(spear.isLeft ? -1 : 1);
3438 if (status != DUCKTOHANG && !stunned && !dead && !isExitingSprite()) {
3440 characterStepEvent();
3442 if (status != DUCKING && status != DUCKTOHANG) status = STANDING;
3443 checkControlKeys(getSprite());
3447 // if (dead or stunned)
3448 if (dead || stunned) {
3450 if (holdItem isa ItemWeaponBow && bowArmed) scrFireBow();
3451 scrDropItem(dead ? LostCause.Dead : LostCause.Stunned, xVel, -3);
3454 yVel += (bounced ? 1.0 : 0.6);
3456 if (isCollisionTop(1) && yVel < 0) yVel = -yVel*0.8;
3457 if (isCollisionLeft(1) || isCollisionRight(1)) xVel = -xVel*0.5;
3459 bool collisionbottomcheck = !!isCollisionBottom(1);
3460 if (collisionbottomcheck || isCollisionBottom(1, &level.cbCollisionPlatform)) {
3462 if (collisionbottomcheck) {
3463 if (yVel > 2.5) yVel = -yVel*0.5; else yVel = 0;
3465 // after falling onto a platform don't take extra damage after recovering from stunning
3468 /* was commented in the original code
3469 if (isCollisionBottom(1)) {
3470 if (yVel > 2.5) yVel = -yVel*0.5; else yVel = 0;
3477 if (fabs(xVel) < 0.1) xVel = 0;
3478 else if (fabs(xVel) != 0 && level.isIceAtPoint(x, y+16)) xVel *= 0.8;
3479 else if (fabs(xVel) != 0) xVel *= 0.3;
3485 //level.forEachObjectInRect(ix, iy, width, height, &doBreakWebsCB);
3487 // apply the limits since the velocity may be too extreme
3489 xVel = fclamp(xVel, -xVelLimit, xVelLimit);
3490 yVel = fclamp(yVel, -yVelLimit, yVelLimit);
3492 moveRel(xVel, yVel);
3496 // fix sprites, spawn blood from spikes
3497 if (isParachuteActive()) {
3498 deactivatePowerup('Parachute');
3499 level.MakeMapObject(ix-8, iy-16-8, 'oParaUsed');
3503 removeActivatedPlayerWeapon();
3505 //!with (oWhip) instance_destroy();
3508 if (global.isDamsel) {
3510 if (dead) setSprite('sDamselDieL');
3511 else if (stunned) setSprite('sDamselStunL');
3512 } else if (bounced) {
3513 if (yVel < 0) setSprite('sDamselBounceL'); else setSprite('sDamselFallL');
3515 if (xVel < 0) setSprite('sDamselDieLL'); else setSprite('sDamselDieLR');
3517 } else if (global.isTunnelMan) {
3519 if (dead) setSprite('sTunnelDieL');
3520 else if (stunned) setSprite('sTunnelStunL');
3521 } else if (bounced) {
3522 if (yVel < 0) setSprite('sTunnelLBounce'); else setSprite('sTunnelFallL');
3524 if (xVel < 0) setSprite('sTunnelDieLL'); else setSprite('sTunnelDieLR');
3528 if (dead) setSprite('sDieL');
3529 else if (stunned) setSprite('sStunL');
3530 } else if (bounced) {
3531 if (yVel < 0) setSprite('sDieLBounce'); else setSprite('sDieLFall');
3533 if (xVel < 0) setSprite('sDieLL'); else setSprite('sDieLR');
3540 auto colobj = isCollisionRight(1);
3541 if (!colobj) colobj = isCollisionLeft(1);
3542 if (!colobj) colobj = isCollisionBottom(1);
3545 scrCreateBlood(colobj.x0, colobj.y0, 3);
3547 if (!dead && global.plife <= 0 /*&& isRealLevel()*/) {
3549 writeln("thrown to death by '", thrownBy, "'");
3550 level.addDeath(thrownBy);
3554 if (wallHurt <= 0) thrownBy = '';
3555 playSound('sndHurt'); //???
3559 colobj = isCollisionBottom(1);
3560 if (colobj && !bounced) {
3562 scrCreateBlood(colobj.x0, colobj.y0, 2);
3565 if (!dead && global.plife <= 0 /*and isRealLevel()*/) {
3567 writeln("thrown to death by '", thrownBy, "'");
3568 level.addDeath(thrownBy);
3572 if (wallHurt <= 0) thrownBy = '';
3577 bool kPay = level.isKeyDown(GameConfig::Key.Pay);
3579 // gnounc's quick look
3580 if (!kRight && !kLeft && (platformCharacterIs(ON_GROUND) || status == HANGING)) {
3581 if (kDown) { if (viewCount <= 6) viewCount += 3; else viewOffset += 6; }
3582 else if (kUp) { if (viewCount <= 6) viewCount += 3; else viewOffset -= 6; }
3588 // default look up/down with delay if pay button not held
3589 if (!kRight && !kLeft && (platformCharacterIs(ON_GROUND) || status == HANGING)) {
3590 if (kDown) { if (viewCount <= 30) viewCount += 1; else viewOffset += 4; }
3591 else if (kUp) { if (viewCount <= 30) viewCount += 1; else viewOffset -= 4; }
3598 if (viewCount == 0 && viewOffset) viewOffset = (viewOffset < 0 ? min(0, viewOffset+8) : max(0, viewOffset-8));
3599 viewOffset = clamp(viewOffset, -16*6, 16*6);
3601 if (!dead) activatePlayerWeapon();
3603 if (!dead) processLevelExit();
3606 if (global.plife < -99 && visible && justdied) spillBlood();
3608 if (global.plife < 1) {
3612 // spikes, and other shit
3613 if (global.plife >= -99 && visible && !isExitingSprite()) {
3614 auto colSpikes = level.checkTilesInRect(x-4, y-4, 9, 13, &level.cbCollisionSpikes);
3616 if (colSpikes && dead) {
3618 if (!level.isSolidAtPoint(x, y+9)) { shiftY(0.02); y = iy; } //0.05;
3619 //else myGrav = 0.6;
3624 if (colSpikes && yVel > 0 && (fallTimer > 3 || stunned)) { // originally fallTimer > 4
3626 // spikes will always instant-kill in Moon room
3627 if (level.levelKind == GameLevel::LevelKind.Moon) global.plife -= 99; else global.plife -= global.config.scumSpikeDamage;
3628 if (/*isRealLevel() &&*/ global.plife <= 0) level.addDeath('spike');
3629 if (global.plife > 0) playSound('sndHurt');
3635 colSpikes.makeBloody();
3637 //else if (not dead) myGrav = 0.6;
3642 if (visible && (status >= STUNNED || stunned || dead || status == DUCKING)) {
3644 checkAndPerformSacrifice(out onAltar);
3645 // block looking down if we're trying to sacrifire ourselves
3646 if (onAltar) viewCount = max(0, viewCount-1);
3648 sacCount = default.sacCount;
3652 if (dead && global.hasAnkh) {
3653 writeln("*** ACTIVATED ANKH");
3654 global.hasAnkh = false;
3656 int newLife = (global.isTunnelMan ? global.config.scumTMLife : global.config.scumStartLife);
3657 global.plife = max(global.plife, newLife);
3658 level.osdMessage("THE ANKH SHATTERS!\nYOU HAVE BEEN REVIVED!", 4);
3660 auto moai = level.forEachTile(delegate bool (MapTile t) { return (t.objType == 'oMoai'); });
3662 level.forEachTile(delegate bool (MapTile t) {
3663 if (t.objType == 'oMoaiInside') {
3664 teleportTo(t.ix+8, t.iy+8);
3669 //teleportTo(moai.ix+16+8, moai.iy+16+8);
3671 if (level.allEnters.length) {
3672 teleportTo(level.allEnters[0].ix+8, level.allEnters[0].iy-8);
3675 level.centerViewAtPlayer();
3676 auto ball = getMyBall();
3677 if (ball) ball.teleportToPrisoner();
3692 //alarm[8] = 60; // this starts music; but we don't need it, 'cause we won't stop the music on player death
3693 playSound('sndTeleport');
3697 if (dead) level.stats.gameOver();
3700 if (status == DUCKTOHANG) {
3702 if (spr.Name != 'sDuckToHangL' && spr.Name != 'sDamselDtHL' && spr.Name != 'sTunnelDtHL') status = STANDING;
3705 foreach (PlayerPowerup pp; powerups) if (pp.active) pp.onPostThink();
3707 if (jetpackFlaresTime > 0) {
3708 if (--jetpackFlaresTime == 0) {
3709 auto obj = level.MakeMapObject(ix+global.randOther(0, 3)-global.randOther(0, 3), iy+global.randOther(0, 3)-global.randOther(0, 3), 'oFlareSpark');
3711 obj.yVel = global.randOther(1, 3);
3712 obj.xVel = global.randOther(0, 3)-global.randOther(0, 3);
3714 playSound('sndJetpack');
3718 // this prevents stucking in a wall when thrown
3720 if (yVel < 0 && isCollisionTop(1)) writeln(":::TOPCOLL! yVel=", yVel, "; yAcc=", yAcc);
3721 else if (isCollisionTop(0)) writeln("+++TOPCOLL! yVel=", yVel, "; yAcc=", yAcc);
3724 if (yVel < 0 && isCollisionTop(1)) { writeln(":::TOPCOLL! yVel=", yVel, "; yAcc=", yAcc); /*yVel = 0; yAcc = 0;*/ }
3725 if (xVel < 0 && isCollisionLeft(1)) { xVel = 0; xAcc = 0; }
3726 else if (xVel > 0 && isCollisionRight(1)) { xVel = 0; xAcc = 0; }
3731 // ////////////////////////////////////////////////////////////////////////// //
3732 void drawPrePrePowerupWithOfs (int xpos, int ypos, int scale, float currFrameDelta) {
3733 // so ducking player will have it's cape correctly rendered
3734 foreach (PlayerPowerup pp; powerups) {
3735 if (pp.active) pp.prePreDrawWithOfs(xpos, ypos, scale, currFrameDelta);
3740 override void drawWithOfs (int xpos, int ypos, int scale, float currFrameDelta) {
3741 //if (heldBy) return; // owner will take care of this
3742 if (blinkHidden) return;
3744 bool renderJetpackBack = false;
3745 if (global.hasJetpack) {
3747 if ((status == CLIMBING || isExitingSprite()) && !whipping) {
3749 renderJetpackBack = true;
3752 getInterpCoords(currFrameDelta, scale, out xi, out yi);
3755 if (dir == Dir.Right) {
3756 spr = level.sprStore['sJetpackRight'];
3759 spr = level.sprStore['sJetpackLeft'];
3763 auto spf = spr.frames[0];
3764 if (spf && spf.width > 0 && spf.height > 0) spf.tex.blitAt(xi-xpos-spf.xofs*scale, yi-ypos-spf.yofs*scale, scale);
3769 bool ducking = (status == DUCKING);
3770 foreach (PlayerPowerup pp; powerups) {
3771 if (pp.active) pp.preDrawWithOfs(xpos, ypos, scale, currFrameDelta);
3774 auto oldColor = Video.color;
3775 if (redColor > 0) Video.color = clamp(200+redColor, 0, 255)<<16;
3776 ::drawWithOfs(xpos, ypos, scale, currFrameDelta);
3777 Video.color = oldColor;
3779 if (renderJetpackBack) {
3781 getInterpCoords(currFrameDelta, scale, out xi, out yi);
3782 SpriteImage spr = level.sprStore['sJetpackBack'];
3784 auto spf = spr.frames[0];
3785 if (spf && spf.width > 0 && spf.height > 0) spf.tex.blitAt(xi-xpos-spf.xofs*scale, yi-ypos-spf.yofs*scale, scale);
3789 foreach (PlayerPowerup pp; powerups) if (pp.active) pp.postDrawWithOfs(xpos, ypos, scale, currFrameDelta);
3793 void lastDrawWithOfs (int xpos, int ypos, int scale, float currFrameDelta) {
3794 foreach (PlayerPowerup pp; powerups) {
3795 if (pp.active) pp.lastDrawWithOfs(xpos, ypos, scale, currFrameDelta);
3802 objType = 'oPlayer';
3805 desc2 = "A strange little man who spends his time exploring caverns. He wants to be just like Indiana Jones when he grows up.";
3807 negateMirrorXOfs = true;
3809 status = FALLING; // the character state, must be one of the following: STANDING, RUNNING, DUCKING, LOOKING_UP, CLIMBING, JUMPING, or FALLING
3819 //thrownBy = ""; // "Yeti", "Hawkman", or "Shopkeeper" for stat tracking deaths by being thrown
3822 //whoaTimerMax = 30;
3823 distToNearestLightSource = 999;
3833 frictionFactor = 0.3;
3835 xVelLimit = 16; // limits the xVel: default 15
3836 yVelLimit = 10; // limits the yVel
3837 xAccLimit = 9; // limits the xAcc
3838 yAccLimit = 6; // limits the yAcc
3839 runAcc = 3; // the running acceleration
3846 //lightRadius = 96; //???