blood flies up slightly lower
[k8vacspelynky.git] / GameLevel.vc
blob618f36f6c5a71547634ae2ab0ccc2fa5ca7f35a8
1 /**********************************************************************************
2  * Copyright (c) 2008, 2009 Derek Yu and Mossmouth, LLC
3  * Copyright (c) 2018, Ketmar Dark
4  *
5  * This file is part of Spelunky.
6  *
7  * You can redistribute and/or modify Spelunky, including its source code, under
8  * the terms of the Spelunky User License.
9  *
10  * Spelunky is distributed in the hope that it will be entertaining and useful,
11  * but WITHOUT WARRANTY.  Please see the Spelunky User License for more details.
12  *
13  * The Spelunky User License should be available in "Game .Information", which
14  * can be found in the Resource Explorer, or as an external file called COPYING.
15  * If not, please obtain a new copy of Spelunky from <http://spelunkyworld.com/>
16  *
17  **********************************************************************************/
18 // this is the level we're playing in, with all objects and tiles
19 class GameLevel : Object;
21 //#define EXPERIMENTAL_RENDER_CACHE
23 const float FrameTime = 1.0f/30.0f;
25 const int dumpGridStats = true;
27 struct IVec2D {
28   int x, y;
31 // in tiles
32 //enum NormalTilesWidth = LevelGen::LevelWidth*RoomGen::Width+2;
33 //enum NormalTilesHeight = LevelGen::LevelHeight*RoomGen::Height+2;
35 enum MaxTilesWidth = 64;
36 enum MaxTilesHeight = 64;
38 GameGlobal global;
39 transient GameStats stats;
40 transient SpriteStore sprStore;
41 transient BackTileStore bgtileStore;
42 transient BackTileImage levBGImg;
43 name levBGImgName;
44 LevelGen lg;
45 transient name lastMusicName;
46 //RoomGen[LevelGen::LevelWidth, LevelGen::LevelHeight] rooms; // moved to levelgen
48 transient float accumTime;
49 transient bool gamePaused = false;
50 transient bool gameShowHelp = false;
51 transient int gameHelpScreen = 0;
52 const int MaxGameHelpScreen = 2;
53 transient bool checkWater;
54 transient int liquidTileCount; // cached
55 /*transient*/ int damselSaved;
57 // hud efffects
58 transient int xmoney;
59 transient int collectCounter;
60 /*transient*/ int levelMoneyStart;
62 // all movable (thinkable) map objects
63 EntityGrid objGrid; // monsters, items and tiles
65 MapBackTile backtiles;
66 bool blockWaterChecking;
68 int inWinCutscene;
69 int inIntroCutscene;
70 bool cameFromIntroRoom; // for title screen
72 LevelGen::RType[MaxTilesWidth, MaxTilesHeight] roomType;
74 enum LevelKind {
75   Normal,
76   Transition,
77   Title,
78   Intro,
79   Tutorial,
80   Scores,
81   Stars,
82   Sun,
83   Moon,
84   //Final,
86 LevelKind levelKind = LevelKind.Normal;
88 array!MapTile allEnters;
89 array!MapTile allExits;
92 int startRoomX, startRoomY;
93 int endRoomX, endRoomY;
95 PlayerPawn player;
96 transient bool playerExited;
97 transient MapEntity playerExitDoor;
98 transient bool disablePlayerThink = false;
99 transient int maxPlayingTime; // in seconds
100 int levelStartTime;
101 int levelEndTime;
103 int ghostTimeLeft;
104 int musicFadeTimer;
105 bool ghostSpawned; // to speed up some checks
106 bool resetBMCOG = false;
107 int udjatAlarm;
110 // FPS, i.e. incremented by 30 in one second
111 int time; // in frames
112 int lastUsedObjectId;
113 transient int lastRenderTime = -1;
114 transient int pausedTime;
116 MapEntity deadItemsHead;
118 // screen shake variables
119 int shakeLeft;
120 IVec2D shakeOfs;
121 IVec2D shakeDir;
123 // set this before calling `fixCamera()`
124 // dimensions should be real, not scaled up/down
125 transient int viewWidth, viewHeight;
126 //transient int viewOffsetX, viewOffsetY;
128 // room bounds, not scaled
129 IVec2D viewMin, viewMax;
131 // for Olmec level cinematics
132 IVec2D cameraSlideToDest;
133 IVec2D cameraSlideToCurr;
134 IVec2D cameraSlideToSpeed; // !0: slide
135 int cameraSlideToPlayer;
136 // `fixCamera()` will set the following
137 // coordinates will be real too (with scale applied)
138 // shake is not applied
139 transient IVec2D viewStart; // with `player.viewOffset`
140 private transient IVec2D realViewStart; // without `player.viewOffset`
142 transient int framesProcessedFromLastClear;
144 transient int BuildYear;
145 transient int BuildMonth;
146 transient int BuildDay;
147 transient int BuildHour;
148 transient int BuildMin;
149 transient string BuildDateString;
152 final string getBuildDateString () {
153   if (!BuildYear) return BuildDateString;
154   if (BuildDateString) return BuildDateString;
155   BuildDateString = va("%d-%02d-%02d %02d:%02d", BuildYear, BuildMonth, BuildDay, BuildHour, BuildMin);
156   return BuildDateString;
160 final void cameraSlideToPoint (int dx, int dy, int speedx, int speedy) {
161   cameraSlideToPlayer = 0;
162   cameraSlideToDest.x = dx;
163   cameraSlideToDest.y = dy;
164   cameraSlideToSpeed.x = abs(speedx);
165   cameraSlideToSpeed.y = abs(speedy);
166   cameraSlideToCurr.x = cameraCurrX;
167   cameraSlideToCurr.y = cameraCurrY;
171 final void cameraReturnToPlayer () {
172   if (!cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y)) {
173     cameraSlideToCurr.x = cameraCurrX;
174     cameraSlideToCurr.y = cameraCurrY;
175     if (cameraSlideToSpeed.x && abs(cameraSlideToSpeed.x) < 8) cameraSlideToSpeed.x = 8;
176     if (cameraSlideToSpeed.y && abs(cameraSlideToSpeed.y) < 8) cameraSlideToSpeed.y = 8;
177     cameraSlideToPlayer = 1;
178   }
182 // if `frameSkip` is `true`, there are more frames waiting
183 // (i.e. you may skip rendering and such)
184 transient void delegate (bool frameSkip) onBeforeFrame;
185 transient void delegate (bool frameSkip) onAfterFrame;
187 transient void delegate () onCameraTeleported;
189 transient void delegate () onLevelExitedCB;
191 // this will be called in-between frames, and
192 // `frameTime` is [0..1)
193 transient void delegate (float frameTime) onInterFrame;
195 final int bizRoomStyle { get { return (lg ? lg.bizRoomStyle : 0); } }
198 final bool isNormalLevel () { return (levelKind == LevelKind.Normal); }
199 final bool isTitleRoom () { return (levelKind == LevelKind.Title); }
200 final bool isTutorialRoom () { return (levelKind == LevelKind.Tutorial); }
201 final bool isTransitionRoom () { return (levelKind == LevelKind.Transition); }
202 final bool isIntroRoom () { return (levelKind == LevelKind.Transition); }
205 bool isHUDEnabled () {
206   if (inWinCutscene) return false;
207   if (inIntroCutscene) return false;
208   if (lg.finalBossLevel) return true;
209   if (isNormalLevel()) return true;
210   return false;
214 // ////////////////////////////////////////////////////////////////////////// //
215 // stats
216 void addDeath (name aname) { if (isNormalLevel()) stats.addDeath(aname); }
218 int starsKills;
219 int sunScore;
220 int moonScore;
221 int moonTimer;
223 void addKill (name aname, optional bool telefrag) {
224        if (isNormalLevel()) stats.addKill(aname, telefrag!optional);
225   else if (aname == 'Shopkeeper' && levelKind == LevelKind.Stars) { ++stats.starsKills; ++starsKills; }
228 void addCollect (name aname, optional int amount) { if (isNormalLevel()) stats.addCollect(aname, amount!optional); }
230 void addDamselSaved () { if (isNormalLevel()) stats.addDamselSaved(); }
231 void addIdolStolen () { if (isNormalLevel()) stats.addIdolStolen(); }
232 void addIdolConverted () { if (isNormalLevel()) stats.addIdolConverted(); }
233 void addCrystalIdolStolen () { if (isNormalLevel()) stats.addCrystalIdolStolen(); }
234 void addCrystalIdolConverted () { if (isNormalLevel()) stats.addCrystalIdolConverted(); }
235 void addGhostSummoned () { if (isNormalLevel()) stats.addGhostSummoned(); }
238 // ////////////////////////////////////////////////////////////////////////// //
239 static final string time2str (int time) {
240   int secs = time%60; time /= 60;
241   int mins = time%60; time /= 60;
242   int hours = time%24; time /= 24;
243   int days = time;
244   if (days) return va("%d DAYS, %d:%02d:%02d", days, hours, mins, secs);
245   if (hours) return va("%d:%02d:%02d", hours, mins, secs);
246   return va("%02d:%02d", mins, secs);
250 // ////////////////////////////////////////////////////////////////////////// //
251 final int tilesWidth () { return lg.levelRoomWidth*RoomGen::Width+2; }
252 final int tilesHeight () { return (lg.finalBossLevel ? 55 : lg.levelRoomHeight*RoomGen::Height+2); }
255 // ////////////////////////////////////////////////////////////////////////// //
256 protected void resetGameInternal () {
257   if (player) player.removeBallAndChain();
258   resetBMCOG = false;
259   inWinCutscene = 0;
260   //inIntroCutscene = 0;
261   shakeLeft = 0;
262   udjatAlarm = 0;
263   starsKills = 0;
264   sunScore = 0;
265   moonScore = 0;
266   moonTimer = 0;
267   damselSaved = 0;
268   xmoney = 0;
269   collectCounter = 0;
270   levelMoneyStart = 0;
271   if (player) {
272     player.removeBallAndChain();
273     auto hi = player.holdItem;
274     player.holdItem = none;
275     if (hi) hi.instanceRemove();
276     hi = player.pickedItem;
277     player.pickedItem = none;
278     if (hi) hi.instanceRemove();
279   }
280   time = 0;
281   lastRenderTime = -1;
282   levelStartTime = 0;
283   levelEndTime = 0;
284   global.resetGame();
285   stats.clearGameTotals();
289 // this won't generate a level yet
290 void restartGame () {
291   resetGameInternal();
292   if (global.startMoney > 0) stats.setMoneyCheat();
293   stats.setMoney(global.startMoney);
294   levelKind = LevelKind.Normal;
298 // complement function to `restart game`
299 void generateNormalLevel () {
300   generateLevel();
301   centerViewAtPlayer();
305 void restartTitle () {
306   resetGameInternal();
307   stats.setMoney(0);
308   createSpecialLevel(LevelKind.Title, &createTitleRoom, 'musTitle');
309   global.plife = 9999;
310   global.bombs = 0;
311   global.rope = 0;
312   global.arrows = 0;
313   global.sgammo = 0;
317 void restartIntro () {
318   resetGameInternal();
319   stats.setMoney(0);
320   createSpecialLevel(LevelKind.Intro, &createIntroRoom, '');
321   global.plife = 9999;
322   global.bombs = 0;
323   global.rope = 1;
324   global.arrows = 0;
325   global.sgammo = 0;
329 void restartTutorial () {
330   resetGameInternal();
331   stats.setMoney(0);
332   createSpecialLevel(LevelKind.Tutorial, &createTutorialRoom, 'musCave');
333   global.plife = 4;
334   global.bombs = 0;
335   global.rope = 4;
336   global.arrows = 0;
337   global.sgammo = 0;
341 void restartScores () {
342   resetGameInternal();
343   stats.setMoney(0);
344   createSpecialLevel(LevelKind.Scores, &createScoresRoom, 'musTitle');
345   global.plife = 4;
346   global.bombs = 0;
347   global.rope = 0;
348   global.arrows = 0;
349   global.sgammo = 0;
353 void restartStarsRoom () {
354   resetGameInternal();
355   stats.setMoney(0);
356   createSpecialLevel(LevelKind.Stars, &createStarsRoom, '');
357   global.plife = 8;
358   global.bombs = 0;
359   global.rope = 0;
360   global.arrows = 0;
361   global.sgammo = 0;
365 void restartSunRoom () {
366   resetGameInternal();
367   stats.setMoney(0);
368   createSpecialLevel(LevelKind.Sun, &createSunRoom, '');
369   global.plife = 8;
370   global.bombs = 0;
371   global.rope = 0;
372   global.arrows = 0;
373   global.sgammo = 0;
377 void restartMoonRoom () {
378   resetGameInternal();
379   stats.setMoney(0);
380   createSpecialLevel(LevelKind.Moon, &createMoonRoom, '');
381   global.plife = 8;
382   global.bombs = 0;
383   global.rope = 0;
384   global.arrows = 100;
385   global.sgammo = 0;
389 // ////////////////////////////////////////////////////////////////////////// //
390 // generate angry shopkeeper at exit if murderer or thief
391 void generateAngryShopkeepers () {
392   if (global.murderer || global.thiefLevel > 0) {
393     foreach (MapTile e; allExits) {
394       auto obj = MonsterShopkeeper(MakeMapObject(e.ix, e.iy, 'oShopkeeper'));
395       if (obj) {
396         obj.style = 'Bounty Hunter';
397         obj.status = MapObject::PATROL;
398       }
399     }
400   }
404 // ////////////////////////////////////////////////////////////////////////// //
405 final void resetRoomBounds () {
406   viewMin.x = 0;
407   viewMin.y = 0;
408   viewMax.x = tilesWidth*16;
409   viewMax.y = tilesHeight*16;
410   // Great Lake is bottomless (nope)
411   //if (global.lake == 1) viewMax.y -= 16;
412   //writeln("ROOM BOUNDS: (", viewMax.x, "x", viewMax.y, ")");
416 final void setRoomBounds (int x0, int y0, int x1, int y1) {
417   viewMin.x = x0;
418   viewMin.y = y0;
419   viewMax.x = x1+16;
420   viewMax.y = y1+16;
424 // ////////////////////////////////////////////////////////////////////////// //
425 struct OSDMessage {
426   string msg;
427   float timeout; // seconds
428   float starttime; // for active
429   bool active; // true: timeout is `GetTickCount()` dismissing time
432 array!OSDMessage msglist; // [0]: current one
435 private final void osdCheckTimeouts () {
436   auto stt = GetTickCount();
437   while (msglist.length) {
438     if (!msglist[0].active) {
439       msglist[0].active = true;
440       msglist[0].starttime = stt;
441     }
442     if (msglist[0].starttime+msglist[0].timeout >= stt) break;
443     msglist.remove(0);
444   }
448 final bool osdHasMessage () {
449   osdCheckTimeouts();
450   return (msglist.length > 0);
454 final string osdGetMessage (out float timeLeft, out float timeStart) {
455   osdCheckTimeouts();
456   if (msglist.length == 0) { timeLeft = 0; return ""; }
457   auto stt = GetTickCount();
458   timeStart = msglist[0].starttime;
459   timeLeft = fmax(0, msglist[0].starttime+msglist[0].timeout-stt);
460   return msglist[0].msg;
464 final void osdClear () {
465   msglist.clear();
469 final void osdMessage (string msg, optional float timeout, optional bool urgent) {
470   if (!msg) return;
471   msg = global.expandString(msg);
472   if (!specified_timeout) timeout = 3.33;
473   // special message for shops
474   if (timeout == -666) {
475     if (!msg) return;
476     if (msglist.length && msglist[0].msg == msg) return;
477     if (msglist.length == 0 || msglist[0].msg != msg) {
478       osdClear();
479       msglist.length += 1;
480       msglist[0].msg = msg;
481     }
482     msglist[0].active = false;
483     msglist[0].timeout = 3.33;
484     osdCheckTimeouts();
485     return;
486   }
487   if (timeout < 0.1) return;
488   timeout = fmax(1.0, timeout);
489   //writeln("OSD: ", msg);
490   // find existing one, and bring it to the top
491   int oldidx = 0;
492   for (; oldidx < msglist.length; ++oldidx) {
493     if (msglist[oldidx].msg == msg) break; // i found her!
494   }
495   // duplicate?
496   if (oldidx < msglist.length) {
497     // yeah, move duplicate to the top
498     msglist[oldidx].timeout = fmax(timeout, msglist[oldidx].timeout);
499     msglist[oldidx].active = false;
500     if (urgent && oldidx != 0) {
501       timeout = msglist[oldidx].timeout;
502       msglist.remove(oldidx);
503       msglist.insert(0);
504       msglist[0].msg = msg;
505       msglist[0].timeout = timeout;
506       msglist[0].active = false;
507     }
508   } else if (urgent) {
509     msglist.insert(0);
510     msglist[0].msg = msg;
511     msglist[0].timeout = timeout;
512     msglist[0].active = false;
513   } else {
514     // new one
515     msglist.length += 1;
516     msglist[$-1].msg = msg;
517     msglist[$-1].timeout = timeout;
518     msglist[$-1].active = false;
519   }
520   osdCheckTimeouts();
524 // ////////////////////////////////////////////////////////////////////////// //
525 void setup (GameGlobal aGlobal, SpriteStore aSprStore, BackTileStore aBGTileStore) {
526   global = aGlobal;
527   sprStore = aSprStore;
528   bgtileStore = aBGTileStore;
530   lg = SpawnObject(LevelGen);
531   lg.global = global;
532   lg.level = self;
534   objGrid = SpawnObject(EntityGrid);
535   objGrid.setup(0, 0, MaxTilesWidth*16, MaxTilesHeight*16);
539 // stores should be set
540 void onLoaded () {
541   checkWater = true;
542   liquidTileCount = 0;
543   levBGImg = bgtileStore[levBGImgName];
544   foreach (MapEntity o; objGrid.allObjects()) {
545     o.onLoaded();
546     auto t = MapTile(o);
547     if (t && (t.lava || t.water)) ++liquidTileCount;
548   }
549   for (MapBackTile bt = backtiles; bt; bt = bt.next) bt.onLoaded();
550   if (player) player.onLoaded();
551   //FIXME
552   if (msglist.length) {
553     msglist[0].active = false;
554     msglist[0].timeout = 0.200;
555     osdCheckTimeouts();
556   }
557   if (lg && lg.musicName) global.playMusic(lg.musicName); else global.stopMusic();
561 // ////////////////////////////////////////////////////////////////////////// //
562 void pickedSpectacles () {
563   foreach (MapTile t; objGrid.allObjectsSafe(MapTile)) t.onGotSpectacles();
567 // ////////////////////////////////////////////////////////////////////////// //
568 #include "rgentile.vc"
569 #include "rgenobj.vc"
572 void onLevelExited () {
573   if (playerExitDoor isa TitleTileXTitle) {
574     playerExitDoor = none;
575     restartTitle();
576     return;
577   }
578   // title
579   if (isTitleRoom() || levelKind == LevelKind.Scores) {
580     if (playerExitDoor) processTitleExit(playerExitDoor);
581     playerExitDoor = none;
582     return;
583   }
584   if (isTutorialRoom()) {
585     playerExitDoor = none;
586     restartGame();
587     global.currLevel = 1;
588     generateNormalLevel();
589     return;
590   }
591   // challenges
592   if (levelKind == LevelKind.Stars || levelKind == LevelKind.Sun || levelKind == LevelKind.Moon) {
593     playerExitDoor = none;
594     levelEndTime = time;
595     if (onLevelExitedCB) onLevelExitedCB();
596     restartTitle();
597     return;
598   }
599   // normal level
600   if (isNormalLevel()) {
601     stats.maxLevelComplete = max(stats.maxLevelComplete, global.currLevel);
602     levelEndTime = time;
603     if (playerExitDoor) {
604       if (playerExitDoor.objType == 'oXGold') {
605         writeln("exiting to City Of Gold");
606         global.cityOfGold = true;
607         //!global.currLevel += 1;
608       } else if (playerExitDoor.objType == 'oXMarket') {
609         writeln("exiting to Black Market");
610         global.genBlackMarket = true;
611         //!global.currLevel += 1;
612       }
613     }
614   }
615   if (onLevelExitedCB) onLevelExitedCB();
616   //
617   playerExitDoor = none;
618   if (levelKind == LevelKind.Transition) {
619     if (global.thiefLevel > 0) global.thiefLevel -= 1;
620     if (global.alienCraft) ++global.alienCraft;
621     if (global.yetiLair) ++global.yetiLair;
622     if (global.lake) ++global.lake;
623     if (global.cityOfGold) ++global.cityOfGold;
624     //orig dbg:if (global.currLevel == 1) global.currLevel += firstLevelSkip; else global.currLevel += levelSkip;
625     /+
626     if (!global.blackMarket && !global.cityOfGold /*&& !global.yetiLair*/) {
627       global.currLevel += 1;
628     }
629     +/
630     ++global.currLevel;
631     generateLevel();
632   } else {
633     // < 20 seconds per level: looks like a speedrun
634     global.noDarkLevel = (levelEndTime > levelStartTime && levelEndTime-levelStartTime < 20*30);
635     if (lg.finalBossLevel) {
636       winTime = time;
637       ++stats.gamesWon;
638       // add money for big idol
639       player.addScore(50000);
640       stats.gameOver();
641       startWinCutscene();
642     } else {
643       generateTransitionLevel();
644     }
645   }
646   //centerViewAtPlayer();
650 void onOlmecDead (MapObject o) {
651   writeln("*** OLMEC IS DEAD!");
652   foreach (MapTile t; allExits) {
653     if (t.exit) {
654       t.openExit();
655       auto st = checkTileAtPoint(t.ix+8, t.iy+16);
656       if (!st) {
657         st = MakeMapTile(t.ix/16, t.iy/16+1, 'oTemple');
658         st.ore = 0;
659       }
660       st.invincible = true;
661     }
662   }
666 void generateLevelMessages () {
667   writeln("LEVEL NUMBER: ", global.currLevel);
668   if (global.darkLevel) {
669     if (global.hasCrown) {
670        osdMessage("THE HEDJET SHINES BRIGHTLY.");
671        global.darkLevel = false;
672     } else if (global.config.scumDarkness < 2) {
673       osdMessage("I CAN'T SEE A THING!\nI'D BETTER USE THESE FLARES!");
674     }
675   }
677   if (global.blackMarket) osdMessage("WELCOME TO THE BLACK MARKET!");
679   if (global.cemetary) osdMessage("THE DEAD ARE RESTLESS!");
680   if (global.lake == 1) osdMessage("I CAN HEAR RUSHING WATER...");
682   if (global.snakePit) osdMessage("I HEAR SNAKES... I HATE SNAKES!");
683   if (global.yetiLair == 1) osdMessage("IT SMELLS LIKE WET FUR IN HERE!");
684   if (global.alienCraft == 1) osdMessage("THERE'S A PSYCHIC PRESENCE HERE!");
685   if (global.cityOfGold == 1) {
686     if (true /*hasOlmec()*/) osdMessage("IT'S THE LEGENDARY CITY OF GOLD!");
687   }
689   if (global.sacrificePit) osdMessage("I CAN HEAR PRAYERS TO KALI!");
693 final MapObject debugSpawnObjectWithClass (class!MapObject oclass, optional bool playerDir) {
694   if (!oclass) return none;
695   int dx = 0, dy = 0;
696   bool canLeft = !isSolidAtPoint(player.ix-8, player.iy);
697   bool canRight = !isSolidAtPoint(player.ix+16, player.iy);
698   if (!canLeft && !canRight) return none;
699   if (canLeft && canRight) {
700     if (playerDir) {
701       dx = (player.dir == MapObject::Dir.Left ? -16 : 16);
702     } else {
703       dx = 16;
704     }
705   } else {
706     dx = (canLeft ? -16 : 16);
707   }
708   auto obj = SpawnMapObjectWithClass(oclass);
709   if (obj isa MapEnemy) { dx -= 8; dy -= 8; }
710   if (obj) PutSpawnedMapObject(player.ix+dx, player.iy+dy, obj);
711   return obj;
715 final MapObject debugSpawnObject (name aname) {
716   if (!aname) return none;
717   return debugSpawnObjectWithClass(findGameObjectClassByName(aname));
721 void createSpecialLevel (LevelKind kind, scope void delegate () creator, name amusic) {
722   global.darkLevel = false;
723   udjatAlarm = 0;
724   xmoney = 0;
725   collectCounter = 0;
726   global.resetStartingItems();
728   global.setMusicPitch(1.0);
729   levelKind = kind;
731   auto olddel = ImmediateDelete;
732   ImmediateDelete = false;
733   clearWholeLevel();
735   creator();
737   setMenuTilesOnTop();
739   fixWallTiles();
740   addBackgroundGfxDetails();
741   //levBGImgName = 'bgCave';
742   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
744   blockWaterChecking = true;
745   fixLiquidTop();
746   cleanDeadTiles();
748   ImmediateDelete = olddel;
749   CollectGarbage(true); // destroy delayed objects too
751   if (dumpGridStats) objGrid.dumpStats();
753   playerExited = false; // just in case
754   playerExitDoor = none;
756   osdClear();
758   setupGhostTime();
759   lg.musicName = amusic;
760   if (amusic) global.playMusic(lg.musicName); else global.stopMusic();
764 void createTitleLevel () {
765   createSpecialLevel(LevelKind.Title, &createTitleRoom, 'musTitle');
769 void createTutorialLevel () {
770   createSpecialLevel(LevelKind.Tutorial, &createTutorialRoom, 'musCave');
771   global.plife = 4;
772   global.bombs = 0;
773   global.rope = 4;
774   global.arrows = 0;
775   global.sgammo = 0;
779 // `global.currLevel` is the new level
780 void generateTransitionLevel () {
781   global.darkLevel = false;
782   udjatAlarm = 0;
783   xmoney = 0;
784   collectCounter = 0;
786   resetTransitionOverlay();
788   global.setMusicPitch(1.0);
789   switch (global.config.transitionMusicMode) {
790     case GameConfig::MusicMode.Silent: global.stopMusic(); break;
791     case GameConfig::MusicMode.Restart: global.restartMusic(); break;
792     case GameConfig::MusicMode.DontTouch: break;
793   }
795   levelKind = LevelKind.Transition;
797   auto olddel = ImmediateDelete;
798   ImmediateDelete = false;
799   clearWholeLevel();
801        if (global.currLevel < 4) createTrans1Room();
802   else if (global.currLevel == 4) createTrans1xRoom();
803   else if (global.currLevel < 8) createTrans2Room();
804   else if (global.currLevel == 8) createTrans2xRoom();
805   else if (global.currLevel < 12) createTrans3Room();
806   else if (global.currLevel == 12) createTrans3xRoom();
807   else if (global.currLevel < 16) createTrans4Room();
808   else if (global.currLevel == 16) createTrans4Room();
809   else createTrans1Room(); //???
811   setMenuTilesOnTop();
813   fixWallTiles();
814   addBackgroundGfxDetails();
815   //levBGImgName = 'bgCave';
816   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
818   blockWaterChecking = true;
819   fixLiquidTop();
820   cleanDeadTiles();
822   if (damselSaved > 0) {
823     // this is special "damsel ready to kiss you" object, not a heart
824     MakeMapObject(176+8, 176+8, 'oDamselKiss');
825     global.plife += damselSaved; // if player skipped transition cutscene
826     damselSaved = 0;
827   }
829   ImmediateDelete = olddel;
830   CollectGarbage(true); // destroy delayed objects too
832   if (dumpGridStats) objGrid.dumpStats();
834   playerExited = false; // just in case
835   playerExitDoor = none;
837   osdClear();
839   setupGhostTime();
840   //global.playMusic(lg.musicName);
844 void generateLevel () {
845   levelStartTime = time;
846   levelEndTime = time;
848   udjatAlarm = 0;
849   if (resetBMCOG) {
850     resetBMCOG = false;
851     global.genBlackMarket = false;
852   }
854   global.setMusicPitch(1.0);
855   stats.clearLevelTotals();
857   levelKind = LevelKind.Normal;
858   lg.generate();
859   //lg.dump();
861   resetRoomBounds();
863   lg.generateRooms();
864   //writeln("tw:", tilesWidth, "; th:", tilesHeight);
866   auto olddel = ImmediateDelete;
867   ImmediateDelete = false;
868   clearWholeLevel();
870   if (lg.finalBossLevel) {
871     blockWaterChecking = true;
872     createOlmecRoom();
873   }
875   // if transition cutscene was skipped...
876   global.plife += max(0, damselSaved); // if player skipped transition cutscene
877   damselSaved = 0;
879   // generate tiles
880   startRoomX = lg.startRoomX;
881   startRoomY = lg.startRoomY;
882   endRoomX = lg.endRoomX;
883   endRoomY = lg.endRoomY;
884   addBackgroundGfxDetails();
885   foreach (int y; 0..tilesHeight) {
886     foreach (int x; 0..tilesWidth) {
887       lg.genTileAt(x, y);
888     }
889   }
890   fixWallTiles();
892   levBGImgName = lg.bgImgName;
893   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
895   if (global.allowAngryShopkeepers) generateAngryShopkeepers();
897   lg.generateEntities();
899   // add box of flares to dark level
900   if (global.darkLevel && allEnters.length) {
901     auto enter = allEnters[0];
902     int x = enter.ix, y = enter.iy;
903          if (!isSolidAtPoint(x-16, y)) MakeMapObject(x-16+8, y+8, 'oFlareCrate');
904     else if (!isSolidAtPoint(x+16, y)) MakeMapObject(x+16+8, y+8, 'oFlareCrate');
905     else MakeMapObject(x+8, y+8, 'oFlareCrate');
906   }
908   //scrGenerateEntities();
909   //foreach (; 0..2) scrGenerateEntities();
911   writeln(objGrid.countObjects, " alive objects inserted");
912   writeln(countBackTiles, " background tiles inserted");
914   if (!player) FatalError("player pawn is not spawned");
916   if (lg.finalBossLevel) {
917     blockWaterChecking = true;
918   } else {
919     blockWaterChecking = false;
920   }
921   fixLiquidTop();
922   cleanDeadTiles();
924   ImmediateDelete = olddel;
925   CollectGarbage(true); // destroy delayed objects too
927   if (dumpGridStats) objGrid.dumpStats();
929   playerExited = false; // just in case
930   playerExitDoor = none;
932   levelMoneyStart = stats.money;
934   osdClear();
935   generateLevelMessages();
937   xmoney = 0;
938   collectCounter = 0;
940   if (lastMusicName != lg.musicName) {
941     global.playMusic(lg.musicName);
942     //writeln("!<", lastMusicName, ">:<", lg.musicName, ">");
943   } else {
944     //writeln("MM: ", global.config.nextLevelMusicMode);
945     switch (global.config.nextLevelMusicMode) {
946       case GameConfig::MusicMode.Silent: global.stopMusic(); break; // the thing that should not be
947       case GameConfig::MusicMode.Restart: global.restartMusic(); break;
948       case GameConfig::MusicMode.DontTouch:
949         if (global.config.transitionMusicMode == GameConfig::MusicMode.Silent) {
950           global.playMusic(lg.musicName);
951         }
952         break;
953     }
954   }
955   lastMusicName = lg.musicName;
956   //global.playMusic(lg.musicName);
958   setupGhostTime();
959   if (global.cityOfGold == 1 || global.genBlackMarket) resetBMCOG = true;
961   if (global.cityOfGold == 1) {
962     lg.mapSprite = 'sMapTemple';
963     lg.mapTitle = "City of Gold";
964   } else if (global.blackMarket) {
965     lg.mapSprite = 'sMapJungle';
966     lg.mapTitle = "Black Market";
967   }
971 // ////////////////////////////////////////////////////////////////////////// //
972 int currKeys, nextKeys;
973 int pressedKeysQ, releasedKeysQ;
974 int keysPressed, keysReleased = -1;
977 struct SavedKeyState {
978   int currKeys, nextKeys;
979   int pressedKeysQ, releasedKeysQ;
980   int keysPressed, keysReleased;
981   // for session
982   int roomSeed, otherSeed;
986 // for saving/replaying
987 final void keysSaveState (out SavedKeyState ks) {
988   ks.currKeys = currKeys;
989   ks.nextKeys = nextKeys;
990   ks.pressedKeysQ = pressedKeysQ;
991   ks.releasedKeysQ = releasedKeysQ;
992   ks.keysPressed = keysPressed;
993   ks.keysReleased = keysReleased;
996 // for saving/replaying
997 final void keysRestoreState (const ref SavedKeyState ks) {
998   currKeys = ks.currKeys;
999   nextKeys = ks.nextKeys;
1000   pressedKeysQ = ks.pressedKeysQ;
1001   releasedKeysQ = ks.releasedKeysQ;
1002   keysPressed = ks.keysPressed;
1003   keysReleased = ks.keysReleased;
1007 final void keysNextFrame () {
1008   currKeys = nextKeys;
1012 final void clearKeys () {
1013   currKeys = 0;
1014   nextKeys = 0;
1015   pressedKeysQ = 0;
1016   releasedKeysQ = 0;
1017   keysPressed = 0;
1018   keysReleased = -1;
1022 final void onKey (int code, bool down) {
1023   if (!code) return;
1024   if (down) {
1025     currKeys |= code;
1026     nextKeys |= code;
1027     if (keysReleased&code) {
1028       keysPressed |= code;
1029       keysReleased &= ~code;
1030       pressedKeysQ |= code;
1031     }
1032   } else {
1033     nextKeys &= ~code;
1034     if (keysPressed&code) {
1035       keysReleased |= code;
1036       keysPressed &= ~code;
1037       releasedKeysQ |= code;
1038     }
1039   }
1042 final bool isKeyDown (int code) {
1043   return !!(currKeys&code);
1046 final bool isKeyPressed (int code) {
1047   bool res = !!(pressedKeysQ&code);
1048   pressedKeysQ &= ~code;
1049   return res;
1052 final bool isKeyReleased (int code) {
1053   bool res = !!(releasedKeysQ&code);
1054   releasedKeysQ &= ~code;
1055   return res;
1059 final void clearKeysPressRelease () {
1060   keysPressed = default.keysPressed;
1061   keysReleased = default.keysReleased;
1062   pressedKeysQ = default.pressedKeysQ;
1063   releasedKeysQ = default.releasedKeysQ;
1064   currKeys = 0;
1065   nextKeys = 0;
1069 // ////////////////////////////////////////////////////////////////////////// //
1070 final void registerEnter (MapTile t) {
1071   if (!t) return;
1072   allEnters[$] = t;
1073   return;
1077 final void registerExit (MapTile t) {
1078   if (!t) return;
1079   allExits[$] = t;
1080   return;
1084 final bool isYAtEntranceRow (int py) {
1085   py /= 16;
1086   foreach (MapTile t; allEnters) if (t.iy == py) return true;
1087   return false;
1091 final int calcNearestEnterDist (int px, int py) {
1092   if (allEnters.length == 0) return int.max;
1093   int curdistsq = int.max;
1094   foreach (MapTile t; allEnters) {
1095     int xc = px-t.xCenter, yc = py-t.yCenter;
1096     int distsq = xc*xc+yc*yc;
1097     if (distsq < curdistsq) curdistsq = distsq;
1098   }
1099   return round(sqrt(curdistsq));
1103 final int calcNearestExitDist (int px, int py) {
1104   if (allExits.length == 0) return int.max;
1105   int curdistsq = int.max;
1106   foreach (MapTile t; allExits) {
1107     int xc = px-t.xCenter, yc = py-t.yCenter;
1108     int distsq = xc*xc+yc*yc;
1109     if (distsq < curdistsq) curdistsq = distsq;
1110   }
1111   return round(sqrt(curdistsq));
1115 // ////////////////////////////////////////////////////////////////////////// //
1116 final void clearForTransition () {
1117   auto olddel = ImmediateDelete;
1118   ImmediateDelete = false;
1119   clearWholeLevel();
1120   ImmediateDelete = olddel;
1121   CollectGarbage(true); // destroy delayed objects too
1122   global.darkLevel = false;
1126 // ////////////////////////////////////////////////////////////////////////// //
1127 final int countBackTiles () {
1128   int res = 0;
1129   for (MapBackTile bt = backtiles; bt; bt = bt.next) ++res;
1130   return res;
1134 final void clearWholeLevel () {
1135   allEnters.clear();
1136   allExits.clear();
1138   // don't kill objects the player is holding
1139   if (player) {
1140     if (player.pickedItem isa ItemBall) {
1141       player.pickedItem.instanceRemove();
1142       player.pickedItem = none;
1143     }
1144     if (player.pickedItem && player.pickedItem.grid) {
1145       player.pickedItem.grid.remove(player.pickedItem.gridId);
1146       writeln("secured pocket item '", GetClassName(player.pickedItem.Class), "'");
1147     }
1148     if (player.holdItem isa ItemBall) {
1149       player.removeBallAndChain(temp:true);
1150       if (player.holdItem) player.holdItem.instanceRemove();
1151       player.holdItem = none;
1152     }
1153     if (player.holdItem && player.holdItem.grid) {
1154       player.holdItem.grid.remove(player.holdItem.gridId);
1155       writeln("secured held item '", GetClassName(player.holdItem.Class), "'");
1156     }
1157     writeln("secured ball; mustBeChained=", player.mustBeChained, "; wasHoldingBall=", player.wasHoldingBall);
1158   }
1160   int count = objGrid.countObjects();
1161   if (dumpGridStats) { if (objGrid.getFirstObjectCID()) objGrid.dumpStats(); }
1162   objGrid.removeAllObjects(true); // and destroy
1163   if (count > 0) writeln(count, " objects destroyed");
1165   lastUsedObjectId = 0;
1166   accumTime = 0;
1167   //!time = 0;
1168   lastRenderTime = -1;
1169   liquidTileCount = 0;
1170   checkWater = false;
1172   while (backtiles) {
1173     MapBackTile t = backtiles;
1174     backtiles = t.next;
1175     delete t;
1176   }
1178   levBGImg = none;
1179   framesProcessedFromLastClear = 0;
1183 final void insertObject (MapEntity o) {
1184   if (!o) return;
1185   if (o.grid) FatalError("cannot put object into level twice");
1186   objGrid.insert(o);
1190 final void spawnPlayerAt (int x, int y) {
1191   // if we have no player, spawn new one
1192   // otherwise this just a level transition, so simply reposition him
1193   if (!player) {
1194     // don't add player to object list, as it has very separate processing anyway
1195     player = SpawnObject(PlayerPawn);
1196     player.global = global;
1197     player.level = self;
1198     if (!player.initialize()) {
1199       delete player;
1200       FatalError("something is wrong with player initialization");
1201       return;
1202     }
1203   }
1204   player.fltx = x;
1205   player.flty = y;
1206   player.saveInterpData();
1207   player.resurrect();
1208   if (player.mustBeChained || global.config.scumBallAndChain) {
1209     writeln("*** spawning ball and chain");
1210     player.spawnBallAndChain(levelStart:true);
1211   }
1212   playerExited = false;
1213   playerExitDoor = none;
1214   if (global.config.startWithKapala) global.hasKapala = true;
1215   centerViewAtPlayer();
1216   // reinsert player items into grid
1217   if (player.pickedItem) objGrid.insert(player.pickedItem);
1218   if (player.holdItem) objGrid.insert(player.holdItem);
1219   //writeln("player spawned; active=", player.active);
1220   player.scrSwitchToPocketItem(forceIfEmpty:false);
1224 final void teleportPlayerTo (int x, int y) {
1225   if (player) {
1226     player.fltx = x;
1227     player.flty = y;
1228     player.saveInterpData();
1229   }
1233 final void resurrectPlayer () {
1234   if (player) player.resurrect();
1235   playerExited = false;
1236   playerExitDoor = none;
1240 // ////////////////////////////////////////////////////////////////////////// //
1241 final void scrShake (int duration) {
1242   if (shakeLeft == 0) {
1243     shakeOfs.x = 0;
1244     shakeOfs.y = 0;
1245     shakeDir.x = 0;
1246     shakeDir.y = 0;
1247   }
1248   shakeLeft = max(shakeLeft, duration);
1253 // ////////////////////////////////////////////////////////////////////////// //
1254 enum SCAnger {
1255   TileDestroyed,
1256   ItemStolen, // including damsel, lol
1257   CrapsCheated,
1258   BombDropped,
1259   DamselWhipped,
1263 // make the nearest shopkeeper angry. RAWR!
1264 void scrShopkeeperAnger (SCAnger reason, optional int maxdist, optional MapEntity offender) {
1265   if (!offender) offender = player;
1266   auto shp = MonsterShopkeeper(findNearestEnemy(offender.ix, offender.iy, delegate bool (MapEnemy o) {
1267     auto sc = MonsterShopkeeper(o);
1268     if (!sc) return false;
1269     if (sc.dead || sc.angered) return false;
1270     return true;
1271   }, castClass:MonsterShopkeeper));
1273   if (shp) {
1274     if (specified_maxdist && offender.directionToEntityCenter(shp) > maxdist) return;
1275     if (!shp.dead && !shp.angered) {
1276       shp.status = MapObject::ATTACK;
1277       string msg;
1278            if (global.murderer) msg = "YOU'LL PAY FOR YOUR CRIMES!";
1279       else if (reason == SCAnger.ItemStolen) msg = "COME BACK HERE, THIEF!";
1280       else if (reason == SCAnger.TileDestroyed) msg = "DIE, YOU VANDAL!";
1281       else if (reason == SCAnger.BombDropped) msg = "TERRORIST!";
1282       else if (reason == SCAnger.DamselWhipped) msg = "HEY, ONLY I CAN DO THAT!";
1283       else if (reason == SCAnger.CrapsCheated) msg = "DIE, CHEATER!";
1284       else msg = "NOW I'M REALLY STEAMED!";
1285       if (msg) osdMessage(msg, -666);
1286       global.thiefLevel += (global.thiefLevel > 0 ? 3 : 2);
1287     }
1288   }
1292 final MapObject findCrapsPrize () {
1293   foreach (MapObject o; objGrid.allObjects(MapObject)) {
1294     if (!o.spectral && o.inDiceHouse) return o;
1295   }
1296   return none;
1300 // ////////////////////////////////////////////////////////////////////////// //
1301 // 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.
1302 // note: idols moved by monkeys will have false `stolenIdol`
1303 void scrTriggerIdolAltar (bool stolenIdol) {
1304   ObjTikiCurse res = none;
1305   int curdistsq = int.max;
1306   int px = player.xCenter, py = player.yCenter;
1307   foreach (MapObject o; objGrid.allObjects(MapObject)) {
1308     auto tcr = ObjTikiCurse(o);
1309     if (!tcr) continue;
1310     if (tcr.activated) continue;
1311     int xc = px-tcr.xCenter, yc = py-tcr.yCenter;
1312     int distsq = xc*xc+yc*yc;
1313     if (distsq < curdistsq) {
1314       res = tcr;
1315       curdistsq = distsq;
1316     }
1317   }
1318   if (res) res.activate(stolenIdol);
1322 // ////////////////////////////////////////////////////////////////////////// //
1323 void setupGhostTime () {
1324   musicFadeTimer = -1;
1325   ghostSpawned = false;
1327   // there is no ghost on the first level
1328   if (inWinCutscene || inIntroCutscene || !isNormalLevel() || lg.finalBossLevel ||
1329       (!global.config.ghostAtFirstLevel && global.currLevel == 1))
1330   {
1331     ghostTimeLeft = -1;
1332     global.setMusicPitch(1.0);
1333     return;
1334   }
1336   if (global.config.scumGhost < 0) {
1337     // instant
1338     ghostTimeLeft = 1;
1339     osdMessage("A GHASTLY VOICE CALLS YOUR NAME!\nLET'S GET OUT OF HERE!", 6.66, urgent:true);
1340     return;
1341   }
1343   if (global.config.scumGhost == 0) {
1344     // never
1345     ghostTimeLeft = -1;
1346     return;
1347   }
1349   // randomizes time until ghost appears once time limit is reached
1350   // global.ghostRandom (true/false) if extra time is randomized - loaded from yasm.cfg. if false, use 30 second default
1351   // ghostTimeLeft (time in seconds * 1000) for currently generated level
1353   if (global.config.ghostRandom) {
1354     auto tMin = global.randOther(10, 30); // min(global.plife*10, 30);
1355     auto tMax = max(tMin+global.randOther(10, 30), min(global.config.scumGhost, 300)); // 5 minutes max
1356     auto tTime = global.randOther(tMin, tMax);
1357     if (tTime <= 0) tTime = round(tMax/2.0);
1358     ghostTimeLeft = tTime;
1359   } else {
1360     ghostTimeLeft = global.config.scumGhost; // 5 minutes max
1361   }
1363   ghostTimeLeft += max(0, global.config.ghostExtraTime);
1365   ghostTimeLeft *= 30; // seconds -> frames
1366   //global.ghostShowTime
1370 void spawnGhost () {
1371   addGhostSummoned();
1372   ghostSpawned = true;
1373   ghostTimeLeft = -1;
1375   int vwdt = (viewMax.x-viewMin.x);
1376   int vhgt = (viewMax.y-viewMin.y);
1378   int gx, gy;
1380   if (player.ix < viewMin.x+vwdt/2) {
1381     // player is in the left side
1382     gx = viewMin.x+vwdt/2+vwdt/4;
1383   } else {
1384     // player is in the right side
1385     gx = viewMin.x+vwdt/4;
1386   }
1388   if (player.iy < viewMin.y+vhgt/2) {
1389     // player is in the left side
1390     gy = viewMin.y+vhgt/2+vhgt/4;
1391   } else {
1392     // player is in the right side
1393     gy = viewMin.y+vhgt/4;
1394   }
1396   writeln("spawning ghost at tile (", gx/16, ",", gy/16, "); player is at tile (", player.ix/16, ",", player.iy/16, ")");
1398   MakeMapObject(gx, gy, 'oGhost');
1400   /*
1401     if (oPlayer1.x &gt; room_width/2) instance_create(view_xview[0]+view_wview[0]+8, view_yview[0]+floor(view_hview[0] / 2), oGhost);
1402     else instance_create(view_xview[0]-32, view_yview[0]+floor(view_hview[0]/2), oGhost);
1403     global.ghostExists = true;
1404   */
1408 void thinkFrameGameGhost () {
1409   if (player.dead) return;
1410   if (!isNormalLevel()) return; // just in case
1412   if (ghostTimeLeft < 0) {
1413     // turned off
1414     if (musicFadeTimer > 0) {
1415       musicFadeTimer = -1;
1416       global.setMusicPitch(1.0);
1417     }
1418     return;
1419   }
1421   if (musicFadeTimer >= 0) {
1422     ++musicFadeTimer;
1423     if (musicFadeTimer > 0 && musicFadeTimer <= 100*3) {
1424       float pitch = 1.0-(0.00226/3.0)*musicFadeTimer;
1425       //writeln("MFT: ", musicFadeTimer, "; pitch=", pitch);
1426       global.setMusicPitch(pitch);
1427     }
1428   }
1430   if (ghostTimeLeft == 0) {
1431     // she is already here!
1432     return;
1433   }
1435   // no ghost if we have a crown
1436   if (global.hasCrown) {
1437     ghostTimeLeft = -1;
1438     return;
1439   }
1441   // if she was already spawned, don't do it again
1442   if (ghostSpawned) {
1443     ghostTimeLeft = 0;
1444     return;
1445   }
1447   if (--ghostTimeLeft != 0) {
1448     // warning
1449     if (global.config.ghostExtraTime > 0) {
1450       if (ghostTimeLeft == global.config.ghostExtraTime*30) {
1451         osdMessage("A CHILL RUNS UP YOUR SPINE...\nLET'S GET OUT OF HERE!", 6.66, urgent:true);
1452       }
1453       if (musicFadeTimer < 0 && ghostTimeLeft <= global.config.ghostExtraTime*30) {
1454         musicFadeTimer = 0;
1455       }
1456     }
1457     return;
1458   }
1460   // spawn her
1461   if (player.isExitingSprite) {
1462     // no reason to spawn her, we're leaving
1463     ghostTimeLeft = -1;
1464     return;
1465   }
1467   spawnGhost();
1471 void thinkFrameGame () {
1472   thinkFrameGameGhost();
1473   // udjat eye blinking
1474   if (global.hasUdjatEye && player) {
1475     foreach (MapTile t; allExits) {
1476       if (t isa MapTileBlackMarketDoor) {
1477         auto dm = int(player.distanceToEntity(t));
1478         if (dm < 4) dm = 4;
1479         if (udjatAlarm < 1 || dm < udjatAlarm) udjatAlarm = dm;
1480       }
1481     }
1482   } else {
1483     global.udjatBlink = false;
1484     udjatAlarm = 0;
1485   }
1486   if (udjatAlarm > 0) {
1487     if (--udjatAlarm == 0) {
1488       global.udjatBlink = !global.udjatBlink;
1489       if (global.hasUdjatEye && player) {
1490         player.playSound(global.udjatBlink ? 'sndBlink1' : 'sndBlink2');
1491       }
1492     }
1493   }
1494   switch (levelKind) {
1495     case LevelKind.Stars: thinkFrameGameStars(); break;
1496     case LevelKind.Sun: thinkFrameGameSun(); break;
1497     case LevelKind.Moon: thinkFrameGameMoon(); break;
1498     case LevelKind.Transition: thinkFrameTransition(); break;
1499     case LevelKind.Intro: thinkFrameIntro(); break;
1500   }
1504 // ////////////////////////////////////////////////////////////////////////// //
1505 private final bool isWaterTileCB (MapTile t) {
1506   return (t && t.visible && t.water);
1510 private final bool isLavaTileCB (MapTile t) {
1511   return (t && t.visible && t.lava);
1515 // ////////////////////////////////////////////////////////////////////////// //
1516 const int GreatLakeStartTileY = 28;
1519 final void fillGreatLake () {
1520   if (global.lake == 1) {
1521     foreach (int y; GreatLakeStartTileY..tilesHeight) {
1522       foreach (int x; 0..tilesWidth) {
1523         auto t = checkTileAtPoint(x*16, y*16, delegate bool (MapTile t) {
1524           if (t.spectral || !t.visible || t.invisible || t.moveable) return false;
1525           return true;
1526         });
1527         if (!t) {
1528           t = MakeMapTile(x, y, 'oWaterSwim');
1529           if (!t) continue;
1530         }
1531         if (t.water) {
1532           t.setSprite(y == GreatLakeStartTileY ? 'sWaterTop' : 'sWater');
1533         } else if (t.lava) {
1534           t.setSprite(y == GreatLakeStartTileY ? 'sLavaTop' : 'sLava');
1535         }
1536       }
1537     }
1538   }
1542 // called once after level generation
1543 final void fixLiquidTop () {
1544   if (global.lake == 1) fillGreatLake();
1546   liquidTileCount = 0;
1547   foreach (MapTile t; objGrid.allObjects(MapTile)) {
1548     if (!t.water && !t.lava) continue;
1550     ++liquidTileCount;
1551     //writeln("fixing water tile(", GetClassName(t.Class), "):'", t.objName, "' (water=", t.water, "; lava=", t.lava, "); lqc=", liquidTileCount);
1553     //if (global.lake == 1) continue; // it is done in `fillGreatLake()`
1555     if (!checkTileAtPoint(t.ix+8, t.iy-8, (t.lava ? &isLavaTileCB : &isWaterTileCB))) {
1556       t.setSprite(t.lava ? 'sLavaTop' : 'sWaterTop');
1557     } else {
1558       // don't do this, it will destroy seaweed
1559       //t.setSprite(t.lava ? 'sLava' : 'sWater');
1560       auto spr = t.getSprite();
1561            if (!spr) t.setSprite(t.lava ? 'sLava' : 'sWater');
1562       else if (spr.Name == 'sLavaTop') t.setSprite('sLava');
1563       else if (spr.Name == 'sWaterTop') t.setSprite('sWater');
1564     }
1565   }
1566   //writeln("liquid tiles count: ", liquidTileCount);
1570 // ////////////////////////////////////////////////////////////////////////// //
1571 transient MapTile curWaterTile;
1572 transient bool curWaterTileCheckHitsLava;
1573 transient bool curWaterTileCheckHitsSolidOrWater; // only for `checkWaterOrSolidTilePartialCB`
1574 transient int curWaterTileLastHDir;
1575 transient ubyte[16, 16] curWaterOccupied;
1576 transient int curWaterOccupiedCount;
1577 transient int curWaterTileCheckX0, curWaterTileCheckY0;
1580 private final void clearCurWaterCheckState () {
1581   curWaterTileCheckHitsLava = false;
1582   curWaterOccupiedCount = 0;
1583   foreach (auto idx; 0..16*16) curWaterOccupied[idx] = 0;
1587 private final bool checkWaterOrSolidTileCB (MapTile t) {
1588   if (t == curWaterTile) return false;
1589   if (t.lava && curWaterTile.water) {
1590     curWaterTileCheckHitsLava = true;
1591     return true;
1592   }
1593   if (t.ix%16 != 0 || t.iy%16 != 0) {
1594     if (t.water || t.solid) {
1595       // fill occupied array
1596       //FIXME: optimize this
1597       if (curWaterOccupiedCount < 16*16) {
1598         foreach (auto dy; t.y0..t.y1+1) {
1599           foreach (auto dx; t.x0..t.x1+1) {
1600             int sx = dx-curWaterTileCheckX0;
1601             int sy = dy-curWaterTileCheckY0;
1602             if (sx >= 0 && sx <= 16 && sy >= 0 && sy <= 15 && !curWaterOccupied[sx, sy]) {
1603               curWaterOccupied[sx, sy] = 1;
1604               ++curWaterOccupiedCount;
1605             }
1606           }
1607         }
1608       }
1609     }
1610     return false; // need to check for lava
1611   }
1612   if (t.water || t.solid || t.lava) {
1613     curWaterOccupiedCount = 16*16;
1614     if (t.water && curWaterTile.lava) t.instanceRemove();
1615   }
1616   return false; // need to check for lava
1620 private final bool checkWaterOrSolidTilePartialCB (MapTile t) {
1621   if (t == curWaterTile) return false;
1622   if (t.lava && curWaterTile.water) {
1623     //writeln("!!!!!!!!");
1624     curWaterTileCheckHitsLava = true;
1625     return true;
1626   }
1627   if (t.water || t.solid || t.lava) {
1628     //writeln("*********");
1629     curWaterTileCheckHitsSolidOrWater = true;
1630     if (t.water && curWaterTile.lava) t.instanceRemove();
1631   }
1632   return false; // need to check for lava
1636 private final bool isFullyOccupiedAtTilePos (int tileX, int tileY) {
1637   clearCurWaterCheckState();
1638   curWaterTileCheckX0 = tileX*16;
1639   curWaterTileCheckY0 = tileY*16;
1640   checkTilesInRect(tileX*16, tileY*16, 16, 16, &checkWaterOrSolidTileCB);
1641   return (curWaterTileCheckHitsLava || curWaterOccupiedCount == 16*16);
1645 private final bool isAtLeastPartiallyOccupiedAtTilePos (int tileX, int tileY) {
1646   curWaterTileCheckHitsLava = false;
1647   curWaterTileCheckHitsSolidOrWater = false;
1648   checkTilesInRect(tileX*16, tileY*16, 16, 16, &checkWaterOrSolidTilePartialCB);
1649   return (curWaterTileCheckHitsSolidOrWater || curWaterTileCheckHitsLava);
1653 private final bool waterCanReachGroundHoleInDir (MapTile wtile, int dx) {
1654   if (dx == 0) return false; // just in case
1655   dx = sign(dx);
1656   int x = wtile.ix/16, y = wtile.iy/16;
1657   x += dx;
1658   while (x >= 0 && x < tilesWidth) {
1659     if (!isAtLeastPartiallyOccupiedAtTilePos(x, y+1)) return true;
1660     if (isAtLeastPartiallyOccupiedAtTilePos(x, y)) return false;
1661     x += dx;
1662   }
1663   return false;
1667 // returns `true` if this tile must be removed
1668 private final bool checkWaterFlow (MapTile wtile) {
1669   if (global.lake == 1) {
1670     if (wtile.iy >= GreatLakeStartTileY*16) return false; // lake tile, don't touch
1671     if (wtile.iy >= GreatLakeStartTileY*16-16) return true; // remove it, so it won't stack on a lake
1672   }
1674   if (wtile.ix%16 != 0 || wtile.iy%16 != 0) return true; // sanity check
1676   curWaterTile = wtile;
1677   curWaterTileLastHDir = 0; // never moved to the side
1679   bool wasMoved = false;
1681   for (;;) {
1682     int tileX = wtile.ix/16, tileY = wtile.iy/16;
1684     // out of level?
1685     if (tileY >= tilesHeight) return true;
1687     // check if we can fall down
1688     auto canFall = !isAtLeastPartiallyOccupiedAtTilePos(tileX, tileY+1);
1689     // disappear if can fall in lava
1690     if (wtile.water && curWaterTileCheckHitsLava) {
1691       //!writeln(wtile.objId, ": LAVA HIT DOWN");
1692       return true;
1693     }
1694     if (wasMoved) {
1695       // fake, so caller will not start removing tiles
1696       if (canFall) wtile.waterMovedDown = true;
1697       break;
1698     }
1699     // can move down?
1700     if (canFall) {
1701       // move down
1702       //!writeln(wtile.objId, ": GOING DOWN");
1703       curWaterTileLastHDir = 0;
1704       wtile.iy = wtile.iy+16;
1705       wasMoved = true;
1706       wtile.waterMovedDown = true;
1707       continue;
1708     }
1710     bool canMoveLeft = (curWaterTileLastHDir > 0 ? false : !isAtLeastPartiallyOccupiedAtTilePos(tileX-1, tileY));
1711     // disappear if near lava
1712     if (wtile.water && curWaterTileCheckHitsLava) {
1713       //!writeln(wtile.objId, ": LAVA HIT LEFT");
1714       return true;
1715     }
1717     bool canMoveRight = (curWaterTileLastHDir < 0 ? false : !isAtLeastPartiallyOccupiedAtTilePos(tileX+1, tileY));
1718     // disappear if near lava
1719     if (wtile.water && curWaterTileCheckHitsLava) {
1720       //!writeln(wtile.objId, ": LAVA HIT RIGHT");
1721       return true;
1722     }
1724     if (!canMoveLeft && !canMoveRight) {
1725       // do final checks
1726       //!if (wasMove) writeln(wtile.objId, ": NO MORE MOVES");
1727       break;
1728     }
1730     if (canMoveLeft && canMoveRight) {
1731       // choose random direction
1732       //!writeln(wtile.objId, ": CHOOSING RANDOM HDIR");
1733       // actually, choose direction that leads to hole in a ground
1734       if (waterCanReachGroundHoleInDir(wtile, -1)) {
1735         // can reach hole at the left side
1736         if (waterCanReachGroundHoleInDir(wtile, 1)) {
1737           // can reach hole at the right side, choose at random
1738           if (global.randOther(0, 1)) canMoveRight = false; else canMoveLeft = false;
1739         } else {
1740           // move left
1741           canMoveRight = false;
1742         }
1743       } else {
1744         // can't reach hole at the left side
1745         if (waterCanReachGroundHoleInDir(wtile, 1)) {
1746           // can reach hole at the right side, choose at random
1747           canMoveLeft = false;
1748         } else {
1749           // no holes at any side, choose at random
1750           if (global.randOther(0, 1)) canMoveRight = false; else canMoveLeft = false;
1751         }
1752       }
1753     }
1755     // move
1756     if (canMoveLeft) {
1757       if (canMoveRight) FatalError("WATERCHECK: WTF RIGHT");
1758       //!writeln(wtile.objId, ": MOVING LEFT (", curWaterTileLastHDir, ")");
1759       curWaterTileLastHDir = -1;
1760       wtile.ix = wtile.ix-16;
1761     } else if (canMoveRight) {
1762       if (canMoveLeft) FatalError("WATERCHECK: WTF LEFT");
1763       //!writeln(wtile.objId, ": MOVING RIGHT (", curWaterTileLastHDir, ")");
1764       curWaterTileLastHDir = 1;
1765       wtile.ix = wtile.ix+16;
1766     }
1767     wasMoved = true;
1768   }
1770   // remove seaweeds
1771   if (wasMoved) {
1772     checkWater = true;
1773     wtile.setSprite(wtile.lava ? 'sLava' : 'sWater');
1774     wtile.waterMoved = true;
1775     // if this tile was not moved down, check if it can move down on any next step
1776     if (!wtile.waterMovedDown) {
1777            if (waterCanReachGroundHoleInDir(wtile, -1)) wtile.waterMovedDown = true;
1778       else if (waterCanReachGroundHoleInDir(wtile, 1)) wtile.waterMovedDown = true;
1779     }
1780   }
1782   return false; // don't remove
1784   //if (!isWetTileAtPix(tileX*16+8, tileY*16-8)) wtile.setSprite(wtile.lava ? 'sLavaTop' : 'sWaterTop');
1788 transient array!MapTile waterTilesList;
1790 final bool sortWaterTilesByCoordsLess (MapTile a, MapTile b) {
1791   int dy = a.iy-b.iy;
1792   if (dy) return (dy < 0);
1793   return (a.ix < b.ix);
1796 transient int waterFlowPause = 0;
1797 transient bool debugWaterFlowPause = false;
1799 final void cleanDeadObjects () {
1800   // remove dead objects
1801   if (deadItemsHead) {
1802     auto olddel = ImmediateDelete;
1803     ImmediateDelete = false;
1804     do {
1805       auto it = deadItemsHead;
1806       deadItemsHead = it.deadItemsNext;
1807       if (it.grid) it.grid.remove(it.gridId);
1808       it.onDestroy();
1809       delete it;
1810     } while (deadItemsHead);
1811     ImmediateDelete = olddel;
1812     if (olddel) CollectGarbage(true); // destroy delayed objects too
1813   }
1816 final void cleanDeadTiles () {
1817   if (checkWater && /*global.lake == 1 ||*/ (!blockWaterChecking && liquidTileCount)) {
1818     if (global.lake == 1) fillGreatLake();
1819     if (waterFlowPause > 1) {
1820       --waterFlowPause;
1821       cleanDeadObjects();
1822       return;
1823     }
1824     if (debugWaterFlowPause) waterFlowPause = 4;
1825     //writeln("checking water");
1826     waterTilesList.clear();
1827     foreach (MapTile wtile; objGrid.allObjectsSafe(MapTile)) {
1828       if (wtile.water || wtile.lava) {
1829         // sanity check
1830         if (wtile.ix%16 == 0 && wtile.iy%16 == 0) {
1831           wtile.waterMoved = false;
1832           wtile.waterMovedDown = false;
1833           wtile.waterSlideOldX = wtile.ix;
1834           wtile.waterSlideOldY = wtile.iy;
1835           waterTilesList[$] = wtile;
1836         }
1837       }
1838     }
1839     checkWater = false;
1840     liquidTileCount = 0;
1841     waterTilesList.sort(&sortWaterTilesByCoordsLess);
1842     // do water flow
1843     bool wasAnyMove = false;
1844     bool wasAnyMoveDown = false;
1845     foreach (MapTile wtile; waterTilesList) {
1846       if (!wtile || !wtile.isInstanceAlive) continue;
1847       auto killIt = checkWaterFlow(wtile);
1848       if (killIt) {
1849         checkWater = true;
1850         wtile.smashMe();
1851         wtile.instanceRemove(); // just in case
1852       } else {
1853         wtile.saveInterpData();
1854         wtile.updateGrid();
1855         wasAnyMove = wasAnyMove || wtile.waterMoved;
1856         wasAnyMoveDown = wasAnyMoveDown || wtile.waterMovedDown;
1857         if (wtile.waterMoved && debugWaterFlowPause) wtile.waterSlideCounter = 4;
1858       }
1859     }
1860     // do water check
1861     liquidTileCount = 0;
1862     foreach (MapTile wtile; waterTilesList) {
1863       if (!wtile || !wtile.isInstanceAlive) continue;
1864       if (wasAnyMoveDown) {
1865         ++liquidTileCount;
1866         continue;
1867       }
1868       //checkWater = checkWater || wtile.waterMoved;
1869       curWaterTile = wtile;
1870       int tileX = wtile.ix/16, tileY = wtile.iy/16;
1871       // check if we are have no way to leak
1872       bool killIt = false;
1873       if (!isFullyOccupiedAtTilePos(tileX-1, tileY) || (wtile.water && curWaterTileCheckHitsLava)) {
1874         //writeln(" LEFT DEATH (", curWaterOccupiedCount, " : ", curWaterTileCheckHitsLava, ")");
1875         killIt = true;
1876       }
1877       if (!killIt && (!isFullyOccupiedAtTilePos(tileX+1, tileY) || (wtile.water && curWaterTileCheckHitsLava))) {
1878         //writeln(" RIGHT DEATH (", curWaterOccupiedCount, " : ", curWaterTileCheckHitsLava, ")");
1879         killIt = true;
1880       }
1881       if (!killIt && (!isFullyOccupiedAtTilePos(tileX, tileY+1) || (wtile.water && curWaterTileCheckHitsLava))) {
1882         //writeln(" DOWN DEATH (", curWaterOccupiedCount, " : ", curWaterTileCheckHitsLava, ")");
1883         killIt = true;
1884       }
1885       //killIt = false;
1886       if (killIt) {
1887         checkWater = true;
1888         wtile.smashMe();
1889         wtile.instanceRemove(); // just in case
1890       } else {
1891         ++liquidTileCount;
1892       }
1893     }
1894     if (wasAnyMove) checkWater = true;
1895     //writeln("water check: liquidTileCount=", liquidTileCount, "; checkWater=", checkWater, "; wasAnyMove=", wasAnyMove, "; wasAnyMoveDown=", wasAnyMoveDown);
1897     // fill empty spaces in lake with water
1898     fixLiquidTop();
1899   }
1901   cleanDeadObjects();
1905 // ////////////////////////////////////////////////////////////////////////// //
1906 private transient array!MapEntity postponedThinkers;
1907 private transient MapEntity thinkerHeld;
1908 private transient array!MapEntity activeThinkerList;
1911 final void doThinkActionsForObject (MapEntity o) {
1912        if (o.justSpawned) o.justSpawned = false;
1913   else if (o.imageSpeed > 0) o.nextAnimFrame();
1914   o.saveInterpData();
1915   o.thinkFrame();
1916   if (o.isInstanceAlive) {
1917     //o.updateGrid();
1918     o.processAlarms();
1919     if (o.isInstanceAlive) {
1920       if (o.whipTimer > 0) --o.whipTimer;
1921       o.updateGrid();
1922       auto obj = MapObject(o);
1923       if (!o.canLiveOutsideOfLevel && (!obj || !obj.heldBy) && o.isOutsideOfLevel()) {
1924         // oops, fallen out of level...
1925         o.onOutOfLevel();
1926       }
1927     }
1928   }
1932 // return `true` if thinker should be removed
1933 final void thinkOne (MapEntity o, optional bool doHeldObject, optional bool dontAddHeldObject) {
1934   if (!o) return;
1935   if (o == thinkerHeld && !doHeldObject) return; // skip it
1937   if (!o.isInstanceAlive) return;
1939   auto obj = MapObject(o);
1941   if (obj) obj.prevhp = obj.hp; // so i don't have to do it in `thinkFrame()`
1942   if (!o.active) return;
1944   if (obj && obj.heldBy == player) {
1945     // fix held item coords
1946     obj.fixHoldCoords();
1947     if (doHeldObject) {
1948       doThinkActionsForObject(o);
1949     } else {
1950       if (!dontAddHeldObject) {
1951         bool found = false;
1952         foreach (MapEntity e; postponedThinkers) if (e == o) { found = true; break; }
1953         if (!found) postponedThinkers[$] = o;
1954       }
1955     }
1956     return;
1957   }
1959   bool doThink = true;
1961   // collision with player weapon
1962   auto hh = PlayerWeapon(player.holdItem);
1963   bool doWeaponAction = false;
1964   if (hh) {
1965     if (hh.blockedBySolids && !global.config.killEnemiesThruWalls) {
1966       int xx = player.ix+(player.dir == MapObject::Dir.Left ? -16 : 16);
1967       //doWeaponAction = !isSolidAtPoint(xx, player.iy);
1968       doWeaponAction = !isSolidAtPoint(xx, hh.yCenter);
1969       /*
1970       int dh = max(1, hh.height-2);
1971       doWeaponAction = !checkTilesInRect(player.ix, player.iy);
1972       */
1973     } else {
1974       doWeaponAction = true;
1975     }
1976   }
1978   if (obj && doWeaponAction && hh && (o.whipTimer <= 0 || hh.ignoreWhipTimer) && hh.collidesWithObject(obj)) {
1979     //writeln("WEAPONED!");
1980     bool dontChangeWhipTimer = hh.dontChangeWhipTimer;
1981     if (!o.onTouchedByPlayerWeapon(player, hh)) {
1982       if (o.isInstanceAlive) hh.onCollisionWithObject(obj);
1983     }
1984     if (!dontChangeWhipTimer) o.whipTimer = o.whipTimerValue; //HACK
1985     doThink = o.isInstanceAlive;
1986   }
1988   if (doThink && o.isInstanceAlive) {
1989     doThinkActionsForObject(o);
1990     doThink = o.isInstanceAlive;
1991   }
1993   // collision with player
1994   if (doThink && obj && o.collidesWith(player)) {
1995     if (!player.onObjectTouched(obj) && o.isInstanceAlive) {
1996       doThink = !o.onTouchedByPlayer(player);
1997       o.updateGrid();
1998     }
1999   }
2003 final void processThinkers (float timeDelta) {
2004   if (timeDelta <= 0) return;
2005   if (gamePaused) {
2006     ++pausedTime;
2007     if (onBeforeFrame) onBeforeFrame(false);
2008     if (onAfterFrame) onAfterFrame(false);
2009     keysNextFrame();
2010     return;
2011   } else {
2012     pausedTime = 0;
2013   }
2014   accumTime += timeDelta;
2015   bool wasFrame = false;
2016   // block GC
2017   auto olddel = ImmediateDelete;
2018   ImmediateDelete = false;
2019   while (accumTime >= FrameTime) {
2020     postponedThinkers.clear();
2021     thinkerHeld = none;
2022     accumTime -= FrameTime;
2023     if (onBeforeFrame) onBeforeFrame(accumTime >= FrameTime);
2024     // shake
2025     if (shakeLeft > 0) {
2026       --shakeLeft;
2027       if (!shakeDir.x) shakeDir.x = (global.randOther(0, 1) ? -3 : 3);
2028       if (!shakeDir.y) shakeDir.y = (global.randOther(0, 1) ? -3 : 3);
2029       shakeOfs.x = shakeDir.x;
2030       shakeOfs.y = shakeDir.y;
2031       int sgnc = global.randOther(1, 3);
2032       if (sgnc&0x01) shakeDir.x = -shakeDir.x;
2033       if (sgnc&0x02) shakeDir.y = -shakeDir.y;
2034     } else {
2035       shakeOfs.x = 0;
2036       shakeOfs.y = 0;
2037       shakeDir.x = 0;
2038       shakeDir.y = 0;
2039     }
2040     // advance time
2041     time += 1;
2042     // we don't want the time to grow too large
2043     if (time < 0) { time = 0; lastRenderTime = -1; }
2044     // game-global events
2045     thinkFrameGame();
2046     // frame thinkers: player
2047     if (player && !disablePlayerThink) {
2048       // time limit
2049       if (!player.dead && isNormalLevel() &&
2050           (maxPlayingTime < 0 ||
2051            (maxPlayingTime > 0 && maxPlayingTime*30 <= time &&
2052             time%30 == 0 && global.randOther(1, 100) <= 20)))
2053       {
2054         MakeMapObject(player.ix, player.iy, 'oExplosion');
2055       }
2056       //HACK: check for stolen items
2057       auto item = MapItem(player.holdItem);
2058       if (item) item.onCheckItemStolen(player);
2059       item = MapItem(player.pickedItem);
2060       if (item) item.onCheckItemStolen(player);
2061       // normal thinking
2062       doThinkActionsForObject(player);
2063     }
2064     // frame thinkers: held object
2065     thinkerHeld = player.holdItem;
2066     if (thinkerHeld && thinkerHeld.isInstanceAlive) {
2067       thinkOne(thinkerHeld, doHeldObject:true);
2068       if (!thinkerHeld.isInstanceAlive) {
2069         if (player.holdItem == thinkerHeld) player.holdItem = none;
2070         thinkerHeld.grid.remove(thinkerHeld.gridId);
2071         /* later
2072         thinkerHeld.onDestroy();
2073         delete thinkerHeld;
2074         */
2075       }
2076     }
2077     // frame thinkers: objects
2078     activeThinkerList.clear();
2079     auto grid = objGrid;
2080     // collect active objects
2081     if (global.config.useFrozenRegion) {
2082       foreach (MapEntity e; grid.inRectPix(viewStart.x/global.scale-64, viewStart.y/global.scale-64, 320+64*2, 240+64*2, precise:false)) {
2083         if (e.active) activeThinkerList[$] = e;
2084       }
2085     } else {
2086       // no frozen area
2087       foreach (MapEntity e; grid.allObjects()) {
2088         if (e.active) activeThinkerList[$] = e;
2089       }
2090     }
2091     // process active objects
2092     //writeln("thinkers: ", activeThinkerList.length);
2093     foreach (MapEntity o; activeThinkerList) {
2094       if (!o) continue;
2095       thinkOne(o, doHeldObject:false);
2096       if (!o.isInstanceAlive) {
2097         //writeln("dead thinker: '", o.objType, "'");
2098         if (o.grid) o.grid.remove(o.gridId);
2099         auto obj = MapObject(o);
2100         if (obj && obj.heldBy) obj.heldBy.holdItem = none;
2101         /* later
2102         o.onDestroy();
2103         delete o;
2104         */
2105       }
2106     }
2107     // postponed thinkers
2108     foreach (MapEntity o; postponedThinkers) {
2109       if (!o) continue;
2110       thinkOne(o, doHeldObject:true, dontAddHeldObject:true);
2111       if (!o.isInstanceAlive) {
2112         //writeln("dead pp-thinker: '", o.objType, "'");
2113         /* later
2114         o.onDestroy();
2115         delete o;
2116         */
2117       }
2118     }
2119     postponedThinkers.clear();
2120     thinkerHeld = none;
2121     // clean dead things
2122     cleanDeadTiles();
2123     // fix held item coords
2124     if (player && player.holdItem) {
2125       if (player.holdItem.isInstanceAlive) {
2126         player.holdItem.fixHoldCoords();
2127       } else {
2128         player.holdItem = none;
2129       }
2130     }
2131     // money counter
2132     if (collectCounter == 0) {
2133       xmoney = max(0, xmoney-100);
2134     } else {
2135       --collectCounter;
2136     }
2137     // other things
2138     if (player) {
2139       if (!player.dead) stats.oneMoreFramePlayed();
2140       SoundSystem.ListenerOrigin = vector(player.xCenter, player.yCenter, -1);
2141       //writeln("plrpos=(", player.xCenter, ",", player.yCenter, "); lo=", SoundSystem.ListenerOrigin);
2142     }
2143     if (onAfterFrame) onAfterFrame(accumTime >= FrameTime);
2144     ++framesProcessedFromLastClear;
2145     keysNextFrame();
2146     wasFrame = true;
2147     if (!player.visible && player.holdItem) player.holdItem.visible = false;
2148     if (winCutsceneSwitchToNext) {
2149       winCutsceneSwitchToNext = false;
2150       switch (++inWinCutscene) {
2151         case 2: startWinCutsceneVolcano(); break;
2152         case 3: default: startWinCutsceneWinFall(); break;
2153       }
2154       break;
2155     }
2156     if (playerExited) break;
2157   }
2158   ImmediateDelete = olddel;
2159   if (playerExited) {
2160     playerExited = false;
2161     onLevelExited();
2162     centerViewAtPlayer();
2163   }
2164   if (wasFrame) {
2165     // if we were processed at least one frame, collect garbage
2166     //keysNextFrame();
2167     CollectGarbage(true); // destroy delayed objects too
2168   }
2169   if (accumTime > 0 && onInterFrame) onInterFrame(accumTime/FrameTime);
2173 // ////////////////////////////////////////////////////////////////////////// //
2174 final void tileXY2roomXY (int tileX, int tileY, out int roomX, out int roomY) {
2175   roomX = (tileX-1)/RoomGen::Width;
2176   roomY = (tileY-1)/RoomGen::Height;
2180 final bool isInShop (int tileX, int tileY) {
2181   if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
2182     auto n = roomType[tileX, tileY];
2183     if (n == 4 || n == 5) return true;
2184     return !!checkTilesInRect(tileX*16, tileY*16, 16, 16, delegate bool (MapTile t) { return t.shopWall; });
2185     //k8: we don't have this
2186     //if (t && t.objType == 'oShop') return true;
2187   }
2188   return false;
2192 // ////////////////////////////////////////////////////////////////////////// //
2193 override void Destroy () {
2194   clearWholeLevel();
2195   delete tempSolidTile;
2196   ::Destroy();
2200 // ////////////////////////////////////////////////////////////////////////// //
2201 // WARNING! delegate should not create/delete objects!
2202 final MapObject findNearestObject (int px, int py, scope bool delegate (MapObject o) dg, optional class!MapObject castClass) {
2203   MapObject res = none;
2204   if (!castClass) castClass = MapObject;
2205   int curdistsq = int.max;
2206   foreach (MapObject o; objGrid.allObjects(MapObject)) {
2207     if (o.spectral) continue;
2208     if (!dg(o)) continue;
2209     int xc = px-o.xCenter, yc = py-o.yCenter;
2210     int distsq = xc*xc+yc*yc;
2211     if (distsq < curdistsq) {
2212       res = o;
2213       curdistsq = distsq;
2214     }
2215   }
2216   return res;
2220 // WARNING! delegate should not create/delete objects!
2221 final MapObject findNearestEnemy (int px, int py, optional scope bool delegate (MapEnemy o) dg, optional class!MapEnemy castClass) {
2222   if (!castClass) castClass = MapEnemy;
2223   if (castClass !isa MapEnemy) return none;
2224   MapObject res = none;
2225   int curdistsq = int.max;
2226   foreach (MapEnemy o; objGrid.allObjects(castClass)) {
2227     //k8: i added `dead` check
2228     if (o.spectral || o.dead) continue;
2229     if (dg) {
2230       if (!dg(o)) continue;
2231     }
2232     int xc = px-o.xCenter, yc = py-o.yCenter;
2233     int distsq = xc*xc+yc*yc;
2234     if (distsq < curdistsq) {
2235       res = o;
2236       curdistsq = distsq;
2237     }
2238   }
2239   return res;
2243 final MonsterShopkeeper findNearestCalmShopkeeper (int px, int py) {
2244   auto obj = MonsterShopkeeper(findNearestEnemy(px, py, delegate bool (MapEnemy o) {
2245     auto sk = MonsterShopkeeper(o);
2246     if (sk && !sk.angered) return true;
2247     return false;
2248   }, castClass:MonsterShopkeeper));
2249   return obj;
2253 final MonsterShopkeeper hasAliveShopkeepers (optional bool skipAngry) {
2254   foreach (MonsterShopkeeper sc; objGrid.allObjects(MonsterShopkeeper)) {
2255     if (sc.spectral || sc.dead) continue;
2256     if (skipAngry && (sc.angered || sc.outlaw)) continue;
2257     return sc;
2258   }
2259   return none;
2263 // WARNING! delegate should not create/delete objects!
2264 final int calcNearestEnemyDist (int px, int py, optional scope bool delegate (MapEnemy o) dg, optional class!MapEnemy castClass) {
2265   auto e = findNearestEnemy(px, py, dg!optional, castClass!optional);
2266   if (!e) return int.max;
2267   int xc = px-e.xCenter, yc = py-e.yCenter;
2268   return round(sqrt(xc*xc+yc*yc));
2272 // WARNING! delegate should not create/delete objects!
2273 final int calcNearestObjectDist (int px, int py, optional scope bool delegate (MapObject o) dg, optional class!MapObject castClass) {
2274   auto e = findNearestObject(px, py, dg!optional, castClass!optional);
2275   if (!e) return int.max;
2276   int xc = px-e.xCenter, yc = py-e.yCenter;
2277   return round(sqrt(xc*xc+yc*yc));
2281 // WARNING! delegate should not create/delete objects!
2282 final MapTile findNearestMoveableSolid (int px, int py, optional scope bool delegate (MapTile t) dg) {
2283   MapTile res = none;
2284   int curdistsq = int.max;
2285   foreach (MapTile t; objGrid.allObjects(MapTile)) {
2286     if (t.spectral) continue;
2287     if (dg) {
2288       if (!dg(t)) continue;
2289     } else {
2290       if (!t.solid || !t.moveable) continue;
2291     }
2292     int xc = px-t.xCenter, yc = py-t.yCenter;
2293     int distsq = xc*xc+yc*yc;
2294     if (distsq < curdistsq) {
2295       res = t;
2296       curdistsq = distsq;
2297     }
2298   }
2299   return res;
2303 // WARNING! delegate should not create/delete objects!
2304 final MapTile findNearestTile (int px, int py, optional scope bool delegate (MapTile t) dg) {
2305   if (!dg) return none;
2306   MapTile res = none;
2307   int curdistsq = int.max;
2309   //FIXME: make this faster!
2310   foreach (MapTile t; objGrid.allObjects(MapTile)) {
2311     if (t.spectral) continue;
2312     int xc = px-t.xCenter, yc = py-t.yCenter;
2313     int distsq = xc*xc+yc*yc;
2314     if (distsq < curdistsq && dg(t)) {
2315       res = t;
2316       curdistsq = distsq;
2317     }
2318   }
2320   return res;
2324 // ////////////////////////////////////////////////////////////////////////// //
2325 final bool cbIsObjectWeb (MapObject o) { return (o isa ItemWeb); }
2326 final bool cbIsObjectBlob (MapObject o) { return (o isa EnemyBlob); }
2327 final bool cbIsObjectArrow (MapObject o) { return (o isa ItemProjectileArrow); }
2328 final bool cbIsObjectEnemy (MapObject o) { return (/*o != self &&*/ o isa MapEnemy); }
2330 final bool cbIsObjectTreasure (MapObject o) { return (o.isTreasure); }
2332 final bool cbIsItemObject (MapObject o) { return (o isa MapItem); }
2334 final bool cbIsItemOrEnemyObject (MapObject o) { return (o isa MapItem || o isa MapEnemy); }
2337 final MapObject isObjectAtTile (int tileX, int tileY, optional scope bool delegate (MapObject o) dg, optional bool precise) {
2338   if (!specified_precise) precise = true;
2339   tileX *= 16;
2340   tileY *= 16;
2341   foreach (MapObject o; objGrid.inRectPix(tileX, tileY, 16, 16, precise:precise, castClass:MapObject)) {
2342     if (o.spectral) continue;
2343     if (dg) {
2344       if (dg(o)) return o;
2345     } else {
2346       return o;
2347     }
2348   }
2349   return none;
2353 final MapObject isObjectAtTilePix (int x, int y, optional scope bool delegate (MapObject o) dg) {
2354   return isObjectAtTile(x/16, y/16, dg!optional);
2358 final MapObject isObjectAtPoint (int xpos, int ypos, optional scope bool delegate (MapObject o) dg, optional bool precise, optional class!MapObject castClass) {
2359   if (!specified_precise) precise = true;
2360   if (!castClass) castClass = MapObject;
2361   foreach (MapObject o; objGrid.inCellPix(xpos, ypos, precise:precise, castClass:castClass)) {
2362     if (o.spectral) continue;
2363     if (dg) {
2364       if (dg(o)) return o;
2365     } else {
2366       if (o isa MapEnemy) return o;
2367     }
2368   }
2369   return none;
2373 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) {
2374   if (w < 1 || h < 1) return none;
2375   if (!castClass) castClass = MapObject;
2376   if (w == 1 && h == 1) return isObjectAtPoint(xpos, ypos, dg!optional, precise!optional, castClass);
2377   if (!specified_precise) precise = true;
2378   foreach (MapObject o; objGrid.inRectPix(xpos, ypos, w, h, precise:precise, castClass:castClass)) {
2379     if (o.spectral) continue;
2380     if (dg) {
2381       if (dg(o)) return o;
2382     } else {
2383       if (o isa MapEnemy) return o;
2384     }
2385   }
2386   return none;
2390 final MapObject forEachObject (scope bool delegate (MapObject o) dg, optional bool allowSpectrals, optional class!MapObject castClass) {
2391   if (!dg) return none;
2392   if (!castClass) castClass = MapObject;
2393   foreach (MapObject o; objGrid.allObjectsSafe(castClass)) {
2394     if (!allowSpectrals && o.spectral) continue;
2395     if (dg(o)) return o;
2396   }
2397   return none;
2401 final MapObject forEachObjectAtPoint (int xpos, int ypos, scope bool delegate (MapObject o) dg, optional bool precise) {
2402   if (!dg) return none;
2403   if (!specified_precise) precise = true;
2404   foreach (MapObject o; objGrid.inCellPix(xpos, ypos, precise:precise, castClass:MapObject)) {
2405     if (o.spectral) continue;
2406     if (dg(o)) return o;
2407   }
2408   return none;
2412 final MapObject forEachObjectInRect (int xpos, int ypos, int w, int h, scope bool delegate (MapObject o) dg, optional bool precise) {
2413   if (!dg || w < 1 || h < 1) return none;
2414   if (w == 1 && h == 1) return forEachObjectAtPoint(xpos, ypos, dg!optional, precise!optional);
2415   if (!specified_precise) precise = true;
2416   foreach (MapObject o; objGrid.inRectPix(xpos, ypos, w, h, precise:precise, castClass:MapObject)) {
2417     if (o.spectral) continue;
2418     if (dg(o)) return o;
2419   }
2420   return none;
2424 final MapEntity forEachEntityInRect (int xpos, int ypos, int w, int h, scope bool delegate (MapEntity o) dg, optional bool precise, optional class!MapEntity castClass) {
2425   if (!dg || w < 1 || h < 1) return none;
2426   if (!castClass) castClass = MapEntity;
2427   if (!specified_precise) precise = true;
2428   foreach (MapEntity e; objGrid.inRectPix(xpos, ypos, w, h, precise:precise, castClass:castClass)) {
2429     if (e.spectral) continue;
2430     if (dg(e)) return e;
2431   }
2432   return none;
2436 private final bool cbIsRopeTile (MapTile t) { return (t isa MapTileRope); }
2438 final MapTile isRopeAtPoint (int px, int py) {
2439   return checkTileAtPoint(px, py, &cbIsRopeTile);
2443 //FIXME!
2444 final MapTile isWaterSwimAtPoint (int px, int py) {
2445   return isWaterAtPoint(px, py);
2449 // ////////////////////////////////////////////////////////////////////////// //
2450 private array!MapEntity tmpEntityList;
2452 private final bool cbCollectEntitiesWithMask (MapEntity t) {
2453   if (!t.visible || t.spectral) return false;
2454   tmpEntityList[$] = t;
2455   return false;
2459 final void touchEntitiesWithMask (int x, int y, SpriteFrame frm, scope bool delegate (MapEntity t) dg, optional class!MapEntity castClass) {
2460   if (!frm || frm.bw < 1 || frm.bh < 1 || !dg) return;
2461   if (frm.isEmptyPixelMask) return;
2462   if (!castClass) castClass = MapEntity;
2463   // collect tiles
2464   if (tmpEntityList.length) tmpEntityList.clear();
2465   if (player isa castClass && player.isRectCollisionFrame(frm, x, y)) tmpEntityList[$] = player;
2466   forEachEntityInRect(x+frm.bx, y+frm.by, frm.bw, frm.bh, &cbCollectEntitiesWithMask, castClass:castClass);
2467   foreach (MapEntity e; tmpEntityList) {
2468     if (!e || !e.isInstanceAlive || !e.visible || e.spectral) continue;
2469     if (e.isRectCollisionFrame(frm, x, y)) {
2470       if (dg(e)) break;
2471     }
2472   }
2476 // ////////////////////////////////////////////////////////////////////////// //
2477 final bool cbCollisionAnySolid (MapTile t) { return t.solid; }
2478 final bool cbCollisionAnyMoveableSolid (MapTile t) { return t.solid && t.moveable; }
2479 final bool cbCollisionLiquid (MapTile t) { return (t.water || t.lava); }
2480 final bool cbCollisionAnySolidOrLiquid (MapTile t) { return (t.solid || t.water || t.lava); }
2481 final bool cbCollisionLadder (MapTile t) { return t.ladder; }
2482 final bool cbCollisionLadderTop (MapTile t) { return t.laddertop; }
2483 final bool cbCollisionAnyLadder (MapTile t) { return (t.ladder || t.laddertop); }
2484 final bool cbCollisionAnySolidIce (MapTile t) { return (t.solid && t.ice); }
2485 final bool cbCollisionWater (MapTile t) { return t.water; }
2486 final bool cbCollisionLava (MapTile t) { return t.lava; }
2487 final bool cbCollisionAnyTree (MapTile t) { return t.tree; }
2488 final bool cbCollisionPlatform (MapTile t) { return t.platform; }
2489 final bool cbCollisionLadderOrPlatform (MapTile t) { return (t.ladder || t.platform); }
2490 //final bool cbCollisionAnyHangeable (MapTile t) { return t.hangeable; } // no need to check for solids here, it is done in MapTile initializer
2491 final bool cbCollisionSpringTrap (MapTile t) { return t.springtrap; }
2492 final bool cbCollisionSpikes (MapTile t) { return t.spikes; }
2493 final bool cbCollisionExitTile (MapTile t) { return t.isExitActive(); }
2495 final bool cbCollisionForWhoa (MapTile t) { return (t.solid || t.laddertop); }
2497 //final bool cbCollisionSacAltar (MapTile t) { return (t.objName == 'oSacAltarLeft'); } // only left part, yeah
2498 final bool cbCollisionSacAltar (MapTile t) { return t.sacrificingAltar; }
2501 // ////////////////////////////////////////////////////////////////////////// //
2502 transient MapTileTemp tempSolidTile;
2504 private final MapTileTemp makeWalkeableSolidTile (MapObject o) {
2505   if (!tempSolidTile) {
2506     tempSolidTile = SpawnObject(MapTileTemp);
2507   } else if (!tempSolidTile.isInstanceAlive) {
2508     delete tempSolidTile;
2509     tempSolidTile = SpawnObject(MapTileTemp);
2510   }
2511   // setup data
2512   tempSolidTile.level = self;
2513   tempSolidTile.global = global;
2514   tempSolidTile.solid = true;
2515   tempSolidTile.objName = MapTileTemp.default.objName;
2516   tempSolidTile.objType = MapTileTemp.default.objType;
2517   tempSolidTile.e = o;
2518   tempSolidTile.fltx = o.fltx;
2519   tempSolidTile.flty = o.flty;
2520   return tempSolidTile;
2524 final MapTile checkTilesInRect (int x0, int y0, const int w, const int h,
2525                                 optional scope bool delegate (MapTile dg) dg, optional bool precise,
2526                                 optional class!MapTile castClass)
2528   if (w < 1 || h < 1) return none;
2529   if (w == 1 && h == 1) return checkTileAtPoint(x0, y0, dg!optional);
2530   int x1 = x0+w-1, y1 = y0+h-1;
2531   if (x0 >= tilesWidth*16 || y0 >= tilesHeight*16 || x1 < 0 || y1 < 0) return none;
2532   if (!specified_precise) precise = true;
2533   if (!castClass) castClass = MapTile;
2534   if (!dg) dg = &cbCollisionAnySolid;
2536   // check walkable solid objects too
2537   foreach (MapEntity e; objGrid.inRectPix(x0, y0, w, h, precise:precise, castClass:castClass)) {
2538     if (e.spectral || !e.visible) continue;
2539     auto t = MapTile(e);
2540     if (t) {
2541       if (dg(t)) return t;
2542       continue;
2543     }
2544     auto o = MapObject(e);
2545     if (o && o.walkableSolid) {
2546       t = makeWalkeableSolidTile(o);
2547       if (dg(t)) return t;
2548       continue;
2549     }
2550   }
2552   return none;
2556 final MapTile checkTileAtPoint (int x0, int y0, optional scope bool delegate (MapTile dg) dg, optional bool precise, optional class!MapTile castClass) {
2557   if (x0 < 0 || y0 < 0 || x0 >= tilesWidth*16 || y0 >= tilesHeight*16) return none;
2558   if (!specified_precise) precise = true;
2559   if (!castClass) castClass = MapTile;
2560   if (!dg) dg = &cbCollisionAnySolid;
2562   // check walkable solid objects
2563   foreach (MapEntity e; objGrid.inCellPix(x0, y0, precise:precise, castClass:castClass)) {
2564     if (e.spectral || !e.visible) continue;
2565     auto t = MapTile(e);
2566     if (t) {
2567       if (dg(t)) return t;
2568       continue;
2569     }
2570     auto o = MapObject(e);
2571     if (o && o.walkableSolid) {
2572       t = makeWalkeableSolidTile(o);
2573       if (dg(t)) return t;
2574       continue;
2575     }
2576   }
2578   return none;
2582 // ////////////////////////////////////////////////////////////////////////// //
2583 final MapTile isSolidAtPoint (int px, int py) { return checkTileAtPoint(px, py); }
2584 final MapTile isSolidInRect (int rx, int ry, int rw, int rh) { return checkTilesInRect(rx, ry, rw, rh); }
2585 final MapTile isLadderAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadder); }
2586 final MapTile isLadderOrPlatformAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadderOrPlatform); }
2587 final MapTile isLadderTopAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadderTop); }
2588 final MapTile isAnyLadderAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyLadder); }
2589 final MapTile isWaterAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionWater); }
2590 final MapTile isLavaAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLava); }
2591 final MapTile isLiquidAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLiquid); }
2592 final MapTile isIceAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnySolidIce); }
2593 final MapTile isTreeAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyTree); }
2594 //final MapTile isHangeableAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyHangeable); }
2597 // ////////////////////////////////////////////////////////////////////////// //
2598 final void setTileTypeAt (int tileX, int tileY, LevelGen::RType rt) {
2599   if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) roomType[tileX, tileY] = rt;
2603 //FIXME: make this faster
2604 transient float gtagX, gtagY;
2606 // only non-moveables and non-specials
2607 final MapTile getTileAtGrid (int tileX, int tileY) {
2608   gtagX = tileX*16;
2609   gtagY = tileY*16;
2610   return checkTileAtPoint(tileX*16, tileY*16, delegate bool (MapTile t) {
2611     if (t.spectral || t.moveable || t.toSpecialGrid || !t.visible) return false;
2612     if (t.fltx != gtagX || t.flty != gtagY) return false; // emulate grid
2613     if (t.width != 16 || t.height != 16) return false;
2614     return true;
2615   }, precise:false);
2616   //return (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight ? tiles[tileX, tileY] : none);
2620 final MapTile getTileAtGridAny (int tileX, int tileY) {
2621   gtagX = tileX*16;
2622   gtagY = tileY*16;
2623   return checkTileAtPoint(tileX*16, tileY*16, delegate bool (MapTile t) {
2624     if (t.spectral /*|| t.moveable*/ || !t.visible) return false;
2625     if (t.fltx != gtagX || t.flty != gtagY) return false; // emulate grid
2626     if (t.width != 16 || t.height != 16) return false;
2627     return true;
2628   }, precise:false);
2629   //return (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight ? tiles[tileX, tileY] : none);
2633 final bool isNamedTileAt (int tileX, int tileY, name atypename) {
2634   if (!atypename) return false;
2635   auto t = getTileAtGridAny(tileX, tileY);
2636   return (t && t.objName == atypename);
2640 final void setTileAtGrid (int tileX, int tileY, MapTile tile) {
2641   if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
2642     if (tile) {
2643       tile.fltx = tileX*16;
2644       tile.flty = tileY*16;
2645       if (!tile.dontReplaceOthers) {
2646         auto osp = tile.spectral;
2647         tile.spectral = true;
2648         auto t = getTileAtGridAny(tileX, tileY);
2649         tile.spectral = osp;
2650         if (t && !t.immuneToReplacement) {
2651           writeln("REPLACING tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
2652           writeln("      NEW tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (tile.toSpecialGrid ? "special " : ""), "grid, pos=(", tile.fltx/16.0, ",", tile.flty/16.0, ")");
2653           t.instanceRemove();
2654         }
2655       }
2656       insertObject(tile);
2657     } else {
2658       auto t = getTileAtGridAny(tileX, tileY);
2659       if (t && !t.immuneToReplacement) {
2660         writeln("REMOVING tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
2661         t.instanceRemove();
2662       }
2663     }
2664   }
2668 // ////////////////////////////////////////////////////////////////////////// //
2669 // return `true` from delegate to stop
2670 MapTile forEachSolidTileOnGrid (scope bool delegate (int tileX, int tileY, MapTile t) dg) {
2671   if (!dg) return none;
2672   foreach (MapTile t; objGrid.allObjectsSafe(MapTile)) {
2673     if (t.spectral || !t.solid || !t.visible) continue;
2674     if (t.ix%16 != 0 || t.iy%16 != 0) continue; // emulate grid
2675     if (t.width != 16 || t.height != 16) continue;
2676     if (dg(t.ix/16, t.iy/16, t)) return t;
2677   }
2678   return none;
2682 // ////////////////////////////////////////////////////////////////////////// //
2683 // return `true` from delegate to stop
2684 MapTile forEachTile (scope bool delegate (MapTile t) dg, optional class!MapTile castClass) {
2685   if (!dg) return none;
2686   if (!castClass) castClass = MapTile;
2687   foreach (MapTile t; objGrid.allObjectsSafe(castClass)) {
2688     if (t.spectral || !t.visible) continue;
2689     if (dg(t)) return t;
2690   }
2691   return none;
2695 // ////////////////////////////////////////////////////////////////////////// //
2696 final void fixWallTiles () {
2697   foreach (MapTile t; objGrid.allObjectsSafe(MapTile)) t.beautifyTile();
2701 // ////////////////////////////////////////////////////////////////////////// //
2702 final MapTile isCollisionAtPoint (int px, int py, optional scope bool delegate (MapTile dg) dg) {
2703   if (!dg) dg = &cbCollisionAnySolid;
2704   return checkTilesInRect(px, py, 1, 1, dg);
2708 // ////////////////////////////////////////////////////////////////////////// //
2709 string scrGetKaliGift (MapTile altar, optional name gift) {
2710   string res;
2712   // find other side of the altar
2713   int sx = player.ix, sy = player.iy;
2714   if (altar) {
2715     sx = altar.ix;
2716     sy = altar.iy;
2717     auto a2 = isCollisionAtPoint(altar.ix+18, altar.iy+8, &cbCollisionSacAltar);
2718     if (!a2) a2 = isCollisionAtPoint(altar.ix-8, altar.iy+8, &cbCollisionSacAltar);
2719     if (a2) { sx = a2.ix; sy = a2.iy; }
2720   }
2722        if (global.favor <= -8) res = "SHE SEEMS VERY ANGRY WITH YOU!";
2723   else if (global.favor < 0) res = "SHE SEEMS ANGRY WITH YOU.";
2724   else if (global.favor == 0) res = "SHE HAS FORGIVEN YOU!";
2725   else if (global.favor >= 32) {
2726     if (global.kaliGift >= 3 && global.favor >= 32+(global.kaliGift-2)*16) {
2727       res = "YOU FEEL INVIGORATED!";
2728       global.kaliGift += 1;
2729       global.plife += global.randOther(4, 8);
2730     } else if (global.kaliGift >= 3) {
2731       res = "SHE SEEMS ECSTATIC WITH YOU!";
2732     } else if (global.bombs < 80) {
2733       res = "YOUR SATCHEL FEELS VERY FULL NOW!";
2734       global.kaliGift = 3;
2735       global.bombs = 99;
2736     } else {
2737       res = "YOU FEEL INVIGORATED!";
2738       global.kaliGift += 1;
2739       global.plife += global.randOther(4, 8);
2740     }
2741   } else if (global.favor >= 16) {
2742     if (global.kaliGift >= 2) {
2743       res = "SHE SEEMS VERY HAPPY WITH YOU!";
2744     } else {
2745       res = "SHE BESTOWS A GIFT UPON YOU!";
2746       global.kaliGift = 2;
2747       // poofs
2748       auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2749       obj.xVel = -1;
2750       obj.yVel = 0;
2751       obj = MakeMapObject(sx, sy-8, 'oPoof');
2752       obj.xVel = 1;
2753       obj.yVel = 0;
2754       // a gift
2755       obj = none;
2756       if (gift) obj = MakeMapObject(sx, sy-8, gift);
2757       if (!obj) obj = MakeMapObject(sx, sy-8, 'oKapala');
2758     }
2759   } else if (global.favor >= 8) {
2760     if (global.kaliGift >= 1) {
2761       res = "SHE SEEMS HAPPY WITH YOU.";
2762     } else {
2763       res = "SHE BESTOWS A GIFT UPON YOU!";
2764       global.kaliGift = 1;
2765       //rAltar = instance_nearest(x, y, oSacAltarRight);
2766       //if (instance_exists(rAltar)) {
2767       // poofs
2768       auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2769       obj.xVel = -1;
2770       obj.yVel = 0;
2771       obj = MakeMapObject(sx, sy-8, 'oPoof');
2772       obj.xVel = 1;
2773       obj.yVel = 0;
2774       obj = none;
2775       if (gift) obj = MakeMapObject(sx, sy-8, gift);
2776       if (!obj) {
2777         auto n = global.randOther(1, 8);
2778         auto m = n;
2779         for (;;) {
2780           name aname = '';
2781                if (n == 1 && !global.hasCape && !global.hasJetpack) aname = 'oCapePickup';
2782           else if (n == 2 && !global.hasGloves) aname = 'oGloves';
2783           else if (n == 3 && !global.hasSpectacles) aname = 'oSpectacles';
2784           else if (n == 4 && !global.hasMitt) aname = 'oMitt';
2785           else if (n == 5 && !global.hasSpringShoes) aname = 'oSpringShoes';
2786           else if (n == 6 && !global.hasSpikeShoes) aname = 'oSpikeShoes';
2787           else if (n == 7 && !global.hasStickyBombs) aname = 'oPaste';
2788           else if (n == 8 && !global.hasCompass) aname = 'oCompass';
2789           if (aname) {
2790             obj = MakeMapObject(sx, sy-8, aname);
2791             if (obj) break;
2792           }
2793           ++n;
2794           if (n > 8) n = 1;
2795           if (n == m) {
2796             obj = MakeMapObject(sx, sy-8, (global.hasJetpack ? 'oBombBox' : 'oJetpack'));
2797             break;
2798           }
2799         }
2800       }
2801     }
2802   } else if (global.favor > 0) {
2803     res = "SHE SEEMS PLEASED WITH YOU.";
2804   }
2806   /*
2807   if (argument1) {
2808     global.message = "";
2809     res = "KALI DEVOURS YOU!"; // sacrifice is player
2810   }
2811   */
2813   return res;
2817 void performSacrifice (MapObject what, MapTile where) {
2818   if (!what || !what.isInstanceAlive) return;
2819   MakeMapObject(what.ix+8, what.iy+8, 'oFlame');
2820   if (where) where.playSound('sndSmallExplode'); else what.playSound('sndSmallExplode');
2821   what.spillBlood(amount:3, forced:true);
2823   string msg = "KALI ACCEPTS THE SACRIFICE!";
2825   auto idol = ItemGoldIdol(what);
2826   if (idol) {
2827     ++stats.totalSacrifices;
2828          if (global.favor <= -8) msg = "SHE SEEMS VERY ANGRY WITH YOU!";
2829     else if (global.favor < 0) msg = "SHE SEEMS ANGRY WITH YOU.";
2830     else if (global.favor >= 0) {
2831       // find other side of the altar
2832       int sx = player.ix, sy = player.iy;
2833       auto altar = where;
2834       if (altar) {
2835         sx = altar.ix;
2836         sy = altar.iy;
2837         auto a2 = isCollisionAtPoint(altar.ix+18, altar.iy+8, &cbCollisionSacAltar);
2838         if (!a2) a2 = isCollisionAtPoint(altar.ix-8, altar.iy+8, &cbCollisionSacAltar);
2839         if (a2) { sx = a2.ix; sy = a2.iy; }
2840       }
2841       // poofs
2842       auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2843       obj.xVel = -1;
2844       obj.yVel = 0;
2845       obj = MakeMapObject(sx, sy-8, 'oPoof');
2846       obj.xVel = 1;
2847       obj.yVel = 0;
2848       // a gift
2849       obj = MakeMapObject(sx, sy-16-8, 'oMonkeyGold');
2850     }
2851     osdMessage(msg, 6.66);
2852     scrShake(10);
2853     idol.instanceRemove();
2854     return;
2855   }
2857   if (global.favor <= -8) {
2858     msg = "KALI DEVOURS THE SACRIFICE!";
2859   } else if (global.favor < 0) {
2860     global.favor += (what.status == MapObject::STUNNED ? what.favor : round(what.favor/2.0)); //k8: added `round()`
2861     if (what.favor > 0) what.favor = 0;
2862   } else {
2863     global.favor += (what.status == MapObject::STUNNED ? what.favor : round(what.favor/2.0)); //k8: added `round()`
2864   }
2866   /*!!
2867        if (myAltar.gift1 != "" and global.kaliGift == 0) scrGetKaliGift(myAltar.gift1);
2868   else if (myAltar.gift2 != "" and global.kaliGift == 1) scrGetKaliGift(myAltar.gift2);
2869   else scrGetKaliGift("");
2870   */
2872   // sacrifice is player?
2873   if (what isa PlayerPawn) {
2874     ++stats.totalSelfSacrifices;
2875     msg = "KALI DEVOURS YOU!";
2876     player.visible = false;
2877     player.removeBallAndChain(temp:true);
2878     player.dead = true;
2879     player.status = MapObject::DEAD;
2880   } else {
2881     ++stats.totalSacrifices;
2882     auto msg2 = scrGetKaliGift(where);
2883     what.instanceRemove();
2884     if (msg2) msg = va("%s\n%s", msg, msg2);
2885   }
2887   osdMessage(msg, 6.66);
2889   scrShake(10);
2893 // ////////////////////////////////////////////////////////////////////////// //
2894 final void addBackgroundGfxDetails () {
2895   // add background details
2896   //if (global.customLevel) return;
2897   foreach (; 0..20) {
2898     // bg = instance_create(16*randRoom(1, 42), 16*randRoom(1, 33), oCaveBG);
2899          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);
2900     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);
2901     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);
2902     else MakeMapBackTile('bgExtras', 32*global.randRoom(0, 1), 0, 32, 32, 16*global.randRoom(1, 42), 16*global.randRoom(1, 33), 10002);
2903   }
2907 // ////////////////////////////////////////////////////////////////////////// //
2908 private final void fixRealViewStart () {
2909   int scale = global.scale;
2910   realViewStart.x = clamp(realViewStart.x, viewMin.x*scale, viewMax.x*scale-viewWidth);
2911   realViewStart.y = clamp(realViewStart.y, viewMin.y*scale, viewMax.y*scale-viewHeight);
2915 final int cameraCurrX () { return realViewStart.x/global.scale; }
2916 final int cameraCurrY () { return realViewStart.y/global.scale; }
2919 private final void fixViewStart () {
2920   int scale = global.scale;
2921   viewStart.x = clamp(viewStart.x, viewMin.x*scale, viewMax.x*scale-viewWidth);
2922   viewStart.y = clamp(viewStart.y, viewMin.y*scale, viewMax.y*scale-viewHeight);
2926 final void centerViewAtPlayer () {
2927   if (viewWidth < 1 || viewHeight < 1 || !player) return;
2928   centerViewAt(player.xCenter, player.yCenter);
2932 final void centerViewAt (int x, int y) {
2933   if (viewWidth < 1 || viewHeight < 1) return;
2935   cameraSlideToSpeed.x = 0;
2936   cameraSlideToSpeed.y = 0;
2937   cameraSlideToPlayer = 0;
2939   int scale = global.scale;
2940   x *= scale;
2941   y *= scale;
2942   realViewStart.x = clamp(x-viewWidth/2, 0, tilesWidth*16*scale-viewWidth);
2943   realViewStart.y = clamp(y-viewHeight/2, 0, tilesHeight*16*scale-viewHeight);
2944   fixRealViewStart();
2946   viewStart.x = realViewStart.x;
2947   viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
2948   fixViewStart();
2950   if (onCameraTeleported) onCameraTeleported();
2954 const int ViewPortToleranceX = 16*1+8;
2955 const int ViewPortToleranceY = 16*1+8;
2957 final void fixCamera () {
2958   if (!player) return;
2959   if (viewWidth < 1 || viewHeight < 1) return;
2960   int scale = global.scale;
2961   auto alwaysCenterX = global.config.alwaysCenterPlayer;
2962   auto alwaysCenterY = alwaysCenterX;
2963   // calculate offset from viewport center (in game units), and fix viewport
2965   int camDestX = player.ix+8;
2966   int camDestY = player.iy+8;
2967   if (!cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y) != 0) {
2968     // slide camera to point
2969     if (cameraSlideToSpeed.x) camDestX = cameraSlideToCurr.x;
2970     if (cameraSlideToSpeed.y) camDestY = cameraSlideToCurr.y;
2971     int dx = cameraSlideToDest.x-camDestX;
2972     int dy = cameraSlideToDest.y-camDestY;
2973     //writeln("speed:(", cameraSlideToSpeed.x, ",", cameraSlideToSpeed.y, "); cur:(", camDestX, ",", camDestY, "); dest:(", cameraSlideToDest.x, ",", cameraSlideToDest.y, "); delta=(", dx, ",", dy, ")");
2974     if (dx && cameraSlideToSpeed.x != 0) {
2975       alwaysCenterX = true;
2976       if (abs(dx) <= abs(cameraSlideToSpeed.x)) {
2977         camDestX = cameraSlideToDest.x;
2978       } else {
2979         camDestX += sign(dx)*abs(cameraSlideToSpeed.x);
2980       }
2981     }
2982     if (dy && abs(cameraSlideToSpeed.y) != 0) {
2983       alwaysCenterY = true;
2984       if (abs(dy) <= cameraSlideToSpeed.y) {
2985         camDestY = cameraSlideToDest.y;
2986       } else {
2987         camDestY += sign(dy)*abs(cameraSlideToSpeed.y);
2988       }
2989     }
2990     //writeln("  new:(", camDestX, ",", camDestY, ")");
2991     if (cameraSlideToSpeed.x) cameraSlideToCurr.x = camDestX;
2992     if (cameraSlideToSpeed.y) cameraSlideToCurr.y = camDestY;
2993   }
2995   // horizontal
2996   if (cameraSlideToSpeed.x && !cameraSlideToPlayer) {
2997     realViewStart.x = clamp(camDestX*scale, 0, tilesWidth*16*scale-viewWidth);
2998   } else if (!player.cameraBlockX) {
2999     int x = camDestX*scale;
3000     int cx = realViewStart.x;
3001     if (alwaysCenterX) {
3002       cx = x-viewWidth/2;
3003     } else {
3004       int xofs = x-(cx+viewWidth/2);
3005            if (xofs < -ViewPortToleranceX*scale) cx += xofs+ViewPortToleranceX*scale;
3006       else if (xofs > ViewPortToleranceX*scale) cx += xofs-ViewPortToleranceX*scale;
3007     }
3008     // slide back to player?
3009     if (cameraSlideToPlayer && cameraSlideToSpeed.x) {
3010       int prevx = cameraSlideToCurr.x*scale;
3011       int dx = (cx-prevx)/scale;
3012       if (abs(dx) <= cameraSlideToSpeed.x) {
3013         writeln("BACKSLIDE X COMPLETE!");
3014         cameraSlideToSpeed.x = 0;
3015       } else {
3016         cx = prevx+(sign(dx)*cameraSlideToSpeed.x)*scale;
3017         cameraSlideToCurr.x += sign(dx)*cameraSlideToSpeed.x;
3018         if ((dx < 0 && cx <= 0) || (dx > 0 && cx >= tilesWidth*16*scale-viewWidth)) {
3019           writeln("BACKSLIDE X COMPLETE!");
3020           cameraSlideToSpeed.x = 0;
3021         }
3022       }
3023     }
3024     realViewStart.x = clamp(cx, 0, tilesWidth*16*scale-viewWidth);
3025   }
3027   // vertical
3028   if (cameraSlideToSpeed.y && !cameraSlideToPlayer) {
3029     realViewStart.y = clamp(camDestY*scale, 0, tilesHeight*16*scale-viewHeight);
3030   } else if (!player.cameraBlockY) {
3031     int y = camDestY*scale;
3032     int cy = realViewStart.y;
3033     if (alwaysCenterY) {
3034       cy = y-viewHeight/2;
3035     } else {
3036       int yofs = y-(cy+viewHeight/2);
3037            if (yofs < -ViewPortToleranceY*scale) cy += yofs+ViewPortToleranceY*scale;
3038       else if (yofs > ViewPortToleranceY*scale) cy += yofs-ViewPortToleranceY*scale;
3039     }
3040     // slide back to player?
3041     if (cameraSlideToPlayer && cameraSlideToSpeed.y) {
3042       int prevy = cameraSlideToCurr.y*scale;
3043       int dy = (cy-prevy)/scale;
3044       if (abs(dy) <= cameraSlideToSpeed.y) {
3045         writeln("BACKSLIDE Y COMPLETE!");
3046         cameraSlideToSpeed.y = 0;
3047       } else {
3048         cy = prevy+(sign(dy)*cameraSlideToSpeed.y)*scale;
3049         cameraSlideToCurr.y += sign(dy)*cameraSlideToSpeed.y;
3050         if ((dy < 0 && cy <= 0) || (dy > 0 && cy >= tilesHeight*16*scale-viewHeight)) {
3051           writeln("BACKSLIDE Y COMPLETE!");
3052           cameraSlideToSpeed.y = 0;
3053         }
3054       }
3055     }
3056     realViewStart.y = clamp(cy, 0, tilesHeight*16*scale-viewHeight);
3057   }
3059   if (cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y) == 0) cameraSlideToPlayer = 0;
3061   fixRealViewStart();
3062   //writeln("  new2:(", cameraCurrX, ",", cameraCurrY, ")");
3064   viewStart.x = realViewStart.x;
3065   viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
3066   fixViewStart();
3070 // ////////////////////////////////////////////////////////////////////////// //
3071 // x0 and y0 are non-scaled (and will be scaled)
3072 final void drawSpriteAt (name sprName, float frnumf, int x0, int y0, optional bool small) {
3073   if (!sprName) return;
3074   auto spr = sprStore[sprName];
3075   if (!spr || !spr.frames.length) return;
3076   int scale = global.scale;
3077   x0 *= scale;
3078   y0 *= scale;
3079   int frnum = max(0, trunc(frnumf))%spr.frames.length;
3080   auto sfr = spr.frames[frnum];
3081   int sx0 = x0-sfr.xofs*scale;
3082   int sy0 = y0-sfr.yofs*scale;
3083   if (small && scale > 1) {
3084     sfr.tex.blitExt(sx0, sy0, round(sx0+sfr.tex.width*(scale/2.0)), round(sy0+sfr.tex.height*(scale/2.0)), 0, 0);
3085   } else {
3086     sfr.tex.blitAt(sx0, sy0, scale);
3087   }
3091 final void drawSpriteAtS3 (name sprName, float frnumf, int x0, int y0) {
3092   if (!sprName) return;
3093   auto spr = sprStore[sprName];
3094   if (!spr || !spr.frames.length) return;
3095   x0 *= 3;
3096   y0 *= 3;
3097   int frnum = max(0, trunc(frnumf))%spr.frames.length;
3098   auto sfr = spr.frames[frnum];
3099   int sx0 = x0-sfr.xofs*3;
3100   int sy0 = y0-sfr.yofs*3;
3101   sfr.tex.blitAt(sx0, sy0, 3);
3105 // x0 and y0 are non-scaled (and will be scaled)
3106 final void drawTextAt (int x0, int y0, string text, optional int scale, optional int hiColor1, optional int hiColor2) {
3107   if (!text) return;
3108   if (!specified_scale) scale = global.scale;
3109   x0 *= scale;
3110   y0 *= scale;
3111   sprStore.renderTextWithHighlight(x0, y0, text, scale, hiColor1!optional, hiColor2!optional);
3115 void renderCompass (float currFrameDelta) {
3116   if (!global.hasCompass) return;
3118   /*
3119   if (isRoom("rOlmec")) {
3120     global.exitX = 648;
3121     global.exitY = 552;
3122   } else if (isRoom("rOlmec2")) {
3123     global.exitX = 648;
3124     global.exitY = 424;
3125   }
3126   */
3128   bool hasMessage = osdHasMessage();
3129   foreach (MapTile et; allExits) {
3130     // original compass
3131     int exitX = et.ix, exitY = et.iy;
3132     int vx0 = viewStart.x/global.scale, vy0 = viewStart.y/global.scale;
3133     int vx1 = (viewStart.x+viewWidth)/global.scale;
3134     int vy1 = (viewStart.y+viewHeight)/global.scale;
3135     if (exitY > vy1-16) {
3136       if (exitX < vx0) {
3137         drawSpriteAt(hasMessage ? 'sCompassSmallLL' : 'sCompassLL', 0, 0, 224);
3138       } else if (exitX > vx1-16) {
3139         drawSpriteAt(hasMessage ? 'sCompassSmallLR' : 'sCompassLR', 0, 304, 224);
3140       } else {
3141         drawSpriteAt(hasMessage ? 'sCompassSmallDown' : 'sCompassDown', 0, exitX-vx0, 224);
3142       }
3143     } else if (exitX < vx0) {
3144       drawSpriteAt(hasMessage ? 'sCompassSmallLeft' : 'sCompassLeft', 0, 0, exitY-vy0);
3145     } else if (exitX > vx1-16) {
3146       drawSpriteAt(hasMessage ? 'sCompassSmallRight' : 'sCompassRight', 0, 304, exitY-vy0);
3147     }
3148     break; // only the first exit
3149   }
3153 final bool totalsNameCmpCB (ref GameStats::TotalItem a, ref GameStats::TotalItem b) {
3154   auto sa = string(a.objName);
3155   auto sb = string(b.objName);
3156   return (sa < sb);
3159 void renderTransitionInfo (float currFrameDelta) {
3160   //FIXME!
3161   /*
3162   GameStats.sortTotalsList(stats.kills, &totalsNameCmpCB);
3164   int maxLen = 0;
3165   foreach (int idx, ref auto k; stats.kills) {
3166     string s = string(k);
3167     maxLen = max(maxLen, s.length);
3168   }
3169   maxLen *= 8;
3171   sprStore.loadFont('sFontSmall');
3172   Video.color = 0xff_ff_00;
3173   foreach (int idx, ref auto k; stats.kills) {
3174     int deaths = 0;
3175     foreach (int xidx, ref auto d; stats.totalKills) {
3176       if (d.objName == k) { deaths = d.count; break; }
3177     }
3178     //drawTextAt(16, 4+idx*8, va("%s: %d (%d)", string(k.objName).toUpperCase, k.count, deaths));
3179     drawTextAt(16, 4+idx*8, string(k).toUpperCase);
3180     drawTextAt(16+maxLen, 4+idx*8, va(": %d (%d)", k.count, deaths));
3181   }
3182   */
3186 void renderGhostTimer (float currFrameDelta) {
3187   if (ghostTimeLeft <= 0) return;
3188   //ghostTimeLeft /= 30; // frames -> seconds
3190   int hgt = viewHeight-64;
3191   if (hgt < 1) return;
3192   int rhgt = round(float(hgt)*(float(ghostTimeLeft)/30.0)/1000.0);
3193   //writeln("hgt=", hgt, "; rhgt=", rhgt, "; gtl=", ghostTimeLeft/30);
3194   if (rhgt > 0) {
3195     auto oclr = Video.color;
3196     Video.color = 0xcf_ff_7f_00;
3197     Video.fillRect(viewWidth-20, 32, 16, hgt-rhgt);
3198     Video.color = 0x7f_ff_7f_00;
3199     Video.fillRect(viewWidth-20, 32+(hgt-rhgt), 16, rhgt);
3200     Video.color = oclr;
3201   }
3205 void renderStarsHUD (float currFrameDelta) {
3206   bool scumSmallHud = global.config.scumSmallHud;
3208   //auto life = max(0, global.plife);
3209   int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3210   int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3211   //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3213   int hhup;
3215   if (scumSmallHud) {
3216     sprStore.loadFont('sFontSmall');
3217     hhup = 6;
3218   } else {
3219     sprStore.loadFont('sFont');
3220     hhup = 2;
3221   }
3223   Video.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3224   //draw_sprite(sHeart, -1, view_xview[0]+8, view_yview[0]+8);
3225   //draw_text(view_xview[0]+24, view_yview[0]+8, life);
3226   if (scumSmallHud) {
3227     if (global.plife == 1) {
3228       drawSpriteAt('sHeartSmallBlink', global.heartBlink, 10, hhup-4);
3229       global.heartBlink += 0.1;
3230       if (global.heartBlink > 3) global.heartBlink = 0;
3231     } else {
3232       drawSpriteAt('sHeartSmall', -1, 10, hhup-4);
3233       global.heartBlink = 0;
3234     }
3235   } else {
3236     if (global.plife == 1) {
3237       drawSpriteAt('sHeartBlink', global.heartBlink, 8, hhup);
3238       global.heartBlink += 0.1;
3239       if (global.heartBlink > 3) global.heartBlink = 0;
3240     } else {
3241       drawSpriteAt('sHeart', -1, 8, hhup);
3242       global.heartBlink = 0;
3243     }
3244   }
3245   int life = clamp(global.plife, 0, 99);
3246   drawTextAt(16+8, hhup, va("%d", life));
3248   //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3249   drawSpriteAt('sShopkeeperIcon', -1, 64, hhup, scumSmallHud);
3250   drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", starsKills));
3252   if (starsRoomTimer1 > 0) {
3253     sprStore.loadFont('sFontSmall');
3254     Video.color = 0xff_ff_00;
3255     int scale = global.scale;
3256     sprStore.renderMultilineTextCentered(viewWidth/2, 216*scale, va("SHOTGUN CHALLENGE BEGINS IN ~%d~", (starsRoomTimer1/30)+1), scale, 0xff_00_00);
3257   }
3261 void renderSunHUD (float currFrameDelta) {
3262   bool scumSmallHud = global.config.scumSmallHud;
3264   //auto life = max(0, global.plife);
3265   int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3266   int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3267   //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3269   int hhup;
3271   if (scumSmallHud) {
3272     sprStore.loadFont('sFontSmall');
3273     hhup = 6;
3274   } else {
3275     sprStore.loadFont('sFont');
3276     hhup = 2;
3277   }
3279   Video.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3280   //draw_sprite(sHeart, -1, view_xview[0]+8, view_yview[0]+8);
3281   //draw_text(view_xview[0]+24, view_yview[0]+8, life);
3282   if (scumSmallHud) {
3283     if (global.plife == 1) {
3284       drawSpriteAt('sHeartSmallBlink', global.heartBlink, 10, hhup-4);
3285       global.heartBlink += 0.1;
3286       if (global.heartBlink > 3) global.heartBlink = 0;
3287     } else {
3288       drawSpriteAt('sHeartSmall', -1, 10, hhup-4);
3289       global.heartBlink = 0;
3290     }
3291   } else {
3292     if (global.plife == 1) {
3293       drawSpriteAt('sHeartBlink', global.heartBlink, 8, hhup);
3294       global.heartBlink += 0.1;
3295       if (global.heartBlink > 3) global.heartBlink = 0;
3296     } else {
3297       drawSpriteAt('sHeart', -1, 8, hhup);
3298       global.heartBlink = 0;
3299     }
3300   }
3301   int life = clamp(global.plife, 0, 99);
3302   drawTextAt(16+8, hhup, va("%d", life));
3304   //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3305   drawSpriteAt('sDamselIcon', -1, 64, hhup, scumSmallHud);
3306   drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", sunScore));
3308   if (sunRoomTimer1 > 0) {
3309     sprStore.loadFont('sFontSmall');
3310     Video.color = 0xff_ff_00;
3311     int scale = global.scale;
3312     sprStore.renderMultilineTextCentered(viewWidth/2, 216*scale, va("DAMSEL CHALLENGE BEGINS IN ~%d~", (sunRoomTimer1/30)+1), scale, 0xff_00_00);
3313   }
3317 void renderMoonHUD (float currFrameDelta) {
3318   bool scumSmallHud = global.config.scumSmallHud;
3320   //auto life = max(0, global.plife);
3321   int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3322   int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3323   //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3325   int hhup;
3327   if (scumSmallHud) {
3328     sprStore.loadFont('sFontSmall');
3329     hhup = 6;
3330   } else {
3331     sprStore.loadFont('sFont');
3332     hhup = 2;
3333   }
3335   Video.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3337   //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3338   drawSpriteAt('sHoopsIcon', -1, 8, hhup, scumSmallHud);
3339   drawTextAt(8+16-(scumSmallHud ? 6 : 0), hhup, va("%d", moonScore));
3340   drawSpriteAt('sTimerIcon', -1, 64, hhup, scumSmallHud);
3341   drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", max(0, moonTimer)));
3343   if (moonRoomTimer1 > 0) {
3344     sprStore.loadFont('sFontSmall');
3345     Video.color = 0xff_ff_00;
3346     int scale = global.scale;
3347     sprStore.renderMultilineTextCentered(viewWidth/2, 216*scale, va("ARCHERY CHALLENGE BEGINS IN ~%d~", (moonRoomTimer1/30)+1), scale, 0xff_00_00);
3348   }
3352 void renderHUD (float currFrameDelta) {
3353   if (levelKind == LevelKind.Stars) { renderStarsHUD(currFrameDelta); return; }
3354   if (levelKind == LevelKind.Sun) { renderSunHUD(currFrameDelta); return; }
3355   if (levelKind == LevelKind.Moon) { renderMoonHUD(currFrameDelta); return; }
3357   if (!isHUDEnabled()) return;
3359   if (levelKind == LevelKind.Transition) { renderTransitionInfo(currFrameDelta); return; }
3361   int lifeX = 4; // 8
3362   int bombX = 56;
3363   int ropeX = 104;
3364   int ammoX = 152;
3365   int moneyX = 200;
3366   int hhup;
3367   bool scumSmallHud = global.config.scumSmallHud;
3368   if (!global.config.optSGAmmo) moneyX = ammoX;
3370   if (scumSmallHud) {
3371     sprStore.loadFont('sFontSmall');
3372     hhup = 6;
3373   } else {
3374     sprStore.loadFont('sFont');
3375     hhup = 0;
3376   }
3377   //int alpha = 0x6f_00_00_00;
3378   int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3379   int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3381   //Video.color = 0xff_ff_ff;
3382   Video.color = 0xff_ff_ff|talpha;
3384   // hearts
3385   if (scumSmallHud) {
3386     if (global.plife == 1) {
3387       drawSpriteAt('sHeartSmallBlink', global.heartBlink, lifeX+2, 4-hhup);
3388       global.heartBlink += 0.1;
3389       if (global.heartBlink > 3) global.heartBlink = 0;
3390     } else {
3391       drawSpriteAt('sHeartSmall', -1, lifeX+2, 4-hhup);
3392       global.heartBlink = 0;
3393     }
3394   } else {
3395     if (global.plife == 1) {
3396       drawSpriteAt('sHeartBlink', global.heartBlink, lifeX, 8-hhup);
3397       global.heartBlink += 0.1;
3398       if (global.heartBlink > 3) global.heartBlink = 0;
3399     } else {
3400       drawSpriteAt('sHeart', -1, lifeX, 8-hhup);
3401       global.heartBlink = 0;
3402     }
3403   }
3405   int life = clamp(global.plife, 0, 99);
3406   //if (!scumHud && life > 99) life = 99;
3407   drawTextAt(lifeX+16, 8-hhup, va("%d", life));
3409   // bombs
3410   if (global.hasStickyBombs && global.stickyBombsActive) {
3411     if (scumSmallHud) drawSpriteAt('sStickyBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sStickyBombIcon', -1, bombX, 8-hhup);
3412   } else {
3413     if (scumSmallHud) drawSpriteAt('sBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sBombIcon', -1, bombX, 8-hhup);
3414   }
3415   int n = global.bombs;
3416   if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3417   drawTextAt(bombX+16, 8-hhup, va("%d", n));
3419   // ropes
3420   if (scumSmallHud) drawSpriteAt('sRopeIconSmall', -1, ropeX+2, 4-hhup); else drawSpriteAt('sRopeIcon', -1, ropeX, 8-hhup);
3421   n = global.rope;
3422   if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3423   drawTextAt(ropeX+16, 8-hhup, va("%d", n));
3425   // shotgun shells
3426   if (global.config.optSGAmmo && (!player || player.holdItem !isa ItemWeaponBow)) {
3427     if (scumSmallHud) drawSpriteAt('sShellsIcon', -1, ammoX+6, 8-hhup); else drawSpriteAt('sShellsIcon', -1, ammoX+7, 12-hhup);
3428     n = global.sgammo;
3429     if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3430     drawTextAt(ammoX+16, 8-hhup, va("%d", n));
3431   } else if (player && player.holdItem isa ItemWeaponBow) {
3432     if (scumSmallHud) drawSpriteAt('sArrowRight', -1, ammoX+6, 8-hhup); else drawSpriteAt('sArrowRight', -1, ammoX+7, 12-hhup);
3433     n = global.arrows;
3434     if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3435     drawTextAt(ammoX+16, 8-hhup, va("%d", n));
3436   }
3438   // money
3439   if (scumSmallHud) drawSpriteAt('sDollarSignSmall', -1, moneyX+2, 3-hhup); else drawSpriteAt('sDollarSign', -1, moneyX, 8-hhup);
3440   drawTextAt(moneyX+16, 8-hhup, va("%d", stats.money-xmoney));
3442   // items
3443   Video.color = 0xff_ff_ff|ialpha;
3445   int ity = (scumSmallHud ? 18-hhup : 24-hhup);
3447   n = 8; //28;
3448   if (global.hasUdjatEye) {
3449     if (global.udjatBlink) drawSpriteAt('sUdjatEyeIcon2', -1, n, ity); else drawSpriteAt('sUdjatEyeIcon', -1, n, ity);
3450     n += 20;
3451   }
3452   if (global.hasAnkh) { drawSpriteAt('sAnkhIcon', -1, n, ity); n += 20; }
3453   if (global.hasCrown) { drawSpriteAt('sCrownIcon', -1, n, ity); n += 20; }
3454   if (global.hasKapala) {
3455          if (global.bloodLevel == 0) drawSpriteAt('sKapalaIcon', 0, n, ity);
3456     else if (global.bloodLevel <= 2) drawSpriteAt('sKapalaIcon', 1, n, ity);
3457     else if (global.bloodLevel <= 4) drawSpriteAt('sKapalaIcon', 2, n, ity);
3458     else if (global.bloodLevel <= 6) drawSpriteAt('sKapalaIcon', 3, n, ity);
3459     else if (global.bloodLevel <= 8) drawSpriteAt('sKapalaIcon', 4, n, ity);
3460     n += 20;
3461   }
3462   if (global.hasSpectacles) { drawSpriteAt('sSpectaclesIcon', -1, n, ity); n += 20; }
3463   if (global.hasGloves) { drawSpriteAt('sGlovesIcon', -1, n, ity); n += 20; }
3464   if (global.hasMitt) { drawSpriteAt('sMittIcon', -1, n, ity); n += 20; }
3465   if (global.hasSpringShoes) { drawSpriteAt('sSpringShoesIcon', -1, n, ity); n += 20; }
3466   if (global.hasJordans) { drawSpriteAt('sJordansIcon', -1, n, ity); n += 20; }
3467   if (global.hasSpikeShoes) { drawSpriteAt('sSpikeShoesIcon', -1, n, ity); n += 20; }
3468   if (global.hasCape) { drawSpriteAt('sCapeIcon', -1, n, ity); n += 20; }
3469   if (global.hasJetpack) { drawSpriteAt('sJetpackIcon', -1, n, ity); n += 20; }
3470   if (global.hasStickyBombs) { drawSpriteAt('sPasteIcon', -1, n, ity); n += 20; }
3471   if (global.hasCompass) { drawSpriteAt('sCompassIcon', -1, n, ity); n += 20; }
3472   if (global.hasParachute) { drawSpriteAt('sParachuteIcon', -1, n, ity); n += 20; }
3474   if (player.holdItem && player.holdItem.objName == 'Bow' && global.arrows > 0) {
3475     int m = 1;
3476     float malpha = 1;
3477     while (m <= global.arrows && m <= 20 && malpha > 0) {
3478       Video.color = trunc(malpha*255)<<24|0xff_ff_ff;
3479       drawSpriteAt('sArrowIcon', -1, n, ity);
3480       n += 4;
3481       if (m > 10) malpha -= 0.1; //and global.arrows > 20 malpha -= 0.1
3482       m += 1;
3483     }
3484   }
3486   if (xmoney > 0) {
3487     sprStore.loadFont('sFontSmall');
3488     Video.color = 0xff_ff_00|talpha;
3489     if (scumSmallHud) drawTextAt(moneyX+8, 8+8-hhup, va("+%d", xmoney));
3490     else drawTextAt(moneyX, 8+16-hhup, va("+%d", xmoney));
3491   }
3493   Video.color = 0xff_ff_ff;
3494   if (global.config.ghostShowTime) renderGhostTimer(currFrameDelta);
3498 // ////////////////////////////////////////////////////////////////////////// //
3499 // x0 and y0 are non-scaled (and will be scaled)
3500 final void drawTextAtS3 (int x0, int y0, string text, optional int hiColor1, optional int hiColor2) {
3501   if (!text) return;
3502   x0 *= 3;
3503   y0 *= 3;
3504   sprStore.renderTextWithHighlight(x0, y0, text, 3, hiColor1!optional, hiColor2!optional);
3508 final void drawTextAtS3Centered (int y0, string text, optional int hiColor1, optional int hiColor2) {
3509   if (!text) return;
3510   int x0 = (viewWidth-sprStore.getTextWidth(text, 3, specified_hiColor1, specified_hiColor2))/2;
3511   sprStore.renderTextWithHighlight(x0, y0*3, text, 3, hiColor1!optional, hiColor2!optional);
3515 void renderHelpOverlay () {
3516   Video.color = 0;
3517   Video.fillRect(0, 0, viewWidth, viewHeight);
3519   int tx = 16;
3520   int txoff = 0; // text x pos offset (for multi-color lines)
3521   int ty = 8;
3522   if (gameHelpScreen) {
3523     sprStore.loadFont('sFontSmall');
3524     Video.color = 0xff_ff_ff;
3525     drawTextAtS3Centered(ty, va("HELP (PAGE ~%d~ OF ~%d~)", gameHelpScreen, MaxGameHelpScreen), 0xff_ff_00);
3526     ty += 24;
3527   }
3529   if (gameHelpScreen == 1) {
3530     sprStore.loadFont('sFontSmall');
3531     Video.color = 0xff_ff_00; drawTextAtS3(tx, ty, "INVENTORY BASICS"); ty += 16;
3532     Video.color = 0xff_ff_ff;
3533     drawTextAtS3(tx, ty, global.expandString("Press $SWITCH to cycle through items."), 0x00_ff_00);
3534     ty += 8;
3535     ty += 56;
3536     Video.color = 0xff_ff_ff;
3537     drawSpriteAtS3('sHelpSprite1', -1, 64, 96);
3538   } else if (gameHelpScreen == 2) {
3539     sprStore.loadFont('sFontSmall');
3540     Video.color = 0xff_ff_00;
3541     drawTextAtS3(tx, ty, "SELLING TO SHOPKEEPERS"); ty += 16;
3542     Video.color = 0xff_ff_ff;
3543     drawTextAtS3(tx, ty, global.expandString("Press $PAY to offer your currently"), 0x00_ff_00); ty += 8;
3544     drawTextAtS3(tx, ty, "held item to the shopkeeper."); ty += 16;
3545     drawTextAtS3(tx, ty, "If the shopkeeper is interested, "); ty += 8;
3546     //drawTextAtS3(tx, ty, global.expandString("press $PAY again to complete the sale."), 0x00_ff_00); ty += 72;
3547     drawTextAtS3(tx, ty, global.expandString("press $PAY again to complete"), 0x00_ff_00);
3548     drawTextAtS3(tx, ty+8, "the sale.");
3549     ty += 72;
3550     drawSpriteAtS3('sHelpSell', -1, 112, 100);
3551     drawTextAtS3(tx, ty, "Purchasing goods from the shopkeeper"); ty += 8;
3552     drawTextAtS3(tx, ty, "will increase the funds he has"); ty += 8;
3553     drawTextAtS3(tx, ty, "available to buy your unwanted stuff."); ty += 8;
3554   } else {
3555     // map
3556     sprStore.loadFont('sFont');
3557     Video.color = 0xff_ff_ff;
3558     drawTextAtS3(136, 8, "MAP");
3560     if (lg.mapSprite && (isNormalLevel() || isTransitionRoom())) {
3561       Video.color = 0xff_ff_00;
3562       drawTextAtS3Centered(24, lg.mapTitle);
3564       auto spf = sprStore[lg.mapSprite].frames[0];
3565       int mapX = 160-spf.width/2;
3566       int mapY = 120-spf.height/2;
3567       //mapTitleX = 160-string_length(global.mapTitle)*8/2;
3569       Video.color = 0xff_ff_ff;
3570       drawSpriteAtS3(lg.mapSprite, -1, mapX, mapY);
3572       if (lg.mapSprite != 'sMapDefault') {
3573         int mx = -1, my = -1;
3575         // set position of player icon
3576         switch (global.currLevel) {
3577           case 1: mx = 81; my = 22; break;
3578           case 2: mx = 113; my = 63; break;
3579           case 3: mx = 197; my = 86; break;
3580           case 4: mx = 133; my = 109; break;
3581           case 5: mx = 181; my = 22; break;
3582           case 6: mx = 126; my = 64; break;
3583           case 7: mx = 158; my = 112; break;
3584           case 8: mx = 66; my = 80; break;
3585           case 9: mx = 30; my = 26; break;
3586           case 10: mx = 88; my = 54; break;
3587           case 11: mx = 148; my = 81; break;
3588           case 12: mx = 210; my = 205; break;
3589           case 13: mx = 66; my = 17; break;
3590           case 14: mx = 146; my = 17; break;
3591           case 15: mx = 82; my = 77; break;
3592           case 16: mx = 178; my = 81; break;
3593         }
3595         if (mx >= 0) {
3596           int plrx = mx+player.ix/16;
3597           int plry = my+player.iy/16;
3598           if (isTransitionRoom()) { plrx = mx+20; plry = my+16; }
3599           name plrspr = 'sMapSpelunker';
3600                if (global.isDamsel) plrspr = 'sMapDamsel';
3601           else if (global.isTunnelMan) plrspr = 'sMapTunnel';
3602           auto ss = sprStore[plrspr];
3603           drawSpriteAtS3(plrspr, (pausedTime/2)%ss.frames.length, mapX+plrx, mapY+plry);
3604           // exit door icon
3605           if (global.hasCompass && allExits.length) {
3606             drawSpriteAtS3('sMapRedDot', -1, mapX+mx+allExits[0].ix/16, mapY+my+allExits[0].iy/16);
3607           }
3608         }
3609       }
3610     }
3611   }
3613   sprStore.loadFont('sFontSmall');
3614   Video.color = 0xff_ff_00;
3615   drawTextAtS3Centered(232, "PRESS ~SPACE~/~LEFT~/~RIGHT~ TO CHANGE PAGE", 0x00_ff_00);
3617   Video.color = 0xff_ff_ff;
3621 void renderPauseOverlay () {
3622   //drawTextAt(256, 432, "PAUSED", scale);
3624   if (gameShowHelp) { renderHelpOverlay(); return; }
3626   Video.color = 0xff_ff_00;
3627   //int hiColor = 0x00_ff_00;
3629   int n = 120;
3630   if (isTutorialRoom()) {
3631     sprStore.loadFont('sFont');
3632     drawTextAtS3(40, n-24, "TUTORIAL CAVE");
3633   } else if (isNormalLevel()) {
3634     sprStore.loadFont('sFont');
3636     drawTextAtS3Centered(n-32, va("LEVEL ~%d~", global.currLevel), 0x00_ff_00);
3638     sprStore.loadFont('sFontSmall');
3640     int depth = round((174.8*(global.currLevel-1)+(player.iy+8)*0.34)*(global.config.scumMetric ? 0.3048 : 1.0)*10);
3641     string depthStr = va("DEPTH: ~%d.%d~ %s", depth/10, depth%10, (global.config.scumMetric ? "METRES" : "FEET"));
3642     drawTextAtS3Centered(n-16, depthStr, 0x00_ff_00);
3644     n += 16;
3645     drawTextAtS3Centered(n, va("MONEY: ~%d~", stats.money), 0x00_ff_00);
3646     drawTextAtS3Centered(n+16, va("KILLS: ~%d~", stats.countKills), 0x00_ff_00);
3647     drawTextAtS3Centered(n+32, va("SAVES: ~%d~", stats.damselsSaved), 0x00_ff_00);
3648     drawTextAtS3Centered(n+48, va("TIME: ~%s~", time2str(time/30)), 0x00_ff_00);
3649     drawTextAtS3Centered(n+64, va("LEVEL TIME: ~%s~", time2str((time-levelStartTime)/30)), 0x00_ff_00);
3650   }
3652   sprStore.loadFont('sFontSmall');
3653   Video.color = 0xff_ff_ff;
3654   drawTextAtS3Centered(240-2-8, "~ESC~-RETURN  ~F10~-QUIT  ~CTRL+DEL~-SUICIDE", 0xff_7f_00);
3655   drawTextAtS3Centered(2, "~O~PTIONS  REDEFINE ~K~EYS  ~S~TATISTICS", 0xff_7f_00);
3659 // ////////////////////////////////////////////////////////////////////////// //
3660 transient int drawLoot;
3661 transient int drawPosX, drawPosY;
3663 void resetTransitionOverlay () {
3664   drawLoot = 0;
3665   drawPosX = 100;
3666   drawPosY = 83;
3670 // current game, uncollapsed
3671 struct LevelStatInfo {
3672   name aname;
3673   // for transition screen
3674   bool render;
3675   int x, y;
3680 void thinkFrameTransition () {
3681   if (drawLoot == 0) {
3682     if (drawPosX > 272) {
3683       drawPosX = 100;
3684       drawPosY += 2;
3685       if (drawPosY > 83+4) drawPosY = 83;
3686     }
3687   } else if (drawPosX > 232) {
3688     drawPosX = 96;
3689     drawPosY += 2;
3690     if (drawPosY > 91+4) drawPosY = 91;
3691   }
3695 void renderTransitionOverlay () {
3696   sprStore.loadFont('sFontSmall');
3697   Video.color = 0xff_ff_00;
3698   //else if (global.currLevel-1 &lt; 1) draw_text(32, 48, "TUTORIAL CAVE COMPLETED!");
3699   //else draw_text(32, 48, "LEVEL "+string(global.currLevel-1)+" COMPLETED!");
3700   drawTextAt(32, 48, va("LEVEL ~%d~ COMPLETED!", global.currLevel), hiColor1:0x00_ff_ff);
3701   Video.color = 0xff_ff_ff;
3702   drawTextAt(32, 64, va("TIME  = ~%s~", time2str((levelEndTime-levelStartTime)/30)), hiColor1:0xff_ff_00);
3704   if (/*stats.collected.length == 0*/stats.money <= levelMoneyStart) {
3705     drawTextAt(32, 80, "LOOT  = ~NONE~", hiColor1:0xff_00_00);
3706   } else {
3707     drawTextAt(32, 80, va("LOOT  = ~%d~", stats.money-levelMoneyStart), hiColor1:0xff_ff_00);
3708   }
3710   if (stats.kills.length == 0) {
3711     drawTextAt(32, 96, "KILLS = ~NONE~", hiColor1:0x00_ff_00);
3712   } else {
3713     drawTextAt(32, 96, va("KILLS = ~%d~", stats.kills.length), hiColor1:0xff_ff_00);
3714   }
3716   drawTextAt(32, 112, va("MONEY = ~%d~", stats.money), hiColor1:0xff_ff_00);
3720 // ////////////////////////////////////////////////////////////////////////// //
3721 private transient array!MapEntity renderVisibleCids;
3722 private transient array!MapEntity renderVisibleLights;
3723 private transient array!MapTile renderFrontTiles; // normal, with fg
3725 final bool renderSortByDepth (MapEntity oa, MapEntity ob) {
3726   auto da = oa.depth, db = ob.depth;
3727   if (da == db) return (oa.objId < ob.objId);
3728   return (da < db);
3732 const int RenderEdgePixNormal = 64;
3733 const int RenderEdgePixLight = 256;
3735 #ifndef EXPERIMENTAL_RENDER_CACHE
3736 enum skipListCreation = false;
3737 #endif
3739 void renderWithOfs (int xofs, int yofs, float currFrameDelta) {
3740   int scale = global.scale;
3742   // don't touch framebuffer alpha
3743   Video.colorMask = Video::CMask.Colors;
3744   Video.color = 0xff_ff_ff;
3746   /*
3747   Video::ScissorRect scsave;
3748   bool doRestoreGL = false;
3750   if (viewOffsetX > 0 || viewOffsetY > 0) {
3751     doRestoreGL = true;
3752     Video.getScissor(scsave);
3753     Video.scissorCombine(viewOffsetX, viewOffsetY, viewWidth, viewHeight);
3754     Video.glPushMatrix();
3755     Video.glTranslate(viewOffsetX, viewOffsetY);
3756     //Video.glTranslate(-550, 0);
3757     //Video.glScale(1, 1);
3758   }
3759   */
3762   bool isDarkLevel = global.darkLevel;
3764   if (isDarkLevel) {
3765     switch (global.config.scumPlayerLit) {
3766       case 0: player.lightRadius = 0; break; // never
3767       case 1: // only in "scumDarkness"
3768         player.lightRadius = (global.config.scumDarkness >= 2 ? 96 : 32);
3769         break;
3770       case 2:
3771         player.lightRadius = 96;
3772         break;
3773     }
3774   }
3776   // render cave background
3777   if (levBGImg) {
3778     int tsz = 16*scale;
3779     int bgw = levBGImg.tex.width*scale;
3780     int bgh = levBGImg.tex.height*scale;
3781     int bgTW = (tilesWidth*tsz+bgw-1)/bgw;
3782     int bgTH = (tilesHeight*tsz+bgh-1)/bgh;
3783     int bgX0 = max(0, xofs/bgw);
3784     int bgY0 = max(0, yofs/bgh);
3785     int bgX1 = min(bgTW, (xofs+viewWidth+bgw-1)/bgw);
3786     int bgY1 = min(bgTH, (yofs+viewHeight+bgh-1)/bgh);
3787     foreach (int ty; bgY0..bgY1) {
3788       foreach (int tx; bgX0..bgX1) {
3789         int x0 = tx*bgw-xofs;
3790         int y0 = ty*bgh-yofs;
3791         levBGImg.tex.blitAt(x0, y0, scale);
3792       }
3793     }
3794   }
3796   int RenderEdgePix = (global.darkLevel ? RenderEdgePixLight : RenderEdgePixNormal);
3798   // render background tiles
3799   for (MapBackTile bt = backtiles; bt; bt = bt.next) {
3800     bt.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3801   }
3803   // collect visible special tiles
3804 #ifdef EXPERIMENTAL_RENDER_CACHE
3805   bool skipListCreation = (lastRenderTime == time && renderVisibleCids.length); //FIXME
3806 #endif
3808   if (!skipListCreation) {
3809     renderVisibleCids.clear();
3810     renderVisibleLights.clear();
3811     renderFrontTiles.clear();
3813     int endVX = xofs+viewWidth;
3814     int endVY = yofs+viewHeight;
3816     // add player
3817     //int cnt = 0;
3818     if (player && player.visible) renderVisibleCids[$] = player; // include player in render list
3820     //FIXME: drop lit objects which cannot affect visible area
3821     if (scale > 1) {
3822       // collect visible objects
3823       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)) {
3824         if (!o.visible) continue;
3825         auto tile = MapTile(o);
3826         if (tile) {
3827           if (isDarkLevel && (o.lightRadius > 4 || tile.litWholeTile)) renderVisibleLights[$] = o;
3828           if (tile.invisible) continue;
3829           if (tile.bgfront) renderFrontTiles[$] = tile;
3830           if (tile.bgback) tile.drawWithOfsBack(xofs, yofs, scale, currFrameDelta);
3831         } else {
3832           if (isDarkLevel && o.lightRadius > 4) renderVisibleLights[$] = o;
3833         }
3834         // check if the object is really visible -- this will speed up later sorting
3835         int fx0, fy0, fx1, fy1;
3836         auto spf = o.getSpriteFrame(default, out fx0, out fy0, out fx1, out fy1);
3837         if (!spf) continue; // no sprite -- nothing to draw (no, really)
3838         int ix = o.ix, iy = o.iy;
3839         int x0 = (ix+fx0)*scale, y0 = (iy+fy0)*scale;
3840         int x1 = (ix+fx1)*scale, y1 = (iy+fy1)*scale;
3841         if (x1 <= xofs || y1 <= yofs || x0 >= endVX || y0 >= endVY) {
3842           //++cnt;
3843           continue;
3844         }
3845         renderVisibleCids[$] = o;
3846       }
3847     } else {
3848       foreach (MapEntity o; objGrid.allObjects()) {
3849         if (!o.visible) continue;
3850         auto tile = MapTile(o);
3851         if (tile) {
3852           if (isDarkLevel && (o.lightRadius > 4 || tile.litWholeTile)) renderVisibleLights[$] = o;
3853           if (tile.invisible) continue;
3854           if (tile.bgfront) renderFrontTiles[$] = tile;
3855           if (tile.bgback) tile.drawWithOfsBack(xofs, yofs, scale, currFrameDelta);
3856         } else {
3857           if (isDarkLevel && o.lightRadius > 4) renderVisibleLights[$] = o;
3858         }
3859         renderVisibleCids[$] = o;
3860       }
3861     }
3862     //writeln("::: ", cnt, " invisible objects dropped");
3864     renderVisibleCids.sort(&renderSortByDepth);
3865     lastRenderTime = time;
3866   }
3868   auto depth4Start = 0;
3869   foreach (auto xidx, MapEntity o; renderVisibleCids) {
3870     if (o.depth >= 4) {
3871       depth4Start = xidx;
3872       break;
3873     }
3874   }
3876   bool playerPowerupRendered = false;
3878   // render objects (part one: depth > 3)
3879   foreach (auto idx; depth4Start..renderVisibleCids.length; reverse) {
3880     MapEntity o = renderVisibleCids[idx];
3881     // 1000 is an ordinary tile
3882     if (!playerPowerupRendered && o.depth <= 1200) {
3883       playerPowerupRendered = true;
3884       // so ducking player will have it's cape correctly rendered
3885       if (player.visible) player.drawPrePrePowerupWithOfs(xofs, yofs, scale, currFrameDelta);
3886     }
3887     //if (idx && renderSortByDepth(o, renderVisibleCids[idx-1])) writeln("VIOLATION!");
3888     o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3889   }
3891   // render object (part two: front tile parts, depth 3.5)
3892   foreach (MapTile tile; renderFrontTiles) {
3893     tile.drawWithOfsFront(xofs, yofs, scale, currFrameDelta);
3894   }
3896   // render objects (part three: depth <= 3)
3897   foreach (auto idx; 0..depth4Start; reverse) {
3898     MapEntity o = renderVisibleCids[idx];
3899     o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3900     //done above;if (isDarkLevel && (o.lightRadius > 4 || (o isa MapTile && MapTile(o).litWholeTile))) renderVisibleLights[$] = o;
3901   }
3903   // render player post-effects, so cape or jetpack will be correctly rendered on level exit, for example
3904   player.lastDrawWithOfs(xofs, yofs, scale, currFrameDelta);
3906   // lighting
3907   if (isDarkLevel) {
3908     auto ltex = bgtileStore.lightTexture('ltx512', 512);
3910     // set screen alpha to min
3911     Video.colorMask = Video::CMask.Alpha;
3912     Video.blendMode = Video::BlendMode.None;
3913     Video.color = 0xff_ff_ff_ff;
3914     Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
3915     //Video.colorMask = Video::CMask.All;
3917     // blend lights
3918     // also, stencil 'em, so we can filter dark areas
3919     Video.textureFiltering = true;
3920     Video.stencil = true;
3921     Video.stencilFunc(Video::StencilFunc.Always, 1);
3922     Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Replace);
3923     Video.alphaTestFunc = Video::AlphaFunc.Greater;
3924     Video.alphaTestVal = 0.03+0.011*global.config.darknessDarkness;
3925     Video.color = 0xff_ff_ff;
3926     Video.blendFunc = Video::BlendFunc.Max;
3927     Video.blendMode = Video::BlendMode.Blend; // anything except `Normal`
3928     Video.colorMask = Video::CMask.Alpha;
3930     foreach (MapEntity e; renderVisibleLights) {
3931       int xi, yi;
3932       e.getInterpCoords(currFrameDelta, scale, out xi, out yi);
3933       auto tile = MapTile(e);
3934       if (tile && tile.litWholeTile) {
3935         //Video.color = 0xff_ff_ff;
3936         Video.fillRect(xi-xofs, yi-yofs, e.width*scale, e.height*scale);
3937       }
3938       int lrad = e.lightRadius;
3939       if (lrad < 4) continue; // just in case
3940       lrad += 8;
3941       float lightscale = float(lrad*scale)/float(ltex.tex.width);
3942 #ifdef OLD_LIGHT_OFFSETS
3943       int fx0, fy0, fx1, fy1;
3944       bool doMirror;
3945       auto spf = e.getSpriteFrame(out doMirror, out fx0, out fy0, out fx1, out fy1);
3946       if (spf) {
3947         xi += (fx1-fx0)*scale/2;
3948         yi += (fy1-fy0)*scale/2;
3949       }
3950 #else
3951       int lxofs, lyofs;
3952       e.getLightOffset(out lxofs, out lyofs);
3953       xi += lxofs*scale;
3954       yi += lyofs*scale;
3956 #endif
3957       lrad = lrad*scale/2;
3958       xi -= xofs+lrad;
3959       yi -= yofs+lrad;
3960       ltex.tex.blitAt(xi, yi, lightscale);
3961     }
3962     Video.textureFiltering = false;
3964     // modify only lit parts
3965     Video.stencilFunc(Video::StencilFunc.Equal, 1);
3966     Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Keep);
3967     // multiply framebuffer colors by framebuffer alpha
3968     Video.color = 0xff_ff_ff; // it doesn't matter
3969     Video.blendFunc = Video::BlendFunc.Add;
3970     Video.blendMode = Video::BlendMode.DstMulDstAlpha;
3971     Video.colorMask = Video::CMask.Colors;
3972     Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
3974     // filter unlit parts
3975     Video.stencilFunc(Video::StencilFunc.NotEqual, 1);
3976     Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Keep);
3977     Video.blendFunc = Video::BlendFunc.Add;
3978     Video.blendMode = Video::BlendMode.Filter;
3979     Video.colorMask = Video::CMask.Colors;
3980     Video.color = 0x00_00_18+0x00_00_10*global.config.darknessDarkness;
3981     //Video.color = 0x00_00_18;
3982     //Video.color = 0x00_00_38;
3983     Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
3985     // restore defaults
3986     Video.blendFunc = Video::BlendFunc.Add;
3987     Video.blendMode = Video::BlendMode.Normal;
3988     Video.colorMask = Video::CMask.All;
3989     Video.alphaTestFunc = Video::AlphaFunc.Always;
3990     Video.stencil = false;
3991   }
3993   // clear visible objects list (nope)
3994   //renderVisibleCids.clear();
3995   //renderVisibleLights.clear();
3998   if (global.config.drawHUD) renderHUD(currFrameDelta);
3999   renderCompass(currFrameDelta);
4001   float osdTimeLeft, osdTimeStart;
4002   string msg = osdGetMessage(out osdTimeLeft, out osdTimeStart);
4003   if (msg) {
4004     auto ct = GetTickCount();
4005     int msgScale = 3;
4006     sprStore.loadFont('sFontSmall');
4007     auto msgHeight = sprStore.getMultilineTextHeight(msg, msgScale);
4008     int x = viewWidth/2;
4009     int y = viewHeight-64-msgHeight;
4010     auto oldColor = Video.color;
4011     Video.color = 0xff_ff_00;
4012     if (osdTimeLeft < 0.5) {
4013       int alpha = 255-clamp(int(510*osdTimeLeft), 0, 255);
4014       Video.color = Video.color|(alpha<<24);
4015     } else if (ct-osdTimeStart < 0.5) {
4016       osdTimeStart = ct-osdTimeStart;
4017       int alpha = 255-clamp(int(510*osdTimeStart), 0, 255);
4018       Video.color = Video.color|(alpha<<24);
4019     }
4020     sprStore.renderMultilineTextCentered(x, y, msg, msgScale, 0x00_ff_00, 0xff_ff_ff);
4021     Video.color = oldColor;
4022   }
4024   if (inWinCutscene) renderWinCutsceneOverlay();
4025   if (inIntroCutscene) renderTitleCutsceneOverlay();
4026   if (isTransitionRoom()) renderTransitionOverlay();
4028   /*
4029   if (doRestoreGL) {
4030     Video.setScissor(scsave);
4031     Video.glPopMatrix();
4032   }
4033   */
4035   Video.color = 0xff_ff_ff;
4039 // ////////////////////////////////////////////////////////////////////////// //
4040 final class!MapObject findGameObjectClassByName (name aname) {
4041   if (!aname) return none; // just in case
4042   auto co = FindClassByGameObjName(aname);
4043   if (!co) {
4044     writeln("***UNKNOWN GAME OBJECT: '", aname, "'");
4045     return none;
4046   }
4047   co = GetClassReplacement(co);
4048   if (!co) FatalError("findGameObjectClassByName: WTF?!");
4049   if (!class!MapObject(co)) FatalError("findGameTileClassByName: object '%n' is not a map object, it is a `%n`", aname, GetClassName(co));
4050   return class!MapObject(co);
4054 final class!MapTile findGameTileClassByName (name aname) {
4055   if (!aname) return none; // just in case
4056   auto co = FindClassByGameObjName(aname);
4057   if (!co) return MapTile; // unknown names will be routed directly to tile object
4058   co = GetClassReplacement(co);
4059   if (!co) FatalError("findGameTileClassByName: WTF?!");
4060   if (!class!MapTile(co)) FatalError("findGameTileClassByName: object '%n' is not a tile, it is a `%n`", aname, GetClassName(co));
4061   return class!MapTile(co);
4065 final MapObject findAnyObjectOfType (name aname) {
4066   if (!aname) return none;
4067   auto cls = FindClassByGameObjName(aname);
4068   if (!cls) return none;
4069   foreach (MapObject obj; objGrid.allObjects(MapObject)) {
4070     if (obj.spectral) continue;
4071     if (obj isa cls) return obj;
4072   }
4073   return none;
4077 // ////////////////////////////////////////////////////////////////////////// //
4078 final bool isRopePlacedAt (int x, int y) {
4079   int[8] covered;
4080   foreach (ref auto v; covered) v = false;
4081   foreach (MapTile t; objGrid.inRectPix(x, y-8, 1, 17, precise:false, castClass:MapTileRope)) {
4082     //if (!cbIsRopeTile(t)) continue;
4083     if (t.ix != x) continue;
4084     if (t.iy == y) return true;
4085     foreach (int ty; t.iy..t.iy+8) {
4086       int d = ty-y;
4087       if (d >= 0 && d < covered.length) covered[d] = true;
4088     }
4089   }
4090   // check if the whole rope height is completely covered with ropes
4091   foreach (auto v; covered) if (!v) return false;
4092   return true;
4096 final MapTile CreateMapTile (int xpos, int ypos, name aname) {
4097   if (!aname) FatalError("cannot create typeless tile");
4098   auto tclass = findGameTileClassByName(aname);
4099   if (!tclass) return none;
4100   MapTile tile = SpawnObject(tclass);
4101   tile.global = global;
4102   tile.level = self;
4103   tile.objName = aname;
4104   tile.objType = aname; // just in case
4105   tile.fltx = xpos;
4106   tile.flty = ypos;
4107   tile.objId = ++lastUsedObjectId;
4108   if (!tile.initialize()) FatalError("cannot create tile '%n'", aname);
4109   return tile;
4113 final bool PutSpawnedMapTile (int x, int y, MapTile tile, optional bool putToGrid) {
4114   if (!tile || !tile.isInstanceAlive) return false;
4116   if (!putToGrid) putToGrid = (tile.moveable || tile.toSpecialGrid || tile.width != 16 || tile.height != 16 || x%16 != 0 || y%16 != 0);
4118   //writeln("putting tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (putToGrid ? "special " : ""), "grid, pos=(", x/16.0, ",", y/16.0, ")");
4120   if (!putToGrid) {
4121     int mapx = x/16, mapy = y/16;
4122     if (mapx < 0 || mapx >= tilesWidth || mapy < 0 || mapy >= tilesHeight) return false;
4123   }
4125   // if we already have rope tile there, there is no reason to add another one
4126   if (tile isa MapTileRope) {
4127     if (isRopePlacedAt(x, y)) return false;
4128   }
4130   // activate special or animated tile
4131   tile.active = tile.active || putToGrid || tile.moveable || tile.toSpecialGrid || tile.lava /*|| tile.water*/; // will be done in MakeMapTile
4132   // animated tiles must be active
4133   if (!tile.active) {
4134     auto spr = tile.getSprite();
4135     if (spr && spr.frames.length > 1) {
4136       writeln("activated animated tile '", tile.objName, "'");
4137       tile.active = true;
4138     }
4139   }
4141   tile.fltx = x;
4142   tile.flty = y;
4143   if (putToGrid) {
4144     //if (tile isa TitleTileCopy) writeln("*** PUTTING COPYRIGHT TILE");
4145     tile.toSpecialGrid = true;
4146     if (!tile.dontReplaceOthers && x&16 == 0 && y%16 == 0) {
4147       auto t = getTileAtGridAny(x/16, y/16);
4148       if (t && !t.immuneToReplacement) {
4149         writeln("REPLACING tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
4150         writeln("      NEW tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (tile.toSpecialGrid ? "special " : ""), "grid, pos=(", tile.fltx/16.0, ",", tile.flty/16.0, ")");
4151         t.instanceRemove();
4152       }
4153     }
4154     objGrid.insert(tile);
4155   } else {
4156     //writeln("SIZE: ", tilesWidth, "x", tilesHeight);
4157     setTileAtGrid(x/16, y/16, tile);
4158     auto t = getTileAtGridAny(x/16, y/16);
4159     /*
4160     if (t != tile) {
4161       writeln("putting tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (putToGrid ? "special " : ""), "grid, pos=(", x/16.0, ",", y/16.0, ")");
4162       checkTilesInRect(x/16, y/16, 16, 16, delegate bool (MapTile tile) {
4163         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, ")");
4164         return false;
4165       });
4166       FatalError("FUUUUUU");
4167     }
4168     */
4169   }
4171   if (tile.enter) registerEnter(tile);
4172   if (tile.exit) registerExit(tile);
4174   return true;
4178 // won't call `onDestroy()`
4179 final void RemoveMapTileFromGrid (int tileX, int tileY, optional string reason) {
4180   if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
4181     auto t = getTileAtGridAny(tileX, tileY);
4182     if (t) {
4183       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, ")");
4184       t.instanceRemove();
4185       checkWater = true;
4186     }
4187   }
4191 final MapTile MakeMapTile (int mapx, int mapy, name aname, optional bool putToGrid) {
4192   //if (mapy >= tilesHeight) { writeln("OOM: ", aname, "; ty=", mapy, "; hgt=", tilesHeight); }
4193   if (mapx < 0 || mapx >= tilesWidth || mapy < 0 || mapy >= tilesHeight) return none;
4195   // if we already have rope tile there, there is no reason to add another one
4196   if (aname == 'oRope') {
4197     if (isRopePlacedAt(mapx*16, mapy*16)) return none;
4198   }
4200   auto tile = CreateMapTile(mapx*16, mapy*16, aname);
4201   if (!tile) return none;
4202   if (!PutSpawnedMapTile(mapx*16, mapy*16, tile, putToGrid!optional)) {
4203     delete tile;
4204     tile = none;
4205   }
4207   return tile;
4211 final MapTile MakeMapTileAtPix (int xpix, int ypix, name aname, optional bool putToGrid) {
4212   // if we already have rope tile there, there is no reason to add another one
4213   if (aname == 'oRope') {
4214     if (isRopePlacedAt(xpix, ypix)) return none;
4215   }
4217   auto tile = CreateMapTile(xpix, ypix, aname);
4218   if (!tile) return none;
4219   if (!PutSpawnedMapTile(xpix, ypix, tile, putToGrid!optional)) {
4220     delete tile;
4221     tile = none;
4222   }
4224   return tile;
4228 final MapTile MakeMapRopeTileAt (int x0, int y0) {
4229   // if we already have rope tile there, there is no reason to add another one
4230   if (isRopePlacedAt(x0, y0)) return none;
4232   auto tile = CreateMapTile(x0, y0, 'oRope');
4233   if (!PutSpawnedMapTile(x0, y0, tile, putToGrid:true)) {
4234     delete tile;
4235     tile = none;
4236   }
4238   return tile;
4242 // ////////////////////////////////////////////////////////////////////////// //
4243 final MapBackTile CreateBgTile (name sprName, optional int atx0, optional int aty0, optional int aw, optional int ah) {
4244   BackTileImage img = bgtileStore[sprName];
4245   auto res = SpawnObject(MapBackTile);
4246   res.global = global;
4247   res.level = self;
4248   res.bgt = img;
4249   res.bgtName = sprName;
4250   if (specified_atx0) res.tx0 = atx0;
4251   if (specified_aty0) res.ty0 = aty0;
4252   if (specified_aw) res.w = aw;
4253   if (specified_ah) res.h = ah;
4254   if (!res.initialize()) FatalError("cannot create background tile with image '%n'", sprName);
4255   return res;
4259 // ////////////////////////////////////////////////////////////////////////// //
4261 background The background asset from which the new tile will be extracted.
4262 left The x coordinate of the left of the new tile, relative to the background asset's top left corner.
4263 top The y coordinate of the top of the new tile, relative to the background assets top left corner.
4264 width The width of the tile.
4265 height The height of the tile.
4266 x The x position in the room to place the tile.
4267 y The y position in the room to place the tile.
4268 depth The depth at which to place the tile.
4270 final void MakeMapBackTile (name bgname, int left, int top, int width, int height, int x, int y, int depth) {
4271   if (width < 1 || height < 1 || !bgname) return;
4272   auto bgt = bgtileStore[bgname];
4273   if (!bgt) FatalError("cannot load background '%n'", bgname);
4274   MapBackTile bt = SpawnObject(MapBackTile);
4275   bt.global = global;
4276   bt.level = self;
4277   bt.objName = bgname;
4278   bt.bgt = bgt;
4279   bt.bgtName = bgname;
4280   bt.fltx = x;
4281   bt.flty = y;
4282   bt.tx0 = left;
4283   bt.ty0 = top;
4284   bt.w = width;
4285   bt.h = height;
4286   bt.depth = depth;
4287   // find a place for it
4288   if (!backtiles) {
4289     backtiles = bt;
4290     return;
4291   }
4292   // back tiles with the highest depth should come first
4293   MapBackTile ct = backtiles, cprev = none;
4294   while (ct && depth <= ct.depth) { cprev = ct; ct = ct.next; }
4295   // insert before ct
4296   if (cprev) {
4297     bt.next = cprev.next;
4298     cprev.next = bt;
4299   } else {
4300     bt.next = backtiles;
4301     backtiles = bt;
4302   }
4306 // ////////////////////////////////////////////////////////////////////////// //
4307 final MapObject SpawnMapObjectWithClass (class!MapObject oclass) {
4308   if (!oclass) return none;
4310   MapObject obj = SpawnObject(oclass);
4311   if (!obj) FatalError("cannot create map object '%n'", GetClassName(oclass));
4313   //if (aname == 'oShells' || aname == 'oShellSingle' || aname == 'oRopePile') writeln("*** CREATED '", aname, "'");
4315   obj.global = global;
4316   obj.level = self;
4317   obj.objId = ++lastUsedObjectId;
4319   return obj;
4323 final MapObject SpawnMapObject (name aname) {
4324   if (!aname) return none;
4325   auto res = SpawnMapObjectWithClass(findGameObjectClassByName(aname));
4326   if (res && !res.objType) res.objType = aname; // just in case
4327   return res;
4331 final MapObject PutSpawnedMapObject (int x, int y, MapObject obj) {
4332   if (!obj /*|| obj.global || obj.level*/) return none; // oops
4334   obj.fltx = x;
4335   obj.flty = y;
4336   if (!obj.initialize()) { delete obj; return none; } // not fatal
4338   insertObject(obj);
4340   return obj;
4344 final MapObject MakeMapObject (int x, int y, name aname) {
4345   MapObject obj = SpawnMapObject(aname);
4346   obj = PutSpawnedMapObject(x, y, obj);
4347   return obj;
4351 // ////////////////////////////////////////////////////////////////////////// //
4352 int winCutSceneTimer = -1;
4353 int winVolcanoTimer = -1;
4354 int winCutScenePhase = 0;
4355 int winSceneDrawStatus = 0;
4356 int winMoneyCount = 0;
4357 int winTime;
4358 bool winFadeOut = false;
4359 int winFadeLevel = 0;
4360 int winCutsceneSkip = 0; // 1: waiting for pay release; 2: pay released, do skip
4361 bool winCutsceneSwitchToNext = false;
4364 void startWinCutscene () {
4365   global.hasParachute = false;
4366   shakeLeft = 0;
4367   winCutsceneSwitchToNext = false;
4368   winCutsceneSkip = 0;
4369   isKeyPressed(GameConfig::Key.Pay);
4370   isKeyReleased(GameConfig::Key.Pay);
4372   auto olddel = ImmediateDelete;
4373   ImmediateDelete = false;
4374   clearWholeLevel();
4376   createEnd1Room();
4377   fixWallTiles();
4378   addBackgroundGfxDetails();
4380   levBGImgName = 'bgCave';
4381   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
4383   blockWaterChecking = true;
4384   fixLiquidTop();
4385   cleanDeadTiles();
4387   ImmediateDelete = olddel;
4388   CollectGarbage(true); // destroy delayed objects too
4390   if (dumpGridStats) objGrid.dumpStats();
4392   playerExited = false; // just in case
4393   playerExitDoor = none;
4395   osdClear();
4397   setupGhostTime();
4398   global.stopMusic();
4400   inWinCutscene = 1;
4401   winCutSceneTimer = -1;
4402   winCutScenePhase = 0;
4404   /+
4405   if (global.config.gameMode != GameConfig::GameMode.Vanilla) {
4406     if (global.config.bizarre) {
4407       global.yasmScore = 1;
4408       global.config.bizarrePlusTitle = true;
4409     }
4411     array!MapTile toReplace;
4412     forEachTile(delegate bool (MapTile t) {
4413       if (t.objType == 'oGTemple' ||
4414           t.objType == 'oIce' ||
4415           t.objType == 'oDark' ||
4416           t.objType == 'oBrick' ||
4417           t.objType == 'oLush')
4418       {
4419         toReplace[$] = t;
4420       }
4421       return false;
4422     });
4424     foreach (MapTile t; miscTileGrid.allObjects()) {
4425       if (t.objType == 'oGTemple' ||
4426           t.objType == 'oIce' ||
4427           t.objType == 'oDark' ||
4428           t.objType == 'oBrick' ||
4429           t.objType == 'oLush')
4430       {
4431         toReplace[$] = t;
4432       }
4433     }
4435     foreach (MapTile t; toReplace) {
4436       if (t.iy < 192) {
4437         t.cleanDeath = true;
4438             if (rand(1,120) == 1) instance_change(oGTemple, false);
4439         else if (rand(1,100) == 1) instance_change(oIce, false);
4440         else if (rand(1,90) == 1) instance_change(oDark, false);
4441         else if (rand(1,80) == 1) instance_change(oBrick, false);
4442         else if (rand(1,70) == 1) instance_change(oLush, false);
4443           }
4444       }
4445       with (oBrick)
4446       {
4447           if (y &lt; 192)
4448           {
4449               cleanDeath = true;
4450               if (rand(1,5) == 1) instance_change(oLush, false);
4451           }
4452       }
4453   }
4454   +/
4455   //!instance_create(0, 0, oBricks);
4457   //shakeToggle = false;
4458   //oPDummy.status = 2;
4460   //timer = 0;
4462   /+
4463   if (global.kaliPunish &gt;= 2) {
4464       instance_create(oPDummy.x, oPDummy.y+2, oBall2);
4465       obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
4466       obj.linkVal = 1;
4467       obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
4468       obj.linkVal = 2;
4469       obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
4470       obj.linkVal = 3;
4471       obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
4472       obj.linkVal = 4;
4473   }
4474   +/
4478 void startWinCutsceneVolcano () {
4479   global.hasParachute = false;
4480   /*
4481   writeln("VOLCANO HOLD 0: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4482   writeln("VOLCANO PICK 0: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4483   */
4485   shakeLeft = 0;
4486   winCutsceneSwitchToNext = false;
4487   auto olddel = ImmediateDelete;
4488   ImmediateDelete = false;
4489   clearWholeLevel();
4491   levBGImgName = '';
4492   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
4494   blockWaterChecking = true;
4496   ImmediateDelete = olddel;
4497   CollectGarbage(true); // destroy delayed objects too
4499   spawnPlayerAt(2*16+8, 11*16+8);
4500   player.dir = MapEntity::Dir.Right;
4502   playerExited = false; // just in case
4503   playerExitDoor = none;
4505   osdClear();
4507   setupGhostTime();
4508   global.stopMusic();
4510   inWinCutscene = 2;
4511   winCutSceneTimer = -1;
4512   winCutScenePhase = 0;
4514   MakeMapTile(0, 0, 'oEnd2BG');
4515   realViewStart.x = 0;
4516   realViewStart.y = 0;
4517   viewStart.x = 0;
4518   viewStart.y = 0;
4520   viewMin.x = 0;
4521   viewMin.y = 0;
4522   viewMax.x = 320;
4523   viewMax.y = 240;
4525   player.dead = false;
4526   player.active = true;
4527   player.visible = false;
4528   player.removeBallAndChain(temp:true);
4529   player.stunned = false;
4530   player.status = MapObject::FALLING;
4531   if (player.holdItem) player.holdItem.visible = false;
4532   player.fltx = 320/2;
4533   player.flty = 0;
4535   /*
4536   writeln("VOLCANO HOLD 1: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4537   writeln("VOLCANO PICK 1: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4538   */
4542 void startWinCutsceneWinFall () {
4543   global.hasParachute = false;
4544   /*
4545   writeln("FALL HOLD 0: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4546   writeln("FALL PICK 0: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4547   */
4549   shakeLeft = 0;
4550   winCutsceneSwitchToNext = false;
4552   auto olddel = ImmediateDelete;
4553   ImmediateDelete = false;
4554   clearWholeLevel();
4556   createEnd3Room();
4557   setMenuTilesVisible(false);
4558   //fixWallTiles();
4559   //addBackgroundGfxDetails();
4561   levBGImgName = '';
4562   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
4564   blockWaterChecking = true;
4565   fixLiquidTop();
4566   cleanDeadTiles();
4568   ImmediateDelete = olddel;
4569   CollectGarbage(true); // destroy delayed objects too
4571   if (dumpGridStats) objGrid.dumpStats();
4573   playerExited = false; // just in case
4574   playerExitDoor = none;
4576   osdClear();
4578   setupGhostTime();
4579   global.stopMusic();
4581   inWinCutscene = 3;
4582   winCutSceneTimer = -1;
4583   winCutScenePhase = 0;
4585   player.dead = false;
4586   player.active = true;
4587   player.visible = false;
4588   player.removeBallAndChain(temp:true);
4589   player.stunned = false;
4590   player.status = MapObject::FALLING;
4591   if (player.holdItem) player.holdItem.visible = false;
4592   player.fltx = 320/2;
4593   player.flty = 0;
4595   winSceneDrawStatus = 0;
4596   winMoneyCount = 0;
4598   winFadeOut = false;
4599   winFadeLevel = 0;
4601   /*
4602   writeln("FALL HOLD 1: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4603   writeln("FALL PICK 1: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4604   */
4608 void setGameOver () {
4609   if (inWinCutscene) {
4610     player.visible = false;
4611     player.removeBallAndChain(temp:true);
4612     if (player.holdItem) player.holdItem.visible = false;
4613   }
4614   player.dead = true;
4615   if (inWinCutscene > 0) {
4616     winFadeOut = true;
4617     winFadeLevel = 255;
4618     winSceneDrawStatus = 8;
4619   }
4623 MapTile findEndPlatTile () {
4624   return forEachTile(delegate bool (MapTile t) { return (t isa MapTileEndPlat); }, castClass:MapTileEndPlat);
4628 MapObject findBigTreasure () {
4629   return forEachObject(delegate bool (MapObject o) { return (o isa MapObjectBigTreasure); }, castClass:MapObjectBigTreasure);
4633 void setMenuTilesVisible (bool vis) {
4634   if (vis) {
4635     forEachTile(delegate bool (MapTile t) {
4636       if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4637         t.invisible = false;
4638       }
4639       return false;
4640     });
4641   } else {
4642     forEachTile(delegate bool (MapTile t) {
4643       if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4644         t.invisible = true;
4645       }
4646       return false;
4647     });
4648   }
4652 void setMenuTilesOnTop () {
4653   forEachTile(delegate bool (MapTile t) {
4654     if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4655       t.depth = 1;
4656     }
4657     return false;
4658   });
4662 void winCutscenePlayerControl (PlayerPawn plr) {
4663   auto payPress = isKeyPressed(GameConfig::Key.Pay);
4664   auto payRelease = isKeyReleased(GameConfig::Key.Pay);
4666   switch (winCutsceneSkip) {
4667     case 0: // nothing was pressed
4668       if (payPress) winCutsceneSkip = 1;
4669       break;
4670     case 1: // waiting for pay release
4671       if (payRelease) winCutsceneSkip = 2;
4672       break;
4673     case 2: // pay released, do skip
4674       setGameOver();
4675       return;
4676   }
4678   // first winning room
4679   if (inWinCutscene == 1) {
4680     if (plr.ix < 448+8) {
4681       plr.kRight = true;
4682       return;
4683     }
4685     // waiting for chest to open
4686     if (winCutScenePhase == 0) {
4687       winCutSceneTimer = 120/2;
4688       winCutScenePhase = 1;
4689       return;
4690     }
4692     // spawn big idol
4693     if (winCutScenePhase == 1) {
4694       if (--winCutSceneTimer == 0) {
4695         winCutScenePhase = 2;
4696         winCutSceneTimer = 20;
4697         forEachObject(delegate bool (MapObject o) {
4698           if (o isa MapObjectBigChest) {
4699             o.setSprite(global.config.gameMode == GameConfig::GameMode.Vanilla ? 'sBigChestOpen' : 'sBigChestOpen2');
4700             auto treasure = MakeMapObject(o.ix, o.iy, 'oBigTreasure');
4701             if (treasure) {
4702               treasure.yVel = -4;
4703               treasure.xVel = -3;
4704               o.playSound('sndClick');
4705               //!!!if (global.config.gameMode != GameConfig::GameMode.Vanilla) scrSprayGems(oBigChest.x+24, oBigChest.y+24);
4706             }
4707           }
4708           return false;
4709         });
4710       }
4711       return;
4712     }
4714     // lava pump wait
4715     if (winCutScenePhase == 2) {
4716       if (--winCutSceneTimer == 0) {
4717         winCutScenePhase = 3;
4718         winCutSceneTimer = 50;
4719       }
4720       return;
4721     }
4723     // lava pump start
4724     if (winCutScenePhase == 3) {
4725       auto ep = findEndPlatTile();
4726       if (ep) MakeMapObject(ep.ix+global.randOther(0, 80), /*ep.iy*/192+32, 'oBurn');
4727       if (--winCutSceneTimer == 0) {
4728         winCutScenePhase = 4;
4729         winCutSceneTimer = 10;
4730         if (ep) MakeMapObject(ep.ix, ep.iy+30, 'oLavaSpray');
4731         scrShake(9999);
4732       }
4733       return;
4734     }
4736     // lava pump first accel
4737     if (winCutScenePhase == 4) {
4738       if (--winCutSceneTimer == 0) {
4739         forEachObject(delegate bool (MapObject o) {
4740           if (o isa MapObjectLavaSpray) o.yAcc = -0.1;
4741           return false;
4742         });
4743       }
4744     }
4746     // lava pump complete
4747     if (winCutScenePhase == 5) {
4748       if (--winCutSceneTimer == 0) {
4749         //if (oLavaSpray) oLavaSpray.yAcc = -0.1;
4750         startWinCutsceneVolcano();
4751       }
4752       return;
4753     }
4754     return;
4755   }
4758   // volcano room
4759   if (inWinCutscene == 2) {
4760     plr.flty = 0;
4762     // initialize
4763     if (winCutScenePhase == 0) {
4764       winCutSceneTimer = 50;
4765       winCutScenePhase = 1;
4766       winVolcanoTimer = 10;
4767       return;
4768     }
4770     if (winVolcanoTimer > 0) {
4771       if (--winVolcanoTimer == 0) {
4772         MakeMapObject(224+global.randOther(0,48), 144+global.randOther(0,8), 'oVolcanoFlame');
4773         winVolcanoTimer = global.randOther(10, 20);
4774       }
4775     }
4777     // plr sil
4778     if (winCutScenePhase == 1) {
4779       if (--winCutSceneTimer == 0) {
4780         winCutSceneTimer = 30;
4781         winCutScenePhase = 2;
4782         auto sil = MakeMapObject(240, 132, 'oPlayerSil');
4783         //sil.xVel = -6;
4784         //sil.yVel = -8;
4785       }
4786       return;
4787     }
4789     // treasure sil
4790     if (winCutScenePhase == 2) {
4791       if (--winCutSceneTimer == 0) {
4792         winCutScenePhase = 3;
4793         auto sil = MakeMapObject(240, 132, 'oTreasureSil');
4794         //sil.xVel = -6;
4795         //sil.yVel = -8;
4796       }
4797       return;
4798     }
4800     return;
4801   }
4803   // winning camel room
4804   if (inWinCutscene == 3) {
4805     //if (!player.holdItem)  writeln("SCENE 3: LOST ITEM!");
4807     if (!plr.visible) plr.flty = -32;
4809     // initialize
4810     if (winCutScenePhase == 0) {
4811       winCutSceneTimer = 50;
4812       winCutScenePhase = 1;
4813       return;
4814     }
4816     // fall sound
4817     if (winCutScenePhase == 1) {
4818       if (--winCutSceneTimer == 0) {
4819         winCutSceneTimer = 50;
4820         winCutScenePhase = 2;
4821         plr.playSound('sndPFall');
4822         plr.visible = true;
4823         plr.active = true;
4824         writeln("MUST BE CHAINED: ", plr.mustBeChained);
4825         if (plr.mustBeChained) {
4826           plr.removeBallAndChain(temp:true);
4827           plr.spawnBallAndChain();
4828         }
4829         /*
4830         writeln("HOLD: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4831         writeln("PICK: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4832         */
4833         if (!player.holdItem && player.pickedItem) player.scrSwitchToPocketItem(forceIfEmpty:false);
4834         if (player.holdItem) {
4835           player.holdItem.visible = true;
4836           player.holdItem.canLiveOutsideOfLevel = true;
4837           writeln("HOLD ITEM: '", GetClassName(player.holdItem.Class), "'");
4838         }
4839         plr.status == MapObject::FALLING;
4840         global.plife += 99; // just in case
4841       }
4842       return;
4843     }
4845     if (winCutScenePhase == 2) {
4846       auto ball = plr.getMyBall();
4847       if (ball && plr.holdItem != ball) {
4848         ball.teleportTo(plr.fltx, plr.flty+8);
4849         ball.yVel = 6;
4850         ball.myGrav = 0.6;
4851       }
4852       if (plr.status == MapObject::STUNNED || plr.stunned) {
4853         //alarm[0] = 70;
4854         //alarm[1] = 50;
4855         //status = GETUP;
4856         auto treasure = MakeMapObject(144+16+8, -32, 'oBigTreasure');
4857         if (treasure) treasure.depth = 1;
4858         winCutScenePhase = 3;
4859         plr.stunTimer = 30;
4860         plr.playSound('sndTFall');
4861       }
4862       return;
4863     }
4865     if (winCutScenePhase == 3) {
4866       if (plr.status != MapObject::STUNNED && !plr.stunned) {
4867         auto bt = findBigTreasure();
4868         if (bt) {
4869           if (bt.yVel == 0) {
4870             //plr.yVel = -4;
4871             //plr.status = MapObject::JUMPING;
4872             plr.kJump = true;
4873             plr.kJumpPressed = true;
4874             winCutScenePhase = 4;
4875             winCutSceneTimer = 50;
4876           }
4877         }
4878       }
4879       return;
4880     }
4882     if (winCutScenePhase == 4) {
4883       if (--winCutSceneTimer == 0) {
4884         setMenuTilesVisible(true);
4885         winCutScenePhase = 5;
4886         winSceneDrawStatus = 1;
4887         global.playMusic('musVictory', loop:false);
4888         winCutSceneTimer = 50;
4889       }
4890       return;
4891     }
4893     if (winCutScenePhase == 5) {
4894       if (winSceneDrawStatus == 3) {
4895         int money = stats.money;
4896         if (winMoneyCount < money) {
4897           if (money-winMoneyCount > 1000) {
4898             winMoneyCount += 1000;
4899           } else if (money-winMoneyCount > 100) {
4900             winMoneyCount += 100;
4901           } else if (money-winMoneyCount > 10) {
4902             winMoneyCount += 10;
4903           } else {
4904             ++winMoneyCount;
4905           }
4906         }
4907         if (winMoneyCount >= money) {
4908           winMoneyCount = money;
4909           ++winSceneDrawStatus;
4910         }
4911         return;
4912       }
4914       if (winSceneDrawStatus == 7) {
4915         winFadeOut = true;
4916         winFadeLevel += 1;
4917         if (winFadeLevel >= 255) {
4918           ++winSceneDrawStatus;
4919           winCutSceneTimer = 30*30;
4920         }
4921         return;
4922       }
4924       if (winSceneDrawStatus == 8) {
4925         if (--winCutSceneTimer == 0) {
4926           setGameOver();
4927         }
4928         return;
4929       }
4931       if (--winCutSceneTimer == 0) {
4932         ++winSceneDrawStatus;
4933         winCutSceneTimer = 50;
4934       }
4935     }
4937     return;
4938   }
4942 // ////////////////////////////////////////////////////////////////////////// //
4943 void renderWinCutsceneOverlay () {
4944   if (inWinCutscene == 3) {
4945     if (winSceneDrawStatus > 0) {
4946       Video.color = 0xff_ff_ff;
4947       sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
4948       //draw_set_color(txtCol);
4949       drawTextAt(64, 32, "YOU MADE IT!");
4951       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4952       if (global.config.gameMode == GameConfig::GameMode.Vanilla) {
4953         Video.color = 0xff_ff_00; //draw_set_color(c_yellow);
4954         drawTextAt(64, 48, "Classic Mode done!");
4955       } else {
4956         Video.color = 0x00_80_80; //draw_set_color(c_teal);
4957         if (global.config.bizarrePlus) drawTextAt(64, 48, "Bizarre Mode Plus done!");
4958         else drawTextAt(64, 48, "Bizarre Mode done!");
4959         //draw_set_color(c_white);
4960       }
4961       if (!global.usedShortcut) {
4962         Video.color = 0xc0_c0_c0; //draw_set_color(c_silver);
4963         drawTextAt(64, 56, "No shortcuts used!");
4964         //draw_set_color(c_yellow);
4965       }
4966     }
4968     if (winSceneDrawStatus > 1) {
4969       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4970       //draw_set_color(txtCol);
4971       Video.color = 0xff_ff_ff;
4972       drawTextAt(64, 64, "FINAL SCORE:");
4973     }
4975     if (winSceneDrawStatus > 2) {
4976       sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
4977       Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4978       drawTextAt(64, 72, va("$%d", winMoneyCount));
4979     }
4981     if (winSceneDrawStatus > 4) {
4982       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4983       Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4984       drawTextAt(64, 96, va("Time: %s", time2str(winTime/30)));
4985       /*
4986       draw_set_color(c_white);
4987       if (s &lt; 10) draw_text(96+24, 96, string(m) + ":0" + string(s));
4988       else draw_text(96+24, 96, string(m) + ":" + string(s));
4989       */
4990     }
4992     if (winSceneDrawStatus > 5) {
4993       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4994       Video.color = 0xff_ff_ff; //draw_set_color(txtCol);
4995       drawTextAt(64, 96+8, "Kills: ");
4996       Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4997       drawTextAt(96+24, 96+8, va("%s", stats.countKills()));
4998     }
5000     if (winSceneDrawStatus > 6) {
5001       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
5002       Video.color = 0xff_ff_ff; //draw_set_color(txtCol);
5003       drawTextAt(64, 96+16, "Saves: ");
5004       Video.color = 0xff_ff_ff; //draw_set_color(c_white);
5005       drawTextAt(96+24, 96+16, va("%s", stats.damselsSaved));
5006     }
5008     if (winFadeOut) {
5009       Video.color = (255-clamp(winFadeLevel, 0, 255))<<24;
5010       Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
5011     }
5013     if (winSceneDrawStatus == 8) {
5014       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
5015       Video.color = 0xff_ff_ff; //draw_set_color(c_white);
5016       string lastString;
5017       if (global.config.gameMode == GameConfig::GameMode.Vanilla) {
5018         Video.color = 0xff_ff_00; //draw_set_color(c_yellow);
5019         lastString = "YOU SHALL BE REMEMBERED AS A HERO.";
5020       } else {
5021         Video.color = 0x00_ff_ff;
5022         if (global.config.bizarrePlus) lastString = "ANOTHER LEGENDARY ADVENTURE!";
5023         else lastString = "YOUR DISCOVERIES WILL BE CELEBRATED!";
5024       }
5025       auto strLen = lastString.length*8;
5026       int n = 320-strLen;
5027       n = trunc(ceil(n/2.0));
5028       drawTextAt(n, 116, lastString);
5029     }
5030   }
5034 // ////////////////////////////////////////////////////////////////////////// //
5035 #include "roomTitle.vc"
5036 #include "roomTrans1.vc"
5037 #include "roomTrans2.vc"
5038 #include "roomTrans3.vc"
5039 #include "roomTrans4.vc"
5040 #include "roomOlmec.vc"
5041 #include "roomEnd.vc"
5042 #include "roomIntro.vc"
5043 #include "roomTutorial.vc"
5044 #include "roomScores.vc"
5045 #include "roomStars.vc"
5046 #include "roomSun.vc"
5047 #include "roomMoon.vc"
5050 // ////////////////////////////////////////////////////////////////////////// //
5051 #include "packages/Generator/loadRoomGens.vc"
5052 #include "packages/Generator/loadEntityGens.vc"