F1 now shows game map
[k8vacspelynky.git] / GameLevel.vc
blobbfc7c2a63125d22567753f48f6db27ccd5002b89
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); }
197 bool isHUDEnabled () {
198   if (inWinCutscene) return false;
199   if (lg.finalBossLevel) return true;
200   if (isNormalLevel()) return true;
201   // allow HUD in challenge chambers
202   return false;
206 // ////////////////////////////////////////////////////////////////////////// //
207 // stats
208 void addDeath (name aname) { if (isNormalLevel()) stats.addDeath(aname); }
210 int starsKills;
211 int sunScore;
212 int moonScore;
213 int moonTimer;
215 void addKill (name aname, optional bool telefrag) {
216        if (isNormalLevel()) stats.addKill(aname, telefrag!optional);
217   else if (aname == 'Shopkeeper' && levelKind == LevelKind.Stars) { ++stats.starsKills; ++starsKills; }
220 void addCollect (name aname, optional int amount) { if (isNormalLevel()) stats.addCollect(aname, amount!optional); }
222 void addDamselSaved () { if (isNormalLevel()) stats.addDamselSaved(); }
223 void addIdolStolen () { if (isNormalLevel()) stats.addIdolStolen(); }
224 void addIdolConverted () { if (isNormalLevel()) stats.addIdolConverted(); }
225 void addCrystalIdolStolen () { if (isNormalLevel()) stats.addCrystalIdolStolen(); }
226 void addCrystalIdolConverted () { if (isNormalLevel()) stats.addCrystalIdolConverted(); }
227 void addGhostSummoned () { if (isNormalLevel()) stats.addGhostSummoned(); }
230 // ////////////////////////////////////////////////////////////////////////// //
231 static final string time2str (int time) {
232   int secs = time%60; time /= 60;
233   int mins = time%60; time /= 60;
234   int hours = time%24; time /= 24;
235   int days = time;
236   if (days) return va("%d DAYS, %d:%02d:%02d", days, hours, mins, secs);
237   if (hours) return va("%d:%02d:%02d", hours, mins, secs);
238   return va("%02d:%02d", mins, secs);
242 // ////////////////////////////////////////////////////////////////////////// //
243 final int tilesWidth () { return lg.levelRoomWidth*RoomGen::Width+2; }
244 final int tilesHeight () { return (lg.finalBossLevel ? 55 : lg.levelRoomHeight*RoomGen::Height+2); }
247 // ////////////////////////////////////////////////////////////////////////// //
248 protected void resetGameInternal () {
249   if (player) player.removeBallAndChain();
250   resetBMCOG = false;
251   inWinCutscene = 0;
252   shakeLeft = 0;
253   udjatAlarm = 0;
254   starsKills = 0;
255   sunScore = 0;
256   moonScore = 0;
257   moonTimer = 0;
258   damselSaved = 0;
259   xmoney = 0;
260   collectCounter = 0;
261   levelMoneyStart = 0;
262   if (player) {
263     player.removeBallAndChain();
264     auto hi = player.holdItem;
265     player.holdItem = none;
266     if (hi) hi.instanceRemove();
267     hi = player.pickedItem;
268     player.pickedItem = none;
269     if (hi) hi.instanceRemove();
270   }
271   time = 0;
272   lastRenderTime = -1;
273   levelStartTime = 0;
274   levelEndTime = 0;
275   global.resetGame();
276   stats.clearGameTotals();
280 // this won't generate a level yet
281 void restartGame () {
282   resetGameInternal();
283   if (global.startMoney > 0) stats.setMoneyCheat();
284   stats.setMoney(global.startMoney);
285   levelKind = LevelKind.Normal;
289 // complement function to `restart game`
290 void generateNormalLevel () {
291   generateLevel();
292   centerViewAtPlayer();
296 void restartTitle () {
297   resetGameInternal();
298   stats.setMoney(0);
299   createSpecialLevel(LevelKind.Title, &createTitleRoom, 'musTitle');
300   global.plife = 9999;
301   global.bombs = 0;
302   global.rope = 0;
303   global.arrows = 0;
304   global.sgammo = 0;
308 void restartTutorial () {
309   resetGameInternal();
310   stats.setMoney(0);
311   createSpecialLevel(LevelKind.Tutorial, &createTutorialRoom, 'musCave');
312   global.plife = 4;
313   global.bombs = 0;
314   global.rope = 4;
315   global.arrows = 0;
316   global.sgammo = 0;
320 void restartScores () {
321   resetGameInternal();
322   stats.setMoney(0);
323   createSpecialLevel(LevelKind.Scores, &createScoresRoom, 'musTitle');
324   global.plife = 4;
325   global.bombs = 0;
326   global.rope = 0;
327   global.arrows = 0;
328   global.sgammo = 0;
332 void restartStarsRoom () {
333   resetGameInternal();
334   stats.setMoney(0);
335   createSpecialLevel(LevelKind.Stars, &createStarsRoom, '');
336   global.plife = 8;
337   global.bombs = 0;
338   global.rope = 0;
339   global.arrows = 0;
340   global.sgammo = 0;
344 void restartSunRoom () {
345   resetGameInternal();
346   stats.setMoney(0);
347   createSpecialLevel(LevelKind.Sun, &createSunRoom, '');
348   global.plife = 8;
349   global.bombs = 0;
350   global.rope = 0;
351   global.arrows = 0;
352   global.sgammo = 0;
356 void restartMoonRoom () {
357   resetGameInternal();
358   stats.setMoney(0);
359   createSpecialLevel(LevelKind.Moon, &createMoonRoom, '');
360   global.plife = 8;
361   global.bombs = 0;
362   global.rope = 0;
363   global.arrows = 100;
364   global.sgammo = 0;
368 // ////////////////////////////////////////////////////////////////////////// //
369 // generate angry shopkeeper at exit if murderer or thief
370 void generateAngryShopkeepers () {
371   if (global.murderer || global.thiefLevel > 0) {
372     foreach (MapTile e; allExits) {
373       auto obj = MonsterShopkeeper(MakeMapObject(e.ix, e.iy, 'oShopkeeper'));
374       if (obj) {
375         obj.style = 'Bounty Hunter';
376         obj.status = MapObject::PATROL;
377       }
378     }
379   }
383 // ////////////////////////////////////////////////////////////////////////// //
384 final void resetRoomBounds () {
385   viewMin.x = 0;
386   viewMin.y = 0;
387   viewMax.x = tilesWidth*16;
388   viewMax.y = tilesHeight*16;
389   // Great Lake is bottomless (nope)
390   //if (global.lake == 1) viewMax.y -= 16;
391   //writeln("ROOM BOUNDS: (", viewMax.x, "x", viewMax.y, ")");
395 final void setRoomBounds (int x0, int y0, int x1, int y1) {
396   viewMin.x = x0;
397   viewMin.y = y0;
398   viewMax.x = x1+16;
399   viewMax.y = y1+16;
403 // ////////////////////////////////////////////////////////////////////////// //
404 struct OSDMessage {
405   string msg;
406   float timeout; // seconds
407   float starttime; // for active
408   bool active; // true: timeout is `GetTickCount()` dismissing time
411 array!OSDMessage msglist; // [0]: current one
414 private final void osdCheckTimeouts () {
415   auto stt = GetTickCount();
416   while (msglist.length) {
417     if (!msglist[0].active) {
418       msglist[0].active = true;
419       msglist[0].starttime = stt;
420     }
421     if (msglist[0].starttime+msglist[0].timeout >= stt) break;
422     msglist.remove(0);
423   }
427 final bool osdHasMessage () {
428   osdCheckTimeouts();
429   return (msglist.length > 0);
433 final string osdGetMessage (out float timeLeft, out float timeStart) {
434   osdCheckTimeouts();
435   if (msglist.length == 0) { timeLeft = 0; return ""; }
436   auto stt = GetTickCount();
437   timeStart = msglist[0].starttime;
438   timeLeft = fmax(0, msglist[0].starttime+msglist[0].timeout-stt);
439   return msglist[0].msg;
443 final void osdClear () {
444   msglist.clear();
448 final void osdMessage (string msg, optional float timeout, optional bool urgent) {
449   if (!msg) return;
450   msg = global.expandString(msg);
451   if (!specified_timeout) timeout = 3.33;
452   // special message for shops
453   if (timeout == -666) {
454     if (!msg) return;
455     if (msglist.length && msglist[0].msg == msg) return;
456     if (msglist.length == 0 || msglist[0].msg != msg) {
457       osdClear();
458       msglist.length += 1;
459       msglist[0].msg = msg;
460     }
461     msglist[0].active = false;
462     msglist[0].timeout = 3.33;
463     osdCheckTimeouts();
464     return;
465   }
466   if (timeout < 0.1) return;
467   timeout = fmax(1.0, timeout);
468   //writeln("OSD: ", msg);
469   // find existing one, and bring it to the top
470   int oldidx = 0;
471   for (; oldidx < msglist.length; ++oldidx) {
472     if (msglist[oldidx].msg == msg) break; // i found her!
473   }
474   // duplicate?
475   if (oldidx < msglist.length) {
476     // yeah, move duplicate to the top
477     msglist[oldidx].timeout = fmax(timeout, msglist[oldidx].timeout);
478     msglist[oldidx].active = false;
479     if (urgent && oldidx != 0) {
480       timeout = msglist[oldidx].timeout;
481       msglist.remove(oldidx);
482       msglist.insert(0);
483       msglist[0].msg = msg;
484       msglist[0].timeout = timeout;
485       msglist[0].active = false;
486     }
487   } else if (urgent) {
488     msglist.insert(0);
489     msglist[0].msg = msg;
490     msglist[0].timeout = timeout;
491     msglist[0].active = false;
492   } else {
493     // new one
494     msglist.length += 1;
495     msglist[$-1].msg = msg;
496     msglist[$-1].timeout = timeout;
497     msglist[$-1].active = false;
498   }
499   osdCheckTimeouts();
503 // ////////////////////////////////////////////////////////////////////////// //
504 void setup (GameGlobal aGlobal, SpriteStore aSprStore, BackTileStore aBGTileStore) {
505   global = aGlobal;
506   sprStore = aSprStore;
507   bgtileStore = aBGTileStore;
509   lg = SpawnObject(LevelGen);
510   lg.global = global;
511   lg.level = self;
513   objGrid = SpawnObject(EntityGrid);
514   objGrid.setup(0, 0, MaxTilesWidth*16, MaxTilesHeight*16);
518 // stores should be set
519 void onLoaded () {
520   checkWater = true;
521   liquidTileCount = 0;
522   levBGImg = bgtileStore[levBGImgName];
523   foreach (MapEntity o; objGrid.allObjects()) {
524     o.onLoaded();
525     auto t = MapTile(o);
526     if (t && (t.lava || t.water)) ++liquidTileCount;
527   }
528   for (MapBackTile bt = backtiles; bt; bt = bt.next) bt.onLoaded();
529   if (player) player.onLoaded();
530   //FIXME
531   if (msglist.length) {
532     msglist[0].active = false;
533     msglist[0].timeout = 0.200;
534     osdCheckTimeouts();
535   }
536   if (lg && lg.musicName) global.playMusic(lg.musicName); else global.stopMusic();
540 // ////////////////////////////////////////////////////////////////////////// //
541 void pickedSpectacles () {
542   foreach (MapTile t; objGrid.allObjectsSafe(MapTile)) t.onGotSpectacles();
546 // ////////////////////////////////////////////////////////////////////////// //
547 #include "rgentile.vc"
548 #include "rgenobj.vc"
551 void onLevelExited () {
552   if (playerExitDoor isa TitleTileXTitle) {
553     playerExitDoor = none;
554     restartTitle();
555     return;
556   }
557   // title
558   if (isTitleRoom() || levelKind == LevelKind.Scores) {
559     if (playerExitDoor) processTitleExit(playerExitDoor);
560     playerExitDoor = none;
561     return;
562   }
563   if (isTutorialRoom()) {
564     playerExitDoor = none;
565     restartGame();
566     global.currLevel = 1;
567     generateNormalLevel();
568     return;
569   }
570   // challenges
571   if (levelKind == LevelKind.Stars || levelKind == LevelKind.Sun || levelKind == LevelKind.Moon) {
572     playerExitDoor = none;
573     levelEndTime = time;
574     if (onLevelExitedCB) onLevelExitedCB();
575     restartTitle();
576     return;
577   }
578   // normal level
579   if (isNormalLevel()) {
580     stats.maxLevelComplete = max(stats.maxLevelComplete, global.currLevel);
581     levelEndTime = time;
582     if (playerExitDoor) {
583       if (playerExitDoor.objType == 'oXGold') {
584         writeln("exiting to City Of Gold");
585         global.cityOfGold = true;
586         //!global.currLevel += 1;
587       } else if (playerExitDoor.objType == 'oXMarket') {
588         writeln("exiting to Black Market");
589         global.genBlackMarket = true;
590         //!global.currLevel += 1;
591       }
592     }
593   }
594   if (onLevelExitedCB) onLevelExitedCB();
595   //
596   playerExitDoor = none;
597   if (levelKind == LevelKind.Transition) {
598     if (global.thiefLevel > 0) global.thiefLevel -= 1;
599     if (global.alienCraft) ++global.alienCraft;
600     if (global.yetiLair) ++global.yetiLair;
601     if (global.lake) ++global.lake;
602     if (global.cityOfGold) ++global.cityOfGold;
603     //orig dbg:if (global.currLevel == 1) global.currLevel += firstLevelSkip; else global.currLevel += levelSkip;
604     /+
605     if (!global.blackMarket && !global.cityOfGold /*&& !global.yetiLair*/) {
606       global.currLevel += 1;
607     }
608     +/
609     ++global.currLevel;
610     generateLevel();
611   } else {
612     // < 20 seconds per level: looks like a speedrun
613     global.noDarkLevel = (levelEndTime > levelStartTime && levelEndTime-levelStartTime < 20*30);
614     if (lg.finalBossLevel) {
615       winTime = time;
616       ++stats.gamesWon;
617       // add money for big idol
618       player.addScore(50000);
619       stats.gameOver();
620       startWinCutscene();
621     } else {
622       generateTransitionLevel();
623     }
624   }
625   //centerViewAtPlayer();
629 void onOlmecDead (MapObject o) {
630   writeln("*** OLMEC IS DEAD!");
631   foreach (MapTile t; allExits) {
632     if (t.exit) {
633       t.openExit();
634       auto st = checkTileAtPoint(t.ix+8, t.iy+16);
635       if (!st) {
636         st = MakeMapTile(t.ix/16, t.iy/16+1, 'oTemple');
637         st.ore = 0;
638       }
639       st.invincible = true;
640     }
641   }
645 void generateLevelMessages () {
646   writeln("LEVEL NUMBER: ", global.currLevel);
647   if (global.darkLevel) {
648     if (global.hasCrown) {
649        osdMessage("THE HEDJET SHINES BRIGHTLY.");
650        global.darkLevel = false;
651     } else if (global.config.scumDarkness < 2) {
652       osdMessage("I CAN'T SEE A THING!\nI'D BETTER USE THESE FLARES!");
653     }
654   }
656   if (global.blackMarket) osdMessage("WELCOME TO THE BLACK MARKET!");
658   if (global.cemetary) osdMessage("THE DEAD ARE RESTLESS!");
659   if (global.lake == 1) osdMessage("I CAN HEAR RUSHING WATER...");
661   if (global.snakePit) osdMessage("I HEAR SNAKES... I HATE SNAKES!");
662   if (global.yetiLair == 1) osdMessage("IT SMELLS LIKE WET FUR IN HERE!");
663   if (global.alienCraft == 1) osdMessage("THERE'S A PSYCHIC PRESENCE HERE!");
664   if (global.cityOfGold == 1) {
665     if (true /*hasOlmec()*/) osdMessage("IT'S THE LEGENDARY CITY OF GOLD!");
666   }
668   if (global.sacrificePit) osdMessage("I CAN HEAR PRAYERS TO KALI!");
672 final MapObject debugSpawnObjectWithClass (class!MapObject oclass, optional bool playerDir) {
673   if (!oclass) return none;
674   int dx = 0, dy = 0;
675   bool canLeft = !isSolidAtPoint(player.ix-8, player.iy);
676   bool canRight = !isSolidAtPoint(player.ix+16, player.iy);
677   if (!canLeft && !canRight) return none;
678   if (canLeft && canRight) {
679     if (playerDir) {
680       dx = (player.dir == MapObject::Dir.Left ? -16 : 16);
681     } else {
682       dx = 16;
683     }
684   } else {
685     dx = (canLeft ? -16 : 16);
686   }
687   auto obj = SpawnMapObjectWithClass(oclass);
688   if (obj isa MapEnemy) { dx -= 8; dy -= 8; }
689   if (obj) PutSpawnedMapObject(player.ix+dx, player.iy+dy, obj);
690   return obj;
694 final MapObject debugSpawnObject (name aname) {
695   if (!aname) return none;
696   return debugSpawnObjectWithClass(findGameObjectClassByName(aname));
700 void createSpecialLevel (LevelKind kind, scope void delegate () creator, name amusic) {
701   global.darkLevel = false;
702   udjatAlarm = 0;
703   xmoney = 0;
704   collectCounter = 0;
705   global.resetStartingItems();
707   global.setMusicPitch(1.0);
708   levelKind = kind;
710   auto olddel = ImmediateDelete;
711   ImmediateDelete = false;
712   clearWholeLevel();
714   creator();
716   setMenuTilesOnTop();
718   fixWallTiles();
719   addBackgroundGfxDetails();
720   //levBGImgName = 'bgCave';
721   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
723   blockWaterChecking = true;
724   fixLiquidTop();
725   cleanDeadTiles();
727   ImmediateDelete = olddel;
728   CollectGarbage(true); // destroy delayed objects too
730   if (dumpGridStats) objGrid.dumpStats();
732   playerExited = false; // just in case
733   playerExitDoor = none;
735   osdClear();
737   setupGhostTime();
738   lg.musicName = amusic;
739   if (amusic) global.playMusic(lg.musicName); else global.stopMusic();
743 void createTitleLevel () {
744   createSpecialLevel(LevelKind.Title, &createTitleRoom, 'musTitle');
748 void createTutorialLevel () {
749   createSpecialLevel(LevelKind.Tutorial, &createTutorialRoom, 'musCave');
750   global.plife = 4;
751   global.bombs = 0;
752   global.rope = 4;
753   global.arrows = 0;
754   global.sgammo = 0;
758 // `global.currLevel` is the new level
759 void generateTransitionLevel () {
760   global.darkLevel = false;
761   udjatAlarm = 0;
762   xmoney = 0;
763   collectCounter = 0;
765   global.setMusicPitch(1.0);
766   switch (global.config.transitionMusicMode) {
767     case GameConfig::MusicMode.Silent: global.stopMusic(); break;
768     case GameConfig::MusicMode.Restart: global.restartMusic(); break;
769     case GameConfig::MusicMode.DontTouch: break;
770   }
772   levelKind = LevelKind.Transition;
774   auto olddel = ImmediateDelete;
775   ImmediateDelete = false;
776   clearWholeLevel();
778        if (global.currLevel < 4) createTrans1Room();
779   else if (global.currLevel == 4) createTrans1xRoom();
780   else if (global.currLevel < 8) createTrans2Room();
781   else if (global.currLevel == 8) createTrans2xRoom();
782   else if (global.currLevel < 12) createTrans3Room();
783   else if (global.currLevel == 12) createTrans3xRoom();
784   else if (global.currLevel < 16) createTrans4Room();
785   else if (global.currLevel == 16) createTrans4Room();
786   else createTrans1Room(); //???
788   setMenuTilesOnTop();
790   fixWallTiles();
791   addBackgroundGfxDetails();
792   //levBGImgName = 'bgCave';
793   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
795   blockWaterChecking = true;
796   fixLiquidTop();
797   cleanDeadTiles();
799   if (damselSaved > 0) {
800     // this is special "damsel ready to kiss you" object, not a heart
801     MakeMapObject(176+8, 176+8, 'oDamselKiss');
802     global.plife += damselSaved; // if player skipped transition cutscene
803     damselSaved = 0;
804   }
806   ImmediateDelete = olddel;
807   CollectGarbage(true); // destroy delayed objects too
809   if (dumpGridStats) objGrid.dumpStats();
811   playerExited = false; // just in case
812   playerExitDoor = none;
814   osdClear();
816   setupGhostTime();
817   //global.playMusic(lg.musicName);
821 void generateLevel () {
822   levelStartTime = time;
823   levelEndTime = time;
825   udjatAlarm = 0;
826   if (resetBMCOG) {
827     resetBMCOG = false;
828     global.genBlackMarket = false;
829   }
831   global.setMusicPitch(1.0);
832   stats.clearLevelTotals();
834   levelKind = LevelKind.Normal;
835   lg.generate();
836   //lg.dump();
838   resetRoomBounds();
840   lg.generateRooms();
841   //writeln("tw:", tilesWidth, "; th:", tilesHeight);
843   auto olddel = ImmediateDelete;
844   ImmediateDelete = false;
845   clearWholeLevel();
847   if (lg.finalBossLevel) {
848     blockWaterChecking = true;
849     createOlmecRoom();
850   }
852   // if transition cutscene was skipped...
853   global.plife += max(0, damselSaved); // if player skipped transition cutscene
854   damselSaved = 0;
856   // generate tiles
857   startRoomX = lg.startRoomX;
858   startRoomY = lg.startRoomY;
859   endRoomX = lg.endRoomX;
860   endRoomY = lg.endRoomY;
861   addBackgroundGfxDetails();
862   foreach (int y; 0..tilesHeight) {
863     foreach (int x; 0..tilesWidth) {
864       lg.genTileAt(x, y);
865     }
866   }
867   fixWallTiles();
869   levBGImgName = lg.bgImgName;
870   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
872   if (global.allowAngryShopkeepers) generateAngryShopkeepers();
874   lg.generateEntities();
876   // add box of flares to dark level
877   if (global.darkLevel && allEnters.length) {
878     auto enter = allEnters[0];
879     int x = enter.ix, y = enter.iy;
880          if (!isSolidAtPoint(x-16, y)) MakeMapObject(x-16+8, y+8, 'oFlareCrate');
881     else if (!isSolidAtPoint(x+16, y)) MakeMapObject(x+16+8, y+8, 'oFlareCrate');
882     else MakeMapObject(x+8, y+8, 'oFlareCrate');
883   }
885   //scrGenerateEntities();
886   //foreach (; 0..2) scrGenerateEntities();
888   writeln(objGrid.countObjects, " alive objects inserted");
889   writeln(countBackTiles, " background tiles inserted");
891   if (!player) FatalError("player pawn is not spawned");
893   if (lg.finalBossLevel) {
894     blockWaterChecking = true;
895   } else {
896     blockWaterChecking = false;
897   }
898   fixLiquidTop();
899   cleanDeadTiles();
901   ImmediateDelete = olddel;
902   CollectGarbage(true); // destroy delayed objects too
904   if (dumpGridStats) objGrid.dumpStats();
906   playerExited = false; // just in case
907   playerExitDoor = none;
909   levelMoneyStart = stats.money;
911   osdClear();
912   generateLevelMessages();
914   xmoney = 0;
915   collectCounter = 0;
917   if (lastMusicName != lg.musicName) {
918     global.playMusic(lg.musicName);
919     //writeln("!<", lastMusicName, ">:<", lg.musicName, ">");
920   } else {
921     //writeln("MM: ", global.config.nextLevelMusicMode);
922     switch (global.config.nextLevelMusicMode) {
923       case GameConfig::MusicMode.Silent: global.stopMusic(); break; // the thing that should not be
924       case GameConfig::MusicMode.Restart: global.restartMusic(); break;
925       case GameConfig::MusicMode.DontTouch:
926         if (global.config.transitionMusicMode == GameConfig::MusicMode.Silent) {
927           global.playMusic(lg.musicName);
928         }
929         break;
930     }
931   }
932   lastMusicName = lg.musicName;
933   //global.playMusic(lg.musicName);
935   setupGhostTime();
936   if (global.cityOfGold == 1 || global.genBlackMarket) resetBMCOG = true;
938   if (global.cityOfGold == 1) {
939     lg.mapSprite = 'sMapTemple';
940     lg.mapTitle = "City of Gold";
941   } else if (global.blackMarket) {
942     lg.mapSprite = 'sMapJungle';
943     lg.mapTitle = "Black Market";
944   }
948 // ////////////////////////////////////////////////////////////////////////// //
949 int currKeys, nextKeys;
950 int pressedKeysQ, releasedKeysQ;
951 int keysPressed, keysReleased = -1;
954 struct SavedKeyState {
955   int currKeys, nextKeys;
956   int pressedKeysQ, releasedKeysQ;
957   int keysPressed, keysReleased;
958   // for session
959   int roomSeed, otherSeed;
963 // for saving/replaying
964 final void keysSaveState (out SavedKeyState ks) {
965   ks.currKeys = currKeys;
966   ks.nextKeys = nextKeys;
967   ks.pressedKeysQ = pressedKeysQ;
968   ks.releasedKeysQ = releasedKeysQ;
969   ks.keysPressed = keysPressed;
970   ks.keysReleased = keysReleased;
973 // for saving/replaying
974 final void keysRestoreState (const ref SavedKeyState ks) {
975   currKeys = ks.currKeys;
976   nextKeys = ks.nextKeys;
977   pressedKeysQ = ks.pressedKeysQ;
978   releasedKeysQ = ks.releasedKeysQ;
979   keysPressed = ks.keysPressed;
980   keysReleased = ks.keysReleased;
984 final void keysNextFrame () {
985   currKeys = nextKeys;
989 final void clearKeys () {
990   currKeys = 0;
991   nextKeys = 0;
992   pressedKeysQ = 0;
993   releasedKeysQ = 0;
994   keysPressed = 0;
995   keysReleased = -1;
999 final void onKey (int code, bool down) {
1000   if (!code) return;
1001   if (down) {
1002     currKeys |= code;
1003     nextKeys |= code;
1004     if (keysReleased&code) {
1005       keysPressed |= code;
1006       keysReleased &= ~code;
1007       pressedKeysQ |= code;
1008     }
1009   } else {
1010     nextKeys &= ~code;
1011     if (keysPressed&code) {
1012       keysReleased |= code;
1013       keysPressed &= ~code;
1014       releasedKeysQ |= code;
1015     }
1016   }
1019 final bool isKeyDown (int code) {
1020   return !!(currKeys&code);
1023 final bool isKeyPressed (int code) {
1024   bool res = !!(pressedKeysQ&code);
1025   pressedKeysQ &= ~code;
1026   return res;
1029 final bool isKeyReleased (int code) {
1030   bool res = !!(releasedKeysQ&code);
1031   releasedKeysQ &= ~code;
1032   return res;
1036 final void clearKeysPressRelease () {
1037   keysPressed = default.keysPressed;
1038   keysReleased = default.keysReleased;
1039   pressedKeysQ = default.pressedKeysQ;
1040   releasedKeysQ = default.releasedKeysQ;
1041   currKeys = 0;
1042   nextKeys = 0;
1046 // ////////////////////////////////////////////////////////////////////////// //
1047 final void registerEnter (MapTile t) {
1048   if (!t) return;
1049   allEnters[$] = t;
1050   return;
1054 final void registerExit (MapTile t) {
1055   if (!t) return;
1056   allExits[$] = t;
1057   return;
1061 final bool isYAtEntranceRow (int py) {
1062   py /= 16;
1063   foreach (MapTile t; allEnters) if (t.iy == py) return true;
1064   return false;
1068 final int calcNearestEnterDist (int px, int py) {
1069   if (allEnters.length == 0) return int.max;
1070   int curdistsq = int.max;
1071   foreach (MapTile t; allEnters) {
1072     int xc = px-t.xCenter, yc = py-t.yCenter;
1073     int distsq = xc*xc+yc*yc;
1074     if (distsq < curdistsq) curdistsq = distsq;
1075   }
1076   return round(sqrt(curdistsq));
1080 final int calcNearestExitDist (int px, int py) {
1081   if (allExits.length == 0) return int.max;
1082   int curdistsq = int.max;
1083   foreach (MapTile t; allExits) {
1084     int xc = px-t.xCenter, yc = py-t.yCenter;
1085     int distsq = xc*xc+yc*yc;
1086     if (distsq < curdistsq) curdistsq = distsq;
1087   }
1088   return round(sqrt(curdistsq));
1092 // ////////////////////////////////////////////////////////////////////////// //
1093 final void clearForTransition () {
1094   auto olddel = ImmediateDelete;
1095   ImmediateDelete = false;
1096   clearWholeLevel();
1097   ImmediateDelete = olddel;
1098   CollectGarbage(true); // destroy delayed objects too
1099   global.darkLevel = false;
1103 // ////////////////////////////////////////////////////////////////////////// //
1104 final int countBackTiles () {
1105   int res = 0;
1106   for (MapBackTile bt = backtiles; bt; bt = bt.next) ++res;
1107   return res;
1111 final void clearWholeLevel () {
1112   allEnters.clear();
1113   allExits.clear();
1115   // don't kill objects the player is holding
1116   if (player) {
1117     if (player.pickedItem isa ItemBall) {
1118       player.pickedItem.instanceRemove();
1119       player.pickedItem = none;
1120     }
1121     if (player.pickedItem && player.pickedItem.grid) {
1122       player.pickedItem.grid.remove(player.pickedItem.gridId);
1123       writeln("secured pocket item '", GetClassName(player.pickedItem.Class), "'");
1124     }
1125     if (player.holdItem isa ItemBall) {
1126       player.removeBallAndChain(temp:true);
1127       if (player.holdItem) player.holdItem.instanceRemove();
1128       player.holdItem = none;
1129     }
1130     if (player.holdItem && player.holdItem.grid) {
1131       player.holdItem.grid.remove(player.holdItem.gridId);
1132       writeln("secured held item '", GetClassName(player.holdItem.Class), "'");
1133     }
1134     writeln("secured ball; mustBeChained=", player.mustBeChained, "; wasHoldingBall=", player.wasHoldingBall);
1135   }
1137   int count = objGrid.countObjects();
1138   if (dumpGridStats) { if (objGrid.getFirstObjectCID()) objGrid.dumpStats(); }
1139   objGrid.removeAllObjects(true); // and destroy
1140   if (count > 0) writeln(count, " objects destroyed");
1142   lastUsedObjectId = 0;
1143   accumTime = 0;
1144   //!time = 0;
1145   lastRenderTime = -1;
1146   liquidTileCount = 0;
1147   checkWater = false;
1149   while (backtiles) {
1150     MapBackTile t = backtiles;
1151     backtiles = t.next;
1152     delete t;
1153   }
1155   levBGImg = none;
1156   framesProcessedFromLastClear = 0;
1160 final void insertObject (MapEntity o) {
1161   if (!o) return;
1162   if (o.grid) FatalError("cannot put object into level twice");
1163   objGrid.insert(o);
1167 final void spawnPlayerAt (int x, int y) {
1168   // if we have no player, spawn new one
1169   // otherwise this just a level transition, so simply reposition him
1170   if (!player) {
1171     // don't add player to object list, as it has very separate processing anyway
1172     player = SpawnObject(PlayerPawn);
1173     player.global = global;
1174     player.level = self;
1175     if (!player.initialize()) {
1176       delete player;
1177       FatalError("something is wrong with player initialization");
1178       return;
1179     }
1180   }
1181   player.fltx = x;
1182   player.flty = y;
1183   player.saveInterpData();
1184   player.resurrect();
1185   if (player.mustBeChained || global.config.scumBallAndChain) {
1186     writeln("*** spawning ball and chain");
1187     player.spawnBallAndChain(levelStart:true);
1188   }
1189   playerExited = false;
1190   playerExitDoor = none;
1191   if (global.config.startWithKapala) global.hasKapala = true;
1192   centerViewAtPlayer();
1193   // reinsert player items into grid
1194   if (player.pickedItem) objGrid.insert(player.pickedItem);
1195   if (player.holdItem) objGrid.insert(player.holdItem);
1196   //writeln("player spawned; active=", player.active);
1197   player.scrSwitchToPocketItem(forceIfEmpty:false);
1201 final void teleportPlayerTo (int x, int y) {
1202   if (player) {
1203     player.fltx = x;
1204     player.flty = y;
1205     player.saveInterpData();
1206   }
1210 final void resurrectPlayer () {
1211   if (player) player.resurrect();
1212   playerExited = false;
1213   playerExitDoor = none;
1217 // ////////////////////////////////////////////////////////////////////////// //
1218 final void scrShake (int duration) {
1219   if (shakeLeft == 0) {
1220     shakeOfs.x = 0;
1221     shakeOfs.y = 0;
1222     shakeDir.x = 0;
1223     shakeDir.y = 0;
1224   }
1225   shakeLeft = max(shakeLeft, duration);
1230 // ////////////////////////////////////////////////////////////////////////// //
1231 enum SCAnger {
1232   TileDestroyed,
1233   ItemStolen, // including damsel, lol
1234   CrapsCheated,
1235   BombDropped,
1236   DamselWhipped,
1240 // make the nearest shopkeeper angry. RAWR!
1241 void scrShopkeeperAnger (SCAnger reason, optional int maxdist, optional MapEntity offender) {
1242   if (!offender) offender = player;
1243   auto shp = MonsterShopkeeper(findNearestEnemy(offender.ix, offender.iy, delegate bool (MapEnemy o) {
1244     auto sc = MonsterShopkeeper(o);
1245     if (!sc) return false;
1246     if (sc.dead || sc.angered) return false;
1247     return true;
1248   }, castClass:MonsterShopkeeper));
1250   if (shp) {
1251     if (specified_maxdist && offender.directionToEntityCenter(shp) > maxdist) return;
1252     if (!shp.dead && !shp.angered) {
1253       shp.status = MapObject::ATTACK;
1254       string msg;
1255            if (global.murderer) msg = "YOU'LL PAY FOR YOUR CRIMES!";
1256       else if (reason == SCAnger.ItemStolen) msg = "COME BACK HERE, THIEF!";
1257       else if (reason == SCAnger.TileDestroyed) msg = "DIE, YOU VANDAL!";
1258       else if (reason == SCAnger.BombDropped) msg = "TERRORIST!";
1259       else if (reason == SCAnger.DamselWhipped) msg = "HEY, ONLY I CAN DO THAT!";
1260       else if (reason == SCAnger.CrapsCheated) msg = "DIE, CHEATER!";
1261       else msg = "NOW I'M REALLY STEAMED!";
1262       if (msg) osdMessage(msg, -666);
1263       global.thiefLevel += (global.thiefLevel > 0 ? 3 : 2);
1264     }
1265   }
1269 final MapObject findCrapsPrize () {
1270   foreach (MapObject o; objGrid.allObjects(MapObject)) {
1271     if (!o.spectral && o.inDiceHouse) return o;
1272   }
1273   return none;
1277 // ////////////////////////////////////////////////////////////////////////// //
1278 // 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.
1279 // note: idols moved by monkeys will have false `stolenIdol`
1280 void scrTriggerIdolAltar (bool stolenIdol) {
1281   ObjTikiCurse res = none;
1282   int curdistsq = int.max;
1283   int px = player.xCenter, py = player.yCenter;
1284   foreach (MapObject o; objGrid.allObjects(MapObject)) {
1285     auto tcr = ObjTikiCurse(o);
1286     if (!tcr) continue;
1287     if (tcr.activated) continue;
1288     int xc = px-tcr.xCenter, yc = py-tcr.yCenter;
1289     int distsq = xc*xc+yc*yc;
1290     if (distsq < curdistsq) {
1291       res = tcr;
1292       curdistsq = distsq;
1293     }
1294   }
1295   if (res) res.activate(stolenIdol);
1299 // ////////////////////////////////////////////////////////////////////////// //
1300 void setupGhostTime () {
1301   musicFadeTimer = -1;
1302   ghostSpawned = false;
1304   // there is no ghost on the first level
1305   if (inWinCutscene || !isNormalLevel() || lg.finalBossLevel ||
1306       (!global.config.ghostAtFirstLevel && global.currLevel == 1))
1307   {
1308     ghostTimeLeft = -1;
1309     global.setMusicPitch(1.0);
1310     return;
1311   }
1313   if (global.config.scumGhost < 0) {
1314     // instant
1315     ghostTimeLeft = 1;
1316     osdMessage("A GHASTLY VOICE CALLS YOUR NAME!\nLET'S GET OUT OF HERE!", 6.66, urgent:true);
1317     return;
1318   }
1320   if (global.config.scumGhost == 0) {
1321     // never
1322     ghostTimeLeft = -1;
1323     return;
1324   }
1326   // randomizes time until ghost appears once time limit is reached
1327   // global.ghostRandom (true/false) if extra time is randomized - loaded from yasm.cfg. if false, use 30 second default
1328   // ghostTimeLeft (time in seconds * 1000) for currently generated level
1330   if (global.config.ghostRandom) {
1331     auto tMin = global.randOther(10, 30); // min(global.plife*10, 30);
1332     auto tMax = max(tMin+global.randOther(10, 30), min(global.config.scumGhost, 300)); // 5 minutes max
1333     auto tTime = global.randOther(tMin, tMax);
1334     if (tTime <= 0) tTime = round(tMax/2.0);
1335     ghostTimeLeft = tTime;
1336   } else {
1337     ghostTimeLeft = global.config.scumGhost; // 5 minutes max
1338   }
1340   ghostTimeLeft += max(0, global.config.ghostExtraTime);
1342   ghostTimeLeft *= 30; // seconds -> frames
1343   //global.ghostShowTime
1347 void spawnGhost () {
1348   addGhostSummoned();
1349   ghostSpawned = true;
1350   ghostTimeLeft = -1;
1352   int vwdt = (viewMax.x-viewMin.x);
1353   int vhgt = (viewMax.y-viewMin.y);
1355   int gx, gy;
1357   if (player.ix < viewMin.x+vwdt/2) {
1358     // player is in the left side
1359     gx = viewMin.x+vwdt/2+vwdt/4;
1360   } else {
1361     // player is in the right side
1362     gx = viewMin.x+vwdt/4;
1363   }
1365   if (player.iy < viewMin.y+vhgt/2) {
1366     // player is in the left side
1367     gy = viewMin.y+vhgt/2+vhgt/4;
1368   } else {
1369     // player is in the right side
1370     gy = viewMin.y+vhgt/4;
1371   }
1373   writeln("spawning ghost at tile (", gx/16, ",", gy/16, "); player is at tile (", player.ix/16, ",", player.iy/16, ")");
1375   MakeMapObject(gx, gy, 'oGhost');
1377   /*
1378     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);
1379     else instance_create(view_xview[0]-32, view_yview[0]+floor(view_hview[0]/2), oGhost);
1380     global.ghostExists = true;
1381   */
1385 void thinkFrameGameGhost () {
1386   if (player.dead) return;
1387   if (!isNormalLevel()) return; // just in case
1389   if (ghostTimeLeft < 0) {
1390     // turned off
1391     if (musicFadeTimer > 0) {
1392       musicFadeTimer = -1;
1393       global.setMusicPitch(1.0);
1394     }
1395     return;
1396   }
1398   if (musicFadeTimer >= 0) {
1399     ++musicFadeTimer;
1400     if (musicFadeTimer > 0 && musicFadeTimer <= 100*3) {
1401       float pitch = 1.0-(0.00226/3.0)*musicFadeTimer;
1402       //writeln("MFT: ", musicFadeTimer, "; pitch=", pitch);
1403       global.setMusicPitch(pitch);
1404     }
1405   }
1407   if (ghostTimeLeft == 0) {
1408     // she is already here!
1409     return;
1410   }
1412   // no ghost if we have a crown
1413   if (global.hasCrown) {
1414     ghostTimeLeft = -1;
1415     return;
1416   }
1418   // if she was already spawned, don't do it again
1419   if (ghostSpawned) {
1420     ghostTimeLeft = 0;
1421     return;
1422   }
1424   if (--ghostTimeLeft != 0) {
1425     // warning
1426     if (global.config.ghostExtraTime > 0) {
1427       if (ghostTimeLeft == global.config.ghostExtraTime*30) {
1428         osdMessage("A CHILL RUNS UP YOUR SPINE...\nLET'S GET OUT OF HERE!", 6.66, urgent:true);
1429       }
1430       if (musicFadeTimer < 0 && ghostTimeLeft <= global.config.ghostExtraTime*30) {
1431         musicFadeTimer = 0;
1432       }
1433     }
1434     return;
1435   }
1437   // spawn her
1438   if (player.isExitingSprite) {
1439     // no reason to spawn her, we're leaving
1440     ghostTimeLeft = -1;
1441     return;
1442   }
1444   spawnGhost();
1448 void thinkFrameGame () {
1449   thinkFrameGameGhost();
1450   // udjat eye blinking
1451   if (global.hasUdjatEye && player) {
1452     foreach (MapTile t; allExits) {
1453       if (t isa MapTileBlackMarketDoor) {
1454         auto dm = int(player.distanceToEntity(t));
1455         if (dm < 4) dm = 4;
1456         if (udjatAlarm < 1 || dm < udjatAlarm) udjatAlarm = dm;
1457       }
1458     }
1459   } else {
1460     global.udjatBlink = false;
1461     udjatAlarm = 0;
1462   }
1463   if (udjatAlarm > 0) {
1464     if (--udjatAlarm == 0) {
1465       global.udjatBlink = !global.udjatBlink;
1466       if (global.hasUdjatEye && player) {
1467         player.playSound(global.udjatBlink ? 'sndBlink1' : 'sndBlink2');
1468       }
1469     }
1470   }
1471   switch (levelKind) {
1472     case LevelKind.Stars: thinkFrameGameStars(); break;
1473     case LevelKind.Sun: thinkFrameGameSun(); break;
1474     case LevelKind.Moon: thinkFrameGameMoon(); break;
1475   }
1479 // ////////////////////////////////////////////////////////////////////////// //
1480 private final bool isWaterTileCB (MapTile t) {
1481   return (t && t.visible && t.water);
1485 private final bool isLavaTileCB (MapTile t) {
1486   return (t && t.visible && t.lava);
1490 // ////////////////////////////////////////////////////////////////////////// //
1491 const int GreatLakeStartTileY = 28;
1494 final void fillGreatLake () {
1495   if (global.lake == 1) {
1496     foreach (int y; GreatLakeStartTileY..tilesHeight) {
1497       foreach (int x; 0..tilesWidth) {
1498         auto t = checkTileAtPoint(x*16, y*16, delegate bool (MapTile t) {
1499           if (t.spectral || !t.visible || t.invisible || t.moveable) return false;
1500           return true;
1501         });
1502         if (!t) {
1503           t = MakeMapTile(x, y, 'oWaterSwim');
1504           if (!t) continue;
1505         }
1506         if (t.water) {
1507           t.setSprite(y == GreatLakeStartTileY ? 'sWaterTop' : 'sWater');
1508         } else if (t.lava) {
1509           t.setSprite(y == GreatLakeStartTileY ? 'sLavaTop' : 'sLava');
1510         }
1511       }
1512     }
1513   }
1517 // called once after level generation
1518 final void fixLiquidTop () {
1519   if (global.lake == 1) fillGreatLake();
1521   liquidTileCount = 0;
1522   foreach (MapTile t; objGrid.allObjects(MapTile)) {
1523     if (!t.water && !t.lava) continue;
1525     ++liquidTileCount;
1526     //writeln("fixing water tile(", GetClassName(t.Class), "):'", t.objName, "' (water=", t.water, "; lava=", t.lava, "); lqc=", liquidTileCount);
1528     //if (global.lake == 1) continue; // it is done in `fillGreatLake()`
1530     if (!checkTileAtPoint(t.ix+8, t.iy-8, (t.lava ? &isLavaTileCB : &isWaterTileCB))) {
1531       t.setSprite(t.lava ? 'sLavaTop' : 'sWaterTop');
1532     } else {
1533       // don't do this, it will destroy seaweed
1534       //t.setSprite(t.lava ? 'sLava' : 'sWater');
1535       auto spr = t.getSprite();
1536            if (!spr) t.setSprite(t.lava ? 'sLava' : 'sWater');
1537       else if (spr.Name == 'sLavaTop') t.setSprite('sLava');
1538       else if (spr.Name == 'sWaterTop') t.setSprite('sWater');
1539     }
1540   }
1541   //writeln("liquid tiles count: ", liquidTileCount);
1545 // ////////////////////////////////////////////////////////////////////////// //
1546 transient MapTile curWaterTile;
1547 transient bool curWaterTileCheckHitsLava;
1548 transient bool curWaterTileCheckHitsSolidOrWater; // only for `checkWaterOrSolidTilePartialCB`
1549 transient int curWaterTileLastHDir;
1550 transient ubyte[16, 16] curWaterOccupied;
1551 transient int curWaterOccupiedCount;
1552 transient int curWaterTileCheckX0, curWaterTileCheckY0;
1555 private final void clearCurWaterCheckState () {
1556   curWaterTileCheckHitsLava = false;
1557   curWaterOccupiedCount = 0;
1558   foreach (auto idx; 0..16*16) curWaterOccupied[idx] = 0;
1562 private final bool checkWaterOrSolidTileCB (MapTile t) {
1563   if (t == curWaterTile) return false;
1564   if (t.lava && curWaterTile.water) {
1565     curWaterTileCheckHitsLava = true;
1566     return true;
1567   }
1568   if (t.ix%16 != 0 || t.iy%16 != 0) {
1569     if (t.water || t.solid) {
1570       // fill occupied array
1571       //FIXME: optimize this
1572       if (curWaterOccupiedCount < 16*16) {
1573         foreach (auto dy; t.y0..t.y1+1) {
1574           foreach (auto dx; t.x0..t.x1+1) {
1575             int sx = dx-curWaterTileCheckX0;
1576             int sy = dy-curWaterTileCheckY0;
1577             if (sx >= 0 && sx <= 16 && sy >= 0 && sy <= 15 && !curWaterOccupied[sx, sy]) {
1578               curWaterOccupied[sx, sy] = 1;
1579               ++curWaterOccupiedCount;
1580             }
1581           }
1582         }
1583       }
1584     }
1585     return false; // need to check for lava
1586   }
1587   if (t.water || t.solid || t.lava) {
1588     curWaterOccupiedCount = 16*16;
1589     if (t.water && curWaterTile.lava) t.instanceRemove();
1590   }
1591   return false; // need to check for lava
1595 private final bool checkWaterOrSolidTilePartialCB (MapTile t) {
1596   if (t == curWaterTile) return false;
1597   if (t.lava && curWaterTile.water) {
1598     //writeln("!!!!!!!!");
1599     curWaterTileCheckHitsLava = true;
1600     return true;
1601   }
1602   if (t.water || t.solid || t.lava) {
1603     //writeln("*********");
1604     curWaterTileCheckHitsSolidOrWater = true;
1605     if (t.water && curWaterTile.lava) t.instanceRemove();
1606   }
1607   return false; // need to check for lava
1611 private final bool isFullyOccupiedAtTilePos (int tileX, int tileY) {
1612   clearCurWaterCheckState();
1613   curWaterTileCheckX0 = tileX*16;
1614   curWaterTileCheckY0 = tileY*16;
1615   checkTilesInRect(tileX*16, tileY*16, 16, 16, &checkWaterOrSolidTileCB);
1616   return (curWaterTileCheckHitsLava || curWaterOccupiedCount == 16*16);
1620 private final bool isAtLeastPartiallyOccupiedAtTilePos (int tileX, int tileY) {
1621   curWaterTileCheckHitsLava = false;
1622   curWaterTileCheckHitsSolidOrWater = false;
1623   checkTilesInRect(tileX*16, tileY*16, 16, 16, &checkWaterOrSolidTilePartialCB);
1624   return (curWaterTileCheckHitsSolidOrWater || curWaterTileCheckHitsLava);
1628 private final bool waterCanReachGroundHoleInDir (MapTile wtile, int dx) {
1629   if (dx == 0) return false; // just in case
1630   dx = sign(dx);
1631   int x = wtile.ix/16, y = wtile.iy/16;
1632   x += dx;
1633   while (x >= 0 && x < tilesWidth) {
1634     if (!isAtLeastPartiallyOccupiedAtTilePos(x, y+1)) return true;
1635     if (isAtLeastPartiallyOccupiedAtTilePos(x, y)) return false;
1636     x += dx;
1637   }
1638   return false;
1642 // returns `true` if this tile must be removed
1643 private final bool checkWaterFlow (MapTile wtile) {
1644   if (global.lake == 1) {
1645     if (wtile.iy >= GreatLakeStartTileY*16) return false; // lake tile, don't touch
1646     if (wtile.iy >= GreatLakeStartTileY*16-16) return true; // remove it, so it won't stack on a lake
1647   }
1649   if (wtile.ix%16 != 0 || wtile.iy%16 != 0) return true; // sanity check
1651   curWaterTile = wtile;
1652   curWaterTileLastHDir = 0; // never moved to the side
1654   bool wasMoved = false;
1656   for (;;) {
1657     int tileX = wtile.ix/16, tileY = wtile.iy/16;
1659     // out of level?
1660     if (tileY >= tilesHeight) return true;
1662     // check if we can fall down
1663     auto canFall = !isAtLeastPartiallyOccupiedAtTilePos(tileX, tileY+1);
1664     // disappear if can fall in lava
1665     if (wtile.water && curWaterTileCheckHitsLava) {
1666       //!writeln(wtile.objId, ": LAVA HIT DOWN");
1667       return true;
1668     }
1669     if (wasMoved) {
1670       // fake, so caller will not start removing tiles
1671       if (canFall) wtile.waterMovedDown = true;
1672       break;
1673     }
1674     // can move down?
1675     if (canFall) {
1676       // move down
1677       //!writeln(wtile.objId, ": GOING DOWN");
1678       curWaterTileLastHDir = 0;
1679       wtile.iy = wtile.iy+16;
1680       wasMoved = true;
1681       wtile.waterMovedDown = true;
1682       continue;
1683     }
1685     bool canMoveLeft = (curWaterTileLastHDir > 0 ? false : !isAtLeastPartiallyOccupiedAtTilePos(tileX-1, tileY));
1686     // disappear if near lava
1687     if (wtile.water && curWaterTileCheckHitsLava) {
1688       //!writeln(wtile.objId, ": LAVA HIT LEFT");
1689       return true;
1690     }
1692     bool canMoveRight = (curWaterTileLastHDir < 0 ? false : !isAtLeastPartiallyOccupiedAtTilePos(tileX+1, tileY));
1693     // disappear if near lava
1694     if (wtile.water && curWaterTileCheckHitsLava) {
1695       //!writeln(wtile.objId, ": LAVA HIT RIGHT");
1696       return true;
1697     }
1699     if (!canMoveLeft && !canMoveRight) {
1700       // do final checks
1701       //!if (wasMove) writeln(wtile.objId, ": NO MORE MOVES");
1702       break;
1703     }
1705     if (canMoveLeft && canMoveRight) {
1706       // choose random direction
1707       //!writeln(wtile.objId, ": CHOOSING RANDOM HDIR");
1708       // actually, choose direction that leads to hole in a ground
1709       if (waterCanReachGroundHoleInDir(wtile, -1)) {
1710         // can reach hole at the left side
1711         if (waterCanReachGroundHoleInDir(wtile, 1)) {
1712           // can reach hole at the right side, choose at random
1713           if (global.randOther(0, 1)) canMoveRight = false; else canMoveLeft = false;
1714         } else {
1715           // move left
1716           canMoveRight = false;
1717         }
1718       } else {
1719         // can't reach hole at the left side
1720         if (waterCanReachGroundHoleInDir(wtile, 1)) {
1721           // can reach hole at the right side, choose at random
1722           canMoveLeft = false;
1723         } else {
1724           // no holes at any side, choose at random
1725           if (global.randOther(0, 1)) canMoveRight = false; else canMoveLeft = false;
1726         }
1727       }
1728     }
1730     // move
1731     if (canMoveLeft) {
1732       if (canMoveRight) FatalError("WATERCHECK: WTF RIGHT");
1733       //!writeln(wtile.objId, ": MOVING LEFT (", curWaterTileLastHDir, ")");
1734       curWaterTileLastHDir = -1;
1735       wtile.ix = wtile.ix-16;
1736     } else if (canMoveRight) {
1737       if (canMoveLeft) FatalError("WATERCHECK: WTF LEFT");
1738       //!writeln(wtile.objId, ": MOVING RIGHT (", curWaterTileLastHDir, ")");
1739       curWaterTileLastHDir = 1;
1740       wtile.ix = wtile.ix+16;
1741     }
1742     wasMoved = true;
1743   }
1745   // remove seaweeds
1746   if (wasMoved) {
1747     checkWater = true;
1748     wtile.setSprite(wtile.lava ? 'sLava' : 'sWater');
1749     wtile.waterMoved = true;
1750     // if this tile was not moved down, check if it can move down on any next step
1751     if (!wtile.waterMovedDown) {
1752            if (waterCanReachGroundHoleInDir(wtile, -1)) wtile.waterMovedDown = true;
1753       else if (waterCanReachGroundHoleInDir(wtile, 1)) wtile.waterMovedDown = true;
1754     }
1755   }
1757   return false; // don't remove
1759   //if (!isWetTileAtPix(tileX*16+8, tileY*16-8)) wtile.setSprite(wtile.lava ? 'sLavaTop' : 'sWaterTop');
1763 transient array!MapTile waterTilesList;
1765 final bool sortWaterTilesByCoordsLess (MapTile a, MapTile b) {
1766   int dy = a.iy-b.iy;
1767   if (dy) return (dy < 0);
1768   return (a.ix < b.ix);
1771 transient int waterFlowPause = 0;
1772 transient bool debugWaterFlowPause = false;
1774 final void cleanDeadObjects () {
1775   // remove dead objects
1776   if (deadItemsHead) {
1777     auto olddel = ImmediateDelete;
1778     ImmediateDelete = false;
1779     do {
1780       auto it = deadItemsHead;
1781       deadItemsHead = it.deadItemsNext;
1782       if (it.grid) it.grid.remove(it.gridId);
1783       it.onDestroy();
1784       delete it;
1785     } while (deadItemsHead);
1786     ImmediateDelete = olddel;
1787     if (olddel) CollectGarbage(true); // destroy delayed objects too
1788   }
1791 final void cleanDeadTiles () {
1792   if (checkWater && /*global.lake == 1 ||*/ (!blockWaterChecking && liquidTileCount)) {
1793     if (global.lake == 1) fillGreatLake();
1794     if (waterFlowPause > 1) {
1795       --waterFlowPause;
1796       cleanDeadObjects();
1797       return;
1798     }
1799     if (debugWaterFlowPause) waterFlowPause = 4;
1800     //writeln("checking water");
1801     waterTilesList.clear();
1802     foreach (MapTile wtile; objGrid.allObjectsSafe(MapTile)) {
1803       if (wtile.water || wtile.lava) {
1804         // sanity check
1805         if (wtile.ix%16 == 0 && wtile.iy%16 == 0) {
1806           wtile.waterMoved = false;
1807           wtile.waterMovedDown = false;
1808           wtile.waterSlideOldX = wtile.ix;
1809           wtile.waterSlideOldY = wtile.iy;
1810           waterTilesList[$] = wtile;
1811         }
1812       }
1813     }
1814     checkWater = false;
1815     liquidTileCount = 0;
1816     waterTilesList.sort(&sortWaterTilesByCoordsLess);
1817     // do water flow
1818     bool wasAnyMove = false;
1819     bool wasAnyMoveDown = false;
1820     foreach (MapTile wtile; waterTilesList) {
1821       if (!wtile || !wtile.isInstanceAlive) continue;
1822       auto killIt = checkWaterFlow(wtile);
1823       if (killIt) {
1824         checkWater = true;
1825         wtile.smashMe();
1826         wtile.instanceRemove(); // just in case
1827       } else {
1828         wtile.saveInterpData();
1829         wtile.updateGrid();
1830         wasAnyMove = wasAnyMove || wtile.waterMoved;
1831         wasAnyMoveDown = wasAnyMoveDown || wtile.waterMovedDown;
1832         if (wtile.waterMoved && debugWaterFlowPause) wtile.waterSlideCounter = 4;
1833       }
1834     }
1835     // do water check
1836     liquidTileCount = 0;
1837     foreach (MapTile wtile; waterTilesList) {
1838       if (!wtile || !wtile.isInstanceAlive) continue;
1839       if (wasAnyMoveDown) {
1840         ++liquidTileCount;
1841         continue;
1842       }
1843       //checkWater = checkWater || wtile.waterMoved;
1844       curWaterTile = wtile;
1845       int tileX = wtile.ix/16, tileY = wtile.iy/16;
1846       // check if we are have no way to leak
1847       bool killIt = false;
1848       if (!isFullyOccupiedAtTilePos(tileX-1, tileY) || (wtile.water && curWaterTileCheckHitsLava)) {
1849         //writeln(" LEFT DEATH (", curWaterOccupiedCount, " : ", curWaterTileCheckHitsLava, ")");
1850         killIt = true;
1851       }
1852       if (!killIt && (!isFullyOccupiedAtTilePos(tileX+1, tileY) || (wtile.water && curWaterTileCheckHitsLava))) {
1853         //writeln(" RIGHT DEATH (", curWaterOccupiedCount, " : ", curWaterTileCheckHitsLava, ")");
1854         killIt = true;
1855       }
1856       if (!killIt && (!isFullyOccupiedAtTilePos(tileX, tileY+1) || (wtile.water && curWaterTileCheckHitsLava))) {
1857         //writeln(" DOWN DEATH (", curWaterOccupiedCount, " : ", curWaterTileCheckHitsLava, ")");
1858         killIt = true;
1859       }
1860       //killIt = false;
1861       if (killIt) {
1862         checkWater = true;
1863         wtile.smashMe();
1864         wtile.instanceRemove(); // just in case
1865       } else {
1866         ++liquidTileCount;
1867       }
1868     }
1869     if (wasAnyMove) checkWater = true;
1870     //writeln("water check: liquidTileCount=", liquidTileCount, "; checkWater=", checkWater, "; wasAnyMove=", wasAnyMove, "; wasAnyMoveDown=", wasAnyMoveDown);
1872     // fill empty spaces in lake with water
1873     fixLiquidTop();
1874   }
1876   cleanDeadObjects();
1880 // ////////////////////////////////////////////////////////////////////////// //
1881 private transient array!MapEntity postponedThinkers;
1882 private transient MapEntity thinkerHeld;
1883 private transient array!MapEntity activeThinkerList;
1886 final void doThinkActionsForObject (MapEntity o) {
1887        if (o.justSpawned) o.justSpawned = false;
1888   else if (o.imageSpeed > 0) o.nextAnimFrame();
1889   o.saveInterpData();
1890   o.thinkFrame();
1891   if (o.isInstanceAlive) {
1892     //o.updateGrid();
1893     o.processAlarms();
1894     if (o.isInstanceAlive) {
1895       if (o.whipTimer > 0) --o.whipTimer;
1896       o.updateGrid();
1897       auto obj = MapObject(o);
1898       if (!o.canLiveOutsideOfLevel && (!obj || !obj.heldBy) && o.isOutsideOfLevel()) {
1899         // oops, fallen out of level...
1900         o.onOutOfLevel();
1901       }
1902     }
1903   }
1907 // return `true` if thinker should be removed
1908 final void thinkOne (MapEntity o, optional bool doHeldObject, optional bool dontAddHeldObject) {
1909   if (!o) return;
1910   if (o == thinkerHeld && !doHeldObject) return; // skip it
1912   if (!o.active || !o.isInstanceAlive) return;
1914   auto obj = MapObject(o);
1916   if (obj && obj.heldBy == player) {
1917     // fix held item coords
1918     obj.fixHoldCoords();
1919     if (doHeldObject) {
1920       doThinkActionsForObject(o);
1921     } else {
1922       if (!dontAddHeldObject) {
1923         bool found = false;
1924         foreach (MapEntity e; postponedThinkers) if (e == o) { found = true; break; }
1925         if (!found) postponedThinkers[$] = o;
1926       }
1927     }
1928     return;
1929   }
1931   bool doThink = true;
1933   // collision with player weapon
1934   auto hh = PlayerWeapon(player.holdItem);
1935   bool doWeaponAction = false;
1936   if (hh) {
1937     if (hh.blockedBySolids && !global.config.killEnemiesThruWalls) {
1938       int xx = player.ix+(player.dir == MapObject::Dir.Left ? -16 : 16);
1939       //doWeaponAction = !isSolidAtPoint(xx, player.iy);
1940       doWeaponAction = !isSolidAtPoint(xx, hh.yCenter);
1941       /*
1942       int dh = max(1, hh.height-2);
1943       doWeaponAction = !checkTilesInRect(player.ix, player.iy);
1944       */
1945     } else {
1946       doWeaponAction = true;
1947     }
1948   }
1950   if (obj && doWeaponAction && hh && (o.whipTimer <= 0 || hh.ignoreWhipTimer) && hh.collidesWithObject(obj)) {
1951     //writeln("WEAPONED!");
1952     bool dontChangeWhipTimer = hh.dontChangeWhipTimer;
1953     if (!o.onTouchedByPlayerWeapon(player, hh)) {
1954       if (o.isInstanceAlive) hh.onCollisionWithObject(obj);
1955     }
1956     if (!dontChangeWhipTimer) o.whipTimer = o.whipTimerValue; //HACK
1957     doThink = o.isInstanceAlive;
1958   }
1960   if (doThink && o.isInstanceAlive) {
1961     doThinkActionsForObject(o);
1962     doThink = o.isInstanceAlive;
1963   }
1965   // collision with player
1966   if (doThink && obj && o.collidesWith(player)) {
1967     if (!player.onObjectTouched(obj) && o.isInstanceAlive) {
1968       doThink = !o.onTouchedByPlayer(player);
1969       o.updateGrid();
1970     }
1971   }
1975 final void processThinkers (float timeDelta) {
1976   if (timeDelta <= 0) return;
1977   if (gamePaused) {
1978     ++pausedTime;
1979     if (onBeforeFrame) onBeforeFrame(false);
1980     if (onAfterFrame) onAfterFrame(false);
1981     keysNextFrame();
1982     return;
1983   } else {
1984     pausedTime = 0;
1985   }
1986   accumTime += timeDelta;
1987   bool wasFrame = false;
1988   // block GC
1989   auto olddel = ImmediateDelete;
1990   ImmediateDelete = false;
1991   while (accumTime >= FrameTime) {
1992     postponedThinkers.clear();
1993     thinkerHeld = none;
1994     accumTime -= FrameTime;
1995     if (onBeforeFrame) onBeforeFrame(accumTime >= FrameTime);
1996     // shake
1997     if (shakeLeft > 0) {
1998       --shakeLeft;
1999       if (!shakeDir.x) shakeDir.x = (global.randOther(0, 1) ? -3 : 3);
2000       if (!shakeDir.y) shakeDir.y = (global.randOther(0, 1) ? -3 : 3);
2001       shakeOfs.x = shakeDir.x;
2002       shakeOfs.y = shakeDir.y;
2003       int sgnc = global.randOther(1, 3);
2004       if (sgnc&0x01) shakeDir.x = -shakeDir.x;
2005       if (sgnc&0x02) shakeDir.y = -shakeDir.y;
2006     } else {
2007       shakeOfs.x = 0;
2008       shakeOfs.y = 0;
2009       shakeDir.x = 0;
2010       shakeDir.y = 0;
2011     }
2012     // advance time
2013     time += 1;
2014     // we don't want the time to grow too large
2015     if (time < 0) { time = 0; lastRenderTime = -1; }
2016     // game-global events
2017     thinkFrameGame();
2018     // frame thinkers: player
2019     if (player && !disablePlayerThink) {
2020       // time limit
2021       if (!player.dead && isNormalLevel() &&
2022           (maxPlayingTime < 0 ||
2023            (maxPlayingTime > 0 && maxPlayingTime*30 <= time &&
2024             time%30 == 0 && global.randOther(1, 100) <= 20)))
2025       {
2026         MakeMapObject(player.ix, player.iy, 'oExplosion');
2027       }
2028       //HACK: check for stolen items
2029       auto item = MapItem(player.holdItem);
2030       if (item) item.onCheckItemStolen(player);
2031       item = MapItem(player.pickedItem);
2032       if (item) item.onCheckItemStolen(player);
2033       // normal thinking
2034       doThinkActionsForObject(player);
2035     }
2036     // frame thinkers: held object
2037     thinkerHeld = player.holdItem;
2038     if (thinkerHeld && thinkerHeld.isInstanceAlive) {
2039       thinkOne(thinkerHeld, doHeldObject:true);
2040       if (!thinkerHeld.isInstanceAlive) {
2041         if (player.holdItem == thinkerHeld) player.holdItem = none;
2042         thinkerHeld.grid.remove(thinkerHeld.gridId);
2043         /* later
2044         thinkerHeld.onDestroy();
2045         delete thinkerHeld;
2046         */
2047       }
2048     }
2049     // frame thinkers: objects
2050     activeThinkerList.clear();
2051     auto grid = objGrid;
2052     // collect active objects
2053     if (global.config.useFrozenRegion) {
2054       foreach (MapEntity e; grid.inRectPix(viewStart.x/global.scale-64, viewStart.y/global.scale-64, 320+64*2, 240+64*2, precise:false)) {
2055         if (e.active) activeThinkerList[$] = e;
2056       }
2057     } else {
2058       // no frozen area
2059       foreach (MapEntity e; grid.allObjects()) {
2060         if (e.active) activeThinkerList[$] = e;
2061       }
2062     }
2063     // process active objects
2064     //writeln("thinkers: ", activeThinkerList.length);
2065     foreach (MapEntity o; activeThinkerList) {
2066       if (!o) continue;
2067       thinkOne(o, doHeldObject:false);
2068       if (!o.isInstanceAlive) {
2069         //writeln("dead thinker: '", o.objType, "'");
2070         if (o.grid) o.grid.remove(o.gridId);
2071         auto obj = MapObject(o);
2072         if (obj && obj.heldBy) obj.heldBy.holdItem = none;
2073         /* later
2074         o.onDestroy();
2075         delete o;
2076         */
2077       }
2078     }
2079     // postponed thinkers
2080     foreach (MapEntity o; postponedThinkers) {
2081       if (!o) continue;
2082       thinkOne(o, doHeldObject:true, dontAddHeldObject:true);
2083       if (!o.isInstanceAlive) {
2084         //writeln("dead pp-thinker: '", o.objType, "'");
2085         /* later
2086         o.onDestroy();
2087         delete o;
2088         */
2089       }
2090     }
2091     postponedThinkers.clear();
2092     thinkerHeld = none;
2093     // clean dead things
2094     cleanDeadTiles();
2095     // fix held item coords
2096     if (player && player.holdItem) {
2097       if (player.holdItem.isInstanceAlive) {
2098         player.holdItem.fixHoldCoords();
2099       } else {
2100         player.holdItem = none;
2101       }
2102     }
2103     // money counter
2104     if (collectCounter == 0) {
2105       xmoney = max(0, xmoney-100);
2106     } else {
2107       --collectCounter;
2108     }
2109     // other things
2110     if (player) {
2111       if (!player.dead) stats.oneMoreFramePlayed();
2112       SoundSystem.ListenerOrigin = vector(player.xCenter, player.yCenter, -1);
2113       //writeln("plrpos=(", player.xCenter, ",", player.yCenter, "); lo=", SoundSystem.ListenerOrigin);
2114     }
2115     if (onAfterFrame) onAfterFrame(accumTime >= FrameTime);
2116     ++framesProcessedFromLastClear;
2117     keysNextFrame();
2118     wasFrame = true;
2119     if (!player.visible && player.holdItem) player.holdItem.visible = false;
2120     if (winCutsceneSwitchToNext) {
2121       winCutsceneSwitchToNext = false;
2122       switch (++inWinCutscene) {
2123         case 2: startWinCutsceneVolcano(); break;
2124         case 3: default: startWinCutsceneWinFall(); break;
2125       }
2126       break;
2127     }
2128     if (playerExited) break;
2129   }
2130   ImmediateDelete = olddel;
2131   if (playerExited) {
2132     playerExited = false;
2133     onLevelExited();
2134     centerViewAtPlayer();
2135   }
2136   if (wasFrame) {
2137     // if we were processed at least one frame, collect garbage
2138     //keysNextFrame();
2139     CollectGarbage(true); // destroy delayed objects too
2140   }
2141   if (accumTime > 0 && onInterFrame) onInterFrame(accumTime/FrameTime);
2145 // ////////////////////////////////////////////////////////////////////////// //
2146 final void tileXY2roomXY (int tileX, int tileY, out int roomX, out int roomY) {
2147   roomX = (tileX-1)/RoomGen::Width;
2148   roomY = (tileY-1)/RoomGen::Height;
2152 final bool isInShop (int tileX, int tileY) {
2153   if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
2154     auto n = roomType[tileX, tileY];
2155     if (n == 4 || n == 5) return true;
2156     return !!checkTilesInRect(tileX*16, tileY*16, 16, 16, delegate bool (MapTile t) { return t.shopWall; });
2157     //k8: we don't have this
2158     //if (t && t.objType == 'oShop') return true;
2159   }
2160   return false;
2164 // ////////////////////////////////////////////////////////////////////////// //
2165 override void Destroy () {
2166   clearWholeLevel();
2167   delete tempSolidTile;
2168   ::Destroy();
2172 // ////////////////////////////////////////////////////////////////////////// //
2173 // WARNING! delegate should not create/delete objects!
2174 final MapObject findNearestObject (int px, int py, scope bool delegate (MapObject o) dg, optional class!MapObject castClass) {
2175   MapObject res = none;
2176   if (!castClass) castClass = MapObject;
2177   int curdistsq = int.max;
2178   foreach (MapObject o; objGrid.allObjects(MapObject)) {
2179     if (o.spectral) continue;
2180     if (!dg(o)) continue;
2181     int xc = px-o.xCenter, yc = py-o.yCenter;
2182     int distsq = xc*xc+yc*yc;
2183     if (distsq < curdistsq) {
2184       res = o;
2185       curdistsq = distsq;
2186     }
2187   }
2188   return res;
2192 // WARNING! delegate should not create/delete objects!
2193 final MapObject findNearestEnemy (int px, int py, optional scope bool delegate (MapEnemy o) dg, optional class!MapEnemy castClass) {
2194   if (!castClass) castClass = MapEnemy;
2195   if (castClass !isa MapEnemy) return none;
2196   MapObject res = none;
2197   int curdistsq = int.max;
2198   foreach (MapEnemy o; objGrid.allObjects(castClass)) {
2199     //k8: i added `dead` check
2200     if (o.spectral || o.dead) continue;
2201     if (dg) {
2202       if (!dg(o)) continue;
2203     }
2204     int xc = px-o.xCenter, yc = py-o.yCenter;
2205     int distsq = xc*xc+yc*yc;
2206     if (distsq < curdistsq) {
2207       res = o;
2208       curdistsq = distsq;
2209     }
2210   }
2211   return res;
2215 final MonsterShopkeeper findNearestCalmShopkeeper (int px, int py) {
2216   auto obj = MonsterShopkeeper(findNearestEnemy(px, py, delegate bool (MapEnemy o) {
2217     auto sk = MonsterShopkeeper(o);
2218     if (sk && !sk.angered) return true;
2219     return false;
2220   }, castClass:MonsterShopkeeper));
2221   return obj;
2225 final MonsterShopkeeper hasAliveShopkeepers (optional bool skipAngry) {
2226   foreach (MonsterShopkeeper sc; objGrid.allObjects(MonsterShopkeeper)) {
2227     if (sc.spectral || sc.dead) continue;
2228     if (skipAngry && (sc.angered || sc.outlaw)) continue;
2229     return sc;
2230   }
2231   return none;
2235 // WARNING! delegate should not create/delete objects!
2236 final int calcNearestEnemyDist (int px, int py, optional scope bool delegate (MapEnemy o) dg, optional class!MapEnemy castClass) {
2237   auto e = findNearestEnemy(px, py, dg!optional, castClass!optional);
2238   if (!e) return int.max;
2239   int xc = px-e.xCenter, yc = py-e.yCenter;
2240   return round(sqrt(xc*xc+yc*yc));
2244 // WARNING! delegate should not create/delete objects!
2245 final int calcNearestObjectDist (int px, int py, optional scope bool delegate (MapObject o) dg, optional class!MapObject castClass) {
2246   auto e = findNearestObject(px, py, dg!optional, castClass!optional);
2247   if (!e) return int.max;
2248   int xc = px-e.xCenter, yc = py-e.yCenter;
2249   return round(sqrt(xc*xc+yc*yc));
2253 // WARNING! delegate should not create/delete objects!
2254 final MapTile findNearestMoveableSolid (int px, int py, optional scope bool delegate (MapTile t) dg) {
2255   MapTile res = none;
2256   int curdistsq = int.max;
2257   foreach (MapTile t; objGrid.allObjects(MapTile)) {
2258     if (t.spectral) continue;
2259     if (dg) {
2260       if (!dg(t)) continue;
2261     } else {
2262       if (!t.solid || !t.moveable) continue;
2263     }
2264     int xc = px-t.xCenter, yc = py-t.yCenter;
2265     int distsq = xc*xc+yc*yc;
2266     if (distsq < curdistsq) {
2267       res = t;
2268       curdistsq = distsq;
2269     }
2270   }
2271   return res;
2275 // WARNING! delegate should not create/delete objects!
2276 final MapTile findNearestTile (int px, int py, optional scope bool delegate (MapTile t) dg) {
2277   if (!dg) return none;
2278   MapTile res = none;
2279   int curdistsq = int.max;
2281   //FIXME: make this faster!
2282   foreach (MapTile t; objGrid.allObjects(MapTile)) {
2283     if (t.spectral) continue;
2284     int xc = px-t.xCenter, yc = py-t.yCenter;
2285     int distsq = xc*xc+yc*yc;
2286     if (distsq < curdistsq && dg(t)) {
2287       res = t;
2288       curdistsq = distsq;
2289     }
2290   }
2292   return res;
2296 // ////////////////////////////////////////////////////////////////////////// //
2297 final bool cbIsObjectWeb (MapObject o) { return (o isa ItemWeb); }
2298 final bool cbIsObjectBlob (MapObject o) { return (o isa EnemyBlob); }
2299 final bool cbIsObjectArrow (MapObject o) { return (o isa ItemProjectileArrow); }
2300 final bool cbIsObjectEnemy (MapObject o) { return (/*o != self &&*/ o isa MapEnemy); }
2302 final bool cbIsObjectTreasure (MapObject o) { return (o.isTreasure); }
2304 final bool cbIsItemObject (MapObject o) { return (o isa MapItem); }
2306 final bool cbIsItemOrEnemyObject (MapObject o) { return (o isa MapItem || o isa MapEnemy); }
2309 final MapObject isObjectAtTile (int tileX, int tileY, optional scope bool delegate (MapObject o) dg, optional bool precise) {
2310   if (!specified_precise) precise = true;
2311   tileX *= 16;
2312   tileY *= 16;
2313   foreach (MapObject o; objGrid.inRectPix(tileX, tileY, 16, 16, precise:precise, castClass:MapObject)) {
2314     if (o.spectral) continue;
2315     if (dg) {
2316       if (dg(o)) return o;
2317     } else {
2318       return o;
2319     }
2320   }
2321   return none;
2325 final MapObject isObjectAtTilePix (int x, int y, optional scope bool delegate (MapObject o) dg) {
2326   return isObjectAtTile(x/16, y/16, dg!optional);
2330 final MapObject isObjectAtPoint (int xpos, int ypos, optional scope bool delegate (MapObject o) dg, optional bool precise, optional class!MapObject castClass) {
2331   if (!specified_precise) precise = true;
2332   if (!castClass) castClass = MapObject;
2333   foreach (MapObject o; objGrid.inCellPix(xpos, ypos, precise:precise, castClass:castClass)) {
2334     if (o.spectral) continue;
2335     if (dg) {
2336       if (dg(o)) return o;
2337     } else {
2338       if (o isa MapEnemy) return o;
2339     }
2340   }
2341   return none;
2345 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) {
2346   if (w < 1 || h < 1) return none;
2347   if (!castClass) castClass = MapObject;
2348   if (w == 1 && h == 1) return isObjectAtPoint(xpos, ypos, dg!optional, precise!optional, castClass);
2349   if (!specified_precise) precise = true;
2350   foreach (MapObject o; objGrid.inRectPix(xpos, ypos, w, h, precise:precise, castClass:castClass)) {
2351     if (o.spectral) continue;
2352     if (dg) {
2353       if (dg(o)) return o;
2354     } else {
2355       if (o isa MapEnemy) return o;
2356     }
2357   }
2358   return none;
2362 final MapObject forEachObject (scope bool delegate (MapObject o) dg, optional bool allowSpectrals, optional class!MapObject castClass) {
2363   if (!dg) return none;
2364   if (!castClass) castClass = MapObject;
2365   foreach (MapObject o; objGrid.allObjectsSafe(castClass)) {
2366     if (!allowSpectrals && o.spectral) continue;
2367     if (dg(o)) return o;
2368   }
2369   return none;
2373 final MapObject forEachObjectAtPoint (int xpos, int ypos, scope bool delegate (MapObject o) dg, optional bool precise) {
2374   if (!dg) return none;
2375   if (!specified_precise) precise = true;
2376   foreach (MapObject o; objGrid.inCellPix(xpos, ypos, precise:precise, castClass:MapObject)) {
2377     if (o.spectral) continue;
2378     if (dg(o)) return o;
2379   }
2380   return none;
2384 final MapObject forEachObjectInRect (int xpos, int ypos, int w, int h, scope bool delegate (MapObject o) dg, optional bool precise) {
2385   if (!dg || w < 1 || h < 1) return none;
2386   if (w == 1 && h == 1) return forEachObjectAtPoint(xpos, ypos, dg!optional, precise!optional);
2387   if (!specified_precise) precise = true;
2388   foreach (MapObject o; objGrid.inRectPix(xpos, ypos, w, h, precise:precise, castClass:MapObject)) {
2389     if (o.spectral) continue;
2390     if (dg(o)) return o;
2391   }
2392   return none;
2396 final MapEntity forEachEntityInRect (int xpos, int ypos, int w, int h, scope bool delegate (MapEntity o) dg, optional bool precise, optional class!MapEntity castClass) {
2397   if (!dg || w < 1 || h < 1) return none;
2398   if (!castClass) castClass = MapEntity;
2399   if (!specified_precise) precise = true;
2400   foreach (MapEntity e; objGrid.inRectPix(xpos, ypos, w, h, precise:precise, castClass:castClass)) {
2401     if (e.spectral) continue;
2402     if (dg(e)) return e;
2403   }
2404   return none;
2408 private final bool cbIsRopeTile (MapTile t) { return (t isa MapTileRope); }
2410 final MapTile isRopeAtPoint (int px, int py) {
2411   return checkTileAtPoint(px, py, &cbIsRopeTile);
2415 //FIXME!
2416 final MapTile isWaterSwimAtPoint (int px, int py) {
2417   return isWaterAtPoint(px, py);
2421 // ////////////////////////////////////////////////////////////////////////// //
2422 private array!MapEntity tmpEntityList;
2424 private final bool cbCollectEntitiesWithMask (MapEntity t) {
2425   if (!t.visible || t.spectral) return false;
2426   tmpEntityList[$] = t;
2427   return false;
2431 final void touchEntitiesWithMask (int x, int y, SpriteFrame frm, scope bool delegate (MapEntity t) dg, optional class!MapEntity castClass) {
2432   if (!frm || frm.bw < 1 || frm.bh < 1 || !dg) return;
2433   if (frm.isEmptyPixelMask) return;
2434   if (!castClass) castClass = MapEntity;
2435   // collect tiles
2436   if (tmpEntityList.length) tmpEntityList.clear();
2437   if (player isa castClass && player.isRectCollisionFrame(frm, x, y)) tmpEntityList[$] = player;
2438   forEachEntityInRect(x+frm.bx, y+frm.by, frm.bw, frm.bh, &cbCollectEntitiesWithMask, castClass:castClass);
2439   foreach (MapEntity e; tmpEntityList) {
2440     if (!e || !e.isInstanceAlive || !e.visible || e.spectral) continue;
2441     if (e.isRectCollisionFrame(frm, x, y)) {
2442       if (dg(e)) break;
2443     }
2444   }
2448 // ////////////////////////////////////////////////////////////////////////// //
2449 final bool cbCollisionAnySolid (MapTile t) { return t.solid; }
2450 final bool cbCollisionAnyMoveableSolid (MapTile t) { return t.solid && t.moveable; }
2451 final bool cbCollisionLiquid (MapTile t) { return (t.water || t.lava); }
2452 final bool cbCollisionAnySolidOrLiquid (MapTile t) { return (t.solid || t.water || t.lava); }
2453 final bool cbCollisionLadder (MapTile t) { return t.ladder; }
2454 final bool cbCollisionLadderTop (MapTile t) { return t.laddertop; }
2455 final bool cbCollisionAnyLadder (MapTile t) { return (t.ladder || t.laddertop); }
2456 final bool cbCollisionAnySolidIce (MapTile t) { return (t.solid && t.ice); }
2457 final bool cbCollisionWater (MapTile t) { return t.water; }
2458 final bool cbCollisionLava (MapTile t) { return t.lava; }
2459 final bool cbCollisionAnyTree (MapTile t) { return t.tree; }
2460 final bool cbCollisionPlatform (MapTile t) { return t.platform; }
2461 final bool cbCollisionLadderOrPlatform (MapTile t) { return (t.ladder || t.platform); }
2462 //final bool cbCollisionAnyHangeable (MapTile t) { return t.hangeable; } // no need to check for solids here, it is done in MapTile initializer
2463 final bool cbCollisionSpringTrap (MapTile t) { return t.springtrap; }
2464 final bool cbCollisionSpikes (MapTile t) { return t.spikes; }
2465 final bool cbCollisionExitTile (MapTile t) { return t.isExitActive(); }
2467 final bool cbCollisionForWhoa (MapTile t) { return (t.solid || t.laddertop); }
2469 //final bool cbCollisionSacAltar (MapTile t) { return (t.objName == 'oSacAltarLeft'); } // only left part, yeah
2470 final bool cbCollisionSacAltar (MapTile t) { return t.sacrificingAltar; }
2473 // ////////////////////////////////////////////////////////////////////////// //
2474 transient MapTileTemp tempSolidTile;
2476 private final MapTileTemp makeWalkeableSolidTile (MapObject o) {
2477   if (!tempSolidTile) {
2478     tempSolidTile = SpawnObject(MapTileTemp);
2479   } else if (!tempSolidTile.isInstanceAlive) {
2480     delete tempSolidTile;
2481     tempSolidTile = SpawnObject(MapTileTemp);
2482   }
2483   // setup data
2484   tempSolidTile.level = self;
2485   tempSolidTile.global = global;
2486   tempSolidTile.solid = true;
2487   tempSolidTile.objName = MapTileTemp.default.objName;
2488   tempSolidTile.objType = MapTileTemp.default.objType;
2489   tempSolidTile.e = o;
2490   tempSolidTile.fltx = o.fltx;
2491   tempSolidTile.flty = o.flty;
2492   return tempSolidTile;
2496 final MapTile checkTilesInRect (int x0, int y0, const int w, const int h,
2497                                 optional scope bool delegate (MapTile dg) dg, optional bool precise,
2498                                 optional class!MapTile castClass)
2500   if (w < 1 || h < 1) return none;
2501   if (w == 1 && h == 1) return checkTileAtPoint(x0, y0, dg!optional);
2502   int x1 = x0+w-1, y1 = y0+h-1;
2503   if (x0 >= tilesWidth*16 || y0 >= tilesHeight*16 || x1 < 0 || y1 < 0) return none;
2504   if (!specified_precise) precise = true;
2505   if (!castClass) castClass = MapTile;
2506   if (!dg) dg = &cbCollisionAnySolid;
2508   // check walkable solid objects too
2509   foreach (MapEntity e; objGrid.inRectPix(x0, y0, w, h, precise:precise, castClass:castClass)) {
2510     if (e.spectral || !e.visible) continue;
2511     auto t = MapTile(e);
2512     if (t) {
2513       if (dg(t)) return t;
2514       continue;
2515     }
2516     auto o = MapObject(e);
2517     if (o && o.walkableSolid) {
2518       t = makeWalkeableSolidTile(o);
2519       if (dg(t)) return t;
2520       continue;
2521     }
2522   }
2524   return none;
2528 final MapTile checkTileAtPoint (int x0, int y0, optional scope bool delegate (MapTile dg) dg, optional bool precise, optional class!MapTile castClass) {
2529   if (x0 < 0 || y0 < 0 || x0 >= tilesWidth*16 || y0 >= tilesHeight*16) return none;
2530   if (!specified_precise) precise = true;
2531   if (!castClass) castClass = MapTile;
2532   if (!dg) dg = &cbCollisionAnySolid;
2534   // check walkable solid objects
2535   foreach (MapEntity e; objGrid.inCellPix(x0, y0, precise:precise, castClass:castClass)) {
2536     if (e.spectral || !e.visible) continue;
2537     auto t = MapTile(e);
2538     if (t) {
2539       if (dg(t)) return t;
2540       continue;
2541     }
2542     auto o = MapObject(e);
2543     if (o && o.walkableSolid) {
2544       t = makeWalkeableSolidTile(o);
2545       if (dg(t)) return t;
2546       continue;
2547     }
2548   }
2550   return none;
2554 // ////////////////////////////////////////////////////////////////////////// //
2555 final MapTile isSolidAtPoint (int px, int py) { return checkTileAtPoint(px, py); }
2556 final MapTile isSolidInRect (int rx, int ry, int rw, int rh) { return checkTilesInRect(rx, ry, rw, rh); }
2557 final MapTile isLadderAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadder); }
2558 final MapTile isLadderOrPlatformAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadderOrPlatform); }
2559 final MapTile isLadderTopAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadderTop); }
2560 final MapTile isAnyLadderAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyLadder); }
2561 final MapTile isWaterAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionWater); }
2562 final MapTile isLavaAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLava); }
2563 final MapTile isLiquidAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLiquid); }
2564 final MapTile isIceAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnySolidIce); }
2565 final MapTile isTreeAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyTree); }
2566 //final MapTile isHangeableAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyHangeable); }
2569 // ////////////////////////////////////////////////////////////////////////// //
2570 final void setTileTypeAt (int tileX, int tileY, LevelGen::RType rt) {
2571   if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) roomType[tileX, tileY] = rt;
2575 //FIXME: make this faster
2576 transient float gtagX, gtagY;
2578 // only non-moveables and non-specials
2579 final MapTile getTileAtGrid (int tileX, int tileY) {
2580   gtagX = tileX*16;
2581   gtagY = tileY*16;
2582   return checkTileAtPoint(tileX*16, tileY*16, delegate bool (MapTile t) {
2583     if (t.spectral || t.moveable || t.toSpecialGrid || !t.visible) return false;
2584     if (t.fltx != gtagX || t.flty != gtagY) return false; // emulate grid
2585     if (t.width != 16 || t.height != 16) return false;
2586     return true;
2587   }, precise:false);
2588   //return (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight ? tiles[tileX, tileY] : none);
2592 final MapTile getTileAtGridAny (int tileX, int tileY) {
2593   gtagX = tileX*16;
2594   gtagY = tileY*16;
2595   return checkTileAtPoint(tileX*16, tileY*16, delegate bool (MapTile t) {
2596     if (t.spectral /*|| t.moveable*/ || !t.visible) return false;
2597     if (t.fltx != gtagX || t.flty != gtagY) return false; // emulate grid
2598     if (t.width != 16 || t.height != 16) return false;
2599     return true;
2600   }, precise:false);
2601   //return (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight ? tiles[tileX, tileY] : none);
2605 final bool isNamedTileAt (int tileX, int tileY, name atypename) {
2606   if (!atypename) return false;
2607   auto t = getTileAtGridAny(tileX, tileY);
2608   return (t && t.objName == atypename);
2612 final void setTileAtGrid (int tileX, int tileY, MapTile tile) {
2613   if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
2614     if (tile) {
2615       tile.fltx = tileX*16;
2616       tile.flty = tileY*16;
2617       if (!tile.dontReplaceOthers) {
2618         auto osp = tile.spectral;
2619         tile.spectral = true;
2620         auto t = getTileAtGridAny(tileX, tileY);
2621         tile.spectral = osp;
2622         if (t && !t.immuneToReplacement) {
2623           writeln("REPLACING tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
2624           writeln("      NEW tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (tile.toSpecialGrid ? "special " : ""), "grid, pos=(", tile.fltx/16.0, ",", tile.flty/16.0, ")");
2625           t.instanceRemove();
2626         }
2627       }
2628       insertObject(tile);
2629     } else {
2630       auto t = getTileAtGridAny(tileX, tileY);
2631       if (t && !t.immuneToReplacement) {
2632         writeln("REMOVING tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
2633         t.instanceRemove();
2634       }
2635     }
2636   }
2640 // ////////////////////////////////////////////////////////////////////////// //
2641 // return `true` from delegate to stop
2642 MapTile forEachSolidTileOnGrid (scope bool delegate (int tileX, int tileY, MapTile t) dg) {
2643   if (!dg) return none;
2644   foreach (MapTile t; objGrid.allObjectsSafe(MapTile)) {
2645     if (t.spectral || !t.solid || !t.visible) continue;
2646     if (t.ix%16 != 0 || t.iy%16 != 0) continue; // emulate grid
2647     if (t.width != 16 || t.height != 16) continue;
2648     if (dg(t.ix/16, t.iy/16, t)) return t;
2649   }
2650   return none;
2654 // ////////////////////////////////////////////////////////////////////////// //
2655 // return `true` from delegate to stop
2656 MapTile forEachTile (scope bool delegate (MapTile t) dg, optional class!MapTile castClass) {
2657   if (!dg) return none;
2658   if (!castClass) castClass = MapTile;
2659   foreach (MapTile t; objGrid.allObjectsSafe(castClass)) {
2660     if (t.spectral || !t.visible) continue;
2661     if (dg(t)) return t;
2662   }
2663   return none;
2667 // ////////////////////////////////////////////////////////////////////////// //
2668 final void fixWallTiles () {
2669   foreach (MapTile t; objGrid.allObjectsSafe(MapTile)) t.beautifyTile();
2673 // ////////////////////////////////////////////////////////////////////////// //
2674 final MapTile isCollisionAtPoint (int px, int py, optional scope bool delegate (MapTile dg) dg) {
2675   if (!dg) dg = &cbCollisionAnySolid;
2676   return checkTilesInRect(px, py, 1, 1, dg);
2680 // ////////////////////////////////////////////////////////////////////////// //
2681 string scrGetKaliGift (MapTile altar, optional name gift) {
2682   string res;
2684   // find other side of the altar
2685   int sx = player.ix, sy = player.iy;
2686   if (altar) {
2687     sx = altar.ix;
2688     sy = altar.iy;
2689     auto a2 = isCollisionAtPoint(altar.ix+18, altar.iy+8, &cbCollisionSacAltar);
2690     if (!a2) a2 = isCollisionAtPoint(altar.ix-8, altar.iy+8, &cbCollisionSacAltar);
2691     if (a2) { sx = a2.ix; sy = a2.iy; }
2692   }
2694        if (global.favor <= -8) res = "SHE SEEMS VERY ANGRY WITH YOU!";
2695   else if (global.favor < 0) res = "SHE SEEMS ANGRY WITH YOU.";
2696   else if (global.favor == 0) res = "SHE HAS FORGIVEN YOU!";
2697   else if (global.favor >= 32) {
2698     if (global.kaliGift >= 3 && global.favor >= 32+(global.kaliGift-2)*16) {
2699       res = "YOU FEEL INVIGORATED!";
2700       global.kaliGift += 1;
2701       global.plife += global.randOther(4, 8);
2702     } else if (global.kaliGift >= 3) {
2703       res = "SHE SEEMS ECSTATIC WITH YOU!";
2704     } else if (global.bombs < 80) {
2705       res = "YOUR SATCHEL FEELS VERY FULL NOW!";
2706       global.kaliGift = 3;
2707       global.bombs = 99;
2708     } else {
2709       res = "YOU FEEL INVIGORATED!";
2710       global.kaliGift += 1;
2711       global.plife += global.randOther(4, 8);
2712     }
2713   } else if (global.favor >= 16) {
2714     if (global.kaliGift >= 2) {
2715       res = "SHE SEEMS VERY HAPPY WITH YOU!";
2716     } else {
2717       res = "SHE BESTOWS A GIFT UPON YOU!";
2718       global.kaliGift = 2;
2719       // poofs
2720       auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2721       obj.xVel = -1;
2722       obj.yVel = 0;
2723       obj = MakeMapObject(sx, sy-8, 'oPoof');
2724       obj.xVel = 1;
2725       obj.yVel = 0;
2726       // a gift
2727       obj = none;
2728       if (gift) obj = MakeMapObject(sx, sy-8, gift);
2729       if (!obj) obj = MakeMapObject(sx, sy-8, 'oKapala');
2730     }
2731   } else if (global.favor >= 8) {
2732     if (global.kaliGift >= 1) {
2733       res = "SHE SEEMS HAPPY WITH YOU.";
2734     } else {
2735       res = "SHE BESTOWS A GIFT UPON YOU!";
2736       global.kaliGift = 1;
2737       //rAltar = instance_nearest(x, y, oSacAltarRight);
2738       //if (instance_exists(rAltar)) {
2739       // poofs
2740       auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2741       obj.xVel = -1;
2742       obj.yVel = 0;
2743       obj = MakeMapObject(sx, sy-8, 'oPoof');
2744       obj.xVel = 1;
2745       obj.yVel = 0;
2746       obj = none;
2747       if (gift) obj = MakeMapObject(sx, sy-8, gift);
2748       if (!obj) {
2749         auto n = global.randOther(1, 8);
2750         auto m = n;
2751         for (;;) {
2752           name aname = '';
2753                if (n == 1 && !global.hasCape && !global.hasJetpack) aname = 'oCapePickup';
2754           else if (n == 2 && !global.hasGloves) aname = 'oGloves';
2755           else if (n == 3 && !global.hasSpectacles) aname = 'oSpectacles';
2756           else if (n == 4 && !global.hasMitt) aname = 'oMitt';
2757           else if (n == 5 && !global.hasSpringShoes) aname = 'oSpringShoes';
2758           else if (n == 6 && !global.hasSpikeShoes) aname = 'oSpikeShoes';
2759           else if (n == 7 && !global.hasStickyBombs) aname = 'oPaste';
2760           else if (n == 8 && !global.hasCompass) aname = 'oCompass';
2761           if (aname) {
2762             obj = MakeMapObject(sx, sy-8, aname);
2763             if (obj) break;
2764           }
2765           ++n;
2766           if (n > 8) n = 1;
2767           if (n == m) {
2768             obj = MakeMapObject(sx, sy-8, (global.hasJetpack ? 'oBombBox' : 'oJetpack'));
2769             break;
2770           }
2771         }
2772       }
2773     }
2774   } else if (global.favor > 0) {
2775     res = "SHE SEEMS PLEASED WITH YOU.";
2776   }
2778   /*
2779   if (argument1) {
2780     global.message = "";
2781     res = "KALI DEVOURS YOU!"; // sacrifice is player
2782   }
2783   */
2785   return res;
2789 void performSacrifice (MapObject what, MapTile where) {
2790   if (!what || !what.isInstanceAlive) return;
2791   MakeMapObject(what.ix+8, what.iy+8, 'oFlame');
2792   if (where) where.playSound('sndSmallExplode'); else what.playSound('sndSmallExplode');
2793   if (!what.bloodless) what.scrCreateBlood(what.ix+8, what.iy+8, 3);
2795   string msg = "KALI ACCEPTS THE SACRIFICE!";
2797   auto idol = ItemGoldIdol(what);
2798   if (idol) {
2799     ++stats.totalSacrifices;
2800          if (global.favor <= -8) msg = "SHE SEEMS VERY ANGRY WITH YOU!";
2801     else if (global.favor < 0) msg = "SHE SEEMS ANGRY WITH YOU.";
2802     else if (global.favor >= 0) {
2803       // find other side of the altar
2804       int sx = player.ix, sy = player.iy;
2805       auto altar = where;
2806       if (altar) {
2807         sx = altar.ix;
2808         sy = altar.iy;
2809         auto a2 = isCollisionAtPoint(altar.ix+18, altar.iy+8, &cbCollisionSacAltar);
2810         if (!a2) a2 = isCollisionAtPoint(altar.ix-8, altar.iy+8, &cbCollisionSacAltar);
2811         if (a2) { sx = a2.ix; sy = a2.iy; }
2812       }
2813       // poofs
2814       auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2815       obj.xVel = -1;
2816       obj.yVel = 0;
2817       obj = MakeMapObject(sx, sy-8, 'oPoof');
2818       obj.xVel = 1;
2819       obj.yVel = 0;
2820       // a gift
2821       obj = MakeMapObject(sx, sy-16-8, 'oMonkeyGold');
2822     }
2823     osdMessage(msg, 6.66);
2824     scrShake(10);
2825     idol.instanceRemove();
2826     return;
2827   }
2829   if (global.favor <= -8) {
2830     msg = "KALI DEVOURS THE SACRIFICE!";
2831   } else if (global.favor < 0) {
2832     global.favor += (what.status == MapObject::STUNNED ? what.favor : round(what.favor/2.0)); //k8: added `round()`
2833     if (what.favor > 0) what.favor = 0;
2834   } else {
2835     global.favor += (what.status == MapObject::STUNNED ? what.favor : round(what.favor/2.0)); //k8: added `round()`
2836   }
2838   /*!!
2839        if (myAltar.gift1 != "" and global.kaliGift == 0) scrGetKaliGift(myAltar.gift1);
2840   else if (myAltar.gift2 != "" and global.kaliGift == 1) scrGetKaliGift(myAltar.gift2);
2841   else scrGetKaliGift("");
2842   */
2844   // sacrifice is player?
2845   if (what isa PlayerPawn) {
2846     ++stats.totalSelfSacrifices;
2847     msg = "KALI DEVOURS YOU!";
2848     player.visible = false;
2849     player.removeBallAndChain(temp:true);
2850     player.dead = true;
2851     player.status = MapObject::DEAD;
2852   } else {
2853     ++stats.totalSacrifices;
2854     auto msg2 = scrGetKaliGift(where);
2855     what.instanceRemove();
2856     if (msg2) msg = va("%s\n%s", msg, msg2);
2857   }
2859   osdMessage(msg, 6.66);
2861   //!if (isRealLevel()) global.totalSacrifices += 1;
2863   //!global.messageTimer = 200;
2864   //!global.shake = 10;
2865   scrShake(10);
2867   /*damsel
2868   instance_create(x, y, oFlame);
2869   playSound(global.sndSmallExplode);
2870   scrCreateBlood(x, y, 3);
2871   global.message = "KALI ACCEPTS YOUR SACRIFICE!";
2872   if (global.favor <= -8) {
2873     global.message = "KALI DEVOURS YOUR SACRIFICE!";
2874   } else if (global.favor < 0) {
2875     if (status == 98) global.favor += favor*1.5; else global.favor += favor;
2876     if (favor > 0) favor = 0;
2877   } else {
2878     if (status == 98) global.favor += favor*1.5; else global.favor += favor;
2879   }
2881        if (myAltar.gift1 != "" and global.kaliGift == 0) scrGetFavorMsg(myAltar.gift1);
2882   else if (myAltar.gift2 != "" and global.kaliGift == 1) scrGetFavorMsg(myAltar.gift2);
2883   else scrGetFavorMsg("");
2885   global.messageTimer = 200;
2886   global.shake = 10;
2887   instance_destroy();
2888   */
2892 // ////////////////////////////////////////////////////////////////////////// //
2893 final void addBackgroundGfxDetails () {
2894   // add background details
2895   //if (global.customLevel) return;
2896   foreach (; 0..20) {
2897     // bg = instance_create(16*randRoom(1, 42), 16*randRoom(1, 33), oCaveBG);
2898          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);
2899     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);
2900     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);
2901     else MakeMapBackTile('bgExtras', 32*global.randRoom(0, 1), 0, 32, 32, 16*global.randRoom(1, 42), 16*global.randRoom(1, 33), 10002);
2902   }
2906 // ////////////////////////////////////////////////////////////////////////// //
2907 private final void fixRealViewStart () {
2908   int scale = global.scale;
2909   realViewStart.x = clamp(realViewStart.x, viewMin.x*scale, viewMax.x*scale-viewWidth);
2910   realViewStart.y = clamp(realViewStart.y, viewMin.y*scale, viewMax.y*scale-viewHeight);
2914 final int cameraCurrX () { return realViewStart.x/global.scale; }
2915 final int cameraCurrY () { return realViewStart.y/global.scale; }
2918 private final void fixViewStart () {
2919   int scale = global.scale;
2920   viewStart.x = clamp(viewStart.x, viewMin.x*scale, viewMax.x*scale-viewWidth);
2921   viewStart.y = clamp(viewStart.y, viewMin.y*scale, viewMax.y*scale-viewHeight);
2925 final void centerViewAtPlayer () {
2926   if (viewWidth < 1 || viewHeight < 1 || !player) return;
2927   centerViewAt(player.xCenter, player.yCenter);
2931 final void centerViewAt (int x, int y) {
2932   if (viewWidth < 1 || viewHeight < 1) return;
2934   cameraSlideToSpeed.x = 0;
2935   cameraSlideToSpeed.y = 0;
2936   cameraSlideToPlayer = 0;
2938   int scale = global.scale;
2939   x *= scale;
2940   y *= scale;
2941   realViewStart.x = clamp(x-viewWidth/2, 0, tilesWidth*16*scale-viewWidth);
2942   realViewStart.y = clamp(y-viewHeight/2, 0, tilesHeight*16*scale-viewHeight);
2943   fixRealViewStart();
2945   viewStart.x = realViewStart.x;
2946   viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
2947   fixViewStart();
2949   if (onCameraTeleported) onCameraTeleported();
2953 const int ViewPortToleranceX = 16*1+8;
2954 const int ViewPortToleranceY = 16*1+8;
2956 final void fixCamera () {
2957   if (!player) return;
2958   if (viewWidth < 1 || viewHeight < 1) return;
2959   int scale = global.scale;
2960   auto alwaysCenterX = global.config.alwaysCenterPlayer;
2961   auto alwaysCenterY = alwaysCenterX;
2962   // calculate offset from viewport center (in game units), and fix viewport
2964   int camDestX = player.ix+8;
2965   int camDestY = player.iy+8;
2966   if (!cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y) != 0) {
2967     // slide camera to point
2968     if (cameraSlideToSpeed.x) camDestX = cameraSlideToCurr.x;
2969     if (cameraSlideToSpeed.y) camDestY = cameraSlideToCurr.y;
2970     int dx = cameraSlideToDest.x-camDestX;
2971     int dy = cameraSlideToDest.y-camDestY;
2972     //writeln("speed:(", cameraSlideToSpeed.x, ",", cameraSlideToSpeed.y, "); cur:(", camDestX, ",", camDestY, "); dest:(", cameraSlideToDest.x, ",", cameraSlideToDest.y, "); delta=(", dx, ",", dy, ")");
2973     if (dx && cameraSlideToSpeed.x != 0) {
2974       alwaysCenterX = true;
2975       if (abs(dx) <= abs(cameraSlideToSpeed.x)) {
2976         camDestX = cameraSlideToDest.x;
2977       } else {
2978         camDestX += sign(dx)*abs(cameraSlideToSpeed.x);
2979       }
2980     }
2981     if (dy && abs(cameraSlideToSpeed.y) != 0) {
2982       alwaysCenterY = true;
2983       if (abs(dy) <= cameraSlideToSpeed.y) {
2984         camDestY = cameraSlideToDest.y;
2985       } else {
2986         camDestY += sign(dy)*abs(cameraSlideToSpeed.y);
2987       }
2988     }
2989     //writeln("  new:(", camDestX, ",", camDestY, ")");
2990     if (cameraSlideToSpeed.x) cameraSlideToCurr.x = camDestX;
2991     if (cameraSlideToSpeed.y) cameraSlideToCurr.y = camDestY;
2992   }
2994   // horizontal
2995   if (cameraSlideToSpeed.x && !cameraSlideToPlayer) {
2996     realViewStart.x = clamp(camDestX*scale, 0, tilesWidth*16*scale-viewWidth);
2997   } else if (!player.cameraBlockX) {
2998     int x = camDestX*scale;
2999     int cx = realViewStart.x;
3000     if (alwaysCenterX) {
3001       cx = x-viewWidth/2;
3002     } else {
3003       int xofs = x-(cx+viewWidth/2);
3004            if (xofs < -ViewPortToleranceX*scale) cx += xofs+ViewPortToleranceX*scale;
3005       else if (xofs > ViewPortToleranceX*scale) cx += xofs-ViewPortToleranceX*scale;
3006     }
3007     // slide back to player?
3008     if (cameraSlideToPlayer && cameraSlideToSpeed.x) {
3009       int prevx = cameraSlideToCurr.x*scale;
3010       int dx = (cx-prevx)/scale;
3011       if (abs(dx) <= cameraSlideToSpeed.x) {
3012         writeln("BACKSLIDE X COMPLETE!");
3013         cameraSlideToSpeed.x = 0;
3014       } else {
3015         cx = prevx+(sign(dx)*cameraSlideToSpeed.x)*scale;
3016         cameraSlideToCurr.x += sign(dx)*cameraSlideToSpeed.x;
3017         if ((dx < 0 && cx <= 0) || (dx > 0 && cx >= tilesWidth*16*scale-viewWidth)) {
3018           writeln("BACKSLIDE X COMPLETE!");
3019           cameraSlideToSpeed.x = 0;
3020         }
3021       }
3022     }
3023     realViewStart.x = clamp(cx, 0, tilesWidth*16*scale-viewWidth);
3024   }
3026   // vertical
3027   if (cameraSlideToSpeed.y && !cameraSlideToPlayer) {
3028     realViewStart.y = clamp(camDestY*scale, 0, tilesHeight*16*scale-viewHeight);
3029   } else if (!player.cameraBlockY) {
3030     int y = camDestY*scale;
3031     int cy = realViewStart.y;
3032     if (alwaysCenterY) {
3033       cy = y-viewHeight/2;
3034     } else {
3035       int yofs = y-(cy+viewHeight/2);
3036            if (yofs < -ViewPortToleranceY*scale) cy += yofs+ViewPortToleranceY*scale;
3037       else if (yofs > ViewPortToleranceY*scale) cy += yofs-ViewPortToleranceY*scale;
3038     }
3039     // slide back to player?
3040     if (cameraSlideToPlayer && cameraSlideToSpeed.y) {
3041       int prevy = cameraSlideToCurr.y*scale;
3042       int dy = (cy-prevy)/scale;
3043       if (abs(dy) <= cameraSlideToSpeed.y) {
3044         writeln("BACKSLIDE Y COMPLETE!");
3045         cameraSlideToSpeed.y = 0;
3046       } else {
3047         cy = prevy+(sign(dy)*cameraSlideToSpeed.y)*scale;
3048         cameraSlideToCurr.y += sign(dy)*cameraSlideToSpeed.y;
3049         if ((dy < 0 && cy <= 0) || (dy > 0 && cy >= tilesHeight*16*scale-viewHeight)) {
3050           writeln("BACKSLIDE Y COMPLETE!");
3051           cameraSlideToSpeed.y = 0;
3052         }
3053       }
3054     }
3055     realViewStart.y = clamp(cy, 0, tilesHeight*16*scale-viewHeight);
3056   }
3058   if (cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y) == 0) cameraSlideToPlayer = 0;
3060   fixRealViewStart();
3061   //writeln("  new2:(", cameraCurrX, ",", cameraCurrY, ")");
3063   viewStart.x = realViewStart.x;
3064   viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
3065   fixViewStart();
3069 // ////////////////////////////////////////////////////////////////////////// //
3070 // x0 and y0 are non-scaled (and will be scaled)
3071 final void drawSpriteAt (name sprName, float frnumf, int x0, int y0, optional bool small) {
3072   if (!sprName) return;
3073   auto spr = sprStore[sprName];
3074   if (!spr || !spr.frames.length) return;
3075   int scale = global.scale;
3076   x0 *= scale;
3077   y0 *= scale;
3078   int frnum = max(0, trunc(frnumf))%spr.frames.length;
3079   auto sfr = spr.frames[frnum];
3080   int sx0 = x0-sfr.xofs*scale;
3081   int sy0 = y0-sfr.yofs*scale;
3082   if (small && scale > 1) {
3083     sfr.tex.blitExt(sx0, sy0, round(sx0+sfr.tex.width*(scale/2.0)), round(sy0+sfr.tex.height*(scale/2.0)), 0, 0);
3084   } else {
3085     sfr.tex.blitAt(sx0, sy0, scale);
3086   }
3090 final void drawSpriteAtS3 (name sprName, float frnumf, int x0, int y0) {
3091   if (!sprName) return;
3092   auto spr = sprStore[sprName];
3093   if (!spr || !spr.frames.length) return;
3094   x0 *= 3;
3095   y0 *= 3;
3096   int frnum = max(0, trunc(frnumf))%spr.frames.length;
3097   auto sfr = spr.frames[frnum];
3098   int sx0 = x0-sfr.xofs*3;
3099   int sy0 = y0-sfr.yofs*3;
3100   sfr.tex.blitAt(sx0, sy0, 3);
3104 // x0 and y0 are non-scaled (and will be scaled)
3105 final void drawTextAt (int x0, int y0, string text, optional int scale) {
3106   if (!text) return;
3107   if (!specified_scale) scale = global.scale;
3108   x0 *= scale;
3109   y0 *= scale;
3110   sprStore.renderText(x0, y0, text, scale);
3114 void renderCompass (float currFrameDelta) {
3115   if (!global.hasCompass) return;
3117   /*
3118   if (isRoom("rOlmec")) {
3119     global.exitX = 648;
3120     global.exitY = 552;
3121   } else if (isRoom("rOlmec2")) {
3122     global.exitX = 648;
3123     global.exitY = 424;
3124   }
3125   */
3127   bool hasMessage = osdHasMessage();
3128   foreach (MapTile et; allExits) {
3129     // original compass
3130     int exitX = et.ix, exitY = et.iy;
3131     int vx0 = viewStart.x/global.scale, vy0 = viewStart.y/global.scale;
3132     int vx1 = (viewStart.x+viewWidth)/global.scale;
3133     int vy1 = (viewStart.y+viewHeight)/global.scale;
3134     if (exitY > vy1-16) {
3135       if (exitX < vx0) {
3136         drawSpriteAt(hasMessage ? 'sCompassSmallLL' : 'sCompassLL', 0, 0, 224);
3137       } else if (exitX > vx1-16) {
3138         drawSpriteAt(hasMessage ? 'sCompassSmallLR' : 'sCompassLR', 0, 304, 224);
3139       } else {
3140         drawSpriteAt(hasMessage ? 'sCompassSmallDown' : 'sCompassDown', 0, exitX-vx0, 224);
3141       }
3142     } else if (exitX < vx0) {
3143       drawSpriteAt(hasMessage ? 'sCompassSmallLeft' : 'sCompassLeft', 0, 0, exitY-vy0);
3144     } else if (exitX > vx1-16) {
3145       drawSpriteAt(hasMessage ? 'sCompassSmallRight' : 'sCompassRight', 0, 304, exitY-vy0);
3146     }
3147     break; // only the first exit
3148   }
3152 final bool totalsNameCmpCB (ref GameStats::TotalItem a, ref GameStats::TotalItem b) {
3153   auto sa = string(a.objName);
3154   auto sb = string(b.objName);
3155   return (sa < sb);
3158 void renderTransitionInfo (float currFrameDelta) {
3159   //FIXME!
3160   /*
3161   GameStats.sortTotalsList(stats.kills, &totalsNameCmpCB);
3163   int maxLen = 0;
3164   foreach (int idx, ref auto k; stats.kills) {
3165     string s = string(k);
3166     maxLen = max(maxLen, s.length);
3167   }
3168   maxLen *= 8;
3170   sprStore.loadFont('sFontSmall');
3171   Video.color = 0xff_ff_00;
3172   foreach (int idx, ref auto k; stats.kills) {
3173     int deaths = 0;
3174     foreach (int xidx, ref auto d; stats.totalKills) {
3175       if (d.objName == k) { deaths = d.count; break; }
3176     }
3177     //drawTextAt(16, 4+idx*8, va("%s: %d (%d)", string(k.objName).toUpperCase, k.count, deaths));
3178     drawTextAt(16, 4+idx*8, string(k).toUpperCase);
3179     drawTextAt(16+maxLen, 4+idx*8, va(": %d (%d)", k.count, deaths));
3180   }
3181   */
3185 void renderGhostTimer (float currFrameDelta) {
3186   if (ghostTimeLeft <= 0) return;
3187   //ghostTimeLeft /= 30; // frames -> seconds
3189   int hgt = Video.screenHeight-64;
3190   if (hgt < 1) return;
3191   int rhgt = round(float(hgt)*(float(ghostTimeLeft)/30.0)/1000.0);
3192   //writeln("hgt=", hgt, "; rhgt=", rhgt, "; gtl=", ghostTimeLeft/30);
3193   if (rhgt > 0) {
3194     auto oclr = Video.color;
3195     Video.color = 0xcf_ff_7f_00;
3196     Video.fillRect(Video.screenWidth-20, 32, 16, hgt-rhgt);
3197     Video.color = 0x7f_ff_7f_00;
3198     Video.fillRect(Video.screenWidth-20, 32+(hgt-rhgt), 16, rhgt);
3199     Video.color = oclr;
3200   }
3204 void renderStarsHUD (float currFrameDelta) {
3205   bool scumSmallHud = global.config.scumSmallHud;
3207   //auto life = max(0, global.plife);
3208   int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3209   int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3210   //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3212   int hhup;
3214   if (scumSmallHud) {
3215     sprStore.loadFont('sFontSmall');
3216     hhup = 6;
3217   } else {
3218     sprStore.loadFont('sFont');
3219     hhup = 2;
3220   }
3222   Video.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3223   //draw_sprite(sHeart, -1, view_xview[0]+8, view_yview[0]+8);
3224   //draw_text(view_xview[0]+24, view_yview[0]+8, life);
3225   if (scumSmallHud) {
3226     if (global.plife == 1) {
3227       drawSpriteAt('sHeartSmallBlink', global.heartBlink, 10, hhup-4);
3228       global.heartBlink += 0.1;
3229       if (global.heartBlink > 3) global.heartBlink = 0;
3230     } else {
3231       drawSpriteAt('sHeartSmall', -1, 10, hhup-4);
3232       global.heartBlink = 0;
3233     }
3234   } else {
3235     if (global.plife == 1) {
3236       drawSpriteAt('sHeartBlink', global.heartBlink, 8, hhup);
3237       global.heartBlink += 0.1;
3238       if (global.heartBlink > 3) global.heartBlink = 0;
3239     } else {
3240       drawSpriteAt('sHeart', -1, 8, hhup);
3241       global.heartBlink = 0;
3242     }
3243   }
3244   int life = clamp(global.plife, 0, 99);
3245   drawTextAt(16+8, hhup, va("%d", life));
3247   //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3248   drawSpriteAt('sShopkeeperIcon', -1, 64, hhup, scumSmallHud);
3249   drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", starsKills));
3251   if (starsRoomTimer1 > 0) {
3252     sprStore.loadFont('sFontSmall');
3253     Video.color = 0xff_ff_00;
3254     int scale = global.scale;
3255     sprStore.renderMultilineTextCentered(Video.screenWidth/2, 216*scale, va("SHOTGUN CHALLENGE BEGINS IN ~%d~", (starsRoomTimer1/30)+1), scale, 0xff_00_00);
3256   }
3260 void renderSunHUD (float currFrameDelta) {
3261   bool scumSmallHud = global.config.scumSmallHud;
3263   //auto life = max(0, global.plife);
3264   int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3265   int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3266   //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3268   int hhup;
3270   if (scumSmallHud) {
3271     sprStore.loadFont('sFontSmall');
3272     hhup = 6;
3273   } else {
3274     sprStore.loadFont('sFont');
3275     hhup = 2;
3276   }
3278   Video.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3279   //draw_sprite(sHeart, -1, view_xview[0]+8, view_yview[0]+8);
3280   //draw_text(view_xview[0]+24, view_yview[0]+8, life);
3281   if (scumSmallHud) {
3282     if (global.plife == 1) {
3283       drawSpriteAt('sHeartSmallBlink', global.heartBlink, 10, hhup-4);
3284       global.heartBlink += 0.1;
3285       if (global.heartBlink > 3) global.heartBlink = 0;
3286     } else {
3287       drawSpriteAt('sHeartSmall', -1, 10, hhup-4);
3288       global.heartBlink = 0;
3289     }
3290   } else {
3291     if (global.plife == 1) {
3292       drawSpriteAt('sHeartBlink', global.heartBlink, 8, hhup);
3293       global.heartBlink += 0.1;
3294       if (global.heartBlink > 3) global.heartBlink = 0;
3295     } else {
3296       drawSpriteAt('sHeart', -1, 8, hhup);
3297       global.heartBlink = 0;
3298     }
3299   }
3300   int life = clamp(global.plife, 0, 99);
3301   drawTextAt(16+8, hhup, va("%d", life));
3303   //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3304   drawSpriteAt('sDamselIcon', -1, 64, hhup, scumSmallHud);
3305   drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", sunScore));
3307   if (sunRoomTimer1 > 0) {
3308     sprStore.loadFont('sFontSmall');
3309     Video.color = 0xff_ff_00;
3310     int scale = global.scale;
3311     sprStore.renderMultilineTextCentered(Video.screenWidth/2, 216*scale, va("DAMSEL CHALLENGE BEGINS IN ~%d~", (sunRoomTimer1/30)+1), scale, 0xff_00_00);
3312   }
3316 void renderMoonHUD (float currFrameDelta) {
3317   bool scumSmallHud = global.config.scumSmallHud;
3319   //auto life = max(0, global.plife);
3320   int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3321   int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3322   //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3324   int hhup;
3326   if (scumSmallHud) {
3327     sprStore.loadFont('sFontSmall');
3328     hhup = 6;
3329   } else {
3330     sprStore.loadFont('sFont');
3331     hhup = 2;
3332   }
3334   Video.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3336   //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3337   drawSpriteAt('sHoopsIcon', -1, 8, hhup, scumSmallHud);
3338   drawTextAt(8+16-(scumSmallHud ? 6 : 0), hhup, va("%d", moonScore));
3339   drawSpriteAt('sTimerIcon', -1, 64, hhup, scumSmallHud);
3340   drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", max(0, moonTimer)));
3342   if (moonRoomTimer1 > 0) {
3343     sprStore.loadFont('sFontSmall');
3344     Video.color = 0xff_ff_00;
3345     int scale = global.scale;
3346     sprStore.renderMultilineTextCentered(Video.screenWidth/2, 216*scale, va("ARCHERY CHALLENGE BEGINS IN ~%d~", (moonRoomTimer1/30)+1), scale, 0xff_00_00);
3347   }
3351 void renderHUD (float currFrameDelta) {
3352   if (levelKind == LevelKind.Stars) { renderStarsHUD(currFrameDelta); return; }
3353   if (levelKind == LevelKind.Sun) { renderSunHUD(currFrameDelta); return; }
3354   if (levelKind == LevelKind.Moon) { renderMoonHUD(currFrameDelta); return; }
3356   if (!isHUDEnabled()) return;
3358   if (levelKind == LevelKind.Transition) { renderTransitionInfo(currFrameDelta); return; }
3360   int lifeX = 4; // 8
3361   int bombX = 56;
3362   int ropeX = 104;
3363   int ammoX = 152;
3364   int moneyX = 200;
3365   int hhup;
3366   bool scumSmallHud = global.config.scumSmallHud;
3367   if (!global.config.optSGAmmo) moneyX = ammoX;
3369   if (scumSmallHud) {
3370     sprStore.loadFont('sFontSmall');
3371     hhup = 6;
3372   } else {
3373     sprStore.loadFont('sFont');
3374     hhup = 0;
3375   }
3376   //int alpha = 0x6f_00_00_00;
3377   int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3378   int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3380   //Video.color = 0xff_ff_ff;
3381   Video.color = 0xff_ff_ff|talpha;
3383   // hearts
3384   if (scumSmallHud) {
3385     if (global.plife == 1) {
3386       drawSpriteAt('sHeartSmallBlink', global.heartBlink, lifeX+2, 4-hhup);
3387       global.heartBlink += 0.1;
3388       if (global.heartBlink > 3) global.heartBlink = 0;
3389     } else {
3390       drawSpriteAt('sHeartSmall', -1, lifeX+2, 4-hhup);
3391       global.heartBlink = 0;
3392     }
3393   } else {
3394     if (global.plife == 1) {
3395       drawSpriteAt('sHeartBlink', global.heartBlink, lifeX, 8-hhup);
3396       global.heartBlink += 0.1;
3397       if (global.heartBlink > 3) global.heartBlink = 0;
3398     } else {
3399       drawSpriteAt('sHeart', -1, lifeX, 8-hhup);
3400       global.heartBlink = 0;
3401     }
3402   }
3404   int life = clamp(global.plife, 0, 99);
3405   //if (!scumHud && life > 99) life = 99;
3406   drawTextAt(lifeX+16, 8-hhup, va("%d", life));
3408   // bombs
3409   if (global.hasStickyBombs && global.stickyBombsActive) {
3410     if (scumSmallHud) drawSpriteAt('sStickyBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sStickyBombIcon', -1, bombX, 8-hhup);
3411   } else {
3412     if (scumSmallHud) drawSpriteAt('sBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sBombIcon', -1, bombX, 8-hhup);
3413   }
3414   int n = global.bombs;
3415   if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3416   drawTextAt(bombX+16, 8-hhup, va("%d", n));
3418   // ropes
3419   if (scumSmallHud) drawSpriteAt('sRopeIconSmall', -1, ropeX+2, 4-hhup); else drawSpriteAt('sRopeIcon', -1, ropeX, 8-hhup);
3420   n = global.rope;
3421   if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3422   drawTextAt(ropeX+16, 8-hhup, va("%d", n));
3424   // shotgun shells
3425   if (global.config.optSGAmmo && (!player || player.holdItem !isa ItemWeaponBow)) {
3426     if (scumSmallHud) drawSpriteAt('sShellsIcon', -1, ammoX+6, 8-hhup); else drawSpriteAt('sShellsIcon', -1, ammoX+7, 12-hhup);
3427     n = global.sgammo;
3428     if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3429     drawTextAt(ammoX+16, 8-hhup, va("%d", n));
3430   } else if (player && player.holdItem isa ItemWeaponBow) {
3431     if (scumSmallHud) drawSpriteAt('sArrowRight', -1, ammoX+6, 8-hhup); else drawSpriteAt('sArrowRight', -1, ammoX+7, 12-hhup);
3432     n = global.arrows;
3433     if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3434     drawTextAt(ammoX+16, 8-hhup, va("%d", n));
3435   }
3437   // money
3438   if (scumSmallHud) drawSpriteAt('sDollarSignSmall', -1, moneyX+2, 3-hhup); else drawSpriteAt('sDollarSign', -1, moneyX, 8-hhup);
3439   drawTextAt(moneyX+16, 8-hhup, va("%d", stats.money-xmoney));
3441   // items
3442   Video.color = 0xff_ff_ff|ialpha;
3444   int ity = (scumSmallHud ? 18-hhup : 24-hhup);
3446   n = 8; //28;
3447   if (global.hasUdjatEye) {
3448     if (global.udjatBlink) drawSpriteAt('sUdjatEyeIcon2', -1, n, ity); else drawSpriteAt('sUdjatEyeIcon', -1, n, ity);
3449     n += 20;
3450   }
3451   if (global.hasAnkh) { drawSpriteAt('sAnkhIcon', -1, n, ity); n += 20; }
3452   if (global.hasCrown) { drawSpriteAt('sCrownIcon', -1, n, ity); n += 20; }
3453   if (global.hasKapala) {
3454          if (global.bloodLevel == 0) drawSpriteAt('sKapalaIcon', 0, n, ity);
3455     else if (global.bloodLevel <= 2) drawSpriteAt('sKapalaIcon', 1, n, ity);
3456     else if (global.bloodLevel <= 4) drawSpriteAt('sKapalaIcon', 2, n, ity);
3457     else if (global.bloodLevel <= 6) drawSpriteAt('sKapalaIcon', 3, n, ity);
3458     else if (global.bloodLevel <= 8) drawSpriteAt('sKapalaIcon', 4, n, ity);
3459     n += 20;
3460   }
3461   if (global.hasSpectacles) { drawSpriteAt('sSpectaclesIcon', -1, n, ity); n += 20; }
3462   if (global.hasGloves) { drawSpriteAt('sGlovesIcon', -1, n, ity); n += 20; }
3463   if (global.hasMitt) { drawSpriteAt('sMittIcon', -1, n, ity); n += 20; }
3464   if (global.hasSpringShoes) { drawSpriteAt('sSpringShoesIcon', -1, n, ity); n += 20; }
3465   if (global.hasJordans) { drawSpriteAt('sJordansIcon', -1, n, ity); n += 20; }
3466   if (global.hasSpikeShoes) { drawSpriteAt('sSpikeShoesIcon', -1, n, ity); n += 20; }
3467   if (global.hasCape) { drawSpriteAt('sCapeIcon', -1, n, ity); n += 20; }
3468   if (global.hasJetpack) { drawSpriteAt('sJetpackIcon', -1, n, ity); n += 20; }
3469   if (global.hasStickyBombs) { drawSpriteAt('sPasteIcon', -1, n, ity); n += 20; }
3470   if (global.hasCompass) { drawSpriteAt('sCompassIcon', -1, n, ity); n += 20; }
3471   if (global.hasParachute) { drawSpriteAt('sParachuteIcon', -1, n, ity); n += 20; }
3473   if (player.holdItem && player.holdItem.objName == 'Bow' && global.arrows > 0) {
3474     int m = 1;
3475     float malpha = 1;
3476     while (m <= global.arrows && m <= 20 && malpha > 0) {
3477       Video.color = trunc(malpha*255)<<24|0xff_ff_ff;
3478       drawSpriteAt('sArrowIcon', -1, n, ity);
3479       n += 4;
3480       if (m > 10) malpha -= 0.1; //and global.arrows > 20 malpha -= 0.1
3481       m += 1;
3482     }
3483   }
3485   if (xmoney > 0) {
3486     sprStore.loadFont('sFontSmall');
3487     Video.color = 0xff_ff_00|talpha;
3488     if (scumSmallHud) drawTextAt(moneyX+8, 8+8-hhup, va("+%d", xmoney));
3489     else drawTextAt(moneyX, 8+16-hhup, va("+%d", xmoney));
3490   }
3492   Video.color = 0xff_ff_ff;
3493   if (global.config.ghostShowTime) renderGhostTimer(currFrameDelta);
3497 // ////////////////////////////////////////////////////////////////////////// //
3498 // x0 and y0 are non-scaled (and will be scaled)
3499 final void drawTextAtS3 (int x0, int y0, string text, optional int hiColor1, optional int hiColor2) {
3500   if (!text) return;
3501   x0 *= 3;
3502   y0 *= 3;
3503   sprStore.renderTextWithHighlight(x0, y0, text, 3, hiColor1!optional, hiColor2!optional);
3507 final void drawTextAtS3Centered (int y0, string text, optional int hiColor1, optional int hiColor2) {
3508   if (!text) return;
3509   int x0 = (Video.screenWidth-sprStore.getTextWidth(text, 3, specified_hiColor1, specified_hiColor2))/2;
3510   sprStore.renderTextWithHighlight(x0, y0*3, text, 3, hiColor1!optional, hiColor2!optional);
3514 void renderHelpOverlay () {
3515   Video.color = 0;
3516   Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
3518   int tx = 16;
3519   int txoff = 0; // text x pos offset (for multi-color lines)
3520   int ty = 8;
3521   if (gameHelpScreen) {
3522     sprStore.loadFont('sFontSmall');
3523     Video.color = 0xff_ff_ff;
3524     drawTextAtS3Centered(ty, va("HELP (PAGE ~%d~ OF ~%d~)", gameHelpScreen, MaxGameHelpScreen), 0xff_ff_00);
3525     ty += 24;
3526   }
3528   if (gameHelpScreen == 1) {
3529     sprStore.loadFont('sFontSmall');
3530     Video.color = 0xff_ff_00; drawTextAtS3(tx, ty, "INVENTORY BASICS"); ty += 16;
3531     Video.color = 0xff_ff_ff;
3532     drawTextAtS3(tx, ty, global.expandString("Press $SWITCH to cycle through items."), 0x00_ff_00);
3533     ty += 8;
3534     ty += 56;
3535     Video.color = 0xff_ff_ff;
3536     drawSpriteAtS3('sHelpSprite1', -1, 64, 96);
3537   } else if (gameHelpScreen == 2) {
3538     sprStore.loadFont('sFontSmall');
3539     Video.color = 0xff_ff_00;
3540     drawTextAtS3(tx, ty, "SELLING TO SHOPKEEPERS"); ty += 16;
3541     Video.color = 0xff_ff_ff;
3542     drawTextAtS3(tx, ty, global.expandString("Press $PAY to offer your currently"), 0x00_ff_00); ty += 8;
3543     drawTextAtS3(tx, ty, "held item to the shopkeeper."); ty += 16;
3544     drawTextAtS3(tx, ty, "If the shopkeeper is interested, "); ty += 8;
3545     //drawTextAtS3(tx, ty, global.expandString("press $PAY again to complete the sale."), 0x00_ff_00); ty += 72;
3546     drawTextAtS3(tx, ty, global.expandString("press $PAY again to complete"), 0x00_ff_00);
3547     drawTextAtS3(tx, ty+8, "the sale.");
3548     ty += 72;
3549     drawSpriteAtS3('sHelpSell', -1, 112, 100);
3550     drawTextAtS3(tx, ty, "Purchasing goods from the shopkeeper"); ty += 8;
3551     drawTextAtS3(tx, ty, "will increase the funds he has"); ty += 8;
3552     drawTextAtS3(tx, ty, "available to buy your unwanted stuff."); ty += 8;
3553   } else {
3554     // map
3555     sprStore.loadFont('sFont');
3556     Video.color = 0xff_ff_ff;
3557     drawTextAtS3(136, 8, "MAP");
3559     Video.color = 0xff_ff_00;
3560     drawTextAtS3Centered(24, lg.mapTitle);
3562     if (lg.mapSprite) {
3563       auto spf = sprStore[lg.mapSprite].frames[0];
3564       int mapX = 160-spf.width/2;
3565       int mapY = 120-spf.height/2;
3566       //mapTitleX = 160-string_length(global.mapTitle)*8/2;
3568       Video.color = 0xff_ff_ff;
3569       drawSpriteAtS3(lg.mapSprite, -1, mapX, mapY);
3571       if (lg.mapSprite != 'sMapDefault') {
3572         int mx = -1, my = -1;
3574         // set position of player icon
3575         switch (global.currLevel) {
3576           case 1: mx = 81; my = 22; break;
3577           case 2: mx = 113; my = 63; break;
3578           case 3: mx = 197; my = 86; break;
3579           case 4: mx = 133; my = 109; break;
3580           case 5: mx = 181; my = 22; break;
3581           case 6: mx = 126; my = 64; break;
3582           case 7: mx = 158; my = 112; break;
3583           case 8: mx = 66; my = 80; break;
3584           case 9: mx = 30; my = 26; break;
3585           case 10: mx = 88; my = 54; break;
3586           case 11: mx = 148; my = 81; break;
3587           case 12: mx = 210; my = 205; break;
3588           case 13: mx = 66; my = 17; break;
3589           case 14: mx = 146; my = 17; break;
3590           case 15: mx = 82; my = 77; break;
3591           case 16: mx = 178; my = 81; break;
3592         }
3594         if (mx >= 0) {
3595           int plrx = mx+player.ix/16;
3596           int plry = my+player.iy/16;
3597           name plrspr = 'sMapSpelunker';
3598                if (global.isDamsel) plrspr = 'sMapDamsel';
3599           else if (global.isTunnelMan) plrspr = 'sMapTunnel';
3600           auto ss = sprStore[plrspr];
3601           drawSpriteAtS3(plrspr, (pausedTime/2)%ss.frames.length, mapX+plrx, mapY+plry);
3602           // exit door icon
3603           if (global.hasCompass && allExits.length) {
3604             drawSpriteAtS3('sMapRedDot', -1, mapX+mx+allExits[0].ix/16, mapY+my+allExits[0].iy/16);
3605           }
3606         }
3607       }
3608     }
3609   }
3611   sprStore.loadFont('sFontSmall');
3612   Video.color = 0xff_ff_00;
3613   drawTextAtS3Centered(232, "PRESS ~SPACE~/~LEFT~/~RIGHT~ TO CHANGE PAGE", 0x00_ff_00);
3615   Video.color = 0xff_ff_ff;
3619 void renderPauseOverlay () {
3620   //drawTextAt(256, 432, "PAUSED", scale);
3622   if (gameShowHelp) { renderHelpOverlay(); return; }
3624   Video.color = 0xff_ff_00;
3625   //int hiColor = 0x00_ff_00;
3627   int n = 120;
3628   if (isTutorialRoom()) {
3629     sprStore.loadFont('sFont');
3630     drawTextAtS3(40, n-24, "TUTORIAL CAVE");
3631   } else if (isNormalLevel()) {
3632     sprStore.loadFont('sFont');
3634     drawTextAtS3Centered(n-32, va("LEVEL ~%d~", global.currLevel), 0x00_ff_00);
3636     sprStore.loadFont('sFontSmall');
3638     int depth = round((174.8*(global.currLevel-1)+(player.iy+8)*0.34)*(global.config.scumMetric ? 0.3048 : 1.0)*10);
3639     string depthStr = va("DEPTH: ~%d.%d~ %s", depth/10, depth%10, (global.config.scumMetric ? "METRES" : "FEET"));
3640     drawTextAtS3Centered(n-16, depthStr, 0x00_ff_00);
3642     n += 16;
3643     drawTextAtS3Centered(n, va("MONEY: ~%d~", stats.money), 0x00_ff_00);
3644     drawTextAtS3Centered(n+16, va("KILLS: ~%d~", stats.countKills), 0x00_ff_00);
3645     drawTextAtS3Centered(n+32, va("SAVES: ~%d~", stats.damselsSaved), 0x00_ff_00);
3646     drawTextAtS3Centered(n+48, va("TIME: ~%s~", time2str(time/30)), 0x00_ff_00);
3647     drawTextAtS3Centered(n+64, va("LEVEL TIME: ~%s~", time2str((time-levelStartTime)/30)), 0x00_ff_00);
3648   }
3650   sprStore.loadFont('sFontSmall');
3651   Video.color = 0xff_ff_ff;
3652   drawTextAtS3Centered(240-2-8, "~ESC~-RETURN  ~F10~-QUIT  ~CTRL+DEL~-SUICIDE", 0xff_7f_00);
3653   drawTextAtS3Centered(2, "~O~PTIONS  REDEFINE ~K~EYS  ~S~TATISTICS", 0xff_7f_00);
3657 // ////////////////////////////////////////////////////////////////////////// //
3658 private transient array!MapEntity renderVisibleCids;
3659 private transient array!MapEntity renderVisibleLights;
3660 private transient array!MapTile renderFrontTiles; // normal, with fg
3662 final bool renderSortByDepth (MapEntity oa, MapEntity ob) {
3663   auto da = oa.depth, db = ob.depth;
3664   if (da == db) return (oa.objId < ob.objId);
3665   return (da < db);
3669 const int RenderEdgePixNormal = 64;
3670 const int RenderEdgePixLight = 256;
3672 #ifndef EXPERIMENTAL_RENDER_CACHE
3673 enum skipListCreation = false;
3674 #endif
3676 void renderWithOfs (int xofs, int yofs, float currFrameDelta) {
3677   int scale = global.scale;
3679   // don't touch framebuffer alpha
3680   Video.colorMask = Video::CMask.Colors;
3681   Video.color = 0xff_ff_ff;
3683   bool isDarkLevel = global.darkLevel;
3685   if (isDarkLevel) {
3686     switch (global.config.scumPlayerLit) {
3687       case 0: player.lightRadius = 0; break; // never
3688       case 1: // only in "scumDarkness"
3689         player.lightRadius = (global.config.scumDarkness >= 2 ? 96 : 32);
3690         break;
3691       case 2:
3692         player.lightRadius = 96;
3693         break;
3694     }
3695   }
3697   // render cave background
3698   if (levBGImg) {
3699     int tsz = 16*scale;
3700     int bgw = levBGImg.tex.width*scale;
3701     int bgh = levBGImg.tex.height*scale;
3702     int bgTW = (tilesWidth*tsz+bgw-1)/bgw;
3703     int bgTH = (tilesHeight*tsz+bgh-1)/bgh;
3704     int bgX0 = max(0, xofs/bgw);
3705     int bgY0 = max(0, yofs/bgh);
3706     int bgX1 = min(bgTW, (xofs+viewWidth+bgw-1)/bgw);
3707     int bgY1 = min(bgTH, (yofs+viewHeight+bgh-1)/bgh);
3708     foreach (int ty; bgY0..bgY1) {
3709       foreach (int tx; bgX0..bgX1) {
3710         int x0 = tx*bgw-xofs;
3711         int y0 = ty*bgh-yofs;
3712         levBGImg.tex.blitAt(x0, y0, scale);
3713       }
3714     }
3715   }
3717   int RenderEdgePix = (global.darkLevel ? RenderEdgePixLight : RenderEdgePixNormal);
3719   // render background tiles
3720   for (MapBackTile bt = backtiles; bt; bt = bt.next) {
3721     bt.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3722   }
3724   // collect visible special tiles
3725 #ifdef EXPERIMENTAL_RENDER_CACHE
3726   bool skipListCreation = (lastRenderTime == time && renderVisibleCids.length); //FIXME
3727 #endif
3729   if (!skipListCreation) {
3730     renderVisibleCids.clear();
3731     renderVisibleLights.clear();
3732     renderFrontTiles.clear();
3734     int endVX = xofs+viewWidth;
3735     int endVY = yofs+viewHeight;
3737     // add player
3738     //int cnt = 0;
3739     if (player && player.visible) renderVisibleCids[$] = player; // include player in render list
3741     //FIXME: drop lit objects which cannot affect visible area
3742     if (scale > 1) {
3743       // collect visible objects
3744       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)) {
3745         if (!o.visible) continue;
3746         auto tile = MapTile(o);
3747         if (tile) {
3748           if (isDarkLevel && (o.lightRadius > 4 || tile.litWholeTile)) renderVisibleLights[$] = o;
3749           if (tile.invisible) continue;
3750           if (tile.bgfront) renderFrontTiles[$] = tile;
3751           if (tile.bgback) tile.drawWithOfsBack(xofs, yofs, scale, currFrameDelta);
3752         } else {
3753           if (isDarkLevel && o.lightRadius > 4) renderVisibleLights[$] = o;
3754         }
3755         // check if the object is really visible -- this will speed up later sorting
3756         int fx0, fy0, fx1, fy1;
3757         auto spf = o.getSpriteFrame(default, out fx0, out fy0, out fx1, out fy1);
3758         if (!spf) continue; // no sprite -- nothing to draw (no, really)
3759         int ix = o.ix, iy = o.iy;
3760         int x0 = (ix+fx0)*scale, y0 = (iy+fy0)*scale;
3761         int x1 = (ix+fx1)*scale, y1 = (iy+fy1)*scale;
3762         if (x1 <= xofs || y1 <= yofs || x0 >= endVX || y0 >= endVY) {
3763           //++cnt;
3764           continue;
3765         }
3766         renderVisibleCids[$] = o;
3767       }
3768     } else {
3769       foreach (MapEntity o; objGrid.allObjects()) {
3770         if (!o.visible) continue;
3771         auto tile = MapTile(o);
3772         if (tile) {
3773           if (isDarkLevel && (o.lightRadius > 4 || tile.litWholeTile)) renderVisibleLights[$] = o;
3774           if (tile.invisible) continue;
3775           if (tile.bgfront) renderFrontTiles[$] = tile;
3776           if (tile.bgback) tile.drawWithOfsBack(xofs, yofs, scale, currFrameDelta);
3777         } else {
3778           if (isDarkLevel && o.lightRadius > 4) renderVisibleLights[$] = o;
3779         }
3780         renderVisibleCids[$] = o;
3781       }
3782     }
3783     //writeln("::: ", cnt, " invisible objects dropped");
3785     renderVisibleCids.sort(&renderSortByDepth);
3786     lastRenderTime = time;
3787   }
3789   auto depth4Start = 0;
3790   foreach (auto xidx, MapEntity o; renderVisibleCids) {
3791     if (o.depth >= 4) {
3792       depth4Start = xidx;
3793       break;
3794     }
3795   }
3797   bool playerPowerupRendered = false;
3799   // render objects (part one: depth > 3)
3800   foreach (auto idx; depth4Start..renderVisibleCids.length; reverse) {
3801     MapEntity o = renderVisibleCids[idx];
3802     // 1000 is an ordinary tile
3803     if (!playerPowerupRendered && o.depth <= 1200) {
3804       playerPowerupRendered = true;
3805       // so ducking player will have it's cape correctly rendered
3806       if (player.visible) player.drawPrePrePowerupWithOfs(xofs, yofs, scale, currFrameDelta);
3807     }
3808     //if (idx && renderSortByDepth(o, renderVisibleCids[idx-1])) writeln("VIOLATION!");
3809     o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3810   }
3812   // render object (part two: front tile parts, depth 3.5)
3813   foreach (MapTile tile; renderFrontTiles) {
3814     tile.drawWithOfsFront(xofs, yofs, scale, currFrameDelta);
3815   }
3817   // render objects (part three: depth <= 3)
3818   foreach (auto idx; 0..depth4Start; reverse) {
3819     MapEntity o = renderVisibleCids[idx];
3820     o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3821     //done above;if (isDarkLevel && (o.lightRadius > 4 || (o isa MapTile && MapTile(o).litWholeTile))) renderVisibleLights[$] = o;
3822   }
3824   // render player post-effects, so cape or jetpack will be correctly rendered on level exit, for example
3825   player.lastDrawWithOfs(xofs, yofs, scale, currFrameDelta);
3827   // lighting
3828   if (isDarkLevel) {
3829     auto ltex = bgtileStore.lightTexture('ltx512', 512);
3831     // set screen alpha to min
3832     Video.colorMask = Video::CMask.Alpha;
3833     Video.blendMode = Video::BlendMode.None;
3834     Video.color = 0xff_ff_ff_ff;
3835     Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
3836     //Video.colorMask = Video::CMask.All;
3838     // blend lights
3839     // also, stencil 'em, so we can filter dark areas
3840     Video.textureFiltering = true;
3841     Video.stencil = true;
3842     Video.stencilFunc(Video::StencilFunc.Always, 1);
3843     Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Replace);
3844     Video.alphaTestFunc = Video::AlphaFunc.Greater;
3845     Video.alphaTestVal = 0.03+0.011*global.config.darknessDarkness;
3846     Video.color = 0xff_ff_ff;
3847     Video.blendFunc = Video::BlendFunc.Max;
3848     Video.blendMode = Video::BlendMode.Blend; // anything except `Normal`
3849     Video.colorMask = Video::CMask.Alpha;
3851     foreach (MapEntity e; renderVisibleLights) {
3852       int xi, yi;
3853       e.getInterpCoords(currFrameDelta, scale, out xi, out yi);
3854       auto tile = MapTile(e);
3855       if (tile && tile.litWholeTile) {
3856         //Video.color = 0xff_ff_ff;
3857         Video.fillRect(xi-xofs, yi-yofs, e.width*scale, e.height*scale);
3858       }
3859       int lrad = e.lightRadius;
3860       if (lrad < 4) continue; // just in case
3861       lrad += 8;
3862       float lightscale = float(lrad*scale)/float(ltex.tex.width);
3863 #ifdef OLD_LIGHT_OFFSETS
3864       int fx0, fy0, fx1, fy1;
3865       bool doMirror;
3866       auto spf = e.getSpriteFrame(out doMirror, out fx0, out fy0, out fx1, out fy1);
3867       if (spf) {
3868         xi += (fx1-fx0)*scale/2;
3869         yi += (fy1-fy0)*scale/2;
3870       }
3871 #else
3872       int lxofs, lyofs;
3873       e.getLightOffset(out lxofs, out lyofs);
3874       xi += lxofs*scale;
3875       yi += lyofs*scale;
3877 #endif
3878       lrad = lrad*scale/2;
3879       xi -= xofs+lrad;
3880       yi -= yofs+lrad;
3881       ltex.tex.blitAt(xi, yi, lightscale);
3882     }
3883     Video.textureFiltering = false;
3885     // modify only lit parts
3886     Video.stencilFunc(Video::StencilFunc.Equal, 1);
3887     Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Keep);
3888     // multiply framebuffer colors by framebuffer alpha
3889     Video.color = 0xff_ff_ff; // it doesn't matter
3890     Video.blendFunc = Video::BlendFunc.Add;
3891     Video.blendMode = Video::BlendMode.DstMulDstAlpha;
3892     Video.colorMask = Video::CMask.Colors;
3893     Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
3895     // filter unlit parts
3896     Video.stencilFunc(Video::StencilFunc.NotEqual, 1);
3897     Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Keep);
3898     Video.blendFunc = Video::BlendFunc.Add;
3899     Video.blendMode = Video::BlendMode.Filter;
3900     Video.colorMask = Video::CMask.Colors;
3901     Video.color = 0x00_00_18+0x00_00_10*global.config.darknessDarkness;
3902     //Video.color = 0x00_00_18;
3903     //Video.color = 0x00_00_38;
3904     Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
3906     // restore defaults
3907     Video.blendFunc = Video::BlendFunc.Add;
3908     Video.blendMode = Video::BlendMode.Normal;
3909     Video.colorMask = Video::CMask.All;
3910     Video.alphaTestFunc = Video::AlphaFunc.Always;
3911     Video.stencil = false;
3912   }
3914   // clear visible objects list (nope)
3915   //renderVisibleCids.clear();
3916   //renderVisibleLights.clear();
3919   if (global.config.drawHUD) renderHUD(currFrameDelta);
3920   renderCompass(currFrameDelta);
3922   float osdTimeLeft, osdTimeStart;
3923   string msg = osdGetMessage(out osdTimeLeft, out osdTimeStart);
3924   if (msg) {
3925     auto ct = GetTickCount();
3926     int msgScale = 3;
3927     sprStore.loadFont('sFontSmall');
3928     auto msgHeight = sprStore.getMultilineTextHeight(msg, msgScale);
3929     int x = Video.screenWidth/2;
3930     int y = Video.screenHeight-64-msgHeight;
3931     auto oldColor = Video.color;
3932     Video.color = 0xff_ff_00;
3933     if (osdTimeLeft < 0.5) {
3934       int alpha = 255-clamp(int(510*osdTimeLeft), 0, 255);
3935       Video.color = Video.color|(alpha<<24);
3936     } else if (ct-osdTimeStart < 0.5) {
3937       osdTimeStart = ct-osdTimeStart;
3938       int alpha = 255-clamp(int(510*osdTimeStart), 0, 255);
3939       Video.color = Video.color|(alpha<<24);
3940     }
3941     sprStore.renderMultilineTextCentered(x, y, msg, msgScale, 0x00_ff_00, 0xff_ff_ff);
3942     Video.color = oldColor;
3943   }
3945   if (inWinCutscene) renderWinCutsceneOverlay();
3946   Video.color = 0xff_ff_ff;
3950 // ////////////////////////////////////////////////////////////////////////// //
3951 final class!MapObject findGameObjectClassByName (name aname) {
3952   if (!aname) return none; // just in case
3953   auto co = FindClassByGameObjName(aname);
3954   if (!co) {
3955     writeln("***UNKNOWN GAME OBJECT: '", aname, "'");
3956     return none;
3957   }
3958   co = GetClassReplacement(co);
3959   if (!co) FatalError("findGameObjectClassByName: WTF?!");
3960   if (!class!MapObject(co)) FatalError("findGameTileClassByName: object '%n' is not a map object, it is a `%n`", aname, GetClassName(co));
3961   return class!MapObject(co);
3965 final class!MapTile findGameTileClassByName (name aname) {
3966   if (!aname) return none; // just in case
3967   auto co = FindClassByGameObjName(aname);
3968   if (!co) return MapTile; // unknown names will be routed directly to tile object
3969   co = GetClassReplacement(co);
3970   if (!co) FatalError("findGameTileClassByName: WTF?!");
3971   if (!class!MapTile(co)) FatalError("findGameTileClassByName: object '%n' is not a tile, it is a `%n`", aname, GetClassName(co));
3972   return class!MapTile(co);
3976 final MapObject findAnyObjectOfType (name aname) {
3977   if (!aname) return none;
3978   auto cls = FindClassByGameObjName(aname);
3979   if (!cls) return none;
3980   foreach (MapObject obj; objGrid.allObjects(MapObject)) {
3981     if (obj.spectral) continue;
3982     if (obj isa cls) return obj;
3983   }
3984   return none;
3988 // ////////////////////////////////////////////////////////////////////////// //
3989 final bool isRopePlacedAt (int x, int y) {
3990   int[8] covered;
3991   foreach (ref auto v; covered) v = false;
3992   foreach (MapTile t; objGrid.inRectPix(x, y-8, 1, 17, precise:false, castClass:MapTileRope)) {
3993     //if (!cbIsRopeTile(t)) continue;
3994     if (t.ix != x) continue;
3995     if (t.iy == y) return true;
3996     foreach (int ty; t.iy..t.iy+8) {
3997       int d = ty-y;
3998       if (d >= 0 && d < covered.length) covered[d] = true;
3999     }
4000   }
4001   // check if the whole rope height is completely covered with ropes
4002   foreach (auto v; covered) if (!v) return false;
4003   return true;
4007 final MapTile CreateMapTile (int xpos, int ypos, name aname) {
4008   if (!aname) FatalError("cannot create typeless tile");
4009   auto tclass = findGameTileClassByName(aname);
4010   if (!tclass) return none;
4011   MapTile tile = SpawnObject(tclass);
4012   tile.global = global;
4013   tile.level = self;
4014   tile.objName = aname;
4015   tile.objType = aname; // just in case
4016   tile.fltx = xpos;
4017   tile.flty = ypos;
4018   tile.objId = ++lastUsedObjectId;
4019   if (!tile.initialize()) FatalError("cannot create tile '%n'", aname);
4020   return tile;
4024 final bool PutSpawnedMapTile (int x, int y, MapTile tile, optional bool putToGrid) {
4025   if (!tile || !tile.isInstanceAlive) return false;
4027   if (!putToGrid) putToGrid = (tile.moveable || tile.toSpecialGrid || tile.width != 16 || tile.height != 16 || x%16 != 0 || y%16 != 0);
4029   //writeln("putting tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (putToGrid ? "special " : ""), "grid, pos=(", x/16.0, ",", y/16.0, ")");
4031   if (!putToGrid) {
4032     int mapx = x/16, mapy = y/16;
4033     if (mapx < 0 || mapx >= tilesWidth || mapy < 0 || mapy >= tilesHeight) return false;
4034   }
4036   // if we already have rope tile there, there is no reason to add another one
4037   if (tile isa MapTileRope) {
4038     if (isRopePlacedAt(x, y)) return false;
4039   }
4041   // activate special or animated tile
4042   tile.active = tile.active || putToGrid || tile.moveable || tile.toSpecialGrid || tile.lava /*|| tile.water*/; // will be done in MakeMapTile
4043   // animated tiles must be active
4044   if (!tile.active) {
4045     auto spr = tile.getSprite();
4046     if (spr && spr.frames.length > 1) {
4047       writeln("activated animated tile '", tile.objName, "'");
4048       tile.active = true;
4049     }
4050   }
4052   tile.fltx = x;
4053   tile.flty = y;
4054   if (putToGrid) {
4055     //if (tile isa TitleTileCopy) writeln("*** PUTTING COPYRIGHT TILE");
4056     tile.toSpecialGrid = true;
4057     if (!tile.dontReplaceOthers && x&16 == 0 && y%16 == 0) {
4058       auto t = getTileAtGridAny(x/16, y/16);
4059       if (t && !t.immuneToReplacement) {
4060         writeln("REPLACING tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
4061         writeln("      NEW tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (tile.toSpecialGrid ? "special " : ""), "grid, pos=(", tile.fltx/16.0, ",", tile.flty/16.0, ")");
4062         t.instanceRemove();
4063       }
4064     }
4065     objGrid.insert(tile);
4066   } else {
4067     //writeln("SIZE: ", tilesWidth, "x", tilesHeight);
4068     setTileAtGrid(x/16, y/16, tile);
4069     auto t = getTileAtGridAny(x/16, y/16);
4070     /*
4071     if (t != tile) {
4072       writeln("putting tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (putToGrid ? "special " : ""), "grid, pos=(", x/16.0, ",", y/16.0, ")");
4073       checkTilesInRect(x/16, y/16, 16, 16, delegate bool (MapTile tile) {
4074         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, ")");
4075         return false;
4076       });
4077       FatalError("FUUUUUU");
4078     }
4079     */
4080   }
4082   if (tile.enter) registerEnter(tile);
4083   if (tile.exit) registerExit(tile);
4085   return true;
4089 // won't call `onDestroy()`
4090 final void RemoveMapTileFromGrid (int tileX, int tileY, optional string reason) {
4091   if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
4092     auto t = getTileAtGridAny(tileX, tileY);
4093     if (t) {
4094       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, ")");
4095       t.instanceRemove();
4096       checkWater = true;
4097     }
4098   }
4102 final MapTile MakeMapTile (int mapx, int mapy, name aname, optional bool putToGrid) {
4103   //if (mapy >= tilesHeight) { writeln("OOM: ", aname, "; ty=", mapy, "; hgt=", tilesHeight); }
4104   if (mapx < 0 || mapx >= tilesWidth || mapy < 0 || mapy >= tilesHeight) return none;
4106   // if we already have rope tile there, there is no reason to add another one
4107   if (aname == 'oRope') {
4108     if (isRopePlacedAt(mapx*16, mapy*16)) return none;
4109   }
4111   auto tile = CreateMapTile(mapx*16, mapy*16, aname);
4112   if (!tile) return none;
4113   if (!PutSpawnedMapTile(mapx*16, mapy*16, tile, putToGrid!optional)) {
4114     delete tile;
4115     tile = none;
4116   }
4118   return tile;
4122 final MapTile MakeMapTileAtPix (int xpix, int ypix, name aname, optional bool putToGrid) {
4123   // if we already have rope tile there, there is no reason to add another one
4124   if (aname == 'oRope') {
4125     if (isRopePlacedAt(xpix, ypix)) return none;
4126   }
4128   auto tile = CreateMapTile(xpix, ypix, aname);
4129   if (!tile) return none;
4130   if (!PutSpawnedMapTile(xpix, ypix, tile, putToGrid!optional)) {
4131     delete tile;
4132     tile = none;
4133   }
4135   return tile;
4139 final MapTile MakeMapRopeTileAt (int x0, int y0) {
4140   // if we already have rope tile there, there is no reason to add another one
4141   if (isRopePlacedAt(x0, y0)) return none;
4143   auto tile = CreateMapTile(x0, y0, 'oRope');
4144   if (!PutSpawnedMapTile(x0, y0, tile, putToGrid:true)) {
4145     delete tile;
4146     tile = none;
4147   }
4149   return tile;
4153 // ////////////////////////////////////////////////////////////////////////// //
4154 final MapBackTile CreateBgTile (name sprName, optional int atx0, optional int aty0, optional int aw, optional int ah) {
4155   BackTileImage img = bgtileStore[sprName];
4156   auto res = SpawnObject(MapBackTile);
4157   res.global = global;
4158   res.level = self;
4159   res.bgt = img;
4160   res.bgtName = sprName;
4161   if (specified_atx0) res.tx0 = atx0;
4162   if (specified_aty0) res.ty0 = aty0;
4163   if (specified_aw) res.w = aw;
4164   if (specified_ah) res.h = ah;
4165   if (!res.initialize()) FatalError("cannot create background tile with image '%n'", sprName);
4166   return res;
4170 // ////////////////////////////////////////////////////////////////////////// //
4172 background The background asset from which the new tile will be extracted.
4173 left The x coordinate of the left of the new tile, relative to the background asset's top left corner.
4174 top The y coordinate of the top of the new tile, relative to the background assets top left corner.
4175 width The width of the tile.
4176 height The height of the tile.
4177 x The x position in the room to place the tile.
4178 y The y position in the room to place the tile.
4179 depth The depth at which to place the tile.
4181 final void MakeMapBackTile (name bgname, int left, int top, int width, int height, int x, int y, int depth) {
4182   if (width < 1 || height < 1 || !bgname) return;
4183   auto bgt = bgtileStore[bgname];
4184   if (!bgt) FatalError("cannot load background '%n'", bgname);
4185   MapBackTile bt = SpawnObject(MapBackTile);
4186   bt.global = global;
4187   bt.level = self;
4188   bt.objName = bgname;
4189   bt.bgt = bgt;
4190   bt.bgtName = bgname;
4191   bt.fltx = x;
4192   bt.flty = y;
4193   bt.tx0 = left;
4194   bt.ty0 = top;
4195   bt.w = width;
4196   bt.h = height;
4197   bt.depth = depth;
4198   // find a place for it
4199   if (!backtiles) {
4200     backtiles = bt;
4201     return;
4202   }
4203   // back tiles with the highest depth should come first
4204   MapBackTile ct = backtiles, cprev = none;
4205   while (ct && depth <= ct.depth) { cprev = ct; ct = ct.next; }
4206   // insert before ct
4207   if (cprev) {
4208     bt.next = cprev.next;
4209     cprev.next = bt;
4210   } else {
4211     bt.next = backtiles;
4212     backtiles = bt;
4213   }
4217 // ////////////////////////////////////////////////////////////////////////// //
4218 final MapObject SpawnMapObjectWithClass (class!MapObject oclass) {
4219   if (!oclass) return none;
4221   MapObject obj = SpawnObject(oclass);
4222   if (!obj) FatalError("cannot create map object '%n'", GetClassName(oclass));
4224   //if (aname == 'oShells' || aname == 'oShellSingle' || aname == 'oRopePile') writeln("*** CREATED '", aname, "'");
4226   obj.global = global;
4227   obj.level = self;
4228   obj.objId = ++lastUsedObjectId;
4230   return obj;
4234 final MapObject SpawnMapObject (name aname) {
4235   if (!aname) return none;
4236   auto res = SpawnMapObjectWithClass(findGameObjectClassByName(aname));
4237   if (res && !res.objType) res.objType = aname; // just in case
4238   return res;
4242 final MapObject PutSpawnedMapObject (int x, int y, MapObject obj) {
4243   if (!obj /*|| obj.global || obj.level*/) return none; // oops
4245   obj.fltx = x;
4246   obj.flty = y;
4247   if (!obj.initialize()) { delete obj; return none; } // not fatal
4249   insertObject(obj);
4251   return obj;
4255 final MapObject MakeMapObject (int x, int y, name aname) {
4256   MapObject obj = SpawnMapObject(aname);
4257   obj = PutSpawnedMapObject(x, y, obj);
4258   return obj;
4262 // ////////////////////////////////////////////////////////////////////////// //
4263 int winCutSceneTimer = -1;
4264 int winVolcanoTimer = -1;
4265 int winCutScenePhase = 0;
4266 int winSceneDrawStatus = 0;
4267 int winMoneyCount = 0;
4268 int winTime;
4269 bool winFadeOut = false;
4270 int winFadeLevel = 0;
4271 int winCutsceneSkip = 0; // 1: waiting for pay release; 2: pay released, do skip
4272 bool winCutsceneSwitchToNext = false;
4275 void startWinCutscene () {
4276   global.hasParachute = false;
4277   shakeLeft = 0;
4278   winCutsceneSwitchToNext = false;
4279   winCutsceneSkip = 0;
4280   isKeyPressed(GameConfig::Key.Pay);
4281   isKeyReleased(GameConfig::Key.Pay);
4283   auto olddel = ImmediateDelete;
4284   ImmediateDelete = false;
4285   clearWholeLevel();
4287   createEnd1Room();
4288   fixWallTiles();
4289   addBackgroundGfxDetails();
4291   levBGImgName = 'bgCave';
4292   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
4294   blockWaterChecking = true;
4295   fixLiquidTop();
4296   cleanDeadTiles();
4298   ImmediateDelete = olddel;
4299   CollectGarbage(true); // destroy delayed objects too
4301   if (dumpGridStats) objGrid.dumpStats();
4303   playerExited = false; // just in case
4304   playerExitDoor = none;
4306   osdClear();
4308   setupGhostTime();
4309   global.stopMusic();
4311   inWinCutscene = 1;
4312   winCutSceneTimer = -1;
4313   winCutScenePhase = 0;
4315   /+
4316   if (global.config.gameMode != GameConfig::GameMode.Vanilla) {
4317     if (global.config.bizarre) {
4318       global.yasmScore = 1;
4319       global.config.bizarrePlusTitle = true;
4320     }
4322     array!MapTile toReplace;
4323     forEachTile(delegate bool (MapTile t) {
4324       if (t.objType == 'oGTemple' ||
4325           t.objType == 'oIce' ||
4326           t.objType == 'oDark' ||
4327           t.objType == 'oBrick' ||
4328           t.objType == 'oLush')
4329       {
4330         toReplace[$] = t;
4331       }
4332       return false;
4333     });
4335     foreach (MapTile t; miscTileGrid.allObjects()) {
4336       if (t.objType == 'oGTemple' ||
4337           t.objType == 'oIce' ||
4338           t.objType == 'oDark' ||
4339           t.objType == 'oBrick' ||
4340           t.objType == 'oLush')
4341       {
4342         toReplace[$] = t;
4343       }
4344     }
4346     foreach (MapTile t; toReplace) {
4347       if (t.iy < 192) {
4348         t.cleanDeath = true;
4349             if (rand(1,120) == 1) instance_change(oGTemple, false);
4350         else if (rand(1,100) == 1) instance_change(oIce, false);
4351         else if (rand(1,90) == 1) instance_change(oDark, false);
4352         else if (rand(1,80) == 1) instance_change(oBrick, false);
4353         else if (rand(1,70) == 1) instance_change(oLush, false);
4354           }
4355       }
4356       with (oBrick)
4357       {
4358           if (y &lt; 192)
4359           {
4360               cleanDeath = true;
4361               if (rand(1,5) == 1) instance_change(oLush, false);
4362           }
4363       }
4364   }
4365   +/
4366   //!instance_create(0, 0, oBricks);
4368   //shakeToggle = false;
4369   //oPDummy.status = 2;
4371   //timer = 0;
4373   /+
4374   if (global.kaliPunish &gt;= 2) {
4375       instance_create(oPDummy.x, oPDummy.y+2, oBall2);
4376       obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
4377       obj.linkVal = 1;
4378       obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
4379       obj.linkVal = 2;
4380       obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
4381       obj.linkVal = 3;
4382       obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
4383       obj.linkVal = 4;
4384   }
4385   +/
4389 void startWinCutsceneVolcano () {
4390   global.hasParachute = false;
4391   /*
4392   writeln("VOLCANO HOLD 0: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4393   writeln("VOLCANO PICK 0: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4394   */
4396   shakeLeft = 0;
4397   winCutsceneSwitchToNext = false;
4398   auto olddel = ImmediateDelete;
4399   ImmediateDelete = false;
4400   clearWholeLevel();
4402   levBGImgName = '';
4403   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
4405   blockWaterChecking = true;
4407   ImmediateDelete = olddel;
4408   CollectGarbage(true); // destroy delayed objects too
4410   spawnPlayerAt(2*16+8, 11*16+8);
4411   player.dir = MapEntity::Dir.Right;
4413   playerExited = false; // just in case
4414   playerExitDoor = none;
4416   osdClear();
4418   setupGhostTime();
4419   global.stopMusic();
4421   inWinCutscene = 2;
4422   winCutSceneTimer = -1;
4423   winCutScenePhase = 0;
4425   MakeMapTile(0, 0, 'oEnd2BG');
4426   realViewStart.x = 0;
4427   realViewStart.y = 0;
4428   viewStart.x = 0;
4429   viewStart.y = 0;
4431   viewMin.x = 0;
4432   viewMin.y = 0;
4433   viewMax.x = 320;
4434   viewMax.y = 240;
4436   player.dead = false;
4437   player.active = true;
4438   player.visible = false;
4439   player.removeBallAndChain(temp:true);
4440   player.stunned = false;
4441   player.status = MapObject::FALLING;
4442   if (player.holdItem) player.holdItem.visible = false;
4443   player.fltx = 320/2;
4444   player.flty = 0;
4446   /*
4447   writeln("VOLCANO HOLD 1: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4448   writeln("VOLCANO PICK 1: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4449   */
4453 void startWinCutsceneWinFall () {
4454   global.hasParachute = false;
4455   /*
4456   writeln("FALL HOLD 0: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4457   writeln("FALL PICK 0: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4458   */
4460   shakeLeft = 0;
4461   winCutsceneSwitchToNext = false;
4463   auto olddel = ImmediateDelete;
4464   ImmediateDelete = false;
4465   clearWholeLevel();
4467   createEnd3Room();
4468   setMenuTilesVisible(false);
4469   //fixWallTiles();
4470   //addBackgroundGfxDetails();
4472   levBGImgName = '';
4473   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
4475   blockWaterChecking = true;
4476   fixLiquidTop();
4477   cleanDeadTiles();
4479   ImmediateDelete = olddel;
4480   CollectGarbage(true); // destroy delayed objects too
4482   if (dumpGridStats) objGrid.dumpStats();
4484   playerExited = false; // just in case
4485   playerExitDoor = none;
4487   osdClear();
4489   setupGhostTime();
4490   global.stopMusic();
4492   inWinCutscene = 3;
4493   winCutSceneTimer = -1;
4494   winCutScenePhase = 0;
4496   player.dead = false;
4497   player.active = true;
4498   player.visible = false;
4499   player.removeBallAndChain(temp:true);
4500   player.stunned = false;
4501   player.status = MapObject::FALLING;
4502   if (player.holdItem) player.holdItem.visible = false;
4503   player.fltx = 320/2;
4504   player.flty = 0;
4506   winSceneDrawStatus = 0;
4507   winMoneyCount = 0;
4509   winFadeOut = false;
4510   winFadeLevel = 0;
4512   /*
4513   writeln("FALL HOLD 1: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4514   writeln("FALL PICK 1: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4515   */
4519 void setGameOver () {
4520   if (inWinCutscene) {
4521     player.visible = false;
4522     player.removeBallAndChain(temp:true);
4523     if (player.holdItem) player.holdItem.visible = false;
4524   }
4525   player.dead = true;
4526   if (inWinCutscene > 0) {
4527     winFadeOut = true;
4528     winFadeLevel = 255;
4529     winSceneDrawStatus = 8;
4530   }
4534 MapTile findEndPlatTile () {
4535   return forEachTile(delegate bool (MapTile t) { return (t isa MapTileEndPlat); }, castClass:MapTileEndPlat);
4539 MapObject findBigTreasure () {
4540   return forEachObject(delegate bool (MapObject o) { return (o isa MapObjectBigTreasure); }, castClass:MapObjectBigTreasure);
4544 void setMenuTilesVisible (bool vis) {
4545   if (vis) {
4546     forEachTile(delegate bool (MapTile t) {
4547       if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4548         t.invisible = false;
4549       }
4550       return false;
4551     });
4552   } else {
4553     forEachTile(delegate bool (MapTile t) {
4554       if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4555         t.invisible = true;
4556       }
4557       return false;
4558     });
4559   }
4563 void setMenuTilesOnTop () {
4564   forEachTile(delegate bool (MapTile t) {
4565     if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4566       t.depth = 1;
4567     }
4568     return false;
4569   });
4573 void winCutscenePlayerControl (PlayerPawn plr) {
4574   auto payPress = isKeyPressed(GameConfig::Key.Pay);
4575   auto payRelease = isKeyReleased(GameConfig::Key.Pay);
4577   switch (winCutsceneSkip) {
4578     case 0: // nothing was pressed
4579       if (payPress) winCutsceneSkip = 1;
4580       break;
4581     case 1: // waiting for pay release
4582       if (payRelease) winCutsceneSkip = 2;
4583       break;
4584     case 2: // pay released, do skip
4585       setGameOver();
4586       return;
4587   }
4589   // first winning room
4590   if (inWinCutscene == 1) {
4591     if (plr.ix < 448+8) {
4592       plr.kRight = true;
4593       return;
4594     }
4596     // waiting for chest to open
4597     if (winCutScenePhase == 0) {
4598       winCutSceneTimer = 120/2;
4599       winCutScenePhase = 1;
4600       return;
4601     }
4603     // spawn big idol
4604     if (winCutScenePhase == 1) {
4605       if (--winCutSceneTimer == 0) {
4606         winCutScenePhase = 2;
4607         winCutSceneTimer = 20;
4608         forEachObject(delegate bool (MapObject o) {
4609           if (o isa MapObjectBigChest) {
4610             o.setSprite(global.config.gameMode == GameConfig::GameMode.Vanilla ? 'sBigChestOpen' : 'sBigChestOpen2');
4611             auto treasure = MakeMapObject(o.ix, o.iy, 'oBigTreasure');
4612             if (treasure) {
4613               treasure.yVel = -4;
4614               treasure.xVel = -3;
4615               o.playSound('sndClick');
4616               //!!!if (global.config.gameMode != GameConfig::GameMode.Vanilla) scrSprayGems(oBigChest.x+24, oBigChest.y+24);
4617             }
4618           }
4619           return false;
4620         });
4621       }
4622       return;
4623     }
4625     // lava pump wait
4626     if (winCutScenePhase == 2) {
4627       if (--winCutSceneTimer == 0) {
4628         winCutScenePhase = 3;
4629         winCutSceneTimer = 50;
4630       }
4631       return;
4632     }
4634     // lava pump start
4635     if (winCutScenePhase == 3) {
4636       auto ep = findEndPlatTile();
4637       if (ep) MakeMapObject(ep.ix+global.randOther(0, 80), /*ep.iy*/192+32, 'oBurn');
4638       if (--winCutSceneTimer == 0) {
4639         winCutScenePhase = 4;
4640         winCutSceneTimer = 10;
4641         if (ep) MakeMapObject(ep.ix, ep.iy+30, 'oLavaSpray');
4642         scrShake(9999);
4643       }
4644       return;
4645     }
4647     // lava pump first accel
4648     if (winCutScenePhase == 4) {
4649       if (--winCutSceneTimer == 0) {
4650         forEachObject(delegate bool (MapObject o) {
4651           if (o isa MapObjectLavaSpray) o.yAcc = -0.1;
4652           return false;
4653         });
4654       }
4655     }
4657     // lava pump complete
4658     if (winCutScenePhase == 5) {
4659       if (--winCutSceneTimer == 0) {
4660         //if (oLavaSpray) oLavaSpray.yAcc = -0.1;
4661         startWinCutsceneVolcano();
4662       }
4663       return;
4664     }
4665     return;
4666   }
4669   // volcano room
4670   if (inWinCutscene == 2) {
4671     plr.flty = 0;
4673     // initialize
4674     if (winCutScenePhase == 0) {
4675       winCutSceneTimer = 50;
4676       winCutScenePhase = 1;
4677       winVolcanoTimer = 10;
4678       return;
4679     }
4681     if (winVolcanoTimer > 0) {
4682       if (--winVolcanoTimer == 0) {
4683         MakeMapObject(224+global.randOther(0,48), 144+global.randOther(0,8), 'oVolcanoFlame');
4684         winVolcanoTimer = global.randOther(10, 20);
4685       }
4686     }
4688     // plr sil
4689     if (winCutScenePhase == 1) {
4690       if (--winCutSceneTimer == 0) {
4691         winCutSceneTimer = 30;
4692         winCutScenePhase = 2;
4693         auto sil = MakeMapObject(240, 132, 'oPlayerSil');
4694         //sil.xVel = -6;
4695         //sil.yVel = -8;
4696       }
4697       return;
4698     }
4700     // treasure sil
4701     if (winCutScenePhase == 2) {
4702       if (--winCutSceneTimer == 0) {
4703         winCutScenePhase = 3;
4704         auto sil = MakeMapObject(240, 132, 'oTreasureSil');
4705         //sil.xVel = -6;
4706         //sil.yVel = -8;
4707       }
4708       return;
4709     }
4711     return;
4712   }
4714   // winning camel room
4715   if (inWinCutscene == 3) {
4716     //if (!player.holdItem)  writeln("SCENE 3: LOST ITEM!");
4718     if (!plr.visible) plr.flty = -32;
4720     // initialize
4721     if (winCutScenePhase == 0) {
4722       winCutSceneTimer = 50;
4723       winCutScenePhase = 1;
4724       return;
4725     }
4727     // fall sound
4728     if (winCutScenePhase == 1) {
4729       if (--winCutSceneTimer == 0) {
4730         winCutSceneTimer = 50;
4731         winCutScenePhase = 2;
4732         plr.playSound('sndPFall');
4733         plr.visible = true;
4734         plr.active = true;
4735         writeln("MUST BE CHAINED: ", plr.mustBeChained);
4736         if (plr.mustBeChained) {
4737           plr.removeBallAndChain(temp:true);
4738           plr.spawnBallAndChain();
4739         }
4740         /*
4741         writeln("HOLD: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4742         writeln("PICK: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4743         */
4744         if (!player.holdItem && player.pickedItem) player.scrSwitchToPocketItem(forceIfEmpty:false);
4745         if (player.holdItem) {
4746           player.holdItem.visible = true;
4747           player.holdItem.canLiveOutsideOfLevel = true;
4748           writeln("HOLD ITEM: '", GetClassName(player.holdItem.Class), "'");
4749         }
4750         plr.status == MapObject::FALLING;
4751         global.plife += 99; // just in case
4752       }
4753       return;
4754     }
4756     if (winCutScenePhase == 2) {
4757       auto ball = plr.getMyBall();
4758       if (ball && plr.holdItem != ball) {
4759         ball.teleportTo(plr.fltx, plr.flty+8);
4760         ball.yVel = 6;
4761         ball.myGrav = 0.6;
4762       }
4763       if (plr.status == MapObject::STUNNED || plr.stunned) {
4764         //alarm[0] = 70;
4765         //alarm[1] = 50;
4766         //status = GETUP;
4767         auto treasure = MakeMapObject(144+16+8, -32, 'oBigTreasure');
4768         if (treasure) treasure.depth = 1;
4769         winCutScenePhase = 3;
4770         plr.stunTimer = 30;
4771         plr.playSound('sndTFall');
4772       }
4773       return;
4774     }
4776     if (winCutScenePhase == 3) {
4777       if (plr.status != MapObject::STUNNED && !plr.stunned) {
4778         auto bt = findBigTreasure();
4779         if (bt) {
4780           if (bt.yVel == 0) {
4781             //plr.yVel = -4;
4782             //plr.status = MapObject::JUMPING;
4783             plr.kJump = true;
4784             plr.kJumpPressed = true;
4785             winCutScenePhase = 4;
4786             winCutSceneTimer = 50;
4787           }
4788         }
4789       }
4790       return;
4791     }
4793     if (winCutScenePhase == 4) {
4794       if (--winCutSceneTimer == 0) {
4795         setMenuTilesVisible(true);
4796         winCutScenePhase = 5;
4797         winSceneDrawStatus = 1;
4798         global.playMusic('musVictory', loop:false);
4799         winCutSceneTimer = 50;
4800       }
4801       return;
4802     }
4804     if (winCutScenePhase == 5) {
4805       if (winSceneDrawStatus == 3) {
4806         int money = stats.money;
4807         if (winMoneyCount < money) {
4808           if (money-winMoneyCount > 1000) {
4809             winMoneyCount += 1000;
4810           } else if (money-winMoneyCount > 100) {
4811             winMoneyCount += 100;
4812           } else if (money-winMoneyCount > 10) {
4813             winMoneyCount += 10;
4814           } else {
4815             ++winMoneyCount;
4816           }
4817         }
4818         if (winMoneyCount >= money) {
4819           winMoneyCount = money;
4820           ++winSceneDrawStatus;
4821         }
4822         return;
4823       }
4825       if (winSceneDrawStatus == 7) {
4826         winFadeOut = true;
4827         winFadeLevel += 1;
4828         if (winFadeLevel >= 255) {
4829           ++winSceneDrawStatus;
4830           winCutSceneTimer = 30*30;
4831         }
4832         return;
4833       }
4835       if (winSceneDrawStatus == 8) {
4836         if (--winCutSceneTimer == 0) {
4837           setGameOver();
4838         }
4839         return;
4840       }
4842       if (--winCutSceneTimer == 0) {
4843         ++winSceneDrawStatus;
4844         winCutSceneTimer = 50;
4845       }
4846     }
4848     return;
4849   }
4853 // ////////////////////////////////////////////////////////////////////////// //
4854 void renderWinCutsceneOverlay () {
4855   if (inWinCutscene == 3) {
4856     if (winSceneDrawStatus > 0) {
4857       Video.color = 0xff_ff_ff;
4858       sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
4859       //draw_set_color(txtCol);
4860       drawTextAt(64, 32, "YOU MADE IT!");
4862       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4863       if (global.config.gameMode == GameConfig::GameMode.Vanilla) {
4864         Video.color = 0xff_ff_00; //draw_set_color(c_yellow);
4865         drawTextAt(64, 48, "Classic Mode done!");
4866       } else {
4867         Video.color = 0x00_80_80; //draw_set_color(c_teal);
4868         if (global.config.bizarrePlus) drawTextAt(64, 48, "Bizarre Mode Plus done!");
4869         else drawTextAt(64, 48, "Bizarre Mode done!");
4870         //draw_set_color(c_white);
4871       }
4872       if (!global.usedShortcut) {
4873         Video.color = 0xc0_c0_c0; //draw_set_color(c_silver);
4874         drawTextAt(64, 56, "No shortcuts used!");
4875         //draw_set_color(c_yellow);
4876       }
4877     }
4879     if (winSceneDrawStatus > 1) {
4880       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4881       //draw_set_color(txtCol);
4882       Video.color = 0xff_ff_ff;
4883       drawTextAt(64, 64, "FINAL SCORE:");
4884     }
4886     if (winSceneDrawStatus > 2) {
4887       sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
4888       Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4889       drawTextAt(64, 72, va("$%d", winMoneyCount));
4890     }
4892     if (winSceneDrawStatus > 4) {
4893       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4894       Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4895       drawTextAt(64, 96, va("Time: %s", time2str(winTime/30)));
4896       /*
4897       draw_set_color(c_white);
4898       if (s &lt; 10) draw_text(96+24, 96, string(m) + ":0" + string(s));
4899       else draw_text(96+24, 96, string(m) + ":" + string(s));
4900       */
4901     }
4903     if (winSceneDrawStatus > 5) {
4904       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4905       Video.color = 0xff_ff_ff; //draw_set_color(txtCol);
4906       drawTextAt(64, 96+8, "Kills: ");
4907       Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4908       drawTextAt(96+24, 96+8, va("%s", stats.countKills()));
4909     }
4911     if (winSceneDrawStatus > 6) {
4912       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4913       Video.color = 0xff_ff_ff; //draw_set_color(txtCol);
4914       drawTextAt(64, 96+16, "Saves: ");
4915       Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4916       drawTextAt(96+24, 96+16, va("%s", stats.damselsSaved));
4917     }
4919     if (winFadeOut) {
4920       Video.color = (255-clamp(winFadeLevel, 0, 255))<<24;
4921       Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
4922     }
4924     if (winSceneDrawStatus == 8) {
4925       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4926       Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4927       string lastString;
4928       if (global.config.gameMode == GameConfig::GameMode.Vanilla) {
4929         Video.color = 0xff_ff_00; //draw_set_color(c_yellow);
4930         lastString = "YOU SHALL BE REMEMBERED AS A HERO.";
4931       } else {
4932         Video.color = 0x00_ff_ff;
4933         if (global.config.bizarrePlus) lastString = "ANOTHER LEGENDARY ADVENTURE!";
4934         else lastString = "YOUR DISCOVERIES WILL BE CELEBRATED!";
4935       }
4936       auto strLen = lastString.length*8;
4937       int n = 320-strLen;
4938       n = trunc(ceil(n/2.0));
4939       drawTextAt(n, 116, lastString);
4940     }
4941   }
4945 // ////////////////////////////////////////////////////////////////////////// //
4946 #include "roomTitle.vc"
4947 #include "roomTrans1.vc"
4948 #include "roomTrans2.vc"
4949 #include "roomTrans3.vc"
4950 #include "roomTrans4.vc"
4951 #include "roomOlmec.vc"
4952 #include "roomEnd.vc"
4953 #include "roomTutorial.vc"
4954 #include "roomScores.vc"
4955 #include "roomStars.vc"
4956 #include "roomSun.vc"
4957 #include "roomMoon.vc"
4960 // ////////////////////////////////////////////////////////////////////////// //
4961 #include "packages/Generator/loadRoomGens.vc"
4962 #include "packages/Generator/loadEntityGens.vc"