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 gameShowHelp = false;
51 transient int gameHelpScreen = 0;
52 const int MaxGameHelpScreen = 2;
53 transient bool checkWater;
54 transient int liquidTileCount; // cached
55 /*transient*/ int damselSaved;
59 transient int collectCounter;
60 /*transient*/ int levelMoneyStart;
62 // all movable (thinkable) map objects
63 EntityGrid objGrid; // monsters, items and tiles
65 MapBackTile backtiles;
66 bool blockWaterChecking;
70 bool cameFromIntroRoom; // for title screen
72 LevelGen::RType[MaxTilesWidth, MaxTilesHeight] roomType;
86 LevelKind levelKind = LevelKind.Normal;
88 array!MapTile allEnters;
89 array!MapTile allExits;
92 int startRoomX, startRoomY;
93 int endRoomX, endRoomY;
96 transient bool playerExited;
97 transient MapEntity playerExitDoor;
98 transient bool disablePlayerThink = false;
99 transient int maxPlayingTime; // in seconds
105 bool ghostSpawned; // to speed up some checks
106 bool resetBMCOG = false;
110 // FPS, i.e. incremented by 30 in one second
111 int time; // in frames
112 int lastUsedObjectId;
113 transient int lastRenderTime = -1;
114 transient int pausedTime;
116 MapEntity deadItemsHead;
118 // screen shake variables
123 // set this before calling `fixCamera()`
124 // dimensions should be real, not scaled up/down
125 transient int viewWidth, viewHeight;
126 //transient int viewOffsetX, viewOffsetY;
128 // room bounds, not scaled
129 IVec2D viewMin, viewMax;
131 // for Olmec level cinematics
132 IVec2D cameraSlideToDest;
133 IVec2D cameraSlideToCurr;
134 IVec2D cameraSlideToSpeed; // !0: slide
135 int cameraSlideToPlayer;
136 // `fixCamera()` will set the following
137 // coordinates will be real too (with scale applied)
138 // shake is not applied
139 transient IVec2D viewStart; // with `player.viewOffset`
140 private transient IVec2D realViewStart; // without `player.viewOffset`
142 transient int framesProcessedFromLastClear;
144 transient int BuildYear;
145 transient int BuildMonth;
146 transient int BuildDay;
147 transient int BuildHour;
148 transient int BuildMin;
149 transient string BuildDateString;
152 final string getBuildDateString () {
153 if (!BuildYear) return BuildDateString;
154 if (BuildDateString) return BuildDateString;
155 BuildDateString = va("%d-%02d-%02d %02d:%02d", BuildYear, BuildMonth, BuildDay, BuildHour, BuildMin);
156 return BuildDateString;
160 final void cameraSlideToPoint (int dx, int dy, int speedx, int speedy) {
161 cameraSlideToPlayer = 0;
162 cameraSlideToDest.x = dx;
163 cameraSlideToDest.y = dy;
164 cameraSlideToSpeed.x = abs(speedx);
165 cameraSlideToSpeed.y = abs(speedy);
166 cameraSlideToCurr.x = cameraCurrX;
167 cameraSlideToCurr.y = cameraCurrY;
171 final void cameraReturnToPlayer () {
172 if (!cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y)) {
173 cameraSlideToCurr.x = cameraCurrX;
174 cameraSlideToCurr.y = cameraCurrY;
175 if (cameraSlideToSpeed.x && abs(cameraSlideToSpeed.x) < 8) cameraSlideToSpeed.x = 8;
176 if (cameraSlideToSpeed.y && abs(cameraSlideToSpeed.y) < 8) cameraSlideToSpeed.y = 8;
177 cameraSlideToPlayer = 1;
182 // if `frameSkip` is `true`, there are more frames waiting
183 // (i.e. you may skip rendering and such)
184 transient void delegate (bool frameSkip) onBeforeFrame;
185 transient void delegate (bool frameSkip) onAfterFrame;
187 transient void delegate () onCameraTeleported;
189 transient void delegate () onLevelExitedCB;
191 // this will be called in-between frames, and
192 // `frameTime` is [0..1)
193 transient void delegate (float frameTime) onInterFrame;
195 final int bizRoomStyle { get { return (lg ? lg.bizRoomStyle : 0); } }
198 final bool isNormalLevel () { return (levelKind == LevelKind.Normal); }
199 final bool isTitleRoom () { return (levelKind == LevelKind.Title); }
200 final bool isTutorialRoom () { return (levelKind == LevelKind.Tutorial); }
201 final bool isTransitionRoom () { return (levelKind == LevelKind.Transition); }
202 final bool isIntroRoom () { return (levelKind == LevelKind.Transition); }
205 bool isHUDEnabled () {
206 if (inWinCutscene) return false;
207 if (inIntroCutscene) return false;
208 if (lg.finalBossLevel) return true;
209 if (isNormalLevel()) return true;
214 // ////////////////////////////////////////////////////////////////////////// //
216 void addDeath (name aname) { if (isNormalLevel()) stats.addDeath(aname); }
223 void addKill (name aname, optional bool telefrag) {
224 if (isNormalLevel()) stats.addKill(aname, telefrag!optional);
225 else if (aname == 'Shopkeeper' && levelKind == LevelKind.Stars) { ++stats.starsKills; ++starsKills; }
228 void addCollect (name aname, optional int amount) { if (isNormalLevel()) stats.addCollect(aname, amount!optional); }
230 void addDamselSaved () { if (isNormalLevel()) stats.addDamselSaved(); }
231 void addIdolStolen () { if (isNormalLevel()) stats.addIdolStolen(); }
232 void addIdolConverted () { if (isNormalLevel()) stats.addIdolConverted(); }
233 void addCrystalIdolStolen () { if (isNormalLevel()) stats.addCrystalIdolStolen(); }
234 void addCrystalIdolConverted () { if (isNormalLevel()) stats.addCrystalIdolConverted(); }
235 void addGhostSummoned () { if (isNormalLevel()) stats.addGhostSummoned(); }
238 // ////////////////////////////////////////////////////////////////////////// //
239 static final string time2str (int time) {
240 int secs = time%60; time /= 60;
241 int mins = time%60; time /= 60;
242 int hours = time%24; time /= 24;
244 if (days) return va("%d DAYS, %d:%02d:%02d", days, hours, mins, secs);
245 if (hours) return va("%d:%02d:%02d", hours, mins, secs);
246 return va("%02d:%02d", mins, secs);
250 // ////////////////////////////////////////////////////////////////////////// //
251 final int tilesWidth () { return lg.levelRoomWidth*RoomGen::Width+2; }
252 final int tilesHeight () { return (lg.finalBossLevel ? 55 : lg.levelRoomHeight*RoomGen::Height+2); }
255 // ////////////////////////////////////////////////////////////////////////// //
256 protected void resetGameInternal () {
257 if (player) player.removeBallAndChain();
260 //inIntroCutscene = 0;
272 player.removeBallAndChain();
273 auto hi = player.holdItem;
274 player.holdItem = none;
275 if (hi) hi.instanceRemove();
276 hi = player.pickedItem;
277 player.pickedItem = none;
278 if (hi) hi.instanceRemove();
285 stats.clearGameTotals();
289 // this won't generate a level yet
290 void restartGame () {
292 if (global.startMoney > 0) stats.setMoneyCheat();
293 stats.setMoney(global.startMoney);
294 levelKind = LevelKind.Normal;
298 // complement function to `restart game`
299 void generateNormalLevel () {
301 centerViewAtPlayer();
305 void restartTitle () {
308 createSpecialLevel(LevelKind.Title, &createTitleRoom, 'musTitle');
317 void restartIntro () {
320 createSpecialLevel(LevelKind.Intro, &createIntroRoom, '');
329 void restartTutorial () {
332 createSpecialLevel(LevelKind.Tutorial, &createTutorialRoom, 'musCave');
341 void restartScores () {
344 createSpecialLevel(LevelKind.Scores, &createScoresRoom, 'musTitle');
353 void restartStarsRoom () {
356 createSpecialLevel(LevelKind.Stars, &createStarsRoom, '');
365 void restartSunRoom () {
368 createSpecialLevel(LevelKind.Sun, &createSunRoom, '');
377 void restartMoonRoom () {
380 createSpecialLevel(LevelKind.Moon, &createMoonRoom, '');
389 // ////////////////////////////////////////////////////////////////////////// //
390 // generate angry shopkeeper at exit if murderer or thief
391 void generateAngryShopkeepers () {
392 if (global.murderer || global.thiefLevel > 0) {
393 foreach (MapTile e; allExits) {
394 auto obj = MonsterShopkeeper(MakeMapObject(e.ix, e.iy, 'oShopkeeper'));
396 obj.style = 'Bounty Hunter';
397 obj.status = MapObject::PATROL;
404 // ////////////////////////////////////////////////////////////////////////// //
405 final void resetRoomBounds () {
408 viewMax.x = tilesWidth*16;
409 viewMax.y = tilesHeight*16;
410 // Great Lake is bottomless (nope)
411 //if (global.lake == 1) viewMax.y -= 16;
412 //writeln("ROOM BOUNDS: (", viewMax.x, "x", viewMax.y, ")");
416 final void setRoomBounds (int x0, int y0, int x1, int y1) {
424 // ////////////////////////////////////////////////////////////////////////// //
427 float timeout; // seconds
428 float starttime; // for active
429 bool active; // true: timeout is `GetTickCount()` dismissing time
432 array!OSDMessage msglist; // [0]: current one
435 private final void osdCheckTimeouts () {
436 auto stt = GetTickCount();
437 while (msglist.length) {
438 if (!msglist[0].active) {
439 msglist[0].active = true;
440 msglist[0].starttime = stt;
442 if (msglist[0].starttime+msglist[0].timeout >= stt) break;
448 final bool osdHasMessage () {
450 return (msglist.length > 0);
454 final string osdGetMessage (out float timeLeft, out float timeStart) {
456 if (msglist.length == 0) { timeLeft = 0; return ""; }
457 auto stt = GetTickCount();
458 timeStart = msglist[0].starttime;
459 timeLeft = fmax(0, msglist[0].starttime+msglist[0].timeout-stt);
460 return msglist[0].msg;
464 final void osdClear () {
469 final void osdMessage (string msg, optional float timeout, optional bool urgent) {
471 msg = global.expandString(msg);
472 if (!specified_timeout) timeout = 3.33;
473 // special message for shops
474 if (timeout == -666) {
476 if (msglist.length && msglist[0].msg == msg) return;
477 if (msglist.length == 0 || msglist[0].msg != msg) {
480 msglist[0].msg = msg;
482 msglist[0].active = false;
483 msglist[0].timeout = 3.33;
487 if (timeout < 0.1) return;
488 timeout = fmax(1.0, timeout);
489 //writeln("OSD: ", msg);
490 // find existing one, and bring it to the top
492 for (; oldidx < msglist.length; ++oldidx) {
493 if (msglist[oldidx].msg == msg) break; // i found her!
496 if (oldidx < msglist.length) {
497 // yeah, move duplicate to the top
498 msglist[oldidx].timeout = fmax(timeout, msglist[oldidx].timeout);
499 msglist[oldidx].active = false;
500 if (urgent && oldidx != 0) {
501 timeout = msglist[oldidx].timeout;
502 msglist.remove(oldidx);
504 msglist[0].msg = msg;
505 msglist[0].timeout = timeout;
506 msglist[0].active = false;
510 msglist[0].msg = msg;
511 msglist[0].timeout = timeout;
512 msglist[0].active = false;
516 msglist[$-1].msg = msg;
517 msglist[$-1].timeout = timeout;
518 msglist[$-1].active = false;
524 // ////////////////////////////////////////////////////////////////////////// //
525 void setup (GameGlobal aGlobal, SpriteStore aSprStore, BackTileStore aBGTileStore) {
527 sprStore = aSprStore;
528 bgtileStore = aBGTileStore;
530 lg = SpawnObject(LevelGen);
534 objGrid = SpawnObject(EntityGrid);
535 objGrid.setup(0, 0, MaxTilesWidth*16, MaxTilesHeight*16);
539 // stores should be set
543 levBGImg = bgtileStore[levBGImgName];
544 foreach (MapEntity o; objGrid.allObjects()) {
547 if (t && (t.lava || t.water)) ++liquidTileCount;
549 for (MapBackTile bt = backtiles; bt; bt = bt.next) bt.onLoaded();
550 if (player) player.onLoaded();
552 if (msglist.length) {
553 msglist[0].active = false;
554 msglist[0].timeout = 0.200;
557 if (lg && lg.musicName) global.playMusic(lg.musicName); else global.stopMusic();
561 // ////////////////////////////////////////////////////////////////////////// //
562 void pickedSpectacles () {
563 foreach (MapTile t; objGrid.allObjectsSafe(MapTile)) t.onGotSpectacles();
567 // ////////////////////////////////////////////////////////////////////////// //
568 #include "rgentile.vc"
569 #include "rgenobj.vc"
572 void onLevelExited () {
573 if (playerExitDoor isa TitleTileXTitle) {
574 playerExitDoor = none;
579 if (isTitleRoom() || levelKind == LevelKind.Scores) {
580 if (playerExitDoor) processTitleExit(playerExitDoor);
581 playerExitDoor = none;
584 if (isTutorialRoom()) {
585 playerExitDoor = none;
587 global.currLevel = 1;
588 generateNormalLevel();
592 if (levelKind == LevelKind.Stars || levelKind == LevelKind.Sun || levelKind == LevelKind.Moon) {
593 playerExitDoor = none;
595 if (onLevelExitedCB) onLevelExitedCB();
600 if (isNormalLevel()) {
601 stats.maxLevelComplete = max(stats.maxLevelComplete, global.currLevel);
603 if (playerExitDoor) {
604 if (playerExitDoor.objType == 'oXGold') {
605 writeln("exiting to City Of Gold");
606 global.cityOfGold = true;
607 //!global.currLevel += 1;
608 } else if (playerExitDoor.objType == 'oXMarket') {
609 writeln("exiting to Black Market");
610 global.genBlackMarket = true;
611 //!global.currLevel += 1;
615 if (onLevelExitedCB) onLevelExitedCB();
617 playerExitDoor = none;
618 if (levelKind == LevelKind.Transition) {
619 if (global.thiefLevel > 0) global.thiefLevel -= 1;
620 if (global.alienCraft) ++global.alienCraft;
621 if (global.yetiLair) ++global.yetiLair;
622 if (global.lake) ++global.lake;
623 if (global.cityOfGold) ++global.cityOfGold;
624 //orig dbg:if (global.currLevel == 1) global.currLevel += firstLevelSkip; else global.currLevel += levelSkip;
626 if (!global.blackMarket && !global.cityOfGold /*&& !global.yetiLair*/) {
627 global.currLevel += 1;
633 // < 20 seconds per level: looks like a speedrun
634 global.noDarkLevel = (levelEndTime > levelStartTime && levelEndTime-levelStartTime < 20*30);
635 if (lg.finalBossLevel) {
638 // add money for big idol
639 player.addScore(50000);
643 generateTransitionLevel();
646 //centerViewAtPlayer();
650 void onOlmecDead (MapObject o) {
651 writeln("*** OLMEC IS DEAD!");
652 foreach (MapTile t; allExits) {
655 auto st = checkTileAtPoint(t.ix+8, t.iy+16);
657 st = MakeMapTile(t.ix/16, t.iy/16+1, 'oTemple');
660 st.invincible = true;
666 void generateLevelMessages () {
667 writeln("LEVEL NUMBER: ", global.currLevel);
668 if (global.darkLevel) {
669 if (global.hasCrown) {
670 osdMessage("THE HEDJET SHINES BRIGHTLY.");
671 global.darkLevel = false;
672 } else if (global.config.scumDarkness < 2) {
673 osdMessage("I CAN'T SEE A THING!\nI'D BETTER USE THESE FLARES!");
677 if (global.blackMarket) osdMessage("WELCOME TO THE BLACK MARKET!");
679 if (global.cemetary) osdMessage("THE DEAD ARE RESTLESS!");
680 if (global.lake == 1) osdMessage("I CAN HEAR RUSHING WATER...");
682 if (global.snakePit) osdMessage("I HEAR SNAKES... I HATE SNAKES!");
683 if (global.yetiLair == 1) osdMessage("IT SMELLS LIKE WET FUR IN HERE!");
684 if (global.alienCraft == 1) osdMessage("THERE'S A PSYCHIC PRESENCE HERE!");
685 if (global.cityOfGold == 1) {
686 if (true /*hasOlmec()*/) osdMessage("IT'S THE LEGENDARY CITY OF GOLD!");
689 if (global.sacrificePit) osdMessage("I CAN HEAR PRAYERS TO KALI!");
693 final MapObject debugSpawnObjectWithClass (class!MapObject oclass, optional bool playerDir) {
694 if (!oclass) return none;
696 bool canLeft = !isSolidAtPoint(player.ix-8, player.iy);
697 bool canRight = !isSolidAtPoint(player.ix+16, player.iy);
698 if (!canLeft && !canRight) return none;
699 if (canLeft && canRight) {
701 dx = (player.dir == MapObject::Dir.Left ? -16 : 16);
706 dx = (canLeft ? -16 : 16);
708 auto obj = SpawnMapObjectWithClass(oclass);
709 if (obj isa MapEnemy) { dx -= 8; dy -= 8; }
710 if (obj) PutSpawnedMapObject(player.ix+dx, player.iy+dy, obj);
715 final MapObject debugSpawnObject (name aname) {
716 if (!aname) return none;
717 return debugSpawnObjectWithClass(findGameObjectClassByName(aname));
721 void createSpecialLevel (LevelKind kind, scope void delegate () creator, name amusic) {
722 global.darkLevel = false;
726 global.resetStartingItems();
728 global.setMusicPitch(1.0);
731 auto olddel = ImmediateDelete;
732 ImmediateDelete = false;
740 addBackgroundGfxDetails();
741 //levBGImgName = 'bgCave';
742 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
744 blockWaterChecking = true;
748 ImmediateDelete = olddel;
749 CollectGarbage(true); // destroy delayed objects too
751 if (dumpGridStats) objGrid.dumpStats();
753 playerExited = false; // just in case
754 playerExitDoor = none;
759 lg.musicName = amusic;
760 if (amusic) global.playMusic(lg.musicName); else global.stopMusic();
764 void createTitleLevel () {
765 createSpecialLevel(LevelKind.Title, &createTitleRoom, 'musTitle');
769 void createTutorialLevel () {
770 createSpecialLevel(LevelKind.Tutorial, &createTutorialRoom, 'musCave');
779 // `global.currLevel` is the new level
780 void generateTransitionLevel () {
781 global.darkLevel = false;
786 resetTransitionOverlay();
788 global.setMusicPitch(1.0);
789 switch (global.config.transitionMusicMode) {
790 case GameConfig::MusicMode.Silent: global.stopMusic(); break;
791 case GameConfig::MusicMode.Restart: global.restartMusic(); break;
792 case GameConfig::MusicMode.DontTouch: break;
795 levelKind = LevelKind.Transition;
797 auto olddel = ImmediateDelete;
798 ImmediateDelete = false;
801 if (global.currLevel < 4) createTrans1Room();
802 else if (global.currLevel == 4) createTrans1xRoom();
803 else if (global.currLevel < 8) createTrans2Room();
804 else if (global.currLevel == 8) createTrans2xRoom();
805 else if (global.currLevel < 12) createTrans3Room();
806 else if (global.currLevel == 12) createTrans3xRoom();
807 else if (global.currLevel < 16) createTrans4Room();
808 else if (global.currLevel == 16) createTrans4Room();
809 else createTrans1Room(); //???
814 addBackgroundGfxDetails();
815 //levBGImgName = 'bgCave';
816 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
818 blockWaterChecking = true;
822 if (damselSaved > 0) {
823 // this is special "damsel ready to kiss you" object, not a heart
824 MakeMapObject(176+8, 176+8, 'oDamselKiss');
825 global.plife += damselSaved; // if player skipped transition cutscene
829 ImmediateDelete = olddel;
830 CollectGarbage(true); // destroy delayed objects too
832 if (dumpGridStats) objGrid.dumpStats();
834 playerExited = false; // just in case
835 playerExitDoor = none;
840 //global.playMusic(lg.musicName);
844 void generateLevel () {
845 levelStartTime = time;
851 global.genBlackMarket = false;
854 global.setMusicPitch(1.0);
855 stats.clearLevelTotals();
857 levelKind = LevelKind.Normal;
864 //writeln("tw:", tilesWidth, "; th:", tilesHeight);
866 auto olddel = ImmediateDelete;
867 ImmediateDelete = false;
870 if (lg.finalBossLevel) {
871 blockWaterChecking = true;
875 // if transition cutscene was skipped...
876 global.plife += max(0, damselSaved); // if player skipped transition cutscene
880 startRoomX = lg.startRoomX;
881 startRoomY = lg.startRoomY;
882 endRoomX = lg.endRoomX;
883 endRoomY = lg.endRoomY;
884 addBackgroundGfxDetails();
885 foreach (int y; 0..tilesHeight) {
886 foreach (int x; 0..tilesWidth) {
892 levBGImgName = lg.bgImgName;
893 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
895 if (global.allowAngryShopkeepers) generateAngryShopkeepers();
897 lg.generateEntities();
899 // add box of flares to dark level
900 if (global.darkLevel && allEnters.length) {
901 auto enter = allEnters[0];
902 int x = enter.ix, y = enter.iy;
903 if (!isSolidAtPoint(x-16, y)) MakeMapObject(x-16+8, y+8, 'oFlareCrate');
904 else if (!isSolidAtPoint(x+16, y)) MakeMapObject(x+16+8, y+8, 'oFlareCrate');
905 else MakeMapObject(x+8, y+8, 'oFlareCrate');
908 //scrGenerateEntities();
909 //foreach (; 0..2) scrGenerateEntities();
911 writeln(objGrid.countObjects, " alive objects inserted");
912 writeln(countBackTiles, " background tiles inserted");
914 if (!player) FatalError("player pawn is not spawned");
916 if (lg.finalBossLevel) {
917 blockWaterChecking = true;
919 blockWaterChecking = false;
924 ImmediateDelete = olddel;
925 CollectGarbage(true); // destroy delayed objects too
927 if (dumpGridStats) objGrid.dumpStats();
929 playerExited = false; // just in case
930 playerExitDoor = none;
932 levelMoneyStart = stats.money;
935 generateLevelMessages();
940 if (lastMusicName != lg.musicName) {
941 global.playMusic(lg.musicName);
942 //writeln("!<", lastMusicName, ">:<", lg.musicName, ">");
944 //writeln("MM: ", global.config.nextLevelMusicMode);
945 switch (global.config.nextLevelMusicMode) {
946 case GameConfig::MusicMode.Silent: global.stopMusic(); break; // the thing that should not be
947 case GameConfig::MusicMode.Restart: global.restartMusic(); break;
948 case GameConfig::MusicMode.DontTouch:
949 if (global.config.transitionMusicMode == GameConfig::MusicMode.Silent) {
950 global.playMusic(lg.musicName);
955 lastMusicName = lg.musicName;
956 //global.playMusic(lg.musicName);
959 if (global.cityOfGold == 1 || global.genBlackMarket) resetBMCOG = true;
961 if (global.cityOfGold == 1) {
962 lg.mapSprite = 'sMapTemple';
963 lg.mapTitle = "City of Gold";
964 } else if (global.blackMarket) {
965 lg.mapSprite = 'sMapJungle';
966 lg.mapTitle = "Black Market";
971 // ////////////////////////////////////////////////////////////////////////// //
972 int currKeys, nextKeys;
973 int pressedKeysQ, releasedKeysQ;
974 int keysPressed, keysReleased = -1;
977 struct SavedKeyState {
978 int currKeys, nextKeys;
979 int pressedKeysQ, releasedKeysQ;
980 int keysPressed, keysReleased;
982 int roomSeed, otherSeed;
986 // for saving/replaying
987 final void keysSaveState (out SavedKeyState ks) {
988 ks.currKeys = currKeys;
989 ks.nextKeys = nextKeys;
990 ks.pressedKeysQ = pressedKeysQ;
991 ks.releasedKeysQ = releasedKeysQ;
992 ks.keysPressed = keysPressed;
993 ks.keysReleased = keysReleased;
996 // for saving/replaying
997 final void keysRestoreState (const ref SavedKeyState ks) {
998 currKeys = ks.currKeys;
999 nextKeys = ks.nextKeys;
1000 pressedKeysQ = ks.pressedKeysQ;
1001 releasedKeysQ = ks.releasedKeysQ;
1002 keysPressed = ks.keysPressed;
1003 keysReleased = ks.keysReleased;
1007 final void keysNextFrame () {
1008 currKeys = nextKeys;
1012 final void clearKeys () {
1022 final void onKey (int code, bool down) {
1027 if (keysReleased&code) {
1028 keysPressed |= code;
1029 keysReleased &= ~code;
1030 pressedKeysQ |= code;
1034 if (keysPressed&code) {
1035 keysReleased |= code;
1036 keysPressed &= ~code;
1037 releasedKeysQ |= code;
1042 final bool isKeyDown (int code) {
1043 return !!(currKeys&code);
1046 final bool isKeyPressed (int code) {
1047 bool res = !!(pressedKeysQ&code);
1048 pressedKeysQ &= ~code;
1052 final bool isKeyReleased (int code) {
1053 bool res = !!(releasedKeysQ&code);
1054 releasedKeysQ &= ~code;
1059 final void clearKeysPressRelease () {
1060 keysPressed = default.keysPressed;
1061 keysReleased = default.keysReleased;
1062 pressedKeysQ = default.pressedKeysQ;
1063 releasedKeysQ = default.releasedKeysQ;
1069 // ////////////////////////////////////////////////////////////////////////// //
1070 final void registerEnter (MapTile t) {
1077 final void registerExit (MapTile t) {
1084 final bool isYAtEntranceRow (int py) {
1086 foreach (MapTile t; allEnters) if (t.iy == py) return true;
1091 final int calcNearestEnterDist (int px, int py) {
1092 if (allEnters.length == 0) return int.max;
1093 int curdistsq = int.max;
1094 foreach (MapTile t; allEnters) {
1095 int xc = px-t.xCenter, yc = py-t.yCenter;
1096 int distsq = xc*xc+yc*yc;
1097 if (distsq < curdistsq) curdistsq = distsq;
1099 return round(sqrt(curdistsq));
1103 final int calcNearestExitDist (int px, int py) {
1104 if (allExits.length == 0) return int.max;
1105 int curdistsq = int.max;
1106 foreach (MapTile t; allExits) {
1107 int xc = px-t.xCenter, yc = py-t.yCenter;
1108 int distsq = xc*xc+yc*yc;
1109 if (distsq < curdistsq) curdistsq = distsq;
1111 return round(sqrt(curdistsq));
1115 // ////////////////////////////////////////////////////////////////////////// //
1116 final void clearForTransition () {
1117 auto olddel = ImmediateDelete;
1118 ImmediateDelete = false;
1120 ImmediateDelete = olddel;
1121 CollectGarbage(true); // destroy delayed objects too
1122 global.darkLevel = false;
1126 // ////////////////////////////////////////////////////////////////////////// //
1127 final int countBackTiles () {
1129 for (MapBackTile bt = backtiles; bt; bt = bt.next) ++res;
1134 final void clearWholeLevel () {
1138 // don't kill objects the player is holding
1140 if (player.pickedItem isa ItemBall) {
1141 player.pickedItem.instanceRemove();
1142 player.pickedItem = none;
1144 if (player.pickedItem && player.pickedItem.grid) {
1145 player.pickedItem.grid.remove(player.pickedItem.gridId);
1146 writeln("secured pocket item '", GetClassName(player.pickedItem.Class), "'");
1148 if (player.holdItem isa ItemBall) {
1149 player.removeBallAndChain(temp:true);
1150 if (player.holdItem) player.holdItem.instanceRemove();
1151 player.holdItem = none;
1153 if (player.holdItem && player.holdItem.grid) {
1154 player.holdItem.grid.remove(player.holdItem.gridId);
1155 writeln("secured held item '", GetClassName(player.holdItem.Class), "'");
1157 writeln("secured ball; mustBeChained=", player.mustBeChained, "; wasHoldingBall=", player.wasHoldingBall);
1160 int count = objGrid.countObjects();
1161 if (dumpGridStats) { if (objGrid.getFirstObjectCID()) objGrid.dumpStats(); }
1162 objGrid.removeAllObjects(true); // and destroy
1163 if (count > 0) writeln(count, " objects destroyed");
1165 lastUsedObjectId = 0;
1168 lastRenderTime = -1;
1169 liquidTileCount = 0;
1173 MapBackTile t = backtiles;
1179 framesProcessedFromLastClear = 0;
1183 final void insertObject (MapEntity o) {
1185 if (o.grid) FatalError("cannot put object into level twice");
1190 final void spawnPlayerAt (int x, int y) {
1191 // if we have no player, spawn new one
1192 // otherwise this just a level transition, so simply reposition him
1194 // don't add player to object list, as it has very separate processing anyway
1195 player = SpawnObject(PlayerPawn);
1196 player.global = global;
1197 player.level = self;
1198 if (!player.initialize()) {
1200 FatalError("something is wrong with player initialization");
1206 player.saveInterpData();
1208 if (player.mustBeChained || global.config.scumBallAndChain) {
1209 writeln("*** spawning ball and chain");
1210 player.spawnBallAndChain(levelStart:true);
1212 playerExited = false;
1213 playerExitDoor = none;
1214 if (global.config.startWithKapala) global.hasKapala = true;
1215 centerViewAtPlayer();
1216 // reinsert player items into grid
1217 if (player.pickedItem) objGrid.insert(player.pickedItem);
1218 if (player.holdItem) objGrid.insert(player.holdItem);
1219 //writeln("player spawned; active=", player.active);
1220 player.scrSwitchToPocketItem(forceIfEmpty:false);
1224 final void teleportPlayerTo (int x, int y) {
1228 player.saveInterpData();
1233 final void resurrectPlayer () {
1234 if (player) player.resurrect();
1235 playerExited = false;
1236 playerExitDoor = none;
1240 // ////////////////////////////////////////////////////////////////////////// //
1241 final void scrShake (int duration) {
1242 if (shakeLeft == 0) {
1248 shakeLeft = max(shakeLeft, duration);
1253 // ////////////////////////////////////////////////////////////////////////// //
1256 ItemStolen, // including damsel, lol
1263 // make the nearest shopkeeper angry. RAWR!
1264 void scrShopkeeperAnger (SCAnger reason, optional int maxdist, optional MapEntity offender) {
1265 if (!offender) offender = player;
1266 auto shp = MonsterShopkeeper(findNearestEnemy(offender.ix, offender.iy, delegate bool (MapEnemy o) {
1267 auto sc = MonsterShopkeeper(o);
1268 if (!sc) return false;
1269 if (sc.dead || sc.angered) return false;
1271 }, castClass:MonsterShopkeeper));
1274 if (specified_maxdist && offender.directionToEntityCenter(shp) > maxdist) return;
1275 if (!shp.dead && !shp.angered) {
1276 shp.status = MapObject::ATTACK;
1278 if (global.murderer) msg = "YOU'LL PAY FOR YOUR CRIMES!";
1279 else if (reason == SCAnger.ItemStolen) msg = "COME BACK HERE, THIEF!";
1280 else if (reason == SCAnger.TileDestroyed) msg = "DIE, YOU VANDAL!";
1281 else if (reason == SCAnger.BombDropped) msg = "TERRORIST!";
1282 else if (reason == SCAnger.DamselWhipped) msg = "HEY, ONLY I CAN DO THAT!";
1283 else if (reason == SCAnger.CrapsCheated) msg = "DIE, CHEATER!";
1284 else msg = "NOW I'M REALLY STEAMED!";
1285 if (msg) osdMessage(msg, -666);
1286 global.thiefLevel += (global.thiefLevel > 0 ? 3 : 2);
1292 final MapObject findCrapsPrize () {
1293 foreach (MapObject o; objGrid.allObjects(MapObject)) {
1294 if (!o.spectral && o.inDiceHouse) return o;
1300 // ////////////////////////////////////////////////////////////////////////// //
1301 // 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.
1302 // note: idols moved by monkeys will have false `stolenIdol`
1303 void scrTriggerIdolAltar (bool stolenIdol) {
1304 ObjTikiCurse res = none;
1305 int curdistsq = int.max;
1306 int px = player.xCenter, py = player.yCenter;
1307 foreach (MapObject o; objGrid.allObjects(MapObject)) {
1308 auto tcr = ObjTikiCurse(o);
1310 if (tcr.activated) continue;
1311 int xc = px-tcr.xCenter, yc = py-tcr.yCenter;
1312 int distsq = xc*xc+yc*yc;
1313 if (distsq < curdistsq) {
1318 if (res) res.activate(stolenIdol);
1322 // ////////////////////////////////////////////////////////////////////////// //
1323 void setupGhostTime () {
1324 musicFadeTimer = -1;
1325 ghostSpawned = false;
1327 // there is no ghost on the first level
1328 if (inWinCutscene || inIntroCutscene || !isNormalLevel() || lg.finalBossLevel ||
1329 (!global.config.ghostAtFirstLevel && global.currLevel == 1))
1332 global.setMusicPitch(1.0);
1336 if (global.config.scumGhost < 0) {
1339 osdMessage("A GHASTLY VOICE CALLS YOUR NAME!\nLET'S GET OUT OF HERE!", 6.66, urgent:true);
1343 if (global.config.scumGhost == 0) {
1349 // randomizes time until ghost appears once time limit is reached
1350 // global.ghostRandom (true/false) if extra time is randomized - loaded from yasm.cfg. if false, use 30 second default
1351 // ghostTimeLeft (time in seconds * 1000) for currently generated level
1353 if (global.config.ghostRandom) {
1354 auto tMin = global.randOther(10, 30); // min(global.plife*10, 30);
1355 auto tMax = max(tMin+global.randOther(10, 30), min(global.config.scumGhost, 300)); // 5 minutes max
1356 auto tTime = global.randOther(tMin, tMax);
1357 if (tTime <= 0) tTime = round(tMax/2.0);
1358 ghostTimeLeft = tTime;
1360 ghostTimeLeft = global.config.scumGhost; // 5 minutes max
1363 ghostTimeLeft += max(0, global.config.ghostExtraTime);
1365 ghostTimeLeft *= 30; // seconds -> frames
1366 //global.ghostShowTime
1370 void spawnGhost () {
1372 ghostSpawned = true;
1375 int vwdt = (viewMax.x-viewMin.x);
1376 int vhgt = (viewMax.y-viewMin.y);
1380 if (player.ix < viewMin.x+vwdt/2) {
1381 // player is in the left side
1382 gx = viewMin.x+vwdt/2+vwdt/4;
1384 // player is in the right side
1385 gx = viewMin.x+vwdt/4;
1388 if (player.iy < viewMin.y+vhgt/2) {
1389 // player is in the left side
1390 gy = viewMin.y+vhgt/2+vhgt/4;
1392 // player is in the right side
1393 gy = viewMin.y+vhgt/4;
1396 writeln("spawning ghost at tile (", gx/16, ",", gy/16, "); player is at tile (", player.ix/16, ",", player.iy/16, ")");
1398 MakeMapObject(gx, gy, 'oGhost');
1401 if (oPlayer1.x > room_width/2) instance_create(view_xview[0]+view_wview[0]+8, view_yview[0]+floor(view_hview[0] / 2), oGhost);
1402 else instance_create(view_xview[0]-32, view_yview[0]+floor(view_hview[0]/2), oGhost);
1403 global.ghostExists = true;
1408 void thinkFrameGameGhost () {
1409 if (player.dead) return;
1410 if (!isNormalLevel()) return; // just in case
1412 if (ghostTimeLeft < 0) {
1414 if (musicFadeTimer > 0) {
1415 musicFadeTimer = -1;
1416 global.setMusicPitch(1.0);
1421 if (musicFadeTimer >= 0) {
1423 if (musicFadeTimer > 0 && musicFadeTimer <= 100*3) {
1424 float pitch = 1.0-(0.00226/3.0)*musicFadeTimer;
1425 //writeln("MFT: ", musicFadeTimer, "; pitch=", pitch);
1426 global.setMusicPitch(pitch);
1430 if (ghostTimeLeft == 0) {
1431 // she is already here!
1435 // no ghost if we have a crown
1436 if (global.hasCrown) {
1441 // if she was already spawned, don't do it again
1447 if (--ghostTimeLeft != 0) {
1449 if (global.config.ghostExtraTime > 0) {
1450 if (ghostTimeLeft == global.config.ghostExtraTime*30) {
1451 osdMessage("A CHILL RUNS UP YOUR SPINE...\nLET'S GET OUT OF HERE!", 6.66, urgent:true);
1453 if (musicFadeTimer < 0 && ghostTimeLeft <= global.config.ghostExtraTime*30) {
1461 if (player.isExitingSprite) {
1462 // no reason to spawn her, we're leaving
1471 void thinkFrameGame () {
1472 thinkFrameGameGhost();
1473 // udjat eye blinking
1474 if (global.hasUdjatEye && player) {
1475 foreach (MapTile t; allExits) {
1476 if (t isa MapTileBlackMarketDoor) {
1477 auto dm = int(player.distanceToEntity(t));
1479 if (udjatAlarm < 1 || dm < udjatAlarm) udjatAlarm = dm;
1483 global.udjatBlink = false;
1486 if (udjatAlarm > 0) {
1487 if (--udjatAlarm == 0) {
1488 global.udjatBlink = !global.udjatBlink;
1489 if (global.hasUdjatEye && player) {
1490 player.playSound(global.udjatBlink ? 'sndBlink1' : 'sndBlink2');
1494 switch (levelKind) {
1495 case LevelKind.Stars: thinkFrameGameStars(); break;
1496 case LevelKind.Sun: thinkFrameGameSun(); break;
1497 case LevelKind.Moon: thinkFrameGameMoon(); break;
1498 case LevelKind.Transition: thinkFrameTransition(); break;
1499 case LevelKind.Intro: thinkFrameIntro(); break;
1504 // ////////////////////////////////////////////////////////////////////////// //
1505 private final bool isWaterTileCB (MapTile t) {
1506 return (t && t.visible && t.water);
1510 private final bool isLavaTileCB (MapTile t) {
1511 return (t && t.visible && t.lava);
1515 // ////////////////////////////////////////////////////////////////////////// //
1516 const int GreatLakeStartTileY = 28;
1519 final void fillGreatLake () {
1520 if (global.lake == 1) {
1521 foreach (int y; GreatLakeStartTileY..tilesHeight) {
1522 foreach (int x; 0..tilesWidth) {
1523 auto t = checkTileAtPoint(x*16, y*16, delegate bool (MapTile t) {
1524 if (t.spectral || !t.visible || t.invisible || t.moveable) return false;
1528 t = MakeMapTile(x, y, 'oWaterSwim');
1532 t.setSprite(y == GreatLakeStartTileY ? 'sWaterTop' : 'sWater');
1533 } else if (t.lava) {
1534 t.setSprite(y == GreatLakeStartTileY ? 'sLavaTop' : 'sLava');
1542 // called once after level generation
1543 final void fixLiquidTop () {
1544 if (global.lake == 1) fillGreatLake();
1546 liquidTileCount = 0;
1547 foreach (MapTile t; objGrid.allObjects(MapTile)) {
1548 if (!t.water && !t.lava) continue;
1551 //writeln("fixing water tile(", GetClassName(t.Class), "):'", t.objName, "' (water=", t.water, "; lava=", t.lava, "); lqc=", liquidTileCount);
1553 //if (global.lake == 1) continue; // it is done in `fillGreatLake()`
1555 if (!checkTileAtPoint(t.ix+8, t.iy-8, (t.lava ? &isLavaTileCB : &isWaterTileCB))) {
1556 t.setSprite(t.lava ? 'sLavaTop' : 'sWaterTop');
1558 // don't do this, it will destroy seaweed
1559 //t.setSprite(t.lava ? 'sLava' : 'sWater');
1560 auto spr = t.getSprite();
1561 if (!spr) t.setSprite(t.lava ? 'sLava' : 'sWater');
1562 else if (spr.Name == 'sLavaTop') t.setSprite('sLava');
1563 else if (spr.Name == 'sWaterTop') t.setSprite('sWater');
1566 //writeln("liquid tiles count: ", liquidTileCount);
1570 // ////////////////////////////////////////////////////////////////////////// //
1571 transient MapTile curWaterTile;
1572 transient bool curWaterTileCheckHitsLava;
1573 transient bool curWaterTileCheckHitsSolidOrWater; // only for `checkWaterOrSolidTilePartialCB`
1574 transient int curWaterTileLastHDir;
1575 transient ubyte[16, 16] curWaterOccupied;
1576 transient int curWaterOccupiedCount;
1577 transient int curWaterTileCheckX0, curWaterTileCheckY0;
1580 private final void clearCurWaterCheckState () {
1581 curWaterTileCheckHitsLava = false;
1582 curWaterOccupiedCount = 0;
1583 foreach (auto idx; 0..16*16) curWaterOccupied[idx] = 0;
1587 private final bool checkWaterOrSolidTileCB (MapTile t) {
1588 if (t == curWaterTile) return false;
1589 if (t.lava && curWaterTile.water) {
1590 curWaterTileCheckHitsLava = true;
1593 if (t.ix%16 != 0 || t.iy%16 != 0) {
1594 if (t.water || t.solid) {
1595 // fill occupied array
1596 //FIXME: optimize this
1597 if (curWaterOccupiedCount < 16*16) {
1598 foreach (auto dy; t.y0..t.y1+1) {
1599 foreach (auto dx; t.x0..t.x1+1) {
1600 int sx = dx-curWaterTileCheckX0;
1601 int sy = dy-curWaterTileCheckY0;
1602 if (sx >= 0 && sx <= 16 && sy >= 0 && sy <= 15 && !curWaterOccupied[sx, sy]) {
1603 curWaterOccupied[sx, sy] = 1;
1604 ++curWaterOccupiedCount;
1610 return false; // need to check for lava
1612 if (t.water || t.solid || t.lava) {
1613 curWaterOccupiedCount = 16*16;
1614 if (t.water && curWaterTile.lava) t.instanceRemove();
1616 return false; // need to check for lava
1620 private final bool checkWaterOrSolidTilePartialCB (MapTile t) {
1621 if (t == curWaterTile) return false;
1622 if (t.lava && curWaterTile.water) {
1623 //writeln("!!!!!!!!");
1624 curWaterTileCheckHitsLava = true;
1627 if (t.water || t.solid || t.lava) {
1628 //writeln("*********");
1629 curWaterTileCheckHitsSolidOrWater = true;
1630 if (t.water && curWaterTile.lava) t.instanceRemove();
1632 return false; // need to check for lava
1636 private final bool isFullyOccupiedAtTilePos (int tileX, int tileY) {
1637 clearCurWaterCheckState();
1638 curWaterTileCheckX0 = tileX*16;
1639 curWaterTileCheckY0 = tileY*16;
1640 checkTilesInRect(tileX*16, tileY*16, 16, 16, &checkWaterOrSolidTileCB);
1641 return (curWaterTileCheckHitsLava || curWaterOccupiedCount == 16*16);
1645 private final bool isAtLeastPartiallyOccupiedAtTilePos (int tileX, int tileY) {
1646 curWaterTileCheckHitsLava = false;
1647 curWaterTileCheckHitsSolidOrWater = false;
1648 checkTilesInRect(tileX*16, tileY*16, 16, 16, &checkWaterOrSolidTilePartialCB);
1649 return (curWaterTileCheckHitsSolidOrWater || curWaterTileCheckHitsLava);
1653 private final bool waterCanReachGroundHoleInDir (MapTile wtile, int dx) {
1654 if (dx == 0) return false; // just in case
1656 int x = wtile.ix/16, y = wtile.iy/16;
1658 while (x >= 0 && x < tilesWidth) {
1659 if (!isAtLeastPartiallyOccupiedAtTilePos(x, y+1)) return true;
1660 if (isAtLeastPartiallyOccupiedAtTilePos(x, y)) return false;
1667 // returns `true` if this tile must be removed
1668 private final bool checkWaterFlow (MapTile wtile) {
1669 if (global.lake == 1) {
1670 if (wtile.iy >= GreatLakeStartTileY*16) return false; // lake tile, don't touch
1671 if (wtile.iy >= GreatLakeStartTileY*16-16) return true; // remove it, so it won't stack on a lake
1674 if (wtile.ix%16 != 0 || wtile.iy%16 != 0) return true; // sanity check
1676 curWaterTile = wtile;
1677 curWaterTileLastHDir = 0; // never moved to the side
1679 bool wasMoved = false;
1682 int tileX = wtile.ix/16, tileY = wtile.iy/16;
1685 if (tileY >= tilesHeight) return true;
1687 // check if we can fall down
1688 auto canFall = !isAtLeastPartiallyOccupiedAtTilePos(tileX, tileY+1);
1689 // disappear if can fall in lava
1690 if (wtile.water && curWaterTileCheckHitsLava) {
1691 //!writeln(wtile.objId, ": LAVA HIT DOWN");
1695 // fake, so caller will not start removing tiles
1696 if (canFall) wtile.waterMovedDown = true;
1702 //!writeln(wtile.objId, ": GOING DOWN");
1703 curWaterTileLastHDir = 0;
1704 wtile.iy = wtile.iy+16;
1706 wtile.waterMovedDown = true;
1710 bool canMoveLeft = (curWaterTileLastHDir > 0 ? false : !isAtLeastPartiallyOccupiedAtTilePos(tileX-1, tileY));
1711 // disappear if near lava
1712 if (wtile.water && curWaterTileCheckHitsLava) {
1713 //!writeln(wtile.objId, ": LAVA HIT LEFT");
1717 bool canMoveRight = (curWaterTileLastHDir < 0 ? false : !isAtLeastPartiallyOccupiedAtTilePos(tileX+1, tileY));
1718 // disappear if near lava
1719 if (wtile.water && curWaterTileCheckHitsLava) {
1720 //!writeln(wtile.objId, ": LAVA HIT RIGHT");
1724 if (!canMoveLeft && !canMoveRight) {
1726 //!if (wasMove) writeln(wtile.objId, ": NO MORE MOVES");
1730 if (canMoveLeft && canMoveRight) {
1731 // choose random direction
1732 //!writeln(wtile.objId, ": CHOOSING RANDOM HDIR");
1733 // actually, choose direction that leads to hole in a ground
1734 if (waterCanReachGroundHoleInDir(wtile, -1)) {
1735 // can reach hole at the left side
1736 if (waterCanReachGroundHoleInDir(wtile, 1)) {
1737 // can reach hole at the right side, choose at random
1738 if (global.randOther(0, 1)) canMoveRight = false; else canMoveLeft = false;
1741 canMoveRight = false;
1744 // can't reach hole at the left side
1745 if (waterCanReachGroundHoleInDir(wtile, 1)) {
1746 // can reach hole at the right side, choose at random
1747 canMoveLeft = false;
1749 // no holes at any side, choose at random
1750 if (global.randOther(0, 1)) canMoveRight = false; else canMoveLeft = false;
1757 if (canMoveRight) FatalError("WATERCHECK: WTF RIGHT");
1758 //!writeln(wtile.objId, ": MOVING LEFT (", curWaterTileLastHDir, ")");
1759 curWaterTileLastHDir = -1;
1760 wtile.ix = wtile.ix-16;
1761 } else if (canMoveRight) {
1762 if (canMoveLeft) FatalError("WATERCHECK: WTF LEFT");
1763 //!writeln(wtile.objId, ": MOVING RIGHT (", curWaterTileLastHDir, ")");
1764 curWaterTileLastHDir = 1;
1765 wtile.ix = wtile.ix+16;
1773 wtile.setSprite(wtile.lava ? 'sLava' : 'sWater');
1774 wtile.waterMoved = true;
1775 // if this tile was not moved down, check if it can move down on any next step
1776 if (!wtile.waterMovedDown) {
1777 if (waterCanReachGroundHoleInDir(wtile, -1)) wtile.waterMovedDown = true;
1778 else if (waterCanReachGroundHoleInDir(wtile, 1)) wtile.waterMovedDown = true;
1782 return false; // don't remove
1784 //if (!isWetTileAtPix(tileX*16+8, tileY*16-8)) wtile.setSprite(wtile.lava ? 'sLavaTop' : 'sWaterTop');
1788 transient array!MapTile waterTilesList;
1790 final bool sortWaterTilesByCoordsLess (MapTile a, MapTile b) {
1792 if (dy) return (dy < 0);
1793 return (a.ix < b.ix);
1796 transient int waterFlowPause = 0;
1797 transient bool debugWaterFlowPause = false;
1799 final void cleanDeadObjects () {
1800 // remove dead objects
1801 if (deadItemsHead) {
1802 auto olddel = ImmediateDelete;
1803 ImmediateDelete = false;
1805 auto it = deadItemsHead;
1806 deadItemsHead = it.deadItemsNext;
1807 if (it.grid) it.grid.remove(it.gridId);
1810 } while (deadItemsHead);
1811 ImmediateDelete = olddel;
1812 if (olddel) CollectGarbage(true); // destroy delayed objects too
1816 final void cleanDeadTiles () {
1817 if (checkWater && /*global.lake == 1 ||*/ (!blockWaterChecking && liquidTileCount)) {
1818 if (global.lake == 1) fillGreatLake();
1819 if (waterFlowPause > 1) {
1824 if (debugWaterFlowPause) waterFlowPause = 4;
1825 //writeln("checking water");
1826 waterTilesList.clear();
1827 foreach (MapTile wtile; objGrid.allObjectsSafe(MapTile)) {
1828 if (wtile.water || wtile.lava) {
1830 if (wtile.ix%16 == 0 && wtile.iy%16 == 0) {
1831 wtile.waterMoved = false;
1832 wtile.waterMovedDown = false;
1833 wtile.waterSlideOldX = wtile.ix;
1834 wtile.waterSlideOldY = wtile.iy;
1835 waterTilesList[$] = wtile;
1840 liquidTileCount = 0;
1841 waterTilesList.sort(&sortWaterTilesByCoordsLess);
1843 bool wasAnyMove = false;
1844 bool wasAnyMoveDown = false;
1845 foreach (MapTile wtile; waterTilesList) {
1846 if (!wtile || !wtile.isInstanceAlive) continue;
1847 auto killIt = checkWaterFlow(wtile);
1851 wtile.instanceRemove(); // just in case
1853 wtile.saveInterpData();
1855 wasAnyMove = wasAnyMove || wtile.waterMoved;
1856 wasAnyMoveDown = wasAnyMoveDown || wtile.waterMovedDown;
1857 if (wtile.waterMoved && debugWaterFlowPause) wtile.waterSlideCounter = 4;
1861 liquidTileCount = 0;
1862 foreach (MapTile wtile; waterTilesList) {
1863 if (!wtile || !wtile.isInstanceAlive) continue;
1864 if (wasAnyMoveDown) {
1868 //checkWater = checkWater || wtile.waterMoved;
1869 curWaterTile = wtile;
1870 int tileX = wtile.ix/16, tileY = wtile.iy/16;
1871 // check if we are have no way to leak
1872 bool killIt = false;
1873 if (!isFullyOccupiedAtTilePos(tileX-1, tileY) || (wtile.water && curWaterTileCheckHitsLava)) {
1874 //writeln(" LEFT DEATH (", curWaterOccupiedCount, " : ", curWaterTileCheckHitsLava, ")");
1877 if (!killIt && (!isFullyOccupiedAtTilePos(tileX+1, tileY) || (wtile.water && curWaterTileCheckHitsLava))) {
1878 //writeln(" RIGHT DEATH (", curWaterOccupiedCount, " : ", curWaterTileCheckHitsLava, ")");
1881 if (!killIt && (!isFullyOccupiedAtTilePos(tileX, tileY+1) || (wtile.water && curWaterTileCheckHitsLava))) {
1882 //writeln(" DOWN DEATH (", curWaterOccupiedCount, " : ", curWaterTileCheckHitsLava, ")");
1889 wtile.instanceRemove(); // just in case
1894 if (wasAnyMove) checkWater = true;
1895 //writeln("water check: liquidTileCount=", liquidTileCount, "; checkWater=", checkWater, "; wasAnyMove=", wasAnyMove, "; wasAnyMoveDown=", wasAnyMoveDown);
1897 // fill empty spaces in lake with water
1905 // ////////////////////////////////////////////////////////////////////////// //
1906 private transient array!MapEntity postponedThinkers;
1907 private transient MapEntity thinkerHeld;
1908 private transient array!MapEntity activeThinkerList;
1911 final void doThinkActionsForObject (MapEntity o) {
1912 if (o.justSpawned) o.justSpawned = false;
1913 else if (o.imageSpeed > 0) o.nextAnimFrame();
1916 if (o.isInstanceAlive) {
1919 if (o.isInstanceAlive) {
1920 if (o.whipTimer > 0) --o.whipTimer;
1922 auto obj = MapObject(o);
1923 if (!o.canLiveOutsideOfLevel && (!obj || !obj.heldBy) && o.isOutsideOfLevel()) {
1924 // oops, fallen out of level...
1932 // return `true` if thinker should be removed
1933 final void thinkOne (MapEntity o, optional bool doHeldObject, optional bool dontAddHeldObject) {
1935 if (o == thinkerHeld && !doHeldObject) return; // skip it
1937 if (!o.isInstanceAlive) return;
1939 auto obj = MapObject(o);
1941 if (obj) obj.prevhp = obj.hp; // so i don't have to do it in `thinkFrame()`
1942 if (!o.active) return;
1944 if (obj && obj.heldBy == player) {
1945 // fix held item coords
1946 obj.fixHoldCoords();
1948 doThinkActionsForObject(o);
1950 if (!dontAddHeldObject) {
1952 foreach (MapEntity e; postponedThinkers) if (e == o) { found = true; break; }
1953 if (!found) postponedThinkers[$] = o;
1959 bool doThink = true;
1961 // collision with player weapon
1962 auto hh = PlayerWeapon(player.holdItem);
1963 bool doWeaponAction = false;
1965 if (hh.blockedBySolids && !global.config.killEnemiesThruWalls) {
1966 int xx = player.ix+(player.dir == MapObject::Dir.Left ? -16 : 16);
1967 //doWeaponAction = !isSolidAtPoint(xx, player.iy);
1968 doWeaponAction = !isSolidAtPoint(xx, hh.yCenter);
1970 int dh = max(1, hh.height-2);
1971 doWeaponAction = !checkTilesInRect(player.ix, player.iy);
1974 doWeaponAction = true;
1978 if (obj && doWeaponAction && hh && (o.whipTimer <= 0 || hh.ignoreWhipTimer) && hh.collidesWithObject(obj)) {
1979 //writeln("WEAPONED!");
1980 bool dontChangeWhipTimer = hh.dontChangeWhipTimer;
1981 if (!o.onTouchedByPlayerWeapon(player, hh)) {
1982 if (o.isInstanceAlive) hh.onCollisionWithObject(obj);
1984 if (!dontChangeWhipTimer) o.whipTimer = o.whipTimerValue; //HACK
1985 doThink = o.isInstanceAlive;
1988 if (doThink && o.isInstanceAlive) {
1989 doThinkActionsForObject(o);
1990 doThink = o.isInstanceAlive;
1993 // collision with player
1994 if (doThink && obj && o.collidesWith(player)) {
1995 if (!player.onObjectTouched(obj) && o.isInstanceAlive) {
1996 doThink = !o.onTouchedByPlayer(player);
2003 final void processThinkers (float timeDelta) {
2004 if (timeDelta <= 0) return;
2007 if (onBeforeFrame) onBeforeFrame(false);
2008 if (onAfterFrame) onAfterFrame(false);
2014 accumTime += timeDelta;
2015 bool wasFrame = false;
2017 auto olddel = ImmediateDelete;
2018 ImmediateDelete = false;
2019 while (accumTime >= FrameTime) {
2020 postponedThinkers.clear();
2022 accumTime -= FrameTime;
2023 if (onBeforeFrame) onBeforeFrame(accumTime >= FrameTime);
2025 if (shakeLeft > 0) {
2027 if (!shakeDir.x) shakeDir.x = (global.randOther(0, 1) ? -3 : 3);
2028 if (!shakeDir.y) shakeDir.y = (global.randOther(0, 1) ? -3 : 3);
2029 shakeOfs.x = shakeDir.x;
2030 shakeOfs.y = shakeDir.y;
2031 int sgnc = global.randOther(1, 3);
2032 if (sgnc&0x01) shakeDir.x = -shakeDir.x;
2033 if (sgnc&0x02) shakeDir.y = -shakeDir.y;
2042 // we don't want the time to grow too large
2043 if (time < 0) { time = 0; lastRenderTime = -1; }
2044 // game-global events
2046 // frame thinkers: player
2047 if (player && !disablePlayerThink) {
2049 if (!player.dead && isNormalLevel() &&
2050 (maxPlayingTime < 0 ||
2051 (maxPlayingTime > 0 && maxPlayingTime*30 <= time &&
2052 time%30 == 0 && global.randOther(1, 100) <= 20)))
2054 MakeMapObject(player.ix, player.iy, 'oExplosion');
2056 //HACK: check for stolen items
2057 auto item = MapItem(player.holdItem);
2058 if (item) item.onCheckItemStolen(player);
2059 item = MapItem(player.pickedItem);
2060 if (item) item.onCheckItemStolen(player);
2062 doThinkActionsForObject(player);
2064 // frame thinkers: held object
2065 thinkerHeld = player.holdItem;
2066 if (thinkerHeld && thinkerHeld.isInstanceAlive) {
2067 thinkOne(thinkerHeld, doHeldObject:true);
2068 if (!thinkerHeld.isInstanceAlive) {
2069 if (player.holdItem == thinkerHeld) player.holdItem = none;
2070 thinkerHeld.grid.remove(thinkerHeld.gridId);
2072 thinkerHeld.onDestroy();
2077 // frame thinkers: objects
2078 activeThinkerList.clear();
2079 auto grid = objGrid;
2080 // collect active objects
2081 if (global.config.useFrozenRegion) {
2082 foreach (MapEntity e; grid.inRectPix(viewStart.x/global.scale-64, viewStart.y/global.scale-64, 320+64*2, 240+64*2, precise:false)) {
2083 if (e.active) activeThinkerList[$] = e;
2087 foreach (MapEntity e; grid.allObjects()) {
2088 if (e.active) activeThinkerList[$] = e;
2091 // process active objects
2092 //writeln("thinkers: ", activeThinkerList.length);
2093 foreach (MapEntity o; activeThinkerList) {
2095 thinkOne(o, doHeldObject:false);
2096 if (!o.isInstanceAlive) {
2097 //writeln("dead thinker: '", o.objType, "'");
2098 if (o.grid) o.grid.remove(o.gridId);
2099 auto obj = MapObject(o);
2100 if (obj && obj.heldBy) obj.heldBy.holdItem = none;
2107 // postponed thinkers
2108 foreach (MapEntity o; postponedThinkers) {
2110 thinkOne(o, doHeldObject:true, dontAddHeldObject:true);
2111 if (!o.isInstanceAlive) {
2112 //writeln("dead pp-thinker: '", o.objType, "'");
2119 postponedThinkers.clear();
2121 // clean dead things
2123 // fix held item coords
2124 if (player && player.holdItem) {
2125 if (player.holdItem.isInstanceAlive) {
2126 player.holdItem.fixHoldCoords();
2128 player.holdItem = none;
2132 if (collectCounter == 0) {
2133 xmoney = max(0, xmoney-100);
2139 if (!player.dead) stats.oneMoreFramePlayed();
2140 SoundSystem.ListenerOrigin = vector(player.xCenter, player.yCenter, -1);
2141 //writeln("plrpos=(", player.xCenter, ",", player.yCenter, "); lo=", SoundSystem.ListenerOrigin);
2143 if (onAfterFrame) onAfterFrame(accumTime >= FrameTime);
2144 ++framesProcessedFromLastClear;
2147 if (!player.visible && player.holdItem) player.holdItem.visible = false;
2148 if (winCutsceneSwitchToNext) {
2149 winCutsceneSwitchToNext = false;
2150 switch (++inWinCutscene) {
2151 case 2: startWinCutsceneVolcano(); break;
2152 case 3: default: startWinCutsceneWinFall(); break;
2156 if (playerExited) break;
2158 ImmediateDelete = olddel;
2160 playerExited = false;
2162 centerViewAtPlayer();
2165 // if we were processed at least one frame, collect garbage
2167 CollectGarbage(true); // destroy delayed objects too
2169 if (accumTime > 0 && onInterFrame) onInterFrame(accumTime/FrameTime);
2173 // ////////////////////////////////////////////////////////////////////////// //
2174 final void tileXY2roomXY (int tileX, int tileY, out int roomX, out int roomY) {
2175 roomX = (tileX-1)/RoomGen::Width;
2176 roomY = (tileY-1)/RoomGen::Height;
2180 final bool isInShop (int tileX, int tileY) {
2181 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
2182 auto n = roomType[tileX, tileY];
2183 if (n == 4 || n == 5) return true;
2184 return !!checkTilesInRect(tileX*16, tileY*16, 16, 16, delegate bool (MapTile t) { return t.shopWall; });
2185 //k8: we don't have this
2186 //if (t && t.objType == 'oShop') return true;
2192 // ////////////////////////////////////////////////////////////////////////// //
2193 override void Destroy () {
2195 delete tempSolidTile;
2200 // ////////////////////////////////////////////////////////////////////////// //
2201 // WARNING! delegate should not create/delete objects!
2202 final MapObject findNearestObject (int px, int py, scope bool delegate (MapObject o) dg, optional class!MapObject castClass) {
2203 MapObject res = none;
2204 if (!castClass) castClass = MapObject;
2205 int curdistsq = int.max;
2206 foreach (MapObject o; objGrid.allObjects(MapObject)) {
2207 if (o.spectral) continue;
2208 if (!dg(o)) continue;
2209 int xc = px-o.xCenter, yc = py-o.yCenter;
2210 int distsq = xc*xc+yc*yc;
2211 if (distsq < curdistsq) {
2220 // WARNING! delegate should not create/delete objects!
2221 final MapObject findNearestEnemy (int px, int py, optional scope bool delegate (MapEnemy o) dg, optional class!MapEnemy castClass) {
2222 if (!castClass) castClass = MapEnemy;
2223 if (castClass !isa MapEnemy) return none;
2224 MapObject res = none;
2225 int curdistsq = int.max;
2226 foreach (MapEnemy o; objGrid.allObjects(castClass)) {
2227 //k8: i added `dead` check
2228 if (o.spectral || o.dead) continue;
2230 if (!dg(o)) continue;
2232 int xc = px-o.xCenter, yc = py-o.yCenter;
2233 int distsq = xc*xc+yc*yc;
2234 if (distsq < curdistsq) {
2243 final MonsterShopkeeper findNearestCalmShopkeeper (int px, int py) {
2244 auto obj = MonsterShopkeeper(findNearestEnemy(px, py, delegate bool (MapEnemy o) {
2245 auto sk = MonsterShopkeeper(o);
2246 if (sk && !sk.angered) return true;
2248 }, castClass:MonsterShopkeeper));
2253 final MonsterShopkeeper hasAliveShopkeepers (optional bool skipAngry) {
2254 foreach (MonsterShopkeeper sc; objGrid.allObjects(MonsterShopkeeper)) {
2255 if (sc.spectral || sc.dead) continue;
2256 if (skipAngry && (sc.angered || sc.outlaw)) continue;
2263 // WARNING! delegate should not create/delete objects!
2264 final int calcNearestEnemyDist (int px, int py, optional scope bool delegate (MapEnemy o) dg, optional class!MapEnemy castClass) {
2265 auto e = findNearestEnemy(px, py, dg!optional, castClass!optional);
2266 if (!e) return int.max;
2267 int xc = px-e.xCenter, yc = py-e.yCenter;
2268 return round(sqrt(xc*xc+yc*yc));
2272 // WARNING! delegate should not create/delete objects!
2273 final int calcNearestObjectDist (int px, int py, optional scope bool delegate (MapObject o) dg, optional class!MapObject castClass) {
2274 auto e = findNearestObject(px, py, dg!optional, castClass!optional);
2275 if (!e) return int.max;
2276 int xc = px-e.xCenter, yc = py-e.yCenter;
2277 return round(sqrt(xc*xc+yc*yc));
2281 // WARNING! delegate should not create/delete objects!
2282 final MapTile findNearestMoveableSolid (int px, int py, optional scope bool delegate (MapTile t) dg) {
2284 int curdistsq = int.max;
2285 foreach (MapTile t; objGrid.allObjects(MapTile)) {
2286 if (t.spectral) continue;
2288 if (!dg(t)) continue;
2290 if (!t.solid || !t.moveable) continue;
2292 int xc = px-t.xCenter, yc = py-t.yCenter;
2293 int distsq = xc*xc+yc*yc;
2294 if (distsq < curdistsq) {
2303 // WARNING! delegate should not create/delete objects!
2304 final MapTile findNearestTile (int px, int py, optional scope bool delegate (MapTile t) dg) {
2305 if (!dg) return none;
2307 int curdistsq = int.max;
2309 //FIXME: make this faster!
2310 foreach (MapTile t; objGrid.allObjects(MapTile)) {
2311 if (t.spectral) continue;
2312 int xc = px-t.xCenter, yc = py-t.yCenter;
2313 int distsq = xc*xc+yc*yc;
2314 if (distsq < curdistsq && dg(t)) {
2324 // ////////////////////////////////////////////////////////////////////////// //
2325 final bool cbIsObjectWeb (MapObject o) { return (o isa ItemWeb); }
2326 final bool cbIsObjectBlob (MapObject o) { return (o isa EnemyBlob); }
2327 final bool cbIsObjectArrow (MapObject o) { return (o isa ItemProjectileArrow); }
2328 final bool cbIsObjectEnemy (MapObject o) { return (/*o != self &&*/ o isa MapEnemy); }
2330 final bool cbIsObjectTreasure (MapObject o) { return (o.isTreasure); }
2332 final bool cbIsItemObject (MapObject o) { return (o isa MapItem); }
2334 final bool cbIsItemOrEnemyObject (MapObject o) { return (o isa MapItem || o isa MapEnemy); }
2337 final MapObject isObjectAtTile (int tileX, int tileY, optional scope bool delegate (MapObject o) dg, optional bool precise) {
2338 if (!specified_precise) precise = true;
2341 foreach (MapObject o; objGrid.inRectPix(tileX, tileY, 16, 16, precise:precise, castClass:MapObject)) {
2342 if (o.spectral) continue;
2344 if (dg(o)) return o;
2353 final MapObject isObjectAtTilePix (int x, int y, optional scope bool delegate (MapObject o) dg) {
2354 return isObjectAtTile(x/16, y/16, dg!optional);
2358 final MapObject isObjectAtPoint (int xpos, int ypos, optional scope bool delegate (MapObject o) dg, optional bool precise, optional class!MapObject castClass) {
2359 if (!specified_precise) precise = true;
2360 if (!castClass) castClass = MapObject;
2361 foreach (MapObject o; objGrid.inCellPix(xpos, ypos, precise:precise, castClass:castClass)) {
2362 if (o.spectral) continue;
2364 if (dg(o)) return o;
2366 if (o isa MapEnemy) return o;
2373 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) {
2374 if (w < 1 || h < 1) return none;
2375 if (!castClass) castClass = MapObject;
2376 if (w == 1 && h == 1) return isObjectAtPoint(xpos, ypos, dg!optional, precise!optional, castClass);
2377 if (!specified_precise) precise = true;
2378 foreach (MapObject o; objGrid.inRectPix(xpos, ypos, w, h, precise:precise, castClass:castClass)) {
2379 if (o.spectral) continue;
2381 if (dg(o)) return o;
2383 if (o isa MapEnemy) return o;
2390 final MapObject forEachObject (scope bool delegate (MapObject o) dg, optional bool allowSpectrals, optional class!MapObject castClass) {
2391 if (!dg) return none;
2392 if (!castClass) castClass = MapObject;
2393 foreach (MapObject o; objGrid.allObjectsSafe(castClass)) {
2394 if (!allowSpectrals && o.spectral) continue;
2395 if (dg(o)) return o;
2401 final MapObject forEachObjectAtPoint (int xpos, int ypos, scope bool delegate (MapObject o) dg, optional bool precise) {
2402 if (!dg) return none;
2403 if (!specified_precise) precise = true;
2404 foreach (MapObject o; objGrid.inCellPix(xpos, ypos, precise:precise, castClass:MapObject)) {
2405 if (o.spectral) continue;
2406 if (dg(o)) return o;
2412 final MapObject forEachObjectInRect (int xpos, int ypos, int w, int h, scope bool delegate (MapObject o) dg, optional bool precise) {
2413 if (!dg || w < 1 || h < 1) return none;
2414 if (w == 1 && h == 1) return forEachObjectAtPoint(xpos, ypos, dg!optional, precise!optional);
2415 if (!specified_precise) precise = true;
2416 foreach (MapObject o; objGrid.inRectPix(xpos, ypos, w, h, precise:precise, castClass:MapObject)) {
2417 if (o.spectral) continue;
2418 if (dg(o)) return o;
2424 final MapEntity forEachEntityInRect (int xpos, int ypos, int w, int h, scope bool delegate (MapEntity o) dg, optional bool precise, optional class!MapEntity castClass) {
2425 if (!dg || w < 1 || h < 1) return none;
2426 if (!castClass) castClass = MapEntity;
2427 if (!specified_precise) precise = true;
2428 foreach (MapEntity e; objGrid.inRectPix(xpos, ypos, w, h, precise:precise, castClass:castClass)) {
2429 if (e.spectral) continue;
2430 if (dg(e)) return e;
2436 private final bool cbIsRopeTile (MapTile t) { return (t isa MapTileRope); }
2438 final MapTile isRopeAtPoint (int px, int py) {
2439 return checkTileAtPoint(px, py, &cbIsRopeTile);
2444 final MapTile isWaterSwimAtPoint (int px, int py) {
2445 return isWaterAtPoint(px, py);
2449 // ////////////////////////////////////////////////////////////////////////// //
2450 private array!MapEntity tmpEntityList;
2452 private final bool cbCollectEntitiesWithMask (MapEntity t) {
2453 if (!t.visible || t.spectral) return false;
2454 tmpEntityList[$] = t;
2459 final void touchEntitiesWithMask (int x, int y, SpriteFrame frm, scope bool delegate (MapEntity t) dg, optional class!MapEntity castClass) {
2460 if (!frm || frm.bw < 1 || frm.bh < 1 || !dg) return;
2461 if (frm.isEmptyPixelMask) return;
2462 if (!castClass) castClass = MapEntity;
2464 if (tmpEntityList.length) tmpEntityList.clear();
2465 if (player isa castClass && player.isRectCollisionFrame(frm, x, y)) tmpEntityList[$] = player;
2466 forEachEntityInRect(x+frm.bx, y+frm.by, frm.bw, frm.bh, &cbCollectEntitiesWithMask, castClass:castClass);
2467 foreach (MapEntity e; tmpEntityList) {
2468 if (!e || !e.isInstanceAlive || !e.visible || e.spectral) continue;
2469 if (e.isRectCollisionFrame(frm, x, y)) {
2476 // ////////////////////////////////////////////////////////////////////////// //
2477 final bool cbCollisionAnySolid (MapTile t) { return t.solid; }
2478 final bool cbCollisionAnyMoveableSolid (MapTile t) { return t.solid && t.moveable; }
2479 final bool cbCollisionLiquid (MapTile t) { return (t.water || t.lava); }
2480 final bool cbCollisionAnySolidOrLiquid (MapTile t) { return (t.solid || t.water || t.lava); }
2481 final bool cbCollisionLadder (MapTile t) { return t.ladder; }
2482 final bool cbCollisionLadderTop (MapTile t) { return t.laddertop; }
2483 final bool cbCollisionAnyLadder (MapTile t) { return (t.ladder || t.laddertop); }
2484 final bool cbCollisionAnySolidIce (MapTile t) { return (t.solid && t.ice); }
2485 final bool cbCollisionWater (MapTile t) { return t.water; }
2486 final bool cbCollisionLava (MapTile t) { return t.lava; }
2487 final bool cbCollisionAnyTree (MapTile t) { return t.tree; }
2488 final bool cbCollisionPlatform (MapTile t) { return t.platform; }
2489 final bool cbCollisionLadderOrPlatform (MapTile t) { return (t.ladder || t.platform); }
2490 //final bool cbCollisionAnyHangeable (MapTile t) { return t.hangeable; } // no need to check for solids here, it is done in MapTile initializer
2491 final bool cbCollisionSpringTrap (MapTile t) { return t.springtrap; }
2492 final bool cbCollisionSpikes (MapTile t) { return t.spikes; }
2493 final bool cbCollisionExitTile (MapTile t) { return t.isExitActive(); }
2495 final bool cbCollisionForWhoa (MapTile t) { return (t.solid || t.laddertop); }
2497 //final bool cbCollisionSacAltar (MapTile t) { return (t.objName == 'oSacAltarLeft'); } // only left part, yeah
2498 final bool cbCollisionSacAltar (MapTile t) { return t.sacrificingAltar; }
2501 // ////////////////////////////////////////////////////////////////////////// //
2502 transient MapTileTemp tempSolidTile;
2504 private final MapTileTemp makeWalkeableSolidTile (MapObject o) {
2505 if (!tempSolidTile) {
2506 tempSolidTile = SpawnObject(MapTileTemp);
2507 } else if (!tempSolidTile.isInstanceAlive) {
2508 delete tempSolidTile;
2509 tempSolidTile = SpawnObject(MapTileTemp);
2512 tempSolidTile.level = self;
2513 tempSolidTile.global = global;
2514 tempSolidTile.solid = true;
2515 tempSolidTile.objName = MapTileTemp.default.objName;
2516 tempSolidTile.objType = MapTileTemp.default.objType;
2517 tempSolidTile.e = o;
2518 tempSolidTile.fltx = o.fltx;
2519 tempSolidTile.flty = o.flty;
2520 return tempSolidTile;
2524 final MapTile checkTilesInRect (int x0, int y0, const int w, const int h,
2525 optional scope bool delegate (MapTile dg) dg, optional bool precise,
2526 optional class!MapTile castClass)
2528 if (w < 1 || h < 1) return none;
2529 if (w == 1 && h == 1) return checkTileAtPoint(x0, y0, dg!optional);
2530 int x1 = x0+w-1, y1 = y0+h-1;
2531 if (x0 >= tilesWidth*16 || y0 >= tilesHeight*16 || x1 < 0 || y1 < 0) return none;
2532 if (!specified_precise) precise = true;
2533 if (!castClass) castClass = MapTile;
2534 if (!dg) dg = &cbCollisionAnySolid;
2536 // check walkable solid objects too
2537 foreach (MapEntity e; objGrid.inRectPix(x0, y0, w, h, precise:precise, castClass:castClass)) {
2538 if (e.spectral || !e.visible) continue;
2539 auto t = MapTile(e);
2541 if (dg(t)) return t;
2544 auto o = MapObject(e);
2545 if (o && o.walkableSolid) {
2546 t = makeWalkeableSolidTile(o);
2547 if (dg(t)) return t;
2556 final MapTile checkTileAtPoint (int x0, int y0, optional scope bool delegate (MapTile dg) dg, optional bool precise, optional class!MapTile castClass) {
2557 if (x0 < 0 || y0 < 0 || x0 >= tilesWidth*16 || y0 >= tilesHeight*16) return none;
2558 if (!specified_precise) precise = true;
2559 if (!castClass) castClass = MapTile;
2560 if (!dg) dg = &cbCollisionAnySolid;
2562 // check walkable solid objects
2563 foreach (MapEntity e; objGrid.inCellPix(x0, y0, precise:precise, castClass:castClass)) {
2564 if (e.spectral || !e.visible) continue;
2565 auto t = MapTile(e);
2567 if (dg(t)) return t;
2570 auto o = MapObject(e);
2571 if (o && o.walkableSolid) {
2572 t = makeWalkeableSolidTile(o);
2573 if (dg(t)) return t;
2582 // ////////////////////////////////////////////////////////////////////////// //
2583 final MapTile isSolidAtPoint (int px, int py) { return checkTileAtPoint(px, py); }
2584 final MapTile isSolidInRect (int rx, int ry, int rw, int rh) { return checkTilesInRect(rx, ry, rw, rh); }
2585 final MapTile isLadderAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadder); }
2586 final MapTile isLadderOrPlatformAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadderOrPlatform); }
2587 final MapTile isLadderTopAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadderTop); }
2588 final MapTile isAnyLadderAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyLadder); }
2589 final MapTile isWaterAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionWater); }
2590 final MapTile isLavaAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLava); }
2591 final MapTile isLiquidAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLiquid); }
2592 final MapTile isIceAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnySolidIce); }
2593 final MapTile isTreeAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyTree); }
2594 //final MapTile isHangeableAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyHangeable); }
2597 // ////////////////////////////////////////////////////////////////////////// //
2598 final void setTileTypeAt (int tileX, int tileY, LevelGen::RType rt) {
2599 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) roomType[tileX, tileY] = rt;
2603 //FIXME: make this faster
2604 transient float gtagX, gtagY;
2606 // only non-moveables and non-specials
2607 final MapTile getTileAtGrid (int tileX, int tileY) {
2610 return checkTileAtPoint(tileX*16, tileY*16, delegate bool (MapTile t) {
2611 if (t.spectral || t.moveable || t.toSpecialGrid || !t.visible) return false;
2612 if (t.fltx != gtagX || t.flty != gtagY) return false; // emulate grid
2613 if (t.width != 16 || t.height != 16) return false;
2616 //return (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight ? tiles[tileX, tileY] : none);
2620 final MapTile getTileAtGridAny (int tileX, int tileY) {
2623 return checkTileAtPoint(tileX*16, tileY*16, delegate bool (MapTile t) {
2624 if (t.spectral /*|| t.moveable*/ || !t.visible) return false;
2625 if (t.fltx != gtagX || t.flty != gtagY) return false; // emulate grid
2626 if (t.width != 16 || t.height != 16) return false;
2629 //return (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight ? tiles[tileX, tileY] : none);
2633 final bool isNamedTileAt (int tileX, int tileY, name atypename) {
2634 if (!atypename) return false;
2635 auto t = getTileAtGridAny(tileX, tileY);
2636 return (t && t.objName == atypename);
2640 final void setTileAtGrid (int tileX, int tileY, MapTile tile) {
2641 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
2643 tile.fltx = tileX*16;
2644 tile.flty = tileY*16;
2645 if (!tile.dontReplaceOthers) {
2646 auto osp = tile.spectral;
2647 tile.spectral = true;
2648 auto t = getTileAtGridAny(tileX, tileY);
2649 tile.spectral = osp;
2650 if (t && !t.immuneToReplacement) {
2651 writeln("REPLACING tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
2652 writeln(" NEW tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (tile.toSpecialGrid ? "special " : ""), "grid, pos=(", tile.fltx/16.0, ",", tile.flty/16.0, ")");
2658 auto t = getTileAtGridAny(tileX, tileY);
2659 if (t && !t.immuneToReplacement) {
2660 writeln("REMOVING tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
2668 // ////////////////////////////////////////////////////////////////////////// //
2669 // return `true` from delegate to stop
2670 MapTile forEachSolidTileOnGrid (scope bool delegate (int tileX, int tileY, MapTile t) dg) {
2671 if (!dg) return none;
2672 foreach (MapTile t; objGrid.allObjectsSafe(MapTile)) {
2673 if (t.spectral || !t.solid || !t.visible) continue;
2674 if (t.ix%16 != 0 || t.iy%16 != 0) continue; // emulate grid
2675 if (t.width != 16 || t.height != 16) continue;
2676 if (dg(t.ix/16, t.iy/16, t)) return t;
2682 // ////////////////////////////////////////////////////////////////////////// //
2683 // return `true` from delegate to stop
2684 MapTile forEachTile (scope bool delegate (MapTile t) dg, optional class!MapTile castClass) {
2685 if (!dg) return none;
2686 if (!castClass) castClass = MapTile;
2687 foreach (MapTile t; objGrid.allObjectsSafe(castClass)) {
2688 if (t.spectral || !t.visible) continue;
2689 if (dg(t)) return t;
2695 // ////////////////////////////////////////////////////////////////////////// //
2696 final void fixWallTiles () {
2697 foreach (MapTile t; objGrid.allObjectsSafe(MapTile)) t.beautifyTile();
2701 // ////////////////////////////////////////////////////////////////////////// //
2702 final MapTile isCollisionAtPoint (int px, int py, optional scope bool delegate (MapTile dg) dg) {
2703 if (!dg) dg = &cbCollisionAnySolid;
2704 return checkTilesInRect(px, py, 1, 1, dg);
2708 // ////////////////////////////////////////////////////////////////////////// //
2709 string scrGetKaliGift (MapTile altar, optional name gift) {
2712 // find other side of the altar
2713 int sx = player.ix, sy = player.iy;
2717 auto a2 = isCollisionAtPoint(altar.ix+18, altar.iy+8, &cbCollisionSacAltar);
2718 if (!a2) a2 = isCollisionAtPoint(altar.ix-8, altar.iy+8, &cbCollisionSacAltar);
2719 if (a2) { sx = a2.ix; sy = a2.iy; }
2722 if (global.favor <= -8) res = "SHE SEEMS VERY ANGRY WITH YOU!";
2723 else if (global.favor < 0) res = "SHE SEEMS ANGRY WITH YOU.";
2724 else if (global.favor == 0) res = "SHE HAS FORGIVEN YOU!";
2725 else if (global.favor >= 32) {
2726 if (global.kaliGift >= 3 && global.favor >= 32+(global.kaliGift-2)*16) {
2727 res = "YOU FEEL INVIGORATED!";
2728 global.kaliGift += 1;
2729 global.plife += global.randOther(4, 8);
2730 } else if (global.kaliGift >= 3) {
2731 res = "SHE SEEMS ECSTATIC WITH YOU!";
2732 } else if (global.bombs < 80) {
2733 res = "YOUR SATCHEL FEELS VERY FULL NOW!";
2734 global.kaliGift = 3;
2737 res = "YOU FEEL INVIGORATED!";
2738 global.kaliGift += 1;
2739 global.plife += global.randOther(4, 8);
2741 } else if (global.favor >= 16) {
2742 if (global.kaliGift >= 2) {
2743 res = "SHE SEEMS VERY HAPPY WITH YOU!";
2745 res = "SHE BESTOWS A GIFT UPON YOU!";
2746 global.kaliGift = 2;
2748 auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2751 obj = MakeMapObject(sx, sy-8, 'oPoof');
2756 if (gift) obj = MakeMapObject(sx, sy-8, gift);
2757 if (!obj) obj = MakeMapObject(sx, sy-8, 'oKapala');
2759 } else if (global.favor >= 8) {
2760 if (global.kaliGift >= 1) {
2761 res = "SHE SEEMS HAPPY WITH YOU.";
2763 res = "SHE BESTOWS A GIFT UPON YOU!";
2764 global.kaliGift = 1;
2765 //rAltar = instance_nearest(x, y, oSacAltarRight);
2766 //if (instance_exists(rAltar)) {
2768 auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2771 obj = MakeMapObject(sx, sy-8, 'oPoof');
2775 if (gift) obj = MakeMapObject(sx, sy-8, gift);
2777 auto n = global.randOther(1, 8);
2781 if (n == 1 && !global.hasCape && !global.hasJetpack) aname = 'oCapePickup';
2782 else if (n == 2 && !global.hasGloves) aname = 'oGloves';
2783 else if (n == 3 && !global.hasSpectacles) aname = 'oSpectacles';
2784 else if (n == 4 && !global.hasMitt) aname = 'oMitt';
2785 else if (n == 5 && !global.hasSpringShoes) aname = 'oSpringShoes';
2786 else if (n == 6 && !global.hasSpikeShoes) aname = 'oSpikeShoes';
2787 else if (n == 7 && !global.hasStickyBombs) aname = 'oPaste';
2788 else if (n == 8 && !global.hasCompass) aname = 'oCompass';
2790 obj = MakeMapObject(sx, sy-8, aname);
2796 obj = MakeMapObject(sx, sy-8, (global.hasJetpack ? 'oBombBox' : 'oJetpack'));
2802 } else if (global.favor > 0) {
2803 res = "SHE SEEMS PLEASED WITH YOU.";
2808 global.message = "";
2809 res = "KALI DEVOURS YOU!"; // sacrifice is player
2817 void performSacrifice (MapObject what, MapTile where) {
2818 if (!what || !what.isInstanceAlive) return;
2819 MakeMapObject(what.ix+8, what.iy+8, 'oFlame');
2820 if (where) where.playSound('sndSmallExplode'); else what.playSound('sndSmallExplode');
2821 what.spillBlood(amount:3, forced:true);
2823 string msg = "KALI ACCEPTS THE SACRIFICE!";
2825 auto idol = ItemGoldIdol(what);
2827 ++stats.totalSacrifices;
2828 if (global.favor <= -8) msg = "SHE SEEMS VERY ANGRY WITH YOU!";
2829 else if (global.favor < 0) msg = "SHE SEEMS ANGRY WITH YOU.";
2830 else if (global.favor >= 0) {
2831 // find other side of the altar
2832 int sx = player.ix, sy = player.iy;
2837 auto a2 = isCollisionAtPoint(altar.ix+18, altar.iy+8, &cbCollisionSacAltar);
2838 if (!a2) a2 = isCollisionAtPoint(altar.ix-8, altar.iy+8, &cbCollisionSacAltar);
2839 if (a2) { sx = a2.ix; sy = a2.iy; }
2842 auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2845 obj = MakeMapObject(sx, sy-8, 'oPoof');
2849 obj = MakeMapObject(sx, sy-16-8, 'oMonkeyGold');
2851 osdMessage(msg, 6.66);
2853 idol.instanceRemove();
2857 if (global.favor <= -8) {
2858 msg = "KALI DEVOURS THE SACRIFICE!";
2859 } else if (global.favor < 0) {
2860 global.favor += (what.status == MapObject::STUNNED ? what.favor : round(what.favor/2.0)); //k8: added `round()`
2861 if (what.favor > 0) what.favor = 0;
2863 global.favor += (what.status == MapObject::STUNNED ? what.favor : round(what.favor/2.0)); //k8: added `round()`
2867 if (myAltar.gift1 != "" and global.kaliGift == 0) scrGetKaliGift(myAltar.gift1);
2868 else if (myAltar.gift2 != "" and global.kaliGift == 1) scrGetKaliGift(myAltar.gift2);
2869 else scrGetKaliGift("");
2872 // sacrifice is player?
2873 if (what isa PlayerPawn) {
2874 ++stats.totalSelfSacrifices;
2875 msg = "KALI DEVOURS YOU!";
2876 player.visible = false;
2877 player.removeBallAndChain(temp:true);
2879 player.status = MapObject::DEAD;
2881 ++stats.totalSacrifices;
2882 auto msg2 = scrGetKaliGift(where);
2883 what.instanceRemove();
2884 if (msg2) msg = va("%s\n%s", msg, msg2);
2887 osdMessage(msg, 6.66);
2893 // ////////////////////////////////////////////////////////////////////////// //
2894 final void addBackgroundGfxDetails () {
2895 // add background details
2896 //if (global.customLevel) return;
2898 // bg = instance_create(16*randRoom(1, 42), 16*randRoom(1, 33), oCaveBG);
2899 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);
2900 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);
2901 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);
2902 else MakeMapBackTile('bgExtras', 32*global.randRoom(0, 1), 0, 32, 32, 16*global.randRoom(1, 42), 16*global.randRoom(1, 33), 10002);
2907 // ////////////////////////////////////////////////////////////////////////// //
2908 private final void fixRealViewStart () {
2909 int scale = global.scale;
2910 realViewStart.x = clamp(realViewStart.x, viewMin.x*scale, viewMax.x*scale-viewWidth);
2911 realViewStart.y = clamp(realViewStart.y, viewMin.y*scale, viewMax.y*scale-viewHeight);
2915 final int cameraCurrX () { return realViewStart.x/global.scale; }
2916 final int cameraCurrY () { return realViewStart.y/global.scale; }
2919 private final void fixViewStart () {
2920 int scale = global.scale;
2921 viewStart.x = clamp(viewStart.x, viewMin.x*scale, viewMax.x*scale-viewWidth);
2922 viewStart.y = clamp(viewStart.y, viewMin.y*scale, viewMax.y*scale-viewHeight);
2926 final void centerViewAtPlayer () {
2927 if (viewWidth < 1 || viewHeight < 1 || !player) return;
2928 centerViewAt(player.xCenter, player.yCenter);
2932 final void centerViewAt (int x, int y) {
2933 if (viewWidth < 1 || viewHeight < 1) return;
2935 cameraSlideToSpeed.x = 0;
2936 cameraSlideToSpeed.y = 0;
2937 cameraSlideToPlayer = 0;
2939 int scale = global.scale;
2942 realViewStart.x = clamp(x-viewWidth/2, 0, tilesWidth*16*scale-viewWidth);
2943 realViewStart.y = clamp(y-viewHeight/2, 0, tilesHeight*16*scale-viewHeight);
2946 viewStart.x = realViewStart.x;
2947 viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
2950 if (onCameraTeleported) onCameraTeleported();
2954 const int ViewPortToleranceX = 16*1+8;
2955 const int ViewPortToleranceY = 16*1+8;
2957 final void fixCamera () {
2958 if (!player) return;
2959 if (viewWidth < 1 || viewHeight < 1) return;
2960 int scale = global.scale;
2961 auto alwaysCenterX = global.config.alwaysCenterPlayer;
2962 auto alwaysCenterY = alwaysCenterX;
2963 // calculate offset from viewport center (in game units), and fix viewport
2965 int camDestX = player.ix+8;
2966 int camDestY = player.iy+8;
2967 if (!cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y) != 0) {
2968 // slide camera to point
2969 if (cameraSlideToSpeed.x) camDestX = cameraSlideToCurr.x;
2970 if (cameraSlideToSpeed.y) camDestY = cameraSlideToCurr.y;
2971 int dx = cameraSlideToDest.x-camDestX;
2972 int dy = cameraSlideToDest.y-camDestY;
2973 //writeln("speed:(", cameraSlideToSpeed.x, ",", cameraSlideToSpeed.y, "); cur:(", camDestX, ",", camDestY, "); dest:(", cameraSlideToDest.x, ",", cameraSlideToDest.y, "); delta=(", dx, ",", dy, ")");
2974 if (dx && cameraSlideToSpeed.x != 0) {
2975 alwaysCenterX = true;
2976 if (abs(dx) <= abs(cameraSlideToSpeed.x)) {
2977 camDestX = cameraSlideToDest.x;
2979 camDestX += sign(dx)*abs(cameraSlideToSpeed.x);
2982 if (dy && abs(cameraSlideToSpeed.y) != 0) {
2983 alwaysCenterY = true;
2984 if (abs(dy) <= cameraSlideToSpeed.y) {
2985 camDestY = cameraSlideToDest.y;
2987 camDestY += sign(dy)*abs(cameraSlideToSpeed.y);
2990 //writeln(" new:(", camDestX, ",", camDestY, ")");
2991 if (cameraSlideToSpeed.x) cameraSlideToCurr.x = camDestX;
2992 if (cameraSlideToSpeed.y) cameraSlideToCurr.y = camDestY;
2996 if (cameraSlideToSpeed.x && !cameraSlideToPlayer) {
2997 realViewStart.x = clamp(camDestX*scale, 0, tilesWidth*16*scale-viewWidth);
2998 } else if (!player.cameraBlockX) {
2999 int x = camDestX*scale;
3000 int cx = realViewStart.x;
3001 if (alwaysCenterX) {
3004 int xofs = x-(cx+viewWidth/2);
3005 if (xofs < -ViewPortToleranceX*scale) cx += xofs+ViewPortToleranceX*scale;
3006 else if (xofs > ViewPortToleranceX*scale) cx += xofs-ViewPortToleranceX*scale;
3008 // slide back to player?
3009 if (cameraSlideToPlayer && cameraSlideToSpeed.x) {
3010 int prevx = cameraSlideToCurr.x*scale;
3011 int dx = (cx-prevx)/scale;
3012 if (abs(dx) <= cameraSlideToSpeed.x) {
3013 writeln("BACKSLIDE X COMPLETE!");
3014 cameraSlideToSpeed.x = 0;
3016 cx = prevx+(sign(dx)*cameraSlideToSpeed.x)*scale;
3017 cameraSlideToCurr.x += sign(dx)*cameraSlideToSpeed.x;
3018 if ((dx < 0 && cx <= 0) || (dx > 0 && cx >= tilesWidth*16*scale-viewWidth)) {
3019 writeln("BACKSLIDE X COMPLETE!");
3020 cameraSlideToSpeed.x = 0;
3024 realViewStart.x = clamp(cx, 0, tilesWidth*16*scale-viewWidth);
3028 if (cameraSlideToSpeed.y && !cameraSlideToPlayer) {
3029 realViewStart.y = clamp(camDestY*scale, 0, tilesHeight*16*scale-viewHeight);
3030 } else if (!player.cameraBlockY) {
3031 int y = camDestY*scale;
3032 int cy = realViewStart.y;
3033 if (alwaysCenterY) {
3034 cy = y-viewHeight/2;
3036 int yofs = y-(cy+viewHeight/2);
3037 if (yofs < -ViewPortToleranceY*scale) cy += yofs+ViewPortToleranceY*scale;
3038 else if (yofs > ViewPortToleranceY*scale) cy += yofs-ViewPortToleranceY*scale;
3040 // slide back to player?
3041 if (cameraSlideToPlayer && cameraSlideToSpeed.y) {
3042 int prevy = cameraSlideToCurr.y*scale;
3043 int dy = (cy-prevy)/scale;
3044 if (abs(dy) <= cameraSlideToSpeed.y) {
3045 writeln("BACKSLIDE Y COMPLETE!");
3046 cameraSlideToSpeed.y = 0;
3048 cy = prevy+(sign(dy)*cameraSlideToSpeed.y)*scale;
3049 cameraSlideToCurr.y += sign(dy)*cameraSlideToSpeed.y;
3050 if ((dy < 0 && cy <= 0) || (dy > 0 && cy >= tilesHeight*16*scale-viewHeight)) {
3051 writeln("BACKSLIDE Y COMPLETE!");
3052 cameraSlideToSpeed.y = 0;
3056 realViewStart.y = clamp(cy, 0, tilesHeight*16*scale-viewHeight);
3059 if (cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y) == 0) cameraSlideToPlayer = 0;
3062 //writeln(" new2:(", cameraCurrX, ",", cameraCurrY, ")");
3064 viewStart.x = realViewStart.x;
3065 viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
3070 // ////////////////////////////////////////////////////////////////////////// //
3071 // x0 and y0 are non-scaled (and will be scaled)
3072 final void drawSpriteAt (name sprName, float frnumf, int x0, int y0, optional bool small) {
3073 if (!sprName) return;
3074 auto spr = sprStore[sprName];
3075 if (!spr || !spr.frames.length) return;
3076 int scale = global.scale;
3079 int frnum = max(0, trunc(frnumf))%spr.frames.length;
3080 auto sfr = spr.frames[frnum];
3081 int sx0 = x0-sfr.xofs*scale;
3082 int sy0 = y0-sfr.yofs*scale;
3083 if (small && scale > 1) {
3084 sfr.tex.blitExt(sx0, sy0, round(sx0+sfr.tex.width*(scale/2.0)), round(sy0+sfr.tex.height*(scale/2.0)), 0, 0);
3086 sfr.tex.blitAt(sx0, sy0, scale);
3091 final void drawSpriteAtS3 (name sprName, float frnumf, int x0, int y0) {
3092 if (!sprName) return;
3093 auto spr = sprStore[sprName];
3094 if (!spr || !spr.frames.length) return;
3097 int frnum = max(0, trunc(frnumf))%spr.frames.length;
3098 auto sfr = spr.frames[frnum];
3099 int sx0 = x0-sfr.xofs*3;
3100 int sy0 = y0-sfr.yofs*3;
3101 sfr.tex.blitAt(sx0, sy0, 3);
3105 // x0 and y0 are non-scaled (and will be scaled)
3106 final void drawTextAt (int x0, int y0, string text, optional int scale, optional int hiColor1, optional int hiColor2) {
3108 if (!specified_scale) scale = global.scale;
3111 sprStore.renderTextWithHighlight(x0, y0, text, scale, hiColor1!optional, hiColor2!optional);
3115 void renderCompass (float currFrameDelta) {
3116 if (!global.hasCompass) return;
3119 if (isRoom("rOlmec")) {
3122 } else if (isRoom("rOlmec2")) {
3128 bool hasMessage = osdHasMessage();
3129 foreach (MapTile et; allExits) {
3131 int exitX = et.ix, exitY = et.iy;
3132 int vx0 = viewStart.x/global.scale, vy0 = viewStart.y/global.scale;
3133 int vx1 = (viewStart.x+viewWidth)/global.scale;
3134 int vy1 = (viewStart.y+viewHeight)/global.scale;
3135 if (exitY > vy1-16) {
3137 drawSpriteAt(hasMessage ? 'sCompassSmallLL' : 'sCompassLL', 0, 0, 224);
3138 } else if (exitX > vx1-16) {
3139 drawSpriteAt(hasMessage ? 'sCompassSmallLR' : 'sCompassLR', 0, 304, 224);
3141 drawSpriteAt(hasMessage ? 'sCompassSmallDown' : 'sCompassDown', 0, exitX-vx0, 224);
3143 } else if (exitX < vx0) {
3144 drawSpriteAt(hasMessage ? 'sCompassSmallLeft' : 'sCompassLeft', 0, 0, exitY-vy0);
3145 } else if (exitX > vx1-16) {
3146 drawSpriteAt(hasMessage ? 'sCompassSmallRight' : 'sCompassRight', 0, 304, exitY-vy0);
3148 break; // only the first exit
3153 final bool totalsNameCmpCB (ref GameStats::TotalItem a, ref GameStats::TotalItem b) {
3154 auto sa = string(a.objName);
3155 auto sb = string(b.objName);
3159 void renderTransitionInfo (float currFrameDelta) {
3162 GameStats.sortTotalsList(stats.kills, &totalsNameCmpCB);
3165 foreach (int idx, ref auto k; stats.kills) {
3166 string s = string(k);
3167 maxLen = max(maxLen, s.length);
3171 sprStore.loadFont('sFontSmall');
3172 Video.color = 0xff_ff_00;
3173 foreach (int idx, ref auto k; stats.kills) {
3175 foreach (int xidx, ref auto d; stats.totalKills) {
3176 if (d.objName == k) { deaths = d.count; break; }
3178 //drawTextAt(16, 4+idx*8, va("%s: %d (%d)", string(k.objName).toUpperCase, k.count, deaths));
3179 drawTextAt(16, 4+idx*8, string(k).toUpperCase);
3180 drawTextAt(16+maxLen, 4+idx*8, va(": %d (%d)", k.count, deaths));
3186 void renderGhostTimer (float currFrameDelta) {
3187 if (ghostTimeLeft <= 0) return;
3188 //ghostTimeLeft /= 30; // frames -> seconds
3190 int hgt = viewHeight-64;
3191 if (hgt < 1) return;
3192 int rhgt = round(float(hgt)*(float(ghostTimeLeft)/30.0)/1000.0);
3193 //writeln("hgt=", hgt, "; rhgt=", rhgt, "; gtl=", ghostTimeLeft/30);
3195 auto oclr = Video.color;
3196 Video.color = 0xcf_ff_7f_00;
3197 Video.fillRect(viewWidth-20, 32, 16, hgt-rhgt);
3198 Video.color = 0x7f_ff_7f_00;
3199 Video.fillRect(viewWidth-20, 32+(hgt-rhgt), 16, rhgt);
3205 void renderStarsHUD (float currFrameDelta) {
3206 bool scumSmallHud = global.config.scumSmallHud;
3208 //auto life = max(0, global.plife);
3209 int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3210 int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3211 //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3216 sprStore.loadFont('sFontSmall');
3219 sprStore.loadFont('sFont');
3223 Video.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3224 //draw_sprite(sHeart, -1, view_xview[0]+8, view_yview[0]+8);
3225 //draw_text(view_xview[0]+24, view_yview[0]+8, life);
3227 if (global.plife == 1) {
3228 drawSpriteAt('sHeartSmallBlink', global.heartBlink, 10, hhup-4);
3229 global.heartBlink += 0.1;
3230 if (global.heartBlink > 3) global.heartBlink = 0;
3232 drawSpriteAt('sHeartSmall', -1, 10, hhup-4);
3233 global.heartBlink = 0;
3236 if (global.plife == 1) {
3237 drawSpriteAt('sHeartBlink', global.heartBlink, 8, hhup);
3238 global.heartBlink += 0.1;
3239 if (global.heartBlink > 3) global.heartBlink = 0;
3241 drawSpriteAt('sHeart', -1, 8, hhup);
3242 global.heartBlink = 0;
3245 int life = clamp(global.plife, 0, 99);
3246 drawTextAt(16+8, hhup, va("%d", life));
3248 //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3249 drawSpriteAt('sShopkeeperIcon', -1, 64, hhup, scumSmallHud);
3250 drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", starsKills));
3252 if (starsRoomTimer1 > 0) {
3253 sprStore.loadFont('sFontSmall');
3254 Video.color = 0xff_ff_00;
3255 int scale = global.scale;
3256 sprStore.renderMultilineTextCentered(viewWidth/2, 216*scale, va("SHOTGUN CHALLENGE BEGINS IN ~%d~", (starsRoomTimer1/30)+1), scale, 0xff_00_00);
3261 void renderSunHUD (float currFrameDelta) {
3262 bool scumSmallHud = global.config.scumSmallHud;
3264 //auto life = max(0, global.plife);
3265 int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3266 int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3267 //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3272 sprStore.loadFont('sFontSmall');
3275 sprStore.loadFont('sFont');
3279 Video.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3280 //draw_sprite(sHeart, -1, view_xview[0]+8, view_yview[0]+8);
3281 //draw_text(view_xview[0]+24, view_yview[0]+8, life);
3283 if (global.plife == 1) {
3284 drawSpriteAt('sHeartSmallBlink', global.heartBlink, 10, hhup-4);
3285 global.heartBlink += 0.1;
3286 if (global.heartBlink > 3) global.heartBlink = 0;
3288 drawSpriteAt('sHeartSmall', -1, 10, hhup-4);
3289 global.heartBlink = 0;
3292 if (global.plife == 1) {
3293 drawSpriteAt('sHeartBlink', global.heartBlink, 8, hhup);
3294 global.heartBlink += 0.1;
3295 if (global.heartBlink > 3) global.heartBlink = 0;
3297 drawSpriteAt('sHeart', -1, 8, hhup);
3298 global.heartBlink = 0;
3301 int life = clamp(global.plife, 0, 99);
3302 drawTextAt(16+8, hhup, va("%d", life));
3304 //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3305 drawSpriteAt('sDamselIcon', -1, 64, hhup, scumSmallHud);
3306 drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", sunScore));
3308 if (sunRoomTimer1 > 0) {
3309 sprStore.loadFont('sFontSmall');
3310 Video.color = 0xff_ff_00;
3311 int scale = global.scale;
3312 sprStore.renderMultilineTextCentered(viewWidth/2, 216*scale, va("DAMSEL CHALLENGE BEGINS IN ~%d~", (sunRoomTimer1/30)+1), scale, 0xff_00_00);
3317 void renderMoonHUD (float currFrameDelta) {
3318 bool scumSmallHud = global.config.scumSmallHud;
3320 //auto life = max(0, global.plife);
3321 int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3322 int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3323 //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3328 sprStore.loadFont('sFontSmall');
3331 sprStore.loadFont('sFont');
3335 Video.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3337 //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3338 drawSpriteAt('sHoopsIcon', -1, 8, hhup, scumSmallHud);
3339 drawTextAt(8+16-(scumSmallHud ? 6 : 0), hhup, va("%d", moonScore));
3340 drawSpriteAt('sTimerIcon', -1, 64, hhup, scumSmallHud);
3341 drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", max(0, moonTimer)));
3343 if (moonRoomTimer1 > 0) {
3344 sprStore.loadFont('sFontSmall');
3345 Video.color = 0xff_ff_00;
3346 int scale = global.scale;
3347 sprStore.renderMultilineTextCentered(viewWidth/2, 216*scale, va("ARCHERY CHALLENGE BEGINS IN ~%d~", (moonRoomTimer1/30)+1), scale, 0xff_00_00);
3352 void renderHUD (float currFrameDelta) {
3353 if (levelKind == LevelKind.Stars) { renderStarsHUD(currFrameDelta); return; }
3354 if (levelKind == LevelKind.Sun) { renderSunHUD(currFrameDelta); return; }
3355 if (levelKind == LevelKind.Moon) { renderMoonHUD(currFrameDelta); return; }
3357 if (!isHUDEnabled()) return;
3359 if (levelKind == LevelKind.Transition) { renderTransitionInfo(currFrameDelta); return; }
3367 bool scumSmallHud = global.config.scumSmallHud;
3368 if (!global.config.optSGAmmo) moneyX = ammoX;
3371 sprStore.loadFont('sFontSmall');
3374 sprStore.loadFont('sFont');
3377 //int alpha = 0x6f_00_00_00;
3378 int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3379 int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3381 //Video.color = 0xff_ff_ff;
3382 Video.color = 0xff_ff_ff|talpha;
3386 if (global.plife == 1) {
3387 drawSpriteAt('sHeartSmallBlink', global.heartBlink, lifeX+2, 4-hhup);
3388 global.heartBlink += 0.1;
3389 if (global.heartBlink > 3) global.heartBlink = 0;
3391 drawSpriteAt('sHeartSmall', -1, lifeX+2, 4-hhup);
3392 global.heartBlink = 0;
3395 if (global.plife == 1) {
3396 drawSpriteAt('sHeartBlink', global.heartBlink, lifeX, 8-hhup);
3397 global.heartBlink += 0.1;
3398 if (global.heartBlink > 3) global.heartBlink = 0;
3400 drawSpriteAt('sHeart', -1, lifeX, 8-hhup);
3401 global.heartBlink = 0;
3405 int life = clamp(global.plife, 0, 99);
3406 //if (!scumHud && life > 99) life = 99;
3407 drawTextAt(lifeX+16, 8-hhup, va("%d", life));
3410 if (global.hasStickyBombs && global.stickyBombsActive) {
3411 if (scumSmallHud) drawSpriteAt('sStickyBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sStickyBombIcon', -1, bombX, 8-hhup);
3413 if (scumSmallHud) drawSpriteAt('sBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sBombIcon', -1, bombX, 8-hhup);
3415 int n = global.bombs;
3416 if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3417 drawTextAt(bombX+16, 8-hhup, va("%d", n));
3420 if (scumSmallHud) drawSpriteAt('sRopeIconSmall', -1, ropeX+2, 4-hhup); else drawSpriteAt('sRopeIcon', -1, ropeX, 8-hhup);
3422 if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3423 drawTextAt(ropeX+16, 8-hhup, va("%d", n));
3426 if (global.config.optSGAmmo && (!player || player.holdItem !isa ItemWeaponBow)) {
3427 if (scumSmallHud) drawSpriteAt('sShellsIcon', -1, ammoX+6, 8-hhup); else drawSpriteAt('sShellsIcon', -1, ammoX+7, 12-hhup);
3429 if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3430 drawTextAt(ammoX+16, 8-hhup, va("%d", n));
3431 } else if (player && player.holdItem isa ItemWeaponBow) {
3432 if (scumSmallHud) drawSpriteAt('sArrowRight', -1, ammoX+6, 8-hhup); else drawSpriteAt('sArrowRight', -1, ammoX+7, 12-hhup);
3434 if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3435 drawTextAt(ammoX+16, 8-hhup, va("%d", n));
3439 if (scumSmallHud) drawSpriteAt('sDollarSignSmall', -1, moneyX+2, 3-hhup); else drawSpriteAt('sDollarSign', -1, moneyX, 8-hhup);
3440 drawTextAt(moneyX+16, 8-hhup, va("%d", stats.money-xmoney));
3443 Video.color = 0xff_ff_ff|ialpha;
3445 int ity = (scumSmallHud ? 18-hhup : 24-hhup);
3448 if (global.hasUdjatEye) {
3449 if (global.udjatBlink) drawSpriteAt('sUdjatEyeIcon2', -1, n, ity); else drawSpriteAt('sUdjatEyeIcon', -1, n, ity);
3452 if (global.hasAnkh) { drawSpriteAt('sAnkhIcon', -1, n, ity); n += 20; }
3453 if (global.hasCrown) { drawSpriteAt('sCrownIcon', -1, n, ity); n += 20; }
3454 if (global.hasKapala) {
3455 if (global.bloodLevel == 0) drawSpriteAt('sKapalaIcon', 0, n, ity);
3456 else if (global.bloodLevel <= 2) drawSpriteAt('sKapalaIcon', 1, n, ity);
3457 else if (global.bloodLevel <= 4) drawSpriteAt('sKapalaIcon', 2, n, ity);
3458 else if (global.bloodLevel <= 6) drawSpriteAt('sKapalaIcon', 3, n, ity);
3459 else if (global.bloodLevel <= 8) drawSpriteAt('sKapalaIcon', 4, n, ity);
3462 if (global.hasSpectacles) { drawSpriteAt('sSpectaclesIcon', -1, n, ity); n += 20; }
3463 if (global.hasGloves) { drawSpriteAt('sGlovesIcon', -1, n, ity); n += 20; }
3464 if (global.hasMitt) { drawSpriteAt('sMittIcon', -1, n, ity); n += 20; }
3465 if (global.hasSpringShoes) { drawSpriteAt('sSpringShoesIcon', -1, n, ity); n += 20; }
3466 if (global.hasJordans) { drawSpriteAt('sJordansIcon', -1, n, ity); n += 20; }
3467 if (global.hasSpikeShoes) { drawSpriteAt('sSpikeShoesIcon', -1, n, ity); n += 20; }
3468 if (global.hasCape) { drawSpriteAt('sCapeIcon', -1, n, ity); n += 20; }
3469 if (global.hasJetpack) { drawSpriteAt('sJetpackIcon', -1, n, ity); n += 20; }
3470 if (global.hasStickyBombs) { drawSpriteAt('sPasteIcon', -1, n, ity); n += 20; }
3471 if (global.hasCompass) { drawSpriteAt('sCompassIcon', -1, n, ity); n += 20; }
3472 if (global.hasParachute) { drawSpriteAt('sParachuteIcon', -1, n, ity); n += 20; }
3474 if (player.holdItem && player.holdItem.objName == 'Bow' && global.arrows > 0) {
3477 while (m <= global.arrows && m <= 20 && malpha > 0) {
3478 Video.color = trunc(malpha*255)<<24|0xff_ff_ff;
3479 drawSpriteAt('sArrowIcon', -1, n, ity);
3481 if (m > 10) malpha -= 0.1; //and global.arrows > 20 malpha -= 0.1
3487 sprStore.loadFont('sFontSmall');
3488 Video.color = 0xff_ff_00|talpha;
3489 if (scumSmallHud) drawTextAt(moneyX+8, 8+8-hhup, va("+%d", xmoney));
3490 else drawTextAt(moneyX, 8+16-hhup, va("+%d", xmoney));
3493 Video.color = 0xff_ff_ff;
3494 if (global.config.ghostShowTime) renderGhostTimer(currFrameDelta);
3498 // ////////////////////////////////////////////////////////////////////////// //
3499 // x0 and y0 are non-scaled (and will be scaled)
3500 final void drawTextAtS3 (int x0, int y0, string text, optional int hiColor1, optional int hiColor2) {
3504 sprStore.renderTextWithHighlight(x0, y0, text, 3, hiColor1!optional, hiColor2!optional);
3508 final void drawTextAtS3Centered (int y0, string text, optional int hiColor1, optional int hiColor2) {
3510 int x0 = (viewWidth-sprStore.getTextWidth(text, 3, specified_hiColor1, specified_hiColor2))/2;
3511 sprStore.renderTextWithHighlight(x0, y0*3, text, 3, hiColor1!optional, hiColor2!optional);
3515 void renderHelpOverlay () {
3517 Video.fillRect(0, 0, viewWidth, viewHeight);
3520 int txoff = 0; // text x pos offset (for multi-color lines)
3522 if (gameHelpScreen) {
3523 sprStore.loadFont('sFontSmall');
3524 Video.color = 0xff_ff_ff;
3525 drawTextAtS3Centered(ty, va("HELP (PAGE ~%d~ OF ~%d~)", gameHelpScreen, MaxGameHelpScreen), 0xff_ff_00);
3529 if (gameHelpScreen == 1) {
3530 sprStore.loadFont('sFontSmall');
3531 Video.color = 0xff_ff_00; drawTextAtS3(tx, ty, "INVENTORY BASICS"); ty += 16;
3532 Video.color = 0xff_ff_ff;
3533 drawTextAtS3(tx, ty, global.expandString("Press $SWITCH to cycle through items."), 0x00_ff_00);
3536 Video.color = 0xff_ff_ff;
3537 drawSpriteAtS3('sHelpSprite1', -1, 64, 96);
3538 } else if (gameHelpScreen == 2) {
3539 sprStore.loadFont('sFontSmall');
3540 Video.color = 0xff_ff_00;
3541 drawTextAtS3(tx, ty, "SELLING TO SHOPKEEPERS"); ty += 16;
3542 Video.color = 0xff_ff_ff;
3543 drawTextAtS3(tx, ty, global.expandString("Press $PAY to offer your currently"), 0x00_ff_00); ty += 8;
3544 drawTextAtS3(tx, ty, "held item to the shopkeeper."); ty += 16;
3545 drawTextAtS3(tx, ty, "If the shopkeeper is interested, "); ty += 8;
3546 //drawTextAtS3(tx, ty, global.expandString("press $PAY again to complete the sale."), 0x00_ff_00); ty += 72;
3547 drawTextAtS3(tx, ty, global.expandString("press $PAY again to complete"), 0x00_ff_00);
3548 drawTextAtS3(tx, ty+8, "the sale.");
3550 drawSpriteAtS3('sHelpSell', -1, 112, 100);
3551 drawTextAtS3(tx, ty, "Purchasing goods from the shopkeeper"); ty += 8;
3552 drawTextAtS3(tx, ty, "will increase the funds he has"); ty += 8;
3553 drawTextAtS3(tx, ty, "available to buy your unwanted stuff."); ty += 8;
3556 sprStore.loadFont('sFont');
3557 Video.color = 0xff_ff_ff;
3558 drawTextAtS3(136, 8, "MAP");
3560 if (lg.mapSprite && (isNormalLevel() || isTransitionRoom())) {
3561 Video.color = 0xff_ff_00;
3562 drawTextAtS3Centered(24, lg.mapTitle);
3564 auto spf = sprStore[lg.mapSprite].frames[0];
3565 int mapX = 160-spf.width/2;
3566 int mapY = 120-spf.height/2;
3567 //mapTitleX = 160-string_length(global.mapTitle)*8/2;
3569 Video.color = 0xff_ff_ff;
3570 drawSpriteAtS3(lg.mapSprite, -1, mapX, mapY);
3572 if (lg.mapSprite != 'sMapDefault') {
3573 int mx = -1, my = -1;
3575 // set position of player icon
3576 switch (global.currLevel) {
3577 case 1: mx = 81; my = 22; break;
3578 case 2: mx = 113; my = 63; break;
3579 case 3: mx = 197; my = 86; break;
3580 case 4: mx = 133; my = 109; break;
3581 case 5: mx = 181; my = 22; break;
3582 case 6: mx = 126; my = 64; break;
3583 case 7: mx = 158; my = 112; break;
3584 case 8: mx = 66; my = 80; break;
3585 case 9: mx = 30; my = 26; break;
3586 case 10: mx = 88; my = 54; break;
3587 case 11: mx = 148; my = 81; break;
3588 case 12: mx = 210; my = 205; break;
3589 case 13: mx = 66; my = 17; break;
3590 case 14: mx = 146; my = 17; break;
3591 case 15: mx = 82; my = 77; break;
3592 case 16: mx = 178; my = 81; break;
3596 int plrx = mx+player.ix/16;
3597 int plry = my+player.iy/16;
3598 if (isTransitionRoom()) { plrx = mx+20; plry = my+16; }
3599 name plrspr = 'sMapSpelunker';
3600 if (global.isDamsel) plrspr = 'sMapDamsel';
3601 else if (global.isTunnelMan) plrspr = 'sMapTunnel';
3602 auto ss = sprStore[plrspr];
3603 drawSpriteAtS3(plrspr, (pausedTime/2)%ss.frames.length, mapX+plrx, mapY+plry);
3605 if (global.hasCompass && allExits.length) {
3606 drawSpriteAtS3('sMapRedDot', -1, mapX+mx+allExits[0].ix/16, mapY+my+allExits[0].iy/16);
3613 sprStore.loadFont('sFontSmall');
3614 Video.color = 0xff_ff_00;
3615 drawTextAtS3Centered(232, "PRESS ~SPACE~/~LEFT~/~RIGHT~ TO CHANGE PAGE", 0x00_ff_00);
3617 Video.color = 0xff_ff_ff;
3621 void renderPauseOverlay () {
3622 //drawTextAt(256, 432, "PAUSED", scale);
3624 if (gameShowHelp) { renderHelpOverlay(); return; }
3626 Video.color = 0xff_ff_00;
3627 //int hiColor = 0x00_ff_00;
3630 if (isTutorialRoom()) {
3631 sprStore.loadFont('sFont');
3632 drawTextAtS3(40, n-24, "TUTORIAL CAVE");
3633 } else if (isNormalLevel()) {
3634 sprStore.loadFont('sFont');
3636 drawTextAtS3Centered(n-32, va("LEVEL ~%d~", global.currLevel), 0x00_ff_00);
3638 sprStore.loadFont('sFontSmall');
3640 int depth = round((174.8*(global.currLevel-1)+(player.iy+8)*0.34)*(global.config.scumMetric ? 0.3048 : 1.0)*10);
3641 string depthStr = va("DEPTH: ~%d.%d~ %s", depth/10, depth%10, (global.config.scumMetric ? "METRES" : "FEET"));
3642 drawTextAtS3Centered(n-16, depthStr, 0x00_ff_00);
3645 drawTextAtS3Centered(n, va("MONEY: ~%d~", stats.money), 0x00_ff_00);
3646 drawTextAtS3Centered(n+16, va("KILLS: ~%d~", stats.countKills), 0x00_ff_00);
3647 drawTextAtS3Centered(n+32, va("SAVES: ~%d~", stats.damselsSaved), 0x00_ff_00);
3648 drawTextAtS3Centered(n+48, va("TIME: ~%s~", time2str(time/30)), 0x00_ff_00);
3649 drawTextAtS3Centered(n+64, va("LEVEL TIME: ~%s~", time2str((time-levelStartTime)/30)), 0x00_ff_00);
3652 sprStore.loadFont('sFontSmall');
3653 Video.color = 0xff_ff_ff;
3654 drawTextAtS3Centered(240-2-8, "~ESC~-RETURN ~F10~-QUIT ~CTRL+DEL~-SUICIDE", 0xff_7f_00);
3655 drawTextAtS3Centered(2, "~O~PTIONS REDEFINE ~K~EYS ~S~TATISTICS", 0xff_7f_00);
3659 // ////////////////////////////////////////////////////////////////////////// //
3660 transient int drawLoot;
3661 transient int drawPosX, drawPosY;
3663 void resetTransitionOverlay () {
3670 // current game, uncollapsed
3671 struct LevelStatInfo {
3673 // for transition screen
3680 void thinkFrameTransition () {
3681 if (drawLoot == 0) {
3682 if (drawPosX > 272) {
3685 if (drawPosY > 83+4) drawPosY = 83;
3687 } else if (drawPosX > 232) {
3690 if (drawPosY > 91+4) drawPosY = 91;
3695 void renderTransitionOverlay () {
3696 sprStore.loadFont('sFontSmall');
3697 Video.color = 0xff_ff_00;
3698 //else if (global.currLevel-1 < 1) draw_text(32, 48, "TUTORIAL CAVE COMPLETED!");
3699 //else draw_text(32, 48, "LEVEL "+string(global.currLevel-1)+" COMPLETED!");
3700 drawTextAt(32, 48, va("LEVEL ~%d~ COMPLETED!", global.currLevel), hiColor1:0x00_ff_ff);
3701 Video.color = 0xff_ff_ff;
3702 drawTextAt(32, 64, va("TIME = ~%s~", time2str((levelEndTime-levelStartTime)/30)), hiColor1:0xff_ff_00);
3704 if (/*stats.collected.length == 0*/stats.money <= levelMoneyStart) {
3705 drawTextAt(32, 80, "LOOT = ~NONE~", hiColor1:0xff_00_00);
3707 drawTextAt(32, 80, va("LOOT = ~%d~", stats.money-levelMoneyStart), hiColor1:0xff_ff_00);
3710 if (stats.kills.length == 0) {
3711 drawTextAt(32, 96, "KILLS = ~NONE~", hiColor1:0x00_ff_00);
3713 drawTextAt(32, 96, va("KILLS = ~%d~", stats.kills.length), hiColor1:0xff_ff_00);
3716 drawTextAt(32, 112, va("MONEY = ~%d~", stats.money), hiColor1:0xff_ff_00);
3720 // ////////////////////////////////////////////////////////////////////////// //
3721 private transient array!MapEntity renderVisibleCids;
3722 private transient array!MapEntity renderVisibleLights;
3723 private transient array!MapTile renderFrontTiles; // normal, with fg
3725 final bool renderSortByDepth (MapEntity oa, MapEntity ob) {
3726 auto da = oa.depth, db = ob.depth;
3727 if (da == db) return (oa.objId < ob.objId);
3732 const int RenderEdgePixNormal = 64;
3733 const int RenderEdgePixLight = 256;
3735 #ifndef EXPERIMENTAL_RENDER_CACHE
3736 enum skipListCreation = false;
3739 void renderWithOfs (int xofs, int yofs, float currFrameDelta) {
3740 int scale = global.scale;
3742 // don't touch framebuffer alpha
3743 Video.colorMask = Video::CMask.Colors;
3744 Video.color = 0xff_ff_ff;
3747 Video::ScissorRect scsave;
3748 bool doRestoreGL = false;
3750 if (viewOffsetX > 0 || viewOffsetY > 0) {
3752 Video.getScissor(scsave);
3753 Video.scissorCombine(viewOffsetX, viewOffsetY, viewWidth, viewHeight);
3754 Video.glPushMatrix();
3755 Video.glTranslate(viewOffsetX, viewOffsetY);
3756 //Video.glTranslate(-550, 0);
3757 //Video.glScale(1, 1);
3762 bool isDarkLevel = global.darkLevel;
3765 switch (global.config.scumPlayerLit) {
3766 case 0: player.lightRadius = 0; break; // never
3767 case 1: // only in "scumDarkness"
3768 player.lightRadius = (global.config.scumDarkness >= 2 ? 96 : 32);
3771 player.lightRadius = 96;
3776 // render cave background
3779 int bgw = levBGImg.tex.width*scale;
3780 int bgh = levBGImg.tex.height*scale;
3781 int bgTW = (tilesWidth*tsz+bgw-1)/bgw;
3782 int bgTH = (tilesHeight*tsz+bgh-1)/bgh;
3783 int bgX0 = max(0, xofs/bgw);
3784 int bgY0 = max(0, yofs/bgh);
3785 int bgX1 = min(bgTW, (xofs+viewWidth+bgw-1)/bgw);
3786 int bgY1 = min(bgTH, (yofs+viewHeight+bgh-1)/bgh);
3787 foreach (int ty; bgY0..bgY1) {
3788 foreach (int tx; bgX0..bgX1) {
3789 int x0 = tx*bgw-xofs;
3790 int y0 = ty*bgh-yofs;
3791 levBGImg.tex.blitAt(x0, y0, scale);
3796 int RenderEdgePix = (global.darkLevel ? RenderEdgePixLight : RenderEdgePixNormal);
3798 // render background tiles
3799 for (MapBackTile bt = backtiles; bt; bt = bt.next) {
3800 bt.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3803 // collect visible special tiles
3804 #ifdef EXPERIMENTAL_RENDER_CACHE
3805 bool skipListCreation = (lastRenderTime == time && renderVisibleCids.length); //FIXME
3808 if (!skipListCreation) {
3809 renderVisibleCids.clear();
3810 renderVisibleLights.clear();
3811 renderFrontTiles.clear();
3813 int endVX = xofs+viewWidth;
3814 int endVY = yofs+viewHeight;
3818 if (player && player.visible) renderVisibleCids[$] = player; // include player in render list
3820 //FIXME: drop lit objects which cannot affect visible area
3822 // collect visible objects
3823 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)) {
3824 if (!o.visible) continue;
3825 auto tile = MapTile(o);
3827 if (isDarkLevel && (o.lightRadius > 4 || tile.litWholeTile)) renderVisibleLights[$] = o;
3828 if (tile.invisible) continue;
3829 if (tile.bgfront) renderFrontTiles[$] = tile;
3830 if (tile.bgback) tile.drawWithOfsBack(xofs, yofs, scale, currFrameDelta);
3832 if (isDarkLevel && o.lightRadius > 4) renderVisibleLights[$] = o;
3834 // check if the object is really visible -- this will speed up later sorting
3835 int fx0, fy0, fx1, fy1;
3836 auto spf = o.getSpriteFrame(default, out fx0, out fy0, out fx1, out fy1);
3837 if (!spf) continue; // no sprite -- nothing to draw (no, really)
3838 int ix = o.ix, iy = o.iy;
3839 int x0 = (ix+fx0)*scale, y0 = (iy+fy0)*scale;
3840 int x1 = (ix+fx1)*scale, y1 = (iy+fy1)*scale;
3841 if (x1 <= xofs || y1 <= yofs || x0 >= endVX || y0 >= endVY) {
3845 renderVisibleCids[$] = o;
3848 foreach (MapEntity o; objGrid.allObjects()) {
3849 if (!o.visible) continue;
3850 auto tile = MapTile(o);
3852 if (isDarkLevel && (o.lightRadius > 4 || tile.litWholeTile)) renderVisibleLights[$] = o;
3853 if (tile.invisible) continue;
3854 if (tile.bgfront) renderFrontTiles[$] = tile;
3855 if (tile.bgback) tile.drawWithOfsBack(xofs, yofs, scale, currFrameDelta);
3857 if (isDarkLevel && o.lightRadius > 4) renderVisibleLights[$] = o;
3859 renderVisibleCids[$] = o;
3862 //writeln("::: ", cnt, " invisible objects dropped");
3864 renderVisibleCids.sort(&renderSortByDepth);
3865 lastRenderTime = time;
3868 auto depth4Start = 0;
3869 foreach (auto xidx, MapEntity o; renderVisibleCids) {
3876 bool playerPowerupRendered = false;
3878 // render objects (part one: depth > 3)
3879 foreach (auto idx; depth4Start..renderVisibleCids.length; reverse) {
3880 MapEntity o = renderVisibleCids[idx];
3881 // 1000 is an ordinary tile
3882 if (!playerPowerupRendered && o.depth <= 1200) {
3883 playerPowerupRendered = true;
3884 // so ducking player will have it's cape correctly rendered
3885 if (player.visible) player.drawPrePrePowerupWithOfs(xofs, yofs, scale, currFrameDelta);
3887 //if (idx && renderSortByDepth(o, renderVisibleCids[idx-1])) writeln("VIOLATION!");
3888 o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3891 // render object (part two: front tile parts, depth 3.5)
3892 foreach (MapTile tile; renderFrontTiles) {
3893 tile.drawWithOfsFront(xofs, yofs, scale, currFrameDelta);
3896 // render objects (part three: depth <= 3)
3897 foreach (auto idx; 0..depth4Start; reverse) {
3898 MapEntity o = renderVisibleCids[idx];
3899 o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3900 //done above;if (isDarkLevel && (o.lightRadius > 4 || (o isa MapTile && MapTile(o).litWholeTile))) renderVisibleLights[$] = o;
3903 // render player post-effects, so cape or jetpack will be correctly rendered on level exit, for example
3904 player.lastDrawWithOfs(xofs, yofs, scale, currFrameDelta);
3908 auto ltex = bgtileStore.lightTexture('ltx512', 512);
3910 // set screen alpha to min
3911 Video.colorMask = Video::CMask.Alpha;
3912 Video.blendMode = Video::BlendMode.None;
3913 Video.color = 0xff_ff_ff_ff;
3914 Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
3915 //Video.colorMask = Video::CMask.All;
3918 // also, stencil 'em, so we can filter dark areas
3919 Video.textureFiltering = true;
3920 Video.stencil = true;
3921 Video.stencilFunc(Video::StencilFunc.Always, 1);
3922 Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Replace);
3923 Video.alphaTestFunc = Video::AlphaFunc.Greater;
3924 Video.alphaTestVal = 0.03+0.011*global.config.darknessDarkness;
3925 Video.color = 0xff_ff_ff;
3926 Video.blendFunc = Video::BlendFunc.Max;
3927 Video.blendMode = Video::BlendMode.Blend; // anything except `Normal`
3928 Video.colorMask = Video::CMask.Alpha;
3930 foreach (MapEntity e; renderVisibleLights) {
3932 e.getInterpCoords(currFrameDelta, scale, out xi, out yi);
3933 auto tile = MapTile(e);
3934 if (tile && tile.litWholeTile) {
3935 //Video.color = 0xff_ff_ff;
3936 Video.fillRect(xi-xofs, yi-yofs, e.width*scale, e.height*scale);
3938 int lrad = e.lightRadius;
3939 if (lrad < 4) continue; // just in case
3941 float lightscale = float(lrad*scale)/float(ltex.tex.width);
3942 #ifdef OLD_LIGHT_OFFSETS
3943 int fx0, fy0, fx1, fy1;
3945 auto spf = e.getSpriteFrame(out doMirror, out fx0, out fy0, out fx1, out fy1);
3947 xi += (fx1-fx0)*scale/2;
3948 yi += (fy1-fy0)*scale/2;
3952 e.getLightOffset(out lxofs, out lyofs);
3957 lrad = lrad*scale/2;
3960 ltex.tex.blitAt(xi, yi, lightscale);
3962 Video.textureFiltering = false;
3964 // modify only lit parts
3965 Video.stencilFunc(Video::StencilFunc.Equal, 1);
3966 Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Keep);
3967 // multiply framebuffer colors by framebuffer alpha
3968 Video.color = 0xff_ff_ff; // it doesn't matter
3969 Video.blendFunc = Video::BlendFunc.Add;
3970 Video.blendMode = Video::BlendMode.DstMulDstAlpha;
3971 Video.colorMask = Video::CMask.Colors;
3972 Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
3974 // filter unlit parts
3975 Video.stencilFunc(Video::StencilFunc.NotEqual, 1);
3976 Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Keep);
3977 Video.blendFunc = Video::BlendFunc.Add;
3978 Video.blendMode = Video::BlendMode.Filter;
3979 Video.colorMask = Video::CMask.Colors;
3980 Video.color = 0x00_00_18+0x00_00_10*global.config.darknessDarkness;
3981 //Video.color = 0x00_00_18;
3982 //Video.color = 0x00_00_38;
3983 Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
3986 Video.blendFunc = Video::BlendFunc.Add;
3987 Video.blendMode = Video::BlendMode.Normal;
3988 Video.colorMask = Video::CMask.All;
3989 Video.alphaTestFunc = Video::AlphaFunc.Always;
3990 Video.stencil = false;
3993 // clear visible objects list (nope)
3994 //renderVisibleCids.clear();
3995 //renderVisibleLights.clear();
3998 if (global.config.drawHUD) renderHUD(currFrameDelta);
3999 renderCompass(currFrameDelta);
4001 float osdTimeLeft, osdTimeStart;
4002 string msg = osdGetMessage(out osdTimeLeft, out osdTimeStart);
4004 auto ct = GetTickCount();
4006 sprStore.loadFont('sFontSmall');
4007 auto msgHeight = sprStore.getMultilineTextHeight(msg, msgScale);
4008 int x = viewWidth/2;
4009 int y = viewHeight-64-msgHeight;
4010 auto oldColor = Video.color;
4011 Video.color = 0xff_ff_00;
4012 if (osdTimeLeft < 0.5) {
4013 int alpha = 255-clamp(int(510*osdTimeLeft), 0, 255);
4014 Video.color = Video.color|(alpha<<24);
4015 } else if (ct-osdTimeStart < 0.5) {
4016 osdTimeStart = ct-osdTimeStart;
4017 int alpha = 255-clamp(int(510*osdTimeStart), 0, 255);
4018 Video.color = Video.color|(alpha<<24);
4020 sprStore.renderMultilineTextCentered(x, y, msg, msgScale, 0x00_ff_00, 0xff_ff_ff);
4021 Video.color = oldColor;
4024 if (inWinCutscene) renderWinCutsceneOverlay();
4025 if (inIntroCutscene) renderTitleCutsceneOverlay();
4026 if (isTransitionRoom()) renderTransitionOverlay();
4030 Video.setScissor(scsave);
4031 Video.glPopMatrix();
4035 Video.color = 0xff_ff_ff;
4039 // ////////////////////////////////////////////////////////////////////////// //
4040 final class!MapObject findGameObjectClassByName (name aname) {
4041 if (!aname) return none; // just in case
4042 auto co = FindClassByGameObjName(aname);
4044 writeln("***UNKNOWN GAME OBJECT: '", aname, "'");
4047 co = GetClassReplacement(co);
4048 if (!co) FatalError("findGameObjectClassByName: WTF?!");
4049 if (!class!MapObject(co)) FatalError("findGameTileClassByName: object '%n' is not a map object, it is a `%n`", aname, GetClassName(co));
4050 return class!MapObject(co);
4054 final class!MapTile findGameTileClassByName (name aname) {
4055 if (!aname) return none; // just in case
4056 auto co = FindClassByGameObjName(aname);
4057 if (!co) return MapTile; // unknown names will be routed directly to tile object
4058 co = GetClassReplacement(co);
4059 if (!co) FatalError("findGameTileClassByName: WTF?!");
4060 if (!class!MapTile(co)) FatalError("findGameTileClassByName: object '%n' is not a tile, it is a `%n`", aname, GetClassName(co));
4061 return class!MapTile(co);
4065 final MapObject findAnyObjectOfType (name aname) {
4066 if (!aname) return none;
4067 auto cls = FindClassByGameObjName(aname);
4068 if (!cls) return none;
4069 foreach (MapObject obj; objGrid.allObjects(MapObject)) {
4070 if (obj.spectral) continue;
4071 if (obj isa cls) return obj;
4077 // ////////////////////////////////////////////////////////////////////////// //
4078 final bool isRopePlacedAt (int x, int y) {
4080 foreach (ref auto v; covered) v = false;
4081 foreach (MapTile t; objGrid.inRectPix(x, y-8, 1, 17, precise:false, castClass:MapTileRope)) {
4082 //if (!cbIsRopeTile(t)) continue;
4083 if (t.ix != x) continue;
4084 if (t.iy == y) return true;
4085 foreach (int ty; t.iy..t.iy+8) {
4087 if (d >= 0 && d < covered.length) covered[d] = true;
4090 // check if the whole rope height is completely covered with ropes
4091 foreach (auto v; covered) if (!v) return false;
4096 final MapTile CreateMapTile (int xpos, int ypos, name aname) {
4097 if (!aname) FatalError("cannot create typeless tile");
4098 auto tclass = findGameTileClassByName(aname);
4099 if (!tclass) return none;
4100 MapTile tile = SpawnObject(tclass);
4101 tile.global = global;
4103 tile.objName = aname;
4104 tile.objType = aname; // just in case
4107 tile.objId = ++lastUsedObjectId;
4108 if (!tile.initialize()) FatalError("cannot create tile '%n'", aname);
4113 final bool PutSpawnedMapTile (int x, int y, MapTile tile, optional bool putToGrid) {
4114 if (!tile || !tile.isInstanceAlive) return false;
4116 if (!putToGrid) putToGrid = (tile.moveable || tile.toSpecialGrid || tile.width != 16 || tile.height != 16 || x%16 != 0 || y%16 != 0);
4118 //writeln("putting tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (putToGrid ? "special " : ""), "grid, pos=(", x/16.0, ",", y/16.0, ")");
4121 int mapx = x/16, mapy = y/16;
4122 if (mapx < 0 || mapx >= tilesWidth || mapy < 0 || mapy >= tilesHeight) return false;
4125 // if we already have rope tile there, there is no reason to add another one
4126 if (tile isa MapTileRope) {
4127 if (isRopePlacedAt(x, y)) return false;
4130 // activate special or animated tile
4131 tile.active = tile.active || putToGrid || tile.moveable || tile.toSpecialGrid || tile.lava /*|| tile.water*/; // will be done in MakeMapTile
4132 // animated tiles must be active
4134 auto spr = tile.getSprite();
4135 if (spr && spr.frames.length > 1) {
4136 writeln("activated animated tile '", tile.objName, "'");
4144 //if (tile isa TitleTileCopy) writeln("*** PUTTING COPYRIGHT TILE");
4145 tile.toSpecialGrid = true;
4146 if (!tile.dontReplaceOthers && x&16 == 0 && y%16 == 0) {
4147 auto t = getTileAtGridAny(x/16, y/16);
4148 if (t && !t.immuneToReplacement) {
4149 writeln("REPLACING tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
4150 writeln(" NEW tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (tile.toSpecialGrid ? "special " : ""), "grid, pos=(", tile.fltx/16.0, ",", tile.flty/16.0, ")");
4154 objGrid.insert(tile);
4156 //writeln("SIZE: ", tilesWidth, "x", tilesHeight);
4157 setTileAtGrid(x/16, y/16, tile);
4158 auto t = getTileAtGridAny(x/16, y/16);
4161 writeln("putting tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (putToGrid ? "special " : ""), "grid, pos=(", x/16.0, ",", y/16.0, ")");
4162 checkTilesInRect(x/16, y/16, 16, 16, delegate bool (MapTile tile) {
4163 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, ")");
4166 FatalError("FUUUUUU");
4171 if (tile.enter) registerEnter(tile);
4172 if (tile.exit) registerExit(tile);
4178 // won't call `onDestroy()`
4179 final void RemoveMapTileFromGrid (int tileX, int tileY, optional string reason) {
4180 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
4181 auto t = getTileAtGridAny(tileX, tileY);
4183 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, ")");
4191 final MapTile MakeMapTile (int mapx, int mapy, name aname, optional bool putToGrid) {
4192 //if (mapy >= tilesHeight) { writeln("OOM: ", aname, "; ty=", mapy, "; hgt=", tilesHeight); }
4193 if (mapx < 0 || mapx >= tilesWidth || mapy < 0 || mapy >= tilesHeight) return none;
4195 // if we already have rope tile there, there is no reason to add another one
4196 if (aname == 'oRope') {
4197 if (isRopePlacedAt(mapx*16, mapy*16)) return none;
4200 auto tile = CreateMapTile(mapx*16, mapy*16, aname);
4201 if (!tile) return none;
4202 if (!PutSpawnedMapTile(mapx*16, mapy*16, tile, putToGrid!optional)) {
4211 final MapTile MakeMapTileAtPix (int xpix, int ypix, name aname, optional bool putToGrid) {
4212 // if we already have rope tile there, there is no reason to add another one
4213 if (aname == 'oRope') {
4214 if (isRopePlacedAt(xpix, ypix)) return none;
4217 auto tile = CreateMapTile(xpix, ypix, aname);
4218 if (!tile) return none;
4219 if (!PutSpawnedMapTile(xpix, ypix, tile, putToGrid!optional)) {
4228 final MapTile MakeMapRopeTileAt (int x0, int y0) {
4229 // if we already have rope tile there, there is no reason to add another one
4230 if (isRopePlacedAt(x0, y0)) return none;
4232 auto tile = CreateMapTile(x0, y0, 'oRope');
4233 if (!PutSpawnedMapTile(x0, y0, tile, putToGrid:true)) {
4242 // ////////////////////////////////////////////////////////////////////////// //
4243 final MapBackTile CreateBgTile (name sprName, optional int atx0, optional int aty0, optional int aw, optional int ah) {
4244 BackTileImage img = bgtileStore[sprName];
4245 auto res = SpawnObject(MapBackTile);
4246 res.global = global;
4249 res.bgtName = sprName;
4250 if (specified_atx0) res.tx0 = atx0;
4251 if (specified_aty0) res.ty0 = aty0;
4252 if (specified_aw) res.w = aw;
4253 if (specified_ah) res.h = ah;
4254 if (!res.initialize()) FatalError("cannot create background tile with image '%n'", sprName);
4259 // ////////////////////////////////////////////////////////////////////////// //
4261 background The background asset from which the new tile will be extracted.
4262 left The x coordinate of the left of the new tile, relative to the background asset's top left corner.
4263 top The y coordinate of the top of the new tile, relative to the background assets top left corner.
4264 width The width of the tile.
4265 height The height of the tile.
4266 x The x position in the room to place the tile.
4267 y The y position in the room to place the tile.
4268 depth The depth at which to place the tile.
4270 final void MakeMapBackTile (name bgname, int left, int top, int width, int height, int x, int y, int depth) {
4271 if (width < 1 || height < 1 || !bgname) return;
4272 auto bgt = bgtileStore[bgname];
4273 if (!bgt) FatalError("cannot load background '%n'", bgname);
4274 MapBackTile bt = SpawnObject(MapBackTile);
4277 bt.objName = bgname;
4279 bt.bgtName = bgname;
4287 // find a place for it
4292 // back tiles with the highest depth should come first
4293 MapBackTile ct = backtiles, cprev = none;
4294 while (ct && depth <= ct.depth) { cprev = ct; ct = ct.next; }
4297 bt.next = cprev.next;
4300 bt.next = backtiles;
4306 // ////////////////////////////////////////////////////////////////////////// //
4307 final MapObject SpawnMapObjectWithClass (class!MapObject oclass) {
4308 if (!oclass) return none;
4310 MapObject obj = SpawnObject(oclass);
4311 if (!obj) FatalError("cannot create map object '%n'", GetClassName(oclass));
4313 //if (aname == 'oShells' || aname == 'oShellSingle' || aname == 'oRopePile') writeln("*** CREATED '", aname, "'");
4315 obj.global = global;
4317 obj.objId = ++lastUsedObjectId;
4323 final MapObject SpawnMapObject (name aname) {
4324 if (!aname) return none;
4325 auto res = SpawnMapObjectWithClass(findGameObjectClassByName(aname));
4326 if (res && !res.objType) res.objType = aname; // just in case
4331 final MapObject PutSpawnedMapObject (int x, int y, MapObject obj) {
4332 if (!obj /*|| obj.global || obj.level*/) return none; // oops
4336 if (!obj.initialize()) { delete obj; return none; } // not fatal
4344 final MapObject MakeMapObject (int x, int y, name aname) {
4345 MapObject obj = SpawnMapObject(aname);
4346 obj = PutSpawnedMapObject(x, y, obj);
4351 // ////////////////////////////////////////////////////////////////////////// //
4352 int winCutSceneTimer = -1;
4353 int winVolcanoTimer = -1;
4354 int winCutScenePhase = 0;
4355 int winSceneDrawStatus = 0;
4356 int winMoneyCount = 0;
4358 bool winFadeOut = false;
4359 int winFadeLevel = 0;
4360 int winCutsceneSkip = 0; // 1: waiting for pay release; 2: pay released, do skip
4361 bool winCutsceneSwitchToNext = false;
4364 void startWinCutscene () {
4365 global.hasParachute = false;
4367 winCutsceneSwitchToNext = false;
4368 winCutsceneSkip = 0;
4369 isKeyPressed(GameConfig::Key.Pay);
4370 isKeyReleased(GameConfig::Key.Pay);
4372 auto olddel = ImmediateDelete;
4373 ImmediateDelete = false;
4378 addBackgroundGfxDetails();
4380 levBGImgName = 'bgCave';
4381 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
4383 blockWaterChecking = true;
4387 ImmediateDelete = olddel;
4388 CollectGarbage(true); // destroy delayed objects too
4390 if (dumpGridStats) objGrid.dumpStats();
4392 playerExited = false; // just in case
4393 playerExitDoor = none;
4401 winCutSceneTimer = -1;
4402 winCutScenePhase = 0;
4405 if (global.config.gameMode != GameConfig::GameMode.Vanilla) {
4406 if (global.config.bizarre) {
4407 global.yasmScore = 1;
4408 global.config.bizarrePlusTitle = true;
4411 array!MapTile toReplace;
4412 forEachTile(delegate bool (MapTile t) {
4413 if (t.objType == 'oGTemple' ||
4414 t.objType == 'oIce' ||
4415 t.objType == 'oDark' ||
4416 t.objType == 'oBrick' ||
4417 t.objType == 'oLush')
4424 foreach (MapTile t; miscTileGrid.allObjects()) {
4425 if (t.objType == 'oGTemple' ||
4426 t.objType == 'oIce' ||
4427 t.objType == 'oDark' ||
4428 t.objType == 'oBrick' ||
4429 t.objType == 'oLush')
4435 foreach (MapTile t; toReplace) {
4437 t.cleanDeath = true;
4438 if (rand(1,120) == 1) instance_change(oGTemple, false);
4439 else if (rand(1,100) == 1) instance_change(oIce, false);
4440 else if (rand(1,90) == 1) instance_change(oDark, false);
4441 else if (rand(1,80) == 1) instance_change(oBrick, false);
4442 else if (rand(1,70) == 1) instance_change(oLush, false);
4450 if (rand(1,5) == 1) instance_change(oLush, false);
4455 //!instance_create(0, 0, oBricks);
4457 //shakeToggle = false;
4458 //oPDummy.status = 2;
4463 if (global.kaliPunish >= 2) {
4464 instance_create(oPDummy.x, oPDummy.y+2, oBall2);
4465 obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
4467 obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
4469 obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
4471 obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
4478 void startWinCutsceneVolcano () {
4479 global.hasParachute = false;
4481 writeln("VOLCANO HOLD 0: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4482 writeln("VOLCANO PICK 0: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4486 winCutsceneSwitchToNext = false;
4487 auto olddel = ImmediateDelete;
4488 ImmediateDelete = false;
4492 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
4494 blockWaterChecking = true;
4496 ImmediateDelete = olddel;
4497 CollectGarbage(true); // destroy delayed objects too
4499 spawnPlayerAt(2*16+8, 11*16+8);
4500 player.dir = MapEntity::Dir.Right;
4502 playerExited = false; // just in case
4503 playerExitDoor = none;
4511 winCutSceneTimer = -1;
4512 winCutScenePhase = 0;
4514 MakeMapTile(0, 0, 'oEnd2BG');
4515 realViewStart.x = 0;
4516 realViewStart.y = 0;
4525 player.dead = false;
4526 player.active = true;
4527 player.visible = false;
4528 player.removeBallAndChain(temp:true);
4529 player.stunned = false;
4530 player.status = MapObject::FALLING;
4531 if (player.holdItem) player.holdItem.visible = false;
4532 player.fltx = 320/2;
4536 writeln("VOLCANO HOLD 1: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4537 writeln("VOLCANO PICK 1: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4542 void startWinCutsceneWinFall () {
4543 global.hasParachute = false;
4545 writeln("FALL HOLD 0: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4546 writeln("FALL PICK 0: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4550 winCutsceneSwitchToNext = false;
4552 auto olddel = ImmediateDelete;
4553 ImmediateDelete = false;
4557 setMenuTilesVisible(false);
4559 //addBackgroundGfxDetails();
4562 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
4564 blockWaterChecking = true;
4568 ImmediateDelete = olddel;
4569 CollectGarbage(true); // destroy delayed objects too
4571 if (dumpGridStats) objGrid.dumpStats();
4573 playerExited = false; // just in case
4574 playerExitDoor = none;
4582 winCutSceneTimer = -1;
4583 winCutScenePhase = 0;
4585 player.dead = false;
4586 player.active = true;
4587 player.visible = false;
4588 player.removeBallAndChain(temp:true);
4589 player.stunned = false;
4590 player.status = MapObject::FALLING;
4591 if (player.holdItem) player.holdItem.visible = false;
4592 player.fltx = 320/2;
4595 winSceneDrawStatus = 0;
4602 writeln("FALL HOLD 1: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4603 writeln("FALL PICK 1: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4608 void setGameOver () {
4609 if (inWinCutscene) {
4610 player.visible = false;
4611 player.removeBallAndChain(temp:true);
4612 if (player.holdItem) player.holdItem.visible = false;
4615 if (inWinCutscene > 0) {
4618 winSceneDrawStatus = 8;
4623 MapTile findEndPlatTile () {
4624 return forEachTile(delegate bool (MapTile t) { return (t isa MapTileEndPlat); }, castClass:MapTileEndPlat);
4628 MapObject findBigTreasure () {
4629 return forEachObject(delegate bool (MapObject o) { return (o isa MapObjectBigTreasure); }, castClass:MapObjectBigTreasure);
4633 void setMenuTilesVisible (bool vis) {
4635 forEachTile(delegate bool (MapTile t) {
4636 if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4637 t.invisible = false;
4642 forEachTile(delegate bool (MapTile t) {
4643 if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4652 void setMenuTilesOnTop () {
4653 forEachTile(delegate bool (MapTile t) {
4654 if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4662 void winCutscenePlayerControl (PlayerPawn plr) {
4663 auto payPress = isKeyPressed(GameConfig::Key.Pay);
4664 auto payRelease = isKeyReleased(GameConfig::Key.Pay);
4666 switch (winCutsceneSkip) {
4667 case 0: // nothing was pressed
4668 if (payPress) winCutsceneSkip = 1;
4670 case 1: // waiting for pay release
4671 if (payRelease) winCutsceneSkip = 2;
4673 case 2: // pay released, do skip
4678 // first winning room
4679 if (inWinCutscene == 1) {
4680 if (plr.ix < 448+8) {
4685 // waiting for chest to open
4686 if (winCutScenePhase == 0) {
4687 winCutSceneTimer = 120/2;
4688 winCutScenePhase = 1;
4693 if (winCutScenePhase == 1) {
4694 if (--winCutSceneTimer == 0) {
4695 winCutScenePhase = 2;
4696 winCutSceneTimer = 20;
4697 forEachObject(delegate bool (MapObject o) {
4698 if (o isa MapObjectBigChest) {
4699 o.setSprite(global.config.gameMode == GameConfig::GameMode.Vanilla ? 'sBigChestOpen' : 'sBigChestOpen2');
4700 auto treasure = MakeMapObject(o.ix, o.iy, 'oBigTreasure');
4704 o.playSound('sndClick');
4705 //!!!if (global.config.gameMode != GameConfig::GameMode.Vanilla) scrSprayGems(oBigChest.x+24, oBigChest.y+24);
4715 if (winCutScenePhase == 2) {
4716 if (--winCutSceneTimer == 0) {
4717 winCutScenePhase = 3;
4718 winCutSceneTimer = 50;
4724 if (winCutScenePhase == 3) {
4725 auto ep = findEndPlatTile();
4726 if (ep) MakeMapObject(ep.ix+global.randOther(0, 80), /*ep.iy*/192+32, 'oBurn');
4727 if (--winCutSceneTimer == 0) {
4728 winCutScenePhase = 4;
4729 winCutSceneTimer = 10;
4730 if (ep) MakeMapObject(ep.ix, ep.iy+30, 'oLavaSpray');
4736 // lava pump first accel
4737 if (winCutScenePhase == 4) {
4738 if (--winCutSceneTimer == 0) {
4739 forEachObject(delegate bool (MapObject o) {
4740 if (o isa MapObjectLavaSpray) o.yAcc = -0.1;
4746 // lava pump complete
4747 if (winCutScenePhase == 5) {
4748 if (--winCutSceneTimer == 0) {
4749 //if (oLavaSpray) oLavaSpray.yAcc = -0.1;
4750 startWinCutsceneVolcano();
4759 if (inWinCutscene == 2) {
4763 if (winCutScenePhase == 0) {
4764 winCutSceneTimer = 50;
4765 winCutScenePhase = 1;
4766 winVolcanoTimer = 10;
4770 if (winVolcanoTimer > 0) {
4771 if (--winVolcanoTimer == 0) {
4772 MakeMapObject(224+global.randOther(0,48), 144+global.randOther(0,8), 'oVolcanoFlame');
4773 winVolcanoTimer = global.randOther(10, 20);
4778 if (winCutScenePhase == 1) {
4779 if (--winCutSceneTimer == 0) {
4780 winCutSceneTimer = 30;
4781 winCutScenePhase = 2;
4782 auto sil = MakeMapObject(240, 132, 'oPlayerSil');
4790 if (winCutScenePhase == 2) {
4791 if (--winCutSceneTimer == 0) {
4792 winCutScenePhase = 3;
4793 auto sil = MakeMapObject(240, 132, 'oTreasureSil');
4803 // winning camel room
4804 if (inWinCutscene == 3) {
4805 //if (!player.holdItem) writeln("SCENE 3: LOST ITEM!");
4807 if (!plr.visible) plr.flty = -32;
4810 if (winCutScenePhase == 0) {
4811 winCutSceneTimer = 50;
4812 winCutScenePhase = 1;
4817 if (winCutScenePhase == 1) {
4818 if (--winCutSceneTimer == 0) {
4819 winCutSceneTimer = 50;
4820 winCutScenePhase = 2;
4821 plr.playSound('sndPFall');
4824 writeln("MUST BE CHAINED: ", plr.mustBeChained);
4825 if (plr.mustBeChained) {
4826 plr.removeBallAndChain(temp:true);
4827 plr.spawnBallAndChain();
4830 writeln("HOLD: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4831 writeln("PICK: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4833 if (!player.holdItem && player.pickedItem) player.scrSwitchToPocketItem(forceIfEmpty:false);
4834 if (player.holdItem) {
4835 player.holdItem.visible = true;
4836 player.holdItem.canLiveOutsideOfLevel = true;
4837 writeln("HOLD ITEM: '", GetClassName(player.holdItem.Class), "'");
4839 plr.status == MapObject::FALLING;
4840 global.plife += 99; // just in case
4845 if (winCutScenePhase == 2) {
4846 auto ball = plr.getMyBall();
4847 if (ball && plr.holdItem != ball) {
4848 ball.teleportTo(plr.fltx, plr.flty+8);
4852 if (plr.status == MapObject::STUNNED || plr.stunned) {
4856 auto treasure = MakeMapObject(144+16+8, -32, 'oBigTreasure');
4857 if (treasure) treasure.depth = 1;
4858 winCutScenePhase = 3;
4860 plr.playSound('sndTFall');
4865 if (winCutScenePhase == 3) {
4866 if (plr.status != MapObject::STUNNED && !plr.stunned) {
4867 auto bt = findBigTreasure();
4871 //plr.status = MapObject::JUMPING;
4873 plr.kJumpPressed = true;
4874 winCutScenePhase = 4;
4875 winCutSceneTimer = 50;
4882 if (winCutScenePhase == 4) {
4883 if (--winCutSceneTimer == 0) {
4884 setMenuTilesVisible(true);
4885 winCutScenePhase = 5;
4886 winSceneDrawStatus = 1;
4887 global.playMusic('musVictory', loop:false);
4888 winCutSceneTimer = 50;
4893 if (winCutScenePhase == 5) {
4894 if (winSceneDrawStatus == 3) {
4895 int money = stats.money;
4896 if (winMoneyCount < money) {
4897 if (money-winMoneyCount > 1000) {
4898 winMoneyCount += 1000;
4899 } else if (money-winMoneyCount > 100) {
4900 winMoneyCount += 100;
4901 } else if (money-winMoneyCount > 10) {
4902 winMoneyCount += 10;
4907 if (winMoneyCount >= money) {
4908 winMoneyCount = money;
4909 ++winSceneDrawStatus;
4914 if (winSceneDrawStatus == 7) {
4917 if (winFadeLevel >= 255) {
4918 ++winSceneDrawStatus;
4919 winCutSceneTimer = 30*30;
4924 if (winSceneDrawStatus == 8) {
4925 if (--winCutSceneTimer == 0) {
4931 if (--winCutSceneTimer == 0) {
4932 ++winSceneDrawStatus;
4933 winCutSceneTimer = 50;
4942 // ////////////////////////////////////////////////////////////////////////// //
4943 void renderWinCutsceneOverlay () {
4944 if (inWinCutscene == 3) {
4945 if (winSceneDrawStatus > 0) {
4946 Video.color = 0xff_ff_ff;
4947 sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
4948 //draw_set_color(txtCol);
4949 drawTextAt(64, 32, "YOU MADE IT!");
4951 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4952 if (global.config.gameMode == GameConfig::GameMode.Vanilla) {
4953 Video.color = 0xff_ff_00; //draw_set_color(c_yellow);
4954 drawTextAt(64, 48, "Classic Mode done!");
4956 Video.color = 0x00_80_80; //draw_set_color(c_teal);
4957 if (global.config.bizarrePlus) drawTextAt(64, 48, "Bizarre Mode Plus done!");
4958 else drawTextAt(64, 48, "Bizarre Mode done!");
4959 //draw_set_color(c_white);
4961 if (!global.usedShortcut) {
4962 Video.color = 0xc0_c0_c0; //draw_set_color(c_silver);
4963 drawTextAt(64, 56, "No shortcuts used!");
4964 //draw_set_color(c_yellow);
4968 if (winSceneDrawStatus > 1) {
4969 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4970 //draw_set_color(txtCol);
4971 Video.color = 0xff_ff_ff;
4972 drawTextAt(64, 64, "FINAL SCORE:");
4975 if (winSceneDrawStatus > 2) {
4976 sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
4977 Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4978 drawTextAt(64, 72, va("$%d", winMoneyCount));
4981 if (winSceneDrawStatus > 4) {
4982 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4983 Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4984 drawTextAt(64, 96, va("Time: %s", time2str(winTime/30)));
4986 draw_set_color(c_white);
4987 if (s < 10) draw_text(96+24, 96, string(m) + ":0" + string(s));
4988 else draw_text(96+24, 96, string(m) + ":" + string(s));
4992 if (winSceneDrawStatus > 5) {
4993 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4994 Video.color = 0xff_ff_ff; //draw_set_color(txtCol);
4995 drawTextAt(64, 96+8, "Kills: ");
4996 Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4997 drawTextAt(96+24, 96+8, va("%s", stats.countKills()));
5000 if (winSceneDrawStatus > 6) {
5001 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
5002 Video.color = 0xff_ff_ff; //draw_set_color(txtCol);
5003 drawTextAt(64, 96+16, "Saves: ");
5004 Video.color = 0xff_ff_ff; //draw_set_color(c_white);
5005 drawTextAt(96+24, 96+16, va("%s", stats.damselsSaved));
5009 Video.color = (255-clamp(winFadeLevel, 0, 255))<<24;
5010 Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
5013 if (winSceneDrawStatus == 8) {
5014 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
5015 Video.color = 0xff_ff_ff; //draw_set_color(c_white);
5017 if (global.config.gameMode == GameConfig::GameMode.Vanilla) {
5018 Video.color = 0xff_ff_00; //draw_set_color(c_yellow);
5019 lastString = "YOU SHALL BE REMEMBERED AS A HERO.";
5021 Video.color = 0x00_ff_ff;
5022 if (global.config.bizarrePlus) lastString = "ANOTHER LEGENDARY ADVENTURE!";
5023 else lastString = "YOUR DISCOVERIES WILL BE CELEBRATED!";
5025 auto strLen = lastString.length*8;
5027 n = trunc(ceil(n/2.0));
5028 drawTextAt(n, 116, lastString);
5034 // ////////////////////////////////////////////////////////////////////////// //
5035 #include "roomTitle.vc"
5036 #include "roomTrans1.vc"
5037 #include "roomTrans2.vc"
5038 #include "roomTrans3.vc"
5039 #include "roomTrans4.vc"
5040 #include "roomOlmec.vc"
5041 #include "roomEnd.vc"
5042 #include "roomIntro.vc"
5043 #include "roomTutorial.vc"
5044 #include "roomScores.vc"
5045 #include "roomStars.vc"
5046 #include "roomSun.vc"
5047 #include "roomMoon.vc"
5050 // ////////////////////////////////////////////////////////////////////////// //
5051 #include "packages/Generator/loadRoomGens.vc"
5052 #include "packages/Generator/loadEntityGens.vc"