stats can be scrolled in "game over" screen
[k8vacspelynky.git] / GameLevel.vc
blob96a2b3f0d552fa69aa82d2ca836b05598d7ecf7f
1 /**********************************************************************************
2  * Copyright (c) 2008, 2009 Derek Yu and Mossmouth, LLC
3  * Copyright (c) 2010, Moloch
4  * Copyright (c) 2018, Ketmar Dark
5  *
6  * This file is part of Spelunky.
7  *
8  * You can redistribute and/or modify Spelunky, including its source code, under
9  * the terms of the Spelunky User License.
10  *
11  * Spelunky is distributed in the hope that it will be entertaining and useful,
12  * but WITHOUT WARRANTY.  Please see the Spelunky User License for more details.
13  *
14  * The Spelunky User License should be available in "Game .Information", which
15  * can be found in the Resource Explorer, or as an external file called COPYING.
16  * If not, please obtain a new copy of Spelunky from <http://spelunkyworld.com/>
17  *
18  **********************************************************************************/
19 // this is the level we're playing in, with all objects and tiles
20 class GameLevel : Object;
22 //#define EXPERIMENTAL_RENDER_CACHE
24 const float FrameTime = 1.0f/30.0f;
26 const int dumpGridStats = true;
28 struct IVec2D {
29   int x, y;
32 // in tiles
33 //enum NormalTilesWidth = LevelGen::LevelWidth*RoomGen::Width+2;
34 //enum NormalTilesHeight = LevelGen::LevelHeight*RoomGen::Height+2;
36 enum MaxTilesWidth = 64;
37 enum MaxTilesHeight = 64;
39 GameGlobal global;
40 transient GameStats stats;
41 transient SpriteStore sprStore;
42 transient BackTileStore bgtileStore;
43 transient BackTileImage levBGImg;
44 name levBGImgName;
45 LevelGen lg;
46 transient name lastMusicName;
47 //RoomGen[LevelGen::LevelWidth, LevelGen::LevelHeight] rooms; // moved to levelgen
49 transient float accumTime;
50 transient bool gamePaused = false;
51 transient bool gameShowHelp = false;
52 transient int gameHelpScreen = 0;
53 const int MaxGameHelpScreen = 2;
54 transient bool checkWater;
55 transient int liquidTileCount; // cached
56 /*transient*/ int damselSaved;
58 // hud efffects
59 transient int xmoney;
60 transient int collectCounter;
61 /*transient*/ int levelMoneyStart;
63 // all movable (thinkable) map objects
64 EntityGrid objGrid; // monsters, items and tiles
66 MapBackTile backtiles;
67 bool blockWaterChecking;
69 int inWinCutscene;
70 int inIntroCutscene;
71 bool cameFromIntroRoom; // for title screen
73 LevelGen::RType[MaxTilesWidth, MaxTilesHeight] roomType;
75 enum LevelKind {
76   Normal,
77   Transition,
78   Title,
79   Intro,
80   Tutorial,
81   Scores,
82   Stars,
83   Sun,
84   Moon,
85   //Final,
87 LevelKind levelKind = LevelKind.Normal;
89 array!MapTile allEnters;
90 array!MapTile allExits;
93 int startRoomX, startRoomY;
94 int endRoomX, endRoomY;
96 PlayerPawn player;
97 transient bool playerExited;
98 transient MapEntity playerExitDoor;
99 transient bool disablePlayerThink = false;
100 transient int maxPlayingTime; // in seconds
101 int levelStartTime;
102 int levelEndTime;
104 int ghostTimeLeft;
105 int musicFadeTimer;
106 bool ghostSpawned; // to speed up some checks
107 bool resetBMCOG = false;
108 int udjatAlarm;
111 // FPS, i.e. incremented by 30 in one second
112 int time; // in frames
113 int lastUsedObjectId;
114 transient int lastRenderTime = -1;
115 transient int pausedTime;
117 MapEntity deadItemsHead;
119 // screen shake variables
120 int shakeLeft;
121 IVec2D shakeOfs;
122 IVec2D shakeDir;
124 // set this before calling `fixCamera()`
125 // dimensions should be real, not scaled up/down
126 transient int viewWidth, viewHeight;
127 //transient int viewOffsetX, viewOffsetY;
129 // room bounds, not scaled
130 IVec2D viewMin, viewMax;
132 // for Olmec level cinematics
133 IVec2D cameraSlideToDest;
134 IVec2D cameraSlideToCurr;
135 IVec2D cameraSlideToSpeed; // !0: slide
136 int cameraSlideToPlayer;
137 // `fixCamera()` will set the following
138 // coordinates will be real too (with scale applied)
139 // shake is not applied
140 transient IVec2D viewStart; // with `player.viewOffset`
141 private transient IVec2D realViewStart; // without `player.viewOffset`
143 transient int framesProcessedFromLastClear;
145 transient int BuildYear;
146 transient int BuildMonth;
147 transient int BuildDay;
148 transient int BuildHour;
149 transient int BuildMin;
150 transient string BuildDateString;
153 final string getBuildDateString () {
154   if (!BuildYear) return BuildDateString;
155   if (BuildDateString) return BuildDateString;
156   BuildDateString = va("%d-%02d-%02d %02d:%02d", BuildYear, BuildMonth, BuildDay, BuildHour, BuildMin);
157   return BuildDateString;
161 final void cameraSlideToPoint (int dx, int dy, int speedx, int speedy) {
162   cameraSlideToPlayer = 0;
163   cameraSlideToDest.x = dx;
164   cameraSlideToDest.y = dy;
165   cameraSlideToSpeed.x = abs(speedx);
166   cameraSlideToSpeed.y = abs(speedy);
167   cameraSlideToCurr.x = cameraCurrX;
168   cameraSlideToCurr.y = cameraCurrY;
172 final void cameraReturnToPlayer () {
173   if (!cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y)) {
174     cameraSlideToCurr.x = cameraCurrX;
175     cameraSlideToCurr.y = cameraCurrY;
176     if (cameraSlideToSpeed.x && abs(cameraSlideToSpeed.x) < 8) cameraSlideToSpeed.x = 8;
177     if (cameraSlideToSpeed.y && abs(cameraSlideToSpeed.y) < 8) cameraSlideToSpeed.y = 8;
178     cameraSlideToPlayer = 1;
179   }
183 // if `frameSkip` is `true`, there are more frames waiting
184 // (i.e. you may skip rendering and such)
185 transient void delegate (bool frameSkip) onBeforeFrame;
186 transient void delegate (bool frameSkip) onAfterFrame;
188 transient void delegate () onCameraTeleported;
190 transient void delegate () onLevelExitedCB;
192 // this will be called in-between frames, and
193 // `frameTime` is [0..1)
194 transient void delegate (float frameTime) onInterFrame;
196 final int bizRoomStyle { get { return (lg ? lg.bizRoomStyle : 0); } }
199 final bool isNormalLevel () { return (levelKind == LevelKind.Normal); }
200 final bool isTitleRoom () { return (levelKind == LevelKind.Title); }
201 final bool isTutorialRoom () { return (levelKind == LevelKind.Tutorial); }
202 final bool isTransitionRoom () { return (levelKind == LevelKind.Transition); }
203 final bool isIntroRoom () { return (levelKind == LevelKind.Transition); }
206 bool isHUDEnabled () {
207   if (inWinCutscene) return false;
208   if (inIntroCutscene) return false;
209   if (lg.finalBossLevel) return true;
210   if (isNormalLevel()) return true;
211   return false;
215 // ////////////////////////////////////////////////////////////////////////// //
216 // stats
217 void addDeath (name aname) { if (isNormalLevel()) stats.addDeath(aname); }
219 int starsKills;
220 int sunScore;
221 int moonScore;
222 int moonTimer;
224 void addKill (name aname, optional bool telefrag) {
225        if (isNormalLevel()) stats.addKill(aname, telefrag!optional);
226   else if (aname == 'Shopkeeper' && levelKind == LevelKind.Stars) { ++stats.starsKills; ++starsKills; }
229 void addCollect (name aname, optional int amount) { if (isNormalLevel()) stats.addCollect(aname, amount!optional); }
231 void addDamselSaved () { if (isNormalLevel()) stats.addDamselSaved(); }
232 void addIdolStolen () { if (isNormalLevel()) stats.addIdolStolen(); }
233 void addIdolConverted () { if (isNormalLevel()) stats.addIdolConverted(); }
234 void addCrystalIdolStolen () { if (isNormalLevel()) stats.addCrystalIdolStolen(); }
235 void addCrystalIdolConverted () { if (isNormalLevel()) stats.addCrystalIdolConverted(); }
236 void addGhostSummoned () { if (isNormalLevel()) stats.addGhostSummoned(); }
239 // ////////////////////////////////////////////////////////////////////////// //
240 static final string time2str (int time) {
241   int secs = time%60; time /= 60;
242   int mins = time%60; time /= 60;
243   int hours = time%24; time /= 24;
244   int days = time;
245   if (days) return va("%d DAYS, %d:%02d:%02d", days, hours, mins, secs);
246   if (hours) return va("%d:%02d:%02d", hours, mins, secs);
247   return va("%02d:%02d", mins, secs);
251 // ////////////////////////////////////////////////////////////////////////// //
252 final int tilesWidth () { return lg.levelRoomWidth*RoomGen::Width+2; }
253 final int tilesHeight () { return (lg.finalBossLevel ? 55 : lg.levelRoomHeight*RoomGen::Height+2); }
256 // ////////////////////////////////////////////////////////////////////////// //
257 protected void resetGameInternal () {
258   if (player) player.removeBallAndChain();
259   resetBMCOG = false;
260   inWinCutscene = 0;
261   //inIntroCutscene = 0;
262   shakeLeft = 0;
263   udjatAlarm = 0;
264   starsKills = 0;
265   sunScore = 0;
266   moonScore = 0;
267   moonTimer = 0;
268   damselSaved = 0;
269   xmoney = 0;
270   collectCounter = 0;
271   levelMoneyStart = 0;
272   if (player) {
273     player.removeBallAndChain();
274     auto hi = player.holdItem;
275     player.holdItem = none;
276     if (hi) hi.instanceRemove();
277     hi = player.pickedItem;
278     player.pickedItem = none;
279     if (hi) hi.instanceRemove();
280   }
281   time = 0;
282   lastRenderTime = -1;
283   levelStartTime = 0;
284   levelEndTime = 0;
285   global.resetGame();
286   stats.clearGameTotals();
290 // this won't generate a level yet
291 void restartGame () {
292   resetGameInternal();
293   if (global.startMoney > 0) stats.setMoneyCheat();
294   stats.setMoney(global.startMoney);
295   levelKind = LevelKind.Normal;
299 // complement function to `restart game`
300 void generateNormalLevel () {
301   generateLevel();
302   centerViewAtPlayer();
306 void restartTitle () {
307   resetGameInternal();
308   stats.setMoney(0);
309   createSpecialLevel(LevelKind.Title, &createTitleRoom, 'musTitle');
310   global.plife = 9999;
311   global.bombs = 0;
312   global.rope = 0;
313   global.arrows = 0;
314   global.sgammo = 0;
318 void restartIntro () {
319   resetGameInternal();
320   stats.setMoney(0);
321   createSpecialLevel(LevelKind.Intro, &createIntroRoom, '');
322   global.plife = 9999;
323   global.bombs = 0;
324   global.rope = 1;
325   global.arrows = 0;
326   global.sgammo = 0;
330 void restartTutorial () {
331   resetGameInternal();
332   stats.setMoney(0);
333   createSpecialLevel(LevelKind.Tutorial, &createTutorialRoom, 'musCave');
334   global.plife = 4;
335   global.bombs = 0;
336   global.rope = 4;
337   global.arrows = 0;
338   global.sgammo = 0;
342 void restartScores () {
343   resetGameInternal();
344   stats.setMoney(0);
345   createSpecialLevel(LevelKind.Scores, &createScoresRoom, 'musTitle');
346   global.plife = 4;
347   global.bombs = 0;
348   global.rope = 0;
349   global.arrows = 0;
350   global.sgammo = 0;
354 void restartStarsRoom () {
355   resetGameInternal();
356   stats.setMoney(0);
357   createSpecialLevel(LevelKind.Stars, &createStarsRoom, '');
358   global.plife = 8;
359   global.bombs = 0;
360   global.rope = 0;
361   global.arrows = 0;
362   global.sgammo = 0;
366 void restartSunRoom () {
367   resetGameInternal();
368   stats.setMoney(0);
369   createSpecialLevel(LevelKind.Sun, &createSunRoom, '');
370   global.plife = 8;
371   global.bombs = 0;
372   global.rope = 0;
373   global.arrows = 0;
374   global.sgammo = 0;
378 void restartMoonRoom () {
379   resetGameInternal();
380   stats.setMoney(0);
381   createSpecialLevel(LevelKind.Moon, &createMoonRoom, '');
382   global.plife = 8;
383   global.bombs = 0;
384   global.rope = 0;
385   global.arrows = 100;
386   global.sgammo = 0;
390 // ////////////////////////////////////////////////////////////////////////// //
391 // generate angry shopkeeper at exit if murderer or thief
392 void generateAngryShopkeepers () {
393   if (global.murderer || global.thiefLevel > 0) {
394     foreach (MapTile e; allExits) {
395       auto obj = MonsterShopkeeper(MakeMapObject(e.ix, e.iy, 'oShopkeeper'));
396       if (obj) {
397         obj.style = 'Bounty Hunter';
398         obj.status = MapObject::PATROL;
399       }
400     }
401   }
405 // ////////////////////////////////////////////////////////////////////////// //
406 final void resetRoomBounds () {
407   viewMin.x = 0;
408   viewMin.y = 0;
409   viewMax.x = tilesWidth*16;
410   viewMax.y = tilesHeight*16;
411   // Great Lake is bottomless (nope)
412   //if (global.lake == 1) viewMax.y -= 16;
413   //writeln("ROOM BOUNDS: (", viewMax.x, "x", viewMax.y, ")");
417 final void setRoomBounds (int x0, int y0, int x1, int y1) {
418   viewMin.x = x0;
419   viewMin.y = y0;
420   viewMax.x = x1+16;
421   viewMax.y = y1+16;
425 // ////////////////////////////////////////////////////////////////////////// //
426 struct OSDMessage {
427   string msg;
428   float timeout; // seconds
429   float starttime; // for active
430   bool active; // true: timeout is `GetTickCount()` dismissing time
433 array!OSDMessage msglist; // [0]: current one
435 struct OSDMessageTalk {
436   string msg;
437   float timeout; // seconds;
438   float starttime; // for active
439   bool active; // true: timeout is `GetTickCount()` dismissing time
440   bool shopOnly; // true: timeout when player exited the shop
441   int hiColor1; // -1: default
442   int hiColor2; // -1: default
445 array!OSDMessageTalk msgtalklist; // [0]: current one
448 private final void osdCheckTimeouts () {
449   auto stt = GetTickCount();
450   while (msglist.length) {
451     if (!msglist[0].msg) { msglist.remove(0); continue; }
452     if (!msglist[0].active) {
453       msglist[0].active = true;
454       msglist[0].starttime = stt;
455     }
456     if (msglist[0].starttime+msglist[0].timeout >= stt) break;
457     msglist.remove(0);
458   }
459   if (msgtalklist.length) {
460     bool inshop = isInShop(player.ix/16, player.iy/16);
461     while (msgtalklist.length) {
462       if (!msgtalklist[0].msg) { msgtalklist.remove(0); continue; }
463       if (msgtalklist[0].shopOnly) {
464         if (inshop == msgtalklist[0].active) {
465           msgtalklist[0].active = !inshop;
466           if (!inshop) msgtalklist[0].starttime = stt;
467         }
468       } else {
469         if (!msgtalklist[0].active) {
470           msgtalklist[0].active = true;
471           msgtalklist[0].starttime = stt;
472         }
473       }
474       if (!msgtalklist[0].active) break;
475       //writeln("timedelta: ", msgtalklist[0].starttime+msgtalklist[0].timeout-stt);
476       if (msgtalklist[0].starttime+msgtalklist[0].timeout >= stt) break;
477       msgtalklist.remove(0);
478     }
479   }
483 final bool osdHasMessage () {
484   osdCheckTimeouts();
485   return (msglist.length > 0);
489 final string osdGetMessage (out float timeLeft, out float timeStart) {
490   osdCheckTimeouts();
491   if (msglist.length == 0) { timeLeft = 0; return ""; }
492   auto stt = GetTickCount();
493   timeStart = msglist[0].starttime;
494   timeLeft = fmax(0, msglist[0].starttime+msglist[0].timeout-stt);
495   return msglist[0].msg;
499 final string osdGetTalkMessage (optional out int hiColor1, optional out int hiColor2) {
500   osdCheckTimeouts();
501   if (msgtalklist.length == 0) return "";
502   hiColor1 = msgtalklist[0].hiColor1;
503   hiColor2 = msgtalklist[0].hiColor2;
504   return msgtalklist[0].msg;
508 final void osdClear (optional bool clearTalk) {
509   msglist.clear();
510   if (clearTalk) msgtalklist.clear();
514 final void osdMessage (string msg, optional float timeout, optional bool urgent) {
515   if (!msg) return;
516   msg = global.expandString(msg);
517   if (!specified_timeout) timeout = 3.33;
518   // special message for shops
519   if (timeout == -666) {
520     if (!msg) return;
521     if (msglist.length && msglist[0].msg == msg) return;
522     if (msglist.length == 0 || msglist[0].msg != msg) {
523       osdClear(clearTalk:false);
524       msglist.length += 1;
525       msglist[0].msg = msg;
526     }
527     msglist[0].active = false;
528     msglist[0].timeout = 3.33;
529     osdCheckTimeouts();
530     return;
531   }
532   if (timeout < 0.1) return;
533   timeout = fmax(1.0, timeout);
534   //writeln("OSD: ", msg);
535   // find existing one, and bring it to the top
536   int oldidx = 0;
537   for (; oldidx < msglist.length; ++oldidx) {
538     if (msglist[oldidx].msg == msg) break; // i found her!
539   }
540   // duplicate?
541   if (oldidx < msglist.length) {
542     // yeah, move duplicate to the top
543     msglist[oldidx].timeout = fmax(timeout, msglist[oldidx].timeout);
544     msglist[oldidx].active = false;
545     if (urgent && oldidx != 0) {
546       timeout = msglist[oldidx].timeout;
547       msglist.remove(oldidx);
548       msglist.insert(0);
549       msglist[0].msg = msg;
550       msglist[0].timeout = timeout;
551       msglist[0].active = false;
552     }
553   } else if (urgent) {
554     msglist.insert(0);
555     msglist[0].msg = msg;
556     msglist[0].timeout = timeout;
557     msglist[0].active = false;
558   } else {
559     // new one
560     msglist.length += 1;
561     msglist[$-1].msg = msg;
562     msglist[$-1].timeout = timeout;
563     msglist[$-1].active = false;
564   }
565   osdCheckTimeouts();
569 void osdMessageTalk (string msg, optional bool replace, optional float timeout, optional bool inShopOnly,
570                      optional int hiColor1, optional int hiColor2)
572   //if (!msg) return;
573   //writeln("talk msg: replace=", replace, "; timeout=", timeout, "; inshop=", inShopOnly, "; msg=", msg);
574   if (!specified_timeout) timeout = 3.33;
575   if (!specified_inShopOnly) inShopOnly = true;
576   if (!specified_hiColor1) hiColor1 = -1;
577   if (!specified_hiColor2) hiColor2 = -1;
578   msg = global.expandString(msg);
579   if (replace) {
580     if (!msg) { msgtalklist.clear(); return; }
581     if (msgtalklist.length && msgtalklist[0].msg == msg) {
582       while (msgtalklist.length > 1) msgtalklist.remove(1);
583       msgtalklist[$-1].timeout = timeout;
584       msgtalklist[$-1].shopOnly = inShopOnly;
585     } else {
586       if (msgtalklist.length) msgtalklist.clear();
587       msgtalklist.length += 1;
588       msgtalklist[$-1].msg = msg;
589       msgtalklist[$-1].timeout = timeout;
590       msgtalklist[$-1].active = false;
591       msgtalklist[$-1].shopOnly = inShopOnly;
592       msgtalklist[$-1].hiColor1 = hiColor1;
593       msgtalklist[$-1].hiColor2 = hiColor2;
594     }
595   } else {
596     if (!msg) return;
597     bool found = false;
598     foreach (auto midx, ref auto mnfo; msgtalklist) {
599       if (mnfo.msg == msg) {
600         mnfo.timeout = timeout;
601         mnfo.shopOnly = inShopOnly;
602         found = true;
603       }
604     }
605     if (!found) {
606       msgtalklist.length += 1;
607       msgtalklist[$-1].msg = msg;
608       msgtalklist[$-1].timeout = timeout;
609       msgtalklist[$-1].active = false;
610       msgtalklist[$-1].shopOnly = inShopOnly;
611       msgtalklist[$-1].hiColor1 = hiColor1;
612       msgtalklist[$-1].hiColor2 = hiColor2;
613     }
614   }
615   osdCheckTimeouts();
619 // ////////////////////////////////////////////////////////////////////////// //
620 void setup (GameGlobal aGlobal, SpriteStore aSprStore, BackTileStore aBGTileStore) {
621   global = aGlobal;
622   sprStore = aSprStore;
623   bgtileStore = aBGTileStore;
625   lg = SpawnObject(LevelGen);
626   lg.global = global;
627   lg.level = self;
629   objGrid = SpawnObject(EntityGrid);
630   objGrid.setup(0, 0, MaxTilesWidth*16, MaxTilesHeight*16);
634 // stores should be set
635 void onLoaded () {
636   checkWater = true;
637   liquidTileCount = 0;
638   levBGImg = bgtileStore[levBGImgName];
639   foreach (MapEntity o; objGrid.allObjects()) {
640     o.onLoaded();
641     auto t = MapTile(o);
642     if (t && (t.lava || t.water)) ++liquidTileCount;
643   }
644   for (MapBackTile bt = backtiles; bt; bt = bt.next) bt.onLoaded();
645   if (player) player.onLoaded();
646   //FIXME
647   if (msglist.length) {
648     msglist[0].active = false;
649     msglist[0].timeout = 0.200;
650     osdCheckTimeouts();
651   }
652   lastMusicName = (lg ? lg.musicName : '');
653   global.setMusicPitch(1.0);
654   if (lg && lg.musicName) global.playMusic(lg.musicName); else global.stopMusic();
658 // ////////////////////////////////////////////////////////////////////////// //
659 void pickedSpectacles () {
660   foreach (MapTile t; objGrid.allObjectsSafe(MapTile)) t.onGotSpectacles();
664 // ////////////////////////////////////////////////////////////////////////// //
665 #include "rgentile.vc"
666 #include "rgenobj.vc"
669 void onLevelExited () {
670   if (playerExitDoor isa TitleTileXTitle) {
671     playerExitDoor = none;
672     restartTitle();
673     return;
674   }
675   // title
676   if (isTitleRoom() || levelKind == LevelKind.Scores) {
677     if (playerExitDoor) processTitleExit(playerExitDoor);
678     playerExitDoor = none;
679     return;
680   }
681   if (isTutorialRoom()) {
682     playerExitDoor = none;
683     restartGame();
684     global.currLevel = 1;
685     generateNormalLevel();
686     return;
687   }
688   // challenges
689   if (levelKind == LevelKind.Stars || levelKind == LevelKind.Sun || levelKind == LevelKind.Moon) {
690     playerExitDoor = none;
691     levelEndTime = time;
692     if (onLevelExitedCB) onLevelExitedCB();
693     restartTitle();
694     return;
695   }
696   // normal level
697   if (isNormalLevel()) {
698     stats.maxLevelComplete = max(stats.maxLevelComplete, global.currLevel);
699     levelEndTime = time;
700     if (playerExitDoor) {
701       if (playerExitDoor.objType == 'oXGold') {
702         writeln("exiting to City Of Gold");
703         global.cityOfGold = -1;
704         //!global.currLevel += 1;
705       } else if (playerExitDoor.objType == 'oXMarket') {
706         writeln("exiting to Black Market");
707         global.genBlackMarket = true;
708         //!global.currLevel += 1;
709       } else {
710         writeln("exit door(", GetClassName(playerExitDoor.Class), "): '", playerExitDoor.objType, "'");
711       }
712     } else {
713       writeln("WTF?! NO EXIT DOOR!");
714     }
715   }
716   if (onLevelExitedCB) onLevelExitedCB();
717   //
718   playerExitDoor = none;
719   if (levelKind == LevelKind.Transition) {
720     if (global.thiefLevel > 0) global.thiefLevel -= 1;
721     if (global.alienCraft) ++global.alienCraft;
722     if (global.yetiLair) ++global.yetiLair;
723     if (global.lake) ++global.lake;
724     if (global.cityOfGold) { if (++global.cityOfGold == 0) global.cityOfGold = 1; }
725     //orig dbg:if (global.currLevel == 1) global.currLevel += firstLevelSkip; else global.currLevel += levelSkip;
726     /+
727     if (!global.blackMarket && !global.cityOfGold /*&& !global.yetiLair*/) {
728       global.currLevel += 1;
729     }
730     +/
731     ++global.currLevel;
732     generateLevel();
733   } else {
734     // < 20 seconds per level: looks like a speedrun
735     global.noDarkLevel = (levelEndTime > levelStartTime && levelEndTime-levelStartTime < 20*30);
736     if (lg.finalBossLevel) {
737       winTime = time;
738       ++stats.gamesWon;
739       // add money for big idol
740       player.addScore(50000);
741       stats.gameOver();
742       startWinCutscene();
743     } else {
744       generateTransitionLevel();
745     }
746   }
747   //centerViewAtPlayer();
751 void onOlmecDead (MapObject o) {
752   writeln("*** OLMEC IS DEAD!");
753   foreach (MapTile t; allExits) {
754     if (t.exit) {
755       t.openExit();
756       auto st = checkTileAtPoint(t.ix+8, t.iy+16);
757       if (!st) {
758         st = MakeMapTile(t.ix/16, t.iy/16+1, 'oTemple');
759         st.ore = 0;
760       }
761       st.invincible = true;
762     }
763   }
767 void generateLevelMessages () {
768   writeln("LEVEL NUMBER: ", global.currLevel);
769   if (global.darkLevel) {
770     if (global.hasCrown) {
771        osdMessage("THE HEDJET SHINES BRIGHTLY.");
772        global.darkLevel = false;
773     } else if (global.config.scumDarkness < 2) {
774       osdMessage("I CAN'T SEE A THING!\nI'D BETTER USE THESE FLARES!");
775     }
776   }
778   if (global.blackMarket) osdMessage("WELCOME TO THE BLACK MARKET!");
780   if (global.cemetary) osdMessage("THE DEAD ARE RESTLESS!");
781   if (global.lake == 1) osdMessage("I CAN HEAR RUSHING WATER...");
783   if (global.snakePit) osdMessage("I HEAR SNAKES... I HATE SNAKES!");
784   if (global.yetiLair == 1) osdMessage("IT SMELLS LIKE WET FUR IN HERE!");
785   if (global.alienCraft == 1) osdMessage("THERE'S A PSYCHIC PRESENCE HERE!");
786   if (global.cityOfGold == 1) {
787     if (true /*hasOlmec()*/) osdMessage("IT'S THE LEGENDARY CITY OF GOLD!");
788   }
790   if (global.sacrificePit) osdMessage("I CAN HEAR PRAYERS TO KALI!");
794 final MapObject debugSpawnObjectWithClass (class!MapObject oclass, optional bool playerDir) {
795   if (!oclass) return none;
796   int dx = 0, dy = 0;
797   bool canLeft = !isSolidAtPoint(player.ix-8, player.iy);
798   bool canRight = !isSolidAtPoint(player.ix+16, player.iy);
799   if (!canLeft && !canRight) return none;
800   if (canLeft && canRight) {
801     if (playerDir) {
802       dx = (player.dir == MapObject::Dir.Left ? -16 : 16);
803     } else {
804       dx = 16;
805     }
806   } else {
807     dx = (canLeft ? -16 : 16);
808   }
809   auto obj = SpawnMapObjectWithClass(oclass);
810   if (obj isa MapEnemy) {
811     dx -= 8;
812     dy -= (obj isa MonsterDamsel ? 2 : 8);
813   }
814   if (obj) PutSpawnedMapObject(player.ix+dx, player.iy+dy, obj);
815   return obj;
819 final MapObject debugSpawnObject (name aname) {
820   if (!aname) return none;
821   return debugSpawnObjectWithClass(findGameObjectClassByName(aname));
825 void createSpecialLevel (LevelKind kind, scope void delegate () creator, name amusic) {
826   global.darkLevel = false;
827   udjatAlarm = 0;
828   xmoney = 0;
829   collectCounter = 0;
830   global.resetStartingItems();
832   global.setMusicPitch(1.0);
833   levelKind = kind;
835   auto olddel = ImmediateDelete;
836   ImmediateDelete = false;
837   clearWholeLevel();
839   creator();
841   setMenuTilesOnTop();
843   fixWallTiles();
844   addBackgroundGfxDetails();
845   //levBGImgName = 'bgCave';
846   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
848   blockWaterChecking = true;
849   fixLiquidTop();
850   cleanDeadTiles();
852   ImmediateDelete = olddel;
853   CollectGarbage(true); // destroy delayed objects too
855   if (dumpGridStats) objGrid.dumpStats();
857   playerExited = false; // just in case
858   playerExitDoor = none;
860   osdClear(clearTalk:true);
862   setupGhostTime();
863   lg.musicName = amusic;
864   lastMusicName = amusic;
865   global.setMusicPitch(1.0);
866   if (amusic) global.playMusic(lg.musicName); else global.stopMusic();
870 void createTitleLevel () {
871   createSpecialLevel(LevelKind.Title, &createTitleRoom, 'musTitle');
875 void createTutorialLevel () {
876   createSpecialLevel(LevelKind.Tutorial, &createTutorialRoom, 'musCave');
877   global.plife = 4;
878   global.bombs = 0;
879   global.rope = 4;
880   global.arrows = 0;
881   global.sgammo = 0;
885 // `global.currLevel` is the new level
886 void generateTransitionLevel () {
887   global.darkLevel = false;
888   udjatAlarm = 0;
889   xmoney = 0;
890   collectCounter = 0;
892   resetTransitionOverlay();
894   global.setMusicPitch(1.0);
895   switch (global.config.transitionMusicMode) {
896     case GameConfig::MusicMode.Silent: global.stopMusic(); break;
897     case GameConfig::MusicMode.Restart: global.restartMusic(); break;
898     case GameConfig::MusicMode.DontTouch: break;
899   }
901   levelKind = LevelKind.Transition;
903   auto olddel = ImmediateDelete;
904   ImmediateDelete = false;
905   clearWholeLevel();
907        if (global.currLevel < 4) createTrans1Room();
908   else if (global.currLevel == 4) createTrans1xRoom();
909   else if (global.currLevel < 8) createTrans2Room();
910   else if (global.currLevel == 8) createTrans2xRoom();
911   else if (global.currLevel < 12) createTrans3Room();
912   else if (global.currLevel == 12) createTrans3xRoom();
913   else if (global.currLevel < 16) createTrans4Room();
914   else if (global.currLevel == 16) createTrans4Room();
915   else createTrans1Room(); //???
917   setMenuTilesOnTop();
919   fixWallTiles();
920   addBackgroundGfxDetails();
921   //levBGImgName = 'bgCave';
922   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
924   blockWaterChecking = true;
925   fixLiquidTop();
926   cleanDeadTiles();
928   if (damselSaved > 0) {
929     // this is special "damsel ready to kiss you" object, not a heart
930     MakeMapObject(176+8, 176+8, 'oDamselKiss');
931     global.plife += damselSaved; // if player skipped transition cutscene
932     damselSaved = 0;
933   }
935   ImmediateDelete = olddel;
936   CollectGarbage(true); // destroy delayed objects too
938   if (dumpGridStats) objGrid.dumpStats();
940   playerExited = false; // just in case
941   playerExitDoor = none;
943   osdClear(clearTalk:true);
945   setupGhostTime();
946   //global.playMusic(lg.musicName);
950 void generateLevel () {
951   levelStartTime = time;
952   levelEndTime = time;
954   udjatAlarm = 0;
955   if (resetBMCOG) {
956     resetBMCOG = false;
957     global.genBlackMarket = false;
958   }
960   global.setMusicPitch(1.0);
961   stats.clearLevelTotals();
963   levelKind = LevelKind.Normal;
964   lg.generate();
965   //lg.dump();
967   resetRoomBounds();
969   lg.generateRooms();
970   //writeln("tw:", tilesWidth, "; th:", tilesHeight);
972   auto olddel = ImmediateDelete;
973   ImmediateDelete = false;
974   clearWholeLevel();
976   if (lg.finalBossLevel) {
977     blockWaterChecking = true;
978     createOlmecRoom();
979   }
981   // if transition cutscene was skipped...
982   global.plife += max(0, damselSaved); // if player skipped transition cutscene
983   damselSaved = 0;
985   // generate tiles
986   startRoomX = lg.startRoomX;
987   startRoomY = lg.startRoomY;
988   endRoomX = lg.endRoomX;
989   endRoomY = lg.endRoomY;
990   addBackgroundGfxDetails();
991   foreach (int y; 0..tilesHeight) {
992     foreach (int x; 0..tilesWidth) {
993       lg.genTileAt(x, y);
994     }
995   }
996   fixWallTiles();
998   levBGImgName = lg.bgImgName;
999   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
1001   if (global.allowAngryShopkeepers) generateAngryShopkeepers();
1003   lg.generateEntities();
1005   // add box of flares to dark level
1006   if (global.darkLevel && allEnters.length) {
1007     auto enter = allEnters[0];
1008     int x = enter.ix, y = enter.iy;
1009          if (!isSolidAtPoint(x-16, y)) MakeMapObject(x-16+8, y+8, 'oFlareCrate');
1010     else if (!isSolidAtPoint(x+16, y)) MakeMapObject(x+16+8, y+8, 'oFlareCrate');
1011     else MakeMapObject(x+8, y+8, 'oFlareCrate');
1012   }
1014   //scrGenerateEntities();
1015   //foreach (; 0..2) scrGenerateEntities();
1017   writeln(objGrid.countObjects, " alive objects inserted");
1018   writeln(countBackTiles, " background tiles inserted");
1020   if (!player) FatalError("player pawn is not spawned");
1022   if (lg.finalBossLevel) {
1023     blockWaterChecking = true;
1024   } else {
1025     blockWaterChecking = false;
1026   }
1027   fixLiquidTop();
1028   cleanDeadTiles();
1030   ImmediateDelete = olddel;
1031   CollectGarbage(true); // destroy delayed objects too
1033   if (dumpGridStats) objGrid.dumpStats();
1035   playerExited = false; // just in case
1036   playerExitDoor = none;
1038   levelMoneyStart = stats.money;
1040   osdClear(clearTalk:true);
1041   generateLevelMessages();
1043   xmoney = 0;
1044   collectCounter = 0;
1046   //writeln("!<", lastMusicName, ">:<", lg.musicName, ">");
1047   global.setMusicPitch(1.0);
1048   if (lastMusicName != lg.musicName) {
1049     global.playMusic(lg.musicName);
1050   } else {
1051     //writeln("MM: ", global.config.nextLevelMusicMode);
1052     switch (global.config.nextLevelMusicMode) {
1053       case GameConfig::MusicMode.Silent: global.stopMusic(); break; // the thing that should not be
1054       case GameConfig::MusicMode.Restart: global.restartMusic(); break;
1055       case GameConfig::MusicMode.DontTouch:
1056         if (global.config.transitionMusicMode == GameConfig::MusicMode.Silent) {
1057           global.playMusic(lg.musicName);
1058         }
1059         break;
1060     }
1061   }
1062   lastMusicName = lg.musicName;
1063   //global.playMusic(lg.musicName);
1065   setupGhostTime();
1066   if (global.cityOfGold == 1 || global.genBlackMarket) resetBMCOG = true;
1068   if (global.cityOfGold == 1) {
1069     lg.mapSprite = 'sMapTemple';
1070     lg.mapTitle = "City of Gold";
1071   } else if (global.blackMarket) {
1072     lg.mapSprite = 'sMapJungle';
1073     lg.mapTitle = "Black Market";
1074   }
1078 // ////////////////////////////////////////////////////////////////////////// //
1079 int currKeys, nextKeys;
1080 int pressedKeysQ, releasedKeysQ;
1081 int keysPressed, keysReleased = -1;
1084 struct SavedKeyState {
1085   int currKeys, nextKeys;
1086   int pressedKeysQ, releasedKeysQ;
1087   int keysPressed, keysReleased;
1088   // for session
1089   int roomSeed, otherSeed;
1093 // for saving/replaying
1094 final void keysSaveState (out SavedKeyState ks) {
1095   ks.currKeys = currKeys;
1096   ks.nextKeys = nextKeys;
1097   ks.pressedKeysQ = pressedKeysQ;
1098   ks.releasedKeysQ = releasedKeysQ;
1099   ks.keysPressed = keysPressed;
1100   ks.keysReleased = keysReleased;
1103 // for saving/replaying
1104 final void keysRestoreState (const ref SavedKeyState ks) {
1105   currKeys = ks.currKeys;
1106   nextKeys = ks.nextKeys;
1107   pressedKeysQ = ks.pressedKeysQ;
1108   releasedKeysQ = ks.releasedKeysQ;
1109   keysPressed = ks.keysPressed;
1110   keysReleased = ks.keysReleased;
1114 final void keysNextFrame () {
1115   currKeys = nextKeys;
1119 final void clearKeys () {
1120   currKeys = 0;
1121   nextKeys = 0;
1122   pressedKeysQ = 0;
1123   releasedKeysQ = 0;
1124   keysPressed = 0;
1125   keysReleased = -1;
1129 final void onKey (int code, bool down) {
1130   if (!code) return;
1131   if (down) {
1132     currKeys |= code;
1133     nextKeys |= code;
1134     if (keysReleased&code) {
1135       keysPressed |= code;
1136       keysReleased &= ~code;
1137       pressedKeysQ |= code;
1138     }
1139   } else {
1140     nextKeys &= ~code;
1141     if (keysPressed&code) {
1142       keysReleased |= code;
1143       keysPressed &= ~code;
1144       releasedKeysQ |= code;
1145     }
1146   }
1149 final bool isKeyDown (int code) {
1150   return !!(currKeys&code);
1153 final bool isKeyPressed (int code) {
1154   bool res = !!(pressedKeysQ&code);
1155   pressedKeysQ &= ~code;
1156   return res;
1159 final bool isKeyReleased (int code) {
1160   bool res = !!(releasedKeysQ&code);
1161   releasedKeysQ &= ~code;
1162   return res;
1166 final void clearKeysPressRelease () {
1167   keysPressed = default.keysPressed;
1168   keysReleased = default.keysReleased;
1169   pressedKeysQ = default.pressedKeysQ;
1170   releasedKeysQ = default.releasedKeysQ;
1171   currKeys = 0;
1172   nextKeys = 0;
1176 // ////////////////////////////////////////////////////////////////////////// //
1177 final void registerEnter (MapTile t) {
1178   if (!t) return;
1179   allEnters[$] = t;
1180   return;
1184 final void registerExit (MapTile t) {
1185   if (!t) return;
1186   allExits[$] = t;
1187   return;
1191 final bool isYAtEntranceRow (int py) {
1192   py /= 16;
1193   foreach (MapTile t; allEnters) if (t.iy == py) return true;
1194   return false;
1198 final int calcNearestEnterDist (int px, int py) {
1199   if (allEnters.length == 0) return int.max;
1200   int curdistsq = int.max;
1201   foreach (MapTile t; allEnters) {
1202     int xc = px-t.xCenter, yc = py-t.yCenter;
1203     int distsq = xc*xc+yc*yc;
1204     if (distsq < curdistsq) curdistsq = distsq;
1205   }
1206   return round(sqrt(curdistsq));
1210 final int calcNearestExitDist (int px, int py) {
1211   if (allExits.length == 0) return int.max;
1212   int curdistsq = int.max;
1213   foreach (MapTile t; allExits) {
1214     int xc = px-t.xCenter, yc = py-t.yCenter;
1215     int distsq = xc*xc+yc*yc;
1216     if (distsq < curdistsq) curdistsq = distsq;
1217   }
1218   return round(sqrt(curdistsq));
1222 // ////////////////////////////////////////////////////////////////////////// //
1223 final void clearForTransition () {
1224   auto olddel = ImmediateDelete;
1225   ImmediateDelete = false;
1226   clearWholeLevel();
1227   ImmediateDelete = olddel;
1228   CollectGarbage(true); // destroy delayed objects too
1229   global.darkLevel = false;
1233 // ////////////////////////////////////////////////////////////////////////// //
1234 final int countBackTiles () {
1235   int res = 0;
1236   for (MapBackTile bt = backtiles; bt; bt = bt.next) ++res;
1237   return res;
1241 final void clearWholeLevel () {
1242   allEnters.clear();
1243   allExits.clear();
1245   // don't kill objects the player is holding
1246   if (player) {
1247     if (player.pickedItem isa ItemBall) {
1248       player.pickedItem.instanceRemove();
1249       player.pickedItem = none;
1250     }
1251     if (player.pickedItem && player.pickedItem.grid) {
1252       player.pickedItem.grid.remove(player.pickedItem.gridId);
1253       writeln("secured pocket item '", GetClassName(player.pickedItem.Class), "'");
1254     }
1255     if (player.holdItem isa ItemBall) {
1256       player.removeBallAndChain(temp:true);
1257       if (player.holdItem) player.holdItem.instanceRemove();
1258       player.holdItem = none;
1259     }
1260     if (player.holdItem && player.holdItem.grid) {
1261       player.holdItem.grid.remove(player.holdItem.gridId);
1262       writeln("secured held item '", GetClassName(player.holdItem.Class), "'");
1263     }
1264     writeln("secured ball; mustBeChained=", player.mustBeChained, "; wasHoldingBall=", player.wasHoldingBall);
1265   }
1267   int count = objGrid.countObjects();
1268   if (dumpGridStats) { if (objGrid.getFirstObjectCID()) objGrid.dumpStats(); }
1269   objGrid.removeAllObjects(true); // and destroy
1270   if (count > 0) writeln(count, " objects destroyed");
1272   lastUsedObjectId = 0;
1273   accumTime = 0;
1274   //!time = 0;
1275   lastRenderTime = -1;
1276   liquidTileCount = 0;
1277   checkWater = false;
1279   while (backtiles) {
1280     MapBackTile t = backtiles;
1281     backtiles = t.next;
1282     delete t;
1283   }
1285   levBGImg = none;
1286   framesProcessedFromLastClear = 0;
1290 final void insertObject (MapEntity o) {
1291   if (!o) return;
1292   if (o.grid) FatalError("cannot put object into level twice");
1293   objGrid.insert(o);
1297 final void spawnPlayerAt (int x, int y) {
1298   // if we have no player, spawn new one
1299   // otherwise this just a level transition, so simply reposition him
1300   if (!player) {
1301     // don't add player to object list, as it has very separate processing anyway
1302     player = SpawnObject(PlayerPawn);
1303     player.global = global;
1304     player.level = self;
1305     if (!player.initialize()) {
1306       delete player;
1307       FatalError("something is wrong with player initialization");
1308       return;
1309     }
1310   }
1311   player.fltx = x;
1312   player.flty = y;
1313   player.saveInterpData();
1314   player.resurrect();
1315   if (player.mustBeChained || global.config.scumBallAndChain) {
1316     writeln("*** spawning ball and chain");
1317     player.spawnBallAndChain(levelStart:true);
1318   }
1319   playerExited = false;
1320   playerExitDoor = none;
1321   if (global.config.startWithKapala) global.hasKapala = true;
1322   centerViewAtPlayer();
1323   // reinsert player items into grid
1324   if (player.pickedItem) objGrid.insert(player.pickedItem);
1325   if (player.holdItem) objGrid.insert(player.holdItem);
1326   //writeln("player spawned; active=", player.active);
1327   player.scrSwitchToPocketItem(forceIfEmpty:false);
1331 final void teleportPlayerTo (int x, int y) {
1332   if (player) {
1333     player.fltx = x;
1334     player.flty = y;
1335     player.saveInterpData();
1336   }
1340 final void resurrectPlayer () {
1341   if (player) player.resurrect();
1342   playerExited = false;
1343   playerExitDoor = none;
1347 // ////////////////////////////////////////////////////////////////////////// //
1348 final void scrShake (int duration) {
1349   if (shakeLeft == 0) {
1350     shakeOfs.x = 0;
1351     shakeOfs.y = 0;
1352     shakeDir.x = 0;
1353     shakeDir.y = 0;
1354   }
1355   shakeLeft = max(shakeLeft, duration);
1360 // ////////////////////////////////////////////////////////////////////////// //
1361 enum SCAnger {
1362   TileDestroyed,
1363   ItemStolen, // including damsel, lol
1364   CrapsCheated,
1365   BombDropped,
1366   DamselWhipped,
1369 // checks for dead, agnered, distance, etc. should be already done
1370 protected void doAngerShopkeeper (MonsterShopkeeper shp, SCAnger reason, ref bool messaged,
1371                                   int maxdist, MapEntity offender)
1373   if (!shp || shp.dead || shp.angered) return;
1374   if (offender.distanceToEntityCenter(shp) > maxdist) return;
1376   shp.status = MapObject::ATTACK;
1377   string msg;
1378   if (global.murderer) {
1379     msg = "~YOU'LL PAY FOR YOUR CRIMES!~";
1380   } else {
1381     switch (reason) {
1382       case SCAnger.TileDestroyed: msg = "~DIE, YOU VANDAL!~"; break;
1383       case SCAnger.ItemStolen: msg = "~COME BACK HERE, THIEF!~"; break;
1384       case SCAnger.CrapsCheated: msg = "~DIE, CHEATER!~"; break;
1385       case SCAnger.BombDropped: msg = "~TERRORIST!~"; break;
1386       case SCAnger.DamselWhipped: msg = "~HEY, ONLY I CAN DO THAT!~"; break;
1387       default: "~NOW I'M REALLY STEAMED!~"; break;
1388     }
1389   }
1391   writeln("shopkeeper angered; reason=", reason, "; maxdist=", maxdist, "; msg=\"", msg, "\"");
1392   if (!messaged) {
1393     messaged = true;
1394     if (msg) osdMessageTalk(msg, replace:true, inShopOnly:false, hiColor1:0xff_00_00);
1395     global.thiefLevel += (global.thiefLevel > 0 ? 3 : 2);
1396   }
1400 // make the nearest shopkeeper angry. RAWR!
1401 void scrShopkeeperAnger (SCAnger reason, optional int maxdist, optional MapEntity offender) {
1402   bool messaged = false;
1403   maxdist = clamp(maxdist, 96, 100000);
1404   if (!offender) offender = player;
1405   if (maxdist == 100000) {
1406     foreach (MonsterShopkeeper shp; objGrid.allObjects(MonsterShopkeeper)) {
1407       doAngerShopkeeper(shp, reason, messaged, maxdist, offender);
1408     }
1409   } else {
1410     foreach (MonsterShopkeeper shp; objGrid.inRectPix(offender.xCenter-maxdist-128, offender.yCenter-maxdist-128, (maxdist+128)*2, (maxdist+128)*2, precise:false, castClass:MonsterShopkeeper)) {
1411       doAngerShopkeeper(shp, reason, messaged, maxdist, offender);
1412     }
1413   }
1417 final MapObject findCrapsPrize () {
1418   foreach (MapObject o; objGrid.allObjects(MapObject)) {
1419     if (!o.spectral && o.inDiceHouse) return o;
1420   }
1421   return none;
1425 // ////////////////////////////////////////////////////////////////////////// //
1426 // moved from oPlayer1.Step.Action so it could be shared with oAltarLeft so that traps will be triggered when the altar is destroyed without picking up the idol.
1427 // note: idols moved by monkeys will have false `stolenIdol`
1428 void scrTriggerIdolAltar (bool stolenIdol) {
1429   ObjTikiCurse res = none;
1430   int curdistsq = int.max;
1431   int px = player.xCenter, py = player.yCenter;
1432   foreach (MapObject o; objGrid.allObjects(MapObject)) {
1433     auto tcr = ObjTikiCurse(o);
1434     if (!tcr) continue;
1435     if (tcr.activated) continue;
1436     int xc = px-tcr.xCenter, yc = py-tcr.yCenter;
1437     int distsq = xc*xc+yc*yc;
1438     if (distsq < curdistsq) {
1439       res = tcr;
1440       curdistsq = distsq;
1441     }
1442   }
1443   if (res) res.activate(stolenIdol);
1447 // ////////////////////////////////////////////////////////////////////////// //
1448 void setupGhostTime () {
1449   musicFadeTimer = -1;
1450   ghostSpawned = false;
1452   // there is no ghost on the first level
1453   if (inWinCutscene || inIntroCutscene || !isNormalLevel() || lg.finalBossLevel ||
1454       (!global.config.ghostAtFirstLevel && global.currLevel == 1))
1455   {
1456     ghostTimeLeft = -1;
1457     global.setMusicPitch(1.0);
1458     return;
1459   }
1461   if (global.config.scumGhost < 0) {
1462     // instant
1463     ghostTimeLeft = 1;
1464     osdMessage("A GHASTLY VOICE CALLS YOUR NAME!\nLET'S GET OUT OF HERE!", 6.66, urgent:true);
1465     return;
1466   }
1468   if (global.config.scumGhost == 0) {
1469     // never
1470     ghostTimeLeft = -1;
1471     return;
1472   }
1474   // randomizes time until ghost appears once time limit is reached
1475   // global.ghostRandom (true/false) if extra time is randomized - loaded from yasm.cfg. if false, use 30 second default
1476   // ghostTimeLeft (time in seconds * 1000) for currently generated level
1478   if (global.config.ghostRandom) {
1479     auto tMin = global.randOther(10, 30); // min(global.plife*10, 30);
1480     auto tMax = max(tMin+global.randOther(10, 30), min(global.config.scumGhost, 300)); // 5 minutes max
1481     auto tTime = global.randOther(tMin, tMax);
1482     if (tTime <= 0) tTime = round(tMax/2.0);
1483     ghostTimeLeft = tTime;
1484   } else {
1485     ghostTimeLeft = global.config.scumGhost; // 5 minutes max
1486   }
1488   ghostTimeLeft += max(0, global.config.ghostExtraTime);
1490   ghostTimeLeft *= 30; // seconds -> frames
1491   //global.ghostShowTime
1495 void spawnGhost () {
1496   addGhostSummoned();
1497   ghostSpawned = true;
1498   ghostTimeLeft = -1;
1500   int vwdt = (viewMax.x-viewMin.x);
1501   int vhgt = (viewMax.y-viewMin.y);
1503   int gx, gy;
1505   if (player.ix < viewMin.x+vwdt/2) {
1506     // player is in the left side
1507     gx = viewMin.x+vwdt/2+vwdt/4;
1508   } else {
1509     // player is in the right side
1510     gx = viewMin.x+vwdt/4;
1511   }
1513   if (player.iy < viewMin.y+vhgt/2) {
1514     // player is in the left side
1515     gy = viewMin.y+vhgt/2+vhgt/4;
1516   } else {
1517     // player is in the right side
1518     gy = viewMin.y+vhgt/4;
1519   }
1521   writeln("spawning ghost at tile (", gx/16, ",", gy/16, "); player is at tile (", player.ix/16, ",", player.iy/16, ")");
1523   MakeMapObject(gx, gy, 'oGhost');
1525   /*
1526     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);
1527     else instance_create(view_xview[0]-32, view_yview[0]+floor(view_hview[0]/2), oGhost);
1528     global.ghostExists = true;
1529   */
1533 void thinkFrameGameGhost () {
1534   if (player.dead) return;
1535   if (!isNormalLevel()) return; // just in case
1537   if (ghostTimeLeft < 0) {
1538     // turned off
1539     if (musicFadeTimer > 0) {
1540       musicFadeTimer = -1;
1541       global.setMusicPitch(1.0);
1542     }
1543     return;
1544   }
1546   if (musicFadeTimer >= 0) {
1547     ++musicFadeTimer;
1548     if (musicFadeTimer > 0 && musicFadeTimer <= 100*3) {
1549       float pitch = 1.0-(0.00226/3.0)*musicFadeTimer;
1550       //writeln("MFT: ", musicFadeTimer, "; pitch=", pitch);
1551       global.setMusicPitch(pitch);
1552     }
1553   }
1555   if (ghostTimeLeft == 0) {
1556     // she is already here!
1557     return;
1558   }
1560   // no ghost if we have a crown
1561   if (global.hasCrown) {
1562     ghostTimeLeft = -1;
1563     return;
1564   }
1566   // if she was already spawned, don't do it again
1567   if (ghostSpawned) {
1568     ghostTimeLeft = 0;
1569     return;
1570   }
1572   if (--ghostTimeLeft != 0) {
1573     // warning
1574     if (global.config.ghostExtraTime > 0) {
1575       if (ghostTimeLeft == global.config.ghostExtraTime*30) {
1576         osdMessage("A CHILL RUNS UP YOUR SPINE...\nLET'S GET OUT OF HERE!", 6.66, urgent:true);
1577       }
1578       if (musicFadeTimer < 0 && ghostTimeLeft <= global.config.ghostExtraTime*30) {
1579         musicFadeTimer = 0;
1580       }
1581     }
1582     return;
1583   }
1585   // spawn her
1586   if (player.isExitingSprite) {
1587     // no reason to spawn her, we're leaving
1588     ghostTimeLeft = -1;
1589     return;
1590   }
1592   spawnGhost();
1596 void thinkFrameGame () {
1597   thinkFrameGameGhost();
1598   // udjat eye blinking
1599   if (global.hasUdjatEye && player) {
1600     foreach (MapTile t; allExits) {
1601       if (t isa MapTileBlackMarketDoor) {
1602         auto dm = int(player.distanceToEntity(t));
1603         if (dm < 4) dm = 4;
1604         if (udjatAlarm < 1 || dm < udjatAlarm) udjatAlarm = dm;
1605       }
1606     }
1607   } else {
1608     global.udjatBlink = false;
1609     udjatAlarm = 0;
1610   }
1611   if (udjatAlarm > 0) {
1612     if (--udjatAlarm == 0) {
1613       global.udjatBlink = !global.udjatBlink;
1614       if (global.hasUdjatEye && player) {
1615         player.playSound(global.udjatBlink ? 'sndBlink1' : 'sndBlink2');
1616       }
1617     }
1618   }
1619   switch (levelKind) {
1620     case LevelKind.Stars: thinkFrameGameStars(); break;
1621     case LevelKind.Sun: thinkFrameGameSun(); break;
1622     case LevelKind.Moon: thinkFrameGameMoon(); break;
1623     case LevelKind.Transition: thinkFrameTransition(); break;
1624     case LevelKind.Intro: thinkFrameIntro(); break;
1625   }
1629 // ////////////////////////////////////////////////////////////////////////// //
1630 private final bool isWaterTileCB (MapTile t) {
1631   return (t && t.visible && t.water);
1635 private final bool isLavaTileCB (MapTile t) {
1636   return (t && t.visible && t.lava);
1640 // ////////////////////////////////////////////////////////////////////////// //
1641 const int GreatLakeStartTileY = 28;
1644 final void fillGreatLake () {
1645   if (global.lake == 1) {
1646     foreach (int y; GreatLakeStartTileY..tilesHeight) {
1647       foreach (int x; 0..tilesWidth) {
1648         auto t = checkTileAtPoint(x*16, y*16, delegate bool (MapTile t) {
1649           if (t.spectral || !t.visible || t.invisible || t.moveable) return false;
1650           return true;
1651         });
1652         if (!t) {
1653           t = MakeMapTile(x, y, 'oWaterSwim');
1654           if (!t) continue;
1655         }
1656         if (t.water) {
1657           t.setSprite(y == GreatLakeStartTileY ? 'sWaterTop' : 'sWater');
1658         } else if (t.lava) {
1659           t.setSprite(y == GreatLakeStartTileY ? 'sLavaTop' : 'sLava');
1660         }
1661       }
1662     }
1663   }
1667 // called once after level generation
1668 final void fixLiquidTop () {
1669   if (global.lake == 1) fillGreatLake();
1671   liquidTileCount = 0;
1672   foreach (MapTile t; objGrid.allObjects(MapTile)) {
1673     if (!t.water && !t.lava) continue;
1675     ++liquidTileCount;
1676     //writeln("fixing water tile(", GetClassName(t.Class), "):'", t.objName, "' (water=", t.water, "; lava=", t.lava, "); lqc=", liquidTileCount);
1678     //if (global.lake == 1) continue; // it is done in `fillGreatLake()`
1680     if (!checkTileAtPoint(t.ix+8, t.iy-8, (t.lava ? &isLavaTileCB : &isWaterTileCB))) {
1681       t.setSprite(t.lava ? 'sLavaTop' : 'sWaterTop');
1682     } else {
1683       // don't do this, it will destroy seaweed
1684       //t.setSprite(t.lava ? 'sLava' : 'sWater');
1685       auto spr = t.getSprite();
1686            if (!spr) t.setSprite(t.lava ? 'sLava' : 'sWater');
1687       else if (spr.Name == 'sLavaTop') t.setSprite('sLava');
1688       else if (spr.Name == 'sWaterTop') t.setSprite('sWater');
1689     }
1690   }
1691   //writeln("liquid tiles count: ", liquidTileCount);
1695 // ////////////////////////////////////////////////////////////////////////// //
1696 transient MapTile curWaterTile;
1697 transient bool curWaterTileCheckHitsLava;
1698 transient bool curWaterTileCheckHitsSolidOrWater; // only for `checkWaterOrSolidTilePartialCB`
1699 transient int curWaterTileLastHDir;
1700 transient ubyte[16, 16] curWaterOccupied;
1701 transient int curWaterOccupiedCount;
1702 transient int curWaterTileCheckX0, curWaterTileCheckY0;
1705 private final void clearCurWaterCheckState () {
1706   curWaterTileCheckHitsLava = false;
1707   curWaterOccupiedCount = 0;
1708   foreach (auto idx; 0..16*16) curWaterOccupied[idx] = 0;
1712 private final bool checkWaterOrSolidTileCB (MapTile t) {
1713   if (t == curWaterTile) return false;
1714   if (t.lava && curWaterTile.water) {
1715     curWaterTileCheckHitsLava = true;
1716     return true;
1717   }
1718   if (t.ix%16 != 0 || t.iy%16 != 0) {
1719     if (t.water || t.solid) {
1720       // fill occupied array
1721       //FIXME: optimize this
1722       if (curWaterOccupiedCount < 16*16) {
1723         foreach (auto dy; t.y0..t.y1+1) {
1724           foreach (auto dx; t.x0..t.x1+1) {
1725             int sx = dx-curWaterTileCheckX0;
1726             int sy = dy-curWaterTileCheckY0;
1727             if (sx >= 0 && sx <= 16 && sy >= 0 && sy <= 15 && !curWaterOccupied[sx, sy]) {
1728               curWaterOccupied[sx, sy] = 1;
1729               ++curWaterOccupiedCount;
1730             }
1731           }
1732         }
1733       }
1734     }
1735     return false; // need to check for lava
1736   }
1737   if (t.water || t.solid || t.lava) {
1738     curWaterOccupiedCount = 16*16;
1739     if (t.water && curWaterTile.lava) t.instanceRemove();
1740   }
1741   return false; // need to check for lava
1745 private final bool checkWaterOrSolidTilePartialCB (MapTile t) {
1746   if (t == curWaterTile) return false;
1747   if (t.lava && curWaterTile.water) {
1748     //writeln("!!!!!!!!");
1749     curWaterTileCheckHitsLava = true;
1750     return true;
1751   }
1752   if (t.water || t.solid || t.lava) {
1753     //writeln("*********");
1754     curWaterTileCheckHitsSolidOrWater = true;
1755     if (t.water && curWaterTile.lava) t.instanceRemove();
1756   }
1757   return false; // need to check for lava
1761 private final bool isFullyOccupiedAtTilePos (int tileX, int tileY) {
1762   clearCurWaterCheckState();
1763   curWaterTileCheckX0 = tileX*16;
1764   curWaterTileCheckY0 = tileY*16;
1765   checkTilesInRect(tileX*16, tileY*16, 16, 16, &checkWaterOrSolidTileCB);
1766   return (curWaterTileCheckHitsLava || curWaterOccupiedCount == 16*16);
1770 private final bool isAtLeastPartiallyOccupiedAtTilePos (int tileX, int tileY) {
1771   curWaterTileCheckHitsLava = false;
1772   curWaterTileCheckHitsSolidOrWater = false;
1773   checkTilesInRect(tileX*16, tileY*16, 16, 16, &checkWaterOrSolidTilePartialCB);
1774   return (curWaterTileCheckHitsSolidOrWater || curWaterTileCheckHitsLava);
1778 private final bool waterCanReachGroundHoleInDir (MapTile wtile, int dx) {
1779   if (dx == 0) return false; // just in case
1780   dx = sign(dx);
1781   int x = wtile.ix/16, y = wtile.iy/16;
1782   x += dx;
1783   while (x >= 0 && x < tilesWidth) {
1784     if (!isAtLeastPartiallyOccupiedAtTilePos(x, y+1)) return true;
1785     if (isAtLeastPartiallyOccupiedAtTilePos(x, y)) return false;
1786     x += dx;
1787   }
1788   return false;
1792 // returns `true` if this tile must be removed
1793 private final bool checkWaterFlow (MapTile wtile) {
1794   if (global.lake == 1) {
1795     if (wtile.iy >= GreatLakeStartTileY*16) return false; // lake tile, don't touch
1796     if (wtile.iy >= GreatLakeStartTileY*16-16) return true; // remove it, so it won't stack on a lake
1797   }
1799   if (wtile.ix%16 != 0 || wtile.iy%16 != 0) return true; // sanity check
1801   curWaterTile = wtile;
1802   curWaterTileLastHDir = 0; // never moved to the side
1804   bool wasMoved = false;
1806   for (;;) {
1807     int tileX = wtile.ix/16, tileY = wtile.iy/16;
1809     // out of level?
1810     if (tileY >= tilesHeight) return true;
1812     // check if we can fall down
1813     auto canFall = !isAtLeastPartiallyOccupiedAtTilePos(tileX, tileY+1);
1814     // disappear if can fall in lava
1815     if (wtile.water && curWaterTileCheckHitsLava) {
1816       //!writeln(wtile.objId, ": LAVA HIT DOWN");
1817       return true;
1818     }
1819     if (wasMoved) {
1820       // fake, so caller will not start removing tiles
1821       if (canFall) wtile.waterMovedDown = true;
1822       break;
1823     }
1824     // can move down?
1825     if (canFall) {
1826       // move down
1827       //!writeln(wtile.objId, ": GOING DOWN");
1828       curWaterTileLastHDir = 0;
1829       wtile.iy = wtile.iy+16;
1830       wasMoved = true;
1831       wtile.waterMovedDown = true;
1832       continue;
1833     }
1835     bool canMoveLeft = (curWaterTileLastHDir > 0 ? false : !isAtLeastPartiallyOccupiedAtTilePos(tileX-1, tileY));
1836     // disappear if near lava
1837     if (wtile.water && curWaterTileCheckHitsLava) {
1838       //!writeln(wtile.objId, ": LAVA HIT LEFT");
1839       return true;
1840     }
1842     bool canMoveRight = (curWaterTileLastHDir < 0 ? false : !isAtLeastPartiallyOccupiedAtTilePos(tileX+1, tileY));
1843     // disappear if near lava
1844     if (wtile.water && curWaterTileCheckHitsLava) {
1845       //!writeln(wtile.objId, ": LAVA HIT RIGHT");
1846       return true;
1847     }
1849     if (!canMoveLeft && !canMoveRight) {
1850       // do final checks
1851       //!if (wasMove) writeln(wtile.objId, ": NO MORE MOVES");
1852       break;
1853     }
1855     if (canMoveLeft && canMoveRight) {
1856       // choose random direction
1857       //!writeln(wtile.objId, ": CHOOSING RANDOM HDIR");
1858       // actually, choose direction that leads to hole in a ground
1859       if (waterCanReachGroundHoleInDir(wtile, -1)) {
1860         // can reach hole at the left side
1861         if (waterCanReachGroundHoleInDir(wtile, 1)) {
1862           // can reach hole at the right side, choose at random
1863           if (global.randOther(0, 1)) canMoveRight = false; else canMoveLeft = false;
1864         } else {
1865           // move left
1866           canMoveRight = false;
1867         }
1868       } else {
1869         // can't reach hole at the left side
1870         if (waterCanReachGroundHoleInDir(wtile, 1)) {
1871           // can reach hole at the right side, choose at random
1872           canMoveLeft = false;
1873         } else {
1874           // no holes at any side, choose at random
1875           if (global.randOther(0, 1)) canMoveRight = false; else canMoveLeft = false;
1876         }
1877       }
1878     }
1880     // move
1881     if (canMoveLeft) {
1882       if (canMoveRight) FatalError("WATERCHECK: WTF RIGHT");
1883       //!writeln(wtile.objId, ": MOVING LEFT (", curWaterTileLastHDir, ")");
1884       curWaterTileLastHDir = -1;
1885       wtile.ix = wtile.ix-16;
1886     } else if (canMoveRight) {
1887       if (canMoveLeft) FatalError("WATERCHECK: WTF LEFT");
1888       //!writeln(wtile.objId, ": MOVING RIGHT (", curWaterTileLastHDir, ")");
1889       curWaterTileLastHDir = 1;
1890       wtile.ix = wtile.ix+16;
1891     }
1892     wasMoved = true;
1893   }
1895   // remove seaweeds
1896   if (wasMoved) {
1897     checkWater = true;
1898     wtile.setSprite(wtile.lava ? 'sLava' : 'sWater');
1899     wtile.waterMoved = true;
1900     // if this tile was not moved down, check if it can move down on any next step
1901     if (!wtile.waterMovedDown) {
1902            if (waterCanReachGroundHoleInDir(wtile, -1)) wtile.waterMovedDown = true;
1903       else if (waterCanReachGroundHoleInDir(wtile, 1)) wtile.waterMovedDown = true;
1904     }
1905   }
1907   return false; // don't remove
1909   //if (!isWetTileAtPix(tileX*16+8, tileY*16-8)) wtile.setSprite(wtile.lava ? 'sLavaTop' : 'sWaterTop');
1913 transient array!MapTile waterTilesList;
1915 final bool sortWaterTilesByCoordsLess (MapTile a, MapTile b) {
1916   int dy = a.iy-b.iy;
1917   if (dy) return (dy < 0);
1918   return (a.ix < b.ix);
1921 transient int waterFlowPause = 0;
1922 transient bool debugWaterFlowPause = false;
1924 final void cleanDeadObjects () {
1925   // remove dead objects
1926   if (deadItemsHead) {
1927     auto olddel = ImmediateDelete;
1928     ImmediateDelete = false;
1929     do {
1930       auto it = deadItemsHead;
1931       deadItemsHead = it.deadItemsNext;
1932       if (it.grid) it.grid.remove(it.gridId);
1933       it.onDestroy();
1934       delete it;
1935     } while (deadItemsHead);
1936     ImmediateDelete = olddel;
1937     if (olddel) CollectGarbage(true); // destroy delayed objects too
1938   }
1941 final void cleanDeadTiles () {
1942   if (checkWater && /*global.lake == 1 ||*/ (!blockWaterChecking && liquidTileCount)) {
1943     if (global.lake == 1) fillGreatLake();
1944     if (waterFlowPause > 1) {
1945       --waterFlowPause;
1946       cleanDeadObjects();
1947       return;
1948     }
1949     if (debugWaterFlowPause) waterFlowPause = 4;
1950     //writeln("checking water");
1951     waterTilesList.clear();
1952     foreach (MapTile wtile; objGrid.allObjectsSafe(MapTile)) {
1953       if (wtile.water || wtile.lava) {
1954         // sanity check
1955         if (wtile.ix%16 == 0 && wtile.iy%16 == 0) {
1956           wtile.waterMoved = false;
1957           wtile.waterMovedDown = false;
1958           wtile.waterSlideOldX = wtile.ix;
1959           wtile.waterSlideOldY = wtile.iy;
1960           waterTilesList[$] = wtile;
1961         }
1962       }
1963     }
1964     checkWater = false;
1965     liquidTileCount = 0;
1966     waterTilesList.sort(&sortWaterTilesByCoordsLess);
1967     // do water flow
1968     bool wasAnyMove = false;
1969     bool wasAnyMoveDown = false;
1970     foreach (MapTile wtile; waterTilesList) {
1971       if (!wtile || !wtile.isInstanceAlive) continue;
1972       auto killIt = checkWaterFlow(wtile);
1973       if (killIt) {
1974         checkWater = true;
1975         wtile.smashMe();
1976         wtile.instanceRemove(); // just in case
1977       } else {
1978         wtile.saveInterpData();
1979         wtile.updateGrid();
1980         wasAnyMove = wasAnyMove || wtile.waterMoved;
1981         wasAnyMoveDown = wasAnyMoveDown || wtile.waterMovedDown;
1982         if (wtile.waterMoved && debugWaterFlowPause) wtile.waterSlideCounter = 4;
1983       }
1984     }
1985     // do water check
1986     liquidTileCount = 0;
1987     foreach (MapTile wtile; waterTilesList) {
1988       if (!wtile || !wtile.isInstanceAlive) continue;
1989       if (wasAnyMoveDown) {
1990         ++liquidTileCount;
1991         continue;
1992       }
1993       //checkWater = checkWater || wtile.waterMoved;
1994       curWaterTile = wtile;
1995       int tileX = wtile.ix/16, tileY = wtile.iy/16;
1996       // check if we are have no way to leak
1997       bool killIt = false;
1998       if (!isFullyOccupiedAtTilePos(tileX-1, tileY) || (wtile.water && curWaterTileCheckHitsLava)) {
1999         //writeln(" LEFT DEATH (", curWaterOccupiedCount, " : ", curWaterTileCheckHitsLava, ")");
2000         killIt = true;
2001       }
2002       if (!killIt && (!isFullyOccupiedAtTilePos(tileX+1, tileY) || (wtile.water && curWaterTileCheckHitsLava))) {
2003         //writeln(" RIGHT DEATH (", curWaterOccupiedCount, " : ", curWaterTileCheckHitsLava, ")");
2004         killIt = true;
2005       }
2006       if (!killIt && (!isFullyOccupiedAtTilePos(tileX, tileY+1) || (wtile.water && curWaterTileCheckHitsLava))) {
2007         //writeln(" DOWN DEATH (", curWaterOccupiedCount, " : ", curWaterTileCheckHitsLava, ")");
2008         killIt = true;
2009       }
2010       //killIt = false;
2011       if (killIt) {
2012         checkWater = true;
2013         wtile.smashMe();
2014         wtile.instanceRemove(); // just in case
2015       } else {
2016         ++liquidTileCount;
2017       }
2018     }
2019     if (wasAnyMove) checkWater = true;
2020     //writeln("water check: liquidTileCount=", liquidTileCount, "; checkWater=", checkWater, "; wasAnyMove=", wasAnyMove, "; wasAnyMoveDown=", wasAnyMoveDown);
2022     // fill empty spaces in lake with water
2023     fixLiquidTop();
2024   }
2026   cleanDeadObjects();
2030 // ////////////////////////////////////////////////////////////////////////// //
2031 private transient array!MapEntity postponedThinkers;
2032 private transient MapEntity thinkerHeld;
2033 private transient array!MapEntity activeThinkerList;
2036 final void doThinkActionsForObject (MapEntity o) {
2037        if (o.justSpawned) o.justSpawned = false;
2038   else if (o.imageSpeed > 0) o.nextAnimFrame();
2039   o.saveInterpData();
2040   o.thinkFrame();
2041   if (o.isInstanceAlive) {
2042     //o.updateGrid();
2043     o.processAlarms();
2044     if (o.isInstanceAlive) {
2045       if (o.whipTimer > 0) --o.whipTimer;
2046       o.updateGrid();
2047       auto obj = MapObject(o);
2048       if (!o.canLiveOutsideOfLevel && (!obj || !obj.heldBy) && o.isOutsideOfLevel()) {
2049         // oops, fallen out of level...
2050         o.onOutOfLevel();
2051       }
2052     }
2053   }
2057 // return `true` if thinker should be removed
2058 final void thinkOne (MapEntity o, optional bool doHeldObject, optional bool dontAddHeldObject) {
2059   if (!o) return;
2060   if (o == thinkerHeld && !doHeldObject) return; // skip it
2062   if (!o.isInstanceAlive) return;
2064   auto obj = MapObject(o);
2066   if (obj) obj.prevhp = obj.hp; // so i don't have to do it in `thinkFrame()`
2067   if (!o.active) return;
2069   if (obj && obj.heldBy == player) {
2070     // fix held item coords
2071     obj.fixHoldCoords();
2072     if (doHeldObject) {
2073       doThinkActionsForObject(o);
2074     } else {
2075       if (!dontAddHeldObject) {
2076         bool found = false;
2077         foreach (MapEntity e; postponedThinkers) if (e == o) { found = true; break; }
2078         if (!found) postponedThinkers[$] = o;
2079       }
2080     }
2081     return;
2082   }
2084   bool doThink = true;
2086   // collision with player weapon
2087   auto hh = PlayerWeapon(player.holdItem);
2088   bool doWeaponAction = false;
2089   if (hh) {
2090     if (hh.blockedBySolids && !global.config.killEnemiesThruWalls) {
2091       int xx = player.ix+(player.dir == MapObject::Dir.Left ? -16 : 16);
2092       //doWeaponAction = !isSolidAtPoint(xx, player.iy);
2093       doWeaponAction = !isSolidAtPoint(xx, hh.yCenter);
2094       /*
2095       int dh = max(1, hh.height-2);
2096       doWeaponAction = !checkTilesInRect(player.ix, player.iy);
2097       */
2098     } else {
2099       doWeaponAction = true;
2100     }
2101   }
2103   if (obj && doWeaponAction && hh && (o.whipTimer <= 0 || hh.ignoreWhipTimer) && hh.collidesWithObject(obj)) {
2104     //writeln("WEAPONED!");
2105     //writeln("weapon collides with '", GetClassName(o.Class), "' (", o.objType, "'");
2106     bool dontChangeWhipTimer = hh.dontChangeWhipTimer;
2107     if (!o.onTouchedByPlayerWeapon(player, hh)) {
2108       if (o.isInstanceAlive) hh.onCollisionWithObject(obj);
2109     }
2110     if (!dontChangeWhipTimer) o.whipTimer = o.whipTimerValue; //HACK
2111     doThink = o.isInstanceAlive;
2112   }
2114   if (doThink && o.isInstanceAlive) {
2115     doThinkActionsForObject(o);
2116     doThink = o.isInstanceAlive;
2117   }
2119   // collision with player
2120   if (doThink && obj && o.collidesWith(player)) {
2121     if (!player.onObjectTouched(obj) && o.isInstanceAlive) {
2122       doThink = !o.onTouchedByPlayer(player);
2123       o.updateGrid();
2124     }
2125   }
2129 final void processThinkers (float timeDelta) {
2130   if (timeDelta <= 0) return;
2131   if (gamePaused) {
2132     ++pausedTime;
2133     if (onBeforeFrame) onBeforeFrame(false);
2134     if (onAfterFrame) onAfterFrame(false);
2135     keysNextFrame();
2136     return;
2137   } else {
2138     pausedTime = 0;
2139   }
2140   accumTime += timeDelta;
2141   bool wasFrame = false;
2142   // block GC
2143   auto olddel = ImmediateDelete;
2144   ImmediateDelete = false;
2145   while (accumTime >= FrameTime) {
2146     postponedThinkers.clear();
2147     thinkerHeld = none;
2148     accumTime -= FrameTime;
2149     if (onBeforeFrame) onBeforeFrame(accumTime >= FrameTime);
2150     // shake
2151     if (shakeLeft > 0) {
2152       --shakeLeft;
2153       if (!shakeDir.x) shakeDir.x = (global.randOther(0, 1) ? -3 : 3);
2154       if (!shakeDir.y) shakeDir.y = (global.randOther(0, 1) ? -3 : 3);
2155       shakeOfs.x = shakeDir.x;
2156       shakeOfs.y = shakeDir.y;
2157       int sgnc = global.randOther(1, 3);
2158       if (sgnc&0x01) shakeDir.x = -shakeDir.x;
2159       if (sgnc&0x02) shakeDir.y = -shakeDir.y;
2160     } else {
2161       shakeOfs.x = 0;
2162       shakeOfs.y = 0;
2163       shakeDir.x = 0;
2164       shakeDir.y = 0;
2165     }
2166     // advance time
2167     time += 1;
2168     // we don't want the time to grow too large
2169     if (time < 0) { time = 0; lastRenderTime = -1; }
2170     // game-global events
2171     thinkFrameGame();
2172     // frame thinkers: player
2173     if (player && !disablePlayerThink) {
2174       // time limit
2175       if (!player.dead && isNormalLevel() &&
2176           (maxPlayingTime < 0 ||
2177            (maxPlayingTime > 0 && maxPlayingTime*30 <= time &&
2178             time%30 == 0 && global.randOther(1, 100) <= 20)))
2179       {
2180         global.hasAnkh = false;
2181         global.plife = 1;
2182         player.invincible = 0;
2183         auto xplo = MapObjExplosion(MakeMapObject(player.ix, player.iy, 'oExplosion'));
2184         if (xplo) xplo.suicide = true;
2185       }
2186       //HACK: check for stolen items
2187       auto item = MapItem(player.holdItem);
2188       if (item) item.onCheckItemStolen(player);
2189       item = MapItem(player.pickedItem);
2190       if (item) item.onCheckItemStolen(player);
2191       // normal thinking
2192       doThinkActionsForObject(player);
2193     }
2194     // frame thinkers: held object
2195     thinkerHeld = player.holdItem;
2196     if (thinkerHeld && thinkerHeld.isInstanceAlive) {
2197       bool wasAct = thinkerHeld.active;
2198       thinkOne(thinkerHeld, doHeldObject:true);
2199       if (!thinkerHeld.isInstanceAlive) {
2200         if (player.holdItem == thinkerHeld) player.holdItem = none;
2201         thinkerHeld.grid.remove(thinkerHeld.gridId);
2202         /* later
2203         thinkerHeld.onDestroy();
2204         delete thinkerHeld;
2205         */
2206       } else if (!wasAct) {
2207         //HACK!
2208         auto item = MapItem(thinkerHeld);
2209         if (item) {
2210           if (item.forSale || item.sellOfferDone) {
2211             if (++item.forSaleFrame < 0) item.forSaleFrame = 0;
2212           }
2213         }
2214       }
2215     }
2216     // frame thinkers: objects
2217     activeThinkerList.clear();
2218     auto grid = objGrid;
2219     // collect active objects
2220     if (global.config.useFrozenRegion) {
2221       foreach (MapEntity e; grid.inRectPix(viewStart.x/global.scale-64, viewStart.y/global.scale-64, 320+64*2, 240+64*2, precise:false)) {
2222         if (e.active) activeThinkerList[$] = e;
2223       }
2224     } else {
2225       // no frozen area
2226       foreach (MapEntity e; grid.allObjects()) {
2227         if (e.active) activeThinkerList[$] = e;
2228       }
2229     }
2230     // process active objects
2231     //writeln("thinkers: ", activeThinkerList.length);
2232     foreach (MapEntity o; activeThinkerList) {
2233       if (!o) continue;
2234       thinkOne(o, doHeldObject:false);
2235       if (!o.isInstanceAlive) {
2236         //writeln("dead thinker: '", o.objType, "'");
2237         if (o.grid) o.grid.remove(o.gridId);
2238         auto obj = MapObject(o);
2239         if (obj && obj.heldBy) obj.heldBy.holdItem = none;
2240         /* later
2241         o.onDestroy();
2242         delete o;
2243         */
2244       }
2245     }
2246     // postponed thinkers
2247     foreach (MapEntity o; postponedThinkers) {
2248       if (!o) continue;
2249       thinkOne(o, doHeldObject:true, dontAddHeldObject:true);
2250       if (!o.isInstanceAlive) {
2251         //writeln("dead pp-thinker: '", o.objType, "'");
2252         /* later
2253         o.onDestroy();
2254         delete o;
2255         */
2256       }
2257     }
2258     postponedThinkers.clear();
2259     thinkerHeld = none;
2260     // clean dead things
2261     cleanDeadTiles();
2262     // fix held item coords
2263     if (player && player.holdItem) {
2264       if (player.holdItem.isInstanceAlive) {
2265         player.holdItem.fixHoldCoords();
2266       } else {
2267         player.holdItem = none;
2268       }
2269     }
2270     // money counter
2271     if (collectCounter == 0) {
2272       xmoney = max(0, xmoney-100);
2273     } else {
2274       --collectCounter;
2275     }
2276     // other things
2277     if (player) {
2278       if (!player.dead) stats.oneMoreFramePlayed();
2279       SoundSystem.ListenerOrigin = vector(player.xCenter, player.yCenter, -1);
2280       //writeln("plrpos=(", player.xCenter, ",", player.yCenter, "); lo=", SoundSystem.ListenerOrigin);
2281     }
2282     if (onAfterFrame) onAfterFrame(accumTime >= FrameTime);
2283     ++framesProcessedFromLastClear;
2284     keysNextFrame();
2285     wasFrame = true;
2286     if (!player.visible && player.holdItem) player.holdItem.visible = false;
2287     if (winCutsceneSwitchToNext) {
2288       winCutsceneSwitchToNext = false;
2289       switch (++inWinCutscene) {
2290         case 2: startWinCutsceneVolcano(); break;
2291         case 3: default: startWinCutsceneWinFall(); break;
2292       }
2293       break;
2294     }
2295     if (playerExited) break;
2296   }
2297   ImmediateDelete = olddel;
2298   if (playerExited) {
2299     playerExited = false;
2300     onLevelExited();
2301     centerViewAtPlayer();
2302   }
2303   if (wasFrame) {
2304     // if we were processed at least one frame, collect garbage
2305     //keysNextFrame();
2306     CollectGarbage(true); // destroy delayed objects too
2307   }
2308   if (accumTime > 0 && onInterFrame) onInterFrame(accumTime/FrameTime);
2312 // ////////////////////////////////////////////////////////////////////////// //
2313 final void tileXY2roomXY (int tileX, int tileY, out int roomX, out int roomY) {
2314   roomX = (tileX-1)/RoomGen::Width;
2315   roomY = (tileY-1)/RoomGen::Height;
2319 final bool isInShop (int tileX, int tileY) {
2320   if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
2321     auto n = roomType[tileX, tileY];
2322     if (n == 4 || n == 5) return true;
2323     return !!checkTilesInRect(tileX*16, tileY*16, 16, 16, delegate bool (MapTile t) { return t.shopWall; });
2324     //k8: we don't have this
2325     //if (t && t.objType == 'oShop') return true;
2326   }
2327   return false;
2331 // ////////////////////////////////////////////////////////////////////////// //
2332 override void Destroy () {
2333   clearWholeLevel();
2334   delete tempSolidTile;
2335   ::Destroy();
2339 // ////////////////////////////////////////////////////////////////////////// //
2340 // WARNING! delegate should not create/delete objects!
2341 final MapObject findNearestObject (int px, int py, scope bool delegate (MapObject o) dg, optional class!MapObject castClass) {
2342   MapObject res = none;
2343   if (!castClass) castClass = MapObject;
2344   int curdistsq = int.max;
2345   foreach (MapObject o; objGrid.allObjects(MapObject)) {
2346     if (o.spectral) continue;
2347     if (!dg(o)) continue;
2348     int xc = px-o.xCenter, yc = py-o.yCenter;
2349     int distsq = xc*xc+yc*yc;
2350     if (distsq < curdistsq) {
2351       res = o;
2352       curdistsq = distsq;
2353     }
2354   }
2355   return res;
2359 // WARNING! delegate should not create/delete objects!
2360 final MapObject findNearestEnemy (int px, int py, optional scope bool delegate (MapEnemy o) dg, optional class!MapEnemy castClass) {
2361   if (!castClass) castClass = MapEnemy;
2362   if (castClass !isa MapEnemy) return none;
2363   MapObject res = none;
2364   int curdistsq = int.max;
2365   foreach (MapEnemy o; objGrid.allObjects(castClass)) {
2366     //k8: i added `dead` check
2367     if (o.spectral || o.dead) continue;
2368     if (dg) {
2369       if (!dg(o)) continue;
2370     }
2371     int xc = px-o.xCenter, yc = py-o.yCenter;
2372     int distsq = xc*xc+yc*yc;
2373     if (distsq < curdistsq) {
2374       res = o;
2375       curdistsq = distsq;
2376     }
2377   }
2378   return res;
2382 final MonsterShopkeeper findNearestCalmShopkeeper (int px, int py) {
2383   auto obj = MonsterShopkeeper(findNearestEnemy(px, py, delegate bool (MapEnemy o) {
2384     auto sk = MonsterShopkeeper(o);
2385     if (sk && !sk.angered) return true;
2386     return false;
2387   }, castClass:MonsterShopkeeper));
2388   return obj;
2392 final MonsterShopkeeper hasAliveShopkeepers (optional bool skipAngry) {
2393   foreach (MonsterShopkeeper sc; objGrid.allObjects(MonsterShopkeeper)) {
2394     if (sc.spectral || sc.dead) continue;
2395     if (skipAngry && (sc.angered || sc.outlaw)) continue;
2396     return sc;
2397   }
2398   return none;
2402 // WARNING! delegate should not create/delete objects!
2403 final int calcNearestEnemyDist (int px, int py, optional scope bool delegate (MapEnemy o) dg, optional class!MapEnemy castClass) {
2404   auto e = findNearestEnemy(px, py, dg!optional, castClass!optional);
2405   if (!e) return int.max;
2406   int xc = px-e.xCenter, yc = py-e.yCenter;
2407   return round(sqrt(xc*xc+yc*yc));
2411 // WARNING! delegate should not create/delete objects!
2412 final int calcNearestObjectDist (int px, int py, optional scope bool delegate (MapObject o) dg, optional class!MapObject castClass) {
2413   auto e = findNearestObject(px, py, dg!optional, castClass!optional);
2414   if (!e) return int.max;
2415   int xc = px-e.xCenter, yc = py-e.yCenter;
2416   return round(sqrt(xc*xc+yc*yc));
2420 // WARNING! delegate should not create/delete objects!
2421 final MapTile findNearestMoveableSolid (int px, int py, optional scope bool delegate (MapTile t) dg) {
2422   MapTile res = none;
2423   int curdistsq = int.max;
2424   foreach (MapTile t; objGrid.allObjects(MapTile)) {
2425     if (t.spectral) continue;
2426     if (dg) {
2427       if (!dg(t)) continue;
2428     } else {
2429       if (!t.solid || !t.moveable) continue;
2430     }
2431     int xc = px-t.xCenter, yc = py-t.yCenter;
2432     int distsq = xc*xc+yc*yc;
2433     if (distsq < curdistsq) {
2434       res = t;
2435       curdistsq = distsq;
2436     }
2437   }
2438   return res;
2442 // WARNING! delegate should not create/delete objects!
2443 final MapTile findNearestTile (int px, int py, optional scope bool delegate (MapTile t) dg) {
2444   if (!dg) return none;
2445   MapTile res = none;
2446   int curdistsq = int.max;
2448   //FIXME: make this faster!
2449   foreach (MapTile t; objGrid.allObjects(MapTile)) {
2450     if (t.spectral) continue;
2451     int xc = px-t.xCenter, yc = py-t.yCenter;
2452     int distsq = xc*xc+yc*yc;
2453     if (distsq < curdistsq && dg(t)) {
2454       res = t;
2455       curdistsq = distsq;
2456     }
2457   }
2459   return res;
2463 // ////////////////////////////////////////////////////////////////////////// //
2464 final bool cbIsObjectWeb (MapObject o) { return (o isa ItemWeb); }
2465 final bool cbIsObjectBlob (MapObject o) { return (o isa EnemyBlob); }
2466 final bool cbIsObjectArrow (MapObject o) { return (o isa ItemProjectileArrow); }
2467 final bool cbIsObjectEnemy (MapObject o) { return (/*o != self &&*/ o isa MapEnemy); }
2469 final bool cbIsObjectTreasure (MapObject o) { return (o.isTreasure); }
2471 final bool cbIsItemObject (MapObject o) { return (o isa MapItem); }
2473 final bool cbIsItemOrEnemyObject (MapObject o) { return (o isa MapItem || o isa MapEnemy); }
2476 final MapObject isObjectAtTile (int tileX, int tileY, optional scope bool delegate (MapObject o) dg, optional bool precise) {
2477   if (!specified_precise) precise = true;
2478   tileX *= 16;
2479   tileY *= 16;
2480   foreach (MapObject o; objGrid.inRectPix(tileX, tileY, 16, 16, precise:precise, castClass:MapObject)) {
2481     if (o.spectral) continue;
2482     if (dg) {
2483       if (dg(o)) return o;
2484     } else {
2485       return o;
2486     }
2487   }
2488   return none;
2492 final MapObject isObjectAtTilePix (int x, int y, optional scope bool delegate (MapObject o) dg) {
2493   return isObjectAtTile(x/16, y/16, dg!optional);
2497 final MapObject isObjectAtPoint (int xpos, int ypos, optional scope bool delegate (MapObject o) dg, optional bool precise, optional class!MapObject castClass) {
2498   if (!specified_precise) precise = true;
2499   if (!castClass) castClass = MapObject;
2500   foreach (MapObject o; objGrid.inCellPix(xpos, ypos, precise:precise, castClass:castClass)) {
2501     if (o.spectral) continue;
2502     if (dg) {
2503       if (dg(o)) return o;
2504     } else {
2505       if (o isa MapEnemy) return o;
2506     }
2507   }
2508   return none;
2512 final MapObject isObjectInRect (int xpos, int ypos, int w, int h, optional scope bool delegate (MapObject o) dg, optional bool precise, optional class!MapObject castClass) {
2513   if (w < 1 || h < 1) return none;
2514   if (!castClass) castClass = MapObject;
2515   if (w == 1 && h == 1) return isObjectAtPoint(xpos, ypos, dg!optional, precise!optional, castClass);
2516   if (!specified_precise) precise = true;
2517   foreach (MapObject o; objGrid.inRectPix(xpos, ypos, w, h, precise:precise, castClass:castClass)) {
2518     if (o.spectral) continue;
2519     if (dg) {
2520       if (dg(o)) return o;
2521     } else {
2522       if (o isa MapEnemy) return o;
2523     }
2524   }
2525   return none;
2529 final MapObject forEachObject (scope bool delegate (MapObject o) dg, optional bool allowSpectrals, optional class!MapObject castClass) {
2530   if (!dg) return none;
2531   if (!castClass) castClass = MapObject;
2532   foreach (MapObject o; objGrid.allObjectsSafe(castClass)) {
2533     if (!allowSpectrals && o.spectral) continue;
2534     if (dg(o)) return o;
2535   }
2536   return none;
2540 final MapObject forEachObjectAtPoint (int xpos, int ypos, scope bool delegate (MapObject o) dg, optional bool precise) {
2541   if (!dg) return none;
2542   if (!specified_precise) precise = true;
2543   foreach (MapObject o; objGrid.inCellPix(xpos, ypos, precise:precise, castClass:MapObject)) {
2544     if (o.spectral) continue;
2545     if (dg(o)) return o;
2546   }
2547   return none;
2551 final MapObject forEachObjectInRect (int xpos, int ypos, int w, int h, scope bool delegate (MapObject o) dg, optional bool precise) {
2552   if (!dg || w < 1 || h < 1) return none;
2553   if (w == 1 && h == 1) return forEachObjectAtPoint(xpos, ypos, dg!optional, precise!optional);
2554   if (!specified_precise) precise = true;
2555   foreach (MapObject o; objGrid.inRectPix(xpos, ypos, w, h, precise:precise, castClass:MapObject)) {
2556     if (o.spectral) continue;
2557     if (dg(o)) return o;
2558   }
2559   return none;
2563 final MapEntity forEachEntityInRect (int xpos, int ypos, int w, int h, scope bool delegate (MapEntity o) dg, optional bool precise, optional class!MapEntity castClass) {
2564   if (!dg || w < 1 || h < 1) return none;
2565   if (!castClass) castClass = MapEntity;
2566   if (!specified_precise) precise = true;
2567   foreach (MapEntity e; objGrid.inRectPix(xpos, ypos, w, h, precise:precise, castClass:castClass)) {
2568     if (e.spectral) continue;
2569     if (dg(e)) return e;
2570   }
2571   return none;
2575 private final bool cbIsRopeTile (MapTile t) { return (t isa MapTileRope); }
2577 final MapTile isRopeAtPoint (int px, int py) {
2578   return checkTileAtPoint(px, py, &cbIsRopeTile);
2582 //FIXME!
2583 final MapTile isWaterSwimAtPoint (int px, int py) {
2584   return isWaterAtPoint(px, py);
2588 // ////////////////////////////////////////////////////////////////////////// //
2589 private array!MapEntity tmpEntityList;
2591 private final bool cbCollectEntitiesWithMask (MapEntity t) {
2592   if (!t.visible || t.spectral) return false;
2593   tmpEntityList[$] = t;
2594   return false;
2598 final void touchEntitiesWithMask (int x, int y, SpriteFrame frm, scope bool delegate (MapEntity t) dg, optional class!MapEntity castClass) {
2599   if (!frm || frm.bw < 1 || frm.bh < 1 || !dg) return;
2600   if (frm.isEmptyPixelMask) return;
2601   if (!castClass) castClass = MapEntity;
2602   // collect tiles
2603   if (tmpEntityList.length) tmpEntityList.clear();
2604   if (player isa castClass && player.isRectCollisionFrame(frm, x, y)) tmpEntityList[$] = player;
2605   forEachEntityInRect(x+frm.bx, y+frm.by, frm.bw, frm.bh, &cbCollectEntitiesWithMask, castClass:castClass);
2606   foreach (MapEntity e; tmpEntityList) {
2607     if (!e || !e.isInstanceAlive || !e.visible || e.spectral) continue;
2608     if (e.isRectCollisionFrame(frm, x, y)) {
2609       if (dg(e)) break;
2610     }
2611   }
2615 // ////////////////////////////////////////////////////////////////////////// //
2616 final bool cbCollisionAnySolid (MapTile t) { return t.solid; }
2617 final bool cbCollisionAnyMoveableSolid (MapTile t) { return t.solid && t.moveable; }
2618 final bool cbCollisionLiquid (MapTile t) { return (t.water || t.lava); }
2619 final bool cbCollisionAnySolidOrLiquid (MapTile t) { return (t.solid || t.water || t.lava); }
2620 final bool cbCollisionLadder (MapTile t) { return t.ladder; }
2621 final bool cbCollisionLadderTop (MapTile t) { return t.laddertop; }
2622 final bool cbCollisionAnyLadder (MapTile t) { return (t.ladder || t.laddertop); }
2623 final bool cbCollisionAnySolidIce (MapTile t) { return (t.solid && t.ice); }
2624 final bool cbCollisionWater (MapTile t) { return t.water; }
2625 final bool cbCollisionLava (MapTile t) { return t.lava; }
2626 final bool cbCollisionAnyTree (MapTile t) { return t.tree; }
2627 final bool cbCollisionPlatform (MapTile t) { return t.platform; }
2628 final bool cbCollisionLadderOrPlatform (MapTile t) { return (t.ladder || t.platform); }
2629 //final bool cbCollisionAnyHangeable (MapTile t) { return t.hangeable; } // no need to check for solids here, it is done in MapTile initializer
2630 final bool cbCollisionSpringTrap (MapTile t) { return t.springtrap; }
2631 final bool cbCollisionSpikes (MapTile t) { return t.spikes; }
2632 final bool cbCollisionExitTile (MapTile t) { return t.isExitActive(); }
2634 final bool cbCollisionForWhoa (MapTile t) { return (t.solid || t.laddertop); }
2636 //final bool cbCollisionSacAltar (MapTile t) { return (t.objName == 'oSacAltarLeft'); } // only left part, yeah
2637 final bool cbCollisionSacAltar (MapTile t) { return t.sacrificingAltar; }
2640 // ////////////////////////////////////////////////////////////////////////// //
2641 transient MapTileTemp tempSolidTile;
2643 private final MapTileTemp makeWalkeableSolidTile (MapObject o) {
2644   if (!tempSolidTile) {
2645     tempSolidTile = SpawnObject(MapTileTemp);
2646   } else if (!tempSolidTile.isInstanceAlive) {
2647     delete tempSolidTile;
2648     tempSolidTile = SpawnObject(MapTileTemp);
2649   }
2650   // setup data
2651   tempSolidTile.level = self;
2652   tempSolidTile.global = global;
2653   tempSolidTile.solid = true;
2654   tempSolidTile.objName = MapTileTemp.default.objName;
2655   tempSolidTile.objType = MapTileTemp.default.objType;
2656   tempSolidTile.e = o;
2657   tempSolidTile.fltx = o.fltx;
2658   tempSolidTile.flty = o.flty;
2659   return tempSolidTile;
2663 final MapTile checkTilesInRect (int x0, int y0, const int w, const int h,
2664                                 optional scope bool delegate (MapTile dg) dg, optional bool precise,
2665                                 optional class!MapTile castClass)
2667   if (w < 1 || h < 1) return none;
2668   if (w == 1 && h == 1) return checkTileAtPoint(x0, y0, dg!optional);
2669   int x1 = x0+w-1, y1 = y0+h-1;
2670   if (x0 >= tilesWidth*16 || y0 >= tilesHeight*16 || x1 < 0 || y1 < 0) return none;
2671   if (!specified_precise) precise = true;
2672   if (!castClass) castClass = MapTile;
2673   if (!dg) dg = &cbCollisionAnySolid;
2675   // check walkable solid objects too
2676   foreach (MapEntity e; objGrid.inRectPix(x0, y0, w, h, precise:precise, castClass:castClass)) {
2677     if (e.spectral || !e.visible) continue;
2678     auto t = MapTile(e);
2679     if (t) {
2680       if (dg(t)) return t;
2681       continue;
2682     }
2683     auto o = MapObject(e);
2684     if (o && o.walkableSolid) {
2685       t = makeWalkeableSolidTile(o);
2686       if (dg(t)) return t;
2687       continue;
2688     }
2689   }
2691   return none;
2695 final MapTile checkTileAtPoint (int x0, int y0, optional scope bool delegate (MapTile dg) dg, optional bool precise, optional class!MapTile castClass) {
2696   if (x0 < 0 || y0 < 0 || x0 >= tilesWidth*16 || y0 >= tilesHeight*16) return none;
2697   if (!specified_precise) precise = true;
2698   if (!castClass) castClass = MapTile;
2699   if (!dg) dg = &cbCollisionAnySolid;
2701   // check walkable solid objects
2702   foreach (MapEntity e; objGrid.inCellPix(x0, y0, precise:precise, castClass:castClass)) {
2703     if (e.spectral || !e.visible) continue;
2704     auto t = MapTile(e);
2705     if (t) {
2706       if (dg(t)) return t;
2707       continue;
2708     }
2709     auto o = MapObject(e);
2710     if (o && o.walkableSolid) {
2711       t = makeWalkeableSolidTile(o);
2712       if (dg(t)) return t;
2713       continue;
2714     }
2715   }
2717   return none;
2721 // ////////////////////////////////////////////////////////////////////////// //
2722 final MapTile isSolidAtPoint (int px, int py) { return checkTileAtPoint(px, py); }
2723 final MapTile isSolidInRect (int rx, int ry, int rw, int rh) { return checkTilesInRect(rx, ry, rw, rh); }
2724 final MapTile isLadderAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadder); }
2725 final MapTile isLadderOrPlatformAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadderOrPlatform); }
2726 final MapTile isLadderTopAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadderTop); }
2727 final MapTile isAnyLadderAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyLadder); }
2728 final MapTile isWaterAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionWater); }
2729 final MapTile isLavaAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLava); }
2730 final MapTile isLiquidAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLiquid); }
2731 final MapTile isIceAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnySolidIce); }
2732 final MapTile isTreeAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyTree); }
2733 //final MapTile isHangeableAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyHangeable); }
2736 // ////////////////////////////////////////////////////////////////////////// //
2737 final void setTileTypeAt (int tileX, int tileY, LevelGen::RType rt) {
2738   if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) roomType[tileX, tileY] = rt;
2742 //FIXME: make this faster
2743 transient float gtagX, gtagY;
2745 // only non-moveables and non-specials
2746 final MapTile getTileAtGrid (int tileX, int tileY) {
2747   gtagX = tileX*16;
2748   gtagY = tileY*16;
2749   return checkTileAtPoint(tileX*16, tileY*16, delegate bool (MapTile t) {
2750     if (t.spectral || t.moveable || t.toSpecialGrid || !t.visible) return false;
2751     if (t.fltx != gtagX || t.flty != gtagY) return false; // emulate grid
2752     if (t.width != 16 || t.height != 16) return false;
2753     return true;
2754   }, precise:false);
2755   //return (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight ? tiles[tileX, tileY] : none);
2759 final MapTile getTileAtGridAny (int tileX, int tileY) {
2760   gtagX = tileX*16;
2761   gtagY = tileY*16;
2762   return checkTileAtPoint(tileX*16, tileY*16, delegate bool (MapTile t) {
2763     if (t.spectral /*|| t.moveable*/ || !t.visible) return false;
2764     if (t.fltx != gtagX || t.flty != gtagY) return false; // emulate grid
2765     if (t.width != 16 || t.height != 16) return false;
2766     return true;
2767   }, precise:false);
2768   //return (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight ? tiles[tileX, tileY] : none);
2772 final bool isNamedTileAt (int tileX, int tileY, name atypename) {
2773   if (!atypename) return false;
2774   auto t = getTileAtGridAny(tileX, tileY);
2775   return (t && t.objName == atypename);
2779 final void setTileAtGrid (int tileX, int tileY, MapTile tile) {
2780   if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
2781     if (tile) {
2782       tile.fltx = tileX*16;
2783       tile.flty = tileY*16;
2784       if (!tile.dontReplaceOthers) {
2785         auto osp = tile.spectral;
2786         tile.spectral = true;
2787         auto t = getTileAtGridAny(tileX, tileY);
2788         tile.spectral = osp;
2789         if (t && !t.immuneToReplacement) {
2790           writeln("REPLACING tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
2791           writeln("      NEW tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (tile.toSpecialGrid ? "special " : ""), "grid, pos=(", tile.fltx/16.0, ",", tile.flty/16.0, ")");
2792           t.instanceRemove();
2793         }
2794       }
2795       insertObject(tile);
2796     } else {
2797       auto t = getTileAtGridAny(tileX, tileY);
2798       if (t && !t.immuneToReplacement) {
2799         writeln("REMOVING tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
2800         t.instanceRemove();
2801       }
2802     }
2803   }
2807 // ////////////////////////////////////////////////////////////////////////// //
2808 // return `true` from delegate to stop
2809 MapTile forEachSolidTileOnGrid (scope bool delegate (int tileX, int tileY, MapTile t) dg) {
2810   if (!dg) return none;
2811   foreach (MapTile t; objGrid.allObjectsSafe(MapTile)) {
2812     if (t.spectral || !t.solid || !t.visible) continue;
2813     if (t.ix%16 != 0 || t.iy%16 != 0) continue; // emulate grid
2814     if (t.width != 16 || t.height != 16) continue;
2815     if (dg(t.ix/16, t.iy/16, t)) return t;
2816   }
2817   return none;
2821 // ////////////////////////////////////////////////////////////////////////// //
2822 // return `true` from delegate to stop
2823 MapTile forEachTile (scope bool delegate (MapTile t) dg, optional class!MapTile castClass) {
2824   if (!dg) return none;
2825   if (!castClass) castClass = MapTile;
2826   foreach (MapTile t; objGrid.allObjectsSafe(castClass)) {
2827     if (t.spectral || !t.visible) continue;
2828     if (dg(t)) return t;
2829   }
2830   return none;
2834 // ////////////////////////////////////////////////////////////////////////// //
2835 final void fixWallTiles () {
2836   foreach (MapTile t; objGrid.allObjectsSafe(MapTile)) t.beautifyTile();
2840 // ////////////////////////////////////////////////////////////////////////// //
2841 final MapTile isCollisionAtPoint (int px, int py, optional scope bool delegate (MapTile dg) dg) {
2842   if (!dg) dg = &cbCollisionAnySolid;
2843   return checkTilesInRect(px, py, 1, 1, dg);
2847 // ////////////////////////////////////////////////////////////////////////// //
2848 string scrGetKaliGift (MapTile altar, optional name gift) {
2849   string res;
2851   // find other side of the altar
2852   int sx = player.ix, sy = player.iy;
2853   if (altar) {
2854     sx = altar.ix;
2855     sy = altar.iy;
2856     auto a2 = isCollisionAtPoint(altar.ix+18, altar.iy+8, &cbCollisionSacAltar);
2857     if (!a2) a2 = isCollisionAtPoint(altar.ix-8, altar.iy+8, &cbCollisionSacAltar);
2858     if (a2) { sx = a2.ix; sy = a2.iy; }
2859   }
2861        if (global.favor <= -8) res = "SHE SEEMS VERY ANGRY WITH YOU!";
2862   else if (global.favor < 0) res = "SHE SEEMS ANGRY WITH YOU.";
2863   else if (global.favor == 0) res = "SHE HAS FORGIVEN YOU!";
2864   else if (global.favor >= 32) {
2865     if (global.kaliGift >= 3 && global.favor >= 32+(global.kaliGift-2)*16) {
2866       res = "YOU FEEL INVIGORATED!";
2867       global.kaliGift += 1;
2868       global.plife += global.randOther(4, 8);
2869     } else if (global.kaliGift >= 3) {
2870       res = "SHE SEEMS ECSTATIC WITH YOU!";
2871     } else if (global.bombs < 80) {
2872       res = "YOUR SATCHEL FEELS VERY FULL NOW!";
2873       global.kaliGift = 3;
2874       global.bombs = 99;
2875     } else {
2876       res = "YOU FEEL INVIGORATED!";
2877       global.kaliGift += 1;
2878       global.plife += global.randOther(4, 8);
2879     }
2880   } else if (global.favor >= 16) {
2881     if (global.kaliGift >= 2) {
2882       res = "SHE SEEMS VERY HAPPY WITH YOU!";
2883     } else {
2884       res = "SHE BESTOWS A GIFT UPON YOU!";
2885       global.kaliGift = 2;
2886       // poofs
2887       auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2888       obj.xVel = -1;
2889       obj.yVel = 0;
2890       obj = MakeMapObject(sx, sy-8, 'oPoof');
2891       obj.xVel = 1;
2892       obj.yVel = 0;
2893       // a gift
2894       obj = none;
2895       if (gift) obj = MakeMapObject(sx, sy-8, gift);
2896       if (!obj) obj = MakeMapObject(sx, sy-8, 'oKapala');
2897     }
2898   } else if (global.favor >= 8) {
2899     if (global.kaliGift >= 1) {
2900       res = "SHE SEEMS HAPPY WITH YOU.";
2901     } else {
2902       res = "SHE BESTOWS A GIFT UPON YOU!";
2903       global.kaliGift = 1;
2904       //rAltar = instance_nearest(x, y, oSacAltarRight);
2905       //if (instance_exists(rAltar)) {
2906       // poofs
2907       auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2908       obj.xVel = -1;
2909       obj.yVel = 0;
2910       obj = MakeMapObject(sx, sy-8, 'oPoof');
2911       obj.xVel = 1;
2912       obj.yVel = 0;
2913       obj = none;
2914       if (gift) obj = MakeMapObject(sx, sy-8, gift);
2915       if (!obj) {
2916         auto n = global.randOther(1, 8);
2917         auto m = n;
2918         for (;;) {
2919           name aname = '';
2920                if (n == 1 && !global.hasCape && !global.hasJetpack) aname = 'oCapePickup';
2921           else if (n == 2 && !global.hasGloves) aname = 'oGloves';
2922           else if (n == 3 && !global.hasSpectacles) aname = 'oSpectacles';
2923           else if (n == 4 && !global.hasMitt) aname = 'oMitt';
2924           else if (n == 5 && !global.hasSpringShoes) aname = 'oSpringShoes';
2925           else if (n == 6 && !global.hasSpikeShoes) aname = 'oSpikeShoes';
2926           else if (n == 7 && !global.hasStickyBombs) aname = 'oPaste';
2927           else if (n == 8 && !global.hasCompass) aname = 'oCompass';
2928           if (aname) {
2929             obj = MakeMapObject(sx, sy-8, aname);
2930             if (obj) break;
2931           }
2932           ++n;
2933           if (n > 8) n = 1;
2934           if (n == m) {
2935             obj = MakeMapObject(sx, sy-8, (global.hasJetpack ? 'oBombBox' : 'oJetpack'));
2936             break;
2937           }
2938         }
2939       }
2940     }
2941   } else if (global.favor > 0) {
2942     res = "SHE SEEMS PLEASED WITH YOU.";
2943   }
2945   /*
2946   if (argument1) {
2947     global.message = "";
2948     res = "KALI DEVOURS YOU!"; // sacrifice is player
2949   }
2950   */
2952   return res;
2956 void performSacrifice (MapObject what, MapTile where) {
2957   if (!what || !what.isInstanceAlive) return;
2958   MakeMapObject(what.ix+8, what.iy+8, 'oFlame');
2959   if (where) where.playSound('sndSmallExplode'); else what.playSound('sndSmallExplode');
2960   what.spillBlood(amount:3, forced:true);
2962   string msg = "KALI ACCEPTS THE SACRIFICE!";
2964   auto idol = ItemGoldIdol(what);
2965   if (idol) {
2966     ++stats.totalSacrifices;
2967          if (global.favor <= -8) msg = "SHE SEEMS VERY ANGRY WITH YOU!";
2968     else if (global.favor < 0) msg = "SHE SEEMS ANGRY WITH YOU.";
2969     else if (global.favor >= 0) {
2970       // find other side of the altar
2971       int sx = player.ix, sy = player.iy;
2972       auto altar = where;
2973       if (altar) {
2974         sx = altar.ix;
2975         sy = altar.iy;
2976         auto a2 = isCollisionAtPoint(altar.ix+18, altar.iy+8, &cbCollisionSacAltar);
2977         if (!a2) a2 = isCollisionAtPoint(altar.ix-8, altar.iy+8, &cbCollisionSacAltar);
2978         if (a2) { sx = a2.ix; sy = a2.iy; }
2979       }
2980       // poofs
2981       auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2982       obj.xVel = -1;
2983       obj.yVel = 0;
2984       obj = MakeMapObject(sx, sy-8, 'oPoof');
2985       obj.xVel = 1;
2986       obj.yVel = 0;
2987       // a gift
2988       obj = MakeMapObject(sx, sy-16-8, 'oMonkeyGold');
2989     }
2990     osdMessage(msg, 6.66);
2991     scrShake(10);
2992     idol.instanceRemove();
2993     return;
2994   }
2996   if (global.favor <= -8) {
2997     msg = "KALI DEVOURS THE SACRIFICE!";
2998   } else if (global.favor < 0) {
2999     global.favor += (what.status == MapObject::STUNNED ? what.favor : round(what.favor/2.0)); //k8: added `round()`
3000     if (what.favor > 0) what.favor = 0;
3001   } else {
3002     global.favor += (what.status == MapObject::STUNNED ? what.favor : round(what.favor/2.0)); //k8: added `round()`
3003   }
3005   /*!!
3006        if (myAltar.gift1 != "" and global.kaliGift == 0) scrGetKaliGift(myAltar.gift1);
3007   else if (myAltar.gift2 != "" and global.kaliGift == 1) scrGetKaliGift(myAltar.gift2);
3008   else scrGetKaliGift("");
3009   */
3011   // sacrifice is player?
3012   if (what isa PlayerPawn) {
3013     ++stats.totalSelfSacrifices;
3014     msg = "KALI DEVOURS YOU!";
3015     player.visible = false;
3016     player.removeBallAndChain(temp:true);
3017     player.dead = true;
3018     player.status = MapObject::DEAD;
3019   } else {
3020     ++stats.totalSacrifices;
3021     auto msg2 = scrGetKaliGift(where);
3022     what.instanceRemove();
3023     if (msg2) msg = va("%s\n%s", msg, msg2);
3024   }
3026   osdMessage(msg, 6.66);
3028   scrShake(10);
3032 // ////////////////////////////////////////////////////////////////////////// //
3033 final void addBackgroundGfxDetails () {
3034   // add background details
3035   //if (global.customLevel) return;
3036   foreach (; 0..20) {
3037     // bg = instance_create(16*randRoom(1, 42), 16*randRoom(1, 33), oCaveBG);
3038          if (global.levelType == 1 && global.randRoom(1, 3) < 3) MakeMapBackTile('bgExtrasLush', 32*global.randRoom(0, 1), 0, 32, 32, 16*global.randRoom(1, 42), 16*global.randRoom(1, 33), 10002);
3039     else if (global.levelType == 2 && global.randRoom(1, 3) < 3) MakeMapBackTile('bgExtrasIce', 32*global.randRoom(0, 1), 0, 32, 32, 16*global.randRoom(1, 42), 16*global.randRoom(1, 33), 10002);
3040     else if (global.levelType == 3 && global.randRoom(1, 3) < 3) MakeMapBackTile('bgExtrasTemple', 32*global.randRoom(0, 1), 0, 32, 32, 16*global.randRoom(1, 42), 16*global.randRoom(1, 33), 10002);
3041     else MakeMapBackTile('bgExtras', 32*global.randRoom(0, 1), 0, 32, 32, 16*global.randRoom(1, 42), 16*global.randRoom(1, 33), 10002);
3042   }
3046 // ////////////////////////////////////////////////////////////////////////// //
3047 private final void fixRealViewStart () {
3048   int scale = global.scale;
3049   realViewStart.x = clamp(realViewStart.x, viewMin.x*scale, viewMax.x*scale-viewWidth);
3050   realViewStart.y = clamp(realViewStart.y, viewMin.y*scale, viewMax.y*scale-viewHeight);
3054 final int cameraCurrX () { return realViewStart.x/global.scale; }
3055 final int cameraCurrY () { return realViewStart.y/global.scale; }
3058 private final void fixViewStart () {
3059   int scale = global.scale;
3060   viewStart.x = clamp(viewStart.x, viewMin.x*scale, viewMax.x*scale-viewWidth);
3061   viewStart.y = clamp(viewStart.y, viewMin.y*scale, viewMax.y*scale-viewHeight);
3065 final void centerViewAtPlayer () {
3066   if (viewWidth < 1 || viewHeight < 1 || !player) return;
3067   centerViewAt(player.xCenter, player.yCenter);
3071 final void centerViewAt (int x, int y) {
3072   if (viewWidth < 1 || viewHeight < 1) return;
3074   cameraSlideToSpeed.x = 0;
3075   cameraSlideToSpeed.y = 0;
3076   cameraSlideToPlayer = 0;
3078   int scale = global.scale;
3079   x *= scale;
3080   y *= scale;
3081   realViewStart.x = clamp(x-viewWidth/2, 0, tilesWidth*16*scale-viewWidth);
3082   realViewStart.y = clamp(y-viewHeight/2, 0, tilesHeight*16*scale-viewHeight);
3083   fixRealViewStart();
3085   viewStart.x = realViewStart.x;
3086   viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
3087   fixViewStart();
3089   if (onCameraTeleported) onCameraTeleported();
3093 const int ViewPortToleranceX = 16*1+8;
3094 const int ViewPortToleranceY = 16*1+8;
3096 final void fixCamera () {
3097   if (!player) return;
3098   if (viewWidth < 1 || viewHeight < 1) return;
3099   int scale = global.scale;
3100   auto alwaysCenterX = global.config.alwaysCenterPlayer;
3101   auto alwaysCenterY = alwaysCenterX;
3102   // calculate offset from viewport center (in game units), and fix viewport
3104   int camDestX = player.ix+8;
3105   int camDestY = player.iy+8;
3106   if (!cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y) != 0) {
3107     // slide camera to point
3108     if (cameraSlideToSpeed.x) camDestX = cameraSlideToCurr.x;
3109     if (cameraSlideToSpeed.y) camDestY = cameraSlideToCurr.y;
3110     int dx = cameraSlideToDest.x-camDestX;
3111     int dy = cameraSlideToDest.y-camDestY;
3112     //writeln("speed:(", cameraSlideToSpeed.x, ",", cameraSlideToSpeed.y, "); cur:(", camDestX, ",", camDestY, "); dest:(", cameraSlideToDest.x, ",", cameraSlideToDest.y, "); delta=(", dx, ",", dy, ")");
3113     if (dx && cameraSlideToSpeed.x != 0) {
3114       alwaysCenterX = true;
3115       if (abs(dx) <= abs(cameraSlideToSpeed.x)) {
3116         camDestX = cameraSlideToDest.x;
3117       } else {
3118         camDestX += sign(dx)*abs(cameraSlideToSpeed.x);
3119       }
3120     }
3121     if (dy && abs(cameraSlideToSpeed.y) != 0) {
3122       alwaysCenterY = true;
3123       if (abs(dy) <= cameraSlideToSpeed.y) {
3124         camDestY = cameraSlideToDest.y;
3125       } else {
3126         camDestY += sign(dy)*abs(cameraSlideToSpeed.y);
3127       }
3128     }
3129     //writeln("  new:(", camDestX, ",", camDestY, ")");
3130     if (cameraSlideToSpeed.x) cameraSlideToCurr.x = camDestX;
3131     if (cameraSlideToSpeed.y) cameraSlideToCurr.y = camDestY;
3132   }
3134   // horizontal
3135   if (cameraSlideToSpeed.x && !cameraSlideToPlayer) {
3136     realViewStart.x = clamp(camDestX*scale, 0, tilesWidth*16*scale-viewWidth);
3137   } else if (!player.cameraBlockX) {
3138     int x = camDestX*scale;
3139     int cx = realViewStart.x;
3140     if (alwaysCenterX) {
3141       cx = x-viewWidth/2;
3142     } else {
3143       int xofs = x-(cx+viewWidth/2);
3144            if (xofs < -ViewPortToleranceX*scale) cx += xofs+ViewPortToleranceX*scale;
3145       else if (xofs > ViewPortToleranceX*scale) cx += xofs-ViewPortToleranceX*scale;
3146     }
3147     // slide back to player?
3148     if (cameraSlideToPlayer && cameraSlideToSpeed.x) {
3149       int prevx = cameraSlideToCurr.x*scale;
3150       int dx = (cx-prevx)/scale;
3151       if (abs(dx) <= cameraSlideToSpeed.x) {
3152         writeln("BACKSLIDE X COMPLETE!");
3153         cameraSlideToSpeed.x = 0;
3154       } else {
3155         cx = prevx+(sign(dx)*cameraSlideToSpeed.x)*scale;
3156         cameraSlideToCurr.x += sign(dx)*cameraSlideToSpeed.x;
3157         if ((dx < 0 && cx <= 0) || (dx > 0 && cx >= tilesWidth*16*scale-viewWidth)) {
3158           writeln("BACKSLIDE X COMPLETE!");
3159           cameraSlideToSpeed.x = 0;
3160         }
3161       }
3162     }
3163     realViewStart.x = clamp(cx, 0, tilesWidth*16*scale-viewWidth);
3164   }
3166   // vertical
3167   if (cameraSlideToSpeed.y && !cameraSlideToPlayer) {
3168     realViewStart.y = clamp(camDestY*scale, 0, tilesHeight*16*scale-viewHeight);
3169   } else if (!player.cameraBlockY) {
3170     int y = camDestY*scale;
3171     int cy = realViewStart.y;
3172     if (alwaysCenterY) {
3173       cy = y-viewHeight/2;
3174     } else {
3175       int yofs = y-(cy+viewHeight/2);
3176            if (yofs < -ViewPortToleranceY*scale) cy += yofs+ViewPortToleranceY*scale;
3177       else if (yofs > ViewPortToleranceY*scale) cy += yofs-ViewPortToleranceY*scale;
3178     }
3179     // slide back to player?
3180     if (cameraSlideToPlayer && cameraSlideToSpeed.y) {
3181       int prevy = cameraSlideToCurr.y*scale;
3182       int dy = (cy-prevy)/scale;
3183       if (abs(dy) <= cameraSlideToSpeed.y) {
3184         writeln("BACKSLIDE Y COMPLETE!");
3185         cameraSlideToSpeed.y = 0;
3186       } else {
3187         cy = prevy+(sign(dy)*cameraSlideToSpeed.y)*scale;
3188         cameraSlideToCurr.y += sign(dy)*cameraSlideToSpeed.y;
3189         if ((dy < 0 && cy <= 0) || (dy > 0 && cy >= tilesHeight*16*scale-viewHeight)) {
3190           writeln("BACKSLIDE Y COMPLETE!");
3191           cameraSlideToSpeed.y = 0;
3192         }
3193       }
3194     }
3195     realViewStart.y = clamp(cy, 0, tilesHeight*16*scale-viewHeight);
3196   }
3198   if (cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y) == 0) cameraSlideToPlayer = 0;
3200   fixRealViewStart();
3201   //writeln("  new2:(", cameraCurrX, ",", cameraCurrY, ")");
3203   viewStart.x = realViewStart.x;
3204   viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
3205   fixViewStart();
3209 // ////////////////////////////////////////////////////////////////////////// //
3210 // x0 and y0 are non-scaled (and will be scaled)
3211 final void drawSpriteAt (name sprName, float frnumf, int x0, int y0, optional bool small) {
3212   if (!sprName) return;
3213   auto spr = sprStore[sprName];
3214   if (!spr || !spr.frames.length) return;
3215   int scale = global.scale;
3216   x0 *= scale;
3217   y0 *= scale;
3218   int frnum = max(0, trunc(frnumf))%spr.frames.length;
3219   auto sfr = spr.frames[frnum];
3220   int sx0 = x0-sfr.xofs*scale;
3221   int sy0 = y0-sfr.yofs*scale;
3222   if (small && scale > 1) {
3223     sfr.tex.blitExt(sx0, sy0, round(sx0+sfr.tex.width*(scale/2.0)), round(sy0+sfr.tex.height*(scale/2.0)), 0, 0);
3224   } else {
3225     sfr.tex.blitAt(sx0, sy0, scale);
3226   }
3230 final void drawSpriteAtS3 (name sprName, float frnumf, int x0, int y0) {
3231   if (!sprName) return;
3232   auto spr = sprStore[sprName];
3233   if (!spr || !spr.frames.length) return;
3234   x0 *= 3;
3235   y0 *= 3;
3236   int frnum = max(0, trunc(frnumf))%spr.frames.length;
3237   auto sfr = spr.frames[frnum];
3238   int sx0 = x0-sfr.xofs*3;
3239   int sy0 = y0-sfr.yofs*3;
3240   sfr.tex.blitAt(sx0, sy0, 3);
3244 // x0 and y0 are non-scaled (and will be scaled)
3245 final void drawTextAt (int x0, int y0, string text, optional int scale, optional int hiColor1, optional int hiColor2) {
3246   if (!text) return;
3247   if (!specified_scale) scale = global.scale;
3248   x0 *= scale;
3249   y0 *= scale;
3250   sprStore.renderTextWithHighlight(x0, y0, text, scale, hiColor1!optional, hiColor2!optional);
3254 void renderCompass (float currFrameDelta) {
3255   if (!global.hasCompass) return;
3257   /*
3258   if (isRoom("rOlmec")) {
3259     global.exitX = 648;
3260     global.exitY = 552;
3261   } else if (isRoom("rOlmec2")) {
3262     global.exitX = 648;
3263     global.exitY = 424;
3264   }
3265   */
3267   bool hasMessage = osdHasMessage();
3268   foreach (MapTile et; allExits) {
3269     // original compass
3270     int exitX = et.ix, exitY = et.iy;
3271     int vx0 = viewStart.x/global.scale, vy0 = viewStart.y/global.scale;
3272     int vx1 = (viewStart.x+viewWidth)/global.scale;
3273     int vy1 = (viewStart.y+viewHeight)/global.scale;
3274     if (exitY > vy1-16) {
3275       if (exitX < vx0) {
3276         drawSpriteAt(hasMessage ? 'sCompassSmallLL' : 'sCompassLL', 0, 0, 224);
3277       } else if (exitX > vx1-16) {
3278         drawSpriteAt(hasMessage ? 'sCompassSmallLR' : 'sCompassLR', 0, 304, 224);
3279       } else {
3280         drawSpriteAt(hasMessage ? 'sCompassSmallDown' : 'sCompassDown', 0, exitX-vx0, 224);
3281       }
3282     } else if (exitX < vx0) {
3283       drawSpriteAt(hasMessage ? 'sCompassSmallLeft' : 'sCompassLeft', 0, 0, exitY-vy0);
3284     } else if (exitX > vx1-16) {
3285       drawSpriteAt(hasMessage ? 'sCompassSmallRight' : 'sCompassRight', 0, 304, exitY-vy0);
3286     }
3287     break; // only the first exit
3288   }
3292 final bool totalsNameCmpCB (ref GameStats::TotalItem a, ref GameStats::TotalItem b) {
3293   auto sa = string(a.objName);
3294   auto sb = string(b.objName);
3295   return (sa < sb);
3298 void renderTransitionInfo (float currFrameDelta) {
3299   //FIXME!
3300   /*
3301   GameStats.sortTotalsList(stats.kills, &totalsNameCmpCB);
3303   int maxLen = 0;
3304   foreach (int idx, ref auto k; stats.kills) {
3305     string s = string(k);
3306     maxLen = max(maxLen, s.length);
3307   }
3308   maxLen *= 8;
3310   sprStore.loadFont('sFontSmall');
3311   Video.color = 0xff_ff_00;
3312   foreach (int idx, ref auto k; stats.kills) {
3313     int deaths = 0;
3314     foreach (int xidx, ref auto d; stats.totalKills) {
3315       if (d.objName == k) { deaths = d.count; break; }
3316     }
3317     //drawTextAt(16, 4+idx*8, va("%s: %d (%d)", string(k.objName).toUpperCase, k.count, deaths));
3318     drawTextAt(16, 4+idx*8, string(k).toUpperCase);
3319     drawTextAt(16+maxLen, 4+idx*8, va(": %d (%d)", k.count, deaths));
3320   }
3321   */
3325 void renderGhostTimer (float currFrameDelta) {
3326   if (ghostTimeLeft <= 0) return;
3327   //ghostTimeLeft /= 30; // frames -> seconds
3329   int hgt = viewHeight-64;
3330   if (hgt < 1) return;
3331   int rhgt = round(float(hgt)*(float(ghostTimeLeft)/30.0)/1000.0);
3332   //writeln("hgt=", hgt, "; rhgt=", rhgt, "; gtl=", ghostTimeLeft/30);
3333   if (rhgt > 0) {
3334     auto oclr = Video.color;
3335     Video.color = 0xcf_ff_7f_00;
3336     Video.fillRect(viewWidth-20, 32, 16, hgt-rhgt);
3337     Video.color = 0x7f_ff_7f_00;
3338     Video.fillRect(viewWidth-20, 32+(hgt-rhgt), 16, rhgt);
3339     Video.color = oclr;
3340   }
3344 void renderStarsHUD (float currFrameDelta) {
3345   bool scumSmallHud = global.config.scumSmallHud;
3347   //auto life = max(0, global.plife);
3348   int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3349   int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3350   //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3352   int hhup;
3354   if (scumSmallHud) {
3355     sprStore.loadFont('sFontSmall');
3356     hhup = 6;
3357   } else {
3358     sprStore.loadFont('sFont');
3359     hhup = 2;
3360   }
3362   Video.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3363   //draw_sprite(sHeart, -1, view_xview[0]+8, view_yview[0]+8);
3364   //draw_text(view_xview[0]+24, view_yview[0]+8, life);
3365   if (scumSmallHud) {
3366     if (global.plife == 1) {
3367       drawSpriteAt('sHeartSmallBlink', global.heartBlink, 10, hhup-4);
3368       global.heartBlink += 0.1;
3369       if (global.heartBlink > 3) global.heartBlink = 0;
3370     } else {
3371       drawSpriteAt('sHeartSmall', -1, 10, hhup-4);
3372       global.heartBlink = 0;
3373     }
3374   } else {
3375     if (global.plife == 1) {
3376       drawSpriteAt('sHeartBlink', global.heartBlink, 8, hhup);
3377       global.heartBlink += 0.1;
3378       if (global.heartBlink > 3) global.heartBlink = 0;
3379     } else {
3380       drawSpriteAt('sHeart', -1, 8, hhup);
3381       global.heartBlink = 0;
3382     }
3383   }
3384   int life = clamp(global.plife, 0, 99);
3385   drawTextAt(16+8, hhup, va("%d", life));
3387   //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3388   drawSpriteAt('sShopkeeperIcon', -1, 64, hhup, scumSmallHud);
3389   drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", starsKills));
3391   if (starsRoomTimer1 > 0) {
3392     sprStore.loadFont('sFontSmall');
3393     Video.color = 0xff_ff_00;
3394     int scale = global.scale;
3395     sprStore.renderMultilineTextCentered(viewWidth/2, 216*scale, va("SHOTGUN CHALLENGE BEGINS IN ~%d~", (starsRoomTimer1/30)+1), scale, 0xff_00_00);
3396   }
3400 void renderSunHUD (float currFrameDelta) {
3401   bool scumSmallHud = global.config.scumSmallHud;
3403   //auto life = max(0, global.plife);
3404   int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3405   int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3406   //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3408   int hhup;
3410   if (scumSmallHud) {
3411     sprStore.loadFont('sFontSmall');
3412     hhup = 6;
3413   } else {
3414     sprStore.loadFont('sFont');
3415     hhup = 2;
3416   }
3418   Video.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3419   //draw_sprite(sHeart, -1, view_xview[0]+8, view_yview[0]+8);
3420   //draw_text(view_xview[0]+24, view_yview[0]+8, life);
3421   if (scumSmallHud) {
3422     if (global.plife == 1) {
3423       drawSpriteAt('sHeartSmallBlink', global.heartBlink, 10, hhup-4);
3424       global.heartBlink += 0.1;
3425       if (global.heartBlink > 3) global.heartBlink = 0;
3426     } else {
3427       drawSpriteAt('sHeartSmall', -1, 10, hhup-4);
3428       global.heartBlink = 0;
3429     }
3430   } else {
3431     if (global.plife == 1) {
3432       drawSpriteAt('sHeartBlink', global.heartBlink, 8, hhup);
3433       global.heartBlink += 0.1;
3434       if (global.heartBlink > 3) global.heartBlink = 0;
3435     } else {
3436       drawSpriteAt('sHeart', -1, 8, hhup);
3437       global.heartBlink = 0;
3438     }
3439   }
3440   int life = clamp(global.plife, 0, 99);
3441   drawTextAt(16+8, hhup, va("%d", life));
3443   //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3444   drawSpriteAt('sDamselIcon', -1, 64, hhup, scumSmallHud);
3445   drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", sunScore));
3447   if (sunRoomTimer1 > 0) {
3448     sprStore.loadFont('sFontSmall');
3449     Video.color = 0xff_ff_00;
3450     int scale = global.scale;
3451     sprStore.renderMultilineTextCentered(viewWidth/2, 216*scale, va("DAMSEL CHALLENGE BEGINS IN ~%d~", (sunRoomTimer1/30)+1), scale, 0xff_00_00);
3452   }
3456 void renderMoonHUD (float currFrameDelta) {
3457   bool scumSmallHud = global.config.scumSmallHud;
3459   //auto life = max(0, global.plife);
3460   int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3461   int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3462   //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3464   int hhup;
3466   if (scumSmallHud) {
3467     sprStore.loadFont('sFontSmall');
3468     hhup = 6;
3469   } else {
3470     sprStore.loadFont('sFont');
3471     hhup = 2;
3472   }
3474   Video.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3476   //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3477   drawSpriteAt('sHoopsIcon', -1, 8, hhup, scumSmallHud);
3478   drawTextAt(8+16-(scumSmallHud ? 6 : 0), hhup, va("%d", moonScore));
3479   drawSpriteAt('sTimerIcon', -1, 64, hhup, scumSmallHud);
3480   drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", max(0, moonTimer)));
3482   if (moonRoomTimer1 > 0) {
3483     sprStore.loadFont('sFontSmall');
3484     Video.color = 0xff_ff_00;
3485     int scale = global.scale;
3486     sprStore.renderMultilineTextCentered(viewWidth/2, 216*scale, va("ARCHERY CHALLENGE BEGINS IN ~%d~", (moonRoomTimer1/30)+1), scale, 0xff_00_00);
3487   }
3491 void renderHUD (float currFrameDelta) {
3492   if (levelKind == LevelKind.Stars) { renderStarsHUD(currFrameDelta); return; }
3493   if (levelKind == LevelKind.Sun) { renderSunHUD(currFrameDelta); return; }
3494   if (levelKind == LevelKind.Moon) { renderMoonHUD(currFrameDelta); return; }
3496   if (!isHUDEnabled()) return;
3498   if (levelKind == LevelKind.Transition) { renderTransitionInfo(currFrameDelta); return; }
3500   int lifeX = 4; // 8
3501   int bombX = 56;
3502   int ropeX = 104;
3503   int ammoX = 152;
3504   int moneyX = 200;
3505   int hhup;
3506   bool scumSmallHud = global.config.scumSmallHud;
3507   if (!global.config.optSGAmmo) moneyX = ammoX;
3509   if (scumSmallHud) {
3510     sprStore.loadFont('sFontSmall');
3511     hhup = 6;
3512   } else {
3513     sprStore.loadFont('sFont');
3514     hhup = 0;
3515   }
3516   //int alpha = 0x6f_00_00_00;
3517   int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3518   int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3520   //Video.color = 0xff_ff_ff;
3521   Video.color = 0xff_ff_ff|talpha;
3523   // hearts
3524   if (scumSmallHud) {
3525     if (global.plife == 1) {
3526       drawSpriteAt('sHeartSmallBlink', global.heartBlink, lifeX+2, 4-hhup);
3527       global.heartBlink += 0.1;
3528       if (global.heartBlink > 3) global.heartBlink = 0;
3529     } else {
3530       drawSpriteAt('sHeartSmall', -1, lifeX+2, 4-hhup);
3531       global.heartBlink = 0;
3532     }
3533   } else {
3534     if (global.plife == 1) {
3535       drawSpriteAt('sHeartBlink', global.heartBlink, lifeX, 8-hhup);
3536       global.heartBlink += 0.1;
3537       if (global.heartBlink > 3) global.heartBlink = 0;
3538     } else {
3539       drawSpriteAt('sHeart', -1, lifeX, 8-hhup);
3540       global.heartBlink = 0;
3541     }
3542   }
3544   int life = clamp(global.plife, 0, 99);
3545   //if (!scumHud && life > 99) life = 99;
3546   drawTextAt(lifeX+16, 8-hhup, va("%d", life));
3548   // bombs
3549   if (global.hasStickyBombs && global.stickyBombsActive) {
3550     if (scumSmallHud) drawSpriteAt('sStickyBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sStickyBombIcon', -1, bombX, 8-hhup);
3551   } else {
3552     if (scumSmallHud) drawSpriteAt('sBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sBombIcon', -1, bombX, 8-hhup);
3553   }
3554   int n = global.bombs;
3555   if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3556   drawTextAt(bombX+16, 8-hhup, va("%d", n));
3558   // ropes
3559   if (scumSmallHud) drawSpriteAt('sRopeIconSmall', -1, ropeX+2, 4-hhup); else drawSpriteAt('sRopeIcon', -1, ropeX, 8-hhup);
3560   n = global.rope;
3561   if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3562   drawTextAt(ropeX+16, 8-hhup, va("%d", n));
3564   // shotgun shells
3565   if (global.config.optSGAmmo && (!player || player.holdItem !isa ItemWeaponBow)) {
3566     if (scumSmallHud) drawSpriteAt('sShellsIcon', -1, ammoX+6, 8-hhup); else drawSpriteAt('sShellsIcon', -1, ammoX+7, 12-hhup);
3567     n = global.sgammo;
3568     if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3569     drawTextAt(ammoX+16, 8-hhup, va("%d", n));
3570   } else if (player && player.holdItem isa ItemWeaponBow) {
3571     if (scumSmallHud) drawSpriteAt('sArrowRight', -1, ammoX+6, 8-hhup); else drawSpriteAt('sArrowRight', -1, ammoX+7, 12-hhup);
3572     n = global.arrows;
3573     if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3574     drawTextAt(ammoX+16, 8-hhup, va("%d", n));
3575   }
3577   // money
3578   if (scumSmallHud) drawSpriteAt('sDollarSignSmall', -1, moneyX+2, 3-hhup); else drawSpriteAt('sDollarSign', -1, moneyX, 8-hhup);
3579   drawTextAt(moneyX+16, 8-hhup, va("%d", stats.money-xmoney));
3581   // items
3582   Video.color = 0xff_ff_ff|ialpha;
3584   int ity = (scumSmallHud ? 18-hhup : 24-hhup);
3586   n = 8; //28;
3587   if (global.hasUdjatEye) {
3588     if (global.udjatBlink) drawSpriteAt('sUdjatEyeIcon2', -1, n, ity); else drawSpriteAt('sUdjatEyeIcon', -1, n, ity);
3589     n += 20;
3590   }
3591   if (global.hasAnkh) { drawSpriteAt('sAnkhIcon', -1, n, ity); n += 20; }
3592   if (global.hasCrown) { drawSpriteAt('sCrownIcon', -1, n, ity); n += 20; }
3593   if (global.hasKapala) {
3594          if (global.bloodLevel == 0) drawSpriteAt('sKapalaIcon', 0, n, ity);
3595     else if (global.bloodLevel <= 2) drawSpriteAt('sKapalaIcon', 1, n, ity);
3596     else if (global.bloodLevel <= 4) drawSpriteAt('sKapalaIcon', 2, n, ity);
3597     else if (global.bloodLevel <= 6) drawSpriteAt('sKapalaIcon', 3, n, ity);
3598     else if (global.bloodLevel <= 8) drawSpriteAt('sKapalaIcon', 4, n, ity);
3599     n += 20;
3600   }
3601   if (global.hasSpectacles) { drawSpriteAt('sSpectaclesIcon', -1, n, ity); n += 20; }
3602   if (global.hasGloves) { drawSpriteAt('sGlovesIcon', -1, n, ity); n += 20; }
3603   if (global.hasMitt) { drawSpriteAt('sMittIcon', -1, n, ity); n += 20; }
3604   if (global.hasSpringShoes) { drawSpriteAt('sSpringShoesIcon', -1, n, ity); n += 20; }
3605   if (global.hasJordans) { drawSpriteAt('sJordansIcon', -1, n, ity); n += 20; }
3606   if (global.hasSpikeShoes) { drawSpriteAt('sSpikeShoesIcon', -1, n, ity); n += 20; }
3607   if (global.hasCape) { drawSpriteAt('sCapeIcon', -1, n, ity); n += 20; }
3608   if (global.hasJetpack) { drawSpriteAt('sJetpackIcon', -1, n, ity); n += 20; }
3609   if (global.hasStickyBombs) { drawSpriteAt('sPasteIcon', -1, n, ity); n += 20; }
3610   if (global.hasCompass) { drawSpriteAt('sCompassIcon', -1, n, ity); n += 20; }
3611   if (global.hasParachute) { drawSpriteAt('sParachuteIcon', -1, n, ity); n += 20; }
3613   if (player.holdItem && player.holdItem.objName == 'Bow' && global.arrows > 0) {
3614     int m = 1;
3615     float malpha = 1;
3616     while (m <= global.arrows && m <= 20 && malpha > 0) {
3617       Video.color = trunc(malpha*255)<<24|0xff_ff_ff;
3618       drawSpriteAt('sArrowIcon', -1, n, ity);
3619       n += 4;
3620       if (m > 10) malpha -= 0.1; //and global.arrows > 20 malpha -= 0.1
3621       m += 1;
3622     }
3623   }
3625   if (xmoney > 0) {
3626     sprStore.loadFont('sFontSmall');
3627     Video.color = 0xff_ff_00|talpha;
3628     if (scumSmallHud) drawTextAt(moneyX+8, 8+8-hhup, va("+%d", xmoney));
3629     else drawTextAt(moneyX, 8+16-hhup, va("+%d", xmoney));
3630   }
3632   Video.color = 0xff_ff_ff;
3633   if (global.config.ghostShowTime) renderGhostTimer(currFrameDelta);
3637 // ////////////////////////////////////////////////////////////////////////// //
3638 // x0 and y0 are non-scaled (and will be scaled)
3639 final void drawTextAtS3 (int x0, int y0, string text, optional int hiColor1, optional int hiColor2) {
3640   if (!text) return;
3641   x0 *= 3;
3642   y0 *= 3;
3643   sprStore.renderTextWithHighlight(x0, y0, text, 3, hiColor1!optional, hiColor2!optional);
3647 final void drawTextAtS3Centered (int y0, string text, optional int hiColor1, optional int hiColor2) {
3648   if (!text) return;
3649   int x0 = (viewWidth-sprStore.getTextWidth(text, 3, specified_hiColor1, specified_hiColor2))/2;
3650   sprStore.renderTextWithHighlight(x0, y0*3, text, 3, hiColor1!optional, hiColor2!optional);
3654 void renderHelpOverlay () {
3655   Video.color = 0;
3656   Video.fillRect(0, 0, viewWidth, viewHeight);
3658   int tx = 16;
3659   int txoff = 0; // text x pos offset (for multi-color lines)
3660   int ty = 8;
3661   if (gameHelpScreen) {
3662     sprStore.loadFont('sFontSmall');
3663     Video.color = 0xff_ff_ff;
3664     drawTextAtS3Centered(ty, va("HELP (PAGE ~%d~ OF ~%d~)", gameHelpScreen, MaxGameHelpScreen), 0xff_ff_00);
3665     ty += 24;
3666   }
3668   if (gameHelpScreen == 1) {
3669     sprStore.loadFont('sFontSmall');
3670     Video.color = 0xff_ff_00; drawTextAtS3(tx, ty, "INVENTORY BASICS"); ty += 16;
3671     Video.color = 0xff_ff_ff;
3672     drawTextAtS3(tx, ty, global.expandString("Press $SWITCH to cycle through items."), 0x00_ff_00);
3673     ty += 8;
3674     ty += 56;
3675     Video.color = 0xff_ff_ff;
3676     drawSpriteAtS3('sHelpSprite1', -1, 64, 96);
3677   } else if (gameHelpScreen == 2) {
3678     sprStore.loadFont('sFontSmall');
3679     Video.color = 0xff_ff_00;
3680     drawTextAtS3(tx, ty, "SELLING TO SHOPKEEPERS"); ty += 16;
3681     Video.color = 0xff_ff_ff;
3682     drawTextAtS3(tx, ty, global.expandString("Press $PAY to offer your currently"), 0x00_ff_00); ty += 8;
3683     drawTextAtS3(tx, ty, "held item to the shopkeeper."); ty += 16;
3684     drawTextAtS3(tx, ty, "If the shopkeeper is interested, "); ty += 8;
3685     //drawTextAtS3(tx, ty, global.expandString("press $PAY again to complete the sale."), 0x00_ff_00); ty += 72;
3686     drawTextAtS3(tx, ty, global.expandString("press $PAY again to complete"), 0x00_ff_00);
3687     drawTextAtS3(tx, ty+8, "the sale.");
3688     ty += 72;
3689     drawSpriteAtS3('sHelpSell', -1, 112, 100);
3690     drawTextAtS3(tx, ty, "Purchasing goods from the shopkeeper"); ty += 8;
3691     drawTextAtS3(tx, ty, "will increase the funds he has"); ty += 8;
3692     drawTextAtS3(tx, ty, "available to buy your unwanted stuff."); ty += 8;
3693   } else {
3694     // map
3695     sprStore.loadFont('sFont');
3696     Video.color = 0xff_ff_ff;
3697     drawTextAtS3(136, 8, "MAP");
3699     if (lg.mapSprite && (isNormalLevel() || isTransitionRoom())) {
3700       Video.color = 0xff_ff_00;
3701       drawTextAtS3Centered(24, lg.mapTitle);
3703       auto spf = sprStore[lg.mapSprite].frames[0];
3704       int mapX = 160-spf.width/2;
3705       int mapY = 120-spf.height/2;
3706       //mapTitleX = 160-string_length(global.mapTitle)*8/2;
3708       Video.color = 0xff_ff_ff;
3709       drawSpriteAtS3(lg.mapSprite, -1, mapX, mapY);
3711       if (lg.mapSprite != 'sMapDefault') {
3712         int mx = -1, my = -1;
3714         // set position of player icon
3715         switch (global.currLevel) {
3716           case 1: mx = 81; my = 22; break;
3717           case 2: mx = 113; my = 63; break;
3718           case 3: mx = 197; my = 86; break;
3719           case 4: mx = 133; my = 109; break;
3720           case 5: mx = 181; my = 22; break;
3721           case 6: mx = 126; my = 64; break;
3722           case 7: mx = 158; my = 112; break;
3723           case 8: mx = 66; my = 80; break;
3724           case 9: mx = 30; my = 26; break;
3725           case 10: mx = 88; my = 54; break;
3726           case 11: mx = 148; my = 81; break;
3727           case 12: mx = 210; my = 205; break;
3728           case 13: mx = 66; my = 17; break;
3729           case 14: mx = 146; my = 17; break;
3730           case 15: mx = 82; my = 77; break;
3731           case 16: mx = 178; my = 81; break;
3732         }
3734         if (mx >= 0) {
3735           int plrx = mx+player.ix/16;
3736           int plry = my+player.iy/16;
3737           if (isTransitionRoom()) { plrx = mx+20; plry = my+16; }
3738           name plrspr = 'sMapSpelunker';
3739                if (global.isDamsel) plrspr = 'sMapDamsel';
3740           else if (global.isTunnelMan) plrspr = 'sMapTunnel';
3741           auto ss = sprStore[plrspr];
3742           drawSpriteAtS3(plrspr, (pausedTime/2)%ss.frames.length, mapX+plrx, mapY+plry);
3743           // exit door icon
3744           if (global.hasCompass && allExits.length) {
3745             drawSpriteAtS3('sMapRedDot', -1, mapX+mx+allExits[0].ix/16, mapY+my+allExits[0].iy/16);
3746           }
3747         }
3748       }
3749     }
3750   }
3752   sprStore.loadFont('sFontSmall');
3753   Video.color = 0xff_ff_00;
3754   drawTextAtS3Centered(232, "PRESS ~SPACE~/~LEFT~/~RIGHT~ TO CHANGE PAGE", 0x00_ff_00);
3756   Video.color = 0xff_ff_ff;
3760 void renderPauseOverlay () {
3761   //drawTextAt(256, 432, "PAUSED", scale);
3763   if (gameShowHelp) { renderHelpOverlay(); return; }
3765   Video.color = 0xff_ff_00;
3766   //int hiColor = 0x00_ff_00;
3768   int n = 120;
3769   if (isTutorialRoom()) {
3770     sprStore.loadFont('sFont');
3771     drawTextAtS3(40, n-24, "TUTORIAL CAVE");
3772   } else if (isNormalLevel()) {
3773     sprStore.loadFont('sFont');
3775     drawTextAtS3Centered(n-32, va("LEVEL ~%d~", global.currLevel), 0x00_ff_00);
3777     sprStore.loadFont('sFontSmall');
3779     int depth = round((174.8*(global.currLevel-1)+(player.iy+8)*0.34)*(global.config.scumMetric ? 0.3048 : 1.0)*10);
3780     string depthStr = va("DEPTH: ~%d.%d~ %s", depth/10, depth%10, (global.config.scumMetric ? "METRES" : "FEET"));
3781     drawTextAtS3Centered(n-16, depthStr, 0x00_ff_00);
3783     n += 16;
3784     drawTextAtS3Centered(n, va("MONEY: ~%d~", stats.money), 0x00_ff_00);
3785     drawTextAtS3Centered(n+16, va("KILLS: ~%d~", stats.countKills), 0x00_ff_00);
3786     drawTextAtS3Centered(n+32, va("SAVES: ~%d~", stats.damselsSaved), 0x00_ff_00);
3787     drawTextAtS3Centered(n+48, va("TIME: ~%s~", time2str(time/30)), 0x00_ff_00);
3788     drawTextAtS3Centered(n+64, va("LEVEL TIME: ~%s~", time2str((time-levelStartTime)/30)), 0x00_ff_00);
3789   }
3791   sprStore.loadFont('sFontSmall');
3792   Video.color = 0xff_ff_ff;
3793   drawTextAtS3Centered(240-2-8, "~ESC~-RETURN  ~F10~-QUIT  ~CTRL+DEL~-SUICIDE", 0xff_7f_00);
3794   drawTextAtS3Centered(2, "~O~PTIONS  REDEFINE ~K~EYS  ~S~TATISTICS", 0xff_7f_00);
3798 // ////////////////////////////////////////////////////////////////////////// //
3799 transient int drawLoot;
3800 transient int drawPosX, drawPosY;
3802 void resetTransitionOverlay () {
3803   drawLoot = 0;
3804   drawPosX = 100;
3805   drawPosY = 83;
3809 // current game, uncollapsed
3810 struct LevelStatInfo {
3811   name aname;
3812   // for transition screen
3813   bool render;
3814   int x, y;
3819 void thinkFrameTransition () {
3820   if (drawLoot == 0) {
3821     if (drawPosX > 272) {
3822       drawPosX = 100;
3823       drawPosY += 2;
3824       if (drawPosY > 83+4) drawPosY = 83;
3825     }
3826   } else if (drawPosX > 232) {
3827     drawPosX = 96;
3828     drawPosY += 2;
3829     if (drawPosY > 91+4) drawPosY = 91;
3830   }
3834 void renderTransitionOverlay () {
3835   sprStore.loadFont('sFontSmall');
3836   Video.color = 0xff_ff_00;
3837   //else if (global.currLevel-1 &lt; 1) draw_text(32, 48, "TUTORIAL CAVE COMPLETED!");
3838   //else draw_text(32, 48, "LEVEL "+string(global.currLevel-1)+" COMPLETED!");
3839   drawTextAt(32, 48, va("LEVEL ~%d~ COMPLETED!", global.currLevel), hiColor1:0x00_ff_ff);
3840   Video.color = 0xff_ff_ff;
3841   drawTextAt(32, 64, va("TIME  = ~%s~", time2str((levelEndTime-levelStartTime)/30)), hiColor1:0xff_ff_00);
3843   if (/*stats.collected.length == 0*/stats.money <= levelMoneyStart) {
3844     drawTextAt(32, 80, "LOOT  = ~NONE~", hiColor1:0xff_00_00);
3845   } else {
3846     drawTextAt(32, 80, va("LOOT  = ~%d~", stats.money-levelMoneyStart), hiColor1:0xff_ff_00);
3847   }
3849   if (stats.kills.length == 0) {
3850     drawTextAt(32, 96, "KILLS = ~NONE~", hiColor1:0x00_ff_00);
3851   } else {
3852     drawTextAt(32, 96, va("KILLS = ~%d~", stats.kills.length), hiColor1:0xff_ff_00);
3853   }
3855   drawTextAt(32, 112, va("MONEY = ~%d~", stats.money), hiColor1:0xff_ff_00);
3859 // ////////////////////////////////////////////////////////////////////////// //
3860 private transient array!MapEntity renderVisibleCids;
3861 private transient array!MapEntity renderVisibleLights;
3862 private transient array!MapTile renderFrontTiles; // normal, with fg
3864 final bool renderSortByDepth (MapEntity oa, MapEntity ob) {
3865   auto da = oa.depth, db = ob.depth;
3866   if (da == db) return (oa.objId < ob.objId);
3867   return (da < db);
3871 const int RenderEdgePixNormal = 64;
3872 const int RenderEdgePixLight = 256;
3874 #ifndef EXPERIMENTAL_RENDER_CACHE
3875 enum skipListCreation = false;
3876 #endif
3878 void renderWithOfs (int xofs, int yofs, float currFrameDelta) {
3879   int scale = global.scale;
3881   // don't touch framebuffer alpha
3882   Video.colorMask = Video::CMask.Colors;
3883   Video.color = 0xff_ff_ff;
3885   /*
3886   Video::ScissorRect scsave;
3887   bool doRestoreGL = false;
3889   if (viewOffsetX > 0 || viewOffsetY > 0) {
3890     doRestoreGL = true;
3891     Video.getScissor(scsave);
3892     Video.scissorCombine(viewOffsetX, viewOffsetY, viewWidth, viewHeight);
3893     Video.glPushMatrix();
3894     Video.glTranslate(viewOffsetX, viewOffsetY);
3895     //Video.glTranslate(-550, 0);
3896     //Video.glScale(1, 1);
3897   }
3898   */
3901   bool isDarkLevel = global.darkLevel;
3903   if (isDarkLevel) {
3904     switch (global.config.scumPlayerLit) {
3905       case 0: player.lightRadius = 0; break; // never
3906       case 1: // only in "scumDarkness"
3907         player.lightRadius = (global.config.scumDarkness >= 2 ? 96 : 32);
3908         break;
3909       case 2:
3910         player.lightRadius = 96;
3911         break;
3912     }
3913   }
3915   // render cave background
3916   if (levBGImg) {
3917     int tsz = 16*scale;
3918     int bgw = levBGImg.tex.width*scale;
3919     int bgh = levBGImg.tex.height*scale;
3920     int bgTW = (tilesWidth*tsz+bgw-1)/bgw;
3921     int bgTH = (tilesHeight*tsz+bgh-1)/bgh;
3922     int bgX0 = max(0, xofs/bgw);
3923     int bgY0 = max(0, yofs/bgh);
3924     int bgX1 = min(bgTW, (xofs+viewWidth+bgw-1)/bgw);
3925     int bgY1 = min(bgTH, (yofs+viewHeight+bgh-1)/bgh);
3926     foreach (int ty; bgY0..bgY1) {
3927       foreach (int tx; bgX0..bgX1) {
3928         int x0 = tx*bgw-xofs;
3929         int y0 = ty*bgh-yofs;
3930         levBGImg.tex.blitAt(x0, y0, scale);
3931       }
3932     }
3933   }
3935   int RenderEdgePix = (global.darkLevel ? RenderEdgePixLight : RenderEdgePixNormal);
3937   // render background tiles
3938   for (MapBackTile bt = backtiles; bt; bt = bt.next) {
3939     bt.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3940   }
3942   // collect visible special tiles
3943 #ifdef EXPERIMENTAL_RENDER_CACHE
3944   bool skipListCreation = (lastRenderTime == time && renderVisibleCids.length); //FIXME
3945 #endif
3947   if (!skipListCreation) {
3948     renderVisibleCids.clear();
3949     renderVisibleLights.clear();
3950     renderFrontTiles.clear();
3952     int endVX = xofs+viewWidth;
3953     int endVY = yofs+viewHeight;
3955     // add player
3956     //int cnt = 0;
3957     if (player && player.visible) renderVisibleCids[$] = player; // include player in render list
3959     //FIXME: drop lit objects which cannot affect visible area
3960     if (scale > 1) {
3961       // collect visible objects
3962       foreach (MapEntity o; objGrid.inRectPix(xofs/scale-RenderEdgePix, yofs/scale-RenderEdgePix, (viewWidth+scale-1)/scale+RenderEdgePix*2, (viewHeight+scale-1)/scale+RenderEdgePix*2, precise:false)) {
3963         if (!o.visible) continue;
3964         auto tile = MapTile(o);
3965         if (tile) {
3966           if (isDarkLevel && (o.lightRadius > 4 || tile.litWholeTile)) renderVisibleLights[$] = o;
3967           if (tile.invisible) continue;
3968           if (tile.bgfront) renderFrontTiles[$] = tile;
3969           if (tile.bgback) tile.drawWithOfsBack(xofs, yofs, scale, currFrameDelta);
3970         } else {
3971           if (isDarkLevel && o.lightRadius > 4) renderVisibleLights[$] = o;
3972         }
3973         // check if the object is really visible -- this will speed up later sorting
3974         int fx0, fy0, fx1, fy1;
3975         auto spf = o.getSpriteFrame(default, out fx0, out fy0, out fx1, out fy1);
3976         if (!spf) continue; // no sprite -- nothing to draw (no, really)
3977         int ix = o.ix, iy = o.iy;
3978         int x0 = (ix+fx0)*scale, y0 = (iy+fy0)*scale;
3979         int x1 = (ix+fx1)*scale, y1 = (iy+fy1)*scale;
3980         if (x1 <= xofs || y1 <= yofs || x0 >= endVX || y0 >= endVY) {
3981           //++cnt;
3982           continue;
3983         }
3984         renderVisibleCids[$] = o;
3985       }
3986     } else {
3987       foreach (MapEntity o; objGrid.allObjects()) {
3988         if (!o.visible) continue;
3989         auto tile = MapTile(o);
3990         if (tile) {
3991           if (isDarkLevel && (o.lightRadius > 4 || tile.litWholeTile)) renderVisibleLights[$] = o;
3992           if (tile.invisible) continue;
3993           if (tile.bgfront) renderFrontTiles[$] = tile;
3994           if (tile.bgback) tile.drawWithOfsBack(xofs, yofs, scale, currFrameDelta);
3995         } else {
3996           if (isDarkLevel && o.lightRadius > 4) renderVisibleLights[$] = o;
3997         }
3998         renderVisibleCids[$] = o;
3999       }
4000     }
4001     //writeln("::: ", cnt, " invisible objects dropped");
4003     renderVisibleCids.sort(&renderSortByDepth);
4004     lastRenderTime = time;
4005   }
4007   auto depth4Start = 0;
4008   foreach (auto xidx, MapEntity o; renderVisibleCids) {
4009     if (o.depth >= 4) {
4010       depth4Start = xidx;
4011       break;
4012     }
4013   }
4015   bool playerPowerupRendered = false;
4017   // render objects (part one: depth > 3)
4018   foreach (auto idx; depth4Start..renderVisibleCids.length; reverse) {
4019     MapEntity o = renderVisibleCids[idx];
4020     // 1000 is an ordinary tile
4021     if (!playerPowerupRendered && o.depth <= 1200) {
4022       playerPowerupRendered = true;
4023       // so ducking player will have it's cape correctly rendered
4024       if (player.visible) player.drawPrePrePowerupWithOfs(xofs, yofs, scale, currFrameDelta);
4025     }
4026     //if (idx && renderSortByDepth(o, renderVisibleCids[idx-1])) writeln("VIOLATION!");
4027     o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
4028   }
4030   // render object (part two: front tile parts, depth 3.5)
4031   foreach (MapTile tile; renderFrontTiles) {
4032     tile.drawWithOfsFront(xofs, yofs, scale, currFrameDelta);
4033   }
4035   // render objects (part three: depth <= 3)
4036   foreach (auto idx; 0..depth4Start; reverse) {
4037     MapEntity o = renderVisibleCids[idx];
4038     o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
4039     //done above;if (isDarkLevel && (o.lightRadius > 4 || (o isa MapTile && MapTile(o).litWholeTile))) renderVisibleLights[$] = o;
4040   }
4042   // render player post-effects, so cape or jetpack will be correctly rendered on level exit, for example
4043   player.lastDrawWithOfs(xofs, yofs, scale, currFrameDelta);
4045   // lighting
4046   if (isDarkLevel) {
4047     auto ltex = bgtileStore.lightTexture('ltx512', 512);
4049     // set screen alpha to min
4050     Video.colorMask = Video::CMask.Alpha;
4051     Video.blendMode = Video::BlendMode.None;
4052     Video.color = 0xff_ff_ff_ff;
4053     Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
4054     //Video.colorMask = Video::CMask.All;
4056     // blend lights
4057     // also, stencil 'em, so we can filter dark areas
4058     Video.textureFiltering = true;
4059     Video.stencil = true;
4060     Video.stencilFunc(Video::StencilFunc.Always, 1);
4061     Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Replace);
4062     Video.alphaTestFunc = Video::AlphaFunc.Greater;
4063     Video.alphaTestVal = 0.03+0.011*global.config.darknessDarkness;
4064     Video.color = 0xff_ff_ff;
4065     Video.blendFunc = Video::BlendFunc.Max;
4066     Video.blendMode = Video::BlendMode.Blend; // anything except `Normal`
4067     Video.colorMask = Video::CMask.Alpha;
4069     foreach (MapEntity e; renderVisibleLights) {
4070       int xi, yi;
4071       e.getInterpCoords(currFrameDelta, scale, out xi, out yi);
4072       auto tile = MapTile(e);
4073       if (tile && tile.litWholeTile) {
4074         //Video.color = 0xff_ff_ff;
4075         Video.fillRect(xi-xofs, yi-yofs, e.width*scale, e.height*scale);
4076       }
4077       int lrad = e.lightRadius;
4078       if (lrad < 4) continue; // just in case
4079       lrad += 8;
4080       float lightscale = float(lrad*scale)/float(ltex.tex.width);
4081 #ifdef OLD_LIGHT_OFFSETS
4082       int fx0, fy0, fx1, fy1;
4083       bool doMirror;
4084       auto spf = e.getSpriteFrame(out doMirror, out fx0, out fy0, out fx1, out fy1);
4085       if (spf) {
4086         xi += (fx1-fx0)*scale/2;
4087         yi += (fy1-fy0)*scale/2;
4088       }
4089 #else
4090       int lxofs, lyofs;
4091       e.getLightOffset(out lxofs, out lyofs);
4092       xi += lxofs*scale;
4093       yi += lyofs*scale;
4095 #endif
4096       lrad = lrad*scale/2;
4097       xi -= xofs+lrad;
4098       yi -= yofs+lrad;
4099       ltex.tex.blitAt(xi, yi, lightscale);
4100     }
4101     Video.textureFiltering = false;
4103     // modify only lit parts
4104     Video.stencilFunc(Video::StencilFunc.Equal, 1);
4105     Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Keep);
4106     // multiply framebuffer colors by framebuffer alpha
4107     Video.color = 0xff_ff_ff; // it doesn't matter
4108     Video.blendFunc = Video::BlendFunc.Add;
4109     Video.blendMode = Video::BlendMode.DstMulDstAlpha;
4110     Video.colorMask = Video::CMask.Colors;
4111     Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
4113     // filter unlit parts
4114     Video.stencilFunc(Video::StencilFunc.NotEqual, 1);
4115     Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Keep);
4116     Video.blendFunc = Video::BlendFunc.Add;
4117     Video.blendMode = Video::BlendMode.Filter;
4118     Video.colorMask = Video::CMask.Colors;
4119     Video.color = 0x00_00_18+0x00_00_10*global.config.darknessDarkness;
4120     //Video.color = 0x00_00_18;
4121     //Video.color = 0x00_00_38;
4122     Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
4124     // restore defaults
4125     Video.blendFunc = Video::BlendFunc.Add;
4126     Video.blendMode = Video::BlendMode.Normal;
4127     Video.colorMask = Video::CMask.All;
4128     Video.alphaTestFunc = Video::AlphaFunc.Always;
4129     Video.stencil = false;
4130   }
4132   // clear visible objects list (nope)
4133   //renderVisibleCids.clear();
4134   //renderVisibleLights.clear();
4137   if (global.config.drawHUD) renderHUD(currFrameDelta);
4138   renderCompass(currFrameDelta);
4140   float osdTimeLeft, osdTimeStart;
4141   string msg = osdGetMessage(out osdTimeLeft, out osdTimeStart);
4142   if (msg) {
4143     auto ct = GetTickCount();
4144     int msgScale = 3;
4145     sprStore.loadFont('sFontSmall');
4146     auto msgHeight = sprStore.getMultilineTextHeight(msg, msgScale);
4147     int x = viewWidth/2;
4148     int y = viewHeight-64-msgHeight;
4149     auto oldColor = Video.color;
4150     Video.color = 0xff_ff_00;
4151     if (osdTimeLeft < 0.5) {
4152       int alpha = 255-clamp(int(510*osdTimeLeft), 0, 255);
4153       Video.color = Video.color|(alpha<<24);
4154     } else if (ct-osdTimeStart < 0.5) {
4155       osdTimeStart = ct-osdTimeStart;
4156       int alpha = 255-clamp(int(510*osdTimeStart), 0, 255);
4157       Video.color = Video.color|(alpha<<24);
4158     }
4159     sprStore.renderMultilineTextCentered(x, y, msg, msgScale, 0x00_ff_00, 0xff_ff_ff);
4160     Video.color = oldColor;
4161   }
4163   int hiColor1, hiColor2;
4164   msg = osdGetTalkMessage(out hiColor1, out hiColor2);
4165   if (msg) {
4166     int msgScale = 2;
4167     sprStore.loadFont('sFontSmall');
4168     auto msgWidth = sprStore.getMultilineTextWidth(msg, processHighlights1:true, processHighlights2:true);
4169     auto msgHeight = sprStore.getMultilineTextHeight(msg);
4170     auto msgWidthOrig = msgWidth*msgScale;
4171     auto msgHeightOrig = msgHeight*msgScale;
4172     if (msgWidth%16 != 0) msgWidth = (msgWidth|0x0f)+1;
4173     if (msgHeight%16 != 0) msgHeight = (msgHeight|0x0f)+1;
4174     msgWidth *= msgScale;
4175     msgHeight *= msgScale;
4176     int x = (viewWidth-msgWidth)/2;
4177     int y = 32*msgScale;
4178     auto oldColor = Video.color;
4179     // draw text frame and text background
4180     Video.color = 0;
4181     Video.fillRect(x, y, msgWidth, msgHeight);
4182     Video.color = 0xff_ff_ff;
4183     for (int fdx = 0; fdx < msgWidth; fdx += 16*msgScale) {
4184       auto spf = sprStore['sMenuTop'].frames[0];
4185       spf.tex.blitAt(x+fdx, y-16*msgScale, msgScale);
4186       spf = sprStore['sMenuBottom'].frames[0];
4187       spf.tex.blitAt(x+fdx, y+msgHeight, msgScale);
4188     }
4189     for (int fdy = 0; fdy < msgHeight; fdy += 16*msgScale) {
4190       auto spf = sprStore['sMenuLeft'].frames[0];
4191       spf.tex.blitAt(x-16*msgScale, y+fdy, msgScale);
4192       spf = sprStore['sMenuRight'].frames[0];
4193       spf.tex.blitAt(x+msgWidth, y+fdy, msgScale);
4194     }
4195     {
4196       auto spf = sprStore['sMenuUL'].frames[0];
4197       spf.tex.blitAt(x-16*msgScale, y-16*msgScale, msgScale);
4198       spf = sprStore['sMenuUR'].frames[0];
4199       spf.tex.blitAt(x+msgWidth, y-16*msgScale, msgScale);
4200       spf = sprStore['sMenuLL'].frames[0];
4201       spf.tex.blitAt(x-16*msgScale, y+msgHeight, msgScale);
4202       spf = sprStore['sMenuLR'].frames[0];
4203       spf.tex.blitAt(x+msgWidth, y+msgHeight, msgScale);
4204     }
4205     Video.color = 0xff_ff_00;
4206     sprStore.renderMultilineText(x+(msgWidth-msgWidthOrig)/2, y+(msgHeight-msgHeightOrig)/2-3*msgScale, msg, msgScale, (hiColor1 == -1 ? 0x00_ff_00 : hiColor1), (hiColor2 == -1 ? 0xff_ff_ff : hiColor2));
4207     Video.color = oldColor;
4208   }
4210   if (inWinCutscene) renderWinCutsceneOverlay();
4211   if (inIntroCutscene) renderTitleCutsceneOverlay();
4212   if (isTransitionRoom()) renderTransitionOverlay();
4214   /*
4215   if (doRestoreGL) {
4216     Video.setScissor(scsave);
4217     Video.glPopMatrix();
4218   }
4219   */
4221   Video.color = 0xff_ff_ff;
4225 // ////////////////////////////////////////////////////////////////////////// //
4226 final class!MapObject findGameObjectClassByName (name aname) {
4227   if (!aname) return none; // just in case
4228   auto co = FindClassByGameObjName(aname);
4229   if (!co) {
4230     writeln("***UNKNOWN GAME OBJECT: '", aname, "'");
4231     return none;
4232   }
4233   co = GetClassReplacement(co);
4234   if (!co) FatalError("findGameObjectClassByName: WTF?!");
4235   if (!class!MapObject(co)) FatalError("findGameTileClassByName: object '%n' is not a map object, it is a `%n`", aname, GetClassName(co));
4236   return class!MapObject(co);
4240 final class!MapTile findGameTileClassByName (name aname) {
4241   if (!aname) return none; // just in case
4242   auto co = FindClassByGameObjName(aname);
4243   if (!co) return MapTile; // unknown names will be routed directly to tile object
4244   co = GetClassReplacement(co);
4245   if (!co) FatalError("findGameTileClassByName: WTF?!");
4246   if (!class!MapTile(co)) FatalError("findGameTileClassByName: object '%n' is not a tile, it is a `%n`", aname, GetClassName(co));
4247   return class!MapTile(co);
4251 final MapObject findAnyObjectOfType (name aname) {
4252   if (!aname) return none;
4253   auto cls = FindClassByGameObjName(aname);
4254   if (!cls) return none;
4255   foreach (MapObject obj; objGrid.allObjects(MapObject)) {
4256     if (obj.spectral) continue;
4257     if (obj isa cls) return obj;
4258   }
4259   return none;
4263 // ////////////////////////////////////////////////////////////////////////// //
4264 final bool isRopePlacedAt (int x, int y) {
4265   int[8] covered;
4266   foreach (ref auto v; covered) v = false;
4267   foreach (MapTile t; objGrid.inRectPix(x, y-8, 1, 17, precise:false, castClass:MapTileRope)) {
4268     //if (!cbIsRopeTile(t)) continue;
4269     if (t.ix != x) continue;
4270     if (t.iy == y) return true;
4271     foreach (int ty; t.iy..t.iy+8) {
4272       int d = ty-y;
4273       if (d >= 0 && d < covered.length) covered[d] = true;
4274     }
4275   }
4276   // check if the whole rope height is completely covered with ropes
4277   foreach (auto v; covered) if (!v) return false;
4278   return true;
4282 final MapTile CreateMapTile (int xpos, int ypos, name aname) {
4283   if (!aname) FatalError("cannot create typeless tile");
4284   auto tclass = findGameTileClassByName(aname);
4285   if (!tclass) return none;
4286   MapTile tile = SpawnObject(tclass);
4287   tile.global = global;
4288   tile.level = self;
4289   tile.objName = aname;
4290   tile.objType = aname; // just in case
4291   tile.fltx = xpos;
4292   tile.flty = ypos;
4293   tile.objId = ++lastUsedObjectId;
4294   if (!tile.initialize()) FatalError("cannot create tile '%n'", aname);
4295   return tile;
4299 final bool PutSpawnedMapTile (int x, int y, MapTile tile, optional bool putToGrid) {
4300   if (!tile || !tile.isInstanceAlive) return false;
4302   if (!putToGrid) putToGrid = (tile.moveable || tile.toSpecialGrid || tile.width != 16 || tile.height != 16 || x%16 != 0 || y%16 != 0);
4304   //writeln("putting tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (putToGrid ? "special " : ""), "grid, pos=(", x/16.0, ",", y/16.0, ")");
4306   if (!putToGrid) {
4307     int mapx = x/16, mapy = y/16;
4308     if (mapx < 0 || mapx >= tilesWidth || mapy < 0 || mapy >= tilesHeight) return false;
4309   }
4311   // if we already have rope tile there, there is no reason to add another one
4312   if (tile isa MapTileRope) {
4313     if (isRopePlacedAt(x, y)) return false;
4314   }
4316   // activate special or animated tile
4317   tile.active = tile.active || putToGrid || tile.moveable || tile.toSpecialGrid || tile.lava /*|| tile.water*/; // will be done in MakeMapTile
4318   // animated tiles must be active
4319   if (!tile.active) {
4320     auto spr = tile.getSprite();
4321     if (spr && spr.frames.length > 1) {
4322       writeln("activated animated tile '", tile.objName, "'");
4323       tile.active = true;
4324     }
4325   }
4327   tile.fltx = x;
4328   tile.flty = y;
4329   if (putToGrid) {
4330     //if (tile isa TitleTileCopy) writeln("*** PUTTING COPYRIGHT TILE");
4331     tile.toSpecialGrid = true;
4332     if (!tile.dontReplaceOthers && x&16 == 0 && y%16 == 0) {
4333       auto t = getTileAtGridAny(x/16, y/16);
4334       if (t && !t.immuneToReplacement) {
4335         writeln("REPLACING tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
4336         writeln("      NEW tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (tile.toSpecialGrid ? "special " : ""), "grid, pos=(", tile.fltx/16.0, ",", tile.flty/16.0, ")");
4337         t.instanceRemove();
4338       }
4339     }
4340     objGrid.insert(tile);
4341   } else {
4342     //writeln("SIZE: ", tilesWidth, "x", tilesHeight);
4343     setTileAtGrid(x/16, y/16, tile);
4344     auto t = getTileAtGridAny(x/16, y/16);
4345     /*
4346     if (t != tile) {
4347       writeln("putting tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (putToGrid ? "special " : ""), "grid, pos=(", x/16.0, ",", y/16.0, ")");
4348       checkTilesInRect(x/16, y/16, 16, 16, delegate bool (MapTile tile) {
4349         writeln("  *** tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (tile.toSpecialGrid || tile.moveable ? "special " : ""), "grid, pos=(", tile.fltx/16.0, ",", tile.flty/16.0, ")");
4350         return false;
4351       });
4352       FatalError("FUUUUUU");
4353     }
4354     */
4355   }
4357   if (tile.enter) registerEnter(tile);
4358   if (tile.exit) registerExit(tile);
4360   // make tile under exit invulnerable
4361   if (checkTilesInRect(tile.ix, tile.iy-16, 16, 16, delegate bool (MapTile t) { return t.exit; })) {
4362     tile.invincible = true;
4363   }
4365   return true;
4369 // won't call `onDestroy()`
4370 final void RemoveMapTileFromGrid (int tileX, int tileY, optional string reason) {
4371   if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
4372     auto t = getTileAtGridAny(tileX, tileY);
4373     if (t) {
4374       writeln("REMOVING(RMT", (reason ? ":"~reason : ""), ") tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
4375       t.instanceRemove();
4376       checkWater = true;
4377     }
4378   }
4382 final MapTile MakeMapTile (int mapx, int mapy, name aname, optional bool putToGrid) {
4383   //writeln("tile at (", mapx, ",", mapy, "): ", aname);
4384   //if (aname == 'oLush') { MapObject fail; fail.initialize(); }
4385   //if (mapy >= tilesHeight) { writeln("OOM: ", aname, "; ty=", mapy, "; hgt=", tilesHeight); }
4386   if (mapx < 0 || mapx >= tilesWidth || mapy < 0 || mapy >= tilesHeight) return none;
4388   // if we already have rope tile there, there is no reason to add another one
4389   if (aname == 'oRope') {
4390     if (isRopePlacedAt(mapx*16, mapy*16)) return none;
4391   }
4393   auto tile = CreateMapTile(mapx*16, mapy*16, aname);
4394   if (!tile) return none;
4395   if (!PutSpawnedMapTile(mapx*16, mapy*16, tile, putToGrid!optional)) {
4396     delete tile;
4397     tile = none;
4398   }
4400   return tile;
4404 final MapTile MakeMapTileAtPix (int xpix, int ypix, name aname, optional bool putToGrid) {
4405   // if we already have rope tile there, there is no reason to add another one
4406   if (aname == 'oRope') {
4407     if (isRopePlacedAt(xpix, ypix)) return none;
4408   }
4410   auto tile = CreateMapTile(xpix, ypix, aname);
4411   if (!tile) return none;
4412   if (!PutSpawnedMapTile(xpix, ypix, tile, putToGrid!optional)) {
4413     delete tile;
4414     tile = none;
4415   }
4417   return tile;
4421 final MapTile MakeMapRopeTileAt (int x0, int y0) {
4422   // if we already have rope tile there, there is no reason to add another one
4423   if (isRopePlacedAt(x0, y0)) return none;
4425   auto tile = CreateMapTile(x0, y0, 'oRope');
4426   if (!PutSpawnedMapTile(x0, y0, tile, putToGrid:true)) {
4427     delete tile;
4428     tile = none;
4429   }
4431   return tile;
4435 // ////////////////////////////////////////////////////////////////////////// //
4436 final MapBackTile CreateBgTile (name sprName, optional int atx0, optional int aty0, optional int aw, optional int ah) {
4437   BackTileImage img = bgtileStore[sprName];
4438   auto res = SpawnObject(MapBackTile);
4439   res.global = global;
4440   res.level = self;
4441   res.bgt = img;
4442   res.bgtName = sprName;
4443   if (specified_atx0) res.tx0 = atx0;
4444   if (specified_aty0) res.ty0 = aty0;
4445   if (specified_aw) res.w = aw;
4446   if (specified_ah) res.h = ah;
4447   if (!res.initialize()) FatalError("cannot create background tile with image '%n'", sprName);
4448   return res;
4452 // ////////////////////////////////////////////////////////////////////////// //
4454 background The background asset from which the new tile will be extracted.
4455 left The x coordinate of the left of the new tile, relative to the background asset's top left corner.
4456 top The y coordinate of the top of the new tile, relative to the background assets top left corner.
4457 width The width of the tile.
4458 height The height of the tile.
4459 x The x position in the room to place the tile.
4460 y The y position in the room to place the tile.
4461 depth The depth at which to place the tile.
4463 final void MakeMapBackTile (name bgname, int left, int top, int width, int height, int x, int y, int depth) {
4464   if (width < 1 || height < 1 || !bgname) return;
4465   auto bgt = bgtileStore[bgname];
4466   if (!bgt) FatalError("cannot load background '%n'", bgname);
4467   MapBackTile bt = SpawnObject(MapBackTile);
4468   bt.global = global;
4469   bt.level = self;
4470   bt.objName = bgname;
4471   bt.bgt = bgt;
4472   bt.bgtName = bgname;
4473   bt.fltx = x;
4474   bt.flty = y;
4475   bt.tx0 = left;
4476   bt.ty0 = top;
4477   bt.w = width;
4478   bt.h = height;
4479   bt.depth = depth;
4480   // find a place for it
4481   if (!backtiles) {
4482     backtiles = bt;
4483     return;
4484   }
4485   // back tiles with the highest depth should come first
4486   MapBackTile ct = backtiles, cprev = none;
4487   while (ct && depth <= ct.depth) { cprev = ct; ct = ct.next; }
4488   // insert before ct
4489   if (cprev) {
4490     bt.next = cprev.next;
4491     cprev.next = bt;
4492   } else {
4493     bt.next = backtiles;
4494     backtiles = bt;
4495   }
4499 // ////////////////////////////////////////////////////////////////////////// //
4500 final MapObject SpawnMapObjectWithClass (class!MapObject oclass) {
4501   if (!oclass) return none;
4503   MapObject obj = SpawnObject(oclass);
4504   if (!obj) FatalError("cannot create map object '%n'", GetClassName(oclass));
4506   //if (aname == 'oShells' || aname == 'oShellSingle' || aname == 'oRopePile') writeln("*** CREATED '", aname, "'");
4508   obj.global = global;
4509   obj.level = self;
4510   obj.objId = ++lastUsedObjectId;
4512   return obj;
4516 final MapObject SpawnMapObject (name aname) {
4517   if (!aname) return none;
4518   auto res = SpawnMapObjectWithClass(findGameObjectClassByName(aname));
4519   if (res && !res.objType) res.objType = aname; // just in case
4520   return res;
4524 final MapObject PutSpawnedMapObject (int x, int y, MapObject obj) {
4525   if (!obj /*|| obj.global || obj.level*/) return none; // oops
4527   obj.fltx = x;
4528   obj.flty = y;
4529   if (!obj.initialize()) { delete obj; return none; } // not fatal
4531   insertObject(obj);
4533   return obj;
4537 final MapObject MakeMapObject (int x, int y, name aname) {
4538   MapObject obj = SpawnMapObject(aname);
4539   obj = PutSpawnedMapObject(x, y, obj);
4540   return obj;
4544 // ////////////////////////////////////////////////////////////////////////// //
4545 int winCutSceneTimer = -1;
4546 int winVolcanoTimer = -1;
4547 int winCutScenePhase = 0;
4548 int winSceneDrawStatus = 0;
4549 int winMoneyCount = 0;
4550 int winTime;
4551 bool winFadeOut = false;
4552 int winFadeLevel = 0;
4553 int winCutsceneSkip = 0; // 1: waiting for pay release; 2: pay released, do skip
4554 bool winCutsceneSwitchToNext = false;
4557 void startWinCutscene () {
4558   global.hasParachute = false;
4559   shakeLeft = 0;
4560   winCutsceneSwitchToNext = false;
4561   winCutsceneSkip = 0;
4562   isKeyPressed(GameConfig::Key.Pay);
4563   isKeyReleased(GameConfig::Key.Pay);
4565   auto olddel = ImmediateDelete;
4566   ImmediateDelete = false;
4567   clearWholeLevel();
4569   createEnd1Room();
4570   fixWallTiles();
4571   addBackgroundGfxDetails();
4573   levBGImgName = 'bgCave';
4574   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
4576   blockWaterChecking = true;
4577   fixLiquidTop();
4578   cleanDeadTiles();
4580   ImmediateDelete = olddel;
4581   CollectGarbage(true); // destroy delayed objects too
4583   if (dumpGridStats) objGrid.dumpStats();
4585   playerExited = false; // just in case
4586   playerExitDoor = none;
4588   osdClear(clearTalk:true);
4590   setupGhostTime();
4591   global.stopMusic();
4593   inWinCutscene = 1;
4594   winCutSceneTimer = -1;
4595   winCutScenePhase = 0;
4597   /+
4598   if (global.config.gameMode != GameConfig::GameMode.Vanilla) {
4599     if (global.config.bizarre) {
4600       global.yasmScore = 1;
4601       global.config.bizarrePlusTitle = true;
4602     }
4604     array!MapTile toReplace;
4605     forEachTile(delegate bool (MapTile t) {
4606       if (t.objType == 'oGTemple' ||
4607           t.objType == 'oIce' ||
4608           t.objType == 'oDark' ||
4609           t.objType == 'oBrick' ||
4610           t.objType == 'oLush')
4611       {
4612         toReplace[$] = t;
4613       }
4614       return false;
4615     });
4617     foreach (MapTile t; miscTileGrid.allObjects()) {
4618       if (t.objType == 'oGTemple' ||
4619           t.objType == 'oIce' ||
4620           t.objType == 'oDark' ||
4621           t.objType == 'oBrick' ||
4622           t.objType == 'oLush')
4623       {
4624         toReplace[$] = t;
4625       }
4626     }
4628     foreach (MapTile t; toReplace) {
4629       if (t.iy < 192) {
4630         t.cleanDeath = true;
4631             if (rand(1,120) == 1) instance_change(oGTemple, false);
4632         else if (rand(1,100) == 1) instance_change(oIce, false);
4633         else if (rand(1,90) == 1) instance_change(oDark, false);
4634         else if (rand(1,80) == 1) instance_change(oBrick, false);
4635         else if (rand(1,70) == 1) instance_change(oLush, false);
4636           }
4637       }
4638       with (oBrick)
4639       {
4640           if (y &lt; 192)
4641           {
4642               cleanDeath = true;
4643               if (rand(1,5) == 1) instance_change(oLush, false);
4644           }
4645       }
4646   }
4647   +/
4648   //!instance_create(0, 0, oBricks);
4650   //shakeToggle = false;
4651   //oPDummy.status = 2;
4653   //timer = 0;
4655   /+
4656   if (global.kaliPunish &gt;= 2) {
4657       instance_create(oPDummy.x, oPDummy.y+2, oBall2);
4658       obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
4659       obj.linkVal = 1;
4660       obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
4661       obj.linkVal = 2;
4662       obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
4663       obj.linkVal = 3;
4664       obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
4665       obj.linkVal = 4;
4666   }
4667   +/
4671 void startWinCutsceneVolcano () {
4672   global.hasParachute = false;
4673   /*
4674   writeln("VOLCANO HOLD 0: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4675   writeln("VOLCANO PICK 0: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4676   */
4678   shakeLeft = 0;
4679   winCutsceneSwitchToNext = false;
4680   auto olddel = ImmediateDelete;
4681   ImmediateDelete = false;
4682   clearWholeLevel();
4684   levBGImgName = '';
4685   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
4687   blockWaterChecking = true;
4689   ImmediateDelete = olddel;
4690   CollectGarbage(true); // destroy delayed objects too
4692   spawnPlayerAt(2*16+8, 11*16+8);
4693   player.dir = MapEntity::Dir.Right;
4695   playerExited = false; // just in case
4696   playerExitDoor = none;
4698   osdClear(clearTalk:true);
4700   setupGhostTime();
4701   global.stopMusic();
4703   inWinCutscene = 2;
4704   winCutSceneTimer = -1;
4705   winCutScenePhase = 0;
4707   MakeMapTile(0, 0, 'oEnd2BG');
4708   realViewStart.x = 0;
4709   realViewStart.y = 0;
4710   viewStart.x = 0;
4711   viewStart.y = 0;
4713   viewMin.x = 0;
4714   viewMin.y = 0;
4715   viewMax.x = 320;
4716   viewMax.y = 240;
4718   player.dead = false;
4719   player.active = true;
4720   player.visible = false;
4721   player.removeBallAndChain(temp:true);
4722   player.stunned = false;
4723   player.status = MapObject::FALLING;
4724   if (player.holdItem) player.holdItem.visible = false;
4725   player.fltx = 320/2;
4726   player.flty = 0;
4728   /*
4729   writeln("VOLCANO HOLD 1: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4730   writeln("VOLCANO PICK 1: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4731   */
4735 void startWinCutsceneWinFall () {
4736   global.hasParachute = false;
4737   /*
4738   writeln("FALL HOLD 0: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4739   writeln("FALL PICK 0: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4740   */
4742   shakeLeft = 0;
4743   winCutsceneSwitchToNext = false;
4745   auto olddel = ImmediateDelete;
4746   ImmediateDelete = false;
4747   clearWholeLevel();
4749   createEnd3Room();
4750   setMenuTilesVisible(false);
4751   //fixWallTiles();
4752   //addBackgroundGfxDetails();
4754   levBGImgName = '';
4755   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
4757   blockWaterChecking = true;
4758   fixLiquidTop();
4759   cleanDeadTiles();
4761   ImmediateDelete = olddel;
4762   CollectGarbage(true); // destroy delayed objects too
4764   if (dumpGridStats) objGrid.dumpStats();
4766   playerExited = false; // just in case
4767   playerExitDoor = none;
4769   osdClear(clearTalk:true);
4771   setupGhostTime();
4772   global.stopMusic();
4774   inWinCutscene = 3;
4775   winCutSceneTimer = -1;
4776   winCutScenePhase = 0;
4778   player.dead = false;
4779   player.active = true;
4780   player.visible = false;
4781   player.removeBallAndChain(temp:true);
4782   player.stunned = false;
4783   player.status = MapObject::FALLING;
4784   if (player.holdItem) player.holdItem.visible = false;
4785   player.fltx = 320/2;
4786   player.flty = 0;
4788   winSceneDrawStatus = 0;
4789   winMoneyCount = 0;
4791   winFadeOut = false;
4792   winFadeLevel = 0;
4794   /*
4795   writeln("FALL HOLD 1: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4796   writeln("FALL PICK 1: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4797   */
4801 void setGameOver () {
4802   if (inWinCutscene) {
4803     player.visible = false;
4804     player.removeBallAndChain(temp:true);
4805     if (player.holdItem) player.holdItem.visible = false;
4806   }
4807   player.dead = true;
4808   if (inWinCutscene > 0) {
4809     winFadeOut = true;
4810     winFadeLevel = 255;
4811     winSceneDrawStatus = 8;
4812   }
4816 MapTile findEndPlatTile () {
4817   return forEachTile(delegate bool (MapTile t) { return (t isa MapTileEndPlat); }, castClass:MapTileEndPlat);
4821 MapObject findBigTreasure () {
4822   return forEachObject(delegate bool (MapObject o) { return (o isa MapObjectBigTreasure); }, castClass:MapObjectBigTreasure);
4826 void setMenuTilesVisible (bool vis) {
4827   if (vis) {
4828     forEachTile(delegate bool (MapTile t) {
4829       if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4830         t.invisible = false;
4831       }
4832       return false;
4833     });
4834   } else {
4835     forEachTile(delegate bool (MapTile t) {
4836       if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4837         t.invisible = true;
4838       }
4839       return false;
4840     });
4841   }
4845 void setMenuTilesOnTop () {
4846   forEachTile(delegate bool (MapTile t) {
4847     if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4848       t.depth = 1;
4849     }
4850     return false;
4851   });
4855 void winCutscenePlayerControl (PlayerPawn plr) {
4856   auto payPress = isKeyPressed(GameConfig::Key.Pay);
4857   auto payRelease = isKeyReleased(GameConfig::Key.Pay);
4859   switch (winCutsceneSkip) {
4860     case 0: // nothing was pressed
4861       if (payPress) winCutsceneSkip = 1;
4862       break;
4863     case 1: // waiting for pay release
4864       if (payRelease) winCutsceneSkip = 2;
4865       break;
4866     case 2: // pay released, do skip
4867       setGameOver();
4868       return;
4869   }
4871   // first winning room
4872   if (inWinCutscene == 1) {
4873     if (plr.ix < 448+8) {
4874       plr.kRight = true;
4875       return;
4876     }
4878     // waiting for chest to open
4879     if (winCutScenePhase == 0) {
4880       winCutSceneTimer = 120/2;
4881       winCutScenePhase = 1;
4882       return;
4883     }
4885     // spawn big idol
4886     if (winCutScenePhase == 1) {
4887       if (--winCutSceneTimer == 0) {
4888         winCutScenePhase = 2;
4889         winCutSceneTimer = 20;
4890         forEachObject(delegate bool (MapObject o) {
4891           if (o isa MapObjectBigChest) {
4892             o.setSprite(global.config.gameMode == GameConfig::GameMode.Vanilla ? 'sBigChestOpen' : 'sBigChestOpen2');
4893             auto treasure = MakeMapObject(o.ix, o.iy, 'oBigTreasure');
4894             if (treasure) {
4895               treasure.yVel = -4;
4896               treasure.xVel = -3;
4897               o.playSound('sndClick');
4898               //!!!if (global.config.gameMode != GameConfig::GameMode.Vanilla) scrSprayGems(oBigChest.x+24, oBigChest.y+24);
4899             }
4900           }
4901           return false;
4902         });
4903       }
4904       return;
4905     }
4907     // lava pump wait
4908     if (winCutScenePhase == 2) {
4909       if (--winCutSceneTimer == 0) {
4910         winCutScenePhase = 3;
4911         winCutSceneTimer = 50;
4912       }
4913       return;
4914     }
4916     // lava pump start
4917     if (winCutScenePhase == 3) {
4918       auto ep = findEndPlatTile();
4919       if (ep) MakeMapObject(ep.ix+global.randOther(0, 80), /*ep.iy*/192+32, 'oBurn');
4920       if (--winCutSceneTimer == 0) {
4921         winCutScenePhase = 4;
4922         winCutSceneTimer = 10;
4923         if (ep) MakeMapObject(ep.ix, ep.iy+30, 'oLavaSpray');
4924         scrShake(9999);
4925       }
4926       return;
4927     }
4929     // lava pump first accel
4930     if (winCutScenePhase == 4) {
4931       if (--winCutSceneTimer == 0) {
4932         forEachObject(delegate bool (MapObject o) {
4933           if (o isa MapObjectLavaSpray) o.yAcc = -0.1;
4934           return false;
4935         });
4936       }
4937     }
4939     // lava pump complete
4940     if (winCutScenePhase == 5) {
4941       if (--winCutSceneTimer == 0) {
4942         //if (oLavaSpray) oLavaSpray.yAcc = -0.1;
4943         startWinCutsceneVolcano();
4944       }
4945       return;
4946     }
4947     return;
4948   }
4951   // volcano room
4952   if (inWinCutscene == 2) {
4953     plr.flty = 0;
4955     // initialize
4956     if (winCutScenePhase == 0) {
4957       winCutSceneTimer = 50;
4958       winCutScenePhase = 1;
4959       winVolcanoTimer = 10;
4960       return;
4961     }
4963     if (winVolcanoTimer > 0) {
4964       if (--winVolcanoTimer == 0) {
4965         MakeMapObject(224+global.randOther(0,48), 144+global.randOther(0,8), 'oVolcanoFlame');
4966         winVolcanoTimer = global.randOther(10, 20);
4967       }
4968     }
4970     // plr sil
4971     if (winCutScenePhase == 1) {
4972       if (--winCutSceneTimer == 0) {
4973         winCutSceneTimer = 30;
4974         winCutScenePhase = 2;
4975         auto sil = MakeMapObject(240, 132, 'oPlayerSil');
4976         //sil.xVel = -6;
4977         //sil.yVel = -8;
4978       }
4979       return;
4980     }
4982     // treasure sil
4983     if (winCutScenePhase == 2) {
4984       if (--winCutSceneTimer == 0) {
4985         winCutScenePhase = 3;
4986         auto sil = MakeMapObject(240, 132, 'oTreasureSil');
4987         //sil.xVel = -6;
4988         //sil.yVel = -8;
4989       }
4990       return;
4991     }
4993     return;
4994   }
4996   // winning camel room
4997   if (inWinCutscene == 3) {
4998     //if (!player.holdItem)  writeln("SCENE 3: LOST ITEM!");
5000     if (!plr.visible) plr.flty = -32;
5002     // initialize
5003     if (winCutScenePhase == 0) {
5004       winCutSceneTimer = 50;
5005       winCutScenePhase = 1;
5006       return;
5007     }
5009     // fall sound
5010     if (winCutScenePhase == 1) {
5011       if (--winCutSceneTimer == 0) {
5012         winCutSceneTimer = 50;
5013         winCutScenePhase = 2;
5014         plr.playSound('sndPFall');
5015         plr.visible = true;
5016         plr.active = true;
5017         writeln("MUST BE CHAINED: ", plr.mustBeChained);
5018         if (plr.mustBeChained) {
5019           plr.removeBallAndChain(temp:true);
5020           plr.spawnBallAndChain();
5021         }
5022         /*
5023         writeln("HOLD: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
5024         writeln("PICK: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
5025         */
5026         if (!player.holdItem && player.pickedItem) player.scrSwitchToPocketItem(forceIfEmpty:false);
5027         if (player.holdItem) {
5028           player.holdItem.visible = true;
5029           player.holdItem.canLiveOutsideOfLevel = true;
5030           writeln("HOLD ITEM: '", GetClassName(player.holdItem.Class), "'");
5031         }
5032         plr.status == MapObject::FALLING;
5033         global.plife += 99; // just in case
5034       }
5035       return;
5036     }
5038     if (winCutScenePhase == 2) {
5039       auto ball = plr.getMyBall();
5040       if (ball && plr.holdItem != ball) {
5041         ball.teleportTo(plr.fltx, plr.flty+8);
5042         ball.yVel = 6;
5043         ball.myGrav = 0.6;
5044       }
5045       if (plr.status == MapObject::STUNNED || plr.stunned) {
5046         //alarm[0] = 70;
5047         //alarm[1] = 50;
5048         //status = GETUP;
5049         auto treasure = MakeMapObject(144+16+8, -32, 'oBigTreasure');
5050         if (treasure) treasure.depth = 1;
5051         winCutScenePhase = 3;
5052         plr.stunTimer = 30;
5053         plr.playSound('sndTFall');
5054       }
5055       return;
5056     }
5058     if (winCutScenePhase == 3) {
5059       if (plr.status != MapObject::STUNNED && !plr.stunned) {
5060         auto bt = findBigTreasure();
5061         if (bt) {
5062           if (bt.yVel == 0) {
5063             //plr.yVel = -4;
5064             //plr.status = MapObject::JUMPING;
5065             plr.kJump = true;
5066             plr.kJumpPressed = true;
5067             winCutScenePhase = 4;
5068             winCutSceneTimer = 50;
5069           }
5070         }
5071       }
5072       return;
5073     }
5075     if (winCutScenePhase == 4) {
5076       if (--winCutSceneTimer == 0) {
5077         setMenuTilesVisible(true);
5078         winCutScenePhase = 5;
5079         winSceneDrawStatus = 1;
5080         lastMusicName = '';
5081         global.setMusicPitch(1.0);
5082         global.playMusic('musVictory', loop:false);
5083         winCutSceneTimer = 50;
5084       }
5085       return;
5086     }
5088     if (winCutScenePhase == 5) {
5089       if (winSceneDrawStatus == 3) {
5090         int money = stats.money;
5091         if (winMoneyCount < money) {
5092           if (money-winMoneyCount > 1000) {
5093             winMoneyCount += 1000;
5094           } else if (money-winMoneyCount > 100) {
5095             winMoneyCount += 100;
5096           } else if (money-winMoneyCount > 10) {
5097             winMoneyCount += 10;
5098           } else {
5099             ++winMoneyCount;
5100           }
5101         }
5102         if (winMoneyCount >= money) {
5103           winMoneyCount = money;
5104           ++winSceneDrawStatus;
5105         }
5106         return;
5107       }
5109       if (winSceneDrawStatus == 7) {
5110         winFadeOut = true;
5111         winFadeLevel += 1;
5112         if (winFadeLevel >= 255) {
5113           ++winSceneDrawStatus;
5114           winCutSceneTimer = 30*30;
5115         }
5116         return;
5117       }
5119       if (winSceneDrawStatus == 8) {
5120         if (--winCutSceneTimer == 0) {
5121           setGameOver();
5122         }
5123         return;
5124       }
5126       if (--winCutSceneTimer == 0) {
5127         ++winSceneDrawStatus;
5128         winCutSceneTimer = 50;
5129       }
5130     }
5132     return;
5133   }
5137 // ////////////////////////////////////////////////////////////////////////// //
5138 void renderWinCutsceneOverlay () {
5139   if (inWinCutscene == 3) {
5140     if (winSceneDrawStatus > 0) {
5141       Video.color = 0xff_ff_ff;
5142       sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
5143       //draw_set_color(txtCol);
5144       drawTextAt(64, 32, "YOU MADE IT!");
5146       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
5147       if (global.config.gameMode == GameConfig::GameMode.Vanilla) {
5148         Video.color = 0xff_ff_00; //draw_set_color(c_yellow);
5149         drawTextAt(64, 48, "Classic Mode done!");
5150       } else {
5151         Video.color = 0x00_80_80; //draw_set_color(c_teal);
5152         if (global.config.bizarrePlus) drawTextAt(64, 48, "Bizarre Mode Plus done!");
5153         else drawTextAt(64, 48, "Bizarre Mode done!");
5154         //draw_set_color(c_white);
5155       }
5156       if (!global.usedShortcut) {
5157         Video.color = 0xc0_c0_c0; //draw_set_color(c_silver);
5158         drawTextAt(64, 56, "No shortcuts used!");
5159         //draw_set_color(c_yellow);
5160       }
5161     }
5163     if (winSceneDrawStatus > 1) {
5164       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
5165       //draw_set_color(txtCol);
5166       Video.color = 0xff_ff_ff;
5167       drawTextAt(64, 64, "FINAL SCORE:");
5168     }
5170     if (winSceneDrawStatus > 2) {
5171       sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
5172       Video.color = 0xff_ff_ff; //draw_set_color(c_white);
5173       drawTextAt(64, 72, va("$%d", winMoneyCount));
5174     }
5176     if (winSceneDrawStatus > 4) {
5177       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
5178       Video.color = 0xff_ff_ff; //draw_set_color(c_white);
5179       drawTextAt(64, 96, va("Time: %s", time2str(winTime/30)));
5180       /*
5181       draw_set_color(c_white);
5182       if (s &lt; 10) draw_text(96+24, 96, string(m) + ":0" + string(s));
5183       else draw_text(96+24, 96, string(m) + ":" + string(s));
5184       */
5185     }
5187     if (winSceneDrawStatus > 5) {
5188       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
5189       Video.color = 0xff_ff_ff; //draw_set_color(txtCol);
5190       drawTextAt(64, 96+8, "Kills: ");
5191       Video.color = 0xff_ff_ff; //draw_set_color(c_white);
5192       drawTextAt(96+24, 96+8, va("%s", stats.countKills()));
5193     }
5195     if (winSceneDrawStatus > 6) {
5196       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
5197       Video.color = 0xff_ff_ff; //draw_set_color(txtCol);
5198       drawTextAt(64, 96+16, "Saves: ");
5199       Video.color = 0xff_ff_ff; //draw_set_color(c_white);
5200       drawTextAt(96+24, 96+16, va("%s", stats.damselsSaved));
5201     }
5203     if (winFadeOut) {
5204       Video.color = (255-clamp(winFadeLevel, 0, 255))<<24;
5205       Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
5206     }
5208     if (winSceneDrawStatus == 8) {
5209       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
5210       Video.color = 0xff_ff_ff; //draw_set_color(c_white);
5211       string lastString;
5212       if (global.config.gameMode == GameConfig::GameMode.Vanilla) {
5213         Video.color = 0xff_ff_00; //draw_set_color(c_yellow);
5214         lastString = "YOU SHALL BE REMEMBERED AS A HERO.";
5215       } else {
5216         Video.color = 0x00_ff_ff;
5217         if (global.config.bizarrePlus) lastString = "ANOTHER LEGENDARY ADVENTURE!";
5218         else lastString = "YOUR DISCOVERIES WILL BE CELEBRATED!";
5219       }
5220       auto strLen = lastString.length*8;
5221       int n = 320-strLen;
5222       n = trunc(ceil(n/2.0));
5223       drawTextAt(n, 116, lastString);
5224     }
5225   }
5229 // ////////////////////////////////////////////////////////////////////////// //
5230 #include "roomTitle.vc"
5231 #include "roomTrans1.vc"
5232 #include "roomTrans2.vc"
5233 #include "roomTrans3.vc"
5234 #include "roomTrans4.vc"
5235 #include "roomOlmec.vc"
5236 #include "roomEnd.vc"
5237 #include "roomIntro.vc"
5238 #include "roomTutorial.vc"
5239 #include "roomScores.vc"
5240 #include "roomStars.vc"
5241 #include "roomSun.vc"
5242 #include "roomMoon.vc"
5245 // ////////////////////////////////////////////////////////////////////////// //
5246 #include "packages/Generator/loadRoomGens.vc"
5247 #include "packages/Generator/loadEntityGens.vc"