vines and ladders are invincible in the original
[k8vacspelynky.git] / GameLevel.vc
blob814a820792ccd7491a084ca0e21e232e52c755bb
1 /**********************************************************************************
2  * Copyright (c) 2008, 2009 Derek Yu and Mossmouth, LLC
3  * Copyright (c) 2010, Moloch
4  * Copyright (c) 2018, Ketmar Dark
5  *
6  * This file is part of Spelunky.
7  *
8  * You can redistribute and/or modify Spelunky, including its source code, under
9  * the terms of the Spelunky User License.
10  *
11  * Spelunky is distributed in the hope that it will be entertaining and useful,
12  * but WITHOUT WARRANTY.  Please see the Spelunky User License for more details.
13  *
14  * The Spelunky User License should be available in "Game .Information", which
15  * can be found in the Resource Explorer, or as an external file called COPYING.
16  * If not, please obtain a new copy of Spelunky from <http://spelunkyworld.com/>
17  *
18  **********************************************************************************/
19 // this is the level we're playing in, with all objects and tiles
20 class GameLevel : Object;
22 //#define EXPERIMENTAL_RENDER_CACHE
24 const float FrameTime = 1.0f/30.0f;
26 const int dumpGridStats = true;
28 struct IVec2D {
29   int x, y;
32 // in tiles
33 //enum NormalTilesWidth = LevelGen::LevelWidth*RoomGen::Width+2;
34 //enum NormalTilesHeight = LevelGen::LevelHeight*RoomGen::Height+2;
36 enum MaxTilesWidth = 64;
37 enum MaxTilesHeight = 64;
39 GameGlobal global;
40 transient GameStats stats;
41 transient SpriteStore sprStore;
42 transient BackTileStore bgtileStore;
43 transient BackTileImage levBGImg;
44 name levBGImgName;
45 LevelGen lg;
46 transient name lastMusicName;
47 //RoomGen[LevelGen::LevelWidth, LevelGen::LevelHeight] rooms; // moved to levelgen
49 transient float accumTime;
50 transient bool gamePaused = false;
51 transient bool gameShowHelp = false;
52 transient int gameHelpScreen = 0;
53 const int MaxGameHelpScreen = 2;
54 transient bool checkWater;
55 transient int liquidTileCount; // cached
56 /*transient*/ int damselSaved;
58 // hud efffects
59 transient int xmoney;
60 transient int collectCounter;
61 /*transient*/ int levelMoneyStart;
63 // all movable (thinkable) map objects
64 EntityGrid objGrid; // monsters, items and tiles
66 MapBackTile backtiles;
67 bool blockWaterChecking;
69 int inWinCutscene;
70 int inIntroCutscene;
71 bool cameFromIntroRoom; // for title screen
72 bool allowFinalCutsceneSkip;
74 LevelGen::RType[MaxTilesWidth, MaxTilesHeight] roomType;
76 enum LevelKind {
77   Normal,
78   Transition,
79   Title,
80   Intro,
81   Tutorial,
82   Scores,
83   Stars,
84   Sun,
85   Moon,
86   //Final,
88 LevelKind levelKind = LevelKind.Normal;
90 array!MapTile allEnters;
91 array!MapTile allExits;
94 int startRoomX, startRoomY;
95 int endRoomX, endRoomY;
97 PlayerPawn player;
98 bool playerExited;
99 MapEntity playerExitDoor;
100 transient bool disablePlayerThink = false;
101 int maxPlayingTime; // in seconds
102 int levelStartTime;
103 int levelEndTime;
105 int ghostTimeLeft;
106 int musicFadeTimer;
107 bool ghostSpawned; // to speed up some checks
108 bool resetBMCOG = false;
109 int udjatAlarm;
112 // FPS, i.e. incremented by 30 in one second
113 int time; // in frames
114 int lastUsedObjectId;
115 transient int lastRenderTime = -1;
116 transient int pausedTime;
118 MapEntity deadItemsHead;
119 transient /*bool*/int hasSolidObjects = true;
121 // screen shake variables
122 int shakeLeft;
123 IVec2D shakeOfs;
124 IVec2D shakeDir;
126 // set this before calling `fixCamera()`
127 // dimensions should be real, not scaled up/down
128 transient int viewWidth, viewHeight;
129 //transient int viewOffsetX, viewOffsetY;
131 // room bounds, not scaled
132 IVec2D viewMin, viewMax;
134 // for Olmec level cinematics
135 IVec2D cameraSlideToDest;
136 IVec2D cameraSlideToCurr;
137 IVec2D cameraSlideToSpeed; // !0: slide
138 int cameraSlideToPlayer;
139 // `fixCamera()` will set the following
140 // coordinates will be real too (with scale applied)
141 // shake is not applied
142 transient IVec2D viewStart; // with `player.viewOffset`
143 private transient IVec2D realViewStart; // without `player.viewOffset`
145 transient int framesProcessedFromLastClear;
147 transient int BuildYear;
148 transient int BuildMonth;
149 transient int BuildDay;
150 transient int BuildHour;
151 transient int BuildMin;
152 transient string BuildDateString;
155 final string getBuildDateString () {
156   if (!BuildYear) return BuildDateString;
157   if (BuildDateString) return BuildDateString;
158   BuildDateString = va("%d-%02d-%02d %02d:%02d", BuildYear, BuildMonth, BuildDay, BuildHour, BuildMin);
159   return BuildDateString;
163 final void cameraSlideToPoint (int dx, int dy, int speedx, int speedy) {
164   cameraSlideToPlayer = 0;
165   cameraSlideToDest.x = dx;
166   cameraSlideToDest.y = dy;
167   cameraSlideToSpeed.x = abs(speedx);
168   cameraSlideToSpeed.y = abs(speedy);
169   cameraSlideToCurr.x = cameraCurrX;
170   cameraSlideToCurr.y = cameraCurrY;
174 final void cameraReturnToPlayer () {
175   if (!cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y)) {
176     cameraSlideToCurr.x = cameraCurrX;
177     cameraSlideToCurr.y = cameraCurrY;
178     if (cameraSlideToSpeed.x && abs(cameraSlideToSpeed.x) < 8) cameraSlideToSpeed.x = 8;
179     if (cameraSlideToSpeed.y && abs(cameraSlideToSpeed.y) < 8) cameraSlideToSpeed.y = 8;
180     cameraSlideToPlayer = 1;
181   }
185 // if `frameSkip` is `true`, there are more frames waiting
186 // (i.e. you may skip rendering and such)
187 transient void delegate (bool frameSkip) onBeforeFrame;
188 transient void delegate (bool frameSkip) onAfterFrame;
190 transient void delegate () onCameraTeleported;
192 transient void delegate () onLevelExitedCB;
194 // this will be called in-between frames, and
195 // `frameTime` is [0..1)
196 transient void delegate (float frameTime) onInterFrame;
198 final int bizRoomStyle { get { return (lg ? lg.bizRoomStyle : 0); } }
201 final bool isNormalLevel () { return (levelKind == LevelKind.Normal); }
202 final bool isTitleRoom () { return (levelKind == LevelKind.Title); }
203 final bool isTutorialRoom () { return (levelKind == LevelKind.Tutorial); }
204 final bool isTransitionRoom () { return (levelKind == LevelKind.Transition); }
205 final bool isIntroRoom () { return (levelKind == LevelKind.Transition); }
208 bool isHUDEnabled () {
209   if (inWinCutscene) return false;
210   if (inIntroCutscene) return false;
211   if (lg.finalBossLevel) return true;
212   if (isNormalLevel()) return true;
213   return false;
217 // ////////////////////////////////////////////////////////////////////////// //
218 // stats
219 void addDeath (name aname) { if (isNormalLevel()) stats.addDeath(aname); }
221 int starsKills;
222 int sunScore;
223 int moonScore;
224 int moonTimer;
226 void addKill (name aname, optional bool telefrag) {
227        if (isNormalLevel()) stats.addKill(aname, telefrag!optional);
228   else if (aname == 'Shopkeeper' && levelKind == LevelKind.Stars) { ++stats.starsKills; ++starsKills; }
231 void addCollect (name aname, optional int amount) { if (isNormalLevel()) stats.addCollect(aname, amount!optional); }
233 void addDamselSaved () { if (isNormalLevel()) stats.addDamselSaved(); }
234 void addIdolStolen () { if (isNormalLevel()) stats.addIdolStolen(); }
235 void addIdolConverted () { if (isNormalLevel()) stats.addIdolConverted(); }
236 void addCrystalIdolStolen () { if (isNormalLevel()) stats.addCrystalIdolStolen(); }
237 void addCrystalIdolConverted () { if (isNormalLevel()) stats.addCrystalIdolConverted(); }
238 void addGhostSummoned () { if (isNormalLevel()) stats.addGhostSummoned(); }
241 // ////////////////////////////////////////////////////////////////////////// //
242 static final string time2str (int time) {
243   int secs = time%60; time /= 60;
244   int mins = time%60; time /= 60;
245   int hours = time%24; time /= 24;
246   int days = time;
247   if (days) return va("%d DAYS, %d:%02d:%02d", days, hours, mins, secs);
248   if (hours) return va("%d:%02d:%02d", hours, mins, secs);
249   return va("%02d:%02d", mins, secs);
253 // ////////////////////////////////////////////////////////////////////////// //
254 final int tilesWidth () { return lg.levelRoomWidth*RoomGen::Width+2; }
255 final int tilesHeight () { return (lg.finalBossLevel ? 55 : lg.levelRoomHeight*RoomGen::Height+2); }
258 // ////////////////////////////////////////////////////////////////////////// //
259 protected void resetGameInternal () {
260   if (player) player.removeBallAndChain();
261   resetBMCOG = false;
262   inWinCutscene = 0;
263   allowFinalCutsceneSkip = true;
264   //inIntroCutscene = 0;
265   shakeLeft = 0;
266   udjatAlarm = 0;
267   starsKills = 0;
268   sunScore = 0;
269   moonScore = 0;
270   moonTimer = 0;
271   damselSaved = 0;
272   xmoney = 0;
273   collectCounter = 0;
274   levelMoneyStart = 0;
275   if (player) {
276     player.removeBallAndChain();
277     auto hi = player.holdItem;
278     player.holdItem = none;
279     if (hi) hi.instanceRemove();
280     hi = player.pickedItem;
281     player.pickedItem = none;
282     if (hi) hi.instanceRemove();
283   }
284   time = 0;
285   lastRenderTime = -1;
286   levelStartTime = 0;
287   levelEndTime = 0;
288   global.resetGame();
289   stats.clearGameTotals();
293 // this won't generate a level yet
294 void restartGame () {
295   resetGameInternal();
296   if (global.startMoney > 0) stats.setMoneyCheat();
297   stats.setMoney(global.startMoney);
298   levelKind = LevelKind.Normal;
302 // complement function to `restart game`
303 void generateNormalLevel () {
304   generateLevel();
305   centerViewAtPlayer();
309 void restartTitle () {
310   resetGameInternal();
311   stats.setMoney(0);
312   createSpecialLevel(LevelKind.Title, &createTitleRoom, 'musTitle');
313   global.plife = 9999;
314   global.bombs = 0;
315   global.rope = 0;
316   global.arrows = 0;
317   global.sgammo = 0;
321 void restartIntro () {
322   resetGameInternal();
323   stats.setMoney(0);
324   createSpecialLevel(LevelKind.Intro, &createIntroRoom, '');
325   global.plife = 9999;
326   global.bombs = 0;
327   global.rope = 1;
328   global.arrows = 0;
329   global.sgammo = 0;
333 void restartTutorial () {
334   resetGameInternal();
335   stats.setMoney(0);
336   createSpecialLevel(LevelKind.Tutorial, &createTutorialRoom, 'musCave');
337   global.plife = 4;
338   global.bombs = 0;
339   global.rope = 4;
340   global.arrows = 0;
341   global.sgammo = 0;
345 void restartScores () {
346   resetGameInternal();
347   stats.setMoney(0);
348   createSpecialLevel(LevelKind.Scores, &createScoresRoom, 'musTitle');
349   global.plife = 4;
350   global.bombs = 0;
351   global.rope = 0;
352   global.arrows = 0;
353   global.sgammo = 0;
357 void restartStarsRoom () {
358   resetGameInternal();
359   stats.setMoney(0);
360   createSpecialLevel(LevelKind.Stars, &createStarsRoom, '');
361   global.plife = 8;
362   global.bombs = 0;
363   global.rope = 0;
364   global.arrows = 0;
365   global.sgammo = 0;
369 void restartSunRoom () {
370   resetGameInternal();
371   stats.setMoney(0);
372   createSpecialLevel(LevelKind.Sun, &createSunRoom, '');
373   global.plife = 8;
374   global.bombs = 0;
375   global.rope = 0;
376   global.arrows = 0;
377   global.sgammo = 0;
381 void restartMoonRoom () {
382   resetGameInternal();
383   stats.setMoney(0);
384   createSpecialLevel(LevelKind.Moon, &createMoonRoom, '');
385   global.plife = 8;
386   global.bombs = 0;
387   global.rope = 0;
388   global.arrows = 100;
389   global.sgammo = 0;
393 // ////////////////////////////////////////////////////////////////////////// //
394 // generate angry shopkeeper at exit if murderer or thief
395 void generateAngryShopkeepers () {
396   if (global.murderer || global.thiefLevel > 0) {
397     foreach (MapTile e; allExits) {
398       auto obj = MonsterShopkeeper(MakeMapObject(e.ix, e.iy, 'oShopkeeper'));
399       if (obj) {
400         obj.style = 'Bounty Hunter';
401         obj.status = MapObject::PATROL;
402       }
403     }
404   }
408 // ////////////////////////////////////////////////////////////////////////// //
409 final void resetRoomBounds () {
410   viewMin.x = 0;
411   viewMin.y = 0;
412   viewMax.x = tilesWidth*16;
413   viewMax.y = tilesHeight*16;
414   // Great Lake is bottomless (nope)
415   //if (global.lake == 1) viewMax.y -= 16;
416   //writeln("ROOM BOUNDS: (", viewMax.x, "x", viewMax.y, ")");
420 final void setRoomBounds (int x0, int y0, int x1, int y1) {
421   viewMin.x = x0;
422   viewMin.y = y0;
423   viewMax.x = x1+16;
424   viewMax.y = y1+16;
428 // ////////////////////////////////////////////////////////////////////////// //
429 struct OSDMessage {
430   string msg;
431   float timeout; // seconds
432   float starttime; // for active
433   bool active; // true: timeout is `GetTickCount()` dismissing time
436 array!OSDMessage msglist; // [0]: current one
438 struct OSDMessageTalk {
439   string msg;
440   float timeout; // seconds;
441   float starttime; // for active
442   bool active; // true: timeout is `GetTickCount()` dismissing time
443   bool shopOnly; // true: timeout when player exited the shop
444   int hiColor1; // -1: default
445   int hiColor2; // -1: default
448 array!OSDMessageTalk msgtalklist; // [0]: current one
451 private final void osdCheckTimeouts () {
452   auto stt = GetTickCount();
453   while (msglist.length) {
454     if (!msglist[0].msg) { msglist.remove(0); continue; }
455     if (!msglist[0].active) {
456       msglist[0].active = true;
457       msglist[0].starttime = stt;
458     }
459     if (msglist[0].starttime+msglist[0].timeout >= stt) break;
460     msglist.remove(0);
461   }
462   if (msgtalklist.length) {
463     bool inshop = isInShop(player.ix/16, player.iy/16);
464     while (msgtalklist.length) {
465       if (!msgtalklist[0].msg) { msgtalklist.remove(0); continue; }
466       if (msgtalklist[0].shopOnly) {
467         if (inshop == msgtalklist[0].active) {
468           msgtalklist[0].active = !inshop;
469           if (!inshop) msgtalklist[0].starttime = stt;
470         }
471       } else {
472         if (!msgtalklist[0].active) {
473           msgtalklist[0].active = true;
474           msgtalklist[0].starttime = stt;
475         }
476       }
477       if (!msgtalklist[0].active) break;
478       //writeln("timedelta: ", msgtalklist[0].starttime+msgtalklist[0].timeout-stt);
479       if (msgtalklist[0].starttime+msgtalklist[0].timeout >= stt) break;
480       msgtalklist.remove(0);
481     }
482   }
486 final bool osdHasMessage () {
487   osdCheckTimeouts();
488   return (msglist.length > 0);
492 final string osdGetMessage (out float timeLeft, out float timeStart) {
493   osdCheckTimeouts();
494   if (msglist.length == 0) { timeLeft = 0; return ""; }
495   auto stt = GetTickCount();
496   timeStart = msglist[0].starttime;
497   timeLeft = fmax(0, msglist[0].starttime+msglist[0].timeout-stt);
498   return msglist[0].msg;
502 final string osdGetTalkMessage (optional out int hiColor1, optional out int hiColor2) {
503   osdCheckTimeouts();
504   if (msgtalklist.length == 0) return "";
505   hiColor1 = msgtalklist[0].hiColor1;
506   hiColor2 = msgtalklist[0].hiColor2;
507   return msgtalklist[0].msg;
511 final void osdClear (optional bool clearTalk) {
512   msglist.clear();
513   if (clearTalk) msgtalklist.clear();
517 final void osdMessage (string msg, optional float timeout, optional bool urgent) {
518   if (!msg) return;
519   msg = global.expandString(msg);
520   if (!specified_timeout) timeout = 3.33;
521   // special message for shops
522   if (timeout == -666) {
523     if (!msg) return;
524     if (msglist.length && msglist[0].msg == msg) return;
525     if (msglist.length == 0 || msglist[0].msg != msg) {
526       osdClear(clearTalk:false);
527       msglist.length += 1;
528       msglist[0].msg = msg;
529     }
530     msglist[0].active = false;
531     msglist[0].timeout = 3.33;
532     osdCheckTimeouts();
533     return;
534   }
535   if (timeout < 0.1) return;
536   timeout = fmax(1.0, timeout);
537   //writeln("OSD: ", msg);
538   // find existing one, and bring it to the top
539   int oldidx = 0;
540   for (; oldidx < msglist.length; ++oldidx) {
541     if (msglist[oldidx].msg == msg) break; // i found her!
542   }
543   // duplicate?
544   if (oldidx < msglist.length) {
545     // yeah, move duplicate to the top
546     msglist[oldidx].timeout = fmax(timeout, msglist[oldidx].timeout);
547     msglist[oldidx].active = false;
548     if (urgent && oldidx != 0) {
549       timeout = msglist[oldidx].timeout;
550       msglist.remove(oldidx);
551       msglist.insert(0);
552       msglist[0].msg = msg;
553       msglist[0].timeout = timeout;
554       msglist[0].active = false;
555     }
556   } else if (urgent) {
557     msglist.insert(0);
558     msglist[0].msg = msg;
559     msglist[0].timeout = timeout;
560     msglist[0].active = false;
561   } else {
562     // new one
563     msglist.length += 1;
564     msglist[$-1].msg = msg;
565     msglist[$-1].timeout = timeout;
566     msglist[$-1].active = false;
567   }
568   osdCheckTimeouts();
572 void osdMessageTalk (string msg, optional bool replace, optional float timeout, optional bool inShopOnly,
573                      optional int hiColor1, optional int hiColor2)
575   //if (!msg) return;
576   //writeln("talk msg: replace=", replace, "; timeout=", timeout, "; inshop=", inShopOnly, "; msg=", msg);
577   if (!specified_timeout) timeout = 3.33;
578   if (!specified_inShopOnly) inShopOnly = true;
579   if (!specified_hiColor1) hiColor1 = -1;
580   if (!specified_hiColor2) hiColor2 = -1;
581   msg = global.expandString(msg);
582   if (replace) {
583     if (!msg) { msgtalklist.clear(); return; }
584     if (msgtalklist.length && msgtalklist[0].msg == msg) {
585       while (msgtalklist.length > 1) msgtalklist.remove(1);
586       msgtalklist[$-1].timeout = timeout;
587       msgtalklist[$-1].shopOnly = inShopOnly;
588     } else {
589       if (msgtalklist.length) msgtalklist.clear();
590       msgtalklist.length += 1;
591       msgtalklist[$-1].msg = msg;
592       msgtalklist[$-1].timeout = timeout;
593       msgtalklist[$-1].active = false;
594       msgtalklist[$-1].shopOnly = inShopOnly;
595       msgtalklist[$-1].hiColor1 = hiColor1;
596       msgtalklist[$-1].hiColor2 = hiColor2;
597     }
598   } else {
599     if (!msg) return;
600     bool found = false;
601     foreach (auto midx, ref auto mnfo; msgtalklist) {
602       if (mnfo.msg == msg) {
603         mnfo.timeout = timeout;
604         mnfo.shopOnly = inShopOnly;
605         found = true;
606       }
607     }
608     if (!found) {
609       msgtalklist.length += 1;
610       msgtalklist[$-1].msg = msg;
611       msgtalklist[$-1].timeout = timeout;
612       msgtalklist[$-1].active = false;
613       msgtalklist[$-1].shopOnly = inShopOnly;
614       msgtalklist[$-1].hiColor1 = hiColor1;
615       msgtalklist[$-1].hiColor2 = hiColor2;
616     }
617   }
618   osdCheckTimeouts();
622 // ////////////////////////////////////////////////////////////////////////// //
623 void setup (GameGlobal aGlobal, SpriteStore aSprStore, BackTileStore aBGTileStore) {
624   global = aGlobal;
625   sprStore = aSprStore;
626   bgtileStore = aBGTileStore;
628   lg = SpawnObject(LevelGen);
629   lg.global = global;
630   lg.level = self;
632   objGrid = SpawnObject(EntityGrid);
633   objGrid.setup(0, 0, MaxTilesWidth*16, MaxTilesHeight*16);
637 // stores should be set
638 void onLoaded () {
639   checkWater = true;
640   liquidTileCount = 0;
641   levBGImg = bgtileStore[levBGImgName];
642   foreach (MapEntity o; objGrid.allObjects()) {
643     o.onLoaded();
644     auto t = MapTile(o);
645     if (t && (t.lava || t.water)) ++liquidTileCount;
646   }
647   for (MapBackTile bt = backtiles; bt; bt = bt.next) bt.onLoaded();
648   if (player) player.onLoaded();
649   //FIXME
650   if (msglist.length) {
651     msglist[0].active = false;
652     msglist[0].timeout = 0.200;
653     osdCheckTimeouts();
654   }
655   lastMusicName = (lg ? lg.musicName : '');
656   global.setMusicPitch(1.0);
657   if (lg && lg.musicName) global.playMusic(lg.musicName); else global.stopMusic();
661 // ////////////////////////////////////////////////////////////////////////// //
662 void pickedSpectacles () {
663   foreach (MapTile t; objGrid.allObjectsSafe(MapTile)) t.onGotSpectacles();
667 // ////////////////////////////////////////////////////////////////////////// //
668 #include "rgentile.vc"
669 #include "rgenobj.vc"
672 void onLevelExited () {
673   if (playerExitDoor isa TitleTileXTitle) {
674     playerExitDoor = none;
675     restartTitle();
676     return;
677   }
678   // title
679   if (isTitleRoom() || levelKind == LevelKind.Scores) {
680     if (playerExitDoor) processTitleExit(playerExitDoor);
681     playerExitDoor = none;
682     return;
683   }
684   if (isTutorialRoom()) {
685     playerExitDoor = none;
686     restartGame();
687     //global.currLevel = 1;
688     //generateNormalLevel();
689     global.currLevel = 0;
690     generateTransitionLevel();
691     return;
692   }
693   // challenges
694   if (levelKind == LevelKind.Stars || levelKind == LevelKind.Sun || levelKind == LevelKind.Moon) {
695     playerExitDoor = none;
696     levelEndTime = time;
697     if (onLevelExitedCB) onLevelExitedCB();
698     restartTitle();
699     return;
700   }
701   // normal level
702   if (isNormalLevel()) {
703     stats.maxLevelComplete = max(stats.maxLevelComplete, global.currLevel);
704     levelEndTime = time;
705     if (playerExitDoor) {
706       if (playerExitDoor.objType == 'oXGold') {
707         writeln("exiting to City Of Gold");
708         global.cityOfGold = -1;
709         //!global.currLevel += 1;
710       } else if (playerExitDoor.objType == 'oXMarket') {
711         writeln("exiting to Black Market");
712         global.genBlackMarket = true;
713         //!global.currLevel += 1;
714       } else {
715         writeln("exit door(", GetClassName(playerExitDoor.Class), "): '", playerExitDoor.objType, "'");
716       }
717     } else {
718       writeln("WTF?! NO EXIT DOOR!");
719     }
720   }
721   if (onLevelExitedCB) onLevelExitedCB();
722   //
723   playerExitDoor = none;
724   if (levelKind == LevelKind.Transition) {
725     if (global.thiefLevel > 0) global.thiefLevel -= 1;
726     if (global.alienCraft) ++global.alienCraft;
727     if (global.yetiLair) ++global.yetiLair;
728     if (global.lake) ++global.lake;
729     if (global.cityOfGold) { if (++global.cityOfGold == 0) global.cityOfGold = 1; }
730     //orig dbg:if (global.currLevel == 1) global.currLevel += firstLevelSkip; else global.currLevel += levelSkip;
731     /+
732     if (!global.blackMarket && !global.cityOfGold /*&& !global.yetiLair*/) {
733       global.currLevel += 1;
734     }
735     +/
736     ++global.currLevel;
737     generateLevel();
738   } else {
739     // < 20 seconds per level: looks like a speedrun
740     global.noDarkLevel = (levelEndTime > levelStartTime && levelEndTime-levelStartTime < 20*30);
741     if (lg.finalBossLevel) {
742       winTime = time;
743       allowFinalCutsceneSkip = (stats.gamesWon != 0);
744       ++stats.gamesWon;
745       // add money for big idol
746       player.addScore(50000);
747       stats.gameOver();
748       startWinCutscene();
749     } else {
750       generateTransitionLevel();
751     }
752   }
753   //centerViewAtPlayer();
757 void onOlmecDead (MapObject o) {
758   writeln("*** OLMEC IS DEAD!");
759   foreach (MapTile t; allExits) {
760     if (t.exit) {
761       t.openExit();
762       auto st = checkTileAtPoint(t.ix+8, t.iy+16);
763       if (!st) {
764         st = MakeMapTile(t.ix/16, t.iy/16+1, 'oTemple');
765         st.ore = 0;
766       }
767       st.invincible = true;
768     }
769   }
773 void generateLevelMessages () {
774   writeln("LEVEL NUMBER: ", global.currLevel);
775   if (global.darkLevel) {
776     if (global.hasCrown) {
777        osdMessage("THE HEDJET SHINES BRIGHTLY.");
778        global.darkLevel = false;
779     } else if (global.config.scumDarkness < 2) {
780       osdMessage("I CAN'T SEE A THING!\nI'D BETTER USE THESE FLARES!");
781     }
782   }
784   if (global.blackMarket) osdMessage("WELCOME TO THE BLACK MARKET!");
786   if (global.cemetary) osdMessage("THE DEAD ARE RESTLESS!");
787   if (global.lake == 1) osdMessage("I CAN HEAR RUSHING WATER...");
789   if (global.snakePit) osdMessage("I HEAR SNAKES... I HATE SNAKES!");
790   if (global.yetiLair == 1) osdMessage("IT SMELLS LIKE WET FUR IN HERE!");
791   if (global.alienCraft == 1) osdMessage("THERE'S A PSYCHIC PRESENCE HERE!");
792   if (global.cityOfGold == 1) {
793     if (true /*hasOlmec()*/) osdMessage("IT'S THE LEGENDARY CITY OF GOLD!");
794   }
796   if (global.sacrificePit) osdMessage("I CAN HEAR PRAYERS TO KALI!");
800 final MapObject debugSpawnObjectWithClass (class!MapObject oclass, optional bool playerDir) {
801   if (!oclass) return none;
802   int dx = 0, dy = 0;
803   bool canLeft = !isSolidAtPoint(player.ix-8, player.iy);
804   bool canRight = !isSolidAtPoint(player.ix+16, player.iy);
805   if (!canLeft && !canRight) return none;
806   if (canLeft && canRight) {
807     if (playerDir) {
808       dx = (player.dir == MapObject::Dir.Left ? -16 : 16);
809     } else {
810       dx = 16;
811     }
812   } else {
813     dx = (canLeft ? -16 : 16);
814   }
815   auto obj = SpawnMapObjectWithClass(oclass);
816   if (obj isa MapEnemy) {
817     dx -= 8;
818     dy -= (obj isa MonsterDamsel ? 2 : 8);
819   }
820   if (obj) PutSpawnedMapObject(player.ix+dx, player.iy+dy, obj);
821   return obj;
825 final MapObject debugSpawnObject (name aname) {
826   if (!aname) return none;
827   return debugSpawnObjectWithClass(findGameObjectClassByName(aname));
831 void createSpecialLevel (LevelKind kind, scope void delegate () creator, name amusic) {
832   global.darkLevel = false;
833   udjatAlarm = 0;
834   xmoney = 0;
835   collectCounter = 0;
836   global.resetStartingItems();
838   global.setMusicPitch(1.0);
839   levelKind = kind;
841   auto olddel = ImmediateDelete;
842   ImmediateDelete = false;
843   clearWholeLevel();
845   creator();
847   setMenuTilesOnTop();
849   fixWallTiles();
850   addBackgroundGfxDetails();
851   //levBGImgName = 'bgCave';
852   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
854   blockWaterChecking = true;
855   fixLiquidTop();
856   cleanDeadTiles();
858   ImmediateDelete = olddel;
859   CollectGarbage(true); // destroy delayed objects too
861   if (dumpGridStats) objGrid.dumpStats();
863   playerExited = false; // just in case
864   playerExitDoor = none;
866   osdClear(clearTalk:true);
868   setupGhostTime();
869   lg.musicName = amusic;
870   lastMusicName = amusic;
871   global.setMusicPitch(1.0);
872   if (amusic) global.playMusic(lg.musicName); else global.stopMusic();
876 void createTitleLevel () {
877   createSpecialLevel(LevelKind.Title, &createTitleRoom, 'musTitle');
881 void createTutorialLevel () {
882   createSpecialLevel(LevelKind.Tutorial, &createTutorialRoom, 'musCave');
883   global.plife = 4;
884   global.bombs = 0;
885   global.rope = 4;
886   global.arrows = 0;
887   global.sgammo = 0;
891 // `global.currLevel` is the new level
892 void generateTransitionLevel () {
893   global.darkLevel = false;
894   udjatAlarm = 0;
895   xmoney = 0;
896   collectCounter = 0;
898   resetTransitionOverlay();
900   global.setMusicPitch(1.0);
901   switch (global.config.transitionMusicMode) {
902     case GameConfig::MusicMode.Silent: global.stopMusic(); break;
903     case GameConfig::MusicMode.Restart: global.restartMusic(); break;
904     case GameConfig::MusicMode.DontTouch: break;
905   }
907   levelKind = LevelKind.Transition;
909   auto olddel = ImmediateDelete;
910   ImmediateDelete = false;
911   clearWholeLevel();
913        if (global.currLevel < 4) createTrans1Room();
914   else if (global.currLevel == 4) createTrans1xRoom();
915   else if (global.currLevel < 8) createTrans2Room();
916   else if (global.currLevel == 8) createTrans2xRoom();
917   else if (global.currLevel < 12) createTrans3Room();
918   else if (global.currLevel == 12) createTrans3xRoom();
919   else if (global.currLevel < 16) createTrans4Room();
920   else if (global.currLevel == 16) createTrans4Room();
921   else createTrans1Room(); //???
923   setMenuTilesOnTop();
925   fixWallTiles();
926   addBackgroundGfxDetails();
927   //levBGImgName = 'bgCave';
928   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
930   blockWaterChecking = true;
931   fixLiquidTop();
932   cleanDeadTiles();
934   if (damselSaved > 0) {
935     // this is special "damsel ready to kiss you" object, not a heart
936     MakeMapObject(176+8, 176+8, 'oDamselKiss');
937     global.plife += damselSaved; // if player skipped transition cutscene
938     damselSaved = 0;
939   }
941   ImmediateDelete = olddel;
942   CollectGarbage(true); // destroy delayed objects too
944   if (dumpGridStats) objGrid.dumpStats();
946   playerExited = false; // just in case
947   playerExitDoor = none;
949   osdClear(clearTalk:true);
951   setupGhostTime();
952   //global.playMusic(lg.musicName);
956 void generateLevel () {
957   levelStartTime = time;
958   levelEndTime = time;
960   udjatAlarm = 0;
961   if (resetBMCOG) {
962     resetBMCOG = false;
963     global.genBlackMarket = false;
964   }
966   global.setMusicPitch(1.0);
967   stats.clearLevelTotals();
969   levelKind = LevelKind.Normal;
970   lg.generate();
971   //lg.dump();
973   resetRoomBounds();
975   lg.generateRooms();
976   //writeln("tw:", tilesWidth, "; th:", tilesHeight);
978   auto olddel = ImmediateDelete;
979   ImmediateDelete = false;
980   clearWholeLevel();
982   if (lg.finalBossLevel) {
983     blockWaterChecking = true;
984     createOlmecRoom();
985   }
987   // if transition cutscene was skipped...
988   global.plife += max(0, damselSaved); // if player skipped transition cutscene
989   damselSaved = 0;
991   // generate tiles
992   startRoomX = lg.startRoomX;
993   startRoomY = lg.startRoomY;
994   endRoomX = lg.endRoomX;
995   endRoomY = lg.endRoomY;
996   addBackgroundGfxDetails();
997   foreach (int y; 0..tilesHeight) {
998     foreach (int x; 0..tilesWidth) {
999       lg.genTileAt(x, y);
1000     }
1001   }
1002   fixWallTiles();
1004   levBGImgName = lg.bgImgName;
1005   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
1007   if (global.allowAngryShopkeepers) generateAngryShopkeepers();
1009   lg.generateEntities();
1011   // add box of flares to dark level
1012   if (global.darkLevel && allEnters.length) {
1013     auto enter = allEnters[0];
1014     int x = enter.ix, y = enter.iy;
1015          if (!isSolidAtPoint(x-16, y)) MakeMapObject(x-16+8, y+8, 'oFlareCrate');
1016     else if (!isSolidAtPoint(x+16, y)) MakeMapObject(x+16+8, y+8, 'oFlareCrate');
1017     else MakeMapObject(x+8, y+8, 'oFlareCrate');
1018   }
1020   //scrGenerateEntities();
1021   //foreach (; 0..2) scrGenerateEntities();
1023   writeln(objGrid.countObjects, " alive objects inserted");
1024   writeln(countBackTiles, " background tiles inserted");
1026   if (!player) FatalError("player pawn is not spawned");
1028   if (lg.finalBossLevel) {
1029     blockWaterChecking = true;
1030   } else {
1031     blockWaterChecking = false;
1032   }
1033   fixLiquidTop();
1034   cleanDeadTiles();
1036   ImmediateDelete = olddel;
1037   CollectGarbage(true); // destroy delayed objects too
1039   if (dumpGridStats) objGrid.dumpStats();
1041   playerExited = false; // just in case
1042   playerExitDoor = none;
1044   levelMoneyStart = stats.money;
1046   osdClear(clearTalk:true);
1047   generateLevelMessages();
1049   xmoney = 0;
1050   collectCounter = 0;
1052   //writeln("!<", lastMusicName, ">:<", lg.musicName, ">");
1053   global.setMusicPitch(1.0);
1054   if (lastMusicName != lg.musicName) {
1055     global.playMusic(lg.musicName);
1056   } else {
1057     //writeln("MM: ", global.config.nextLevelMusicMode);
1058     switch (global.config.nextLevelMusicMode) {
1059       case GameConfig::MusicMode.Silent: global.stopMusic(); break; // the thing that should not be
1060       case GameConfig::MusicMode.Restart: global.restartMusic(); break;
1061       case GameConfig::MusicMode.DontTouch:
1062         if (global.config.transitionMusicMode == GameConfig::MusicMode.Silent) {
1063           global.playMusic(lg.musicName);
1064         }
1065         break;
1066     }
1067   }
1068   lastMusicName = lg.musicName;
1069   //global.playMusic(lg.musicName);
1071   setupGhostTime();
1072   if (global.cityOfGold == 1 || global.genBlackMarket) resetBMCOG = true;
1074   if (global.cityOfGold == 1) {
1075     lg.mapSprite = 'sMapTemple';
1076     lg.mapTitle = "City of Gold";
1077   } else if (global.blackMarket) {
1078     lg.mapSprite = 'sMapJungle';
1079     lg.mapTitle = "Black Market";
1080   }
1084 // ////////////////////////////////////////////////////////////////////////// //
1085 int currKeys, nextKeys;
1086 int pressedKeysQ, releasedKeysQ;
1087 int keysPressed, keysReleased = -1;
1090 struct SavedKeyState {
1091   int currKeys, nextKeys;
1092   int pressedKeysQ, releasedKeysQ;
1093   int keysPressed, keysReleased;
1094   // for session
1095   int roomSeed, otherSeed;
1099 // for saving/replaying
1100 final void keysSaveState (out SavedKeyState ks) {
1101   ks.currKeys = currKeys;
1102   ks.nextKeys = nextKeys;
1103   ks.pressedKeysQ = pressedKeysQ;
1104   ks.releasedKeysQ = releasedKeysQ;
1105   ks.keysPressed = keysPressed;
1106   ks.keysReleased = keysReleased;
1109 // for saving/replaying
1110 final void keysRestoreState (const ref SavedKeyState ks) {
1111   currKeys = ks.currKeys;
1112   nextKeys = ks.nextKeys;
1113   pressedKeysQ = ks.pressedKeysQ;
1114   releasedKeysQ = ks.releasedKeysQ;
1115   keysPressed = ks.keysPressed;
1116   keysReleased = ks.keysReleased;
1120 final void keysNextFrame () {
1121   currKeys = nextKeys;
1125 final void clearKeys () {
1126   currKeys = 0;
1127   nextKeys = 0;
1128   pressedKeysQ = 0;
1129   releasedKeysQ = 0;
1130   keysPressed = 0;
1131   keysReleased = -1;
1135 final void onKey (int code, bool down) {
1136   if (!code) return;
1137   if (down) {
1138     currKeys |= code;
1139     nextKeys |= code;
1140     if (keysReleased&code) {
1141       keysPressed |= code;
1142       keysReleased &= ~code;
1143       pressedKeysQ |= code;
1144     }
1145   } else {
1146     nextKeys &= ~code;
1147     if (keysPressed&code) {
1148       keysReleased |= code;
1149       keysPressed &= ~code;
1150       releasedKeysQ |= code;
1151     }
1152   }
1155 final bool isKeyDown (int code) {
1156   return !!(currKeys&code);
1159 final bool isKeyPressed (int code) {
1160   bool res = !!(pressedKeysQ&code);
1161   pressedKeysQ &= ~code;
1162   return res;
1165 final bool isKeyReleased (int code) {
1166   bool res = !!(releasedKeysQ&code);
1167   releasedKeysQ &= ~code;
1168   return res;
1172 final void clearKeysPressRelease () {
1173   keysPressed = default.keysPressed;
1174   keysReleased = default.keysReleased;
1175   pressedKeysQ = default.pressedKeysQ;
1176   releasedKeysQ = default.releasedKeysQ;
1177   currKeys = 0;
1178   nextKeys = 0;
1182 // ////////////////////////////////////////////////////////////////////////// //
1183 final void registerEnter (MapTile t) {
1184   if (!t) return;
1185   allEnters[$] = t;
1186   return;
1190 final void registerExit (MapTile t) {
1191   if (!t) return;
1192   allExits[$] = t;
1193   return;
1197 final bool isYAtEntranceRow (int py) {
1198   py /= 16;
1199   foreach (MapTile t; allEnters) if (t.iy == py) return true;
1200   return false;
1204 final int calcNearestEnterDist (int px, int py) {
1205   if (allEnters.length == 0) return int.max;
1206   int curdistsq = int.max;
1207   foreach (MapTile t; allEnters) {
1208     int xc = px-t.xCenter, yc = py-t.yCenter;
1209     int distsq = xc*xc+yc*yc;
1210     if (distsq < curdistsq) curdistsq = distsq;
1211   }
1212   return round(sqrt(curdistsq));
1216 final int calcNearestExitDist (int px, int py) {
1217   if (allExits.length == 0) return int.max;
1218   int curdistsq = int.max;
1219   foreach (MapTile t; allExits) {
1220     int xc = px-t.xCenter, yc = py-t.yCenter;
1221     int distsq = xc*xc+yc*yc;
1222     if (distsq < curdistsq) curdistsq = distsq;
1223   }
1224   return round(sqrt(curdistsq));
1228 // ////////////////////////////////////////////////////////////////////////// //
1229 final void clearForTransition () {
1230   auto olddel = ImmediateDelete;
1231   ImmediateDelete = false;
1232   clearWholeLevel();
1233   ImmediateDelete = olddel;
1234   CollectGarbage(true); // destroy delayed objects too
1235   global.darkLevel = false;
1239 // ////////////////////////////////////////////////////////////////////////// //
1240 final int countBackTiles () {
1241   int res = 0;
1242   for (MapBackTile bt = backtiles; bt; bt = bt.next) ++res;
1243   return res;
1247 final void clearWholeLevel () {
1248   allEnters.clear();
1249   allExits.clear();
1251   // don't kill objects the player is holding
1252   if (player) {
1253     if (player.pickedItem isa ItemBall) {
1254       player.pickedItem.instanceRemove();
1255       player.pickedItem = none;
1256     }
1257     if (player.pickedItem && player.pickedItem.grid) {
1258       player.pickedItem.grid.remove(player.pickedItem.gridId);
1259       writeln("secured pocket item '", GetClassName(player.pickedItem.Class), "'");
1260     }
1261     if (player.holdItem isa ItemBall) {
1262       player.removeBallAndChain(temp:true);
1263       if (player.holdItem) player.holdItem.instanceRemove();
1264       player.holdItem = none;
1265     }
1266     if (player.holdItem && player.holdItem.grid) {
1267       player.holdItem.grid.remove(player.holdItem.gridId);
1268       writeln("secured held item '", GetClassName(player.holdItem.Class), "'");
1269     }
1270     writeln("secured ball; mustBeChained=", player.mustBeChained, "; wasHoldingBall=", player.wasHoldingBall);
1271   }
1273   int count = objGrid.countObjects();
1274   if (dumpGridStats) { if (objGrid.getFirstObjectCID()) objGrid.dumpStats(); }
1275   objGrid.removeAllObjects(true); // and destroy
1276   if (count > 0) writeln(count, " objects destroyed");
1278   lastUsedObjectId = 0;
1279   accumTime = 0;
1280   //!time = 0;
1281   lastRenderTime = -1;
1282   liquidTileCount = 0;
1283   checkWater = false;
1285   while (backtiles) {
1286     MapBackTile t = backtiles;
1287     backtiles = t.next;
1288     delete t;
1289   }
1291   levBGImg = none;
1292   framesProcessedFromLastClear = 0;
1296 final void insertObject (MapEntity o) {
1297   if (!o) return;
1298   if (o.grid) FatalError("cannot put object into level twice");
1299   objGrid.insert(o);
1303 final void spawnPlayerAt (int x, int y) {
1304   // if we have no player, spawn new one
1305   // otherwise this just a level transition, so simply reposition him
1306   if (!player) {
1307     // don't add player to object list, as it has very separate processing anyway
1308     player = SpawnObject(PlayerPawn);
1309     player.global = global;
1310     player.level = self;
1311     if (!player.initialize()) {
1312       delete player;
1313       FatalError("something is wrong with player initialization");
1314       return;
1315     }
1316   }
1317   player.fltx = x;
1318   player.flty = y;
1319   player.saveInterpData();
1320   player.resurrect();
1321   if (player.mustBeChained || global.config.scumBallAndChain) {
1322     writeln("*** spawning ball and chain");
1323     player.spawnBallAndChain(levelStart:true);
1324   }
1325   playerExited = false;
1326   playerExitDoor = none;
1327   if (global.config.startWithKapala) global.hasKapala = true;
1328   centerViewAtPlayer();
1329   // reinsert player items into grid
1330   if (player.pickedItem) objGrid.insert(player.pickedItem);
1331   if (player.holdItem) objGrid.insert(player.holdItem);
1332   //writeln("player spawned; active=", player.active);
1333   player.scrSwitchToPocketItem(forceIfEmpty:false);
1337 final void teleportPlayerTo (int x, int y) {
1338   if (player) {
1339     player.fltx = x;
1340     player.flty = y;
1341     player.saveInterpData();
1342   }
1346 final void resurrectPlayer () {
1347   if (player) player.resurrect();
1348   playerExited = false;
1349   playerExitDoor = none;
1353 // ////////////////////////////////////////////////////////////////////////// //
1354 final void scrShake (int duration) {
1355   if (shakeLeft == 0) {
1356     shakeOfs.x = 0;
1357     shakeOfs.y = 0;
1358     shakeDir.x = 0;
1359     shakeDir.y = 0;
1360   }
1361   shakeLeft = max(shakeLeft, duration);
1366 // ////////////////////////////////////////////////////////////////////////// //
1367 enum SCAnger {
1368   TileDestroyed,
1369   ItemStolen, // including damsel, lol
1370   CrapsCheated,
1371   BombDropped,
1372   DamselWhipped,
1375 // checks for dead, agnered, distance, etc. should be already done
1376 protected void doAngerShopkeeper (MonsterShopkeeper shp, SCAnger reason, ref bool messaged,
1377                                   int maxdist, MapEntity offender)
1379   if (!shp || shp.dead || shp.angered) return;
1380   if (offender.distanceToEntityCenter(shp) > maxdist) return;
1382   shp.status = MapObject::ATTACK;
1383   string msg;
1384   if (global.murderer) {
1385     msg = "~YOU'LL PAY FOR YOUR CRIMES!~";
1386   } else {
1387     switch (reason) {
1388       case SCAnger.TileDestroyed: msg = "~DIE, YOU VANDAL!~"; break;
1389       case SCAnger.ItemStolen: msg = "~COME BACK HERE, THIEF!~"; break;
1390       case SCAnger.CrapsCheated: msg = "~DIE, CHEATER!~"; break;
1391       case SCAnger.BombDropped: msg = "~TERRORIST!~"; break;
1392       case SCAnger.DamselWhipped: msg = "~HEY, ONLY I CAN DO THAT!~"; break;
1393       default: "~NOW I'M REALLY STEAMED!~"; break;
1394     }
1395   }
1397   writeln("shopkeeper angered; reason=", reason, "; maxdist=", maxdist, "; msg=\"", msg, "\"");
1398   if (!messaged) {
1399     messaged = true;
1400     if (msg) osdMessageTalk(msg, replace:true, inShopOnly:false, hiColor1:0xff_00_00);
1401     global.thiefLevel += (global.thiefLevel > 0 ? 3 : 2);
1402   }
1406 // make the nearest shopkeeper angry. RAWR!
1407 void scrShopkeeperAnger (SCAnger reason, optional int maxdist, optional MapEntity offender) {
1408   bool messaged = false;
1409   maxdist = clamp(maxdist, 96, 100000);
1410   if (!offender) offender = player;
1411   if (maxdist == 100000) {
1412     foreach (MonsterShopkeeper shp; objGrid.allObjects(MonsterShopkeeper)) {
1413       doAngerShopkeeper(shp, reason, messaged, maxdist, offender);
1414     }
1415   } else {
1416     foreach (MonsterShopkeeper shp; objGrid.inRectPix(offender.xCenter-maxdist-128, offender.yCenter-maxdist-128, (maxdist+128)*2, (maxdist+128)*2, precise:false, castClass:MonsterShopkeeper)) {
1417       doAngerShopkeeper(shp, reason, messaged, maxdist, offender);
1418     }
1419   }
1423 final MapObject findCrapsPrize () {
1424   foreach (MapObject o; objGrid.allObjects(MapObject)) {
1425     if (!o.spectral && o.inDiceHouse) return o;
1426   }
1427   return none;
1431 // ////////////////////////////////////////////////////////////////////////// //
1432 // 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.
1433 // note: idols moved by monkeys will have false `stolenIdol`
1434 void scrTriggerIdolAltar (bool stolenIdol) {
1435   ObjTikiCurse res = none;
1436   int curdistsq = int.max;
1437   int px = player.xCenter, py = player.yCenter;
1438   foreach (MapObject o; objGrid.allObjects(MapObject)) {
1439     auto tcr = ObjTikiCurse(o);
1440     if (!tcr) continue;
1441     if (tcr.activated) continue;
1442     int xc = px-tcr.xCenter, yc = py-tcr.yCenter;
1443     int distsq = xc*xc+yc*yc;
1444     if (distsq < curdistsq) {
1445       res = tcr;
1446       curdistsq = distsq;
1447     }
1448   }
1449   if (res) res.activate(stolenIdol);
1453 // ////////////////////////////////////////////////////////////////////////// //
1454 void setupGhostTime () {
1455   musicFadeTimer = -1;
1456   ghostSpawned = false;
1458   // there is no ghost on the first level
1459   if (inWinCutscene || inIntroCutscene || !isNormalLevel() || lg.finalBossLevel ||
1460       (!global.config.ghostAtFirstLevel && global.currLevel == 1))
1461   {
1462     ghostTimeLeft = -1;
1463     global.setMusicPitch(1.0);
1464     return;
1465   }
1467   if (global.config.scumGhost < 0) {
1468     // instant
1469     ghostTimeLeft = 1;
1470     osdMessage("A GHASTLY VOICE CALLS YOUR NAME!\nLET'S GET OUT OF HERE!", 6.66, urgent:true);
1471     return;
1472   }
1474   if (global.config.scumGhost == 0) {
1475     // never
1476     ghostTimeLeft = -1;
1477     return;
1478   }
1480   // randomizes time until ghost appears once time limit is reached
1481   // global.ghostRandom (true/false) if extra time is randomized - loaded from yasm.cfg. if false, use 30 second default
1482   // ghostTimeLeft (time in seconds * 1000) for currently generated level
1484   if (global.config.ghostRandom) {
1485     auto tMin = global.randOther(10, 30); // min(global.plife*10, 30);
1486     auto tMax = max(tMin+global.randOther(10, 30), min(global.config.scumGhost, 300)); // 5 minutes max
1487     auto tTime = global.randOther(tMin, tMax);
1488     if (tTime <= 0) tTime = round(tMax/2.0);
1489     ghostTimeLeft = tTime;
1490   } else {
1491     ghostTimeLeft = global.config.scumGhost; // 5 minutes max
1492   }
1494   ghostTimeLeft += max(0, global.config.ghostExtraTime);
1496   ghostTimeLeft *= 30; // seconds -> frames
1497   //global.ghostShowTime
1501 void spawnGhost () {
1502   addGhostSummoned();
1503   ghostSpawned = true;
1504   ghostTimeLeft = -1;
1506   int vwdt = (viewMax.x-viewMin.x);
1507   int vhgt = (viewMax.y-viewMin.y);
1509   int gx, gy;
1511   if (player.ix < viewMin.x+vwdt/2) {
1512     // player is in the left side
1513     gx = viewMin.x+vwdt/2+vwdt/4;
1514   } else {
1515     // player is in the right side
1516     gx = viewMin.x+vwdt/4;
1517   }
1519   if (player.iy < viewMin.y+vhgt/2) {
1520     // player is in the left side
1521     gy = viewMin.y+vhgt/2+vhgt/4;
1522   } else {
1523     // player is in the right side
1524     gy = viewMin.y+vhgt/4;
1525   }
1527   writeln("spawning ghost at tile (", gx/16, ",", gy/16, "); player is at tile (", player.ix/16, ",", player.iy/16, ")");
1529   MakeMapObject(gx, gy, 'oGhost');
1531   /*
1532     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);
1533     else instance_create(view_xview[0]-32, view_yview[0]+floor(view_hview[0]/2), oGhost);
1534     global.ghostExists = true;
1535   */
1539 void thinkFrameGameGhost () {
1540   if (player.dead) return;
1541   if (!isNormalLevel()) return; // just in case
1543   if (ghostTimeLeft < 0) {
1544     // turned off
1545     if (musicFadeTimer > 0) {
1546       musicFadeTimer = -1;
1547       global.setMusicPitch(1.0);
1548     }
1549     return;
1550   }
1552   if (musicFadeTimer >= 0) {
1553     ++musicFadeTimer;
1554     if (musicFadeTimer > 0 && musicFadeTimer <= 100*3) {
1555       float pitch = 1.0-(0.00226/3.0)*musicFadeTimer;
1556       //writeln("MFT: ", musicFadeTimer, "; pitch=", pitch);
1557       global.setMusicPitch(pitch);
1558     }
1559   }
1561   if (ghostTimeLeft == 0) {
1562     // she is already here!
1563     return;
1564   }
1566   // no ghost if we have a crown
1567   if (global.hasCrown) {
1568     ghostTimeLeft = -1;
1569     return;
1570   }
1572   // if she was already spawned, don't do it again
1573   if (ghostSpawned) {
1574     ghostTimeLeft = 0;
1575     return;
1576   }
1578   if (--ghostTimeLeft != 0) {
1579     // warning
1580     if (global.config.ghostExtraTime > 0) {
1581       if (ghostTimeLeft == global.config.ghostExtraTime*30) {
1582         osdMessage("A CHILL RUNS UP YOUR SPINE...\nLET'S GET OUT OF HERE!", 6.66, urgent:true);
1583       }
1584       if (musicFadeTimer < 0 && ghostTimeLeft <= global.config.ghostExtraTime*30) {
1585         musicFadeTimer = 0;
1586       }
1587     }
1588     return;
1589   }
1591   // spawn her
1592   if (player.isExitingSprite) {
1593     // no reason to spawn her, we're leaving
1594     ghostTimeLeft = -1;
1595     return;
1596   }
1598   spawnGhost();
1602 void thinkFrameGame () {
1603   thinkFrameGameGhost();
1604   // udjat eye blinking
1605   if (global.hasUdjatEye && player) {
1606     foreach (MapTile t; allExits) {
1607       if (t isa MapTileBlackMarketDoor) {
1608         auto dm = int(player.distanceToEntity(t));
1609         if (dm < 4) dm = 4;
1610         if (udjatAlarm < 1 || dm < udjatAlarm) udjatAlarm = dm;
1611       }
1612     }
1613   } else {
1614     global.udjatBlink = false;
1615     udjatAlarm = 0;
1616   }
1617   if (udjatAlarm > 0) {
1618     if (--udjatAlarm == 0) {
1619       global.udjatBlink = !global.udjatBlink;
1620       if (global.hasUdjatEye && player) {
1621         player.playSound(global.udjatBlink ? 'sndBlink1' : 'sndBlink2');
1622       }
1623     }
1624   }
1625   switch (levelKind) {
1626     case LevelKind.Stars: thinkFrameGameStars(); break;
1627     case LevelKind.Sun: thinkFrameGameSun(); break;
1628     case LevelKind.Moon: thinkFrameGameMoon(); break;
1629     case LevelKind.Transition: thinkFrameTransition(); break;
1630     case LevelKind.Intro: thinkFrameIntro(); break;
1631   }
1635 // ////////////////////////////////////////////////////////////////////////// //
1636 private final bool isWaterTileCB (MapTile t) {
1637   return (t && t.visible && t.water);
1641 private final bool isLavaTileCB (MapTile t) {
1642   return (t && t.visible && t.lava);
1646 // ////////////////////////////////////////////////////////////////////////// //
1647 const int GreatLakeStartTileY = 28;
1650 final void fillGreatLake () {
1651   if (global.lake == 1) {
1652     foreach (int y; GreatLakeStartTileY..tilesHeight) {
1653       foreach (int x; 0..tilesWidth) {
1654         auto t = checkTileAtPoint(x*16, y*16, delegate bool (MapTile t) {
1655           if (t.spectral || !t.visible || t.invisible || t.moveable) return false;
1656           return true;
1657         });
1658         if (!t) {
1659           t = MakeMapTile(x, y, 'oWaterSwim');
1660           if (!t) continue;
1661         }
1662         if (t.water) {
1663           t.setSprite(y == GreatLakeStartTileY ? 'sWaterTop' : 'sWater');
1664         } else if (t.lava) {
1665           t.setSprite(y == GreatLakeStartTileY ? 'sLavaTop' : 'sLava');
1666         }
1667       }
1668     }
1669   }
1673 // called once after level generation
1674 final void fixLiquidTop () {
1675   if (global.lake == 1) fillGreatLake();
1677   liquidTileCount = 0;
1678   foreach (MapTile t; objGrid.allObjects(MapTile)) {
1679     if (!t.water && !t.lava) continue;
1681     ++liquidTileCount;
1682     //writeln("fixing water tile(", GetClassName(t.Class), "):'", t.objName, "' (water=", t.water, "; lava=", t.lava, "); lqc=", liquidTileCount);
1684     //if (global.lake == 1) continue; // it is done in `fillGreatLake()`
1686     if (!checkTileAtPoint(t.ix+8, t.iy-8, (t.lava ? &isLavaTileCB : &isWaterTileCB))) {
1687       t.setSprite(t.lava ? 'sLavaTop' : 'sWaterTop');
1688     } else {
1689       // don't do this, it will destroy seaweed
1690       //t.setSprite(t.lava ? 'sLava' : 'sWater');
1691       auto spr = t.getSprite();
1692            if (!spr) t.setSprite(t.lava ? 'sLava' : 'sWater');
1693       else if (spr.Name == 'sLavaTop') t.setSprite('sLava');
1694       else if (spr.Name == 'sWaterTop') t.setSprite('sWater');
1695     }
1696   }
1697   //writeln("liquid tiles count: ", liquidTileCount);
1701 // ////////////////////////////////////////////////////////////////////////// //
1702 transient MapTile curWaterTile;
1703 transient bool curWaterTileCheckHitsLava;
1704 transient bool curWaterTileCheckHitsSolidOrWater; // only for `checkWaterOrSolidTilePartialCB`
1705 transient int curWaterTileLastHDir;
1706 transient ubyte[16, 16] curWaterOccupied;
1707 transient int curWaterOccupiedCount;
1708 transient int curWaterTileCheckX0, curWaterTileCheckY0;
1711 private final void clearCurWaterCheckState () {
1712   curWaterTileCheckHitsLava = false;
1713   curWaterOccupiedCount = 0;
1714   foreach (auto idx; 0..16*16) curWaterOccupied[idx] = 0;
1718 private final bool checkWaterOrSolidTileCB (MapTile t) {
1719   if (t == curWaterTile) return false;
1720   if (t.lava && curWaterTile.water) {
1721     curWaterTileCheckHitsLava = true;
1722     return true;
1723   }
1724   if (t.ix%16 != 0 || t.iy%16 != 0) {
1725     if (t.water || t.solid) {
1726       // fill occupied array
1727       //FIXME: optimize this
1728       if (curWaterOccupiedCount < 16*16) {
1729         foreach (auto dy; t.y0..t.y1+1) {
1730           foreach (auto dx; t.x0..t.x1+1) {
1731             int sx = dx-curWaterTileCheckX0;
1732             int sy = dy-curWaterTileCheckY0;
1733             if (sx >= 0 && sx <= 16 && sy >= 0 && sy <= 15 && !curWaterOccupied[sx, sy]) {
1734               curWaterOccupied[sx, sy] = 1;
1735               ++curWaterOccupiedCount;
1736             }
1737           }
1738         }
1739       }
1740     }
1741     return false; // need to check for lava
1742   }
1743   if (t.water || t.solid || t.lava) {
1744     curWaterOccupiedCount = 16*16;
1745     if (t.water && curWaterTile.lava) t.instanceRemove();
1746   }
1747   return false; // need to check for lava
1751 private final bool checkWaterOrSolidTilePartialCB (MapTile t) {
1752   if (t == curWaterTile) return false;
1753   if (t.lava && curWaterTile.water) {
1754     //writeln("!!!!!!!!");
1755     curWaterTileCheckHitsLava = true;
1756     return true;
1757   }
1758   if (t.water || t.solid || t.lava) {
1759     //writeln("*********");
1760     curWaterTileCheckHitsSolidOrWater = true;
1761     if (t.water && curWaterTile.lava) t.instanceRemove();
1762   }
1763   return false; // need to check for lava
1767 private final bool isFullyOccupiedAtTilePos (int tileX, int tileY) {
1768   clearCurWaterCheckState();
1769   curWaterTileCheckX0 = tileX*16;
1770   curWaterTileCheckY0 = tileY*16;
1771   checkTilesInRect(tileX*16, tileY*16, 16, 16, &checkWaterOrSolidTileCB);
1772   return (curWaterTileCheckHitsLava || curWaterOccupiedCount == 16*16);
1776 private final bool isAtLeastPartiallyOccupiedAtTilePos (int tileX, int tileY) {
1777   curWaterTileCheckHitsLava = false;
1778   curWaterTileCheckHitsSolidOrWater = false;
1779   checkTilesInRect(tileX*16, tileY*16, 16, 16, &checkWaterOrSolidTilePartialCB);
1780   return (curWaterTileCheckHitsSolidOrWater || curWaterTileCheckHitsLava);
1784 private final bool waterCanReachGroundHoleInDir (MapTile wtile, int dx) {
1785   if (dx == 0) return false; // just in case
1786   dx = sign(dx);
1787   int x = wtile.ix/16, y = wtile.iy/16;
1788   x += dx;
1789   while (x >= 0 && x < tilesWidth) {
1790     if (!isAtLeastPartiallyOccupiedAtTilePos(x, y+1)) return true;
1791     if (isAtLeastPartiallyOccupiedAtTilePos(x, y)) return false;
1792     x += dx;
1793   }
1794   return false;
1798 // returns `true` if this tile must be removed
1799 private final bool checkWaterFlow (MapTile wtile) {
1800   if (global.lake == 1) {
1801     if (wtile.iy >= GreatLakeStartTileY*16) return false; // lake tile, don't touch
1802     if (wtile.iy >= GreatLakeStartTileY*16-16) return true; // remove it, so it won't stack on a lake
1803   }
1805   if (wtile.ix%16 != 0 || wtile.iy%16 != 0) return true; // sanity check
1807   curWaterTile = wtile;
1808   curWaterTileLastHDir = 0; // never moved to the side
1810   bool wasMoved = false;
1812   for (;;) {
1813     int tileX = wtile.ix/16, tileY = wtile.iy/16;
1815     // out of level?
1816     if (tileY >= tilesHeight) return true;
1818     // check if we can fall down
1819     auto canFall = !isAtLeastPartiallyOccupiedAtTilePos(tileX, tileY+1);
1820     // disappear if can fall in lava
1821     if (wtile.water && curWaterTileCheckHitsLava) {
1822       //!writeln(wtile.objId, ": LAVA HIT DOWN");
1823       return true;
1824     }
1825     if (wasMoved) {
1826       // fake, so caller will not start removing tiles
1827       if (canFall) wtile.waterMovedDown = true;
1828       break;
1829     }
1830     // can move down?
1831     if (canFall) {
1832       // move down
1833       //!writeln(wtile.objId, ": GOING DOWN");
1834       curWaterTileLastHDir = 0;
1835       wtile.iy = wtile.iy+16;
1836       wasMoved = true;
1837       wtile.waterMovedDown = true;
1838       continue;
1839     }
1841     bool canMoveLeft = (curWaterTileLastHDir > 0 ? false : !isAtLeastPartiallyOccupiedAtTilePos(tileX-1, tileY));
1842     // disappear if near lava
1843     if (wtile.water && curWaterTileCheckHitsLava) {
1844       //!writeln(wtile.objId, ": LAVA HIT LEFT");
1845       return true;
1846     }
1848     bool canMoveRight = (curWaterTileLastHDir < 0 ? false : !isAtLeastPartiallyOccupiedAtTilePos(tileX+1, tileY));
1849     // disappear if near lava
1850     if (wtile.water && curWaterTileCheckHitsLava) {
1851       //!writeln(wtile.objId, ": LAVA HIT RIGHT");
1852       return true;
1853     }
1855     if (!canMoveLeft && !canMoveRight) {
1856       // do final checks
1857       //!if (wasMove) writeln(wtile.objId, ": NO MORE MOVES");
1858       break;
1859     }
1861     if (canMoveLeft && canMoveRight) {
1862       // choose random direction
1863       //!writeln(wtile.objId, ": CHOOSING RANDOM HDIR");
1864       // actually, choose direction that leads to hole in a ground
1865       if (waterCanReachGroundHoleInDir(wtile, -1)) {
1866         // can reach hole at the left side
1867         if (waterCanReachGroundHoleInDir(wtile, 1)) {
1868           // can reach hole at the right side, choose at random
1869           if (global.randOther(0, 1)) canMoveRight = false; else canMoveLeft = false;
1870         } else {
1871           // move left
1872           canMoveRight = false;
1873         }
1874       } else {
1875         // can't reach hole at the left side
1876         if (waterCanReachGroundHoleInDir(wtile, 1)) {
1877           // can reach hole at the right side, choose at random
1878           canMoveLeft = false;
1879         } else {
1880           // no holes at any side, choose at random
1881           if (global.randOther(0, 1)) canMoveRight = false; else canMoveLeft = false;
1882         }
1883       }
1884     }
1886     // move
1887     if (canMoveLeft) {
1888       if (canMoveRight) FatalError("WATERCHECK: WTF RIGHT");
1889       //!writeln(wtile.objId, ": MOVING LEFT (", curWaterTileLastHDir, ")");
1890       curWaterTileLastHDir = -1;
1891       wtile.ix = wtile.ix-16;
1892     } else if (canMoveRight) {
1893       if (canMoveLeft) FatalError("WATERCHECK: WTF LEFT");
1894       //!writeln(wtile.objId, ": MOVING RIGHT (", curWaterTileLastHDir, ")");
1895       curWaterTileLastHDir = 1;
1896       wtile.ix = wtile.ix+16;
1897     }
1898     wasMoved = true;
1899   }
1901   // remove seaweeds
1902   if (wasMoved) {
1903     checkWater = true;
1904     wtile.setSprite(wtile.lava ? 'sLava' : 'sWater');
1905     wtile.waterMoved = true;
1906     // if this tile was not moved down, check if it can move down on any next step
1907     if (!wtile.waterMovedDown) {
1908            if (waterCanReachGroundHoleInDir(wtile, -1)) wtile.waterMovedDown = true;
1909       else if (waterCanReachGroundHoleInDir(wtile, 1)) wtile.waterMovedDown = true;
1910     }
1911   }
1913   return false; // don't remove
1915   //if (!isWetTileAtPix(tileX*16+8, tileY*16-8)) wtile.setSprite(wtile.lava ? 'sLavaTop' : 'sWaterTop');
1919 transient array!MapTile waterTilesList;
1921 final bool sortWaterTilesByCoordsLess (MapTile a, MapTile b) {
1922   int dy = a.iy-b.iy;
1923   if (dy) return (dy < 0);
1924   return (a.ix < b.ix);
1927 transient int waterFlowPause = 0;
1928 transient bool debugWaterFlowPause = false;
1930 final void cleanDeadObjects () {
1931   // remove dead objects
1932   if (deadItemsHead) {
1933     auto olddel = ImmediateDelete;
1934     ImmediateDelete = false;
1935     do {
1936       auto it = deadItemsHead;
1937       deadItemsHead = it.deadItemsNext;
1938       if (it.grid) it.grid.remove(it.gridId);
1939       it.onDestroy();
1940       delete it;
1941     } while (deadItemsHead);
1942     ImmediateDelete = olddel;
1943     if (olddel) CollectGarbage(true); // destroy delayed objects too
1944   }
1947 final void cleanDeadTiles () {
1948   if (checkWater && /*global.lake == 1 ||*/ (!blockWaterChecking && liquidTileCount)) {
1949     if (global.lake == 1) fillGreatLake();
1950     if (waterFlowPause > 1) {
1951       --waterFlowPause;
1952       cleanDeadObjects();
1953       return;
1954     }
1955     if (debugWaterFlowPause) waterFlowPause = 4;
1956     //writeln("checking water");
1957     waterTilesList.clear();
1958     foreach (MapTile wtile; objGrid.allObjectsSafe(MapTile)) {
1959       if (wtile.water || wtile.lava) {
1960         // sanity check
1961         if (wtile.ix%16 == 0 && wtile.iy%16 == 0) {
1962           wtile.waterMoved = false;
1963           wtile.waterMovedDown = false;
1964           wtile.waterSlideOldX = wtile.ix;
1965           wtile.waterSlideOldY = wtile.iy;
1966           waterTilesList[$] = wtile;
1967         }
1968       }
1969     }
1970     checkWater = false;
1971     liquidTileCount = 0;
1972     waterTilesList.sort(&sortWaterTilesByCoordsLess);
1973     // do water flow
1974     bool wasAnyMove = false;
1975     bool wasAnyMoveDown = false;
1976     foreach (MapTile wtile; waterTilesList) {
1977       if (!wtile || !wtile.isInstanceAlive) continue;
1978       auto killIt = checkWaterFlow(wtile);
1979       if (killIt) {
1980         checkWater = true;
1981         wtile.smashMe();
1982         wtile.instanceRemove(); // just in case
1983       } else {
1984         wtile.saveInterpData();
1985         wtile.updateGrid();
1986         wasAnyMove = wasAnyMove || wtile.waterMoved;
1987         wasAnyMoveDown = wasAnyMoveDown || wtile.waterMovedDown;
1988         if (wtile.waterMoved && debugWaterFlowPause) wtile.waterSlideCounter = 4;
1989       }
1990     }
1991     // do water check
1992     liquidTileCount = 0;
1993     foreach (MapTile wtile; waterTilesList) {
1994       if (!wtile || !wtile.isInstanceAlive) continue;
1995       if (wasAnyMoveDown) {
1996         ++liquidTileCount;
1997         continue;
1998       }
1999       //checkWater = checkWater || wtile.waterMoved;
2000       curWaterTile = wtile;
2001       int tileX = wtile.ix/16, tileY = wtile.iy/16;
2002       // check if we are have no way to leak
2003       bool killIt = false;
2004       if (!isFullyOccupiedAtTilePos(tileX-1, tileY) || (wtile.water && curWaterTileCheckHitsLava)) {
2005         //writeln(" LEFT DEATH (", curWaterOccupiedCount, " : ", curWaterTileCheckHitsLava, ")");
2006         killIt = true;
2007       }
2008       if (!killIt && (!isFullyOccupiedAtTilePos(tileX+1, tileY) || (wtile.water && curWaterTileCheckHitsLava))) {
2009         //writeln(" RIGHT DEATH (", curWaterOccupiedCount, " : ", curWaterTileCheckHitsLava, ")");
2010         killIt = true;
2011       }
2012       if (!killIt && (!isFullyOccupiedAtTilePos(tileX, tileY+1) || (wtile.water && curWaterTileCheckHitsLava))) {
2013         //writeln(" DOWN DEATH (", curWaterOccupiedCount, " : ", curWaterTileCheckHitsLava, ")");
2014         killIt = true;
2015       }
2016       //killIt = false;
2017       if (killIt) {
2018         checkWater = true;
2019         wtile.smashMe();
2020         wtile.instanceRemove(); // just in case
2021       } else {
2022         ++liquidTileCount;
2023       }
2024     }
2025     if (wasAnyMove) checkWater = true;
2026     //writeln("water check: liquidTileCount=", liquidTileCount, "; checkWater=", checkWater, "; wasAnyMove=", wasAnyMove, "; wasAnyMoveDown=", wasAnyMoveDown);
2028     // fill empty spaces in lake with water
2029     fixLiquidTop();
2030   }
2032   cleanDeadObjects();
2036 // ////////////////////////////////////////////////////////////////////////// //
2037 private transient array!MapEntity postponedThinkers;
2038 private transient MapEntity thinkerHeld;
2039 private transient array!MapEntity activeThinkerList;
2042 final void doThinkActionsForObject (MapEntity o) {
2043        if (o.justSpawned) o.justSpawned = false;
2044   else if (o.imageSpeed > 0) o.nextAnimFrame();
2045   o.saveInterpData();
2046   o.thinkFrame();
2047   if (o.isInstanceAlive) {
2048     //o.updateGrid();
2049     o.processAlarms();
2050     if (o.isInstanceAlive) {
2051       if (o.whipTimer > 0) --o.whipTimer;
2052       o.updateGrid();
2053       auto obj = MapObject(o);
2054       if (!o.canLiveOutsideOfLevel && (!obj || !obj.heldBy) && o.isOutsideOfLevel()) {
2055         // oops, fallen out of level...
2056         o.onOutOfLevel();
2057       }
2058     }
2059   }
2063 // return `true` if thinker should be removed
2064 final void thinkOne (MapEntity o, optional bool doHeldObject, optional bool dontAddHeldObject) {
2065   if (!o) return;
2066   if (o == thinkerHeld && !doHeldObject) return; // skip it
2068   if (!o.isInstanceAlive) return;
2070   if (!o.active) return;
2072   auto obj = MapObject(o);
2074   if (obj && obj.heldBy == player) {
2075     // fix held item coords
2076     obj.fixHoldCoords();
2077     if (doHeldObject) {
2078       doThinkActionsForObject(o);
2079     } else {
2080       if (!dontAddHeldObject) {
2081         bool found = false;
2082         foreach (MapEntity e; postponedThinkers) if (e == o) { found = true; break; }
2083         if (!found) postponedThinkers[$] = o;
2084       }
2085     }
2086     return;
2087   }
2089   bool doThink = true;
2091   // collision with player weapon
2092   auto hh = PlayerWeapon(player.holdItem);
2093   bool doWeaponAction = false;
2094   if (hh) {
2095     if (hh.blockedBySolids && !global.config.killEnemiesThruWalls) {
2096       int xx = player.ix+(player.dir == MapObject::Dir.Left ? -16 : 16);
2097       //doWeaponAction = !isSolidAtPoint(xx, player.iy);
2098       doWeaponAction = !isSolidAtPoint(xx, hh.yCenter);
2099       /*
2100       int dh = max(1, hh.height-2);
2101       doWeaponAction = !checkTilesInRect(player.ix, player.iy);
2102       */
2103     } else {
2104       doWeaponAction = true;
2105     }
2106   }
2108   if (obj && doWeaponAction && hh && (o.whipTimer <= 0 || hh.ignoreWhipTimer) && hh.collidesWithObject(obj)) {
2109     //writeln("WEAPONED!");
2110     //writeln("weapon collides with '", GetClassName(o.Class), "' (", o.objType, "'");
2111     bool dontChangeWhipTimer = hh.dontChangeWhipTimer;
2112     if (!o.onTouchedByPlayerWeapon(player, hh)) {
2113       if (o.isInstanceAlive) hh.onCollisionWithObject(obj);
2114     }
2115     if (!dontChangeWhipTimer) o.whipTimer = o.whipTimerValue; //HACK
2116     doThink = o.isInstanceAlive;
2117   }
2119   if (doThink && o.isInstanceAlive) {
2120     doThinkActionsForObject(o);
2121     doThink = o.isInstanceAlive;
2122   }
2124   // collision with player
2125   if (doThink && obj && o.collidesWith(player)) {
2126     if (!player.onObjectTouched(obj) && o.isInstanceAlive) {
2127       doThink = !o.onTouchedByPlayer(player);
2128       o.updateGrid();
2129     }
2130   }
2134 final void processThinkers (float timeDelta) {
2135   if (timeDelta <= 0) return;
2136   if (gamePaused) {
2137     ++pausedTime;
2138     if (onBeforeFrame) onBeforeFrame(false);
2139     if (onAfterFrame) onAfterFrame(false);
2140     keysNextFrame();
2141     return;
2142   } else {
2143     pausedTime = 0;
2144   }
2145   accumTime += timeDelta;
2146   bool wasFrame = false;
2147   // block GC
2148   auto olddel = ImmediateDelete;
2149   ImmediateDelete = false;
2150   while (accumTime >= FrameTime) {
2151     bool solidObjectSeen = false;
2152     postponedThinkers.clear();
2153     thinkerHeld = none;
2154     accumTime -= FrameTime;
2155     if (onBeforeFrame) onBeforeFrame(accumTime >= FrameTime);
2156     // shake
2157     if (shakeLeft > 0) {
2158       --shakeLeft;
2159       if (!shakeDir.x) shakeDir.x = (global.randOther(0, 1) ? -3 : 3);
2160       if (!shakeDir.y) shakeDir.y = (global.randOther(0, 1) ? -3 : 3);
2161       shakeOfs.x = shakeDir.x;
2162       shakeOfs.y = shakeDir.y;
2163       int sgnc = global.randOther(1, 3);
2164       if (sgnc&0x01) shakeDir.x = -shakeDir.x;
2165       if (sgnc&0x02) shakeDir.y = -shakeDir.y;
2166     } else {
2167       shakeOfs.x = 0;
2168       shakeOfs.y = 0;
2169       shakeDir.x = 0;
2170       shakeDir.y = 0;
2171     }
2172     // advance time
2173     time += 1;
2174     // we don't want the time to grow too large
2175     if (time < 0) { time = 0; lastRenderTime = -1; }
2176     // game-global events
2177     thinkFrameGame();
2178     // frame thinkers: player
2179     if (player && !disablePlayerThink) {
2180       // time limit
2181       if (!player.dead && isNormalLevel() &&
2182           (maxPlayingTime < 0 ||
2183            (maxPlayingTime > 0 && maxPlayingTime*30 <= time &&
2184             time%30 == 0 && global.randOther(1, 100) <= 20)))
2185       {
2186         global.hasAnkh = false;
2187         global.plife = 1;
2188         player.invincible = 0;
2189         auto xplo = MapObjExplosion(MakeMapObject(player.ix, player.iy, 'oExplosion'));
2190         if (xplo) xplo.suicide = true;
2191       }
2192       //HACK: check for stolen items
2193       auto item = MapItem(player.holdItem);
2194       if (item) item.onCheckItemStolen(player);
2195       item = MapItem(player.pickedItem);
2196       if (item) item.onCheckItemStolen(player);
2197       // normal thinking
2198       doThinkActionsForObject(player);
2199     }
2200     // frame thinkers: held object
2201     thinkerHeld = player.holdItem;
2202     if (thinkerHeld && thinkerHeld.isInstanceAlive) {
2203       if (thinkerHeld.active) {
2204         thinkOne(thinkerHeld, doHeldObject:true);
2205         if (!thinkerHeld.isInstanceAlive) {
2206           if (player.holdItem == thinkerHeld) player.holdItem = none;
2207           thinkerHeld.grid.remove(thinkerHeld.gridId);
2208         }
2209       } else {
2210         //HACK!
2211         auto item = MapItem(thinkerHeld);
2212         if (item) {
2213           if (item.forSale || item.sellOfferDone) {
2214             if (++item.forSaleFrame < 0) item.forSaleFrame = 0;
2215           }
2216         }
2217       }
2218     }
2219     // frame thinkers: objects
2220     activeThinkerList.clear();
2221     auto grid = objGrid;
2222     // collect active objects
2223     if (global.config.useFrozenRegion) {
2224       foreach (MapEntity e; grid.inRectPix(viewStart.x/global.scale-64, viewStart.y/global.scale-64, 320+64*2, 240+64*2, precise:false)) {
2225         if (e.active) activeThinkerList[$] = e;
2226       }
2227     } else {
2228       // no frozen area
2229       /*
2230       foreach (MapEntity e; grid.allObjects()) {
2231         if (e.active) activeThinkerList[$] = e;
2232       }
2233       */
2234       grid.collectAllActiveObjects(activeThinkerList);
2235     }
2236     // process active objects
2237     //writeln("thinkers: ", activeThinkerList.length);
2238     foreach (MapEntity o; activeThinkerList) {
2239       if (!o) continue;
2240       if (o.active) {
2241         thinkOne(o, doHeldObject:false);
2242         if (!o.isInstanceAlive) {
2243           if (o.grid) o.grid.remove(o.gridId);
2244           auto obj = MapObject(o);
2245           if (obj && obj.heldBy) obj.heldBy.holdItem = none;
2246         } else if (!solidObjectSeen && o.walkableSolid) {
2247           solidObjectSeen = true;
2248           hasSolidObjects = true;
2249         }
2250       }
2251     }
2252     // postponed thinkers
2253     foreach (MapEntity o; postponedThinkers) {
2254       if (!o) continue;
2255       if (o.active) thinkOne(o, doHeldObject:true, dontAddHeldObject:true);
2256       if (!o.isInstanceAlive) {
2257         if (o.grid) o.grid.remove(o.gridId);
2258         auto obj = MapObject(o);
2259         if (obj && obj.heldBy) obj.heldBy.holdItem = none;
2260       } else if (!solidObjectSeen && o.walkableSolid) {
2261         solidObjectSeen = true;
2262         hasSolidObjects = true;
2263       }
2264     }
2265     postponedThinkers.clear();
2266     thinkerHeld = none;
2267     // clean dead things
2268     cleanDeadTiles();
2269     hasSolidObjects = !!solidObjectSeen;
2270     // fix held item coords
2271     if (player && player.holdItem) {
2272       if (player.holdItem.isInstanceAlive) {
2273         player.holdItem.fixHoldCoords();
2274       } else {
2275         player.holdItem = none;
2276       }
2277     }
2278     // money counter
2279     if (collectCounter == 0) {
2280       xmoney = max(0, xmoney-100);
2281     } else {
2282       --collectCounter;
2283     }
2284     // other things
2285     if (player) {
2286       if (!player.dead) stats.oneMoreFramePlayed();
2287       SoundSystem.ListenerOrigin = vector(player.xCenter, player.yCenter, -1);
2288       //writeln("plrpos=(", player.xCenter, ",", player.yCenter, "); lo=", SoundSystem.ListenerOrigin);
2289     }
2290     if (onAfterFrame) onAfterFrame(accumTime >= FrameTime);
2291     ++framesProcessedFromLastClear;
2292     keysNextFrame();
2293     wasFrame = true;
2294     if (!player.visible && player.holdItem) player.holdItem.visible = false;
2295     if (winCutsceneSwitchToNext) {
2296       winCutsceneSwitchToNext = false;
2297       switch (++inWinCutscene) {
2298         case 2: startWinCutsceneVolcano(); break;
2299         case 3: default: startWinCutsceneWinFall(); break;
2300       }
2301       break;
2302     }
2303     if (playerExited) break;
2304   }
2305   ImmediateDelete = olddel;
2306   if (playerExited) {
2307     playerExited = false;
2308     onLevelExited();
2309     centerViewAtPlayer();
2310   }
2311   if (wasFrame) {
2312     // if we were processed at least one frame, collect garbage
2313     //keysNextFrame();
2314     CollectGarbage(true); // destroy delayed objects too
2315   }
2316   if (accumTime > 0 && onInterFrame) onInterFrame(accumTime/FrameTime);
2320 // ////////////////////////////////////////////////////////////////////////// //
2321 final void tileXY2roomXY (int tileX, int tileY, out int roomX, out int roomY) {
2322   roomX = (tileX-1)/RoomGen::Width;
2323   roomY = (tileY-1)/RoomGen::Height;
2327 final bool isInShop (int tileX, int tileY) {
2328   if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
2329     auto n = roomType[tileX, tileY];
2330     if (n == 4 || n == 5) return true;
2331     return !!checkTilesInRect(tileX*16, tileY*16, 16, 16, delegate bool (MapTile t) { return t.shopWall; });
2332     //k8: we don't have this
2333     //if (t && t.objType == 'oShop') return true;
2334   }
2335   return false;
2339 // ////////////////////////////////////////////////////////////////////////// //
2340 override void Destroy () {
2341   clearWholeLevel();
2342   delete tempSolidTile;
2343   ::Destroy();
2347 // ////////////////////////////////////////////////////////////////////////// //
2348 // WARNING! delegate should not create/delete objects!
2349 final MapObject findNearestObject (int px, int py, scope bool delegate (MapObject o) dg, optional class!MapObject castClass) {
2350   MapObject res = none;
2351   if (!castClass) castClass = MapObject;
2352   int curdistsq = int.max;
2353   foreach (MapObject o; objGrid.allObjects(MapObject)) {
2354     if (o.spectral) continue;
2355     if (!dg(o)) continue;
2356     int xc = px-o.xCenter, yc = py-o.yCenter;
2357     int distsq = xc*xc+yc*yc;
2358     if (distsq < curdistsq) {
2359       res = o;
2360       curdistsq = distsq;
2361     }
2362   }
2363   return res;
2367 // WARNING! delegate should not create/delete objects!
2368 final MapObject findNearestEnemy (int px, int py, optional scope bool delegate (MapEnemy o) dg, optional class!MapEnemy castClass) {
2369   if (!castClass) castClass = MapEnemy;
2370   if (castClass !isa MapEnemy) return none;
2371   MapObject res = none;
2372   int curdistsq = int.max;
2373   foreach (MapEnemy o; objGrid.allObjects(castClass)) {
2374     //k8: i added `dead` check
2375     if (o.spectral || o.dead) continue;
2376     if (dg) {
2377       if (!dg(o)) continue;
2378     }
2379     int xc = px-o.xCenter, yc = py-o.yCenter;
2380     int distsq = xc*xc+yc*yc;
2381     if (distsq < curdistsq) {
2382       res = o;
2383       curdistsq = distsq;
2384     }
2385   }
2386   return res;
2390 final MonsterShopkeeper findNearestCalmShopkeeper (int px, int py) {
2391   auto obj = MonsterShopkeeper(findNearestEnemy(px, py, delegate bool (MapEnemy o) {
2392     auto sk = MonsterShopkeeper(o);
2393     if (sk && !sk.angered) return true;
2394     return false;
2395   }, castClass:MonsterShopkeeper));
2396   return obj;
2400 final MonsterShopkeeper hasAliveShopkeepers (optional bool skipAngry) {
2401   foreach (MonsterShopkeeper sc; objGrid.allObjects(MonsterShopkeeper)) {
2402     if (sc.spectral || sc.dead) continue;
2403     if (skipAngry && (sc.angered || sc.outlaw)) continue;
2404     return sc;
2405   }
2406   return none;
2410 // WARNING! delegate should not create/delete objects!
2411 final int calcNearestEnemyDist (int px, int py, optional scope bool delegate (MapEnemy o) dg, optional class!MapEnemy castClass) {
2412   auto e = findNearestEnemy(px, py, dg!optional, castClass!optional);
2413   if (!e) return int.max;
2414   int xc = px-e.xCenter, yc = py-e.yCenter;
2415   return round(sqrt(xc*xc+yc*yc));
2419 // WARNING! delegate should not create/delete objects!
2420 final int calcNearestObjectDist (int px, int py, optional scope bool delegate (MapObject o) dg, optional class!MapObject castClass) {
2421   auto e = findNearestObject(px, py, dg!optional, castClass!optional);
2422   if (!e) return int.max;
2423   int xc = px-e.xCenter, yc = py-e.yCenter;
2424   return round(sqrt(xc*xc+yc*yc));
2428 // WARNING! delegate should not create/delete objects!
2429 final MapTile findNearestMoveableSolid (int px, int py, optional scope bool delegate (MapTile t) dg) {
2430   MapTile res = none;
2431   int curdistsq = int.max;
2432   foreach (MapTile t; objGrid.allObjects(MapTile)) {
2433     if (t.spectral) continue;
2434     if (dg) {
2435       if (!dg(t)) continue;
2436     } else {
2437       if (!t.solid || !t.moveable) continue;
2438     }
2439     int xc = px-t.xCenter, yc = py-t.yCenter;
2440     int distsq = xc*xc+yc*yc;
2441     if (distsq < curdistsq) {
2442       res = t;
2443       curdistsq = distsq;
2444     }
2445   }
2446   return res;
2450 // WARNING! delegate should not create/delete objects!
2451 final MapTile findNearestTile (int px, int py, optional scope bool delegate (MapTile t) dg) {
2452   if (!dg) return none;
2453   MapTile res = none;
2454   int curdistsq = int.max;
2456   //FIXME: make this faster!
2457   foreach (MapTile t; objGrid.allObjects(MapTile)) {
2458     if (t.spectral) continue;
2459     int xc = px-t.xCenter, yc = py-t.yCenter;
2460     int distsq = xc*xc+yc*yc;
2461     if (distsq < curdistsq && dg(t)) {
2462       res = t;
2463       curdistsq = distsq;
2464     }
2465   }
2467   return res;
2471 // ////////////////////////////////////////////////////////////////////////// //
2472 final bool cbIsObjectWeb (MapObject o) { return (o isa ItemWeb); }
2473 final bool cbIsObjectBlob (MapObject o) { return (o isa EnemyBlob); }
2474 final bool cbIsObjectArrow (MapObject o) { return (o isa ItemProjectileArrow); }
2475 final bool cbIsObjectEnemy (MapObject o) { return (/*o != self &&*/ o isa MapEnemy); }
2477 final bool cbIsObjectTreasure (MapObject o) { return (o.isTreasure); }
2479 final bool cbIsItemObject (MapObject o) { return (o isa MapItem); }
2481 final bool cbIsItemOrEnemyObject (MapObject o) { return (o isa MapItem || o isa MapEnemy); }
2484 final MapObject isObjectAtTile (int tileX, int tileY, optional scope bool delegate (MapObject o) dg, optional bool precise) {
2485   if (!specified_precise) precise = true;
2486   tileX *= 16;
2487   tileY *= 16;
2488   foreach (MapObject o; objGrid.inRectPix(tileX, tileY, 16, 16, precise:precise, castClass:MapObject)) {
2489     if (o.spectral) continue;
2490     if (dg) {
2491       if (dg(o)) return o;
2492     } else {
2493       return o;
2494     }
2495   }
2496   return none;
2500 final MapObject isObjectAtTilePix (int x, int y, optional scope bool delegate (MapObject o) dg) {
2501   return isObjectAtTile(x/16, y/16, dg!optional);
2505 final MapObject isObjectAtPoint (int xpos, int ypos, optional scope bool delegate (MapObject o) dg, optional bool precise, optional class!MapObject castClass) {
2506   if (!specified_precise) precise = true;
2507   if (!castClass) castClass = MapObject;
2508   foreach (MapObject o; objGrid.inCellPix(xpos, ypos, precise:precise, castClass:castClass)) {
2509     if (o.spectral) continue;
2510     if (dg) {
2511       if (dg(o)) return o;
2512     } else {
2513       if (o isa MapEnemy) return o;
2514     }
2515   }
2516   return none;
2520 final MapObject isObjectInRect (int xpos, int ypos, int w, int h, optional scope bool delegate (MapObject o) dg, optional bool precise, optional class!MapObject castClass) {
2521   if (w < 1 || h < 1) return none;
2522   if (!castClass) castClass = MapObject;
2523   if (w == 1 && h == 1) return isObjectAtPoint(xpos, ypos, dg!optional, precise!optional, castClass);
2524   if (!specified_precise) precise = true;
2525   foreach (MapObject o; objGrid.inRectPix(xpos, ypos, w, h, precise:precise, castClass:castClass)) {
2526     if (o.spectral) continue;
2527     if (dg) {
2528       if (dg(o)) return o;
2529     } else {
2530       if (o isa MapEnemy) return o;
2531     }
2532   }
2533   return none;
2537 final MapObject forEachObject (scope bool delegate (MapObject o) dg, optional bool allowSpectrals, optional class!MapObject castClass) {
2538   if (!dg) return none;
2539   if (!castClass) castClass = MapObject;
2540   foreach (MapObject o; objGrid.allObjectsSafe(castClass)) {
2541     if (!allowSpectrals && o.spectral) continue;
2542     if (dg(o)) return o;
2543   }
2544   return none;
2548 final MapObject forEachObjectAtPoint (int xpos, int ypos, scope bool delegate (MapObject o) dg, optional bool precise) {
2549   if (!dg) return none;
2550   if (!specified_precise) precise = true;
2551   foreach (MapObject o; objGrid.inCellPix(xpos, ypos, precise:precise, castClass:MapObject)) {
2552     if (o.spectral) continue;
2553     if (dg(o)) return o;
2554   }
2555   return none;
2559 final MapObject forEachObjectInRect (int xpos, int ypos, int w, int h, scope bool delegate (MapObject o) dg, optional bool precise) {
2560   if (!dg || w < 1 || h < 1) return none;
2561   if (w == 1 && h == 1) return forEachObjectAtPoint(xpos, ypos, dg!optional, precise!optional);
2562   if (!specified_precise) precise = true;
2563   foreach (MapObject o; objGrid.inRectPix(xpos, ypos, w, h, precise:precise, castClass:MapObject)) {
2564     if (o.spectral) continue;
2565     if (dg(o)) return o;
2566   }
2567   return none;
2571 final MapEntity forEachEntityInRect (int xpos, int ypos, int w, int h, scope bool delegate (MapEntity o) dg, optional bool precise, optional class!MapEntity castClass) {
2572   if (!dg || w < 1 || h < 1) return none;
2573   if (!castClass) castClass = MapEntity;
2574   if (!specified_precise) precise = true;
2575   foreach (MapEntity e; objGrid.inRectPix(xpos, ypos, w, h, precise:precise, castClass:castClass)) {
2576     if (e.spectral) continue;
2577     if (dg(e)) return e;
2578   }
2579   return none;
2583 private final bool cbIsRopeTile (MapTile t) { return (t isa MapTileRope); }
2585 final MapTile isRopeAtPoint (int px, int py) {
2586   return checkTileAtPoint(px, py, &cbIsRopeTile);
2590 //FIXME!
2591 final MapTile isWaterSwimAtPoint (int px, int py) {
2592   return isWaterAtPoint(px, py);
2596 // ////////////////////////////////////////////////////////////////////////// //
2597 private array!MapEntity tmpEntityList;
2599 private final bool cbCollectEntitiesWithMask (MapEntity t) {
2600   if (!t.visible || t.spectral) return false;
2601   tmpEntityList[$] = t;
2602   return false;
2606 final void touchEntitiesWithMask (int x, int y, SpriteFrame frm, scope bool delegate (MapEntity t) dg, optional class!MapEntity castClass) {
2607   if (!frm || frm.bw < 1 || frm.bh < 1 || !dg) return;
2608   if (frm.isEmptyPixelMask) return;
2609   if (!castClass) castClass = MapEntity;
2610   // collect tiles
2611   if (tmpEntityList.length) tmpEntityList.clear();
2612   if (player isa castClass && player.isRectCollisionFrame(frm, x, y)) tmpEntityList[$] = player;
2613   forEachEntityInRect(x+frm.bx, y+frm.by, frm.bw, frm.bh, &cbCollectEntitiesWithMask, castClass:castClass);
2614   foreach (MapEntity e; tmpEntityList) {
2615     if (!e || !e.isInstanceAlive || !e.visible || e.spectral) continue;
2616     if (e.isRectCollisionFrame(frm, x, y)) {
2617       if (dg(e)) break;
2618     }
2619   }
2623 // ////////////////////////////////////////////////////////////////////////// //
2624 final bool cbCollisionAnySolid (MapTile t) { return t.solid; }
2625 final bool cbCollisionAnyMoveableSolid (MapTile t) { return t.solid && t.moveable; }
2626 final bool cbCollisionLiquid (MapTile t) { return (t.water || t.lava); }
2627 final bool cbCollisionAnySolidOrLiquid (MapTile t) { return (t.solid || t.water || t.lava); }
2628 final bool cbCollisionLadder (MapTile t) { return t.ladder; }
2629 final bool cbCollisionLadderTop (MapTile t) { return t.laddertop; }
2630 final bool cbCollisionAnyLadder (MapTile t) { return (t.ladder || t.laddertop); }
2631 final bool cbCollisionAnySolidIce (MapTile t) { return (t.solid && t.ice); }
2632 final bool cbCollisionWater (MapTile t) { return t.water; }
2633 final bool cbCollisionLava (MapTile t) { return t.lava; }
2634 final bool cbCollisionAnyTree (MapTile t) { return t.tree; }
2635 final bool cbCollisionPlatform (MapTile t) { return t.platform; }
2636 final bool cbCollisionLadderOrPlatform (MapTile t) { return (t.ladder || t.platform); }
2637 //final bool cbCollisionAnyHangeable (MapTile t) { return t.hangeable; } // no need to check for solids here, it is done in MapTile initializer
2638 final bool cbCollisionSpringTrap (MapTile t) { return t.springtrap; }
2639 final bool cbCollisionSpikes (MapTile t) { return t.spikes; }
2640 final bool cbCollisionExitTile (MapTile t) { return t.isExitActive(); }
2642 final bool cbCollisionForWhoa (MapTile t) { return (t.solid || t.laddertop); }
2644 //final bool cbCollisionSacAltar (MapTile t) { return (t.objName == 'oSacAltarLeft'); } // only left part, yeah
2645 final bool cbCollisionSacAltar (MapTile t) { return t.sacrificingAltar; }
2648 // ////////////////////////////////////////////////////////////////////////// //
2649 transient MapTileTemp tempSolidTile;
2651 private final MapTileTemp makeWalkeableSolidTile (MapObject o) {
2652   if (!tempSolidTile) {
2653     tempSolidTile = SpawnObject(MapTileTemp);
2654   } else if (!tempSolidTile.isInstanceAlive) {
2655     delete tempSolidTile;
2656     tempSolidTile = SpawnObject(MapTileTemp);
2657   }
2658   // setup data
2659   tempSolidTile.level = self;
2660   tempSolidTile.global = global;
2661   tempSolidTile.solid = true;
2662   tempSolidTile.objName = MapTileTemp.default.objName;
2663   tempSolidTile.objType = MapTileTemp.default.objType;
2664   tempSolidTile.e = o;
2665   tempSolidTile.fltx = o.fltx;
2666   tempSolidTile.flty = o.flty;
2667   return tempSolidTile;
2671 final MapTile checkTilesInRect (int x0, int y0, const int w, const int h,
2672                                 optional scope bool delegate (MapTile dg) dg, optional bool precise,
2673                                 optional class!MapTile castClass)
2675   if (w < 1 || h < 1) return none;
2676   if (w == 1 && h == 1) return checkTileAtPoint(x0, y0, dg!optional);
2677   int x1 = x0+w-1, y1 = y0+h-1;
2678   if (x0 >= tilesWidth*16 || y0 >= tilesHeight*16 || x1 < 0 || y1 < 0) return none;
2679   if (!specified_precise) precise = true;
2680   if (!castClass) castClass = MapTile;
2681   if (castClass !isa MapTile) return none;
2682   if (!dg) dg = &cbCollisionAnySolid;
2684   if (hasSolidObjects) {
2685     // check walkable solid objects too
2686     foreach (MapEntity e; objGrid.inRectPix(x0, y0, w, h, precise:precise/*, castClass:castClass*/)) {
2687       if (e.spectral || !e.visible) continue;
2688       auto t = MapTile(e);
2689       if (t) {
2690         if (t isa castClass && dg(t)) return t;
2691         continue;
2692       }
2693       auto o = MapObject(e);
2694       if (o && o.walkableSolid) {
2695         t = makeWalkeableSolidTile(o);
2696         if (t isa castClass && dg(t)) return t;
2697         continue;
2698       }
2699     }
2700   } else {
2701     // no walkeable solid MapObjects, speed it up
2702     foreach (MapTile t; objGrid.inRectPix(x0, y0, w, h, precise:precise, castClass:castClass)) {
2703       if (t.spectral || !t.visible) continue;
2704       if (dg(t)) return t;
2705     }
2706   }
2708   return none;
2712 final MapTile checkTileAtPoint (int x0, int y0, optional scope bool delegate (MapTile dg) dg, optional bool precise, optional class!MapTile castClass) {
2713   if (x0 < 0 || y0 < 0 || x0 >= tilesWidth*16 || y0 >= tilesHeight*16) return none;
2714   if (!specified_precise) precise = true;
2715   if (!castClass) castClass = MapTile;
2716   if (castClass !isa MapTile) return none;
2717   if (!dg) dg = &cbCollisionAnySolid;
2719   if (hasSolidObjects) {
2720     // check walkable solid objects
2721     foreach (MapEntity e; objGrid.inCellPix(x0, y0, precise:precise/*, castClass:castClass*/)) {
2722       if (e.spectral || !e.visible) continue;
2723       auto t = MapTile(e);
2724       if (t) {
2725         if (t isa castClass && dg(t)) return t;
2726         continue;
2727       }
2728       auto o = MapObject(e);
2729       if (o && o.walkableSolid) {
2730         t = makeWalkeableSolidTile(o);
2731         if (t isa castClass && dg(t)) return t;
2732         continue;
2733       }
2734     }
2735   } else {
2736     //writeln("NOWS!");
2737     // no walkeable solid MapObjects, speed it up
2738     foreach (MapTile t; objGrid.inCellPix(x0, y0, precise:precise, castClass:castClass)) {
2739       if (t.spectral || !t.visible) continue;
2740       if (dg(t)) return t;
2741     }
2742   }
2744   return none;
2748 // ////////////////////////////////////////////////////////////////////////// //
2749 final MapTile isSolidAtPoint (int px, int py) { return checkTileAtPoint(px, py); }
2750 final MapTile isSolidInRect (int rx, int ry, int rw, int rh) { return checkTilesInRect(rx, ry, rw, rh); }
2751 final MapTile isLadderAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadder); }
2752 final MapTile isLadderOrPlatformAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadderOrPlatform); }
2753 final MapTile isLadderTopAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadderTop); }
2754 final MapTile isAnyLadderAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyLadder); }
2755 final MapTile isWaterAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionWater); }
2756 final MapTile isLavaAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLava); }
2757 final MapTile isLiquidAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLiquid); }
2758 final MapTile isIceAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnySolidIce); }
2759 final MapTile isTreeAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyTree); }
2760 //final MapTile isHangeableAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyHangeable); }
2763 // ////////////////////////////////////////////////////////////////////////// //
2764 final void setTileTypeAt (int tileX, int tileY, LevelGen::RType rt) {
2765   if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) roomType[tileX, tileY] = rt;
2769 //FIXME: make this faster
2770 transient float gtagX, gtagY;
2772 // only non-moveables and non-specials
2773 final MapTile getTileAtGrid (int tileX, int tileY) {
2774   gtagX = tileX*16;
2775   gtagY = tileY*16;
2776   return checkTileAtPoint(tileX*16, tileY*16, delegate bool (MapTile t) {
2777     if (t.spectral || t.moveable || t.toSpecialGrid || !t.visible) return false;
2778     if (t.fltx != gtagX || t.flty != gtagY) return false; // emulate grid
2779     if (t.width != 16 || t.height != 16) return false;
2780     return true;
2781   }, precise:false);
2782   //return (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight ? tiles[tileX, tileY] : none);
2786 final MapTile getTileAtGridAny (int tileX, int tileY) {
2787   gtagX = tileX*16;
2788   gtagY = tileY*16;
2789   return checkTileAtPoint(tileX*16, tileY*16, delegate bool (MapTile t) {
2790     if (t.spectral /*|| t.moveable*/ || !t.visible) return false;
2791     if (t.fltx != gtagX || t.flty != gtagY) return false; // emulate grid
2792     if (t.width != 16 || t.height != 16) return false;
2793     return true;
2794   }, precise:false);
2795   //return (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight ? tiles[tileX, tileY] : none);
2799 final bool isNamedTileAt (int tileX, int tileY, name atypename) {
2800   if (!atypename) return false;
2801   auto t = getTileAtGridAny(tileX, tileY);
2802   return (t && t.objName == atypename);
2806 final void setTileAtGrid (int tileX, int tileY, MapTile tile) {
2807   if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
2808     if (tile) {
2809       tile.fltx = tileX*16;
2810       tile.flty = tileY*16;
2811       if (!tile.dontReplaceOthers) {
2812         auto osp = tile.spectral;
2813         tile.spectral = true;
2814         auto t = getTileAtGridAny(tileX, tileY);
2815         tile.spectral = osp;
2816         if (t && !t.immuneToReplacement) {
2817           writeln("REPLACING tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
2818           writeln("      NEW tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (tile.toSpecialGrid ? "special " : ""), "grid, pos=(", tile.fltx/16.0, ",", tile.flty/16.0, ")");
2819           t.instanceRemove();
2820         }
2821       }
2822       insertObject(tile);
2823     } else {
2824       auto t = getTileAtGridAny(tileX, tileY);
2825       if (t && !t.immuneToReplacement) {
2826         writeln("REMOVING tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
2827         t.instanceRemove();
2828       }
2829     }
2830   }
2834 // ////////////////////////////////////////////////////////////////////////// //
2835 // return `true` from delegate to stop
2836 MapTile forEachSolidTileOnGrid (scope bool delegate (int tileX, int tileY, MapTile t) dg) {
2837   if (!dg) return none;
2838   foreach (MapTile t; objGrid.allObjectsSafe(MapTile)) {
2839     if (t.spectral || !t.solid || !t.visible) continue;
2840     if (t.ix%16 != 0 || t.iy%16 != 0) continue; // emulate grid
2841     if (t.width != 16 || t.height != 16) continue;
2842     if (dg(t.ix/16, t.iy/16, t)) return t;
2843   }
2844   return none;
2848 // ////////////////////////////////////////////////////////////////////////// //
2849 // return `true` from delegate to stop
2850 MapTile forEachTile (scope bool delegate (MapTile t) dg, optional class!MapTile castClass) {
2851   if (!dg) return none;
2852   if (!castClass) castClass = MapTile;
2853   foreach (MapTile t; objGrid.allObjectsSafe(castClass)) {
2854     if (t.spectral || !t.visible) continue;
2855     if (dg(t)) return t;
2856   }
2857   return none;
2861 // ////////////////////////////////////////////////////////////////////////// //
2862 final void fixWallTiles () {
2863   foreach (MapTile t; objGrid.allObjectsSafe(MapTile)) {
2864     //writeln("beautify: '", GetClassName(t.Class), "' (", t.objType, "' (name:", t.objName, ")");
2865     t.beautifyTile();
2866   }
2870 // ////////////////////////////////////////////////////////////////////////// //
2871 final MapTile isCollisionAtPoint (int px, int py, optional scope bool delegate (MapTile dg) dg) {
2872   if (!dg) dg = &cbCollisionAnySolid;
2873   return checkTilesInRect(px, py, 1, 1, dg);
2877 // ////////////////////////////////////////////////////////////////////////// //
2878 string scrGetKaliGift (MapTile altar, optional name gift) {
2879   string res;
2881   // find other side of the altar
2882   int sx = player.ix, sy = player.iy;
2883   if (altar) {
2884     sx = altar.ix;
2885     sy = altar.iy;
2886     auto a2 = isCollisionAtPoint(altar.ix+18, altar.iy+8, &cbCollisionSacAltar);
2887     if (!a2) a2 = isCollisionAtPoint(altar.ix-8, altar.iy+8, &cbCollisionSacAltar);
2888     if (a2) { sx = a2.ix; sy = a2.iy; }
2889   }
2891        if (global.favor <= -8) res = "SHE SEEMS VERY ANGRY WITH YOU!";
2892   else if (global.favor < 0) res = "SHE SEEMS ANGRY WITH YOU.";
2893   else if (global.favor == 0) res = "SHE HAS FORGIVEN YOU!";
2894   else if (global.favor >= 32) {
2895     if (global.kaliGift >= 3 && global.favor >= 32+(global.kaliGift-2)*16) {
2896       res = "YOU FEEL INVIGORATED!";
2897       global.kaliGift += 1;
2898       global.plife += global.randOther(4, 8);
2899     } else if (global.kaliGift >= 3) {
2900       res = "SHE SEEMS ECSTATIC WITH YOU!";
2901     } else if (global.bombs < 80) {
2902       res = "YOUR SATCHEL FEELS VERY FULL NOW!";
2903       global.kaliGift = 3;
2904       global.bombs = 99;
2905     } else {
2906       res = "YOU FEEL INVIGORATED!";
2907       global.kaliGift += 1;
2908       global.plife += global.randOther(4, 8);
2909     }
2910   } else if (global.favor >= 16) {
2911     if (global.kaliGift >= 2) {
2912       res = "SHE SEEMS VERY HAPPY WITH YOU!";
2913     } else {
2914       res = "SHE BESTOWS A GIFT UPON YOU!";
2915       global.kaliGift = 2;
2916       // poofs
2917       auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2918       obj.xVel = -1;
2919       obj.yVel = 0;
2920       obj = MakeMapObject(sx, sy-8, 'oPoof');
2921       obj.xVel = 1;
2922       obj.yVel = 0;
2923       // a gift
2924       obj = none;
2925       if (gift) obj = MakeMapObject(sx, sy-8, gift);
2926       if (!obj) obj = MakeMapObject(sx, sy-8, 'oKapala');
2927     }
2928   } else if (global.favor >= 8) {
2929     if (global.kaliGift >= 1) {
2930       res = "SHE SEEMS HAPPY WITH YOU.";
2931     } else {
2932       res = "SHE BESTOWS A GIFT UPON YOU!";
2933       global.kaliGift = 1;
2934       //rAltar = instance_nearest(x, y, oSacAltarRight);
2935       //if (instance_exists(rAltar)) {
2936       // poofs
2937       auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2938       obj.xVel = -1;
2939       obj.yVel = 0;
2940       obj = MakeMapObject(sx, sy-8, 'oPoof');
2941       obj.xVel = 1;
2942       obj.yVel = 0;
2943       obj = none;
2944       if (gift) obj = MakeMapObject(sx, sy-8, gift);
2945       if (!obj) {
2946         auto n = global.randOther(1, 8);
2947         auto m = n;
2948         for (;;) {
2949           name aname = '';
2950                if (n == 1 && !global.hasCape && !global.hasJetpack) aname = 'oCapePickup';
2951           else if (n == 2 && !global.hasGloves) aname = 'oGloves';
2952           else if (n == 3 && !global.hasSpectacles) aname = 'oSpectacles';
2953           else if (n == 4 && !global.hasMitt) aname = 'oMitt';
2954           else if (n == 5 && !global.hasSpringShoes) aname = 'oSpringShoes';
2955           else if (n == 6 && !global.hasSpikeShoes) aname = 'oSpikeShoes';
2956           else if (n == 7 && !global.hasStickyBombs) aname = 'oPaste';
2957           else if (n == 8 && !global.hasCompass) aname = 'oCompass';
2958           if (aname) {
2959             obj = MakeMapObject(sx, sy-8, aname);
2960             if (obj) break;
2961           }
2962           ++n;
2963           if (n > 8) n = 1;
2964           if (n == m) {
2965             obj = MakeMapObject(sx, sy-8, (global.hasJetpack ? 'oBombBox' : 'oJetpack'));
2966             break;
2967           }
2968         }
2969       }
2970     }
2971   } else if (global.favor > 0) {
2972     res = "SHE SEEMS PLEASED WITH YOU.";
2973   }
2975   /*
2976   if (argument1) {
2977     global.message = "";
2978     res = "KALI DEVOURS YOU!"; // sacrifice is player
2979   }
2980   */
2982   return res;
2986 void performSacrifice (MapObject what, MapTile where) {
2987   if (!what || !what.isInstanceAlive) return;
2988   MakeMapObject(what.ix+8, what.iy+8, 'oFlame');
2989   if (where) where.playSound('sndSmallExplode'); else what.playSound('sndSmallExplode');
2990   what.spillBlood(amount:3, forced:true);
2992   string msg = "KALI ACCEPTS THE SACRIFICE!";
2994   auto idol = ItemGoldIdol(what);
2995   if (idol) {
2996     ++stats.totalSacrifices;
2997          if (global.favor <= -8) msg = "SHE SEEMS VERY ANGRY WITH YOU!";
2998     else if (global.favor < 0) msg = "SHE SEEMS ANGRY WITH YOU.";
2999     else if (global.favor >= 0) {
3000       // find other side of the altar
3001       int sx = player.ix, sy = player.iy;
3002       auto altar = where;
3003       if (altar) {
3004         sx = altar.ix;
3005         sy = altar.iy;
3006         auto a2 = isCollisionAtPoint(altar.ix+18, altar.iy+8, &cbCollisionSacAltar);
3007         if (!a2) a2 = isCollisionAtPoint(altar.ix-8, altar.iy+8, &cbCollisionSacAltar);
3008         if (a2) { sx = a2.ix; sy = a2.iy; }
3009       }
3010       // poofs
3011       auto obj = MakeMapObject(sx, sy-8, 'oPoof');
3012       obj.xVel = -1;
3013       obj.yVel = 0;
3014       obj = MakeMapObject(sx, sy-8, 'oPoof');
3015       obj.xVel = 1;
3016       obj.yVel = 0;
3017       // a gift
3018       obj = MakeMapObject(sx, sy-16-8, 'oMonkeyGold');
3019     }
3020     osdMessage(msg, 6.66);
3021     scrShake(10);
3022     idol.instanceRemove();
3023     return;
3024   }
3026   if (global.favor <= -8) {
3027     msg = "KALI DEVOURS THE SACRIFICE!";
3028   } else if (global.favor < 0) {
3029     global.favor += (what.status == MapObject::STUNNED ? what.favor : round(what.favor/2.0)); //k8: added `round()`
3030     if (what.favor > 0) what.favor = 0;
3031   } else {
3032     global.favor += (what.status == MapObject::STUNNED ? what.favor : round(what.favor/2.0)); //k8: added `round()`
3033   }
3035   /*!!
3036        if (myAltar.gift1 != "" and global.kaliGift == 0) scrGetKaliGift(myAltar.gift1);
3037   else if (myAltar.gift2 != "" and global.kaliGift == 1) scrGetKaliGift(myAltar.gift2);
3038   else scrGetKaliGift("");
3039   */
3041   // sacrifice is player?
3042   if (what isa PlayerPawn) {
3043     ++stats.totalSelfSacrifices;
3044     msg = "KALI DEVOURS YOU!";
3045     player.visible = false;
3046     player.removeBallAndChain(temp:true);
3047     player.dead = true;
3048     player.status = MapObject::DEAD;
3049   } else {
3050     ++stats.totalSacrifices;
3051     auto msg2 = scrGetKaliGift(where);
3052     what.instanceRemove();
3053     if (msg2) msg = va("%s\n%s", msg, msg2);
3054   }
3056   osdMessage(msg, 6.66);
3058   scrShake(10);
3062 // ////////////////////////////////////////////////////////////////////////// //
3063 final void addBackgroundGfxDetails () {
3064   // add background details
3065   //if (global.customLevel) return;
3066   foreach (; 0..20) {
3067     // bg = instance_create(16*randRoom(1, 42), 16*randRoom(1, 33), oCaveBG);
3068          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);
3069     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);
3070     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);
3071     else MakeMapBackTile('bgExtras', 32*global.randRoom(0, 1), 0, 32, 32, 16*global.randRoom(1, 42), 16*global.randRoom(1, 33), 10002);
3072   }
3076 // ////////////////////////////////////////////////////////////////////////// //
3077 private final void fixRealViewStart () {
3078   int scale = global.scale;
3079   realViewStart.x = clamp(realViewStart.x, viewMin.x*scale, viewMax.x*scale-viewWidth);
3080   realViewStart.y = clamp(realViewStart.y, viewMin.y*scale, viewMax.y*scale-viewHeight);
3084 final int cameraCurrX () { return realViewStart.x/global.scale; }
3085 final int cameraCurrY () { return realViewStart.y/global.scale; }
3088 private final void fixViewStart () {
3089   int scale = global.scale;
3090   viewStart.x = clamp(viewStart.x, viewMin.x*scale, viewMax.x*scale-viewWidth);
3091   viewStart.y = clamp(viewStart.y, viewMin.y*scale, viewMax.y*scale-viewHeight);
3095 final void centerViewAtPlayer () {
3096   if (viewWidth < 1 || viewHeight < 1 || !player) return;
3097   centerViewAt(player.xCenter, player.yCenter);
3101 final void centerViewAt (int x, int y) {
3102   if (viewWidth < 1 || viewHeight < 1) return;
3104   cameraSlideToSpeed.x = 0;
3105   cameraSlideToSpeed.y = 0;
3106   cameraSlideToPlayer = 0;
3108   int scale = global.scale;
3109   x *= scale;
3110   y *= scale;
3111   realViewStart.x = clamp(x-viewWidth/2, 0, tilesWidth*16*scale-viewWidth);
3112   realViewStart.y = clamp(y-viewHeight/2, 0, tilesHeight*16*scale-viewHeight);
3113   fixRealViewStart();
3115   viewStart.x = realViewStart.x;
3116   viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
3117   fixViewStart();
3119   if (onCameraTeleported) onCameraTeleported();
3123 const int ViewPortToleranceX = 16*1+8;
3124 const int ViewPortToleranceY = 16*1+8;
3126 final void fixCamera () {
3127   if (!player) return;
3128   if (viewWidth < 1 || viewHeight < 1) return;
3129   int scale = global.scale;
3130   auto alwaysCenterX = global.config.alwaysCenterPlayer;
3131   auto alwaysCenterY = alwaysCenterX;
3132   // calculate offset from viewport center (in game units), and fix viewport
3134   int camDestX = player.ix+8;
3135   int camDestY = player.iy+8;
3136   if (!cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y) != 0) {
3137     // slide camera to point
3138     if (cameraSlideToSpeed.x) camDestX = cameraSlideToCurr.x;
3139     if (cameraSlideToSpeed.y) camDestY = cameraSlideToCurr.y;
3140     int dx = cameraSlideToDest.x-camDestX;
3141     int dy = cameraSlideToDest.y-camDestY;
3142     //writeln("speed:(", cameraSlideToSpeed.x, ",", cameraSlideToSpeed.y, "); cur:(", camDestX, ",", camDestY, "); dest:(", cameraSlideToDest.x, ",", cameraSlideToDest.y, "); delta=(", dx, ",", dy, ")");
3143     if (dx && cameraSlideToSpeed.x != 0) {
3144       alwaysCenterX = true;
3145       if (abs(dx) <= abs(cameraSlideToSpeed.x)) {
3146         camDestX = cameraSlideToDest.x;
3147       } else {
3148         camDestX += sign(dx)*abs(cameraSlideToSpeed.x);
3149       }
3150     }
3151     if (dy && abs(cameraSlideToSpeed.y) != 0) {
3152       alwaysCenterY = true;
3153       if (abs(dy) <= cameraSlideToSpeed.y) {
3154         camDestY = cameraSlideToDest.y;
3155       } else {
3156         camDestY += sign(dy)*abs(cameraSlideToSpeed.y);
3157       }
3158     }
3159     //writeln("  new:(", camDestX, ",", camDestY, ")");
3160     if (cameraSlideToSpeed.x) cameraSlideToCurr.x = camDestX;
3161     if (cameraSlideToSpeed.y) cameraSlideToCurr.y = camDestY;
3162   }
3164   // horizontal
3165   if (cameraSlideToSpeed.x && !cameraSlideToPlayer) {
3166     realViewStart.x = clamp(camDestX*scale, 0, tilesWidth*16*scale-viewWidth);
3167   } else if (!player.cameraBlockX) {
3168     int x = camDestX*scale;
3169     int cx = realViewStart.x;
3170     if (alwaysCenterX) {
3171       cx = x-viewWidth/2;
3172     } else {
3173       int xofs = x-(cx+viewWidth/2);
3174            if (xofs < -ViewPortToleranceX*scale) cx += xofs+ViewPortToleranceX*scale;
3175       else if (xofs > ViewPortToleranceX*scale) cx += xofs-ViewPortToleranceX*scale;
3176     }
3177     // slide back to player?
3178     if (cameraSlideToPlayer && cameraSlideToSpeed.x) {
3179       int prevx = cameraSlideToCurr.x*scale;
3180       int dx = (cx-prevx)/scale;
3181       if (abs(dx) <= cameraSlideToSpeed.x) {
3182         writeln("BACKSLIDE X COMPLETE!");
3183         cameraSlideToSpeed.x = 0;
3184       } else {
3185         cx = prevx+(sign(dx)*cameraSlideToSpeed.x)*scale;
3186         cameraSlideToCurr.x += sign(dx)*cameraSlideToSpeed.x;
3187         if ((dx < 0 && cx <= 0) || (dx > 0 && cx >= tilesWidth*16*scale-viewWidth)) {
3188           writeln("BACKSLIDE X COMPLETE!");
3189           cameraSlideToSpeed.x = 0;
3190         }
3191       }
3192     }
3193     realViewStart.x = clamp(cx, 0, tilesWidth*16*scale-viewWidth);
3194   }
3196   // vertical
3197   if (cameraSlideToSpeed.y && !cameraSlideToPlayer) {
3198     realViewStart.y = clamp(camDestY*scale, 0, tilesHeight*16*scale-viewHeight);
3199   } else if (!player.cameraBlockY) {
3200     int y = camDestY*scale;
3201     int cy = realViewStart.y;
3202     if (alwaysCenterY) {
3203       cy = y-viewHeight/2;
3204     } else {
3205       int yofs = y-(cy+viewHeight/2);
3206            if (yofs < -ViewPortToleranceY*scale) cy += yofs+ViewPortToleranceY*scale;
3207       else if (yofs > ViewPortToleranceY*scale) cy += yofs-ViewPortToleranceY*scale;
3208     }
3209     // slide back to player?
3210     if (cameraSlideToPlayer && cameraSlideToSpeed.y) {
3211       int prevy = cameraSlideToCurr.y*scale;
3212       int dy = (cy-prevy)/scale;
3213       if (abs(dy) <= cameraSlideToSpeed.y) {
3214         writeln("BACKSLIDE Y COMPLETE!");
3215         cameraSlideToSpeed.y = 0;
3216       } else {
3217         cy = prevy+(sign(dy)*cameraSlideToSpeed.y)*scale;
3218         cameraSlideToCurr.y += sign(dy)*cameraSlideToSpeed.y;
3219         if ((dy < 0 && cy <= 0) || (dy > 0 && cy >= tilesHeight*16*scale-viewHeight)) {
3220           writeln("BACKSLIDE Y COMPLETE!");
3221           cameraSlideToSpeed.y = 0;
3222         }
3223       }
3224     }
3225     realViewStart.y = clamp(cy, 0, tilesHeight*16*scale-viewHeight);
3226   }
3228   if (cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y) == 0) cameraSlideToPlayer = 0;
3230   fixRealViewStart();
3231   //writeln("  new2:(", cameraCurrX, ",", cameraCurrY, ")");
3233   viewStart.x = realViewStart.x;
3234   viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
3235   fixViewStart();
3239 // ////////////////////////////////////////////////////////////////////////// //
3240 // x0 and y0 are non-scaled (and will be scaled)
3241 final void drawSpriteAt (name sprName, float frnumf, int x0, int y0, optional bool small) {
3242   if (!sprName) return;
3243   auto spr = sprStore[sprName];
3244   if (!spr || !spr.frames.length) return;
3245   int scale = global.scale;
3246   x0 *= scale;
3247   y0 *= scale;
3248   int frnum = max(0, trunc(frnumf))%spr.frames.length;
3249   auto sfr = spr.frames[frnum];
3250   int sx0 = x0-sfr.xofs*scale;
3251   int sy0 = y0-sfr.yofs*scale;
3252   if (small && scale > 1) {
3253     sfr.tex.blitExt(sx0, sy0, round(sx0+sfr.tex.width*(scale/2.0)), round(sy0+sfr.tex.height*(scale/2.0)), 0, 0);
3254   } else {
3255     sfr.tex.blitAt(sx0, sy0, scale);
3256   }
3260 final void drawSpriteAtS3 (name sprName, float frnumf, int x0, int y0) {
3261   if (!sprName) return;
3262   auto spr = sprStore[sprName];
3263   if (!spr || !spr.frames.length) return;
3264   x0 *= 3;
3265   y0 *= 3;
3266   int frnum = max(0, trunc(frnumf))%spr.frames.length;
3267   auto sfr = spr.frames[frnum];
3268   int sx0 = x0-sfr.xofs*3;
3269   int sy0 = y0-sfr.yofs*3;
3270   sfr.tex.blitAt(sx0, sy0, 3);
3274 // x0 and y0 are non-scaled (and will be scaled)
3275 final void drawTextAt (int x0, int y0, string text, optional int scale, optional int hiColor1, optional int hiColor2) {
3276   if (!text) return;
3277   if (!specified_scale) scale = global.scale;
3278   x0 *= scale;
3279   y0 *= scale;
3280   sprStore.renderTextWithHighlight(x0, y0, text, scale, hiColor1!optional, hiColor2!optional);
3284 void renderCompass (float currFrameDelta) {
3285   if (!global.hasCompass) return;
3287   /*
3288   if (isRoom("rOlmec")) {
3289     global.exitX = 648;
3290     global.exitY = 552;
3291   } else if (isRoom("rOlmec2")) {
3292     global.exitX = 648;
3293     global.exitY = 424;
3294   }
3295   */
3297   bool hasMessage = osdHasMessage();
3298   foreach (MapTile et; allExits) {
3299     // original compass
3300     int exitX = et.ix, exitY = et.iy;
3301     int vx0 = viewStart.x/global.scale, vy0 = viewStart.y/global.scale;
3302     int vx1 = (viewStart.x+viewWidth)/global.scale;
3303     int vy1 = (viewStart.y+viewHeight)/global.scale;
3304     if (exitY > vy1-16) {
3305       if (exitX < vx0) {
3306         drawSpriteAt(hasMessage ? 'sCompassSmallLL' : 'sCompassLL', 0, 0, 224);
3307       } else if (exitX > vx1-16) {
3308         drawSpriteAt(hasMessage ? 'sCompassSmallLR' : 'sCompassLR', 0, 304, 224);
3309       } else {
3310         drawSpriteAt(hasMessage ? 'sCompassSmallDown' : 'sCompassDown', 0, exitX-vx0, 224);
3311       }
3312     } else if (exitX < vx0) {
3313       drawSpriteAt(hasMessage ? 'sCompassSmallLeft' : 'sCompassLeft', 0, 0, exitY-vy0);
3314     } else if (exitX > vx1-16) {
3315       drawSpriteAt(hasMessage ? 'sCompassSmallRight' : 'sCompassRight', 0, 304, exitY-vy0);
3316     }
3317     break; // only the first exit
3318   }
3322 final bool totalsNameCmpCB (ref GameStats::TotalItem a, ref GameStats::TotalItem b) {
3323   auto sa = string(a.objName);
3324   auto sb = string(b.objName);
3325   return (sa < sb);
3328 void renderTransitionInfo (float currFrameDelta) {
3329   //FIXME!
3330   /*
3331   GameStats.sortTotalsList(stats.kills, &totalsNameCmpCB);
3333   int maxLen = 0;
3334   foreach (int idx, ref auto k; stats.kills) {
3335     string s = string(k);
3336     maxLen = max(maxLen, s.length);
3337   }
3338   maxLen *= 8;
3340   sprStore.loadFont('sFontSmall');
3341   Video.color = 0xff_ff_00;
3342   foreach (int idx, ref auto k; stats.kills) {
3343     int deaths = 0;
3344     foreach (int xidx, ref auto d; stats.totalKills) {
3345       if (d.objName == k) { deaths = d.count; break; }
3346     }
3347     //drawTextAt(16, 4+idx*8, va("%s: %d (%d)", string(k.objName).toUpperCase, k.count, deaths));
3348     drawTextAt(16, 4+idx*8, string(k).toUpperCase);
3349     drawTextAt(16+maxLen, 4+idx*8, va(": %d (%d)", k.count, deaths));
3350   }
3351   */
3355 void renderGhostTimer (float currFrameDelta) {
3356   if (ghostTimeLeft <= 0) return;
3357   //ghostTimeLeft /= 30; // frames -> seconds
3359   int hgt = viewHeight-64;
3360   if (hgt < 1) return;
3361   int rhgt = round(float(hgt)*(float(ghostTimeLeft)/30.0)/1000.0);
3362   //writeln("hgt=", hgt, "; rhgt=", rhgt, "; gtl=", ghostTimeLeft/30);
3363   if (rhgt > 0) {
3364     auto oclr = Video.color;
3365     Video.color = 0xcf_ff_7f_00;
3366     Video.fillRect(viewWidth-20, 32, 16, hgt-rhgt);
3367     Video.color = 0x7f_ff_7f_00;
3368     Video.fillRect(viewWidth-20, 32+(hgt-rhgt), 16, rhgt);
3369     Video.color = oclr;
3370   }
3374 void renderStarsHUD (float currFrameDelta) {
3375   bool scumSmallHud = global.config.scumSmallHud;
3377   //auto life = max(0, global.plife);
3378   int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3379   int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3380   //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3382   int hhup;
3384   if (scumSmallHud) {
3385     sprStore.loadFont('sFontSmall');
3386     hhup = 6;
3387   } else {
3388     sprStore.loadFont('sFont');
3389     hhup = 2;
3390   }
3392   Video.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3393   //draw_sprite(sHeart, -1, view_xview[0]+8, view_yview[0]+8);
3394   //draw_text(view_xview[0]+24, view_yview[0]+8, life);
3395   if (scumSmallHud) {
3396     if (global.plife == 1) {
3397       drawSpriteAt('sHeartSmallBlink', global.heartBlink, 10, hhup-4);
3398       global.heartBlink += 0.1;
3399       if (global.heartBlink > 3) global.heartBlink = 0;
3400     } else {
3401       drawSpriteAt('sHeartSmall', -1, 10, hhup-4);
3402       global.heartBlink = 0;
3403     }
3404   } else {
3405     if (global.plife == 1) {
3406       drawSpriteAt('sHeartBlink', global.heartBlink, 8, hhup);
3407       global.heartBlink += 0.1;
3408       if (global.heartBlink > 3) global.heartBlink = 0;
3409     } else {
3410       drawSpriteAt('sHeart', -1, 8, hhup);
3411       global.heartBlink = 0;
3412     }
3413   }
3414   int life = clamp(global.plife, 0, 99);
3415   drawTextAt(16+8, hhup, va("%d", life));
3417   //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3418   drawSpriteAt('sShopkeeperIcon', -1, 64, hhup, scumSmallHud);
3419   drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", starsKills));
3421   if (starsRoomTimer1 > 0) {
3422     sprStore.loadFont('sFontSmall');
3423     Video.color = 0xff_ff_00;
3424     int scale = global.scale;
3425     sprStore.renderMultilineTextCentered(viewWidth/2, 216*scale, va("SHOTGUN CHALLENGE BEGINS IN ~%d~", (starsRoomTimer1/30)+1), scale, 0xff_00_00);
3426   }
3430 void renderSunHUD (float currFrameDelta) {
3431   bool scumSmallHud = global.config.scumSmallHud;
3433   //auto life = max(0, global.plife);
3434   int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3435   int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3436   //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3438   int hhup;
3440   if (scumSmallHud) {
3441     sprStore.loadFont('sFontSmall');
3442     hhup = 6;
3443   } else {
3444     sprStore.loadFont('sFont');
3445     hhup = 2;
3446   }
3448   Video.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3449   //draw_sprite(sHeart, -1, view_xview[0]+8, view_yview[0]+8);
3450   //draw_text(view_xview[0]+24, view_yview[0]+8, life);
3451   if (scumSmallHud) {
3452     if (global.plife == 1) {
3453       drawSpriteAt('sHeartSmallBlink', global.heartBlink, 10, hhup-4);
3454       global.heartBlink += 0.1;
3455       if (global.heartBlink > 3) global.heartBlink = 0;
3456     } else {
3457       drawSpriteAt('sHeartSmall', -1, 10, hhup-4);
3458       global.heartBlink = 0;
3459     }
3460   } else {
3461     if (global.plife == 1) {
3462       drawSpriteAt('sHeartBlink', global.heartBlink, 8, hhup);
3463       global.heartBlink += 0.1;
3464       if (global.heartBlink > 3) global.heartBlink = 0;
3465     } else {
3466       drawSpriteAt('sHeart', -1, 8, hhup);
3467       global.heartBlink = 0;
3468     }
3469   }
3470   int life = clamp(global.plife, 0, 99);
3471   drawTextAt(16+8, hhup, va("%d", life));
3473   //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3474   drawSpriteAt('sDamselIcon', -1, 64, hhup, scumSmallHud);
3475   drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", sunScore));
3477   if (sunRoomTimer1 > 0) {
3478     sprStore.loadFont('sFontSmall');
3479     Video.color = 0xff_ff_00;
3480     int scale = global.scale;
3481     sprStore.renderMultilineTextCentered(viewWidth/2, 216*scale, va("DAMSEL CHALLENGE BEGINS IN ~%d~", (sunRoomTimer1/30)+1), scale, 0xff_00_00);
3482   }
3486 void renderMoonHUD (float currFrameDelta) {
3487   bool scumSmallHud = global.config.scumSmallHud;
3489   //auto life = max(0, global.plife);
3490   int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3491   int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3492   //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3494   int hhup;
3496   if (scumSmallHud) {
3497     sprStore.loadFont('sFontSmall');
3498     hhup = 6;
3499   } else {
3500     sprStore.loadFont('sFont');
3501     hhup = 2;
3502   }
3504   Video.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3506   //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3507   drawSpriteAt('sHoopsIcon', -1, 8, hhup, scumSmallHud);
3508   drawTextAt(8+16-(scumSmallHud ? 6 : 0), hhup, va("%d", moonScore));
3509   drawSpriteAt('sTimerIcon', -1, 64, hhup, scumSmallHud);
3510   drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", max(0, moonTimer)));
3512   if (moonRoomTimer1 > 0) {
3513     sprStore.loadFont('sFontSmall');
3514     Video.color = 0xff_ff_00;
3515     int scale = global.scale;
3516     sprStore.renderMultilineTextCentered(viewWidth/2, 216*scale, va("ARCHERY CHALLENGE BEGINS IN ~%d~", (moonRoomTimer1/30)+1), scale, 0xff_00_00);
3517   }
3521 void renderHUD (float currFrameDelta) {
3522   if (levelKind == LevelKind.Stars) { renderStarsHUD(currFrameDelta); return; }
3523   if (levelKind == LevelKind.Sun) { renderSunHUD(currFrameDelta); return; }
3524   if (levelKind == LevelKind.Moon) { renderMoonHUD(currFrameDelta); return; }
3526   if (!isHUDEnabled()) return;
3528   if (levelKind == LevelKind.Transition) { renderTransitionInfo(currFrameDelta); return; }
3530   int lifeX = 4; // 8
3531   int bombX = 56;
3532   int ropeX = 104;
3533   int ammoX = 152;
3534   int moneyX = 200;
3535   int hhup;
3536   bool scumSmallHud = global.config.scumSmallHud;
3537   if (!global.config.optSGAmmo) moneyX = ammoX;
3539   if (scumSmallHud) {
3540     sprStore.loadFont('sFontSmall');
3541     hhup = 6;
3542   } else {
3543     sprStore.loadFont('sFont');
3544     hhup = 0;
3545   }
3546   //int alpha = 0x6f_00_00_00;
3547   int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3548   int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3550   //Video.color = 0xff_ff_ff;
3551   Video.color = 0xff_ff_ff|talpha;
3553   // hearts
3554   if (scumSmallHud) {
3555     if (global.plife == 1) {
3556       drawSpriteAt('sHeartSmallBlink', global.heartBlink, lifeX+2, 4-hhup);
3557       global.heartBlink += 0.1;
3558       if (global.heartBlink > 3) global.heartBlink = 0;
3559     } else {
3560       drawSpriteAt('sHeartSmall', -1, lifeX+2, 4-hhup);
3561       global.heartBlink = 0;
3562     }
3563   } else {
3564     if (global.plife == 1) {
3565       drawSpriteAt('sHeartBlink', global.heartBlink, lifeX, 8-hhup);
3566       global.heartBlink += 0.1;
3567       if (global.heartBlink > 3) global.heartBlink = 0;
3568     } else {
3569       drawSpriteAt('sHeart', -1, lifeX, 8-hhup);
3570       global.heartBlink = 0;
3571     }
3572   }
3574   int life = clamp(global.plife, 0, 99);
3575   //if (!scumHud && life > 99) life = 99;
3576   drawTextAt(lifeX+16, 8-hhup, va("%d", life));
3578   // bombs
3579   if (global.hasStickyBombs && global.stickyBombsActive) {
3580     if (scumSmallHud) drawSpriteAt('sStickyBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sStickyBombIcon', -1, bombX, 8-hhup);
3581   } else {
3582     if (scumSmallHud) drawSpriteAt('sBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sBombIcon', -1, bombX, 8-hhup);
3583   }
3584   int n = global.bombs;
3585   if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3586   drawTextAt(bombX+16, 8-hhup, va("%d", n));
3588   // ropes
3589   if (scumSmallHud) drawSpriteAt('sRopeIconSmall', -1, ropeX+2, 4-hhup); else drawSpriteAt('sRopeIcon', -1, ropeX, 8-hhup);
3590   n = global.rope;
3591   if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3592   drawTextAt(ropeX+16, 8-hhup, va("%d", n));
3594   // shotgun shells
3595   if (global.config.optSGAmmo && (!player || player.holdItem !isa ItemWeaponBow)) {
3596     if (scumSmallHud) drawSpriteAt('sShellsIcon', -1, ammoX+6, 8-hhup); else drawSpriteAt('sShellsIcon', -1, ammoX+7, 12-hhup);
3597     n = global.sgammo;
3598     if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3599     drawTextAt(ammoX+16, 8-hhup, va("%d", n));
3600   } else if (player && player.holdItem isa ItemWeaponBow) {
3601     if (scumSmallHud) drawSpriteAt('sArrowRight', -1, ammoX+6, 8-hhup); else drawSpriteAt('sArrowRight', -1, ammoX+7, 12-hhup);
3602     n = global.arrows;
3603     if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3604     drawTextAt(ammoX+16, 8-hhup, va("%d", n));
3605   }
3607   // money
3608   if (scumSmallHud) drawSpriteAt('sDollarSignSmall', -1, moneyX+2, 3-hhup); else drawSpriteAt('sDollarSign', -1, moneyX, 8-hhup);
3609   drawTextAt(moneyX+16, 8-hhup, va("%d", stats.money-xmoney));
3611   // items
3612   Video.color = 0xff_ff_ff|ialpha;
3614   int ity = (scumSmallHud ? 18-hhup : 24-hhup);
3616   n = 8; //28;
3617   if (global.hasUdjatEye) {
3618     if (global.udjatBlink) drawSpriteAt('sUdjatEyeIcon2', -1, n, ity); else drawSpriteAt('sUdjatEyeIcon', -1, n, ity);
3619     n += 20;
3620   }
3621   if (global.hasAnkh) { drawSpriteAt('sAnkhIcon', -1, n, ity); n += 20; }
3622   if (global.hasCrown) { drawSpriteAt('sCrownIcon', -1, n, ity); n += 20; }
3623   if (global.hasKapala) {
3624          if (global.bloodLevel == 0) drawSpriteAt('sKapalaIcon', 0, n, ity);
3625     else if (global.bloodLevel <= 2) drawSpriteAt('sKapalaIcon', 1, n, ity);
3626     else if (global.bloodLevel <= 4) drawSpriteAt('sKapalaIcon', 2, n, ity);
3627     else if (global.bloodLevel <= 6) drawSpriteAt('sKapalaIcon', 3, n, ity);
3628     else if (global.bloodLevel <= 8) drawSpriteAt('sKapalaIcon', 4, n, ity);
3629     n += 20;
3630   }
3631   if (global.hasSpectacles) { drawSpriteAt('sSpectaclesIcon', -1, n, ity); n += 20; }
3632   if (global.hasGloves) { drawSpriteAt('sGlovesIcon', -1, n, ity); n += 20; }
3633   if (global.hasMitt) { drawSpriteAt('sMittIcon', -1, n, ity); n += 20; }
3634   if (global.hasSpringShoes) { drawSpriteAt('sSpringShoesIcon', -1, n, ity); n += 20; }
3635   if (global.hasJordans) { drawSpriteAt('sJordansIcon', -1, n, ity); n += 20; }
3636   if (global.hasSpikeShoes) { drawSpriteAt('sSpikeShoesIcon', -1, n, ity); n += 20; }
3637   if (global.hasCape) { drawSpriteAt('sCapeIcon', -1, n, ity); n += 20; }
3638   if (global.hasJetpack) { drawSpriteAt('sJetpackIcon', -1, n, ity); n += 20; }
3639   if (global.hasStickyBombs) { drawSpriteAt('sPasteIcon', -1, n, ity); n += 20; }
3640   if (global.hasCompass) { drawSpriteAt('sCompassIcon', -1, n, ity); n += 20; }
3641   if (global.hasParachute) { drawSpriteAt('sParachuteIcon', -1, n, ity); n += 20; }
3643   if (player.holdItem && player.holdItem.objName == 'Bow' && global.arrows > 0) {
3644     int m = 1;
3645     float malpha = 1;
3646     while (m <= global.arrows && m <= 20 && malpha > 0) {
3647       Video.color = trunc(malpha*255)<<24|0xff_ff_ff;
3648       drawSpriteAt('sArrowIcon', -1, n, ity);
3649       n += 4;
3650       if (m > 10) malpha -= 0.1; //and global.arrows > 20 malpha -= 0.1
3651       m += 1;
3652     }
3653   }
3655   if (xmoney > 0) {
3656     sprStore.loadFont('sFontSmall');
3657     Video.color = 0xff_ff_00|talpha;
3658     if (scumSmallHud) drawTextAt(moneyX+8, 8+8-hhup, va("+%d", xmoney));
3659     else drawTextAt(moneyX, 8+16-hhup, va("+%d", xmoney));
3660   }
3662   Video.color = 0xff_ff_ff;
3663   if (global.config.ghostShowTime) renderGhostTimer(currFrameDelta);
3667 // ////////////////////////////////////////////////////////////////////////// //
3668 // x0 and y0 are non-scaled (and will be scaled)
3669 final void drawTextAtS3 (int x0, int y0, string text, optional int hiColor1, optional int hiColor2) {
3670   if (!text) return;
3671   x0 *= 3;
3672   y0 *= 3;
3673   sprStore.renderTextWithHighlight(x0, y0, text, 3, hiColor1!optional, hiColor2!optional);
3677 final void drawTextAtS3Centered (int y0, string text, optional int hiColor1, optional int hiColor2) {
3678   if (!text) return;
3679   int x0 = (viewWidth-sprStore.getTextWidth(text, 3, specified_hiColor1, specified_hiColor2))/2;
3680   sprStore.renderTextWithHighlight(x0, y0*3, text, 3, hiColor1!optional, hiColor2!optional);
3684 void renderHelpOverlay () {
3685   Video.color = 0;
3686   Video.fillRect(0, 0, viewWidth, viewHeight);
3688   int tx = 16;
3689   int txoff = 0; // text x pos offset (for multi-color lines)
3690   int ty = 8;
3691   if (gameHelpScreen) {
3692     sprStore.loadFont('sFontSmall');
3693     Video.color = 0xff_ff_ff;
3694     drawTextAtS3Centered(ty, va("HELP (PAGE ~%d~ OF ~%d~)", gameHelpScreen, MaxGameHelpScreen), 0xff_ff_00);
3695     ty += 24;
3696   }
3698   if (gameHelpScreen == 1) {
3699     sprStore.loadFont('sFontSmall');
3700     Video.color = 0xff_ff_00; drawTextAtS3(tx, ty, "INVENTORY BASICS"); ty += 16;
3701     Video.color = 0xff_ff_ff;
3702     drawTextAtS3(tx, ty, global.expandString("Press $SWITCH to cycle through items."), 0x00_ff_00);
3703     ty += 8;
3704     ty += 56;
3705     Video.color = 0xff_ff_ff;
3706     drawSpriteAtS3('sHelpSprite1', -1, 64, 96);
3707   } else if (gameHelpScreen == 2) {
3708     sprStore.loadFont('sFontSmall');
3709     Video.color = 0xff_ff_00;
3710     drawTextAtS3(tx, ty, "SELLING TO SHOPKEEPERS"); ty += 16;
3711     Video.color = 0xff_ff_ff;
3712     drawTextAtS3(tx, ty, global.expandString("Press $PAY to offer your currently"), 0x00_ff_00); ty += 8;
3713     drawTextAtS3(tx, ty, "held item to the shopkeeper."); ty += 16;
3714     drawTextAtS3(tx, ty, "If the shopkeeper is interested, "); ty += 8;
3715     //drawTextAtS3(tx, ty, global.expandString("press $PAY again to complete the sale."), 0x00_ff_00); ty += 72;
3716     drawTextAtS3(tx, ty, global.expandString("press $PAY again to complete"), 0x00_ff_00);
3717     drawTextAtS3(tx, ty+8, "the sale.");
3718     ty += 72;
3719     drawSpriteAtS3('sHelpSell', -1, 112, 100);
3720     drawTextAtS3(tx, ty, "Purchasing goods from the shopkeeper"); ty += 8;
3721     drawTextAtS3(tx, ty, "will increase the funds he has"); ty += 8;
3722     drawTextAtS3(tx, ty, "available to buy your unwanted stuff."); ty += 8;
3723   } else {
3724     // map
3725     sprStore.loadFont('sFont');
3726     Video.color = 0xff_ff_ff;
3727     drawTextAtS3(136, 8, "MAP");
3729     if (lg.mapSprite && (isNormalLevel() || isTransitionRoom())) {
3730       Video.color = 0xff_ff_00;
3731       drawTextAtS3Centered(24, lg.mapTitle);
3733       auto spf = sprStore[lg.mapSprite].frames[0];
3734       int mapX = 160-spf.width/2;
3735       int mapY = 120-spf.height/2;
3736       //mapTitleX = 160-string_length(global.mapTitle)*8/2;
3738       Video.color = 0xff_ff_ff;
3739       drawSpriteAtS3(lg.mapSprite, -1, mapX, mapY);
3741       if (lg.mapSprite != 'sMapDefault') {
3742         int mx = -1, my = -1;
3744         // set position of player icon
3745         switch (global.currLevel) {
3746           case 1: mx = 81; my = 22; break;
3747           case 2: mx = 113; my = 63; break;
3748           case 3: mx = 197; my = 86; break;
3749           case 4: mx = 133; my = 109; break;
3750           case 5: mx = 181; my = 22; break;
3751           case 6: mx = 126; my = 64; break;
3752           case 7: mx = 158; my = 112; break;
3753           case 8: mx = 66; my = 80; break;
3754           case 9: mx = 30; my = 26; break;
3755           case 10: mx = 88; my = 54; break;
3756           case 11: mx = 148; my = 81; break;
3757           case 12: mx = 210; my = 205; break;
3758           case 13: mx = 66; my = 17; break;
3759           case 14: mx = 146; my = 17; break;
3760           case 15: mx = 82; my = 77; break;
3761           case 16: mx = 178; my = 81; break;
3762         }
3764         if (mx >= 0) {
3765           int plrx = mx+player.ix/16;
3766           int plry = my+player.iy/16;
3767           if (isTransitionRoom()) { plrx = mx+20; plry = my+16; }
3768           name plrspr = 'sMapSpelunker';
3769                if (global.isDamsel) plrspr = 'sMapDamsel';
3770           else if (global.isTunnelMan) plrspr = 'sMapTunnel';
3771           auto ss = sprStore[plrspr];
3772           drawSpriteAtS3(plrspr, (pausedTime/2)%ss.frames.length, mapX+plrx, mapY+plry);
3773           // exit door icon
3774           if (global.hasCompass && allExits.length) {
3775             drawSpriteAtS3('sMapRedDot', -1, mapX+mx+allExits[0].ix/16, mapY+my+allExits[0].iy/16);
3776           }
3777         }
3778       }
3779     }
3780   }
3782   sprStore.loadFont('sFontSmall');
3783   Video.color = 0xff_ff_00;
3784   drawTextAtS3Centered(232, "PRESS ~SPACE~/~LEFT~/~RIGHT~ TO CHANGE PAGE", 0x00_ff_00);
3786   Video.color = 0xff_ff_ff;
3790 void renderPauseOverlay () {
3791   //drawTextAt(256, 432, "PAUSED", scale);
3793   if (gameShowHelp) { renderHelpOverlay(); return; }
3795   Video.color = 0xff_ff_00;
3796   //int hiColor = 0x00_ff_00;
3798   int n = 120;
3799   if (isTutorialRoom()) {
3800     sprStore.loadFont('sFont');
3801     drawTextAtS3Centered(n-24, "TUTORIAL CAVE");
3802   } else if (isNormalLevel()) {
3803     sprStore.loadFont('sFont');
3805     drawTextAtS3Centered(n-32, va("LEVEL ~%d~", global.currLevel), 0x00_ff_00);
3807     sprStore.loadFont('sFontSmall');
3809     int depth = round((174.8*(global.currLevel-1)+(player.iy+8)*0.34)*(global.config.scumMetric ? 0.3048 : 1.0)*10);
3810     string depthStr = va("DEPTH: ~%d.%d~ %s", depth/10, depth%10, (global.config.scumMetric ? "METRES" : "FEET"));
3811     drawTextAtS3Centered(n-16, depthStr, 0x00_ff_00);
3813     n += 16;
3814     drawTextAtS3Centered(n, va("MONEY: ~%d~", stats.money), 0x00_ff_00);
3815     drawTextAtS3Centered(n+16, va("KILLS: ~%d~", stats.countKills), 0x00_ff_00);
3816     drawTextAtS3Centered(n+32, va("SAVES: ~%d~", stats.damselsSaved), 0x00_ff_00);
3817     drawTextAtS3Centered(n+48, va("TIME: ~%s~", time2str(time/30)), 0x00_ff_00);
3818     drawTextAtS3Centered(n+64, va("LEVEL TIME: ~%s~", time2str((time-levelStartTime)/30)), 0x00_ff_00);
3819   }
3821   sprStore.loadFont('sFontSmall');
3822   Video.color = 0xff_ff_ff;
3823   drawTextAtS3Centered(240-2-8, "~ESC~-RETURN  ~F10~-QUIT  ~CTRL+DEL~-SUICIDE", 0xff_7f_00);
3824   drawTextAtS3Centered(2, "~O~PTIONS  REDEFINE ~K~EYS  ~S~TATISTICS", 0xff_7f_00);
3828 // ////////////////////////////////////////////////////////////////////////// //
3829 transient int drawLoot;
3830 transient int drawPosX, drawPosY;
3832 void resetTransitionOverlay () {
3833   drawLoot = 0;
3834   drawPosX = 100;
3835   drawPosY = 83;
3839 // current game, uncollapsed
3840 struct LevelStatInfo {
3841   name aname;
3842   // for transition screen
3843   bool render;
3844   int x, y;
3849 void thinkFrameTransition () {
3850   if (drawLoot == 0) {
3851     if (drawPosX > 272) {
3852       drawPosX = 100;
3853       drawPosY += 2;
3854       if (drawPosY > 83+4) drawPosY = 83;
3855     }
3856   } else if (drawPosX > 232) {
3857     drawPosX = 96;
3858     drawPosY += 2;
3859     if (drawPosY > 91+4) drawPosY = 91;
3860   }
3864 void renderTransitionOverlay () {
3865   sprStore.loadFont('sFontSmall');
3866   Video.color = 0xff_ff_00;
3867   //else if (global.currLevel-1 &lt; 1) draw_text(32, 48, "TUTORIAL CAVE COMPLETED!");
3868   //else draw_text(32, 48, "LEVEL "+string(global.currLevel-1)+" COMPLETED!");
3869   if (global.currLevel == 0) {
3870     drawTextAt(32, 48, "TUTORIAL CAVE COMPLETED!");
3871   } else {
3872     drawTextAt(32, 48, va("LEVEL ~%d~ COMPLETED!", global.currLevel), hiColor1:0x00_ff_ff);
3873   }
3874   Video.color = 0xff_ff_ff;
3875   drawTextAt(32, 64, va("TIME  = ~%s~", time2str((levelEndTime-levelStartTime)/30)), hiColor1:0xff_ff_00);
3877   if (/*stats.collected.length == 0*/stats.money <= levelMoneyStart) {
3878     drawTextAt(32, 80, "LOOT  = ~NONE~", hiColor1:0xff_00_00);
3879   } else {
3880     drawTextAt(32, 80, va("LOOT  = ~%d~", stats.money-levelMoneyStart), hiColor1:0xff_ff_00);
3881   }
3883   if (stats.kills.length == 0) {
3884     drawTextAt(32, 96, "KILLS = ~NONE~", hiColor1:0x00_ff_00);
3885   } else {
3886     drawTextAt(32, 96, va("KILLS = ~%d~", stats.kills.length), hiColor1:0xff_ff_00);
3887   }
3889   drawTextAt(32, 112, va("MONEY = ~%d~", stats.money), hiColor1:0xff_ff_00);
3893 // ////////////////////////////////////////////////////////////////////////// //
3894 private transient array!MapEntity renderVisibleCids;
3895 private transient array!MapEntity renderVisibleLights;
3896 private transient array!MapTile renderFrontTiles; // normal, with fg
3898 final bool renderSortByDepth (MapEntity oa, MapEntity ob) {
3899   auto da = oa.depth, db = ob.depth;
3900   if (da == db) return (oa.objId < ob.objId);
3901   return (da < db);
3905 const int RenderEdgePixNormal = 64;
3906 const int RenderEdgePixLight = 256;
3908 #ifndef EXPERIMENTAL_RENDER_CACHE
3909 enum skipListCreation = false;
3910 #endif
3912 void renderWithOfs (int xofs, int yofs, float currFrameDelta) {
3913   int scale = global.scale;
3915   // don't touch framebuffer alpha
3916   Video.colorMask = Video::CMask.Colors;
3917   Video.color = 0xff_ff_ff;
3919   /*
3920   Video::ScissorRect scsave;
3921   bool doRestoreGL = false;
3923   if (viewOffsetX > 0 || viewOffsetY > 0) {
3924     doRestoreGL = true;
3925     Video.getScissor(scsave);
3926     Video.scissorCombine(viewOffsetX, viewOffsetY, viewWidth, viewHeight);
3927     Video.glPushMatrix();
3928     Video.glTranslate(viewOffsetX, viewOffsetY);
3929     //Video.glTranslate(-550, 0);
3930     //Video.glScale(1, 1);
3931   }
3932   */
3935   bool isDarkLevel = global.darkLevel;
3937   if (isDarkLevel) {
3938     switch (global.config.scumPlayerLit) {
3939       case 0: player.lightRadius = 0; break; // never
3940       case 1: // only in "scumDarkness"
3941         player.lightRadius = (global.config.scumDarkness >= 2 ? 96 : 32);
3942         break;
3943       case 2:
3944         player.lightRadius = 96;
3945         break;
3946     }
3947   }
3949   // render cave background
3950   if (levBGImg) {
3951     int tsz = 16*scale;
3952     int bgw = levBGImg.tex.width*scale;
3953     int bgh = levBGImg.tex.height*scale;
3954     int bgTW = (tilesWidth*tsz+bgw-1)/bgw;
3955     int bgTH = (tilesHeight*tsz+bgh-1)/bgh;
3956     int bgX0 = max(0, xofs/bgw);
3957     int bgY0 = max(0, yofs/bgh);
3958     int bgX1 = min(bgTW, (xofs+viewWidth+bgw-1)/bgw);
3959     int bgY1 = min(bgTH, (yofs+viewHeight+bgh-1)/bgh);
3960     foreach (int ty; bgY0..bgY1) {
3961       foreach (int tx; bgX0..bgX1) {
3962         int x0 = tx*bgw-xofs;
3963         int y0 = ty*bgh-yofs;
3964         levBGImg.tex.blitAt(x0, y0, scale);
3965       }
3966     }
3967   }
3969   int RenderEdgePix = (global.darkLevel ? RenderEdgePixLight : RenderEdgePixNormal);
3971   // render background tiles
3972   for (MapBackTile bt = backtiles; bt; bt = bt.next) {
3973     bt.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3974   }
3976   // collect visible special tiles
3977 #ifdef EXPERIMENTAL_RENDER_CACHE
3978   bool skipListCreation = (lastRenderTime == time && renderVisibleCids.length); //FIXME
3979 #endif
3981   if (!skipListCreation) {
3982     renderVisibleCids.clear();
3983     renderVisibleLights.clear();
3984     renderFrontTiles.clear();
3986     int endVX = xofs+viewWidth;
3987     int endVY = yofs+viewHeight;
3989     // add player
3990     //int cnt = 0;
3991     if (player && player.visible) renderVisibleCids[$] = player; // include player in render list
3993     //FIXME: drop lit objects which cannot affect visible area
3994     if (scale > 1) {
3995       // collect visible objects
3996       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)) {
3997         if (!o.visible) continue;
3998         auto tile = MapTile(o);
3999         if (tile) {
4000           if (isDarkLevel && (o.lightRadius > 4 || tile.litWholeTile)) renderVisibleLights[$] = o;
4001           if (tile.invisible) continue;
4002           if (tile.bgfront /*|| tile.spriteLeftDeco || tile.spriteRightDeco*/) renderFrontTiles[$] = tile;
4003           if (tile.bgback) tile.drawWithOfsBack(xofs, yofs, scale, currFrameDelta);
4004         } else {
4005           if (isDarkLevel && o.lightRadius > 4) renderVisibleLights[$] = o;
4006         }
4007         // check if the object is really visible -- this will speed up later sorting
4008         int fx0, fy0, fx1, fy1;
4009         auto spf = o.getSpriteFrame(default, out fx0, out fy0, out fx1, out fy1);
4010         if (!spf) continue; // no sprite -- nothing to draw (no, really)
4011         int ix = o.ix, iy = o.iy;
4012         int x0 = (ix+fx0)*scale, y0 = (iy+fy0)*scale;
4013         int x1 = (ix+fx1)*scale, y1 = (iy+fy1)*scale;
4014         if (x1 <= xofs || y1 <= yofs || x0 >= endVX || y0 >= endVY) {
4015           //++cnt;
4016           continue;
4017         }
4018         renderVisibleCids[$] = o;
4019       }
4020     } else {
4021       foreach (MapEntity o; objGrid.allObjects()) {
4022         if (!o.visible) continue;
4023         auto tile = MapTile(o);
4024         if (tile) {
4025           if (isDarkLevel && (o.lightRadius > 4 || tile.litWholeTile)) renderVisibleLights[$] = o;
4026           if (tile.invisible) continue;
4027           if (tile.bgfront /*|| tile.spriteLeftDeco || tile.spriteRightDeco*/) renderFrontTiles[$] = tile;
4028           if (tile.bgback) tile.drawWithOfsBack(xofs, yofs, scale, currFrameDelta);
4029         } else {
4030           if (isDarkLevel && o.lightRadius > 4) renderVisibleLights[$] = o;
4031         }
4032         renderVisibleCids[$] = o;
4033       }
4034     }
4035     //writeln("::: ", cnt, " invisible objects dropped");
4037     renderVisibleCids.sort(&renderSortByDepth);
4038     lastRenderTime = time;
4039   }
4041   auto depth4Start = 0;
4042   foreach (auto xidx, MapEntity o; renderVisibleCids) {
4043     if (o.depth >= 4) {
4044       depth4Start = xidx;
4045       break;
4046     }
4047   }
4049   bool playerPowerupRendered = false;
4051   // render objects (part one: depth > 3)
4052   foreach (auto idx; depth4Start..renderVisibleCids.length; reverse) {
4053     MapEntity o = renderVisibleCids[idx];
4054     // 1000 is an ordinary tile
4055     if (!playerPowerupRendered && o.depth <= 1200) {
4056       playerPowerupRendered = true;
4057       // so ducking player will have it's cape correctly rendered
4058       if (player.visible) player.drawPrePrePowerupWithOfs(xofs, yofs, scale, currFrameDelta);
4059     }
4060     //if (idx && renderSortByDepth(o, renderVisibleCids[idx-1])) writeln("VIOLATION!");
4061     o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
4062   }
4064   // render object (part two: front tile parts, depth 3.5)
4065   foreach (MapTile tile; renderFrontTiles) {
4066     tile.drawWithOfsFront(xofs, yofs, scale, currFrameDelta);
4067   }
4069   // render objects (part three: depth <= 3)
4070   foreach (auto idx; 0..depth4Start; reverse) {
4071     MapEntity o = renderVisibleCids[idx];
4072     o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
4073     //done above;if (isDarkLevel && (o.lightRadius > 4 || (o isa MapTile && MapTile(o).litWholeTile))) renderVisibleLights[$] = o;
4074   }
4076   // render player post-effects, so cape or jetpack will be correctly rendered on level exit, for example
4077   player.lastDrawWithOfs(xofs, yofs, scale, currFrameDelta);
4079   // lighting
4080   if (isDarkLevel) {
4081     auto ltex = bgtileStore.lightTexture('ltx512', 512);
4083     // set screen alpha to min
4084     Video.colorMask = Video::CMask.Alpha;
4085     Video.blendMode = Video::BlendMode.None;
4086     Video.color = 0xff_ff_ff_ff;
4087     Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
4088     //Video.colorMask = Video::CMask.All;
4090     // blend lights
4091     // also, stencil 'em, so we can filter dark areas
4092     Video.textureFiltering = true;
4093     Video.stencil = true;
4094     Video.stencilFunc(Video::StencilFunc.Always, 1);
4095     Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Replace);
4096     Video.alphaTestFunc = Video::AlphaFunc.Greater;
4097     Video.alphaTestVal = 0.03+0.011*global.config.darknessDarkness;
4098     Video.color = 0xff_ff_ff;
4099     Video.blendFunc = Video::BlendFunc.Max;
4100     Video.blendMode = Video::BlendMode.Blend; // anything except `Normal`
4101     Video.colorMask = Video::CMask.Alpha;
4103     foreach (MapEntity e; renderVisibleLights) {
4104       int xi, yi;
4105       e.getInterpCoords(currFrameDelta, scale, out xi, out yi);
4106       auto tile = MapTile(e);
4107       if (tile && tile.litWholeTile) {
4108         //Video.color = 0xff_ff_ff;
4109         Video.fillRect(xi-xofs, yi-yofs, e.width*scale, e.height*scale);
4110       }
4111       int lrad = e.lightRadius;
4112       if (lrad < 4) continue; // just in case
4113       lrad += 8;
4114       float lightscale = float(lrad*scale)/float(ltex.tex.width);
4115 #ifdef OLD_LIGHT_OFFSETS
4116       int fx0, fy0, fx1, fy1;
4117       bool doMirror;
4118       auto spf = e.getSpriteFrame(out doMirror, out fx0, out fy0, out fx1, out fy1);
4119       if (spf) {
4120         xi += (fx1-fx0)*scale/2;
4121         yi += (fy1-fy0)*scale/2;
4122       }
4123 #else
4124       int lxofs, lyofs;
4125       e.getLightOffset(out lxofs, out lyofs);
4126       xi += lxofs*scale;
4127       yi += lyofs*scale;
4129 #endif
4130       lrad = lrad*scale/2;
4131       xi -= xofs+lrad;
4132       yi -= yofs+lrad;
4133       ltex.tex.blitAt(xi, yi, lightscale);
4134     }
4135     Video.textureFiltering = false;
4137     // modify only lit parts
4138     Video.stencilFunc(Video::StencilFunc.Equal, 1);
4139     Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Keep);
4140     // multiply framebuffer colors by framebuffer alpha
4141     Video.color = 0xff_ff_ff; // it doesn't matter
4142     Video.blendFunc = Video::BlendFunc.Add;
4143     Video.blendMode = Video::BlendMode.DstMulDstAlpha;
4144     Video.colorMask = Video::CMask.Colors;
4145     Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
4147     // filter unlit parts
4148     Video.stencilFunc(Video::StencilFunc.NotEqual, 1);
4149     Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Keep);
4150     Video.blendFunc = Video::BlendFunc.Add;
4151     Video.blendMode = Video::BlendMode.Filter;
4152     Video.colorMask = Video::CMask.Colors;
4153     Video.color = 0x00_00_18+0x00_00_10*global.config.darknessDarkness;
4154     //Video.color = 0x00_00_18;
4155     //Video.color = 0x00_00_38;
4156     Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
4158     // restore defaults
4159     Video.blendFunc = Video::BlendFunc.Add;
4160     Video.blendMode = Video::BlendMode.Normal;
4161     Video.colorMask = Video::CMask.All;
4162     Video.alphaTestFunc = Video::AlphaFunc.Always;
4163     Video.stencil = false;
4164   }
4166   // clear visible objects list (nope)
4167   //renderVisibleCids.clear();
4168   //renderVisibleLights.clear();
4171   if (global.config.drawHUD) renderHUD(currFrameDelta);
4172   renderCompass(currFrameDelta);
4174   float osdTimeLeft, osdTimeStart;
4175   string msg = osdGetMessage(out osdTimeLeft, out osdTimeStart);
4176   if (msg) {
4177     auto ct = GetTickCount();
4178     int msgScale = 3;
4179     sprStore.loadFont('sFontSmall');
4180     auto msgHeight = sprStore.getMultilineTextHeight(msg, msgScale);
4181     int x = viewWidth/2;
4182     int y = viewHeight-64-msgHeight;
4183     auto oldColor = Video.color;
4184     Video.color = 0xff_ff_00;
4185     if (osdTimeLeft < 0.5) {
4186       int alpha = 255-clamp(int(510*osdTimeLeft), 0, 255);
4187       Video.color = Video.color|(alpha<<24);
4188     } else if (ct-osdTimeStart < 0.5) {
4189       osdTimeStart = ct-osdTimeStart;
4190       int alpha = 255-clamp(int(510*osdTimeStart), 0, 255);
4191       Video.color = Video.color|(alpha<<24);
4192     }
4193     sprStore.renderMultilineTextCentered(x, y, msg, msgScale, 0x00_ff_00, 0xff_ff_ff);
4194     Video.color = oldColor;
4195   }
4197   int hiColor1, hiColor2;
4198   msg = osdGetTalkMessage(out hiColor1, out hiColor2);
4199   if (msg) {
4200     int msgScale = 2;
4201     sprStore.loadFont('sFontSmall');
4202     auto msgWidth = sprStore.getMultilineTextWidth(msg, processHighlights1:true, processHighlights2:true);
4203     auto msgHeight = sprStore.getMultilineTextHeight(msg);
4204     auto msgWidthOrig = msgWidth*msgScale;
4205     auto msgHeightOrig = msgHeight*msgScale;
4206     if (msgWidth%16 != 0) msgWidth = (msgWidth|0x0f)+1;
4207     if (msgHeight%16 != 0) msgHeight = (msgHeight|0x0f)+1;
4208     msgWidth *= msgScale;
4209     msgHeight *= msgScale;
4210     int x = (viewWidth-msgWidth)/2;
4211     int y = 32*msgScale;
4212     auto oldColor = Video.color;
4213     // draw text frame and text background
4214     Video.color = 0;
4215     Video.fillRect(x, y, msgWidth, msgHeight);
4216     Video.color = 0xff_ff_ff;
4217     for (int fdx = 0; fdx < msgWidth; fdx += 16*msgScale) {
4218       auto spf = sprStore['sMenuTop'].frames[0];
4219       spf.tex.blitAt(x+fdx, y-16*msgScale, msgScale);
4220       spf = sprStore['sMenuBottom'].frames[0];
4221       spf.tex.blitAt(x+fdx, y+msgHeight, msgScale);
4222     }
4223     for (int fdy = 0; fdy < msgHeight; fdy += 16*msgScale) {
4224       auto spf = sprStore['sMenuLeft'].frames[0];
4225       spf.tex.blitAt(x-16*msgScale, y+fdy, msgScale);
4226       spf = sprStore['sMenuRight'].frames[0];
4227       spf.tex.blitAt(x+msgWidth, y+fdy, msgScale);
4228     }
4229     {
4230       auto spf = sprStore['sMenuUL'].frames[0];
4231       spf.tex.blitAt(x-16*msgScale, y-16*msgScale, msgScale);
4232       spf = sprStore['sMenuUR'].frames[0];
4233       spf.tex.blitAt(x+msgWidth, y-16*msgScale, msgScale);
4234       spf = sprStore['sMenuLL'].frames[0];
4235       spf.tex.blitAt(x-16*msgScale, y+msgHeight, msgScale);
4236       spf = sprStore['sMenuLR'].frames[0];
4237       spf.tex.blitAt(x+msgWidth, y+msgHeight, msgScale);
4238     }
4239     Video.color = 0xff_ff_00;
4240     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));
4241     Video.color = oldColor;
4242   }
4244   if (inWinCutscene) renderWinCutsceneOverlay();
4245   if (inIntroCutscene) renderTitleCutsceneOverlay();
4246   if (isTransitionRoom()) renderTransitionOverlay();
4248   /*
4249   if (doRestoreGL) {
4250     Video.setScissor(scsave);
4251     Video.glPopMatrix();
4252   }
4253   */
4255   Video.color = 0xff_ff_ff;
4259 // ////////////////////////////////////////////////////////////////////////// //
4260 final class!MapObject findGameObjectClassByName (name aname) {
4261   if (!aname) return none; // just in case
4262   auto co = FindClassByGameObjName(aname);
4263   if (!co) {
4264     writeln("***UNKNOWN GAME OBJECT: '", aname, "'");
4265     return none;
4266   }
4267   co = GetClassReplacement(co);
4268   if (!co) FatalError("findGameObjectClassByName: WTF?!");
4269   if (!class!MapObject(co)) FatalError("findGameTileClassByName: object '%n' is not a map object, it is a `%n`", aname, GetClassName(co));
4270   return class!MapObject(co);
4274 final class!MapTile findGameTileClassByName (name aname) {
4275   if (!aname) return none; // just in case
4276   auto co = FindClassByGameObjName(aname);
4277   if (!co) return MapTile; // unknown names will be routed directly to tile object
4278   co = GetClassReplacement(co);
4279   if (!co) FatalError("findGameTileClassByName: WTF?!");
4280   if (!class!MapTile(co)) FatalError("findGameTileClassByName: object '%n' is not a tile, it is a `%n`", aname, GetClassName(co));
4281   return class!MapTile(co);
4285 final MapObject findAnyObjectOfType (name aname) {
4286   if (!aname) return none;
4287   auto cls = FindClassByGameObjName(aname);
4288   if (!cls) return none;
4289   foreach (MapObject obj; objGrid.allObjects(MapObject)) {
4290     if (obj.spectral) continue;
4291     if (obj isa cls) return obj;
4292   }
4293   return none;
4297 // ////////////////////////////////////////////////////////////////////////// //
4298 final bool isRopePlacedAt (int x, int y) {
4299   int[8] covered;
4300   foreach (ref auto v; covered) v = false;
4301   foreach (MapTile t; objGrid.inRectPix(x, y-8, 1, 17, precise:false, castClass:MapTileRope)) {
4302     //if (!cbIsRopeTile(t)) continue;
4303     if (t.ix != x) continue;
4304     if (t.iy == y) return true;
4305     foreach (int ty; t.iy..t.iy+8) {
4306       int d = ty-y;
4307       if (d >= 0 && d < covered.length) covered[d] = true;
4308     }
4309   }
4310   // check if the whole rope height is completely covered with ropes
4311   foreach (auto v; covered) if (!v) return false;
4312   return true;
4316 final MapTile CreateMapTile (int xpos, int ypos, name aname) {
4317   if (!aname) FatalError("cannot create typeless tile");
4318   auto tclass = findGameTileClassByName(aname);
4319   if (!tclass) return none;
4320   MapTile tile = SpawnObject(tclass);
4321   tile.global = global;
4322   tile.level = self;
4323   tile.objName = aname;
4324   tile.objType = aname; // just in case
4325   tile.fltx = xpos;
4326   tile.flty = ypos;
4327   tile.objId = ++lastUsedObjectId;
4328   if (!tile.initialize()) FatalError("cannot create tile '%n'", aname);
4329   return tile;
4333 final bool PutSpawnedMapTile (int x, int y, MapTile tile) {
4334   if (!tile || !tile.isInstanceAlive) return false;
4336   //if (putToGrid) tile.active = true;
4337   bool putToGrid = (tile.moveable || tile.toSpecialGrid || tile.width != 16 || tile.height != 16 || x%16 != 0 || y%16 != 0);
4339   //writeln("putting tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (putToGrid ? "special " : ""), "grid, pos=(", x/16.0, ",", y/16.0, ")");
4341   if (!putToGrid) {
4342     int mapx = x/16, mapy = y/16;
4343     if (mapx < 0 || mapx >= tilesWidth || mapy < 0 || mapy >= tilesHeight) return false;
4344   }
4346   // if we already have rope tile there, there is no reason to add another one
4347   if (tile isa MapTileRope) {
4348     if (isRopePlacedAt(x, y)) return false;
4349   }
4351   // activate special or animated tile
4352   tile.active = tile.active || tile.moveable || tile.toSpecialGrid;
4353   // animated tiles must be active
4354   if (!tile.active) {
4355     auto spr = tile.getSprite();
4356     if (spr && spr.frames.length > 1) {
4357       writeln("activated animated tile '", tile.objName, "'");
4358       tile.active = true;
4359     }
4360   }
4362   tile.fltx = x;
4363   tile.flty = y;
4364   if (putToGrid) {
4365     //if (tile isa TitleTileCopy) writeln("*** PUTTING COPYRIGHT TILE");
4366     //tile.toSpecialGrid = true;
4367     if (!tile.dontReplaceOthers && x&16 == 0 && y%16 == 0) {
4368       auto t = getTileAtGridAny(x/16, y/16);
4369       if (t && !t.immuneToReplacement) {
4370         writeln("REPLACING tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
4371         writeln("      NEW tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (tile.toSpecialGrid ? "special " : ""), "grid, pos=(", tile.fltx/16.0, ",", tile.flty/16.0, ")");
4372         t.instanceRemove();
4373       }
4374     }
4375     objGrid.insert(tile);
4376   } else {
4377     //writeln("SIZE: ", tilesWidth, "x", tilesHeight);
4378     setTileAtGrid(x/16, y/16, tile);
4379     /*
4380     auto t = getTileAtGridAny(x/16, y/16);
4381     if (t != tile) {
4382       writeln("putting tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (putToGrid ? "special " : ""), "grid, pos=(", x/16.0, ",", y/16.0, ")");
4383       checkTilesInRect(x/16, y/16, 16, 16, delegate bool (MapTile tile) {
4384         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, ")");
4385         return false;
4386       });
4387       FatalError("FUUUUUU");
4388     }
4389     */
4390   }
4392   if (tile.enter) registerEnter(tile);
4393   if (tile.exit) registerExit(tile);
4395   // make tile under exit invulnerable
4396   if (checkTilesInRect(tile.ix, tile.iy-16, 16, 16, delegate bool (MapTile t) { return t.exit; })) {
4397     tile.invincible = true;
4398   }
4400   return true;
4404 // won't call `onDestroy()`
4405 final void RemoveMapTileFromGrid (int tileX, int tileY, optional string reason) {
4406   if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
4407     auto t = getTileAtGridAny(tileX, tileY);
4408     if (t) {
4409       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, ")");
4410       t.instanceRemove();
4411       checkWater = true;
4412     }
4413   }
4417 final MapTile MakeMapTile (int mapx, int mapy, name aname) {
4418   //writeln("tile at (", mapx, ",", mapy, "): ", aname);
4419   //if (aname == 'oLush') { MapObject fail; fail.initialize(); }
4420   //if (mapy >= tilesHeight) { writeln("OOM: ", aname, "; ty=", mapy, "; hgt=", tilesHeight); }
4421   if (mapx < 0 || mapx >= tilesWidth || mapy < 0 || mapy >= tilesHeight) return none;
4423   // if we already have rope tile there, there is no reason to add another one
4424   if (aname == 'oRope') {
4425     if (isRopePlacedAt(mapx*16, mapy*16)) return none;
4426   }
4428   auto tile = CreateMapTile(mapx*16, mapy*16, aname);
4429   if (!tile) return none;
4430   if (!PutSpawnedMapTile(mapx*16, mapy*16, tile)) {
4431     delete tile;
4432     tile = none;
4433   }
4435   return tile;
4439 final MapTile MakeMapTileAtPix (int xpix, int ypix, name aname) {
4440   // if we already have rope tile there, there is no reason to add another one
4441   if (aname == 'oRope') {
4442     if (isRopePlacedAt(xpix, ypix)) return none;
4443   }
4445   auto tile = CreateMapTile(xpix, ypix, aname);
4446   if (!tile) return none;
4447   if (!PutSpawnedMapTile(xpix, ypix, tile)) {
4448     delete tile;
4449     tile = none;
4450   }
4452   return tile;
4456 final MapTile MakeMapRopeTileAt (int x0, int y0) {
4457   // if we already have rope tile there, there is no reason to add another one
4458   if (isRopePlacedAt(x0, y0)) return none;
4460   auto tile = CreateMapTile(x0, y0, 'oRope');
4461   if (!PutSpawnedMapTile(x0, y0, tile)) {
4462     delete tile;
4463     tile = none;
4464   }
4466   return tile;
4470 // ////////////////////////////////////////////////////////////////////////// //
4471 final MapBackTile CreateBgTile (name sprName, optional int atx0, optional int aty0, optional int aw, optional int ah) {
4472   BackTileImage img = bgtileStore[sprName];
4473   auto res = SpawnObject(MapBackTile);
4474   res.global = global;
4475   res.level = self;
4476   res.bgt = img;
4477   res.bgtName = sprName;
4478   if (specified_atx0) res.tx0 = atx0;
4479   if (specified_aty0) res.ty0 = aty0;
4480   if (specified_aw) res.w = aw;
4481   if (specified_ah) res.h = ah;
4482   if (!res.initialize()) FatalError("cannot create background tile with image '%n'", sprName);
4483   return res;
4487 // ////////////////////////////////////////////////////////////////////////// //
4489 background The background asset from which the new tile will be extracted.
4490 left The x coordinate of the left of the new tile, relative to the background asset's top left corner.
4491 top The y coordinate of the top of the new tile, relative to the background assets top left corner.
4492 width The width of the tile.
4493 height The height of the tile.
4494 x The x position in the room to place the tile.
4495 y The y position in the room to place the tile.
4496 depth The depth at which to place the tile.
4498 final void MakeMapBackTile (name bgname, int left, int top, int width, int height, int x, int y, int depth) {
4499   if (width < 1 || height < 1 || !bgname) return;
4500   auto bgt = bgtileStore[bgname];
4501   if (!bgt) FatalError("cannot load background '%n'", bgname);
4502   MapBackTile bt = SpawnObject(MapBackTile);
4503   bt.global = global;
4504   bt.level = self;
4505   bt.objName = bgname;
4506   bt.bgt = bgt;
4507   bt.bgtName = bgname;
4508   bt.fltx = x;
4509   bt.flty = y;
4510   bt.tx0 = left;
4511   bt.ty0 = top;
4512   bt.w = width;
4513   bt.h = height;
4514   bt.depth = depth;
4515   // find a place for it
4516   if (!backtiles) {
4517     backtiles = bt;
4518     return;
4519   }
4520   // back tiles with the highest depth should come first
4521   MapBackTile ct = backtiles, cprev = none;
4522   while (ct && depth <= ct.depth) { cprev = ct; ct = ct.next; }
4523   // insert before ct
4524   if (cprev) {
4525     bt.next = cprev.next;
4526     cprev.next = bt;
4527   } else {
4528     bt.next = backtiles;
4529     backtiles = bt;
4530   }
4534 // ////////////////////////////////////////////////////////////////////////// //
4535 final MapObject SpawnMapObjectWithClass (class!MapObject oclass) {
4536   if (!oclass) return none;
4538   MapObject obj = SpawnObject(oclass);
4539   if (!obj) FatalError("cannot create map object '%n'", GetClassName(oclass));
4541   //if (aname == 'oShells' || aname == 'oShellSingle' || aname == 'oRopePile') writeln("*** CREATED '", aname, "'");
4543   obj.global = global;
4544   obj.level = self;
4545   obj.objId = ++lastUsedObjectId;
4547   return obj;
4551 final MapObject SpawnMapObject (name aname) {
4552   if (!aname) return none;
4553   auto res = SpawnMapObjectWithClass(findGameObjectClassByName(aname));
4554   if (res && !res.objType) res.objType = aname; // just in case
4555   return res;
4559 final MapObject PutSpawnedMapObject (int x, int y, MapObject obj) {
4560   if (!obj /*|| obj.global || obj.level*/) return none; // oops
4562   obj.fltx = x;
4563   obj.flty = y;
4564   if (!obj.initialize()) { delete obj; return none; } // not fatal
4566   insertObject(obj);
4567   if (obj.walkableSolid) hasSolidObjects = true;
4569   return obj;
4573 final MapObject MakeMapObject (int x, int y, name aname) {
4574   MapObject obj = SpawnMapObject(aname);
4575   obj = PutSpawnedMapObject(x, y, obj);
4576   return obj;
4580 // ////////////////////////////////////////////////////////////////////////// //
4581 void setMenuTilesVisible (bool vis) {
4582   if (vis) {
4583     forEachTile(delegate bool (MapTile t) {
4584       if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4585         t.invisible = false;
4586       }
4587       return false;
4588     });
4589   } else {
4590     forEachTile(delegate bool (MapTile t) {
4591       if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4592         t.invisible = true;
4593       }
4594       return false;
4595     });
4596   }
4600 void setMenuTilesOnTop () {
4601   forEachTile(delegate bool (MapTile t) {
4602     if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4603       t.depth = 1;
4604     }
4605     return false;
4606   });
4610 // ////////////////////////////////////////////////////////////////////////// //
4611 #include "roomTitle.vc"
4612 #include "roomTrans1.vc"
4613 #include "roomTrans2.vc"
4614 #include "roomTrans3.vc"
4615 #include "roomTrans4.vc"
4616 #include "roomOlmec.vc"
4617 #include "roomEnd.vc"
4618 #include "roomIntro.vc"
4619 #include "roomTutorial.vc"
4620 #include "roomScores.vc"
4621 #include "roomStars.vc"
4622 #include "roomSun.vc"
4623 #include "roomMoon.vc"
4626 // ////////////////////////////////////////////////////////////////////////// //
4627 #include "packages/Generator/loadRoomGens.vc"
4628 #include "packages/Generator/loadEntityGens.vc"