1 /**********************************************************************************
2 * Copyright (c) 2008, 2009 Derek Yu and Mossmouth, LLC
3 * Copyright (c) 2010, Moloch
4 * Copyright (c) 2018, Ketmar Dark
6 * This file is part of Spelunky.
8 * You can redistribute and/or modify Spelunky, including its source code, under
9 * the terms of the Spelunky User License.
11 * Spelunky is distributed in the hope that it will be entertaining and useful,
12 * but WITHOUT WARRANTY. Please see the Spelunky User License for more details.
14 * The Spelunky User License should be available in "Game .Information", which
15 * can be found in the Resource Explorer, or as an external file called COPYING.
16 * If not, please obtain a new copy of Spelunky from <http://spelunkyworld.com/>
18 **********************************************************************************/
19 // this is the level we're playing in, with all objects and tiles
20 class GameLevel : Object;
22 //#define EXPERIMENTAL_RENDER_CACHE
24 const float FrameTime = 1.0f/30.0f;
26 const int dumpGridStats = true;
33 //enum NormalTilesWidth = LevelGen::LevelWidth*RoomGen::Width+2;
34 //enum NormalTilesHeight = LevelGen::LevelHeight*RoomGen::Height+2;
36 enum MaxTilesWidth = 64;
37 enum MaxTilesHeight = 64;
40 transient GameStats stats;
41 transient SpriteStore sprStore;
42 transient BackTileStore bgtileStore;
43 transient BackTileImage levBGImg;
46 transient name lastMusicName;
47 //RoomGen[LevelGen::LevelWidth, LevelGen::LevelHeight] rooms; // moved to levelgen
49 transient float accumTime;
50 transient bool gamePaused = false;
51 transient bool gameShowHelp = false;
52 transient int gameHelpScreen = 0;
53 const int MaxGameHelpScreen = 2;
54 transient bool checkWater;
55 transient int liquidTileCount; // cached
56 /*transient*/ int damselSaved;
60 transient int collectCounter;
61 /*transient*/ int levelMoneyStart;
63 // all movable (thinkable) map objects
64 EntityGrid objGrid; // monsters, items and tiles
66 MapBackTile backtiles;
67 bool blockWaterChecking;
71 bool cameFromIntroRoom; // for title screen
73 LevelGen::RType[MaxTilesWidth, MaxTilesHeight] roomType;
87 LevelKind levelKind = LevelKind.Normal;
89 array!MapTile allEnters;
90 array!MapTile allExits;
93 int startRoomX, startRoomY;
94 int endRoomX, endRoomY;
97 transient bool playerExited;
98 transient MapEntity playerExitDoor;
99 transient bool disablePlayerThink = false;
100 transient int maxPlayingTime; // in seconds
106 bool ghostSpawned; // to speed up some checks
107 bool resetBMCOG = false;
111 // FPS, i.e. incremented by 30 in one second
112 int time; // in frames
113 int lastUsedObjectId;
114 transient int lastRenderTime = -1;
115 transient int pausedTime;
117 MapEntity deadItemsHead;
119 // screen shake variables
124 // set this before calling `fixCamera()`
125 // dimensions should be real, not scaled up/down
126 transient int viewWidth, viewHeight;
127 //transient int viewOffsetX, viewOffsetY;
129 // room bounds, not scaled
130 IVec2D viewMin, viewMax;
132 // for Olmec level cinematics
133 IVec2D cameraSlideToDest;
134 IVec2D cameraSlideToCurr;
135 IVec2D cameraSlideToSpeed; // !0: slide
136 int cameraSlideToPlayer;
137 // `fixCamera()` will set the following
138 // coordinates will be real too (with scale applied)
139 // shake is not applied
140 transient IVec2D viewStart; // with `player.viewOffset`
141 private transient IVec2D realViewStart; // without `player.viewOffset`
143 transient int framesProcessedFromLastClear;
145 transient int BuildYear;
146 transient int BuildMonth;
147 transient int BuildDay;
148 transient int BuildHour;
149 transient int BuildMin;
150 transient string BuildDateString;
153 final string getBuildDateString () {
154 if (!BuildYear) return BuildDateString;
155 if (BuildDateString) return BuildDateString;
156 BuildDateString = va("%d-%02d-%02d %02d:%02d", BuildYear, BuildMonth, BuildDay, BuildHour, BuildMin);
157 return BuildDateString;
161 final void cameraSlideToPoint (int dx, int dy, int speedx, int speedy) {
162 cameraSlideToPlayer = 0;
163 cameraSlideToDest.x = dx;
164 cameraSlideToDest.y = dy;
165 cameraSlideToSpeed.x = abs(speedx);
166 cameraSlideToSpeed.y = abs(speedy);
167 cameraSlideToCurr.x = cameraCurrX;
168 cameraSlideToCurr.y = cameraCurrY;
172 final void cameraReturnToPlayer () {
173 if (!cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y)) {
174 cameraSlideToCurr.x = cameraCurrX;
175 cameraSlideToCurr.y = cameraCurrY;
176 if (cameraSlideToSpeed.x && abs(cameraSlideToSpeed.x) < 8) cameraSlideToSpeed.x = 8;
177 if (cameraSlideToSpeed.y && abs(cameraSlideToSpeed.y) < 8) cameraSlideToSpeed.y = 8;
178 cameraSlideToPlayer = 1;
183 // if `frameSkip` is `true`, there are more frames waiting
184 // (i.e. you may skip rendering and such)
185 transient void delegate (bool frameSkip) onBeforeFrame;
186 transient void delegate (bool frameSkip) onAfterFrame;
188 transient void delegate () onCameraTeleported;
190 transient void delegate () onLevelExitedCB;
192 // this will be called in-between frames, and
193 // `frameTime` is [0..1)
194 transient void delegate (float frameTime) onInterFrame;
196 final int bizRoomStyle { get { return (lg ? lg.bizRoomStyle : 0); } }
199 final bool isNormalLevel () { return (levelKind == LevelKind.Normal); }
200 final bool isTitleRoom () { return (levelKind == LevelKind.Title); }
201 final bool isTutorialRoom () { return (levelKind == LevelKind.Tutorial); }
202 final bool isTransitionRoom () { return (levelKind == LevelKind.Transition); }
203 final bool isIntroRoom () { return (levelKind == LevelKind.Transition); }
206 bool isHUDEnabled () {
207 if (inWinCutscene) return false;
208 if (inIntroCutscene) return false;
209 if (lg.finalBossLevel) return true;
210 if (isNormalLevel()) return true;
215 // ////////////////////////////////////////////////////////////////////////// //
217 void addDeath (name aname) { if (isNormalLevel()) stats.addDeath(aname); }
224 void addKill (name aname, optional bool telefrag) {
225 if (isNormalLevel()) stats.addKill(aname, telefrag!optional);
226 else if (aname == 'Shopkeeper' && levelKind == LevelKind.Stars) { ++stats.starsKills; ++starsKills; }
229 void addCollect (name aname, optional int amount) { if (isNormalLevel()) stats.addCollect(aname, amount!optional); }
231 void addDamselSaved () { if (isNormalLevel()) stats.addDamselSaved(); }
232 void addIdolStolen () { if (isNormalLevel()) stats.addIdolStolen(); }
233 void addIdolConverted () { if (isNormalLevel()) stats.addIdolConverted(); }
234 void addCrystalIdolStolen () { if (isNormalLevel()) stats.addCrystalIdolStolen(); }
235 void addCrystalIdolConverted () { if (isNormalLevel()) stats.addCrystalIdolConverted(); }
236 void addGhostSummoned () { if (isNormalLevel()) stats.addGhostSummoned(); }
239 // ////////////////////////////////////////////////////////////////////////// //
240 static final string time2str (int time) {
241 int secs = time%60; time /= 60;
242 int mins = time%60; time /= 60;
243 int hours = time%24; time /= 24;
245 if (days) return va("%d DAYS, %d:%02d:%02d", days, hours, mins, secs);
246 if (hours) return va("%d:%02d:%02d", hours, mins, secs);
247 return va("%02d:%02d", mins, secs);
251 // ////////////////////////////////////////////////////////////////////////// //
252 final int tilesWidth () { return lg.levelRoomWidth*RoomGen::Width+2; }
253 final int tilesHeight () { return (lg.finalBossLevel ? 55 : lg.levelRoomHeight*RoomGen::Height+2); }
256 // ////////////////////////////////////////////////////////////////////////// //
257 protected void resetGameInternal () {
258 if (player) player.removeBallAndChain();
261 //inIntroCutscene = 0;
273 player.removeBallAndChain();
274 auto hi = player.holdItem;
275 player.holdItem = none;
276 if (hi) hi.instanceRemove();
277 hi = player.pickedItem;
278 player.pickedItem = none;
279 if (hi) hi.instanceRemove();
286 stats.clearGameTotals();
290 // this won't generate a level yet
291 void restartGame () {
293 if (global.startMoney > 0) stats.setMoneyCheat();
294 stats.setMoney(global.startMoney);
295 levelKind = LevelKind.Normal;
299 // complement function to `restart game`
300 void generateNormalLevel () {
302 centerViewAtPlayer();
306 void restartTitle () {
309 createSpecialLevel(LevelKind.Title, &createTitleRoom, 'musTitle');
318 void restartIntro () {
321 createSpecialLevel(LevelKind.Intro, &createIntroRoom, '');
330 void restartTutorial () {
333 createSpecialLevel(LevelKind.Tutorial, &createTutorialRoom, 'musCave');
342 void restartScores () {
345 createSpecialLevel(LevelKind.Scores, &createScoresRoom, 'musTitle');
354 void restartStarsRoom () {
357 createSpecialLevel(LevelKind.Stars, &createStarsRoom, '');
366 void restartSunRoom () {
369 createSpecialLevel(LevelKind.Sun, &createSunRoom, '');
378 void restartMoonRoom () {
381 createSpecialLevel(LevelKind.Moon, &createMoonRoom, '');
390 // ////////////////////////////////////////////////////////////////////////// //
391 // generate angry shopkeeper at exit if murderer or thief
392 void generateAngryShopkeepers () {
393 if (global.murderer || global.thiefLevel > 0) {
394 foreach (MapTile e; allExits) {
395 auto obj = MonsterShopkeeper(MakeMapObject(e.ix, e.iy, 'oShopkeeper'));
397 obj.style = 'Bounty Hunter';
398 obj.status = MapObject::PATROL;
405 // ////////////////////////////////////////////////////////////////////////// //
406 final void resetRoomBounds () {
409 viewMax.x = tilesWidth*16;
410 viewMax.y = tilesHeight*16;
411 // Great Lake is bottomless (nope)
412 //if (global.lake == 1) viewMax.y -= 16;
413 //writeln("ROOM BOUNDS: (", viewMax.x, "x", viewMax.y, ")");
417 final void setRoomBounds (int x0, int y0, int x1, int y1) {
425 // ////////////////////////////////////////////////////////////////////////// //
428 float timeout; // seconds
429 float starttime; // for active
430 bool active; // true: timeout is `GetTickCount()` dismissing time
433 array!OSDMessage msglist; // [0]: current one
435 struct OSDMessageTalk {
437 float timeout; // seconds;
438 float starttime; // for active
439 bool active; // true: timeout is `GetTickCount()` dismissing time
440 bool shopOnly; // true: timeout when player exited the shop
441 int hiColor1; // -1: default
442 int hiColor2; // -1: default
445 array!OSDMessageTalk msgtalklist; // [0]: current one
448 private final void osdCheckTimeouts () {
449 auto stt = GetTickCount();
450 while (msglist.length) {
451 if (!msglist[0].msg) { msglist.remove(0); continue; }
452 if (!msglist[0].active) {
453 msglist[0].active = true;
454 msglist[0].starttime = stt;
456 if (msglist[0].starttime+msglist[0].timeout >= stt) break;
459 if (msgtalklist.length) {
460 bool inshop = isInShop(player.ix/16, player.iy/16);
461 while (msgtalklist.length) {
462 if (!msgtalklist[0].msg) { msgtalklist.remove(0); continue; }
463 if (msgtalklist[0].shopOnly) {
464 if (inshop == msgtalklist[0].active) {
465 msgtalklist[0].active = !inshop;
466 if (!inshop) msgtalklist[0].starttime = stt;
469 if (!msgtalklist[0].active) {
470 msgtalklist[0].active = true;
471 msgtalklist[0].starttime = stt;
474 if (!msgtalklist[0].active) break;
475 //writeln("timedelta: ", msgtalklist[0].starttime+msgtalklist[0].timeout-stt);
476 if (msgtalklist[0].starttime+msgtalklist[0].timeout >= stt) break;
477 msgtalklist.remove(0);
483 final bool osdHasMessage () {
485 return (msglist.length > 0);
489 final string osdGetMessage (out float timeLeft, out float timeStart) {
491 if (msglist.length == 0) { timeLeft = 0; return ""; }
492 auto stt = GetTickCount();
493 timeStart = msglist[0].starttime;
494 timeLeft = fmax(0, msglist[0].starttime+msglist[0].timeout-stt);
495 return msglist[0].msg;
499 final string osdGetTalkMessage (optional out int hiColor1, optional out int hiColor2) {
501 if (msgtalklist.length == 0) return "";
502 hiColor1 = msgtalklist[0].hiColor1;
503 hiColor2 = msgtalklist[0].hiColor2;
504 return msgtalklist[0].msg;
508 final void osdClear (optional bool clearTalk) {
510 if (clearTalk) msgtalklist.clear();
514 final void osdMessage (string msg, optional float timeout, optional bool urgent) {
516 msg = global.expandString(msg);
517 if (!specified_timeout) timeout = 3.33;
518 // special message for shops
519 if (timeout == -666) {
521 if (msglist.length && msglist[0].msg == msg) return;
522 if (msglist.length == 0 || msglist[0].msg != msg) {
523 osdClear(clearTalk:false);
525 msglist[0].msg = msg;
527 msglist[0].active = false;
528 msglist[0].timeout = 3.33;
532 if (timeout < 0.1) return;
533 timeout = fmax(1.0, timeout);
534 //writeln("OSD: ", msg);
535 // find existing one, and bring it to the top
537 for (; oldidx < msglist.length; ++oldidx) {
538 if (msglist[oldidx].msg == msg) break; // i found her!
541 if (oldidx < msglist.length) {
542 // yeah, move duplicate to the top
543 msglist[oldidx].timeout = fmax(timeout, msglist[oldidx].timeout);
544 msglist[oldidx].active = false;
545 if (urgent && oldidx != 0) {
546 timeout = msglist[oldidx].timeout;
547 msglist.remove(oldidx);
549 msglist[0].msg = msg;
550 msglist[0].timeout = timeout;
551 msglist[0].active = false;
555 msglist[0].msg = msg;
556 msglist[0].timeout = timeout;
557 msglist[0].active = false;
561 msglist[$-1].msg = msg;
562 msglist[$-1].timeout = timeout;
563 msglist[$-1].active = false;
569 void osdMessageTalk (string msg, optional bool replace, optional float timeout, optional bool inShopOnly,
570 optional int hiColor1, optional int hiColor2)
573 //writeln("talk msg: replace=", replace, "; timeout=", timeout, "; inshop=", inShopOnly, "; msg=", msg);
574 if (!specified_timeout) timeout = 3.33;
575 if (!specified_inShopOnly) inShopOnly = true;
576 if (!specified_hiColor1) hiColor1 = -1;
577 if (!specified_hiColor2) hiColor2 = -1;
578 msg = global.expandString(msg);
580 if (!msg) { msgtalklist.clear(); return; }
581 if (msgtalklist.length && msgtalklist[0].msg == msg) {
582 while (msgtalklist.length > 1) msgtalklist.remove(1);
583 msgtalklist[$-1].timeout = timeout;
584 msgtalklist[$-1].shopOnly = inShopOnly;
586 if (msgtalklist.length) msgtalklist.clear();
587 msgtalklist.length += 1;
588 msgtalklist[$-1].msg = msg;
589 msgtalklist[$-1].timeout = timeout;
590 msgtalklist[$-1].active = false;
591 msgtalklist[$-1].shopOnly = inShopOnly;
592 msgtalklist[$-1].hiColor1 = hiColor1;
593 msgtalklist[$-1].hiColor2 = hiColor2;
598 foreach (auto midx, ref auto mnfo; msgtalklist) {
599 if (mnfo.msg == msg) {
600 mnfo.timeout = timeout;
601 mnfo.shopOnly = inShopOnly;
606 msgtalklist.length += 1;
607 msgtalklist[$-1].msg = msg;
608 msgtalklist[$-1].timeout = timeout;
609 msgtalklist[$-1].active = false;
610 msgtalklist[$-1].shopOnly = inShopOnly;
611 msgtalklist[$-1].hiColor1 = hiColor1;
612 msgtalklist[$-1].hiColor2 = hiColor2;
619 // ////////////////////////////////////////////////////////////////////////// //
620 void setup (GameGlobal aGlobal, SpriteStore aSprStore, BackTileStore aBGTileStore) {
622 sprStore = aSprStore;
623 bgtileStore = aBGTileStore;
625 lg = SpawnObject(LevelGen);
629 objGrid = SpawnObject(EntityGrid);
630 objGrid.setup(0, 0, MaxTilesWidth*16, MaxTilesHeight*16);
634 // stores should be set
638 levBGImg = bgtileStore[levBGImgName];
639 foreach (MapEntity o; objGrid.allObjects()) {
642 if (t && (t.lava || t.water)) ++liquidTileCount;
644 for (MapBackTile bt = backtiles; bt; bt = bt.next) bt.onLoaded();
645 if (player) player.onLoaded();
647 if (msglist.length) {
648 msglist[0].active = false;
649 msglist[0].timeout = 0.200;
652 lastMusicName = (lg ? lg.musicName : '');
653 global.setMusicPitch(1.0);
654 if (lg && lg.musicName) global.playMusic(lg.musicName); else global.stopMusic();
658 // ////////////////////////////////////////////////////////////////////////// //
659 void pickedSpectacles () {
660 foreach (MapTile t; objGrid.allObjectsSafe(MapTile)) t.onGotSpectacles();
664 // ////////////////////////////////////////////////////////////////////////// //
665 #include "rgentile.vc"
666 #include "rgenobj.vc"
669 void onLevelExited () {
670 if (playerExitDoor isa TitleTileXTitle) {
671 playerExitDoor = none;
676 if (isTitleRoom() || levelKind == LevelKind.Scores) {
677 if (playerExitDoor) processTitleExit(playerExitDoor);
678 playerExitDoor = none;
681 if (isTutorialRoom()) {
682 playerExitDoor = none;
684 global.currLevel = 1;
685 generateNormalLevel();
689 if (levelKind == LevelKind.Stars || levelKind == LevelKind.Sun || levelKind == LevelKind.Moon) {
690 playerExitDoor = none;
692 if (onLevelExitedCB) onLevelExitedCB();
697 if (isNormalLevel()) {
698 stats.maxLevelComplete = max(stats.maxLevelComplete, global.currLevel);
700 if (playerExitDoor) {
701 if (playerExitDoor.objType == 'oXGold') {
702 writeln("exiting to City Of Gold");
703 global.cityOfGold = -1;
704 //!global.currLevel += 1;
705 } else if (playerExitDoor.objType == 'oXMarket') {
706 writeln("exiting to Black Market");
707 global.genBlackMarket = true;
708 //!global.currLevel += 1;
710 writeln("exit door(", GetClassName(playerExitDoor.Class), "): '", playerExitDoor.objType, "'");
713 writeln("WTF?! NO EXIT DOOR!");
716 if (onLevelExitedCB) onLevelExitedCB();
718 playerExitDoor = none;
719 if (levelKind == LevelKind.Transition) {
720 if (global.thiefLevel > 0) global.thiefLevel -= 1;
721 if (global.alienCraft) ++global.alienCraft;
722 if (global.yetiLair) ++global.yetiLair;
723 if (global.lake) ++global.lake;
724 if (global.cityOfGold) { if (++global.cityOfGold == 0) global.cityOfGold = 1; }
725 //orig dbg:if (global.currLevel == 1) global.currLevel += firstLevelSkip; else global.currLevel += levelSkip;
727 if (!global.blackMarket && !global.cityOfGold /*&& !global.yetiLair*/) {
728 global.currLevel += 1;
734 // < 20 seconds per level: looks like a speedrun
735 global.noDarkLevel = (levelEndTime > levelStartTime && levelEndTime-levelStartTime < 20*30);
736 if (lg.finalBossLevel) {
739 // add money for big idol
740 player.addScore(50000);
744 generateTransitionLevel();
747 //centerViewAtPlayer();
751 void onOlmecDead (MapObject o) {
752 writeln("*** OLMEC IS DEAD!");
753 foreach (MapTile t; allExits) {
756 auto st = checkTileAtPoint(t.ix+8, t.iy+16);
758 st = MakeMapTile(t.ix/16, t.iy/16+1, 'oTemple');
761 st.invincible = true;
767 void generateLevelMessages () {
768 writeln("LEVEL NUMBER: ", global.currLevel);
769 if (global.darkLevel) {
770 if (global.hasCrown) {
771 osdMessage("THE HEDJET SHINES BRIGHTLY.");
772 global.darkLevel = false;
773 } else if (global.config.scumDarkness < 2) {
774 osdMessage("I CAN'T SEE A THING!\nI'D BETTER USE THESE FLARES!");
778 if (global.blackMarket) osdMessage("WELCOME TO THE BLACK MARKET!");
780 if (global.cemetary) osdMessage("THE DEAD ARE RESTLESS!");
781 if (global.lake == 1) osdMessage("I CAN HEAR RUSHING WATER...");
783 if (global.snakePit) osdMessage("I HEAR SNAKES... I HATE SNAKES!");
784 if (global.yetiLair == 1) osdMessage("IT SMELLS LIKE WET FUR IN HERE!");
785 if (global.alienCraft == 1) osdMessage("THERE'S A PSYCHIC PRESENCE HERE!");
786 if (global.cityOfGold == 1) {
787 if (true /*hasOlmec()*/) osdMessage("IT'S THE LEGENDARY CITY OF GOLD!");
790 if (global.sacrificePit) osdMessage("I CAN HEAR PRAYERS TO KALI!");
794 final MapObject debugSpawnObjectWithClass (class!MapObject oclass, optional bool playerDir) {
795 if (!oclass) return none;
797 bool canLeft = !isSolidAtPoint(player.ix-8, player.iy);
798 bool canRight = !isSolidAtPoint(player.ix+16, player.iy);
799 if (!canLeft && !canRight) return none;
800 if (canLeft && canRight) {
802 dx = (player.dir == MapObject::Dir.Left ? -16 : 16);
807 dx = (canLeft ? -16 : 16);
809 auto obj = SpawnMapObjectWithClass(oclass);
810 if (obj isa MapEnemy) {
812 dy -= (obj isa MonsterDamsel ? 2 : 8);
814 if (obj) PutSpawnedMapObject(player.ix+dx, player.iy+dy, obj);
819 final MapObject debugSpawnObject (name aname) {
820 if (!aname) return none;
821 return debugSpawnObjectWithClass(findGameObjectClassByName(aname));
825 void createSpecialLevel (LevelKind kind, scope void delegate () creator, name amusic) {
826 global.darkLevel = false;
830 global.resetStartingItems();
832 global.setMusicPitch(1.0);
835 auto olddel = ImmediateDelete;
836 ImmediateDelete = false;
844 addBackgroundGfxDetails();
845 //levBGImgName = 'bgCave';
846 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
848 blockWaterChecking = true;
852 ImmediateDelete = olddel;
853 CollectGarbage(true); // destroy delayed objects too
855 if (dumpGridStats) objGrid.dumpStats();
857 playerExited = false; // just in case
858 playerExitDoor = none;
860 osdClear(clearTalk:true);
863 lg.musicName = amusic;
864 lastMusicName = amusic;
865 global.setMusicPitch(1.0);
866 if (amusic) global.playMusic(lg.musicName); else global.stopMusic();
870 void createTitleLevel () {
871 createSpecialLevel(LevelKind.Title, &createTitleRoom, 'musTitle');
875 void createTutorialLevel () {
876 createSpecialLevel(LevelKind.Tutorial, &createTutorialRoom, 'musCave');
885 // `global.currLevel` is the new level
886 void generateTransitionLevel () {
887 global.darkLevel = false;
892 resetTransitionOverlay();
894 global.setMusicPitch(1.0);
895 switch (global.config.transitionMusicMode) {
896 case GameConfig::MusicMode.Silent: global.stopMusic(); break;
897 case GameConfig::MusicMode.Restart: global.restartMusic(); break;
898 case GameConfig::MusicMode.DontTouch: break;
901 levelKind = LevelKind.Transition;
903 auto olddel = ImmediateDelete;
904 ImmediateDelete = false;
907 if (global.currLevel < 4) createTrans1Room();
908 else if (global.currLevel == 4) createTrans1xRoom();
909 else if (global.currLevel < 8) createTrans2Room();
910 else if (global.currLevel == 8) createTrans2xRoom();
911 else if (global.currLevel < 12) createTrans3Room();
912 else if (global.currLevel == 12) createTrans3xRoom();
913 else if (global.currLevel < 16) createTrans4Room();
914 else if (global.currLevel == 16) createTrans4Room();
915 else createTrans1Room(); //???
920 addBackgroundGfxDetails();
921 //levBGImgName = 'bgCave';
922 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
924 blockWaterChecking = true;
928 if (damselSaved > 0) {
929 // this is special "damsel ready to kiss you" object, not a heart
930 MakeMapObject(176+8, 176+8, 'oDamselKiss');
931 global.plife += damselSaved; // if player skipped transition cutscene
935 ImmediateDelete = olddel;
936 CollectGarbage(true); // destroy delayed objects too
938 if (dumpGridStats) objGrid.dumpStats();
940 playerExited = false; // just in case
941 playerExitDoor = none;
943 osdClear(clearTalk:true);
946 //global.playMusic(lg.musicName);
950 void generateLevel () {
951 levelStartTime = time;
957 global.genBlackMarket = false;
960 global.setMusicPitch(1.0);
961 stats.clearLevelTotals();
963 levelKind = LevelKind.Normal;
970 //writeln("tw:", tilesWidth, "; th:", tilesHeight);
972 auto olddel = ImmediateDelete;
973 ImmediateDelete = false;
976 if (lg.finalBossLevel) {
977 blockWaterChecking = true;
981 // if transition cutscene was skipped...
982 global.plife += max(0, damselSaved); // if player skipped transition cutscene
986 startRoomX = lg.startRoomX;
987 startRoomY = lg.startRoomY;
988 endRoomX = lg.endRoomX;
989 endRoomY = lg.endRoomY;
990 addBackgroundGfxDetails();
991 foreach (int y; 0..tilesHeight) {
992 foreach (int x; 0..tilesWidth) {
998 levBGImgName = lg.bgImgName;
999 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
1001 if (global.allowAngryShopkeepers) generateAngryShopkeepers();
1003 lg.generateEntities();
1005 // add box of flares to dark level
1006 if (global.darkLevel && allEnters.length) {
1007 auto enter = allEnters[0];
1008 int x = enter.ix, y = enter.iy;
1009 if (!isSolidAtPoint(x-16, y)) MakeMapObject(x-16+8, y+8, 'oFlareCrate');
1010 else if (!isSolidAtPoint(x+16, y)) MakeMapObject(x+16+8, y+8, 'oFlareCrate');
1011 else MakeMapObject(x+8, y+8, 'oFlareCrate');
1014 //scrGenerateEntities();
1015 //foreach (; 0..2) scrGenerateEntities();
1017 writeln(objGrid.countObjects, " alive objects inserted");
1018 writeln(countBackTiles, " background tiles inserted");
1020 if (!player) FatalError("player pawn is not spawned");
1022 if (lg.finalBossLevel) {
1023 blockWaterChecking = true;
1025 blockWaterChecking = false;
1030 ImmediateDelete = olddel;
1031 CollectGarbage(true); // destroy delayed objects too
1033 if (dumpGridStats) objGrid.dumpStats();
1035 playerExited = false; // just in case
1036 playerExitDoor = none;
1038 levelMoneyStart = stats.money;
1040 osdClear(clearTalk:true);
1041 generateLevelMessages();
1046 //writeln("!<", lastMusicName, ">:<", lg.musicName, ">");
1047 global.setMusicPitch(1.0);
1048 if (lastMusicName != lg.musicName) {
1049 global.playMusic(lg.musicName);
1051 //writeln("MM: ", global.config.nextLevelMusicMode);
1052 switch (global.config.nextLevelMusicMode) {
1053 case GameConfig::MusicMode.Silent: global.stopMusic(); break; // the thing that should not be
1054 case GameConfig::MusicMode.Restart: global.restartMusic(); break;
1055 case GameConfig::MusicMode.DontTouch:
1056 if (global.config.transitionMusicMode == GameConfig::MusicMode.Silent) {
1057 global.playMusic(lg.musicName);
1062 lastMusicName = lg.musicName;
1063 //global.playMusic(lg.musicName);
1066 if (global.cityOfGold == 1 || global.genBlackMarket) resetBMCOG = true;
1068 if (global.cityOfGold == 1) {
1069 lg.mapSprite = 'sMapTemple';
1070 lg.mapTitle = "City of Gold";
1071 } else if (global.blackMarket) {
1072 lg.mapSprite = 'sMapJungle';
1073 lg.mapTitle = "Black Market";
1078 // ////////////////////////////////////////////////////////////////////////// //
1079 int currKeys, nextKeys;
1080 int pressedKeysQ, releasedKeysQ;
1081 int keysPressed, keysReleased = -1;
1084 struct SavedKeyState {
1085 int currKeys, nextKeys;
1086 int pressedKeysQ, releasedKeysQ;
1087 int keysPressed, keysReleased;
1089 int roomSeed, otherSeed;
1093 // for saving/replaying
1094 final void keysSaveState (out SavedKeyState ks) {
1095 ks.currKeys = currKeys;
1096 ks.nextKeys = nextKeys;
1097 ks.pressedKeysQ = pressedKeysQ;
1098 ks.releasedKeysQ = releasedKeysQ;
1099 ks.keysPressed = keysPressed;
1100 ks.keysReleased = keysReleased;
1103 // for saving/replaying
1104 final void keysRestoreState (const ref SavedKeyState ks) {
1105 currKeys = ks.currKeys;
1106 nextKeys = ks.nextKeys;
1107 pressedKeysQ = ks.pressedKeysQ;
1108 releasedKeysQ = ks.releasedKeysQ;
1109 keysPressed = ks.keysPressed;
1110 keysReleased = ks.keysReleased;
1114 final void keysNextFrame () {
1115 currKeys = nextKeys;
1119 final void clearKeys () {
1129 final void onKey (int code, bool down) {
1134 if (keysReleased&code) {
1135 keysPressed |= code;
1136 keysReleased &= ~code;
1137 pressedKeysQ |= code;
1141 if (keysPressed&code) {
1142 keysReleased |= code;
1143 keysPressed &= ~code;
1144 releasedKeysQ |= code;
1149 final bool isKeyDown (int code) {
1150 return !!(currKeys&code);
1153 final bool isKeyPressed (int code) {
1154 bool res = !!(pressedKeysQ&code);
1155 pressedKeysQ &= ~code;
1159 final bool isKeyReleased (int code) {
1160 bool res = !!(releasedKeysQ&code);
1161 releasedKeysQ &= ~code;
1166 final void clearKeysPressRelease () {
1167 keysPressed = default.keysPressed;
1168 keysReleased = default.keysReleased;
1169 pressedKeysQ = default.pressedKeysQ;
1170 releasedKeysQ = default.releasedKeysQ;
1176 // ////////////////////////////////////////////////////////////////////////// //
1177 final void registerEnter (MapTile t) {
1184 final void registerExit (MapTile t) {
1191 final bool isYAtEntranceRow (int py) {
1193 foreach (MapTile t; allEnters) if (t.iy == py) return true;
1198 final int calcNearestEnterDist (int px, int py) {
1199 if (allEnters.length == 0) return int.max;
1200 int curdistsq = int.max;
1201 foreach (MapTile t; allEnters) {
1202 int xc = px-t.xCenter, yc = py-t.yCenter;
1203 int distsq = xc*xc+yc*yc;
1204 if (distsq < curdistsq) curdistsq = distsq;
1206 return round(sqrt(curdistsq));
1210 final int calcNearestExitDist (int px, int py) {
1211 if (allExits.length == 0) return int.max;
1212 int curdistsq = int.max;
1213 foreach (MapTile t; allExits) {
1214 int xc = px-t.xCenter, yc = py-t.yCenter;
1215 int distsq = xc*xc+yc*yc;
1216 if (distsq < curdistsq) curdistsq = distsq;
1218 return round(sqrt(curdistsq));
1222 // ////////////////////////////////////////////////////////////////////////// //
1223 final void clearForTransition () {
1224 auto olddel = ImmediateDelete;
1225 ImmediateDelete = false;
1227 ImmediateDelete = olddel;
1228 CollectGarbage(true); // destroy delayed objects too
1229 global.darkLevel = false;
1233 // ////////////////////////////////////////////////////////////////////////// //
1234 final int countBackTiles () {
1236 for (MapBackTile bt = backtiles; bt; bt = bt.next) ++res;
1241 final void clearWholeLevel () {
1245 // don't kill objects the player is holding
1247 if (player.pickedItem isa ItemBall) {
1248 player.pickedItem.instanceRemove();
1249 player.pickedItem = none;
1251 if (player.pickedItem && player.pickedItem.grid) {
1252 player.pickedItem.grid.remove(player.pickedItem.gridId);
1253 writeln("secured pocket item '", GetClassName(player.pickedItem.Class), "'");
1255 if (player.holdItem isa ItemBall) {
1256 player.removeBallAndChain(temp:true);
1257 if (player.holdItem) player.holdItem.instanceRemove();
1258 player.holdItem = none;
1260 if (player.holdItem && player.holdItem.grid) {
1261 player.holdItem.grid.remove(player.holdItem.gridId);
1262 writeln("secured held item '", GetClassName(player.holdItem.Class), "'");
1264 writeln("secured ball; mustBeChained=", player.mustBeChained, "; wasHoldingBall=", player.wasHoldingBall);
1267 int count = objGrid.countObjects();
1268 if (dumpGridStats) { if (objGrid.getFirstObjectCID()) objGrid.dumpStats(); }
1269 objGrid.removeAllObjects(true); // and destroy
1270 if (count > 0) writeln(count, " objects destroyed");
1272 lastUsedObjectId = 0;
1275 lastRenderTime = -1;
1276 liquidTileCount = 0;
1280 MapBackTile t = backtiles;
1286 framesProcessedFromLastClear = 0;
1290 final void insertObject (MapEntity o) {
1292 if (o.grid) FatalError("cannot put object into level twice");
1297 final void spawnPlayerAt (int x, int y) {
1298 // if we have no player, spawn new one
1299 // otherwise this just a level transition, so simply reposition him
1301 // don't add player to object list, as it has very separate processing anyway
1302 player = SpawnObject(PlayerPawn);
1303 player.global = global;
1304 player.level = self;
1305 if (!player.initialize()) {
1307 FatalError("something is wrong with player initialization");
1313 player.saveInterpData();
1315 if (player.mustBeChained || global.config.scumBallAndChain) {
1316 writeln("*** spawning ball and chain");
1317 player.spawnBallAndChain(levelStart:true);
1319 playerExited = false;
1320 playerExitDoor = none;
1321 if (global.config.startWithKapala) global.hasKapala = true;
1322 centerViewAtPlayer();
1323 // reinsert player items into grid
1324 if (player.pickedItem) objGrid.insert(player.pickedItem);
1325 if (player.holdItem) objGrid.insert(player.holdItem);
1326 //writeln("player spawned; active=", player.active);
1327 player.scrSwitchToPocketItem(forceIfEmpty:false);
1331 final void teleportPlayerTo (int x, int y) {
1335 player.saveInterpData();
1340 final void resurrectPlayer () {
1341 if (player) player.resurrect();
1342 playerExited = false;
1343 playerExitDoor = none;
1347 // ////////////////////////////////////////////////////////////////////////// //
1348 final void scrShake (int duration) {
1349 if (shakeLeft == 0) {
1355 shakeLeft = max(shakeLeft, duration);
1360 // ////////////////////////////////////////////////////////////////////////// //
1363 ItemStolen, // including damsel, lol
1369 // checks for dead, agnered, distance, etc. should be already done
1370 protected void doAngerShopkeeper (MonsterShopkeeper shp, SCAnger reason, ref bool messaged,
1371 int maxdist, MapEntity offender)
1373 if (!shp || shp.dead || shp.angered) return;
1374 if (offender.distanceToEntityCenter(shp) > maxdist) return;
1376 shp.status = MapObject::ATTACK;
1378 if (global.murderer) {
1379 msg = "~YOU'LL PAY FOR YOUR CRIMES!~";
1382 case SCAnger.TileDestroyed: msg = "~DIE, YOU VANDAL!~"; break;
1383 case SCAnger.ItemStolen: msg = "~COME BACK HERE, THIEF!~"; break;
1384 case SCAnger.CrapsCheated: msg = "~DIE, CHEATER!~"; break;
1385 case SCAnger.BombDropped: msg = "~TERRORIST!~"; break;
1386 case SCAnger.DamselWhipped: msg = "~HEY, ONLY I CAN DO THAT!~"; break;
1387 default: "~NOW I'M REALLY STEAMED!~"; break;
1391 writeln("shopkeeper angered; reason=", reason, "; maxdist=", maxdist, "; msg=\"", msg, "\"");
1394 if (msg) osdMessageTalk(msg, replace:true, inShopOnly:false, hiColor1:0xff_00_00);
1395 global.thiefLevel += (global.thiefLevel > 0 ? 3 : 2);
1400 // make the nearest shopkeeper angry. RAWR!
1401 void scrShopkeeperAnger (SCAnger reason, optional int maxdist, optional MapEntity offender) {
1402 bool messaged = false;
1403 maxdist = clamp(maxdist, 96, 100000);
1404 if (!offender) offender = player;
1405 if (maxdist == 100000) {
1406 foreach (MonsterShopkeeper shp; objGrid.allObjects(MonsterShopkeeper)) {
1407 doAngerShopkeeper(shp, reason, messaged, maxdist, offender);
1410 foreach (MonsterShopkeeper shp; objGrid.inRectPix(offender.xCenter-maxdist-128, offender.yCenter-maxdist-128, (maxdist+128)*2, (maxdist+128)*2, precise:false, castClass:MonsterShopkeeper)) {
1411 doAngerShopkeeper(shp, reason, messaged, maxdist, offender);
1417 final MapObject findCrapsPrize () {
1418 foreach (MapObject o; objGrid.allObjects(MapObject)) {
1419 if (!o.spectral && o.inDiceHouse) return o;
1425 // ////////////////////////////////////////////////////////////////////////// //
1426 // moved from oPlayer1.Step.Action so it could be shared with oAltarLeft so that traps will be triggered when the altar is destroyed without picking up the idol.
1427 // note: idols moved by monkeys will have false `stolenIdol`
1428 void scrTriggerIdolAltar (bool stolenIdol) {
1429 ObjTikiCurse res = none;
1430 int curdistsq = int.max;
1431 int px = player.xCenter, py = player.yCenter;
1432 foreach (MapObject o; objGrid.allObjects(MapObject)) {
1433 auto tcr = ObjTikiCurse(o);
1435 if (tcr.activated) continue;
1436 int xc = px-tcr.xCenter, yc = py-tcr.yCenter;
1437 int distsq = xc*xc+yc*yc;
1438 if (distsq < curdistsq) {
1443 if (res) res.activate(stolenIdol);
1447 // ////////////////////////////////////////////////////////////////////////// //
1448 void setupGhostTime () {
1449 musicFadeTimer = -1;
1450 ghostSpawned = false;
1452 // there is no ghost on the first level
1453 if (inWinCutscene || inIntroCutscene || !isNormalLevel() || lg.finalBossLevel ||
1454 (!global.config.ghostAtFirstLevel && global.currLevel == 1))
1457 global.setMusicPitch(1.0);
1461 if (global.config.scumGhost < 0) {
1464 osdMessage("A GHASTLY VOICE CALLS YOUR NAME!\nLET'S GET OUT OF HERE!", 6.66, urgent:true);
1468 if (global.config.scumGhost == 0) {
1474 // randomizes time until ghost appears once time limit is reached
1475 // global.ghostRandom (true/false) if extra time is randomized - loaded from yasm.cfg. if false, use 30 second default
1476 // ghostTimeLeft (time in seconds * 1000) for currently generated level
1478 if (global.config.ghostRandom) {
1479 auto tMin = global.randOther(10, 30); // min(global.plife*10, 30);
1480 auto tMax = max(tMin+global.randOther(10, 30), min(global.config.scumGhost, 300)); // 5 minutes max
1481 auto tTime = global.randOther(tMin, tMax);
1482 if (tTime <= 0) tTime = round(tMax/2.0);
1483 ghostTimeLeft = tTime;
1485 ghostTimeLeft = global.config.scumGhost; // 5 minutes max
1488 ghostTimeLeft += max(0, global.config.ghostExtraTime);
1490 ghostTimeLeft *= 30; // seconds -> frames
1491 //global.ghostShowTime
1495 void spawnGhost () {
1497 ghostSpawned = true;
1500 int vwdt = (viewMax.x-viewMin.x);
1501 int vhgt = (viewMax.y-viewMin.y);
1505 if (player.ix < viewMin.x+vwdt/2) {
1506 // player is in the left side
1507 gx = viewMin.x+vwdt/2+vwdt/4;
1509 // player is in the right side
1510 gx = viewMin.x+vwdt/4;
1513 if (player.iy < viewMin.y+vhgt/2) {
1514 // player is in the left side
1515 gy = viewMin.y+vhgt/2+vhgt/4;
1517 // player is in the right side
1518 gy = viewMin.y+vhgt/4;
1521 writeln("spawning ghost at tile (", gx/16, ",", gy/16, "); player is at tile (", player.ix/16, ",", player.iy/16, ")");
1523 MakeMapObject(gx, gy, 'oGhost');
1526 if (oPlayer1.x > room_width/2) instance_create(view_xview[0]+view_wview[0]+8, view_yview[0]+floor(view_hview[0] / 2), oGhost);
1527 else instance_create(view_xview[0]-32, view_yview[0]+floor(view_hview[0]/2), oGhost);
1528 global.ghostExists = true;
1533 void thinkFrameGameGhost () {
1534 if (player.dead) return;
1535 if (!isNormalLevel()) return; // just in case
1537 if (ghostTimeLeft < 0) {
1539 if (musicFadeTimer > 0) {
1540 musicFadeTimer = -1;
1541 global.setMusicPitch(1.0);
1546 if (musicFadeTimer >= 0) {
1548 if (musicFadeTimer > 0 && musicFadeTimer <= 100*3) {
1549 float pitch = 1.0-(0.00226/3.0)*musicFadeTimer;
1550 //writeln("MFT: ", musicFadeTimer, "; pitch=", pitch);
1551 global.setMusicPitch(pitch);
1555 if (ghostTimeLeft == 0) {
1556 // she is already here!
1560 // no ghost if we have a crown
1561 if (global.hasCrown) {
1566 // if she was already spawned, don't do it again
1572 if (--ghostTimeLeft != 0) {
1574 if (global.config.ghostExtraTime > 0) {
1575 if (ghostTimeLeft == global.config.ghostExtraTime*30) {
1576 osdMessage("A CHILL RUNS UP YOUR SPINE...\nLET'S GET OUT OF HERE!", 6.66, urgent:true);
1578 if (musicFadeTimer < 0 && ghostTimeLeft <= global.config.ghostExtraTime*30) {
1586 if (player.isExitingSprite) {
1587 // no reason to spawn her, we're leaving
1596 void thinkFrameGame () {
1597 thinkFrameGameGhost();
1598 // udjat eye blinking
1599 if (global.hasUdjatEye && player) {
1600 foreach (MapTile t; allExits) {
1601 if (t isa MapTileBlackMarketDoor) {
1602 auto dm = int(player.distanceToEntity(t));
1604 if (udjatAlarm < 1 || dm < udjatAlarm) udjatAlarm = dm;
1608 global.udjatBlink = false;
1611 if (udjatAlarm > 0) {
1612 if (--udjatAlarm == 0) {
1613 global.udjatBlink = !global.udjatBlink;
1614 if (global.hasUdjatEye && player) {
1615 player.playSound(global.udjatBlink ? 'sndBlink1' : 'sndBlink2');
1619 switch (levelKind) {
1620 case LevelKind.Stars: thinkFrameGameStars(); break;
1621 case LevelKind.Sun: thinkFrameGameSun(); break;
1622 case LevelKind.Moon: thinkFrameGameMoon(); break;
1623 case LevelKind.Transition: thinkFrameTransition(); break;
1624 case LevelKind.Intro: thinkFrameIntro(); break;
1629 // ////////////////////////////////////////////////////////////////////////// //
1630 private final bool isWaterTileCB (MapTile t) {
1631 return (t && t.visible && t.water);
1635 private final bool isLavaTileCB (MapTile t) {
1636 return (t && t.visible && t.lava);
1640 // ////////////////////////////////////////////////////////////////////////// //
1641 const int GreatLakeStartTileY = 28;
1644 final void fillGreatLake () {
1645 if (global.lake == 1) {
1646 foreach (int y; GreatLakeStartTileY..tilesHeight) {
1647 foreach (int x; 0..tilesWidth) {
1648 auto t = checkTileAtPoint(x*16, y*16, delegate bool (MapTile t) {
1649 if (t.spectral || !t.visible || t.invisible || t.moveable) return false;
1653 t = MakeMapTile(x, y, 'oWaterSwim');
1657 t.setSprite(y == GreatLakeStartTileY ? 'sWaterTop' : 'sWater');
1658 } else if (t.lava) {
1659 t.setSprite(y == GreatLakeStartTileY ? 'sLavaTop' : 'sLava');
1667 // called once after level generation
1668 final void fixLiquidTop () {
1669 if (global.lake == 1) fillGreatLake();
1671 liquidTileCount = 0;
1672 foreach (MapTile t; objGrid.allObjects(MapTile)) {
1673 if (!t.water && !t.lava) continue;
1676 //writeln("fixing water tile(", GetClassName(t.Class), "):'", t.objName, "' (water=", t.water, "; lava=", t.lava, "); lqc=", liquidTileCount);
1678 //if (global.lake == 1) continue; // it is done in `fillGreatLake()`
1680 if (!checkTileAtPoint(t.ix+8, t.iy-8, (t.lava ? &isLavaTileCB : &isWaterTileCB))) {
1681 t.setSprite(t.lava ? 'sLavaTop' : 'sWaterTop');
1683 // don't do this, it will destroy seaweed
1684 //t.setSprite(t.lava ? 'sLava' : 'sWater');
1685 auto spr = t.getSprite();
1686 if (!spr) t.setSprite(t.lava ? 'sLava' : 'sWater');
1687 else if (spr.Name == 'sLavaTop') t.setSprite('sLava');
1688 else if (spr.Name == 'sWaterTop') t.setSprite('sWater');
1691 //writeln("liquid tiles count: ", liquidTileCount);
1695 // ////////////////////////////////////////////////////////////////////////// //
1696 transient MapTile curWaterTile;
1697 transient bool curWaterTileCheckHitsLava;
1698 transient bool curWaterTileCheckHitsSolidOrWater; // only for `checkWaterOrSolidTilePartialCB`
1699 transient int curWaterTileLastHDir;
1700 transient ubyte[16, 16] curWaterOccupied;
1701 transient int curWaterOccupiedCount;
1702 transient int curWaterTileCheckX0, curWaterTileCheckY0;
1705 private final void clearCurWaterCheckState () {
1706 curWaterTileCheckHitsLava = false;
1707 curWaterOccupiedCount = 0;
1708 foreach (auto idx; 0..16*16) curWaterOccupied[idx] = 0;
1712 private final bool checkWaterOrSolidTileCB (MapTile t) {
1713 if (t == curWaterTile) return false;
1714 if (t.lava && curWaterTile.water) {
1715 curWaterTileCheckHitsLava = true;
1718 if (t.ix%16 != 0 || t.iy%16 != 0) {
1719 if (t.water || t.solid) {
1720 // fill occupied array
1721 //FIXME: optimize this
1722 if (curWaterOccupiedCount < 16*16) {
1723 foreach (auto dy; t.y0..t.y1+1) {
1724 foreach (auto dx; t.x0..t.x1+1) {
1725 int sx = dx-curWaterTileCheckX0;
1726 int sy = dy-curWaterTileCheckY0;
1727 if (sx >= 0 && sx <= 16 && sy >= 0 && sy <= 15 && !curWaterOccupied[sx, sy]) {
1728 curWaterOccupied[sx, sy] = 1;
1729 ++curWaterOccupiedCount;
1735 return false; // need to check for lava
1737 if (t.water || t.solid || t.lava) {
1738 curWaterOccupiedCount = 16*16;
1739 if (t.water && curWaterTile.lava) t.instanceRemove();
1741 return false; // need to check for lava
1745 private final bool checkWaterOrSolidTilePartialCB (MapTile t) {
1746 if (t == curWaterTile) return false;
1747 if (t.lava && curWaterTile.water) {
1748 //writeln("!!!!!!!!");
1749 curWaterTileCheckHitsLava = true;
1752 if (t.water || t.solid || t.lava) {
1753 //writeln("*********");
1754 curWaterTileCheckHitsSolidOrWater = true;
1755 if (t.water && curWaterTile.lava) t.instanceRemove();
1757 return false; // need to check for lava
1761 private final bool isFullyOccupiedAtTilePos (int tileX, int tileY) {
1762 clearCurWaterCheckState();
1763 curWaterTileCheckX0 = tileX*16;
1764 curWaterTileCheckY0 = tileY*16;
1765 checkTilesInRect(tileX*16, tileY*16, 16, 16, &checkWaterOrSolidTileCB);
1766 return (curWaterTileCheckHitsLava || curWaterOccupiedCount == 16*16);
1770 private final bool isAtLeastPartiallyOccupiedAtTilePos (int tileX, int tileY) {
1771 curWaterTileCheckHitsLava = false;
1772 curWaterTileCheckHitsSolidOrWater = false;
1773 checkTilesInRect(tileX*16, tileY*16, 16, 16, &checkWaterOrSolidTilePartialCB);
1774 return (curWaterTileCheckHitsSolidOrWater || curWaterTileCheckHitsLava);
1778 private final bool waterCanReachGroundHoleInDir (MapTile wtile, int dx) {
1779 if (dx == 0) return false; // just in case
1781 int x = wtile.ix/16, y = wtile.iy/16;
1783 while (x >= 0 && x < tilesWidth) {
1784 if (!isAtLeastPartiallyOccupiedAtTilePos(x, y+1)) return true;
1785 if (isAtLeastPartiallyOccupiedAtTilePos(x, y)) return false;
1792 // returns `true` if this tile must be removed
1793 private final bool checkWaterFlow (MapTile wtile) {
1794 if (global.lake == 1) {
1795 if (wtile.iy >= GreatLakeStartTileY*16) return false; // lake tile, don't touch
1796 if (wtile.iy >= GreatLakeStartTileY*16-16) return true; // remove it, so it won't stack on a lake
1799 if (wtile.ix%16 != 0 || wtile.iy%16 != 0) return true; // sanity check
1801 curWaterTile = wtile;
1802 curWaterTileLastHDir = 0; // never moved to the side
1804 bool wasMoved = false;
1807 int tileX = wtile.ix/16, tileY = wtile.iy/16;
1810 if (tileY >= tilesHeight) return true;
1812 // check if we can fall down
1813 auto canFall = !isAtLeastPartiallyOccupiedAtTilePos(tileX, tileY+1);
1814 // disappear if can fall in lava
1815 if (wtile.water && curWaterTileCheckHitsLava) {
1816 //!writeln(wtile.objId, ": LAVA HIT DOWN");
1820 // fake, so caller will not start removing tiles
1821 if (canFall) wtile.waterMovedDown = true;
1827 //!writeln(wtile.objId, ": GOING DOWN");
1828 curWaterTileLastHDir = 0;
1829 wtile.iy = wtile.iy+16;
1831 wtile.waterMovedDown = true;
1835 bool canMoveLeft = (curWaterTileLastHDir > 0 ? false : !isAtLeastPartiallyOccupiedAtTilePos(tileX-1, tileY));
1836 // disappear if near lava
1837 if (wtile.water && curWaterTileCheckHitsLava) {
1838 //!writeln(wtile.objId, ": LAVA HIT LEFT");
1842 bool canMoveRight = (curWaterTileLastHDir < 0 ? false : !isAtLeastPartiallyOccupiedAtTilePos(tileX+1, tileY));
1843 // disappear if near lava
1844 if (wtile.water && curWaterTileCheckHitsLava) {
1845 //!writeln(wtile.objId, ": LAVA HIT RIGHT");
1849 if (!canMoveLeft && !canMoveRight) {
1851 //!if (wasMove) writeln(wtile.objId, ": NO MORE MOVES");
1855 if (canMoveLeft && canMoveRight) {
1856 // choose random direction
1857 //!writeln(wtile.objId, ": CHOOSING RANDOM HDIR");
1858 // actually, choose direction that leads to hole in a ground
1859 if (waterCanReachGroundHoleInDir(wtile, -1)) {
1860 // can reach hole at the left side
1861 if (waterCanReachGroundHoleInDir(wtile, 1)) {
1862 // can reach hole at the right side, choose at random
1863 if (global.randOther(0, 1)) canMoveRight = false; else canMoveLeft = false;
1866 canMoveRight = false;
1869 // can't reach hole at the left side
1870 if (waterCanReachGroundHoleInDir(wtile, 1)) {
1871 // can reach hole at the right side, choose at random
1872 canMoveLeft = false;
1874 // no holes at any side, choose at random
1875 if (global.randOther(0, 1)) canMoveRight = false; else canMoveLeft = false;
1882 if (canMoveRight) FatalError("WATERCHECK: WTF RIGHT");
1883 //!writeln(wtile.objId, ": MOVING LEFT (", curWaterTileLastHDir, ")");
1884 curWaterTileLastHDir = -1;
1885 wtile.ix = wtile.ix-16;
1886 } else if (canMoveRight) {
1887 if (canMoveLeft) FatalError("WATERCHECK: WTF LEFT");
1888 //!writeln(wtile.objId, ": MOVING RIGHT (", curWaterTileLastHDir, ")");
1889 curWaterTileLastHDir = 1;
1890 wtile.ix = wtile.ix+16;
1898 wtile.setSprite(wtile.lava ? 'sLava' : 'sWater');
1899 wtile.waterMoved = true;
1900 // if this tile was not moved down, check if it can move down on any next step
1901 if (!wtile.waterMovedDown) {
1902 if (waterCanReachGroundHoleInDir(wtile, -1)) wtile.waterMovedDown = true;
1903 else if (waterCanReachGroundHoleInDir(wtile, 1)) wtile.waterMovedDown = true;
1907 return false; // don't remove
1909 //if (!isWetTileAtPix(tileX*16+8, tileY*16-8)) wtile.setSprite(wtile.lava ? 'sLavaTop' : 'sWaterTop');
1913 transient array!MapTile waterTilesList;
1915 final bool sortWaterTilesByCoordsLess (MapTile a, MapTile b) {
1917 if (dy) return (dy < 0);
1918 return (a.ix < b.ix);
1921 transient int waterFlowPause = 0;
1922 transient bool debugWaterFlowPause = false;
1924 final void cleanDeadObjects () {
1925 // remove dead objects
1926 if (deadItemsHead) {
1927 auto olddel = ImmediateDelete;
1928 ImmediateDelete = false;
1930 auto it = deadItemsHead;
1931 deadItemsHead = it.deadItemsNext;
1932 if (it.grid) it.grid.remove(it.gridId);
1935 } while (deadItemsHead);
1936 ImmediateDelete = olddel;
1937 if (olddel) CollectGarbage(true); // destroy delayed objects too
1941 final void cleanDeadTiles () {
1942 if (checkWater && /*global.lake == 1 ||*/ (!blockWaterChecking && liquidTileCount)) {
1943 if (global.lake == 1) fillGreatLake();
1944 if (waterFlowPause > 1) {
1949 if (debugWaterFlowPause) waterFlowPause = 4;
1950 //writeln("checking water");
1951 waterTilesList.clear();
1952 foreach (MapTile wtile; objGrid.allObjectsSafe(MapTile)) {
1953 if (wtile.water || wtile.lava) {
1955 if (wtile.ix%16 == 0 && wtile.iy%16 == 0) {
1956 wtile.waterMoved = false;
1957 wtile.waterMovedDown = false;
1958 wtile.waterSlideOldX = wtile.ix;
1959 wtile.waterSlideOldY = wtile.iy;
1960 waterTilesList[$] = wtile;
1965 liquidTileCount = 0;
1966 waterTilesList.sort(&sortWaterTilesByCoordsLess);
1968 bool wasAnyMove = false;
1969 bool wasAnyMoveDown = false;
1970 foreach (MapTile wtile; waterTilesList) {
1971 if (!wtile || !wtile.isInstanceAlive) continue;
1972 auto killIt = checkWaterFlow(wtile);
1976 wtile.instanceRemove(); // just in case
1978 wtile.saveInterpData();
1980 wasAnyMove = wasAnyMove || wtile.waterMoved;
1981 wasAnyMoveDown = wasAnyMoveDown || wtile.waterMovedDown;
1982 if (wtile.waterMoved && debugWaterFlowPause) wtile.waterSlideCounter = 4;
1986 liquidTileCount = 0;
1987 foreach (MapTile wtile; waterTilesList) {
1988 if (!wtile || !wtile.isInstanceAlive) continue;
1989 if (wasAnyMoveDown) {
1993 //checkWater = checkWater || wtile.waterMoved;
1994 curWaterTile = wtile;
1995 int tileX = wtile.ix/16, tileY = wtile.iy/16;
1996 // check if we are have no way to leak
1997 bool killIt = false;
1998 if (!isFullyOccupiedAtTilePos(tileX-1, tileY) || (wtile.water && curWaterTileCheckHitsLava)) {
1999 //writeln(" LEFT DEATH (", curWaterOccupiedCount, " : ", curWaterTileCheckHitsLava, ")");
2002 if (!killIt && (!isFullyOccupiedAtTilePos(tileX+1, tileY) || (wtile.water && curWaterTileCheckHitsLava))) {
2003 //writeln(" RIGHT DEATH (", curWaterOccupiedCount, " : ", curWaterTileCheckHitsLava, ")");
2006 if (!killIt && (!isFullyOccupiedAtTilePos(tileX, tileY+1) || (wtile.water && curWaterTileCheckHitsLava))) {
2007 //writeln(" DOWN DEATH (", curWaterOccupiedCount, " : ", curWaterTileCheckHitsLava, ")");
2014 wtile.instanceRemove(); // just in case
2019 if (wasAnyMove) checkWater = true;
2020 //writeln("water check: liquidTileCount=", liquidTileCount, "; checkWater=", checkWater, "; wasAnyMove=", wasAnyMove, "; wasAnyMoveDown=", wasAnyMoveDown);
2022 // fill empty spaces in lake with water
2030 // ////////////////////////////////////////////////////////////////////////// //
2031 private transient array!MapEntity postponedThinkers;
2032 private transient MapEntity thinkerHeld;
2033 private transient array!MapEntity activeThinkerList;
2036 final void doThinkActionsForObject (MapEntity o) {
2037 if (o.justSpawned) o.justSpawned = false;
2038 else if (o.imageSpeed > 0) o.nextAnimFrame();
2041 if (o.isInstanceAlive) {
2044 if (o.isInstanceAlive) {
2045 if (o.whipTimer > 0) --o.whipTimer;
2047 auto obj = MapObject(o);
2048 if (!o.canLiveOutsideOfLevel && (!obj || !obj.heldBy) && o.isOutsideOfLevel()) {
2049 // oops, fallen out of level...
2057 // return `true` if thinker should be removed
2058 final void thinkOne (MapEntity o, optional bool doHeldObject, optional bool dontAddHeldObject) {
2060 if (o == thinkerHeld && !doHeldObject) return; // skip it
2062 if (!o.isInstanceAlive) return;
2064 auto obj = MapObject(o);
2066 if (obj) obj.prevhp = obj.hp; // so i don't have to do it in `thinkFrame()`
2067 if (!o.active) return;
2069 if (obj && obj.heldBy == player) {
2070 // fix held item coords
2071 obj.fixHoldCoords();
2073 doThinkActionsForObject(o);
2075 if (!dontAddHeldObject) {
2077 foreach (MapEntity e; postponedThinkers) if (e == o) { found = true; break; }
2078 if (!found) postponedThinkers[$] = o;
2084 bool doThink = true;
2086 // collision with player weapon
2087 auto hh = PlayerWeapon(player.holdItem);
2088 bool doWeaponAction = false;
2090 if (hh.blockedBySolids && !global.config.killEnemiesThruWalls) {
2091 int xx = player.ix+(player.dir == MapObject::Dir.Left ? -16 : 16);
2092 //doWeaponAction = !isSolidAtPoint(xx, player.iy);
2093 doWeaponAction = !isSolidAtPoint(xx, hh.yCenter);
2095 int dh = max(1, hh.height-2);
2096 doWeaponAction = !checkTilesInRect(player.ix, player.iy);
2099 doWeaponAction = true;
2103 if (obj && doWeaponAction && hh && (o.whipTimer <= 0 || hh.ignoreWhipTimer) && hh.collidesWithObject(obj)) {
2104 //writeln("WEAPONED!");
2105 //writeln("weapon collides with '", GetClassName(o.Class), "' (", o.objType, "'");
2106 bool dontChangeWhipTimer = hh.dontChangeWhipTimer;
2107 if (!o.onTouchedByPlayerWeapon(player, hh)) {
2108 if (o.isInstanceAlive) hh.onCollisionWithObject(obj);
2110 if (!dontChangeWhipTimer) o.whipTimer = o.whipTimerValue; //HACK
2111 doThink = o.isInstanceAlive;
2114 if (doThink && o.isInstanceAlive) {
2115 doThinkActionsForObject(o);
2116 doThink = o.isInstanceAlive;
2119 // collision with player
2120 if (doThink && obj && o.collidesWith(player)) {
2121 if (!player.onObjectTouched(obj) && o.isInstanceAlive) {
2122 doThink = !o.onTouchedByPlayer(player);
2129 final void processThinkers (float timeDelta) {
2130 if (timeDelta <= 0) return;
2133 if (onBeforeFrame) onBeforeFrame(false);
2134 if (onAfterFrame) onAfterFrame(false);
2140 accumTime += timeDelta;
2141 bool wasFrame = false;
2143 auto olddel = ImmediateDelete;
2144 ImmediateDelete = false;
2145 while (accumTime >= FrameTime) {
2146 postponedThinkers.clear();
2148 accumTime -= FrameTime;
2149 if (onBeforeFrame) onBeforeFrame(accumTime >= FrameTime);
2151 if (shakeLeft > 0) {
2153 if (!shakeDir.x) shakeDir.x = (global.randOther(0, 1) ? -3 : 3);
2154 if (!shakeDir.y) shakeDir.y = (global.randOther(0, 1) ? -3 : 3);
2155 shakeOfs.x = shakeDir.x;
2156 shakeOfs.y = shakeDir.y;
2157 int sgnc = global.randOther(1, 3);
2158 if (sgnc&0x01) shakeDir.x = -shakeDir.x;
2159 if (sgnc&0x02) shakeDir.y = -shakeDir.y;
2168 // we don't want the time to grow too large
2169 if (time < 0) { time = 0; lastRenderTime = -1; }
2170 // game-global events
2172 // frame thinkers: player
2173 if (player && !disablePlayerThink) {
2175 if (!player.dead && isNormalLevel() &&
2176 (maxPlayingTime < 0 ||
2177 (maxPlayingTime > 0 && maxPlayingTime*30 <= time &&
2178 time%30 == 0 && global.randOther(1, 100) <= 20)))
2180 global.hasAnkh = false;
2182 player.invincible = 0;
2183 auto xplo = MapObjExplosion(MakeMapObject(player.ix, player.iy, 'oExplosion'));
2184 if (xplo) xplo.suicide = true;
2186 //HACK: check for stolen items
2187 auto item = MapItem(player.holdItem);
2188 if (item) item.onCheckItemStolen(player);
2189 item = MapItem(player.pickedItem);
2190 if (item) item.onCheckItemStolen(player);
2192 doThinkActionsForObject(player);
2194 // frame thinkers: held object
2195 thinkerHeld = player.holdItem;
2196 if (thinkerHeld && thinkerHeld.isInstanceAlive) {
2197 bool wasAct = thinkerHeld.active;
2198 thinkOne(thinkerHeld, doHeldObject:true);
2199 if (!thinkerHeld.isInstanceAlive) {
2200 if (player.holdItem == thinkerHeld) player.holdItem = none;
2201 thinkerHeld.grid.remove(thinkerHeld.gridId);
2203 thinkerHeld.onDestroy();
2206 } else if (!wasAct) {
2208 auto item = MapItem(thinkerHeld);
2210 if (item.forSale || item.sellOfferDone) {
2211 if (++item.forSaleFrame < 0) item.forSaleFrame = 0;
2216 // frame thinkers: objects
2217 activeThinkerList.clear();
2218 auto grid = objGrid;
2219 // collect active objects
2220 if (global.config.useFrozenRegion) {
2221 foreach (MapEntity e; grid.inRectPix(viewStart.x/global.scale-64, viewStart.y/global.scale-64, 320+64*2, 240+64*2, precise:false)) {
2222 if (e.active) activeThinkerList[$] = e;
2226 foreach (MapEntity e; grid.allObjects()) {
2227 if (e.active) activeThinkerList[$] = e;
2230 // process active objects
2231 //writeln("thinkers: ", activeThinkerList.length);
2232 foreach (MapEntity o; activeThinkerList) {
2234 thinkOne(o, doHeldObject:false);
2235 if (!o.isInstanceAlive) {
2236 //writeln("dead thinker: '", o.objType, "'");
2237 if (o.grid) o.grid.remove(o.gridId);
2238 auto obj = MapObject(o);
2239 if (obj && obj.heldBy) obj.heldBy.holdItem = none;
2246 // postponed thinkers
2247 foreach (MapEntity o; postponedThinkers) {
2249 thinkOne(o, doHeldObject:true, dontAddHeldObject:true);
2250 if (!o.isInstanceAlive) {
2251 //writeln("dead pp-thinker: '", o.objType, "'");
2258 postponedThinkers.clear();
2260 // clean dead things
2262 // fix held item coords
2263 if (player && player.holdItem) {
2264 if (player.holdItem.isInstanceAlive) {
2265 player.holdItem.fixHoldCoords();
2267 player.holdItem = none;
2271 if (collectCounter == 0) {
2272 xmoney = max(0, xmoney-100);
2278 if (!player.dead) stats.oneMoreFramePlayed();
2279 SoundSystem.ListenerOrigin = vector(player.xCenter, player.yCenter, -1);
2280 //writeln("plrpos=(", player.xCenter, ",", player.yCenter, "); lo=", SoundSystem.ListenerOrigin);
2282 if (onAfterFrame) onAfterFrame(accumTime >= FrameTime);
2283 ++framesProcessedFromLastClear;
2286 if (!player.visible && player.holdItem) player.holdItem.visible = false;
2287 if (winCutsceneSwitchToNext) {
2288 winCutsceneSwitchToNext = false;
2289 switch (++inWinCutscene) {
2290 case 2: startWinCutsceneVolcano(); break;
2291 case 3: default: startWinCutsceneWinFall(); break;
2295 if (playerExited) break;
2297 ImmediateDelete = olddel;
2299 playerExited = false;
2301 centerViewAtPlayer();
2304 // if we were processed at least one frame, collect garbage
2306 CollectGarbage(true); // destroy delayed objects too
2308 if (accumTime > 0 && onInterFrame) onInterFrame(accumTime/FrameTime);
2312 // ////////////////////////////////////////////////////////////////////////// //
2313 final void tileXY2roomXY (int tileX, int tileY, out int roomX, out int roomY) {
2314 roomX = (tileX-1)/RoomGen::Width;
2315 roomY = (tileY-1)/RoomGen::Height;
2319 final bool isInShop (int tileX, int tileY) {
2320 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
2321 auto n = roomType[tileX, tileY];
2322 if (n == 4 || n == 5) return true;
2323 return !!checkTilesInRect(tileX*16, tileY*16, 16, 16, delegate bool (MapTile t) { return t.shopWall; });
2324 //k8: we don't have this
2325 //if (t && t.objType == 'oShop') return true;
2331 // ////////////////////////////////////////////////////////////////////////// //
2332 override void Destroy () {
2334 delete tempSolidTile;
2339 // ////////////////////////////////////////////////////////////////////////// //
2340 // WARNING! delegate should not create/delete objects!
2341 final MapObject findNearestObject (int px, int py, scope bool delegate (MapObject o) dg, optional class!MapObject castClass) {
2342 MapObject res = none;
2343 if (!castClass) castClass = MapObject;
2344 int curdistsq = int.max;
2345 foreach (MapObject o; objGrid.allObjects(MapObject)) {
2346 if (o.spectral) continue;
2347 if (!dg(o)) continue;
2348 int xc = px-o.xCenter, yc = py-o.yCenter;
2349 int distsq = xc*xc+yc*yc;
2350 if (distsq < curdistsq) {
2359 // WARNING! delegate should not create/delete objects!
2360 final MapObject findNearestEnemy (int px, int py, optional scope bool delegate (MapEnemy o) dg, optional class!MapEnemy castClass) {
2361 if (!castClass) castClass = MapEnemy;
2362 if (castClass !isa MapEnemy) return none;
2363 MapObject res = none;
2364 int curdistsq = int.max;
2365 foreach (MapEnemy o; objGrid.allObjects(castClass)) {
2366 //k8: i added `dead` check
2367 if (o.spectral || o.dead) continue;
2369 if (!dg(o)) continue;
2371 int xc = px-o.xCenter, yc = py-o.yCenter;
2372 int distsq = xc*xc+yc*yc;
2373 if (distsq < curdistsq) {
2382 final MonsterShopkeeper findNearestCalmShopkeeper (int px, int py) {
2383 auto obj = MonsterShopkeeper(findNearestEnemy(px, py, delegate bool (MapEnemy o) {
2384 auto sk = MonsterShopkeeper(o);
2385 if (sk && !sk.angered) return true;
2387 }, castClass:MonsterShopkeeper));
2392 final MonsterShopkeeper hasAliveShopkeepers (optional bool skipAngry) {
2393 foreach (MonsterShopkeeper sc; objGrid.allObjects(MonsterShopkeeper)) {
2394 if (sc.spectral || sc.dead) continue;
2395 if (skipAngry && (sc.angered || sc.outlaw)) continue;
2402 // WARNING! delegate should not create/delete objects!
2403 final int calcNearestEnemyDist (int px, int py, optional scope bool delegate (MapEnemy o) dg, optional class!MapEnemy castClass) {
2404 auto e = findNearestEnemy(px, py, dg!optional, castClass!optional);
2405 if (!e) return int.max;
2406 int xc = px-e.xCenter, yc = py-e.yCenter;
2407 return round(sqrt(xc*xc+yc*yc));
2411 // WARNING! delegate should not create/delete objects!
2412 final int calcNearestObjectDist (int px, int py, optional scope bool delegate (MapObject o) dg, optional class!MapObject castClass) {
2413 auto e = findNearestObject(px, py, dg!optional, castClass!optional);
2414 if (!e) return int.max;
2415 int xc = px-e.xCenter, yc = py-e.yCenter;
2416 return round(sqrt(xc*xc+yc*yc));
2420 // WARNING! delegate should not create/delete objects!
2421 final MapTile findNearestMoveableSolid (int px, int py, optional scope bool delegate (MapTile t) dg) {
2423 int curdistsq = int.max;
2424 foreach (MapTile t; objGrid.allObjects(MapTile)) {
2425 if (t.spectral) continue;
2427 if (!dg(t)) continue;
2429 if (!t.solid || !t.moveable) continue;
2431 int xc = px-t.xCenter, yc = py-t.yCenter;
2432 int distsq = xc*xc+yc*yc;
2433 if (distsq < curdistsq) {
2442 // WARNING! delegate should not create/delete objects!
2443 final MapTile findNearestTile (int px, int py, optional scope bool delegate (MapTile t) dg) {
2444 if (!dg) return none;
2446 int curdistsq = int.max;
2448 //FIXME: make this faster!
2449 foreach (MapTile t; objGrid.allObjects(MapTile)) {
2450 if (t.spectral) continue;
2451 int xc = px-t.xCenter, yc = py-t.yCenter;
2452 int distsq = xc*xc+yc*yc;
2453 if (distsq < curdistsq && dg(t)) {
2463 // ////////////////////////////////////////////////////////////////////////// //
2464 final bool cbIsObjectWeb (MapObject o) { return (o isa ItemWeb); }
2465 final bool cbIsObjectBlob (MapObject o) { return (o isa EnemyBlob); }
2466 final bool cbIsObjectArrow (MapObject o) { return (o isa ItemProjectileArrow); }
2467 final bool cbIsObjectEnemy (MapObject o) { return (/*o != self &&*/ o isa MapEnemy); }
2469 final bool cbIsObjectTreasure (MapObject o) { return (o.isTreasure); }
2471 final bool cbIsItemObject (MapObject o) { return (o isa MapItem); }
2473 final bool cbIsItemOrEnemyObject (MapObject o) { return (o isa MapItem || o isa MapEnemy); }
2476 final MapObject isObjectAtTile (int tileX, int tileY, optional scope bool delegate (MapObject o) dg, optional bool precise) {
2477 if (!specified_precise) precise = true;
2480 foreach (MapObject o; objGrid.inRectPix(tileX, tileY, 16, 16, precise:precise, castClass:MapObject)) {
2481 if (o.spectral) continue;
2483 if (dg(o)) return o;
2492 final MapObject isObjectAtTilePix (int x, int y, optional scope bool delegate (MapObject o) dg) {
2493 return isObjectAtTile(x/16, y/16, dg!optional);
2497 final MapObject isObjectAtPoint (int xpos, int ypos, optional scope bool delegate (MapObject o) dg, optional bool precise, optional class!MapObject castClass) {
2498 if (!specified_precise) precise = true;
2499 if (!castClass) castClass = MapObject;
2500 foreach (MapObject o; objGrid.inCellPix(xpos, ypos, precise:precise, castClass:castClass)) {
2501 if (o.spectral) continue;
2503 if (dg(o)) return o;
2505 if (o isa MapEnemy) return o;
2512 final MapObject isObjectInRect (int xpos, int ypos, int w, int h, optional scope bool delegate (MapObject o) dg, optional bool precise, optional class!MapObject castClass) {
2513 if (w < 1 || h < 1) return none;
2514 if (!castClass) castClass = MapObject;
2515 if (w == 1 && h == 1) return isObjectAtPoint(xpos, ypos, dg!optional, precise!optional, castClass);
2516 if (!specified_precise) precise = true;
2517 foreach (MapObject o; objGrid.inRectPix(xpos, ypos, w, h, precise:precise, castClass:castClass)) {
2518 if (o.spectral) continue;
2520 if (dg(o)) return o;
2522 if (o isa MapEnemy) return o;
2529 final MapObject forEachObject (scope bool delegate (MapObject o) dg, optional bool allowSpectrals, optional class!MapObject castClass) {
2530 if (!dg) return none;
2531 if (!castClass) castClass = MapObject;
2532 foreach (MapObject o; objGrid.allObjectsSafe(castClass)) {
2533 if (!allowSpectrals && o.spectral) continue;
2534 if (dg(o)) return o;
2540 final MapObject forEachObjectAtPoint (int xpos, int ypos, scope bool delegate (MapObject o) dg, optional bool precise) {
2541 if (!dg) return none;
2542 if (!specified_precise) precise = true;
2543 foreach (MapObject o; objGrid.inCellPix(xpos, ypos, precise:precise, castClass:MapObject)) {
2544 if (o.spectral) continue;
2545 if (dg(o)) return o;
2551 final MapObject forEachObjectInRect (int xpos, int ypos, int w, int h, scope bool delegate (MapObject o) dg, optional bool precise) {
2552 if (!dg || w < 1 || h < 1) return none;
2553 if (w == 1 && h == 1) return forEachObjectAtPoint(xpos, ypos, dg!optional, precise!optional);
2554 if (!specified_precise) precise = true;
2555 foreach (MapObject o; objGrid.inRectPix(xpos, ypos, w, h, precise:precise, castClass:MapObject)) {
2556 if (o.spectral) continue;
2557 if (dg(o)) return o;
2563 final MapEntity forEachEntityInRect (int xpos, int ypos, int w, int h, scope bool delegate (MapEntity o) dg, optional bool precise, optional class!MapEntity castClass) {
2564 if (!dg || w < 1 || h < 1) return none;
2565 if (!castClass) castClass = MapEntity;
2566 if (!specified_precise) precise = true;
2567 foreach (MapEntity e; objGrid.inRectPix(xpos, ypos, w, h, precise:precise, castClass:castClass)) {
2568 if (e.spectral) continue;
2569 if (dg(e)) return e;
2575 private final bool cbIsRopeTile (MapTile t) { return (t isa MapTileRope); }
2577 final MapTile isRopeAtPoint (int px, int py) {
2578 return checkTileAtPoint(px, py, &cbIsRopeTile);
2583 final MapTile isWaterSwimAtPoint (int px, int py) {
2584 return isWaterAtPoint(px, py);
2588 // ////////////////////////////////////////////////////////////////////////// //
2589 private array!MapEntity tmpEntityList;
2591 private final bool cbCollectEntitiesWithMask (MapEntity t) {
2592 if (!t.visible || t.spectral) return false;
2593 tmpEntityList[$] = t;
2598 final void touchEntitiesWithMask (int x, int y, SpriteFrame frm, scope bool delegate (MapEntity t) dg, optional class!MapEntity castClass) {
2599 if (!frm || frm.bw < 1 || frm.bh < 1 || !dg) return;
2600 if (frm.isEmptyPixelMask) return;
2601 if (!castClass) castClass = MapEntity;
2603 if (tmpEntityList.length) tmpEntityList.clear();
2604 if (player isa castClass && player.isRectCollisionFrame(frm, x, y)) tmpEntityList[$] = player;
2605 forEachEntityInRect(x+frm.bx, y+frm.by, frm.bw, frm.bh, &cbCollectEntitiesWithMask, castClass:castClass);
2606 foreach (MapEntity e; tmpEntityList) {
2607 if (!e || !e.isInstanceAlive || !e.visible || e.spectral) continue;
2608 if (e.isRectCollisionFrame(frm, x, y)) {
2615 // ////////////////////////////////////////////////////////////////////////// //
2616 final bool cbCollisionAnySolid (MapTile t) { return t.solid; }
2617 final bool cbCollisionAnyMoveableSolid (MapTile t) { return t.solid && t.moveable; }
2618 final bool cbCollisionLiquid (MapTile t) { return (t.water || t.lava); }
2619 final bool cbCollisionAnySolidOrLiquid (MapTile t) { return (t.solid || t.water || t.lava); }
2620 final bool cbCollisionLadder (MapTile t) { return t.ladder; }
2621 final bool cbCollisionLadderTop (MapTile t) { return t.laddertop; }
2622 final bool cbCollisionAnyLadder (MapTile t) { return (t.ladder || t.laddertop); }
2623 final bool cbCollisionAnySolidIce (MapTile t) { return (t.solid && t.ice); }
2624 final bool cbCollisionWater (MapTile t) { return t.water; }
2625 final bool cbCollisionLava (MapTile t) { return t.lava; }
2626 final bool cbCollisionAnyTree (MapTile t) { return t.tree; }
2627 final bool cbCollisionPlatform (MapTile t) { return t.platform; }
2628 final bool cbCollisionLadderOrPlatform (MapTile t) { return (t.ladder || t.platform); }
2629 //final bool cbCollisionAnyHangeable (MapTile t) { return t.hangeable; } // no need to check for solids here, it is done in MapTile initializer
2630 final bool cbCollisionSpringTrap (MapTile t) { return t.springtrap; }
2631 final bool cbCollisionSpikes (MapTile t) { return t.spikes; }
2632 final bool cbCollisionExitTile (MapTile t) { return t.isExitActive(); }
2634 final bool cbCollisionForWhoa (MapTile t) { return (t.solid || t.laddertop); }
2636 //final bool cbCollisionSacAltar (MapTile t) { return (t.objName == 'oSacAltarLeft'); } // only left part, yeah
2637 final bool cbCollisionSacAltar (MapTile t) { return t.sacrificingAltar; }
2640 // ////////////////////////////////////////////////////////////////////////// //
2641 transient MapTileTemp tempSolidTile;
2643 private final MapTileTemp makeWalkeableSolidTile (MapObject o) {
2644 if (!tempSolidTile) {
2645 tempSolidTile = SpawnObject(MapTileTemp);
2646 } else if (!tempSolidTile.isInstanceAlive) {
2647 delete tempSolidTile;
2648 tempSolidTile = SpawnObject(MapTileTemp);
2651 tempSolidTile.level = self;
2652 tempSolidTile.global = global;
2653 tempSolidTile.solid = true;
2654 tempSolidTile.objName = MapTileTemp.default.objName;
2655 tempSolidTile.objType = MapTileTemp.default.objType;
2656 tempSolidTile.e = o;
2657 tempSolidTile.fltx = o.fltx;
2658 tempSolidTile.flty = o.flty;
2659 return tempSolidTile;
2663 final MapTile checkTilesInRect (int x0, int y0, const int w, const int h,
2664 optional scope bool delegate (MapTile dg) dg, optional bool precise,
2665 optional class!MapTile castClass)
2667 if (w < 1 || h < 1) return none;
2668 if (w == 1 && h == 1) return checkTileAtPoint(x0, y0, dg!optional);
2669 int x1 = x0+w-1, y1 = y0+h-1;
2670 if (x0 >= tilesWidth*16 || y0 >= tilesHeight*16 || x1 < 0 || y1 < 0) return none;
2671 if (!specified_precise) precise = true;
2672 if (!castClass) castClass = MapTile;
2673 if (!dg) dg = &cbCollisionAnySolid;
2675 // check walkable solid objects too
2676 foreach (MapEntity e; objGrid.inRectPix(x0, y0, w, h, precise:precise, castClass:castClass)) {
2677 if (e.spectral || !e.visible) continue;
2678 auto t = MapTile(e);
2680 if (dg(t)) return t;
2683 auto o = MapObject(e);
2684 if (o && o.walkableSolid) {
2685 t = makeWalkeableSolidTile(o);
2686 if (dg(t)) return t;
2695 final MapTile checkTileAtPoint (int x0, int y0, optional scope bool delegate (MapTile dg) dg, optional bool precise, optional class!MapTile castClass) {
2696 if (x0 < 0 || y0 < 0 || x0 >= tilesWidth*16 || y0 >= tilesHeight*16) return none;
2697 if (!specified_precise) precise = true;
2698 if (!castClass) castClass = MapTile;
2699 if (!dg) dg = &cbCollisionAnySolid;
2701 // check walkable solid objects
2702 foreach (MapEntity e; objGrid.inCellPix(x0, y0, precise:precise, castClass:castClass)) {
2703 if (e.spectral || !e.visible) continue;
2704 auto t = MapTile(e);
2706 if (dg(t)) return t;
2709 auto o = MapObject(e);
2710 if (o && o.walkableSolid) {
2711 t = makeWalkeableSolidTile(o);
2712 if (dg(t)) return t;
2721 // ////////////////////////////////////////////////////////////////////////// //
2722 final MapTile isSolidAtPoint (int px, int py) { return checkTileAtPoint(px, py); }
2723 final MapTile isSolidInRect (int rx, int ry, int rw, int rh) { return checkTilesInRect(rx, ry, rw, rh); }
2724 final MapTile isLadderAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadder); }
2725 final MapTile isLadderOrPlatformAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadderOrPlatform); }
2726 final MapTile isLadderTopAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadderTop); }
2727 final MapTile isAnyLadderAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyLadder); }
2728 final MapTile isWaterAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionWater); }
2729 final MapTile isLavaAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLava); }
2730 final MapTile isLiquidAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLiquid); }
2731 final MapTile isIceAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnySolidIce); }
2732 final MapTile isTreeAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyTree); }
2733 //final MapTile isHangeableAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyHangeable); }
2736 // ////////////////////////////////////////////////////////////////////////// //
2737 final void setTileTypeAt (int tileX, int tileY, LevelGen::RType rt) {
2738 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) roomType[tileX, tileY] = rt;
2742 //FIXME: make this faster
2743 transient float gtagX, gtagY;
2745 // only non-moveables and non-specials
2746 final MapTile getTileAtGrid (int tileX, int tileY) {
2749 return checkTileAtPoint(tileX*16, tileY*16, delegate bool (MapTile t) {
2750 if (t.spectral || t.moveable || t.toSpecialGrid || !t.visible) return false;
2751 if (t.fltx != gtagX || t.flty != gtagY) return false; // emulate grid
2752 if (t.width != 16 || t.height != 16) return false;
2755 //return (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight ? tiles[tileX, tileY] : none);
2759 final MapTile getTileAtGridAny (int tileX, int tileY) {
2762 return checkTileAtPoint(tileX*16, tileY*16, delegate bool (MapTile t) {
2763 if (t.spectral /*|| t.moveable*/ || !t.visible) return false;
2764 if (t.fltx != gtagX || t.flty != gtagY) return false; // emulate grid
2765 if (t.width != 16 || t.height != 16) return false;
2768 //return (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight ? tiles[tileX, tileY] : none);
2772 final bool isNamedTileAt (int tileX, int tileY, name atypename) {
2773 if (!atypename) return false;
2774 auto t = getTileAtGridAny(tileX, tileY);
2775 return (t && t.objName == atypename);
2779 final void setTileAtGrid (int tileX, int tileY, MapTile tile) {
2780 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
2782 tile.fltx = tileX*16;
2783 tile.flty = tileY*16;
2784 if (!tile.dontReplaceOthers) {
2785 auto osp = tile.spectral;
2786 tile.spectral = true;
2787 auto t = getTileAtGridAny(tileX, tileY);
2788 tile.spectral = osp;
2789 if (t && !t.immuneToReplacement) {
2790 writeln("REPLACING tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
2791 writeln(" NEW tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (tile.toSpecialGrid ? "special " : ""), "grid, pos=(", tile.fltx/16.0, ",", tile.flty/16.0, ")");
2797 auto t = getTileAtGridAny(tileX, tileY);
2798 if (t && !t.immuneToReplacement) {
2799 writeln("REMOVING tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
2807 // ////////////////////////////////////////////////////////////////////////// //
2808 // return `true` from delegate to stop
2809 MapTile forEachSolidTileOnGrid (scope bool delegate (int tileX, int tileY, MapTile t) dg) {
2810 if (!dg) return none;
2811 foreach (MapTile t; objGrid.allObjectsSafe(MapTile)) {
2812 if (t.spectral || !t.solid || !t.visible) continue;
2813 if (t.ix%16 != 0 || t.iy%16 != 0) continue; // emulate grid
2814 if (t.width != 16 || t.height != 16) continue;
2815 if (dg(t.ix/16, t.iy/16, t)) return t;
2821 // ////////////////////////////////////////////////////////////////////////// //
2822 // return `true` from delegate to stop
2823 MapTile forEachTile (scope bool delegate (MapTile t) dg, optional class!MapTile castClass) {
2824 if (!dg) return none;
2825 if (!castClass) castClass = MapTile;
2826 foreach (MapTile t; objGrid.allObjectsSafe(castClass)) {
2827 if (t.spectral || !t.visible) continue;
2828 if (dg(t)) return t;
2834 // ////////////////////////////////////////////////////////////////////////// //
2835 final void fixWallTiles () {
2836 foreach (MapTile t; objGrid.allObjectsSafe(MapTile)) t.beautifyTile();
2840 // ////////////////////////////////////////////////////////////////////////// //
2841 final MapTile isCollisionAtPoint (int px, int py, optional scope bool delegate (MapTile dg) dg) {
2842 if (!dg) dg = &cbCollisionAnySolid;
2843 return checkTilesInRect(px, py, 1, 1, dg);
2847 // ////////////////////////////////////////////////////////////////////////// //
2848 string scrGetKaliGift (MapTile altar, optional name gift) {
2851 // find other side of the altar
2852 int sx = player.ix, sy = player.iy;
2856 auto a2 = isCollisionAtPoint(altar.ix+18, altar.iy+8, &cbCollisionSacAltar);
2857 if (!a2) a2 = isCollisionAtPoint(altar.ix-8, altar.iy+8, &cbCollisionSacAltar);
2858 if (a2) { sx = a2.ix; sy = a2.iy; }
2861 if (global.favor <= -8) res = "SHE SEEMS VERY ANGRY WITH YOU!";
2862 else if (global.favor < 0) res = "SHE SEEMS ANGRY WITH YOU.";
2863 else if (global.favor == 0) res = "SHE HAS FORGIVEN YOU!";
2864 else if (global.favor >= 32) {
2865 if (global.kaliGift >= 3 && global.favor >= 32+(global.kaliGift-2)*16) {
2866 res = "YOU FEEL INVIGORATED!";
2867 global.kaliGift += 1;
2868 global.plife += global.randOther(4, 8);
2869 } else if (global.kaliGift >= 3) {
2870 res = "SHE SEEMS ECSTATIC WITH YOU!";
2871 } else if (global.bombs < 80) {
2872 res = "YOUR SATCHEL FEELS VERY FULL NOW!";
2873 global.kaliGift = 3;
2876 res = "YOU FEEL INVIGORATED!";
2877 global.kaliGift += 1;
2878 global.plife += global.randOther(4, 8);
2880 } else if (global.favor >= 16) {
2881 if (global.kaliGift >= 2) {
2882 res = "SHE SEEMS VERY HAPPY WITH YOU!";
2884 res = "SHE BESTOWS A GIFT UPON YOU!";
2885 global.kaliGift = 2;
2887 auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2890 obj = MakeMapObject(sx, sy-8, 'oPoof');
2895 if (gift) obj = MakeMapObject(sx, sy-8, gift);
2896 if (!obj) obj = MakeMapObject(sx, sy-8, 'oKapala');
2898 } else if (global.favor >= 8) {
2899 if (global.kaliGift >= 1) {
2900 res = "SHE SEEMS HAPPY WITH YOU.";
2902 res = "SHE BESTOWS A GIFT UPON YOU!";
2903 global.kaliGift = 1;
2904 //rAltar = instance_nearest(x, y, oSacAltarRight);
2905 //if (instance_exists(rAltar)) {
2907 auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2910 obj = MakeMapObject(sx, sy-8, 'oPoof');
2914 if (gift) obj = MakeMapObject(sx, sy-8, gift);
2916 auto n = global.randOther(1, 8);
2920 if (n == 1 && !global.hasCape && !global.hasJetpack) aname = 'oCapePickup';
2921 else if (n == 2 && !global.hasGloves) aname = 'oGloves';
2922 else if (n == 3 && !global.hasSpectacles) aname = 'oSpectacles';
2923 else if (n == 4 && !global.hasMitt) aname = 'oMitt';
2924 else if (n == 5 && !global.hasSpringShoes) aname = 'oSpringShoes';
2925 else if (n == 6 && !global.hasSpikeShoes) aname = 'oSpikeShoes';
2926 else if (n == 7 && !global.hasStickyBombs) aname = 'oPaste';
2927 else if (n == 8 && !global.hasCompass) aname = 'oCompass';
2929 obj = MakeMapObject(sx, sy-8, aname);
2935 obj = MakeMapObject(sx, sy-8, (global.hasJetpack ? 'oBombBox' : 'oJetpack'));
2941 } else if (global.favor > 0) {
2942 res = "SHE SEEMS PLEASED WITH YOU.";
2947 global.message = "";
2948 res = "KALI DEVOURS YOU!"; // sacrifice is player
2956 void performSacrifice (MapObject what, MapTile where) {
2957 if (!what || !what.isInstanceAlive) return;
2958 MakeMapObject(what.ix+8, what.iy+8, 'oFlame');
2959 if (where) where.playSound('sndSmallExplode'); else what.playSound('sndSmallExplode');
2960 what.spillBlood(amount:3, forced:true);
2962 string msg = "KALI ACCEPTS THE SACRIFICE!";
2964 auto idol = ItemGoldIdol(what);
2966 ++stats.totalSacrifices;
2967 if (global.favor <= -8) msg = "SHE SEEMS VERY ANGRY WITH YOU!";
2968 else if (global.favor < 0) msg = "SHE SEEMS ANGRY WITH YOU.";
2969 else if (global.favor >= 0) {
2970 // find other side of the altar
2971 int sx = player.ix, sy = player.iy;
2976 auto a2 = isCollisionAtPoint(altar.ix+18, altar.iy+8, &cbCollisionSacAltar);
2977 if (!a2) a2 = isCollisionAtPoint(altar.ix-8, altar.iy+8, &cbCollisionSacAltar);
2978 if (a2) { sx = a2.ix; sy = a2.iy; }
2981 auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2984 obj = MakeMapObject(sx, sy-8, 'oPoof');
2988 obj = MakeMapObject(sx, sy-16-8, 'oMonkeyGold');
2990 osdMessage(msg, 6.66);
2992 idol.instanceRemove();
2996 if (global.favor <= -8) {
2997 msg = "KALI DEVOURS THE SACRIFICE!";
2998 } else if (global.favor < 0) {
2999 global.favor += (what.status == MapObject::STUNNED ? what.favor : round(what.favor/2.0)); //k8: added `round()`
3000 if (what.favor > 0) what.favor = 0;
3002 global.favor += (what.status == MapObject::STUNNED ? what.favor : round(what.favor/2.0)); //k8: added `round()`
3006 if (myAltar.gift1 != "" and global.kaliGift == 0) scrGetKaliGift(myAltar.gift1);
3007 else if (myAltar.gift2 != "" and global.kaliGift == 1) scrGetKaliGift(myAltar.gift2);
3008 else scrGetKaliGift("");
3011 // sacrifice is player?
3012 if (what isa PlayerPawn) {
3013 ++stats.totalSelfSacrifices;
3014 msg = "KALI DEVOURS YOU!";
3015 player.visible = false;
3016 player.removeBallAndChain(temp:true);
3018 player.status = MapObject::DEAD;
3020 ++stats.totalSacrifices;
3021 auto msg2 = scrGetKaliGift(where);
3022 what.instanceRemove();
3023 if (msg2) msg = va("%s\n%s", msg, msg2);
3026 osdMessage(msg, 6.66);
3032 // ////////////////////////////////////////////////////////////////////////// //
3033 final void addBackgroundGfxDetails () {
3034 // add background details
3035 //if (global.customLevel) return;
3037 // bg = instance_create(16*randRoom(1, 42), 16*randRoom(1, 33), oCaveBG);
3038 if (global.levelType == 1 && global.randRoom(1, 3) < 3) MakeMapBackTile('bgExtrasLush', 32*global.randRoom(0, 1), 0, 32, 32, 16*global.randRoom(1, 42), 16*global.randRoom(1, 33), 10002);
3039 else if (global.levelType == 2 && global.randRoom(1, 3) < 3) MakeMapBackTile('bgExtrasIce', 32*global.randRoom(0, 1), 0, 32, 32, 16*global.randRoom(1, 42), 16*global.randRoom(1, 33), 10002);
3040 else if (global.levelType == 3 && global.randRoom(1, 3) < 3) MakeMapBackTile('bgExtrasTemple', 32*global.randRoom(0, 1), 0, 32, 32, 16*global.randRoom(1, 42), 16*global.randRoom(1, 33), 10002);
3041 else MakeMapBackTile('bgExtras', 32*global.randRoom(0, 1), 0, 32, 32, 16*global.randRoom(1, 42), 16*global.randRoom(1, 33), 10002);
3046 // ////////////////////////////////////////////////////////////////////////// //
3047 private final void fixRealViewStart () {
3048 int scale = global.scale;
3049 realViewStart.x = clamp(realViewStart.x, viewMin.x*scale, viewMax.x*scale-viewWidth);
3050 realViewStart.y = clamp(realViewStart.y, viewMin.y*scale, viewMax.y*scale-viewHeight);
3054 final int cameraCurrX () { return realViewStart.x/global.scale; }
3055 final int cameraCurrY () { return realViewStart.y/global.scale; }
3058 private final void fixViewStart () {
3059 int scale = global.scale;
3060 viewStart.x = clamp(viewStart.x, viewMin.x*scale, viewMax.x*scale-viewWidth);
3061 viewStart.y = clamp(viewStart.y, viewMin.y*scale, viewMax.y*scale-viewHeight);
3065 final void centerViewAtPlayer () {
3066 if (viewWidth < 1 || viewHeight < 1 || !player) return;
3067 centerViewAt(player.xCenter, player.yCenter);
3071 final void centerViewAt (int x, int y) {
3072 if (viewWidth < 1 || viewHeight < 1) return;
3074 cameraSlideToSpeed.x = 0;
3075 cameraSlideToSpeed.y = 0;
3076 cameraSlideToPlayer = 0;
3078 int scale = global.scale;
3081 realViewStart.x = clamp(x-viewWidth/2, 0, tilesWidth*16*scale-viewWidth);
3082 realViewStart.y = clamp(y-viewHeight/2, 0, tilesHeight*16*scale-viewHeight);
3085 viewStart.x = realViewStart.x;
3086 viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
3089 if (onCameraTeleported) onCameraTeleported();
3093 const int ViewPortToleranceX = 16*1+8;
3094 const int ViewPortToleranceY = 16*1+8;
3096 final void fixCamera () {
3097 if (!player) return;
3098 if (viewWidth < 1 || viewHeight < 1) return;
3099 int scale = global.scale;
3100 auto alwaysCenterX = global.config.alwaysCenterPlayer;
3101 auto alwaysCenterY = alwaysCenterX;
3102 // calculate offset from viewport center (in game units), and fix viewport
3104 int camDestX = player.ix+8;
3105 int camDestY = player.iy+8;
3106 if (!cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y) != 0) {
3107 // slide camera to point
3108 if (cameraSlideToSpeed.x) camDestX = cameraSlideToCurr.x;
3109 if (cameraSlideToSpeed.y) camDestY = cameraSlideToCurr.y;
3110 int dx = cameraSlideToDest.x-camDestX;
3111 int dy = cameraSlideToDest.y-camDestY;
3112 //writeln("speed:(", cameraSlideToSpeed.x, ",", cameraSlideToSpeed.y, "); cur:(", camDestX, ",", camDestY, "); dest:(", cameraSlideToDest.x, ",", cameraSlideToDest.y, "); delta=(", dx, ",", dy, ")");
3113 if (dx && cameraSlideToSpeed.x != 0) {
3114 alwaysCenterX = true;
3115 if (abs(dx) <= abs(cameraSlideToSpeed.x)) {
3116 camDestX = cameraSlideToDest.x;
3118 camDestX += sign(dx)*abs(cameraSlideToSpeed.x);
3121 if (dy && abs(cameraSlideToSpeed.y) != 0) {
3122 alwaysCenterY = true;
3123 if (abs(dy) <= cameraSlideToSpeed.y) {
3124 camDestY = cameraSlideToDest.y;
3126 camDestY += sign(dy)*abs(cameraSlideToSpeed.y);
3129 //writeln(" new:(", camDestX, ",", camDestY, ")");
3130 if (cameraSlideToSpeed.x) cameraSlideToCurr.x = camDestX;
3131 if (cameraSlideToSpeed.y) cameraSlideToCurr.y = camDestY;
3135 if (cameraSlideToSpeed.x && !cameraSlideToPlayer) {
3136 realViewStart.x = clamp(camDestX*scale, 0, tilesWidth*16*scale-viewWidth);
3137 } else if (!player.cameraBlockX) {
3138 int x = camDestX*scale;
3139 int cx = realViewStart.x;
3140 if (alwaysCenterX) {
3143 int xofs = x-(cx+viewWidth/2);
3144 if (xofs < -ViewPortToleranceX*scale) cx += xofs+ViewPortToleranceX*scale;
3145 else if (xofs > ViewPortToleranceX*scale) cx += xofs-ViewPortToleranceX*scale;
3147 // slide back to player?
3148 if (cameraSlideToPlayer && cameraSlideToSpeed.x) {
3149 int prevx = cameraSlideToCurr.x*scale;
3150 int dx = (cx-prevx)/scale;
3151 if (abs(dx) <= cameraSlideToSpeed.x) {
3152 writeln("BACKSLIDE X COMPLETE!");
3153 cameraSlideToSpeed.x = 0;
3155 cx = prevx+(sign(dx)*cameraSlideToSpeed.x)*scale;
3156 cameraSlideToCurr.x += sign(dx)*cameraSlideToSpeed.x;
3157 if ((dx < 0 && cx <= 0) || (dx > 0 && cx >= tilesWidth*16*scale-viewWidth)) {
3158 writeln("BACKSLIDE X COMPLETE!");
3159 cameraSlideToSpeed.x = 0;
3163 realViewStart.x = clamp(cx, 0, tilesWidth*16*scale-viewWidth);
3167 if (cameraSlideToSpeed.y && !cameraSlideToPlayer) {
3168 realViewStart.y = clamp(camDestY*scale, 0, tilesHeight*16*scale-viewHeight);
3169 } else if (!player.cameraBlockY) {
3170 int y = camDestY*scale;
3171 int cy = realViewStart.y;
3172 if (alwaysCenterY) {
3173 cy = y-viewHeight/2;
3175 int yofs = y-(cy+viewHeight/2);
3176 if (yofs < -ViewPortToleranceY*scale) cy += yofs+ViewPortToleranceY*scale;
3177 else if (yofs > ViewPortToleranceY*scale) cy += yofs-ViewPortToleranceY*scale;
3179 // slide back to player?
3180 if (cameraSlideToPlayer && cameraSlideToSpeed.y) {
3181 int prevy = cameraSlideToCurr.y*scale;
3182 int dy = (cy-prevy)/scale;
3183 if (abs(dy) <= cameraSlideToSpeed.y) {
3184 writeln("BACKSLIDE Y COMPLETE!");
3185 cameraSlideToSpeed.y = 0;
3187 cy = prevy+(sign(dy)*cameraSlideToSpeed.y)*scale;
3188 cameraSlideToCurr.y += sign(dy)*cameraSlideToSpeed.y;
3189 if ((dy < 0 && cy <= 0) || (dy > 0 && cy >= tilesHeight*16*scale-viewHeight)) {
3190 writeln("BACKSLIDE Y COMPLETE!");
3191 cameraSlideToSpeed.y = 0;
3195 realViewStart.y = clamp(cy, 0, tilesHeight*16*scale-viewHeight);
3198 if (cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y) == 0) cameraSlideToPlayer = 0;
3201 //writeln(" new2:(", cameraCurrX, ",", cameraCurrY, ")");
3203 viewStart.x = realViewStart.x;
3204 viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
3209 // ////////////////////////////////////////////////////////////////////////// //
3210 // x0 and y0 are non-scaled (and will be scaled)
3211 final void drawSpriteAt (name sprName, float frnumf, int x0, int y0, optional bool small) {
3212 if (!sprName) return;
3213 auto spr = sprStore[sprName];
3214 if (!spr || !spr.frames.length) return;
3215 int scale = global.scale;
3218 int frnum = max(0, trunc(frnumf))%spr.frames.length;
3219 auto sfr = spr.frames[frnum];
3220 int sx0 = x0-sfr.xofs*scale;
3221 int sy0 = y0-sfr.yofs*scale;
3222 if (small && scale > 1) {
3223 sfr.tex.blitExt(sx0, sy0, round(sx0+sfr.tex.width*(scale/2.0)), round(sy0+sfr.tex.height*(scale/2.0)), 0, 0);
3225 sfr.tex.blitAt(sx0, sy0, scale);
3230 final void drawSpriteAtS3 (name sprName, float frnumf, int x0, int y0) {
3231 if (!sprName) return;
3232 auto spr = sprStore[sprName];
3233 if (!spr || !spr.frames.length) return;
3236 int frnum = max(0, trunc(frnumf))%spr.frames.length;
3237 auto sfr = spr.frames[frnum];
3238 int sx0 = x0-sfr.xofs*3;
3239 int sy0 = y0-sfr.yofs*3;
3240 sfr.tex.blitAt(sx0, sy0, 3);
3244 // x0 and y0 are non-scaled (and will be scaled)
3245 final void drawTextAt (int x0, int y0, string text, optional int scale, optional int hiColor1, optional int hiColor2) {
3247 if (!specified_scale) scale = global.scale;
3250 sprStore.renderTextWithHighlight(x0, y0, text, scale, hiColor1!optional, hiColor2!optional);
3254 void renderCompass (float currFrameDelta) {
3255 if (!global.hasCompass) return;
3258 if (isRoom("rOlmec")) {
3261 } else if (isRoom("rOlmec2")) {
3267 bool hasMessage = osdHasMessage();
3268 foreach (MapTile et; allExits) {
3270 int exitX = et.ix, exitY = et.iy;
3271 int vx0 = viewStart.x/global.scale, vy0 = viewStart.y/global.scale;
3272 int vx1 = (viewStart.x+viewWidth)/global.scale;
3273 int vy1 = (viewStart.y+viewHeight)/global.scale;
3274 if (exitY > vy1-16) {
3276 drawSpriteAt(hasMessage ? 'sCompassSmallLL' : 'sCompassLL', 0, 0, 224);
3277 } else if (exitX > vx1-16) {
3278 drawSpriteAt(hasMessage ? 'sCompassSmallLR' : 'sCompassLR', 0, 304, 224);
3280 drawSpriteAt(hasMessage ? 'sCompassSmallDown' : 'sCompassDown', 0, exitX-vx0, 224);
3282 } else if (exitX < vx0) {
3283 drawSpriteAt(hasMessage ? 'sCompassSmallLeft' : 'sCompassLeft', 0, 0, exitY-vy0);
3284 } else if (exitX > vx1-16) {
3285 drawSpriteAt(hasMessage ? 'sCompassSmallRight' : 'sCompassRight', 0, 304, exitY-vy0);
3287 break; // only the first exit
3292 final bool totalsNameCmpCB (ref GameStats::TotalItem a, ref GameStats::TotalItem b) {
3293 auto sa = string(a.objName);
3294 auto sb = string(b.objName);
3298 void renderTransitionInfo (float currFrameDelta) {
3301 GameStats.sortTotalsList(stats.kills, &totalsNameCmpCB);
3304 foreach (int idx, ref auto k; stats.kills) {
3305 string s = string(k);
3306 maxLen = max(maxLen, s.length);
3310 sprStore.loadFont('sFontSmall');
3311 Video.color = 0xff_ff_00;
3312 foreach (int idx, ref auto k; stats.kills) {
3314 foreach (int xidx, ref auto d; stats.totalKills) {
3315 if (d.objName == k) { deaths = d.count; break; }
3317 //drawTextAt(16, 4+idx*8, va("%s: %d (%d)", string(k.objName).toUpperCase, k.count, deaths));
3318 drawTextAt(16, 4+idx*8, string(k).toUpperCase);
3319 drawTextAt(16+maxLen, 4+idx*8, va(": %d (%d)", k.count, deaths));
3325 void renderGhostTimer (float currFrameDelta) {
3326 if (ghostTimeLeft <= 0) return;
3327 //ghostTimeLeft /= 30; // frames -> seconds
3329 int hgt = viewHeight-64;
3330 if (hgt < 1) return;
3331 int rhgt = round(float(hgt)*(float(ghostTimeLeft)/30.0)/1000.0);
3332 //writeln("hgt=", hgt, "; rhgt=", rhgt, "; gtl=", ghostTimeLeft/30);
3334 auto oclr = Video.color;
3335 Video.color = 0xcf_ff_7f_00;
3336 Video.fillRect(viewWidth-20, 32, 16, hgt-rhgt);
3337 Video.color = 0x7f_ff_7f_00;
3338 Video.fillRect(viewWidth-20, 32+(hgt-rhgt), 16, rhgt);
3344 void renderStarsHUD (float currFrameDelta) {
3345 bool scumSmallHud = global.config.scumSmallHud;
3347 //auto life = max(0, global.plife);
3348 int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3349 int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3350 //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3355 sprStore.loadFont('sFontSmall');
3358 sprStore.loadFont('sFont');
3362 Video.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3363 //draw_sprite(sHeart, -1, view_xview[0]+8, view_yview[0]+8);
3364 //draw_text(view_xview[0]+24, view_yview[0]+8, life);
3366 if (global.plife == 1) {
3367 drawSpriteAt('sHeartSmallBlink', global.heartBlink, 10, hhup-4);
3368 global.heartBlink += 0.1;
3369 if (global.heartBlink > 3) global.heartBlink = 0;
3371 drawSpriteAt('sHeartSmall', -1, 10, hhup-4);
3372 global.heartBlink = 0;
3375 if (global.plife == 1) {
3376 drawSpriteAt('sHeartBlink', global.heartBlink, 8, hhup);
3377 global.heartBlink += 0.1;
3378 if (global.heartBlink > 3) global.heartBlink = 0;
3380 drawSpriteAt('sHeart', -1, 8, hhup);
3381 global.heartBlink = 0;
3384 int life = clamp(global.plife, 0, 99);
3385 drawTextAt(16+8, hhup, va("%d", life));
3387 //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3388 drawSpriteAt('sShopkeeperIcon', -1, 64, hhup, scumSmallHud);
3389 drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", starsKills));
3391 if (starsRoomTimer1 > 0) {
3392 sprStore.loadFont('sFontSmall');
3393 Video.color = 0xff_ff_00;
3394 int scale = global.scale;
3395 sprStore.renderMultilineTextCentered(viewWidth/2, 216*scale, va("SHOTGUN CHALLENGE BEGINS IN ~%d~", (starsRoomTimer1/30)+1), scale, 0xff_00_00);
3400 void renderSunHUD (float currFrameDelta) {
3401 bool scumSmallHud = global.config.scumSmallHud;
3403 //auto life = max(0, global.plife);
3404 int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3405 int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3406 //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3411 sprStore.loadFont('sFontSmall');
3414 sprStore.loadFont('sFont');
3418 Video.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3419 //draw_sprite(sHeart, -1, view_xview[0]+8, view_yview[0]+8);
3420 //draw_text(view_xview[0]+24, view_yview[0]+8, life);
3422 if (global.plife == 1) {
3423 drawSpriteAt('sHeartSmallBlink', global.heartBlink, 10, hhup-4);
3424 global.heartBlink += 0.1;
3425 if (global.heartBlink > 3) global.heartBlink = 0;
3427 drawSpriteAt('sHeartSmall', -1, 10, hhup-4);
3428 global.heartBlink = 0;
3431 if (global.plife == 1) {
3432 drawSpriteAt('sHeartBlink', global.heartBlink, 8, hhup);
3433 global.heartBlink += 0.1;
3434 if (global.heartBlink > 3) global.heartBlink = 0;
3436 drawSpriteAt('sHeart', -1, 8, hhup);
3437 global.heartBlink = 0;
3440 int life = clamp(global.plife, 0, 99);
3441 drawTextAt(16+8, hhup, va("%d", life));
3443 //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3444 drawSpriteAt('sDamselIcon', -1, 64, hhup, scumSmallHud);
3445 drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", sunScore));
3447 if (sunRoomTimer1 > 0) {
3448 sprStore.loadFont('sFontSmall');
3449 Video.color = 0xff_ff_00;
3450 int scale = global.scale;
3451 sprStore.renderMultilineTextCentered(viewWidth/2, 216*scale, va("DAMSEL CHALLENGE BEGINS IN ~%d~", (sunRoomTimer1/30)+1), scale, 0xff_00_00);
3456 void renderMoonHUD (float currFrameDelta) {
3457 bool scumSmallHud = global.config.scumSmallHud;
3459 //auto life = max(0, global.plife);
3460 int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3461 int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3462 //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3467 sprStore.loadFont('sFontSmall');
3470 sprStore.loadFont('sFont');
3474 Video.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3476 //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3477 drawSpriteAt('sHoopsIcon', -1, 8, hhup, scumSmallHud);
3478 drawTextAt(8+16-(scumSmallHud ? 6 : 0), hhup, va("%d", moonScore));
3479 drawSpriteAt('sTimerIcon', -1, 64, hhup, scumSmallHud);
3480 drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", max(0, moonTimer)));
3482 if (moonRoomTimer1 > 0) {
3483 sprStore.loadFont('sFontSmall');
3484 Video.color = 0xff_ff_00;
3485 int scale = global.scale;
3486 sprStore.renderMultilineTextCentered(viewWidth/2, 216*scale, va("ARCHERY CHALLENGE BEGINS IN ~%d~", (moonRoomTimer1/30)+1), scale, 0xff_00_00);
3491 void renderHUD (float currFrameDelta) {
3492 if (levelKind == LevelKind.Stars) { renderStarsHUD(currFrameDelta); return; }
3493 if (levelKind == LevelKind.Sun) { renderSunHUD(currFrameDelta); return; }
3494 if (levelKind == LevelKind.Moon) { renderMoonHUD(currFrameDelta); return; }
3496 if (!isHUDEnabled()) return;
3498 if (levelKind == LevelKind.Transition) { renderTransitionInfo(currFrameDelta); return; }
3506 bool scumSmallHud = global.config.scumSmallHud;
3507 if (!global.config.optSGAmmo) moneyX = ammoX;
3510 sprStore.loadFont('sFontSmall');
3513 sprStore.loadFont('sFont');
3516 //int alpha = 0x6f_00_00_00;
3517 int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3518 int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3520 //Video.color = 0xff_ff_ff;
3521 Video.color = 0xff_ff_ff|talpha;
3525 if (global.plife == 1) {
3526 drawSpriteAt('sHeartSmallBlink', global.heartBlink, lifeX+2, 4-hhup);
3527 global.heartBlink += 0.1;
3528 if (global.heartBlink > 3) global.heartBlink = 0;
3530 drawSpriteAt('sHeartSmall', -1, lifeX+2, 4-hhup);
3531 global.heartBlink = 0;
3534 if (global.plife == 1) {
3535 drawSpriteAt('sHeartBlink', global.heartBlink, lifeX, 8-hhup);
3536 global.heartBlink += 0.1;
3537 if (global.heartBlink > 3) global.heartBlink = 0;
3539 drawSpriteAt('sHeart', -1, lifeX, 8-hhup);
3540 global.heartBlink = 0;
3544 int life = clamp(global.plife, 0, 99);
3545 //if (!scumHud && life > 99) life = 99;
3546 drawTextAt(lifeX+16, 8-hhup, va("%d", life));
3549 if (global.hasStickyBombs && global.stickyBombsActive) {
3550 if (scumSmallHud) drawSpriteAt('sStickyBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sStickyBombIcon', -1, bombX, 8-hhup);
3552 if (scumSmallHud) drawSpriteAt('sBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sBombIcon', -1, bombX, 8-hhup);
3554 int n = global.bombs;
3555 if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3556 drawTextAt(bombX+16, 8-hhup, va("%d", n));
3559 if (scumSmallHud) drawSpriteAt('sRopeIconSmall', -1, ropeX+2, 4-hhup); else drawSpriteAt('sRopeIcon', -1, ropeX, 8-hhup);
3561 if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3562 drawTextAt(ropeX+16, 8-hhup, va("%d", n));
3565 if (global.config.optSGAmmo && (!player || player.holdItem !isa ItemWeaponBow)) {
3566 if (scumSmallHud) drawSpriteAt('sShellsIcon', -1, ammoX+6, 8-hhup); else drawSpriteAt('sShellsIcon', -1, ammoX+7, 12-hhup);
3568 if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3569 drawTextAt(ammoX+16, 8-hhup, va("%d", n));
3570 } else if (player && player.holdItem isa ItemWeaponBow) {
3571 if (scumSmallHud) drawSpriteAt('sArrowRight', -1, ammoX+6, 8-hhup); else drawSpriteAt('sArrowRight', -1, ammoX+7, 12-hhup);
3573 if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3574 drawTextAt(ammoX+16, 8-hhup, va("%d", n));
3578 if (scumSmallHud) drawSpriteAt('sDollarSignSmall', -1, moneyX+2, 3-hhup); else drawSpriteAt('sDollarSign', -1, moneyX, 8-hhup);
3579 drawTextAt(moneyX+16, 8-hhup, va("%d", stats.money-xmoney));
3582 Video.color = 0xff_ff_ff|ialpha;
3584 int ity = (scumSmallHud ? 18-hhup : 24-hhup);
3587 if (global.hasUdjatEye) {
3588 if (global.udjatBlink) drawSpriteAt('sUdjatEyeIcon2', -1, n, ity); else drawSpriteAt('sUdjatEyeIcon', -1, n, ity);
3591 if (global.hasAnkh) { drawSpriteAt('sAnkhIcon', -1, n, ity); n += 20; }
3592 if (global.hasCrown) { drawSpriteAt('sCrownIcon', -1, n, ity); n += 20; }
3593 if (global.hasKapala) {
3594 if (global.bloodLevel == 0) drawSpriteAt('sKapalaIcon', 0, n, ity);
3595 else if (global.bloodLevel <= 2) drawSpriteAt('sKapalaIcon', 1, n, ity);
3596 else if (global.bloodLevel <= 4) drawSpriteAt('sKapalaIcon', 2, n, ity);
3597 else if (global.bloodLevel <= 6) drawSpriteAt('sKapalaIcon', 3, n, ity);
3598 else if (global.bloodLevel <= 8) drawSpriteAt('sKapalaIcon', 4, n, ity);
3601 if (global.hasSpectacles) { drawSpriteAt('sSpectaclesIcon', -1, n, ity); n += 20; }
3602 if (global.hasGloves) { drawSpriteAt('sGlovesIcon', -1, n, ity); n += 20; }
3603 if (global.hasMitt) { drawSpriteAt('sMittIcon', -1, n, ity); n += 20; }
3604 if (global.hasSpringShoes) { drawSpriteAt('sSpringShoesIcon', -1, n, ity); n += 20; }
3605 if (global.hasJordans) { drawSpriteAt('sJordansIcon', -1, n, ity); n += 20; }
3606 if (global.hasSpikeShoes) { drawSpriteAt('sSpikeShoesIcon', -1, n, ity); n += 20; }
3607 if (global.hasCape) { drawSpriteAt('sCapeIcon', -1, n, ity); n += 20; }
3608 if (global.hasJetpack) { drawSpriteAt('sJetpackIcon', -1, n, ity); n += 20; }
3609 if (global.hasStickyBombs) { drawSpriteAt('sPasteIcon', -1, n, ity); n += 20; }
3610 if (global.hasCompass) { drawSpriteAt('sCompassIcon', -1, n, ity); n += 20; }
3611 if (global.hasParachute) { drawSpriteAt('sParachuteIcon', -1, n, ity); n += 20; }
3613 if (player.holdItem && player.holdItem.objName == 'Bow' && global.arrows > 0) {
3616 while (m <= global.arrows && m <= 20 && malpha > 0) {
3617 Video.color = trunc(malpha*255)<<24|0xff_ff_ff;
3618 drawSpriteAt('sArrowIcon', -1, n, ity);
3620 if (m > 10) malpha -= 0.1; //and global.arrows > 20 malpha -= 0.1
3626 sprStore.loadFont('sFontSmall');
3627 Video.color = 0xff_ff_00|talpha;
3628 if (scumSmallHud) drawTextAt(moneyX+8, 8+8-hhup, va("+%d", xmoney));
3629 else drawTextAt(moneyX, 8+16-hhup, va("+%d", xmoney));
3632 Video.color = 0xff_ff_ff;
3633 if (global.config.ghostShowTime) renderGhostTimer(currFrameDelta);
3637 // ////////////////////////////////////////////////////////////////////////// //
3638 // x0 and y0 are non-scaled (and will be scaled)
3639 final void drawTextAtS3 (int x0, int y0, string text, optional int hiColor1, optional int hiColor2) {
3643 sprStore.renderTextWithHighlight(x0, y0, text, 3, hiColor1!optional, hiColor2!optional);
3647 final void drawTextAtS3Centered (int y0, string text, optional int hiColor1, optional int hiColor2) {
3649 int x0 = (viewWidth-sprStore.getTextWidth(text, 3, specified_hiColor1, specified_hiColor2))/2;
3650 sprStore.renderTextWithHighlight(x0, y0*3, text, 3, hiColor1!optional, hiColor2!optional);
3654 void renderHelpOverlay () {
3656 Video.fillRect(0, 0, viewWidth, viewHeight);
3659 int txoff = 0; // text x pos offset (for multi-color lines)
3661 if (gameHelpScreen) {
3662 sprStore.loadFont('sFontSmall');
3663 Video.color = 0xff_ff_ff;
3664 drawTextAtS3Centered(ty, va("HELP (PAGE ~%d~ OF ~%d~)", gameHelpScreen, MaxGameHelpScreen), 0xff_ff_00);
3668 if (gameHelpScreen == 1) {
3669 sprStore.loadFont('sFontSmall');
3670 Video.color = 0xff_ff_00; drawTextAtS3(tx, ty, "INVENTORY BASICS"); ty += 16;
3671 Video.color = 0xff_ff_ff;
3672 drawTextAtS3(tx, ty, global.expandString("Press $SWITCH to cycle through items."), 0x00_ff_00);
3675 Video.color = 0xff_ff_ff;
3676 drawSpriteAtS3('sHelpSprite1', -1, 64, 96);
3677 } else if (gameHelpScreen == 2) {
3678 sprStore.loadFont('sFontSmall');
3679 Video.color = 0xff_ff_00;
3680 drawTextAtS3(tx, ty, "SELLING TO SHOPKEEPERS"); ty += 16;
3681 Video.color = 0xff_ff_ff;
3682 drawTextAtS3(tx, ty, global.expandString("Press $PAY to offer your currently"), 0x00_ff_00); ty += 8;
3683 drawTextAtS3(tx, ty, "held item to the shopkeeper."); ty += 16;
3684 drawTextAtS3(tx, ty, "If the shopkeeper is interested, "); ty += 8;
3685 //drawTextAtS3(tx, ty, global.expandString("press $PAY again to complete the sale."), 0x00_ff_00); ty += 72;
3686 drawTextAtS3(tx, ty, global.expandString("press $PAY again to complete"), 0x00_ff_00);
3687 drawTextAtS3(tx, ty+8, "the sale.");
3689 drawSpriteAtS3('sHelpSell', -1, 112, 100);
3690 drawTextAtS3(tx, ty, "Purchasing goods from the shopkeeper"); ty += 8;
3691 drawTextAtS3(tx, ty, "will increase the funds he has"); ty += 8;
3692 drawTextAtS3(tx, ty, "available to buy your unwanted stuff."); ty += 8;
3695 sprStore.loadFont('sFont');
3696 Video.color = 0xff_ff_ff;
3697 drawTextAtS3(136, 8, "MAP");
3699 if (lg.mapSprite && (isNormalLevel() || isTransitionRoom())) {
3700 Video.color = 0xff_ff_00;
3701 drawTextAtS3Centered(24, lg.mapTitle);
3703 auto spf = sprStore[lg.mapSprite].frames[0];
3704 int mapX = 160-spf.width/2;
3705 int mapY = 120-spf.height/2;
3706 //mapTitleX = 160-string_length(global.mapTitle)*8/2;
3708 Video.color = 0xff_ff_ff;
3709 drawSpriteAtS3(lg.mapSprite, -1, mapX, mapY);
3711 if (lg.mapSprite != 'sMapDefault') {
3712 int mx = -1, my = -1;
3714 // set position of player icon
3715 switch (global.currLevel) {
3716 case 1: mx = 81; my = 22; break;
3717 case 2: mx = 113; my = 63; break;
3718 case 3: mx = 197; my = 86; break;
3719 case 4: mx = 133; my = 109; break;
3720 case 5: mx = 181; my = 22; break;
3721 case 6: mx = 126; my = 64; break;
3722 case 7: mx = 158; my = 112; break;
3723 case 8: mx = 66; my = 80; break;
3724 case 9: mx = 30; my = 26; break;
3725 case 10: mx = 88; my = 54; break;
3726 case 11: mx = 148; my = 81; break;
3727 case 12: mx = 210; my = 205; break;
3728 case 13: mx = 66; my = 17; break;
3729 case 14: mx = 146; my = 17; break;
3730 case 15: mx = 82; my = 77; break;
3731 case 16: mx = 178; my = 81; break;
3735 int plrx = mx+player.ix/16;
3736 int plry = my+player.iy/16;
3737 if (isTransitionRoom()) { plrx = mx+20; plry = my+16; }
3738 name plrspr = 'sMapSpelunker';
3739 if (global.isDamsel) plrspr = 'sMapDamsel';
3740 else if (global.isTunnelMan) plrspr = 'sMapTunnel';
3741 auto ss = sprStore[plrspr];
3742 drawSpriteAtS3(plrspr, (pausedTime/2)%ss.frames.length, mapX+plrx, mapY+plry);
3744 if (global.hasCompass && allExits.length) {
3745 drawSpriteAtS3('sMapRedDot', -1, mapX+mx+allExits[0].ix/16, mapY+my+allExits[0].iy/16);
3752 sprStore.loadFont('sFontSmall');
3753 Video.color = 0xff_ff_00;
3754 drawTextAtS3Centered(232, "PRESS ~SPACE~/~LEFT~/~RIGHT~ TO CHANGE PAGE", 0x00_ff_00);
3756 Video.color = 0xff_ff_ff;
3760 void renderPauseOverlay () {
3761 //drawTextAt(256, 432, "PAUSED", scale);
3763 if (gameShowHelp) { renderHelpOverlay(); return; }
3765 Video.color = 0xff_ff_00;
3766 //int hiColor = 0x00_ff_00;
3769 if (isTutorialRoom()) {
3770 sprStore.loadFont('sFont');
3771 drawTextAtS3(40, n-24, "TUTORIAL CAVE");
3772 } else if (isNormalLevel()) {
3773 sprStore.loadFont('sFont');
3775 drawTextAtS3Centered(n-32, va("LEVEL ~%d~", global.currLevel), 0x00_ff_00);
3777 sprStore.loadFont('sFontSmall');
3779 int depth = round((174.8*(global.currLevel-1)+(player.iy+8)*0.34)*(global.config.scumMetric ? 0.3048 : 1.0)*10);
3780 string depthStr = va("DEPTH: ~%d.%d~ %s", depth/10, depth%10, (global.config.scumMetric ? "METRES" : "FEET"));
3781 drawTextAtS3Centered(n-16, depthStr, 0x00_ff_00);
3784 drawTextAtS3Centered(n, va("MONEY: ~%d~", stats.money), 0x00_ff_00);
3785 drawTextAtS3Centered(n+16, va("KILLS: ~%d~", stats.countKills), 0x00_ff_00);
3786 drawTextAtS3Centered(n+32, va("SAVES: ~%d~", stats.damselsSaved), 0x00_ff_00);
3787 drawTextAtS3Centered(n+48, va("TIME: ~%s~", time2str(time/30)), 0x00_ff_00);
3788 drawTextAtS3Centered(n+64, va("LEVEL TIME: ~%s~", time2str((time-levelStartTime)/30)), 0x00_ff_00);
3791 sprStore.loadFont('sFontSmall');
3792 Video.color = 0xff_ff_ff;
3793 drawTextAtS3Centered(240-2-8, "~ESC~-RETURN ~F10~-QUIT ~CTRL+DEL~-SUICIDE", 0xff_7f_00);
3794 drawTextAtS3Centered(2, "~O~PTIONS REDEFINE ~K~EYS ~S~TATISTICS", 0xff_7f_00);
3798 // ////////////////////////////////////////////////////////////////////////// //
3799 transient int drawLoot;
3800 transient int drawPosX, drawPosY;
3802 void resetTransitionOverlay () {
3809 // current game, uncollapsed
3810 struct LevelStatInfo {
3812 // for transition screen
3819 void thinkFrameTransition () {
3820 if (drawLoot == 0) {
3821 if (drawPosX > 272) {
3824 if (drawPosY > 83+4) drawPosY = 83;
3826 } else if (drawPosX > 232) {
3829 if (drawPosY > 91+4) drawPosY = 91;
3834 void renderTransitionOverlay () {
3835 sprStore.loadFont('sFontSmall');
3836 Video.color = 0xff_ff_00;
3837 //else if (global.currLevel-1 < 1) draw_text(32, 48, "TUTORIAL CAVE COMPLETED!");
3838 //else draw_text(32, 48, "LEVEL "+string(global.currLevel-1)+" COMPLETED!");
3839 drawTextAt(32, 48, va("LEVEL ~%d~ COMPLETED!", global.currLevel), hiColor1:0x00_ff_ff);
3840 Video.color = 0xff_ff_ff;
3841 drawTextAt(32, 64, va("TIME = ~%s~", time2str((levelEndTime-levelStartTime)/30)), hiColor1:0xff_ff_00);
3843 if (/*stats.collected.length == 0*/stats.money <= levelMoneyStart) {
3844 drawTextAt(32, 80, "LOOT = ~NONE~", hiColor1:0xff_00_00);
3846 drawTextAt(32, 80, va("LOOT = ~%d~", stats.money-levelMoneyStart), hiColor1:0xff_ff_00);
3849 if (stats.kills.length == 0) {
3850 drawTextAt(32, 96, "KILLS = ~NONE~", hiColor1:0x00_ff_00);
3852 drawTextAt(32, 96, va("KILLS = ~%d~", stats.kills.length), hiColor1:0xff_ff_00);
3855 drawTextAt(32, 112, va("MONEY = ~%d~", stats.money), hiColor1:0xff_ff_00);
3859 // ////////////////////////////////////////////////////////////////////////// //
3860 private transient array!MapEntity renderVisibleCids;
3861 private transient array!MapEntity renderVisibleLights;
3862 private transient array!MapTile renderFrontTiles; // normal, with fg
3864 final bool renderSortByDepth (MapEntity oa, MapEntity ob) {
3865 auto da = oa.depth, db = ob.depth;
3866 if (da == db) return (oa.objId < ob.objId);
3871 const int RenderEdgePixNormal = 64;
3872 const int RenderEdgePixLight = 256;
3874 #ifndef EXPERIMENTAL_RENDER_CACHE
3875 enum skipListCreation = false;
3878 void renderWithOfs (int xofs, int yofs, float currFrameDelta) {
3879 int scale = global.scale;
3881 // don't touch framebuffer alpha
3882 Video.colorMask = Video::CMask.Colors;
3883 Video.color = 0xff_ff_ff;
3886 Video::ScissorRect scsave;
3887 bool doRestoreGL = false;
3889 if (viewOffsetX > 0 || viewOffsetY > 0) {
3891 Video.getScissor(scsave);
3892 Video.scissorCombine(viewOffsetX, viewOffsetY, viewWidth, viewHeight);
3893 Video.glPushMatrix();
3894 Video.glTranslate(viewOffsetX, viewOffsetY);
3895 //Video.glTranslate(-550, 0);
3896 //Video.glScale(1, 1);
3901 bool isDarkLevel = global.darkLevel;
3904 switch (global.config.scumPlayerLit) {
3905 case 0: player.lightRadius = 0; break; // never
3906 case 1: // only in "scumDarkness"
3907 player.lightRadius = (global.config.scumDarkness >= 2 ? 96 : 32);
3910 player.lightRadius = 96;
3915 // render cave background
3918 int bgw = levBGImg.tex.width*scale;
3919 int bgh = levBGImg.tex.height*scale;
3920 int bgTW = (tilesWidth*tsz+bgw-1)/bgw;
3921 int bgTH = (tilesHeight*tsz+bgh-1)/bgh;
3922 int bgX0 = max(0, xofs/bgw);
3923 int bgY0 = max(0, yofs/bgh);
3924 int bgX1 = min(bgTW, (xofs+viewWidth+bgw-1)/bgw);
3925 int bgY1 = min(bgTH, (yofs+viewHeight+bgh-1)/bgh);
3926 foreach (int ty; bgY0..bgY1) {
3927 foreach (int tx; bgX0..bgX1) {
3928 int x0 = tx*bgw-xofs;
3929 int y0 = ty*bgh-yofs;
3930 levBGImg.tex.blitAt(x0, y0, scale);
3935 int RenderEdgePix = (global.darkLevel ? RenderEdgePixLight : RenderEdgePixNormal);
3937 // render background tiles
3938 for (MapBackTile bt = backtiles; bt; bt = bt.next) {
3939 bt.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3942 // collect visible special tiles
3943 #ifdef EXPERIMENTAL_RENDER_CACHE
3944 bool skipListCreation = (lastRenderTime == time && renderVisibleCids.length); //FIXME
3947 if (!skipListCreation) {
3948 renderVisibleCids.clear();
3949 renderVisibleLights.clear();
3950 renderFrontTiles.clear();
3952 int endVX = xofs+viewWidth;
3953 int endVY = yofs+viewHeight;
3957 if (player && player.visible) renderVisibleCids[$] = player; // include player in render list
3959 //FIXME: drop lit objects which cannot affect visible area
3961 // collect visible objects
3962 foreach (MapEntity o; objGrid.inRectPix(xofs/scale-RenderEdgePix, yofs/scale-RenderEdgePix, (viewWidth+scale-1)/scale+RenderEdgePix*2, (viewHeight+scale-1)/scale+RenderEdgePix*2, precise:false)) {
3963 if (!o.visible) continue;
3964 auto tile = MapTile(o);
3966 if (isDarkLevel && (o.lightRadius > 4 || tile.litWholeTile)) renderVisibleLights[$] = o;
3967 if (tile.invisible) continue;
3968 if (tile.bgfront) renderFrontTiles[$] = tile;
3969 if (tile.bgback) tile.drawWithOfsBack(xofs, yofs, scale, currFrameDelta);
3971 if (isDarkLevel && o.lightRadius > 4) renderVisibleLights[$] = o;
3973 // check if the object is really visible -- this will speed up later sorting
3974 int fx0, fy0, fx1, fy1;
3975 auto spf = o.getSpriteFrame(default, out fx0, out fy0, out fx1, out fy1);
3976 if (!spf) continue; // no sprite -- nothing to draw (no, really)
3977 int ix = o.ix, iy = o.iy;
3978 int x0 = (ix+fx0)*scale, y0 = (iy+fy0)*scale;
3979 int x1 = (ix+fx1)*scale, y1 = (iy+fy1)*scale;
3980 if (x1 <= xofs || y1 <= yofs || x0 >= endVX || y0 >= endVY) {
3984 renderVisibleCids[$] = o;
3987 foreach (MapEntity o; objGrid.allObjects()) {
3988 if (!o.visible) continue;
3989 auto tile = MapTile(o);
3991 if (isDarkLevel && (o.lightRadius > 4 || tile.litWholeTile)) renderVisibleLights[$] = o;
3992 if (tile.invisible) continue;
3993 if (tile.bgfront) renderFrontTiles[$] = tile;
3994 if (tile.bgback) tile.drawWithOfsBack(xofs, yofs, scale, currFrameDelta);
3996 if (isDarkLevel && o.lightRadius > 4) renderVisibleLights[$] = o;
3998 renderVisibleCids[$] = o;
4001 //writeln("::: ", cnt, " invisible objects dropped");
4003 renderVisibleCids.sort(&renderSortByDepth);
4004 lastRenderTime = time;
4007 auto depth4Start = 0;
4008 foreach (auto xidx, MapEntity o; renderVisibleCids) {
4015 bool playerPowerupRendered = false;
4017 // render objects (part one: depth > 3)
4018 foreach (auto idx; depth4Start..renderVisibleCids.length; reverse) {
4019 MapEntity o = renderVisibleCids[idx];
4020 // 1000 is an ordinary tile
4021 if (!playerPowerupRendered && o.depth <= 1200) {
4022 playerPowerupRendered = true;
4023 // so ducking player will have it's cape correctly rendered
4024 if (player.visible) player.drawPrePrePowerupWithOfs(xofs, yofs, scale, currFrameDelta);
4026 //if (idx && renderSortByDepth(o, renderVisibleCids[idx-1])) writeln("VIOLATION!");
4027 o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
4030 // render object (part two: front tile parts, depth 3.5)
4031 foreach (MapTile tile; renderFrontTiles) {
4032 tile.drawWithOfsFront(xofs, yofs, scale, currFrameDelta);
4035 // render objects (part three: depth <= 3)
4036 foreach (auto idx; 0..depth4Start; reverse) {
4037 MapEntity o = renderVisibleCids[idx];
4038 o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
4039 //done above;if (isDarkLevel && (o.lightRadius > 4 || (o isa MapTile && MapTile(o).litWholeTile))) renderVisibleLights[$] = o;
4042 // render player post-effects, so cape or jetpack will be correctly rendered on level exit, for example
4043 player.lastDrawWithOfs(xofs, yofs, scale, currFrameDelta);
4047 auto ltex = bgtileStore.lightTexture('ltx512', 512);
4049 // set screen alpha to min
4050 Video.colorMask = Video::CMask.Alpha;
4051 Video.blendMode = Video::BlendMode.None;
4052 Video.color = 0xff_ff_ff_ff;
4053 Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
4054 //Video.colorMask = Video::CMask.All;
4057 // also, stencil 'em, so we can filter dark areas
4058 Video.textureFiltering = true;
4059 Video.stencil = true;
4060 Video.stencilFunc(Video::StencilFunc.Always, 1);
4061 Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Replace);
4062 Video.alphaTestFunc = Video::AlphaFunc.Greater;
4063 Video.alphaTestVal = 0.03+0.011*global.config.darknessDarkness;
4064 Video.color = 0xff_ff_ff;
4065 Video.blendFunc = Video::BlendFunc.Max;
4066 Video.blendMode = Video::BlendMode.Blend; // anything except `Normal`
4067 Video.colorMask = Video::CMask.Alpha;
4069 foreach (MapEntity e; renderVisibleLights) {
4071 e.getInterpCoords(currFrameDelta, scale, out xi, out yi);
4072 auto tile = MapTile(e);
4073 if (tile && tile.litWholeTile) {
4074 //Video.color = 0xff_ff_ff;
4075 Video.fillRect(xi-xofs, yi-yofs, e.width*scale, e.height*scale);
4077 int lrad = e.lightRadius;
4078 if (lrad < 4) continue; // just in case
4080 float lightscale = float(lrad*scale)/float(ltex.tex.width);
4081 #ifdef OLD_LIGHT_OFFSETS
4082 int fx0, fy0, fx1, fy1;
4084 auto spf = e.getSpriteFrame(out doMirror, out fx0, out fy0, out fx1, out fy1);
4086 xi += (fx1-fx0)*scale/2;
4087 yi += (fy1-fy0)*scale/2;
4091 e.getLightOffset(out lxofs, out lyofs);
4096 lrad = lrad*scale/2;
4099 ltex.tex.blitAt(xi, yi, lightscale);
4101 Video.textureFiltering = false;
4103 // modify only lit parts
4104 Video.stencilFunc(Video::StencilFunc.Equal, 1);
4105 Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Keep);
4106 // multiply framebuffer colors by framebuffer alpha
4107 Video.color = 0xff_ff_ff; // it doesn't matter
4108 Video.blendFunc = Video::BlendFunc.Add;
4109 Video.blendMode = Video::BlendMode.DstMulDstAlpha;
4110 Video.colorMask = Video::CMask.Colors;
4111 Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
4113 // filter unlit parts
4114 Video.stencilFunc(Video::StencilFunc.NotEqual, 1);
4115 Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Keep);
4116 Video.blendFunc = Video::BlendFunc.Add;
4117 Video.blendMode = Video::BlendMode.Filter;
4118 Video.colorMask = Video::CMask.Colors;
4119 Video.color = 0x00_00_18+0x00_00_10*global.config.darknessDarkness;
4120 //Video.color = 0x00_00_18;
4121 //Video.color = 0x00_00_38;
4122 Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
4125 Video.blendFunc = Video::BlendFunc.Add;
4126 Video.blendMode = Video::BlendMode.Normal;
4127 Video.colorMask = Video::CMask.All;
4128 Video.alphaTestFunc = Video::AlphaFunc.Always;
4129 Video.stencil = false;
4132 // clear visible objects list (nope)
4133 //renderVisibleCids.clear();
4134 //renderVisibleLights.clear();
4137 if (global.config.drawHUD) renderHUD(currFrameDelta);
4138 renderCompass(currFrameDelta);
4140 float osdTimeLeft, osdTimeStart;
4141 string msg = osdGetMessage(out osdTimeLeft, out osdTimeStart);
4143 auto ct = GetTickCount();
4145 sprStore.loadFont('sFontSmall');
4146 auto msgHeight = sprStore.getMultilineTextHeight(msg, msgScale);
4147 int x = viewWidth/2;
4148 int y = viewHeight-64-msgHeight;
4149 auto oldColor = Video.color;
4150 Video.color = 0xff_ff_00;
4151 if (osdTimeLeft < 0.5) {
4152 int alpha = 255-clamp(int(510*osdTimeLeft), 0, 255);
4153 Video.color = Video.color|(alpha<<24);
4154 } else if (ct-osdTimeStart < 0.5) {
4155 osdTimeStart = ct-osdTimeStart;
4156 int alpha = 255-clamp(int(510*osdTimeStart), 0, 255);
4157 Video.color = Video.color|(alpha<<24);
4159 sprStore.renderMultilineTextCentered(x, y, msg, msgScale, 0x00_ff_00, 0xff_ff_ff);
4160 Video.color = oldColor;
4163 int hiColor1, hiColor2;
4164 msg = osdGetTalkMessage(out hiColor1, out hiColor2);
4167 sprStore.loadFont('sFontSmall');
4168 auto msgWidth = sprStore.getMultilineTextWidth(msg, processHighlights1:true, processHighlights2:true);
4169 auto msgHeight = sprStore.getMultilineTextHeight(msg);
4170 auto msgWidthOrig = msgWidth*msgScale;
4171 auto msgHeightOrig = msgHeight*msgScale;
4172 if (msgWidth%16 != 0) msgWidth = (msgWidth|0x0f)+1;
4173 if (msgHeight%16 != 0) msgHeight = (msgHeight|0x0f)+1;
4174 msgWidth *= msgScale;
4175 msgHeight *= msgScale;
4176 int x = (viewWidth-msgWidth)/2;
4177 int y = 32*msgScale;
4178 auto oldColor = Video.color;
4179 // draw text frame and text background
4181 Video.fillRect(x, y, msgWidth, msgHeight);
4182 Video.color = 0xff_ff_ff;
4183 for (int fdx = 0; fdx < msgWidth; fdx += 16*msgScale) {
4184 auto spf = sprStore['sMenuTop'].frames[0];
4185 spf.tex.blitAt(x+fdx, y-16*msgScale, msgScale);
4186 spf = sprStore['sMenuBottom'].frames[0];
4187 spf.tex.blitAt(x+fdx, y+msgHeight, msgScale);
4189 for (int fdy = 0; fdy < msgHeight; fdy += 16*msgScale) {
4190 auto spf = sprStore['sMenuLeft'].frames[0];
4191 spf.tex.blitAt(x-16*msgScale, y+fdy, msgScale);
4192 spf = sprStore['sMenuRight'].frames[0];
4193 spf.tex.blitAt(x+msgWidth, y+fdy, msgScale);
4196 auto spf = sprStore['sMenuUL'].frames[0];
4197 spf.tex.blitAt(x-16*msgScale, y-16*msgScale, msgScale);
4198 spf = sprStore['sMenuUR'].frames[0];
4199 spf.tex.blitAt(x+msgWidth, y-16*msgScale, msgScale);
4200 spf = sprStore['sMenuLL'].frames[0];
4201 spf.tex.blitAt(x-16*msgScale, y+msgHeight, msgScale);
4202 spf = sprStore['sMenuLR'].frames[0];
4203 spf.tex.blitAt(x+msgWidth, y+msgHeight, msgScale);
4205 Video.color = 0xff_ff_00;
4206 sprStore.renderMultilineText(x+(msgWidth-msgWidthOrig)/2, y+(msgHeight-msgHeightOrig)/2-3*msgScale, msg, msgScale, (hiColor1 == -1 ? 0x00_ff_00 : hiColor1), (hiColor2 == -1 ? 0xff_ff_ff : hiColor2));
4207 Video.color = oldColor;
4210 if (inWinCutscene) renderWinCutsceneOverlay();
4211 if (inIntroCutscene) renderTitleCutsceneOverlay();
4212 if (isTransitionRoom()) renderTransitionOverlay();
4216 Video.setScissor(scsave);
4217 Video.glPopMatrix();
4221 Video.color = 0xff_ff_ff;
4225 // ////////////////////////////////////////////////////////////////////////// //
4226 final class!MapObject findGameObjectClassByName (name aname) {
4227 if (!aname) return none; // just in case
4228 auto co = FindClassByGameObjName(aname);
4230 writeln("***UNKNOWN GAME OBJECT: '", aname, "'");
4233 co = GetClassReplacement(co);
4234 if (!co) FatalError("findGameObjectClassByName: WTF?!");
4235 if (!class!MapObject(co)) FatalError("findGameTileClassByName: object '%n' is not a map object, it is a `%n`", aname, GetClassName(co));
4236 return class!MapObject(co);
4240 final class!MapTile findGameTileClassByName (name aname) {
4241 if (!aname) return none; // just in case
4242 auto co = FindClassByGameObjName(aname);
4243 if (!co) return MapTile; // unknown names will be routed directly to tile object
4244 co = GetClassReplacement(co);
4245 if (!co) FatalError("findGameTileClassByName: WTF?!");
4246 if (!class!MapTile(co)) FatalError("findGameTileClassByName: object '%n' is not a tile, it is a `%n`", aname, GetClassName(co));
4247 return class!MapTile(co);
4251 final MapObject findAnyObjectOfType (name aname) {
4252 if (!aname) return none;
4253 auto cls = FindClassByGameObjName(aname);
4254 if (!cls) return none;
4255 foreach (MapObject obj; objGrid.allObjects(MapObject)) {
4256 if (obj.spectral) continue;
4257 if (obj isa cls) return obj;
4263 // ////////////////////////////////////////////////////////////////////////// //
4264 final bool isRopePlacedAt (int x, int y) {
4266 foreach (ref auto v; covered) v = false;
4267 foreach (MapTile t; objGrid.inRectPix(x, y-8, 1, 17, precise:false, castClass:MapTileRope)) {
4268 //if (!cbIsRopeTile(t)) continue;
4269 if (t.ix != x) continue;
4270 if (t.iy == y) return true;
4271 foreach (int ty; t.iy..t.iy+8) {
4273 if (d >= 0 && d < covered.length) covered[d] = true;
4276 // check if the whole rope height is completely covered with ropes
4277 foreach (auto v; covered) if (!v) return false;
4282 final MapTile CreateMapTile (int xpos, int ypos, name aname) {
4283 if (!aname) FatalError("cannot create typeless tile");
4284 auto tclass = findGameTileClassByName(aname);
4285 if (!tclass) return none;
4286 MapTile tile = SpawnObject(tclass);
4287 tile.global = global;
4289 tile.objName = aname;
4290 tile.objType = aname; // just in case
4293 tile.objId = ++lastUsedObjectId;
4294 if (!tile.initialize()) FatalError("cannot create tile '%n'", aname);
4299 final bool PutSpawnedMapTile (int x, int y, MapTile tile, optional bool putToGrid) {
4300 if (!tile || !tile.isInstanceAlive) return false;
4302 if (!putToGrid) putToGrid = (tile.moveable || tile.toSpecialGrid || tile.width != 16 || tile.height != 16 || x%16 != 0 || y%16 != 0);
4304 //writeln("putting tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (putToGrid ? "special " : ""), "grid, pos=(", x/16.0, ",", y/16.0, ")");
4307 int mapx = x/16, mapy = y/16;
4308 if (mapx < 0 || mapx >= tilesWidth || mapy < 0 || mapy >= tilesHeight) return false;
4311 // if we already have rope tile there, there is no reason to add another one
4312 if (tile isa MapTileRope) {
4313 if (isRopePlacedAt(x, y)) return false;
4316 // activate special or animated tile
4317 tile.active = tile.active || putToGrid || tile.moveable || tile.toSpecialGrid || tile.lava /*|| tile.water*/; // will be done in MakeMapTile
4318 // animated tiles must be active
4320 auto spr = tile.getSprite();
4321 if (spr && spr.frames.length > 1) {
4322 writeln("activated animated tile '", tile.objName, "'");
4330 //if (tile isa TitleTileCopy) writeln("*** PUTTING COPYRIGHT TILE");
4331 tile.toSpecialGrid = true;
4332 if (!tile.dontReplaceOthers && x&16 == 0 && y%16 == 0) {
4333 auto t = getTileAtGridAny(x/16, y/16);
4334 if (t && !t.immuneToReplacement) {
4335 writeln("REPLACING tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
4336 writeln(" NEW tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (tile.toSpecialGrid ? "special " : ""), "grid, pos=(", tile.fltx/16.0, ",", tile.flty/16.0, ")");
4340 objGrid.insert(tile);
4342 //writeln("SIZE: ", tilesWidth, "x", tilesHeight);
4343 setTileAtGrid(x/16, y/16, tile);
4344 auto t = getTileAtGridAny(x/16, y/16);
4347 writeln("putting tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (putToGrid ? "special " : ""), "grid, pos=(", x/16.0, ",", y/16.0, ")");
4348 checkTilesInRect(x/16, y/16, 16, 16, delegate bool (MapTile tile) {
4349 writeln(" *** tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (tile.toSpecialGrid || tile.moveable ? "special " : ""), "grid, pos=(", tile.fltx/16.0, ",", tile.flty/16.0, ")");
4352 FatalError("FUUUUUU");
4357 if (tile.enter) registerEnter(tile);
4358 if (tile.exit) registerExit(tile);
4360 // make tile under exit invulnerable
4361 if (checkTilesInRect(tile.ix, tile.iy-16, 16, 16, delegate bool (MapTile t) { return t.exit; })) {
4362 tile.invincible = true;
4369 // won't call `onDestroy()`
4370 final void RemoveMapTileFromGrid (int tileX, int tileY, optional string reason) {
4371 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
4372 auto t = getTileAtGridAny(tileX, tileY);
4374 writeln("REMOVING(RMT", (reason ? ":"~reason : ""), ") tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
4382 final MapTile MakeMapTile (int mapx, int mapy, name aname, optional bool putToGrid) {
4383 //writeln("tile at (", mapx, ",", mapy, "): ", aname);
4384 //if (aname == 'oLush') { MapObject fail; fail.initialize(); }
4385 //if (mapy >= tilesHeight) { writeln("OOM: ", aname, "; ty=", mapy, "; hgt=", tilesHeight); }
4386 if (mapx < 0 || mapx >= tilesWidth || mapy < 0 || mapy >= tilesHeight) return none;
4388 // if we already have rope tile there, there is no reason to add another one
4389 if (aname == 'oRope') {
4390 if (isRopePlacedAt(mapx*16, mapy*16)) return none;
4393 auto tile = CreateMapTile(mapx*16, mapy*16, aname);
4394 if (!tile) return none;
4395 if (!PutSpawnedMapTile(mapx*16, mapy*16, tile, putToGrid!optional)) {
4404 final MapTile MakeMapTileAtPix (int xpix, int ypix, name aname, optional bool putToGrid) {
4405 // if we already have rope tile there, there is no reason to add another one
4406 if (aname == 'oRope') {
4407 if (isRopePlacedAt(xpix, ypix)) return none;
4410 auto tile = CreateMapTile(xpix, ypix, aname);
4411 if (!tile) return none;
4412 if (!PutSpawnedMapTile(xpix, ypix, tile, putToGrid!optional)) {
4421 final MapTile MakeMapRopeTileAt (int x0, int y0) {
4422 // if we already have rope tile there, there is no reason to add another one
4423 if (isRopePlacedAt(x0, y0)) return none;
4425 auto tile = CreateMapTile(x0, y0, 'oRope');
4426 if (!PutSpawnedMapTile(x0, y0, tile, putToGrid:true)) {
4435 // ////////////////////////////////////////////////////////////////////////// //
4436 final MapBackTile CreateBgTile (name sprName, optional int atx0, optional int aty0, optional int aw, optional int ah) {
4437 BackTileImage img = bgtileStore[sprName];
4438 auto res = SpawnObject(MapBackTile);
4439 res.global = global;
4442 res.bgtName = sprName;
4443 if (specified_atx0) res.tx0 = atx0;
4444 if (specified_aty0) res.ty0 = aty0;
4445 if (specified_aw) res.w = aw;
4446 if (specified_ah) res.h = ah;
4447 if (!res.initialize()) FatalError("cannot create background tile with image '%n'", sprName);
4452 // ////////////////////////////////////////////////////////////////////////// //
4454 background The background asset from which the new tile will be extracted.
4455 left The x coordinate of the left of the new tile, relative to the background asset's top left corner.
4456 top The y coordinate of the top of the new tile, relative to the background assets top left corner.
4457 width The width of the tile.
4458 height The height of the tile.
4459 x The x position in the room to place the tile.
4460 y The y position in the room to place the tile.
4461 depth The depth at which to place the tile.
4463 final void MakeMapBackTile (name bgname, int left, int top, int width, int height, int x, int y, int depth) {
4464 if (width < 1 || height < 1 || !bgname) return;
4465 auto bgt = bgtileStore[bgname];
4466 if (!bgt) FatalError("cannot load background '%n'", bgname);
4467 MapBackTile bt = SpawnObject(MapBackTile);
4470 bt.objName = bgname;
4472 bt.bgtName = bgname;
4480 // find a place for it
4485 // back tiles with the highest depth should come first
4486 MapBackTile ct = backtiles, cprev = none;
4487 while (ct && depth <= ct.depth) { cprev = ct; ct = ct.next; }
4490 bt.next = cprev.next;
4493 bt.next = backtiles;
4499 // ////////////////////////////////////////////////////////////////////////// //
4500 final MapObject SpawnMapObjectWithClass (class!MapObject oclass) {
4501 if (!oclass) return none;
4503 MapObject obj = SpawnObject(oclass);
4504 if (!obj) FatalError("cannot create map object '%n'", GetClassName(oclass));
4506 //if (aname == 'oShells' || aname == 'oShellSingle' || aname == 'oRopePile') writeln("*** CREATED '", aname, "'");
4508 obj.global = global;
4510 obj.objId = ++lastUsedObjectId;
4516 final MapObject SpawnMapObject (name aname) {
4517 if (!aname) return none;
4518 auto res = SpawnMapObjectWithClass(findGameObjectClassByName(aname));
4519 if (res && !res.objType) res.objType = aname; // just in case
4524 final MapObject PutSpawnedMapObject (int x, int y, MapObject obj) {
4525 if (!obj /*|| obj.global || obj.level*/) return none; // oops
4529 if (!obj.initialize()) { delete obj; return none; } // not fatal
4537 final MapObject MakeMapObject (int x, int y, name aname) {
4538 MapObject obj = SpawnMapObject(aname);
4539 obj = PutSpawnedMapObject(x, y, obj);
4544 // ////////////////////////////////////////////////////////////////////////// //
4545 int winCutSceneTimer = -1;
4546 int winVolcanoTimer = -1;
4547 int winCutScenePhase = 0;
4548 int winSceneDrawStatus = 0;
4549 int winMoneyCount = 0;
4551 bool winFadeOut = false;
4552 int winFadeLevel = 0;
4553 int winCutsceneSkip = 0; // 1: waiting for pay release; 2: pay released, do skip
4554 bool winCutsceneSwitchToNext = false;
4557 void startWinCutscene () {
4558 global.hasParachute = false;
4560 winCutsceneSwitchToNext = false;
4561 winCutsceneSkip = 0;
4562 isKeyPressed(GameConfig::Key.Pay);
4563 isKeyReleased(GameConfig::Key.Pay);
4565 auto olddel = ImmediateDelete;
4566 ImmediateDelete = false;
4571 addBackgroundGfxDetails();
4573 levBGImgName = 'bgCave';
4574 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
4576 blockWaterChecking = true;
4580 ImmediateDelete = olddel;
4581 CollectGarbage(true); // destroy delayed objects too
4583 if (dumpGridStats) objGrid.dumpStats();
4585 playerExited = false; // just in case
4586 playerExitDoor = none;
4588 osdClear(clearTalk:true);
4594 winCutSceneTimer = -1;
4595 winCutScenePhase = 0;
4598 if (global.config.gameMode != GameConfig::GameMode.Vanilla) {
4599 if (global.config.bizarre) {
4600 global.yasmScore = 1;
4601 global.config.bizarrePlusTitle = true;
4604 array!MapTile toReplace;
4605 forEachTile(delegate bool (MapTile t) {
4606 if (t.objType == 'oGTemple' ||
4607 t.objType == 'oIce' ||
4608 t.objType == 'oDark' ||
4609 t.objType == 'oBrick' ||
4610 t.objType == 'oLush')
4617 foreach (MapTile t; miscTileGrid.allObjects()) {
4618 if (t.objType == 'oGTemple' ||
4619 t.objType == 'oIce' ||
4620 t.objType == 'oDark' ||
4621 t.objType == 'oBrick' ||
4622 t.objType == 'oLush')
4628 foreach (MapTile t; toReplace) {
4630 t.cleanDeath = true;
4631 if (rand(1,120) == 1) instance_change(oGTemple, false);
4632 else if (rand(1,100) == 1) instance_change(oIce, false);
4633 else if (rand(1,90) == 1) instance_change(oDark, false);
4634 else if (rand(1,80) == 1) instance_change(oBrick, false);
4635 else if (rand(1,70) == 1) instance_change(oLush, false);
4643 if (rand(1,5) == 1) instance_change(oLush, false);
4648 //!instance_create(0, 0, oBricks);
4650 //shakeToggle = false;
4651 //oPDummy.status = 2;
4656 if (global.kaliPunish >= 2) {
4657 instance_create(oPDummy.x, oPDummy.y+2, oBall2);
4658 obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
4660 obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
4662 obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
4664 obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
4671 void startWinCutsceneVolcano () {
4672 global.hasParachute = false;
4674 writeln("VOLCANO HOLD 0: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4675 writeln("VOLCANO PICK 0: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4679 winCutsceneSwitchToNext = false;
4680 auto olddel = ImmediateDelete;
4681 ImmediateDelete = false;
4685 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
4687 blockWaterChecking = true;
4689 ImmediateDelete = olddel;
4690 CollectGarbage(true); // destroy delayed objects too
4692 spawnPlayerAt(2*16+8, 11*16+8);
4693 player.dir = MapEntity::Dir.Right;
4695 playerExited = false; // just in case
4696 playerExitDoor = none;
4698 osdClear(clearTalk:true);
4704 winCutSceneTimer = -1;
4705 winCutScenePhase = 0;
4707 MakeMapTile(0, 0, 'oEnd2BG');
4708 realViewStart.x = 0;
4709 realViewStart.y = 0;
4718 player.dead = false;
4719 player.active = true;
4720 player.visible = false;
4721 player.removeBallAndChain(temp:true);
4722 player.stunned = false;
4723 player.status = MapObject::FALLING;
4724 if (player.holdItem) player.holdItem.visible = false;
4725 player.fltx = 320/2;
4729 writeln("VOLCANO HOLD 1: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4730 writeln("VOLCANO PICK 1: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4735 void startWinCutsceneWinFall () {
4736 global.hasParachute = false;
4738 writeln("FALL HOLD 0: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4739 writeln("FALL PICK 0: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4743 winCutsceneSwitchToNext = false;
4745 auto olddel = ImmediateDelete;
4746 ImmediateDelete = false;
4750 setMenuTilesVisible(false);
4752 //addBackgroundGfxDetails();
4755 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
4757 blockWaterChecking = true;
4761 ImmediateDelete = olddel;
4762 CollectGarbage(true); // destroy delayed objects too
4764 if (dumpGridStats) objGrid.dumpStats();
4766 playerExited = false; // just in case
4767 playerExitDoor = none;
4769 osdClear(clearTalk:true);
4775 winCutSceneTimer = -1;
4776 winCutScenePhase = 0;
4778 player.dead = false;
4779 player.active = true;
4780 player.visible = false;
4781 player.removeBallAndChain(temp:true);
4782 player.stunned = false;
4783 player.status = MapObject::FALLING;
4784 if (player.holdItem) player.holdItem.visible = false;
4785 player.fltx = 320/2;
4788 winSceneDrawStatus = 0;
4795 writeln("FALL HOLD 1: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4796 writeln("FALL PICK 1: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4801 void setGameOver () {
4802 if (inWinCutscene) {
4803 player.visible = false;
4804 player.removeBallAndChain(temp:true);
4805 if (player.holdItem) player.holdItem.visible = false;
4808 if (inWinCutscene > 0) {
4811 winSceneDrawStatus = 8;
4816 MapTile findEndPlatTile () {
4817 return forEachTile(delegate bool (MapTile t) { return (t isa MapTileEndPlat); }, castClass:MapTileEndPlat);
4821 MapObject findBigTreasure () {
4822 return forEachObject(delegate bool (MapObject o) { return (o isa MapObjectBigTreasure); }, castClass:MapObjectBigTreasure);
4826 void setMenuTilesVisible (bool vis) {
4828 forEachTile(delegate bool (MapTile t) {
4829 if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4830 t.invisible = false;
4835 forEachTile(delegate bool (MapTile t) {
4836 if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4845 void setMenuTilesOnTop () {
4846 forEachTile(delegate bool (MapTile t) {
4847 if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4855 void winCutscenePlayerControl (PlayerPawn plr) {
4856 auto payPress = isKeyPressed(GameConfig::Key.Pay);
4857 auto payRelease = isKeyReleased(GameConfig::Key.Pay);
4859 switch (winCutsceneSkip) {
4860 case 0: // nothing was pressed
4861 if (payPress) winCutsceneSkip = 1;
4863 case 1: // waiting for pay release
4864 if (payRelease) winCutsceneSkip = 2;
4866 case 2: // pay released, do skip
4871 // first winning room
4872 if (inWinCutscene == 1) {
4873 if (plr.ix < 448+8) {
4878 // waiting for chest to open
4879 if (winCutScenePhase == 0) {
4880 winCutSceneTimer = 120/2;
4881 winCutScenePhase = 1;
4886 if (winCutScenePhase == 1) {
4887 if (--winCutSceneTimer == 0) {
4888 winCutScenePhase = 2;
4889 winCutSceneTimer = 20;
4890 forEachObject(delegate bool (MapObject o) {
4891 if (o isa MapObjectBigChest) {
4892 o.setSprite(global.config.gameMode == GameConfig::GameMode.Vanilla ? 'sBigChestOpen' : 'sBigChestOpen2');
4893 auto treasure = MakeMapObject(o.ix, o.iy, 'oBigTreasure');
4897 o.playSound('sndClick');
4898 //!!!if (global.config.gameMode != GameConfig::GameMode.Vanilla) scrSprayGems(oBigChest.x+24, oBigChest.y+24);
4908 if (winCutScenePhase == 2) {
4909 if (--winCutSceneTimer == 0) {
4910 winCutScenePhase = 3;
4911 winCutSceneTimer = 50;
4917 if (winCutScenePhase == 3) {
4918 auto ep = findEndPlatTile();
4919 if (ep) MakeMapObject(ep.ix+global.randOther(0, 80), /*ep.iy*/192+32, 'oBurn');
4920 if (--winCutSceneTimer == 0) {
4921 winCutScenePhase = 4;
4922 winCutSceneTimer = 10;
4923 if (ep) MakeMapObject(ep.ix, ep.iy+30, 'oLavaSpray');
4929 // lava pump first accel
4930 if (winCutScenePhase == 4) {
4931 if (--winCutSceneTimer == 0) {
4932 forEachObject(delegate bool (MapObject o) {
4933 if (o isa MapObjectLavaSpray) o.yAcc = -0.1;
4939 // lava pump complete
4940 if (winCutScenePhase == 5) {
4941 if (--winCutSceneTimer == 0) {
4942 //if (oLavaSpray) oLavaSpray.yAcc = -0.1;
4943 startWinCutsceneVolcano();
4952 if (inWinCutscene == 2) {
4956 if (winCutScenePhase == 0) {
4957 winCutSceneTimer = 50;
4958 winCutScenePhase = 1;
4959 winVolcanoTimer = 10;
4963 if (winVolcanoTimer > 0) {
4964 if (--winVolcanoTimer == 0) {
4965 MakeMapObject(224+global.randOther(0,48), 144+global.randOther(0,8), 'oVolcanoFlame');
4966 winVolcanoTimer = global.randOther(10, 20);
4971 if (winCutScenePhase == 1) {
4972 if (--winCutSceneTimer == 0) {
4973 winCutSceneTimer = 30;
4974 winCutScenePhase = 2;
4975 auto sil = MakeMapObject(240, 132, 'oPlayerSil');
4983 if (winCutScenePhase == 2) {
4984 if (--winCutSceneTimer == 0) {
4985 winCutScenePhase = 3;
4986 auto sil = MakeMapObject(240, 132, 'oTreasureSil');
4996 // winning camel room
4997 if (inWinCutscene == 3) {
4998 //if (!player.holdItem) writeln("SCENE 3: LOST ITEM!");
5000 if (!plr.visible) plr.flty = -32;
5003 if (winCutScenePhase == 0) {
5004 winCutSceneTimer = 50;
5005 winCutScenePhase = 1;
5010 if (winCutScenePhase == 1) {
5011 if (--winCutSceneTimer == 0) {
5012 winCutSceneTimer = 50;
5013 winCutScenePhase = 2;
5014 plr.playSound('sndPFall');
5017 writeln("MUST BE CHAINED: ", plr.mustBeChained);
5018 if (plr.mustBeChained) {
5019 plr.removeBallAndChain(temp:true);
5020 plr.spawnBallAndChain();
5023 writeln("HOLD: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
5024 writeln("PICK: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
5026 if (!player.holdItem && player.pickedItem) player.scrSwitchToPocketItem(forceIfEmpty:false);
5027 if (player.holdItem) {
5028 player.holdItem.visible = true;
5029 player.holdItem.canLiveOutsideOfLevel = true;
5030 writeln("HOLD ITEM: '", GetClassName(player.holdItem.Class), "'");
5032 plr.status == MapObject::FALLING;
5033 global.plife += 99; // just in case
5038 if (winCutScenePhase == 2) {
5039 auto ball = plr.getMyBall();
5040 if (ball && plr.holdItem != ball) {
5041 ball.teleportTo(plr.fltx, plr.flty+8);
5045 if (plr.status == MapObject::STUNNED || plr.stunned) {
5049 auto treasure = MakeMapObject(144+16+8, -32, 'oBigTreasure');
5050 if (treasure) treasure.depth = 1;
5051 winCutScenePhase = 3;
5053 plr.playSound('sndTFall');
5058 if (winCutScenePhase == 3) {
5059 if (plr.status != MapObject::STUNNED && !plr.stunned) {
5060 auto bt = findBigTreasure();
5064 //plr.status = MapObject::JUMPING;
5066 plr.kJumpPressed = true;
5067 winCutScenePhase = 4;
5068 winCutSceneTimer = 50;
5075 if (winCutScenePhase == 4) {
5076 if (--winCutSceneTimer == 0) {
5077 setMenuTilesVisible(true);
5078 winCutScenePhase = 5;
5079 winSceneDrawStatus = 1;
5081 global.setMusicPitch(1.0);
5082 global.playMusic('musVictory', loop:false);
5083 winCutSceneTimer = 50;
5088 if (winCutScenePhase == 5) {
5089 if (winSceneDrawStatus == 3) {
5090 int money = stats.money;
5091 if (winMoneyCount < money) {
5092 if (money-winMoneyCount > 1000) {
5093 winMoneyCount += 1000;
5094 } else if (money-winMoneyCount > 100) {
5095 winMoneyCount += 100;
5096 } else if (money-winMoneyCount > 10) {
5097 winMoneyCount += 10;
5102 if (winMoneyCount >= money) {
5103 winMoneyCount = money;
5104 ++winSceneDrawStatus;
5109 if (winSceneDrawStatus == 7) {
5112 if (winFadeLevel >= 255) {
5113 ++winSceneDrawStatus;
5114 winCutSceneTimer = 30*30;
5119 if (winSceneDrawStatus == 8) {
5120 if (--winCutSceneTimer == 0) {
5126 if (--winCutSceneTimer == 0) {
5127 ++winSceneDrawStatus;
5128 winCutSceneTimer = 50;
5137 // ////////////////////////////////////////////////////////////////////////// //
5138 void renderWinCutsceneOverlay () {
5139 if (inWinCutscene == 3) {
5140 if (winSceneDrawStatus > 0) {
5141 Video.color = 0xff_ff_ff;
5142 sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
5143 //draw_set_color(txtCol);
5144 drawTextAt(64, 32, "YOU MADE IT!");
5146 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
5147 if (global.config.gameMode == GameConfig::GameMode.Vanilla) {
5148 Video.color = 0xff_ff_00; //draw_set_color(c_yellow);
5149 drawTextAt(64, 48, "Classic Mode done!");
5151 Video.color = 0x00_80_80; //draw_set_color(c_teal);
5152 if (global.config.bizarrePlus) drawTextAt(64, 48, "Bizarre Mode Plus done!");
5153 else drawTextAt(64, 48, "Bizarre Mode done!");
5154 //draw_set_color(c_white);
5156 if (!global.usedShortcut) {
5157 Video.color = 0xc0_c0_c0; //draw_set_color(c_silver);
5158 drawTextAt(64, 56, "No shortcuts used!");
5159 //draw_set_color(c_yellow);
5163 if (winSceneDrawStatus > 1) {
5164 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
5165 //draw_set_color(txtCol);
5166 Video.color = 0xff_ff_ff;
5167 drawTextAt(64, 64, "FINAL SCORE:");
5170 if (winSceneDrawStatus > 2) {
5171 sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
5172 Video.color = 0xff_ff_ff; //draw_set_color(c_white);
5173 drawTextAt(64, 72, va("$%d", winMoneyCount));
5176 if (winSceneDrawStatus > 4) {
5177 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
5178 Video.color = 0xff_ff_ff; //draw_set_color(c_white);
5179 drawTextAt(64, 96, va("Time: %s", time2str(winTime/30)));
5181 draw_set_color(c_white);
5182 if (s < 10) draw_text(96+24, 96, string(m) + ":0" + string(s));
5183 else draw_text(96+24, 96, string(m) + ":" + string(s));
5187 if (winSceneDrawStatus > 5) {
5188 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
5189 Video.color = 0xff_ff_ff; //draw_set_color(txtCol);
5190 drawTextAt(64, 96+8, "Kills: ");
5191 Video.color = 0xff_ff_ff; //draw_set_color(c_white);
5192 drawTextAt(96+24, 96+8, va("%s", stats.countKills()));
5195 if (winSceneDrawStatus > 6) {
5196 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
5197 Video.color = 0xff_ff_ff; //draw_set_color(txtCol);
5198 drawTextAt(64, 96+16, "Saves: ");
5199 Video.color = 0xff_ff_ff; //draw_set_color(c_white);
5200 drawTextAt(96+24, 96+16, va("%s", stats.damselsSaved));
5204 Video.color = (255-clamp(winFadeLevel, 0, 255))<<24;
5205 Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
5208 if (winSceneDrawStatus == 8) {
5209 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
5210 Video.color = 0xff_ff_ff; //draw_set_color(c_white);
5212 if (global.config.gameMode == GameConfig::GameMode.Vanilla) {
5213 Video.color = 0xff_ff_00; //draw_set_color(c_yellow);
5214 lastString = "YOU SHALL BE REMEMBERED AS A HERO.";
5216 Video.color = 0x00_ff_ff;
5217 if (global.config.bizarrePlus) lastString = "ANOTHER LEGENDARY ADVENTURE!";
5218 else lastString = "YOUR DISCOVERIES WILL BE CELEBRATED!";
5220 auto strLen = lastString.length*8;
5222 n = trunc(ceil(n/2.0));
5223 drawTextAt(n, 116, lastString);
5229 // ////////////////////////////////////////////////////////////////////////// //
5230 #include "roomTitle.vc"
5231 #include "roomTrans1.vc"
5232 #include "roomTrans2.vc"
5233 #include "roomTrans3.vc"
5234 #include "roomTrans4.vc"
5235 #include "roomOlmec.vc"
5236 #include "roomEnd.vc"
5237 #include "roomIntro.vc"
5238 #include "roomTutorial.vc"
5239 #include "roomScores.vc"
5240 #include "roomStars.vc"
5241 #include "roomSun.vc"
5242 #include "roomMoon.vc"
5245 // ////////////////////////////////////////////////////////////////////////// //
5246 #include "packages/Generator/loadRoomGens.vc"
5247 #include "packages/Generator/loadEntityGens.vc"