moved moveable tiles to front (slightly); lighting fixes
[k8vacspelynky.git] / GameLevel.vc
blob0f18e1f179d24f212094e7ada8e6562073c24e5f
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
94 bool resetBMCOG = false;
95 int udjatAlarm;
98 // FPS, i.e. incremented by 30 in one second
99 int time; // in frames
100 int lastUsedObjectId;
102 // screen shake variables
103 int shakeLeft;
104 IVec2D shakeOfs;
105 IVec2D shakeDir;
107 // set this before calling `fixCamera()`
108 // dimensions should be real, not scaled up/down
109 transient int viewWidth, viewHeight;
110 // room bounds, not scaled
111 IVec2D viewMin, viewMax;
113 // for Olmec level cinematics
114 IVec2D cameraSlideToDest;
115 IVec2D cameraSlideToCurr;
116 IVec2D cameraSlideToSpeed; // !0: slide
117 int cameraSlideToPlayer;
118 // `fixCamera()` will set the following
119 // coordinates will be real too (with scale applied)
120 // shake is not applied
121 transient IVec2D viewStart; // with `player.viewOffset`
122 private transient IVec2D realViewStart; // without `player.viewOffset`
124 final void cameraSlideToPoint (int dx, int dy, int speedx, int speedy) {
125   cameraSlideToPlayer = 0;
126   cameraSlideToDest.x = dx;
127   cameraSlideToDest.y = dy;
128   cameraSlideToSpeed.x = abs(speedx);
129   cameraSlideToSpeed.y = abs(speedy);
130   cameraSlideToCurr.x = cameraCurrX;
131   cameraSlideToCurr.y = cameraCurrY;
134 final void cameraReturnToPlayer () {
135   if (!cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y)) {
136     cameraSlideToCurr.x = cameraCurrX;
137     cameraSlideToCurr.y = cameraCurrY;
138     if (cameraSlideToSpeed.x && abs(cameraSlideToSpeed.x) < 8) cameraSlideToSpeed.x = 8;
139     if (cameraSlideToSpeed.y && abs(cameraSlideToSpeed.y) < 8) cameraSlideToSpeed.y = 8;
140     cameraSlideToPlayer = 1;
141   }
144 // if `frameSkip` is `true`, there are more frames waiting
145 // (i.e. you may skip rendering and such)
146 transient void delegate (bool frameSkip) onBeforeFrame;
147 transient void delegate (bool frameSkip) onAfterFrame;
149 transient void delegate () onLevelExitedCB;
151 // this will be called in-between frames, and
152 // `frameTime` is [0..1)
153 transient void delegate (float frameTime) onInterFrame;
155 final int bizRoomStyle { get { return (lg ? lg.bizRoomStyle : 0); } }
158 final bool isNormalLevel () { return (levelKind == LevelKind.Normal); }
159 final bool isTitleRoom () { return (levelKind == LevelKind.Title); }
162 // ////////////////////////////////////////////////////////////////////////// //
163 // stats
164 void addDeath (name aname) { if (isNormalLevel()) stats.addDeath(aname); }
165 void addKill (name aname) { if (isNormalLevel()) stats.addKill(aname); }
166 void addCollect (name aname, optional int amount) { if (isNormalLevel()) stats.addCollect(aname, amount!optional); }
168 void addDamselSaved () { if (isNormalLevel()) stats.addDamselSaved(); }
169 void addIdolStolen () { if (isNormalLevel()) stats.addIdolStolen(); }
170 void addIdolConverted () { if (isNormalLevel()) stats.addIdolConverted(); }
171 void addCrystalIdolStolen () { if (isNormalLevel()) stats.addCrystalIdolStolen(); }
172 void addCrystalIdolConverted () { if (isNormalLevel()) stats.addCrystalIdolConverted(); }
173 void addGhostSummoned () { if (isNormalLevel()) stats.addGhostSummoned(); }
176 // ////////////////////////////////////////////////////////////////////////// //
177 static final string val2dig (int n) {
178   return (n < 10 ? va("0%d", n) : va("%d", n));
182 static final string time2str (int time) {
183   int secs = time%60; time /= 60;
184   int mins = time%60; time /= 60;
185   int hours = time%24; time /= 24;
186   int days = time;
187   if (days) return va("%d DAYS, %d:%s:%s", days, hours, val2dig(mins), val2dig(secs));
188   if (hours) return va("%d:%s:%s", hours, val2dig(mins), val2dig(secs));
189   return va("%s:%s", val2dig(mins), val2dig(secs));
193 // ////////////////////////////////////////////////////////////////////////// //
194 final int tilesWidth () { return lg.levelRoomWidth*RoomGen::Width+2; }
195 final int tilesHeight () { return (lg.finalBossLevel ? 55 : lg.levelRoomHeight*RoomGen::Height+2); }
198 // ////////////////////////////////////////////////////////////////////////// //
199 // this won't generate a level yet
200 void restartGame () {
201   resetBMCOG = false;
202   inWinCutscene = 0;
203   shakeLeft = 0;
204   udjatAlarm = 0;
205   if (player) {
206     player.removeBallAndChain(temp:false);
207     auto hi = player.holdItem;
208     player.holdItem = none;
209     if (hi) hi.instanceRemove();
210     hi = player.pickedItem;
211     player.pickedItem = none;
212     if (hi) hi.instanceRemove();
213   }
214   time = 0;
215   global.resetGame();
216   stats.clearGameTotals();
217   if (global.startMoney > 0) stats.setMoneyCheat();
218   stats.setMoney(global.startMoney);
219   levelKind = LevelKind.Normal;
220   //writeln("level=", global.currLevel, "; lt=", global.levelType);
224 void restartTitle () {
225   resetBMCOG = false;
226   inWinCutscene = 0;
227   shakeLeft = 0;
228   udjatAlarm = 0;
229   if (player) {
230     player.removeBallAndChain(temp:false);
231     auto hi = player.holdItem;
232     player.holdItem = none;
233     if (hi) hi.instanceRemove();
234     hi = player.pickedItem;
235     player.pickedItem = none;
236     if (hi) hi.instanceRemove();
237   }
238   time = 0;
239   global.resetGame();
240   stats.clearGameTotals();
241   stats.setMoney(0);
242   createTitleRoom();
243   levelKind = LevelKind.Title;
244   global.bombs = 9999;
245   global.rope = 9999;
246   global.arrows = 9999;
247   global.sgammo = 9999;
248   //writeln("level=", global.currLevel, "; lt=", global.levelType);
252 // complement function to `restart game`
253 void generateNormalLevel () {
254   generateLevel();
255   centerViewAtPlayer();
259 // ////////////////////////////////////////////////////////////////////////// //
260 // generate angry shopkeeper at exit if murderer or thief
261 void generateAngryShopkeepers () {
262   if (global.murderer || global.thiefLevel > 0) {
263     foreach (MapTile e; allExits) {
264       auto obj = MonsterShopkeeper(MakeMapObject(e.ix, e.iy, 'oShopkeeper'));
265       if (obj) {
266         obj.style = 'Bounty Hunter';
267         obj.status = MapObject::PATROL;
268       }
269     }
270   }
274 // ////////////////////////////////////////////////////////////////////////// //
275 final void resetRoomBounds () {
276   viewMin.x = 0;
277   viewMin.y = 0;
278   viewMax.x = tilesWidth*16;
279   viewMax.y = tilesHeight*16;
280   // Great Lake is bottomless (nope)
281   //if (global.lake) viewMax.y -= 16;
282   //writeln("ROOM BOUNDS: (", viewMax.x, "x", viewMax.y, ")");
286 final void setRoomBounds (int x0, int y0, int x1, int y1) {
287   viewMin.x = x0;
288   viewMin.y = y0;
289   viewMax.x = x1+16;
290   viewMax.y = y1+16;
294 // ////////////////////////////////////////////////////////////////////////// //
295 struct OSDMessage {
296   string msg;
297   float timeout; // seconds
298   float starttime; // for active
299   bool active; // true: timeout is `GetTickCount()` dismissing time
302 array!OSDMessage msglist; // [0]: current one
305 private final void osdCheckTimeouts () {
306   auto stt = GetTickCount();
307   while (msglist.length) {
308     if (!msglist[0].active) {
309       msglist[0].active = true;
310       msglist[0].starttime = stt;
311     }
312     if (msglist[0].starttime+msglist[0].timeout >= stt) break;
313     msglist.remove(0);
314   }
318 final bool osdHasMessage () {
319   osdCheckTimeouts();
320   return (msglist.length > 0);
324 final string osdGetMessage (out float timeLeft, out float timeStart) {
325   osdCheckTimeouts();
326   if (msglist.length == 0) { timeLeft = 0; return ""; }
327   auto stt = GetTickCount();
328   timeStart = msglist[0].starttime;
329   timeLeft = fmax(0, msglist[0].starttime+msglist[0].timeout-stt);
330   return msglist[0].msg;
334 final void osdClear () {
335   msglist.length -= msglist.length;
339 final void osdMessage (string msg, optional float timeout, optional bool urgent) {
340   if (!msg) return;
341   if (!specified_timeout) timeout = 3.33;
342   // special message for shops
343   if (timeout == -666) {
344     if (!msg) return;
345     if (msglist.length && msglist[0].msg == msg) return;
346     if (msglist.length == 0 || msglist[0].msg != msg) {
347       osdClear();
348       msglist.length += 1;
349       msglist[0].msg = msg;
350     }
351     msglist[0].active = false;
352     msglist[0].timeout = 3.33;
353     osdCheckTimeouts();
354     return;
355   }
356   if (timeout < 0.1) return;
357   timeout = fmax(1.0, timeout);
358   //writeln("OSD: ", msg);
359   // find existing one, and bring it to the top
360   int oldidx = 0;
361   for (; oldidx < msglist.length; ++oldidx) {
362     if (msglist[oldidx].msg == msg) break; // i found her!
363   }
364   // duplicate?
365   if (oldidx < msglist.length) {
366     // yeah, move duplicate to the top
367     msglist[oldidx].timeout = fmax(timeout, msglist[oldidx].timeout);
368     msglist[oldidx].active = false;
369     if (urgent && oldidx != 0) {
370       timeout = msglist[oldidx].timeout;
371       msglist.remove(oldidx);
372       msglist.insert(0);
373       msglist[0].msg = msg;
374       msglist[0].timeout = timeout;
375       msglist[0].active = false;
376     }
377   } else if (urgent) {
378     msglist.insert(0);
379     msglist[0].msg = msg;
380     msglist[0].timeout = timeout;
381     msglist[0].active = false;
382   } else {
383     // new one
384     msglist.length += 1;
385     msglist[$-1].msg = msg;
386     msglist[$-1].timeout = timeout;
387     msglist[$-1].active = false;
388   }
389   osdCheckTimeouts();
393 // ////////////////////////////////////////////////////////////////////////// //
394 void setup (GameGlobal aGlobal, SpriteStore aSprStore, BackTileStore aBGTileStore) {
395   global = aGlobal;
396   sprStore = aSprStore;
397   bgtileStore = aBGTileStore;
399   lg = SpawnObject(LevelGen);
400   lg.global = global;
401   lg.level = self;
403   miscTileGrid = SpawnObject(EntityGrid);
404   miscTileGrid.setup(0, 0, MaxTilesWidth*16, MaxTilesHeight*16, MapTile);
405   //miscTileGrid.ownObjects = true;
407   objGrid = SpawnObject(EntityGrid);
408   objGrid.setup(0, 0, MaxTilesWidth*16, MaxTilesHeight*16, MapObject);
412 // stores should be set
413 void onLoaded () {
414   checkWater = true;
415   levBGImg = bgtileStore[levBGImgName];
416   foreach (int y; 0..MaxTilesHeight) {
417     foreach (int x; 0..MaxTilesWidth) {
418       if (tiles[x, y]) tiles[x, y].onLoaded();
419     }
420   }
421   foreach (MapEntity o; miscTileGrid.allObjects()) o.onLoaded();
422   foreach (MapEntity o; objGrid.allObjects()) o.onLoaded();
423   for (MapBackTile bt = backtiles; bt; bt = bt.next) bt.onLoaded();
424   if (player) player.onLoaded();
425   //FIXME
426   if (msglist.length) {
427     msglist[0].active = false;
428     msglist[0].timeout = 0.200;
429     osdCheckTimeouts();
430   }
434 // ////////////////////////////////////////////////////////////////////////// //
435 void pickedSpectacles () {
436   foreach (int y; 0..tilesHeight) {
437     foreach (int x; 0..tilesWidth) {
438       MapTile t = tiles[x, y];
439       if (t && t.isInstanceAlive) t.onGotSpectacles();
440     }
441   }
442   foreach (MapTile t; miscTileGrid.allObjects()) {
443     if (t.isInstanceAlive) t.onGotSpectacles();
444   }
448 // ////////////////////////////////////////////////////////////////////////// //
449 #include "rgentile.vc"
450 #include "rgenobj.vc"
453 void onLevelExited () {
454   if (isNormalLevel()) stats.maxLevelComplete = max(stats.maxLevelComplete, global.currLevel);
455   if (onLevelExitedCB) onLevelExitedCB();
456   if (isTitleRoom()) restartGame();
457   if (levelKind == LevelKind.Transition) {
458     if (global.thiefLevel > 0) global.thiefLevel -= 1;
459     //orig dbg:if (global.currLevel == 1) global.currLevel += firstLevelSkip; else global.currLevel += levelSkip;
460     if (!global.blackMarket && !global.cityOfGold /*&& !global.yetiLair*/) {
461       global.currLevel += 1;
462     }
463     generateLevel();
464   } else {
465     if (lg.finalBossLevel) {
466       winTime = time;
467       ++stats.gamesWon;
468       // add money for big idol
469       player.addScore(50000);
470       stats.gameOver();
471       startWinCutscene();
472     } else {
473       generateTransitionLevel();
474     }
475   }
476   centerViewAtPlayer();
480 void onOlmecDead (MapObject o) {
481   writeln("*** OLMEC IS DEAD!");
482   foreach (MapTile t; allExits) {
483     if (t.exit) {
484       t.openExit();
485       auto st = checkTileAtPoint(t.ix+8, t.iy+16);
486       if (!st) {
487         st = MakeMapTile(t.ix/16, t.iy/16+1, 'oTemple');
488         st.ore = 0;
489       }
490       st.invincible = true;
491     }
492   }
496 void generateLevelMessages () {
497   if (global.darkLevel) {
498     if (global.hasCrown) {
499        osdMessage("THE HEDJET SHINES BRIGHTLY.");
500        global.darkLevel = false;
501     } else if (global.config.scumDarkness < 2) {
502       osdMessage("I CAN'T SEE A THING!\nI'D BETTER USE THESE FLARES!");
503     }
504   }
506   if (global.blackMarket) osdMessage("WELCOME TO THE BLACK MARKET!");
508   if (global.cemetary) osdMessage("THE DEAD ARE RESTLESS!");
509   if (global.lake) osdMessage("I CAN HEAR RUSHING WATER...");
511   if (global.snakePit) osdMessage("I HEAR SNAKES... I HATE SNAKES!");
512   if (global.yetiLair) osdMessage("IT SMELLS LIKE WET FUR IN HERE!");
513   if (global.alienCraft) osdMessage("THERE'S A PSYCHIC PRESENCE HERE!");
514   if (global.cityOfGold) {
515     if (true /*hasOlmec()*/) osdMessage("IT'S THE LEGENDARY CITY OF GOLD!");
516   }
518   if (global.sacrificePit) osdMessage("I CAN HEAR PRAYERS TO KALI!");
522 final MapObject debugSpawnObjectWithClass (class!MapObject oclass, optional bool playerDir) {
523   if (!oclass) return none;
524   int dx = 0, dy = 0;
525   bool canLeft = !isSolidAtPoint(player.ix-8, player.iy);
526   bool canRight = !isSolidAtPoint(player.ix+16, player.iy);
527   if (!canLeft && !canRight) return none;
528   if (canLeft && canRight) {
529     if (playerDir) {
530       dx = (player.dir == MapObject::Dir.Left ? -16 : 16);
531     } else {
532       dx = 16;
533     }
534   } else {
535     dx = (canLeft ? -16 : 16);
536   }
537   auto obj = SpawnMapObjectWithClass(oclass);
538   if (obj isa MapEnemy) { dx -= 8; dy -= 8; }
539   if (obj) PutSpawnedMapObject(player.ix+dx, player.iy+dy, obj);
540   return obj;
544 final MapObject debugSpawnObject (name aname) {
545   if (!aname) return none;
546   return debugSpawnObjectWithClass(findGameObjectClassByName(aname));
550 // `global.currLevel` is the new level
551 void generateTransitionLevel () {
552   global.darkLevel = false;
553   udjatAlarm = 0;
554   xmoney = 0;
555   collectCounter = 0;
557   global.setMusicPitch(1.0);
558   switch (global.config.transitionMusicMode) {
559     case GameConfig::MusicMode.Silent: global.stopMusic(); break;
560     case GameConfig::MusicMode.Restart: global.restartMusic(); break;
561     case GameConfig::MusicMode.DontTouch: break;
562   }
564   levelKind = LevelKind.Transition;
566   auto olddel = ImmediateDelete;
567   ImmediateDelete = false;
568   clearTiles();
569   clearObjects();
571        if (global.currLevel < 4) createTrans1Room();
572   else if (global.currLevel == 4) createTrans1xRoom();
573   else if (global.currLevel < 8) createTrans2Room();
574   else if (global.currLevel == 8) createTrans2xRoom();
575   else if (global.currLevel < 12) createTrans3Room();
576   else if (global.currLevel == 12) createTrans3xRoom();
577   else if (global.currLevel < 16) createTrans4Room();
578   else if (global.currLevel == 16) createTrans4Room();
579   else createTrans1Room(); //???
581   setMenuTilesOnTop();
583   fixWallTiles();
584   addBackgroundGfxDetails();
585   levBGImgName = 'bgCave';
586   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
588   blockWaterChecking = true;
589   fixLiquidTop();
590   cleanDeadTiles();
592   if (damselSaved > 0) {
593     MakeMapObject(176+8, 176+8, 'oDamselKiss');
594     global.plife += damselSaved; // if player skipped transition cutscene
595     damselSaved = 0;
596   }
598   collectLavaTiles();
600   ImmediateDelete = olddel;
601   CollectGarbage(true); // destroy delayed objects too
603   if (dumpGridStats) {
604     miscTileGrid.dumpStats();
605     objGrid.dumpStats();
606   }
608   playerExited = false; // just in case
610   osdClear();
612   setupGhostTime();
613   //global.playMusic(lg.musicName);
617 void generateLevel () {
618   udjatAlarm = 0;
619   if (resetBMCOG) {
620     resetBMCOG = false;
621     global.cityOfGold = false;
622     global.genBlackMarket = false;
623   }
625   global.setMusicPitch(1.0);
626   stats.clearLevelTotals();
628   levelKind = LevelKind.Normal;
629   lg.generate();
630   //lg.dump();
632   resetRoomBounds();
634   lg.generateRooms();
635   //writeln("tw:", tilesWidth, "; th:", tilesHeight);
637   auto olddel = ImmediateDelete;
638   ImmediateDelete = false;
639   clearTiles();
640   clearObjects();
642   if (lg.finalBossLevel) {
643     blockWaterChecking = true;
644     createOlmecRoom();
645   }
647   // if transition cutscene was skipped...
648   if (damselSaved > 0) global.plife += damselSaved; // if player skipped transition cutscene
649   damselSaved = 0;
651   // generate tiles
652   startRoomX = lg.startRoomX;
653   startRoomY = lg.startRoomY;
654   endRoomX = lg.endRoomX;
655   endRoomY = lg.endRoomY;
656   addBackgroundGfxDetails();
657   foreach (int y; 0..tilesHeight) {
658     foreach (int x; 0..tilesWidth) {
659       lg.genTileAt(x, y);
660     }
661   }
662   fixWallTiles();
664   levBGImgName = lg.bgImgName;
665   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
667   if (global.allowAngryShopkeepers) generateAngryShopkeepers();
669   lg.generateEntities();
671   // add box of flares to dark level
672   if (global.darkLevel && allEnters.length) {
673     auto enter = allEnters[0];
674     int x = enter.ix, y = enter.iy;
675          if (!isSolidAtPoint(x-16, y)) MakeMapObject(x-16+8, y+8, 'oFlareCrate');
676     else if (!isSolidAtPoint(x+16, y)) MakeMapObject(x+16+8, y+8, 'oFlareCrate');
677     else MakeMapObject(x+8, y+8, 'oFlareCrate');
678   }
680   //scrGenerateEntities();
681   //foreach (; 0..2) scrGenerateEntities();
683   writeln(countObjects, " alive objects inserted");
684   writeln(countBackTiles, " background tiles inserted");
686   if (!player) FatalError("player pawn is not spawned");
688   if (lg.finalBossLevel) {
689     blockWaterChecking = true;
690   } else {
691     blockWaterChecking = false;
692   }
693   fixLiquidTop();
694   cleanDeadTiles();
696   collectLavaTiles();
698   ImmediateDelete = olddel;
699   CollectGarbage(true); // destroy delayed objects too
701   if (dumpGridStats) {
702     miscTileGrid.dumpStats();
703     objGrid.dumpStats();
704   }
706   playerExited = false; // just in case
708   levelMoneyStart = stats.money;
710   osdClear();
711   generateLevelMessages();
713   xmoney = 0;
714   collectCounter = 0;
716   if (lastMusicName != lg.musicName) {
717     global.playMusic(lg.musicName);
718     //writeln("!<", lastMusicName, ">:<", lg.musicName, ">");
719   } else {
720     //writeln("MM: ", global.config.nextLevelMusicMode);
721     switch (global.config.nextLevelMusicMode) {
722       case GameConfig::MusicMode.Silent: global.stopMusic(); break; // the thing that should not be
723       case GameConfig::MusicMode.Restart: global.restartMusic(); break;
724       case GameConfig::MusicMode.DontTouch:
725         if (global.config.transitionMusicMode == GameConfig::MusicMode.Silent) {
726           global.playMusic(lg.musicName);
727         }
728         break;
729     }
730   }
731   lastMusicName = lg.musicName;
732   //global.playMusic(lg.musicName);
734   setupGhostTime();
735   if (global.cityOfGold || global.genBlackMarket) resetBMCOG = true;
739 // ////////////////////////////////////////////////////////////////////////// //
740 int currKeys, nextKeys;
741 int pressedKeysQ, releasedKeysQ;
742 int keysPressed, keysReleased = -1;
745 struct SavedKeyState {
746   int currKeys, nextKeys;
747   int pressedKeysQ, releasedKeysQ;
748   int keysPressed, keysReleased;
749   // for session
750   int roomSeed, otherSeed;
754 // for saving/replaying
755 final void keysSaveState (out SavedKeyState ks) {
756   ks.currKeys = currKeys;
757   ks.nextKeys = nextKeys;
758   ks.pressedKeysQ = pressedKeysQ;
759   ks.releasedKeysQ = releasedKeysQ;
760   ks.keysPressed = keysPressed;
761   ks.keysReleased = keysReleased;
764 // for saving/replaying
765 final void keysRestoreState (const ref SavedKeyState ks) {
766   currKeys = ks.currKeys;
767   nextKeys = ks.nextKeys;
768   pressedKeysQ = ks.pressedKeysQ;
769   releasedKeysQ = ks.releasedKeysQ;
770   keysPressed = ks.keysPressed;
771   keysReleased = ks.keysReleased;
775 final void keysNextFrame () {
776   currKeys = nextKeys;
780 final void clearKeys () {
781   currKeys = 0;
782   nextKeys = 0;
783   pressedKeysQ = 0;
784   releasedKeysQ = 0;
785   keysPressed = 0;
786   keysReleased = -1;
790 final void onKey (int code, bool down) {
791   if (!code) return;
792   if (down) {
793     currKeys |= code;
794     nextKeys |= code;
795     if (keysReleased&code) {
796       keysPressed |= code;
797       keysReleased &= ~code;
798       pressedKeysQ |= code;
799     }
800   } else {
801     nextKeys &= ~code;
802     if (keysPressed&code) {
803       keysReleased |= code;
804       keysPressed &= ~code;
805       releasedKeysQ |= code;
806     }
807   }
810 final bool isKeyDown (int code) {
811   return !!(currKeys&code);
814 final bool isKeyPressed (int code) {
815   bool res = !!(pressedKeysQ&code);
816   pressedKeysQ &= ~code;
817   return res;
820 final bool isKeyReleased (int code) {
821   bool res = !!(releasedKeysQ&code);
822   releasedKeysQ &= ~code;
823   return res;
827 final void clearKeysPressRelease () {
828   keysPressed = default.keysPressed;
829   keysReleased = default.keysReleased;
830   pressedKeysQ = default.pressedKeysQ;
831   releasedKeysQ = default.releasedKeysQ;
832   currKeys = 0;
833   nextKeys = 0;
837 // ////////////////////////////////////////////////////////////////////////// //
838 final void registerEnter (MapTile t) {
839   if (!t) return;
840   allEnters[$] = t;
841   return;
845 final void registerExit (MapTile t) {
846   if (!t) return;
847   allExits[$] = t;
848   return;
852 final bool isYAtEntranceRow (int py) {
853   py /= 16;
854   foreach (MapTile t; allEnters) if (t.iy == py) return true;
855   return false;
859 final int calcNearestEnterDist (int px, int py) {
860   if (allEnters.length == 0) return int.max;
861   int curdistsq = int.max;
862   foreach (MapTile t; allEnters) {
863     int xc = px-t.xCenter, yc = py-t.yCenter;
864     int distsq = xc*xc+yc*yc;
865     if (distsq < curdistsq) curdistsq = distsq;
866   }
867   return round(sqrt(curdistsq));
871 final int calcNearestExitDist (int px, int py) {
872   if (allExits.length == 0) return int.max;
873   int curdistsq = int.max;
874   foreach (MapTile t; allExits) {
875     int xc = px-t.xCenter, yc = py-t.yCenter;
876     int distsq = xc*xc+yc*yc;
877     if (distsq < curdistsq) curdistsq = distsq;
878   }
879   return round(sqrt(curdistsq));
883 // ////////////////////////////////////////////////////////////////////////// //
884 final void clearForTransition () {
885   auto olddel = ImmediateDelete;
886   ImmediateDelete = false;
887   clearTiles();
888   clearObjects();
889   ImmediateDelete = olddel;
890   CollectGarbage(true); // destroy delayed objects too
891   global.darkLevel = false;
895 final void clearTiles () {
896   accumTime = 0;
897   time = 0;
898   allEnters.length -= allEnters.length; // don't deallocate
899   allExits.length -= allExits.length; // don't deallocate
900   lavatiles.length -= lavatiles.length;
901   foreach (ref auto tile; tiles) delete tile;
902   if (dumpGridStats) { if (miscTileGrid.getFirstObject()) miscTileGrid.dumpStats(); }
903   miscTileGrid.removeAllObjects(true); // and destroy
904   while (backtiles) {
905     MapBackTile t = backtiles;
906     backtiles = t.next;
907     delete t;
908   }
909   levBGImg = none;
913 // ////////////////////////////////////////////////////////////////////////// //
914 final int countObjects () {
915   return objGrid.countObjects();
918 final int countBackTiles () {
919   int res = 0;
920   for (MapBackTile bt = backtiles; bt; bt = bt.next) ++res;
921   return res;
924 final void clearObjects () {
925   // don't kill objects player is holding
926   if (player) {
927     if (player.pickedItem isa ItemBall) {
928       player.pickedItem.instanceRemove();
929       player.pickedItem = none;
930     }
931     if (player.pickedItem && player.pickedItem.grid) {
932       player.pickedItem.grid.remove(player.pickedItem.gridId);
933       writeln("secured pocket item '", GetClassName(player.pickedItem.Class), "'");
934       //player.pickedItem.grid = none;
935     }
936     if (player.holdItem isa ItemBall) {
937       player.removeBallAndChain(temp:true);
938       player.holdItem.instanceRemove();
939       player.holdItem = none;
940     }
941     if (player.holdItem && player.holdItem.grid) {
942       player.holdItem.grid.remove(player.holdItem.gridId);
943       writeln("secured held item '", GetClassName(player.holdItem.Class), "'");
944       //player.holdItem.grid = none;
945     }
946   }
947   //
948   int count = objGrid.countObjects();
949   if (dumpGridStats) { if (objGrid.getFirstObject()) objGrid.dumpStats(); }
950   objGrid.removeAllObjects(true); // and destroy
951   if (count > 0) writeln(count, " objects destroyed");
952   ballObjects.length = 0;
953   lastUsedObjectId = 0;
957 final void insertObject (MapObject o) {
958   if (!o) return;
959   if (o.grid) FatalError("cannot put object into level twice");
960   o.objId = ++lastUsedObjectId;
962   // ball from ball-and-chain
963   if (o isa ItemBall) {
964     bool found = false;
965     int emptyBallIdx = -1;
966     foreach (auto idx, MapObject bo; ballObjects) {
967       if (bo == o) { found = true; break; }
968       if (emptyBallIdx < 0 && (!bo || !bo.isInstanceAlive)) emptyBallIdx = idx;
969     }
970     if (!found) {
971       if (emptyBallIdx < 0) {
972         ballObjects[$] = o;
973       } else {
974         ballObjects[emptyBallIdx] = o;
975       }
976     }
977   }
979   objGrid.insert(o);
983 final void spawnPlayerAt (int x, int y) {
984   // if we have no player, spawn new one
985   // otherwise this just a level transition, so simply reposition him
986   if (!player) {
987     // don't add player to object list, as it has very separate processing anyway
988     player = SpawnObject(PlayerPawn);
989     player.global = global;
990     player.level = self;
991     if (!player.initialize()) {
992       delete player;
993       FatalError("something is wrong with player initialization");
994       return;
995     }
996   }
997   player.fltx = x;
998   player.flty = y;
999   player.saveInterpData();
1000   player.resurrect();
1001   if (player.mustBeChained || global.config.scumBallAndChain) player.spawnBallAndChain();
1002   playerExited = false;
1003   if (global.config.startWithKapala) global.hasKapala = true;
1004   centerViewAtPlayer();
1005   // reinsert player items into grid
1006   if (player.pickedItem) objGrid.insert(player.pickedItem);
1007   if (player.holdItem) objGrid.insert(player.holdItem);
1008   //writeln("player spawned; active=", player.active);
1009   player.scrSwitchToPocketItem(forceIfEmpty:false);
1013 final void teleportPlayerTo (int x, int y) {
1014   if (player) {
1015     player.fltx = x;
1016     player.flty = y;
1017     player.saveInterpData();
1018   }
1022 final void resurrectPlayer () {
1023   if (player) player.resurrect();
1024   playerExited = false;
1028 // ////////////////////////////////////////////////////////////////////////// //
1029 final void scrShake (int duration) {
1030   if (shakeLeft == 0) {
1031     shakeOfs.x = 0;
1032     shakeOfs.y = 0;
1033     shakeDir.x = 0;
1034     shakeDir.y = 0;
1035   }
1036   shakeLeft = max(shakeLeft, duration);
1041 // ////////////////////////////////////////////////////////////////////////// //
1042 enum SCAnger {
1043   TileDestroyed,
1044   ItemStolen, // including damsel, lol
1045   CrapsCheated,
1046   BombDropped,
1047   DamselWhipped,
1051 // make the nearest shopkeeper angry. RAWR!
1052 void scrShopkeeperAnger (SCAnger reason, optional int maxdist, optional MapEntity offender) {
1053   if (!offender) offender = player;
1054   auto shp = MonsterShopkeeper(findNearestEnemy(offender.ix, offender.iy, delegate bool (MapEnemy o) {
1055     auto sc = MonsterShopkeeper(o);
1056     if (!sc) return false;
1057     if (sc.dead || sc.angered) return false;
1058     return true;
1059   }));
1061   if (shp) {
1062     if (specified_maxdist && offender.directionToEntityCenter(shp) > maxdist) return;
1063     if (!shp.dead && !shp.angered) {
1064       shp.status = MapObject::ATTACK;
1065       string msg;
1066            if (global.murderer) msg = "YOU'LL PAY FOR YOUR CRIMES!";
1067       else if (reason == SCAnger.ItemStolen) msg = "COME BACK HERE, THIEF!";
1068       else if (reason == SCAnger.TileDestroyed) msg = "DIE, YOU VANDAL!";
1069       else if (reason == SCAnger.BombDropped) msg = "TERRORIST!";
1070       else if (reason == SCAnger.DamselWhipped) msg = "HEY, ONLY I CAN DO THAT!";
1071       else if (reason == SCAnger.CrapsCheated) msg = "DIE, CHEATER!";
1072       else msg = "NOW I'M REALLY STEAMED!";
1073       if (msg) osdMessage(msg, -666);
1074       global.thiefLevel += (global.thiefLevel > 0 ? 3 : 2);
1075     }
1076   }
1080 final MapObject findCrapsPrize () {
1081   foreach (MapObject o; objGrid.allObjects()) {
1082     if (o.spectral || !o.isInstanceAlive) continue;
1083     if (o.inDiceHouse) return o;
1084   }
1085   return none;
1089 // ////////////////////////////////////////////////////////////////////////// //
1090 // 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.
1091 // note: idols moved by monkeys will have false `stolenIdol`
1092 void scrTriggerIdolAltar (bool stolenIdol) {
1093   ObjTikiCurse res = none;
1094   int curdistsq = int.max;
1095   int px = player.xCenter, py = player.yCenter;
1096   foreach (MapObject o; objGrid.allObjects()) {
1097     auto tcr = ObjTikiCurse(o);
1098     if (!tcr || !tcr.isInstanceAlive) continue;
1099     if (tcr.activated) continue;
1100     int xc = px-tcr.xCenter, yc = py-tcr.yCenter;
1101     int distsq = xc*xc+yc*yc;
1102     if (distsq < curdistsq) {
1103       res = tcr;
1104       curdistsq = distsq;
1105     }
1106   }
1107   if (res) res.activate(stolenIdol);
1111 // ////////////////////////////////////////////////////////////////////////// //
1112 void setupGhostTime () {
1113   musicFadeTimer = -1;
1114   ghostSpawned = false;
1116   // there is no ghost on the first level
1117   if (inWinCutscene || !isNormalLevel() || lg.finalBossLevel || global.currLevel == 1) {
1118     ghostTimeLeft = -1;
1119     global.setMusicPitch(1.0);
1120     return;
1121   }
1123   if (global.config.scumGhost < 0) {
1124     // instant
1125     ghostTimeLeft = 1;
1126     osdMessage("A GHASTLY VOICE CALLS YOUR NAME!\nLET'S GET OUT OF HERE!", 6.66, urgent:true);
1127     return;
1128   }
1130   if (global.config.scumGhost == 0) {
1131     // never
1132     ghostTimeLeft = -1;
1133     return;
1134   }
1136   // randomizes time until ghost appears once time limit is reached
1137   // global.ghostRandom (true/false) if extra time is randomized - loaded from yasm.cfg. if false, use 30 second default
1138   // ghostTimeLeft (time in seconds * 1000) for currently generated level
1140   if (global.config.ghostRandom) {
1141     auto tMin = global.randOther(10, 30); // min(global.plife*10, 30);
1142     auto tMax = max(tMin+global.randOther(10, 30), min(global.config.scumGhost, 300)); // 5 minutes max
1143     auto tTime = global.randOther(tMin, tMax);
1144     if (tTime <= 0) tTime = round(tMax/2.0);
1145     ghostTimeLeft = tTime;
1146   } else {
1147     ghostTimeLeft = global.config.scumGhost; // 5 minutes max
1148   }
1150   ghostTimeLeft += max(0, global.config.ghostExtraTime);
1152   ghostTimeLeft *= 30; // seconds -> frames
1153   //global.ghostShowTime
1157 void spawnGhost () {
1158   addGhostSummoned();
1159   ghostSpawned = true;
1160   ghostTimeLeft = -1;
1162   int vwdt = (viewMax.x-viewMin.x);
1163   int vhgt = (viewMax.y-viewMin.y);
1165   int gx, gy;
1167   if (player.ix < viewMin.x+vwdt/2) {
1168     // player is in the left side
1169     gx = viewMin.x+vwdt/2+vwdt/4;
1170   } else {
1171     // player is in the right side
1172     gx = viewMin.x+vwdt/4;
1173   }
1175   if (player.iy < viewMin.y+vhgt/2) {
1176     // player is in the left side
1177     gy = viewMin.y+vhgt/2+vhgt/4;
1178   } else {
1179     // player is in the right side
1180     gy = viewMin.y+vhgt/4;
1181   }
1183   writeln("spawning ghost at tile (", gx/16, ",", gy/16, "); player is at tile (", player.ix/16, ",", player.iy/16, ")");
1185   MakeMapObject(gx, gy, 'oGhost');
1187   /*
1188     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);
1189     else instance_create(view_xview[0]-32, view_yview[0]+floor(view_hview[0]/2), oGhost);
1190     global.ghostExists = true;
1191   */
1195 void thinkFrameGameGhost () {
1196   if (player.dead) return;
1197   if (!isNormalLevel()) return; // just in case
1199   if (ghostTimeLeft < 0) {
1200     // turned off
1201     if (musicFadeTimer > 0) {
1202       musicFadeTimer = -1;
1203       global.setMusicPitch(1.0);
1204     }
1205     return;
1206   }
1208   if (musicFadeTimer >= 0) {
1209     ++musicFadeTimer;
1210     if (musicFadeTimer > 0 && musicFadeTimer <= 100*3) {
1211       float pitch = 1.0-(0.00226/3.0)*musicFadeTimer;
1212       //writeln("MFT: ", musicFadeTimer, "; pitch=", pitch);
1213       global.setMusicPitch(pitch);
1214     }
1215   }
1217   if (ghostTimeLeft == 0) {
1218     // she is already here!
1219     return;
1220   }
1222   // no ghost if we have a crown
1223   if (global.hasCrown) {
1224     ghostTimeLeft = -1;
1225     return;
1226   }
1228   // if she was already spawned, don't do it again
1229   if (ghostSpawned) {
1230     ghostTimeLeft = 0;
1231     return;
1232   }
1234   if (--ghostTimeLeft != 0) {
1235     // warning
1236     if (global.config.ghostExtraTime > 0) {
1237       if (ghostTimeLeft == global.config.ghostExtraTime*30) {
1238         osdMessage("A CHILL RUNS UP YOUR SPINE...\nLET'S GET OUT OF HERE!", 6.66, urgent:true);
1239       }
1240       if (musicFadeTimer < 0 && ghostTimeLeft <= global.config.ghostExtraTime*30) {
1241         musicFadeTimer = 0;
1242       }
1243     }
1244     return;
1245   }
1247   // spawn her
1248   if (player.isExitingSprite) {
1249     // no reason to spawn her, we're leaving
1250     ghostTimeLeft = -1;
1251     return;
1252   }
1254   spawnGhost();
1258 void thinkFrameGame () {
1259   thinkFrameGameGhost();
1260   // udjat eye blinking
1261   if (global.hasUdjatEye && player) {
1262     foreach (MapTile t; allExits) {
1263       if (t isa MapTileBlackMarketDoor) {
1264         auto dm = int(player.distanceToEntity(t));
1265         if (dm < 4) dm = 4;
1266         if (udjatAlarm < 1 || dm < udjatAlarm) udjatAlarm = dm;
1267       }
1268     }
1269   } else {
1270     global.udjatBlink = false;
1271     udjatAlarm = 0;
1272   }
1273   if (udjatAlarm > 0) {
1274     if (--udjatAlarm == 0) {
1275       global.udjatBlink = !global.udjatBlink;
1276       if (global.hasUdjatEye && player) {
1277         player.playSound(global.udjatBlink ? 'sndBlink1' : 'sndBlink2');
1278       }
1279     }
1280   }
1284 // ////////////////////////////////////////////////////////////////////////// //
1285 private transient array!MapObject activeThinkerList;
1288 private final bool isWetTile (MapTile t) {
1289   return (t && t.visible && (t.water || t.lava || t.wet));
1293 private final bool isWetOrSolidTile (MapTile t) {
1294   return (t && t.visible && (t.water || t.lava || t.wet || t.solid) && t.isInstanceAlive);
1298 final bool isWetOrSolidTileAtPoint (int px, int py) {
1299   return !!checkTileAtPoint(px, py, &isWetOrSolidTile);
1303 final bool isWetOrSolidTileAtTile (int tx, int ty) {
1304   return !!checkTileAtPoint(tx*16, ty*16, &isWetOrSolidTile);
1308 final bool isWetTileAtTile (int tx, int ty) {
1309   return !!checkTileAtPoint(tx*16, ty*16, &isWetTile);
1313 // ////////////////////////////////////////////////////////////////////////// //
1314 const int GreatLakeStartTileY = 28;
1316 // called once after level generation
1317 final void fixLiquidTop () {
1318   foreach (int tileY; 0..tilesHeight) {
1319     foreach (int tileX; 0..tilesWidth) {
1320       auto t = tiles[tileX, tileY];
1322       if (t && !t.isInstanceAlive) {
1323         delete tiles[tileX, tileY];
1324         t = none;
1325       }
1327       if (!t) {
1328         if (global.lake && tileY >= GreatLakeStartTileY) {
1329           // fill level with water for lake
1330           MakeMapTile(tileX, tileY, 'oWaterSwim');
1331           t = tiles[tileX, tileY];
1332         } else {
1333           continue;
1334         }
1335       }
1337       if (!t.water && !t.lava) {
1338         // mark as wet for lake
1339         if (global.lake && tileY >= GreatLakeStartTileY) {
1340           t.wet = true;
1341         }
1342         continue;
1343       }
1345       if (!isWetTileAtTile(tileX, tileY-1)) {
1346         t.setSprite(t.lava ? 'sLavaTop' : 'sWaterTop');
1347       } else {
1348              if (t.spriteName == 'sWaterTop') t.setSprite('sWater');
1349         else if (t.spriteName == 'sLavaTop') t.setSprite('sLava');
1350       }
1351     }
1352   }
1356 private final void checkWaterFlow (MapTile wtile) {
1357   //if (!wtile || (!wtile.water && !wtile.lava)) return;
1358   //instance_activate_region(x-16, y-16, 48, 48, true);
1360   //int x = wtile.ix, y = wtile.iy;
1361   int tileX = wtile.ix/16, tileY = wtile.iy/16;
1363   if (global.lake && tileY >= GreatLakeStartTileY) return;
1365   /*
1366   if ((not collision_point(x-16, y, oSolid, 0, 0) and not collision_point(x-16, y, oWater, 0, 0)) or
1367       (not collision_point(x+16, y, oSolid, 0, 0) and not collision_point(x+16, y, oWater, 0, 0)) or
1368       (not collision_point(x, y+16, oSolid, 0, 0) and not collision_point(x, y+16, oWater, 0, 0)))
1369   */
1370   if (!isWetOrSolidTileAtTile(tileX-1, tileY) ||
1371       !isWetOrSolidTileAtTile(tileX+1, tileY) ||
1372       !isWetOrSolidTileAtTile(tileX, tileY+1))
1373   {
1374     checkWater = true;
1375     wtile.smashMe();
1376     wtile.instanceRemove();
1377     wtile.onDestroy();
1378     delete wtile;
1379     tiles[tileX, tileY] = none;
1380     return;
1381   }
1383   //if (!isSolidAtPoint(x, y-16) && !isLiquidAtPoint(x, y-16)) {
1384   if (!isWetTileAtTile(tileX, tileY-1)) {
1385     wtile.setSprite(wtile.lava ? 'sLavaTop' : 'sWaterTop');
1386   }
1390 transient private array!MapTile waterTilesToCheck;
1392 final void cleanDeadTiles () {
1393   bool hasWater = false;
1394   waterTilesToCheck.length -= waterTilesToCheck.length;
1395   foreach (int y; 0..tilesHeight) {
1396     foreach (int x; 0..tilesWidth) {
1397       auto t = tiles[x, y];
1398       if (!t) continue;
1399       if (t.isInstanceAlive) {
1400         if (t.water || t.lava) waterTilesToCheck[$] = t;
1401         continue;
1402       }
1403       checkWater = true;
1404       t.onDestroy();
1405       delete t;
1406       tiles[x, y] = none;
1407     }
1408   }
1409   if (waterTilesToCheck.length && checkWater && !blockWaterChecking) {
1410     //writeln("checking water");
1411     checkWater = false; // `checkWaterFlow()` can set it again
1412     foreach (MapTile t; waterTilesToCheck) {
1413       if (t && t.isInstanceAlive && (t.water || t.lava)) checkWaterFlow(t);
1414     }
1415     // fill empty spaces in lake with water
1416     if (global.lake) {
1417       foreach (int y; GreatLakeStartTileY..tilesHeight) {
1418         foreach (int x; 0..tilesWidth) {
1419           auto t = tiles[x, y];
1420           // just in case
1421           if (t && !t.isInstanceAlive) {
1422             t.onDestroy();
1423             delete tiles[x, y];
1424             t = none;
1425           }
1426           if (t) {
1427             if (!t.water || !t.lava) { t.wet = true; continue; }
1428           } else {
1429             MakeMapTile(x, y, 'oWaterSwim');
1430             t = tiles[x, y];
1431           }
1432           if (t.water) {
1433             t.setSprite(y == GreatLakeStartTileY ? 'sWaterTop' : 'sWater');
1434           } else if (t.lava) {
1435             t.setSprite(y == GreatLakeStartTileY ? 'sLavaTop' : 'sLava');
1436           }
1437         }
1438       }
1439     }
1440   }
1444 // ////////////////////////////////////////////////////////////////////////// //
1445 void collectLavaTiles () {
1446   lavatiles.length -= lavatiles.length;
1447   foreach (MapTile t; tiles) {
1448     if (t && t.lava && t.isInstanceAlive) lavatiles[$] = t;
1449   }
1453 void processLavaTiles () {
1454   int tn = 0, tlen = lavatiles.length;
1455   while (tn < tlen) {
1456     MapTile t = lavatiles[tn];
1457     if (t && t.isInstanceAlive) {
1458       t.thinkFrame();
1459       ++tn;
1460     } else {
1461       lavatiles.remove(tn, 1);
1462       --tlen;
1463     }
1464   }
1468 // ////////////////////////////////////////////////////////////////////////// //
1469 // return `true` if thinker should be removed
1470 final bool thinkOne (MapObject o) {
1471   if (!o) return true;
1472   if (o.active && o.isInstanceAlive) {
1473     bool doThink = true;
1475     // collision with player weapon
1476     auto hh = PlayerWeapon(player.holdItem);
1477     bool doWeaponAction;
1478     if (hh) {
1479       if (hh.blockedBySolids) {
1480         int xx = player.ix+(player.dir == MapObject::Dir.Left ? -16 : 16);
1481         doWeaponAction = !isSolidAtPoint(xx, player.iy);
1482       } else {
1483         doWeaponAction = true;
1484       }
1485     } else {
1486       doWeaponAction = false;
1487     }
1489     if (doWeaponAction && o.whipTimer <= 0 && hh && hh.collidesWithObject(o)) {
1490       //writeln("WEAPONED!");
1491       if (!o.onTouchedByPlayerWeapon(player, hh)) {
1492         if (o.isInstanceAlive) hh.onCollisionWithObject(o);
1493       }
1494       o.whipTimer = o.whipTimerValue; //HACK
1495       doThink = o.isInstanceAlive;
1496     }
1498     // collision with player
1499     if (doThink && o.collidesWith(player)) {
1500       if (!player.onObjectTouched(o) && o.isInstanceAlive) {
1501         doThink = !o.onTouchedByPlayer(player);
1502         if (o.grid && o.gridId) o.grid.update(o.gridId, markAsDead:true);
1503       }
1504     }
1506     if (doThink && o.isInstanceAlive) {
1507       o.saveInterpData();
1508       o.processAlarms();
1509       if (o.isInstanceAlive) {
1510         if (o.grid && o.gridId) o.grid.update(o.gridId, markAsDead:true);
1511         o.thinkFrame();
1512         if (o.isInstanceAlive) {
1513           o.nextAnimFrame();
1514           if (o.grid && o.gridId) o.grid.update(o.gridId, markAsDead:true);
1515         }
1516       }
1517     }
1518   }
1519   if (o.isInstanceAlive) {
1520     if (!o.canLiveOutsideOfLevel && !o.heldBy && o.isOutsideOfLevel()) {
1521       //dead
1522       o.instanceRemove();
1523       return true;
1524     }
1525     // alive
1526     return false;
1527   } else {
1528     // dead
1529     return true;
1530   }
1534 final void processThinkers (float timeDelta) {
1535   if (timeDelta <= 0) return;
1536   if (gamePaused) {
1537     if (onBeforeFrame) onBeforeFrame(false);
1538     if (onAfterFrame) onAfterFrame(false);
1539     keysNextFrame();
1540     return;
1541   }
1542   accumTime += timeDelta;
1543   bool wasFrame = false;
1544   // block GC
1545   auto olddel = ImmediateDelete;
1546   ImmediateDelete = false;
1547   while (accumTime >= FrameTime) {
1548     accumTime -= FrameTime;
1549     if (onBeforeFrame) onBeforeFrame(accumTime >= FrameTime);
1550     // shake
1551     if (shakeLeft > 0) {
1552       --shakeLeft;
1553       if (!shakeDir.x) shakeDir.x = (global.randOther(0, 1) ? -3 : 3);
1554       if (!shakeDir.y) shakeDir.y = (global.randOther(0, 1) ? -3 : 3);
1555       shakeOfs.x = shakeDir.x;
1556       shakeOfs.y = shakeDir.y;
1557       int sgnc = global.randOther(1, 3);
1558       if (sgnc&0x01) shakeDir.x = -shakeDir.x;
1559       if (sgnc&0x02) shakeDir.y = -shakeDir.y;
1560     } else {
1561       shakeOfs.x = 0;
1562       shakeOfs.y = 0;
1563       shakeDir.x = 0;
1564       shakeDir.y = 0;
1565     }
1566     // game-global events
1567     thinkFrameGame();
1568     // frame thinkers: lava tiles
1569     processLavaTiles();
1570     // frame thinkers: player
1571     if (player && !disablePlayerThink) {
1572       // time limit
1573       if (!player.dead && isNormalLevel() &&
1574           (maxPlayingTime < 0 ||
1575            (maxPlayingTime > 0 && maxPlayingTime*30 <= time &&
1576             time%30 == 0 && global.randOther(1, 100) <= 20)))
1577       {
1578         MakeMapObject(player.ix, player.iy, 'oExplosion');
1579       }
1580       //HACK: check for stolen items
1581       auto item = MapItem(player.holdItem);
1582       if (item) item.onCheckItemStolen(player);
1583       item = MapItem(player.pickedItem);
1584       if (item) item.onCheckItemStolen(player);
1585       // normal thinking
1586       player.saveInterpData();
1587       player.processAlarms();
1588       if (player.isInstanceAlive) {
1589         player.thinkFrame();
1590         if (player.isInstanceAlive) player.nextAnimFrame();
1591       }
1592     }
1593     // frame thinkers: moveable solids
1594     physStep();
1595     // frame thinkers: objects
1596     auto grid = objGrid;
1597     // collect active objects
1598     if (global.config.useFrozenRegion) {
1599       activeThinkerList.length -= activeThinkerList.length;
1600       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)) {
1601         activeThinkerList[$] = o;
1602       }
1603       //writeln("thinkers: ", activeThinkerList.length);
1604       foreach (MapObject o; activeThinkerList) {
1605         if (thinkOne(o)) {
1606           grid.remove(o.gridId);
1607           o.onDestroy();
1608           delete o;
1609         }
1610       }
1611     } else {
1612       // no frozen area
1613       bool killThisOne = false;
1614       for (int cid = grid.getFirstObject(); cid; cid = grid.getNextObject(cid, killThisOne)) {
1615         killThisOne = false;
1616         MapObject o = grid.getObject(MapObject, cid);
1617         if (!o) { killThisOne = true; continue; }
1618         // remove this object if it is dead
1619         if (thinkOne(o)) {
1620           killThisOne = true;
1621           if (o) {
1622             o.onDestroy();
1623             delete o;
1624           }
1625         }
1626       }
1627     }
1628     if (player && player.holdItem) {
1629       if (player.holdItem.isInstanceAlive) {
1630         player.holdItem.fixHoldCoords();
1631       } else {
1632         player.holdItem = none;
1633       }
1634     }
1635     // done with thinkers
1636     cleanDeadTiles();
1637     // money counter
1638     if (collectCounter == 0) {
1639       xmoney = max(0, xmoney-100);
1640     } else {
1641       --collectCounter;
1642     }
1643     // other things
1644     if (player && !player.dead) stats.oneMoreFramePlayed();
1645     if (onAfterFrame) onAfterFrame(accumTime >= FrameTime);
1646     keysNextFrame();
1647     wasFrame = true;
1648     if (!player.visible && player.holdItem) player.holdItem.visible = false;
1649     if (winCutsceneSwitchToNext) {
1650       winCutsceneSwitchToNext = false;
1651       switch (++inWinCutscene) {
1652         case 2: startWinCutsceneVolcano(); break;
1653         case 3: default: startWinCutsceneWinFall(); break;
1654       }
1655       break;
1656     }
1657     if (playerExited) break;
1658   }
1659   ImmediateDelete = olddel;
1660   if (playerExited) {
1661     playerExited = false;
1662     onLevelExited();
1663   }
1664   if (wasFrame) {
1665     // if we were processed at least one frame, collect garbage
1666     //keysNextFrame();
1667     CollectGarbage(true); // destroy delayed objects too
1668   }
1669   if (accumTime > 0 && onInterFrame) onInterFrame(accumTime/FrameTime);
1673 // ////////////////////////////////////////////////////////////////////////// //
1674 final void tileXY2roomXY (int tileX, int tileY, out int roomX, out int roomY) {
1675   roomX = (tileX-1)/RoomGen::Width;
1676   roomY = (tileY-1)/RoomGen::Height;
1680 final bool isInShop (int tileX, int tileY) {
1681   if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
1682     auto n = roomType[tileX, tileY];
1683     if (n == 4 || n == 5) return true;
1684     auto t = getTileAt(tileX, tileY);
1685     if (t && t.shopWall) return true;
1686     //k8: we don't have this
1687     //if (t && t.objType == 'oShop') return true;
1688   }
1689   return false;
1693 // ////////////////////////////////////////////////////////////////////////// //
1694 override void Destroy () {
1695   clearTiles();
1696   clearObjects();
1697   delete tempSolidTile;
1698   ::Destroy();
1702 // ////////////////////////////////////////////////////////////////////////// //
1703 final MapObject findNearestBall (int px, int py) {
1704   MapObject res = none;
1705   int curdistsq = int.max;
1706   foreach (MapObject o; ballObjects) {
1707     if (!o || o.spectral || !o.isInstanceAlive) continue;
1708     int xc = px-o.xCenter, yc = py-o.yCenter;
1709     int distsq = xc*xc+yc*yc;
1710     if (distsq < curdistsq) {
1711       res = o;
1712       curdistsq = distsq;
1713     }
1714   }
1715   return res;
1719 final int calcNearestBallDist (int px, int py) {
1720   auto e = findNearestBall(px, py);
1721   if (!e) return int.max;
1722   int xc = px-e.xCenter, yc = py-e.yCenter;
1723   return round(sqrt(xc*xc+yc*yc));
1727 final MapObject findNearestObject (int px, int py, bool delegate (MapObject o) dg) {
1728   MapObject res = none;
1729   int curdistsq = int.max;
1730   foreach (MapObject o; objGrid.allObjects()) {
1731     if (o.spectral || !o.isInstanceAlive) continue;
1732     if (!dg(o)) continue;
1733     int xc = px-o.xCenter, yc = py-o.yCenter;
1734     int distsq = xc*xc+yc*yc;
1735     if (distsq < curdistsq) {
1736       res = o;
1737       curdistsq = distsq;
1738     }
1739   }
1740   return res;
1744 final MapObject findNearestEnemy (int px, int py, optional bool delegate (MapEnemy o) dg) {
1745   MapObject res = none;
1746   int curdistsq = int.max;
1747   foreach (MapObject o; objGrid.allObjects()) {
1748     //k8: i added `dead` check
1749     if (o.spectral || o !isa MapEnemy || o.dead || !o.isInstanceAlive) continue;
1750     if (dg) {
1751       if (!dg(MapEnemy(o))) continue;
1752     }
1753     int xc = px-o.xCenter, yc = py-o.yCenter;
1754     int distsq = xc*xc+yc*yc;
1755     if (distsq < curdistsq) {
1756       res = o;
1757       curdistsq = distsq;
1758     }
1759   }
1760   return res;
1764 final MonsterShopkeeper findNearestCalmShopkeeper (int px, int py) {
1765   auto obj = MonsterShopkeeper(findNearestEnemy(px, py, delegate bool (MapEnemy o) {
1766     auto sk = MonsterShopkeeper(o);
1767     if (sk && !sk.angered) return true;
1768     return false;
1769   }));
1770   return obj;
1774 final MonsterShopkeeper hasAliveShopkeepers (optional bool skipAngry) {
1775   foreach (MapObject o; objGrid.allObjects()) {
1776     auto sc = MonsterShopkeeper(o);
1777     if (!sc || o.spectral || !o.isInstanceAlive) continue;
1778     if (sc.dead) continue;
1779     if (skipAngry && sc.angered) continue;
1780     return sc;
1781   }
1782   return none;
1786 final int calcNearestEnemyDist (int px, int py, optional bool delegate (MapEnemy o) dg) {
1787   auto e = findNearestEnemy(px, py, dg!optional);
1788   if (!e) return int.max;
1789   int xc = px-e.xCenter, yc = py-e.yCenter;
1790   return round(sqrt(xc*xc+yc*yc));
1794 final int calcNearestObjectDist (int px, int py, optional bool delegate (MapObject o) dg) {
1795   auto e = findNearestObject(px, py, dg!optional);
1796   if (!e) return int.max;
1797   int xc = px-e.xCenter, yc = py-e.yCenter;
1798   return round(sqrt(xc*xc+yc*yc));
1802 final MapTile findNearestMoveableSolid (int px, int py, optional bool delegate (MapTile t) dg) {
1803   MapTile res = none;
1804   int curdistsq = int.max;
1805   foreach (MapTile t; miscTileGrid.allObjects()) {
1806     if (t.spectral || !t.isInstanceAlive) continue;
1807     if (dg) {
1808       if (!dg(t)) continue;
1809     } else {
1810       if (!t.solid || !t.moveable) continue;
1811     }
1812     int xc = px-t.xCenter, yc = py-t.yCenter;
1813     int distsq = xc*xc+yc*yc;
1814     if (distsq < curdistsq) {
1815       res = t;
1816       curdistsq = distsq;
1817     }
1818   }
1819   return res;
1823 final MapTile findNearestTile (int px, int py, optional bool delegate (MapTile t) dg) {
1824   if (!dg) return none;
1825   MapTile res = none;
1826   int curdistsq = int.max;
1828   //FIXME: make this faster!
1829   foreach (MapTile t; tiles) {
1830     if (!t || t.spectral || !t.isInstanceAlive) continue;
1831     int xc = px-t.xCenter, yc = py-t.yCenter;
1832     int distsq = xc*xc+yc*yc;
1833     if (distsq < curdistsq && dg(t)) {
1834       res = t;
1835       curdistsq = distsq;
1836     }
1837   }
1839   foreach (MapTile t; miscTileGrid.allObjects()) {
1840     if (!t || t.spectral || !t.isInstanceAlive) continue;
1841     int xc = px-t.xCenter, yc = py-t.yCenter;
1842     int distsq = xc*xc+yc*yc;
1843     if (distsq < curdistsq && dg(t)) {
1844       res = t;
1845       curdistsq = distsq;
1846     }
1847   }
1849   return res;
1853 // ////////////////////////////////////////////////////////////////////////// //
1854 final bool cbIsObjectWeb (MapObject o) { return (o isa ItemWeb); }
1855 final bool cbIsObjectBlob (MapObject o) { return (o.objName == 'oBlob'); }
1856 final bool cbIsObjectArrow (MapObject o) { return (o isa ItemProjectileArrow); }
1857 final bool cbIsObjectEnemy (MapObject o) { return (/*o != self &&*/ o isa MapEnemy); }
1859 final bool cbIsObjectTreasure (MapObject o) { return (o.isTreasure); }
1861 final bool cbIsItemObject (MapObject o) { return (o isa MapItem); }
1863 final bool cbIsItemOrEnemyObject (MapObject o) { return (o isa MapItem || o isa MapEnemy); }
1866 final MapObject isObjectAtTile (int tileX, int tileY, optional bool delegate (MapObject o) dg) {
1867   tileX *= 16;
1868   tileY *= 16;
1869   foreach (MapObject o; objGrid.inRectPix(tileX, tileY, 16, 16, objGrid.nextTag(), precise: true)) {
1870     if (o.spectral || !o.isInstanceAlive) continue;
1871     if (dg) {
1872       if (dg(o)) return o;
1873     } else {
1874       return o;
1875     }
1876   }
1877   return none;
1881 final MapObject isObjectAtTilePix (int x, int y, optional bool delegate (MapObject o) dg) {
1882   return isObjectAtTile(x/16, y/16, dg!optional);
1886 final MapObject isObjectAtPoint (int xpos, int ypos, optional bool delegate (MapObject o) dg, optional bool precise) {
1887   if (!specified_precise) precise = true;
1888   foreach (MapObject o; objGrid.inCellPix(xpos, ypos, objGrid.nextTag(), precise: precise)) {
1889     if (o.spectral || !o.isInstanceAlive) continue;
1890     if (dg) {
1891       if (dg(o)) return o;
1892     } else {
1893       if (o isa MapEnemy) return o;
1894     }
1895   }
1896   return none;
1900 final MapObject isObjectInRect (int xpos, int ypos, int w, int h, optional bool delegate (MapObject o) dg, optional bool precise) {
1901   if (w < 1 || h < 1) return none;
1902   if (w == 1 && h == 1) return isObjectAtPoint(xpos, ypos, dg!optional, precise!optional);
1903   if (!specified_precise) precise = true;
1904   foreach (MapObject o; objGrid.inRectPix(xpos, ypos, w, h, objGrid.nextTag(), precise: precise)) {
1905     if (o.spectral || !o.isInstanceAlive) continue;
1906     if (dg) {
1907       if (dg(o)) return o;
1908     } else {
1909       if (o isa MapEnemy) return o;
1910     }
1911   }
1912   return none;
1916 final MapObject forEachObject (bool delegate (MapObject o) dg, optional bool allowSpectrals) {
1917   if (!dg) return none;
1918   /*
1919   foreach (MapObject o; objGrid.allObjects()) {
1920     if (o.spectral || !o.isInstanceAlive) continue;
1921     if (dg(o)) return o;
1922   }
1923   */
1924   // process gravity for moveable solids and burning for ropes
1925   auto grid = objGrid;
1926   int cid = grid.getFirstObject();
1927   while (cid) {
1928     MapObject o = grid.getObject(MapObject, cid);
1929     if (!o || !o.isInstanceAlive) {
1930       cid = grid.getNextObject(cid, removeThis:true);
1931       if (o) {
1932         o.instanceRemove(); // just in case
1933         o.onDestroy();
1934         delete o;
1935       }
1936       continue;
1937     }
1938     if (!allowSpectrals && o.spectral) {
1939       cid = grid.getNextObject(cid, removeThis:false);
1940       continue;
1941     }
1942     if (dg(o)) return o;
1943     if (o.isInstanceAlive) {
1944       cid = grid.getNextObject(cid, removeThis:false);
1945     } else {
1946       cid = grid.getNextObject(cid, removeThis:true);
1947       o.instanceRemove(); // just in case
1948       o.onDestroy();
1949       delete o;
1950     }
1951   }
1952   return none;
1956 final MapObject forEachObjectAtPoint (int xpos, int ypos, bool delegate (MapObject o) dg, optional bool precise) {
1957   if (!dg) return none;
1958   if (!specified_precise) precise = true;
1959   foreach (MapObject o; objGrid.inCellPix(xpos, ypos, objGrid.nextTag(), precise: precise)) {
1960     if (o.spectral || !o.isInstanceAlive) continue;
1961     if (dg(o)) return o;
1962   }
1963   return none;
1967 final MapObject forEachObjectInRect (int xpos, int ypos, int w, int h, bool delegate (MapObject o) dg, optional bool precise) {
1968   if (!dg || w < 1 || h < 1) return none;
1969   if (w == 1 && h == 1) return forEachObjectAtPoint(xpos, ypos, dg!optional, precise!optional);
1970   if (!specified_precise) precise = true;
1971   foreach (MapObject o; objGrid.inRectPix(xpos, ypos, w, h, objGrid.nextTag(), precise: precise)) {
1972     if (o.spectral || !o.isInstanceAlive) continue;
1973     if (dg(o)) return o;
1974   }
1975   return none;
1979 private final bool cbIsRopeTile (MapTile t) { return t.rope; }
1981 final MapTile isRopeAtPoint (int px, int py) {
1982   return checkTileAtPoint(px, py, &cbIsRopeTile);
1986 //FIXME!
1987 final MapTile isWaterSwimAtPoint (int px, int py) {
1988   return isWaterAtPoint(px, py);
1992 // ////////////////////////////////////////////////////////////////////////// //
1993 private array!MapObject tmpObjectList;
1995 private final bool cbCollectObjectsWithMask (MapObject t) {
1996   if (!t || !t.visible || t.spectral /*|| t.invincible*/ || !t.isInstanceAlive) return false;
1997   //auto spf = getSpriteFrame();
1998   //if (!t.sprite || t.sprite.frames.length < 1) return false;
1999   tmpObjectList[$] = t;
2000   return false;
2004 final void touchObjectsWithMask (int x, int y, SpriteFrame frm, bool delegate (MapObject t) dg) {
2005   if (!frm || frm.bw < 1 || frm.bh < 1 || !dg) return;
2006   if (frm.isEmptyPixelMask) return;
2007   // collect tiles
2008   if (tmpObjectList.length) tmpObjectList.length -= tmpObjectList.length; // don't realloc
2009   if (player.isRectCollisionFrame(frm, x, y)) {
2010     //writeln("player hit");
2011     tmpObjectList[$] = player;
2012   } else {
2013     /*
2014     writeln("no player hit: plr=(", player.ix, ",", player.iy, ")-(", player.ix+player.width-1, ",", player.iy+player.height-1, "); ",
2015       "frm=(", x+frm.bx, ",", y+frm.by, ")-(", x+frm.bx+frm.bw-1, ",", y+frm.by+frm.bh-1, ")");
2016     */
2017   }
2018   forEachObjectInRect(x+frm.bx, y+frm.by, frm.bw, frm.bh, &cbCollectObjectsWithMask);
2019   foreach (MapObject t; tmpObjectList) {
2020     if (!t || !t.isInstanceAlive || !t.visible || t.spectral) continue;
2021     /*
2022     auto tf = t.getSpriteFrame();
2023     if (!tf) {
2024       //writeln("no sprite frame for ", GetClassName(t.Class));
2025       continue;
2026     }
2027     */
2028     /*
2029     if (frm.pixelCheck(tf, t.ix-tf.xofs-x, t.iy-tf.yofs-y)) {
2030       //writeln("pixel hit for ", GetClassName(t.Class));
2031       if (dg(t)) break;
2032     }
2033     */
2034     if (t.isRectCollisionFrame(frm, x, y)) {
2035       if (dg(t)) break;
2036     }
2037   }
2041 // ////////////////////////////////////////////////////////////////////////// //
2042 final void destroyTileAt (int x, int y) {
2043   if (x < 0 || y < 0 || x >= tilesWidth*16 || y >= tilesHeight*16) return;
2044   x /= 16;
2045   y /= 16;
2046   MapTile t = tiles[x, y];
2047   if (!t || !t.visible || t.spectral || t.invincible || !t.isInstanceAlive) return;
2048   t.instanceRemove();
2049   t.onDestroy();
2050   delete tiles[x, y];
2051   checkWater = true;
2055 private array!MapTile tmpTileList;
2057 private final bool cbCollectTilesWithMask (MapTile t) {
2058   if (!t || !t.visible || t.spectral /*|| t.invincible*/ || !t.isInstanceAlive) return false;
2059   if (!t.sprite || t.sprite.frames.length < 1) return false;
2060   tmpTileList[$] = t;
2061   return false;
2064 final void touchTilesWithMask (int x, int y, SpriteFrame frm, bool delegate (MapTile t) dg) {
2065   if (!frm || frm.bw < 1 || frm.bh < 1 || !dg) return;
2066   if (frm.isEmptyPixelMask) return;
2067   // collect tiles
2068   if (tmpTileList.length) tmpTileList.length -= tmpTileList.length; // don't realloc
2069   checkTilesInRect(x+frm.bx, y+frm.by, frm.bw, frm.bh, &cbCollectTilesWithMask);
2070   foreach (MapTile t; tmpTileList) {
2071     if (!t || !t.isInstanceAlive || !t.visible || t.spectral) continue;
2072     /*
2073     auto tf = t.sprite.frames[0];
2074     if (frm.pixelCheck(tf, t.ix-x, t.iy-y)) {
2075       if (dg(t)) break;
2076       //doCleanup = doCleanup || !t.isInstanceAlive;
2077       //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, ")");
2078     }
2079     */
2080     if (t.isRectCollisionFrame(frm, x, y)) {
2081       if (dg(t)) break;
2082     }
2083   }
2087 // ////////////////////////////////////////////////////////////////////////// //
2088 final bool cbCollisionAnySolid (MapTile t) { return t.solid; }
2089 final bool cbCollisionAnyMoveableSolid (MapTile t) { return t.solid && t.moveable; }
2090 final bool cbCollisionLiquid (MapTile t) { return (t.water || t.lava); }
2091 final bool cbCollisionAnySolidOrLiquid (MapTile t) { return (t.solid || t.water || t.lava); }
2092 final bool cbCollisionLadder (MapTile t) { return t.ladder; }
2093 final bool cbCollisionLadderTop (MapTile t) { return t.laddertop; }
2094 final bool cbCollisionAnyLadder (MapTile t) { return (t.ladder || t.laddertop); }
2095 final bool cbCollisionAnySolidIce (MapTile t) { return (t.solid && t.ice); }
2096 final bool cbCollisionWater (MapTile t) { return t.water; }
2097 final bool cbCollisionLava (MapTile t) { return t.lava; }
2098 final bool cbCollisionAnyTree (MapTile t) { return t.tree; }
2099 final bool cbCollisionPlatform (MapTile t) { return t.platform; }
2100 final bool cbCollisionLadderOrPlatform (MapTile t) { return (t.ladder || t.platform); }
2101 //final bool cbCollisionAnyHangeable (MapTile t) { return t.hangeable; } // no need to check for solids here, it is done in MapTile initializer
2102 final bool cbCollisionSpringTrap (MapTile t) { return t.springtrap; }
2103 final bool cbCollisionSpikes (MapTile t) { return t.spikes; }
2104 final bool cbCollisionExitTile (MapTile t) { return t.isExitActive(); }
2106 final bool cbCollisionForWhoa (MapTile t) { return (t.solid || t.laddertop); }
2108 //final bool cbCollisionSacAltar (MapTile t) { return (t.objName == 'oSacAltarLeft'); } // only left part, yeah
2109 final bool cbCollisionSacAltar (MapTile t) { return t.sacrificingAltar; }
2112 // ////////////////////////////////////////////////////////////////////////// //
2113 transient MapTile tempSolidTile;
2115 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*/) {
2116   //!if (dbgdump) writeln("checkTilesInRect: (", x0, ",", y0, ")-(", x0+w-1, ",", y0+h-1, ") ; w=", w, "; h=", h);
2117   if (w < 1 || h < 1) return none;
2118   if (w == 1 && h == 1) return checkTileAtPoint(x0, y0, dg!optional);
2119   int x1 = x0+w-1, y1 = y0+h-1;
2120   if (x0 >= tilesWidth*16 || y0 >= tilesHeight*16 || x1 < 0 || y1 < 0) return none;
2121   if (!dg) {
2122     //!if (dbgdump) writeln("default checker set");
2123     dg = &cbCollisionAnySolid;
2124   }
2125   //!if (dbgdump) writeln("delegate: ", dg);
2126   int origx0 = x0, origy0 = y0;
2127   int tileSX = max(0, x0)/16;
2128   int tileSY = max(0, y0)/16;
2129   int tileEX = min(tilesWidth*16-1, x1)/16;
2130   int tileEY = min(tilesHeight*16-1, y1)/16;
2131   //!if (dbgdump) writeln("  tiles: (", tileSX, ",", tileSY, ")-(", tileEX, ",", tileEY, ")");
2132   //!!!auto grid = miscTileGrid;
2133   //!!!int tag = grid.nextTag();
2134   for (int ty = tileSY; ty <= tileEY; ++ty) {
2135     for (int tx = tileSX; tx <= tileEX; ++tx) {
2136       MapTile t = tiles[tx, ty];
2137       //!if (dbgdump && t && t.visible && t.isInstanceAlive && t.isRectCollision(origx0, origy0, w, h) ) writeln("   tile: ", GetClassName(t.Class), " : ", t.objName, " : ", t.objType, " : ", dg(t));
2138       if (t && !t.spectral && t.visible && t.isInstanceAlive && t.isRectCollision(origx0, origy0, w, h) && dg(t)) return t;
2139       // moveable tiles are in separate grid
2140       /+
2141       foreach (t; grid.inCellPix(tx*16, ty*16, tag, precise:precise)) {
2142         //if (t.isRectCollision(origx0, origy0, w, h) && dg(t)) return t;
2143         if (t.isInstanceAlive && !t.spectral && t.visible && t.isRectCollision(origx0, origy0, w, h) && dg(t)) return t;
2144       }
2145       +/
2146     }
2147   }
2149   // moveable tiles are in separate grid
2150   foreach (MapTile t; miscTileGrid.inRectPix(x0, y0, w, h, miscTileGrid.nextTag(), precise:precise)) {
2151     //if (t.isRectCollision(origx0, origy0, w, h) && dg(t)) return t;
2152     if (t.isInstanceAlive && !t.spectral && t.visible && t.isRectCollision(origx0, origy0, w, h) && dg(t)) return t;
2153   }
2155   // check walkable solid objects
2156   foreach (MapObject o; objGrid.inRectPix(x0, y0, w, h, objGrid.nextTag(), precise:precise)) {
2157     if (o && !o.spectral && o.visible && o.walkableSolid && o.isInstanceAlive && o.isRectCollision(origx0, origy0, w, h)) {
2158       if (!tempSolidTile) {
2159         tempSolidTile = SpawnObject(MapTile);
2160       } else if (!tempSolidTile.isInstanceAlive) {
2161         delete tempSolidTile;
2162         tempSolidTile = SpawnObject(MapTile);
2163       }
2164       tempSolidTile.solid = true;
2165       if (dg(tempSolidTile)) return tempSolidTile;
2166     }
2167   }
2169   return none;
2173 final MapTile checkTileAtPoint (int x0, int y0, optional bool delegate (MapTile dg) dg) {
2174   if (x0 < 0 || y0 < 0 || x0 >= tilesWidth*16 || y0 >= tilesHeight*16) return none;
2175   if (!dg) dg = &cbCollisionAnySolid;
2176   //if (!self) { writeln("WTF?!"); return none; }
2177   MapTile t = tiles[x0/16, y0/16];
2178   if (t && !t.spectral && t.visible && t.isInstanceAlive && t.isPointCollision(x0, y0) && dg(t)) return t;
2180   // moveable tiles are in separate grid
2181   foreach (t; miscTileGrid.inCellPix(x0, y0, miscTileGrid.nextTag(), precise:true)) {
2182     if (t.isInstanceAlive && !t.spectral && t.visible && dg(t)) return t;
2183   }
2185   // check walkable solid objects
2186   foreach (MapObject o; objGrid.inCellPix(x0, y0, objGrid.nextTag(), precise:true)) {
2187     if (o && !o.spectral && o.visible && o.walkableSolid && o.isInstanceAlive && o.isRectCollision(x0, y0, 1, 1)) {
2188       if (!tempSolidTile) {
2189         tempSolidTile = SpawnObject(MapTile);
2190       } else if (!tempSolidTile.isInstanceAlive) {
2191         delete tempSolidTile;
2192         tempSolidTile = SpawnObject(MapTile);
2193       }
2194       tempSolidTile.solid = true;
2195       if (dg(tempSolidTile)) return tempSolidTile;
2196     }
2197   }
2199   return none;
2203 //FIXME: optimize this with clipping first
2204 //TODO: moveable tiles
2206 final MapTile checkTilesAtLine (int ax0, int ay0, int ax1, int ay1, optional bool delegate (MapTile dg) dg) {
2207   // do it faster if we can
2209   // strict vertical check?
2210   if (ax0 == ax1 && ay0 <= ay1) return checkTilesInRect(ax0, ay0, 1, ay1-ay0+1, dg!optional);
2211   // strict horizontal check?
2212   if (ay0 == ay1 && ax0 <= ax1) return checkTilesInRect(ax0, ay0, ax1-ax0+1, 1, dg!optional);
2214   float x0 = float(ax0)/16.0, y0 = float(ay0)/16.0, x1 = float(ax1)/16.0, y1 = float(ay1)/16.0;
2216   // fix delegate
2217   if (!dg) dg = &cbCollisionAnySolid;
2219   // get starting and enging tile
2220   int tileSX = trunc(x0), tileSY = trunc(y0);
2221   int tileEX = trunc(x1), tileEY = trunc(y1);
2223   // first hit is always landed
2224   if (tileSX >= 0 && tileSY >= 0 && tileSX < TilesWidth && tileSY < TilesHeight) {
2225     MapTile t = tiles[tileSX, tileSY];
2226     if (t && t.visible && Geom.lineAABBIntersects(ax0, ay0, ax1, ay1, tileSX*16, tileSY*16, 16, 16) && dg(t)) return t;
2227   }
2229   // if starting and ending tile is the same, we don't need to do anything more
2230   if (tileSX == tileEX && tileSY == tileEY) return none;
2232   // calculate ray direction
2233   TVec dv = (vector(x1, y1)-vector(x0, y0)).normalise2d;
2235   // length of ray from one x or y-side to next x or y-side
2236   float deltaDistX = (fabs(dv.x) > 0.0001 ? fabs(1.0/dv.x) : 0.0);
2237   float deltaDistY = (fabs(dv.y) > 0.0001 ? fabs(1.0/dv.y) : 0.0);
2239   // calculate step and initial sideDists
2241   float sideDistX; // length of ray from current position to next x-side
2242   int stepX; // what direction to step in x (either +1 or -1)
2243   if (dv.x < 0) {
2244     stepX = -1;
2245     sideDistX = (x0-tileSX)*deltaDistX;
2246   } else {
2247     stepX = 1;
2248     sideDistX = (tileSX+1.0-x0)*deltaDistX;
2249   }
2251   float sideDistY; // length of ray from current position to next y-side
2252   int stepY; // what direction to step in y (either +1 or -1)
2253   if (dv.y < 0) {
2254     stepY = -1;
2255     sideDistY = (y0-tileSY)*deltaDistY;
2256   } else {
2257     stepY = 1;
2258     sideDistY = (tileSY+1.0-y0)*deltaDistY;
2259   }
2261   // perform DDA
2262   //int side; // was a NS or a EW wall hit?
2263   for (;;) {
2264     // jump to next map square, either in x-direction, or in y-direction
2265     if (sideDistX < sideDistY) {
2266       sideDistX += deltaDistX;
2267       tileSX += stepX;
2268       //side = 0;
2269     } else {
2270       sideDistY += deltaDistY;
2271       tileSY += stepY;
2272       //side = 1;
2273     }
2274     // check tile
2275     if (tileSX >= 0 && tileSY >= 0 && tileSX < TilesWidth && tileSY < TilesHeight) {
2276       MapTile t = tiles[tileSX, tileSY];
2277       if (t && t.visible && Geom.lineAABBIntersects(ax0, ay0, ax1, ay1, tileSX*16, tileSY*16, 16, 16) && dg(t)) return t;
2278     }
2279     // did we arrived at the destination?
2280     if (tileSX == tileEX && tileSY == tileEY) break;
2281   }
2283   return none;
2288 // ////////////////////////////////////////////////////////////////////////// //
2289 final MapTile isSolidAtPoint (int px, int py) { return checkTileAtPoint(px, py); }
2290 final MapTile isSolidInRect (int rx, int ry, int rw, int rh) { return checkTilesInRect(rx, ry, rw, rh); }
2291 final MapTile isLadderAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadder); }
2292 final MapTile isLadderOrPlatformAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadderOrPlatform); }
2293 final MapTile isLadderTopAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadderTop); }
2294 final MapTile isAnyLadderAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyLadder); }
2295 final MapTile isWaterAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionWater); }
2296 final MapTile isLavaAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLava); }
2297 final MapTile isLiquidAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLiquid); }
2298 final MapTile isIceAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnySolidIce); }
2299 final MapTile isTreeAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyTree); }
2300 //final MapTile isHangeableAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyHangeable); }
2303 // ////////////////////////////////////////////////////////////////////////// //
2304 // PlayerPawn has it's own movement code, so don't process it here
2305 // but process moveable solids here, yeah
2306 final void physStep () {
2307   // advance time
2308   time += 1;
2309   // we don't want the time to grow too large
2310   if (time > 100000000) time = 0;
2312   auto grid = miscTileGrid;
2314   // process gravity for moveable solids and burning for ropes
2315   int cid = grid.getFirstObject();
2316   while (cid) {
2317     MapTile t = grid.getObject(MapTile, cid);
2318     if (!t) {
2319       cid = grid.getNextObject(cid, removeThis:false);
2320       continue;
2321     }
2322     if (t.isInstanceAlive) {
2323       t.saveInterpData();
2324       t.processAlarms();
2325       if (t.isInstanceAlive) {
2326         grid.update(cid, markAsDead:false);
2327         t.thinkFrame();
2328         if (t.isInstanceAlive && !t.canLiveOutsideOfLevel && t.isOutsideOfLevel()) t.instanceRemove();
2329         grid.update(cid, markAsDead:false);
2330       }
2331     }
2332     if (t.isInstanceAlive) {
2333       cid = grid.getNextObject(cid, removeThis:false);
2334     } else {
2335       cid = grid.getNextObject(cid, removeThis:true);
2336       t.instanceRemove(); // just in case
2337       t.onDestroy();
2338       delete t;
2339       checkWater = true;
2340     }
2341   }
2345 // ////////////////////////////////////////////////////////////////////////// //
2346 final void setTileTypeAt (int tileX, int tileY, LevelGen::RType rt) {
2347   if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) roomType[tileX, tileY] = rt;
2350 final MapTile getTileAt (int tileX, int tileY) {
2351   return (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight ? tiles[tileX, tileY] : none);
2354 final bool isNamedTileAt (int tileX, int tileY, name atypename) {
2355   if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
2356     auto t = tiles[tileX, tileY];
2357     if (t && t.objName == atypename) return true;
2358   }
2359   return false;
2362 final void setTileAt (int tileX, int tileY, MapTile tile) {
2363   if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
2364     //FIXME
2365     if (tiles[tileX, tileY]) checkWater = true;
2366     delete tiles[tileX, tileY];
2367     tiles[tileX, tileY] = tile;
2368   }
2372 // ////////////////////////////////////////////////////////////////////////// //
2373 // return `true` from delegate to stop
2374 MapTile forEachSolidTile (bool delegate (int x, int y, MapTile t) dg) {
2375   if (!dg) return none;
2376   foreach (int y; 0..tilesHeight) {
2377     foreach (int x; 0..tilesWidth) {
2378       auto t = tiles[x, y];
2379       if (t && t.solid && t.visible && t.isInstanceAlive) {
2380         if (dg(x, y, t)) return t;
2381       }
2382     }
2383   }
2384   return none;
2388 // ////////////////////////////////////////////////////////////////////////// //
2389 // return `true` from delegate to stop
2390 MapTile forEachNormalTile (bool delegate (int x, int y, MapTile t) dg) {
2391   if (!dg) return none;
2392   foreach (int y; 0..tilesHeight) {
2393     foreach (int x; 0..tilesWidth) {
2394       auto t = tiles[x, y];
2395       if (t && t.visible && t.isInstanceAlive) {
2396         if (dg(x, y, t)) return t;
2397       }
2398     }
2399   }
2400   return none;
2404 // WARNING! don't destroy tiles here! (instanceRemove() is ok, tho)
2405 MapTile forEachTile (bool delegate (MapTile t) dg) {
2406   if (!dg) return none;
2407   foreach (int y; 0..tilesHeight) {
2408     foreach (int x; 0..tilesWidth) {
2409       auto t = tiles[x, y];
2410       if (t && t.visible && !t.spectral && t.isInstanceAlive) {
2411         if (dg(t)) return t;
2412       }
2413     }
2414   }
2415   /*
2416   foreach (MapObject o; miscTileGrid.allObjects()) {
2417     auto mt = MapTile(o);
2418     if (!mt) continue;
2419     if (mt.visible && !mt.spectral && mt.isInstanceAlive) {
2420       //writeln("special map tile: '", GetClassName(mt.Class), "'");
2421       if (dg(mt)) return mt;
2422     }
2423   }
2424   */
2425   auto grid = miscTileGrid;
2426   int cid = grid.getFirstObject();
2427   while (cid) {
2428     MapTile t = grid.getObject(MapTile, cid);
2429     if (!t || !t.isInstanceAlive) {
2430       cid = grid.getNextObject(cid, removeThis:true);
2431       if (t) {
2432         t.instanceRemove(); // just in case
2433         t.onDestroy();
2434         delete t;
2435       }
2436       continue;
2437     }
2438     if (!t.visible || t.spectral) {
2439       cid = grid.getNextObject(cid, removeThis:true);
2440       continue;
2441     }
2442     if (dg(t)) return t;
2443     if (t.isInstanceAlive) {
2444       cid = grid.getNextObject(cid, removeThis:false);
2445     } else {
2446       cid = grid.getNextObject(cid, removeThis:true);
2447       t.instanceRemove(); // just in case
2448       t.onDestroy();
2449       delete t;
2450     }
2451   }
2452   return none;
2456 // ////////////////////////////////////////////////////////////////////////// //
2457 final void fixWallTiles () {
2458   foreach (int y; 0..tilesHeight) {
2459     foreach (int x; 0..tilesWidth) {
2460       auto t = getTileAt(x, y);
2461       if (!t) continue;
2462       /*
2463       if (y == tilesHeight-2) {
2464         writeln("0: tx=", x, "; tile=", t.objName, "; ty=", y, ": (", t.iy/16, ")");
2465       } else if (y == tilesHeight-1) {
2466         writeln("1: tx=", x, "; tile=", t.objName, "; ty=", y, ": (", t.iy/16, ")");
2467       }
2468       */
2469       t.beautifyTile();
2470     }
2471   }
2472   foreach (MapTile t; miscTileGrid.allObjects()) {
2473     if (t.isInstanceAlive) t.beautifyTile();
2474   }
2478 // ////////////////////////////////////////////////////////////////////////// //
2479 final MapTile isCollisionAtPoint (int px, int py, optional bool delegate (MapTile dg) dg) {
2480   if (!dg) dg = &cbCollisionAnySolid;
2481   return checkTilesInRect(px, py, 1, 1, dg);
2485 // ////////////////////////////////////////////////////////////////////////// //
2486 string scrGetKaliGift (MapTile altar, optional name gift) {
2487   string res;
2489   // find other side of the altar
2490   int sx = player.ix, sy = player.iy;
2491   if (altar) {
2492     sx = altar.ix;
2493     sy = altar.iy;
2494     auto a2 = isCollisionAtPoint(altar.ix+18, altar.iy+8, &cbCollisionSacAltar);
2495     if (!a2) a2 = isCollisionAtPoint(altar.ix-8, altar.iy+8, &cbCollisionSacAltar);
2496     if (a2) { sx = a2.ix; sy = a2.iy; }
2497   }
2499        if (global.favor <= -8) res = "SHE SEEMS VERY ANGRY WITH YOU!";
2500   else if (global.favor < 0) res = "SHE SEEMS ANGRY WITH YOU.";
2501   else if (global.favor == 0) res = "SHE HAS FORGIVEN YOU!";
2502   else if (global.favor >= 32) {
2503     if (global.kaliGift >= 3 && global.favor >= 32+(global.kaliGift-2)*16) {
2504       res = "YOU FEEL INVIGORATED!";
2505       global.kaliGift += 1;
2506       global.plife += global.randOther(4, 8);
2507     } else if (global.kaliGift >= 3) {
2508       res = "SHE SEEMS ECSTATIC WITH YOU!";
2509     } else if (global.bombs < 80) {
2510       res = "YOUR SATCHEL FEELS VERY FULL NOW!";
2511       global.kaliGift = 3;
2512       global.bombs = 99;
2513     } else {
2514       res = "YOU FEEL INVIGORATED!";
2515       global.kaliGift += 1;
2516       global.plife += global.randOther(4, 8);
2517     }
2518   } else if (global.favor >= 16) {
2519     if (global.kaliGift >= 2) {
2520       res = "SHE SEEMS VERY HAPPY WITH YOU!";
2521     } else {
2522       res = "SHE BESTOWS A GIFT UPON YOU!";
2523       global.kaliGift = 2;
2524       // poofs
2525       auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2526       obj.xVel = -1;
2527       obj.yVel = 0;
2528       obj = MakeMapObject(sx, sy-8, 'oPoof');
2529       obj.xVel = 1;
2530       obj.yVel = 0;
2531       // a gift
2532       obj = none;
2533       if (gift) obj = MakeMapObject(sx, sy-8, gift);
2534       if (!obj) obj = MakeMapObject(sx, sy-8, 'oKapala');
2535     }
2536   } else if (global.favor >= 8) {
2537     if (global.kaliGift >= 1) {
2538       res = "SHE SEEMS HAPPY WITH YOU.";
2539     } else {
2540       res = "SHE BESTOWS A GIFT UPON YOU!";
2541       global.kaliGift = 1;
2542       //rAltar = instance_nearest(x, y, oSacAltarRight);
2543       //if (instance_exists(rAltar)) {
2544       // poofs
2545       auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2546       obj.xVel = -1;
2547       obj.yVel = 0;
2548       obj = MakeMapObject(sx, sy-8, 'oPoof');
2549       obj.xVel = 1;
2550       obj.yVel = 0;
2551       obj = none;
2552       if (gift) obj = MakeMapObject(sx, sy-8, gift);
2553       if (!obj) {
2554         auto n = global.randOther(1, 8);
2555         auto m = n;
2556         for (;;) {
2557           name aname = '';
2558                if (n == 1 && !global.hasCape && !global.hasJetpack) aname = 'oCapePickup';
2559           else if (n == 2 && !global.hasGloves) aname = 'oGloves';
2560           else if (n == 3 && !global.hasSpectacles) aname = 'oSpectacles';
2561           else if (n == 4 && !global.hasMitt) aname = 'oMitt';
2562           else if (n == 5 && !global.hasSpringShoes) aname = 'oSpringShoes';
2563           else if (n == 6 && !global.hasSpikeShoes) aname = 'oSpikeShoes';
2564           else if (n == 7 && !global.hasStickyBombs) aname = 'oPaste';
2565           else if (n == 8 && !global.hasCompass) aname = 'oCompass';
2566           if (aname) {
2567             obj = MakeMapObject(sx, sy-8, aname);
2568             if (obj) break;
2569           }
2570           ++n;
2571           if (n > 8) n = 1;
2572           if (n == m) {
2573             obj = MakeMapObject(sx, sy-8, (global.hasJetpack ? 'oBombBox' : 'oJetpack'));
2574             break;
2575           }
2576         }
2577       }
2578     }
2579   } else if (global.favor > 0) {
2580     res = "SHE SEEMS PLEASED WITH YOU.";
2581   }
2583   /*
2584   if (argument1) {
2585     global.message = "";
2586     res = "KALI DEVOURS YOU!"; // sacrifice is player
2587   }
2588   */
2590   return res;
2594 void performSacrifice (MapObject what, MapTile where) {
2595   if (!what || !what.isInstanceAlive) return;
2596   MakeMapObject(what.ix+8, what.iy+8, 'oFlame');
2597   if (where) where.playSound('sndSmallExplode'); else what.playSound('sndSmallExplode');
2598   if (!what.bloodless) what.scrCreateBlood(what.ix+8, what.iy+8, 3);
2600   string msg = "KALI ACCEPTS THE SACRIFICE!";
2602   auto idol = ItemGoldIdol(what);
2603   if (idol) {
2604     ++stats.totalSacrifices;
2605          if (global.favor <= -8) msg = "SHE SEEMS VERY ANGRY WITH YOU!";
2606     else if (global.favor < 0) msg = "SHE SEEMS ANGRY WITH YOU.";
2607     else if (global.favor >= 0) {
2608       // find other side of the altar
2609       int sx = player.ix, sy = player.iy;
2610       auto altar = where;
2611       if (altar) {
2612         sx = altar.ix;
2613         sy = altar.iy;
2614         auto a2 = isCollisionAtPoint(altar.ix+18, altar.iy+8, &cbCollisionSacAltar);
2615         if (!a2) a2 = isCollisionAtPoint(altar.ix-8, altar.iy+8, &cbCollisionSacAltar);
2616         if (a2) { sx = a2.ix; sy = a2.iy; }
2617       }
2618       // poofs
2619       auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2620       obj.xVel = -1;
2621       obj.yVel = 0;
2622       obj = MakeMapObject(sx, sy-8, 'oPoof');
2623       obj.xVel = 1;
2624       obj.yVel = 0;
2625       // a gift
2626       obj = MakeMapObject(sx, sy-16-8, 'oMonkeyGold');
2627     }
2628     osdMessage(msg, 6.66);
2629     scrShake(10);
2630     idol.instanceRemove();
2631     return;
2632   }
2634   if (global.favor <= -8) {
2635     msg = "KALI DEVOURS THE SACRIFICE!";
2636   } else if (global.favor < 0) {
2637     global.favor += (what.status == MapObject::STUNNED ? what.favor : round(what.favor/2.0)); //k8: added `round()`
2638     if (what.favor > 0) what.favor = 0;
2639   } else {
2640     global.favor += (what.status == MapObject::STUNNED ? what.favor : round(what.favor/2.0)); //k8: added `round()`
2641   }
2643   /*!!
2644        if (myAltar.gift1 != "" and global.kaliGift == 0) scrGetKaliGift(myAltar.gift1);
2645   else if (myAltar.gift2 != "" and global.kaliGift == 1) scrGetKaliGift(myAltar.gift2);
2646   else scrGetKaliGift("");
2647   */
2649   // sacrifice is player?
2650   if (what isa PlayerPawn) {
2651     ++stats.totalSelfSacrifices;
2652     msg = "KALI DEVOURS YOU!";
2653     player.visible = false;
2654     player.removeBallAndChain(temp:true);
2655     player.dead = true;
2656     player.status = MapObject::DEAD;
2657   } else {
2658     ++stats.totalSacrifices;
2659     auto msg2 = scrGetKaliGift(where);
2660     what.instanceRemove();
2661     if (msg2) msg = va("%s\n%s", msg, msg2);
2662   }
2664   osdMessage(msg, 6.66);
2666   //!if (isRealLevel()) global.totalSacrifices += 1;
2668   //!global.messageTimer = 200;
2669   //!global.shake = 10;
2670   scrShake(10);
2672   /*damsel
2673   instance_create(x, y, oFlame);
2674   playSound(global.sndSmallExplode);
2675   scrCreateBlood(x, y, 3);
2676   global.message = "KALI ACCEPTS YOUR SACRIFICE!";
2677   if (global.favor <= -8) {
2678     global.message = "KALI DEVOURS YOUR SACRIFICE!";
2679   } else if (global.favor < 0) {
2680     if (status == 98) global.favor += favor*1.5; else global.favor += favor;
2681     if (favor > 0) favor = 0;
2682   } else {
2683     if (status == 98) global.favor += favor*1.5; else global.favor += favor;
2684   }
2686        if (myAltar.gift1 != "" and global.kaliGift == 0) scrGetFavorMsg(myAltar.gift1);
2687   else if (myAltar.gift2 != "" and global.kaliGift == 1) scrGetFavorMsg(myAltar.gift2);
2688   else scrGetFavorMsg("");
2690   global.messageTimer = 200;
2691   global.shake = 10;
2692   instance_destroy();
2693   */
2697 // ////////////////////////////////////////////////////////////////////////// //
2698 final void addBackgroundGfxDetails () {
2699   // add background details
2700   //if (global.customLevel || global.parallax) return;
2701   foreach (; 0..20) {
2702     // bg = instance_create(16*randRoom(1, 42), 16*randRoom(1, 33), oCaveBG);
2703          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);
2704     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);
2705     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);
2706     else MakeMapBackTile('bgExtras', 32*global.randRoom(0, 1), 0, 32, 32, 16*global.randRoom(1, 42), 16*global.randRoom(1, 33), 10002);
2707   }
2711 // ////////////////////////////////////////////////////////////////////////// //
2712 private final void fixRealViewStart () {
2713   int scale = global.scale;
2714   realViewStart.x = clamp(realViewStart.x, viewMin.x*scale, viewMax.x*scale-viewWidth);
2715   realViewStart.y = clamp(realViewStart.y, viewMin.y*scale, viewMax.y*scale-viewHeight);
2719 final int cameraCurrX () { return realViewStart.x/global.scale; }
2720 final int cameraCurrY () { return realViewStart.y/global.scale; }
2723 private final void fixViewStart () {
2724   int scale = global.scale;
2725   viewStart.x = clamp(viewStart.x, viewMin.x*scale, viewMax.x*scale-viewWidth);
2726   viewStart.y = clamp(viewStart.y, viewMin.y*scale, viewMax.y*scale-viewHeight);
2730 final void centerViewAtPlayer () {
2731   if (viewWidth < 1 || viewHeight < 1 || !player) return;
2732   centerViewAt(player.xCenter, player.yCenter);
2736 final void centerViewAt (int x, int y) {
2737   if (viewWidth < 1 || viewHeight < 1) return;
2739   cameraSlideToSpeed.x = 0;
2740   cameraSlideToSpeed.y = 0;
2741   cameraSlideToPlayer = 0;
2743   int scale = global.scale;
2744   x *= scale;
2745   y *= scale;
2746   realViewStart.x = clamp(x-viewWidth/2, 0, tilesWidth*16*scale-viewWidth);
2747   realViewStart.y = clamp(y-viewHeight/2, 0, tilesHeight*16*scale-viewHeight);
2748   fixRealViewStart();
2750   viewStart.x = realViewStart.x;
2751   viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
2752   fixViewStart();
2756 const int ViewPortToleranceX = 16*1+8;
2757 const int ViewPortToleranceY = 16*1+8;
2759 final void fixCamera () {
2760   if (!player) return;
2761   if (viewWidth < 1 || viewHeight < 1) return;
2762   int scale = global.scale;
2763   auto alwaysCenterX = global.config.alwaysCenterPlayer;
2764   auto alwaysCenterY = alwaysCenterX;
2765   // calculate offset from viewport center (in game units), and fix viewport
2767   int camDestX = player.ix+8;
2768   int camDestY = player.iy+8;
2769   if (!cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y) != 0) {
2770     // slide camera to point
2771     if (cameraSlideToSpeed.x) camDestX = cameraSlideToCurr.x;
2772     if (cameraSlideToSpeed.y) camDestY = cameraSlideToCurr.y;
2773     int dx = cameraSlideToDest.x-camDestX;
2774     int dy = cameraSlideToDest.y-camDestY;
2775     //writeln("speed:(", cameraSlideToSpeed.x, ",", cameraSlideToSpeed.y, "); cur:(", camDestX, ",", camDestY, "); dest:(", cameraSlideToDest.x, ",", cameraSlideToDest.y, "); delta=(", dx, ",", dy, ")");
2776     if (dx && cameraSlideToSpeed.x != 0) {
2777       alwaysCenterX = true;
2778       if (abs(dx) <= abs(cameraSlideToSpeed.x)) {
2779         camDestX = cameraSlideToDest.x;
2780       } else {
2781         camDestX += sign(dx)*abs(cameraSlideToSpeed.x);
2782       }
2783     }
2784     if (dy && abs(cameraSlideToSpeed.y) != 0) {
2785       alwaysCenterY = true;
2786       if (abs(dy) <= cameraSlideToSpeed.y) {
2787         camDestY = cameraSlideToDest.y;
2788       } else {
2789         camDestY += sign(dy)*abs(cameraSlideToSpeed.y);
2790       }
2791     }
2792     //writeln("  new:(", camDestX, ",", camDestY, ")");
2793     if (cameraSlideToSpeed.x) cameraSlideToCurr.x = camDestX;
2794     if (cameraSlideToSpeed.y) cameraSlideToCurr.y = camDestY;
2795   }
2797   // horizontal
2798   if (cameraSlideToSpeed.x && !cameraSlideToPlayer) {
2799     realViewStart.x = clamp(camDestX*scale, 0, tilesWidth*16*scale-viewWidth);
2800   } else if (!player.cameraBlockX) {
2801     int x = camDestX*scale;
2802     int cx = realViewStart.x;
2803     if (alwaysCenterX) {
2804       cx = x-viewWidth/2;
2805     } else {
2806       int xofs = x-(cx+viewWidth/2);
2807            if (xofs < -ViewPortToleranceX*scale) cx += xofs+ViewPortToleranceX*scale;
2808       else if (xofs > ViewPortToleranceX*scale) cx += xofs-ViewPortToleranceX*scale;
2809     }
2810     // slide back to player?
2811     if (cameraSlideToPlayer && cameraSlideToSpeed.x) {
2812       int prevx = cameraSlideToCurr.x*scale;
2813       int dx = (cx-prevx)/scale;
2814       if (abs(dx) <= cameraSlideToSpeed.x) {
2815         writeln("BACKSLIDE X COMPLETE!");
2816         cameraSlideToSpeed.x = 0;
2817       } else {
2818         cx = prevx+(sign(dx)*cameraSlideToSpeed.x)*scale;
2819         cameraSlideToCurr.x += sign(dx)*cameraSlideToSpeed.x;
2820         if ((dx < 0 && cx <= 0) || (dx > 0 && cx >= tilesWidth*16*scale-viewWidth)) {
2821           writeln("BACKSLIDE X COMPLETE!");
2822           cameraSlideToSpeed.x = 0;
2823         }
2824       }
2825     }
2826     realViewStart.x = clamp(cx, 0, tilesWidth*16*scale-viewWidth);
2827   }
2829   // vertical
2830   if (cameraSlideToSpeed.y && !cameraSlideToPlayer) {
2831     realViewStart.y = clamp(camDestY*scale, 0, tilesHeight*16*scale-viewHeight);
2832   } else if (!player.cameraBlockY) {
2833     int y = camDestY*scale;
2834     int cy = realViewStart.y;
2835     if (alwaysCenterY) {
2836       cy = y-viewHeight/2;
2837     } else {
2838       int yofs = y-(cy+viewHeight/2);
2839            if (yofs < -ViewPortToleranceY*scale) cy += yofs+ViewPortToleranceY*scale;
2840       else if (yofs > ViewPortToleranceY*scale) cy += yofs-ViewPortToleranceY*scale;
2841     }
2842     // slide back to player?
2843     if (cameraSlideToPlayer && cameraSlideToSpeed.y) {
2844       int prevy = cameraSlideToCurr.y*scale;
2845       int dy = (cy-prevy)/scale;
2846       if (abs(dy) <= cameraSlideToSpeed.y) {
2847         writeln("BACKSLIDE Y COMPLETE!");
2848         cameraSlideToSpeed.y = 0;
2849       } else {
2850         cy = prevy+(sign(dy)*cameraSlideToSpeed.y)*scale;
2851         cameraSlideToCurr.y += sign(dy)*cameraSlideToSpeed.y;
2852         if ((dy < 0 && cy <= 0) || (dy > 0 && cy >= tilesHeight*16*scale-viewHeight)) {
2853           writeln("BACKSLIDE Y COMPLETE!");
2854           cameraSlideToSpeed.y = 0;
2855         }
2856       }
2857     }
2858     realViewStart.y = clamp(cy, 0, tilesHeight*16*scale-viewHeight);
2859   }
2861   if (cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y) == 0) cameraSlideToPlayer = 0;
2863   fixRealViewStart();
2864   //writeln("  new2:(", cameraCurrX, ",", cameraCurrY, ")");
2866   viewStart.x = realViewStart.x;
2867   viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
2868   fixViewStart();
2872 // ////////////////////////////////////////////////////////////////////////// //
2873 // x0 and y0 are non-scaled (and will be scaled)
2874 final void drawSpriteAt (name sprName, float frnumf, int x0, int y0, optional bool small) {
2875   if (!sprName) return;
2876   auto spr = sprStore[sprName];
2877   if (!spr || !spr.frames.length) return;
2878   int scale = global.scale;
2879   x0 *= scale;
2880   y0 *= scale;
2881   int frnum = max(0, trunc(frnumf))%spr.frames.length;
2882   auto sfr = spr.frames[frnum];
2883   int sx0 = x0-sfr.xofs*scale;
2884   int sy0 = y0-sfr.yofs*scale;
2885   if (small && scale > 1) {
2886     sfr.tex.blitExt(sx0, sy0, round(sx0+sfr.tex.width*(scale/2.0)), round(sy0+sfr.tex.height*(scale/2.0)), 0, 0);
2887   } else {
2888     sfr.tex.blitAt(sx0, sy0, scale);
2889   }
2893 // x0 and y0 are non-scaled (and will be scaled)
2894 final void drawTextAt (int x0, int y0, string text) {
2895   if (!text) return;
2896   int scale = global.scale;
2897   x0 *= scale;
2898   y0 *= scale;
2899   sprStore.renderText(x0, y0, text, scale);
2903 void renderCompass (float currFrameDelta) {
2904   if (!global.hasCompass) return;
2906   /*
2907   if (isRoom("rOlmec")) {
2908     global.exitX = 648;
2909     global.exitY = 552;
2910   } else if (isRoom("rOlmec2")) {
2911     global.exitX = 648;
2912     global.exitY = 424;
2913   }
2914   */
2916   bool hasMessage = osdHasMessage();
2917   foreach (MapTile et; allExits) {
2918     // original compass
2919     int exitX = et.ix, exitY = et.iy;
2920     int vx0 = viewStart.x/global.scale, vy0 = viewStart.y/global.scale;
2921     int vx1 = (viewStart.x+viewWidth)/global.scale;
2922     int vy1 = (viewStart.y+viewHeight)/global.scale;
2923     if (exitY > vy1-16) {
2924       if (exitX < vx0) {
2925         drawSpriteAt(hasMessage ? 'sCompassSmallLL' : 'sCompassLL', 0, 0, 224);
2926       } else if (exitX > vx1-16) {
2927         drawSpriteAt(hasMessage ? 'sCompassSmallLR' : 'sCompassLR', 0, 304, 224);
2928       } else {
2929         drawSpriteAt(hasMessage ? 'sCompassSmallDown' : 'sCompassDown', 0, exitX-vx0, 224);
2930       }
2931     } else if (exitX < vx0) {
2932       drawSpriteAt(hasMessage ? 'sCompassSmallLeft' : 'sCompassLeft', 0, 0, exitY-vy0);
2933     } else if (exitX > vx1-16) {
2934       drawSpriteAt(hasMessage ? 'sCompassSmallRight' : 'sCompassRight', 0, 304, exitY-vy0);
2935     }
2936   }
2940 final bool totalsNameCmpCB (ref GameStats::TotalItem a, ref GameStats::TotalItem b) {
2941   auto sa = string(a.objName);
2942   auto sb = string(b.objName);
2943   return (sa < sb);
2946 void renderTransitionInfo (float currFrameDelta) {
2947   //FIXME!
2948   /*
2949   GameStats.sortTotalsList(stats.kills, &totalsNameCmpCB);
2951   int maxLen = 0;
2952   foreach (int idx, ref auto k; stats.kills) {
2953     string s = string(k);
2954     maxLen = max(maxLen, s.length);
2955   }
2956   maxLen *= 8;
2958   sprStore.loadFont('sFontSmall');
2959   Video.color = 0xff_ff_00;
2960   foreach (int idx, ref auto k; stats.kills) {
2961     int deaths = 0;
2962     foreach (int xidx, ref auto d; stats.totalKills) {
2963       if (d.objName == k) { deaths = d.count; break; }
2964     }
2965     //drawTextAt(16, 4+idx*8, va("%s: %d (%d)", string(k.objName).toUpperCase, k.count, deaths));
2966     drawTextAt(16, 4+idx*8, string(k).toUpperCase);
2967     drawTextAt(16+maxLen, 4+idx*8, va(": %d (%d)", k.count, deaths));
2968   }
2969   */
2973 void renderGhostTimer (float currFrameDelta) {
2974   if (ghostTimeLeft <= 0) return;
2975   //ghostTimeLeft /= 30; // frames -> seconds
2977   int hgt = Video.screenHeight-64;
2978   if (hgt < 1) return;
2979   int rhgt = round(float(hgt)*(float(ghostTimeLeft)/30.0)/1000.0);
2980   //writeln("hgt=", hgt, "; rhgt=", rhgt, "; gtl=", ghostTimeLeft/30);
2981   if (rhgt > 0) {
2982     auto oclr = Video.color;
2983     Video.color = 0xcf_ff_7f_00;
2984     Video.fillRect(Video.screenWidth-20, 32, 16, hgt-rhgt);
2985     Video.color = 0x7f_ff_7f_00;
2986     Video.fillRect(Video.screenWidth-20, 32+(hgt-rhgt), 16, rhgt);
2987     Video.color = oclr;
2988   }
2992 void renderHUD (float currFrameDelta) {
2993   if (inWinCutscene || isTitleRoom()) return;
2995   if (levelKind == LevelKind.Transition) { renderTransitionInfo(currFrameDelta); return; }
2997   int lifeX = 4; // 8
2998   int bombX = 56;
2999   int ropeX = 104;
3000   int ammoX = 152;
3001   int moneyX = 200;
3002   int hhup;
3003   bool scumSmallHud = global.config.scumSmallHud;
3004   if (!global.config.optSGAmmo) moneyX = ammoX;
3006   if (scumSmallHud) {
3007     sprStore.loadFont('sFontSmall');
3008     hhup = 6;
3009   } else {
3010     sprStore.loadFont('sFont');
3011     hhup = 0;
3012   }
3013   //int alpha = 0x6f_00_00_00;
3014   int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3015   int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3017   //Video.color = 0xff_ff_ff;
3018   Video.color = 0xff_ff_ff|talpha;
3020   // hearts
3021   if (scumSmallHud) {
3022     if (global.plife == 1) {
3023       drawSpriteAt('sHeartSmallBlink', global.heartBlink, lifeX+2, 4-hhup);
3024       global.heartBlink += 0.1;
3025       if (global.heartBlink > 3) global.heartBlink = 0;
3026     } else {
3027       drawSpriteAt('sHeartSmall', -1, lifeX+2, 4-hhup);
3028       global.heartBlink = 0;
3029     }
3030   } else {
3031     if (global.plife == 1) {
3032       drawSpriteAt('sHeartBlink', global.heartBlink, lifeX, 8-hhup);
3033       global.heartBlink += 0.1;
3034       if (global.heartBlink > 3) global.heartBlink = 0;
3035     } else {
3036       drawSpriteAt('sHeart', -1, lifeX, 8-hhup);
3037       global.heartBlink = 0;
3038     }
3039   }
3041   int life = clamp(global.plife, 0, 99);
3042   //if (!scumHud && life > 99) life = 99;
3043   drawTextAt(lifeX+16, 8-hhup, va("%d", life));
3045   // bombs
3046   if (global.hasStickyBombs && global.stickyBombsActive) {
3047     if (scumSmallHud) drawSpriteAt('sStickyBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sStickyBombIcon', -1, bombX, 8-hhup);
3048   } else {
3049     if (scumSmallHud) drawSpriteAt('sBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sBombIcon', -1, bombX, 8-hhup);
3050   }
3051   int n = global.bombs;
3052   if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3053   drawTextAt(bombX+16, 8-hhup, va("%d", n));
3055   // ropes
3056   if (scumSmallHud) drawSpriteAt('sRopeIconSmall', -1, ropeX+2, 4-hhup); else drawSpriteAt('sRopeIcon', -1, ropeX, 8-hhup);
3057   n = global.rope;
3058   if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3059   drawTextAt(ropeX+16, 8-hhup, va("%d", n));
3061   // shotgun shells
3062   if (global.config.optSGAmmo && (!player || player.holdItem !isa ItemWeaponBow)) {
3063     if (scumSmallHud) drawSpriteAt('sShellsIcon', -1, ammoX+6, 8-hhup); else drawSpriteAt('sShellsIcon', -1, ammoX+7, 12-hhup);
3064     n = global.sgammo;
3065     if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3066     drawTextAt(ammoX+16, 8-hhup, va("%d", n));
3067   } else if (player && player.holdItem isa ItemWeaponBow) {
3068     if (scumSmallHud) drawSpriteAt('sArrowRight', -1, ammoX+6, 8-hhup); else drawSpriteAt('sArrowRight', -1, ammoX+7, 12-hhup);
3069     n = global.arrows;
3070     if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3071     drawTextAt(ammoX+16, 8-hhup, va("%d", n));
3072   }
3074   // money
3075   if (scumSmallHud) drawSpriteAt('sDollarSignSmall', -1, moneyX+2, 3-hhup); else drawSpriteAt('sDollarSign', -1, moneyX, 8-hhup);
3076   drawTextAt(moneyX+16, 8-hhup, va("%d", stats.money-xmoney));
3078   // items
3079   Video.color = 0xff_ff_ff|ialpha;
3081   int ity = (scumSmallHud ? 18-hhup : 24-hhup);
3083   n = 8; //28;
3084   if (global.hasUdjatEye) {
3085     if (global.udjatBlink) drawSpriteAt('sUdjatEyeIcon2', -1, n, ity); else drawSpriteAt('sUdjatEyeIcon', -1, n, ity);
3086     n += 20;
3087   }
3088   if (global.hasAnkh) { drawSpriteAt('sAnkhIcon', -1, n, ity); n += 20; }
3089   if (global.hasCrown) { drawSpriteAt('sCrownIcon', -1, n, ity); n += 20; }
3090   if (global.hasKapala) {
3091          if (global.bloodLevel == 0) drawSpriteAt('sKapalaIcon', 0, n, ity);
3092     else if (global.bloodLevel <= 2) drawSpriteAt('sKapalaIcon', 1, n, ity);
3093     else if (global.bloodLevel <= 4) drawSpriteAt('sKapalaIcon', 2, n, ity);
3094     else if (global.bloodLevel <= 6) drawSpriteAt('sKapalaIcon', 3, n, ity);
3095     else if (global.bloodLevel <= 8) drawSpriteAt('sKapalaIcon', 4, n, ity);
3096     n += 20;
3097   }
3098   if (global.hasSpectacles) { drawSpriteAt('sSpectaclesIcon', -1, n, ity); n += 20; }
3099   if (global.hasGloves) { drawSpriteAt('sGlovesIcon', -1, n, ity); n += 20; }
3100   if (global.hasMitt) { drawSpriteAt('sMittIcon', -1, n, ity); n += 20; }
3101   if (global.hasSpringShoes) { drawSpriteAt('sSpringShoesIcon', -1, n, ity); n += 20; }
3102   if (global.hasJordans) { drawSpriteAt('sJordansIcon', -1, n, ity); n += 20; }
3103   if (global.hasSpikeShoes) { drawSpriteAt('sSpikeShoesIcon', -1, n, ity); n += 20; }
3104   if (global.hasCape) { drawSpriteAt('sCapeIcon', -1, n, ity); n += 20; }
3105   if (global.hasJetpack) { drawSpriteAt('sJetpackIcon', -1, n, ity); n += 20; }
3106   if (global.hasStickyBombs) { drawSpriteAt('sPasteIcon', -1, n, ity); n += 20; }
3107   if (global.hasCompass) { drawSpriteAt('sCompassIcon', -1, n, ity); n += 20; }
3108   if (global.hasParachute) { drawSpriteAt('sParachuteIcon', -1, n, ity); n += 20; }
3110   if (player.holdItem && player.holdItem.objName == 'Bow' && global.arrows > 0) {
3111     int m = 1;
3112     float malpha = 1;
3113     while (m <= global.arrows && m <= 20 && malpha > 0) {
3114       Video.color = trunc(malpha*255)<<24|0xff_ff_ff;
3115       drawSpriteAt('sArrowIcon', -1, n, ity);
3116       n += 4;
3117       if (m > 10) malpha -= 0.1; //and global.arrows > 20 malpha -= 0.1
3118       m += 1;
3119     }
3120   }
3122   if (xmoney > 0) {
3123     sprStore.loadFont('sFontSmall');
3124     Video.color = 0xff_ff_00|talpha;
3125     if (scumSmallHud) drawTextAt(moneyX+8, 8+8-hhup, va("+%d", xmoney));
3126     else drawTextAt(moneyX, 8+16-hhup, va("+%d", xmoney));
3127   }
3129   Video.color = 0xff_ff_ff;
3130   if (global.config.ghostShowTime) renderGhostTimer(currFrameDelta);
3134 // ////////////////////////////////////////////////////////////////////////// //
3135 private transient array!MapEntity renderVisibleCids;
3136 private transient array!MapTile renderMidTiles, renderFrontTiles; // normal, with fg
3138 final bool renderSortByDepth (MapEntity oa, MapEntity ob) {
3139   //MapObject oa = MapObject(a);
3140   //MapObject ob = MapObject(b);
3141   auto da = oa.depth, db = ob.depth;
3142   if (da == db) return (oa.objId < ob.objId);
3143   return (da < db);
3147 const int RenderEdgePixNormal = 64;
3148 const int RenderEdgePixLight = 256;
3150 void renderWithOfs (int xofs, int yofs, float currFrameDelta) {
3151   int scale = global.scale;
3152   int tsz = 16*scale;
3154   // don't touch framebuffer alpha
3155   Video.colorMask = Video::CMask.Colors;
3157   Video.color = 0xff_ff_ff;
3159   // render cave background
3160   if (levBGImg) {
3161     int bgw = levBGImg.tex.width*scale;
3162     int bgh = levBGImg.tex.height*scale;
3163     int bgTW = (tilesWidth*tsz+bgw-1)/bgw;
3164     int bgTH = (tilesHeight*tsz+bgh-1)/bgh;
3165     int bgX0 = max(0, xofs/bgw);
3166     int bgY0 = max(0, yofs/bgh);
3167     int bgX1 = min(bgTW, (xofs+viewWidth+bgw-1)/bgw);
3168     int bgY1 = min(bgTH, (yofs+viewHeight+bgh-1)/bgh);
3169     foreach (int ty; bgY0..bgY1) {
3170       foreach (int tx; bgX0..bgX1) {
3171         int x0 = tx*bgw-xofs;
3172         int y0 = ty*bgh-yofs;
3173         levBGImg.tex.blitAt(x0, y0, scale);
3174       }
3175     }
3176   }
3178   int RenderEdgePix = (global.darkLevel ? RenderEdgePixLight : RenderEdgePixNormal);
3180   // render background tiles
3181   for (MapBackTile bt = backtiles; bt; bt = bt.next) {
3182     bt.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3183   }
3185   // collect visible special tiles
3186   renderVisibleCids.length -= renderVisibleCids.length;
3187   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)) {
3188     if (!mt.visible || !mt.isInstanceAlive) continue;
3189     //Video.color = (mt.moveable ? 0xff_7f_00 : 0xff_ff_ff);
3190     //!mt.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3191     renderVisibleCids[$] = mt;
3192   }
3193   // add player
3194   if (player && player.visible) renderVisibleCids[$] = player; // include player in render list
3195   // collect visible objects
3196   auto ogrid = objGrid;
3197   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)) {
3198     if (o.visible && o.isInstanceAlive) renderVisibleCids[$] = o;
3199   }
3201   // collect stationary tiles
3202   int tileX0 = max(0, xofs/tsz);
3203   int tileY0 = max(0, yofs/tsz);
3204   int tileX1 = min(tilesWidth, (xofs+viewWidth+tsz-1)/tsz);
3205   int tileY1 = min(tilesHeight, (yofs+viewHeight+tsz-1)/tsz);
3207   // render backs; collect tile arrays
3208   renderMidTiles.length -= renderMidTiles.length; // don't realloc
3209   renderFrontTiles.length -= renderFrontTiles.length; // don't realloc
3211   foreach (int ty; tileY0..tileY1) {
3212     foreach (int tx; tileX0..tileX1) {
3213       auto tile = getTileAt(tx, ty);
3214       if (tile && tile.visible && tile.isInstanceAlive) {
3215         renderMidTiles[$] = tile;
3216         if (tile.bgfront) renderFrontTiles[$] = tile;
3217         if (tile.bgback) tile.drawWithOfsBack(xofs, yofs, scale, currFrameDelta);
3218       }
3219     }
3220   }
3222   // collect "mid" (i.e. normal) tiles
3223   foreach (MapTile tile; renderMidTiles) {
3224     //tile.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3225     renderVisibleCids[$] = tile;
3226   }
3228   EntityGrid.sortEntList(renderVisibleCids, &renderSortByDepth);
3230   auto depth4Start = 0;
3231   foreach (auto xidx, MapEntity o; renderVisibleCids) {
3232     if (o.depth >= 4) {
3233       depth4Start = xidx;
3234       break;
3235     }
3236   }
3238   // render objects (part one: depth > 3)
3239   foreach (auto idx; depth4Start..renderVisibleCids.length; reverse) {
3240     MapEntity o = renderVisibleCids[idx];
3241     //if (idx && renderSortByDepth(o, renderVisibleCids[idx-1])) writeln("VIOLATION!");
3242     o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3243   }
3245   // render object (part two: front tile parts, depth 3.5)
3246   foreach (MapTile tile; renderFrontTiles) tile.drawWithOfsFront(xofs, yofs, scale, currFrameDelta);
3248   // render objects (part three: depth <= 3)
3249   foreach (auto idx; 0..depth4Start; reverse) {
3250     MapEntity o = renderVisibleCids[idx];
3251     o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3252   }
3254   // render player post-effects, so cape or jetpack will be correctly rendered on level exit, for example
3255   player.lastDrawWithOfs(xofs, yofs, scale, currFrameDelta);
3257   // lighting
3258   if (global.darkLevel) {
3259          if (global.config.scumDarkness >= 2) player.lightRadius = 96;
3260     else player.lightRadius = 32; //40;
3262     auto ltex = bgtileStore.lightTexture('ltx512', 512);
3264     // set screen alpha to min
3265     Video.colorMask = Video::CMask.Alpha;
3266     Video.blendMode = Video::BlendMode.None;
3267     Video.color = 0xff_ff_ff_ff;
3268     Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
3269     //Video.colorMask = Video::CMask.All;
3271     // blend lights
3272     // also, stencil 'em, so we can filter dark areas
3273     Video.textureFiltering = true;
3274     Video.stencil = true;
3275     Video.stencilFunc(Video::StencilFunc.Always, 1);
3276     Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Replace);
3277     Video.alphaTestFunc = Video::AlphaFunc.Greater;
3278     Video.alphaTestVal = 0.03;
3279     Video.color = 0xff_ff_ff;
3280     Video.blendFunc = Video::BlendFunc.Max;
3281     Video.blendMode = Video::BlendMode.Blend; // anything except `Normal`
3282     Video.colorMask = Video::CMask.Alpha;
3284     foreach (MapEntity e; renderVisibleCids) {
3285       int lrad = e.lightRadius;
3286       if (lrad < 4) continue;
3287       lrad += 8;
3288       float lightscale = float(lrad*scale)/float(ltex.tex.width);
3289       int xi, yi;
3290       e.getInterpCoords(currFrameDelta, scale, out xi, out yi);
3291 #ifdef OLD_LIGHT_OFFSETS
3292       int fx0, fy0, fx1, fy1;
3293       bool doMirror;
3294       auto spf = e.getSpriteFrame(out doMirror, out fx0, out fy0, out fx1, out fy1);
3295       if (spf) {
3296         xi += (fx1-fx0)*scale/2;
3297         yi += (fy1-fy0)*scale/2;
3298       }
3299 #else
3300       int lxofs, lyofs;
3301       e.getLightOffset(out lxofs, out lyofs);
3302       xi += lxofs*scale;
3303       yi += lyofs*scale;
3305 #endif
3306       lrad = lrad*scale/2;
3307       xi -= xofs+lrad;
3308       yi -= yofs+lrad;
3309       ltex.tex.blitAt(xi, yi, lightscale);
3310       //ltex.tex.blitAt(xi-xofs, yi-yofs, lightscale);
3311     }
3312     Video.textureFiltering = false;
3314     // modify only lit parts
3315     Video.stencilFunc(Video::StencilFunc.Equal, 1);
3316     Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Keep);
3317     // multiply framebuffer colors by framebuffer alpha
3318     Video.color = 0xff_ff_ff; // it doesn't matter
3319     Video.blendFunc = Video::BlendFunc.Add;
3320     Video.blendMode = Video::BlendMode.DstMulDstAlpha;
3321     Video.colorMask = Video::CMask.Colors;
3322     Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
3324     // filter unlit parts
3325     Video.stencilFunc(Video::StencilFunc.NotEqual, 1);
3326     Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Keep);
3327     Video.blendFunc = Video::BlendFunc.Add;
3328     Video.blendMode = Video::BlendMode.Filter;
3329     Video.colorMask = Video::CMask.Colors;
3330     Video.color = 0x00_00_18;
3331     Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
3333     // restore defaults
3334     Video.blendFunc = Video::BlendFunc.Add;
3335     Video.blendMode = Video::BlendMode.Normal;
3336     Video.colorMask = Video::CMask.All;
3337     Video.alphaTestFunc = Video::AlphaFunc.Always;
3338     Video.stencil = false;
3339   }
3341   // clear visible objects list
3342   renderVisibleCids.length -= renderVisibleCids.length;
3345   if (global.config.drawHUD) renderHUD(currFrameDelta);
3346   renderCompass(currFrameDelta);
3348   float osdTimeLeft, osdTimeStart;
3349   string msg = osdGetMessage(out osdTimeLeft, out osdTimeStart);
3350   if (msg) {
3351     auto ct = GetTickCount();
3352     int msgScale = 3;
3353     sprStore.loadFont('sFontSmall');
3354     auto msgHeight = sprStore.getMultilineTextHeight(msg, msgScale);
3355     int x = Video.screenWidth/2;
3356     int y = Video.screenHeight-64-msgHeight;
3357     auto oldColor = Video.color;
3358     Video.color = 0xff_ff_00;
3359     if (osdTimeLeft < 0.5) {
3360       int alpha = 255-clamp(int(510*osdTimeLeft), 0, 255);
3361       Video.color = Video.color|(alpha<<24);
3362     } else if (ct-osdTimeStart < 0.5) {
3363       osdTimeStart = ct-osdTimeStart;
3364       int alpha = 255-clamp(int(510*osdTimeStart), 0, 255);
3365       Video.color = Video.color|(alpha<<24);
3366     }
3367     sprStore.renderMultilineTextCentered(x, y, msg, msgScale);
3368     Video.color = oldColor;
3369   }
3371   if (inWinCutscene) renderWinCutsceneOverlay();
3372   Video.color = 0xff_ff_ff;
3376 // ////////////////////////////////////////////////////////////////////////// //
3377 final class!MapObject findGameObjectClassByName (name aname) {
3378   if (!aname) return none; // just in case
3379   auto co = FindClassByGameObjName(aname);
3380   if (!co) {
3381     writeln("***UNKNOWN GAME OBJECT: '", aname, "'");
3382     return none;
3383   }
3384   co = GetClassReplacement(co);
3385   if (!co) FatalError("findGameObjectClassByName: WTF?!");
3386   if (!class!MapObject(co)) FatalError("findGameTileClassByName: object '%n' is not a map object, it is a `%n`", aname, GetClassName(co));
3387   return class!MapObject(co);
3391 final class!MapTile findGameTileClassByName (name aname) {
3392   if (!aname) return none; // just in case
3393   auto co = FindClassByGameObjName(aname);
3394   if (!co) return MapTile; // unknown names will be routed directly to tile object
3395   co = GetClassReplacement(co);
3396   if (!co) FatalError("findGameTileClassByName: WTF?!");
3397   if (!class!MapTile(co)) FatalError("findGameTileClassByName: object '%n' is not a tile, it is a `%n`", aname, GetClassName(co));
3398   return class!MapTile(co);
3402 final MapObject findAnyObjectOfType (name aname) {
3403   if (!aname) return none;
3404   auto cls = FindClassByGameObjName(aname);
3405   if (!cls) return none;
3406   for (auto cid = objGrid.getFirstObject(); cid; cid = objGrid.getNextObject(cid)) {
3407     MapObject obj = objGrid.getObject(MapObject, cid);
3408     if (!obj || obj.spectral || !obj.isInstanceAlive) continue;
3409     if (obj isa cls) return obj;
3410   }
3411   return none;
3415 // ////////////////////////////////////////////////////////////////////////// //
3416 final MapTile CreateMapTile (int xpos, int ypos, name aname) {
3417   if (!aname) FatalError("cannot create typeless tile");
3418   //MapTile tile = SpawnObject(aname == 'oRope' ? MapTileRope : MapTile);
3419   auto tclass = findGameTileClassByName(aname);
3420   if (!tclass) return none;
3421   MapTile tile = SpawnObject(tclass);
3422   tile.global = global;
3423   tile.level = self;
3424   tile.objName = aname;
3425   tile.objType = aname; // just in case
3426   tile.fltx = xpos;
3427   tile.flty = ypos;
3428   if (!tile.initialize()) FatalError("cannot create tile '%n'", aname);
3429   return tile;
3433 final bool isRopePlacedAt (int x, int y) {
3434   int[8] covered;
3435   foreach (ref auto v; covered) v = false;
3436   foreach (MapTile t; miscTileGrid.inRectPix(x, y-8, 1, 17, precise:false)) {
3437     if (!cbIsRopeTile(t)) continue;
3438     if (t.ix != x) continue;
3439     if (t.iy == y) return true;
3440     foreach (int ty; t.iy..t.iy+8) {
3441       int d = ty-y;
3442       if (d >= 0 && d < covered.length) covered[d] = true;
3443     }
3444   }
3445   // check if the whole rope height is completely covered with ropes
3446   foreach (auto v; covered) if (!v) return false;
3447   return true;
3451 // won't call `onDestroy()`
3452 final void RemoveMapTile (int tileX, int tileY) {
3453   if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
3454     if (tiles[tileX, tileY]) checkWater = true;
3455     delete tiles[tileX, tileY];
3456     tiles[tileX, tileY] = none;
3457   }
3461 final MapTile MakeMapTile (int mapx, int mapy, name aname/*, optional name sprname*/) {
3462   //if (mapy >= tilesHeight) { writeln("OOM: ", aname, "; ty=", mapy, "; hgt=", tilesHeight); }
3463   if (mapx < 0 || mapx >= tilesWidth || mapy < 0 || mapy >= tilesHeight) return none;
3465   // if we already have rope tile there, there is no reason to add another one
3466   if (aname == 'oRope') {
3467     if (isRopePlacedAt(mapx*16, mapy*16)) {
3468       //writeln("dupe rope (0)!");
3469       return none;
3470     }
3471   }
3473   auto tile = CreateMapTile(mapx*16, mapy*16, aname);
3474   if (tile.moveable || tile.toSpecialGrid) {
3475     // moveable tiles goes to the separate list
3476     miscTileGrid.insert(tile);
3477   } else {
3478     setTileAt(mapx, mapy, tile);
3479   }
3481   /*
3482   switch (aname) {
3483     case 'oEntrance': registerEnter(tile); break;
3484     case 'oExit': registerExit(tile); break;
3485   }
3486   */
3487   if (tile.enter) registerEnter(tile);
3488   if (tile.exit) registerExit(tile);
3490   return tile;
3494 final void MarkTileAsWet (int tileX, int tileY) {
3495   auto t = getTileAt(tileX, tileY);
3496   if (t) t.wet = true;
3500 final MapTile MakeMapTileAtPix (int xpix, int ypix, name aname/*, optional name sprname*/) {
3501   if (xpix%16 == 0 && ypix%16 == 0) return MakeMapTile(xpix/16, ypix/16, aname);
3502   //if (mapx < 0 || mapx >= TilesWidth || mapy < 0 || mapy >= TilesHeight) return none;
3504   // if we already have rope tile there, there is no reason to add another one
3505   if (aname == 'oRope') {
3506     if (isRopePlacedAt(xpix, ypix)) {
3507       //writeln("dupe rope (0)!");
3508       return none;
3509     }
3510   }
3512   auto tile = CreateMapTile(xpix, ypix, aname);
3513   // non-aligned tiles goes to the special grid
3514   miscTileGrid.insert(tile);
3516   switch (aname) {
3517     case 'oEntrance': registerEnter(tile); break;
3518     case 'oExit': registerExit(tile); break;
3519   }
3521   return tile;
3525 final MapTile MakeMapRopeTileAt (int x0, int y0) {
3526   // if we already have rope tile there, there is no reason to add another one
3527   if (isRopePlacedAt(x0, y0)) {
3528     //writeln("dupe rope (1)!");
3529     return none;
3530   }
3532   auto tile = CreateMapTile(x0, y0, 'oRope');
3533   miscTileGrid.insert(tile);
3535   return tile;
3539 // ////////////////////////////////////////////////////////////////////////// //
3540 final MapBackTile CreateBgTile (name sprName, optional int atx0, optional int aty0, optional int aw, optional int ah) {
3541   BackTileImage img = bgtileStore[sprName];
3542   auto res = SpawnObject(MapBackTile);
3543   res.global = global;
3544   res.level = self;
3545   res.bgt = img;
3546   res.bgtName = sprName;
3547   if (specified_atx0) res.tx0 = atx0;
3548   if (specified_aty0) res.ty0 = aty0;
3549   if (specified_aw) res.w = aw;
3550   if (specified_ah) res.h = ah;
3551   if (!res.initialize()) FatalError("cannot create background tile with image '%n'", sprName);
3552   return res;
3556 // ////////////////////////////////////////////////////////////////////////// //
3558 background The background asset from which the new tile will be extracted.
3559 left The x coordinate of the left of the new tile, relative to the background asset's top left corner.
3560 top The y coordinate of the top of the new tile, relative to the background assets top left corner.
3561 width The width of the tile.
3562 height The height of the tile.
3563 x The x position in the room to place the tile.
3564 y The y position in the room to place the tile.
3565 depth The depth at which to place the tile.
3567 final void MakeMapBackTile (name bgname, int left, int top, int width, int height, int x, int y, int depth) {
3568   if (width < 1 || height < 1 || !bgname) return;
3569   auto bgt = bgtileStore[bgname];
3570   if (!bgt) FatalError("cannot load background '%n'", bgname);
3571   MapBackTile bt = SpawnObject(MapBackTile);
3572   bt.global = global;
3573   bt.level = self;
3574   bt.objName = bgname;
3575   bt.bgt = bgt;
3576   bt.bgtName = bgname;
3577   bt.fltx = x;
3578   bt.flty = y;
3579   bt.tx0 = left;
3580   bt.ty0 = top;
3581   bt.w = width;
3582   bt.h = height;
3583   bt.depth = depth;
3584   // find a place for it
3585   if (!backtiles) {
3586     backtiles = bt;
3587     return;
3588   }
3589   // back tiles with the highest depth should come first
3590   MapBackTile ct = backtiles, cprev = none;
3591   while (ct && depth <= ct.depth) { cprev = ct; ct = ct.next; }
3592   // insert before ct
3593   if (cprev) {
3594     bt.next = cprev.next;
3595     cprev.next = bt;
3596   } else {
3597     bt.next = backtiles;
3598     backtiles = bt;
3599   }
3603 // ////////////////////////////////////////////////////////////////////////// //
3604 final MapObject SpawnMapObjectWithClass (class!MapObject oclass) {
3605   if (!oclass) return none;
3607   MapObject obj = SpawnObject(oclass);
3608   if (!obj) FatalError("cannot create map object '%n'", GetClassName(oclass));
3610   //if (aname == 'oShells' || aname == 'oShellSingle' || aname == 'oRopePile') writeln("*** CREATED '", aname, "'");
3612   obj.global = global;
3613   obj.level = self;
3615   return obj;
3619 final MapObject SpawnMapObject (name aname) {
3620   if (!aname) return none;
3621   return SpawnMapObjectWithClass(findGameObjectClassByName(aname));
3625 final MapObject PutSpawnedMapObject (int x, int y, MapObject obj) {
3626   if (!obj /*|| obj.global || obj.level*/) return none; // oops
3628   obj.fltx = x;
3629   obj.flty = y;
3630   if (!obj.initialize()) { delete obj; return none; } // not fatal
3632   insertObject(obj);
3634   return obj;
3638 final MapObject MakeMapObject (int x, int y, name aname) {
3639   MapObject obj = SpawnMapObject(aname);
3640   obj = PutSpawnedMapObject(x, y, obj);
3641   return obj;
3645 // ////////////////////////////////////////////////////////////////////////// //
3646 int winCutSceneTimer = -1;
3647 int winVolcanoTimer = -1;
3648 int winCutScenePhase = 0;
3649 int winSceneDrawStatus = 0;
3650 int winMoneyCount = 0;
3651 int winTime;
3652 bool winFadeOut = false;
3653 int winFadeLevel = 0;
3654 int winCutsceneSkip = 0; // 1: waiting for pay release; 2: pay released, do skip
3655 bool winCutsceneSwitchToNext = false;
3658 void startWinCutscene () {
3659   global.hasParachute = false;
3660   shakeLeft = 0;
3661   winCutsceneSwitchToNext = false;
3662   winCutsceneSkip = 0;
3663   isKeyPressed(GameConfig::Key.Pay);
3664   isKeyReleased(GameConfig::Key.Pay);
3666   auto olddel = ImmediateDelete;
3667   ImmediateDelete = false;
3668   clearTiles();
3669   clearObjects();
3671   createEnd1Room();
3672   fixWallTiles();
3673   addBackgroundGfxDetails();
3675   levBGImgName = 'bgCave';
3676   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
3678   blockWaterChecking = true;
3679   fixLiquidTop();
3680   cleanDeadTiles();
3682   collectLavaTiles();
3684   ImmediateDelete = olddel;
3685   CollectGarbage(true); // destroy delayed objects too
3687   if (dumpGridStats) {
3688     miscTileGrid.dumpStats();
3689     objGrid.dumpStats();
3690   }
3692   playerExited = false; // just in case
3694   osdClear();
3696   setupGhostTime();
3697   global.stopMusic();
3699   inWinCutscene = 1;
3700   winCutSceneTimer = -1;
3701   winCutScenePhase = 0;
3703   /+
3704   if (global.config.gameMode != GameConfig::GameMode.Vanilla) {
3705     if (global.config.bizarre) {
3706       global.yasmScore = 1;
3707       global.config.bizarrePlusTitle = true;
3708     }
3710     array!MapTile toReplace;
3711     forEachTile(delegate bool (MapTile t) {
3712       if (t.objType == 'oGTemple' ||
3713           t.objType == 'oIce' ||
3714           t.objType == 'oDark' ||
3715           t.objType == 'oBrick' ||
3716           t.objType == 'oLush')
3717       {
3718         toReplace[$] = t;
3719       }
3720       return false;
3721     });
3723     foreach (MapTile t; miscTileGrid.allObjects()) {
3724       if (t.objType == 'oGTemple' ||
3725           t.objType == 'oIce' ||
3726           t.objType == 'oDark' ||
3727           t.objType == 'oBrick' ||
3728           t.objType == 'oLush')
3729       {
3730         toReplace[$] = t;
3731       }
3732     }
3734     foreach (MapTile t; toReplace) {
3735       if (t.iy < 192) {
3736         t.cleanDeath = true;
3737             if (rand(1,120) == 1) instance_change(oGTemple, false);
3738         else if (rand(1,100) == 1) instance_change(oIce, false);
3739         else if (rand(1,90) == 1) instance_change(oDark, false);
3740         else if (rand(1,80) == 1) instance_change(oBrick, false);
3741         else if (rand(1,70) == 1) instance_change(oLush, false);
3742           }
3743       }
3744       with (oBrick)
3745       {
3746           if (y &lt; 192)
3747           {
3748               cleanDeath = true;
3749               if (rand(1,5) == 1) instance_change(oLush, false);
3750           }
3751       }
3752   }
3753   +/
3754   //!instance_create(0, 0, oBricks);
3756   //shakeToggle = false;
3757   //oPDummy.status = 2;
3759   //timer = 0;
3761   /+
3762   if (global.kaliPunish &gt;= 2) {
3763       instance_create(oPDummy.x, oPDummy.y+2, oBall2);
3764       obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
3765       obj.linkVal = 1;
3766       obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
3767       obj.linkVal = 2;
3768       obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
3769       obj.linkVal = 3;
3770       obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
3771       obj.linkVal = 4;
3772   }
3773   +/
3777 void startWinCutsceneVolcano () {
3778   global.hasParachute = false;
3779   /*
3780   writeln("VOLCANO HOLD 0: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
3781   writeln("VOLCANO PICK 0: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
3782   */
3784   shakeLeft = 0;
3785   winCutsceneSwitchToNext = false;
3786   auto olddel = ImmediateDelete;
3787   ImmediateDelete = false;
3788   clearTiles();
3789   clearObjects();
3791   levBGImgName = '';
3792   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
3794   blockWaterChecking = true;
3796   ImmediateDelete = olddel;
3797   CollectGarbage(true); // destroy delayed objects too
3799   spawnPlayerAt(2*16+8, 11*16+8);
3800   player.dir = MapEntity::Dir.Right;
3802   playerExited = false; // just in case
3804   osdClear();
3806   setupGhostTime();
3807   global.stopMusic();
3809   inWinCutscene = 2;
3810   winCutSceneTimer = -1;
3811   winCutScenePhase = 0;
3813   MakeMapTile(0, 0, 'oEnd2BG');
3814   realViewStart.x = 0;
3815   realViewStart.y = 0;
3816   viewStart.x = 0;
3817   viewStart.y = 0;
3819   viewMin.x = 0;
3820   viewMin.y = 0;
3821   viewMax.x = 320;
3822   viewMax.y = 240;
3824   player.dead = false;
3825   player.active = true;
3826   player.visible = false;
3827   player.removeBallAndChain(temp:true);
3828   player.stunned = false;
3829   player.status = MapObject::FALLING;
3830   if (player.holdItem) player.holdItem.visible = false;
3831   player.fltx = 320/2;
3832   player.flty = 0;
3834   /*
3835   writeln("VOLCANO HOLD 1: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
3836   writeln("VOLCANO PICK 1: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
3837   */
3841 void startWinCutsceneWinFall () {
3842   global.hasParachute = false;
3843   /*
3844   writeln("FALL HOLD 0: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
3845   writeln("FALL PICK 0: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
3846   */
3848   shakeLeft = 0;
3849   winCutsceneSwitchToNext = false;
3851   auto olddel = ImmediateDelete;
3852   ImmediateDelete = false;
3853   clearTiles();
3854   clearObjects();
3856   createEnd3Room();
3857   setMenuTilesVisible(false);
3858   //fixWallTiles();
3859   //addBackgroundGfxDetails();
3861   levBGImgName = '';
3862   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
3864   blockWaterChecking = true;
3865   fixLiquidTop();
3866   cleanDeadTiles();
3868   collectLavaTiles();
3870   ImmediateDelete = olddel;
3871   CollectGarbage(true); // destroy delayed objects too
3873   if (dumpGridStats) {
3874     miscTileGrid.dumpStats();
3875     objGrid.dumpStats();
3876   }
3878   playerExited = false; // just in case
3880   osdClear();
3882   setupGhostTime();
3883   global.stopMusic();
3885   inWinCutscene = 3;
3886   winCutSceneTimer = -1;
3887   winCutScenePhase = 0;
3889   player.dead = false;
3890   player.active = true;
3891   player.visible = false;
3892   player.removeBallAndChain(temp:true);
3893   player.stunned = false;
3894   player.status = MapObject::FALLING;
3895   if (player.holdItem) player.holdItem.visible = false;
3896   player.fltx = 320/2;
3897   player.flty = 0;
3899   winSceneDrawStatus = 0;
3900   winMoneyCount = 0;
3902   winFadeOut = false;
3903   winFadeLevel = 0;
3905   /*
3906   writeln("FALL HOLD 1: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
3907   writeln("FALL PICK 1: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
3908   */
3912 void setGameOver () {
3913   if (inWinCutscene) {
3914     player.visible = false;
3915     player.removeBallAndChain(temp:true);
3916     if (player.holdItem) player.holdItem.visible = false;
3917   }
3918   player.dead = true;
3919   if (inWinCutscene > 0) {
3920     winFadeOut = true;
3921     winFadeLevel = 255;
3922     winSceneDrawStatus = 8;
3923   }
3927 MapTile findEndPlatTile () {
3928   return forEachTile(delegate bool (MapTile t) { return (t isa MapTileEndPlat); });
3932 MapObject findBigTreasure () {
3933   return forEachObject(delegate bool (MapObject o) { return (o isa MapObjectBigTreasure); });
3937 void setMenuTilesVisible (bool vis) {
3938   if (vis) {
3939     forEachTile(delegate bool (MapTile t) {
3940       if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
3941         t.invisible = false;
3942       }
3943       return false;
3944     });
3945   } else {
3946     forEachTile(delegate bool (MapTile t) {
3947       if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
3948         t.invisible = true;
3949       }
3950       return false;
3951     });
3952   }
3956 void setMenuTilesOnTop () {
3957   forEachTile(delegate bool (MapTile t) {
3958     if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
3959       t.depth = 1;
3960     }
3961     return false;
3962   });
3966 void winCutscenePlayerControl (PlayerPawn plr) {
3967   auto payPress = isKeyPressed(GameConfig::Key.Pay);
3968   auto payRelease = isKeyReleased(GameConfig::Key.Pay);
3970   switch (winCutsceneSkip) {
3971     case 0: // nothing was pressed
3972       if (payPress) winCutsceneSkip = 1;
3973       break;
3974     case 1: // waiting for pay release
3975       if (payRelease) winCutsceneSkip = 2;
3976       break;
3977     case 2: // pay released, do skip
3978       setGameOver();
3979       return;
3980   }
3982   // first winning room
3983   if (inWinCutscene == 1) {
3984     if (plr.ix < 448+8) {
3985       plr.kRight = true;
3986       return;
3987     }
3989     // waiting for chest to open
3990     if (winCutScenePhase == 0) {
3991       winCutSceneTimer = 120/2;
3992       winCutScenePhase = 1;
3993       return;
3994     }
3996     // spawn big idol
3997     if (winCutScenePhase == 1) {
3998       if (--winCutSceneTimer == 0) {
3999         winCutScenePhase = 2;
4000         winCutSceneTimer = 20;
4001         forEachObject(delegate bool (MapObject o) {
4002           if (o isa MapObjectBigChest) {
4003             o.setSprite(global.config.gameMode == GameConfig::GameMode.Vanilla ? 'sBigChestOpen' : 'sBigChestOpen2');
4004             auto treasure = MakeMapObject(o.ix, o.iy, 'oBigTreasure');
4005             if (treasure) {
4006               treasure.yVel = -4;
4007               treasure.xVel = -3;
4008               o.playSound('sndClick');
4009               //!!!if (global.config.gameMode != GameConfig::GameMode.Vanilla) scrSprayGems(oBigChest.x+24, oBigChest.y+24);
4010             }
4011           }
4012           return false;
4013         });
4014       }
4015       return;
4016     }
4018     // lava pump wait
4019     if (winCutScenePhase == 2) {
4020       if (--winCutSceneTimer == 0) {
4021         winCutScenePhase = 3;
4022         winCutSceneTimer = 50;
4023       }
4024       return;
4025     }
4027     // lava pump start
4028     if (winCutScenePhase == 3) {
4029       auto ep = findEndPlatTile();
4030       if (ep) MakeMapObject(ep.ix+global.randOther(0, 80), /*ep.iy*/192+32, 'oBurn');
4031       if (--winCutSceneTimer == 0) {
4032         winCutScenePhase = 4;
4033         winCutSceneTimer = 10;
4034         if (ep) MakeMapObject(ep.ix, ep.iy+30, 'oLavaSpray');
4035         scrShake(9999);
4036       }
4037       return;
4038     }
4040     // lava pump first accel
4041     if (winCutScenePhase == 4) {
4042       if (--winCutSceneTimer == 0) {
4043         forEachObject(delegate bool (MapObject o) {
4044           if (o isa MapObjectLavaSpray) o.yAcc = -0.1;
4045           return false;
4046         });
4047       }
4048     }
4050     // lava pump complete
4051     if (winCutScenePhase == 5) {
4052       if (--winCutSceneTimer == 0) {
4053         //if (oLavaSpray) oLavaSpray.yAcc = -0.1;
4054         startWinCutsceneVolcano();
4055       }
4056       return;
4057     }
4058     return;
4059   }
4062   // volcano room
4063   if (inWinCutscene == 2) {
4064     plr.flty = 0;
4066     // initialize
4067     if (winCutScenePhase == 0) {
4068       winCutSceneTimer = 50;
4069       winCutScenePhase = 1;
4070       winVolcanoTimer = 10;
4071       return;
4072     }
4074     if (winVolcanoTimer > 0) {
4075       if (--winVolcanoTimer == 0) {
4076         MakeMapObject(224+global.randOther(0,48), 144+global.randOther(0,8), 'oVolcanoFlame');
4077         winVolcanoTimer = global.randOther(10, 20);
4078       }
4079     }
4081     // plr sil
4082     if (winCutScenePhase == 1) {
4083       if (--winCutSceneTimer == 0) {
4084         winCutSceneTimer = 30;
4085         winCutScenePhase = 2;
4086         auto sil = MakeMapObject(240, 132, 'oPlayerSil');
4087         //sil.xVel = -6;
4088         //sil.yVel = -8;
4089       }
4090       return;
4091     }
4093     // treasure sil
4094     if (winCutScenePhase == 2) {
4095       if (--winCutSceneTimer == 0) {
4096         winCutScenePhase = 3;
4097         auto sil = MakeMapObject(240, 132, 'oTreasureSil');
4098         //sil.xVel = -6;
4099         //sil.yVel = -8;
4100       }
4101       return;
4102     }
4104     return;
4105   }
4107   // winning camel room
4108   if (inWinCutscene == 3) {
4109     //if (!player.holdItem)  writeln("SCENE 3: LOST ITEM!");
4111     if (!plr.visible) plr.flty = -32;
4113     // initialize
4114     if (winCutScenePhase == 0) {
4115       winCutSceneTimer = 50;
4116       winCutScenePhase = 1;
4117       return;
4118     }
4120     // fall sound
4121     if (winCutScenePhase == 1) {
4122       if (--winCutSceneTimer == 0) {
4123         winCutSceneTimer = 50;
4124         winCutScenePhase = 2;
4125         plr.playSound('sndPFall');
4126         plr.visible = true;
4127         plr.active = true;
4128         writeln("MUST BE CHAINED: ", plr.mustBeChained);
4129         if (plr.mustBeChained) {
4130           plr.removeBallAndChain(temp:true);
4131           plr.spawnBallAndChain();
4132         }
4133         /*
4134         writeln("HOLD: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4135         writeln("PICK: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4136         */
4137         if (!player.holdItem && player.pickedItem) player.scrSwitchToPocketItem(forceIfEmpty:false);
4138         if (player.holdItem) {
4139           player.holdItem.visible = true;
4140           player.holdItem.canLiveOutsideOfLevel = true;
4141           writeln("HOLD ITEM: '", GetClassName(player.holdItem.Class), "'");
4142         }
4143         plr.status == MapObject::FALLING;
4144         global.plife += 99; // just in case
4145       }
4146       return;
4147     }
4149     if (winCutScenePhase == 2) {
4150       auto ball = plr.getMyBall();
4151       if (ball && plr.holdItem != ball) {
4152         ball.teleportTo(plr.fltx, plr.flty+8);
4153         ball.yVel = 6;
4154         ball.myGrav = 0.6;
4155       }
4156       if (plr.status == MapObject::STUNNED || plr.stunned) {
4157         //alarm[0] = 70;
4158         //alarm[1] = 50;
4159         //status = GETUP;
4160         auto treasure = MakeMapObject(144+16+8, -32, 'oBigTreasure');
4161         if (treasure) treasure.depth = 1;
4162         winCutScenePhase = 3;
4163         plr.stunTimer = 30;
4164         plr.playSound('sndTFall');
4165       }
4166       return;
4167     }
4169     if (winCutScenePhase == 3) {
4170       if (plr.status != MapObject::STUNNED && !plr.stunned) {
4171         auto bt = findBigTreasure();
4172         if (bt) {
4173           if (bt.yVel == 0) {
4174             //plr.yVel = -4;
4175             //plr.status = MapObject::JUMPING;
4176             plr.kJump = true;
4177             plr.kJumpPressed = true;
4178             winCutScenePhase = 4;
4179             winCutSceneTimer = 50;
4180           }
4181         }
4182       }
4183       return;
4184     }
4186     if (winCutScenePhase == 4) {
4187       if (--winCutSceneTimer == 0) {
4188         setMenuTilesVisible(true);
4189         winCutScenePhase = 5;
4190         winSceneDrawStatus = 1;
4191         global.playMusic('musVictory', loop:false);
4192         winCutSceneTimer = 50;
4193       }
4194       return;
4195     }
4197     if (winCutScenePhase == 5) {
4198       if (winSceneDrawStatus == 3) {
4199         int money = stats.money;
4200         if (winMoneyCount < money) {
4201           if (money-winMoneyCount > 1000) {
4202             winMoneyCount += 1000;
4203           } else if (money-winMoneyCount > 100) {
4204             winMoneyCount += 100;
4205           } else if (money-winMoneyCount > 10) {
4206             winMoneyCount += 10;
4207           } else {
4208             ++winMoneyCount;
4209           }
4210         }
4211         if (winMoneyCount >= money) {
4212           winMoneyCount = money;
4213           ++winSceneDrawStatus;
4214         }
4215         return;
4216       }
4218       if (winSceneDrawStatus == 7) {
4219         winFadeOut = true;
4220         winFadeLevel += 1;
4221         if (winFadeLevel >= 255) {
4222           ++winSceneDrawStatus;
4223           winCutSceneTimer = 30*30;
4224         }
4225         return;
4226       }
4228       if (winSceneDrawStatus == 8) {
4229         if (--winCutSceneTimer == 0) {
4230           setGameOver();
4231         }
4232         return;
4233       }
4235       if (--winCutSceneTimer == 0) {
4236         ++winSceneDrawStatus;
4237         winCutSceneTimer = 50;
4238       }
4239     }
4241     return;
4242   }
4246 // ////////////////////////////////////////////////////////////////////////// //
4247 void renderWinCutsceneOverlay () {
4248   if (inWinCutscene == 3) {
4249     if (winSceneDrawStatus > 0) {
4250       Video.color = 0xff_ff_ff;
4251       sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
4252       //draw_set_color(txtCol);
4253       drawTextAt(64, 32, "YOU MADE IT!");
4255       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4256       if (global.config.gameMode == GameConfig::GameMode.Vanilla) {
4257         Video.color = 0xff_ff_00; //draw_set_color(c_yellow);
4258         drawTextAt(64, 48, "Classic Mode done!");
4259       } else {
4260         Video.color = 0x00_80_80; //draw_set_color(c_teal);
4261         if (global.config.bizarrePlus) drawTextAt(64, 48, "Bizarre Mode Plus done!");
4262         else drawTextAt(64, 48, "Bizarre Mode done!");
4263         //draw_set_color(c_white);
4264       }
4265       if (!global.usedShortcut) {
4266         Video.color = 0xc0_c0_c0; //draw_set_color(c_silver);
4267         drawTextAt(64, 56, "No shortcuts used!");
4268         //draw_set_color(c_yellow);
4269       }
4270     }
4272     if (winSceneDrawStatus > 1) {
4273       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4274       //draw_set_color(txtCol);
4275       Video.color = 0xff_ff_ff;
4276       drawTextAt(64, 64, "FINAL SCORE:");
4277     }
4279     if (winSceneDrawStatus > 2) {
4280       sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
4281       Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4282       drawTextAt(64, 72, va("$%d", winMoneyCount));
4283     }
4285     if (winSceneDrawStatus > 4) {
4286       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4287       Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4288       drawTextAt(64, 96, va("Time: %s", time2str(winTime/30)));
4289       /*
4290       draw_set_color(c_white);
4291       if (s &lt; 10) draw_text(96+24, 96, string(m) + ":0" + string(s));
4292       else draw_text(96+24, 96, string(m) + ":" + string(s));
4293       */
4294     }
4296     if (winSceneDrawStatus > 5) {
4297       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4298       Video.color = 0xff_ff_ff; //draw_set_color(txtCol);
4299       drawTextAt(64, 96+8, "Kills: ");
4300       Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4301       drawTextAt(96+24, 96+8, va("%s", stats.countKills()));
4302     }
4304     if (winSceneDrawStatus > 6) {
4305       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4306       Video.color = 0xff_ff_ff; //draw_set_color(txtCol);
4307       drawTextAt(64, 96+16, "Saves: ");
4308       Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4309       drawTextAt(96+24, 96+16, va("%s", stats.damselsSaved));
4310     }
4312     if (winFadeOut) {
4313       Video.color = (255-clamp(winFadeLevel, 0, 255))<<24;
4314       Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
4315     }
4317     if (winSceneDrawStatus == 8) {
4318       sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4319       Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4320       string lastString;
4321       if (global.config.gameMode == GameConfig::GameMode.Vanilla) {
4322         Video.color = 0xff_ff_00; //draw_set_color(c_yellow);
4323         lastString = "YOU SHALL BE REMEMBERED AS A HERO.";
4324       } else {
4325         Video.color = 0x00_ff_ff;
4326         if (global.config.bizarrePlus) lastString = "ANOTHER LEGENDARY ADVENTURE!";
4327         else lastString = "YOUR DISCOVERIES WILL BE CELEBRATED!";
4328       }
4329       auto strLen = lastString.length*8;
4330       int n = 320-strLen;
4331       n = trunc(ceil(n/2.0));
4332       drawTextAt(n, 116, lastString);
4333     }
4334   }
4338 // ////////////////////////////////////////////////////////////////////////// //
4339 #include "roomTitle.vc"
4340 #include "roomTrans1.vc"
4341 #include "roomTrans2.vc"
4342 #include "roomTrans3.vc"
4343 #include "roomTrans4.vc"
4344 #include "roomOlmec.vc"
4345 #include "roomEnd.vc"
4348 // ////////////////////////////////////////////////////////////////////////// //
4349 #include "packages/Generator/loadRoomGens.vc"
4350 #include "packages/Generator/loadEntityGens.vc"