1 /**********************************************************************************
2 * Copyright (c) 2008, 2009 Derek Yu and Mossmouth, LLC
3 * Copyright (c) 2018, Ketmar Dark
5 * This file is part of Spelunky.
7 * You can redistribute and/or modify Spelunky, including its source code, under
8 * the terms of the Spelunky User License.
10 * Spelunky is distributed in the hope that it will be entertaining and useful,
11 * but WITHOUT WARRANTY. Please see the Spelunky User License for more details.
13 * The Spelunky User License should be available in "Game Information", which
14 * can be found in the Resource Explorer, or as an external file called COPYING.
15 * If not, please obtain a new copy of Spelunky from <http://spelunkyworld.com/>
17 **********************************************************************************/
18 // recoded by Ketmar // Invisible Vector
19 class PlayerPawn : MapEnemy;
24 const int hangCountMax = 3;
26 array!PlayerPowerup powerups; // created in initializer
28 int cameraBlockX; // >0: don't center camera
29 int cameraBlockY; // >0: don't center camera
35 const int bubbleTimerMax = 20;
36 int jetpackFlaresTime;
47 bool justdied = true; // so dead body won't spill blood endlessly
55 const int firingMax = 20;
56 const int firingPistolMax = 20;
57 const int firingShotgunMax = 40;
61 int hotkeyPressed = -1;
73 //!!!global.poisonStrength = max(global.poisonStrength-0.5, 1);
75 //string holdItemType = "";
76 //string pickupItemType = "";
78 // this is what we had picked up
79 // picked item will be stored here by bomb/rope/item switcher
82 bool canDropStuff = true;
94 int bombArrowCounter = 80;
103 // the keys that the platform character will use (don't edit)
115 bool jumpButtonReleased; // whether the jump button was released. (Stops the user from pressing the jump button many times to get extra jumps)
118 bool kAttackReleased;
120 const float gravNorm = 1;
121 //float grav = 1; // the gravity
123 const float initialJumpAcc = -2; // relates to how high the character will jump
124 const int jumpTimeTotal = 10; // how long the user must hold the jump button to get the maximum jump height
126 float climbAcc = 0.6; // how fast the character will climb
127 float climbAnimSpeed = 0.4; // relates to how fast the climbing animation should go
128 int climbSndSpeed = 8;
132 // these flags are used to recreate ball and chain when player is moved at a new level
136 const float departLadderXVel = 4; // how fast character should be moving horizontally when he leaves the ladder
137 const float departLadderYVel = -4; // how fast the character should be moving vertically when he leaves the ladder
139 const float frictionRunningX = 0.6; // friction obtained while running
140 const float frictionRunningFastX = 0.98; // friction obtained while holding the shift button for some time while running
141 const float frictionClimbingX = 0.6; // friction obtained while climbing
142 const float frictionClimbingY = 0.6; // friction obtained while climbing
143 const float frictionDuckingX = 0.8; // friction obtained while ducking
144 const float frictionFlyingX = 0.99; // friction obtained while "flying"
146 const float runAnimSpeed = 0.1; // relates to the how fast the running animation should go
148 // hidden variables (don't edit)
149 protected int statePrev;
150 protected int statePrevPrev;
151 protected float gravityIntensity = grav; // this variable describes the current force due to gravity (this variable is altered for variable jumping)
152 protected float jumpTime = jumpTimeTotal; // current time of the jump (0=start of jump, jumpTimeTotal=end of jump)
153 protected int ladderTimer; // relates to whether the character can climb a ladder
154 protected int kLeftPushedSteps;
155 protected int kRightPushedSteps;
157 transient protected bool skipCutscenePressed;
162 //PlayerWeapon actWeapon; // active weapon object
175 // ////////////////////////////////////////////////////////////////////////// //
181 final ItemBall getMyBall () {
182 ItemBall res = myBall;
183 if (res && !res.isInstanceAlive) { res = none; myBall = none; }
188 void spawnBallAndChain (optional bool levelStart) {
190 auto owh = wasHoldingBall;
191 removeBallAndChain();
192 wasHoldingBall = owh;
194 mustBeChained = true;
195 auto ball = getMyBall();
197 if (levelStart) writeln("::: respawning ball; old ball is missing (it is ok)");
198 writeln("creating new ball");
199 ball = ItemBall(level.MakeMapObject(ix, iy, 'oBall'));
201 ball.attachTo(self, levelStart);
202 writeln("ball created");
206 if (levelStart) writeln("::: attaching ball to player");
207 ball.attachTo(self, levelStart);
208 if (wasHoldingBall) {
209 if (levelStart) writeln("::: picking ball");
211 pickedItem.instanceRemove();
214 if (holdItem && holdItem != ball) {
215 holdItem.instanceRemove();
221 if (myBall != ball) FatalError("error in ball management");
222 if (levelStart) writeln("ballpos=(", ball.ix, ",", ball.iy, "); plrpos=(", ix, ",", iy, "); ballalive=", ball.isInstanceAlive);
224 writeln("failed to create a new ball");
225 mustBeChained = false;
227 wasHoldingBall = false;
231 void removeBallAndChain (optional bool temp) {
232 auto ball = getMyBall();
234 writeln("removing ball and chain...", (temp ? " (temporarily)" : ""));
235 wasHoldingBall = (holdItem == ball);
236 writeln(" has ball, holding=", wasHoldingBall);
237 mustBeChained = true;
239 ball.instanceRemove();
243 wasHoldingBall = false;
244 mustBeChained = false;
248 // ////////////////////////////////////////////////////////////////////////// //
249 final PlayerPowerup findPowerup (name id) {
250 foreach (PlayerPowerup pp; powerups) if (pp.id == id) return pp;
255 final bool setPowerupState (name id, bool active) {
256 auto pp = findPowerup(id);
257 if (!pp) return false;
258 return (active ? pp.onActivate() : pp.onDeactivate());
262 final bool togglePowerupState (name id) {
263 auto pp = findPowerup(id);
264 if (!pp) return false;
265 return (pp.active ? pp.onDeactivate() : pp.onActivate());
269 final bool activatePowerup (name id) { return setPowerupState(id, true); }
270 final bool deactivatePowerup (name id) { return setPowerupState(id, false); }
273 final bool isActivePowerup (name id) {
274 auto pp = findPowerup(id);
275 return (pp && pp.active);
279 // ////////////////////////////////////////////////////////////////////////// //
280 override void Destroy () {
281 foreach (PlayerPowerup pp; powerups) delete pp;
286 void unpressAllKeys () {
288 kLeftPressed = false;
289 kLeftReleased = false;
291 kRightPressed = false;
292 kRightReleased = false;
296 kJumpPressed = false;
297 kJumpReleased = false;
299 kAttackPressed = false;
300 kAttackReleased = false;
301 kItemPressed = false;
302 kRopePressed = false;
303 kBombPressed = false;
305 kExitPressed = false;
309 // ////////////////////////////////////////////////////////////////////////// //
310 // called on level start too
316 skipCutscenePressed = false;
317 movementBlocked = false;
318 if (global.plife < 1) global.plife = max(1, global.config.scumStartLife);
323 myGrav = default.myGrav;
327 depth = default.depth;
328 status = default.status;
334 distToNearestLightSource = 999;
336 justdied = default.justdied;
338 if (holdItem isa PlayerWeapon) {
344 blink = default.blink;
345 blinkHidden = default.blinkHidden;
351 level.clearKeysPressRelease();
354 //scrSwitchToPocketItem(forceIfEmpty:false);
358 // ////////////////////////////////////////////////////////////////////////// //
359 bool isExitingSprite () {
360 auto spr = getSprite();
361 return (spr.Name == 'sPExit' || spr.Name == 'sDamselExit' || spr.Name == 'sTunnelExit');
365 // ////////////////////////////////////////////////////////////////////////// //
366 override void playSound (name aname, optional bool unique) {
367 if (unique && global.sndIsPlaying(0, aname)) return;
368 global.playSound(0, 0, 0, aname); // it is local
372 override bool sndIsPlaying (name aname) {
373 return global.sndIsPlaying(0, aname);
377 override void sndStopSound (name aname) {
378 global.sndStopSound(0, aname);
382 // ////////////////////////////////////////////////////////////////////////// //
383 transient ItemDice currDie;
385 void onDieRolled (ItemDice die) {
386 if (!die.forSale) return;
387 // only law-abiding players can play
388 if (global.thiefLevel > 0 || global.murderer) return;
389 if (bet == 0) return;
392 level.forEachObject(delegate bool (MapObject o) {
393 MonsterShopkeeper sc = MonsterShopkeeper(o);
394 if (sc && !sc.dead && !sc.angered) return sc.onDiePlayed(self, currDie);
401 // ////////////////////////////////////////////////////////////////////////// //
402 override bool onExplosionTouch (MapObject xplo) {
403 //writeln("PlayerPawn: on explo touch! ", invincible);
404 if (invincible) return false;
405 if (global.config.scumExplosionHurt) {
406 global.plife -= global.config.explosionDmg;
407 if (!dead && global.plife <= 0 /*&& isRealLevel()*/) {
408 auto xp = MapObjExplosion(xplo);
409 if (xp && xp.suicide) level.addDeath('suicide'); else level.addDeath('explosion');
412 if (global.config.scumExplosionStun) {
418 if (xplo.ix < ix) xVel = global.randOther(4, 6); else xVel = -global.randOther(4, 6);
424 // ////////////////////////////////////////////////////////////////////////// //
425 // start new game when exiting from title, and process other custom exits
426 void scrPlayerExit () {
427 level.playerExited = true;
433 // ////////////////////////////////////////////////////////////////////////// //
434 bool scrHideItemToPocket (optional bool forBombOrRope) {
435 if (!holdItem) return true;
436 if (holdItem isa PlayerWeapon) return false;
437 if (holdItem.forSale) return false;
438 if (!forBombOrRope) {
439 if (holdItem isa ItemBall) return false;
442 // cannot hide armed bomb
443 ItemBomb bomb = ItemBomb(holdItem);
444 if (bomb && bomb.armed) return false;
445 if (bomb || holdItem isa ItemRopeThrow) {
446 holdItem.instanceRemove();
452 if (holdItem isa MapEnemy) return false;
453 //writeln("hiding: '", GetClassName(holdItem.Class), "'");
455 if (pickedItem) FatalError("we are already holding '%n'", GetClassName(pickedItem.Class));
456 pickedItem = holdItem;
458 pickedItem.active = false;
459 pickedItem.visible = false;
460 if (pickedItem.heldBy) FatalError("oooops (scrHideItemToPocket)");
465 bool scrSwitchToBombs () {
466 if (holdItem isa PlayerWeapon) return false;
468 if (global.bombs < 1) return false;
469 if (ItemBomb(holdItem)) return true;
470 if (!scrHideItemToPocket(forBombOrRope:true)) return false;
472 ItemBomb bomb = ItemBomb(level.MakeMapObject(ix, iy, 'oBomb'));
473 if (!bomb) return false;
474 bomb.setSticky(global.stickyBombsActive);
476 whoaTimer = whoaTimerMax;
481 bool scrSwitchToStickyBombs () {
482 if (holdItem isa PlayerWeapon) return false;
483 if (!global.hasStickyBombs) {
484 global.stickyBombsActive = false;
488 global.stickyBombsActive = !global.stickyBombsActive;
493 bool scrSwitchToRopes () {
494 if (holdItem isa PlayerWeapon) return false;
496 if (global.rope < 1) return false;
497 if (ItemRopeThrow(holdItem)) return true;
498 if (!scrHideItemToPocket(forBombOrRope:true)) return false;
500 ItemRopeThrow rope = ItemRopeThrow(level.MakeMapObject(ix, iy, 'oRopeThrow'));
501 if (!rope) return false;
503 whoaTimer = whoaTimerMax;
508 bool isHoldingBombOrRope () {
510 if (!hit) return false;
511 return (hit isa ItemBomb || hit isa ItemRopeThrow);
515 bool isHoldingBomb () {
517 if (!hit) return false;
518 return (hit isa ItemBomb);
522 bool isHoldingArmedBomb () {
523 auto hit = ItemBomb(holdItem);
524 if (!hit) return false;
529 bool isHoldingRope () {
531 if (!hit) return false;
532 return (hit isa ItemRopeThrow);
536 bool scrSwitchToPocketItem (bool forceIfEmpty) {
537 if (holdItem isa PlayerWeapon) return false;
538 if (holdItem && holdItem.forSale) return false;
540 if (holdItem == pickedItem) { pickedItem = none; whoaTimer = whoaTimerMax; return true; }
542 if (!forceIfEmpty && !pickedItem) return false;
544 // destroy currently holded item if it is a bomb or a rope
546 // you cannot do it with an armed bomb
547 if (holdItem isa MapEnemy) return false; // cannot hide an enemy
548 ItemBomb bomb = ItemBomb(holdItem);
549 if (bomb && bomb.armed) return false;
550 if (bomb || holdItem isa ItemRopeThrow) {
552 holdItem.instanceRemove();
556 writeln(va("cannot switch to pocket item while carrying '%n' ('%n' is in pocket, why?)", GetClassName(holdItem.Class), GetClassName(pickedItem.Class)));
562 auto oldHold = holdItem;
563 holdItem = pickedItem;
564 pickedItem = oldHold;
565 // all flag management is done in property handler
567 oldHold.active = false;
568 oldHold.visible = false;
570 whoaTimer = whoaTimerMax;
575 bool scrSwitchToNextItem () {
576 if (holdItem isa PlayerWeapon) return false;
577 if (holdItem && holdItem.forSale) return false;
580 if (ItemBomb(holdItem)) {
581 if (ItemBomb(holdItem).armed) return false; // cannot switch out of armed bomb
582 if (scrSwitchToRopes()) return true;
583 return scrSwitchToPocketItem(forceIfEmpty:true);
587 if (ItemRopeThrow(holdItem)) {
588 if (scrSwitchToPocketItem(forceIfEmpty:true)) return true;
589 if (scrSwitchToBombs()) return true;
590 return scrHideItemToPocket();
593 // either nothing, or normal item
594 bool tryPocket = !!holdItem;
595 if (scrSwitchToBombs()) return true;
596 if (scrSwitchToRopes()) return true;
597 if (holdItem isa ItemBall) return false;
598 if (tryPocket) return scrSwitchToPocketItem(forceIfEmpty:true);
603 // ////////////////////////////////////////////////////////////////////////// //
604 bool scrPickupItem (MapObject obj) {
605 if (holdItem isa PlayerWeapon) return false;
607 if (!obj) return false;
610 if (pickedItem) return false;
611 if (isHoldingArmedBomb()) return false;
612 if (isHoldingBombOrRope()) {
613 if (!scrSwitchToPocketItem(forceIfEmpty:true)) return false;
615 if (holdItem) return false;
618 if (pickedItem) return false;
621 if (obj isa ItemBomb && !ItemBomb(obj).armed) ++global.bombs;
622 else if (obj isa ItemRopeThrow) ++global.rope;
624 whoaTimer = whoaTimerMax;
625 obj.onPickedUp(self);
630 // drop currently held item
631 bool scrDropItem (LostCause cause, optional float xVel, optional float yVel) {
632 if (holdItem isa PlayerWeapon) return false;
634 if (!holdItem) return false;
636 if (!onLoosingHeldItem(cause)) return false;
641 if (!hi.onLostAsHeldItem(self, cause, xVel!optional, yVel!optional)) {
647 if (hi isa ItemRopeThrow) global.rope = max(0, global.rope-1);
648 else if (hi isa ItemBomb && !ItemBomb(hi).armed) global.bombs = max(0, global.bombs-1);
652 scrSwitchToPocketItem(forceIfEmpty:true);
657 // ////////////////////////////////////////////////////////////////////////// //
658 void scrUseThrowIt (MapObject it) {
661 it.onBeforeThrowBy(self);
666 if (dir == Dir.Left) {
667 it.xVel = (it.heavy ? -4+xVel : -8+xVel);
668 //foreach (; 0..8) if (level.isSolidAtPoint(ix-8, iy)) it.shiftX(1);
669 //while (!level.isSolidAtPoint(ix-8, iy)) it.shiftX(1); // prevent getting stuck in wall
670 } else if (dir == Dir.Right) {
671 it.xVel = (it.heavy ? 4+xVel : 8+xVel);
672 //foreach (; 0..8) if (level.isSolidAtPoint(ix+8, iy)) it.shiftX(-1);
673 //while (!level.isSolidAtPoint(ix+8, iy)) it.shiftX(-1); // prevent getting stuck in wall
675 it.yVel = (it.heavy ? (kUp ? -4 : -2) : (kUp ? -9 : -3));
676 if (kDown || scrPlayerIsDucking()) {
677 if (platformCharacterIs(ON_GROUND)) {
684 } else if (!global.hasMitt) {
685 if (dir == Dir.Left) {
686 if (level.isSolidAtPoint(ix-8, iy-10)) {
690 } else if (dir == Dir.Right) {
691 if (level.isSolidAtPoint(ix+8, iy-10)) {
698 if (global.hasMitt && !scrPlayerIsDucking()) {
699 it.xVel += (it.xVel < 0 ? -6 : 6);
700 if (!kUp && !kDown) it.yVel = -0.4;
701 else if (kDown) it.yVel = 6;
705 // prevent getting stuck in a wall
706 if (it.isCollision()) {
707 //foreach (; 0..8) if (level.isSolidAtPoint(ix-8, iy)) it.shiftX(1);
709 if (level.isSolidAtPoint(it.ix-8, it.iy)) it.shiftX(8);
710 } else if (it.xVel > 0) {
711 if (level.isSolidAtPoint(it.ix+8, it.iy)) it.shiftX(-8);
712 } else if (it.isCollision()) {
713 int dx = (it.isCollisionLeft(0) ? 1 : it.isCollisionRight(0) ? -1 : 0);
717 if (!it.isCollision()) break;
723 while (dx > 8 && it.isCollisionLeft(dx)) ++dx;
724 if (dx < 8) it.shiftX(8);
727 while (dx > 8 && it.isCollisionRight(dx)) ++dx;
728 if (dx < 8) it.shiftX(-8);
734 if (it.sprite_index == sBombBag ||
735 it.sprite_index == sBombBox ||
736 it.sprite_index == sRopePile)
740 playSound('sndThrow');
743 auto proj = ItemProjectile(it);
744 if (proj) proj.launchedByPlayer = true;
748 bool scrUseThrowItem () {
749 if (holdItem isa PlayerWeapon) return false;
751 auto hitem = holdItem;
753 if (!hitem) return false;
754 if (!onLoosingHeldItem(LostCause.Unknown)) return false;
759 scrUseThrowIt(hitem);
761 // if we throwing away armed bomb, get previous item back into our hands
763 if (/*ItemBomb(hitem)*/isHoldingBombOrRope()) scrSwitchToPocketItem(forceIfEmpty:false);
769 // ////////////////////////////////////////////////////////////////////////// //
770 bool scrPlayerIsDucking () {
771 if (dead) return false;
772 auto spr = getSprite();
773 //if (!spr) return false;
775 spr.Name == 'sDuckLeft' ||
776 spr.Name == 'sCrawlLeft' ||
777 spr.Name == 'sDamselDuckL' ||
778 spr.Name == 'sDamselCrawlL' ||
779 spr.Name == 'sTunnelDuckL' ||
780 spr.Name == 'sTunnelCrawlL';
785 if (holdItem !isa ItemWeaponBow) return false;
786 sndStopSound('sndBowPull');
787 if (!bowArmed) return false;
788 if (!holdItem.onTryUseItem(self)) return false;
793 void scrUsePutItOnGroundHelper (MapObject it, optional float xVelMult, optional float yVelNew) {
796 if (!specified_xVelMult) xVelMult = 0.4;
797 if (!specified_yVelNew) yVelNew = 0.5;
799 //writeln("putting '", GetClassName(hi.Class), "'");
801 if (dir == Dir.Left) {
802 it.xVel = (it.heavy ? -4 : -8);
803 } else if (dir == Dir.Right) {
804 it.xVel = (it.heavy ? 4 : 8);
812 if (ItemGoldIdol(it)) it.flty = iy;
815 if (it.isCollisionBottom(0) && !it.isCollisionTop(1)) {
823 if (it.isCollisionLeft(0)) {
824 if (it.isCollisionRight(1)) break;
826 } else if (it.isCollisionRight(0)) {
827 if (it.isCollisionLeft(1)) break;
836 // put item which player holds in his hands on the ground if player is ducking
837 // return `true` if item was put
838 bool scrUsePutItemOnGround (optional float xVelMult, optional float yVelNew) {
839 if (holdItem isa PlayerWeapon) return false;
842 if (!hi || !scrPlayerIsDucking()) return false;
844 if (!onLoosingHeldItem(LostCause.Unknown)) return false;
846 //writeln("putting '", GetClassName(hi.Class), "'");
848 if (global.bombs > 0) {
849 auto bomb = ItemBomb(hi);
850 if (bomb && !bomb.armed) global.bombs -= 1;
853 if (global.rope > 0) {
854 auto rope = ItemRopeThrow(hi);
857 rope.falling = false;
867 scrUsePutItOnGroundHelper(hi, xVelMult!optional, yVelNew!optional);
873 bool launchRope (bool goDown, bool doDrop) {
874 if (global.rope < 1) {
876 if (ItemRopeThrow(holdItem)) scrSwitchToPocketItem(forceIfEmpty:false);
882 bool wasHeld = false;
883 ItemRopeThrow rp = ItemRopeThrow(holdItem);
884 int xdelta = (doDrop ? 12 : 16)*(dir == Dir.Left ? -1 : 1);
886 //FIXME: call handler
889 rp.setXY(ix+xdelta, iy);
891 rp = ItemRopeThrow(level.MakeMapObject(ix+xdelta, iy, 'oRopeThrow'));
893 if (rp.heldBy) FatalError("PlayerPawn::launchRope: hold management fucked");
896 //rp.resaleValue = 0;
900 if (platformCharacterIs(ON_GROUND)) rp.startY = iy; // YASM 1.7
912 if (!level.isSolidAtPoint(ix+(doDrop ? 2 : 8), iy)) { //2
913 if (!level.checkTilesInRect(rp.ix-8, rp.iy, 2, 17)) rp.shiftX(-8);
914 else if (!level.checkTilesInRect(rp.ix+7, rp.iy, 2, 17)) rp.shiftX(8);
919 } else if (!level.isSolidAtPoint(ix-(doDrop ? 2 : 8), iy)) { //2
920 if (!level.checkTilesInRect(rp.ix+7, rp.iy, 2, 17)) rp.shiftX(8);
921 else if (!level.checkTilesInRect(rp.ix-8, rp.iy, 2, 17)) rp.shiftX(-8);
928 // cannot launch rope
929 /* was commented in the original
930 if (oPlayer1.facing == 18) {
931 obj = instance_create(oPlayer1.x-4, oPlayer1.y+2, oRopeThrow);
934 obj = instance_create(oPlayer1.x+4, oPlayer1.y+2, oRopeThrow);
939 //writeln("!!! goDown=", goDown, "; doDrop=", doDrop, "; wasHeld=", wasHeld);
942 if (!wasHeld) doDrop = true;
946 if (dir == Dir.Left) rp.xVel = -3.2; else rp.xVel = 3.2;
949 rp.forceFixHoldCoords(self);
951 scrUsePutItOnGroundHelper(rp);
955 if (wasHeld) scrSwitchToPocketItem(forceIfEmpty:false);
957 //writeln("NO DROP!");
961 //rp.resaleValue = 1; //k8:???
965 if (wasHeld) scrSwitchToPocketItem(forceIfEmpty:false);
970 level.MakeMapObject(rp.ix, rp.iy, 'oRopeTop');
977 if (wasHeld) scrSwitchToPocketItem(forceIfEmpty:false);
978 playSound('sndThrow');
983 bool scrLaunchBomb () {
984 if (whipping || global.bombs < 1) return false;
987 ItemBomb bomb = ItemBomb(level.MakeMapObject(ix, iy, 'oBomb'));
988 if (!bomb) return false;
989 bomb.forceFixHoldCoords(self);
990 bomb.setSticky(global.stickyBombsActive);
992 bomb.resaleValue = 0;
994 if (kDown || scrPlayerIsDucking()) {
995 scrUsePutItOnGroundHelper(bomb);
1004 bool scrUseItem () {
1006 if (!it) return false;
1007 //writeln(GetClassName(holdItem.Class));
1009 //auto spr = holdItem.getSprite();
1011 } else if (holdItem.type == "Sceptre") {
1012 if (kDown) scrUsePutItemOnGround(0.4, 0.5);
1013 if (firing == 0 && !scrPlayerIsDucking()) {
1014 if (facing == LEFT) {
1023 obj = instance_create(x+xofs, y+4, oPsychicCreateP);
1024 obj.xVel = xsgn*rand(1, 3);
1025 obj.yVel = -random(2);
1027 obj = instance_create(x+xofs, y-2, oPsychicWaveP);
1029 playSound(global.sndPsychic);
1030 firing = firingPistolMax;
1032 } else if (holdItem.type == "Teleporter II") {
1033 scrUseTeleporter2();
1034 } else if (holdItem.type == "Bow") {
1036 scrUsePutItemOnGround(0.4, 0.5);
1037 } else if (firing == 0 && !scrPlayerIsDucking() && !bowArmed && global.arrows > 0) {
1039 playSound(global.sndBowPull);
1040 } else if (global.arrows <= 0) {
1041 global.message = "I'M OUT OF ARROWS!";
1042 global.message2 = "";
1043 global.messageTimer = 80;
1049 if (whipping) return false;
1052 if (scrPlayerIsDucking()) scrUsePutItemOnGround();
1056 // you cannot throw away shop items, but can throw dices
1057 if (it.forSale && it !isa ItemDice) {
1058 if (!level.isInShop(ix/16, iy/16)) {
1061 // allow throw/use shop items
1066 if (!it.onTryUseItem(self)) {
1075 // ////////////////////////////////////////////////////////////////////////// //
1076 // called by characterStepEvent
1077 // help player jump up through one block wide gaps by nudging them to one side so they don't hit their head
1078 void scrJumpHelper () {
1079 int d = 4; // max distance to nudge player
1081 if (!level.checkTilesInRect(x, y-12, 1, 7)) {
1082 if (level.checkTilesInRect(x-5, y-12, 1, 7) &&
1083 level.checkTilesInRect(x+14, y-12, 1, 7))
1085 while (d > 0 && level.checkTilesInRect(x-5, y-12, 1, 7)) { ++x; shiftX(1); --d; }
1086 } else if (level.checkTilesInRect(x+5, y-12, 1, 7) &&
1087 level.checkTilesInRect(x-14, y-12, 1, 7))
1089 while (d > 0 && level.checkTilesInRect(x+5, y-12, 1, 7)) { --x; shiftX(-1); --d; }
1093 if (!collision_line(x, y-6, x, y-12, oSolid, 0, 0)) {
1094 if (collision_line(x-5, y-6, x-5, y-12, oSolid, 0, 0) &&
1095 collision_line(x+14, y-6, x+14, y-12, oSolid, 0, 0))
1097 while (collision_line(x-5, y-6, x-5, y-12, oSolid, 0, 0) && d > 0) {
1102 else if (collision_line(x+5, y-6, x+5, y-12, oSolid, 0, 0) and
1103 collision_line(x-14, y-6, x-14, y-12, oSolid, 0, 0))
1105 while (collision_line(x+5, y-6, x+5, y-12, oSolid, 0, 0) && d > 0) {
1115 // ////////////////////////////////////////////////////////////////////////// //
1117 * Returns whether a GENERAL trait about a character is true.
1118 * Only the platform character should run this script.
1120 * `tp` can be one of the following:
1125 final bool platformCharacterIs (int tp) {
1126 if (tp == ON_GROUND && (status == RUNNING || status == STANDING || status == DUCKING || status == LOOKING_UP)) return true;
1127 if (tp == IN_AIR && (status == JUMPING || status == FALLING)) return true;
1128 if (tp == ON_LADDER && status == CLIMBING) return true;
1133 // ////////////////////////////////////////////////////////////////////////// //
1134 // sets the sprite of the character depending on his/her status
1135 final void characterSprite () {
1136 if (status == STOPPED) {
1137 if (global.isDamsel) setSprite('sDamselLeft');
1138 else if (global.isTunnelMan) setSprite('sTunnelLeft');
1139 else setSprite('sStandLeft');
1144 if (global.isTunnelMan && !stunned && !whipping) {
1146 if (status == STANDING) {
1147 if (!level.isSolidAtPoint(x-2, y+9)) {
1149 setSprite('sTunnelWhoaL');
1151 setSprite('sTunnelLeft');
1154 if (status == RUNNING) {
1155 if (kUp) setSprite('sTunnelLookRunL'); else setSprite('sTunnelRunL');
1157 if (status == DUCKING) {
1158 if (xVel == 0) setSprite('sTunnelDuckL');
1159 else if (fabs(xVel) < 3) setSprite('sTunnelCrawlL');
1160 else setSprite('sTunnelRunL');
1162 if (status == LOOKING_UP) {
1163 if (fabs(xVel) > 0) setSprite('sTunnelRunL'); else setSprite('sTunnelLookL');
1165 if (status == JUMPING) setSprite('sTunnelJumpL');
1166 if (status == FALLING && statePrev == FALLING && statePrevPrev == FALLING) setSprite('sTunnelFallL');
1167 if (status == HANGING) setSprite('sTunnelHangL');
1168 if (pushTimer > 20) setSprite('sTunnelPushL');
1169 if (status == DUCKTOHANG) setSprite('sTunnelDtHL');
1170 if (status == CLIMBING) {
1171 if (level.isRopeAtPoint(x, y)) {
1172 if (kDown) setSprite('sTunnelClimb3'); else setSprite('sTunnelClimb2');
1174 setSprite('sTunnelClimb');
1177 } else if (global.isDamsel && !stunned && !whipping) {
1179 if (status == STANDING) {
1180 if (!level.isSolidAtPoint(x-2, y+9)) {
1182 setSprite('sDamselWhoaL');
1183 /* was commented out in the original
1184 if (holdItem && whoaTimer < 1) {
1185 holdItem.held = false;
1186 if (facing == LEFT) holdItem.xVel = -2; else holdItem.xVel = 2;
1187 if (holdItem.type == "Damsel") playSound('sndDamsel');
1188 if (holdItem.type == pickupItemType) { holdItem = 0; pickupItemType = ""; } else scrSwitchToPocketItem();
1192 setSprite('sDamselLeft');
1195 if (status == RUNNING) {
1196 if (kUp) setSprite('sDamselRunL'); else setSprite('sDamselRunL');
1198 if (status == DUCKING) {
1199 if (xVel == 0) setSprite('sDamselDuckL');
1200 else if (fabs(xVel) < 3) setSprite('sDamselCrawlL');
1201 else setSprite('sDamselRunL');
1203 if (status == LOOKING_UP) {
1204 if (fabs(xVel) > 0) setSprite('sDamselRunL'); else setSprite('sDamselLookL');
1206 if (status == JUMPING) setSprite('sDamselDieLR');
1207 if (status == FALLING && statePrev == FALLING && statePrevPrev == FALLING) setSprite('sDamselFallL');
1208 if (status == HANGING) setSprite('sDamselHangL');
1209 if (pushTimer > 20) setSprite('sDamselPushL');
1210 if (status == DUCKTOHANG) setSprite('sDamselDtHL');
1211 if (status == CLIMBING) {
1212 if (level.isRopeAtPoint(x, y)) {
1213 if (kDown) setSprite('sDamselClimb3'); else setSprite('sDamselClimb2');
1215 setSprite('sDamselClimb');
1218 } else if (!stunned && !whipping) {
1220 if (status == STANDING) {
1221 if (!level.checkTileAtPoint(x-(dir == Dir.Left ? 2 : 0), y+9, &level.cbCollisionForWhoa)) {
1223 setSprite('sWhoaLeft');
1224 /* was commented out in the original
1225 if (holdItem && whoaTimer < 1) {
1226 holdItem.held = false;
1227 if (facing == LEFT) holdItem.xVel = -2; else holdItem.xVel = 2;
1228 if (holdItem.type == "Damsel") playSound('sndDamsel');
1229 if (holdItem.type == pickupItemType) { holdItem = 0; pickupItemType = ""; } else scrSwitchToPocketItem();
1233 setSprite('sStandLeft');
1236 if (status == RUNNING) {
1237 if (kUp) setSprite('sLookRunL'); else setSprite('sRunLeft');
1239 if (status == DUCKING) {
1240 if (xVel == 0) setSprite('sDuckLeft');
1241 else if (fabs(xVel) < 3) setSprite('sCrawlLeft');
1242 else setSprite('sRunLeft');
1244 if (status == LOOKING_UP) {
1245 if (fabs(xVel) > 0) setSprite('sLookRunL'); else setSprite('sLookLeft');
1247 if (status == JUMPING) setSprite('sJumpLeft');
1248 if (status == FALLING && statePrev == FALLING && statePrevPrev == FALLING) setSprite('sFallLeft');
1249 if (status == HANGING) setSprite('sHangLeft');
1250 if (pushTimer > 20) setSprite('sPushLeft');
1251 if (status == CLIMBING) {
1252 if (level.isRopeAtPoint(x, y)) {
1253 if (kDown) setSprite('sClimbUp3'); else setSprite('sClimbUp2');
1255 setSprite('sClimbUp');
1258 if (status == DUCKTOHANG) setSprite('sDuckToHangL');
1264 // ////////////////////////////////////////////////////////////////////////// //
1265 void addScore (int delta) {
1266 if (!level.isNormalLevel()) return;
1268 if (delta == 0) return;
1269 level.stats.addMoney(delta);
1271 level.xmoney += delta;
1272 level.collectCounter = min(100, level.collectCounter+20);
1277 // ////////////////////////////////////////////////////////////////////////// //
1278 // for dead players too
1279 // first, the code will call `onObjectTouched()` for player
1280 // if it returned `false`, the code will call `obj.onTouchedByPlayer()`
1281 // note that player's handler is called *after* its frame thinker,
1282 // but object handler is called *before* frame thinker for the object
1283 // i.e. return `true` to block calling `obj.onTouchedByPlayer()`,
1284 // (but NOT object thinker)
1285 bool onObjectTouched (MapObject obj) {
1287 if (dead || global.plife <= 0) return false; // player may be rendered dead, but not yet transited to dead state
1289 if (obj isa ItemProjectileArrow && holdItem isa ItemWeaponBow && !stunned && global.arrows < 99) {
1290 if (fabs(obj.xVel) < 1 && fabs(obj.yVel) < 1 && !obj.stuck) {
1292 playSound('sndPickup');
1293 obj.instanceRemove();
1299 auto treasure = ItemTreasure(obj);
1300 if (treasure && treasure.canCollect) {
1301 if (treasure.value) addScore(treasure.value);
1302 treasure.onCollected(self); // various other effects
1303 playSound(treasure.soundName);
1304 treasure.instanceRemove();
1309 if (global.hasKapala && obj isa MapObjBlood) {
1310 global.bloodLevel += 1;
1311 level.MakeMapObject(obj.ix, obj.iy, 'oBloodSpark');
1312 obj.instanceRemove();
1314 if (global.bloodLevel > 8) {
1315 global.bloodLevel = 0;
1317 level.MakeMapObject(ix, iy-8, 'oHeart');
1318 playSound('sndKiss');
1321 if (redColor < 55) redColor += 5;
1325 // other objects will take care of themselves
1330 // return `false` to prevent
1331 // holdItem is valid
1332 bool onLoosingHeldItem (LostCause cause) {
1333 if (level.inWinCutscene != 0) return false;
1338 // ////////////////////////////////////////////////////////////////////////// //
1339 // k8: don't even ask me! the following mess is almost straightforward port of the original Derek's code!
1340 private final void closeCape () {
1341 auto pp = PPCape(findPowerup('Cape'));
1342 if (pp) pp.open = false;
1346 private final void switchCape () {
1347 auto pp = PPCape(findPowerup('Cape'));
1348 if (pp) pp.open = !pp.open;
1352 final bool isCapeActiveAndOpen () {
1353 auto pp = PPCape(findPowerup('Cape'));
1354 return (pp && pp.active && pp.open);
1358 final bool isParachuteActive () {
1359 auto pp = findPowerup('Parachute');
1360 return (pp && pp.active);
1364 // ////////////////////////////////////////////////////////////////////////// //
1366 bool checkSkipCutScene () {
1367 if (skipCutscenePressed) {
1368 return level.isKeyReleased(GameConfig::Key.Pay);
1370 skipCutscenePressed = level.isKeyPressed(GameConfig::Key.Pay);
1378 bool forcePlayerControls () {
1379 if (level.inWinCutscene) {
1381 level.winCutscenePlayerControl(self);
1383 } else if (level.inIntroCutscene) {
1385 level.introCutscenePlayerControl(self);
1388 } else if (level.levelKind == GameLevel::LevelKind.Transition) {
1391 if (checkSkipCutScene()) {
1392 level.playerExited = true;
1396 auto door = level.checkTileAtPoint(ix, iy, &level.cbCollisionExitTile);
1398 kExitPressed = true;
1402 if (status == STOPPED) {
1403 if (--transKissTimer > 0) return true;
1408 auto dms = MonsterDamselKiss(level.isObjectAtPoint(ix+8, iy+4, delegate bool (MapObject o) { return (o isa MonsterDamselKiss); }));
1409 if (dms && !dms.kissed) {
1414 transKissTimer = 30;
1419 kRightPressed = true;
1426 // ////////////////////////////////////////////////////////////////////////// //
1427 private final void checkControlKeys (SpriteImage spr) {
1428 if (forcePlayerControls()) {
1429 if (movementBlocked) unpressAllKeys();
1430 if (kLeft) kLeftPushedSteps += 1; else kLeftPushedSteps = 0;
1431 if (kRight) kRightPushedSteps += 1; else kRightPushedSteps = 0;
1435 kLeft = level.isKeyDown(GameConfig::Key.Left);
1436 if (movementBlocked) kLeft = false;
1437 if (kLeft) kLeftPushedSteps += 1; else kLeftPushedSteps = 0;
1438 kLeftPressed = level.isKeyPressed(GameConfig::Key.Left);
1439 kLeftReleased = level.isKeyReleased(GameConfig::Key.Left);
1441 kRight = level.isKeyDown(GameConfig::Key.Right);
1442 if (movementBlocked) kRight = false;
1443 if (kRight) kRightPushedSteps += 1; else kRightPushedSteps = 0;
1444 kRightPressed = level.isKeyPressed(GameConfig::Key.Right);
1445 kRightReleased = level.isKeyReleased(GameConfig::Key.Right);
1447 kUp = level.isKeyDown(GameConfig::Key.Up);
1448 kDown = level.isKeyDown(GameConfig::Key.Down);
1450 kJump = level.isKeyDown(GameConfig::Key.Jump);
1451 kJumpPressed = level.isKeyPressed(GameConfig::Key.Jump);
1452 kJumpReleased = level.isKeyReleased(GameConfig::Key.Jump);
1454 if (movementBlocked) unpressAllKeys();
1458 kJumpPressed = false;
1459 kJumpReleased = false;
1461 } else if (spr && global.isTunnelMan && spr.Name == 'sTunnelAttackL' && !holdItem) {
1463 kJumpPressed = false;
1464 kJumpReleased = false;
1465 cantJump = max(0, cantJump-1);
1468 kAttack = level.isKeyDown(GameConfig::Key.Attack);
1469 kAttackPressed = level.isKeyPressed(GameConfig::Key.Attack);
1470 kAttackReleased = level.isKeyReleased(GameConfig::Key.Attack);
1472 kItemPressed = level.isKeyPressed(GameConfig::Key.Switch);
1473 kRopePressed = level.isKeyPressed(GameConfig::Key.Rope);
1474 kBombPressed = level.isKeyPressed(GameConfig::Key.Bomb);
1476 kPayPressed = level.isKeyPressed(GameConfig::Key.Pay);
1478 if (movementBlocked) unpressAllKeys();
1480 kExitPressed = false;
1481 if (global.config.useDoorWithButton) {
1482 if (kPayPressed) kExitPressed = true;
1484 if (kUp) kExitPressed = true;
1487 if (stunned || dead) {
1489 //level.clearKeysPressRelease();
1494 // ////////////////////////////////////////////////////////////////////////// //
1495 // knock off monkeys that grabbed you
1496 void knockOffMonkeys () {
1497 level.forEachObject(delegate bool (MapObject o) {
1498 auto mk = EnemyMonkey(o);
1499 if (mk && !mk.dead && mk.status == GRAB) {
1500 mk.xVel = global.randOther(0, 1)-global.randOther(0, 1);
1503 mk.vineCounter = 20;
1504 mk.grabCounter = 60;
1511 // ////////////////////////////////////////////////////////////////////////// //
1512 // fix collision with boulder (bug with non-aligned boulder)
1513 void hackBoulderCollision () {
1514 auto bld = level.checkTilesInRect(x0, y0, width, height, delegate bool (MapTile o) { return (o isa ObjBoulder); });
1515 if (bld && fabs(bld.xVel) <= 1) {
1516 writeln("IN BOULDER!");
1519 writeln(" LEFT: dx=", dx);
1520 if (dx <= 2) fltx = x0-dx;
1521 } else if (x1 > bld.x1) {
1523 writeln(" RIGHT: dx=", dx);
1524 if (dx <= 2) fltx = x1-dx;
1530 // ////////////////////////////////////////////////////////////////////////// //
1531 bool checkHangTileDG (MapTile t) { return (t.solid || t.tree); }
1534 void checkPerformHang (bool colLeft, bool colRight) {
1535 if (status == HANGING || platformCharacterIs(ON_GROUND)) return;
1536 if ((kLeft && kRight) || (!kLeft && !kRight)) return;
1537 if (kLeft && !colLeft) {
1539 writeln("checkPerformHang: no left solid");
1543 if (kRight && !colRight) {
1545 writeln("checkPerformHang: no right solid");
1549 if (hangCount != 0) {
1551 writeln("checkPerformHang: hangCount=", hangCount);
1555 if (iy <= 16) return;
1556 int dx = (kLeft ? -9 : 9);
1558 writeln("checkPerformHang: trying to hang at ", dx);
1561 bool doHang = false;
1563 if (global.hasGloves) {
1564 doHang = (yVel > 0 && !!level.checkTilesInRect(ix+dx, iy-6, 1, 2, &checkHangTileDG));
1567 doHang = !!level.checkTilesInRect(ix+dx, iy-6, 1, 2, &level.cbCollisionAnyTree);
1569 writeln(" tree: ", doHang);
1573 doHang = level.checkTilesInRect(ix+dx, iy-6, 1, 2) &&
1574 !level.isSolidAtPoint(ix+dx, iy-9) && !level.isSolidAtPoint(ix, iy+9);
1576 writeln(" solid: ", doHang);
1581 writeln(" solid at dx, -6(1): ", !!level.checkTilesInRect(ix+dx, iy-6, 1, 2));
1582 writeln(" solid at dx, -9(0): ", !!level.isSolidAtPoint(ix+dx, iy-9));
1583 writeln(" solid at 0, +9(0): ", !!level.isSolidAtPoint(ix, iy-9));
1586 doHang = level.checkTilesInRect(ix+dx, iy-6, 1, 2) &&
1587 !level.isSolidAtPoint(ix+dx, iy-10) && !level.isSolidAtPoint(ix, iy+9);
1589 if (!doHang) writeln(" easier hang failed");
1592 if (!level.isSolidAtPoint(ix, iy-9)) {
1593 foreach (int dy; 6..24) {
1594 writeln(" solid at dx:-", dy, "(0): ", !!level.isSolidAtPoint(ix+dx, iy-dy));
1596 writeln(" ix=", ix, "; iy=", iy);
1613 // ////////////////////////////////////////////////////////////////////////// //
1614 final void characterStepEvent () {
1615 if (climbSoundTimer > 0) {
1616 if (--climbSoundTimer == 0) {
1617 playSound(climbSndToggle ? 'sndClimb2' : 'sndClimb1');
1618 climbSndToggle = !climbSndToggle;
1622 auto spr = getSprite();
1623 checkControlKeys(spr);
1625 float xPrev = fltx, yPrev = flty;
1628 // check collisions in various directions
1629 bool colSolidLeft = !!getPushableLeft(1);
1630 bool colSolidRight = !!getPushableRight(1);
1631 bool colLeft = !!isCollisionLeft(1);
1632 bool colRight = !!isCollisionRight(1);
1633 bool colTop = !!isCollisionTop(1);
1634 bool colBot = !!isCollisionBottom(1);
1635 bool colLadder = !!isCollisionLadder();
1636 bool colPlatBot = !!isCollisionBottom(1, &level.cbCollisionPlatform);
1637 bool colPlat = !!isCollision(&level.cbCollisionPlatform);
1638 //bool colWaterTop = !!isCollisionTop(1, &level.cbCollisionWater);
1639 bool colWaterTop = !!level.checkTilesInRect(x0, y0-1, width, 3, &level.cbCollisionWater);
1640 bool colIceBot = !!level.isIceAtPoint(x, y+8);
1642 bool runKey = false;
1643 if (level.isKeyDown(GameConfig::Key.Run)) { runHeld = 100; runKey = true; }
1644 if (level.isKeyDown(GameConfig::Key.Attack) && !whipping) { runHeld += 1; runKey = true; }
1645 if (!runKey || (!kLeft && !kRight)) runHeld = 0;
1647 // allows the character to run left and right
1648 // if state!=DUCKING and state!=LOOKING_UP and state!=CLIMBING
1649 if (status != CLIMBING && status != HANGING) {
1650 if (kLeftReleased && fabs(xVel) < 0.0001) xAcc -= 0.5;
1651 if (kRightReleased && fabs(xVel) < 0.0001) xAcc += 0.5;
1652 if (kLeft && !kRight) {
1654 //xVel = 3; // in orig
1655 if (platformCharacterIs(ON_GROUND) && status != DUCKING) {
1658 //playSound('sndPush', unique:true);
1660 } else if (kLeftPushedSteps > 2 && (dir == Dir.Left || fabs(xVel) < 0.0001)) {
1664 //if (platformCharacterIs(ON_GROUND) && fabs(xVel) > 0 && alarm[3] < 1) alarm[3] = floor(16/-xVel);
1666 if (kRight && !kLeft) {
1667 if (colSolidRight) {
1668 //xVel = 3; // in orig
1669 if (platformCharacterIs(ON_GROUND) && status != DUCKING) {
1672 //playSound('sndPush', unique:true);
1674 } else if ((kRightPushedSteps > 2 || colSolidLeft) && (dir == Dir.Right || fabs(xVel) < 0.0001)) {
1678 //if (platformCharacterIs(ON_GROUND) && fabs(xVel) > 0 && alarm[3] < 1) alarm[3] = floor(16/xVel);
1683 if (status == CLIMBING) {
1687 auto ladder = level.isLadderAtPoint(x, y);
1688 if (ladder) { x = ladder.ix+8; setX(x); }
1689 if (kLeft) dir = Dir.Left; else if (kRight) dir = Dir.Right;
1691 // checks both ladder and laddertop
1692 if (level.isAnyLadderAtPoint(x, y-8)) {
1693 //writeln("LADDER00! old yAcc=", yAcc, "; climbAcc=", climbAcc, "; new yAcc=", yAcc-climbAcc);
1695 if (climbSoundTimer < 1) climbSoundTimer = climbSndSpeed;
1696 //!if (alarm[2] < 1) alarm[2] = climbSndSpeed;
1699 for (int dy = -6; dy > -12; --dy) {
1700 ladder = level.isAnyLadderAtPoint(x, y+dy);
1702 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));
1707 auto grid = level.miscTileGrid;
1708 foreach (MapTile t; grid.inCellPix(48, 96, grid.nextTag(), precise:false)) {
1709 writeln("at 48, 96: ", GetClassName(t.Class), "; pos=(", t.ix, ",", t.iy, ")");
1711 foreach (MapTile t; grid.inCellPix(48, 94, grid.nextTag(), precise:false)) {
1712 writeln("at 48, 94: ", GetClassName(t.Class), "; pos=(", t.ix, ",", t.iy, ")");
1714 foreach (int dy; 90..102) {
1715 ladder = level.isAnyLadderAtPoint(48, dy);
1717 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));
1723 // checks both ladder and laddertop
1724 if (level.isAnyLadderAtPoint(x, y+8)) {
1726 //!if (alarm[2] < 1) alarm[2] = climbSndSpeed;
1727 if (climbSoundTimer < 1) climbSoundTimer = climbSndSpeed;
1731 if (colBot) status = STANDING;
1734 if (kJumpPressed && !whipping) {
1735 if (kLeft) xVel = -departLadderXVel; else if (kRight) xVel = departLadderXVel; else xVel = 0;
1736 //yAcc += departLadderYVel;
1737 //k8: was `0.6`, but with `0.4` we can jump onto the wall above, and with `0.6` we cannot
1738 yAcc = 0.4+departLadderYVel; // YASM 1.8.1 Fix for extra air when jumping off ladders due to increased climb speed option
1740 jumpButtonReleased = false;
1745 if (ladderTimer > 0) ladderTimer -= 1;
1748 if (platformCharacterIs(IN_AIR) && status != HANGING) yAcc += gravityIntensity;
1750 // player has landed
1751 if ((colBot || colPlatBot) && platformCharacterIs(IN_AIR) && yVel >= 0) {
1752 if (!colPlat || colBot) {
1757 playSound('sndLand');
1759 if ((colBot || colPlatBot) && !colPlat) yVel = 0;
1761 // player has just walked off of the edge of a solid
1762 if (colBot == 0 && (!colPlatBot || colPlat) && platformCharacterIs(ON_GROUND)) {
1766 if (global.hasGloves) hangCount = 5;
1770 if (dead || stunned) yVel = -yVel*0.8;
1771 else if (status == JUMPING) yVel = fabs(yVel*0.3);
1774 if ((colLeft && dir == Dir.Left) || (colRight && dir == Dir.Right)) {
1775 if (dead || stunned) xVel = -xVel*0.5; else xVel = 0;
1779 if (kJumpReleased && platformCharacterIs(IN_AIR)) {
1781 } else if (platformCharacterIs(ON_GROUND)) {
1786 MapObject oWeb = none, oBlob = none;
1788 oWeb = level.isObjectAtPoint(x, y, &level.cbIsObjectWeb);
1789 if (!oWeb) oBlob = level.isObjectAtPoint(x, y, &level.cbIsObjectBlob);
1792 bool invokeJumpHelper = false;
1794 if (kJumpPressed && oWeb) {
1795 ItemWeb(oWeb).tear(1);
1796 yAcc += initialJumpAcc*2;
1801 jumpButtonReleased = false;
1805 invokeJumpHelper = true;
1806 } else if (kJumpPressed && oBlob) {
1808 scrCreateBloblets(oBlob.x0+8, oBlob.y0+8, 1);
1809 playSound('sndHit');
1810 yAcc += initialJumpAcc*2;
1814 jumpButtonReleased = false; // k8: was `jumpButtonRelease`
1816 invokeJumpHelper = true;
1817 } else if (kJumpPressed && colWaterTop) {
1818 yAcc += initialJumpAcc*2;
1823 jumpButtonReleased = false;
1827 invokeJumpHelper = true;
1828 } else if (global.hasCape && kJumpPressed && kJumped && platformCharacterIs(IN_AIR)) {
1830 } else if (global.hasJetpack && !swimming && kJump && kJumped && platformCharacterIs(IN_AIR) && jetpackFuel > 0) {
1831 yAcc += initialJumpAcc;
1834 if (jetpackFlaresTime < 1) jetpackFlaresTime = 3;
1835 //!if (alarm[10] < 1) alarm[10] = 3; // jetpack flares
1839 jumpButtonReleased = false;
1843 invokeJumpHelper = true;
1844 } else if (platformCharacterIs(ON_GROUND) && kJumpPressed && fallTimer == 0) {
1845 if (fabs(xVel) > 3 /*xVel > 3 || xVel < -3*/) {
1846 yAcc += initialJumpAcc*2;
1849 yAcc += initialJumpAcc*2;
1851 //scrJumpHelper(); // move to location where player doesn't have to be on ground
1853 if (global.hasJordans) {
1857 } else if (global.hasSpringShoes) {
1864 playSound('sndJump');
1868 // the "state" gets changed to JUMPING later on in the code
1870 // "variable jumping" states
1871 jumpButtonReleased = false;
1873 invokeJumpHelper = true;
1876 if (kJumpPressed && invokeJumpHelper) scrJumpHelper(); // YASM 1.8.1
1878 if (jumpTime < jumpTimeTotal) jumpTime += 1;
1879 // let the character continue to jump
1880 if (!kJump) jumpButtonReleased = true;
1881 if (jumpButtonReleased) jumpTime = jumpTimeTotal;
1883 gravityIntensity = (jumpTime/jumpTimeTotal)*grav;
1885 if (kUp && platformCharacterIs(ON_GROUND) && !colLadder) {
1886 //k8:!!!looking = UP;
1887 if (xVel == 0 && xAcc == 0) status = LOOKING_UP;
1889 //k8:!!!looking = 0;
1892 if (!kUp && status == LOOKING_UP) status = STANDING;
1896 checkPerformHang(colLeft, colRight);
1900 // hang on stuck arrow
1901 if (status == FALLING && hangCount == 0 && y > 16 && !platformCharacterIs(ON_GROUND) &&
1902 !level.isSolidAtPoint(x, y+12)) // from Spelunky Natural
1904 auto arrow = level.isObjectInRect(ix, iy, 16, 16, delegate bool (MapObject o) {
1907 writeln(" ARROW : (", o.x0, ",", o.y0, ")-(", o.x1, ",", o.y1, "); coll=", o.collidesWith(self));
1908 writeln(" PLAYER: (", x0, ",", y0, ")-(", x1, ",", y1, "); coll=", self.collidesWith(o), "; dy=", iy-o.iy);
1910 if (o.stuck && iy-o.iy >= -6 && iy-o.iy <= -5 && o.collidesWith(self)) {
1911 //writeln(" *** HANG IS POSSIBLE! p5=", !!level.isObjectAtPoint(ix, iy-5, &level.cbIsObjectArrow), "; p6=", !!level.isObjectAtPoint(ix, iy-6, &level.cbIsObjectArrow));
1915 }, castClass:ItemProjectileArrow, precise:false);
1918 // move_snap(1, 8); // was commented out in the original
1924 writeln("TRYING ARROW HANG ALLOWED");
1925 writeln(" Z00: ", !level.isObjectAtPoint(x, y-9, &level.cbIsObjectArrow));
1926 writeln(" Z01: ", !level.isObjectAtPoint(x, y+9, &level.cbIsObjectArrow));
1927 writeln(" Z02: ", !!level.isObjectAtPoint(x, y-5, &level.cbIsObjectArrow));
1928 writeln(" Z03: ", !!level.isObjectAtPoint(x, y-6, &level.cbIsObjectArrow));
1929 level.isObjectInRect(ix, iy, 16, 16, delegate bool (MapObject o) {
1931 writeln(" ARROW : (", o.x0, ",", o.y0, ")-(", o.x1, ",", o.y1, "); coll=", o.collidesWith(self));
1932 writeln(" PLAYER: (", x0, ",", y0, ")-(", x1, ",", y1, "); coll=", self.collidesWith(o), "; dy=", iy-o.iy);
1933 if (iy-o.iy >= -6 && iy-o.iy <= -5 && o.collidesWith(self)) {
1934 writeln(" *** HANG IS POSSIBLE! p5=", !!level.isObjectAtPoint(ix, iy-5, &level.cbIsObjectArrow), "; p6=", !!level.isObjectAtPoint(ix, iy-6, &level.cbIsObjectArrow));
1937 }, castClass:ItemProjectileArrow, precise:false);
1941 // hang on stuck arrow
1942 /*k8: this is not working due to collision issues; see the fixed code above
1943 if (status == FALLING && hangCount == 0 && y > 16 && !platformCharacterIs(ON_GROUND) &&
1944 !level.isSolidAtPoint(x, y+12) && // from Spelunky Natural
1945 !level.isObjectAtPoint(x, y-9, &level.cbIsObjectArrow) && !level.isObjectAtPoint(x, y+9, &level.cbIsObjectArrow))
1947 //obj = instance_nearest(x, y-5, oArrow);
1948 auto arr0 = level.isObjectAtPoint(x, y-5, &level.cbIsObjectArrow);
1949 auto arr1 = level.isObjectAtPoint(x, y-6, &level.cbIsObjectArrow);
1951 writeln("ARROW HANG!");
1952 // get nearest arrow
1955 arr = (arr0.distanceToPoint(x, y-5) < arr1.distanceToPoint(x, y-5) ? arr0 : arr1);
1957 arr = (arr0 ? arr0 : arr1);
1961 // move_snap(1, 8); // was commented out in the original
1969 /* this was commented in the original
1970 if (hangCount == 0 && y > 16 && !platformCharacterIs(ON_GROUND) && state == FALLING &&
1971 (collision_point(x, y-5, oTreeBranch, 0, 0) || collision_point(x, y-6, oTreeBranch, 0, 0)) &&
1972 !collision_point(x, y-9, oTreeBranch, 0, 0) && !collision_point(x, y+9, oTreeBranch, 0, 0))
1975 // move_snap(1, 8); // was commented out in the original
1983 if (hangCount > 0) --hangCount;
1985 if (status == HANGING) {
1990 if (global.hasGloves) {
1991 if (hangCount == 0 && y > 16 && !platformCharacterIs(ON_GROUND)) {
1992 if (kRight && colRight &&
1993 (level.isSolidAtPoint(x+9, y-5) || level.isSolidAtPoint(x+9, y-6)))
1999 } else if (kLeft && colLeft &&
2000 (level.isSolidAtPoint(x-9, y-5) || level.isSolidAtPoint(x-9, y-6)))
2022 yAcc += initialJumpAcc*2;
2023 shiftX(dir == Dir.Right ? -2 : 2);
2026 hangCount = hangCountMax;
2027 if (level.isObjectAtPoint(x, y-5, &level.cbIsObjectArrow) || level.isObjectAtPoint(x, y-6, &level.cbIsObjectArrow)) hangCount /= 2; //Spelunky Natural
2030 if ((dir == Dir.Left && !isCollisionLeft(2)) ||
2031 (dir == Dir.Right && !isCollisionRight(2)))
2042 // pressing down while standing
2043 if (kDown && platformCharacterIs(ON_GROUND) && !whipping) {
2046 } else if (colPlatBot) {
2047 // climb down ladder if possible, else jump down
2050 //ladder = instance_place(x, y+16, oLadder);
2052 // from Spelunky Natural
2054 ladder = collision_line(x-4, y+16, x+4, y+16, oLadder, 0, 0);
2055 if (!ladder) ladder = collision_line(x-4, y+16, x+4, y+16, oLadderTop, 0, 0);
2057 auto ladder = level.checkTilesInRect(x-4, y+16, 9, 1, &level.cbCollisionAnyLadder);
2058 //writeln("DOWN; cpb=", colPlatBot, "; cb=", colBot, "; ladder=", !!ladder);
2061 if (abs(x-(ladder.x0+8)) < 4) {
2075 kJumped = true; // Spelunky Natural
2079 // the character can't move down because there is a solid in the way
2084 if (!kDown && status == DUCKING) {
2089 if (xVel == 0 && xAcc == 0 && status == RUNNING) status = STANDING;
2090 if (xAcc != 0 && status == STANDING) status = RUNNING;
2091 if (yVel < 0 && platformCharacterIs(IN_AIR) && status != HANGING) status = JUMPING;
2092 if (yVel > 0 && platformCharacterIs(IN_AIR) && status != HANGING) status = FALLING;
2093 setCollisionBounds(-5, -6, 5, 8);
2096 bool colPointLadder = !!level.isAnyLadderAtPoint(x, y);
2098 /* this was commented in the original
2099 if ((kUp && platformCharacterIs(IN_AIR) && collision_point(x, y-8, oLadder, 0, 0) && ladderTimer == 0) ||
2100 (kUp && colPointLadder && ladderTimer == 0) ||
2101 (kDown && colPointLadder && ladderTimer == 0 && platformCharacterIs(ON_GROUND) && collision_point(x, y+9, oLadderTop, 0, 0) && xVel == 0))
2104 ladder = instance_place(x, y-8, oLadder);
2105 if (instance_exists(ladder)) {
2106 if (abs(x-(ladder.x0+8)) < 4) {
2109 if (!collision_point(x, y, oLadder, 0, 0) && !collision_point(x, y, oLadderTop, 0, 0)) { y = ladder.iy+14; setY(y); }
2119 // Spelunky Natural - Multiple changes to this big "if" condition
2120 if ((kUp && platformCharacterIs(IN_AIR) && ladderTimer == 0 && level.checkTilesInRect(x-2, y-8, 5, 1, &level.cbCollisionLadder)) ||
2121 (kUp && colPointLadder && ladderTimer == 0) ||
2122 (kDown && colPointLadder && ladderTimer == 0 && platformCharacterIs(ON_GROUND) && xVel == 0 && level.isLadderTopAtPoint(x, y+9)) ||
2123 ((kUp || kDown) && status == HANGING && level.checkTilesInRect(x-2, y, 5, 1, &level.cbCollisionLadder)))
2126 //auto ladder = instance_place(x, y-8, oLadder);
2127 auto ladder = level.isLadderAtPoint(x, y-8);
2129 //writeln("LADDER01! plrx=", x, "; ladder.x0=", ladder.x0, "; ladder.ix=", ladder.ix, "; ladder class=", GetClassName(ladder.Class));
2130 if (abs(x-(ladder.x0+8)) < 4) {
2133 if (!level.isAnyLadderAtPoint(x, y)) { y = ladder.y0+14; setY(y); }
2143 /* this was commented in the original
2144 if (sprite_index == sDuckToHangL || sprite_index == sDamselDtHL) {
2146 if (facing == LEFT && collision_rectangle(x-8, y, x, y+16, oLadder, 0, 0) && !collision_point(x-4, y+16, oSolid, 0, 0)) {
2147 ladder = instance_nearest(x-4, y+16, oLadder);
2148 } else if (facing == RIGHT && collision_rectangle(x, y, x+8, y+16, oLadder, 0, 0) && !collision_point(x+4, y+16, oSolid, 0, 0)) {
2149 ladder = instance_nearest(x+4, y+16, oLadder);
2162 if (colLadder && state == CLIMBING && kJumpPressed && !whipping) {
2163 if (kLeft) xVel = -departLadderXVel; else if (kRight) xVel = departLadderXVel; else xVel = 0;
2164 yAcc += departLadderYVel;
2166 jumpButtonReleased = false;
2172 // calculate horizontal/vertical friction
2173 if (status == CLIMBING) {
2174 xFric = frictionClimbingX;
2175 yFric = frictionClimbingY;
2177 //if (runKey && platformCharacterIs(ON_GROUND) && runHeld >= 10)
2178 if ((runKey && runHeld >= 10) && (platformCharacterIs(ON_GROUND) || global.config.toggleRunAnywhere)) {
2184 xFric = frictionRunningFastX;
2185 } else if (kRight) {
2188 xFric = frictionRunningFastX;
2190 } else if (status == DUCKING) {
2191 if (xVel < 2 && xVel > -2) {
2195 } else if (kLeft && global.config.downToRun) {
2199 xFric = frictionRunningFastX;
2200 } else if (kRight && global.config.downToRun) {
2203 xFric = frictionRunningFastX;
2206 if (xVel < 0.5) xVel = 0;
2212 // decrease the friction when the character is "flying"
2213 if (platformCharacterIs(IN_AIR)) {
2214 if (dead || stunned) xFric = 1.0; else xFric = 0.8;
2216 xFric = frictionRunningX;
2220 /* // ORIGINAL RUN/WALK xVel/xFric code this was commented in the original
2221 if (runKey && platformCharacterIs(ON_GROUND) && runHeld >= 10) {
2226 xFric = frictionRunningFastX;
2227 } else if (kRight) {
2230 xFric = frictionRunningFastX;
2232 } else if (state == DUCKING) {
2233 if (xVel < 2 && xVel > -2) {
2237 } else if (kLeft && global.downToRun) {
2241 xFric = frictionRunningFastX;
2242 } else if (kRight && global.downToRun) {
2245 xFric = frictionRunningFastX;
2248 if (xVel < 0.5) xVel = 0;
2254 // decrease the friction when the character is "flying"
2255 if (platformCharacterIs(IN_AIR)) {
2256 if (dead || stunned) xFric = 1.0; else xFric = 0.8;
2258 xFric = frictionRunningX;
2263 // stuck on web or underwater
2264 if (level.isObjectAtPoint(x, y, &level.cbIsObjectWeb)) {
2268 } else if (level.isObjectAtPoint(x, y, &level.cbIsObjectBlob)) {
2270 //obj = instance_place(x, y, oBlob); this was commented in the original
2271 //xVel += obj.xVel; this was commented in the original
2275 } else if (level.isWaterAtPoint(x, y/*, oWater, -1, -1*/)) {
2277 //if (!runKey && global.toggleRunAnywhere) xFric = frictionRunningX; // YASM 1.8.1 this was commented in the original
2278 if (!platformCharacterIs(ON_GROUND)) xFric = frictionRunningX;
2279 if (status == FALLING && yVel > 0) {
2281 if (global.config.naturalSwim && kUp) yFric = 0.2;
2282 else if (global.config.naturalSwim && kDown) yFric = 0.8;
2284 } else if (!level.isWaterAtPoint(x, y-9/*, oWater, -1, -1*/)) {
2289 if (yVel < -6 && global.config.noDolphin) {
2290 // Spelunky Natural (changed from -4 to -6)
2299 if (colIceBot && status != DUCKING && !global.hasSpikeShoes) {
2305 if (global.config.toggleRunAnywhere) {
2306 if (!kJump && !kDown && !runKey) xVelLimit = 3;
2310 if (platformCharacterIs(ON_GROUND)) {
2311 if (status == RUNNING && kLeft && colLeft) pushTimer += 1;
2312 else if (status == RUNNING && kRight && colRight) pushTimer += 1;
2315 //if (platformCharacterIs(ON_GROUND) && !kJump && !kDown && !runKey) this was commented in the original
2316 if (!kJump && !kDown && !runKey) xVelLimit = 3;
2318 /* this was commented in the original
2320 if (state == DUCKING && fabs(xVel) < 3 && facing == LEFT &&
2321 //collision_point(x, y+9, oSolid, 0, 0) && !collision_point(x-1, y+9, oSolid, 0, 0) && kLeft)
2322 collision_point(x, y+9, oSolid, 0, 0) && !collision_line(x-1, y+9, x-10, y+9, oSolid, 0, 0) && kLeft)
2327 if (kLeft && dir == Dir.Left) dhdir = -1;
2328 else if (kRight && dir == Dir.Right) dhdir = 1;
2330 if (dhdir && status == DUCKING && fabs(xVel) < 3+(dhdir < 0 ? 1 : 0) &&
2331 level.isSolidAtPoint(x, y+9) && !level.checkTilesInRect(x+(dhdir < 0 ? -8 : 1), y+9, 8, 8))
2333 status = DUCKTOHANG;
2335 if (!global.config.scumFlipHold || holdItem.heavy) {
2337 holdItem.heldBy = none;
2338 if (holdItem.objName == 'GoldIdol') holdItem.shiftY(-8);
2340 //else if (holdItem.type == "Block Item") { with (oBlockPreview) instance_destroy(); }
2341 scrDropItem(LostCause.Hang, (dir == Dir.Left ? -1 : 1), -4);
2348 if (status == DUCKTOHANG) {
2349 setXY(xPrev, yPrev);
2359 // parachute and cape
2360 if (!level.inWinCutscene) {
2361 if (isParachuteActive() || isCapeActiveAndOpen()) yFric = 0.5;
2364 if (pushTimer > 100) pushTimer = 100;
2366 // limits the acceleration if it is too extreme
2367 xAcc = fclamp(xAcc, -xAccLimit, xAccLimit);
2368 yAcc = fclamp(yAcc, -yAccLimit, yAccLimit);
2370 // applies the acceleration
2372 if (dead || stunned) yVel += 0.6; else yVel += yAcc;
2374 // nullifies the acceleration
2378 // applies the friction to the velocity, now that the velocity has been calculated
2382 auto oBall = getMyBall();
2383 // apply ball and chain
2385 int distsq = (ix-oBall.ix)*(ix-oBall.ix)+(iy-oBall.iy)*(iy-oBall.iy);
2386 if (distsq >= 24*24) {
2387 if (xVel > 0 && oBall.ix < ix && abs(oBall.ix-ix) > 24) xVel = 0;
2388 if (xVel < 0 && oBall.ix > ix && abs(oBall.ix-ix) > 24) xVel = 0;
2389 if (yVel > 0 && oBall.iy < iy && abs(oBall.iy-iy) > 24) {
2390 if (abs(oBall.ix-ix) < 1) {
2391 //teleportTo(destx:oBall.ix);
2393 prevFltX = oBall.prevFltX;
2395 } else if (oBall.ix < ix && !kRight) {
2396 if (xVel > 0) xVel *= -0.25;
2397 else if (xVel == 0) xVel -= 1;
2398 } else if (oBall.ix > ix && !kLeft) {
2399 if (xVel < 0) xVel *= -0.25;
2400 else if (xVel == 0) xVel += 1;
2405 if (yVel < 0 && oBall.iy > iy && abs(oBall.iy-iy) > 24) yVel = 0;
2409 // apply the limits since the velocity may be too extreme
2410 if (!dead && !stunned) xVel = fclamp(xVel, -xVelLimit, xVelLimit);
2411 yVel = fclamp(yVel, -yVelLimit, yVelLimit);
2413 // approximates the "active" variables
2414 if (fabs(xVel) < 0.0001) xVel = 0;
2415 if (fabs(yVel) < 0.0001) yVel = 0;
2416 if (fabs(xAcc) < 0.0001) xAcc = 0;
2417 if (fabs(yAcc) < 0.0001) yAcc = 0;
2419 bool wasInWall = !!isCollision();
2420 moveRel(xVel, yVel);
2422 // don't go out of level (if we're not in ending sequence)
2423 if (!level.inWinCutscene && !level.inIntroCutscene) {
2424 if (ix < 0) fltx = 0;
2425 else if (ix > level.tilesWidth*16-16) fltx = level.tilesWidth*16-16;
2426 if (iy < 0) flty = 0;
2428 if (!dead) hackBoulderCollision();
2430 if (!wasInWall && isCollision()) {
2431 writeln("** FUUUU (XXX)");
2432 if (isCollisionBottom(0) && !isCollisionBottom(-2)) {
2435 // we can stuck in the wall with this
2436 if (isCollisionLeft(0)) {
2437 writeln("** FUUUU (001: left)");
2438 while (isCollisionLeft(0) && !isCollisionRight(1)) shiftX(1);
2439 } else if (isCollisionRight(0)) {
2440 writeln("** FUUUU (001: right)");
2441 while (isCollisionRight(0) && !isCollisionLeft(1)) shiftX(-1);
2445 if (!dead) hackBoulderCollision();
2447 // move out of wall by 1 px, if possible
2448 if (!dead && isCollision()) {
2449 if (isCollisionBottom(0) && !isCollisionBottom(-1)) flty = iy-1;
2450 if (isCollisionTop(0) && !isCollisionTop(1)) flty = iy+1;
2451 if (isCollisionLeft(0) && !isCollisionLeft(1)) fltx = ix+1;
2452 if (isCollisionRight(0) && !isCollisionRight(-1)) fltx = ix-1;
2455 if (!dead && isCollision()) {
2456 //k8:HACK: try to duck
2457 bool wallDeath = true;
2458 if (platformCharacterIs(ON_GROUND)) {
2459 setCollisionBounds(-5, -6, 5, 8);
2460 auto ohbX = hitboxX, ohbY = hitboxY;
2461 auto ohbW = hitboxW, ohbH = hitboxH;
2462 wallDeath = !!isCollision();
2464 //setCollisionBounds(-8, -6, 8, 8);
2465 hitboxX = ohbX; hitboxY = ohbY;
2466 hitboxW = ohbW; hitboxH = ohbH;
2475 if (isCollision()) {
2476 if (isCollisionLeft(0) && !isCollisionRight(4)) fltx = ix+1;
2477 else if (isCollisionRight(0) && !isCollisionLeft(4)) fltx = ix-1;
2478 else if (isCollisionBottom(0) && !isCollisionTop(4)) flty = iy-1;
2479 else if (isCollisionTop(0) && !isCollisionBottom(4)) flty = iy+1;
2484 if (wallDeath && isCollision()) {
2485 if (!dead) level.addDeath('wall');
2488 writeln("PLAYER KILLED BY WALL");
2489 global.plife = 0; // oops
2495 //writeln("flty=", flty, "; iy=", iy);
2501 // figures out what the sprite index of the character should be
2504 // sets the previous state and the previously previous state
2505 statePrevPrev = statePrev;
2508 // calculates the imageSpeed based on the character's velocity
2509 if (status == RUNNING || status == DUCKING || status == LOOKING_UP) {
2510 if (status == RUNNING || status == LOOKING_UP) imageSpeed = fabs(xVel)*runAnimSpeed+0.1;
2513 if (status == CLIMBING) imageSpeed = sqrt(xVel*xVel+yVel*yVel)*climbAnimSpeed;
2515 if (xVel >= 4 || xVel <= -4) {
2517 if (platformCharacterIs(ON_GROUND)) {
2518 setCollisionBounds(-8, -6, 8, 8);
2520 setCollisionBounds(-5, -6, 5, 8);
2523 setCollisionBounds(-5, -6, 5, 8);
2526 if (whipping) imageSpeed = 1;
2528 if (status == DUCKTOHANG) {
2533 // limit the imageSpeed at 1 so the animation always looks good
2534 if (imageSpeed > 1) imageSpeed = 1;
2536 //if (kItemPressed) writeln("ITEM! dead=", dead, "; stunned=", stunned, "; active=", active);
2537 if (dead || stunned || !active) {
2539 } else if (/*inGame &&*/ kItemPressed && !whipping) {
2541 if (kUp) scrSwitchToStickyBombs(); else scrSwitchToNextItem();
2542 } else if (/*inGame &&*/ kRopePressed && global.rope > 0 && !whipping) {
2543 if (!kDown && colTop) {
2546 launchRope(kDown, doDrop:true);
2548 } else if (/*inGame &&*/ kBombPressed && global.bombs > 0 && !whipping) {
2549 if (holdItem isa ItemWeaponBow && bowArmed) {
2550 if (holdArrow != ARROW_BOMB) {
2551 //writeln("set bow arrows to bomb");
2552 holdArrow = ARROW_BOMB;
2554 //writeln("set bow arrows to normal");
2555 holdArrow = ARROW_NORM;
2564 if (!dead && !stunned && kUp && kAttackPressed) {
2565 auto octr = ItemOpenableContainer(level.isObjectInRect(ix, iy, width, height, delegate bool (MapObject o) {
2566 return (o isa ItemOpenableContainer);
2569 if (octr.openMe()) kAttackPressed = false;
2574 // use weapon / attack
2575 if (!dead && !stunned && kAttackPressed && !holdItem /*&& !pickedItem*/) {
2578 sndStopSound('sndBowPull');
2579 if (status != DUCKING && status != DUCKTOHANG && !whipping && !isExitingSprite()) {
2581 if (global.isTunnelMan) {
2582 if (platformCharacterIs(ON_GROUND) || platformCharacterIs(IN_AIR)) {
2583 setSprite('sTunnelAttackL');
2586 } else if (global.isDamsel) {
2587 setSprite('sDamselAttackL');
2590 setSprite('sAttackLeft');
2593 } else if (kDown && !pickedItem) {
2595 //HACK: always select dice to throw if there are two dices there
2596 MapObject diceToThrow = level.isObjectInRect(x-8, y, 9, 9, delegate bool (MapObject o) {
2597 if (o.spectral || !o.canPickUp) return false;
2598 if (ItemDice(o).isReadyToThrowForBet) return o.collidesWith(self);
2600 }, precise:false, castClass:ItemDice);
2605 obj = level.isObjectInRect(x-8, y, 9, 9, delegate bool (MapObject o) {
2606 if (o.spectral || !o.canPickUp) return false;
2607 if (!o.collidesWith(self)) return false;
2608 return o.onCanBePickedUp(self);
2610 if (o isa MapItem) return (o.active && o.canPickUp && !o.spectral);
2611 if (o isa MapEnemy) return (o.active && o.canPickUp && !o.spectral && (o.dead || o.status >= MapObject::STUNNED || o.meGoldMonkey));
2616 if (!obj && diceToThrow) obj = diceToThrow;
2618 // `canPickUp` is checked in callback
2619 if (/*obj.canPickUp &&*/ true /*k8: do we really need this? !level.isSolidAtPoint(obj.ix+2, obj.iy)*/) {
2620 //pickupItemType = holdItem.type;
2621 //!if (isAshShotgun(holdItem)) pickupItemType = "Boomstick";
2622 //!if (isGoldMonkey(obj) and obj.status < 98) obj.status = 0; // do not play walk animation while held
2624 if (!obj.onTryPickup(self)) {
2625 if (obj.isInstanceAlive) scrPickupItem(obj);
2629 if (holdItem.type == "Bow" and holdItem.new) {
2630 holdItem.new = false;
2632 if (global.arrows > 99) global.arrows = 99;
2638 } else if (!dead && !stunned) {
2639 if (holdItem isa ItemWeaponBow) {
2640 //writeln("BOW! kAttack=", kAttack, "; kAttackPressed=", kAttackPressed, "; bowArmed=", bowArmed, "; bowStrength=", bowStrength, "; holdArrow=", holdArrow);
2641 if (kAttackPressed) {
2642 if (scrPlayerIsDucking()) {
2643 scrUsePutItemOnGround();
2644 } else if (!bowArmed) {
2646 ItemWeaponBow(holdItem).armBow(self);
2650 if (bowArmed && bowStrength < 12) {
2652 //writeln("arming: ", bowStrength);
2654 sndStopSound('sndBowPull');
2657 //writeln(" xxBOW!");
2661 if (!holdArrow) holdArrow = ARROW_NORM;
2663 if (kAttackPressed && holdItem) scrUseItem();
2668 if (!dead && !stunned && kPayPressed) {
2669 // find nearest shopkeeper
2670 auto sc = MonsterShopkeeper(level.findNearestObject(ix, iy, delegate bool (MapObject o) {
2671 auto sc = MonsterShopkeeper(o);
2672 if (!sc) return false;
2673 //if (needCraps && sc.stype != 'Craps') return false;
2674 if (sc.dead || sc.angered || sc.outlaw) return false;
2675 return sc.canSellItem(self, holdItem);
2677 if (level.isInShop(ix/16, iy/16)) {
2678 // if no shopkeepers found, just use it
2681 holdItem.forSale = false;
2682 holdItem.onTryPickup(self);
2684 } else if (global.thiefLevel == 0 && !global.murderer) {
2685 // only law-abiding players can buy/sell items or play games
2686 if (holdItem) writeln("shop item interaction: ", holdItem.objName, "; cost=", holdItem.cost);
2687 if (sc.doSellItem(self, holdItem)) {
2690 holdItem.forSale = false;
2691 holdItem.onTryPickup(self);
2694 if (holdItem && !holdItem.isInstanceAlive) {
2696 scrSwitchToPocketItem(forceIfEmpty:false); // just in case
2700 // use pickup, if any
2701 if (holdItem isa ItemPickup) {
2702 // make nearest shopkeeper angry (an unlikely situation, but still...)
2703 if (sc && holdItem.forSale) level.scrShopkeeperAnger(GameLevel::SCAnger.ItemStolen);
2704 holdItem.forSale = false;
2705 holdItem.onTryPickup(self);
2707 pickupsAround.clear();
2708 level.isObjectInRect(x0, y0, width, height, delegate bool (MapObject o) {
2709 auto pk = ItemPickup(o);
2710 if (pk && pk.collidesWith(self)) {
2712 foreach (auto opk; pickupsAround) if (opk == pk) { found = true; break; }
2713 if (!found) pickupsAround[$] = pk;
2717 // now try to use all pickups
2718 foreach (ItemPickup pk; pickupsAround) {
2719 if (pk.isInstanceAlive) {
2720 if (sc && pk.forSale) level.scrShopkeeperAnger(GameLevel::SCAnger.ItemStolen);
2722 pk.onTryPickup(self);
2725 pickupsAround.clear();
2731 transient array!ItemPickup pickupsAround;
2734 // ////////////////////////////////////////////////////////////////////////// //
2735 override bool initialize () {
2736 if (!::initialize()) return false;
2738 powerups.length = 0;
2739 powerups[$] = SpawnObject(PPParachute);
2740 powerups[$] = SpawnObject(PPCape);
2742 foreach (PlayerPowerup pp; powerups) pp.owner = self;
2744 if (global.isDamsel) {
2746 desc2 = "An athletic, unfittingly-dressed woman with extremely awkward running form.";
2747 setSprite('sDamselLeft');
2748 } else if (global.isTunnelMan) {
2749 desc = "Tunnel Man";
2750 desc2 = "A miner from the desert. His tools are a cut above the rest.";
2751 setSprite('sTunnelLeft');
2754 desc2 = "A strange little man who spends his time exploring caverns. He wants to be just like Indiana Jones when he grows up.";
2755 setSprite('sStandLeft');
2763 switch (global.config.scumClimbSpeed) {
2766 climbAnimSpeed = 0.4;
2771 climbAnimSpeed = 0.45;
2776 climbAnimSpeed = 0.5;
2781 climbAnimSpeed = 0.5;
2785 climbAcc = 0.6; // how fast the character will climb
2786 climbAnimSpeed = 0.4; // relates to how fast the climbing animation should go
2791 // sets the collision bounds to fit the default sprites (you can edit the arguments of the script)
2792 setCollisionBounds(-5, -5, 5, 8); // setCollisionBounds(-5, -8, 5, 8);
2795 statePrevPrev = statePrev;
2796 gravityIntensity = grav; // this variable describes the current force due to gravity (this variable is altered for variable jumping)
2797 jumpTime = jumpTimeTotal; // current time of the jump (0=start of jump, jumpTimeTotal=end of jump)
2803 // ////////////////////////////////////////////////////////////////////////// //
2804 override void onAnimationLooped () {
2805 auto spr = getSprite();
2806 if (spr.Name == 'sAttackLeft' || spr.Name == 'sDamselAttackL' || spr.Name == 'sTunnelAttackL') {
2808 if (holdItem) holdItem.visible = true;
2809 } else if (spr.Name == 'sDuckToHangL' || spr.Name == 'sDamselDtHL' || spr.Name == 'sTunnelDtHL') {
2819 if (dir == Dir.Left) {
2821 obj = level.isAnyLadderAtPoint(x-8, y);
2824 obj = level.isAnyLadderAtPoint(x+8, y);
2829 } else if (dir == Dir.Left) {
2839 } else if (isExitingSprite()) {
2841 //!global.cleanSolids = true;
2846 void activatePlayerWeapon () {
2848 if (holdItem isa PlayerWeapon) {
2849 auto wep = holdItem;
2851 wep.instanceRemove();
2855 if (global.config.unarmed) return;
2857 if (holdItem isa PlayerWeapon) {
2860 writeln("000: !!!!!!!");
2861 if (holdItem) writeln(" H(", GetClassName(holdItem.Class), "): '", holdItem.objType, "'");
2862 if (pickedItem) writeln(" P(", GetClassName(pickedItem.Class), "): '", pickedItem.objType, "'");
2864 auto wep = holdItem;
2866 wep.instanceRemove();
2871 if (holdItem) return;
2873 auto spr = getSprite();
2874 if (spr.Name != 'sAttackLeft' && spr.Name != 'sDamselAttackL' && spr.Name != 'sTunnelAttackL') return;
2876 if (imageFrame > 4) {
2877 //bool hitEnemy = (PlayerWeapon(holdItem) ? PlayerWeapon(holdItem).hitEnemy : false);
2878 if (global.isTunnelMan || pickedItem isa ItemWeaponMattock) {
2879 holdItem = level.MakeMapObject(ix+(dir == Dir.Left ? -16 : 16), iy, 'oMattockHit');
2880 if (imageFrame < 7) playSound('sndWhip');
2881 } else if (pickedItem isa ItemWeaponMachete) {
2882 holdItem = level.MakeMapObject(ix+(dir == Dir.Left ? -16 : 16), iy, 'oSlash');
2883 playSound('sndWhip');
2885 holdItem = level.MakeMapObject(ix+(dir == Dir.Left ? -16 : 16), iy, 'oWhip');
2886 playSound('sndWhip');
2888 /+ not needed anymore
2890 holdItem.active = true;
2891 //if (PlayerWeapon(holdItem)) PlayerWeapon(holdItem).hitEnemy = hitEnemy;
2894 } else if (imageFrame < 2) {
2895 if (global.isTunnelMan || pickedItem isa ItemWeaponMattock) {
2896 holdItem = level.MakeMapObject(ix+(dir == Dir.Left ? -16 : 16), iy, 'oMattockPre');
2897 } else if (pickedItem isa ItemWeaponMachete) {
2898 holdItem = level.MakeMapObject(ix+(dir == Dir.Left ? -16 : 16), iy, 'oMachetePre');
2900 holdItem = level.MakeMapObject(ix+(dir == Dir.Left ? 16 : -16), iy, 'oWhipPre');
2902 /+ not needed anymore
2903 if (holdItem) holdItem.active = true;
2909 if (holdItem.type == "Machete") {
2910 obj = instance_create(x-16, y, oSlash);
2911 obj.sprite_index = sSlashLeft;
2912 playSound(global.sndWhip);
2913 } else if (holdItem.type == "Mattock") {
2914 obj = instance_create(x-16, y, oMattockHit);
2915 obj.sprite_index = sMattockHitL;
2916 if (image_index < 7) playSound(global.sndWhip);
2919 if (global.isTunnelMan) {
2920 obj = instance_create(x-16, y, oMattockHit);
2921 obj.sprite_index = sMattockHitL;
2922 if (image_index < 7) playSound(global.sndWhip);
2924 obj = instance_create(x-16, y, oWhip);
2925 if (global.scumWhipUpgrade == 1) obj.sprite_index = sWhipLongLeft; else obj.sprite_index = sWhipLeft;
2926 playSound(global.sndWhip);
2933 //bool webHit = false;
2935 bool doBreakWebsCB (MapObject o) {
2936 if (o isa ItemWeb) {
2939 if (fabs(xVel) > 1) {
2941 if (!o.dying) ItemWeb(o).life -= 5;
2945 if (fabs(yVel) > 1) {
2947 if (!o.dying) ItemWeb(o).life -= 5;
2957 void initiateExitSequence () {
2958 writeln("exit sequence initiated...");
2959 if (global.isDamsel) setSprite('sDamselExit');
2960 else if (global.isTunnelMan) setSprite('sTunnelExit');
2961 else setSprite('sPExit');
2968 /*k8: the following is done in `GameLevel`
2969 if (global.thiefLevel > 0) global.thiefLevel -= 1;
2970 //orig dbg:if (global.currLevel == 1) global.currLevel += firstLevelSkip; else global.currLevel += levelSkip;
2971 global.currLevel += 1;
2973 playSound('sndSteps');
2977 void processLevelExit () {
2978 if (dead || stunned || whipping || level.playerExited) return;
2979 if (!platformCharacterIs(ON_GROUND)) return;
2980 if (isExitingSprite()) return; // just in case
2982 auto hld = holdItem;
2983 if (hld isa PlayerWeapon) return; // oops
2985 //if (!kExitPressed && !hld) return false;
2987 auto door = level.checkTileAtPoint(ix, iy, &level.cbCollisionExitTile);
2988 if (!door || !door.visible) return; // note that `invisible` doors still works
2990 // sell idol, or free damsel
2991 if (hld isa ItemGoldIdol) {
2992 //!if (isRealLevel()) global.idolsConverted += 1;
2993 //not thisglobal.money += hld.value*(global.levelType+1);
2994 ItemGoldIdol(hld).registerConverted();
2995 addScore(hld.value*(global.levelType+1));
2996 //!if (hld.sprite_index == sCrystalSkull) global.skulls += 1; else global.idols += 1;
2997 playSound('sndCoin');
2998 level.MakeMapObject(ix, iy-8, 'oBigCollect');
3000 hld.instanceRemove();
3001 //!with (hld) instance_destroy();
3003 //!pickupItemType = "";
3004 } else if (hld isa MonsterDamsel) {
3006 MonsterDamsel(hld).exitAtDoor(door);
3009 if (!kExitPressed) {
3010 if (!door.invisible) {
3011 string msg = door.getExitMessage();
3012 if (msg.length == 0) {
3013 level.osdMessage(va("PRESS %s TO ENTER.", (global.config.useDoorWithButton ? "$PAY" : "$UP")), -666);
3014 } else if (msg[$-1] != '\n') {
3015 level.osdMessage(va("%s\nPRESS %s TO ENTER.", msg, (global.config.useDoorWithButton ? "$PAY" : "$UP")), -666);
3017 level.osdMessage(msg, -666);
3028 if (isHoldingArmedBomb()) scrUseThrowItem();
3030 if (isHoldingBombOrRope()) scrSwitchToPocketItem(forceIfEmpty:true);
3032 wasHoldingBall = false;
3035 if (hld isa ItemGoldIdol) {
3036 //!if (isRealLevel()) global.idolsConverted += 1;
3037 //not thisglobal.money += hld.value*(global.levelType+1);
3038 ItemGoldIdol(hld).registerConverted();
3039 addScore(hld.value*(global.levelType+1));
3040 //!if (hld.sprite_index == sCrystalSkull) global.skulls += 1; else global.idols += 1;
3041 playSound('sndCoin');
3042 level.MakeMapObject(ix, iy-8, 'oBigCollect');
3044 hld.instanceRemove();
3045 //!with (hld) instance_destroy();
3047 //!pickupItemType = "";
3048 } else if (hld isa MonsterDamsel) {
3050 MonsterDamsel(hld).exitAtDoor(door);
3051 } else if (hld.heavy || hld isa MapEnemy) {
3052 // drop heavy items, characters and enemies (but not ball)
3053 if (hld !isa ItemBall) scrUseThrowItem();
3054 } else if (hld isa ItemBall) {
3056 // other items are carried thru
3057 if (hld.cannotBeCarriedOnNextLevel) {
3059 holdItem = none; // just in case
3061 scrHideItemToPocket();
3064 global.pickupItem = hld.type;
3065 if (isAshShotgun(hld)) global.pickupItem = "Boomstick";
3067 breakPieces = false;
3071 //scrHideItemToPocket();
3077 //door = instance_place(x, y, oExit); // done above
3078 door.snapToExit(self);
3080 initiateExitSequence();
3082 level.playerExitDoor = door;
3086 override bool onFellInWater (MapTile water) {
3087 level.MakeMapObject(ix, iy-8, 'oSplash');
3089 playSound('sndSplash');
3090 myGrav = 0.2; //k8:???
3095 override bool onOutOfWater () {
3102 // ////////////////////////////////////////////////////////////////////////// //
3103 override void thinkFrame () {
3104 // remove whip, etc. when dead
3105 if (dead && holdItem isa PlayerWeapon) {
3108 pw.instanceRemove();
3109 scrSwitchToPocketItem(forceIfEmpty:false);
3112 setPowerupState('Cape', global.hasCape);
3114 foreach (PlayerPowerup pp; powerups) if (pp.active) pp.onPreThink();
3118 if (redToggle) redColor -= 5;
3119 else if (redColor < 20) redColor += 5;
3120 else redToggle = true;
3125 if (dead) justdied = false;
3128 if (invincible > 0) --invincible;
3134 blinkHidden = !blinkHidden;
3137 blinkHidden = false;
3140 auto spr = getSprite();
3143 cameraBlockX = max(0, cameraBlockX-1);
3144 cameraBlockY = max(0, cameraBlockY-1);
3147 if (spr.Name == 'sWhoaLeft' || spr.Name == 'sDamselWhoaL' || spr.Name == 'sTunnelWhoaL') {
3148 if (whoaTimer > 0) {
3150 } else if (holdItem && onLoosingHeldItem(LostCause.Whoa)) {
3153 if (!hi.onLostAsHeldItem(self, LostCause.Whoa)) {
3157 scrSwitchToPocketItem(forceIfEmpty:true);
3161 whoaTimer = whoaTimerMax;
3165 if (firing > 0) firing -= 1;
3168 auto wtile = level.isWaterAtPoint(x, y/*, oWaterSwim, -1, -1*/);
3171 if (onFellInWater(wtile) || !isInstanceAlive) return;
3175 if (onOutOfWater() || !isInstanceAlive) return;
3181 if (global.randOther(1, 5) == 1) level.MakeMapObject(x-8+global.randOther(4, 12), y-8+global.randOther(4, 12), 'oBurn');
3186 if (!dead && level.isLavaAtPoint(x, y+6/*, oLava, 0, 0*/)) {
3187 //!if (isRealLevel()) global.miscDeaths[11] += 1;
3188 level.addDeath('lava');
3189 playSound('sndFlame');
3203 if (global.hasJetpack && platformCharacterIs(ON_GROUND)) {
3207 // fall off bottom of screen
3208 if (!level.inWinCutscene && !level.inIntroCutscene) {
3209 if (!dead && y > level.tilesHeight*16+16) {
3210 //!if (isRealLevel()) global.miscDeaths[10] += 1;
3211 level.addDeath('void');
3212 global.plife = -90; // spill blood
3218 scrDropItem(LostCause.Falloff);
3219 playSound('sndThud'); //???
3220 playSound('sndDie'); //???
3223 if (dead && y > level.tilesHeight*16+16) {
3231 if (/*active*/true) {
3232 if (spr.Name == 'sStunL' || spr.Name == 'sDamselStunL' || spr.Name == 'sTunnelStunL') {
3233 if (stunTimer > 0) {
3237 if (stunTimer < 1) {
3239 canDropStuff = true;
3243 if (!level.inWinCutscene) {
3244 if (isParachuteActive() || isCapeActiveAndOpen()) fallTimer = 0;
3247 // changed to yVel > 1 from yVel > 0
3248 if (yVel > 1 && status != CLIMBING) {
3250 if (fallTimer > 16) wallHurt = 0; // no sense in them taking extra damage from being thrown here
3251 int paraOpenHeight = (global.config.scumSpringShoesReduceFallDamage && (global.hasSpringShoes || global.hasJordans) ? 22 : 14);
3252 //paraOpenHeight = 4;
3253 if (global.hasParachute && !stunned && fallTimer > paraOpenHeight) {
3254 //if (not collision_point(x, y+32, oSolid, 0, 0)) // was commented in the original code
3255 //!*if (not collision_line(x, y+16, x, y+32, oSolid, 0, 0))
3256 if (!level.checkTilesInRect(x, y+16, 1, 17, &level.cbCollisionAnySolid)) {
3258 //!instance_create(x-8, y-16, oParachute);
3260 global.hasParachute = false;
3261 activatePowerup('Parachute');
3262 //writeln("parachute state: ", isParachuteActive());
3265 } else if (fallTimer > 16 && platformCharacterIs(ON_GROUND) &&
3266 !level.checkTilesInRect(x-8, y-8, 17, 17, &level.cbCollisionSpringTrap) /* not onto springtrap */)
3268 // long drop -- player has just landed
3269 bool reducedDamage = (global.config.scumSpringShoesReduceFallDamage && (global.hasSpringShoes || global.hasJordans));
3270 if (reducedDamage && fallTimer <= 24) {
3271 // land without taking damage
3275 if (fallTimer > (reducedDamage ? 72 : 48)) global.plife -= 10*global.config.scumFallDamage;
3276 else if (fallTimer > (reducedDamage ? 48 : 32)) global.plife -= 2*global.config.scumFallDamage;
3277 else global.plife -= 1*global.config.scumFallDamage;
3278 if (global.plife < 1) {
3279 if (!dead) level.addDeath('fall');
3283 if (global.config.scumFallDamage > 0) stunTimer += 60;
3286 auto obj = level.MakeMapObject(x-4, y+6, 'oPoof');
3287 if (obj) obj.xVel = -0.4;
3288 obj = level.MakeMapObject(x+4, y+6, 'oPoof');
3289 if (obj) obj.xVel = 0.4;
3290 playSound('sndThud');
3292 } else if (yVel <= 0) {
3294 if (isParachuteActive()) {
3295 deactivatePowerup('Parachute');
3296 level.MakeMapObject(ix-8, iy-16-8, 'oParaUsed');
3300 // if (stunned) fallTimer = 0; // was commented in the original code
3302 if (swimming && !level.isLavaAtPoint(x, y/*, oLava, 0, 0*/)) {
3304 if (bubbleTimer > 0) {
3307 if (level.isWaterAtPoint(x, (y&~0x0f)-8)) level.MakeMapObject(x, y-4, 'oBubble');
3308 bubbleTimer = bubbleTimerMax;
3311 bubbleTimer = bubbleTimerMax;
3314 //TODO: k8: move spear checking to spear handler
3315 if (!isExitingSprite()) {
3316 auto spear = MapObjectSpearsBase(level.isObjectInRect(ix-6, iy-6, 13, 14, delegate bool (MapObject o) {
3317 auto tt = MapObjectSpearsBase(o);
3318 if (!tt) return false;
3319 return tt.isHitFrame;
3324 global.plife -= global.config.spearDmg; // 4
3325 if (!dead && global.plife <= 0 /*and isRealLevel()*/) level.addDeath('spear');
3326 xVel = global.randOther(4, 6)*(spear.isLeft ? -1 : 1);
3335 if (status != DUCKTOHANG && !stunned && !dead && !isExitingSprite()) {
3337 characterStepEvent();
3339 if (status != DUCKING && status != DUCKTOHANG) status = STANDING;
3340 checkControlKeys(getSprite());
3344 // if (dead or stunned)
3345 if (dead || stunned) {
3347 if (holdItem isa ItemWeaponBow && bowArmed) scrFireBow();
3348 scrDropItem(dead ? LostCause.Dead : LostCause.Stunned, xVel, -3);
3351 yVel += (bounced ? 1.0 : 0.6);
3353 if (isCollisionTop(1) && yVel < 0) yVel = -yVel*0.8;
3354 if (isCollisionLeft(1) || isCollisionRight(1)) xVel = -xVel*0.5;
3356 bool collisionbottomcheck = !!isCollisionBottom(1);
3357 if (collisionbottomcheck || isCollisionBottom(1, &level.cbCollisionPlatform)) {
3359 if (collisionbottomcheck) {
3360 if (yVel > 2.5) yVel = -yVel*0.5; else yVel = 0;
3362 // after falling onto a platform don't take extra damage after recovering from stunning
3365 /* was commented in the original code
3366 if (isCollisionBottom(1)) {
3367 if (yVel > 2.5) yVel = -yVel*0.5; else yVel = 0;
3374 if (fabs(xVel) < 0.1) xVel = 0;
3375 else if (fabs(xVel) != 0 && level.isIceAtPoint(x, y+16)) xVel *= 0.8;
3376 else if (fabs(xVel) != 0) xVel *= 0.3;
3382 //level.forEachObjectInRect(ix, iy, width, height, &doBreakWebsCB);
3384 // apply the limits since the velocity may be too extreme
3386 xVel = fclamp(xVel, -xVelLimit, xVelLimit);
3387 yVel = fclamp(yVel, -yVelLimit, yVelLimit);
3389 moveRel(xVel, yVel);
3393 // fix sprites, spawn blood from spikes
3394 if (isParachuteActive()) {
3395 deactivatePowerup('Parachute');
3396 level.MakeMapObject(ix-8, iy-16-8, 'oParaUsed');
3401 //!with (oWhip) instance_destroy();
3404 if (global.isDamsel) {
3406 if (dead) setSprite('sDamselDieL');
3407 else if (stunned) setSprite('sDamselStunL');
3408 } else if (bounced) {
3409 if (yVel < 0) setSprite('sDamselBounceL'); else setSprite('sDamselFallL');
3411 if (xVel < 0) setSprite('sDamselDieLL'); else setSprite('sDamselDieLR');
3413 } else if (global.isTunnelMan) {
3415 if (dead) setSprite('sTunnelDieL');
3416 else if (stunned) setSprite('sTunnelStunL');
3417 } else if (bounced) {
3418 if (yVel < 0) setSprite('sTunnelLBounce'); else setSprite('sTunnelFallL');
3420 if (xVel < 0) setSprite('sTunnelDieLL'); else setSprite('sTunnelDieLR');
3424 if (dead) setSprite('sDieL');
3425 else if (stunned) setSprite('sStunL');
3426 } else if (bounced) {
3427 if (yVel < 0) setSprite('sDieLBounce'); else setSprite('sDieLFall');
3429 if (xVel < 0) setSprite('sDieLL'); else setSprite('sDieLR');
3436 auto colobj = isCollisionRight(1);
3437 if (!colobj) colobj = isCollisionLeft(1);
3438 if (!colobj) colobj = isCollisionBottom(1);
3441 scrCreateBlood(colobj.x0, colobj.y0, 3);
3443 if (!dead && global.plife <= 0 /*&& isRealLevel()*/) {
3445 writeln("thrown to death by '", thrownBy, "'");
3446 level.addDeath(thrownBy);
3450 if (wallHurt <= 0) thrownBy = '';
3451 playSound('sndHurt'); //???
3455 colobj = isCollisionBottom(1);
3456 if (colobj && !bounced) {
3458 scrCreateBlood(colobj.x0, colobj.y0, 2);
3461 if (!dead && global.plife <= 0 /*and isRealLevel()*/) {
3463 writeln("thrown to death by '", thrownBy, "'");
3464 level.addDeath(thrownBy);
3468 if (wallHurt <= 0) thrownBy = '';
3473 bool kPay = level.isKeyDown(GameConfig::Key.Pay);
3475 // gnounc's quick look
3476 if (!kRight && !kLeft && (platformCharacterIs(ON_GROUND) || status == HANGING)) {
3477 if (kDown) { if (viewCount <= 6) viewCount += 3; else viewOffset += 6; }
3478 else if (kUp) { if (viewCount <= 6) viewCount += 3; else viewOffset -= 6; }
3484 // default look up/down with delay if pay button not held
3485 if (!kRight && !kLeft && (platformCharacterIs(ON_GROUND) || status == HANGING)) {
3486 if (kDown) { if (viewCount <= 30) viewCount += 1; else viewOffset += 4; }
3487 else if (kUp) { if (viewCount <= 30) viewCount += 1; else viewOffset -= 4; }
3494 if (viewCount == 0 && viewOffset) viewOffset = (viewOffset < 0 ? min(0, viewOffset+8) : max(0, viewOffset-8));
3495 viewOffset = clamp(viewOffset, -16*6, 16*6);
3497 if (!dead) activatePlayerWeapon();
3499 if (!dead) processLevelExit();
3502 if (global.plife < -99 && visible && justdied) spillBlood();
3504 if (global.plife < 1) {
3508 // spikes, and other shit
3509 if (global.plife >= -99 && visible && !isExitingSprite()) {
3510 auto colSpikes = level.checkTilesInRect(x-4, y-4, 9, 13, &level.cbCollisionSpikes);
3512 if (colSpikes && dead) {
3514 if (!level.isSolidAtPoint(x, y+9)) { shiftY(0.02); y = iy; } //0.05;
3515 //else myGrav = 0.6;
3520 if (colSpikes && yVel > 0 && (fallTimer > 3 || stunned)) { // originally fallTimer > 4
3522 // spikes will always instant-kill in Moon room
3523 /*if (isRoom("rMoon")) global.plife -= 99; else*/ global.plife -= global.config.scumSpikeDamage;
3524 if (/*isRealLevel() &&*/ global.plife <= 0) level.addDeath('spike');
3525 if (global.plife > 0) playSound('sndHurt');
3531 colSpikes.makeBloody();
3533 //else if (not dead) myGrav = 0.6;
3538 if (visible && (status >= STUNNED || stunned || dead || status == DUCKING)) {
3540 checkAndPerformSacrifice(out onAltar);
3541 // block looking down if we're trying to sacrifire ourselves
3542 if (onAltar) viewCount = max(0, viewCount-1);
3544 sacCount = default.sacCount;
3548 if (dead && global.hasAnkh) {
3549 writeln("*** ACTIVATED ANKH");
3550 global.hasAnkh = false;
3552 int newLife = (global.isTunnelMan ? global.config.scumTMLife : global.config.scumStartLife);
3553 global.plife = max(global.plife, newLife);
3554 level.osdMessage("THE ANKH SHATTERS!\nYOU HAVE BEEN REVIVED!", 4);
3556 auto moai = level.forEachTile(delegate bool (MapTile t) { return (t.objType == 'oMoai'); });
3558 level.forEachTile(delegate bool (MapTile t) {
3559 if (t.objType == 'oMoaiInside') {
3560 teleportTo(t.ix+8, t.iy+8);
3565 //teleportTo(moai.ix+16+8, moai.iy+16+8);
3567 if (level.allEnters.length) {
3568 teleportTo(level.allEnters[0].ix+8, level.allEnters[0].iy-8);
3571 level.centerViewAtPlayer();
3572 auto ball = getMyBall();
3573 if (ball) ball.teleportToPrisoner();
3586 //alarm[8] = 60; // this starts music; but we don't need it, 'cause we won't stop the music on player death
3587 playSound('sndTeleport');
3591 if (dead) level.stats.gameOver();
3594 if (status == DUCKTOHANG) {
3596 if (spr.Name != 'sDuckToHangL' && spr.Name != 'sDamselDtHL' && spr.Name != 'sTunnelDtHL') status = STANDING;
3599 foreach (PlayerPowerup pp; powerups) if (pp.active) pp.onPostThink();
3601 if (jetpackFlaresTime > 0) {
3602 if (--jetpackFlaresTime == 0) {
3603 auto obj = level.MakeMapObject(ix+global.randOther(0, 3)-global.randOther(0, 3), iy+global.randOther(0, 3)-global.randOther(0, 3), 'oFlareSpark');
3605 obj.yVel = global.randOther(1, 3);
3606 obj.xVel = global.randOther(0, 3)-global.randOther(0, 3);
3608 playSound('sndJetpack');
3614 // ////////////////////////////////////////////////////////////////////////// //
3615 void drawPrePrePowerupWithOfs (int xpos, int ypos, int scale, float currFrameDelta) {
3616 // so ducking player will have it's cape correctly rendered
3617 foreach (PlayerPowerup pp; powerups) {
3618 if (pp.active) pp.prePreDrawWithOfs(xpos, ypos, scale, currFrameDelta);
3623 override void drawWithOfs (int xpos, int ypos, int scale, float currFrameDelta) {
3624 //if (heldBy) return; // owner will take care of this
3625 if (blinkHidden) return;
3627 bool renderJetpackBack = false;
3628 if (global.hasJetpack) {
3630 if ((status == CLIMBING || isExitingSprite()) && !whipping) {
3632 renderJetpackBack = true;
3635 getInterpCoords(currFrameDelta, scale, out xi, out yi);
3638 if (dir == Dir.Right) {
3639 spr = level.sprStore['sJetpackRight'];
3642 spr = level.sprStore['sJetpackLeft'];
3646 auto spf = spr.frames[0];
3647 if (spf && spf.width > 0 && spf.height > 0) spf.tex.blitAt(xi-xpos-spf.xofs*scale, yi-ypos-spf.yofs*scale, scale);
3652 bool ducking = (status == DUCKING);
3653 foreach (PlayerPowerup pp; powerups) {
3654 if (pp.active) pp.preDrawWithOfs(xpos, ypos, scale, currFrameDelta);
3657 auto oldColor = Video.color;
3658 if (redColor > 0) Video.color = clamp(200+redColor, 0, 255)<<16;
3659 ::drawWithOfs(xpos, ypos, scale, currFrameDelta);
3660 Video.color = oldColor;
3662 if (renderJetpackBack) {
3664 getInterpCoords(currFrameDelta, scale, out xi, out yi);
3665 SpriteImage spr = level.sprStore['sJetpackBack'];
3667 auto spf = spr.frames[0];
3668 if (spf && spf.width > 0 && spf.height > 0) spf.tex.blitAt(xi-xpos-spf.xofs*scale, yi-ypos-spf.yofs*scale, scale);
3672 foreach (PlayerPowerup pp; powerups) if (pp.active) pp.postDrawWithOfs(xpos, ypos, scale, currFrameDelta);
3676 void lastDrawWithOfs (int xpos, int ypos, int scale, float currFrameDelta) {
3677 foreach (PlayerPowerup pp; powerups) {
3678 if (pp.active) pp.lastDrawWithOfs(xpos, ypos, scale, currFrameDelta);
3685 objType = 'oPlayer';
3688 desc2 = "A strange little man who spends his time exploring caverns. He wants to be just like Indiana Jones when he grows up.";
3690 negateMirrorXOfs = true;
3692 status = FALLING; // the character state, must be one of the following: STANDING, RUNNING, DUCKING, LOOKING_UP, CLIMBING, JUMPING, or FALLING
3702 //thrownBy = ""; // "Yeti", "Hawkman", or "Shopkeeper" for stat tracking deaths by being thrown
3705 //whoaTimerMax = 30;
3706 distToNearestLightSource = 999;
3716 frictionFactor = 0.3;
3718 xVelLimit = 16; // limits the xVel: default 15
3719 yVelLimit = 10; // limits the yVel
3720 xAccLimit = 9; // limits the xAcc
3721 yAccLimit = 6; // limits the yAcc
3722 runAcc = 3; // the running acceleration
3729 //lightRadius = 96; //???