better check for dead players in kapala/treasure code
[k8vacspelynky.git] / GameLevel.vc
blobc0dc18f8436e0f1925cca021463d0ca4a574e706
1 /**********************************************************************************
2  * Copyright (c) 2008, 2009 Derek Yu and Mossmouth, LLC
3  * Copyright (c) 2018, Ketmar Dark
4  *
5  * This file is part of Spelunky.
6  *
7  * You can redistribute and/or modify Spelunky, including its source code, under
8  * the terms of the Spelunky User License.
9  *
10  * Spelunky is distributed in the hope that it will be entertaining and useful,
11  * but WITHOUT WARRANTY.  Please see the Spelunky User License for more details.
12  *
13  * The Spelunky User License should be available in "Game .Information", which
14  * can be found in the Resource Explorer, or as an external file called COPYING.
15  * If not, please obtain a new copy of Spelunky from <http://spelunkyworld.com/>
16  *
17  **********************************************************************************/
18 // this is the level we're playing in, with all objects and tiles
19 class GameLevel : Object;
21 //#define EXPERIMENTAL_RENDER_CACHE
23 const float FrameTime = 1.0f/30.0f;
25 const int dumpGridStats = true;
27 struct IVec2D {
28   int x, y;
31 // in tiles
32 //enum NormalTilesWidth = LevelGen::LevelWidth*RoomGen::Width+2;
33 //enum NormalTilesHeight = LevelGen::LevelHeight*RoomGen::Height+2;
35 enum MaxTilesWidth = 64;
36 enum MaxTilesHeight = 64;
38 GameGlobal global;
39 transient GameStats stats;
40 transient SpriteStore sprStore;
41 transient BackTileStore bgtileStore;
42 transient BackTileImage levBGImg;
43 name levBGImgName;
44 LevelGen lg;
45 transient name lastMusicName;
46 //RoomGen[LevelGen::LevelWidth, LevelGen::LevelHeight] rooms; // moved to levelgen
48 transient float accumTime;
49 transient bool gamePaused = false;
50 transient bool checkWater;
51 transient int liquidTileCount; // cached
52 transient int damselSaved;
54 // hud efffects
55 transient int xmoney;
56 transient int collectCounter;
57 transient int levelMoneyStart;
59 // all movable (thinkable) map objects
60 EntityGrid objGrid; // monsters, items and tiles
62 MapBackTile backtiles;
63 bool blockWaterChecking;
65 int inWinCutscene;
67 LevelGen::RType[MaxTilesWidth, MaxTilesHeight] roomType;
69 enum LevelKind {
70   Normal,
71   Transition,
72   Title,
73   Tutorial,
74   Scores,
75   Stars,
76   Sun,
77   Moon,
78   //Final,
80 LevelKind levelKind = LevelKind.Normal;
82 array!MapTile allEnters;
83 array!MapTile allExits;
86 int startRoomX, startRoomY;
87 int endRoomX, endRoomY;
89 PlayerPawn player;
90 transient bool playerExited;
91 transient MapEntity playerExitDoor;
92 transient bool disablePlayerThink = false;
93 transient int maxPlayingTime; // in seconds
94 int levelStartTime;
95 int levelEndTime;
97 int ghostTimeLeft;
98 int musicFadeTimer;
99 bool ghostSpawned; // to speed up some checks
100 bool resetBMCOG = false;
101 int udjatAlarm;
104 // FPS, i.e. incremented by 30 in one second
105 int time; // in frames
106 int lastUsedObjectId;
107 transient int lastRenderTime = -1;
109 MapEntity deadItemsHead;
111 // screen shake variables
112 int shakeLeft;
113 IVec2D shakeOfs;
114 IVec2D shakeDir;
116 // set this before calling `fixCamera()`
117 // dimensions should be real, not scaled up/down
118 transient int viewWidth, viewHeight;
119 // room bounds, not scaled
120 IVec2D viewMin, viewMax;
122 // for Olmec level cinematics
123 IVec2D cameraSlideToDest;
124 IVec2D cameraSlideToCurr;
125 IVec2D cameraSlideToSpeed; // !0: slide
126 int cameraSlideToPlayer;
127 // `fixCamera()` will set the following
128 // coordinates will be real too (with scale applied)
129 // shake is not applied
130 transient IVec2D viewStart; // with `player.viewOffset`
131 private transient IVec2D realViewStart; // without `player.viewOffset`
133 transient int framesProcessedFromLastClear;
135 transient int BuildYear;
136 transient int BuildMonth;
137 transient int BuildDay;
138 transient int BuildHour;
139 transient int BuildMin;
140 transient string BuildDateString;
143 final string getBuildDateString () {
144   if (!BuildYear) return BuildDateString;
145   if (BuildDateString) return BuildDateString;
146   BuildDateString = va("%d-%s-%s %s:%s", BuildYear, val2dig(BuildMonth), val2dig(BuildDay), val2dig(BuildHour), val2dig(BuildMin));
147   return BuildDateString;
151 final void cameraSlideToPoint (int dx, int dy, int speedx, int speedy) {
152   cameraSlideToPlayer = 0;
153   cameraSlideToDest.x = dx;
154   cameraSlideToDest.y = dy;
155   cameraSlideToSpeed.x = abs(speedx);
156   cameraSlideToSpeed.y = abs(speedy);
157   cameraSlideToCurr.x = cameraCurrX;
158   cameraSlideToCurr.y = cameraCurrY;
162 final void cameraReturnToPlayer () {
163   if (!cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y)) {
164     cameraSlideToCurr.x = cameraCurrX;
165     cameraSlideToCurr.y = cameraCurrY;
166     if (cameraSlideToSpeed.x && abs(cameraSlideToSpeed.x) < 8) cameraSlideToSpeed.x = 8;
167     if (cameraSlideToSpeed.y && abs(cameraSlideToSpeed.y) < 8) cameraSlideToSpeed.y = 8;
168     cameraSlideToPlayer = 1;
169   }
173 // if `frameSkip` is `true`, there are more frames waiting
174 // (i.e. you may skip rendering and such)
175 transient void delegate (bool frameSkip) onBeforeFrame;
176 transient void delegate (bool frameSkip) onAfterFrame;
178 transient void delegate () onCameraTeleported;
180 transient void delegate () onLevelExitedCB;
182 // this will be called in-between frames, and
183 // `frameTime` is [0..1)
184 transient void delegate (float frameTime) onInterFrame;
186 final int bizRoomStyle { get { return (lg ? lg.bizRoomStyle : 0); } }
189 final bool isNormalLevel () { return (levelKind == LevelKind.Normal); }
190 final bool isTitleRoom () { return (levelKind == LevelKind.Title); }
191 final bool isTutorialRoom () { return (levelKind == LevelKind.Tutorial); }
193 bool isHUDEnabled () {
194   if (inWinCutscene) return false;
195   if (lg.finalBossLevel) return true;
196   if (isNormalLevel()) return true;
197   // allow HUD in challenge chambers
198   return false;
202 // ////////////////////////////////////////////////////////////////////////// //
203 // stats
204 void addDeath (name aname) { if (isNormalLevel()) stats.addDeath(aname); }
206 int starsKills;
207 int sunScore;
208 int moonScore;
209 int moonTimer;
211 void addKill (name aname, optional bool telefrag) {
212        if (isNormalLevel()) stats.addKill(aname, telefrag!optional);
213   else if (aname == 'Shopkeeper' && levelKind == LevelKind.Stars) { ++stats.starsKills; ++starsKills; }
216 void addCollect (name aname, optional int amount) { if (isNormalLevel()) stats.addCollect(aname, amount!optional); }
218 void addDamselSaved () { if (isNormalLevel()) stats.addDamselSaved(); }
219 void addIdolStolen () { if (isNormalLevel()) stats.addIdolStolen(); }
220 void addIdolConverted () { if (isNormalLevel()) stats.addIdolConverted(); }
221 void addCrystalIdolStolen () { if (isNormalLevel()) stats.addCrystalIdolStolen(); }
222 void addCrystalIdolConverted () { if (isNormalLevel()) stats.addCrystalIdolConverted(); }
223 void addGhostSummoned () { if (isNormalLevel()) stats.addGhostSummoned(); }
226 // ////////////////////////////////////////////////////////////////////////// //
227 static final string val2dig (int n) {
228   return (n < 10 ? va("0%d", n) : va("%d", n));
232 static final string time2str (int time) {
233   int secs = time%60; time /= 60;
234   int mins = time%60; time /= 60;
235   int hours = time%24; time /= 24;
236   int days = time;
237   if (days) return va("%d DAYS, %d:%s:%s", days, hours, val2dig(mins), val2dig(secs));
238   if (hours) return va("%d:%s:%s", hours, val2dig(mins), val2dig(secs));
239   return va("%s:%s", val2dig(mins), val2dig(secs));
243 // ////////////////////////////////////////////////////////////////////////// //
244 final int tilesWidth () { return lg.levelRoomWidth*RoomGen::Width+2; }
245 final int tilesHeight () { return (lg.finalBossLevel ? 55 : lg.levelRoomHeight*RoomGen::Height+2); }
248 // ////////////////////////////////////////////////////////////////////////// //
249 protected void resetGameInternal () {
250   if (player) player.removeBallAndChain();
251   resetBMCOG = false;
252   inWinCutscene = 0;
253   shakeLeft = 0;
254   udjatAlarm = 0;
255   starsKills = 0;
256   sunScore = 0;
257   moonScore = 0;
258   moonTimer = 0;
259   damselSaved = 0;
260   xmoney = 0;
261   collectCounter = 0;
262   levelMoneyStart = 0;
263   if (player) {
264     player.removeBallAndChain();
265     auto hi = player.holdItem;
266     player.holdItem = none;
267     if (hi) hi.instanceRemove();
268     hi = player.pickedItem;
269     player.pickedItem = none;
270     if (hi) hi.instanceRemove();
271   }
272   time = 0;
273   lastRenderTime = -1;
274   levelStartTime = 0;
275   levelEndTime = 0;
276   global.resetGame();
277   stats.clearGameTotals();
281 // this won't generate a level yet
282 void restartGame () {
283   resetGameInternal();
284   if (global.startMoney > 0) stats.setMoneyCheat();
285   stats.setMoney(global.startMoney);
286   levelKind = LevelKind.Normal;
290 // complement function to `restart game`
291 void generateNormalLevel () {
292   generateLevel();
293   centerViewAtPlayer();
297 void restartTitle () {
298   resetGameInternal();
299   stats.setMoney(0);
300   createSpecialLevel(LevelKind.Title, &createTitleRoom, 'musTitle');
301   global.plife = 9999;
302   global.bombs = 0;
303   global.rope = 0;
304   global.arrows = 0;
305   global.sgammo = 0;
309 void restartTutorial () {
310   resetGameInternal();
311   stats.setMoney(0);
312   createSpecialLevel(LevelKind.Tutorial, &createTutorialRoom, 'musCave');
313   global.plife = 4;
314   global.bombs = 0;
315   global.rope = 4;
316   global.arrows = 0;
317   global.sgammo = 0;
321 void restartScores () {
322   resetGameInternal();
323   stats.setMoney(0);
324   createSpecialLevel(LevelKind.Scores, &createScoresRoom, 'musTitle');
325   global.plife = 4;
326   global.bombs = 0;
327   global.rope = 0;
328   global.arrows = 0;
329   global.sgammo = 0;
333 void restartStarsRoom () {
334   resetGameInternal();
335   stats.setMoney(0);
336   createSpecialLevel(LevelKind.Stars, &createStarsRoom, '');
337   global.plife = 8;
338   global.bombs = 0;
339   global.rope = 0;
340   global.arrows = 0;
341   global.sgammo = 0;
345 void restartSunRoom () {
346   resetGameInternal();
347   stats.setMoney(0);
348   createSpecialLevel(LevelKind.Sun, &createSunRoom, '');
349   global.plife = 8;
350   global.bombs = 0;
351   global.rope = 0;
352   global.arrows = 0;
353   global.sgammo = 0;
357 void restartMoonRoom () {
358   resetGameInternal();
359   stats.setMoney(0);
360   createSpecialLevel(LevelKind.Moon, &createMoonRoom, '');
361   global.plife = 8;
362   global.bombs = 0;
363   global.rope = 0;
364   global.arrows = 100;
365   global.sgammo = 0;
369 // ////////////////////////////////////////////////////////////////////////// //
370 // generate angry shopkeeper at exit if murderer or thief
371 void generateAngryShopkeepers () {
372   if (global.murderer || global.thiefLevel > 0) {
373     foreach (MapTile e; allExits) {
374       auto obj = MonsterShopkeeper(MakeMapObject(e.ix, e.iy, 'oShopkeeper'));
375       if (obj) {
376         obj.style = 'Bounty Hunter';
377         obj.status = MapObject::PATROL;
378       }
379     }
380   }
384 // ////////////////////////////////////////////////////////////////////////// //
385 final void resetRoomBounds () {
386   viewMin.x = 0;
387   viewMin.y = 0;
388   viewMax.x = tilesWidth*16;
389   viewMax.y = tilesHeight*16;
390   // Great Lake is bottomless (nope)
391   //if (global.lake == 1) viewMax.y -= 16;
392   //writeln("ROOM BOUNDS: (", viewMax.x, "x", viewMax.y, ")");
396 final void setRoomBounds (int x0, int y0, int x1, int y1) {
397   viewMin.x = x0;
398   viewMin.y = y0;
399   viewMax.x = x1+16;
400   viewMax.y = y1+16;
404 // ////////////////////////////////////////////////////////////////////////// //
405 struct OSDMessage {
406   string msg;
407   float timeout; // seconds
408   float starttime; // for active
409   bool active; // true: timeout is `GetTickCount()` dismissing time
412 array!OSDMessage msglist; // [0]: current one
415 private final void osdCheckTimeouts () {
416   auto stt = GetTickCount();
417   while (msglist.length) {
418     if (!msglist[0].active) {
419       msglist[0].active = true;
420       msglist[0].starttime = stt;
421     }
422     if (msglist[0].starttime+msglist[0].timeout >= stt) break;
423     msglist.remove(0);
424   }
428 final bool osdHasMessage () {
429   osdCheckTimeouts();
430   return (msglist.length > 0);
434 final string osdGetMessage (out float timeLeft, out float timeStart) {
435   osdCheckTimeouts();
436   if (msglist.length == 0) { timeLeft = 0; return ""; }
437   auto stt = GetTickCount();
438   timeStart = msglist[0].starttime;
439   timeLeft = fmax(0, msglist[0].starttime+msglist[0].timeout-stt);
440   return msglist[0].msg;
444 final void osdClear () {
445   msglist.clear();
449 final void osdMessage (string msg, optional float timeout, optional bool urgent) {
450   if (!msg) return;
451   msg = global.expandString(msg);
452   if (!specified_timeout) timeout = 3.33;
453   // special message for shops
454   if (timeout == -666) {
455     if (!msg) return;
456     if (msglist.length && msglist[0].msg == msg) return;
457     if (msglist.length == 0 || msglist[0].msg != msg) {
458       osdClear();
459       msglist.length += 1;
460       msglist[0].msg = msg;
461     }
462     msglist[0].active = false;
463     msglist[0].timeout = 3.33;
464     osdCheckTimeouts();
465     return;
466   }
467   if (timeout < 0.1) return;
468   timeout = fmax(1.0, timeout);
469   //writeln("OSD: ", msg);
470   // find existing one, and bring it to the top
471   int oldidx = 0;
472   for (; oldidx < msglist.length; ++oldidx) {
473     if (msglist[oldidx].msg == msg) break; // i found her!
474   }
475   // duplicate?
476   if (oldidx < msglist.length) {
477     // yeah, move duplicate to the top
478     msglist[oldidx].timeout = fmax(timeout, msglist[oldidx].timeout);
479     msglist[oldidx].active = false;
480     if (urgent && oldidx != 0) {
481       timeout = msglist[oldidx].timeout;
482       msglist.remove(oldidx);
483       msglist.insert(0);
484       msglist[0].msg = msg;
485       msglist[0].timeout = timeout;
486       msglist[0].active = false;
487     }
488   } else if (urgent) {
489     msglist.insert(0);
490     msglist[0].msg = msg;
491     msglist[0].timeout = timeout;
492     msglist[0].active = false;
493   } else {
494     // new one
495     msglist.length += 1;
496     msglist[$-1].msg = msg;
497     msglist[$-1].timeout = timeout;
498     msglist[$-1].active = false;
499   }
500   osdCheckTimeouts();
504 // ////////////////////////////////////////////////////////////////////////// //
505 void setup (GameGlobal aGlobal, SpriteStore aSprStore, BackTileStore aBGTileStore) {
506   global = aGlobal;
507   sprStore = aSprStore;
508   bgtileStore = aBGTileStore;
510   lg = SpawnObject(LevelGen);
511   lg.global = global;
512   lg.level = self;
514   objGrid = SpawnObject(EntityGrid);
515   objGrid.setup(0, 0, MaxTilesWidth*16, MaxTilesHeight*16);
519 // stores should be set
520 void onLoaded () {
521   checkWater = true;
522   liquidTileCount = 0;
523   levBGImg = bgtileStore[levBGImgName];
524   foreach (MapEntity o; objGrid.allObjects()) {
525     o.onLoaded();
526     auto t = MapTile(o);
527     if (t && (t.lava || t.water)) ++liquidTileCount;
528   }
529   for (MapBackTile bt = backtiles; bt; bt = bt.next) bt.onLoaded();
530   if (player) player.onLoaded();
531   //FIXME
532   if (msglist.length) {
533     msglist[0].active = false;
534     msglist[0].timeout = 0.200;
535     osdCheckTimeouts();
536   }
537   if (lg && lg.musicName) global.playMusic(lg.musicName); else global.stopMusic();
541 // ////////////////////////////////////////////////////////////////////////// //
542 void pickedSpectacles () {
543   foreach (MapTile t; objGrid.allObjectsSafe(MapTile)) t.onGotSpectacles();
547 // ////////////////////////////////////////////////////////////////////////// //
548 #include "rgentile.vc"
549 #include "rgenobj.vc"
552 void onLevelExited () {
553   if (playerExitDoor isa TitleTileXTitle) {
554     playerExitDoor = none;
555     restartTitle();
556     return;
557   }
558   // title
559   if (isTitleRoom() || levelKind == LevelKind.Scores) {
560     if (playerExitDoor) processTitleExit(playerExitDoor);
561     playerExitDoor = none;
562     return;
563   }
564   if (isTutorialRoom()) {
565     playerExitDoor = none;
566     restartGame();
567     global.currLevel = 1;
568     generateNormalLevel();
569     return;
570   }
571   // challenges
572   if (levelKind == LevelKind.Stars || levelKind == LevelKind.Sun || levelKind == LevelKind.Moon) {
573     playerExitDoor = none;
574     levelEndTime = time;
575     if (onLevelExitedCB) onLevelExitedCB();
576     restartTitle();
577     return;
578   }
579   // normal level
580   if (isNormalLevel()) {
581     stats.maxLevelComplete = max(stats.maxLevelComplete, global.currLevel);
582     levelEndTime = time;
583     if (playerExitDoor) {
584       if (playerExitDoor.objType == 'oXGold') {
585         writeln("exiting to City Of Gold");
586         global.cityOfGold = true;
587         //!global.currLevel += 1;
588       } else if (playerExitDoor.objType == 'oXMarket') {
589         writeln("exiting to Black Market");
590         global.genBlackMarket = true;
591         //!global.currLevel += 1;
592       }
593     }
594   }
595   if (onLevelExitedCB) onLevelExitedCB();
596   //
597   playerExitDoor = none;
598   if (levelKind == LevelKind.Transition) {
599     if (global.thiefLevel > 0) global.thiefLevel -= 1;
600     if (global.alienCraft) ++global.alienCraft;
601     if (global.yetiLair) ++global.yetiLair;
602     if (global.lake) ++global.lake;
603     if (global.cityOfGold) ++global.cityOfGold;
604     //orig dbg:if (global.currLevel == 1) global.currLevel += firstLevelSkip; else global.currLevel += levelSkip;
605     /+
606     if (!global.blackMarket && !global.cityOfGold /*&& !global.yetiLair*/) {
607       global.currLevel += 1;
608     }
609     +/
610     ++global.currLevel;
611     generateLevel();
612   } else {
613     // < 20 seconds per level: looks like a speedrun
614     global.noDarkLevel = (levelEndTime > levelStartTime && levelEndTime-levelStartTime < 20*30);
615     if (lg.finalBossLevel) {
616       winTime = time;
617       ++stats.gamesWon;
618       // add money for big idol
619       player.addScore(50000);
620       stats.gameOver();
621       startWinCutscene();
622     } else {
623       generateTransitionLevel();
624     }
625   }
626   //centerViewAtPlayer();
630 void onOlmecDead (MapObject o) {
631   writeln("*** OLMEC IS DEAD!");
632   foreach (MapTile t; allExits) {
633     if (t.exit) {
634       t.openExit();
635       auto st = checkTileAtPoint(t.ix+8, t.iy+16);
636       if (!st) {
637         st = MakeMapTile(t.ix/16, t.iy/16+1, 'oTemple');
638         st.ore = 0;
639       }
640       st.invincible = true;
641     }
642   }
646 void generateLevelMessages () {
647   writeln("LEVEL NUMBER: ", global.currLevel);
648   if (global.darkLevel) {
649     if (global.hasCrown) {
650        osdMessage("THE HEDJET SHINES BRIGHTLY.");
651        global.darkLevel = false;
652     } else if (global.config.scumDarkness < 2) {
653       osdMessage("I CAN'T SEE A THING!\nI'D BETTER USE THESE FLARES!");
654     }
655   }
657   if (global.blackMarket) osdMessage("WELCOME TO THE BLACK MARKET!");
659   if (global.cemetary) osdMessage("THE DEAD ARE RESTLESS!");
660   if (global.lake == 1) osdMessage("I CAN HEAR RUSHING WATER...");
662   if (global.snakePit) osdMessage("I HEAR SNAKES... I HATE SNAKES!");
663   if (global.yetiLair == 1) osdMessage("IT SMELLS LIKE WET FUR IN HERE!");
664   if (global.alienCraft == 1) osdMessage("THERE'S A PSYCHIC PRESENCE HERE!");
665   if (global.cityOfGold == 1) {
666     if (true /*hasOlmec()*/) osdMessage("IT'S THE LEGENDARY CITY OF GOLD!");
667   }
669   if (global.sacrificePit) osdMessage("I CAN HEAR PRAYERS TO KALI!");
673 final MapObject debugSpawnObjectWithClass (class!MapObject oclass, optional bool playerDir) {
674   if (!oclass) return none;
675   int dx = 0, dy = 0;
676   bool canLeft = !isSolidAtPoint(player.ix-8, player.iy);
677   bool canRight = !isSolidAtPoint(player.ix+16, player.iy);
678   if (!canLeft && !canRight) return none;
679   if (canLeft && canRight) {
680     if (playerDir) {
681       dx = (player.dir == MapObject::Dir.Left ? -16 : 16);
682     } else {
683       dx = 16;
684     }
685   } else {
686     dx = (canLeft ? -16 : 16);
687   }
688   auto obj = SpawnMapObjectWithClass(oclass);
689   if (obj isa MapEnemy) { dx -= 8; dy -= 8; }
690   if (obj) PutSpawnedMapObject(player.ix+dx, player.iy+dy, obj);
691   return obj;
695 final MapObject debugSpawnObject (name aname) {
696   if (!aname) return none;
697   return debugSpawnObjectWithClass(findGameObjectClassByName(aname));
701 void createSpecialLevel (LevelKind kind, scope void delegate () creator, name amusic) {
702   global.darkLevel = false;
703   udjatAlarm = 0;
704   xmoney = 0;
705   collectCounter = 0;
706   global.resetStartingItems();
708   global.setMusicPitch(1.0);
709   levelKind = kind;
711   auto olddel = ImmediateDelete;
712   ImmediateDelete = false;
713   clearWholeLevel();
715   creator();
717   setMenuTilesOnTop();
719   fixWallTiles();
720   addBackgroundGfxDetails();
721   //levBGImgName = 'bgCave';
722   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
724   blockWaterChecking = true;
725   fixLiquidTop();
726   cleanDeadTiles();
728   ImmediateDelete = olddel;
729   CollectGarbage(true); // destroy delayed objects too
731   if (dumpGridStats) objGrid.dumpStats();
733   playerExited = false; // just in case
734   playerExitDoor = none;
736   osdClear();
738   setupGhostTime();
739   lg.musicName = amusic;
740   if (amusic) global.playMusic(lg.musicName); else global.stopMusic();
744 void createTitleLevel () {
745   createSpecialLevel(LevelKind.Title, &createTitleRoom, 'musTitle');
749 void createTutorialLevel () {
750   createSpecialLevel(LevelKind.Tutorial, &createTutorialRoom, 'musCave');
751   global.plife = 4;
752   global.bombs = 0;
753   global.rope = 4;
754   global.arrows = 0;
755   global.sgammo = 0;
759 // `global.currLevel` is the new level
760 void generateTransitionLevel () {
761   global.darkLevel = false;
762   udjatAlarm = 0;
763   xmoney = 0;
764   collectCounter = 0;
766   global.setMusicPitch(1.0);
767   switch (global.config.transitionMusicMode) {
768     case GameConfig::MusicMode.Silent: global.stopMusic(); break;
769     case GameConfig::MusicMode.Restart: global.restartMusic(); break;
770     case GameConfig::MusicMode.DontTouch: break;
771   }
773   levelKind = LevelKind.Transition;
775   auto olddel = ImmediateDelete;
776   ImmediateDelete = false;
777   clearWholeLevel();
779        if (global.currLevel < 4) createTrans1Room();
780   else if (global.currLevel == 4) createTrans1xRoom();
781   else if (global.currLevel < 8) createTrans2Room();
782   else if (global.currLevel == 8) createTrans2xRoom();
783   else if (global.currLevel < 12) createTrans3Room();
784   else if (global.currLevel == 12) createTrans3xRoom();
785   else if (global.currLevel < 16) createTrans4Room();
786   else if (global.currLevel == 16) createTrans4Room();
787   else createTrans1Room(); //???
789   setMenuTilesOnTop();
791   fixWallTiles();
792   addBackgroundGfxDetails();
793   //levBGImgName = 'bgCave';
794   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
796   blockWaterChecking = true;
797   fixLiquidTop();
798   cleanDeadTiles();
800   if (damselSaved > 0) {
801     // this is special "damsel ready to kiss you" object, not a heart
802     MakeMapObject(176+8, 176+8, 'oDamselKiss');
803     global.plife += damselSaved; // if player skipped transition cutscene
804     damselSaved = 0;
805   }
807   ImmediateDelete = olddel;
808   CollectGarbage(true); // destroy delayed objects too
810   if (dumpGridStats) objGrid.dumpStats();
812   playerExited = false; // just in case
813   playerExitDoor = none;
815   osdClear();
817   setupGhostTime();
818   //global.playMusic(lg.musicName);
822 void generateLevel () {
823   levelStartTime = time;
824   levelEndTime = time;
826   udjatAlarm = 0;
827   if (resetBMCOG) {
828     resetBMCOG = false;
829     global.cityOfGold = false;
830     global.genBlackMarket = false;
831   }
833   global.setMusicPitch(1.0);
834   stats.clearLevelTotals();
836   levelKind = LevelKind.Normal;
837   lg.generate();
838   //lg.dump();
840   resetRoomBounds();
842   lg.generateRooms();
843   //writeln("tw:", tilesWidth, "; th:", tilesHeight);
845   auto olddel = ImmediateDelete;
846   ImmediateDelete = false;
847   clearWholeLevel();
849   if (lg.finalBossLevel) {
850     blockWaterChecking = true;
851     createOlmecRoom();
852   }
854   // if transition cutscene was skipped...
855   global.plife += max(0, damselSaved); // if player skipped transition cutscene
856   damselSaved = 0;
858   // generate tiles
859   startRoomX = lg.startRoomX;
860   startRoomY = lg.startRoomY;
861   endRoomX = lg.endRoomX;
862   endRoomY = lg.endRoomY;
863   addBackgroundGfxDetails();
864   foreach (int y; 0..tilesHeight) {
865     foreach (int x; 0..tilesWidth) {
866       lg.genTileAt(x, y);
867     }
868   }
869   fixWallTiles();
871   levBGImgName = lg.bgImgName;
872   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
874   if (global.allowAngryShopkeepers) generateAngryShopkeepers();
876   lg.generateEntities();
878   // add box of flares to dark level
879   if (global.darkLevel && allEnters.length) {
880     auto enter = allEnters[0];
881     int x = enter.ix, y = enter.iy;
882          if (!isSolidAtPoint(x-16, y)) MakeMapObject(x-16+8, y+8, 'oFlareCrate');
883     else if (!isSolidAtPoint(x+16, y)) MakeMapObject(x+16+8, y+8, 'oFlareCrate');
884     else MakeMapObject(x+8, y+8, 'oFlareCrate');
885   }
887   //scrGenerateEntities();
888   //foreach (; 0..2) scrGenerateEntities();
890   writeln(objGrid.countObjects, " alive objects inserted");
891   writeln(countBackTiles, " background tiles inserted");
893   if (!player) FatalError("player pawn is not spawned");
895   if (lg.finalBossLevel) {
896     blockWaterChecking = true;
897   } else {
898     blockWaterChecking = false;
899   }
900   fixLiquidTop();
901   cleanDeadTiles();
903   ImmediateDelete = olddel;
904   CollectGarbage(true); // destroy delayed objects too
906   if (dumpGridStats) objGrid.dumpStats();
908   playerExited = false; // just in case
909   playerExitDoor = none;
911   levelMoneyStart = stats.money;
913   osdClear();
914   generateLevelMessages();
916   xmoney = 0;
917   collectCounter = 0;
919   if (lastMusicName != lg.musicName) {
920     global.playMusic(lg.musicName);
921     //writeln("!<", lastMusicName, ">:<", lg.musicName, ">");
922   } else {
923     //writeln("MM: ", global.config.nextLevelMusicMode);
924     switch (global.config.nextLevelMusicMode) {
925       case GameConfig::MusicMode.Silent: global.stopMusic(); break; // the thing that should not be
926       case GameConfig::MusicMode.Restart: global.restartMusic(); break;
927       case GameConfig::MusicMode.DontTouch:
928         if (global.config.transitionMusicMode == GameConfig::MusicMode.Silent) {
929           global.playMusic(lg.musicName);
930         }
931         break;
932     }
933   }
934   lastMusicName = lg.musicName;
935   //global.playMusic(lg.musicName);
937   setupGhostTime();
938   if (global.cityOfGold == 1 || global.genBlackMarket) resetBMCOG = true;
942 // ////////////////////////////////////////////////////////////////////////// //
943 int currKeys, nextKeys;
944 int pressedKeysQ, releasedKeysQ;
945 int keysPressed, keysReleased = -1;
948 struct SavedKeyState {
949   int currKeys, nextKeys;
950   int pressedKeysQ, releasedKeysQ;
951   int keysPressed, keysReleased;
952   // for session
953   int roomSeed, otherSeed;
957 // for saving/replaying
958 final void keysSaveState (out SavedKeyState ks) {
959   ks.currKeys = currKeys;
960   ks.nextKeys = nextKeys;
961   ks.pressedKeysQ = pressedKeysQ;
962   ks.releasedKeysQ = releasedKeysQ;
963   ks.keysPressed = keysPressed;
964   ks.keysReleased = keysReleased;
967 // for saving/replaying
968 final void keysRestoreState (const ref SavedKeyState ks) {
969   currKeys = ks.currKeys;
970   nextKeys = ks.nextKeys;
971   pressedKeysQ = ks.pressedKeysQ;
972   releasedKeysQ = ks.releasedKeysQ;
973   keysPressed = ks.keysPressed;
974   keysReleased = ks.keysReleased;
978 final void keysNextFrame () {
979   currKeys = nextKeys;
983 final void clearKeys () {
984   currKeys = 0;
985   nextKeys = 0;
986   pressedKeysQ = 0;
987   releasedKeysQ = 0;
988   keysPressed = 0;
989   keysReleased = -1;
993 final void onKey (int code, bool down) {
994   if (!code) return;
995   if (down) {
996     currKeys |= code;
997     nextKeys |= code;
998     if (keysReleased&code) {
999       keysPressed |= code;
1000       keysReleased &= ~code;
1001       pressedKeysQ |= code;
1002     }
1003   } else {
1004     nextKeys &= ~code;
1005     if (keysPressed&code) {
1006       keysReleased |= code;
1007       keysPressed &= ~code;
1008       releasedKeysQ |= code;
1009     }
1010   }
1013 final bool isKeyDown (int code) {
1014   return !!(currKeys&code);
1017 final bool isKeyPressed (int code) {
1018   bool res = !!(pressedKeysQ&code);
1019   pressedKeysQ &= ~code;
1020   return res;
1023 final bool isKeyReleased (int code) {
1024   bool res = !!(releasedKeysQ&code);
1025   releasedKeysQ &= ~code;
1026   return res;
1030 final void clearKeysPressRelease () {
1031   keysPressed = default.keysPressed;
1032   keysReleased = default.keysReleased;
1033   pressedKeysQ = default.pressedKeysQ;
1034   releasedKeysQ = default.releasedKeysQ;
1035   currKeys = 0;
1036   nextKeys = 0;
1040 // ////////////////////////////////////////////////////////////////////////// //
1041 final void registerEnter (MapTile t) {
1042   if (!t) return;
1043   allEnters[$] = t;
1044   return;
1048 final void registerExit (MapTile t) {
1049   if (!t) return;
1050   allExits[$] = t;
1051   return;
1055 final bool isYAtEntranceRow (int py) {
1056   py /= 16;
1057   foreach (MapTile t; allEnters) if (t.iy == py) return true;
1058   return false;
1062 final int calcNearestEnterDist (int px, int py) {
1063   if (allEnters.length == 0) return int.max;
1064   int curdistsq = int.max;
1065   foreach (MapTile t; allEnters) {
1066     int xc = px-t.xCenter, yc = py-t.yCenter;
1067     int distsq = xc*xc+yc*yc;
1068     if (distsq < curdistsq) curdistsq = distsq;
1069   }
1070   return round(sqrt(curdistsq));
1074 final int calcNearestExitDist (int px, int py) {
1075   if (allExits.length == 0) return int.max;
1076   int curdistsq = int.max;
1077   foreach (MapTile t; allExits) {
1078     int xc = px-t.xCenter, yc = py-t.yCenter;
1079     int distsq = xc*xc+yc*yc;
1080     if (distsq < curdistsq) curdistsq = distsq;
1081   }
1082   return round(sqrt(curdistsq));
1086 // ////////////////////////////////////////////////////////////////////////// //
1087 final void clearForTransition () {
1088   auto olddel = ImmediateDelete;
1089   ImmediateDelete = false;
1090   clearWholeLevel();
1091   ImmediateDelete = olddel;
1092   CollectGarbage(true); // destroy delayed objects too
1093   global.darkLevel = false;
1097 // ////////////////////////////////////////////////////////////////////////// //
1098 final int countBackTiles () {
1099   int res = 0;
1100   for (MapBackTile bt = backtiles; bt; bt = bt.next) ++res;
1101   return res;
1105 final void clearWholeLevel () {
1106   allEnters.clear();
1107   allExits.clear();
1109   // don't kill objects the player is holding
1110   if (player) {
1111     if (player.pickedItem isa ItemBall) {
1112       player.pickedItem.instanceRemove();
1113       player.pickedItem = none;
1114     }
1115     if (player.pickedItem && player.pickedItem.grid) {
1116       player.pickedItem.grid.remove(player.pickedItem.gridId);
1117       writeln("secured pocket item '", GetClassName(player.pickedItem.Class), "'");
1118     }
1119     if (player.holdItem isa ItemBall) {
1120       player.removeBallAndChain(temp:true);
1121       if (player.holdItem) player.holdItem.instanceRemove();
1122       player.holdItem = none;
1123     }
1124     if (player.holdItem && player.holdItem.grid) {
1125       player.holdItem.grid.remove(player.holdItem.gridId);
1126       writeln("secured held item '", GetClassName(player.holdItem.Class), "'");
1127     }
1128     writeln("secured ball; mustBeChained=", player.mustBeChained, "; wasHoldingBall=", player.wasHoldingBall);
1129   }
1131   int count = objGrid.countObjects();
1132   if (dumpGridStats) { if (objGrid.getFirstObjectCID()) objGrid.dumpStats(); }
1133   objGrid.removeAllObjects(true); // and destroy
1134   if (count > 0) writeln(count, " objects destroyed");
1136   lastUsedObjectId = 0;
1137   accumTime = 0;
1138   time = 0;
1139   lastRenderTime = -1;
1140   liquidTileCount = 0;
1141   checkWater = false;
1143   while (backtiles) {
1144     MapBackTile t = backtiles;
1145     backtiles = t.next;
1146     delete t;
1147   }
1149   levBGImg = none;
1150   framesProcessedFromLastClear = 0;
1154 final void insertObject (MapEntity o) {
1155   if (!o) return;
1156   if (o.grid) FatalError("cannot put object into level twice");
1157   objGrid.insert(o);
1161 final void spawnPlayerAt (int x, int y) {
1162   // if we have no player, spawn new one
1163   // otherwise this just a level transition, so simply reposition him
1164   if (!player) {
1165     // don't add player to object list, as it has very separate processing anyway
1166     player = SpawnObject(PlayerPawn);
1167     player.global = global;
1168     player.level = self;
1169     if (!player.initialize()) {
1170       delete player;
1171       FatalError("something is wrong with player initialization");
1172       return;
1173     }
1174   }
1175   player.fltx = x;
1176   player.flty = y;
1177   player.saveInterpData();
1178   player.resurrect();
1179   if (player.mustBeChained || global.config.scumBallAndChain) {
1180     writeln("*** spawning ball and chain");
1181     player.spawnBallAndChain(levelStart:true);
1182   }
1183   playerExited = false;
1184   playerExitDoor = none;
1185   if (global.config.startWithKapala) global.hasKapala = true;
1186   centerViewAtPlayer();
1187   // reinsert player items into grid
1188   if (player.pickedItem) objGrid.insert(player.pickedItem);
1189   if (player.holdItem) objGrid.insert(player.holdItem);
1190   //writeln("player spawned; active=", player.active);
1191   player.scrSwitchToPocketItem(forceIfEmpty:false);
1195 final void teleportPlayerTo (int x, int y) {
1196   if (player) {
1197     player.fltx = x;
1198     player.flty = y;
1199     player.saveInterpData();
1200   }
1204 final void resurrectPlayer () {
1205   if (player) player.resurrect();
1206   playerExited = false;
1207   playerExitDoor = none;
1211 // ////////////////////////////////////////////////////////////////////////// //
1212 final void scrShake (int duration) {
1213   if (shakeLeft == 0) {
1214     shakeOfs.x = 0;
1215     shakeOfs.y = 0;
1216     shakeDir.x = 0;
1217     shakeDir.y = 0;
1218   }
1219   shakeLeft = max(shakeLeft, duration);
1224 // ////////////////////////////////////////////////////////////////////////// //
1225 enum SCAnger {
1226   TileDestroyed,
1227   ItemStolen, // including damsel, lol
1228   CrapsCheated,
1229   BombDropped,
1230   DamselWhipped,
1234 // make the nearest shopkeeper angry. RAWR!
1235 void scrShopkeeperAnger (SCAnger reason, optional int maxdist, optional MapEntity offender) {
1236   if (!offender) offender = player;
1237   auto shp = MonsterShopkeeper(findNearestEnemy(offender.ix, offender.iy, delegate bool (MapEnemy o) {
1238     auto sc = MonsterShopkeeper(o);
1239     if (!sc) return false;
1240     if (sc.dead || sc.angered) return false;
1241     return true;
1242   }, castClass:MonsterShopkeeper));
1244   if (shp) {
1245     if (specified_maxdist && offender.directionToEntityCenter(shp) > maxdist) return;
1246     if (!shp.dead && !shp.angered) {
1247       shp.status = MapObject::ATTACK;
1248       string msg;
1249            if (global.murderer) msg = "YOU'LL PAY FOR YOUR CRIMES!";
1250       else if (reason == SCAnger.ItemStolen) msg = "COME BACK HERE, THIEF!";
1251       else if (reason == SCAnger.TileDestroyed) msg = "DIE, YOU VANDAL!";
1252       else if (reason == SCAnger.BombDropped) msg = "TERRORIST!";
1253       else if (reason == SCAnger.DamselWhipped) msg = "HEY, ONLY I CAN DO THAT!";
1254       else if (reason == SCAnger.CrapsCheated) msg = "DIE, CHEATER!";
1255       else msg = "NOW I'M REALLY STEAMED!";
1256       if (msg) osdMessage(msg, -666);
1257       global.thiefLevel += (global.thiefLevel > 0 ? 3 : 2);
1258     }
1259   }
1263 final MapObject findCrapsPrize () {
1264   foreach (MapObject o; objGrid.allObjects(MapObject)) {
1265     if (!o.spectral && o.inDiceHouse) return o;
1266   }
1267   return none;
1271 // ////////////////////////////////////////////////////////////////////////// //
1272 // 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.
1273 // note: idols moved by monkeys will have false `stolenIdol`
1274 void scrTriggerIdolAltar (bool stolenIdol) {
1275   ObjTikiCurse res = none;
1276   int curdistsq = int.max;
1277   int px = player.xCenter, py = player.yCenter;
1278   foreach (MapObject o; objGrid.allObjects(MapObject)) {
1279     auto tcr = ObjTikiCurse(o);
1280     if (!tcr) continue;
1281     if (tcr.activated) continue;
1282     int xc = px-tcr.xCenter, yc = py-tcr.yCenter;
1283     int distsq = xc*xc+yc*yc;
1284     if (distsq < curdistsq) {
1285       res = tcr;
1286       curdistsq = distsq;
1287     }
1288   }
1289   if (res) res.activate(stolenIdol);
1293 // ////////////////////////////////////////////////////////////////////////// //
1294 void setupGhostTime () {
1295   musicFadeTimer = -1;
1296   ghostSpawned = false;
1298   // there is no ghost on the first level
1299   if (inWinCutscene || !isNormalLevel() || lg.finalBossLevel || global.currLevel == 1) {
1300     ghostTimeLeft = -1;
1301     global.setMusicPitch(1.0);
1302     return;
1303   }
1305   if (global.config.scumGhost < 0) {
1306     // instant
1307     ghostTimeLeft = 1;
1308     osdMessage("A GHASTLY VOICE CALLS YOUR NAME!\nLET'S GET OUT OF HERE!", 6.66, urgent:true);
1309     return;
1310   }
1312   if (global.config.scumGhost == 0) {
1313     // never
1314     ghostTimeLeft = -1;
1315     return;
1316   }
1318   // randomizes time until ghost appears once time limit is reached
1319   // global.ghostRandom (true/false) if extra time is randomized - loaded from yasm.cfg. if false, use 30 second default
1320   // ghostTimeLeft (time in seconds * 1000) for currently generated level
1322   if (global.config.ghostRandom) {
1323     auto tMin = global.randOther(10, 30); // min(global.plife*10, 30);
1324     auto tMax = max(tMin+global.randOther(10, 30), min(global.config.scumGhost, 300)); // 5 minutes max
1325     auto tTime = global.randOther(tMin, tMax);
1326     if (tTime <= 0) tTime = round(tMax/2.0);
1327     ghostTimeLeft = tTime;
1328   } else {
1329     ghostTimeLeft = global.config.scumGhost; // 5 minutes max
1330   }
1332   ghostTimeLeft += max(0, global.config.ghostExtraTime);
1334   ghostTimeLeft *= 30; // seconds -> frames
1335   //global.ghostShowTime
1339 void spawnGhost () {
1340   addGhostSummoned();
1341   ghostSpawned = true;
1342   ghostTimeLeft = -1;
1344   int vwdt = (viewMax.x-viewMin.x);
1345   int vhgt = (viewMax.y-viewMin.y);
1347   int gx, gy;
1349   if (player.ix < viewMin.x+vwdt/2) {
1350     // player is in the left side
1351     gx = viewMin.x+vwdt/2+vwdt/4;
1352   } else {
1353     // player is in the right side
1354     gx = viewMin.x+vwdt/4;
1355   }
1357   if (player.iy < viewMin.y+vhgt/2) {
1358     // player is in the left side
1359     gy = viewMin.y+vhgt/2+vhgt/4;
1360   } else {
1361     // player is in the right side
1362     gy = viewMin.y+vhgt/4;
1363   }
1365   writeln("spawning ghost at tile (", gx/16, ",", gy/16, "); player is at tile (", player.ix/16, ",", player.iy/16, ")");
1367   MakeMapObject(gx, gy, 'oGhost');
1369   /*
1370     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);
1371     else instance_create(view_xview[0]-32, view_yview[0]+floor(view_hview[0]/2), oGhost);
1372     global.ghostExists = true;
1373   */
1377 void thinkFrameGameGhost () {
1378   if (player.dead) return;
1379   if (!isNormalLevel()) return; // just in case
1381   if (ghostTimeLeft < 0) {
1382     // turned off
1383     if (musicFadeTimer > 0) {
1384       musicFadeTimer = -1;
1385       global.setMusicPitch(1.0);
1386     }
1387     return;
1388   }
1390   if (musicFadeTimer >= 0) {
1391     ++musicFadeTimer;
1392     if (musicFadeTimer > 0 && musicFadeTimer <= 100*3) {
1393       float pitch = 1.0-(0.00226/3.0)*musicFadeTimer;
1394       //writeln("MFT: ", musicFadeTimer, "; pitch=", pitch);
1395       global.setMusicPitch(pitch);
1396     }
1397   }
1399   if (ghostTimeLeft == 0) {
1400     // she is already here!
1401     return;
1402   }
1404   // no ghost if we have a crown
1405   if (global.hasCrown) {
1406     ghostTimeLeft = -1;
1407     return;
1408   }
1410   // if she was already spawned, don't do it again
1411   if (ghostSpawned) {
1412     ghostTimeLeft = 0;
1413     return;
1414   }
1416   if (--ghostTimeLeft != 0) {
1417     // warning
1418     if (global.config.ghostExtraTime > 0) {
1419       if (ghostTimeLeft == global.config.ghostExtraTime*30) {
1420         osdMessage("A CHILL RUNS UP YOUR SPINE...\nLET'S GET OUT OF HERE!", 6.66, urgent:true);
1421       }
1422       if (musicFadeTimer < 0 && ghostTimeLeft <= global.config.ghostExtraTime*30) {
1423         musicFadeTimer = 0;
1424       }
1425     }
1426     return;
1427   }
1429   // spawn her
1430   if (player.isExitingSprite) {
1431     // no reason to spawn her, we're leaving
1432     ghostTimeLeft = -1;
1433     return;
1434   }
1436   spawnGhost();
1440 void thinkFrameGame () {
1441   thinkFrameGameGhost();
1442   // udjat eye blinking
1443   if (global.hasUdjatEye && player) {
1444     foreach (MapTile t; allExits) {
1445       if (t isa MapTileBlackMarketDoor) {
1446         auto dm = int(player.distanceToEntity(t));
1447         if (dm < 4) dm = 4;
1448         if (udjatAlarm < 1 || dm < udjatAlarm) udjatAlarm = dm;
1449       }
1450     }
1451   } else {
1452     global.udjatBlink = false;
1453     udjatAlarm = 0;
1454   }
1455   if (udjatAlarm > 0) {
1456     if (--udjatAlarm == 0) {
1457       global.udjatBlink = !global.udjatBlink;
1458       if (global.hasUdjatEye && player) {
1459         player.playSound(global.udjatBlink ? 'sndBlink1' : 'sndBlink2');
1460       }
1461     }
1462   }
1463   switch (levelKind) {
1464     case LevelKind.Stars: thinkFrameGameStars(); break;
1465     case LevelKind.Sun: thinkFrameGameSun(); break;
1466     case LevelKind.Moon: thinkFrameGameMoon(); break;
1467   }
1471 // ////////////////////////////////////////////////////////////////////////// //
1472 private final bool isWaterTileCB (MapTile t) {
1473   return (t && t.visible && t.water);
1477 private final bool isLavaTileCB (MapTile t) {
1478   return (t && t.visible && t.lava);
1482 private final bool isWetTile (MapTile t) {
1483   return (t && t.visible && (t.water || t.lava || t.wet));
1487 private final bool isWetOrSolidTile (MapTile t) {
1488   return (t && t.visible && (t.water || t.lava || t.solid) && t.isInstanceAlive);
1493 final bool isWetOrSolidTileAtPoint (int px, int py) {
1494   return !!checkTileAtPoint(px, py, &isWetOrSolidTile);
1500 final bool isWetOrSolidTileAtTile (int tx, int ty) {
1501   return !!checkTileAtPoint(tx*16, ty*16, &isWetOrSolidTile);
1505 final bool isWetTileAtPix (int tx, int ty) {
1506   return !!checkTileAtPoint(tx, ty, &isWetTile);
1511 // ////////////////////////////////////////////////////////////////////////// //
1512 const int GreatLakeStartTileY = 28;
1514 final void fillGreatLake () {
1515   if (global.lake == 1) {
1516     foreach (int y; GreatLakeStartTileY..tilesHeight) {
1517       foreach (int x; 0..tilesWidth) {
1518         auto t = checkTileAtPoint(x*16, y*16, delegate bool (MapTile t) {
1519           if (t.spectral || !t.visible || t.invisible || t.moveable) return false;
1520           return true;
1521         });
1522         if (t) {
1523           //if (!t.water || !t.lava) { t.wet = true; continue; }
1524         } else {
1525           t = MakeMapTile(x, y, 'oWaterSwim');
1526           if (!t) continue;
1527         }
1528         if (t.water) {
1529           t.setSprite(y == GreatLakeStartTileY ? 'sWaterTop' : 'sWater');
1530         } else if (t.lava) {
1531           t.setSprite(y == GreatLakeStartTileY ? 'sLavaTop' : 'sLava');
1532         }
1533       }
1534     }
1535   }
1539 // called once after level generation
1540 final void fixLiquidTop () {
1541   if (global.lake == 1) fillGreatLake();
1543   liquidTileCount = 0;
1544   forEachTile(delegate bool (MapTile t) {
1545     if (!t.water && !t.lava) {
1546       // mark as wet for lake
1547       //if (global.lake == 1 && t.iy >= GreatLakeStartTileY*16) t.wet = true;
1548       return false;
1549     }
1551     ++liquidTileCount;
1552     //writeln("fixing water tile(", GetClassName(t.Class), "):'", t.objName, "' (water=", t.water, "; lava=", t.lava, "); lqc=", liquidTileCount);
1554     if (global.lake == 1) return false; // it is done in `fillGreatLake()`
1556     if (!checkTileAtPoint(t.ix+8, t.iy-8, (t.lava ? &isLavaTileCB : &isWaterTileCB))) {
1557       t.setSprite(t.lava ? 'sLavaTop' : 'sWaterTop');
1558     } else {
1559       // don't do this, it will destroy seaweed
1560       //t.setSprite(t.lava ? 'sLava' : 'sWater');
1561       auto spr = t.getSprite();
1562            if (!spr) t.setSprite(t.lava ? 'sLava' : 'sWater');
1563       else if (spr.Name == 'sLavaTop') t.setSprite('sLava');
1564       else if (spr.Name == 'sWaterTop') t.setSprite('sWater');
1565     }
1567     return false;
1568   });
1569   //writeln("liquid tiles count: ", liquidTileCount);
1574 private final void checkWaterFlow (MapTile wtile) {
1575   if (global.lake == 1 && wtile.iy >= GreatLakeStartTileY*16) return;
1577   int tileX = wtile.ix/16, tileY = wtile.iy/16;
1579   bool wetLeft = !!isWetOrSolidTileAtTile(tileX-1, tileY);
1580   bool wetRight = !!isWetOrSolidTileAtTile(tileX+1, tileY);
1581   bool wetBottom = !!isWetOrSolidTileAtTile(tileX, tileY+1);
1583   if (!wetBottom || !wetLeft || !wetRight) {
1584     //TODO: if this is some pool created by a mattock or by an explosion, fill it
1585     if (true /*!isGoodPoolAtTile(tileX, tileY)*/) {
1586       checkWater = true;
1587       wtile.smashMe();
1588       wtile.instanceRemove(); // just in case
1589     }
1590     return;
1591   }
1593   ++liquidTileCount;
1595   if (!isWetTileAtPix(tileX*16+8, tileY*16-8)) {
1596     wtile.setSprite(wtile.lava ? 'sLavaTop' : 'sWaterTop');
1597   }
1602   [.] write water flowing. algo:
1603       start with the lowest water tile that has any room do drop, or to move to the side
1604       if water can move down, move it down until it hits the floor, repeat the algo for it
1605       if water can move left or right, choose a direction (if both dirs are available, make
1606       a random choice), move water there, and repeat algo
1607       NOTE: if we're sitting on top of another water tile, choose the side where we can move
1608             down on the next step
1609         WARNING! never return to the horizontal tile we already visited! such tiles are
1610                  considered "occupied"
1611       if water cannot move anymore, set "check mark" to 1, and continue with other water tiles.
1613       after all water tiles are moved, check all water tiles again. if some tile is not enclosed
1614       by another water tiles or solids, remove it
1616 transient MapTile curWaterTile;
1617 transient bool curWaterTileCheckHitsLava;
1618 transient bool curWaterTileCheckHitsSolidOrWater; // only for `checkWaterOrSolidTilePartialCB`
1619 transient int curWaterTileLastHDir;
1620 transient ubyte[16, 16] curWaterOccupied;
1621 transient int curWaterOccupiedCount;
1622 transient int curWaterTileCheckX0, curWaterTileCheckY0;
1625 private final void clearCurWaterCheckState () {
1626   curWaterTileCheckHitsLava = false;
1627   curWaterOccupiedCount = 0;
1628   foreach (auto idx; 0..16*16) curWaterOccupied[idx] = 0;
1632 private final bool checkWaterOrSolidTileCB (MapTile t) {
1633   if (t == curWaterTile) return false;
1634   if (t.lava && curWaterTile.water) {
1635     curWaterTileCheckHitsLava = true;
1636     return true;
1637   }
1638   if (t.ix%16 != 0 || t.iy%16 != 0) {
1639     if (t.water || t.solid) {
1640       // fill occupied array
1641       //FIXME: optimize this
1642       if (curWaterOccupiedCount < 16*16) {
1643         foreach (auto dy; t.y0..t.y1+1) {
1644           foreach (auto dx; t.x0..t.x1+1) {
1645             int sx = dx-curWaterTileCheckX0;
1646             int sy = dy-curWaterTileCheckY0;
1647             if (sx >= 0 && sx <= 16 && sy >= 0 && sy <= 15 && !curWaterOccupied[sx, sy]) {
1648               curWaterOccupied[sx, sy] = 1;
1649               ++curWaterOccupiedCount;
1650             }
1651           }
1652         }
1653       }
1654     }
1655     return false; // need to check for lava
1656   }
1657   if (t.water || t.solid || t.lava) {
1658     curWaterOccupiedCount = 16*16;
1659     if (t.water && curWaterTile.lava) t.instanceRemove();
1660   }
1661   return false; // need to check for lava
1665 private final bool checkWaterOrSolidTilePartialCB (MapTile t) {
1666   if (t == curWaterTile) return false;
1667   if (t.lava && curWaterTile.water) {
1668     //writeln("!!!!!!!!");
1669     curWaterTileCheckHitsLava = true;
1670     return true;
1671   }
1672   if (t.water || t.solid || t.lava) {
1673     //writeln("*********");
1674     curWaterTileCheckHitsSolidOrWater = true;
1675     if (t.water && curWaterTile.lava) t.instanceRemove();
1676   }
1677   return false; // need to check for lava
1681 private final bool isFullyOccupiedAtTilePos (int tileX, int tileY) {
1682   clearCurWaterCheckState();
1683   curWaterTileCheckX0 = tileX*16;
1684   curWaterTileCheckY0 = tileY*16;
1685   checkTilesInRect(tileX*16, tileY*16, 16, 16, &checkWaterOrSolidTileCB);
1686   return (curWaterTileCheckHitsLava || curWaterOccupiedCount == 16*16);
1690 private final bool isAtLeastPartiallyOccupiedAtTilePos (int tileX, int tileY) {
1691   curWaterTileCheckHitsLava = false;
1692   curWaterTileCheckHitsSolidOrWater = false;
1693   checkTilesInRect(tileX*16, tileY*16, 16, 16, &checkWaterOrSolidTilePartialCB);
1694   return (curWaterTileCheckHitsSolidOrWater || curWaterTileCheckHitsLava);
1698 private final bool waterCanReachGroundHoleInDir (MapTile wtile, int dx) {
1699   if (dx == 0) return false; // just in case
1700   dx = sign(dx);
1701   int x = wtile.ix/16, y = wtile.iy/16;
1702   x += dx;
1703   while (x >= 0 && x < tilesWidth) {
1704     if (!isAtLeastPartiallyOccupiedAtTilePos(x, y+1)) return true;
1705     if (isAtLeastPartiallyOccupiedAtTilePos(x, y)) return false;
1706     x += dx;
1707   }
1708   return false;
1712 // returns `true` if this tile must be removed
1713 private final bool checkWaterFlow (MapTile wtile) {
1714   if (global.lake == 1 && wtile.iy >= GreatLakeStartTileY*16) return false;
1716   if (wtile.ix%16 != 0 || wtile.iy%16 != 0) return true; // sanity check
1718   curWaterTile = wtile;
1719   curWaterTileLastHDir = 0; // never moved to the side
1721   bool wasMoved = false;
1723   for (;;) {
1724     int tileX = wtile.ix/16, tileY = wtile.iy/16;
1726     // out of level?
1727     if (tileY >= tilesHeight) return true;
1729     // check if we can fall down
1730     auto canFall = !isAtLeastPartiallyOccupiedAtTilePos(tileX, tileY+1);
1731     // disappear if can fall in lava
1732     if (wtile.water && curWaterTileCheckHitsLava) {
1733       //!writeln(wtile.objId, ": LAVA HIT DOWN");
1734       return true;
1735     }
1736     if (wasMoved) {
1737       // fake, so caller will not start removing tiles
1738       if (canFall) wtile.waterMovedDown = true;
1739       break;
1740     }
1741     // can move down?
1742     if (canFall) {
1743       // move down
1744       //!writeln(wtile.objId, ": GOING DOWN");
1745       curWaterTileLastHDir = 0;
1746       wtile.iy = wtile.iy+16;
1747       wasMoved = true;
1748       wtile.waterMovedDown = true;
1749       continue;
1750     }
1752     bool canMoveLeft = (curWaterTileLastHDir > 0 ? false : !isAtLeastPartiallyOccupiedAtTilePos(tileX-1, tileY));
1753     // disappear if near lava
1754     if (wtile.water && curWaterTileCheckHitsLava) {
1755       //!writeln(wtile.objId, ": LAVA HIT LEFT");
1756       return true;
1757     }
1759     bool canMoveRight = (curWaterTileLastHDir < 0 ? false : !isAtLeastPartiallyOccupiedAtTilePos(tileX+1, tileY));
1760     // disappear if near lava
1761     if (wtile.water && curWaterTileCheckHitsLava) {
1762       //!writeln(wtile.objId, ": LAVA HIT RIGHT");
1763       return true;
1764     }
1766     if (!canMoveLeft && !canMoveRight) {
1767       // do final checks
1768       //!if (wasMove) writeln(wtile.objId, ": NO MORE MOVES");
1769       break;
1770     }
1772     if (canMoveLeft && canMoveRight) {
1773       // choose random direction
1774       //!writeln(wtile.objId, ": CHOOSING RANDOM HDIR");
1775       // actually, choose direction that leads to hole in a ground
1776       if (waterCanReachGroundHoleInDir(wtile, -1)) {
1777         // can reach hole at the left side
1778         if (waterCanReachGroundHoleInDir(wtile, 1)) {
1779           // can reach hole at the right side, choose at random
1780           if (global.randOther(0, 1)) canMoveRight = false; else canMoveLeft = false;
1781         } else {
1782           // move left
1783           canMoveRight = false;
1784         }
1785       } else {
1786         // can't reach hole at the left side
1787         if (waterCanReachGroundHoleInDir(wtile, 1)) {
1788           // can reach hole at the right side, choose at random
1789           canMoveLeft = false;
1790         } else {
1791           // no holes at any side, choose at random
1792           if (global.randOther(0, 1)) canMoveRight = false; else canMoveLeft = false;
1793         }
1794       }
1795     }
1797     // move
1798     if (canMoveLeft) {
1799       if (canMoveRight) FatalError("WATERCHECK: WTF RIGHT");
1800       //!writeln(wtile.objId, ": MOVING LEFT (", curWaterTileLastHDir, ")");
1801       curWaterTileLastHDir = -1;
1802       wtile.ix = wtile.ix-16;
1803     } else if (canMoveRight) {
1804       if (canMoveLeft) FatalError("WATERCHECK: WTF LEFT");
1805       //!writeln(wtile.objId, ": MOVING RIGHT (", curWaterTileLastHDir, ")");
1806       curWaterTileLastHDir = 1;
1807       wtile.ix = wtile.ix+16;
1808     }
1809     wasMoved = true;
1810   }
1812   // remove seaweeds
1813   if (wasMoved) {
1814     checkWater = true;
1815     wtile.setSprite(wtile.lava ? 'sLava' : 'sWater');
1816     wtile.waterMoved = true;
1817     // if this tile was not moved down, check if it can move down on any next step
1818     if (!wtile.waterMovedDown) {
1819            if (waterCanReachGroundHoleInDir(wtile, -1)) wtile.waterMovedDown = true;
1820       else if (waterCanReachGroundHoleInDir(wtile, 1)) wtile.waterMovedDown = true;
1821     }
1822   }
1824   return false; // don't remove
1826   //if (!isWetTileAtPix(tileX*16+8, tileY*16-8)) wtile.setSprite(wtile.lava ? 'sLavaTop' : 'sWaterTop');
1830 transient array!MapTile waterTilesList;
1832 final bool sortWaterTilesByCoordsLess (MapTile a, MapTile b) {
1833   int dy = a.iy-b.iy;
1834   if (dy) return (dy < 0);
1835   return (a.ix < b.ix);
1838 transient int waterFlowPause = 0;
1839 transient bool debugWaterFlowPause = false;
1841 final void cleanDeadObjects () {
1842   // remove dead objects
1843   if (deadItemsHead) {
1844     auto olddel = ImmediateDelete;
1845     ImmediateDelete = false;
1846     do {
1847       auto it = deadItemsHead;
1848       deadItemsHead = it.deadItemsNext;
1849       if (it.grid) it.grid.remove(it.gridId);
1850       it.onDestroy();
1851       delete it;
1852     } while (deadItemsHead);
1853     ImmediateDelete = olddel;
1854     if (olddel) CollectGarbage(true); // destroy delayed objects too
1855   }
1858 final void cleanDeadTiles () {
1859   if (checkWater && /*global.lake == 1 ||*/ (!blockWaterChecking && liquidTileCount)) {
1860     if (global.lake == 1) fillGreatLake();
1861     if (waterFlowPause > 1) {
1862       --waterFlowPause;
1863       cleanDeadObjects();
1864       return;
1865     }
1866     if (debugWaterFlowPause) waterFlowPause = 4;
1867     //writeln("checking water");
1868     waterTilesList.clear();
1869     foreach (MapTile wtile; objGrid.allObjectsSafe(MapTile)) {
1870       if (wtile.water || wtile.lava) {
1871         // sanity check
1872         if (wtile.ix%16 == 0 && wtile.iy%16 == 0) {
1873           wtile.waterMoved = false;
1874           wtile.waterMovedDown = false;
1875           wtile.waterSlideOldX = wtile.ix;
1876           wtile.waterSlideOldY = wtile.iy;
1877           waterTilesList[$] = wtile;
1878         }
1879       }
1880     }
1881     checkWater = false;
1882     liquidTileCount = 0;
1883     waterTilesList.sort(&sortWaterTilesByCoordsLess);
1884     // do water flow
1885     bool wasAnyMove = false;
1886     bool wasAnyMoveDown = false;
1887     foreach (MapTile wtile; waterTilesList) {
1888       if (!wtile || !wtile.isInstanceAlive) continue;
1889       auto killIt = checkWaterFlow(wtile);
1890       if (killIt) {
1891         checkWater = true;
1892         wtile.smashMe();
1893         wtile.instanceRemove(); // just in case
1894       } else {
1895         wtile.saveInterpData();
1896         wtile.updateGrid();
1897         wasAnyMove = wasAnyMove || wtile.waterMoved;
1898         wasAnyMoveDown = wasAnyMoveDown || wtile.waterMovedDown;
1899         if (wtile.waterMoved && debugWaterFlowPause) wtile.waterSlideCounter = 4;
1900       }
1901     }
1902     // do water check
1903     liquidTileCount = 0;
1904     foreach (MapTile wtile; waterTilesList) {
1905       if (!wtile || !wtile.isInstanceAlive) continue;
1906       if (wasAnyMoveDown) {
1907         ++liquidTileCount;
1908         continue;
1909       }
1910       //checkWater = checkWater || wtile.waterMoved;
1911       curWaterTile = wtile;
1912       int tileX = wtile.ix/16, tileY = wtile.iy/16;
1913       // check if we are have no way to leak
1914       bool killIt = false;
1915       if (!isFullyOccupiedAtTilePos(tileX-1, tileY) || (wtile.water && curWaterTileCheckHitsLava)) {
1916         //writeln(" LEFT DEATH (", curWaterOccupiedCount, " : ", curWaterTileCheckHitsLava, ")");
1917         killIt = true;
1918       }
1919       if (!killIt && (!isFullyOccupiedAtTilePos(tileX+1, tileY) || (wtile.water && curWaterTileCheckHitsLava))) {
1920         //writeln(" RIGHT DEATH (", curWaterOccupiedCount, " : ", curWaterTileCheckHitsLava, ")");
1921         killIt = true;
1922       }
1923       if (!killIt && (!isFullyOccupiedAtTilePos(tileX, tileY+1) || (wtile.water && curWaterTileCheckHitsLava))) {
1924         //writeln(" DOWN DEATH (", curWaterOccupiedCount, " : ", curWaterTileCheckHitsLava, ")");
1925         killIt = true;
1926       }
1927       //killIt = false;
1928       if (killIt) {
1929         checkWater = true;
1930         wtile.smashMe();
1931         wtile.instanceRemove(); // just in case
1932       } else {
1933         ++liquidTileCount;
1934       }
1935     }
1936     if (wasAnyMove) checkWater = true;
1937     //writeln("water check: liquidTileCount=", liquidTileCount, "; checkWater=", checkWater, "; wasAnyMove=", wasAnyMove, "; wasAnyMoveDown=", wasAnyMoveDown);
1939     // fill empty spaces in lake with water
1940     fixLiquidTop();
1941   }
1943   cleanDeadObjects();
1947 // ////////////////////////////////////////////////////////////////////////// //
1948 private transient array!MapEntity postponedThinkers;
1949 private transient MapEntity thinkerHeld;
1950 private transient array!MapEntity activeThinkerList;
1953 final void doThinkActionsForObject (MapEntity o) {
1954        if (o.justSpawned) o.justSpawned = false;
1955   else if (o.imageSpeed > 0) o.nextAnimFrame();
1956   o.saveInterpData();
1957   o.thinkFrame();
1958   if (o.isInstanceAlive) {
1959     //o.updateGrid();
1960     o.processAlarms();
1961     if (o.isInstanceAlive) {
1962       if (o.whipTimer > 0) --o.whipTimer;
1963       o.updateGrid();
1964       auto obj = MapObject(o);
1965       if (!o.canLiveOutsideOfLevel && (!obj || !obj.heldBy) && o.isOutsideOfLevel()) {
1966         // oops, fallen out of level...
1967         o.onOutOfLevel();
1968       }
1969     }
1970   }
1974 // return `true` if thinker should be removed
1975 final void thinkOne (MapEntity o, optional bool doHeldObject, optional bool dontAddHeldObject) {
1976   if (!o) return;
1977   if (o == thinkerHeld && !doHeldObject) return; // skip it
1979   if (!o.active || !o.isInstanceAlive) return;
1981   auto obj = MapObject(o);
1983   if (obj && obj.heldBy == player) {
1984     // fix held item coords
1985     obj.fixHoldCoords();
1986     if (doHeldObject) {
1987       doThinkActionsForObject(o);
1988     } else {
1989       if (!dontAddHeldObject) {
1990         bool found = false;
1991         foreach (MapEntity e; postponedThinkers) if (e == o) { found = true; break; }
1992         if (!found) postponedThinkers[$] = o;
1993       }
1994     }
1995     return;
1996   }
1998   bool doThink = true;
2000   // collision with player weapon
2001   auto hh = PlayerWeapon(player.holdItem);
2002   bool doWeaponAction = false;
2003   if (hh) {
2004     if (hh.blockedBySolids && !global.config.killEnemiesThruWalls) {
2005       int xx = player.ix+(player.dir == MapObject::Dir.Left ? -16 : 16);
2006       //doWeaponAction = !isSolidAtPoint(xx, player.iy);
2007       doWeaponAction = !isSolidAtPoint(xx, hh.yCenter);
2008       /*
2009       int dh = max(1, hh.height-2);
2010       doWeaponAction = !checkTilesInRect(player.ix, player.iy);
2011       */
2012     } else {
2013       doWeaponAction = true;
2014     }
2015   }
2017   if (obj && doWeaponAction && hh && (o.whipTimer <= 0 || hh.ignoreWhipTimer) && hh.collidesWithObject(obj)) {
2018     //writeln("WEAPONED!");
2019     bool dontChangeWhipTimer = hh.dontChangeWhipTimer;
2020     if (!o.onTouchedByPlayerWeapon(player, hh)) {
2021       if (o.isInstanceAlive) hh.onCollisionWithObject(obj);
2022     }
2023     if (!dontChangeWhipTimer) o.whipTimer = o.whipTimerValue; //HACK
2024     doThink = o.isInstanceAlive;
2025   }
2027   if (doThink && o.isInstanceAlive) {
2028     doThinkActionsForObject(o);
2029     doThink = o.isInstanceAlive;
2030   }
2032   // collision with player
2033   if (doThink && obj && o.collidesWith(player)) {
2034     if (!player.onObjectTouched(obj) && o.isInstanceAlive) {
2035       doThink = !o.onTouchedByPlayer(player);
2036       o.updateGrid();
2037     }
2038   }
2042 final void processThinkers (float timeDelta) {
2043   if (timeDelta <= 0) return;
2044   if (gamePaused) {
2045     if (onBeforeFrame) onBeforeFrame(false);
2046     if (onAfterFrame) onAfterFrame(false);
2047     keysNextFrame();
2048     return;
2049   }
2050   accumTime += timeDelta;
2051   bool wasFrame = false;
2052   // block GC
2053   auto olddel = ImmediateDelete;
2054   ImmediateDelete = false;
2055   while (accumTime >= FrameTime) {
2056     postponedThinkers.clear();
2057     thinkerHeld = none;
2058     accumTime -= FrameTime;
2059     if (onBeforeFrame) onBeforeFrame(accumTime >= FrameTime);
2060     // shake
2061     if (shakeLeft > 0) {
2062       --shakeLeft;
2063       if (!shakeDir.x) shakeDir.x = (global.randOther(0, 1) ? -3 : 3);
2064       if (!shakeDir.y) shakeDir.y = (global.randOther(0, 1) ? -3 : 3);
2065       shakeOfs.x = shakeDir.x;
2066       shakeOfs.y = shakeDir.y;
2067       int sgnc = global.randOther(1, 3);
2068       if (sgnc&0x01) shakeDir.x = -shakeDir.x;
2069       if (sgnc&0x02) shakeDir.y = -shakeDir.y;
2070     } else {
2071       shakeOfs.x = 0;
2072       shakeOfs.y = 0;
2073       shakeDir.x = 0;
2074       shakeDir.y = 0;
2075     }
2076     // advance time
2077     time += 1;
2078     // we don't want the time to grow too large
2079     if (time < 0) { time = 0; lastRenderTime = -1; }
2080     // game-global events
2081     thinkFrameGame();
2082     // frame thinkers: player
2083     if (player && !disablePlayerThink) {
2084       // time limit
2085       if (!player.dead && isNormalLevel() &&
2086           (maxPlayingTime < 0 ||
2087            (maxPlayingTime > 0 && maxPlayingTime*30 <= time &&
2088             time%30 == 0 && global.randOther(1, 100) <= 20)))
2089       {
2090         MakeMapObject(player.ix, player.iy, 'oExplosion');
2091       }
2092       //HACK: check for stolen items
2093       auto item = MapItem(player.holdItem);
2094       if (item) item.onCheckItemStolen(player);
2095       item = MapItem(player.pickedItem);
2096       if (item) item.onCheckItemStolen(player);
2097       // normal thinking
2098       doThinkActionsForObject(player);
2099     }
2100     // frame thinkers: held object
2101     thinkerHeld = player.holdItem;
2102     if (thinkerHeld && thinkerHeld.isInstanceAlive) {
2103       thinkOne(thinkerHeld, doHeldObject:true);
2104       if (!thinkerHeld.isInstanceAlive) {
2105         if (player.holdItem == thinkerHeld) player.holdItem = none;
2106         thinkerHeld.grid.remove(thinkerHeld.gridId);
2107         /* later
2108         thinkerHeld.onDestroy();
2109         delete thinkerHeld;
2110         */
2111       }
2112     }
2113     // frame thinkers: objects
2114     activeThinkerList.clear();
2115     auto grid = objGrid;
2116     // collect active objects
2117     if (global.config.useFrozenRegion) {
2118       foreach (MapEntity e; grid.inRectPix(viewStart.x/global.scale-64, viewStart.y/global.scale-64, 320+64*2, 240+64*2, precise:false)) {
2119         if (e.active) activeThinkerList[$] = e;
2120       }
2121     } else {
2122       // no frozen area
2123       foreach (MapEntity e; grid.allObjects()) {
2124         if (e.active) activeThinkerList[$] = e;
2125       }
2126     }
2127     // process active objects
2128     //writeln("thinkers: ", activeThinkerList.length);
2129     foreach (MapEntity o; activeThinkerList) {
2130       if (!o) continue;
2131       thinkOne(o, doHeldObject:false);
2132       if (!o.isInstanceAlive) {
2133         //writeln("dead thinker: '", o.objType, "'");
2134         if (o.grid) o.grid.remove(o.gridId);
2135         auto obj = MapObject(o);
2136         if (obj && obj.heldBy) obj.heldBy.holdItem = none;
2137         /* later
2138         o.onDestroy();
2139         delete o;
2140         */
2141       }
2142     }
2143     // postponed thinkers
2144     foreach (MapEntity o; postponedThinkers) {
2145       if (!o) continue;
2146       thinkOne(o, doHeldObject:true, dontAddHeldObject:true);
2147       if (!o.isInstanceAlive) {
2148         //writeln("dead pp-thinker: '", o.objType, "'");
2149         /* later
2150         o.onDestroy();
2151         delete o;
2152         */
2153       }
2154     }
2155     postponedThinkers.clear();
2156     thinkerHeld = none;
2157     // clean dead things
2158     cleanDeadTiles();
2159     // fix held item coords
2160     if (player && player.holdItem) {
2161       if (player.holdItem.isInstanceAlive) {
2162         player.holdItem.fixHoldCoords();
2163       } else {
2164         player.holdItem = none;
2165       }
2166     }
2167     // money counter
2168     if (collectCounter == 0) {
2169       xmoney = max(0, xmoney-100);
2170     } else {
2171       --collectCounter;
2172     }
2173     // other things
2174     if (player) {
2175       if (!player.dead) stats.oneMoreFramePlayed();
2176       SoundSystem.ListenerOrigin = vector(player.xCenter, player.yCenter, -1);
2177       //writeln("plrpos=(", player.xCenter, ",", player.yCenter, "); lo=", SoundSystem.ListenerOrigin);
2178     }
2179     if (onAfterFrame) onAfterFrame(accumTime >= FrameTime);
2180     ++framesProcessedFromLastClear;
2181     keysNextFrame();
2182     wasFrame = true;
2183     if (!player.visible && player.holdItem) player.holdItem.visible = false;
2184     if (winCutsceneSwitchToNext) {
2185       winCutsceneSwitchToNext = false;
2186       switch (++inWinCutscene) {
2187         case 2: startWinCutsceneVolcano(); break;
2188         case 3: default: startWinCutsceneWinFall(); break;
2189       }
2190       break;
2191     }
2192     if (playerExited) break;
2193   }
2194   ImmediateDelete = olddel;
2195   if (playerExited) {
2196     playerExited = false;
2197     onLevelExited();
2198     centerViewAtPlayer();
2199   }
2200   if (wasFrame) {
2201     // if we were processed at least one frame, collect garbage
2202     //keysNextFrame();
2203     CollectGarbage(true); // destroy delayed objects too
2204   }
2205   if (accumTime > 0 && onInterFrame) onInterFrame(accumTime/FrameTime);
2209 // ////////////////////////////////////////////////////////////////////////// //
2210 final void tileXY2roomXY (int tileX, int tileY, out int roomX, out int roomY) {
2211   roomX = (tileX-1)/RoomGen::Width;
2212   roomY = (tileY-1)/RoomGen::Height;
2216 final bool isInShop (int tileX, int tileY) {
2217   if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
2218     auto n = roomType[tileX, tileY];
2219     if (n == 4 || n == 5) return true;
2220     return !!checkTilesInRect(tileX*16, tileY*16, 16, 16, delegate bool (MapTile t) { return t.shopWall; });
2221     //k8: we don't have this
2222     //if (t && t.objType == 'oShop') return true;
2223   }
2224   return false;
2228 // ////////////////////////////////////////////////////////////////////////// //
2229 override void Destroy () {
2230   clearWholeLevel();
2231   delete tempSolidTile;
2232   ::Destroy();
2236 // ////////////////////////////////////////////////////////////////////////// //
2237 // WARNING! delegate should not create/delete objects!
2238 final MapObject findNearestObject (int px, int py, scope bool delegate (MapObject o) dg, optional class!MapObject castClass) {
2239   MapObject res = none;
2240   if (!castClass) castClass = MapObject;
2241   int curdistsq = int.max;
2242   foreach (MapObject o; objGrid.allObjects(MapObject)) {
2243     if (o.spectral) continue;
2244     if (!dg(o)) continue;
2245     int xc = px-o.xCenter, yc = py-o.yCenter;
2246     int distsq = xc*xc+yc*yc;
2247     if (distsq < curdistsq) {
2248       res = o;
2249       curdistsq = distsq;
2250     }
2251   }
2252   return res;
2256 // WARNING! delegate should not create/delete objects!
2257 final MapObject findNearestEnemy (int px, int py, optional scope bool delegate (MapEnemy o) dg, optional class!MapEnemy castClass) {
2258   if (!castClass) castClass = MapEnemy;
2259   if (castClass !isa MapEnemy) return none;
2260   MapObject res = none;
2261   int curdistsq = int.max;
2262   foreach (MapEnemy o; objGrid.allObjects(castClass)) {
2263     //k8: i added `dead` check
2264     if (o.spectral || o.dead) continue;
2265     if (dg) {
2266       if (!dg(o)) continue;
2267     }
2268     int xc = px-o.xCenter, yc = py-o.yCenter;
2269     int distsq = xc*xc+yc*yc;
2270     if (distsq < curdistsq) {
2271       res = o;
2272       curdistsq = distsq;
2273     }
2274   }
2275   return res;
2279 final MonsterShopkeeper findNearestCalmShopkeeper (int px, int py) {
2280   auto obj = MonsterShopkeeper(findNearestEnemy(px, py, delegate bool (MapEnemy o) {
2281     auto sk = MonsterShopkeeper(o);
2282     if (sk && !sk.angered) return true;
2283     return false;
2284   }, castClass:MonsterShopkeeper));
2285   return obj;
2289 final MonsterShopkeeper hasAliveShopkeepers (optional bool skipAngry) {
2290   foreach (MonsterShopkeeper sc; objGrid.allObjects(MonsterShopkeeper)) {
2291     if (sc.spectral || sc.dead) continue;
2292     if (skipAngry && (sc.angered || sc.outlaw)) continue;
2293     return sc;
2294   }
2295   return none;
2299 // WARNING! delegate should not create/delete objects!
2300 final int calcNearestEnemyDist (int px, int py, optional scope bool delegate (MapEnemy o) dg, optional class!MapEnemy castClass) {
2301   auto e = findNearestEnemy(px, py, dg!optional, castClass!optional);
2302   if (!e) return int.max;
2303   int xc = px-e.xCenter, yc = py-e.yCenter;
2304   return round(sqrt(xc*xc+yc*yc));
2308 // WARNING! delegate should not create/delete objects!
2309 final int calcNearestObjectDist (int px, int py, optional scope bool delegate (MapObject o) dg, optional class!MapObject castClass) {
2310   auto e = findNearestObject(px, py, dg!optional, castClass!optional);
2311   if (!e) return int.max;
2312   int xc = px-e.xCenter, yc = py-e.yCenter;
2313   return round(sqrt(xc*xc+yc*yc));
2317 // WARNING! delegate should not create/delete objects!
2318 final MapTile findNearestMoveableSolid (int px, int py, optional scope bool delegate (MapTile t) dg) {
2319   MapTile res = none;
2320   int curdistsq = int.max;
2321   foreach (MapTile t; objGrid.allObjects(MapTile)) {
2322     if (t.spectral) continue;
2323     if (dg) {
2324       if (!dg(t)) continue;
2325     } else {
2326       if (!t.solid || !t.moveable) continue;
2327     }
2328     int xc = px-t.xCenter, yc = py-t.yCenter;
2329     int distsq = xc*xc+yc*yc;
2330     if (distsq < curdistsq) {
2331       res = t;
2332       curdistsq = distsq;
2333     }
2334   }
2335   return res;
2339 // WARNING! delegate should not create/delete objects!
2340 final MapTile findNearestTile (int px, int py, optional scope bool delegate (MapTile t) dg) {
2341   if (!dg) return none;
2342   MapTile res = none;
2343   int curdistsq = int.max;
2345   //FIXME: make this faster!
2346   foreach (MapTile t; objGrid.allObjects(MapTile)) {
2347     if (t.spectral) continue;
2348     int xc = px-t.xCenter, yc = py-t.yCenter;
2349     int distsq = xc*xc+yc*yc;
2350     if (distsq < curdistsq && dg(t)) {
2351       res = t;
2352       curdistsq = distsq;
2353     }
2354   }
2356   return res;
2360 // ////////////////////////////////////////////////////////////////////////// //
2361 final bool cbIsObjectWeb (MapObject o) { return (o isa ItemWeb); }
2362 final bool cbIsObjectBlob (MapObject o) { return (o isa EnemyBlob); }
2363 final bool cbIsObjectArrow (MapObject o) { return (o isa ItemProjectileArrow); }
2364 final bool cbIsObjectEnemy (MapObject o) { return (/*o != self &&*/ o isa MapEnemy); }
2366 final bool cbIsObjectTreasure (MapObject o) { return (o.isTreasure); }
2368 final bool cbIsItemObject (MapObject o) { return (o isa MapItem); }
2370 final bool cbIsItemOrEnemyObject (MapObject o) { return (o isa MapItem || o isa MapEnemy); }
2373 final MapObject isObjectAtTile (int tileX, int tileY, optional scope bool delegate (MapObject o) dg, optional bool precise) {
2374   if (!specified_precise) precise = true;
2375   tileX *= 16;
2376   tileY *= 16;
2377   foreach (MapObject o; objGrid.inRectPix(tileX, tileY, 16, 16, precise:precise, castClass:MapObject)) {
2378     if (o.spectral) continue;
2379     if (dg) {
2380       if (dg(o)) return o;
2381     } else {
2382       return o;
2383     }
2384   }
2385   return none;
2389 final MapObject isObjectAtTilePix (int x, int y, optional scope bool delegate (MapObject o) dg) {
2390   return isObjectAtTile(x/16, y/16, dg!optional);
2394 final MapObject isObjectAtPoint (int xpos, int ypos, optional scope bool delegate (MapObject o) dg, optional bool precise, optional class!MapObject castClass) {
2395   if (!specified_precise) precise = true;
2396   if (!castClass) castClass = MapObject;
2397   foreach (MapObject o; objGrid.inCellPix(xpos, ypos, precise:precise, castClass:castClass)) {
2398     if (o.spectral) continue;
2399     if (dg) {
2400       if (dg(o)) return o;
2401     } else {
2402       if (o isa MapEnemy) return o;
2403     }
2404   }
2405   return none;
2409 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) {
2410   if (w < 1 || h < 1) return none;
2411   if (!castClass) castClass = MapObject;
2412   if (w == 1 && h == 1) return isObjectAtPoint(xpos, ypos, dg!optional, precise!optional, castClass);
2413   if (!specified_precise) precise = true;
2414   foreach (MapObject o; objGrid.inRectPix(xpos, ypos, w, h, precise:precise, castClass:castClass)) {
2415     if (o.spectral) continue;
2416     if (dg) {
2417       if (dg(o)) return o;
2418     } else {
2419       if (o isa MapEnemy) return o;
2420     }
2421   }
2422   return none;
2426 final MapObject forEachObject (scope bool delegate (MapObject o) dg, optional bool allowSpectrals, optional class!MapObject castClass) {
2427   if (!dg) return none;
2428   if (!castClass) castClass = MapObject;
2429   foreach (MapObject o; objGrid.allObjectsSafe(castClass)) {
2430     if (!allowSpectrals && o.spectral) continue;
2431     if (dg(o)) return o;
2432   }
2433   return none;
2437 final MapObject forEachObjectAtPoint (int xpos, int ypos, scope bool delegate (MapObject o) dg, optional bool precise) {
2438   if (!dg) return none;
2439   if (!specified_precise) precise = true;
2440   foreach (MapObject o; objGrid.inCellPix(xpos, ypos, precise:precise, castClass:MapObject)) {
2441     if (o.spectral) continue;
2442     if (dg(o)) return o;
2443   }
2444   return none;
2448 final MapObject forEachObjectInRect (int xpos, int ypos, int w, int h, scope bool delegate (MapObject o) dg, optional bool precise) {
2449   if (!dg || w < 1 || h < 1) return none;
2450   if (w == 1 && h == 1) return forEachObjectAtPoint(xpos, ypos, dg!optional, precise!optional);
2451   if (!specified_precise) precise = true;
2452   foreach (MapObject o; objGrid.inRectPix(xpos, ypos, w, h, precise:precise, castClass:MapObject)) {
2453     if (o.spectral) continue;
2454     if (dg(o)) return o;
2455   }
2456   return none;
2460 final MapEntity forEachEntityInRect (int xpos, int ypos, int w, int h, scope bool delegate (MapEntity o) dg, optional bool precise, optional class!MapEntity castClass) {
2461   if (!dg || w < 1 || h < 1) return none;
2462   if (!castClass) castClass = MapEntity;
2463   if (!specified_precise) precise = true;
2464   foreach (MapEntity e; objGrid.inRectPix(xpos, ypos, w, h, precise:precise, castClass:castClass)) {
2465     if (e.spectral) continue;
2466     if (dg(e)) return e;
2467   }
2468   return none;
2472 private final bool cbIsRopeTile (MapTile t) { return (t isa MapTileRope); }
2474 final MapTile isRopeAtPoint (int px, int py) {
2475   return checkTileAtPoint(px, py, &cbIsRopeTile);
2479 //FIXME!
2480 final MapTile isWaterSwimAtPoint (int px, int py) {
2481   return isWaterAtPoint(px, py);
2485 // ////////////////////////////////////////////////////////////////////////// //
2486 private array!MapEntity tmpEntityList;
2488 private final bool cbCollectEntitiesWithMask (MapEntity t) {
2489   if (!t.visible || t.spectral) return false;
2490   tmpEntityList[$] = t;
2491   return false;
2495 final void touchEntitiesWithMask (int x, int y, SpriteFrame frm, scope bool delegate (MapEntity t) dg, optional class!MapEntity castClass) {
2496   if (!frm || frm.bw < 1 || frm.bh < 1 || !dg) return;
2497   if (frm.isEmptyPixelMask) return;
2498   if (!castClass) castClass = MapEntity;
2499   // collect tiles
2500   if (tmpEntityList.length) tmpEntityList.clear();
2501   if (player isa castClass && player.isRectCollisionFrame(frm, x, y)) tmpEntityList[$] = player;
2502   forEachEntityInRect(x+frm.bx, y+frm.by, frm.bw, frm.bh, &cbCollectEntitiesWithMask, castClass:castClass);
2503   foreach (MapEntity e; tmpEntityList) {
2504     if (!e || !e.isInstanceAlive || !e.visible || e.spectral) continue;
2505     if (e.isRectCollisionFrame(frm, x, y)) {
2506       if (dg(e)) break;
2507     }
2508   }
2512 // ////////////////////////////////////////////////////////////////////////// //
2513 final bool cbCollisionAnySolid (MapTile t) { return t.solid; }
2514 final bool cbCollisionAnyMoveableSolid (MapTile t) { return t.solid && t.moveable; }
2515 final bool cbCollisionLiquid (MapTile t) { return (t.water || t.lava); }
2516 final bool cbCollisionAnySolidOrLiquid (MapTile t) { return (t.solid || t.water || t.lava); }
2517 final bool cbCollisionLadder (MapTile t) { return t.ladder; }
2518 final bool cbCollisionLadderTop (MapTile t) { return t.laddertop; }
2519 final bool cbCollisionAnyLadder (MapTile t) { return (t.ladder || t.laddertop); }
2520 final bool cbCollisionAnySolidIce (MapTile t) { return (t.solid && t.ice); }
2521 final bool cbCollisionWater (MapTile t) { return t.water; }
2522 final bool cbCollisionLava (MapTile t) { return t.lava; }
2523 final bool cbCollisionAnyTree (MapTile t) { return t.tree; }
2524 final bool cbCollisionPlatform (MapTile t) { return t.platform; }
2525 final bool cbCollisionLadderOrPlatform (MapTile t) { return (t.ladder || t.platform); }
2526 //final bool cbCollisionAnyHangeable (MapTile t) { return t.hangeable; } // no need to check for solids here, it is done in MapTile initializer
2527 final bool cbCollisionSpringTrap (MapTile t) { return t.springtrap; }
2528 final bool cbCollisionSpikes (MapTile t) { return t.spikes; }
2529 final bool cbCollisionExitTile (MapTile t) { return t.isExitActive(); }
2531 final bool cbCollisionForWhoa (MapTile t) { return (t.solid || t.laddertop); }
2533 //final bool cbCollisionSacAltar (MapTile t) { return (t.objName == 'oSacAltarLeft'); } // only left part, yeah
2534 final bool cbCollisionSacAltar (MapTile t) { return t.sacrificingAltar; }
2537 // ////////////////////////////////////////////////////////////////////////// //
2538 transient MapTileTemp tempSolidTile;
2540 private final MapTileTemp makeWalkeableSolidTile (MapObject o) {
2541   if (!tempSolidTile) {
2542     tempSolidTile = SpawnObject(MapTileTemp);
2543   } else if (!tempSolidTile.isInstanceAlive) {
2544     delete tempSolidTile;
2545     tempSolidTile = SpawnObject(MapTileTemp);
2546   }
2547   // setup data
2548   tempSolidTile.level = self;
2549   tempSolidTile.global = global;
2550   tempSolidTile.solid = true;
2551   tempSolidTile.objName = MapTileTemp.default.objName;
2552   tempSolidTile.objType = MapTileTemp.default.objType;
2553   tempSolidTile.e = o;
2554   tempSolidTile.fltx = o.fltx;
2555   tempSolidTile.flty = o.flty;
2556   return tempSolidTile;
2560 final MapTile checkTilesInRect (int x0, int y0, const int w, const int h, optional scope bool delegate (MapTile dg) dg, optional bool precise) {
2561   if (w < 1 || h < 1) return none;
2562   if (w == 1 && h == 1) return checkTileAtPoint(x0, y0, dg!optional);
2563   int x1 = x0+w-1, y1 = y0+h-1;
2564   if (x0 >= tilesWidth*16 || y0 >= tilesHeight*16 || x1 < 0 || y1 < 0) return none;
2565   if (!specified_precise) precise = true;
2566   if (!dg) dg = &cbCollisionAnySolid;
2568   // check walkable solid objects too
2569   foreach (MapEntity e; objGrid.inRectPix(x0, y0, w, h, precise:precise)) {
2570     if (e.spectral || !e.visible) continue;
2571     auto t = MapTile(e);
2572     if (t) {
2573       if (dg(t)) return t;
2574       continue;
2575     }
2576     auto o = MapObject(e);
2577     if (o && o.walkableSolid) {
2578       t = makeWalkeableSolidTile(o);
2579       if (dg(t)) return t;
2580       continue;
2581     }
2582   }
2584   return none;
2588 final MapTile checkTileAtPoint (int x0, int y0, optional scope bool delegate (MapTile dg) dg, optional bool precise) {
2589   if (x0 < 0 || y0 < 0 || x0 >= tilesWidth*16 || y0 >= tilesHeight*16) return none;
2590   if (!specified_precise) precise = true;
2591   if (!dg) dg = &cbCollisionAnySolid;
2593   // check walkable solid objects
2594   foreach (MapEntity e; objGrid.inCellPix(x0, y0, precise:precise)) {
2595     if (e.spectral || !e.visible) continue;
2596     auto t = MapTile(e);
2597     if (t) {
2598       if (dg(t)) return t;
2599       continue;
2600     }
2601     auto o = MapObject(e);
2602     if (o && o.walkableSolid) {
2603       t = makeWalkeableSolidTile(o);
2604       if (dg(t)) return t;
2605       continue;
2606     }
2607   }
2609   return none;
2613 // ////////////////////////////////////////////////////////////////////////// //
2614 final MapTile isSolidAtPoint (int px, int py) { return checkTileAtPoint(px, py); }
2615 final MapTile isSolidInRect (int rx, int ry, int rw, int rh) { return checkTilesInRect(rx, ry, rw, rh); }
2616 final MapTile isLadderAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadder); }
2617 final MapTile isLadderOrPlatformAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadderOrPlatform); }
2618 final MapTile isLadderTopAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadderTop); }
2619 final MapTile isAnyLadderAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyLadder); }
2620 final MapTile isWaterAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionWater); }
2621 final MapTile isLavaAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLava); }
2622 final MapTile isLiquidAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLiquid); }
2623 final MapTile isIceAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnySolidIce); }
2624 final MapTile isTreeAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyTree); }
2625 //final MapTile isHangeableAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyHangeable); }
2628 // ////////////////////////////////////////////////////////////////////////// //
2629 final void setTileTypeAt (int tileX, int tileY, LevelGen::RType rt) {
2630   if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) roomType[tileX, tileY] = rt;
2634 //FIXME: make this faster
2635 transient float gtagX, gtagY;
2637 // only non-moveables and non-specials
2638 final MapTile getTileAtGrid (int tileX, int tileY) {
2639   gtagX = tileX*16;
2640   gtagY = tileY*16;
2641   return checkTileAtPoint(tileX*16, tileY*16, delegate bool (MapTile t) {
2642     if (t.spectral || t.moveable || t.toSpecialGrid || !t.visible) return false;
2643     if (t.fltx != gtagX || t.flty != gtagY) return false; // emulate grid
2644     if (t.width != 16 || t.height != 16) return false;
2645     return true;
2646   }, precise:false);
2647   //return (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight ? tiles[tileX, tileY] : none);
2651 final MapTile getTileAtGridAny (int tileX, int tileY) {
2652   gtagX = tileX*16;
2653   gtagY = tileY*16;
2654   return checkTileAtPoint(tileX*16, tileY*16, delegate bool (MapTile t) {
2655     if (t.spectral /*|| t.moveable*/ || !t.visible) return false;
2656     if (t.fltx != gtagX || t.flty != gtagY) return false; // emulate grid
2657     if (t.width != 16 || t.height != 16) return false;
2658     return true;
2659   }, precise:false);
2660   //return (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight ? tiles[tileX, tileY] : none);
2664 final bool isNamedTileAt (int tileX, int tileY, name atypename) {
2665   if (!atypename) return false;
2666   auto t = getTileAtGridAny(tileX, tileY);
2667   return (t && t.objName == atypename);
2671 final void setTileAtGrid (int tileX, int tileY, MapTile tile) {
2672   if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
2673     if (tile) {
2674       tile.fltx = tileX*16;
2675       tile.flty = tileY*16;
2676       if (!tile.dontReplaceOthers) {
2677         auto osp = tile.spectral;
2678         tile.spectral = true;
2679         auto t = getTileAtGridAny(tileX, tileY);
2680         tile.spectral = osp;
2681         if (t && !t.immuneToReplacement) {
2682           writeln("REPLACING tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
2683           writeln("      NEW tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (tile.toSpecialGrid ? "special " : ""), "grid, pos=(", tile.fltx/16.0, ",", tile.flty/16.0, ")");
2684           t.instanceRemove();
2685         }
2686       }
2687       insertObject(tile);
2688     } else {
2689       auto t = getTileAtGridAny(tileX, tileY);
2690       if (t && !t.immuneToReplacement) {
2691         writeln("REMOVING tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
2692         t.instanceRemove();
2693       }
2694     }
2695   }
2699 // ////////////////////////////////////////////////////////////////////////// //
2700 // return `true` from delegate to stop
2701 MapTile forEachSolidTileOnGrid (scope bool delegate (int tileX, int tileY, MapTile t) dg) {
2702   if (!dg) return none;
2703   foreach (MapTile t; objGrid.allObjectsSafe(MapTile)) {
2704     if (t.spectral || !t.solid || !t.visible) continue;
2705     if (t.ix%16 != 0 || t.iy%16 != 0) continue; // emulate grid
2706     if (t.width != 16 || t.height != 16) continue;
2707     if (dg(t.ix/16, t.iy/16, t)) return t;
2708   }
2709   return none;
2713 // ////////////////////////////////////////////////////////////////////////// //
2714 // return `true` from delegate to stop
2715 MapTile forEachTile (scope bool delegate (MapTile t) dg, optional class!MapTile castClass) {
2716   if (!dg) return none;
2717   if (!castClass) castClass = MapTile;
2718   foreach (MapTile t; objGrid.allObjectsSafe(castClass)) {
2719     if (t.spectral || !t.visible) continue;
2720     if (dg(t)) return t;
2721   }
2722   return none;
2726 // ////////////////////////////////////////////////////////////////////////// //
2727 final void fixWallTiles () {
2728   foreach (MapTile t; objGrid.allObjectsSafe(MapTile)) t.beautifyTile();
2732 // ////////////////////////////////////////////////////////////////////////// //
2733 final MapTile isCollisionAtPoint (int px, int py, optional scope bool delegate (MapTile dg) dg) {
2734   if (!dg) dg = &cbCollisionAnySolid;
2735   return checkTilesInRect(px, py, 1, 1, dg);
2739 // ////////////////////////////////////////////////////////////////////////// //
2740 string scrGetKaliGift (MapTile altar, optional name gift) {
2741   string res;
2743   // find other side of the altar
2744   int sx = player.ix, sy = player.iy;
2745   if (altar) {
2746     sx = altar.ix;
2747     sy = altar.iy;
2748     auto a2 = isCollisionAtPoint(altar.ix+18, altar.iy+8, &cbCollisionSacAltar);
2749     if (!a2) a2 = isCollisionAtPoint(altar.ix-8, altar.iy+8, &cbCollisionSacAltar);
2750     if (a2) { sx = a2.ix; sy = a2.iy; }
2751   }
2753        if (global.favor <= -8) res = "SHE SEEMS VERY ANGRY WITH YOU!";
2754   else if (global.favor < 0) res = "SHE SEEMS ANGRY WITH YOU.";
2755   else if (global.favor == 0) res = "SHE HAS FORGIVEN YOU!";
2756   else if (global.favor >= 32) {
2757     if (global.kaliGift >= 3 && global.favor >= 32+(global.kaliGift-2)*16) {
2758       res = "YOU FEEL INVIGORATED!";
2759       global.kaliGift += 1;
2760       global.plife += global.randOther(4, 8);
2761     } else if (global.kaliGift >= 3) {
2762       res = "SHE SEEMS ECSTATIC WITH YOU!";
2763     } else if (global.bombs < 80) {
2764       res = "YOUR SATCHEL FEELS VERY FULL NOW!";
2765       global.kaliGift = 3;
2766       global.bombs = 99;
2767     } else {
2768       res = "YOU FEEL INVIGORATED!";
2769       global.kaliGift += 1;
2770       global.plife += global.randOther(4, 8);
2771     }
2772   } else if (global.favor >= 16) {
2773     if (global.kaliGift >= 2) {
2774       res = "SHE SEEMS VERY HAPPY WITH YOU!";
2775     } else {
2776       res = "SHE BESTOWS A GIFT UPON YOU!";
2777       global.kaliGift = 2;
2778       // poofs
2779       auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2780       obj.xVel = -1;
2781       obj.yVel = 0;
2782       obj = MakeMapObject(sx, sy-8, 'oPoof');
2783       obj.xVel = 1;
2784       obj.yVel = 0;
2785       // a gift
2786       obj = none;
2787       if (gift) obj = MakeMapObject(sx, sy-8, gift);
2788       if (!obj) obj = MakeMapObject(sx, sy-8, 'oKapala');
2789     }
2790   } else if (global.favor >= 8) {
2791     if (global.kaliGift >= 1) {
2792       res = "SHE SEEMS HAPPY WITH YOU.";
2793     } else {
2794       res = "SHE BESTOWS A GIFT UPON YOU!";
2795       global.kaliGift = 1;
2796       //rAltar = instance_nearest(x, y, oSacAltarRight);
2797       //if (instance_exists(rAltar)) {
2798       // poofs
2799       auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2800       obj.xVel = -1;
2801       obj.yVel = 0;
2802       obj = MakeMapObject(sx, sy-8, 'oPoof');
2803       obj.xVel = 1;
2804       obj.yVel = 0;
2805       obj = none;
2806       if (gift) obj = MakeMapObject(sx, sy-8, gift);
2807       if (!obj) {
2808         auto n = global.randOther(1, 8);
2809         auto m = n;
2810         for (;;) {
2811           name aname = '';
2812                if (n == 1 && !global.hasCape && !global.hasJetpack) aname = 'oCapePickup';
2813           else if (n == 2 && !global.hasGloves) aname = 'oGloves';
2814           else if (n == 3 && !global.hasSpectacles) aname = 'oSpectacles';
2815           else if (n == 4 && !global.hasMitt) aname = 'oMitt';
2816           else if (n == 5 && !global.hasSpringShoes) aname = 'oSpringShoes';
2817           else if (n == 6 && !global.hasSpikeShoes) aname = 'oSpikeShoes';
2818           else if (n == 7 && !global.hasStickyBombs) aname = 'oPaste';
2819           else if (n == 8 && !global.hasCompass) aname = 'oCompass';
2820           if (aname) {
2821             obj = MakeMapObject(sx, sy-8, aname);
2822             if (obj) break;
2823           }
2824           ++n;
2825           if (n > 8) n = 1;
2826           if (n == m) {
2827             obj = MakeMapObject(sx, sy-8, (global.hasJetpack ? 'oBombBox' : 'oJetpack'));
2828             break;
2829           }
2830         }
2831       }
2832     }
2833   } else if (global.favor > 0) {
2834     res = "SHE SEEMS PLEASED WITH YOU.";
2835   }
2837   /*
2838   if (argument1) {
2839     global.message = "";
2840     res = "KALI DEVOURS YOU!"; // sacrifice is player
2841   }
2842   */
2844   return res;
2848 void performSacrifice (MapObject what, MapTile where) {
2849   if (!what || !what.isInstanceAlive) return;
2850   MakeMapObject(what.ix+8, what.iy+8, 'oFlame');
2851   if (where) where.playSound('sndSmallExplode'); else what.playSound('sndSmallExplode');
2852   if (!what.bloodless) what.scrCreateBlood(what.ix+8, what.iy+8, 3);
2854   string msg = "KALI ACCEPTS THE SACRIFICE!";
2856   auto idol = ItemGoldIdol(what);
2857   if (idol) {
2858     ++stats.totalSacrifices;
2859          if (global.favor <= -8) msg = "SHE SEEMS VERY ANGRY WITH YOU!";
2860     else if (global.favor < 0) msg = "SHE SEEMS ANGRY WITH YOU.";
2861     else if (global.favor >= 0) {
2862       // find other side of the altar
2863       int sx = player.ix, sy = player.iy;
2864       auto altar = where;
2865       if (altar) {
2866         sx = altar.ix;
2867         sy = altar.iy;
2868         auto a2 = isCollisionAtPoint(altar.ix+18, altar.iy+8, &cbCollisionSacAltar);
2869         if (!a2) a2 = isCollisionAtPoint(altar.ix-8, altar.iy+8, &cbCollisionSacAltar);
2870         if (a2) { sx = a2.ix; sy = a2.iy; }
2871       }
2872       // poofs
2873       auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2874       obj.xVel = -1;
2875       obj.yVel = 0;
2876       obj = MakeMapObject(sx, sy-8, 'oPoof');
2877       obj.xVel = 1;
2878       obj.yVel = 0;
2879       // a gift
2880       obj = MakeMapObject(sx, sy-16-8, 'oMonkeyGold');
2881     }
2882     osdMessage(msg, 6.66);
2883     scrShake(10);
2884     idol.instanceRemove();
2885     return;
2886   }
2888   if (global.favor <= -8) {
2889     msg = "KALI DEVOURS THE SACRIFICE!";
2890   } else if (global.favor < 0) {
2891     global.favor += (what.status == MapObject::STUNNED ? what.favor : round(what.favor/2.0)); //k8: added `round()`
2892     if (what.favor > 0) what.favor = 0;
2893   } else {
2894     global.favor += (what.status == MapObject::STUNNED ? what.favor : round(what.favor/2.0)); //k8: added `round()`
2895   }
2897   /*!!
2898        if (myAltar.gift1 != "" and global.kaliGift == 0) scrGetKaliGift(myAltar.gift1);
2899   else if (myAltar.gift2 != "" and global.kaliGift == 1) scrGetKaliGift(myAltar.gift2);
2900   else scrGetKaliGift("");
2901   */
2903   // sacrifice is player?
2904   if (what isa PlayerPawn) {
2905     ++stats.totalSelfSacrifices;
2906     msg = "KALI DEVOURS YOU!";
2907     player.visible = false;
2908     player.removeBallAndChain(temp:true);
2909     player.dead = true;
2910     player.status = MapObject::DEAD;
2911   } else {
2912     ++stats.totalSacrifices;
2913     auto msg2 = scrGetKaliGift(where);
2914     what.instanceRemove();
2915     if (msg2) msg = va("%s\n%s", msg, msg2);
2916   }
2918   osdMessage(msg, 6.66);
2920   //!if (isRealLevel()) global.totalSacrifices += 1;
2922   //!global.messageTimer = 200;
2923   //!global.shake = 10;
2924   scrShake(10);
2926   /*damsel
2927   instance_create(x, y, oFlame);
2928   playSound(global.sndSmallExplode);
2929   scrCreateBlood(x, y, 3);
2930   global.message = "KALI ACCEPTS YOUR SACRIFICE!";
2931   if (global.favor <= -8) {
2932     global.message = "KALI DEVOURS YOUR SACRIFICE!";
2933   } else if (global.favor < 0) {
2934     if (status == 98) global.favor += favor*1.5; else global.favor += favor;
2935     if (favor > 0) favor = 0;
2936   } else {
2937     if (status == 98) global.favor += favor*1.5; else global.favor += favor;
2938   }
2940        if (myAltar.gift1 != "" and global.kaliGift == 0) scrGetFavorMsg(myAltar.gift1);
2941   else if (myAltar.gift2 != "" and global.kaliGift == 1) scrGetFavorMsg(myAltar.gift2);
2942   else scrGetFavorMsg("");
2944   global.messageTimer = 200;
2945   global.shake = 10;
2946   instance_destroy();
2947   */
2951 // ////////////////////////////////////////////////////////////////////////// //
2952 final void addBackgroundGfxDetails () {
2953   // add background details
2954   //if (global.customLevel) return;
2955   foreach (; 0..20) {
2956     // bg = instance_create(16*randRoom(1, 42), 16*randRoom(1, 33), oCaveBG);
2957          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);
2958     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);
2959     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);
2960     else MakeMapBackTile('bgExtras', 32*global.randRoom(0, 1), 0, 32, 32, 16*global.randRoom(1, 42), 16*global.randRoom(1, 33), 10002);
2961   }
2965 // ////////////////////////////////////////////////////////////////////////// //
2966 private final void fixRealViewStart () {
2967   int scale = global.scale;
2968   realViewStart.x = clamp(realViewStart.x, viewMin.x*scale, viewMax.x*scale-viewWidth);
2969   realViewStart.y = clamp(realViewStart.y, viewMin.y*scale, viewMax.y*scale-viewHeight);
2973 final int cameraCurrX () { return realViewStart.x/global.scale; }
2974 final int cameraCurrY () { return realViewStart.y/global.scale; }
2977 private final void fixViewStart () {
2978   int scale = global.scale;
2979   viewStart.x = clamp(viewStart.x, viewMin.x*scale, viewMax.x*scale-viewWidth);
2980   viewStart.y = clamp(viewStart.y, viewMin.y*scale, viewMax.y*scale-viewHeight);
2984 final void centerViewAtPlayer () {
2985   if (viewWidth < 1 || viewHeight < 1 || !player) return;
2986   centerViewAt(player.xCenter, player.yCenter);
2990 final void centerViewAt (int x, int y) {
2991   if (viewWidth < 1 || viewHeight < 1) return;
2993   cameraSlideToSpeed.x = 0;
2994   cameraSlideToSpeed.y = 0;
2995   cameraSlideToPlayer = 0;
2997   int scale = global.scale;
2998   x *= scale;
2999   y *= scale;
3000   realViewStart.x = clamp(x-viewWidth/2, 0, tilesWidth*16*scale-viewWidth);
3001   realViewStart.y = clamp(y-viewHeight/2, 0, tilesHeight*16*scale-viewHeight);
3002   fixRealViewStart();
3004   viewStart.x = realViewStart.x;
3005   viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
3006   fixViewStart();
3008   if (onCameraTeleported) onCameraTeleported();
3012 const int ViewPortToleranceX = 16*1+8;
3013 const int ViewPortToleranceY = 16*1+8;
3015 final void fixCamera () {
3016   if (!player) return;
3017   if (viewWidth < 1 || viewHeight < 1) return;
3018   int scale = global.scale;
3019   auto alwaysCenterX = global.config.alwaysCenterPlayer;
3020   auto alwaysCenterY = alwaysCenterX;
3021   // calculate offset from viewport center (in game units), and fix viewport
3023   int camDestX = player.ix+8;
3024   int camDestY = player.iy+8;
3025   if (!cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y) != 0) {
3026     // slide camera to point
3027     if (cameraSlideToSpeed.x) camDestX = cameraSlideToCurr.x;
3028     if (cameraSlideToSpeed.y) camDestY = cameraSlideToCurr.y;
3029     int dx = cameraSlideToDest.x-camDestX;
3030     int dy = cameraSlideToDest.y-camDestY;
3031     //writeln("speed:(", cameraSlideToSpeed.x, ",", cameraSlideToSpeed.y, "); cur:(", camDestX, ",", camDestY, "); dest:(", cameraSlideToDest.x, ",", cameraSlideToDest.y, "); delta=(", dx, ",", dy, ")");
3032     if (dx && cameraSlideToSpeed.x != 0) {
3033       alwaysCenterX = true;
3034       if (abs(dx) <= abs(cameraSlideToSpeed.x)) {
3035         camDestX = cameraSlideToDest.x;
3036       } else {
3037         camDestX += sign(dx)*abs(cameraSlideToSpeed.x);
3038       }
3039     }
3040     if (dy && abs(cameraSlideToSpeed.y) != 0) {
3041       alwaysCenterY = true;
3042       if (abs(dy) <= cameraSlideToSpeed.y) {
3043         camDestY = cameraSlideToDest.y;
3044       } else {
3045         camDestY += sign(dy)*abs(cameraSlideToSpeed.y);
3046       }
3047     }
3048     //writeln("  new:(", camDestX, ",", camDestY, ")");
3049     if (cameraSlideToSpeed.x) cameraSlideToCurr.x = camDestX;
3050     if (cameraSlideToSpeed.y) cameraSlideToCurr.y = camDestY;
3051   }
3053   // horizontal
3054   if (cameraSlideToSpeed.x && !cameraSlideToPlayer) {
3055     realViewStart.x = clamp(camDestX*scale, 0, tilesWidth*16*scale-viewWidth);
3056   } else if (!player.cameraBlockX) {
3057     int x = camDestX*scale;
3058     int cx = realViewStart.x;
3059     if (alwaysCenterX) {
3060       cx = x-viewWidth/2;
3061     } else {
3062       int xofs = x-(cx+viewWidth/2);
3063            if (xofs < -ViewPortToleranceX*scale) cx += xofs+ViewPortToleranceX*scale;
3064       else if (xofs > ViewPortToleranceX*scale) cx += xofs-ViewPortToleranceX*scale;
3065     }
3066     // slide back to player?
3067     if (cameraSlideToPlayer && cameraSlideToSpeed.x) {
3068       int prevx = cameraSlideToCurr.x*scale;
3069       int dx = (cx-prevx)/scale;
3070       if (abs(dx) <= cameraSlideToSpeed.x) {
3071         writeln("BACKSLIDE X COMPLETE!");
3072         cameraSlideToSpeed.x = 0;
3073       } else {
3074         cx = prevx+(sign(dx)*cameraSlideToSpeed.x)*scale;
3075         cameraSlideToCurr.x += sign(dx)*cameraSlideToSpeed.x;
3076         if ((dx < 0 && cx <= 0) || (dx > 0 && cx >= tilesWidth*16*scale-viewWidth)) {
3077           writeln("BACKSLIDE X COMPLETE!");
3078           cameraSlideToSpeed.x = 0;
3079         }
3080       }
3081     }
3082     realViewStart.x = clamp(cx, 0, tilesWidth*16*scale-viewWidth);
3083   }
3085   // vertical
3086   if (cameraSlideToSpeed.y && !cameraSlideToPlayer) {
3087     realViewStart.y = clamp(camDestY*scale, 0, tilesHeight*16*scale-viewHeight);
3088   } else if (!player.cameraBlockY) {
3089     int y = camDestY*scale;
3090     int cy = realViewStart.y;
3091     if (alwaysCenterY) {
3092       cy = y-viewHeight/2;
3093     } else {
3094       int yofs = y-(cy+viewHeight/2);
3095            if (yofs < -ViewPortToleranceY*scale) cy += yofs+ViewPortToleranceY*scale;
3096       else if (yofs > ViewPortToleranceY*scale) cy += yofs-ViewPortToleranceY*scale;
3097     }
3098     // slide back to player?
3099     if (cameraSlideToPlayer && cameraSlideToSpeed.y) {
3100       int prevy = cameraSlideToCurr.y*scale;
3101       int dy = (cy-prevy)/scale;
3102       if (abs(dy) <= cameraSlideToSpeed.y) {
3103         writeln("BACKSLIDE Y COMPLETE!");
3104         cameraSlideToSpeed.y = 0;
3105       } else {
3106         cy = prevy+(sign(dy)*cameraSlideToSpeed.y)*scale;
3107         cameraSlideToCurr.y += sign(dy)*cameraSlideToSpeed.y;
3108         if ((dy < 0 && cy <= 0) || (dy > 0 && cy >= tilesHeight*16*scale-viewHeight)) {
3109           writeln("BACKSLIDE Y COMPLETE!");
3110           cameraSlideToSpeed.y = 0;
3111         }
3112       }
3113     }
3114     realViewStart.y = clamp(cy, 0, tilesHeight*16*scale-viewHeight);
3115   }
3117   if (cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y) == 0) cameraSlideToPlayer = 0;
3119   fixRealViewStart();
3120   //writeln("  new2:(", cameraCurrX, ",", cameraCurrY, ")");
3122   viewStart.x = realViewStart.x;
3123   viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
3124   fixViewStart();
3128 // ////////////////////////////////////////////////////////////////////////// //
3129 // x0 and y0 are non-scaled (and will be scaled)
3130 final void drawSpriteAt (name sprName, float frnumf, int x0, int y0, optional bool small) {
3131   if (!sprName) return;
3132   auto spr = sprStore[sprName];
3133   if (!spr || !spr.frames.length) return;
3134   int scale = global.scale;
3135   x0 *= scale;
3136   y0 *= scale;
3137   int frnum = max(0, trunc(frnumf))%spr.frames.length;
3138   auto sfr = spr.frames[frnum];
3139   int sx0 = x0-sfr.xofs*scale;
3140   int sy0 = y0-sfr.yofs*scale;
3141   if (small && scale > 1) {
3142     sfr.tex.blitExt(sx0, sy0, round(sx0+sfr.tex.width*(scale/2.0)), round(sy0+sfr.tex.height*(scale/2.0)), 0, 0);
3143   } else {
3144     sfr.tex.blitAt(sx0, sy0, scale);
3145   }
3149 // x0 and y0 are non-scaled (and will be scaled)
3150 final void drawTextAt (int x0, int y0, string text) {
3151   if (!text) return;
3152   int scale = global.scale;
3153   x0 *= scale;
3154   y0 *= scale;
3155   sprStore.renderText(x0, y0, text, scale);
3159 void renderCompass (float currFrameDelta) {
3160   if (!global.hasCompass) return;
3162   /*
3163   if (isRoom("rOlmec")) {
3164     global.exitX = 648;
3165     global.exitY = 552;
3166   } else if (isRoom("rOlmec2")) {
3167     global.exitX = 648;
3168     global.exitY = 424;
3169   }
3170   */
3172   bool hasMessage = osdHasMessage();
3173   foreach (MapTile et; allExits) {
3174     // original compass
3175     int exitX = et.ix, exitY = et.iy;
3176     int vx0 = viewStart.x/global.scale, vy0 = viewStart.y/global.scale;
3177     int vx1 = (viewStart.x+viewWidth)/global.scale;
3178     int vy1 = (viewStart.y+viewHeight)/global.scale;
3179     if (exitY > vy1-16) {
3180       if (exitX < vx0) {
3181         drawSpriteAt(hasMessage ? 'sCompassSmallLL' : 'sCompassLL', 0, 0, 224);
3182       } else if (exitX > vx1-16) {
3183         drawSpriteAt(hasMessage ? 'sCompassSmallLR' : 'sCompassLR', 0, 304, 224);
3184       } else {
3185         drawSpriteAt(hasMessage ? 'sCompassSmallDown' : 'sCompassDown', 0, exitX-vx0, 224);
3186       }
3187     } else if (exitX < vx0) {
3188       drawSpriteAt(hasMessage ? 'sCompassSmallLeft' : 'sCompassLeft', 0, 0, exitY-vy0);
3189     } else if (exitX > vx1-16) {
3190       drawSpriteAt(hasMessage ? 'sCompassSmallRight' : 'sCompassRight', 0, 304, exitY-vy0);
3191     }
3192   }
3196 final bool totalsNameCmpCB (ref GameStats::TotalItem a, ref GameStats::TotalItem b) {
3197   auto sa = string(a.objName);
3198   auto sb = string(b.objName);
3199   return (sa < sb);
3202 void renderTransitionInfo (float currFrameDelta) {
3203   //FIXME!
3204   /*
3205   GameStats.sortTotalsList(stats.kills, &totalsNameCmpCB);
3207   int maxLen = 0;
3208   foreach (int idx, ref auto k; stats.kills) {
3209     string s = string(k);
3210     maxLen = max(maxLen, s.length);
3211   }
3212   maxLen *= 8;
3214   sprStore.loadFont('sFontSmall');
3215   Video.color = 0xff_ff_00;
3216   foreach (int idx, ref auto k; stats.kills) {
3217     int deaths = 0;
3218     foreach (int xidx, ref auto d; stats.totalKills) {
3219       if (d.objName == k) { deaths = d.count; break; }
3220     }
3221     //drawTextAt(16, 4+idx*8, va("%s: %d (%d)", string(k.objName).toUpperCase, k.count, deaths));
3222     drawTextAt(16, 4+idx*8, string(k).toUpperCase);
3223     drawTextAt(16+maxLen, 4+idx*8, va(": %d (%d)", k.count, deaths));
3224   }
3225   */
3229 void renderGhostTimer (float currFrameDelta) {
3230   if (ghostTimeLeft <= 0) return;
3231   //ghostTimeLeft /= 30; // frames -> seconds
3233   int hgt = Video.screenHeight-64;
3234   if (hgt < 1) return;
3235   int rhgt = round(float(hgt)*(float(ghostTimeLeft)/30.0)/1000.0);
3236   //writeln("hgt=", hgt, "; rhgt=", rhgt, "; gtl=", ghostTimeLeft/30);
3237   if (rhgt > 0) {
3238     auto oclr = Video.color;
3239     Video.color = 0xcf_ff_7f_00;
3240     Video.fillRect(Video.screenWidth-20, 32, 16, hgt-rhgt);
3241     Video.color = 0x7f_ff_7f_00;
3242     Video.fillRect(Video.screenWidth-20, 32+(hgt-rhgt), 16, rhgt);
3243     Video.color = oclr;
3244   }
3248 void renderStarsHUD (float currFrameDelta) {
3249   bool scumSmallHud = global.config.scumSmallHud;
3251   //auto life = max(0, global.plife);
3252   int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3253   int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3254   //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3256   int hhup;
3258   if (scumSmallHud) {
3259     sprStore.loadFont('sFontSmall');
3260     hhup = 6;
3261   } else {
3262     sprStore.loadFont('sFont');
3263     hhup = 2;
3264   }
3266   Video.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3267   //draw_sprite(sHeart, -1, view_xview[0]+8, view_yview[0]+8);
3268   //draw_text(view_xview[0]+24, view_yview[0]+8, life);
3269   if (scumSmallHud) {
3270     if (global.plife == 1) {
3271       drawSpriteAt('sHeartSmallBlink', global.heartBlink, 10, hhup-4);
3272       global.heartBlink += 0.1;
3273       if (global.heartBlink > 3) global.heartBlink = 0;
3274     } else {
3275       drawSpriteAt('sHeartSmall', -1, 10, hhup-4);
3276       global.heartBlink = 0;
3277     }
3278   } else {
3279     if (global.plife == 1) {
3280       drawSpriteAt('sHeartBlink', global.heartBlink, 8, hhup);
3281       global.heartBlink += 0.1;
3282       if (global.heartBlink > 3) global.heartBlink = 0;
3283     } else {
3284       drawSpriteAt('sHeart', -1, 8, hhup);
3285       global.heartBlink = 0;
3286     }
3287   }
3288   int life = clamp(global.plife, 0, 99);
3289   drawTextAt(16+8, hhup, va("%d", life));
3291   //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3292   drawSpriteAt('sShopkeeperIcon', -1, 64, hhup, scumSmallHud);
3293   drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", starsKills));
3295   if (starsRoomTimer1 > 0) {
3296     sprStore.loadFont('sFontSmall');
3297     Video.color = 0xff_ff_00;
3298     int scale = global.scale;
3299     sprStore.renderMultilineTextCentered(Video.screenWidth/2, 216*scale, va("SHOTGUN CHALLENGE BEGINS IN ~%d~", (starsRoomTimer1/30)+1), scale, 0xff_00_00);
3300   }
3304 void renderSunHUD (float currFrameDelta) {
3305   bool scumSmallHud = global.config.scumSmallHud;
3307   //auto life = max(0, global.plife);
3308   int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3309   int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3310   //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3312   int hhup;
3314   if (scumSmallHud) {
3315     sprStore.loadFont('sFontSmall');
3316     hhup = 6;
3317   } else {
3318     sprStore.loadFont('sFont');
3319     hhup = 2;
3320   }
3322   Video.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3323   //draw_sprite(sHeart, -1, view_xview[0]+8, view_yview[0]+8);
3324   //draw_text(view_xview[0]+24, view_yview[0]+8, life);
3325   if (scumSmallHud) {
3326     if (global.plife == 1) {
3327       drawSpriteAt('sHeartSmallBlink', global.heartBlink, 10, hhup-4);
3328       global.heartBlink += 0.1;
3329       if (global.heartBlink > 3) global.heartBlink = 0;
3330     } else {
3331       drawSpriteAt('sHeartSmall', -1, 10, hhup-4);
3332       global.heartBlink = 0;
3333     }
3334   } else {
3335     if (global.plife == 1) {
3336       drawSpriteAt('sHeartBlink', global.heartBlink, 8, hhup);
3337       global.heartBlink += 0.1;
3338       if (global.heartBlink > 3) global.heartBlink = 0;
3339     } else {
3340       drawSpriteAt('sHeart', -1, 8, hhup);
3341       global.heartBlink = 0;
3342     }
3343   }
3344   int life = clamp(global.plife, 0, 99);
3345   drawTextAt(16+8, hhup, va("%d", life));
3347   //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3348   drawSpriteAt('sDamselIcon', -1, 64, hhup, scumSmallHud);
3349   drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", sunScore));
3351   if (sunRoomTimer1 > 0) {
3352     sprStore.loadFont('sFontSmall');
3353     Video.color = 0xff_ff_00;
3354     int scale = global.scale;
3355     sprStore.renderMultilineTextCentered(Video.screenWidth/2, 216*scale, va("DAMSEL CHALLENGE BEGINS IN ~%d~", (sunRoomTimer1/30)+1), scale, 0xff_00_00);
3356   }
3360 void renderMoonHUD (float currFrameDelta) {
3361   bool scumSmallHud = global.config.scumSmallHud;
3363   //auto life = max(0, global.plife);
3364   int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3365   int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3366   //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3368   int hhup;
3370   if (scumSmallHud) {
3371     sprStore.loadFont('sFontSmall');
3372     hhup = 6;
3373   } else {
3374     sprStore.loadFont('sFont');
3375     hhup = 2;
3376   }
3378   Video.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3380   //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3381   drawSpriteAt('sHoopsIcon', -1, 8, hhup, scumSmallHud);
3382   drawTextAt(8+16-(scumSmallHud ? 6 : 0), hhup, va("%d", moonScore));
3383   drawSpriteAt('sTimerIcon', -1, 64, hhup, scumSmallHud);
3384   drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", max(0, moonTimer)));
3386   if (moonRoomTimer1 > 0) {
3387     sprStore.loadFont('sFontSmall');
3388     Video.color = 0xff_ff_00;
3389     int scale = global.scale;
3390     sprStore.renderMultilineTextCentered(Video.screenWidth/2, 216*scale, va("ARCHERY CHALLENGE BEGINS IN ~%d~", (moonRoomTimer1/30)+1), scale, 0xff_00_00);
3391   }
3395 void renderHUD (float currFrameDelta) {
3396   if (levelKind == LevelKind.Stars) { renderStarsHUD(currFrameDelta); return; }
3397   if (levelKind == LevelKind.Sun) { renderSunHUD(currFrameDelta); return; }
3398   if (levelKind == LevelKind.Moon) { renderMoonHUD(currFrameDelta); return; }
3400   if (!isHUDEnabled()) return;
3402   if (levelKind == LevelKind.Transition) { renderTransitionInfo(currFrameDelta); return; }
3404   int lifeX = 4; // 8
3405   int bombX = 56;
3406   int ropeX = 104;
3407   int ammoX = 152;
3408   int moneyX = 200;
3409   int hhup;
3410   bool scumSmallHud = global.config.scumSmallHud;
3411   if (!global.config.optSGAmmo) moneyX = ammoX;
3413   if (scumSmallHud) {
3414     sprStore.loadFont('sFontSmall');
3415     hhup = 6;
3416   } else {
3417     sprStore.loadFont('sFont');
3418     hhup = 0;
3419   }
3420   //int alpha = 0x6f_00_00_00;
3421   int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3422   int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3424   //Video.color = 0xff_ff_ff;
3425   Video.color = 0xff_ff_ff|talpha;
3427   // hearts
3428   if (scumSmallHud) {
3429     if (global.plife == 1) {
3430       drawSpriteAt('sHeartSmallBlink', global.heartBlink, lifeX+2, 4-hhup);
3431       global.heartBlink += 0.1;
3432       if (global.heartBlink > 3) global.heartBlink = 0;
3433     } else {
3434       drawSpriteAt('sHeartSmall', -1, lifeX+2, 4-hhup);
3435       global.heartBlink = 0;
3436     }
3437   } else {
3438     if (global.plife == 1) {
3439       drawSpriteAt('sHeartBlink', global.heartBlink, lifeX, 8-hhup);
3440       global.heartBlink += 0.1;
3441       if (global.heartBlink > 3) global.heartBlink = 0;
3442     } else {
3443       drawSpriteAt('sHeart', -1, lifeX, 8-hhup);
3444       global.heartBlink = 0;
3445     }
3446   }
3448   int life = clamp(global.plife, 0, 99);
3449   //if (!scumHud && life > 99) life = 99;
3450   drawTextAt(lifeX+16, 8-hhup, va("%d", life));
3452   // bombs
3453   if (global.hasStickyBombs && global.stickyBombsActive) {
3454     if (scumSmallHud) drawSpriteAt('sStickyBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sStickyBombIcon', -1, bombX, 8-hhup);
3455   } else {
3456     if (scumSmallHud) drawSpriteAt('sBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sBombIcon', -1, bombX, 8-hhup);
3457   }
3458   int n = global.bombs;
3459   if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3460   drawTextAt(bombX+16, 8-hhup, va("%d", n));
3462   // ropes
3463   if (scumSmallHud) drawSpriteAt('sRopeIconSmall', -1, ropeX+2, 4-hhup); else drawSpriteAt('sRopeIcon', -1, ropeX, 8-hhup);
3464   n = global.rope;
3465   if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3466   drawTextAt(ropeX+16, 8-hhup, va("%d", n));
3468   // shotgun shells
3469   if (global.config.optSGAmmo && (!player || player.holdItem !isa ItemWeaponBow)) {
3470     if (scumSmallHud) drawSpriteAt('sShellsIcon', -1, ammoX+6, 8-hhup); else drawSpriteAt('sShellsIcon', -1, ammoX+7, 12-hhup);
3471     n = global.sgammo;
3472     if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3473     drawTextAt(ammoX+16, 8-hhup, va("%d", n));
3474   } else if (player && player.holdItem isa ItemWeaponBow) {
3475     if (scumSmallHud) drawSpriteAt('sArrowRight', -1, ammoX+6, 8-hhup); else drawSpriteAt('sArrowRight', -1, ammoX+7, 12-hhup);
3476     n = global.arrows;
3477     if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3478     drawTextAt(ammoX+16, 8-hhup, va("%d", n));
3479   }
3481   // money
3482   if (scumSmallHud) drawSpriteAt('sDollarSignSmall', -1, moneyX+2, 3-hhup); else drawSpriteAt('sDollarSign', -1, moneyX, 8-hhup);
3483   drawTextAt(moneyX+16, 8-hhup, va("%d", stats.money-xmoney));
3485   // items
3486   Video.color = 0xff_ff_ff|ialpha;
3488   int ity = (scumSmallHud ? 18-hhup : 24-hhup);
3490   n = 8; //28;
3491   if (global.hasUdjatEye) {
3492     if (global.udjatBlink) drawSpriteAt('sUdjatEyeIcon2', -1, n, ity); else drawSpriteAt('sUdjatEyeIcon', -1, n, ity);
3493     n += 20;
3494   }
3495   if (global.hasAnkh) { drawSpriteAt('sAnkhIcon', -1, n, ity); n += 20; }
3496   if (global.hasCrown) { drawSpriteAt('sCrownIcon', -1, n, ity); n += 20; }
3497   if (global.hasKapala) {
3498          if (global.bloodLevel == 0) drawSpriteAt('sKapalaIcon', 0, n, ity);
3499     else if (global.bloodLevel <= 2) drawSpriteAt('sKapalaIcon', 1, n, ity);
3500     else if (global.bloodLevel <= 4) drawSpriteAt('sKapalaIcon', 2, n, ity);
3501     else if (global.bloodLevel <= 6) drawSpriteAt('sKapalaIcon', 3, n, ity);
3502     else if (global.bloodLevel <= 8) drawSpriteAt('sKapalaIcon', 4, n, ity);
3503     n += 20;
3504   }
3505   if (global.hasSpectacles) { drawSpriteAt('sSpectaclesIcon', -1, n, ity); n += 20; }
3506   if (global.hasGloves) { drawSpriteAt('sGlovesIcon', -1, n, ity); n += 20; }
3507   if (global.hasMitt) { drawSpriteAt('sMittIcon', -1, n, ity); n += 20; }
3508   if (global.hasSpringShoes) { drawSpriteAt('sSpringShoesIcon', -1, n, ity); n += 20; }
3509   if (global.hasJordans) { drawSpriteAt('sJordansIcon', -1, n, ity); n += 20; }
3510   if (global.hasSpikeShoes) { drawSpriteAt('sSpikeShoesIcon', -1, n, ity); n += 20; }
3511   if (global.hasCape) { drawSpriteAt('sCapeIcon', -1, n, ity); n += 20; }
3512   if (global.hasJetpack) { drawSpriteAt('sJetpackIcon', -1, n, ity); n += 20; }
3513   if (global.hasStickyBombs) { drawSpriteAt('sPasteIcon', -1, n, ity); n += 20; }
3514   if (global.hasCompass) { drawSpriteAt('sCompassIcon', -1, n, ity); n += 20; }
3515   if (global.hasParachute) { drawSpriteAt('sParachuteIcon', -1, n, ity); n += 20; }
3517   if (player.holdItem && player.holdItem.objName == 'Bow' && global.arrows > 0) {
3518     int m = 1;
3519     float malpha = 1;
3520     while (m <= global.arrows && m <= 20 && malpha > 0) {
3521       Video.color = trunc(malpha*255)<<24|0xff_ff_ff;
3522       drawSpriteAt('sArrowIcon', -1, n, ity);
3523       n += 4;
3524       if (m > 10) malpha -= 0.1; //and global.arrows > 20 malpha -= 0.1
3525       m += 1;
3526     }
3527   }
3529   if (xmoney > 0) {
3530     sprStore.loadFont('sFontSmall');
3531     Video.color = 0xff_ff_00|talpha;
3532     if (scumSmallHud) drawTextAt(moneyX+8, 8+8-hhup, va("+%d", xmoney));
3533     else drawTextAt(moneyX, 8+16-hhup, va("+%d", xmoney));
3534   }
3536   Video.color = 0xff_ff_ff;
3537   if (global.config.ghostShowTime) renderGhostTimer(currFrameDelta);
3541 // ////////////////////////////////////////////////////////////////////////// //
3542 private transient array!MapEntity renderVisibleCids;
3543 private transient array!MapEntity renderVisibleLights;
3544 private transient array!MapTile renderFrontTiles; // normal, with fg
3546 final bool renderSortByDepth (MapEntity oa, MapEntity ob) {
3547   auto da = oa.depth, db = ob.depth;
3548   if (da == db) return (oa.objId < ob.objId);
3549   return (da < db);
3553 const int RenderEdgePixNormal = 64;
3554 const int RenderEdgePixLight = 256;
3556 #ifndef EXPERIMENTAL_RENDER_CACHE
3557 enum skipListCreation = false;
3558 #endif
3560 void renderWithOfs (int xofs, int yofs, float currFrameDelta) {
3561   int scale = global.scale;
3563   // don't touch framebuffer alpha
3564   Video.colorMask = Video::CMask.Colors;
3565   Video.color = 0xff_ff_ff;
3567   bool isDarkLevel = global.darkLevel;
3569   if (isDarkLevel) {
3570     switch (global.config.scumPlayerLit) {
3571       case 0: player.lightRadius = 0; break; // never
3572       case 1: // only in "scumDarkness"
3573         player.lightRadius = (global.config.scumDarkness >= 2 ? 96 : 32);
3574         break;
3575       case 2:
3576         player.lightRadius = 96;
3577         break;
3578     }
3579   }
3581   // render cave background
3582   if (levBGImg) {
3583     int tsz = 16*scale;
3584     int bgw = levBGImg.tex.width*scale;
3585     int bgh = levBGImg.tex.height*scale;
3586     int bgTW = (tilesWidth*tsz+bgw-1)/bgw;
3587     int bgTH = (tilesHeight*tsz+bgh-1)/bgh;
3588     int bgX0 = max(0, xofs/bgw);
3589     int bgY0 = max(0, yofs/bgh);
3590     int bgX1 = min(bgTW, (xofs+viewWidth+bgw-1)/bgw);
3591     int bgY1 = min(bgTH, (yofs+viewHeight+bgh-1)/bgh);
3592     foreach (int ty; bgY0..bgY1) {
3593       foreach (int tx; bgX0..bgX1) {
3594         int x0 = tx*bgw-xofs;
3595         int y0 = ty*bgh-yofs;
3596         levBGImg.tex.blitAt(x0, y0, scale);
3597       }
3598     }
3599   }
3601   int RenderEdgePix = (global.darkLevel ? RenderEdgePixLight : RenderEdgePixNormal);
3603   // render background tiles
3604   for (MapBackTile bt = backtiles; bt; bt = bt.next) {
3605     bt.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3606   }
3608   // collect visible special tiles
3609 #ifdef EXPERIMENTAL_RENDER_CACHE
3610   bool skipListCreation = (lastRenderTime == time && renderVisibleCids.length); //FIXME
3611 #endif
3613   if (!skipListCreation) {
3614     renderVisibleCids.clear();
3615     renderVisibleLights.clear();
3616     renderFrontTiles.clear();
3618     int endVX = xofs+viewWidth;
3619     int endVY = yofs+viewHeight;
3621     // add player
3622     //int cnt = 0;
3623     if (player && player.visible) renderVisibleCids[$] = player; // include player in render list
3625     //FIXME: drop lit objects which cannot affect visible area
3626     if (scale > 1) {
3627       // collect visible objects
3628       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)) {
3629         if (!o.visible) continue;
3630         auto tile = MapTile(o);
3631         if (tile) {
3632           if (isDarkLevel && (o.lightRadius > 4 || tile.litWholeTile)) renderVisibleLights[$] = o;
3633           if (tile.invisible) continue;
3634           if (tile.bgfront) renderFrontTiles[$] = tile;
3635           if (tile.bgback) tile.drawWithOfsBack(xofs, yofs, scale, currFrameDelta);
3636         } else {
3637           if (isDarkLevel && o.lightRadius > 4) renderVisibleLights[$] = o;
3638         }
3639         // check if the object is really visible -- this will speed up later sorting
3640         int fx0, fy0, fx1, fy1;
3641         auto spf = o.getSpriteFrame(default, out fx0, out fy0, out fx1, out fy1);
3642         if (!spf) continue; // no sprite -- nothing to draw (no, really)
3643         int ix = o.ix, iy = o.iy;
3644         int x0 = (ix+fx0)*scale, y0 = (iy+fy0)*scale;
3645         int x1 = (ix+fx1)*scale, y1 = (iy+fy1)*scale;
3646         if (x1 <= xofs || y1 <= yofs || x0 >= endVX || y0 >= endVY) {
3647           //++cnt;
3648           continue;
3649         }
3650         renderVisibleCids[$] = o;
3651       }
3652     } else {
3653       foreach (MapEntity o; objGrid.allObjects()) {
3654         if (!o.visible) continue;
3655         auto tile = MapTile(o);
3656         if (tile) {
3657           if (isDarkLevel && (o.lightRadius > 4 || tile.litWholeTile)) renderVisibleLights[$] = o;
3658           if (tile.invisible) continue;
3659           if (tile.bgfront) renderFrontTiles[$] = tile;
3660           if (tile.bgback) tile.drawWithOfsBack(xofs, yofs, scale, currFrameDelta);
3661         } else {
3662           if (isDarkLevel && o.lightRadius > 4) renderVisibleLights[$] = o;
3663         }
3664         renderVisibleCids[$] = o;
3665       }
3666     }
3667     //writeln("::: ", cnt, " invisible objects dropped");
3669     renderVisibleCids.sort(&renderSortByDepth);
3670     lastRenderTime = time;
3671   }
3673   auto depth4Start = 0;
3674   foreach (auto xidx, MapEntity o; renderVisibleCids) {
3675     if (o.depth >= 4) {
3676       depth4Start = xidx;
3677       break;
3678     }
3679   }
3681   // render objects (part one: depth > 3)
3682   foreach (auto idx; depth4Start..renderVisibleCids.length; reverse) {
3683     MapEntity o = renderVisibleCids[idx];
3684     //if (idx && renderSortByDepth(o, renderVisibleCids[idx-1])) writeln("VIOLATION!");
3685     o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3686   }
3688   // render object (part two: front tile parts, depth 3.5)
3689   foreach (MapTile tile; renderFrontTiles) {
3690     tile.drawWithOfsFront(xofs, yofs, scale, currFrameDelta);
3691   }
3693   // render objects (part three: depth <= 3)
3694   foreach (auto idx; 0..depth4Start; reverse) {
3695     MapEntity o = renderVisibleCids[idx];
3696     o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3697     //done above;if (isDarkLevel && (o.lightRadius > 4 || (o isa MapTile && MapTile(o).litWholeTile))) renderVisibleLights[$] = o;
3698   }
3700   // render player post-effects, so cape or jetpack will be correctly rendered on level exit, for example
3701   player.lastDrawWithOfs(xofs, yofs, scale, currFrameDelta);
3703   // lighting
3704   if (isDarkLevel) {
3705     auto ltex = bgtileStore.lightTexture('ltx512', 512);
3707     // set screen alpha to min
3708     Video.colorMask = Video::CMask.Alpha;
3709     Video.blendMode = Video::BlendMode.None;
3710     Video.color = 0xff_ff_ff_ff;
3711     Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
3712     //Video.colorMask = Video::CMask.All;
3714     // blend lights
3715     // also, stencil 'em, so we can filter dark areas
3716     Video.textureFiltering = true;
3717     Video.stencil = true;
3718     Video.stencilFunc(Video::StencilFunc.Always, 1);
3719     Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Replace);
3720     Video.alphaTestFunc = Video::AlphaFunc.Greater;
3721     Video.alphaTestVal = 0.03;
3722     Video.color = 0xff_ff_ff;
3723     Video.blendFunc = Video::BlendFunc.Max;
3724     Video.blendMode = Video::BlendMode.Blend; // anything except `Normal`
3725     Video.colorMask = Video::CMask.Alpha;
3727     foreach (MapEntity e; renderVisibleLights) {
3728       int xi, yi;
3729       e.getInterpCoords(currFrameDelta, scale, out xi, out yi);
3730       auto tile = MapTile(e);
3731       if (tile && tile.litWholeTile) {
3732         //Video.color = 0xff_ff_ff;
3733         Video.fillRect(xi-xofs, yi-yofs, e.width*scale, e.height*scale);
3734       }
3735       int lrad = e.lightRadius;
3736       if (lrad < 4) continue; // just in case
3737       lrad += 8;
3738       float lightscale = float(lrad*scale)/float(ltex.tex.width);
3739 #ifdef OLD_LIGHT_OFFSETS
3740       int fx0, fy0, fx1, fy1;
3741       bool doMirror;
3742       auto spf = e.getSpriteFrame(out doMirror, out fx0, out fy0, out fx1, out fy1);
3743       if (spf) {
3744         xi += (fx1-fx0)*scale/2;
3745         yi += (fy1-fy0)*scale/2;
3746       }
3747 #else
3748       int lxofs, lyofs;
3749       e.getLightOffset(out lxofs, out lyofs);
3750       xi += lxofs*scale;
3751       yi += lyofs*scale;
3753 #endif
3754       lrad = lrad*scale/2;
3755       xi -= xofs+lrad;
3756       yi -= yofs+lrad;
3757       ltex.tex.blitAt(xi, yi, lightscale);
3758     }
3759     Video.textureFiltering = false;
3761     // modify only lit parts
3762     Video.stencilFunc(Video::StencilFunc.Equal, 1);
3763     Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Keep);
3764     // multiply framebuffer colors by framebuffer alpha
3765     Video.color = 0xff_ff_ff; // it doesn't matter
3766     Video.blendFunc = Video::BlendFunc.Add;
3767     Video.blendMode = Video::BlendMode.DstMulDstAlpha;
3768     Video.colorMask = Video::CMask.Colors;
3769     Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
3771     // filter unlit parts
3772     Video.stencilFunc(Video::StencilFunc.NotEqual, 1);
3773     Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Keep);
3774     Video.blendFunc = Video::BlendFunc.Add;
3775     Video.blendMode = Video::BlendMode.Filter;
3776     Video.colorMask = Video::CMask.Colors;
3777     Video.color = 0x00_00_18;
3778     Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
3780     // restore defaults
3781     Video.blendFunc = Video::BlendFunc.Add;
3782     Video.blendMode = Video::BlendMode.Normal;
3783     Video.colorMask = Video::CMask.All;
3784     Video.alphaTestFunc = Video::AlphaFunc.Always;
3785     Video.stencil = false;
3786   }
3788   // clear visible objects list (nope)
3789   //renderVisibleCids.clear();
3790   //renderVisibleLights.clear();
3793   if (global.config.drawHUD) renderHUD(currFrameDelta);
3794   renderCompass(currFrameDelta);
3796   float osdTimeLeft, osdTimeStart;
3797   string msg = osdGetMessage(out osdTimeLeft, out osdTimeStart);
3798   if (msg) {
3799     auto ct = GetTickCount();
3800     int msgScale = 3;
3801     sprStore.loadFont('sFontSmall');
3802     auto msgHeight = sprStore.getMultilineTextHeight(msg, msgScale);
3803     int x = Video.screenWidth/2;
3804     int y = Video.screenHeight-64-msgHeight;
3805     auto oldColor = Video.color;
3806     Video.color = 0xff_ff_00;
3807     if (osdTimeLeft < 0.5) {
3808       int alpha = 255-clamp(int(510*osdTimeLeft), 0, 255);
3809       Video.color = Video.color|(alpha<<24);
3810     } else if (ct-osdTimeStart < 0.5) {
3811       osdTimeStart = ct-osdTimeStart;
3812       int alpha = 255-clamp(int(510*osdTimeStart), 0, 255);
3813       Video.color = Video.color|(alpha<<24);
3814     }
3815     sprStore.renderMultilineTextCentered(x, y, msg, msgScale, 0x00_ff_00, 0xff_ff_ff);
3816     Video.color = oldColor;
3817   }
3819   if (inWinCutscene) renderWinCutsceneOverlay();
3820   Video.color = 0xff_ff_ff;
3824 // ////////////////////////////////////////////////////////////////////////// //
3825 final class!MapObject findGameObjectClassByName (name aname) {
3826   if (!aname) return none; // just in case
3827   auto co = FindClassByGameObjName(aname);
3828   if (!co) {
3829     writeln("***UNKNOWN GAME OBJECT: '", aname, "'");
3830     return none;
3831   }
3832   co = GetClassReplacement(co);
3833   if (!co) FatalError("findGameObjectClassByName: WTF?!");
3834   if (!class!MapObject(co)) FatalError("findGameTileClassByName: object '%n' is not a map object, it is a `%n`", aname, GetClassName(co));
3835   return class!MapObject(co);
3839 final class!MapTile findGameTileClassByName (name aname) {
3840   if (!aname) return none; // just in case
3841   auto co = FindClassByGameObjName(aname);
3842   if (!co) return MapTile; // unknown names will be routed directly to tile object
3843   co = GetClassReplacement(co);
3844   if (!co) FatalError("findGameTileClassByName: WTF?!");
3845   if (!class!MapTile(co)) FatalError("findGameTileClassByName: object '%n' is not a tile, it is a `%n`", aname, GetClassName(co));
3846   return class!MapTile(co);
3850 final MapObject findAnyObjectOfType (name aname) {
3851   if (!aname) return none;
3852   auto cls = FindClassByGameObjName(aname);
3853   if (!cls) return none;
3854   foreach (MapObject obj; objGrid.allObjects(MapObject)) {
3855     if (obj.spectral) continue;
3856     if (obj isa cls) return obj;
3857   }
3858   return none;
3862 // ////////////////////////////////////////////////////////////////////////// //
3863 final bool isRopePlacedAt (int x, int y) {
3864   int[8] covered;
3865   foreach (ref auto v; covered) v = false;
3866   foreach (MapTile t; objGrid.inRectPix(x, y-8, 1, 17, precise:false, castClass:MapTileRope)) {
3867     //if (!cbIsRopeTile(t)) continue;
3868     if (t.ix != x) continue;
3869     if (t.iy == y) return true;
3870     foreach (int ty; t.iy..t.iy+8) {
3871       int d = ty-y;
3872       if (d >= 0 && d < covered.length) covered[d] = true;
3873     }
3874   }
3875   // check if the whole rope height is completely covered with ropes
3876   foreach (auto v; covered) if (!v) return false;
3877   return true;
3881 final MapTile CreateMapTile (int xpos, int ypos, name aname) {
3882   if (!aname) FatalError("cannot create typeless tile");
3883   auto tclass = findGameTileClassByName(aname);
3884   if (!tclass) return none;
3885   MapTile tile = SpawnObject(tclass);
3886   tile.global = global;
3887   tile.level = self;
3888   tile.objName = aname;
3889   tile.objType = aname; // just in case
3890   tile.fltx = xpos;
3891   tile.flty = ypos;
3892   tile.objId = ++lastUsedObjectId;
3893   if (!tile.initialize()) FatalError("cannot create tile '%n'", aname);
3894   return tile;
3898 final bool PutSpawnedMapTile (int x, int y, MapTile tile, optional bool putToGrid) {
3899   if (!tile || !tile.isInstanceAlive) return false;
3901   if (!putToGrid) putToGrid = (tile.moveable || tile.toSpecialGrid || tile.width != 16 || tile.height != 16 || x%16 != 0 || y%16 != 0);
3903   //writeln("putting tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (putToGrid ? "special " : ""), "grid, pos=(", x/16.0, ",", y/16.0, ")");
3905   if (!putToGrid) {
3906     int mapx = x/16, mapy = y/16;
3907     if (mapx < 0 || mapx >= tilesWidth || mapy < 0 || mapy >= tilesHeight) return false;
3908   }
3910   // if we already have rope tile there, there is no reason to add another one
3911   if (tile isa MapTileRope) {
3912     if (isRopePlacedAt(x, y)) return false;
3913   }
3915   // activate special or animated tile
3916   tile.active = tile.active || putToGrid || tile.moveable || tile.toSpecialGrid || tile.lava /*|| tile.water*/; // will be done in MakeMapTile
3917   // animated tiles must be active
3918   if (!tile.active) {
3919     auto spr = tile.getSprite();
3920     if (spr && spr.frames.length > 1) {
3921       writeln("activated animated tile '", tile.objName, "'");
3922       tile.active = true;
3923     }
3924   }
3926   tile.fltx = x;
3927   tile.flty = y;
3928   if (putToGrid) {
3929     //if (tile isa TitleTileCopy) writeln("*** PUTTING COPYRIGHT TILE");
3930     tile.toSpecialGrid = true;
3931     if (!tile.dontReplaceOthers && x&16 == 0 && y%16 == 0) {
3932       auto t = getTileAtGridAny(x/16, y/16);
3933       if (t && !t.immuneToReplacement) {
3934         writeln("REPLACING tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
3935         writeln("      NEW tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (tile.toSpecialGrid ? "special " : ""), "grid, pos=(", tile.fltx/16.0, ",", tile.flty/16.0, ")");
3936         t.instanceRemove();
3937       }
3938     }
3939     objGrid.insert(tile);
3940   } else {
3941     //writeln("SIZE: ", tilesWidth, "x", tilesHeight);
3942     setTileAtGrid(x/16, y/16, tile);
3943     auto t = getTileAtGridAny(x/16, y/16);
3944     /*
3945     if (t != tile) {
3946       writeln("putting tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (putToGrid ? "special " : ""), "grid, pos=(", x/16.0, ",", y/16.0, ")");
3947       checkTilesInRect(x/16, y/16, 16, 16, delegate bool (MapTile tile) {
3948         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, ")");
3949         return false;
3950       });
3951       FatalError("FUUUUUU");
3952     }
3953     */
3954   }
3956   if (tile.enter) registerEnter(tile);
3957   if (tile.exit) registerExit(tile);
3959   return true;
3963 // won't call `onDestroy()`
3964 final void RemoveMapTileFromGrid (int tileX, int tileY, optional string reason) {
3965   if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
3966     auto t = getTileAtGridAny(tileX, tileY);
3967     if (t) {
3968       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, ")");
3969       t.instanceRemove();
3970       checkWater = true;
3971     }
3972   }
3976 final MapTile MakeMapTile (int mapx, int mapy, name aname, optional bool putToGrid) {
3977   //if (mapy >= tilesHeight) { writeln("OOM: ", aname, "; ty=", mapy, "; hgt=", tilesHeight); }
3978   if (mapx < 0 || mapx >= tilesWidth || mapy < 0 || mapy >= tilesHeight) return none;
3980   // if we already have rope tile there, there is no reason to add another one
3981   if (aname == 'oRope') {
3982     if (isRopePlacedAt(mapx*16, mapy*16)) return none;
3983   }
3985   auto tile = CreateMapTile(mapx*16, mapy*16, aname);
3986   if (!tile) return none;
3987   if (!PutSpawnedMapTile(mapx*16, mapy*16, tile, putToGrid!optional)) {
3988     delete tile;
3989     tile = none;
3990   }
3992   return tile;
3997 final void MarkTileAsWet (int tileX, int tileY) {
3998   auto t = getTileAtGrid(tileX, tileY);
3999   if (t) t.wet = true;
4004 final MapTile MakeMapTileAtPix (int xpix, int ypix, name aname, optional bool putToGrid) {
4005   // if we already have rope tile there, there is no reason to add another one
4006   if (aname == 'oRope') {
4007     if (isRopePlacedAt(xpix, ypix)) return none;
4008   }
4010   auto tile = CreateMapTile(xpix, ypix, aname);
4011   if (!tile) return none;
4012   if (!PutSpawnedMapTile(xpix, ypix, tile, putToGrid!optional)) {
4013     delete tile;
4014     tile = none;
4015   }
4017   return tile;
4021 final MapTile MakeMapRopeTileAt (int x0, int y0) {
4022   // if we already have rope tile there, there is no reason to add another one
4023   if (isRopePlacedAt(x0, y0)) return none;
4025   auto tile = CreateMapTile(x0, y0, 'oRope');
4026   if (!PutSpawnedMapTile(x0, y0, tile, putToGrid:true)) {
4027     delete tile;
4028     tile = none;
4029   }
4031   return tile;
4035 // ////////////////////////////////////////////////////////////////////////// //
4036 final MapBackTile CreateBgTile (name sprName, optional int atx0, optional int aty0, optional int aw, optional int ah) {
4037   BackTileImage img = bgtileStore[sprName];
4038   auto res = SpawnObject(MapBackTile);
4039   res.global = global;
4040   res.level = self;
4041   res.bgt = img;
4042   res.bgtName = sprName;
4043   if (specified_atx0) res.tx0 = atx0;
4044   if (specified_aty0) res.ty0 = aty0;
4045   if (specified_aw) res.w = aw;
4046   if (specified_ah) res.h = ah;
4047   if (!res.initialize()) FatalError("cannot create background tile with image '%n'", sprName);
4048   return res;
4052 // ////////////////////////////////////////////////////////////////////////// //
4054 background The background asset from which the new tile will be extracted.
4055 left The x coordinate of the left of the new tile, relative to the background asset's top left corner.
4056 top The y coordinate of the top of the new tile, relative to the background assets top left corner.
4057 width The width of the tile.
4058 height The height of the tile.
4059 x The x position in the room to place the tile.
4060 y The y position in the room to place the tile.
4061 depth The depth at which to place the tile.
4063 final void MakeMapBackTile (name bgname, int left, int top, int width, int height, int x, int y, int depth) {
4064   if (width < 1 || height < 1 || !bgname) return;
4065   auto bgt = bgtileStore[bgname];
4066   if (!bgt) FatalError("cannot load background '%n'", bgname);
4067   MapBackTile bt = SpawnObject(MapBackTile);
4068   bt.global = global;
4069   bt.level = self;
4070   bt.objName = bgname;
4071   bt.bgt = bgt;
4072   bt.bgtName = bgname;
4073   bt.fltx = x;
4074   bt.flty = y;
4075   bt.tx0 = left;
4076   bt.ty0 = top;
4077   bt.w = width;
4078   bt.h = height;
4079   bt.depth = depth;
4080   // find a place for it
4081   if (!backtiles) {
4082     backtiles = bt;
4083     return;
4084   }
4085   // back tiles with the highest depth should come first
4086   MapBackTile ct = backtiles, cprev = none;
4087   while (ct && depth <= ct.depth) { cprev = ct; ct = ct.next; }
4088   // insert before ct
4089   if (cprev) {
4090     bt.next = cprev.next;
4091     cprev.next = bt;
4092   } else {
4093     bt.next = backtiles;
4094     backtiles = bt;
4095   }
4099 // ////////////////////////////////////////////////////////////////////////// //
4100 final MapObject SpawnMapObjectWithClass (class!MapObject oclass) {
4101   if (!oclass) return none;
4103   MapObject obj = SpawnObject(oclass);
4104   if (!obj) FatalError("cannot create map object '%n'", GetClassName(oclass));
4106   //if (aname == 'oShells' || aname == 'oShellSingle' || aname == 'oRopePile') writeln("*** CREATED '", aname, "'");
4108   obj.global = global;
4109   obj.level = self;
4110   obj.objId = ++lastUsedObjectId;
4112   return obj;
4116 final MapObject SpawnMapObject (name aname) {
4117   if (!aname) return none;
4118   auto res = SpawnMapObjectWithClass(findGameObjectClassByName(aname));
4119   if (res && !res.objType) res.objType = aname; // just in case
4120   return res;
4124 final MapObject PutSpawnedMapObject (int x, int y, MapObject obj) {
4125   if (!obj /*|| obj.global || obj.level*/) return none; // oops
4127   obj.fltx = x;
4128   obj.flty = y;
4129   if (!obj.initialize()) { delete obj; return none; } // not fatal
4131   insertObject(obj);
4133   return obj;
4137 final MapObject MakeMapObject (int x, int y, name aname) {
4138   MapObject obj = SpawnMapObject(aname);
4139   obj = PutSpawnedMapObject(x, y, obj);
4140   return obj;
4144 // ////////////////////////////////////////////////////////////////////////// //
4145 int winCutSceneTimer = -1;
4146 int winVolcanoTimer = -1;
4147 int winCutScenePhase = 0;
4148 int winSceneDrawStatus = 0;
4149 int winMoneyCount = 0;
4150 int winTime;
4151 bool winFadeOut = false;
4152 int winFadeLevel = 0;
4153 int winCutsceneSkip = 0; // 1: waiting for pay release; 2: pay released, do skip
4154 bool winCutsceneSwitchToNext = false;
4157 void startWinCutscene () {
4158   global.hasParachute = false;
4159   shakeLeft = 0;
4160   winCutsceneSwitchToNext = false;
4161   winCutsceneSkip = 0;
4162   isKeyPressed(GameConfig::Key.Pay);
4163   isKeyReleased(GameConfig::Key.Pay);
4165   auto olddel = ImmediateDelete;
4166   ImmediateDelete = false;
4167   clearWholeLevel();
4169   createEnd1Room();
4170   fixWallTiles();
4171   addBackgroundGfxDetails();
4173   levBGImgName = 'bgCave';
4174   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
4176   blockWaterChecking = true;
4177   fixLiquidTop();
4178   cleanDeadTiles();
4180   ImmediateDelete = olddel;
4181   CollectGarbage(true); // destroy delayed objects too
4183   if (dumpGridStats) objGrid.dumpStats();
4185   playerExited = false; // just in case
4186   playerExitDoor = none;
4188   osdClear();
4190   setupGhostTime();
4191   global.stopMusic();
4193   inWinCutscene = 1;
4194   winCutSceneTimer = -1;
4195   winCutScenePhase = 0;
4197   /+
4198   if (global.config.gameMode != GameConfig::GameMode.Vanilla) {
4199     if (global.config.bizarre) {
4200       global.yasmScore = 1;
4201       global.config.bizarrePlusTitle = true;
4202     }
4204     array!MapTile toReplace;
4205     forEachTile(delegate bool (MapTile t) {
4206       if (t.objType == 'oGTemple' ||
4207           t.objType == 'oIce' ||
4208           t.objType == 'oDark' ||
4209           t.objType == 'oBrick' ||
4210           t.objType == 'oLush')
4211       {
4212         toReplace[$] = t;
4213       }
4214       return false;
4215     });
4217     foreach (MapTile t; miscTileGrid.allObjects()) {
4218       if (t.objType == 'oGTemple' ||
4219           t.objType == 'oIce' ||
4220           t.objType == 'oDark' ||
4221           t.objType == 'oBrick' ||
4222           t.objType == 'oLush')
4223       {
4224         toReplace[$] = t;
4225       }
4226     }
4228     foreach (MapTile t; toReplace) {
4229       if (t.iy < 192) {
4230         t.cleanDeath = true;
4231             if (rand(1,120) == 1) instance_change(oGTemple, false);
4232         else if (rand(1,100) == 1) instance_change(oIce, false);
4233         else if (rand(1,90) == 1) instance_change(oDark, false);
4234         else if (rand(1,80) == 1) instance_change(oBrick, false);
4235         else if (rand(1,70) == 1) instance_change(oLush, false);
4236           }
4237       }
4238       with (oBrick)
4239       {
4240           if (y &lt; 192)
4241           {
4242               cleanDeath = true;
4243               if (rand(1,5) == 1) instance_change(oLush, false);
4244           }
4245       }
4246   }
4247   +/
4248   //!instance_create(0, 0, oBricks);
4250   //shakeToggle = false;
4251   //oPDummy.status = 2;
4253   //timer = 0;
4255   /+
4256   if (global.kaliPunish &gt;= 2) {
4257       instance_create(oPDummy.x, oPDummy.y+2, oBall2);
4258       obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
4259       obj.linkVal = 1;
4260       obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
4261       obj.linkVal = 2;
4262       obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
4263       obj.linkVal = 3;
4264       obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
4265       obj.linkVal = 4;
4266   }
4267   +/
4271 void startWinCutsceneVolcano () {
4272   global.hasParachute = false;
4273   /*
4274   writeln("VOLCANO HOLD 0: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4275   writeln("VOLCANO PICK 0: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4276   */
4278   shakeLeft = 0;
4279   winCutsceneSwitchToNext = false;
4280   auto olddel = ImmediateDelete;
4281   ImmediateDelete = false;
4282   clearWholeLevel();
4284   levBGImgName = '';
4285   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
4287   blockWaterChecking = true;
4289   ImmediateDelete = olddel;
4290   CollectGarbage(true); // destroy delayed objects too
4292   spawnPlayerAt(2*16+8, 11*16+8);
4293   player.dir = MapEntity::Dir.Right;
4295   playerExited = false; // just in case
4296   playerExitDoor = none;
4298   osdClear();
4300   setupGhostTime();
4301   global.stopMusic();
4303   inWinCutscene = 2;
4304   winCutSceneTimer = -1;
4305   winCutScenePhase = 0;
4307   MakeMapTile(0, 0, 'oEnd2BG');
4308   realViewStart.x = 0;
4309   realViewStart.y = 0;
4310   viewStart.x = 0;
4311   viewStart.y = 0;
4313   viewMin.x = 0;
4314   viewMin.y = 0;
4315   viewMax.x = 320;
4316   viewMax.y = 240;
4318   player.dead = false;
4319   player.active = true;
4320   player.visible = false;
4321   player.removeBallAndChain(temp:true);
4322   player.stunned = false;
4323   player.status = MapObject::FALLING;
4324   if (player.holdItem) player.holdItem.visible = false;
4325   player.fltx = 320/2;
4326   player.flty = 0;
4328   /*
4329   writeln("VOLCANO HOLD 1: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4330   writeln("VOLCANO PICK 1: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4331   */
4335 void startWinCutsceneWinFall () {
4336   global.hasParachute = false;
4337   /*
4338   writeln("FALL HOLD 0: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4339   writeln("FALL PICK 0: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4340   */
4342   shakeLeft = 0;
4343   winCutsceneSwitchToNext = false;
4345   auto olddel = ImmediateDelete;
4346   ImmediateDelete = false;
4347   clearWholeLevel();
4349   createEnd3Room();
4350   setMenuTilesVisible(false);
4351   //fixWallTiles();
4352   //addBackgroundGfxDetails();
4354   levBGImgName = '';
4355   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
4357   blockWaterChecking = true;
4358   fixLiquidTop();
4359   cleanDeadTiles();
4361   ImmediateDelete = olddel;
4362   CollectGarbage(true); // destroy delayed objects too
4364   if (dumpGridStats) objGrid.dumpStats();
4366   playerExited = false; // just in case
4367   playerExitDoor = none;
4369   osdClear();
4371   setupGhostTime();
4372   global.stopMusic();
4374   inWinCutscene = 3;
4375   winCutSceneTimer = -1;
4376   winCutScenePhase = 0;
4378   player.dead = false;
4379   player.active = true;
4380   player.visible = false;
4381   player.removeBallAndChain(temp:true);
4382   player.stunned = false;
4383   player.status = MapObject::FALLING;
4384   if (player.holdItem) player.holdItem.visible = false;
4385   player.fltx = 320/2;
4386   player.flty = 0;
4388   winSceneDrawStatus = 0;
4389   winMoneyCount = 0;
4391   winFadeOut = false;
4392   winFadeLevel = 0;
4394   /*
4395   writeln("FALL HOLD 1: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4396   writeln("FALL PICK 1: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4397   */
4401 void setGameOver () {
4402   if (inWinCutscene) {
4403     player.visible = false;
4404     player.removeBallAndChain(temp:true);
4405     if (player.holdItem) player.holdItem.visible = false;
4406   }
4407   player.dead = true;
4408   if (inWinCutscene > 0) {
4409     winFadeOut = true;
4410     winFadeLevel = 255;
4411     winSceneDrawStatus = 8;
4412   }
4416 MapTile findEndPlatTile () {
4417   return forEachTile(delegate bool (MapTile t) { return (t isa MapTileEndPlat); }, castClass:MapTileEndPlat);
4421 MapObject findBigTreasure () {
4422   return forEachObject(delegate bool (MapObject o) { return (o isa MapObjectBigTreasure); }, castClass:MapObjectBigTreasure);
4426 void setMenuTilesVisible (bool vis) {
4427   if (vis) {
4428     forEachTile(delegate bool (MapTile t) {
4429       if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4430         t.invisible = false;
4431       }
4432       return false;
4433     });
4434   } else {
4435     forEachTile(delegate bool (MapTile t) {
4436       if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4437         t.invisible = true;
4438       }
4439       return false;
4440     });
4441   }
4445 void setMenuTilesOnTop () {
4446   forEachTile(delegate bool (MapTile t) {
4447     if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4448       t.depth = 1;
4449     }
4450     return false;
4451   });
4455 void winCutscenePlayerControl (PlayerPawn plr) {
4456   auto payPress = isKeyPressed(GameConfig::Key.Pay);
4457   auto payRelease = isKeyReleased(GameConfig::Key.Pay);
4459   switch (winCutsceneSkip) {
4460     case 0: // nothing was pressed
4461       if (payPress) winCutsceneSkip = 1;
4462       break;
4463     case 1: // waiting for pay release
4464       if (payRelease) winCutsceneSkip = 2;
4465       break;
4466     case 2: // pay released, do skip
4467       setGameOver();
4468       return;
4469   }
4471   // first winning room
4472   if (inWinCutscene == 1) {
4473     if (plr.ix < 448+8) {
4474       plr.kRight = true;
4475       return;
4476     }
4478     // waiting for chest to open
4479     if (winCutScenePhase == 0) {
4480       winCutSceneTimer = 120/2;
4481       winCutScenePhase = 1;
4482       return;
4483     }
4485     // spawn big idol
4486     if (winCutScenePhase == 1) {
4487       if (--winCutSceneTimer == 0) {
4488         winCutScenePhase = 2;
4489         winCutSceneTimer = 20;
4490         forEachObject(delegate bool (MapObject o) {
4491           if (o isa MapObjectBigChest) {
4492             o.setSprite(global.config.gameMode == GameConfig::GameMode.Vanilla ? 'sBigChestOpen' : 'sBigChestOpen2');
4493             auto treasure = MakeMapObject(o.ix, o.iy, 'oBigTreasure');
4494             if (treasure) {
4495               treasure.yVel = -4;
4496               treasure.xVel = -3;
4497               o.playSound('sndClick');
4498               //!!!if (global.config.gameMode != GameConfig::GameMode.Vanilla) scrSprayGems(oBigChest.x+24, oBigChest.y+24);
4499             }
4500           }
4501           return false;
4502         });
4503       }
4504       return;
4505     }
4507     // lava pump wait
4508     if (winCutScenePhase == 2) {
4509       if (--winCutSceneTimer == 0) {
4510         winCutScenePhase = 3;
4511         winCutSceneTimer = 50;
4512       }
4513       return;
4514     }
4516     // lava pump start
4517     if (winCutScenePhase == 3) {
4518       auto ep = findEndPlatTile();
4519       if (ep) MakeMapObject(ep.ix+global.randOther(0, 80), /*ep.iy*/192+32, 'oBurn');
4520       if (--winCutSceneTimer == 0) {
4521         winCutScenePhase = 4;
4522         winCutSceneTimer = 10;
4523         if (ep) MakeMapObject(ep.ix, ep.iy+30, 'oLavaSpray');
4524         scrShake(9999);
4525       }
4526       return;
4527     }
4529     // lava pump first accel
4530     if (winCutScenePhase == 4) {
4531       if (--winCutSceneTimer == 0) {
4532         forEachObject(delegate bool (MapObject o) {
4533           if (o isa MapObjectLavaSpray) o.yAcc = -0.1;
4534           return false;
4535         });
4536       }
4537     }
4539     // lava pump complete
4540     if (winCutScenePhase == 5) {
4541       if (--winCutSceneTimer == 0) {
4542         //if (oLavaSpray) oLavaSpray.yAcc = -0.1;
4543         startWinCutsceneVolcano();
4544       }
4545       return;
4546     }
4547     return;
4548   }
4551   // volcano room
4552   if (inWinCutscene == 2) {
4553     plr.flty = 0;
4555     // initialize
4556     if (winCutScenePhase == 0) {
4557       winCutSceneTimer = 50;
4558       winCutScenePhase = 1;
4559       winVolcanoTimer = 10;
4560       return;
4561     }
4563     if (winVolcanoTimer > 0) {
4564       if (--winVolcanoTimer == 0) {
4565         MakeMapObject(224+global.randOther(0,48), 144+global.randOther(0,8), 'oVolcanoFlame');
4566         winVolcanoTimer = global.randOther(10, 20);
4567       }
4568     }
4570     // plr sil
4571     if (winCutScenePhase == 1) {
4572       if (--winCutSceneTimer == 0) {
4573         winCutSceneTimer = 30;
4574         winCutScenePhase = 2;
4575         auto sil = MakeMapObject(240, 132, 'oPlayerSil');
4576         //sil.xVel = -6;
4577         //sil.yVel = -8;
4578       }
4579       return;
4580     }
4582     // treasure sil
4583     if (winCutScenePhase == 2) {
4584       if (--winCutSceneTimer == 0) {
4585         winCutScenePhase = 3;
4586         auto sil = MakeMapObject(240, 132, 'oTreasureSil');
4587         //sil.xVel = -6;
4588         //sil.yVel = -8;
4589       }
4590       return;
4591     }
4593     return;
4594   }
4596   // winning camel room
4597   if (inWinCutscene == 3) {
4598     //if (!player.holdItem)  writeln("SCENE 3: LOST ITEM!");
4600     if (!plr.visible) plr.flty = -32;
4602     // initialize
4603     if (winCutScenePhase == 0) {
4604       winCutSceneTimer = 50;
4605       winCutScenePhase = 1;
4606       return;
4607     }
4609     // fall sound
4610     if (winCutScenePhase == 1) {
4611       if (--winCutSceneTimer == 0) {
4612         winCutSceneTimer = 50;
4613         winCutScenePhase = 2;
4614         plr.playSound('sndPFall');
4615         plr.visible = true;
4616         plr.active = true;
4617         writeln("MUST BE CHAINED: ", plr.mustBeChained);
4618         if (plr.mustBeChained) {
4619           plr.removeBallAndChain(temp:true);
4620           plr.spawnBallAndChain();
4621         }
4622         /*
4623         writeln("HOLD: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4624         writeln("PICK: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4625         */
4626         if (!player.holdItem && player.pickedItem) player.scrSwitchToPocketItem(forceIfEmpty:false);
4627         if (player.holdItem) {
4628           player.holdItem.visible = true;
4629           player.holdItem.canLiveOutsideOfLevel = true;
4630           writeln("HOLD ITEM: '", GetClassName(player.holdItem.Class), "'");
4631         }
4632         plr.status == MapObject::FALLING;
4633         global.plife += 99; // just in case
4634       }
4635       return;
4636     }
4638     if (winCutScenePhase == 2) {
4639       auto ball = plr.getMyBall();
4640       if (ball && plr.holdItem != ball) {
4641         ball.teleportTo(plr.fltx, plr.flty+8);
4642         ball.yVel = 6;
4643         ball.myGrav = 0.6;
4644       }
4645       if (plr.status == MapObject::STUNNED || plr.stunned) {
4646         //alarm[0] = 70;
4647         //alarm[1] = 50;
4648         //status = GETUP;
4649         auto treasure = MakeMapObject(144+16+8, -32, 'oBigTreasure');
4650         if (treasure) treasure.depth = 1;
4651         winCutScenePhase = 3;
4652         plr.stunTimer = 30;
4653         plr.playSound('sndTFall');
4654       }
4655       return;
4656     }
4658     if (winCutScenePhase == 3) {
4659       if (plr.status != MapObject::STUNNED && !plr.stunned) {
4660         auto bt = findBigTreasure();
4661         if (bt) {
4662           if (bt.yVel == 0) {
4663             //plr.yVel = -4;
4664             //plr.status = MapObject::JUMPING;
4665             plr.kJump = true;
4666             plr.kJumpPressed = true;
4667             winCutScenePhase = 4;
4668             winCutSceneTimer = 50;
4669           }
4670         }
4671       }
4672       return;
4673     }
4675     if (winCutScenePhase == 4) {
4676       if (--winCutSceneTimer == 0) {
4677         setMenuTilesVisible(true);
4678         winCutScenePhase = 5;
4679         winSceneDrawStatus = 1;
4680         global.playMusic('musVictory', loop:false);
4681         winCutSceneTimer = 50;
4682       }
4683       return;
4684     }
4686     if (winCutScenePhase == 5) {
4687       if (winSceneDrawStatus == 3) {
4688         int money = stats.money;
4689         if (winMoneyCount < money) {
4690           if (money-winMoneyCount > 1000) {
4691             winMoneyCount += 1000;
4692           } else if (money-winMoneyCount > 100) {
4693             winMoneyCount += 100;
4694           } else if (money-winMoneyCount > 10) {
4695             winMoneyCount += 10;
4696           } else {
4697             ++winMoneyCount;
4698           }
4699         }
4700         if (winMoneyCount >= money) {
4701           winMoneyCount = money;
4702           ++winSceneDrawStatus;
4703         }
4704         return;
4705       }
4707       if (winSceneDrawStatus == 7) {
4708         winFadeOut = true;
4709         winFadeLevel += 1;
4710         if (winFadeLevel >= 255) {
4711           ++winSceneDrawStatus;
4712           winCutSceneTimer = 30*30;
4713         }
4714         return;
4715       }
4717       if (winSceneDrawStatus == 8) {
4718         if (--winCutSceneTimer == 0) {
4719           setGameOver();
4720         }
4721         return;
4722       }
4724       if (--winCutSceneTimer == 0) {
4725         ++winSceneDrawStatus;
4726         winCutSceneTimer = 50;
4727       }
4728     }
4730     return;
4731   }
4735 // ////////////////////////////////////////////////////////////////////////// //
4736 void renderWinCutsceneOverlay () {
4737   if (inWinCutscene == 3) {
4738     if (winSceneDrawStatus > 0) {
4739       Video.color = 0xff_ff_ff;
4740       sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
4741       //draw_set_color(txtCol);
4742       drawTextAt(64, 32, "YOU MADE IT!");
4744       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4745       if (global.config.gameMode == GameConfig::GameMode.Vanilla) {
4746         Video.color = 0xff_ff_00; //draw_set_color(c_yellow);
4747         drawTextAt(64, 48, "Classic Mode done!");
4748       } else {
4749         Video.color = 0x00_80_80; //draw_set_color(c_teal);
4750         if (global.config.bizarrePlus) drawTextAt(64, 48, "Bizarre Mode Plus done!");
4751         else drawTextAt(64, 48, "Bizarre Mode done!");
4752         //draw_set_color(c_white);
4753       }
4754       if (!global.usedShortcut) {
4755         Video.color = 0xc0_c0_c0; //draw_set_color(c_silver);
4756         drawTextAt(64, 56, "No shortcuts used!");
4757         //draw_set_color(c_yellow);
4758       }
4759     }
4761     if (winSceneDrawStatus > 1) {
4762       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4763       //draw_set_color(txtCol);
4764       Video.color = 0xff_ff_ff;
4765       drawTextAt(64, 64, "FINAL SCORE:");
4766     }
4768     if (winSceneDrawStatus > 2) {
4769       sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
4770       Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4771       drawTextAt(64, 72, va("$%d", winMoneyCount));
4772     }
4774     if (winSceneDrawStatus > 4) {
4775       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4776       Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4777       drawTextAt(64, 96, va("Time: %s", time2str(winTime/30)));
4778       /*
4779       draw_set_color(c_white);
4780       if (s &lt; 10) draw_text(96+24, 96, string(m) + ":0" + string(s));
4781       else draw_text(96+24, 96, string(m) + ":" + string(s));
4782       */
4783     }
4785     if (winSceneDrawStatus > 5) {
4786       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4787       Video.color = 0xff_ff_ff; //draw_set_color(txtCol);
4788       drawTextAt(64, 96+8, "Kills: ");
4789       Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4790       drawTextAt(96+24, 96+8, va("%s", stats.countKills()));
4791     }
4793     if (winSceneDrawStatus > 6) {
4794       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4795       Video.color = 0xff_ff_ff; //draw_set_color(txtCol);
4796       drawTextAt(64, 96+16, "Saves: ");
4797       Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4798       drawTextAt(96+24, 96+16, va("%s", stats.damselsSaved));
4799     }
4801     if (winFadeOut) {
4802       Video.color = (255-clamp(winFadeLevel, 0, 255))<<24;
4803       Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
4804     }
4806     if (winSceneDrawStatus == 8) {
4807       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4808       Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4809       string lastString;
4810       if (global.config.gameMode == GameConfig::GameMode.Vanilla) {
4811         Video.color = 0xff_ff_00; //draw_set_color(c_yellow);
4812         lastString = "YOU SHALL BE REMEMBERED AS A HERO.";
4813       } else {
4814         Video.color = 0x00_ff_ff;
4815         if (global.config.bizarrePlus) lastString = "ANOTHER LEGENDARY ADVENTURE!";
4816         else lastString = "YOUR DISCOVERIES WILL BE CELEBRATED!";
4817       }
4818       auto strLen = lastString.length*8;
4819       int n = 320-strLen;
4820       n = trunc(ceil(n/2.0));
4821       drawTextAt(n, 116, lastString);
4822     }
4823   }
4827 // ////////////////////////////////////////////////////////////////////////// //
4828 #include "roomTitle.vc"
4829 #include "roomTrans1.vc"
4830 #include "roomTrans2.vc"
4831 #include "roomTrans3.vc"
4832 #include "roomTrans4.vc"
4833 #include "roomOlmec.vc"
4834 #include "roomEnd.vc"
4835 #include "roomTutorial.vc"
4836 #include "roomScores.vc"
4837 #include "roomStars.vc"
4838 #include "roomSun.vc"
4839 #include "roomMoon.vc"
4842 // ////////////////////////////////////////////////////////////////////////// //
4843 #include "packages/Generator/loadRoomGens.vc"
4844 #include "packages/Generator/loadEntityGens.vc"