fixed alot of warnings, and some bugs, so it can be compiled with the current vccrun
[k8vacspelynky.git] / GameLevel.vc
blob6c8be4ae5593fecdc3dca92bc39083afe5f473e4
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 transient int loserGPU;
48 //RoomGen[LevelGen::LevelWidth, LevelGen::LevelHeight] rooms; // moved to levelgen
50 transient float accumTime;
51 transient bool gamePaused = false;
52 transient bool gameShowHelp = false;
53 transient bool checkWater;
54 transient int gameHelpScreen = 0;
55 const int MaxGameHelpScreen = 2;
56 transient int liquidTileCount; // cached
57 /*transient*/ int damselSaved;
59 // hud efffects
60 transient int xmoney;
61 transient int collectCounter;
62 /*transient*/ int levelMoneyStart;
64 // all movable (thinkable) map objects
65 EntityGrid objGrid; // monsters, items and tiles
67 MapBackTile backtiles;
68 bool blockWaterChecking;
69 bool someTilesRemoved;
71 int inWinCutscene;
72 int inIntroCutscene;
73 bool cameFromIntroRoom; // for title screen
74 bool allowFinalCutsceneSkip;
76 LevelGen::RType[MaxTilesWidth, MaxTilesHeight] roomType;
78 enum LevelKind {
79   Normal,
80   Transition,
81   Title,
82   Intro,
83   Tutorial,
84   Scores,
85   Stars,
86   Sun,
87   Moon,
88   //Final,
90 LevelKind levelKind = LevelKind.Normal;
91 int transitionLevelIndex; // set in `generateTransitionLevel()`, used by Tunnel Man
93 array!MapTile allEnters;
94 array!MapTile allExits;
97 int startRoomX, startRoomY;
98 int endRoomX, endRoomY;
100 PlayerPawn player;
101 bool playerExited;
102 MapEntity playerExitDoor;
103 transient bool disablePlayerThink = false;
104 int maxPlayingTime; // in seconds
105 int levelStartTime;
106 int levelEndTime;
108 int ghostTimeLeft;
109 int musicFadeTimer;
110 bool ghostSpawned; // to speed up some checks
111 bool resetBMCOG = false;
112 int udjatAlarm;
115 // FPS, i.e. incremented by 30 in one second
116 int time; // in frames
117 int lastUsedObjectId;
118 transient int lastRenderTime = -1;
119 transient int pausedTime;
121 MapEntity deadItemsHead;
122 transient /*bool*/int hasSolidObjects = true; // to speed up tilechecks
123 // as we have ALOT of inactive tiles on level, we'd better have
124 // a separate list of active items
125 private array!MapEntity activeItemsList;
128 final int activeItemsCount { get { return activeItemsList.length; } }
130 // WARNING! don't add the entity twice!
131 private final void addActiveEntity (MapEntity e) {
132   if (!e) return;
133   if (e.activeItemListIndex) FatalError("addActiveEntity: duplicate!");
134   int fh = activeItemsList.length;
135   activeItemsList[fh] = e;
136   e.activeItemListIndex = fh+1;
139 private final void removeActiveEntity (MapEntity e) {
140   if (!e) return;
141   int ei = e.activeItemListIndex;
142   if (!ei) return;
143   --ei;
144   if (activeItemsList[ei] != e) FatalError("removeActiveEntity: entity management failed (0)");
145   // swap last item and `e`, so we don't have to fix alot of index backrefs
146   auto alen = activeItemsList.length-1;
147   if (alen > 0) {
148     MapEntity ne = activeItemsList[alen];
149     if (ne.activeItemListIndex-1 != alen) FatalError("removeActiveEntity: entity management failed (1)");
150     ne.activeItemListIndex = ei+1;
151     activeItemsList[ei] = ne;
152   } else {
153     if (ei != 0) FatalError("removeActiveEntity: entity management failed (2)");
154   }
155   activeItemsList.length -= 1;
156   e.activeItemListIndex = 0;
159 private final void clearActiveEntities () {
160   foreach (ref auto ai; activeItemsList) if (ai) ai.activeItemListIndex = 0;
161   activeItemsList.clear();
165 // screen shake variables
166 int shakeLeft;
167 IVec2D shakeOfs;
168 IVec2D shakeDir;
170 // set this before calling `fixCamera()`
171 // dimensions should be real, not scaled up/down
172 transient int viewWidth, viewHeight;
173 //transient int viewOffsetX, viewOffsetY;
175 // room bounds, not scaled
176 IVec2D viewMin, viewMax;
178 // for Olmec level cinematics
179 IVec2D cameraSlideToDest;
180 IVec2D cameraSlideToCurr;
181 IVec2D cameraSlideToSpeed; // !0: slide
182 int cameraSlideToPlayer;
183 // `fixCamera()` will set the following
184 // coordinates will be real too (with scale applied)
185 // shake is not applied
186 transient IVec2D viewStart; // with `player.viewOffset`
187 private transient IVec2D realViewStart; // without `player.viewOffset`
189 transient int framesProcessedFromLastClear;
191 transient int BuildYear;
192 transient int BuildMonth;
193 transient int BuildDay;
194 transient int BuildHour;
195 transient int BuildMin;
196 transient string BuildDateString;
199 final string getBuildDateString () {
200   if (!BuildYear) return BuildDateString;
201   if (BuildDateString) return BuildDateString;
202   BuildDateString = va("%d-%02d-%02d %02d:%02d", BuildYear, BuildMonth, BuildDay, BuildHour, BuildMin);
203   return BuildDateString;
207 final void cameraSlideToPoint (int dx, int dy, int speedx, int speedy) {
208   cameraSlideToPlayer = 0;
209   cameraSlideToDest.x = dx;
210   cameraSlideToDest.y = dy;
211   cameraSlideToSpeed.x = abs(speedx);
212   cameraSlideToSpeed.y = abs(speedy);
213   cameraSlideToCurr.x = cameraCurrX;
214   cameraSlideToCurr.y = cameraCurrY;
218 final void cameraReturnToPlayer () {
219   if (!cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y)) {
220     cameraSlideToCurr.x = cameraCurrX;
221     cameraSlideToCurr.y = cameraCurrY;
222     if (cameraSlideToSpeed.x && abs(cameraSlideToSpeed.x) < 8) cameraSlideToSpeed.x = 8;
223     if (cameraSlideToSpeed.y && abs(cameraSlideToSpeed.y) < 8) cameraSlideToSpeed.y = 8;
224     cameraSlideToPlayer = 1;
225   }
229 // if `frameSkip` is `true`, there are more frames waiting
230 // (i.e. you may skip rendering and such)
231 transient void delegate (bool frameSkip) onBeforeFrame;
232 transient void delegate (bool frameSkip) onAfterFrame;
234 transient void delegate () onCameraTeleported;
236 transient void delegate () onLevelExitedCB;
238 // this will be called in-between frames, and
239 // `frameTime` is [0..1)
240 transient void delegate (float frameTime) onInterFrame;
242 final int bizRoomStyle { get { return (lg ? lg.bizRoomStyle : 0); } }
245 final bool isNormalLevel () { return (levelKind == LevelKind.Normal); }
246 final bool isTitleRoom () { return (levelKind == LevelKind.Title); }
247 final bool isTutorialRoom () { return (levelKind == LevelKind.Tutorial); }
248 final bool isTransitionRoom () { return (levelKind == LevelKind.Transition); }
249 final bool isIntroRoom () { return (levelKind == LevelKind.Transition); }
252 bool isHUDEnabled () {
253   if (inWinCutscene) return false;
254   if (inIntroCutscene) return false;
255   if (lg.finalBossLevel) return true;
256   if (isNormalLevel()) return true;
257   return false;
261 // ////////////////////////////////////////////////////////////////////////// //
262 // stats
263 void addDeath (name aname) { if (isNormalLevel()) stats.addDeath(aname); }
265 int starsKills;
266 int sunScore;
267 int moonScore;
268 int moonTimer;
270 void addKill (name aname, optional bool telefrag) {
271        if (isNormalLevel()) stats.addKill(aname, telefrag!optional);
272   else if (aname == 'Shopkeeper' && levelKind == LevelKind.Stars) { ++stats.starsKills; ++starsKills; }
275 void addCollect (name aname, optional int amount) { if (isNormalLevel()) stats.addCollect(aname, amount!optional); }
277 void addDamselSaved () { if (isNormalLevel()) stats.addDamselSaved(); }
278 void addIdolStolen () { if (isNormalLevel()) stats.addIdolStolen(); }
279 void addIdolConverted () { if (isNormalLevel()) stats.addIdolConverted(); }
280 void addCrystalIdolStolen () { if (isNormalLevel()) stats.addCrystalIdolStolen(); }
281 void addCrystalIdolConverted () { if (isNormalLevel()) stats.addCrystalIdolConverted(); }
282 void addGhostSummoned () { if (isNormalLevel()) stats.addGhostSummoned(); }
285 // ////////////////////////////////////////////////////////////////////////// //
286 static final string time2str (int time) {
287   int secs = time%60; time /= 60;
288   int mins = time%60; time /= 60;
289   int hours = time%24; time /= 24;
290   int days = time;
291   if (days) return va("%d DAYS, %d:%02d:%02d", days, hours, mins, secs);
292   if (hours) return va("%d:%02d:%02d", hours, mins, secs);
293   return va("%02d:%02d", mins, secs);
297 // ////////////////////////////////////////////////////////////////////////// //
298 final int tilesWidth () { return lg.levelRoomWidth*RoomGen::Width+2; }
299 final int tilesHeight () { return (lg.finalBossLevel ? 55 : lg.levelRoomHeight*RoomGen::Height+2); }
302 // ////////////////////////////////////////////////////////////////////////// //
303 protected void resetGameInternal () {
304   if (player) player.removeBallAndChain();
305   resetBMCOG = false;
306   inWinCutscene = 0;
307   allowFinalCutsceneSkip = true;
308   //inIntroCutscene = 0;
309   shakeLeft = 0;
310   udjatAlarm = 0;
311   starsKills = 0;
312   sunScore = 0;
313   moonScore = 0;
314   moonTimer = 0;
315   damselSaved = 0;
316   xmoney = 0;
317   collectCounter = 0;
318   levelMoneyStart = 0;
319   if (player) {
320     player.removeBallAndChain();
321     auto hi = player.holdItem;
322     player.holdItem = none;
323     if (hi) hi.instanceRemove();
324     hi = player.pickedItem;
325     player.pickedItem = none;
326     if (hi) hi.instanceRemove();
327   }
328   time = 0;
329   lastRenderTime = -1;
330   levelStartTime = 0;
331   levelEndTime = 0;
332   global.resetGame();
333   stats.clearGameTotals();
334   someTilesRemoved = false;
338 // this won't generate a level yet
339 void restartGame () {
340   resetGameInternal();
341   if (global.startMoney > 0) stats.setMoneyCheat();
342   stats.setMoney(global.startMoney);
343   levelKind = LevelKind.Normal;
347 // complement function to `restart game`
348 void generateNormalLevel () {
349   generateLevel();
350   centerViewAtPlayer();
354 void restartTitle () {
355   resetGameInternal();
356   stats.setMoney(0);
357   createSpecialLevel(LevelKind.Title, &createTitleRoom, 'musTitle');
358   global.plife = 9999;
359   global.bombs = 0;
360   global.rope = 0;
361   global.arrows = 0;
362   global.sgammo = 0;
366 void restartIntro () {
367   resetGameInternal();
368   stats.setMoney(0);
369   createSpecialLevel(LevelKind.Intro, &createIntroRoom, '');
370   global.plife = 9999;
371   global.bombs = 0;
372   global.rope = 1;
373   global.arrows = 0;
374   global.sgammo = 0;
378 void restartTutorial () {
379   resetGameInternal();
380   stats.setMoney(0);
381   createSpecialLevel(LevelKind.Tutorial, &createTutorialRoom, 'musCave');
382   global.plife = 4;
383   global.bombs = 0;
384   global.rope = 4;
385   global.arrows = 0;
386   global.sgammo = 0;
390 void restartScores () {
391   resetGameInternal();
392   stats.setMoney(0);
393   createSpecialLevel(LevelKind.Scores, &createScoresRoom, 'musTitle');
394   global.plife = 4;
395   global.bombs = 0;
396   global.rope = 0;
397   global.arrows = 0;
398   global.sgammo = 0;
402 void restartStarsRoom () {
403   resetGameInternal();
404   stats.setMoney(0);
405   createSpecialLevel(LevelKind.Stars, &createStarsRoom, '');
406   global.plife = 8;
407   global.bombs = 0;
408   global.rope = 0;
409   global.arrows = 0;
410   global.sgammo = 0;
414 void restartSunRoom () {
415   resetGameInternal();
416   stats.setMoney(0);
417   createSpecialLevel(LevelKind.Sun, &createSunRoom, '');
418   global.plife = 8;
419   global.bombs = 0;
420   global.rope = 0;
421   global.arrows = 0;
422   global.sgammo = 0;
426 void restartMoonRoom () {
427   resetGameInternal();
428   stats.setMoney(0);
429   createSpecialLevel(LevelKind.Moon, &createMoonRoom, '');
430   global.plife = 8;
431   global.bombs = 0;
432   global.rope = 0;
433   global.arrows = 100;
434   global.sgammo = 0;
438 // ////////////////////////////////////////////////////////////////////////// //
439 // generate angry shopkeeper at exit if murderer or thief
440 void generateAngryShopkeepers () {
441   if (global.murderer || global.thiefLevel > 0) {
442     foreach (MapTile e; allExits) {
443       if (e.specialExit || !e.isInstanceAlive) continue;
444       auto obj = MonsterShopkeeper(MakeMapObject(e.ix, e.iy, 'oShopkeeper'));
445       if (obj) {
446         obj.style = 'Bounty Hunter';
447         obj.status = MapObject::PATROL;
448       }
449     }
450   }
454 // ////////////////////////////////////////////////////////////////////////// //
455 final void resetRoomBounds () {
456   viewMin.x = 0;
457   viewMin.y = 0;
458   viewMax.x = tilesWidth*16;
459   viewMax.y = tilesHeight*16;
460   // Great Lake is bottomless (nope)
461   //if (global.lake == 1) viewMax.y -= 16;
462   //writeln("ROOM BOUNDS: (", viewMax.x, "x", viewMax.y, ")");
466 final void setRoomBounds (int x0, int y0, int x1, int y1) {
467   viewMin.x = x0;
468   viewMin.y = y0;
469   viewMax.x = x1+16;
470   viewMax.y = y1+16;
474 // ////////////////////////////////////////////////////////////////////////// //
475 struct OSDMessage {
476   string msg;
477   float timeout; // seconds
478   float starttime; // for active
479   bool active; // true: timeout is `GetTickCount()` dismissing time
482 array!OSDMessage msglist; // [0]: current one
484 struct OSDMessageTalk {
485   string msg;
486   float timeout; // seconds;
487   float starttime; // for active
488   bool active; // true: timeout is `GetTickCount()` dismissing time
489   bool shopOnly; // true: timeout when player exited the shop
490   int hiColor1; // -1: default
491   int hiColor2; // -1: default
494 array!OSDMessageTalk msgtalklist; // [0]: current one
497 private final void osdCheckTimeouts () {
498   auto stt = GetTickCount();
499   while (msglist.length) {
500     if (!msglist[0].msg) { msglist.remove(0); continue; }
501     if (!msglist[0].active) {
502       msglist[0].active = true;
503       msglist[0].starttime = stt;
504     }
505     if (msglist[0].starttime+msglist[0].timeout >= stt) break;
506     msglist.remove(0);
507   }
508   if (msgtalklist.length) {
509     bool inshop = isInShop(player.ix/16, player.iy/16);
510     while (msgtalklist.length) {
511       if (!msgtalklist[0].msg) { msgtalklist.remove(0); continue; }
512       if (msgtalklist[0].shopOnly) {
513         if (inshop == msgtalklist[0].active) {
514           msgtalklist[0].active = !inshop;
515           if (!inshop) msgtalklist[0].starttime = stt;
516         }
517       } else {
518         if (!msgtalklist[0].active) {
519           msgtalklist[0].active = true;
520           msgtalklist[0].starttime = stt;
521         }
522       }
523       if (!msgtalklist[0].active) break;
524       //writeln("timedelta: ", msgtalklist[0].starttime+msgtalklist[0].timeout-stt);
525       if (msgtalklist[0].starttime+msgtalklist[0].timeout >= stt) break;
526       msgtalklist.remove(0);
527     }
528   }
532 final bool osdHasMessage () {
533   osdCheckTimeouts();
534   return (msglist.length > 0);
538 final string osdGetMessage (out float timeLeft, out float timeStart) {
539   osdCheckTimeouts();
540   if (msglist.length == 0) { timeLeft = 0; return ""; }
541   auto stt = GetTickCount();
542   timeStart = msglist[0].starttime;
543   timeLeft = fmax(0, msglist[0].starttime+msglist[0].timeout-stt);
544   return msglist[0].msg;
548 final string osdGetTalkMessage (optional out int hiColor1, optional out int hiColor2) {
549   osdCheckTimeouts();
550   if (msgtalklist.length == 0) return "";
551   hiColor1 = msgtalklist[0].hiColor1;
552   hiColor2 = msgtalklist[0].hiColor2;
553   return msgtalklist[0].msg;
557 final void osdClear (optional bool clearTalk) {
558   msglist.clear();
559   if (clearTalk) msgtalklist.clear();
563 final void osdMessage (string msg, optional float timeout, optional bool urgent) {
564   if (!msg) return;
565   msg = global.expandString(msg);
566   if (!specified_timeout) timeout = 3.33;
567   // special message for shops
568   if (timeout == -666) {
569     if (!msg) return;
570     if (msglist.length && msglist[0].msg == msg) return;
571     if (msglist.length == 0 || msglist[0].msg != msg) {
572       osdClear(clearTalk:false);
573       msglist.length += 1;
574       msglist[0].msg = msg;
575     }
576     msglist[0].active = false;
577     msglist[0].timeout = 3.33;
578     osdCheckTimeouts();
579     return;
580   }
581   if (timeout < 0.1) return;
582   timeout = fmax(1.0, timeout);
583   //writeln("OSD: ", msg);
584   // find existing one, and bring it to the top
585   int oldidx = 0;
586   for (; oldidx < msglist.length; ++oldidx) {
587     if (msglist[oldidx].msg == msg) break; // i found her!
588   }
589   // duplicate?
590   if (oldidx < msglist.length) {
591     // yeah, move duplicate to the top
592     msglist[oldidx].timeout = fmax(timeout, msglist[oldidx].timeout);
593     msglist[oldidx].active = false;
594     if (urgent && oldidx != 0) {
595       timeout = msglist[oldidx].timeout;
596       msglist.remove(oldidx);
597       msglist.insert(0);
598       msglist[0].msg = msg;
599       msglist[0].timeout = timeout;
600       msglist[0].active = false;
601     }
602   } else if (urgent) {
603     msglist.insert(0);
604     msglist[0].msg = msg;
605     msglist[0].timeout = timeout;
606     msglist[0].active = false;
607   } else {
608     // new one
609     msglist.length += 1;
610     msglist[$-1].msg = msg;
611     msglist[$-1].timeout = timeout;
612     msglist[$-1].active = false;
613   }
614   osdCheckTimeouts();
618 void osdMessageTalk (string msg, optional bool replace, optional float timeout, optional bool inShopOnly,
619                      optional int hiColor1, optional int hiColor2)
621   //if (!msg) return;
622   //writeln("talk msg: replace=", replace, "; timeout=", timeout, "; inshop=", inShopOnly, "; msg=", msg);
623   if (!specified_timeout) timeout = 3.33;
624   if (!specified_inShopOnly) inShopOnly = true;
625   if (!specified_hiColor1) hiColor1 = -1;
626   if (!specified_hiColor2) hiColor2 = -1;
627   msg = global.expandString(msg);
628   if (replace) {
629     if (!msg) { msgtalklist.clear(); return; }
630     if (msgtalklist.length && msgtalklist[0].msg == msg) {
631       while (msgtalklist.length > 1) msgtalklist.remove(1);
632       msgtalklist[$-1].timeout = timeout;
633       msgtalklist[$-1].shopOnly = inShopOnly;
634     } else {
635       if (msgtalklist.length) msgtalklist.clear();
636       msgtalklist.length += 1;
637       msgtalklist[$-1].msg = msg;
638       msgtalklist[$-1].timeout = timeout;
639       msgtalklist[$-1].active = false;
640       msgtalklist[$-1].shopOnly = inShopOnly;
641       msgtalklist[$-1].hiColor1 = hiColor1;
642       msgtalklist[$-1].hiColor2 = hiColor2;
643     }
644   } else {
645     if (!msg) return;
646     bool found = false;
647     foreach (auto midx, ref auto mnfo; msgtalklist) {
648       if (mnfo.msg == msg) {
649         mnfo.timeout = timeout;
650         mnfo.shopOnly = inShopOnly;
651         found = true;
652       }
653     }
654     if (!found) {
655       msgtalklist.length += 1;
656       msgtalklist[$-1].msg = msg;
657       msgtalklist[$-1].timeout = timeout;
658       msgtalklist[$-1].active = false;
659       msgtalklist[$-1].shopOnly = inShopOnly;
660       msgtalklist[$-1].hiColor1 = hiColor1;
661       msgtalklist[$-1].hiColor2 = hiColor2;
662     }
663   }
664   osdCheckTimeouts();
668 // ////////////////////////////////////////////////////////////////////////// //
669 void setup (GameGlobal aGlobal, SpriteStore aSprStore, BackTileStore aBGTileStore) {
670   global = aGlobal;
671   sprStore = aSprStore;
672   bgtileStore = aBGTileStore;
674   lg = SpawnObject(LevelGen);
675   lg.global = global;
676   lg.level = self;
678   objGrid = SpawnObject(EntityGrid);
679   objGrid.setup(0, 0, MaxTilesWidth*16, MaxTilesHeight*16);
683 // stores should be set
684 void onLoaded () {
685   checkWater = true;
686   liquidTileCount = 0;
687   levBGImg = bgtileStore[levBGImgName];
688   foreach (MapEntity o; objGrid.allObjects()) {
689     o.onLoaded();
690     auto t = MapTile(o);
691     if (t && (t.lava || t.water)) ++liquidTileCount;
692   }
693   for (MapBackTile bt = backtiles; bt; bt = bt.next) bt.onLoaded();
694   if (player) player.onLoaded();
695   //FIXME
696   if (msglist.length) {
697     msglist[0].active = false;
698     msglist[0].timeout = 0.200;
699     osdCheckTimeouts();
700   }
701   lastMusicName = (lg ? lg.musicName : '');
702   global.setMusicPitch(1.0);
703   if (lg && lg.musicName) global.playMusic(lg.musicName); else global.stopMusic();
707 // ////////////////////////////////////////////////////////////////////////// //
708 void pickedSpectacles () {
709   foreach (MapTile t; objGrid.allObjectsSafe(MapTile)) t.onGotSpectacles();
713 // ////////////////////////////////////////////////////////////////////////// //
714 #include "rgentile.vc"
715 #include "rgenobj.vc"
718 void onLevelExited () {
719   if (playerExitDoor isa TitleTileXTitle) {
720     playerExitDoor = none;
721     restartTitle();
722     return;
723   }
724   // title
725   if (isTitleRoom() || levelKind == LevelKind.Scores) {
726     if (playerExitDoor) processTitleExit(playerExitDoor);
727     playerExitDoor = none;
728     return;
729   }
730   if (isTutorialRoom()) {
731     playerExitDoor = none;
732     restartGame();
733     //global.currLevel = 1;
734     //generateNormalLevel();
735     global.currLevel = 0;
736     generateTransitionLevel();
737     return;
738   }
739   // challenges
740   if (levelKind == LevelKind.Stars || levelKind == LevelKind.Sun || levelKind == LevelKind.Moon) {
741     playerExitDoor = none;
742     levelEndTime = time;
743     if (onLevelExitedCB) onLevelExitedCB();
744     restartTitle();
745     return;
746   }
747   // normal level
748   if (isNormalLevel()) {
749     stats.maxLevelComplete = max(stats.maxLevelComplete, global.currLevel);
750     levelEndTime = time;
751     global.genBlackMarket = false;
752     if (playerExitDoor) {
753       if (playerExitDoor.objType == 'oXGold') {
754         writeln("exiting to City Of Gold");
755         global.cityOfGold = -1;
756         //!global.currLevel += 1;
757       } else if (playerExitDoor.objType == 'oXMarket') {
758         writeln("exiting to Black Market");
759         global.genBlackMarket = true;
760         //!global.currLevel += 1;
761       } else {
762         writeln("exit door(", GetClassName(playerExitDoor.Class), "): '", playerExitDoor.objType, "'");
763       }
764     } else {
765       writeln("WTF?! NO EXIT DOOR!");
766     }
767   }
768   if (onLevelExitedCB) onLevelExitedCB();
769   //
770   playerExitDoor = none;
771   if (levelKind == LevelKind.Transition) {
772     if (global.thiefLevel > 0) global.thiefLevel -= 1;
773     if (global.alienCraft) ++global.alienCraft;
774     if (global.yetiLair) ++global.yetiLair;
775     if (global.lake) ++global.lake;
776     if (global.cityOfGold) { if (++global.cityOfGold == 0) global.cityOfGold = 1; }
777     //orig dbg:if (global.currLevel == 1) global.currLevel += firstLevelSkip; else global.currLevel += levelSkip;
778     /+
779     if (!global.blackMarket && !global.cityOfGold /*&& !global.yetiLair*/) {
780       global.currLevel += 1;
781     }
782     +/
783     ++global.currLevel;
784     generateLevel();
785   } else {
786     // < 20 seconds per level: looks like a speedrun
787     global.noDarkLevel = (levelEndTime > levelStartTime && levelEndTime-levelStartTime < 20*30);
788     if (lg.finalBossLevel) {
789       winTime = time;
790       allowFinalCutsceneSkip = (stats.gamesWon != 0);
791       ++stats.gamesWon;
792       // add money for big idol
793       player.addScore(50000);
794       stats.gameOver();
795       startWinCutscene();
796     } else {
797       generateTransitionLevel();
798     }
799   }
800   //centerViewAtPlayer();
804 void onOlmecDead (MapObject o) {
805   writeln("*** OLMEC IS DEAD!");
806   foreach (MapTile t; allExits) {
807     if (t.exit) {
808       t.openExit();
809       auto st = checkTileAtPoint(t.ix+8, t.iy+16);
810       if (!st) {
811         st = MakeMapTile(t.ix/16, t.iy/16+1, 'oTemple');
812         st.ore = 0;
813       }
814       st.invincible = true;
815     }
816   }
820 void generateLevelMessages () {
821   writeln("LEVEL NUMBER: ", global.currLevel);
822   if (global.darkLevel) {
823     if (global.hasCrown) {
824        osdMessage("THE HEDJET SHINES BRIGHTLY.");
825        global.darkLevel = false;
826     } else if (global.config.scumDarkness < 2) {
827       osdMessage("I CAN'T SEE A THING!\nI'D BETTER USE THESE FLARES!");
828     }
829   }
831   if (global.blackMarket) osdMessage("WELCOME TO THE BLACK MARKET!");
833   if (global.cemetary) osdMessage("THE DEAD ARE RESTLESS!");
834   if (global.lake == 1) osdMessage("I CAN HEAR RUSHING WATER...");
836   if (global.snakePit) osdMessage("I HEAR SNAKES... I HATE SNAKES!");
837   if (global.yetiLair == 1) osdMessage("IT SMELLS LIKE WET FUR IN HERE!");
838   if (global.alienCraft == 1) osdMessage("THERE'S A PSYCHIC PRESENCE HERE!");
839   if (global.cityOfGold == 1) {
840     if (true /*hasOlmec()*/) osdMessage("IT'S THE LEGENDARY CITY OF GOLD!");
841   }
843   if (global.sacrificePit) osdMessage("I CAN HEAR PRAYERS TO KALI!");
847 final MapObject debugSpawnObjectWithClass (class!MapObject oclass, optional bool playerDir) {
848   if (!oclass) return none;
849   int dx = 0, dy = 0;
850   bool canLeft = !isSolidAtPoint(player.x0-12, player.yCenter);
851   bool canRight = !isSolidAtPoint(player.x1+12, player.yCenter);
852   if (!canLeft && !canRight) return none;
853   if (canLeft && canRight) {
854     if (playerDir) {
855       dx = (player.dir == MapObject::Dir.Left ? -16 : 16);
856     } else {
857       dx = 16;
858     }
859   } else {
860     dx = (canLeft ? -16 : 16);
861   }
862   auto obj = SpawnMapObjectWithClass(oclass);
863   if (obj isa MapEnemy) {
864     dx -= 8;
865     dy -= (obj isa MonsterDamsel ? 2 : 8);
866   }
867   if (obj) PutSpawnedMapObject(player.ix+dx, player.iy+dy, obj);
868   return obj;
872 final MapObject debugSpawnObject (name aname) {
873   if (!aname) return none;
874   return debugSpawnObjectWithClass(findGameObjectClassByName(aname));
878 void createSpecialLevel (LevelKind kind, scope void delegate () creator, name amusic) {
879   global.darkLevel = false;
880   udjatAlarm = 0;
881   xmoney = 0;
882   collectCounter = 0;
883   global.resetStartingItems();
884   shakeLeft = 0;
885   transitionLevelIndex = 0;
887   global.setMusicPitch(1.0);
888   levelKind = kind;
890   auto olddel = GC_ImmediateDelete;
891   GC_ImmediateDelete = false;
892   clearWholeLevel();
894   creator();
896   setMenuTilesOnTop();
898   fixWallTiles();
899   addBackgroundGfxDetails();
900   //levBGImgName = 'bgCave';
901   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
903   blockWaterChecking = true;
904   fixLiquidTop();
905   cleanDeadTiles();
907   GC_ImmediateDelete = olddel;
908   GC_CollectGarbage(true); // destroy delayed objects too
910   if (dumpGridStats) objGrid.dumpStats();
912   playerExited = false; // just in case
913   playerExitDoor = none;
915   osdClear(clearTalk:true);
917   setupGhostTime();
918   lg.musicName = amusic;
919   lastMusicName = amusic;
920   global.setMusicPitch(1.0);
921   if (amusic) global.playMusic(lg.musicName); else global.stopMusic();
922   someTilesRemoved = false;
926 void createTitleLevel () {
927   createSpecialLevel(LevelKind.Title, &createTitleRoom, 'musTitle');
931 void createTutorialLevel () {
932   createSpecialLevel(LevelKind.Tutorial, &createTutorialRoom, 'musCave');
933   global.plife = 4;
934   global.bombs = 0;
935   global.rope = 4;
936   global.arrows = 0;
937   global.sgammo = 0;
941 // `global.currLevel` is the new level
942 void generateTransitionLevel () {
943   global.darkLevel = false;
944   udjatAlarm = 0;
945   xmoney = 0;
946   collectCounter = 0;
947   shakeLeft = 0;
948   transitionLevelIndex = 0;
950   resetTransitionOverlay();
952   global.setMusicPitch(1.0);
953   switch (global.config.transitionMusicMode) {
954     case GameConfig::MusicMode.Silent: global.stopMusic(); break;
955     case GameConfig::MusicMode.Restart: global.stopMusic(); global.playMusic(lastMusicName); break;
956     case GameConfig::MusicMode.DontTouch: break;
957   }
959   levelKind = LevelKind.Transition;
961   auto olddel = GC_ImmediateDelete;
962   GC_ImmediateDelete = false;
963   clearWholeLevel();
965        if (global.currLevel < 4) { createTrans1Room(); transitionLevelIndex = 0; }
966   else if (global.currLevel == 4) { createTrans1xRoom(); transitionLevelIndex = 1; }
967   else if (global.currLevel < 8) { createTrans2Room(); transitionLevelIndex = 0; }
968   else if (global.currLevel == 8) { createTrans2xRoom(); transitionLevelIndex = 2; }
969   else if (global.currLevel < 12) { createTrans3Room(); transitionLevelIndex = 0; }
970   else if (global.currLevel == 12) { createTrans3xRoom(); transitionLevelIndex = 3; }
971   else if (global.currLevel < 16) { createTrans4Room(); transitionLevelIndex = 0; }
972   else if (global.currLevel == 16) { createTrans4Room(); transitionLevelIndex = 0; }
973   else { createTrans1Room(); transitionLevelIndex = 0; } //???
975   bool createTunnelMan = true;
976   if (global.config.scumUnlocked || global.isTunnelMan) {
977     createTunnelMan = false;
978   } else if (stats.money > 0) {
979     // WARNING! call `stats.needTunnelMan()` only once!
980     createTunnelMan = stats.needTunnelMan(transitionLevelIndex);
981   } else {
982     createTunnelMan = false;
983   }
985   if (!createTunnelMan) {
986     // don't create tunnel man
987     if (/*global.config.bizarre &&*/ global.randOther(1, 3) == 1) {
988            if (global.randOther(1, 3) == 1) MakeMapObject(56+global.randOther(1, 6)*16, 188, 'oRock');
989       else if (global.randOther(1, 3) == 1) MakeMapObject(56+global.randOther(1, 6)*16, 186, 'oJar');
990       else {
991         MakeMapObject(48+global.randOther(1, 6)*16, 176, 'oBones');
992         MakeMapObject(48+global.randOther(1, 6)*16, 188, 'oSkull');
993       }
994     }
995     if (global.config.bizarre && global.randOther(1, 5) == 1) MakeMapObject(16+global.randOther(1, 16)*16, 144, 'oWeb');
996   } else {
997     // create tunnel man
998     MakeMapObject(96+8, 176+8, 'oTunnelMan');
999   }
1002   setMenuTilesOnTop();
1004   fixWallTiles();
1005   addBackgroundGfxDetails();
1006   //levBGImgName = 'bgCave';
1007   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
1009   blockWaterChecking = true;
1010   fixLiquidTop();
1011   cleanDeadTiles();
1013   if (damselSaved > 0) {
1014     // this is special "damsel ready to kiss you" object, not a heart
1015     MakeMapObject(176+8, 176+8, 'oDamselKiss');
1016     global.plife += damselSaved; // if player skipped transition cutscene
1017     damselSaved = 0;
1018   }
1020   GC_ImmediateDelete = olddel;
1021   GC_CollectGarbage(true); // destroy delayed objects too
1023   if (dumpGridStats) objGrid.dumpStats();
1025   playerExited = false; // just in case
1026   playerExitDoor = none;
1028   osdClear(clearTalk:true);
1030   setupGhostTime();
1031   //global.playMusic(lg.musicName);
1032   someTilesRemoved = false;
1036 void generateLevel () {
1037   levelStartTime = time;
1038   levelEndTime = time;
1039   shakeLeft = 0;
1040   transitionLevelIndex = 0;
1042   udjatAlarm = 0;
1043   if (resetBMCOG) {
1044     resetBMCOG = false;
1045     global.genBlackMarket = false;
1046   }
1048   global.setMusicPitch(1.0);
1049   stats.clearLevelTotals();
1051   levelKind = LevelKind.Normal;
1052   lg.generate();
1053   //lg.dump();
1055   resetRoomBounds();
1057   lg.generateRooms();
1058   //writeln("tw:", tilesWidth, "; th:", tilesHeight);
1060   auto olddel = GC_ImmediateDelete;
1061   GC_ImmediateDelete = false;
1062   clearWholeLevel();
1064   if (lg.finalBossLevel) {
1065     blockWaterChecking = true;
1066     createOlmecRoom();
1067   }
1069   // if transition cutscene was skipped...
1070   global.plife += max(0, damselSaved); // if player skipped transition cutscene
1071   damselSaved = 0;
1073   // generate tiles
1074   startRoomX = lg.startRoomX;
1075   startRoomY = lg.startRoomY;
1076   endRoomX = lg.endRoomX;
1077   endRoomY = lg.endRoomY;
1078   addBackgroundGfxDetails();
1079   foreach (int y; 0..tilesHeight) {
1080     foreach (int x; 0..tilesWidth) {
1081       lg.genTileAt(x, y);
1082     }
1083   }
1084   fixWallTiles();
1086   levBGImgName = lg.bgImgName;
1087   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
1089   if (global.allowAngryShopkeepers) generateAngryShopkeepers();
1091   lg.generateEntities();
1093   // add box of flares to dark level
1094   if (global.darkLevel && allEnters.length) {
1095     auto enter = allEnters[0];
1096     int x = enter.ix, y = enter.iy;
1097          if (!isSolidAtPoint(x-16, y)) MakeMapObject(x-16+8, y+8, 'oFlareCrate');
1098     else if (!isSolidAtPoint(x+16, y)) MakeMapObject(x+16+8, y+8, 'oFlareCrate');
1099     else MakeMapObject(x+8, y+8, 'oFlareCrate');
1100   }
1102   //scrGenerateEntities();
1103   //foreach (; 0..2) scrGenerateEntities();
1105   writeln(objGrid.countObjects, " alive objects inserted");
1106   writeln(countBackTiles, " background tiles inserted");
1108   if (!player) FatalError("player pawn is not spawned");
1110   if (lg.finalBossLevel) {
1111     blockWaterChecking = true;
1112   } else {
1113     blockWaterChecking = false;
1114   }
1115   fixLiquidTop();
1116   cleanDeadTiles();
1118   GC_ImmediateDelete = olddel;
1119   GC_CollectGarbage(true); // destroy delayed objects too
1121   if (dumpGridStats) objGrid.dumpStats();
1123   playerExited = false; // just in case
1124   playerExitDoor = none;
1126   levelMoneyStart = stats.money;
1128   osdClear(clearTalk:true);
1129   generateLevelMessages();
1131   xmoney = 0;
1132   collectCounter = 0;
1134   //writeln("!<", lastMusicName, ">:<", lg.musicName, ">");
1135   global.setMusicPitch(1.0);
1136   if (lastMusicName != lg.musicName) {
1137     global.playMusic(lg.musicName);
1138   } else {
1139     writeln("MM: ", global.config.nextLevelMusicMode);
1140     switch (global.config.nextLevelMusicMode) {
1141       case GameConfig::MusicMode.Silent: global.stopMusic(); break; // the thing that should not be
1142       case GameConfig::MusicMode.Restart: global.stopMusic(); global.playMusic(lg.musicName); break;
1143       case GameConfig::MusicMode.DontTouch:
1144         if (global.config.transitionMusicMode == GameConfig::MusicMode.Silent) {
1145           global.playMusic(lg.musicName);
1146         }
1147         break;
1148     }
1149   }
1150   lastMusicName = lg.musicName;
1151   //global.playMusic(lg.musicName);
1153   setupGhostTime();
1154   if (global.cityOfGold == 1 || global.genBlackMarket) resetBMCOG = true;
1156   if (global.cityOfGold == 1) {
1157     lg.mapSprite = 'sMapTemple';
1158     lg.mapTitle = "City of Gold";
1159   } else if (global.blackMarket) {
1160     lg.mapSprite = 'sMapJungle';
1161     lg.mapTitle = "Black Market";
1162   }
1164   someTilesRemoved = false;
1168 // ////////////////////////////////////////////////////////////////////////// //
1169 int currKeys, nextKeys;
1170 int pressedKeysQ, releasedKeysQ;
1171 int keysPressed, keysReleased = -1;
1174 struct SavedKeyState {
1175   int currKeys, nextKeys;
1176   int pressedKeysQ, releasedKeysQ;
1177   int keysPressed, keysReleased;
1178   // for session
1179   int roomSeed, otherSeed;
1183 // for saving/replaying
1184 final void keysSaveState (out SavedKeyState ks) {
1185   ks.currKeys = currKeys;
1186   ks.nextKeys = nextKeys;
1187   ks.pressedKeysQ = pressedKeysQ;
1188   ks.releasedKeysQ = releasedKeysQ;
1189   ks.keysPressed = keysPressed;
1190   ks.keysReleased = keysReleased;
1193 // for saving/replaying
1194 final void keysRestoreState (const ref SavedKeyState ks) {
1195   currKeys = ks.currKeys;
1196   nextKeys = ks.nextKeys;
1197   pressedKeysQ = ks.pressedKeysQ;
1198   releasedKeysQ = ks.releasedKeysQ;
1199   keysPressed = ks.keysPressed;
1200   keysReleased = ks.keysReleased;
1204 final void keysNextFrame () {
1205   currKeys = nextKeys;
1209 final void clearKeys () {
1210   currKeys = 0;
1211   nextKeys = 0;
1212   pressedKeysQ = 0;
1213   releasedKeysQ = 0;
1214   keysPressed = 0;
1215   keysReleased = -1;
1219 final void onKey (int code, bool down) {
1220   if (!code) return;
1221   if (down) {
1222     currKeys |= code;
1223     nextKeys |= code;
1224     if (keysReleased&code) {
1225       keysPressed |= code;
1226       keysReleased &= ~code;
1227       pressedKeysQ |= code;
1228     }
1229   } else {
1230     nextKeys &= ~code;
1231     if (keysPressed&code) {
1232       keysReleased |= code;
1233       keysPressed &= ~code;
1234       releasedKeysQ |= code;
1235     }
1236   }
1239 final bool isKeyDown (int code) {
1240   return !!(currKeys&code);
1243 final bool isKeyPressed (int code) {
1244   bool res = !!(pressedKeysQ&code);
1245   pressedKeysQ &= ~code;
1246   return res;
1249 final bool isKeyReleased (int code) {
1250   bool res = !!(releasedKeysQ&code);
1251   releasedKeysQ &= ~code;
1252   return res;
1256 final void clearKeysPressRelease () {
1257   keysPressed = default.keysPressed;
1258   keysReleased = default.keysReleased;
1259   pressedKeysQ = default.pressedKeysQ;
1260   releasedKeysQ = default.releasedKeysQ;
1261   currKeys = 0;
1262   nextKeys = 0;
1266 // ////////////////////////////////////////////////////////////////////////// //
1267 final void registerEnter (MapTile t) {
1268   if (!t) return;
1269   allEnters[$] = t;
1270   return;
1274 final void registerExit (MapTile t) {
1275   if (!t) return;
1276   allExits[$] = t;
1277   return;
1281 final bool isYAtEntranceRow (int py) {
1282   py /= 16;
1283   foreach (MapTile t; allEnters) if (t.iy == py) return true;
1284   return false;
1288 final int calcNearestEnterDist (int px, int py) {
1289   if (allEnters.length == 0) return int.max;
1290   int curdistsq = int.max;
1291   foreach (MapTile t; allEnters) {
1292     int xc = px-t.xCenter, yc = py-t.yCenter;
1293     int distsq = xc*xc+yc*yc;
1294     if (distsq < curdistsq) curdistsq = distsq;
1295   }
1296   return roundi(sqrt(curdistsq));
1300 final int calcNearestExitDist (int px, int py) {
1301   if (allExits.length == 0) return int.max;
1302   int curdistsq = int.max;
1303   foreach (MapTile t; allExits) {
1304     int xc = px-t.xCenter, yc = py-t.yCenter;
1305     int distsq = xc*xc+yc*yc;
1306     if (distsq < curdistsq) curdistsq = distsq;
1307   }
1308   return roundi(sqrt(curdistsq));
1312 // ////////////////////////////////////////////////////////////////////////// //
1313 final void clearForTransition () {
1314   auto olddel = GC_ImmediateDelete;
1315   GC_ImmediateDelete = false;
1316   clearWholeLevel();
1317   GC_ImmediateDelete = olddel;
1318   GC_CollectGarbage(true); // destroy delayed objects too
1319   global.darkLevel = false;
1323 // ////////////////////////////////////////////////////////////////////////// //
1324 final int countBackTiles () {
1325   int res = 0;
1326   for (MapBackTile bt = backtiles; bt; bt = bt.next) ++res;
1327   return res;
1331 final void clearWholeLevel () {
1332   allEnters.clear();
1333   allExits.clear();
1334   clearActiveEntities();
1336   // don't kill objects the player is holding
1337   if (player) {
1338     if (player.pickedItem isa ItemBall) {
1339       player.pickedItem.instanceRemove();
1340       player.pickedItem = none;
1341     }
1342     if (player.pickedItem && player.pickedItem.grid) {
1343       player.pickedItem.grid.remove(player.pickedItem.gridId);
1344       writeln("secured pocket item '", GetClassName(player.pickedItem.Class), "'");
1345     }
1346     if (player.holdItem isa ItemBall) {
1347       player.removeBallAndChain(temp:true);
1348       if (player.holdItem) player.holdItem.instanceRemove();
1349       player.holdItem = none;
1350     }
1351     if (player.holdItem && player.holdItem.grid) {
1352       player.holdItem.grid.remove(player.holdItem.gridId);
1353       writeln("secured held item '", GetClassName(player.holdItem.Class), "'");
1354     }
1355     writeln("secured ball; mustBeChained=", player.mustBeChained, "; wasHoldingBall=", player.wasHoldingBall);
1356   }
1358   int count = objGrid.countObjects();
1359   if (dumpGridStats) { if (objGrid.getFirstObjectCID()) objGrid.dumpStats(); }
1360   objGrid.removeAllObjects(true); // and destroy
1361   if (count > 0) writeln(count, " objects destroyed");
1363   lastUsedObjectId = 0;
1364   accumTime = 0;
1365   //!time = 0;
1366   lastRenderTime = -1;
1367   liquidTileCount = 0;
1368   checkWater = false;
1370   while (backtiles) {
1371     MapBackTile t = backtiles;
1372     backtiles = t.next;
1373     delete t;
1374   }
1376   levBGImg = none;
1377   framesProcessedFromLastClear = 0;
1381 final void insertObject (MapEntity o) {
1382   if (!o) return;
1383   if (o.grid) FatalError("cannot put object into level twice");
1384   objGrid.insert(o);
1385   if (o.active || o isa MapObject) addActiveEntity(o);
1389 final void reinsertObject (MapEntity o) {
1390   if (!o || !o.isInstanceAlive) return;
1391   if (o.grid) o.grid.remove(o.gridId);
1392   objGrid.insert(o);
1393   if (o.active || o isa MapObject) addActiveEntity(o);
1397 final void spawnPlayerAt (int x, int y) {
1398   // if we have no player, spawn new one
1399   // otherwise this just a level transition, so simply reposition him
1400   if (!player) {
1401     // don't add player to object list, as it has very separate processing anyway
1402     player = SpawnObject(PlayerPawn);
1403     player.global = global;
1404     player.level = self;
1405     if (!player.initialize()) {
1406       delete player;
1407       FatalError("something is wrong with player initialization");
1408       return;
1409     }
1410   }
1411   player.fltx = x;
1412   player.flty = y;
1413   player.saveInterpData();
1414   player.resurrect();
1415   if (player.mustBeChained || global.config.scumBallAndChain) {
1416     writeln("*** spawning ball and chain");
1417     player.spawnBallAndChain(levelStart:true);
1418   }
1419   playerExited = false;
1420   playerExitDoor = none;
1421   if (global.config.startWithKapala) global.hasKapala = true;
1422   centerViewAtPlayer();
1423   // reinsert player items into grid
1424   if (player.pickedItem) reinsertObject(player.pickedItem);
1425   if (player.holdItem) reinsertObject(player.holdItem);
1426   //writeln("player spawned; active=", player.active);
1427   player.scrSwitchToPocketItem(forceIfEmpty:false);
1431 final void teleportPlayerTo (int x, int y) {
1432   if (player) {
1433     player.fltx = x;
1434     player.flty = y;
1435     player.saveInterpData();
1436   }
1440 final void resurrectPlayer () {
1441   if (player) player.resurrect();
1442   playerExited = false;
1443   playerExitDoor = none;
1447 // ////////////////////////////////////////////////////////////////////////// //
1448 final void scrShake (int duration) {
1449   if (shakeLeft == 0) {
1450     shakeOfs.x = 0;
1451     shakeOfs.y = 0;
1452     shakeDir.x = 0;
1453     shakeDir.y = 0;
1454   }
1455   shakeLeft = max(shakeLeft, duration);
1460 // ////////////////////////////////////////////////////////////////////////// //
1461 enum SCAnger {
1462   TileDestroyed,
1463   ItemStolen, // including damsel, lol
1464   CrapsCheated,
1465   BombDropped,
1466   DamselWhipped,
1469 // checks for dead, agnered, distance, etc. should be already done
1470 protected void doAngerShopkeeper (MonsterShopkeeper shp, SCAnger reason, ref bool messaged,
1471                                   int maxdist, MapEntity offender)
1473   if (!shp || shp.dead || shp.angered) return;
1474   if (offender.distanceToEntityCenter(shp) > maxdist) return;
1476   shp.status = MapObject::ATTACK;
1477   string msg;
1478   if (global.murderer) {
1479     msg = "~YOU'LL PAY FOR YOUR CRIMES!~";
1480   } else {
1481     switch (reason) {
1482       case SCAnger.TileDestroyed: msg = "~DIE, YOU VANDAL!~"; break;
1483       case SCAnger.ItemStolen: msg = "~COME BACK HERE, THIEF!~"; break;
1484       case SCAnger.CrapsCheated: msg = "~DIE, CHEATER!~"; break;
1485       case SCAnger.BombDropped: msg = "~TERRORIST!~"; break;
1486       case SCAnger.DamselWhipped: msg = "~HEY, ONLY I CAN DO THAT!~"; break;
1487       default: "~NOW I'M REALLY STEAMED!~"; break;
1488     }
1489   }
1491   writeln("shopkeeper angered; reason=", reason, "; maxdist=", maxdist, "; msg=\"", msg, "\"");
1492   if (!messaged) {
1493     messaged = true;
1494     if (msg) osdMessageTalk(msg, replace:true, inShopOnly:false, hiColor1:0xff_00_00);
1495     global.thiefLevel += (global.thiefLevel > 0 ? 3 : 2);
1496   }
1500 // make the nearest shopkeeper angry. RAWR!
1501 void scrShopkeeperAnger (SCAnger reason, optional int maxdist, optional MapEntity offender) {
1502   bool messaged = false;
1503   maxdist = clamp(maxdist, 96, 100000);
1504   if (!offender) offender = player;
1505   if (maxdist == 100000) {
1506     foreach (MonsterShopkeeper shp; objGrid.allObjects(MonsterShopkeeper)) {
1507       doAngerShopkeeper(shp, reason, messaged, maxdist, offender);
1508     }
1509   } else {
1510     foreach (MonsterShopkeeper shp; objGrid.inRectPix(offender.xCenter-maxdist-128, offender.yCenter-maxdist-128, (maxdist+128)*2, (maxdist+128)*2, precise:false, castClass:MonsterShopkeeper)) {
1511       doAngerShopkeeper(shp, reason, messaged, maxdist, offender);
1512     }
1513   }
1517 final MapObject findCrapsPrize () {
1518   foreach (MapObject o; objGrid.allObjects(MapObject)) {
1519     if (!o.spectral && o.inDiceHouse) return o;
1520   }
1521   return none;
1525 // ////////////////////////////////////////////////////////////////////////// //
1526 // 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.
1527 // note: idols moved by monkeys will have false `stolenIdol`
1528 void scrTriggerIdolAltar (bool stolenIdol) {
1529   ObjTikiCurse res = none;
1530   int curdistsq = int.max;
1531   int px = player.xCenter, py = player.yCenter;
1532   foreach (MapObject o; objGrid.allObjects(MapObject)) {
1533     auto tcr = ObjTikiCurse(o);
1534     if (!tcr) continue;
1535     if (tcr.activated) continue;
1536     int xc = px-tcr.xCenter, yc = py-tcr.yCenter;
1537     int distsq = xc*xc+yc*yc;
1538     if (distsq < curdistsq) {
1539       res = tcr;
1540       curdistsq = distsq;
1541     }
1542   }
1543   if (res) res.activate(stolenIdol);
1547 // ////////////////////////////////////////////////////////////////////////// //
1548 void setupGhostTime () {
1549   musicFadeTimer = -1;
1550   ghostSpawned = false;
1552   // there is no ghost on the first level
1553   if (inWinCutscene || inIntroCutscene || !isNormalLevel() || lg.finalBossLevel ||
1554       (!global.config.ghostAtFirstLevel && global.currLevel == 1))
1555   {
1556     ghostTimeLeft = -1;
1557     global.setMusicPitch(1.0);
1558     return;
1559   }
1561   if (global.config.scumGhost < 0) {
1562     // instant
1563     ghostTimeLeft = 1;
1564     osdMessage("A GHASTLY VOICE CALLS YOUR NAME!\nLET'S GET OUT OF HERE!", 6.66, urgent:true);
1565     return;
1566   }
1568   if (global.config.scumGhost == 0) {
1569     // never
1570     ghostTimeLeft = -1;
1571     return;
1572   }
1574   // randomizes time until ghost appears once time limit is reached
1575   // global.ghostRandom (true/false) if extra time is randomized - loaded from yasm.cfg. if false, use 30 second default
1576   // ghostTimeLeft (time in seconds * 1000) for currently generated level
1578   if (global.config.ghostRandom) {
1579     auto tMin = global.randOther(10, 30); // min(global.plife*10, 30);
1580     auto tMax = max(tMin+global.randOther(10, 30), min(global.config.scumGhost, 300)); // 5 minutes max
1581     auto tTime = global.randOther(tMin, tMax);
1582     if (tTime <= 0) tTime = roundi(tMax/2.0);
1583     ghostTimeLeft = tTime;
1584   } else {
1585     ghostTimeLeft = global.config.scumGhost; // 5 minutes max
1586   }
1588   ghostTimeLeft += max(0, global.config.ghostExtraTime);
1590   ghostTimeLeft *= 30; // seconds -> frames
1591   //global.ghostShowTime
1595 void spawnGhost () {
1596   addGhostSummoned();
1597   ghostSpawned = true;
1598   ghostTimeLeft = -1;
1600   int vwdt = (viewMax.x-viewMin.x);
1601   int vhgt = (viewMax.y-viewMin.y);
1603   int gx, gy;
1605   if (player.ix < viewMin.x+vwdt/2) {
1606     // player is in the left side
1607     gx = viewMin.x+vwdt/2+vwdt/4;
1608   } else {
1609     // player is in the right side
1610     gx = viewMin.x+vwdt/4;
1611   }
1613   if (player.iy < viewMin.y+vhgt/2) {
1614     // player is in the left side
1615     gy = viewMin.y+vhgt/2+vhgt/4;
1616   } else {
1617     // player is in the right side
1618     gy = viewMin.y+vhgt/4;
1619   }
1621   writeln("spawning ghost at tile (", gx/16, ",", gy/16, "); player is at tile (", player.ix/16, ",", player.iy/16, ")");
1623   MakeMapObject(gx, gy, 'oGhost');
1625   /*
1626     if (oPlayer1.x &gt; room_width/2) instance_create(view_xview[0]+view_wview[0]+8, view_yview[0]+ffloor(view_hview[0] / 2), oGhost);
1627     else instance_create(view_xview[0]-32, view_yview[0]+ffloor(view_hview[0]/2), oGhost);
1628     global.ghostExists = true;
1629   */
1633 void thinkFrameGameGhost () {
1634   if (player.dead) return;
1635   if (!isNormalLevel()) return; // just in case
1637   if (ghostTimeLeft < 0) {
1638     // turned off
1639     if (musicFadeTimer > 0) {
1640       musicFadeTimer = -1;
1641       global.setMusicPitch(1.0);
1642     }
1643     return;
1644   }
1646   if (musicFadeTimer >= 0) {
1647     ++musicFadeTimer;
1648     if (musicFadeTimer > 0 && musicFadeTimer <= 100*3) {
1649       float pitch = 1.0-(0.00226/3.0)*musicFadeTimer;
1650       //writeln("MFT: ", musicFadeTimer, "; pitch=", pitch);
1651       global.setMusicPitch(pitch);
1652     }
1653   }
1655   if (ghostTimeLeft == 0) {
1656     // she is already here!
1657     return;
1658   }
1660   // no ghost if we have a crown
1661   if (global.hasCrown) {
1662     ghostTimeLeft = -1;
1663     return;
1664   }
1666   // if she was already spawned, don't do it again
1667   if (ghostSpawned) {
1668     ghostTimeLeft = 0;
1669     return;
1670   }
1672   if (--ghostTimeLeft != 0) {
1673     // warning
1674     if (global.config.ghostExtraTime > 0) {
1675       if (ghostTimeLeft == global.config.ghostExtraTime*30) {
1676         osdMessage("A CHILL RUNS UP YOUR SPINE...\nLET'S GET OUT OF HERE!", 6.66, urgent:true);
1677       }
1678       if (musicFadeTimer < 0 && ghostTimeLeft <= global.config.ghostExtraTime*30) {
1679         musicFadeTimer = 0;
1680       }
1681     }
1682     return;
1683   }
1685   // spawn her
1686   if (player.isExitingSprite) {
1687     // no reason to spawn her, we're leaving
1688     ghostTimeLeft = -1;
1689     return;
1690   }
1692   spawnGhost();
1696 void thinkFrameGame () {
1697   thinkFrameGameGhost();
1698   // udjat eye blinking
1699   if (global.hasUdjatEye && player) {
1700     foreach (MapTile t; allExits) {
1701       if (t isa MapTileBlackMarketDoor) {
1702         auto dm = int(player.distanceToEntityCenter(t));
1703         if (dm < 4) dm = 4;
1704         if (udjatAlarm < 1 || dm < udjatAlarm) udjatAlarm = dm;
1705       }
1706     }
1707   } else {
1708     global.udjatBlink = false;
1709     udjatAlarm = 0;
1710   }
1711   if (udjatAlarm > 0) {
1712     if (--udjatAlarm == 0) {
1713       global.udjatBlink = !global.udjatBlink;
1714       if (global.hasUdjatEye && player) {
1715         player.playSound(global.udjatBlink ? 'sndBlink1' : 'sndBlink2');
1716       }
1717     }
1718   }
1719   switch (levelKind) {
1720     case LevelKind.Stars: thinkFrameGameStars(); break;
1721     case LevelKind.Sun: thinkFrameGameSun(); break;
1722     case LevelKind.Moon: thinkFrameGameMoon(); break;
1723     case LevelKind.Transition: thinkFrameTransition(); break;
1724     case LevelKind.Intro: thinkFrameIntro(); break;
1725   }
1729 // ////////////////////////////////////////////////////////////////////////// //
1730 private final bool isWaterTileCB (MapTile t) {
1731   return (t && t.visible && t.water);
1735 private final bool isLavaTileCB (MapTile t) {
1736   return (t && t.visible && t.lava);
1740 // ////////////////////////////////////////////////////////////////////////// //
1741 const int GreatLakeStartTileY = 28;
1744 final void fillGreatLake () {
1745   if (global.lake == 1) {
1746     foreach (int y; GreatLakeStartTileY..tilesHeight) {
1747       foreach (int x; 0..tilesWidth) {
1748         auto t = checkTileAtPoint(x*16, y*16, delegate bool (MapTile t) {
1749           if (t.spectral || !t.visible || t.invisible || t.moveable) return false;
1750           return true;
1751         });
1752         if (!t) {
1753           t = MakeMapTile(x, y, 'oWaterSwim');
1754           if (!t) continue;
1755         }
1756         if (t.water) {
1757           t.setSprite(y == GreatLakeStartTileY ? 'sWaterTop' : 'sWater');
1758         } else if (t.lava) {
1759           t.setSprite(y == GreatLakeStartTileY ? 'sLavaTop' : 'sLava');
1760         }
1761       }
1762     }
1763   }
1767 // called once after level generation
1768 final void fixLiquidTop () {
1769   if (global.lake == 1) fillGreatLake();
1771   liquidTileCount = 0;
1772   foreach (MapTile t; objGrid.allObjects(MapTile)) {
1773     if (!t.water && !t.lava) continue;
1775     ++liquidTileCount;
1776     //writeln("fixing water tile(", GetClassName(t.Class), "):'", t.objName, "' (water=", t.water, "; lava=", t.lava, "); lqc=", liquidTileCount);
1778     //if (global.lake == 1) continue; // it is done in `fillGreatLake()`
1780     if (!checkTileAtPoint(t.ix+8, t.iy-8, (t.lava ? &isLavaTileCB : &isWaterTileCB))) {
1781       t.setSprite(t.lava ? 'sLavaTop' : 'sWaterTop');
1782     } else {
1783       // don't do this, it will destroy seaweed
1784       //t.setSprite(t.lava ? 'sLava' : 'sWater');
1785       auto spr = t.getSprite();
1786            if (!spr) t.setSprite(t.lava ? 'sLava' : 'sWater');
1787       else if (spr.Name == 'sLavaTop') t.setSprite('sLava');
1788       else if (spr.Name == 'sWaterTop') t.setSprite('sWater');
1789     }
1790   }
1791   //writeln("liquid tiles count: ", liquidTileCount);
1795 // ////////////////////////////////////////////////////////////////////////// //
1796 transient MapTile curWaterTile;
1797 transient bool curWaterTileCheckHitsLava;
1798 transient bool curWaterTileCheckHitsSolidOrWater; // only for `checkWaterOrSolidTilePartialCB`
1799 transient int curWaterTileLastHDir;
1800 transient ubyte[16, 16] curWaterOccupied;
1801 transient int curWaterOccupiedCount;
1802 transient int curWaterTileCheckX0, curWaterTileCheckY0;
1805 private final void clearCurWaterCheckState () {
1806   curWaterTileCheckHitsLava = false;
1807   curWaterOccupiedCount = 0;
1808   foreach (auto idx; 0..16*16) curWaterOccupied[idx] = 0;
1812 private final bool checkWaterOrSolidTileCB (MapTile t) {
1813   if (t == curWaterTile) return false;
1814   if (t.lava && curWaterTile.water) {
1815     curWaterTileCheckHitsLava = true;
1816     return true;
1817   }
1818   if (t.ix%16 != 0 || t.iy%16 != 0) {
1819     if (t.water || t.solid) {
1820       // fill occupied array
1821       //FIXME: optimize this
1822       if (curWaterOccupiedCount < 16*16) {
1823         foreach (auto dy; t.y0..t.y1+1) {
1824           foreach (auto dx; t.x0..t.x1+1) {
1825             int sx = dx-curWaterTileCheckX0;
1826             int sy = dy-curWaterTileCheckY0;
1827             if (sx >= 0 && sx <= 16 && sy >= 0 && sy <= 15 && !curWaterOccupied[sx, sy]) {
1828               curWaterOccupied[sx, sy] = 1;
1829               ++curWaterOccupiedCount;
1830             }
1831           }
1832         }
1833       }
1834     }
1835     return false; // need to check for lava
1836   }
1837   if (t.water || t.solid || t.lava) {
1838     curWaterOccupiedCount = 16*16;
1839     if (t.water && curWaterTile.lava) t.instanceRemove();
1840   }
1841   return false; // need to check for lava
1845 private final bool checkWaterOrSolidTilePartialCB (MapTile t) {
1846   if (t == curWaterTile) return false;
1847   if (t.lava && curWaterTile.water) {
1848     //writeln("!!!!!!!!");
1849     curWaterTileCheckHitsLava = true;
1850     return true;
1851   }
1852   if (t.water || t.solid || t.lava) {
1853     //writeln("*********");
1854     curWaterTileCheckHitsSolidOrWater = true;
1855     if (t.water && curWaterTile.lava) t.instanceRemove();
1856   }
1857   return false; // need to check for lava
1861 private final bool isFullyOccupiedAtTilePos (int tileX, int tileY) {
1862   clearCurWaterCheckState();
1863   curWaterTileCheckX0 = tileX*16;
1864   curWaterTileCheckY0 = tileY*16;
1865   checkTilesInRect(tileX*16, tileY*16, 16, 16, &checkWaterOrSolidTileCB);
1866   return (curWaterTileCheckHitsLava || curWaterOccupiedCount == 16*16);
1870 private final bool isAtLeastPartiallyOccupiedAtTilePos (int tileX, int tileY) {
1871   curWaterTileCheckHitsLava = false;
1872   curWaterTileCheckHitsSolidOrWater = false;
1873   checkTilesInRect(tileX*16, tileY*16, 16, 16, &checkWaterOrSolidTilePartialCB);
1874   return (curWaterTileCheckHitsSolidOrWater || curWaterTileCheckHitsLava);
1878 private final bool waterCanReachGroundHoleInDir (MapTile wtile, int dx) {
1879   if (dx == 0) return false; // just in case
1880   dx = sign(dx);
1881   int x = wtile.ix/16, y = wtile.iy/16;
1882   x += dx;
1883   while (x >= 0 && x < tilesWidth) {
1884     if (!isAtLeastPartiallyOccupiedAtTilePos(x, y+1)) return true;
1885     if (isAtLeastPartiallyOccupiedAtTilePos(x, y)) return false;
1886     x += dx;
1887   }
1888   return false;
1892 // returns `true` if this tile must be removed
1893 private final bool checkWaterFlow (MapTile wtile) {
1894   if (global.lake == 1) {
1895     if (wtile.iy >= GreatLakeStartTileY*16) return false; // lake tile, don't touch
1896     if (wtile.iy >= GreatLakeStartTileY*16-16) return true; // remove it, so it won't stack on a lake
1897   }
1899   if (wtile.ix%16 != 0 || wtile.iy%16 != 0) return true; // sanity check
1901   curWaterTile = wtile;
1902   curWaterTileLastHDir = 0; // never moved to the side
1904   bool wasMoved = false;
1906   for (;;) {
1907     int tileX = wtile.ix/16, tileY = wtile.iy/16;
1909     // out of level?
1910     if (tileY >= tilesHeight) return true;
1912     // check if we can fall down
1913     auto canFall = !isAtLeastPartiallyOccupiedAtTilePos(tileX, tileY+1);
1914     // disappear if can fall in lava
1915     if (wtile.water && curWaterTileCheckHitsLava) {
1916       //!writeln(wtile.objId, ": LAVA HIT DOWN");
1917       return true;
1918     }
1919     if (wasMoved) {
1920       // fake, so caller will not start removing tiles
1921       if (canFall) wtile.waterMovedDown = true;
1922       break;
1923     }
1924     // can move down?
1925     if (canFall) {
1926       // move down
1927       //!writeln(wtile.objId, ": GOING DOWN");
1928       curWaterTileLastHDir = 0;
1929       wtile.iy = wtile.iy+16;
1930       wasMoved = true;
1931       wtile.waterMovedDown = true;
1932       continue;
1933     }
1935     bool canMoveLeft = (curWaterTileLastHDir > 0 ? false : !isAtLeastPartiallyOccupiedAtTilePos(tileX-1, tileY));
1936     // disappear if near lava
1937     if (wtile.water && curWaterTileCheckHitsLava) {
1938       //!writeln(wtile.objId, ": LAVA HIT LEFT");
1939       return true;
1940     }
1942     bool canMoveRight = (curWaterTileLastHDir < 0 ? false : !isAtLeastPartiallyOccupiedAtTilePos(tileX+1, tileY));
1943     // disappear if near lava
1944     if (wtile.water && curWaterTileCheckHitsLava) {
1945       //!writeln(wtile.objId, ": LAVA HIT RIGHT");
1946       return true;
1947     }
1949     if (!canMoveLeft && !canMoveRight) {
1950       // do final checks
1951       //!if (wasMove) writeln(wtile.objId, ": NO MORE MOVES");
1952       break;
1953     }
1955     if (canMoveLeft && canMoveRight) {
1956       // choose random direction
1957       //!writeln(wtile.objId, ": CHOOSING RANDOM HDIR");
1958       // actually, choose direction that leads to hole in a ground
1959       if (waterCanReachGroundHoleInDir(wtile, -1)) {
1960         // can reach hole at the left side
1961         if (waterCanReachGroundHoleInDir(wtile, 1)) {
1962           // can reach hole at the right side, choose at random
1963           if (global.randOther(0, 1)) canMoveRight = false; else canMoveLeft = false;
1964         } else {
1965           // move left
1966           canMoveRight = false;
1967         }
1968       } else {
1969         // can't reach hole at the left side
1970         if (waterCanReachGroundHoleInDir(wtile, 1)) {
1971           // can reach hole at the right side, choose at random
1972           canMoveLeft = false;
1973         } else {
1974           // no holes at any side, choose at random
1975           if (global.randOther(0, 1)) canMoveRight = false; else canMoveLeft = false;
1976         }
1977       }
1978     }
1980     // move
1981     if (canMoveLeft) {
1982       if (canMoveRight) FatalError("WATERCHECK: WTF RIGHT");
1983       //!writeln(wtile.objId, ": MOVING LEFT (", curWaterTileLastHDir, ")");
1984       curWaterTileLastHDir = -1;
1985       wtile.ix = wtile.ix-16;
1986     } else if (canMoveRight) {
1987       if (canMoveLeft) FatalError("WATERCHECK: WTF LEFT");
1988       //!writeln(wtile.objId, ": MOVING RIGHT (", curWaterTileLastHDir, ")");
1989       curWaterTileLastHDir = 1;
1990       wtile.ix = wtile.ix+16;
1991     }
1992     wasMoved = true;
1993   }
1995   // remove seaweeds
1996   if (wasMoved) {
1997     checkWater = true;
1998     wtile.setSprite(wtile.lava ? 'sLava' : 'sWater');
1999     wtile.waterMoved = true;
2000     // if this tile was not moved down, check if it can move down on any next step
2001     if (!wtile.waterMovedDown) {
2002            if (waterCanReachGroundHoleInDir(wtile, -1)) wtile.waterMovedDown = true;
2003       else if (waterCanReachGroundHoleInDir(wtile, 1)) wtile.waterMovedDown = true;
2004     }
2005   }
2007   return false; // don't remove
2009   //if (!isWetTileAtPix(tileX*16+8, tileY*16-8)) wtile.setSprite(wtile.lava ? 'sLavaTop' : 'sWaterTop');
2013 transient array!MapTile waterTilesList;
2015 final int sortWaterTilesByCoordsCmp (MapTile a, MapTile b) {
2016   int dy = a.iy-b.iy;
2017   if (dy) return dy;
2018   return (a.ix-b.ix);
2021 transient int waterFlowPause = 0;
2022 transient bool debugWaterFlowPause = false;
2024 final void cleanDeadObjects () {
2025   // remove dead objects
2026   if (deadItemsHead) {
2027     auto olddel = GC_ImmediateDelete;
2028     GC_ImmediateDelete = false;
2029     do {
2030       auto it = deadItemsHead;
2031       deadItemsHead = it.deadItemsNext;
2032       if (!someTilesRemoved && it isa MapTile) someTilesRemoved = true;
2033       if (it.grid) it.grid.remove(it.gridId);
2034       it.onDestroy();
2035       removeActiveEntity(it);
2036       delete it;
2037     } while (deadItemsHead);
2038     GC_ImmediateDelete = olddel;
2039     if (olddel) GC_CollectGarbage(true); // destroy delayed objects too
2040   }
2043 final void cleanDeadTiles () {
2044   if (checkWater && /*global.lake == 1 ||*/ (!blockWaterChecking && liquidTileCount)) {
2045     if (global.lake == 1) fillGreatLake();
2046     if (waterFlowPause > 1) {
2047       --waterFlowPause;
2048       cleanDeadObjects();
2049       return;
2050     }
2051     if (debugWaterFlowPause) waterFlowPause = 4;
2052     //writeln("checking water");
2053     waterTilesList.clear();
2054     foreach (MapTile wtile; objGrid.allObjectsSafe(MapTile)) {
2055       if (wtile.water || wtile.lava) {
2056         // sanity check
2057         if (wtile.ix%16 == 0 && wtile.iy%16 == 0) {
2058           wtile.waterMoved = false;
2059           wtile.waterMovedDown = false;
2060           wtile.waterSlideOldX = wtile.ix;
2061           wtile.waterSlideOldY = wtile.iy;
2062           waterTilesList[$] = wtile;
2063         }
2064       }
2065     }
2066     checkWater = false;
2067     liquidTileCount = 0;
2068     waterTilesList.sort(&sortWaterTilesByCoordsCmp);
2069     // do water flow
2070     bool wasAnyMove = false;
2071     bool wasAnyMoveDown = false;
2072     foreach (MapTile wtile; waterTilesList) {
2073       if (!wtile || !wtile.isInstanceAlive) continue;
2074       auto killIt = checkWaterFlow(wtile);
2075       if (killIt) {
2076         checkWater = true;
2077         wtile.smashMe();
2078         wtile.instanceRemove(); // just in case
2079       } else {
2080         wtile.saveInterpData();
2081         wtile.updateGrid();
2082         wasAnyMove = wasAnyMove || wtile.waterMoved;
2083         wasAnyMoveDown = wasAnyMoveDown || wtile.waterMovedDown;
2084         if (wtile.waterMoved && debugWaterFlowPause) wtile.waterSlideCounter = 4;
2085       }
2086     }
2087     // do water check
2088     liquidTileCount = 0;
2089     foreach (MapTile wtile; waterTilesList) {
2090       if (!wtile || !wtile.isInstanceAlive) continue;
2091       if (wasAnyMoveDown) {
2092         ++liquidTileCount;
2093         continue;
2094       }
2095       //checkWater = checkWater || wtile.waterMoved;
2096       curWaterTile = wtile;
2097       int tileX = wtile.ix/16, tileY = wtile.iy/16;
2098       // check if we are have no way to leak
2099       bool killIt = false;
2100       if (!isFullyOccupiedAtTilePos(tileX-1, tileY) || (wtile.water && curWaterTileCheckHitsLava)) {
2101         //writeln(" LEFT DEATH (", curWaterOccupiedCount, " : ", curWaterTileCheckHitsLava, ")");
2102         killIt = true;
2103       }
2104       if (!killIt && (!isFullyOccupiedAtTilePos(tileX+1, tileY) || (wtile.water && curWaterTileCheckHitsLava))) {
2105         //writeln(" RIGHT DEATH (", curWaterOccupiedCount, " : ", curWaterTileCheckHitsLava, ")");
2106         killIt = true;
2107       }
2108       if (!killIt && (!isFullyOccupiedAtTilePos(tileX, tileY+1) || (wtile.water && curWaterTileCheckHitsLava))) {
2109         //writeln(" DOWN DEATH (", curWaterOccupiedCount, " : ", curWaterTileCheckHitsLava, ")");
2110         killIt = true;
2111       }
2112       //killIt = false;
2113       if (killIt) {
2114         checkWater = true;
2115         wtile.smashMe();
2116         wtile.instanceRemove(); // just in case
2117       } else {
2118         ++liquidTileCount;
2119       }
2120     }
2121     if (wasAnyMove) checkWater = true;
2122     //writeln("water check: liquidTileCount=", liquidTileCount, "; checkWater=", checkWater, "; wasAnyMove=", wasAnyMove, "; wasAnyMoveDown=", wasAnyMoveDown);
2124     // fill empty spaces in lake with water
2125     fixLiquidTop();
2126   }
2128   cleanDeadObjects();
2132 // ////////////////////////////////////////////////////////////////////////// //
2133 private transient MapEntity thinkerHeld;
2136 private final void doThinkActionsForObject (MapEntity o) {
2137        if (o.justSpawned) o.justSpawned = false;
2138   else if (o.imageSpeed > 0) o.nextAnimFrame();
2139   o.saveInterpData();
2140   o.thinkFrame();
2141   if (o.isInstanceAlive) {
2142     //o.updateGrid();
2143     o.processAlarms();
2144     if (o.isInstanceAlive) {
2145       if (o.whipTimer > 0) --o.whipTimer;
2146       o.updateGrid();
2147       auto obj = MapObject(o);
2148       if (!o.canLiveOutsideOfLevel && (!obj || !obj.heldBy) && o.isOutsideOfLevel()) {
2149         // oops, fallen out of level...
2150         o.onOutOfLevel();
2151       }
2152     }
2153   }
2157 // return `true` if thinker should be removed
2158 private final void thinkOne (MapEntity o) {
2159   //if (!o) return;
2160   //if (!o.isInstanceAlive) return;
2161   //if (!o.active) return;
2163   auto obj = MapObject(o);
2165   if (obj && obj.heldBy == player) {
2166     // fix held item coords
2167     obj.fixHoldCoords();
2168     doThinkActionsForObject(o);
2169     return;
2170   }
2172   bool doThink = true;
2174   // collision with player weapon
2175   auto hh = PlayerWeapon(player.holdItem);
2176   bool doWeaponAction = false;
2177   if (hh) {
2178     if (hh.blockedBySolids && !global.config.killEnemiesThruWalls) {
2179       int xx = player.ix+(player.dir == MapObject::Dir.Left ? -16 : 16);
2180       //doWeaponAction = !isSolidAtPoint(xx, player.iy);
2181       doWeaponAction = !isSolidAtPoint(xx, hh.yCenter);
2182       /*
2183       int dh = max(1, hh.height-2);
2184       doWeaponAction = !checkTilesInRect(player.ix, player.iy);
2185       */
2186     } else {
2187       doWeaponAction = true;
2188     }
2189   }
2191   if (obj && doWeaponAction && hh && (o.whipTimer <= 0 || hh.ignoreWhipTimer) && hh.collidesWithObject(obj)) {
2192     //writeln("WEAPONED!");
2193     //writeln("weapon collides with '", GetClassName(o.Class), "' (", o.objType, "'");
2194     bool dontChangeWhipTimer = hh.dontChangeWhipTimer;
2195     if (!o.onTouchedByPlayerWeapon(player, hh)) {
2196       if (o.isInstanceAlive) hh.onCollisionWithObject(obj);
2197     }
2198     if (!dontChangeWhipTimer) o.whipTimer = o.whipTimerValue; //HACK
2199     doThink = o.isInstanceAlive;
2200   }
2202   if (doThink && o.isInstanceAlive) {
2203     doThinkActionsForObject(o);
2204     doThink = o.isInstanceAlive;
2205   }
2207   // collision with player
2208   if (doThink && obj && o.collidesWith(player)) {
2209     if (!player.onObjectTouched(obj) && o.isInstanceAlive) {
2210       doThink = !o.onTouchedByPlayer(player);
2211       o.updateGrid();
2212     }
2213   }
2217 final void processThinkers (float timeDelta) {
2218   if (timeDelta <= 0) return;
2219   if (gamePaused) {
2220     ++pausedTime;
2221     if (onBeforeFrame) onBeforeFrame(false);
2222     if (onAfterFrame) onAfterFrame(false);
2223     keysNextFrame();
2224     return;
2225   } else {
2226     pausedTime = 0;
2227   }
2228   accumTime += timeDelta;
2229   bool wasFrame = false;
2230   // block GC
2231   auto olddel = GC_ImmediateDelete;
2232   GC_ImmediateDelete = false;
2233   while (accumTime >= FrameTime) {
2234     bool solidObjectSeen = false;
2235     //postponedThinkers.clear();
2236     thinkerHeld = none;
2237     accumTime -= FrameTime;
2238     if (onBeforeFrame) onBeforeFrame(accumTime >= FrameTime);
2239     // shake
2240     if (shakeLeft > 0) {
2241       --shakeLeft;
2242       if (!shakeDir.x) shakeDir.x = (global.randOther(0, 1) ? -3 : 3);
2243       if (!shakeDir.y) shakeDir.y = (global.randOther(0, 1) ? -3 : 3);
2244       shakeOfs.x = shakeDir.x;
2245       shakeOfs.y = shakeDir.y;
2246       int sgnc = global.randOther(1, 3);
2247       if (sgnc&0x01) shakeDir.x = -shakeDir.x;
2248       if (sgnc&0x02) shakeDir.y = -shakeDir.y;
2249     } else {
2250       shakeOfs.x = 0;
2251       shakeOfs.y = 0;
2252       shakeDir.x = 0;
2253       shakeDir.y = 0;
2254     }
2255     // advance time
2256     time += 1;
2257     // we don't want the time to grow too large
2258     if (time < 0) { time = 0; lastRenderTime = -1; }
2259     // game-global events
2260     thinkFrameGame();
2261     // frame thinkers: player
2262     if (player && !disablePlayerThink) {
2263       // time limit
2264       if (!player.dead && isNormalLevel() &&
2265           (maxPlayingTime < 0 ||
2266            (maxPlayingTime > 0 && maxPlayingTime*30 <= time &&
2267             time%30 == 0 && global.randOther(1, 100) <= 20)))
2268       {
2269         global.hasAnkh = false;
2270         global.plife = 1;
2271         player.invincible = 0;
2272         auto xplo = MapObjExplosion(MakeMapObject(player.ix, player.iy, 'oExplosion'));
2273         if (xplo) xplo.suicide = true;
2274       }
2275       //HACK: check for stolen items
2276       auto item = MapItem(player.holdItem);
2277       if (item) item.onCheckItemStolen(player);
2278       item = MapItem(player.pickedItem);
2279       if (item) item.onCheckItemStolen(player);
2280       // normal thinking
2281       doThinkActionsForObject(player);
2282     }
2283     // frame thinkers: held object
2284     thinkerHeld = player.holdItem;
2285     if (thinkerHeld && thinkerHeld.isInstanceAlive) {
2286       if (thinkerHeld.active) {
2287         thinkOne(thinkerHeld);
2288         if (!thinkerHeld.isInstanceAlive) {
2289           if (player.holdItem == thinkerHeld) player.holdItem = none;
2290           thinkerHeld.grid.remove(thinkerHeld.gridId);
2291         }
2292       } else {
2293         //HACK!
2294         auto item = MapItem(thinkerHeld);
2295         if (item) {
2296           if (item.forSale || item.sellOfferDone) {
2297             if (++item.forSaleFrame < 0) item.forSaleFrame = 0;
2298           }
2299         }
2300       }
2301     }
2302     // frame thinkers: objects
2303     foreach (MapEntity e; activeItemsList) {
2304       if (!e || e == thinkerHeld) continue;
2305       if (!e.active || !e.isInstanceAlive) continue;
2306       thinkOne(e);
2307       if (!e.isInstanceAlive) {
2308         if (e.grid) e.grid.remove(e.gridId);
2309         auto obj = MapObject(e);
2310         if (obj && obj.heldBy) obj.heldBy.holdItem = none;
2311       } else if (!solidObjectSeen && e.walkableSolid) {
2312         solidObjectSeen = true;
2313         hasSolidObjects = true;
2314       }
2315     }
2316     thinkerHeld = none;
2317     // clean dead things
2318     someTilesRemoved = false;
2319     cleanDeadTiles();
2320     hasSolidObjects = !!solidObjectSeen;
2321     // fix held item coords
2322     if (player && player.holdItem) {
2323       if (player.holdItem.isInstanceAlive) {
2324         player.holdItem.fixHoldCoords();
2325       } else {
2326         player.holdItem = none;
2327       }
2328     }
2329     // money counter
2330     if (collectCounter == 0) {
2331       xmoney = max(0, xmoney-100);
2332     } else {
2333       --collectCounter;
2334     }
2335     // other things
2336     if (player) {
2337       if (!player.dead) stats.oneMoreFramePlayed();
2338       SoundSystem.ListenerOrigin = vector(player.xCenter, player.yCenter, -1);
2339       //writeln("plrpos=(", player.xCenter, ",", player.yCenter, "); lo=", SoundSystem.ListenerOrigin);
2340     }
2341     if (onAfterFrame) onAfterFrame(accumTime >= FrameTime);
2342     ++framesProcessedFromLastClear;
2343     keysNextFrame();
2344     wasFrame = true;
2345     if (!player.visible && player.holdItem) player.holdItem.visible = false;
2346     if (winCutsceneSwitchToNext) {
2347       winCutsceneSwitchToNext = false;
2348       switch (++inWinCutscene) {
2349         case 2: startWinCutsceneVolcano(); break;
2350         case 3: default: startWinCutsceneWinFall(); break;
2351       }
2352       break;
2353     }
2354     if (playerExited) break;
2355   }
2356   GC_ImmediateDelete = olddel;
2357   if (playerExited) {
2358     playerExited = false;
2359     onLevelExited();
2360     centerViewAtPlayer();
2361   }
2362   if (wasFrame) {
2363     // if we were processed at least one frame, collect garbage
2364     //keysNextFrame();
2365     GC_CollectGarbage(true); // destroy delayed objects too
2366   }
2367   if (accumTime > 0 && onInterFrame) onInterFrame(accumTime/FrameTime);
2371 // ////////////////////////////////////////////////////////////////////////// //
2372 final void tileXY2roomXY (int tileX, int tileY, out int roomX, out int roomY) {
2373   roomX = (tileX-1)/RoomGen::Width;
2374   roomY = (tileY-1)/RoomGen::Height;
2378 final bool isInShop (int tileX, int tileY) {
2379   if (tileX >= 0 && tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
2380     auto n = roomType[tileX, tileY];
2381     if (n == 4 || n == 5) return true;
2382     return !!checkTilesInRect(tileX*16, tileY*16, 16, 16, delegate bool (MapTile t) { return t.shopWall; });
2383     //k8: we don't have this
2384     //if (t && t.objType == 'oShop') return true;
2385   }
2386   return false;
2390 // ////////////////////////////////////////////////////////////////////////// //
2391 override void Destroy () {
2392   clearWholeLevel();
2393   delete tempSolidTile;
2394   ::Destroy();
2398 // ////////////////////////////////////////////////////////////////////////// //
2399 // WARNING! delegate should not create/delete objects!
2400 final MapObject findNearestObject (int px, int py, scope bool delegate (MapObject o) dg, optional class!MapObject castClass) {
2401   MapObject res = none;
2402   if (!castClass) castClass = MapObject;
2403   int curdistsq = int.max;
2404   foreach (MapObject o; objGrid.allObjects(MapObject)) {
2405     if (o.spectral) continue;
2406     if (!dg(o)) continue;
2407     int xc = px-o.xCenter, yc = py-o.yCenter;
2408     int distsq = xc*xc+yc*yc;
2409     if (distsq < curdistsq) {
2410       res = o;
2411       curdistsq = distsq;
2412     }
2413   }
2414   return res;
2418 // WARNING! delegate should not create/delete objects!
2419 final MapObject findNearestEnemy (int px, int py, optional scope bool delegate (MapEnemy o) dg, optional class!MapEnemy castClass) {
2420   if (!castClass) castClass = MapEnemy;
2421   if (castClass !isa MapEnemy) return none;
2422   MapObject res = none;
2423   int curdistsq = int.max;
2424   foreach (MapEnemy o; objGrid.allObjects(castClass)) {
2425     //k8: i added `dead` check
2426     if (o.spectral || o.dead) continue;
2427     if (dg) {
2428       if (!dg(o)) continue;
2429     }
2430     int xc = px-o.xCenter, yc = py-o.yCenter;
2431     int distsq = xc*xc+yc*yc;
2432     if (distsq < curdistsq) {
2433       res = o;
2434       curdistsq = distsq;
2435     }
2436   }
2437   return res;
2441 final MonsterShopkeeper findNearestCalmShopkeeper (int px, int py) {
2442   auto obj = MonsterShopkeeper(findNearestEnemy(px, py, delegate bool (MapEnemy o) {
2443     auto sk = MonsterShopkeeper(o);
2444     if (sk && !sk.angered) return true;
2445     return false;
2446   }, castClass:MonsterShopkeeper));
2447   return obj;
2451 final MonsterShopkeeper hasAliveShopkeepers (optional bool skipAngry) {
2452   foreach (MonsterShopkeeper sc; objGrid.allObjects(MonsterShopkeeper)) {
2453     if (sc.spectral || sc.dead) continue;
2454     if (skipAngry && (sc.angered || sc.outlaw)) continue;
2455     return sc;
2456   }
2457   return none;
2461 // WARNING! delegate should not create/delete objects!
2462 final int calcNearestEnemyDist (int px, int py, optional scope bool delegate (MapEnemy o) dg, optional class!MapEnemy castClass) {
2463   auto e = findNearestEnemy(px, py, dg!optional, castClass!optional);
2464   if (!e) return int.max;
2465   int xc = px-e.xCenter, yc = py-e.yCenter;
2466   return roundi(sqrt(xc*xc+yc*yc));
2470 // WARNING! delegate should not create/delete objects!
2471 final int calcNearestObjectDist (int px, int py, optional scope bool delegate (MapObject o) dg, optional class!MapObject castClass) {
2472   auto e = findNearestObject(px, py, dg!optional, castClass!optional);
2473   if (!e) return int.max;
2474   int xc = px-e.xCenter, yc = py-e.yCenter;
2475   return roundi(sqrt(xc*xc+yc*yc));
2479 // WARNING! delegate should not create/delete objects!
2480 final MapTile findNearestMoveableSolid (int px, int py, optional scope bool delegate (MapTile t) dg) {
2481   MapTile res = none;
2482   int curdistsq = int.max;
2483   foreach (MapTile t; objGrid.allObjects(MapTile)) {
2484     if (t.spectral) continue;
2485     if (dg) {
2486       if (!dg(t)) continue;
2487     } else {
2488       if (!t.solid || !t.moveable) continue;
2489     }
2490     int xc = px-t.xCenter, yc = py-t.yCenter;
2491     int distsq = xc*xc+yc*yc;
2492     if (distsq < curdistsq) {
2493       res = t;
2494       curdistsq = distsq;
2495     }
2496   }
2497   return res;
2501 // WARNING! delegate should not create/delete objects!
2502 final MapTile findNearestTile (int px, int py, optional scope bool delegate (MapTile t) dg) {
2503   if (!dg) return none;
2504   MapTile res = none;
2505   int curdistsq = int.max;
2507   //FIXME: make this faster!
2508   foreach (MapTile t; objGrid.allObjects(MapTile)) {
2509     if (t.spectral) continue;
2510     int xc = px-t.xCenter, yc = py-t.yCenter;
2511     int distsq = xc*xc+yc*yc;
2512     if (distsq < curdistsq && dg(t)) {
2513       res = t;
2514       curdistsq = distsq;
2515     }
2516   }
2518   return res;
2522 // ////////////////////////////////////////////////////////////////////////// //
2523 final bool cbIsObjectWeb (MapObject o) { return (o isa ItemWeb); }
2524 final bool cbIsObjectBlob (MapObject o) { return (o isa EnemyBlob); }
2525 final bool cbIsObjectArrow (MapObject o) { return (o isa ItemProjectileArrow); }
2526 final bool cbIsObjectEnemy (MapObject o) { return (/*o != self &&*/ o isa MapEnemy); }
2528 final bool cbIsObjectTreasure (MapObject o) { return (o.isTreasure); }
2530 final bool cbIsItemObject (MapObject o) { return (o isa MapItem); }
2532 final bool cbIsItemOrEnemyObject (MapObject o) { return (o isa MapItem || o isa MapEnemy); }
2535 final MapObject isObjectAtTile (int tileX, int tileY, optional scope bool delegate (MapObject o) dg, optional bool precise) {
2536   if (!specified_precise) precise = true;
2537   tileX *= 16;
2538   tileY *= 16;
2539   foreach (MapObject o; objGrid.inRectPix(tileX, tileY, 16, 16, precise:precise, castClass:MapObject)) {
2540     if (o.spectral) continue;
2541     if (dg) {
2542       if (dg(o)) return o;
2543     } else {
2544       return o;
2545     }
2546   }
2547   return none;
2551 final MapObject isObjectAtTilePix (int x, int y, optional scope bool delegate (MapObject o) dg) {
2552   return isObjectAtTile(x/16, y/16, dg!optional);
2556 final MapObject isObjectAtPoint (int xpos, int ypos, optional scope bool delegate (MapObject o) dg, optional bool precise, optional class!MapObject castClass) {
2557   if (!specified_precise) precise = true;
2558   if (!castClass) castClass = MapObject;
2559   foreach (MapObject o; objGrid.inCellPix(xpos, ypos, precise:precise, castClass:castClass)) {
2560     if (o.spectral) continue;
2561     if (dg) {
2562       if (dg(o)) return o;
2563     } else {
2564       if (o isa MapEnemy) return o;
2565     }
2566   }
2567   return none;
2571 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) {
2572   if (w < 1 || h < 1) return none;
2573   if (!castClass) castClass = MapObject;
2574   if (w == 1 && h == 1) return isObjectAtPoint(xpos, ypos, dg!optional, precise!optional, castClass);
2575   if (!specified_precise) precise = true;
2576   foreach (MapObject o; objGrid.inRectPix(xpos, ypos, w, h, precise:precise, castClass:castClass)) {
2577     if (o.spectral) continue;
2578     if (dg) {
2579       if (dg(o)) return o;
2580     } else {
2581       if (o isa MapEnemy) return o;
2582     }
2583   }
2584   return none;
2588 final MapObject forEachObject (scope bool delegate (MapObject o) dg, optional bool allowSpectrals, optional class!MapObject castClass) {
2589   if (!dg) return none;
2590   if (!castClass) castClass = MapObject;
2591   foreach (MapObject o; objGrid.allObjectsSafe(castClass)) {
2592     if (!allowSpectrals && o.spectral) continue;
2593     if (dg(o)) return o;
2594   }
2595   return none;
2599 final MapObject forEachObjectAtPoint (int xpos, int ypos, scope bool delegate (MapObject o) dg, optional bool precise) {
2600   if (!dg) return none;
2601   if (!specified_precise) precise = true;
2602   foreach (MapObject o; objGrid.inCellPix(xpos, ypos, precise:precise, castClass:MapObject)) {
2603     if (o.spectral) continue;
2604     if (dg(o)) return o;
2605   }
2606   return none;
2610 final MapObject forEachObjectInRect (int xpos, int ypos, int w, int h, scope bool delegate (MapObject o) dg, optional bool precise) {
2611   if (!dg || w < 1 || h < 1) return none;
2612   if (w == 1 && h == 1) return forEachObjectAtPoint(xpos, ypos, dg!optional, precise!optional);
2613   if (!specified_precise) precise = true;
2614   foreach (MapObject o; objGrid.inRectPix(xpos, ypos, w, h, precise:precise, castClass:MapObject)) {
2615     if (o.spectral) continue;
2616     if (dg(o)) return o;
2617   }
2618   return none;
2622 final MapEntity forEachEntityInRect (int xpos, int ypos, int w, int h, scope bool delegate (MapEntity o) dg, optional bool precise, optional class!MapEntity castClass) {
2623   if (!dg || w < 1 || h < 1) return none;
2624   if (!castClass) castClass = MapEntity;
2625   if (!specified_precise) precise = true;
2626   foreach (MapEntity e; objGrid.inRectPix(xpos, ypos, w, h, precise:precise, castClass:castClass)) {
2627     if (e.spectral) continue;
2628     if (dg(e)) return e;
2629   }
2630   return none;
2634 private final bool cbIsRopeTile (MapTile t) { return (t isa MapTileRope); }
2636 final MapTile isRopeAtPoint (int px, int py) {
2637   return checkTileAtPoint(px, py, &cbIsRopeTile);
2641 //FIXME!
2642 final MapTile isWaterSwimAtPoint (int px, int py) {
2643   return isWaterAtPoint(px, py);
2647 // ////////////////////////////////////////////////////////////////////////// //
2648 private array!MapEntity tmpEntityList;
2650 private final bool cbCollectEntitiesWithMask (MapEntity t) {
2651   if (!t.visible || t.spectral) return false;
2652   tmpEntityList[$] = t;
2653   return false;
2657 final void touchEntitiesWithMask (int x, int y, SpriteFrame frm, scope bool delegate (MapEntity t) dg, optional class!MapEntity castClass) {
2658   if (!frm || frm.bw < 1 || frm.bh < 1 || !dg) return;
2659   if (frm.isEmptyPixelMask) return;
2660   if (!castClass) castClass = MapEntity;
2661   // collect tiles
2662   if (tmpEntityList.length) tmpEntityList.clear();
2663   if (player isa castClass && player.isRectCollisionFrame(frm, x, y)) tmpEntityList[$] = player;
2664   forEachEntityInRect(x+frm.bx, y+frm.by, frm.bw, frm.bh, &cbCollectEntitiesWithMask, castClass:castClass);
2665   foreach (MapEntity e; tmpEntityList) {
2666     if (!e || !e.isInstanceAlive || !e.visible || e.spectral) continue;
2667     if (e.isRectCollisionFrame(frm, x, y)) {
2668       if (dg(e)) break;
2669     }
2670   }
2674 // ////////////////////////////////////////////////////////////////////////// //
2675 final bool cbCollisionAnySolid (MapTile t) { return t.solid; }
2676 final bool cbCollisionAnyMoveableSolid (MapTile t) { return t.solid && t.moveable; }
2677 final bool cbCollisionLiquid (MapTile t) { return (t.water || t.lava); }
2678 final bool cbCollisionAnySolidOrLiquid (MapTile t) { return (t.solid || t.water || t.lava); }
2679 final bool cbCollisionLadder (MapTile t) { return t.ladder; }
2680 final bool cbCollisionLadderTop (MapTile t) { return t.laddertop; }
2681 final bool cbCollisionAnyLadder (MapTile t) { return (t.ladder || t.laddertop); }
2682 final bool cbCollisionAnySolidIce (MapTile t) { return (t.solid && t.ice); }
2683 final bool cbCollisionWater (MapTile t) { return t.water; }
2684 final bool cbCollisionLava (MapTile t) { return t.lava; }
2685 final bool cbCollisionAnyTree (MapTile t) { return t.tree; }
2686 final bool cbCollisionPlatform (MapTile t) { return t.platform; }
2687 final bool cbCollisionLadderOrPlatform (MapTile t) { return (t.ladder || t.platform); }
2688 //final bool cbCollisionAnyHangeable (MapTile t) { return t.hangeable; } // no need to check for solids here, it is done in MapTile initializer
2689 final bool cbCollisionSpringTrap (MapTile t) { return t.springtrap; }
2690 final bool cbCollisionSpikes (MapTile t) { return t.spikes; }
2691 final bool cbCollisionExitTile (MapTile t) { return t.isExitActive(); }
2693 final bool cbCollisionForWhoa (MapTile t) { return (t.solid || t.laddertop); }
2695 //final bool cbCollisionSacAltar (MapTile t) { return (t.objName == 'oSacAltarLeft'); } // only left part, yeah
2696 final bool cbCollisionSacAltar (MapTile t) { return t.sacrificingAltar; }
2699 // ////////////////////////////////////////////////////////////////////////// //
2700 transient MapTileTemp tempSolidTile;
2702 private final MapTileTemp makeWalkeableSolidTile (MapObject o) {
2703   if (!tempSolidTile) {
2704     tempSolidTile = SpawnObject(MapTileTemp);
2705   } else if (!tempSolidTile.isInstanceAlive) {
2706     delete tempSolidTile;
2707     tempSolidTile = SpawnObject(MapTileTemp);
2708   }
2709   // setup data
2710   tempSolidTile.level = self;
2711   tempSolidTile.global = global;
2712   tempSolidTile.solid = true;
2713   tempSolidTile.objName = MapTileTemp.default.objName;
2714   tempSolidTile.objType = MapTileTemp.default.objType;
2715   tempSolidTile.e = o;
2716   tempSolidTile.fltx = o.fltx;
2717   tempSolidTile.flty = o.flty;
2718   return tempSolidTile;
2722 final MapTile checkTilesInRect (int x0, int y0, const int w, const int h,
2723                                 optional scope bool delegate (MapTile dg) dg, optional bool precise,
2724                                 optional class!MapTile castClass)
2726   if (w < 1 || h < 1) return none;
2727   if (w == 1 && h == 1) return checkTileAtPoint(x0, y0, dg!optional);
2728   int x1 = x0+w-1, y1 = y0+h-1;
2729   if (x0 >= tilesWidth*16 || y0 >= tilesHeight*16 || x1 < 0 || y1 < 0) return none;
2730   if (!specified_precise) precise = true;
2731   if (!castClass) castClass = MapTile;
2732   if (castClass !isa MapTile) return none;
2733   if (!dg) dg = &cbCollisionAnySolid;
2735   if (hasSolidObjects) {
2736     // check walkable solid objects too
2737     foreach (MapEntity e; objGrid.inRectPix(x0, y0, w, h, precise:precise/*, castClass:castClass*/)) {
2738       if (e.spectral || !e.visible) continue;
2739       auto t = MapTile(e);
2740       if (t) {
2741         if (t isa castClass && dg(t)) return t;
2742         continue;
2743       }
2744       auto o = MapObject(e);
2745       if (o && o.walkableSolid) {
2746         t = makeWalkeableSolidTile(o);
2747         if (t isa castClass && dg(t)) return t;
2748         continue;
2749       }
2750     }
2751   } else {
2752     // no walkeable solid MapObjects, speed it up
2753     foreach (MapTile t; objGrid.inRectPix(x0, y0, w, h, precise:precise, castClass:castClass)) {
2754       if (t.spectral || !t.visible) continue;
2755       if (dg(t)) return t;
2756     }
2757   }
2759   return none;
2763 final MapTile checkTileAtPoint (int x0, int y0, optional scope bool delegate (MapTile dg) dg, optional bool precise, optional class!MapTile castClass) {
2764   if (x0 < 0 || y0 < 0 || x0 >= tilesWidth*16 || y0 >= tilesHeight*16) return none;
2765   if (!specified_precise) precise = true;
2766   if (!castClass) castClass = MapTile;
2767   if (castClass !isa MapTile) return none;
2768   if (!dg) dg = &cbCollisionAnySolid;
2770   if (hasSolidObjects) {
2771     // check walkable solid objects
2772     foreach (MapEntity e; objGrid.inCellPix(x0, y0, precise:precise/*, castClass:castClass*/)) {
2773       if (e.spectral || !e.visible) continue;
2774       auto t = MapTile(e);
2775       if (t) {
2776         if (t isa castClass && dg(t)) return t;
2777         continue;
2778       }
2779       auto o = MapObject(e);
2780       if (o && o.walkableSolid) {
2781         t = makeWalkeableSolidTile(o);
2782         if (t isa castClass && dg(t)) return t;
2783         continue;
2784       }
2785     }
2786   } else {
2787     //writeln("NOWS!");
2788     // no walkeable solid MapObjects, speed it up
2789     foreach (MapTile t; objGrid.inCellPix(x0, y0, precise:precise, castClass:castClass)) {
2790       if (t.spectral || !t.visible) continue;
2791       if (dg(t)) return t;
2792     }
2793   }
2795   return none;
2799 // ////////////////////////////////////////////////////////////////////////// //
2800 final MapTile isSolidAtPoint (int px, int py) { return checkTileAtPoint(px, py); }
2801 final MapTile isSolidInRect (int rx, int ry, int rw, int rh) { return checkTilesInRect(rx, ry, rw, rh); }
2802 final MapTile isLadderAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadder); }
2803 final MapTile isLadderOrPlatformAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadderOrPlatform); }
2804 final MapTile isLadderTopAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadderTop); }
2805 final MapTile isAnyLadderAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyLadder); }
2806 final MapTile isWaterAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionWater); }
2807 final MapTile isLavaAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLava); }
2808 final MapTile isLiquidAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLiquid); }
2809 final MapTile isIceAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnySolidIce); }
2810 final MapTile isTreeAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyTree); }
2811 //final MapTile isHangeableAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyHangeable); }
2814 // ////////////////////////////////////////////////////////////////////////// //
2815 final void setTileTypeAt (int tileX, int tileY, LevelGen::RType rt) {
2816   if (tileX >= 0 && tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) roomType[tileX, tileY] = rt;
2820 //FIXME: make this faster
2821 transient float gtagX, gtagY;
2823 // only non-moveables and non-specials
2824 final MapTile getTileAtGrid (int tileX, int tileY) {
2825   gtagX = tileX*16;
2826   gtagY = tileY*16;
2827   return checkTileAtPoint(tileX*16, tileY*16, delegate bool (MapTile t) {
2828     if (t.spectral || t.moveable || t.toSpecialGrid || !t.visible) return false;
2829     if (t.fltx != gtagX || t.flty != gtagY) return false; // emulate grid
2830     if (t.width != 16 || t.height != 16) return false;
2831     return true;
2832   }, precise:false);
2833   //return (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight ? tiles[tileX, tileY] : none);
2837 final MapTile getTileAtGridAny (int tileX, int tileY) {
2838   gtagX = tileX*16;
2839   gtagY = tileY*16;
2840   return checkTileAtPoint(tileX*16, tileY*16, delegate bool (MapTile t) {
2841     if (t.spectral /*|| t.moveable*/ || !t.visible) return false;
2842     if (t.fltx != gtagX || t.flty != gtagY) return false; // emulate grid
2843     if (t.width != 16 || t.height != 16) return false;
2844     return true;
2845   }, precise:false);
2846   //return (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight ? tiles[tileX, tileY] : none);
2850 final bool isNamedTileAt (int tileX, int tileY, name atypename) {
2851   if (!atypename) return false;
2852   auto t = getTileAtGridAny(tileX, tileY);
2853   return (t && t.objName == atypename);
2857 final void setTileAtGrid (int tileX, int tileY, MapTile tile) {
2858   if (tileX >= 0 && tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
2859     if (tile) {
2860       tile.fltx = tileX*16;
2861       tile.flty = tileY*16;
2862       if (!tile.dontReplaceOthers) {
2863         auto osp = tile.spectral;
2864         tile.spectral = true;
2865         auto t = getTileAtGridAny(tileX, tileY);
2866         tile.spectral = osp;
2867         if (t && !t.immuneToReplacement) {
2868           writeln("REPLACING tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
2869           writeln("      NEW tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (tile.toSpecialGrid ? "special " : ""), "grid, pos=(", tile.fltx/16.0, ",", tile.flty/16.0, ")");
2870           t.instanceRemove();
2871         }
2872       }
2873       insertObject(tile);
2874     } else {
2875       auto t = getTileAtGridAny(tileX, tileY);
2876       if (t && !t.immuneToReplacement) {
2877         writeln("REMOVING tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
2878         t.instanceRemove();
2879       }
2880     }
2881   }
2885 // ////////////////////////////////////////////////////////////////////////// //
2886 // return `true` from delegate to stop
2887 MapTile forEachSolidTileOnGrid (scope bool delegate (int tileX, int tileY, MapTile t) dg) {
2888   if (!dg) return none;
2889   foreach (MapTile t; objGrid.allObjectsSafe(MapTile)) {
2890     if (t.spectral || !t.solid || !t.visible) continue;
2891     if (t.ix%16 != 0 || t.iy%16 != 0) continue; // emulate grid
2892     if (t.width != 16 || t.height != 16) continue;
2893     if (dg(t.ix/16, t.iy/16, t)) return t;
2894   }
2895   return none;
2899 // ////////////////////////////////////////////////////////////////////////// //
2900 // return `true` from delegate to stop
2901 MapTile forEachTile (scope bool delegate (MapTile t) dg, optional class!MapTile castClass) {
2902   if (!dg) return none;
2903   if (!castClass) castClass = MapTile;
2904   foreach (MapTile t; objGrid.allObjectsSafe(castClass)) {
2905     if (t.spectral || !t.visible) continue;
2906     if (dg(t)) return t;
2907   }
2908   return none;
2912 // ////////////////////////////////////////////////////////////////////////// //
2913 final void fixWallTiles () {
2914   foreach (MapTile t; objGrid.allObjectsSafe(MapTile)) {
2915     //writeln("beautify: '", GetClassName(t.Class), "' (", t.objType, "' (name:", t.objName, ")");
2916     t.beautifyTile();
2917   }
2921 // ////////////////////////////////////////////////////////////////////////// //
2922 final MapTile isCollisionAtPoint (int px, int py, optional scope bool delegate (MapTile dg) dg) {
2923   if (!dg) dg = &cbCollisionAnySolid;
2924   return checkTilesInRect(px, py, 1, 1, dg);
2928 // ////////////////////////////////////////////////////////////////////////// //
2929 string scrGetKaliGift (MapTile altar, optional name gift) {
2930   string res;
2932   // find other side of the altar
2933   int sx = player.ix, sy = player.iy;
2934   if (altar) {
2935     sx = altar.ix;
2936     sy = altar.iy;
2937     auto a2 = isCollisionAtPoint(altar.ix+18, altar.iy+8, &cbCollisionSacAltar);
2938     if (!a2) a2 = isCollisionAtPoint(altar.ix-8, altar.iy+8, &cbCollisionSacAltar);
2939     if (a2) { sx = a2.ix; sy = a2.iy; }
2940   }
2942        if (global.favor <= -8) res = "SHE SEEMS VERY ANGRY WITH YOU!";
2943   else if (global.favor < 0) res = "SHE SEEMS ANGRY WITH YOU.";
2944   else if (global.favor == 0) res = "SHE HAS FORGIVEN YOU!";
2945   else if (global.favor >= 32) {
2946     if (global.kaliGift >= 3 && global.favor >= 32+(global.kaliGift-2)*16) {
2947       res = "YOU FEEL INVIGORATED!";
2948       global.kaliGift += 1;
2949       global.plife += global.randOther(4, 8);
2950     } else if (global.kaliGift >= 3) {
2951       res = "SHE SEEMS ECSTATIC WITH YOU!";
2952     } else if (global.bombs < 80) {
2953       res = "YOUR SATCHEL FEELS VERY FULL NOW!";
2954       global.kaliGift = 3;
2955       global.bombs = 99;
2956     } else {
2957       res = "YOU FEEL INVIGORATED!";
2958       global.kaliGift += 1;
2959       global.plife += global.randOther(4, 8);
2960     }
2961   } else if (global.favor >= 16) {
2962     if (global.kaliGift >= 2) {
2963       res = "SHE SEEMS VERY HAPPY WITH YOU!";
2964     } else {
2965       res = "SHE BESTOWS A GIFT UPON YOU!";
2966       global.kaliGift = 2;
2967       // poofs
2968       auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2969       obj.xVel = -1;
2970       obj.yVel = 0;
2971       obj = MakeMapObject(sx, sy-8, 'oPoof');
2972       obj.xVel = 1;
2973       obj.yVel = 0;
2974       // a gift
2975       obj = none;
2976       if (gift) obj = MakeMapObject(sx, sy-8, gift);
2977       if (!obj) obj = MakeMapObject(sx, sy-8, 'oKapala');
2978     }
2979   } else if (global.favor >= 8) {
2980     if (global.kaliGift >= 1) {
2981       res = "SHE SEEMS HAPPY WITH YOU.";
2982     } else {
2983       res = "SHE BESTOWS A GIFT UPON YOU!";
2984       global.kaliGift = 1;
2985       //rAltar = instance_nearest(x, y, oSacAltarRight);
2986       //if (instance_exists(rAltar)) {
2987       // poofs
2988       auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2989       obj.xVel = -1;
2990       obj.yVel = 0;
2991       obj = MakeMapObject(sx, sy-8, 'oPoof');
2992       obj.xVel = 1;
2993       obj.yVel = 0;
2994       obj = none;
2995       if (gift) obj = MakeMapObject(sx, sy-8, gift);
2996       if (!obj) {
2997         auto n = global.randOther(1, 8);
2998         auto m = n;
2999         for (;;) {
3000           name aname = '';
3001                if (n == 1 && !global.hasCape && !global.hasJetpack) aname = 'oCapePickup';
3002           else if (n == 2 && !global.hasGloves) aname = 'oGloves';
3003           else if (n == 3 && !global.hasSpectacles) aname = 'oSpectacles';
3004           else if (n == 4 && !global.hasMitt) aname = 'oMitt';
3005           else if (n == 5 && !global.hasSpringShoes) aname = 'oSpringShoes';
3006           else if (n == 6 && !global.hasSpikeShoes) aname = 'oSpikeShoes';
3007           else if (n == 7 && !global.hasStickyBombs) aname = 'oPaste';
3008           else if (n == 8 && !global.hasCompass) aname = 'oCompass';
3009           if (aname) {
3010             obj = MakeMapObject(sx, sy-8, aname);
3011             if (obj) break;
3012           }
3013           ++n;
3014           if (n > 8) n = 1;
3015           if (n == m) {
3016             obj = MakeMapObject(sx, sy-8, (global.hasJetpack ? 'oBombBox' : 'oJetpack'));
3017             break;
3018           }
3019         }
3020       }
3021     }
3022   } else if (global.favor > 0) {
3023     res = "SHE SEEMS PLEASED WITH YOU.";
3024   }
3026   /*
3027   if (argument1) {
3028     global.message = "";
3029     res = "KALI DEVOURS YOU!"; // sacrifice is player
3030   }
3031   */
3033   return res;
3037 void performSacrifice (MapObject what, MapTile where) {
3038   if (!what || !what.isInstanceAlive) return;
3039   MakeMapObject(what.ix+8, what.iy+8, 'oFlame');
3040   if (where) where.playSound('sndSmallExplode'); else what.playSound('sndSmallExplode');
3041   what.spillBlood(amount:3, forced:true);
3043   string msg = "KALI ACCEPTS THE SACRIFICE!";
3045   auto idol = ItemGoldIdol(what);
3046   if (idol) {
3047     ++stats.totalSacrifices;
3048          if (global.favor <= -8) msg = "SHE SEEMS VERY ANGRY WITH YOU!";
3049     else if (global.favor < 0) msg = "SHE SEEMS ANGRY WITH YOU.";
3050     else if (global.favor >= 0) {
3051       // find other side of the altar
3052       int sx = player.ix, sy = player.iy;
3053       auto altar = where;
3054       if (altar) {
3055         sx = altar.ix;
3056         sy = altar.iy;
3057         auto a2 = isCollisionAtPoint(altar.ix+18, altar.iy+8, &cbCollisionSacAltar);
3058         if (!a2) a2 = isCollisionAtPoint(altar.ix-8, altar.iy+8, &cbCollisionSacAltar);
3059         if (a2) { sx = a2.ix; sy = a2.iy; }
3060       }
3061       // poofs
3062       auto obj = MakeMapObject(sx, sy-8, 'oPoof');
3063       obj.xVel = -1;
3064       obj.yVel = 0;
3065       obj = MakeMapObject(sx, sy-8, 'oPoof');
3066       obj.xVel = 1;
3067       obj.yVel = 0;
3068       // a gift
3069       obj = MakeMapObject(sx, sy-16-8, 'oMonkeyGold');
3070     }
3071     osdMessage(msg, 6.66);
3072     scrShake(10);
3073     idol.instanceRemove();
3074     return;
3075   }
3077   if (global.favor <= -8) {
3078     msg = "KALI DEVOURS THE SACRIFICE!";
3079   } else if (global.favor < 0) {
3080     global.favor += (what.status == MapObject::STUNNED ? what.favor : roundi(what.favor/2.0)); //k8: added `roundi()`
3081     if (what.favor > 0) what.favor = 0;
3082   } else {
3083     global.favor += (what.status == MapObject::STUNNED ? what.favor : roundi(what.favor/2.0)); //k8: added `roundi()`
3084   }
3086   /*!!
3087        if (myAltar.gift1 != "" and global.kaliGift == 0) scrGetKaliGift(myAltar.gift1);
3088   else if (myAltar.gift2 != "" and global.kaliGift == 1) scrGetKaliGift(myAltar.gift2);
3089   else scrGetKaliGift("");
3090   */
3092   // sacrifice is player?
3093   if (what isa PlayerPawn) {
3094     ++stats.totalSelfSacrifices;
3095     msg = "KALI DEVOURS YOU!";
3096     player.visible = false;
3097     player.removeBallAndChain(temp:true);
3098     player.dead = true;
3099     player.status = MapObject::DEAD;
3100   } else {
3101     ++stats.totalSacrifices;
3102     auto msg2 = scrGetKaliGift(where);
3103     what.instanceRemove();
3104     if (msg2) msg = va("%s\n%s", msg, msg2);
3105   }
3107   osdMessage(msg, 6.66);
3109   scrShake(10);
3113 // ////////////////////////////////////////////////////////////////////////// //
3114 final void addBackgroundGfxDetails () {
3115   // add background details
3116   //if (global.customLevel) return;
3117   foreach (; 0..20) {
3118     // bg = instance_create(16*randRoom(1, 42), 16*randRoom(1, 33), oCaveBG);
3119          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);
3120     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);
3121     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);
3122     else MakeMapBackTile('bgExtras', 32*global.randRoom(0, 1), 0, 32, 32, 16*global.randRoom(1, 42), 16*global.randRoom(1, 33), 10002);
3123   }
3127 // ////////////////////////////////////////////////////////////////////////// //
3128 private final void fixRealViewStart () {
3129   int scale = global.scale;
3130   realViewStart.x = clamp(realViewStart.x, viewMin.x*scale, viewMax.x*scale-viewWidth);
3131   realViewStart.y = clamp(realViewStart.y, viewMin.y*scale, viewMax.y*scale-viewHeight);
3135 final int cameraCurrX () { return realViewStart.x/global.scale; }
3136 final int cameraCurrY () { return realViewStart.y/global.scale; }
3139 private final void fixViewStart () {
3140   int scale = global.scale;
3141   viewStart.x = clamp(viewStart.x, viewMin.x*scale, viewMax.x*scale-viewWidth);
3142   viewStart.y = clamp(viewStart.y, viewMin.y*scale, viewMax.y*scale-viewHeight);
3143   //print("vy=%s; lo=%s; hi=%s", viewStart.y, viewMin.y*scale, viewMax.y*scale-viewHeight);
3147 final void centerViewAtPlayer () {
3148   if (viewWidth < 1 || viewHeight < 1 || !player) return;
3149   centerViewAt(player.xCenter, player.yCenter);
3153 final void centerViewAt (int x, int y) {
3154   if (viewWidth < 1 || viewHeight < 1) return;
3156   cameraSlideToSpeed.x = 0;
3157   cameraSlideToSpeed.y = 0;
3158   cameraSlideToPlayer = 0;
3160   int scale = global.scale;
3161   x *= scale;
3162   y *= scale;
3163   realViewStart.x = clamp(x-viewWidth/2, 0, tilesWidth*16*scale-viewWidth);
3164   realViewStart.y = clamp(y-viewHeight/2, 0, tilesHeight*16*scale-viewHeight);
3165   fixRealViewStart();
3167   viewStart.x = realViewStart.x;
3168   viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
3169   fixViewStart();
3171   if (onCameraTeleported) onCameraTeleported();
3175 const int ViewPortToleranceX = 16*1+8;
3176 const int ViewPortToleranceY = 16*1+8;
3178 final void fixCamera () {
3179   if (!player) return;
3180   if (viewWidth < 1 || viewHeight < 1) return;
3181   int scale = global.scale;
3182   auto alwaysCenterX = global.config.alwaysCenterPlayer;
3183   auto alwaysCenterY = alwaysCenterX;
3184   // calculate offset from viewport center (in game units), and fix viewport
3186   int camDestX = player.ix+8;
3187   int camDestY = player.iy+8;
3188   if (!cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y) != 0) {
3189     // slide camera to point
3190     if (cameraSlideToSpeed.x) camDestX = cameraSlideToCurr.x;
3191     if (cameraSlideToSpeed.y) camDestY = cameraSlideToCurr.y;
3192     int dx = cameraSlideToDest.x-camDestX;
3193     int dy = cameraSlideToDest.y-camDestY;
3194     //writeln("speed:(", cameraSlideToSpeed.x, ",", cameraSlideToSpeed.y, "); cur:(", camDestX, ",", camDestY, "); dest:(", cameraSlideToDest.x, ",", cameraSlideToDest.y, "); delta=(", dx, ",", dy, ")");
3195     if (dx && cameraSlideToSpeed.x != 0) {
3196       alwaysCenterX = true;
3197       if (abs(dx) <= abs(cameraSlideToSpeed.x)) {
3198         camDestX = cameraSlideToDest.x;
3199       } else {
3200         camDestX += sign(dx)*abs(cameraSlideToSpeed.x);
3201       }
3202     }
3203     if (dy && abs(cameraSlideToSpeed.y) != 0) {
3204       alwaysCenterY = true;
3205       if (abs(dy) <= cameraSlideToSpeed.y) {
3206         camDestY = cameraSlideToDest.y;
3207       } else {
3208         camDestY += sign(dy)*abs(cameraSlideToSpeed.y);
3209       }
3210     }
3211     //writeln("  new:(", camDestX, ",", camDestY, ")");
3212     if (cameraSlideToSpeed.x) cameraSlideToCurr.x = camDestX;
3213     if (cameraSlideToSpeed.y) cameraSlideToCurr.y = camDestY;
3214   }
3216   // horizontal
3217   if (cameraSlideToSpeed.x && !cameraSlideToPlayer) {
3218     realViewStart.x = clamp(camDestX*scale, 0, tilesWidth*16*scale-viewWidth);
3219   } else if (!player.cameraBlockX) {
3220     int x = camDestX*scale;
3221     int cx = realViewStart.x;
3222     if (alwaysCenterX) {
3223       cx = x-viewWidth/2;
3224     } else {
3225       int xofs = x-(cx+viewWidth/2);
3226            if (xofs < -ViewPortToleranceX*scale) cx += xofs+ViewPortToleranceX*scale;
3227       else if (xofs > ViewPortToleranceX*scale) cx += xofs-ViewPortToleranceX*scale;
3228     }
3229     // slide back to player?
3230     if (cameraSlideToPlayer && cameraSlideToSpeed.x) {
3231       int prevx = cameraSlideToCurr.x*scale;
3232       int dx = (cx-prevx)/scale;
3233       if (abs(dx) <= cameraSlideToSpeed.x) {
3234         writeln("BACKSLIDE X COMPLETE!");
3235         cameraSlideToSpeed.x = 0;
3236       } else {
3237         cx = prevx+(sign(dx)*cameraSlideToSpeed.x)*scale;
3238         cameraSlideToCurr.x += sign(dx)*cameraSlideToSpeed.x;
3239         if ((dx < 0 && cx <= 0) || (dx > 0 && cx >= tilesWidth*16*scale-viewWidth)) {
3240           writeln("BACKSLIDE X COMPLETE!");
3241           cameraSlideToSpeed.x = 0;
3242         }
3243       }
3244     }
3245     realViewStart.x = clamp(cx, 0, tilesWidth*16*scale-viewWidth);
3246   }
3248   // vertical
3249   if (cameraSlideToSpeed.y && !cameraSlideToPlayer) {
3250     realViewStart.y = clamp(camDestY*scale, 0, tilesHeight*16*scale-viewHeight);
3251   } else if (!player.cameraBlockY) {
3252     int y = camDestY*scale;
3253     int cy = realViewStart.y;
3254     if (alwaysCenterY) {
3255       cy = y-viewHeight/2;
3256     } else {
3257       int yofs = y-(cy+viewHeight/2);
3258            if (yofs < -ViewPortToleranceY*scale) cy += yofs+ViewPortToleranceY*scale;
3259       else if (yofs > ViewPortToleranceY*scale) cy += yofs-ViewPortToleranceY*scale;
3260     }
3261     // slide back to player?
3262     if (cameraSlideToPlayer && cameraSlideToSpeed.y) {
3263       int prevy = cameraSlideToCurr.y*scale;
3264       int dy = (cy-prevy)/scale;
3265       if (abs(dy) <= cameraSlideToSpeed.y) {
3266         writeln("BACKSLIDE Y COMPLETE!");
3267         cameraSlideToSpeed.y = 0;
3268       } else {
3269         cy = prevy+(sign(dy)*cameraSlideToSpeed.y)*scale;
3270         cameraSlideToCurr.y += sign(dy)*cameraSlideToSpeed.y;
3271         if ((dy < 0 && cy <= 0) || (dy > 0 && cy >= tilesHeight*16*scale-viewHeight)) {
3272           writeln("BACKSLIDE Y COMPLETE!");
3273           cameraSlideToSpeed.y = 0;
3274         }
3275       }
3276     }
3277     realViewStart.y = clamp(cy, 0, tilesHeight*16*scale-viewHeight);
3278   }
3280   if (cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y) == 0) cameraSlideToPlayer = 0;
3282   fixRealViewStart();
3283   //writeln("  new2:(", cameraCurrX, ",", cameraCurrY, ")");
3285   viewStart.x = realViewStart.x;
3286   viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
3287   fixViewStart();
3291 // ////////////////////////////////////////////////////////////////////////// //
3292 // x0 and y0 are non-scaled (and will be scaled)
3293 final void drawSpriteAt (name sprName, float frnumf, int x0, int y0, optional bool small) {
3294   if (!sprName) return;
3295   auto spr = sprStore[sprName];
3296   if (!spr || !spr.frames.length) return;
3297   int scale = global.scale;
3298   x0 *= scale;
3299   y0 *= scale;
3300   int frnum = max(0, trunci(frnumf))%spr.frames.length;
3301   auto sfr = spr.frames[frnum];
3302   int sx0 = x0-sfr.xofs*scale;
3303   int sy0 = y0-sfr.yofs*scale;
3304   if (small && scale > 1) {
3305     sfr.tex.blitExt(sx0, sy0, roundi(sx0+sfr.width*(scale/2.0)), roundi(sy0+sfr.height*(scale/2.0)), 0, 0);
3306   } else {
3307     sfr.blitAt(sx0, sy0, scale);
3308   }
3312 final void drawSpriteAtS3 (name sprName, float frnumf, int x0, int y0) {
3313   if (!sprName) return;
3314   auto spr = sprStore[sprName];
3315   if (!spr || !spr.frames.length) return;
3316   x0 *= 3;
3317   y0 *= 3;
3318   int frnum = max(0, trunci(frnumf))%spr.frames.length;
3319   auto sfr = spr.frames[frnum];
3320   int sx0 = x0-sfr.xofs*3;
3321   int sy0 = y0-sfr.yofs*3;
3322   sfr.blitAt(sx0, sy0, 3);
3326 // x0 and y0 are non-scaled (and will be scaled)
3327 final void drawTextAt (int x0, int y0, string text, optional int scale, optional int hiColor1, optional int hiColor2) {
3328   if (!text) return;
3329   if (!specified_scale) scale = global.scale;
3330   x0 *= scale;
3331   y0 *= scale;
3332   sprStore.renderTextWithHighlight(x0, y0, text, scale, hiColor1!optional, hiColor2!optional);
3336 void renderCompass (float currFrameDelta) {
3337   if (!global.hasCompass) return;
3339   /*
3340   if (isRoom("rOlmec")) {
3341     global.exitX = 648;
3342     global.exitY = 552;
3343   } else if (isRoom("rOlmec2")) {
3344     global.exitX = 648;
3345     global.exitY = 424;
3346   }
3347   */
3349   bool hasMessage = osdHasMessage();
3350   foreach (MapTile et; allExits) {
3351     // original compass
3352     int exitX = et.ix, exitY = et.iy;
3353     int vx0 = viewStart.x/global.scale, vy0 = viewStart.y/global.scale;
3354     int vx1 = (viewStart.x+viewWidth)/global.scale;
3355     int vy1 = (viewStart.y+viewHeight)/global.scale;
3356     if (exitY > vy1-16) {
3357       if (exitX < vx0) {
3358         drawSpriteAt(hasMessage ? 'sCompassSmallLL' : 'sCompassLL', 0, 0, 224);
3359       } else if (exitX > vx1-16) {
3360         drawSpriteAt(hasMessage ? 'sCompassSmallLR' : 'sCompassLR', 0, 304, 224);
3361       } else {
3362         drawSpriteAt(hasMessage ? 'sCompassSmallDown' : 'sCompassDown', 0, exitX-vx0, 224);
3363       }
3364     } else if (exitX < vx0) {
3365       drawSpriteAt(hasMessage ? 'sCompassSmallLeft' : 'sCompassLeft', 0, 0, exitY-vy0);
3366     } else if (exitX > vx1-16) {
3367       drawSpriteAt(hasMessage ? 'sCompassSmallRight' : 'sCompassRight', 0, 304, exitY-vy0);
3368     }
3369     break; // only the first exit
3370   }
3374 final bool totalsNameCmpCB (ref GameStats::TotalItem a, ref GameStats::TotalItem b) {
3375   auto sa = string(a.objName);
3376   auto sb = string(b.objName);
3377   return (sa < sb);
3380 void renderTransitionInfo (float currFrameDelta) {
3381   //FIXME!
3382   /*
3383   GameStats.sortTotalsList(stats.kills, &totalsNameCmpCB);
3385   int maxLen = 0;
3386   foreach (int idx, ref auto k; stats.kills) {
3387     string s = string(k);
3388     maxLen = max(maxLen, s.length);
3389   }
3390   maxLen *= 8;
3392   sprStore.loadFont('sFontSmall');
3393   GLVideo.color = 0xff_ff_00;
3394   foreach (int idx, ref auto k; stats.kills) {
3395     int deaths = 0;
3396     foreach (int xidx, ref auto d; stats.totalKills) {
3397       if (d.objName == k) { deaths = d.count; break; }
3398     }
3399     //drawTextAt(16, 4+idx*8, va("%s: %d (%d)", string(k.objName).toUpperCase, k.count, deaths));
3400     drawTextAt(16, 4+idx*8, string(k).toUpperCase);
3401     drawTextAt(16+maxLen, 4+idx*8, va(": %d (%d)", k.count, deaths));
3402   }
3403   */
3407 void renderGhostTimer (float currFrameDelta) {
3408   if (ghostTimeLeft <= 0) return;
3409   //ghostTimeLeft /= 30; // frames -> seconds
3411   int hgt = viewHeight-64;
3412   if (hgt < 1) return;
3413   int rhgt = roundi(float(hgt)*(float(ghostTimeLeft)/30.0)/1000.0);
3414   //writeln("hgt=", hgt, "; rhgt=", rhgt, "; gtl=", ghostTimeLeft/30);
3415   if (rhgt > 0) {
3416     auto oclr = GLVideo.color;
3417     GLVideo.color = 0xcf_ff_7f_00;
3418     GLVideo.fillRect(viewWidth-20, 32, 16, hgt-rhgt);
3419     GLVideo.color = 0x7f_ff_7f_00;
3420     GLVideo.fillRect(viewWidth-20, 32+(hgt-rhgt), 16, rhgt);
3421     GLVideo.color = oclr;
3422   }
3426 void renderStarsHUD (float currFrameDelta) {
3427   bool scumSmallHud = global.config.scumSmallHud;
3429   //auto life = max(0, global.plife);
3430   int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3431   //int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3432   //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3434   int hhup;
3436   if (scumSmallHud) {
3437     sprStore.loadFont('sFontSmall');
3438     hhup = 6;
3439   } else {
3440     sprStore.loadFont('sFont');
3441     hhup = 2;
3442   }
3444   GLVideo.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3445   //draw_sprite(sHeart, -1, view_xview[0]+8, view_yview[0]+8);
3446   //draw_text(view_xview[0]+24, view_yview[0]+8, life);
3447   if (scumSmallHud) {
3448     if (global.plife == 1) {
3449       drawSpriteAt('sHeartSmallBlink', global.heartBlink, 10, hhup-4);
3450       global.heartBlink += 0.1;
3451       if (global.heartBlink > 3) global.heartBlink = 0;
3452     } else {
3453       drawSpriteAt('sHeartSmall', -1, 10, hhup-4);
3454       global.heartBlink = 0;
3455     }
3456   } else {
3457     if (global.plife == 1) {
3458       drawSpriteAt('sHeartBlink', global.heartBlink, 8, hhup);
3459       global.heartBlink += 0.1;
3460       if (global.heartBlink > 3) global.heartBlink = 0;
3461     } else {
3462       drawSpriteAt('sHeart', -1, 8, hhup);
3463       global.heartBlink = 0;
3464     }
3465   }
3466   int life = clamp(global.plife, 0, 99);
3467   drawTextAt(16+8, hhup, va("%d", life));
3469   //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3470   drawSpriteAt('sShopkeeperIcon', -1, 64, hhup, scumSmallHud);
3471   drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", starsKills));
3473   if (starsRoomTimer1 > 0) {
3474     sprStore.loadFont('sFontSmall');
3475     GLVideo.color = 0xff_ff_00;
3476     int scale = global.scale;
3477     sprStore.renderMultilineTextCentered(viewWidth/2, 216*scale, va("SHOTGUN CHALLENGE BEGINS IN ~%d~", (starsRoomTimer1/30)+1), scale, 0xff_00_00);
3478   }
3482 void renderSunHUD (float currFrameDelta) {
3483   bool scumSmallHud = global.config.scumSmallHud;
3485   //auto life = max(0, global.plife);
3486   int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3487   //int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3488   //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3490   int hhup;
3492   if (scumSmallHud) {
3493     sprStore.loadFont('sFontSmall');
3494     hhup = 6;
3495   } else {
3496     sprStore.loadFont('sFont');
3497     hhup = 2;
3498   }
3500   GLVideo.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3501   //draw_sprite(sHeart, -1, view_xview[0]+8, view_yview[0]+8);
3502   //draw_text(view_xview[0]+24, view_yview[0]+8, life);
3503   if (scumSmallHud) {
3504     if (global.plife == 1) {
3505       drawSpriteAt('sHeartSmallBlink', global.heartBlink, 10, hhup-4);
3506       global.heartBlink += 0.1;
3507       if (global.heartBlink > 3) global.heartBlink = 0;
3508     } else {
3509       drawSpriteAt('sHeartSmall', -1, 10, hhup-4);
3510       global.heartBlink = 0;
3511     }
3512   } else {
3513     if (global.plife == 1) {
3514       drawSpriteAt('sHeartBlink', global.heartBlink, 8, hhup);
3515       global.heartBlink += 0.1;
3516       if (global.heartBlink > 3) global.heartBlink = 0;
3517     } else {
3518       drawSpriteAt('sHeart', -1, 8, hhup);
3519       global.heartBlink = 0;
3520     }
3521   }
3522   int life = clamp(global.plife, 0, 99);
3523   drawTextAt(16+8, hhup, va("%d", life));
3525   //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3526   drawSpriteAt('sDamselIcon', -1, 64, hhup, scumSmallHud);
3527   drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", sunScore));
3529   if (sunRoomTimer1 > 0) {
3530     sprStore.loadFont('sFontSmall');
3531     GLVideo.color = 0xff_ff_00;
3532     int scale = global.scale;
3533     sprStore.renderMultilineTextCentered(viewWidth/2, 216*scale, va("DAMSEL CHALLENGE BEGINS IN ~%d~", (sunRoomTimer1/30)+1), scale, 0xff_00_00);
3534   }
3538 void renderMoonHUD (float currFrameDelta) {
3539   bool scumSmallHud = global.config.scumSmallHud;
3541   //auto life = max(0, global.plife);
3542   int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3543   //int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3544   //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3546   int hhup;
3548   if (scumSmallHud) {
3549     sprStore.loadFont('sFontSmall');
3550     hhup = 6;
3551   } else {
3552     sprStore.loadFont('sFont');
3553     hhup = 2;
3554   }
3556   GLVideo.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3558   //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3559   drawSpriteAt('sHoopsIcon', -1, 8, hhup, scumSmallHud);
3560   drawTextAt(8+16-(scumSmallHud ? 6 : 0), hhup, va("%d", moonScore));
3561   drawSpriteAt('sTimerIcon', -1, 64, hhup, scumSmallHud);
3562   drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", max(0, moonTimer)));
3564   if (moonRoomTimer1 > 0) {
3565     sprStore.loadFont('sFontSmall');
3566     GLVideo.color = 0xff_ff_00;
3567     int scale = global.scale;
3568     sprStore.renderMultilineTextCentered(viewWidth/2, 216*scale, va("ARCHERY CHALLENGE BEGINS IN ~%d~", (moonRoomTimer1/30)+1), scale, 0xff_00_00);
3569   }
3573 void renderHUD (float currFrameDelta) {
3574   if (levelKind == LevelKind.Stars) { renderStarsHUD(currFrameDelta); return; }
3575   if (levelKind == LevelKind.Sun) { renderSunHUD(currFrameDelta); return; }
3576   if (levelKind == LevelKind.Moon) { renderMoonHUD(currFrameDelta); return; }
3578   if (!isHUDEnabled()) return;
3580   if (levelKind == LevelKind.Transition) { renderTransitionInfo(currFrameDelta); return; }
3582   int lifeX = 4; // 8
3583   int bombX = 56;
3584   int ropeX = 104;
3585   int ammoX = 152;
3586   int moneyX = 200;
3587   int hhup;
3588   bool scumSmallHud = global.config.scumSmallHud;
3589   if (!global.config.optSGAmmo && (!player || player.holdItem !isa ItemWeaponBow)) moneyX = ammoX;
3591   if (scumSmallHud) {
3592     sprStore.loadFont('sFontSmall');
3593     hhup = 6;
3594   } else {
3595     sprStore.loadFont('sFont');
3596     hhup = 0;
3597   }
3598   //int alpha = 0x6f_00_00_00;
3599   int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3600   int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3602   //GLVideo.color = 0xff_ff_ff;
3603   GLVideo.color = 0xff_ff_ff|talpha;
3605   // hearts
3606   if (scumSmallHud) {
3607     if (global.plife == 1) {
3608       drawSpriteAt('sHeartSmallBlink', global.heartBlink, lifeX+2, 4-hhup);
3609       global.heartBlink += 0.1;
3610       if (global.heartBlink > 3) global.heartBlink = 0;
3611     } else {
3612       drawSpriteAt('sHeartSmall', -1, lifeX+2, 4-hhup);
3613       global.heartBlink = 0;
3614     }
3615   } else {
3616     if (global.plife == 1) {
3617       drawSpriteAt('sHeartBlink', global.heartBlink, lifeX, 8-hhup);
3618       global.heartBlink += 0.1;
3619       if (global.heartBlink > 3) global.heartBlink = 0;
3620     } else {
3621       drawSpriteAt('sHeart', -1, lifeX, 8-hhup);
3622       global.heartBlink = 0;
3623     }
3624   }
3626   int life = clamp(global.plife, 0, 99);
3627   //if (!scumHud && life > 99) life = 99;
3628   drawTextAt(lifeX+16, 8-hhup, va("%d", life));
3630   // bombs
3631   if (global.hasStickyBombs && global.stickyBombsActive) {
3632     if (scumSmallHud) drawSpriteAt('sStickyBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sStickyBombIcon', -1, bombX, 8-hhup);
3633   } else {
3634     if (scumSmallHud) drawSpriteAt('sBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sBombIcon', -1, bombX, 8-hhup);
3635   }
3636   int n = global.bombs;
3637   if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3638   drawTextAt(bombX+16, 8-hhup, va("%d", n));
3640   // ropes
3641   if (scumSmallHud) drawSpriteAt('sRopeIconSmall', -1, ropeX+2, 4-hhup); else drawSpriteAt('sRopeIcon', -1, ropeX, 8-hhup);
3642   n = global.rope;
3643   if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3644   drawTextAt(ropeX+16, 8-hhup, va("%d", n));
3646   // shotgun shells
3647   if (global.config.optSGAmmo && (!player || player.holdItem !isa ItemWeaponBow)) {
3648     if (scumSmallHud) drawSpriteAt('sShellsIcon', -1, ammoX+6, 8-hhup); else drawSpriteAt('sShellsIcon', -1, ammoX+7, 12-hhup);
3649     n = global.sgammo;
3650     if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3651     drawTextAt(ammoX+16, 8-hhup, va("%d", n));
3652   } else if (player && player.holdItem isa ItemWeaponBow) {
3653     if (scumSmallHud) drawSpriteAt('sArrowRight', -1, ammoX+6, 8-hhup); else drawSpriteAt('sArrowRight', -1, ammoX+7, 12-hhup);
3654     n = global.arrows;
3655     if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3656     drawTextAt(ammoX+16, 8-hhup, va("%d", n));
3657   }
3659   // money
3660   if (scumSmallHud) drawSpriteAt('sDollarSignSmall', -1, moneyX+2, 3-hhup); else drawSpriteAt('sDollarSign', -1, moneyX, 8-hhup);
3661   drawTextAt(moneyX+16, 8-hhup, va("%d", stats.money-xmoney));
3663   // items
3664   GLVideo.color = 0xff_ff_ff|ialpha;
3666   int ity = (scumSmallHud ? 18-hhup : 24-hhup);
3668   n = 8; //28;
3669   if (global.hasUdjatEye) {
3670     if (global.udjatBlink) drawSpriteAt('sUdjatEyeIcon2', -1, n, ity); else drawSpriteAt('sUdjatEyeIcon', -1, n, ity);
3671     n += 20;
3672   }
3673   if (global.hasAnkh) { drawSpriteAt('sAnkhIcon', -1, n, ity); n += 20; }
3674   if (global.hasCrown) { drawSpriteAt('sCrownIcon', -1, n, ity); n += 20; }
3675   if (global.hasKapala) {
3676          if (global.bloodLevel == 0) drawSpriteAt('sKapalaIcon', 0, n, ity);
3677     else if (global.bloodLevel <= 2) drawSpriteAt('sKapalaIcon', 1, n, ity);
3678     else if (global.bloodLevel <= 4) drawSpriteAt('sKapalaIcon', 2, n, ity);
3679     else if (global.bloodLevel <= 6) drawSpriteAt('sKapalaIcon', 3, n, ity);
3680     else if (global.bloodLevel <= 8) drawSpriteAt('sKapalaIcon', 4, n, ity);
3681     n += 20;
3682   }
3683   if (global.hasSpectacles) { drawSpriteAt('sSpectaclesIcon', -1, n, ity); n += 20; }
3684   if (global.hasGloves) { drawSpriteAt('sGlovesIcon', -1, n, ity); n += 20; }
3685   if (global.hasMitt) { drawSpriteAt('sMittIcon', -1, n, ity); n += 20; }
3686   if (global.hasSpringShoes) { drawSpriteAt('sSpringShoesIcon', -1, n, ity); n += 20; }
3687   if (global.hasJordans) { drawSpriteAt('sJordansIcon', -1, n, ity); n += 20; }
3688   if (global.hasSpikeShoes) { drawSpriteAt('sSpikeShoesIcon', -1, n, ity); n += 20; }
3689   if (global.hasCape) { drawSpriteAt('sCapeIcon', -1, n, ity); n += 20; }
3690   if (global.hasJetpack) { drawSpriteAt('sJetpackIcon', -1, n, ity); n += 20; }
3691   if (global.hasStickyBombs) { drawSpriteAt('sPasteIcon', -1, n, ity); n += 20; }
3692   if (global.hasCompass) { drawSpriteAt('sCompassIcon', -1, n, ity); n += 20; }
3693   if (global.hasParachute) { drawSpriteAt('sParachuteIcon', -1, n, ity); n += 20; }
3695   if (player.holdItem && player.holdItem.objName == 'Bow' && global.arrows > 0) {
3696     int m = 1;
3697     float malpha = 1;
3698     while (m <= global.arrows && m <= 20 && malpha > 0) {
3699       GLVideo.color = trunci(malpha*255)<<24|0xff_ff_ff;
3700       drawSpriteAt('sArrowIcon', -1, n, ity);
3701       n += 4;
3702       if (m > 10) malpha -= 0.1; //and global.arrows > 20 malpha -= 0.1
3703       m += 1;
3704     }
3705   }
3707   if (xmoney > 0) {
3708     sprStore.loadFont('sFontSmall');
3709     GLVideo.color = 0xff_ff_00|talpha;
3710     if (scumSmallHud) drawTextAt(moneyX+8, 8+8-hhup, va("+%d", xmoney));
3711     else drawTextAt(moneyX, 8+16-hhup, va("+%d", xmoney));
3712   }
3714   GLVideo.color = 0xff_ff_ff;
3715   if (global.config.ghostShowTime) renderGhostTimer(currFrameDelta);
3719 // ////////////////////////////////////////////////////////////////////////// //
3720 // x0 and y0 are non-scaled (and will be scaled)
3721 final void drawTextAtS3 (int x0, int y0, string text, optional int hiColor1, optional int hiColor2) {
3722   if (!text) return;
3723   x0 *= 3;
3724   y0 *= 3;
3725   sprStore.renderTextWithHighlight(x0, y0, text, 3, hiColor1!optional, hiColor2!optional);
3729 final void drawTextAtS3Centered (int y0, string text, optional int hiColor1, optional int hiColor2) {
3730   if (!text) return;
3731   int x0 = (viewWidth-sprStore.getTextWidth(text, 3, specified_hiColor1, specified_hiColor2))/2;
3732   sprStore.renderTextWithHighlight(x0, y0*3, text, 3, hiColor1!optional, hiColor2!optional);
3736 void renderHelpOverlay () {
3737   GLVideo.color = 0;
3738   GLVideo.fillRect(0, 0, viewWidth, viewHeight);
3740   int tx = 16;
3741   //int txoff = 0; // text x pos offset (for multi-color lines)
3742   int ty = 8;
3743   if (gameHelpScreen) {
3744     sprStore.loadFont('sFontSmall');
3745     GLVideo.color = 0xff_ff_ff;
3746     drawTextAtS3Centered(ty, va("HELP (PAGE ~%d~ OF ~%d~)", gameHelpScreen, MaxGameHelpScreen), 0xff_ff_00);
3747     ty += 24;
3748   }
3750   if (gameHelpScreen == 1) {
3751     sprStore.loadFont('sFontSmall');
3752     GLVideo.color = 0xff_ff_00; drawTextAtS3(tx, ty, "INVENTORY BASICS"); ty += 16;
3753     GLVideo.color = 0xff_ff_ff;
3754     drawTextAtS3(tx, ty, global.expandString("Press $SWITCH to cycle through items."), 0x00_ff_00);
3755     ty += 8;
3756     ty += 56;
3757     GLVideo.color = 0xff_ff_ff;
3758     drawSpriteAtS3('sHelpSprite1', -1, 64, 96);
3759   } else if (gameHelpScreen == 2) {
3760     sprStore.loadFont('sFontSmall');
3761     GLVideo.color = 0xff_ff_00;
3762     drawTextAtS3(tx, ty, "SELLING TO SHOPKEEPERS"); ty += 16;
3763     GLVideo.color = 0xff_ff_ff;
3764     drawTextAtS3(tx, ty, global.expandString("Press $PAY to offer your currently"), 0x00_ff_00); ty += 8;
3765     drawTextAtS3(tx, ty, "held item to the shopkeeper."); ty += 16;
3766     drawTextAtS3(tx, ty, "If the shopkeeper is interested, "); ty += 8;
3767     //drawTextAtS3(tx, ty, global.expandString("press $PAY again to complete the sale."), 0x00_ff_00); ty += 72;
3768     drawTextAtS3(tx, ty, global.expandString("press $PAY again to complete"), 0x00_ff_00);
3769     drawTextAtS3(tx, ty+8, "the sale.");
3770     ty += 72;
3771     drawSpriteAtS3('sHelpSell', -1, 112, 100);
3772     drawTextAtS3(tx, ty, "Purchasing goods from the shopkeeper"); ty += 8;
3773     drawTextAtS3(tx, ty, "will increase the funds he has"); ty += 8;
3774     drawTextAtS3(tx, ty, "available to buy your unwanted stuff."); ty += 8;
3775   } else {
3776     // map
3777     sprStore.loadFont('sFont');
3778     GLVideo.color = 0xff_ff_ff;
3779     drawTextAtS3(136, 8, "MAP");
3781     if (lg.mapSprite && (isNormalLevel() || isTransitionRoom())) {
3782       GLVideo.color = 0xff_ff_00;
3783       drawTextAtS3Centered(24, lg.mapTitle);
3785       auto spf = sprStore[lg.mapSprite].frames[0];
3786       int mapX = 160-spf.width/2;
3787       int mapY = 120-spf.height/2;
3788       //mapTitleX = 160-string_length(global.mapTitle)*8/2;
3790       GLVideo.color = 0xff_ff_ff;
3791       drawSpriteAtS3(lg.mapSprite, -1, mapX, mapY);
3793       if (lg.mapSprite != 'sMapDefault') {
3794         int mx = -1, my = -1;
3796         // set position of player icon
3797         switch (global.currLevel) {
3798           case 1: mx = 81; my = 22; break;
3799           case 2: mx = 113; my = 63; break;
3800           case 3: mx = 197; my = 86; break;
3801           case 4: mx = 133; my = 109; break;
3802           case 5: mx = 181; my = 22; break;
3803           case 6: mx = 126; my = 64; break;
3804           case 7: mx = 158; my = 112; break;
3805           case 8: mx = 66; my = 80; break;
3806           case 9: mx = 30; my = 26; break;
3807           case 10: mx = 88; my = 54; break;
3808           case 11: mx = 148; my = 81; break;
3809           case 12: mx = 210; my = 205; break;
3810           case 13: mx = 66; my = 17; break;
3811           case 14: mx = 146; my = 17; break;
3812           case 15: mx = 82; my = 77; break;
3813           case 16: mx = 178; my = 81; break;
3814         }
3816         if (mx >= 0) {
3817           int plrx = mx+player.ix/16;
3818           int plry = my+player.iy/16;
3819           if (isTransitionRoom()) { plrx = mx+20; plry = my+16; }
3820           name plrspr = 'sMapSpelunker';
3821                if (global.isDamsel) plrspr = 'sMapDamsel';
3822           else if (global.isTunnelMan) plrspr = 'sMapTunnel';
3823           auto ss = sprStore[plrspr];
3824           drawSpriteAtS3(plrspr, (pausedTime/2)%ss.frames.length, mapX+plrx, mapY+plry);
3825           // exit door icon
3826           if (global.hasCompass && allExits.length) {
3827             drawSpriteAtS3('sMapRedDot', -1, mapX+mx+allExits[0].ix/16, mapY+my+allExits[0].iy/16);
3828           }
3829         }
3830       }
3831     }
3832   }
3834   sprStore.loadFont('sFontSmall');
3835   GLVideo.color = 0xff_ff_00;
3836   drawTextAtS3Centered(232, "PRESS ~SPACE~/~LEFT~/~RIGHT~ TO CHANGE PAGE", 0x00_ff_00);
3838   GLVideo.color = 0xff_ff_ff;
3842 void renderPauseOverlay () {
3843   //drawTextAt(256, 432, "PAUSED", scale);
3845   if (gameShowHelp) { renderHelpOverlay(); return; }
3847   GLVideo.color = 0xff_ff_00;
3848   //int hiColor = 0x00_ff_00;
3850   int n = 120;
3851   if (isTutorialRoom()) {
3852     sprStore.loadFont('sFont');
3853     drawTextAtS3Centered(n-24, "TUTORIAL CAVE");
3854   } else if (isNormalLevel()) {
3855     sprStore.loadFont('sFont');
3857     drawTextAtS3Centered(n-32, va("LEVEL ~%d~", global.currLevel), 0x00_ff_00);
3859     sprStore.loadFont('sFontSmall');
3861     int depth = roundi((174.8*(global.currLevel-1)+(player.iy+8)*0.34)*(global.config.scumMetric ? 0.3048 : 1.0)*10);
3862     string depthStr = va("DEPTH: ~%d.%d~ %s", depth/10, depth%10, (global.config.scumMetric ? "METRES" : "FEET"));
3863     drawTextAtS3Centered(n-16, depthStr, 0x00_ff_00);
3865     n += 16;
3866     drawTextAtS3Centered(n, va("MONEY: ~%d~", stats.money), 0x00_ff_00);
3867     drawTextAtS3Centered(n+16, va("KILLS: ~%d~", stats.countKills), 0x00_ff_00);
3868     drawTextAtS3Centered(n+32, va("SAVES: ~%d~", stats.damselsSaved), 0x00_ff_00);
3869     drawTextAtS3Centered(n+48, va("TIME: ~%s~", time2str(time/30)), 0x00_ff_00);
3870     drawTextAtS3Centered(n+64, va("LEVEL TIME: ~%s~", time2str((time-levelStartTime)/30)), 0x00_ff_00);
3871   }
3873   sprStore.loadFont('sFontSmall');
3874   GLVideo.color = 0xff_ff_ff;
3875   drawTextAtS3Centered(240-2-8, "~ESC~-RETURN  ~F10~-QUIT  ~CTRL+DEL~-SUICIDE", 0xff_7f_00);
3876   drawTextAtS3Centered(2, "~O~PTIONS  REDEFINE ~K~EYS  ~S~TATISTICS", 0xff_7f_00);
3880 // ////////////////////////////////////////////////////////////////////////// //
3881 transient int drawLoot;
3882 transient int drawPosX, drawPosY;
3884 void resetTransitionOverlay () {
3885   drawLoot = 0;
3886   drawPosX = 100;
3887   drawPosY = 83;
3891 // current game, uncollapsed
3892 struct LevelStatInfo {
3893   name aname;
3894   // for transition screen
3895   bool render;
3896   int x, y;
3901 void thinkFrameTransition () {
3902   if (drawLoot == 0) {
3903     if (drawPosX > 272) {
3904       drawPosX = 100;
3905       drawPosY += 2;
3906       if (drawPosY > 83+4) drawPosY = 83;
3907     }
3908   } else if (drawPosX > 232) {
3909     drawPosX = 96;
3910     drawPosY += 2;
3911     if (drawPosY > 91+4) drawPosY = 91;
3912   }
3916 void renderTransitionOverlay () {
3917   sprStore.loadFont('sFontSmall');
3918   GLVideo.color = 0xff_ff_00;
3919   //else if (global.currLevel-1 &lt; 1) draw_text(32, 48, "TUTORIAL CAVE COMPLETED!");
3920   //else draw_text(32, 48, "LEVEL "+string(global.currLevel-1)+" COMPLETED!");
3921   if (global.currLevel == 0) {
3922     drawTextAt(32, 48, "TUTORIAL CAVE COMPLETED!");
3923   } else {
3924     drawTextAt(32, 48, va("LEVEL ~%d~ COMPLETED!", global.currLevel), hiColor1:0x00_ff_ff);
3925   }
3926   GLVideo.color = 0xff_ff_ff;
3927   drawTextAt(32, 64, va("TIME  = ~%s~", time2str((levelEndTime-levelStartTime)/30)), hiColor1:0xff_ff_00);
3929   if (/*stats.collected.length == 0*/stats.money <= levelMoneyStart) {
3930     drawTextAt(32, 80, "LOOT  = ~NONE~", hiColor1:0xff_00_00);
3931   } else {
3932     drawTextAt(32, 80, va("LOOT  = ~%d~", stats.money-levelMoneyStart), hiColor1:0xff_ff_00);
3933   }
3935   if (stats.kills.length == 0) {
3936     drawTextAt(32, 96, "KILLS = ~NONE~", hiColor1:0x00_ff_00);
3937   } else {
3938     drawTextAt(32, 96, va("KILLS = ~%d~", stats.kills.length), hiColor1:0xff_ff_00);
3939   }
3941   drawTextAt(32, 112, va("MONEY = ~%d~", stats.money), hiColor1:0xff_ff_00);
3945 // ////////////////////////////////////////////////////////////////////////// //
3946 private transient array!MapEntity renderVisibleCids;
3947 private transient array!MapEntity renderVisibleLights;
3948 private transient array!MapTile renderFrontTiles; // normal, with fg
3950 final int renderSortByDepth (MapEntity oa, MapEntity ob) {
3951   //auto da = oa.depth, db = ob.depth;
3952   //if (da == db) return (oa.objId < ob.objId);
3953   //return (da < db);
3954   auto d = oa.depth-ob.depth;
3955   return (d ? d : oa.objId-ob.objId);
3959 const int RenderEdgePixNormal = 64;
3960 const int RenderEdgePixLight = 256;
3962 #ifndef EXPERIMENTAL_RENDER_CACHE
3963 enum skipListCreation = false;
3964 #endif
3966 void renderWithOfs (int xofs, int yofs, float currFrameDelta) {
3967   int scale = global.scale;
3969   // don't touch framebuffer alpha
3970   GLVideo.colorMask = GLVideo::CMask.Colors;
3971   GLVideo.color = 0xff_ff_ff;
3973   /*
3974   GLVideo::ScissorRect scsave;
3975   bool doRestoreGL = false;
3977   if (viewOffsetX > 0 || viewOffsetY > 0) {
3978     doRestoreGL = true;
3979     GLVideo.getScissor(scsave);
3980     GLVideo.scissorCombine(viewOffsetX, viewOffsetY, viewWidth, viewHeight);
3981     GLVideo.glPushMatrix();
3982     GLVideo.glTranslate(viewOffsetX, viewOffsetY);
3983     //GLVideo.glTranslate(-550, 0);
3984     //GLVideo.glScale(1, 1);
3985   }
3986   */
3989   bool isDarkLevel = global.darkLevel;
3991   if (isDarkLevel) {
3992     switch (global.config.scumPlayerLit) {
3993       case 0: player.lightRadius = 0; break; // never
3994       case 1: // only in "scumDarkness"
3995         player.lightRadius = (global.config.scumDarkness >= 2 ? 96 : 32);
3996         break;
3997       case 2:
3998         player.lightRadius = 96;
3999         break;
4000     }
4001   }
4003   // render cave background
4004   if (levBGImg) {
4005     int tsz = 16*scale;
4006     int bgw = levBGImg.tex.width*scale;
4007     int bgh = levBGImg.tex.height*scale;
4008     int bgTW = (tilesWidth*tsz+bgw-1)/bgw;
4009     int bgTH = (tilesHeight*tsz+bgh-1)/bgh;
4010     int bgX0 = max(0, xofs/bgw);
4011     int bgY0 = max(0, yofs/bgh);
4012     int bgX1 = min(bgTW, (xofs+viewWidth+bgw-1)/bgw);
4013     int bgY1 = min(bgTH, (yofs+viewHeight+bgh-1)/bgh);
4014     foreach (int ty; bgY0..bgY1) {
4015       foreach (int tx; bgX0..bgX1) {
4016         int x0 = tx*bgw-xofs;
4017         int y0 = ty*bgh-yofs;
4018         levBGImg.blitAt(x0, y0, scale);
4019       }
4020     }
4021   }
4023   int RenderEdgePix = (global.darkLevel ? RenderEdgePixLight : RenderEdgePixNormal);
4025   // render background tiles
4026   for (MapBackTile bt = backtiles; bt; bt = bt.next) {
4027     bt.drawWithOfs(xofs, yofs, scale, currFrameDelta);
4028   }
4030   // collect visible special tiles
4031 #ifdef EXPERIMENTAL_RENDER_CACHE
4032   bool skipListCreation = (lastRenderTime == time && renderVisibleCids.length); //FIXME
4033 #endif
4035   if (!skipListCreation) {
4036     renderVisibleCids.clear();
4037     renderVisibleLights.clear();
4038     renderFrontTiles.clear();
4040     int endVX = xofs+viewWidth;
4041     int endVY = yofs+viewHeight;
4043     // add player
4044     //int cnt = 0;
4045     if (player && player.visible) renderVisibleCids[$] = player; // include player in render list
4047     //FIXME: drop lit objects which cannot affect visible area
4048     if (scale > 1) {
4049       // collect visible objects
4050       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)) {
4051         if (!o.visible) continue;
4052         auto tile = MapTile(o);
4053         if (tile) {
4054           if (isDarkLevel && (o.lightRadius > 4 || tile.litWholeTile)) renderVisibleLights[$] = o;
4055           if (tile.invisible) continue;
4056           if (tile.bgfront /*|| tile.spriteLeftDeco || tile.spriteRightDeco*/) renderFrontTiles[$] = tile;
4057           if (tile.bgback) tile.drawWithOfsBack(xofs, yofs, scale, currFrameDelta);
4058         } else {
4059           if (isDarkLevel && o.lightRadius > 4) renderVisibleLights[$] = o;
4060         }
4061         // check if the object is really visible -- this will speed up later sorting
4062         int fx0, fy0, fx1, fy1;
4063         /*auto*/SpriteFrame spf = o.getSpriteFrame(default, out fx0, out fy0, out fx1, out fy1);
4064         if (!spf) continue; // no sprite -- nothing to draw (no, really)
4065         int ix = o.ix, iy = o.iy;
4066         int x0 = (ix+fx0)*scale, y0 = (iy+fy0)*scale;
4067         int x1 = (ix+fx1)*scale, y1 = (iy+fy1)*scale;
4068         if (x1 <= xofs || y1 <= yofs || x0 >= endVX || y0 >= endVY) {
4069           //++cnt;
4070           continue;
4071         }
4072         renderVisibleCids[$] = o;
4073       }
4074     } else {
4075       foreach (MapEntity o; objGrid.allObjects()) {
4076         if (!o.visible) continue;
4077         auto tile = MapTile(o);
4078         if (tile) {
4079           if (isDarkLevel && (o.lightRadius > 4 || tile.litWholeTile)) renderVisibleLights[$] = o;
4080           if (tile.invisible) continue;
4081           if (tile.bgfront /*|| tile.spriteLeftDeco || tile.spriteRightDeco*/) renderFrontTiles[$] = tile;
4082           if (tile.bgback) tile.drawWithOfsBack(xofs, yofs, scale, currFrameDelta);
4083         } else {
4084           if (isDarkLevel && o.lightRadius > 4) renderVisibleLights[$] = o;
4085         }
4086         renderVisibleCids[$] = o;
4087       }
4088     }
4089     //writeln("::: ", cnt, " invisible objects dropped");
4091     renderVisibleCids.sort(&renderSortByDepth);
4092     lastRenderTime = time;
4093   }
4095   auto depth4Start = 0;
4096   foreach (auto xidx, MapEntity o; renderVisibleCids) {
4097     if (o.depth >= 4) {
4098       depth4Start = xidx;
4099       break;
4100     }
4101   }
4103   bool playerPowerupRendered = false;
4105   // render objects (part one: depth > 3)
4106   foreach (auto idx; depth4Start..renderVisibleCids.length; reverse) {
4107     MapEntity o = renderVisibleCids[idx];
4108     // 1000 is an ordinary tile
4109     if (!playerPowerupRendered && o.depth <= 1200) {
4110       playerPowerupRendered = true;
4111       // so ducking player will have it's cape correctly rendered
4112       if (player.visible) player.drawPrePrePowerupWithOfs(xofs, yofs, scale, currFrameDelta);
4113     }
4114     //if (idx && renderSortByDepth(o, renderVisibleCids[idx-1])) writeln("VIOLATION!");
4115     o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
4116   }
4118   // render object (part two: front tile parts, depth 3.5)
4119   foreach (MapTile tile; renderFrontTiles) {
4120     tile.drawWithOfsFront(xofs, yofs, scale, currFrameDelta);
4121   }
4123   // render objects (part three: depth <= 3)
4124   foreach (auto idx; 0..depth4Start; reverse) {
4125     MapEntity o = renderVisibleCids[idx];
4126     o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
4127     //done above;if (isDarkLevel && (o.lightRadius > 4 || (o isa MapTile && MapTile(o).litWholeTile))) renderVisibleLights[$] = o;
4128   }
4130   // render player post-effects, so cape or jetpack will be correctly rendered on level exit, for example
4131   player.lastDrawWithOfs(xofs, yofs, scale, currFrameDelta);
4133   // lighting
4134   if (isDarkLevel) {
4135     auto ltex = bgtileStore.lightTexture('ltx512', 512);
4137     // set screen alpha to min
4138     GLVideo.colorMask = GLVideo::CMask.Alpha;
4139     GLVideo.blendMode = GLVideo::BlendMode.None;
4140     GLVideo.color = 0xff_ff_ff_ff;
4141     GLVideo.fillRect(0, 0, GLVideo.screenWidth, GLVideo.screenHeight);
4142     //GLVideo.colorMask = GLVideo::CMask.All;
4144     // blend lights
4145     // also, stencil 'em, so we can filter dark areas
4146     GLVideo.textureFiltering = true;
4147     GLVideo.stencil = true;
4148     GLVideo.stencilFunc(GLVideo::StencilFunc.Always, 1);
4149     GLVideo.stencilOp(GLVideo::StencilOp.Keep, GLVideo::StencilOp.Replace);
4150     GLVideo.alphaTestFunc = GLVideo::AlphaFunc.Greater;
4151     GLVideo.alphaTestVal = 0.03+0.011*global.config.darknessDarkness;
4152     GLVideo.color = 0xff_ff_ff;
4153     GLVideo.blendFunc = GLVideo::BlendFunc.Max;
4154     GLVideo.blendMode = GLVideo::BlendMode.Blend; // anything except `Normal`
4155     GLVideo.colorMask = GLVideo::CMask.Alpha;
4157     foreach (MapEntity e; renderVisibleLights) {
4158       int xi, yi;
4159       e.getInterpCoords(currFrameDelta, scale, out xi, out yi);
4160       auto tile = MapTile(e);
4161       if (tile && tile.litWholeTile) {
4162         //GLVideo.color = 0xff_ff_ff;
4163         GLVideo.fillRect(xi-xofs, yi-yofs, e.width*scale, e.height*scale);
4164       }
4165       int lrad = e.lightRadius;
4166       if (lrad < 4) continue; // just in case
4167       lrad += 8;
4168       //if (loserGPU && lrad%12 != 0) lrad = (lrad/12)*12;
4169       float lightscale = float(lrad*scale)/float(ltex.tex.width);
4170 #ifdef OLD_LIGHT_OFFSETS
4171       int fx0, fy0, fx1, fy1;
4172       bool doMirror;
4173       auto spf = e.getSpriteFrame(out doMirror, out fx0, out fy0, out fx1, out fy1);
4174       if (spf) {
4175         xi += (fx1-fx0)*scale/2;
4176         yi += (fy1-fy0)*scale/2;
4177       }
4178 #else
4179       int lxofs, lyofs;
4180       e.getLightOffset(out lxofs, out lyofs);
4181       xi += lxofs*scale;
4182       yi += lyofs*scale;
4184 #endif
4185       lrad = lrad*scale/2;
4186       xi -= xofs+lrad;
4187       yi -= yofs+lrad;
4188       ltex.tex.blitAt(xi, yi, lightscale);
4189     }
4190     GLVideo.textureFiltering = false;
4192     if (!loserGPU) {
4193       // modify only lit parts
4194       GLVideo.stencilFunc(GLVideo::StencilFunc.Equal, 1);
4195       GLVideo.stencilOp(GLVideo::StencilOp.Keep, GLVideo::StencilOp.Keep);
4196       // multiply framebuffer colors by framebuffer alpha
4197       GLVideo.color = 0xff_ff_ff; // it doesn't matter
4198       GLVideo.blendFunc = GLVideo::BlendFunc.Add;
4199       GLVideo.blendMode = GLVideo::BlendMode.DstMulDstAlpha;
4200       GLVideo.colorMask = GLVideo::CMask.Colors;
4201       GLVideo.fillRect(0, 0, GLVideo.screenWidth, GLVideo.screenHeight);
4202     }
4204     // filter unlit parts
4205     GLVideo.stencilFunc(GLVideo::StencilFunc.NotEqual, 1);
4206     GLVideo.stencilOp(GLVideo::StencilOp.Keep, GLVideo::StencilOp.Keep);
4207     GLVideo.blendFunc = GLVideo::BlendFunc.Add;
4208     GLVideo.blendMode = GLVideo::BlendMode.Filter;
4209     GLVideo.colorMask = GLVideo::CMask.Colors;
4210     GLVideo.color = 0x00_00_18+0x00_00_10*global.config.darknessDarkness;
4211     //GLVideo.color = 0x00_00_18;
4212     //GLVideo.color = 0x00_00_38;
4213     GLVideo.fillRect(0, 0, GLVideo.screenWidth, GLVideo.screenHeight);
4215     // restore defaults
4216     GLVideo.blendFunc = GLVideo::BlendFunc.Add;
4217     GLVideo.blendMode = GLVideo::BlendMode.Normal;
4218     GLVideo.colorMask = GLVideo::CMask.All;
4219     GLVideo.alphaTestFunc = GLVideo::AlphaFunc.Always;
4220     GLVideo.stencil = false;
4221   }
4223   // clear visible objects list (nope)
4224   //renderVisibleCids.clear();
4225   //renderVisibleLights.clear();
4228   if (global.config.drawHUD) renderHUD(currFrameDelta);
4229   renderCompass(currFrameDelta);
4231   float osdTimeLeft, osdTimeStart;
4232   string msg = osdGetMessage(out osdTimeLeft, out osdTimeStart);
4233   if (msg) {
4234     auto ct = GetTickCount();
4235     int msgScale = 3;
4236     sprStore.loadFont('sFontSmall');
4237     auto msgHeight = sprStore.getMultilineTextHeight(msg, msgScale);
4238     int x = viewWidth/2;
4239     int y = viewHeight-64-msgHeight;
4240     auto oldColor = GLVideo.color;
4241     GLVideo.color = 0xff_ff_00;
4242     if (osdTimeLeft < 0.5) {
4243       int alpha = 255-clamp(int(510*osdTimeLeft), 0, 255);
4244       GLVideo.color = GLVideo.color|(alpha<<24);
4245     } else if (ct-osdTimeStart < 0.5) {
4246       osdTimeStart = ct-osdTimeStart;
4247       int alpha = 255-clamp(int(510*osdTimeStart), 0, 255);
4248       GLVideo.color = GLVideo.color|(alpha<<24);
4249     }
4250     sprStore.renderMultilineTextCentered(x, y, msg, msgScale, 0x00_ff_00, 0xff_ff_ff);
4251     GLVideo.color = oldColor;
4252   }
4254   int hiColor1, hiColor2;
4255   msg = osdGetTalkMessage(out hiColor1, out hiColor2);
4256   if (msg) {
4257     int msgScale = 2;
4258     sprStore.loadFont('sFontSmall');
4259     auto msgWidth = sprStore.getMultilineTextWidth(msg, processHighlights1:true, processHighlights2:true);
4260     auto msgHeight = sprStore.getMultilineTextHeight(msg);
4261     auto msgWidthOrig = msgWidth*msgScale;
4262     auto msgHeightOrig = msgHeight*msgScale;
4263     if (msgWidth%16 != 0) msgWidth = (msgWidth|0x0f)+1;
4264     if (msgHeight%16 != 0) msgHeight = (msgHeight|0x0f)+1;
4265     msgWidth *= msgScale;
4266     msgHeight *= msgScale;
4267     int x = (viewWidth-msgWidth)/2;
4268     int y = 32*msgScale;
4269     auto oldColor = GLVideo.color;
4270     // draw text frame and text background
4271     GLVideo.color = 0;
4272     GLVideo.fillRect(x, y, msgWidth, msgHeight);
4273     GLVideo.color = 0xff_ff_ff;
4274     for (int fdx = 0; fdx < msgWidth; fdx += 16*msgScale) {
4275       auto spf = sprStore['sMenuTop'].frames[0];
4276       spf.blitAt(x+fdx, y-16*msgScale, msgScale);
4277       spf = sprStore['sMenuBottom'].frames[0];
4278       spf.blitAt(x+fdx, y+msgHeight, msgScale);
4279     }
4280     for (int fdy = 0; fdy < msgHeight; fdy += 16*msgScale) {
4281       auto spf = sprStore['sMenuLeft'].frames[0];
4282       spf.blitAt(x-16*msgScale, y+fdy, msgScale);
4283       spf = sprStore['sMenuRight'].frames[0];
4284       spf.blitAt(x+msgWidth, y+fdy, msgScale);
4285     }
4286     {
4287       auto spf = sprStore['sMenuUL'].frames[0];
4288       spf.blitAt(x-16*msgScale, y-16*msgScale, msgScale);
4289       spf = sprStore['sMenuUR'].frames[0];
4290       spf.blitAt(x+msgWidth, y-16*msgScale, msgScale);
4291       spf = sprStore['sMenuLL'].frames[0];
4292       spf.blitAt(x-16*msgScale, y+msgHeight, msgScale);
4293       spf = sprStore['sMenuLR'].frames[0];
4294       spf.blitAt(x+msgWidth, y+msgHeight, msgScale);
4295     }
4296     GLVideo.color = 0xff_ff_00;
4297     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));
4298     GLVideo.color = oldColor;
4299   }
4301   if (inWinCutscene) renderWinCutsceneOverlay();
4302   if (inIntroCutscene) renderTitleCutsceneOverlay();
4303   if (isTransitionRoom()) renderTransitionOverlay();
4305   /*
4306   if (doRestoreGL) {
4307     GLVideo.setScissor(scsave);
4308     GLVideo.glPopMatrix();
4309   }
4310   */
4312   GLVideo.color = 0xff_ff_ff;
4316 // ////////////////////////////////////////////////////////////////////////// //
4317 final class!MapObject findGameObjectClassByName (name aname) {
4318   if (!aname) return none; // just in case
4319   auto co = FindClassByGameObjName(aname);
4320   if (!co) {
4321     writeln("***UNKNOWN GAME OBJECT: '", aname, "'");
4322     return none;
4323   }
4324   co = GetClassReplacement(co);
4325   if (!co) FatalError("findGameObjectClassByName: WTF?!");
4326   if (!class!MapObject(co)) FatalError("findGameTileClassByName: object '%n' is not a map object, it is a `%n`", aname, GetClassName(co));
4327   return class!MapObject(co);
4331 final class!MapTile findGameTileClassByName (name aname) {
4332   if (!aname) return none; // just in case
4333   auto co = FindClassByGameObjName(aname);
4334   if (!co) return MapTile; // unknown names will be routed directly to tile object
4335   co = GetClassReplacement(co);
4336   if (!co) FatalError("findGameTileClassByName: WTF?!");
4337   if (!class!MapTile(co)) FatalError("findGameTileClassByName: object '%n' is not a tile, it is a `%n`", aname, GetClassName(co));
4338   return class!MapTile(co);
4342 final MapObject findAnyObjectOfType (name aname) {
4343   if (!aname) return none;
4344   auto cls = FindClassByGameObjName(aname);
4345   if (!cls) return none;
4346   foreach (MapObject obj; objGrid.allObjects(MapObject)) {
4347     if (obj.spectral) continue;
4348     if (obj isa cls) return obj;
4349   }
4350   return none;
4354 // ////////////////////////////////////////////////////////////////////////// //
4355 final bool isRopePlacedAt (int x, int y) {
4356   int[8] covered;
4357   foreach (ref auto v; covered) v = false;
4358   foreach (MapTile t; objGrid.inRectPix(x, y-8, 1, 17, precise:false, castClass:MapTileRope)) {
4359     //if (!cbIsRopeTile(t)) continue;
4360     if (t.ix != x) continue;
4361     if (t.iy == y) return true;
4362     foreach (int ty; t.iy..t.iy+8) {
4363       int d = ty-y;
4364       if (d >= 0 && d < covered.length) covered[d] = true;
4365     }
4366   }
4367   // check if the whole rope height is completely covered with ropes
4368   foreach (auto v; covered) if (!v) return false;
4369   return true;
4373 final MapTile CreateMapTile (int xpos, int ypos, name aname) {
4374   if (!aname) FatalError("cannot create typeless tile");
4375   auto tclass = findGameTileClassByName(aname);
4376   if (!tclass) return none;
4377   MapTile tile = SpawnObject(tclass);
4378   tile.global = global;
4379   tile.level = self;
4380   tile.objName = aname;
4381   tile.objType = aname; // just in case
4382   tile.fltx = xpos;
4383   tile.flty = ypos;
4384   tile.objId = ++lastUsedObjectId;
4385   if (!tile.initialize()) FatalError("cannot create tile '%n'", aname);
4386   return tile;
4390 final bool PutSpawnedMapTile (int x, int y, MapTile tile) {
4391   if (!tile || !tile.isInstanceAlive) return false;
4393   //if (putToGrid) tile.active = true;
4394   bool putToGrid = (tile.moveable || tile.toSpecialGrid || tile.width != 16 || tile.height != 16 || x%16 != 0 || y%16 != 0);
4396   //writeln("putting tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (putToGrid ? "special " : ""), "grid, pos=(", x/16.0, ",", y/16.0, ")");
4398   if (!putToGrid) {
4399     int mapx = x/16, mapy = y/16;
4400     if (mapx < 0 || mapx >= tilesWidth || mapy < 0 || mapy >= tilesHeight) return false;
4401   }
4403   // if we already have rope tile there, there is no reason to add another one
4404   if (tile isa MapTileRope) {
4405     if (isRopePlacedAt(x, y)) return false;
4406   }
4408   // activate special or animated tile
4409   tile.active = tile.active || tile.moveable || tile.toSpecialGrid;
4410   // animated tiles must be active
4411   if (!tile.active) {
4412     auto spr = tile.getSprite();
4413     if (spr && spr.frames.length > 1) {
4414       writeln("activated animated tile '", tile.objName, "'");
4415       tile.active = true;
4416     }
4417   }
4419   tile.fltx = x;
4420   tile.flty = y;
4421   if (putToGrid) {
4422     //if (tile isa TitleTileCopy) writeln("*** PUTTING COPYRIGHT TILE");
4423     //tile.toSpecialGrid = true;
4424     if (!tile.dontReplaceOthers && x%16 == 0 && y%16 == 0) {
4425       auto t = getTileAtGridAny(x/16, y/16);
4426       if (t && !t.immuneToReplacement) {
4427         writeln("REPLACING tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
4428         writeln("      NEW tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (tile.toSpecialGrid ? "special " : ""), "grid, pos=(", tile.fltx/16.0, ",", tile.flty/16.0, ")");
4429         t.instanceRemove();
4430       }
4431     }
4432     insertObject(tile);
4433   } else {
4434     //writeln("SIZE: ", tilesWidth, "x", tilesHeight);
4435     setTileAtGrid(x/16, y/16, tile);
4436     /*
4437     auto t = getTileAtGridAny(x/16, y/16);
4438     if (t != tile) {
4439       writeln("putting tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (putToGrid ? "special " : ""), "grid, pos=(", x/16.0, ",", y/16.0, ")");
4440       checkTilesInRect(x/16, y/16, 16, 16, delegate bool (MapTile tile) {
4441         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, ")");
4442         return false;
4443       });
4444       FatalError("FUUUUUU");
4445     }
4446     */
4447   }
4449   if (tile.enter) registerEnter(tile);
4450   if (tile.exit) registerExit(tile);
4452   // make tile under exit invulnerable
4453   if (checkTilesInRect(tile.ix, tile.iy-16, 16, 16, delegate bool (MapTile t) { return t.exit; })) {
4454     tile.invincible = true;
4455   }
4457   return true;
4461 // won't call `onDestroy()`
4462 final void RemoveMapTileFromGrid (int tileX, int tileY, optional string reason) {
4463   if (tileX >= 0 && tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
4464     auto t = getTileAtGridAny(tileX, tileY);
4465     if (t) {
4466       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, ")");
4467       t.instanceRemove();
4468       checkWater = true;
4469     }
4470   }
4474 final MapTile MakeMapTile (int mapx, int mapy, name aname) {
4475   //writeln("tile at (", mapx, ",", mapy, "): ", aname);
4476   //if (aname == 'oLush') { MapObject fail; fail.initialize(); }
4477   //if (mapy >= tilesHeight) { writeln("OOM: ", aname, "; ty=", mapy, "; hgt=", tilesHeight); }
4478   if (mapx < 0 || mapx >= tilesWidth || mapy < 0 || mapy >= tilesHeight) return none;
4480   // if we already have rope tile there, there is no reason to add another one
4481   if (aname == 'oRope') {
4482     if (isRopePlacedAt(mapx*16, mapy*16)) return none;
4483   }
4485   auto tile = CreateMapTile(mapx*16, mapy*16, aname);
4486   if (!tile) return none;
4487   if (!PutSpawnedMapTile(mapx*16, mapy*16, tile)) {
4488     delete tile;
4489     tile = none;
4490   }
4492   return tile;
4496 final MapTile MakeMapTileAtPix (int xpix, int ypix, name aname) {
4497   // if we already have rope tile there, there is no reason to add another one
4498   if (aname == 'oRope') {
4499     if (isRopePlacedAt(xpix, ypix)) return none;
4500   }
4502   auto tile = CreateMapTile(xpix, ypix, aname);
4503   if (!tile) return none;
4504   if (!PutSpawnedMapTile(xpix, ypix, tile)) {
4505     delete tile;
4506     tile = none;
4507   }
4509   return tile;
4513 final MapTile MakeMapRopeTileAt (int x0, int y0) {
4514   // if we already have rope tile there, there is no reason to add another one
4515   if (isRopePlacedAt(x0, y0)) return none;
4517   auto tile = CreateMapTile(x0, y0, 'oRope');
4518   if (!PutSpawnedMapTile(x0, y0, tile)) {
4519     delete tile;
4520     tile = none;
4521   }
4523   return tile;
4527 // ////////////////////////////////////////////////////////////////////////// //
4528 final MapBackTile CreateBgTile (name sprName, optional int atx0, optional int aty0, optional int aw, optional int ah) {
4529   BackTileImage img = bgtileStore[sprName];
4530   auto res = SpawnObject(MapBackTile);
4531   res.global = global;
4532   res.level = self;
4533   res.bgt = img;
4534   res.bgtName = sprName;
4535   if (specified_atx0) res.tx0 = atx0;
4536   if (specified_aty0) res.ty0 = aty0;
4537   if (specified_aw) res.w = aw;
4538   if (specified_ah) res.h = ah;
4539   if (!res.initialize()) FatalError("cannot create background tile with image '%n'", sprName);
4540   return res;
4544 // ////////////////////////////////////////////////////////////////////////// //
4546 background The background asset from which the new tile will be extracted.
4547 left The x coordinate of the left of the new tile, relative to the background asset's top left corner.
4548 top The y coordinate of the top of the new tile, relative to the background assets top left corner.
4549 width The width of the tile.
4550 height The height of the tile.
4551 x The x position in the room to place the tile.
4552 y The y position in the room to place the tile.
4553 depth The depth at which to place the tile.
4555 final void MakeMapBackTile (name bgname, int left, int top, int width, int height, int x, int y, int depth) {
4556   if (width < 1 || height < 1 || !bgname) return;
4557   auto bgt = bgtileStore[bgname];
4558   if (!bgt) FatalError("cannot load background '%n'", bgname);
4559   MapBackTile bt = SpawnObject(MapBackTile);
4560   bt.global = global;
4561   bt.level = self;
4562   bt.objName = bgname;
4563   bt.bgt = bgt;
4564   bt.bgtName = bgname;
4565   bt.fltx = x;
4566   bt.flty = y;
4567   bt.tx0 = left;
4568   bt.ty0 = top;
4569   bt.w = width;
4570   bt.h = height;
4571   bt.depth = depth;
4572   // find a place for it
4573   if (!backtiles) {
4574     backtiles = bt;
4575     return;
4576   }
4577   // back tiles with the highest depth should come first
4578   MapBackTile ct = backtiles, cprev = none;
4579   while (ct && depth <= ct.depth) { cprev = ct; ct = ct.next; }
4580   // insert before ct
4581   if (cprev) {
4582     bt.next = cprev.next;
4583     cprev.next = bt;
4584   } else {
4585     bt.next = backtiles;
4586     backtiles = bt;
4587   }
4591 // ////////////////////////////////////////////////////////////////////////// //
4592 final MapObject SpawnMapObjectWithClass (class!MapObject oclass) {
4593   if (!oclass) return none;
4595   MapObject obj = SpawnObject(oclass);
4596   if (!obj) FatalError("cannot create map object '%n'", GetClassName(oclass));
4598   //if (aname == 'oShells' || aname == 'oShellSingle' || aname == 'oRopePile') writeln("*** CREATED '", aname, "'");
4600   obj.global = global;
4601   obj.level = self;
4602   obj.objId = ++lastUsedObjectId;
4604   return obj;
4608 final MapObject SpawnMapObject (name aname) {
4609   if (!aname) return none;
4610   auto res = SpawnMapObjectWithClass(findGameObjectClassByName(aname));
4611   if (res && !res.objType) res.objType = aname; // just in case
4612   return res;
4616 final MapObject PutSpawnedMapObject (int x, int y, MapObject obj) {
4617   if (!obj /*|| obj.global || obj.level*/) return none; // oops
4619   obj.fltx = x;
4620   obj.flty = y;
4621   if (!obj.initialize()) { delete obj; return none; } // not fatal
4623   insertObject(obj);
4624   if (obj.walkableSolid) hasSolidObjects = true;
4626   return obj;
4630 final MapObject MakeMapObject (int x, int y, name aname) {
4631   MapObject obj = SpawnMapObject(aname);
4632   obj = PutSpawnedMapObject(x, y, obj);
4633   return obj;
4637 // ////////////////////////////////////////////////////////////////////////// //
4638 void setMenuTilesVisible (bool vis) {
4639   if (vis) {
4640     forEachTile(delegate bool (MapTile t) {
4641       if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4642         t.invisible = false;
4643       }
4644       return false;
4645     });
4646   } else {
4647     forEachTile(delegate bool (MapTile t) {
4648       if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4649         t.invisible = true;
4650       }
4651       return false;
4652     });
4653   }
4657 void setMenuTilesOnTop () {
4658   forEachTile(delegate bool (MapTile t) {
4659     if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4660       t.depth = 1;
4661     }
4662     return false;
4663   });
4667 // ////////////////////////////////////////////////////////////////////////// //
4668 #include "roomTitle.vc"
4669 #include "roomTrans1.vc"
4670 #include "roomTrans2.vc"
4671 #include "roomTrans3.vc"
4672 #include "roomTrans4.vc"
4673 #include "roomOlmec.vc"
4674 #include "roomEnd.vc"
4675 #include "roomIntro.vc"
4676 #include "roomTutorial.vc"
4677 #include "roomScores.vc"
4678 #include "roomStars.vc"
4679 #include "roomSun.vc"
4680 #include "roomMoon.vc"
4683 // ////////////////////////////////////////////////////////////////////////// //
4684 #include "packages/Generator/loadRoomGens.vc"
4685 #include "packages/Generator/loadEntityGens.vc"