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;
21 const int hangCountMax = 3;
23 array!PlayerPowerup powerups; // created in initializer
25 int cameraBlockX; // >0: don't center camera
26 int cameraBlockY; // >0: don't center camera
32 const int bubbleTimerMax = 20;
33 int jetpackFlaresTime;
44 bool justdied = true; // so dead body won't spill blood endlessly
52 const int firingMax = 20;
53 const int firingPistolMax = 20;
54 const int firingShotgunMax = 40;
58 int hotkeyPressed = -1;
68 //!!!global.poisonStrength = max(global.poisonStrength-0.5, 1);
70 //string holdItemType = "";
71 //string pickupItemType = "";
73 // this is what we had picked up
74 // picked item will be stored here by bomb/rope/item switcher
77 bool canDropStuff = true;
89 int bombArrowCounter = 80;
98 // the keys that the platform character will use (don't edit)
110 bool jumpButtonReleased; // whether the jump button was released. (Stops the user from pressing the jump button many times to get extra jumps)
113 bool kAttackReleased;
115 const float gravNorm = 1;
116 //float grav = 1; // the gravity
118 const float initialJumpAcc = -2; // relates to how high the character will jump
119 const int jumpTimeTotal = 10; // how long the user must hold the jump button to get the maximum jump height
121 float climbAcc = 0.6; // how fast the character will climb
122 float climbAnimSpeed = 0.4; // relates to how fast the climbing animation should go
123 int climbSndSpeed = 8;
128 const float departLadderXVel = 4; // how fast character should be moving horizontally when he leaves the ladder
129 const float departLadderYVel = -4; // how fast the character should be moving vertically when he leaves the ladder
131 const float frictionRunningX = 0.6; // friction obtained while running
132 const float frictionRunningFastX = 0.98; // friction obtained while holding the shift button for some time while running
133 const float frictionClimbingX = 0.6; // friction obtained while climbing
134 const float frictionClimbingY = 0.6; // friction obtained while climbing
135 const float frictionDuckingX = 0.8; // friction obtained while ducking
136 const float frictionFlyingX = 0.99; // friction obtained while "flying"
138 const float runAnimSpeed = 0.1; // relates to the how fast the running animation should go
140 // hidden variables (don't edit)
141 protected int statePrev;
142 protected int statePrevPrev;
143 protected float gravityIntensity = grav; // this variable describes the current force due to gravity (this variable is altered for variable jumping)
144 protected float jumpTime = jumpTimeTotal; // current time of the jump (0=start of jump, jumpTimeTotal=end of jump)
145 protected int ladderTimer; // relates to whether the character can climb a ladder
146 protected int kLeftPushedSteps;
147 protected int kRightPushedSteps;
149 transient protected bool skipCutscenePressed;
154 //PlayerWeapon actWeapon; // active weapon object
195 // ////////////////////////////////////////////////////////////////////////// //
200 final ItemBall getMyBall () {
201 ItemBall res = none; //level.findNearestBall(x, y);
202 foreach (MapObject o; level.ballObjects) {
203 auto ball = ItemBall(o);
204 if (ball && ball.isInstanceAlive && ball.prisoner == self) {
213 void spawnBallAndChain () {
214 mustBeChained = true;
215 auto ball = getMyBall();
217 ball = ItemBall(level.MakeMapObject(ix, iy, 'oBall'));
218 if (ball) ball.attachTo(self);
221 if (wasHoldingBall) {
223 pickedItem.instanceRemove();
226 if (holdItem && holdItem != ball) {
227 holdItem.instanceRemove();
233 mustBeChained = false;
235 wasHoldingBall = false;
239 void removeBallAndChain (optional bool temp) {
240 auto ball = getMyBall();
242 wasHoldingBall = (holdItem == ball);
243 mustBeChained = true;
245 ball.instanceRemove();
247 if (specified_temp && temp) return;
248 wasHoldingBall = false;
249 mustBeChained = false;
253 // ////////////////////////////////////////////////////////////////////////// //
254 final PlayerPowerup findPowerup (name id) {
255 foreach (PlayerPowerup pp; powerups) if (pp.id == id) return pp;
260 final bool setPowerupState (name id, bool active) {
261 auto pp = findPowerup(id);
262 if (!pp) return false;
263 return (active ? pp.onActivate() : pp.onDeactivate());
267 final bool togglePowerupState (name id) {
268 auto pp = findPowerup(id);
269 if (!pp) return false;
270 return (pp.active ? pp.onDeactivate() : pp.onActivate());
274 final bool activatePowerup (name id) { return setPowerupState(id, true); }
275 final bool deactivatePowerup (name id) { return setPowerupState(id, false); }
278 final bool isActivePowerup (name id) {
279 auto pp = findPowerup(id);
280 return (pp && pp.active);
284 // ////////////////////////////////////////////////////////////////////////// //
285 override void Destroy () {
286 foreach (PlayerPowerup pp; powerups) delete pp;
291 void unpressAllKeys () {
293 kLeftPressed = false;
294 kLeftReleased = false;
296 kRightPressed = false;
297 kRightReleased = false;
301 kJumpPressed = false;
302 kJumpReleased = false;
304 kAttackPressed = false;
305 kAttackReleased = false;
306 kItemPressed = false;
307 kRopePressed = false;
308 kBombPressed = false;
310 kExitPressed = false;
314 // ////////////////////////////////////////////////////////////////////////// //
315 // called on level start too
321 skipCutscenePressed = false;
322 movementBlocked = false;
323 if (global.plife < 1) global.plife = max(1, global.config.scumStartLife);
328 myGrav = default.myGrav;
332 depth = default.depth;
333 status = default.status;
339 distToNearestLightSource = 999;
341 justdied = default.justdied;
343 if (holdItem isa PlayerWeapon) {
349 blink = default.blink;
350 blinkHidden = default.blinkHidden;
356 level.clearKeysPressRelease();
359 //scrSwitchToPocketItem(forceIfEmpty:false);
363 // ////////////////////////////////////////////////////////////////////////// //
364 bool isExitingSprite () {
365 auto spr = getSprite();
366 return (spr.Name == 'sPExit' || spr.Name == 'sDamselExit' || spr.Name == 'sTunnelExit');
370 // ////////////////////////////////////////////////////////////////////////// //
371 override void playSound (name aname, optional bool unique) {
372 if (unique && global.sndIsPlaying(aname)) return;
373 global.playSound(xCenter, yCenter, aname, relative:true);
377 override bool sndIsPlaying (name aname) {
378 return global.sndIsPlaying(aname, relative:true);
382 override void sndStopSound (name aname) {
383 global.sndStopSound(aname, relative:true);
387 // ////////////////////////////////////////////////////////////////////////// //
388 transient ItemDice currDie;
390 void onDieRolled (ItemDice die) {
391 if (!die.forSale) return;
392 // only law-abiding players can play
393 if (global.thiefLevel > 0 || global.murderer) return;
394 if (bet == 0) return;
397 level.forEachObject(delegate bool (MapObject o) {
398 MonsterShopkeeper sc = MonsterShopkeeper(o);
399 if (sc && !sc.dead && !sc.angered) return sc.onDiePlayed(self, currDie);
406 // ////////////////////////////////////////////////////////////////////////// //
407 override bool onExplosionTouch (MapObject xplo) {
408 //writeln("PlayerPawn: on explo touch! ", invincible);
409 if (invincible) return false;
410 if (global.config.scumExplosionHurt) {
411 global.plife -= global.config.explosionDmg;
412 if (!dead && global.plife <= 0 /*&& isRealLevel()*/) level.addDeath('explosion');
414 if (global.config.scumExplosionStun) {
418 scrCreateBlood(ix, iy, 1);
420 if (xplo.ix < ix) xVel = global.randOther(4, 6); else xVel = -global.randOther(4, 6);
426 // ////////////////////////////////////////////////////////////////////////// //
427 // start new game when exiting from title, and process other custom exits
428 void scrPlayerExit () {
429 level.playerExited = true;
435 // ////////////////////////////////////////////////////////////////////////// //
436 bool scrHideItemToPocket (optional bool forBombOrRope) {
437 if (!holdItem) return true;
438 if (holdItem isa PlayerWeapon) return false;
439 if (holdItem.forSale) return false;
440 if (!forBombOrRope) {
441 if (holdItem isa ItemBall) return false;
444 // cannot hide armed bomb
445 ItemBomb bomb = ItemBomb(holdItem);
446 if (bomb && bomb.armed) return false;
447 if (bomb || holdItem isa ItemRopeThrow) {
448 holdItem.instanceRemove();
454 if (holdItem isa MapEnemy) return false;
455 //writeln("hiding: '", GetClassName(holdItem.Class), "'");
457 if (pickedItem) FatalError("we are already holding '%n'", GetClassName(pickedItem.Class));
458 pickedItem = holdItem;
460 pickedItem.active = false;
461 pickedItem.visible = false;
462 if (pickedItem.heldBy) FatalError("oooops (scrHideItemToPocket)");
467 bool scrSwitchToBombs () {
468 if (holdItem isa PlayerWeapon) return false;
470 if (global.bombs < 1) return false;
471 if (ItemBomb(holdItem)) return true;
472 if (!scrHideItemToPocket(forBombOrRope:true)) return false;
474 ItemBomb bomb = ItemBomb(level.MakeMapObject(ix, iy, 'oBomb'));
475 if (!bomb) return false;
476 bomb.setSticky(global.stickyBombsActive);
478 whoaTimer = whoaTimerMax;
483 bool scrSwitchToStickyBombs () {
484 if (holdItem isa PlayerWeapon) return false;
485 if (!global.hasStickyBombs) {
486 global.stickyBombsActive = false;
490 global.stickyBombsActive = !global.stickyBombsActive;
495 bool scrSwitchToRopes () {
496 if (holdItem isa PlayerWeapon) return false;
498 if (global.rope < 1) return false;
499 if (ItemRopeThrow(holdItem)) return true;
500 if (!scrHideItemToPocket(forBombOrRope:true)) return false;
502 ItemRopeThrow rope = ItemRopeThrow(level.MakeMapObject(ix, iy, 'oRopeThrow'));
503 if (!rope) return false;
505 whoaTimer = whoaTimerMax;
510 bool isHoldingBombOrRope () {
512 if (!hit) return false;
513 return (hit isa ItemBomb || hit isa ItemRopeThrow);
517 bool isHoldingBomb () {
519 if (!hit) return false;
520 return (hit isa ItemBomb);
524 bool isHoldingArmedBomb () {
525 auto hit = ItemBomb(holdItem);
526 if (!hit) return false;
531 bool isHoldingRope () {
533 if (!hit) return false;
534 return (hit isa ItemRopeThrow);
538 bool scrSwitchToPocketItem (bool forceIfEmpty) {
539 if (holdItem isa PlayerWeapon) return false;
540 if (holdItem && holdItem.forSale) return false;
542 if (holdItem == pickedItem) { pickedItem = none; whoaTimer = whoaTimerMax; return true; }
544 if (!forceIfEmpty && !pickedItem) return false;
546 // destroy currently holded item if it is a bomb or a rope
548 // you cannot do it with an armed bomb
549 if (holdItem isa MapEnemy) return false; // cannot hide an enemy
550 ItemBomb bomb = ItemBomb(holdItem);
551 if (bomb && bomb.armed) return false;
552 if (bomb || holdItem isa ItemRopeThrow) {
554 holdItem.instanceRemove();
558 writeln(va("cannot switch to pocket item while carrying '%n' ('%n' is in pocket, why?)", GetClassName(holdItem.Class), GetClassName(pickedItem.Class)));
564 auto oldHold = holdItem;
565 holdItem = pickedItem;
566 pickedItem = oldHold;
567 // all flag management is done in property handler
569 oldHold.active = false;
570 oldHold.visible = false;
572 whoaTimer = whoaTimerMax;
577 bool scrSwitchToNextItem () {
578 if (holdItem isa PlayerWeapon) return false;
579 if (holdItem && holdItem.forSale) return false;
582 if (ItemBomb(holdItem)) {
583 if (ItemBomb(holdItem).armed) return false; // cannot switch out of armed bomb
584 if (scrSwitchToRopes()) return true;
585 return scrSwitchToPocketItem(forceIfEmpty:true);
589 if (ItemRopeThrow(holdItem)) {
590 if (scrSwitchToPocketItem(forceIfEmpty:true)) return true;
591 if (scrSwitchToBombs()) return true;
592 return scrHideItemToPocket();
595 // either nothing, or normal item
596 bool tryPocket = !!holdItem;
597 if (scrSwitchToBombs()) return true;
598 if (scrSwitchToRopes()) return true;
599 if (holdItem isa ItemBall) return false;
600 if (tryPocket) return scrSwitchToPocketItem(forceIfEmpty:true);
605 // ////////////////////////////////////////////////////////////////////////// //
606 bool scrPickupItem (MapObject obj) {
607 if (holdItem isa PlayerWeapon) return false;
609 if (!obj) return false;
612 if (pickedItem) return false;
613 if (isHoldingArmedBomb()) return false;
614 if (isHoldingBombOrRope()) {
615 if (!scrSwitchToPocketItem(forceIfEmpty:true)) return false;
617 if (holdItem) return false;
620 if (pickedItem) return false;
623 if (obj isa ItemBomb && !ItemBomb(obj).armed) ++global.bombs;
624 else if (obj isa ItemRopeThrow) ++global.rope;
626 whoaTimer = whoaTimerMax;
627 obj.onPickedUp(self);
632 // drop currently held item
633 bool scrDropItem (LostCause cause, optional float xVel, optional float yVel) {
634 if (holdItem isa PlayerWeapon) return false;
636 if (!holdItem) return false;
638 if (!onLoosingHeldItem(cause)) return false;
643 if (!hi.onLostAsHeldItem(self, cause, xVel!optional, yVel!optional)) {
649 if (hi isa ItemRopeThrow) global.rope = max(0, global.rope-1);
650 else if (hi isa ItemBomb && !ItemBomb(hi).armed) global.bombs = max(0, global.bombs-1);
654 scrSwitchToPocketItem(forceIfEmpty:true);
659 // ////////////////////////////////////////////////////////////////////////// //
660 void scrUseThrowIt (MapObject it) {
663 it.onBeforeThrowBy(self);
668 if (dir == Dir.Left) {
669 it.xVel = (it.heavy ? -4+xVel : -8+xVel);
670 //foreach (; 0..8) if (level.isSolidAtPoint(ix-8, iy)) it.shiftX(1);
671 //while (!level.isSolidAtPoint(ix-8, iy)) it.shiftX(1); // prevent getting stuck in wall
672 } else if (dir == Dir.Right) {
673 it.xVel = (it.heavy ? 4+xVel : 8+xVel);
674 //foreach (; 0..8) if (level.isSolidAtPoint(ix+8, iy)) it.shiftX(-1);
675 //while (!level.isSolidAtPoint(ix+8, iy)) it.shiftX(-1); // prevent getting stuck in wall
677 it.yVel = (it.heavy ? (kUp ? -4 : -2) : (kUp ? -9 : -3));
678 if (kDown || scrPlayerIsDucking()) {
679 if (platformCharacterIs(ON_GROUND)) {
686 } else if (!global.hasMitt) {
687 if (dir == Dir.Left) {
688 if (level.isSolidAtPoint(ix-8, iy-10)) {
692 } else if (dir == Dir.Right) {
693 if (level.isSolidAtPoint(ix+8, iy-10)) {
700 if (global.hasMitt && !scrPlayerIsDucking()) {
701 it.xVel += (it.xVel < 0 ? -6 : 6);
702 if (!kUp && !kDown) it.yVel = -0.4;
703 else if (kDown) it.yVel = 6;
707 // prevent getting stuck in a wall
708 if (it.isCollision()) {
709 //foreach (; 0..8) if (level.isSolidAtPoint(ix-8, iy)) it.shiftX(1);
711 if (level.isSolidAtPoint(it.ix-8, it.iy)) it.shiftX(8);
712 } else if (it.xVel > 0) {
713 if (level.isSolidAtPoint(it.ix+8, it.iy)) it.shiftX(-8);
714 } else if (it.isCollision()) {
715 int dx = (it.isCollisionLeft(0) ? 1 : it.isCollisionRight(0) ? -1 : 0);
719 if (!it.isCollision()) break;
725 while (dx > 8 && it.isCollisionLeft(dx)) ++dx;
726 if (dx < 8) it.shiftX(8);
729 while (dx > 8 && it.isCollisionRight(dx)) ++dx;
730 if (dx < 8) it.shiftX(-8);
736 if (it.sprite_index == sBombBag ||
737 it.sprite_index == sBombBox ||
738 it.sprite_index == sRopePile)
742 playSound('sndThrow');
747 bool scrUseThrowItem () {
748 if (holdItem isa PlayerWeapon) return false;
750 auto hitem = holdItem;
752 if (!hitem) return false;
753 if (!onLoosingHeldItem(LostCause.Unknown)) return false;
758 scrUseThrowIt(hitem);
760 // if we throwing away armed bomb, get previous item back into our hands
762 if (/*ItemBomb(hitem)*/isHoldingBombOrRope()) scrSwitchToPocketItem(forceIfEmpty:false);
768 // ////////////////////////////////////////////////////////////////////////// //
769 bool scrPlayerIsDucking () {
770 if (dead) return false;
771 auto spr = getSprite();
772 //if (!spr) return false;
774 spr.Name == 'sDuckLeft' ||
775 spr.Name == 'sCrawlLeft' ||
776 spr.Name == 'sDamselDuckL' ||
777 spr.Name == 'sDamselCrawlL' ||
778 spr.Name == 'sTunnelDuckL' ||
779 spr.Name == 'sTunnelCrawlL';
784 if (holdItem !isa ItemWeaponBow) return false;
785 if (!bowArmed) return false;
786 if (!holdItem.onTryUseItem(self)) return false;
791 void scrUsePutItOnGroundHelper (MapObject it, optional float xVelMult, optional float yVelNew) {
794 if (!specified_xVelMult) xVelMult = 0.4;
795 if (!specified_yVelNew) yVelNew = 0.5;
797 //writeln("putting '", GetClassName(hi.Class), "'");
799 if (dir == Dir.Left) {
800 it.xVel = (it.heavy ? -4 : -8);
801 } else if (dir == Dir.Right) {
802 it.xVel = (it.heavy ? 4 : 8);
810 if (ItemGoldIdol(it)) it.flty = iy;
813 if (it.isCollisionBottom(0) && !it.isCollisionTop(1)) {
821 if (it.isCollisionLeft(0)) {
822 if (it.isCollisionRight(1)) break;
824 } else if (it.isCollisionRight(0)) {
825 if (it.isCollisionLeft(1)) break;
834 // put item which player holds in his hands on the ground if player is ducking
835 // return `true` if item was put
836 bool scrUsePutItemOnGround (optional float xVelMult, optional float yVelNew) {
837 if (holdItem isa PlayerWeapon) return false;
840 if (!hi || !scrPlayerIsDucking()) return false;
842 if (!onLoosingHeldItem(LostCause.Unknown)) return false;
844 //writeln("putting '", GetClassName(hi.Class), "'");
846 if (global.bombs > 0) {
847 auto bomb = ItemBomb(hi);
848 if (bomb && !bomb.armed) global.bombs -= 1;
851 if (global.rope > 0) {
852 auto rope = ItemRopeThrow(hi);
855 rope.falling = false;
865 scrUsePutItOnGroundHelper(hi, xVelMult!optional, yVelNew!optional);
871 bool launchRope (bool goDown, bool doDrop) {
872 if (global.rope < 1) {
874 if (ItemRopeThrow(holdItem)) scrSwitchToPocketItem(forceIfEmpty:false);
880 bool wasHeld = false;
881 ItemRopeThrow rp = ItemRopeThrow(holdItem);
882 int xdelta = (doDrop ? 12 : 16)*(dir == Dir.Left ? -1 : 1);
884 //FIXME: call handler
887 rp.setXY(ix+xdelta, iy);
889 rp = ItemRopeThrow(level.MakeMapObject(ix+xdelta, iy, 'oRopeThrow'));
891 if (rp.heldBy) FatalError("PlayerPawn::launchRope: hold management fucked");
894 //rp.resaleValue = 0;
898 if (platformCharacterIs(ON_GROUND)) rp.startY = iy; // YASM 1.7
910 if (!level.isSolidAtPoint(ix+(doDrop ? 2 : 8), iy)) { //2
911 if (!level.checkTilesInRect(rp.ix-8, rp.iy, 2, 17)) rp.shiftX(-8);
912 else if (!level.checkTilesInRect(rp.ix+7, rp.iy, 2, 17)) rp.shiftX(8);
917 } else if (!level.isSolidAtPoint(ix-(doDrop ? 2 : 8), iy)) { //2
918 if (!level.checkTilesInRect(rp.ix+7, rp.iy, 2, 17)) rp.shiftX(8);
919 else if (!level.checkTilesInRect(rp.ix-8, rp.iy, 2, 17)) rp.shiftX(-8);
926 // cannot launch rope
927 /* was commented in the original
928 if (oPlayer1.facing == 18) {
929 obj = instance_create(oPlayer1.x-4, oPlayer1.y+2, oRopeThrow);
932 obj = instance_create(oPlayer1.x+4, oPlayer1.y+2, oRopeThrow);
937 //writeln("!!! goDown=", goDown, "; doDrop=", doDrop, "; wasHeld=", wasHeld);
940 if (!wasHeld) doDrop = true;
944 if (dir == Dir.Left) rp.xVel = -3.2; else rp.xVel = 3.2;
947 rp.forceFixHoldCoords(self);
949 scrUsePutItOnGroundHelper(rp);
953 if (wasHeld) scrSwitchToPocketItem(forceIfEmpty:false);
955 //writeln("NO DROP!");
959 //rp.resaleValue = 1; //k8:???
963 if (wasHeld) scrSwitchToPocketItem(forceIfEmpty:false);
968 level.MakeMapObject(rp.ix, rp.iy, 'oRopeTop');
975 if (wasHeld) scrSwitchToPocketItem(forceIfEmpty:false);
976 playSound('sndThrow');
981 bool scrLaunchBomb () {
982 if (whipping || global.bombs < 1) return false;
985 ItemBomb bomb = ItemBomb(level.MakeMapObject(ix, iy, 'oBomb'));
986 if (!bomb) return false;
987 bomb.forceFixHoldCoords(self);
988 bomb.setSticky(global.stickyBombsActive);
990 bomb.resaleValue = 0;
992 if (kDown || scrPlayerIsDucking()) {
993 scrUsePutItOnGroundHelper(bomb);
1002 bool scrUseItem () {
1004 if (!it) return false;
1005 //writeln(GetClassName(holdItem.Class));
1007 //auto spr = holdItem.getSprite();
1009 } else if (holdItem.type == "Sceptre") {
1010 if (kDown) scrUsePutItemOnGround(0.4, 0.5);
1011 if (firing == 0 && !scrPlayerIsDucking()) {
1012 if (facing == LEFT) {
1021 obj = instance_create(x+xofs, y+4, oPsychicCreateP);
1022 obj.xVel = xsgn*rand(1, 3);
1023 obj.yVel = -random(2);
1025 obj = instance_create(x+xofs, y-2, oPsychicWaveP);
1027 playSound(global.sndPsychic);
1028 firing = firingPistolMax;
1030 } else if (holdItem.type == "Teleporter II") {
1031 scrUseTeleporter2();
1032 } else if (holdItem.type == "Bow") {
1034 scrUsePutItemOnGround(0.4, 0.5);
1035 } else if (firing == 0 && !scrPlayerIsDucking() && !bowArmed && global.arrows > 0) {
1037 playSound(global.sndBowPull);
1038 } else if (global.arrows <= 0) {
1039 global.message = "I'M OUT OF ARROWS!";
1040 global.message2 = "";
1041 global.messageTimer = 80;
1047 if (whipping) return false;
1050 scrUsePutItemOnGround();
1054 // you cannot throw away shop items, but can throw dices
1055 if (it.forSale && it !isa ItemDice) {
1056 if (!level.isInShop(ix/16, iy/16)) {
1059 // allow throw/use shop items
1064 if (!it.onTryUseItem(self)) {
1073 // ////////////////////////////////////////////////////////////////////////// //
1074 // called by characterStepEvent
1075 // help player jump up through one block wide gaps by nudging them to one side so they don't hit their head
1076 void scrJumpHelper () {
1077 int d = 4; // max distance to nudge player
1079 if (!level.checkTilesInRect(x, y-12, 1, 7)) {
1080 if (level.checkTilesInRect(x-5, y-12, 1, 7) &&
1081 level.checkTilesInRect(x+14, y-12, 1, 7))
1083 while (d > 0 && level.checkTilesInRect(x-5, y-12, 1, 7)) { ++x; shiftX(1); --d; }
1084 } else if (level.checkTilesInRect(x+5, y-12, 1, 7) &&
1085 level.checkTilesInRect(x-14, y-12, 1, 7))
1087 while (d > 0 && level.checkTilesInRect(x+5, y-12, 1, 7)) { --x; shiftX(-1); --d; }
1091 if (!collision_line(x, y-6, x, y-12, oSolid, 0, 0)) {
1092 if (collision_line(x-5, y-6, x-5, y-12, oSolid, 0, 0) &&
1093 collision_line(x+14, y-6, x+14, y-12, oSolid, 0, 0))
1095 while (collision_line(x-5, y-6, x-5, y-12, oSolid, 0, 0) && d > 0) {
1100 else if (collision_line(x+5, y-6, x+5, y-12, oSolid, 0, 0) and
1101 collision_line(x-14, y-6, x-14, y-12, oSolid, 0, 0))
1103 while (collision_line(x+5, y-6, x+5, y-12, oSolid, 0, 0) && d > 0) {
1113 // ////////////////////////////////////////////////////////////////////////// //
1115 * Returns whether a GENERAL trait about a character is true.
1116 * Only the platform character should run this script.
1118 * `tp` can be one of the following:
1123 final bool platformCharacterIs (int tp) {
1124 if (tp == ON_GROUND && (status == RUNNING || status == STANDING || status == DUCKING || status == LOOKING_UP)) return true;
1125 if (tp == IN_AIR && (status == JUMPING || status == FALLING)) return true;
1126 if (tp == ON_LADDER && status == CLIMBING) return true;
1131 // ////////////////////////////////////////////////////////////////////////// //
1132 // sets the sprite of the character depending on his/her status
1133 final void characterSprite () {
1134 if (status == STOPPED) {
1135 if (global.isDamsel) setSprite('sDamselLeft');
1136 else if (global.isTunnelMan) setSprite('sTunnelLeft');
1137 else setSprite('sStandLeft');
1142 if (global.isTunnelMan && !stunned && !whipping) {
1144 if (status == STANDING) {
1145 if (!level.isSolidAtPoint(x-2, y+9)) {
1147 setSprite('sTunnelWhoaL');
1149 setSprite('sTunnelLeft');
1152 if (status == RUNNING) {
1153 if (kUp) setSprite('sTunnelLookRunL'); else setSprite('sTunnelRunL');
1155 if (status == DUCKING) {
1156 if (xVel == 0) setSprite('sTunnelDuckL');
1157 else if (fabs(xVel) < 3) setSprite('sTunnelCrawlL');
1158 else setSprite('sTunnelRunL');
1160 if (status == LOOKING_UP) {
1161 if (fabs(xVel) > 0) setSprite('sTunnelRunL'); else setSprite('sTunnelLookL');
1163 if (status == JUMPING) setSprite('sTunnelJumpL');
1164 if (status == FALLING && statePrev == FALLING && statePrevPrev == FALLING) setSprite('sTunnelFallL');
1165 if (status == HANGING) setSprite('sTunnelHangL');
1166 if (pushTimer > 20) setSprite('sTunnelPushL');
1167 if (status == DUCKTOHANG) setSprite('sTunnelDtHL');
1168 if (status == CLIMBING) {
1169 if (level.isRopeAtPoint(x, y)) {
1170 if (kDown) setSprite('sTunnelClimb3'); else setSprite('sTunnelClimb2');
1172 setSprite('sTunnelClimb');
1175 } else if (global.isDamsel && !stunned && !whipping) {
1177 if (status == STANDING) {
1178 if (!level.isSolidAtPoint(x-2, y+9)) {
1180 setSprite('sDamselWhoaL');
1181 /* was commented out in the original
1182 if (holdItem && whoaTimer < 1) {
1183 holdItem.held = false;
1184 if (facing == LEFT) holdItem.xVel = -2; else holdItem.xVel = 2;
1185 if (holdItem.type == "Damsel") playSound('sndDamsel');
1186 if (holdItem.type == pickupItemType) { holdItem = 0; pickupItemType = ""; } else scrSwitchToPocketItem();
1190 setSprite('sDamselLeft');
1193 if (status == RUNNING) {
1194 if (kUp) setSprite('sDamselRunL'); else setSprite('sDamselRunL');
1196 if (status == DUCKING) {
1197 if (xVel == 0) setSprite('sDamselDuckL');
1198 else if (fabs(xVel) < 3) setSprite('sDamselCrawlL');
1199 else setSprite('sDamselRunL');
1201 if (status == LOOKING_UP) {
1202 if (fabs(xVel) > 0) setSprite('sDamselRunL'); else setSprite('sDamselLookL');
1204 if (status == JUMPING) setSprite('sDamselDieLR');
1205 if (status == FALLING && statePrev == FALLING && statePrevPrev == FALLING) setSprite('sDamselFallL');
1206 if (status == HANGING) setSprite('sDamselHangL');
1207 if (pushTimer > 20) setSprite('sDamselPushL');
1208 if (status == DUCKTOHANG) setSprite('sDamselDtHL');
1209 if (status == CLIMBING) {
1210 if (level.isRopeAtPoint(x, y)) {
1211 if (kDown) setSprite('sDamselClimb3'); else setSprite('sDamselClimb2');
1213 setSprite('sDamselClimb');
1216 } else if (!stunned && !whipping) {
1218 if (status == STANDING) {
1219 if (!level.checkTileAtPoint(x-(dir == Dir.Left ? 2 : 0), y+9, &level.cbCollisionForWhoa)) {
1221 setSprite('sWhoaLeft');
1222 /* was commented out in the original
1223 if (holdItem && whoaTimer < 1) {
1224 holdItem.held = false;
1225 if (facing == LEFT) holdItem.xVel = -2; else holdItem.xVel = 2;
1226 if (holdItem.type == "Damsel") playSound('sndDamsel');
1227 if (holdItem.type == pickupItemType) { holdItem = 0; pickupItemType = ""; } else scrSwitchToPocketItem();
1231 setSprite('sStandLeft');
1234 if (status == RUNNING) {
1235 if (kUp) setSprite('sLookRunL'); else setSprite('sRunLeft');
1237 if (status == DUCKING) {
1238 if (xVel == 0) setSprite('sDuckLeft');
1239 else if (fabs(xVel) < 3) setSprite('sCrawlLeft');
1240 else setSprite('sRunLeft');
1242 if (status == LOOKING_UP) {
1243 if (fabs(xVel) > 0) setSprite('sLookRunL'); else setSprite('sLookLeft');
1245 if (status == JUMPING) setSprite('sJumpLeft');
1246 if (status == FALLING && statePrev == FALLING && statePrevPrev == FALLING) setSprite('sFallLeft');
1247 if (status == HANGING) setSprite('sHangLeft');
1248 if (pushTimer > 20) setSprite('sPushLeft');
1249 if (status == CLIMBING) {
1250 if (level.isRopeAtPoint(x, y)) {
1251 if (kDown) setSprite('sClimbUp3'); else setSprite('sClimbUp2');
1253 setSprite('sClimbUp');
1256 if (status == DUCKTOHANG) setSprite('sDuckToHangL');
1262 // ////////////////////////////////////////////////////////////////////////// //
1263 void addScore (int delta) {
1264 if (!level.isNormalLevel()) return;
1266 if (delta == 0) return;
1267 level.stats.addMoney(delta);
1269 level.xmoney += delta;
1270 level.collectCounter = min(100, level.collectCounter+20);
1275 // ////////////////////////////////////////////////////////////////////////// //
1276 // for dead players too
1277 // first, the code will call `onObjectTouched()` for player
1278 // if it returned `false`, the code will call `obj.onTouchedByPlayer()`
1279 // note that player's handler is called *after* its frame thinker,
1280 // but object handler is called *before* frame thinker for the object
1281 // i.e. return `true` to block calling `obj.onTouchedByPlayer()`,
1282 // (but NOT object thinker)
1283 bool onObjectTouched (MapObject obj) {
1285 if (dead) return false;
1287 if (obj isa ItemProjectileArrow && holdItem isa ItemWeaponBow && !stunned && global.arrows < 99) {
1288 if (fabs(obj.xVel) < 1 && fabs(obj.yVel) < 1 && !obj.stuck) {
1290 playSound('sndPickup');
1291 obj.instanceRemove();
1297 auto treasure = ItemTreasure(obj);
1298 if (treasure && treasure.canCollect) {
1299 if (treasure.value) addScore(treasure.value);
1300 treasure.onCollected(self); // various other effects
1301 playSound(treasure.soundName);
1302 treasure.instanceRemove();
1307 if (global.hasKapala && obj isa MapObjBlood) {
1308 global.bloodLevel += 1;
1309 level.MakeMapObject(obj.ix, obj.iy, 'oBloodSpark');
1310 obj.instanceRemove();
1312 if (global.bloodLevel > 8) {
1313 global.bloodLevel = 0;
1315 level.MakeMapObject(ix, iy-8, 'oHeart');
1316 playSound('sndKiss');
1319 if (redColor < 55) redColor += 5;
1323 // other objects will take care of themselves
1328 // return `false` to prevent
1329 // holdItem is valid
1330 bool onLoosingHeldItem (LostCause cause) {
1331 if (level.inWinCutscene != 0) return false;
1336 // ////////////////////////////////////////////////////////////////////////// //
1337 // k8: don't even ask me! the following mess is almost straightforward port of the original Derek's code!
1338 private final void closeCape () {
1339 auto pp = PPCape(findPowerup('Cape'));
1340 if (pp) pp.open = false;
1344 private final void switchCape () {
1345 auto pp = PPCape(findPowerup('Cape'));
1346 if (pp) pp.open = !pp.open;
1350 final bool isCapeActiveAndOpen () {
1351 auto pp = PPCape(findPowerup('Cape'));
1352 return (pp && pp.active && pp.open);
1356 final bool isParachuteActive () {
1357 auto pp = findPowerup('Parachute');
1358 return (pp && pp.active);
1362 // ////////////////////////////////////////////////////////////////////////// //
1364 bool checkSkipCutScene () {
1365 if (skipCutscenePressed) {
1366 return level.isKeyReleased(GameConfig::Key.Pay);
1368 skipCutscenePressed = level.isKeyPressed(GameConfig::Key.Pay);
1376 bool forcePlayerControls () {
1377 if (level.inWinCutscene) {
1379 level.winCutscenePlayerControl(self);
1381 } else if (level.levelKind == GameLevel::LevelKind.Transition) {
1384 if (checkSkipCutScene()) {
1385 level.playerExited = true;
1389 auto door = level.checkTileAtPoint(ix, iy, &level.cbCollisionExitTile);
1391 kExitPressed = true;
1395 if (status == STOPPED) {
1396 if (--transKissTimer > 0) return true;
1401 auto dms = MonsterDamselKiss(level.isObjectAtPoint(ix+8, iy+4, delegate bool (MapObject o) { return (o isa MonsterDamselKiss); }));
1402 if (dms && !dms.kissed) {
1407 transKissTimer = 30;
1412 kRightPressed = true;
1419 // ////////////////////////////////////////////////////////////////////////// //
1420 private final void checkControlKeys (SpriteImage spr) {
1421 if (forcePlayerControls()) {
1422 if (movementBlocked) unpressAllKeys();
1423 if (kLeft) kLeftPushedSteps += 1; else kLeftPushedSteps = 0;
1424 if (kRight) kRightPushedSteps += 1; else kRightPushedSteps = 0;
1428 kLeft = level.isKeyDown(GameConfig::Key.Left);
1429 if (movementBlocked) kLeft = false;
1430 if (kLeft) kLeftPushedSteps += 1; else kLeftPushedSteps = 0;
1431 kLeftPressed = level.isKeyPressed(GameConfig::Key.Left);
1432 kLeftReleased = level.isKeyReleased(GameConfig::Key.Left);
1434 kRight = level.isKeyDown(GameConfig::Key.Right);
1435 if (movementBlocked) kRight = false;
1436 if (kRight) kRightPushedSteps += 1; else kRightPushedSteps = 0;
1437 kRightPressed = level.isKeyPressed(GameConfig::Key.Right);
1438 kRightReleased = level.isKeyReleased(GameConfig::Key.Right);
1440 kUp = level.isKeyDown(GameConfig::Key.Up);
1441 kDown = level.isKeyDown(GameConfig::Key.Down);
1443 kJump = level.isKeyDown(GameConfig::Key.Jump);
1444 kJumpPressed = level.isKeyPressed(GameConfig::Key.Jump);
1445 kJumpReleased = level.isKeyReleased(GameConfig::Key.Jump);
1447 if (movementBlocked) unpressAllKeys();
1451 kJumpPressed = false;
1452 kJumpReleased = false;
1454 } else if (spr && global.isTunnelMan && spr.Name == 'sTunnelAttackL' && !holdItem) {
1456 kJumpPressed = false;
1457 kJumpReleased = false;
1458 cantJump = max(0, cantJump-1);
1461 kAttack = level.isKeyDown(GameConfig::Key.Attack);
1462 kAttackPressed = level.isKeyPressed(GameConfig::Key.Attack);
1463 kAttackReleased = level.isKeyReleased(GameConfig::Key.Attack);
1465 kItemPressed = level.isKeyPressed(GameConfig::Key.Switch);
1466 kRopePressed = level.isKeyPressed(GameConfig::Key.Rope);
1467 kBombPressed = level.isKeyPressed(GameConfig::Key.Bomb);
1469 kPayPressed = level.isKeyPressed(GameConfig::Key.Pay);
1471 if (movementBlocked) unpressAllKeys();
1473 kExitPressed = false;
1474 if (global.config.useDoorWithButton) {
1475 if (kPayPressed) kExitPressed = true;
1477 if (kUp) kExitPressed = true;
1480 if (stunned || dead) {
1482 //level.clearKeysPressRelease();
1487 // ////////////////////////////////////////////////////////////////////////// //
1488 // knock off monkeys that grabbed you
1489 void knockOffMonkeys () {
1490 level.forEachObject(delegate bool (MapObject o) {
1491 auto mk = EnemyMonkey(o);
1492 if (mk && !mk.dead && mk.status == GRAB) {
1493 mk.xVel = global.randOther(0, 1)-global.randOther(0, 1);
1496 mk.vineCounter = 20;
1497 mk.grabCounter = 60;
1504 // ////////////////////////////////////////////////////////////////////////// //
1505 final void characterStepEvent () {
1506 if (climbSoundTimer > 0) {
1507 if (--climbSoundTimer == 0) {
1508 playSound(climbSndToggle ? 'sndClimb2' : 'sndClimb1');
1509 climbSndToggle = !climbSndToggle;
1513 auto spr = getSprite();
1514 checkControlKeys(spr);
1516 float xPrev = fltx, yPrev = flty;
1519 // check collisions in various directions
1520 bool colSolidLeft = !!getPushableLeft(1);
1521 bool colSolidRight = !!getPushableRight(1);
1522 bool colLeft = !!isCollisionLeft(1);
1523 bool colRight = !!isCollisionRight(1);
1524 bool colTop = !!isCollisionTop(1);
1525 bool colBot = !!isCollisionBottom(1);
1526 bool colLadder = !!isCollisionLadder();
1527 bool colPlatBot = !!isCollisionBottom(1, &level.cbCollisionPlatform);
1528 bool colPlat = !!isCollision(&level.cbCollisionPlatform);
1529 bool colWaterTop = !!isCollisionTop(1, &level.cbCollisionWater);
1530 bool colIceBot = !!level.isIceAtPoint(x, y+8);
1532 bool runKey = false;
1533 if (level.isKeyDown(GameConfig::Key.Run)) { runHeld = 100; runKey = true; }
1534 if (level.isKeyDown(GameConfig::Key.Attack) && !whipping) { runHeld += 1; runKey = true; }
1535 if (!runKey || (!kLeft && !kRight)) runHeld = 0;
1537 // allows the character to run left and right
1538 // if state!=DUCKING and state!=LOOKING_UP and state!=CLIMBING
1539 if (status != CLIMBING && status != HANGING) {
1540 if (kLeftReleased && fabs(xVel) < 0.0001) xAcc -= 0.5;
1541 if (kRightReleased && fabs(xVel) < 0.0001) xAcc += 0.5;
1542 if (kLeft && !kRight) {
1544 //xVel = 3; // in orig
1545 if (platformCharacterIs(ON_GROUND) && status != DUCKING) {
1548 //playSound('sndPush', unique:true);
1550 } else if (kLeftPushedSteps > 2 && (dir == Dir.Left || fabs(xVel) < 0.0001)) {
1554 //if (platformCharacterIs(ON_GROUND) && fabs(xVel) > 0 && alarm[3] < 1) alarm[3] = floor(16/-xVel);
1556 if (kRight && !kLeft) {
1557 if (colSolidRight) {
1558 //xVel = 3; // in orig
1559 if (platformCharacterIs(ON_GROUND) && status != DUCKING) {
1562 //playSound('sndPush', unique:true);
1564 } else if ((kRightPushedSteps > 2 || colSolidLeft) && (dir == Dir.Right || fabs(xVel) < 0.0001)) {
1568 //if (platformCharacterIs(ON_GROUND) && fabs(xVel) > 0 && alarm[3] < 1) alarm[3] = floor(16/xVel);
1573 if (status == CLIMBING) {
1577 auto ladder = level.isLadderAtPoint(x, y);
1578 if (ladder) { x = ladder.ix+8; setX(x); }
1579 if (kLeft) dir = Dir.Left; else if (kRight) dir = Dir.Right;
1581 // checks both ladder and laddertop
1582 if (level.isAnyLadderAtPoint(x, y-8)) {
1583 //writeln("LADDER00! old yAcc=", yAcc, "; climbAcc=", climbAcc, "; new yAcc=", yAcc-climbAcc);
1585 if (climbSoundTimer < 1) climbSoundTimer = climbSndSpeed;
1586 //!if (alarm[2] < 1) alarm[2] = climbSndSpeed;
1589 for (int dy = -6; dy > -12; --dy) {
1590 ladder = level.isAnyLadderAtPoint(x, y+dy);
1592 writeln("::: ", dy, ": plrx=", x, "; ladder.xy0=(", ladder.x0, ",", ladder.y0, "); ladder.ixy=(", ladder.ix, ",", ladder.iy, "); wdt=", ladder.width, "; hgt=", ladder.height, "; ladder class=", GetClassName(ladder.Class));
1597 auto grid = level.miscTileGrid;
1598 foreach (MapTile t; grid.inCellPix(48, 96, grid.nextTag(), precise:false)) {
1599 writeln("at 48, 96: ", GetClassName(t.Class), "; pos=(", t.ix, ",", t.iy, ")");
1601 foreach (MapTile t; grid.inCellPix(48, 94, grid.nextTag(), precise:false)) {
1602 writeln("at 48, 94: ", GetClassName(t.Class), "; pos=(", t.ix, ",", t.iy, ")");
1604 foreach (int dy; 90..102) {
1605 ladder = level.isAnyLadderAtPoint(48, dy);
1607 writeln("::: ", dy, ": plrx=", x, "; ladder.xy0=(", ladder.x0, ",", ladder.y0, "); ladder.ixy=(", ladder.ix, ",", ladder.iy, "); wdt=", ladder.width, "; hgt=", ladder.height, "; ladder class=", GetClassName(ladder.Class));
1613 // checks both ladder and laddertop
1614 if (level.isAnyLadderAtPoint(x, y+8)) {
1616 //!if (alarm[2] < 1) alarm[2] = climbSndSpeed;
1617 if (climbSoundTimer < 1) climbSoundTimer = climbSndSpeed;
1621 if (colBot) status = STANDING;
1624 if (kJumpPressed && !whipping) {
1625 if (kLeft) xVel = -departLadderXVel; else if (kRight) xVel = departLadderXVel; else xVel = 0;
1626 //yAcc += departLadderYVel;
1627 //k8: was `0.6`, but with `0.4` we can jump onto the wall above, and with `0.6` we cannot
1628 yAcc = 0.4+departLadderYVel; // YASM 1.8.1 Fix for extra air when jumping off ladders due to increased climb speed option
1630 jumpButtonReleased = false;
1635 if (ladderTimer > 0) ladderTimer -= 1;
1638 if (platformCharacterIs(IN_AIR) && status != HANGING) yAcc += gravityIntensity;
1640 // player has landed
1641 if ((colBot || colPlatBot) && platformCharacterIs(IN_AIR) && yVel >= 0) {
1642 if (!colPlat || colBot) {
1647 playSound('sndLand');
1649 if ((colBot || colPlatBot) && !colPlat) yVel = 0;
1651 // player has just walked off of the edge of a solid
1652 if (colBot == 0 && (!colPlatBot || colPlat) && platformCharacterIs(ON_GROUND)) {
1656 if (global.hasGloves) hangCount = 5;
1660 if (dead || stunned) yVel = -yVel*0.8;
1661 else if (status == JUMPING) yVel = fabs(yVel*0.3);
1664 if ((colLeft && dir == Dir.Left) || (colRight && dir == Dir.Right)) {
1665 if (dead || stunned) xVel = -xVel*0.5; else xVel = 0;
1669 if (kJumpReleased && platformCharacterIs(IN_AIR)) {
1671 } else if (platformCharacterIs(ON_GROUND)) {
1676 MapObject oWeb = none, oBlob = none;
1678 oWeb = level.isObjectAtPoint(x, y, &level.cbIsObjectWeb);
1679 if (!oWeb) oBlob = level.isObjectAtPoint(x, y, &level.cbIsObjectBlob);
1682 bool invokeJumpHelper = false;
1684 if (kJumpPressed && oWeb) {
1685 ItemWeb(oWeb).tear(1);
1686 yAcc += initialJumpAcc*2;
1691 jumpButtonReleased = false;
1695 invokeJumpHelper = true;
1696 } else if (kJumpPressed && oBlob) {
1698 scrCreateBloblets(oBlob.x0+8, oBlob.y0+8, 1);
1699 playSound('sndHit');
1700 yAcc += initialJumpAcc*2;
1704 jumpButtonReleased = false; // k8: was `jumpButtonRelease`
1706 invokeJumpHelper = true;
1707 } else if (kJumpPressed && colWaterTop) {
1708 yAcc += initialJumpAcc*2;
1713 jumpButtonReleased = false;
1717 invokeJumpHelper = true;
1718 } else if (global.hasCape && kJumpPressed && kJumped && platformCharacterIs(IN_AIR)) {
1720 } else if (global.hasJetpack && !swimming && kJump && kJumped && platformCharacterIs(IN_AIR) && jetpackFuel > 0) {
1721 yAcc += initialJumpAcc;
1724 if (jetpackFlaresTime < 1) jetpackFlaresTime = 3;
1725 //!if (alarm[10] < 1) alarm[10] = 3; // jetpack flares
1729 jumpButtonReleased = false;
1733 invokeJumpHelper = true;
1734 } else if (platformCharacterIs(ON_GROUND) && kJumpPressed && fallTimer == 0) {
1735 if (fabs(xVel) > 3 /*xVel > 3 || xVel < -3*/) {
1736 yAcc += initialJumpAcc*2;
1739 yAcc += initialJumpAcc*2;
1741 //scrJumpHelper(); // move to location where player doesn't have to be on ground
1743 if (global.hasJordans) {
1747 } else if (global.hasSpringShoes) {
1754 playSound('sndJump');
1758 // the "state" gets changed to JUMPING later on in the code
1760 // "variable jumping" states
1761 jumpButtonReleased = false;
1763 invokeJumpHelper = true;
1766 if (kJumpPressed && invokeJumpHelper) scrJumpHelper(); // YASM 1.8.1
1768 if (jumpTime < jumpTimeTotal) jumpTime += 1;
1769 // let the character continue to jump
1770 if (!kJump) jumpButtonReleased = true;
1771 if (jumpButtonReleased) jumpTime = jumpTimeTotal;
1773 gravityIntensity = (jumpTime/jumpTimeTotal)*grav;
1775 if (kUp && platformCharacterIs(ON_GROUND) && !colLadder) {
1776 //k8:!!!looking = UP;
1777 if (xVel == 0 && xAcc == 0) status = LOOKING_UP;
1779 //k8:!!!looking = 0;
1782 if (!kUp && status == LOOKING_UP) status = STANDING;
1786 if (global.hasGloves) {
1788 if (hangCount == 0 && y > 16 && !platformCharacterIs(ON_GROUND) && kRight && colRight &&
1789 (level.isSolidAtPoint(x+9, y-5) || level.isSolidAtPoint(x+9, y-6)))
1792 if (moveSnap(1, 8)) { x = ix; y = iy; }
1796 } else if (hangCount == 0 && y > 16 && !platformCharacterIs(ON_GROUND) && kLeft && colLeft &&
1797 (level.isSolidAtPoint(x-9, y-5) || level.isSolidAtPoint(x-9, y-6)))
1800 if (moveSnap(1, 8)) { x = ix; y = iy; }
1806 } else if (hangCount == 0 && y > 16 && !platformCharacterIs(ON_GROUND) && kRight && colRight &&
1807 (level.isTreeAtPoint(x+9, y-5) || level.isTreeAtPoint(x+9, y-6)))
1810 if (moveSnap(1, 8)) { x = ix; y = iy; }
1814 } else if (hangCount == 0 && y > 16 && !platformCharacterIs(ON_GROUND) && kLeft && colLeft &&
1815 (level.isTreeAtPoint(x-9, y-5) || level.isTreeAtPoint(x-9, y-6)))
1818 if (moveSnap(1, 8)) { x = ix; y = iy; }
1822 } else if (hangCount == 0 && y > 16 && !platformCharacterIs(ON_GROUND) && kRight && colRight &&
1823 (level.isSolidAtPoint(x+9, y-5) || level.isSolidAtPoint(x+9, y-6)) &&
1824 !level.isSolidAtPoint(x+9, y-9) && !level.isSolidAtPoint(x, y+9))
1827 if (moveSnap(1, 8)) { x = ix; y = iy; }
1831 } else if (hangCount == 0 && y > 16 && !platformCharacterIs(ON_GROUND) && kLeft && colLeft &&
1832 (level.isSolidAtPoint(x-9, y-5) || level.isSolidAtPoint(x-9, y-6)) &&
1833 !level.isSolidAtPoint(x-9, y-9) && !level.isSolidAtPoint(x, y+9))
1836 if (moveSnap(1, 8)) { x = ix; y = iy; }
1842 if (hangCount == 0 && y > 16 && !platformCharacterIs(ON_GROUND) &&
1843 !level.isSolidAtPoint(x, y+12) && // from Spelunky Natural
1844 status == FALLING &&
1845 !level.isObjectAtPoint(x, y-9, &level.cbIsObjectArrow) && !level.isObjectAtPoint(x, y+9, &level.cbIsObjectArrow))
1847 auto obj0 = level.isObjectAtPoint(x, y-5, &level.cbIsObjectArrow);
1848 auto obj1 = level.isObjectAtPoint(x, y-6, &level.cbIsObjectArrow);
1850 // get nearest arrow
1852 if (obj1 && obj0) obj = (obj0.distanceToPoint(x, y-5) < obj1.distanceToPoint(x, y-5) ? obj0 : obj1);
1853 else obj = (obj0 ? obj0 : obj1);
1854 //obj = instance_nearest(x, y-5, oArrow);
1857 // move_snap(1, 8); // was commented out in the original
1864 /* this was commented in the original
1865 if (hangCount == 0 && y > 16 && !platformCharacterIs(ON_GROUND) && state == FALLING &&
1866 (collision_point(x, y-5, oTreeBranch, 0, 0) || collision_point(x, y-6, oTreeBranch, 0, 0)) &&
1867 !collision_point(x, y-9, oTreeBranch, 0, 0) && !collision_point(x, y+9, oTreeBranch, 0, 0))
1870 // move_snap(1, 8); // was commented out in the original
1878 if (hangCount > 0) hangCount -= 1;
1880 if (status == HANGING) {
1885 if (global.hasGloves) {
1886 if (hangCount == 0 && y > 16 && !platformCharacterIs(ON_GROUND)) {
1887 if (kRight && colRight &&
1888 (level.isSolidAtPoint(x+9, y-5) || level.isSolidAtPoint(x+9, y-6)))
1894 } else if (kLeft && colLeft &&
1895 (level.isSolidAtPoint(x-9, y-5) || level.isSolidAtPoint(x-9, y-6)))
1917 yAcc += initialJumpAcc*2;
1918 shiftX(dir == Dir.Right ? -2 : 2);
1921 hangCount = hangCountMax;
1922 if (level.isObjectAtPoint(x, y-5, &level.cbIsObjectArrow) || level.isObjectAtPoint(x, y-6, &level.cbIsObjectArrow)) hangCount /= 2; //Spelunky Natural
1925 if ((dir == Dir.Left && !isCollisionLeft(2)) ||
1926 (dir == Dir.Right && !isCollisionRight(2)))
1937 // pressing down while standing
1938 if (kDown && platformCharacterIs(ON_GROUND) && !whipping) {
1941 } else if (colPlatBot) {
1942 // climb down ladder if possible, else jump down
1945 //ladder = instance_place(x, y+16, oLadder);
1947 // from Spelunky Natural
1949 ladder = collision_line(x-4, y+16, x+4, y+16, oLadder, 0, 0);
1950 if (!ladder) ladder = collision_line(x-4, y+16, x+4, y+16, oLadderTop, 0, 0);
1952 auto ladder = level.checkTilesInRect(x-4, y+16, 9, 1, &level.cbCollisionAnyLadder);
1953 //writeln("DOWN; cpb=", colPlatBot, "; cb=", colBot, "; ladder=", !!ladder);
1956 if (abs(x-(ladder.x0+8)) < 4) {
1970 kJumped = true; // Spelunky Natural
1974 // the character can't move down because there is a solid in the way
1979 if (!kDown && status == DUCKING) {
1984 if (xVel == 0 && xAcc == 0 && status == RUNNING) status = STANDING;
1985 if (xAcc != 0 && status == STANDING) status = RUNNING;
1986 if (yVel < 0 && platformCharacterIs(IN_AIR) && status != HANGING) status = JUMPING;
1987 if (yVel > 0 && platformCharacterIs(IN_AIR) && status != HANGING) {
1989 setCollisionBounds(-5, -6, 5, 8);
1991 setCollisionBounds(-5, -6, 5, 8);
1995 bool colPointLadder = !!level.isAnyLadderAtPoint(x, y);
1997 /* this was commented in the original
1998 if ((kUp && platformCharacterIs(IN_AIR) && collision_point(x, y-8, oLadder, 0, 0) && ladderTimer == 0) ||
1999 (kUp && colPointLadder && ladderTimer == 0) ||
2000 (kDown && colPointLadder && ladderTimer == 0 && platformCharacterIs(ON_GROUND) && collision_point(x, y+9, oLadderTop, 0, 0) && xVel == 0))
2003 ladder = instance_place(x, y-8, oLadder);
2004 if (instance_exists(ladder)) {
2005 if (abs(x-(ladder.x0+8)) < 4) {
2008 if (!collision_point(x, y, oLadder, 0, 0) && !collision_point(x, y, oLadderTop, 0, 0)) { y = ladder.iy+14; setY(y); }
2018 // Spelunky Natural - Multiple changes to this big "if" condition
2019 if ((kUp && platformCharacterIs(IN_AIR) && ladderTimer == 0 && level.checkTilesInRect(x-2, y-8, 5, 1, &level.cbCollisionLadder)) ||
2020 (kUp && colPointLadder && ladderTimer == 0) ||
2021 (kDown && colPointLadder && ladderTimer == 0 && platformCharacterIs(ON_GROUND) && xVel == 0 && level.isLadderTopAtPoint(x, y+9)) ||
2022 ((kUp || kDown) && status == HANGING && level.checkTilesInRect(x-2, y, 5, 1, &level.cbCollisionLadder)))
2025 //auto ladder = instance_place(x, y-8, oLadder);
2026 auto ladder = level.isLadderAtPoint(x, y-8);
2028 //writeln("LADDER01! plrx=", x, "; ladder.x0=", ladder.x0, "; ladder.ix=", ladder.ix, "; ladder class=", GetClassName(ladder.Class));
2029 if (abs(x-(ladder.x0+8)) < 4) {
2032 if (!level.isAnyLadderAtPoint(x, y)) { y = ladder.y0+14; setY(y); }
2042 /* this was commented in the original
2043 if (sprite_index == sDuckToHangL || sprite_index == sDamselDtHL) {
2045 if (facing == LEFT && collision_rectangle(x-8, y, x, y+16, oLadder, 0, 0) && !collision_point(x-4, y+16, oSolid, 0, 0)) {
2046 ladder = instance_nearest(x-4, y+16, oLadder);
2047 } else if (facing == RIGHT && collision_rectangle(x, y, x+8, y+16, oLadder, 0, 0) && !collision_point(x+4, y+16, oSolid, 0, 0)) {
2048 ladder = instance_nearest(x+4, y+16, oLadder);
2061 if (colLadder && state == CLIMBING && kJumpPressed && !whipping) {
2062 if (kLeft) xVel = -departLadderXVel; else if (kRight) xVel = departLadderXVel; else xVel = 0;
2063 yAcc += departLadderYVel;
2065 jumpButtonReleased = false;
2071 // calculate horizontal/vertical friction
2072 if (status == CLIMBING) {
2073 xFric = frictionClimbingX;
2074 yFric = frictionClimbingY;
2076 //if (runKey && platformCharacterIs(ON_GROUND) && runHeld >= 10)
2077 if ((runKey && runHeld >= 10) && (platformCharacterIs(ON_GROUND) || global.config.toggleRunAnywhere)) {
2083 xFric = frictionRunningFastX;
2084 } else if (kRight) {
2087 xFric = frictionRunningFastX;
2089 } else if (status == DUCKING) {
2090 if (xVel < 2 && xVel > -2) {
2094 } else if (kLeft && global.config.downToRun) {
2098 xFric = frictionRunningFastX;
2099 } else if (kRight && global.config.downToRun) {
2102 xFric = frictionRunningFastX;
2105 if (xVel < 0.5) xVel = 0;
2111 // decrease the friction when the character is "flying"
2112 if (platformCharacterIs(IN_AIR)) {
2113 if (dead || stunned) xFric = 1.0; else xFric = 0.8;
2115 xFric = frictionRunningX;
2119 /* // ORIGINAL RUN/WALK xVel/xFric code this was commented in the original
2120 if (runKey && platformCharacterIs(ON_GROUND) && runHeld >= 10) {
2125 xFric = frictionRunningFastX;
2126 } else if (kRight) {
2129 xFric = frictionRunningFastX;
2131 } else if (state == DUCKING) {
2132 if (xVel < 2 && xVel > -2) {
2136 } else if (kLeft && global.downToRun) {
2140 xFric = frictionRunningFastX;
2141 } else if (kRight && global.downToRun) {
2144 xFric = frictionRunningFastX;
2147 if (xVel < 0.5) xVel = 0;
2153 // decrease the friction when the character is "flying"
2154 if (platformCharacterIs(IN_AIR)) {
2155 if (dead || stunned) xFric = 1.0; else xFric = 0.8;
2157 xFric = frictionRunningX;
2162 // stuck on web or underwater
2163 if (level.isObjectAtPoint(x, y, &level.cbIsObjectWeb)) {
2167 } else if (level.isObjectAtPoint(x, y, &level.cbIsObjectBlob)) {
2169 //obj = instance_place(x, y, oBlob); this was commented in the original
2170 //xVel += obj.xVel; this was commented in the original
2174 } else if (level.isWaterAtPoint(x, y/*, oWater, -1, -1*/)) {
2176 //if (!runKey && global.toggleRunAnywhere) xFric = frictionRunningX; // YASM 1.8.1 this was commented in the original
2177 if (!platformCharacterIs(ON_GROUND)) xFric = frictionRunningX;
2178 if (status == FALLING && yVel > 0) {
2180 if (global.config.naturalSwim && kUp) yFric = 0.2;
2181 else if (global.config.naturalSwim && kDown) yFric = 0.8;
2183 } else if (!level.isWaterAtPoint(x, y-9/*, oWater, -1, -1*/)) {
2188 if (yVel < -6 && global.config.noDolphin) {
2189 // Spelunky Natural (changed from -4 to -6)
2198 if (colIceBot && status != DUCKING && !global.hasSpikeShoes) {
2204 if (global.config.toggleRunAnywhere) {
2205 if (!kJump && !kDown && !runKey) xVelLimit = 3;
2209 if (platformCharacterIs(ON_GROUND)) {
2210 if (status == RUNNING && kLeft && colLeft) pushTimer += 1;
2211 else if (status == RUNNING && kRight && colRight) pushTimer += 1;
2214 //if (platformCharacterIs(ON_GROUND) && !kJump && !kDown && !runKey) this was commented in the original
2215 if (!kJump && !kDown && !runKey) xVelLimit = 3;
2217 /* this was commented in the original
2219 if (state == DUCKING && fabs(xVel) < 3 && facing == LEFT &&
2220 //collision_point(x, y+9, oSolid, 0, 0) && !collision_point(x-1, y+9, oSolid, 0, 0) && kLeft)
2221 collision_point(x, y+9, oSolid, 0, 0) && !collision_line(x-1, y+9, x-10, y+9, oSolid, 0, 0) && kLeft)
2226 if (kLeft && dir == Dir.Left) dhdir = -1;
2227 else if (kRight && dir == Dir.Right) dhdir = 1;
2229 if (dhdir && status == DUCKING && fabs(xVel) < 3+(dhdir < 0 ? 1 : 0) &&
2230 level.isSolidAtPoint(x, y+9) && !level.checkTilesInRect(x+(dhdir < 0 ? -8 : 1), y+9, 8, 8))
2232 status = DUCKTOHANG;
2234 if (!global.config.scumFlipHold || holdItem.heavy) {
2236 holdItem.heldBy = none;
2237 if (holdItem.objName == 'GoldIdol') holdItem.shiftY(-8);
2239 //else if (holdItem.type == "Block Item") { with (oBlockPreview) instance_destroy(); }
2240 scrDropItem(LostCause.Hang, (dir == Dir.Left ? -1 : 1), -4);
2247 if (status == DUCKTOHANG) {
2248 setXY(xPrev, yPrev);
2258 // parachute and cape
2259 if (!level.inWinCutscene) {
2260 if (isParachuteActive() || isCapeActiveAndOpen()) yFric = 0.5;
2263 if (pushTimer > 100) pushTimer = 100;
2265 // limits the acceleration if it is too extreme
2266 xAcc = fclamp(xAcc, -xAccLimit, xAccLimit);
2267 yAcc = fclamp(yAcc, -yAccLimit, yAccLimit);
2269 // applies the acceleration
2271 if (dead || stunned) yVel += 0.6; else yVel += yAcc;
2273 // nullifies the acceleration
2277 // applies the friction to the velocity, now that the velocity has been calculated
2281 auto oBall = getMyBall();
2282 // apply ball and chain
2284 int distsq = (ix-oBall.ix)*(ix-oBall.ix)+(iy-oBall.iy)*(iy-oBall.iy);
2285 if (distsq >= 24*24) {
2286 if (xVel > 0 && oBall.ix < ix && abs(oBall.ix-ix) > 24) xVel = 0;
2287 if (xVel < 0 && oBall.ix > ix && abs(oBall.ix-ix) > 24) xVel = 0;
2288 if (yVel > 0 && oBall.iy < iy && abs(oBall.iy-iy) > 24) {
2289 if (abs(oBall.ix-ix) < 1) {
2290 //teleportTo(destx:oBall.ix);
2292 prevFltX = oBall.prevFltX;
2294 } else if (oBall.ix < ix && !kRight) {
2295 if (xVel > 0) xVel *= -0.25;
2296 else if (xVel == 0) xVel -= 1;
2297 } else if (oBall.ix > ix && !kLeft) {
2298 if (xVel < 0) xVel *= -0.25;
2299 else if (xVel == 0) xVel += 1;
2304 if (yVel < 0 && oBall.iy > iy && abs(oBall.iy-iy) > 24) yVel = 0;
2308 // apply the limits since the velocity may be too extreme
2309 if (!dead && !stunned) xVel = fclamp(xVel, -xVelLimit, xVelLimit);
2310 yVel = fclamp(yVel, -yVelLimit, yVelLimit);
2312 // approximates the "active" variables
2313 if (fabs(xVel) < 0.0001) xVel = 0;
2314 if (fabs(yVel) < 0.0001) yVel = 0;
2315 if (fabs(xAcc) < 0.0001) xAcc = 0;
2316 if (fabs(yAcc) < 0.0001) yAcc = 0;
2318 bool wasInWall = !!isCollision();
2319 moveRel(xVel, yVel);
2321 // don't go out of level (if we're not in ending sequence)
2322 if (!level.inWinCutscene) {
2323 if (ix < 0) fltx = 0;
2324 else if (ix > level.tilesWidth*16-16) fltx = level.tilesWidth*16-16;
2325 if (iy < 0) flty = 0;
2327 if (!wasInWall && isCollision()) {
2328 writeln("** FUUUU (XXX)");
2329 if (isCollisionBottom(0) && !isCollisionBottom(-2)) {
2332 // we can stuck in the wall with this
2333 if (isCollisionLeft(0)) {
2334 writeln("** FUUUU (001: left)");
2335 while (isCollisionLeft(0) && !isCollisionRight(1)) shiftX(1);
2336 } else if (isCollisionRight(0)) {
2337 writeln("** FUUUU (001: right)");
2338 while (isCollisionRight(0) && !isCollisionLeft(1)) shiftX(-1);
2342 // move out of wall by 1 px, if possible
2343 if (!dead && isCollision()) {
2344 if (isCollisionBottom(0) && !isCollisionBottom(-1)) flty = iy-1;
2345 if (isCollisionTop(0) && !isCollisionTop(1)) flty = iy+1;
2346 if (isCollisionLeft(0) && !isCollisionLeft(1)) fltx = ix+1;
2347 if (isCollisionRight(0) && !isCollisionRight(-1)) fltx = ix-1;
2350 if (isCollision()) {
2351 //k8:HACK: try to duck
2352 bool wallDeath = true;
2353 if (platformCharacterIs(ON_GROUND)) {
2354 setCollisionBounds(-5, -6, 5, 8);
2355 wallDeath = !!isCollision();
2357 setCollisionBounds(-8, -6, 8, 8);
2366 if (isCollision()) {
2367 if (isCollisionLeft(0) && !isCollisionRight(4)) fltx = ix+1;
2368 else if (isCollisionRight(0) && !isCollisionLeft(4)) fltx = ix-1;
2369 else if (isCollisionBottom(0) && !isCollisionTop(4)) flty = iy-1;
2370 else if (isCollisionTop(0) && !isCollisionBottom(4)) flty = iy+1;
2375 if (wallDeath && isCollision()) {
2376 if (!dead) level.addDeath('wall');
2379 writeln("PLAYER KILLED BY WALL");
2380 global.plife = 0; // oops
2386 //writeln("flty=", flty, "; iy=", iy);
2392 // figures out what the sprite index of the character should be
2395 // sets the previous state and the previously previous state
2396 statePrevPrev = statePrev;
2399 // calculates the imageSpeed based on the character's velocity
2400 if (status == RUNNING || status == DUCKING || status == LOOKING_UP) {
2401 if (status == RUNNING || status == LOOKING_UP) imageSpeed = fabs(xVel)*runAnimSpeed+0.1;
2404 if (status == CLIMBING) imageSpeed = sqrt(xVel*xVel+yVel*yVel)*climbAnimSpeed;
2406 if (xVel >= 4 || xVel <= -4) {
2408 if (platformCharacterIs(ON_GROUND)) {
2409 setCollisionBounds(-8, -6, 8, 8);
2411 setCollisionBounds(-5, -6, 5, 8);
2414 setCollisionBounds(-5, -6, 5, 8);
2417 if (whipping) imageSpeed = 1;
2419 if (status == DUCKTOHANG) {
2424 // limit the imageSpeed at 1 so the animation always looks good
2425 if (imageSpeed > 1) imageSpeed = 1;
2427 //if (kItemPressed) writeln("ITEM! dead=", dead, "; stunned=", stunned, "; active=", active);
2428 if (dead || stunned || !active) {
2430 } else if (/*inGame &&*/ kItemPressed && !whipping) {
2432 if (kUp) scrSwitchToStickyBombs(); else scrSwitchToNextItem();
2433 } else if (/*inGame &&*/ kRopePressed && global.rope > 0 && !whipping) {
2434 if (!kDown && colTop) {
2437 launchRope(kDown, doDrop:true);
2439 } else if (/*inGame &&*/ kBombPressed && global.bombs > 0 && !whipping) {
2440 if (holdItem isa ItemWeaponBow && bowArmed) {
2441 if (holdArrow != ARROW_BOMB) {
2442 //writeln("set bow arrows to bomb");
2443 holdArrow = ARROW_BOMB;
2445 //writeln("set bow arrows to normal");
2446 holdArrow = ARROW_NORM;
2455 if (!dead && !stunned && kUp && kAttackPressed) {
2456 auto octr = ItemOpenableContainer(level.isObjectInRect(ix, iy, width, height, delegate bool (MapObject o) {
2457 return (o isa ItemOpenableContainer);
2460 if (octr.openMe()) kAttackPressed = false;
2465 // use weapon / attack
2466 if (!dead && !stunned && kAttackPressed && !holdItem /*&& !pickedItem*/) {
2469 sndStopSound('sndBowPull');
2470 if (status != DUCKING && status != DUCKTOHANG && !whipping && !isExitingSprite()) {
2472 if (global.isTunnelMan) {
2473 if (platformCharacterIs(ON_GROUND) || platformCharacterIs(IN_AIR)) {
2474 setSprite('sTunnelAttackL');
2477 } else if (global.isDamsel) {
2478 setSprite('sDamselAttackL');
2481 setSprite('sAttackLeft');
2484 } else if (kDown && !pickedItem) {
2486 MapObject obj = level.isObjectInRect(x-8, y, 9, 9, delegate bool (MapObject o) {
2487 if (o.spectral || !o.canPickUp) return false;
2488 if (!o.collidesWith(self)) return false;
2489 return o.onCanBePickedUp(self);
2491 if (o isa MapItem) return (o.active && o.canPickUp && !o.spectral);
2492 if (o isa MapEnemy) return (o.active && o.canPickUp && !o.spectral && (o.dead || o.status >= MapObject::STUNNED || o.meGoldMonkey));
2493 if (o isa ObjCharacter) return (o.active && o.canPickUp && !o.spectral);
2498 // `canPickUp` is checked in callback
2499 if (/*obj.canPickUp &&*/ true /*k8: do we really need this? !level.isSolidAtPoint(obj.ix+2, obj.iy)*/) {
2500 //pickupItemType = holdItem.type;
2501 //!if (isAshShotgun(holdItem)) pickupItemType = "Boomstick";
2502 //!if (isGoldMonkey(obj) and obj.status < 98) obj.status = 0; // do not play walk animation while held
2504 if (!obj.onTryPickup(self)) {
2505 if (obj.isInstanceAlive) scrPickupItem(obj);
2509 if (holdItem.type == "Bow" and holdItem.new) {
2510 holdItem.new = false;
2512 if (global.arrows > 99) global.arrows = 99;
2518 } else if (!dead && !stunned) {
2519 if (holdItem isa ItemWeaponBow) {
2520 if (kAttack && bowArmed && bowStrength < 12) {
2522 //writeln("arming: ", bowStrength);
2524 //!!!if (sndIsPlaying('sndBowPull')) sndStopSound('sndBowPull');
2525 sndStopSound('sndBowPull');
2527 if (bowArmed && !kAttack) scrUseItem();
2529 if (!holdArrow) holdArrow = ARROW_NORM;
2531 if (kAttackPressed && holdItem) scrUseItem();
2535 if (!dead && !stunned && kPayPressed) {
2536 // find nearest shopkeeper
2537 auto sc = MonsterShopkeeper(level.findNearestObject(ix, iy, delegate bool (MapObject o) {
2538 auto sc = MonsterShopkeeper(o);
2539 if (!sc) return false;
2540 //if (needCraps && sc.stype != 'Craps') return false;
2541 if (sc.dead || sc.angered) return false;
2542 return sc.canSellItem(self, holdItem);
2544 if (level.isInShop(ix/16, iy/16)) {
2545 // if no shopkeepers found, just use it
2548 holdItem.forSale = false;
2549 holdItem.onTryPickup(self);
2551 } else if (global.thiefLevel == 0 && !global.murderer) {
2552 // only law-abiding players can buy items or play games
2553 if (sc.doSellItem(self, holdItem)) {
2556 holdItem.forSale = false;
2557 holdItem.onTryPickup(self);
2560 if (holdItem && !holdItem.isInstanceAlive) {
2562 scrSwitchToPocketItem(forceIfEmpty:false); // just in case
2566 // use pickup, if any
2567 if (holdItem isa ItemPickup) {
2568 // make nearest shopkeeper angry (an unlikely situation, but still...)
2569 if (sc && holdItem.forSale) level.scrShopkeeperAnger(GameLevel::SCAnger.ItemStolen);
2570 holdItem.forSale = false;
2571 holdItem.onTryPickup(self);
2573 pickupsAround.clear();
2574 level.isObjectInRect(x0, y0, width, height, delegate bool (MapObject o) {
2575 auto pk = ItemPickup(o);
2576 if (pk && pk.collidesWith(self)) {
2578 foreach (auto opk; pickupsAround) if (opk == pk) { found = true; break; }
2579 if (!found) pickupsAround[$] = pk;
2583 // now try to use all pickups
2584 foreach (ItemPickup pk; pickupsAround) {
2585 if (pk.isInstanceAlive) {
2586 if (sc && pk.forSale) level.scrShopkeeperAnger(GameLevel::SCAnger.ItemStolen);
2588 pk.onTryPickup(self);
2591 pickupsAround.clear();
2597 transient array!ItemPickup pickupsAround;
2600 // ////////////////////////////////////////////////////////////////////////// //
2601 override bool initialize () {
2602 if (!::initialize()) return false;
2604 powerups.length = 0;
2605 powerups[$] = SpawnObject(PPParachute);
2606 powerups[$] = SpawnObject(PPCape);
2608 foreach (PlayerPowerup pp; powerups) pp.owner = self;
2610 if (global.isDamsel) {
2612 desc2 = "An athletic, unfittingly-dressed woman with extremely awkward running form.";
2613 setSprite('sDamselLeft');
2614 } else if (global.isTunnelMan) {
2615 desc = "Tunnel Man";
2616 desc2 = "A miner from the desert. His tools are a cut above the rest.";
2617 setSprite('sTunnelLeft');
2620 desc2 = "A strange little man who spends his time exploring caverns. He wants to be just like Indiana Jones when he grows up.";
2621 setSprite('sStandLeft');
2629 switch (global.config.scumClimbSpeed) {
2632 climbAnimSpeed = 0.4;
2637 climbAnimSpeed = 0.45;
2642 climbAnimSpeed = 0.5;
2647 climbAnimSpeed = 0.5;
2651 climbAcc = 0.6; // how fast the character will climb
2652 climbAnimSpeed = 0.4; // relates to how fast the climbing animation should go
2657 // sets the collision bounds to fit the default sprites (you can edit the arguments of the script)
2658 setCollisionBounds(-5, -5, 5, 8); // setCollisionBounds(-5, -8, 5, 8);
2661 statePrevPrev = statePrev;
2662 gravityIntensity = grav; // this variable describes the current force due to gravity (this variable is altered for variable jumping)
2663 jumpTime = jumpTimeTotal; // current time of the jump (0=start of jump, jumpTimeTotal=end of jump)
2669 // ////////////////////////////////////////////////////////////////////////// //
2670 override void onAnimationLooped () {
2671 auto spr = getSprite();
2672 if (spr.Name == 'sAttackLeft' || spr.Name == 'sDamselAttackL' || spr.Name == 'sTunnelAttackL') {
2674 if (holdItem) holdItem.visible = true;
2675 } else if (spr.Name == 'sDuckToHangL' || spr.Name == 'sDamselDtHL' || spr.Name == 'sTunnelDtHL') {
2685 if (dir == Dir.Left) {
2687 obj = level.isAnyLadderAtPoint(x-8, y);
2690 obj = level.isAnyLadderAtPoint(x+8, y);
2695 } else if (dir == Dir.Left) {
2705 } else if (isExitingSprite()) {
2707 //!global.cleanSolids = true;
2712 void activatePlayerWeapon () {
2714 if (holdItem isa PlayerWeapon) {
2715 auto wep = holdItem;
2717 wep.instanceRemove();
2721 if (global.config.unarmed) return;
2723 //TODO: Mattock and Machete
2724 if (holdItem isa PlayerWeapon) {
2725 if (!whipping /+&& holdItem isa /*WeaponWhip*/PlayerWeapon+/) {
2726 auto wep = holdItem;
2728 wep.instanceRemove();
2733 if (holdItem) return;
2735 auto spr = getSprite();
2736 if (spr.Name != 'sAttackLeft' && spr.Name != 'sDamselAttackL' && spr.Name != 'sTunnelAttackL') return;
2737 if (imageFrame > 4) {
2738 //bool hitEnemy = (PlayerWeapon(holdItem) ? PlayerWeapon(holdItem).hitEnemy : false);
2739 if (global.isTunnelMan || pickedItem isa ItemWeaponMattock) {
2740 holdItem = level.MakeMapObject(ix+(dir == Dir.Left ? -16 : 16), iy, 'oMattockHit');
2741 if (imageFrame < 7) playSound('sndWhip');
2742 } else if (pickedItem isa ItemWeaponMachete) {
2743 holdItem = level.MakeMapObject(ix+(dir == Dir.Left ? -16 : 16), iy, 'oSlash');
2744 playSound('sndWhip');
2746 holdItem = level.MakeMapObject(ix+(dir == Dir.Left ? -16 : 16), iy, 'oWhip');
2747 playSound('sndWhip');
2749 /+ not needed anymore
2751 holdItem.active = true;
2752 //if (PlayerWeapon(holdItem)) PlayerWeapon(holdItem).hitEnemy = hitEnemy;
2755 } else if (imageFrame < 2) {
2756 if (global.isTunnelMan || pickedItem isa ItemWeaponMattock) {
2757 holdItem = level.MakeMapObject(ix+(dir == Dir.Left ? -16 : 16), iy, 'oMattockPre');
2758 } else if (pickedItem isa ItemWeaponMachete) {
2759 holdItem = level.MakeMapObject(ix+(dir == Dir.Left ? -16 : 16), iy, 'oMachetePre');
2761 holdItem = level.MakeMapObject(ix+(dir == Dir.Left ? 16 : -16), iy, 'oWhipPre');
2763 /+ not needed anymore
2764 if (holdItem) holdItem.active = true;
2770 if (holdItem.type == "Machete") {
2771 obj = instance_create(x-16, y, oSlash);
2772 obj.sprite_index = sSlashLeft;
2773 playSound(global.sndWhip);
2774 } else if (holdItem.type == "Mattock") {
2775 obj = instance_create(x-16, y, oMattockHit);
2776 obj.sprite_index = sMattockHitL;
2777 if (image_index < 7) playSound(global.sndWhip);
2780 if (global.isTunnelMan) {
2781 obj = instance_create(x-16, y, oMattockHit);
2782 obj.sprite_index = sMattockHitL;
2783 if (image_index < 7) playSound(global.sndWhip);
2785 obj = instance_create(x-16, y, oWhip);
2786 if (global.scumWhipUpgrade == 1) obj.sprite_index = sWhipLongLeft; else obj.sprite_index = sWhipLeft;
2787 playSound(global.sndWhip);
2794 //bool webHit = false;
2796 bool doBreakWebsCB (MapObject o) {
2797 if (o isa ItemWeb) {
2800 if (fabs(xVel) > 1) {
2802 if (!o.dying) ItemWeb(o).life -= 5;
2806 if (fabs(yVel) > 1) {
2808 if (!o.dying) ItemWeb(o).life -= 5;
2818 void initiateExitSequence () {
2819 if (global.isDamsel) setSprite('sDamselExit');
2820 else if (global.isTunnelMan) setSprite('sTunnelExit');
2821 else setSprite('sPExit');
2828 /*k8: the following is done in `GameLevel`
2829 if (global.thiefLevel > 0) global.thiefLevel -= 1;
2830 //orig dbg:if (global.currLevel == 1) global.currLevel += firstLevelSkip; else global.currLevel += levelSkip;
2831 global.currLevel += 1;
2833 playSound('sndSteps');
2837 void processLevelExit () {
2838 if (dead || stunned || whipping) return;
2839 if (!platformCharacterIs(ON_GROUND)) return;
2840 if (isExitingSprite()) return; // just in case
2842 auto hld = holdItem;
2843 if (hld isa PlayerWeapon) return; // oops
2845 //if (!kExitPressed && !hld) return false;
2847 auto door = level.checkTileAtPoint(ix, iy, &level.cbCollisionExitTile);
2848 if (!door || !door.visible) return; // note that `invisible` doors still works
2850 // sell idol, or free damsel
2851 if (hld isa ItemGoldIdol) {
2852 //!if (isRealLevel()) global.idolsConverted += 1;
2853 //not thisglobal.money += hld.value*(global.levelType+1);
2854 ItemGoldIdol(hld).registerConverted();
2855 addScore(hld.value*(global.levelType+1));
2856 //!if (hld.sprite_index == sCrystalSkull) global.skulls += 1; else global.idols += 1;
2857 playSound('sndCoin');
2858 level.MakeMapObject(ix, iy-8, 'oBigCollect');
2860 hld.instanceRemove();
2861 //!with (hld) instance_destroy();
2863 //!pickupItemType = "";
2864 } else if (hld isa MonsterDamsel) {
2866 MonsterDamsel(hld).exitAtDoor(door);
2869 if (!kExitPressed) {
2870 if (!door.invisible) {
2871 string msg = door.getExitMessage();
2872 if (msg.length == 0) {
2873 level.osdMessage(va("PRESS %s TO ENTER.", (global.config.useDoorWithButton ? "$PAY" : "$UP")), -666);
2874 } else if (msg[$-1] != '\n') {
2875 level.osdMessage(va("%s\nPRESS %s TO ENTER.", msg, (global.config.useDoorWithButton ? "$PAY" : "$UP")), -666);
2877 level.osdMessage(msg, -666);
2888 if (isHoldingArmedBomb()) scrUseThrowItem();
2890 if (isHoldingBombOrRope()) scrSwitchToPocketItem(forceIfEmpty:true);
2892 wasHoldingBall = false;
2895 if (hld isa ItemGoldIdol) {
2896 //!if (isRealLevel()) global.idolsConverted += 1;
2897 //not thisglobal.money += hld.value*(global.levelType+1);
2898 ItemGoldIdol(hld).registerConverted();
2899 addScore(hld.value*(global.levelType+1));
2900 //!if (hld.sprite_index == sCrystalSkull) global.skulls += 1; else global.idols += 1;
2901 playSound('sndCoin');
2902 level.MakeMapObject(ix, iy-8, 'oBigCollect');
2904 hld.instanceRemove();
2905 //!with (hld) instance_destroy();
2907 //!pickupItemType = "";
2908 } else if (hld isa MonsterDamsel) {
2910 MonsterDamsel(hld).exitAtDoor(door);
2911 } else if (hld.heavy || hld isa MapEnemy || hld isa ObjCharacter) {
2912 // drop heavy items, characters and enemies (but not ball)
2913 if (hld isa ItemBall) {
2914 wasHoldingBall = true;
2915 removeBallAndChain(temp:true);
2920 // other items are carried thru
2921 if (hld.cannotBeCarriedOnNextLevel) {
2923 holdItem = none; // just in case
2925 scrHideItemToPocket();
2928 global.pickupItem = hld.type;
2929 if (isAshShotgun(hld)) global.pickupItem = "Boomstick";
2931 breakPieces = false;
2935 //scrHideItemToPocket();
2941 //door = instance_place(x, y, oExit); // done above
2942 door.snapToExit(self);
2944 initiateExitSequence();
2946 level.playerExitDoor = door;
2950 override bool onFellInWater (MapTile water) {
2951 level.MakeMapObject(ix, iy-8, 'oSplash');
2953 playSound('sndSplash');
2954 myGrav = 0.2; //k8:???
2958 override bool onOutOfWater () {
2965 // ////////////////////////////////////////////////////////////////////////// //
2966 override void thinkFrame () {
2967 // remove whip, etc. when dead
2968 if (dead && holdItem isa PlayerWeapon) {
2971 pw.instanceRemove();
2972 scrSwitchToPocketItem(forceIfEmpty:false);
2975 setPowerupState('Cape', global.hasCape);
2977 foreach (PlayerPowerup pp; powerups) if (pp.active) pp.onPreThink();
2981 if (redToggle) redColor -= 5;
2982 else if (redColor < 20) redColor += 5;
2983 else redToggle = true;
2988 if (dead) justdied = false;
2991 if (invincible > 0) --invincible;
2997 blinkHidden = !blinkHidden;
3000 blinkHidden = false;
3003 auto spr = getSprite();
3006 cameraBlockX = max(0, cameraBlockX-1);
3007 cameraBlockY = max(0, cameraBlockY-1);
3010 if (spr.Name == 'sWhoaLeft' || spr.Name == 'sDamselWhoaL' || spr.Name == 'sTunnelWhoaL') {
3011 if (whoaTimer > 0) {
3013 } else if (holdItem && onLoosingHeldItem(LostCause.Whoa)) {
3016 if (!hi.onLostAsHeldItem(self, LostCause.Whoa)) {
3020 scrSwitchToPocketItem(forceIfEmpty:true);
3024 whoaTimer = whoaTimerMax;
3028 if (firing > 0) firing -= 1;
3031 auto wtile = level.isWaterAtPoint(x, y/*, oWaterSwim, -1, -1*/);
3034 if (onFellInWater(wtile) || !isInstanceAlive) return;
3038 if (onOutOfWater() || !isInstanceAlive) return;
3044 if (global.randOther(1, 5) == 1) level.MakeMapObject(x-8+global.randOther(4, 12), y-8+global.randOther(4, 12), 'oBurn');
3049 if (!dead && level.isLavaAtPoint(x, y+6/*, oLava, 0, 0*/)) {
3050 //!if (isRealLevel()) global.miscDeaths[11] += 1;
3051 level.addDeath('lava');
3052 playSound('sndFlame');
3066 if (global.hasJetpack && platformCharacterIs(ON_GROUND)) {
3070 // fall off bottom of screen
3071 if (!dead && y > level.tilesHeight*16+16) {
3072 //!if (isRealLevel()) global.miscDeaths[10] += 1;
3073 level.addDeath('void');
3074 global.plife -= 99; // spill blood
3080 scrDropItem(LostCause.Falloff);
3081 playSound('sndThud'); //???
3082 playSound('sndDie'); //???
3085 if (dead && y > level.tilesHeight*16+16) {
3092 if (/*active*/true) {
3093 if (spr.Name == 'sStunL' || spr.Name == 'sDamselStunL' || spr.Name == 'sTunnelStunL') {
3094 if (stunTimer > 0) {
3098 if (stunTimer < 1) {
3100 canDropStuff = true;
3104 if (!level.inWinCutscene) {
3105 if (isParachuteActive() || isCapeActiveAndOpen()) fallTimer = 0;
3108 // changed to yVel > 1 from yVel > 0
3109 if (yVel > 1 && status != CLIMBING) {
3111 if (fallTimer > 16) wallHurt = 0; // no sense in them taking extra damage from being thrown here
3112 int paraOpenHeight = (global.config.scumSpringShoesReduceFallDamage && (global.hasSpringShoes || global.hasJordans) ? 22 : 14);
3113 //paraOpenHeight = 4;
3114 if (global.hasParachute && !stunned && fallTimer > paraOpenHeight) {
3115 //if (not collision_point(x, y+32, oSolid, 0, 0)) // was commented in the original code
3116 //!*if (not collision_line(x, y+16, x, y+32, oSolid, 0, 0))
3117 if (!level.checkTilesInRect(x, y+16, 1, 17, &level.cbCollisionAnySolid)) {
3119 //!instance_create(x-8, y-16, oParachute);
3121 global.hasParachute = false;
3122 activatePowerup('Parachute');
3123 //writeln("parachute state: ", isParachuteActive());
3126 } else if (fallTimer > 16 && platformCharacterIs(ON_GROUND) &&
3127 !level.checkTilesInRect(x-8, y-8, 17, 17, &level.cbCollisionSpringTrap) /* not onto springtrap */)
3129 // long drop -- player has just landed
3130 bool reducedDamage = (global.config.scumSpringShoesReduceFallDamage && (global.hasSpringShoes || global.hasJordans));
3131 if (reducedDamage && fallTimer <= 24) {
3132 // land without taking damage
3136 if (fallTimer > (reducedDamage ? 72 : 48)) global.plife -= 10*global.config.scumFallDamage;
3137 else if (fallTimer > (reducedDamage ? 48 : 32)) global.plife -= 2*global.config.scumFallDamage;
3138 else global.plife -= 1*global.config.scumFallDamage;
3139 if (global.plife < 1) {
3140 if (!dead) level.addDeath('fall');
3141 scrCreateBlood(x, y, 3);
3142 //!if (isRealLevel()) global.miscDeaths[3] += 1;
3145 if (global.config.scumFallDamage > 0) stunTimer += 60;
3148 auto obj = level.MakeMapObject(x-4, y+6, 'oPoof');
3149 if (obj) obj.xVel = -0.4;
3150 obj = level.MakeMapObject(x+4, y+6, 'oPoof');
3151 if (obj) obj.xVel = 0.4;
3152 playSound('sndThud');
3154 } else if (yVel <= 0) {
3156 if (isParachuteActive()) {
3157 deactivatePowerup('Parachute');
3158 level.MakeMapObject(ix-8, iy-16-8, 'oParaUsed');
3162 // if (stunned) fallTimer = 0; // was commented in the original code
3164 if (swimming && !level.isLavaAtPoint(x, y/*, oLava, 0, 0*/)) {
3166 if (bubbleTimer > 0) {
3169 level.MakeMapObject(x, y-4, 'oBubble');
3170 bubbleTimer = bubbleTimerMax;
3173 bubbleTimer = bubbleTimerMax;
3176 //TODO: k8: move spear checking to spear handler
3177 if (!isExitingSprite()) {
3178 auto spear = MapObjectSpearsBase(level.isObjectInRect(ix-6, iy-6, 13, 14, delegate bool (MapObject o) {
3179 auto tt = MapObjectSpearsBase(o);
3180 if (!tt) return false;
3181 return tt.isHitFrame;
3186 global.plife -= global.config.spearDmg; // 4
3187 if (!dead && global.plife <= 0 /*and isRealLevel()*/) level.addDeath('spear');
3188 xVel = global.randOther(4, 6)*(spear.isLeft ? -1 : 1);
3193 scrCreateBlood(x, y, 1);
3197 if (status != DUCKTOHANG && !stunned && !dead && !isExitingSprite()) {
3199 characterStepEvent();
3201 if (status != DUCKING && status != DUCKTOHANG) status = STANDING;
3202 checkControlKeys(getSprite());
3206 // if (dead or stunned)
3207 if (dead || stunned) {
3209 if (holdItem isa ItemWeaponBow && bowArmed) scrFireBow();
3210 scrDropItem(dead ? LostCause.Dead : LostCause.Stunned, xVel, -3);
3213 yVel += (bounced ? 1.0 : 0.6);
3215 if (isCollisionTop(1) && yVel < 0) yVel = -yVel*0.8;
3216 if (isCollisionLeft(1) || isCollisionRight(1)) xVel = -xVel*0.5;
3218 bool collisionbottomcheck = !!isCollisionBottom(1);
3219 if (collisionbottomcheck || isCollisionBottom(1, &level.cbCollisionPlatform)) {
3221 if (collisionbottomcheck) {
3222 if (yVel > 2.5) yVel = -yVel*0.5; else yVel = 0;
3224 // after falling onto a platform don't take extra damage after recovering from stunning
3227 /* was commented in the original code
3228 if (isCollisionBottom(1)) {
3229 if (yVel > 2.5) yVel = -yVel*0.5; else yVel = 0;
3236 if (fabs(xVel) < 0.1) xVel = 0;
3237 else if (fabs(xVel) != 0 && level.isIceAtPoint(x, y+16)) xVel *= 0.8;
3238 else if (fabs(xVel) != 0) xVel *= 0.3;
3244 //level.forEachObjectInRect(ix, iy, width, height, &doBreakWebsCB);
3246 // apply the limits since the velocity may be too extreme
3248 xVel = fclamp(xVel, -xVelLimit, xVelLimit);
3249 yVel = fclamp(yVel, -yVelLimit, yVelLimit);
3251 moveRel(xVel, yVel);
3255 // fix sprites, spawn blood from spikes
3256 if (isParachuteActive()) {
3257 deactivatePowerup('Parachute');
3258 level.MakeMapObject(ix-8, iy-16-8, 'oParaUsed');
3263 //!with (oWhip) instance_destroy();
3266 if (global.isDamsel) {
3268 if (dead) setSprite('sDamselDieL');
3269 else if (stunned) setSprite('sDamselStunL');
3270 } else if (bounced) {
3271 if (yVel < 0) setSprite('sDamselBounceL'); else setSprite('sDamselFallL');
3273 if (xVel < 0) setSprite('sDamselDieLL'); else setSprite('sDamselDieLR');
3275 } else if (global.isTunnelMan) {
3277 if (dead) setSprite('sTunnelDieL');
3278 else if (stunned) setSprite('sTunnelStunL');
3279 } else if (bounced) {
3280 if (yVel < 0) setSprite('sTunnelLBounce'); else setSprite('sTunnelFallL');
3282 if (xVel < 0) setSprite('sTunnelDieLL'); else setSprite('sTunnelDieLR');
3286 if (dead) setSprite('sDieL');
3287 else if (stunned) setSprite('sStunL');
3288 } else if (bounced) {
3289 if (yVel < 0) setSprite('sDieLBounce'); else setSprite('sDieLFall');
3291 if (xVel < 0) setSprite('sDieLL'); else setSprite('sDieLR');
3299 if (dead && justdied && yVel != 0) {
3300 auto spk = level.checkTileAtPoint(x, y, &level.cbCollisionSpikes);
3301 if (spk && global.randOther(1, 8) == 1) scrCreateBlood(spk.x0, spk.y0, 1);
3305 auto colobj = isCollisionRight(1);
3306 if (!colobj) colobj = isCollisionLeft(1);
3307 if (!colobj) colobj = isCollisionBottom(1);
3310 foreach (; 0..3) level.MakeMapObject(colobj.x0, colobj.y0, 'oBlood');
3312 if (!dead && global.plife <= 0 /*&& isRealLevel()*/) {
3314 writeln("thrown to death by '", thrownBy, "'");
3315 level.addDeath(thrownBy);
3319 if (wallHurt <= 0) thrownBy = '';
3320 playSound('sndHurt'); //???
3324 colobj = isCollisionBottom(1);
3325 if (colobj && !bounced) {
3327 foreach (; 0..3) level.MakeMapObject(colobj.x0, colobj.y0, 'oBlood');
3330 if (!dead && global.plife <= 0 /*and isRealLevel()*/) {
3332 writeln("thrown to death by '", thrownBy, "'");
3333 level.addDeath(thrownBy);
3337 if (wallHurt <= 0) thrownBy = '';
3342 bool kPay = level.isKeyDown(GameConfig::Key.Pay);
3344 // gnounc's quick look
3345 if (!kRight && !kLeft && (platformCharacterIs(ON_GROUND) || status == HANGING)) {
3346 if (kDown) { if (viewCount <= 6) viewCount += 3; else viewOffset += 6; }
3347 else if (kUp) { if (viewCount <= 6) viewCount += 3; else viewOffset -= 6; }
3353 // default look up/down with delay if pay button not held
3354 if (!kRight && !kLeft && (platformCharacterIs(ON_GROUND) || status == HANGING)) {
3355 if (kDown) { if (viewCount <= 30) viewCount += 1; else viewOffset += 4; }
3356 else if (kUp) { if (viewCount <= 30) viewCount += 1; else viewOffset -= 4; }
3363 if (viewCount == 0 && viewOffset) viewOffset = (viewOffset < 0 ? min(0, viewOffset+8) : max(0, viewOffset-8));
3364 viewOffset = clamp(viewOffset, -16*6, 16*6);
3366 if (!dead) activatePlayerWeapon();
3368 if (!dead) processLevelExit();
3371 if (global.plife < -99 && visible && justdied) scrCreateBlood(x, y, 3);
3373 if (global.plife < 1) {
3377 // spikes, and other shit
3378 if (global.plife >= -99 && visible && !isExitingSprite()) {
3379 auto colSpikes = level.checkTilesInRect(x-4, y-4, 9, 13, &level.cbCollisionSpikes);
3381 if (colSpikes && dead) {
3383 if (!level.isSolidAtPoint(x, y+9)) { shiftY(0.02); y = iy; } //0.05;
3384 //else myGrav = 0.6;
3389 if (colSpikes && yVel > 0 && (fallTimer > 3 || stunned)) { // originally fallTimer > 4
3391 scrCreateBlood(ix, iy, 3);
3392 // spikes will always instant-kill in Moon room
3393 /*if (isRoom("rMoon")) global.plife -= 99; else*/ global.plife -= global.config.scumSpikeDamage;
3394 if (/*isRealLevel() &&*/ global.plife <= 0) level.addDeath('spike');
3395 if (global.plife > 0) playSound('sndHurt');
3400 colSpikes.makeBloody();
3402 //else if (not dead) myGrav = 0.6;
3407 if (visible && (status >= STUNNED || stunned || dead || status == DUCKING)) {
3408 /*if (*/checkAndPerformSacrifice()/*) return*/;
3410 sacCount = default.sacCount;
3414 if (dead && global.hasAnkh) {
3415 writeln("*** ACTIVATED ANKH");
3416 global.hasAnkh = false;
3418 int newLife = (global.isTunnelMan ? global.config.scumTMLife : global.config.scumStartLife);
3419 global.plife = max(global.plife, newLife);
3420 level.osdMessage("THE ANKH SHATTERS!\nYOU HAVE BEEN REVIVED!", 4);
3422 auto moai = level.forEachTile(delegate bool (MapTile t) { return (t.objType == 'oMoai'); });
3424 level.forEachTile(delegate bool (MapTile t) {
3425 if (t.objType == 'oMoaiInside') {
3426 teleportTo(t.ix+8, t.iy+8);
3431 //teleportTo(moai.ix+16+8, moai.iy+16+8);
3433 if (level.allEnters.length) {
3434 teleportTo(level.allEnters[0].ix+8, level.allEnters[0].iy-8);
3437 level.centerViewAtPlayer();
3438 auto ball = getMyBall();
3439 if (ball) ball.teleportToPrisoner();
3452 //alarm[8] = 60; // this starts music; but we don't need it, 'cause we won't stop the music on player death
3453 playSound('sndTeleport');
3457 if (dead) level.stats.gameOver();
3460 if (status == DUCKTOHANG) {
3462 if (spr.Name != 'sDuckToHangL' && spr.Name != 'sDamselDtHL' && spr.Name != 'sTunnelDtHL') status = STANDING;
3465 foreach (PlayerPowerup pp; powerups) if (pp.active) pp.onPostThink();
3467 if (jetpackFlaresTime > 0) {
3468 if (--jetpackFlaresTime == 0) {
3469 auto obj = level.MakeMapObject(ix+global.randOther(0, 3)-global.randOther(0, 3), iy+global.randOther(0, 3)-global.randOther(0, 3), 'oFlareSpark');
3471 obj.yVel = global.randOther(1, 3);
3472 obj.xVel = global.randOther(0, 3)-global.randOther(0, 3);
3474 playSound('sndJetpack');
3480 // ////////////////////////////////////////////////////////////////////////// //
3481 override void drawWithOfs (int xpos, int ypos, int scale, float currFrameDelta) {
3482 //if (heldBy) return; // owner will take care of this
3483 if (blinkHidden) return;
3485 bool renderJetpackBack = false;
3486 if (global.hasJetpack) {
3488 if ((status == CLIMBING || isExitingSprite()) && !whipping) {
3490 renderJetpackBack = true;
3493 getInterpCoords(currFrameDelta, scale, out xi, out yi);
3496 if (dir == Dir.Right) {
3497 spr = level.sprStore['sJetpackRight'];
3500 spr = level.sprStore['sJetpackLeft'];
3504 auto spf = spr.frames[0];
3505 if (spf && spf.width > 0 && spf.height > 0) spf.tex.blitAt(xi-xpos-spf.xofs*scale, yi-ypos-spf.yofs*scale, scale);
3510 foreach (PlayerPowerup pp; powerups) if (pp.active) pp.preDrawWithOfs(xpos, ypos, scale, currFrameDelta);
3512 auto oldColor = Video.color;
3513 if (redColor > 0) Video.color = clamp(200+redColor, 0, 255)<<16;
3514 ::drawWithOfs(xpos, ypos, scale, currFrameDelta);
3515 Video.color = oldColor;
3517 if (renderJetpackBack) {
3519 getInterpCoords(currFrameDelta, scale, out xi, out yi);
3520 SpriteImage spr = level.sprStore['sJetpackBack'];
3522 auto spf = spr.frames[0];
3523 if (spf && spf.width > 0 && spf.height > 0) spf.tex.blitAt(xi-xpos-spf.xofs*scale, yi-ypos-spf.yofs*scale, scale);
3527 foreach (PlayerPowerup pp; powerups) if (pp.active) pp.postDrawWithOfs(xpos, ypos, scale, currFrameDelta);
3531 getInterpCoords(currFrameDelta, scale, out xi, out yi);
3535 auto spf = getSpriteFrame(out doMirror);
3536 int spry0 = yi-spf.yofs*scale-ypos;
3538 int sprx0 = xi-spf.xofs*scale-xpos;
3539 spf.tex.blitAt(sprx0, spry0, scale);
3542 int sprx0 = xi+spf.xofs*scale-xpos;
3543 spf.tex.blitExt(sprx0, spry0, sprx0-spf.tex.width*scale, spry0+spf.tex.height*scale, 0, 0, spf.tex.width, spf.tex.height);
3547 auto oclr = Video.color;
3548 Video.color = 0xff_ff_00;
3549 Video.drawRect(x0*scale-xpos, y0*scale-ypos, width*scale, height*scale);
3550 Video.color = 0x00_ff_00;
3551 Video.drawRect(ix*scale-xpos, iy*scale-ypos, 2, 2);
3553 if (isCollision()) {
3554 Video.color = 0x3f_ff_00_00;
3555 Video.fillRect(x0*scale-xpos, y0*scale-ypos, width*scale, height*scale);
3564 void lastDrawWithOfs (int xpos, int ypos, int scale, float currFrameDelta) {
3565 foreach (PlayerPowerup pp; powerups) if (pp.active) pp.lastDrawWithOfs(xpos, ypos, scale, currFrameDelta);
3571 if (redColor > 0) draw_sprite_ext(sprite_index, -1, x, y, image_xscale, image_yscale, image_angle, make_color_rgb(200+redColor, 0, 0), image_alpha);
3572 else if (greenColor > 0) draw_sprite_ext(sprite_index, -1, x, y, image_xscale, image_yscale, image_angle, make_color_rgb(round(103+greenColor), round(161+greenColor), 0), image_alpha);
3573 else draw_sprite_ext(sprite_index, -1, x, y, image_xscale, image_yscale, image_angle, image_blend, image_alpha);
3575 if (facing == RIGHT) {
3576 if (holdArrow == ARROW_NORM) {
3577 draw_sprite(sArrowRight, -1, x+4, y+1);
3578 } else if (holdArrow == ARROW_BOMB) {
3579 if (holdArrowToggle) draw_sprite(sBombArrowRight, 0, x+4, y+2); else draw_sprite(sBombArrowRight, 1, x+4, y+2);
3581 } else if (facing == LEFT) {
3582 if (holdArrow == ARROW_NORM) {
3583 draw_sprite(sArrowLeft, -1, x-4, y+1);
3584 } else if (holdArrow == ARROW_BOMB) {
3585 if (holdArrowToggle) draw_sprite(sBombArrowLeft, 0, x-4, y+2); else draw_sprite(sBombArrowLeft, 1, x-4, y+2);
3593 objType = 'oPlayer';
3596 desc2 = "A strange little man who spends his time exploring caverns. He wants to be just like Indiana Jones when he grows up.";
3598 negateMirrorXOfs = true;
3600 status = FALLING; // the character state, must be one of the following: STANDING, RUNNING, DUCKING, LOOKING_UP, CLIMBING, JUMPING, or FALLING
3610 //thrownBy = ""; // "Yeti", "Hawkman", or "Shopkeeper" for stat tracking deaths by being thrown
3613 //whoaTimerMax = 30;
3614 distToNearestLightSource = 999;
3624 frictionFactor = 0.3;
3626 xVelLimit = 16; // limits the xVel: default 15
3627 yVelLimit = 10; // limits the yVel
3628 xAccLimit = 9; // limits the xAcc
3629 yAccLimit = 6; // limits the yAcc
3630 runAcc = 3; // the running acceleration
3635 //lightRadius = 96; //???