end sequence now works with ball
[k8vacspelynky.git] / GameLevel.vc
blob6c3ce15bcdb8b00d131e8026662b16d99d4c4644
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   shakeLeft = 0;
200   if (player) {
201     player.removeBallAndChain(temp:false);
202     auto hi = player.holdItem;
203     player.holdItem = none;
204     if (hi) hi.instanceRemove();
205     hi = player.pickedItem;
206     player.pickedItem = none;
207     if (hi) hi.instanceRemove();
208   }
209   time = 0;
210   global.resetGame();
211   stats.clearGameTotals();
212   if (global.startMoney > 0) stats.setMoneyCheat();
213   stats.setMoney(global.startMoney);
214   //writeln("level=", global.currLevel, "; lt=", global.levelType);
218 // complement function to `restart game`
219 void generateNormalLevel () {
220   generateLevel();
221   centerViewAtPlayer();
225 // ////////////////////////////////////////////////////////////////////////// //
226 // generate angry shopkeeper at exit if murderer or thief
227 void generateAngryShopkeepers () {
228   if (global.murderer || global.thiefLevel > 0) {
229     foreach (MapTile e; allExits) {
230       auto obj = MonsterShopkeeper(MakeMapObject(e.ix, e.iy, 'oShopkeeper'));
231       if (obj) {
232         obj.style = 'Bounty Hunter';
233         obj.status = MapObject::PATROL;
234       }
235     }
236   }
240 // ////////////////////////////////////////////////////////////////////////// //
241 final void resetRoomBounds () {
242   viewMin.x = 0;
243   viewMin.y = 0;
244   viewMax.x = tilesWidth*16;
245   viewMax.y = tilesHeight*16;
246   // Great Lake is bottomless (nope)
247   //if (global.lake) viewMax.y -= 16;
248   //writeln("ROOM BOUNDS: (", viewMax.x, "x", viewMax.y, ")");
252 final void setRoomBounds (int x0, int y0, int x1, int y1) {
253   viewMin.x = x0;
254   viewMin.y = y0;
255   viewMax.x = x1+16;
256   viewMax.y = y1+16;
260 // ////////////////////////////////////////////////////////////////////////// //
261 struct OSDMessage {
262   string msg;
263   float timeout; // seconds
264   float starttime; // for active
265   bool active; // true: timeout is `GetTickCount()` dismissing time
268 array!OSDMessage msglist; // [0]: current one
271 private final void osdCheckTimeouts () {
272   auto stt = GetTickCount();
273   while (msglist.length) {
274     if (!msglist[0].active) {
275       msglist[0].active = true;
276       msglist[0].starttime = stt;
277     }
278     if (msglist[0].starttime+msglist[0].timeout >= stt) break;
279     msglist.remove(0);
280   }
284 final bool osdHasMessage () {
285   osdCheckTimeouts();
286   return (msglist.length > 0);
290 final string osdGetMessage (out float timeLeft, out float timeStart) {
291   osdCheckTimeouts();
292   if (msglist.length == 0) { timeLeft = 0; return ""; }
293   auto stt = GetTickCount();
294   timeStart = msglist[0].starttime;
295   timeLeft = fmax(0, msglist[0].starttime+msglist[0].timeout-stt);
296   return msglist[0].msg;
300 final void osdClear () {
301   msglist.length -= msglist.length;
305 final void osdMessage (string msg, optional float timeout, optional bool urgent) {
306   if (!msg) return;
307   if (!specified_timeout) timeout = 3.33;
308   // special message for shops
309   if (timeout == -666) {
310     if (!msg) return;
311     if (msglist.length && msglist[0].msg == msg) return;
312     if (msglist.length == 0 || msglist[0].msg != msg) {
313       osdClear();
314       msglist.length += 1;
315       msglist[0].msg = msg;
316     }
317     msglist[0].active = false;
318     msglist[0].timeout = 3.33;
319     osdCheckTimeouts();
320     return;
321   }
322   if (timeout < 0.1) return;
323   timeout = fmax(1.0, timeout);
324   //writeln("OSD: ", msg);
325   // find existing one, and bring it to the top
326   int oldidx = 0;
327   for (; oldidx < msglist.length; ++oldidx) {
328     if (msglist[oldidx].msg == msg) break; // i found her!
329   }
330   // duplicate?
331   if (oldidx < msglist.length) {
332     // yeah, move duplicate to the top
333     msglist[oldidx].timeout = fmax(timeout, msglist[oldidx].timeout);
334     msglist[oldidx].active = false;
335     if (urgent && oldidx != 0) {
336       timeout = msglist[oldidx].timeout;
337       msglist.remove(oldidx);
338       msglist.insert(0);
339       msglist[0].msg = msg;
340       msglist[0].timeout = timeout;
341       msglist[0].active = false;
342     }
343   } else if (urgent) {
344     msglist.insert(0);
345     msglist[0].msg = msg;
346     msglist[0].timeout = timeout;
347     msglist[0].active = false;
348   } else {
349     // new one
350     msglist.length += 1;
351     msglist[$-1].msg = msg;
352     msglist[$-1].timeout = timeout;
353     msglist[$-1].active = false;
354   }
355   osdCheckTimeouts();
359 // ////////////////////////////////////////////////////////////////////////// //
360 void setup (GameGlobal aGlobal, SpriteStore aSprStore, BackTileStore aBGTileStore) {
361   global = aGlobal;
362   sprStore = aSprStore;
363   bgtileStore = aBGTileStore;
365   lg = SpawnObject(LevelGen);
366   lg.global = global;
367   lg.level = self;
369   miscTileGrid = SpawnObject(EntityGrid);
370   miscTileGrid.setup(0, 0, MaxTilesWidth*16, MaxTilesHeight*16, MapTile);
371   //miscTileGrid.ownObjects = true;
373   objGrid = SpawnObject(EntityGrid);
374   objGrid.setup(0, 0, MaxTilesWidth*16, MaxTilesHeight*16, MapObject);
378 // stores should be set
379 void onLoaded () {
380   checkWater = true;
381   levBGImg = bgtileStore[levBGImgName];
382   foreach (int y; 0..MaxTilesHeight) {
383     foreach (int x; 0..MaxTilesWidth) {
384       if (tiles[x, y]) tiles[x, y].onLoaded();
385     }
386   }
387   foreach (MapEntity o; miscTileGrid.allObjects()) o.onLoaded();
388   foreach (MapEntity o; objGrid.allObjects()) o.onLoaded();
389   for (MapBackTile bt = backtiles; bt; bt = bt.next) bt.onLoaded();
390   if (player) player.onLoaded();
391   //FIXME
392   if (msglist.length) {
393     msglist[0].active = false;
394     msglist[0].timeout = 0.200;
395     osdCheckTimeouts();
396   }
400 // ////////////////////////////////////////////////////////////////////////// //
401 void pickedSpectacles () {
402   foreach (int y; 0..tilesHeight) {
403     foreach (int x; 0..tilesWidth) {
404       MapTile t = tiles[x, y];
405       if (t && t.isInstanceAlive) t.onGotSpectacles();
406     }
407   }
408   foreach (MapTile t; miscTileGrid.allObjects()) {
409     if (t.isInstanceAlive) t.onGotSpectacles();
410   }
414 // ////////////////////////////////////////////////////////////////////////// //
415 #include "rgentile.vc"
416 #include "rgenobj.vc"
419 void onLevelExited () {
420   if (isNormalLevel()) stats.maxLevelComplete = max(stats.maxLevelComplete, global.currLevel);
421   if (onLevelExitedCB) onLevelExitedCB();
422   if (levelKind == LevelKind.Transition) {
423     if (global.thiefLevel > 0) global.thiefLevel -= 1;
424     //orig dbg:if (global.currLevel == 1) global.currLevel += firstLevelSkip; else global.currLevel += levelSkip;
425     global.currLevel += 1;
426     generateLevel();
427   } else {
428     if (lg.finalBossLevel) {
429       winTime = time;
430       ++stats.gamesWon;
431       // add money for big idol
432       player.addScore(50000);
433       startWinCutscene();
434     } else {
435       generateTransitionLevel();
436     }
437   }
438   centerViewAtPlayer();
442 void onOlmecDead (MapObject o) {
443   writeln("*** OLMEC IS DEAD!");
444   foreach (MapTile t; allExits) {
445     if (t.exit) {
446       t.openExit();
447       auto st = checkTileAtPoint(t.ix+8, t.iy+16);
448       if (!st) {
449         st = MakeMapTile(t.ix/16, t.iy/16+1, 'oTemple');
450         st.ore = 0;
451       }
452       st.invincible = true;
453     }
454   }
458 void generateLevelMessages () {
459   if (global.darkLevel) {
460          if (global.hasCrown) osdMessage("THE HEDJET SHINES BRIGHTLY.");
461     else if (global.config.scumDarkness < 2) osdMessage("I CAN'T SEE A THING!");
462     /*
463     else global.message = "";
464          if (global.hasCrown) global.message2 = "";
465     else if (global.scumDarkness &lt; 2) global.message2 = "I'D BETTER USE THESE FLARES!";
466     else global.message2 = "";
467     global.messageTimer = 200;
468     alarm[1] = 210;
469     */
470   }
472   if (global.blackMarket) osdMessage("WELCOME TO THE BLACK MARKET!");
474   if (global.cemetary) osdMessage("THE DEAD ARE RESTLESS!");
475   if (global.lake) osdMessage("I CAN HEAR RUSHING WATER...");
477   if (global.snakePit) osdMessage("I HEAR SNAKES... I HATE SNAKES!");
478   if (global.yetiLair) osdMessage("IT SMELLS LIKE WET FUR IN HERE!");
479   if (global.alienCraft) osdMessage("THERE'S A PSYCHIC PRESENCE HERE!");
480   if (global.cityOfGold) {
481     if (true /*hasOlmec()*/) osdMessage("IT'S THE LEGENDARY CITY OF GOLD!");
482   }
484   if (global.sacrificePit) osdMessage("I CAN HEAR PRAYERS TO KALI!");
488 final MapObject debugSpawnObjectWithClass (class!MapObject oclass, optional bool playerDir) {
489   if (!oclass) return none;
490   int dx = 0, dy = 0;
491   bool canLeft = !isSolidAtPoint(player.ix-8, player.iy);
492   bool canRight = !isSolidAtPoint(player.ix+16, player.iy);
493   if (!canLeft && !canRight) return none;
494   if (canLeft && canRight) {
495     if (playerDir) {
496       dx = (player.dir == MapObject::Dir.Left ? -16 : 16);
497     } else {
498       dx = 16;
499     }
500   } else {
501     dx = (canLeft ? -16 : 16);
502   }
503   auto obj = SpawnMapObjectWithClass(oclass);
504   if (obj isa MapEnemy) { dx -= 8; dy -= 8; }
505   if (obj) PutSpawnedMapObject(player.ix+dx, player.iy+dy, obj);
506   return obj;
510 final MapObject debugSpawnObject (name aname) {
511   if (!aname) return none;
512   return debugSpawnObjectWithClass(findGameObjectClassByName(aname));
516 // `global.currLevel` is the new level
517 void generateTransitionLevel () {
518   xmoney = 0;
519   collectCounter = 0;
521   global.setMusicPitch(1.0);
522   switch (global.config.transitionMusicMode) {
523     case GameConfig::MusicMode.Silent: global.stopMusic(); break;
524     case GameConfig::MusicMode.Restart: global.restartMusic(); break;
525     case GameConfig::MusicMode.DontTouch: break;
526   }
528   levelKind = LevelKind.Transition;
530   auto olddel = ImmediateDelete;
531   ImmediateDelete = false;
532   clearTiles();
533   clearObjects();
535        if (global.currLevel < 4) createTrans1Room();
536   else if (global.currLevel == 4) createTrans1xRoom();
537   else if (global.currLevel < 8) createTrans2Room();
538   else if (global.currLevel == 8) createTrans2xRoom();
539   else if (global.currLevel < 12) createTrans3Room();
540   else if (global.currLevel == 12) createTrans3xRoom();
541   else if (global.currLevel < 16) createTrans4Room();
542   else if (global.currLevel == 16) createTrans4Room();
543   else createTrans1Room(); //???
545   setMenuTilesOnTop();
547   fixWallTiles();
548   addBackgroundGfxDetails();
549   levBGImgName = 'bgCave';
550   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
552   blockWaterChecking = true;
553   fixLiquidTop();
554   cleanDeadTiles();
556   if (damselSaved > 0) {
557     MakeMapObject(176+8, 176+8, 'oDamselKiss');
558     global.plife += damselSaved; // if player skipped transition cutscene
559     damselSaved = 0;
560   }
562   collectLavaTiles();
564   ImmediateDelete = olddel;
565   CollectGarbage(true); // destroy delayed objects too
567   if (dumpGridStats) {
568     miscTileGrid.dumpStats();
569     objGrid.dumpStats();
570   }
572   playerExited = false; // just in case
574   osdClear();
576   setupGhostTime();
577   //global.playMusic(lg.musicName);
581 void generateLevel () {
582   global.setMusicPitch(1.0);
583   stats.clearLevelTotals();
585   levelKind = LevelKind.Normal;
586   lg.generate();
587   //lg.dump();
589   resetRoomBounds();
591   lg.generateRooms();
592   //writeln("tw:", tilesWidth, "; th:", tilesHeight);
594   auto olddel = ImmediateDelete;
595   ImmediateDelete = false;
596   clearTiles();
597   clearObjects();
599   if (lg.finalBossLevel) {
600     blockWaterChecking = true;
601     createOlmecRoom();
602   }
604   // if transition cutscene was skipped...
605   if (damselSaved > 0) global.plife += damselSaved; // if player skipped transition cutscene
606   damselSaved = 0;
608   // generate tiles
609   startRoomX = lg.startRoomX;
610   startRoomY = lg.startRoomY;
611   endRoomX = lg.endRoomX;
612   endRoomY = lg.endRoomY;
613   addBackgroundGfxDetails();
614   foreach (int y; 0..tilesHeight) {
615     foreach (int x; 0..tilesWidth) {
616       lg.genTileAt(x, y);
617     }
618   }
619   fixWallTiles();
621   levBGImgName = lg.bgImgName;
622   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
624   if (global.allowAngryShopkeepers) generateAngryShopkeepers();
626   lg.generateEntities();
628   // add box of flares to dark level
629   if (global.darkLevel && allEnters.length) {
630     auto enter = allEnters[0];
631     int x = enter.ix, y = enter.iy;
632          if (!isSolidAtPoint(x-16, y)) MakeMapObject(x-16+8, y+8, 'oFlareCrate');
633     else if (!isSolidAtPoint(x+16, y)) MakeMapObject(x+16+8, y+8, 'oFlareCrate');
634     else MakeMapObject(x+8, y+8, 'oFlareCrate');
635   }
637   //scrGenerateEntities();
638   //foreach (; 0..2) scrGenerateEntities();
640   writeln(countObjects, " alive objects inserted");
641   writeln(countBackTiles, " background tiles inserted");
643   if (!player) FatalError("player pawn is not spawned");
645   if (lg.finalBossLevel) {
646     blockWaterChecking = true;
647   } else {
648     blockWaterChecking = false;
649   }
650   fixLiquidTop();
651   cleanDeadTiles();
653   collectLavaTiles();
655   ImmediateDelete = olddel;
656   CollectGarbage(true); // destroy delayed objects too
658   if (dumpGridStats) {
659     miscTileGrid.dumpStats();
660     objGrid.dumpStats();
661   }
663   playerExited = false; // just in case
665   levelMoneyStart = stats.money;
667   osdClear();
668   generateLevelMessages();
670   xmoney = 0;
671   collectCounter = 0;
673   if (lastMusicName != lg.musicName) {
674     global.playMusic(lg.musicName);
675     //writeln("!<", lastMusicName, ">:<", lg.musicName, ">");
676   } else {
677     //writeln("MM: ", global.config.nextLevelMusicMode);
678     switch (global.config.nextLevelMusicMode) {
679       case GameConfig::MusicMode.Silent: global.stopMusic(); break; // the thing that should not be
680       case GameConfig::MusicMode.Restart: global.restartMusic(); break;
681       case GameConfig::MusicMode.DontTouch:
682         if (global.config.transitionMusicMode == GameConfig::MusicMode.Silent) {
683           global.playMusic(lg.musicName);
684         }
685         break;
686     }
687   }
688   lastMusicName = lg.musicName;
689   //global.playMusic(lg.musicName);
691   setupGhostTime();
695 // ////////////////////////////////////////////////////////////////////////// //
696 int currKeys, nextKeys;
697 int pressedKeysQ, releasedKeysQ;
698 int keysPressed, keysReleased = -1;
701 struct SavedKeyState {
702   int currKeys, nextKeys;
703   int pressedKeysQ, releasedKeysQ;
704   int keysPressed, keysReleased;
705   // for session
706   int roomSeed, otherSeed;
710 // for saving/replaying
711 final void keysSaveState (out SavedKeyState ks) {
712   ks.currKeys = currKeys;
713   ks.nextKeys = nextKeys;
714   ks.pressedKeysQ = pressedKeysQ;
715   ks.releasedKeysQ = releasedKeysQ;
716   ks.keysPressed = keysPressed;
717   ks.keysReleased = keysReleased;
720 // for saving/replaying
721 final void keysRestoreState (const ref SavedKeyState ks) {
722   currKeys = ks.currKeys;
723   nextKeys = ks.nextKeys;
724   pressedKeysQ = ks.pressedKeysQ;
725   releasedKeysQ = ks.releasedKeysQ;
726   keysPressed = ks.keysPressed;
727   keysReleased = ks.keysReleased;
731 final void keysNextFrame () {
732   currKeys = nextKeys;
736 final void clearKeys () {
737   currKeys = 0;
738   nextKeys = 0;
739   pressedKeysQ = 0;
740   releasedKeysQ = 0;
741   keysPressed = 0;
742   keysReleased = -1;
746 final void onKey (int code, bool down) {
747   if (!code) return;
748   if (down) {
749     currKeys |= code;
750     nextKeys |= code;
751     if (keysReleased&code) {
752       keysPressed |= code;
753       keysReleased &= ~code;
754       pressedKeysQ |= code;
755     }
756   } else {
757     nextKeys &= ~code;
758     if (keysPressed&code) {
759       keysReleased |= code;
760       keysPressed &= ~code;
761       releasedKeysQ |= code;
762     }
763   }
766 final bool isKeyDown (int code) {
767   return !!(currKeys&code);
770 final bool isKeyPressed (int code) {
771   bool res = !!(pressedKeysQ&code);
772   pressedKeysQ &= ~code;
773   return res;
776 final bool isKeyReleased (int code) {
777   bool res = !!(releasedKeysQ&code);
778   releasedKeysQ &= ~code;
779   return res;
783 final void clearKeysPressRelease () {
784   keysPressed = default.keysPressed;
785   keysReleased = default.keysReleased;
786   pressedKeysQ = default.pressedKeysQ;
787   releasedKeysQ = default.releasedKeysQ;
788   currKeys = 0;
789   nextKeys = 0;
793 // ////////////////////////////////////////////////////////////////////////// //
794 final void registerEnter (MapTile t) {
795   if (!t) return;
796   allEnters[$] = t;
797   return;
801 final void registerExit (MapTile t) {
802   if (!t) return;
803   allExits[$] = t;
804   return;
808 final bool isYAtEntranceRow (int py) {
809   py /= 16;
810   foreach (MapTile t; allEnters) if (t.iy == py) return true;
811   return false;
815 final int calcNearestEnterDist (int px, int py) {
816   if (allEnters.length == 0) return int.max;
817   int curdistsq = int.max;
818   foreach (MapTile t; allEnters) {
819     int xc = px-t.xCenter, yc = py-t.yCenter;
820     int distsq = xc*xc+yc*yc;
821     if (distsq < curdistsq) curdistsq = distsq;
822   }
823   return round(sqrt(curdistsq));
827 final int calcNearestExitDist (int px, int py) {
828   if (allExits.length == 0) return int.max;
829   int curdistsq = int.max;
830   foreach (MapTile t; allExits) {
831     int xc = px-t.xCenter, yc = py-t.yCenter;
832     int distsq = xc*xc+yc*yc;
833     if (distsq < curdistsq) curdistsq = distsq;
834   }
835   return round(sqrt(curdistsq));
839 // ////////////////////////////////////////////////////////////////////////// //
840 final void clearForTransition () {
841   auto olddel = ImmediateDelete;
842   ImmediateDelete = false;
843   clearTiles();
844   clearObjects();
845   ImmediateDelete = olddel;
846   CollectGarbage(true); // destroy delayed objects too
850 final void clearTiles () {
851   accumTime = 0;
852   time = 0;
853   allEnters.length -= allEnters.length; // don't deallocate
854   allExits.length -= allExits.length; // don't deallocate
855   lavatiles.length -= lavatiles.length;
856   foreach (ref auto tile; tiles) delete tile;
857   if (dumpGridStats) { if (miscTileGrid.getFirstObject()) miscTileGrid.dumpStats(); }
858   miscTileGrid.removeAllObjects(true); // and destroy
859   while (backtiles) {
860     MapBackTile t = backtiles;
861     backtiles = t.next;
862     delete t;
863   }
864   levBGImg = none;
868 // ////////////////////////////////////////////////////////////////////////// //
869 final int countObjects () {
870   return objGrid.countObjects();
873 final int countBackTiles () {
874   int res = 0;
875   for (MapBackTile bt = backtiles; bt; bt = bt.next) ++res;
876   return res;
879 final void clearObjects () {
880   // don't kill objects player is holding
881   if (player) {
882     if (player.pickedItem isa ItemBall) {
883       player.pickedItem.instanceRemove();
884       player.pickedItem = none;
885     }
886     if (player.pickedItem && player.pickedItem.grid) {
887       player.pickedItem.grid.remove(player.pickedItem.gridId);
888       writeln("secured pocket item '", GetClassName(player.pickedItem.Class), "'");
889       //player.pickedItem.grid = none;
890     }
891     if (player.holdItem isa ItemBall) {
892       player.removeBallAndChain(temp:true);
893       player.holdItem.instanceRemove();
894       player.holdItem = none;
895     }
896     if (player.holdItem && player.holdItem.grid) {
897       player.holdItem.grid.remove(player.holdItem.gridId);
898       writeln("secured held item '", GetClassName(player.holdItem.Class), "'");
899       //player.holdItem.grid = none;
900     }
901   }
902   //
903   int count = objGrid.countObjects();
904   if (dumpGridStats) { if (objGrid.getFirstObject()) objGrid.dumpStats(); }
905   objGrid.removeAllObjects(true); // and destroy
906   if (count > 0) writeln(count, " objects destroyed");
907   ballObjects.length = 0;
908   lastUsedObjectId = 0;
912 final void insertObject (MapObject o) {
913   if (!o) return;
914   if (o.grid) FatalError("cannot put object into level twice");
915   o.objId = ++lastUsedObjectId;
917   // ball from ball-and-chain
918   if (o isa ItemBall) {
919     bool found = false;
920     foreach (MapObject bo; ballObjects) if (bo == o) { found = true; break; }
921     if (!found) ballObjects[$] = o;
922   }
924   objGrid.insert(o);
928 final void spawnPlayerAt (int x, int y) {
929   // if we have no player, spawn new one
930   // otherwise this just a level transition, so simply reposition him
931   if (!player) {
932     // don't add player to object list, as it has very separate processing anyway
933     player = SpawnObject(PlayerPawn);
934     player.global = global;
935     player.level = self;
936     if (!player.initialize()) {
937       delete player;
938       FatalError("something is wrong with player initialization");
939       return;
940     }
941   }
942   player.fltx = x;
943   player.flty = y;
944   player.saveInterpData();
945   player.resurrect();
946   if (player.mustBeChained || global.config.scumBallAndChain) player.spawnBallAndChain();
947   playerExited = false;
948   if (global.config.startWithKapala) global.hasKapala = true;
949   centerViewAtPlayer();
950   // reinsert player items into grid
951   if (player.pickedItem) objGrid.insert(player.pickedItem);
952   if (player.holdItem) objGrid.insert(player.holdItem);
953   //writeln("player spawned; active=", player.active);
954   player.scrSwitchToPocketItem(forceIfEmpty:false);
958 final void teleportPlayerTo (int x, int y) {
959   if (player) {
960     player.fltx = x;
961     player.flty = y;
962     player.saveInterpData();
963   }
967 final void resurrectPlayer () {
968   if (player) player.resurrect();
969   playerExited = false;
973 // ////////////////////////////////////////////////////////////////////////// //
974 final void scrShake (int duration) {
975   if (shakeLeft == 0) {
976     shakeOfs.x = 0;
977     shakeOfs.y = 0;
978     shakeDir.x = 0;
979     shakeDir.y = 0;
980   }
981   shakeLeft = max(shakeLeft, duration);
986 // ////////////////////////////////////////////////////////////////////////// //
987 enum SCAnger {
988   TileDestroyed,
989   ItemStolen, // including damsel, lol
990   CrapsCheated,
991   BombDropped,
992   DamselWhipped,
996 // make the nearest shopkeeper angry. RAWR!
997 void scrShopkeeperAnger (SCAnger reason, optional int maxdist, optional MapEntity offender) {
998   if (!offender) offender = player;
999   auto shp = MonsterShopkeeper(findNearestEnemy(offender.ix, offender.iy, delegate bool (MapEnemy o) {
1000     auto sc = MonsterShopkeeper(o);
1001     if (!sc) return false;
1002     if (sc.dead || sc.angered) return false;
1003     return true;
1004   }));
1006   if (shp) {
1007     if (specified_maxdist && offender.directionToEntityCenter(shp) > maxdist) return;
1008     if (!shp.dead && !shp.angered) {
1009       shp.status = MapObject::ATTACK;
1010       string msg;
1011            if (global.murderer) msg = "YOU'LL PAY FOR YOUR CRIMES!";
1012       else if (reason == SCAnger.ItemStolen) msg = "COME BACK HERE, THIEF!";
1013       else if (reason == SCAnger.TileDestroyed) msg = "DIE, YOU VANDAL!";
1014       else if (reason == SCAnger.BombDropped) msg = "TERRORIST!";
1015       else if (reason == SCAnger.DamselWhipped) msg = "HEY, ONLY I CAN DO THAT!";
1016       else if (reason == SCAnger.CrapsCheated) msg = "DIE, CHEATER!";
1017       else msg = "NOW I'M REALLY STEAMED!";
1018       if (msg) osdMessage(msg, -666);
1019       global.thiefLevel += (global.thiefLevel > 0 ? 3 : 2);
1020     }
1021   }
1025 final MapObject findCrapsPrize () {
1026   foreach (MapObject o; objGrid.allObjects()) {
1027     if (o.spectral || !o.isInstanceAlive) continue;
1028     if (o.inDiceHouse) return o;
1029   }
1030   return none;
1034 // ////////////////////////////////////////////////////////////////////////// //
1035 // 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.
1036 // note: idols moved by monkeys will have false `stolenIdol`
1037 void scrTriggerIdolAltar (bool stolenIdol) {
1038   ObjTikiCurse res = none;
1039   int curdistsq = int.max;
1040   int px = player.xCenter, py = player.yCenter;
1041   foreach (MapObject o; objGrid.allObjects()) {
1042     auto tcr = ObjTikiCurse(o);
1043     if (!tcr || !tcr.isInstanceAlive) continue;
1044     if (tcr.activated) continue;
1045     int xc = px-tcr.xCenter, yc = py-tcr.yCenter;
1046     int distsq = xc*xc+yc*yc;
1047     if (distsq < curdistsq) {
1048       res = tcr;
1049       curdistsq = distsq;
1050     }
1051   }
1052   if (res) res.activate(stolenIdol);
1056 // ////////////////////////////////////////////////////////////////////////// //
1057 void setupGhostTime () {
1058   musicFadeTimer = -1;
1059   ghostSpawned = false;
1061   if (inWinCutscene || !isNormalLevel() || lg.finalBossLevel) {
1062     ghostTimeLeft = -1;
1063     global.setMusicPitch(1.0);
1064     return;
1065   }
1067   if (global.config.scumGhost < 0) {
1068     // instant
1069     ghostTimeLeft = 1;
1070     osdMessage("A GHASTLY VOICE CALLS YOUR NAME!\nLET'S GET OUT OF HERE!", 6.66, urgent:true);
1071     return;
1072   }
1074   if (global.config.scumGhost == 0) {
1075     // never
1076     ghostTimeLeft = -1;
1077     return;
1078   }
1080   // randomizes time until ghost appears once time limit is reached
1081   // global.ghostRandom (true/false) if extra time is randomized - loaded from yasm.cfg. if false, use 30 second default
1082   // ghostTimeLeft (time in seconds * 1000) for currently generated level
1084   if (global.config.ghostRandom) {
1085     auto tMin = global.randOther(10, 30); // min(global.plife*10, 30);
1086     auto tMax = max(tMin+global.randOther(10, 30), min(global.config.scumGhost, 300)); // 5 minutes max
1087     auto tTime = global.randOther(tMin, tMax);
1088     if (tTime <= 0) tTime = round(tMax/2.0);
1089     ghostTimeLeft = tTime;
1090   } else {
1091     ghostTimeLeft = global.config.scumGhost; // 5 minutes max
1092   }
1094   ghostTimeLeft += max(0, global.config.ghostExtraTime);
1096   ghostTimeLeft *= 30; // seconds -> frames
1097   //global.ghostShowTime
1101 void spawnGhost () {
1102   addGhostSummoned();
1103   ghostSpawned = true;
1105   int vwdt = (viewMax.x-viewMin.x);
1106   int vhgt = (viewMax.y-viewMin.y);
1108   int gx, gy;
1110   if (player.ix < viewMin.x+vwdt/2) {
1111     // player is in the left side
1112     gx = viewMin.x+vwdt/2+vwdt/4;
1113   } else {
1114     // player is in the right side
1115     gx = viewMin.x+vwdt/4;
1116   }
1118   if (player.iy < viewMin.y+vhgt/2) {
1119     // player is in the left side
1120     gy = viewMin.y+vhgt/2+vhgt/4;
1121   } else {
1122     // player is in the right side
1123     gy = viewMin.y+vhgt/4;
1124   }
1126   writeln("spawning ghost at tile (", gx/16, ",", gy/16, "); player is at tile (", player.ix/16, ",", player.iy/16, ")");
1128   MakeMapObject(gx, gy, 'oGhost');
1130   /*
1131     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);
1132     else instance_create(view_xview[0]-32, view_yview[0]+floor(view_hview[0]/2), oGhost);
1133     global.ghostExists = true;
1134   */
1138 void thinkFrameGameGhost () {
1139   if (player.dead) return;
1140   if (!isNormalLevel()) return; // just in case
1142   if (ghostTimeLeft < 0) {
1143     // turned off
1144     if (musicFadeTimer > 0) {
1145       musicFadeTimer = -1;
1146       global.setMusicPitch(1.0);
1147     }
1148     return;
1149   }
1151   if (musicFadeTimer >= 0) {
1152     ++musicFadeTimer;
1153     if (musicFadeTimer > 0 && musicFadeTimer <= 100*3) {
1154       float pitch = 1.0-(0.00226/3.0)*musicFadeTimer;
1155       //writeln("MFT: ", musicFadeTimer, "; pitch=", pitch);
1156       global.setMusicPitch(pitch);
1157     }
1158   }
1160   if (ghostTimeLeft == 0) {
1161     // she is already here!
1162     return;
1163   }
1165   // no ghost if we have a crown
1166   if (global.hasCrown) {
1167     ghostTimeLeft = -1;
1168     return;
1169   }
1171   // if she was already spawned, don't do it again
1172   if (ghostSpawned) {
1173     ghostTimeLeft = 0;
1174     return;
1175   }
1177   if (--ghostTimeLeft != 0) {
1178     // warning
1179     if (global.config.ghostExtraTime > 0) {
1180       if (ghostTimeLeft == global.config.ghostExtraTime*30) {
1181         osdMessage("A CHILL RUNS UP YOUR SPINE...\nLET'S GET OUT OF HERE!", 6.66, urgent:true);
1182       }
1183       if (musicFadeTimer < 0 && ghostTimeLeft <= global.config.ghostExtraTime*30) {
1184         musicFadeTimer = 0;
1185       }
1186     }
1187     return;
1188   }
1190   // spawn her
1191   if (player.isExitingSprite) {
1192     // no reason to spawn her, we're leaving
1193     ghostTimeLeft = -1;
1194     return;
1195   }
1197   spawnGhost();
1201 void thinkFrameGame () {
1202   thinkFrameGameGhost();
1206 // ////////////////////////////////////////////////////////////////////////// //
1207 private transient array!MapObject activeThinkerList;
1210 private final bool isWetTile (MapTile t) {
1211   return (t && t.visible && (t.water || t.lava || t.wet));
1215 private final bool isWetOrSolidTile (MapTile t) {
1216   return (t && t.visible && (t.water || t.lava || t.wet || t.solid) && t.isInstanceAlive);
1220 final bool isWetOrSolidTileAtPoint (int px, int py) {
1221   return !!checkTileAtPoint(px, py, &isWetOrSolidTile);
1225 final bool isWetOrSolidTileAtTile (int tx, int ty) {
1226   return !!checkTileAtPoint(tx*16, ty*16, &isWetOrSolidTile);
1230 final bool isWetTileAtTile (int tx, int ty) {
1231   return !!checkTileAtPoint(tx*16, ty*16, &isWetTile);
1235 // ////////////////////////////////////////////////////////////////////////// //
1236 const int GreatLakeStartTileY = 28;
1238 // called once after level generation
1239 final void fixLiquidTop () {
1240   foreach (int tileY; 0..tilesHeight) {
1241     foreach (int tileX; 0..tilesWidth) {
1242       auto t = tiles[tileX, tileY];
1244       if (t && !t.isInstanceAlive) {
1245         delete tiles[tileX, tileY];
1246         t = none;
1247       }
1249       if (!t) {
1250         if (global.lake && tileY >= GreatLakeStartTileY) {
1251           // fill level with water for lake
1252           MakeMapTile(tileX, tileY, 'oWaterSwim');
1253           t = tiles[tileX, tileY];
1254         } else {
1255           continue;
1256         }
1257       }
1259       if (!t.water && !t.lava) {
1260         // mark as wet for lake
1261         if (global.lake && tileY >= GreatLakeStartTileY) {
1262           t.wet = true;
1263         }
1264         continue;
1265       }
1267       if (!isWetTileAtTile(tileX, tileY-1)) {
1268         t.setSprite(t.lava ? 'sLavaTop' : 'sWaterTop');
1269       } else {
1270              if (t.spriteName == 'sWaterTop') t.setSprite('sWater');
1271         else if (t.spriteName == 'sLavaTop') t.setSprite('sLava');
1272       }
1273     }
1274   }
1278 private final void checkWaterFlow (MapTile wtile) {
1279   //if (!wtile || (!wtile.water && !wtile.lava)) return;
1280   //instance_activate_region(x-16, y-16, 48, 48, true);
1282   //int x = wtile.ix, y = wtile.iy;
1283   int tileX = wtile.ix/16, tileY = wtile.iy/16;
1285   if (global.lake && tileY >= GreatLakeStartTileY) return;
1287   /*
1288   if ((not collision_point(x-16, y, oSolid, 0, 0) and not collision_point(x-16, y, oWater, 0, 0)) or
1289       (not collision_point(x+16, y, oSolid, 0, 0) and not collision_point(x+16, y, oWater, 0, 0)) or
1290       (not collision_point(x, y+16, oSolid, 0, 0) and not collision_point(x, y+16, oWater, 0, 0)))
1291   */
1292   if (!isWetOrSolidTileAtTile(tileX-1, tileY) ||
1293       !isWetOrSolidTileAtTile(tileX+1, tileY) ||
1294       !isWetOrSolidTileAtTile(tileX, tileY+1))
1295   {
1296     checkWater = true;
1297     wtile.smashMe();
1298     wtile.instanceRemove();
1299     wtile.onDestroy();
1300     delete wtile;
1301     tiles[tileX, tileY] = none;
1302     return;
1303   }
1305   //if (!isSolidAtPoint(x, y-16) && !isLiquidAtPoint(x, y-16)) {
1306   if (!isWetTileAtTile(tileX, tileY-1)) {
1307     wtile.setSprite(wtile.lava ? 'sLavaTop' : 'sWaterTop');
1308   }
1312 transient private array!MapTile waterTilesToCheck;
1314 final void cleanDeadTiles () {
1315   bool hasWater = false;
1316   waterTilesToCheck.length -= waterTilesToCheck.length;
1317   foreach (int y; 0..tilesHeight) {
1318     foreach (int x; 0..tilesWidth) {
1319       auto t = tiles[x, y];
1320       if (!t) continue;
1321       if (t.isInstanceAlive) {
1322         if (t.water || t.lava) waterTilesToCheck[$] = t;
1323         continue;
1324       }
1325       checkWater = true;
1326       t.onDestroy();
1327       delete t;
1328       tiles[x, y] = none;
1329     }
1330   }
1331   if (waterTilesToCheck.length && checkWater && !blockWaterChecking) {
1332     //writeln("checking water");
1333     checkWater = false; // `checkWaterFlow()` can set it again
1334     foreach (MapTile t; waterTilesToCheck) {
1335       if (t && t.isInstanceAlive && (t.water || t.lava)) checkWaterFlow(t);
1336     }
1337     // fill empty spaces in lake with water
1338     if (global.lake) {
1339       foreach (int y; GreatLakeStartTileY..tilesHeight) {
1340         foreach (int x; 0..tilesWidth) {
1341           auto t = tiles[x, y];
1342           // just in case
1343           if (t && !t.isInstanceAlive) {
1344             t.onDestroy();
1345             delete tiles[x, y];
1346             t = none;
1347           }
1348           if (t) {
1349             if (!t.water || !t.lava) { t.wet = true; continue; }
1350           } else {
1351             MakeMapTile(x, y, 'oWaterSwim');
1352             t = tiles[x, y];
1353           }
1354           if (t.water) {
1355             t.setSprite(y == GreatLakeStartTileY ? 'sWaterTop' : 'sWater');
1356           } else if (t.lava) {
1357             t.setSprite(y == GreatLakeStartTileY ? 'sLavaTop' : 'sLava');
1358           }
1359         }
1360       }
1361     }
1362   }
1366 // ////////////////////////////////////////////////////////////////////////// //
1367 void collectLavaTiles () {
1368   lavatiles.length -= lavatiles.length;
1369   foreach (MapTile t; tiles) {
1370     if (t && t.lava && t.isInstanceAlive) lavatiles[$] = t;
1371   }
1375 void processLavaTiles () {
1376   int tn = 0, tlen = lavatiles.length;
1377   while (tn < tlen) {
1378     MapTile t = lavatiles[tn];
1379     if (t && t.isInstanceAlive) {
1380       t.thinkFrame();
1381       ++tn;
1382     } else {
1383       lavatiles.remove(tn, 1);
1384       --tlen;
1385     }
1386   }
1390 // ////////////////////////////////////////////////////////////////////////// //
1391 // return `true` if thinker should be removed
1392 final bool thinkOne (MapObject o) {
1393   if (!o) return true;
1394   if (o.active && o.isInstanceAlive) {
1395     bool doThink = true;
1397     // collision with player weapon
1398     auto hh = PlayerWeapon(player.holdItem);
1399     bool doWeaponAction;
1400     if (hh) {
1401       if (hh.blockedBySolids) {
1402         int xx = player.ix+(player.dir == MapObject::Dir.Left ? -16 : 16);
1403         doWeaponAction = !isSolidAtPoint(xx, player.iy);
1404       } else {
1405         doWeaponAction = true;
1406       }
1407     } else {
1408       doWeaponAction = false;
1409     }
1411     if (doWeaponAction && o.whipTimer <= 0 && hh && hh.collidesWithObject(o)) {
1412       //writeln("WEAPONED!");
1413       if (!o.onTouchedByPlayerWeapon(player, hh)) {
1414         if (o.isInstanceAlive) hh.onCollisionWithObject(o);
1415       }
1416       o.whipTimer = o.whipTimerValue; //HACK
1417       doThink = o.isInstanceAlive;
1418     }
1420     // collision with player
1421     if (doThink && o.collidesWith(player)) {
1422       if (!player.onObjectTouched(o) && o.isInstanceAlive) {
1423         doThink = !o.onTouchedByPlayer(player);
1424         if (o.grid && o.gridId) o.grid.update(o.gridId, markAsDead:true);
1425       }
1426     }
1428     if (doThink && o.isInstanceAlive) {
1429       o.saveInterpData();
1430       o.processAlarms();
1431       if (o.isInstanceAlive) {
1432         if (o.grid && o.gridId) o.grid.update(o.gridId, markAsDead:true);
1433         o.thinkFrame();
1434         if (o.isInstanceAlive) {
1435           o.nextAnimFrame();
1436           if (o.grid && o.gridId) o.grid.update(o.gridId, markAsDead:true);
1437         }
1438       }
1439     }
1440   }
1441   if (o.isInstanceAlive) {
1442     if (!o.canLiveOutsideOfLevel && !o.heldBy && o.isOutsideOfLevel()) {
1443       //dead
1444       o.instanceRemove();
1445       return true;
1446     }
1447     // alive
1448     return false;
1449   } else {
1450     // dead
1451     return true;
1452   }
1456 final void processThinkers (float timeDelta) {
1457   if (timeDelta <= 0) return;
1458   if (gamePaused) {
1459     if (onBeforeFrame) onBeforeFrame(false);
1460     if (onAfterFrame) onAfterFrame(false);
1461     keysNextFrame();
1462     return;
1463   }
1464   accumTime += timeDelta;
1465   bool wasFrame = false;
1466   // block GC
1467   auto olddel = ImmediateDelete;
1468   ImmediateDelete = false;
1469   while (accumTime >= FrameTime) {
1470     accumTime -= FrameTime;
1471     if (onBeforeFrame) onBeforeFrame(accumTime >= FrameTime);
1472     // shake
1473     if (shakeLeft > 0) {
1474       --shakeLeft;
1475       if (!shakeDir.x) shakeDir.x = (global.randOther(0, 1) ? -3 : 3);
1476       if (!shakeDir.y) shakeDir.y = (global.randOther(0, 1) ? -3 : 3);
1477       shakeOfs.x = shakeDir.x;
1478       shakeOfs.y = shakeDir.y;
1479       int sgnc = global.randOther(1, 3);
1480       if (sgnc&0x01) shakeDir.x = -shakeDir.x;
1481       if (sgnc&0x02) shakeDir.y = -shakeDir.y;
1482     } else {
1483       shakeOfs.x = 0;
1484       shakeOfs.y = 0;
1485       shakeDir.x = 0;
1486       shakeDir.y = 0;
1487     }
1488     // game-global events
1489     thinkFrameGame();
1490     // frame thinkers: lava tiles
1491     processLavaTiles();
1492     // frame thinkers: player
1493     if (player && !disablePlayerThink) {
1494       // time limit
1495       if (!player.dead && isNormalLevel() &&
1496           (maxPlayingTime < 0 ||
1497            (maxPlayingTime > 0 && maxPlayingTime*30 <= time &&
1498             time%30 == 0 && global.randOther(1, 100) <= 20)))
1499       {
1500         MakeMapObject(player.ix, player.iy, 'oExplosion');
1501         player.scrCreateFlame(player.ix, player.iy, 3);
1502       }
1503       //HACK: check for stolen items
1504       auto item = MapItem(player.holdItem);
1505       if (item) item.onCheckItemStolen(player);
1506       item = MapItem(player.pickedItem);
1507       if (item) item.onCheckItemStolen(player);
1508       // normal thinking
1509       player.saveInterpData();
1510       player.processAlarms();
1511       if (player.isInstanceAlive) {
1512         player.thinkFrame();
1513         if (player.isInstanceAlive) player.nextAnimFrame();
1514       }
1515     }
1516     // frame thinkers: moveable solids
1517     physStep();
1518     // frame thinkers: objects
1519     auto grid = objGrid;
1520     // collect active objects
1521     if (global.config.useFrozenRegion) {
1522       activeThinkerList.length -= activeThinkerList.length;
1523       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)) {
1524         activeThinkerList[$] = o;
1525       }
1526       //writeln("thinkers: ", activeThinkerList.length);
1527       foreach (MapObject o; activeThinkerList) {
1528         if (thinkOne(o)) {
1529           grid.remove(o.gridId);
1530           o.onDestroy();
1531           delete o;
1532         }
1533       }
1534     } else {
1535       // no frozen area
1536       bool killThisOne = false;
1537       for (int cid = grid.getFirstObject(); cid; cid = grid.getNextObject(cid, killThisOne)) {
1538         killThisOne = false;
1539         MapObject o = grid.getObject(MapObject, cid);
1540         if (!o) { killThisOne = true; continue; }
1541         // remove this object if it is dead
1542         if (thinkOne(o)) {
1543           killThisOne = true;
1544           if (o) {
1545             o.onDestroy();
1546             delete o;
1547           }
1548         }
1549       }
1550     }
1551     if (player && player.holdItem) {
1552       if (player.holdItem.isInstanceAlive) {
1553         player.holdItem.fixHoldCoords();
1554       } else {
1555         player.holdItem = none;
1556       }
1557     }
1558     // done with thinkers
1559     cleanDeadTiles();
1560     // money counter
1561     if (collectCounter == 0) {
1562       xmoney = max(0, xmoney-100);
1563     } else {
1564       --collectCounter;
1565     }
1566     // other things
1567     if (player && !player.dead) stats.oneMoreFramePlayed();
1568     if (onAfterFrame) onAfterFrame(accumTime >= FrameTime);
1569     keysNextFrame();
1570     wasFrame = true;
1571     if (!player.visible && player.holdItem) player.holdItem.visible = false;
1572     if (winCutsceneSwitchToNext) {
1573       winCutsceneSwitchToNext = false;
1574       switch (++inWinCutscene) {
1575         case 2: startWinCutsceneVolcano(); break;
1576         case 3: default: startWinCutsceneWinFall(); break;
1577       }
1578       break;
1579     }
1580     if (playerExited) break;
1581   }
1582   ImmediateDelete = olddel;
1583   if (playerExited) {
1584     playerExited = false;
1585     onLevelExited();
1586   }
1587   if (wasFrame) {
1588     // if we were processed at least one frame, collect garbage
1589     //keysNextFrame();
1590     CollectGarbage(true); // destroy delayed objects too
1591   }
1592   if (accumTime > 0 && onInterFrame) onInterFrame(accumTime/FrameTime);
1596 // ////////////////////////////////////////////////////////////////////////// //
1597 final void tileXY2roomXY (int tileX, int tileY, out int roomX, out int roomY) {
1598   roomX = (tileX-1)/RoomGen::Width;
1599   roomY = (tileY-1)/RoomGen::Height;
1603 final bool isInShop (int tileX, int tileY) {
1604   if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
1605     auto n = roomType[tileX, tileY];
1606     if (n == 4 || n == 5) return true;
1607     auto t = getTileAt(tileX, tileY);
1608     if (t && t.shopWall) return true;
1609     //k8: we don't have this
1610     //if (t && t.objType == 'oShop') return true;
1611   }
1612   return false;
1616 // ////////////////////////////////////////////////////////////////////////// //
1617 override void Destroy () {
1618   clearTiles();
1619   clearObjects();
1620   delete tempSolidTile;
1621   ::Destroy();
1625 // ////////////////////////////////////////////////////////////////////////// //
1626 final MapObject findNearestBall (int px, int py) {
1627   MapObject res = none;
1628   int curdistsq = int.max;
1629   foreach (MapObject o; ballObjects) {
1630     if (!o || o.spectral || !o.isInstanceAlive) continue;
1631     int xc = px-o.xCenter, yc = py-o.yCenter;
1632     int distsq = xc*xc+yc*yc;
1633     if (distsq < curdistsq) {
1634       res = o;
1635       curdistsq = distsq;
1636     }
1637   }
1638   return res;
1642 final int calcNearestBallDist (int px, int py) {
1643   auto e = findNearestBall(px, py);
1644   if (!e) return int.max;
1645   int xc = px-e.xCenter, yc = py-e.yCenter;
1646   return round(sqrt(xc*xc+yc*yc));
1650 final MapObject findNearestObject (int px, int py, bool delegate (MapObject o) dg) {
1651   MapObject res = none;
1652   int curdistsq = int.max;
1653   foreach (MapObject o; objGrid.allObjects()) {
1654     if (o.spectral || !o.isInstanceAlive) continue;
1655     if (!dg(o)) continue;
1656     int xc = px-o.xCenter, yc = py-o.yCenter;
1657     int distsq = xc*xc+yc*yc;
1658     if (distsq < curdistsq) {
1659       res = o;
1660       curdistsq = distsq;
1661     }
1662   }
1663   return res;
1667 final MapObject findNearestEnemy (int px, int py, optional bool delegate (MapEnemy o) dg) {
1668   MapObject res = none;
1669   int curdistsq = int.max;
1670   foreach (MapObject o; objGrid.allObjects()) {
1671     //k8: i added `dead` check
1672     if (o.spectral || o !isa MapEnemy || o.dead || !o.isInstanceAlive) continue;
1673     if (dg) {
1674       if (!dg(MapEnemy(o))) continue;
1675     }
1676     int xc = px-o.xCenter, yc = py-o.yCenter;
1677     int distsq = xc*xc+yc*yc;
1678     if (distsq < curdistsq) {
1679       res = o;
1680       curdistsq = distsq;
1681     }
1682   }
1683   return res;
1687 final MonsterShopkeeper hasAliveShopkeepers (optional bool skipAngry) {
1688   foreach (MapObject o; objGrid.allObjects()) {
1689     auto sc = MonsterShopkeeper(o);
1690     if (!sc || o.spectral || !o.isInstanceAlive) continue;
1691     if (sc.dead) continue;
1692     if (skipAngry && sc.angered) continue;
1693     return sc;
1694   }
1695   return none;
1699 final int calcNearestEnemyDist (int px, int py, optional bool delegate (MapEnemy o) dg) {
1700   auto e = findNearestEnemy(px, py, dg!optional);
1701   if (!e) return int.max;
1702   int xc = px-e.xCenter, yc = py-e.yCenter;
1703   return round(sqrt(xc*xc+yc*yc));
1707 final int calcNearestObjectDist (int px, int py, optional bool delegate (MapObject o) dg) {
1708   auto e = findNearestObject(px, py, dg!optional);
1709   if (!e) return int.max;
1710   int xc = px-e.xCenter, yc = py-e.yCenter;
1711   return round(sqrt(xc*xc+yc*yc));
1715 final MapTile findNearestMoveableSolid (int px, int py, optional bool delegate (MapTile t) dg) {
1716   MapTile res = none;
1717   int curdistsq = int.max;
1718   foreach (MapTile t; miscTileGrid.allObjects()) {
1719     if (t.spectral || !t.isInstanceAlive) continue;
1720     if (dg) {
1721       if (!dg(t)) continue;
1722     } else {
1723       if (!t.solid || !t.moveable) continue;
1724     }
1725     int xc = px-t.xCenter, yc = py-t.yCenter;
1726     int distsq = xc*xc+yc*yc;
1727     if (distsq < curdistsq) {
1728       res = t;
1729       curdistsq = distsq;
1730     }
1731   }
1732   return res;
1736 final MapTile findNearestTile (int px, int py, optional bool delegate (MapTile t) dg) {
1737   if (!dg) return none;
1738   MapTile res = none;
1739   int curdistsq = int.max;
1741   //FIXME: make this faster!
1742   foreach (MapTile t; tiles) {
1743     if (!t || t.spectral || !t.isInstanceAlive) continue;
1744     int xc = px-t.xCenter, yc = py-t.yCenter;
1745     int distsq = xc*xc+yc*yc;
1746     if (distsq < curdistsq && dg(t)) {
1747       res = t;
1748       curdistsq = distsq;
1749     }
1750   }
1752   foreach (MapTile t; miscTileGrid.allObjects()) {
1753     if (!t || t.spectral || !t.isInstanceAlive) continue;
1754     int xc = px-t.xCenter, yc = py-t.yCenter;
1755     int distsq = xc*xc+yc*yc;
1756     if (distsq < curdistsq && dg(t)) {
1757       res = t;
1758       curdistsq = distsq;
1759     }
1760   }
1762   return res;
1766 // ////////////////////////////////////////////////////////////////////////// //
1767 final bool cbIsObjectWeb (MapObject o) { return (o isa ItemWeb); }
1768 final bool cbIsObjectBlob (MapObject o) { return (o.objName == 'oBlob'); }
1769 final bool cbIsObjectArrow (MapObject o) { return (o isa ItemProjectileArrow); }
1770 final bool cbIsObjectEnemy (MapObject o) { return (/*o != self &&*/ o isa MapEnemy); }
1772 final bool cbIsObjectTreasure (MapObject o) { return (o.isTreasure); }
1774 final bool cbIsItemObject (MapObject o) { return (o isa MapItem); }
1776 final bool cbIsItemOrEnemyObject (MapObject o) { return (o isa MapItem || o isa MapEnemy); }
1779 final MapObject isObjectAtTile (int tileX, int tileY, optional bool delegate (MapObject o) dg) {
1780   tileX *= 16;
1781   tileY *= 16;
1782   foreach (MapObject o; objGrid.inRectPix(tileX, tileY, 16, 16, objGrid.nextTag(), precise: true)) {
1783     if (o.spectral || !o.isInstanceAlive) continue;
1784     if (dg) {
1785       if (dg(o)) return o;
1786     } else {
1787       return o;
1788     }
1789   }
1790   return none;
1794 final MapObject isObjectAtTilePix (int x, int y, optional bool delegate (MapObject o) dg) {
1795   return isObjectAtTile(x/16, y/16, dg!optional);
1799 final MapObject isObjectAtPoint (int xpos, int ypos, optional bool delegate (MapObject o) dg, optional bool precise) {
1800   if (!specified_precise) precise = true;
1801   foreach (MapObject o; objGrid.inCellPix(xpos, ypos, objGrid.nextTag(), precise: precise)) {
1802     if (o.spectral || !o.isInstanceAlive) continue;
1803     if (dg) {
1804       if (dg(o)) return o;
1805     } else {
1806       if (o isa MapEnemy) return o;
1807     }
1808   }
1809   return none;
1813 final MapObject isObjectInRect (int xpos, int ypos, int w, int h, optional bool delegate (MapObject o) dg, optional bool precise) {
1814   if (w < 1 || h < 1) return none;
1815   if (w == 1 && h == 1) return isObjectAtPoint(xpos, ypos, dg!optional, precise!optional);
1816   if (!specified_precise) precise = true;
1817   foreach (MapObject o; objGrid.inRectPix(xpos, ypos, w, h, objGrid.nextTag(), precise: precise)) {
1818     if (o.spectral || !o.isInstanceAlive) continue;
1819     if (dg) {
1820       if (dg(o)) return o;
1821     } else {
1822       if (o isa MapEnemy) return o;
1823     }
1824   }
1825   return none;
1829 final MapObject forEachObject (bool delegate (MapObject o) dg, optional bool allowSpectrals) {
1830   if (!dg) return none;
1831   /*
1832   foreach (MapObject o; objGrid.allObjects()) {
1833     if (o.spectral || !o.isInstanceAlive) continue;
1834     if (dg(o)) return o;
1835   }
1836   */
1837   // process gravity for moveable solids and burning for ropes
1838   auto grid = objGrid;
1839   int cid = grid.getFirstObject();
1840   while (cid) {
1841     MapObject o = grid.getObject(MapObject, cid);
1842     if (!o || !o.isInstanceAlive) {
1843       cid = grid.getNextObject(cid, removeThis:true);
1844       continue;
1845     }
1846     if (!allowSpectrals && o.spectral) {
1847       cid = grid.getNextObject(cid, removeThis:false);
1848       continue;
1849     }
1850     if (dg(o)) return o;
1851     if (o.isInstanceAlive) {
1852       cid = grid.getNextObject(cid, removeThis:false);
1853     } else {
1854       cid = grid.getNextObject(cid, removeThis:true);
1855       o.instanceRemove(); // just in case
1856       o.onDestroy();
1857       delete o;
1858     }
1859   }
1860   return none;
1864 final MapObject forEachObjectAtPoint (int xpos, int ypos, bool delegate (MapObject o) dg, optional bool precise) {
1865   if (!dg) return none;
1866   if (!specified_precise) precise = true;
1867   foreach (MapObject o; objGrid.inCellPix(xpos, ypos, objGrid.nextTag(), precise: precise)) {
1868     if (o.spectral || !o.isInstanceAlive) continue;
1869     if (dg(o)) return o;
1870   }
1871   return none;
1875 final MapObject forEachObjectInRect (int xpos, int ypos, int w, int h, bool delegate (MapObject o) dg, optional bool precise) {
1876   if (!dg || w < 1 || h < 1) return none;
1877   if (w == 1 && h == 1) return forEachObjectAtPoint(xpos, ypos, dg!optional, precise!optional);
1878   if (!specified_precise) precise = true;
1879   foreach (MapObject o; objGrid.inRectPix(xpos, ypos, w, h, objGrid.nextTag(), precise: precise)) {
1880     if (o.spectral || !o.isInstanceAlive) continue;
1881     if (dg(o)) return o;
1882   }
1883   return none;
1887 private final bool cbIsRopeTile (MapTile t) { return t.rope; }
1889 final MapTile isRopeAtPoint (int px, int py) {
1890   return checkTileAtPoint(px, py, &cbIsRopeTile);
1894 //FIXME!
1895 final MapTile isWaterSwimAtPoint (int px, int py) {
1896   return isWaterAtPoint(px, py);
1900 // ////////////////////////////////////////////////////////////////////////// //
1901 private array!MapObject tmpObjectList;
1903 private final bool cbCollectObjectsWithMask (MapObject t) {
1904   if (!t || !t.visible || t.spectral /*|| t.invincible*/ || !t.isInstanceAlive) return false;
1905   //auto spf = getSpriteFrame();
1906   //if (!t.sprite || t.sprite.frames.length < 1) return false;
1907   tmpObjectList[$] = t;
1908   return false;
1912 final void touchObjectsWithMask (int x, int y, SpriteFrame frm, bool delegate (MapObject t) dg) {
1913   if (!frm || frm.bw < 1 || frm.bh < 1 || !dg) return;
1914   if (frm.isEmptyPixelMask) return;
1915   // collect tiles
1916   if (tmpObjectList.length) tmpObjectList.length -= tmpObjectList.length; // don't realloc
1917   if (player.isRectCollisionFrame(frm, x, y)) {
1918     //writeln("player hit");
1919     tmpObjectList[$] = player;
1920   } else {
1921     /*
1922     writeln("no player hit: plr=(", player.ix, ",", player.iy, ")-(", player.ix+player.width-1, ",", player.iy+player.height-1, "); ",
1923       "frm=(", x+frm.bx, ",", y+frm.by, ")-(", x+frm.bx+frm.bw-1, ",", y+frm.by+frm.bh-1, ")");
1924     */
1925   }
1926   forEachObjectInRect(x+frm.bx, y+frm.by, frm.bw, frm.bh, &cbCollectObjectsWithMask);
1927   foreach (MapObject t; tmpObjectList) {
1928     if (!t || !t.isInstanceAlive || !t.visible || t.spectral) continue;
1929     /*
1930     auto tf = t.getSpriteFrame();
1931     if (!tf) {
1932       //writeln("no sprite frame for ", GetClassName(t.Class));
1933       continue;
1934     }
1935     */
1936     /*
1937     if (frm.pixelCheck(tf, t.ix-tf.xofs-x, t.iy-tf.yofs-y)) {
1938       //writeln("pixel hit for ", GetClassName(t.Class));
1939       if (dg(t)) break;
1940     }
1941     */
1942     if (t.isRectCollisionFrame(frm, x, y)) {
1943       if (dg(t)) break;
1944     }
1945   }
1949 // ////////////////////////////////////////////////////////////////////////// //
1950 final void destroyTileAt (int x, int y) {
1951   if (x < 0 || y < 0 || x >= tilesWidth*16 || y >= tilesHeight*16) return;
1952   x /= 16;
1953   y /= 16;
1954   MapTile t = tiles[x, y];
1955   if (!t || !t.visible || t.spectral || t.invincible || !t.isInstanceAlive) return;
1956   t.instanceRemove();
1957   t.onDestroy();
1958   delete tiles[x, y];
1959   checkWater = true;
1963 private array!MapTile tmpTileList;
1965 private final bool cbCollectTilesWithMask (MapTile t) {
1966   if (!t || !t.visible || t.spectral /*|| t.invincible*/ || !t.isInstanceAlive) return false;
1967   if (!t.sprite || t.sprite.frames.length < 1) return false;
1968   tmpTileList[$] = t;
1969   return false;
1972 final void touchTilesWithMask (int x, int y, SpriteFrame frm, bool delegate (MapTile t) dg) {
1973   if (!frm || frm.bw < 1 || frm.bh < 1 || !dg) return;
1974   if (frm.isEmptyPixelMask) return;
1975   // collect tiles
1976   if (tmpTileList.length) tmpTileList.length -= tmpTileList.length; // don't realloc
1977   checkTilesInRect(x+frm.bx, y+frm.by, frm.bw, frm.bh, &cbCollectTilesWithMask);
1978   foreach (MapTile t; tmpTileList) {
1979     if (!t || !t.isInstanceAlive || !t.visible || t.spectral) continue;
1980     /*
1981     auto tf = t.sprite.frames[0];
1982     if (frm.pixelCheck(tf, t.ix-x, t.iy-y)) {
1983       if (dg(t)) break;
1984       //doCleanup = doCleanup || !t.isInstanceAlive;
1985       //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, ")");
1986     }
1987     */
1988     if (t.isRectCollisionFrame(frm, x, y)) {
1989       if (dg(t)) break;
1990     }
1991   }
1995 // ////////////////////////////////////////////////////////////////////////// //
1996 final bool cbCollisionAnySolid (MapTile t) { return t.solid; }
1997 final bool cbCollisionAnyMoveableSolid (MapTile t) { return t.solid && t.moveable; }
1998 final bool cbCollisionLiquid (MapTile t) { return (t.water || t.lava); }
1999 final bool cbCollisionAnySolidOrLiquid (MapTile t) { return (t.solid || t.water || t.lava); }
2000 final bool cbCollisionLadder (MapTile t) { return t.ladder; }
2001 final bool cbCollisionLadderTop (MapTile t) { return t.laddertop; }
2002 final bool cbCollisionAnyLadder (MapTile t) { return (t.ladder || t.laddertop); }
2003 final bool cbCollisionAnySolidIce (MapTile t) { return (t.solid && t.ice); }
2004 final bool cbCollisionWater (MapTile t) { return t.water; }
2005 final bool cbCollisionLava (MapTile t) { return t.lava; }
2006 final bool cbCollisionAnyTree (MapTile t) { return t.tree; }
2007 final bool cbCollisionPlatform (MapTile t) { return t.platform; }
2008 final bool cbCollisionLadderOrPlatform (MapTile t) { return (t.ladder || t.platform); }
2009 //final bool cbCollisionAnyHangeable (MapTile t) { return t.hangeable; } // no need to check for solids here, it is done in MapTile initializer
2010 final bool cbCollisionSpringTrap (MapTile t) { return t.springtrap; }
2011 final bool cbCollisionSpikes (MapTile t) { return t.spikes; }
2012 final bool cbCollisionExitTile (MapTile t) { return t.isExitActive(); }
2014 final bool cbCollisionForWhoa (MapTile t) { return (t.solid || t.laddertop); }
2016 //final bool cbCollisionSacAltar (MapTile t) { return (t.objName == 'oSacAltarLeft'); } // only left part, yeah
2017 final bool cbCollisionSacAltar (MapTile t) { return t.sacrificingAltar; }
2020 // ////////////////////////////////////////////////////////////////////////// //
2021 transient MapTile tempSolidTile;
2023 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*/) {
2024   //!if (dbgdump) writeln("checkTilesInRect: (", x0, ",", y0, ")-(", x0+w-1, ",", y0+h-1, ") ; w=", w, "; h=", h);
2025   if (w < 1 || h < 1) return none;
2026   if (w == 1 && h == 1) return checkTileAtPoint(x0, y0, dg!optional);
2027   int x1 = x0+w-1, y1 = y0+h-1;
2028   if (x0 >= tilesWidth*16 || y0 >= tilesHeight*16 || x1 < 0 || y1 < 0) return none;
2029   if (!dg) {
2030     //!if (dbgdump) writeln("default checker set");
2031     dg = &cbCollisionAnySolid;
2032   }
2033   //!if (dbgdump) writeln("delegate: ", dg);
2034   int origx0 = x0, origy0 = y0;
2035   int tileSX = max(0, x0)/16;
2036   int tileSY = max(0, y0)/16;
2037   int tileEX = min(tilesWidth*16-1, x1)/16;
2038   int tileEY = min(tilesHeight*16-1, y1)/16;
2039   //!if (dbgdump) writeln("  tiles: (", tileSX, ",", tileSY, ")-(", tileEX, ",", tileEY, ")");
2040   //!!!auto grid = miscTileGrid;
2041   //!!!int tag = grid.nextTag();
2042   for (int ty = tileSY; ty <= tileEY; ++ty) {
2043     for (int tx = tileSX; tx <= tileEX; ++tx) {
2044       MapTile t = tiles[tx, ty];
2045       //!if (dbgdump && t && t.visible && t.isInstanceAlive && t.isRectCollision(origx0, origy0, w, h) ) writeln("   tile: ", GetClassName(t.Class), " : ", t.objName, " : ", t.objType, " : ", dg(t));
2046       if (t && !t.spectral && t.visible && t.isInstanceAlive && t.isRectCollision(origx0, origy0, w, h) && dg(t)) return t;
2047       // moveable tiles are in separate grid
2048       /+
2049       foreach (t; grid.inCellPix(tx*16, ty*16, tag, precise:precise)) {
2050         //if (t.isRectCollision(origx0, origy0, w, h) && dg(t)) return t;
2051         if (t.isInstanceAlive && !t.spectral && t.visible && t.isRectCollision(origx0, origy0, w, h) && dg(t)) return t;
2052       }
2053       +/
2054     }
2055   }
2057   // moveable tiles are in separate grid
2058   foreach (MapTile t; miscTileGrid.inRectPix(x0, y0, w, h, miscTileGrid.nextTag(), precise:precise)) {
2059     //if (t.isRectCollision(origx0, origy0, w, h) && dg(t)) return t;
2060     if (t.isInstanceAlive && !t.spectral && t.visible && t.isRectCollision(origx0, origy0, w, h) && dg(t)) return t;
2061   }
2063   // check walkable solid objects
2064   foreach (MapObject o; objGrid.inRectPix(x0, y0, w, h, objGrid.nextTag(), precise:precise)) {
2065     if (o && !o.spectral && o.visible && o.walkableSolid && o.isInstanceAlive && o.isRectCollision(origx0, origy0, w, h)) {
2066       if (!tempSolidTile) {
2067         tempSolidTile = SpawnObject(MapTile);
2068       } else if (!tempSolidTile.isInstanceAlive) {
2069         delete tempSolidTile;
2070         tempSolidTile = SpawnObject(MapTile);
2071       }
2072       tempSolidTile.solid = true;
2073       if (dg(tempSolidTile)) return tempSolidTile;
2074     }
2075   }
2077   return none;
2081 final MapTile checkTileAtPoint (int x0, int y0, optional bool delegate (MapTile dg) dg) {
2082   if (x0 < 0 || y0 < 0 || x0 >= tilesWidth*16 || y0 >= tilesHeight*16) return none;
2083   if (!dg) dg = &cbCollisionAnySolid;
2084   //if (!self) { writeln("WTF?!"); return none; }
2085   MapTile t = tiles[x0/16, y0/16];
2086   if (t && !t.spectral && t.visible && t.isInstanceAlive && t.isPointCollision(x0, y0) && dg(t)) return t;
2088   // moveable tiles are in separate grid
2089   foreach (t; miscTileGrid.inCellPix(x0, y0, miscTileGrid.nextTag(), precise:true)) {
2090     if (t.isInstanceAlive && !t.spectral && t.visible && dg(t)) return t;
2091   }
2093   // check walkable solid objects
2094   foreach (MapObject o; objGrid.inCellPix(x0, y0, objGrid.nextTag(), precise:true)) {
2095     if (o && !o.spectral && o.visible && o.walkableSolid && o.isInstanceAlive && o.isRectCollision(x0, y0, 1, 1)) {
2096       if (!tempSolidTile) {
2097         tempSolidTile = SpawnObject(MapTile);
2098       } else if (!tempSolidTile.isInstanceAlive) {
2099         delete tempSolidTile;
2100         tempSolidTile = SpawnObject(MapTile);
2101       }
2102       tempSolidTile.solid = true;
2103       if (dg(tempSolidTile)) return tempSolidTile;
2104     }
2105   }
2107   return none;
2111 //FIXME: optimize this with clipping first
2112 //TODO: moveable tiles
2114 final MapTile checkTilesAtLine (int ax0, int ay0, int ax1, int ay1, optional bool delegate (MapTile dg) dg) {
2115   // do it faster if we can
2117   // strict vertical check?
2118   if (ax0 == ax1 && ay0 <= ay1) return checkTilesInRect(ax0, ay0, 1, ay1-ay0+1, dg!optional);
2119   // strict horizontal check?
2120   if (ay0 == ay1 && ax0 <= ax1) return checkTilesInRect(ax0, ay0, ax1-ax0+1, 1, dg!optional);
2122   float x0 = float(ax0)/16.0, y0 = float(ay0)/16.0, x1 = float(ax1)/16.0, y1 = float(ay1)/16.0;
2124   // fix delegate
2125   if (!dg) dg = &cbCollisionAnySolid;
2127   // get starting and enging tile
2128   int tileSX = trunc(x0), tileSY = trunc(y0);
2129   int tileEX = trunc(x1), tileEY = trunc(y1);
2131   // first hit is always landed
2132   if (tileSX >= 0 && tileSY >= 0 && tileSX < TilesWidth && tileSY < TilesHeight) {
2133     MapTile t = tiles[tileSX, tileSY];
2134     if (t && t.visible && Geom.lineAABBIntersects(ax0, ay0, ax1, ay1, tileSX*16, tileSY*16, 16, 16) && dg(t)) return t;
2135   }
2137   // if starting and ending tile is the same, we don't need to do anything more
2138   if (tileSX == tileEX && tileSY == tileEY) return none;
2140   // calculate ray direction
2141   TVec dv = (vector(x1, y1)-vector(x0, y0)).normalise2d;
2143   // length of ray from one x or y-side to next x or y-side
2144   float deltaDistX = (fabs(dv.x) > 0.0001 ? fabs(1.0/dv.x) : 0.0);
2145   float deltaDistY = (fabs(dv.y) > 0.0001 ? fabs(1.0/dv.y) : 0.0);
2147   // calculate step and initial sideDists
2149   float sideDistX; // length of ray from current position to next x-side
2150   int stepX; // what direction to step in x (either +1 or -1)
2151   if (dv.x < 0) {
2152     stepX = -1;
2153     sideDistX = (x0-tileSX)*deltaDistX;
2154   } else {
2155     stepX = 1;
2156     sideDistX = (tileSX+1.0-x0)*deltaDistX;
2157   }
2159   float sideDistY; // length of ray from current position to next y-side
2160   int stepY; // what direction to step in y (either +1 or -1)
2161   if (dv.y < 0) {
2162     stepY = -1;
2163     sideDistY = (y0-tileSY)*deltaDistY;
2164   } else {
2165     stepY = 1;
2166     sideDistY = (tileSY+1.0-y0)*deltaDistY;
2167   }
2169   // perform DDA
2170   //int side; // was a NS or a EW wall hit?
2171   for (;;) {
2172     // jump to next map square, either in x-direction, or in y-direction
2173     if (sideDistX < sideDistY) {
2174       sideDistX += deltaDistX;
2175       tileSX += stepX;
2176       //side = 0;
2177     } else {
2178       sideDistY += deltaDistY;
2179       tileSY += stepY;
2180       //side = 1;
2181     }
2182     // check tile
2183     if (tileSX >= 0 && tileSY >= 0 && tileSX < TilesWidth && tileSY < TilesHeight) {
2184       MapTile t = tiles[tileSX, tileSY];
2185       if (t && t.visible && Geom.lineAABBIntersects(ax0, ay0, ax1, ay1, tileSX*16, tileSY*16, 16, 16) && dg(t)) return t;
2186     }
2187     // did we arrived at the destination?
2188     if (tileSX == tileEX && tileSY == tileEY) break;
2189   }
2191   return none;
2196 // ////////////////////////////////////////////////////////////////////////// //
2197 final MapTile isSolidAtPoint (int px, int py) { return checkTileAtPoint(px, py); }
2198 final MapTile isSolidInRect (int rx, int ry, int rw, int rh) { return checkTilesInRect(rx, ry, rw, rh); }
2199 final MapTile isLadderAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadder); }
2200 final MapTile isLadderOrPlatformAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadderOrPlatform); }
2201 final MapTile isLadderTopAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadderTop); }
2202 final MapTile isAnyLadderAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyLadder); }
2203 final MapTile isWaterAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionWater); }
2204 final MapTile isLavaAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLava); }
2205 final MapTile isLiquidAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLiquid); }
2206 final MapTile isIceAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnySolidIce); }
2207 final MapTile isTreeAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyTree); }
2208 //final MapTile isHangeableAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyHangeable); }
2211 // ////////////////////////////////////////////////////////////////////////// //
2212 // PlayerPawn has it's own movement code, so don't process it here
2213 // but process moveable solids here, yeah
2214 final void physStep () {
2215   // advance time
2216   time += 1;
2217   // we don't want the time to grow too large
2218   if (time > 100000000) time = 0;
2220   auto grid = miscTileGrid;
2222   // process gravity for moveable solids and burning for ropes
2223   int cid = grid.getFirstObject();
2224   while (cid) {
2225     MapTile t = grid.getObject(MapTile, cid);
2226     if (!t) {
2227       cid = grid.getNextObject(cid, removeThis:false);
2228       continue;
2229     }
2230     if (t.isInstanceAlive) {
2231       t.saveInterpData();
2232       t.processAlarms();
2233       if (t.isInstanceAlive) {
2234         grid.update(cid, markAsDead:false);
2235         t.thinkFrame();
2236         if (t.isInstanceAlive && !t.canLiveOutsideOfLevel && t.isOutsideOfLevel()) t.instanceRemove();
2237         grid.update(cid, markAsDead:false);
2238       }
2239     }
2240     if (t.isInstanceAlive) {
2241       cid = grid.getNextObject(cid, removeThis:false);
2242     } else {
2243       cid = grid.getNextObject(cid, removeThis:true);
2244       t.instanceRemove(); // just in case
2245       t.onDestroy();
2246       delete t;
2247       checkWater = true;
2248     }
2249   }
2253 // ////////////////////////////////////////////////////////////////////////// //
2254 final void setTileTypeAt (int tileX, int tileY, LevelGen::RType rt) {
2255   if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) roomType[tileX, tileY] = rt;
2258 final MapTile getTileAt (int tileX, int tileY) {
2259   return (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight ? tiles[tileX, tileY] : none);
2262 final bool isNamedTileAt (int tileX, int tileY, name atypename) {
2263   if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
2264     auto t = tiles[tileX, tileY];
2265     if (t && t.objName == atypename) return true;
2266   }
2267   return false;
2270 final void setTileAt (int tileX, int tileY, MapTile tile) {
2271   if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
2272     //FIXME
2273     if (tiles[tileX, tileY]) checkWater = true;
2274     delete tiles[tileX, tileY];
2275     tiles[tileX, tileY] = tile;
2276   }
2280 // ////////////////////////////////////////////////////////////////////////// //
2281 // return `true` from delegate to stop
2282 MapTile forEachSolidTile (bool delegate (int x, int y, MapTile t) dg) {
2283   if (!dg) return none;
2284   foreach (int y; 0..tilesHeight) {
2285     foreach (int x; 0..tilesWidth) {
2286       auto t = tiles[x, y];
2287       if (t && t.solid && t.visible && t.isInstanceAlive) {
2288         if (dg(x, y, t)) return t;
2289       }
2290     }
2291   }
2292   return none;
2296 // ////////////////////////////////////////////////////////////////////////// //
2297 // return `true` from delegate to stop
2298 MapTile forEachNormalTile (bool delegate (int x, int y, MapTile t) dg) {
2299   if (!dg) return none;
2300   foreach (int y; 0..tilesHeight) {
2301     foreach (int x; 0..tilesWidth) {
2302       auto t = tiles[x, y];
2303       if (t && t.visible && t.isInstanceAlive) {
2304         if (dg(x, y, t)) return t;
2305       }
2306     }
2307   }
2308   return none;
2312 // WARNING! don't destroy tiles here! (instanceRemove() is ok, tho)
2313 MapTile forEachTile (bool delegate (MapTile t) dg) {
2314   if (!dg) return none;
2315   foreach (int y; 0..tilesHeight) {
2316     foreach (int x; 0..tilesWidth) {
2317       auto t = tiles[x, y];
2318       if (t && t.visible && !t.spectral && t.isInstanceAlive) {
2319         if (dg(t)) return t;
2320       }
2321     }
2322   }
2323   foreach (MapObject o; miscTileGrid.allObjects()) {
2324     auto mt = MapTile(o);
2325     if (!mt) continue;
2326     if (mt.visible && !mt.spectral && mt.isInstanceAlive) {
2327       //writeln("special map tile: '", GetClassName(mt.Class), "'");
2328       if (dg(mt)) return mt;
2329     }
2330   }
2331   return none;
2335 // ////////////////////////////////////////////////////////////////////////// //
2336 final void fixWallTiles () {
2337   foreach (int y; 0..tilesHeight) {
2338     foreach (int x; 0..tilesWidth) {
2339       auto t = getTileAt(x, y);
2340       if (!t) continue;
2341       /*
2342       if (y == tilesHeight-2) {
2343         writeln("0: tx=", x, "; tile=", t.objName, "; ty=", y, ": (", t.iy/16, ")");
2344       } else if (y == tilesHeight-1) {
2345         writeln("1: tx=", x, "; tile=", t.objName, "; ty=", y, ": (", t.iy/16, ")");
2346       }
2347       */
2348       t.beautifyTile();
2349     }
2350   }
2351   foreach (MapTile t; miscTileGrid.allObjects()) {
2352     if (t.isInstanceAlive) t.beautifyTile();
2353   }
2357 // ////////////////////////////////////////////////////////////////////////// //
2358 final MapTile isCollisionAtPoint (int px, int py, optional bool delegate (MapTile dg) dg) {
2359   if (!dg) dg = &cbCollisionAnySolid;
2360   return checkTilesInRect(px, py, 1, 1, dg);
2364 // ////////////////////////////////////////////////////////////////////////// //
2365 string scrGetKaliGift (MapTile altar, optional name gift) {
2366   string res;
2368   // find other side of the altar
2369   int sx = player.ix, sy = player.iy;
2370   if (altar) {
2371     sx = altar.ix;
2372     sy = altar.iy;
2373     auto a2 = isCollisionAtPoint(altar.ix+18, altar.iy+8, &cbCollisionSacAltar);
2374     if (!a2) a2 = isCollisionAtPoint(altar.ix-8, altar.iy+8, &cbCollisionSacAltar);
2375     if (a2) { sx = a2.ix; sy = a2.iy; }
2376   }
2378        if (global.favor <= -8) res = "SHE SEEMS VERY ANGRY WITH YOU!";
2379   else if (global.favor < 0) res = "SHE SEEMS ANGRY WITH YOU.";
2380   else if (global.favor == 0) res = "SHE HAS FORGIVEN YOU!";
2381   else if (global.favor >= 32) {
2382     if (global.kaliGift >= 3 && global.favor >= 32+(global.kaliGift-2)*16) {
2383       res = "YOU FEEL INVIGORATED!";
2384       global.kaliGift += 1;
2385       global.plife += global.randOther(4, 8);
2386     } else if (global.kaliGift >= 3) {
2387       res = "SHE SEEMS ECSTATIC WITH YOU!";
2388     } else if (global.bombs < 80) {
2389       res = "YOUR SATCHEL FEELS VERY FULL NOW!";
2390       global.kaliGift = 3;
2391       global.bombs = 99;
2392     } else {
2393       res = "YOU FEEL INVIGORATED!";
2394       global.kaliGift += 1;
2395       global.plife += global.randOther(4, 8);
2396     }
2397   } else if (global.favor >= 16) {
2398     if (global.kaliGift >= 2) {
2399       res = "SHE SEEMS VERY HAPPY WITH YOU!";
2400     } else {
2401       res = "SHE BESTOWS A GIFT UPON YOU!";
2402       global.kaliGift = 2;
2403       // poofs
2404       auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2405       obj.xVel = -1;
2406       obj.yVel = 0;
2407       obj = MakeMapObject(sx, sy-8, 'oPoof');
2408       obj.xVel = 1;
2409       obj.yVel = 0;
2410       // a gift
2411       obj = none;
2412       if (gift) obj = MakeMapObject(sx, sy-8, gift);
2413       if (!obj) obj = MakeMapObject(sx, sy-8, 'oKapala');
2414     }
2415   } else if (global.favor >= 8) {
2416     if (global.kaliGift >= 1) {
2417       res = "SHE SEEMS HAPPY WITH YOU.";
2418     } else {
2419       res = "SHE BESTOWS A GIFT UPON YOU!";
2420       global.kaliGift = 1;
2421       //rAltar = instance_nearest(x, y, oSacAltarRight);
2422       //if (instance_exists(rAltar)) {
2423       // poofs
2424       auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2425       obj.xVel = -1;
2426       obj.yVel = 0;
2427       obj = MakeMapObject(sx, sy-8, 'oPoof');
2428       obj.xVel = 1;
2429       obj.yVel = 0;
2430       obj = none;
2431       if (gift) obj = MakeMapObject(sx, sy-8, gift);
2432       if (!obj) {
2433         auto n = global.randOther(1, 8);
2434         auto m = n;
2435         for (;;) {
2436           name aname = '';
2437                if (n == 1 && !global.hasCape && !global.hasJetpack) aname = 'oCapePickup';
2438           else if (n == 2 && !global.hasGloves) aname = 'oGloves';
2439           else if (n == 3 && !global.hasSpectacles) aname = 'oSpectacles';
2440           else if (n == 4 && !global.hasMitt) aname = 'oMitt';
2441           else if (n == 5 && !global.hasSpringShoes) aname = 'oSpringShoes';
2442           else if (n == 6 && !global.hasSpikeShoes) aname = 'oSpikeShoes';
2443           else if (n == 7 && !global.hasStickyBombs) aname = 'oPaste';
2444           else if (n == 8 && !global.hasCompass) aname = 'oCompass';
2445           if (aname) {
2446             obj = MakeMapObject(sx, sy-8, aname);
2447             if (obj) break;
2448           }
2449           ++n;
2450           if (n > 8) n = 1;
2451           if (n == m) {
2452             obj = MakeMapObject(sx, sy-8, (global.hasJetpack ? 'oBombBox' : 'oJetpack'));
2453             break;
2454           }
2455         }
2456       }
2457     }
2458   } else if (global.favor > 0) {
2459     res = "SHE SEEMS PLEASED WITH YOU.";
2460   }
2462   /*
2463   if (argument1) {
2464     global.message = "";
2465     res = "KALI DEVOURS YOU!"; // sacrifice is player
2466   }
2467   */
2469   return res;
2473 void performSacrifice (MapObject what, MapTile where) {
2474   if (!what || !what.isInstanceAlive) return;
2475   MakeMapObject(what.ix+8, what.iy+8, 'oFlame');
2476   if (where) where.playSound('sndSmallExplode'); else what.playSound('sndSmallExplode');
2477   if (!what.bloodless) what.scrCreateBlood(what.ix+8, what.iy+8, 3);
2479   string msg = "KALI ACCEPTS THE SACRIFICE!";
2481   auto idol = ItemGoldIdol(what);
2482   if (idol) {
2483     ++stats.totalSacrifices;
2484          if (global.favor <= -8) msg = "SHE SEEMS VERY ANGRY WITH YOU!";
2485     else if (global.favor < 0) msg = "SHE SEEMS ANGRY WITH YOU.";
2486     else if (global.favor >= 0) {
2487       // find other side of the altar
2488       int sx = player.ix, sy = player.iy;
2489       auto altar = where;
2490       if (altar) {
2491         sx = altar.ix;
2492         sy = altar.iy;
2493         auto a2 = isCollisionAtPoint(altar.ix+18, altar.iy+8, &cbCollisionSacAltar);
2494         if (!a2) a2 = isCollisionAtPoint(altar.ix-8, altar.iy+8, &cbCollisionSacAltar);
2495         if (a2) { sx = a2.ix; sy = a2.iy; }
2496       }
2497       // poofs
2498       auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2499       obj.xVel = -1;
2500       obj.yVel = 0;
2501       obj = MakeMapObject(sx, sy-8, 'oPoof');
2502       obj.xVel = 1;
2503       obj.yVel = 0;
2504       // a gift
2505       obj = MakeMapObject(sx, sy-16-8, 'oMonkeyGold');
2506     }
2507     osdMessage(msg, 6.66);
2508     scrShake(10);
2509     idol.instanceRemove();
2510     return;
2511   }
2513   if (global.favor <= -8) {
2514     msg = "KALI DEVOURS THE SACRIFICE!";
2515   } else if (global.favor < 0) {
2516     global.favor += (what.status == MapObject::STUNNED ? what.favor : round(what.favor/2.0)); //k8: added `round()`
2517     if (what.favor > 0) what.favor = 0;
2518   } else {
2519     global.favor += (what.status == MapObject::STUNNED ? what.favor : round(what.favor/2.0)); //k8: added `round()`
2520   }
2522   /*!!
2523        if (myAltar.gift1 != "" and global.kaliGift == 0) scrGetKaliGift(myAltar.gift1);
2524   else if (myAltar.gift2 != "" and global.kaliGift == 1) scrGetKaliGift(myAltar.gift2);
2525   else scrGetKaliGift("");
2526   */
2528   // sacrifice is player?
2529   if (what isa PlayerPawn) {
2530     ++stats.totalSelfSacrifices;
2531     msg = "KALI DEVOURS YOU!";
2532     player.visible = false;
2533     player.removeBallAndChain(temp:true);
2534     player.dead = true;
2535     player.status = MapObject::DEAD;
2536   } else {
2537     ++stats.totalSacrifices;
2538     auto msg2 = scrGetKaliGift(where);
2539     what.instanceRemove();
2540     if (msg2) msg = va("%s\n%s", msg, msg2);
2541   }
2543   osdMessage(msg, 6.66);
2545   //!if (isRealLevel()) global.totalSacrifices += 1;
2547   //!global.messageTimer = 200;
2548   //!global.shake = 10;
2549   scrShake(10);
2551   /*damsel
2552   instance_create(x, y, oFlame);
2553   playSound(global.sndSmallExplode);
2554   scrCreateBlood(x, y, 3);
2555   global.message = "KALI ACCEPTS YOUR SACRIFICE!";
2556   if (global.favor <= -8) {
2557     global.message = "KALI DEVOURS YOUR SACRIFICE!";
2558   } else if (global.favor < 0) {
2559     if (status == 98) global.favor += favor*1.5; else global.favor += favor;
2560     if (favor > 0) favor = 0;
2561   } else {
2562     if (status == 98) global.favor += favor*1.5; else global.favor += favor;
2563   }
2565        if (myAltar.gift1 != "" and global.kaliGift == 0) scrGetFavorMsg(myAltar.gift1);
2566   else if (myAltar.gift2 != "" and global.kaliGift == 1) scrGetFavorMsg(myAltar.gift2);
2567   else scrGetFavorMsg("");
2569   global.messageTimer = 200;
2570   global.shake = 10;
2571   instance_destroy();
2572   */
2576 // ////////////////////////////////////////////////////////////////////////// //
2577 final void addBackgroundGfxDetails () {
2578   // add background details
2579   //if (global.customLevel || global.parallax) return;
2580   foreach (; 0..20) {
2581     // bg = instance_create(16*randRoom(1, 42), 16*randRoom(1, 33), oCaveBG);
2582          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);
2583     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);
2584     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);
2585     else MakeMapBackTile('bgExtras', 32*global.randRoom(0, 1), 0, 32, 32, 16*global.randRoom(1, 42), 16*global.randRoom(1, 33), 10002);
2586   }
2590 // ////////////////////////////////////////////////////////////////////////// //
2591 private final void fixRealViewStart () {
2592   int scale = global.scale;
2593   realViewStart.x = clamp(realViewStart.x, viewMin.x*scale, viewMax.x*scale-viewWidth);
2594   realViewStart.y = clamp(realViewStart.y, viewMin.y*scale, viewMax.y*scale-viewHeight);
2598 final int cameraCurrX () { return realViewStart.x/global.scale; }
2599 final int cameraCurrY () { return realViewStart.y/global.scale; }
2602 private final void fixViewStart () {
2603   int scale = global.scale;
2604   viewStart.x = clamp(viewStart.x, viewMin.x*scale, viewMax.x*scale-viewWidth);
2605   viewStart.y = clamp(viewStart.y, viewMin.y*scale, viewMax.y*scale-viewHeight);
2609 final void centerViewAtPlayer () {
2610   if (viewWidth < 1 || viewHeight < 1 || !player) return;
2611   centerViewAt(player.xCenter, player.yCenter);
2615 final void centerViewAt (int x, int y) {
2616   if (viewWidth < 1 || viewHeight < 1) return;
2618   cameraSlideToSpeed.x = 0;
2619   cameraSlideToSpeed.y = 0;
2620   cameraSlideToPlayer = 0;
2622   int scale = global.scale;
2623   x *= scale;
2624   y *= scale;
2625   realViewStart.x = clamp(x-viewWidth/2, 0, tilesWidth*16*scale-viewWidth);
2626   realViewStart.y = clamp(y-viewHeight/2, 0, tilesHeight*16*scale-viewHeight);
2627   fixRealViewStart();
2629   viewStart.x = realViewStart.x;
2630   viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
2631   fixViewStart();
2635 const int ViewPortToleranceX = 16*1+8;
2636 const int ViewPortToleranceY = 16*1+8;
2638 final void fixCamera () {
2639   if (!player) return;
2640   if (viewWidth < 1 || viewHeight < 1) return;
2641   int scale = global.scale;
2642   auto alwaysCenterX = global.config.alwaysCenterPlayer;
2643   auto alwaysCenterY = alwaysCenterX;
2644   // calculate offset from viewport center (in game units), and fix viewport
2646   int camDestX = player.ix+8;
2647   int camDestY = player.iy+8;
2648   if (!cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y) != 0) {
2649     // slide camera to point
2650     if (cameraSlideToSpeed.x) camDestX = cameraSlideToCurr.x;
2651     if (cameraSlideToSpeed.y) camDestY = cameraSlideToCurr.y;
2652     int dx = cameraSlideToDest.x-camDestX;
2653     int dy = cameraSlideToDest.y-camDestY;
2654     //writeln("speed:(", cameraSlideToSpeed.x, ",", cameraSlideToSpeed.y, "); cur:(", camDestX, ",", camDestY, "); dest:(", cameraSlideToDest.x, ",", cameraSlideToDest.y, "); delta=(", dx, ",", dy, ")");
2655     if (dx && cameraSlideToSpeed.x != 0) {
2656       alwaysCenterX = true;
2657       if (abs(dx) <= abs(cameraSlideToSpeed.x)) {
2658         camDestX = cameraSlideToDest.x;
2659       } else {
2660         camDestX += sign(dx)*abs(cameraSlideToSpeed.x);
2661       }
2662     }
2663     if (dy && abs(cameraSlideToSpeed.y) != 0) {
2664       alwaysCenterY = true;
2665       if (abs(dy) <= cameraSlideToSpeed.y) {
2666         camDestY = cameraSlideToDest.y;
2667       } else {
2668         camDestY += sign(dy)*abs(cameraSlideToSpeed.y);
2669       }
2670     }
2671     //writeln("  new:(", camDestX, ",", camDestY, ")");
2672     if (cameraSlideToSpeed.x) cameraSlideToCurr.x = camDestX;
2673     if (cameraSlideToSpeed.y) cameraSlideToCurr.y = camDestY;
2674   }
2676   // horizontal
2677   if (cameraSlideToSpeed.x && !cameraSlideToPlayer) {
2678     realViewStart.x = clamp(camDestX*scale, 0, tilesWidth*16*scale-viewWidth);
2679   } else if (!player.cameraBlockX) {
2680     int x = camDestX*scale;
2681     int cx = realViewStart.x;
2682     if (alwaysCenterX) {
2683       cx = x-viewWidth/2;
2684     } else {
2685       int xofs = x-(cx+viewWidth/2);
2686            if (xofs < -ViewPortToleranceX*scale) cx += xofs+ViewPortToleranceX*scale;
2687       else if (xofs > ViewPortToleranceX*scale) cx += xofs-ViewPortToleranceX*scale;
2688     }
2689     // slide back to player?
2690     if (cameraSlideToPlayer && cameraSlideToSpeed.x) {
2691       int prevx = cameraSlideToCurr.x*scale;
2692       int dx = (cx-prevx)/scale;
2693       if (abs(dx) <= cameraSlideToSpeed.x) {
2694         writeln("BACKSLIDE X COMPLETE!");
2695         cameraSlideToSpeed.x = 0;
2696       } else {
2697         cx = prevx+(sign(dx)*cameraSlideToSpeed.x)*scale;
2698         cameraSlideToCurr.x += sign(dx)*cameraSlideToSpeed.x;
2699         if ((dx < 0 && cx <= 0) || (dx > 0 && cx >= tilesWidth*16*scale-viewWidth)) {
2700           writeln("BACKSLIDE X COMPLETE!");
2701           cameraSlideToSpeed.x = 0;
2702         }
2703       }
2704     }
2705     realViewStart.x = clamp(cx, 0, tilesWidth*16*scale-viewWidth);
2706   }
2708   // vertical
2709   if (cameraSlideToSpeed.y && !cameraSlideToPlayer) {
2710     realViewStart.y = clamp(camDestY*scale, 0, tilesHeight*16*scale-viewHeight);
2711   } else if (!player.cameraBlockY) {
2712     int y = camDestY*scale;
2713     int cy = realViewStart.y;
2714     if (alwaysCenterY) {
2715       cy = y-viewHeight/2;
2716     } else {
2717       int yofs = y-(cy+viewHeight/2);
2718            if (yofs < -ViewPortToleranceY*scale) cy += yofs+ViewPortToleranceY*scale;
2719       else if (yofs > ViewPortToleranceY*scale) cy += yofs-ViewPortToleranceY*scale;
2720     }
2721     // slide back to player?
2722     if (cameraSlideToPlayer && cameraSlideToSpeed.y) {
2723       int prevy = cameraSlideToCurr.y*scale;
2724       int dy = (cy-prevy)/scale;
2725       if (abs(dy) <= cameraSlideToSpeed.y) {
2726         writeln("BACKSLIDE Y COMPLETE!");
2727         cameraSlideToSpeed.y = 0;
2728       } else {
2729         cy = prevy+(sign(dy)*cameraSlideToSpeed.y)*scale;
2730         cameraSlideToCurr.y += sign(dy)*cameraSlideToSpeed.y;
2731         if ((dy < 0 && cy <= 0) || (dy > 0 && cy >= tilesHeight*16*scale-viewHeight)) {
2732           writeln("BACKSLIDE Y COMPLETE!");
2733           cameraSlideToSpeed.y = 0;
2734         }
2735       }
2736     }
2737     realViewStart.y = clamp(cy, 0, tilesHeight*16*scale-viewHeight);
2738   }
2740   if (cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y) == 0) cameraSlideToPlayer = 0;
2742   fixRealViewStart();
2743   //writeln("  new2:(", cameraCurrX, ",", cameraCurrY, ")");
2745   viewStart.x = realViewStart.x;
2746   viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
2747   fixViewStart();
2751 // ////////////////////////////////////////////////////////////////////////// //
2752 // x0 and y0 are non-scaled (and will be scaled)
2753 final void drawSpriteAt (name sprName, float frnumf, int x0, int y0, optional bool small) {
2754   if (!sprName) return;
2755   auto spr = sprStore[sprName];
2756   if (!spr || !spr.frames.length) return;
2757   int scale = global.scale;
2758   x0 *= scale;
2759   y0 *= scale;
2760   int frnum = max(0, trunc(frnumf))%spr.frames.length;
2761   auto sfr = spr.frames[frnum];
2762   int sx0 = x0-sfr.xofs*scale;
2763   int sy0 = y0-sfr.yofs*scale;
2764   if (small && scale > 1) {
2765     sfr.tex.blitExt(sx0, sy0, round(sx0+sfr.tex.width*(scale/2.0)), round(sy0+sfr.tex.height*(scale/2.0)), 0, 0);
2766   } else {
2767     sfr.tex.blitAt(sx0, sy0, scale);
2768   }
2772 // x0 and y0 are non-scaled (and will be scaled)
2773 final void drawTextAt (int x0, int y0, string text) {
2774   if (!text) return;
2775   int scale = global.scale;
2776   x0 *= scale;
2777   y0 *= scale;
2778   sprStore.renderText(x0, y0, text, scale);
2782 void renderCompass (float currFrameDelta) {
2783   if (!global.hasCompass) return;
2785   /*
2786   if (isRoom("rOlmec")) {
2787     global.exitX = 648;
2788     global.exitY = 552;
2789   } else if (isRoom("rOlmec2")) {
2790     global.exitX = 648;
2791     global.exitY = 424;
2792   }
2793   */
2795   bool hasMessage = osdHasMessage();
2796   foreach (MapTile et; allExits) {
2797     // original compass
2798     int exitX = et.ix, exitY = et.iy;
2799     int vx0 = viewStart.x/global.scale, vy0 = viewStart.y/global.scale;
2800     int vx1 = (viewStart.x+viewWidth)/global.scale;
2801     int vy1 = (viewStart.y+viewHeight)/global.scale;
2802     if (exitY > vy1-16) {
2803       if (exitX < vx0) {
2804         drawSpriteAt(hasMessage ? 'sCompassSmallLL' : 'sCompassLL', 0, 0, 224);
2805       } else if (exitX > vx1-16) {
2806         drawSpriteAt(hasMessage ? 'sCompassSmallLR' : 'sCompassLR', 0, 304, 224);
2807       } else {
2808         drawSpriteAt(hasMessage ? 'sCompassSmallDown' : 'sCompassDown', 0, exitX-vx0, 224);
2809       }
2810     } else if (exitX < vx0) {
2811       drawSpriteAt(hasMessage ? 'sCompassSmallLeft' : 'sCompassLeft', 0, 0, exitY-vy0);
2812     } else if (exitX > vx1-16) {
2813       drawSpriteAt(hasMessage ? 'sCompassSmallRight' : 'sCompassRight', 0, 304, exitY-vy0);
2814     }
2815   }
2819 final bool totalsNameCmpCB (ref GameStats::TotalItem a, ref GameStats::TotalItem b) {
2820   auto sa = string(a.objName);
2821   auto sb = string(b.objName);
2822   return (sa < sb);
2825 void renderTransitionInfo (float currFrameDelta) {
2826   //FIXME!
2827   /*
2828   GameStats.sortTotalsList(stats.kills, &totalsNameCmpCB);
2830   int maxLen = 0;
2831   foreach (int idx, ref auto k; stats.kills) {
2832     string s = string(k);
2833     maxLen = max(maxLen, s.length);
2834   }
2835   maxLen *= 8;
2837   sprStore.loadFont('sFontSmall');
2838   Video.color = 0xff_ff_00;
2839   foreach (int idx, ref auto k; stats.kills) {
2840     int deaths = 0;
2841     foreach (int xidx, ref auto d; stats.totalKills) {
2842       if (d.objName == k) { deaths = d.count; break; }
2843     }
2844     //drawTextAt(16, 4+idx*8, va("%s: %d (%d)", string(k.objName).toUpperCase, k.count, deaths));
2845     drawTextAt(16, 4+idx*8, string(k).toUpperCase);
2846     drawTextAt(16+maxLen, 4+idx*8, va(": %d (%d)", k.count, deaths));
2847   }
2848   */
2852 void renderGhostTimer (float currFrameDelta) {
2853   if (ghostTimeLeft <= 0) return;
2854   //ghostTimeLeft /= 30; // frames -> seconds
2856   int hgt = Video.screenHeight-64;
2857   if (hgt < 1) return;
2858   int rhgt = round(float(hgt)*(float(ghostTimeLeft)/30.0)/1000.0);
2859   //writeln("hgt=", hgt, "; rhgt=", rhgt, "; gtl=", ghostTimeLeft/30);
2860   if (rhgt > 0) {
2861     auto oclr = Video.color;
2862     Video.color = 0xcf_ff_7f_00;
2863     Video.fillRect(Video.screenWidth-20, 32, 16, hgt-rhgt);
2864     Video.color = 0x7f_ff_7f_00;
2865     Video.fillRect(Video.screenWidth-20, 32+(hgt-rhgt), 16, rhgt);
2866     Video.color = oclr;
2867   }
2871 void renderHUD (float currFrameDelta) {
2872   if (inWinCutscene) return;
2874   if (levelKind == LevelKind.Transition) { renderTransitionInfo(currFrameDelta); return; }
2876   int lifeX = 4; // 8
2877   int bombX = 56;
2878   int ropeX = 104;
2879   int ammoX = 152;
2880   int moneyX = 200;
2881   int hhup;
2882   bool scumSmallHud = global.config.scumSmallHud;
2883   if (!global.config.optSGAmmo) moneyX = ammoX;
2885   if (scumSmallHud) {
2886     sprStore.loadFont('sFontSmall');
2887     hhup = 6;
2888   } else {
2889     sprStore.loadFont('sFont');
2890     hhup = 0;
2891   }
2892   Video.color = 0xff_ff_ff;
2894   // hearts
2895   if (scumSmallHud) {
2896     if (global.plife == 1) {
2897       drawSpriteAt('sHeartSmallBlink', global.heartBlink, lifeX+2, 4-hhup);
2898       global.heartBlink += 0.1;
2899       if (global.heartBlink > 3) global.heartBlink = 0;
2900     } else {
2901       drawSpriteAt('sHeartSmall', -1, lifeX+2, 4-hhup);
2902       global.heartBlink = 0;
2903     }
2904   } else {
2905     if (global.plife == 1) {
2906       drawSpriteAt('sHeartBlink', global.heartBlink, lifeX, 8-hhup);
2907       global.heartBlink += 0.1;
2908       if (global.heartBlink > 3) global.heartBlink = 0;
2909     } else {
2910       drawSpriteAt('sHeart', -1, lifeX, 8-hhup);
2911       global.heartBlink = 0;
2912     }
2913   }
2915   int life = clamp(global.plife, 0, 99);
2916   //if (!scumHud && life > 99) life = 99;
2917   drawTextAt(lifeX+16, 8-hhup, va("%d", life));
2919   // bombs
2920   if (global.hasStickyBombs && global.stickyBombsActive) {
2921     if (scumSmallHud) drawSpriteAt('sStickyBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sStickyBombIcon', -1, bombX, 8-hhup);
2922   } else {
2923     if (scumSmallHud) drawSpriteAt('sBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sBombIcon', -1, bombX, 8-hhup);
2924   }
2925   int n = global.bombs;
2926   if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
2927   drawTextAt(bombX+16, 8-hhup, va("%d", n));
2929   // ropes
2930   if (scumSmallHud) drawSpriteAt('sRopeIconSmall', -1, ropeX+2, 4-hhup); else drawSpriteAt('sRopeIcon', -1, ropeX, 8-hhup);
2931   n = global.rope;
2932   if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
2933   drawTextAt(ropeX+16, 8-hhup, va("%d", n));
2935   // shotgun shells
2936   if (global.config.optSGAmmo) {
2937     if (scumSmallHud) drawSpriteAt('sShellsIcon', -1, ammoX+6, 8-hhup); else drawSpriteAt('sShellsIcon', -1, ammoX+7, 12-hhup);
2938     n = global.sgammo;
2939     if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
2940     drawTextAt(ammoX+16, 8-hhup, va("%d", n));
2941   }
2943   // money
2944   if (scumSmallHud) drawSpriteAt('sDollarSignSmall', -1, moneyX+2, 3-hhup); else drawSpriteAt('sDollarSign', -1, moneyX, 8-hhup);
2945   drawTextAt(moneyX+16, 8-hhup, va("%d", stats.money-xmoney));
2947   int ity = (scumSmallHud ? 18-hhup : 24-hhup);
2949   n = 8; //28;
2950   if (global.hasUdjatEye) {
2951     if (global.udjatBlink) drawSpriteAt('sUdjatEyeIcon2', -1, n, ity); else drawSpriteAt('sUdjatEyeIcon', -1, n, ity);
2952     n += 20;
2953   }
2954   if (global.hasAnkh) { drawSpriteAt('sAnkhIcon', -1, n, ity); n += 20; }
2955   if (global.hasCrown) { drawSpriteAt('sCrownIcon', -1, n, ity); n += 20; }
2956   if (global.hasKapala) {
2957          if (global.bloodLevel == 0) drawSpriteAt('sKapalaIcon', 0, n, ity);
2958     else if (global.bloodLevel <= 2) drawSpriteAt('sKapalaIcon', 1, n, ity);
2959     else if (global.bloodLevel <= 4) drawSpriteAt('sKapalaIcon', 2, n, ity);
2960     else if (global.bloodLevel <= 6) drawSpriteAt('sKapalaIcon', 3, n, ity);
2961     else if (global.bloodLevel <= 8) drawSpriteAt('sKapalaIcon', 4, n, ity);
2962     n += 20;
2963   }
2964   if (global.hasSpectacles) { drawSpriteAt('sSpectaclesIcon', -1, n, ity); n += 20; }
2965   if (global.hasGloves) { drawSpriteAt('sGlovesIcon', -1, n, ity); n += 20; }
2966   if (global.hasMitt) { drawSpriteAt('sMittIcon', -1, n, ity); n += 20; }
2967   if (global.hasSpringShoes) { drawSpriteAt('sSpringShoesIcon', -1, n, ity); n += 20; }
2968   if (global.hasJordans) { drawSpriteAt('sJordansIcon', -1, n, ity); n += 20; }
2969   if (global.hasSpikeShoes) { drawSpriteAt('sSpikeShoesIcon', -1, n, ity); n += 20; }
2970   if (global.hasCape) { drawSpriteAt('sCapeIcon', -1, n, ity); n += 20; }
2971   if (global.hasJetpack) { drawSpriteAt('sJetpackIcon', -1, n, ity); n += 20; }
2972   if (global.hasStickyBombs) { drawSpriteAt('sPasteIcon', -1, n, ity); n += 20; }
2973   if (global.hasCompass) { drawSpriteAt('sCompassIcon', -1, n, ity); n += 20; }
2974   if (global.hasParachute) { drawSpriteAt('sParachuteIcon', -1, n, ity); n += 20; }
2976   if (player.holdItem && player.holdItem.objName == 'Bow' && global.arrows > 0) {
2977     int m = 1;
2978     float malpha = 1;
2979     while (m <= global.arrows && m <= 20 && malpha > 0) {
2980       Video.color = trunc(malpha*255)<<24|0xff_ff_ff;
2981       drawSpriteAt('sArrowIcon', -1, n, ity);
2982       n += 4;
2983       if (m > 10) malpha -= 0.1; //and global.arrows > 20 malpha -= 0.1
2984       m += 1;
2985     }
2986     Video.color = 0xff_ff_ff;
2987   }
2989   if (xmoney > 0) {
2990     sprStore.loadFont('sFontSmall');
2991     Video.color = 0xff_ff_00;
2992     if (scumSmallHud) drawTextAt(moneyX+8, 8+8-hhup, va("+%d", xmoney));
2993     else drawTextAt(moneyX, 8+16-hhup, va("+%d", xmoney));
2994   }
2996   if (global.config.ghostShowTime) renderGhostTimer(currFrameDelta);
3000 // ////////////////////////////////////////////////////////////////////////// //
3001 private transient array!MapEntity renderVisibleCids;
3002 private transient array!MapTile renderMidTiles, renderFrontTiles; // normal, with fg
3004 final bool renderSortByDepth (MapEntity oa, MapEntity ob) {
3005   //MapObject oa = MapObject(a);
3006   //MapObject ob = MapObject(b);
3007   auto da = oa.depth, db = ob.depth;
3008   if (da == db) return (oa.objId < ob.objId);
3009   return (da < db);
3013 const int RenderEdgePix = 32;
3015 void renderWithOfs (int xofs, int yofs, float currFrameDelta) {
3016   int scale = global.scale;
3017   int tsz = 16*scale;
3019   Video.color = 0xff_ff_ff;
3021   // render cave background
3022   if (levBGImg) {
3023     int bgw = levBGImg.tex.width*scale;
3024     int bgh = levBGImg.tex.height*scale;
3025     int bgTW = (tilesWidth*tsz+bgw-1)/bgw;
3026     int bgTH = (tilesHeight*tsz+bgh-1)/bgh;
3027     int bgX0 = max(0, xofs/bgw);
3028     int bgY0 = max(0, yofs/bgh);
3029     int bgX1 = min(bgTW, (xofs+viewWidth+bgw-1)/bgw);
3030     int bgY1 = min(bgTH, (yofs+viewHeight+bgh-1)/bgh);
3031     foreach (int ty; bgY0..bgY1) {
3032       foreach (int tx; bgX0..bgX1) {
3033         int x0 = tx*bgw-xofs;
3034         int y0 = ty*bgh-yofs;
3035         levBGImg.tex.blitAt(x0, y0, scale);
3036       }
3037     }
3038   }
3040   // render background tiles
3041   for (MapBackTile bt = backtiles; bt; bt = bt.next) {
3042     bt.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3043   }
3045   // collect visible objects
3046   renderVisibleCids.length -= renderVisibleCids.length;
3047   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)) {
3048     if (!mt.visible || !mt.isInstanceAlive) continue;
3049     //Video.color = (mt.moveable ? 0xff_7f_00 : 0xff_ff_ff);
3050     //!mt.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3051     renderVisibleCids[$] = mt;
3052   }
3053   // render objects (and player)
3054   if (player && player.visible) renderVisibleCids[$] = player; // include player in render list
3055   auto ogrid = objGrid;
3056   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)) {
3057     if (o.visible && o.isInstanceAlive) renderVisibleCids[$] = o;
3058   }
3060   // collect stationary tiles
3061   int tileX0 = max(0, xofs/tsz);
3062   int tileY0 = max(0, yofs/tsz);
3063   int tileX1 = min(tilesWidth, (xofs+viewWidth+tsz-1)/tsz);
3064   int tileY1 = min(tilesHeight, (yofs+viewHeight+tsz-1)/tsz);
3066   // render backs; collect tile arrays
3067   renderMidTiles.length -= renderMidTiles.length; // don't realloc
3068   renderFrontTiles.length -= renderFrontTiles.length; // don't realloc
3070   foreach (int ty; tileY0..tileY1) {
3071     foreach (int tx; tileX0..tileX1) {
3072       auto tile = getTileAt(tx, ty);
3073       if (tile && tile.visible && tile.isInstanceAlive) {
3074         renderMidTiles[$] = tile;
3075         if (tile.bgfront) renderFrontTiles[$] = tile;
3076         if (tile.bgback) tile.drawWithOfsBack(xofs, yofs, scale, currFrameDelta);
3077       }
3078     }
3079   }
3081   // render "mid" (i.e. normal) tiles
3082   foreach (MapTile tile; renderMidTiles) {
3083     //tile.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3084     renderVisibleCids[$] = tile;
3085   }
3087   EntityGrid.sortEntList(renderVisibleCids, &renderSortByDepth);
3090   auto depth4Start = 0;
3091   foreach (auto xidx, MapEntity o; renderVisibleCids) {
3092     if (o.depth >= 4) {
3093       depth4Start = xidx;
3094       break;
3095     }
3096   }
3098   foreach (auto idx; depth4Start..renderVisibleCids.length; reverse) {
3099     MapEntity o = renderVisibleCids[idx];
3100     //if (idx && renderSortByDepth(o, renderVisibleCids[idx-1])) writeln("VIOLATION!");
3101     o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3102   }
3104   // render front tile parts (depth 3.5)
3105   foreach (MapTile tile; renderFrontTiles) tile.drawWithOfsFront(xofs, yofs, scale, currFrameDelta);
3107   // render items with depth 3 and less
3108   foreach (auto idx; 0..depth4Start; reverse) {
3109     MapEntity o = renderVisibleCids[idx];
3110     o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3111   }
3113   renderVisibleCids.length -= renderVisibleCids.length;
3115   // render player post-effects, so cape or jetpack will be correctly rendered on level exit, for example
3116   player.lastDrawWithOfs(xofs, yofs, scale, currFrameDelta);
3118   if (global.config.drawHUD) renderHUD(currFrameDelta);
3119   renderCompass(currFrameDelta);
3121   float osdTimeLeft, osdTimeStart;
3122   string msg = osdGetMessage(out osdTimeLeft, out osdTimeStart);
3123   if (msg) {
3124     auto ct = GetTickCount();
3125     int msgScale = 3;
3126     sprStore.loadFont('sFontSmall');
3127     auto msgHeight = sprStore.getMultilineTextHeight(msg, msgScale);
3128     int x = Video.screenWidth/2;
3129     int y = Video.screenHeight-64-msgHeight;
3130     auto oldColor = Video.color;
3131     Video.color = 0xff_ff_00;
3132     if (osdTimeLeft < 0.5) {
3133       int alpha = 255-clamp(int(510*osdTimeLeft), 0, 255);
3134       Video.color = Video.color|(alpha<<24);
3135     } else if (ct-osdTimeStart < 0.5) {
3136       osdTimeStart = ct-osdTimeStart;
3137       int alpha = 255-clamp(int(510*osdTimeStart), 0, 255);
3138       Video.color = Video.color|(alpha<<24);
3139     }
3140     sprStore.renderMultilineTextCentered(x, y, msg, msgScale);
3141     Video.color = oldColor;
3142   }
3144   if (inWinCutscene) renderWinCutsceneOverlay();
3145   Video.color = 0xff_ff_ff;
3149 // ////////////////////////////////////////////////////////////////////////// //
3150 final class!MapObject findGameObjectClassByName (name aname) {
3151   if (!aname) return none; // just in case
3152   auto co = FindClassByGameObjName(aname);
3153   if (!co) {
3154     writeln("***UNKNOWN GAME OBJECT: '", aname, "'");
3155     return none;
3156   }
3157   co = GetClassReplacement(co);
3158   if (!co) FatalError("findGameObjectClassByName: WTF?!");
3159   if (!class!MapObject(co)) FatalError("findGameTileClassByName: object '%n' is not a map object, it is a `%n`", aname, GetClassName(co));
3160   return class!MapObject(co);
3164 final class!MapTile findGameTileClassByName (name aname) {
3165   if (!aname) return none; // just in case
3166   auto co = FindClassByGameObjName(aname);
3167   if (!co) return MapTile; // unknown names will be routed directly to tile object
3168   co = GetClassReplacement(co);
3169   if (!co) FatalError("findGameTileClassByName: WTF?!");
3170   if (!class!MapTile(co)) FatalError("findGameTileClassByName: object '%n' is not a tile, it is a `%n`", aname, GetClassName(co));
3171   return class!MapTile(co);
3175 final MapObject findAnyObjectOfType (name aname) {
3176   if (!aname) return none;
3177   auto cls = FindClassByGameObjName(aname);
3178   if (!cls) return none;
3179   for (auto cid = objGrid.getFirstObject(); cid; cid = objGrid.getNextObject(cid)) {
3180     MapObject obj = objGrid.getObject(MapObject, cid);
3181     if (!obj || obj.spectral || !obj.isInstanceAlive) continue;
3182     if (obj isa cls) return obj;
3183   }
3184   return none;
3188 // ////////////////////////////////////////////////////////////////////////// //
3189 final MapTile CreateMapTile (int xpos, int ypos, name aname) {
3190   if (!aname) FatalError("cannot create typeless tile");
3191   //MapTile tile = SpawnObject(aname == 'oRope' ? MapTileRope : MapTile);
3192   auto tclass = findGameTileClassByName(aname);
3193   if (!tclass) return none;
3194   MapTile tile = SpawnObject(tclass);
3195   tile.global = global;
3196   tile.level = self;
3197   tile.objName = aname;
3198   tile.objType = aname; // just in case
3199   tile.fltx = xpos;
3200   tile.flty = ypos;
3201   if (!tile.initialize()) FatalError("cannot create tile '%n'", aname);
3202   return tile;
3206 final bool isRopePlacedAt (int x, int y) {
3207   int[8] covered;
3208   foreach (ref auto v; covered) v = false;
3209   foreach (MapTile t; miscTileGrid.inRectPix(x, y-8, 1, 17, precise:false)) {
3210     if (!cbIsRopeTile(t)) continue;
3211     if (t.ix != x) continue;
3212     if (t.iy == y) return true;
3213     foreach (int ty; t.iy..t.iy+8) {
3214       int d = ty-y;
3215       if (d >= 0 && d < covered.length) covered[d] = true;
3216     }
3217   }
3218   // check if the whole rope height is completely covered with ropes
3219   foreach (auto v; covered) if (!v) return false;
3220   return true;
3224 // won't call `onDestroy()`
3225 final void RemoveMapTile (int tileX, int tileY) {
3226   if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
3227     if (tiles[tileX, tileY]) checkWater = true;
3228     delete tiles[tileX, tileY];
3229     tiles[tileX, tileY] = none;
3230   }
3234 final MapTile MakeMapTile (int mapx, int mapy, name aname/*, optional name sprname*/) {
3235   //if (mapy >= tilesHeight) { writeln("OOM: ", aname, "; ty=", mapy, "; hgt=", tilesHeight); }
3236   if (mapx < 0 || mapx >= tilesWidth || mapy < 0 || mapy >= tilesHeight) return none;
3238   // if we already have rope tile there, there is no reason to add another one
3239   if (aname == 'oRope') {
3240     if (isRopePlacedAt(mapx*16, mapy*16)) {
3241       //writeln("dupe rope (0)!");
3242       return none;
3243     }
3244   }
3246   auto tile = CreateMapTile(mapx*16, mapy*16, aname);
3247   if (tile.moveable || tile.toSpecialGrid) {
3248     // moveable tiles goes to the separate list
3249     miscTileGrid.insert(tile);
3250   } else {
3251     setTileAt(mapx, mapy, tile);
3252   }
3254   switch (aname) {
3255     case 'oEntrance': registerEnter(tile); break;
3256     case 'oExit': registerExit(tile); break;
3257   }
3259   return tile;
3263 final void MarkTileAsWet (int tileX, int tileY) {
3264   auto t = getTileAt(tileX, tileY);
3265   if (t) t.wet = true;
3269 final MapTile MakeMapTileAtPix (int xpix, int ypix, name aname/*, optional name sprname*/) {
3270   if (xpix%16 == 0 && ypix%16 == 0) return MakeMapTile(xpix/16, ypix/16, aname);
3271   //if (mapx < 0 || mapx >= TilesWidth || mapy < 0 || mapy >= TilesHeight) return none;
3273   // if we already have rope tile there, there is no reason to add another one
3274   if (aname == 'oRope') {
3275     if (isRopePlacedAt(xpix, ypix)) {
3276       //writeln("dupe rope (0)!");
3277       return none;
3278     }
3279   }
3281   auto tile = CreateMapTile(xpix, ypix, aname);
3282   // non-aligned tiles goes to the special grid
3283   miscTileGrid.insert(tile);
3285   switch (aname) {
3286     case 'oEntrance': registerEnter(tile); break;
3287     case 'oExit': registerExit(tile); break;
3288   }
3290   return tile;
3294 final MapTile MakeMapRopeTileAt (int x0, int y0) {
3295   // if we already have rope tile there, there is no reason to add another one
3296   if (isRopePlacedAt(x0, y0)) {
3297     //writeln("dupe rope (1)!");
3298     return none;
3299   }
3301   auto tile = CreateMapTile(x0, y0, 'oRope');
3302   miscTileGrid.insert(tile);
3304   return tile;
3308 // ////////////////////////////////////////////////////////////////////////// //
3309 final MapBackTile CreateBgTile (name sprName, optional int atx0, optional int aty0, optional int aw, optional int ah) {
3310   BackTileImage img = bgtileStore[sprName];
3311   auto res = SpawnObject(MapBackTile);
3312   res.global = global;
3313   res.level = self;
3314   res.bgt = img;
3315   res.bgtName = sprName;
3316   if (specified_atx0) res.tx0 = atx0;
3317   if (specified_aty0) res.ty0 = aty0;
3318   if (specified_aw) res.w = aw;
3319   if (specified_ah) res.h = ah;
3320   if (!res.initialize()) FatalError("cannot create background tile with image '%n'", sprName);
3321   return res;
3325 // ////////////////////////////////////////////////////////////////////////// //
3327 background The background asset from which the new tile will be extracted.
3328 left The x coordinate of the left of the new tile, relative to the background asset's top left corner.
3329 top The y coordinate of the top of the new tile, relative to the background assets top left corner.
3330 width The width of the tile.
3331 height The height of the tile.
3332 x The x position in the room to place the tile.
3333 y The y position in the room to place the tile.
3334 depth The depth at which to place the tile.
3336 final void MakeMapBackTile (name bgname, int left, int top, int width, int height, int x, int y, int depth) {
3337   if (width < 1 || height < 1 || !bgname) return;
3338   auto bgt = bgtileStore[bgname];
3339   if (!bgt) FatalError("cannot load background '%n'", bgname);
3340   MapBackTile bt = SpawnObject(MapBackTile);
3341   bt.global = global;
3342   bt.level = self;
3343   bt.objName = bgname;
3344   bt.bgt = bgt;
3345   bt.bgtName = bgname;
3346   bt.fltx = x;
3347   bt.flty = y;
3348   bt.tx0 = left;
3349   bt.ty0 = top;
3350   bt.w = width;
3351   bt.h = height;
3352   bt.depth = depth;
3353   // find a place for it
3354   if (!backtiles) {
3355     backtiles = bt;
3356     return;
3357   }
3358   // back tiles with the highest depth should come first
3359   MapBackTile ct = backtiles, cprev = none;
3360   while (ct && depth <= ct.depth) { cprev = ct; ct = ct.next; }
3361   // insert before ct
3362   if (cprev) {
3363     bt.next = cprev.next;
3364     cprev.next = bt;
3365   } else {
3366     bt.next = backtiles;
3367     backtiles = bt;
3368   }
3372 // ////////////////////////////////////////////////////////////////////////// //
3373 final MapObject SpawnMapObjectWithClass (class!MapObject oclass) {
3374   if (!oclass) return none;
3376   MapObject obj = SpawnObject(oclass);
3377   if (!obj) FatalError("cannot create map object '%n'", GetClassName(oclass));
3379   //if (aname == 'oShells' || aname == 'oShellSingle' || aname == 'oRopePile') writeln("*** CREATED '", aname, "'");
3381   obj.global = global;
3382   obj.level = self;
3384   return obj;
3388 final MapObject SpawnMapObject (name aname) {
3389   if (!aname) return none;
3390   return SpawnMapObjectWithClass(findGameObjectClassByName(aname));
3394 final MapObject PutSpawnedMapObject (int x, int y, MapObject obj) {
3395   if (!obj /*|| obj.global || obj.level*/) return none; // oops
3397   obj.fltx = x;
3398   obj.flty = y;
3399   if (!obj.initialize()) { delete obj; return none; } // not fatal
3401   insertObject(obj);
3403   return obj;
3407 final MapObject MakeMapObject (int x, int y, name aname) {
3408   MapObject obj = SpawnMapObject(aname);
3409   obj = PutSpawnedMapObject(x, y, obj);
3410   return obj;
3414 // ////////////////////////////////////////////////////////////////////////// //
3415 int winCutSceneTimer = -1;
3416 int winVolcanoTimer = -1;
3417 int winCutScenePhase = 0;
3418 int winSceneDrawStatus = 0;
3419 int winMoneyCount = 0;
3420 int winTime;
3421 bool winFadeOut = false;
3422 int winFadeLevel = 0;
3423 int winCutsceneSkip = 0; // 1: waiting for pay release; 2: pay released, do skip
3424 bool winCutsceneSwitchToNext = false;
3427 void startWinCutscene () {
3428   shakeLeft = 0;
3429   winCutsceneSwitchToNext = false;
3430   winCutsceneSkip = 0;
3431   isKeyPressed(GameConfig::Key.Pay);
3432   isKeyReleased(GameConfig::Key.Pay);
3434   auto olddel = ImmediateDelete;
3435   ImmediateDelete = false;
3436   clearTiles();
3437   clearObjects();
3439   createEnd1Room();
3440   fixWallTiles();
3441   addBackgroundGfxDetails();
3443   levBGImgName = 'bgCave';
3444   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
3446   blockWaterChecking = true;
3447   fixLiquidTop();
3448   cleanDeadTiles();
3450   collectLavaTiles();
3452   ImmediateDelete = olddel;
3453   CollectGarbage(true); // destroy delayed objects too
3455   if (dumpGridStats) {
3456     miscTileGrid.dumpStats();
3457     objGrid.dumpStats();
3458   }
3460   playerExited = false; // just in case
3462   osdClear();
3464   setupGhostTime();
3465   global.stopMusic();
3467   inWinCutscene = 1;
3468   winCutSceneTimer = -1;
3469   winCutScenePhase = 0;
3471   /+
3472   if (global.config.gameMode != GameConfig::GameMode.Vanilla) {
3473     if (global.config.bizarre) {
3474       global.yasmScore = 1;
3475       global.config.bizarrePlusTitle = true;
3476     }
3478     array!MapTile toReplace;
3479     forEachTile(delegate bool (MapTile t) {
3480       if (t.objType == 'oGTemple' ||
3481           t.objType == 'oIce' ||
3482           t.objType == 'oDark' ||
3483           t.objType == 'oBrick' ||
3484           t.objType == 'oLush')
3485       {
3486         toReplace[$] = t;
3487       }
3488       return false;
3489     });
3491     foreach (MapTile t; miscTileGrid.allObjects()) {
3492       if (t.objType == 'oGTemple' ||
3493           t.objType == 'oIce' ||
3494           t.objType == 'oDark' ||
3495           t.objType == 'oBrick' ||
3496           t.objType == 'oLush')
3497       {
3498         toReplace[$] = t;
3499       }
3500     }
3502     foreach (MapTile t; toReplace) {
3503       if (t.iy < 192) {
3504         t.cleanDeath = true;
3505             if (rand(1,120) == 1) instance_change(oGTemple, false);
3506         else if (rand(1,100) == 1) instance_change(oIce, false);
3507         else if (rand(1,90) == 1) instance_change(oDark, false);
3508         else if (rand(1,80) == 1) instance_change(oBrick, false);
3509         else if (rand(1,70) == 1) instance_change(oLush, false);
3510           }
3511       }
3512       with (oBrick)
3513       {
3514           if (y &lt; 192)
3515           {
3516               cleanDeath = true;
3517               if (rand(1,5) == 1) instance_change(oLush, false);
3518           }
3519       }
3520   }
3521   +/
3522   //!instance_create(0, 0, oBricks);
3524   //shakeToggle = false;
3525   //oPDummy.status = 2;
3527   //timer = 0;
3529   /+
3530   if (global.kaliPunish &gt;= 2) {
3531       instance_create(oPDummy.x, oPDummy.y+2, oBall2);
3532       obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
3533       obj.linkVal = 1;
3534       obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
3535       obj.linkVal = 2;
3536       obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
3537       obj.linkVal = 3;
3538       obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
3539       obj.linkVal = 4;
3540   }
3541   +/
3545 void startWinCutsceneVolcano () {
3546   /*
3547   writeln("VOLCANO HOLD 0: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
3548   writeln("VOLCANO PICK 0: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
3549   */
3551   shakeLeft = 0;
3552   winCutsceneSwitchToNext = false;
3553   auto olddel = ImmediateDelete;
3554   ImmediateDelete = false;
3555   clearTiles();
3556   clearObjects();
3558   levBGImgName = '';
3559   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
3561   blockWaterChecking = true;
3563   ImmediateDelete = olddel;
3564   CollectGarbage(true); // destroy delayed objects too
3566   spawnPlayerAt(2*16+8, 11*16+8);
3567   player.dir = MapEntity::Dir.Right;
3569   playerExited = false; // just in case
3571   osdClear();
3573   setupGhostTime();
3574   global.stopMusic();
3576   inWinCutscene = 2;
3577   winCutSceneTimer = -1;
3578   winCutScenePhase = 0;
3580   MakeMapTile(0, 0, 'oEnd2BG');
3581   realViewStart.x = 0;
3582   realViewStart.y = 0;
3583   viewStart.x = 0;
3584   viewStart.y = 0;
3586   viewMin.x = 0;
3587   viewMin.y = 0;
3588   viewMax.x = 320;
3589   viewMax.y = 240;
3591   player.dead = false;
3592   player.active = true;
3593   player.visible = false;
3594   player.removeBallAndChain(temp:true);
3595   player.stunned = false;
3596   player.status = MapObject::FALLING;
3597   if (player.holdItem) player.holdItem.visible = false;
3598   player.fltx = 320/2;
3599   player.flty = 0;
3601   /*
3602   writeln("VOLCANO HOLD 1: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
3603   writeln("VOLCANO PICK 1: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
3604   */
3608 void startWinCutsceneWinFall () {
3609   /*
3610   writeln("FALL HOLD 0: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
3611   writeln("FALL PICK 0: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
3612   */
3614   shakeLeft = 0;
3615   winCutsceneSwitchToNext = false;
3617   auto olddel = ImmediateDelete;
3618   ImmediateDelete = false;
3619   clearTiles();
3620   clearObjects();
3622   createEnd3Room();
3623   setMenuTilesVisible(false);
3624   //fixWallTiles();
3625   //addBackgroundGfxDetails();
3627   levBGImgName = '';
3628   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
3630   blockWaterChecking = true;
3631   fixLiquidTop();
3632   cleanDeadTiles();
3634   collectLavaTiles();
3636   ImmediateDelete = olddel;
3637   CollectGarbage(true); // destroy delayed objects too
3639   if (dumpGridStats) {
3640     miscTileGrid.dumpStats();
3641     objGrid.dumpStats();
3642   }
3644   playerExited = false; // just in case
3646   osdClear();
3648   setupGhostTime();
3649   global.stopMusic();
3651   inWinCutscene = 3;
3652   winCutSceneTimer = -1;
3653   winCutScenePhase = 0;
3655   player.dead = false;
3656   player.active = true;
3657   player.visible = false;
3658   player.removeBallAndChain(temp:true);
3659   player.stunned = false;
3660   player.status = MapObject::FALLING;
3661   if (player.holdItem) player.holdItem.visible = false;
3662   player.fltx = 320/2;
3663   player.flty = 0;
3665   winSceneDrawStatus = 0;
3666   winMoneyCount = 0;
3668   winFadeOut = false;
3669   winFadeLevel = 0;
3671   /*
3672   writeln("FALL HOLD 1: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
3673   writeln("FALL PICK 1: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
3674   */
3678 void setGameOver () {
3679   if (inWinCutscene) {
3680     player.visible = false;
3681     player.removeBallAndChain(temp:true);
3682     if (player.holdItem) player.holdItem.visible = false;
3683   }
3684   player.dead = true;
3685   if (inWinCutscene > 0) {
3686     winFadeOut = true;
3687     winFadeLevel = 255;
3688     winSceneDrawStatus = 8;
3689   }
3693 MapTile findEndPlatTile () {
3694   return forEachTile(delegate bool (MapTile t) { return (t isa MapTileEndPlat); });
3698 MapObject findBigTreasure () {
3699   return forEachObject(delegate bool (MapObject o) { return (o isa MapObjectBigTreasure); });
3703 void setMenuTilesVisible (bool vis) {
3704   if (vis) {
3705     forEachTile(delegate bool (MapTile t) {
3706       if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
3707         t.invisible = false;
3708       }
3709       return false;
3710     });
3711   } else {
3712     forEachTile(delegate bool (MapTile t) {
3713       if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
3714         t.invisible = true;
3715       }
3716       return false;
3717     });
3718   }
3722 void setMenuTilesOnTop () {
3723   forEachTile(delegate bool (MapTile t) {
3724     if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
3725       t.depth = 1;
3726     }
3727     return false;
3728   });
3732 void winCutscenePlayerControl (PlayerPawn plr) {
3733   auto payPress = isKeyPressed(GameConfig::Key.Pay);
3734   auto payRelease = isKeyReleased(GameConfig::Key.Pay);
3736   switch (winCutsceneSkip) {
3737     case 0: // nothing was pressed
3738       if (payPress) winCutsceneSkip = 1;
3739       break;
3740     case 1: // waiting for pay release
3741       if (payRelease) winCutsceneSkip = 2;
3742       break;
3743     case 2: // pay released, do skip
3744       setGameOver();
3745       return;
3746   }
3748   // first winning room
3749   if (inWinCutscene == 1) {
3750     if (plr.ix < 448+8) {
3751       plr.kRight = true;
3752       return;
3753     }
3755     // waiting for chest to open
3756     if (winCutScenePhase == 0) {
3757       winCutSceneTimer = 120/2;
3758       winCutScenePhase = 1;
3759       return;
3760     }
3762     // spawn big idol
3763     if (winCutScenePhase == 1) {
3764       if (--winCutSceneTimer == 0) {
3765         winCutScenePhase = 2;
3766         winCutSceneTimer = 20;
3767         forEachObject(delegate bool (MapObject o) {
3768           if (o isa MapObjectBigChest) {
3769             o.setSprite(global.config.gameMode == GameConfig::GameMode.Vanilla ? 'sBigChestOpen' : 'sBigChestOpen2');
3770             auto treasure = MakeMapObject(o.ix, o.iy, 'oBigTreasure');
3771             if (treasure) {
3772               treasure.yVel = -4;
3773               treasure.xVel = -3;
3774               o.playSound('sndClick');
3775               //!!!if (global.config.gameMode != GameConfig::GameMode.Vanilla) scrSprayGems(oBigChest.x+24, oBigChest.y+24);
3776             }
3777           }
3778           return false;
3779         });
3780       }
3781       return;
3782     }
3784     // lava pump wait
3785     if (winCutScenePhase == 2) {
3786       if (--winCutSceneTimer == 0) {
3787         winCutScenePhase = 3;
3788         winCutSceneTimer = 50;
3789       }
3790       return;
3791     }
3793     // lava pump start
3794     if (winCutScenePhase == 3) {
3795       auto ep = findEndPlatTile();
3796       if (ep) MakeMapObject(ep.ix+global.randOther(0, 80), /*ep.iy*/192+32, 'oBurn');
3797       if (--winCutSceneTimer == 0) {
3798         winCutScenePhase = 4;
3799         winCutSceneTimer = 10;
3800         if (ep) MakeMapObject(ep.ix, ep.iy+30, 'oLavaSpray');
3801         scrShake(9999);
3802       }
3803       return;
3804     }
3806     // lava pump first accel
3807     if (winCutScenePhase == 4) {
3808       if (--winCutSceneTimer == 0) {
3809         forEachObject(delegate bool (MapObject o) {
3810           if (o isa MapObjectLavaSpray) o.yAcc = -0.1;
3811           return false;
3812         });
3813       }
3814     }
3816     // lava pump complete
3817     if (winCutScenePhase == 5) {
3818       if (--winCutSceneTimer == 0) {
3819         //if (oLavaSpray) oLavaSpray.yAcc = -0.1;
3820         startWinCutsceneVolcano();
3821       }
3822       return;
3823     }
3824     return;
3825   }
3828   // volcano room
3829   if (inWinCutscene == 2) {
3830     plr.flty = 0;
3832     // initialize
3833     if (winCutScenePhase == 0) {
3834       winCutSceneTimer = 50;
3835       winCutScenePhase = 1;
3836       winVolcanoTimer = 10;
3837       return;
3838     }
3840     if (winVolcanoTimer > 0) {
3841       if (--winVolcanoTimer == 0) {
3842         MakeMapObject(224+global.randOther(0,48), 144+global.randOther(0,8), 'oVolcanoFlame');
3843         winVolcanoTimer = global.randOther(10, 20);
3844       }
3845     }
3847     // plr sil
3848     if (winCutScenePhase == 1) {
3849       if (--winCutSceneTimer == 0) {
3850         winCutSceneTimer = 30;
3851         winCutScenePhase = 2;
3852         auto sil = MakeMapObject(240, 132, 'oPlayerSil');
3853         //sil.xVel = -6;
3854         //sil.yVel = -8;
3855       }
3856       return;
3857     }
3859     // treasure sil
3860     if (winCutScenePhase == 2) {
3861       if (--winCutSceneTimer == 0) {
3862         winCutScenePhase = 3;
3863         auto sil = MakeMapObject(240, 132, 'oTreasureSil');
3864         //sil.xVel = -6;
3865         //sil.yVel = -8;
3866       }
3867       return;
3868     }
3870     return;
3871   }
3873   // winning camel room
3874   if (inWinCutscene == 3) {
3875     //if (!player.holdItem)  writeln("SCENE 3: LOST ITEM!");
3877     if (!plr.visible) plr.flty = -32;
3879     // initialize
3880     if (winCutScenePhase == 0) {
3881       winCutSceneTimer = 50;
3882       winCutScenePhase = 1;
3883       return;
3884     }
3886     // fall sound
3887     if (winCutScenePhase == 1) {
3888       if (--winCutSceneTimer == 0) {
3889         winCutSceneTimer = 50;
3890         winCutScenePhase = 2;
3891         plr.playSound('sndPFall');
3892         plr.visible = true;
3893         plr.active = true;
3894         writeln("MUST BE CHAINED: ", plr.mustBeChained);
3895         if (plr.mustBeChained) {
3896           plr.removeBallAndChain(temp:true);
3897           plr.spawnBallAndChain();
3898         }
3899         /*
3900         writeln("HOLD: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
3901         writeln("PICK: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
3902         */
3903         if (!player.holdItem && player.pickedItem) player.scrSwitchToPocketItem(forceIfEmpty:false);
3904         if (player.holdItem) {
3905           player.holdItem.visible = true;
3906           player.holdItem.canLiveOutsideOfLevel = true;
3907           writeln("HOLD ITEM: '", GetClassName(player.holdItem.Class), "'");
3908         }
3909         plr.status == MapObject::FALLING;
3910         global.plife += 99; // just in case
3911       }
3912       return;
3913     }
3915     if (winCutScenePhase == 2) {
3916       auto ball = plr.getMyBall();
3917       if (ball && plr.holdItem != ball) {
3918         ball.teleportTo(plr.fltx, plr.flty+8);
3919         ball.yVel = 6;
3920         ball.myGrav = 0.6;
3921       }
3922       if (plr.status == MapObject::STUNNED || plr.stunned) {
3923         //alarm[0] = 70;
3924         //alarm[1] = 50;
3925         //status = GETUP;
3926         auto treasure = MakeMapObject(144+16+8, -32, 'oBigTreasure');
3927         if (treasure) treasure.depth = 1;
3928         winCutScenePhase = 3;
3929         plr.stunTimer = 30;
3930         plr.playSound('sndTFall');
3931       }
3932       return;
3933     }
3935     if (winCutScenePhase == 3) {
3936       if (plr.status != MapObject::STUNNED && !plr.stunned) {
3937         auto bt = findBigTreasure();
3938         if (bt) {
3939           if (bt.yVel == 0) {
3940             //plr.yVel = -4;
3941             //plr.status = MapObject::JUMPING;
3942             plr.kJump = true;
3943             plr.kJumpPressed = true;
3944             winCutScenePhase = 4;
3945             winCutSceneTimer = 50;
3946           }
3947         }
3948       }
3949       return;
3950     }
3952     if (winCutScenePhase == 4) {
3953       if (--winCutSceneTimer == 0) {
3954         setMenuTilesVisible(true);
3955         winCutScenePhase = 5;
3956         winSceneDrawStatus = 1;
3957         global.playMusic('musVictory', loop:false);
3958         winCutSceneTimer = 50;
3959       }
3960       return;
3961     }
3963     if (winCutScenePhase == 5) {
3964       if (winSceneDrawStatus == 3) {
3965         int money = stats.money;
3966         if (winMoneyCount < money) {
3967           if (money-winMoneyCount > 1000) {
3968             winMoneyCount += 1000;
3969           } else if (money-winMoneyCount > 100) {
3970             winMoneyCount += 100;
3971           } else if (money-winMoneyCount > 10) {
3972             winMoneyCount += 10;
3973           } else {
3974             ++winMoneyCount;
3975           }
3976         }
3977         if (winMoneyCount >= money) {
3978           winMoneyCount = money;
3979           ++winSceneDrawStatus;
3980         }
3981         return;
3982       }
3984       if (winSceneDrawStatus == 7) {
3985         winFadeOut = true;
3986         winFadeLevel += 1;
3987         if (winFadeLevel >= 255) {
3988           ++winSceneDrawStatus;
3989           winCutSceneTimer = 30*30;
3990         }
3991         return;
3992       }
3994       if (winSceneDrawStatus == 8) {
3995         if (--winCutSceneTimer == 0) {
3996           setGameOver();
3997         }
3998         return;
3999       }
4001       if (--winCutSceneTimer == 0) {
4002         ++winSceneDrawStatus;
4003         winCutSceneTimer = 50;
4004       }
4005     }
4007     return;
4008   }
4012 // ////////////////////////////////////////////////////////////////////////// //
4013 void renderWinCutsceneOverlay () {
4014   if (inWinCutscene == 3) {
4015     if (winSceneDrawStatus > 0) {
4016       Video.color = 0xff_ff_ff;
4017       sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
4018       //draw_set_color(txtCol);
4019       drawTextAt(64, 32, "YOU MADE IT!");
4021       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4022       if (global.config.gameMode == GameConfig::GameMode.Vanilla) {
4023         Video.color = 0xff_ff_00; //draw_set_color(c_yellow);
4024         drawTextAt(64, 48, "Classic Mode done!");
4025       } else {
4026         Video.color = 0x00_80_80; //draw_set_color(c_teal);
4027         if (global.config.bizarrePlus) drawTextAt(64, 48, "Bizarre Mode Plus done!");
4028         else drawTextAt(64, 48, "Bizarre Mode done!");
4029         //draw_set_color(c_white);
4030       }
4031       if (!global.usedShortcut) {
4032         Video.color = 0xc0_c0_c0; //draw_set_color(c_silver);
4033         drawTextAt(64, 56, "No shortcuts used!");
4034         //draw_set_color(c_yellow);
4035       }
4036     }
4038     if (winSceneDrawStatus > 1) {
4039       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4040       //draw_set_color(txtCol);
4041       Video.color = 0xff_ff_ff;
4042       drawTextAt(64, 64, "FINAL SCORE:");
4043     }
4045     if (winSceneDrawStatus > 2) {
4046       sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
4047       Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4048       drawTextAt(64, 72, va("$%d", winMoneyCount));
4049     }
4051     if (winSceneDrawStatus > 4) {
4052       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4053       Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4054       drawTextAt(64, 96, va("Time: %s", time2str(winTime/30)));
4055       /*
4056       draw_set_color(c_white);
4057       if (s &lt; 10) draw_text(96+24, 96, string(m) + ":0" + string(s));
4058       else draw_text(96+24, 96, string(m) + ":" + string(s));
4059       */
4060     }
4062     if (winSceneDrawStatus > 5) {
4063       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4064       Video.color = 0xff_ff_ff; //draw_set_color(txtCol);
4065       drawTextAt(64, 96+8, "Kills: ");
4066       Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4067       drawTextAt(96+24, 96+8, va("%s", stats.countKills()));
4068     }
4070     if (winSceneDrawStatus > 6) {
4071       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4072       Video.color = 0xff_ff_ff; //draw_set_color(txtCol);
4073       drawTextAt(64, 96+16, "Saves: ");
4074       Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4075       drawTextAt(96+24, 96+16, va("%s", stats.damselsSaved));
4076     }
4078     if (winFadeOut) {
4079       Video.color = (255-clamp(winFadeLevel, 0, 255))<<24;
4080       Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
4081     }
4083     if (winSceneDrawStatus == 8) {
4084       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4085       Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4086       string lastString;
4087       if (global.config.gameMode == GameConfig::GameMode.Vanilla) {
4088         Video.color = 0xff_ff_00; //draw_set_color(c_yellow);
4089         lastString = "YOU SHALL BE REMEMBERED AS A HERO.";
4090       } else {
4091         Video.color = 0x00_ff_ff;
4092         if (global.config.bizarrePlus) lastString = "ANOTHER LEGENDARY ADVENTURE!";
4093         else lastString = "YOUR DISCOVERIES WILL BE CELEBRATED!";
4094       }
4095       auto strLen = lastString.length*8;
4096       int n = 320-strLen;
4097       n = trunc(ceil(n/2.0));
4098       drawTextAt(n, 116, lastString);
4099     }
4100   }
4104 // ////////////////////////////////////////////////////////////////////////// //
4105 #include "roomTitle.vc"
4106 #include "roomTrans1.vc"
4107 #include "roomTrans2.vc"
4108 #include "roomTrans3.vc"
4109 #include "roomTrans4.vc"
4110 #include "roomOlmec.vc"
4111 #include "roomEnd.vc"
4114 // ////////////////////////////////////////////////////////////////////////// //
4115 #include "packages/Generator/loadRoomGens.vc"
4116 #include "packages/Generator/loadEntityGens.vc"