alot more data for replays, but replays are alot more reliable; also, fast-forwarding...
[k8vacspelynky.git] / GameLevel.vc
blob05af7fd56f44db9fdbf546a74d9354f6f6d6a473
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 //RoomGen[LevelGen::LevelWidth, LevelGen::LevelHeight] rooms; // moved to levelgen
45 transient float accumTime;
46 transient bool gamePaused = false;
47 transient bool checkWater;
48 transient int damselSaved;
50 // hud efffects
51 transient int xmoney;
52 transient int collectCounter;
53 transient int levelMoneyStart;
55 // all movable (thinkable) map objects
56 EntityGrid objGrid; // monsters and items
58 MapTile[MaxTilesWidth, MaxTilesHeight] tiles;
59 MapBackTile backtiles;
60 EntityGrid miscTileGrid; // moveables and ropes
61 bool blockWaterChecking;
63 array!MapObject ballObjects; // list of all ball objects, for speed
65 LevelGen::RType[MaxTilesWidth, MaxTilesHeight] roomType;
67 enum LevelKind {
68   Normal,
69   Transition,
70   Title,
71   Final,
73 LevelKind levelKind = LevelKind.Normal;
75 array!MapTile allEnters;
76 array!MapTile allExits;
79 int startRoomX, startRoomY;
80 int endRoomX, endRoomY;
82 PlayerPawn player;
83 transient bool playerExited;
84 transient bool disablePlayerThink = false;
85 transient int maxPlayingTime; // in seconds
87 int ghostTimeLeft;
88 int musicFadeTimer;
89 bool ghostSpawned; // to speed up some checks
92 // FPS, i.e. incremented by 30 in one second
93 int time; // in frames
94 int lastUsedObjectId;
96 // screen shake variables
97 int shakeLeft;
98 IVec2D shakeOfs;
99 IVec2D shakeDir;
101 // set this before calling `fixCamera()`
102 // dimensions should be real, not scaled up/down
103 transient int viewWidth, viewHeight;
104 // room bounds, not scaled
105 IVec2D viewMin, viewMax;
107 // `fixCamera()` will set the following
108 // coordinates will be real too (with scale applied)
109 // shake is not applied
110 transient IVec2D viewStart; // with `player.viewOffset`
111 private transient IVec2D realViewStart; // without `player.viewOffset`
113 // if `frameSkip` is `true`, there are more frames waiting
114 // (i.e. you may skip rendering and such)
115 transient void delegate (bool frameSkip) onBeforeFrame;
116 transient void delegate (bool frameSkip) onAfterFrame;
118 transient void delegate () onLevelExitedCB;
120 // this will be called in-between frames, and
121 // `frameTime` is [0..1)
122 transient void delegate (float frameTime) onInterFrame;
124 final int bizRoomStyle { get { return (lg ? lg.bizRoomStyle : 0); } }
127 final bool isNormalLevel () { return (levelKind == LevelKind.Normal); }
130 // ////////////////////////////////////////////////////////////////////////// //
131 // stats
132 void addDeath (name aname) { stats.addDeath(aname); }
133 void addKill (name aname) { stats.addKill(aname); }
134 void addCollect (name aname, optional int amount) { stats.addCollect(aname, amount!optional); }
136 void addDamselSaved () { stats.addDamselSaved(); }
137 void addIdolStolen () { stats.addIdolStolen(); }
138 void addIdolConverted () { stats.addIdolConverted(); }
139 void addCrystalIdolStolen () { stats.addCrystalIdolStolen(); }
140 void addCrystalIdolConverted () { stats.addCrystalIdolConverted(); }
141 void addGhostSummoned () { stats.addGhostSummoned(); }
144 // ////////////////////////////////////////////////////////////////////////// //
145 final int tilesWidth () { return lg.levelRoomWidth*RoomGen::Width+2; }
146 final int tilesHeight () { return lg.levelRoomHeight*RoomGen::Height+2; }
149 // ////////////////////////////////////////////////////////////////////////// //
150 // this won't generate a level yet
151 void restartGame () {
152   if (player) {
153     auto hi = player.holdItem;
154     player.holdItem = none;
155     if (hi) hi.instanceRemove();
156     hi = player.pickedItem;
157     player.pickedItem = none;
158     if (hi) hi.instanceRemove();
159   }
160   global.resetGame();
161   stats.clearGameTotals();
162   if (global.startMoney > 0) stats.setMoneyCheat();
163   stats.setMoney(global.startMoney);
164   //writeln("level=", global.currLevel, "; lt=", global.levelType);
168 // complement function to `restart game`
169 void generateNormalLevel () {
170   generateLevel();
171   centerViewAtPlayer();
175 // ////////////////////////////////////////////////////////////////////////// //
176 // generate angry shopkeeper at exit if murderer or thief
177 void generateAngryShopkeepers () {
178   if (global.murderer || global.thiefLevel > 0) {
179     foreach (MapTile e; allExits) {
180       auto obj = MonsterShopkeeper(MakeMapObject(e.ix, e.iy, 'oShopkeeper'));
181       if (obj) {
182         obj.style = 'Bounty Hunter';
183         obj.status = MapObject::PATROL;
184       }
185     }
186   }
190 // ////////////////////////////////////////////////////////////////////////// //
191 final void resetRoomBounds () {
192   viewMin.x = 0;
193   viewMin.y = 0;
194   viewMax.x = tilesWidth*16;
195   viewMax.y = tilesHeight*16;
196   // Great Lake is bottomless (nope)
197   //if (global.lake) viewMax.y -= 16;
198   //writeln("ROOM BOUNDS: (", viewMax.x, "x", viewMax.y, ")");
202 final void setRoomBounds (int x0, int y0, int x1, int y1) {
203   viewMin.x = x0;
204   viewMin.y = y0;
205   viewMax.x = x1+16;
206   viewMax.y = y1+16;
210 // ////////////////////////////////////////////////////////////////////////// //
211 struct OSDMessage {
212   string msg;
213   float timeout; // seconds
214   float starttime; // for active
215   bool active; // true: timeout is `GetTickCount()` dismissing time
218 array!OSDMessage msglist; // [0]: current one
221 private final void osdCheckTimeouts () {
222   auto stt = GetTickCount();
223   while (msglist.length) {
224     if (!msglist[0].active) {
225       msglist[0].active = true;
226       msglist[0].starttime = stt;
227     }
228     if (msglist[0].starttime+msglist[0].timeout >= stt) break;
229     msglist.remove(0);
230   }
234 final bool osdHasMessage () {
235   osdCheckTimeouts();
236   return (msglist.length > 0);
240 final string osdGetMessage (out float timeLeft, out float timeStart) {
241   osdCheckTimeouts();
242   if (msglist.length == 0) { timeLeft = 0; return ""; }
243   auto stt = GetTickCount();
244   timeStart = msglist[0].starttime;
245   timeLeft = fmax(0, msglist[0].starttime+msglist[0].timeout-stt);
246   return msglist[0].msg;
250 final void osdClear () {
251   msglist.length -= msglist.length;
255 final void osdMessage (string msg, optional float timeout, optional bool urgent) {
256   if (!msg) return;
257   if (!specified_timeout) timeout = 3.33;
258   // special message for shops
259   if (timeout == -666) {
260     if (!msg) return;
261     if (msglist.length && msglist[0].msg == msg) return;
262     if (msglist.length == 0 || msglist[0].msg != msg) {
263       osdClear();
264       msglist.length += 1;
265       msglist[0].msg = msg;
266     }
267     msglist[0].active = false;
268     msglist[0].timeout = 3.33;
269     osdCheckTimeouts();
270     return;
271   }
272   if (timeout < 0.1) return;
273   timeout = fmax(1.0, timeout);
274   //writeln("OSD: ", msg);
275   // find existing one, and bring it to the top
276   int oldidx = 0;
277   for (; oldidx < msglist.length; ++oldidx) {
278     if (msglist[oldidx].msg == msg) break; // i found her!
279   }
280   // duplicate?
281   if (oldidx < msglist.length) {
282     // yeah, move duplicate to the top
283     msglist[oldidx].timeout = fmax(timeout, msglist[oldidx].timeout);
284     msglist[oldidx].active = false;
285     if (urgent && oldidx != 0) {
286       timeout = msglist[oldidx].timeout;
287       msglist.remove(oldidx);
288       msglist.insert(0);
289       msglist[0].msg = msg;
290       msglist[0].timeout = timeout;
291       msglist[0].active = false;
292     }
293   } else if (urgent) {
294     msglist.insert(0);
295     msglist[0].msg = msg;
296     msglist[0].timeout = timeout;
297     msglist[0].active = false;
298   } else {
299     // new one
300     msglist.length += 1;
301     msglist[$-1].msg = msg;
302     msglist[$-1].timeout = timeout;
303     msglist[$-1].active = false;
304   }
305   osdCheckTimeouts();
309 // ////////////////////////////////////////////////////////////////////////// //
310 void setup (GameGlobal aGlobal, SpriteStore aSprStore, BackTileStore aBGTileStore) {
311   global = aGlobal;
312   sprStore = aSprStore;
313   bgtileStore = aBGTileStore;
315   lg = SpawnObject(LevelGen);
316   lg.global = global;
317   lg.level = self;
319   miscTileGrid = SpawnObject(EntityGrid);
320   miscTileGrid.setup(0, 0, MaxTilesWidth*16, MaxTilesHeight*16, MapTile);
321   //miscTileGrid.ownObjects = true;
323   objGrid = SpawnObject(EntityGrid);
324   objGrid.setup(0, 0, MaxTilesWidth*16, MaxTilesHeight*16, MapObject);
328 // stores should be set
329 void onLoaded () {
330   checkWater = true;
331   levBGImg = bgtileStore[levBGImgName];
332   foreach (int y; 0..MaxTilesHeight) {
333     foreach (int x; 0..MaxTilesWidth) {
334       if (tiles[x, y]) tiles[x, y].onLoaded();
335     }
336   }
337   foreach (MapEntity o; miscTileGrid.allObjects()) o.onLoaded();
338   foreach (MapEntity o; objGrid.allObjects()) o.onLoaded();
339   for (MapBackTile bt = backtiles; bt; bt = bt.next) bt.onLoaded();
340   if (player) player.onLoaded();
341   //FIXME
342   if (msglist.length) {
343     msglist[0].active = false;
344     msglist[0].timeout = 0.200;
345     osdCheckTimeouts();
346   }
350 // ////////////////////////////////////////////////////////////////////////// //
351 void pickedSpectacles () {
352   foreach (int y; 0..tilesHeight) {
353     foreach (int x; 0..tilesWidth) {
354       MapTile t = tiles[x, y];
355       if (t && t.isInstanceAlive) t.onGotSpectacles();
356     }
357   }
358   foreach (MapTile t; miscTileGrid.allObjects()) {
359     if (t.isInstanceAlive) t.onGotSpectacles();
360   }
364 // ////////////////////////////////////////////////////////////////////////// //
365 #include "rgentile.vc"
366 #include "rgenobj.vc"
369 void onLevelExited () {
370   if (isNormalLevel()) stats.maxLevelComplete = max(stats.maxLevelComplete, global.currLevel);
371   if (onLevelExitedCB) onLevelExitedCB();
372   if (levelKind == LevelKind.Transition) {
373     if (global.thiefLevel > 0) global.thiefLevel -= 1;
374     //orig dbg:if (global.currLevel == 1) global.currLevel += firstLevelSkip; else global.currLevel += levelSkip;
375     global.currLevel += 1;
376     generateLevel();
377   } else {
378     generateTransitionLevel();
379   }
380   centerViewAtPlayer();
384 void generateLevelMessages () {
385   if (global.darkLevel) {
386          if (global.hasCrown) osdMessage("THE HEDJET SHINES BRIGHTLY.");
387     else if (global.config.scumDarkness < 2) osdMessage("I CAN'T SEE A THING!");
388     /*
389     else global.message = "";
390          if (global.hasCrown) global.message2 = "";
391     else if (global.scumDarkness &lt; 2) global.message2 = "I'D BETTER USE THESE FLARES!";
392     else global.message2 = "";
393     global.messageTimer = 200;
394     alarm[1] = 210;
395     */
396   }
398   if (global.blackMarket) osdMessage("WELCOME TO THE BLACK MARKET!");
400   if (global.cemetary) osdMessage("THE DEAD ARE RESTLESS!");
401   if (global.lake) osdMessage("I CAN HEAR RUSHING WATER...");
403   if (global.snakePit) osdMessage("I HEAR SNAKES... I HATE SNAKES!");
404   if (global.yetiLair) osdMessage("IT SMELLS LIKE WET FUR IN HERE!");
405   if (global.alienCraft) osdMessage("THERE'S A PSYCHIC PRESENCE HERE!");
406   if (global.cityOfGold) {
407     if (true /*hasOlmec()*/) osdMessage("IT'S THE LEGENDARY CITY OF GOLD!");
408   }
410   if (global.sacrificePit) osdMessage("I CAN HEAR PRAYERS TO KALI!");
414 final MapObject debugSpawnObjectWithClass (class!MapObject oclass, optional bool playerDir) {
415   if (!oclass) return none;
416   int dx = 0, dy = 0;
417   bool canLeft = !isSolidAtPoint(player.ix-8, player.iy);
418   bool canRight = !isSolidAtPoint(player.ix+16, player.iy);
419   if (!canLeft && !canRight) return none;
420   if (canLeft && canRight) {
421     if (playerDir) {
422       dx = (player.dir == MapObject::Dir.Left ? -16 : 16);
423     } else {
424       dx = 16;
425     }
426   } else {
427     dx = (canLeft ? -16 : 16);
428   }
429   auto obj = SpawnMapObjectWithClass(oclass);
430   if (obj isa MapEnemy) { dx -= 8; dy -= 8; }
431   if (obj) PutSpawnedMapObject(player.ix+dx, player.iy+dy, obj);
432   return obj;
436 final MapObject debugSpawnObject (name aname) {
437   if (!aname) return none;
438   return debugSpawnObjectWithClass(findGameObjectClassByName(aname));
442 // `global.currLevel` is the new level
443 void generateTransitionLevel () {
444   xmoney = 0;
445   collectCounter = 0;
447   global.setMusicPitch(1.0);
448   global.stopMusic();
450   levelKind = LevelKind.Transition;
452   auto olddel = ImmediateDelete;
453   ImmediateDelete = false;
454   clearTiles();
455   clearObjects();
457        if (global.currLevel < 4) createTrans1Room();
458   else if (global.currLevel == 4) createTrans1xRoom();
459   else if (global.currLevel < 8) createTrans2Room();
460   else if (global.currLevel == 8) createTrans2xRoom();
461   else if (global.currLevel < 12) createTrans3Room();
462   else if (global.currLevel == 12) createTrans3xRoom();
463   else if (global.currLevel < 16) createTrans4Room();
464   else if (global.currLevel == 16) createTrans4Room();
465   else createTrans1Room(); //???
467   fixWallTiles();
468   addBackgroundGfxDetails();
469   levBGImgName = 'bgCave';
470   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
472   blockWaterChecking = true;
473   fixLiquidTop();
474   cleanDeadTiles();
476   if (damselSaved > 0) {
477     MakeMapObject(176+8, 176+8, 'oDamselKiss');
478     global.plife += damselSaved; // if player skipped transition cutscene
479     damselSaved = 0;
480   }
482   ImmediateDelete = olddel;
483   CollectGarbage(true); // destroy delayed objects too
485   if (dumpGridStats) {
486     miscTileGrid.dumpStats();
487     objGrid.dumpStats();
488   }
490   playerExited = false; // just in case
492   osdClear();
494   setupGhostTime();
495   //global.playMusic(lg.musicName);
499 void generateLevel () {
500   global.setMusicPitch(1.0);
501   stats.clearLevelTotals();
503   levelKind = LevelKind.Normal;
504   lg.generate();
505   //lg.dump();
507   resetRoomBounds();
509   lg.generateRooms();
511   auto olddel = ImmediateDelete;
512   ImmediateDelete = false;
513   clearTiles();
514   clearObjects();
516   // if transition cutscene was skipped...
517   if (damselSaved > 0) global.plife += damselSaved; // if player skipped transition cutscene
518   damselSaved = 0;
520   // generate tiles
521   startRoomX = lg.startRoomX;
522   startRoomY = lg.startRoomY;
523   endRoomX = lg.endRoomX;
524   endRoomY = lg.endRoomY;
525   addBackgroundGfxDetails();
526   foreach (int y; 0..tilesHeight) {
527     foreach (int x; 0..tilesWidth) {
528       lg.genTileAt(x, y);
529     }
530   }
531   fixWallTiles();
533   levBGImgName = lg.bgImgName;
534   levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
536   if (global.allowAngryShopkeepers) generateAngryShopkeepers();
538   lg.generateEntities();
540   // add box of flares to dark level
541   if (global.darkLevel && allEnters.length) {
542     auto enter = allEnters[0];
543     int x = enter.ix, y = enter.iy;
544          if (!isSolidAtPoint(x-16, y)) MakeMapObject(x-16+8, y+8, 'oFlareCrate');
545     else if (!isSolidAtPoint(x+16, y)) MakeMapObject(x+16+8, y+8, 'oFlareCrate');
546     else MakeMapObject(x+8, y+8, 'oFlareCrate');
547   }
549   //scrGenerateEntities();
550   //foreach (; 0..2) scrGenerateEntities();
552   writeln(countObjects, " alive objects inserted");
553   writeln(countBackTiles, " background tiles inserted");
555   if (!player) FatalError("player pawn is not spawned");
557   blockWaterChecking = false;
558   fixLiquidTop();
559   cleanDeadTiles();
561   ImmediateDelete = olddel;
562   CollectGarbage(true); // destroy delayed objects too
564   if (dumpGridStats) {
565     miscTileGrid.dumpStats();
566     objGrid.dumpStats();
567   }
569   playerExited = false; // just in case
571   levelMoneyStart = stats.money;
573   osdClear();
574   generateLevelMessages();
576   xmoney = 0;
577   collectCounter = 0;
579   global.playMusic(lg.musicName);
581   setupGhostTime();
585 // ////////////////////////////////////////////////////////////////////////// //
586 int currKeys, nextKeys;
587 int pressedKeysQ, releasedKeysQ;
588 int keysPressed, keysReleased = -1;
591 struct SavedKeyState {
592   int currKeys, nextKeys;
593   int pressedKeysQ, releasedKeysQ;
594   int keysPressed, keysReleased;
595   // for session
596   int roomSeed, otherSeed;
600 // for saving/replaying
601 final void keysSaveState (out SavedKeyState ks) {
602   ks.currKeys = currKeys;
603   ks.nextKeys = nextKeys;
604   ks.pressedKeysQ = pressedKeysQ;
605   ks.releasedKeysQ = releasedKeysQ;
606   ks.keysPressed = keysPressed;
607   ks.keysReleased = keysReleased;
610 // for saving/replaying
611 final void keysRestoreState (const ref SavedKeyState ks) {
612   currKeys = ks.currKeys;
613   nextKeys = ks.nextKeys;
614   pressedKeysQ = ks.pressedKeysQ;
615   releasedKeysQ = ks.releasedKeysQ;
616   keysPressed = ks.keysPressed;
617   keysReleased = ks.keysReleased;
621 final void keysNextFrame () {
622   currKeys = nextKeys;
626 final void clearKeys () {
627   currKeys = 0;
628   nextKeys = 0;
629   pressedKeysQ = 0;
630   releasedKeysQ = 0;
631   keysPressed = 0;
632   keysReleased = -1;
636 final void onKey (int code, bool down) {
637   if (!code) return;
638   if (down) {
639     currKeys |= code;
640     nextKeys |= code;
641     if (keysReleased&code) {
642       keysPressed |= code;
643       keysReleased &= ~code;
644       pressedKeysQ |= code;
645     }
646   } else {
647     nextKeys &= ~code;
648     if (keysPressed&code) {
649       keysReleased |= code;
650       keysPressed &= ~code;
651       releasedKeysQ |= code;
652     }
653   }
656 final bool isKeyDown (int code) {
657   return !!(currKeys&code);
660 final bool isKeyPressed (int code) {
661   bool res = !!(pressedKeysQ&code);
662   pressedKeysQ &= ~code;
663   return res;
666 final bool isKeyReleased (int code) {
667   bool res = !!(releasedKeysQ&code);
668   releasedKeysQ &= ~code;
669   return res;
673 final void clearKeysPressRelease () {
674   keysPressed = default.keysPressed;
675   keysReleased = default.keysReleased;
676   pressedKeysQ = default.pressedKeysQ;
677   releasedKeysQ = default.releasedKeysQ;
678   currKeys = 0;
679   nextKeys = 0;
683 // ////////////////////////////////////////////////////////////////////////// //
684 final void registerEnter (MapTile t) {
685   if (!t) return;
686   allEnters[$] = t;
687   return;
691 final void registerExit (MapTile t) {
692   if (!t) return;
693   allExits[$] = t;
694   return;
698 final bool isYAtEntranceRow (int py) {
699   py /= 16;
700   foreach (MapTile t; allEnters) if (t.iy == py) return true;
701   return false;
705 final int calcNearestEnterDist (int px, int py) {
706   if (allEnters.length == 0) return int.max;
707   int curdistsq = int.max;
708   foreach (MapTile t; allEnters) {
709     int xc = px-t.xCenter, yc = py-t.yCenter;
710     int distsq = xc*xc+yc*yc;
711     if (distsq < curdistsq) curdistsq = distsq;
712   }
713   return round(sqrt(curdistsq));
717 final int calcNearestExitDist (int px, int py) {
718   if (allExits.length == 0) return int.max;
719   int curdistsq = int.max;
720   foreach (MapTile t; allExits) {
721     int xc = px-t.xCenter, yc = py-t.yCenter;
722     int distsq = xc*xc+yc*yc;
723     if (distsq < curdistsq) curdistsq = distsq;
724   }
725   return round(sqrt(curdistsq));
729 // ////////////////////////////////////////////////////////////////////////// //
730 final void clearForTransition () {
731   auto olddel = ImmediateDelete;
732   ImmediateDelete = false;
733   clearTiles();
734   clearObjects();
735   ImmediateDelete = olddel;
736   CollectGarbage(true); // destroy delayed objects too
740 final void clearTiles () {
741   accumTime = 0;
742   time = 0;
743   allEnters.length -= allEnters.length; // don't deallocate
744   allExits.length -= allExits.length; // don't deallocate
745   foreach (ref auto tile; tiles) delete tile;
746   if (dumpGridStats) { if (miscTileGrid.getFirstObject()) miscTileGrid.dumpStats(); }
747   miscTileGrid.removeAllObjects(true); // and destroy
748   while (backtiles) {
749     MapBackTile t = backtiles;
750     backtiles = t.next;
751     delete t;
752   }
753   levBGImg = none;
757 // ////////////////////////////////////////////////////////////////////////// //
758 final int countObjects () {
759   return objGrid.countObjects();
762 final int countBackTiles () {
763   int res = 0;
764   for (MapBackTile bt = backtiles; bt; bt = bt.next) ++res;
765   return res;
768 final void clearObjects () {
769   // don't kill objects player is holding
770   if (player) {
771     if (player.pickedItem && player.pickedItem.grid) {
772       player.pickedItem.grid.remove(player.pickedItem.gridId);
773       writeln("secured pocket item '", GetClassName(player.pickedItem.Class), "'");
774       //player.pickedItem.grid = none;
775     }
776     if (player.holdItem && player.holdItem.grid) {
777       player.holdItem.grid.remove(player.holdItem.gridId);
778       writeln("secured held item '", GetClassName(player.holdItem.Class), "'");
779       //player.holdItem.grid = none;
780     }
781   }
782   //
783   int count = objGrid.countObjects();
784   if (dumpGridStats) { if (objGrid.getFirstObject()) objGrid.dumpStats(); }
785   objGrid.removeAllObjects(true); // and destroy
786   if (count > 0) writeln(count, " objects destroyed");
787   ballObjects.length = 0;
788   lastUsedObjectId = 0;
792 final void insertObject (MapObject o) {
793   if (!o) return;
794   if (o.grid) FatalError("cannot put object into level twice");
795   o.objId = ++lastUsedObjectId;
797   // ball from ball-and-chain
798   if (o.objType == 'oBall') {
799     bool found = false;
800     foreach (MapObject bo; ballObjects) if (bo == o) { found = true; break; }
801     if (!found) ballObjects[$] = o;
802   }
804   objGrid.insert(o);
808 final void spawnPlayerAt (int x, int y) {
809   // if we have no player, spawn new one
810   // otherwise this just a level transition, so simply reposition him
811   if (!player) {
812     // don't add player to object list, as it has very separate processing anyway
813     player = SpawnObject(PlayerPawn);
814     player.global = global;
815     player.level = self;
816     if (!player.initialize()) {
817       delete player;
818       FatalError("something is wrong with player initialization");
819       return;
820     }
821   }
822   player.fltx = x;
823   player.flty = y;
824   player.saveInterpData();
825   player.resurrect();
826   playerExited = false;
827   if (global.config.startWithKapala) global.hasKapala = true;
828   centerViewAtPlayer();
829   // reinsert player items into grid
830   if (player.pickedItem) objGrid.insert(player.pickedItem);
831   if (player.holdItem) objGrid.insert(player.holdItem);
832   //writeln("player spawned; active=", player.active);
833   player.scrSwitchToPocketItem(forceIfEmpty:false);
837 final void teleportPlayerTo (int x, int y) {
838   if (player) {
839     player.fltx = x;
840     player.flty = y;
841     player.saveInterpData();
842   }
846 final void resurrectPlayer () {
847   if (player) player.resurrect();
848   playerExited = false;
852 // ////////////////////////////////////////////////////////////////////////// //
853 final void scrShake (int duration) {
854   if (shakeLeft == 0) {
855     shakeOfs.x = 0;
856     shakeOfs.y = 0;
857     shakeDir.x = 0;
858     shakeDir.y = 0;
859   }
860   shakeLeft = max(shakeLeft, duration);
865 // ////////////////////////////////////////////////////////////////////////// //
866 enum SCAnger {
867   TileDestroyed,
868   ItemStolen, // including damsel, lol
869   CrapsCheated,
870   BombDropped,
871   DamselWhipped,
875 // make the nearest shopkeeper angry. RAWR!
876 void scrShopkeeperAnger (SCAnger reason, optional int maxdist, optional MapEntity offender) {
877   if (!offender) offender = player;
878   auto shp = MonsterShopkeeper(findNearestEnemy(offender.ix, offender.iy, delegate bool (MapEnemy o) {
879     auto sc = MonsterShopkeeper(o);
880     if (!sc) return false;
881     if (sc.dead || sc.angered) return false;
882     return true;
883   }));
885   if (shp) {
886     if (specified_maxdist && offender.directionToEntityCenter(shp) > maxdist) return;
887     if (!shp.dead && !shp.angered) {
888       shp.status = MapObject::ATTACK;
889       string msg;
890            if (global.murderer) msg = "YOU'LL PAY FOR YOUR CRIMES!";
891       else if (reason == SCAnger.ItemStolen) msg = "COME BACK HERE, THIEF!";
892       else if (reason == SCAnger.TileDestroyed) msg = "DIE, YOU VANDAL!";
893       else if (reason == SCAnger.BombDropped) msg = "TERRORIST!";
894       else if (reason == SCAnger.DamselWhipped) msg = "HEY, ONLY I CAN DO THAT!";
895       else if (reason == SCAnger.CrapsCheated) msg = "DIE, CHEATER!";
896       else msg = "NOW I'M REALLY STEAMED!";
897       if (msg) osdMessage(msg, -666);
898       global.thiefLevel += (global.thiefLevel > 0 ? 3 : 2);
899     }
900   }
904 final MapObject findCrapsPrize () {
905   foreach (MapObject o; objGrid.allObjects()) {
906     if (o.spectral || !o.isInstanceAlive) continue;
907     if (o.inDiceHouse) return o;
908   }
909   return none;
913 // ////////////////////////////////////////////////////////////////////////// //
914 // 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.
915 // note: idols moved by monkeys will have false `stolenIdol`
916 void scrTriggerIdolAltar (bool stolenIdol) {
917   ObjTikiCurse res = none;
918   int curdistsq = int.max;
919   int px = player.xCenter, py = player.yCenter;
920   foreach (MapObject o; objGrid.allObjects()) {
921     auto tcr = ObjTikiCurse(o);
922     if (!tcr || !tcr.isInstanceAlive) continue;
923     if (tcr.activated) continue;
924     int xc = px-tcr.xCenter, yc = py-tcr.yCenter;
925     int distsq = xc*xc+yc*yc;
926     if (distsq < curdistsq) {
927       res = tcr;
928       curdistsq = distsq;
929     }
930   }
931   if (res) res.activate(stolenIdol);
935 // ////////////////////////////////////////////////////////////////////////// //
936 void setupGhostTime () {
937   musicFadeTimer = -1;
938   ghostSpawned = false;
940   if (!isNormalLevel()) {
941     ghostTimeLeft = -1;
942     global.setMusicPitch(1.0);
943     return;
944   }
946   if (global.config.scumGhost < 0) {
947     // instant
948     ghostTimeLeft = 1;
949     osdMessage("A GHASTLY VOICE CALLS YOUR NAME!\nLET'S GET OUT OF HERE!", 6.66, urgent:true);
950     return;
951   }
953   if (global.config.scumGhost == 0) {
954     // never
955     ghostTimeLeft = -1;
956     return;
957   }
959   // randomizes time until ghost appears once time limit is reached
960   // global.ghostRandom (true/false) if extra time is randomized - loaded from yasm.cfg. if false, use 30 second default
961   // ghostTimeLeft (time in seconds * 1000) for currently generated level
963   if (global.config.ghostRandom) {
964     auto tMin = global.randOther(10, 30); // min(global.plife*10, 30);
965     auto tMax = max(tMin+global.randOther(10, 30), min(global.config.scumGhost, 300)); // 5 minutes max
966     auto tTime = global.randOther(tMin, tMax);
967     if (tTime <= 0) tTime = round(tMax/2.0);
968     ghostTimeLeft = tTime;
969   } else {
970     ghostTimeLeft = global.config.scumGhost; // 5 minutes max
971   }
973   ghostTimeLeft += max(0, global.config.ghostExtraTime);
975   ghostTimeLeft *= 30; // seconds -> frames
976   //global.ghostShowTime
980 void spawnGhost () {
981   addGhostSummoned();
982   ghostSpawned = true;
984   int vwdt = (viewMax.x-viewMin.x);
985   int vhgt = (viewMax.y-viewMin.y);
987   int gx, gy;
989   if (player.ix < viewMin.x+vwdt/2) {
990     // player is in the left side
991     gx = viewMin.x+vwdt/2+vwdt/4;
992   } else {
993     // player is in the right side
994     gx = viewMin.x+vwdt/4;
995   }
997   if (player.iy < viewMin.y+vhgt/2) {
998     // player is in the left side
999     gy = viewMin.y+vhgt/2+vhgt/4;
1000   } else {
1001     // player is in the right side
1002     gy = viewMin.y+vhgt/4;
1003   }
1005   writeln("spawning ghost at tile (", gx/16, ",", gy/16, "); player is at tile (", player.ix/16, ",", player.iy/16, ")");
1007   MakeMapObject(gx, gy, 'oGhost');
1009   /*
1010     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);
1011     else instance_create(view_xview[0]-32, view_yview[0]+floor(view_hview[0]/2), oGhost);
1012     global.ghostExists = true;
1013   */
1017 void thinkFrameGameGhost () {
1018   if (player.dead) return;
1019   if (!isNormalLevel()) return; // just in case
1021   if (ghostTimeLeft < 0) {
1022     // turned off
1023     if (musicFadeTimer > 0) {
1024       musicFadeTimer = -1;
1025       global.setMusicPitch(1.0);
1026     }
1027     return;
1028   }
1030   if (musicFadeTimer >= 0) {
1031     ++musicFadeTimer;
1032     if (musicFadeTimer > 0 && musicFadeTimer <= 100*3) {
1033       float pitch = 1.0-(0.00226/3.0)*musicFadeTimer;
1034       //writeln("MFT: ", musicFadeTimer, "; pitch=", pitch);
1035       global.setMusicPitch(pitch);
1036     }
1037   }
1039   if (ghostTimeLeft == 0) {
1040     // she is already here!
1041     return;
1042   }
1044   // no ghost if we have a crown
1045   if (global.hasCrown) {
1046     ghostTimeLeft = -1;
1047     return;
1048   }
1050   // if she was already spawned, don't do it again
1051   if (ghostSpawned) {
1052     ghostTimeLeft = 0;
1053     return;
1054   }
1056   if (--ghostTimeLeft != 0) {
1057     // warning
1058     if (global.config.ghostExtraTime > 0) {
1059       if (ghostTimeLeft == global.config.ghostExtraTime*30) {
1060         osdMessage("A CHILL RUNS UP YOUR SPINE...\nLET'S GET OUT OF HERE!", 6.66, urgent:true);
1061       }
1062       if (musicFadeTimer < 0 && ghostTimeLeft <= global.config.ghostExtraTime*30) {
1063         musicFadeTimer = 0;
1064       }
1065     }
1066     return;
1067   }
1069   // spawn her
1070   if (player.isExitingSprite) {
1071     // no reason to spawn her, we're leaving
1072     ghostTimeLeft = -1;
1073     return;
1074   }
1076   spawnGhost();
1080 void thinkFrameGame () {
1081   thinkFrameGameGhost();
1085 // ////////////////////////////////////////////////////////////////////////// //
1086 private transient array!MapObject activeThinkerList;
1089 private final bool isWetTile (MapTile t) {
1090   return (t && t.visible && (t.water || t.lava || t.wet));
1094 private final bool isWetOrSolidTile (MapTile t) {
1095   return (t && t.visible && (t.water || t.lava || t.wet || t.solid) && t.isInstanceAlive);
1099 final bool isWetOrSolidTileAtPoint (int px, int py) {
1100   return !!checkTileAtPoint(px, py, &isWetOrSolidTile);
1104 final bool isWetOrSolidTileAtTile (int tx, int ty) {
1105   return !!checkTileAtPoint(tx*16, ty*16, &isWetOrSolidTile);
1109 final bool isWetTileAtTile (int tx, int ty) {
1110   return !!checkTileAtPoint(tx*16, ty*16, &isWetTile);
1114 const int GreatLakeStartTileY = 28;
1116 // called once after level generation
1117 final void fixLiquidTop () {
1118   foreach (int tileY; 0..tilesHeight) {
1119     foreach (int tileX; 0..tilesWidth) {
1120       auto t = tiles[tileX, tileY];
1122       if (t && !t.isInstanceAlive) {
1123         delete tiles[tileX, tileY];
1124         t = none;
1125       }
1127       //if (!t) continue;
1128       //if (!t.water && !t.lava) continue;
1130       if (!t) {
1131         if (global.lake && tileY >= GreatLakeStartTileY) {
1132           //if (tileX >= NormalTilesWidth) continue;
1133           // fill level with water for lake
1134           MakeMapTile(tileX, tileY, 'oWaterSwim');
1135           t = tiles[tileX, tileY];
1136         } else {
1137           continue;
1138         }
1139       }
1141       if (!t.water && !t.lava) {
1142         // mark as wet for lake
1143         if (global.lake && tileY >= GreatLakeStartTileY) {
1144           /*
1145           if (!t.solid) {
1146             delete tiles[tileX, tileY];
1147             MakeMapTile(tileX, tileY, 'oWaterSwim');
1148             t = tiles[tileX, tileY];
1149           } else {
1150             t.wet = true;
1151           }
1152           */
1153           t.wet = true;
1154         }
1155         continue;
1156       }
1158       if (!isWetTileAtTile(tileX, tileY-1)) {
1159         t.setSprite(t.lava ? 'sLavaTop' : 'sWaterTop');
1160         continue;
1161       }
1163       /* k8: don't distribute, the code above will take care of this
1164       if (t.spriteName != 'sWaterTop' && t.spriteName != 'sLavaTop') {
1165         MapTile obj = getTileAt(tileX-1, tileY);
1166         if (obj && (obj.water || obj.lava)) {
1167           if (obj.spriteName == 'sWaterTop' || obj.spriteName == 'sLavaTop') {
1168             //writeln("FIXLTOP (01)!");
1169             t.setSprite(t.lava ? 'sLavaTop' : 'sWaterTop');
1170           }
1171         }
1172       }
1174       if (t.spriteName != 'sWaterTop' && t.spriteName != 'sLavaTop') {
1175         MapTile obj = getTileAt(tileX+1, tileY);
1176         if (obj && (obj.water || obj.lava)) {
1177           if (obj.spriteName == 'sWaterTop' || obj.spriteName == 'sLavaTop') {
1178             //writeln("FIXLTOP (02)!");
1179             t.setSprite(t.lava ? 'sLavaTop' : 'sWaterTop');
1180           }
1181         }
1182       }
1183       */
1184     }
1185   }
1189 private final void checkWaterFlow (MapTile wtile) {
1190   //if (!wtile || (!wtile.water && !wtile.lava)) return;
1191   //instance_activate_region(x-16, y-16, 48, 48, true);
1193   //int x = wtile.ix, y = wtile.iy;
1194   int tileX = wtile.ix/16, tileY = wtile.iy/16;
1196   if (global.lake && tileY >= GreatLakeStartTileY) return;
1198   /*
1199   if ((not collision_point(x-16, y, oSolid, 0, 0) and not collision_point(x-16, y, oWater, 0, 0)) or
1200       (not collision_point(x+16, y, oSolid, 0, 0) and not collision_point(x+16, y, oWater, 0, 0)) or
1201       (not collision_point(x, y+16, oSolid, 0, 0) and not collision_point(x, y+16, oWater, 0, 0)))
1202   */
1203   if (!isWetOrSolidTileAtTile(tileX-1, tileY) ||
1204       !isWetOrSolidTileAtTile(tileX+1, tileY) ||
1205       !isWetOrSolidTileAtTile(tileX, tileY+1))
1206   {
1207     checkWater = true;
1208     wtile.instanceRemove();
1209     wtile.onDestroy();
1210     delete wtile;
1211     tiles[tileX, tileY] = none;
1212     return;
1213   }
1215   //if (!isSolidAtPoint(x, y-16) && !isLiquidAtPoint(x, y-16)) {
1216   if (!isWetTileAtTile(tileX, tileY-1)) {
1217     wtile.setSprite(wtile.lava ? 'sLavaTop' : 'sWaterTop');
1218   }
1220   /* k8: don't distribute, the code above will take care of this
1221   if (wtile.spriteName != 'sWaterTop' && wtile.spriteName != 'sLavaTop') {
1222     MapTile obj = getTileAt(tileX-1, tileY);
1223     if (obj && (obj.water || obj.lava)) {
1224       if (obj.spriteName == 'sWaterTop' || obj.spriteName == 'sLavaTop') {
1225         wtile.setSprite(wtile.lava ? 'sLavaTop' : 'sWaterTop');
1226       }
1227     }
1228   }
1230   if (wtile.spriteName != 'sWaterTop' && wtile.spriteName != 'sLavaTop') {
1231     MapTile obj = getTileAt(tileX+1, tileY);
1232     if (obj && (obj.water || obj.lava)) {
1233       if (obj.spriteName == 'sWaterTop' || obj.spriteName == 'sLavaTop') {
1234         wtile.setSprite(wtile.lava ? 'sLavaTop' : 'sWaterTop');
1235       }
1236     }
1237   }
1238   */
1242 transient private array!MapTile waterTilesToCheck;
1244 final void cleanDeadTiles () {
1245   bool hasWater = false;
1246   waterTilesToCheck.length -= waterTilesToCheck.length;
1247   foreach (int y; 0..tilesHeight) {
1248     foreach (int x; 0..tilesWidth) {
1249       auto t = tiles[x, y];
1250       if (!t) continue;
1251       if (t.isInstanceAlive) {
1252         if (t.water || t.lava) waterTilesToCheck[$] = t;
1253         continue;
1254       }
1255       checkWater = true;
1256       t.onDestroy();
1257       delete t;
1258       tiles[x, y] = none;
1259     }
1260   }
1261   if (waterTilesToCheck.length && checkWater && !blockWaterChecking) {
1262     //writeln("checking water");
1263     checkWater = false; // `checkWaterFlow()` can set it again
1264     foreach (MapTile t; waterTilesToCheck) {
1265       if (t && t.isInstanceAlive && (t.water || t.lava)) checkWaterFlow(t);
1266     }
1267     // fill empty spaces in lake with water
1268     if (global.lake) {
1269       foreach (int y; GreatLakeStartTileY..tilesHeight) {
1270         foreach (int x; 0..tilesWidth) {
1271           auto t = tiles[x, y];
1272           // just in case
1273           if (t && !t.isInstanceAlive) {
1274             t.onDestroy();
1275             delete tiles[x, y];
1276             t = none;
1277           }
1278           if (t) {
1279             if (!t.water || !t.lava) { t.wet = true; continue; }
1280           } else {
1281             MakeMapTile(x, y, 'oWaterSwim');
1282             t = tiles[x, y];
1283           }
1284           if (t.water) {
1285             t.setSprite(y == GreatLakeStartTileY ? 'sWaterTop' : 'sWater');
1286           } else if (t.lava) {
1287             t.setSprite(y == GreatLakeStartTileY ? 'sLavaTop' : 'sLava');
1288           }
1289         }
1290       }
1291     }
1292   }
1296 // return `true` if thinker should be removed
1297 final bool thinkOne (MapObject o) {
1298   if (!o) return true;
1299   if (o.active && o.isInstanceAlive) {
1300     bool doThink = true;
1302     // collision with player weapon
1303     auto hh = PlayerWeapon(player.holdItem);
1304     bool doWeaponAction;
1305     if (hh) {
1306       if (hh.blockedBySolids) {
1307         int xx = player.ix+(player.dir == MapObject::Dir.Left ? -16 : 16);
1308         doWeaponAction = !isSolidAtPoint(xx, player.iy);
1309       } else {
1310         doWeaponAction = true;
1311       }
1312     } else {
1313       doWeaponAction = false;
1314     }
1316     if (doWeaponAction && o.whipTimer <= 0 && hh && hh.collidesWithObject(o)) {
1317       //writeln("WEAPONED!");
1318       if (!o.onTouchedByPlayerWeapon(player, hh)) {
1319         if (o.isInstanceAlive) hh.onCollisionWithObject(o);
1320       }
1321       o.whipTimer = o.whipTimerValue; //HACK
1322       doThink = o.isInstanceAlive;
1323     }
1325     // collision with player
1326     if (doThink && o.collidesWith(player)) {
1327       if (!player.onObjectTouched(o) && o.isInstanceAlive) {
1328         doThink = !o.onTouchedByPlayer(player);
1329         if (o.grid && o.gridId) o.grid.update(o.gridId, markAsDead:true);
1330       }
1331     }
1333     if (doThink && o.isInstanceAlive) {
1334       o.saveInterpData();
1335       o.processAlarms();
1336       if (o.isInstanceAlive) {
1337         if (o.grid && o.gridId) o.grid.update(o.gridId, markAsDead:true);
1338         o.thinkFrame();
1339         if (o.isInstanceAlive) {
1340           o.nextAnimFrame();
1341           if (o.grid && o.gridId) o.grid.update(o.gridId, markAsDead:true);
1342         }
1343       }
1344     }
1345   }
1346   if (o.isInstanceAlive) {
1347     if (!o.canLiveOutsideOfLevel && o.isOutsideOfLevel()) {
1348       //dead
1349       o.instanceRemove();
1350       return true;
1351     }
1352     // alive
1353     return false;
1354   } else {
1355     // dead
1356     return true;
1357   }
1361 final void processThinkers (float timeDelta) {
1362   if (timeDelta <= 0) return;
1363   if (gamePaused) {
1364     if (onBeforeFrame) onBeforeFrame(false);
1365     if (onAfterFrame) onAfterFrame(false);
1366     keysNextFrame();
1367     return;
1368   }
1369   accumTime += timeDelta;
1370   bool wasFrame = false;
1371   // block GC
1372   auto olddel = ImmediateDelete;
1373   ImmediateDelete = false;
1374   while (accumTime >= FrameTime) {
1375     accumTime -= FrameTime;
1376     if (onBeforeFrame) onBeforeFrame(accumTime >= FrameTime);
1377     // shake
1378     if (shakeLeft > 0) {
1379       --shakeLeft;
1380       if (!shakeDir.x) shakeDir.x = (global.randOther(0, 1) ? -3 : 3);
1381       if (!shakeDir.y) shakeDir.y = (global.randOther(0, 1) ? -3 : 3);
1382       shakeOfs.x = shakeDir.x;
1383       shakeOfs.y = shakeDir.y;
1384       int sgnc = global.randOther(1, 3);
1385       if (sgnc&0x01) shakeDir.x = -shakeDir.x;
1386       if (sgnc&0x02) shakeDir.y = -shakeDir.y;
1387     } else {
1388       shakeOfs.x = 0;
1389       shakeOfs.y = 0;
1390       shakeDir.x = 0;
1391       shakeDir.y = 0;
1392     }
1393     // game-global events
1394     thinkFrameGame();
1395     // frame thinkers: player
1396     if (player && !disablePlayerThink) {
1397       // time limit
1398       if (!player.dead && isNormalLevel() &&
1399           (maxPlayingTime < 0 ||
1400            (maxPlayingTime > 0 && maxPlayingTime*30 <= time &&
1401             time%30 == 0 && global.randOther(1, 100) <= 20)))
1402       {
1403         MakeMapObject(player.ix, player.iy, 'oExplosion');
1404         player.scrCreateFlame(player.ix, player.iy, 3);
1405       }
1406       //HACK: check for stolen items
1407       auto item = MapItem(player.holdItem);
1408       if (item) item.onCheckItemStolen(player);
1409       item = MapItem(player.pickedItem);
1410       if (item) item.onCheckItemStolen(player);
1411       // normal thinking
1412       player.saveInterpData();
1413       player.processAlarms();
1414       if (player.isInstanceAlive) {
1415         player.thinkFrame();
1416         if (player.isInstanceAlive) player.nextAnimFrame();
1417       }
1418     }
1419     // frame thinkers: moveable solids
1420     physStep();
1421     // frame thinkers: objects
1422     auto grid = objGrid;
1423     // collect active objects
1424     if (global.config.useFrozenRegion) {
1425       activeThinkerList.length -= activeThinkerList.length;
1426       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)) {
1427         activeThinkerList[$] = o;
1428       }
1429       //writeln("thinkers: ", activeThinkerList.length);
1430       foreach (MapObject o; activeThinkerList) {
1431         if (thinkOne(o)) {
1432           grid.remove(o.gridId);
1433           o.onDestroy();
1434           delete o;
1435         }
1436       }
1437     } else {
1438       // no frozen area
1439       bool killThisOne = false;
1440       for (int cid = grid.getFirstObject(); cid; cid = grid.getNextObject(cid, killThisOne)) {
1441         killThisOne = false;
1442         MapObject o = grid.getObject(MapObject, cid);
1443         if (!o) { killThisOne = true; continue; }
1444         // remove this object if it is dead
1445         if (thinkOne(o)) {
1446           killThisOne = true;
1447           if (o) {
1448             o.onDestroy();
1449             delete o;
1450           }
1451         }
1452       }
1453     }
1454     // done with thinkers
1455     cleanDeadTiles();
1456     // money counter
1457     if (collectCounter == 0) {
1458       xmoney = max(0, xmoney-100);
1459     } else {
1460       --collectCounter;
1461     }
1462     // other things
1463     if (player && !player.dead) stats.oneMoreFramePlayed();
1464     if (onAfterFrame) onAfterFrame(accumTime >= FrameTime);
1465     keysNextFrame();
1466     wasFrame = true;
1467     if (playerExited) break;
1468   }
1469   ImmediateDelete = olddel;
1470   if (playerExited) {
1471     playerExited = false;
1472     onLevelExited();
1473   }
1474   if (wasFrame) {
1475     // if we were processed at least one frame, collect garbage
1476     //keysNextFrame();
1477     CollectGarbage(true); // destroy delayed objects too
1478   }
1479   if (player.holdItem) player.holdItem.fixHoldCoords();
1480   if (accumTime > 0 && onInterFrame) onInterFrame(accumTime/FrameTime);
1484 // ////////////////////////////////////////////////////////////////////////// //
1485 final void tileXY2roomXY (int tileX, int tileY, out int roomX, out int roomY) {
1486   roomX = (tileX-1)/RoomGen::Width;
1487   roomY = (tileY-1)/RoomGen::Height;
1491 final bool isInShop (int tileX, int tileY) {
1492   if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
1493     auto n = roomType[tileX, tileY];
1494     if (n == 4 || n == 5) return true;
1495     auto t = getTileAt(tileX, tileY);
1496     if (t && t.shopWall) return true;
1497     //k8: we don't have this
1498     //if (t && t.objType == 'oShop') return true;
1499   }
1500   return false;
1504 // ////////////////////////////////////////////////////////////////////////// //
1505 override void Destroy () {
1506   clearTiles();
1507   clearObjects();
1508   ::Destroy();
1512 // ////////////////////////////////////////////////////////////////////////// //
1513 final MapObject findNearestBall (int px, int py) {
1514   MapObject res = none;
1515   int curdistsq = int.max;
1516   foreach (MapObject o; ballObjects) {
1517     if (!o || o.spectral || !o.isInstanceAlive) continue;
1518     int xc = px-o.xCenter, yc = py-o.yCenter;
1519     int distsq = xc*xc+yc*yc;
1520     if (distsq < curdistsq) {
1521       res = o;
1522       curdistsq = distsq;
1523     }
1524   }
1525   return res;
1529 final int calcNearestBallDist (int px, int py) {
1530   auto e = findNearestBall(px, py);
1531   if (!e) return int.max;
1532   int xc = px-e.xCenter, yc = py-e.yCenter;
1533   return round(sqrt(xc*xc+yc*yc));
1537 final MapObject findNearestObject (int px, int py, bool delegate (MapObject o) dg) {
1538   MapObject res = none;
1539   int curdistsq = int.max;
1540   foreach (MapObject o; objGrid.allObjects()) {
1541     if (o.spectral || !o.isInstanceAlive) continue;
1542     if (!dg(o)) continue;
1543     int xc = px-o.xCenter, yc = py-o.yCenter;
1544     int distsq = xc*xc+yc*yc;
1545     if (distsq < curdistsq) {
1546       res = o;
1547       curdistsq = distsq;
1548     }
1549   }
1550   return res;
1554 final MapObject findNearestEnemy (int px, int py, optional bool delegate (MapEnemy o) dg) {
1555   MapObject res = none;
1556   int curdistsq = int.max;
1557   foreach (MapObject o; objGrid.allObjects()) {
1558     //k8: i added `dead` check
1559     if (o.spectral || o !isa MapEnemy || o.dead || !o.isInstanceAlive) continue;
1560     if (dg) {
1561       if (!dg(MapEnemy(o))) continue;
1562     }
1563     int xc = px-o.xCenter, yc = py-o.yCenter;
1564     int distsq = xc*xc+yc*yc;
1565     if (distsq < curdistsq) {
1566       res = o;
1567       curdistsq = distsq;
1568     }
1569   }
1570   return res;
1574 final MonsterShopkeeper hasAliveShopkeepers (optional bool skipAngry) {
1575   foreach (MapObject o; objGrid.allObjects()) {
1576     auto sc = MonsterShopkeeper(o);
1577     if (!sc || o.spectral || !o.isInstanceAlive) continue;
1578     if (sc.dead) continue;
1579     if (skipAngry && sc.angered) continue;
1580     return sc;
1581   }
1582   return none;
1586 final int calcNearestEnemyDist (int px, int py, optional bool delegate (MapEnemy o) dg) {
1587   auto e = findNearestEnemy(px, py, dg!optional);
1588   if (!e) return int.max;
1589   int xc = px-e.xCenter, yc = py-e.yCenter;
1590   return round(sqrt(xc*xc+yc*yc));
1594 final int calcNearestObjectDist (int px, int py, optional bool delegate (MapObject o) dg) {
1595   auto e = findNearestObject(px, py, dg!optional);
1596   if (!e) return int.max;
1597   int xc = px-e.xCenter, yc = py-e.yCenter;
1598   return round(sqrt(xc*xc+yc*yc));
1602 final MapTile findNearestMoveableSolid (int px, int py, optional bool delegate (MapTile t) dg) {
1603   MapTile res = none;
1604   int curdistsq = int.max;
1605   foreach (MapTile t; miscTileGrid.allObjects()) {
1606     if (t.spectral || !t.isInstanceAlive) continue;
1607     if (dg) {
1608       if (!dg(t)) continue;
1609     } else {
1610       if (!t.solid || !t.moveable) continue;
1611     }
1612     int xc = px-t.xCenter, yc = py-t.yCenter;
1613     int distsq = xc*xc+yc*yc;
1614     if (distsq < curdistsq) {
1615       res = t;
1616       curdistsq = distsq;
1617     }
1618   }
1619   return res;
1623 final MapTile findNearestTile (int px, int py, optional bool delegate (MapTile t) dg) {
1624   if (!dg) return none;
1625   MapTile res = none;
1626   int curdistsq = int.max;
1628   //FIXME: make this faster!
1629   foreach (MapTile t; tiles) {
1630     if (!t || t.spectral || !t.isInstanceAlive) continue;
1631     int xc = px-t.xCenter, yc = py-t.yCenter;
1632     int distsq = xc*xc+yc*yc;
1633     if (distsq < curdistsq && dg(t)) {
1634       res = t;
1635       curdistsq = distsq;
1636     }
1637   }
1639   foreach (MapTile t; miscTileGrid.allObjects()) {
1640     if (!t || t.spectral || !t.isInstanceAlive) continue;
1641     int xc = px-t.xCenter, yc = py-t.yCenter;
1642     int distsq = xc*xc+yc*yc;
1643     if (distsq < curdistsq && dg(t)) {
1644       res = t;
1645       curdistsq = distsq;
1646     }
1647   }
1649   return res;
1653 // ////////////////////////////////////////////////////////////////////////// //
1654 final bool cbIsObjectWeb (MapObject o) { return (o isa ItemWeb); }
1655 final bool cbIsObjectBlob (MapObject o) { return (o.objName == 'oBlob'); }
1656 final bool cbIsObjectArrow (MapObject o) { return (o isa ItemProjectileArrow); }
1657 final bool cbIsObjectEnemy (MapObject o) { return (/*o != self &&*/ o isa MapEnemy); }
1659 final bool cbIsObjectTreasure (MapObject o) { return (o.isTreasure); }
1661 final bool cbIsItemObject (MapObject o) { return (o isa MapItem); }
1663 final bool cbIsItemOrEnemyObject (MapObject o) { return (o isa MapItem || o isa MapEnemy); }
1666 final MapObject isObjectAtTile (int tileX, int tileY, optional bool delegate (MapObject o) dg) {
1667   tileX *= 16;
1668   tileY *= 16;
1669   foreach (MapObject o; objGrid.inRectPix(tileX, tileY, 16, 16, objGrid.nextTag(), precise: true)) {
1670     if (o.spectral || !o.isInstanceAlive) continue;
1671     if (dg) {
1672       if (dg(o)) return o;
1673     } else {
1674       return o;
1675     }
1676   }
1677   return none;
1681 final MapObject isObjectAtTilePix (int x, int y, optional bool delegate (MapObject o) dg) {
1682   return isObjectAtTile(x/16, y/16, dg!optional);
1686 final MapObject isObjectAtPoint (int xpos, int ypos, optional bool delegate (MapObject o) dg, optional bool precise) {
1687   if (!specified_precise) precise = true;
1688   foreach (MapObject o; objGrid.inCellPix(xpos, ypos, objGrid.nextTag(), precise: precise)) {
1689     if (o.spectral || !o.isInstanceAlive) continue;
1690     if (dg) {
1691       if (dg(o)) return o;
1692     } else {
1693       if (o isa MapEnemy) return o;
1694     }
1695   }
1696   return none;
1700 final MapObject isObjectInRect (int xpos, int ypos, int w, int h, optional bool delegate (MapObject o) dg, optional bool precise) {
1701   if (w < 1 || h < 1) return none;
1702   if (w == 1 && h == 1) return isObjectAtPoint(xpos, ypos, dg!optional, precise!optional);
1703   if (!specified_precise) precise = true;
1704   foreach (MapObject o; objGrid.inRectPix(xpos, ypos, w, h, objGrid.nextTag(), precise: precise)) {
1705     if (o.spectral || !o.isInstanceAlive) continue;
1706     if (dg) {
1707       if (dg(o)) return o;
1708     } else {
1709       if (o isa MapEnemy) return o;
1710     }
1711   }
1712   return none;
1716 final MapObject forEachObject (bool delegate (MapObject o) dg) {
1717   if (!dg) return none;
1718   foreach (MapObject o; objGrid.allObjects()) {
1719     if (o.spectral || !o.isInstanceAlive) continue;
1720     if (dg(o)) return o;
1721   }
1722   return none;
1726 final MapObject forEachObjectAtPoint (int xpos, int ypos, bool delegate (MapObject o) dg, optional bool precise) {
1727   if (!dg) return none;
1728   if (!specified_precise) precise = true;
1729   foreach (MapObject o; objGrid.inCellPix(xpos, ypos, objGrid.nextTag(), precise: precise)) {
1730     if (o.spectral || !o.isInstanceAlive) continue;
1731     if (dg(o)) return o;
1732   }
1733   return none;
1737 final MapObject forEachObjectInRect (int xpos, int ypos, int w, int h, bool delegate (MapObject o) dg, optional bool precise) {
1738   if (!dg || w < 1 || h < 1) return none;
1739   if (w == 1 && h == 1) return forEachObjectAtPoint(xpos, ypos, dg!optional, precise!optional);
1740   if (!specified_precise) precise = true;
1741   foreach (MapObject o; objGrid.inRectPix(xpos, ypos, w, h, objGrid.nextTag(), precise: precise)) {
1742     if (o.spectral || !o.isInstanceAlive) continue;
1743     if (dg(o)) return o;
1744   }
1745   return none;
1749 private final bool cbIsRopeTile (MapTile t) { return t.rope; }
1751 final MapTile isRopeAtPoint (int px, int py) {
1752   return checkTileAtPoint(px, py, &cbIsRopeTile);
1756 //FIXME!
1757 final MapTile isWaterSwimAtPoint (int px, int py) {
1758   return isWaterAtPoint(px, py);
1762 // ////////////////////////////////////////////////////////////////////////// //
1763 private array!MapObject tmpObjectList;
1765 private final bool cbCollectObjectsWithMask (MapObject t) {
1766   if (!t || !t.visible || t.spectral /*|| t.invincible*/ || !t.isInstanceAlive) return false;
1767   //auto spf = getSpriteFrame();
1768   //if (!t.sprite || t.sprite.frames.length < 1) return false;
1769   tmpObjectList[$] = t;
1770   return false;
1774 final void touchObjectsWithMask (int x, int y, SpriteFrame frm, bool delegate (MapObject t) dg) {
1775   if (!frm || frm.bw < 1 || frm.bh < 1 || !dg) return;
1776   if (frm.isEmptyPixelMask) return;
1777   // collect tiles
1778   if (tmpObjectList.length) tmpObjectList.length -= tmpObjectList.length; // don't realloc
1779   if (player.isRectCollisionFrame(frm, x, y)) {
1780     //writeln("player hit");
1781     tmpObjectList[$] = player;
1782   } else {
1783     /*
1784     writeln("no player hit: plr=(", player.ix, ",", player.iy, ")-(", player.ix+player.width-1, ",", player.iy+player.height-1, "); ",
1785       "frm=(", x+frm.bx, ",", y+frm.by, ")-(", x+frm.bx+frm.bw-1, ",", y+frm.by+frm.bh-1, ")");
1786     */
1787   }
1788   forEachObjectInRect(x+frm.bx, y+frm.by, frm.bw, frm.bh, &cbCollectObjectsWithMask);
1789   foreach (MapObject t; tmpObjectList) {
1790     if (!t || !t.isInstanceAlive || !t.visible || t.spectral) continue;
1791     /*
1792     auto tf = t.getSpriteFrame();
1793     if (!tf) {
1794       //writeln("no sprite frame for ", GetClassName(t.Class));
1795       continue;
1796     }
1797     */
1798     /*
1799     if (frm.pixelCheck(tf, t.ix-tf.xofs-x, t.iy-tf.yofs-y)) {
1800       //writeln("pixel hit for ", GetClassName(t.Class));
1801       if (dg(t)) break;
1802     }
1803     */
1804     if (t.isRectCollisionFrame(frm, x, y)) {
1805       if (dg(t)) break;
1806     }
1807   }
1811 // ////////////////////////////////////////////////////////////////////////// //
1812 final void destroyTileAt (int x, int y) {
1813   if (x < 0 || y < 0 || x >= tilesWidth*16 || y >= tilesHeight*16) return;
1814   x /= 16;
1815   y /= 16;
1816   MapTile t = tiles[x, y];
1817   if (!t || !t.visible || t.spectral || t.invincible || !t.isInstanceAlive) return;
1818   t.instanceRemove();
1819   t.onDestroy();
1820   delete tiles[x, y];
1821   checkWater = true;
1825 private array!MapTile tmpTileList;
1827 private final bool cbCollectTilesWithMask (MapTile t) {
1828   if (!t || !t.visible || t.spectral /*|| t.invincible*/ || !t.isInstanceAlive) return false;
1829   if (!t.sprite || t.sprite.frames.length < 1) return false;
1830   tmpTileList[$] = t;
1831   return false;
1834 final void touchTilesWithMask (int x, int y, SpriteFrame frm, bool delegate (MapTile t) dg) {
1835   if (!frm || frm.bw < 1 || frm.bh < 1 || !dg) return;
1836   if (frm.isEmptyPixelMask) return;
1837   // collect tiles
1838   if (tmpTileList.length) tmpTileList.length -= tmpTileList.length; // don't realloc
1839   checkTilesInRect(x+frm.bx, y+frm.by, frm.bw, frm.bh, &cbCollectTilesWithMask);
1840   foreach (MapTile t; tmpTileList) {
1841     if (!t || !t.isInstanceAlive || !t.visible || t.spectral) continue;
1842     /*
1843     auto tf = t.sprite.frames[0];
1844     if (frm.pixelCheck(tf, t.ix-x, t.iy-y)) {
1845       if (dg(t)) break;
1846       //doCleanup = doCleanup || !t.isInstanceAlive;
1847       //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, ")");
1848     }
1849     */
1850     if (t.isRectCollisionFrame(frm, x, y)) {
1851       if (dg(t)) break;
1852     }
1853   }
1857 // ////////////////////////////////////////////////////////////////////////// //
1858 final bool cbCollisionAnySolid (MapTile t) { return t.solid; }
1859 final bool cbCollisionAnyMoveableSolid (MapTile t) { return t.solid && t.moveable; }
1860 final bool cbCollisionLiquid (MapTile t) { return (t.water || t.lava); }
1861 final bool cbCollisionAnySolidOrLiquid (MapTile t) { return (t.solid || t.water || t.lava); }
1862 final bool cbCollisionLadder (MapTile t) { return t.ladder; }
1863 final bool cbCollisionLadderTop (MapTile t) { return t.laddertop; }
1864 final bool cbCollisionAnyLadder (MapTile t) { return (t.ladder || t.laddertop); }
1865 final bool cbCollisionAnySolidIce (MapTile t) { return (t.solid && t.ice); }
1866 final bool cbCollisionWater (MapTile t) { return t.water; }
1867 final bool cbCollisionLava (MapTile t) { return t.lava; }
1868 final bool cbCollisionAnyTree (MapTile t) { return t.tree; }
1869 final bool cbCollisionPlatform (MapTile t) { return t.platform; }
1870 final bool cbCollisionLadderOrPlatform (MapTile t) { return (t.ladder || t.platform); }
1871 //final bool cbCollisionAnyHangeable (MapTile t) { return t.hangeable; } // no need to check for solids here, it is done in MapTile initializer
1872 final bool cbCollisionSpringTrap (MapTile t) { return t.springtrap; }
1873 final bool cbCollisionSpikes (MapTile t) { return t.spikes; }
1874 final bool cbCollisionExitTile (MapTile t) { return t.exit; }
1876 final bool cbCollisionForWhoa (MapTile t) { return (t.solid || t.laddertop); }
1878 //final bool cbCollisionSacAltar (MapTile t) { return (t.objName == 'oSacAltarLeft'); } // only left part, yeah
1879 final bool cbCollisionSacAltar (MapTile t) { return t.sacrificingAltar; }
1882 // ////////////////////////////////////////////////////////////////////////// //
1883 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*/) {
1884   //!if (dbgdump) writeln("checkTilesInRect: (", x0, ",", y0, ")-(", x0+w-1, ",", y0+h-1, ") ; w=", w, "; h=", h);
1885   if (w < 1 || h < 1) return none;
1886   if (w == 1 && h == 1) return checkTileAtPoint(x0, y0, dg!optional);
1887   int x1 = x0+w-1, y1 = y0+h-1;
1888   if (x0 >= tilesWidth*16 || y0 >= tilesHeight*16 || x1 < 0 || y1 < 0) return none;
1889   if (!dg) {
1890     //!if (dbgdump) writeln("default checker set");
1891     dg = &cbCollisionAnySolid;
1892   }
1893   //!if (dbgdump) writeln("delegate: ", dg);
1894   int origx0 = x0, origy0 = y0;
1895   int tileSX = max(0, x0)/16;
1896   int tileSY = max(0, y0)/16;
1897   int tileEX = min(tilesWidth*16-1, x1)/16;
1898   int tileEY = min(tilesHeight*16-1, y1)/16;
1899   //!if (dbgdump) writeln("  tiles: (", tileSX, ",", tileSY, ")-(", tileEX, ",", tileEY, ")");
1900   auto grid = miscTileGrid;
1901   int tag = grid.nextTag();
1902   for (int ty = tileSY; ty <= tileEY; ++ty) {
1903     for (int tx = tileSX; tx <= tileEX; ++tx) {
1904       MapTile t = tiles[tx, ty];
1905       //!if (dbgdump && t && t.visible && t.isInstanceAlive && t.isRectCollision(origx0, origy0, w, h) ) writeln("   tile: ", GetClassName(t.Class), " : ", t.objName, " : ", t.objType, " : ", dg(t));
1906       if (t && !t.spectral && t.visible && t.isInstanceAlive && t.isRectCollision(origx0, origy0, w, h) && dg(t)) return t;
1907       // moveable tiles are in separate grid
1908       foreach (t; grid.inCellPix(tx*16, ty*16, tag, precise:precise)) {
1909         //if (t.isRectCollision(origx0, origy0, w, h) && dg(t)) return t;
1910         if (t.isInstanceAlive && !t.spectral && t.visible && t.isRectCollision(origx0, origy0, w, h) && dg(t)) return t;
1911       }
1912     }
1913   }
1914   return none;
1918 final MapTile checkTileAtPoint (int x0, int y0, optional bool delegate (MapTile dg) dg) {
1919   if (x0 < 0 || y0 < 0 || x0 >= tilesWidth*16 || y0 >= tilesHeight*16) return none;
1920   if (!dg) dg = &cbCollisionAnySolid;
1921   //if (!self) { writeln("WTF?!"); return none; }
1922   MapTile t = tiles[x0/16, y0/16];
1923   if (t && !t.spectral && t.visible && t.isInstanceAlive && t.isPointCollision(x0, y0) && dg(t)) return t;
1924   // moveable tiles are in separate grid
1925   auto grid = miscTileGrid;
1926   foreach (t; grid.inCellPix(x0, y0, grid.nextTag(), precise:true)) {
1927     if (t.isInstanceAlive && !t.spectral && t.visible && dg(t)) return t;
1928   }
1929   return none;
1933 //FIXME: optimize this with clipping first
1934 //TODO: moveable tiles
1936 final MapTile checkTilesAtLine (int ax0, int ay0, int ax1, int ay1, optional bool delegate (MapTile dg) dg) {
1937   // do it faster if we can
1939   // strict vertical check?
1940   if (ax0 == ax1 && ay0 <= ay1) return checkTilesInRect(ax0, ay0, 1, ay1-ay0+1, dg!optional);
1941   // strict horizontal check?
1942   if (ay0 == ay1 && ax0 <= ax1) return checkTilesInRect(ax0, ay0, ax1-ax0+1, 1, dg!optional);
1944   float x0 = float(ax0)/16.0, y0 = float(ay0)/16.0, x1 = float(ax1)/16.0, y1 = float(ay1)/16.0;
1946   // fix delegate
1947   if (!dg) dg = &cbCollisionAnySolid;
1949   // get starting and enging tile
1950   int tileSX = trunc(x0), tileSY = trunc(y0);
1951   int tileEX = trunc(x1), tileEY = trunc(y1);
1953   // first hit is always landed
1954   if (tileSX >= 0 && tileSY >= 0 && tileSX < TilesWidth && tileSY < TilesHeight) {
1955     MapTile t = tiles[tileSX, tileSY];
1956     if (t && t.visible && Geom.lineAABBIntersects(ax0, ay0, ax1, ay1, tileSX*16, tileSY*16, 16, 16) && dg(t)) return t;
1957   }
1959   // if starting and ending tile is the same, we don't need to do anything more
1960   if (tileSX == tileEX && tileSY == tileEY) return none;
1962   // calculate ray direction
1963   TVec dv = (vector(x1, y1)-vector(x0, y0)).normalise2d;
1965   // length of ray from one x or y-side to next x or y-side
1966   float deltaDistX = (fabs(dv.x) > 0.0001 ? fabs(1.0/dv.x) : 0.0);
1967   float deltaDistY = (fabs(dv.y) > 0.0001 ? fabs(1.0/dv.y) : 0.0);
1969   // calculate step and initial sideDists
1971   float sideDistX; // length of ray from current position to next x-side
1972   int stepX; // what direction to step in x (either +1 or -1)
1973   if (dv.x < 0) {
1974     stepX = -1;
1975     sideDistX = (x0-tileSX)*deltaDistX;
1976   } else {
1977     stepX = 1;
1978     sideDistX = (tileSX+1.0-x0)*deltaDistX;
1979   }
1981   float sideDistY; // length of ray from current position to next y-side
1982   int stepY; // what direction to step in y (either +1 or -1)
1983   if (dv.y < 0) {
1984     stepY = -1;
1985     sideDistY = (y0-tileSY)*deltaDistY;
1986   } else {
1987     stepY = 1;
1988     sideDistY = (tileSY+1.0-y0)*deltaDistY;
1989   }
1991   // perform DDA
1992   //int side; // was a NS or a EW wall hit?
1993   for (;;) {
1994     // jump to next map square, either in x-direction, or in y-direction
1995     if (sideDistX < sideDistY) {
1996       sideDistX += deltaDistX;
1997       tileSX += stepX;
1998       //side = 0;
1999     } else {
2000       sideDistY += deltaDistY;
2001       tileSY += stepY;
2002       //side = 1;
2003     }
2004     // check tile
2005     if (tileSX >= 0 && tileSY >= 0 && tileSX < TilesWidth && tileSY < TilesHeight) {
2006       MapTile t = tiles[tileSX, tileSY];
2007       if (t && t.visible && Geom.lineAABBIntersects(ax0, ay0, ax1, ay1, tileSX*16, tileSY*16, 16, 16) && dg(t)) return t;
2008     }
2009     // did we arrived at the destination?
2010     if (tileSX == tileEX && tileSY == tileEY) break;
2011   }
2013   return none;
2018 // ////////////////////////////////////////////////////////////////////////// //
2019 final MapTile isSolidAtPoint (int px, int py) { return checkTileAtPoint(px, py); }
2020 final MapTile isSolidInRect (int rx, int ry, int rw, int rh) { return checkTilesInRect(rx, ry, rw, rh); }
2021 final MapTile isLadderAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadder); }
2022 final MapTile isLadderOrPlatformAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadderOrPlatform); }
2023 final MapTile isLadderTopAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadderTop); }
2024 final MapTile isAnyLadderAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyLadder); }
2025 final MapTile isWaterAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionWater); }
2026 final MapTile isLavaAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLava); }
2027 final MapTile isLiquidAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLiquid); }
2028 final MapTile isIceAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnySolidIce); }
2029 final MapTile isTreeAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyTree); }
2030 //final MapTile isHangeableAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyHangeable); }
2033 // ////////////////////////////////////////////////////////////////////////// //
2034 // PlayerPawn has it's own movement code, so don't process it here
2035 // but process moveable solids here, yeah
2036 final void physStep () {
2037   // advance time
2038   time += 1;
2039   // we don't want the time to grow too large
2040   if (time > 100000000) time = 0;
2042   auto grid = miscTileGrid;
2044   // process gravity for moveable solids and burning for ropes
2045   int cid = grid.getFirstObject();
2046   while (cid) {
2047     MapTile t = grid.getObject(MapTile, cid);
2048     if (!t) continue;
2049     if (t.isInstanceAlive) {
2050       t.saveInterpData();
2051       t.processAlarms();
2052       if (t.isInstanceAlive) {
2053         grid.update(cid, markAsDead:false);
2054         t.thinkFrame();
2055         if (t.isInstanceAlive && !t.canLiveOutsideOfLevel && t.isOutsideOfLevel()) t.instanceRemove();
2056         grid.update(cid, markAsDead:false);
2057       }
2058     }
2059     if (t.isInstanceAlive) {
2060       cid = grid.getNextObject(cid, removeThis:false);
2061     } else {
2062       cid = grid.getNextObject(cid, removeThis:true);
2063       t.instanceRemove(); // just in case
2064       t.onDestroy();
2065       delete t;
2066       checkWater = true;
2067     }
2068   }
2072 // ////////////////////////////////////////////////////////////////////////// //
2073 final void setTileTypeAt (int tileX, int tileY, LevelGen::RType rt) {
2074   if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) roomType[tileX, tileY] = rt;
2077 final MapTile getTileAt (int tileX, int tileY) {
2078   return (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight ? tiles[tileX, tileY] : none);
2081 final bool isNamedTileAt (int tileX, int tileY, name atypename) {
2082   if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
2083     auto t = tiles[tileX, tileY];
2084     if (t && t.objName == atypename) return true;
2085   }
2086   return false;
2089 final void setTileAt (int tileX, int tileY, MapTile tile) {
2090   if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
2091     //FIXME
2092     if (tiles[tileX, tileY]) checkWater = true;
2093     delete tiles[tileX, tileY];
2094     tiles[tileX, tileY] = tile;
2095   }
2099 // ////////////////////////////////////////////////////////////////////////// //
2100 // return `true` from delegate to stop
2101 MapTile forEachSolidTile (bool delegate (int x, int y, MapTile t) dg) {
2102   if (!dg) return none;
2103   foreach (int y; 0..tilesHeight) {
2104     foreach (int x; 0..tilesWidth) {
2105       auto t = tiles[x, y];
2106       if (t && t.solid && t.visible && t.isInstanceAlive) {
2107         if (dg(x, y, t)) return t;
2108       }
2109     }
2110   }
2111   return none;
2115 // ////////////////////////////////////////////////////////////////////////// //
2116 // return `true` from delegate to stop
2117 MapTile forEachNormalTile (bool delegate (int x, int y, MapTile t) dg) {
2118   if (!dg) return none;
2119   foreach (int y; 0..tilesHeight) {
2120     foreach (int x; 0..tilesWidth) {
2121       auto t = tiles[x, y];
2122       if (t && t.visible && t.isInstanceAlive) {
2123         if (dg(x, y, t)) return t;
2124       }
2125     }
2126   }
2127   return none;
2131 // WARNING! don't destroy tiles here! (instanceRemove() is ok, tho)
2132 MapTile forEachTile (bool delegate (MapTile t) dg) {
2133   if (!dg) return none;
2134   foreach (int y; 0..tilesHeight) {
2135     foreach (int x; 0..tilesWidth) {
2136       auto t = tiles[x, y];
2137       if (t && t.visible && !t.spectral && t.isInstanceAlive) {
2138         if (dg(t)) return t;
2139       }
2140     }
2141   }
2142   foreach (MapObject o; miscTileGrid.allObjects()) {
2143     auto mt = MapTile(o);
2144     if (!mt) continue;
2145     if (mt.visible && !mt.spectral && mt.isInstanceAlive) {
2146       //writeln("special map tile: '", GetClassName(mt.Class), "'");
2147       if (dg(mt)) return mt;
2148     }
2149   }
2150   return none;
2154 // ////////////////////////////////////////////////////////////////////////// //
2155 final void fixWallTiles () {
2156   foreach (int y; 0..tilesHeight) {
2157     foreach (int x; 0..tilesWidth) {
2158       auto t = getTileAt(x, y);
2159       if (!t) continue;
2160       /*
2161       if (y == tilesHeight-2) {
2162         writeln("0: tx=", x, "; tile=", t.objName, "; ty=", y, ": (", t.iy/16, ")");
2163       } else if (y == tilesHeight-1) {
2164         writeln("1: tx=", x, "; tile=", t.objName, "; ty=", y, ": (", t.iy/16, ")");
2165       }
2166       */
2167       t.beautifyTile();
2168     }
2169   }
2170   foreach (MapTile t; miscTileGrid.allObjects()) {
2171     if (t.isInstanceAlive) t.beautifyTile();
2172   }
2176 // ////////////////////////////////////////////////////////////////////////// //
2177 final MapTile isCollisionAtPoint (int px, int py, optional bool delegate (MapTile dg) dg) {
2178   if (!dg) dg = &cbCollisionAnySolid;
2179   return checkTilesInRect(px, py, 1, 1, dg);
2183 // ////////////////////////////////////////////////////////////////////////// //
2184 string scrGetKaliGift (MapTile altar, optional name gift) {
2185   string res;
2187   // find other side of the altar
2188   int sx = player.ix, sy = player.iy;
2189   if (altar) {
2190     sx = altar.ix;
2191     sy = altar.iy;
2192     auto a2 = isCollisionAtPoint(altar.ix+18, altar.iy+8, &cbCollisionSacAltar);
2193     if (!a2) a2 = isCollisionAtPoint(altar.ix-8, altar.iy+8, &cbCollisionSacAltar);
2194     if (a2) { sx = a2.ix; sy = a2.iy; }
2195   }
2197        if (global.favor <= -8) res = "SHE SEEMS VERY ANGRY WITH YOU!";
2198   else if (global.favor < 0) res = "SHE SEEMS ANGRY WITH YOU.";
2199   else if (global.favor == 0) res = "SHE HAS FORGIVEN YOU!";
2200   else if (global.favor >= 32) {
2201     if (global.kaliGift >= 3 && global.favor >= 32+(global.kaliGift-2)*16) {
2202       res = "YOU FEEL INVIGORATED!";
2203       global.kaliGift += 1;
2204       global.plife += global.randOther(4, 8);
2205     } else if (global.kaliGift >= 3) {
2206       res = "SHE SEEMS ECSTATIC WITH YOU!";
2207     } else if (global.bombs < 80) {
2208       res = "YOUR SATCHEL FEELS VERY FULL NOW!";
2209       global.kaliGift = 3;
2210       global.bombs = 99;
2211     } else {
2212       res = "YOU FEEL INVIGORATED!";
2213       global.kaliGift += 1;
2214       global.plife += global.randOther(4, 8);
2215     }
2216   } else if (global.favor >= 16) {
2217     if (global.kaliGift >= 2) {
2218       res = "SHE SEEMS VERY HAPPY WITH YOU!";
2219     } else {
2220       res = "SHE BESTOWS A GIFT UPON YOU!";
2221       global.kaliGift = 2;
2222       // poofs
2223       auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2224       obj.xVel = -1;
2225       obj.yVel = 0;
2226       obj = MakeMapObject(sx, sy-8, 'oPoof');
2227       obj.xVel = 1;
2228       obj.yVel = 0;
2229       // a gift
2230       obj = none;
2231       if (gift) obj = MakeMapObject(sx, sy-8, gift);
2232       if (!obj) obj = MakeMapObject(sx, sy-8, 'oKapala');
2233     }
2234   } else if (global.favor >= 8) {
2235     if (global.kaliGift >= 1) {
2236       res = "SHE SEEMS HAPPY WITH YOU.";
2237     } else {
2238       res = "SHE BESTOWS A GIFT UPON YOU!";
2239       global.kaliGift = 1;
2240       //rAltar = instance_nearest(x, y, oSacAltarRight);
2241       //if (instance_exists(rAltar)) {
2242       // poofs
2243       auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2244       obj.xVel = -1;
2245       obj.yVel = 0;
2246       obj = MakeMapObject(sx, sy-8, 'oPoof');
2247       obj.xVel = 1;
2248       obj.yVel = 0;
2249       obj = none;
2250       if (gift) obj = MakeMapObject(sx, sy-8, gift);
2251       if (!obj) {
2252         auto n = global.randOther(1, 8);
2253         auto m = n;
2254         for (;;) {
2255           name aname = '';
2256                if (n == 1 && !global.hasCape && !global.hasJetpack) aname = 'oCapePickup';
2257           else if (n == 2 && !global.hasGloves) aname = 'oGloves';
2258           else if (n == 3 && !global.hasSpectacles) aname = 'oSpectacles';
2259           else if (n == 4 && !global.hasMitt) aname = 'oMitt';
2260           else if (n == 5 && !global.hasSpringShoes) aname = 'oSpringShoes';
2261           else if (n == 6 && !global.hasSpikeShoes) aname = 'oSpikeShoes';
2262           else if (n == 7 && !global.hasStickyBombs) aname = 'oPaste';
2263           else if (n == 8 && !global.hasCompass) aname = 'oCompass';
2264           if (aname) {
2265             obj = MakeMapObject(sx, sy-8, aname);
2266             if (obj) break;
2267           }
2268           ++n;
2269           if (n > 8) n = 1;
2270           if (n == m) {
2271             obj = MakeMapObject(sx, sy-8, (global.hasJetpack ? 'oBombBox' : 'oJetpack'));
2272             break;
2273           }
2274         }
2275       }
2276     }
2277   } else if (global.favor > 0) {
2278     res = "SHE SEEMS PLEASED WITH YOU.";
2279   }
2281   /*
2282   if (argument1) {
2283     global.message = "";
2284     res = "KALI DEVOURS YOU!"; // sacrifice is player
2285   }
2286   */
2288   return res;
2292 void performSacrifice (MapObject what, MapTile where) {
2293   if (!what || !what.isInstanceAlive) return;
2294   MakeMapObject(what.ix+8, what.iy+8, 'oFlame');
2295   if (where) where.playSound('sndSmallExplode'); else what.playSound('sndSmallExplode');
2296   if (!what.bloodless) what.scrCreateBlood(what.ix+8, what.iy+8, 3);
2298   string msg = "KALI ACCEPTS THE SACRIFICE!";
2300   auto idol = ItemGoldIdol(what);
2301   if (idol) {
2302     ++stats.totalSacrifices;
2303          if (global.favor <= -8) msg = "SHE SEEMS VERY ANGRY WITH YOU!";
2304     else if (global.favor < 0) msg = "SHE SEEMS ANGRY WITH YOU.";
2305     else if (global.favor >= 0) {
2306       // find other side of the altar
2307       int sx = player.ix, sy = player.iy;
2308       auto altar = where;
2309       if (altar) {
2310         sx = altar.ix;
2311         sy = altar.iy;
2312         auto a2 = isCollisionAtPoint(altar.ix+18, altar.iy+8, &cbCollisionSacAltar);
2313         if (!a2) a2 = isCollisionAtPoint(altar.ix-8, altar.iy+8, &cbCollisionSacAltar);
2314         if (a2) { sx = a2.ix; sy = a2.iy; }
2315       }
2316       // poofs
2317       auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2318       obj.xVel = -1;
2319       obj.yVel = 0;
2320       obj = MakeMapObject(sx, sy-8, 'oPoof');
2321       obj.xVel = 1;
2322       obj.yVel = 0;
2323       // a gift
2324       obj = MakeMapObject(sx, sy-16-8, 'oMonkeyGold');
2325     }
2326     osdMessage(msg, 6.66);
2327     scrShake(10);
2328     idol.instanceRemove();
2329     return;
2330   }
2332   if (global.favor <= -8) {
2333     msg = "KALI DEVOURS THE SACRIFICE!";
2334   } else if (global.favor < 0) {
2335     global.favor += (what.status == MapObject::STUNNED ? what.favor : round(what.favor/2.0)); //k8: added `round()`
2336     if (what.favor > 0) what.favor = 0;
2337   } else {
2338     global.favor += (what.status == MapObject::STUNNED ? what.favor : round(what.favor/2.0)); //k8: added `round()`
2339   }
2341   /*!!
2342        if (myAltar.gift1 != "" and global.kaliGift == 0) scrGetKaliGift(myAltar.gift1);
2343   else if (myAltar.gift2 != "" and global.kaliGift == 1) scrGetKaliGift(myAltar.gift2);
2344   else scrGetKaliGift("");
2345   */
2347   // sacrifice is player?
2348   if (what isa PlayerPawn) {
2349     ++stats.totalSelfSacrifices;
2350     msg = "KALI DEVOURS YOU!";
2351     player.visible = false;
2352     player.dead = true;
2353     player.status = MapObject::DEAD;
2354   } else {
2355     ++stats.totalSacrifices;
2356     auto msg2 = scrGetKaliGift(where);
2357     what.instanceRemove();
2358     if (msg2) msg = va("%s\n%s", msg, msg2);
2359   }
2361   osdMessage(msg, 6.66);
2363   //!if (isRealLevel()) global.totalSacrifices += 1;
2365   //!global.messageTimer = 200;
2366   //!global.shake = 10;
2367   scrShake(10);
2369   /*damsel
2370   instance_create(x, y, oFlame);
2371   playSound(global.sndSmallExplode);
2372   scrCreateBlood(x, y, 3);
2373   global.message = "KALI ACCEPTS YOUR SACRIFICE!";
2374   if (global.favor <= -8) {
2375     global.message = "KALI DEVOURS YOUR SACRIFICE!";
2376   } else if (global.favor < 0) {
2377     if (status == 98) global.favor += favor*1.5; else global.favor += favor;
2378     if (favor > 0) favor = 0;
2379   } else {
2380     if (status == 98) global.favor += favor*1.5; else global.favor += favor;
2381   }
2383        if (myAltar.gift1 != "" and global.kaliGift == 0) scrGetFavorMsg(myAltar.gift1);
2384   else if (myAltar.gift2 != "" and global.kaliGift == 1) scrGetFavorMsg(myAltar.gift2);
2385   else scrGetFavorMsg("");
2387   global.messageTimer = 200;
2388   global.shake = 10;
2389   instance_destroy();
2390   */
2394 // ////////////////////////////////////////////////////////////////////////// //
2395 final void addBackgroundGfxDetails () {
2396   // add background details
2397   //if (global.customLevel || global.parallax) return;
2398   foreach (; 0..20) {
2399     // bg = instance_create(16*randRoom(1, 42), 16*randRoom(1, 33), oCaveBG);
2400          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);
2401     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);
2402     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);
2403     else MakeMapBackTile('bgExtras', 32*global.randRoom(0, 1), 0, 32, 32, 16*global.randRoom(1, 42), 16*global.randRoom(1, 33), 10002);
2404   }
2408 // ////////////////////////////////////////////////////////////////////////// //
2409 private final void fixRealViewStart () {
2410   int scale = global.scale;
2411   realViewStart.x = clamp(realViewStart.x, viewMin.x*scale, viewMax.x*scale-viewWidth);
2412   realViewStart.y = clamp(realViewStart.y, viewMin.y*scale, viewMax.y*scale-viewHeight);
2416 private final void fixViewStart () {
2417   int scale = global.scale;
2418   viewStart.x = clamp(viewStart.x, viewMin.x*scale, viewMax.x*scale-viewWidth);
2419   viewStart.y = clamp(viewStart.y, viewMin.y*scale, viewMax.y*scale-viewHeight);
2423 final void centerViewAtPlayer () {
2424   if (viewWidth < 1 || viewHeight < 1 || !player) return;
2425   centerViewAt(player.xCenter, player.yCenter);
2429 final void centerViewAt (int x, int y) {
2430   if (viewWidth < 1 || viewHeight < 1) return;
2431   int scale = global.scale;
2432   x *= scale;
2433   y *= scale;
2434   realViewStart.x = clamp(x-viewWidth/2, 0, tilesWidth*16*scale-viewWidth);
2435   realViewStart.y = clamp(y-viewHeight/2, 0, tilesHeight*16*scale-viewHeight);
2436   fixRealViewStart();
2438   viewStart.x = realViewStart.x;
2439   viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
2440   fixViewStart();
2444 const int ViewPortToleranceX = 16*1+8;
2445 const int ViewPortToleranceY = 16*1+8;
2447 final void fixCamera () {
2448   if (!player) return;
2449   if (viewWidth < 1 || viewHeight < 1) return;
2450   int scale = global.scale;
2451   auto alwaysCenter = global.config.alwaysCenterPlayer;
2452   // calculate offset from viewport center (in game units), and fix viewport
2453   // horizontal
2454   if (!player.cameraBlockX) {
2455     int x = player.xCenter*scale;
2456     int cx = realViewStart.x;
2457     if (alwaysCenter) {
2458       cx = x-viewWidth/2;
2459     } else {
2460       int xofs = x-(cx+viewWidth/2);
2461            if (xofs < -ViewPortToleranceX*scale) cx += xofs+ViewPortToleranceX*scale;
2462       else if (xofs > ViewPortToleranceX*scale) cx += xofs-ViewPortToleranceX*scale;
2463     }
2464     realViewStart.x = clamp(cx, 0, tilesWidth*16*scale-viewWidth);
2465   }
2466   // vertical
2467   if (!player.cameraBlockY) {
2468     int y = player.yCenter*scale;
2469     int cy = realViewStart.y;
2470     if (alwaysCenter) {
2471       cy = y-viewHeight/2;
2472     } else {
2473       int yofs = y-(cy+viewHeight/2);
2474            if (yofs < -ViewPortToleranceY*scale) cy += yofs+ViewPortToleranceY*scale;
2475       else if (yofs > ViewPortToleranceY*scale) cy += yofs-ViewPortToleranceY*scale;
2476     }
2477     realViewStart.y = clamp(cy, 0, tilesHeight*16*scale-viewHeight);
2478   }
2479   fixRealViewStart();
2481   viewStart.x = realViewStart.x;
2482   viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
2483   fixViewStart();
2487 // ////////////////////////////////////////////////////////////////////////// //
2488 // x0 and y0 are non-scaled (and will be scaled)
2489 final void drawSpriteAt (name sprName, float frnumf, int x0, int y0, optional bool small) {
2490   if (!sprName) return;
2491   auto spr = sprStore[sprName];
2492   if (!spr || !spr.frames.length) return;
2493   int scale = global.scale;
2494   x0 *= scale;
2495   y0 *= scale;
2496   int frnum = max(0, trunc(frnumf))%spr.frames.length;
2497   auto sfr = spr.frames[frnum];
2498   int sx0 = x0-sfr.xofs*scale;
2499   int sy0 = y0-sfr.yofs*scale;
2500   if (small && scale > 1) {
2501     sfr.tex.blitExt(sx0, sy0, round(sx0+sfr.tex.width*(scale/2.0)), round(sy0+sfr.tex.height*(scale/2.0)), 0, 0);
2502   } else {
2503     sfr.tex.blitAt(sx0, sy0, scale);
2504   }
2508 // x0 and y0 are non-scaled (and will be scaled)
2509 final void drawTextAt (int x0, int y0, string text) {
2510   if (!text) return;
2511   int scale = global.scale;
2512   x0 *= scale;
2513   y0 *= scale;
2514   sprStore.renderText(x0, y0, text, scale);
2518 void renderCompass (float currFrameDelta) {
2519   if (!global.hasCompass) return;
2521   /*
2522   if (isRoom("rOlmec")) {
2523     global.exitX = 648;
2524     global.exitY = 552;
2525   } else if (isRoom("rOlmec2")) {
2526     global.exitX = 648;
2527     global.exitY = 424;
2528   }
2529   */
2531   bool hasMessage = osdHasMessage();
2532   foreach (MapTile et; allExits) {
2533     // original compass
2534     int exitX = et.ix, exitY = et.iy;
2535     int vx0 = viewStart.x/global.scale, vy0 = viewStart.y/global.scale;
2536     int vx1 = (viewStart.x+viewWidth)/global.scale;
2537     int vy1 = (viewStart.y+viewHeight)/global.scale;
2538     if (exitY > vy1-16) {
2539       if (exitX < vx0) {
2540         drawSpriteAt(hasMessage ? 'sCompassSmallLL' : 'sCompassLL', 0, 0, 224);
2541       } else if (exitX > vx1-16) {
2542         drawSpriteAt(hasMessage ? 'sCompassSmallLR' : 'sCompassLR', 0, 304, 224);
2543       } else {
2544         drawSpriteAt(hasMessage ? 'sCompassSmallDown' : 'sCompassDown', 0, exitX-vx0, 224);
2545       }
2546     } else if (exitX < vx0) {
2547       drawSpriteAt(hasMessage ? 'sCompassSmallLeft' : 'sCompassLeft', 0, 0, exitY-vy0);
2548     } else if (exitX > vx1-16) {
2549       drawSpriteAt(hasMessage ? 'sCompassSmallRight' : 'sCompassRight', 0, 304, exitY-vy0);
2550     }
2551   }
2555 final bool totalsNameCmpCB (ref GameStats::TotalItem a, ref GameStats::TotalItem b) {
2556   auto sa = string(a.objName);
2557   auto sb = string(b.objName);
2558   return (sa < sb);
2561 void renderTransitionInfo (float currFrameDelta) {
2562   //FIXME!
2563   /*
2564   GameStats.sortTotalsList(stats.kills, &totalsNameCmpCB);
2566   int maxLen = 0;
2567   foreach (int idx, ref auto k; stats.kills) {
2568     string s = string(k);
2569     maxLen = max(maxLen, s.length);
2570   }
2571   maxLen *= 8;
2573   sprStore.loadFont('sFontSmall');
2574   Video.color = 0xff_ff_00;
2575   foreach (int idx, ref auto k; stats.kills) {
2576     int deaths = 0;
2577     foreach (int xidx, ref auto d; stats.totalKills) {
2578       if (d.objName == k) { deaths = d.count; break; }
2579     }
2580     //drawTextAt(16, 4+idx*8, va("%s: %d (%d)", string(k.objName).toUpperCase, k.count, deaths));
2581     drawTextAt(16, 4+idx*8, string(k).toUpperCase);
2582     drawTextAt(16+maxLen, 4+idx*8, va(": %d (%d)", k.count, deaths));
2583   }
2584   */
2588 void renderGhostTimer (float currFrameDelta) {
2589   if (ghostTimeLeft <= 0) return;
2590   //ghostTimeLeft /= 30; // frames -> seconds
2592   int hgt = Video.screenHeight-64;
2593   if (hgt < 1) return;
2594   int rhgt = round(float(hgt)*(float(ghostTimeLeft)/30.0)/1000.0);
2595   //writeln("hgt=", hgt, "; rhgt=", rhgt, "; gtl=", ghostTimeLeft/30);
2596   if (rhgt > 0) {
2597     auto oclr = Video.color;
2598     Video.color = 0xcf_ff_7f_00;
2599     Video.fillRect(Video.screenWidth-20, 32, 16, hgt-rhgt);
2600     Video.color = 0x7f_ff_7f_00;
2601     Video.fillRect(Video.screenWidth-20, 32+(hgt-rhgt), 16, rhgt);
2602     Video.color = oclr;
2603   }
2607 void renderHUD (float currFrameDelta) {
2608   if (levelKind == LevelKind.Transition) { renderTransitionInfo(currFrameDelta); return; }
2610   int lifeX = 4; // 8
2611   int bombX = 56;
2612   int ropeX = 104;
2613   int ammoX = 152;
2614   int moneyX = 200;
2615   int hhup;
2616   bool scumSmallHud = global.config.scumSmallHud;
2617   if (!global.config.optSGAmmo) moneyX = ammoX;
2619   if (scumSmallHud) {
2620     sprStore.loadFont('sFontSmall');
2621     hhup = 6;
2622   } else {
2623     sprStore.loadFont('sFont');
2624     hhup = 0;
2625   }
2626   Video.color = 0xff_ff_ff;
2628   // hearts
2629   if (scumSmallHud) {
2630     if (global.plife == 1) {
2631       drawSpriteAt('sHeartSmallBlink', global.heartBlink, lifeX+2, 4-hhup);
2632       global.heartBlink += 0.1;
2633       if (global.heartBlink > 3) global.heartBlink = 0;
2634     } else {
2635       drawSpriteAt('sHeartSmall', -1, lifeX+2, 4-hhup);
2636       global.heartBlink = 0;
2637     }
2638   } else {
2639     if (global.plife == 1) {
2640       drawSpriteAt('sHeartBlink', global.heartBlink, lifeX, 8-hhup);
2641       global.heartBlink += 0.1;
2642       if (global.heartBlink > 3) global.heartBlink = 0;
2643     } else {
2644       drawSpriteAt('sHeart', -1, lifeX, 8-hhup);
2645       global.heartBlink = 0;
2646     }
2647   }
2649   int life = clamp(global.plife, 0, 99);
2650   //if (!scumHud && life > 99) life = 99;
2651   drawTextAt(lifeX+16, 8-hhup, va("%d", life));
2653   // bombs
2654   if (global.hasStickyBombs && global.stickyBombsActive) {
2655     if (scumSmallHud) drawSpriteAt('sStickyBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sStickyBombIcon', -1, bombX, 8-hhup);
2656   } else {
2657     if (scumSmallHud) drawSpriteAt('sBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sBombIcon', -1, bombX, 8-hhup);
2658   }
2659   int n = global.bombs;
2660   if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
2661   drawTextAt(bombX+16, 8-hhup, va("%d", n));
2663   // ropes
2664   if (scumSmallHud) drawSpriteAt('sRopeIconSmall', -1, ropeX+2, 4-hhup); else drawSpriteAt('sRopeIcon', -1, ropeX, 8-hhup);
2665   n = global.rope;
2666   if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
2667   drawTextAt(ropeX+16, 8-hhup, va("%d", n));
2669   // shotgun shells
2670   if (global.config.optSGAmmo) {
2671     if (scumSmallHud) drawSpriteAt('sShellsIcon', -1, ammoX+6, 8-hhup); else drawSpriteAt('sShellsIcon', -1, ammoX+7, 12-hhup);
2672     n = global.sgammo;
2673     if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
2674     drawTextAt(ammoX+16, 8-hhup, va("%d", n));
2675   }
2677   // money
2678   if (scumSmallHud) drawSpriteAt('sDollarSignSmall', -1, moneyX+2, 3-hhup); else drawSpriteAt('sDollarSign', -1, moneyX, 8-hhup);
2679   drawTextAt(moneyX+16, 8-hhup, va("%d", stats.money-xmoney));
2681   int ity = (scumSmallHud ? 18-hhup : 24-hhup);
2683   n = 8; //28;
2684   if (global.hasUdjatEye) {
2685     if (global.udjatBlink) drawSpriteAt('sUdjatEyeIcon2', -1, n, ity); else drawSpriteAt('sUdjatEyeIcon', -1, n, ity);
2686     n += 20;
2687   }
2688   if (global.hasAnkh) { drawSpriteAt('sAnkhIcon', -1, n, ity); n += 20; }
2689   if (global.hasCrown) { drawSpriteAt('sCrownIcon', -1, n, ity); n += 20; }
2690   if (global.hasKapala) {
2691          if (global.bloodLevel == 0) drawSpriteAt('sKapalaIcon', 0, n, ity);
2692     else if (global.bloodLevel <= 2) drawSpriteAt('sKapalaIcon', 1, n, ity);
2693     else if (global.bloodLevel <= 4) drawSpriteAt('sKapalaIcon', 2, n, ity);
2694     else if (global.bloodLevel <= 6) drawSpriteAt('sKapalaIcon', 3, n, ity);
2695     else if (global.bloodLevel <= 8) drawSpriteAt('sKapalaIcon', 4, n, ity);
2696     n += 20;
2697   }
2698   if (global.hasSpectacles) { drawSpriteAt('sSpectaclesIcon', -1, n, ity); n += 20; }
2699   if (global.hasGloves) { drawSpriteAt('sGlovesIcon', -1, n, ity); n += 20; }
2700   if (global.hasMitt) { drawSpriteAt('sMittIcon', -1, n, ity); n += 20; }
2701   if (global.hasSpringShoes) { drawSpriteAt('sSpringShoesIcon', -1, n, ity); n += 20; }
2702   if (global.hasJordans) { drawSpriteAt('sJordansIcon', -1, n, ity); n += 20; }
2703   if (global.hasSpikeShoes) { drawSpriteAt('sSpikeShoesIcon', -1, n, ity); n += 20; }
2704   if (global.hasCape) { drawSpriteAt('sCapeIcon', -1, n, ity); n += 20; }
2705   if (global.hasJetpack) { drawSpriteAt('sJetpackIcon', -1, n, ity); n += 20; }
2706   if (global.hasStickyBombs) { drawSpriteAt('sPasteIcon', -1, n, ity); n += 20; }
2707   if (global.hasCompass) { drawSpriteAt('sCompassIcon', -1, n, ity); n += 20; }
2708   if (global.hasParachute) { drawSpriteAt('sParachuteIcon', -1, n, ity); n += 20; }
2710   if (player.holdItem && player.holdItem.objName == 'Bow' && global.arrows > 0) {
2711     int m = 1;
2712     float malpha = 1;
2713     while (m <= global.arrows && m <= 20 && malpha > 0) {
2714       Video.color = trunc(malpha*255)<<24|0xff_ff_ff;
2715       drawSpriteAt('sArrowIcon', -1, n, ity);
2716       n += 4;
2717       if (m > 10) malpha -= 0.1; //and global.arrows > 20 malpha -= 0.1
2718       m += 1;
2719     }
2720     Video.color = 0xff_ff_ff;
2721   }
2723   if (xmoney > 0) {
2724     sprStore.loadFont('sFontSmall');
2725     Video.color = 0xff_ff_00;
2726     if (scumSmallHud) drawTextAt(moneyX+8, 8+8-hhup, va("+%d", xmoney));
2727     else drawTextAt(moneyX, 8+16-hhup, va("+%d", xmoney));
2728   }
2730   if (global.config.ghostShowTime) renderGhostTimer(currFrameDelta);
2734 // ////////////////////////////////////////////////////////////////////////// //
2735 private transient array!MapEntity renderVisibleCids;
2736 private transient array!MapTile renderMidTiles, renderFrontTiles; // normal, with fg
2738 final bool renderSortByDepth (MapEntity oa, MapEntity ob) {
2739   //MapObject oa = MapObject(a);
2740   //MapObject ob = MapObject(b);
2741   auto da = oa.depth, db = ob.depth;
2742   if (da == db) return (oa.objId < ob.objId);
2743   return (da < db);
2747 const int RenderEdgePix = 32;
2749 void renderWithOfs (int xofs, int yofs, float currFrameDelta) {
2750   int scale = global.scale;
2751   int tsz = 16*scale;
2753   Video.color = 0xff_ff_ff;
2755   // render cave background
2756   if (levBGImg) {
2757     int bgw = levBGImg.tex.width*scale;
2758     int bgh = levBGImg.tex.height*scale;
2759     int bgTW = (tilesWidth*tsz+bgw-1)/bgw;
2760     int bgTH = (tilesHeight*tsz+bgh-1)/bgh;
2761     int bgX0 = max(0, xofs/bgw);
2762     int bgY0 = max(0, yofs/bgh);
2763     int bgX1 = min(bgTW, (xofs+viewWidth+bgw-1)/bgw);
2764     int bgY1 = min(bgTH, (yofs+viewHeight+bgh-1)/bgh);
2765     foreach (int ty; bgY0..bgY1) {
2766       foreach (int tx; bgX0..bgX1) {
2767         int x0 = tx*bgw-xofs;
2768         int y0 = ty*bgh-yofs;
2769         levBGImg.tex.blitAt(x0, y0, scale);
2770       }
2771     }
2772   }
2774   // render background tiles
2775   for (MapBackTile bt = backtiles; bt; bt = bt.next) {
2776     bt.drawWithOfs(xofs, yofs, scale, currFrameDelta);
2777   }
2779   // render stationary tiles
2780   int tileX0 = max(0, xofs/tsz);
2781   int tileY0 = max(0, yofs/tsz);
2782   int tileX1 = min(tilesWidth, (xofs+viewWidth+tsz-1)/tsz);
2783   int tileY1 = min(tilesHeight, (yofs+viewHeight+tsz-1)/tsz);
2785   // render backs; collect tile arrays
2786   renderMidTiles.length -= renderMidTiles.length; // don't realloc
2787   renderFrontTiles.length -= renderFrontTiles.length; // don't realloc
2789   foreach (int ty; tileY0..tileY1) {
2790     foreach (int tx; tileX0..tileX1) {
2791       auto tile = getTileAt(tx, ty);
2792       if (tile && tile.visible && tile.isInstanceAlive) {
2793         renderMidTiles[$] = tile;
2794         if (tile.bgfront) renderFrontTiles[$] = tile;
2795         if (tile.bgback) tile.drawWithOfsBack(xofs, yofs, scale, currFrameDelta);
2796       }
2797     }
2798   }
2800   // render "mid" (i.e. normal) tiles
2801   foreach (MapTile tile; renderMidTiles) tile.drawWithOfs(xofs, yofs, scale, currFrameDelta);
2803   // render moveable tiles
2804   /*
2805   foreach (MapTile mt; miscTileGrid.allObjects()) {
2806     if (mt.visible && mt.isInstanceAlive) {
2807       Video.color = (mt.moveable ? 0xff_7f_00 : 0xff_ff_ff);
2808       mt.drawWithOfs(xofs, yofs, scale, currFrameDelta);
2809     }
2810   }
2811   */
2812   renderVisibleCids.length -= renderVisibleCids.length;
2814   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)) {
2815     if (!mt.visible || !mt.isInstanceAlive) continue;
2816     //Video.color = (mt.moveable ? 0xff_7f_00 : 0xff_ff_ff);
2817     //!mt.drawWithOfs(xofs, yofs, scale, currFrameDelta);
2818     renderVisibleCids[$] = mt;
2819   }
2820   //Video.color = 0xff_ff_ff;
2822   // render objects (and player)
2823   if (player && player.visible) renderVisibleCids[$] = player; // include player in render list
2824   auto ogrid = objGrid;
2825   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)) {
2826     if (o.visible && o.isInstanceAlive) renderVisibleCids[$] = o;
2827   }
2828   EntityGrid.sortEntList(renderVisibleCids, &renderSortByDepth);
2830   auto depth4Start = 0;
2831   foreach (auto xidx, MapEntity o; renderVisibleCids) {
2832     if (o.depth >= 4) {
2833       depth4Start = xidx;
2834       break;
2835     }
2836   }
2838   foreach (auto idx; depth4Start..renderVisibleCids.length; reverse) {
2839     MapEntity o = renderVisibleCids[idx];
2840     //if (idx && renderSortByDepth(o, renderVisibleCids[idx-1])) writeln("VIOLATION!");
2841     o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
2842   }
2844   // render player
2845   //if (player && player.visible) player.drawWithOfs(xofs, yofs, scale, currFrameDelta);
2847   // render front tile parts (depth 3.5)
2848   foreach (MapTile tile; renderFrontTiles) tile.drawWithOfsFront(xofs, yofs, scale, currFrameDelta);
2850   // render items with depth 3 and less
2851   foreach (auto idx; 0..depth4Start; reverse) {
2852     MapEntity o = renderVisibleCids[idx];
2853     o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
2854   }
2856   renderVisibleCids.length -= renderVisibleCids.length;
2858   // render player post-effects, so cape or jetpack will be correctly rendered on level exit, for example
2859   player.lastDrawWithOfs(xofs, yofs, scale, currFrameDelta);
2861   if (global.config.drawHUD) renderHUD(currFrameDelta);
2862   renderCompass(currFrameDelta);
2864   float osdTimeLeft, osdTimeStart;
2865   string msg = osdGetMessage(out osdTimeLeft, out osdTimeStart);
2866   if (msg) {
2867     auto ct = GetTickCount();
2868     int msgScale = 3;
2869     sprStore.loadFont('sFontSmall');
2870     auto msgHeight = sprStore.getMultilineTextHeight(msg, msgScale);
2871     int x = Video.screenWidth/2;
2872     int y = Video.screenHeight-64-msgHeight;
2873     auto oldColor = Video.color;
2874     Video.color = 0xff_ff_00;
2875     if (osdTimeLeft < 0.5) {
2876       int alpha = 255-clamp(int(510*osdTimeLeft), 0, 255);
2877       Video.color = Video.color|(alpha<<24);
2878     } else if (ct-osdTimeStart < 0.5) {
2879       osdTimeStart = ct-osdTimeStart;
2880       int alpha = 255-clamp(int(510*osdTimeStart), 0, 255);
2881       Video.color = Video.color|(alpha<<24);
2882     }
2883     sprStore.renderMultilineTextCentered(x, y, msg, msgScale);
2884     Video.color = oldColor;
2885   }
2889 // ////////////////////////////////////////////////////////////////////////// //
2890 final class!MapObject findGameObjectClassByName (name aname) {
2891   if (!aname) return none; // just in case
2892   auto co = FindClassByGameObjName(aname);
2893   if (!co) {
2894     writeln("***UNKNOWN GAME OBJECT: '", aname, "'");
2895     return none;
2896   }
2897   co = GetClassReplacement(co);
2898   if (!co) FatalError("findGameObjectClassByName: WTF?!");
2899   if (!class!MapObject(co)) FatalError("findGameTileClassByName: object '%n' is not a map object, it is a `%n`", aname, GetClassName(co));
2900   return class!MapObject(co);
2904 final class!MapTile findGameTileClassByName (name aname) {
2905   if (!aname) return none; // just in case
2906   auto co = FindClassByGameObjName(aname);
2907   if (!co) return MapTile; // unknown names will be routed directly to tile object
2908   co = GetClassReplacement(co);
2909   if (!co) FatalError("findGameTileClassByName: WTF?!");
2910   if (!class!MapTile(co)) FatalError("findGameTileClassByName: object '%n' is not a tile, it is a `%n`", aname, GetClassName(co));
2911   return class!MapTile(co);
2915 final MapObject findAnyObjectOfType (name aname) {
2916   if (!aname) return none;
2917   auto cls = FindClassByGameObjName(aname);
2918   if (!cls) return none;
2919   for (auto cid = objGrid.getFirstObject(); cid; cid = objGrid.getNextObject(cid)) {
2920     MapObject obj = objGrid.getObject(MapObject, cid);
2921     if (!obj || obj.spectral || !obj.isInstanceAlive) continue;
2922     if (obj isa cls) return obj;
2923   }
2924   return none;
2928 // ////////////////////////////////////////////////////////////////////////// //
2929 final MapTile CreateMapTile (int xpos, int ypos, name aname) {
2930   if (!aname) FatalError("cannot create typeless tile");
2931   //MapTile tile = SpawnObject(aname == 'oRope' ? MapTileRope : MapTile);
2932   auto tclass = findGameTileClassByName(aname);
2933   if (!tclass) return none;
2934   MapTile tile = SpawnObject(tclass);
2935   tile.global = global;
2936   tile.level = self;
2937   tile.objName = aname;
2938   tile.objType = aname; // just in case
2939   tile.fltx = xpos;
2940   tile.flty = ypos;
2941   if (!tile.initialize()) FatalError("cannot create tile '%n'", aname);
2942   return tile;
2946 final bool isRopePlacedAt (int x, int y) {
2947   int[8] covered;
2948   foreach (ref auto v; covered) v = false;
2949   foreach (MapTile t; miscTileGrid.inRectPix(x, y-8, 1, 17, precise:false)) {
2950     if (!cbIsRopeTile(t)) continue;
2951     if (t.ix != x) continue;
2952     if (t.iy == y) return true;
2953     foreach (int ty; t.iy..t.iy+8) {
2954       int d = ty-y;
2955       if (d >= 0 && d < covered.length) covered[d] = true;
2956     }
2957   }
2958   // check if the whole rope height is completely covered with ropes
2959   foreach (auto v; covered) if (!v) return false;
2960   return true;
2964 // won't call `onDestroy()`
2965 final void RemoveMapTile (int tileX, int tileY) {
2966   if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
2967     if (tiles[tileX, tileY]) checkWater = true;
2968     delete tiles[tileX, tileY];
2969     tiles[tileX, tileY] = none;
2970   }
2974 final MapTile MakeMapTile (int mapx, int mapy, name aname/*, optional name sprname*/) {
2975   if (mapx < 0 || mapx >= tilesWidth || mapy < 0 || mapy >= tilesHeight) return none;
2977   // if we already have rope tile there, there is no reason to add another one
2978   if (aname == 'oRope') {
2979     if (isRopePlacedAt(mapx*16, mapy*16)) {
2980       //writeln("dupe rope (0)!");
2981       return none;
2982     }
2983   }
2985   auto tile = CreateMapTile(mapx*16, mapy*16, aname);
2986   if (tile.moveable || tile.toSpecialGrid) {
2987     // moveable tiles goes to the separate list
2988     miscTileGrid.insert(tile);
2989   } else {
2990     setTileAt(mapx, mapy, tile);
2991   }
2993   switch (aname) {
2994     case 'oEntrance': registerEnter(tile); break;
2995     case 'oExit': registerExit(tile); break;
2996   }
2998   return tile;
3002 final void MarkTileAsWet (int tileX, int tileY) {
3003   auto t = getTileAt(tileX, tileY);
3004   if (t) t.wet = true;
3008 final MapTile MakeMapTileAtPix (int xpix, int ypix, name aname/*, optional name sprname*/) {
3009   if (xpix%16 == 0 && ypix%16 == 0) return MakeMapTile(xpix/16, ypix/16, aname);
3010   //if (mapx < 0 || mapx >= TilesWidth || mapy < 0 || mapy >= TilesHeight) return none;
3012   // if we already have rope tile there, there is no reason to add another one
3013   if (aname == 'oRope') {
3014     if (isRopePlacedAt(xpix, ypix)) {
3015       //writeln("dupe rope (0)!");
3016       return none;
3017     }
3018   }
3020   auto tile = CreateMapTile(xpix, ypix, aname);
3021   // non-aligned tiles goes to the special grid
3022   miscTileGrid.insert(tile);
3024   switch (aname) {
3025     case 'oEntrance': registerEnter(tile); break;
3026     case 'oExit': registerExit(tile); break;
3027   }
3029   return tile;
3033 final MapTile MakeMapRopeTileAt (int x0, int y0) {
3034   // if we already have rope tile there, there is no reason to add another one
3035   if (isRopePlacedAt(x0, y0)) {
3036     //writeln("dupe rope (1)!");
3037     return none;
3038   }
3040   auto tile = CreateMapTile(x0, y0, 'oRope');
3041   miscTileGrid.insert(tile);
3043   return tile;
3047 // ////////////////////////////////////////////////////////////////////////// //
3048 final MapBackTile CreateBgTile (name sprName, optional int atx0, optional int aty0, optional int aw, optional int ah) {
3049   BackTileImage img = bgtileStore[sprName];
3050   auto res = SpawnObject(MapBackTile);
3051   res.global = global;
3052   res.level = self;
3053   res.bgt = img;
3054   res.bgtName = sprName;
3055   if (specified_atx0) res.tx0 = atx0;
3056   if (specified_aty0) res.ty0 = aty0;
3057   if (specified_aw) res.w = aw;
3058   if (specified_ah) res.h = ah;
3059   if (!res.initialize()) FatalError("cannot create background tile with image '%n'", sprName);
3060   return res;
3064 // ////////////////////////////////////////////////////////////////////////// //
3066 background The background asset from which the new tile will be extracted.
3067 left The x coordinate of the left of the new tile, relative to the background asset's top left corner.
3068 top The y coordinate of the top of the new tile, relative to the background assets top left corner.
3069 width The width of the tile.
3070 height The height of the tile.
3071 x The x position in the room to place the tile.
3072 y The y position in the room to place the tile.
3073 depth The depth at which to place the tile.
3075 final void MakeMapBackTile (name bgname, int left, int top, int width, int height, int x, int y, int depth) {
3076   if (width < 1 || height < 1 || !bgname) return;
3077   auto bgt = bgtileStore[bgname];
3078   if (!bgt) FatalError("cannot load background '%n'", bgname);
3079   MapBackTile bt = SpawnObject(MapBackTile);
3080   bt.global = global;
3081   bt.level = self;
3082   bt.objName = bgname;
3083   bt.bgt = bgt;
3084   bt.bgtName = bgname;
3085   bt.fltx = x;
3086   bt.flty = y;
3087   bt.tx0 = left;
3088   bt.ty0 = top;
3089   bt.w = width;
3090   bt.h = height;
3091   bt.depth = depth;
3092   // find a place for it
3093   if (!backtiles) {
3094     backtiles = bt;
3095     return;
3096   }
3097   // back tiles with the highest depth should come first
3098   MapBackTile ct = backtiles, cprev = none;
3099   while (ct && depth <= ct.depth) { cprev = ct; ct = ct.next; }
3100   // insert before ct
3101   if (cprev) {
3102     bt.next = cprev.next;
3103     cprev.next = bt;
3104   } else {
3105     bt.next = backtiles;
3106     backtiles = bt;
3107   }
3111 // ////////////////////////////////////////////////////////////////////////// //
3112 final MapObject SpawnMapObjectWithClass (class!MapObject oclass) {
3113   if (!oclass) return none;
3115   MapObject obj = SpawnObject(oclass);
3116   if (!obj) FatalError("cannot create map object '%n'", GetClassName(oclass));
3118   //if (aname == 'oShells' || aname == 'oShellSingle' || aname == 'oRopePile') writeln("*** CREATED '", aname, "'");
3120   obj.global = global;
3121   obj.level = self;
3123   return obj;
3127 final MapObject SpawnMapObject (name aname) {
3128   if (!aname) return none;
3129   return SpawnMapObjectWithClass(findGameObjectClassByName(aname));
3133 final MapObject PutSpawnedMapObject (int x, int y, MapObject obj) {
3134   if (!obj /*|| obj.global || obj.level*/) return none; // oops
3136   obj.fltx = x;
3137   obj.flty = y;
3138   if (!obj.initialize()) { delete obj; return none; } // not fatal
3140   insertObject(obj);
3142   return obj;
3146 final MapObject MakeMapObject (int x, int y, name aname) {
3147   MapObject obj = SpawnMapObject(aname);
3148   obj = PutSpawnedMapObject(x, y, obj);
3149   return obj;
3153 // ////////////////////////////////////////////////////////////////////////// //
3154 #include "roomTitle.vc"
3155 #include "roomTrans1.vc"
3156 #include "roomTrans2.vc"
3157 #include "roomTrans3.vc"
3158 #include "roomTrans4.vc"
3159 #include "roomOlmec.vc"
3162 // ////////////////////////////////////////////////////////////////////////// //
3163 #include "packages/Generator/loadRoomGens.vc"
3164 #include "packages/Generator/loadEntityGens.vc"