1 /**********************************************************************************
2 * Copyright (c) 2008, 2009 Derek Yu and Mossmouth, LLC
3 * Copyright (c) 2018, Ketmar Dark
5 * This file is part of Spelunky.
7 * You can redistribute and/or modify Spelunky, including its source code, under
8 * the terms of the Spelunky User License.
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.
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/>
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;
30 //enum NormalTilesWidth = LevelGen::LevelWidth*RoomGen::Width+2;
31 //enum NormalTilesHeight = LevelGen::LevelHeight*RoomGen::Height+2;
33 enum MaxTilesWidth = 64;
34 enum MaxTilesHeight = 64;
37 transient GameStats stats;
38 transient SpriteStore sprStore;
39 transient BackTileStore bgtileStore;
40 transient BackTileImage levBGImg;
43 transient name lastMusicName;
44 //RoomGen[LevelGen::LevelWidth, LevelGen::LevelHeight] rooms; // moved to levelgen
46 transient float accumTime;
47 transient bool gamePaused = false;
48 transient bool checkWater;
49 transient int damselSaved;
53 transient int collectCounter;
54 transient int levelMoneyStart;
56 // all movable (thinkable) map objects
57 EntityGrid objGrid; // monsters and items
59 MapTile[MaxTilesWidth, MaxTilesHeight] tiles;
60 MapBackTile backtiles;
61 EntityGrid miscTileGrid; // moveables and ropes
62 bool blockWaterChecking;
63 array!MapTile lavatiles; // they need to think/animate, but i don't want to move 'em out of `tiles`
65 array!MapObject ballObjects; // list of all ball objects, for speed
69 LevelGen::RType[MaxTilesWidth, MaxTilesHeight] roomType;
77 LevelKind levelKind = LevelKind.Normal;
79 array!MapTile allEnters;
80 array!MapTile allExits;
83 int startRoomX, startRoomY;
84 int endRoomX, endRoomY;
87 transient bool playerExited;
88 transient bool disablePlayerThink = false;
89 transient int maxPlayingTime; // in seconds
93 bool ghostSpawned; // to speed up some checks
94 bool resetBMCOG = false;
98 // FPS, i.e. incremented by 30 in one second
99 int time; // in frames
100 int lastUsedObjectId;
102 // screen shake variables
107 // set this before calling `fixCamera()`
108 // dimensions should be real, not scaled up/down
109 transient int viewWidth, viewHeight;
110 // room bounds, not scaled
111 IVec2D viewMin, viewMax;
113 // for Olmec level cinematics
114 IVec2D cameraSlideToDest;
115 IVec2D cameraSlideToCurr;
116 IVec2D cameraSlideToSpeed; // !0: slide
117 int cameraSlideToPlayer;
118 // `fixCamera()` will set the following
119 // coordinates will be real too (with scale applied)
120 // shake is not applied
121 transient IVec2D viewStart; // with `player.viewOffset`
122 private transient IVec2D realViewStart; // without `player.viewOffset`
124 final void cameraSlideToPoint (int dx, int dy, int speedx, int speedy) {
125 cameraSlideToPlayer = 0;
126 cameraSlideToDest.x = dx;
127 cameraSlideToDest.y = dy;
128 cameraSlideToSpeed.x = abs(speedx);
129 cameraSlideToSpeed.y = abs(speedy);
130 cameraSlideToCurr.x = cameraCurrX;
131 cameraSlideToCurr.y = cameraCurrY;
134 final void cameraReturnToPlayer () {
135 if (!cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y)) {
136 cameraSlideToCurr.x = cameraCurrX;
137 cameraSlideToCurr.y = cameraCurrY;
138 if (cameraSlideToSpeed.x && abs(cameraSlideToSpeed.x) < 8) cameraSlideToSpeed.x = 8;
139 if (cameraSlideToSpeed.y && abs(cameraSlideToSpeed.y) < 8) cameraSlideToSpeed.y = 8;
140 cameraSlideToPlayer = 1;
144 // if `frameSkip` is `true`, there are more frames waiting
145 // (i.e. you may skip rendering and such)
146 transient void delegate (bool frameSkip) onBeforeFrame;
147 transient void delegate (bool frameSkip) onAfterFrame;
149 transient void delegate () onLevelExitedCB;
151 // this will be called in-between frames, and
152 // `frameTime` is [0..1)
153 transient void delegate (float frameTime) onInterFrame;
155 final int bizRoomStyle { get { return (lg ? lg.bizRoomStyle : 0); } }
158 final bool isNormalLevel () { return (levelKind == LevelKind.Normal); }
159 final bool isTitleRoom () { return (levelKind == LevelKind.Title); }
162 // ////////////////////////////////////////////////////////////////////////// //
164 void addDeath (name aname) { if (isNormalLevel()) stats.addDeath(aname); }
165 void addKill (name aname) { if (isNormalLevel()) stats.addKill(aname); }
166 void addCollect (name aname, optional int amount) { if (isNormalLevel()) stats.addCollect(aname, amount!optional); }
168 void addDamselSaved () { if (isNormalLevel()) stats.addDamselSaved(); }
169 void addIdolStolen () { if (isNormalLevel()) stats.addIdolStolen(); }
170 void addIdolConverted () { if (isNormalLevel()) stats.addIdolConverted(); }
171 void addCrystalIdolStolen () { if (isNormalLevel()) stats.addCrystalIdolStolen(); }
172 void addCrystalIdolConverted () { if (isNormalLevel()) stats.addCrystalIdolConverted(); }
173 void addGhostSummoned () { if (isNormalLevel()) stats.addGhostSummoned(); }
176 // ////////////////////////////////////////////////////////////////////////// //
177 static final string val2dig (int n) {
178 return (n < 10 ? va("0%d", n) : va("%d", n));
182 static final string time2str (int time) {
183 int secs = time%60; time /= 60;
184 int mins = time%60; time /= 60;
185 int hours = time%24; time /= 24;
187 if (days) return va("%d DAYS, %d:%s:%s", days, hours, val2dig(mins), val2dig(secs));
188 if (hours) return va("%d:%s:%s", hours, val2dig(mins), val2dig(secs));
189 return va("%s:%s", val2dig(mins), val2dig(secs));
193 // ////////////////////////////////////////////////////////////////////////// //
194 final int tilesWidth () { return lg.levelRoomWidth*RoomGen::Width+2; }
195 final int tilesHeight () { return (lg.finalBossLevel ? 55 : lg.levelRoomHeight*RoomGen::Height+2); }
198 // ////////////////////////////////////////////////////////////////////////// //
199 // this won't generate a level yet
200 void restartGame () {
206 player.removeBallAndChain(temp:false);
207 auto hi = player.holdItem;
208 player.holdItem = none;
209 if (hi) hi.instanceRemove();
210 hi = player.pickedItem;
211 player.pickedItem = none;
212 if (hi) hi.instanceRemove();
216 stats.clearGameTotals();
217 if (global.startMoney > 0) stats.setMoneyCheat();
218 stats.setMoney(global.startMoney);
219 levelKind = LevelKind.Normal;
220 //writeln("level=", global.currLevel, "; lt=", global.levelType);
224 void restartTitle () {
230 player.removeBallAndChain(temp:false);
231 auto hi = player.holdItem;
232 player.holdItem = none;
233 if (hi) hi.instanceRemove();
234 hi = player.pickedItem;
235 player.pickedItem = none;
236 if (hi) hi.instanceRemove();
240 stats.clearGameTotals();
243 levelKind = LevelKind.Title;
246 global.arrows = 9999;
247 global.sgammo = 9999;
248 //writeln("level=", global.currLevel, "; lt=", global.levelType);
252 // complement function to `restart game`
253 void generateNormalLevel () {
255 centerViewAtPlayer();
259 // ////////////////////////////////////////////////////////////////////////// //
260 // generate angry shopkeeper at exit if murderer or thief
261 void generateAngryShopkeepers () {
262 if (global.murderer || global.thiefLevel > 0) {
263 foreach (MapTile e; allExits) {
264 auto obj = MonsterShopkeeper(MakeMapObject(e.ix, e.iy, 'oShopkeeper'));
266 obj.style = 'Bounty Hunter';
267 obj.status = MapObject::PATROL;
274 // ////////////////////////////////////////////////////////////////////////// //
275 final void resetRoomBounds () {
278 viewMax.x = tilesWidth*16;
279 viewMax.y = tilesHeight*16;
280 // Great Lake is bottomless (nope)
281 //if (global.lake) viewMax.y -= 16;
282 //writeln("ROOM BOUNDS: (", viewMax.x, "x", viewMax.y, ")");
286 final void setRoomBounds (int x0, int y0, int x1, int y1) {
294 // ////////////////////////////////////////////////////////////////////////// //
297 float timeout; // seconds
298 float starttime; // for active
299 bool active; // true: timeout is `GetTickCount()` dismissing time
302 array!OSDMessage msglist; // [0]: current one
305 private final void osdCheckTimeouts () {
306 auto stt = GetTickCount();
307 while (msglist.length) {
308 if (!msglist[0].active) {
309 msglist[0].active = true;
310 msglist[0].starttime = stt;
312 if (msglist[0].starttime+msglist[0].timeout >= stt) break;
318 final bool osdHasMessage () {
320 return (msglist.length > 0);
324 final string osdGetMessage (out float timeLeft, out float timeStart) {
326 if (msglist.length == 0) { timeLeft = 0; return ""; }
327 auto stt = GetTickCount();
328 timeStart = msglist[0].starttime;
329 timeLeft = fmax(0, msglist[0].starttime+msglist[0].timeout-stt);
330 return msglist[0].msg;
334 final void osdClear () {
335 msglist.length -= msglist.length;
339 final void osdMessage (string msg, optional float timeout, optional bool urgent) {
341 if (!specified_timeout) timeout = 3.33;
342 // special message for shops
343 if (timeout == -666) {
345 if (msglist.length && msglist[0].msg == msg) return;
346 if (msglist.length == 0 || msglist[0].msg != msg) {
349 msglist[0].msg = msg;
351 msglist[0].active = false;
352 msglist[0].timeout = 3.33;
356 if (timeout < 0.1) return;
357 timeout = fmax(1.0, timeout);
358 //writeln("OSD: ", msg);
359 // find existing one, and bring it to the top
361 for (; oldidx < msglist.length; ++oldidx) {
362 if (msglist[oldidx].msg == msg) break; // i found her!
365 if (oldidx < msglist.length) {
366 // yeah, move duplicate to the top
367 msglist[oldidx].timeout = fmax(timeout, msglist[oldidx].timeout);
368 msglist[oldidx].active = false;
369 if (urgent && oldidx != 0) {
370 timeout = msglist[oldidx].timeout;
371 msglist.remove(oldidx);
373 msglist[0].msg = msg;
374 msglist[0].timeout = timeout;
375 msglist[0].active = false;
379 msglist[0].msg = msg;
380 msglist[0].timeout = timeout;
381 msglist[0].active = false;
385 msglist[$-1].msg = msg;
386 msglist[$-1].timeout = timeout;
387 msglist[$-1].active = false;
393 // ////////////////////////////////////////////////////////////////////////// //
394 void setup (GameGlobal aGlobal, SpriteStore aSprStore, BackTileStore aBGTileStore) {
396 sprStore = aSprStore;
397 bgtileStore = aBGTileStore;
399 lg = SpawnObject(LevelGen);
403 miscTileGrid = SpawnObject(EntityGrid);
404 miscTileGrid.setup(0, 0, MaxTilesWidth*16, MaxTilesHeight*16, MapTile);
405 //miscTileGrid.ownObjects = true;
407 objGrid = SpawnObject(EntityGrid);
408 objGrid.setup(0, 0, MaxTilesWidth*16, MaxTilesHeight*16, MapObject);
412 // stores should be set
415 levBGImg = bgtileStore[levBGImgName];
416 foreach (int y; 0..MaxTilesHeight) {
417 foreach (int x; 0..MaxTilesWidth) {
418 if (tiles[x, y]) tiles[x, y].onLoaded();
421 foreach (MapEntity o; miscTileGrid.allObjects()) o.onLoaded();
422 foreach (MapEntity o; objGrid.allObjects()) o.onLoaded();
423 for (MapBackTile bt = backtiles; bt; bt = bt.next) bt.onLoaded();
424 if (player) player.onLoaded();
426 if (msglist.length) {
427 msglist[0].active = false;
428 msglist[0].timeout = 0.200;
434 // ////////////////////////////////////////////////////////////////////////// //
435 void pickedSpectacles () {
436 foreach (int y; 0..tilesHeight) {
437 foreach (int x; 0..tilesWidth) {
438 MapTile t = tiles[x, y];
439 if (t && t.isInstanceAlive) t.onGotSpectacles();
442 foreach (MapTile t; miscTileGrid.allObjects()) {
443 if (t.isInstanceAlive) t.onGotSpectacles();
448 // ////////////////////////////////////////////////////////////////////////// //
449 #include "rgentile.vc"
450 #include "rgenobj.vc"
453 void onLevelExited () {
454 if (isNormalLevel()) stats.maxLevelComplete = max(stats.maxLevelComplete, global.currLevel);
455 if (onLevelExitedCB) onLevelExitedCB();
456 if (isTitleRoom()) restartGame();
457 if (levelKind == LevelKind.Transition) {
458 if (global.thiefLevel > 0) global.thiefLevel -= 1;
459 //orig dbg:if (global.currLevel == 1) global.currLevel += firstLevelSkip; else global.currLevel += levelSkip;
460 if (!global.blackMarket && !global.cityOfGold /*&& !global.yetiLair*/) {
461 global.currLevel += 1;
465 if (lg.finalBossLevel) {
468 // add money for big idol
469 player.addScore(50000);
473 generateTransitionLevel();
476 centerViewAtPlayer();
480 void onOlmecDead (MapObject o) {
481 writeln("*** OLMEC IS DEAD!");
482 foreach (MapTile t; allExits) {
485 auto st = checkTileAtPoint(t.ix+8, t.iy+16);
487 st = MakeMapTile(t.ix/16, t.iy/16+1, 'oTemple');
490 st.invincible = true;
496 void generateLevelMessages () {
497 if (global.darkLevel) {
498 if (global.hasCrown) {
499 osdMessage("THE HEDJET SHINES BRIGHTLY.");
500 global.darkLevel = false;
501 } else if (global.config.scumDarkness < 2) {
502 osdMessage("I CAN'T SEE A THING!\nI'D BETTER USE THESE FLARES!");
506 if (global.blackMarket) osdMessage("WELCOME TO THE BLACK MARKET!");
508 if (global.cemetary) osdMessage("THE DEAD ARE RESTLESS!");
509 if (global.lake) osdMessage("I CAN HEAR RUSHING WATER...");
511 if (global.snakePit) osdMessage("I HEAR SNAKES... I HATE SNAKES!");
512 if (global.yetiLair) osdMessage("IT SMELLS LIKE WET FUR IN HERE!");
513 if (global.alienCraft) osdMessage("THERE'S A PSYCHIC PRESENCE HERE!");
514 if (global.cityOfGold) {
515 if (true /*hasOlmec()*/) osdMessage("IT'S THE LEGENDARY CITY OF GOLD!");
518 if (global.sacrificePit) osdMessage("I CAN HEAR PRAYERS TO KALI!");
522 final MapObject debugSpawnObjectWithClass (class!MapObject oclass, optional bool playerDir) {
523 if (!oclass) return none;
525 bool canLeft = !isSolidAtPoint(player.ix-8, player.iy);
526 bool canRight = !isSolidAtPoint(player.ix+16, player.iy);
527 if (!canLeft && !canRight) return none;
528 if (canLeft && canRight) {
530 dx = (player.dir == MapObject::Dir.Left ? -16 : 16);
535 dx = (canLeft ? -16 : 16);
537 auto obj = SpawnMapObjectWithClass(oclass);
538 if (obj isa MapEnemy) { dx -= 8; dy -= 8; }
539 if (obj) PutSpawnedMapObject(player.ix+dx, player.iy+dy, obj);
544 final MapObject debugSpawnObject (name aname) {
545 if (!aname) return none;
546 return debugSpawnObjectWithClass(findGameObjectClassByName(aname));
550 // `global.currLevel` is the new level
551 void generateTransitionLevel () {
552 global.darkLevel = false;
557 global.setMusicPitch(1.0);
558 switch (global.config.transitionMusicMode) {
559 case GameConfig::MusicMode.Silent: global.stopMusic(); break;
560 case GameConfig::MusicMode.Restart: global.restartMusic(); break;
561 case GameConfig::MusicMode.DontTouch: break;
564 levelKind = LevelKind.Transition;
566 auto olddel = ImmediateDelete;
567 ImmediateDelete = false;
571 if (global.currLevel < 4) createTrans1Room();
572 else if (global.currLevel == 4) createTrans1xRoom();
573 else if (global.currLevel < 8) createTrans2Room();
574 else if (global.currLevel == 8) createTrans2xRoom();
575 else if (global.currLevel < 12) createTrans3Room();
576 else if (global.currLevel == 12) createTrans3xRoom();
577 else if (global.currLevel < 16) createTrans4Room();
578 else if (global.currLevel == 16) createTrans4Room();
579 else createTrans1Room(); //???
584 addBackgroundGfxDetails();
585 levBGImgName = 'bgCave';
586 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
588 blockWaterChecking = true;
592 if (damselSaved > 0) {
593 MakeMapObject(176+8, 176+8, 'oDamselKiss');
594 global.plife += damselSaved; // if player skipped transition cutscene
600 ImmediateDelete = olddel;
601 CollectGarbage(true); // destroy delayed objects too
604 miscTileGrid.dumpStats();
608 playerExited = false; // just in case
613 //global.playMusic(lg.musicName);
617 void generateLevel () {
621 global.cityOfGold = false;
622 global.genBlackMarket = false;
625 global.setMusicPitch(1.0);
626 stats.clearLevelTotals();
628 levelKind = LevelKind.Normal;
635 //writeln("tw:", tilesWidth, "; th:", tilesHeight);
637 auto olddel = ImmediateDelete;
638 ImmediateDelete = false;
642 if (lg.finalBossLevel) {
643 blockWaterChecking = true;
647 // if transition cutscene was skipped...
648 if (damselSaved > 0) global.plife += damselSaved; // if player skipped transition cutscene
652 startRoomX = lg.startRoomX;
653 startRoomY = lg.startRoomY;
654 endRoomX = lg.endRoomX;
655 endRoomY = lg.endRoomY;
656 addBackgroundGfxDetails();
657 foreach (int y; 0..tilesHeight) {
658 foreach (int x; 0..tilesWidth) {
664 levBGImgName = lg.bgImgName;
665 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
667 if (global.allowAngryShopkeepers) generateAngryShopkeepers();
669 lg.generateEntities();
671 // add box of flares to dark level
672 if (global.darkLevel && allEnters.length) {
673 auto enter = allEnters[0];
674 int x = enter.ix, y = enter.iy;
675 if (!isSolidAtPoint(x-16, y)) MakeMapObject(x-16+8, y+8, 'oFlareCrate');
676 else if (!isSolidAtPoint(x+16, y)) MakeMapObject(x+16+8, y+8, 'oFlareCrate');
677 else MakeMapObject(x+8, y+8, 'oFlareCrate');
680 //scrGenerateEntities();
681 //foreach (; 0..2) scrGenerateEntities();
683 writeln(countObjects, " alive objects inserted");
684 writeln(countBackTiles, " background tiles inserted");
686 if (!player) FatalError("player pawn is not spawned");
688 if (lg.finalBossLevel) {
689 blockWaterChecking = true;
691 blockWaterChecking = false;
698 ImmediateDelete = olddel;
699 CollectGarbage(true); // destroy delayed objects too
702 miscTileGrid.dumpStats();
706 playerExited = false; // just in case
708 levelMoneyStart = stats.money;
711 generateLevelMessages();
716 if (lastMusicName != lg.musicName) {
717 global.playMusic(lg.musicName);
718 //writeln("!<", lastMusicName, ">:<", lg.musicName, ">");
720 //writeln("MM: ", global.config.nextLevelMusicMode);
721 switch (global.config.nextLevelMusicMode) {
722 case GameConfig::MusicMode.Silent: global.stopMusic(); break; // the thing that should not be
723 case GameConfig::MusicMode.Restart: global.restartMusic(); break;
724 case GameConfig::MusicMode.DontTouch:
725 if (global.config.transitionMusicMode == GameConfig::MusicMode.Silent) {
726 global.playMusic(lg.musicName);
731 lastMusicName = lg.musicName;
732 //global.playMusic(lg.musicName);
735 if (global.cityOfGold || global.genBlackMarket) resetBMCOG = true;
739 // ////////////////////////////////////////////////////////////////////////// //
740 int currKeys, nextKeys;
741 int pressedKeysQ, releasedKeysQ;
742 int keysPressed, keysReleased = -1;
745 struct SavedKeyState {
746 int currKeys, nextKeys;
747 int pressedKeysQ, releasedKeysQ;
748 int keysPressed, keysReleased;
750 int roomSeed, otherSeed;
754 // for saving/replaying
755 final void keysSaveState (out SavedKeyState ks) {
756 ks.currKeys = currKeys;
757 ks.nextKeys = nextKeys;
758 ks.pressedKeysQ = pressedKeysQ;
759 ks.releasedKeysQ = releasedKeysQ;
760 ks.keysPressed = keysPressed;
761 ks.keysReleased = keysReleased;
764 // for saving/replaying
765 final void keysRestoreState (const ref SavedKeyState ks) {
766 currKeys = ks.currKeys;
767 nextKeys = ks.nextKeys;
768 pressedKeysQ = ks.pressedKeysQ;
769 releasedKeysQ = ks.releasedKeysQ;
770 keysPressed = ks.keysPressed;
771 keysReleased = ks.keysReleased;
775 final void keysNextFrame () {
780 final void clearKeys () {
790 final void onKey (int code, bool down) {
795 if (keysReleased&code) {
797 keysReleased &= ~code;
798 pressedKeysQ |= code;
802 if (keysPressed&code) {
803 keysReleased |= code;
804 keysPressed &= ~code;
805 releasedKeysQ |= code;
810 final bool isKeyDown (int code) {
811 return !!(currKeys&code);
814 final bool isKeyPressed (int code) {
815 bool res = !!(pressedKeysQ&code);
816 pressedKeysQ &= ~code;
820 final bool isKeyReleased (int code) {
821 bool res = !!(releasedKeysQ&code);
822 releasedKeysQ &= ~code;
827 final void clearKeysPressRelease () {
828 keysPressed = default.keysPressed;
829 keysReleased = default.keysReleased;
830 pressedKeysQ = default.pressedKeysQ;
831 releasedKeysQ = default.releasedKeysQ;
837 // ////////////////////////////////////////////////////////////////////////// //
838 final void registerEnter (MapTile t) {
845 final void registerExit (MapTile t) {
852 final bool isYAtEntranceRow (int py) {
854 foreach (MapTile t; allEnters) if (t.iy == py) return true;
859 final int calcNearestEnterDist (int px, int py) {
860 if (allEnters.length == 0) return int.max;
861 int curdistsq = int.max;
862 foreach (MapTile t; allEnters) {
863 int xc = px-t.xCenter, yc = py-t.yCenter;
864 int distsq = xc*xc+yc*yc;
865 if (distsq < curdistsq) curdistsq = distsq;
867 return round(sqrt(curdistsq));
871 final int calcNearestExitDist (int px, int py) {
872 if (allExits.length == 0) return int.max;
873 int curdistsq = int.max;
874 foreach (MapTile t; allExits) {
875 int xc = px-t.xCenter, yc = py-t.yCenter;
876 int distsq = xc*xc+yc*yc;
877 if (distsq < curdistsq) curdistsq = distsq;
879 return round(sqrt(curdistsq));
883 // ////////////////////////////////////////////////////////////////////////// //
884 final void clearForTransition () {
885 auto olddel = ImmediateDelete;
886 ImmediateDelete = false;
889 ImmediateDelete = olddel;
890 CollectGarbage(true); // destroy delayed objects too
891 global.darkLevel = false;
895 final void clearTiles () {
898 allEnters.length -= allEnters.length; // don't deallocate
899 allExits.length -= allExits.length; // don't deallocate
900 lavatiles.length -= lavatiles.length;
901 foreach (ref auto tile; tiles) delete tile;
902 if (dumpGridStats) { if (miscTileGrid.getFirstObject()) miscTileGrid.dumpStats(); }
903 miscTileGrid.removeAllObjects(true); // and destroy
905 MapBackTile t = backtiles;
913 // ////////////////////////////////////////////////////////////////////////// //
914 final int countObjects () {
915 return objGrid.countObjects();
918 final int countBackTiles () {
920 for (MapBackTile bt = backtiles; bt; bt = bt.next) ++res;
924 final void clearObjects () {
925 // don't kill objects player is holding
927 if (player.pickedItem isa ItemBall) {
928 player.pickedItem.instanceRemove();
929 player.pickedItem = none;
931 if (player.pickedItem && player.pickedItem.grid) {
932 player.pickedItem.grid.remove(player.pickedItem.gridId);
933 writeln("secured pocket item '", GetClassName(player.pickedItem.Class), "'");
934 //player.pickedItem.grid = none;
936 if (player.holdItem isa ItemBall) {
937 player.removeBallAndChain(temp:true);
938 player.holdItem.instanceRemove();
939 player.holdItem = none;
941 if (player.holdItem && player.holdItem.grid) {
942 player.holdItem.grid.remove(player.holdItem.gridId);
943 writeln("secured held item '", GetClassName(player.holdItem.Class), "'");
944 //player.holdItem.grid = none;
948 int count = objGrid.countObjects();
949 if (dumpGridStats) { if (objGrid.getFirstObject()) objGrid.dumpStats(); }
950 objGrid.removeAllObjects(true); // and destroy
951 if (count > 0) writeln(count, " objects destroyed");
952 ballObjects.length = 0;
953 lastUsedObjectId = 0;
957 final void insertObject (MapObject o) {
959 if (o.grid) FatalError("cannot put object into level twice");
960 o.objId = ++lastUsedObjectId;
962 // ball from ball-and-chain
963 if (o isa ItemBall) {
965 int emptyBallIdx = -1;
966 foreach (auto idx, MapObject bo; ballObjects) {
967 if (bo == o) { found = true; break; }
968 if (emptyBallIdx < 0 && (!bo || !bo.isInstanceAlive)) emptyBallIdx = idx;
971 if (emptyBallIdx < 0) {
974 ballObjects[emptyBallIdx] = o;
983 final void spawnPlayerAt (int x, int y) {
984 // if we have no player, spawn new one
985 // otherwise this just a level transition, so simply reposition him
987 // don't add player to object list, as it has very separate processing anyway
988 player = SpawnObject(PlayerPawn);
989 player.global = global;
991 if (!player.initialize()) {
993 FatalError("something is wrong with player initialization");
999 player.saveInterpData();
1001 if (player.mustBeChained || global.config.scumBallAndChain) player.spawnBallAndChain();
1002 playerExited = false;
1003 if (global.config.startWithKapala) global.hasKapala = true;
1004 centerViewAtPlayer();
1005 // reinsert player items into grid
1006 if (player.pickedItem) objGrid.insert(player.pickedItem);
1007 if (player.holdItem) objGrid.insert(player.holdItem);
1008 //writeln("player spawned; active=", player.active);
1009 player.scrSwitchToPocketItem(forceIfEmpty:false);
1013 final void teleportPlayerTo (int x, int y) {
1017 player.saveInterpData();
1022 final void resurrectPlayer () {
1023 if (player) player.resurrect();
1024 playerExited = false;
1028 // ////////////////////////////////////////////////////////////////////////// //
1029 final void scrShake (int duration) {
1030 if (shakeLeft == 0) {
1036 shakeLeft = max(shakeLeft, duration);
1041 // ////////////////////////////////////////////////////////////////////////// //
1044 ItemStolen, // including damsel, lol
1051 // make the nearest shopkeeper angry. RAWR!
1052 void scrShopkeeperAnger (SCAnger reason, optional int maxdist, optional MapEntity offender) {
1053 if (!offender) offender = player;
1054 auto shp = MonsterShopkeeper(findNearestEnemy(offender.ix, offender.iy, delegate bool (MapEnemy o) {
1055 auto sc = MonsterShopkeeper(o);
1056 if (!sc) return false;
1057 if (sc.dead || sc.angered) return false;
1062 if (specified_maxdist && offender.directionToEntityCenter(shp) > maxdist) return;
1063 if (!shp.dead && !shp.angered) {
1064 shp.status = MapObject::ATTACK;
1066 if (global.murderer) msg = "YOU'LL PAY FOR YOUR CRIMES!";
1067 else if (reason == SCAnger.ItemStolen) msg = "COME BACK HERE, THIEF!";
1068 else if (reason == SCAnger.TileDestroyed) msg = "DIE, YOU VANDAL!";
1069 else if (reason == SCAnger.BombDropped) msg = "TERRORIST!";
1070 else if (reason == SCAnger.DamselWhipped) msg = "HEY, ONLY I CAN DO THAT!";
1071 else if (reason == SCAnger.CrapsCheated) msg = "DIE, CHEATER!";
1072 else msg = "NOW I'M REALLY STEAMED!";
1073 if (msg) osdMessage(msg, -666);
1074 global.thiefLevel += (global.thiefLevel > 0 ? 3 : 2);
1080 final MapObject findCrapsPrize () {
1081 foreach (MapObject o; objGrid.allObjects()) {
1082 if (o.spectral || !o.isInstanceAlive) continue;
1083 if (o.inDiceHouse) return o;
1089 // ////////////////////////////////////////////////////////////////////////// //
1090 // moved from oPlayer1.Step.Action so it could be shared with oAltarLeft so that traps will be triggered when the altar is destroyed without picking up the idol.
1091 // note: idols moved by monkeys will have false `stolenIdol`
1092 void scrTriggerIdolAltar (bool stolenIdol) {
1093 ObjTikiCurse res = none;
1094 int curdistsq = int.max;
1095 int px = player.xCenter, py = player.yCenter;
1096 foreach (MapObject o; objGrid.allObjects()) {
1097 auto tcr = ObjTikiCurse(o);
1098 if (!tcr || !tcr.isInstanceAlive) continue;
1099 if (tcr.activated) continue;
1100 int xc = px-tcr.xCenter, yc = py-tcr.yCenter;
1101 int distsq = xc*xc+yc*yc;
1102 if (distsq < curdistsq) {
1107 if (res) res.activate(stolenIdol);
1111 // ////////////////////////////////////////////////////////////////////////// //
1112 void setupGhostTime () {
1113 musicFadeTimer = -1;
1114 ghostSpawned = false;
1116 // there is no ghost on the first level
1117 if (inWinCutscene || !isNormalLevel() || lg.finalBossLevel || global.currLevel == 1) {
1119 global.setMusicPitch(1.0);
1123 if (global.config.scumGhost < 0) {
1126 osdMessage("A GHASTLY VOICE CALLS YOUR NAME!\nLET'S GET OUT OF HERE!", 6.66, urgent:true);
1130 if (global.config.scumGhost == 0) {
1136 // randomizes time until ghost appears once time limit is reached
1137 // global.ghostRandom (true/false) if extra time is randomized - loaded from yasm.cfg. if false, use 30 second default
1138 // ghostTimeLeft (time in seconds * 1000) for currently generated level
1140 if (global.config.ghostRandom) {
1141 auto tMin = global.randOther(10, 30); // min(global.plife*10, 30);
1142 auto tMax = max(tMin+global.randOther(10, 30), min(global.config.scumGhost, 300)); // 5 minutes max
1143 auto tTime = global.randOther(tMin, tMax);
1144 if (tTime <= 0) tTime = round(tMax/2.0);
1145 ghostTimeLeft = tTime;
1147 ghostTimeLeft = global.config.scumGhost; // 5 minutes max
1150 ghostTimeLeft += max(0, global.config.ghostExtraTime);
1152 ghostTimeLeft *= 30; // seconds -> frames
1153 //global.ghostShowTime
1157 void spawnGhost () {
1159 ghostSpawned = true;
1162 int vwdt = (viewMax.x-viewMin.x);
1163 int vhgt = (viewMax.y-viewMin.y);
1167 if (player.ix < viewMin.x+vwdt/2) {
1168 // player is in the left side
1169 gx = viewMin.x+vwdt/2+vwdt/4;
1171 // player is in the right side
1172 gx = viewMin.x+vwdt/4;
1175 if (player.iy < viewMin.y+vhgt/2) {
1176 // player is in the left side
1177 gy = viewMin.y+vhgt/2+vhgt/4;
1179 // player is in the right side
1180 gy = viewMin.y+vhgt/4;
1183 writeln("spawning ghost at tile (", gx/16, ",", gy/16, "); player is at tile (", player.ix/16, ",", player.iy/16, ")");
1185 MakeMapObject(gx, gy, 'oGhost');
1188 if (oPlayer1.x > room_width/2) instance_create(view_xview[0]+view_wview[0]+8, view_yview[0]+floor(view_hview[0] / 2), oGhost);
1189 else instance_create(view_xview[0]-32, view_yview[0]+floor(view_hview[0]/2), oGhost);
1190 global.ghostExists = true;
1195 void thinkFrameGameGhost () {
1196 if (player.dead) return;
1197 if (!isNormalLevel()) return; // just in case
1199 if (ghostTimeLeft < 0) {
1201 if (musicFadeTimer > 0) {
1202 musicFadeTimer = -1;
1203 global.setMusicPitch(1.0);
1208 if (musicFadeTimer >= 0) {
1210 if (musicFadeTimer > 0 && musicFadeTimer <= 100*3) {
1211 float pitch = 1.0-(0.00226/3.0)*musicFadeTimer;
1212 //writeln("MFT: ", musicFadeTimer, "; pitch=", pitch);
1213 global.setMusicPitch(pitch);
1217 if (ghostTimeLeft == 0) {
1218 // she is already here!
1222 // no ghost if we have a crown
1223 if (global.hasCrown) {
1228 // if she was already spawned, don't do it again
1234 if (--ghostTimeLeft != 0) {
1236 if (global.config.ghostExtraTime > 0) {
1237 if (ghostTimeLeft == global.config.ghostExtraTime*30) {
1238 osdMessage("A CHILL RUNS UP YOUR SPINE...\nLET'S GET OUT OF HERE!", 6.66, urgent:true);
1240 if (musicFadeTimer < 0 && ghostTimeLeft <= global.config.ghostExtraTime*30) {
1248 if (player.isExitingSprite) {
1249 // no reason to spawn her, we're leaving
1258 void thinkFrameGame () {
1259 thinkFrameGameGhost();
1260 // udjat eye blinking
1261 if (global.hasUdjatEye && player) {
1262 foreach (MapTile t; allExits) {
1263 if (t isa MapTileBlackMarketDoor) {
1264 auto dm = int(player.distanceToEntity(t));
1266 if (udjatAlarm < 1 || dm < udjatAlarm) udjatAlarm = dm;
1270 global.udjatBlink = false;
1273 if (udjatAlarm > 0) {
1274 if (--udjatAlarm == 0) {
1275 global.udjatBlink = !global.udjatBlink;
1276 if (global.hasUdjatEye && player) {
1277 player.playSound(global.udjatBlink ? 'sndBlink1' : 'sndBlink2');
1284 // ////////////////////////////////////////////////////////////////////////// //
1285 private transient array!MapObject activeThinkerList;
1288 private final bool isWetTile (MapTile t) {
1289 return (t && t.visible && (t.water || t.lava || t.wet));
1293 private final bool isWetOrSolidTile (MapTile t) {
1294 return (t && t.visible && (t.water || t.lava || t.wet || t.solid) && t.isInstanceAlive);
1298 final bool isWetOrSolidTileAtPoint (int px, int py) {
1299 return !!checkTileAtPoint(px, py, &isWetOrSolidTile);
1303 final bool isWetOrSolidTileAtTile (int tx, int ty) {
1304 return !!checkTileAtPoint(tx*16, ty*16, &isWetOrSolidTile);
1308 final bool isWetTileAtTile (int tx, int ty) {
1309 return !!checkTileAtPoint(tx*16, ty*16, &isWetTile);
1313 // ////////////////////////////////////////////////////////////////////////// //
1314 const int GreatLakeStartTileY = 28;
1316 // called once after level generation
1317 final void fixLiquidTop () {
1318 foreach (int tileY; 0..tilesHeight) {
1319 foreach (int tileX; 0..tilesWidth) {
1320 auto t = tiles[tileX, tileY];
1322 if (t && !t.isInstanceAlive) {
1323 delete tiles[tileX, tileY];
1328 if (global.lake && tileY >= GreatLakeStartTileY) {
1329 // fill level with water for lake
1330 MakeMapTile(tileX, tileY, 'oWaterSwim');
1331 t = tiles[tileX, tileY];
1337 if (!t.water && !t.lava) {
1338 // mark as wet for lake
1339 if (global.lake && tileY >= GreatLakeStartTileY) {
1345 if (!isWetTileAtTile(tileX, tileY-1)) {
1346 t.setSprite(t.lava ? 'sLavaTop' : 'sWaterTop');
1348 if (t.spriteName == 'sWaterTop') t.setSprite('sWater');
1349 else if (t.spriteName == 'sLavaTop') t.setSprite('sLava');
1356 private final void checkWaterFlow (MapTile wtile) {
1357 //if (!wtile || (!wtile.water && !wtile.lava)) return;
1358 //instance_activate_region(x-16, y-16, 48, 48, true);
1360 //int x = wtile.ix, y = wtile.iy;
1361 int tileX = wtile.ix/16, tileY = wtile.iy/16;
1363 if (global.lake && tileY >= GreatLakeStartTileY) return;
1366 if ((not collision_point(x-16, y, oSolid, 0, 0) and not collision_point(x-16, y, oWater, 0, 0)) or
1367 (not collision_point(x+16, y, oSolid, 0, 0) and not collision_point(x+16, y, oWater, 0, 0)) or
1368 (not collision_point(x, y+16, oSolid, 0, 0) and not collision_point(x, y+16, oWater, 0, 0)))
1370 if (!isWetOrSolidTileAtTile(tileX-1, tileY) ||
1371 !isWetOrSolidTileAtTile(tileX+1, tileY) ||
1372 !isWetOrSolidTileAtTile(tileX, tileY+1))
1376 wtile.instanceRemove();
1379 tiles[tileX, tileY] = none;
1383 //if (!isSolidAtPoint(x, y-16) && !isLiquidAtPoint(x, y-16)) {
1384 if (!isWetTileAtTile(tileX, tileY-1)) {
1385 wtile.setSprite(wtile.lava ? 'sLavaTop' : 'sWaterTop');
1390 transient private array!MapTile waterTilesToCheck;
1392 final void cleanDeadTiles () {
1393 bool hasWater = false;
1394 waterTilesToCheck.length -= waterTilesToCheck.length;
1395 foreach (int y; 0..tilesHeight) {
1396 foreach (int x; 0..tilesWidth) {
1397 auto t = tiles[x, y];
1399 if (t.isInstanceAlive) {
1400 if (t.water || t.lava) waterTilesToCheck[$] = t;
1409 if (waterTilesToCheck.length && checkWater && !blockWaterChecking) {
1410 //writeln("checking water");
1411 checkWater = false; // `checkWaterFlow()` can set it again
1412 foreach (MapTile t; waterTilesToCheck) {
1413 if (t && t.isInstanceAlive && (t.water || t.lava)) checkWaterFlow(t);
1415 // fill empty spaces in lake with water
1417 foreach (int y; GreatLakeStartTileY..tilesHeight) {
1418 foreach (int x; 0..tilesWidth) {
1419 auto t = tiles[x, y];
1421 if (t && !t.isInstanceAlive) {
1427 if (!t.water || !t.lava) { t.wet = true; continue; }
1429 MakeMapTile(x, y, 'oWaterSwim');
1433 t.setSprite(y == GreatLakeStartTileY ? 'sWaterTop' : 'sWater');
1434 } else if (t.lava) {
1435 t.setSprite(y == GreatLakeStartTileY ? 'sLavaTop' : 'sLava');
1444 // ////////////////////////////////////////////////////////////////////////// //
1445 void collectLavaTiles () {
1446 lavatiles.length -= lavatiles.length;
1447 foreach (MapTile t; tiles) {
1448 if (t && t.lava && t.isInstanceAlive) lavatiles[$] = t;
1453 void processLavaTiles () {
1454 int tn = 0, tlen = lavatiles.length;
1456 MapTile t = lavatiles[tn];
1457 if (t && t.isInstanceAlive) {
1461 lavatiles.remove(tn, 1);
1468 // ////////////////////////////////////////////////////////////////////////// //
1469 // return `true` if thinker should be removed
1470 final bool thinkOne (MapObject o) {
1471 if (!o) return true;
1472 if (o.active && o.isInstanceAlive) {
1473 bool doThink = true;
1475 // collision with player weapon
1476 auto hh = PlayerWeapon(player.holdItem);
1477 bool doWeaponAction;
1479 if (hh.blockedBySolids) {
1480 int xx = player.ix+(player.dir == MapObject::Dir.Left ? -16 : 16);
1481 doWeaponAction = !isSolidAtPoint(xx, player.iy);
1483 doWeaponAction = true;
1486 doWeaponAction = false;
1489 if (doWeaponAction && o.whipTimer <= 0 && hh && hh.collidesWithObject(o)) {
1490 //writeln("WEAPONED!");
1491 if (!o.onTouchedByPlayerWeapon(player, hh)) {
1492 if (o.isInstanceAlive) hh.onCollisionWithObject(o);
1494 o.whipTimer = o.whipTimerValue; //HACK
1495 doThink = o.isInstanceAlive;
1498 // collision with player
1499 if (doThink && o.collidesWith(player)) {
1500 if (!player.onObjectTouched(o) && o.isInstanceAlive) {
1501 doThink = !o.onTouchedByPlayer(player);
1502 if (o.grid && o.gridId) o.grid.update(o.gridId, markAsDead:true);
1506 if (doThink && o.isInstanceAlive) {
1509 if (o.isInstanceAlive) {
1510 if (o.grid && o.gridId) o.grid.update(o.gridId, markAsDead:true);
1512 if (o.isInstanceAlive) {
1514 if (o.grid && o.gridId) o.grid.update(o.gridId, markAsDead:true);
1519 if (o.isInstanceAlive) {
1520 if (!o.canLiveOutsideOfLevel && !o.heldBy && o.isOutsideOfLevel()) {
1534 final void processThinkers (float timeDelta) {
1535 if (timeDelta <= 0) return;
1537 if (onBeforeFrame) onBeforeFrame(false);
1538 if (onAfterFrame) onAfterFrame(false);
1542 accumTime += timeDelta;
1543 bool wasFrame = false;
1545 auto olddel = ImmediateDelete;
1546 ImmediateDelete = false;
1547 while (accumTime >= FrameTime) {
1548 accumTime -= FrameTime;
1549 if (onBeforeFrame) onBeforeFrame(accumTime >= FrameTime);
1551 if (shakeLeft > 0) {
1553 if (!shakeDir.x) shakeDir.x = (global.randOther(0, 1) ? -3 : 3);
1554 if (!shakeDir.y) shakeDir.y = (global.randOther(0, 1) ? -3 : 3);
1555 shakeOfs.x = shakeDir.x;
1556 shakeOfs.y = shakeDir.y;
1557 int sgnc = global.randOther(1, 3);
1558 if (sgnc&0x01) shakeDir.x = -shakeDir.x;
1559 if (sgnc&0x02) shakeDir.y = -shakeDir.y;
1566 // game-global events
1568 // frame thinkers: lava tiles
1570 // frame thinkers: player
1571 if (player && !disablePlayerThink) {
1573 if (!player.dead && isNormalLevel() &&
1574 (maxPlayingTime < 0 ||
1575 (maxPlayingTime > 0 && maxPlayingTime*30 <= time &&
1576 time%30 == 0 && global.randOther(1, 100) <= 20)))
1578 MakeMapObject(player.ix, player.iy, 'oExplosion');
1580 //HACK: check for stolen items
1581 auto item = MapItem(player.holdItem);
1582 if (item) item.onCheckItemStolen(player);
1583 item = MapItem(player.pickedItem);
1584 if (item) item.onCheckItemStolen(player);
1586 player.saveInterpData();
1587 player.processAlarms();
1588 if (player.isInstanceAlive) {
1589 player.thinkFrame();
1590 if (player.isInstanceAlive) player.nextAnimFrame();
1593 // frame thinkers: moveable solids
1595 // frame thinkers: objects
1596 auto grid = objGrid;
1597 // collect active objects
1598 if (global.config.useFrozenRegion) {
1599 activeThinkerList.length -= activeThinkerList.length;
1600 foreach (MapObject o; grid.inRectPix(viewStart.x/global.scale-64, viewStart.y/global.scale-64, 320+64*2, 240+64*2, tag:grid.nextTag(), precise:false)) {
1601 activeThinkerList[$] = o;
1603 //writeln("thinkers: ", activeThinkerList.length);
1604 foreach (MapObject o; activeThinkerList) {
1606 grid.remove(o.gridId);
1613 bool killThisOne = false;
1614 for (int cid = grid.getFirstObject(); cid; cid = grid.getNextObject(cid, killThisOne)) {
1615 killThisOne = false;
1616 MapObject o = grid.getObject(MapObject, cid);
1617 if (!o) { killThisOne = true; continue; }
1618 // remove this object if it is dead
1628 if (player && player.holdItem) {
1629 if (player.holdItem.isInstanceAlive) {
1630 player.holdItem.fixHoldCoords();
1632 player.holdItem = none;
1635 // done with thinkers
1638 if (collectCounter == 0) {
1639 xmoney = max(0, xmoney-100);
1644 if (player && !player.dead) stats.oneMoreFramePlayed();
1645 if (onAfterFrame) onAfterFrame(accumTime >= FrameTime);
1648 if (!player.visible && player.holdItem) player.holdItem.visible = false;
1649 if (winCutsceneSwitchToNext) {
1650 winCutsceneSwitchToNext = false;
1651 switch (++inWinCutscene) {
1652 case 2: startWinCutsceneVolcano(); break;
1653 case 3: default: startWinCutsceneWinFall(); break;
1657 if (playerExited) break;
1659 ImmediateDelete = olddel;
1661 playerExited = false;
1665 // if we were processed at least one frame, collect garbage
1667 CollectGarbage(true); // destroy delayed objects too
1669 if (accumTime > 0 && onInterFrame) onInterFrame(accumTime/FrameTime);
1673 // ////////////////////////////////////////////////////////////////////////// //
1674 final void tileXY2roomXY (int tileX, int tileY, out int roomX, out int roomY) {
1675 roomX = (tileX-1)/RoomGen::Width;
1676 roomY = (tileY-1)/RoomGen::Height;
1680 final bool isInShop (int tileX, int tileY) {
1681 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
1682 auto n = roomType[tileX, tileY];
1683 if (n == 4 || n == 5) return true;
1684 auto t = getTileAt(tileX, tileY);
1685 if (t && t.shopWall) return true;
1686 //k8: we don't have this
1687 //if (t && t.objType == 'oShop') return true;
1693 // ////////////////////////////////////////////////////////////////////////// //
1694 override void Destroy () {
1697 delete tempSolidTile;
1702 // ////////////////////////////////////////////////////////////////////////// //
1703 final MapObject findNearestBall (int px, int py) {
1704 MapObject res = none;
1705 int curdistsq = int.max;
1706 foreach (MapObject o; ballObjects) {
1707 if (!o || o.spectral || !o.isInstanceAlive) continue;
1708 int xc = px-o.xCenter, yc = py-o.yCenter;
1709 int distsq = xc*xc+yc*yc;
1710 if (distsq < curdistsq) {
1719 final int calcNearestBallDist (int px, int py) {
1720 auto e = findNearestBall(px, py);
1721 if (!e) return int.max;
1722 int xc = px-e.xCenter, yc = py-e.yCenter;
1723 return round(sqrt(xc*xc+yc*yc));
1727 final MapObject findNearestObject (int px, int py, bool delegate (MapObject o) dg) {
1728 MapObject res = none;
1729 int curdistsq = int.max;
1730 foreach (MapObject o; objGrid.allObjects()) {
1731 if (o.spectral || !o.isInstanceAlive) continue;
1732 if (!dg(o)) continue;
1733 int xc = px-o.xCenter, yc = py-o.yCenter;
1734 int distsq = xc*xc+yc*yc;
1735 if (distsq < curdistsq) {
1744 final MapObject findNearestEnemy (int px, int py, optional bool delegate (MapEnemy o) dg) {
1745 MapObject res = none;
1746 int curdistsq = int.max;
1747 foreach (MapObject o; objGrid.allObjects()) {
1748 //k8: i added `dead` check
1749 if (o.spectral || o !isa MapEnemy || o.dead || !o.isInstanceAlive) continue;
1751 if (!dg(MapEnemy(o))) continue;
1753 int xc = px-o.xCenter, yc = py-o.yCenter;
1754 int distsq = xc*xc+yc*yc;
1755 if (distsq < curdistsq) {
1764 final MonsterShopkeeper findNearestCalmShopkeeper (int px, int py) {
1765 auto obj = MonsterShopkeeper(findNearestEnemy(px, py, delegate bool (MapEnemy o) {
1766 auto sk = MonsterShopkeeper(o);
1767 if (sk && !sk.angered) return true;
1774 final MonsterShopkeeper hasAliveShopkeepers (optional bool skipAngry) {
1775 foreach (MapObject o; objGrid.allObjects()) {
1776 auto sc = MonsterShopkeeper(o);
1777 if (!sc || o.spectral || !o.isInstanceAlive) continue;
1778 if (sc.dead) continue;
1779 if (skipAngry && sc.angered) continue;
1786 final int calcNearestEnemyDist (int px, int py, optional bool delegate (MapEnemy o) dg) {
1787 auto e = findNearestEnemy(px, py, dg!optional);
1788 if (!e) return int.max;
1789 int xc = px-e.xCenter, yc = py-e.yCenter;
1790 return round(sqrt(xc*xc+yc*yc));
1794 final int calcNearestObjectDist (int px, int py, optional bool delegate (MapObject o) dg) {
1795 auto e = findNearestObject(px, py, dg!optional);
1796 if (!e) return int.max;
1797 int xc = px-e.xCenter, yc = py-e.yCenter;
1798 return round(sqrt(xc*xc+yc*yc));
1802 final MapTile findNearestMoveableSolid (int px, int py, optional bool delegate (MapTile t) dg) {
1804 int curdistsq = int.max;
1805 foreach (MapTile t; miscTileGrid.allObjects()) {
1806 if (t.spectral || !t.isInstanceAlive) continue;
1808 if (!dg(t)) continue;
1810 if (!t.solid || !t.moveable) continue;
1812 int xc = px-t.xCenter, yc = py-t.yCenter;
1813 int distsq = xc*xc+yc*yc;
1814 if (distsq < curdistsq) {
1823 final MapTile findNearestTile (int px, int py, optional bool delegate (MapTile t) dg) {
1824 if (!dg) return none;
1826 int curdistsq = int.max;
1828 //FIXME: make this faster!
1829 foreach (MapTile t; tiles) {
1830 if (!t || t.spectral || !t.isInstanceAlive) continue;
1831 int xc = px-t.xCenter, yc = py-t.yCenter;
1832 int distsq = xc*xc+yc*yc;
1833 if (distsq < curdistsq && dg(t)) {
1839 foreach (MapTile t; miscTileGrid.allObjects()) {
1840 if (!t || t.spectral || !t.isInstanceAlive) continue;
1841 int xc = px-t.xCenter, yc = py-t.yCenter;
1842 int distsq = xc*xc+yc*yc;
1843 if (distsq < curdistsq && dg(t)) {
1853 // ////////////////////////////////////////////////////////////////////////// //
1854 final bool cbIsObjectWeb (MapObject o) { return (o isa ItemWeb); }
1855 final bool cbIsObjectBlob (MapObject o) { return (o.objName == 'oBlob'); }
1856 final bool cbIsObjectArrow (MapObject o) { return (o isa ItemProjectileArrow); }
1857 final bool cbIsObjectEnemy (MapObject o) { return (/*o != self &&*/ o isa MapEnemy); }
1859 final bool cbIsObjectTreasure (MapObject o) { return (o.isTreasure); }
1861 final bool cbIsItemObject (MapObject o) { return (o isa MapItem); }
1863 final bool cbIsItemOrEnemyObject (MapObject o) { return (o isa MapItem || o isa MapEnemy); }
1866 final MapObject isObjectAtTile (int tileX, int tileY, optional bool delegate (MapObject o) dg) {
1869 foreach (MapObject o; objGrid.inRectPix(tileX, tileY, 16, 16, objGrid.nextTag(), precise: true)) {
1870 if (o.spectral || !o.isInstanceAlive) continue;
1872 if (dg(o)) return o;
1881 final MapObject isObjectAtTilePix (int x, int y, optional bool delegate (MapObject o) dg) {
1882 return isObjectAtTile(x/16, y/16, dg!optional);
1886 final MapObject isObjectAtPoint (int xpos, int ypos, optional bool delegate (MapObject o) dg, optional bool precise) {
1887 if (!specified_precise) precise = true;
1888 foreach (MapObject o; objGrid.inCellPix(xpos, ypos, objGrid.nextTag(), precise: precise)) {
1889 if (o.spectral || !o.isInstanceAlive) continue;
1891 if (dg(o)) return o;
1893 if (o isa MapEnemy) return o;
1900 final MapObject isObjectInRect (int xpos, int ypos, int w, int h, optional bool delegate (MapObject o) dg, optional bool precise) {
1901 if (w < 1 || h < 1) return none;
1902 if (w == 1 && h == 1) return isObjectAtPoint(xpos, ypos, dg!optional, precise!optional);
1903 if (!specified_precise) precise = true;
1904 foreach (MapObject o; objGrid.inRectPix(xpos, ypos, w, h, objGrid.nextTag(), precise: precise)) {
1905 if (o.spectral || !o.isInstanceAlive) continue;
1907 if (dg(o)) return o;
1909 if (o isa MapEnemy) return o;
1916 final MapObject forEachObject (bool delegate (MapObject o) dg, optional bool allowSpectrals) {
1917 if (!dg) return none;
1919 foreach (MapObject o; objGrid.allObjects()) {
1920 if (o.spectral || !o.isInstanceAlive) continue;
1921 if (dg(o)) return o;
1924 // process gravity for moveable solids and burning for ropes
1925 auto grid = objGrid;
1926 int cid = grid.getFirstObject();
1928 MapObject o = grid.getObject(MapObject, cid);
1929 if (!o || !o.isInstanceAlive) {
1930 cid = grid.getNextObject(cid, removeThis:true);
1932 o.instanceRemove(); // just in case
1938 if (!allowSpectrals && o.spectral) {
1939 cid = grid.getNextObject(cid, removeThis:false);
1942 if (dg(o)) return o;
1943 if (o.isInstanceAlive) {
1944 cid = grid.getNextObject(cid, removeThis:false);
1946 cid = grid.getNextObject(cid, removeThis:true);
1947 o.instanceRemove(); // just in case
1956 final MapObject forEachObjectAtPoint (int xpos, int ypos, bool delegate (MapObject o) dg, optional bool precise) {
1957 if (!dg) return none;
1958 if (!specified_precise) precise = true;
1959 foreach (MapObject o; objGrid.inCellPix(xpos, ypos, objGrid.nextTag(), precise: precise)) {
1960 if (o.spectral || !o.isInstanceAlive) continue;
1961 if (dg(o)) return o;
1967 final MapObject forEachObjectInRect (int xpos, int ypos, int w, int h, bool delegate (MapObject o) dg, optional bool precise) {
1968 if (!dg || w < 1 || h < 1) return none;
1969 if (w == 1 && h == 1) return forEachObjectAtPoint(xpos, ypos, dg!optional, precise!optional);
1970 if (!specified_precise) precise = true;
1971 foreach (MapObject o; objGrid.inRectPix(xpos, ypos, w, h, objGrid.nextTag(), precise: precise)) {
1972 if (o.spectral || !o.isInstanceAlive) continue;
1973 if (dg(o)) return o;
1979 private final bool cbIsRopeTile (MapTile t) { return t.rope; }
1981 final MapTile isRopeAtPoint (int px, int py) {
1982 return checkTileAtPoint(px, py, &cbIsRopeTile);
1987 final MapTile isWaterSwimAtPoint (int px, int py) {
1988 return isWaterAtPoint(px, py);
1992 // ////////////////////////////////////////////////////////////////////////// //
1993 private array!MapObject tmpObjectList;
1995 private final bool cbCollectObjectsWithMask (MapObject t) {
1996 if (!t || !t.visible || t.spectral /*|| t.invincible*/ || !t.isInstanceAlive) return false;
1997 //auto spf = getSpriteFrame();
1998 //if (!t.sprite || t.sprite.frames.length < 1) return false;
1999 tmpObjectList[$] = t;
2004 final void touchObjectsWithMask (int x, int y, SpriteFrame frm, bool delegate (MapObject t) dg) {
2005 if (!frm || frm.bw < 1 || frm.bh < 1 || !dg) return;
2006 if (frm.isEmptyPixelMask) return;
2008 if (tmpObjectList.length) tmpObjectList.length -= tmpObjectList.length; // don't realloc
2009 if (player.isRectCollisionFrame(frm, x, y)) {
2010 //writeln("player hit");
2011 tmpObjectList[$] = player;
2014 writeln("no player hit: plr=(", player.ix, ",", player.iy, ")-(", player.ix+player.width-1, ",", player.iy+player.height-1, "); ",
2015 "frm=(", x+frm.bx, ",", y+frm.by, ")-(", x+frm.bx+frm.bw-1, ",", y+frm.by+frm.bh-1, ")");
2018 forEachObjectInRect(x+frm.bx, y+frm.by, frm.bw, frm.bh, &cbCollectObjectsWithMask);
2019 foreach (MapObject t; tmpObjectList) {
2020 if (!t || !t.isInstanceAlive || !t.visible || t.spectral) continue;
2022 auto tf = t.getSpriteFrame();
2024 //writeln("no sprite frame for ", GetClassName(t.Class));
2029 if (frm.pixelCheck(tf, t.ix-tf.xofs-x, t.iy-tf.yofs-y)) {
2030 //writeln("pixel hit for ", GetClassName(t.Class));
2034 if (t.isRectCollisionFrame(frm, x, y)) {
2041 // ////////////////////////////////////////////////////////////////////////// //
2042 final void destroyTileAt (int x, int y) {
2043 if (x < 0 || y < 0 || x >= tilesWidth*16 || y >= tilesHeight*16) return;
2046 MapTile t = tiles[x, y];
2047 if (!t || !t.visible || t.spectral || t.invincible || !t.isInstanceAlive) return;
2055 private array!MapTile tmpTileList;
2057 private final bool cbCollectTilesWithMask (MapTile t) {
2058 if (!t || !t.visible || t.spectral /*|| t.invincible*/ || !t.isInstanceAlive) return false;
2059 if (!t.sprite || t.sprite.frames.length < 1) return false;
2064 final void touchTilesWithMask (int x, int y, SpriteFrame frm, bool delegate (MapTile t) dg) {
2065 if (!frm || frm.bw < 1 || frm.bh < 1 || !dg) return;
2066 if (frm.isEmptyPixelMask) return;
2068 if (tmpTileList.length) tmpTileList.length -= tmpTileList.length; // don't realloc
2069 checkTilesInRect(x+frm.bx, y+frm.by, frm.bw, frm.bh, &cbCollectTilesWithMask);
2070 foreach (MapTile t; tmpTileList) {
2071 if (!t || !t.isInstanceAlive || !t.visible || t.spectral) continue;
2073 auto tf = t.sprite.frames[0];
2074 if (frm.pixelCheck(tf, t.ix-x, t.iy-y)) {
2076 //doCleanup = doCleanup || !t.isInstanceAlive;
2077 //writeln("dtwm at (", x, ",", y, "): dead at (", t.ix, ",", t.iy, ") : (", x/16, ",", y/16, ") : (", t.ix/16, ",", t.iy/16, ") <", GetClassName(t.Class), "> (name:", t.objName, "; type:", t.objType, ")");
2080 if (t.isRectCollisionFrame(frm, x, y)) {
2087 // ////////////////////////////////////////////////////////////////////////// //
2088 final bool cbCollisionAnySolid (MapTile t) { return t.solid; }
2089 final bool cbCollisionAnyMoveableSolid (MapTile t) { return t.solid && t.moveable; }
2090 final bool cbCollisionLiquid (MapTile t) { return (t.water || t.lava); }
2091 final bool cbCollisionAnySolidOrLiquid (MapTile t) { return (t.solid || t.water || t.lava); }
2092 final bool cbCollisionLadder (MapTile t) { return t.ladder; }
2093 final bool cbCollisionLadderTop (MapTile t) { return t.laddertop; }
2094 final bool cbCollisionAnyLadder (MapTile t) { return (t.ladder || t.laddertop); }
2095 final bool cbCollisionAnySolidIce (MapTile t) { return (t.solid && t.ice); }
2096 final bool cbCollisionWater (MapTile t) { return t.water; }
2097 final bool cbCollisionLava (MapTile t) { return t.lava; }
2098 final bool cbCollisionAnyTree (MapTile t) { return t.tree; }
2099 final bool cbCollisionPlatform (MapTile t) { return t.platform; }
2100 final bool cbCollisionLadderOrPlatform (MapTile t) { return (t.ladder || t.platform); }
2101 //final bool cbCollisionAnyHangeable (MapTile t) { return t.hangeable; } // no need to check for solids here, it is done in MapTile initializer
2102 final bool cbCollisionSpringTrap (MapTile t) { return t.springtrap; }
2103 final bool cbCollisionSpikes (MapTile t) { return t.spikes; }
2104 final bool cbCollisionExitTile (MapTile t) { return t.isExitActive(); }
2106 final bool cbCollisionForWhoa (MapTile t) { return (t.solid || t.laddertop); }
2108 //final bool cbCollisionSacAltar (MapTile t) { return (t.objName == 'oSacAltarLeft'); } // only left part, yeah
2109 final bool cbCollisionSacAltar (MapTile t) { return t.sacrificingAltar; }
2112 // ////////////////////////////////////////////////////////////////////////// //
2113 transient MapTile tempSolidTile;
2115 final MapTile checkTilesInRect (int x0, int y0, const int w, const int h, optional bool delegate (MapTile dg) dg, optional bool precise/*, optional bool dbgdump*/) {
2116 //!if (dbgdump) writeln("checkTilesInRect: (", x0, ",", y0, ")-(", x0+w-1, ",", y0+h-1, ") ; w=", w, "; h=", h);
2117 if (w < 1 || h < 1) return none;
2118 if (w == 1 && h == 1) return checkTileAtPoint(x0, y0, dg!optional);
2119 int x1 = x0+w-1, y1 = y0+h-1;
2120 if (x0 >= tilesWidth*16 || y0 >= tilesHeight*16 || x1 < 0 || y1 < 0) return none;
2122 //!if (dbgdump) writeln("default checker set");
2123 dg = &cbCollisionAnySolid;
2125 //!if (dbgdump) writeln("delegate: ", dg);
2126 int origx0 = x0, origy0 = y0;
2127 int tileSX = max(0, x0)/16;
2128 int tileSY = max(0, y0)/16;
2129 int tileEX = min(tilesWidth*16-1, x1)/16;
2130 int tileEY = min(tilesHeight*16-1, y1)/16;
2131 //!if (dbgdump) writeln(" tiles: (", tileSX, ",", tileSY, ")-(", tileEX, ",", tileEY, ")");
2132 //!!!auto grid = miscTileGrid;
2133 //!!!int tag = grid.nextTag();
2134 for (int ty = tileSY; ty <= tileEY; ++ty) {
2135 for (int tx = tileSX; tx <= tileEX; ++tx) {
2136 MapTile t = tiles[tx, ty];
2137 //!if (dbgdump && t && t.visible && t.isInstanceAlive && t.isRectCollision(origx0, origy0, w, h) ) writeln(" tile: ", GetClassName(t.Class), " : ", t.objName, " : ", t.objType, " : ", dg(t));
2138 if (t && !t.spectral && t.visible && t.isInstanceAlive && t.isRectCollision(origx0, origy0, w, h) && dg(t)) return t;
2139 // moveable tiles are in separate grid
2141 foreach (t; grid.inCellPix(tx*16, ty*16, tag, precise:precise)) {
2142 //if (t.isRectCollision(origx0, origy0, w, h) && dg(t)) return t;
2143 if (t.isInstanceAlive && !t.spectral && t.visible && t.isRectCollision(origx0, origy0, w, h) && dg(t)) return t;
2149 // moveable tiles are in separate grid
2150 foreach (MapTile t; miscTileGrid.inRectPix(x0, y0, w, h, miscTileGrid.nextTag(), precise:precise)) {
2151 //if (t.isRectCollision(origx0, origy0, w, h) && dg(t)) return t;
2152 if (t.isInstanceAlive && !t.spectral && t.visible && t.isRectCollision(origx0, origy0, w, h) && dg(t)) return t;
2155 // check walkable solid objects
2156 foreach (MapObject o; objGrid.inRectPix(x0, y0, w, h, objGrid.nextTag(), precise:precise)) {
2157 if (o && !o.spectral && o.visible && o.walkableSolid && o.isInstanceAlive && o.isRectCollision(origx0, origy0, w, h)) {
2158 if (!tempSolidTile) {
2159 tempSolidTile = SpawnObject(MapTile);
2160 } else if (!tempSolidTile.isInstanceAlive) {
2161 delete tempSolidTile;
2162 tempSolidTile = SpawnObject(MapTile);
2164 tempSolidTile.solid = true;
2165 if (dg(tempSolidTile)) return tempSolidTile;
2173 final MapTile checkTileAtPoint (int x0, int y0, optional bool delegate (MapTile dg) dg) {
2174 if (x0 < 0 || y0 < 0 || x0 >= tilesWidth*16 || y0 >= tilesHeight*16) return none;
2175 if (!dg) dg = &cbCollisionAnySolid;
2176 //if (!self) { writeln("WTF?!"); return none; }
2177 MapTile t = tiles[x0/16, y0/16];
2178 if (t && !t.spectral && t.visible && t.isInstanceAlive && t.isPointCollision(x0, y0) && dg(t)) return t;
2180 // moveable tiles are in separate grid
2181 foreach (t; miscTileGrid.inCellPix(x0, y0, miscTileGrid.nextTag(), precise:true)) {
2182 if (t.isInstanceAlive && !t.spectral && t.visible && dg(t)) return t;
2185 // check walkable solid objects
2186 foreach (MapObject o; objGrid.inCellPix(x0, y0, objGrid.nextTag(), precise:true)) {
2187 if (o && !o.spectral && o.visible && o.walkableSolid && o.isInstanceAlive && o.isRectCollision(x0, y0, 1, 1)) {
2188 if (!tempSolidTile) {
2189 tempSolidTile = SpawnObject(MapTile);
2190 } else if (!tempSolidTile.isInstanceAlive) {
2191 delete tempSolidTile;
2192 tempSolidTile = SpawnObject(MapTile);
2194 tempSolidTile.solid = true;
2195 if (dg(tempSolidTile)) return tempSolidTile;
2203 //FIXME: optimize this with clipping first
2204 //TODO: moveable tiles
2206 final MapTile checkTilesAtLine (int ax0, int ay0, int ax1, int ay1, optional bool delegate (MapTile dg) dg) {
2207 // do it faster if we can
2209 // strict vertical check?
2210 if (ax0 == ax1 && ay0 <= ay1) return checkTilesInRect(ax0, ay0, 1, ay1-ay0+1, dg!optional);
2211 // strict horizontal check?
2212 if (ay0 == ay1 && ax0 <= ax1) return checkTilesInRect(ax0, ay0, ax1-ax0+1, 1, dg!optional);
2214 float x0 = float(ax0)/16.0, y0 = float(ay0)/16.0, x1 = float(ax1)/16.0, y1 = float(ay1)/16.0;
2217 if (!dg) dg = &cbCollisionAnySolid;
2219 // get starting and enging tile
2220 int tileSX = trunc(x0), tileSY = trunc(y0);
2221 int tileEX = trunc(x1), tileEY = trunc(y1);
2223 // first hit is always landed
2224 if (tileSX >= 0 && tileSY >= 0 && tileSX < TilesWidth && tileSY < TilesHeight) {
2225 MapTile t = tiles[tileSX, tileSY];
2226 if (t && t.visible && Geom.lineAABBIntersects(ax0, ay0, ax1, ay1, tileSX*16, tileSY*16, 16, 16) && dg(t)) return t;
2229 // if starting and ending tile is the same, we don't need to do anything more
2230 if (tileSX == tileEX && tileSY == tileEY) return none;
2232 // calculate ray direction
2233 TVec dv = (vector(x1, y1)-vector(x0, y0)).normalise2d;
2235 // length of ray from one x or y-side to next x or y-side
2236 float deltaDistX = (fabs(dv.x) > 0.0001 ? fabs(1.0/dv.x) : 0.0);
2237 float deltaDistY = (fabs(dv.y) > 0.0001 ? fabs(1.0/dv.y) : 0.0);
2239 // calculate step and initial sideDists
2241 float sideDistX; // length of ray from current position to next x-side
2242 int stepX; // what direction to step in x (either +1 or -1)
2245 sideDistX = (x0-tileSX)*deltaDistX;
2248 sideDistX = (tileSX+1.0-x0)*deltaDistX;
2251 float sideDistY; // length of ray from current position to next y-side
2252 int stepY; // what direction to step in y (either +1 or -1)
2255 sideDistY = (y0-tileSY)*deltaDistY;
2258 sideDistY = (tileSY+1.0-y0)*deltaDistY;
2262 //int side; // was a NS or a EW wall hit?
2264 // jump to next map square, either in x-direction, or in y-direction
2265 if (sideDistX < sideDistY) {
2266 sideDistX += deltaDistX;
2270 sideDistY += deltaDistY;
2275 if (tileSX >= 0 && tileSY >= 0 && tileSX < TilesWidth && tileSY < TilesHeight) {
2276 MapTile t = tiles[tileSX, tileSY];
2277 if (t && t.visible && Geom.lineAABBIntersects(ax0, ay0, ax1, ay1, tileSX*16, tileSY*16, 16, 16) && dg(t)) return t;
2279 // did we arrived at the destination?
2280 if (tileSX == tileEX && tileSY == tileEY) break;
2288 // ////////////////////////////////////////////////////////////////////////// //
2289 final MapTile isSolidAtPoint (int px, int py) { return checkTileAtPoint(px, py); }
2290 final MapTile isSolidInRect (int rx, int ry, int rw, int rh) { return checkTilesInRect(rx, ry, rw, rh); }
2291 final MapTile isLadderAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadder); }
2292 final MapTile isLadderOrPlatformAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadderOrPlatform); }
2293 final MapTile isLadderTopAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadderTop); }
2294 final MapTile isAnyLadderAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyLadder); }
2295 final MapTile isWaterAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionWater); }
2296 final MapTile isLavaAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLava); }
2297 final MapTile isLiquidAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLiquid); }
2298 final MapTile isIceAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnySolidIce); }
2299 final MapTile isTreeAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyTree); }
2300 //final MapTile isHangeableAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyHangeable); }
2303 // ////////////////////////////////////////////////////////////////////////// //
2304 // PlayerPawn has it's own movement code, so don't process it here
2305 // but process moveable solids here, yeah
2306 final void physStep () {
2309 // we don't want the time to grow too large
2310 if (time > 100000000) time = 0;
2312 auto grid = miscTileGrid;
2314 // process gravity for moveable solids and burning for ropes
2315 int cid = grid.getFirstObject();
2317 MapTile t = grid.getObject(MapTile, cid);
2319 cid = grid.getNextObject(cid, removeThis:false);
2322 if (t.isInstanceAlive) {
2325 if (t.isInstanceAlive) {
2326 grid.update(cid, markAsDead:false);
2328 if (t.isInstanceAlive && !t.canLiveOutsideOfLevel && t.isOutsideOfLevel()) t.instanceRemove();
2329 grid.update(cid, markAsDead:false);
2332 if (t.isInstanceAlive) {
2333 cid = grid.getNextObject(cid, removeThis:false);
2335 cid = grid.getNextObject(cid, removeThis:true);
2336 t.instanceRemove(); // just in case
2345 // ////////////////////////////////////////////////////////////////////////// //
2346 final void setTileTypeAt (int tileX, int tileY, LevelGen::RType rt) {
2347 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) roomType[tileX, tileY] = rt;
2350 final MapTile getTileAt (int tileX, int tileY) {
2351 return (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight ? tiles[tileX, tileY] : none);
2354 final bool isNamedTileAt (int tileX, int tileY, name atypename) {
2355 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
2356 auto t = tiles[tileX, tileY];
2357 if (t && t.objName == atypename) return true;
2362 final void setTileAt (int tileX, int tileY, MapTile tile) {
2363 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
2365 if (tiles[tileX, tileY]) checkWater = true;
2366 delete tiles[tileX, tileY];
2367 tiles[tileX, tileY] = tile;
2372 // ////////////////////////////////////////////////////////////////////////// //
2373 // return `true` from delegate to stop
2374 MapTile forEachSolidTile (bool delegate (int x, int y, MapTile t) dg) {
2375 if (!dg) return none;
2376 foreach (int y; 0..tilesHeight) {
2377 foreach (int x; 0..tilesWidth) {
2378 auto t = tiles[x, y];
2379 if (t && t.solid && t.visible && t.isInstanceAlive) {
2380 if (dg(x, y, t)) return t;
2388 // ////////////////////////////////////////////////////////////////////////// //
2389 // return `true` from delegate to stop
2390 MapTile forEachNormalTile (bool delegate (int x, int y, MapTile t) dg) {
2391 if (!dg) return none;
2392 foreach (int y; 0..tilesHeight) {
2393 foreach (int x; 0..tilesWidth) {
2394 auto t = tiles[x, y];
2395 if (t && t.visible && t.isInstanceAlive) {
2396 if (dg(x, y, t)) return t;
2404 // WARNING! don't destroy tiles here! (instanceRemove() is ok, tho)
2405 MapTile forEachTile (bool delegate (MapTile t) dg) {
2406 if (!dg) return none;
2407 foreach (int y; 0..tilesHeight) {
2408 foreach (int x; 0..tilesWidth) {
2409 auto t = tiles[x, y];
2410 if (t && t.visible && !t.spectral && t.isInstanceAlive) {
2411 if (dg(t)) return t;
2416 foreach (MapObject o; miscTileGrid.allObjects()) {
2417 auto mt = MapTile(o);
2419 if (mt.visible && !mt.spectral && mt.isInstanceAlive) {
2420 //writeln("special map tile: '", GetClassName(mt.Class), "'");
2421 if (dg(mt)) return mt;
2425 auto grid = miscTileGrid;
2426 int cid = grid.getFirstObject();
2428 MapTile t = grid.getObject(MapTile, cid);
2429 if (!t || !t.isInstanceAlive) {
2430 cid = grid.getNextObject(cid, removeThis:true);
2432 t.instanceRemove(); // just in case
2438 if (!t.visible || t.spectral) {
2439 cid = grid.getNextObject(cid, removeThis:true);
2442 if (dg(t)) return t;
2443 if (t.isInstanceAlive) {
2444 cid = grid.getNextObject(cid, removeThis:false);
2446 cid = grid.getNextObject(cid, removeThis:true);
2447 t.instanceRemove(); // just in case
2456 // ////////////////////////////////////////////////////////////////////////// //
2457 final void fixWallTiles () {
2458 foreach (int y; 0..tilesHeight) {
2459 foreach (int x; 0..tilesWidth) {
2460 auto t = getTileAt(x, y);
2463 if (y == tilesHeight-2) {
2464 writeln("0: tx=", x, "; tile=", t.objName, "; ty=", y, ": (", t.iy/16, ")");
2465 } else if (y == tilesHeight-1) {
2466 writeln("1: tx=", x, "; tile=", t.objName, "; ty=", y, ": (", t.iy/16, ")");
2472 foreach (MapTile t; miscTileGrid.allObjects()) {
2473 if (t.isInstanceAlive) t.beautifyTile();
2478 // ////////////////////////////////////////////////////////////////////////// //
2479 final MapTile isCollisionAtPoint (int px, int py, optional bool delegate (MapTile dg) dg) {
2480 if (!dg) dg = &cbCollisionAnySolid;
2481 return checkTilesInRect(px, py, 1, 1, dg);
2485 // ////////////////////////////////////////////////////////////////////////// //
2486 string scrGetKaliGift (MapTile altar, optional name gift) {
2489 // find other side of the altar
2490 int sx = player.ix, sy = player.iy;
2494 auto a2 = isCollisionAtPoint(altar.ix+18, altar.iy+8, &cbCollisionSacAltar);
2495 if (!a2) a2 = isCollisionAtPoint(altar.ix-8, altar.iy+8, &cbCollisionSacAltar);
2496 if (a2) { sx = a2.ix; sy = a2.iy; }
2499 if (global.favor <= -8) res = "SHE SEEMS VERY ANGRY WITH YOU!";
2500 else if (global.favor < 0) res = "SHE SEEMS ANGRY WITH YOU.";
2501 else if (global.favor == 0) res = "SHE HAS FORGIVEN YOU!";
2502 else if (global.favor >= 32) {
2503 if (global.kaliGift >= 3 && global.favor >= 32+(global.kaliGift-2)*16) {
2504 res = "YOU FEEL INVIGORATED!";
2505 global.kaliGift += 1;
2506 global.plife += global.randOther(4, 8);
2507 } else if (global.kaliGift >= 3) {
2508 res = "SHE SEEMS ECSTATIC WITH YOU!";
2509 } else if (global.bombs < 80) {
2510 res = "YOUR SATCHEL FEELS VERY FULL NOW!";
2511 global.kaliGift = 3;
2514 res = "YOU FEEL INVIGORATED!";
2515 global.kaliGift += 1;
2516 global.plife += global.randOther(4, 8);
2518 } else if (global.favor >= 16) {
2519 if (global.kaliGift >= 2) {
2520 res = "SHE SEEMS VERY HAPPY WITH YOU!";
2522 res = "SHE BESTOWS A GIFT UPON YOU!";
2523 global.kaliGift = 2;
2525 auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2528 obj = MakeMapObject(sx, sy-8, 'oPoof');
2533 if (gift) obj = MakeMapObject(sx, sy-8, gift);
2534 if (!obj) obj = MakeMapObject(sx, sy-8, 'oKapala');
2536 } else if (global.favor >= 8) {
2537 if (global.kaliGift >= 1) {
2538 res = "SHE SEEMS HAPPY WITH YOU.";
2540 res = "SHE BESTOWS A GIFT UPON YOU!";
2541 global.kaliGift = 1;
2542 //rAltar = instance_nearest(x, y, oSacAltarRight);
2543 //if (instance_exists(rAltar)) {
2545 auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2548 obj = MakeMapObject(sx, sy-8, 'oPoof');
2552 if (gift) obj = MakeMapObject(sx, sy-8, gift);
2554 auto n = global.randOther(1, 8);
2558 if (n == 1 && !global.hasCape && !global.hasJetpack) aname = 'oCapePickup';
2559 else if (n == 2 && !global.hasGloves) aname = 'oGloves';
2560 else if (n == 3 && !global.hasSpectacles) aname = 'oSpectacles';
2561 else if (n == 4 && !global.hasMitt) aname = 'oMitt';
2562 else if (n == 5 && !global.hasSpringShoes) aname = 'oSpringShoes';
2563 else if (n == 6 && !global.hasSpikeShoes) aname = 'oSpikeShoes';
2564 else if (n == 7 && !global.hasStickyBombs) aname = 'oPaste';
2565 else if (n == 8 && !global.hasCompass) aname = 'oCompass';
2567 obj = MakeMapObject(sx, sy-8, aname);
2573 obj = MakeMapObject(sx, sy-8, (global.hasJetpack ? 'oBombBox' : 'oJetpack'));
2579 } else if (global.favor > 0) {
2580 res = "SHE SEEMS PLEASED WITH YOU.";
2585 global.message = "";
2586 res = "KALI DEVOURS YOU!"; // sacrifice is player
2594 void performSacrifice (MapObject what, MapTile where) {
2595 if (!what || !what.isInstanceAlive) return;
2596 MakeMapObject(what.ix+8, what.iy+8, 'oFlame');
2597 if (where) where.playSound('sndSmallExplode'); else what.playSound('sndSmallExplode');
2598 if (!what.bloodless) what.scrCreateBlood(what.ix+8, what.iy+8, 3);
2600 string msg = "KALI ACCEPTS THE SACRIFICE!";
2602 auto idol = ItemGoldIdol(what);
2604 ++stats.totalSacrifices;
2605 if (global.favor <= -8) msg = "SHE SEEMS VERY ANGRY WITH YOU!";
2606 else if (global.favor < 0) msg = "SHE SEEMS ANGRY WITH YOU.";
2607 else if (global.favor >= 0) {
2608 // find other side of the altar
2609 int sx = player.ix, sy = player.iy;
2614 auto a2 = isCollisionAtPoint(altar.ix+18, altar.iy+8, &cbCollisionSacAltar);
2615 if (!a2) a2 = isCollisionAtPoint(altar.ix-8, altar.iy+8, &cbCollisionSacAltar);
2616 if (a2) { sx = a2.ix; sy = a2.iy; }
2619 auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2622 obj = MakeMapObject(sx, sy-8, 'oPoof');
2626 obj = MakeMapObject(sx, sy-16-8, 'oMonkeyGold');
2628 osdMessage(msg, 6.66);
2630 idol.instanceRemove();
2634 if (global.favor <= -8) {
2635 msg = "KALI DEVOURS THE SACRIFICE!";
2636 } else if (global.favor < 0) {
2637 global.favor += (what.status == MapObject::STUNNED ? what.favor : round(what.favor/2.0)); //k8: added `round()`
2638 if (what.favor > 0) what.favor = 0;
2640 global.favor += (what.status == MapObject::STUNNED ? what.favor : round(what.favor/2.0)); //k8: added `round()`
2644 if (myAltar.gift1 != "" and global.kaliGift == 0) scrGetKaliGift(myAltar.gift1);
2645 else if (myAltar.gift2 != "" and global.kaliGift == 1) scrGetKaliGift(myAltar.gift2);
2646 else scrGetKaliGift("");
2649 // sacrifice is player?
2650 if (what isa PlayerPawn) {
2651 ++stats.totalSelfSacrifices;
2652 msg = "KALI DEVOURS YOU!";
2653 player.visible = false;
2654 player.removeBallAndChain(temp:true);
2656 player.status = MapObject::DEAD;
2658 ++stats.totalSacrifices;
2659 auto msg2 = scrGetKaliGift(where);
2660 what.instanceRemove();
2661 if (msg2) msg = va("%s\n%s", msg, msg2);
2664 osdMessage(msg, 6.66);
2666 //!if (isRealLevel()) global.totalSacrifices += 1;
2668 //!global.messageTimer = 200;
2669 //!global.shake = 10;
2673 instance_create(x, y, oFlame);
2674 playSound(global.sndSmallExplode);
2675 scrCreateBlood(x, y, 3);
2676 global.message = "KALI ACCEPTS YOUR SACRIFICE!";
2677 if (global.favor <= -8) {
2678 global.message = "KALI DEVOURS YOUR SACRIFICE!";
2679 } else if (global.favor < 0) {
2680 if (status == 98) global.favor += favor*1.5; else global.favor += favor;
2681 if (favor > 0) favor = 0;
2683 if (status == 98) global.favor += favor*1.5; else global.favor += favor;
2686 if (myAltar.gift1 != "" and global.kaliGift == 0) scrGetFavorMsg(myAltar.gift1);
2687 else if (myAltar.gift2 != "" and global.kaliGift == 1) scrGetFavorMsg(myAltar.gift2);
2688 else scrGetFavorMsg("");
2690 global.messageTimer = 200;
2697 // ////////////////////////////////////////////////////////////////////////// //
2698 final void addBackgroundGfxDetails () {
2699 // add background details
2700 //if (global.customLevel || global.parallax) return;
2702 // bg = instance_create(16*randRoom(1, 42), 16*randRoom(1, 33), oCaveBG);
2703 if (global.levelType == 1 && global.randRoom(1, 3) < 3) MakeMapBackTile('bgExtrasLush', 32*global.randRoom(0, 1), 0, 32, 32, 16*global.randRoom(1, 42), 16*global.randRoom(1, 33), 10002);
2704 else if (global.levelType == 2 && global.randRoom(1, 3) < 3) MakeMapBackTile('bgExtrasIce', 32*global.randRoom(0, 1), 0, 32, 32, 16*global.randRoom(1, 42), 16*global.randRoom(1, 33), 10002);
2705 else if (global.levelType == 3 && global.randRoom(1, 3) < 3) MakeMapBackTile('bgExtrasTemple', 32*global.randRoom(0, 1), 0, 32, 32, 16*global.randRoom(1, 42), 16*global.randRoom(1, 33), 10002);
2706 else MakeMapBackTile('bgExtras', 32*global.randRoom(0, 1), 0, 32, 32, 16*global.randRoom(1, 42), 16*global.randRoom(1, 33), 10002);
2711 // ////////////////////////////////////////////////////////////////////////// //
2712 private final void fixRealViewStart () {
2713 int scale = global.scale;
2714 realViewStart.x = clamp(realViewStart.x, viewMin.x*scale, viewMax.x*scale-viewWidth);
2715 realViewStart.y = clamp(realViewStart.y, viewMin.y*scale, viewMax.y*scale-viewHeight);
2719 final int cameraCurrX () { return realViewStart.x/global.scale; }
2720 final int cameraCurrY () { return realViewStart.y/global.scale; }
2723 private final void fixViewStart () {
2724 int scale = global.scale;
2725 viewStart.x = clamp(viewStart.x, viewMin.x*scale, viewMax.x*scale-viewWidth);
2726 viewStart.y = clamp(viewStart.y, viewMin.y*scale, viewMax.y*scale-viewHeight);
2730 final void centerViewAtPlayer () {
2731 if (viewWidth < 1 || viewHeight < 1 || !player) return;
2732 centerViewAt(player.xCenter, player.yCenter);
2736 final void centerViewAt (int x, int y) {
2737 if (viewWidth < 1 || viewHeight < 1) return;
2739 cameraSlideToSpeed.x = 0;
2740 cameraSlideToSpeed.y = 0;
2741 cameraSlideToPlayer = 0;
2743 int scale = global.scale;
2746 realViewStart.x = clamp(x-viewWidth/2, 0, tilesWidth*16*scale-viewWidth);
2747 realViewStart.y = clamp(y-viewHeight/2, 0, tilesHeight*16*scale-viewHeight);
2750 viewStart.x = realViewStart.x;
2751 viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
2756 const int ViewPortToleranceX = 16*1+8;
2757 const int ViewPortToleranceY = 16*1+8;
2759 final void fixCamera () {
2760 if (!player) return;
2761 if (viewWidth < 1 || viewHeight < 1) return;
2762 int scale = global.scale;
2763 auto alwaysCenterX = global.config.alwaysCenterPlayer;
2764 auto alwaysCenterY = alwaysCenterX;
2765 // calculate offset from viewport center (in game units), and fix viewport
2767 int camDestX = player.ix+8;
2768 int camDestY = player.iy+8;
2769 if (!cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y) != 0) {
2770 // slide camera to point
2771 if (cameraSlideToSpeed.x) camDestX = cameraSlideToCurr.x;
2772 if (cameraSlideToSpeed.y) camDestY = cameraSlideToCurr.y;
2773 int dx = cameraSlideToDest.x-camDestX;
2774 int dy = cameraSlideToDest.y-camDestY;
2775 //writeln("speed:(", cameraSlideToSpeed.x, ",", cameraSlideToSpeed.y, "); cur:(", camDestX, ",", camDestY, "); dest:(", cameraSlideToDest.x, ",", cameraSlideToDest.y, "); delta=(", dx, ",", dy, ")");
2776 if (dx && cameraSlideToSpeed.x != 0) {
2777 alwaysCenterX = true;
2778 if (abs(dx) <= abs(cameraSlideToSpeed.x)) {
2779 camDestX = cameraSlideToDest.x;
2781 camDestX += sign(dx)*abs(cameraSlideToSpeed.x);
2784 if (dy && abs(cameraSlideToSpeed.y) != 0) {
2785 alwaysCenterY = true;
2786 if (abs(dy) <= cameraSlideToSpeed.y) {
2787 camDestY = cameraSlideToDest.y;
2789 camDestY += sign(dy)*abs(cameraSlideToSpeed.y);
2792 //writeln(" new:(", camDestX, ",", camDestY, ")");
2793 if (cameraSlideToSpeed.x) cameraSlideToCurr.x = camDestX;
2794 if (cameraSlideToSpeed.y) cameraSlideToCurr.y = camDestY;
2798 if (cameraSlideToSpeed.x && !cameraSlideToPlayer) {
2799 realViewStart.x = clamp(camDestX*scale, 0, tilesWidth*16*scale-viewWidth);
2800 } else if (!player.cameraBlockX) {
2801 int x = camDestX*scale;
2802 int cx = realViewStart.x;
2803 if (alwaysCenterX) {
2806 int xofs = x-(cx+viewWidth/2);
2807 if (xofs < -ViewPortToleranceX*scale) cx += xofs+ViewPortToleranceX*scale;
2808 else if (xofs > ViewPortToleranceX*scale) cx += xofs-ViewPortToleranceX*scale;
2810 // slide back to player?
2811 if (cameraSlideToPlayer && cameraSlideToSpeed.x) {
2812 int prevx = cameraSlideToCurr.x*scale;
2813 int dx = (cx-prevx)/scale;
2814 if (abs(dx) <= cameraSlideToSpeed.x) {
2815 writeln("BACKSLIDE X COMPLETE!");
2816 cameraSlideToSpeed.x = 0;
2818 cx = prevx+(sign(dx)*cameraSlideToSpeed.x)*scale;
2819 cameraSlideToCurr.x += sign(dx)*cameraSlideToSpeed.x;
2820 if ((dx < 0 && cx <= 0) || (dx > 0 && cx >= tilesWidth*16*scale-viewWidth)) {
2821 writeln("BACKSLIDE X COMPLETE!");
2822 cameraSlideToSpeed.x = 0;
2826 realViewStart.x = clamp(cx, 0, tilesWidth*16*scale-viewWidth);
2830 if (cameraSlideToSpeed.y && !cameraSlideToPlayer) {
2831 realViewStart.y = clamp(camDestY*scale, 0, tilesHeight*16*scale-viewHeight);
2832 } else if (!player.cameraBlockY) {
2833 int y = camDestY*scale;
2834 int cy = realViewStart.y;
2835 if (alwaysCenterY) {
2836 cy = y-viewHeight/2;
2838 int yofs = y-(cy+viewHeight/2);
2839 if (yofs < -ViewPortToleranceY*scale) cy += yofs+ViewPortToleranceY*scale;
2840 else if (yofs > ViewPortToleranceY*scale) cy += yofs-ViewPortToleranceY*scale;
2842 // slide back to player?
2843 if (cameraSlideToPlayer && cameraSlideToSpeed.y) {
2844 int prevy = cameraSlideToCurr.y*scale;
2845 int dy = (cy-prevy)/scale;
2846 if (abs(dy) <= cameraSlideToSpeed.y) {
2847 writeln("BACKSLIDE Y COMPLETE!");
2848 cameraSlideToSpeed.y = 0;
2850 cy = prevy+(sign(dy)*cameraSlideToSpeed.y)*scale;
2851 cameraSlideToCurr.y += sign(dy)*cameraSlideToSpeed.y;
2852 if ((dy < 0 && cy <= 0) || (dy > 0 && cy >= tilesHeight*16*scale-viewHeight)) {
2853 writeln("BACKSLIDE Y COMPLETE!");
2854 cameraSlideToSpeed.y = 0;
2858 realViewStart.y = clamp(cy, 0, tilesHeight*16*scale-viewHeight);
2861 if (cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y) == 0) cameraSlideToPlayer = 0;
2864 //writeln(" new2:(", cameraCurrX, ",", cameraCurrY, ")");
2866 viewStart.x = realViewStart.x;
2867 viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
2872 // ////////////////////////////////////////////////////////////////////////// //
2873 // x0 and y0 are non-scaled (and will be scaled)
2874 final void drawSpriteAt (name sprName, float frnumf, int x0, int y0, optional bool small) {
2875 if (!sprName) return;
2876 auto spr = sprStore[sprName];
2877 if (!spr || !spr.frames.length) return;
2878 int scale = global.scale;
2881 int frnum = max(0, trunc(frnumf))%spr.frames.length;
2882 auto sfr = spr.frames[frnum];
2883 int sx0 = x0-sfr.xofs*scale;
2884 int sy0 = y0-sfr.yofs*scale;
2885 if (small && scale > 1) {
2886 sfr.tex.blitExt(sx0, sy0, round(sx0+sfr.tex.width*(scale/2.0)), round(sy0+sfr.tex.height*(scale/2.0)), 0, 0);
2888 sfr.tex.blitAt(sx0, sy0, scale);
2893 // x0 and y0 are non-scaled (and will be scaled)
2894 final void drawTextAt (int x0, int y0, string text) {
2896 int scale = global.scale;
2899 sprStore.renderText(x0, y0, text, scale);
2903 void renderCompass (float currFrameDelta) {
2904 if (!global.hasCompass) return;
2907 if (isRoom("rOlmec")) {
2910 } else if (isRoom("rOlmec2")) {
2916 bool hasMessage = osdHasMessage();
2917 foreach (MapTile et; allExits) {
2919 int exitX = et.ix, exitY = et.iy;
2920 int vx0 = viewStart.x/global.scale, vy0 = viewStart.y/global.scale;
2921 int vx1 = (viewStart.x+viewWidth)/global.scale;
2922 int vy1 = (viewStart.y+viewHeight)/global.scale;
2923 if (exitY > vy1-16) {
2925 drawSpriteAt(hasMessage ? 'sCompassSmallLL' : 'sCompassLL', 0, 0, 224);
2926 } else if (exitX > vx1-16) {
2927 drawSpriteAt(hasMessage ? 'sCompassSmallLR' : 'sCompassLR', 0, 304, 224);
2929 drawSpriteAt(hasMessage ? 'sCompassSmallDown' : 'sCompassDown', 0, exitX-vx0, 224);
2931 } else if (exitX < vx0) {
2932 drawSpriteAt(hasMessage ? 'sCompassSmallLeft' : 'sCompassLeft', 0, 0, exitY-vy0);
2933 } else if (exitX > vx1-16) {
2934 drawSpriteAt(hasMessage ? 'sCompassSmallRight' : 'sCompassRight', 0, 304, exitY-vy0);
2940 final bool totalsNameCmpCB (ref GameStats::TotalItem a, ref GameStats::TotalItem b) {
2941 auto sa = string(a.objName);
2942 auto sb = string(b.objName);
2946 void renderTransitionInfo (float currFrameDelta) {
2949 GameStats.sortTotalsList(stats.kills, &totalsNameCmpCB);
2952 foreach (int idx, ref auto k; stats.kills) {
2953 string s = string(k);
2954 maxLen = max(maxLen, s.length);
2958 sprStore.loadFont('sFontSmall');
2959 Video.color = 0xff_ff_00;
2960 foreach (int idx, ref auto k; stats.kills) {
2962 foreach (int xidx, ref auto d; stats.totalKills) {
2963 if (d.objName == k) { deaths = d.count; break; }
2965 //drawTextAt(16, 4+idx*8, va("%s: %d (%d)", string(k.objName).toUpperCase, k.count, deaths));
2966 drawTextAt(16, 4+idx*8, string(k).toUpperCase);
2967 drawTextAt(16+maxLen, 4+idx*8, va(": %d (%d)", k.count, deaths));
2973 void renderGhostTimer (float currFrameDelta) {
2974 if (ghostTimeLeft <= 0) return;
2975 //ghostTimeLeft /= 30; // frames -> seconds
2977 int hgt = Video.screenHeight-64;
2978 if (hgt < 1) return;
2979 int rhgt = round(float(hgt)*(float(ghostTimeLeft)/30.0)/1000.0);
2980 //writeln("hgt=", hgt, "; rhgt=", rhgt, "; gtl=", ghostTimeLeft/30);
2982 auto oclr = Video.color;
2983 Video.color = 0xcf_ff_7f_00;
2984 Video.fillRect(Video.screenWidth-20, 32, 16, hgt-rhgt);
2985 Video.color = 0x7f_ff_7f_00;
2986 Video.fillRect(Video.screenWidth-20, 32+(hgt-rhgt), 16, rhgt);
2992 void renderHUD (float currFrameDelta) {
2993 if (inWinCutscene || isTitleRoom()) return;
2995 if (levelKind == LevelKind.Transition) { renderTransitionInfo(currFrameDelta); return; }
3003 bool scumSmallHud = global.config.scumSmallHud;
3004 if (!global.config.optSGAmmo) moneyX = ammoX;
3007 sprStore.loadFont('sFontSmall');
3010 sprStore.loadFont('sFont');
3013 //int alpha = 0x6f_00_00_00;
3014 int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3015 int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3017 //Video.color = 0xff_ff_ff;
3018 Video.color = 0xff_ff_ff|talpha;
3022 if (global.plife == 1) {
3023 drawSpriteAt('sHeartSmallBlink', global.heartBlink, lifeX+2, 4-hhup);
3024 global.heartBlink += 0.1;
3025 if (global.heartBlink > 3) global.heartBlink = 0;
3027 drawSpriteAt('sHeartSmall', -1, lifeX+2, 4-hhup);
3028 global.heartBlink = 0;
3031 if (global.plife == 1) {
3032 drawSpriteAt('sHeartBlink', global.heartBlink, lifeX, 8-hhup);
3033 global.heartBlink += 0.1;
3034 if (global.heartBlink > 3) global.heartBlink = 0;
3036 drawSpriteAt('sHeart', -1, lifeX, 8-hhup);
3037 global.heartBlink = 0;
3041 int life = clamp(global.plife, 0, 99);
3042 //if (!scumHud && life > 99) life = 99;
3043 drawTextAt(lifeX+16, 8-hhup, va("%d", life));
3046 if (global.hasStickyBombs && global.stickyBombsActive) {
3047 if (scumSmallHud) drawSpriteAt('sStickyBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sStickyBombIcon', -1, bombX, 8-hhup);
3049 if (scumSmallHud) drawSpriteAt('sBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sBombIcon', -1, bombX, 8-hhup);
3051 int n = global.bombs;
3052 if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3053 drawTextAt(bombX+16, 8-hhup, va("%d", n));
3056 if (scumSmallHud) drawSpriteAt('sRopeIconSmall', -1, ropeX+2, 4-hhup); else drawSpriteAt('sRopeIcon', -1, ropeX, 8-hhup);
3058 if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3059 drawTextAt(ropeX+16, 8-hhup, va("%d", n));
3062 if (global.config.optSGAmmo && (!player || player.holdItem !isa ItemWeaponBow)) {
3063 if (scumSmallHud) drawSpriteAt('sShellsIcon', -1, ammoX+6, 8-hhup); else drawSpriteAt('sShellsIcon', -1, ammoX+7, 12-hhup);
3065 if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3066 drawTextAt(ammoX+16, 8-hhup, va("%d", n));
3067 } else if (player && player.holdItem isa ItemWeaponBow) {
3068 if (scumSmallHud) drawSpriteAt('sArrowRight', -1, ammoX+6, 8-hhup); else drawSpriteAt('sArrowRight', -1, ammoX+7, 12-hhup);
3070 if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3071 drawTextAt(ammoX+16, 8-hhup, va("%d", n));
3075 if (scumSmallHud) drawSpriteAt('sDollarSignSmall', -1, moneyX+2, 3-hhup); else drawSpriteAt('sDollarSign', -1, moneyX, 8-hhup);
3076 drawTextAt(moneyX+16, 8-hhup, va("%d", stats.money-xmoney));
3079 Video.color = 0xff_ff_ff|ialpha;
3081 int ity = (scumSmallHud ? 18-hhup : 24-hhup);
3084 if (global.hasUdjatEye) {
3085 if (global.udjatBlink) drawSpriteAt('sUdjatEyeIcon2', -1, n, ity); else drawSpriteAt('sUdjatEyeIcon', -1, n, ity);
3088 if (global.hasAnkh) { drawSpriteAt('sAnkhIcon', -1, n, ity); n += 20; }
3089 if (global.hasCrown) { drawSpriteAt('sCrownIcon', -1, n, ity); n += 20; }
3090 if (global.hasKapala) {
3091 if (global.bloodLevel == 0) drawSpriteAt('sKapalaIcon', 0, n, ity);
3092 else if (global.bloodLevel <= 2) drawSpriteAt('sKapalaIcon', 1, n, ity);
3093 else if (global.bloodLevel <= 4) drawSpriteAt('sKapalaIcon', 2, n, ity);
3094 else if (global.bloodLevel <= 6) drawSpriteAt('sKapalaIcon', 3, n, ity);
3095 else if (global.bloodLevel <= 8) drawSpriteAt('sKapalaIcon', 4, n, ity);
3098 if (global.hasSpectacles) { drawSpriteAt('sSpectaclesIcon', -1, n, ity); n += 20; }
3099 if (global.hasGloves) { drawSpriteAt('sGlovesIcon', -1, n, ity); n += 20; }
3100 if (global.hasMitt) { drawSpriteAt('sMittIcon', -1, n, ity); n += 20; }
3101 if (global.hasSpringShoes) { drawSpriteAt('sSpringShoesIcon', -1, n, ity); n += 20; }
3102 if (global.hasJordans) { drawSpriteAt('sJordansIcon', -1, n, ity); n += 20; }
3103 if (global.hasSpikeShoes) { drawSpriteAt('sSpikeShoesIcon', -1, n, ity); n += 20; }
3104 if (global.hasCape) { drawSpriteAt('sCapeIcon', -1, n, ity); n += 20; }
3105 if (global.hasJetpack) { drawSpriteAt('sJetpackIcon', -1, n, ity); n += 20; }
3106 if (global.hasStickyBombs) { drawSpriteAt('sPasteIcon', -1, n, ity); n += 20; }
3107 if (global.hasCompass) { drawSpriteAt('sCompassIcon', -1, n, ity); n += 20; }
3108 if (global.hasParachute) { drawSpriteAt('sParachuteIcon', -1, n, ity); n += 20; }
3110 if (player.holdItem && player.holdItem.objName == 'Bow' && global.arrows > 0) {
3113 while (m <= global.arrows && m <= 20 && malpha > 0) {
3114 Video.color = trunc(malpha*255)<<24|0xff_ff_ff;
3115 drawSpriteAt('sArrowIcon', -1, n, ity);
3117 if (m > 10) malpha -= 0.1; //and global.arrows > 20 malpha -= 0.1
3123 sprStore.loadFont('sFontSmall');
3124 Video.color = 0xff_ff_00|talpha;
3125 if (scumSmallHud) drawTextAt(moneyX+8, 8+8-hhup, va("+%d", xmoney));
3126 else drawTextAt(moneyX, 8+16-hhup, va("+%d", xmoney));
3129 Video.color = 0xff_ff_ff;
3130 if (global.config.ghostShowTime) renderGhostTimer(currFrameDelta);
3134 // ////////////////////////////////////////////////////////////////////////// //
3135 private transient array!MapEntity renderVisibleCids;
3136 private transient array!MapTile renderMidTiles, renderFrontTiles; // normal, with fg
3138 final bool renderSortByDepth (MapEntity oa, MapEntity ob) {
3139 //MapObject oa = MapObject(a);
3140 //MapObject ob = MapObject(b);
3141 auto da = oa.depth, db = ob.depth;
3142 if (da == db) return (oa.objId < ob.objId);
3147 const int RenderEdgePixNormal = 64;
3148 const int RenderEdgePixLight = 256;
3150 void renderWithOfs (int xofs, int yofs, float currFrameDelta) {
3151 int scale = global.scale;
3154 // don't touch framebuffer alpha
3155 Video.colorMask = Video::CMask.Colors;
3157 Video.color = 0xff_ff_ff;
3159 // render cave background
3161 int bgw = levBGImg.tex.width*scale;
3162 int bgh = levBGImg.tex.height*scale;
3163 int bgTW = (tilesWidth*tsz+bgw-1)/bgw;
3164 int bgTH = (tilesHeight*tsz+bgh-1)/bgh;
3165 int bgX0 = max(0, xofs/bgw);
3166 int bgY0 = max(0, yofs/bgh);
3167 int bgX1 = min(bgTW, (xofs+viewWidth+bgw-1)/bgw);
3168 int bgY1 = min(bgTH, (yofs+viewHeight+bgh-1)/bgh);
3169 foreach (int ty; bgY0..bgY1) {
3170 foreach (int tx; bgX0..bgX1) {
3171 int x0 = tx*bgw-xofs;
3172 int y0 = ty*bgh-yofs;
3173 levBGImg.tex.blitAt(x0, y0, scale);
3178 int RenderEdgePix = (global.darkLevel ? RenderEdgePixLight : RenderEdgePixNormal);
3180 // render background tiles
3181 for (MapBackTile bt = backtiles; bt; bt = bt.next) {
3182 bt.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3185 // collect visible special tiles
3186 renderVisibleCids.length -= renderVisibleCids.length;
3187 foreach (MapTile mt; miscTileGrid.inRectPix(xofs/scale-RenderEdgePix, yofs/scale-RenderEdgePix, (viewWidth+scale-1)/scale+RenderEdgePix*2, (viewHeight+scale-1)/scale+RenderEdgePix*2, tag:miscTileGrid.nextTag(), precise:false)) {
3188 if (!mt.visible || !mt.isInstanceAlive) continue;
3189 //Video.color = (mt.moveable ? 0xff_7f_00 : 0xff_ff_ff);
3190 //!mt.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3191 renderVisibleCids[$] = mt;
3194 if (player && player.visible) renderVisibleCids[$] = player; // include player in render list
3195 // collect visible objects
3196 auto ogrid = objGrid;
3197 foreach (MapObject o; ogrid.inRectPix(xofs/scale-RenderEdgePix, yofs/scale-RenderEdgePix, (viewWidth+scale-1)/scale+RenderEdgePix*2, (viewHeight+scale-1)/scale+RenderEdgePix*2, tag:ogrid.nextTag(), precise:false)) {
3198 if (o.visible && o.isInstanceAlive) renderVisibleCids[$] = o;
3201 // collect stationary tiles
3202 int tileX0 = max(0, xofs/tsz);
3203 int tileY0 = max(0, yofs/tsz);
3204 int tileX1 = min(tilesWidth, (xofs+viewWidth+tsz-1)/tsz);
3205 int tileY1 = min(tilesHeight, (yofs+viewHeight+tsz-1)/tsz);
3207 // render backs; collect tile arrays
3208 renderMidTiles.length -= renderMidTiles.length; // don't realloc
3209 renderFrontTiles.length -= renderFrontTiles.length; // don't realloc
3211 foreach (int ty; tileY0..tileY1) {
3212 foreach (int tx; tileX0..tileX1) {
3213 auto tile = getTileAt(tx, ty);
3214 if (tile && tile.visible && tile.isInstanceAlive) {
3215 renderMidTiles[$] = tile;
3216 if (tile.bgfront) renderFrontTiles[$] = tile;
3217 if (tile.bgback) tile.drawWithOfsBack(xofs, yofs, scale, currFrameDelta);
3222 // collect "mid" (i.e. normal) tiles
3223 foreach (MapTile tile; renderMidTiles) {
3224 //tile.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3225 renderVisibleCids[$] = tile;
3228 EntityGrid.sortEntList(renderVisibleCids, &renderSortByDepth);
3230 auto depth4Start = 0;
3231 foreach (auto xidx, MapEntity o; renderVisibleCids) {
3238 // render objects (part one: depth > 3)
3239 foreach (auto idx; depth4Start..renderVisibleCids.length; reverse) {
3240 MapEntity o = renderVisibleCids[idx];
3241 //if (idx && renderSortByDepth(o, renderVisibleCids[idx-1])) writeln("VIOLATION!");
3242 o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3245 // render object (part two: front tile parts, depth 3.5)
3246 foreach (MapTile tile; renderFrontTiles) tile.drawWithOfsFront(xofs, yofs, scale, currFrameDelta);
3248 // render objects (part three: depth <= 3)
3249 foreach (auto idx; 0..depth4Start; reverse) {
3250 MapEntity o = renderVisibleCids[idx];
3251 o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3254 // render player post-effects, so cape or jetpack will be correctly rendered on level exit, for example
3255 player.lastDrawWithOfs(xofs, yofs, scale, currFrameDelta);
3258 if (global.darkLevel) {
3259 if (global.config.scumDarkness >= 2) player.lightRadius = 96;
3260 else player.lightRadius = 32; //40;
3262 auto ltex = bgtileStore.lightTexture('ltx512', 512);
3264 // set screen alpha to min
3265 Video.colorMask = Video::CMask.Alpha;
3266 Video.blendMode = Video::BlendMode.None;
3267 Video.color = 0xff_ff_ff_ff;
3268 Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
3269 //Video.colorMask = Video::CMask.All;
3272 // also, stencil 'em, so we can filter dark areas
3273 Video.textureFiltering = true;
3274 Video.stencil = true;
3275 Video.stencilFunc(Video::StencilFunc.Always, 1);
3276 Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Replace);
3277 Video.alphaTestFunc = Video::AlphaFunc.Greater;
3278 Video.alphaTestVal = 0.03;
3279 Video.color = 0xff_ff_ff;
3280 Video.blendFunc = Video::BlendFunc.Max;
3281 Video.blendMode = Video::BlendMode.Blend; // anything except `Normal`
3282 Video.colorMask = Video::CMask.Alpha;
3284 foreach (MapEntity e; renderVisibleCids) {
3285 int lrad = e.lightRadius;
3286 if (lrad < 4) continue;
3288 float lightscale = float(lrad*scale)/float(ltex.tex.width);
3290 e.getInterpCoords(currFrameDelta, scale, out xi, out yi);
3291 #ifdef OLD_LIGHT_OFFSETS
3292 int fx0, fy0, fx1, fy1;
3294 auto spf = e.getSpriteFrame(out doMirror, out fx0, out fy0, out fx1, out fy1);
3296 xi += (fx1-fx0)*scale/2;
3297 yi += (fy1-fy0)*scale/2;
3301 e.getLightOffset(out lxofs, out lyofs);
3306 lrad = lrad*scale/2;
3309 ltex.tex.blitAt(xi, yi, lightscale);
3310 //ltex.tex.blitAt(xi-xofs, yi-yofs, lightscale);
3312 Video.textureFiltering = false;
3314 // modify only lit parts
3315 Video.stencilFunc(Video::StencilFunc.Equal, 1);
3316 Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Keep);
3317 // multiply framebuffer colors by framebuffer alpha
3318 Video.color = 0xff_ff_ff; // it doesn't matter
3319 Video.blendFunc = Video::BlendFunc.Add;
3320 Video.blendMode = Video::BlendMode.DstMulDstAlpha;
3321 Video.colorMask = Video::CMask.Colors;
3322 Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
3324 // filter unlit parts
3325 Video.stencilFunc(Video::StencilFunc.NotEqual, 1);
3326 Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Keep);
3327 Video.blendFunc = Video::BlendFunc.Add;
3328 Video.blendMode = Video::BlendMode.Filter;
3329 Video.colorMask = Video::CMask.Colors;
3330 Video.color = 0x00_00_18;
3331 Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
3334 Video.blendFunc = Video::BlendFunc.Add;
3335 Video.blendMode = Video::BlendMode.Normal;
3336 Video.colorMask = Video::CMask.All;
3337 Video.alphaTestFunc = Video::AlphaFunc.Always;
3338 Video.stencil = false;
3341 // clear visible objects list
3342 renderVisibleCids.length -= renderVisibleCids.length;
3345 if (global.config.drawHUD) renderHUD(currFrameDelta);
3346 renderCompass(currFrameDelta);
3348 float osdTimeLeft, osdTimeStart;
3349 string msg = osdGetMessage(out osdTimeLeft, out osdTimeStart);
3351 auto ct = GetTickCount();
3353 sprStore.loadFont('sFontSmall');
3354 auto msgHeight = sprStore.getMultilineTextHeight(msg, msgScale);
3355 int x = Video.screenWidth/2;
3356 int y = Video.screenHeight-64-msgHeight;
3357 auto oldColor = Video.color;
3358 Video.color = 0xff_ff_00;
3359 if (osdTimeLeft < 0.5) {
3360 int alpha = 255-clamp(int(510*osdTimeLeft), 0, 255);
3361 Video.color = Video.color|(alpha<<24);
3362 } else if (ct-osdTimeStart < 0.5) {
3363 osdTimeStart = ct-osdTimeStart;
3364 int alpha = 255-clamp(int(510*osdTimeStart), 0, 255);
3365 Video.color = Video.color|(alpha<<24);
3367 sprStore.renderMultilineTextCentered(x, y, msg, msgScale);
3368 Video.color = oldColor;
3371 if (inWinCutscene) renderWinCutsceneOverlay();
3372 Video.color = 0xff_ff_ff;
3376 // ////////////////////////////////////////////////////////////////////////// //
3377 final class!MapObject findGameObjectClassByName (name aname) {
3378 if (!aname) return none; // just in case
3379 auto co = FindClassByGameObjName(aname);
3381 writeln("***UNKNOWN GAME OBJECT: '", aname, "'");
3384 co = GetClassReplacement(co);
3385 if (!co) FatalError("findGameObjectClassByName: WTF?!");
3386 if (!class!MapObject(co)) FatalError("findGameTileClassByName: object '%n' is not a map object, it is a `%n`", aname, GetClassName(co));
3387 return class!MapObject(co);
3391 final class!MapTile findGameTileClassByName (name aname) {
3392 if (!aname) return none; // just in case
3393 auto co = FindClassByGameObjName(aname);
3394 if (!co) return MapTile; // unknown names will be routed directly to tile object
3395 co = GetClassReplacement(co);
3396 if (!co) FatalError("findGameTileClassByName: WTF?!");
3397 if (!class!MapTile(co)) FatalError("findGameTileClassByName: object '%n' is not a tile, it is a `%n`", aname, GetClassName(co));
3398 return class!MapTile(co);
3402 final MapObject findAnyObjectOfType (name aname) {
3403 if (!aname) return none;
3404 auto cls = FindClassByGameObjName(aname);
3405 if (!cls) return none;
3406 for (auto cid = objGrid.getFirstObject(); cid; cid = objGrid.getNextObject(cid)) {
3407 MapObject obj = objGrid.getObject(MapObject, cid);
3408 if (!obj || obj.spectral || !obj.isInstanceAlive) continue;
3409 if (obj isa cls) return obj;
3415 // ////////////////////////////////////////////////////////////////////////// //
3416 final MapTile CreateMapTile (int xpos, int ypos, name aname) {
3417 if (!aname) FatalError("cannot create typeless tile");
3418 //MapTile tile = SpawnObject(aname == 'oRope' ? MapTileRope : MapTile);
3419 auto tclass = findGameTileClassByName(aname);
3420 if (!tclass) return none;
3421 MapTile tile = SpawnObject(tclass);
3422 tile.global = global;
3424 tile.objName = aname;
3425 tile.objType = aname; // just in case
3428 if (!tile.initialize()) FatalError("cannot create tile '%n'", aname);
3433 final bool isRopePlacedAt (int x, int y) {
3435 foreach (ref auto v; covered) v = false;
3436 foreach (MapTile t; miscTileGrid.inRectPix(x, y-8, 1, 17, precise:false)) {
3437 if (!cbIsRopeTile(t)) continue;
3438 if (t.ix != x) continue;
3439 if (t.iy == y) return true;
3440 foreach (int ty; t.iy..t.iy+8) {
3442 if (d >= 0 && d < covered.length) covered[d] = true;
3445 // check if the whole rope height is completely covered with ropes
3446 foreach (auto v; covered) if (!v) return false;
3451 // won't call `onDestroy()`
3452 final void RemoveMapTile (int tileX, int tileY) {
3453 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
3454 if (tiles[tileX, tileY]) checkWater = true;
3455 delete tiles[tileX, tileY];
3456 tiles[tileX, tileY] = none;
3461 final MapTile MakeMapTile (int mapx, int mapy, name aname/*, optional name sprname*/) {
3462 //if (mapy >= tilesHeight) { writeln("OOM: ", aname, "; ty=", mapy, "; hgt=", tilesHeight); }
3463 if (mapx < 0 || mapx >= tilesWidth || mapy < 0 || mapy >= tilesHeight) return none;
3465 // if we already have rope tile there, there is no reason to add another one
3466 if (aname == 'oRope') {
3467 if (isRopePlacedAt(mapx*16, mapy*16)) {
3468 //writeln("dupe rope (0)!");
3473 auto tile = CreateMapTile(mapx*16, mapy*16, aname);
3474 if (tile.moveable || tile.toSpecialGrid) {
3475 // moveable tiles goes to the separate list
3476 miscTileGrid.insert(tile);
3478 setTileAt(mapx, mapy, tile);
3483 case 'oEntrance': registerEnter(tile); break;
3484 case 'oExit': registerExit(tile); break;
3487 if (tile.enter) registerEnter(tile);
3488 if (tile.exit) registerExit(tile);
3494 final void MarkTileAsWet (int tileX, int tileY) {
3495 auto t = getTileAt(tileX, tileY);
3496 if (t) t.wet = true;
3500 final MapTile MakeMapTileAtPix (int xpix, int ypix, name aname/*, optional name sprname*/) {
3501 if (xpix%16 == 0 && ypix%16 == 0) return MakeMapTile(xpix/16, ypix/16, aname);
3502 //if (mapx < 0 || mapx >= TilesWidth || mapy < 0 || mapy >= TilesHeight) return none;
3504 // if we already have rope tile there, there is no reason to add another one
3505 if (aname == 'oRope') {
3506 if (isRopePlacedAt(xpix, ypix)) {
3507 //writeln("dupe rope (0)!");
3512 auto tile = CreateMapTile(xpix, ypix, aname);
3513 // non-aligned tiles goes to the special grid
3514 miscTileGrid.insert(tile);
3517 case 'oEntrance': registerEnter(tile); break;
3518 case 'oExit': registerExit(tile); break;
3525 final MapTile MakeMapRopeTileAt (int x0, int y0) {
3526 // if we already have rope tile there, there is no reason to add another one
3527 if (isRopePlacedAt(x0, y0)) {
3528 //writeln("dupe rope (1)!");
3532 auto tile = CreateMapTile(x0, y0, 'oRope');
3533 miscTileGrid.insert(tile);
3539 // ////////////////////////////////////////////////////////////////////////// //
3540 final MapBackTile CreateBgTile (name sprName, optional int atx0, optional int aty0, optional int aw, optional int ah) {
3541 BackTileImage img = bgtileStore[sprName];
3542 auto res = SpawnObject(MapBackTile);
3543 res.global = global;
3546 res.bgtName = sprName;
3547 if (specified_atx0) res.tx0 = atx0;
3548 if (specified_aty0) res.ty0 = aty0;
3549 if (specified_aw) res.w = aw;
3550 if (specified_ah) res.h = ah;
3551 if (!res.initialize()) FatalError("cannot create background tile with image '%n'", sprName);
3556 // ////////////////////////////////////////////////////////////////////////// //
3558 background The background asset from which the new tile will be extracted.
3559 left The x coordinate of the left of the new tile, relative to the background asset's top left corner.
3560 top The y coordinate of the top of the new tile, relative to the background assets top left corner.
3561 width The width of the tile.
3562 height The height of the tile.
3563 x The x position in the room to place the tile.
3564 y The y position in the room to place the tile.
3565 depth The depth at which to place the tile.
3567 final void MakeMapBackTile (name bgname, int left, int top, int width, int height, int x, int y, int depth) {
3568 if (width < 1 || height < 1 || !bgname) return;
3569 auto bgt = bgtileStore[bgname];
3570 if (!bgt) FatalError("cannot load background '%n'", bgname);
3571 MapBackTile bt = SpawnObject(MapBackTile);
3574 bt.objName = bgname;
3576 bt.bgtName = bgname;
3584 // find a place for it
3589 // back tiles with the highest depth should come first
3590 MapBackTile ct = backtiles, cprev = none;
3591 while (ct && depth <= ct.depth) { cprev = ct; ct = ct.next; }
3594 bt.next = cprev.next;
3597 bt.next = backtiles;
3603 // ////////////////////////////////////////////////////////////////////////// //
3604 final MapObject SpawnMapObjectWithClass (class!MapObject oclass) {
3605 if (!oclass) return none;
3607 MapObject obj = SpawnObject(oclass);
3608 if (!obj) FatalError("cannot create map object '%n'", GetClassName(oclass));
3610 //if (aname == 'oShells' || aname == 'oShellSingle' || aname == 'oRopePile') writeln("*** CREATED '", aname, "'");
3612 obj.global = global;
3619 final MapObject SpawnMapObject (name aname) {
3620 if (!aname) return none;
3621 return SpawnMapObjectWithClass(findGameObjectClassByName(aname));
3625 final MapObject PutSpawnedMapObject (int x, int y, MapObject obj) {
3626 if (!obj /*|| obj.global || obj.level*/) return none; // oops
3630 if (!obj.initialize()) { delete obj; return none; } // not fatal
3638 final MapObject MakeMapObject (int x, int y, name aname) {
3639 MapObject obj = SpawnMapObject(aname);
3640 obj = PutSpawnedMapObject(x, y, obj);
3645 // ////////////////////////////////////////////////////////////////////////// //
3646 int winCutSceneTimer = -1;
3647 int winVolcanoTimer = -1;
3648 int winCutScenePhase = 0;
3649 int winSceneDrawStatus = 0;
3650 int winMoneyCount = 0;
3652 bool winFadeOut = false;
3653 int winFadeLevel = 0;
3654 int winCutsceneSkip = 0; // 1: waiting for pay release; 2: pay released, do skip
3655 bool winCutsceneSwitchToNext = false;
3658 void startWinCutscene () {
3659 global.hasParachute = false;
3661 winCutsceneSwitchToNext = false;
3662 winCutsceneSkip = 0;
3663 isKeyPressed(GameConfig::Key.Pay);
3664 isKeyReleased(GameConfig::Key.Pay);
3666 auto olddel = ImmediateDelete;
3667 ImmediateDelete = false;
3673 addBackgroundGfxDetails();
3675 levBGImgName = 'bgCave';
3676 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
3678 blockWaterChecking = true;
3684 ImmediateDelete = olddel;
3685 CollectGarbage(true); // destroy delayed objects too
3687 if (dumpGridStats) {
3688 miscTileGrid.dumpStats();
3689 objGrid.dumpStats();
3692 playerExited = false; // just in case
3700 winCutSceneTimer = -1;
3701 winCutScenePhase = 0;
3704 if (global.config.gameMode != GameConfig::GameMode.Vanilla) {
3705 if (global.config.bizarre) {
3706 global.yasmScore = 1;
3707 global.config.bizarrePlusTitle = true;
3710 array!MapTile toReplace;
3711 forEachTile(delegate bool (MapTile t) {
3712 if (t.objType == 'oGTemple' ||
3713 t.objType == 'oIce' ||
3714 t.objType == 'oDark' ||
3715 t.objType == 'oBrick' ||
3716 t.objType == 'oLush')
3723 foreach (MapTile t; miscTileGrid.allObjects()) {
3724 if (t.objType == 'oGTemple' ||
3725 t.objType == 'oIce' ||
3726 t.objType == 'oDark' ||
3727 t.objType == 'oBrick' ||
3728 t.objType == 'oLush')
3734 foreach (MapTile t; toReplace) {
3736 t.cleanDeath = true;
3737 if (rand(1,120) == 1) instance_change(oGTemple, false);
3738 else if (rand(1,100) == 1) instance_change(oIce, false);
3739 else if (rand(1,90) == 1) instance_change(oDark, false);
3740 else if (rand(1,80) == 1) instance_change(oBrick, false);
3741 else if (rand(1,70) == 1) instance_change(oLush, false);
3749 if (rand(1,5) == 1) instance_change(oLush, false);
3754 //!instance_create(0, 0, oBricks);
3756 //shakeToggle = false;
3757 //oPDummy.status = 2;
3762 if (global.kaliPunish >= 2) {
3763 instance_create(oPDummy.x, oPDummy.y+2, oBall2);
3764 obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
3766 obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
3768 obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
3770 obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
3777 void startWinCutsceneVolcano () {
3778 global.hasParachute = false;
3780 writeln("VOLCANO HOLD 0: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
3781 writeln("VOLCANO PICK 0: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
3785 winCutsceneSwitchToNext = false;
3786 auto olddel = ImmediateDelete;
3787 ImmediateDelete = false;
3792 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
3794 blockWaterChecking = true;
3796 ImmediateDelete = olddel;
3797 CollectGarbage(true); // destroy delayed objects too
3799 spawnPlayerAt(2*16+8, 11*16+8);
3800 player.dir = MapEntity::Dir.Right;
3802 playerExited = false; // just in case
3810 winCutSceneTimer = -1;
3811 winCutScenePhase = 0;
3813 MakeMapTile(0, 0, 'oEnd2BG');
3814 realViewStart.x = 0;
3815 realViewStart.y = 0;
3824 player.dead = false;
3825 player.active = true;
3826 player.visible = false;
3827 player.removeBallAndChain(temp:true);
3828 player.stunned = false;
3829 player.status = MapObject::FALLING;
3830 if (player.holdItem) player.holdItem.visible = false;
3831 player.fltx = 320/2;
3835 writeln("VOLCANO HOLD 1: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
3836 writeln("VOLCANO PICK 1: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
3841 void startWinCutsceneWinFall () {
3842 global.hasParachute = false;
3844 writeln("FALL HOLD 0: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
3845 writeln("FALL PICK 0: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
3849 winCutsceneSwitchToNext = false;
3851 auto olddel = ImmediateDelete;
3852 ImmediateDelete = false;
3857 setMenuTilesVisible(false);
3859 //addBackgroundGfxDetails();
3862 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
3864 blockWaterChecking = true;
3870 ImmediateDelete = olddel;
3871 CollectGarbage(true); // destroy delayed objects too
3873 if (dumpGridStats) {
3874 miscTileGrid.dumpStats();
3875 objGrid.dumpStats();
3878 playerExited = false; // just in case
3886 winCutSceneTimer = -1;
3887 winCutScenePhase = 0;
3889 player.dead = false;
3890 player.active = true;
3891 player.visible = false;
3892 player.removeBallAndChain(temp:true);
3893 player.stunned = false;
3894 player.status = MapObject::FALLING;
3895 if (player.holdItem) player.holdItem.visible = false;
3896 player.fltx = 320/2;
3899 winSceneDrawStatus = 0;
3906 writeln("FALL HOLD 1: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
3907 writeln("FALL PICK 1: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
3912 void setGameOver () {
3913 if (inWinCutscene) {
3914 player.visible = false;
3915 player.removeBallAndChain(temp:true);
3916 if (player.holdItem) player.holdItem.visible = false;
3919 if (inWinCutscene > 0) {
3922 winSceneDrawStatus = 8;
3927 MapTile findEndPlatTile () {
3928 return forEachTile(delegate bool (MapTile t) { return (t isa MapTileEndPlat); });
3932 MapObject findBigTreasure () {
3933 return forEachObject(delegate bool (MapObject o) { return (o isa MapObjectBigTreasure); });
3937 void setMenuTilesVisible (bool vis) {
3939 forEachTile(delegate bool (MapTile t) {
3940 if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
3941 t.invisible = false;
3946 forEachTile(delegate bool (MapTile t) {
3947 if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
3956 void setMenuTilesOnTop () {
3957 forEachTile(delegate bool (MapTile t) {
3958 if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
3966 void winCutscenePlayerControl (PlayerPawn plr) {
3967 auto payPress = isKeyPressed(GameConfig::Key.Pay);
3968 auto payRelease = isKeyReleased(GameConfig::Key.Pay);
3970 switch (winCutsceneSkip) {
3971 case 0: // nothing was pressed
3972 if (payPress) winCutsceneSkip = 1;
3974 case 1: // waiting for pay release
3975 if (payRelease) winCutsceneSkip = 2;
3977 case 2: // pay released, do skip
3982 // first winning room
3983 if (inWinCutscene == 1) {
3984 if (plr.ix < 448+8) {
3989 // waiting for chest to open
3990 if (winCutScenePhase == 0) {
3991 winCutSceneTimer = 120/2;
3992 winCutScenePhase = 1;
3997 if (winCutScenePhase == 1) {
3998 if (--winCutSceneTimer == 0) {
3999 winCutScenePhase = 2;
4000 winCutSceneTimer = 20;
4001 forEachObject(delegate bool (MapObject o) {
4002 if (o isa MapObjectBigChest) {
4003 o.setSprite(global.config.gameMode == GameConfig::GameMode.Vanilla ? 'sBigChestOpen' : 'sBigChestOpen2');
4004 auto treasure = MakeMapObject(o.ix, o.iy, 'oBigTreasure');
4008 o.playSound('sndClick');
4009 //!!!if (global.config.gameMode != GameConfig::GameMode.Vanilla) scrSprayGems(oBigChest.x+24, oBigChest.y+24);
4019 if (winCutScenePhase == 2) {
4020 if (--winCutSceneTimer == 0) {
4021 winCutScenePhase = 3;
4022 winCutSceneTimer = 50;
4028 if (winCutScenePhase == 3) {
4029 auto ep = findEndPlatTile();
4030 if (ep) MakeMapObject(ep.ix+global.randOther(0, 80), /*ep.iy*/192+32, 'oBurn');
4031 if (--winCutSceneTimer == 0) {
4032 winCutScenePhase = 4;
4033 winCutSceneTimer = 10;
4034 if (ep) MakeMapObject(ep.ix, ep.iy+30, 'oLavaSpray');
4040 // lava pump first accel
4041 if (winCutScenePhase == 4) {
4042 if (--winCutSceneTimer == 0) {
4043 forEachObject(delegate bool (MapObject o) {
4044 if (o isa MapObjectLavaSpray) o.yAcc = -0.1;
4050 // lava pump complete
4051 if (winCutScenePhase == 5) {
4052 if (--winCutSceneTimer == 0) {
4053 //if (oLavaSpray) oLavaSpray.yAcc = -0.1;
4054 startWinCutsceneVolcano();
4063 if (inWinCutscene == 2) {
4067 if (winCutScenePhase == 0) {
4068 winCutSceneTimer = 50;
4069 winCutScenePhase = 1;
4070 winVolcanoTimer = 10;
4074 if (winVolcanoTimer > 0) {
4075 if (--winVolcanoTimer == 0) {
4076 MakeMapObject(224+global.randOther(0,48), 144+global.randOther(0,8), 'oVolcanoFlame');
4077 winVolcanoTimer = global.randOther(10, 20);
4082 if (winCutScenePhase == 1) {
4083 if (--winCutSceneTimer == 0) {
4084 winCutSceneTimer = 30;
4085 winCutScenePhase = 2;
4086 auto sil = MakeMapObject(240, 132, 'oPlayerSil');
4094 if (winCutScenePhase == 2) {
4095 if (--winCutSceneTimer == 0) {
4096 winCutScenePhase = 3;
4097 auto sil = MakeMapObject(240, 132, 'oTreasureSil');
4107 // winning camel room
4108 if (inWinCutscene == 3) {
4109 //if (!player.holdItem) writeln("SCENE 3: LOST ITEM!");
4111 if (!plr.visible) plr.flty = -32;
4114 if (winCutScenePhase == 0) {
4115 winCutSceneTimer = 50;
4116 winCutScenePhase = 1;
4121 if (winCutScenePhase == 1) {
4122 if (--winCutSceneTimer == 0) {
4123 winCutSceneTimer = 50;
4124 winCutScenePhase = 2;
4125 plr.playSound('sndPFall');
4128 writeln("MUST BE CHAINED: ", plr.mustBeChained);
4129 if (plr.mustBeChained) {
4130 plr.removeBallAndChain(temp:true);
4131 plr.spawnBallAndChain();
4134 writeln("HOLD: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4135 writeln("PICK: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4137 if (!player.holdItem && player.pickedItem) player.scrSwitchToPocketItem(forceIfEmpty:false);
4138 if (player.holdItem) {
4139 player.holdItem.visible = true;
4140 player.holdItem.canLiveOutsideOfLevel = true;
4141 writeln("HOLD ITEM: '", GetClassName(player.holdItem.Class), "'");
4143 plr.status == MapObject::FALLING;
4144 global.plife += 99; // just in case
4149 if (winCutScenePhase == 2) {
4150 auto ball = plr.getMyBall();
4151 if (ball && plr.holdItem != ball) {
4152 ball.teleportTo(plr.fltx, plr.flty+8);
4156 if (plr.status == MapObject::STUNNED || plr.stunned) {
4160 auto treasure = MakeMapObject(144+16+8, -32, 'oBigTreasure');
4161 if (treasure) treasure.depth = 1;
4162 winCutScenePhase = 3;
4164 plr.playSound('sndTFall');
4169 if (winCutScenePhase == 3) {
4170 if (plr.status != MapObject::STUNNED && !plr.stunned) {
4171 auto bt = findBigTreasure();
4175 //plr.status = MapObject::JUMPING;
4177 plr.kJumpPressed = true;
4178 winCutScenePhase = 4;
4179 winCutSceneTimer = 50;
4186 if (winCutScenePhase == 4) {
4187 if (--winCutSceneTimer == 0) {
4188 setMenuTilesVisible(true);
4189 winCutScenePhase = 5;
4190 winSceneDrawStatus = 1;
4191 global.playMusic('musVictory', loop:false);
4192 winCutSceneTimer = 50;
4197 if (winCutScenePhase == 5) {
4198 if (winSceneDrawStatus == 3) {
4199 int money = stats.money;
4200 if (winMoneyCount < money) {
4201 if (money-winMoneyCount > 1000) {
4202 winMoneyCount += 1000;
4203 } else if (money-winMoneyCount > 100) {
4204 winMoneyCount += 100;
4205 } else if (money-winMoneyCount > 10) {
4206 winMoneyCount += 10;
4211 if (winMoneyCount >= money) {
4212 winMoneyCount = money;
4213 ++winSceneDrawStatus;
4218 if (winSceneDrawStatus == 7) {
4221 if (winFadeLevel >= 255) {
4222 ++winSceneDrawStatus;
4223 winCutSceneTimer = 30*30;
4228 if (winSceneDrawStatus == 8) {
4229 if (--winCutSceneTimer == 0) {
4235 if (--winCutSceneTimer == 0) {
4236 ++winSceneDrawStatus;
4237 winCutSceneTimer = 50;
4246 // ////////////////////////////////////////////////////////////////////////// //
4247 void renderWinCutsceneOverlay () {
4248 if (inWinCutscene == 3) {
4249 if (winSceneDrawStatus > 0) {
4250 Video.color = 0xff_ff_ff;
4251 sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
4252 //draw_set_color(txtCol);
4253 drawTextAt(64, 32, "YOU MADE IT!");
4255 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4256 if (global.config.gameMode == GameConfig::GameMode.Vanilla) {
4257 Video.color = 0xff_ff_00; //draw_set_color(c_yellow);
4258 drawTextAt(64, 48, "Classic Mode done!");
4260 Video.color = 0x00_80_80; //draw_set_color(c_teal);
4261 if (global.config.bizarrePlus) drawTextAt(64, 48, "Bizarre Mode Plus done!");
4262 else drawTextAt(64, 48, "Bizarre Mode done!");
4263 //draw_set_color(c_white);
4265 if (!global.usedShortcut) {
4266 Video.color = 0xc0_c0_c0; //draw_set_color(c_silver);
4267 drawTextAt(64, 56, "No shortcuts used!");
4268 //draw_set_color(c_yellow);
4272 if (winSceneDrawStatus > 1) {
4273 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4274 //draw_set_color(txtCol);
4275 Video.color = 0xff_ff_ff;
4276 drawTextAt(64, 64, "FINAL SCORE:");
4279 if (winSceneDrawStatus > 2) {
4280 sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
4281 Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4282 drawTextAt(64, 72, va("$%d", winMoneyCount));
4285 if (winSceneDrawStatus > 4) {
4286 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4287 Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4288 drawTextAt(64, 96, va("Time: %s", time2str(winTime/30)));
4290 draw_set_color(c_white);
4291 if (s < 10) draw_text(96+24, 96, string(m) + ":0" + string(s));
4292 else draw_text(96+24, 96, string(m) + ":" + string(s));
4296 if (winSceneDrawStatus > 5) {
4297 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4298 Video.color = 0xff_ff_ff; //draw_set_color(txtCol);
4299 drawTextAt(64, 96+8, "Kills: ");
4300 Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4301 drawTextAt(96+24, 96+8, va("%s", stats.countKills()));
4304 if (winSceneDrawStatus > 6) {
4305 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4306 Video.color = 0xff_ff_ff; //draw_set_color(txtCol);
4307 drawTextAt(64, 96+16, "Saves: ");
4308 Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4309 drawTextAt(96+24, 96+16, va("%s", stats.damselsSaved));
4313 Video.color = (255-clamp(winFadeLevel, 0, 255))<<24;
4314 Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
4317 if (winSceneDrawStatus == 8) {
4318 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4319 Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4321 if (global.config.gameMode == GameConfig::GameMode.Vanilla) {
4322 Video.color = 0xff_ff_00; //draw_set_color(c_yellow);
4323 lastString = "YOU SHALL BE REMEMBERED AS A HERO.";
4325 Video.color = 0x00_ff_ff;
4326 if (global.config.bizarrePlus) lastString = "ANOTHER LEGENDARY ADVENTURE!";
4327 else lastString = "YOUR DISCOVERIES WILL BE CELEBRATED!";
4329 auto strLen = lastString.length*8;
4331 n = trunc(ceil(n/2.0));
4332 drawTextAt(n, 116, lastString);
4338 // ////////////////////////////////////////////////////////////////////////// //
4339 #include "roomTitle.vc"
4340 #include "roomTrans1.vc"
4341 #include "roomTrans2.vc"
4342 #include "roomTrans3.vc"
4343 #include "roomTrans4.vc"
4344 #include "roomOlmec.vc"
4345 #include "roomEnd.vc"
4348 // ////////////////////////////////////////////////////////////////////////// //
4349 #include "packages/Generator/loadRoomGens.vc"
4350 #include "packages/Generator/loadEntityGens.vc"