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 //#define EXPERIMENTAL_RENDER_CACHE
23 const float FrameTime = 1.0f/30.0f;
25 const int dumpGridStats = true;
32 //enum NormalTilesWidth = LevelGen::LevelWidth*RoomGen::Width+2;
33 //enum NormalTilesHeight = LevelGen::LevelHeight*RoomGen::Height+2;
35 enum MaxTilesWidth = 64;
36 enum MaxTilesHeight = 64;
39 transient GameStats stats;
40 transient SpriteStore sprStore;
41 transient BackTileStore bgtileStore;
42 transient BackTileImage levBGImg;
45 transient name lastMusicName;
46 //RoomGen[LevelGen::LevelWidth, LevelGen::LevelHeight] rooms; // moved to levelgen
48 transient float accumTime;
49 transient bool gamePaused = false;
50 transient bool checkWater;
51 transient int liquidTileCount; // cached
52 transient int damselSaved;
56 transient int collectCounter;
57 transient int levelMoneyStart;
59 // all movable (thinkable) map objects
60 EntityGrid objGrid; // monsters, items and tiles
62 MapBackTile backtiles;
63 bool blockWaterChecking;
67 LevelGen::RType[MaxTilesWidth, MaxTilesHeight] roomType;
80 LevelKind levelKind = LevelKind.Normal;
82 array!MapTile allEnters;
83 array!MapTile allExits;
86 int startRoomX, startRoomY;
87 int endRoomX, endRoomY;
90 transient bool playerExited;
91 transient MapEntity playerExitDoor;
92 transient bool disablePlayerThink = false;
93 transient int maxPlayingTime; // in seconds
99 bool ghostSpawned; // to speed up some checks
100 bool resetBMCOG = false;
104 // FPS, i.e. incremented by 30 in one second
105 int time; // in frames
106 int lastUsedObjectId;
107 transient int lastRenderTime = -1;
109 MapEntity deadItemsHead;
111 // screen shake variables
116 // set this before calling `fixCamera()`
117 // dimensions should be real, not scaled up/down
118 transient int viewWidth, viewHeight;
119 // room bounds, not scaled
120 IVec2D viewMin, viewMax;
122 // for Olmec level cinematics
123 IVec2D cameraSlideToDest;
124 IVec2D cameraSlideToCurr;
125 IVec2D cameraSlideToSpeed; // !0: slide
126 int cameraSlideToPlayer;
127 // `fixCamera()` will set the following
128 // coordinates will be real too (with scale applied)
129 // shake is not applied
130 transient IVec2D viewStart; // with `player.viewOffset`
131 private transient IVec2D realViewStart; // without `player.viewOffset`
133 transient int framesProcessedFromLastClear;
135 transient int BuildYear;
136 transient int BuildMonth;
137 transient int BuildDay;
138 transient int BuildHour;
139 transient int BuildMin;
140 transient string BuildDateString;
143 final string getBuildDateString () {
144 if (!BuildYear) return BuildDateString;
145 if (BuildDateString) return BuildDateString;
146 BuildDateString = va("%d-%s-%s %s:%s", BuildYear, val2dig(BuildMonth), val2dig(BuildDay), val2dig(BuildHour), val2dig(BuildMin));
147 return BuildDateString;
151 final void cameraSlideToPoint (int dx, int dy, int speedx, int speedy) {
152 cameraSlideToPlayer = 0;
153 cameraSlideToDest.x = dx;
154 cameraSlideToDest.y = dy;
155 cameraSlideToSpeed.x = abs(speedx);
156 cameraSlideToSpeed.y = abs(speedy);
157 cameraSlideToCurr.x = cameraCurrX;
158 cameraSlideToCurr.y = cameraCurrY;
162 final void cameraReturnToPlayer () {
163 if (!cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y)) {
164 cameraSlideToCurr.x = cameraCurrX;
165 cameraSlideToCurr.y = cameraCurrY;
166 if (cameraSlideToSpeed.x && abs(cameraSlideToSpeed.x) < 8) cameraSlideToSpeed.x = 8;
167 if (cameraSlideToSpeed.y && abs(cameraSlideToSpeed.y) < 8) cameraSlideToSpeed.y = 8;
168 cameraSlideToPlayer = 1;
173 // if `frameSkip` is `true`, there are more frames waiting
174 // (i.e. you may skip rendering and such)
175 transient void delegate (bool frameSkip) onBeforeFrame;
176 transient void delegate (bool frameSkip) onAfterFrame;
178 transient void delegate () onCameraTeleported;
180 transient void delegate () onLevelExitedCB;
182 // this will be called in-between frames, and
183 // `frameTime` is [0..1)
184 transient void delegate (float frameTime) onInterFrame;
186 final int bizRoomStyle { get { return (lg ? lg.bizRoomStyle : 0); } }
189 final bool isNormalLevel () { return (levelKind == LevelKind.Normal); }
190 final bool isTitleRoom () { return (levelKind == LevelKind.Title); }
191 final bool isTutorialRoom () { return (levelKind == LevelKind.Tutorial); }
193 bool isHUDEnabled () {
194 if (inWinCutscene) return false;
195 if (lg.finalBossLevel) return true;
196 if (isNormalLevel()) return true;
197 // allow HUD in challenge chambers
202 // ////////////////////////////////////////////////////////////////////////// //
204 void addDeath (name aname) { if (isNormalLevel()) stats.addDeath(aname); }
211 void addKill (name aname, optional bool telefrag) {
212 if (isNormalLevel()) stats.addKill(aname, telefrag!optional);
213 else if (aname == 'Shopkeeper' && levelKind == LevelKind.Stars) { ++stats.starsKills; ++starsKills; }
216 void addCollect (name aname, optional int amount) { if (isNormalLevel()) stats.addCollect(aname, amount!optional); }
218 void addDamselSaved () { if (isNormalLevel()) stats.addDamselSaved(); }
219 void addIdolStolen () { if (isNormalLevel()) stats.addIdolStolen(); }
220 void addIdolConverted () { if (isNormalLevel()) stats.addIdolConverted(); }
221 void addCrystalIdolStolen () { if (isNormalLevel()) stats.addCrystalIdolStolen(); }
222 void addCrystalIdolConverted () { if (isNormalLevel()) stats.addCrystalIdolConverted(); }
223 void addGhostSummoned () { if (isNormalLevel()) stats.addGhostSummoned(); }
226 // ////////////////////////////////////////////////////////////////////////// //
227 static final string val2dig (int n) {
228 return (n < 10 ? va("0%d", n) : va("%d", n));
232 static final string time2str (int time) {
233 int secs = time%60; time /= 60;
234 int mins = time%60; time /= 60;
235 int hours = time%24; time /= 24;
237 if (days) return va("%d DAYS, %d:%s:%s", days, hours, val2dig(mins), val2dig(secs));
238 if (hours) return va("%d:%s:%s", hours, val2dig(mins), val2dig(secs));
239 return va("%s:%s", val2dig(mins), val2dig(secs));
243 // ////////////////////////////////////////////////////////////////////////// //
244 final int tilesWidth () { return lg.levelRoomWidth*RoomGen::Width+2; }
245 final int tilesHeight () { return (lg.finalBossLevel ? 55 : lg.levelRoomHeight*RoomGen::Height+2); }
248 // ////////////////////////////////////////////////////////////////////////// //
249 protected void resetGameInternal () {
250 if (player) player.removeBallAndChain();
264 player.removeBallAndChain();
265 auto hi = player.holdItem;
266 player.holdItem = none;
267 if (hi) hi.instanceRemove();
268 hi = player.pickedItem;
269 player.pickedItem = none;
270 if (hi) hi.instanceRemove();
277 stats.clearGameTotals();
281 // this won't generate a level yet
282 void restartGame () {
284 if (global.startMoney > 0) stats.setMoneyCheat();
285 stats.setMoney(global.startMoney);
286 levelKind = LevelKind.Normal;
290 // complement function to `restart game`
291 void generateNormalLevel () {
293 centerViewAtPlayer();
297 void restartTitle () {
300 createSpecialLevel(LevelKind.Title, &createTitleRoom, 'musTitle');
309 void restartTutorial () {
312 createSpecialLevel(LevelKind.Tutorial, &createTutorialRoom, 'musCave');
321 void restartScores () {
324 createSpecialLevel(LevelKind.Scores, &createScoresRoom, 'musTitle');
333 void restartStarsRoom () {
336 createSpecialLevel(LevelKind.Stars, &createStarsRoom, '');
345 void restartSunRoom () {
348 createSpecialLevel(LevelKind.Sun, &createSunRoom, '');
357 void restartMoonRoom () {
360 createSpecialLevel(LevelKind.Moon, &createMoonRoom, '');
369 // ////////////////////////////////////////////////////////////////////////// //
370 // generate angry shopkeeper at exit if murderer or thief
371 void generateAngryShopkeepers () {
372 if (global.murderer || global.thiefLevel > 0) {
373 foreach (MapTile e; allExits) {
374 auto obj = MonsterShopkeeper(MakeMapObject(e.ix, e.iy, 'oShopkeeper'));
376 obj.style = 'Bounty Hunter';
377 obj.status = MapObject::PATROL;
384 // ////////////////////////////////////////////////////////////////////////// //
385 final void resetRoomBounds () {
388 viewMax.x = tilesWidth*16;
389 viewMax.y = tilesHeight*16;
390 // Great Lake is bottomless (nope)
391 //if (global.lake == 1) viewMax.y -= 16;
392 //writeln("ROOM BOUNDS: (", viewMax.x, "x", viewMax.y, ")");
396 final void setRoomBounds (int x0, int y0, int x1, int y1) {
404 // ////////////////////////////////////////////////////////////////////////// //
407 float timeout; // seconds
408 float starttime; // for active
409 bool active; // true: timeout is `GetTickCount()` dismissing time
412 array!OSDMessage msglist; // [0]: current one
415 private final void osdCheckTimeouts () {
416 auto stt = GetTickCount();
417 while (msglist.length) {
418 if (!msglist[0].active) {
419 msglist[0].active = true;
420 msglist[0].starttime = stt;
422 if (msglist[0].starttime+msglist[0].timeout >= stt) break;
428 final bool osdHasMessage () {
430 return (msglist.length > 0);
434 final string osdGetMessage (out float timeLeft, out float timeStart) {
436 if (msglist.length == 0) { timeLeft = 0; return ""; }
437 auto stt = GetTickCount();
438 timeStart = msglist[0].starttime;
439 timeLeft = fmax(0, msglist[0].starttime+msglist[0].timeout-stt);
440 return msglist[0].msg;
444 final void osdClear () {
449 final void osdMessage (string msg, optional float timeout, optional bool urgent) {
451 msg = global.expandString(msg);
452 if (!specified_timeout) timeout = 3.33;
453 // special message for shops
454 if (timeout == -666) {
456 if (msglist.length && msglist[0].msg == msg) return;
457 if (msglist.length == 0 || msglist[0].msg != msg) {
460 msglist[0].msg = msg;
462 msglist[0].active = false;
463 msglist[0].timeout = 3.33;
467 if (timeout < 0.1) return;
468 timeout = fmax(1.0, timeout);
469 //writeln("OSD: ", msg);
470 // find existing one, and bring it to the top
472 for (; oldidx < msglist.length; ++oldidx) {
473 if (msglist[oldidx].msg == msg) break; // i found her!
476 if (oldidx < msglist.length) {
477 // yeah, move duplicate to the top
478 msglist[oldidx].timeout = fmax(timeout, msglist[oldidx].timeout);
479 msglist[oldidx].active = false;
480 if (urgent && oldidx != 0) {
481 timeout = msglist[oldidx].timeout;
482 msglist.remove(oldidx);
484 msglist[0].msg = msg;
485 msglist[0].timeout = timeout;
486 msglist[0].active = false;
490 msglist[0].msg = msg;
491 msglist[0].timeout = timeout;
492 msglist[0].active = false;
496 msglist[$-1].msg = msg;
497 msglist[$-1].timeout = timeout;
498 msglist[$-1].active = false;
504 // ////////////////////////////////////////////////////////////////////////// //
505 void setup (GameGlobal aGlobal, SpriteStore aSprStore, BackTileStore aBGTileStore) {
507 sprStore = aSprStore;
508 bgtileStore = aBGTileStore;
510 lg = SpawnObject(LevelGen);
514 objGrid = SpawnObject(EntityGrid);
515 objGrid.setup(0, 0, MaxTilesWidth*16, MaxTilesHeight*16);
519 // stores should be set
523 levBGImg = bgtileStore[levBGImgName];
524 foreach (MapEntity o; objGrid.allObjects()) {
527 if (t && (t.lava || t.water)) ++liquidTileCount;
529 for (MapBackTile bt = backtiles; bt; bt = bt.next) bt.onLoaded();
530 if (player) player.onLoaded();
532 if (msglist.length) {
533 msglist[0].active = false;
534 msglist[0].timeout = 0.200;
537 if (lg && lg.musicName) global.playMusic(lg.musicName); else global.stopMusic();
541 // ////////////////////////////////////////////////////////////////////////// //
542 void pickedSpectacles () {
543 foreach (MapTile t; objGrid.allObjectsSafe(MapTile)) t.onGotSpectacles();
547 // ////////////////////////////////////////////////////////////////////////// //
548 #include "rgentile.vc"
549 #include "rgenobj.vc"
552 void onLevelExited () {
553 if (playerExitDoor isa TitleTileXTitle) {
554 playerExitDoor = none;
559 if (isTitleRoom() || levelKind == LevelKind.Scores) {
560 if (playerExitDoor) processTitleExit(playerExitDoor);
561 playerExitDoor = none;
564 if (isTutorialRoom()) {
565 playerExitDoor = none;
567 global.currLevel = 1;
568 generateNormalLevel();
572 if (levelKind == LevelKind.Stars || levelKind == LevelKind.Sun || levelKind == LevelKind.Moon) {
573 playerExitDoor = none;
575 if (onLevelExitedCB) onLevelExitedCB();
580 if (isNormalLevel()) {
581 stats.maxLevelComplete = max(stats.maxLevelComplete, global.currLevel);
583 if (playerExitDoor) {
584 if (playerExitDoor.objType == 'oXGold') {
585 writeln("exiting to City Of Gold");
586 global.cityOfGold = true;
587 //!global.currLevel += 1;
588 } else if (playerExitDoor.objType == 'oXMarket') {
589 writeln("exiting to Black Market");
590 global.genBlackMarket = true;
591 //!global.currLevel += 1;
595 if (onLevelExitedCB) onLevelExitedCB();
597 playerExitDoor = none;
598 if (levelKind == LevelKind.Transition) {
599 if (global.thiefLevel > 0) global.thiefLevel -= 1;
600 if (global.alienCraft) ++global.alienCraft;
601 if (global.yetiLair) ++global.yetiLair;
602 if (global.lake) ++global.lake;
603 if (global.cityOfGold) ++global.cityOfGold;
604 //orig dbg:if (global.currLevel == 1) global.currLevel += firstLevelSkip; else global.currLevel += levelSkip;
606 if (!global.blackMarket && !global.cityOfGold /*&& !global.yetiLair*/) {
607 global.currLevel += 1;
613 // < 20 seconds per level: looks like a speedrun
614 global.noDarkLevel = (levelEndTime > levelStartTime && levelEndTime-levelStartTime < 20*30);
615 if (lg.finalBossLevel) {
618 // add money for big idol
619 player.addScore(50000);
623 generateTransitionLevel();
626 //centerViewAtPlayer();
630 void onOlmecDead (MapObject o) {
631 writeln("*** OLMEC IS DEAD!");
632 foreach (MapTile t; allExits) {
635 auto st = checkTileAtPoint(t.ix+8, t.iy+16);
637 st = MakeMapTile(t.ix/16, t.iy/16+1, 'oTemple');
640 st.invincible = true;
646 void generateLevelMessages () {
647 writeln("LEVEL NUMBER: ", global.currLevel);
648 if (global.darkLevel) {
649 if (global.hasCrown) {
650 osdMessage("THE HEDJET SHINES BRIGHTLY.");
651 global.darkLevel = false;
652 } else if (global.config.scumDarkness < 2) {
653 osdMessage("I CAN'T SEE A THING!\nI'D BETTER USE THESE FLARES!");
657 if (global.blackMarket) osdMessage("WELCOME TO THE BLACK MARKET!");
659 if (global.cemetary) osdMessage("THE DEAD ARE RESTLESS!");
660 if (global.lake == 1) osdMessage("I CAN HEAR RUSHING WATER...");
662 if (global.snakePit) osdMessage("I HEAR SNAKES... I HATE SNAKES!");
663 if (global.yetiLair == 1) osdMessage("IT SMELLS LIKE WET FUR IN HERE!");
664 if (global.alienCraft == 1) osdMessage("THERE'S A PSYCHIC PRESENCE HERE!");
665 if (global.cityOfGold == 1) {
666 if (true /*hasOlmec()*/) osdMessage("IT'S THE LEGENDARY CITY OF GOLD!");
669 if (global.sacrificePit) osdMessage("I CAN HEAR PRAYERS TO KALI!");
673 final MapObject debugSpawnObjectWithClass (class!MapObject oclass, optional bool playerDir) {
674 if (!oclass) return none;
676 bool canLeft = !isSolidAtPoint(player.ix-8, player.iy);
677 bool canRight = !isSolidAtPoint(player.ix+16, player.iy);
678 if (!canLeft && !canRight) return none;
679 if (canLeft && canRight) {
681 dx = (player.dir == MapObject::Dir.Left ? -16 : 16);
686 dx = (canLeft ? -16 : 16);
688 auto obj = SpawnMapObjectWithClass(oclass);
689 if (obj isa MapEnemy) { dx -= 8; dy -= 8; }
690 if (obj) PutSpawnedMapObject(player.ix+dx, player.iy+dy, obj);
695 final MapObject debugSpawnObject (name aname) {
696 if (!aname) return none;
697 return debugSpawnObjectWithClass(findGameObjectClassByName(aname));
701 void createSpecialLevel (LevelKind kind, scope void delegate () creator, name amusic) {
702 global.darkLevel = false;
706 global.resetStartingItems();
708 global.setMusicPitch(1.0);
711 auto olddel = ImmediateDelete;
712 ImmediateDelete = false;
720 addBackgroundGfxDetails();
721 //levBGImgName = 'bgCave';
722 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
724 blockWaterChecking = true;
728 ImmediateDelete = olddel;
729 CollectGarbage(true); // destroy delayed objects too
731 if (dumpGridStats) objGrid.dumpStats();
733 playerExited = false; // just in case
734 playerExitDoor = none;
739 lg.musicName = amusic;
740 if (amusic) global.playMusic(lg.musicName); else global.stopMusic();
744 void createTitleLevel () {
745 createSpecialLevel(LevelKind.Title, &createTitleRoom, 'musTitle');
749 void createTutorialLevel () {
750 createSpecialLevel(LevelKind.Tutorial, &createTutorialRoom, 'musCave');
759 // `global.currLevel` is the new level
760 void generateTransitionLevel () {
761 global.darkLevel = false;
766 global.setMusicPitch(1.0);
767 switch (global.config.transitionMusicMode) {
768 case GameConfig::MusicMode.Silent: global.stopMusic(); break;
769 case GameConfig::MusicMode.Restart: global.restartMusic(); break;
770 case GameConfig::MusicMode.DontTouch: break;
773 levelKind = LevelKind.Transition;
775 auto olddel = ImmediateDelete;
776 ImmediateDelete = false;
779 if (global.currLevel < 4) createTrans1Room();
780 else if (global.currLevel == 4) createTrans1xRoom();
781 else if (global.currLevel < 8) createTrans2Room();
782 else if (global.currLevel == 8) createTrans2xRoom();
783 else if (global.currLevel < 12) createTrans3Room();
784 else if (global.currLevel == 12) createTrans3xRoom();
785 else if (global.currLevel < 16) createTrans4Room();
786 else if (global.currLevel == 16) createTrans4Room();
787 else createTrans1Room(); //???
792 addBackgroundGfxDetails();
793 //levBGImgName = 'bgCave';
794 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
796 blockWaterChecking = true;
800 if (damselSaved > 0) {
801 // this is special "damsel ready to kiss you" object, not a heart
802 MakeMapObject(176+8, 176+8, 'oDamselKiss');
803 global.plife += damselSaved; // if player skipped transition cutscene
807 ImmediateDelete = olddel;
808 CollectGarbage(true); // destroy delayed objects too
810 if (dumpGridStats) objGrid.dumpStats();
812 playerExited = false; // just in case
813 playerExitDoor = none;
818 //global.playMusic(lg.musicName);
822 void generateLevel () {
823 levelStartTime = time;
829 global.cityOfGold = false;
830 global.genBlackMarket = false;
833 global.setMusicPitch(1.0);
834 stats.clearLevelTotals();
836 levelKind = LevelKind.Normal;
843 //writeln("tw:", tilesWidth, "; th:", tilesHeight);
845 auto olddel = ImmediateDelete;
846 ImmediateDelete = false;
849 if (lg.finalBossLevel) {
850 blockWaterChecking = true;
854 // if transition cutscene was skipped...
855 global.plife += max(0, damselSaved); // if player skipped transition cutscene
859 startRoomX = lg.startRoomX;
860 startRoomY = lg.startRoomY;
861 endRoomX = lg.endRoomX;
862 endRoomY = lg.endRoomY;
863 addBackgroundGfxDetails();
864 foreach (int y; 0..tilesHeight) {
865 foreach (int x; 0..tilesWidth) {
871 levBGImgName = lg.bgImgName;
872 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
874 if (global.allowAngryShopkeepers) generateAngryShopkeepers();
876 lg.generateEntities();
878 // add box of flares to dark level
879 if (global.darkLevel && allEnters.length) {
880 auto enter = allEnters[0];
881 int x = enter.ix, y = enter.iy;
882 if (!isSolidAtPoint(x-16, y)) MakeMapObject(x-16+8, y+8, 'oFlareCrate');
883 else if (!isSolidAtPoint(x+16, y)) MakeMapObject(x+16+8, y+8, 'oFlareCrate');
884 else MakeMapObject(x+8, y+8, 'oFlareCrate');
887 //scrGenerateEntities();
888 //foreach (; 0..2) scrGenerateEntities();
890 writeln(objGrid.countObjects, " alive objects inserted");
891 writeln(countBackTiles, " background tiles inserted");
893 if (!player) FatalError("player pawn is not spawned");
895 if (lg.finalBossLevel) {
896 blockWaterChecking = true;
898 blockWaterChecking = false;
903 ImmediateDelete = olddel;
904 CollectGarbage(true); // destroy delayed objects too
906 if (dumpGridStats) objGrid.dumpStats();
908 playerExited = false; // just in case
909 playerExitDoor = none;
911 levelMoneyStart = stats.money;
914 generateLevelMessages();
919 if (lastMusicName != lg.musicName) {
920 global.playMusic(lg.musicName);
921 //writeln("!<", lastMusicName, ">:<", lg.musicName, ">");
923 //writeln("MM: ", global.config.nextLevelMusicMode);
924 switch (global.config.nextLevelMusicMode) {
925 case GameConfig::MusicMode.Silent: global.stopMusic(); break; // the thing that should not be
926 case GameConfig::MusicMode.Restart: global.restartMusic(); break;
927 case GameConfig::MusicMode.DontTouch:
928 if (global.config.transitionMusicMode == GameConfig::MusicMode.Silent) {
929 global.playMusic(lg.musicName);
934 lastMusicName = lg.musicName;
935 //global.playMusic(lg.musicName);
938 if (global.cityOfGold == 1 || global.genBlackMarket) resetBMCOG = true;
942 // ////////////////////////////////////////////////////////////////////////// //
943 int currKeys, nextKeys;
944 int pressedKeysQ, releasedKeysQ;
945 int keysPressed, keysReleased = -1;
948 struct SavedKeyState {
949 int currKeys, nextKeys;
950 int pressedKeysQ, releasedKeysQ;
951 int keysPressed, keysReleased;
953 int roomSeed, otherSeed;
957 // for saving/replaying
958 final void keysSaveState (out SavedKeyState ks) {
959 ks.currKeys = currKeys;
960 ks.nextKeys = nextKeys;
961 ks.pressedKeysQ = pressedKeysQ;
962 ks.releasedKeysQ = releasedKeysQ;
963 ks.keysPressed = keysPressed;
964 ks.keysReleased = keysReleased;
967 // for saving/replaying
968 final void keysRestoreState (const ref SavedKeyState ks) {
969 currKeys = ks.currKeys;
970 nextKeys = ks.nextKeys;
971 pressedKeysQ = ks.pressedKeysQ;
972 releasedKeysQ = ks.releasedKeysQ;
973 keysPressed = ks.keysPressed;
974 keysReleased = ks.keysReleased;
978 final void keysNextFrame () {
983 final void clearKeys () {
993 final void onKey (int code, bool down) {
998 if (keysReleased&code) {
1000 keysReleased &= ~code;
1001 pressedKeysQ |= code;
1005 if (keysPressed&code) {
1006 keysReleased |= code;
1007 keysPressed &= ~code;
1008 releasedKeysQ |= code;
1013 final bool isKeyDown (int code) {
1014 return !!(currKeys&code);
1017 final bool isKeyPressed (int code) {
1018 bool res = !!(pressedKeysQ&code);
1019 pressedKeysQ &= ~code;
1023 final bool isKeyReleased (int code) {
1024 bool res = !!(releasedKeysQ&code);
1025 releasedKeysQ &= ~code;
1030 final void clearKeysPressRelease () {
1031 keysPressed = default.keysPressed;
1032 keysReleased = default.keysReleased;
1033 pressedKeysQ = default.pressedKeysQ;
1034 releasedKeysQ = default.releasedKeysQ;
1040 // ////////////////////////////////////////////////////////////////////////// //
1041 final void registerEnter (MapTile t) {
1048 final void registerExit (MapTile t) {
1055 final bool isYAtEntranceRow (int py) {
1057 foreach (MapTile t; allEnters) if (t.iy == py) return true;
1062 final int calcNearestEnterDist (int px, int py) {
1063 if (allEnters.length == 0) return int.max;
1064 int curdistsq = int.max;
1065 foreach (MapTile t; allEnters) {
1066 int xc = px-t.xCenter, yc = py-t.yCenter;
1067 int distsq = xc*xc+yc*yc;
1068 if (distsq < curdistsq) curdistsq = distsq;
1070 return round(sqrt(curdistsq));
1074 final int calcNearestExitDist (int px, int py) {
1075 if (allExits.length == 0) return int.max;
1076 int curdistsq = int.max;
1077 foreach (MapTile t; allExits) {
1078 int xc = px-t.xCenter, yc = py-t.yCenter;
1079 int distsq = xc*xc+yc*yc;
1080 if (distsq < curdistsq) curdistsq = distsq;
1082 return round(sqrt(curdistsq));
1086 // ////////////////////////////////////////////////////////////////////////// //
1087 final void clearForTransition () {
1088 auto olddel = ImmediateDelete;
1089 ImmediateDelete = false;
1091 ImmediateDelete = olddel;
1092 CollectGarbage(true); // destroy delayed objects too
1093 global.darkLevel = false;
1097 // ////////////////////////////////////////////////////////////////////////// //
1098 final int countBackTiles () {
1100 for (MapBackTile bt = backtiles; bt; bt = bt.next) ++res;
1105 final void clearWholeLevel () {
1109 // don't kill objects the player is holding
1111 if (player.pickedItem isa ItemBall) {
1112 player.pickedItem.instanceRemove();
1113 player.pickedItem = none;
1115 if (player.pickedItem && player.pickedItem.grid) {
1116 player.pickedItem.grid.remove(player.pickedItem.gridId);
1117 writeln("secured pocket item '", GetClassName(player.pickedItem.Class), "'");
1119 if (player.holdItem isa ItemBall) {
1120 player.removeBallAndChain(temp:true);
1121 if (player.holdItem) player.holdItem.instanceRemove();
1122 player.holdItem = none;
1124 if (player.holdItem && player.holdItem.grid) {
1125 player.holdItem.grid.remove(player.holdItem.gridId);
1126 writeln("secured held item '", GetClassName(player.holdItem.Class), "'");
1128 writeln("secured ball; mustBeChained=", player.mustBeChained, "; wasHoldingBall=", player.wasHoldingBall);
1131 int count = objGrid.countObjects();
1132 if (dumpGridStats) { if (objGrid.getFirstObjectCID()) objGrid.dumpStats(); }
1133 objGrid.removeAllObjects(true); // and destroy
1134 if (count > 0) writeln(count, " objects destroyed");
1136 lastUsedObjectId = 0;
1139 lastRenderTime = -1;
1140 liquidTileCount = 0;
1144 MapBackTile t = backtiles;
1150 framesProcessedFromLastClear = 0;
1154 final void insertObject (MapEntity o) {
1156 if (o.grid) FatalError("cannot put object into level twice");
1161 final void spawnPlayerAt (int x, int y) {
1162 // if we have no player, spawn new one
1163 // otherwise this just a level transition, so simply reposition him
1165 // don't add player to object list, as it has very separate processing anyway
1166 player = SpawnObject(PlayerPawn);
1167 player.global = global;
1168 player.level = self;
1169 if (!player.initialize()) {
1171 FatalError("something is wrong with player initialization");
1177 player.saveInterpData();
1179 if (player.mustBeChained || global.config.scumBallAndChain) {
1180 writeln("*** spawning ball and chain");
1181 player.spawnBallAndChain(levelStart:true);
1183 playerExited = false;
1184 playerExitDoor = none;
1185 if (global.config.startWithKapala) global.hasKapala = true;
1186 centerViewAtPlayer();
1187 // reinsert player items into grid
1188 if (player.pickedItem) objGrid.insert(player.pickedItem);
1189 if (player.holdItem) objGrid.insert(player.holdItem);
1190 //writeln("player spawned; active=", player.active);
1191 player.scrSwitchToPocketItem(forceIfEmpty:false);
1195 final void teleportPlayerTo (int x, int y) {
1199 player.saveInterpData();
1204 final void resurrectPlayer () {
1205 if (player) player.resurrect();
1206 playerExited = false;
1207 playerExitDoor = none;
1211 // ////////////////////////////////////////////////////////////////////////// //
1212 final void scrShake (int duration) {
1213 if (shakeLeft == 0) {
1219 shakeLeft = max(shakeLeft, duration);
1224 // ////////////////////////////////////////////////////////////////////////// //
1227 ItemStolen, // including damsel, lol
1234 // make the nearest shopkeeper angry. RAWR!
1235 void scrShopkeeperAnger (SCAnger reason, optional int maxdist, optional MapEntity offender) {
1236 if (!offender) offender = player;
1237 auto shp = MonsterShopkeeper(findNearestEnemy(offender.ix, offender.iy, delegate bool (MapEnemy o) {
1238 auto sc = MonsterShopkeeper(o);
1239 if (!sc) return false;
1240 if (sc.dead || sc.angered) return false;
1242 }, castClass:MonsterShopkeeper));
1245 if (specified_maxdist && offender.directionToEntityCenter(shp) > maxdist) return;
1246 if (!shp.dead && !shp.angered) {
1247 shp.status = MapObject::ATTACK;
1249 if (global.murderer) msg = "YOU'LL PAY FOR YOUR CRIMES!";
1250 else if (reason == SCAnger.ItemStolen) msg = "COME BACK HERE, THIEF!";
1251 else if (reason == SCAnger.TileDestroyed) msg = "DIE, YOU VANDAL!";
1252 else if (reason == SCAnger.BombDropped) msg = "TERRORIST!";
1253 else if (reason == SCAnger.DamselWhipped) msg = "HEY, ONLY I CAN DO THAT!";
1254 else if (reason == SCAnger.CrapsCheated) msg = "DIE, CHEATER!";
1255 else msg = "NOW I'M REALLY STEAMED!";
1256 if (msg) osdMessage(msg, -666);
1257 global.thiefLevel += (global.thiefLevel > 0 ? 3 : 2);
1263 final MapObject findCrapsPrize () {
1264 foreach (MapObject o; objGrid.allObjects(MapObject)) {
1265 if (!o.spectral && o.inDiceHouse) return o;
1271 // ////////////////////////////////////////////////////////////////////////// //
1272 // 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.
1273 // note: idols moved by monkeys will have false `stolenIdol`
1274 void scrTriggerIdolAltar (bool stolenIdol) {
1275 ObjTikiCurse res = none;
1276 int curdistsq = int.max;
1277 int px = player.xCenter, py = player.yCenter;
1278 foreach (MapObject o; objGrid.allObjects(MapObject)) {
1279 auto tcr = ObjTikiCurse(o);
1281 if (tcr.activated) continue;
1282 int xc = px-tcr.xCenter, yc = py-tcr.yCenter;
1283 int distsq = xc*xc+yc*yc;
1284 if (distsq < curdistsq) {
1289 if (res) res.activate(stolenIdol);
1293 // ////////////////////////////////////////////////////////////////////////// //
1294 void setupGhostTime () {
1295 musicFadeTimer = -1;
1296 ghostSpawned = false;
1298 // there is no ghost on the first level
1299 if (inWinCutscene || !isNormalLevel() || lg.finalBossLevel || global.currLevel == 1) {
1301 global.setMusicPitch(1.0);
1305 if (global.config.scumGhost < 0) {
1308 osdMessage("A GHASTLY VOICE CALLS YOUR NAME!\nLET'S GET OUT OF HERE!", 6.66, urgent:true);
1312 if (global.config.scumGhost == 0) {
1318 // randomizes time until ghost appears once time limit is reached
1319 // global.ghostRandom (true/false) if extra time is randomized - loaded from yasm.cfg. if false, use 30 second default
1320 // ghostTimeLeft (time in seconds * 1000) for currently generated level
1322 if (global.config.ghostRandom) {
1323 auto tMin = global.randOther(10, 30); // min(global.plife*10, 30);
1324 auto tMax = max(tMin+global.randOther(10, 30), min(global.config.scumGhost, 300)); // 5 minutes max
1325 auto tTime = global.randOther(tMin, tMax);
1326 if (tTime <= 0) tTime = round(tMax/2.0);
1327 ghostTimeLeft = tTime;
1329 ghostTimeLeft = global.config.scumGhost; // 5 minutes max
1332 ghostTimeLeft += max(0, global.config.ghostExtraTime);
1334 ghostTimeLeft *= 30; // seconds -> frames
1335 //global.ghostShowTime
1339 void spawnGhost () {
1341 ghostSpawned = true;
1344 int vwdt = (viewMax.x-viewMin.x);
1345 int vhgt = (viewMax.y-viewMin.y);
1349 if (player.ix < viewMin.x+vwdt/2) {
1350 // player is in the left side
1351 gx = viewMin.x+vwdt/2+vwdt/4;
1353 // player is in the right side
1354 gx = viewMin.x+vwdt/4;
1357 if (player.iy < viewMin.y+vhgt/2) {
1358 // player is in the left side
1359 gy = viewMin.y+vhgt/2+vhgt/4;
1361 // player is in the right side
1362 gy = viewMin.y+vhgt/4;
1365 writeln("spawning ghost at tile (", gx/16, ",", gy/16, "); player is at tile (", player.ix/16, ",", player.iy/16, ")");
1367 MakeMapObject(gx, gy, 'oGhost');
1370 if (oPlayer1.x > room_width/2) instance_create(view_xview[0]+view_wview[0]+8, view_yview[0]+floor(view_hview[0] / 2), oGhost);
1371 else instance_create(view_xview[0]-32, view_yview[0]+floor(view_hview[0]/2), oGhost);
1372 global.ghostExists = true;
1377 void thinkFrameGameGhost () {
1378 if (player.dead) return;
1379 if (!isNormalLevel()) return; // just in case
1381 if (ghostTimeLeft < 0) {
1383 if (musicFadeTimer > 0) {
1384 musicFadeTimer = -1;
1385 global.setMusicPitch(1.0);
1390 if (musicFadeTimer >= 0) {
1392 if (musicFadeTimer > 0 && musicFadeTimer <= 100*3) {
1393 float pitch = 1.0-(0.00226/3.0)*musicFadeTimer;
1394 //writeln("MFT: ", musicFadeTimer, "; pitch=", pitch);
1395 global.setMusicPitch(pitch);
1399 if (ghostTimeLeft == 0) {
1400 // she is already here!
1404 // no ghost if we have a crown
1405 if (global.hasCrown) {
1410 // if she was already spawned, don't do it again
1416 if (--ghostTimeLeft != 0) {
1418 if (global.config.ghostExtraTime > 0) {
1419 if (ghostTimeLeft == global.config.ghostExtraTime*30) {
1420 osdMessage("A CHILL RUNS UP YOUR SPINE...\nLET'S GET OUT OF HERE!", 6.66, urgent:true);
1422 if (musicFadeTimer < 0 && ghostTimeLeft <= global.config.ghostExtraTime*30) {
1430 if (player.isExitingSprite) {
1431 // no reason to spawn her, we're leaving
1440 void thinkFrameGame () {
1441 thinkFrameGameGhost();
1442 // udjat eye blinking
1443 if (global.hasUdjatEye && player) {
1444 foreach (MapTile t; allExits) {
1445 if (t isa MapTileBlackMarketDoor) {
1446 auto dm = int(player.distanceToEntity(t));
1448 if (udjatAlarm < 1 || dm < udjatAlarm) udjatAlarm = dm;
1452 global.udjatBlink = false;
1455 if (udjatAlarm > 0) {
1456 if (--udjatAlarm == 0) {
1457 global.udjatBlink = !global.udjatBlink;
1458 if (global.hasUdjatEye && player) {
1459 player.playSound(global.udjatBlink ? 'sndBlink1' : 'sndBlink2');
1463 switch (levelKind) {
1464 case LevelKind.Stars: thinkFrameGameStars(); break;
1465 case LevelKind.Sun: thinkFrameGameSun(); break;
1466 case LevelKind.Moon: thinkFrameGameMoon(); break;
1471 // ////////////////////////////////////////////////////////////////////////// //
1472 private final bool isWaterTileCB (MapTile t) {
1473 return (t && t.visible && t.water);
1477 private final bool isLavaTileCB (MapTile t) {
1478 return (t && t.visible && t.lava);
1482 private final bool isWetTile (MapTile t) {
1483 return (t && t.visible && (t.water || t.lava || t.wet));
1487 private final bool isWetOrSolidTile (MapTile t) {
1488 return (t && t.visible && (t.water || t.lava || t.solid) && t.isInstanceAlive);
1493 final bool isWetOrSolidTileAtPoint (int px, int py) {
1494 return !!checkTileAtPoint(px, py, &isWetOrSolidTile);
1500 final bool isWetOrSolidTileAtTile (int tx, int ty) {
1501 return !!checkTileAtPoint(tx*16, ty*16, &isWetOrSolidTile);
1505 final bool isWetTileAtPix (int tx, int ty) {
1506 return !!checkTileAtPoint(tx, ty, &isWetTile);
1511 // ////////////////////////////////////////////////////////////////////////// //
1512 const int GreatLakeStartTileY = 28;
1514 final void fillGreatLake () {
1515 if (global.lake == 1) {
1516 foreach (int y; GreatLakeStartTileY..tilesHeight) {
1517 foreach (int x; 0..tilesWidth) {
1518 auto t = checkTileAtPoint(x*16, y*16, delegate bool (MapTile t) {
1519 if (t.spectral || !t.visible || t.invisible || t.moveable) return false;
1523 //if (!t.water || !t.lava) { t.wet = true; continue; }
1525 t = MakeMapTile(x, y, 'oWaterSwim');
1529 t.setSprite(y == GreatLakeStartTileY ? 'sWaterTop' : 'sWater');
1530 } else if (t.lava) {
1531 t.setSprite(y == GreatLakeStartTileY ? 'sLavaTop' : 'sLava');
1539 // called once after level generation
1540 final void fixLiquidTop () {
1541 if (global.lake == 1) fillGreatLake();
1543 liquidTileCount = 0;
1544 forEachTile(delegate bool (MapTile t) {
1545 if (!t.water && !t.lava) {
1546 // mark as wet for lake
1547 //if (global.lake == 1 && t.iy >= GreatLakeStartTileY*16) t.wet = true;
1552 //writeln("fixing water tile(", GetClassName(t.Class), "):'", t.objName, "' (water=", t.water, "; lava=", t.lava, "); lqc=", liquidTileCount);
1554 if (global.lake == 1) return false; // it is done in `fillGreatLake()`
1556 if (!checkTileAtPoint(t.ix+8, t.iy-8, (t.lava ? &isLavaTileCB : &isWaterTileCB))) {
1557 t.setSprite(t.lava ? 'sLavaTop' : 'sWaterTop');
1559 // don't do this, it will destroy seaweed
1560 //t.setSprite(t.lava ? 'sLava' : 'sWater');
1561 auto spr = t.getSprite();
1562 if (!spr) t.setSprite(t.lava ? 'sLava' : 'sWater');
1563 else if (spr.Name == 'sLavaTop') t.setSprite('sLava');
1564 else if (spr.Name == 'sWaterTop') t.setSprite('sWater');
1569 //writeln("liquid tiles count: ", liquidTileCount);
1574 private final void checkWaterFlow (MapTile wtile) {
1575 if (global.lake == 1 && wtile.iy >= GreatLakeStartTileY*16) return;
1577 int tileX = wtile.ix/16, tileY = wtile.iy/16;
1579 bool wetLeft = !!isWetOrSolidTileAtTile(tileX-1, tileY);
1580 bool wetRight = !!isWetOrSolidTileAtTile(tileX+1, tileY);
1581 bool wetBottom = !!isWetOrSolidTileAtTile(tileX, tileY+1);
1583 if (!wetBottom || !wetLeft || !wetRight) {
1584 //TODO: if this is some pool created by a mattock or by an explosion, fill it
1585 if (true /*!isGoodPoolAtTile(tileX, tileY)*/) {
1588 wtile.instanceRemove(); // just in case
1595 if (!isWetTileAtPix(tileX*16+8, tileY*16-8)) {
1596 wtile.setSprite(wtile.lava ? 'sLavaTop' : 'sWaterTop');
1602 [.] write water flowing. algo:
1603 start with the lowest water tile that has any room do drop, or to move to the side
1604 if water can move down, move it down until it hits the floor, repeat the algo for it
1605 if water can move left or right, choose a direction (if both dirs are available, make
1606 a random choice), move water there, and repeat algo
1607 NOTE: if we're sitting on top of another water tile, choose the side where we can move
1608 down on the next step
1609 WARNING! never return to the horizontal tile we already visited! such tiles are
1610 considered "occupied"
1611 if water cannot move anymore, set "check mark" to 1, and continue with other water tiles.
1613 after all water tiles are moved, check all water tiles again. if some tile is not enclosed
1614 by another water tiles or solids, remove it
1616 transient MapTile curWaterTile;
1617 transient bool curWaterTileCheckHitsLava;
1618 transient bool curWaterTileCheckHitsSolidOrWater; // only for `checkWaterOrSolidTilePartialCB`
1619 transient int curWaterTileLastHDir;
1620 transient ubyte[16, 16] curWaterOccupied;
1621 transient int curWaterOccupiedCount;
1622 transient int curWaterTileCheckX0, curWaterTileCheckY0;
1625 private final void clearCurWaterCheckState () {
1626 curWaterTileCheckHitsLava = false;
1627 curWaterOccupiedCount = 0;
1628 foreach (auto idx; 0..16*16) curWaterOccupied[idx] = 0;
1632 private final bool checkWaterOrSolidTileCB (MapTile t) {
1633 if (t == curWaterTile) return false;
1634 if (t.lava && curWaterTile.water) {
1635 curWaterTileCheckHitsLava = true;
1638 if (t.ix%16 != 0 || t.iy%16 != 0) {
1639 if (t.water || t.solid) {
1640 // fill occupied array
1641 //FIXME: optimize this
1642 if (curWaterOccupiedCount < 16*16) {
1643 foreach (auto dy; t.y0..t.y1+1) {
1644 foreach (auto dx; t.x0..t.x1+1) {
1645 int sx = dx-curWaterTileCheckX0;
1646 int sy = dy-curWaterTileCheckY0;
1647 if (sx >= 0 && sx <= 16 && sy >= 0 && sy <= 15 && !curWaterOccupied[sx, sy]) {
1648 curWaterOccupied[sx, sy] = 1;
1649 ++curWaterOccupiedCount;
1655 return false; // need to check for lava
1657 if (t.water || t.solid || t.lava) {
1658 curWaterOccupiedCount = 16*16;
1659 if (t.water && curWaterTile.lava) t.instanceRemove();
1661 return false; // need to check for lava
1665 private final bool checkWaterOrSolidTilePartialCB (MapTile t) {
1666 if (t == curWaterTile) return false;
1667 if (t.lava && curWaterTile.water) {
1668 //writeln("!!!!!!!!");
1669 curWaterTileCheckHitsLava = true;
1672 if (t.water || t.solid || t.lava) {
1673 //writeln("*********");
1674 curWaterTileCheckHitsSolidOrWater = true;
1675 if (t.water && curWaterTile.lava) t.instanceRemove();
1677 return false; // need to check for lava
1681 private final bool isFullyOccupiedAtTilePos (int tileX, int tileY) {
1682 clearCurWaterCheckState();
1683 curWaterTileCheckX0 = tileX*16;
1684 curWaterTileCheckY0 = tileY*16;
1685 checkTilesInRect(tileX*16, tileY*16, 16, 16, &checkWaterOrSolidTileCB);
1686 return (curWaterTileCheckHitsLava || curWaterOccupiedCount == 16*16);
1690 private final bool isAtLeastPartiallyOccupiedAtTilePos (int tileX, int tileY) {
1691 curWaterTileCheckHitsLava = false;
1692 curWaterTileCheckHitsSolidOrWater = false;
1693 checkTilesInRect(tileX*16, tileY*16, 16, 16, &checkWaterOrSolidTilePartialCB);
1694 return (curWaterTileCheckHitsSolidOrWater || curWaterTileCheckHitsLava);
1698 private final bool waterCanReachGroundHoleInDir (MapTile wtile, int dx) {
1699 if (dx == 0) return false; // just in case
1701 int x = wtile.ix/16, y = wtile.iy/16;
1703 while (x >= 0 && x < tilesWidth) {
1704 if (!isAtLeastPartiallyOccupiedAtTilePos(x, y+1)) return true;
1705 if (isAtLeastPartiallyOccupiedAtTilePos(x, y)) return false;
1712 // returns `true` if this tile must be removed
1713 private final bool checkWaterFlow (MapTile wtile) {
1714 if (global.lake == 1 && wtile.iy >= GreatLakeStartTileY*16) return false;
1716 if (wtile.ix%16 != 0 || wtile.iy%16 != 0) return true; // sanity check
1718 curWaterTile = wtile;
1719 curWaterTileLastHDir = 0; // never moved to the side
1721 bool wasMoved = false;
1724 int tileX = wtile.ix/16, tileY = wtile.iy/16;
1727 if (tileY >= tilesHeight) return true;
1729 // check if we can fall down
1730 auto canFall = !isAtLeastPartiallyOccupiedAtTilePos(tileX, tileY+1);
1731 // disappear if can fall in lava
1732 if (wtile.water && curWaterTileCheckHitsLava) {
1733 //!writeln(wtile.objId, ": LAVA HIT DOWN");
1737 // fake, so caller will not start removing tiles
1738 if (canFall) wtile.waterMovedDown = true;
1744 //!writeln(wtile.objId, ": GOING DOWN");
1745 curWaterTileLastHDir = 0;
1746 wtile.iy = wtile.iy+16;
1748 wtile.waterMovedDown = true;
1752 bool canMoveLeft = (curWaterTileLastHDir > 0 ? false : !isAtLeastPartiallyOccupiedAtTilePos(tileX-1, tileY));
1753 // disappear if near lava
1754 if (wtile.water && curWaterTileCheckHitsLava) {
1755 //!writeln(wtile.objId, ": LAVA HIT LEFT");
1759 bool canMoveRight = (curWaterTileLastHDir < 0 ? false : !isAtLeastPartiallyOccupiedAtTilePos(tileX+1, tileY));
1760 // disappear if near lava
1761 if (wtile.water && curWaterTileCheckHitsLava) {
1762 //!writeln(wtile.objId, ": LAVA HIT RIGHT");
1766 if (!canMoveLeft && !canMoveRight) {
1768 //!if (wasMove) writeln(wtile.objId, ": NO MORE MOVES");
1772 if (canMoveLeft && canMoveRight) {
1773 // choose random direction
1774 //!writeln(wtile.objId, ": CHOOSING RANDOM HDIR");
1775 // actually, choose direction that leads to hole in a ground
1776 if (waterCanReachGroundHoleInDir(wtile, -1)) {
1777 // can reach hole at the left side
1778 if (waterCanReachGroundHoleInDir(wtile, 1)) {
1779 // can reach hole at the right side, choose at random
1780 if (global.randOther(0, 1)) canMoveRight = false; else canMoveLeft = false;
1783 canMoveRight = false;
1786 // can't reach hole at the left side
1787 if (waterCanReachGroundHoleInDir(wtile, 1)) {
1788 // can reach hole at the right side, choose at random
1789 canMoveLeft = false;
1791 // no holes at any side, choose at random
1792 if (global.randOther(0, 1)) canMoveRight = false; else canMoveLeft = false;
1799 if (canMoveRight) FatalError("WATERCHECK: WTF RIGHT");
1800 //!writeln(wtile.objId, ": MOVING LEFT (", curWaterTileLastHDir, ")");
1801 curWaterTileLastHDir = -1;
1802 wtile.ix = wtile.ix-16;
1803 } else if (canMoveRight) {
1804 if (canMoveLeft) FatalError("WATERCHECK: WTF LEFT");
1805 //!writeln(wtile.objId, ": MOVING RIGHT (", curWaterTileLastHDir, ")");
1806 curWaterTileLastHDir = 1;
1807 wtile.ix = wtile.ix+16;
1815 wtile.setSprite(wtile.lava ? 'sLava' : 'sWater');
1816 wtile.waterMoved = true;
1817 // if this tile was not moved down, check if it can move down on any next step
1818 if (!wtile.waterMovedDown) {
1819 if (waterCanReachGroundHoleInDir(wtile, -1)) wtile.waterMovedDown = true;
1820 else if (waterCanReachGroundHoleInDir(wtile, 1)) wtile.waterMovedDown = true;
1824 return false; // don't remove
1826 //if (!isWetTileAtPix(tileX*16+8, tileY*16-8)) wtile.setSprite(wtile.lava ? 'sLavaTop' : 'sWaterTop');
1830 transient array!MapTile waterTilesList;
1832 final bool sortWaterTilesByCoordsLess (MapTile a, MapTile b) {
1834 if (dy) return (dy < 0);
1835 return (a.ix < b.ix);
1838 transient int waterFlowPause = 0;
1839 transient bool debugWaterFlowPause = false;
1841 final void cleanDeadObjects () {
1842 // remove dead objects
1843 if (deadItemsHead) {
1844 auto olddel = ImmediateDelete;
1845 ImmediateDelete = false;
1847 auto it = deadItemsHead;
1848 deadItemsHead = it.deadItemsNext;
1849 if (it.grid) it.grid.remove(it.gridId);
1852 } while (deadItemsHead);
1853 ImmediateDelete = olddel;
1854 if (olddel) CollectGarbage(true); // destroy delayed objects too
1858 final void cleanDeadTiles () {
1859 if (checkWater && /*global.lake == 1 ||*/ (!blockWaterChecking && liquidTileCount)) {
1860 if (global.lake == 1) fillGreatLake();
1861 if (waterFlowPause > 1) {
1866 if (debugWaterFlowPause) waterFlowPause = 4;
1867 //writeln("checking water");
1868 waterTilesList.clear();
1869 foreach (MapTile wtile; objGrid.allObjectsSafe(MapTile)) {
1870 if (wtile.water || wtile.lava) {
1872 if (wtile.ix%16 == 0 && wtile.iy%16 == 0) {
1873 wtile.waterMoved = false;
1874 wtile.waterMovedDown = false;
1875 wtile.waterSlideOldX = wtile.ix;
1876 wtile.waterSlideOldY = wtile.iy;
1877 waterTilesList[$] = wtile;
1882 liquidTileCount = 0;
1883 waterTilesList.sort(&sortWaterTilesByCoordsLess);
1885 bool wasAnyMove = false;
1886 bool wasAnyMoveDown = false;
1887 foreach (MapTile wtile; waterTilesList) {
1888 if (!wtile || !wtile.isInstanceAlive) continue;
1889 auto killIt = checkWaterFlow(wtile);
1893 wtile.instanceRemove(); // just in case
1895 wtile.saveInterpData();
1897 wasAnyMove = wasAnyMove || wtile.waterMoved;
1898 wasAnyMoveDown = wasAnyMoveDown || wtile.waterMovedDown;
1899 if (wtile.waterMoved && debugWaterFlowPause) wtile.waterSlideCounter = 4;
1903 liquidTileCount = 0;
1904 foreach (MapTile wtile; waterTilesList) {
1905 if (!wtile || !wtile.isInstanceAlive) continue;
1906 if (wasAnyMoveDown) {
1910 //checkWater = checkWater || wtile.waterMoved;
1911 curWaterTile = wtile;
1912 int tileX = wtile.ix/16, tileY = wtile.iy/16;
1913 // check if we are have no way to leak
1914 bool killIt = false;
1915 if (!isFullyOccupiedAtTilePos(tileX-1, tileY) || (wtile.water && curWaterTileCheckHitsLava)) {
1916 //writeln(" LEFT DEATH (", curWaterOccupiedCount, " : ", curWaterTileCheckHitsLava, ")");
1919 if (!killIt && (!isFullyOccupiedAtTilePos(tileX+1, tileY) || (wtile.water && curWaterTileCheckHitsLava))) {
1920 //writeln(" RIGHT DEATH (", curWaterOccupiedCount, " : ", curWaterTileCheckHitsLava, ")");
1923 if (!killIt && (!isFullyOccupiedAtTilePos(tileX, tileY+1) || (wtile.water && curWaterTileCheckHitsLava))) {
1924 //writeln(" DOWN DEATH (", curWaterOccupiedCount, " : ", curWaterTileCheckHitsLava, ")");
1931 wtile.instanceRemove(); // just in case
1936 if (wasAnyMove) checkWater = true;
1937 //writeln("water check: liquidTileCount=", liquidTileCount, "; checkWater=", checkWater, "; wasAnyMove=", wasAnyMove, "; wasAnyMoveDown=", wasAnyMoveDown);
1939 // fill empty spaces in lake with water
1947 // ////////////////////////////////////////////////////////////////////////// //
1948 private transient array!MapEntity postponedThinkers;
1949 private transient MapEntity thinkerHeld;
1950 private transient array!MapEntity activeThinkerList;
1953 final void doThinkActionsForObject (MapEntity o) {
1954 if (o.justSpawned) o.justSpawned = false;
1955 else if (o.imageSpeed > 0) o.nextAnimFrame();
1958 if (o.isInstanceAlive) {
1961 if (o.isInstanceAlive) {
1962 if (o.whipTimer > 0) --o.whipTimer;
1964 auto obj = MapObject(o);
1965 if (!o.canLiveOutsideOfLevel && (!obj || !obj.heldBy) && o.isOutsideOfLevel()) {
1966 // oops, fallen out of level...
1974 // return `true` if thinker should be removed
1975 final void thinkOne (MapEntity o, optional bool doHeldObject, optional bool dontAddHeldObject) {
1977 if (o == thinkerHeld && !doHeldObject) return; // skip it
1979 if (!o.active || !o.isInstanceAlive) return;
1981 auto obj = MapObject(o);
1983 if (obj && obj.heldBy == player) {
1984 // fix held item coords
1985 obj.fixHoldCoords();
1987 doThinkActionsForObject(o);
1989 if (!dontAddHeldObject) {
1991 foreach (MapEntity e; postponedThinkers) if (e == o) { found = true; break; }
1992 if (!found) postponedThinkers[$] = o;
1998 bool doThink = true;
2000 // collision with player weapon
2001 auto hh = PlayerWeapon(player.holdItem);
2002 bool doWeaponAction = false;
2004 if (hh.blockedBySolids && !global.config.killEnemiesThruWalls) {
2005 int xx = player.ix+(player.dir == MapObject::Dir.Left ? -16 : 16);
2006 //doWeaponAction = !isSolidAtPoint(xx, player.iy);
2007 doWeaponAction = !isSolidAtPoint(xx, hh.yCenter);
2009 int dh = max(1, hh.height-2);
2010 doWeaponAction = !checkTilesInRect(player.ix, player.iy);
2013 doWeaponAction = true;
2017 if (obj && doWeaponAction && hh && (o.whipTimer <= 0 || hh.ignoreWhipTimer) && hh.collidesWithObject(obj)) {
2018 //writeln("WEAPONED!");
2019 bool dontChangeWhipTimer = hh.dontChangeWhipTimer;
2020 if (!o.onTouchedByPlayerWeapon(player, hh)) {
2021 if (o.isInstanceAlive) hh.onCollisionWithObject(obj);
2023 if (!dontChangeWhipTimer) o.whipTimer = o.whipTimerValue; //HACK
2024 doThink = o.isInstanceAlive;
2027 if (doThink && o.isInstanceAlive) {
2028 doThinkActionsForObject(o);
2029 doThink = o.isInstanceAlive;
2032 // collision with player
2033 if (doThink && obj && o.collidesWith(player)) {
2034 if (!player.onObjectTouched(obj) && o.isInstanceAlive) {
2035 doThink = !o.onTouchedByPlayer(player);
2042 final void processThinkers (float timeDelta) {
2043 if (timeDelta <= 0) return;
2045 if (onBeforeFrame) onBeforeFrame(false);
2046 if (onAfterFrame) onAfterFrame(false);
2050 accumTime += timeDelta;
2051 bool wasFrame = false;
2053 auto olddel = ImmediateDelete;
2054 ImmediateDelete = false;
2055 while (accumTime >= FrameTime) {
2056 postponedThinkers.clear();
2058 accumTime -= FrameTime;
2059 if (onBeforeFrame) onBeforeFrame(accumTime >= FrameTime);
2061 if (shakeLeft > 0) {
2063 if (!shakeDir.x) shakeDir.x = (global.randOther(0, 1) ? -3 : 3);
2064 if (!shakeDir.y) shakeDir.y = (global.randOther(0, 1) ? -3 : 3);
2065 shakeOfs.x = shakeDir.x;
2066 shakeOfs.y = shakeDir.y;
2067 int sgnc = global.randOther(1, 3);
2068 if (sgnc&0x01) shakeDir.x = -shakeDir.x;
2069 if (sgnc&0x02) shakeDir.y = -shakeDir.y;
2078 // we don't want the time to grow too large
2079 if (time < 0) { time = 0; lastRenderTime = -1; }
2080 // game-global events
2082 // frame thinkers: player
2083 if (player && !disablePlayerThink) {
2085 if (!player.dead && isNormalLevel() &&
2086 (maxPlayingTime < 0 ||
2087 (maxPlayingTime > 0 && maxPlayingTime*30 <= time &&
2088 time%30 == 0 && global.randOther(1, 100) <= 20)))
2090 MakeMapObject(player.ix, player.iy, 'oExplosion');
2092 //HACK: check for stolen items
2093 auto item = MapItem(player.holdItem);
2094 if (item) item.onCheckItemStolen(player);
2095 item = MapItem(player.pickedItem);
2096 if (item) item.onCheckItemStolen(player);
2098 doThinkActionsForObject(player);
2100 // frame thinkers: held object
2101 thinkerHeld = player.holdItem;
2102 if (thinkerHeld && thinkerHeld.isInstanceAlive) {
2103 thinkOne(thinkerHeld, doHeldObject:true);
2104 if (!thinkerHeld.isInstanceAlive) {
2105 if (player.holdItem == thinkerHeld) player.holdItem = none;
2106 thinkerHeld.grid.remove(thinkerHeld.gridId);
2108 thinkerHeld.onDestroy();
2113 // frame thinkers: objects
2114 activeThinkerList.clear();
2115 auto grid = objGrid;
2116 // collect active objects
2117 if (global.config.useFrozenRegion) {
2118 foreach (MapEntity e; grid.inRectPix(viewStart.x/global.scale-64, viewStart.y/global.scale-64, 320+64*2, 240+64*2, precise:false)) {
2119 if (e.active) activeThinkerList[$] = e;
2123 foreach (MapEntity e; grid.allObjects()) {
2124 if (e.active) activeThinkerList[$] = e;
2127 // process active objects
2128 //writeln("thinkers: ", activeThinkerList.length);
2129 foreach (MapEntity o; activeThinkerList) {
2131 thinkOne(o, doHeldObject:false);
2132 if (!o.isInstanceAlive) {
2133 //writeln("dead thinker: '", o.objType, "'");
2134 if (o.grid) o.grid.remove(o.gridId);
2135 auto obj = MapObject(o);
2136 if (obj && obj.heldBy) obj.heldBy.holdItem = none;
2143 // postponed thinkers
2144 foreach (MapEntity o; postponedThinkers) {
2146 thinkOne(o, doHeldObject:true, dontAddHeldObject:true);
2147 if (!o.isInstanceAlive) {
2148 //writeln("dead pp-thinker: '", o.objType, "'");
2155 postponedThinkers.clear();
2157 // clean dead things
2159 // fix held item coords
2160 if (player && player.holdItem) {
2161 if (player.holdItem.isInstanceAlive) {
2162 player.holdItem.fixHoldCoords();
2164 player.holdItem = none;
2168 if (collectCounter == 0) {
2169 xmoney = max(0, xmoney-100);
2175 if (!player.dead) stats.oneMoreFramePlayed();
2176 SoundSystem.ListenerOrigin = vector(player.xCenter, player.yCenter, -1);
2177 //writeln("plrpos=(", player.xCenter, ",", player.yCenter, "); lo=", SoundSystem.ListenerOrigin);
2179 if (onAfterFrame) onAfterFrame(accumTime >= FrameTime);
2180 ++framesProcessedFromLastClear;
2183 if (!player.visible && player.holdItem) player.holdItem.visible = false;
2184 if (winCutsceneSwitchToNext) {
2185 winCutsceneSwitchToNext = false;
2186 switch (++inWinCutscene) {
2187 case 2: startWinCutsceneVolcano(); break;
2188 case 3: default: startWinCutsceneWinFall(); break;
2192 if (playerExited) break;
2194 ImmediateDelete = olddel;
2196 playerExited = false;
2198 centerViewAtPlayer();
2201 // if we were processed at least one frame, collect garbage
2203 CollectGarbage(true); // destroy delayed objects too
2205 if (accumTime > 0 && onInterFrame) onInterFrame(accumTime/FrameTime);
2209 // ////////////////////////////////////////////////////////////////////////// //
2210 final void tileXY2roomXY (int tileX, int tileY, out int roomX, out int roomY) {
2211 roomX = (tileX-1)/RoomGen::Width;
2212 roomY = (tileY-1)/RoomGen::Height;
2216 final bool isInShop (int tileX, int tileY) {
2217 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
2218 auto n = roomType[tileX, tileY];
2219 if (n == 4 || n == 5) return true;
2220 return !!checkTilesInRect(tileX*16, tileY*16, 16, 16, delegate bool (MapTile t) { return t.shopWall; });
2221 //k8: we don't have this
2222 //if (t && t.objType == 'oShop') return true;
2228 // ////////////////////////////////////////////////////////////////////////// //
2229 override void Destroy () {
2231 delete tempSolidTile;
2236 // ////////////////////////////////////////////////////////////////////////// //
2237 // WARNING! delegate should not create/delete objects!
2238 final MapObject findNearestObject (int px, int py, scope bool delegate (MapObject o) dg, optional class!MapObject castClass) {
2239 MapObject res = none;
2240 if (!castClass) castClass = MapObject;
2241 int curdistsq = int.max;
2242 foreach (MapObject o; objGrid.allObjects(MapObject)) {
2243 if (o.spectral) continue;
2244 if (!dg(o)) continue;
2245 int xc = px-o.xCenter, yc = py-o.yCenter;
2246 int distsq = xc*xc+yc*yc;
2247 if (distsq < curdistsq) {
2256 // WARNING! delegate should not create/delete objects!
2257 final MapObject findNearestEnemy (int px, int py, optional scope bool delegate (MapEnemy o) dg, optional class!MapEnemy castClass) {
2258 if (!castClass) castClass = MapEnemy;
2259 if (castClass !isa MapEnemy) return none;
2260 MapObject res = none;
2261 int curdistsq = int.max;
2262 foreach (MapEnemy o; objGrid.allObjects(castClass)) {
2263 //k8: i added `dead` check
2264 if (o.spectral || o.dead) continue;
2266 if (!dg(o)) continue;
2268 int xc = px-o.xCenter, yc = py-o.yCenter;
2269 int distsq = xc*xc+yc*yc;
2270 if (distsq < curdistsq) {
2279 final MonsterShopkeeper findNearestCalmShopkeeper (int px, int py) {
2280 auto obj = MonsterShopkeeper(findNearestEnemy(px, py, delegate bool (MapEnemy o) {
2281 auto sk = MonsterShopkeeper(o);
2282 if (sk && !sk.angered) return true;
2284 }, castClass:MonsterShopkeeper));
2289 final MonsterShopkeeper hasAliveShopkeepers (optional bool skipAngry) {
2290 foreach (MonsterShopkeeper sc; objGrid.allObjects(MonsterShopkeeper)) {
2291 if (sc.spectral || sc.dead) continue;
2292 if (skipAngry && (sc.angered || sc.outlaw)) continue;
2299 // WARNING! delegate should not create/delete objects!
2300 final int calcNearestEnemyDist (int px, int py, optional scope bool delegate (MapEnemy o) dg, optional class!MapEnemy castClass) {
2301 auto e = findNearestEnemy(px, py, dg!optional, castClass!optional);
2302 if (!e) return int.max;
2303 int xc = px-e.xCenter, yc = py-e.yCenter;
2304 return round(sqrt(xc*xc+yc*yc));
2308 // WARNING! delegate should not create/delete objects!
2309 final int calcNearestObjectDist (int px, int py, optional scope bool delegate (MapObject o) dg, optional class!MapObject castClass) {
2310 auto e = findNearestObject(px, py, dg!optional, castClass!optional);
2311 if (!e) return int.max;
2312 int xc = px-e.xCenter, yc = py-e.yCenter;
2313 return round(sqrt(xc*xc+yc*yc));
2317 // WARNING! delegate should not create/delete objects!
2318 final MapTile findNearestMoveableSolid (int px, int py, optional scope bool delegate (MapTile t) dg) {
2320 int curdistsq = int.max;
2321 foreach (MapTile t; objGrid.allObjects(MapTile)) {
2322 if (t.spectral) continue;
2324 if (!dg(t)) continue;
2326 if (!t.solid || !t.moveable) continue;
2328 int xc = px-t.xCenter, yc = py-t.yCenter;
2329 int distsq = xc*xc+yc*yc;
2330 if (distsq < curdistsq) {
2339 // WARNING! delegate should not create/delete objects!
2340 final MapTile findNearestTile (int px, int py, optional scope bool delegate (MapTile t) dg) {
2341 if (!dg) return none;
2343 int curdistsq = int.max;
2345 //FIXME: make this faster!
2346 foreach (MapTile t; objGrid.allObjects(MapTile)) {
2347 if (t.spectral) continue;
2348 int xc = px-t.xCenter, yc = py-t.yCenter;
2349 int distsq = xc*xc+yc*yc;
2350 if (distsq < curdistsq && dg(t)) {
2360 // ////////////////////////////////////////////////////////////////////////// //
2361 final bool cbIsObjectWeb (MapObject o) { return (o isa ItemWeb); }
2362 final bool cbIsObjectBlob (MapObject o) { return (o isa EnemyBlob); }
2363 final bool cbIsObjectArrow (MapObject o) { return (o isa ItemProjectileArrow); }
2364 final bool cbIsObjectEnemy (MapObject o) { return (/*o != self &&*/ o isa MapEnemy); }
2366 final bool cbIsObjectTreasure (MapObject o) { return (o.isTreasure); }
2368 final bool cbIsItemObject (MapObject o) { return (o isa MapItem); }
2370 final bool cbIsItemOrEnemyObject (MapObject o) { return (o isa MapItem || o isa MapEnemy); }
2373 final MapObject isObjectAtTile (int tileX, int tileY, optional scope bool delegate (MapObject o) dg, optional bool precise) {
2374 if (!specified_precise) precise = true;
2377 foreach (MapObject o; objGrid.inRectPix(tileX, tileY, 16, 16, precise:precise, castClass:MapObject)) {
2378 if (o.spectral) continue;
2380 if (dg(o)) return o;
2389 final MapObject isObjectAtTilePix (int x, int y, optional scope bool delegate (MapObject o) dg) {
2390 return isObjectAtTile(x/16, y/16, dg!optional);
2394 final MapObject isObjectAtPoint (int xpos, int ypos, optional scope bool delegate (MapObject o) dg, optional bool precise, optional class!MapObject castClass) {
2395 if (!specified_precise) precise = true;
2396 if (!castClass) castClass = MapObject;
2397 foreach (MapObject o; objGrid.inCellPix(xpos, ypos, precise:precise, castClass:castClass)) {
2398 if (o.spectral) continue;
2400 if (dg(o)) return o;
2402 if (o isa MapEnemy) return o;
2409 final MapObject isObjectInRect (int xpos, int ypos, int w, int h, optional scope bool delegate (MapObject o) dg, optional bool precise, optional class!MapObject castClass) {
2410 if (w < 1 || h < 1) return none;
2411 if (!castClass) castClass = MapObject;
2412 if (w == 1 && h == 1) return isObjectAtPoint(xpos, ypos, dg!optional, precise!optional, castClass);
2413 if (!specified_precise) precise = true;
2414 foreach (MapObject o; objGrid.inRectPix(xpos, ypos, w, h, precise:precise, castClass:castClass)) {
2415 if (o.spectral) continue;
2417 if (dg(o)) return o;
2419 if (o isa MapEnemy) return o;
2426 final MapObject forEachObject (scope bool delegate (MapObject o) dg, optional bool allowSpectrals, optional class!MapObject castClass) {
2427 if (!dg) return none;
2428 if (!castClass) castClass = MapObject;
2429 foreach (MapObject o; objGrid.allObjectsSafe(castClass)) {
2430 if (!allowSpectrals && o.spectral) continue;
2431 if (dg(o)) return o;
2437 final MapObject forEachObjectAtPoint (int xpos, int ypos, scope bool delegate (MapObject o) dg, optional bool precise) {
2438 if (!dg) return none;
2439 if (!specified_precise) precise = true;
2440 foreach (MapObject o; objGrid.inCellPix(xpos, ypos, precise:precise, castClass:MapObject)) {
2441 if (o.spectral) continue;
2442 if (dg(o)) return o;
2448 final MapObject forEachObjectInRect (int xpos, int ypos, int w, int h, scope bool delegate (MapObject o) dg, optional bool precise) {
2449 if (!dg || w < 1 || h < 1) return none;
2450 if (w == 1 && h == 1) return forEachObjectAtPoint(xpos, ypos, dg!optional, precise!optional);
2451 if (!specified_precise) precise = true;
2452 foreach (MapObject o; objGrid.inRectPix(xpos, ypos, w, h, precise:precise, castClass:MapObject)) {
2453 if (o.spectral) continue;
2454 if (dg(o)) return o;
2460 final MapEntity forEachEntityInRect (int xpos, int ypos, int w, int h, scope bool delegate (MapEntity o) dg, optional bool precise, optional class!MapEntity castClass) {
2461 if (!dg || w < 1 || h < 1) return none;
2462 if (!castClass) castClass = MapEntity;
2463 if (!specified_precise) precise = true;
2464 foreach (MapEntity e; objGrid.inRectPix(xpos, ypos, w, h, precise:precise, castClass:castClass)) {
2465 if (e.spectral) continue;
2466 if (dg(e)) return e;
2472 private final bool cbIsRopeTile (MapTile t) { return (t isa MapTileRope); }
2474 final MapTile isRopeAtPoint (int px, int py) {
2475 return checkTileAtPoint(px, py, &cbIsRopeTile);
2480 final MapTile isWaterSwimAtPoint (int px, int py) {
2481 return isWaterAtPoint(px, py);
2485 // ////////////////////////////////////////////////////////////////////////// //
2486 private array!MapEntity tmpEntityList;
2488 private final bool cbCollectEntitiesWithMask (MapEntity t) {
2489 if (!t.visible || t.spectral) return false;
2490 tmpEntityList[$] = t;
2495 final void touchEntitiesWithMask (int x, int y, SpriteFrame frm, scope bool delegate (MapEntity t) dg, optional class!MapEntity castClass) {
2496 if (!frm || frm.bw < 1 || frm.bh < 1 || !dg) return;
2497 if (frm.isEmptyPixelMask) return;
2498 if (!castClass) castClass = MapEntity;
2500 if (tmpEntityList.length) tmpEntityList.clear();
2501 if (player isa castClass && player.isRectCollisionFrame(frm, x, y)) tmpEntityList[$] = player;
2502 forEachEntityInRect(x+frm.bx, y+frm.by, frm.bw, frm.bh, &cbCollectEntitiesWithMask, castClass:castClass);
2503 foreach (MapEntity e; tmpEntityList) {
2504 if (!e || !e.isInstanceAlive || !e.visible || e.spectral) continue;
2505 if (e.isRectCollisionFrame(frm, x, y)) {
2512 // ////////////////////////////////////////////////////////////////////////// //
2513 final bool cbCollisionAnySolid (MapTile t) { return t.solid; }
2514 final bool cbCollisionAnyMoveableSolid (MapTile t) { return t.solid && t.moveable; }
2515 final bool cbCollisionLiquid (MapTile t) { return (t.water || t.lava); }
2516 final bool cbCollisionAnySolidOrLiquid (MapTile t) { return (t.solid || t.water || t.lava); }
2517 final bool cbCollisionLadder (MapTile t) { return t.ladder; }
2518 final bool cbCollisionLadderTop (MapTile t) { return t.laddertop; }
2519 final bool cbCollisionAnyLadder (MapTile t) { return (t.ladder || t.laddertop); }
2520 final bool cbCollisionAnySolidIce (MapTile t) { return (t.solid && t.ice); }
2521 final bool cbCollisionWater (MapTile t) { return t.water; }
2522 final bool cbCollisionLava (MapTile t) { return t.lava; }
2523 final bool cbCollisionAnyTree (MapTile t) { return t.tree; }
2524 final bool cbCollisionPlatform (MapTile t) { return t.platform; }
2525 final bool cbCollisionLadderOrPlatform (MapTile t) { return (t.ladder || t.platform); }
2526 //final bool cbCollisionAnyHangeable (MapTile t) { return t.hangeable; } // no need to check for solids here, it is done in MapTile initializer
2527 final bool cbCollisionSpringTrap (MapTile t) { return t.springtrap; }
2528 final bool cbCollisionSpikes (MapTile t) { return t.spikes; }
2529 final bool cbCollisionExitTile (MapTile t) { return t.isExitActive(); }
2531 final bool cbCollisionForWhoa (MapTile t) { return (t.solid || t.laddertop); }
2533 //final bool cbCollisionSacAltar (MapTile t) { return (t.objName == 'oSacAltarLeft'); } // only left part, yeah
2534 final bool cbCollisionSacAltar (MapTile t) { return t.sacrificingAltar; }
2537 // ////////////////////////////////////////////////////////////////////////// //
2538 transient MapTileTemp tempSolidTile;
2540 private final MapTileTemp makeWalkeableSolidTile (MapObject o) {
2541 if (!tempSolidTile) {
2542 tempSolidTile = SpawnObject(MapTileTemp);
2543 } else if (!tempSolidTile.isInstanceAlive) {
2544 delete tempSolidTile;
2545 tempSolidTile = SpawnObject(MapTileTemp);
2548 tempSolidTile.level = self;
2549 tempSolidTile.global = global;
2550 tempSolidTile.solid = true;
2551 tempSolidTile.objName = MapTileTemp.default.objName;
2552 tempSolidTile.objType = MapTileTemp.default.objType;
2553 tempSolidTile.e = o;
2554 tempSolidTile.fltx = o.fltx;
2555 tempSolidTile.flty = o.flty;
2556 return tempSolidTile;
2560 final MapTile checkTilesInRect (int x0, int y0, const int w, const int h, optional scope bool delegate (MapTile dg) dg, optional bool precise) {
2561 if (w < 1 || h < 1) return none;
2562 if (w == 1 && h == 1) return checkTileAtPoint(x0, y0, dg!optional);
2563 int x1 = x0+w-1, y1 = y0+h-1;
2564 if (x0 >= tilesWidth*16 || y0 >= tilesHeight*16 || x1 < 0 || y1 < 0) return none;
2565 if (!specified_precise) precise = true;
2566 if (!dg) dg = &cbCollisionAnySolid;
2568 // check walkable solid objects too
2569 foreach (MapEntity e; objGrid.inRectPix(x0, y0, w, h, precise:precise)) {
2570 if (e.spectral || !e.visible) continue;
2571 auto t = MapTile(e);
2573 if (dg(t)) return t;
2576 auto o = MapObject(e);
2577 if (o && o.walkableSolid) {
2578 t = makeWalkeableSolidTile(o);
2579 if (dg(t)) return t;
2588 final MapTile checkTileAtPoint (int x0, int y0, optional scope bool delegate (MapTile dg) dg, optional bool precise) {
2589 if (x0 < 0 || y0 < 0 || x0 >= tilesWidth*16 || y0 >= tilesHeight*16) return none;
2590 if (!specified_precise) precise = true;
2591 if (!dg) dg = &cbCollisionAnySolid;
2593 // check walkable solid objects
2594 foreach (MapEntity e; objGrid.inCellPix(x0, y0, precise:precise)) {
2595 if (e.spectral || !e.visible) continue;
2596 auto t = MapTile(e);
2598 if (dg(t)) return t;
2601 auto o = MapObject(e);
2602 if (o && o.walkableSolid) {
2603 t = makeWalkeableSolidTile(o);
2604 if (dg(t)) return t;
2613 // ////////////////////////////////////////////////////////////////////////// //
2614 final MapTile isSolidAtPoint (int px, int py) { return checkTileAtPoint(px, py); }
2615 final MapTile isSolidInRect (int rx, int ry, int rw, int rh) { return checkTilesInRect(rx, ry, rw, rh); }
2616 final MapTile isLadderAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadder); }
2617 final MapTile isLadderOrPlatformAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadderOrPlatform); }
2618 final MapTile isLadderTopAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadderTop); }
2619 final MapTile isAnyLadderAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyLadder); }
2620 final MapTile isWaterAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionWater); }
2621 final MapTile isLavaAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLava); }
2622 final MapTile isLiquidAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLiquid); }
2623 final MapTile isIceAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnySolidIce); }
2624 final MapTile isTreeAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyTree); }
2625 //final MapTile isHangeableAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyHangeable); }
2628 // ////////////////////////////////////////////////////////////////////////// //
2629 final void setTileTypeAt (int tileX, int tileY, LevelGen::RType rt) {
2630 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) roomType[tileX, tileY] = rt;
2634 //FIXME: make this faster
2635 transient float gtagX, gtagY;
2637 // only non-moveables and non-specials
2638 final MapTile getTileAtGrid (int tileX, int tileY) {
2641 return checkTileAtPoint(tileX*16, tileY*16, delegate bool (MapTile t) {
2642 if (t.spectral || t.moveable || t.toSpecialGrid || !t.visible) return false;
2643 if (t.fltx != gtagX || t.flty != gtagY) return false; // emulate grid
2644 if (t.width != 16 || t.height != 16) return false;
2647 //return (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight ? tiles[tileX, tileY] : none);
2651 final MapTile getTileAtGridAny (int tileX, int tileY) {
2654 return checkTileAtPoint(tileX*16, tileY*16, delegate bool (MapTile t) {
2655 if (t.spectral /*|| t.moveable*/ || !t.visible) return false;
2656 if (t.fltx != gtagX || t.flty != gtagY) return false; // emulate grid
2657 if (t.width != 16 || t.height != 16) return false;
2660 //return (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight ? tiles[tileX, tileY] : none);
2664 final bool isNamedTileAt (int tileX, int tileY, name atypename) {
2665 if (!atypename) return false;
2666 auto t = getTileAtGridAny(tileX, tileY);
2667 return (t && t.objName == atypename);
2671 final void setTileAtGrid (int tileX, int tileY, MapTile tile) {
2672 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
2674 tile.fltx = tileX*16;
2675 tile.flty = tileY*16;
2676 if (!tile.dontReplaceOthers) {
2677 auto osp = tile.spectral;
2678 tile.spectral = true;
2679 auto t = getTileAtGridAny(tileX, tileY);
2680 tile.spectral = osp;
2681 if (t && !t.immuneToReplacement) {
2682 writeln("REPLACING tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
2683 writeln(" NEW tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (tile.toSpecialGrid ? "special " : ""), "grid, pos=(", tile.fltx/16.0, ",", tile.flty/16.0, ")");
2689 auto t = getTileAtGridAny(tileX, tileY);
2690 if (t && !t.immuneToReplacement) {
2691 writeln("REMOVING tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
2699 // ////////////////////////////////////////////////////////////////////////// //
2700 // return `true` from delegate to stop
2701 MapTile forEachSolidTileOnGrid (scope bool delegate (int tileX, int tileY, MapTile t) dg) {
2702 if (!dg) return none;
2703 foreach (MapTile t; objGrid.allObjectsSafe(MapTile)) {
2704 if (t.spectral || !t.solid || !t.visible) continue;
2705 if (t.ix%16 != 0 || t.iy%16 != 0) continue; // emulate grid
2706 if (t.width != 16 || t.height != 16) continue;
2707 if (dg(t.ix/16, t.iy/16, t)) return t;
2713 // ////////////////////////////////////////////////////////////////////////// //
2714 // return `true` from delegate to stop
2715 MapTile forEachTile (scope bool delegate (MapTile t) dg, optional class!MapTile castClass) {
2716 if (!dg) return none;
2717 if (!castClass) castClass = MapTile;
2718 foreach (MapTile t; objGrid.allObjectsSafe(castClass)) {
2719 if (t.spectral || !t.visible) continue;
2720 if (dg(t)) return t;
2726 // ////////////////////////////////////////////////////////////////////////// //
2727 final void fixWallTiles () {
2728 foreach (MapTile t; objGrid.allObjectsSafe(MapTile)) t.beautifyTile();
2732 // ////////////////////////////////////////////////////////////////////////// //
2733 final MapTile isCollisionAtPoint (int px, int py, optional scope bool delegate (MapTile dg) dg) {
2734 if (!dg) dg = &cbCollisionAnySolid;
2735 return checkTilesInRect(px, py, 1, 1, dg);
2739 // ////////////////////////////////////////////////////////////////////////// //
2740 string scrGetKaliGift (MapTile altar, optional name gift) {
2743 // find other side of the altar
2744 int sx = player.ix, sy = player.iy;
2748 auto a2 = isCollisionAtPoint(altar.ix+18, altar.iy+8, &cbCollisionSacAltar);
2749 if (!a2) a2 = isCollisionAtPoint(altar.ix-8, altar.iy+8, &cbCollisionSacAltar);
2750 if (a2) { sx = a2.ix; sy = a2.iy; }
2753 if (global.favor <= -8) res = "SHE SEEMS VERY ANGRY WITH YOU!";
2754 else if (global.favor < 0) res = "SHE SEEMS ANGRY WITH YOU.";
2755 else if (global.favor == 0) res = "SHE HAS FORGIVEN YOU!";
2756 else if (global.favor >= 32) {
2757 if (global.kaliGift >= 3 && global.favor >= 32+(global.kaliGift-2)*16) {
2758 res = "YOU FEEL INVIGORATED!";
2759 global.kaliGift += 1;
2760 global.plife += global.randOther(4, 8);
2761 } else if (global.kaliGift >= 3) {
2762 res = "SHE SEEMS ECSTATIC WITH YOU!";
2763 } else if (global.bombs < 80) {
2764 res = "YOUR SATCHEL FEELS VERY FULL NOW!";
2765 global.kaliGift = 3;
2768 res = "YOU FEEL INVIGORATED!";
2769 global.kaliGift += 1;
2770 global.plife += global.randOther(4, 8);
2772 } else if (global.favor >= 16) {
2773 if (global.kaliGift >= 2) {
2774 res = "SHE SEEMS VERY HAPPY WITH YOU!";
2776 res = "SHE BESTOWS A GIFT UPON YOU!";
2777 global.kaliGift = 2;
2779 auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2782 obj = MakeMapObject(sx, sy-8, 'oPoof');
2787 if (gift) obj = MakeMapObject(sx, sy-8, gift);
2788 if (!obj) obj = MakeMapObject(sx, sy-8, 'oKapala');
2790 } else if (global.favor >= 8) {
2791 if (global.kaliGift >= 1) {
2792 res = "SHE SEEMS HAPPY WITH YOU.";
2794 res = "SHE BESTOWS A GIFT UPON YOU!";
2795 global.kaliGift = 1;
2796 //rAltar = instance_nearest(x, y, oSacAltarRight);
2797 //if (instance_exists(rAltar)) {
2799 auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2802 obj = MakeMapObject(sx, sy-8, 'oPoof');
2806 if (gift) obj = MakeMapObject(sx, sy-8, gift);
2808 auto n = global.randOther(1, 8);
2812 if (n == 1 && !global.hasCape && !global.hasJetpack) aname = 'oCapePickup';
2813 else if (n == 2 && !global.hasGloves) aname = 'oGloves';
2814 else if (n == 3 && !global.hasSpectacles) aname = 'oSpectacles';
2815 else if (n == 4 && !global.hasMitt) aname = 'oMitt';
2816 else if (n == 5 && !global.hasSpringShoes) aname = 'oSpringShoes';
2817 else if (n == 6 && !global.hasSpikeShoes) aname = 'oSpikeShoes';
2818 else if (n == 7 && !global.hasStickyBombs) aname = 'oPaste';
2819 else if (n == 8 && !global.hasCompass) aname = 'oCompass';
2821 obj = MakeMapObject(sx, sy-8, aname);
2827 obj = MakeMapObject(sx, sy-8, (global.hasJetpack ? 'oBombBox' : 'oJetpack'));
2833 } else if (global.favor > 0) {
2834 res = "SHE SEEMS PLEASED WITH YOU.";
2839 global.message = "";
2840 res = "KALI DEVOURS YOU!"; // sacrifice is player
2848 void performSacrifice (MapObject what, MapTile where) {
2849 if (!what || !what.isInstanceAlive) return;
2850 MakeMapObject(what.ix+8, what.iy+8, 'oFlame');
2851 if (where) where.playSound('sndSmallExplode'); else what.playSound('sndSmallExplode');
2852 if (!what.bloodless) what.scrCreateBlood(what.ix+8, what.iy+8, 3);
2854 string msg = "KALI ACCEPTS THE SACRIFICE!";
2856 auto idol = ItemGoldIdol(what);
2858 ++stats.totalSacrifices;
2859 if (global.favor <= -8) msg = "SHE SEEMS VERY ANGRY WITH YOU!";
2860 else if (global.favor < 0) msg = "SHE SEEMS ANGRY WITH YOU.";
2861 else if (global.favor >= 0) {
2862 // find other side of the altar
2863 int sx = player.ix, sy = player.iy;
2868 auto a2 = isCollisionAtPoint(altar.ix+18, altar.iy+8, &cbCollisionSacAltar);
2869 if (!a2) a2 = isCollisionAtPoint(altar.ix-8, altar.iy+8, &cbCollisionSacAltar);
2870 if (a2) { sx = a2.ix; sy = a2.iy; }
2873 auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2876 obj = MakeMapObject(sx, sy-8, 'oPoof');
2880 obj = MakeMapObject(sx, sy-16-8, 'oMonkeyGold');
2882 osdMessage(msg, 6.66);
2884 idol.instanceRemove();
2888 if (global.favor <= -8) {
2889 msg = "KALI DEVOURS THE SACRIFICE!";
2890 } else if (global.favor < 0) {
2891 global.favor += (what.status == MapObject::STUNNED ? what.favor : round(what.favor/2.0)); //k8: added `round()`
2892 if (what.favor > 0) what.favor = 0;
2894 global.favor += (what.status == MapObject::STUNNED ? what.favor : round(what.favor/2.0)); //k8: added `round()`
2898 if (myAltar.gift1 != "" and global.kaliGift == 0) scrGetKaliGift(myAltar.gift1);
2899 else if (myAltar.gift2 != "" and global.kaliGift == 1) scrGetKaliGift(myAltar.gift2);
2900 else scrGetKaliGift("");
2903 // sacrifice is player?
2904 if (what isa PlayerPawn) {
2905 ++stats.totalSelfSacrifices;
2906 msg = "KALI DEVOURS YOU!";
2907 player.visible = false;
2908 player.removeBallAndChain(temp:true);
2910 player.status = MapObject::DEAD;
2912 ++stats.totalSacrifices;
2913 auto msg2 = scrGetKaliGift(where);
2914 what.instanceRemove();
2915 if (msg2) msg = va("%s\n%s", msg, msg2);
2918 osdMessage(msg, 6.66);
2920 //!if (isRealLevel()) global.totalSacrifices += 1;
2922 //!global.messageTimer = 200;
2923 //!global.shake = 10;
2927 instance_create(x, y, oFlame);
2928 playSound(global.sndSmallExplode);
2929 scrCreateBlood(x, y, 3);
2930 global.message = "KALI ACCEPTS YOUR SACRIFICE!";
2931 if (global.favor <= -8) {
2932 global.message = "KALI DEVOURS YOUR SACRIFICE!";
2933 } else if (global.favor < 0) {
2934 if (status == 98) global.favor += favor*1.5; else global.favor += favor;
2935 if (favor > 0) favor = 0;
2937 if (status == 98) global.favor += favor*1.5; else global.favor += favor;
2940 if (myAltar.gift1 != "" and global.kaliGift == 0) scrGetFavorMsg(myAltar.gift1);
2941 else if (myAltar.gift2 != "" and global.kaliGift == 1) scrGetFavorMsg(myAltar.gift2);
2942 else scrGetFavorMsg("");
2944 global.messageTimer = 200;
2951 // ////////////////////////////////////////////////////////////////////////// //
2952 final void addBackgroundGfxDetails () {
2953 // add background details
2954 //if (global.customLevel) return;
2956 // bg = instance_create(16*randRoom(1, 42), 16*randRoom(1, 33), oCaveBG);
2957 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);
2958 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);
2959 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);
2960 else MakeMapBackTile('bgExtras', 32*global.randRoom(0, 1), 0, 32, 32, 16*global.randRoom(1, 42), 16*global.randRoom(1, 33), 10002);
2965 // ////////////////////////////////////////////////////////////////////////// //
2966 private final void fixRealViewStart () {
2967 int scale = global.scale;
2968 realViewStart.x = clamp(realViewStart.x, viewMin.x*scale, viewMax.x*scale-viewWidth);
2969 realViewStart.y = clamp(realViewStart.y, viewMin.y*scale, viewMax.y*scale-viewHeight);
2973 final int cameraCurrX () { return realViewStart.x/global.scale; }
2974 final int cameraCurrY () { return realViewStart.y/global.scale; }
2977 private final void fixViewStart () {
2978 int scale = global.scale;
2979 viewStart.x = clamp(viewStart.x, viewMin.x*scale, viewMax.x*scale-viewWidth);
2980 viewStart.y = clamp(viewStart.y, viewMin.y*scale, viewMax.y*scale-viewHeight);
2984 final void centerViewAtPlayer () {
2985 if (viewWidth < 1 || viewHeight < 1 || !player) return;
2986 centerViewAt(player.xCenter, player.yCenter);
2990 final void centerViewAt (int x, int y) {
2991 if (viewWidth < 1 || viewHeight < 1) return;
2993 cameraSlideToSpeed.x = 0;
2994 cameraSlideToSpeed.y = 0;
2995 cameraSlideToPlayer = 0;
2997 int scale = global.scale;
3000 realViewStart.x = clamp(x-viewWidth/2, 0, tilesWidth*16*scale-viewWidth);
3001 realViewStart.y = clamp(y-viewHeight/2, 0, tilesHeight*16*scale-viewHeight);
3004 viewStart.x = realViewStart.x;
3005 viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
3008 if (onCameraTeleported) onCameraTeleported();
3012 const int ViewPortToleranceX = 16*1+8;
3013 const int ViewPortToleranceY = 16*1+8;
3015 final void fixCamera () {
3016 if (!player) return;
3017 if (viewWidth < 1 || viewHeight < 1) return;
3018 int scale = global.scale;
3019 auto alwaysCenterX = global.config.alwaysCenterPlayer;
3020 auto alwaysCenterY = alwaysCenterX;
3021 // calculate offset from viewport center (in game units), and fix viewport
3023 int camDestX = player.ix+8;
3024 int camDestY = player.iy+8;
3025 if (!cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y) != 0) {
3026 // slide camera to point
3027 if (cameraSlideToSpeed.x) camDestX = cameraSlideToCurr.x;
3028 if (cameraSlideToSpeed.y) camDestY = cameraSlideToCurr.y;
3029 int dx = cameraSlideToDest.x-camDestX;
3030 int dy = cameraSlideToDest.y-camDestY;
3031 //writeln("speed:(", cameraSlideToSpeed.x, ",", cameraSlideToSpeed.y, "); cur:(", camDestX, ",", camDestY, "); dest:(", cameraSlideToDest.x, ",", cameraSlideToDest.y, "); delta=(", dx, ",", dy, ")");
3032 if (dx && cameraSlideToSpeed.x != 0) {
3033 alwaysCenterX = true;
3034 if (abs(dx) <= abs(cameraSlideToSpeed.x)) {
3035 camDestX = cameraSlideToDest.x;
3037 camDestX += sign(dx)*abs(cameraSlideToSpeed.x);
3040 if (dy && abs(cameraSlideToSpeed.y) != 0) {
3041 alwaysCenterY = true;
3042 if (abs(dy) <= cameraSlideToSpeed.y) {
3043 camDestY = cameraSlideToDest.y;
3045 camDestY += sign(dy)*abs(cameraSlideToSpeed.y);
3048 //writeln(" new:(", camDestX, ",", camDestY, ")");
3049 if (cameraSlideToSpeed.x) cameraSlideToCurr.x = camDestX;
3050 if (cameraSlideToSpeed.y) cameraSlideToCurr.y = camDestY;
3054 if (cameraSlideToSpeed.x && !cameraSlideToPlayer) {
3055 realViewStart.x = clamp(camDestX*scale, 0, tilesWidth*16*scale-viewWidth);
3056 } else if (!player.cameraBlockX) {
3057 int x = camDestX*scale;
3058 int cx = realViewStart.x;
3059 if (alwaysCenterX) {
3062 int xofs = x-(cx+viewWidth/2);
3063 if (xofs < -ViewPortToleranceX*scale) cx += xofs+ViewPortToleranceX*scale;
3064 else if (xofs > ViewPortToleranceX*scale) cx += xofs-ViewPortToleranceX*scale;
3066 // slide back to player?
3067 if (cameraSlideToPlayer && cameraSlideToSpeed.x) {
3068 int prevx = cameraSlideToCurr.x*scale;
3069 int dx = (cx-prevx)/scale;
3070 if (abs(dx) <= cameraSlideToSpeed.x) {
3071 writeln("BACKSLIDE X COMPLETE!");
3072 cameraSlideToSpeed.x = 0;
3074 cx = prevx+(sign(dx)*cameraSlideToSpeed.x)*scale;
3075 cameraSlideToCurr.x += sign(dx)*cameraSlideToSpeed.x;
3076 if ((dx < 0 && cx <= 0) || (dx > 0 && cx >= tilesWidth*16*scale-viewWidth)) {
3077 writeln("BACKSLIDE X COMPLETE!");
3078 cameraSlideToSpeed.x = 0;
3082 realViewStart.x = clamp(cx, 0, tilesWidth*16*scale-viewWidth);
3086 if (cameraSlideToSpeed.y && !cameraSlideToPlayer) {
3087 realViewStart.y = clamp(camDestY*scale, 0, tilesHeight*16*scale-viewHeight);
3088 } else if (!player.cameraBlockY) {
3089 int y = camDestY*scale;
3090 int cy = realViewStart.y;
3091 if (alwaysCenterY) {
3092 cy = y-viewHeight/2;
3094 int yofs = y-(cy+viewHeight/2);
3095 if (yofs < -ViewPortToleranceY*scale) cy += yofs+ViewPortToleranceY*scale;
3096 else if (yofs > ViewPortToleranceY*scale) cy += yofs-ViewPortToleranceY*scale;
3098 // slide back to player?
3099 if (cameraSlideToPlayer && cameraSlideToSpeed.y) {
3100 int prevy = cameraSlideToCurr.y*scale;
3101 int dy = (cy-prevy)/scale;
3102 if (abs(dy) <= cameraSlideToSpeed.y) {
3103 writeln("BACKSLIDE Y COMPLETE!");
3104 cameraSlideToSpeed.y = 0;
3106 cy = prevy+(sign(dy)*cameraSlideToSpeed.y)*scale;
3107 cameraSlideToCurr.y += sign(dy)*cameraSlideToSpeed.y;
3108 if ((dy < 0 && cy <= 0) || (dy > 0 && cy >= tilesHeight*16*scale-viewHeight)) {
3109 writeln("BACKSLIDE Y COMPLETE!");
3110 cameraSlideToSpeed.y = 0;
3114 realViewStart.y = clamp(cy, 0, tilesHeight*16*scale-viewHeight);
3117 if (cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y) == 0) cameraSlideToPlayer = 0;
3120 //writeln(" new2:(", cameraCurrX, ",", cameraCurrY, ")");
3122 viewStart.x = realViewStart.x;
3123 viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
3128 // ////////////////////////////////////////////////////////////////////////// //
3129 // x0 and y0 are non-scaled (and will be scaled)
3130 final void drawSpriteAt (name sprName, float frnumf, int x0, int y0, optional bool small) {
3131 if (!sprName) return;
3132 auto spr = sprStore[sprName];
3133 if (!spr || !spr.frames.length) return;
3134 int scale = global.scale;
3137 int frnum = max(0, trunc(frnumf))%spr.frames.length;
3138 auto sfr = spr.frames[frnum];
3139 int sx0 = x0-sfr.xofs*scale;
3140 int sy0 = y0-sfr.yofs*scale;
3141 if (small && scale > 1) {
3142 sfr.tex.blitExt(sx0, sy0, round(sx0+sfr.tex.width*(scale/2.0)), round(sy0+sfr.tex.height*(scale/2.0)), 0, 0);
3144 sfr.tex.blitAt(sx0, sy0, scale);
3149 // x0 and y0 are non-scaled (and will be scaled)
3150 final void drawTextAt (int x0, int y0, string text) {
3152 int scale = global.scale;
3155 sprStore.renderText(x0, y0, text, scale);
3159 void renderCompass (float currFrameDelta) {
3160 if (!global.hasCompass) return;
3163 if (isRoom("rOlmec")) {
3166 } else if (isRoom("rOlmec2")) {
3172 bool hasMessage = osdHasMessage();
3173 foreach (MapTile et; allExits) {
3175 int exitX = et.ix, exitY = et.iy;
3176 int vx0 = viewStart.x/global.scale, vy0 = viewStart.y/global.scale;
3177 int vx1 = (viewStart.x+viewWidth)/global.scale;
3178 int vy1 = (viewStart.y+viewHeight)/global.scale;
3179 if (exitY > vy1-16) {
3181 drawSpriteAt(hasMessage ? 'sCompassSmallLL' : 'sCompassLL', 0, 0, 224);
3182 } else if (exitX > vx1-16) {
3183 drawSpriteAt(hasMessage ? 'sCompassSmallLR' : 'sCompassLR', 0, 304, 224);
3185 drawSpriteAt(hasMessage ? 'sCompassSmallDown' : 'sCompassDown', 0, exitX-vx0, 224);
3187 } else if (exitX < vx0) {
3188 drawSpriteAt(hasMessage ? 'sCompassSmallLeft' : 'sCompassLeft', 0, 0, exitY-vy0);
3189 } else if (exitX > vx1-16) {
3190 drawSpriteAt(hasMessage ? 'sCompassSmallRight' : 'sCompassRight', 0, 304, exitY-vy0);
3196 final bool totalsNameCmpCB (ref GameStats::TotalItem a, ref GameStats::TotalItem b) {
3197 auto sa = string(a.objName);
3198 auto sb = string(b.objName);
3202 void renderTransitionInfo (float currFrameDelta) {
3205 GameStats.sortTotalsList(stats.kills, &totalsNameCmpCB);
3208 foreach (int idx, ref auto k; stats.kills) {
3209 string s = string(k);
3210 maxLen = max(maxLen, s.length);
3214 sprStore.loadFont('sFontSmall');
3215 Video.color = 0xff_ff_00;
3216 foreach (int idx, ref auto k; stats.kills) {
3218 foreach (int xidx, ref auto d; stats.totalKills) {
3219 if (d.objName == k) { deaths = d.count; break; }
3221 //drawTextAt(16, 4+idx*8, va("%s: %d (%d)", string(k.objName).toUpperCase, k.count, deaths));
3222 drawTextAt(16, 4+idx*8, string(k).toUpperCase);
3223 drawTextAt(16+maxLen, 4+idx*8, va(": %d (%d)", k.count, deaths));
3229 void renderGhostTimer (float currFrameDelta) {
3230 if (ghostTimeLeft <= 0) return;
3231 //ghostTimeLeft /= 30; // frames -> seconds
3233 int hgt = Video.screenHeight-64;
3234 if (hgt < 1) return;
3235 int rhgt = round(float(hgt)*(float(ghostTimeLeft)/30.0)/1000.0);
3236 //writeln("hgt=", hgt, "; rhgt=", rhgt, "; gtl=", ghostTimeLeft/30);
3238 auto oclr = Video.color;
3239 Video.color = 0xcf_ff_7f_00;
3240 Video.fillRect(Video.screenWidth-20, 32, 16, hgt-rhgt);
3241 Video.color = 0x7f_ff_7f_00;
3242 Video.fillRect(Video.screenWidth-20, 32+(hgt-rhgt), 16, rhgt);
3248 void renderStarsHUD (float currFrameDelta) {
3249 bool scumSmallHud = global.config.scumSmallHud;
3251 //auto life = max(0, global.plife);
3252 int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3253 int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3254 //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3259 sprStore.loadFont('sFontSmall');
3262 sprStore.loadFont('sFont');
3266 Video.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3267 //draw_sprite(sHeart, -1, view_xview[0]+8, view_yview[0]+8);
3268 //draw_text(view_xview[0]+24, view_yview[0]+8, life);
3270 if (global.plife == 1) {
3271 drawSpriteAt('sHeartSmallBlink', global.heartBlink, 10, hhup-4);
3272 global.heartBlink += 0.1;
3273 if (global.heartBlink > 3) global.heartBlink = 0;
3275 drawSpriteAt('sHeartSmall', -1, 10, hhup-4);
3276 global.heartBlink = 0;
3279 if (global.plife == 1) {
3280 drawSpriteAt('sHeartBlink', global.heartBlink, 8, hhup);
3281 global.heartBlink += 0.1;
3282 if (global.heartBlink > 3) global.heartBlink = 0;
3284 drawSpriteAt('sHeart', -1, 8, hhup);
3285 global.heartBlink = 0;
3288 int life = clamp(global.plife, 0, 99);
3289 drawTextAt(16+8, hhup, va("%d", life));
3291 //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3292 drawSpriteAt('sShopkeeperIcon', -1, 64, hhup, scumSmallHud);
3293 drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", starsKills));
3295 if (starsRoomTimer1 > 0) {
3296 sprStore.loadFont('sFontSmall');
3297 Video.color = 0xff_ff_00;
3298 int scale = global.scale;
3299 sprStore.renderMultilineTextCentered(Video.screenWidth/2, 216*scale, va("SHOTGUN CHALLENGE BEGINS IN ~%d~", (starsRoomTimer1/30)+1), scale, 0xff_00_00);
3304 void renderSunHUD (float currFrameDelta) {
3305 bool scumSmallHud = global.config.scumSmallHud;
3307 //auto life = max(0, global.plife);
3308 int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3309 int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3310 //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3315 sprStore.loadFont('sFontSmall');
3318 sprStore.loadFont('sFont');
3322 Video.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3323 //draw_sprite(sHeart, -1, view_xview[0]+8, view_yview[0]+8);
3324 //draw_text(view_xview[0]+24, view_yview[0]+8, life);
3326 if (global.plife == 1) {
3327 drawSpriteAt('sHeartSmallBlink', global.heartBlink, 10, hhup-4);
3328 global.heartBlink += 0.1;
3329 if (global.heartBlink > 3) global.heartBlink = 0;
3331 drawSpriteAt('sHeartSmall', -1, 10, hhup-4);
3332 global.heartBlink = 0;
3335 if (global.plife == 1) {
3336 drawSpriteAt('sHeartBlink', global.heartBlink, 8, hhup);
3337 global.heartBlink += 0.1;
3338 if (global.heartBlink > 3) global.heartBlink = 0;
3340 drawSpriteAt('sHeart', -1, 8, hhup);
3341 global.heartBlink = 0;
3344 int life = clamp(global.plife, 0, 99);
3345 drawTextAt(16+8, hhup, va("%d", life));
3347 //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3348 drawSpriteAt('sDamselIcon', -1, 64, hhup, scumSmallHud);
3349 drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", sunScore));
3351 if (sunRoomTimer1 > 0) {
3352 sprStore.loadFont('sFontSmall');
3353 Video.color = 0xff_ff_00;
3354 int scale = global.scale;
3355 sprStore.renderMultilineTextCentered(Video.screenWidth/2, 216*scale, va("DAMSEL CHALLENGE BEGINS IN ~%d~", (sunRoomTimer1/30)+1), scale, 0xff_00_00);
3360 void renderMoonHUD (float currFrameDelta) {
3361 bool scumSmallHud = global.config.scumSmallHud;
3363 //auto life = max(0, global.plife);
3364 int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3365 int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3366 //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3371 sprStore.loadFont('sFontSmall');
3374 sprStore.loadFont('sFont');
3378 Video.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3380 //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3381 drawSpriteAt('sHoopsIcon', -1, 8, hhup, scumSmallHud);
3382 drawTextAt(8+16-(scumSmallHud ? 6 : 0), hhup, va("%d", moonScore));
3383 drawSpriteAt('sTimerIcon', -1, 64, hhup, scumSmallHud);
3384 drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", max(0, moonTimer)));
3386 if (moonRoomTimer1 > 0) {
3387 sprStore.loadFont('sFontSmall');
3388 Video.color = 0xff_ff_00;
3389 int scale = global.scale;
3390 sprStore.renderMultilineTextCentered(Video.screenWidth/2, 216*scale, va("ARCHERY CHALLENGE BEGINS IN ~%d~", (moonRoomTimer1/30)+1), scale, 0xff_00_00);
3395 void renderHUD (float currFrameDelta) {
3396 if (levelKind == LevelKind.Stars) { renderStarsHUD(currFrameDelta); return; }
3397 if (levelKind == LevelKind.Sun) { renderSunHUD(currFrameDelta); return; }
3398 if (levelKind == LevelKind.Moon) { renderMoonHUD(currFrameDelta); return; }
3400 if (!isHUDEnabled()) return;
3402 if (levelKind == LevelKind.Transition) { renderTransitionInfo(currFrameDelta); return; }
3410 bool scumSmallHud = global.config.scumSmallHud;
3411 if (!global.config.optSGAmmo) moneyX = ammoX;
3414 sprStore.loadFont('sFontSmall');
3417 sprStore.loadFont('sFont');
3420 //int alpha = 0x6f_00_00_00;
3421 int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3422 int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3424 //Video.color = 0xff_ff_ff;
3425 Video.color = 0xff_ff_ff|talpha;
3429 if (global.plife == 1) {
3430 drawSpriteAt('sHeartSmallBlink', global.heartBlink, lifeX+2, 4-hhup);
3431 global.heartBlink += 0.1;
3432 if (global.heartBlink > 3) global.heartBlink = 0;
3434 drawSpriteAt('sHeartSmall', -1, lifeX+2, 4-hhup);
3435 global.heartBlink = 0;
3438 if (global.plife == 1) {
3439 drawSpriteAt('sHeartBlink', global.heartBlink, lifeX, 8-hhup);
3440 global.heartBlink += 0.1;
3441 if (global.heartBlink > 3) global.heartBlink = 0;
3443 drawSpriteAt('sHeart', -1, lifeX, 8-hhup);
3444 global.heartBlink = 0;
3448 int life = clamp(global.plife, 0, 99);
3449 //if (!scumHud && life > 99) life = 99;
3450 drawTextAt(lifeX+16, 8-hhup, va("%d", life));
3453 if (global.hasStickyBombs && global.stickyBombsActive) {
3454 if (scumSmallHud) drawSpriteAt('sStickyBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sStickyBombIcon', -1, bombX, 8-hhup);
3456 if (scumSmallHud) drawSpriteAt('sBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sBombIcon', -1, bombX, 8-hhup);
3458 int n = global.bombs;
3459 if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3460 drawTextAt(bombX+16, 8-hhup, va("%d", n));
3463 if (scumSmallHud) drawSpriteAt('sRopeIconSmall', -1, ropeX+2, 4-hhup); else drawSpriteAt('sRopeIcon', -1, ropeX, 8-hhup);
3465 if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3466 drawTextAt(ropeX+16, 8-hhup, va("%d", n));
3469 if (global.config.optSGAmmo && (!player || player.holdItem !isa ItemWeaponBow)) {
3470 if (scumSmallHud) drawSpriteAt('sShellsIcon', -1, ammoX+6, 8-hhup); else drawSpriteAt('sShellsIcon', -1, ammoX+7, 12-hhup);
3472 if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3473 drawTextAt(ammoX+16, 8-hhup, va("%d", n));
3474 } else if (player && player.holdItem isa ItemWeaponBow) {
3475 if (scumSmallHud) drawSpriteAt('sArrowRight', -1, ammoX+6, 8-hhup); else drawSpriteAt('sArrowRight', -1, ammoX+7, 12-hhup);
3477 if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3478 drawTextAt(ammoX+16, 8-hhup, va("%d", n));
3482 if (scumSmallHud) drawSpriteAt('sDollarSignSmall', -1, moneyX+2, 3-hhup); else drawSpriteAt('sDollarSign', -1, moneyX, 8-hhup);
3483 drawTextAt(moneyX+16, 8-hhup, va("%d", stats.money-xmoney));
3486 Video.color = 0xff_ff_ff|ialpha;
3488 int ity = (scumSmallHud ? 18-hhup : 24-hhup);
3491 if (global.hasUdjatEye) {
3492 if (global.udjatBlink) drawSpriteAt('sUdjatEyeIcon2', -1, n, ity); else drawSpriteAt('sUdjatEyeIcon', -1, n, ity);
3495 if (global.hasAnkh) { drawSpriteAt('sAnkhIcon', -1, n, ity); n += 20; }
3496 if (global.hasCrown) { drawSpriteAt('sCrownIcon', -1, n, ity); n += 20; }
3497 if (global.hasKapala) {
3498 if (global.bloodLevel == 0) drawSpriteAt('sKapalaIcon', 0, n, ity);
3499 else if (global.bloodLevel <= 2) drawSpriteAt('sKapalaIcon', 1, n, ity);
3500 else if (global.bloodLevel <= 4) drawSpriteAt('sKapalaIcon', 2, n, ity);
3501 else if (global.bloodLevel <= 6) drawSpriteAt('sKapalaIcon', 3, n, ity);
3502 else if (global.bloodLevel <= 8) drawSpriteAt('sKapalaIcon', 4, n, ity);
3505 if (global.hasSpectacles) { drawSpriteAt('sSpectaclesIcon', -1, n, ity); n += 20; }
3506 if (global.hasGloves) { drawSpriteAt('sGlovesIcon', -1, n, ity); n += 20; }
3507 if (global.hasMitt) { drawSpriteAt('sMittIcon', -1, n, ity); n += 20; }
3508 if (global.hasSpringShoes) { drawSpriteAt('sSpringShoesIcon', -1, n, ity); n += 20; }
3509 if (global.hasJordans) { drawSpriteAt('sJordansIcon', -1, n, ity); n += 20; }
3510 if (global.hasSpikeShoes) { drawSpriteAt('sSpikeShoesIcon', -1, n, ity); n += 20; }
3511 if (global.hasCape) { drawSpriteAt('sCapeIcon', -1, n, ity); n += 20; }
3512 if (global.hasJetpack) { drawSpriteAt('sJetpackIcon', -1, n, ity); n += 20; }
3513 if (global.hasStickyBombs) { drawSpriteAt('sPasteIcon', -1, n, ity); n += 20; }
3514 if (global.hasCompass) { drawSpriteAt('sCompassIcon', -1, n, ity); n += 20; }
3515 if (global.hasParachute) { drawSpriteAt('sParachuteIcon', -1, n, ity); n += 20; }
3517 if (player.holdItem && player.holdItem.objName == 'Bow' && global.arrows > 0) {
3520 while (m <= global.arrows && m <= 20 && malpha > 0) {
3521 Video.color = trunc(malpha*255)<<24|0xff_ff_ff;
3522 drawSpriteAt('sArrowIcon', -1, n, ity);
3524 if (m > 10) malpha -= 0.1; //and global.arrows > 20 malpha -= 0.1
3530 sprStore.loadFont('sFontSmall');
3531 Video.color = 0xff_ff_00|talpha;
3532 if (scumSmallHud) drawTextAt(moneyX+8, 8+8-hhup, va("+%d", xmoney));
3533 else drawTextAt(moneyX, 8+16-hhup, va("+%d", xmoney));
3536 Video.color = 0xff_ff_ff;
3537 if (global.config.ghostShowTime) renderGhostTimer(currFrameDelta);
3541 // ////////////////////////////////////////////////////////////////////////// //
3542 private transient array!MapEntity renderVisibleCids;
3543 private transient array!MapEntity renderVisibleLights;
3544 private transient array!MapTile renderFrontTiles; // normal, with fg
3546 final bool renderSortByDepth (MapEntity oa, MapEntity ob) {
3547 auto da = oa.depth, db = ob.depth;
3548 if (da == db) return (oa.objId < ob.objId);
3553 const int RenderEdgePixNormal = 64;
3554 const int RenderEdgePixLight = 256;
3556 #ifndef EXPERIMENTAL_RENDER_CACHE
3557 enum skipListCreation = false;
3560 void renderWithOfs (int xofs, int yofs, float currFrameDelta) {
3561 int scale = global.scale;
3563 // don't touch framebuffer alpha
3564 Video.colorMask = Video::CMask.Colors;
3565 Video.color = 0xff_ff_ff;
3567 bool isDarkLevel = global.darkLevel;
3570 switch (global.config.scumPlayerLit) {
3571 case 0: player.lightRadius = 0; break; // never
3572 case 1: // only in "scumDarkness"
3573 player.lightRadius = (global.config.scumDarkness >= 2 ? 96 : 32);
3576 player.lightRadius = 96;
3581 // render cave background
3584 int bgw = levBGImg.tex.width*scale;
3585 int bgh = levBGImg.tex.height*scale;
3586 int bgTW = (tilesWidth*tsz+bgw-1)/bgw;
3587 int bgTH = (tilesHeight*tsz+bgh-1)/bgh;
3588 int bgX0 = max(0, xofs/bgw);
3589 int bgY0 = max(0, yofs/bgh);
3590 int bgX1 = min(bgTW, (xofs+viewWidth+bgw-1)/bgw);
3591 int bgY1 = min(bgTH, (yofs+viewHeight+bgh-1)/bgh);
3592 foreach (int ty; bgY0..bgY1) {
3593 foreach (int tx; bgX0..bgX1) {
3594 int x0 = tx*bgw-xofs;
3595 int y0 = ty*bgh-yofs;
3596 levBGImg.tex.blitAt(x0, y0, scale);
3601 int RenderEdgePix = (global.darkLevel ? RenderEdgePixLight : RenderEdgePixNormal);
3603 // render background tiles
3604 for (MapBackTile bt = backtiles; bt; bt = bt.next) {
3605 bt.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3608 // collect visible special tiles
3609 #ifdef EXPERIMENTAL_RENDER_CACHE
3610 bool skipListCreation = (lastRenderTime == time && renderVisibleCids.length); //FIXME
3613 if (!skipListCreation) {
3614 renderVisibleCids.clear();
3615 renderVisibleLights.clear();
3616 renderFrontTiles.clear();
3618 int endVX = xofs+viewWidth;
3619 int endVY = yofs+viewHeight;
3623 if (player && player.visible) renderVisibleCids[$] = player; // include player in render list
3625 //FIXME: drop lit objects which cannot affect visible area
3627 // collect visible objects
3628 foreach (MapEntity o; objGrid.inRectPix(xofs/scale-RenderEdgePix, yofs/scale-RenderEdgePix, (viewWidth+scale-1)/scale+RenderEdgePix*2, (viewHeight+scale-1)/scale+RenderEdgePix*2, precise:false)) {
3629 if (!o.visible) continue;
3630 auto tile = MapTile(o);
3632 if (isDarkLevel && (o.lightRadius > 4 || tile.litWholeTile)) renderVisibleLights[$] = o;
3633 if (tile.invisible) continue;
3634 if (tile.bgfront) renderFrontTiles[$] = tile;
3635 if (tile.bgback) tile.drawWithOfsBack(xofs, yofs, scale, currFrameDelta);
3637 if (isDarkLevel && o.lightRadius > 4) renderVisibleLights[$] = o;
3639 // check if the object is really visible -- this will speed up later sorting
3640 int fx0, fy0, fx1, fy1;
3641 auto spf = o.getSpriteFrame(default, out fx0, out fy0, out fx1, out fy1);
3642 if (!spf) continue; // no sprite -- nothing to draw (no, really)
3643 int ix = o.ix, iy = o.iy;
3644 int x0 = (ix+fx0)*scale, y0 = (iy+fy0)*scale;
3645 int x1 = (ix+fx1)*scale, y1 = (iy+fy1)*scale;
3646 if (x1 <= xofs || y1 <= yofs || x0 >= endVX || y0 >= endVY) {
3650 renderVisibleCids[$] = o;
3653 foreach (MapEntity o; objGrid.allObjects()) {
3654 if (!o.visible) continue;
3655 auto tile = MapTile(o);
3657 if (isDarkLevel && (o.lightRadius > 4 || tile.litWholeTile)) renderVisibleLights[$] = o;
3658 if (tile.invisible) continue;
3659 if (tile.bgfront) renderFrontTiles[$] = tile;
3660 if (tile.bgback) tile.drawWithOfsBack(xofs, yofs, scale, currFrameDelta);
3662 if (isDarkLevel && o.lightRadius > 4) renderVisibleLights[$] = o;
3664 renderVisibleCids[$] = o;
3667 //writeln("::: ", cnt, " invisible objects dropped");
3669 renderVisibleCids.sort(&renderSortByDepth);
3670 lastRenderTime = time;
3673 auto depth4Start = 0;
3674 foreach (auto xidx, MapEntity o; renderVisibleCids) {
3681 // render objects (part one: depth > 3)
3682 foreach (auto idx; depth4Start..renderVisibleCids.length; reverse) {
3683 MapEntity o = renderVisibleCids[idx];
3684 //if (idx && renderSortByDepth(o, renderVisibleCids[idx-1])) writeln("VIOLATION!");
3685 o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3688 // render object (part two: front tile parts, depth 3.5)
3689 foreach (MapTile tile; renderFrontTiles) {
3690 tile.drawWithOfsFront(xofs, yofs, scale, currFrameDelta);
3693 // render objects (part three: depth <= 3)
3694 foreach (auto idx; 0..depth4Start; reverse) {
3695 MapEntity o = renderVisibleCids[idx];
3696 o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3697 //done above;if (isDarkLevel && (o.lightRadius > 4 || (o isa MapTile && MapTile(o).litWholeTile))) renderVisibleLights[$] = o;
3700 // render player post-effects, so cape or jetpack will be correctly rendered on level exit, for example
3701 player.lastDrawWithOfs(xofs, yofs, scale, currFrameDelta);
3705 auto ltex = bgtileStore.lightTexture('ltx512', 512);
3707 // set screen alpha to min
3708 Video.colorMask = Video::CMask.Alpha;
3709 Video.blendMode = Video::BlendMode.None;
3710 Video.color = 0xff_ff_ff_ff;
3711 Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
3712 //Video.colorMask = Video::CMask.All;
3715 // also, stencil 'em, so we can filter dark areas
3716 Video.textureFiltering = true;
3717 Video.stencil = true;
3718 Video.stencilFunc(Video::StencilFunc.Always, 1);
3719 Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Replace);
3720 Video.alphaTestFunc = Video::AlphaFunc.Greater;
3721 Video.alphaTestVal = 0.03;
3722 Video.color = 0xff_ff_ff;
3723 Video.blendFunc = Video::BlendFunc.Max;
3724 Video.blendMode = Video::BlendMode.Blend; // anything except `Normal`
3725 Video.colorMask = Video::CMask.Alpha;
3727 foreach (MapEntity e; renderVisibleLights) {
3729 e.getInterpCoords(currFrameDelta, scale, out xi, out yi);
3730 auto tile = MapTile(e);
3731 if (tile && tile.litWholeTile) {
3732 //Video.color = 0xff_ff_ff;
3733 Video.fillRect(xi-xofs, yi-yofs, e.width*scale, e.height*scale);
3735 int lrad = e.lightRadius;
3736 if (lrad < 4) continue; // just in case
3738 float lightscale = float(lrad*scale)/float(ltex.tex.width);
3739 #ifdef OLD_LIGHT_OFFSETS
3740 int fx0, fy0, fx1, fy1;
3742 auto spf = e.getSpriteFrame(out doMirror, out fx0, out fy0, out fx1, out fy1);
3744 xi += (fx1-fx0)*scale/2;
3745 yi += (fy1-fy0)*scale/2;
3749 e.getLightOffset(out lxofs, out lyofs);
3754 lrad = lrad*scale/2;
3757 ltex.tex.blitAt(xi, yi, lightscale);
3759 Video.textureFiltering = false;
3761 // modify only lit parts
3762 Video.stencilFunc(Video::StencilFunc.Equal, 1);
3763 Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Keep);
3764 // multiply framebuffer colors by framebuffer alpha
3765 Video.color = 0xff_ff_ff; // it doesn't matter
3766 Video.blendFunc = Video::BlendFunc.Add;
3767 Video.blendMode = Video::BlendMode.DstMulDstAlpha;
3768 Video.colorMask = Video::CMask.Colors;
3769 Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
3771 // filter unlit parts
3772 Video.stencilFunc(Video::StencilFunc.NotEqual, 1);
3773 Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Keep);
3774 Video.blendFunc = Video::BlendFunc.Add;
3775 Video.blendMode = Video::BlendMode.Filter;
3776 Video.colorMask = Video::CMask.Colors;
3777 Video.color = 0x00_00_18;
3778 Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
3781 Video.blendFunc = Video::BlendFunc.Add;
3782 Video.blendMode = Video::BlendMode.Normal;
3783 Video.colorMask = Video::CMask.All;
3784 Video.alphaTestFunc = Video::AlphaFunc.Always;
3785 Video.stencil = false;
3788 // clear visible objects list (nope)
3789 //renderVisibleCids.clear();
3790 //renderVisibleLights.clear();
3793 if (global.config.drawHUD) renderHUD(currFrameDelta);
3794 renderCompass(currFrameDelta);
3796 float osdTimeLeft, osdTimeStart;
3797 string msg = osdGetMessage(out osdTimeLeft, out osdTimeStart);
3799 auto ct = GetTickCount();
3801 sprStore.loadFont('sFontSmall');
3802 auto msgHeight = sprStore.getMultilineTextHeight(msg, msgScale);
3803 int x = Video.screenWidth/2;
3804 int y = Video.screenHeight-64-msgHeight;
3805 auto oldColor = Video.color;
3806 Video.color = 0xff_ff_00;
3807 if (osdTimeLeft < 0.5) {
3808 int alpha = 255-clamp(int(510*osdTimeLeft), 0, 255);
3809 Video.color = Video.color|(alpha<<24);
3810 } else if (ct-osdTimeStart < 0.5) {
3811 osdTimeStart = ct-osdTimeStart;
3812 int alpha = 255-clamp(int(510*osdTimeStart), 0, 255);
3813 Video.color = Video.color|(alpha<<24);
3815 sprStore.renderMultilineTextCentered(x, y, msg, msgScale, 0x00_ff_00, 0xff_ff_ff);
3816 Video.color = oldColor;
3819 if (inWinCutscene) renderWinCutsceneOverlay();
3820 Video.color = 0xff_ff_ff;
3824 // ////////////////////////////////////////////////////////////////////////// //
3825 final class!MapObject findGameObjectClassByName (name aname) {
3826 if (!aname) return none; // just in case
3827 auto co = FindClassByGameObjName(aname);
3829 writeln("***UNKNOWN GAME OBJECT: '", aname, "'");
3832 co = GetClassReplacement(co);
3833 if (!co) FatalError("findGameObjectClassByName: WTF?!");
3834 if (!class!MapObject(co)) FatalError("findGameTileClassByName: object '%n' is not a map object, it is a `%n`", aname, GetClassName(co));
3835 return class!MapObject(co);
3839 final class!MapTile findGameTileClassByName (name aname) {
3840 if (!aname) return none; // just in case
3841 auto co = FindClassByGameObjName(aname);
3842 if (!co) return MapTile; // unknown names will be routed directly to tile object
3843 co = GetClassReplacement(co);
3844 if (!co) FatalError("findGameTileClassByName: WTF?!");
3845 if (!class!MapTile(co)) FatalError("findGameTileClassByName: object '%n' is not a tile, it is a `%n`", aname, GetClassName(co));
3846 return class!MapTile(co);
3850 final MapObject findAnyObjectOfType (name aname) {
3851 if (!aname) return none;
3852 auto cls = FindClassByGameObjName(aname);
3853 if (!cls) return none;
3854 foreach (MapObject obj; objGrid.allObjects(MapObject)) {
3855 if (obj.spectral) continue;
3856 if (obj isa cls) return obj;
3862 // ////////////////////////////////////////////////////////////////////////// //
3863 final bool isRopePlacedAt (int x, int y) {
3865 foreach (ref auto v; covered) v = false;
3866 foreach (MapTile t; objGrid.inRectPix(x, y-8, 1, 17, precise:false, castClass:MapTileRope)) {
3867 //if (!cbIsRopeTile(t)) continue;
3868 if (t.ix != x) continue;
3869 if (t.iy == y) return true;
3870 foreach (int ty; t.iy..t.iy+8) {
3872 if (d >= 0 && d < covered.length) covered[d] = true;
3875 // check if the whole rope height is completely covered with ropes
3876 foreach (auto v; covered) if (!v) return false;
3881 final MapTile CreateMapTile (int xpos, int ypos, name aname) {
3882 if (!aname) FatalError("cannot create typeless tile");
3883 auto tclass = findGameTileClassByName(aname);
3884 if (!tclass) return none;
3885 MapTile tile = SpawnObject(tclass);
3886 tile.global = global;
3888 tile.objName = aname;
3889 tile.objType = aname; // just in case
3892 tile.objId = ++lastUsedObjectId;
3893 if (!tile.initialize()) FatalError("cannot create tile '%n'", aname);
3898 final bool PutSpawnedMapTile (int x, int y, MapTile tile, optional bool putToGrid) {
3899 if (!tile || !tile.isInstanceAlive) return false;
3901 if (!putToGrid) putToGrid = (tile.moveable || tile.toSpecialGrid || tile.width != 16 || tile.height != 16 || x%16 != 0 || y%16 != 0);
3903 //writeln("putting tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (putToGrid ? "special " : ""), "grid, pos=(", x/16.0, ",", y/16.0, ")");
3906 int mapx = x/16, mapy = y/16;
3907 if (mapx < 0 || mapx >= tilesWidth || mapy < 0 || mapy >= tilesHeight) return false;
3910 // if we already have rope tile there, there is no reason to add another one
3911 if (tile isa MapTileRope) {
3912 if (isRopePlacedAt(x, y)) return false;
3915 // activate special or animated tile
3916 tile.active = tile.active || putToGrid || tile.moveable || tile.toSpecialGrid || tile.lava /*|| tile.water*/; // will be done in MakeMapTile
3917 // animated tiles must be active
3919 auto spr = tile.getSprite();
3920 if (spr && spr.frames.length > 1) {
3921 writeln("activated animated tile '", tile.objName, "'");
3929 //if (tile isa TitleTileCopy) writeln("*** PUTTING COPYRIGHT TILE");
3930 tile.toSpecialGrid = true;
3931 if (!tile.dontReplaceOthers && x&16 == 0 && y%16 == 0) {
3932 auto t = getTileAtGridAny(x/16, y/16);
3933 if (t && !t.immuneToReplacement) {
3934 writeln("REPLACING tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
3935 writeln(" NEW tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (tile.toSpecialGrid ? "special " : ""), "grid, pos=(", tile.fltx/16.0, ",", tile.flty/16.0, ")");
3939 objGrid.insert(tile);
3941 //writeln("SIZE: ", tilesWidth, "x", tilesHeight);
3942 setTileAtGrid(x/16, y/16, tile);
3943 auto t = getTileAtGridAny(x/16, y/16);
3946 writeln("putting tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (putToGrid ? "special " : ""), "grid, pos=(", x/16.0, ",", y/16.0, ")");
3947 checkTilesInRect(x/16, y/16, 16, 16, delegate bool (MapTile tile) {
3948 writeln(" *** tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (tile.toSpecialGrid || tile.moveable ? "special " : ""), "grid, pos=(", tile.fltx/16.0, ",", tile.flty/16.0, ")");
3951 FatalError("FUUUUUU");
3956 if (tile.enter) registerEnter(tile);
3957 if (tile.exit) registerExit(tile);
3963 // won't call `onDestroy()`
3964 final void RemoveMapTileFromGrid (int tileX, int tileY, optional string reason) {
3965 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
3966 auto t = getTileAtGridAny(tileX, tileY);
3968 writeln("REMOVING(RMT", (reason ? ":"~reason : ""), ") tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
3976 final MapTile MakeMapTile (int mapx, int mapy, name aname, optional bool putToGrid) {
3977 //if (mapy >= tilesHeight) { writeln("OOM: ", aname, "; ty=", mapy, "; hgt=", tilesHeight); }
3978 if (mapx < 0 || mapx >= tilesWidth || mapy < 0 || mapy >= tilesHeight) return none;
3980 // if we already have rope tile there, there is no reason to add another one
3981 if (aname == 'oRope') {
3982 if (isRopePlacedAt(mapx*16, mapy*16)) return none;
3985 auto tile = CreateMapTile(mapx*16, mapy*16, aname);
3986 if (!tile) return none;
3987 if (!PutSpawnedMapTile(mapx*16, mapy*16, tile, putToGrid!optional)) {
3997 final void MarkTileAsWet (int tileX, int tileY) {
3998 auto t = getTileAtGrid(tileX, tileY);
3999 if (t) t.wet = true;
4004 final MapTile MakeMapTileAtPix (int xpix, int ypix, name aname, optional bool putToGrid) {
4005 // if we already have rope tile there, there is no reason to add another one
4006 if (aname == 'oRope') {
4007 if (isRopePlacedAt(xpix, ypix)) return none;
4010 auto tile = CreateMapTile(xpix, ypix, aname);
4011 if (!tile) return none;
4012 if (!PutSpawnedMapTile(xpix, ypix, tile, putToGrid!optional)) {
4021 final MapTile MakeMapRopeTileAt (int x0, int y0) {
4022 // if we already have rope tile there, there is no reason to add another one
4023 if (isRopePlacedAt(x0, y0)) return none;
4025 auto tile = CreateMapTile(x0, y0, 'oRope');
4026 if (!PutSpawnedMapTile(x0, y0, tile, putToGrid:true)) {
4035 // ////////////////////////////////////////////////////////////////////////// //
4036 final MapBackTile CreateBgTile (name sprName, optional int atx0, optional int aty0, optional int aw, optional int ah) {
4037 BackTileImage img = bgtileStore[sprName];
4038 auto res = SpawnObject(MapBackTile);
4039 res.global = global;
4042 res.bgtName = sprName;
4043 if (specified_atx0) res.tx0 = atx0;
4044 if (specified_aty0) res.ty0 = aty0;
4045 if (specified_aw) res.w = aw;
4046 if (specified_ah) res.h = ah;
4047 if (!res.initialize()) FatalError("cannot create background tile with image '%n'", sprName);
4052 // ////////////////////////////////////////////////////////////////////////// //
4054 background The background asset from which the new tile will be extracted.
4055 left The x coordinate of the left of the new tile, relative to the background asset's top left corner.
4056 top The y coordinate of the top of the new tile, relative to the background assets top left corner.
4057 width The width of the tile.
4058 height The height of the tile.
4059 x The x position in the room to place the tile.
4060 y The y position in the room to place the tile.
4061 depth The depth at which to place the tile.
4063 final void MakeMapBackTile (name bgname, int left, int top, int width, int height, int x, int y, int depth) {
4064 if (width < 1 || height < 1 || !bgname) return;
4065 auto bgt = bgtileStore[bgname];
4066 if (!bgt) FatalError("cannot load background '%n'", bgname);
4067 MapBackTile bt = SpawnObject(MapBackTile);
4070 bt.objName = bgname;
4072 bt.bgtName = bgname;
4080 // find a place for it
4085 // back tiles with the highest depth should come first
4086 MapBackTile ct = backtiles, cprev = none;
4087 while (ct && depth <= ct.depth) { cprev = ct; ct = ct.next; }
4090 bt.next = cprev.next;
4093 bt.next = backtiles;
4099 // ////////////////////////////////////////////////////////////////////////// //
4100 final MapObject SpawnMapObjectWithClass (class!MapObject oclass) {
4101 if (!oclass) return none;
4103 MapObject obj = SpawnObject(oclass);
4104 if (!obj) FatalError("cannot create map object '%n'", GetClassName(oclass));
4106 //if (aname == 'oShells' || aname == 'oShellSingle' || aname == 'oRopePile') writeln("*** CREATED '", aname, "'");
4108 obj.global = global;
4110 obj.objId = ++lastUsedObjectId;
4116 final MapObject SpawnMapObject (name aname) {
4117 if (!aname) return none;
4118 auto res = SpawnMapObjectWithClass(findGameObjectClassByName(aname));
4119 if (res && !res.objType) res.objType = aname; // just in case
4124 final MapObject PutSpawnedMapObject (int x, int y, MapObject obj) {
4125 if (!obj /*|| obj.global || obj.level*/) return none; // oops
4129 if (!obj.initialize()) { delete obj; return none; } // not fatal
4137 final MapObject MakeMapObject (int x, int y, name aname) {
4138 MapObject obj = SpawnMapObject(aname);
4139 obj = PutSpawnedMapObject(x, y, obj);
4144 // ////////////////////////////////////////////////////////////////////////// //
4145 int winCutSceneTimer = -1;
4146 int winVolcanoTimer = -1;
4147 int winCutScenePhase = 0;
4148 int winSceneDrawStatus = 0;
4149 int winMoneyCount = 0;
4151 bool winFadeOut = false;
4152 int winFadeLevel = 0;
4153 int winCutsceneSkip = 0; // 1: waiting for pay release; 2: pay released, do skip
4154 bool winCutsceneSwitchToNext = false;
4157 void startWinCutscene () {
4158 global.hasParachute = false;
4160 winCutsceneSwitchToNext = false;
4161 winCutsceneSkip = 0;
4162 isKeyPressed(GameConfig::Key.Pay);
4163 isKeyReleased(GameConfig::Key.Pay);
4165 auto olddel = ImmediateDelete;
4166 ImmediateDelete = false;
4171 addBackgroundGfxDetails();
4173 levBGImgName = 'bgCave';
4174 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
4176 blockWaterChecking = true;
4180 ImmediateDelete = olddel;
4181 CollectGarbage(true); // destroy delayed objects too
4183 if (dumpGridStats) objGrid.dumpStats();
4185 playerExited = false; // just in case
4186 playerExitDoor = none;
4194 winCutSceneTimer = -1;
4195 winCutScenePhase = 0;
4198 if (global.config.gameMode != GameConfig::GameMode.Vanilla) {
4199 if (global.config.bizarre) {
4200 global.yasmScore = 1;
4201 global.config.bizarrePlusTitle = true;
4204 array!MapTile toReplace;
4205 forEachTile(delegate bool (MapTile t) {
4206 if (t.objType == 'oGTemple' ||
4207 t.objType == 'oIce' ||
4208 t.objType == 'oDark' ||
4209 t.objType == 'oBrick' ||
4210 t.objType == 'oLush')
4217 foreach (MapTile t; miscTileGrid.allObjects()) {
4218 if (t.objType == 'oGTemple' ||
4219 t.objType == 'oIce' ||
4220 t.objType == 'oDark' ||
4221 t.objType == 'oBrick' ||
4222 t.objType == 'oLush')
4228 foreach (MapTile t; toReplace) {
4230 t.cleanDeath = true;
4231 if (rand(1,120) == 1) instance_change(oGTemple, false);
4232 else if (rand(1,100) == 1) instance_change(oIce, false);
4233 else if (rand(1,90) == 1) instance_change(oDark, false);
4234 else if (rand(1,80) == 1) instance_change(oBrick, false);
4235 else if (rand(1,70) == 1) instance_change(oLush, false);
4243 if (rand(1,5) == 1) instance_change(oLush, false);
4248 //!instance_create(0, 0, oBricks);
4250 //shakeToggle = false;
4251 //oPDummy.status = 2;
4256 if (global.kaliPunish >= 2) {
4257 instance_create(oPDummy.x, oPDummy.y+2, oBall2);
4258 obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
4260 obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
4262 obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
4264 obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
4271 void startWinCutsceneVolcano () {
4272 global.hasParachute = false;
4274 writeln("VOLCANO HOLD 0: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4275 writeln("VOLCANO PICK 0: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4279 winCutsceneSwitchToNext = false;
4280 auto olddel = ImmediateDelete;
4281 ImmediateDelete = false;
4285 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
4287 blockWaterChecking = true;
4289 ImmediateDelete = olddel;
4290 CollectGarbage(true); // destroy delayed objects too
4292 spawnPlayerAt(2*16+8, 11*16+8);
4293 player.dir = MapEntity::Dir.Right;
4295 playerExited = false; // just in case
4296 playerExitDoor = none;
4304 winCutSceneTimer = -1;
4305 winCutScenePhase = 0;
4307 MakeMapTile(0, 0, 'oEnd2BG');
4308 realViewStart.x = 0;
4309 realViewStart.y = 0;
4318 player.dead = false;
4319 player.active = true;
4320 player.visible = false;
4321 player.removeBallAndChain(temp:true);
4322 player.stunned = false;
4323 player.status = MapObject::FALLING;
4324 if (player.holdItem) player.holdItem.visible = false;
4325 player.fltx = 320/2;
4329 writeln("VOLCANO HOLD 1: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4330 writeln("VOLCANO PICK 1: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4335 void startWinCutsceneWinFall () {
4336 global.hasParachute = false;
4338 writeln("FALL HOLD 0: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4339 writeln("FALL PICK 0: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4343 winCutsceneSwitchToNext = false;
4345 auto olddel = ImmediateDelete;
4346 ImmediateDelete = false;
4350 setMenuTilesVisible(false);
4352 //addBackgroundGfxDetails();
4355 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
4357 blockWaterChecking = true;
4361 ImmediateDelete = olddel;
4362 CollectGarbage(true); // destroy delayed objects too
4364 if (dumpGridStats) objGrid.dumpStats();
4366 playerExited = false; // just in case
4367 playerExitDoor = none;
4375 winCutSceneTimer = -1;
4376 winCutScenePhase = 0;
4378 player.dead = false;
4379 player.active = true;
4380 player.visible = false;
4381 player.removeBallAndChain(temp:true);
4382 player.stunned = false;
4383 player.status = MapObject::FALLING;
4384 if (player.holdItem) player.holdItem.visible = false;
4385 player.fltx = 320/2;
4388 winSceneDrawStatus = 0;
4395 writeln("FALL HOLD 1: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4396 writeln("FALL PICK 1: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4401 void setGameOver () {
4402 if (inWinCutscene) {
4403 player.visible = false;
4404 player.removeBallAndChain(temp:true);
4405 if (player.holdItem) player.holdItem.visible = false;
4408 if (inWinCutscene > 0) {
4411 winSceneDrawStatus = 8;
4416 MapTile findEndPlatTile () {
4417 return forEachTile(delegate bool (MapTile t) { return (t isa MapTileEndPlat); }, castClass:MapTileEndPlat);
4421 MapObject findBigTreasure () {
4422 return forEachObject(delegate bool (MapObject o) { return (o isa MapObjectBigTreasure); }, castClass:MapObjectBigTreasure);
4426 void setMenuTilesVisible (bool vis) {
4428 forEachTile(delegate bool (MapTile t) {
4429 if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4430 t.invisible = false;
4435 forEachTile(delegate bool (MapTile t) {
4436 if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4445 void setMenuTilesOnTop () {
4446 forEachTile(delegate bool (MapTile t) {
4447 if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4455 void winCutscenePlayerControl (PlayerPawn plr) {
4456 auto payPress = isKeyPressed(GameConfig::Key.Pay);
4457 auto payRelease = isKeyReleased(GameConfig::Key.Pay);
4459 switch (winCutsceneSkip) {
4460 case 0: // nothing was pressed
4461 if (payPress) winCutsceneSkip = 1;
4463 case 1: // waiting for pay release
4464 if (payRelease) winCutsceneSkip = 2;
4466 case 2: // pay released, do skip
4471 // first winning room
4472 if (inWinCutscene == 1) {
4473 if (plr.ix < 448+8) {
4478 // waiting for chest to open
4479 if (winCutScenePhase == 0) {
4480 winCutSceneTimer = 120/2;
4481 winCutScenePhase = 1;
4486 if (winCutScenePhase == 1) {
4487 if (--winCutSceneTimer == 0) {
4488 winCutScenePhase = 2;
4489 winCutSceneTimer = 20;
4490 forEachObject(delegate bool (MapObject o) {
4491 if (o isa MapObjectBigChest) {
4492 o.setSprite(global.config.gameMode == GameConfig::GameMode.Vanilla ? 'sBigChestOpen' : 'sBigChestOpen2');
4493 auto treasure = MakeMapObject(o.ix, o.iy, 'oBigTreasure');
4497 o.playSound('sndClick');
4498 //!!!if (global.config.gameMode != GameConfig::GameMode.Vanilla) scrSprayGems(oBigChest.x+24, oBigChest.y+24);
4508 if (winCutScenePhase == 2) {
4509 if (--winCutSceneTimer == 0) {
4510 winCutScenePhase = 3;
4511 winCutSceneTimer = 50;
4517 if (winCutScenePhase == 3) {
4518 auto ep = findEndPlatTile();
4519 if (ep) MakeMapObject(ep.ix+global.randOther(0, 80), /*ep.iy*/192+32, 'oBurn');
4520 if (--winCutSceneTimer == 0) {
4521 winCutScenePhase = 4;
4522 winCutSceneTimer = 10;
4523 if (ep) MakeMapObject(ep.ix, ep.iy+30, 'oLavaSpray');
4529 // lava pump first accel
4530 if (winCutScenePhase == 4) {
4531 if (--winCutSceneTimer == 0) {
4532 forEachObject(delegate bool (MapObject o) {
4533 if (o isa MapObjectLavaSpray) o.yAcc = -0.1;
4539 // lava pump complete
4540 if (winCutScenePhase == 5) {
4541 if (--winCutSceneTimer == 0) {
4542 //if (oLavaSpray) oLavaSpray.yAcc = -0.1;
4543 startWinCutsceneVolcano();
4552 if (inWinCutscene == 2) {
4556 if (winCutScenePhase == 0) {
4557 winCutSceneTimer = 50;
4558 winCutScenePhase = 1;
4559 winVolcanoTimer = 10;
4563 if (winVolcanoTimer > 0) {
4564 if (--winVolcanoTimer == 0) {
4565 MakeMapObject(224+global.randOther(0,48), 144+global.randOther(0,8), 'oVolcanoFlame');
4566 winVolcanoTimer = global.randOther(10, 20);
4571 if (winCutScenePhase == 1) {
4572 if (--winCutSceneTimer == 0) {
4573 winCutSceneTimer = 30;
4574 winCutScenePhase = 2;
4575 auto sil = MakeMapObject(240, 132, 'oPlayerSil');
4583 if (winCutScenePhase == 2) {
4584 if (--winCutSceneTimer == 0) {
4585 winCutScenePhase = 3;
4586 auto sil = MakeMapObject(240, 132, 'oTreasureSil');
4596 // winning camel room
4597 if (inWinCutscene == 3) {
4598 //if (!player.holdItem) writeln("SCENE 3: LOST ITEM!");
4600 if (!plr.visible) plr.flty = -32;
4603 if (winCutScenePhase == 0) {
4604 winCutSceneTimer = 50;
4605 winCutScenePhase = 1;
4610 if (winCutScenePhase == 1) {
4611 if (--winCutSceneTimer == 0) {
4612 winCutSceneTimer = 50;
4613 winCutScenePhase = 2;
4614 plr.playSound('sndPFall');
4617 writeln("MUST BE CHAINED: ", plr.mustBeChained);
4618 if (plr.mustBeChained) {
4619 plr.removeBallAndChain(temp:true);
4620 plr.spawnBallAndChain();
4623 writeln("HOLD: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4624 writeln("PICK: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4626 if (!player.holdItem && player.pickedItem) player.scrSwitchToPocketItem(forceIfEmpty:false);
4627 if (player.holdItem) {
4628 player.holdItem.visible = true;
4629 player.holdItem.canLiveOutsideOfLevel = true;
4630 writeln("HOLD ITEM: '", GetClassName(player.holdItem.Class), "'");
4632 plr.status == MapObject::FALLING;
4633 global.plife += 99; // just in case
4638 if (winCutScenePhase == 2) {
4639 auto ball = plr.getMyBall();
4640 if (ball && plr.holdItem != ball) {
4641 ball.teleportTo(plr.fltx, plr.flty+8);
4645 if (plr.status == MapObject::STUNNED || plr.stunned) {
4649 auto treasure = MakeMapObject(144+16+8, -32, 'oBigTreasure');
4650 if (treasure) treasure.depth = 1;
4651 winCutScenePhase = 3;
4653 plr.playSound('sndTFall');
4658 if (winCutScenePhase == 3) {
4659 if (plr.status != MapObject::STUNNED && !plr.stunned) {
4660 auto bt = findBigTreasure();
4664 //plr.status = MapObject::JUMPING;
4666 plr.kJumpPressed = true;
4667 winCutScenePhase = 4;
4668 winCutSceneTimer = 50;
4675 if (winCutScenePhase == 4) {
4676 if (--winCutSceneTimer == 0) {
4677 setMenuTilesVisible(true);
4678 winCutScenePhase = 5;
4679 winSceneDrawStatus = 1;
4680 global.playMusic('musVictory', loop:false);
4681 winCutSceneTimer = 50;
4686 if (winCutScenePhase == 5) {
4687 if (winSceneDrawStatus == 3) {
4688 int money = stats.money;
4689 if (winMoneyCount < money) {
4690 if (money-winMoneyCount > 1000) {
4691 winMoneyCount += 1000;
4692 } else if (money-winMoneyCount > 100) {
4693 winMoneyCount += 100;
4694 } else if (money-winMoneyCount > 10) {
4695 winMoneyCount += 10;
4700 if (winMoneyCount >= money) {
4701 winMoneyCount = money;
4702 ++winSceneDrawStatus;
4707 if (winSceneDrawStatus == 7) {
4710 if (winFadeLevel >= 255) {
4711 ++winSceneDrawStatus;
4712 winCutSceneTimer = 30*30;
4717 if (winSceneDrawStatus == 8) {
4718 if (--winCutSceneTimer == 0) {
4724 if (--winCutSceneTimer == 0) {
4725 ++winSceneDrawStatus;
4726 winCutSceneTimer = 50;
4735 // ////////////////////////////////////////////////////////////////////////// //
4736 void renderWinCutsceneOverlay () {
4737 if (inWinCutscene == 3) {
4738 if (winSceneDrawStatus > 0) {
4739 Video.color = 0xff_ff_ff;
4740 sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
4741 //draw_set_color(txtCol);
4742 drawTextAt(64, 32, "YOU MADE IT!");
4744 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4745 if (global.config.gameMode == GameConfig::GameMode.Vanilla) {
4746 Video.color = 0xff_ff_00; //draw_set_color(c_yellow);
4747 drawTextAt(64, 48, "Classic Mode done!");
4749 Video.color = 0x00_80_80; //draw_set_color(c_teal);
4750 if (global.config.bizarrePlus) drawTextAt(64, 48, "Bizarre Mode Plus done!");
4751 else drawTextAt(64, 48, "Bizarre Mode done!");
4752 //draw_set_color(c_white);
4754 if (!global.usedShortcut) {
4755 Video.color = 0xc0_c0_c0; //draw_set_color(c_silver);
4756 drawTextAt(64, 56, "No shortcuts used!");
4757 //draw_set_color(c_yellow);
4761 if (winSceneDrawStatus > 1) {
4762 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4763 //draw_set_color(txtCol);
4764 Video.color = 0xff_ff_ff;
4765 drawTextAt(64, 64, "FINAL SCORE:");
4768 if (winSceneDrawStatus > 2) {
4769 sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
4770 Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4771 drawTextAt(64, 72, va("$%d", winMoneyCount));
4774 if (winSceneDrawStatus > 4) {
4775 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4776 Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4777 drawTextAt(64, 96, va("Time: %s", time2str(winTime/30)));
4779 draw_set_color(c_white);
4780 if (s < 10) draw_text(96+24, 96, string(m) + ":0" + string(s));
4781 else draw_text(96+24, 96, string(m) + ":" + string(s));
4785 if (winSceneDrawStatus > 5) {
4786 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4787 Video.color = 0xff_ff_ff; //draw_set_color(txtCol);
4788 drawTextAt(64, 96+8, "Kills: ");
4789 Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4790 drawTextAt(96+24, 96+8, va("%s", stats.countKills()));
4793 if (winSceneDrawStatus > 6) {
4794 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4795 Video.color = 0xff_ff_ff; //draw_set_color(txtCol);
4796 drawTextAt(64, 96+16, "Saves: ");
4797 Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4798 drawTextAt(96+24, 96+16, va("%s", stats.damselsSaved));
4802 Video.color = (255-clamp(winFadeLevel, 0, 255))<<24;
4803 Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
4806 if (winSceneDrawStatus == 8) {
4807 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4808 Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4810 if (global.config.gameMode == GameConfig::GameMode.Vanilla) {
4811 Video.color = 0xff_ff_00; //draw_set_color(c_yellow);
4812 lastString = "YOU SHALL BE REMEMBERED AS A HERO.";
4814 Video.color = 0x00_ff_ff;
4815 if (global.config.bizarrePlus) lastString = "ANOTHER LEGENDARY ADVENTURE!";
4816 else lastString = "YOUR DISCOVERIES WILL BE CELEBRATED!";
4818 auto strLen = lastString.length*8;
4820 n = trunc(ceil(n/2.0));
4821 drawTextAt(n, 116, lastString);
4827 // ////////////////////////////////////////////////////////////////////////// //
4828 #include "roomTitle.vc"
4829 #include "roomTrans1.vc"
4830 #include "roomTrans2.vc"
4831 #include "roomTrans3.vc"
4832 #include "roomTrans4.vc"
4833 #include "roomOlmec.vc"
4834 #include "roomEnd.vc"
4835 #include "roomTutorial.vc"
4836 #include "roomScores.vc"
4837 #include "roomStars.vc"
4838 #include "roomSun.vc"
4839 #include "roomMoon.vc"
4842 // ////////////////////////////////////////////////////////////////////////// //
4843 #include "packages/Generator/loadRoomGens.vc"
4844 #include "packages/Generator/loadEntityGens.vc"