slightly bounced arrow should not hit anyone
[k8vacspelynky.git] / GameLevel.vc
blob684cc4e1cb21ad11555dc4be9aaf437dac5343d6
1 /**********************************************************************************
2  * Copyright (c) 2008, 2009 Derek Yu and Mossmouth, LLC
3  * Copyright (c) 2018, Ketmar Dark
4  *
5  * This file is part of Spelunky.
6  *
7  * You can redistribute and/or modify Spelunky, including its source code, under
8  * the terms of the Spelunky User License.
9  *
10  * Spelunky is distributed in the hope that it will be entertaining and useful,
11  * but WITHOUT WARRANTY.  Please see the Spelunky User License for more details.
12  *
13  * The Spelunky User License should be available in "Game .Information", which
14  * can be found in the Resource Explorer, or as an external file called COPYING.
15  * If not, please obtain a new copy of Spelunky from <http://spelunkyworld.com/>
16  *
17  **********************************************************************************/
18 // this is the level we're playing in, with all objects and tiles
19 class GameLevel : Object;
21 //#define EXPERIMENTAL_RENDER_CACHE
23 const float FrameTime = 1.0f/30.0f;
25 const int dumpGridStats = true;
27 struct IVec2D {
28   int x, y;
31 // in tiles
32 //enum NormalTilesWidth = LevelGen::LevelWidth*RoomGen::Width+2;
33 //enum NormalTilesHeight = LevelGen::LevelHeight*RoomGen::Height+2;
35 enum MaxTilesWidth = 64;
36 enum MaxTilesHeight = 64;
38 GameGlobal global;
39 transient GameStats stats;
40 transient SpriteStore sprStore;
41 transient BackTileStore bgtileStore;
42 transient BackTileImage levBGImg;
43 name levBGImgName;
44 LevelGen lg;
45 transient name lastMusicName;
46 //RoomGen[LevelGen::LevelWidth, LevelGen::LevelHeight] rooms; // moved to levelgen
48 transient float accumTime;
49 transient bool gamePaused = false;
50 transient bool gameShowHelp = false;
51 transient int gameHelpScreen = 0;
52 const int MaxGameHelpScreen = 2;
53 transient bool checkWater;
54 transient int liquidTileCount; // cached
55 /*transient*/ int damselSaved;
57 // hud efffects
58 transient int xmoney;
59 transient int collectCounter;
60 /*transient*/ int levelMoneyStart;
62 // all movable (thinkable) map objects
63 EntityGrid objGrid; // monsters, items and tiles
65 MapBackTile backtiles;
66 bool blockWaterChecking;
68 int inWinCutscene;
69 int inIntroCutscene;
70 bool cameFromIntroRoom; // for title screen
72 LevelGen::RType[MaxTilesWidth, MaxTilesHeight] roomType;
74 enum LevelKind {
75   Normal,
76   Transition,
77   Title,
78   Intro,
79   Tutorial,
80   Scores,
81   Stars,
82   Sun,
83   Moon,
84   //Final,
86 LevelKind levelKind = LevelKind.Normal;
88 array!MapTile allEnters;
89 array!MapTile allExits;
92 int startRoomX, startRoomY;
93 int endRoomX, endRoomY;
95 PlayerPawn player;
96 transient bool playerExited;
97 transient MapEntity playerExitDoor;
98 transient bool disablePlayerThink = false;
99 transient int maxPlayingTime; // in seconds
100 int levelStartTime;
101 int levelEndTime;
103 int ghostTimeLeft;
104 int musicFadeTimer;
105 bool ghostSpawned; // to speed up some checks
106 bool resetBMCOG = false;
107 int udjatAlarm;
110 // FPS, i.e. incremented by 30 in one second
111 int time; // in frames
112 int lastUsedObjectId;
113 transient int lastRenderTime = -1;
114 transient int pausedTime;
116 MapEntity deadItemsHead;
118 // screen shake variables
119 int shakeLeft;
120 IVec2D shakeOfs;
121 IVec2D shakeDir;
123 // set this before calling `fixCamera()`
124 // dimensions should be real, not scaled up/down
125 transient int viewWidth, viewHeight;
126 //transient int viewOffsetX, viewOffsetY;
128 // room bounds, not scaled
129 IVec2D viewMin, viewMax;
131 // for Olmec level cinematics
132 IVec2D cameraSlideToDest;
133 IVec2D cameraSlideToCurr;
134 IVec2D cameraSlideToSpeed; // !0: slide
135 int cameraSlideToPlayer;
136 // `fixCamera()` will set the following
137 // coordinates will be real too (with scale applied)
138 // shake is not applied
139 transient IVec2D viewStart; // with `player.viewOffset`
140 private transient IVec2D realViewStart; // without `player.viewOffset`
142 transient int framesProcessedFromLastClear;
144 transient int BuildYear;
145 transient int BuildMonth;
146 transient int BuildDay;
147 transient int BuildHour;
148 transient int BuildMin;
149 transient string BuildDateString;
152 final string getBuildDateString () {
153   if (!BuildYear) return BuildDateString;
154   if (BuildDateString) return BuildDateString;
155   BuildDateString = va("%d-%02d-%02d %02d:%02d", BuildYear, BuildMonth, BuildDay, BuildHour, BuildMin);
156   return BuildDateString;
160 final void cameraSlideToPoint (int dx, int dy, int speedx, int speedy) {
161   cameraSlideToPlayer = 0;
162   cameraSlideToDest.x = dx;
163   cameraSlideToDest.y = dy;
164   cameraSlideToSpeed.x = abs(speedx);
165   cameraSlideToSpeed.y = abs(speedy);
166   cameraSlideToCurr.x = cameraCurrX;
167   cameraSlideToCurr.y = cameraCurrY;
171 final void cameraReturnToPlayer () {
172   if (!cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y)) {
173     cameraSlideToCurr.x = cameraCurrX;
174     cameraSlideToCurr.y = cameraCurrY;
175     if (cameraSlideToSpeed.x && abs(cameraSlideToSpeed.x) < 8) cameraSlideToSpeed.x = 8;
176     if (cameraSlideToSpeed.y && abs(cameraSlideToSpeed.y) < 8) cameraSlideToSpeed.y = 8;
177     cameraSlideToPlayer = 1;
178   }
182 // if `frameSkip` is `true`, there are more frames waiting
183 // (i.e. you may skip rendering and such)
184 transient void delegate (bool frameSkip) onBeforeFrame;
185 transient void delegate (bool frameSkip) onAfterFrame;
187 transient void delegate () onCameraTeleported;
189 transient void delegate () onLevelExitedCB;
191 // this will be called in-between frames, and
192 // `frameTime` is [0..1)
193 transient void delegate (float frameTime) onInterFrame;
195 final int bizRoomStyle { get { return (lg ? lg.bizRoomStyle : 0); } }
198 final bool isNormalLevel () { return (levelKind == LevelKind.Normal); }
199 final bool isTitleRoom () { return (levelKind == LevelKind.Title); }
200 final bool isTutorialRoom () { return (levelKind == LevelKind.Tutorial); }
201 final bool isTransitionRoom () { return (levelKind == LevelKind.Transition); }
202 final bool isIntroRoom () { return (levelKind == LevelKind.Transition); }
205 bool isHUDEnabled () {
206   if (inWinCutscene) return false;
207   if (inIntroCutscene) return false;
208   if (lg.finalBossLevel) return true;
209   if (isNormalLevel()) return true;
210   return false;
214 // ////////////////////////////////////////////////////////////////////////// //
215 // stats
216 void addDeath (name aname) { if (isNormalLevel()) stats.addDeath(aname); }
218 int starsKills;
219 int sunScore;
220 int moonScore;
221 int moonTimer;
223 void addKill (name aname, optional bool telefrag) {
224        if (isNormalLevel()) stats.addKill(aname, telefrag!optional);
225   else if (aname == 'Shopkeeper' && levelKind == LevelKind.Stars) { ++stats.starsKills; ++starsKills; }
228 void addCollect (name aname, optional int amount) { if (isNormalLevel()) stats.addCollect(aname, amount!optional); }
230 void addDamselSaved () { if (isNormalLevel()) stats.addDamselSaved(); }
231 void addIdolStolen () { if (isNormalLevel()) stats.addIdolStolen(); }
232 void addIdolConverted () { if (isNormalLevel()) stats.addIdolConverted(); }
233 void addCrystalIdolStolen () { if (isNormalLevel()) stats.addCrystalIdolStolen(); }
234 void addCrystalIdolConverted () { if (isNormalLevel()) stats.addCrystalIdolConverted(); }
235 void addGhostSummoned () { if (isNormalLevel()) stats.addGhostSummoned(); }
238 // ////////////////////////////////////////////////////////////////////////// //
239 static final string time2str (int time) {
240   int secs = time%60; time /= 60;
241   int mins = time%60; time /= 60;
242   int hours = time%24; time /= 24;
243   int days = time;
244   if (days) return va("%d DAYS, %d:%02d:%02d", days, hours, mins, secs);
245   if (hours) return va("%d:%02d:%02d", hours, mins, secs);
246   return va("%02d:%02d", mins, secs);
250 // ////////////////////////////////////////////////////////////////////////// //
251 final int tilesWidth () { return lg.levelRoomWidth*RoomGen::Width+2; }
252 final int tilesHeight () { return (lg.finalBossLevel ? 55 : lg.levelRoomHeight*RoomGen::Height+2); }
255 // ////////////////////////////////////////////////////////////////////////// //
256 protected void resetGameInternal () {
257   if (player) player.removeBallAndChain();
258   resetBMCOG = false;
259   inWinCutscene = 0;
260   //inIntroCutscene = 0;
261   shakeLeft = 0;
262   udjatAlarm = 0;
263   starsKills = 0;
264   sunScore = 0;
265   moonScore = 0;
266   moonTimer = 0;
267   damselSaved = 0;
268   xmoney = 0;
269   collectCounter = 0;
270   levelMoneyStart = 0;
271   if (player) {
272     player.removeBallAndChain();
273     auto hi = player.holdItem;
274     player.holdItem = none;
275     if (hi) hi.instanceRemove();
276     hi = player.pickedItem;
277     player.pickedItem = none;
278     if (hi) hi.instanceRemove();
279   }
280   time = 0;
281   lastRenderTime = -1;
282   levelStartTime = 0;
283   levelEndTime = 0;
284   global.resetGame();
285   stats.clearGameTotals();
289 // this won't generate a level yet
290 void restartGame () {
291   resetGameInternal();
292   if (global.startMoney > 0) stats.setMoneyCheat();
293   stats.setMoney(global.startMoney);
294   levelKind = LevelKind.Normal;
298 // complement function to `restart game`
299 void generateNormalLevel () {
300   generateLevel();
301   centerViewAtPlayer();
305 void restartTitle () {
306   resetGameInternal();
307   stats.setMoney(0);
308   createSpecialLevel(LevelKind.Title, &createTitleRoom, 'musTitle');
309   global.plife = 9999;
310   global.bombs = 0;
311   global.rope = 0;
312   global.arrows = 0;
313   global.sgammo = 0;
317 void restartIntro () {
318   resetGameInternal();
319   stats.setMoney(0);
320   createSpecialLevel(LevelKind.Intro, &createIntroRoom, '');
321   global.plife = 9999;
322   global.bombs = 0;
323   global.rope = 1;
324   global.arrows = 0;
325   global.sgammo = 0;
329 void restartTutorial () {
330   resetGameInternal();
331   stats.setMoney(0);
332   createSpecialLevel(LevelKind.Tutorial, &createTutorialRoom, 'musCave');
333   global.plife = 4;
334   global.bombs = 0;
335   global.rope = 4;
336   global.arrows = 0;
337   global.sgammo = 0;
341 void restartScores () {
342   resetGameInternal();
343   stats.setMoney(0);
344   createSpecialLevel(LevelKind.Scores, &createScoresRoom, 'musTitle');
345   global.plife = 4;
346   global.bombs = 0;
347   global.rope = 0;
348   global.arrows = 0;
349   global.sgammo = 0;
353 void restartStarsRoom () {
354   resetGameInternal();
355   stats.setMoney(0);
356   createSpecialLevel(LevelKind.Stars, &createStarsRoom, '');
357   global.plife = 8;
358   global.bombs = 0;
359   global.rope = 0;
360   global.arrows = 0;
361   global.sgammo = 0;
365 void restartSunRoom () {
366   resetGameInternal();
367   stats.setMoney(0);
368   createSpecialLevel(LevelKind.Sun, &createSunRoom, '');
369   global.plife = 8;
370   global.bombs = 0;
371   global.rope = 0;
372   global.arrows = 0;
373   global.sgammo = 0;
377 void restartMoonRoom () {
378   resetGameInternal();
379   stats.setMoney(0);
380   createSpecialLevel(LevelKind.Moon, &createMoonRoom, '');
381   global.plife = 8;
382   global.bombs = 0;
383   global.rope = 0;
384   global.arrows = 100;
385   global.sgammo = 0;
389 // ////////////////////////////////////////////////////////////////////////// //
390 // generate angry shopkeeper at exit if murderer or thief
391 void generateAngryShopkeepers () {
392   if (global.murderer || global.thiefLevel > 0) {
393     foreach (MapTile e; allExits) {
394       auto obj = MonsterShopkeeper(MakeMapObject(e.ix, e.iy, 'oShopkeeper'));
395       if (obj) {
396         obj.style = 'Bounty Hunter';
397         obj.status = MapObject::PATROL;
398       }
399     }
400   }
404 // ////////////////////////////////////////////////////////////////////////// //
405 final void resetRoomBounds () {
406   viewMin.x = 0;
407   viewMin.y = 0;
408   viewMax.x = tilesWidth*16;
409   viewMax.y = tilesHeight*16;
410   // Great Lake is bottomless (nope)
411   //if (global.lake == 1) viewMax.y -= 16;
412   //writeln("ROOM BOUNDS: (", viewMax.x, "x", viewMax.y, ")");
416 final void setRoomBounds (int x0, int y0, int x1, int y1) {
417   viewMin.x = x0;
418   viewMin.y = y0;
419   viewMax.x = x1+16;
420   viewMax.y = y1+16;
424 // ////////////////////////////////////////////////////////////////////////// //
425 struct OSDMessage {
426   string msg;
427   float timeout; // seconds
428   float starttime; // for active
429   bool active; // true: timeout is `GetTickCount()` dismissing time
432 array!OSDMessage msglist; // [0]: current one
434 struct OSDMessageTalk {
435   string msg;
436   float timeout; // seconds;
437   float starttime; // for active
438   bool active; // true: timeout is `GetTickCount()` dismissing time
439   bool shopOnly; // true: timeout when player exited the shop
440   int hiColor1; // -1: default
441   int hiColor2; // -1: default
444 array!OSDMessageTalk msgtalklist; // [0]: current one
447 private final void osdCheckTimeouts () {
448   auto stt = GetTickCount();
449   while (msglist.length) {
450     if (!msglist[0].msg) { msglist.remove(0); continue; }
451     if (!msglist[0].active) {
452       msglist[0].active = true;
453       msglist[0].starttime = stt;
454     }
455     if (msglist[0].starttime+msglist[0].timeout >= stt) break;
456     msglist.remove(0);
457   }
458   if (msgtalklist.length) {
459     bool inshop = isInShop(player.ix/16, player.iy/16);
460     while (msgtalklist.length) {
461       if (!msgtalklist[0].msg) { msgtalklist.remove(0); continue; }
462       if (msgtalklist[0].shopOnly) {
463         if (inshop == msgtalklist[0].active) {
464           msgtalklist[0].active = !inshop;
465           if (!inshop) msgtalklist[0].starttime = stt;
466         }
467       } else {
468         if (!msgtalklist[0].active) {
469           msgtalklist[0].active = true;
470           msgtalklist[0].starttime = stt;
471         }
472       }
473       if (!msgtalklist[0].active) break;
474       if (msgtalklist[0].starttime+msgtalklist[0].timeout >= stt) break;
475       msgtalklist.remove(0);
476     }
477   }
481 final bool osdHasMessage () {
482   osdCheckTimeouts();
483   return (msglist.length > 0);
487 final string osdGetMessage (out float timeLeft, out float timeStart) {
488   osdCheckTimeouts();
489   if (msglist.length == 0) { timeLeft = 0; return ""; }
490   auto stt = GetTickCount();
491   timeStart = msglist[0].starttime;
492   timeLeft = fmax(0, msglist[0].starttime+msglist[0].timeout-stt);
493   return msglist[0].msg;
497 final string osdGetTalkMessage (optional out int hiColor1, optional out int hiColor2) {
498   osdCheckTimeouts();
499   if (msgtalklist.length == 0) return "";
500   hiColor1 = msgtalklist[0].hiColor1;
501   hiColor2 = msgtalklist[0].hiColor2;
502   return msgtalklist[0].msg;
506 final void osdClear () {
507   msglist.clear();
508   msgtalklist.clear();
512 final void osdMessage (string msg, optional float timeout, optional bool urgent) {
513   if (!msg) return;
514   msg = global.expandString(msg);
515   if (!specified_timeout) timeout = 3.33;
516   // special message for shops
517   if (timeout == -666) {
518     if (!msg) return;
519     if (msglist.length && msglist[0].msg == msg) return;
520     if (msglist.length == 0 || msglist[0].msg != msg) {
521       osdClear();
522       msglist.length += 1;
523       msglist[0].msg = msg;
524     }
525     msglist[0].active = false;
526     msglist[0].timeout = 3.33;
527     osdCheckTimeouts();
528     return;
529   }
530   if (timeout < 0.1) return;
531   timeout = fmax(1.0, timeout);
532   //writeln("OSD: ", msg);
533   // find existing one, and bring it to the top
534   int oldidx = 0;
535   for (; oldidx < msglist.length; ++oldidx) {
536     if (msglist[oldidx].msg == msg) break; // i found her!
537   }
538   // duplicate?
539   if (oldidx < msglist.length) {
540     // yeah, move duplicate to the top
541     msglist[oldidx].timeout = fmax(timeout, msglist[oldidx].timeout);
542     msglist[oldidx].active = false;
543     if (urgent && oldidx != 0) {
544       timeout = msglist[oldidx].timeout;
545       msglist.remove(oldidx);
546       msglist.insert(0);
547       msglist[0].msg = msg;
548       msglist[0].timeout = timeout;
549       msglist[0].active = false;
550     }
551   } else if (urgent) {
552     msglist.insert(0);
553     msglist[0].msg = msg;
554     msglist[0].timeout = timeout;
555     msglist[0].active = false;
556   } else {
557     // new one
558     msglist.length += 1;
559     msglist[$-1].msg = msg;
560     msglist[$-1].timeout = timeout;
561     msglist[$-1].active = false;
562   }
563   osdCheckTimeouts();
567 void osdMessageTalk (string msg, optional bool replace, optional float timeout, optional bool inShopOnly,
568                      optional int hiColor1, optional int hiColor2)
570   if (!specified_timeout) timeout = 3.33;
571   if (!specified_inShopOnly) inShopOnly = true;
572   if (!specified_hiColor1) hiColor1 = -1;
573   if (!specified_hiColor2) hiColor2 = -1;
574   msg = global.expandString(msg);
575   if (replace) {
576     if (!msg) { msgtalklist.clear(); return; }
577     if (msgtalklist.length && msgtalklist[0].msg == msg) {
578       while (msgtalklist.length > 1) msgtalklist.remove(1);
579       msgtalklist[$-1].timeout = timeout;
580       msgtalklist[$-1].shopOnly = inShopOnly;
581     } else {
582       if (msgtalklist.length) msgtalklist.clear();
583       msgtalklist.length += 1;
584       msgtalklist[$-1].msg = msg;
585       msgtalklist[$-1].timeout = timeout;
586       msgtalklist[$-1].active = false;
587       msgtalklist[$-1].shopOnly = inShopOnly;
588       msgtalklist[$-1].hiColor1 = hiColor1;
589       msgtalklist[$-1].hiColor2 = hiColor2;
590     }
591   } else {
592     if (!msg) return;
593     bool found = false;
594     foreach (auto midx, ref auto mnfo; msgtalklist) {
595       if (mnfo.msg == msg) {
596         mnfo.timeout = timeout;
597         mnfo.shopOnly = inShopOnly;
598         found = true;
599       }
600     }
601     if (!found) {
602       msgtalklist.length += 1;
603       msgtalklist[$-1].msg = msg;
604       msgtalklist[$-1].timeout = timeout;
605       msgtalklist[$-1].active = false;
606       msgtalklist[$-1].hiColor1 = hiColor1;
607       msgtalklist[$-1].hiColor2 = hiColor2;
608     }
609   }
610   osdCheckTimeouts();
614 // ////////////////////////////////////////////////////////////////////////// //
615 void setup (GameGlobal aGlobal, SpriteStore aSprStore, BackTileStore aBGTileStore) {
616   global = aGlobal;
617   sprStore = aSprStore;
618   bgtileStore = aBGTileStore;
620   lg = SpawnObject(LevelGen);
621   lg.global = global;
622   lg.level = self;
624   objGrid = SpawnObject(EntityGrid);
625   objGrid.setup(0, 0, MaxTilesWidth*16, MaxTilesHeight*16);
629 // stores should be set
630 void onLoaded () {
631   checkWater = true;
632   liquidTileCount = 0;
633   levBGImg = bgtileStore[levBGImgName];
634   foreach (MapEntity o; objGrid.allObjects()) {
635     o.onLoaded();
636     auto t = MapTile(o);
637     if (t && (t.lava || t.water)) ++liquidTileCount;
638   }
639   for (MapBackTile bt = backtiles; bt; bt = bt.next) bt.onLoaded();
640   if (player) player.onLoaded();
641   //FIXME
642   if (msglist.length) {
643     msglist[0].active = false;
644     msglist[0].timeout = 0.200;
645     osdCheckTimeouts();
646   }
647   lastMusicName = (lg ? lg.musicName : '');
648   global.setMusicPitch(1.0);
649   if (lg && lg.musicName) global.playMusic(lg.musicName); else global.stopMusic();
653 // ////////////////////////////////////////////////////////////////////////// //
654 void pickedSpectacles () {
655   foreach (MapTile t; objGrid.allObjectsSafe(MapTile)) t.onGotSpectacles();
659 // ////////////////////////////////////////////////////////////////////////// //
660 #include "rgentile.vc"
661 #include "rgenobj.vc"
664 void onLevelExited () {
665   if (playerExitDoor isa TitleTileXTitle) {
666     playerExitDoor = none;
667     restartTitle();
668     return;
669   }
670   // title
671   if (isTitleRoom() || levelKind == LevelKind.Scores) {
672     if (playerExitDoor) processTitleExit(playerExitDoor);
673     playerExitDoor = none;
674     return;
675   }
676   if (isTutorialRoom()) {
677     playerExitDoor = none;
678     restartGame();
679     global.currLevel = 1;
680     generateNormalLevel();
681     return;
682   }
683   // challenges
684   if (levelKind == LevelKind.Stars || levelKind == LevelKind.Sun || levelKind == LevelKind.Moon) {
685     playerExitDoor = none;
686     levelEndTime = time;
687     if (onLevelExitedCB) onLevelExitedCB();
688     restartTitle();
689     return;
690   }
691   // normal level
692   if (isNormalLevel()) {
693     stats.maxLevelComplete = max(stats.maxLevelComplete, global.currLevel);
694     levelEndTime = time;
695     if (playerExitDoor) {
696       if (playerExitDoor.objType == 'oXGold') {
697         writeln("exiting to City Of Gold");
698         global.cityOfGold = true;
699         //!global.currLevel += 1;
700       } else if (playerExitDoor.objType == 'oXMarket') {
701         writeln("exiting to Black Market");
702         global.genBlackMarket = true;
703         //!global.currLevel += 1;
704       }
705     }
706   }
707   if (onLevelExitedCB) onLevelExitedCB();
708   //
709   playerExitDoor = none;
710   if (levelKind == LevelKind.Transition) {
711     if (global.thiefLevel > 0) global.thiefLevel -= 1;
712     if (global.alienCraft) ++global.alienCraft;
713     if (global.yetiLair) ++global.yetiLair;
714     if (global.lake) ++global.lake;
715     if (global.cityOfGold) ++global.cityOfGold;
716     //orig dbg:if (global.currLevel == 1) global.currLevel += firstLevelSkip; else global.currLevel += levelSkip;
717     /+
718     if (!global.blackMarket && !global.cityOfGold /*&& !global.yetiLair*/) {
719       global.currLevel += 1;
720     }
721     +/
722     ++global.currLevel;
723     generateLevel();
724   } else {
725     // < 20 seconds per level: looks like a speedrun
726     global.noDarkLevel = (levelEndTime > levelStartTime && levelEndTime-levelStartTime < 20*30);
727     if (lg.finalBossLevel) {
728       winTime = time;
729       ++stats.gamesWon;
730       // add money for big idol
731       player.addScore(50000);
732       stats.gameOver();
733       startWinCutscene();
734     } else {
735       generateTransitionLevel();
736     }
737   }
738   //centerViewAtPlayer();
742 void onOlmecDead (MapObject o) {
743   writeln("*** OLMEC IS DEAD!");
744   foreach (MapTile t; allExits) {
745     if (t.exit) {
746       t.openExit();
747       auto st = checkTileAtPoint(t.ix+8, t.iy+16);
748       if (!st) {
749         st = MakeMapTile(t.ix/16, t.iy/16+1, 'oTemple');
750         st.ore = 0;
751       }
752       st.invincible = true;
753     }
754   }
758 void generateLevelMessages () {
759   writeln("LEVEL NUMBER: ", global.currLevel);
760   if (global.darkLevel) {
761     if (global.hasCrown) {
762        osdMessage("THE HEDJET SHINES BRIGHTLY.");
763        global.darkLevel = false;
764     } else if (global.config.scumDarkness < 2) {
765       osdMessage("I CAN'T SEE A THING!\nI'D BETTER USE THESE FLARES!");
766     }
767   }
769   if (global.blackMarket) osdMessage("WELCOME TO THE BLACK MARKET!");
771   if (global.cemetary) osdMessage("THE DEAD ARE RESTLESS!");
772   if (global.lake == 1) osdMessage("I CAN HEAR RUSHING WATER...");
774   if (global.snakePit) osdMessage("I HEAR SNAKES... I HATE SNAKES!");
775   if (global.yetiLair == 1) osdMessage("IT SMELLS LIKE WET FUR IN HERE!");
776   if (global.alienCraft == 1) osdMessage("THERE'S A PSYCHIC PRESENCE HERE!");
777   if (global.cityOfGold == 1) {
778     if (true /*hasOlmec()*/) osdMessage("IT'S THE LEGENDARY CITY OF GOLD!");
779   }
781   if (global.sacrificePit) osdMessage("I CAN HEAR PRAYERS TO KALI!");
785 final MapObject debugSpawnObjectWithClass (class!MapObject oclass, optional bool playerDir) {
786   if (!oclass) return none;
787   int dx = 0, dy = 0;
788   bool canLeft = !isSolidAtPoint(player.ix-8, player.iy);
789   bool canRight = !isSolidAtPoint(player.ix+16, player.iy);
790   if (!canLeft && !canRight) return none;
791   if (canLeft && canRight) {
792     if (playerDir) {
793       dx = (player.dir == MapObject::Dir.Left ? -16 : 16);
794     } else {
795       dx = 16;
796     }
797   } else {
798     dx = (canLeft ? -16 : 16);
799   }
800   auto obj = SpawnMapObjectWithClass(oclass);
801   if (obj isa MapEnemy) {
802     dx -= 8;
803     dy -= (obj isa MonsterDamsel ? 2 : 8);
804   }
805   if (obj) PutSpawnedMapObject(player.ix+dx, player.iy+dy, obj);
806   return obj;
810 final MapObject debugSpawnObject (name aname) {
811   if (!aname) return none;
812   return debugSpawnObjectWithClass(findGameObjectClassByName(aname));
816 void createSpecialLevel (LevelKind kind, scope void delegate () creator, name amusic) {
817   global.darkLevel = false;
818   udjatAlarm = 0;
819   xmoney = 0;
820   collectCounter = 0;
821   global.resetStartingItems();
823   global.setMusicPitch(1.0);
824   levelKind = kind;
826   auto olddel = ImmediateDelete;
827   ImmediateDelete = false;
828   clearWholeLevel();
830   creator();
832   setMenuTilesOnTop();
834   fixWallTiles();
835   addBackgroundGfxDetails();
836   //levBGImgName = 'bgCave';
837   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
839   blockWaterChecking = true;
840   fixLiquidTop();
841   cleanDeadTiles();
843   ImmediateDelete = olddel;
844   CollectGarbage(true); // destroy delayed objects too
846   if (dumpGridStats) objGrid.dumpStats();
848   playerExited = false; // just in case
849   playerExitDoor = none;
851   osdClear();
853   setupGhostTime();
854   lg.musicName = amusic;
855   lastMusicName = amusic;
856   global.setMusicPitch(1.0);
857   if (amusic) global.playMusic(lg.musicName); else global.stopMusic();
861 void createTitleLevel () {
862   createSpecialLevel(LevelKind.Title, &createTitleRoom, 'musTitle');
866 void createTutorialLevel () {
867   createSpecialLevel(LevelKind.Tutorial, &createTutorialRoom, 'musCave');
868   global.plife = 4;
869   global.bombs = 0;
870   global.rope = 4;
871   global.arrows = 0;
872   global.sgammo = 0;
876 // `global.currLevel` is the new level
877 void generateTransitionLevel () {
878   global.darkLevel = false;
879   udjatAlarm = 0;
880   xmoney = 0;
881   collectCounter = 0;
883   resetTransitionOverlay();
885   global.setMusicPitch(1.0);
886   switch (global.config.transitionMusicMode) {
887     case GameConfig::MusicMode.Silent: global.stopMusic(); break;
888     case GameConfig::MusicMode.Restart: global.restartMusic(); break;
889     case GameConfig::MusicMode.DontTouch: break;
890   }
892   levelKind = LevelKind.Transition;
894   auto olddel = ImmediateDelete;
895   ImmediateDelete = false;
896   clearWholeLevel();
898        if (global.currLevel < 4) createTrans1Room();
899   else if (global.currLevel == 4) createTrans1xRoom();
900   else if (global.currLevel < 8) createTrans2Room();
901   else if (global.currLevel == 8) createTrans2xRoom();
902   else if (global.currLevel < 12) createTrans3Room();
903   else if (global.currLevel == 12) createTrans3xRoom();
904   else if (global.currLevel < 16) createTrans4Room();
905   else if (global.currLevel == 16) createTrans4Room();
906   else createTrans1Room(); //???
908   setMenuTilesOnTop();
910   fixWallTiles();
911   addBackgroundGfxDetails();
912   //levBGImgName = 'bgCave';
913   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
915   blockWaterChecking = true;
916   fixLiquidTop();
917   cleanDeadTiles();
919   if (damselSaved > 0) {
920     // this is special "damsel ready to kiss you" object, not a heart
921     MakeMapObject(176+8, 176+8, 'oDamselKiss');
922     global.plife += damselSaved; // if player skipped transition cutscene
923     damselSaved = 0;
924   }
926   ImmediateDelete = olddel;
927   CollectGarbage(true); // destroy delayed objects too
929   if (dumpGridStats) objGrid.dumpStats();
931   playerExited = false; // just in case
932   playerExitDoor = none;
934   osdClear();
936   setupGhostTime();
937   //global.playMusic(lg.musicName);
941 void generateLevel () {
942   levelStartTime = time;
943   levelEndTime = time;
945   udjatAlarm = 0;
946   if (resetBMCOG) {
947     resetBMCOG = false;
948     global.genBlackMarket = false;
949   }
951   global.setMusicPitch(1.0);
952   stats.clearLevelTotals();
954   levelKind = LevelKind.Normal;
955   lg.generate();
956   //lg.dump();
958   resetRoomBounds();
960   lg.generateRooms();
961   //writeln("tw:", tilesWidth, "; th:", tilesHeight);
963   auto olddel = ImmediateDelete;
964   ImmediateDelete = false;
965   clearWholeLevel();
967   if (lg.finalBossLevel) {
968     blockWaterChecking = true;
969     createOlmecRoom();
970   }
972   // if transition cutscene was skipped...
973   global.plife += max(0, damselSaved); // if player skipped transition cutscene
974   damselSaved = 0;
976   // generate tiles
977   startRoomX = lg.startRoomX;
978   startRoomY = lg.startRoomY;
979   endRoomX = lg.endRoomX;
980   endRoomY = lg.endRoomY;
981   addBackgroundGfxDetails();
982   foreach (int y; 0..tilesHeight) {
983     foreach (int x; 0..tilesWidth) {
984       lg.genTileAt(x, y);
985     }
986   }
987   fixWallTiles();
989   levBGImgName = lg.bgImgName;
990   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
992   if (global.allowAngryShopkeepers) generateAngryShopkeepers();
994   lg.generateEntities();
996   // add box of flares to dark level
997   if (global.darkLevel && allEnters.length) {
998     auto enter = allEnters[0];
999     int x = enter.ix, y = enter.iy;
1000          if (!isSolidAtPoint(x-16, y)) MakeMapObject(x-16+8, y+8, 'oFlareCrate');
1001     else if (!isSolidAtPoint(x+16, y)) MakeMapObject(x+16+8, y+8, 'oFlareCrate');
1002     else MakeMapObject(x+8, y+8, 'oFlareCrate');
1003   }
1005   //scrGenerateEntities();
1006   //foreach (; 0..2) scrGenerateEntities();
1008   writeln(objGrid.countObjects, " alive objects inserted");
1009   writeln(countBackTiles, " background tiles inserted");
1011   if (!player) FatalError("player pawn is not spawned");
1013   if (lg.finalBossLevel) {
1014     blockWaterChecking = true;
1015   } else {
1016     blockWaterChecking = false;
1017   }
1018   fixLiquidTop();
1019   cleanDeadTiles();
1021   ImmediateDelete = olddel;
1022   CollectGarbage(true); // destroy delayed objects too
1024   if (dumpGridStats) objGrid.dumpStats();
1026   playerExited = false; // just in case
1027   playerExitDoor = none;
1029   levelMoneyStart = stats.money;
1031   osdClear();
1032   generateLevelMessages();
1034   xmoney = 0;
1035   collectCounter = 0;
1037   //writeln("!<", lastMusicName, ">:<", lg.musicName, ">");
1038   global.setMusicPitch(1.0);
1039   if (lastMusicName != lg.musicName) {
1040     global.playMusic(lg.musicName);
1041   } else {
1042     //writeln("MM: ", global.config.nextLevelMusicMode);
1043     switch (global.config.nextLevelMusicMode) {
1044       case GameConfig::MusicMode.Silent: global.stopMusic(); break; // the thing that should not be
1045       case GameConfig::MusicMode.Restart: global.restartMusic(); break;
1046       case GameConfig::MusicMode.DontTouch:
1047         if (global.config.transitionMusicMode == GameConfig::MusicMode.Silent) {
1048           global.playMusic(lg.musicName);
1049         }
1050         break;
1051     }
1052   }
1053   lastMusicName = lg.musicName;
1054   //global.playMusic(lg.musicName);
1056   setupGhostTime();
1057   if (global.cityOfGold == 1 || global.genBlackMarket) resetBMCOG = true;
1059   if (global.cityOfGold == 1) {
1060     lg.mapSprite = 'sMapTemple';
1061     lg.mapTitle = "City of Gold";
1062   } else if (global.blackMarket) {
1063     lg.mapSprite = 'sMapJungle';
1064     lg.mapTitle = "Black Market";
1065   }
1069 // ////////////////////////////////////////////////////////////////////////// //
1070 int currKeys, nextKeys;
1071 int pressedKeysQ, releasedKeysQ;
1072 int keysPressed, keysReleased = -1;
1075 struct SavedKeyState {
1076   int currKeys, nextKeys;
1077   int pressedKeysQ, releasedKeysQ;
1078   int keysPressed, keysReleased;
1079   // for session
1080   int roomSeed, otherSeed;
1084 // for saving/replaying
1085 final void keysSaveState (out SavedKeyState ks) {
1086   ks.currKeys = currKeys;
1087   ks.nextKeys = nextKeys;
1088   ks.pressedKeysQ = pressedKeysQ;
1089   ks.releasedKeysQ = releasedKeysQ;
1090   ks.keysPressed = keysPressed;
1091   ks.keysReleased = keysReleased;
1094 // for saving/replaying
1095 final void keysRestoreState (const ref SavedKeyState ks) {
1096   currKeys = ks.currKeys;
1097   nextKeys = ks.nextKeys;
1098   pressedKeysQ = ks.pressedKeysQ;
1099   releasedKeysQ = ks.releasedKeysQ;
1100   keysPressed = ks.keysPressed;
1101   keysReleased = ks.keysReleased;
1105 final void keysNextFrame () {
1106   currKeys = nextKeys;
1110 final void clearKeys () {
1111   currKeys = 0;
1112   nextKeys = 0;
1113   pressedKeysQ = 0;
1114   releasedKeysQ = 0;
1115   keysPressed = 0;
1116   keysReleased = -1;
1120 final void onKey (int code, bool down) {
1121   if (!code) return;
1122   if (down) {
1123     currKeys |= code;
1124     nextKeys |= code;
1125     if (keysReleased&code) {
1126       keysPressed |= code;
1127       keysReleased &= ~code;
1128       pressedKeysQ |= code;
1129     }
1130   } else {
1131     nextKeys &= ~code;
1132     if (keysPressed&code) {
1133       keysReleased |= code;
1134       keysPressed &= ~code;
1135       releasedKeysQ |= code;
1136     }
1137   }
1140 final bool isKeyDown (int code) {
1141   return !!(currKeys&code);
1144 final bool isKeyPressed (int code) {
1145   bool res = !!(pressedKeysQ&code);
1146   pressedKeysQ &= ~code;
1147   return res;
1150 final bool isKeyReleased (int code) {
1151   bool res = !!(releasedKeysQ&code);
1152   releasedKeysQ &= ~code;
1153   return res;
1157 final void clearKeysPressRelease () {
1158   keysPressed = default.keysPressed;
1159   keysReleased = default.keysReleased;
1160   pressedKeysQ = default.pressedKeysQ;
1161   releasedKeysQ = default.releasedKeysQ;
1162   currKeys = 0;
1163   nextKeys = 0;
1167 // ////////////////////////////////////////////////////////////////////////// //
1168 final void registerEnter (MapTile t) {
1169   if (!t) return;
1170   allEnters[$] = t;
1171   return;
1175 final void registerExit (MapTile t) {
1176   if (!t) return;
1177   allExits[$] = t;
1178   return;
1182 final bool isYAtEntranceRow (int py) {
1183   py /= 16;
1184   foreach (MapTile t; allEnters) if (t.iy == py) return true;
1185   return false;
1189 final int calcNearestEnterDist (int px, int py) {
1190   if (allEnters.length == 0) return int.max;
1191   int curdistsq = int.max;
1192   foreach (MapTile t; allEnters) {
1193     int xc = px-t.xCenter, yc = py-t.yCenter;
1194     int distsq = xc*xc+yc*yc;
1195     if (distsq < curdistsq) curdistsq = distsq;
1196   }
1197   return round(sqrt(curdistsq));
1201 final int calcNearestExitDist (int px, int py) {
1202   if (allExits.length == 0) return int.max;
1203   int curdistsq = int.max;
1204   foreach (MapTile t; allExits) {
1205     int xc = px-t.xCenter, yc = py-t.yCenter;
1206     int distsq = xc*xc+yc*yc;
1207     if (distsq < curdistsq) curdistsq = distsq;
1208   }
1209   return round(sqrt(curdistsq));
1213 // ////////////////////////////////////////////////////////////////////////// //
1214 final void clearForTransition () {
1215   auto olddel = ImmediateDelete;
1216   ImmediateDelete = false;
1217   clearWholeLevel();
1218   ImmediateDelete = olddel;
1219   CollectGarbage(true); // destroy delayed objects too
1220   global.darkLevel = false;
1224 // ////////////////////////////////////////////////////////////////////////// //
1225 final int countBackTiles () {
1226   int res = 0;
1227   for (MapBackTile bt = backtiles; bt; bt = bt.next) ++res;
1228   return res;
1232 final void clearWholeLevel () {
1233   allEnters.clear();
1234   allExits.clear();
1236   // don't kill objects the player is holding
1237   if (player) {
1238     if (player.pickedItem isa ItemBall) {
1239       player.pickedItem.instanceRemove();
1240       player.pickedItem = none;
1241     }
1242     if (player.pickedItem && player.pickedItem.grid) {
1243       player.pickedItem.grid.remove(player.pickedItem.gridId);
1244       writeln("secured pocket item '", GetClassName(player.pickedItem.Class), "'");
1245     }
1246     if (player.holdItem isa ItemBall) {
1247       player.removeBallAndChain(temp:true);
1248       if (player.holdItem) player.holdItem.instanceRemove();
1249       player.holdItem = none;
1250     }
1251     if (player.holdItem && player.holdItem.grid) {
1252       player.holdItem.grid.remove(player.holdItem.gridId);
1253       writeln("secured held item '", GetClassName(player.holdItem.Class), "'");
1254     }
1255     writeln("secured ball; mustBeChained=", player.mustBeChained, "; wasHoldingBall=", player.wasHoldingBall);
1256   }
1258   int count = objGrid.countObjects();
1259   if (dumpGridStats) { if (objGrid.getFirstObjectCID()) objGrid.dumpStats(); }
1260   objGrid.removeAllObjects(true); // and destroy
1261   if (count > 0) writeln(count, " objects destroyed");
1263   lastUsedObjectId = 0;
1264   accumTime = 0;
1265   //!time = 0;
1266   lastRenderTime = -1;
1267   liquidTileCount = 0;
1268   checkWater = false;
1270   while (backtiles) {
1271     MapBackTile t = backtiles;
1272     backtiles = t.next;
1273     delete t;
1274   }
1276   levBGImg = none;
1277   framesProcessedFromLastClear = 0;
1281 final void insertObject (MapEntity o) {
1282   if (!o) return;
1283   if (o.grid) FatalError("cannot put object into level twice");
1284   objGrid.insert(o);
1288 final void spawnPlayerAt (int x, int y) {
1289   // if we have no player, spawn new one
1290   // otherwise this just a level transition, so simply reposition him
1291   if (!player) {
1292     // don't add player to object list, as it has very separate processing anyway
1293     player = SpawnObject(PlayerPawn);
1294     player.global = global;
1295     player.level = self;
1296     if (!player.initialize()) {
1297       delete player;
1298       FatalError("something is wrong with player initialization");
1299       return;
1300     }
1301   }
1302   player.fltx = x;
1303   player.flty = y;
1304   player.saveInterpData();
1305   player.resurrect();
1306   if (player.mustBeChained || global.config.scumBallAndChain) {
1307     writeln("*** spawning ball and chain");
1308     player.spawnBallAndChain(levelStart:true);
1309   }
1310   playerExited = false;
1311   playerExitDoor = none;
1312   if (global.config.startWithKapala) global.hasKapala = true;
1313   centerViewAtPlayer();
1314   // reinsert player items into grid
1315   if (player.pickedItem) objGrid.insert(player.pickedItem);
1316   if (player.holdItem) objGrid.insert(player.holdItem);
1317   //writeln("player spawned; active=", player.active);
1318   player.scrSwitchToPocketItem(forceIfEmpty:false);
1322 final void teleportPlayerTo (int x, int y) {
1323   if (player) {
1324     player.fltx = x;
1325     player.flty = y;
1326     player.saveInterpData();
1327   }
1331 final void resurrectPlayer () {
1332   if (player) player.resurrect();
1333   playerExited = false;
1334   playerExitDoor = none;
1338 // ////////////////////////////////////////////////////////////////////////// //
1339 final void scrShake (int duration) {
1340   if (shakeLeft == 0) {
1341     shakeOfs.x = 0;
1342     shakeOfs.y = 0;
1343     shakeDir.x = 0;
1344     shakeDir.y = 0;
1345   }
1346   shakeLeft = max(shakeLeft, duration);
1351 // ////////////////////////////////////////////////////////////////////////// //
1352 enum SCAnger {
1353   TileDestroyed,
1354   ItemStolen, // including damsel, lol
1355   CrapsCheated,
1356   BombDropped,
1357   DamselWhipped,
1360 // checks for dead, agnered, distance, etc. should be already done
1361 protected void doAngerShopkeeper (MonsterShopkeeper shp, SCAnger reason, ref bool messaged,
1362                                   int maxdist, MapEntity offender)
1364   if (!shp || shp.dead || shp.angered) return;
1365   if (offender.distanceToEntityCenter(shp) > maxdist) return;
1367   shp.status = MapObject::ATTACK;
1368   string msg;
1369   if (global.murderer) {
1370     msg = "~YOU'LL PAY FOR YOUR CRIMES!~";
1371   } else {
1372     switch (reason) {
1373       case SCAnger.TileDestroyed: msg = "~DIE, YOU VANDAL!~"; break;
1374       case SCAnger.ItemStolen: msg = "~COME BACK HERE, THIEF!~"; break;
1375       case SCAnger.CrapsCheated: msg = "~DIE, CHEATER!~"; break;
1376       case SCAnger.BombDropped: msg = "~TERRORIST!~"; break;
1377       case SCAnger.DamselWhipped: msg = "~HEY, ONLY I CAN DO THAT!~"; break;
1378       default: "~NOW I'M REALLY STEAMED!~"; break;
1379     }
1380   }
1382   writeln("shopkeeper angered; reason=", reason, "; maxdist=", maxdist, "; msg=\"", msg, "\"");
1383   if (!messaged) {
1384     messaged = true;
1385     if (msg) osdMessageTalk(msg, replace:true, inShopOnly:false, hiColor1:0xff_00_00);
1386     global.thiefLevel += (global.thiefLevel > 0 ? 3 : 2);
1387   }
1391 // make the nearest shopkeeper angry. RAWR!
1392 void scrShopkeeperAnger (SCAnger reason, optional int maxdist, optional MapEntity offender) {
1393   bool messaged = false;
1394   maxdist = clamp(maxdist, 96, 100000);
1395   if (!offender) offender = player;
1396   if (maxdist == 100000) {
1397     foreach (MonsterShopkeeper shp; objGrid.allObjects(MonsterShopkeeper)) {
1398       doAngerShopkeeper(shp, reason, messaged, maxdist, offender);
1399     }
1400   } else {
1401     foreach (MonsterShopkeeper shp; objGrid.inRectPix(offender.xCenter-maxdist-128, offender.yCenter-maxdist-128, (maxdist+128)*2, (maxdist+128)*2, precise:false, castClass:MonsterShopkeeper)) {
1402       doAngerShopkeeper(shp, reason, messaged, maxdist, offender);
1403     }
1404   }
1408 final MapObject findCrapsPrize () {
1409   foreach (MapObject o; objGrid.allObjects(MapObject)) {
1410     if (!o.spectral && o.inDiceHouse) return o;
1411   }
1412   return none;
1416 // ////////////////////////////////////////////////////////////////////////// //
1417 // 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.
1418 // note: idols moved by monkeys will have false `stolenIdol`
1419 void scrTriggerIdolAltar (bool stolenIdol) {
1420   ObjTikiCurse res = none;
1421   int curdistsq = int.max;
1422   int px = player.xCenter, py = player.yCenter;
1423   foreach (MapObject o; objGrid.allObjects(MapObject)) {
1424     auto tcr = ObjTikiCurse(o);
1425     if (!tcr) continue;
1426     if (tcr.activated) continue;
1427     int xc = px-tcr.xCenter, yc = py-tcr.yCenter;
1428     int distsq = xc*xc+yc*yc;
1429     if (distsq < curdistsq) {
1430       res = tcr;
1431       curdistsq = distsq;
1432     }
1433   }
1434   if (res) res.activate(stolenIdol);
1438 // ////////////////////////////////////////////////////////////////////////// //
1439 void setupGhostTime () {
1440   musicFadeTimer = -1;
1441   ghostSpawned = false;
1443   // there is no ghost on the first level
1444   if (inWinCutscene || inIntroCutscene || !isNormalLevel() || lg.finalBossLevel ||
1445       (!global.config.ghostAtFirstLevel && global.currLevel == 1))
1446   {
1447     ghostTimeLeft = -1;
1448     global.setMusicPitch(1.0);
1449     return;
1450   }
1452   if (global.config.scumGhost < 0) {
1453     // instant
1454     ghostTimeLeft = 1;
1455     osdMessage("A GHASTLY VOICE CALLS YOUR NAME!\nLET'S GET OUT OF HERE!", 6.66, urgent:true);
1456     return;
1457   }
1459   if (global.config.scumGhost == 0) {
1460     // never
1461     ghostTimeLeft = -1;
1462     return;
1463   }
1465   // randomizes time until ghost appears once time limit is reached
1466   // global.ghostRandom (true/false) if extra time is randomized - loaded from yasm.cfg. if false, use 30 second default
1467   // ghostTimeLeft (time in seconds * 1000) for currently generated level
1469   if (global.config.ghostRandom) {
1470     auto tMin = global.randOther(10, 30); // min(global.plife*10, 30);
1471     auto tMax = max(tMin+global.randOther(10, 30), min(global.config.scumGhost, 300)); // 5 minutes max
1472     auto tTime = global.randOther(tMin, tMax);
1473     if (tTime <= 0) tTime = round(tMax/2.0);
1474     ghostTimeLeft = tTime;
1475   } else {
1476     ghostTimeLeft = global.config.scumGhost; // 5 minutes max
1477   }
1479   ghostTimeLeft += max(0, global.config.ghostExtraTime);
1481   ghostTimeLeft *= 30; // seconds -> frames
1482   //global.ghostShowTime
1486 void spawnGhost () {
1487   addGhostSummoned();
1488   ghostSpawned = true;
1489   ghostTimeLeft = -1;
1491   int vwdt = (viewMax.x-viewMin.x);
1492   int vhgt = (viewMax.y-viewMin.y);
1494   int gx, gy;
1496   if (player.ix < viewMin.x+vwdt/2) {
1497     // player is in the left side
1498     gx = viewMin.x+vwdt/2+vwdt/4;
1499   } else {
1500     // player is in the right side
1501     gx = viewMin.x+vwdt/4;
1502   }
1504   if (player.iy < viewMin.y+vhgt/2) {
1505     // player is in the left side
1506     gy = viewMin.y+vhgt/2+vhgt/4;
1507   } else {
1508     // player is in the right side
1509     gy = viewMin.y+vhgt/4;
1510   }
1512   writeln("spawning ghost at tile (", gx/16, ",", gy/16, "); player is at tile (", player.ix/16, ",", player.iy/16, ")");
1514   MakeMapObject(gx, gy, 'oGhost');
1516   /*
1517     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);
1518     else instance_create(view_xview[0]-32, view_yview[0]+floor(view_hview[0]/2), oGhost);
1519     global.ghostExists = true;
1520   */
1524 void thinkFrameGameGhost () {
1525   if (player.dead) return;
1526   if (!isNormalLevel()) return; // just in case
1528   if (ghostTimeLeft < 0) {
1529     // turned off
1530     if (musicFadeTimer > 0) {
1531       musicFadeTimer = -1;
1532       global.setMusicPitch(1.0);
1533     }
1534     return;
1535   }
1537   if (musicFadeTimer >= 0) {
1538     ++musicFadeTimer;
1539     if (musicFadeTimer > 0 && musicFadeTimer <= 100*3) {
1540       float pitch = 1.0-(0.00226/3.0)*musicFadeTimer;
1541       //writeln("MFT: ", musicFadeTimer, "; pitch=", pitch);
1542       global.setMusicPitch(pitch);
1543     }
1544   }
1546   if (ghostTimeLeft == 0) {
1547     // she is already here!
1548     return;
1549   }
1551   // no ghost if we have a crown
1552   if (global.hasCrown) {
1553     ghostTimeLeft = -1;
1554     return;
1555   }
1557   // if she was already spawned, don't do it again
1558   if (ghostSpawned) {
1559     ghostTimeLeft = 0;
1560     return;
1561   }
1563   if (--ghostTimeLeft != 0) {
1564     // warning
1565     if (global.config.ghostExtraTime > 0) {
1566       if (ghostTimeLeft == global.config.ghostExtraTime*30) {
1567         osdMessage("A CHILL RUNS UP YOUR SPINE...\nLET'S GET OUT OF HERE!", 6.66, urgent:true);
1568       }
1569       if (musicFadeTimer < 0 && ghostTimeLeft <= global.config.ghostExtraTime*30) {
1570         musicFadeTimer = 0;
1571       }
1572     }
1573     return;
1574   }
1576   // spawn her
1577   if (player.isExitingSprite) {
1578     // no reason to spawn her, we're leaving
1579     ghostTimeLeft = -1;
1580     return;
1581   }
1583   spawnGhost();
1587 void thinkFrameGame () {
1588   thinkFrameGameGhost();
1589   // udjat eye blinking
1590   if (global.hasUdjatEye && player) {
1591     foreach (MapTile t; allExits) {
1592       if (t isa MapTileBlackMarketDoor) {
1593         auto dm = int(player.distanceToEntity(t));
1594         if (dm < 4) dm = 4;
1595         if (udjatAlarm < 1 || dm < udjatAlarm) udjatAlarm = dm;
1596       }
1597     }
1598   } else {
1599     global.udjatBlink = false;
1600     udjatAlarm = 0;
1601   }
1602   if (udjatAlarm > 0) {
1603     if (--udjatAlarm == 0) {
1604       global.udjatBlink = !global.udjatBlink;
1605       if (global.hasUdjatEye && player) {
1606         player.playSound(global.udjatBlink ? 'sndBlink1' : 'sndBlink2');
1607       }
1608     }
1609   }
1610   switch (levelKind) {
1611     case LevelKind.Stars: thinkFrameGameStars(); break;
1612     case LevelKind.Sun: thinkFrameGameSun(); break;
1613     case LevelKind.Moon: thinkFrameGameMoon(); break;
1614     case LevelKind.Transition: thinkFrameTransition(); break;
1615     case LevelKind.Intro: thinkFrameIntro(); break;
1616   }
1620 // ////////////////////////////////////////////////////////////////////////// //
1621 private final bool isWaterTileCB (MapTile t) {
1622   return (t && t.visible && t.water);
1626 private final bool isLavaTileCB (MapTile t) {
1627   return (t && t.visible && t.lava);
1631 // ////////////////////////////////////////////////////////////////////////// //
1632 const int GreatLakeStartTileY = 28;
1635 final void fillGreatLake () {
1636   if (global.lake == 1) {
1637     foreach (int y; GreatLakeStartTileY..tilesHeight) {
1638       foreach (int x; 0..tilesWidth) {
1639         auto t = checkTileAtPoint(x*16, y*16, delegate bool (MapTile t) {
1640           if (t.spectral || !t.visible || t.invisible || t.moveable) return false;
1641           return true;
1642         });
1643         if (!t) {
1644           t = MakeMapTile(x, y, 'oWaterSwim');
1645           if (!t) continue;
1646         }
1647         if (t.water) {
1648           t.setSprite(y == GreatLakeStartTileY ? 'sWaterTop' : 'sWater');
1649         } else if (t.lava) {
1650           t.setSprite(y == GreatLakeStartTileY ? 'sLavaTop' : 'sLava');
1651         }
1652       }
1653     }
1654   }
1658 // called once after level generation
1659 final void fixLiquidTop () {
1660   if (global.lake == 1) fillGreatLake();
1662   liquidTileCount = 0;
1663   foreach (MapTile t; objGrid.allObjects(MapTile)) {
1664     if (!t.water && !t.lava) continue;
1666     ++liquidTileCount;
1667     //writeln("fixing water tile(", GetClassName(t.Class), "):'", t.objName, "' (water=", t.water, "; lava=", t.lava, "); lqc=", liquidTileCount);
1669     //if (global.lake == 1) continue; // it is done in `fillGreatLake()`
1671     if (!checkTileAtPoint(t.ix+8, t.iy-8, (t.lava ? &isLavaTileCB : &isWaterTileCB))) {
1672       t.setSprite(t.lava ? 'sLavaTop' : 'sWaterTop');
1673     } else {
1674       // don't do this, it will destroy seaweed
1675       //t.setSprite(t.lava ? 'sLava' : 'sWater');
1676       auto spr = t.getSprite();
1677            if (!spr) t.setSprite(t.lava ? 'sLava' : 'sWater');
1678       else if (spr.Name == 'sLavaTop') t.setSprite('sLava');
1679       else if (spr.Name == 'sWaterTop') t.setSprite('sWater');
1680     }
1681   }
1682   //writeln("liquid tiles count: ", liquidTileCount);
1686 // ////////////////////////////////////////////////////////////////////////// //
1687 transient MapTile curWaterTile;
1688 transient bool curWaterTileCheckHitsLava;
1689 transient bool curWaterTileCheckHitsSolidOrWater; // only for `checkWaterOrSolidTilePartialCB`
1690 transient int curWaterTileLastHDir;
1691 transient ubyte[16, 16] curWaterOccupied;
1692 transient int curWaterOccupiedCount;
1693 transient int curWaterTileCheckX0, curWaterTileCheckY0;
1696 private final void clearCurWaterCheckState () {
1697   curWaterTileCheckHitsLava = false;
1698   curWaterOccupiedCount = 0;
1699   foreach (auto idx; 0..16*16) curWaterOccupied[idx] = 0;
1703 private final bool checkWaterOrSolidTileCB (MapTile t) {
1704   if (t == curWaterTile) return false;
1705   if (t.lava && curWaterTile.water) {
1706     curWaterTileCheckHitsLava = true;
1707     return true;
1708   }
1709   if (t.ix%16 != 0 || t.iy%16 != 0) {
1710     if (t.water || t.solid) {
1711       // fill occupied array
1712       //FIXME: optimize this
1713       if (curWaterOccupiedCount < 16*16) {
1714         foreach (auto dy; t.y0..t.y1+1) {
1715           foreach (auto dx; t.x0..t.x1+1) {
1716             int sx = dx-curWaterTileCheckX0;
1717             int sy = dy-curWaterTileCheckY0;
1718             if (sx >= 0 && sx <= 16 && sy >= 0 && sy <= 15 && !curWaterOccupied[sx, sy]) {
1719               curWaterOccupied[sx, sy] = 1;
1720               ++curWaterOccupiedCount;
1721             }
1722           }
1723         }
1724       }
1725     }
1726     return false; // need to check for lava
1727   }
1728   if (t.water || t.solid || t.lava) {
1729     curWaterOccupiedCount = 16*16;
1730     if (t.water && curWaterTile.lava) t.instanceRemove();
1731   }
1732   return false; // need to check for lava
1736 private final bool checkWaterOrSolidTilePartialCB (MapTile t) {
1737   if (t == curWaterTile) return false;
1738   if (t.lava && curWaterTile.water) {
1739     //writeln("!!!!!!!!");
1740     curWaterTileCheckHitsLava = true;
1741     return true;
1742   }
1743   if (t.water || t.solid || t.lava) {
1744     //writeln("*********");
1745     curWaterTileCheckHitsSolidOrWater = true;
1746     if (t.water && curWaterTile.lava) t.instanceRemove();
1747   }
1748   return false; // need to check for lava
1752 private final bool isFullyOccupiedAtTilePos (int tileX, int tileY) {
1753   clearCurWaterCheckState();
1754   curWaterTileCheckX0 = tileX*16;
1755   curWaterTileCheckY0 = tileY*16;
1756   checkTilesInRect(tileX*16, tileY*16, 16, 16, &checkWaterOrSolidTileCB);
1757   return (curWaterTileCheckHitsLava || curWaterOccupiedCount == 16*16);
1761 private final bool isAtLeastPartiallyOccupiedAtTilePos (int tileX, int tileY) {
1762   curWaterTileCheckHitsLava = false;
1763   curWaterTileCheckHitsSolidOrWater = false;
1764   checkTilesInRect(tileX*16, tileY*16, 16, 16, &checkWaterOrSolidTilePartialCB);
1765   return (curWaterTileCheckHitsSolidOrWater || curWaterTileCheckHitsLava);
1769 private final bool waterCanReachGroundHoleInDir (MapTile wtile, int dx) {
1770   if (dx == 0) return false; // just in case
1771   dx = sign(dx);
1772   int x = wtile.ix/16, y = wtile.iy/16;
1773   x += dx;
1774   while (x >= 0 && x < tilesWidth) {
1775     if (!isAtLeastPartiallyOccupiedAtTilePos(x, y+1)) return true;
1776     if (isAtLeastPartiallyOccupiedAtTilePos(x, y)) return false;
1777     x += dx;
1778   }
1779   return false;
1783 // returns `true` if this tile must be removed
1784 private final bool checkWaterFlow (MapTile wtile) {
1785   if (global.lake == 1) {
1786     if (wtile.iy >= GreatLakeStartTileY*16) return false; // lake tile, don't touch
1787     if (wtile.iy >= GreatLakeStartTileY*16-16) return true; // remove it, so it won't stack on a lake
1788   }
1790   if (wtile.ix%16 != 0 || wtile.iy%16 != 0) return true; // sanity check
1792   curWaterTile = wtile;
1793   curWaterTileLastHDir = 0; // never moved to the side
1795   bool wasMoved = false;
1797   for (;;) {
1798     int tileX = wtile.ix/16, tileY = wtile.iy/16;
1800     // out of level?
1801     if (tileY >= tilesHeight) return true;
1803     // check if we can fall down
1804     auto canFall = !isAtLeastPartiallyOccupiedAtTilePos(tileX, tileY+1);
1805     // disappear if can fall in lava
1806     if (wtile.water && curWaterTileCheckHitsLava) {
1807       //!writeln(wtile.objId, ": LAVA HIT DOWN");
1808       return true;
1809     }
1810     if (wasMoved) {
1811       // fake, so caller will not start removing tiles
1812       if (canFall) wtile.waterMovedDown = true;
1813       break;
1814     }
1815     // can move down?
1816     if (canFall) {
1817       // move down
1818       //!writeln(wtile.objId, ": GOING DOWN");
1819       curWaterTileLastHDir = 0;
1820       wtile.iy = wtile.iy+16;
1821       wasMoved = true;
1822       wtile.waterMovedDown = true;
1823       continue;
1824     }
1826     bool canMoveLeft = (curWaterTileLastHDir > 0 ? false : !isAtLeastPartiallyOccupiedAtTilePos(tileX-1, tileY));
1827     // disappear if near lava
1828     if (wtile.water && curWaterTileCheckHitsLava) {
1829       //!writeln(wtile.objId, ": LAVA HIT LEFT");
1830       return true;
1831     }
1833     bool canMoveRight = (curWaterTileLastHDir < 0 ? false : !isAtLeastPartiallyOccupiedAtTilePos(tileX+1, tileY));
1834     // disappear if near lava
1835     if (wtile.water && curWaterTileCheckHitsLava) {
1836       //!writeln(wtile.objId, ": LAVA HIT RIGHT");
1837       return true;
1838     }
1840     if (!canMoveLeft && !canMoveRight) {
1841       // do final checks
1842       //!if (wasMove) writeln(wtile.objId, ": NO MORE MOVES");
1843       break;
1844     }
1846     if (canMoveLeft && canMoveRight) {
1847       // choose random direction
1848       //!writeln(wtile.objId, ": CHOOSING RANDOM HDIR");
1849       // actually, choose direction that leads to hole in a ground
1850       if (waterCanReachGroundHoleInDir(wtile, -1)) {
1851         // can reach hole at the left side
1852         if (waterCanReachGroundHoleInDir(wtile, 1)) {
1853           // can reach hole at the right side, choose at random
1854           if (global.randOther(0, 1)) canMoveRight = false; else canMoveLeft = false;
1855         } else {
1856           // move left
1857           canMoveRight = false;
1858         }
1859       } else {
1860         // can't reach hole at the left side
1861         if (waterCanReachGroundHoleInDir(wtile, 1)) {
1862           // can reach hole at the right side, choose at random
1863           canMoveLeft = false;
1864         } else {
1865           // no holes at any side, choose at random
1866           if (global.randOther(0, 1)) canMoveRight = false; else canMoveLeft = false;
1867         }
1868       }
1869     }
1871     // move
1872     if (canMoveLeft) {
1873       if (canMoveRight) FatalError("WATERCHECK: WTF RIGHT");
1874       //!writeln(wtile.objId, ": MOVING LEFT (", curWaterTileLastHDir, ")");
1875       curWaterTileLastHDir = -1;
1876       wtile.ix = wtile.ix-16;
1877     } else if (canMoveRight) {
1878       if (canMoveLeft) FatalError("WATERCHECK: WTF LEFT");
1879       //!writeln(wtile.objId, ": MOVING RIGHT (", curWaterTileLastHDir, ")");
1880       curWaterTileLastHDir = 1;
1881       wtile.ix = wtile.ix+16;
1882     }
1883     wasMoved = true;
1884   }
1886   // remove seaweeds
1887   if (wasMoved) {
1888     checkWater = true;
1889     wtile.setSprite(wtile.lava ? 'sLava' : 'sWater');
1890     wtile.waterMoved = true;
1891     // if this tile was not moved down, check if it can move down on any next step
1892     if (!wtile.waterMovedDown) {
1893            if (waterCanReachGroundHoleInDir(wtile, -1)) wtile.waterMovedDown = true;
1894       else if (waterCanReachGroundHoleInDir(wtile, 1)) wtile.waterMovedDown = true;
1895     }
1896   }
1898   return false; // don't remove
1900   //if (!isWetTileAtPix(tileX*16+8, tileY*16-8)) wtile.setSprite(wtile.lava ? 'sLavaTop' : 'sWaterTop');
1904 transient array!MapTile waterTilesList;
1906 final bool sortWaterTilesByCoordsLess (MapTile a, MapTile b) {
1907   int dy = a.iy-b.iy;
1908   if (dy) return (dy < 0);
1909   return (a.ix < b.ix);
1912 transient int waterFlowPause = 0;
1913 transient bool debugWaterFlowPause = false;
1915 final void cleanDeadObjects () {
1916   // remove dead objects
1917   if (deadItemsHead) {
1918     auto olddel = ImmediateDelete;
1919     ImmediateDelete = false;
1920     do {
1921       auto it = deadItemsHead;
1922       deadItemsHead = it.deadItemsNext;
1923       if (it.grid) it.grid.remove(it.gridId);
1924       it.onDestroy();
1925       delete it;
1926     } while (deadItemsHead);
1927     ImmediateDelete = olddel;
1928     if (olddel) CollectGarbage(true); // destroy delayed objects too
1929   }
1932 final void cleanDeadTiles () {
1933   if (checkWater && /*global.lake == 1 ||*/ (!blockWaterChecking && liquidTileCount)) {
1934     if (global.lake == 1) fillGreatLake();
1935     if (waterFlowPause > 1) {
1936       --waterFlowPause;
1937       cleanDeadObjects();
1938       return;
1939     }
1940     if (debugWaterFlowPause) waterFlowPause = 4;
1941     //writeln("checking water");
1942     waterTilesList.clear();
1943     foreach (MapTile wtile; objGrid.allObjectsSafe(MapTile)) {
1944       if (wtile.water || wtile.lava) {
1945         // sanity check
1946         if (wtile.ix%16 == 0 && wtile.iy%16 == 0) {
1947           wtile.waterMoved = false;
1948           wtile.waterMovedDown = false;
1949           wtile.waterSlideOldX = wtile.ix;
1950           wtile.waterSlideOldY = wtile.iy;
1951           waterTilesList[$] = wtile;
1952         }
1953       }
1954     }
1955     checkWater = false;
1956     liquidTileCount = 0;
1957     waterTilesList.sort(&sortWaterTilesByCoordsLess);
1958     // do water flow
1959     bool wasAnyMove = false;
1960     bool wasAnyMoveDown = false;
1961     foreach (MapTile wtile; waterTilesList) {
1962       if (!wtile || !wtile.isInstanceAlive) continue;
1963       auto killIt = checkWaterFlow(wtile);
1964       if (killIt) {
1965         checkWater = true;
1966         wtile.smashMe();
1967         wtile.instanceRemove(); // just in case
1968       } else {
1969         wtile.saveInterpData();
1970         wtile.updateGrid();
1971         wasAnyMove = wasAnyMove || wtile.waterMoved;
1972         wasAnyMoveDown = wasAnyMoveDown || wtile.waterMovedDown;
1973         if (wtile.waterMoved && debugWaterFlowPause) wtile.waterSlideCounter = 4;
1974       }
1975     }
1976     // do water check
1977     liquidTileCount = 0;
1978     foreach (MapTile wtile; waterTilesList) {
1979       if (!wtile || !wtile.isInstanceAlive) continue;
1980       if (wasAnyMoveDown) {
1981         ++liquidTileCount;
1982         continue;
1983       }
1984       //checkWater = checkWater || wtile.waterMoved;
1985       curWaterTile = wtile;
1986       int tileX = wtile.ix/16, tileY = wtile.iy/16;
1987       // check if we are have no way to leak
1988       bool killIt = false;
1989       if (!isFullyOccupiedAtTilePos(tileX-1, tileY) || (wtile.water && curWaterTileCheckHitsLava)) {
1990         //writeln(" LEFT DEATH (", curWaterOccupiedCount, " : ", curWaterTileCheckHitsLava, ")");
1991         killIt = true;
1992       }
1993       if (!killIt && (!isFullyOccupiedAtTilePos(tileX+1, tileY) || (wtile.water && curWaterTileCheckHitsLava))) {
1994         //writeln(" RIGHT DEATH (", curWaterOccupiedCount, " : ", curWaterTileCheckHitsLava, ")");
1995         killIt = true;
1996       }
1997       if (!killIt && (!isFullyOccupiedAtTilePos(tileX, tileY+1) || (wtile.water && curWaterTileCheckHitsLava))) {
1998         //writeln(" DOWN DEATH (", curWaterOccupiedCount, " : ", curWaterTileCheckHitsLava, ")");
1999         killIt = true;
2000       }
2001       //killIt = false;
2002       if (killIt) {
2003         checkWater = true;
2004         wtile.smashMe();
2005         wtile.instanceRemove(); // just in case
2006       } else {
2007         ++liquidTileCount;
2008       }
2009     }
2010     if (wasAnyMove) checkWater = true;
2011     //writeln("water check: liquidTileCount=", liquidTileCount, "; checkWater=", checkWater, "; wasAnyMove=", wasAnyMove, "; wasAnyMoveDown=", wasAnyMoveDown);
2013     // fill empty spaces in lake with water
2014     fixLiquidTop();
2015   }
2017   cleanDeadObjects();
2021 // ////////////////////////////////////////////////////////////////////////// //
2022 private transient array!MapEntity postponedThinkers;
2023 private transient MapEntity thinkerHeld;
2024 private transient array!MapEntity activeThinkerList;
2027 final void doThinkActionsForObject (MapEntity o) {
2028        if (o.justSpawned) o.justSpawned = false;
2029   else if (o.imageSpeed > 0) o.nextAnimFrame();
2030   o.saveInterpData();
2031   o.thinkFrame();
2032   if (o.isInstanceAlive) {
2033     //o.updateGrid();
2034     o.processAlarms();
2035     if (o.isInstanceAlive) {
2036       if (o.whipTimer > 0) --o.whipTimer;
2037       o.updateGrid();
2038       auto obj = MapObject(o);
2039       if (!o.canLiveOutsideOfLevel && (!obj || !obj.heldBy) && o.isOutsideOfLevel()) {
2040         // oops, fallen out of level...
2041         o.onOutOfLevel();
2042       }
2043     }
2044   }
2048 // return `true` if thinker should be removed
2049 final void thinkOne (MapEntity o, optional bool doHeldObject, optional bool dontAddHeldObject) {
2050   if (!o) return;
2051   if (o == thinkerHeld && !doHeldObject) return; // skip it
2053   if (!o.isInstanceAlive) return;
2055   auto obj = MapObject(o);
2057   if (obj) obj.prevhp = obj.hp; // so i don't have to do it in `thinkFrame()`
2058   if (!o.active) return;
2060   if (obj && obj.heldBy == player) {
2061     // fix held item coords
2062     obj.fixHoldCoords();
2063     if (doHeldObject) {
2064       doThinkActionsForObject(o);
2065     } else {
2066       if (!dontAddHeldObject) {
2067         bool found = false;
2068         foreach (MapEntity e; postponedThinkers) if (e == o) { found = true; break; }
2069         if (!found) postponedThinkers[$] = o;
2070       }
2071     }
2072     return;
2073   }
2075   bool doThink = true;
2077   // collision with player weapon
2078   auto hh = PlayerWeapon(player.holdItem);
2079   bool doWeaponAction = false;
2080   if (hh) {
2081     if (hh.blockedBySolids && !global.config.killEnemiesThruWalls) {
2082       int xx = player.ix+(player.dir == MapObject::Dir.Left ? -16 : 16);
2083       //doWeaponAction = !isSolidAtPoint(xx, player.iy);
2084       doWeaponAction = !isSolidAtPoint(xx, hh.yCenter);
2085       /*
2086       int dh = max(1, hh.height-2);
2087       doWeaponAction = !checkTilesInRect(player.ix, player.iy);
2088       */
2089     } else {
2090       doWeaponAction = true;
2091     }
2092   }
2094   if (obj && doWeaponAction && hh && (o.whipTimer <= 0 || hh.ignoreWhipTimer) && hh.collidesWithObject(obj)) {
2095     //writeln("WEAPONED!");
2096     //writeln("weapon collides with '", GetClassName(o.Class), "' (", o.objType, "'");
2097     bool dontChangeWhipTimer = hh.dontChangeWhipTimer;
2098     if (!o.onTouchedByPlayerWeapon(player, hh)) {
2099       if (o.isInstanceAlive) hh.onCollisionWithObject(obj);
2100     }
2101     if (!dontChangeWhipTimer) o.whipTimer = o.whipTimerValue; //HACK
2102     doThink = o.isInstanceAlive;
2103   }
2105   if (doThink && o.isInstanceAlive) {
2106     doThinkActionsForObject(o);
2107     doThink = o.isInstanceAlive;
2108   }
2110   // collision with player
2111   if (doThink && obj && o.collidesWith(player)) {
2112     if (!player.onObjectTouched(obj) && o.isInstanceAlive) {
2113       doThink = !o.onTouchedByPlayer(player);
2114       o.updateGrid();
2115     }
2116   }
2120 final void processThinkers (float timeDelta) {
2121   if (timeDelta <= 0) return;
2122   if (gamePaused) {
2123     ++pausedTime;
2124     if (onBeforeFrame) onBeforeFrame(false);
2125     if (onAfterFrame) onAfterFrame(false);
2126     keysNextFrame();
2127     return;
2128   } else {
2129     pausedTime = 0;
2130   }
2131   accumTime += timeDelta;
2132   bool wasFrame = false;
2133   // block GC
2134   auto olddel = ImmediateDelete;
2135   ImmediateDelete = false;
2136   while (accumTime >= FrameTime) {
2137     postponedThinkers.clear();
2138     thinkerHeld = none;
2139     accumTime -= FrameTime;
2140     if (onBeforeFrame) onBeforeFrame(accumTime >= FrameTime);
2141     // shake
2142     if (shakeLeft > 0) {
2143       --shakeLeft;
2144       if (!shakeDir.x) shakeDir.x = (global.randOther(0, 1) ? -3 : 3);
2145       if (!shakeDir.y) shakeDir.y = (global.randOther(0, 1) ? -3 : 3);
2146       shakeOfs.x = shakeDir.x;
2147       shakeOfs.y = shakeDir.y;
2148       int sgnc = global.randOther(1, 3);
2149       if (sgnc&0x01) shakeDir.x = -shakeDir.x;
2150       if (sgnc&0x02) shakeDir.y = -shakeDir.y;
2151     } else {
2152       shakeOfs.x = 0;
2153       shakeOfs.y = 0;
2154       shakeDir.x = 0;
2155       shakeDir.y = 0;
2156     }
2157     // advance time
2158     time += 1;
2159     // we don't want the time to grow too large
2160     if (time < 0) { time = 0; lastRenderTime = -1; }
2161     // game-global events
2162     thinkFrameGame();
2163     // frame thinkers: player
2164     if (player && !disablePlayerThink) {
2165       // time limit
2166       if (!player.dead && isNormalLevel() &&
2167           (maxPlayingTime < 0 ||
2168            (maxPlayingTime > 0 && maxPlayingTime*30 <= time &&
2169             time%30 == 0 && global.randOther(1, 100) <= 20)))
2170       {
2171         global.hasAnkh = false;
2172         global.plife = 1;
2173         player.invincible = 0;
2174         auto xplo = MapObjExplosion(MakeMapObject(player.ix, player.iy, 'oExplosion'));
2175         if (xplo) xplo.suicide = true;
2176       }
2177       //HACK: check for stolen items
2178       auto item = MapItem(player.holdItem);
2179       if (item) item.onCheckItemStolen(player);
2180       item = MapItem(player.pickedItem);
2181       if (item) item.onCheckItemStolen(player);
2182       // normal thinking
2183       doThinkActionsForObject(player);
2184     }
2185     // frame thinkers: held object
2186     thinkerHeld = player.holdItem;
2187     if (thinkerHeld && thinkerHeld.isInstanceAlive) {
2188       bool wasAct = thinkerHeld.active;
2189       thinkOne(thinkerHeld, doHeldObject:true);
2190       if (!thinkerHeld.isInstanceAlive) {
2191         if (player.holdItem == thinkerHeld) player.holdItem = none;
2192         thinkerHeld.grid.remove(thinkerHeld.gridId);
2193         /* later
2194         thinkerHeld.onDestroy();
2195         delete thinkerHeld;
2196         */
2197       } else if (!wasAct) {
2198         //HACK!
2199         auto item = MapItem(thinkerHeld);
2200         if (item) {
2201           if (item.forSale || item.sellOfferDone) {
2202             if (++item.forSaleFrame < 0) item.forSaleFrame = 0;
2203           }
2204         }
2205       }
2206     }
2207     // frame thinkers: objects
2208     activeThinkerList.clear();
2209     auto grid = objGrid;
2210     // collect active objects
2211     if (global.config.useFrozenRegion) {
2212       foreach (MapEntity e; grid.inRectPix(viewStart.x/global.scale-64, viewStart.y/global.scale-64, 320+64*2, 240+64*2, precise:false)) {
2213         if (e.active) activeThinkerList[$] = e;
2214       }
2215     } else {
2216       // no frozen area
2217       foreach (MapEntity e; grid.allObjects()) {
2218         if (e.active) activeThinkerList[$] = e;
2219       }
2220     }
2221     // process active objects
2222     //writeln("thinkers: ", activeThinkerList.length);
2223     foreach (MapEntity o; activeThinkerList) {
2224       if (!o) continue;
2225       thinkOne(o, doHeldObject:false);
2226       if (!o.isInstanceAlive) {
2227         //writeln("dead thinker: '", o.objType, "'");
2228         if (o.grid) o.grid.remove(o.gridId);
2229         auto obj = MapObject(o);
2230         if (obj && obj.heldBy) obj.heldBy.holdItem = none;
2231         /* later
2232         o.onDestroy();
2233         delete o;
2234         */
2235       }
2236     }
2237     // postponed thinkers
2238     foreach (MapEntity o; postponedThinkers) {
2239       if (!o) continue;
2240       thinkOne(o, doHeldObject:true, dontAddHeldObject:true);
2241       if (!o.isInstanceAlive) {
2242         //writeln("dead pp-thinker: '", o.objType, "'");
2243         /* later
2244         o.onDestroy();
2245         delete o;
2246         */
2247       }
2248     }
2249     postponedThinkers.clear();
2250     thinkerHeld = none;
2251     // clean dead things
2252     cleanDeadTiles();
2253     // fix held item coords
2254     if (player && player.holdItem) {
2255       if (player.holdItem.isInstanceAlive) {
2256         player.holdItem.fixHoldCoords();
2257       } else {
2258         player.holdItem = none;
2259       }
2260     }
2261     // money counter
2262     if (collectCounter == 0) {
2263       xmoney = max(0, xmoney-100);
2264     } else {
2265       --collectCounter;
2266     }
2267     // other things
2268     if (player) {
2269       if (!player.dead) stats.oneMoreFramePlayed();
2270       SoundSystem.ListenerOrigin = vector(player.xCenter, player.yCenter, -1);
2271       //writeln("plrpos=(", player.xCenter, ",", player.yCenter, "); lo=", SoundSystem.ListenerOrigin);
2272     }
2273     if (onAfterFrame) onAfterFrame(accumTime >= FrameTime);
2274     ++framesProcessedFromLastClear;
2275     keysNextFrame();
2276     wasFrame = true;
2277     if (!player.visible && player.holdItem) player.holdItem.visible = false;
2278     if (winCutsceneSwitchToNext) {
2279       winCutsceneSwitchToNext = false;
2280       switch (++inWinCutscene) {
2281         case 2: startWinCutsceneVolcano(); break;
2282         case 3: default: startWinCutsceneWinFall(); break;
2283       }
2284       break;
2285     }
2286     if (playerExited) break;
2287   }
2288   ImmediateDelete = olddel;
2289   if (playerExited) {
2290     playerExited = false;
2291     onLevelExited();
2292     centerViewAtPlayer();
2293   }
2294   if (wasFrame) {
2295     // if we were processed at least one frame, collect garbage
2296     //keysNextFrame();
2297     CollectGarbage(true); // destroy delayed objects too
2298   }
2299   if (accumTime > 0 && onInterFrame) onInterFrame(accumTime/FrameTime);
2303 // ////////////////////////////////////////////////////////////////////////// //
2304 final void tileXY2roomXY (int tileX, int tileY, out int roomX, out int roomY) {
2305   roomX = (tileX-1)/RoomGen::Width;
2306   roomY = (tileY-1)/RoomGen::Height;
2310 final bool isInShop (int tileX, int tileY) {
2311   if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
2312     auto n = roomType[tileX, tileY];
2313     if (n == 4 || n == 5) return true;
2314     return !!checkTilesInRect(tileX*16, tileY*16, 16, 16, delegate bool (MapTile t) { return t.shopWall; });
2315     //k8: we don't have this
2316     //if (t && t.objType == 'oShop') return true;
2317   }
2318   return false;
2322 // ////////////////////////////////////////////////////////////////////////// //
2323 override void Destroy () {
2324   clearWholeLevel();
2325   delete tempSolidTile;
2326   ::Destroy();
2330 // ////////////////////////////////////////////////////////////////////////// //
2331 // WARNING! delegate should not create/delete objects!
2332 final MapObject findNearestObject (int px, int py, scope bool delegate (MapObject o) dg, optional class!MapObject castClass) {
2333   MapObject res = none;
2334   if (!castClass) castClass = MapObject;
2335   int curdistsq = int.max;
2336   foreach (MapObject o; objGrid.allObjects(MapObject)) {
2337     if (o.spectral) continue;
2338     if (!dg(o)) continue;
2339     int xc = px-o.xCenter, yc = py-o.yCenter;
2340     int distsq = xc*xc+yc*yc;
2341     if (distsq < curdistsq) {
2342       res = o;
2343       curdistsq = distsq;
2344     }
2345   }
2346   return res;
2350 // WARNING! delegate should not create/delete objects!
2351 final MapObject findNearestEnemy (int px, int py, optional scope bool delegate (MapEnemy o) dg, optional class!MapEnemy castClass) {
2352   if (!castClass) castClass = MapEnemy;
2353   if (castClass !isa MapEnemy) return none;
2354   MapObject res = none;
2355   int curdistsq = int.max;
2356   foreach (MapEnemy o; objGrid.allObjects(castClass)) {
2357     //k8: i added `dead` check
2358     if (o.spectral || o.dead) continue;
2359     if (dg) {
2360       if (!dg(o)) continue;
2361     }
2362     int xc = px-o.xCenter, yc = py-o.yCenter;
2363     int distsq = xc*xc+yc*yc;
2364     if (distsq < curdistsq) {
2365       res = o;
2366       curdistsq = distsq;
2367     }
2368   }
2369   return res;
2373 final MonsterShopkeeper findNearestCalmShopkeeper (int px, int py) {
2374   auto obj = MonsterShopkeeper(findNearestEnemy(px, py, delegate bool (MapEnemy o) {
2375     auto sk = MonsterShopkeeper(o);
2376     if (sk && !sk.angered) return true;
2377     return false;
2378   }, castClass:MonsterShopkeeper));
2379   return obj;
2383 final MonsterShopkeeper hasAliveShopkeepers (optional bool skipAngry) {
2384   foreach (MonsterShopkeeper sc; objGrid.allObjects(MonsterShopkeeper)) {
2385     if (sc.spectral || sc.dead) continue;
2386     if (skipAngry && (sc.angered || sc.outlaw)) continue;
2387     return sc;
2388   }
2389   return none;
2393 // WARNING! delegate should not create/delete objects!
2394 final int calcNearestEnemyDist (int px, int py, optional scope bool delegate (MapEnemy o) dg, optional class!MapEnemy castClass) {
2395   auto e = findNearestEnemy(px, py, dg!optional, castClass!optional);
2396   if (!e) return int.max;
2397   int xc = px-e.xCenter, yc = py-e.yCenter;
2398   return round(sqrt(xc*xc+yc*yc));
2402 // WARNING! delegate should not create/delete objects!
2403 final int calcNearestObjectDist (int px, int py, optional scope bool delegate (MapObject o) dg, optional class!MapObject castClass) {
2404   auto e = findNearestObject(px, py, dg!optional, castClass!optional);
2405   if (!e) return int.max;
2406   int xc = px-e.xCenter, yc = py-e.yCenter;
2407   return round(sqrt(xc*xc+yc*yc));
2411 // WARNING! delegate should not create/delete objects!
2412 final MapTile findNearestMoveableSolid (int px, int py, optional scope bool delegate (MapTile t) dg) {
2413   MapTile res = none;
2414   int curdistsq = int.max;
2415   foreach (MapTile t; objGrid.allObjects(MapTile)) {
2416     if (t.spectral) continue;
2417     if (dg) {
2418       if (!dg(t)) continue;
2419     } else {
2420       if (!t.solid || !t.moveable) continue;
2421     }
2422     int xc = px-t.xCenter, yc = py-t.yCenter;
2423     int distsq = xc*xc+yc*yc;
2424     if (distsq < curdistsq) {
2425       res = t;
2426       curdistsq = distsq;
2427     }
2428   }
2429   return res;
2433 // WARNING! delegate should not create/delete objects!
2434 final MapTile findNearestTile (int px, int py, optional scope bool delegate (MapTile t) dg) {
2435   if (!dg) return none;
2436   MapTile res = none;
2437   int curdistsq = int.max;
2439   //FIXME: make this faster!
2440   foreach (MapTile t; objGrid.allObjects(MapTile)) {
2441     if (t.spectral) continue;
2442     int xc = px-t.xCenter, yc = py-t.yCenter;
2443     int distsq = xc*xc+yc*yc;
2444     if (distsq < curdistsq && dg(t)) {
2445       res = t;
2446       curdistsq = distsq;
2447     }
2448   }
2450   return res;
2454 // ////////////////////////////////////////////////////////////////////////// //
2455 final bool cbIsObjectWeb (MapObject o) { return (o isa ItemWeb); }
2456 final bool cbIsObjectBlob (MapObject o) { return (o isa EnemyBlob); }
2457 final bool cbIsObjectArrow (MapObject o) { return (o isa ItemProjectileArrow); }
2458 final bool cbIsObjectEnemy (MapObject o) { return (/*o != self &&*/ o isa MapEnemy); }
2460 final bool cbIsObjectTreasure (MapObject o) { return (o.isTreasure); }
2462 final bool cbIsItemObject (MapObject o) { return (o isa MapItem); }
2464 final bool cbIsItemOrEnemyObject (MapObject o) { return (o isa MapItem || o isa MapEnemy); }
2467 final MapObject isObjectAtTile (int tileX, int tileY, optional scope bool delegate (MapObject o) dg, optional bool precise) {
2468   if (!specified_precise) precise = true;
2469   tileX *= 16;
2470   tileY *= 16;
2471   foreach (MapObject o; objGrid.inRectPix(tileX, tileY, 16, 16, precise:precise, castClass:MapObject)) {
2472     if (o.spectral) continue;
2473     if (dg) {
2474       if (dg(o)) return o;
2475     } else {
2476       return o;
2477     }
2478   }
2479   return none;
2483 final MapObject isObjectAtTilePix (int x, int y, optional scope bool delegate (MapObject o) dg) {
2484   return isObjectAtTile(x/16, y/16, dg!optional);
2488 final MapObject isObjectAtPoint (int xpos, int ypos, optional scope bool delegate (MapObject o) dg, optional bool precise, optional class!MapObject castClass) {
2489   if (!specified_precise) precise = true;
2490   if (!castClass) castClass = MapObject;
2491   foreach (MapObject o; objGrid.inCellPix(xpos, ypos, precise:precise, castClass:castClass)) {
2492     if (o.spectral) continue;
2493     if (dg) {
2494       if (dg(o)) return o;
2495     } else {
2496       if (o isa MapEnemy) return o;
2497     }
2498   }
2499   return none;
2503 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) {
2504   if (w < 1 || h < 1) return none;
2505   if (!castClass) castClass = MapObject;
2506   if (w == 1 && h == 1) return isObjectAtPoint(xpos, ypos, dg!optional, precise!optional, castClass);
2507   if (!specified_precise) precise = true;
2508   foreach (MapObject o; objGrid.inRectPix(xpos, ypos, w, h, precise:precise, castClass:castClass)) {
2509     if (o.spectral) continue;
2510     if (dg) {
2511       if (dg(o)) return o;
2512     } else {
2513       if (o isa MapEnemy) return o;
2514     }
2515   }
2516   return none;
2520 final MapObject forEachObject (scope bool delegate (MapObject o) dg, optional bool allowSpectrals, optional class!MapObject castClass) {
2521   if (!dg) return none;
2522   if (!castClass) castClass = MapObject;
2523   foreach (MapObject o; objGrid.allObjectsSafe(castClass)) {
2524     if (!allowSpectrals && o.spectral) continue;
2525     if (dg(o)) return o;
2526   }
2527   return none;
2531 final MapObject forEachObjectAtPoint (int xpos, int ypos, scope bool delegate (MapObject o) dg, optional bool precise) {
2532   if (!dg) return none;
2533   if (!specified_precise) precise = true;
2534   foreach (MapObject o; objGrid.inCellPix(xpos, ypos, precise:precise, castClass:MapObject)) {
2535     if (o.spectral) continue;
2536     if (dg(o)) return o;
2537   }
2538   return none;
2542 final MapObject forEachObjectInRect (int xpos, int ypos, int w, int h, scope bool delegate (MapObject o) dg, optional bool precise) {
2543   if (!dg || w < 1 || h < 1) return none;
2544   if (w == 1 && h == 1) return forEachObjectAtPoint(xpos, ypos, dg!optional, precise!optional);
2545   if (!specified_precise) precise = true;
2546   foreach (MapObject o; objGrid.inRectPix(xpos, ypos, w, h, precise:precise, castClass:MapObject)) {
2547     if (o.spectral) continue;
2548     if (dg(o)) return o;
2549   }
2550   return none;
2554 final MapEntity forEachEntityInRect (int xpos, int ypos, int w, int h, scope bool delegate (MapEntity o) dg, optional bool precise, optional class!MapEntity castClass) {
2555   if (!dg || w < 1 || h < 1) return none;
2556   if (!castClass) castClass = MapEntity;
2557   if (!specified_precise) precise = true;
2558   foreach (MapEntity e; objGrid.inRectPix(xpos, ypos, w, h, precise:precise, castClass:castClass)) {
2559     if (e.spectral) continue;
2560     if (dg(e)) return e;
2561   }
2562   return none;
2566 private final bool cbIsRopeTile (MapTile t) { return (t isa MapTileRope); }
2568 final MapTile isRopeAtPoint (int px, int py) {
2569   return checkTileAtPoint(px, py, &cbIsRopeTile);
2573 //FIXME!
2574 final MapTile isWaterSwimAtPoint (int px, int py) {
2575   return isWaterAtPoint(px, py);
2579 // ////////////////////////////////////////////////////////////////////////// //
2580 private array!MapEntity tmpEntityList;
2582 private final bool cbCollectEntitiesWithMask (MapEntity t) {
2583   if (!t.visible || t.spectral) return false;
2584   tmpEntityList[$] = t;
2585   return false;
2589 final void touchEntitiesWithMask (int x, int y, SpriteFrame frm, scope bool delegate (MapEntity t) dg, optional class!MapEntity castClass) {
2590   if (!frm || frm.bw < 1 || frm.bh < 1 || !dg) return;
2591   if (frm.isEmptyPixelMask) return;
2592   if (!castClass) castClass = MapEntity;
2593   // collect tiles
2594   if (tmpEntityList.length) tmpEntityList.clear();
2595   if (player isa castClass && player.isRectCollisionFrame(frm, x, y)) tmpEntityList[$] = player;
2596   forEachEntityInRect(x+frm.bx, y+frm.by, frm.bw, frm.bh, &cbCollectEntitiesWithMask, castClass:castClass);
2597   foreach (MapEntity e; tmpEntityList) {
2598     if (!e || !e.isInstanceAlive || !e.visible || e.spectral) continue;
2599     if (e.isRectCollisionFrame(frm, x, y)) {
2600       if (dg(e)) break;
2601     }
2602   }
2606 // ////////////////////////////////////////////////////////////////////////// //
2607 final bool cbCollisionAnySolid (MapTile t) { return t.solid; }
2608 final bool cbCollisionAnyMoveableSolid (MapTile t) { return t.solid && t.moveable; }
2609 final bool cbCollisionLiquid (MapTile t) { return (t.water || t.lava); }
2610 final bool cbCollisionAnySolidOrLiquid (MapTile t) { return (t.solid || t.water || t.lava); }
2611 final bool cbCollisionLadder (MapTile t) { return t.ladder; }
2612 final bool cbCollisionLadderTop (MapTile t) { return t.laddertop; }
2613 final bool cbCollisionAnyLadder (MapTile t) { return (t.ladder || t.laddertop); }
2614 final bool cbCollisionAnySolidIce (MapTile t) { return (t.solid && t.ice); }
2615 final bool cbCollisionWater (MapTile t) { return t.water; }
2616 final bool cbCollisionLava (MapTile t) { return t.lava; }
2617 final bool cbCollisionAnyTree (MapTile t) { return t.tree; }
2618 final bool cbCollisionPlatform (MapTile t) { return t.platform; }
2619 final bool cbCollisionLadderOrPlatform (MapTile t) { return (t.ladder || t.platform); }
2620 //final bool cbCollisionAnyHangeable (MapTile t) { return t.hangeable; } // no need to check for solids here, it is done in MapTile initializer
2621 final bool cbCollisionSpringTrap (MapTile t) { return t.springtrap; }
2622 final bool cbCollisionSpikes (MapTile t) { return t.spikes; }
2623 final bool cbCollisionExitTile (MapTile t) { return t.isExitActive(); }
2625 final bool cbCollisionForWhoa (MapTile t) { return (t.solid || t.laddertop); }
2627 //final bool cbCollisionSacAltar (MapTile t) { return (t.objName == 'oSacAltarLeft'); } // only left part, yeah
2628 final bool cbCollisionSacAltar (MapTile t) { return t.sacrificingAltar; }
2631 // ////////////////////////////////////////////////////////////////////////// //
2632 transient MapTileTemp tempSolidTile;
2634 private final MapTileTemp makeWalkeableSolidTile (MapObject o) {
2635   if (!tempSolidTile) {
2636     tempSolidTile = SpawnObject(MapTileTemp);
2637   } else if (!tempSolidTile.isInstanceAlive) {
2638     delete tempSolidTile;
2639     tempSolidTile = SpawnObject(MapTileTemp);
2640   }
2641   // setup data
2642   tempSolidTile.level = self;
2643   tempSolidTile.global = global;
2644   tempSolidTile.solid = true;
2645   tempSolidTile.objName = MapTileTemp.default.objName;
2646   tempSolidTile.objType = MapTileTemp.default.objType;
2647   tempSolidTile.e = o;
2648   tempSolidTile.fltx = o.fltx;
2649   tempSolidTile.flty = o.flty;
2650   return tempSolidTile;
2654 final MapTile checkTilesInRect (int x0, int y0, const int w, const int h,
2655                                 optional scope bool delegate (MapTile dg) dg, optional bool precise,
2656                                 optional class!MapTile castClass)
2658   if (w < 1 || h < 1) return none;
2659   if (w == 1 && h == 1) return checkTileAtPoint(x0, y0, dg!optional);
2660   int x1 = x0+w-1, y1 = y0+h-1;
2661   if (x0 >= tilesWidth*16 || y0 >= tilesHeight*16 || x1 < 0 || y1 < 0) return none;
2662   if (!specified_precise) precise = true;
2663   if (!castClass) castClass = MapTile;
2664   if (!dg) dg = &cbCollisionAnySolid;
2666   // check walkable solid objects too
2667   foreach (MapEntity e; objGrid.inRectPix(x0, y0, w, h, precise:precise, castClass:castClass)) {
2668     if (e.spectral || !e.visible) continue;
2669     auto t = MapTile(e);
2670     if (t) {
2671       if (dg(t)) return t;
2672       continue;
2673     }
2674     auto o = MapObject(e);
2675     if (o && o.walkableSolid) {
2676       t = makeWalkeableSolidTile(o);
2677       if (dg(t)) return t;
2678       continue;
2679     }
2680   }
2682   return none;
2686 final MapTile checkTileAtPoint (int x0, int y0, optional scope bool delegate (MapTile dg) dg, optional bool precise, optional class!MapTile castClass) {
2687   if (x0 < 0 || y0 < 0 || x0 >= tilesWidth*16 || y0 >= tilesHeight*16) return none;
2688   if (!specified_precise) precise = true;
2689   if (!castClass) castClass = MapTile;
2690   if (!dg) dg = &cbCollisionAnySolid;
2692   // check walkable solid objects
2693   foreach (MapEntity e; objGrid.inCellPix(x0, y0, precise:precise, castClass:castClass)) {
2694     if (e.spectral || !e.visible) continue;
2695     auto t = MapTile(e);
2696     if (t) {
2697       if (dg(t)) return t;
2698       continue;
2699     }
2700     auto o = MapObject(e);
2701     if (o && o.walkableSolid) {
2702       t = makeWalkeableSolidTile(o);
2703       if (dg(t)) return t;
2704       continue;
2705     }
2706   }
2708   return none;
2712 // ////////////////////////////////////////////////////////////////////////// //
2713 final MapTile isSolidAtPoint (int px, int py) { return checkTileAtPoint(px, py); }
2714 final MapTile isSolidInRect (int rx, int ry, int rw, int rh) { return checkTilesInRect(rx, ry, rw, rh); }
2715 final MapTile isLadderAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadder); }
2716 final MapTile isLadderOrPlatformAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadderOrPlatform); }
2717 final MapTile isLadderTopAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadderTop); }
2718 final MapTile isAnyLadderAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyLadder); }
2719 final MapTile isWaterAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionWater); }
2720 final MapTile isLavaAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLava); }
2721 final MapTile isLiquidAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLiquid); }
2722 final MapTile isIceAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnySolidIce); }
2723 final MapTile isTreeAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyTree); }
2724 //final MapTile isHangeableAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyHangeable); }
2727 // ////////////////////////////////////////////////////////////////////////// //
2728 final void setTileTypeAt (int tileX, int tileY, LevelGen::RType rt) {
2729   if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) roomType[tileX, tileY] = rt;
2733 //FIXME: make this faster
2734 transient float gtagX, gtagY;
2736 // only non-moveables and non-specials
2737 final MapTile getTileAtGrid (int tileX, int tileY) {
2738   gtagX = tileX*16;
2739   gtagY = tileY*16;
2740   return checkTileAtPoint(tileX*16, tileY*16, delegate bool (MapTile t) {
2741     if (t.spectral || t.moveable || t.toSpecialGrid || !t.visible) return false;
2742     if (t.fltx != gtagX || t.flty != gtagY) return false; // emulate grid
2743     if (t.width != 16 || t.height != 16) return false;
2744     return true;
2745   }, precise:false);
2746   //return (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight ? tiles[tileX, tileY] : none);
2750 final MapTile getTileAtGridAny (int tileX, int tileY) {
2751   gtagX = tileX*16;
2752   gtagY = tileY*16;
2753   return checkTileAtPoint(tileX*16, tileY*16, delegate bool (MapTile t) {
2754     if (t.spectral /*|| t.moveable*/ || !t.visible) return false;
2755     if (t.fltx != gtagX || t.flty != gtagY) return false; // emulate grid
2756     if (t.width != 16 || t.height != 16) return false;
2757     return true;
2758   }, precise:false);
2759   //return (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight ? tiles[tileX, tileY] : none);
2763 final bool isNamedTileAt (int tileX, int tileY, name atypename) {
2764   if (!atypename) return false;
2765   auto t = getTileAtGridAny(tileX, tileY);
2766   return (t && t.objName == atypename);
2770 final void setTileAtGrid (int tileX, int tileY, MapTile tile) {
2771   if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
2772     if (tile) {
2773       tile.fltx = tileX*16;
2774       tile.flty = tileY*16;
2775       if (!tile.dontReplaceOthers) {
2776         auto osp = tile.spectral;
2777         tile.spectral = true;
2778         auto t = getTileAtGridAny(tileX, tileY);
2779         tile.spectral = osp;
2780         if (t && !t.immuneToReplacement) {
2781           writeln("REPLACING tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
2782           writeln("      NEW tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (tile.toSpecialGrid ? "special " : ""), "grid, pos=(", tile.fltx/16.0, ",", tile.flty/16.0, ")");
2783           t.instanceRemove();
2784         }
2785       }
2786       insertObject(tile);
2787     } else {
2788       auto t = getTileAtGridAny(tileX, tileY);
2789       if (t && !t.immuneToReplacement) {
2790         writeln("REMOVING tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
2791         t.instanceRemove();
2792       }
2793     }
2794   }
2798 // ////////////////////////////////////////////////////////////////////////// //
2799 // return `true` from delegate to stop
2800 MapTile forEachSolidTileOnGrid (scope bool delegate (int tileX, int tileY, MapTile t) dg) {
2801   if (!dg) return none;
2802   foreach (MapTile t; objGrid.allObjectsSafe(MapTile)) {
2803     if (t.spectral || !t.solid || !t.visible) continue;
2804     if (t.ix%16 != 0 || t.iy%16 != 0) continue; // emulate grid
2805     if (t.width != 16 || t.height != 16) continue;
2806     if (dg(t.ix/16, t.iy/16, t)) return t;
2807   }
2808   return none;
2812 // ////////////////////////////////////////////////////////////////////////// //
2813 // return `true` from delegate to stop
2814 MapTile forEachTile (scope bool delegate (MapTile t) dg, optional class!MapTile castClass) {
2815   if (!dg) return none;
2816   if (!castClass) castClass = MapTile;
2817   foreach (MapTile t; objGrid.allObjectsSafe(castClass)) {
2818     if (t.spectral || !t.visible) continue;
2819     if (dg(t)) return t;
2820   }
2821   return none;
2825 // ////////////////////////////////////////////////////////////////////////// //
2826 final void fixWallTiles () {
2827   foreach (MapTile t; objGrid.allObjectsSafe(MapTile)) t.beautifyTile();
2831 // ////////////////////////////////////////////////////////////////////////// //
2832 final MapTile isCollisionAtPoint (int px, int py, optional scope bool delegate (MapTile dg) dg) {
2833   if (!dg) dg = &cbCollisionAnySolid;
2834   return checkTilesInRect(px, py, 1, 1, dg);
2838 // ////////////////////////////////////////////////////////////////////////// //
2839 string scrGetKaliGift (MapTile altar, optional name gift) {
2840   string res;
2842   // find other side of the altar
2843   int sx = player.ix, sy = player.iy;
2844   if (altar) {
2845     sx = altar.ix;
2846     sy = altar.iy;
2847     auto a2 = isCollisionAtPoint(altar.ix+18, altar.iy+8, &cbCollisionSacAltar);
2848     if (!a2) a2 = isCollisionAtPoint(altar.ix-8, altar.iy+8, &cbCollisionSacAltar);
2849     if (a2) { sx = a2.ix; sy = a2.iy; }
2850   }
2852        if (global.favor <= -8) res = "SHE SEEMS VERY ANGRY WITH YOU!";
2853   else if (global.favor < 0) res = "SHE SEEMS ANGRY WITH YOU.";
2854   else if (global.favor == 0) res = "SHE HAS FORGIVEN YOU!";
2855   else if (global.favor >= 32) {
2856     if (global.kaliGift >= 3 && global.favor >= 32+(global.kaliGift-2)*16) {
2857       res = "YOU FEEL INVIGORATED!";
2858       global.kaliGift += 1;
2859       global.plife += global.randOther(4, 8);
2860     } else if (global.kaliGift >= 3) {
2861       res = "SHE SEEMS ECSTATIC WITH YOU!";
2862     } else if (global.bombs < 80) {
2863       res = "YOUR SATCHEL FEELS VERY FULL NOW!";
2864       global.kaliGift = 3;
2865       global.bombs = 99;
2866     } else {
2867       res = "YOU FEEL INVIGORATED!";
2868       global.kaliGift += 1;
2869       global.plife += global.randOther(4, 8);
2870     }
2871   } else if (global.favor >= 16) {
2872     if (global.kaliGift >= 2) {
2873       res = "SHE SEEMS VERY HAPPY WITH YOU!";
2874     } else {
2875       res = "SHE BESTOWS A GIFT UPON YOU!";
2876       global.kaliGift = 2;
2877       // poofs
2878       auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2879       obj.xVel = -1;
2880       obj.yVel = 0;
2881       obj = MakeMapObject(sx, sy-8, 'oPoof');
2882       obj.xVel = 1;
2883       obj.yVel = 0;
2884       // a gift
2885       obj = none;
2886       if (gift) obj = MakeMapObject(sx, sy-8, gift);
2887       if (!obj) obj = MakeMapObject(sx, sy-8, 'oKapala');
2888     }
2889   } else if (global.favor >= 8) {
2890     if (global.kaliGift >= 1) {
2891       res = "SHE SEEMS HAPPY WITH YOU.";
2892     } else {
2893       res = "SHE BESTOWS A GIFT UPON YOU!";
2894       global.kaliGift = 1;
2895       //rAltar = instance_nearest(x, y, oSacAltarRight);
2896       //if (instance_exists(rAltar)) {
2897       // poofs
2898       auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2899       obj.xVel = -1;
2900       obj.yVel = 0;
2901       obj = MakeMapObject(sx, sy-8, 'oPoof');
2902       obj.xVel = 1;
2903       obj.yVel = 0;
2904       obj = none;
2905       if (gift) obj = MakeMapObject(sx, sy-8, gift);
2906       if (!obj) {
2907         auto n = global.randOther(1, 8);
2908         auto m = n;
2909         for (;;) {
2910           name aname = '';
2911                if (n == 1 && !global.hasCape && !global.hasJetpack) aname = 'oCapePickup';
2912           else if (n == 2 && !global.hasGloves) aname = 'oGloves';
2913           else if (n == 3 && !global.hasSpectacles) aname = 'oSpectacles';
2914           else if (n == 4 && !global.hasMitt) aname = 'oMitt';
2915           else if (n == 5 && !global.hasSpringShoes) aname = 'oSpringShoes';
2916           else if (n == 6 && !global.hasSpikeShoes) aname = 'oSpikeShoes';
2917           else if (n == 7 && !global.hasStickyBombs) aname = 'oPaste';
2918           else if (n == 8 && !global.hasCompass) aname = 'oCompass';
2919           if (aname) {
2920             obj = MakeMapObject(sx, sy-8, aname);
2921             if (obj) break;
2922           }
2923           ++n;
2924           if (n > 8) n = 1;
2925           if (n == m) {
2926             obj = MakeMapObject(sx, sy-8, (global.hasJetpack ? 'oBombBox' : 'oJetpack'));
2927             break;
2928           }
2929         }
2930       }
2931     }
2932   } else if (global.favor > 0) {
2933     res = "SHE SEEMS PLEASED WITH YOU.";
2934   }
2936   /*
2937   if (argument1) {
2938     global.message = "";
2939     res = "KALI DEVOURS YOU!"; // sacrifice is player
2940   }
2941   */
2943   return res;
2947 void performSacrifice (MapObject what, MapTile where) {
2948   if (!what || !what.isInstanceAlive) return;
2949   MakeMapObject(what.ix+8, what.iy+8, 'oFlame');
2950   if (where) where.playSound('sndSmallExplode'); else what.playSound('sndSmallExplode');
2951   what.spillBlood(amount:3, forced:true);
2953   string msg = "KALI ACCEPTS THE SACRIFICE!";
2955   auto idol = ItemGoldIdol(what);
2956   if (idol) {
2957     ++stats.totalSacrifices;
2958          if (global.favor <= -8) msg = "SHE SEEMS VERY ANGRY WITH YOU!";
2959     else if (global.favor < 0) msg = "SHE SEEMS ANGRY WITH YOU.";
2960     else if (global.favor >= 0) {
2961       // find other side of the altar
2962       int sx = player.ix, sy = player.iy;
2963       auto altar = where;
2964       if (altar) {
2965         sx = altar.ix;
2966         sy = altar.iy;
2967         auto a2 = isCollisionAtPoint(altar.ix+18, altar.iy+8, &cbCollisionSacAltar);
2968         if (!a2) a2 = isCollisionAtPoint(altar.ix-8, altar.iy+8, &cbCollisionSacAltar);
2969         if (a2) { sx = a2.ix; sy = a2.iy; }
2970       }
2971       // poofs
2972       auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2973       obj.xVel = -1;
2974       obj.yVel = 0;
2975       obj = MakeMapObject(sx, sy-8, 'oPoof');
2976       obj.xVel = 1;
2977       obj.yVel = 0;
2978       // a gift
2979       obj = MakeMapObject(sx, sy-16-8, 'oMonkeyGold');
2980     }
2981     osdMessage(msg, 6.66);
2982     scrShake(10);
2983     idol.instanceRemove();
2984     return;
2985   }
2987   if (global.favor <= -8) {
2988     msg = "KALI DEVOURS THE SACRIFICE!";
2989   } else if (global.favor < 0) {
2990     global.favor += (what.status == MapObject::STUNNED ? what.favor : round(what.favor/2.0)); //k8: added `round()`
2991     if (what.favor > 0) what.favor = 0;
2992   } else {
2993     global.favor += (what.status == MapObject::STUNNED ? what.favor : round(what.favor/2.0)); //k8: added `round()`
2994   }
2996   /*!!
2997        if (myAltar.gift1 != "" and global.kaliGift == 0) scrGetKaliGift(myAltar.gift1);
2998   else if (myAltar.gift2 != "" and global.kaliGift == 1) scrGetKaliGift(myAltar.gift2);
2999   else scrGetKaliGift("");
3000   */
3002   // sacrifice is player?
3003   if (what isa PlayerPawn) {
3004     ++stats.totalSelfSacrifices;
3005     msg = "KALI DEVOURS YOU!";
3006     player.visible = false;
3007     player.removeBallAndChain(temp:true);
3008     player.dead = true;
3009     player.status = MapObject::DEAD;
3010   } else {
3011     ++stats.totalSacrifices;
3012     auto msg2 = scrGetKaliGift(where);
3013     what.instanceRemove();
3014     if (msg2) msg = va("%s\n%s", msg, msg2);
3015   }
3017   osdMessage(msg, 6.66);
3019   scrShake(10);
3023 // ////////////////////////////////////////////////////////////////////////// //
3024 final void addBackgroundGfxDetails () {
3025   // add background details
3026   //if (global.customLevel) return;
3027   foreach (; 0..20) {
3028     // bg = instance_create(16*randRoom(1, 42), 16*randRoom(1, 33), oCaveBG);
3029          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);
3030     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);
3031     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);
3032     else MakeMapBackTile('bgExtras', 32*global.randRoom(0, 1), 0, 32, 32, 16*global.randRoom(1, 42), 16*global.randRoom(1, 33), 10002);
3033   }
3037 // ////////////////////////////////////////////////////////////////////////// //
3038 private final void fixRealViewStart () {
3039   int scale = global.scale;
3040   realViewStart.x = clamp(realViewStart.x, viewMin.x*scale, viewMax.x*scale-viewWidth);
3041   realViewStart.y = clamp(realViewStart.y, viewMin.y*scale, viewMax.y*scale-viewHeight);
3045 final int cameraCurrX () { return realViewStart.x/global.scale; }
3046 final int cameraCurrY () { return realViewStart.y/global.scale; }
3049 private final void fixViewStart () {
3050   int scale = global.scale;
3051   viewStart.x = clamp(viewStart.x, viewMin.x*scale, viewMax.x*scale-viewWidth);
3052   viewStart.y = clamp(viewStart.y, viewMin.y*scale, viewMax.y*scale-viewHeight);
3056 final void centerViewAtPlayer () {
3057   if (viewWidth < 1 || viewHeight < 1 || !player) return;
3058   centerViewAt(player.xCenter, player.yCenter);
3062 final void centerViewAt (int x, int y) {
3063   if (viewWidth < 1 || viewHeight < 1) return;
3065   cameraSlideToSpeed.x = 0;
3066   cameraSlideToSpeed.y = 0;
3067   cameraSlideToPlayer = 0;
3069   int scale = global.scale;
3070   x *= scale;
3071   y *= scale;
3072   realViewStart.x = clamp(x-viewWidth/2, 0, tilesWidth*16*scale-viewWidth);
3073   realViewStart.y = clamp(y-viewHeight/2, 0, tilesHeight*16*scale-viewHeight);
3074   fixRealViewStart();
3076   viewStart.x = realViewStart.x;
3077   viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
3078   fixViewStart();
3080   if (onCameraTeleported) onCameraTeleported();
3084 const int ViewPortToleranceX = 16*1+8;
3085 const int ViewPortToleranceY = 16*1+8;
3087 final void fixCamera () {
3088   if (!player) return;
3089   if (viewWidth < 1 || viewHeight < 1) return;
3090   int scale = global.scale;
3091   auto alwaysCenterX = global.config.alwaysCenterPlayer;
3092   auto alwaysCenterY = alwaysCenterX;
3093   // calculate offset from viewport center (in game units), and fix viewport
3095   int camDestX = player.ix+8;
3096   int camDestY = player.iy+8;
3097   if (!cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y) != 0) {
3098     // slide camera to point
3099     if (cameraSlideToSpeed.x) camDestX = cameraSlideToCurr.x;
3100     if (cameraSlideToSpeed.y) camDestY = cameraSlideToCurr.y;
3101     int dx = cameraSlideToDest.x-camDestX;
3102     int dy = cameraSlideToDest.y-camDestY;
3103     //writeln("speed:(", cameraSlideToSpeed.x, ",", cameraSlideToSpeed.y, "); cur:(", camDestX, ",", camDestY, "); dest:(", cameraSlideToDest.x, ",", cameraSlideToDest.y, "); delta=(", dx, ",", dy, ")");
3104     if (dx && cameraSlideToSpeed.x != 0) {
3105       alwaysCenterX = true;
3106       if (abs(dx) <= abs(cameraSlideToSpeed.x)) {
3107         camDestX = cameraSlideToDest.x;
3108       } else {
3109         camDestX += sign(dx)*abs(cameraSlideToSpeed.x);
3110       }
3111     }
3112     if (dy && abs(cameraSlideToSpeed.y) != 0) {
3113       alwaysCenterY = true;
3114       if (abs(dy) <= cameraSlideToSpeed.y) {
3115         camDestY = cameraSlideToDest.y;
3116       } else {
3117         camDestY += sign(dy)*abs(cameraSlideToSpeed.y);
3118       }
3119     }
3120     //writeln("  new:(", camDestX, ",", camDestY, ")");
3121     if (cameraSlideToSpeed.x) cameraSlideToCurr.x = camDestX;
3122     if (cameraSlideToSpeed.y) cameraSlideToCurr.y = camDestY;
3123   }
3125   // horizontal
3126   if (cameraSlideToSpeed.x && !cameraSlideToPlayer) {
3127     realViewStart.x = clamp(camDestX*scale, 0, tilesWidth*16*scale-viewWidth);
3128   } else if (!player.cameraBlockX) {
3129     int x = camDestX*scale;
3130     int cx = realViewStart.x;
3131     if (alwaysCenterX) {
3132       cx = x-viewWidth/2;
3133     } else {
3134       int xofs = x-(cx+viewWidth/2);
3135            if (xofs < -ViewPortToleranceX*scale) cx += xofs+ViewPortToleranceX*scale;
3136       else if (xofs > ViewPortToleranceX*scale) cx += xofs-ViewPortToleranceX*scale;
3137     }
3138     // slide back to player?
3139     if (cameraSlideToPlayer && cameraSlideToSpeed.x) {
3140       int prevx = cameraSlideToCurr.x*scale;
3141       int dx = (cx-prevx)/scale;
3142       if (abs(dx) <= cameraSlideToSpeed.x) {
3143         writeln("BACKSLIDE X COMPLETE!");
3144         cameraSlideToSpeed.x = 0;
3145       } else {
3146         cx = prevx+(sign(dx)*cameraSlideToSpeed.x)*scale;
3147         cameraSlideToCurr.x += sign(dx)*cameraSlideToSpeed.x;
3148         if ((dx < 0 && cx <= 0) || (dx > 0 && cx >= tilesWidth*16*scale-viewWidth)) {
3149           writeln("BACKSLIDE X COMPLETE!");
3150           cameraSlideToSpeed.x = 0;
3151         }
3152       }
3153     }
3154     realViewStart.x = clamp(cx, 0, tilesWidth*16*scale-viewWidth);
3155   }
3157   // vertical
3158   if (cameraSlideToSpeed.y && !cameraSlideToPlayer) {
3159     realViewStart.y = clamp(camDestY*scale, 0, tilesHeight*16*scale-viewHeight);
3160   } else if (!player.cameraBlockY) {
3161     int y = camDestY*scale;
3162     int cy = realViewStart.y;
3163     if (alwaysCenterY) {
3164       cy = y-viewHeight/2;
3165     } else {
3166       int yofs = y-(cy+viewHeight/2);
3167            if (yofs < -ViewPortToleranceY*scale) cy += yofs+ViewPortToleranceY*scale;
3168       else if (yofs > ViewPortToleranceY*scale) cy += yofs-ViewPortToleranceY*scale;
3169     }
3170     // slide back to player?
3171     if (cameraSlideToPlayer && cameraSlideToSpeed.y) {
3172       int prevy = cameraSlideToCurr.y*scale;
3173       int dy = (cy-prevy)/scale;
3174       if (abs(dy) <= cameraSlideToSpeed.y) {
3175         writeln("BACKSLIDE Y COMPLETE!");
3176         cameraSlideToSpeed.y = 0;
3177       } else {
3178         cy = prevy+(sign(dy)*cameraSlideToSpeed.y)*scale;
3179         cameraSlideToCurr.y += sign(dy)*cameraSlideToSpeed.y;
3180         if ((dy < 0 && cy <= 0) || (dy > 0 && cy >= tilesHeight*16*scale-viewHeight)) {
3181           writeln("BACKSLIDE Y COMPLETE!");
3182           cameraSlideToSpeed.y = 0;
3183         }
3184       }
3185     }
3186     realViewStart.y = clamp(cy, 0, tilesHeight*16*scale-viewHeight);
3187   }
3189   if (cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y) == 0) cameraSlideToPlayer = 0;
3191   fixRealViewStart();
3192   //writeln("  new2:(", cameraCurrX, ",", cameraCurrY, ")");
3194   viewStart.x = realViewStart.x;
3195   viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
3196   fixViewStart();
3200 // ////////////////////////////////////////////////////////////////////////// //
3201 // x0 and y0 are non-scaled (and will be scaled)
3202 final void drawSpriteAt (name sprName, float frnumf, int x0, int y0, optional bool small) {
3203   if (!sprName) return;
3204   auto spr = sprStore[sprName];
3205   if (!spr || !spr.frames.length) return;
3206   int scale = global.scale;
3207   x0 *= scale;
3208   y0 *= scale;
3209   int frnum = max(0, trunc(frnumf))%spr.frames.length;
3210   auto sfr = spr.frames[frnum];
3211   int sx0 = x0-sfr.xofs*scale;
3212   int sy0 = y0-sfr.yofs*scale;
3213   if (small && scale > 1) {
3214     sfr.tex.blitExt(sx0, sy0, round(sx0+sfr.tex.width*(scale/2.0)), round(sy0+sfr.tex.height*(scale/2.0)), 0, 0);
3215   } else {
3216     sfr.tex.blitAt(sx0, sy0, scale);
3217   }
3221 final void drawSpriteAtS3 (name sprName, float frnumf, int x0, int y0) {
3222   if (!sprName) return;
3223   auto spr = sprStore[sprName];
3224   if (!spr || !spr.frames.length) return;
3225   x0 *= 3;
3226   y0 *= 3;
3227   int frnum = max(0, trunc(frnumf))%spr.frames.length;
3228   auto sfr = spr.frames[frnum];
3229   int sx0 = x0-sfr.xofs*3;
3230   int sy0 = y0-sfr.yofs*3;
3231   sfr.tex.blitAt(sx0, sy0, 3);
3235 // x0 and y0 are non-scaled (and will be scaled)
3236 final void drawTextAt (int x0, int y0, string text, optional int scale, optional int hiColor1, optional int hiColor2) {
3237   if (!text) return;
3238   if (!specified_scale) scale = global.scale;
3239   x0 *= scale;
3240   y0 *= scale;
3241   sprStore.renderTextWithHighlight(x0, y0, text, scale, hiColor1!optional, hiColor2!optional);
3245 void renderCompass (float currFrameDelta) {
3246   if (!global.hasCompass) return;
3248   /*
3249   if (isRoom("rOlmec")) {
3250     global.exitX = 648;
3251     global.exitY = 552;
3252   } else if (isRoom("rOlmec2")) {
3253     global.exitX = 648;
3254     global.exitY = 424;
3255   }
3256   */
3258   bool hasMessage = osdHasMessage();
3259   foreach (MapTile et; allExits) {
3260     // original compass
3261     int exitX = et.ix, exitY = et.iy;
3262     int vx0 = viewStart.x/global.scale, vy0 = viewStart.y/global.scale;
3263     int vx1 = (viewStart.x+viewWidth)/global.scale;
3264     int vy1 = (viewStart.y+viewHeight)/global.scale;
3265     if (exitY > vy1-16) {
3266       if (exitX < vx0) {
3267         drawSpriteAt(hasMessage ? 'sCompassSmallLL' : 'sCompassLL', 0, 0, 224);
3268       } else if (exitX > vx1-16) {
3269         drawSpriteAt(hasMessage ? 'sCompassSmallLR' : 'sCompassLR', 0, 304, 224);
3270       } else {
3271         drawSpriteAt(hasMessage ? 'sCompassSmallDown' : 'sCompassDown', 0, exitX-vx0, 224);
3272       }
3273     } else if (exitX < vx0) {
3274       drawSpriteAt(hasMessage ? 'sCompassSmallLeft' : 'sCompassLeft', 0, 0, exitY-vy0);
3275     } else if (exitX > vx1-16) {
3276       drawSpriteAt(hasMessage ? 'sCompassSmallRight' : 'sCompassRight', 0, 304, exitY-vy0);
3277     }
3278     break; // only the first exit
3279   }
3283 final bool totalsNameCmpCB (ref GameStats::TotalItem a, ref GameStats::TotalItem b) {
3284   auto sa = string(a.objName);
3285   auto sb = string(b.objName);
3286   return (sa < sb);
3289 void renderTransitionInfo (float currFrameDelta) {
3290   //FIXME!
3291   /*
3292   GameStats.sortTotalsList(stats.kills, &totalsNameCmpCB);
3294   int maxLen = 0;
3295   foreach (int idx, ref auto k; stats.kills) {
3296     string s = string(k);
3297     maxLen = max(maxLen, s.length);
3298   }
3299   maxLen *= 8;
3301   sprStore.loadFont('sFontSmall');
3302   Video.color = 0xff_ff_00;
3303   foreach (int idx, ref auto k; stats.kills) {
3304     int deaths = 0;
3305     foreach (int xidx, ref auto d; stats.totalKills) {
3306       if (d.objName == k) { deaths = d.count; break; }
3307     }
3308     //drawTextAt(16, 4+idx*8, va("%s: %d (%d)", string(k.objName).toUpperCase, k.count, deaths));
3309     drawTextAt(16, 4+idx*8, string(k).toUpperCase);
3310     drawTextAt(16+maxLen, 4+idx*8, va(": %d (%d)", k.count, deaths));
3311   }
3312   */
3316 void renderGhostTimer (float currFrameDelta) {
3317   if (ghostTimeLeft <= 0) return;
3318   //ghostTimeLeft /= 30; // frames -> seconds
3320   int hgt = viewHeight-64;
3321   if (hgt < 1) return;
3322   int rhgt = round(float(hgt)*(float(ghostTimeLeft)/30.0)/1000.0);
3323   //writeln("hgt=", hgt, "; rhgt=", rhgt, "; gtl=", ghostTimeLeft/30);
3324   if (rhgt > 0) {
3325     auto oclr = Video.color;
3326     Video.color = 0xcf_ff_7f_00;
3327     Video.fillRect(viewWidth-20, 32, 16, hgt-rhgt);
3328     Video.color = 0x7f_ff_7f_00;
3329     Video.fillRect(viewWidth-20, 32+(hgt-rhgt), 16, rhgt);
3330     Video.color = oclr;
3331   }
3335 void renderStarsHUD (float currFrameDelta) {
3336   bool scumSmallHud = global.config.scumSmallHud;
3338   //auto life = max(0, global.plife);
3339   int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3340   int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3341   //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3343   int hhup;
3345   if (scumSmallHud) {
3346     sprStore.loadFont('sFontSmall');
3347     hhup = 6;
3348   } else {
3349     sprStore.loadFont('sFont');
3350     hhup = 2;
3351   }
3353   Video.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3354   //draw_sprite(sHeart, -1, view_xview[0]+8, view_yview[0]+8);
3355   //draw_text(view_xview[0]+24, view_yview[0]+8, life);
3356   if (scumSmallHud) {
3357     if (global.plife == 1) {
3358       drawSpriteAt('sHeartSmallBlink', global.heartBlink, 10, hhup-4);
3359       global.heartBlink += 0.1;
3360       if (global.heartBlink > 3) global.heartBlink = 0;
3361     } else {
3362       drawSpriteAt('sHeartSmall', -1, 10, hhup-4);
3363       global.heartBlink = 0;
3364     }
3365   } else {
3366     if (global.plife == 1) {
3367       drawSpriteAt('sHeartBlink', global.heartBlink, 8, hhup);
3368       global.heartBlink += 0.1;
3369       if (global.heartBlink > 3) global.heartBlink = 0;
3370     } else {
3371       drawSpriteAt('sHeart', -1, 8, hhup);
3372       global.heartBlink = 0;
3373     }
3374   }
3375   int life = clamp(global.plife, 0, 99);
3376   drawTextAt(16+8, hhup, va("%d", life));
3378   //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3379   drawSpriteAt('sShopkeeperIcon', -1, 64, hhup, scumSmallHud);
3380   drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", starsKills));
3382   if (starsRoomTimer1 > 0) {
3383     sprStore.loadFont('sFontSmall');
3384     Video.color = 0xff_ff_00;
3385     int scale = global.scale;
3386     sprStore.renderMultilineTextCentered(viewWidth/2, 216*scale, va("SHOTGUN CHALLENGE BEGINS IN ~%d~", (starsRoomTimer1/30)+1), scale, 0xff_00_00);
3387   }
3391 void renderSunHUD (float currFrameDelta) {
3392   bool scumSmallHud = global.config.scumSmallHud;
3394   //auto life = max(0, global.plife);
3395   int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3396   int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3397   //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3399   int hhup;
3401   if (scumSmallHud) {
3402     sprStore.loadFont('sFontSmall');
3403     hhup = 6;
3404   } else {
3405     sprStore.loadFont('sFont');
3406     hhup = 2;
3407   }
3409   Video.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3410   //draw_sprite(sHeart, -1, view_xview[0]+8, view_yview[0]+8);
3411   //draw_text(view_xview[0]+24, view_yview[0]+8, life);
3412   if (scumSmallHud) {
3413     if (global.plife == 1) {
3414       drawSpriteAt('sHeartSmallBlink', global.heartBlink, 10, hhup-4);
3415       global.heartBlink += 0.1;
3416       if (global.heartBlink > 3) global.heartBlink = 0;
3417     } else {
3418       drawSpriteAt('sHeartSmall', -1, 10, hhup-4);
3419       global.heartBlink = 0;
3420     }
3421   } else {
3422     if (global.plife == 1) {
3423       drawSpriteAt('sHeartBlink', global.heartBlink, 8, hhup);
3424       global.heartBlink += 0.1;
3425       if (global.heartBlink > 3) global.heartBlink = 0;
3426     } else {
3427       drawSpriteAt('sHeart', -1, 8, hhup);
3428       global.heartBlink = 0;
3429     }
3430   }
3431   int life = clamp(global.plife, 0, 99);
3432   drawTextAt(16+8, hhup, va("%d", life));
3434   //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3435   drawSpriteAt('sDamselIcon', -1, 64, hhup, scumSmallHud);
3436   drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", sunScore));
3438   if (sunRoomTimer1 > 0) {
3439     sprStore.loadFont('sFontSmall');
3440     Video.color = 0xff_ff_00;
3441     int scale = global.scale;
3442     sprStore.renderMultilineTextCentered(viewWidth/2, 216*scale, va("DAMSEL CHALLENGE BEGINS IN ~%d~", (sunRoomTimer1/30)+1), scale, 0xff_00_00);
3443   }
3447 void renderMoonHUD (float currFrameDelta) {
3448   bool scumSmallHud = global.config.scumSmallHud;
3450   //auto life = max(0, global.plife);
3451   int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3452   int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3453   //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3455   int hhup;
3457   if (scumSmallHud) {
3458     sprStore.loadFont('sFontSmall');
3459     hhup = 6;
3460   } else {
3461     sprStore.loadFont('sFont');
3462     hhup = 2;
3463   }
3465   Video.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3467   //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3468   drawSpriteAt('sHoopsIcon', -1, 8, hhup, scumSmallHud);
3469   drawTextAt(8+16-(scumSmallHud ? 6 : 0), hhup, va("%d", moonScore));
3470   drawSpriteAt('sTimerIcon', -1, 64, hhup, scumSmallHud);
3471   drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", max(0, moonTimer)));
3473   if (moonRoomTimer1 > 0) {
3474     sprStore.loadFont('sFontSmall');
3475     Video.color = 0xff_ff_00;
3476     int scale = global.scale;
3477     sprStore.renderMultilineTextCentered(viewWidth/2, 216*scale, va("ARCHERY CHALLENGE BEGINS IN ~%d~", (moonRoomTimer1/30)+1), scale, 0xff_00_00);
3478   }
3482 void renderHUD (float currFrameDelta) {
3483   if (levelKind == LevelKind.Stars) { renderStarsHUD(currFrameDelta); return; }
3484   if (levelKind == LevelKind.Sun) { renderSunHUD(currFrameDelta); return; }
3485   if (levelKind == LevelKind.Moon) { renderMoonHUD(currFrameDelta); return; }
3487   if (!isHUDEnabled()) return;
3489   if (levelKind == LevelKind.Transition) { renderTransitionInfo(currFrameDelta); return; }
3491   int lifeX = 4; // 8
3492   int bombX = 56;
3493   int ropeX = 104;
3494   int ammoX = 152;
3495   int moneyX = 200;
3496   int hhup;
3497   bool scumSmallHud = global.config.scumSmallHud;
3498   if (!global.config.optSGAmmo) moneyX = ammoX;
3500   if (scumSmallHud) {
3501     sprStore.loadFont('sFontSmall');
3502     hhup = 6;
3503   } else {
3504     sprStore.loadFont('sFont');
3505     hhup = 0;
3506   }
3507   //int alpha = 0x6f_00_00_00;
3508   int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3509   int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3511   //Video.color = 0xff_ff_ff;
3512   Video.color = 0xff_ff_ff|talpha;
3514   // hearts
3515   if (scumSmallHud) {
3516     if (global.plife == 1) {
3517       drawSpriteAt('sHeartSmallBlink', global.heartBlink, lifeX+2, 4-hhup);
3518       global.heartBlink += 0.1;
3519       if (global.heartBlink > 3) global.heartBlink = 0;
3520     } else {
3521       drawSpriteAt('sHeartSmall', -1, lifeX+2, 4-hhup);
3522       global.heartBlink = 0;
3523     }
3524   } else {
3525     if (global.plife == 1) {
3526       drawSpriteAt('sHeartBlink', global.heartBlink, lifeX, 8-hhup);
3527       global.heartBlink += 0.1;
3528       if (global.heartBlink > 3) global.heartBlink = 0;
3529     } else {
3530       drawSpriteAt('sHeart', -1, lifeX, 8-hhup);
3531       global.heartBlink = 0;
3532     }
3533   }
3535   int life = clamp(global.plife, 0, 99);
3536   //if (!scumHud && life > 99) life = 99;
3537   drawTextAt(lifeX+16, 8-hhup, va("%d", life));
3539   // bombs
3540   if (global.hasStickyBombs && global.stickyBombsActive) {
3541     if (scumSmallHud) drawSpriteAt('sStickyBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sStickyBombIcon', -1, bombX, 8-hhup);
3542   } else {
3543     if (scumSmallHud) drawSpriteAt('sBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sBombIcon', -1, bombX, 8-hhup);
3544   }
3545   int n = global.bombs;
3546   if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3547   drawTextAt(bombX+16, 8-hhup, va("%d", n));
3549   // ropes
3550   if (scumSmallHud) drawSpriteAt('sRopeIconSmall', -1, ropeX+2, 4-hhup); else drawSpriteAt('sRopeIcon', -1, ropeX, 8-hhup);
3551   n = global.rope;
3552   if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3553   drawTextAt(ropeX+16, 8-hhup, va("%d", n));
3555   // shotgun shells
3556   if (global.config.optSGAmmo && (!player || player.holdItem !isa ItemWeaponBow)) {
3557     if (scumSmallHud) drawSpriteAt('sShellsIcon', -1, ammoX+6, 8-hhup); else drawSpriteAt('sShellsIcon', -1, ammoX+7, 12-hhup);
3558     n = global.sgammo;
3559     if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3560     drawTextAt(ammoX+16, 8-hhup, va("%d", n));
3561   } else if (player && player.holdItem isa ItemWeaponBow) {
3562     if (scumSmallHud) drawSpriteAt('sArrowRight', -1, ammoX+6, 8-hhup); else drawSpriteAt('sArrowRight', -1, ammoX+7, 12-hhup);
3563     n = global.arrows;
3564     if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3565     drawTextAt(ammoX+16, 8-hhup, va("%d", n));
3566   }
3568   // money
3569   if (scumSmallHud) drawSpriteAt('sDollarSignSmall', -1, moneyX+2, 3-hhup); else drawSpriteAt('sDollarSign', -1, moneyX, 8-hhup);
3570   drawTextAt(moneyX+16, 8-hhup, va("%d", stats.money-xmoney));
3572   // items
3573   Video.color = 0xff_ff_ff|ialpha;
3575   int ity = (scumSmallHud ? 18-hhup : 24-hhup);
3577   n = 8; //28;
3578   if (global.hasUdjatEye) {
3579     if (global.udjatBlink) drawSpriteAt('sUdjatEyeIcon2', -1, n, ity); else drawSpriteAt('sUdjatEyeIcon', -1, n, ity);
3580     n += 20;
3581   }
3582   if (global.hasAnkh) { drawSpriteAt('sAnkhIcon', -1, n, ity); n += 20; }
3583   if (global.hasCrown) { drawSpriteAt('sCrownIcon', -1, n, ity); n += 20; }
3584   if (global.hasKapala) {
3585          if (global.bloodLevel == 0) drawSpriteAt('sKapalaIcon', 0, n, ity);
3586     else if (global.bloodLevel <= 2) drawSpriteAt('sKapalaIcon', 1, n, ity);
3587     else if (global.bloodLevel <= 4) drawSpriteAt('sKapalaIcon', 2, n, ity);
3588     else if (global.bloodLevel <= 6) drawSpriteAt('sKapalaIcon', 3, n, ity);
3589     else if (global.bloodLevel <= 8) drawSpriteAt('sKapalaIcon', 4, n, ity);
3590     n += 20;
3591   }
3592   if (global.hasSpectacles) { drawSpriteAt('sSpectaclesIcon', -1, n, ity); n += 20; }
3593   if (global.hasGloves) { drawSpriteAt('sGlovesIcon', -1, n, ity); n += 20; }
3594   if (global.hasMitt) { drawSpriteAt('sMittIcon', -1, n, ity); n += 20; }
3595   if (global.hasSpringShoes) { drawSpriteAt('sSpringShoesIcon', -1, n, ity); n += 20; }
3596   if (global.hasJordans) { drawSpriteAt('sJordansIcon', -1, n, ity); n += 20; }
3597   if (global.hasSpikeShoes) { drawSpriteAt('sSpikeShoesIcon', -1, n, ity); n += 20; }
3598   if (global.hasCape) { drawSpriteAt('sCapeIcon', -1, n, ity); n += 20; }
3599   if (global.hasJetpack) { drawSpriteAt('sJetpackIcon', -1, n, ity); n += 20; }
3600   if (global.hasStickyBombs) { drawSpriteAt('sPasteIcon', -1, n, ity); n += 20; }
3601   if (global.hasCompass) { drawSpriteAt('sCompassIcon', -1, n, ity); n += 20; }
3602   if (global.hasParachute) { drawSpriteAt('sParachuteIcon', -1, n, ity); n += 20; }
3604   if (player.holdItem && player.holdItem.objName == 'Bow' && global.arrows > 0) {
3605     int m = 1;
3606     float malpha = 1;
3607     while (m <= global.arrows && m <= 20 && malpha > 0) {
3608       Video.color = trunc(malpha*255)<<24|0xff_ff_ff;
3609       drawSpriteAt('sArrowIcon', -1, n, ity);
3610       n += 4;
3611       if (m > 10) malpha -= 0.1; //and global.arrows > 20 malpha -= 0.1
3612       m += 1;
3613     }
3614   }
3616   if (xmoney > 0) {
3617     sprStore.loadFont('sFontSmall');
3618     Video.color = 0xff_ff_00|talpha;
3619     if (scumSmallHud) drawTextAt(moneyX+8, 8+8-hhup, va("+%d", xmoney));
3620     else drawTextAt(moneyX, 8+16-hhup, va("+%d", xmoney));
3621   }
3623   Video.color = 0xff_ff_ff;
3624   if (global.config.ghostShowTime) renderGhostTimer(currFrameDelta);
3628 // ////////////////////////////////////////////////////////////////////////// //
3629 // x0 and y0 are non-scaled (and will be scaled)
3630 final void drawTextAtS3 (int x0, int y0, string text, optional int hiColor1, optional int hiColor2) {
3631   if (!text) return;
3632   x0 *= 3;
3633   y0 *= 3;
3634   sprStore.renderTextWithHighlight(x0, y0, text, 3, hiColor1!optional, hiColor2!optional);
3638 final void drawTextAtS3Centered (int y0, string text, optional int hiColor1, optional int hiColor2) {
3639   if (!text) return;
3640   int x0 = (viewWidth-sprStore.getTextWidth(text, 3, specified_hiColor1, specified_hiColor2))/2;
3641   sprStore.renderTextWithHighlight(x0, y0*3, text, 3, hiColor1!optional, hiColor2!optional);
3645 void renderHelpOverlay () {
3646   Video.color = 0;
3647   Video.fillRect(0, 0, viewWidth, viewHeight);
3649   int tx = 16;
3650   int txoff = 0; // text x pos offset (for multi-color lines)
3651   int ty = 8;
3652   if (gameHelpScreen) {
3653     sprStore.loadFont('sFontSmall');
3654     Video.color = 0xff_ff_ff;
3655     drawTextAtS3Centered(ty, va("HELP (PAGE ~%d~ OF ~%d~)", gameHelpScreen, MaxGameHelpScreen), 0xff_ff_00);
3656     ty += 24;
3657   }
3659   if (gameHelpScreen == 1) {
3660     sprStore.loadFont('sFontSmall');
3661     Video.color = 0xff_ff_00; drawTextAtS3(tx, ty, "INVENTORY BASICS"); ty += 16;
3662     Video.color = 0xff_ff_ff;
3663     drawTextAtS3(tx, ty, global.expandString("Press $SWITCH to cycle through items."), 0x00_ff_00);
3664     ty += 8;
3665     ty += 56;
3666     Video.color = 0xff_ff_ff;
3667     drawSpriteAtS3('sHelpSprite1', -1, 64, 96);
3668   } else if (gameHelpScreen == 2) {
3669     sprStore.loadFont('sFontSmall');
3670     Video.color = 0xff_ff_00;
3671     drawTextAtS3(tx, ty, "SELLING TO SHOPKEEPERS"); ty += 16;
3672     Video.color = 0xff_ff_ff;
3673     drawTextAtS3(tx, ty, global.expandString("Press $PAY to offer your currently"), 0x00_ff_00); ty += 8;
3674     drawTextAtS3(tx, ty, "held item to the shopkeeper."); ty += 16;
3675     drawTextAtS3(tx, ty, "If the shopkeeper is interested, "); ty += 8;
3676     //drawTextAtS3(tx, ty, global.expandString("press $PAY again to complete the sale."), 0x00_ff_00); ty += 72;
3677     drawTextAtS3(tx, ty, global.expandString("press $PAY again to complete"), 0x00_ff_00);
3678     drawTextAtS3(tx, ty+8, "the sale.");
3679     ty += 72;
3680     drawSpriteAtS3('sHelpSell', -1, 112, 100);
3681     drawTextAtS3(tx, ty, "Purchasing goods from the shopkeeper"); ty += 8;
3682     drawTextAtS3(tx, ty, "will increase the funds he has"); ty += 8;
3683     drawTextAtS3(tx, ty, "available to buy your unwanted stuff."); ty += 8;
3684   } else {
3685     // map
3686     sprStore.loadFont('sFont');
3687     Video.color = 0xff_ff_ff;
3688     drawTextAtS3(136, 8, "MAP");
3690     if (lg.mapSprite && (isNormalLevel() || isTransitionRoom())) {
3691       Video.color = 0xff_ff_00;
3692       drawTextAtS3Centered(24, lg.mapTitle);
3694       auto spf = sprStore[lg.mapSprite].frames[0];
3695       int mapX = 160-spf.width/2;
3696       int mapY = 120-spf.height/2;
3697       //mapTitleX = 160-string_length(global.mapTitle)*8/2;
3699       Video.color = 0xff_ff_ff;
3700       drawSpriteAtS3(lg.mapSprite, -1, mapX, mapY);
3702       if (lg.mapSprite != 'sMapDefault') {
3703         int mx = -1, my = -1;
3705         // set position of player icon
3706         switch (global.currLevel) {
3707           case 1: mx = 81; my = 22; break;
3708           case 2: mx = 113; my = 63; break;
3709           case 3: mx = 197; my = 86; break;
3710           case 4: mx = 133; my = 109; break;
3711           case 5: mx = 181; my = 22; break;
3712           case 6: mx = 126; my = 64; break;
3713           case 7: mx = 158; my = 112; break;
3714           case 8: mx = 66; my = 80; break;
3715           case 9: mx = 30; my = 26; break;
3716           case 10: mx = 88; my = 54; break;
3717           case 11: mx = 148; my = 81; break;
3718           case 12: mx = 210; my = 205; break;
3719           case 13: mx = 66; my = 17; break;
3720           case 14: mx = 146; my = 17; break;
3721           case 15: mx = 82; my = 77; break;
3722           case 16: mx = 178; my = 81; break;
3723         }
3725         if (mx >= 0) {
3726           int plrx = mx+player.ix/16;
3727           int plry = my+player.iy/16;
3728           if (isTransitionRoom()) { plrx = mx+20; plry = my+16; }
3729           name plrspr = 'sMapSpelunker';
3730                if (global.isDamsel) plrspr = 'sMapDamsel';
3731           else if (global.isTunnelMan) plrspr = 'sMapTunnel';
3732           auto ss = sprStore[plrspr];
3733           drawSpriteAtS3(plrspr, (pausedTime/2)%ss.frames.length, mapX+plrx, mapY+plry);
3734           // exit door icon
3735           if (global.hasCompass && allExits.length) {
3736             drawSpriteAtS3('sMapRedDot', -1, mapX+mx+allExits[0].ix/16, mapY+my+allExits[0].iy/16);
3737           }
3738         }
3739       }
3740     }
3741   }
3743   sprStore.loadFont('sFontSmall');
3744   Video.color = 0xff_ff_00;
3745   drawTextAtS3Centered(232, "PRESS ~SPACE~/~LEFT~/~RIGHT~ TO CHANGE PAGE", 0x00_ff_00);
3747   Video.color = 0xff_ff_ff;
3751 void renderPauseOverlay () {
3752   //drawTextAt(256, 432, "PAUSED", scale);
3754   if (gameShowHelp) { renderHelpOverlay(); return; }
3756   Video.color = 0xff_ff_00;
3757   //int hiColor = 0x00_ff_00;
3759   int n = 120;
3760   if (isTutorialRoom()) {
3761     sprStore.loadFont('sFont');
3762     drawTextAtS3(40, n-24, "TUTORIAL CAVE");
3763   } else if (isNormalLevel()) {
3764     sprStore.loadFont('sFont');
3766     drawTextAtS3Centered(n-32, va("LEVEL ~%d~", global.currLevel), 0x00_ff_00);
3768     sprStore.loadFont('sFontSmall');
3770     int depth = round((174.8*(global.currLevel-1)+(player.iy+8)*0.34)*(global.config.scumMetric ? 0.3048 : 1.0)*10);
3771     string depthStr = va("DEPTH: ~%d.%d~ %s", depth/10, depth%10, (global.config.scumMetric ? "METRES" : "FEET"));
3772     drawTextAtS3Centered(n-16, depthStr, 0x00_ff_00);
3774     n += 16;
3775     drawTextAtS3Centered(n, va("MONEY: ~%d~", stats.money), 0x00_ff_00);
3776     drawTextAtS3Centered(n+16, va("KILLS: ~%d~", stats.countKills), 0x00_ff_00);
3777     drawTextAtS3Centered(n+32, va("SAVES: ~%d~", stats.damselsSaved), 0x00_ff_00);
3778     drawTextAtS3Centered(n+48, va("TIME: ~%s~", time2str(time/30)), 0x00_ff_00);
3779     drawTextAtS3Centered(n+64, va("LEVEL TIME: ~%s~", time2str((time-levelStartTime)/30)), 0x00_ff_00);
3780   }
3782   sprStore.loadFont('sFontSmall');
3783   Video.color = 0xff_ff_ff;
3784   drawTextAtS3Centered(240-2-8, "~ESC~-RETURN  ~F10~-QUIT  ~CTRL+DEL~-SUICIDE", 0xff_7f_00);
3785   drawTextAtS3Centered(2, "~O~PTIONS  REDEFINE ~K~EYS  ~S~TATISTICS", 0xff_7f_00);
3789 // ////////////////////////////////////////////////////////////////////////// //
3790 transient int drawLoot;
3791 transient int drawPosX, drawPosY;
3793 void resetTransitionOverlay () {
3794   drawLoot = 0;
3795   drawPosX = 100;
3796   drawPosY = 83;
3800 // current game, uncollapsed
3801 struct LevelStatInfo {
3802   name aname;
3803   // for transition screen
3804   bool render;
3805   int x, y;
3810 void thinkFrameTransition () {
3811   if (drawLoot == 0) {
3812     if (drawPosX > 272) {
3813       drawPosX = 100;
3814       drawPosY += 2;
3815       if (drawPosY > 83+4) drawPosY = 83;
3816     }
3817   } else if (drawPosX > 232) {
3818     drawPosX = 96;
3819     drawPosY += 2;
3820     if (drawPosY > 91+4) drawPosY = 91;
3821   }
3825 void renderTransitionOverlay () {
3826   sprStore.loadFont('sFontSmall');
3827   Video.color = 0xff_ff_00;
3828   //else if (global.currLevel-1 &lt; 1) draw_text(32, 48, "TUTORIAL CAVE COMPLETED!");
3829   //else draw_text(32, 48, "LEVEL "+string(global.currLevel-1)+" COMPLETED!");
3830   drawTextAt(32, 48, va("LEVEL ~%d~ COMPLETED!", global.currLevel), hiColor1:0x00_ff_ff);
3831   Video.color = 0xff_ff_ff;
3832   drawTextAt(32, 64, va("TIME  = ~%s~", time2str((levelEndTime-levelStartTime)/30)), hiColor1:0xff_ff_00);
3834   if (/*stats.collected.length == 0*/stats.money <= levelMoneyStart) {
3835     drawTextAt(32, 80, "LOOT  = ~NONE~", hiColor1:0xff_00_00);
3836   } else {
3837     drawTextAt(32, 80, va("LOOT  = ~%d~", stats.money-levelMoneyStart), hiColor1:0xff_ff_00);
3838   }
3840   if (stats.kills.length == 0) {
3841     drawTextAt(32, 96, "KILLS = ~NONE~", hiColor1:0x00_ff_00);
3842   } else {
3843     drawTextAt(32, 96, va("KILLS = ~%d~", stats.kills.length), hiColor1:0xff_ff_00);
3844   }
3846   drawTextAt(32, 112, va("MONEY = ~%d~", stats.money), hiColor1:0xff_ff_00);
3850 // ////////////////////////////////////////////////////////////////////////// //
3851 private transient array!MapEntity renderVisibleCids;
3852 private transient array!MapEntity renderVisibleLights;
3853 private transient array!MapTile renderFrontTiles; // normal, with fg
3855 final bool renderSortByDepth (MapEntity oa, MapEntity ob) {
3856   auto da = oa.depth, db = ob.depth;
3857   if (da == db) return (oa.objId < ob.objId);
3858   return (da < db);
3862 const int RenderEdgePixNormal = 64;
3863 const int RenderEdgePixLight = 256;
3865 #ifndef EXPERIMENTAL_RENDER_CACHE
3866 enum skipListCreation = false;
3867 #endif
3869 void renderWithOfs (int xofs, int yofs, float currFrameDelta) {
3870   int scale = global.scale;
3872   // don't touch framebuffer alpha
3873   Video.colorMask = Video::CMask.Colors;
3874   Video.color = 0xff_ff_ff;
3876   /*
3877   Video::ScissorRect scsave;
3878   bool doRestoreGL = false;
3880   if (viewOffsetX > 0 || viewOffsetY > 0) {
3881     doRestoreGL = true;
3882     Video.getScissor(scsave);
3883     Video.scissorCombine(viewOffsetX, viewOffsetY, viewWidth, viewHeight);
3884     Video.glPushMatrix();
3885     Video.glTranslate(viewOffsetX, viewOffsetY);
3886     //Video.glTranslate(-550, 0);
3887     //Video.glScale(1, 1);
3888   }
3889   */
3892   bool isDarkLevel = global.darkLevel;
3894   if (isDarkLevel) {
3895     switch (global.config.scumPlayerLit) {
3896       case 0: player.lightRadius = 0; break; // never
3897       case 1: // only in "scumDarkness"
3898         player.lightRadius = (global.config.scumDarkness >= 2 ? 96 : 32);
3899         break;
3900       case 2:
3901         player.lightRadius = 96;
3902         break;
3903     }
3904   }
3906   // render cave background
3907   if (levBGImg) {
3908     int tsz = 16*scale;
3909     int bgw = levBGImg.tex.width*scale;
3910     int bgh = levBGImg.tex.height*scale;
3911     int bgTW = (tilesWidth*tsz+bgw-1)/bgw;
3912     int bgTH = (tilesHeight*tsz+bgh-1)/bgh;
3913     int bgX0 = max(0, xofs/bgw);
3914     int bgY0 = max(0, yofs/bgh);
3915     int bgX1 = min(bgTW, (xofs+viewWidth+bgw-1)/bgw);
3916     int bgY1 = min(bgTH, (yofs+viewHeight+bgh-1)/bgh);
3917     foreach (int ty; bgY0..bgY1) {
3918       foreach (int tx; bgX0..bgX1) {
3919         int x0 = tx*bgw-xofs;
3920         int y0 = ty*bgh-yofs;
3921         levBGImg.tex.blitAt(x0, y0, scale);
3922       }
3923     }
3924   }
3926   int RenderEdgePix = (global.darkLevel ? RenderEdgePixLight : RenderEdgePixNormal);
3928   // render background tiles
3929   for (MapBackTile bt = backtiles; bt; bt = bt.next) {
3930     bt.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3931   }
3933   // collect visible special tiles
3934 #ifdef EXPERIMENTAL_RENDER_CACHE
3935   bool skipListCreation = (lastRenderTime == time && renderVisibleCids.length); //FIXME
3936 #endif
3938   if (!skipListCreation) {
3939     renderVisibleCids.clear();
3940     renderVisibleLights.clear();
3941     renderFrontTiles.clear();
3943     int endVX = xofs+viewWidth;
3944     int endVY = yofs+viewHeight;
3946     // add player
3947     //int cnt = 0;
3948     if (player && player.visible) renderVisibleCids[$] = player; // include player in render list
3950     //FIXME: drop lit objects which cannot affect visible area
3951     if (scale > 1) {
3952       // collect visible objects
3953       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)) {
3954         if (!o.visible) continue;
3955         auto tile = MapTile(o);
3956         if (tile) {
3957           if (isDarkLevel && (o.lightRadius > 4 || tile.litWholeTile)) renderVisibleLights[$] = o;
3958           if (tile.invisible) continue;
3959           if (tile.bgfront) renderFrontTiles[$] = tile;
3960           if (tile.bgback) tile.drawWithOfsBack(xofs, yofs, scale, currFrameDelta);
3961         } else {
3962           if (isDarkLevel && o.lightRadius > 4) renderVisibleLights[$] = o;
3963         }
3964         // check if the object is really visible -- this will speed up later sorting
3965         int fx0, fy0, fx1, fy1;
3966         auto spf = o.getSpriteFrame(default, out fx0, out fy0, out fx1, out fy1);
3967         if (!spf) continue; // no sprite -- nothing to draw (no, really)
3968         int ix = o.ix, iy = o.iy;
3969         int x0 = (ix+fx0)*scale, y0 = (iy+fy0)*scale;
3970         int x1 = (ix+fx1)*scale, y1 = (iy+fy1)*scale;
3971         if (x1 <= xofs || y1 <= yofs || x0 >= endVX || y0 >= endVY) {
3972           //++cnt;
3973           continue;
3974         }
3975         renderVisibleCids[$] = o;
3976       }
3977     } else {
3978       foreach (MapEntity o; objGrid.allObjects()) {
3979         if (!o.visible) continue;
3980         auto tile = MapTile(o);
3981         if (tile) {
3982           if (isDarkLevel && (o.lightRadius > 4 || tile.litWholeTile)) renderVisibleLights[$] = o;
3983           if (tile.invisible) continue;
3984           if (tile.bgfront) renderFrontTiles[$] = tile;
3985           if (tile.bgback) tile.drawWithOfsBack(xofs, yofs, scale, currFrameDelta);
3986         } else {
3987           if (isDarkLevel && o.lightRadius > 4) renderVisibleLights[$] = o;
3988         }
3989         renderVisibleCids[$] = o;
3990       }
3991     }
3992     //writeln("::: ", cnt, " invisible objects dropped");
3994     renderVisibleCids.sort(&renderSortByDepth);
3995     lastRenderTime = time;
3996   }
3998   auto depth4Start = 0;
3999   foreach (auto xidx, MapEntity o; renderVisibleCids) {
4000     if (o.depth >= 4) {
4001       depth4Start = xidx;
4002       break;
4003     }
4004   }
4006   bool playerPowerupRendered = false;
4008   // render objects (part one: depth > 3)
4009   foreach (auto idx; depth4Start..renderVisibleCids.length; reverse) {
4010     MapEntity o = renderVisibleCids[idx];
4011     // 1000 is an ordinary tile
4012     if (!playerPowerupRendered && o.depth <= 1200) {
4013       playerPowerupRendered = true;
4014       // so ducking player will have it's cape correctly rendered
4015       if (player.visible) player.drawPrePrePowerupWithOfs(xofs, yofs, scale, currFrameDelta);
4016     }
4017     //if (idx && renderSortByDepth(o, renderVisibleCids[idx-1])) writeln("VIOLATION!");
4018     o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
4019   }
4021   // render object (part two: front tile parts, depth 3.5)
4022   foreach (MapTile tile; renderFrontTiles) {
4023     tile.drawWithOfsFront(xofs, yofs, scale, currFrameDelta);
4024   }
4026   // render objects (part three: depth <= 3)
4027   foreach (auto idx; 0..depth4Start; reverse) {
4028     MapEntity o = renderVisibleCids[idx];
4029     o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
4030     //done above;if (isDarkLevel && (o.lightRadius > 4 || (o isa MapTile && MapTile(o).litWholeTile))) renderVisibleLights[$] = o;
4031   }
4033   // render player post-effects, so cape or jetpack will be correctly rendered on level exit, for example
4034   player.lastDrawWithOfs(xofs, yofs, scale, currFrameDelta);
4036   // lighting
4037   if (isDarkLevel) {
4038     auto ltex = bgtileStore.lightTexture('ltx512', 512);
4040     // set screen alpha to min
4041     Video.colorMask = Video::CMask.Alpha;
4042     Video.blendMode = Video::BlendMode.None;
4043     Video.color = 0xff_ff_ff_ff;
4044     Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
4045     //Video.colorMask = Video::CMask.All;
4047     // blend lights
4048     // also, stencil 'em, so we can filter dark areas
4049     Video.textureFiltering = true;
4050     Video.stencil = true;
4051     Video.stencilFunc(Video::StencilFunc.Always, 1);
4052     Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Replace);
4053     Video.alphaTestFunc = Video::AlphaFunc.Greater;
4054     Video.alphaTestVal = 0.03+0.011*global.config.darknessDarkness;
4055     Video.color = 0xff_ff_ff;
4056     Video.blendFunc = Video::BlendFunc.Max;
4057     Video.blendMode = Video::BlendMode.Blend; // anything except `Normal`
4058     Video.colorMask = Video::CMask.Alpha;
4060     foreach (MapEntity e; renderVisibleLights) {
4061       int xi, yi;
4062       e.getInterpCoords(currFrameDelta, scale, out xi, out yi);
4063       auto tile = MapTile(e);
4064       if (tile && tile.litWholeTile) {
4065         //Video.color = 0xff_ff_ff;
4066         Video.fillRect(xi-xofs, yi-yofs, e.width*scale, e.height*scale);
4067       }
4068       int lrad = e.lightRadius;
4069       if (lrad < 4) continue; // just in case
4070       lrad += 8;
4071       float lightscale = float(lrad*scale)/float(ltex.tex.width);
4072 #ifdef OLD_LIGHT_OFFSETS
4073       int fx0, fy0, fx1, fy1;
4074       bool doMirror;
4075       auto spf = e.getSpriteFrame(out doMirror, out fx0, out fy0, out fx1, out fy1);
4076       if (spf) {
4077         xi += (fx1-fx0)*scale/2;
4078         yi += (fy1-fy0)*scale/2;
4079       }
4080 #else
4081       int lxofs, lyofs;
4082       e.getLightOffset(out lxofs, out lyofs);
4083       xi += lxofs*scale;
4084       yi += lyofs*scale;
4086 #endif
4087       lrad = lrad*scale/2;
4088       xi -= xofs+lrad;
4089       yi -= yofs+lrad;
4090       ltex.tex.blitAt(xi, yi, lightscale);
4091     }
4092     Video.textureFiltering = false;
4094     // modify only lit parts
4095     Video.stencilFunc(Video::StencilFunc.Equal, 1);
4096     Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Keep);
4097     // multiply framebuffer colors by framebuffer alpha
4098     Video.color = 0xff_ff_ff; // it doesn't matter
4099     Video.blendFunc = Video::BlendFunc.Add;
4100     Video.blendMode = Video::BlendMode.DstMulDstAlpha;
4101     Video.colorMask = Video::CMask.Colors;
4102     Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
4104     // filter unlit parts
4105     Video.stencilFunc(Video::StencilFunc.NotEqual, 1);
4106     Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Keep);
4107     Video.blendFunc = Video::BlendFunc.Add;
4108     Video.blendMode = Video::BlendMode.Filter;
4109     Video.colorMask = Video::CMask.Colors;
4110     Video.color = 0x00_00_18+0x00_00_10*global.config.darknessDarkness;
4111     //Video.color = 0x00_00_18;
4112     //Video.color = 0x00_00_38;
4113     Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
4115     // restore defaults
4116     Video.blendFunc = Video::BlendFunc.Add;
4117     Video.blendMode = Video::BlendMode.Normal;
4118     Video.colorMask = Video::CMask.All;
4119     Video.alphaTestFunc = Video::AlphaFunc.Always;
4120     Video.stencil = false;
4121   }
4123   // clear visible objects list (nope)
4124   //renderVisibleCids.clear();
4125   //renderVisibleLights.clear();
4128   if (global.config.drawHUD) renderHUD(currFrameDelta);
4129   renderCompass(currFrameDelta);
4131   float osdTimeLeft, osdTimeStart;
4132   string msg = osdGetMessage(out osdTimeLeft, out osdTimeStart);
4133   if (msg) {
4134     auto ct = GetTickCount();
4135     int msgScale = 3;
4136     sprStore.loadFont('sFontSmall');
4137     auto msgHeight = sprStore.getMultilineTextHeight(msg, msgScale);
4138     int x = viewWidth/2;
4139     int y = viewHeight-64-msgHeight;
4140     auto oldColor = Video.color;
4141     Video.color = 0xff_ff_00;
4142     if (osdTimeLeft < 0.5) {
4143       int alpha = 255-clamp(int(510*osdTimeLeft), 0, 255);
4144       Video.color = Video.color|(alpha<<24);
4145     } else if (ct-osdTimeStart < 0.5) {
4146       osdTimeStart = ct-osdTimeStart;
4147       int alpha = 255-clamp(int(510*osdTimeStart), 0, 255);
4148       Video.color = Video.color|(alpha<<24);
4149     }
4150     sprStore.renderMultilineTextCentered(x, y, msg, msgScale, 0x00_ff_00, 0xff_ff_ff);
4151     Video.color = oldColor;
4152   }
4154   int hiColor1, hiColor2;
4155   msg = osdGetTalkMessage(out hiColor1, out hiColor2);
4156   if (msg) {
4157     int msgScale = 2;
4158     sprStore.loadFont('sFontSmall');
4159     auto msgWidth = sprStore.getMultilineTextWidth(msg, processHighlights1:true, processHighlights2:true);
4160     auto msgHeight = sprStore.getMultilineTextHeight(msg);
4161     auto msgWidthOrig = msgWidth*msgScale;
4162     auto msgHeightOrig = msgHeight*msgScale;
4163     if (msgWidth%16 != 0) msgWidth = (msgWidth|0x0f)+1;
4164     if (msgHeight%16 != 0) msgHeight = (msgHeight|0x0f)+1;
4165     msgWidth *= msgScale;
4166     msgHeight *= msgScale;
4167     int x = (viewWidth-msgWidth)/2;
4168     int y = 32*msgScale;
4169     auto oldColor = Video.color;
4170     // draw text frame and text background
4171     Video.color = 0;
4172     Video.fillRect(x, y, msgWidth, msgHeight);
4173     Video.color = 0xff_ff_ff;
4174     for (int fdx = 0; fdx < msgWidth; fdx += 16*msgScale) {
4175       auto spf = sprStore['sMenuTop'].frames[0];
4176       spf.tex.blitAt(x+fdx, y-16*msgScale, msgScale);
4177       spf = sprStore['sMenuBottom'].frames[0];
4178       spf.tex.blitAt(x+fdx, y+msgHeight, msgScale);
4179     }
4180     for (int fdy = 0; fdy < msgHeight; fdy += 16*msgScale) {
4181       auto spf = sprStore['sMenuLeft'].frames[0];
4182       spf.tex.blitAt(x-16*msgScale, y+fdy, msgScale);
4183       spf = sprStore['sMenuRight'].frames[0];
4184       spf.tex.blitAt(x+msgWidth, y+fdy, msgScale);
4185     }
4186     {
4187       auto spf = sprStore['sMenuUL'].frames[0];
4188       spf.tex.blitAt(x-16*msgScale, y-16*msgScale, msgScale);
4189       spf = sprStore['sMenuUR'].frames[0];
4190       spf.tex.blitAt(x+msgWidth, y-16*msgScale, msgScale);
4191       spf = sprStore['sMenuLL'].frames[0];
4192       spf.tex.blitAt(x-16*msgScale, y+msgHeight, msgScale);
4193       spf = sprStore['sMenuLR'].frames[0];
4194       spf.tex.blitAt(x+msgWidth, y+msgHeight, msgScale);
4195     }
4196     Video.color = 0xff_ff_00;
4197     sprStore.renderMultilineText(x+(msgWidth-msgWidthOrig)/2, y+(msgHeight-msgHeightOrig)/2-3*msgScale, msg, msgScale, (hiColor1 == -1 ? 0x00_ff_00 : hiColor1), (hiColor2 == -1 ? 0xff_ff_ff : hiColor2));
4198     Video.color = oldColor;
4199   }
4201   if (inWinCutscene) renderWinCutsceneOverlay();
4202   if (inIntroCutscene) renderTitleCutsceneOverlay();
4203   if (isTransitionRoom()) renderTransitionOverlay();
4205   /*
4206   if (doRestoreGL) {
4207     Video.setScissor(scsave);
4208     Video.glPopMatrix();
4209   }
4210   */
4212   Video.color = 0xff_ff_ff;
4216 // ////////////////////////////////////////////////////////////////////////// //
4217 final class!MapObject findGameObjectClassByName (name aname) {
4218   if (!aname) return none; // just in case
4219   auto co = FindClassByGameObjName(aname);
4220   if (!co) {
4221     writeln("***UNKNOWN GAME OBJECT: '", aname, "'");
4222     return none;
4223   }
4224   co = GetClassReplacement(co);
4225   if (!co) FatalError("findGameObjectClassByName: WTF?!");
4226   if (!class!MapObject(co)) FatalError("findGameTileClassByName: object '%n' is not a map object, it is a `%n`", aname, GetClassName(co));
4227   return class!MapObject(co);
4231 final class!MapTile findGameTileClassByName (name aname) {
4232   if (!aname) return none; // just in case
4233   auto co = FindClassByGameObjName(aname);
4234   if (!co) return MapTile; // unknown names will be routed directly to tile object
4235   co = GetClassReplacement(co);
4236   if (!co) FatalError("findGameTileClassByName: WTF?!");
4237   if (!class!MapTile(co)) FatalError("findGameTileClassByName: object '%n' is not a tile, it is a `%n`", aname, GetClassName(co));
4238   return class!MapTile(co);
4242 final MapObject findAnyObjectOfType (name aname) {
4243   if (!aname) return none;
4244   auto cls = FindClassByGameObjName(aname);
4245   if (!cls) return none;
4246   foreach (MapObject obj; objGrid.allObjects(MapObject)) {
4247     if (obj.spectral) continue;
4248     if (obj isa cls) return obj;
4249   }
4250   return none;
4254 // ////////////////////////////////////////////////////////////////////////// //
4255 final bool isRopePlacedAt (int x, int y) {
4256   int[8] covered;
4257   foreach (ref auto v; covered) v = false;
4258   foreach (MapTile t; objGrid.inRectPix(x, y-8, 1, 17, precise:false, castClass:MapTileRope)) {
4259     //if (!cbIsRopeTile(t)) continue;
4260     if (t.ix != x) continue;
4261     if (t.iy == y) return true;
4262     foreach (int ty; t.iy..t.iy+8) {
4263       int d = ty-y;
4264       if (d >= 0 && d < covered.length) covered[d] = true;
4265     }
4266   }
4267   // check if the whole rope height is completely covered with ropes
4268   foreach (auto v; covered) if (!v) return false;
4269   return true;
4273 final MapTile CreateMapTile (int xpos, int ypos, name aname) {
4274   if (!aname) FatalError("cannot create typeless tile");
4275   auto tclass = findGameTileClassByName(aname);
4276   if (!tclass) return none;
4277   MapTile tile = SpawnObject(tclass);
4278   tile.global = global;
4279   tile.level = self;
4280   tile.objName = aname;
4281   tile.objType = aname; // just in case
4282   tile.fltx = xpos;
4283   tile.flty = ypos;
4284   tile.objId = ++lastUsedObjectId;
4285   if (!tile.initialize()) FatalError("cannot create tile '%n'", aname);
4286   return tile;
4290 final bool PutSpawnedMapTile (int x, int y, MapTile tile, optional bool putToGrid) {
4291   if (!tile || !tile.isInstanceAlive) return false;
4293   if (!putToGrid) putToGrid = (tile.moveable || tile.toSpecialGrid || tile.width != 16 || tile.height != 16 || x%16 != 0 || y%16 != 0);
4295   //writeln("putting tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (putToGrid ? "special " : ""), "grid, pos=(", x/16.0, ",", y/16.0, ")");
4297   if (!putToGrid) {
4298     int mapx = x/16, mapy = y/16;
4299     if (mapx < 0 || mapx >= tilesWidth || mapy < 0 || mapy >= tilesHeight) return false;
4300   }
4302   // if we already have rope tile there, there is no reason to add another one
4303   if (tile isa MapTileRope) {
4304     if (isRopePlacedAt(x, y)) return false;
4305   }
4307   // activate special or animated tile
4308   tile.active = tile.active || putToGrid || tile.moveable || tile.toSpecialGrid || tile.lava /*|| tile.water*/; // will be done in MakeMapTile
4309   // animated tiles must be active
4310   if (!tile.active) {
4311     auto spr = tile.getSprite();
4312     if (spr && spr.frames.length > 1) {
4313       writeln("activated animated tile '", tile.objName, "'");
4314       tile.active = true;
4315     }
4316   }
4318   tile.fltx = x;
4319   tile.flty = y;
4320   if (putToGrid) {
4321     //if (tile isa TitleTileCopy) writeln("*** PUTTING COPYRIGHT TILE");
4322     tile.toSpecialGrid = true;
4323     if (!tile.dontReplaceOthers && x&16 == 0 && y%16 == 0) {
4324       auto t = getTileAtGridAny(x/16, y/16);
4325       if (t && !t.immuneToReplacement) {
4326         writeln("REPLACING tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
4327         writeln("      NEW tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (tile.toSpecialGrid ? "special " : ""), "grid, pos=(", tile.fltx/16.0, ",", tile.flty/16.0, ")");
4328         t.instanceRemove();
4329       }
4330     }
4331     objGrid.insert(tile);
4332   } else {
4333     //writeln("SIZE: ", tilesWidth, "x", tilesHeight);
4334     setTileAtGrid(x/16, y/16, tile);
4335     auto t = getTileAtGridAny(x/16, y/16);
4336     /*
4337     if (t != tile) {
4338       writeln("putting tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (putToGrid ? "special " : ""), "grid, pos=(", x/16.0, ",", y/16.0, ")");
4339       checkTilesInRect(x/16, y/16, 16, 16, delegate bool (MapTile tile) {
4340         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, ")");
4341         return false;
4342       });
4343       FatalError("FUUUUUU");
4344     }
4345     */
4346   }
4348   if (tile.enter) registerEnter(tile);
4349   if (tile.exit) registerExit(tile);
4351   return true;
4355 // won't call `onDestroy()`
4356 final void RemoveMapTileFromGrid (int tileX, int tileY, optional string reason) {
4357   if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
4358     auto t = getTileAtGridAny(tileX, tileY);
4359     if (t) {
4360       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, ")");
4361       t.instanceRemove();
4362       checkWater = true;
4363     }
4364   }
4368 final MapTile MakeMapTile (int mapx, int mapy, name aname, optional bool putToGrid) {
4369   //writeln("tile at (", mapx, ",", mapy, "): ", aname);
4370   //if (aname == 'oLush') { MapObject fail; fail.initialize(); }
4371   //if (mapy >= tilesHeight) { writeln("OOM: ", aname, "; ty=", mapy, "; hgt=", tilesHeight); }
4372   if (mapx < 0 || mapx >= tilesWidth || mapy < 0 || mapy >= tilesHeight) return none;
4374   // if we already have rope tile there, there is no reason to add another one
4375   if (aname == 'oRope') {
4376     if (isRopePlacedAt(mapx*16, mapy*16)) return none;
4377   }
4379   auto tile = CreateMapTile(mapx*16, mapy*16, aname);
4380   if (!tile) return none;
4381   if (!PutSpawnedMapTile(mapx*16, mapy*16, tile, putToGrid!optional)) {
4382     delete tile;
4383     tile = none;
4384   }
4386   return tile;
4390 final MapTile MakeMapTileAtPix (int xpix, int ypix, name aname, optional bool putToGrid) {
4391   // if we already have rope tile there, there is no reason to add another one
4392   if (aname == 'oRope') {
4393     if (isRopePlacedAt(xpix, ypix)) return none;
4394   }
4396   auto tile = CreateMapTile(xpix, ypix, aname);
4397   if (!tile) return none;
4398   if (!PutSpawnedMapTile(xpix, ypix, tile, putToGrid!optional)) {
4399     delete tile;
4400     tile = none;
4401   }
4403   return tile;
4407 final MapTile MakeMapRopeTileAt (int x0, int y0) {
4408   // if we already have rope tile there, there is no reason to add another one
4409   if (isRopePlacedAt(x0, y0)) return none;
4411   auto tile = CreateMapTile(x0, y0, 'oRope');
4412   if (!PutSpawnedMapTile(x0, y0, tile, putToGrid:true)) {
4413     delete tile;
4414     tile = none;
4415   }
4417   return tile;
4421 // ////////////////////////////////////////////////////////////////////////// //
4422 final MapBackTile CreateBgTile (name sprName, optional int atx0, optional int aty0, optional int aw, optional int ah) {
4423   BackTileImage img = bgtileStore[sprName];
4424   auto res = SpawnObject(MapBackTile);
4425   res.global = global;
4426   res.level = self;
4427   res.bgt = img;
4428   res.bgtName = sprName;
4429   if (specified_atx0) res.tx0 = atx0;
4430   if (specified_aty0) res.ty0 = aty0;
4431   if (specified_aw) res.w = aw;
4432   if (specified_ah) res.h = ah;
4433   if (!res.initialize()) FatalError("cannot create background tile with image '%n'", sprName);
4434   return res;
4438 // ////////////////////////////////////////////////////////////////////////// //
4440 background The background asset from which the new tile will be extracted.
4441 left The x coordinate of the left of the new tile, relative to the background asset's top left corner.
4442 top The y coordinate of the top of the new tile, relative to the background assets top left corner.
4443 width The width of the tile.
4444 height The height of the tile.
4445 x The x position in the room to place the tile.
4446 y The y position in the room to place the tile.
4447 depth The depth at which to place the tile.
4449 final void MakeMapBackTile (name bgname, int left, int top, int width, int height, int x, int y, int depth) {
4450   if (width < 1 || height < 1 || !bgname) return;
4451   auto bgt = bgtileStore[bgname];
4452   if (!bgt) FatalError("cannot load background '%n'", bgname);
4453   MapBackTile bt = SpawnObject(MapBackTile);
4454   bt.global = global;
4455   bt.level = self;
4456   bt.objName = bgname;
4457   bt.bgt = bgt;
4458   bt.bgtName = bgname;
4459   bt.fltx = x;
4460   bt.flty = y;
4461   bt.tx0 = left;
4462   bt.ty0 = top;
4463   bt.w = width;
4464   bt.h = height;
4465   bt.depth = depth;
4466   // find a place for it
4467   if (!backtiles) {
4468     backtiles = bt;
4469     return;
4470   }
4471   // back tiles with the highest depth should come first
4472   MapBackTile ct = backtiles, cprev = none;
4473   while (ct && depth <= ct.depth) { cprev = ct; ct = ct.next; }
4474   // insert before ct
4475   if (cprev) {
4476     bt.next = cprev.next;
4477     cprev.next = bt;
4478   } else {
4479     bt.next = backtiles;
4480     backtiles = bt;
4481   }
4485 // ////////////////////////////////////////////////////////////////////////// //
4486 final MapObject SpawnMapObjectWithClass (class!MapObject oclass) {
4487   if (!oclass) return none;
4489   MapObject obj = SpawnObject(oclass);
4490   if (!obj) FatalError("cannot create map object '%n'", GetClassName(oclass));
4492   //if (aname == 'oShells' || aname == 'oShellSingle' || aname == 'oRopePile') writeln("*** CREATED '", aname, "'");
4494   obj.global = global;
4495   obj.level = self;
4496   obj.objId = ++lastUsedObjectId;
4498   return obj;
4502 final MapObject SpawnMapObject (name aname) {
4503   if (!aname) return none;
4504   auto res = SpawnMapObjectWithClass(findGameObjectClassByName(aname));
4505   if (res && !res.objType) res.objType = aname; // just in case
4506   return res;
4510 final MapObject PutSpawnedMapObject (int x, int y, MapObject obj) {
4511   if (!obj /*|| obj.global || obj.level*/) return none; // oops
4513   obj.fltx = x;
4514   obj.flty = y;
4515   if (!obj.initialize()) { delete obj; return none; } // not fatal
4517   insertObject(obj);
4519   return obj;
4523 final MapObject MakeMapObject (int x, int y, name aname) {
4524   MapObject obj = SpawnMapObject(aname);
4525   obj = PutSpawnedMapObject(x, y, obj);
4526   return obj;
4530 // ////////////////////////////////////////////////////////////////////////// //
4531 int winCutSceneTimer = -1;
4532 int winVolcanoTimer = -1;
4533 int winCutScenePhase = 0;
4534 int winSceneDrawStatus = 0;
4535 int winMoneyCount = 0;
4536 int winTime;
4537 bool winFadeOut = false;
4538 int winFadeLevel = 0;
4539 int winCutsceneSkip = 0; // 1: waiting for pay release; 2: pay released, do skip
4540 bool winCutsceneSwitchToNext = false;
4543 void startWinCutscene () {
4544   global.hasParachute = false;
4545   shakeLeft = 0;
4546   winCutsceneSwitchToNext = false;
4547   winCutsceneSkip = 0;
4548   isKeyPressed(GameConfig::Key.Pay);
4549   isKeyReleased(GameConfig::Key.Pay);
4551   auto olddel = ImmediateDelete;
4552   ImmediateDelete = false;
4553   clearWholeLevel();
4555   createEnd1Room();
4556   fixWallTiles();
4557   addBackgroundGfxDetails();
4559   levBGImgName = 'bgCave';
4560   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
4562   blockWaterChecking = true;
4563   fixLiquidTop();
4564   cleanDeadTiles();
4566   ImmediateDelete = olddel;
4567   CollectGarbage(true); // destroy delayed objects too
4569   if (dumpGridStats) objGrid.dumpStats();
4571   playerExited = false; // just in case
4572   playerExitDoor = none;
4574   osdClear();
4576   setupGhostTime();
4577   global.stopMusic();
4579   inWinCutscene = 1;
4580   winCutSceneTimer = -1;
4581   winCutScenePhase = 0;
4583   /+
4584   if (global.config.gameMode != GameConfig::GameMode.Vanilla) {
4585     if (global.config.bizarre) {
4586       global.yasmScore = 1;
4587       global.config.bizarrePlusTitle = true;
4588     }
4590     array!MapTile toReplace;
4591     forEachTile(delegate bool (MapTile t) {
4592       if (t.objType == 'oGTemple' ||
4593           t.objType == 'oIce' ||
4594           t.objType == 'oDark' ||
4595           t.objType == 'oBrick' ||
4596           t.objType == 'oLush')
4597       {
4598         toReplace[$] = t;
4599       }
4600       return false;
4601     });
4603     foreach (MapTile t; miscTileGrid.allObjects()) {
4604       if (t.objType == 'oGTemple' ||
4605           t.objType == 'oIce' ||
4606           t.objType == 'oDark' ||
4607           t.objType == 'oBrick' ||
4608           t.objType == 'oLush')
4609       {
4610         toReplace[$] = t;
4611       }
4612     }
4614     foreach (MapTile t; toReplace) {
4615       if (t.iy < 192) {
4616         t.cleanDeath = true;
4617             if (rand(1,120) == 1) instance_change(oGTemple, false);
4618         else if (rand(1,100) == 1) instance_change(oIce, false);
4619         else if (rand(1,90) == 1) instance_change(oDark, false);
4620         else if (rand(1,80) == 1) instance_change(oBrick, false);
4621         else if (rand(1,70) == 1) instance_change(oLush, false);
4622           }
4623       }
4624       with (oBrick)
4625       {
4626           if (y &lt; 192)
4627           {
4628               cleanDeath = true;
4629               if (rand(1,5) == 1) instance_change(oLush, false);
4630           }
4631       }
4632   }
4633   +/
4634   //!instance_create(0, 0, oBricks);
4636   //shakeToggle = false;
4637   //oPDummy.status = 2;
4639   //timer = 0;
4641   /+
4642   if (global.kaliPunish &gt;= 2) {
4643       instance_create(oPDummy.x, oPDummy.y+2, oBall2);
4644       obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
4645       obj.linkVal = 1;
4646       obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
4647       obj.linkVal = 2;
4648       obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
4649       obj.linkVal = 3;
4650       obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
4651       obj.linkVal = 4;
4652   }
4653   +/
4657 void startWinCutsceneVolcano () {
4658   global.hasParachute = false;
4659   /*
4660   writeln("VOLCANO HOLD 0: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4661   writeln("VOLCANO PICK 0: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4662   */
4664   shakeLeft = 0;
4665   winCutsceneSwitchToNext = false;
4666   auto olddel = ImmediateDelete;
4667   ImmediateDelete = false;
4668   clearWholeLevel();
4670   levBGImgName = '';
4671   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
4673   blockWaterChecking = true;
4675   ImmediateDelete = olddel;
4676   CollectGarbage(true); // destroy delayed objects too
4678   spawnPlayerAt(2*16+8, 11*16+8);
4679   player.dir = MapEntity::Dir.Right;
4681   playerExited = false; // just in case
4682   playerExitDoor = none;
4684   osdClear();
4686   setupGhostTime();
4687   global.stopMusic();
4689   inWinCutscene = 2;
4690   winCutSceneTimer = -1;
4691   winCutScenePhase = 0;
4693   MakeMapTile(0, 0, 'oEnd2BG');
4694   realViewStart.x = 0;
4695   realViewStart.y = 0;
4696   viewStart.x = 0;
4697   viewStart.y = 0;
4699   viewMin.x = 0;
4700   viewMin.y = 0;
4701   viewMax.x = 320;
4702   viewMax.y = 240;
4704   player.dead = false;
4705   player.active = true;
4706   player.visible = false;
4707   player.removeBallAndChain(temp:true);
4708   player.stunned = false;
4709   player.status = MapObject::FALLING;
4710   if (player.holdItem) player.holdItem.visible = false;
4711   player.fltx = 320/2;
4712   player.flty = 0;
4714   /*
4715   writeln("VOLCANO HOLD 1: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4716   writeln("VOLCANO PICK 1: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4717   */
4721 void startWinCutsceneWinFall () {
4722   global.hasParachute = false;
4723   /*
4724   writeln("FALL HOLD 0: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4725   writeln("FALL PICK 0: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4726   */
4728   shakeLeft = 0;
4729   winCutsceneSwitchToNext = false;
4731   auto olddel = ImmediateDelete;
4732   ImmediateDelete = false;
4733   clearWholeLevel();
4735   createEnd3Room();
4736   setMenuTilesVisible(false);
4737   //fixWallTiles();
4738   //addBackgroundGfxDetails();
4740   levBGImgName = '';
4741   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
4743   blockWaterChecking = true;
4744   fixLiquidTop();
4745   cleanDeadTiles();
4747   ImmediateDelete = olddel;
4748   CollectGarbage(true); // destroy delayed objects too
4750   if (dumpGridStats) objGrid.dumpStats();
4752   playerExited = false; // just in case
4753   playerExitDoor = none;
4755   osdClear();
4757   setupGhostTime();
4758   global.stopMusic();
4760   inWinCutscene = 3;
4761   winCutSceneTimer = -1;
4762   winCutScenePhase = 0;
4764   player.dead = false;
4765   player.active = true;
4766   player.visible = false;
4767   player.removeBallAndChain(temp:true);
4768   player.stunned = false;
4769   player.status = MapObject::FALLING;
4770   if (player.holdItem) player.holdItem.visible = false;
4771   player.fltx = 320/2;
4772   player.flty = 0;
4774   winSceneDrawStatus = 0;
4775   winMoneyCount = 0;
4777   winFadeOut = false;
4778   winFadeLevel = 0;
4780   /*
4781   writeln("FALL HOLD 1: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4782   writeln("FALL PICK 1: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4783   */
4787 void setGameOver () {
4788   if (inWinCutscene) {
4789     player.visible = false;
4790     player.removeBallAndChain(temp:true);
4791     if (player.holdItem) player.holdItem.visible = false;
4792   }
4793   player.dead = true;
4794   if (inWinCutscene > 0) {
4795     winFadeOut = true;
4796     winFadeLevel = 255;
4797     winSceneDrawStatus = 8;
4798   }
4802 MapTile findEndPlatTile () {
4803   return forEachTile(delegate bool (MapTile t) { return (t isa MapTileEndPlat); }, castClass:MapTileEndPlat);
4807 MapObject findBigTreasure () {
4808   return forEachObject(delegate bool (MapObject o) { return (o isa MapObjectBigTreasure); }, castClass:MapObjectBigTreasure);
4812 void setMenuTilesVisible (bool vis) {
4813   if (vis) {
4814     forEachTile(delegate bool (MapTile t) {
4815       if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4816         t.invisible = false;
4817       }
4818       return false;
4819     });
4820   } else {
4821     forEachTile(delegate bool (MapTile t) {
4822       if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4823         t.invisible = true;
4824       }
4825       return false;
4826     });
4827   }
4831 void setMenuTilesOnTop () {
4832   forEachTile(delegate bool (MapTile t) {
4833     if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4834       t.depth = 1;
4835     }
4836     return false;
4837   });
4841 void winCutscenePlayerControl (PlayerPawn plr) {
4842   auto payPress = isKeyPressed(GameConfig::Key.Pay);
4843   auto payRelease = isKeyReleased(GameConfig::Key.Pay);
4845   switch (winCutsceneSkip) {
4846     case 0: // nothing was pressed
4847       if (payPress) winCutsceneSkip = 1;
4848       break;
4849     case 1: // waiting for pay release
4850       if (payRelease) winCutsceneSkip = 2;
4851       break;
4852     case 2: // pay released, do skip
4853       setGameOver();
4854       return;
4855   }
4857   // first winning room
4858   if (inWinCutscene == 1) {
4859     if (plr.ix < 448+8) {
4860       plr.kRight = true;
4861       return;
4862     }
4864     // waiting for chest to open
4865     if (winCutScenePhase == 0) {
4866       winCutSceneTimer = 120/2;
4867       winCutScenePhase = 1;
4868       return;
4869     }
4871     // spawn big idol
4872     if (winCutScenePhase == 1) {
4873       if (--winCutSceneTimer == 0) {
4874         winCutScenePhase = 2;
4875         winCutSceneTimer = 20;
4876         forEachObject(delegate bool (MapObject o) {
4877           if (o isa MapObjectBigChest) {
4878             o.setSprite(global.config.gameMode == GameConfig::GameMode.Vanilla ? 'sBigChestOpen' : 'sBigChestOpen2');
4879             auto treasure = MakeMapObject(o.ix, o.iy, 'oBigTreasure');
4880             if (treasure) {
4881               treasure.yVel = -4;
4882               treasure.xVel = -3;
4883               o.playSound('sndClick');
4884               //!!!if (global.config.gameMode != GameConfig::GameMode.Vanilla) scrSprayGems(oBigChest.x+24, oBigChest.y+24);
4885             }
4886           }
4887           return false;
4888         });
4889       }
4890       return;
4891     }
4893     // lava pump wait
4894     if (winCutScenePhase == 2) {
4895       if (--winCutSceneTimer == 0) {
4896         winCutScenePhase = 3;
4897         winCutSceneTimer = 50;
4898       }
4899       return;
4900     }
4902     // lava pump start
4903     if (winCutScenePhase == 3) {
4904       auto ep = findEndPlatTile();
4905       if (ep) MakeMapObject(ep.ix+global.randOther(0, 80), /*ep.iy*/192+32, 'oBurn');
4906       if (--winCutSceneTimer == 0) {
4907         winCutScenePhase = 4;
4908         winCutSceneTimer = 10;
4909         if (ep) MakeMapObject(ep.ix, ep.iy+30, 'oLavaSpray');
4910         scrShake(9999);
4911       }
4912       return;
4913     }
4915     // lava pump first accel
4916     if (winCutScenePhase == 4) {
4917       if (--winCutSceneTimer == 0) {
4918         forEachObject(delegate bool (MapObject o) {
4919           if (o isa MapObjectLavaSpray) o.yAcc = -0.1;
4920           return false;
4921         });
4922       }
4923     }
4925     // lava pump complete
4926     if (winCutScenePhase == 5) {
4927       if (--winCutSceneTimer == 0) {
4928         //if (oLavaSpray) oLavaSpray.yAcc = -0.1;
4929         startWinCutsceneVolcano();
4930       }
4931       return;
4932     }
4933     return;
4934   }
4937   // volcano room
4938   if (inWinCutscene == 2) {
4939     plr.flty = 0;
4941     // initialize
4942     if (winCutScenePhase == 0) {
4943       winCutSceneTimer = 50;
4944       winCutScenePhase = 1;
4945       winVolcanoTimer = 10;
4946       return;
4947     }
4949     if (winVolcanoTimer > 0) {
4950       if (--winVolcanoTimer == 0) {
4951         MakeMapObject(224+global.randOther(0,48), 144+global.randOther(0,8), 'oVolcanoFlame');
4952         winVolcanoTimer = global.randOther(10, 20);
4953       }
4954     }
4956     // plr sil
4957     if (winCutScenePhase == 1) {
4958       if (--winCutSceneTimer == 0) {
4959         winCutSceneTimer = 30;
4960         winCutScenePhase = 2;
4961         auto sil = MakeMapObject(240, 132, 'oPlayerSil');
4962         //sil.xVel = -6;
4963         //sil.yVel = -8;
4964       }
4965       return;
4966     }
4968     // treasure sil
4969     if (winCutScenePhase == 2) {
4970       if (--winCutSceneTimer == 0) {
4971         winCutScenePhase = 3;
4972         auto sil = MakeMapObject(240, 132, 'oTreasureSil');
4973         //sil.xVel = -6;
4974         //sil.yVel = -8;
4975       }
4976       return;
4977     }
4979     return;
4980   }
4982   // winning camel room
4983   if (inWinCutscene == 3) {
4984     //if (!player.holdItem)  writeln("SCENE 3: LOST ITEM!");
4986     if (!plr.visible) plr.flty = -32;
4988     // initialize
4989     if (winCutScenePhase == 0) {
4990       winCutSceneTimer = 50;
4991       winCutScenePhase = 1;
4992       return;
4993     }
4995     // fall sound
4996     if (winCutScenePhase == 1) {
4997       if (--winCutSceneTimer == 0) {
4998         winCutSceneTimer = 50;
4999         winCutScenePhase = 2;
5000         plr.playSound('sndPFall');
5001         plr.visible = true;
5002         plr.active = true;
5003         writeln("MUST BE CHAINED: ", plr.mustBeChained);
5004         if (plr.mustBeChained) {
5005           plr.removeBallAndChain(temp:true);
5006           plr.spawnBallAndChain();
5007         }
5008         /*
5009         writeln("HOLD: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
5010         writeln("PICK: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
5011         */
5012         if (!player.holdItem && player.pickedItem) player.scrSwitchToPocketItem(forceIfEmpty:false);
5013         if (player.holdItem) {
5014           player.holdItem.visible = true;
5015           player.holdItem.canLiveOutsideOfLevel = true;
5016           writeln("HOLD ITEM: '", GetClassName(player.holdItem.Class), "'");
5017         }
5018         plr.status == MapObject::FALLING;
5019         global.plife += 99; // just in case
5020       }
5021       return;
5022     }
5024     if (winCutScenePhase == 2) {
5025       auto ball = plr.getMyBall();
5026       if (ball && plr.holdItem != ball) {
5027         ball.teleportTo(plr.fltx, plr.flty+8);
5028         ball.yVel = 6;
5029         ball.myGrav = 0.6;
5030       }
5031       if (plr.status == MapObject::STUNNED || plr.stunned) {
5032         //alarm[0] = 70;
5033         //alarm[1] = 50;
5034         //status = GETUP;
5035         auto treasure = MakeMapObject(144+16+8, -32, 'oBigTreasure');
5036         if (treasure) treasure.depth = 1;
5037         winCutScenePhase = 3;
5038         plr.stunTimer = 30;
5039         plr.playSound('sndTFall');
5040       }
5041       return;
5042     }
5044     if (winCutScenePhase == 3) {
5045       if (plr.status != MapObject::STUNNED && !plr.stunned) {
5046         auto bt = findBigTreasure();
5047         if (bt) {
5048           if (bt.yVel == 0) {
5049             //plr.yVel = -4;
5050             //plr.status = MapObject::JUMPING;
5051             plr.kJump = true;
5052             plr.kJumpPressed = true;
5053             winCutScenePhase = 4;
5054             winCutSceneTimer = 50;
5055           }
5056         }
5057       }
5058       return;
5059     }
5061     if (winCutScenePhase == 4) {
5062       if (--winCutSceneTimer == 0) {
5063         setMenuTilesVisible(true);
5064         winCutScenePhase = 5;
5065         winSceneDrawStatus = 1;
5066         lastMusicName = '';
5067         global.setMusicPitch(1.0);
5068         global.playMusic('musVictory', loop:false);
5069         winCutSceneTimer = 50;
5070       }
5071       return;
5072     }
5074     if (winCutScenePhase == 5) {
5075       if (winSceneDrawStatus == 3) {
5076         int money = stats.money;
5077         if (winMoneyCount < money) {
5078           if (money-winMoneyCount > 1000) {
5079             winMoneyCount += 1000;
5080           } else if (money-winMoneyCount > 100) {
5081             winMoneyCount += 100;
5082           } else if (money-winMoneyCount > 10) {
5083             winMoneyCount += 10;
5084           } else {
5085             ++winMoneyCount;
5086           }
5087         }
5088         if (winMoneyCount >= money) {
5089           winMoneyCount = money;
5090           ++winSceneDrawStatus;
5091         }
5092         return;
5093       }
5095       if (winSceneDrawStatus == 7) {
5096         winFadeOut = true;
5097         winFadeLevel += 1;
5098         if (winFadeLevel >= 255) {
5099           ++winSceneDrawStatus;
5100           winCutSceneTimer = 30*30;
5101         }
5102         return;
5103       }
5105       if (winSceneDrawStatus == 8) {
5106         if (--winCutSceneTimer == 0) {
5107           setGameOver();
5108         }
5109         return;
5110       }
5112       if (--winCutSceneTimer == 0) {
5113         ++winSceneDrawStatus;
5114         winCutSceneTimer = 50;
5115       }
5116     }
5118     return;
5119   }
5123 // ////////////////////////////////////////////////////////////////////////// //
5124 void renderWinCutsceneOverlay () {
5125   if (inWinCutscene == 3) {
5126     if (winSceneDrawStatus > 0) {
5127       Video.color = 0xff_ff_ff;
5128       sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
5129       //draw_set_color(txtCol);
5130       drawTextAt(64, 32, "YOU MADE IT!");
5132       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
5133       if (global.config.gameMode == GameConfig::GameMode.Vanilla) {
5134         Video.color = 0xff_ff_00; //draw_set_color(c_yellow);
5135         drawTextAt(64, 48, "Classic Mode done!");
5136       } else {
5137         Video.color = 0x00_80_80; //draw_set_color(c_teal);
5138         if (global.config.bizarrePlus) drawTextAt(64, 48, "Bizarre Mode Plus done!");
5139         else drawTextAt(64, 48, "Bizarre Mode done!");
5140         //draw_set_color(c_white);
5141       }
5142       if (!global.usedShortcut) {
5143         Video.color = 0xc0_c0_c0; //draw_set_color(c_silver);
5144         drawTextAt(64, 56, "No shortcuts used!");
5145         //draw_set_color(c_yellow);
5146       }
5147     }
5149     if (winSceneDrawStatus > 1) {
5150       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
5151       //draw_set_color(txtCol);
5152       Video.color = 0xff_ff_ff;
5153       drawTextAt(64, 64, "FINAL SCORE:");
5154     }
5156     if (winSceneDrawStatus > 2) {
5157       sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
5158       Video.color = 0xff_ff_ff; //draw_set_color(c_white);
5159       drawTextAt(64, 72, va("$%d", winMoneyCount));
5160     }
5162     if (winSceneDrawStatus > 4) {
5163       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
5164       Video.color = 0xff_ff_ff; //draw_set_color(c_white);
5165       drawTextAt(64, 96, va("Time: %s", time2str(winTime/30)));
5166       /*
5167       draw_set_color(c_white);
5168       if (s &lt; 10) draw_text(96+24, 96, string(m) + ":0" + string(s));
5169       else draw_text(96+24, 96, string(m) + ":" + string(s));
5170       */
5171     }
5173     if (winSceneDrawStatus > 5) {
5174       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
5175       Video.color = 0xff_ff_ff; //draw_set_color(txtCol);
5176       drawTextAt(64, 96+8, "Kills: ");
5177       Video.color = 0xff_ff_ff; //draw_set_color(c_white);
5178       drawTextAt(96+24, 96+8, va("%s", stats.countKills()));
5179     }
5181     if (winSceneDrawStatus > 6) {
5182       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
5183       Video.color = 0xff_ff_ff; //draw_set_color(txtCol);
5184       drawTextAt(64, 96+16, "Saves: ");
5185       Video.color = 0xff_ff_ff; //draw_set_color(c_white);
5186       drawTextAt(96+24, 96+16, va("%s", stats.damselsSaved));
5187     }
5189     if (winFadeOut) {
5190       Video.color = (255-clamp(winFadeLevel, 0, 255))<<24;
5191       Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
5192     }
5194     if (winSceneDrawStatus == 8) {
5195       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
5196       Video.color = 0xff_ff_ff; //draw_set_color(c_white);
5197       string lastString;
5198       if (global.config.gameMode == GameConfig::GameMode.Vanilla) {
5199         Video.color = 0xff_ff_00; //draw_set_color(c_yellow);
5200         lastString = "YOU SHALL BE REMEMBERED AS A HERO.";
5201       } else {
5202         Video.color = 0x00_ff_ff;
5203         if (global.config.bizarrePlus) lastString = "ANOTHER LEGENDARY ADVENTURE!";
5204         else lastString = "YOUR DISCOVERIES WILL BE CELEBRATED!";
5205       }
5206       auto strLen = lastString.length*8;
5207       int n = 320-strLen;
5208       n = trunc(ceil(n/2.0));
5209       drawTextAt(n, 116, lastString);
5210     }
5211   }
5215 // ////////////////////////////////////////////////////////////////////////// //
5216 #include "roomTitle.vc"
5217 #include "roomTrans1.vc"
5218 #include "roomTrans2.vc"
5219 #include "roomTrans3.vc"
5220 #include "roomTrans4.vc"
5221 #include "roomOlmec.vc"
5222 #include "roomEnd.vc"
5223 #include "roomIntro.vc"
5224 #include "roomTutorial.vc"
5225 #include "roomScores.vc"
5226 #include "roomStars.vc"
5227 #include "roomSun.vc"
5228 #include "roomMoon.vc"
5231 // ////////////////////////////////////////////////////////////////////////// //
5232 #include "packages/Generator/loadRoomGens.vc"
5233 #include "packages/Generator/loadEntityGens.vc"