fixed current level number on transition screen
[k8vacspelynky.git] / GameLevel.vc
blob8f2046ff7b9b6f2103d7d1ba9ec611cfd9383c1a
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;
70 LevelGen::RType[MaxTilesWidth, MaxTilesHeight] roomType;
72 enum LevelKind {
73   Normal,
74   Transition,
75   Title,
76   Tutorial,
77   Scores,
78   Stars,
79   Sun,
80   Moon,
81   //Final,
83 LevelKind levelKind = LevelKind.Normal;
85 array!MapTile allEnters;
86 array!MapTile allExits;
89 int startRoomX, startRoomY;
90 int endRoomX, endRoomY;
92 PlayerPawn player;
93 transient bool playerExited;
94 transient MapEntity playerExitDoor;
95 transient bool disablePlayerThink = false;
96 transient int maxPlayingTime; // in seconds
97 int levelStartTime;
98 int levelEndTime;
100 int ghostTimeLeft;
101 int musicFadeTimer;
102 bool ghostSpawned; // to speed up some checks
103 bool resetBMCOG = false;
104 int udjatAlarm;
107 // FPS, i.e. incremented by 30 in one second
108 int time; // in frames
109 int lastUsedObjectId;
110 transient int lastRenderTime = -1;
111 transient int pausedTime;
113 MapEntity deadItemsHead;
115 // screen shake variables
116 int shakeLeft;
117 IVec2D shakeOfs;
118 IVec2D shakeDir;
120 // set this before calling `fixCamera()`
121 // dimensions should be real, not scaled up/down
122 transient int viewWidth, viewHeight;
123 // room bounds, not scaled
124 IVec2D viewMin, viewMax;
126 // for Olmec level cinematics
127 IVec2D cameraSlideToDest;
128 IVec2D cameraSlideToCurr;
129 IVec2D cameraSlideToSpeed; // !0: slide
130 int cameraSlideToPlayer;
131 // `fixCamera()` will set the following
132 // coordinates will be real too (with scale applied)
133 // shake is not applied
134 transient IVec2D viewStart; // with `player.viewOffset`
135 private transient IVec2D realViewStart; // without `player.viewOffset`
137 transient int framesProcessedFromLastClear;
139 transient int BuildYear;
140 transient int BuildMonth;
141 transient int BuildDay;
142 transient int BuildHour;
143 transient int BuildMin;
144 transient string BuildDateString;
147 final string getBuildDateString () {
148   if (!BuildYear) return BuildDateString;
149   if (BuildDateString) return BuildDateString;
150   BuildDateString = va("%d-%02d-%02d %02d:%02d", BuildYear, BuildMonth, BuildDay, BuildHour, BuildMin);
151   return BuildDateString;
155 final void cameraSlideToPoint (int dx, int dy, int speedx, int speedy) {
156   cameraSlideToPlayer = 0;
157   cameraSlideToDest.x = dx;
158   cameraSlideToDest.y = dy;
159   cameraSlideToSpeed.x = abs(speedx);
160   cameraSlideToSpeed.y = abs(speedy);
161   cameraSlideToCurr.x = cameraCurrX;
162   cameraSlideToCurr.y = cameraCurrY;
166 final void cameraReturnToPlayer () {
167   if (!cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y)) {
168     cameraSlideToCurr.x = cameraCurrX;
169     cameraSlideToCurr.y = cameraCurrY;
170     if (cameraSlideToSpeed.x && abs(cameraSlideToSpeed.x) < 8) cameraSlideToSpeed.x = 8;
171     if (cameraSlideToSpeed.y && abs(cameraSlideToSpeed.y) < 8) cameraSlideToSpeed.y = 8;
172     cameraSlideToPlayer = 1;
173   }
177 // if `frameSkip` is `true`, there are more frames waiting
178 // (i.e. you may skip rendering and such)
179 transient void delegate (bool frameSkip) onBeforeFrame;
180 transient void delegate (bool frameSkip) onAfterFrame;
182 transient void delegate () onCameraTeleported;
184 transient void delegate () onLevelExitedCB;
186 // this will be called in-between frames, and
187 // `frameTime` is [0..1)
188 transient void delegate (float frameTime) onInterFrame;
190 final int bizRoomStyle { get { return (lg ? lg.bizRoomStyle : 0); } }
193 final bool isNormalLevel () { return (levelKind == LevelKind.Normal); }
194 final bool isTitleRoom () { return (levelKind == LevelKind.Title); }
195 final bool isTutorialRoom () { return (levelKind == LevelKind.Tutorial); }
196 final bool isTransitionRoom () { return (levelKind == LevelKind.Transition); }
198 bool isHUDEnabled () {
199   if (inWinCutscene) return false;
200   if (lg.finalBossLevel) return true;
201   if (isNormalLevel()) return true;
202   // allow HUD in challenge chambers
203   return false;
207 // ////////////////////////////////////////////////////////////////////////// //
208 // stats
209 void addDeath (name aname) { if (isNormalLevel()) stats.addDeath(aname); }
211 int starsKills;
212 int sunScore;
213 int moonScore;
214 int moonTimer;
216 void addKill (name aname, optional bool telefrag) {
217        if (isNormalLevel()) stats.addKill(aname, telefrag!optional);
218   else if (aname == 'Shopkeeper' && levelKind == LevelKind.Stars) { ++stats.starsKills; ++starsKills; }
221 void addCollect (name aname, optional int amount) { if (isNormalLevel()) stats.addCollect(aname, amount!optional); }
223 void addDamselSaved () { if (isNormalLevel()) stats.addDamselSaved(); }
224 void addIdolStolen () { if (isNormalLevel()) stats.addIdolStolen(); }
225 void addIdolConverted () { if (isNormalLevel()) stats.addIdolConverted(); }
226 void addCrystalIdolStolen () { if (isNormalLevel()) stats.addCrystalIdolStolen(); }
227 void addCrystalIdolConverted () { if (isNormalLevel()) stats.addCrystalIdolConverted(); }
228 void addGhostSummoned () { if (isNormalLevel()) stats.addGhostSummoned(); }
231 // ////////////////////////////////////////////////////////////////////////// //
232 static final string time2str (int time) {
233   int secs = time%60; time /= 60;
234   int mins = time%60; time /= 60;
235   int hours = time%24; time /= 24;
236   int days = time;
237   if (days) return va("%d DAYS, %d:%02d:%02d", days, hours, mins, secs);
238   if (hours) return va("%d:%02d:%02d", hours, mins, secs);
239   return va("%02d:%02d", mins, secs);
243 // ////////////////////////////////////////////////////////////////////////// //
244 final int tilesWidth () { return lg.levelRoomWidth*RoomGen::Width+2; }
245 final int tilesHeight () { return (lg.finalBossLevel ? 55 : lg.levelRoomHeight*RoomGen::Height+2); }
248 // ////////////////////////////////////////////////////////////////////////// //
249 protected void resetGameInternal () {
250   if (player) player.removeBallAndChain();
251   resetBMCOG = false;
252   inWinCutscene = 0;
253   shakeLeft = 0;
254   udjatAlarm = 0;
255   starsKills = 0;
256   sunScore = 0;
257   moonScore = 0;
258   moonTimer = 0;
259   damselSaved = 0;
260   xmoney = 0;
261   collectCounter = 0;
262   levelMoneyStart = 0;
263   if (player) {
264     player.removeBallAndChain();
265     auto hi = player.holdItem;
266     player.holdItem = none;
267     if (hi) hi.instanceRemove();
268     hi = player.pickedItem;
269     player.pickedItem = none;
270     if (hi) hi.instanceRemove();
271   }
272   time = 0;
273   lastRenderTime = -1;
274   levelStartTime = 0;
275   levelEndTime = 0;
276   global.resetGame();
277   stats.clearGameTotals();
281 // this won't generate a level yet
282 void restartGame () {
283   resetGameInternal();
284   if (global.startMoney > 0) stats.setMoneyCheat();
285   stats.setMoney(global.startMoney);
286   levelKind = LevelKind.Normal;
290 // complement function to `restart game`
291 void generateNormalLevel () {
292   generateLevel();
293   centerViewAtPlayer();
297 void restartTitle () {
298   resetGameInternal();
299   stats.setMoney(0);
300   createSpecialLevel(LevelKind.Title, &createTitleRoom, 'musTitle');
301   global.plife = 9999;
302   global.bombs = 0;
303   global.rope = 0;
304   global.arrows = 0;
305   global.sgammo = 0;
309 void restartTutorial () {
310   resetGameInternal();
311   stats.setMoney(0);
312   createSpecialLevel(LevelKind.Tutorial, &createTutorialRoom, 'musCave');
313   global.plife = 4;
314   global.bombs = 0;
315   global.rope = 4;
316   global.arrows = 0;
317   global.sgammo = 0;
321 void restartScores () {
322   resetGameInternal();
323   stats.setMoney(0);
324   createSpecialLevel(LevelKind.Scores, &createScoresRoom, 'musTitle');
325   global.plife = 4;
326   global.bombs = 0;
327   global.rope = 0;
328   global.arrows = 0;
329   global.sgammo = 0;
333 void restartStarsRoom () {
334   resetGameInternal();
335   stats.setMoney(0);
336   createSpecialLevel(LevelKind.Stars, &createStarsRoom, '');
337   global.plife = 8;
338   global.bombs = 0;
339   global.rope = 0;
340   global.arrows = 0;
341   global.sgammo = 0;
345 void restartSunRoom () {
346   resetGameInternal();
347   stats.setMoney(0);
348   createSpecialLevel(LevelKind.Sun, &createSunRoom, '');
349   global.plife = 8;
350   global.bombs = 0;
351   global.rope = 0;
352   global.arrows = 0;
353   global.sgammo = 0;
357 void restartMoonRoom () {
358   resetGameInternal();
359   stats.setMoney(0);
360   createSpecialLevel(LevelKind.Moon, &createMoonRoom, '');
361   global.plife = 8;
362   global.bombs = 0;
363   global.rope = 0;
364   global.arrows = 100;
365   global.sgammo = 0;
369 // ////////////////////////////////////////////////////////////////////////// //
370 // generate angry shopkeeper at exit if murderer or thief
371 void generateAngryShopkeepers () {
372   if (global.murderer || global.thiefLevel > 0) {
373     foreach (MapTile e; allExits) {
374       auto obj = MonsterShopkeeper(MakeMapObject(e.ix, e.iy, 'oShopkeeper'));
375       if (obj) {
376         obj.style = 'Bounty Hunter';
377         obj.status = MapObject::PATROL;
378       }
379     }
380   }
384 // ////////////////////////////////////////////////////////////////////////// //
385 final void resetRoomBounds () {
386   viewMin.x = 0;
387   viewMin.y = 0;
388   viewMax.x = tilesWidth*16;
389   viewMax.y = tilesHeight*16;
390   // Great Lake is bottomless (nope)
391   //if (global.lake == 1) viewMax.y -= 16;
392   //writeln("ROOM BOUNDS: (", viewMax.x, "x", viewMax.y, ")");
396 final void setRoomBounds (int x0, int y0, int x1, int y1) {
397   viewMin.x = x0;
398   viewMin.y = y0;
399   viewMax.x = x1+16;
400   viewMax.y = y1+16;
404 // ////////////////////////////////////////////////////////////////////////// //
405 struct OSDMessage {
406   string msg;
407   float timeout; // seconds
408   float starttime; // for active
409   bool active; // true: timeout is `GetTickCount()` dismissing time
412 array!OSDMessage msglist; // [0]: current one
415 private final void osdCheckTimeouts () {
416   auto stt = GetTickCount();
417   while (msglist.length) {
418     if (!msglist[0].active) {
419       msglist[0].active = true;
420       msglist[0].starttime = stt;
421     }
422     if (msglist[0].starttime+msglist[0].timeout >= stt) break;
423     msglist.remove(0);
424   }
428 final bool osdHasMessage () {
429   osdCheckTimeouts();
430   return (msglist.length > 0);
434 final string osdGetMessage (out float timeLeft, out float timeStart) {
435   osdCheckTimeouts();
436   if (msglist.length == 0) { timeLeft = 0; return ""; }
437   auto stt = GetTickCount();
438   timeStart = msglist[0].starttime;
439   timeLeft = fmax(0, msglist[0].starttime+msglist[0].timeout-stt);
440   return msglist[0].msg;
444 final void osdClear () {
445   msglist.clear();
449 final void osdMessage (string msg, optional float timeout, optional bool urgent) {
450   if (!msg) return;
451   msg = global.expandString(msg);
452   if (!specified_timeout) timeout = 3.33;
453   // special message for shops
454   if (timeout == -666) {
455     if (!msg) return;
456     if (msglist.length && msglist[0].msg == msg) return;
457     if (msglist.length == 0 || msglist[0].msg != msg) {
458       osdClear();
459       msglist.length += 1;
460       msglist[0].msg = msg;
461     }
462     msglist[0].active = false;
463     msglist[0].timeout = 3.33;
464     osdCheckTimeouts();
465     return;
466   }
467   if (timeout < 0.1) return;
468   timeout = fmax(1.0, timeout);
469   //writeln("OSD: ", msg);
470   // find existing one, and bring it to the top
471   int oldidx = 0;
472   for (; oldidx < msglist.length; ++oldidx) {
473     if (msglist[oldidx].msg == msg) break; // i found her!
474   }
475   // duplicate?
476   if (oldidx < msglist.length) {
477     // yeah, move duplicate to the top
478     msglist[oldidx].timeout = fmax(timeout, msglist[oldidx].timeout);
479     msglist[oldidx].active = false;
480     if (urgent && oldidx != 0) {
481       timeout = msglist[oldidx].timeout;
482       msglist.remove(oldidx);
483       msglist.insert(0);
484       msglist[0].msg = msg;
485       msglist[0].timeout = timeout;
486       msglist[0].active = false;
487     }
488   } else if (urgent) {
489     msglist.insert(0);
490     msglist[0].msg = msg;
491     msglist[0].timeout = timeout;
492     msglist[0].active = false;
493   } else {
494     // new one
495     msglist.length += 1;
496     msglist[$-1].msg = msg;
497     msglist[$-1].timeout = timeout;
498     msglist[$-1].active = false;
499   }
500   osdCheckTimeouts();
504 // ////////////////////////////////////////////////////////////////////////// //
505 void setup (GameGlobal aGlobal, SpriteStore aSprStore, BackTileStore aBGTileStore) {
506   global = aGlobal;
507   sprStore = aSprStore;
508   bgtileStore = aBGTileStore;
510   lg = SpawnObject(LevelGen);
511   lg.global = global;
512   lg.level = self;
514   objGrid = SpawnObject(EntityGrid);
515   objGrid.setup(0, 0, MaxTilesWidth*16, MaxTilesHeight*16);
519 // stores should be set
520 void onLoaded () {
521   checkWater = true;
522   liquidTileCount = 0;
523   levBGImg = bgtileStore[levBGImgName];
524   foreach (MapEntity o; objGrid.allObjects()) {
525     o.onLoaded();
526     auto t = MapTile(o);
527     if (t && (t.lava || t.water)) ++liquidTileCount;
528   }
529   for (MapBackTile bt = backtiles; bt; bt = bt.next) bt.onLoaded();
530   if (player) player.onLoaded();
531   //FIXME
532   if (msglist.length) {
533     msglist[0].active = false;
534     msglist[0].timeout = 0.200;
535     osdCheckTimeouts();
536   }
537   if (lg && lg.musicName) global.playMusic(lg.musicName); else global.stopMusic();
541 // ////////////////////////////////////////////////////////////////////////// //
542 void pickedSpectacles () {
543   foreach (MapTile t; objGrid.allObjectsSafe(MapTile)) t.onGotSpectacles();
547 // ////////////////////////////////////////////////////////////////////////// //
548 #include "rgentile.vc"
549 #include "rgenobj.vc"
552 void onLevelExited () {
553   if (playerExitDoor isa TitleTileXTitle) {
554     playerExitDoor = none;
555     restartTitle();
556     return;
557   }
558   // title
559   if (isTitleRoom() || levelKind == LevelKind.Scores) {
560     if (playerExitDoor) processTitleExit(playerExitDoor);
561     playerExitDoor = none;
562     return;
563   }
564   if (isTutorialRoom()) {
565     playerExitDoor = none;
566     restartGame();
567     global.currLevel = 1;
568     generateNormalLevel();
569     return;
570   }
571   // challenges
572   if (levelKind == LevelKind.Stars || levelKind == LevelKind.Sun || levelKind == LevelKind.Moon) {
573     playerExitDoor = none;
574     levelEndTime = time;
575     if (onLevelExitedCB) onLevelExitedCB();
576     restartTitle();
577     return;
578   }
579   // normal level
580   if (isNormalLevel()) {
581     stats.maxLevelComplete = max(stats.maxLevelComplete, global.currLevel);
582     levelEndTime = time;
583     if (playerExitDoor) {
584       if (playerExitDoor.objType == 'oXGold') {
585         writeln("exiting to City Of Gold");
586         global.cityOfGold = true;
587         //!global.currLevel += 1;
588       } else if (playerExitDoor.objType == 'oXMarket') {
589         writeln("exiting to Black Market");
590         global.genBlackMarket = true;
591         //!global.currLevel += 1;
592       }
593     }
594   }
595   if (onLevelExitedCB) onLevelExitedCB();
596   //
597   playerExitDoor = none;
598   if (levelKind == LevelKind.Transition) {
599     if (global.thiefLevel > 0) global.thiefLevel -= 1;
600     if (global.alienCraft) ++global.alienCraft;
601     if (global.yetiLair) ++global.yetiLair;
602     if (global.lake) ++global.lake;
603     if (global.cityOfGold) ++global.cityOfGold;
604     //orig dbg:if (global.currLevel == 1) global.currLevel += firstLevelSkip; else global.currLevel += levelSkip;
605     /+
606     if (!global.blackMarket && !global.cityOfGold /*&& !global.yetiLair*/) {
607       global.currLevel += 1;
608     }
609     +/
610     ++global.currLevel;
611     generateLevel();
612   } else {
613     // < 20 seconds per level: looks like a speedrun
614     global.noDarkLevel = (levelEndTime > levelStartTime && levelEndTime-levelStartTime < 20*30);
615     if (lg.finalBossLevel) {
616       winTime = time;
617       ++stats.gamesWon;
618       // add money for big idol
619       player.addScore(50000);
620       stats.gameOver();
621       startWinCutscene();
622     } else {
623       generateTransitionLevel();
624     }
625   }
626   //centerViewAtPlayer();
630 void onOlmecDead (MapObject o) {
631   writeln("*** OLMEC IS DEAD!");
632   foreach (MapTile t; allExits) {
633     if (t.exit) {
634       t.openExit();
635       auto st = checkTileAtPoint(t.ix+8, t.iy+16);
636       if (!st) {
637         st = MakeMapTile(t.ix/16, t.iy/16+1, 'oTemple');
638         st.ore = 0;
639       }
640       st.invincible = true;
641     }
642   }
646 void generateLevelMessages () {
647   writeln("LEVEL NUMBER: ", global.currLevel);
648   if (global.darkLevel) {
649     if (global.hasCrown) {
650        osdMessage("THE HEDJET SHINES BRIGHTLY.");
651        global.darkLevel = false;
652     } else if (global.config.scumDarkness < 2) {
653       osdMessage("I CAN'T SEE A THING!\nI'D BETTER USE THESE FLARES!");
654     }
655   }
657   if (global.blackMarket) osdMessage("WELCOME TO THE BLACK MARKET!");
659   if (global.cemetary) osdMessage("THE DEAD ARE RESTLESS!");
660   if (global.lake == 1) osdMessage("I CAN HEAR RUSHING WATER...");
662   if (global.snakePit) osdMessage("I HEAR SNAKES... I HATE SNAKES!");
663   if (global.yetiLair == 1) osdMessage("IT SMELLS LIKE WET FUR IN HERE!");
664   if (global.alienCraft == 1) osdMessage("THERE'S A PSYCHIC PRESENCE HERE!");
665   if (global.cityOfGold == 1) {
666     if (true /*hasOlmec()*/) osdMessage("IT'S THE LEGENDARY CITY OF GOLD!");
667   }
669   if (global.sacrificePit) osdMessage("I CAN HEAR PRAYERS TO KALI!");
673 final MapObject debugSpawnObjectWithClass (class!MapObject oclass, optional bool playerDir) {
674   if (!oclass) return none;
675   int dx = 0, dy = 0;
676   bool canLeft = !isSolidAtPoint(player.ix-8, player.iy);
677   bool canRight = !isSolidAtPoint(player.ix+16, player.iy);
678   if (!canLeft && !canRight) return none;
679   if (canLeft && canRight) {
680     if (playerDir) {
681       dx = (player.dir == MapObject::Dir.Left ? -16 : 16);
682     } else {
683       dx = 16;
684     }
685   } else {
686     dx = (canLeft ? -16 : 16);
687   }
688   auto obj = SpawnMapObjectWithClass(oclass);
689   if (obj isa MapEnemy) { dx -= 8; dy -= 8; }
690   if (obj) PutSpawnedMapObject(player.ix+dx, player.iy+dy, obj);
691   return obj;
695 final MapObject debugSpawnObject (name aname) {
696   if (!aname) return none;
697   return debugSpawnObjectWithClass(findGameObjectClassByName(aname));
701 void createSpecialLevel (LevelKind kind, scope void delegate () creator, name amusic) {
702   global.darkLevel = false;
703   udjatAlarm = 0;
704   xmoney = 0;
705   collectCounter = 0;
706   global.resetStartingItems();
708   global.setMusicPitch(1.0);
709   levelKind = kind;
711   auto olddel = ImmediateDelete;
712   ImmediateDelete = false;
713   clearWholeLevel();
715   creator();
717   setMenuTilesOnTop();
719   fixWallTiles();
720   addBackgroundGfxDetails();
721   //levBGImgName = 'bgCave';
722   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
724   blockWaterChecking = true;
725   fixLiquidTop();
726   cleanDeadTiles();
728   ImmediateDelete = olddel;
729   CollectGarbage(true); // destroy delayed objects too
731   if (dumpGridStats) objGrid.dumpStats();
733   playerExited = false; // just in case
734   playerExitDoor = none;
736   osdClear();
738   setupGhostTime();
739   lg.musicName = amusic;
740   if (amusic) global.playMusic(lg.musicName); else global.stopMusic();
744 void createTitleLevel () {
745   createSpecialLevel(LevelKind.Title, &createTitleRoom, 'musTitle');
749 void createTutorialLevel () {
750   createSpecialLevel(LevelKind.Tutorial, &createTutorialRoom, 'musCave');
751   global.plife = 4;
752   global.bombs = 0;
753   global.rope = 4;
754   global.arrows = 0;
755   global.sgammo = 0;
759 // `global.currLevel` is the new level
760 void generateTransitionLevel () {
761   global.darkLevel = false;
762   udjatAlarm = 0;
763   xmoney = 0;
764   collectCounter = 0;
766   resetTransitionOverlay();
768   global.setMusicPitch(1.0);
769   switch (global.config.transitionMusicMode) {
770     case GameConfig::MusicMode.Silent: global.stopMusic(); break;
771     case GameConfig::MusicMode.Restart: global.restartMusic(); break;
772     case GameConfig::MusicMode.DontTouch: break;
773   }
775   levelKind = LevelKind.Transition;
777   auto olddel = ImmediateDelete;
778   ImmediateDelete = false;
779   clearWholeLevel();
781        if (global.currLevel < 4) createTrans1Room();
782   else if (global.currLevel == 4) createTrans1xRoom();
783   else if (global.currLevel < 8) createTrans2Room();
784   else if (global.currLevel == 8) createTrans2xRoom();
785   else if (global.currLevel < 12) createTrans3Room();
786   else if (global.currLevel == 12) createTrans3xRoom();
787   else if (global.currLevel < 16) createTrans4Room();
788   else if (global.currLevel == 16) createTrans4Room();
789   else createTrans1Room(); //???
791   setMenuTilesOnTop();
793   fixWallTiles();
794   addBackgroundGfxDetails();
795   //levBGImgName = 'bgCave';
796   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
798   blockWaterChecking = true;
799   fixLiquidTop();
800   cleanDeadTiles();
802   if (damselSaved > 0) {
803     // this is special "damsel ready to kiss you" object, not a heart
804     MakeMapObject(176+8, 176+8, 'oDamselKiss');
805     global.plife += damselSaved; // if player skipped transition cutscene
806     damselSaved = 0;
807   }
809   ImmediateDelete = olddel;
810   CollectGarbage(true); // destroy delayed objects too
812   if (dumpGridStats) objGrid.dumpStats();
814   playerExited = false; // just in case
815   playerExitDoor = none;
817   osdClear();
819   setupGhostTime();
820   //global.playMusic(lg.musicName);
824 void generateLevel () {
825   levelStartTime = time;
826   levelEndTime = time;
828   udjatAlarm = 0;
829   if (resetBMCOG) {
830     resetBMCOG = false;
831     global.genBlackMarket = false;
832   }
834   global.setMusicPitch(1.0);
835   stats.clearLevelTotals();
837   levelKind = LevelKind.Normal;
838   lg.generate();
839   //lg.dump();
841   resetRoomBounds();
843   lg.generateRooms();
844   //writeln("tw:", tilesWidth, "; th:", tilesHeight);
846   auto olddel = ImmediateDelete;
847   ImmediateDelete = false;
848   clearWholeLevel();
850   if (lg.finalBossLevel) {
851     blockWaterChecking = true;
852     createOlmecRoom();
853   }
855   // if transition cutscene was skipped...
856   global.plife += max(0, damselSaved); // if player skipped transition cutscene
857   damselSaved = 0;
859   // generate tiles
860   startRoomX = lg.startRoomX;
861   startRoomY = lg.startRoomY;
862   endRoomX = lg.endRoomX;
863   endRoomY = lg.endRoomY;
864   addBackgroundGfxDetails();
865   foreach (int y; 0..tilesHeight) {
866     foreach (int x; 0..tilesWidth) {
867       lg.genTileAt(x, y);
868     }
869   }
870   fixWallTiles();
872   levBGImgName = lg.bgImgName;
873   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
875   if (global.allowAngryShopkeepers) generateAngryShopkeepers();
877   lg.generateEntities();
879   // add box of flares to dark level
880   if (global.darkLevel && allEnters.length) {
881     auto enter = allEnters[0];
882     int x = enter.ix, y = enter.iy;
883          if (!isSolidAtPoint(x-16, y)) MakeMapObject(x-16+8, y+8, 'oFlareCrate');
884     else if (!isSolidAtPoint(x+16, y)) MakeMapObject(x+16+8, y+8, 'oFlareCrate');
885     else MakeMapObject(x+8, y+8, 'oFlareCrate');
886   }
888   //scrGenerateEntities();
889   //foreach (; 0..2) scrGenerateEntities();
891   writeln(objGrid.countObjects, " alive objects inserted");
892   writeln(countBackTiles, " background tiles inserted");
894   if (!player) FatalError("player pawn is not spawned");
896   if (lg.finalBossLevel) {
897     blockWaterChecking = true;
898   } else {
899     blockWaterChecking = false;
900   }
901   fixLiquidTop();
902   cleanDeadTiles();
904   ImmediateDelete = olddel;
905   CollectGarbage(true); // destroy delayed objects too
907   if (dumpGridStats) objGrid.dumpStats();
909   playerExited = false; // just in case
910   playerExitDoor = none;
912   levelMoneyStart = stats.money;
914   osdClear();
915   generateLevelMessages();
917   xmoney = 0;
918   collectCounter = 0;
920   if (lastMusicName != lg.musicName) {
921     global.playMusic(lg.musicName);
922     //writeln("!<", lastMusicName, ">:<", lg.musicName, ">");
923   } else {
924     //writeln("MM: ", global.config.nextLevelMusicMode);
925     switch (global.config.nextLevelMusicMode) {
926       case GameConfig::MusicMode.Silent: global.stopMusic(); break; // the thing that should not be
927       case GameConfig::MusicMode.Restart: global.restartMusic(); break;
928       case GameConfig::MusicMode.DontTouch:
929         if (global.config.transitionMusicMode == GameConfig::MusicMode.Silent) {
930           global.playMusic(lg.musicName);
931         }
932         break;
933     }
934   }
935   lastMusicName = lg.musicName;
936   //global.playMusic(lg.musicName);
938   setupGhostTime();
939   if (global.cityOfGold == 1 || global.genBlackMarket) resetBMCOG = true;
941   if (global.cityOfGold == 1) {
942     lg.mapSprite = 'sMapTemple';
943     lg.mapTitle = "City of Gold";
944   } else if (global.blackMarket) {
945     lg.mapSprite = 'sMapJungle';
946     lg.mapTitle = "Black Market";
947   }
951 // ////////////////////////////////////////////////////////////////////////// //
952 int currKeys, nextKeys;
953 int pressedKeysQ, releasedKeysQ;
954 int keysPressed, keysReleased = -1;
957 struct SavedKeyState {
958   int currKeys, nextKeys;
959   int pressedKeysQ, releasedKeysQ;
960   int keysPressed, keysReleased;
961   // for session
962   int roomSeed, otherSeed;
966 // for saving/replaying
967 final void keysSaveState (out SavedKeyState ks) {
968   ks.currKeys = currKeys;
969   ks.nextKeys = nextKeys;
970   ks.pressedKeysQ = pressedKeysQ;
971   ks.releasedKeysQ = releasedKeysQ;
972   ks.keysPressed = keysPressed;
973   ks.keysReleased = keysReleased;
976 // for saving/replaying
977 final void keysRestoreState (const ref SavedKeyState ks) {
978   currKeys = ks.currKeys;
979   nextKeys = ks.nextKeys;
980   pressedKeysQ = ks.pressedKeysQ;
981   releasedKeysQ = ks.releasedKeysQ;
982   keysPressed = ks.keysPressed;
983   keysReleased = ks.keysReleased;
987 final void keysNextFrame () {
988   currKeys = nextKeys;
992 final void clearKeys () {
993   currKeys = 0;
994   nextKeys = 0;
995   pressedKeysQ = 0;
996   releasedKeysQ = 0;
997   keysPressed = 0;
998   keysReleased = -1;
1002 final void onKey (int code, bool down) {
1003   if (!code) return;
1004   if (down) {
1005     currKeys |= code;
1006     nextKeys |= code;
1007     if (keysReleased&code) {
1008       keysPressed |= code;
1009       keysReleased &= ~code;
1010       pressedKeysQ |= code;
1011     }
1012   } else {
1013     nextKeys &= ~code;
1014     if (keysPressed&code) {
1015       keysReleased |= code;
1016       keysPressed &= ~code;
1017       releasedKeysQ |= code;
1018     }
1019   }
1022 final bool isKeyDown (int code) {
1023   return !!(currKeys&code);
1026 final bool isKeyPressed (int code) {
1027   bool res = !!(pressedKeysQ&code);
1028   pressedKeysQ &= ~code;
1029   return res;
1032 final bool isKeyReleased (int code) {
1033   bool res = !!(releasedKeysQ&code);
1034   releasedKeysQ &= ~code;
1035   return res;
1039 final void clearKeysPressRelease () {
1040   keysPressed = default.keysPressed;
1041   keysReleased = default.keysReleased;
1042   pressedKeysQ = default.pressedKeysQ;
1043   releasedKeysQ = default.releasedKeysQ;
1044   currKeys = 0;
1045   nextKeys = 0;
1049 // ////////////////////////////////////////////////////////////////////////// //
1050 final void registerEnter (MapTile t) {
1051   if (!t) return;
1052   allEnters[$] = t;
1053   return;
1057 final void registerExit (MapTile t) {
1058   if (!t) return;
1059   allExits[$] = t;
1060   return;
1064 final bool isYAtEntranceRow (int py) {
1065   py /= 16;
1066   foreach (MapTile t; allEnters) if (t.iy == py) return true;
1067   return false;
1071 final int calcNearestEnterDist (int px, int py) {
1072   if (allEnters.length == 0) return int.max;
1073   int curdistsq = int.max;
1074   foreach (MapTile t; allEnters) {
1075     int xc = px-t.xCenter, yc = py-t.yCenter;
1076     int distsq = xc*xc+yc*yc;
1077     if (distsq < curdistsq) curdistsq = distsq;
1078   }
1079   return round(sqrt(curdistsq));
1083 final int calcNearestExitDist (int px, int py) {
1084   if (allExits.length == 0) return int.max;
1085   int curdistsq = int.max;
1086   foreach (MapTile t; allExits) {
1087     int xc = px-t.xCenter, yc = py-t.yCenter;
1088     int distsq = xc*xc+yc*yc;
1089     if (distsq < curdistsq) curdistsq = distsq;
1090   }
1091   return round(sqrt(curdistsq));
1095 // ////////////////////////////////////////////////////////////////////////// //
1096 final void clearForTransition () {
1097   auto olddel = ImmediateDelete;
1098   ImmediateDelete = false;
1099   clearWholeLevel();
1100   ImmediateDelete = olddel;
1101   CollectGarbage(true); // destroy delayed objects too
1102   global.darkLevel = false;
1106 // ////////////////////////////////////////////////////////////////////////// //
1107 final int countBackTiles () {
1108   int res = 0;
1109   for (MapBackTile bt = backtiles; bt; bt = bt.next) ++res;
1110   return res;
1114 final void clearWholeLevel () {
1115   allEnters.clear();
1116   allExits.clear();
1118   // don't kill objects the player is holding
1119   if (player) {
1120     if (player.pickedItem isa ItemBall) {
1121       player.pickedItem.instanceRemove();
1122       player.pickedItem = none;
1123     }
1124     if (player.pickedItem && player.pickedItem.grid) {
1125       player.pickedItem.grid.remove(player.pickedItem.gridId);
1126       writeln("secured pocket item '", GetClassName(player.pickedItem.Class), "'");
1127     }
1128     if (player.holdItem isa ItemBall) {
1129       player.removeBallAndChain(temp:true);
1130       if (player.holdItem) player.holdItem.instanceRemove();
1131       player.holdItem = none;
1132     }
1133     if (player.holdItem && player.holdItem.grid) {
1134       player.holdItem.grid.remove(player.holdItem.gridId);
1135       writeln("secured held item '", GetClassName(player.holdItem.Class), "'");
1136     }
1137     writeln("secured ball; mustBeChained=", player.mustBeChained, "; wasHoldingBall=", player.wasHoldingBall);
1138   }
1140   int count = objGrid.countObjects();
1141   if (dumpGridStats) { if (objGrid.getFirstObjectCID()) objGrid.dumpStats(); }
1142   objGrid.removeAllObjects(true); // and destroy
1143   if (count > 0) writeln(count, " objects destroyed");
1145   lastUsedObjectId = 0;
1146   accumTime = 0;
1147   //!time = 0;
1148   lastRenderTime = -1;
1149   liquidTileCount = 0;
1150   checkWater = false;
1152   while (backtiles) {
1153     MapBackTile t = backtiles;
1154     backtiles = t.next;
1155     delete t;
1156   }
1158   levBGImg = none;
1159   framesProcessedFromLastClear = 0;
1163 final void insertObject (MapEntity o) {
1164   if (!o) return;
1165   if (o.grid) FatalError("cannot put object into level twice");
1166   objGrid.insert(o);
1170 final void spawnPlayerAt (int x, int y) {
1171   // if we have no player, spawn new one
1172   // otherwise this just a level transition, so simply reposition him
1173   if (!player) {
1174     // don't add player to object list, as it has very separate processing anyway
1175     player = SpawnObject(PlayerPawn);
1176     player.global = global;
1177     player.level = self;
1178     if (!player.initialize()) {
1179       delete player;
1180       FatalError("something is wrong with player initialization");
1181       return;
1182     }
1183   }
1184   player.fltx = x;
1185   player.flty = y;
1186   player.saveInterpData();
1187   player.resurrect();
1188   if (player.mustBeChained || global.config.scumBallAndChain) {
1189     writeln("*** spawning ball and chain");
1190     player.spawnBallAndChain(levelStart:true);
1191   }
1192   playerExited = false;
1193   playerExitDoor = none;
1194   if (global.config.startWithKapala) global.hasKapala = true;
1195   centerViewAtPlayer();
1196   // reinsert player items into grid
1197   if (player.pickedItem) objGrid.insert(player.pickedItem);
1198   if (player.holdItem) objGrid.insert(player.holdItem);
1199   //writeln("player spawned; active=", player.active);
1200   player.scrSwitchToPocketItem(forceIfEmpty:false);
1204 final void teleportPlayerTo (int x, int y) {
1205   if (player) {
1206     player.fltx = x;
1207     player.flty = y;
1208     player.saveInterpData();
1209   }
1213 final void resurrectPlayer () {
1214   if (player) player.resurrect();
1215   playerExited = false;
1216   playerExitDoor = none;
1220 // ////////////////////////////////////////////////////////////////////////// //
1221 final void scrShake (int duration) {
1222   if (shakeLeft == 0) {
1223     shakeOfs.x = 0;
1224     shakeOfs.y = 0;
1225     shakeDir.x = 0;
1226     shakeDir.y = 0;
1227   }
1228   shakeLeft = max(shakeLeft, duration);
1233 // ////////////////////////////////////////////////////////////////////////// //
1234 enum SCAnger {
1235   TileDestroyed,
1236   ItemStolen, // including damsel, lol
1237   CrapsCheated,
1238   BombDropped,
1239   DamselWhipped,
1243 // make the nearest shopkeeper angry. RAWR!
1244 void scrShopkeeperAnger (SCAnger reason, optional int maxdist, optional MapEntity offender) {
1245   if (!offender) offender = player;
1246   auto shp = MonsterShopkeeper(findNearestEnemy(offender.ix, offender.iy, delegate bool (MapEnemy o) {
1247     auto sc = MonsterShopkeeper(o);
1248     if (!sc) return false;
1249     if (sc.dead || sc.angered) return false;
1250     return true;
1251   }, castClass:MonsterShopkeeper));
1253   if (shp) {
1254     if (specified_maxdist && offender.directionToEntityCenter(shp) > maxdist) return;
1255     if (!shp.dead && !shp.angered) {
1256       shp.status = MapObject::ATTACK;
1257       string msg;
1258            if (global.murderer) msg = "YOU'LL PAY FOR YOUR CRIMES!";
1259       else if (reason == SCAnger.ItemStolen) msg = "COME BACK HERE, THIEF!";
1260       else if (reason == SCAnger.TileDestroyed) msg = "DIE, YOU VANDAL!";
1261       else if (reason == SCAnger.BombDropped) msg = "TERRORIST!";
1262       else if (reason == SCAnger.DamselWhipped) msg = "HEY, ONLY I CAN DO THAT!";
1263       else if (reason == SCAnger.CrapsCheated) msg = "DIE, CHEATER!";
1264       else msg = "NOW I'M REALLY STEAMED!";
1265       if (msg) osdMessage(msg, -666);
1266       global.thiefLevel += (global.thiefLevel > 0 ? 3 : 2);
1267     }
1268   }
1272 final MapObject findCrapsPrize () {
1273   foreach (MapObject o; objGrid.allObjects(MapObject)) {
1274     if (!o.spectral && o.inDiceHouse) return o;
1275   }
1276   return none;
1280 // ////////////////////////////////////////////////////////////////////////// //
1281 // 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.
1282 // note: idols moved by monkeys will have false `stolenIdol`
1283 void scrTriggerIdolAltar (bool stolenIdol) {
1284   ObjTikiCurse res = none;
1285   int curdistsq = int.max;
1286   int px = player.xCenter, py = player.yCenter;
1287   foreach (MapObject o; objGrid.allObjects(MapObject)) {
1288     auto tcr = ObjTikiCurse(o);
1289     if (!tcr) continue;
1290     if (tcr.activated) continue;
1291     int xc = px-tcr.xCenter, yc = py-tcr.yCenter;
1292     int distsq = xc*xc+yc*yc;
1293     if (distsq < curdistsq) {
1294       res = tcr;
1295       curdistsq = distsq;
1296     }
1297   }
1298   if (res) res.activate(stolenIdol);
1302 // ////////////////////////////////////////////////////////////////////////// //
1303 void setupGhostTime () {
1304   musicFadeTimer = -1;
1305   ghostSpawned = false;
1307   // there is no ghost on the first level
1308   if (inWinCutscene || !isNormalLevel() || lg.finalBossLevel ||
1309       (!global.config.ghostAtFirstLevel && global.currLevel == 1))
1310   {
1311     ghostTimeLeft = -1;
1312     global.setMusicPitch(1.0);
1313     return;
1314   }
1316   if (global.config.scumGhost < 0) {
1317     // instant
1318     ghostTimeLeft = 1;
1319     osdMessage("A GHASTLY VOICE CALLS YOUR NAME!\nLET'S GET OUT OF HERE!", 6.66, urgent:true);
1320     return;
1321   }
1323   if (global.config.scumGhost == 0) {
1324     // never
1325     ghostTimeLeft = -1;
1326     return;
1327   }
1329   // randomizes time until ghost appears once time limit is reached
1330   // global.ghostRandom (true/false) if extra time is randomized - loaded from yasm.cfg. if false, use 30 second default
1331   // ghostTimeLeft (time in seconds * 1000) for currently generated level
1333   if (global.config.ghostRandom) {
1334     auto tMin = global.randOther(10, 30); // min(global.plife*10, 30);
1335     auto tMax = max(tMin+global.randOther(10, 30), min(global.config.scumGhost, 300)); // 5 minutes max
1336     auto tTime = global.randOther(tMin, tMax);
1337     if (tTime <= 0) tTime = round(tMax/2.0);
1338     ghostTimeLeft = tTime;
1339   } else {
1340     ghostTimeLeft = global.config.scumGhost; // 5 minutes max
1341   }
1343   ghostTimeLeft += max(0, global.config.ghostExtraTime);
1345   ghostTimeLeft *= 30; // seconds -> frames
1346   //global.ghostShowTime
1350 void spawnGhost () {
1351   addGhostSummoned();
1352   ghostSpawned = true;
1353   ghostTimeLeft = -1;
1355   int vwdt = (viewMax.x-viewMin.x);
1356   int vhgt = (viewMax.y-viewMin.y);
1358   int gx, gy;
1360   if (player.ix < viewMin.x+vwdt/2) {
1361     // player is in the left side
1362     gx = viewMin.x+vwdt/2+vwdt/4;
1363   } else {
1364     // player is in the right side
1365     gx = viewMin.x+vwdt/4;
1366   }
1368   if (player.iy < viewMin.y+vhgt/2) {
1369     // player is in the left side
1370     gy = viewMin.y+vhgt/2+vhgt/4;
1371   } else {
1372     // player is in the right side
1373     gy = viewMin.y+vhgt/4;
1374   }
1376   writeln("spawning ghost at tile (", gx/16, ",", gy/16, "); player is at tile (", player.ix/16, ",", player.iy/16, ")");
1378   MakeMapObject(gx, gy, 'oGhost');
1380   /*
1381     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);
1382     else instance_create(view_xview[0]-32, view_yview[0]+floor(view_hview[0]/2), oGhost);
1383     global.ghostExists = true;
1384   */
1388 void thinkFrameGameGhost () {
1389   if (player.dead) return;
1390   if (!isNormalLevel()) return; // just in case
1392   if (ghostTimeLeft < 0) {
1393     // turned off
1394     if (musicFadeTimer > 0) {
1395       musicFadeTimer = -1;
1396       global.setMusicPitch(1.0);
1397     }
1398     return;
1399   }
1401   if (musicFadeTimer >= 0) {
1402     ++musicFadeTimer;
1403     if (musicFadeTimer > 0 && musicFadeTimer <= 100*3) {
1404       float pitch = 1.0-(0.00226/3.0)*musicFadeTimer;
1405       //writeln("MFT: ", musicFadeTimer, "; pitch=", pitch);
1406       global.setMusicPitch(pitch);
1407     }
1408   }
1410   if (ghostTimeLeft == 0) {
1411     // she is already here!
1412     return;
1413   }
1415   // no ghost if we have a crown
1416   if (global.hasCrown) {
1417     ghostTimeLeft = -1;
1418     return;
1419   }
1421   // if she was already spawned, don't do it again
1422   if (ghostSpawned) {
1423     ghostTimeLeft = 0;
1424     return;
1425   }
1427   if (--ghostTimeLeft != 0) {
1428     // warning
1429     if (global.config.ghostExtraTime > 0) {
1430       if (ghostTimeLeft == global.config.ghostExtraTime*30) {
1431         osdMessage("A CHILL RUNS UP YOUR SPINE...\nLET'S GET OUT OF HERE!", 6.66, urgent:true);
1432       }
1433       if (musicFadeTimer < 0 && ghostTimeLeft <= global.config.ghostExtraTime*30) {
1434         musicFadeTimer = 0;
1435       }
1436     }
1437     return;
1438   }
1440   // spawn her
1441   if (player.isExitingSprite) {
1442     // no reason to spawn her, we're leaving
1443     ghostTimeLeft = -1;
1444     return;
1445   }
1447   spawnGhost();
1451 void thinkFrameGame () {
1452   thinkFrameGameGhost();
1453   // udjat eye blinking
1454   if (global.hasUdjatEye && player) {
1455     foreach (MapTile t; allExits) {
1456       if (t isa MapTileBlackMarketDoor) {
1457         auto dm = int(player.distanceToEntity(t));
1458         if (dm < 4) dm = 4;
1459         if (udjatAlarm < 1 || dm < udjatAlarm) udjatAlarm = dm;
1460       }
1461     }
1462   } else {
1463     global.udjatBlink = false;
1464     udjatAlarm = 0;
1465   }
1466   if (udjatAlarm > 0) {
1467     if (--udjatAlarm == 0) {
1468       global.udjatBlink = !global.udjatBlink;
1469       if (global.hasUdjatEye && player) {
1470         player.playSound(global.udjatBlink ? 'sndBlink1' : 'sndBlink2');
1471       }
1472     }
1473   }
1474   switch (levelKind) {
1475     case LevelKind.Stars: thinkFrameGameStars(); break;
1476     case LevelKind.Sun: thinkFrameGameSun(); break;
1477     case LevelKind.Moon: thinkFrameGameMoon(); break;
1478     case LevelKind.Transition: thinkFrameTransition(); break;
1479   }
1483 // ////////////////////////////////////////////////////////////////////////// //
1484 private final bool isWaterTileCB (MapTile t) {
1485   return (t && t.visible && t.water);
1489 private final bool isLavaTileCB (MapTile t) {
1490   return (t && t.visible && t.lava);
1494 // ////////////////////////////////////////////////////////////////////////// //
1495 const int GreatLakeStartTileY = 28;
1498 final void fillGreatLake () {
1499   if (global.lake == 1) {
1500     foreach (int y; GreatLakeStartTileY..tilesHeight) {
1501       foreach (int x; 0..tilesWidth) {
1502         auto t = checkTileAtPoint(x*16, y*16, delegate bool (MapTile t) {
1503           if (t.spectral || !t.visible || t.invisible || t.moveable) return false;
1504           return true;
1505         });
1506         if (!t) {
1507           t = MakeMapTile(x, y, 'oWaterSwim');
1508           if (!t) continue;
1509         }
1510         if (t.water) {
1511           t.setSprite(y == GreatLakeStartTileY ? 'sWaterTop' : 'sWater');
1512         } else if (t.lava) {
1513           t.setSprite(y == GreatLakeStartTileY ? 'sLavaTop' : 'sLava');
1514         }
1515       }
1516     }
1517   }
1521 // called once after level generation
1522 final void fixLiquidTop () {
1523   if (global.lake == 1) fillGreatLake();
1525   liquidTileCount = 0;
1526   foreach (MapTile t; objGrid.allObjects(MapTile)) {
1527     if (!t.water && !t.lava) continue;
1529     ++liquidTileCount;
1530     //writeln("fixing water tile(", GetClassName(t.Class), "):'", t.objName, "' (water=", t.water, "; lava=", t.lava, "); lqc=", liquidTileCount);
1532     //if (global.lake == 1) continue; // it is done in `fillGreatLake()`
1534     if (!checkTileAtPoint(t.ix+8, t.iy-8, (t.lava ? &isLavaTileCB : &isWaterTileCB))) {
1535       t.setSprite(t.lava ? 'sLavaTop' : 'sWaterTop');
1536     } else {
1537       // don't do this, it will destroy seaweed
1538       //t.setSprite(t.lava ? 'sLava' : 'sWater');
1539       auto spr = t.getSprite();
1540            if (!spr) t.setSprite(t.lava ? 'sLava' : 'sWater');
1541       else if (spr.Name == 'sLavaTop') t.setSprite('sLava');
1542       else if (spr.Name == 'sWaterTop') t.setSprite('sWater');
1543     }
1544   }
1545   //writeln("liquid tiles count: ", liquidTileCount);
1549 // ////////////////////////////////////////////////////////////////////////// //
1550 transient MapTile curWaterTile;
1551 transient bool curWaterTileCheckHitsLava;
1552 transient bool curWaterTileCheckHitsSolidOrWater; // only for `checkWaterOrSolidTilePartialCB`
1553 transient int curWaterTileLastHDir;
1554 transient ubyte[16, 16] curWaterOccupied;
1555 transient int curWaterOccupiedCount;
1556 transient int curWaterTileCheckX0, curWaterTileCheckY0;
1559 private final void clearCurWaterCheckState () {
1560   curWaterTileCheckHitsLava = false;
1561   curWaterOccupiedCount = 0;
1562   foreach (auto idx; 0..16*16) curWaterOccupied[idx] = 0;
1566 private final bool checkWaterOrSolidTileCB (MapTile t) {
1567   if (t == curWaterTile) return false;
1568   if (t.lava && curWaterTile.water) {
1569     curWaterTileCheckHitsLava = true;
1570     return true;
1571   }
1572   if (t.ix%16 != 0 || t.iy%16 != 0) {
1573     if (t.water || t.solid) {
1574       // fill occupied array
1575       //FIXME: optimize this
1576       if (curWaterOccupiedCount < 16*16) {
1577         foreach (auto dy; t.y0..t.y1+1) {
1578           foreach (auto dx; t.x0..t.x1+1) {
1579             int sx = dx-curWaterTileCheckX0;
1580             int sy = dy-curWaterTileCheckY0;
1581             if (sx >= 0 && sx <= 16 && sy >= 0 && sy <= 15 && !curWaterOccupied[sx, sy]) {
1582               curWaterOccupied[sx, sy] = 1;
1583               ++curWaterOccupiedCount;
1584             }
1585           }
1586         }
1587       }
1588     }
1589     return false; // need to check for lava
1590   }
1591   if (t.water || t.solid || t.lava) {
1592     curWaterOccupiedCount = 16*16;
1593     if (t.water && curWaterTile.lava) t.instanceRemove();
1594   }
1595   return false; // need to check for lava
1599 private final bool checkWaterOrSolidTilePartialCB (MapTile t) {
1600   if (t == curWaterTile) return false;
1601   if (t.lava && curWaterTile.water) {
1602     //writeln("!!!!!!!!");
1603     curWaterTileCheckHitsLava = true;
1604     return true;
1605   }
1606   if (t.water || t.solid || t.lava) {
1607     //writeln("*********");
1608     curWaterTileCheckHitsSolidOrWater = true;
1609     if (t.water && curWaterTile.lava) t.instanceRemove();
1610   }
1611   return false; // need to check for lava
1615 private final bool isFullyOccupiedAtTilePos (int tileX, int tileY) {
1616   clearCurWaterCheckState();
1617   curWaterTileCheckX0 = tileX*16;
1618   curWaterTileCheckY0 = tileY*16;
1619   checkTilesInRect(tileX*16, tileY*16, 16, 16, &checkWaterOrSolidTileCB);
1620   return (curWaterTileCheckHitsLava || curWaterOccupiedCount == 16*16);
1624 private final bool isAtLeastPartiallyOccupiedAtTilePos (int tileX, int tileY) {
1625   curWaterTileCheckHitsLava = false;
1626   curWaterTileCheckHitsSolidOrWater = false;
1627   checkTilesInRect(tileX*16, tileY*16, 16, 16, &checkWaterOrSolidTilePartialCB);
1628   return (curWaterTileCheckHitsSolidOrWater || curWaterTileCheckHitsLava);
1632 private final bool waterCanReachGroundHoleInDir (MapTile wtile, int dx) {
1633   if (dx == 0) return false; // just in case
1634   dx = sign(dx);
1635   int x = wtile.ix/16, y = wtile.iy/16;
1636   x += dx;
1637   while (x >= 0 && x < tilesWidth) {
1638     if (!isAtLeastPartiallyOccupiedAtTilePos(x, y+1)) return true;
1639     if (isAtLeastPartiallyOccupiedAtTilePos(x, y)) return false;
1640     x += dx;
1641   }
1642   return false;
1646 // returns `true` if this tile must be removed
1647 private final bool checkWaterFlow (MapTile wtile) {
1648   if (global.lake == 1) {
1649     if (wtile.iy >= GreatLakeStartTileY*16) return false; // lake tile, don't touch
1650     if (wtile.iy >= GreatLakeStartTileY*16-16) return true; // remove it, so it won't stack on a lake
1651   }
1653   if (wtile.ix%16 != 0 || wtile.iy%16 != 0) return true; // sanity check
1655   curWaterTile = wtile;
1656   curWaterTileLastHDir = 0; // never moved to the side
1658   bool wasMoved = false;
1660   for (;;) {
1661     int tileX = wtile.ix/16, tileY = wtile.iy/16;
1663     // out of level?
1664     if (tileY >= tilesHeight) return true;
1666     // check if we can fall down
1667     auto canFall = !isAtLeastPartiallyOccupiedAtTilePos(tileX, tileY+1);
1668     // disappear if can fall in lava
1669     if (wtile.water && curWaterTileCheckHitsLava) {
1670       //!writeln(wtile.objId, ": LAVA HIT DOWN");
1671       return true;
1672     }
1673     if (wasMoved) {
1674       // fake, so caller will not start removing tiles
1675       if (canFall) wtile.waterMovedDown = true;
1676       break;
1677     }
1678     // can move down?
1679     if (canFall) {
1680       // move down
1681       //!writeln(wtile.objId, ": GOING DOWN");
1682       curWaterTileLastHDir = 0;
1683       wtile.iy = wtile.iy+16;
1684       wasMoved = true;
1685       wtile.waterMovedDown = true;
1686       continue;
1687     }
1689     bool canMoveLeft = (curWaterTileLastHDir > 0 ? false : !isAtLeastPartiallyOccupiedAtTilePos(tileX-1, tileY));
1690     // disappear if near lava
1691     if (wtile.water && curWaterTileCheckHitsLava) {
1692       //!writeln(wtile.objId, ": LAVA HIT LEFT");
1693       return true;
1694     }
1696     bool canMoveRight = (curWaterTileLastHDir < 0 ? false : !isAtLeastPartiallyOccupiedAtTilePos(tileX+1, tileY));
1697     // disappear if near lava
1698     if (wtile.water && curWaterTileCheckHitsLava) {
1699       //!writeln(wtile.objId, ": LAVA HIT RIGHT");
1700       return true;
1701     }
1703     if (!canMoveLeft && !canMoveRight) {
1704       // do final checks
1705       //!if (wasMove) writeln(wtile.objId, ": NO MORE MOVES");
1706       break;
1707     }
1709     if (canMoveLeft && canMoveRight) {
1710       // choose random direction
1711       //!writeln(wtile.objId, ": CHOOSING RANDOM HDIR");
1712       // actually, choose direction that leads to hole in a ground
1713       if (waterCanReachGroundHoleInDir(wtile, -1)) {
1714         // can reach hole at the left side
1715         if (waterCanReachGroundHoleInDir(wtile, 1)) {
1716           // can reach hole at the right side, choose at random
1717           if (global.randOther(0, 1)) canMoveRight = false; else canMoveLeft = false;
1718         } else {
1719           // move left
1720           canMoveRight = false;
1721         }
1722       } else {
1723         // can't reach hole at the left side
1724         if (waterCanReachGroundHoleInDir(wtile, 1)) {
1725           // can reach hole at the right side, choose at random
1726           canMoveLeft = false;
1727         } else {
1728           // no holes at any side, choose at random
1729           if (global.randOther(0, 1)) canMoveRight = false; else canMoveLeft = false;
1730         }
1731       }
1732     }
1734     // move
1735     if (canMoveLeft) {
1736       if (canMoveRight) FatalError("WATERCHECK: WTF RIGHT");
1737       //!writeln(wtile.objId, ": MOVING LEFT (", curWaterTileLastHDir, ")");
1738       curWaterTileLastHDir = -1;
1739       wtile.ix = wtile.ix-16;
1740     } else if (canMoveRight) {
1741       if (canMoveLeft) FatalError("WATERCHECK: WTF LEFT");
1742       //!writeln(wtile.objId, ": MOVING RIGHT (", curWaterTileLastHDir, ")");
1743       curWaterTileLastHDir = 1;
1744       wtile.ix = wtile.ix+16;
1745     }
1746     wasMoved = true;
1747   }
1749   // remove seaweeds
1750   if (wasMoved) {
1751     checkWater = true;
1752     wtile.setSprite(wtile.lava ? 'sLava' : 'sWater');
1753     wtile.waterMoved = true;
1754     // if this tile was not moved down, check if it can move down on any next step
1755     if (!wtile.waterMovedDown) {
1756            if (waterCanReachGroundHoleInDir(wtile, -1)) wtile.waterMovedDown = true;
1757       else if (waterCanReachGroundHoleInDir(wtile, 1)) wtile.waterMovedDown = true;
1758     }
1759   }
1761   return false; // don't remove
1763   //if (!isWetTileAtPix(tileX*16+8, tileY*16-8)) wtile.setSprite(wtile.lava ? 'sLavaTop' : 'sWaterTop');
1767 transient array!MapTile waterTilesList;
1769 final bool sortWaterTilesByCoordsLess (MapTile a, MapTile b) {
1770   int dy = a.iy-b.iy;
1771   if (dy) return (dy < 0);
1772   return (a.ix < b.ix);
1775 transient int waterFlowPause = 0;
1776 transient bool debugWaterFlowPause = false;
1778 final void cleanDeadObjects () {
1779   // remove dead objects
1780   if (deadItemsHead) {
1781     auto olddel = ImmediateDelete;
1782     ImmediateDelete = false;
1783     do {
1784       auto it = deadItemsHead;
1785       deadItemsHead = it.deadItemsNext;
1786       if (it.grid) it.grid.remove(it.gridId);
1787       it.onDestroy();
1788       delete it;
1789     } while (deadItemsHead);
1790     ImmediateDelete = olddel;
1791     if (olddel) CollectGarbage(true); // destroy delayed objects too
1792   }
1795 final void cleanDeadTiles () {
1796   if (checkWater && /*global.lake == 1 ||*/ (!blockWaterChecking && liquidTileCount)) {
1797     if (global.lake == 1) fillGreatLake();
1798     if (waterFlowPause > 1) {
1799       --waterFlowPause;
1800       cleanDeadObjects();
1801       return;
1802     }
1803     if (debugWaterFlowPause) waterFlowPause = 4;
1804     //writeln("checking water");
1805     waterTilesList.clear();
1806     foreach (MapTile wtile; objGrid.allObjectsSafe(MapTile)) {
1807       if (wtile.water || wtile.lava) {
1808         // sanity check
1809         if (wtile.ix%16 == 0 && wtile.iy%16 == 0) {
1810           wtile.waterMoved = false;
1811           wtile.waterMovedDown = false;
1812           wtile.waterSlideOldX = wtile.ix;
1813           wtile.waterSlideOldY = wtile.iy;
1814           waterTilesList[$] = wtile;
1815         }
1816       }
1817     }
1818     checkWater = false;
1819     liquidTileCount = 0;
1820     waterTilesList.sort(&sortWaterTilesByCoordsLess);
1821     // do water flow
1822     bool wasAnyMove = false;
1823     bool wasAnyMoveDown = false;
1824     foreach (MapTile wtile; waterTilesList) {
1825       if (!wtile || !wtile.isInstanceAlive) continue;
1826       auto killIt = checkWaterFlow(wtile);
1827       if (killIt) {
1828         checkWater = true;
1829         wtile.smashMe();
1830         wtile.instanceRemove(); // just in case
1831       } else {
1832         wtile.saveInterpData();
1833         wtile.updateGrid();
1834         wasAnyMove = wasAnyMove || wtile.waterMoved;
1835         wasAnyMoveDown = wasAnyMoveDown || wtile.waterMovedDown;
1836         if (wtile.waterMoved && debugWaterFlowPause) wtile.waterSlideCounter = 4;
1837       }
1838     }
1839     // do water check
1840     liquidTileCount = 0;
1841     foreach (MapTile wtile; waterTilesList) {
1842       if (!wtile || !wtile.isInstanceAlive) continue;
1843       if (wasAnyMoveDown) {
1844         ++liquidTileCount;
1845         continue;
1846       }
1847       //checkWater = checkWater || wtile.waterMoved;
1848       curWaterTile = wtile;
1849       int tileX = wtile.ix/16, tileY = wtile.iy/16;
1850       // check if we are have no way to leak
1851       bool killIt = false;
1852       if (!isFullyOccupiedAtTilePos(tileX-1, tileY) || (wtile.water && curWaterTileCheckHitsLava)) {
1853         //writeln(" LEFT DEATH (", curWaterOccupiedCount, " : ", curWaterTileCheckHitsLava, ")");
1854         killIt = true;
1855       }
1856       if (!killIt && (!isFullyOccupiedAtTilePos(tileX+1, tileY) || (wtile.water && curWaterTileCheckHitsLava))) {
1857         //writeln(" RIGHT DEATH (", curWaterOccupiedCount, " : ", curWaterTileCheckHitsLava, ")");
1858         killIt = true;
1859       }
1860       if (!killIt && (!isFullyOccupiedAtTilePos(tileX, tileY+1) || (wtile.water && curWaterTileCheckHitsLava))) {
1861         //writeln(" DOWN DEATH (", curWaterOccupiedCount, " : ", curWaterTileCheckHitsLava, ")");
1862         killIt = true;
1863       }
1864       //killIt = false;
1865       if (killIt) {
1866         checkWater = true;
1867         wtile.smashMe();
1868         wtile.instanceRemove(); // just in case
1869       } else {
1870         ++liquidTileCount;
1871       }
1872     }
1873     if (wasAnyMove) checkWater = true;
1874     //writeln("water check: liquidTileCount=", liquidTileCount, "; checkWater=", checkWater, "; wasAnyMove=", wasAnyMove, "; wasAnyMoveDown=", wasAnyMoveDown);
1876     // fill empty spaces in lake with water
1877     fixLiquidTop();
1878   }
1880   cleanDeadObjects();
1884 // ////////////////////////////////////////////////////////////////////////// //
1885 private transient array!MapEntity postponedThinkers;
1886 private transient MapEntity thinkerHeld;
1887 private transient array!MapEntity activeThinkerList;
1890 final void doThinkActionsForObject (MapEntity o) {
1891        if (o.justSpawned) o.justSpawned = false;
1892   else if (o.imageSpeed > 0) o.nextAnimFrame();
1893   o.saveInterpData();
1894   o.thinkFrame();
1895   if (o.isInstanceAlive) {
1896     //o.updateGrid();
1897     o.processAlarms();
1898     if (o.isInstanceAlive) {
1899       if (o.whipTimer > 0) --o.whipTimer;
1900       o.updateGrid();
1901       auto obj = MapObject(o);
1902       if (!o.canLiveOutsideOfLevel && (!obj || !obj.heldBy) && o.isOutsideOfLevel()) {
1903         // oops, fallen out of level...
1904         o.onOutOfLevel();
1905       }
1906     }
1907   }
1911 // return `true` if thinker should be removed
1912 final void thinkOne (MapEntity o, optional bool doHeldObject, optional bool dontAddHeldObject) {
1913   if (!o) return;
1914   if (o == thinkerHeld && !doHeldObject) return; // skip it
1916   if (!o.active || !o.isInstanceAlive) return;
1918   auto obj = MapObject(o);
1920   if (obj && obj.heldBy == player) {
1921     // fix held item coords
1922     obj.fixHoldCoords();
1923     if (doHeldObject) {
1924       doThinkActionsForObject(o);
1925     } else {
1926       if (!dontAddHeldObject) {
1927         bool found = false;
1928         foreach (MapEntity e; postponedThinkers) if (e == o) { found = true; break; }
1929         if (!found) postponedThinkers[$] = o;
1930       }
1931     }
1932     return;
1933   }
1935   bool doThink = true;
1937   // collision with player weapon
1938   auto hh = PlayerWeapon(player.holdItem);
1939   bool doWeaponAction = false;
1940   if (hh) {
1941     if (hh.blockedBySolids && !global.config.killEnemiesThruWalls) {
1942       int xx = player.ix+(player.dir == MapObject::Dir.Left ? -16 : 16);
1943       //doWeaponAction = !isSolidAtPoint(xx, player.iy);
1944       doWeaponAction = !isSolidAtPoint(xx, hh.yCenter);
1945       /*
1946       int dh = max(1, hh.height-2);
1947       doWeaponAction = !checkTilesInRect(player.ix, player.iy);
1948       */
1949     } else {
1950       doWeaponAction = true;
1951     }
1952   }
1954   if (obj && doWeaponAction && hh && (o.whipTimer <= 0 || hh.ignoreWhipTimer) && hh.collidesWithObject(obj)) {
1955     //writeln("WEAPONED!");
1956     bool dontChangeWhipTimer = hh.dontChangeWhipTimer;
1957     if (!o.onTouchedByPlayerWeapon(player, hh)) {
1958       if (o.isInstanceAlive) hh.onCollisionWithObject(obj);
1959     }
1960     if (!dontChangeWhipTimer) o.whipTimer = o.whipTimerValue; //HACK
1961     doThink = o.isInstanceAlive;
1962   }
1964   if (doThink && o.isInstanceAlive) {
1965     doThinkActionsForObject(o);
1966     doThink = o.isInstanceAlive;
1967   }
1969   // collision with player
1970   if (doThink && obj && o.collidesWith(player)) {
1971     if (!player.onObjectTouched(obj) && o.isInstanceAlive) {
1972       doThink = !o.onTouchedByPlayer(player);
1973       o.updateGrid();
1974     }
1975   }
1979 final void processThinkers (float timeDelta) {
1980   if (timeDelta <= 0) return;
1981   if (gamePaused) {
1982     ++pausedTime;
1983     if (onBeforeFrame) onBeforeFrame(false);
1984     if (onAfterFrame) onAfterFrame(false);
1985     keysNextFrame();
1986     return;
1987   } else {
1988     pausedTime = 0;
1989   }
1990   accumTime += timeDelta;
1991   bool wasFrame = false;
1992   // block GC
1993   auto olddel = ImmediateDelete;
1994   ImmediateDelete = false;
1995   while (accumTime >= FrameTime) {
1996     postponedThinkers.clear();
1997     thinkerHeld = none;
1998     accumTime -= FrameTime;
1999     if (onBeforeFrame) onBeforeFrame(accumTime >= FrameTime);
2000     // shake
2001     if (shakeLeft > 0) {
2002       --shakeLeft;
2003       if (!shakeDir.x) shakeDir.x = (global.randOther(0, 1) ? -3 : 3);
2004       if (!shakeDir.y) shakeDir.y = (global.randOther(0, 1) ? -3 : 3);
2005       shakeOfs.x = shakeDir.x;
2006       shakeOfs.y = shakeDir.y;
2007       int sgnc = global.randOther(1, 3);
2008       if (sgnc&0x01) shakeDir.x = -shakeDir.x;
2009       if (sgnc&0x02) shakeDir.y = -shakeDir.y;
2010     } else {
2011       shakeOfs.x = 0;
2012       shakeOfs.y = 0;
2013       shakeDir.x = 0;
2014       shakeDir.y = 0;
2015     }
2016     // advance time
2017     time += 1;
2018     // we don't want the time to grow too large
2019     if (time < 0) { time = 0; lastRenderTime = -1; }
2020     // game-global events
2021     thinkFrameGame();
2022     // frame thinkers: player
2023     if (player && !disablePlayerThink) {
2024       // time limit
2025       if (!player.dead && isNormalLevel() &&
2026           (maxPlayingTime < 0 ||
2027            (maxPlayingTime > 0 && maxPlayingTime*30 <= time &&
2028             time%30 == 0 && global.randOther(1, 100) <= 20)))
2029       {
2030         MakeMapObject(player.ix, player.iy, 'oExplosion');
2031       }
2032       //HACK: check for stolen items
2033       auto item = MapItem(player.holdItem);
2034       if (item) item.onCheckItemStolen(player);
2035       item = MapItem(player.pickedItem);
2036       if (item) item.onCheckItemStolen(player);
2037       // normal thinking
2038       doThinkActionsForObject(player);
2039     }
2040     // frame thinkers: held object
2041     thinkerHeld = player.holdItem;
2042     if (thinkerHeld && thinkerHeld.isInstanceAlive) {
2043       thinkOne(thinkerHeld, doHeldObject:true);
2044       if (!thinkerHeld.isInstanceAlive) {
2045         if (player.holdItem == thinkerHeld) player.holdItem = none;
2046         thinkerHeld.grid.remove(thinkerHeld.gridId);
2047         /* later
2048         thinkerHeld.onDestroy();
2049         delete thinkerHeld;
2050         */
2051       }
2052     }
2053     // frame thinkers: objects
2054     activeThinkerList.clear();
2055     auto grid = objGrid;
2056     // collect active objects
2057     if (global.config.useFrozenRegion) {
2058       foreach (MapEntity e; grid.inRectPix(viewStart.x/global.scale-64, viewStart.y/global.scale-64, 320+64*2, 240+64*2, precise:false)) {
2059         if (e.active) activeThinkerList[$] = e;
2060       }
2061     } else {
2062       // no frozen area
2063       foreach (MapEntity e; grid.allObjects()) {
2064         if (e.active) activeThinkerList[$] = e;
2065       }
2066     }
2067     // process active objects
2068     //writeln("thinkers: ", activeThinkerList.length);
2069     foreach (MapEntity o; activeThinkerList) {
2070       if (!o) continue;
2071       thinkOne(o, doHeldObject:false);
2072       if (!o.isInstanceAlive) {
2073         //writeln("dead thinker: '", o.objType, "'");
2074         if (o.grid) o.grid.remove(o.gridId);
2075         auto obj = MapObject(o);
2076         if (obj && obj.heldBy) obj.heldBy.holdItem = none;
2077         /* later
2078         o.onDestroy();
2079         delete o;
2080         */
2081       }
2082     }
2083     // postponed thinkers
2084     foreach (MapEntity o; postponedThinkers) {
2085       if (!o) continue;
2086       thinkOne(o, doHeldObject:true, dontAddHeldObject:true);
2087       if (!o.isInstanceAlive) {
2088         //writeln("dead pp-thinker: '", o.objType, "'");
2089         /* later
2090         o.onDestroy();
2091         delete o;
2092         */
2093       }
2094     }
2095     postponedThinkers.clear();
2096     thinkerHeld = none;
2097     // clean dead things
2098     cleanDeadTiles();
2099     // fix held item coords
2100     if (player && player.holdItem) {
2101       if (player.holdItem.isInstanceAlive) {
2102         player.holdItem.fixHoldCoords();
2103       } else {
2104         player.holdItem = none;
2105       }
2106     }
2107     // money counter
2108     if (collectCounter == 0) {
2109       xmoney = max(0, xmoney-100);
2110     } else {
2111       --collectCounter;
2112     }
2113     // other things
2114     if (player) {
2115       if (!player.dead) stats.oneMoreFramePlayed();
2116       SoundSystem.ListenerOrigin = vector(player.xCenter, player.yCenter, -1);
2117       //writeln("plrpos=(", player.xCenter, ",", player.yCenter, "); lo=", SoundSystem.ListenerOrigin);
2118     }
2119     if (onAfterFrame) onAfterFrame(accumTime >= FrameTime);
2120     ++framesProcessedFromLastClear;
2121     keysNextFrame();
2122     wasFrame = true;
2123     if (!player.visible && player.holdItem) player.holdItem.visible = false;
2124     if (winCutsceneSwitchToNext) {
2125       winCutsceneSwitchToNext = false;
2126       switch (++inWinCutscene) {
2127         case 2: startWinCutsceneVolcano(); break;
2128         case 3: default: startWinCutsceneWinFall(); break;
2129       }
2130       break;
2131     }
2132     if (playerExited) break;
2133   }
2134   ImmediateDelete = olddel;
2135   if (playerExited) {
2136     playerExited = false;
2137     onLevelExited();
2138     centerViewAtPlayer();
2139   }
2140   if (wasFrame) {
2141     // if we were processed at least one frame, collect garbage
2142     //keysNextFrame();
2143     CollectGarbage(true); // destroy delayed objects too
2144   }
2145   if (accumTime > 0 && onInterFrame) onInterFrame(accumTime/FrameTime);
2149 // ////////////////////////////////////////////////////////////////////////// //
2150 final void tileXY2roomXY (int tileX, int tileY, out int roomX, out int roomY) {
2151   roomX = (tileX-1)/RoomGen::Width;
2152   roomY = (tileY-1)/RoomGen::Height;
2156 final bool isInShop (int tileX, int tileY) {
2157   if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
2158     auto n = roomType[tileX, tileY];
2159     if (n == 4 || n == 5) return true;
2160     return !!checkTilesInRect(tileX*16, tileY*16, 16, 16, delegate bool (MapTile t) { return t.shopWall; });
2161     //k8: we don't have this
2162     //if (t && t.objType == 'oShop') return true;
2163   }
2164   return false;
2168 // ////////////////////////////////////////////////////////////////////////// //
2169 override void Destroy () {
2170   clearWholeLevel();
2171   delete tempSolidTile;
2172   ::Destroy();
2176 // ////////////////////////////////////////////////////////////////////////// //
2177 // WARNING! delegate should not create/delete objects!
2178 final MapObject findNearestObject (int px, int py, scope bool delegate (MapObject o) dg, optional class!MapObject castClass) {
2179   MapObject res = none;
2180   if (!castClass) castClass = MapObject;
2181   int curdistsq = int.max;
2182   foreach (MapObject o; objGrid.allObjects(MapObject)) {
2183     if (o.spectral) continue;
2184     if (!dg(o)) continue;
2185     int xc = px-o.xCenter, yc = py-o.yCenter;
2186     int distsq = xc*xc+yc*yc;
2187     if (distsq < curdistsq) {
2188       res = o;
2189       curdistsq = distsq;
2190     }
2191   }
2192   return res;
2196 // WARNING! delegate should not create/delete objects!
2197 final MapObject findNearestEnemy (int px, int py, optional scope bool delegate (MapEnemy o) dg, optional class!MapEnemy castClass) {
2198   if (!castClass) castClass = MapEnemy;
2199   if (castClass !isa MapEnemy) return none;
2200   MapObject res = none;
2201   int curdistsq = int.max;
2202   foreach (MapEnemy o; objGrid.allObjects(castClass)) {
2203     //k8: i added `dead` check
2204     if (o.spectral || o.dead) continue;
2205     if (dg) {
2206       if (!dg(o)) continue;
2207     }
2208     int xc = px-o.xCenter, yc = py-o.yCenter;
2209     int distsq = xc*xc+yc*yc;
2210     if (distsq < curdistsq) {
2211       res = o;
2212       curdistsq = distsq;
2213     }
2214   }
2215   return res;
2219 final MonsterShopkeeper findNearestCalmShopkeeper (int px, int py) {
2220   auto obj = MonsterShopkeeper(findNearestEnemy(px, py, delegate bool (MapEnemy o) {
2221     auto sk = MonsterShopkeeper(o);
2222     if (sk && !sk.angered) return true;
2223     return false;
2224   }, castClass:MonsterShopkeeper));
2225   return obj;
2229 final MonsterShopkeeper hasAliveShopkeepers (optional bool skipAngry) {
2230   foreach (MonsterShopkeeper sc; objGrid.allObjects(MonsterShopkeeper)) {
2231     if (sc.spectral || sc.dead) continue;
2232     if (skipAngry && (sc.angered || sc.outlaw)) continue;
2233     return sc;
2234   }
2235   return none;
2239 // WARNING! delegate should not create/delete objects!
2240 final int calcNearestEnemyDist (int px, int py, optional scope bool delegate (MapEnemy o) dg, optional class!MapEnemy castClass) {
2241   auto e = findNearestEnemy(px, py, dg!optional, castClass!optional);
2242   if (!e) return int.max;
2243   int xc = px-e.xCenter, yc = py-e.yCenter;
2244   return round(sqrt(xc*xc+yc*yc));
2248 // WARNING! delegate should not create/delete objects!
2249 final int calcNearestObjectDist (int px, int py, optional scope bool delegate (MapObject o) dg, optional class!MapObject castClass) {
2250   auto e = findNearestObject(px, py, dg!optional, castClass!optional);
2251   if (!e) return int.max;
2252   int xc = px-e.xCenter, yc = py-e.yCenter;
2253   return round(sqrt(xc*xc+yc*yc));
2257 // WARNING! delegate should not create/delete objects!
2258 final MapTile findNearestMoveableSolid (int px, int py, optional scope bool delegate (MapTile t) dg) {
2259   MapTile res = none;
2260   int curdistsq = int.max;
2261   foreach (MapTile t; objGrid.allObjects(MapTile)) {
2262     if (t.spectral) continue;
2263     if (dg) {
2264       if (!dg(t)) continue;
2265     } else {
2266       if (!t.solid || !t.moveable) continue;
2267     }
2268     int xc = px-t.xCenter, yc = py-t.yCenter;
2269     int distsq = xc*xc+yc*yc;
2270     if (distsq < curdistsq) {
2271       res = t;
2272       curdistsq = distsq;
2273     }
2274   }
2275   return res;
2279 // WARNING! delegate should not create/delete objects!
2280 final MapTile findNearestTile (int px, int py, optional scope bool delegate (MapTile t) dg) {
2281   if (!dg) return none;
2282   MapTile res = none;
2283   int curdistsq = int.max;
2285   //FIXME: make this faster!
2286   foreach (MapTile t; objGrid.allObjects(MapTile)) {
2287     if (t.spectral) continue;
2288     int xc = px-t.xCenter, yc = py-t.yCenter;
2289     int distsq = xc*xc+yc*yc;
2290     if (distsq < curdistsq && dg(t)) {
2291       res = t;
2292       curdistsq = distsq;
2293     }
2294   }
2296   return res;
2300 // ////////////////////////////////////////////////////////////////////////// //
2301 final bool cbIsObjectWeb (MapObject o) { return (o isa ItemWeb); }
2302 final bool cbIsObjectBlob (MapObject o) { return (o isa EnemyBlob); }
2303 final bool cbIsObjectArrow (MapObject o) { return (o isa ItemProjectileArrow); }
2304 final bool cbIsObjectEnemy (MapObject o) { return (/*o != self &&*/ o isa MapEnemy); }
2306 final bool cbIsObjectTreasure (MapObject o) { return (o.isTreasure); }
2308 final bool cbIsItemObject (MapObject o) { return (o isa MapItem); }
2310 final bool cbIsItemOrEnemyObject (MapObject o) { return (o isa MapItem || o isa MapEnemy); }
2313 final MapObject isObjectAtTile (int tileX, int tileY, optional scope bool delegate (MapObject o) dg, optional bool precise) {
2314   if (!specified_precise) precise = true;
2315   tileX *= 16;
2316   tileY *= 16;
2317   foreach (MapObject o; objGrid.inRectPix(tileX, tileY, 16, 16, precise:precise, castClass:MapObject)) {
2318     if (o.spectral) continue;
2319     if (dg) {
2320       if (dg(o)) return o;
2321     } else {
2322       return o;
2323     }
2324   }
2325   return none;
2329 final MapObject isObjectAtTilePix (int x, int y, optional scope bool delegate (MapObject o) dg) {
2330   return isObjectAtTile(x/16, y/16, dg!optional);
2334 final MapObject isObjectAtPoint (int xpos, int ypos, optional scope bool delegate (MapObject o) dg, optional bool precise, optional class!MapObject castClass) {
2335   if (!specified_precise) precise = true;
2336   if (!castClass) castClass = MapObject;
2337   foreach (MapObject o; objGrid.inCellPix(xpos, ypos, precise:precise, castClass:castClass)) {
2338     if (o.spectral) continue;
2339     if (dg) {
2340       if (dg(o)) return o;
2341     } else {
2342       if (o isa MapEnemy) return o;
2343     }
2344   }
2345   return none;
2349 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) {
2350   if (w < 1 || h < 1) return none;
2351   if (!castClass) castClass = MapObject;
2352   if (w == 1 && h == 1) return isObjectAtPoint(xpos, ypos, dg!optional, precise!optional, castClass);
2353   if (!specified_precise) precise = true;
2354   foreach (MapObject o; objGrid.inRectPix(xpos, ypos, w, h, precise:precise, castClass:castClass)) {
2355     if (o.spectral) continue;
2356     if (dg) {
2357       if (dg(o)) return o;
2358     } else {
2359       if (o isa MapEnemy) return o;
2360     }
2361   }
2362   return none;
2366 final MapObject forEachObject (scope bool delegate (MapObject o) dg, optional bool allowSpectrals, optional class!MapObject castClass) {
2367   if (!dg) return none;
2368   if (!castClass) castClass = MapObject;
2369   foreach (MapObject o; objGrid.allObjectsSafe(castClass)) {
2370     if (!allowSpectrals && o.spectral) continue;
2371     if (dg(o)) return o;
2372   }
2373   return none;
2377 final MapObject forEachObjectAtPoint (int xpos, int ypos, scope bool delegate (MapObject o) dg, optional bool precise) {
2378   if (!dg) return none;
2379   if (!specified_precise) precise = true;
2380   foreach (MapObject o; objGrid.inCellPix(xpos, ypos, precise:precise, castClass:MapObject)) {
2381     if (o.spectral) continue;
2382     if (dg(o)) return o;
2383   }
2384   return none;
2388 final MapObject forEachObjectInRect (int xpos, int ypos, int w, int h, scope bool delegate (MapObject o) dg, optional bool precise) {
2389   if (!dg || w < 1 || h < 1) return none;
2390   if (w == 1 && h == 1) return forEachObjectAtPoint(xpos, ypos, dg!optional, precise!optional);
2391   if (!specified_precise) precise = true;
2392   foreach (MapObject o; objGrid.inRectPix(xpos, ypos, w, h, precise:precise, castClass:MapObject)) {
2393     if (o.spectral) continue;
2394     if (dg(o)) return o;
2395   }
2396   return none;
2400 final MapEntity forEachEntityInRect (int xpos, int ypos, int w, int h, scope bool delegate (MapEntity o) dg, optional bool precise, optional class!MapEntity castClass) {
2401   if (!dg || w < 1 || h < 1) return none;
2402   if (!castClass) castClass = MapEntity;
2403   if (!specified_precise) precise = true;
2404   foreach (MapEntity e; objGrid.inRectPix(xpos, ypos, w, h, precise:precise, castClass:castClass)) {
2405     if (e.spectral) continue;
2406     if (dg(e)) return e;
2407   }
2408   return none;
2412 private final bool cbIsRopeTile (MapTile t) { return (t isa MapTileRope); }
2414 final MapTile isRopeAtPoint (int px, int py) {
2415   return checkTileAtPoint(px, py, &cbIsRopeTile);
2419 //FIXME!
2420 final MapTile isWaterSwimAtPoint (int px, int py) {
2421   return isWaterAtPoint(px, py);
2425 // ////////////////////////////////////////////////////////////////////////// //
2426 private array!MapEntity tmpEntityList;
2428 private final bool cbCollectEntitiesWithMask (MapEntity t) {
2429   if (!t.visible || t.spectral) return false;
2430   tmpEntityList[$] = t;
2431   return false;
2435 final void touchEntitiesWithMask (int x, int y, SpriteFrame frm, scope bool delegate (MapEntity t) dg, optional class!MapEntity castClass) {
2436   if (!frm || frm.bw < 1 || frm.bh < 1 || !dg) return;
2437   if (frm.isEmptyPixelMask) return;
2438   if (!castClass) castClass = MapEntity;
2439   // collect tiles
2440   if (tmpEntityList.length) tmpEntityList.clear();
2441   if (player isa castClass && player.isRectCollisionFrame(frm, x, y)) tmpEntityList[$] = player;
2442   forEachEntityInRect(x+frm.bx, y+frm.by, frm.bw, frm.bh, &cbCollectEntitiesWithMask, castClass:castClass);
2443   foreach (MapEntity e; tmpEntityList) {
2444     if (!e || !e.isInstanceAlive || !e.visible || e.spectral) continue;
2445     if (e.isRectCollisionFrame(frm, x, y)) {
2446       if (dg(e)) break;
2447     }
2448   }
2452 // ////////////////////////////////////////////////////////////////////////// //
2453 final bool cbCollisionAnySolid (MapTile t) { return t.solid; }
2454 final bool cbCollisionAnyMoveableSolid (MapTile t) { return t.solid && t.moveable; }
2455 final bool cbCollisionLiquid (MapTile t) { return (t.water || t.lava); }
2456 final bool cbCollisionAnySolidOrLiquid (MapTile t) { return (t.solid || t.water || t.lava); }
2457 final bool cbCollisionLadder (MapTile t) { return t.ladder; }
2458 final bool cbCollisionLadderTop (MapTile t) { return t.laddertop; }
2459 final bool cbCollisionAnyLadder (MapTile t) { return (t.ladder || t.laddertop); }
2460 final bool cbCollisionAnySolidIce (MapTile t) { return (t.solid && t.ice); }
2461 final bool cbCollisionWater (MapTile t) { return t.water; }
2462 final bool cbCollisionLava (MapTile t) { return t.lava; }
2463 final bool cbCollisionAnyTree (MapTile t) { return t.tree; }
2464 final bool cbCollisionPlatform (MapTile t) { return t.platform; }
2465 final bool cbCollisionLadderOrPlatform (MapTile t) { return (t.ladder || t.platform); }
2466 //final bool cbCollisionAnyHangeable (MapTile t) { return t.hangeable; } // no need to check for solids here, it is done in MapTile initializer
2467 final bool cbCollisionSpringTrap (MapTile t) { return t.springtrap; }
2468 final bool cbCollisionSpikes (MapTile t) { return t.spikes; }
2469 final bool cbCollisionExitTile (MapTile t) { return t.isExitActive(); }
2471 final bool cbCollisionForWhoa (MapTile t) { return (t.solid || t.laddertop); }
2473 //final bool cbCollisionSacAltar (MapTile t) { return (t.objName == 'oSacAltarLeft'); } // only left part, yeah
2474 final bool cbCollisionSacAltar (MapTile t) { return t.sacrificingAltar; }
2477 // ////////////////////////////////////////////////////////////////////////// //
2478 transient MapTileTemp tempSolidTile;
2480 private final MapTileTemp makeWalkeableSolidTile (MapObject o) {
2481   if (!tempSolidTile) {
2482     tempSolidTile = SpawnObject(MapTileTemp);
2483   } else if (!tempSolidTile.isInstanceAlive) {
2484     delete tempSolidTile;
2485     tempSolidTile = SpawnObject(MapTileTemp);
2486   }
2487   // setup data
2488   tempSolidTile.level = self;
2489   tempSolidTile.global = global;
2490   tempSolidTile.solid = true;
2491   tempSolidTile.objName = MapTileTemp.default.objName;
2492   tempSolidTile.objType = MapTileTemp.default.objType;
2493   tempSolidTile.e = o;
2494   tempSolidTile.fltx = o.fltx;
2495   tempSolidTile.flty = o.flty;
2496   return tempSolidTile;
2500 final MapTile checkTilesInRect (int x0, int y0, const int w, const int h,
2501                                 optional scope bool delegate (MapTile dg) dg, optional bool precise,
2502                                 optional class!MapTile castClass)
2504   if (w < 1 || h < 1) return none;
2505   if (w == 1 && h == 1) return checkTileAtPoint(x0, y0, dg!optional);
2506   int x1 = x0+w-1, y1 = y0+h-1;
2507   if (x0 >= tilesWidth*16 || y0 >= tilesHeight*16 || x1 < 0 || y1 < 0) return none;
2508   if (!specified_precise) precise = true;
2509   if (!castClass) castClass = MapTile;
2510   if (!dg) dg = &cbCollisionAnySolid;
2512   // check walkable solid objects too
2513   foreach (MapEntity e; objGrid.inRectPix(x0, y0, w, h, precise:precise, castClass:castClass)) {
2514     if (e.spectral || !e.visible) continue;
2515     auto t = MapTile(e);
2516     if (t) {
2517       if (dg(t)) return t;
2518       continue;
2519     }
2520     auto o = MapObject(e);
2521     if (o && o.walkableSolid) {
2522       t = makeWalkeableSolidTile(o);
2523       if (dg(t)) return t;
2524       continue;
2525     }
2526   }
2528   return none;
2532 final MapTile checkTileAtPoint (int x0, int y0, optional scope bool delegate (MapTile dg) dg, optional bool precise, optional class!MapTile castClass) {
2533   if (x0 < 0 || y0 < 0 || x0 >= tilesWidth*16 || y0 >= tilesHeight*16) return none;
2534   if (!specified_precise) precise = true;
2535   if (!castClass) castClass = MapTile;
2536   if (!dg) dg = &cbCollisionAnySolid;
2538   // check walkable solid objects
2539   foreach (MapEntity e; objGrid.inCellPix(x0, y0, precise:precise, castClass:castClass)) {
2540     if (e.spectral || !e.visible) continue;
2541     auto t = MapTile(e);
2542     if (t) {
2543       if (dg(t)) return t;
2544       continue;
2545     }
2546     auto o = MapObject(e);
2547     if (o && o.walkableSolid) {
2548       t = makeWalkeableSolidTile(o);
2549       if (dg(t)) return t;
2550       continue;
2551     }
2552   }
2554   return none;
2558 // ////////////////////////////////////////////////////////////////////////// //
2559 final MapTile isSolidAtPoint (int px, int py) { return checkTileAtPoint(px, py); }
2560 final MapTile isSolidInRect (int rx, int ry, int rw, int rh) { return checkTilesInRect(rx, ry, rw, rh); }
2561 final MapTile isLadderAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadder); }
2562 final MapTile isLadderOrPlatformAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadderOrPlatform); }
2563 final MapTile isLadderTopAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadderTop); }
2564 final MapTile isAnyLadderAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyLadder); }
2565 final MapTile isWaterAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionWater); }
2566 final MapTile isLavaAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLava); }
2567 final MapTile isLiquidAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLiquid); }
2568 final MapTile isIceAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnySolidIce); }
2569 final MapTile isTreeAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyTree); }
2570 //final MapTile isHangeableAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyHangeable); }
2573 // ////////////////////////////////////////////////////////////////////////// //
2574 final void setTileTypeAt (int tileX, int tileY, LevelGen::RType rt) {
2575   if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) roomType[tileX, tileY] = rt;
2579 //FIXME: make this faster
2580 transient float gtagX, gtagY;
2582 // only non-moveables and non-specials
2583 final MapTile getTileAtGrid (int tileX, int tileY) {
2584   gtagX = tileX*16;
2585   gtagY = tileY*16;
2586   return checkTileAtPoint(tileX*16, tileY*16, delegate bool (MapTile t) {
2587     if (t.spectral || t.moveable || t.toSpecialGrid || !t.visible) return false;
2588     if (t.fltx != gtagX || t.flty != gtagY) return false; // emulate grid
2589     if (t.width != 16 || t.height != 16) return false;
2590     return true;
2591   }, precise:false);
2592   //return (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight ? tiles[tileX, tileY] : none);
2596 final MapTile getTileAtGridAny (int tileX, int tileY) {
2597   gtagX = tileX*16;
2598   gtagY = tileY*16;
2599   return checkTileAtPoint(tileX*16, tileY*16, delegate bool (MapTile t) {
2600     if (t.spectral /*|| t.moveable*/ || !t.visible) return false;
2601     if (t.fltx != gtagX || t.flty != gtagY) return false; // emulate grid
2602     if (t.width != 16 || t.height != 16) return false;
2603     return true;
2604   }, precise:false);
2605   //return (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight ? tiles[tileX, tileY] : none);
2609 final bool isNamedTileAt (int tileX, int tileY, name atypename) {
2610   if (!atypename) return false;
2611   auto t = getTileAtGridAny(tileX, tileY);
2612   return (t && t.objName == atypename);
2616 final void setTileAtGrid (int tileX, int tileY, MapTile tile) {
2617   if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
2618     if (tile) {
2619       tile.fltx = tileX*16;
2620       tile.flty = tileY*16;
2621       if (!tile.dontReplaceOthers) {
2622         auto osp = tile.spectral;
2623         tile.spectral = true;
2624         auto t = getTileAtGridAny(tileX, tileY);
2625         tile.spectral = osp;
2626         if (t && !t.immuneToReplacement) {
2627           writeln("REPLACING tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
2628           writeln("      NEW tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (tile.toSpecialGrid ? "special " : ""), "grid, pos=(", tile.fltx/16.0, ",", tile.flty/16.0, ")");
2629           t.instanceRemove();
2630         }
2631       }
2632       insertObject(tile);
2633     } else {
2634       auto t = getTileAtGridAny(tileX, tileY);
2635       if (t && !t.immuneToReplacement) {
2636         writeln("REMOVING tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
2637         t.instanceRemove();
2638       }
2639     }
2640   }
2644 // ////////////////////////////////////////////////////////////////////////// //
2645 // return `true` from delegate to stop
2646 MapTile forEachSolidTileOnGrid (scope bool delegate (int tileX, int tileY, MapTile t) dg) {
2647   if (!dg) return none;
2648   foreach (MapTile t; objGrid.allObjectsSafe(MapTile)) {
2649     if (t.spectral || !t.solid || !t.visible) continue;
2650     if (t.ix%16 != 0 || t.iy%16 != 0) continue; // emulate grid
2651     if (t.width != 16 || t.height != 16) continue;
2652     if (dg(t.ix/16, t.iy/16, t)) return t;
2653   }
2654   return none;
2658 // ////////////////////////////////////////////////////////////////////////// //
2659 // return `true` from delegate to stop
2660 MapTile forEachTile (scope bool delegate (MapTile t) dg, optional class!MapTile castClass) {
2661   if (!dg) return none;
2662   if (!castClass) castClass = MapTile;
2663   foreach (MapTile t; objGrid.allObjectsSafe(castClass)) {
2664     if (t.spectral || !t.visible) continue;
2665     if (dg(t)) return t;
2666   }
2667   return none;
2671 // ////////////////////////////////////////////////////////////////////////// //
2672 final void fixWallTiles () {
2673   foreach (MapTile t; objGrid.allObjectsSafe(MapTile)) t.beautifyTile();
2677 // ////////////////////////////////////////////////////////////////////////// //
2678 final MapTile isCollisionAtPoint (int px, int py, optional scope bool delegate (MapTile dg) dg) {
2679   if (!dg) dg = &cbCollisionAnySolid;
2680   return checkTilesInRect(px, py, 1, 1, dg);
2684 // ////////////////////////////////////////////////////////////////////////// //
2685 string scrGetKaliGift (MapTile altar, optional name gift) {
2686   string res;
2688   // find other side of the altar
2689   int sx = player.ix, sy = player.iy;
2690   if (altar) {
2691     sx = altar.ix;
2692     sy = altar.iy;
2693     auto a2 = isCollisionAtPoint(altar.ix+18, altar.iy+8, &cbCollisionSacAltar);
2694     if (!a2) a2 = isCollisionAtPoint(altar.ix-8, altar.iy+8, &cbCollisionSacAltar);
2695     if (a2) { sx = a2.ix; sy = a2.iy; }
2696   }
2698        if (global.favor <= -8) res = "SHE SEEMS VERY ANGRY WITH YOU!";
2699   else if (global.favor < 0) res = "SHE SEEMS ANGRY WITH YOU.";
2700   else if (global.favor == 0) res = "SHE HAS FORGIVEN YOU!";
2701   else if (global.favor >= 32) {
2702     if (global.kaliGift >= 3 && global.favor >= 32+(global.kaliGift-2)*16) {
2703       res = "YOU FEEL INVIGORATED!";
2704       global.kaliGift += 1;
2705       global.plife += global.randOther(4, 8);
2706     } else if (global.kaliGift >= 3) {
2707       res = "SHE SEEMS ECSTATIC WITH YOU!";
2708     } else if (global.bombs < 80) {
2709       res = "YOUR SATCHEL FEELS VERY FULL NOW!";
2710       global.kaliGift = 3;
2711       global.bombs = 99;
2712     } else {
2713       res = "YOU FEEL INVIGORATED!";
2714       global.kaliGift += 1;
2715       global.plife += global.randOther(4, 8);
2716     }
2717   } else if (global.favor >= 16) {
2718     if (global.kaliGift >= 2) {
2719       res = "SHE SEEMS VERY HAPPY WITH YOU!";
2720     } else {
2721       res = "SHE BESTOWS A GIFT UPON YOU!";
2722       global.kaliGift = 2;
2723       // poofs
2724       auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2725       obj.xVel = -1;
2726       obj.yVel = 0;
2727       obj = MakeMapObject(sx, sy-8, 'oPoof');
2728       obj.xVel = 1;
2729       obj.yVel = 0;
2730       // a gift
2731       obj = none;
2732       if (gift) obj = MakeMapObject(sx, sy-8, gift);
2733       if (!obj) obj = MakeMapObject(sx, sy-8, 'oKapala');
2734     }
2735   } else if (global.favor >= 8) {
2736     if (global.kaliGift >= 1) {
2737       res = "SHE SEEMS HAPPY WITH YOU.";
2738     } else {
2739       res = "SHE BESTOWS A GIFT UPON YOU!";
2740       global.kaliGift = 1;
2741       //rAltar = instance_nearest(x, y, oSacAltarRight);
2742       //if (instance_exists(rAltar)) {
2743       // poofs
2744       auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2745       obj.xVel = -1;
2746       obj.yVel = 0;
2747       obj = MakeMapObject(sx, sy-8, 'oPoof');
2748       obj.xVel = 1;
2749       obj.yVel = 0;
2750       obj = none;
2751       if (gift) obj = MakeMapObject(sx, sy-8, gift);
2752       if (!obj) {
2753         auto n = global.randOther(1, 8);
2754         auto m = n;
2755         for (;;) {
2756           name aname = '';
2757                if (n == 1 && !global.hasCape && !global.hasJetpack) aname = 'oCapePickup';
2758           else if (n == 2 && !global.hasGloves) aname = 'oGloves';
2759           else if (n == 3 && !global.hasSpectacles) aname = 'oSpectacles';
2760           else if (n == 4 && !global.hasMitt) aname = 'oMitt';
2761           else if (n == 5 && !global.hasSpringShoes) aname = 'oSpringShoes';
2762           else if (n == 6 && !global.hasSpikeShoes) aname = 'oSpikeShoes';
2763           else if (n == 7 && !global.hasStickyBombs) aname = 'oPaste';
2764           else if (n == 8 && !global.hasCompass) aname = 'oCompass';
2765           if (aname) {
2766             obj = MakeMapObject(sx, sy-8, aname);
2767             if (obj) break;
2768           }
2769           ++n;
2770           if (n > 8) n = 1;
2771           if (n == m) {
2772             obj = MakeMapObject(sx, sy-8, (global.hasJetpack ? 'oBombBox' : 'oJetpack'));
2773             break;
2774           }
2775         }
2776       }
2777     }
2778   } else if (global.favor > 0) {
2779     res = "SHE SEEMS PLEASED WITH YOU.";
2780   }
2782   /*
2783   if (argument1) {
2784     global.message = "";
2785     res = "KALI DEVOURS YOU!"; // sacrifice is player
2786   }
2787   */
2789   return res;
2793 void performSacrifice (MapObject what, MapTile where) {
2794   if (!what || !what.isInstanceAlive) return;
2795   MakeMapObject(what.ix+8, what.iy+8, 'oFlame');
2796   if (where) where.playSound('sndSmallExplode'); else what.playSound('sndSmallExplode');
2797   if (!what.bloodless) what.scrCreateBlood(what.ix+8, what.iy+8, 3);
2799   string msg = "KALI ACCEPTS THE SACRIFICE!";
2801   auto idol = ItemGoldIdol(what);
2802   if (idol) {
2803     ++stats.totalSacrifices;
2804          if (global.favor <= -8) msg = "SHE SEEMS VERY ANGRY WITH YOU!";
2805     else if (global.favor < 0) msg = "SHE SEEMS ANGRY WITH YOU.";
2806     else if (global.favor >= 0) {
2807       // find other side of the altar
2808       int sx = player.ix, sy = player.iy;
2809       auto altar = where;
2810       if (altar) {
2811         sx = altar.ix;
2812         sy = altar.iy;
2813         auto a2 = isCollisionAtPoint(altar.ix+18, altar.iy+8, &cbCollisionSacAltar);
2814         if (!a2) a2 = isCollisionAtPoint(altar.ix-8, altar.iy+8, &cbCollisionSacAltar);
2815         if (a2) { sx = a2.ix; sy = a2.iy; }
2816       }
2817       // poofs
2818       auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2819       obj.xVel = -1;
2820       obj.yVel = 0;
2821       obj = MakeMapObject(sx, sy-8, 'oPoof');
2822       obj.xVel = 1;
2823       obj.yVel = 0;
2824       // a gift
2825       obj = MakeMapObject(sx, sy-16-8, 'oMonkeyGold');
2826     }
2827     osdMessage(msg, 6.66);
2828     scrShake(10);
2829     idol.instanceRemove();
2830     return;
2831   }
2833   if (global.favor <= -8) {
2834     msg = "KALI DEVOURS THE SACRIFICE!";
2835   } else if (global.favor < 0) {
2836     global.favor += (what.status == MapObject::STUNNED ? what.favor : round(what.favor/2.0)); //k8: added `round()`
2837     if (what.favor > 0) what.favor = 0;
2838   } else {
2839     global.favor += (what.status == MapObject::STUNNED ? what.favor : round(what.favor/2.0)); //k8: added `round()`
2840   }
2842   /*!!
2843        if (myAltar.gift1 != "" and global.kaliGift == 0) scrGetKaliGift(myAltar.gift1);
2844   else if (myAltar.gift2 != "" and global.kaliGift == 1) scrGetKaliGift(myAltar.gift2);
2845   else scrGetKaliGift("");
2846   */
2848   // sacrifice is player?
2849   if (what isa PlayerPawn) {
2850     ++stats.totalSelfSacrifices;
2851     msg = "KALI DEVOURS YOU!";
2852     player.visible = false;
2853     player.removeBallAndChain(temp:true);
2854     player.dead = true;
2855     player.status = MapObject::DEAD;
2856   } else {
2857     ++stats.totalSacrifices;
2858     auto msg2 = scrGetKaliGift(where);
2859     what.instanceRemove();
2860     if (msg2) msg = va("%s\n%s", msg, msg2);
2861   }
2863   osdMessage(msg, 6.66);
2865   //!if (isRealLevel()) global.totalSacrifices += 1;
2867   //!global.messageTimer = 200;
2868   //!global.shake = 10;
2869   scrShake(10);
2871   /*damsel
2872   instance_create(x, y, oFlame);
2873   playSound(global.sndSmallExplode);
2874   scrCreateBlood(x, y, 3);
2875   global.message = "KALI ACCEPTS YOUR SACRIFICE!";
2876   if (global.favor <= -8) {
2877     global.message = "KALI DEVOURS YOUR SACRIFICE!";
2878   } else if (global.favor < 0) {
2879     if (status == 98) global.favor += favor*1.5; else global.favor += favor;
2880     if (favor > 0) favor = 0;
2881   } else {
2882     if (status == 98) global.favor += favor*1.5; else global.favor += favor;
2883   }
2885        if (myAltar.gift1 != "" and global.kaliGift == 0) scrGetFavorMsg(myAltar.gift1);
2886   else if (myAltar.gift2 != "" and global.kaliGift == 1) scrGetFavorMsg(myAltar.gift2);
2887   else scrGetFavorMsg("");
2889   global.messageTimer = 200;
2890   global.shake = 10;
2891   instance_destroy();
2892   */
2896 // ////////////////////////////////////////////////////////////////////////// //
2897 final void addBackgroundGfxDetails () {
2898   // add background details
2899   //if (global.customLevel) return;
2900   foreach (; 0..20) {
2901     // bg = instance_create(16*randRoom(1, 42), 16*randRoom(1, 33), oCaveBG);
2902          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);
2903     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);
2904     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);
2905     else MakeMapBackTile('bgExtras', 32*global.randRoom(0, 1), 0, 32, 32, 16*global.randRoom(1, 42), 16*global.randRoom(1, 33), 10002);
2906   }
2910 // ////////////////////////////////////////////////////////////////////////// //
2911 private final void fixRealViewStart () {
2912   int scale = global.scale;
2913   realViewStart.x = clamp(realViewStart.x, viewMin.x*scale, viewMax.x*scale-viewWidth);
2914   realViewStart.y = clamp(realViewStart.y, viewMin.y*scale, viewMax.y*scale-viewHeight);
2918 final int cameraCurrX () { return realViewStart.x/global.scale; }
2919 final int cameraCurrY () { return realViewStart.y/global.scale; }
2922 private final void fixViewStart () {
2923   int scale = global.scale;
2924   viewStart.x = clamp(viewStart.x, viewMin.x*scale, viewMax.x*scale-viewWidth);
2925   viewStart.y = clamp(viewStart.y, viewMin.y*scale, viewMax.y*scale-viewHeight);
2929 final void centerViewAtPlayer () {
2930   if (viewWidth < 1 || viewHeight < 1 || !player) return;
2931   centerViewAt(player.xCenter, player.yCenter);
2935 final void centerViewAt (int x, int y) {
2936   if (viewWidth < 1 || viewHeight < 1) return;
2938   cameraSlideToSpeed.x = 0;
2939   cameraSlideToSpeed.y = 0;
2940   cameraSlideToPlayer = 0;
2942   int scale = global.scale;
2943   x *= scale;
2944   y *= scale;
2945   realViewStart.x = clamp(x-viewWidth/2, 0, tilesWidth*16*scale-viewWidth);
2946   realViewStart.y = clamp(y-viewHeight/2, 0, tilesHeight*16*scale-viewHeight);
2947   fixRealViewStart();
2949   viewStart.x = realViewStart.x;
2950   viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
2951   fixViewStart();
2953   if (onCameraTeleported) onCameraTeleported();
2957 const int ViewPortToleranceX = 16*1+8;
2958 const int ViewPortToleranceY = 16*1+8;
2960 final void fixCamera () {
2961   if (!player) return;
2962   if (viewWidth < 1 || viewHeight < 1) return;
2963   int scale = global.scale;
2964   auto alwaysCenterX = global.config.alwaysCenterPlayer;
2965   auto alwaysCenterY = alwaysCenterX;
2966   // calculate offset from viewport center (in game units), and fix viewport
2968   int camDestX = player.ix+8;
2969   int camDestY = player.iy+8;
2970   if (!cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y) != 0) {
2971     // slide camera to point
2972     if (cameraSlideToSpeed.x) camDestX = cameraSlideToCurr.x;
2973     if (cameraSlideToSpeed.y) camDestY = cameraSlideToCurr.y;
2974     int dx = cameraSlideToDest.x-camDestX;
2975     int dy = cameraSlideToDest.y-camDestY;
2976     //writeln("speed:(", cameraSlideToSpeed.x, ",", cameraSlideToSpeed.y, "); cur:(", camDestX, ",", camDestY, "); dest:(", cameraSlideToDest.x, ",", cameraSlideToDest.y, "); delta=(", dx, ",", dy, ")");
2977     if (dx && cameraSlideToSpeed.x != 0) {
2978       alwaysCenterX = true;
2979       if (abs(dx) <= abs(cameraSlideToSpeed.x)) {
2980         camDestX = cameraSlideToDest.x;
2981       } else {
2982         camDestX += sign(dx)*abs(cameraSlideToSpeed.x);
2983       }
2984     }
2985     if (dy && abs(cameraSlideToSpeed.y) != 0) {
2986       alwaysCenterY = true;
2987       if (abs(dy) <= cameraSlideToSpeed.y) {
2988         camDestY = cameraSlideToDest.y;
2989       } else {
2990         camDestY += sign(dy)*abs(cameraSlideToSpeed.y);
2991       }
2992     }
2993     //writeln("  new:(", camDestX, ",", camDestY, ")");
2994     if (cameraSlideToSpeed.x) cameraSlideToCurr.x = camDestX;
2995     if (cameraSlideToSpeed.y) cameraSlideToCurr.y = camDestY;
2996   }
2998   // horizontal
2999   if (cameraSlideToSpeed.x && !cameraSlideToPlayer) {
3000     realViewStart.x = clamp(camDestX*scale, 0, tilesWidth*16*scale-viewWidth);
3001   } else if (!player.cameraBlockX) {
3002     int x = camDestX*scale;
3003     int cx = realViewStart.x;
3004     if (alwaysCenterX) {
3005       cx = x-viewWidth/2;
3006     } else {
3007       int xofs = x-(cx+viewWidth/2);
3008            if (xofs < -ViewPortToleranceX*scale) cx += xofs+ViewPortToleranceX*scale;
3009       else if (xofs > ViewPortToleranceX*scale) cx += xofs-ViewPortToleranceX*scale;
3010     }
3011     // slide back to player?
3012     if (cameraSlideToPlayer && cameraSlideToSpeed.x) {
3013       int prevx = cameraSlideToCurr.x*scale;
3014       int dx = (cx-prevx)/scale;
3015       if (abs(dx) <= cameraSlideToSpeed.x) {
3016         writeln("BACKSLIDE X COMPLETE!");
3017         cameraSlideToSpeed.x = 0;
3018       } else {
3019         cx = prevx+(sign(dx)*cameraSlideToSpeed.x)*scale;
3020         cameraSlideToCurr.x += sign(dx)*cameraSlideToSpeed.x;
3021         if ((dx < 0 && cx <= 0) || (dx > 0 && cx >= tilesWidth*16*scale-viewWidth)) {
3022           writeln("BACKSLIDE X COMPLETE!");
3023           cameraSlideToSpeed.x = 0;
3024         }
3025       }
3026     }
3027     realViewStart.x = clamp(cx, 0, tilesWidth*16*scale-viewWidth);
3028   }
3030   // vertical
3031   if (cameraSlideToSpeed.y && !cameraSlideToPlayer) {
3032     realViewStart.y = clamp(camDestY*scale, 0, tilesHeight*16*scale-viewHeight);
3033   } else if (!player.cameraBlockY) {
3034     int y = camDestY*scale;
3035     int cy = realViewStart.y;
3036     if (alwaysCenterY) {
3037       cy = y-viewHeight/2;
3038     } else {
3039       int yofs = y-(cy+viewHeight/2);
3040            if (yofs < -ViewPortToleranceY*scale) cy += yofs+ViewPortToleranceY*scale;
3041       else if (yofs > ViewPortToleranceY*scale) cy += yofs-ViewPortToleranceY*scale;
3042     }
3043     // slide back to player?
3044     if (cameraSlideToPlayer && cameraSlideToSpeed.y) {
3045       int prevy = cameraSlideToCurr.y*scale;
3046       int dy = (cy-prevy)/scale;
3047       if (abs(dy) <= cameraSlideToSpeed.y) {
3048         writeln("BACKSLIDE Y COMPLETE!");
3049         cameraSlideToSpeed.y = 0;
3050       } else {
3051         cy = prevy+(sign(dy)*cameraSlideToSpeed.y)*scale;
3052         cameraSlideToCurr.y += sign(dy)*cameraSlideToSpeed.y;
3053         if ((dy < 0 && cy <= 0) || (dy > 0 && cy >= tilesHeight*16*scale-viewHeight)) {
3054           writeln("BACKSLIDE Y COMPLETE!");
3055           cameraSlideToSpeed.y = 0;
3056         }
3057       }
3058     }
3059     realViewStart.y = clamp(cy, 0, tilesHeight*16*scale-viewHeight);
3060   }
3062   if (cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y) == 0) cameraSlideToPlayer = 0;
3064   fixRealViewStart();
3065   //writeln("  new2:(", cameraCurrX, ",", cameraCurrY, ")");
3067   viewStart.x = realViewStart.x;
3068   viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
3069   fixViewStart();
3073 // ////////////////////////////////////////////////////////////////////////// //
3074 // x0 and y0 are non-scaled (and will be scaled)
3075 final void drawSpriteAt (name sprName, float frnumf, int x0, int y0, optional bool small) {
3076   if (!sprName) return;
3077   auto spr = sprStore[sprName];
3078   if (!spr || !spr.frames.length) return;
3079   int scale = global.scale;
3080   x0 *= scale;
3081   y0 *= scale;
3082   int frnum = max(0, trunc(frnumf))%spr.frames.length;
3083   auto sfr = spr.frames[frnum];
3084   int sx0 = x0-sfr.xofs*scale;
3085   int sy0 = y0-sfr.yofs*scale;
3086   if (small && scale > 1) {
3087     sfr.tex.blitExt(sx0, sy0, round(sx0+sfr.tex.width*(scale/2.0)), round(sy0+sfr.tex.height*(scale/2.0)), 0, 0);
3088   } else {
3089     sfr.tex.blitAt(sx0, sy0, scale);
3090   }
3094 final void drawSpriteAtS3 (name sprName, float frnumf, int x0, int y0) {
3095   if (!sprName) return;
3096   auto spr = sprStore[sprName];
3097   if (!spr || !spr.frames.length) return;
3098   x0 *= 3;
3099   y0 *= 3;
3100   int frnum = max(0, trunc(frnumf))%spr.frames.length;
3101   auto sfr = spr.frames[frnum];
3102   int sx0 = x0-sfr.xofs*3;
3103   int sy0 = y0-sfr.yofs*3;
3104   sfr.tex.blitAt(sx0, sy0, 3);
3108 // x0 and y0 are non-scaled (and will be scaled)
3109 final void drawTextAt (int x0, int y0, string text, optional int scale, optional int hiColor1, optional int hiColor2) {
3110   if (!text) return;
3111   if (!specified_scale) scale = global.scale;
3112   x0 *= scale;
3113   y0 *= scale;
3114   sprStore.renderTextWithHighlight(x0, y0, text, scale, hiColor1!optional, hiColor2!optional);
3118 void renderCompass (float currFrameDelta) {
3119   if (!global.hasCompass) return;
3121   /*
3122   if (isRoom("rOlmec")) {
3123     global.exitX = 648;
3124     global.exitY = 552;
3125   } else if (isRoom("rOlmec2")) {
3126     global.exitX = 648;
3127     global.exitY = 424;
3128   }
3129   */
3131   bool hasMessage = osdHasMessage();
3132   foreach (MapTile et; allExits) {
3133     // original compass
3134     int exitX = et.ix, exitY = et.iy;
3135     int vx0 = viewStart.x/global.scale, vy0 = viewStart.y/global.scale;
3136     int vx1 = (viewStart.x+viewWidth)/global.scale;
3137     int vy1 = (viewStart.y+viewHeight)/global.scale;
3138     if (exitY > vy1-16) {
3139       if (exitX < vx0) {
3140         drawSpriteAt(hasMessage ? 'sCompassSmallLL' : 'sCompassLL', 0, 0, 224);
3141       } else if (exitX > vx1-16) {
3142         drawSpriteAt(hasMessage ? 'sCompassSmallLR' : 'sCompassLR', 0, 304, 224);
3143       } else {
3144         drawSpriteAt(hasMessage ? 'sCompassSmallDown' : 'sCompassDown', 0, exitX-vx0, 224);
3145       }
3146     } else if (exitX < vx0) {
3147       drawSpriteAt(hasMessage ? 'sCompassSmallLeft' : 'sCompassLeft', 0, 0, exitY-vy0);
3148     } else if (exitX > vx1-16) {
3149       drawSpriteAt(hasMessage ? 'sCompassSmallRight' : 'sCompassRight', 0, 304, exitY-vy0);
3150     }
3151     break; // only the first exit
3152   }
3156 final bool totalsNameCmpCB (ref GameStats::TotalItem a, ref GameStats::TotalItem b) {
3157   auto sa = string(a.objName);
3158   auto sb = string(b.objName);
3159   return (sa < sb);
3162 void renderTransitionInfo (float currFrameDelta) {
3163   //FIXME!
3164   /*
3165   GameStats.sortTotalsList(stats.kills, &totalsNameCmpCB);
3167   int maxLen = 0;
3168   foreach (int idx, ref auto k; stats.kills) {
3169     string s = string(k);
3170     maxLen = max(maxLen, s.length);
3171   }
3172   maxLen *= 8;
3174   sprStore.loadFont('sFontSmall');
3175   Video.color = 0xff_ff_00;
3176   foreach (int idx, ref auto k; stats.kills) {
3177     int deaths = 0;
3178     foreach (int xidx, ref auto d; stats.totalKills) {
3179       if (d.objName == k) { deaths = d.count; break; }
3180     }
3181     //drawTextAt(16, 4+idx*8, va("%s: %d (%d)", string(k.objName).toUpperCase, k.count, deaths));
3182     drawTextAt(16, 4+idx*8, string(k).toUpperCase);
3183     drawTextAt(16+maxLen, 4+idx*8, va(": %d (%d)", k.count, deaths));
3184   }
3185   */
3189 void renderGhostTimer (float currFrameDelta) {
3190   if (ghostTimeLeft <= 0) return;
3191   //ghostTimeLeft /= 30; // frames -> seconds
3193   int hgt = Video.screenHeight-64;
3194   if (hgt < 1) return;
3195   int rhgt = round(float(hgt)*(float(ghostTimeLeft)/30.0)/1000.0);
3196   //writeln("hgt=", hgt, "; rhgt=", rhgt, "; gtl=", ghostTimeLeft/30);
3197   if (rhgt > 0) {
3198     auto oclr = Video.color;
3199     Video.color = 0xcf_ff_7f_00;
3200     Video.fillRect(Video.screenWidth-20, 32, 16, hgt-rhgt);
3201     Video.color = 0x7f_ff_7f_00;
3202     Video.fillRect(Video.screenWidth-20, 32+(hgt-rhgt), 16, rhgt);
3203     Video.color = oclr;
3204   }
3208 void renderStarsHUD (float currFrameDelta) {
3209   bool scumSmallHud = global.config.scumSmallHud;
3211   //auto life = max(0, global.plife);
3212   int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3213   int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3214   //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3216   int hhup;
3218   if (scumSmallHud) {
3219     sprStore.loadFont('sFontSmall');
3220     hhup = 6;
3221   } else {
3222     sprStore.loadFont('sFont');
3223     hhup = 2;
3224   }
3226   Video.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3227   //draw_sprite(sHeart, -1, view_xview[0]+8, view_yview[0]+8);
3228   //draw_text(view_xview[0]+24, view_yview[0]+8, life);
3229   if (scumSmallHud) {
3230     if (global.plife == 1) {
3231       drawSpriteAt('sHeartSmallBlink', global.heartBlink, 10, hhup-4);
3232       global.heartBlink += 0.1;
3233       if (global.heartBlink > 3) global.heartBlink = 0;
3234     } else {
3235       drawSpriteAt('sHeartSmall', -1, 10, hhup-4);
3236       global.heartBlink = 0;
3237     }
3238   } else {
3239     if (global.plife == 1) {
3240       drawSpriteAt('sHeartBlink', global.heartBlink, 8, hhup);
3241       global.heartBlink += 0.1;
3242       if (global.heartBlink > 3) global.heartBlink = 0;
3243     } else {
3244       drawSpriteAt('sHeart', -1, 8, hhup);
3245       global.heartBlink = 0;
3246     }
3247   }
3248   int life = clamp(global.plife, 0, 99);
3249   drawTextAt(16+8, hhup, va("%d", life));
3251   //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3252   drawSpriteAt('sShopkeeperIcon', -1, 64, hhup, scumSmallHud);
3253   drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", starsKills));
3255   if (starsRoomTimer1 > 0) {
3256     sprStore.loadFont('sFontSmall');
3257     Video.color = 0xff_ff_00;
3258     int scale = global.scale;
3259     sprStore.renderMultilineTextCentered(Video.screenWidth/2, 216*scale, va("SHOTGUN CHALLENGE BEGINS IN ~%d~", (starsRoomTimer1/30)+1), scale, 0xff_00_00);
3260   }
3264 void renderSunHUD (float currFrameDelta) {
3265   bool scumSmallHud = global.config.scumSmallHud;
3267   //auto life = max(0, global.plife);
3268   int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3269   int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3270   //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3272   int hhup;
3274   if (scumSmallHud) {
3275     sprStore.loadFont('sFontSmall');
3276     hhup = 6;
3277   } else {
3278     sprStore.loadFont('sFont');
3279     hhup = 2;
3280   }
3282   Video.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3283   //draw_sprite(sHeart, -1, view_xview[0]+8, view_yview[0]+8);
3284   //draw_text(view_xview[0]+24, view_yview[0]+8, life);
3285   if (scumSmallHud) {
3286     if (global.plife == 1) {
3287       drawSpriteAt('sHeartSmallBlink', global.heartBlink, 10, hhup-4);
3288       global.heartBlink += 0.1;
3289       if (global.heartBlink > 3) global.heartBlink = 0;
3290     } else {
3291       drawSpriteAt('sHeartSmall', -1, 10, hhup-4);
3292       global.heartBlink = 0;
3293     }
3294   } else {
3295     if (global.plife == 1) {
3296       drawSpriteAt('sHeartBlink', global.heartBlink, 8, hhup);
3297       global.heartBlink += 0.1;
3298       if (global.heartBlink > 3) global.heartBlink = 0;
3299     } else {
3300       drawSpriteAt('sHeart', -1, 8, hhup);
3301       global.heartBlink = 0;
3302     }
3303   }
3304   int life = clamp(global.plife, 0, 99);
3305   drawTextAt(16+8, hhup, va("%d", life));
3307   //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3308   drawSpriteAt('sDamselIcon', -1, 64, hhup, scumSmallHud);
3309   drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", sunScore));
3311   if (sunRoomTimer1 > 0) {
3312     sprStore.loadFont('sFontSmall');
3313     Video.color = 0xff_ff_00;
3314     int scale = global.scale;
3315     sprStore.renderMultilineTextCentered(Video.screenWidth/2, 216*scale, va("DAMSEL CHALLENGE BEGINS IN ~%d~", (sunRoomTimer1/30)+1), scale, 0xff_00_00);
3316   }
3320 void renderMoonHUD (float currFrameDelta) {
3321   bool scumSmallHud = global.config.scumSmallHud;
3323   //auto life = max(0, global.plife);
3324   int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3325   int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3326   //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3328   int hhup;
3330   if (scumSmallHud) {
3331     sprStore.loadFont('sFontSmall');
3332     hhup = 6;
3333   } else {
3334     sprStore.loadFont('sFont');
3335     hhup = 2;
3336   }
3338   Video.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3340   //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3341   drawSpriteAt('sHoopsIcon', -1, 8, hhup, scumSmallHud);
3342   drawTextAt(8+16-(scumSmallHud ? 6 : 0), hhup, va("%d", moonScore));
3343   drawSpriteAt('sTimerIcon', -1, 64, hhup, scumSmallHud);
3344   drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", max(0, moonTimer)));
3346   if (moonRoomTimer1 > 0) {
3347     sprStore.loadFont('sFontSmall');
3348     Video.color = 0xff_ff_00;
3349     int scale = global.scale;
3350     sprStore.renderMultilineTextCentered(Video.screenWidth/2, 216*scale, va("ARCHERY CHALLENGE BEGINS IN ~%d~", (moonRoomTimer1/30)+1), scale, 0xff_00_00);
3351   }
3355 void renderHUD (float currFrameDelta) {
3356   if (levelKind == LevelKind.Stars) { renderStarsHUD(currFrameDelta); return; }
3357   if (levelKind == LevelKind.Sun) { renderSunHUD(currFrameDelta); return; }
3358   if (levelKind == LevelKind.Moon) { renderMoonHUD(currFrameDelta); return; }
3360   if (!isHUDEnabled()) return;
3362   if (levelKind == LevelKind.Transition) { renderTransitionInfo(currFrameDelta); return; }
3364   int lifeX = 4; // 8
3365   int bombX = 56;
3366   int ropeX = 104;
3367   int ammoX = 152;
3368   int moneyX = 200;
3369   int hhup;
3370   bool scumSmallHud = global.config.scumSmallHud;
3371   if (!global.config.optSGAmmo) moneyX = ammoX;
3373   if (scumSmallHud) {
3374     sprStore.loadFont('sFontSmall');
3375     hhup = 6;
3376   } else {
3377     sprStore.loadFont('sFont');
3378     hhup = 0;
3379   }
3380   //int alpha = 0x6f_00_00_00;
3381   int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3382   int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3384   //Video.color = 0xff_ff_ff;
3385   Video.color = 0xff_ff_ff|talpha;
3387   // hearts
3388   if (scumSmallHud) {
3389     if (global.plife == 1) {
3390       drawSpriteAt('sHeartSmallBlink', global.heartBlink, lifeX+2, 4-hhup);
3391       global.heartBlink += 0.1;
3392       if (global.heartBlink > 3) global.heartBlink = 0;
3393     } else {
3394       drawSpriteAt('sHeartSmall', -1, lifeX+2, 4-hhup);
3395       global.heartBlink = 0;
3396     }
3397   } else {
3398     if (global.plife == 1) {
3399       drawSpriteAt('sHeartBlink', global.heartBlink, lifeX, 8-hhup);
3400       global.heartBlink += 0.1;
3401       if (global.heartBlink > 3) global.heartBlink = 0;
3402     } else {
3403       drawSpriteAt('sHeart', -1, lifeX, 8-hhup);
3404       global.heartBlink = 0;
3405     }
3406   }
3408   int life = clamp(global.plife, 0, 99);
3409   //if (!scumHud && life > 99) life = 99;
3410   drawTextAt(lifeX+16, 8-hhup, va("%d", life));
3412   // bombs
3413   if (global.hasStickyBombs && global.stickyBombsActive) {
3414     if (scumSmallHud) drawSpriteAt('sStickyBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sStickyBombIcon', -1, bombX, 8-hhup);
3415   } else {
3416     if (scumSmallHud) drawSpriteAt('sBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sBombIcon', -1, bombX, 8-hhup);
3417   }
3418   int n = global.bombs;
3419   if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3420   drawTextAt(bombX+16, 8-hhup, va("%d", n));
3422   // ropes
3423   if (scumSmallHud) drawSpriteAt('sRopeIconSmall', -1, ropeX+2, 4-hhup); else drawSpriteAt('sRopeIcon', -1, ropeX, 8-hhup);
3424   n = global.rope;
3425   if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3426   drawTextAt(ropeX+16, 8-hhup, va("%d", n));
3428   // shotgun shells
3429   if (global.config.optSGAmmo && (!player || player.holdItem !isa ItemWeaponBow)) {
3430     if (scumSmallHud) drawSpriteAt('sShellsIcon', -1, ammoX+6, 8-hhup); else drawSpriteAt('sShellsIcon', -1, ammoX+7, 12-hhup);
3431     n = global.sgammo;
3432     if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3433     drawTextAt(ammoX+16, 8-hhup, va("%d", n));
3434   } else if (player && player.holdItem isa ItemWeaponBow) {
3435     if (scumSmallHud) drawSpriteAt('sArrowRight', -1, ammoX+6, 8-hhup); else drawSpriteAt('sArrowRight', -1, ammoX+7, 12-hhup);
3436     n = global.arrows;
3437     if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3438     drawTextAt(ammoX+16, 8-hhup, va("%d", n));
3439   }
3441   // money
3442   if (scumSmallHud) drawSpriteAt('sDollarSignSmall', -1, moneyX+2, 3-hhup); else drawSpriteAt('sDollarSign', -1, moneyX, 8-hhup);
3443   drawTextAt(moneyX+16, 8-hhup, va("%d", stats.money-xmoney));
3445   // items
3446   Video.color = 0xff_ff_ff|ialpha;
3448   int ity = (scumSmallHud ? 18-hhup : 24-hhup);
3450   n = 8; //28;
3451   if (global.hasUdjatEye) {
3452     if (global.udjatBlink) drawSpriteAt('sUdjatEyeIcon2', -1, n, ity); else drawSpriteAt('sUdjatEyeIcon', -1, n, ity);
3453     n += 20;
3454   }
3455   if (global.hasAnkh) { drawSpriteAt('sAnkhIcon', -1, n, ity); n += 20; }
3456   if (global.hasCrown) { drawSpriteAt('sCrownIcon', -1, n, ity); n += 20; }
3457   if (global.hasKapala) {
3458          if (global.bloodLevel == 0) drawSpriteAt('sKapalaIcon', 0, n, ity);
3459     else if (global.bloodLevel <= 2) drawSpriteAt('sKapalaIcon', 1, n, ity);
3460     else if (global.bloodLevel <= 4) drawSpriteAt('sKapalaIcon', 2, n, ity);
3461     else if (global.bloodLevel <= 6) drawSpriteAt('sKapalaIcon', 3, n, ity);
3462     else if (global.bloodLevel <= 8) drawSpriteAt('sKapalaIcon', 4, n, ity);
3463     n += 20;
3464   }
3465   if (global.hasSpectacles) { drawSpriteAt('sSpectaclesIcon', -1, n, ity); n += 20; }
3466   if (global.hasGloves) { drawSpriteAt('sGlovesIcon', -1, n, ity); n += 20; }
3467   if (global.hasMitt) { drawSpriteAt('sMittIcon', -1, n, ity); n += 20; }
3468   if (global.hasSpringShoes) { drawSpriteAt('sSpringShoesIcon', -1, n, ity); n += 20; }
3469   if (global.hasJordans) { drawSpriteAt('sJordansIcon', -1, n, ity); n += 20; }
3470   if (global.hasSpikeShoes) { drawSpriteAt('sSpikeShoesIcon', -1, n, ity); n += 20; }
3471   if (global.hasCape) { drawSpriteAt('sCapeIcon', -1, n, ity); n += 20; }
3472   if (global.hasJetpack) { drawSpriteAt('sJetpackIcon', -1, n, ity); n += 20; }
3473   if (global.hasStickyBombs) { drawSpriteAt('sPasteIcon', -1, n, ity); n += 20; }
3474   if (global.hasCompass) { drawSpriteAt('sCompassIcon', -1, n, ity); n += 20; }
3475   if (global.hasParachute) { drawSpriteAt('sParachuteIcon', -1, n, ity); n += 20; }
3477   if (player.holdItem && player.holdItem.objName == 'Bow' && global.arrows > 0) {
3478     int m = 1;
3479     float malpha = 1;
3480     while (m <= global.arrows && m <= 20 && malpha > 0) {
3481       Video.color = trunc(malpha*255)<<24|0xff_ff_ff;
3482       drawSpriteAt('sArrowIcon', -1, n, ity);
3483       n += 4;
3484       if (m > 10) malpha -= 0.1; //and global.arrows > 20 malpha -= 0.1
3485       m += 1;
3486     }
3487   }
3489   if (xmoney > 0) {
3490     sprStore.loadFont('sFontSmall');
3491     Video.color = 0xff_ff_00|talpha;
3492     if (scumSmallHud) drawTextAt(moneyX+8, 8+8-hhup, va("+%d", xmoney));
3493     else drawTextAt(moneyX, 8+16-hhup, va("+%d", xmoney));
3494   }
3496   Video.color = 0xff_ff_ff;
3497   if (global.config.ghostShowTime) renderGhostTimer(currFrameDelta);
3501 // ////////////////////////////////////////////////////////////////////////// //
3502 // x0 and y0 are non-scaled (and will be scaled)
3503 final void drawTextAtS3 (int x0, int y0, string text, optional int hiColor1, optional int hiColor2) {
3504   if (!text) return;
3505   x0 *= 3;
3506   y0 *= 3;
3507   sprStore.renderTextWithHighlight(x0, y0, text, 3, hiColor1!optional, hiColor2!optional);
3511 final void drawTextAtS3Centered (int y0, string text, optional int hiColor1, optional int hiColor2) {
3512   if (!text) return;
3513   int x0 = (Video.screenWidth-sprStore.getTextWidth(text, 3, specified_hiColor1, specified_hiColor2))/2;
3514   sprStore.renderTextWithHighlight(x0, y0*3, text, 3, hiColor1!optional, hiColor2!optional);
3518 void renderHelpOverlay () {
3519   Video.color = 0;
3520   Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
3522   int tx = 16;
3523   int txoff = 0; // text x pos offset (for multi-color lines)
3524   int ty = 8;
3525   if (gameHelpScreen) {
3526     sprStore.loadFont('sFontSmall');
3527     Video.color = 0xff_ff_ff;
3528     drawTextAtS3Centered(ty, va("HELP (PAGE ~%d~ OF ~%d~)", gameHelpScreen, MaxGameHelpScreen), 0xff_ff_00);
3529     ty += 24;
3530   }
3532   if (gameHelpScreen == 1) {
3533     sprStore.loadFont('sFontSmall');
3534     Video.color = 0xff_ff_00; drawTextAtS3(tx, ty, "INVENTORY BASICS"); ty += 16;
3535     Video.color = 0xff_ff_ff;
3536     drawTextAtS3(tx, ty, global.expandString("Press $SWITCH to cycle through items."), 0x00_ff_00);
3537     ty += 8;
3538     ty += 56;
3539     Video.color = 0xff_ff_ff;
3540     drawSpriteAtS3('sHelpSprite1', -1, 64, 96);
3541   } else if (gameHelpScreen == 2) {
3542     sprStore.loadFont('sFontSmall');
3543     Video.color = 0xff_ff_00;
3544     drawTextAtS3(tx, ty, "SELLING TO SHOPKEEPERS"); ty += 16;
3545     Video.color = 0xff_ff_ff;
3546     drawTextAtS3(tx, ty, global.expandString("Press $PAY to offer your currently"), 0x00_ff_00); ty += 8;
3547     drawTextAtS3(tx, ty, "held item to the shopkeeper."); ty += 16;
3548     drawTextAtS3(tx, ty, "If the shopkeeper is interested, "); ty += 8;
3549     //drawTextAtS3(tx, ty, global.expandString("press $PAY again to complete the sale."), 0x00_ff_00); ty += 72;
3550     drawTextAtS3(tx, ty, global.expandString("press $PAY again to complete"), 0x00_ff_00);
3551     drawTextAtS3(tx, ty+8, "the sale.");
3552     ty += 72;
3553     drawSpriteAtS3('sHelpSell', -1, 112, 100);
3554     drawTextAtS3(tx, ty, "Purchasing goods from the shopkeeper"); ty += 8;
3555     drawTextAtS3(tx, ty, "will increase the funds he has"); ty += 8;
3556     drawTextAtS3(tx, ty, "available to buy your unwanted stuff."); ty += 8;
3557   } else {
3558     // map
3559     sprStore.loadFont('sFont');
3560     Video.color = 0xff_ff_ff;
3561     drawTextAtS3(136, 8, "MAP");
3563     Video.color = 0xff_ff_00;
3564     drawTextAtS3Centered(24, lg.mapTitle);
3566     if (lg.mapSprite) {
3567       auto spf = sprStore[lg.mapSprite].frames[0];
3568       int mapX = 160-spf.width/2;
3569       int mapY = 120-spf.height/2;
3570       //mapTitleX = 160-string_length(global.mapTitle)*8/2;
3572       Video.color = 0xff_ff_ff;
3573       drawSpriteAtS3(lg.mapSprite, -1, mapX, mapY);
3575       if (lg.mapSprite != 'sMapDefault') {
3576         int mx = -1, my = -1;
3578         // set position of player icon
3579         switch (global.currLevel) {
3580           case 1: mx = 81; my = 22; break;
3581           case 2: mx = 113; my = 63; break;
3582           case 3: mx = 197; my = 86; break;
3583           case 4: mx = 133; my = 109; break;
3584           case 5: mx = 181; my = 22; break;
3585           case 6: mx = 126; my = 64; break;
3586           case 7: mx = 158; my = 112; break;
3587           case 8: mx = 66; my = 80; break;
3588           case 9: mx = 30; my = 26; break;
3589           case 10: mx = 88; my = 54; break;
3590           case 11: mx = 148; my = 81; break;
3591           case 12: mx = 210; my = 205; break;
3592           case 13: mx = 66; my = 17; break;
3593           case 14: mx = 146; my = 17; break;
3594           case 15: mx = 82; my = 77; break;
3595           case 16: mx = 178; my = 81; break;
3596         }
3598         if (mx >= 0) {
3599           int plrx = mx+player.ix/16;
3600           int plry = my+player.iy/16;
3601           name plrspr = 'sMapSpelunker';
3602                if (global.isDamsel) plrspr = 'sMapDamsel';
3603           else if (global.isTunnelMan) plrspr = 'sMapTunnel';
3604           auto ss = sprStore[plrspr];
3605           drawSpriteAtS3(plrspr, (pausedTime/2)%ss.frames.length, mapX+plrx, mapY+plry);
3606           // exit door icon
3607           if (global.hasCompass && allExits.length) {
3608             drawSpriteAtS3('sMapRedDot', -1, mapX+mx+allExits[0].ix/16, mapY+my+allExits[0].iy/16);
3609           }
3610         }
3611       }
3612     }
3613   }
3615   sprStore.loadFont('sFontSmall');
3616   Video.color = 0xff_ff_00;
3617   drawTextAtS3Centered(232, "PRESS ~SPACE~/~LEFT~/~RIGHT~ TO CHANGE PAGE", 0x00_ff_00);
3619   Video.color = 0xff_ff_ff;
3623 void renderPauseOverlay () {
3624   //drawTextAt(256, 432, "PAUSED", scale);
3626   if (gameShowHelp) { renderHelpOverlay(); return; }
3628   Video.color = 0xff_ff_00;
3629   //int hiColor = 0x00_ff_00;
3631   int n = 120;
3632   if (isTutorialRoom()) {
3633     sprStore.loadFont('sFont');
3634     drawTextAtS3(40, n-24, "TUTORIAL CAVE");
3635   } else if (isNormalLevel()) {
3636     sprStore.loadFont('sFont');
3638     drawTextAtS3Centered(n-32, va("LEVEL ~%d~", global.currLevel), 0x00_ff_00);
3640     sprStore.loadFont('sFontSmall');
3642     int depth = round((174.8*(global.currLevel-1)+(player.iy+8)*0.34)*(global.config.scumMetric ? 0.3048 : 1.0)*10);
3643     string depthStr = va("DEPTH: ~%d.%d~ %s", depth/10, depth%10, (global.config.scumMetric ? "METRES" : "FEET"));
3644     drawTextAtS3Centered(n-16, depthStr, 0x00_ff_00);
3646     n += 16;
3647     drawTextAtS3Centered(n, va("MONEY: ~%d~", stats.money), 0x00_ff_00);
3648     drawTextAtS3Centered(n+16, va("KILLS: ~%d~", stats.countKills), 0x00_ff_00);
3649     drawTextAtS3Centered(n+32, va("SAVES: ~%d~", stats.damselsSaved), 0x00_ff_00);
3650     drawTextAtS3Centered(n+48, va("TIME: ~%s~", time2str(time/30)), 0x00_ff_00);
3651     drawTextAtS3Centered(n+64, va("LEVEL TIME: ~%s~", time2str((time-levelStartTime)/30)), 0x00_ff_00);
3652   }
3654   sprStore.loadFont('sFontSmall');
3655   Video.color = 0xff_ff_ff;
3656   drawTextAtS3Centered(240-2-8, "~ESC~-RETURN  ~F10~-QUIT  ~CTRL+DEL~-SUICIDE", 0xff_7f_00);
3657   drawTextAtS3Centered(2, "~O~PTIONS  REDEFINE ~K~EYS  ~S~TATISTICS", 0xff_7f_00);
3661 // ////////////////////////////////////////////////////////////////////////// //
3662 transient int drawLoot;
3663 transient int drawPosX, drawPosY;
3665 void resetTransitionOverlay () {
3666   drawLoot = 0;
3667   drawPosX = 100;
3668   drawPosY = 83;
3672 // current game, uncollapsed
3673 struct LevelStatInfo {
3674   name aname;
3675   // for transition screen
3676   bool render;
3677   int x, y;
3682 void thinkFrameTransition () {
3683   if (drawLoot == 0) {
3684     if (drawPosX > 272) {
3685       drawPosX = 100;
3686       drawPosY += 2;
3687       if (drawPosY > 83+4) drawPosY = 83;
3688     }
3689   } else if (drawPosX > 232) {
3690     drawPosX = 96;
3691     drawPosY += 2;
3692     if (drawPosY > 91+4) drawPosY = 91;
3693   }
3697 void renderTransitionOverlay () {
3698   sprStore.loadFont('sFontSmall');
3699   Video.color = 0xff_ff_00;
3700   //else if (global.currLevel-1 &lt; 1) draw_text(32, 48, "TUTORIAL CAVE COMPLETED!");
3701   //else draw_text(32, 48, "LEVEL "+string(global.currLevel-1)+" COMPLETED!");
3702   drawTextAt(32, 48, va("LEVEL ~%d~ COMPLETED!", global.currLevel), hiColor1:0x00_ff_ff);
3703   Video.color = 0xff_ff_ff;
3704   drawTextAt(32, 64, va("TIME  = ~%s~", time2str((levelEndTime-levelStartTime)/30)), hiColor1:0xff_ff_00);
3706   if (/*stats.collected.length == 0*/stats.money <= levelMoneyStart) {
3707     drawTextAt(32, 80, "LOOT  = ~NONE~", hiColor1:0xff_00_00);
3708   } else {
3709     drawTextAt(32, 80, va("LOOT  = ~%d~", stats.money-levelMoneyStart), hiColor1:0xff_ff_00);
3710   }
3712   if (stats.kills.length == 0) {
3713     drawTextAt(32, 96, "KILLS = ~NONE~", hiColor1:0x00_ff_00);
3714   } else {
3715     drawTextAt(32, 96, va("KILLS = ~%d~", stats.kills.length), hiColor1:0xff_ff_00);
3716   }
3718   drawTextAt(32, 112, va("MONEY = ~%d~", stats.money), hiColor1:0xff_ff_00);
3722 // ////////////////////////////////////////////////////////////////////////// //
3723 private transient array!MapEntity renderVisibleCids;
3724 private transient array!MapEntity renderVisibleLights;
3725 private transient array!MapTile renderFrontTiles; // normal, with fg
3727 final bool renderSortByDepth (MapEntity oa, MapEntity ob) {
3728   auto da = oa.depth, db = ob.depth;
3729   if (da == db) return (oa.objId < ob.objId);
3730   return (da < db);
3734 const int RenderEdgePixNormal = 64;
3735 const int RenderEdgePixLight = 256;
3737 #ifndef EXPERIMENTAL_RENDER_CACHE
3738 enum skipListCreation = false;
3739 #endif
3741 void renderWithOfs (int xofs, int yofs, float currFrameDelta) {
3742   int scale = global.scale;
3744   // don't touch framebuffer alpha
3745   Video.colorMask = Video::CMask.Colors;
3746   Video.color = 0xff_ff_ff;
3748   bool isDarkLevel = global.darkLevel;
3750   if (isDarkLevel) {
3751     switch (global.config.scumPlayerLit) {
3752       case 0: player.lightRadius = 0; break; // never
3753       case 1: // only in "scumDarkness"
3754         player.lightRadius = (global.config.scumDarkness >= 2 ? 96 : 32);
3755         break;
3756       case 2:
3757         player.lightRadius = 96;
3758         break;
3759     }
3760   }
3762   // render cave background
3763   if (levBGImg) {
3764     int tsz = 16*scale;
3765     int bgw = levBGImg.tex.width*scale;
3766     int bgh = levBGImg.tex.height*scale;
3767     int bgTW = (tilesWidth*tsz+bgw-1)/bgw;
3768     int bgTH = (tilesHeight*tsz+bgh-1)/bgh;
3769     int bgX0 = max(0, xofs/bgw);
3770     int bgY0 = max(0, yofs/bgh);
3771     int bgX1 = min(bgTW, (xofs+viewWidth+bgw-1)/bgw);
3772     int bgY1 = min(bgTH, (yofs+viewHeight+bgh-1)/bgh);
3773     foreach (int ty; bgY0..bgY1) {
3774       foreach (int tx; bgX0..bgX1) {
3775         int x0 = tx*bgw-xofs;
3776         int y0 = ty*bgh-yofs;
3777         levBGImg.tex.blitAt(x0, y0, scale);
3778       }
3779     }
3780   }
3782   int RenderEdgePix = (global.darkLevel ? RenderEdgePixLight : RenderEdgePixNormal);
3784   // render background tiles
3785   for (MapBackTile bt = backtiles; bt; bt = bt.next) {
3786     bt.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3787   }
3789   // collect visible special tiles
3790 #ifdef EXPERIMENTAL_RENDER_CACHE
3791   bool skipListCreation = (lastRenderTime == time && renderVisibleCids.length); //FIXME
3792 #endif
3794   if (!skipListCreation) {
3795     renderVisibleCids.clear();
3796     renderVisibleLights.clear();
3797     renderFrontTiles.clear();
3799     int endVX = xofs+viewWidth;
3800     int endVY = yofs+viewHeight;
3802     // add player
3803     //int cnt = 0;
3804     if (player && player.visible) renderVisibleCids[$] = player; // include player in render list
3806     //FIXME: drop lit objects which cannot affect visible area
3807     if (scale > 1) {
3808       // collect visible objects
3809       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)) {
3810         if (!o.visible) continue;
3811         auto tile = MapTile(o);
3812         if (tile) {
3813           if (isDarkLevel && (o.lightRadius > 4 || tile.litWholeTile)) renderVisibleLights[$] = o;
3814           if (tile.invisible) continue;
3815           if (tile.bgfront) renderFrontTiles[$] = tile;
3816           if (tile.bgback) tile.drawWithOfsBack(xofs, yofs, scale, currFrameDelta);
3817         } else {
3818           if (isDarkLevel && o.lightRadius > 4) renderVisibleLights[$] = o;
3819         }
3820         // check if the object is really visible -- this will speed up later sorting
3821         int fx0, fy0, fx1, fy1;
3822         auto spf = o.getSpriteFrame(default, out fx0, out fy0, out fx1, out fy1);
3823         if (!spf) continue; // no sprite -- nothing to draw (no, really)
3824         int ix = o.ix, iy = o.iy;
3825         int x0 = (ix+fx0)*scale, y0 = (iy+fy0)*scale;
3826         int x1 = (ix+fx1)*scale, y1 = (iy+fy1)*scale;
3827         if (x1 <= xofs || y1 <= yofs || x0 >= endVX || y0 >= endVY) {
3828           //++cnt;
3829           continue;
3830         }
3831         renderVisibleCids[$] = o;
3832       }
3833     } else {
3834       foreach (MapEntity o; objGrid.allObjects()) {
3835         if (!o.visible) continue;
3836         auto tile = MapTile(o);
3837         if (tile) {
3838           if (isDarkLevel && (o.lightRadius > 4 || tile.litWholeTile)) renderVisibleLights[$] = o;
3839           if (tile.invisible) continue;
3840           if (tile.bgfront) renderFrontTiles[$] = tile;
3841           if (tile.bgback) tile.drawWithOfsBack(xofs, yofs, scale, currFrameDelta);
3842         } else {
3843           if (isDarkLevel && o.lightRadius > 4) renderVisibleLights[$] = o;
3844         }
3845         renderVisibleCids[$] = o;
3846       }
3847     }
3848     //writeln("::: ", cnt, " invisible objects dropped");
3850     renderVisibleCids.sort(&renderSortByDepth);
3851     lastRenderTime = time;
3852   }
3854   auto depth4Start = 0;
3855   foreach (auto xidx, MapEntity o; renderVisibleCids) {
3856     if (o.depth >= 4) {
3857       depth4Start = xidx;
3858       break;
3859     }
3860   }
3862   bool playerPowerupRendered = false;
3864   // render objects (part one: depth > 3)
3865   foreach (auto idx; depth4Start..renderVisibleCids.length; reverse) {
3866     MapEntity o = renderVisibleCids[idx];
3867     // 1000 is an ordinary tile
3868     if (!playerPowerupRendered && o.depth <= 1200) {
3869       playerPowerupRendered = true;
3870       // so ducking player will have it's cape correctly rendered
3871       if (player.visible) player.drawPrePrePowerupWithOfs(xofs, yofs, scale, currFrameDelta);
3872     }
3873     //if (idx && renderSortByDepth(o, renderVisibleCids[idx-1])) writeln("VIOLATION!");
3874     o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3875   }
3877   // render object (part two: front tile parts, depth 3.5)
3878   foreach (MapTile tile; renderFrontTiles) {
3879     tile.drawWithOfsFront(xofs, yofs, scale, currFrameDelta);
3880   }
3882   // render objects (part three: depth <= 3)
3883   foreach (auto idx; 0..depth4Start; reverse) {
3884     MapEntity o = renderVisibleCids[idx];
3885     o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3886     //done above;if (isDarkLevel && (o.lightRadius > 4 || (o isa MapTile && MapTile(o).litWholeTile))) renderVisibleLights[$] = o;
3887   }
3889   // render player post-effects, so cape or jetpack will be correctly rendered on level exit, for example
3890   player.lastDrawWithOfs(xofs, yofs, scale, currFrameDelta);
3892   // lighting
3893   if (isDarkLevel) {
3894     auto ltex = bgtileStore.lightTexture('ltx512', 512);
3896     // set screen alpha to min
3897     Video.colorMask = Video::CMask.Alpha;
3898     Video.blendMode = Video::BlendMode.None;
3899     Video.color = 0xff_ff_ff_ff;
3900     Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
3901     //Video.colorMask = Video::CMask.All;
3903     // blend lights
3904     // also, stencil 'em, so we can filter dark areas
3905     Video.textureFiltering = true;
3906     Video.stencil = true;
3907     Video.stencilFunc(Video::StencilFunc.Always, 1);
3908     Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Replace);
3909     Video.alphaTestFunc = Video::AlphaFunc.Greater;
3910     Video.alphaTestVal = 0.03+0.011*global.config.darknessDarkness;
3911     Video.color = 0xff_ff_ff;
3912     Video.blendFunc = Video::BlendFunc.Max;
3913     Video.blendMode = Video::BlendMode.Blend; // anything except `Normal`
3914     Video.colorMask = Video::CMask.Alpha;
3916     foreach (MapEntity e; renderVisibleLights) {
3917       int xi, yi;
3918       e.getInterpCoords(currFrameDelta, scale, out xi, out yi);
3919       auto tile = MapTile(e);
3920       if (tile && tile.litWholeTile) {
3921         //Video.color = 0xff_ff_ff;
3922         Video.fillRect(xi-xofs, yi-yofs, e.width*scale, e.height*scale);
3923       }
3924       int lrad = e.lightRadius;
3925       if (lrad < 4) continue; // just in case
3926       lrad += 8;
3927       float lightscale = float(lrad*scale)/float(ltex.tex.width);
3928 #ifdef OLD_LIGHT_OFFSETS
3929       int fx0, fy0, fx1, fy1;
3930       bool doMirror;
3931       auto spf = e.getSpriteFrame(out doMirror, out fx0, out fy0, out fx1, out fy1);
3932       if (spf) {
3933         xi += (fx1-fx0)*scale/2;
3934         yi += (fy1-fy0)*scale/2;
3935       }
3936 #else
3937       int lxofs, lyofs;
3938       e.getLightOffset(out lxofs, out lyofs);
3939       xi += lxofs*scale;
3940       yi += lyofs*scale;
3942 #endif
3943       lrad = lrad*scale/2;
3944       xi -= xofs+lrad;
3945       yi -= yofs+lrad;
3946       ltex.tex.blitAt(xi, yi, lightscale);
3947     }
3948     Video.textureFiltering = false;
3950     // modify only lit parts
3951     Video.stencilFunc(Video::StencilFunc.Equal, 1);
3952     Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Keep);
3953     // multiply framebuffer colors by framebuffer alpha
3954     Video.color = 0xff_ff_ff; // it doesn't matter
3955     Video.blendFunc = Video::BlendFunc.Add;
3956     Video.blendMode = Video::BlendMode.DstMulDstAlpha;
3957     Video.colorMask = Video::CMask.Colors;
3958     Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
3960     // filter unlit parts
3961     Video.stencilFunc(Video::StencilFunc.NotEqual, 1);
3962     Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Keep);
3963     Video.blendFunc = Video::BlendFunc.Add;
3964     Video.blendMode = Video::BlendMode.Filter;
3965     Video.colorMask = Video::CMask.Colors;
3966     Video.color = 0x00_00_18+0x00_00_10*global.config.darknessDarkness;
3967     //Video.color = 0x00_00_18;
3968     //Video.color = 0x00_00_38;
3969     Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
3971     // restore defaults
3972     Video.blendFunc = Video::BlendFunc.Add;
3973     Video.blendMode = Video::BlendMode.Normal;
3974     Video.colorMask = Video::CMask.All;
3975     Video.alphaTestFunc = Video::AlphaFunc.Always;
3976     Video.stencil = false;
3977   }
3979   // clear visible objects list (nope)
3980   //renderVisibleCids.clear();
3981   //renderVisibleLights.clear();
3984   if (global.config.drawHUD) renderHUD(currFrameDelta);
3985   renderCompass(currFrameDelta);
3987   float osdTimeLeft, osdTimeStart;
3988   string msg = osdGetMessage(out osdTimeLeft, out osdTimeStart);
3989   if (msg) {
3990     auto ct = GetTickCount();
3991     int msgScale = 3;
3992     sprStore.loadFont('sFontSmall');
3993     auto msgHeight = sprStore.getMultilineTextHeight(msg, msgScale);
3994     int x = Video.screenWidth/2;
3995     int y = Video.screenHeight-64-msgHeight;
3996     auto oldColor = Video.color;
3997     Video.color = 0xff_ff_00;
3998     if (osdTimeLeft < 0.5) {
3999       int alpha = 255-clamp(int(510*osdTimeLeft), 0, 255);
4000       Video.color = Video.color|(alpha<<24);
4001     } else if (ct-osdTimeStart < 0.5) {
4002       osdTimeStart = ct-osdTimeStart;
4003       int alpha = 255-clamp(int(510*osdTimeStart), 0, 255);
4004       Video.color = Video.color|(alpha<<24);
4005     }
4006     sprStore.renderMultilineTextCentered(x, y, msg, msgScale, 0x00_ff_00, 0xff_ff_ff);
4007     Video.color = oldColor;
4008   }
4010   if (inWinCutscene) renderWinCutsceneOverlay();
4011   if (isTransitionRoom()) renderTransitionOverlay();
4012   Video.color = 0xff_ff_ff;
4016 // ////////////////////////////////////////////////////////////////////////// //
4017 final class!MapObject findGameObjectClassByName (name aname) {
4018   if (!aname) return none; // just in case
4019   auto co = FindClassByGameObjName(aname);
4020   if (!co) {
4021     writeln("***UNKNOWN GAME OBJECT: '", aname, "'");
4022     return none;
4023   }
4024   co = GetClassReplacement(co);
4025   if (!co) FatalError("findGameObjectClassByName: WTF?!");
4026   if (!class!MapObject(co)) FatalError("findGameTileClassByName: object '%n' is not a map object, it is a `%n`", aname, GetClassName(co));
4027   return class!MapObject(co);
4031 final class!MapTile findGameTileClassByName (name aname) {
4032   if (!aname) return none; // just in case
4033   auto co = FindClassByGameObjName(aname);
4034   if (!co) return MapTile; // unknown names will be routed directly to tile object
4035   co = GetClassReplacement(co);
4036   if (!co) FatalError("findGameTileClassByName: WTF?!");
4037   if (!class!MapTile(co)) FatalError("findGameTileClassByName: object '%n' is not a tile, it is a `%n`", aname, GetClassName(co));
4038   return class!MapTile(co);
4042 final MapObject findAnyObjectOfType (name aname) {
4043   if (!aname) return none;
4044   auto cls = FindClassByGameObjName(aname);
4045   if (!cls) return none;
4046   foreach (MapObject obj; objGrid.allObjects(MapObject)) {
4047     if (obj.spectral) continue;
4048     if (obj isa cls) return obj;
4049   }
4050   return none;
4054 // ////////////////////////////////////////////////////////////////////////// //
4055 final bool isRopePlacedAt (int x, int y) {
4056   int[8] covered;
4057   foreach (ref auto v; covered) v = false;
4058   foreach (MapTile t; objGrid.inRectPix(x, y-8, 1, 17, precise:false, castClass:MapTileRope)) {
4059     //if (!cbIsRopeTile(t)) continue;
4060     if (t.ix != x) continue;
4061     if (t.iy == y) return true;
4062     foreach (int ty; t.iy..t.iy+8) {
4063       int d = ty-y;
4064       if (d >= 0 && d < covered.length) covered[d] = true;
4065     }
4066   }
4067   // check if the whole rope height is completely covered with ropes
4068   foreach (auto v; covered) if (!v) return false;
4069   return true;
4073 final MapTile CreateMapTile (int xpos, int ypos, name aname) {
4074   if (!aname) FatalError("cannot create typeless tile");
4075   auto tclass = findGameTileClassByName(aname);
4076   if (!tclass) return none;
4077   MapTile tile = SpawnObject(tclass);
4078   tile.global = global;
4079   tile.level = self;
4080   tile.objName = aname;
4081   tile.objType = aname; // just in case
4082   tile.fltx = xpos;
4083   tile.flty = ypos;
4084   tile.objId = ++lastUsedObjectId;
4085   if (!tile.initialize()) FatalError("cannot create tile '%n'", aname);
4086   return tile;
4090 final bool PutSpawnedMapTile (int x, int y, MapTile tile, optional bool putToGrid) {
4091   if (!tile || !tile.isInstanceAlive) return false;
4093   if (!putToGrid) putToGrid = (tile.moveable || tile.toSpecialGrid || tile.width != 16 || tile.height != 16 || x%16 != 0 || y%16 != 0);
4095   //writeln("putting tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (putToGrid ? "special " : ""), "grid, pos=(", x/16.0, ",", y/16.0, ")");
4097   if (!putToGrid) {
4098     int mapx = x/16, mapy = y/16;
4099     if (mapx < 0 || mapx >= tilesWidth || mapy < 0 || mapy >= tilesHeight) return false;
4100   }
4102   // if we already have rope tile there, there is no reason to add another one
4103   if (tile isa MapTileRope) {
4104     if (isRopePlacedAt(x, y)) return false;
4105   }
4107   // activate special or animated tile
4108   tile.active = tile.active || putToGrid || tile.moveable || tile.toSpecialGrid || tile.lava /*|| tile.water*/; // will be done in MakeMapTile
4109   // animated tiles must be active
4110   if (!tile.active) {
4111     auto spr = tile.getSprite();
4112     if (spr && spr.frames.length > 1) {
4113       writeln("activated animated tile '", tile.objName, "'");
4114       tile.active = true;
4115     }
4116   }
4118   tile.fltx = x;
4119   tile.flty = y;
4120   if (putToGrid) {
4121     //if (tile isa TitleTileCopy) writeln("*** PUTTING COPYRIGHT TILE");
4122     tile.toSpecialGrid = true;
4123     if (!tile.dontReplaceOthers && x&16 == 0 && y%16 == 0) {
4124       auto t = getTileAtGridAny(x/16, y/16);
4125       if (t && !t.immuneToReplacement) {
4126         writeln("REPLACING tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
4127         writeln("      NEW tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (tile.toSpecialGrid ? "special " : ""), "grid, pos=(", tile.fltx/16.0, ",", tile.flty/16.0, ")");
4128         t.instanceRemove();
4129       }
4130     }
4131     objGrid.insert(tile);
4132   } else {
4133     //writeln("SIZE: ", tilesWidth, "x", tilesHeight);
4134     setTileAtGrid(x/16, y/16, tile);
4135     auto t = getTileAtGridAny(x/16, y/16);
4136     /*
4137     if (t != tile) {
4138       writeln("putting tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (putToGrid ? "special " : ""), "grid, pos=(", x/16.0, ",", y/16.0, ")");
4139       checkTilesInRect(x/16, y/16, 16, 16, delegate bool (MapTile tile) {
4140         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, ")");
4141         return false;
4142       });
4143       FatalError("FUUUUUU");
4144     }
4145     */
4146   }
4148   if (tile.enter) registerEnter(tile);
4149   if (tile.exit) registerExit(tile);
4151   return true;
4155 // won't call `onDestroy()`
4156 final void RemoveMapTileFromGrid (int tileX, int tileY, optional string reason) {
4157   if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
4158     auto t = getTileAtGridAny(tileX, tileY);
4159     if (t) {
4160       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, ")");
4161       t.instanceRemove();
4162       checkWater = true;
4163     }
4164   }
4168 final MapTile MakeMapTile (int mapx, int mapy, name aname, optional bool putToGrid) {
4169   //if (mapy >= tilesHeight) { writeln("OOM: ", aname, "; ty=", mapy, "; hgt=", tilesHeight); }
4170   if (mapx < 0 || mapx >= tilesWidth || mapy < 0 || mapy >= tilesHeight) return none;
4172   // if we already have rope tile there, there is no reason to add another one
4173   if (aname == 'oRope') {
4174     if (isRopePlacedAt(mapx*16, mapy*16)) return none;
4175   }
4177   auto tile = CreateMapTile(mapx*16, mapy*16, aname);
4178   if (!tile) return none;
4179   if (!PutSpawnedMapTile(mapx*16, mapy*16, tile, putToGrid!optional)) {
4180     delete tile;
4181     tile = none;
4182   }
4184   return tile;
4188 final MapTile MakeMapTileAtPix (int xpix, int ypix, name aname, optional bool putToGrid) {
4189   // if we already have rope tile there, there is no reason to add another one
4190   if (aname == 'oRope') {
4191     if (isRopePlacedAt(xpix, ypix)) return none;
4192   }
4194   auto tile = CreateMapTile(xpix, ypix, aname);
4195   if (!tile) return none;
4196   if (!PutSpawnedMapTile(xpix, ypix, tile, putToGrid!optional)) {
4197     delete tile;
4198     tile = none;
4199   }
4201   return tile;
4205 final MapTile MakeMapRopeTileAt (int x0, int y0) {
4206   // if we already have rope tile there, there is no reason to add another one
4207   if (isRopePlacedAt(x0, y0)) return none;
4209   auto tile = CreateMapTile(x0, y0, 'oRope');
4210   if (!PutSpawnedMapTile(x0, y0, tile, putToGrid:true)) {
4211     delete tile;
4212     tile = none;
4213   }
4215   return tile;
4219 // ////////////////////////////////////////////////////////////////////////// //
4220 final MapBackTile CreateBgTile (name sprName, optional int atx0, optional int aty0, optional int aw, optional int ah) {
4221   BackTileImage img = bgtileStore[sprName];
4222   auto res = SpawnObject(MapBackTile);
4223   res.global = global;
4224   res.level = self;
4225   res.bgt = img;
4226   res.bgtName = sprName;
4227   if (specified_atx0) res.tx0 = atx0;
4228   if (specified_aty0) res.ty0 = aty0;
4229   if (specified_aw) res.w = aw;
4230   if (specified_ah) res.h = ah;
4231   if (!res.initialize()) FatalError("cannot create background tile with image '%n'", sprName);
4232   return res;
4236 // ////////////////////////////////////////////////////////////////////////// //
4238 background The background asset from which the new tile will be extracted.
4239 left The x coordinate of the left of the new tile, relative to the background asset's top left corner.
4240 top The y coordinate of the top of the new tile, relative to the background assets top left corner.
4241 width The width of the tile.
4242 height The height of the tile.
4243 x The x position in the room to place the tile.
4244 y The y position in the room to place the tile.
4245 depth The depth at which to place the tile.
4247 final void MakeMapBackTile (name bgname, int left, int top, int width, int height, int x, int y, int depth) {
4248   if (width < 1 || height < 1 || !bgname) return;
4249   auto bgt = bgtileStore[bgname];
4250   if (!bgt) FatalError("cannot load background '%n'", bgname);
4251   MapBackTile bt = SpawnObject(MapBackTile);
4252   bt.global = global;
4253   bt.level = self;
4254   bt.objName = bgname;
4255   bt.bgt = bgt;
4256   bt.bgtName = bgname;
4257   bt.fltx = x;
4258   bt.flty = y;
4259   bt.tx0 = left;
4260   bt.ty0 = top;
4261   bt.w = width;
4262   bt.h = height;
4263   bt.depth = depth;
4264   // find a place for it
4265   if (!backtiles) {
4266     backtiles = bt;
4267     return;
4268   }
4269   // back tiles with the highest depth should come first
4270   MapBackTile ct = backtiles, cprev = none;
4271   while (ct && depth <= ct.depth) { cprev = ct; ct = ct.next; }
4272   // insert before ct
4273   if (cprev) {
4274     bt.next = cprev.next;
4275     cprev.next = bt;
4276   } else {
4277     bt.next = backtiles;
4278     backtiles = bt;
4279   }
4283 // ////////////////////////////////////////////////////////////////////////// //
4284 final MapObject SpawnMapObjectWithClass (class!MapObject oclass) {
4285   if (!oclass) return none;
4287   MapObject obj = SpawnObject(oclass);
4288   if (!obj) FatalError("cannot create map object '%n'", GetClassName(oclass));
4290   //if (aname == 'oShells' || aname == 'oShellSingle' || aname == 'oRopePile') writeln("*** CREATED '", aname, "'");
4292   obj.global = global;
4293   obj.level = self;
4294   obj.objId = ++lastUsedObjectId;
4296   return obj;
4300 final MapObject SpawnMapObject (name aname) {
4301   if (!aname) return none;
4302   auto res = SpawnMapObjectWithClass(findGameObjectClassByName(aname));
4303   if (res && !res.objType) res.objType = aname; // just in case
4304   return res;
4308 final MapObject PutSpawnedMapObject (int x, int y, MapObject obj) {
4309   if (!obj /*|| obj.global || obj.level*/) return none; // oops
4311   obj.fltx = x;
4312   obj.flty = y;
4313   if (!obj.initialize()) { delete obj; return none; } // not fatal
4315   insertObject(obj);
4317   return obj;
4321 final MapObject MakeMapObject (int x, int y, name aname) {
4322   MapObject obj = SpawnMapObject(aname);
4323   obj = PutSpawnedMapObject(x, y, obj);
4324   return obj;
4328 // ////////////////////////////////////////////////////////////////////////// //
4329 int winCutSceneTimer = -1;
4330 int winVolcanoTimer = -1;
4331 int winCutScenePhase = 0;
4332 int winSceneDrawStatus = 0;
4333 int winMoneyCount = 0;
4334 int winTime;
4335 bool winFadeOut = false;
4336 int winFadeLevel = 0;
4337 int winCutsceneSkip = 0; // 1: waiting for pay release; 2: pay released, do skip
4338 bool winCutsceneSwitchToNext = false;
4341 void startWinCutscene () {
4342   global.hasParachute = false;
4343   shakeLeft = 0;
4344   winCutsceneSwitchToNext = false;
4345   winCutsceneSkip = 0;
4346   isKeyPressed(GameConfig::Key.Pay);
4347   isKeyReleased(GameConfig::Key.Pay);
4349   auto olddel = ImmediateDelete;
4350   ImmediateDelete = false;
4351   clearWholeLevel();
4353   createEnd1Room();
4354   fixWallTiles();
4355   addBackgroundGfxDetails();
4357   levBGImgName = 'bgCave';
4358   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
4360   blockWaterChecking = true;
4361   fixLiquidTop();
4362   cleanDeadTiles();
4364   ImmediateDelete = olddel;
4365   CollectGarbage(true); // destroy delayed objects too
4367   if (dumpGridStats) objGrid.dumpStats();
4369   playerExited = false; // just in case
4370   playerExitDoor = none;
4372   osdClear();
4374   setupGhostTime();
4375   global.stopMusic();
4377   inWinCutscene = 1;
4378   winCutSceneTimer = -1;
4379   winCutScenePhase = 0;
4381   /+
4382   if (global.config.gameMode != GameConfig::GameMode.Vanilla) {
4383     if (global.config.bizarre) {
4384       global.yasmScore = 1;
4385       global.config.bizarrePlusTitle = true;
4386     }
4388     array!MapTile toReplace;
4389     forEachTile(delegate bool (MapTile t) {
4390       if (t.objType == 'oGTemple' ||
4391           t.objType == 'oIce' ||
4392           t.objType == 'oDark' ||
4393           t.objType == 'oBrick' ||
4394           t.objType == 'oLush')
4395       {
4396         toReplace[$] = t;
4397       }
4398       return false;
4399     });
4401     foreach (MapTile t; miscTileGrid.allObjects()) {
4402       if (t.objType == 'oGTemple' ||
4403           t.objType == 'oIce' ||
4404           t.objType == 'oDark' ||
4405           t.objType == 'oBrick' ||
4406           t.objType == 'oLush')
4407       {
4408         toReplace[$] = t;
4409       }
4410     }
4412     foreach (MapTile t; toReplace) {
4413       if (t.iy < 192) {
4414         t.cleanDeath = true;
4415             if (rand(1,120) == 1) instance_change(oGTemple, false);
4416         else if (rand(1,100) == 1) instance_change(oIce, false);
4417         else if (rand(1,90) == 1) instance_change(oDark, false);
4418         else if (rand(1,80) == 1) instance_change(oBrick, false);
4419         else if (rand(1,70) == 1) instance_change(oLush, false);
4420           }
4421       }
4422       with (oBrick)
4423       {
4424           if (y &lt; 192)
4425           {
4426               cleanDeath = true;
4427               if (rand(1,5) == 1) instance_change(oLush, false);
4428           }
4429       }
4430   }
4431   +/
4432   //!instance_create(0, 0, oBricks);
4434   //shakeToggle = false;
4435   //oPDummy.status = 2;
4437   //timer = 0;
4439   /+
4440   if (global.kaliPunish &gt;= 2) {
4441       instance_create(oPDummy.x, oPDummy.y+2, oBall2);
4442       obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
4443       obj.linkVal = 1;
4444       obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
4445       obj.linkVal = 2;
4446       obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
4447       obj.linkVal = 3;
4448       obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
4449       obj.linkVal = 4;
4450   }
4451   +/
4455 void startWinCutsceneVolcano () {
4456   global.hasParachute = false;
4457   /*
4458   writeln("VOLCANO HOLD 0: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4459   writeln("VOLCANO PICK 0: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4460   */
4462   shakeLeft = 0;
4463   winCutsceneSwitchToNext = false;
4464   auto olddel = ImmediateDelete;
4465   ImmediateDelete = false;
4466   clearWholeLevel();
4468   levBGImgName = '';
4469   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
4471   blockWaterChecking = true;
4473   ImmediateDelete = olddel;
4474   CollectGarbage(true); // destroy delayed objects too
4476   spawnPlayerAt(2*16+8, 11*16+8);
4477   player.dir = MapEntity::Dir.Right;
4479   playerExited = false; // just in case
4480   playerExitDoor = none;
4482   osdClear();
4484   setupGhostTime();
4485   global.stopMusic();
4487   inWinCutscene = 2;
4488   winCutSceneTimer = -1;
4489   winCutScenePhase = 0;
4491   MakeMapTile(0, 0, 'oEnd2BG');
4492   realViewStart.x = 0;
4493   realViewStart.y = 0;
4494   viewStart.x = 0;
4495   viewStart.y = 0;
4497   viewMin.x = 0;
4498   viewMin.y = 0;
4499   viewMax.x = 320;
4500   viewMax.y = 240;
4502   player.dead = false;
4503   player.active = true;
4504   player.visible = false;
4505   player.removeBallAndChain(temp:true);
4506   player.stunned = false;
4507   player.status = MapObject::FALLING;
4508   if (player.holdItem) player.holdItem.visible = false;
4509   player.fltx = 320/2;
4510   player.flty = 0;
4512   /*
4513   writeln("VOLCANO HOLD 1: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4514   writeln("VOLCANO PICK 1: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4515   */
4519 void startWinCutsceneWinFall () {
4520   global.hasParachute = false;
4521   /*
4522   writeln("FALL HOLD 0: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4523   writeln("FALL PICK 0: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4524   */
4526   shakeLeft = 0;
4527   winCutsceneSwitchToNext = false;
4529   auto olddel = ImmediateDelete;
4530   ImmediateDelete = false;
4531   clearWholeLevel();
4533   createEnd3Room();
4534   setMenuTilesVisible(false);
4535   //fixWallTiles();
4536   //addBackgroundGfxDetails();
4538   levBGImgName = '';
4539   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
4541   blockWaterChecking = true;
4542   fixLiquidTop();
4543   cleanDeadTiles();
4545   ImmediateDelete = olddel;
4546   CollectGarbage(true); // destroy delayed objects too
4548   if (dumpGridStats) objGrid.dumpStats();
4550   playerExited = false; // just in case
4551   playerExitDoor = none;
4553   osdClear();
4555   setupGhostTime();
4556   global.stopMusic();
4558   inWinCutscene = 3;
4559   winCutSceneTimer = -1;
4560   winCutScenePhase = 0;
4562   player.dead = false;
4563   player.active = true;
4564   player.visible = false;
4565   player.removeBallAndChain(temp:true);
4566   player.stunned = false;
4567   player.status = MapObject::FALLING;
4568   if (player.holdItem) player.holdItem.visible = false;
4569   player.fltx = 320/2;
4570   player.flty = 0;
4572   winSceneDrawStatus = 0;
4573   winMoneyCount = 0;
4575   winFadeOut = false;
4576   winFadeLevel = 0;
4578   /*
4579   writeln("FALL HOLD 1: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4580   writeln("FALL PICK 1: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4581   */
4585 void setGameOver () {
4586   if (inWinCutscene) {
4587     player.visible = false;
4588     player.removeBallAndChain(temp:true);
4589     if (player.holdItem) player.holdItem.visible = false;
4590   }
4591   player.dead = true;
4592   if (inWinCutscene > 0) {
4593     winFadeOut = true;
4594     winFadeLevel = 255;
4595     winSceneDrawStatus = 8;
4596   }
4600 MapTile findEndPlatTile () {
4601   return forEachTile(delegate bool (MapTile t) { return (t isa MapTileEndPlat); }, castClass:MapTileEndPlat);
4605 MapObject findBigTreasure () {
4606   return forEachObject(delegate bool (MapObject o) { return (o isa MapObjectBigTreasure); }, castClass:MapObjectBigTreasure);
4610 void setMenuTilesVisible (bool vis) {
4611   if (vis) {
4612     forEachTile(delegate bool (MapTile t) {
4613       if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4614         t.invisible = false;
4615       }
4616       return false;
4617     });
4618   } else {
4619     forEachTile(delegate bool (MapTile t) {
4620       if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4621         t.invisible = true;
4622       }
4623       return false;
4624     });
4625   }
4629 void setMenuTilesOnTop () {
4630   forEachTile(delegate bool (MapTile t) {
4631     if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4632       t.depth = 1;
4633     }
4634     return false;
4635   });
4639 void winCutscenePlayerControl (PlayerPawn plr) {
4640   auto payPress = isKeyPressed(GameConfig::Key.Pay);
4641   auto payRelease = isKeyReleased(GameConfig::Key.Pay);
4643   switch (winCutsceneSkip) {
4644     case 0: // nothing was pressed
4645       if (payPress) winCutsceneSkip = 1;
4646       break;
4647     case 1: // waiting for pay release
4648       if (payRelease) winCutsceneSkip = 2;
4649       break;
4650     case 2: // pay released, do skip
4651       setGameOver();
4652       return;
4653   }
4655   // first winning room
4656   if (inWinCutscene == 1) {
4657     if (plr.ix < 448+8) {
4658       plr.kRight = true;
4659       return;
4660     }
4662     // waiting for chest to open
4663     if (winCutScenePhase == 0) {
4664       winCutSceneTimer = 120/2;
4665       winCutScenePhase = 1;
4666       return;
4667     }
4669     // spawn big idol
4670     if (winCutScenePhase == 1) {
4671       if (--winCutSceneTimer == 0) {
4672         winCutScenePhase = 2;
4673         winCutSceneTimer = 20;
4674         forEachObject(delegate bool (MapObject o) {
4675           if (o isa MapObjectBigChest) {
4676             o.setSprite(global.config.gameMode == GameConfig::GameMode.Vanilla ? 'sBigChestOpen' : 'sBigChestOpen2');
4677             auto treasure = MakeMapObject(o.ix, o.iy, 'oBigTreasure');
4678             if (treasure) {
4679               treasure.yVel = -4;
4680               treasure.xVel = -3;
4681               o.playSound('sndClick');
4682               //!!!if (global.config.gameMode != GameConfig::GameMode.Vanilla) scrSprayGems(oBigChest.x+24, oBigChest.y+24);
4683             }
4684           }
4685           return false;
4686         });
4687       }
4688       return;
4689     }
4691     // lava pump wait
4692     if (winCutScenePhase == 2) {
4693       if (--winCutSceneTimer == 0) {
4694         winCutScenePhase = 3;
4695         winCutSceneTimer = 50;
4696       }
4697       return;
4698     }
4700     // lava pump start
4701     if (winCutScenePhase == 3) {
4702       auto ep = findEndPlatTile();
4703       if (ep) MakeMapObject(ep.ix+global.randOther(0, 80), /*ep.iy*/192+32, 'oBurn');
4704       if (--winCutSceneTimer == 0) {
4705         winCutScenePhase = 4;
4706         winCutSceneTimer = 10;
4707         if (ep) MakeMapObject(ep.ix, ep.iy+30, 'oLavaSpray');
4708         scrShake(9999);
4709       }
4710       return;
4711     }
4713     // lava pump first accel
4714     if (winCutScenePhase == 4) {
4715       if (--winCutSceneTimer == 0) {
4716         forEachObject(delegate bool (MapObject o) {
4717           if (o isa MapObjectLavaSpray) o.yAcc = -0.1;
4718           return false;
4719         });
4720       }
4721     }
4723     // lava pump complete
4724     if (winCutScenePhase == 5) {
4725       if (--winCutSceneTimer == 0) {
4726         //if (oLavaSpray) oLavaSpray.yAcc = -0.1;
4727         startWinCutsceneVolcano();
4728       }
4729       return;
4730     }
4731     return;
4732   }
4735   // volcano room
4736   if (inWinCutscene == 2) {
4737     plr.flty = 0;
4739     // initialize
4740     if (winCutScenePhase == 0) {
4741       winCutSceneTimer = 50;
4742       winCutScenePhase = 1;
4743       winVolcanoTimer = 10;
4744       return;
4745     }
4747     if (winVolcanoTimer > 0) {
4748       if (--winVolcanoTimer == 0) {
4749         MakeMapObject(224+global.randOther(0,48), 144+global.randOther(0,8), 'oVolcanoFlame');
4750         winVolcanoTimer = global.randOther(10, 20);
4751       }
4752     }
4754     // plr sil
4755     if (winCutScenePhase == 1) {
4756       if (--winCutSceneTimer == 0) {
4757         winCutSceneTimer = 30;
4758         winCutScenePhase = 2;
4759         auto sil = MakeMapObject(240, 132, 'oPlayerSil');
4760         //sil.xVel = -6;
4761         //sil.yVel = -8;
4762       }
4763       return;
4764     }
4766     // treasure sil
4767     if (winCutScenePhase == 2) {
4768       if (--winCutSceneTimer == 0) {
4769         winCutScenePhase = 3;
4770         auto sil = MakeMapObject(240, 132, 'oTreasureSil');
4771         //sil.xVel = -6;
4772         //sil.yVel = -8;
4773       }
4774       return;
4775     }
4777     return;
4778   }
4780   // winning camel room
4781   if (inWinCutscene == 3) {
4782     //if (!player.holdItem)  writeln("SCENE 3: LOST ITEM!");
4784     if (!plr.visible) plr.flty = -32;
4786     // initialize
4787     if (winCutScenePhase == 0) {
4788       winCutSceneTimer = 50;
4789       winCutScenePhase = 1;
4790       return;
4791     }
4793     // fall sound
4794     if (winCutScenePhase == 1) {
4795       if (--winCutSceneTimer == 0) {
4796         winCutSceneTimer = 50;
4797         winCutScenePhase = 2;
4798         plr.playSound('sndPFall');
4799         plr.visible = true;
4800         plr.active = true;
4801         writeln("MUST BE CHAINED: ", plr.mustBeChained);
4802         if (plr.mustBeChained) {
4803           plr.removeBallAndChain(temp:true);
4804           plr.spawnBallAndChain();
4805         }
4806         /*
4807         writeln("HOLD: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4808         writeln("PICK: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4809         */
4810         if (!player.holdItem && player.pickedItem) player.scrSwitchToPocketItem(forceIfEmpty:false);
4811         if (player.holdItem) {
4812           player.holdItem.visible = true;
4813           player.holdItem.canLiveOutsideOfLevel = true;
4814           writeln("HOLD ITEM: '", GetClassName(player.holdItem.Class), "'");
4815         }
4816         plr.status == MapObject::FALLING;
4817         global.plife += 99; // just in case
4818       }
4819       return;
4820     }
4822     if (winCutScenePhase == 2) {
4823       auto ball = plr.getMyBall();
4824       if (ball && plr.holdItem != ball) {
4825         ball.teleportTo(plr.fltx, plr.flty+8);
4826         ball.yVel = 6;
4827         ball.myGrav = 0.6;
4828       }
4829       if (plr.status == MapObject::STUNNED || plr.stunned) {
4830         //alarm[0] = 70;
4831         //alarm[1] = 50;
4832         //status = GETUP;
4833         auto treasure = MakeMapObject(144+16+8, -32, 'oBigTreasure');
4834         if (treasure) treasure.depth = 1;
4835         winCutScenePhase = 3;
4836         plr.stunTimer = 30;
4837         plr.playSound('sndTFall');
4838       }
4839       return;
4840     }
4842     if (winCutScenePhase == 3) {
4843       if (plr.status != MapObject::STUNNED && !plr.stunned) {
4844         auto bt = findBigTreasure();
4845         if (bt) {
4846           if (bt.yVel == 0) {
4847             //plr.yVel = -4;
4848             //plr.status = MapObject::JUMPING;
4849             plr.kJump = true;
4850             plr.kJumpPressed = true;
4851             winCutScenePhase = 4;
4852             winCutSceneTimer = 50;
4853           }
4854         }
4855       }
4856       return;
4857     }
4859     if (winCutScenePhase == 4) {
4860       if (--winCutSceneTimer == 0) {
4861         setMenuTilesVisible(true);
4862         winCutScenePhase = 5;
4863         winSceneDrawStatus = 1;
4864         global.playMusic('musVictory', loop:false);
4865         winCutSceneTimer = 50;
4866       }
4867       return;
4868     }
4870     if (winCutScenePhase == 5) {
4871       if (winSceneDrawStatus == 3) {
4872         int money = stats.money;
4873         if (winMoneyCount < money) {
4874           if (money-winMoneyCount > 1000) {
4875             winMoneyCount += 1000;
4876           } else if (money-winMoneyCount > 100) {
4877             winMoneyCount += 100;
4878           } else if (money-winMoneyCount > 10) {
4879             winMoneyCount += 10;
4880           } else {
4881             ++winMoneyCount;
4882           }
4883         }
4884         if (winMoneyCount >= money) {
4885           winMoneyCount = money;
4886           ++winSceneDrawStatus;
4887         }
4888         return;
4889       }
4891       if (winSceneDrawStatus == 7) {
4892         winFadeOut = true;
4893         winFadeLevel += 1;
4894         if (winFadeLevel >= 255) {
4895           ++winSceneDrawStatus;
4896           winCutSceneTimer = 30*30;
4897         }
4898         return;
4899       }
4901       if (winSceneDrawStatus == 8) {
4902         if (--winCutSceneTimer == 0) {
4903           setGameOver();
4904         }
4905         return;
4906       }
4908       if (--winCutSceneTimer == 0) {
4909         ++winSceneDrawStatus;
4910         winCutSceneTimer = 50;
4911       }
4912     }
4914     return;
4915   }
4919 // ////////////////////////////////////////////////////////////////////////// //
4920 void renderWinCutsceneOverlay () {
4921   if (inWinCutscene == 3) {
4922     if (winSceneDrawStatus > 0) {
4923       Video.color = 0xff_ff_ff;
4924       sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
4925       //draw_set_color(txtCol);
4926       drawTextAt(64, 32, "YOU MADE IT!");
4928       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4929       if (global.config.gameMode == GameConfig::GameMode.Vanilla) {
4930         Video.color = 0xff_ff_00; //draw_set_color(c_yellow);
4931         drawTextAt(64, 48, "Classic Mode done!");
4932       } else {
4933         Video.color = 0x00_80_80; //draw_set_color(c_teal);
4934         if (global.config.bizarrePlus) drawTextAt(64, 48, "Bizarre Mode Plus done!");
4935         else drawTextAt(64, 48, "Bizarre Mode done!");
4936         //draw_set_color(c_white);
4937       }
4938       if (!global.usedShortcut) {
4939         Video.color = 0xc0_c0_c0; //draw_set_color(c_silver);
4940         drawTextAt(64, 56, "No shortcuts used!");
4941         //draw_set_color(c_yellow);
4942       }
4943     }
4945     if (winSceneDrawStatus > 1) {
4946       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4947       //draw_set_color(txtCol);
4948       Video.color = 0xff_ff_ff;
4949       drawTextAt(64, 64, "FINAL SCORE:");
4950     }
4952     if (winSceneDrawStatus > 2) {
4953       sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
4954       Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4955       drawTextAt(64, 72, va("$%d", winMoneyCount));
4956     }
4958     if (winSceneDrawStatus > 4) {
4959       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4960       Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4961       drawTextAt(64, 96, va("Time: %s", time2str(winTime/30)));
4962       /*
4963       draw_set_color(c_white);
4964       if (s &lt; 10) draw_text(96+24, 96, string(m) + ":0" + string(s));
4965       else draw_text(96+24, 96, string(m) + ":" + string(s));
4966       */
4967     }
4969     if (winSceneDrawStatus > 5) {
4970       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4971       Video.color = 0xff_ff_ff; //draw_set_color(txtCol);
4972       drawTextAt(64, 96+8, "Kills: ");
4973       Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4974       drawTextAt(96+24, 96+8, va("%s", stats.countKills()));
4975     }
4977     if (winSceneDrawStatus > 6) {
4978       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4979       Video.color = 0xff_ff_ff; //draw_set_color(txtCol);
4980       drawTextAt(64, 96+16, "Saves: ");
4981       Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4982       drawTextAt(96+24, 96+16, va("%s", stats.damselsSaved));
4983     }
4985     if (winFadeOut) {
4986       Video.color = (255-clamp(winFadeLevel, 0, 255))<<24;
4987       Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
4988     }
4990     if (winSceneDrawStatus == 8) {
4991       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4992       Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4993       string lastString;
4994       if (global.config.gameMode == GameConfig::GameMode.Vanilla) {
4995         Video.color = 0xff_ff_00; //draw_set_color(c_yellow);
4996         lastString = "YOU SHALL BE REMEMBERED AS A HERO.";
4997       } else {
4998         Video.color = 0x00_ff_ff;
4999         if (global.config.bizarrePlus) lastString = "ANOTHER LEGENDARY ADVENTURE!";
5000         else lastString = "YOUR DISCOVERIES WILL BE CELEBRATED!";
5001       }
5002       auto strLen = lastString.length*8;
5003       int n = 320-strLen;
5004       n = trunc(ceil(n/2.0));
5005       drawTextAt(n, 116, lastString);
5006     }
5007   }
5011 // ////////////////////////////////////////////////////////////////////////// //
5012 #include "roomTitle.vc"
5013 #include "roomTrans1.vc"
5014 #include "roomTrans2.vc"
5015 #include "roomTrans3.vc"
5016 #include "roomTrans4.vc"
5017 #include "roomOlmec.vc"
5018 #include "roomEnd.vc"
5019 #include "roomTutorial.vc"
5020 #include "roomScores.vc"
5021 #include "roomStars.vc"
5022 #include "roomSun.vc"
5023 #include "roomMoon.vc"
5026 // ////////////////////////////////////////////////////////////////////////// //
5027 #include "packages/Generator/loadRoomGens.vc"
5028 #include "packages/Generator/loadEntityGens.vc"