as VaVoom C built-in array sort is using TimSort now, comparator callbacks should...
[k8vacspelynky.git] / GameLevel.vc
blobfeae4b358df65f106dba04511117b7ff8535a457
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 = ImmediateDelete;
891   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   ImmediateDelete = olddel;
908   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 = ImmediateDelete;
962   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   ImmediateDelete = olddel;
1021   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 = ImmediateDelete;
1061   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   ImmediateDelete = olddel;
1119   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 round(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 round(sqrt(curdistsq));
1312 // ////////////////////////////////////////////////////////////////////////// //
1313 final void clearForTransition () {
1314   auto olddel = ImmediateDelete;
1315   ImmediateDelete = false;
1316   clearWholeLevel();
1317   ImmediateDelete = olddel;
1318   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 = round(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]+floor(view_hview[0] / 2), oGhost);
1627     else instance_create(view_xview[0]-32, view_yview[0]+floor(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 = ImmediateDelete;
2028     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     ImmediateDelete = olddel;
2039     if (olddel) 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 = ImmediateDelete;
2232   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   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     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 round(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 round(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 : round(what.favor/2.0)); //k8: added `round()`
3081     if (what.favor > 0) what.favor = 0;
3082   } else {
3083     global.favor += (what.status == MapObject::STUNNED ? what.favor : round(what.favor/2.0)); //k8: added `round()`
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);
3146 final void centerViewAtPlayer () {
3147   if (viewWidth < 1 || viewHeight < 1 || !player) return;
3148   centerViewAt(player.xCenter, player.yCenter);
3152 final void centerViewAt (int x, int y) {
3153   if (viewWidth < 1 || viewHeight < 1) return;
3155   cameraSlideToSpeed.x = 0;
3156   cameraSlideToSpeed.y = 0;
3157   cameraSlideToPlayer = 0;
3159   int scale = global.scale;
3160   x *= scale;
3161   y *= scale;
3162   realViewStart.x = clamp(x-viewWidth/2, 0, tilesWidth*16*scale-viewWidth);
3163   realViewStart.y = clamp(y-viewHeight/2, 0, tilesHeight*16*scale-viewHeight);
3164   fixRealViewStart();
3166   viewStart.x = realViewStart.x;
3167   viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
3168   fixViewStart();
3170   if (onCameraTeleported) onCameraTeleported();
3174 const int ViewPortToleranceX = 16*1+8;
3175 const int ViewPortToleranceY = 16*1+8;
3177 final void fixCamera () {
3178   if (!player) return;
3179   if (viewWidth < 1 || viewHeight < 1) return;
3180   int scale = global.scale;
3181   auto alwaysCenterX = global.config.alwaysCenterPlayer;
3182   auto alwaysCenterY = alwaysCenterX;
3183   // calculate offset from viewport center (in game units), and fix viewport
3185   int camDestX = player.ix+8;
3186   int camDestY = player.iy+8;
3187   if (!cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y) != 0) {
3188     // slide camera to point
3189     if (cameraSlideToSpeed.x) camDestX = cameraSlideToCurr.x;
3190     if (cameraSlideToSpeed.y) camDestY = cameraSlideToCurr.y;
3191     int dx = cameraSlideToDest.x-camDestX;
3192     int dy = cameraSlideToDest.y-camDestY;
3193     //writeln("speed:(", cameraSlideToSpeed.x, ",", cameraSlideToSpeed.y, "); cur:(", camDestX, ",", camDestY, "); dest:(", cameraSlideToDest.x, ",", cameraSlideToDest.y, "); delta=(", dx, ",", dy, ")");
3194     if (dx && cameraSlideToSpeed.x != 0) {
3195       alwaysCenterX = true;
3196       if (abs(dx) <= abs(cameraSlideToSpeed.x)) {
3197         camDestX = cameraSlideToDest.x;
3198       } else {
3199         camDestX += sign(dx)*abs(cameraSlideToSpeed.x);
3200       }
3201     }
3202     if (dy && abs(cameraSlideToSpeed.y) != 0) {
3203       alwaysCenterY = true;
3204       if (abs(dy) <= cameraSlideToSpeed.y) {
3205         camDestY = cameraSlideToDest.y;
3206       } else {
3207         camDestY += sign(dy)*abs(cameraSlideToSpeed.y);
3208       }
3209     }
3210     //writeln("  new:(", camDestX, ",", camDestY, ")");
3211     if (cameraSlideToSpeed.x) cameraSlideToCurr.x = camDestX;
3212     if (cameraSlideToSpeed.y) cameraSlideToCurr.y = camDestY;
3213   }
3215   // horizontal
3216   if (cameraSlideToSpeed.x && !cameraSlideToPlayer) {
3217     realViewStart.x = clamp(camDestX*scale, 0, tilesWidth*16*scale-viewWidth);
3218   } else if (!player.cameraBlockX) {
3219     int x = camDestX*scale;
3220     int cx = realViewStart.x;
3221     if (alwaysCenterX) {
3222       cx = x-viewWidth/2;
3223     } else {
3224       int xofs = x-(cx+viewWidth/2);
3225            if (xofs < -ViewPortToleranceX*scale) cx += xofs+ViewPortToleranceX*scale;
3226       else if (xofs > ViewPortToleranceX*scale) cx += xofs-ViewPortToleranceX*scale;
3227     }
3228     // slide back to player?
3229     if (cameraSlideToPlayer && cameraSlideToSpeed.x) {
3230       int prevx = cameraSlideToCurr.x*scale;
3231       int dx = (cx-prevx)/scale;
3232       if (abs(dx) <= cameraSlideToSpeed.x) {
3233         writeln("BACKSLIDE X COMPLETE!");
3234         cameraSlideToSpeed.x = 0;
3235       } else {
3236         cx = prevx+(sign(dx)*cameraSlideToSpeed.x)*scale;
3237         cameraSlideToCurr.x += sign(dx)*cameraSlideToSpeed.x;
3238         if ((dx < 0 && cx <= 0) || (dx > 0 && cx >= tilesWidth*16*scale-viewWidth)) {
3239           writeln("BACKSLIDE X COMPLETE!");
3240           cameraSlideToSpeed.x = 0;
3241         }
3242       }
3243     }
3244     realViewStart.x = clamp(cx, 0, tilesWidth*16*scale-viewWidth);
3245   }
3247   // vertical
3248   if (cameraSlideToSpeed.y && !cameraSlideToPlayer) {
3249     realViewStart.y = clamp(camDestY*scale, 0, tilesHeight*16*scale-viewHeight);
3250   } else if (!player.cameraBlockY) {
3251     int y = camDestY*scale;
3252     int cy = realViewStart.y;
3253     if (alwaysCenterY) {
3254       cy = y-viewHeight/2;
3255     } else {
3256       int yofs = y-(cy+viewHeight/2);
3257            if (yofs < -ViewPortToleranceY*scale) cy += yofs+ViewPortToleranceY*scale;
3258       else if (yofs > ViewPortToleranceY*scale) cy += yofs-ViewPortToleranceY*scale;
3259     }
3260     // slide back to player?
3261     if (cameraSlideToPlayer && cameraSlideToSpeed.y) {
3262       int prevy = cameraSlideToCurr.y*scale;
3263       int dy = (cy-prevy)/scale;
3264       if (abs(dy) <= cameraSlideToSpeed.y) {
3265         writeln("BACKSLIDE Y COMPLETE!");
3266         cameraSlideToSpeed.y = 0;
3267       } else {
3268         cy = prevy+(sign(dy)*cameraSlideToSpeed.y)*scale;
3269         cameraSlideToCurr.y += sign(dy)*cameraSlideToSpeed.y;
3270         if ((dy < 0 && cy <= 0) || (dy > 0 && cy >= tilesHeight*16*scale-viewHeight)) {
3271           writeln("BACKSLIDE Y COMPLETE!");
3272           cameraSlideToSpeed.y = 0;
3273         }
3274       }
3275     }
3276     realViewStart.y = clamp(cy, 0, tilesHeight*16*scale-viewHeight);
3277   }
3279   if (cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y) == 0) cameraSlideToPlayer = 0;
3281   fixRealViewStart();
3282   //writeln("  new2:(", cameraCurrX, ",", cameraCurrY, ")");
3284   viewStart.x = realViewStart.x;
3285   viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
3286   fixViewStart();
3290 // ////////////////////////////////////////////////////////////////////////// //
3291 // x0 and y0 are non-scaled (and will be scaled)
3292 final void drawSpriteAt (name sprName, float frnumf, int x0, int y0, optional bool small) {
3293   if (!sprName) return;
3294   auto spr = sprStore[sprName];
3295   if (!spr || !spr.frames.length) return;
3296   int scale = global.scale;
3297   x0 *= scale;
3298   y0 *= scale;
3299   int frnum = max(0, trunc(frnumf))%spr.frames.length;
3300   auto sfr = spr.frames[frnum];
3301   int sx0 = x0-sfr.xofs*scale;
3302   int sy0 = y0-sfr.yofs*scale;
3303   if (small && scale > 1) {
3304     sfr.tex.blitExt(sx0, sy0, round(sx0+sfr.width*(scale/2.0)), round(sy0+sfr.height*(scale/2.0)), 0, 0);
3305   } else {
3306     sfr.blitAt(sx0, sy0, scale);
3307   }
3311 final void drawSpriteAtS3 (name sprName, float frnumf, int x0, int y0) {
3312   if (!sprName) return;
3313   auto spr = sprStore[sprName];
3314   if (!spr || !spr.frames.length) return;
3315   x0 *= 3;
3316   y0 *= 3;
3317   int frnum = max(0, trunc(frnumf))%spr.frames.length;
3318   auto sfr = spr.frames[frnum];
3319   int sx0 = x0-sfr.xofs*3;
3320   int sy0 = y0-sfr.yofs*3;
3321   sfr.blitAt(sx0, sy0, 3);
3325 // x0 and y0 are non-scaled (and will be scaled)
3326 final void drawTextAt (int x0, int y0, string text, optional int scale, optional int hiColor1, optional int hiColor2) {
3327   if (!text) return;
3328   if (!specified_scale) scale = global.scale;
3329   x0 *= scale;
3330   y0 *= scale;
3331   sprStore.renderTextWithHighlight(x0, y0, text, scale, hiColor1!optional, hiColor2!optional);
3335 void renderCompass (float currFrameDelta) {
3336   if (!global.hasCompass) return;
3338   /*
3339   if (isRoom("rOlmec")) {
3340     global.exitX = 648;
3341     global.exitY = 552;
3342   } else if (isRoom("rOlmec2")) {
3343     global.exitX = 648;
3344     global.exitY = 424;
3345   }
3346   */
3348   bool hasMessage = osdHasMessage();
3349   foreach (MapTile et; allExits) {
3350     // original compass
3351     int exitX = et.ix, exitY = et.iy;
3352     int vx0 = viewStart.x/global.scale, vy0 = viewStart.y/global.scale;
3353     int vx1 = (viewStart.x+viewWidth)/global.scale;
3354     int vy1 = (viewStart.y+viewHeight)/global.scale;
3355     if (exitY > vy1-16) {
3356       if (exitX < vx0) {
3357         drawSpriteAt(hasMessage ? 'sCompassSmallLL' : 'sCompassLL', 0, 0, 224);
3358       } else if (exitX > vx1-16) {
3359         drawSpriteAt(hasMessage ? 'sCompassSmallLR' : 'sCompassLR', 0, 304, 224);
3360       } else {
3361         drawSpriteAt(hasMessage ? 'sCompassSmallDown' : 'sCompassDown', 0, exitX-vx0, 224);
3362       }
3363     } else if (exitX < vx0) {
3364       drawSpriteAt(hasMessage ? 'sCompassSmallLeft' : 'sCompassLeft', 0, 0, exitY-vy0);
3365     } else if (exitX > vx1-16) {
3366       drawSpriteAt(hasMessage ? 'sCompassSmallRight' : 'sCompassRight', 0, 304, exitY-vy0);
3367     }
3368     break; // only the first exit
3369   }
3373 final bool totalsNameCmpCB (ref GameStats::TotalItem a, ref GameStats::TotalItem b) {
3374   auto sa = string(a.objName);
3375   auto sb = string(b.objName);
3376   return (sa < sb);
3379 void renderTransitionInfo (float currFrameDelta) {
3380   //FIXME!
3381   /*
3382   GameStats.sortTotalsList(stats.kills, &totalsNameCmpCB);
3384   int maxLen = 0;
3385   foreach (int idx, ref auto k; stats.kills) {
3386     string s = string(k);
3387     maxLen = max(maxLen, s.length);
3388   }
3389   maxLen *= 8;
3391   sprStore.loadFont('sFontSmall');
3392   Video.color = 0xff_ff_00;
3393   foreach (int idx, ref auto k; stats.kills) {
3394     int deaths = 0;
3395     foreach (int xidx, ref auto d; stats.totalKills) {
3396       if (d.objName == k) { deaths = d.count; break; }
3397     }
3398     //drawTextAt(16, 4+idx*8, va("%s: %d (%d)", string(k.objName).toUpperCase, k.count, deaths));
3399     drawTextAt(16, 4+idx*8, string(k).toUpperCase);
3400     drawTextAt(16+maxLen, 4+idx*8, va(": %d (%d)", k.count, deaths));
3401   }
3402   */
3406 void renderGhostTimer (float currFrameDelta) {
3407   if (ghostTimeLeft <= 0) return;
3408   //ghostTimeLeft /= 30; // frames -> seconds
3410   int hgt = viewHeight-64;
3411   if (hgt < 1) return;
3412   int rhgt = round(float(hgt)*(float(ghostTimeLeft)/30.0)/1000.0);
3413   //writeln("hgt=", hgt, "; rhgt=", rhgt, "; gtl=", ghostTimeLeft/30);
3414   if (rhgt > 0) {
3415     auto oclr = Video.color;
3416     Video.color = 0xcf_ff_7f_00;
3417     Video.fillRect(viewWidth-20, 32, 16, hgt-rhgt);
3418     Video.color = 0x7f_ff_7f_00;
3419     Video.fillRect(viewWidth-20, 32+(hgt-rhgt), 16, rhgt);
3420     Video.color = oclr;
3421   }
3425 void renderStarsHUD (float currFrameDelta) {
3426   bool scumSmallHud = global.config.scumSmallHud;
3428   //auto life = max(0, global.plife);
3429   int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3430   int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3431   //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3433   int hhup;
3435   if (scumSmallHud) {
3436     sprStore.loadFont('sFontSmall');
3437     hhup = 6;
3438   } else {
3439     sprStore.loadFont('sFont');
3440     hhup = 2;
3441   }
3443   Video.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3444   //draw_sprite(sHeart, -1, view_xview[0]+8, view_yview[0]+8);
3445   //draw_text(view_xview[0]+24, view_yview[0]+8, life);
3446   if (scumSmallHud) {
3447     if (global.plife == 1) {
3448       drawSpriteAt('sHeartSmallBlink', global.heartBlink, 10, hhup-4);
3449       global.heartBlink += 0.1;
3450       if (global.heartBlink > 3) global.heartBlink = 0;
3451     } else {
3452       drawSpriteAt('sHeartSmall', -1, 10, hhup-4);
3453       global.heartBlink = 0;
3454     }
3455   } else {
3456     if (global.plife == 1) {
3457       drawSpriteAt('sHeartBlink', global.heartBlink, 8, hhup);
3458       global.heartBlink += 0.1;
3459       if (global.heartBlink > 3) global.heartBlink = 0;
3460     } else {
3461       drawSpriteAt('sHeart', -1, 8, hhup);
3462       global.heartBlink = 0;
3463     }
3464   }
3465   int life = clamp(global.plife, 0, 99);
3466   drawTextAt(16+8, hhup, va("%d", life));
3468   //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3469   drawSpriteAt('sShopkeeperIcon', -1, 64, hhup, scumSmallHud);
3470   drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", starsKills));
3472   if (starsRoomTimer1 > 0) {
3473     sprStore.loadFont('sFontSmall');
3474     Video.color = 0xff_ff_00;
3475     int scale = global.scale;
3476     sprStore.renderMultilineTextCentered(viewWidth/2, 216*scale, va("SHOTGUN CHALLENGE BEGINS IN ~%d~", (starsRoomTimer1/30)+1), scale, 0xff_00_00);
3477   }
3481 void renderSunHUD (float currFrameDelta) {
3482   bool scumSmallHud = global.config.scumSmallHud;
3484   //auto life = max(0, global.plife);
3485   int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3486   int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3487   //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3489   int hhup;
3491   if (scumSmallHud) {
3492     sprStore.loadFont('sFontSmall');
3493     hhup = 6;
3494   } else {
3495     sprStore.loadFont('sFont');
3496     hhup = 2;
3497   }
3499   Video.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3500   //draw_sprite(sHeart, -1, view_xview[0]+8, view_yview[0]+8);
3501   //draw_text(view_xview[0]+24, view_yview[0]+8, life);
3502   if (scumSmallHud) {
3503     if (global.plife == 1) {
3504       drawSpriteAt('sHeartSmallBlink', global.heartBlink, 10, hhup-4);
3505       global.heartBlink += 0.1;
3506       if (global.heartBlink > 3) global.heartBlink = 0;
3507     } else {
3508       drawSpriteAt('sHeartSmall', -1, 10, hhup-4);
3509       global.heartBlink = 0;
3510     }
3511   } else {
3512     if (global.plife == 1) {
3513       drawSpriteAt('sHeartBlink', global.heartBlink, 8, hhup);
3514       global.heartBlink += 0.1;
3515       if (global.heartBlink > 3) global.heartBlink = 0;
3516     } else {
3517       drawSpriteAt('sHeart', -1, 8, hhup);
3518       global.heartBlink = 0;
3519     }
3520   }
3521   int life = clamp(global.plife, 0, 99);
3522   drawTextAt(16+8, hhup, va("%d", life));
3524   //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3525   drawSpriteAt('sDamselIcon', -1, 64, hhup, scumSmallHud);
3526   drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", sunScore));
3528   if (sunRoomTimer1 > 0) {
3529     sprStore.loadFont('sFontSmall');
3530     Video.color = 0xff_ff_00;
3531     int scale = global.scale;
3532     sprStore.renderMultilineTextCentered(viewWidth/2, 216*scale, va("DAMSEL CHALLENGE BEGINS IN ~%d~", (sunRoomTimer1/30)+1), scale, 0xff_00_00);
3533   }
3537 void renderMoonHUD (float currFrameDelta) {
3538   bool scumSmallHud = global.config.scumSmallHud;
3540   //auto life = max(0, global.plife);
3541   int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3542   int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3543   //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3545   int hhup;
3547   if (scumSmallHud) {
3548     sprStore.loadFont('sFontSmall');
3549     hhup = 6;
3550   } else {
3551     sprStore.loadFont('sFont');
3552     hhup = 2;
3553   }
3555   Video.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3557   //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3558   drawSpriteAt('sHoopsIcon', -1, 8, hhup, scumSmallHud);
3559   drawTextAt(8+16-(scumSmallHud ? 6 : 0), hhup, va("%d", moonScore));
3560   drawSpriteAt('sTimerIcon', -1, 64, hhup, scumSmallHud);
3561   drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", max(0, moonTimer)));
3563   if (moonRoomTimer1 > 0) {
3564     sprStore.loadFont('sFontSmall');
3565     Video.color = 0xff_ff_00;
3566     int scale = global.scale;
3567     sprStore.renderMultilineTextCentered(viewWidth/2, 216*scale, va("ARCHERY CHALLENGE BEGINS IN ~%d~", (moonRoomTimer1/30)+1), scale, 0xff_00_00);
3568   }
3572 void renderHUD (float currFrameDelta) {
3573   if (levelKind == LevelKind.Stars) { renderStarsHUD(currFrameDelta); return; }
3574   if (levelKind == LevelKind.Sun) { renderSunHUD(currFrameDelta); return; }
3575   if (levelKind == LevelKind.Moon) { renderMoonHUD(currFrameDelta); return; }
3577   if (!isHUDEnabled()) return;
3579   if (levelKind == LevelKind.Transition) { renderTransitionInfo(currFrameDelta); return; }
3581   int lifeX = 4; // 8
3582   int bombX = 56;
3583   int ropeX = 104;
3584   int ammoX = 152;
3585   int moneyX = 200;
3586   int hhup;
3587   bool scumSmallHud = global.config.scumSmallHud;
3588   if (!global.config.optSGAmmo && (!player || player.holdItem !isa ItemWeaponBow)) moneyX = ammoX;
3590   if (scumSmallHud) {
3591     sprStore.loadFont('sFontSmall');
3592     hhup = 6;
3593   } else {
3594     sprStore.loadFont('sFont');
3595     hhup = 0;
3596   }
3597   //int alpha = 0x6f_00_00_00;
3598   int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3599   int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3601   //Video.color = 0xff_ff_ff;
3602   Video.color = 0xff_ff_ff|talpha;
3604   // hearts
3605   if (scumSmallHud) {
3606     if (global.plife == 1) {
3607       drawSpriteAt('sHeartSmallBlink', global.heartBlink, lifeX+2, 4-hhup);
3608       global.heartBlink += 0.1;
3609       if (global.heartBlink > 3) global.heartBlink = 0;
3610     } else {
3611       drawSpriteAt('sHeartSmall', -1, lifeX+2, 4-hhup);
3612       global.heartBlink = 0;
3613     }
3614   } else {
3615     if (global.plife == 1) {
3616       drawSpriteAt('sHeartBlink', global.heartBlink, lifeX, 8-hhup);
3617       global.heartBlink += 0.1;
3618       if (global.heartBlink > 3) global.heartBlink = 0;
3619     } else {
3620       drawSpriteAt('sHeart', -1, lifeX, 8-hhup);
3621       global.heartBlink = 0;
3622     }
3623   }
3625   int life = clamp(global.plife, 0, 99);
3626   //if (!scumHud && life > 99) life = 99;
3627   drawTextAt(lifeX+16, 8-hhup, va("%d", life));
3629   // bombs
3630   if (global.hasStickyBombs && global.stickyBombsActive) {
3631     if (scumSmallHud) drawSpriteAt('sStickyBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sStickyBombIcon', -1, bombX, 8-hhup);
3632   } else {
3633     if (scumSmallHud) drawSpriteAt('sBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sBombIcon', -1, bombX, 8-hhup);
3634   }
3635   int n = global.bombs;
3636   if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3637   drawTextAt(bombX+16, 8-hhup, va("%d", n));
3639   // ropes
3640   if (scumSmallHud) drawSpriteAt('sRopeIconSmall', -1, ropeX+2, 4-hhup); else drawSpriteAt('sRopeIcon', -1, ropeX, 8-hhup);
3641   n = global.rope;
3642   if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3643   drawTextAt(ropeX+16, 8-hhup, va("%d", n));
3645   // shotgun shells
3646   if (global.config.optSGAmmo && (!player || player.holdItem !isa ItemWeaponBow)) {
3647     if (scumSmallHud) drawSpriteAt('sShellsIcon', -1, ammoX+6, 8-hhup); else drawSpriteAt('sShellsIcon', -1, ammoX+7, 12-hhup);
3648     n = global.sgammo;
3649     if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3650     drawTextAt(ammoX+16, 8-hhup, va("%d", n));
3651   } else if (player && player.holdItem isa ItemWeaponBow) {
3652     if (scumSmallHud) drawSpriteAt('sArrowRight', -1, ammoX+6, 8-hhup); else drawSpriteAt('sArrowRight', -1, ammoX+7, 12-hhup);
3653     n = global.arrows;
3654     if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3655     drawTextAt(ammoX+16, 8-hhup, va("%d", n));
3656   }
3658   // money
3659   if (scumSmallHud) drawSpriteAt('sDollarSignSmall', -1, moneyX+2, 3-hhup); else drawSpriteAt('sDollarSign', -1, moneyX, 8-hhup);
3660   drawTextAt(moneyX+16, 8-hhup, va("%d", stats.money-xmoney));
3662   // items
3663   Video.color = 0xff_ff_ff|ialpha;
3665   int ity = (scumSmallHud ? 18-hhup : 24-hhup);
3667   n = 8; //28;
3668   if (global.hasUdjatEye) {
3669     if (global.udjatBlink) drawSpriteAt('sUdjatEyeIcon2', -1, n, ity); else drawSpriteAt('sUdjatEyeIcon', -1, n, ity);
3670     n += 20;
3671   }
3672   if (global.hasAnkh) { drawSpriteAt('sAnkhIcon', -1, n, ity); n += 20; }
3673   if (global.hasCrown) { drawSpriteAt('sCrownIcon', -1, n, ity); n += 20; }
3674   if (global.hasKapala) {
3675          if (global.bloodLevel == 0) drawSpriteAt('sKapalaIcon', 0, n, ity);
3676     else if (global.bloodLevel <= 2) drawSpriteAt('sKapalaIcon', 1, n, ity);
3677     else if (global.bloodLevel <= 4) drawSpriteAt('sKapalaIcon', 2, n, ity);
3678     else if (global.bloodLevel <= 6) drawSpriteAt('sKapalaIcon', 3, n, ity);
3679     else if (global.bloodLevel <= 8) drawSpriteAt('sKapalaIcon', 4, n, ity);
3680     n += 20;
3681   }
3682   if (global.hasSpectacles) { drawSpriteAt('sSpectaclesIcon', -1, n, ity); n += 20; }
3683   if (global.hasGloves) { drawSpriteAt('sGlovesIcon', -1, n, ity); n += 20; }
3684   if (global.hasMitt) { drawSpriteAt('sMittIcon', -1, n, ity); n += 20; }
3685   if (global.hasSpringShoes) { drawSpriteAt('sSpringShoesIcon', -1, n, ity); n += 20; }
3686   if (global.hasJordans) { drawSpriteAt('sJordansIcon', -1, n, ity); n += 20; }
3687   if (global.hasSpikeShoes) { drawSpriteAt('sSpikeShoesIcon', -1, n, ity); n += 20; }
3688   if (global.hasCape) { drawSpriteAt('sCapeIcon', -1, n, ity); n += 20; }
3689   if (global.hasJetpack) { drawSpriteAt('sJetpackIcon', -1, n, ity); n += 20; }
3690   if (global.hasStickyBombs) { drawSpriteAt('sPasteIcon', -1, n, ity); n += 20; }
3691   if (global.hasCompass) { drawSpriteAt('sCompassIcon', -1, n, ity); n += 20; }
3692   if (global.hasParachute) { drawSpriteAt('sParachuteIcon', -1, n, ity); n += 20; }
3694   if (player.holdItem && player.holdItem.objName == 'Bow' && global.arrows > 0) {
3695     int m = 1;
3696     float malpha = 1;
3697     while (m <= global.arrows && m <= 20 && malpha > 0) {
3698       Video.color = trunc(malpha*255)<<24|0xff_ff_ff;
3699       drawSpriteAt('sArrowIcon', -1, n, ity);
3700       n += 4;
3701       if (m > 10) malpha -= 0.1; //and global.arrows > 20 malpha -= 0.1
3702       m += 1;
3703     }
3704   }
3706   if (xmoney > 0) {
3707     sprStore.loadFont('sFontSmall');
3708     Video.color = 0xff_ff_00|talpha;
3709     if (scumSmallHud) drawTextAt(moneyX+8, 8+8-hhup, va("+%d", xmoney));
3710     else drawTextAt(moneyX, 8+16-hhup, va("+%d", xmoney));
3711   }
3713   Video.color = 0xff_ff_ff;
3714   if (global.config.ghostShowTime) renderGhostTimer(currFrameDelta);
3718 // ////////////////////////////////////////////////////////////////////////// //
3719 // x0 and y0 are non-scaled (and will be scaled)
3720 final void drawTextAtS3 (int x0, int y0, string text, optional int hiColor1, optional int hiColor2) {
3721   if (!text) return;
3722   x0 *= 3;
3723   y0 *= 3;
3724   sprStore.renderTextWithHighlight(x0, y0, text, 3, hiColor1!optional, hiColor2!optional);
3728 final void drawTextAtS3Centered (int y0, string text, optional int hiColor1, optional int hiColor2) {
3729   if (!text) return;
3730   int x0 = (viewWidth-sprStore.getTextWidth(text, 3, specified_hiColor1, specified_hiColor2))/2;
3731   sprStore.renderTextWithHighlight(x0, y0*3, text, 3, hiColor1!optional, hiColor2!optional);
3735 void renderHelpOverlay () {
3736   Video.color = 0;
3737   Video.fillRect(0, 0, viewWidth, viewHeight);
3739   int tx = 16;
3740   int txoff = 0; // text x pos offset (for multi-color lines)
3741   int ty = 8;
3742   if (gameHelpScreen) {
3743     sprStore.loadFont('sFontSmall');
3744     Video.color = 0xff_ff_ff;
3745     drawTextAtS3Centered(ty, va("HELP (PAGE ~%d~ OF ~%d~)", gameHelpScreen, MaxGameHelpScreen), 0xff_ff_00);
3746     ty += 24;
3747   }
3749   if (gameHelpScreen == 1) {
3750     sprStore.loadFont('sFontSmall');
3751     Video.color = 0xff_ff_00; drawTextAtS3(tx, ty, "INVENTORY BASICS"); ty += 16;
3752     Video.color = 0xff_ff_ff;
3753     drawTextAtS3(tx, ty, global.expandString("Press $SWITCH to cycle through items."), 0x00_ff_00);
3754     ty += 8;
3755     ty += 56;
3756     Video.color = 0xff_ff_ff;
3757     drawSpriteAtS3('sHelpSprite1', -1, 64, 96);
3758   } else if (gameHelpScreen == 2) {
3759     sprStore.loadFont('sFontSmall');
3760     Video.color = 0xff_ff_00;
3761     drawTextAtS3(tx, ty, "SELLING TO SHOPKEEPERS"); ty += 16;
3762     Video.color = 0xff_ff_ff;
3763     drawTextAtS3(tx, ty, global.expandString("Press $PAY to offer your currently"), 0x00_ff_00); ty += 8;
3764     drawTextAtS3(tx, ty, "held item to the shopkeeper."); ty += 16;
3765     drawTextAtS3(tx, ty, "If the shopkeeper is interested, "); ty += 8;
3766     //drawTextAtS3(tx, ty, global.expandString("press $PAY again to complete the sale."), 0x00_ff_00); ty += 72;
3767     drawTextAtS3(tx, ty, global.expandString("press $PAY again to complete"), 0x00_ff_00);
3768     drawTextAtS3(tx, ty+8, "the sale.");
3769     ty += 72;
3770     drawSpriteAtS3('sHelpSell', -1, 112, 100);
3771     drawTextAtS3(tx, ty, "Purchasing goods from the shopkeeper"); ty += 8;
3772     drawTextAtS3(tx, ty, "will increase the funds he has"); ty += 8;
3773     drawTextAtS3(tx, ty, "available to buy your unwanted stuff."); ty += 8;
3774   } else {
3775     // map
3776     sprStore.loadFont('sFont');
3777     Video.color = 0xff_ff_ff;
3778     drawTextAtS3(136, 8, "MAP");
3780     if (lg.mapSprite && (isNormalLevel() || isTransitionRoom())) {
3781       Video.color = 0xff_ff_00;
3782       drawTextAtS3Centered(24, lg.mapTitle);
3784       auto spf = sprStore[lg.mapSprite].frames[0];
3785       int mapX = 160-spf.width/2;
3786       int mapY = 120-spf.height/2;
3787       //mapTitleX = 160-string_length(global.mapTitle)*8/2;
3789       Video.color = 0xff_ff_ff;
3790       drawSpriteAtS3(lg.mapSprite, -1, mapX, mapY);
3792       if (lg.mapSprite != 'sMapDefault') {
3793         int mx = -1, my = -1;
3795         // set position of player icon
3796         switch (global.currLevel) {
3797           case 1: mx = 81; my = 22; break;
3798           case 2: mx = 113; my = 63; break;
3799           case 3: mx = 197; my = 86; break;
3800           case 4: mx = 133; my = 109; break;
3801           case 5: mx = 181; my = 22; break;
3802           case 6: mx = 126; my = 64; break;
3803           case 7: mx = 158; my = 112; break;
3804           case 8: mx = 66; my = 80; break;
3805           case 9: mx = 30; my = 26; break;
3806           case 10: mx = 88; my = 54; break;
3807           case 11: mx = 148; my = 81; break;
3808           case 12: mx = 210; my = 205; break;
3809           case 13: mx = 66; my = 17; break;
3810           case 14: mx = 146; my = 17; break;
3811           case 15: mx = 82; my = 77; break;
3812           case 16: mx = 178; my = 81; break;
3813         }
3815         if (mx >= 0) {
3816           int plrx = mx+player.ix/16;
3817           int plry = my+player.iy/16;
3818           if (isTransitionRoom()) { plrx = mx+20; plry = my+16; }
3819           name plrspr = 'sMapSpelunker';
3820                if (global.isDamsel) plrspr = 'sMapDamsel';
3821           else if (global.isTunnelMan) plrspr = 'sMapTunnel';
3822           auto ss = sprStore[plrspr];
3823           drawSpriteAtS3(plrspr, (pausedTime/2)%ss.frames.length, mapX+plrx, mapY+plry);
3824           // exit door icon
3825           if (global.hasCompass && allExits.length) {
3826             drawSpriteAtS3('sMapRedDot', -1, mapX+mx+allExits[0].ix/16, mapY+my+allExits[0].iy/16);
3827           }
3828         }
3829       }
3830     }
3831   }
3833   sprStore.loadFont('sFontSmall');
3834   Video.color = 0xff_ff_00;
3835   drawTextAtS3Centered(232, "PRESS ~SPACE~/~LEFT~/~RIGHT~ TO CHANGE PAGE", 0x00_ff_00);
3837   Video.color = 0xff_ff_ff;
3841 void renderPauseOverlay () {
3842   //drawTextAt(256, 432, "PAUSED", scale);
3844   if (gameShowHelp) { renderHelpOverlay(); return; }
3846   Video.color = 0xff_ff_00;
3847   //int hiColor = 0x00_ff_00;
3849   int n = 120;
3850   if (isTutorialRoom()) {
3851     sprStore.loadFont('sFont');
3852     drawTextAtS3Centered(n-24, "TUTORIAL CAVE");
3853   } else if (isNormalLevel()) {
3854     sprStore.loadFont('sFont');
3856     drawTextAtS3Centered(n-32, va("LEVEL ~%d~", global.currLevel), 0x00_ff_00);
3858     sprStore.loadFont('sFontSmall');
3860     int depth = round((174.8*(global.currLevel-1)+(player.iy+8)*0.34)*(global.config.scumMetric ? 0.3048 : 1.0)*10);
3861     string depthStr = va("DEPTH: ~%d.%d~ %s", depth/10, depth%10, (global.config.scumMetric ? "METRES" : "FEET"));
3862     drawTextAtS3Centered(n-16, depthStr, 0x00_ff_00);
3864     n += 16;
3865     drawTextAtS3Centered(n, va("MONEY: ~%d~", stats.money), 0x00_ff_00);
3866     drawTextAtS3Centered(n+16, va("KILLS: ~%d~", stats.countKills), 0x00_ff_00);
3867     drawTextAtS3Centered(n+32, va("SAVES: ~%d~", stats.damselsSaved), 0x00_ff_00);
3868     drawTextAtS3Centered(n+48, va("TIME: ~%s~", time2str(time/30)), 0x00_ff_00);
3869     drawTextAtS3Centered(n+64, va("LEVEL TIME: ~%s~", time2str((time-levelStartTime)/30)), 0x00_ff_00);
3870   }
3872   sprStore.loadFont('sFontSmall');
3873   Video.color = 0xff_ff_ff;
3874   drawTextAtS3Centered(240-2-8, "~ESC~-RETURN  ~F10~-QUIT  ~CTRL+DEL~-SUICIDE", 0xff_7f_00);
3875   drawTextAtS3Centered(2, "~O~PTIONS  REDEFINE ~K~EYS  ~S~TATISTICS", 0xff_7f_00);
3879 // ////////////////////////////////////////////////////////////////////////// //
3880 transient int drawLoot;
3881 transient int drawPosX, drawPosY;
3883 void resetTransitionOverlay () {
3884   drawLoot = 0;
3885   drawPosX = 100;
3886   drawPosY = 83;
3890 // current game, uncollapsed
3891 struct LevelStatInfo {
3892   name aname;
3893   // for transition screen
3894   bool render;
3895   int x, y;
3900 void thinkFrameTransition () {
3901   if (drawLoot == 0) {
3902     if (drawPosX > 272) {
3903       drawPosX = 100;
3904       drawPosY += 2;
3905       if (drawPosY > 83+4) drawPosY = 83;
3906     }
3907   } else if (drawPosX > 232) {
3908     drawPosX = 96;
3909     drawPosY += 2;
3910     if (drawPosY > 91+4) drawPosY = 91;
3911   }
3915 void renderTransitionOverlay () {
3916   sprStore.loadFont('sFontSmall');
3917   Video.color = 0xff_ff_00;
3918   //else if (global.currLevel-1 &lt; 1) draw_text(32, 48, "TUTORIAL CAVE COMPLETED!");
3919   //else draw_text(32, 48, "LEVEL "+string(global.currLevel-1)+" COMPLETED!");
3920   if (global.currLevel == 0) {
3921     drawTextAt(32, 48, "TUTORIAL CAVE COMPLETED!");
3922   } else {
3923     drawTextAt(32, 48, va("LEVEL ~%d~ COMPLETED!", global.currLevel), hiColor1:0x00_ff_ff);
3924   }
3925   Video.color = 0xff_ff_ff;
3926   drawTextAt(32, 64, va("TIME  = ~%s~", time2str((levelEndTime-levelStartTime)/30)), hiColor1:0xff_ff_00);
3928   if (/*stats.collected.length == 0*/stats.money <= levelMoneyStart) {
3929     drawTextAt(32, 80, "LOOT  = ~NONE~", hiColor1:0xff_00_00);
3930   } else {
3931     drawTextAt(32, 80, va("LOOT  = ~%d~", stats.money-levelMoneyStart), hiColor1:0xff_ff_00);
3932   }
3934   if (stats.kills.length == 0) {
3935     drawTextAt(32, 96, "KILLS = ~NONE~", hiColor1:0x00_ff_00);
3936   } else {
3937     drawTextAt(32, 96, va("KILLS = ~%d~", stats.kills.length), hiColor1:0xff_ff_00);
3938   }
3940   drawTextAt(32, 112, va("MONEY = ~%d~", stats.money), hiColor1:0xff_ff_00);
3944 // ////////////////////////////////////////////////////////////////////////// //
3945 private transient array!MapEntity renderVisibleCids;
3946 private transient array!MapEntity renderVisibleLights;
3947 private transient array!MapTile renderFrontTiles; // normal, with fg
3949 final int renderSortByDepth (MapEntity oa, MapEntity ob) {
3950   //auto da = oa.depth, db = ob.depth;
3951   //if (da == db) return (oa.objId < ob.objId);
3952   //return (da < db);
3953   auto d = oa.depth-ob.depth;
3954   return (d ? d : oa.objId-ob.objId);
3958 const int RenderEdgePixNormal = 64;
3959 const int RenderEdgePixLight = 256;
3961 #ifndef EXPERIMENTAL_RENDER_CACHE
3962 enum skipListCreation = false;
3963 #endif
3965 void renderWithOfs (int xofs, int yofs, float currFrameDelta) {
3966   int scale = global.scale;
3968   // don't touch framebuffer alpha
3969   Video.colorMask = Video::CMask.Colors;
3970   Video.color = 0xff_ff_ff;
3972   /*
3973   Video::ScissorRect scsave;
3974   bool doRestoreGL = false;
3976   if (viewOffsetX > 0 || viewOffsetY > 0) {
3977     doRestoreGL = true;
3978     Video.getScissor(scsave);
3979     Video.scissorCombine(viewOffsetX, viewOffsetY, viewWidth, viewHeight);
3980     Video.glPushMatrix();
3981     Video.glTranslate(viewOffsetX, viewOffsetY);
3982     //Video.glTranslate(-550, 0);
3983     //Video.glScale(1, 1);
3984   }
3985   */
3988   bool isDarkLevel = global.darkLevel;
3990   if (isDarkLevel) {
3991     switch (global.config.scumPlayerLit) {
3992       case 0: player.lightRadius = 0; break; // never
3993       case 1: // only in "scumDarkness"
3994         player.lightRadius = (global.config.scumDarkness >= 2 ? 96 : 32);
3995         break;
3996       case 2:
3997         player.lightRadius = 96;
3998         break;
3999     }
4000   }
4002   // render cave background
4003   if (levBGImg) {
4004     int tsz = 16*scale;
4005     int bgw = levBGImg.tex.width*scale;
4006     int bgh = levBGImg.tex.height*scale;
4007     int bgTW = (tilesWidth*tsz+bgw-1)/bgw;
4008     int bgTH = (tilesHeight*tsz+bgh-1)/bgh;
4009     int bgX0 = max(0, xofs/bgw);
4010     int bgY0 = max(0, yofs/bgh);
4011     int bgX1 = min(bgTW, (xofs+viewWidth+bgw-1)/bgw);
4012     int bgY1 = min(bgTH, (yofs+viewHeight+bgh-1)/bgh);
4013     foreach (int ty; bgY0..bgY1) {
4014       foreach (int tx; bgX0..bgX1) {
4015         int x0 = tx*bgw-xofs;
4016         int y0 = ty*bgh-yofs;
4017         levBGImg.blitAt(x0, y0, scale);
4018       }
4019     }
4020   }
4022   int RenderEdgePix = (global.darkLevel ? RenderEdgePixLight : RenderEdgePixNormal);
4024   // render background tiles
4025   for (MapBackTile bt = backtiles; bt; bt = bt.next) {
4026     bt.drawWithOfs(xofs, yofs, scale, currFrameDelta);
4027   }
4029   // collect visible special tiles
4030 #ifdef EXPERIMENTAL_RENDER_CACHE
4031   bool skipListCreation = (lastRenderTime == time && renderVisibleCids.length); //FIXME
4032 #endif
4034   if (!skipListCreation) {
4035     renderVisibleCids.clear();
4036     renderVisibleLights.clear();
4037     renderFrontTiles.clear();
4039     int endVX = xofs+viewWidth;
4040     int endVY = yofs+viewHeight;
4042     // add player
4043     //int cnt = 0;
4044     if (player && player.visible) renderVisibleCids[$] = player; // include player in render list
4046     //FIXME: drop lit objects which cannot affect visible area
4047     if (scale > 1) {
4048       // collect visible objects
4049       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)) {
4050         if (!o.visible) continue;
4051         auto tile = MapTile(o);
4052         if (tile) {
4053           if (isDarkLevel && (o.lightRadius > 4 || tile.litWholeTile)) renderVisibleLights[$] = o;
4054           if (tile.invisible) continue;
4055           if (tile.bgfront /*|| tile.spriteLeftDeco || tile.spriteRightDeco*/) renderFrontTiles[$] = tile;
4056           if (tile.bgback) tile.drawWithOfsBack(xofs, yofs, scale, currFrameDelta);
4057         } else {
4058           if (isDarkLevel && o.lightRadius > 4) renderVisibleLights[$] = o;
4059         }
4060         // check if the object is really visible -- this will speed up later sorting
4061         int fx0, fy0, fx1, fy1;
4062         auto spf = o.getSpriteFrame(default, out fx0, out fy0, out fx1, out fy1);
4063         if (!spf) continue; // no sprite -- nothing to draw (no, really)
4064         int ix = o.ix, iy = o.iy;
4065         int x0 = (ix+fx0)*scale, y0 = (iy+fy0)*scale;
4066         int x1 = (ix+fx1)*scale, y1 = (iy+fy1)*scale;
4067         if (x1 <= xofs || y1 <= yofs || x0 >= endVX || y0 >= endVY) {
4068           //++cnt;
4069           continue;
4070         }
4071         renderVisibleCids[$] = o;
4072       }
4073     } else {
4074       foreach (MapEntity o; objGrid.allObjects()) {
4075         if (!o.visible) continue;
4076         auto tile = MapTile(o);
4077         if (tile) {
4078           if (isDarkLevel && (o.lightRadius > 4 || tile.litWholeTile)) renderVisibleLights[$] = o;
4079           if (tile.invisible) continue;
4080           if (tile.bgfront /*|| tile.spriteLeftDeco || tile.spriteRightDeco*/) renderFrontTiles[$] = tile;
4081           if (tile.bgback) tile.drawWithOfsBack(xofs, yofs, scale, currFrameDelta);
4082         } else {
4083           if (isDarkLevel && o.lightRadius > 4) renderVisibleLights[$] = o;
4084         }
4085         renderVisibleCids[$] = o;
4086       }
4087     }
4088     //writeln("::: ", cnt, " invisible objects dropped");
4090     renderVisibleCids.sort(&renderSortByDepth);
4091     lastRenderTime = time;
4092   }
4094   auto depth4Start = 0;
4095   foreach (auto xidx, MapEntity o; renderVisibleCids) {
4096     if (o.depth >= 4) {
4097       depth4Start = xidx;
4098       break;
4099     }
4100   }
4102   bool playerPowerupRendered = false;
4104   // render objects (part one: depth > 3)
4105   foreach (auto idx; depth4Start..renderVisibleCids.length; reverse) {
4106     MapEntity o = renderVisibleCids[idx];
4107     // 1000 is an ordinary tile
4108     if (!playerPowerupRendered && o.depth <= 1200) {
4109       playerPowerupRendered = true;
4110       // so ducking player will have it's cape correctly rendered
4111       if (player.visible) player.drawPrePrePowerupWithOfs(xofs, yofs, scale, currFrameDelta);
4112     }
4113     //if (idx && renderSortByDepth(o, renderVisibleCids[idx-1])) writeln("VIOLATION!");
4114     o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
4115   }
4117   // render object (part two: front tile parts, depth 3.5)
4118   foreach (MapTile tile; renderFrontTiles) {
4119     tile.drawWithOfsFront(xofs, yofs, scale, currFrameDelta);
4120   }
4122   // render objects (part three: depth <= 3)
4123   foreach (auto idx; 0..depth4Start; reverse) {
4124     MapEntity o = renderVisibleCids[idx];
4125     o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
4126     //done above;if (isDarkLevel && (o.lightRadius > 4 || (o isa MapTile && MapTile(o).litWholeTile))) renderVisibleLights[$] = o;
4127   }
4129   // render player post-effects, so cape or jetpack will be correctly rendered on level exit, for example
4130   player.lastDrawWithOfs(xofs, yofs, scale, currFrameDelta);
4132   // lighting
4133   if (isDarkLevel) {
4134     auto ltex = bgtileStore.lightTexture('ltx512', 512);
4136     // set screen alpha to min
4137     Video.colorMask = Video::CMask.Alpha;
4138     Video.blendMode = Video::BlendMode.None;
4139     Video.color = 0xff_ff_ff_ff;
4140     Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
4141     //Video.colorMask = Video::CMask.All;
4143     // blend lights
4144     // also, stencil 'em, so we can filter dark areas
4145     Video.textureFiltering = true;
4146     Video.stencil = true;
4147     Video.stencilFunc(Video::StencilFunc.Always, 1);
4148     Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Replace);
4149     Video.alphaTestFunc = Video::AlphaFunc.Greater;
4150     Video.alphaTestVal = 0.03+0.011*global.config.darknessDarkness;
4151     Video.color = 0xff_ff_ff;
4152     Video.blendFunc = Video::BlendFunc.Max;
4153     Video.blendMode = Video::BlendMode.Blend; // anything except `Normal`
4154     Video.colorMask = Video::CMask.Alpha;
4156     foreach (MapEntity e; renderVisibleLights) {
4157       int xi, yi;
4158       e.getInterpCoords(currFrameDelta, scale, out xi, out yi);
4159       auto tile = MapTile(e);
4160       if (tile && tile.litWholeTile) {
4161         //Video.color = 0xff_ff_ff;
4162         Video.fillRect(xi-xofs, yi-yofs, e.width*scale, e.height*scale);
4163       }
4164       int lrad = e.lightRadius;
4165       if (lrad < 4) continue; // just in case
4166       lrad += 8;
4167       //if (loserGPU && lrad%12 != 0) lrad = (lrad/12)*12;
4168       float lightscale = float(lrad*scale)/float(ltex.tex.width);
4169 #ifdef OLD_LIGHT_OFFSETS
4170       int fx0, fy0, fx1, fy1;
4171       bool doMirror;
4172       auto spf = e.getSpriteFrame(out doMirror, out fx0, out fy0, out fx1, out fy1);
4173       if (spf) {
4174         xi += (fx1-fx0)*scale/2;
4175         yi += (fy1-fy0)*scale/2;
4176       }
4177 #else
4178       int lxofs, lyofs;
4179       e.getLightOffset(out lxofs, out lyofs);
4180       xi += lxofs*scale;
4181       yi += lyofs*scale;
4183 #endif
4184       lrad = lrad*scale/2;
4185       xi -= xofs+lrad;
4186       yi -= yofs+lrad;
4187       ltex.tex.blitAt(xi, yi, lightscale);
4188     }
4189     Video.textureFiltering = false;
4191     if (!loserGPU) {
4192       // modify only lit parts
4193       Video.stencilFunc(Video::StencilFunc.Equal, 1);
4194       Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Keep);
4195       // multiply framebuffer colors by framebuffer alpha
4196       Video.color = 0xff_ff_ff; // it doesn't matter
4197       Video.blendFunc = Video::BlendFunc.Add;
4198       Video.blendMode = Video::BlendMode.DstMulDstAlpha;
4199       Video.colorMask = Video::CMask.Colors;
4200       Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
4201     }
4203     // filter unlit parts
4204     Video.stencilFunc(Video::StencilFunc.NotEqual, 1);
4205     Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Keep);
4206     Video.blendFunc = Video::BlendFunc.Add;
4207     Video.blendMode = Video::BlendMode.Filter;
4208     Video.colorMask = Video::CMask.Colors;
4209     Video.color = 0x00_00_18+0x00_00_10*global.config.darknessDarkness;
4210     //Video.color = 0x00_00_18;
4211     //Video.color = 0x00_00_38;
4212     Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
4214     // restore defaults
4215     Video.blendFunc = Video::BlendFunc.Add;
4216     Video.blendMode = Video::BlendMode.Normal;
4217     Video.colorMask = Video::CMask.All;
4218     Video.alphaTestFunc = Video::AlphaFunc.Always;
4219     Video.stencil = false;
4220   }
4222   // clear visible objects list (nope)
4223   //renderVisibleCids.clear();
4224   //renderVisibleLights.clear();
4227   if (global.config.drawHUD) renderHUD(currFrameDelta);
4228   renderCompass(currFrameDelta);
4230   float osdTimeLeft, osdTimeStart;
4231   string msg = osdGetMessage(out osdTimeLeft, out osdTimeStart);
4232   if (msg) {
4233     auto ct = GetTickCount();
4234     int msgScale = 3;
4235     sprStore.loadFont('sFontSmall');
4236     auto msgHeight = sprStore.getMultilineTextHeight(msg, msgScale);
4237     int x = viewWidth/2;
4238     int y = viewHeight-64-msgHeight;
4239     auto oldColor = Video.color;
4240     Video.color = 0xff_ff_00;
4241     if (osdTimeLeft < 0.5) {
4242       int alpha = 255-clamp(int(510*osdTimeLeft), 0, 255);
4243       Video.color = Video.color|(alpha<<24);
4244     } else if (ct-osdTimeStart < 0.5) {
4245       osdTimeStart = ct-osdTimeStart;
4246       int alpha = 255-clamp(int(510*osdTimeStart), 0, 255);
4247       Video.color = Video.color|(alpha<<24);
4248     }
4249     sprStore.renderMultilineTextCentered(x, y, msg, msgScale, 0x00_ff_00, 0xff_ff_ff);
4250     Video.color = oldColor;
4251   }
4253   int hiColor1, hiColor2;
4254   msg = osdGetTalkMessage(out hiColor1, out hiColor2);
4255   if (msg) {
4256     int msgScale = 2;
4257     sprStore.loadFont('sFontSmall');
4258     auto msgWidth = sprStore.getMultilineTextWidth(msg, processHighlights1:true, processHighlights2:true);
4259     auto msgHeight = sprStore.getMultilineTextHeight(msg);
4260     auto msgWidthOrig = msgWidth*msgScale;
4261     auto msgHeightOrig = msgHeight*msgScale;
4262     if (msgWidth%16 != 0) msgWidth = (msgWidth|0x0f)+1;
4263     if (msgHeight%16 != 0) msgHeight = (msgHeight|0x0f)+1;
4264     msgWidth *= msgScale;
4265     msgHeight *= msgScale;
4266     int x = (viewWidth-msgWidth)/2;
4267     int y = 32*msgScale;
4268     auto oldColor = Video.color;
4269     // draw text frame and text background
4270     Video.color = 0;
4271     Video.fillRect(x, y, msgWidth, msgHeight);
4272     Video.color = 0xff_ff_ff;
4273     for (int fdx = 0; fdx < msgWidth; fdx += 16*msgScale) {
4274       auto spf = sprStore['sMenuTop'].frames[0];
4275       spf.blitAt(x+fdx, y-16*msgScale, msgScale);
4276       spf = sprStore['sMenuBottom'].frames[0];
4277       spf.blitAt(x+fdx, y+msgHeight, msgScale);
4278     }
4279     for (int fdy = 0; fdy < msgHeight; fdy += 16*msgScale) {
4280       auto spf = sprStore['sMenuLeft'].frames[0];
4281       spf.blitAt(x-16*msgScale, y+fdy, msgScale);
4282       spf = sprStore['sMenuRight'].frames[0];
4283       spf.blitAt(x+msgWidth, y+fdy, msgScale);
4284     }
4285     {
4286       auto spf = sprStore['sMenuUL'].frames[0];
4287       spf.blitAt(x-16*msgScale, y-16*msgScale, msgScale);
4288       spf = sprStore['sMenuUR'].frames[0];
4289       spf.blitAt(x+msgWidth, y-16*msgScale, msgScale);
4290       spf = sprStore['sMenuLL'].frames[0];
4291       spf.blitAt(x-16*msgScale, y+msgHeight, msgScale);
4292       spf = sprStore['sMenuLR'].frames[0];
4293       spf.blitAt(x+msgWidth, y+msgHeight, msgScale);
4294     }
4295     Video.color = 0xff_ff_00;
4296     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));
4297     Video.color = oldColor;
4298   }
4300   if (inWinCutscene) renderWinCutsceneOverlay();
4301   if (inIntroCutscene) renderTitleCutsceneOverlay();
4302   if (isTransitionRoom()) renderTransitionOverlay();
4304   /*
4305   if (doRestoreGL) {
4306     Video.setScissor(scsave);
4307     Video.glPopMatrix();
4308   }
4309   */
4311   Video.color = 0xff_ff_ff;
4315 // ////////////////////////////////////////////////////////////////////////// //
4316 final class!MapObject findGameObjectClassByName (name aname) {
4317   if (!aname) return none; // just in case
4318   auto co = FindClassByGameObjName(aname);
4319   if (!co) {
4320     writeln("***UNKNOWN GAME OBJECT: '", aname, "'");
4321     return none;
4322   }
4323   co = GetClassReplacement(co);
4324   if (!co) FatalError("findGameObjectClassByName: WTF?!");
4325   if (!class!MapObject(co)) FatalError("findGameTileClassByName: object '%n' is not a map object, it is a `%n`", aname, GetClassName(co));
4326   return class!MapObject(co);
4330 final class!MapTile findGameTileClassByName (name aname) {
4331   if (!aname) return none; // just in case
4332   auto co = FindClassByGameObjName(aname);
4333   if (!co) return MapTile; // unknown names will be routed directly to tile object
4334   co = GetClassReplacement(co);
4335   if (!co) FatalError("findGameTileClassByName: WTF?!");
4336   if (!class!MapTile(co)) FatalError("findGameTileClassByName: object '%n' is not a tile, it is a `%n`", aname, GetClassName(co));
4337   return class!MapTile(co);
4341 final MapObject findAnyObjectOfType (name aname) {
4342   if (!aname) return none;
4343   auto cls = FindClassByGameObjName(aname);
4344   if (!cls) return none;
4345   foreach (MapObject obj; objGrid.allObjects(MapObject)) {
4346     if (obj.spectral) continue;
4347     if (obj isa cls) return obj;
4348   }
4349   return none;
4353 // ////////////////////////////////////////////////////////////////////////// //
4354 final bool isRopePlacedAt (int x, int y) {
4355   int[8] covered;
4356   foreach (ref auto v; covered) v = false;
4357   foreach (MapTile t; objGrid.inRectPix(x, y-8, 1, 17, precise:false, castClass:MapTileRope)) {
4358     //if (!cbIsRopeTile(t)) continue;
4359     if (t.ix != x) continue;
4360     if (t.iy == y) return true;
4361     foreach (int ty; t.iy..t.iy+8) {
4362       int d = ty-y;
4363       if (d >= 0 && d < covered.length) covered[d] = true;
4364     }
4365   }
4366   // check if the whole rope height is completely covered with ropes
4367   foreach (auto v; covered) if (!v) return false;
4368   return true;
4372 final MapTile CreateMapTile (int xpos, int ypos, name aname) {
4373   if (!aname) FatalError("cannot create typeless tile");
4374   auto tclass = findGameTileClassByName(aname);
4375   if (!tclass) return none;
4376   MapTile tile = SpawnObject(tclass);
4377   tile.global = global;
4378   tile.level = self;
4379   tile.objName = aname;
4380   tile.objType = aname; // just in case
4381   tile.fltx = xpos;
4382   tile.flty = ypos;
4383   tile.objId = ++lastUsedObjectId;
4384   if (!tile.initialize()) FatalError("cannot create tile '%n'", aname);
4385   return tile;
4389 final bool PutSpawnedMapTile (int x, int y, MapTile tile) {
4390   if (!tile || !tile.isInstanceAlive) return false;
4392   //if (putToGrid) tile.active = true;
4393   bool putToGrid = (tile.moveable || tile.toSpecialGrid || tile.width != 16 || tile.height != 16 || x%16 != 0 || y%16 != 0);
4395   //writeln("putting tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (putToGrid ? "special " : ""), "grid, pos=(", x/16.0, ",", y/16.0, ")");
4397   if (!putToGrid) {
4398     int mapx = x/16, mapy = y/16;
4399     if (mapx < 0 || mapx >= tilesWidth || mapy < 0 || mapy >= tilesHeight) return false;
4400   }
4402   // if we already have rope tile there, there is no reason to add another one
4403   if (tile isa MapTileRope) {
4404     if (isRopePlacedAt(x, y)) return false;
4405   }
4407   // activate special or animated tile
4408   tile.active = tile.active || tile.moveable || tile.toSpecialGrid;
4409   // animated tiles must be active
4410   if (!tile.active) {
4411     auto spr = tile.getSprite();
4412     if (spr && spr.frames.length > 1) {
4413       writeln("activated animated tile '", tile.objName, "'");
4414       tile.active = true;
4415     }
4416   }
4418   tile.fltx = x;
4419   tile.flty = y;
4420   if (putToGrid) {
4421     //if (tile isa TitleTileCopy) writeln("*** PUTTING COPYRIGHT TILE");
4422     //tile.toSpecialGrid = true;
4423     if (!tile.dontReplaceOthers && x&16 == 0 && y%16 == 0) {
4424       auto t = getTileAtGridAny(x/16, y/16);
4425       if (t && !t.immuneToReplacement) {
4426         writeln("REPLACING tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
4427         writeln("      NEW tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (tile.toSpecialGrid ? "special " : ""), "grid, pos=(", tile.fltx/16.0, ",", tile.flty/16.0, ")");
4428         t.instanceRemove();
4429       }
4430     }
4431     insertObject(tile);
4432   } else {
4433     //writeln("SIZE: ", tilesWidth, "x", tilesHeight);
4434     setTileAtGrid(x/16, y/16, tile);
4435     /*
4436     auto t = getTileAtGridAny(x/16, y/16);
4437     if (t != tile) {
4438       writeln("putting tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (putToGrid ? "special " : ""), "grid, pos=(", x/16.0, ",", y/16.0, ")");
4439       checkTilesInRect(x/16, y/16, 16, 16, delegate bool (MapTile tile) {
4440         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, ")");
4441         return false;
4442       });
4443       FatalError("FUUUUUU");
4444     }
4445     */
4446   }
4448   if (tile.enter) registerEnter(tile);
4449   if (tile.exit) registerExit(tile);
4451   // make tile under exit invulnerable
4452   if (checkTilesInRect(tile.ix, tile.iy-16, 16, 16, delegate bool (MapTile t) { return t.exit; })) {
4453     tile.invincible = true;
4454   }
4456   return true;
4460 // won't call `onDestroy()`
4461 final void RemoveMapTileFromGrid (int tileX, int tileY, optional string reason) {
4462   if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
4463     auto t = getTileAtGridAny(tileX, tileY);
4464     if (t) {
4465       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, ")");
4466       t.instanceRemove();
4467       checkWater = true;
4468     }
4469   }
4473 final MapTile MakeMapTile (int mapx, int mapy, name aname) {
4474   //writeln("tile at (", mapx, ",", mapy, "): ", aname);
4475   //if (aname == 'oLush') { MapObject fail; fail.initialize(); }
4476   //if (mapy >= tilesHeight) { writeln("OOM: ", aname, "; ty=", mapy, "; hgt=", tilesHeight); }
4477   if (mapx < 0 || mapx >= tilesWidth || mapy < 0 || mapy >= tilesHeight) return none;
4479   // if we already have rope tile there, there is no reason to add another one
4480   if (aname == 'oRope') {
4481     if (isRopePlacedAt(mapx*16, mapy*16)) return none;
4482   }
4484   auto tile = CreateMapTile(mapx*16, mapy*16, aname);
4485   if (!tile) return none;
4486   if (!PutSpawnedMapTile(mapx*16, mapy*16, tile)) {
4487     delete tile;
4488     tile = none;
4489   }
4491   return tile;
4495 final MapTile MakeMapTileAtPix (int xpix, int ypix, name aname) {
4496   // if we already have rope tile there, there is no reason to add another one
4497   if (aname == 'oRope') {
4498     if (isRopePlacedAt(xpix, ypix)) return none;
4499   }
4501   auto tile = CreateMapTile(xpix, ypix, aname);
4502   if (!tile) return none;
4503   if (!PutSpawnedMapTile(xpix, ypix, tile)) {
4504     delete tile;
4505     tile = none;
4506   }
4508   return tile;
4512 final MapTile MakeMapRopeTileAt (int x0, int y0) {
4513   // if we already have rope tile there, there is no reason to add another one
4514   if (isRopePlacedAt(x0, y0)) return none;
4516   auto tile = CreateMapTile(x0, y0, 'oRope');
4517   if (!PutSpawnedMapTile(x0, y0, tile)) {
4518     delete tile;
4519     tile = none;
4520   }
4522   return tile;
4526 // ////////////////////////////////////////////////////////////////////////// //
4527 final MapBackTile CreateBgTile (name sprName, optional int atx0, optional int aty0, optional int aw, optional int ah) {
4528   BackTileImage img = bgtileStore[sprName];
4529   auto res = SpawnObject(MapBackTile);
4530   res.global = global;
4531   res.level = self;
4532   res.bgt = img;
4533   res.bgtName = sprName;
4534   if (specified_atx0) res.tx0 = atx0;
4535   if (specified_aty0) res.ty0 = aty0;
4536   if (specified_aw) res.w = aw;
4537   if (specified_ah) res.h = ah;
4538   if (!res.initialize()) FatalError("cannot create background tile with image '%n'", sprName);
4539   return res;
4543 // ////////////////////////////////////////////////////////////////////////// //
4545 background The background asset from which the new tile will be extracted.
4546 left The x coordinate of the left of the new tile, relative to the background asset's top left corner.
4547 top The y coordinate of the top of the new tile, relative to the background assets top left corner.
4548 width The width of the tile.
4549 height The height of the tile.
4550 x The x position in the room to place the tile.
4551 y The y position in the room to place the tile.
4552 depth The depth at which to place the tile.
4554 final void MakeMapBackTile (name bgname, int left, int top, int width, int height, int x, int y, int depth) {
4555   if (width < 1 || height < 1 || !bgname) return;
4556   auto bgt = bgtileStore[bgname];
4557   if (!bgt) FatalError("cannot load background '%n'", bgname);
4558   MapBackTile bt = SpawnObject(MapBackTile);
4559   bt.global = global;
4560   bt.level = self;
4561   bt.objName = bgname;
4562   bt.bgt = bgt;
4563   bt.bgtName = bgname;
4564   bt.fltx = x;
4565   bt.flty = y;
4566   bt.tx0 = left;
4567   bt.ty0 = top;
4568   bt.w = width;
4569   bt.h = height;
4570   bt.depth = depth;
4571   // find a place for it
4572   if (!backtiles) {
4573     backtiles = bt;
4574     return;
4575   }
4576   // back tiles with the highest depth should come first
4577   MapBackTile ct = backtiles, cprev = none;
4578   while (ct && depth <= ct.depth) { cprev = ct; ct = ct.next; }
4579   // insert before ct
4580   if (cprev) {
4581     bt.next = cprev.next;
4582     cprev.next = bt;
4583   } else {
4584     bt.next = backtiles;
4585     backtiles = bt;
4586   }
4590 // ////////////////////////////////////////////////////////////////////////// //
4591 final MapObject SpawnMapObjectWithClass (class!MapObject oclass) {
4592   if (!oclass) return none;
4594   MapObject obj = SpawnObject(oclass);
4595   if (!obj) FatalError("cannot create map object '%n'", GetClassName(oclass));
4597   //if (aname == 'oShells' || aname == 'oShellSingle' || aname == 'oRopePile') writeln("*** CREATED '", aname, "'");
4599   obj.global = global;
4600   obj.level = self;
4601   obj.objId = ++lastUsedObjectId;
4603   return obj;
4607 final MapObject SpawnMapObject (name aname) {
4608   if (!aname) return none;
4609   auto res = SpawnMapObjectWithClass(findGameObjectClassByName(aname));
4610   if (res && !res.objType) res.objType = aname; // just in case
4611   return res;
4615 final MapObject PutSpawnedMapObject (int x, int y, MapObject obj) {
4616   if (!obj /*|| obj.global || obj.level*/) return none; // oops
4618   obj.fltx = x;
4619   obj.flty = y;
4620   if (!obj.initialize()) { delete obj; return none; } // not fatal
4622   insertObject(obj);
4623   if (obj.walkableSolid) hasSolidObjects = true;
4625   return obj;
4629 final MapObject MakeMapObject (int x, int y, name aname) {
4630   MapObject obj = SpawnMapObject(aname);
4631   obj = PutSpawnedMapObject(x, y, obj);
4632   return obj;
4636 // ////////////////////////////////////////////////////////////////////////// //
4637 void setMenuTilesVisible (bool vis) {
4638   if (vis) {
4639     forEachTile(delegate bool (MapTile t) {
4640       if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4641         t.invisible = false;
4642       }
4643       return false;
4644     });
4645   } else {
4646     forEachTile(delegate bool (MapTile t) {
4647       if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4648         t.invisible = true;
4649       }
4650       return false;
4651     });
4652   }
4656 void setMenuTilesOnTop () {
4657   forEachTile(delegate bool (MapTile t) {
4658     if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4659       t.depth = 1;
4660     }
4661     return false;
4662   });
4666 // ////////////////////////////////////////////////////////////////////////// //
4667 #include "roomTitle.vc"
4668 #include "roomTrans1.vc"
4669 #include "roomTrans2.vc"
4670 #include "roomTrans3.vc"
4671 #include "roomTrans4.vc"
4672 #include "roomOlmec.vc"
4673 #include "roomEnd.vc"
4674 #include "roomIntro.vc"
4675 #include "roomTutorial.vc"
4676 #include "roomScores.vc"
4677 #include "roomStars.vc"
4678 #include "roomSun.vc"
4679 #include "roomMoon.vc"
4682 // ////////////////////////////////////////////////////////////////////////// //
4683 #include "packages/Generator/loadRoomGens.vc"
4684 #include "packages/Generator/loadEntityGens.vc"