wingame sequence
[k8vacspelynky.git] / GameLevel.vc
blob166d2083049b002c5b35a3ed41da74dab28dd16c
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 const float FrameTime = 1.0f/30.0f;
23 const int dumpGridStats = false;
25 struct IVec2D {
26   int x, y;
29 // in tiles
30 //enum NormalTilesWidth = LevelGen::LevelWidth*RoomGen::Width+2;
31 //enum NormalTilesHeight = LevelGen::LevelHeight*RoomGen::Height+2;
33 enum MaxTilesWidth = 64;
34 enum MaxTilesHeight = 64;
36 GameGlobal global;
37 transient GameStats stats;
38 transient SpriteStore sprStore;
39 transient BackTileStore bgtileStore;
40 transient BackTileImage levBGImg;
41 name levBGImgName;
42 LevelGen lg;
43 transient name lastMusicName;
44 //RoomGen[LevelGen::LevelWidth, LevelGen::LevelHeight] rooms; // moved to levelgen
46 transient float accumTime;
47 transient bool gamePaused = false;
48 transient bool checkWater;
49 transient int damselSaved;
51 // hud efffects
52 transient int xmoney;
53 transient int collectCounter;
54 transient int levelMoneyStart;
56 // all movable (thinkable) map objects
57 EntityGrid objGrid; // monsters and items
59 MapTile[MaxTilesWidth, MaxTilesHeight] tiles;
60 MapBackTile backtiles;
61 EntityGrid miscTileGrid; // moveables and ropes
62 bool blockWaterChecking;
63 array!MapTile lavatiles; // they need to think/animate, but i don't want to move 'em out of `tiles`
65 array!MapObject ballObjects; // list of all ball objects, for speed
67 int inWinCutscene;
69 LevelGen::RType[MaxTilesWidth, MaxTilesHeight] roomType;
71 enum LevelKind {
72   Normal,
73   Transition,
74   Title,
75   Final,
77 LevelKind levelKind = LevelKind.Normal;
79 array!MapTile allEnters;
80 array!MapTile allExits;
83 int startRoomX, startRoomY;
84 int endRoomX, endRoomY;
86 PlayerPawn player;
87 transient bool playerExited;
88 transient bool disablePlayerThink = false;
89 transient int maxPlayingTime; // in seconds
91 int ghostTimeLeft;
92 int musicFadeTimer;
93 bool ghostSpawned; // to speed up some checks
96 // FPS, i.e. incremented by 30 in one second
97 int time; // in frames
98 int lastUsedObjectId;
100 // screen shake variables
101 int shakeLeft;
102 IVec2D shakeOfs;
103 IVec2D shakeDir;
105 // set this before calling `fixCamera()`
106 // dimensions should be real, not scaled up/down
107 transient int viewWidth, viewHeight;
108 // room bounds, not scaled
109 IVec2D viewMin, viewMax;
111 // for Olmec level cinematics
112 IVec2D cameraSlideToDest;
113 IVec2D cameraSlideToCurr;
114 IVec2D cameraSlideToSpeed; // !0: slide
115 int cameraSlideToPlayer;
116 // `fixCamera()` will set the following
117 // coordinates will be real too (with scale applied)
118 // shake is not applied
119 transient IVec2D viewStart; // with `player.viewOffset`
120 private transient IVec2D realViewStart; // without `player.viewOffset`
122 final void cameraSlideToPoint (int dx, int dy, int speedx, int speedy) {
123   cameraSlideToPlayer = 0;
124   cameraSlideToDest.x = dx;
125   cameraSlideToDest.y = dy;
126   cameraSlideToSpeed.x = abs(speedx);
127   cameraSlideToSpeed.y = abs(speedy);
128   cameraSlideToCurr.x = cameraCurrX;
129   cameraSlideToCurr.y = cameraCurrY;
132 final void cameraReturnToPlayer () {
133   if (!cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y)) {
134     cameraSlideToCurr.x = cameraCurrX;
135     cameraSlideToCurr.y = cameraCurrY;
136     if (cameraSlideToSpeed.x && abs(cameraSlideToSpeed.x) < 8) cameraSlideToSpeed.x = 8;
137     if (cameraSlideToSpeed.y && abs(cameraSlideToSpeed.y) < 8) cameraSlideToSpeed.y = 8;
138     cameraSlideToPlayer = 1;
139   }
142 // if `frameSkip` is `true`, there are more frames waiting
143 // (i.e. you may skip rendering and such)
144 transient void delegate (bool frameSkip) onBeforeFrame;
145 transient void delegate (bool frameSkip) onAfterFrame;
147 transient void delegate () onLevelExitedCB;
149 // this will be called in-between frames, and
150 // `frameTime` is [0..1)
151 transient void delegate (float frameTime) onInterFrame;
153 final int bizRoomStyle { get { return (lg ? lg.bizRoomStyle : 0); } }
156 final bool isNormalLevel () { return (levelKind == LevelKind.Normal); }
159 // ////////////////////////////////////////////////////////////////////////// //
160 // stats
161 void addDeath (name aname) { stats.addDeath(aname); }
162 void addKill (name aname) { stats.addKill(aname); }
163 void addCollect (name aname, optional int amount) { stats.addCollect(aname, amount!optional); }
165 void addDamselSaved () { stats.addDamselSaved(); }
166 void addIdolStolen () { stats.addIdolStolen(); }
167 void addIdolConverted () { stats.addIdolConverted(); }
168 void addCrystalIdolStolen () { stats.addCrystalIdolStolen(); }
169 void addCrystalIdolConverted () { stats.addCrystalIdolConverted(); }
170 void addGhostSummoned () { stats.addGhostSummoned(); }
173 // ////////////////////////////////////////////////////////////////////////// //
174 static final string val2dig (int n) {
175   return (n < 10 ? va("0%d", n) : va("%d", n));
179 static final string time2str (int time) {
180   int secs = time%60; time /= 60;
181   int mins = time%60; time /= 60;
182   int hours = time%24; time /= 24;
183   int days = time;
184   if (days) return va("%d DAYS, %d:%s:%s", days, hours, val2dig(mins), val2dig(secs));
185   if (hours) return va("%d:%s:%s", hours, val2dig(mins), val2dig(secs));
186   return va("%s:%s", val2dig(mins), val2dig(secs));
190 // ////////////////////////////////////////////////////////////////////////// //
191 final int tilesWidth () { return lg.levelRoomWidth*RoomGen::Width+2; }
192 final int tilesHeight () { return (lg.finalBossLevel ? 55 : lg.levelRoomHeight*RoomGen::Height+2); }
195 // ////////////////////////////////////////////////////////////////////////// //
196 // this won't generate a level yet
197 void restartGame () {
198   inWinCutscene = 0;
199   if (player) {
200     auto hi = player.holdItem;
201     player.holdItem = none;
202     if (hi) hi.instanceRemove();
203     hi = player.pickedItem;
204     player.pickedItem = none;
205     if (hi) hi.instanceRemove();
206   }
207   global.resetGame();
208   stats.clearGameTotals();
209   if (global.startMoney > 0) stats.setMoneyCheat();
210   stats.setMoney(global.startMoney);
211   //writeln("level=", global.currLevel, "; lt=", global.levelType);
215 // complement function to `restart game`
216 void generateNormalLevel () {
217   generateLevel();
218   centerViewAtPlayer();
222 // ////////////////////////////////////////////////////////////////////////// //
223 // generate angry shopkeeper at exit if murderer or thief
224 void generateAngryShopkeepers () {
225   if (global.murderer || global.thiefLevel > 0) {
226     foreach (MapTile e; allExits) {
227       auto obj = MonsterShopkeeper(MakeMapObject(e.ix, e.iy, 'oShopkeeper'));
228       if (obj) {
229         obj.style = 'Bounty Hunter';
230         obj.status = MapObject::PATROL;
231       }
232     }
233   }
237 // ////////////////////////////////////////////////////////////////////////// //
238 final void resetRoomBounds () {
239   viewMin.x = 0;
240   viewMin.y = 0;
241   viewMax.x = tilesWidth*16;
242   viewMax.y = tilesHeight*16;
243   // Great Lake is bottomless (nope)
244   //if (global.lake) viewMax.y -= 16;
245   //writeln("ROOM BOUNDS: (", viewMax.x, "x", viewMax.y, ")");
249 final void setRoomBounds (int x0, int y0, int x1, int y1) {
250   viewMin.x = x0;
251   viewMin.y = y0;
252   viewMax.x = x1+16;
253   viewMax.y = y1+16;
257 // ////////////////////////////////////////////////////////////////////////// //
258 struct OSDMessage {
259   string msg;
260   float timeout; // seconds
261   float starttime; // for active
262   bool active; // true: timeout is `GetTickCount()` dismissing time
265 array!OSDMessage msglist; // [0]: current one
268 private final void osdCheckTimeouts () {
269   auto stt = GetTickCount();
270   while (msglist.length) {
271     if (!msglist[0].active) {
272       msglist[0].active = true;
273       msglist[0].starttime = stt;
274     }
275     if (msglist[0].starttime+msglist[0].timeout >= stt) break;
276     msglist.remove(0);
277   }
281 final bool osdHasMessage () {
282   osdCheckTimeouts();
283   return (msglist.length > 0);
287 final string osdGetMessage (out float timeLeft, out float timeStart) {
288   osdCheckTimeouts();
289   if (msglist.length == 0) { timeLeft = 0; return ""; }
290   auto stt = GetTickCount();
291   timeStart = msglist[0].starttime;
292   timeLeft = fmax(0, msglist[0].starttime+msglist[0].timeout-stt);
293   return msglist[0].msg;
297 final void osdClear () {
298   msglist.length -= msglist.length;
302 final void osdMessage (string msg, optional float timeout, optional bool urgent) {
303   if (!msg) return;
304   if (!specified_timeout) timeout = 3.33;
305   // special message for shops
306   if (timeout == -666) {
307     if (!msg) return;
308     if (msglist.length && msglist[0].msg == msg) return;
309     if (msglist.length == 0 || msglist[0].msg != msg) {
310       osdClear();
311       msglist.length += 1;
312       msglist[0].msg = msg;
313     }
314     msglist[0].active = false;
315     msglist[0].timeout = 3.33;
316     osdCheckTimeouts();
317     return;
318   }
319   if (timeout < 0.1) return;
320   timeout = fmax(1.0, timeout);
321   //writeln("OSD: ", msg);
322   // find existing one, and bring it to the top
323   int oldidx = 0;
324   for (; oldidx < msglist.length; ++oldidx) {
325     if (msglist[oldidx].msg == msg) break; // i found her!
326   }
327   // duplicate?
328   if (oldidx < msglist.length) {
329     // yeah, move duplicate to the top
330     msglist[oldidx].timeout = fmax(timeout, msglist[oldidx].timeout);
331     msglist[oldidx].active = false;
332     if (urgent && oldidx != 0) {
333       timeout = msglist[oldidx].timeout;
334       msglist.remove(oldidx);
335       msglist.insert(0);
336       msglist[0].msg = msg;
337       msglist[0].timeout = timeout;
338       msglist[0].active = false;
339     }
340   } else if (urgent) {
341     msglist.insert(0);
342     msglist[0].msg = msg;
343     msglist[0].timeout = timeout;
344     msglist[0].active = false;
345   } else {
346     // new one
347     msglist.length += 1;
348     msglist[$-1].msg = msg;
349     msglist[$-1].timeout = timeout;
350     msglist[$-1].active = false;
351   }
352   osdCheckTimeouts();
356 // ////////////////////////////////////////////////////////////////////////// //
357 void setup (GameGlobal aGlobal, SpriteStore aSprStore, BackTileStore aBGTileStore) {
358   global = aGlobal;
359   sprStore = aSprStore;
360   bgtileStore = aBGTileStore;
362   lg = SpawnObject(LevelGen);
363   lg.global = global;
364   lg.level = self;
366   miscTileGrid = SpawnObject(EntityGrid);
367   miscTileGrid.setup(0, 0, MaxTilesWidth*16, MaxTilesHeight*16, MapTile);
368   //miscTileGrid.ownObjects = true;
370   objGrid = SpawnObject(EntityGrid);
371   objGrid.setup(0, 0, MaxTilesWidth*16, MaxTilesHeight*16, MapObject);
375 // stores should be set
376 void onLoaded () {
377   checkWater = true;
378   levBGImg = bgtileStore[levBGImgName];
379   foreach (int y; 0..MaxTilesHeight) {
380     foreach (int x; 0..MaxTilesWidth) {
381       if (tiles[x, y]) tiles[x, y].onLoaded();
382     }
383   }
384   foreach (MapEntity o; miscTileGrid.allObjects()) o.onLoaded();
385   foreach (MapEntity o; objGrid.allObjects()) o.onLoaded();
386   for (MapBackTile bt = backtiles; bt; bt = bt.next) bt.onLoaded();
387   if (player) player.onLoaded();
388   //FIXME
389   if (msglist.length) {
390     msglist[0].active = false;
391     msglist[0].timeout = 0.200;
392     osdCheckTimeouts();
393   }
397 // ////////////////////////////////////////////////////////////////////////// //
398 void pickedSpectacles () {
399   foreach (int y; 0..tilesHeight) {
400     foreach (int x; 0..tilesWidth) {
401       MapTile t = tiles[x, y];
402       if (t && t.isInstanceAlive) t.onGotSpectacles();
403     }
404   }
405   foreach (MapTile t; miscTileGrid.allObjects()) {
406     if (t.isInstanceAlive) t.onGotSpectacles();
407   }
411 // ////////////////////////////////////////////////////////////////////////// //
412 #include "rgentile.vc"
413 #include "rgenobj.vc"
416 void onLevelExited () {
417   if (isNormalLevel()) stats.maxLevelComplete = max(stats.maxLevelComplete, global.currLevel);
418   if (onLevelExitedCB) onLevelExitedCB();
419   if (levelKind == LevelKind.Transition) {
420     if (global.thiefLevel > 0) global.thiefLevel -= 1;
421     //orig dbg:if (global.currLevel == 1) global.currLevel += firstLevelSkip; else global.currLevel += levelSkip;
422     global.currLevel += 1;
423     generateLevel();
424   } else {
425     if (lg.finalBossLevel) {
426       startWinCutscene();
427     } else {
428       generateTransitionLevel();
429     }
430   }
431   centerViewAtPlayer();
435 void onOlmecDead (MapObject o) {
436   //class EnemyOlmec['oOlmec'] : MapEnemy;
437   //level.onOlmecDead(self);
438   writeln("*** OLMEC IS DEAD!");
439   foreach (MapTile t; allExits) {
440     if (t.exit) {
441       t.openExit();
442       auto st = checkTileAtPoint(t.ix+8, t.iy+16);
443       if (!st) {
444         st = MakeMapTile(t.ix/16, t.iy/16+1, 'oTemple');
445         st.ore = 0;
446       }
447       st.invincible = true;
448     }
449   }
453 void generateLevelMessages () {
454   if (global.darkLevel) {
455          if (global.hasCrown) osdMessage("THE HEDJET SHINES BRIGHTLY.");
456     else if (global.config.scumDarkness < 2) osdMessage("I CAN'T SEE A THING!");
457     /*
458     else global.message = "";
459          if (global.hasCrown) global.message2 = "";
460     else if (global.scumDarkness &lt; 2) global.message2 = "I'D BETTER USE THESE FLARES!";
461     else global.message2 = "";
462     global.messageTimer = 200;
463     alarm[1] = 210;
464     */
465   }
467   if (global.blackMarket) osdMessage("WELCOME TO THE BLACK MARKET!");
469   if (global.cemetary) osdMessage("THE DEAD ARE RESTLESS!");
470   if (global.lake) osdMessage("I CAN HEAR RUSHING WATER...");
472   if (global.snakePit) osdMessage("I HEAR SNAKES... I HATE SNAKES!");
473   if (global.yetiLair) osdMessage("IT SMELLS LIKE WET FUR IN HERE!");
474   if (global.alienCraft) osdMessage("THERE'S A PSYCHIC PRESENCE HERE!");
475   if (global.cityOfGold) {
476     if (true /*hasOlmec()*/) osdMessage("IT'S THE LEGENDARY CITY OF GOLD!");
477   }
479   if (global.sacrificePit) osdMessage("I CAN HEAR PRAYERS TO KALI!");
483 final MapObject debugSpawnObjectWithClass (class!MapObject oclass, optional bool playerDir) {
484   if (!oclass) return none;
485   int dx = 0, dy = 0;
486   bool canLeft = !isSolidAtPoint(player.ix-8, player.iy);
487   bool canRight = !isSolidAtPoint(player.ix+16, player.iy);
488   if (!canLeft && !canRight) return none;
489   if (canLeft && canRight) {
490     if (playerDir) {
491       dx = (player.dir == MapObject::Dir.Left ? -16 : 16);
492     } else {
493       dx = 16;
494     }
495   } else {
496     dx = (canLeft ? -16 : 16);
497   }
498   auto obj = SpawnMapObjectWithClass(oclass);
499   if (obj isa MapEnemy) { dx -= 8; dy -= 8; }
500   if (obj) PutSpawnedMapObject(player.ix+dx, player.iy+dy, obj);
501   return obj;
505 final MapObject debugSpawnObject (name aname) {
506   if (!aname) return none;
507   return debugSpawnObjectWithClass(findGameObjectClassByName(aname));
511 // `global.currLevel` is the new level
512 void generateTransitionLevel () {
513   xmoney = 0;
514   collectCounter = 0;
516   global.setMusicPitch(1.0);
517   switch (global.config.transitionMusicMode) {
518     case GameConfig::MusicMode.Silent: global.stopMusic(); break;
519     case GameConfig::MusicMode.Restart: global.restartMusic(); break;
520     case GameConfig::MusicMode.DontTouch: break;
521   }
523   levelKind = LevelKind.Transition;
525   auto olddel = ImmediateDelete;
526   ImmediateDelete = false;
527   clearTiles();
528   clearObjects();
530        if (global.currLevel < 4) createTrans1Room();
531   else if (global.currLevel == 4) createTrans1xRoom();
532   else if (global.currLevel < 8) createTrans2Room();
533   else if (global.currLevel == 8) createTrans2xRoom();
534   else if (global.currLevel < 12) createTrans3Room();
535   else if (global.currLevel == 12) createTrans3xRoom();
536   else if (global.currLevel < 16) createTrans4Room();
537   else if (global.currLevel == 16) createTrans4Room();
538   else createTrans1Room(); //???
540   fixWallTiles();
541   addBackgroundGfxDetails();
542   levBGImgName = 'bgCave';
543   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
545   blockWaterChecking = true;
546   fixLiquidTop();
547   cleanDeadTiles();
549   if (damselSaved > 0) {
550     MakeMapObject(176+8, 176+8, 'oDamselKiss');
551     global.plife += damselSaved; // if player skipped transition cutscene
552     damselSaved = 0;
553   }
555   collectLavaTiles();
557   ImmediateDelete = olddel;
558   CollectGarbage(true); // destroy delayed objects too
560   if (dumpGridStats) {
561     miscTileGrid.dumpStats();
562     objGrid.dumpStats();
563   }
565   playerExited = false; // just in case
567   osdClear();
569   setupGhostTime();
570   //global.playMusic(lg.musicName);
574 void generateLevel () {
575   global.setMusicPitch(1.0);
576   stats.clearLevelTotals();
578   levelKind = LevelKind.Normal;
579   lg.generate();
580   //lg.dump();
582   resetRoomBounds();
584   lg.generateRooms();
585   writeln("tw:", tilesWidth, "; th:", tilesHeight);
587   auto olddel = ImmediateDelete;
588   ImmediateDelete = false;
589   clearTiles();
590   clearObjects();
592   if (lg.finalBossLevel) {
593     blockWaterChecking = true;
594     createOlmecRoom();
595   }
597   // if transition cutscene was skipped...
598   if (damselSaved > 0) global.plife += damselSaved; // if player skipped transition cutscene
599   damselSaved = 0;
601   // generate tiles
602   startRoomX = lg.startRoomX;
603   startRoomY = lg.startRoomY;
604   endRoomX = lg.endRoomX;
605   endRoomY = lg.endRoomY;
606   addBackgroundGfxDetails();
607   foreach (int y; 0..tilesHeight) {
608     foreach (int x; 0..tilesWidth) {
609       lg.genTileAt(x, y);
610     }
611   }
612   fixWallTiles();
614   levBGImgName = lg.bgImgName;
615   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
617   if (global.allowAngryShopkeepers) generateAngryShopkeepers();
619   lg.generateEntities();
621   // add box of flares to dark level
622   if (global.darkLevel && allEnters.length) {
623     auto enter = allEnters[0];
624     int x = enter.ix, y = enter.iy;
625          if (!isSolidAtPoint(x-16, y)) MakeMapObject(x-16+8, y+8, 'oFlareCrate');
626     else if (!isSolidAtPoint(x+16, y)) MakeMapObject(x+16+8, y+8, 'oFlareCrate');
627     else MakeMapObject(x+8, y+8, 'oFlareCrate');
628   }
630   //scrGenerateEntities();
631   //foreach (; 0..2) scrGenerateEntities();
633   writeln(countObjects, " alive objects inserted");
634   writeln(countBackTiles, " background tiles inserted");
636   if (!player) FatalError("player pawn is not spawned");
638   if (lg.finalBossLevel) {
639     blockWaterChecking = true;
640   } else {
641     blockWaterChecking = false;
642   }
643   fixLiquidTop();
644   cleanDeadTiles();
646   collectLavaTiles();
648   ImmediateDelete = olddel;
649   CollectGarbage(true); // destroy delayed objects too
651   if (dumpGridStats) {
652     miscTileGrid.dumpStats();
653     objGrid.dumpStats();
654   }
656   playerExited = false; // just in case
658   levelMoneyStart = stats.money;
660   osdClear();
661   generateLevelMessages();
663   xmoney = 0;
664   collectCounter = 0;
666   if (lastMusicName != lg.musicName) {
667     global.playMusic(lg.musicName);
668     //writeln("!<", lastMusicName, ">:<", lg.musicName, ">");
669   } else {
670     //writeln("MM: ", global.config.nextLevelMusicMode);
671     switch (global.config.nextLevelMusicMode) {
672       case GameConfig::MusicMode.Silent: global.stopMusic(); break; // the thing that should not be
673       case GameConfig::MusicMode.Restart: global.restartMusic(); break;
674       case GameConfig::MusicMode.DontTouch:
675         if (global.config.transitionMusicMode == GameConfig::MusicMode.Silent) {
676           global.playMusic(lg.musicName);
677         }
678         break;
679     }
680   }
681   lastMusicName = lg.musicName;
682   //global.playMusic(lg.musicName);
684   setupGhostTime();
688 // ////////////////////////////////////////////////////////////////////////// //
689 int currKeys, nextKeys;
690 int pressedKeysQ, releasedKeysQ;
691 int keysPressed, keysReleased = -1;
694 struct SavedKeyState {
695   int currKeys, nextKeys;
696   int pressedKeysQ, releasedKeysQ;
697   int keysPressed, keysReleased;
698   // for session
699   int roomSeed, otherSeed;
703 // for saving/replaying
704 final void keysSaveState (out SavedKeyState ks) {
705   ks.currKeys = currKeys;
706   ks.nextKeys = nextKeys;
707   ks.pressedKeysQ = pressedKeysQ;
708   ks.releasedKeysQ = releasedKeysQ;
709   ks.keysPressed = keysPressed;
710   ks.keysReleased = keysReleased;
713 // for saving/replaying
714 final void keysRestoreState (const ref SavedKeyState ks) {
715   currKeys = ks.currKeys;
716   nextKeys = ks.nextKeys;
717   pressedKeysQ = ks.pressedKeysQ;
718   releasedKeysQ = ks.releasedKeysQ;
719   keysPressed = ks.keysPressed;
720   keysReleased = ks.keysReleased;
724 final void keysNextFrame () {
725   currKeys = nextKeys;
729 final void clearKeys () {
730   currKeys = 0;
731   nextKeys = 0;
732   pressedKeysQ = 0;
733   releasedKeysQ = 0;
734   keysPressed = 0;
735   keysReleased = -1;
739 final void onKey (int code, bool down) {
740   if (!code) return;
741   if (down) {
742     currKeys |= code;
743     nextKeys |= code;
744     if (keysReleased&code) {
745       keysPressed |= code;
746       keysReleased &= ~code;
747       pressedKeysQ |= code;
748     }
749   } else {
750     nextKeys &= ~code;
751     if (keysPressed&code) {
752       keysReleased |= code;
753       keysPressed &= ~code;
754       releasedKeysQ |= code;
755     }
756   }
759 final bool isKeyDown (int code) {
760   return !!(currKeys&code);
763 final bool isKeyPressed (int code) {
764   bool res = !!(pressedKeysQ&code);
765   pressedKeysQ &= ~code;
766   return res;
769 final bool isKeyReleased (int code) {
770   bool res = !!(releasedKeysQ&code);
771   releasedKeysQ &= ~code;
772   return res;
776 final void clearKeysPressRelease () {
777   keysPressed = default.keysPressed;
778   keysReleased = default.keysReleased;
779   pressedKeysQ = default.pressedKeysQ;
780   releasedKeysQ = default.releasedKeysQ;
781   currKeys = 0;
782   nextKeys = 0;
786 // ////////////////////////////////////////////////////////////////////////// //
787 final void registerEnter (MapTile t) {
788   if (!t) return;
789   allEnters[$] = t;
790   return;
794 final void registerExit (MapTile t) {
795   if (!t) return;
796   allExits[$] = t;
797   return;
801 final bool isYAtEntranceRow (int py) {
802   py /= 16;
803   foreach (MapTile t; allEnters) if (t.iy == py) return true;
804   return false;
808 final int calcNearestEnterDist (int px, int py) {
809   if (allEnters.length == 0) return int.max;
810   int curdistsq = int.max;
811   foreach (MapTile t; allEnters) {
812     int xc = px-t.xCenter, yc = py-t.yCenter;
813     int distsq = xc*xc+yc*yc;
814     if (distsq < curdistsq) curdistsq = distsq;
815   }
816   return round(sqrt(curdistsq));
820 final int calcNearestExitDist (int px, int py) {
821   if (allExits.length == 0) return int.max;
822   int curdistsq = int.max;
823   foreach (MapTile t; allExits) {
824     int xc = px-t.xCenter, yc = py-t.yCenter;
825     int distsq = xc*xc+yc*yc;
826     if (distsq < curdistsq) curdistsq = distsq;
827   }
828   return round(sqrt(curdistsq));
832 // ////////////////////////////////////////////////////////////////////////// //
833 final void clearForTransition () {
834   auto olddel = ImmediateDelete;
835   ImmediateDelete = false;
836   clearTiles();
837   clearObjects();
838   ImmediateDelete = olddel;
839   CollectGarbage(true); // destroy delayed objects too
843 final void clearTiles () {
844   accumTime = 0;
845   time = 0;
846   allEnters.length -= allEnters.length; // don't deallocate
847   allExits.length -= allExits.length; // don't deallocate
848   lavatiles.length -= lavatiles.length;
849   foreach (ref auto tile; tiles) delete tile;
850   if (dumpGridStats) { if (miscTileGrid.getFirstObject()) miscTileGrid.dumpStats(); }
851   miscTileGrid.removeAllObjects(true); // and destroy
852   while (backtiles) {
853     MapBackTile t = backtiles;
854     backtiles = t.next;
855     delete t;
856   }
857   levBGImg = none;
861 // ////////////////////////////////////////////////////////////////////////// //
862 final int countObjects () {
863   return objGrid.countObjects();
866 final int countBackTiles () {
867   int res = 0;
868   for (MapBackTile bt = backtiles; bt; bt = bt.next) ++res;
869   return res;
872 final void clearObjects () {
873   // don't kill objects player is holding
874   if (player) {
875     if (player.pickedItem && player.pickedItem.grid) {
876       player.pickedItem.grid.remove(player.pickedItem.gridId);
877       writeln("secured pocket item '", GetClassName(player.pickedItem.Class), "'");
878       //player.pickedItem.grid = none;
879     }
880     if (player.holdItem && player.holdItem.grid) {
881       player.holdItem.grid.remove(player.holdItem.gridId);
882       writeln("secured held item '", GetClassName(player.holdItem.Class), "'");
883       //player.holdItem.grid = none;
884     }
885   }
886   //
887   int count = objGrid.countObjects();
888   if (dumpGridStats) { if (objGrid.getFirstObject()) objGrid.dumpStats(); }
889   objGrid.removeAllObjects(true); // and destroy
890   if (count > 0) writeln(count, " objects destroyed");
891   ballObjects.length = 0;
892   lastUsedObjectId = 0;
896 final void insertObject (MapObject o) {
897   if (!o) return;
898   if (o.grid) FatalError("cannot put object into level twice");
899   o.objId = ++lastUsedObjectId;
901   // ball from ball-and-chain
902   if (o.objType == 'oBall') {
903     bool found = false;
904     foreach (MapObject bo; ballObjects) if (bo == o) { found = true; break; }
905     if (!found) ballObjects[$] = o;
906   }
908   objGrid.insert(o);
912 final void spawnPlayerAt (int x, int y) {
913   // if we have no player, spawn new one
914   // otherwise this just a level transition, so simply reposition him
915   if (!player) {
916     // don't add player to object list, as it has very separate processing anyway
917     player = SpawnObject(PlayerPawn);
918     player.global = global;
919     player.level = self;
920     if (!player.initialize()) {
921       delete player;
922       FatalError("something is wrong with player initialization");
923       return;
924     }
925   }
926   player.fltx = x;
927   player.flty = y;
928   player.saveInterpData();
929   player.resurrect();
930   playerExited = false;
931   if (global.config.startWithKapala) global.hasKapala = true;
932   centerViewAtPlayer();
933   // reinsert player items into grid
934   if (player.pickedItem) objGrid.insert(player.pickedItem);
935   if (player.holdItem) objGrid.insert(player.holdItem);
936   //writeln("player spawned; active=", player.active);
937   player.scrSwitchToPocketItem(forceIfEmpty:false);
941 final void teleportPlayerTo (int x, int y) {
942   if (player) {
943     player.fltx = x;
944     player.flty = y;
945     player.saveInterpData();
946   }
950 final void resurrectPlayer () {
951   if (player) player.resurrect();
952   playerExited = false;
956 // ////////////////////////////////////////////////////////////////////////// //
957 final void scrShake (int duration) {
958   if (shakeLeft == 0) {
959     shakeOfs.x = 0;
960     shakeOfs.y = 0;
961     shakeDir.x = 0;
962     shakeDir.y = 0;
963   }
964   shakeLeft = max(shakeLeft, duration);
969 // ////////////////////////////////////////////////////////////////////////// //
970 enum SCAnger {
971   TileDestroyed,
972   ItemStolen, // including damsel, lol
973   CrapsCheated,
974   BombDropped,
975   DamselWhipped,
979 // make the nearest shopkeeper angry. RAWR!
980 void scrShopkeeperAnger (SCAnger reason, optional int maxdist, optional MapEntity offender) {
981   if (!offender) offender = player;
982   auto shp = MonsterShopkeeper(findNearestEnemy(offender.ix, offender.iy, delegate bool (MapEnemy o) {
983     auto sc = MonsterShopkeeper(o);
984     if (!sc) return false;
985     if (sc.dead || sc.angered) return false;
986     return true;
987   }));
989   if (shp) {
990     if (specified_maxdist && offender.directionToEntityCenter(shp) > maxdist) return;
991     if (!shp.dead && !shp.angered) {
992       shp.status = MapObject::ATTACK;
993       string msg;
994            if (global.murderer) msg = "YOU'LL PAY FOR YOUR CRIMES!";
995       else if (reason == SCAnger.ItemStolen) msg = "COME BACK HERE, THIEF!";
996       else if (reason == SCAnger.TileDestroyed) msg = "DIE, YOU VANDAL!";
997       else if (reason == SCAnger.BombDropped) msg = "TERRORIST!";
998       else if (reason == SCAnger.DamselWhipped) msg = "HEY, ONLY I CAN DO THAT!";
999       else if (reason == SCAnger.CrapsCheated) msg = "DIE, CHEATER!";
1000       else msg = "NOW I'M REALLY STEAMED!";
1001       if (msg) osdMessage(msg, -666);
1002       global.thiefLevel += (global.thiefLevel > 0 ? 3 : 2);
1003     }
1004   }
1008 final MapObject findCrapsPrize () {
1009   foreach (MapObject o; objGrid.allObjects()) {
1010     if (o.spectral || !o.isInstanceAlive) continue;
1011     if (o.inDiceHouse) return o;
1012   }
1013   return none;
1017 // ////////////////////////////////////////////////////////////////////////// //
1018 // 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.
1019 // note: idols moved by monkeys will have false `stolenIdol`
1020 void scrTriggerIdolAltar (bool stolenIdol) {
1021   ObjTikiCurse res = none;
1022   int curdistsq = int.max;
1023   int px = player.xCenter, py = player.yCenter;
1024   foreach (MapObject o; objGrid.allObjects()) {
1025     auto tcr = ObjTikiCurse(o);
1026     if (!tcr || !tcr.isInstanceAlive) continue;
1027     if (tcr.activated) continue;
1028     int xc = px-tcr.xCenter, yc = py-tcr.yCenter;
1029     int distsq = xc*xc+yc*yc;
1030     if (distsq < curdistsq) {
1031       res = tcr;
1032       curdistsq = distsq;
1033     }
1034   }
1035   if (res) res.activate(stolenIdol);
1039 // ////////////////////////////////////////////////////////////////////////// //
1040 void setupGhostTime () {
1041   musicFadeTimer = -1;
1042   ghostSpawned = false;
1044   if (inWinCutscene || !isNormalLevel() || lg.finalBossLevel) {
1045     ghostTimeLeft = -1;
1046     global.setMusicPitch(1.0);
1047     return;
1048   }
1050   if (global.config.scumGhost < 0) {
1051     // instant
1052     ghostTimeLeft = 1;
1053     osdMessage("A GHASTLY VOICE CALLS YOUR NAME!\nLET'S GET OUT OF HERE!", 6.66, urgent:true);
1054     return;
1055   }
1057   if (global.config.scumGhost == 0) {
1058     // never
1059     ghostTimeLeft = -1;
1060     return;
1061   }
1063   // randomizes time until ghost appears once time limit is reached
1064   // global.ghostRandom (true/false) if extra time is randomized - loaded from yasm.cfg. if false, use 30 second default
1065   // ghostTimeLeft (time in seconds * 1000) for currently generated level
1067   if (global.config.ghostRandom) {
1068     auto tMin = global.randOther(10, 30); // min(global.plife*10, 30);
1069     auto tMax = max(tMin+global.randOther(10, 30), min(global.config.scumGhost, 300)); // 5 minutes max
1070     auto tTime = global.randOther(tMin, tMax);
1071     if (tTime <= 0) tTime = round(tMax/2.0);
1072     ghostTimeLeft = tTime;
1073   } else {
1074     ghostTimeLeft = global.config.scumGhost; // 5 minutes max
1075   }
1077   ghostTimeLeft += max(0, global.config.ghostExtraTime);
1079   ghostTimeLeft *= 30; // seconds -> frames
1080   //global.ghostShowTime
1084 void spawnGhost () {
1085   addGhostSummoned();
1086   ghostSpawned = true;
1088   int vwdt = (viewMax.x-viewMin.x);
1089   int vhgt = (viewMax.y-viewMin.y);
1091   int gx, gy;
1093   if (player.ix < viewMin.x+vwdt/2) {
1094     // player is in the left side
1095     gx = viewMin.x+vwdt/2+vwdt/4;
1096   } else {
1097     // player is in the right side
1098     gx = viewMin.x+vwdt/4;
1099   }
1101   if (player.iy < viewMin.y+vhgt/2) {
1102     // player is in the left side
1103     gy = viewMin.y+vhgt/2+vhgt/4;
1104   } else {
1105     // player is in the right side
1106     gy = viewMin.y+vhgt/4;
1107   }
1109   writeln("spawning ghost at tile (", gx/16, ",", gy/16, "); player is at tile (", player.ix/16, ",", player.iy/16, ")");
1111   MakeMapObject(gx, gy, 'oGhost');
1113   /*
1114     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);
1115     else instance_create(view_xview[0]-32, view_yview[0]+floor(view_hview[0]/2), oGhost);
1116     global.ghostExists = true;
1117   */
1121 void thinkFrameGameGhost () {
1122   if (player.dead) return;
1123   if (!isNormalLevel()) return; // just in case
1125   if (ghostTimeLeft < 0) {
1126     // turned off
1127     if (musicFadeTimer > 0) {
1128       musicFadeTimer = -1;
1129       global.setMusicPitch(1.0);
1130     }
1131     return;
1132   }
1134   if (musicFadeTimer >= 0) {
1135     ++musicFadeTimer;
1136     if (musicFadeTimer > 0 && musicFadeTimer <= 100*3) {
1137       float pitch = 1.0-(0.00226/3.0)*musicFadeTimer;
1138       //writeln("MFT: ", musicFadeTimer, "; pitch=", pitch);
1139       global.setMusicPitch(pitch);
1140     }
1141   }
1143   if (ghostTimeLeft == 0) {
1144     // she is already here!
1145     return;
1146   }
1148   // no ghost if we have a crown
1149   if (global.hasCrown) {
1150     ghostTimeLeft = -1;
1151     return;
1152   }
1154   // if she was already spawned, don't do it again
1155   if (ghostSpawned) {
1156     ghostTimeLeft = 0;
1157     return;
1158   }
1160   if (--ghostTimeLeft != 0) {
1161     // warning
1162     if (global.config.ghostExtraTime > 0) {
1163       if (ghostTimeLeft == global.config.ghostExtraTime*30) {
1164         osdMessage("A CHILL RUNS UP YOUR SPINE...\nLET'S GET OUT OF HERE!", 6.66, urgent:true);
1165       }
1166       if (musicFadeTimer < 0 && ghostTimeLeft <= global.config.ghostExtraTime*30) {
1167         musicFadeTimer = 0;
1168       }
1169     }
1170     return;
1171   }
1173   // spawn her
1174   if (player.isExitingSprite) {
1175     // no reason to spawn her, we're leaving
1176     ghostTimeLeft = -1;
1177     return;
1178   }
1180   spawnGhost();
1184 void thinkFrameGame () {
1185   thinkFrameGameGhost();
1189 // ////////////////////////////////////////////////////////////////////////// //
1190 private transient array!MapObject activeThinkerList;
1193 private final bool isWetTile (MapTile t) {
1194   return (t && t.visible && (t.water || t.lava || t.wet));
1198 private final bool isWetOrSolidTile (MapTile t) {
1199   return (t && t.visible && (t.water || t.lava || t.wet || t.solid) && t.isInstanceAlive);
1203 final bool isWetOrSolidTileAtPoint (int px, int py) {
1204   return !!checkTileAtPoint(px, py, &isWetOrSolidTile);
1208 final bool isWetOrSolidTileAtTile (int tx, int ty) {
1209   return !!checkTileAtPoint(tx*16, ty*16, &isWetOrSolidTile);
1213 final bool isWetTileAtTile (int tx, int ty) {
1214   return !!checkTileAtPoint(tx*16, ty*16, &isWetTile);
1218 // ////////////////////////////////////////////////////////////////////////// //
1219 const int GreatLakeStartTileY = 28;
1221 // called once after level generation
1222 final void fixLiquidTop () {
1223   foreach (int tileY; 0..tilesHeight) {
1224     foreach (int tileX; 0..tilesWidth) {
1225       auto t = tiles[tileX, tileY];
1227       if (t && !t.isInstanceAlive) {
1228         delete tiles[tileX, tileY];
1229         t = none;
1230       }
1232       if (!t) {
1233         if (global.lake && tileY >= GreatLakeStartTileY) {
1234           // fill level with water for lake
1235           MakeMapTile(tileX, tileY, 'oWaterSwim');
1236           t = tiles[tileX, tileY];
1237         } else {
1238           continue;
1239         }
1240       }
1242       if (!t.water && !t.lava) {
1243         // mark as wet for lake
1244         if (global.lake && tileY >= GreatLakeStartTileY) {
1245           t.wet = true;
1246         }
1247         continue;
1248       }
1250       if (!isWetTileAtTile(tileX, tileY-1)) {
1251         t.setSprite(t.lava ? 'sLavaTop' : 'sWaterTop');
1252       } else {
1253              if (t.spriteName == 'sWaterTop') t.setSprite('sWater');
1254         else if (t.spriteName == 'sLavaTop') t.setSprite('sLava');
1255       }
1256     }
1257   }
1261 private final void checkWaterFlow (MapTile wtile) {
1262   //if (!wtile || (!wtile.water && !wtile.lava)) return;
1263   //instance_activate_region(x-16, y-16, 48, 48, true);
1265   //int x = wtile.ix, y = wtile.iy;
1266   int tileX = wtile.ix/16, tileY = wtile.iy/16;
1268   if (global.lake && tileY >= GreatLakeStartTileY) return;
1270   /*
1271   if ((not collision_point(x-16, y, oSolid, 0, 0) and not collision_point(x-16, y, oWater, 0, 0)) or
1272       (not collision_point(x+16, y, oSolid, 0, 0) and not collision_point(x+16, y, oWater, 0, 0)) or
1273       (not collision_point(x, y+16, oSolid, 0, 0) and not collision_point(x, y+16, oWater, 0, 0)))
1274   */
1275   if (!isWetOrSolidTileAtTile(tileX-1, tileY) ||
1276       !isWetOrSolidTileAtTile(tileX+1, tileY) ||
1277       !isWetOrSolidTileAtTile(tileX, tileY+1))
1278   {
1279     checkWater = true;
1280     wtile.smashMe();
1281     wtile.instanceRemove();
1282     wtile.onDestroy();
1283     delete wtile;
1284     tiles[tileX, tileY] = none;
1285     return;
1286   }
1288   //if (!isSolidAtPoint(x, y-16) && !isLiquidAtPoint(x, y-16)) {
1289   if (!isWetTileAtTile(tileX, tileY-1)) {
1290     wtile.setSprite(wtile.lava ? 'sLavaTop' : 'sWaterTop');
1291   }
1295 transient private array!MapTile waterTilesToCheck;
1297 final void cleanDeadTiles () {
1298   bool hasWater = false;
1299   waterTilesToCheck.length -= waterTilesToCheck.length;
1300   foreach (int y; 0..tilesHeight) {
1301     foreach (int x; 0..tilesWidth) {
1302       auto t = tiles[x, y];
1303       if (!t) continue;
1304       if (t.isInstanceAlive) {
1305         if (t.water || t.lava) waterTilesToCheck[$] = t;
1306         continue;
1307       }
1308       checkWater = true;
1309       t.onDestroy();
1310       delete t;
1311       tiles[x, y] = none;
1312     }
1313   }
1314   if (waterTilesToCheck.length && checkWater && !blockWaterChecking) {
1315     //writeln("checking water");
1316     checkWater = false; // `checkWaterFlow()` can set it again
1317     foreach (MapTile t; waterTilesToCheck) {
1318       if (t && t.isInstanceAlive && (t.water || t.lava)) checkWaterFlow(t);
1319     }
1320     // fill empty spaces in lake with water
1321     if (global.lake) {
1322       foreach (int y; GreatLakeStartTileY..tilesHeight) {
1323         foreach (int x; 0..tilesWidth) {
1324           auto t = tiles[x, y];
1325           // just in case
1326           if (t && !t.isInstanceAlive) {
1327             t.onDestroy();
1328             delete tiles[x, y];
1329             t = none;
1330           }
1331           if (t) {
1332             if (!t.water || !t.lava) { t.wet = true; continue; }
1333           } else {
1334             MakeMapTile(x, y, 'oWaterSwim');
1335             t = tiles[x, y];
1336           }
1337           if (t.water) {
1338             t.setSprite(y == GreatLakeStartTileY ? 'sWaterTop' : 'sWater');
1339           } else if (t.lava) {
1340             t.setSprite(y == GreatLakeStartTileY ? 'sLavaTop' : 'sLava');
1341           }
1342         }
1343       }
1344     }
1345   }
1349 // ////////////////////////////////////////////////////////////////////////// //
1350 void collectLavaTiles () {
1351   lavatiles.length -= lavatiles.length;
1352   foreach (MapTile t; tiles) {
1353     if (t && t.lava && t.isInstanceAlive) lavatiles[$] = t;
1354   }
1358 void processLavaTiles () {
1359   int tn = 0, tlen = lavatiles.length;
1360   while (tn < tlen) {
1361     MapTile t = lavatiles[tn];
1362     if (t && t.isInstanceAlive) {
1363       t.thinkFrame();
1364       ++tn;
1365     } else {
1366       lavatiles.remove(tn, 1);
1367       --tlen;
1368     }
1369   }
1373 // ////////////////////////////////////////////////////////////////////////// //
1374 // return `true` if thinker should be removed
1375 final bool thinkOne (MapObject o) {
1376   if (!o) return true;
1377   if (o.active && o.isInstanceAlive) {
1378     bool doThink = true;
1380     // collision with player weapon
1381     auto hh = PlayerWeapon(player.holdItem);
1382     bool doWeaponAction;
1383     if (hh) {
1384       if (hh.blockedBySolids) {
1385         int xx = player.ix+(player.dir == MapObject::Dir.Left ? -16 : 16);
1386         doWeaponAction = !isSolidAtPoint(xx, player.iy);
1387       } else {
1388         doWeaponAction = true;
1389       }
1390     } else {
1391       doWeaponAction = false;
1392     }
1394     if (doWeaponAction && o.whipTimer <= 0 && hh && hh.collidesWithObject(o)) {
1395       //writeln("WEAPONED!");
1396       if (!o.onTouchedByPlayerWeapon(player, hh)) {
1397         if (o.isInstanceAlive) hh.onCollisionWithObject(o);
1398       }
1399       o.whipTimer = o.whipTimerValue; //HACK
1400       doThink = o.isInstanceAlive;
1401     }
1403     // collision with player
1404     if (doThink && o.collidesWith(player)) {
1405       if (!player.onObjectTouched(o) && o.isInstanceAlive) {
1406         doThink = !o.onTouchedByPlayer(player);
1407         if (o.grid && o.gridId) o.grid.update(o.gridId, markAsDead:true);
1408       }
1409     }
1411     if (doThink && o.isInstanceAlive) {
1412       o.saveInterpData();
1413       o.processAlarms();
1414       if (o.isInstanceAlive) {
1415         if (o.grid && o.gridId) o.grid.update(o.gridId, markAsDead:true);
1416         o.thinkFrame();
1417         if (o.isInstanceAlive) {
1418           o.nextAnimFrame();
1419           if (o.grid && o.gridId) o.grid.update(o.gridId, markAsDead:true);
1420         }
1421       }
1422     }
1423   }
1424   if (o.isInstanceAlive) {
1425     if (!o.canLiveOutsideOfLevel && o.isOutsideOfLevel()) {
1426       //dead
1427       o.instanceRemove();
1428       return true;
1429     }
1430     // alive
1431     return false;
1432   } else {
1433     // dead
1434     return true;
1435   }
1439 final void processThinkers (float timeDelta) {
1440   if (timeDelta <= 0) return;
1441   if (gamePaused) {
1442     if (onBeforeFrame) onBeforeFrame(false);
1443     if (onAfterFrame) onAfterFrame(false);
1444     keysNextFrame();
1445     return;
1446   }
1447   accumTime += timeDelta;
1448   bool wasFrame = false;
1449   // block GC
1450   auto olddel = ImmediateDelete;
1451   ImmediateDelete = false;
1452   while (accumTime >= FrameTime) {
1453     accumTime -= FrameTime;
1454     if (onBeforeFrame) onBeforeFrame(accumTime >= FrameTime);
1455     // shake
1456     if (shakeLeft > 0) {
1457       --shakeLeft;
1458       if (!shakeDir.x) shakeDir.x = (global.randOther(0, 1) ? -3 : 3);
1459       if (!shakeDir.y) shakeDir.y = (global.randOther(0, 1) ? -3 : 3);
1460       shakeOfs.x = shakeDir.x;
1461       shakeOfs.y = shakeDir.y;
1462       int sgnc = global.randOther(1, 3);
1463       if (sgnc&0x01) shakeDir.x = -shakeDir.x;
1464       if (sgnc&0x02) shakeDir.y = -shakeDir.y;
1465     } else {
1466       shakeOfs.x = 0;
1467       shakeOfs.y = 0;
1468       shakeDir.x = 0;
1469       shakeDir.y = 0;
1470     }
1471     // game-global events
1472     thinkFrameGame();
1473     // frame thinkers: lava tiles
1474     processLavaTiles();
1475     // frame thinkers: player
1476     if (player && !disablePlayerThink) {
1477       // time limit
1478       if (!player.dead && isNormalLevel() &&
1479           (maxPlayingTime < 0 ||
1480            (maxPlayingTime > 0 && maxPlayingTime*30 <= time &&
1481             time%30 == 0 && global.randOther(1, 100) <= 20)))
1482       {
1483         MakeMapObject(player.ix, player.iy, 'oExplosion');
1484         player.scrCreateFlame(player.ix, player.iy, 3);
1485       }
1486       //HACK: check for stolen items
1487       auto item = MapItem(player.holdItem);
1488       if (item) item.onCheckItemStolen(player);
1489       item = MapItem(player.pickedItem);
1490       if (item) item.onCheckItemStolen(player);
1491       // normal thinking
1492       player.saveInterpData();
1493       player.processAlarms();
1494       if (player.isInstanceAlive) {
1495         player.thinkFrame();
1496         if (player.isInstanceAlive) player.nextAnimFrame();
1497       }
1498     }
1499     // frame thinkers: moveable solids
1500     physStep();
1501     // frame thinkers: objects
1502     auto grid = objGrid;
1503     // collect active objects
1504     if (global.config.useFrozenRegion) {
1505       activeThinkerList.length -= activeThinkerList.length;
1506       foreach (MapObject o; grid.inRectPix(viewStart.x/global.scale-64, viewStart.y/global.scale-64, 320+64*2, 240+64*2, tag:grid.nextTag(), precise:false)) {
1507         activeThinkerList[$] = o;
1508       }
1509       //writeln("thinkers: ", activeThinkerList.length);
1510       foreach (MapObject o; activeThinkerList) {
1511         if (thinkOne(o)) {
1512           grid.remove(o.gridId);
1513           o.onDestroy();
1514           delete o;
1515         }
1516       }
1517     } else {
1518       // no frozen area
1519       bool killThisOne = false;
1520       for (int cid = grid.getFirstObject(); cid; cid = grid.getNextObject(cid, killThisOne)) {
1521         killThisOne = false;
1522         MapObject o = grid.getObject(MapObject, cid);
1523         if (!o) { killThisOne = true; continue; }
1524         // remove this object if it is dead
1525         if (thinkOne(o)) {
1526           killThisOne = true;
1527           if (o) {
1528             o.onDestroy();
1529             delete o;
1530           }
1531         }
1532       }
1533     }
1534     if (player && player.holdItem) {
1535       if (player.holdItem.isInstanceAlive) {
1536         player.holdItem.fixHoldCoords();
1537       } else {
1538         player.holdItem = none;
1539       }
1540     }
1541     // done with thinkers
1542     cleanDeadTiles();
1543     // money counter
1544     if (collectCounter == 0) {
1545       xmoney = max(0, xmoney-100);
1546     } else {
1547       --collectCounter;
1548     }
1549     // other things
1550     if (player && !player.dead) stats.oneMoreFramePlayed();
1551     if (onAfterFrame) onAfterFrame(accumTime >= FrameTime);
1552     keysNextFrame();
1553     wasFrame = true;
1554     if (playerExited) break;
1555   }
1556   ImmediateDelete = olddel;
1557   if (playerExited) {
1558     playerExited = false;
1559     onLevelExited();
1560   }
1561   if (wasFrame) {
1562     // if we were processed at least one frame, collect garbage
1563     //keysNextFrame();
1564     CollectGarbage(true); // destroy delayed objects too
1565   }
1566   if (accumTime > 0 && onInterFrame) onInterFrame(accumTime/FrameTime);
1570 // ////////////////////////////////////////////////////////////////////////// //
1571 final void tileXY2roomXY (int tileX, int tileY, out int roomX, out int roomY) {
1572   roomX = (tileX-1)/RoomGen::Width;
1573   roomY = (tileY-1)/RoomGen::Height;
1577 final bool isInShop (int tileX, int tileY) {
1578   if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
1579     auto n = roomType[tileX, tileY];
1580     if (n == 4 || n == 5) return true;
1581     auto t = getTileAt(tileX, tileY);
1582     if (t && t.shopWall) return true;
1583     //k8: we don't have this
1584     //if (t && t.objType == 'oShop') return true;
1585   }
1586   return false;
1590 // ////////////////////////////////////////////////////////////////////////// //
1591 override void Destroy () {
1592   clearTiles();
1593   clearObjects();
1594   delete tempSolidTile;
1595   ::Destroy();
1599 // ////////////////////////////////////////////////////////////////////////// //
1600 final MapObject findNearestBall (int px, int py) {
1601   MapObject res = none;
1602   int curdistsq = int.max;
1603   foreach (MapObject o; ballObjects) {
1604     if (!o || o.spectral || !o.isInstanceAlive) continue;
1605     int xc = px-o.xCenter, yc = py-o.yCenter;
1606     int distsq = xc*xc+yc*yc;
1607     if (distsq < curdistsq) {
1608       res = o;
1609       curdistsq = distsq;
1610     }
1611   }
1612   return res;
1616 final int calcNearestBallDist (int px, int py) {
1617   auto e = findNearestBall(px, py);
1618   if (!e) return int.max;
1619   int xc = px-e.xCenter, yc = py-e.yCenter;
1620   return round(sqrt(xc*xc+yc*yc));
1624 final MapObject findNearestObject (int px, int py, bool delegate (MapObject o) dg) {
1625   MapObject res = none;
1626   int curdistsq = int.max;
1627   foreach (MapObject o; objGrid.allObjects()) {
1628     if (o.spectral || !o.isInstanceAlive) continue;
1629     if (!dg(o)) continue;
1630     int xc = px-o.xCenter, yc = py-o.yCenter;
1631     int distsq = xc*xc+yc*yc;
1632     if (distsq < curdistsq) {
1633       res = o;
1634       curdistsq = distsq;
1635     }
1636   }
1637   return res;
1641 final MapObject findNearestEnemy (int px, int py, optional bool delegate (MapEnemy o) dg) {
1642   MapObject res = none;
1643   int curdistsq = int.max;
1644   foreach (MapObject o; objGrid.allObjects()) {
1645     //k8: i added `dead` check
1646     if (o.spectral || o !isa MapEnemy || o.dead || !o.isInstanceAlive) continue;
1647     if (dg) {
1648       if (!dg(MapEnemy(o))) continue;
1649     }
1650     int xc = px-o.xCenter, yc = py-o.yCenter;
1651     int distsq = xc*xc+yc*yc;
1652     if (distsq < curdistsq) {
1653       res = o;
1654       curdistsq = distsq;
1655     }
1656   }
1657   return res;
1661 final MonsterShopkeeper hasAliveShopkeepers (optional bool skipAngry) {
1662   foreach (MapObject o; objGrid.allObjects()) {
1663     auto sc = MonsterShopkeeper(o);
1664     if (!sc || o.spectral || !o.isInstanceAlive) continue;
1665     if (sc.dead) continue;
1666     if (skipAngry && sc.angered) continue;
1667     return sc;
1668   }
1669   return none;
1673 final int calcNearestEnemyDist (int px, int py, optional bool delegate (MapEnemy o) dg) {
1674   auto e = findNearestEnemy(px, py, dg!optional);
1675   if (!e) return int.max;
1676   int xc = px-e.xCenter, yc = py-e.yCenter;
1677   return round(sqrt(xc*xc+yc*yc));
1681 final int calcNearestObjectDist (int px, int py, optional bool delegate (MapObject o) dg) {
1682   auto e = findNearestObject(px, py, dg!optional);
1683   if (!e) return int.max;
1684   int xc = px-e.xCenter, yc = py-e.yCenter;
1685   return round(sqrt(xc*xc+yc*yc));
1689 final MapTile findNearestMoveableSolid (int px, int py, optional bool delegate (MapTile t) dg) {
1690   MapTile res = none;
1691   int curdistsq = int.max;
1692   foreach (MapTile t; miscTileGrid.allObjects()) {
1693     if (t.spectral || !t.isInstanceAlive) continue;
1694     if (dg) {
1695       if (!dg(t)) continue;
1696     } else {
1697       if (!t.solid || !t.moveable) continue;
1698     }
1699     int xc = px-t.xCenter, yc = py-t.yCenter;
1700     int distsq = xc*xc+yc*yc;
1701     if (distsq < curdistsq) {
1702       res = t;
1703       curdistsq = distsq;
1704     }
1705   }
1706   return res;
1710 final MapTile findNearestTile (int px, int py, optional bool delegate (MapTile t) dg) {
1711   if (!dg) return none;
1712   MapTile res = none;
1713   int curdistsq = int.max;
1715   //FIXME: make this faster!
1716   foreach (MapTile t; tiles) {
1717     if (!t || t.spectral || !t.isInstanceAlive) continue;
1718     int xc = px-t.xCenter, yc = py-t.yCenter;
1719     int distsq = xc*xc+yc*yc;
1720     if (distsq < curdistsq && dg(t)) {
1721       res = t;
1722       curdistsq = distsq;
1723     }
1724   }
1726   foreach (MapTile t; miscTileGrid.allObjects()) {
1727     if (!t || t.spectral || !t.isInstanceAlive) continue;
1728     int xc = px-t.xCenter, yc = py-t.yCenter;
1729     int distsq = xc*xc+yc*yc;
1730     if (distsq < curdistsq && dg(t)) {
1731       res = t;
1732       curdistsq = distsq;
1733     }
1734   }
1736   return res;
1740 // ////////////////////////////////////////////////////////////////////////// //
1741 final bool cbIsObjectWeb (MapObject o) { return (o isa ItemWeb); }
1742 final bool cbIsObjectBlob (MapObject o) { return (o.objName == 'oBlob'); }
1743 final bool cbIsObjectArrow (MapObject o) { return (o isa ItemProjectileArrow); }
1744 final bool cbIsObjectEnemy (MapObject o) { return (/*o != self &&*/ o isa MapEnemy); }
1746 final bool cbIsObjectTreasure (MapObject o) { return (o.isTreasure); }
1748 final bool cbIsItemObject (MapObject o) { return (o isa MapItem); }
1750 final bool cbIsItemOrEnemyObject (MapObject o) { return (o isa MapItem || o isa MapEnemy); }
1753 final MapObject isObjectAtTile (int tileX, int tileY, optional bool delegate (MapObject o) dg) {
1754   tileX *= 16;
1755   tileY *= 16;
1756   foreach (MapObject o; objGrid.inRectPix(tileX, tileY, 16, 16, objGrid.nextTag(), precise: true)) {
1757     if (o.spectral || !o.isInstanceAlive) continue;
1758     if (dg) {
1759       if (dg(o)) return o;
1760     } else {
1761       return o;
1762     }
1763   }
1764   return none;
1768 final MapObject isObjectAtTilePix (int x, int y, optional bool delegate (MapObject o) dg) {
1769   return isObjectAtTile(x/16, y/16, dg!optional);
1773 final MapObject isObjectAtPoint (int xpos, int ypos, optional bool delegate (MapObject o) dg, optional bool precise) {
1774   if (!specified_precise) precise = true;
1775   foreach (MapObject o; objGrid.inCellPix(xpos, ypos, objGrid.nextTag(), precise: precise)) {
1776     if (o.spectral || !o.isInstanceAlive) continue;
1777     if (dg) {
1778       if (dg(o)) return o;
1779     } else {
1780       if (o isa MapEnemy) return o;
1781     }
1782   }
1783   return none;
1787 final MapObject isObjectInRect (int xpos, int ypos, int w, int h, optional bool delegate (MapObject o) dg, optional bool precise) {
1788   if (w < 1 || h < 1) return none;
1789   if (w == 1 && h == 1) return isObjectAtPoint(xpos, ypos, dg!optional, precise!optional);
1790   if (!specified_precise) precise = true;
1791   foreach (MapObject o; objGrid.inRectPix(xpos, ypos, w, h, objGrid.nextTag(), precise: precise)) {
1792     if (o.spectral || !o.isInstanceAlive) continue;
1793     if (dg) {
1794       if (dg(o)) return o;
1795     } else {
1796       if (o isa MapEnemy) return o;
1797     }
1798   }
1799   return none;
1803 final MapObject forEachObject (bool delegate (MapObject o) dg, optional bool allowSpectrals) {
1804   if (!dg) return none;
1805   /*
1806   foreach (MapObject o; objGrid.allObjects()) {
1807     if (o.spectral || !o.isInstanceAlive) continue;
1808     if (dg(o)) return o;
1809   }
1810   */
1811   // process gravity for moveable solids and burning for ropes
1812   auto grid = objGrid;
1813   int cid = grid.getFirstObject();
1814   while (cid) {
1815     MapObject o = grid.getObject(MapObject, cid);
1816     if (!o || !o.isInstanceAlive) {
1817       cid = grid.getNextObject(cid, removeThis:true);
1818       continue;
1819     }
1820     if (!allowSpectrals && o.spectral) {
1821       cid = grid.getNextObject(cid, removeThis:false);
1822       continue;
1823     }
1824     if (dg(o)) return o;
1825     if (o.isInstanceAlive) {
1826       cid = grid.getNextObject(cid, removeThis:false);
1827     } else {
1828       cid = grid.getNextObject(cid, removeThis:true);
1829       o.instanceRemove(); // just in case
1830       o.onDestroy();
1831       delete o;
1832     }
1833   }
1834   return none;
1838 final MapObject forEachObjectAtPoint (int xpos, int ypos, bool delegate (MapObject o) dg, optional bool precise) {
1839   if (!dg) return none;
1840   if (!specified_precise) precise = true;
1841   foreach (MapObject o; objGrid.inCellPix(xpos, ypos, objGrid.nextTag(), precise: precise)) {
1842     if (o.spectral || !o.isInstanceAlive) continue;
1843     if (dg(o)) return o;
1844   }
1845   return none;
1849 final MapObject forEachObjectInRect (int xpos, int ypos, int w, int h, bool delegate (MapObject o) dg, optional bool precise) {
1850   if (!dg || w < 1 || h < 1) return none;
1851   if (w == 1 && h == 1) return forEachObjectAtPoint(xpos, ypos, dg!optional, precise!optional);
1852   if (!specified_precise) precise = true;
1853   foreach (MapObject o; objGrid.inRectPix(xpos, ypos, w, h, objGrid.nextTag(), precise: precise)) {
1854     if (o.spectral || !o.isInstanceAlive) continue;
1855     if (dg(o)) return o;
1856   }
1857   return none;
1861 private final bool cbIsRopeTile (MapTile t) { return t.rope; }
1863 final MapTile isRopeAtPoint (int px, int py) {
1864   return checkTileAtPoint(px, py, &cbIsRopeTile);
1868 //FIXME!
1869 final MapTile isWaterSwimAtPoint (int px, int py) {
1870   return isWaterAtPoint(px, py);
1874 // ////////////////////////////////////////////////////////////////////////// //
1875 private array!MapObject tmpObjectList;
1877 private final bool cbCollectObjectsWithMask (MapObject t) {
1878   if (!t || !t.visible || t.spectral /*|| t.invincible*/ || !t.isInstanceAlive) return false;
1879   //auto spf = getSpriteFrame();
1880   //if (!t.sprite || t.sprite.frames.length < 1) return false;
1881   tmpObjectList[$] = t;
1882   return false;
1886 final void touchObjectsWithMask (int x, int y, SpriteFrame frm, bool delegate (MapObject t) dg) {
1887   if (!frm || frm.bw < 1 || frm.bh < 1 || !dg) return;
1888   if (frm.isEmptyPixelMask) return;
1889   // collect tiles
1890   if (tmpObjectList.length) tmpObjectList.length -= tmpObjectList.length; // don't realloc
1891   if (player.isRectCollisionFrame(frm, x, y)) {
1892     //writeln("player hit");
1893     tmpObjectList[$] = player;
1894   } else {
1895     /*
1896     writeln("no player hit: plr=(", player.ix, ",", player.iy, ")-(", player.ix+player.width-1, ",", player.iy+player.height-1, "); ",
1897       "frm=(", x+frm.bx, ",", y+frm.by, ")-(", x+frm.bx+frm.bw-1, ",", y+frm.by+frm.bh-1, ")");
1898     */
1899   }
1900   forEachObjectInRect(x+frm.bx, y+frm.by, frm.bw, frm.bh, &cbCollectObjectsWithMask);
1901   foreach (MapObject t; tmpObjectList) {
1902     if (!t || !t.isInstanceAlive || !t.visible || t.spectral) continue;
1903     /*
1904     auto tf = t.getSpriteFrame();
1905     if (!tf) {
1906       //writeln("no sprite frame for ", GetClassName(t.Class));
1907       continue;
1908     }
1909     */
1910     /*
1911     if (frm.pixelCheck(tf, t.ix-tf.xofs-x, t.iy-tf.yofs-y)) {
1912       //writeln("pixel hit for ", GetClassName(t.Class));
1913       if (dg(t)) break;
1914     }
1915     */
1916     if (t.isRectCollisionFrame(frm, x, y)) {
1917       if (dg(t)) break;
1918     }
1919   }
1923 // ////////////////////////////////////////////////////////////////////////// //
1924 final void destroyTileAt (int x, int y) {
1925   if (x < 0 || y < 0 || x >= tilesWidth*16 || y >= tilesHeight*16) return;
1926   x /= 16;
1927   y /= 16;
1928   MapTile t = tiles[x, y];
1929   if (!t || !t.visible || t.spectral || t.invincible || !t.isInstanceAlive) return;
1930   t.instanceRemove();
1931   t.onDestroy();
1932   delete tiles[x, y];
1933   checkWater = true;
1937 private array!MapTile tmpTileList;
1939 private final bool cbCollectTilesWithMask (MapTile t) {
1940   if (!t || !t.visible || t.spectral /*|| t.invincible*/ || !t.isInstanceAlive) return false;
1941   if (!t.sprite || t.sprite.frames.length < 1) return false;
1942   tmpTileList[$] = t;
1943   return false;
1946 final void touchTilesWithMask (int x, int y, SpriteFrame frm, bool delegate (MapTile t) dg) {
1947   if (!frm || frm.bw < 1 || frm.bh < 1 || !dg) return;
1948   if (frm.isEmptyPixelMask) return;
1949   // collect tiles
1950   if (tmpTileList.length) tmpTileList.length -= tmpTileList.length; // don't realloc
1951   checkTilesInRect(x+frm.bx, y+frm.by, frm.bw, frm.bh, &cbCollectTilesWithMask);
1952   foreach (MapTile t; tmpTileList) {
1953     if (!t || !t.isInstanceAlive || !t.visible || t.spectral) continue;
1954     /*
1955     auto tf = t.sprite.frames[0];
1956     if (frm.pixelCheck(tf, t.ix-x, t.iy-y)) {
1957       if (dg(t)) break;
1958       //doCleanup = doCleanup || !t.isInstanceAlive;
1959       //writeln("dtwm at (", x, ",", y, "): dead at (", t.ix, ",", t.iy, ") : (", x/16, ",", y/16, ") : (", t.ix/16, ",", t.iy/16, ") <", GetClassName(t.Class), "> (name:", t.objName, "; type:", t.objType, ")");
1960     }
1961     */
1962     if (t.isRectCollisionFrame(frm, x, y)) {
1963       if (dg(t)) break;
1964     }
1965   }
1969 // ////////////////////////////////////////////////////////////////////////// //
1970 final bool cbCollisionAnySolid (MapTile t) { return t.solid; }
1971 final bool cbCollisionAnyMoveableSolid (MapTile t) { return t.solid && t.moveable; }
1972 final bool cbCollisionLiquid (MapTile t) { return (t.water || t.lava); }
1973 final bool cbCollisionAnySolidOrLiquid (MapTile t) { return (t.solid || t.water || t.lava); }
1974 final bool cbCollisionLadder (MapTile t) { return t.ladder; }
1975 final bool cbCollisionLadderTop (MapTile t) { return t.laddertop; }
1976 final bool cbCollisionAnyLadder (MapTile t) { return (t.ladder || t.laddertop); }
1977 final bool cbCollisionAnySolidIce (MapTile t) { return (t.solid && t.ice); }
1978 final bool cbCollisionWater (MapTile t) { return t.water; }
1979 final bool cbCollisionLava (MapTile t) { return t.lava; }
1980 final bool cbCollisionAnyTree (MapTile t) { return t.tree; }
1981 final bool cbCollisionPlatform (MapTile t) { return t.platform; }
1982 final bool cbCollisionLadderOrPlatform (MapTile t) { return (t.ladder || t.platform); }
1983 //final bool cbCollisionAnyHangeable (MapTile t) { return t.hangeable; } // no need to check for solids here, it is done in MapTile initializer
1984 final bool cbCollisionSpringTrap (MapTile t) { return t.springtrap; }
1985 final bool cbCollisionSpikes (MapTile t) { return t.spikes; }
1986 final bool cbCollisionExitTile (MapTile t) { return t.isExitActive(); }
1988 final bool cbCollisionForWhoa (MapTile t) { return (t.solid || t.laddertop); }
1990 //final bool cbCollisionSacAltar (MapTile t) { return (t.objName == 'oSacAltarLeft'); } // only left part, yeah
1991 final bool cbCollisionSacAltar (MapTile t) { return t.sacrificingAltar; }
1994 // ////////////////////////////////////////////////////////////////////////// //
1995 transient MapTile tempSolidTile;
1997 final MapTile checkTilesInRect (int x0, int y0, const int w, const int h, optional bool delegate (MapTile dg) dg, optional bool precise/*, optional bool dbgdump*/) {
1998   //!if (dbgdump) writeln("checkTilesInRect: (", x0, ",", y0, ")-(", x0+w-1, ",", y0+h-1, ") ; w=", w, "; h=", h);
1999   if (w < 1 || h < 1) return none;
2000   if (w == 1 && h == 1) return checkTileAtPoint(x0, y0, dg!optional);
2001   int x1 = x0+w-1, y1 = y0+h-1;
2002   if (x0 >= tilesWidth*16 || y0 >= tilesHeight*16 || x1 < 0 || y1 < 0) return none;
2003   if (!dg) {
2004     //!if (dbgdump) writeln("default checker set");
2005     dg = &cbCollisionAnySolid;
2006   }
2007   //!if (dbgdump) writeln("delegate: ", dg);
2008   int origx0 = x0, origy0 = y0;
2009   int tileSX = max(0, x0)/16;
2010   int tileSY = max(0, y0)/16;
2011   int tileEX = min(tilesWidth*16-1, x1)/16;
2012   int tileEY = min(tilesHeight*16-1, y1)/16;
2013   //!if (dbgdump) writeln("  tiles: (", tileSX, ",", tileSY, ")-(", tileEX, ",", tileEY, ")");
2014   //!!!auto grid = miscTileGrid;
2015   //!!!int tag = grid.nextTag();
2016   for (int ty = tileSY; ty <= tileEY; ++ty) {
2017     for (int tx = tileSX; tx <= tileEX; ++tx) {
2018       MapTile t = tiles[tx, ty];
2019       //!if (dbgdump && t && t.visible && t.isInstanceAlive && t.isRectCollision(origx0, origy0, w, h) ) writeln("   tile: ", GetClassName(t.Class), " : ", t.objName, " : ", t.objType, " : ", dg(t));
2020       if (t && !t.spectral && t.visible && t.isInstanceAlive && t.isRectCollision(origx0, origy0, w, h) && dg(t)) return t;
2021       // moveable tiles are in separate grid
2022       /+
2023       foreach (t; grid.inCellPix(tx*16, ty*16, tag, precise:precise)) {
2024         //if (t.isRectCollision(origx0, origy0, w, h) && dg(t)) return t;
2025         if (t.isInstanceAlive && !t.spectral && t.visible && t.isRectCollision(origx0, origy0, w, h) && dg(t)) return t;
2026       }
2027       +/
2028     }
2029   }
2031   // moveable tiles are in separate grid
2032   foreach (MapTile t; miscTileGrid.inRectPix(x0, y0, w, h, miscTileGrid.nextTag(), precise:precise)) {
2033     //if (t.isRectCollision(origx0, origy0, w, h) && dg(t)) return t;
2034     if (t.isInstanceAlive && !t.spectral && t.visible && t.isRectCollision(origx0, origy0, w, h) && dg(t)) return t;
2035   }
2037   // check walkable solid objects
2038   foreach (MapObject o; objGrid.inRectPix(x0, y0, w, h, objGrid.nextTag(), precise:precise)) {
2039     if (o && !o.spectral && o.visible && o.walkableSolid && o.isInstanceAlive && o.isRectCollision(origx0, origy0, w, h)) {
2040       if (!tempSolidTile) {
2041         tempSolidTile = SpawnObject(MapTile);
2042       } else if (!tempSolidTile.isInstanceAlive) {
2043         delete tempSolidTile;
2044         tempSolidTile = SpawnObject(MapTile);
2045       }
2046       tempSolidTile.solid = true;
2047       if (dg(tempSolidTile)) return tempSolidTile;
2048     }
2049   }
2051   return none;
2055 final MapTile checkTileAtPoint (int x0, int y0, optional bool delegate (MapTile dg) dg) {
2056   if (x0 < 0 || y0 < 0 || x0 >= tilesWidth*16 || y0 >= tilesHeight*16) return none;
2057   if (!dg) dg = &cbCollisionAnySolid;
2058   //if (!self) { writeln("WTF?!"); return none; }
2059   MapTile t = tiles[x0/16, y0/16];
2060   if (t && !t.spectral && t.visible && t.isInstanceAlive && t.isPointCollision(x0, y0) && dg(t)) return t;
2062   // moveable tiles are in separate grid
2063   foreach (t; miscTileGrid.inCellPix(x0, y0, miscTileGrid.nextTag(), precise:true)) {
2064     if (t.isInstanceAlive && !t.spectral && t.visible && dg(t)) return t;
2065   }
2067   // check walkable solid objects
2068   foreach (MapObject o; objGrid.inCellPix(x0, y0, objGrid.nextTag(), precise:true)) {
2069     if (o && !o.spectral && o.visible && o.walkableSolid && o.isInstanceAlive && o.isRectCollision(x0, y0, 1, 1)) {
2070       if (!tempSolidTile) {
2071         tempSolidTile = SpawnObject(MapTile);
2072       } else if (!tempSolidTile.isInstanceAlive) {
2073         delete tempSolidTile;
2074         tempSolidTile = SpawnObject(MapTile);
2075       }
2076       tempSolidTile.solid = true;
2077       if (dg(tempSolidTile)) return tempSolidTile;
2078     }
2079   }
2081   return none;
2085 //FIXME: optimize this with clipping first
2086 //TODO: moveable tiles
2088 final MapTile checkTilesAtLine (int ax0, int ay0, int ax1, int ay1, optional bool delegate (MapTile dg) dg) {
2089   // do it faster if we can
2091   // strict vertical check?
2092   if (ax0 == ax1 && ay0 <= ay1) return checkTilesInRect(ax0, ay0, 1, ay1-ay0+1, dg!optional);
2093   // strict horizontal check?
2094   if (ay0 == ay1 && ax0 <= ax1) return checkTilesInRect(ax0, ay0, ax1-ax0+1, 1, dg!optional);
2096   float x0 = float(ax0)/16.0, y0 = float(ay0)/16.0, x1 = float(ax1)/16.0, y1 = float(ay1)/16.0;
2098   // fix delegate
2099   if (!dg) dg = &cbCollisionAnySolid;
2101   // get starting and enging tile
2102   int tileSX = trunc(x0), tileSY = trunc(y0);
2103   int tileEX = trunc(x1), tileEY = trunc(y1);
2105   // first hit is always landed
2106   if (tileSX >= 0 && tileSY >= 0 && tileSX < TilesWidth && tileSY < TilesHeight) {
2107     MapTile t = tiles[tileSX, tileSY];
2108     if (t && t.visible && Geom.lineAABBIntersects(ax0, ay0, ax1, ay1, tileSX*16, tileSY*16, 16, 16) && dg(t)) return t;
2109   }
2111   // if starting and ending tile is the same, we don't need to do anything more
2112   if (tileSX == tileEX && tileSY == tileEY) return none;
2114   // calculate ray direction
2115   TVec dv = (vector(x1, y1)-vector(x0, y0)).normalise2d;
2117   // length of ray from one x or y-side to next x or y-side
2118   float deltaDistX = (fabs(dv.x) > 0.0001 ? fabs(1.0/dv.x) : 0.0);
2119   float deltaDistY = (fabs(dv.y) > 0.0001 ? fabs(1.0/dv.y) : 0.0);
2121   // calculate step and initial sideDists
2123   float sideDistX; // length of ray from current position to next x-side
2124   int stepX; // what direction to step in x (either +1 or -1)
2125   if (dv.x < 0) {
2126     stepX = -1;
2127     sideDistX = (x0-tileSX)*deltaDistX;
2128   } else {
2129     stepX = 1;
2130     sideDistX = (tileSX+1.0-x0)*deltaDistX;
2131   }
2133   float sideDistY; // length of ray from current position to next y-side
2134   int stepY; // what direction to step in y (either +1 or -1)
2135   if (dv.y < 0) {
2136     stepY = -1;
2137     sideDistY = (y0-tileSY)*deltaDistY;
2138   } else {
2139     stepY = 1;
2140     sideDistY = (tileSY+1.0-y0)*deltaDistY;
2141   }
2143   // perform DDA
2144   //int side; // was a NS or a EW wall hit?
2145   for (;;) {
2146     // jump to next map square, either in x-direction, or in y-direction
2147     if (sideDistX < sideDistY) {
2148       sideDistX += deltaDistX;
2149       tileSX += stepX;
2150       //side = 0;
2151     } else {
2152       sideDistY += deltaDistY;
2153       tileSY += stepY;
2154       //side = 1;
2155     }
2156     // check tile
2157     if (tileSX >= 0 && tileSY >= 0 && tileSX < TilesWidth && tileSY < TilesHeight) {
2158       MapTile t = tiles[tileSX, tileSY];
2159       if (t && t.visible && Geom.lineAABBIntersects(ax0, ay0, ax1, ay1, tileSX*16, tileSY*16, 16, 16) && dg(t)) return t;
2160     }
2161     // did we arrived at the destination?
2162     if (tileSX == tileEX && tileSY == tileEY) break;
2163   }
2165   return none;
2170 // ////////////////////////////////////////////////////////////////////////// //
2171 final MapTile isSolidAtPoint (int px, int py) { return checkTileAtPoint(px, py); }
2172 final MapTile isSolidInRect (int rx, int ry, int rw, int rh) { return checkTilesInRect(rx, ry, rw, rh); }
2173 final MapTile isLadderAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadder); }
2174 final MapTile isLadderOrPlatformAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadderOrPlatform); }
2175 final MapTile isLadderTopAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadderTop); }
2176 final MapTile isAnyLadderAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyLadder); }
2177 final MapTile isWaterAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionWater); }
2178 final MapTile isLavaAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLava); }
2179 final MapTile isLiquidAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLiquid); }
2180 final MapTile isIceAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnySolidIce); }
2181 final MapTile isTreeAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyTree); }
2182 //final MapTile isHangeableAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyHangeable); }
2185 // ////////////////////////////////////////////////////////////////////////// //
2186 // PlayerPawn has it's own movement code, so don't process it here
2187 // but process moveable solids here, yeah
2188 final void physStep () {
2189   // advance time
2190   time += 1;
2191   // we don't want the time to grow too large
2192   if (time > 100000000) time = 0;
2194   auto grid = miscTileGrid;
2196   // process gravity for moveable solids and burning for ropes
2197   int cid = grid.getFirstObject();
2198   while (cid) {
2199     MapTile t = grid.getObject(MapTile, cid);
2200     if (!t) {
2201       cid = grid.getNextObject(cid, removeThis:false);
2202       continue;
2203     }
2204     if (t.isInstanceAlive) {
2205       t.saveInterpData();
2206       t.processAlarms();
2207       if (t.isInstanceAlive) {
2208         grid.update(cid, markAsDead:false);
2209         t.thinkFrame();
2210         if (t.isInstanceAlive && !t.canLiveOutsideOfLevel && t.isOutsideOfLevel()) t.instanceRemove();
2211         grid.update(cid, markAsDead:false);
2212       }
2213     }
2214     if (t.isInstanceAlive) {
2215       cid = grid.getNextObject(cid, removeThis:false);
2216     } else {
2217       cid = grid.getNextObject(cid, removeThis:true);
2218       t.instanceRemove(); // just in case
2219       t.onDestroy();
2220       delete t;
2221       checkWater = true;
2222     }
2223   }
2227 // ////////////////////////////////////////////////////////////////////////// //
2228 final void setTileTypeAt (int tileX, int tileY, LevelGen::RType rt) {
2229   if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) roomType[tileX, tileY] = rt;
2232 final MapTile getTileAt (int tileX, int tileY) {
2233   return (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight ? tiles[tileX, tileY] : none);
2236 final bool isNamedTileAt (int tileX, int tileY, name atypename) {
2237   if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
2238     auto t = tiles[tileX, tileY];
2239     if (t && t.objName == atypename) return true;
2240   }
2241   return false;
2244 final void setTileAt (int tileX, int tileY, MapTile tile) {
2245   if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
2246     //FIXME
2247     if (tiles[tileX, tileY]) checkWater = true;
2248     delete tiles[tileX, tileY];
2249     tiles[tileX, tileY] = tile;
2250   }
2254 // ////////////////////////////////////////////////////////////////////////// //
2255 // return `true` from delegate to stop
2256 MapTile forEachSolidTile (bool delegate (int x, int y, MapTile t) dg) {
2257   if (!dg) return none;
2258   foreach (int y; 0..tilesHeight) {
2259     foreach (int x; 0..tilesWidth) {
2260       auto t = tiles[x, y];
2261       if (t && t.solid && t.visible && t.isInstanceAlive) {
2262         if (dg(x, y, t)) return t;
2263       }
2264     }
2265   }
2266   return none;
2270 // ////////////////////////////////////////////////////////////////////////// //
2271 // return `true` from delegate to stop
2272 MapTile forEachNormalTile (bool delegate (int x, int y, MapTile t) dg) {
2273   if (!dg) return none;
2274   foreach (int y; 0..tilesHeight) {
2275     foreach (int x; 0..tilesWidth) {
2276       auto t = tiles[x, y];
2277       if (t && t.visible && t.isInstanceAlive) {
2278         if (dg(x, y, t)) return t;
2279       }
2280     }
2281   }
2282   return none;
2286 // WARNING! don't destroy tiles here! (instanceRemove() is ok, tho)
2287 MapTile forEachTile (bool delegate (MapTile t) dg) {
2288   if (!dg) return none;
2289   foreach (int y; 0..tilesHeight) {
2290     foreach (int x; 0..tilesWidth) {
2291       auto t = tiles[x, y];
2292       if (t && t.visible && !t.spectral && t.isInstanceAlive) {
2293         if (dg(t)) return t;
2294       }
2295     }
2296   }
2297   foreach (MapObject o; miscTileGrid.allObjects()) {
2298     auto mt = MapTile(o);
2299     if (!mt) continue;
2300     if (mt.visible && !mt.spectral && mt.isInstanceAlive) {
2301       //writeln("special map tile: '", GetClassName(mt.Class), "'");
2302       if (dg(mt)) return mt;
2303     }
2304   }
2305   return none;
2309 // ////////////////////////////////////////////////////////////////////////// //
2310 final void fixWallTiles () {
2311   foreach (int y; 0..tilesHeight) {
2312     foreach (int x; 0..tilesWidth) {
2313       auto t = getTileAt(x, y);
2314       if (!t) continue;
2315       /*
2316       if (y == tilesHeight-2) {
2317         writeln("0: tx=", x, "; tile=", t.objName, "; ty=", y, ": (", t.iy/16, ")");
2318       } else if (y == tilesHeight-1) {
2319         writeln("1: tx=", x, "; tile=", t.objName, "; ty=", y, ": (", t.iy/16, ")");
2320       }
2321       */
2322       t.beautifyTile();
2323     }
2324   }
2325   foreach (MapTile t; miscTileGrid.allObjects()) {
2326     if (t.isInstanceAlive) t.beautifyTile();
2327   }
2331 // ////////////////////////////////////////////////////////////////////////// //
2332 final MapTile isCollisionAtPoint (int px, int py, optional bool delegate (MapTile dg) dg) {
2333   if (!dg) dg = &cbCollisionAnySolid;
2334   return checkTilesInRect(px, py, 1, 1, dg);
2338 // ////////////////////////////////////////////////////////////////////////// //
2339 string scrGetKaliGift (MapTile altar, optional name gift) {
2340   string res;
2342   // find other side of the altar
2343   int sx = player.ix, sy = player.iy;
2344   if (altar) {
2345     sx = altar.ix;
2346     sy = altar.iy;
2347     auto a2 = isCollisionAtPoint(altar.ix+18, altar.iy+8, &cbCollisionSacAltar);
2348     if (!a2) a2 = isCollisionAtPoint(altar.ix-8, altar.iy+8, &cbCollisionSacAltar);
2349     if (a2) { sx = a2.ix; sy = a2.iy; }
2350   }
2352        if (global.favor <= -8) res = "SHE SEEMS VERY ANGRY WITH YOU!";
2353   else if (global.favor < 0) res = "SHE SEEMS ANGRY WITH YOU.";
2354   else if (global.favor == 0) res = "SHE HAS FORGIVEN YOU!";
2355   else if (global.favor >= 32) {
2356     if (global.kaliGift >= 3 && global.favor >= 32+(global.kaliGift-2)*16) {
2357       res = "YOU FEEL INVIGORATED!";
2358       global.kaliGift += 1;
2359       global.plife += global.randOther(4, 8);
2360     } else if (global.kaliGift >= 3) {
2361       res = "SHE SEEMS ECSTATIC WITH YOU!";
2362     } else if (global.bombs < 80) {
2363       res = "YOUR SATCHEL FEELS VERY FULL NOW!";
2364       global.kaliGift = 3;
2365       global.bombs = 99;
2366     } else {
2367       res = "YOU FEEL INVIGORATED!";
2368       global.kaliGift += 1;
2369       global.plife += global.randOther(4, 8);
2370     }
2371   } else if (global.favor >= 16) {
2372     if (global.kaliGift >= 2) {
2373       res = "SHE SEEMS VERY HAPPY WITH YOU!";
2374     } else {
2375       res = "SHE BESTOWS A GIFT UPON YOU!";
2376       global.kaliGift = 2;
2377       // poofs
2378       auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2379       obj.xVel = -1;
2380       obj.yVel = 0;
2381       obj = MakeMapObject(sx, sy-8, 'oPoof');
2382       obj.xVel = 1;
2383       obj.yVel = 0;
2384       // a gift
2385       obj = none;
2386       if (gift) obj = MakeMapObject(sx, sy-8, gift);
2387       if (!obj) obj = MakeMapObject(sx, sy-8, 'oKapala');
2388     }
2389   } else if (global.favor >= 8) {
2390     if (global.kaliGift >= 1) {
2391       res = "SHE SEEMS HAPPY WITH YOU.";
2392     } else {
2393       res = "SHE BESTOWS A GIFT UPON YOU!";
2394       global.kaliGift = 1;
2395       //rAltar = instance_nearest(x, y, oSacAltarRight);
2396       //if (instance_exists(rAltar)) {
2397       // poofs
2398       auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2399       obj.xVel = -1;
2400       obj.yVel = 0;
2401       obj = MakeMapObject(sx, sy-8, 'oPoof');
2402       obj.xVel = 1;
2403       obj.yVel = 0;
2404       obj = none;
2405       if (gift) obj = MakeMapObject(sx, sy-8, gift);
2406       if (!obj) {
2407         auto n = global.randOther(1, 8);
2408         auto m = n;
2409         for (;;) {
2410           name aname = '';
2411                if (n == 1 && !global.hasCape && !global.hasJetpack) aname = 'oCapePickup';
2412           else if (n == 2 && !global.hasGloves) aname = 'oGloves';
2413           else if (n == 3 && !global.hasSpectacles) aname = 'oSpectacles';
2414           else if (n == 4 && !global.hasMitt) aname = 'oMitt';
2415           else if (n == 5 && !global.hasSpringShoes) aname = 'oSpringShoes';
2416           else if (n == 6 && !global.hasSpikeShoes) aname = 'oSpikeShoes';
2417           else if (n == 7 && !global.hasStickyBombs) aname = 'oPaste';
2418           else if (n == 8 && !global.hasCompass) aname = 'oCompass';
2419           if (aname) {
2420             obj = MakeMapObject(sx, sy-8, aname);
2421             if (obj) break;
2422           }
2423           ++n;
2424           if (n > 8) n = 1;
2425           if (n == m) {
2426             obj = MakeMapObject(sx, sy-8, (global.hasJetpack ? 'oBombBox' : 'oJetpack'));
2427             break;
2428           }
2429         }
2430       }
2431     }
2432   } else if (global.favor > 0) {
2433     res = "SHE SEEMS PLEASED WITH YOU.";
2434   }
2436   /*
2437   if (argument1) {
2438     global.message = "";
2439     res = "KALI DEVOURS YOU!"; // sacrifice is player
2440   }
2441   */
2443   return res;
2447 void performSacrifice (MapObject what, MapTile where) {
2448   if (!what || !what.isInstanceAlive) return;
2449   MakeMapObject(what.ix+8, what.iy+8, 'oFlame');
2450   if (where) where.playSound('sndSmallExplode'); else what.playSound('sndSmallExplode');
2451   if (!what.bloodless) what.scrCreateBlood(what.ix+8, what.iy+8, 3);
2453   string msg = "KALI ACCEPTS THE SACRIFICE!";
2455   auto idol = ItemGoldIdol(what);
2456   if (idol) {
2457     ++stats.totalSacrifices;
2458          if (global.favor <= -8) msg = "SHE SEEMS VERY ANGRY WITH YOU!";
2459     else if (global.favor < 0) msg = "SHE SEEMS ANGRY WITH YOU.";
2460     else if (global.favor >= 0) {
2461       // find other side of the altar
2462       int sx = player.ix, sy = player.iy;
2463       auto altar = where;
2464       if (altar) {
2465         sx = altar.ix;
2466         sy = altar.iy;
2467         auto a2 = isCollisionAtPoint(altar.ix+18, altar.iy+8, &cbCollisionSacAltar);
2468         if (!a2) a2 = isCollisionAtPoint(altar.ix-8, altar.iy+8, &cbCollisionSacAltar);
2469         if (a2) { sx = a2.ix; sy = a2.iy; }
2470       }
2471       // poofs
2472       auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2473       obj.xVel = -1;
2474       obj.yVel = 0;
2475       obj = MakeMapObject(sx, sy-8, 'oPoof');
2476       obj.xVel = 1;
2477       obj.yVel = 0;
2478       // a gift
2479       obj = MakeMapObject(sx, sy-16-8, 'oMonkeyGold');
2480     }
2481     osdMessage(msg, 6.66);
2482     scrShake(10);
2483     idol.instanceRemove();
2484     return;
2485   }
2487   if (global.favor <= -8) {
2488     msg = "KALI DEVOURS THE SACRIFICE!";
2489   } else if (global.favor < 0) {
2490     global.favor += (what.status == MapObject::STUNNED ? what.favor : round(what.favor/2.0)); //k8: added `round()`
2491     if (what.favor > 0) what.favor = 0;
2492   } else {
2493     global.favor += (what.status == MapObject::STUNNED ? what.favor : round(what.favor/2.0)); //k8: added `round()`
2494   }
2496   /*!!
2497        if (myAltar.gift1 != "" and global.kaliGift == 0) scrGetKaliGift(myAltar.gift1);
2498   else if (myAltar.gift2 != "" and global.kaliGift == 1) scrGetKaliGift(myAltar.gift2);
2499   else scrGetKaliGift("");
2500   */
2502   // sacrifice is player?
2503   if (what isa PlayerPawn) {
2504     ++stats.totalSelfSacrifices;
2505     msg = "KALI DEVOURS YOU!";
2506     player.visible = false;
2507     player.dead = true;
2508     player.status = MapObject::DEAD;
2509   } else {
2510     ++stats.totalSacrifices;
2511     auto msg2 = scrGetKaliGift(where);
2512     what.instanceRemove();
2513     if (msg2) msg = va("%s\n%s", msg, msg2);
2514   }
2516   osdMessage(msg, 6.66);
2518   //!if (isRealLevel()) global.totalSacrifices += 1;
2520   //!global.messageTimer = 200;
2521   //!global.shake = 10;
2522   scrShake(10);
2524   /*damsel
2525   instance_create(x, y, oFlame);
2526   playSound(global.sndSmallExplode);
2527   scrCreateBlood(x, y, 3);
2528   global.message = "KALI ACCEPTS YOUR SACRIFICE!";
2529   if (global.favor <= -8) {
2530     global.message = "KALI DEVOURS YOUR SACRIFICE!";
2531   } else if (global.favor < 0) {
2532     if (status == 98) global.favor += favor*1.5; else global.favor += favor;
2533     if (favor > 0) favor = 0;
2534   } else {
2535     if (status == 98) global.favor += favor*1.5; else global.favor += favor;
2536   }
2538        if (myAltar.gift1 != "" and global.kaliGift == 0) scrGetFavorMsg(myAltar.gift1);
2539   else if (myAltar.gift2 != "" and global.kaliGift == 1) scrGetFavorMsg(myAltar.gift2);
2540   else scrGetFavorMsg("");
2542   global.messageTimer = 200;
2543   global.shake = 10;
2544   instance_destroy();
2545   */
2549 // ////////////////////////////////////////////////////////////////////////// //
2550 final void addBackgroundGfxDetails () {
2551   // add background details
2552   //if (global.customLevel || global.parallax) return;
2553   foreach (; 0..20) {
2554     // bg = instance_create(16*randRoom(1, 42), 16*randRoom(1, 33), oCaveBG);
2555          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);
2556     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);
2557     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);
2558     else MakeMapBackTile('bgExtras', 32*global.randRoom(0, 1), 0, 32, 32, 16*global.randRoom(1, 42), 16*global.randRoom(1, 33), 10002);
2559   }
2563 // ////////////////////////////////////////////////////////////////////////// //
2564 private final void fixRealViewStart () {
2565   int scale = global.scale;
2566   realViewStart.x = clamp(realViewStart.x, viewMin.x*scale, viewMax.x*scale-viewWidth);
2567   realViewStart.y = clamp(realViewStart.y, viewMin.y*scale, viewMax.y*scale-viewHeight);
2571 final int cameraCurrX () { return realViewStart.x/global.scale; }
2572 final int cameraCurrY () { return realViewStart.y/global.scale; }
2575 private final void fixViewStart () {
2576   int scale = global.scale;
2577   viewStart.x = clamp(viewStart.x, viewMin.x*scale, viewMax.x*scale-viewWidth);
2578   viewStart.y = clamp(viewStart.y, viewMin.y*scale, viewMax.y*scale-viewHeight);
2582 final void centerViewAtPlayer () {
2583   if (viewWidth < 1 || viewHeight < 1 || !player) return;
2584   centerViewAt(player.xCenter, player.yCenter);
2588 final void centerViewAt (int x, int y) {
2589   if (viewWidth < 1 || viewHeight < 1) return;
2591   cameraSlideToSpeed.x = 0;
2592   cameraSlideToSpeed.y = 0;
2593   cameraSlideToPlayer = 0;
2595   int scale = global.scale;
2596   x *= scale;
2597   y *= scale;
2598   realViewStart.x = clamp(x-viewWidth/2, 0, tilesWidth*16*scale-viewWidth);
2599   realViewStart.y = clamp(y-viewHeight/2, 0, tilesHeight*16*scale-viewHeight);
2600   fixRealViewStart();
2602   viewStart.x = realViewStart.x;
2603   viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
2604   fixViewStart();
2608 const int ViewPortToleranceX = 16*1+8;
2609 const int ViewPortToleranceY = 16*1+8;
2611 final void fixCamera () {
2612   if (!player) return;
2613   if (viewWidth < 1 || viewHeight < 1) return;
2614   int scale = global.scale;
2615   auto alwaysCenterX = global.config.alwaysCenterPlayer;
2616   auto alwaysCenterY = alwaysCenterX;
2617   // calculate offset from viewport center (in game units), and fix viewport
2619   int camDestX = player.ix+8;
2620   int camDestY = player.iy+8;
2621   if (!cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y) != 0) {
2622     // slide camera to point
2623     if (cameraSlideToSpeed.x) camDestX = cameraSlideToCurr.x;
2624     if (cameraSlideToSpeed.y) camDestY = cameraSlideToCurr.y;
2625     int dx = cameraSlideToDest.x-camDestX;
2626     int dy = cameraSlideToDest.y-camDestY;
2627     //writeln("speed:(", cameraSlideToSpeed.x, ",", cameraSlideToSpeed.y, "); cur:(", camDestX, ",", camDestY, "); dest:(", cameraSlideToDest.x, ",", cameraSlideToDest.y, "); delta=(", dx, ",", dy, ")");
2628     if (dx && cameraSlideToSpeed.x != 0) {
2629       alwaysCenterX = true;
2630       if (abs(dx) <= abs(cameraSlideToSpeed.x)) {
2631         camDestX = cameraSlideToDest.x;
2632       } else {
2633         camDestX += sign(dx)*abs(cameraSlideToSpeed.x);
2634       }
2635     }
2636     if (dy && abs(cameraSlideToSpeed.y) != 0) {
2637       alwaysCenterY = true;
2638       if (abs(dy) <= cameraSlideToSpeed.y) {
2639         camDestY = cameraSlideToDest.y;
2640       } else {
2641         camDestY += sign(dy)*abs(cameraSlideToSpeed.y);
2642       }
2643     }
2644     //writeln("  new:(", camDestX, ",", camDestY, ")");
2645     if (cameraSlideToSpeed.x) cameraSlideToCurr.x = camDestX;
2646     if (cameraSlideToSpeed.y) cameraSlideToCurr.y = camDestY;
2647   }
2649   // horizontal
2650   if (cameraSlideToSpeed.x && !cameraSlideToPlayer) {
2651     realViewStart.x = clamp(camDestX*scale, 0, tilesWidth*16*scale-viewWidth);
2652   } else if (!player.cameraBlockX) {
2653     int x = camDestX*scale;
2654     int cx = realViewStart.x;
2655     if (alwaysCenterX) {
2656       cx = x-viewWidth/2;
2657     } else {
2658       int xofs = x-(cx+viewWidth/2);
2659            if (xofs < -ViewPortToleranceX*scale) cx += xofs+ViewPortToleranceX*scale;
2660       else if (xofs > ViewPortToleranceX*scale) cx += xofs-ViewPortToleranceX*scale;
2661     }
2662     // slide back to player?
2663     if (cameraSlideToPlayer && cameraSlideToSpeed.x) {
2664       int prevx = cameraSlideToCurr.x*scale;
2665       int dx = (cx-prevx)/scale;
2666       if (abs(dx) <= cameraSlideToSpeed.x) {
2667         writeln("BACKSLIDE X COMPLETE!");
2668         cameraSlideToSpeed.x = 0;
2669       } else {
2670         cx = prevx+(sign(dx)*cameraSlideToSpeed.x)*scale;
2671         cameraSlideToCurr.x += sign(dx)*cameraSlideToSpeed.x;
2672         if ((dx < 0 && cx <= 0) || (dx > 0 && cx >= tilesWidth*16*scale-viewWidth)) {
2673           writeln("BACKSLIDE X COMPLETE!");
2674           cameraSlideToSpeed.x = 0;
2675         }
2676       }
2677     }
2678     realViewStart.x = clamp(cx, 0, tilesWidth*16*scale-viewWidth);
2679   }
2681   // vertical
2682   if (cameraSlideToSpeed.y && !cameraSlideToPlayer) {
2683     realViewStart.y = clamp(camDestY*scale, 0, tilesHeight*16*scale-viewHeight);
2684   } else if (!player.cameraBlockY) {
2685     int y = camDestY*scale;
2686     int cy = realViewStart.y;
2687     if (alwaysCenterY) {
2688       cy = y-viewHeight/2;
2689     } else {
2690       int yofs = y-(cy+viewHeight/2);
2691            if (yofs < -ViewPortToleranceY*scale) cy += yofs+ViewPortToleranceY*scale;
2692       else if (yofs > ViewPortToleranceY*scale) cy += yofs-ViewPortToleranceY*scale;
2693     }
2694     // slide back to player?
2695     if (cameraSlideToPlayer && cameraSlideToSpeed.y) {
2696       int prevy = cameraSlideToCurr.y*scale;
2697       int dy = (cy-prevy)/scale;
2698       if (abs(dy) <= cameraSlideToSpeed.y) {
2699         writeln("BACKSLIDE Y COMPLETE!");
2700         cameraSlideToSpeed.y = 0;
2701       } else {
2702         cy = prevy+(sign(dy)*cameraSlideToSpeed.y)*scale;
2703         cameraSlideToCurr.y += sign(dy)*cameraSlideToSpeed.y;
2704         if ((dy < 0 && cy <= 0) || (dy > 0 && cy >= tilesHeight*16*scale-viewHeight)) {
2705           writeln("BACKSLIDE Y COMPLETE!");
2706           cameraSlideToSpeed.y = 0;
2707         }
2708       }
2709     }
2710     realViewStart.y = clamp(cy, 0, tilesHeight*16*scale-viewHeight);
2711   }
2713   if (cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y) == 0) cameraSlideToPlayer = 0;
2715   fixRealViewStart();
2716   //writeln("  new2:(", cameraCurrX, ",", cameraCurrY, ")");
2718   viewStart.x = realViewStart.x;
2719   viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
2720   fixViewStart();
2724 // ////////////////////////////////////////////////////////////////////////// //
2725 // x0 and y0 are non-scaled (and will be scaled)
2726 final void drawSpriteAt (name sprName, float frnumf, int x0, int y0, optional bool small) {
2727   if (!sprName) return;
2728   auto spr = sprStore[sprName];
2729   if (!spr || !spr.frames.length) return;
2730   int scale = global.scale;
2731   x0 *= scale;
2732   y0 *= scale;
2733   int frnum = max(0, trunc(frnumf))%spr.frames.length;
2734   auto sfr = spr.frames[frnum];
2735   int sx0 = x0-sfr.xofs*scale;
2736   int sy0 = y0-sfr.yofs*scale;
2737   if (small && scale > 1) {
2738     sfr.tex.blitExt(sx0, sy0, round(sx0+sfr.tex.width*(scale/2.0)), round(sy0+sfr.tex.height*(scale/2.0)), 0, 0);
2739   } else {
2740     sfr.tex.blitAt(sx0, sy0, scale);
2741   }
2745 // x0 and y0 are non-scaled (and will be scaled)
2746 final void drawTextAt (int x0, int y0, string text) {
2747   if (!text) return;
2748   int scale = global.scale;
2749   x0 *= scale;
2750   y0 *= scale;
2751   sprStore.renderText(x0, y0, text, scale);
2755 void renderCompass (float currFrameDelta) {
2756   if (!global.hasCompass) return;
2758   /*
2759   if (isRoom("rOlmec")) {
2760     global.exitX = 648;
2761     global.exitY = 552;
2762   } else if (isRoom("rOlmec2")) {
2763     global.exitX = 648;
2764     global.exitY = 424;
2765   }
2766   */
2768   bool hasMessage = osdHasMessage();
2769   foreach (MapTile et; allExits) {
2770     // original compass
2771     int exitX = et.ix, exitY = et.iy;
2772     int vx0 = viewStart.x/global.scale, vy0 = viewStart.y/global.scale;
2773     int vx1 = (viewStart.x+viewWidth)/global.scale;
2774     int vy1 = (viewStart.y+viewHeight)/global.scale;
2775     if (exitY > vy1-16) {
2776       if (exitX < vx0) {
2777         drawSpriteAt(hasMessage ? 'sCompassSmallLL' : 'sCompassLL', 0, 0, 224);
2778       } else if (exitX > vx1-16) {
2779         drawSpriteAt(hasMessage ? 'sCompassSmallLR' : 'sCompassLR', 0, 304, 224);
2780       } else {
2781         drawSpriteAt(hasMessage ? 'sCompassSmallDown' : 'sCompassDown', 0, exitX-vx0, 224);
2782       }
2783     } else if (exitX < vx0) {
2784       drawSpriteAt(hasMessage ? 'sCompassSmallLeft' : 'sCompassLeft', 0, 0, exitY-vy0);
2785     } else if (exitX > vx1-16) {
2786       drawSpriteAt(hasMessage ? 'sCompassSmallRight' : 'sCompassRight', 0, 304, exitY-vy0);
2787     }
2788   }
2792 final bool totalsNameCmpCB (ref GameStats::TotalItem a, ref GameStats::TotalItem b) {
2793   auto sa = string(a.objName);
2794   auto sb = string(b.objName);
2795   return (sa < sb);
2798 void renderTransitionInfo (float currFrameDelta) {
2799   //FIXME!
2800   /*
2801   GameStats.sortTotalsList(stats.kills, &totalsNameCmpCB);
2803   int maxLen = 0;
2804   foreach (int idx, ref auto k; stats.kills) {
2805     string s = string(k);
2806     maxLen = max(maxLen, s.length);
2807   }
2808   maxLen *= 8;
2810   sprStore.loadFont('sFontSmall');
2811   Video.color = 0xff_ff_00;
2812   foreach (int idx, ref auto k; stats.kills) {
2813     int deaths = 0;
2814     foreach (int xidx, ref auto d; stats.totalKills) {
2815       if (d.objName == k) { deaths = d.count; break; }
2816     }
2817     //drawTextAt(16, 4+idx*8, va("%s: %d (%d)", string(k.objName).toUpperCase, k.count, deaths));
2818     drawTextAt(16, 4+idx*8, string(k).toUpperCase);
2819     drawTextAt(16+maxLen, 4+idx*8, va(": %d (%d)", k.count, deaths));
2820   }
2821   */
2825 void renderGhostTimer (float currFrameDelta) {
2826   if (ghostTimeLeft <= 0) return;
2827   //ghostTimeLeft /= 30; // frames -> seconds
2829   int hgt = Video.screenHeight-64;
2830   if (hgt < 1) return;
2831   int rhgt = round(float(hgt)*(float(ghostTimeLeft)/30.0)/1000.0);
2832   //writeln("hgt=", hgt, "; rhgt=", rhgt, "; gtl=", ghostTimeLeft/30);
2833   if (rhgt > 0) {
2834     auto oclr = Video.color;
2835     Video.color = 0xcf_ff_7f_00;
2836     Video.fillRect(Video.screenWidth-20, 32, 16, hgt-rhgt);
2837     Video.color = 0x7f_ff_7f_00;
2838     Video.fillRect(Video.screenWidth-20, 32+(hgt-rhgt), 16, rhgt);
2839     Video.color = oclr;
2840   }
2844 void renderHUD (float currFrameDelta) {
2845   if (inWinCutscene) return;
2847   if (levelKind == LevelKind.Transition) { renderTransitionInfo(currFrameDelta); return; }
2849   int lifeX = 4; // 8
2850   int bombX = 56;
2851   int ropeX = 104;
2852   int ammoX = 152;
2853   int moneyX = 200;
2854   int hhup;
2855   bool scumSmallHud = global.config.scumSmallHud;
2856   if (!global.config.optSGAmmo) moneyX = ammoX;
2858   if (scumSmallHud) {
2859     sprStore.loadFont('sFontSmall');
2860     hhup = 6;
2861   } else {
2862     sprStore.loadFont('sFont');
2863     hhup = 0;
2864   }
2865   Video.color = 0xff_ff_ff;
2867   // hearts
2868   if (scumSmallHud) {
2869     if (global.plife == 1) {
2870       drawSpriteAt('sHeartSmallBlink', global.heartBlink, lifeX+2, 4-hhup);
2871       global.heartBlink += 0.1;
2872       if (global.heartBlink > 3) global.heartBlink = 0;
2873     } else {
2874       drawSpriteAt('sHeartSmall', -1, lifeX+2, 4-hhup);
2875       global.heartBlink = 0;
2876     }
2877   } else {
2878     if (global.plife == 1) {
2879       drawSpriteAt('sHeartBlink', global.heartBlink, lifeX, 8-hhup);
2880       global.heartBlink += 0.1;
2881       if (global.heartBlink > 3) global.heartBlink = 0;
2882     } else {
2883       drawSpriteAt('sHeart', -1, lifeX, 8-hhup);
2884       global.heartBlink = 0;
2885     }
2886   }
2888   int life = clamp(global.plife, 0, 99);
2889   //if (!scumHud && life > 99) life = 99;
2890   drawTextAt(lifeX+16, 8-hhup, va("%d", life));
2892   // bombs
2893   if (global.hasStickyBombs && global.stickyBombsActive) {
2894     if (scumSmallHud) drawSpriteAt('sStickyBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sStickyBombIcon', -1, bombX, 8-hhup);
2895   } else {
2896     if (scumSmallHud) drawSpriteAt('sBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sBombIcon', -1, bombX, 8-hhup);
2897   }
2898   int n = global.bombs;
2899   if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
2900   drawTextAt(bombX+16, 8-hhup, va("%d", n));
2902   // ropes
2903   if (scumSmallHud) drawSpriteAt('sRopeIconSmall', -1, ropeX+2, 4-hhup); else drawSpriteAt('sRopeIcon', -1, ropeX, 8-hhup);
2904   n = global.rope;
2905   if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
2906   drawTextAt(ropeX+16, 8-hhup, va("%d", n));
2908   // shotgun shells
2909   if (global.config.optSGAmmo) {
2910     if (scumSmallHud) drawSpriteAt('sShellsIcon', -1, ammoX+6, 8-hhup); else drawSpriteAt('sShellsIcon', -1, ammoX+7, 12-hhup);
2911     n = global.sgammo;
2912     if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
2913     drawTextAt(ammoX+16, 8-hhup, va("%d", n));
2914   }
2916   // money
2917   if (scumSmallHud) drawSpriteAt('sDollarSignSmall', -1, moneyX+2, 3-hhup); else drawSpriteAt('sDollarSign', -1, moneyX, 8-hhup);
2918   drawTextAt(moneyX+16, 8-hhup, va("%d", stats.money-xmoney));
2920   int ity = (scumSmallHud ? 18-hhup : 24-hhup);
2922   n = 8; //28;
2923   if (global.hasUdjatEye) {
2924     if (global.udjatBlink) drawSpriteAt('sUdjatEyeIcon2', -1, n, ity); else drawSpriteAt('sUdjatEyeIcon', -1, n, ity);
2925     n += 20;
2926   }
2927   if (global.hasAnkh) { drawSpriteAt('sAnkhIcon', -1, n, ity); n += 20; }
2928   if (global.hasCrown) { drawSpriteAt('sCrownIcon', -1, n, ity); n += 20; }
2929   if (global.hasKapala) {
2930          if (global.bloodLevel == 0) drawSpriteAt('sKapalaIcon', 0, n, ity);
2931     else if (global.bloodLevel <= 2) drawSpriteAt('sKapalaIcon', 1, n, ity);
2932     else if (global.bloodLevel <= 4) drawSpriteAt('sKapalaIcon', 2, n, ity);
2933     else if (global.bloodLevel <= 6) drawSpriteAt('sKapalaIcon', 3, n, ity);
2934     else if (global.bloodLevel <= 8) drawSpriteAt('sKapalaIcon', 4, n, ity);
2935     n += 20;
2936   }
2937   if (global.hasSpectacles) { drawSpriteAt('sSpectaclesIcon', -1, n, ity); n += 20; }
2938   if (global.hasGloves) { drawSpriteAt('sGlovesIcon', -1, n, ity); n += 20; }
2939   if (global.hasMitt) { drawSpriteAt('sMittIcon', -1, n, ity); n += 20; }
2940   if (global.hasSpringShoes) { drawSpriteAt('sSpringShoesIcon', -1, n, ity); n += 20; }
2941   if (global.hasJordans) { drawSpriteAt('sJordansIcon', -1, n, ity); n += 20; }
2942   if (global.hasSpikeShoes) { drawSpriteAt('sSpikeShoesIcon', -1, n, ity); n += 20; }
2943   if (global.hasCape) { drawSpriteAt('sCapeIcon', -1, n, ity); n += 20; }
2944   if (global.hasJetpack) { drawSpriteAt('sJetpackIcon', -1, n, ity); n += 20; }
2945   if (global.hasStickyBombs) { drawSpriteAt('sPasteIcon', -1, n, ity); n += 20; }
2946   if (global.hasCompass) { drawSpriteAt('sCompassIcon', -1, n, ity); n += 20; }
2947   if (global.hasParachute) { drawSpriteAt('sParachuteIcon', -1, n, ity); n += 20; }
2949   if (player.holdItem && player.holdItem.objName == 'Bow' && global.arrows > 0) {
2950     int m = 1;
2951     float malpha = 1;
2952     while (m <= global.arrows && m <= 20 && malpha > 0) {
2953       Video.color = trunc(malpha*255)<<24|0xff_ff_ff;
2954       drawSpriteAt('sArrowIcon', -1, n, ity);
2955       n += 4;
2956       if (m > 10) malpha -= 0.1; //and global.arrows > 20 malpha -= 0.1
2957       m += 1;
2958     }
2959     Video.color = 0xff_ff_ff;
2960   }
2962   if (xmoney > 0) {
2963     sprStore.loadFont('sFontSmall');
2964     Video.color = 0xff_ff_00;
2965     if (scumSmallHud) drawTextAt(moneyX+8, 8+8-hhup, va("+%d", xmoney));
2966     else drawTextAt(moneyX, 8+16-hhup, va("+%d", xmoney));
2967   }
2969   if (global.config.ghostShowTime) renderGhostTimer(currFrameDelta);
2973 // ////////////////////////////////////////////////////////////////////////// //
2974 private transient array!MapEntity renderVisibleCids;
2975 private transient array!MapTile renderMidTiles, renderFrontTiles; // normal, with fg
2977 final bool renderSortByDepth (MapEntity oa, MapEntity ob) {
2978   //MapObject oa = MapObject(a);
2979   //MapObject ob = MapObject(b);
2980   auto da = oa.depth, db = ob.depth;
2981   if (da == db) return (oa.objId < ob.objId);
2982   return (da < db);
2986 const int RenderEdgePix = 32;
2988 void renderWithOfs (int xofs, int yofs, float currFrameDelta) {
2989   int scale = global.scale;
2990   int tsz = 16*scale;
2992   Video.color = 0xff_ff_ff;
2994   // render cave background
2995   if (levBGImg) {
2996     int bgw = levBGImg.tex.width*scale;
2997     int bgh = levBGImg.tex.height*scale;
2998     int bgTW = (tilesWidth*tsz+bgw-1)/bgw;
2999     int bgTH = (tilesHeight*tsz+bgh-1)/bgh;
3000     int bgX0 = max(0, xofs/bgw);
3001     int bgY0 = max(0, yofs/bgh);
3002     int bgX1 = min(bgTW, (xofs+viewWidth+bgw-1)/bgw);
3003     int bgY1 = min(bgTH, (yofs+viewHeight+bgh-1)/bgh);
3004     foreach (int ty; bgY0..bgY1) {
3005       foreach (int tx; bgX0..bgX1) {
3006         int x0 = tx*bgw-xofs;
3007         int y0 = ty*bgh-yofs;
3008         levBGImg.tex.blitAt(x0, y0, scale);
3009       }
3010     }
3011   }
3013   // render background tiles
3014   for (MapBackTile bt = backtiles; bt; bt = bt.next) {
3015     bt.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3016   }
3018   // collect visible objects
3019   renderVisibleCids.length -= renderVisibleCids.length;
3020   foreach (MapTile mt; miscTileGrid.inRectPix(xofs/scale-RenderEdgePix, yofs/scale-RenderEdgePix, (viewWidth+scale-1)/scale+RenderEdgePix*2, (viewHeight+scale-1)/scale+RenderEdgePix*2, tag:miscTileGrid.nextTag(), precise:false)) {
3021     if (!mt.visible || !mt.isInstanceAlive) continue;
3022     //Video.color = (mt.moveable ? 0xff_7f_00 : 0xff_ff_ff);
3023     //!mt.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3024     renderVisibleCids[$] = mt;
3025   }
3026   // render objects (and player)
3027   if (player && player.visible) renderVisibleCids[$] = player; // include player in render list
3028   auto ogrid = objGrid;
3029   foreach (MapObject o; ogrid.inRectPix(xofs/scale-RenderEdgePix, yofs/scale-RenderEdgePix, (viewWidth+scale-1)/scale+RenderEdgePix*2, (viewHeight+scale-1)/scale+RenderEdgePix*2, tag:ogrid.nextTag(), precise:false)) {
3030     if (o.visible && o.isInstanceAlive) renderVisibleCids[$] = o;
3031   }
3033   // collect stationary tiles
3034   int tileX0 = max(0, xofs/tsz);
3035   int tileY0 = max(0, yofs/tsz);
3036   int tileX1 = min(tilesWidth, (xofs+viewWidth+tsz-1)/tsz);
3037   int tileY1 = min(tilesHeight, (yofs+viewHeight+tsz-1)/tsz);
3039   // render backs; collect tile arrays
3040   renderMidTiles.length -= renderMidTiles.length; // don't realloc
3041   renderFrontTiles.length -= renderFrontTiles.length; // don't realloc
3043   foreach (int ty; tileY0..tileY1) {
3044     foreach (int tx; tileX0..tileX1) {
3045       auto tile = getTileAt(tx, ty);
3046       if (tile && tile.visible && tile.isInstanceAlive) {
3047         renderMidTiles[$] = tile;
3048         if (tile.bgfront) renderFrontTiles[$] = tile;
3049         if (tile.bgback) tile.drawWithOfsBack(xofs, yofs, scale, currFrameDelta);
3050       }
3051     }
3052   }
3054   // render "mid" (i.e. normal) tiles
3055   foreach (MapTile tile; renderMidTiles) {
3056     //tile.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3057     renderVisibleCids[$] = tile;
3058   }
3060   EntityGrid.sortEntList(renderVisibleCids, &renderSortByDepth);
3063   auto depth4Start = 0;
3064   foreach (auto xidx, MapEntity o; renderVisibleCids) {
3065     if (o.depth >= 4) {
3066       depth4Start = xidx;
3067       break;
3068     }
3069   }
3071   foreach (auto idx; depth4Start..renderVisibleCids.length; reverse) {
3072     MapEntity o = renderVisibleCids[idx];
3073     //if (idx && renderSortByDepth(o, renderVisibleCids[idx-1])) writeln("VIOLATION!");
3074     o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3075   }
3077   // render front tile parts (depth 3.5)
3078   foreach (MapTile tile; renderFrontTiles) tile.drawWithOfsFront(xofs, yofs, scale, currFrameDelta);
3080   // render items with depth 3 and less
3081   foreach (auto idx; 0..depth4Start; reverse) {
3082     MapEntity o = renderVisibleCids[idx];
3083     o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3084   }
3086   renderVisibleCids.length -= renderVisibleCids.length;
3088   // render player post-effects, so cape or jetpack will be correctly rendered on level exit, for example
3089   player.lastDrawWithOfs(xofs, yofs, scale, currFrameDelta);
3091   if (global.config.drawHUD) renderHUD(currFrameDelta);
3092   renderCompass(currFrameDelta);
3094   float osdTimeLeft, osdTimeStart;
3095   string msg = osdGetMessage(out osdTimeLeft, out osdTimeStart);
3096   if (msg) {
3097     auto ct = GetTickCount();
3098     int msgScale = 3;
3099     sprStore.loadFont('sFontSmall');
3100     auto msgHeight = sprStore.getMultilineTextHeight(msg, msgScale);
3101     int x = Video.screenWidth/2;
3102     int y = Video.screenHeight-64-msgHeight;
3103     auto oldColor = Video.color;
3104     Video.color = 0xff_ff_00;
3105     if (osdTimeLeft < 0.5) {
3106       int alpha = 255-clamp(int(510*osdTimeLeft), 0, 255);
3107       Video.color = Video.color|(alpha<<24);
3108     } else if (ct-osdTimeStart < 0.5) {
3109       osdTimeStart = ct-osdTimeStart;
3110       int alpha = 255-clamp(int(510*osdTimeStart), 0, 255);
3111       Video.color = Video.color|(alpha<<24);
3112     }
3113     sprStore.renderMultilineTextCentered(x, y, msg, msgScale);
3114     Video.color = oldColor;
3115   }
3117   if (inWinCutscene) renderWinCutsceneOverlay();
3118   Video.color = 0xff_ff_ff;
3122 // ////////////////////////////////////////////////////////////////////////// //
3123 final class!MapObject findGameObjectClassByName (name aname) {
3124   if (!aname) return none; // just in case
3125   auto co = FindClassByGameObjName(aname);
3126   if (!co) {
3127     writeln("***UNKNOWN GAME OBJECT: '", aname, "'");
3128     return none;
3129   }
3130   co = GetClassReplacement(co);
3131   if (!co) FatalError("findGameObjectClassByName: WTF?!");
3132   if (!class!MapObject(co)) FatalError("findGameTileClassByName: object '%n' is not a map object, it is a `%n`", aname, GetClassName(co));
3133   return class!MapObject(co);
3137 final class!MapTile findGameTileClassByName (name aname) {
3138   if (!aname) return none; // just in case
3139   auto co = FindClassByGameObjName(aname);
3140   if (!co) return MapTile; // unknown names will be routed directly to tile object
3141   co = GetClassReplacement(co);
3142   if (!co) FatalError("findGameTileClassByName: WTF?!");
3143   if (!class!MapTile(co)) FatalError("findGameTileClassByName: object '%n' is not a tile, it is a `%n`", aname, GetClassName(co));
3144   return class!MapTile(co);
3148 final MapObject findAnyObjectOfType (name aname) {
3149   if (!aname) return none;
3150   auto cls = FindClassByGameObjName(aname);
3151   if (!cls) return none;
3152   for (auto cid = objGrid.getFirstObject(); cid; cid = objGrid.getNextObject(cid)) {
3153     MapObject obj = objGrid.getObject(MapObject, cid);
3154     if (!obj || obj.spectral || !obj.isInstanceAlive) continue;
3155     if (obj isa cls) return obj;
3156   }
3157   return none;
3161 // ////////////////////////////////////////////////////////////////////////// //
3162 final MapTile CreateMapTile (int xpos, int ypos, name aname) {
3163   if (!aname) FatalError("cannot create typeless tile");
3164   //MapTile tile = SpawnObject(aname == 'oRope' ? MapTileRope : MapTile);
3165   auto tclass = findGameTileClassByName(aname);
3166   if (!tclass) return none;
3167   MapTile tile = SpawnObject(tclass);
3168   tile.global = global;
3169   tile.level = self;
3170   tile.objName = aname;
3171   tile.objType = aname; // just in case
3172   tile.fltx = xpos;
3173   tile.flty = ypos;
3174   if (!tile.initialize()) FatalError("cannot create tile '%n'", aname);
3175   return tile;
3179 final bool isRopePlacedAt (int x, int y) {
3180   int[8] covered;
3181   foreach (ref auto v; covered) v = false;
3182   foreach (MapTile t; miscTileGrid.inRectPix(x, y-8, 1, 17, precise:false)) {
3183     if (!cbIsRopeTile(t)) continue;
3184     if (t.ix != x) continue;
3185     if (t.iy == y) return true;
3186     foreach (int ty; t.iy..t.iy+8) {
3187       int d = ty-y;
3188       if (d >= 0 && d < covered.length) covered[d] = true;
3189     }
3190   }
3191   // check if the whole rope height is completely covered with ropes
3192   foreach (auto v; covered) if (!v) return false;
3193   return true;
3197 // won't call `onDestroy()`
3198 final void RemoveMapTile (int tileX, int tileY) {
3199   if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
3200     if (tiles[tileX, tileY]) checkWater = true;
3201     delete tiles[tileX, tileY];
3202     tiles[tileX, tileY] = none;
3203   }
3207 final MapTile MakeMapTile (int mapx, int mapy, name aname/*, optional name sprname*/) {
3208   //if (mapy >= tilesHeight) { writeln("OOM: ", aname, "; ty=", mapy, "; hgt=", tilesHeight); }
3209   if (mapx < 0 || mapx >= tilesWidth || mapy < 0 || mapy >= tilesHeight) return none;
3211   // if we already have rope tile there, there is no reason to add another one
3212   if (aname == 'oRope') {
3213     if (isRopePlacedAt(mapx*16, mapy*16)) {
3214       //writeln("dupe rope (0)!");
3215       return none;
3216     }
3217   }
3219   auto tile = CreateMapTile(mapx*16, mapy*16, aname);
3220   if (tile.moveable || tile.toSpecialGrid) {
3221     // moveable tiles goes to the separate list
3222     miscTileGrid.insert(tile);
3223   } else {
3224     setTileAt(mapx, mapy, tile);
3225   }
3227   switch (aname) {
3228     case 'oEntrance': registerEnter(tile); break;
3229     case 'oExit': registerExit(tile); break;
3230   }
3232   return tile;
3236 final void MarkTileAsWet (int tileX, int tileY) {
3237   auto t = getTileAt(tileX, tileY);
3238   if (t) t.wet = true;
3242 final MapTile MakeMapTileAtPix (int xpix, int ypix, name aname/*, optional name sprname*/) {
3243   if (xpix%16 == 0 && ypix%16 == 0) return MakeMapTile(xpix/16, ypix/16, aname);
3244   //if (mapx < 0 || mapx >= TilesWidth || mapy < 0 || mapy >= TilesHeight) return none;
3246   // if we already have rope tile there, there is no reason to add another one
3247   if (aname == 'oRope') {
3248     if (isRopePlacedAt(xpix, ypix)) {
3249       //writeln("dupe rope (0)!");
3250       return none;
3251     }
3252   }
3254   auto tile = CreateMapTile(xpix, ypix, aname);
3255   // non-aligned tiles goes to the special grid
3256   miscTileGrid.insert(tile);
3258   switch (aname) {
3259     case 'oEntrance': registerEnter(tile); break;
3260     case 'oExit': registerExit(tile); break;
3261   }
3263   return tile;
3267 final MapTile MakeMapRopeTileAt (int x0, int y0) {
3268   // if we already have rope tile there, there is no reason to add another one
3269   if (isRopePlacedAt(x0, y0)) {
3270     //writeln("dupe rope (1)!");
3271     return none;
3272   }
3274   auto tile = CreateMapTile(x0, y0, 'oRope');
3275   miscTileGrid.insert(tile);
3277   return tile;
3281 // ////////////////////////////////////////////////////////////////////////// //
3282 final MapBackTile CreateBgTile (name sprName, optional int atx0, optional int aty0, optional int aw, optional int ah) {
3283   BackTileImage img = bgtileStore[sprName];
3284   auto res = SpawnObject(MapBackTile);
3285   res.global = global;
3286   res.level = self;
3287   res.bgt = img;
3288   res.bgtName = sprName;
3289   if (specified_atx0) res.tx0 = atx0;
3290   if (specified_aty0) res.ty0 = aty0;
3291   if (specified_aw) res.w = aw;
3292   if (specified_ah) res.h = ah;
3293   if (!res.initialize()) FatalError("cannot create background tile with image '%n'", sprName);
3294   return res;
3298 // ////////////////////////////////////////////////////////////////////////// //
3300 background The background asset from which the new tile will be extracted.
3301 left The x coordinate of the left of the new tile, relative to the background asset's top left corner.
3302 top The y coordinate of the top of the new tile, relative to the background assets top left corner.
3303 width The width of the tile.
3304 height The height of the tile.
3305 x The x position in the room to place the tile.
3306 y The y position in the room to place the tile.
3307 depth The depth at which to place the tile.
3309 final void MakeMapBackTile (name bgname, int left, int top, int width, int height, int x, int y, int depth) {
3310   if (width < 1 || height < 1 || !bgname) return;
3311   auto bgt = bgtileStore[bgname];
3312   if (!bgt) FatalError("cannot load background '%n'", bgname);
3313   MapBackTile bt = SpawnObject(MapBackTile);
3314   bt.global = global;
3315   bt.level = self;
3316   bt.objName = bgname;
3317   bt.bgt = bgt;
3318   bt.bgtName = bgname;
3319   bt.fltx = x;
3320   bt.flty = y;
3321   bt.tx0 = left;
3322   bt.ty0 = top;
3323   bt.w = width;
3324   bt.h = height;
3325   bt.depth = depth;
3326   // find a place for it
3327   if (!backtiles) {
3328     backtiles = bt;
3329     return;
3330   }
3331   // back tiles with the highest depth should come first
3332   MapBackTile ct = backtiles, cprev = none;
3333   while (ct && depth <= ct.depth) { cprev = ct; ct = ct.next; }
3334   // insert before ct
3335   if (cprev) {
3336     bt.next = cprev.next;
3337     cprev.next = bt;
3338   } else {
3339     bt.next = backtiles;
3340     backtiles = bt;
3341   }
3345 // ////////////////////////////////////////////////////////////////////////// //
3346 final MapObject SpawnMapObjectWithClass (class!MapObject oclass) {
3347   if (!oclass) return none;
3349   MapObject obj = SpawnObject(oclass);
3350   if (!obj) FatalError("cannot create map object '%n'", GetClassName(oclass));
3352   //if (aname == 'oShells' || aname == 'oShellSingle' || aname == 'oRopePile') writeln("*** CREATED '", aname, "'");
3354   obj.global = global;
3355   obj.level = self;
3357   return obj;
3361 final MapObject SpawnMapObject (name aname) {
3362   if (!aname) return none;
3363   return SpawnMapObjectWithClass(findGameObjectClassByName(aname));
3367 final MapObject PutSpawnedMapObject (int x, int y, MapObject obj) {
3368   if (!obj /*|| obj.global || obj.level*/) return none; // oops
3370   obj.fltx = x;
3371   obj.flty = y;
3372   if (!obj.initialize()) { delete obj; return none; } // not fatal
3374   insertObject(obj);
3376   return obj;
3380 final MapObject MakeMapObject (int x, int y, name aname) {
3381   MapObject obj = SpawnMapObject(aname);
3382   obj = PutSpawnedMapObject(x, y, obj);
3383   return obj;
3387 // ////////////////////////////////////////////////////////////////////////// //
3388 int winCutSceneTimer = -1;
3389 int winVolcanoTimer = -1;
3390 int winCutScenePhase = 0;
3391 int winSceneDrawStatus = 0;
3392 int winMoneyCount = 0;
3393 int winTime;
3394 bool winFadeOut = false;
3395 int winFadeLevel = 0;
3396 int winCutsceneSkip = 0; // 1: waiting for pay release; 2: pay released, do skip
3399 void startWinCutscene () {
3400   winCutsceneSkip = 0;
3401   isKeyPressed(GameConfig::Key.Pay);
3402   isKeyReleased(GameConfig::Key.Pay);
3404   winTime = time;
3406   auto olddel = ImmediateDelete;
3407   ImmediateDelete = false;
3408   clearTiles();
3409   clearObjects();
3411   createEnd1Room();
3412   fixWallTiles();
3413   addBackgroundGfxDetails();
3415   levBGImgName = 'bgCave';
3416   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
3418   blockWaterChecking = true;
3419   fixLiquidTop();
3420   cleanDeadTiles();
3422   collectLavaTiles();
3424   ImmediateDelete = olddel;
3425   CollectGarbage(true); // destroy delayed objects too
3427   if (dumpGridStats) {
3428     miscTileGrid.dumpStats();
3429     objGrid.dumpStats();
3430   }
3432   playerExited = false; // just in case
3434   osdClear();
3436   setupGhostTime();
3437   global.stopMusic();
3439   inWinCutscene = 1;
3440   winCutSceneTimer = -1;
3441   winCutScenePhase = 0;
3443   /+
3444   if (global.config.gameMode != GameConfig::GameMode.Vanilla) {
3445     if (global.config.bizarre) {
3446       global.yasmScore = 1;
3447       global.config.bizarrePlusTitle = true;
3448     }
3450     array!MapTile toReplace;
3451     forEachTile(delegate bool (MapTile t) {
3452       if (t.objType == 'oGTemple' ||
3453           t.objType == 'oIce' ||
3454           t.objType == 'oDark' ||
3455           t.objType == 'oBrick' ||
3456           t.objType == 'oLush')
3457       {
3458         toReplace[$] = t;
3459       }
3460       return false;
3461     });
3463     foreach (MapTile t; miscTileGrid.allObjects()) {
3464       if (t.objType == 'oGTemple' ||
3465           t.objType == 'oIce' ||
3466           t.objType == 'oDark' ||
3467           t.objType == 'oBrick' ||
3468           t.objType == 'oLush')
3469       {
3470         toReplace[$] = t;
3471       }
3472     }
3474     foreach (MapTile t; toReplace) {
3475       if (t.iy < 192) {
3476         t.cleanDeath = true;
3477             if (rand(1,120) == 1) instance_change(oGTemple, false);
3478         else if (rand(1,100) == 1) instance_change(oIce, false);
3479         else if (rand(1,90) == 1) instance_change(oDark, false);
3480         else if (rand(1,80) == 1) instance_change(oBrick, false);
3481         else if (rand(1,70) == 1) instance_change(oLush, false);
3482           }
3483       }
3484       with (oBrick)
3485       {
3486           if (y &lt; 192)
3487           {
3488               cleanDeath = true;
3489               if (rand(1,5) == 1) instance_change(oLush, false);
3490           }
3491       }
3492   }
3493   +/
3494   //!instance_create(0, 0, oBricks);
3496   //shakeToggle = false;
3497   //oPDummy.status = 2;
3499   //timer = 0;
3501   /+
3502   if (global.kaliPunish &gt;= 2) {
3503       instance_create(oPDummy.x, oPDummy.y+2, oBall2);
3504       obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
3505       obj.linkVal = 1;
3506       obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
3507       obj.linkVal = 2;
3508       obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
3509       obj.linkVal = 3;
3510       obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
3511       obj.linkVal = 4;
3512   }
3513   +/
3514   //player.
3518 void startWinCutsceneVolcano () {
3519   auto olddel = ImmediateDelete;
3520   ImmediateDelete = false;
3521   clearTiles();
3522   clearObjects();
3524   levBGImgName = '';
3525   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
3527   blockWaterChecking = true;
3529   ImmediateDelete = olddel;
3530   CollectGarbage(true); // destroy delayed objects too
3532   spawnPlayerAt(2*16+8, 11*16+8);
3533   player.dir = MapEntity::Dir.Right;
3535   playerExited = false; // just in case
3537   osdClear();
3539   setupGhostTime();
3540   global.stopMusic();
3542   inWinCutscene = 2;
3543   winCutSceneTimer = -1;
3544   winCutScenePhase = 0;
3546   MakeMapTile(0, 0, 'oEnd2BG');
3547   realViewStart.x = 0;
3548   realViewStart.y = 0;
3549   viewStart.x = 0;
3550   viewStart.y = 0;
3552   viewMin.x = 0;
3553   viewMin.y = 0;
3554   viewMax.x = 320;
3555   viewMax.y = 240;
3557   player.dead = false;
3558   player.active = true;
3559   player.visible = false;
3560   player.fltx = 320/2;
3561   player.flty = 0;
3565 void startWinCutsceneWinFall () {
3566   auto olddel = ImmediateDelete;
3567   ImmediateDelete = false;
3568   clearTiles();
3569   clearObjects();
3571   createEnd3Room();
3572   setMenuTilesVisible(false);
3573   //fixWallTiles();
3574   //addBackgroundGfxDetails();
3576   levBGImgName = '';
3577   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
3579   blockWaterChecking = true;
3580   fixLiquidTop();
3581   cleanDeadTiles();
3583   collectLavaTiles();
3585   ImmediateDelete = olddel;
3586   CollectGarbage(true); // destroy delayed objects too
3588   if (dumpGridStats) {
3589     miscTileGrid.dumpStats();
3590     objGrid.dumpStats();
3591   }
3593   playerExited = false; // just in case
3595   osdClear();
3597   setupGhostTime();
3598   global.stopMusic();
3600   inWinCutscene = 3;
3601   winCutSceneTimer = -1;
3602   winCutScenePhase = 0;
3604   player.dead = false;
3605   player.active = true;
3606   player.visible = false;
3607   player.fltx = 320/2;
3608   player.flty = 0;
3610   winSceneDrawStatus = 0;
3611   winMoneyCount = 0;
3613   winFadeOut = false;
3614   winFadeLevel = 0;
3618 void setGameOver () {
3619   if (inWinCutscene) player.visible = false;
3620   player.dead = true;
3621   if (inWinCutscene > 0) {
3622     winFadeOut = true;
3623     winFadeLevel = 255;
3624     winSceneDrawStatus = 8;
3625   }
3629 MapTile findEndPlatTile () {
3630   return forEachTile(delegate bool (MapTile t) { return (t isa MapTileEndPlat); });
3634 MapObject findBigTreasure () {
3635   return forEachObject(delegate bool (MapObject o) { return (o isa MapObjectBigTreasure); });
3639 void setMenuTilesVisible (bool vis) {
3640   if (vis) {
3641     forEachTile(delegate bool (MapTile t) {
3642       if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
3643         t.invisible = false;
3644       }
3645       return false;
3646     });
3647   } else {
3648     forEachTile(delegate bool (MapTile t) {
3649       if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
3650         t.invisible = true;
3651       }
3652       return false;
3653     });
3654   }
3658 void winCutscenePlayerControl (PlayerPawn plr) {
3659   auto payPress = isKeyPressed(GameConfig::Key.Pay);
3660   auto payRelease = isKeyReleased(GameConfig::Key.Pay);
3662   switch (winCutsceneSkip) {
3663     case 0: // nothing was pressed
3664       if (payPress) winCutsceneSkip = 1;
3665       break;
3666     case 1: // waiting for pay release
3667       if (payRelease) winCutsceneSkip = 2;
3668       break;
3669     case 2: // pay released, do skip
3670       setGameOver();
3671       return;
3672   }
3674   /+
3675   auto sname = plr.getSprite().Name;
3676   if (sname != 'sStandLeft' && sname != 'sDamselLeft' && sname != 'sTunnelLeft') {
3677     if (x &gt;= 448 + 8) {
3678       // stop
3679            if (global.isDamsel) sprite_index = sDamselLeft;
3680       else if (global.isTunnelMan) sprite_index = sTunnelLeft;
3681       else sprite_index = sStandLeft;
3682       alarm[0] = 20;
3683     } else {
3684       x += 2;
3685     }
3686   } else if (status == LAVA) {
3687     alarm[3] = 50;
3688     status += 1;
3689   } else if (status == LAVA+1) {
3690     instance_create(oEndPlat.x+rand(0,80), 192+32, oBurn);
3691   }
3692   +/
3694   // first winning room
3695   if (inWinCutscene == 1) {
3696     if (plr.ix < 448+8) {
3697       plr.kRight = true;
3698       return;
3699     }
3701     // waiting for chest to open
3702     if (winCutScenePhase == 0) {
3703       winCutSceneTimer = 120/2;
3704       winCutScenePhase = 1;
3705       return;
3706     }
3708     // spawn big idol
3709     if (winCutScenePhase == 1) {
3710       if (--winCutSceneTimer == 0) {
3711         winCutScenePhase = 2;
3712         winCutSceneTimer = 20;
3713         forEachObject(delegate bool (MapObject o) {
3714           if (o isa MapObjectBigChest) {
3715             o.setSprite(global.config.gameMode == GameConfig::GameMode.Vanilla ? 'sBigChestOpen' : 'sBigChestOpen2');
3716             auto treasure = MakeMapObject(o.ix, o.iy, 'oBigTreasure');
3717             if (treasure) {
3718               treasure.yVel = -4;
3719               treasure.xVel = -3;
3720               o.playSound('sndClick');
3721               //!!!if (global.config.gameMode != GameConfig::GameMode.Vanilla) scrSprayGems(oBigChest.x+24, oBigChest.y+24);
3722             }
3723           }
3724           return false;
3725         });
3726       }
3727       return;
3728     }
3730     // lava pump wait
3731     if (winCutScenePhase == 2) {
3732       if (--winCutSceneTimer == 0) {
3733         winCutScenePhase = 3;
3734         winCutSceneTimer = 50;
3735       }
3736       return;
3737     }
3739     // lava pump start
3740     if (winCutScenePhase == 3) {
3741       auto ep = findEndPlatTile();
3742       if (ep) MakeMapObject(ep.ix+global.randOther(0, 80), /*ep.iy*/192+32, 'oBurn');
3743       if (--winCutSceneTimer == 0) {
3744         winCutScenePhase = 4;
3745         winCutSceneTimer = 10;
3746         if (ep) MakeMapObject(ep.ix, ep.iy+30, 'oLavaSpray');
3747         //!scrShake(9999);
3748       }
3749       return;
3750     }
3752     // lava pump first accel
3753     if (winCutScenePhase == 4) {
3754       if (--winCutSceneTimer == 0) {
3755         forEachObject(delegate bool (MapObject o) {
3756           if (o isa MapObjectLavaSpray) o.yAcc = -0.1;
3757           return false;
3758         });
3759       }
3760     }
3762     // lava pump complete
3763     if (winCutScenePhase == 5) {
3764       if (--winCutSceneTimer == 0) {
3765         //if (oLavaSpray) oLavaSpray.yAcc = -0.1;
3766         startWinCutsceneVolcano();
3767       }
3768       return;
3769     }
3770     return;
3771   }
3774   // volcano room
3775   if (inWinCutscene == 2) {
3776     plr.flty = 0;
3778     // initialize
3779     if (winCutScenePhase == 0) {
3780       winCutSceneTimer = 50;
3781       winCutScenePhase = 1;
3782       winVolcanoTimer = 10;
3783       return;
3784     }
3786     if (winVolcanoTimer > 0) {
3787       if (--winVolcanoTimer == 0) {
3788         MakeMapObject(224+global.randOther(0,48), 144+global.randOther(0,8), 'oVolcanoFlame');
3789         winVolcanoTimer = global.randOther(10, 20);
3790       }
3791     }
3793     // plr sil
3794     if (winCutScenePhase == 1) {
3795       if (--winCutSceneTimer == 0) {
3796         winCutSceneTimer = 30;
3797         winCutScenePhase = 2;
3798         auto sil = MakeMapObject(240, 132, 'oPlayerSil');
3799         //sil.xVel = -6;
3800         //sil.yVel = -8;
3801       }
3802       return;
3803     }
3805     // treasure sil
3806     if (winCutScenePhase == 2) {
3807       if (--winCutSceneTimer == 0) {
3808         winCutScenePhase = 3;
3809         auto sil = MakeMapObject(240, 132, 'oTreasureSil');
3810         //sil.xVel = -6;
3811         //sil.yVel = -8;
3812       }
3813       return;
3814     }
3816     return;
3817   }
3819   // winning camel room
3820   if (inWinCutscene == 3) {
3821     if (!plr.visible) plr.flty = -32;
3823     // initialize
3824     if (winCutScenePhase == 0) {
3825       winCutSceneTimer = 50;
3826       winCutScenePhase = 1;
3827       return;
3828     }
3830     // fall sound
3831     if (winCutScenePhase == 1) {
3832       if (--winCutSceneTimer == 0) {
3833         winCutSceneTimer = 50;
3834         winCutScenePhase = 2;
3835         plr.playSound('sndPFall');
3836         plr.visible = true;
3837         plr.active = true;
3838         plr.status == MapObject::FALLING;
3839         global.plife += 99; // just in case
3840       }
3841       return;
3842     }
3844     if (winCutScenePhase == 2) {
3845       if (plr.status == MapObject::STUNNED || plr.stunned) {
3846         //alarm[0] = 70;
3847         //alarm[1] = 50;
3848         //status = GETUP;
3849         auto treasure = MakeMapObject(144+16+8, -32, 'oBigTreasure');
3850         if (treasure) treasure.depth = 1;
3851         winCutScenePhase = 3;
3852         plr.stunTimer = 30;
3853         plr.playSound('sndTFall');
3854       }
3855       return;
3856     }
3858     if (winCutScenePhase == 3) {
3859       if (plr.status != MapObject::STUNNED && !plr.stunned) {
3860         auto bt = findBigTreasure();
3861         if (bt) {
3862           if (bt.yVel == 0) {
3863             //plr.yVel = -4;
3864             //plr.status = MapObject::JUMPING;
3865             plr.kJump = true;
3866             plr.kJumpPressed = true;
3867             winCutScenePhase = 4;
3868             winCutSceneTimer = 50;
3869           }
3870         }
3871       }
3872       return;
3873     }
3875     if (winCutScenePhase == 4) {
3876       if (--winCutSceneTimer == 0) {
3877         setMenuTilesVisible(true);
3878         winCutScenePhase = 5;
3879         winSceneDrawStatus = 1;
3880         global.playMusic('musVictory', loop:false);
3881         winCutSceneTimer = 50;
3882       }
3883       return;
3884     }
3886     if (winCutScenePhase == 5) {
3887       if (winSceneDrawStatus == 3) {
3888         int money = stats.money;
3889         if (winMoneyCount < money) {
3890           if (money-winMoneyCount > 1000) {
3891             winMoneyCount += 1000;
3892           } else if (money-winMoneyCount > 100) {
3893             winMoneyCount += 100;
3894           } else if (money-winMoneyCount > 10) {
3895             winMoneyCount += 10;
3896           } else {
3897             ++winMoneyCount;
3898           }
3899         }
3900         if (winMoneyCount >= money) {
3901           winMoneyCount = money;
3902           ++winSceneDrawStatus;
3903         }
3904         return;
3905       }
3907       if (winSceneDrawStatus == 7) {
3908         winFadeOut = true;
3909         winFadeLevel += 1;
3910         if (winFadeLevel >= 255) {
3911           ++winSceneDrawStatus;
3912           winCutSceneTimer = 30*30;
3913         }
3914         return;
3915       }
3917       if (winSceneDrawStatus == 8) {
3918         if (--winCutSceneTimer == 0) {
3919           setGameOver();
3920         }
3921         return;
3922       }
3924       if (--winCutSceneTimer == 0) {
3925         ++winSceneDrawStatus;
3926         winCutSceneTimer = 50;
3927       }
3928     }
3930     return;
3931   }
3935 // ////////////////////////////////////////////////////////////////////////// //
3936 void renderWinCutsceneOverlay () {
3937   if (inWinCutscene == 3) {
3938     if (winSceneDrawStatus > 0) {
3939       Video.color = 0xff_ff_ff;
3940       sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3941       //draw_set_color(txtCol);
3942       drawTextAt(64, 32, "YOU MADE IT!");
3944       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
3945       if (global.config.gameMode == GameConfig::GameMode.Vanilla) {
3946         Video.color = 0xff_ff_00; //draw_set_color(c_yellow);
3947         drawTextAt(64, 48, "Classic Mode done!");
3948       } else {
3949         Video.color = 0x00_80_80; //draw_set_color(c_teal);
3950         if (global.config.bizarrePlus) drawTextAt(64, 48, "Bizarre Mode Plus done!");
3951         else drawTextAt(64, 48, "Bizarre Mode done!");
3952         //draw_set_color(c_white);
3953       }
3954       if (!global.usedShortcut) {
3955         Video.color = 0xc0_c0_c0; //draw_set_color(c_silver);
3956         drawTextAt(64, 56, "No shortcuts used!");
3957         //draw_set_color(c_yellow);
3958       }
3959     }
3961     if (winSceneDrawStatus > 1) {
3962       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
3963       //draw_set_color(txtCol);
3964       Video.color = 0xff_ff_ff;
3965       drawTextAt(64, 64, "FINAL SCORE:");
3966     }
3968     if (winSceneDrawStatus > 2) {
3969       sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3970       Video.color = 0xff_ff_ff; //draw_set_color(c_white);
3971       drawTextAt(64, 72, va("$%d", winMoneyCount));
3972     }
3974     if (winSceneDrawStatus > 4) {
3975       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
3976       Video.color = 0xff_ff_ff; //draw_set_color(c_white);
3977       drawTextAt(64, 96, va("Time: %s", time2str(winTime)));
3978       /*
3979       draw_set_color(c_white);
3980       if (s &lt; 10) draw_text(96+24, 96, string(m) + ":0" + string(s));
3981       else draw_text(96+24, 96, string(m) + ":" + string(s));
3982       */
3983     }
3985     if (winSceneDrawStatus > 5) {
3986       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
3987       Video.color = 0xff_ff_ff; //draw_set_color(txtCol);
3988       drawTextAt(64, 96+8, "Kills: ");
3989       Video.color = 0xff_ff_ff; //draw_set_color(c_white);
3990       drawTextAt(96+24, 96+8, va("%s", stats.countKills()));
3991     }
3993     if (winSceneDrawStatus > 6) {
3994       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
3995       Video.color = 0xff_ff_ff; //draw_set_color(txtCol);
3996       drawTextAt(64, 96+16, "Saves: ");
3997       Video.color = 0xff_ff_ff; //draw_set_color(c_white);
3998       drawTextAt(96+24, 96+16, va("%s", stats.damselsSaved));
3999     }
4001     if (winFadeOut) {
4002       Video.color = (255-clamp(winFadeLevel, 0, 255))<<24;
4003       Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
4004     }
4006     if (winSceneDrawStatus == 8) {
4007       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4008       Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4009       string lastString;
4010       if (global.config.gameMode == GameConfig::GameMode.Vanilla) {
4011         Video.color = 0xff_ff_00; //draw_set_color(c_yellow);
4012         lastString = "YOU SHALL BE REMEMBERED AS A HERO.";
4013       } else {
4014         Video.color = 0x00_ff_ff;
4015         if (global.config.bizarrePlus) lastString = "ANOTHER LEGENDARY ADVENTURE!";
4016         else lastString = "YOUR DISCOVERIES WILL BE CELEBRATED!";
4017       }
4018       auto strLen = lastString.length*8;
4019       int n = 320-strLen;
4020       n = trunc(ceil(n/2.0));
4021       drawTextAt(n, 116, lastString);
4022     }
4023   }
4027 // ////////////////////////////////////////////////////////////////////////// //
4028 #include "roomTitle.vc"
4029 #include "roomTrans1.vc"
4030 #include "roomTrans2.vc"
4031 #include "roomTrans3.vc"
4032 #include "roomTrans4.vc"
4033 #include "roomOlmec.vc"
4034 #include "roomEnd.vc"
4037 // ////////////////////////////////////////////////////////////////////////// //
4038 #include "packages/Generator/loadRoomGens.vc"
4039 #include "packages/Generator/loadEntityGens.vc"