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 LevelGen::RType[MaxTilesWidth, MaxTilesHeight] roomType;
83 LevelKind levelKind = LevelKind.Normal;
85 array!MapTile allEnters;
86 array!MapTile allExits;
89 int startRoomX, startRoomY;
90 int endRoomX, endRoomY;
93 transient bool playerExited;
94 transient MapEntity playerExitDoor;
95 transient bool disablePlayerThink = false;
96 transient int maxPlayingTime; // in seconds
102 bool ghostSpawned; // to speed up some checks
103 bool resetBMCOG = false;
107 // FPS, i.e. incremented by 30 in one second
108 int time; // in frames
109 int lastUsedObjectId;
110 transient int lastRenderTime = -1;
111 transient int pausedTime;
113 MapEntity deadItemsHead;
115 // screen shake variables
120 // set this before calling `fixCamera()`
121 // dimensions should be real, not scaled up/down
122 transient int viewWidth, viewHeight;
123 // room bounds, not scaled
124 IVec2D viewMin, viewMax;
126 // for Olmec level cinematics
127 IVec2D cameraSlideToDest;
128 IVec2D cameraSlideToCurr;
129 IVec2D cameraSlideToSpeed; // !0: slide
130 int cameraSlideToPlayer;
131 // `fixCamera()` will set the following
132 // coordinates will be real too (with scale applied)
133 // shake is not applied
134 transient IVec2D viewStart; // with `player.viewOffset`
135 private transient IVec2D realViewStart; // without `player.viewOffset`
137 transient int framesProcessedFromLastClear;
139 transient int BuildYear;
140 transient int BuildMonth;
141 transient int BuildDay;
142 transient int BuildHour;
143 transient int BuildMin;
144 transient string BuildDateString;
147 final string getBuildDateString () {
148 if (!BuildYear) return BuildDateString;
149 if (BuildDateString) return BuildDateString;
150 BuildDateString = va("%d-%02d-%02d %02d:%02d", BuildYear, BuildMonth, BuildDay, BuildHour, BuildMin);
151 return BuildDateString;
155 final void cameraSlideToPoint (int dx, int dy, int speedx, int speedy) {
156 cameraSlideToPlayer = 0;
157 cameraSlideToDest.x = dx;
158 cameraSlideToDest.y = dy;
159 cameraSlideToSpeed.x = abs(speedx);
160 cameraSlideToSpeed.y = abs(speedy);
161 cameraSlideToCurr.x = cameraCurrX;
162 cameraSlideToCurr.y = cameraCurrY;
166 final void cameraReturnToPlayer () {
167 if (!cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y)) {
168 cameraSlideToCurr.x = cameraCurrX;
169 cameraSlideToCurr.y = cameraCurrY;
170 if (cameraSlideToSpeed.x && abs(cameraSlideToSpeed.x) < 8) cameraSlideToSpeed.x = 8;
171 if (cameraSlideToSpeed.y && abs(cameraSlideToSpeed.y) < 8) cameraSlideToSpeed.y = 8;
172 cameraSlideToPlayer = 1;
177 // if `frameSkip` is `true`, there are more frames waiting
178 // (i.e. you may skip rendering and such)
179 transient void delegate (bool frameSkip) onBeforeFrame;
180 transient void delegate (bool frameSkip) onAfterFrame;
182 transient void delegate () onCameraTeleported;
184 transient void delegate () onLevelExitedCB;
186 // this will be called in-between frames, and
187 // `frameTime` is [0..1)
188 transient void delegate (float frameTime) onInterFrame;
190 final int bizRoomStyle { get { return (lg ? lg.bizRoomStyle : 0); } }
193 final bool isNormalLevel () { return (levelKind == LevelKind.Normal); }
194 final bool isTitleRoom () { return (levelKind == LevelKind.Title); }
195 final bool isTutorialRoom () { return (levelKind == LevelKind.Tutorial); }
196 final bool isTransitionRoom () { return (levelKind == LevelKind.Transition); }
198 bool isHUDEnabled () {
199 if (inWinCutscene) return false;
200 if (lg.finalBossLevel) return true;
201 if (isNormalLevel()) return true;
202 // allow HUD in challenge chambers
207 // ////////////////////////////////////////////////////////////////////////// //
209 void addDeath (name aname) { if (isNormalLevel()) stats.addDeath(aname); }
216 void addKill (name aname, optional bool telefrag) {
217 if (isNormalLevel()) stats.addKill(aname, telefrag!optional);
218 else if (aname == 'Shopkeeper' && levelKind == LevelKind.Stars) { ++stats.starsKills; ++starsKills; }
221 void addCollect (name aname, optional int amount) { if (isNormalLevel()) stats.addCollect(aname, amount!optional); }
223 void addDamselSaved () { if (isNormalLevel()) stats.addDamselSaved(); }
224 void addIdolStolen () { if (isNormalLevel()) stats.addIdolStolen(); }
225 void addIdolConverted () { if (isNormalLevel()) stats.addIdolConverted(); }
226 void addCrystalIdolStolen () { if (isNormalLevel()) stats.addCrystalIdolStolen(); }
227 void addCrystalIdolConverted () { if (isNormalLevel()) stats.addCrystalIdolConverted(); }
228 void addGhostSummoned () { if (isNormalLevel()) stats.addGhostSummoned(); }
231 // ////////////////////////////////////////////////////////////////////////// //
232 static final string time2str (int time) {
233 int secs = time%60; time /= 60;
234 int mins = time%60; time /= 60;
235 int hours = time%24; time /= 24;
237 if (days) return va("%d DAYS, %d:%02d:%02d", days, hours, mins, secs);
238 if (hours) return va("%d:%02d:%02d", hours, mins, secs);
239 return va("%02d:%02d", mins, secs);
243 // ////////////////////////////////////////////////////////////////////////// //
244 final int tilesWidth () { return lg.levelRoomWidth*RoomGen::Width+2; }
245 final int tilesHeight () { return (lg.finalBossLevel ? 55 : lg.levelRoomHeight*RoomGen::Height+2); }
248 // ////////////////////////////////////////////////////////////////////////// //
249 protected void resetGameInternal () {
250 if (player) player.removeBallAndChain();
264 player.removeBallAndChain();
265 auto hi = player.holdItem;
266 player.holdItem = none;
267 if (hi) hi.instanceRemove();
268 hi = player.pickedItem;
269 player.pickedItem = none;
270 if (hi) hi.instanceRemove();
277 stats.clearGameTotals();
281 // this won't generate a level yet
282 void restartGame () {
284 if (global.startMoney > 0) stats.setMoneyCheat();
285 stats.setMoney(global.startMoney);
286 levelKind = LevelKind.Normal;
290 // complement function to `restart game`
291 void generateNormalLevel () {
293 centerViewAtPlayer();
297 void restartTitle () {
300 createSpecialLevel(LevelKind.Title, &createTitleRoom, 'musTitle');
309 void restartTutorial () {
312 createSpecialLevel(LevelKind.Tutorial, &createTutorialRoom, 'musCave');
321 void restartScores () {
324 createSpecialLevel(LevelKind.Scores, &createScoresRoom, 'musTitle');
333 void restartStarsRoom () {
336 createSpecialLevel(LevelKind.Stars, &createStarsRoom, '');
345 void restartSunRoom () {
348 createSpecialLevel(LevelKind.Sun, &createSunRoom, '');
357 void restartMoonRoom () {
360 createSpecialLevel(LevelKind.Moon, &createMoonRoom, '');
369 // ////////////////////////////////////////////////////////////////////////// //
370 // generate angry shopkeeper at exit if murderer or thief
371 void generateAngryShopkeepers () {
372 if (global.murderer || global.thiefLevel > 0) {
373 foreach (MapTile e; allExits) {
374 auto obj = MonsterShopkeeper(MakeMapObject(e.ix, e.iy, 'oShopkeeper'));
376 obj.style = 'Bounty Hunter';
377 obj.status = MapObject::PATROL;
384 // ////////////////////////////////////////////////////////////////////////// //
385 final void resetRoomBounds () {
388 viewMax.x = tilesWidth*16;
389 viewMax.y = tilesHeight*16;
390 // Great Lake is bottomless (nope)
391 //if (global.lake == 1) viewMax.y -= 16;
392 //writeln("ROOM BOUNDS: (", viewMax.x, "x", viewMax.y, ")");
396 final void setRoomBounds (int x0, int y0, int x1, int y1) {
404 // ////////////////////////////////////////////////////////////////////////// //
407 float timeout; // seconds
408 float starttime; // for active
409 bool active; // true: timeout is `GetTickCount()` dismissing time
412 array!OSDMessage msglist; // [0]: current one
415 private final void osdCheckTimeouts () {
416 auto stt = GetTickCount();
417 while (msglist.length) {
418 if (!msglist[0].active) {
419 msglist[0].active = true;
420 msglist[0].starttime = stt;
422 if (msglist[0].starttime+msglist[0].timeout >= stt) break;
428 final bool osdHasMessage () {
430 return (msglist.length > 0);
434 final string osdGetMessage (out float timeLeft, out float timeStart) {
436 if (msglist.length == 0) { timeLeft = 0; return ""; }
437 auto stt = GetTickCount();
438 timeStart = msglist[0].starttime;
439 timeLeft = fmax(0, msglist[0].starttime+msglist[0].timeout-stt);
440 return msglist[0].msg;
444 final void osdClear () {
449 final void osdMessage (string msg, optional float timeout, optional bool urgent) {
451 msg = global.expandString(msg);
452 if (!specified_timeout) timeout = 3.33;
453 // special message for shops
454 if (timeout == -666) {
456 if (msglist.length && msglist[0].msg == msg) return;
457 if (msglist.length == 0 || msglist[0].msg != msg) {
460 msglist[0].msg = msg;
462 msglist[0].active = false;
463 msglist[0].timeout = 3.33;
467 if (timeout < 0.1) return;
468 timeout = fmax(1.0, timeout);
469 //writeln("OSD: ", msg);
470 // find existing one, and bring it to the top
472 for (; oldidx < msglist.length; ++oldidx) {
473 if (msglist[oldidx].msg == msg) break; // i found her!
476 if (oldidx < msglist.length) {
477 // yeah, move duplicate to the top
478 msglist[oldidx].timeout = fmax(timeout, msglist[oldidx].timeout);
479 msglist[oldidx].active = false;
480 if (urgent && oldidx != 0) {
481 timeout = msglist[oldidx].timeout;
482 msglist.remove(oldidx);
484 msglist[0].msg = msg;
485 msglist[0].timeout = timeout;
486 msglist[0].active = false;
490 msglist[0].msg = msg;
491 msglist[0].timeout = timeout;
492 msglist[0].active = false;
496 msglist[$-1].msg = msg;
497 msglist[$-1].timeout = timeout;
498 msglist[$-1].active = false;
504 // ////////////////////////////////////////////////////////////////////////// //
505 void setup (GameGlobal aGlobal, SpriteStore aSprStore, BackTileStore aBGTileStore) {
507 sprStore = aSprStore;
508 bgtileStore = aBGTileStore;
510 lg = SpawnObject(LevelGen);
514 objGrid = SpawnObject(EntityGrid);
515 objGrid.setup(0, 0, MaxTilesWidth*16, MaxTilesHeight*16);
519 // stores should be set
523 levBGImg = bgtileStore[levBGImgName];
524 foreach (MapEntity o; objGrid.allObjects()) {
527 if (t && (t.lava || t.water)) ++liquidTileCount;
529 for (MapBackTile bt = backtiles; bt; bt = bt.next) bt.onLoaded();
530 if (player) player.onLoaded();
532 if (msglist.length) {
533 msglist[0].active = false;
534 msglist[0].timeout = 0.200;
537 if (lg && lg.musicName) global.playMusic(lg.musicName); else global.stopMusic();
541 // ////////////////////////////////////////////////////////////////////////// //
542 void pickedSpectacles () {
543 foreach (MapTile t; objGrid.allObjectsSafe(MapTile)) t.onGotSpectacles();
547 // ////////////////////////////////////////////////////////////////////////// //
548 #include "rgentile.vc"
549 #include "rgenobj.vc"
552 void onLevelExited () {
553 if (playerExitDoor isa TitleTileXTitle) {
554 playerExitDoor = none;
559 if (isTitleRoom() || levelKind == LevelKind.Scores) {
560 if (playerExitDoor) processTitleExit(playerExitDoor);
561 playerExitDoor = none;
564 if (isTutorialRoom()) {
565 playerExitDoor = none;
567 global.currLevel = 1;
568 generateNormalLevel();
572 if (levelKind == LevelKind.Stars || levelKind == LevelKind.Sun || levelKind == LevelKind.Moon) {
573 playerExitDoor = none;
575 if (onLevelExitedCB) onLevelExitedCB();
580 if (isNormalLevel()) {
581 stats.maxLevelComplete = max(stats.maxLevelComplete, global.currLevel);
583 if (playerExitDoor) {
584 if (playerExitDoor.objType == 'oXGold') {
585 writeln("exiting to City Of Gold");
586 global.cityOfGold = true;
587 //!global.currLevel += 1;
588 } else if (playerExitDoor.objType == 'oXMarket') {
589 writeln("exiting to Black Market");
590 global.genBlackMarket = true;
591 //!global.currLevel += 1;
595 if (onLevelExitedCB) onLevelExitedCB();
597 playerExitDoor = none;
598 if (levelKind == LevelKind.Transition) {
599 if (global.thiefLevel > 0) global.thiefLevel -= 1;
600 if (global.alienCraft) ++global.alienCraft;
601 if (global.yetiLair) ++global.yetiLair;
602 if (global.lake) ++global.lake;
603 if (global.cityOfGold) ++global.cityOfGold;
604 //orig dbg:if (global.currLevel == 1) global.currLevel += firstLevelSkip; else global.currLevel += levelSkip;
606 if (!global.blackMarket && !global.cityOfGold /*&& !global.yetiLair*/) {
607 global.currLevel += 1;
613 // < 20 seconds per level: looks like a speedrun
614 global.noDarkLevel = (levelEndTime > levelStartTime && levelEndTime-levelStartTime < 20*30);
615 if (lg.finalBossLevel) {
618 // add money for big idol
619 player.addScore(50000);
623 generateTransitionLevel();
626 //centerViewAtPlayer();
630 void onOlmecDead (MapObject o) {
631 writeln("*** OLMEC IS DEAD!");
632 foreach (MapTile t; allExits) {
635 auto st = checkTileAtPoint(t.ix+8, t.iy+16);
637 st = MakeMapTile(t.ix/16, t.iy/16+1, 'oTemple');
640 st.invincible = true;
646 void generateLevelMessages () {
647 writeln("LEVEL NUMBER: ", global.currLevel);
648 if (global.darkLevel) {
649 if (global.hasCrown) {
650 osdMessage("THE HEDJET SHINES BRIGHTLY.");
651 global.darkLevel = false;
652 } else if (global.config.scumDarkness < 2) {
653 osdMessage("I CAN'T SEE A THING!\nI'D BETTER USE THESE FLARES!");
657 if (global.blackMarket) osdMessage("WELCOME TO THE BLACK MARKET!");
659 if (global.cemetary) osdMessage("THE DEAD ARE RESTLESS!");
660 if (global.lake == 1) osdMessage("I CAN HEAR RUSHING WATER...");
662 if (global.snakePit) osdMessage("I HEAR SNAKES... I HATE SNAKES!");
663 if (global.yetiLair == 1) osdMessage("IT SMELLS LIKE WET FUR IN HERE!");
664 if (global.alienCraft == 1) osdMessage("THERE'S A PSYCHIC PRESENCE HERE!");
665 if (global.cityOfGold == 1) {
666 if (true /*hasOlmec()*/) osdMessage("IT'S THE LEGENDARY CITY OF GOLD!");
669 if (global.sacrificePit) osdMessage("I CAN HEAR PRAYERS TO KALI!");
673 final MapObject debugSpawnObjectWithClass (class!MapObject oclass, optional bool playerDir) {
674 if (!oclass) return none;
676 bool canLeft = !isSolidAtPoint(player.ix-8, player.iy);
677 bool canRight = !isSolidAtPoint(player.ix+16, player.iy);
678 if (!canLeft && !canRight) return none;
679 if (canLeft && canRight) {
681 dx = (player.dir == MapObject::Dir.Left ? -16 : 16);
686 dx = (canLeft ? -16 : 16);
688 auto obj = SpawnMapObjectWithClass(oclass);
689 if (obj isa MapEnemy) { dx -= 8; dy -= 8; }
690 if (obj) PutSpawnedMapObject(player.ix+dx, player.iy+dy, obj);
695 final MapObject debugSpawnObject (name aname) {
696 if (!aname) return none;
697 return debugSpawnObjectWithClass(findGameObjectClassByName(aname));
701 void createSpecialLevel (LevelKind kind, scope void delegate () creator, name amusic) {
702 global.darkLevel = false;
706 global.resetStartingItems();
708 global.setMusicPitch(1.0);
711 auto olddel = ImmediateDelete;
712 ImmediateDelete = false;
720 addBackgroundGfxDetails();
721 //levBGImgName = 'bgCave';
722 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
724 blockWaterChecking = true;
728 ImmediateDelete = olddel;
729 CollectGarbage(true); // destroy delayed objects too
731 if (dumpGridStats) objGrid.dumpStats();
733 playerExited = false; // just in case
734 playerExitDoor = none;
739 lg.musicName = amusic;
740 if (amusic) global.playMusic(lg.musicName); else global.stopMusic();
744 void createTitleLevel () {
745 createSpecialLevel(LevelKind.Title, &createTitleRoom, 'musTitle');
749 void createTutorialLevel () {
750 createSpecialLevel(LevelKind.Tutorial, &createTutorialRoom, 'musCave');
759 // `global.currLevel` is the new level
760 void generateTransitionLevel () {
761 global.darkLevel = false;
766 resetTransitionOverlay();
768 global.setMusicPitch(1.0);
769 switch (global.config.transitionMusicMode) {
770 case GameConfig::MusicMode.Silent: global.stopMusic(); break;
771 case GameConfig::MusicMode.Restart: global.restartMusic(); break;
772 case GameConfig::MusicMode.DontTouch: break;
775 levelKind = LevelKind.Transition;
777 auto olddel = ImmediateDelete;
778 ImmediateDelete = false;
781 if (global.currLevel < 4) createTrans1Room();
782 else if (global.currLevel == 4) createTrans1xRoom();
783 else if (global.currLevel < 8) createTrans2Room();
784 else if (global.currLevel == 8) createTrans2xRoom();
785 else if (global.currLevel < 12) createTrans3Room();
786 else if (global.currLevel == 12) createTrans3xRoom();
787 else if (global.currLevel < 16) createTrans4Room();
788 else if (global.currLevel == 16) createTrans4Room();
789 else createTrans1Room(); //???
794 addBackgroundGfxDetails();
795 //levBGImgName = 'bgCave';
796 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
798 blockWaterChecking = true;
802 if (damselSaved > 0) {
803 // this is special "damsel ready to kiss you" object, not a heart
804 MakeMapObject(176+8, 176+8, 'oDamselKiss');
805 global.plife += damselSaved; // if player skipped transition cutscene
809 ImmediateDelete = olddel;
810 CollectGarbage(true); // destroy delayed objects too
812 if (dumpGridStats) objGrid.dumpStats();
814 playerExited = false; // just in case
815 playerExitDoor = none;
820 //global.playMusic(lg.musicName);
824 void generateLevel () {
825 levelStartTime = time;
831 global.genBlackMarket = false;
834 global.setMusicPitch(1.0);
835 stats.clearLevelTotals();
837 levelKind = LevelKind.Normal;
844 //writeln("tw:", tilesWidth, "; th:", tilesHeight);
846 auto olddel = ImmediateDelete;
847 ImmediateDelete = false;
850 if (lg.finalBossLevel) {
851 blockWaterChecking = true;
855 // if transition cutscene was skipped...
856 global.plife += max(0, damselSaved); // if player skipped transition cutscene
860 startRoomX = lg.startRoomX;
861 startRoomY = lg.startRoomY;
862 endRoomX = lg.endRoomX;
863 endRoomY = lg.endRoomY;
864 addBackgroundGfxDetails();
865 foreach (int y; 0..tilesHeight) {
866 foreach (int x; 0..tilesWidth) {
872 levBGImgName = lg.bgImgName;
873 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
875 if (global.allowAngryShopkeepers) generateAngryShopkeepers();
877 lg.generateEntities();
879 // add box of flares to dark level
880 if (global.darkLevel && allEnters.length) {
881 auto enter = allEnters[0];
882 int x = enter.ix, y = enter.iy;
883 if (!isSolidAtPoint(x-16, y)) MakeMapObject(x-16+8, y+8, 'oFlareCrate');
884 else if (!isSolidAtPoint(x+16, y)) MakeMapObject(x+16+8, y+8, 'oFlareCrate');
885 else MakeMapObject(x+8, y+8, 'oFlareCrate');
888 //scrGenerateEntities();
889 //foreach (; 0..2) scrGenerateEntities();
891 writeln(objGrid.countObjects, " alive objects inserted");
892 writeln(countBackTiles, " background tiles inserted");
894 if (!player) FatalError("player pawn is not spawned");
896 if (lg.finalBossLevel) {
897 blockWaterChecking = true;
899 blockWaterChecking = false;
904 ImmediateDelete = olddel;
905 CollectGarbage(true); // destroy delayed objects too
907 if (dumpGridStats) objGrid.dumpStats();
909 playerExited = false; // just in case
910 playerExitDoor = none;
912 levelMoneyStart = stats.money;
915 generateLevelMessages();
920 if (lastMusicName != lg.musicName) {
921 global.playMusic(lg.musicName);
922 //writeln("!<", lastMusicName, ">:<", lg.musicName, ">");
924 //writeln("MM: ", global.config.nextLevelMusicMode);
925 switch (global.config.nextLevelMusicMode) {
926 case GameConfig::MusicMode.Silent: global.stopMusic(); break; // the thing that should not be
927 case GameConfig::MusicMode.Restart: global.restartMusic(); break;
928 case GameConfig::MusicMode.DontTouch:
929 if (global.config.transitionMusicMode == GameConfig::MusicMode.Silent) {
930 global.playMusic(lg.musicName);
935 lastMusicName = lg.musicName;
936 //global.playMusic(lg.musicName);
939 if (global.cityOfGold == 1 || global.genBlackMarket) resetBMCOG = true;
941 if (global.cityOfGold == 1) {
942 lg.mapSprite = 'sMapTemple';
943 lg.mapTitle = "City of Gold";
944 } else if (global.blackMarket) {
945 lg.mapSprite = 'sMapJungle';
946 lg.mapTitle = "Black Market";
951 // ////////////////////////////////////////////////////////////////////////// //
952 int currKeys, nextKeys;
953 int pressedKeysQ, releasedKeysQ;
954 int keysPressed, keysReleased = -1;
957 struct SavedKeyState {
958 int currKeys, nextKeys;
959 int pressedKeysQ, releasedKeysQ;
960 int keysPressed, keysReleased;
962 int roomSeed, otherSeed;
966 // for saving/replaying
967 final void keysSaveState (out SavedKeyState ks) {
968 ks.currKeys = currKeys;
969 ks.nextKeys = nextKeys;
970 ks.pressedKeysQ = pressedKeysQ;
971 ks.releasedKeysQ = releasedKeysQ;
972 ks.keysPressed = keysPressed;
973 ks.keysReleased = keysReleased;
976 // for saving/replaying
977 final void keysRestoreState (const ref SavedKeyState ks) {
978 currKeys = ks.currKeys;
979 nextKeys = ks.nextKeys;
980 pressedKeysQ = ks.pressedKeysQ;
981 releasedKeysQ = ks.releasedKeysQ;
982 keysPressed = ks.keysPressed;
983 keysReleased = ks.keysReleased;
987 final void keysNextFrame () {
992 final void clearKeys () {
1002 final void onKey (int code, bool down) {
1007 if (keysReleased&code) {
1008 keysPressed |= code;
1009 keysReleased &= ~code;
1010 pressedKeysQ |= code;
1014 if (keysPressed&code) {
1015 keysReleased |= code;
1016 keysPressed &= ~code;
1017 releasedKeysQ |= code;
1022 final bool isKeyDown (int code) {
1023 return !!(currKeys&code);
1026 final bool isKeyPressed (int code) {
1027 bool res = !!(pressedKeysQ&code);
1028 pressedKeysQ &= ~code;
1032 final bool isKeyReleased (int code) {
1033 bool res = !!(releasedKeysQ&code);
1034 releasedKeysQ &= ~code;
1039 final void clearKeysPressRelease () {
1040 keysPressed = default.keysPressed;
1041 keysReleased = default.keysReleased;
1042 pressedKeysQ = default.pressedKeysQ;
1043 releasedKeysQ = default.releasedKeysQ;
1049 // ////////////////////////////////////////////////////////////////////////// //
1050 final void registerEnter (MapTile t) {
1057 final void registerExit (MapTile t) {
1064 final bool isYAtEntranceRow (int py) {
1066 foreach (MapTile t; allEnters) if (t.iy == py) return true;
1071 final int calcNearestEnterDist (int px, int py) {
1072 if (allEnters.length == 0) return int.max;
1073 int curdistsq = int.max;
1074 foreach (MapTile t; allEnters) {
1075 int xc = px-t.xCenter, yc = py-t.yCenter;
1076 int distsq = xc*xc+yc*yc;
1077 if (distsq < curdistsq) curdistsq = distsq;
1079 return round(sqrt(curdistsq));
1083 final int calcNearestExitDist (int px, int py) {
1084 if (allExits.length == 0) return int.max;
1085 int curdistsq = int.max;
1086 foreach (MapTile t; allExits) {
1087 int xc = px-t.xCenter, yc = py-t.yCenter;
1088 int distsq = xc*xc+yc*yc;
1089 if (distsq < curdistsq) curdistsq = distsq;
1091 return round(sqrt(curdistsq));
1095 // ////////////////////////////////////////////////////////////////////////// //
1096 final void clearForTransition () {
1097 auto olddel = ImmediateDelete;
1098 ImmediateDelete = false;
1100 ImmediateDelete = olddel;
1101 CollectGarbage(true); // destroy delayed objects too
1102 global.darkLevel = false;
1106 // ////////////////////////////////////////////////////////////////////////// //
1107 final int countBackTiles () {
1109 for (MapBackTile bt = backtiles; bt; bt = bt.next) ++res;
1114 final void clearWholeLevel () {
1118 // don't kill objects the player is holding
1120 if (player.pickedItem isa ItemBall) {
1121 player.pickedItem.instanceRemove();
1122 player.pickedItem = none;
1124 if (player.pickedItem && player.pickedItem.grid) {
1125 player.pickedItem.grid.remove(player.pickedItem.gridId);
1126 writeln("secured pocket item '", GetClassName(player.pickedItem.Class), "'");
1128 if (player.holdItem isa ItemBall) {
1129 player.removeBallAndChain(temp:true);
1130 if (player.holdItem) player.holdItem.instanceRemove();
1131 player.holdItem = none;
1133 if (player.holdItem && player.holdItem.grid) {
1134 player.holdItem.grid.remove(player.holdItem.gridId);
1135 writeln("secured held item '", GetClassName(player.holdItem.Class), "'");
1137 writeln("secured ball; mustBeChained=", player.mustBeChained, "; wasHoldingBall=", player.wasHoldingBall);
1140 int count = objGrid.countObjects();
1141 if (dumpGridStats) { if (objGrid.getFirstObjectCID()) objGrid.dumpStats(); }
1142 objGrid.removeAllObjects(true); // and destroy
1143 if (count > 0) writeln(count, " objects destroyed");
1145 lastUsedObjectId = 0;
1148 lastRenderTime = -1;
1149 liquidTileCount = 0;
1153 MapBackTile t = backtiles;
1159 framesProcessedFromLastClear = 0;
1163 final void insertObject (MapEntity o) {
1165 if (o.grid) FatalError("cannot put object into level twice");
1170 final void spawnPlayerAt (int x, int y) {
1171 // if we have no player, spawn new one
1172 // otherwise this just a level transition, so simply reposition him
1174 // don't add player to object list, as it has very separate processing anyway
1175 player = SpawnObject(PlayerPawn);
1176 player.global = global;
1177 player.level = self;
1178 if (!player.initialize()) {
1180 FatalError("something is wrong with player initialization");
1186 player.saveInterpData();
1188 if (player.mustBeChained || global.config.scumBallAndChain) {
1189 writeln("*** spawning ball and chain");
1190 player.spawnBallAndChain(levelStart:true);
1192 playerExited = false;
1193 playerExitDoor = none;
1194 if (global.config.startWithKapala) global.hasKapala = true;
1195 centerViewAtPlayer();
1196 // reinsert player items into grid
1197 if (player.pickedItem) objGrid.insert(player.pickedItem);
1198 if (player.holdItem) objGrid.insert(player.holdItem);
1199 //writeln("player spawned; active=", player.active);
1200 player.scrSwitchToPocketItem(forceIfEmpty:false);
1204 final void teleportPlayerTo (int x, int y) {
1208 player.saveInterpData();
1213 final void resurrectPlayer () {
1214 if (player) player.resurrect();
1215 playerExited = false;
1216 playerExitDoor = none;
1220 // ////////////////////////////////////////////////////////////////////////// //
1221 final void scrShake (int duration) {
1222 if (shakeLeft == 0) {
1228 shakeLeft = max(shakeLeft, duration);
1233 // ////////////////////////////////////////////////////////////////////////// //
1236 ItemStolen, // including damsel, lol
1243 // make the nearest shopkeeper angry. RAWR!
1244 void scrShopkeeperAnger (SCAnger reason, optional int maxdist, optional MapEntity offender) {
1245 if (!offender) offender = player;
1246 auto shp = MonsterShopkeeper(findNearestEnemy(offender.ix, offender.iy, delegate bool (MapEnemy o) {
1247 auto sc = MonsterShopkeeper(o);
1248 if (!sc) return false;
1249 if (sc.dead || sc.angered) return false;
1251 }, castClass:MonsterShopkeeper));
1254 if (specified_maxdist && offender.directionToEntityCenter(shp) > maxdist) return;
1255 if (!shp.dead && !shp.angered) {
1256 shp.status = MapObject::ATTACK;
1258 if (global.murderer) msg = "YOU'LL PAY FOR YOUR CRIMES!";
1259 else if (reason == SCAnger.ItemStolen) msg = "COME BACK HERE, THIEF!";
1260 else if (reason == SCAnger.TileDestroyed) msg = "DIE, YOU VANDAL!";
1261 else if (reason == SCAnger.BombDropped) msg = "TERRORIST!";
1262 else if (reason == SCAnger.DamselWhipped) msg = "HEY, ONLY I CAN DO THAT!";
1263 else if (reason == SCAnger.CrapsCheated) msg = "DIE, CHEATER!";
1264 else msg = "NOW I'M REALLY STEAMED!";
1265 if (msg) osdMessage(msg, -666);
1266 global.thiefLevel += (global.thiefLevel > 0 ? 3 : 2);
1272 final MapObject findCrapsPrize () {
1273 foreach (MapObject o; objGrid.allObjects(MapObject)) {
1274 if (!o.spectral && o.inDiceHouse) return o;
1280 // ////////////////////////////////////////////////////////////////////////// //
1281 // 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.
1282 // note: idols moved by monkeys will have false `stolenIdol`
1283 void scrTriggerIdolAltar (bool stolenIdol) {
1284 ObjTikiCurse res = none;
1285 int curdistsq = int.max;
1286 int px = player.xCenter, py = player.yCenter;
1287 foreach (MapObject o; objGrid.allObjects(MapObject)) {
1288 auto tcr = ObjTikiCurse(o);
1290 if (tcr.activated) continue;
1291 int xc = px-tcr.xCenter, yc = py-tcr.yCenter;
1292 int distsq = xc*xc+yc*yc;
1293 if (distsq < curdistsq) {
1298 if (res) res.activate(stolenIdol);
1302 // ////////////////////////////////////////////////////////////////////////// //
1303 void setupGhostTime () {
1304 musicFadeTimer = -1;
1305 ghostSpawned = false;
1307 // there is no ghost on the first level
1308 if (inWinCutscene || !isNormalLevel() || lg.finalBossLevel ||
1309 (!global.config.ghostAtFirstLevel && global.currLevel == 1))
1312 global.setMusicPitch(1.0);
1316 if (global.config.scumGhost < 0) {
1319 osdMessage("A GHASTLY VOICE CALLS YOUR NAME!\nLET'S GET OUT OF HERE!", 6.66, urgent:true);
1323 if (global.config.scumGhost == 0) {
1329 // randomizes time until ghost appears once time limit is reached
1330 // global.ghostRandom (true/false) if extra time is randomized - loaded from yasm.cfg. if false, use 30 second default
1331 // ghostTimeLeft (time in seconds * 1000) for currently generated level
1333 if (global.config.ghostRandom) {
1334 auto tMin = global.randOther(10, 30); // min(global.plife*10, 30);
1335 auto tMax = max(tMin+global.randOther(10, 30), min(global.config.scumGhost, 300)); // 5 minutes max
1336 auto tTime = global.randOther(tMin, tMax);
1337 if (tTime <= 0) tTime = round(tMax/2.0);
1338 ghostTimeLeft = tTime;
1340 ghostTimeLeft = global.config.scumGhost; // 5 minutes max
1343 ghostTimeLeft += max(0, global.config.ghostExtraTime);
1345 ghostTimeLeft *= 30; // seconds -> frames
1346 //global.ghostShowTime
1350 void spawnGhost () {
1352 ghostSpawned = true;
1355 int vwdt = (viewMax.x-viewMin.x);
1356 int vhgt = (viewMax.y-viewMin.y);
1360 if (player.ix < viewMin.x+vwdt/2) {
1361 // player is in the left side
1362 gx = viewMin.x+vwdt/2+vwdt/4;
1364 // player is in the right side
1365 gx = viewMin.x+vwdt/4;
1368 if (player.iy < viewMin.y+vhgt/2) {
1369 // player is in the left side
1370 gy = viewMin.y+vhgt/2+vhgt/4;
1372 // player is in the right side
1373 gy = viewMin.y+vhgt/4;
1376 writeln("spawning ghost at tile (", gx/16, ",", gy/16, "); player is at tile (", player.ix/16, ",", player.iy/16, ")");
1378 MakeMapObject(gx, gy, 'oGhost');
1381 if (oPlayer1.x > room_width/2) instance_create(view_xview[0]+view_wview[0]+8, view_yview[0]+floor(view_hview[0] / 2), oGhost);
1382 else instance_create(view_xview[0]-32, view_yview[0]+floor(view_hview[0]/2), oGhost);
1383 global.ghostExists = true;
1388 void thinkFrameGameGhost () {
1389 if (player.dead) return;
1390 if (!isNormalLevel()) return; // just in case
1392 if (ghostTimeLeft < 0) {
1394 if (musicFadeTimer > 0) {
1395 musicFadeTimer = -1;
1396 global.setMusicPitch(1.0);
1401 if (musicFadeTimer >= 0) {
1403 if (musicFadeTimer > 0 && musicFadeTimer <= 100*3) {
1404 float pitch = 1.0-(0.00226/3.0)*musicFadeTimer;
1405 //writeln("MFT: ", musicFadeTimer, "; pitch=", pitch);
1406 global.setMusicPitch(pitch);
1410 if (ghostTimeLeft == 0) {
1411 // she is already here!
1415 // no ghost if we have a crown
1416 if (global.hasCrown) {
1421 // if she was already spawned, don't do it again
1427 if (--ghostTimeLeft != 0) {
1429 if (global.config.ghostExtraTime > 0) {
1430 if (ghostTimeLeft == global.config.ghostExtraTime*30) {
1431 osdMessage("A CHILL RUNS UP YOUR SPINE...\nLET'S GET OUT OF HERE!", 6.66, urgent:true);
1433 if (musicFadeTimer < 0 && ghostTimeLeft <= global.config.ghostExtraTime*30) {
1441 if (player.isExitingSprite) {
1442 // no reason to spawn her, we're leaving
1451 void thinkFrameGame () {
1452 thinkFrameGameGhost();
1453 // udjat eye blinking
1454 if (global.hasUdjatEye && player) {
1455 foreach (MapTile t; allExits) {
1456 if (t isa MapTileBlackMarketDoor) {
1457 auto dm = int(player.distanceToEntity(t));
1459 if (udjatAlarm < 1 || dm < udjatAlarm) udjatAlarm = dm;
1463 global.udjatBlink = false;
1466 if (udjatAlarm > 0) {
1467 if (--udjatAlarm == 0) {
1468 global.udjatBlink = !global.udjatBlink;
1469 if (global.hasUdjatEye && player) {
1470 player.playSound(global.udjatBlink ? 'sndBlink1' : 'sndBlink2');
1474 switch (levelKind) {
1475 case LevelKind.Stars: thinkFrameGameStars(); break;
1476 case LevelKind.Sun: thinkFrameGameSun(); break;
1477 case LevelKind.Moon: thinkFrameGameMoon(); break;
1478 case LevelKind.Transition: thinkFrameTransition(); break;
1483 // ////////////////////////////////////////////////////////////////////////// //
1484 private final bool isWaterTileCB (MapTile t) {
1485 return (t && t.visible && t.water);
1489 private final bool isLavaTileCB (MapTile t) {
1490 return (t && t.visible && t.lava);
1494 // ////////////////////////////////////////////////////////////////////////// //
1495 const int GreatLakeStartTileY = 28;
1498 final void fillGreatLake () {
1499 if (global.lake == 1) {
1500 foreach (int y; GreatLakeStartTileY..tilesHeight) {
1501 foreach (int x; 0..tilesWidth) {
1502 auto t = checkTileAtPoint(x*16, y*16, delegate bool (MapTile t) {
1503 if (t.spectral || !t.visible || t.invisible || t.moveable) return false;
1507 t = MakeMapTile(x, y, 'oWaterSwim');
1511 t.setSprite(y == GreatLakeStartTileY ? 'sWaterTop' : 'sWater');
1512 } else if (t.lava) {
1513 t.setSprite(y == GreatLakeStartTileY ? 'sLavaTop' : 'sLava');
1521 // called once after level generation
1522 final void fixLiquidTop () {
1523 if (global.lake == 1) fillGreatLake();
1525 liquidTileCount = 0;
1526 foreach (MapTile t; objGrid.allObjects(MapTile)) {
1527 if (!t.water && !t.lava) continue;
1530 //writeln("fixing water tile(", GetClassName(t.Class), "):'", t.objName, "' (water=", t.water, "; lava=", t.lava, "); lqc=", liquidTileCount);
1532 //if (global.lake == 1) continue; // it is done in `fillGreatLake()`
1534 if (!checkTileAtPoint(t.ix+8, t.iy-8, (t.lava ? &isLavaTileCB : &isWaterTileCB))) {
1535 t.setSprite(t.lava ? 'sLavaTop' : 'sWaterTop');
1537 // don't do this, it will destroy seaweed
1538 //t.setSprite(t.lava ? 'sLava' : 'sWater');
1539 auto spr = t.getSprite();
1540 if (!spr) t.setSprite(t.lava ? 'sLava' : 'sWater');
1541 else if (spr.Name == 'sLavaTop') t.setSprite('sLava');
1542 else if (spr.Name == 'sWaterTop') t.setSprite('sWater');
1545 //writeln("liquid tiles count: ", liquidTileCount);
1549 // ////////////////////////////////////////////////////////////////////////// //
1550 transient MapTile curWaterTile;
1551 transient bool curWaterTileCheckHitsLava;
1552 transient bool curWaterTileCheckHitsSolidOrWater; // only for `checkWaterOrSolidTilePartialCB`
1553 transient int curWaterTileLastHDir;
1554 transient ubyte[16, 16] curWaterOccupied;
1555 transient int curWaterOccupiedCount;
1556 transient int curWaterTileCheckX0, curWaterTileCheckY0;
1559 private final void clearCurWaterCheckState () {
1560 curWaterTileCheckHitsLava = false;
1561 curWaterOccupiedCount = 0;
1562 foreach (auto idx; 0..16*16) curWaterOccupied[idx] = 0;
1566 private final bool checkWaterOrSolidTileCB (MapTile t) {
1567 if (t == curWaterTile) return false;
1568 if (t.lava && curWaterTile.water) {
1569 curWaterTileCheckHitsLava = true;
1572 if (t.ix%16 != 0 || t.iy%16 != 0) {
1573 if (t.water || t.solid) {
1574 // fill occupied array
1575 //FIXME: optimize this
1576 if (curWaterOccupiedCount < 16*16) {
1577 foreach (auto dy; t.y0..t.y1+1) {
1578 foreach (auto dx; t.x0..t.x1+1) {
1579 int sx = dx-curWaterTileCheckX0;
1580 int sy = dy-curWaterTileCheckY0;
1581 if (sx >= 0 && sx <= 16 && sy >= 0 && sy <= 15 && !curWaterOccupied[sx, sy]) {
1582 curWaterOccupied[sx, sy] = 1;
1583 ++curWaterOccupiedCount;
1589 return false; // need to check for lava
1591 if (t.water || t.solid || t.lava) {
1592 curWaterOccupiedCount = 16*16;
1593 if (t.water && curWaterTile.lava) t.instanceRemove();
1595 return false; // need to check for lava
1599 private final bool checkWaterOrSolidTilePartialCB (MapTile t) {
1600 if (t == curWaterTile) return false;
1601 if (t.lava && curWaterTile.water) {
1602 //writeln("!!!!!!!!");
1603 curWaterTileCheckHitsLava = true;
1606 if (t.water || t.solid || t.lava) {
1607 //writeln("*********");
1608 curWaterTileCheckHitsSolidOrWater = true;
1609 if (t.water && curWaterTile.lava) t.instanceRemove();
1611 return false; // need to check for lava
1615 private final bool isFullyOccupiedAtTilePos (int tileX, int tileY) {
1616 clearCurWaterCheckState();
1617 curWaterTileCheckX0 = tileX*16;
1618 curWaterTileCheckY0 = tileY*16;
1619 checkTilesInRect(tileX*16, tileY*16, 16, 16, &checkWaterOrSolidTileCB);
1620 return (curWaterTileCheckHitsLava || curWaterOccupiedCount == 16*16);
1624 private final bool isAtLeastPartiallyOccupiedAtTilePos (int tileX, int tileY) {
1625 curWaterTileCheckHitsLava = false;
1626 curWaterTileCheckHitsSolidOrWater = false;
1627 checkTilesInRect(tileX*16, tileY*16, 16, 16, &checkWaterOrSolidTilePartialCB);
1628 return (curWaterTileCheckHitsSolidOrWater || curWaterTileCheckHitsLava);
1632 private final bool waterCanReachGroundHoleInDir (MapTile wtile, int dx) {
1633 if (dx == 0) return false; // just in case
1635 int x = wtile.ix/16, y = wtile.iy/16;
1637 while (x >= 0 && x < tilesWidth) {
1638 if (!isAtLeastPartiallyOccupiedAtTilePos(x, y+1)) return true;
1639 if (isAtLeastPartiallyOccupiedAtTilePos(x, y)) return false;
1646 // returns `true` if this tile must be removed
1647 private final bool checkWaterFlow (MapTile wtile) {
1648 if (global.lake == 1) {
1649 if (wtile.iy >= GreatLakeStartTileY*16) return false; // lake tile, don't touch
1650 if (wtile.iy >= GreatLakeStartTileY*16-16) return true; // remove it, so it won't stack on a lake
1653 if (wtile.ix%16 != 0 || wtile.iy%16 != 0) return true; // sanity check
1655 curWaterTile = wtile;
1656 curWaterTileLastHDir = 0; // never moved to the side
1658 bool wasMoved = false;
1661 int tileX = wtile.ix/16, tileY = wtile.iy/16;
1664 if (tileY >= tilesHeight) return true;
1666 // check if we can fall down
1667 auto canFall = !isAtLeastPartiallyOccupiedAtTilePos(tileX, tileY+1);
1668 // disappear if can fall in lava
1669 if (wtile.water && curWaterTileCheckHitsLava) {
1670 //!writeln(wtile.objId, ": LAVA HIT DOWN");
1674 // fake, so caller will not start removing tiles
1675 if (canFall) wtile.waterMovedDown = true;
1681 //!writeln(wtile.objId, ": GOING DOWN");
1682 curWaterTileLastHDir = 0;
1683 wtile.iy = wtile.iy+16;
1685 wtile.waterMovedDown = true;
1689 bool canMoveLeft = (curWaterTileLastHDir > 0 ? false : !isAtLeastPartiallyOccupiedAtTilePos(tileX-1, tileY));
1690 // disappear if near lava
1691 if (wtile.water && curWaterTileCheckHitsLava) {
1692 //!writeln(wtile.objId, ": LAVA HIT LEFT");
1696 bool canMoveRight = (curWaterTileLastHDir < 0 ? false : !isAtLeastPartiallyOccupiedAtTilePos(tileX+1, tileY));
1697 // disappear if near lava
1698 if (wtile.water && curWaterTileCheckHitsLava) {
1699 //!writeln(wtile.objId, ": LAVA HIT RIGHT");
1703 if (!canMoveLeft && !canMoveRight) {
1705 //!if (wasMove) writeln(wtile.objId, ": NO MORE MOVES");
1709 if (canMoveLeft && canMoveRight) {
1710 // choose random direction
1711 //!writeln(wtile.objId, ": CHOOSING RANDOM HDIR");
1712 // actually, choose direction that leads to hole in a ground
1713 if (waterCanReachGroundHoleInDir(wtile, -1)) {
1714 // can reach hole at the left side
1715 if (waterCanReachGroundHoleInDir(wtile, 1)) {
1716 // can reach hole at the right side, choose at random
1717 if (global.randOther(0, 1)) canMoveRight = false; else canMoveLeft = false;
1720 canMoveRight = false;
1723 // can't reach hole at the left side
1724 if (waterCanReachGroundHoleInDir(wtile, 1)) {
1725 // can reach hole at the right side, choose at random
1726 canMoveLeft = false;
1728 // no holes at any side, choose at random
1729 if (global.randOther(0, 1)) canMoveRight = false; else canMoveLeft = false;
1736 if (canMoveRight) FatalError("WATERCHECK: WTF RIGHT");
1737 //!writeln(wtile.objId, ": MOVING LEFT (", curWaterTileLastHDir, ")");
1738 curWaterTileLastHDir = -1;
1739 wtile.ix = wtile.ix-16;
1740 } else if (canMoveRight) {
1741 if (canMoveLeft) FatalError("WATERCHECK: WTF LEFT");
1742 //!writeln(wtile.objId, ": MOVING RIGHT (", curWaterTileLastHDir, ")");
1743 curWaterTileLastHDir = 1;
1744 wtile.ix = wtile.ix+16;
1752 wtile.setSprite(wtile.lava ? 'sLava' : 'sWater');
1753 wtile.waterMoved = true;
1754 // if this tile was not moved down, check if it can move down on any next step
1755 if (!wtile.waterMovedDown) {
1756 if (waterCanReachGroundHoleInDir(wtile, -1)) wtile.waterMovedDown = true;
1757 else if (waterCanReachGroundHoleInDir(wtile, 1)) wtile.waterMovedDown = true;
1761 return false; // don't remove
1763 //if (!isWetTileAtPix(tileX*16+8, tileY*16-8)) wtile.setSprite(wtile.lava ? 'sLavaTop' : 'sWaterTop');
1767 transient array!MapTile waterTilesList;
1769 final bool sortWaterTilesByCoordsLess (MapTile a, MapTile b) {
1771 if (dy) return (dy < 0);
1772 return (a.ix < b.ix);
1775 transient int waterFlowPause = 0;
1776 transient bool debugWaterFlowPause = false;
1778 final void cleanDeadObjects () {
1779 // remove dead objects
1780 if (deadItemsHead) {
1781 auto olddel = ImmediateDelete;
1782 ImmediateDelete = false;
1784 auto it = deadItemsHead;
1785 deadItemsHead = it.deadItemsNext;
1786 if (it.grid) it.grid.remove(it.gridId);
1789 } while (deadItemsHead);
1790 ImmediateDelete = olddel;
1791 if (olddel) CollectGarbage(true); // destroy delayed objects too
1795 final void cleanDeadTiles () {
1796 if (checkWater && /*global.lake == 1 ||*/ (!blockWaterChecking && liquidTileCount)) {
1797 if (global.lake == 1) fillGreatLake();
1798 if (waterFlowPause > 1) {
1803 if (debugWaterFlowPause) waterFlowPause = 4;
1804 //writeln("checking water");
1805 waterTilesList.clear();
1806 foreach (MapTile wtile; objGrid.allObjectsSafe(MapTile)) {
1807 if (wtile.water || wtile.lava) {
1809 if (wtile.ix%16 == 0 && wtile.iy%16 == 0) {
1810 wtile.waterMoved = false;
1811 wtile.waterMovedDown = false;
1812 wtile.waterSlideOldX = wtile.ix;
1813 wtile.waterSlideOldY = wtile.iy;
1814 waterTilesList[$] = wtile;
1819 liquidTileCount = 0;
1820 waterTilesList.sort(&sortWaterTilesByCoordsLess);
1822 bool wasAnyMove = false;
1823 bool wasAnyMoveDown = false;
1824 foreach (MapTile wtile; waterTilesList) {
1825 if (!wtile || !wtile.isInstanceAlive) continue;
1826 auto killIt = checkWaterFlow(wtile);
1830 wtile.instanceRemove(); // just in case
1832 wtile.saveInterpData();
1834 wasAnyMove = wasAnyMove || wtile.waterMoved;
1835 wasAnyMoveDown = wasAnyMoveDown || wtile.waterMovedDown;
1836 if (wtile.waterMoved && debugWaterFlowPause) wtile.waterSlideCounter = 4;
1840 liquidTileCount = 0;
1841 foreach (MapTile wtile; waterTilesList) {
1842 if (!wtile || !wtile.isInstanceAlive) continue;
1843 if (wasAnyMoveDown) {
1847 //checkWater = checkWater || wtile.waterMoved;
1848 curWaterTile = wtile;
1849 int tileX = wtile.ix/16, tileY = wtile.iy/16;
1850 // check if we are have no way to leak
1851 bool killIt = false;
1852 if (!isFullyOccupiedAtTilePos(tileX-1, tileY) || (wtile.water && curWaterTileCheckHitsLava)) {
1853 //writeln(" LEFT DEATH (", curWaterOccupiedCount, " : ", curWaterTileCheckHitsLava, ")");
1856 if (!killIt && (!isFullyOccupiedAtTilePos(tileX+1, tileY) || (wtile.water && curWaterTileCheckHitsLava))) {
1857 //writeln(" RIGHT DEATH (", curWaterOccupiedCount, " : ", curWaterTileCheckHitsLava, ")");
1860 if (!killIt && (!isFullyOccupiedAtTilePos(tileX, tileY+1) || (wtile.water && curWaterTileCheckHitsLava))) {
1861 //writeln(" DOWN DEATH (", curWaterOccupiedCount, " : ", curWaterTileCheckHitsLava, ")");
1868 wtile.instanceRemove(); // just in case
1873 if (wasAnyMove) checkWater = true;
1874 //writeln("water check: liquidTileCount=", liquidTileCount, "; checkWater=", checkWater, "; wasAnyMove=", wasAnyMove, "; wasAnyMoveDown=", wasAnyMoveDown);
1876 // fill empty spaces in lake with water
1884 // ////////////////////////////////////////////////////////////////////////// //
1885 private transient array!MapEntity postponedThinkers;
1886 private transient MapEntity thinkerHeld;
1887 private transient array!MapEntity activeThinkerList;
1890 final void doThinkActionsForObject (MapEntity o) {
1891 if (o.justSpawned) o.justSpawned = false;
1892 else if (o.imageSpeed > 0) o.nextAnimFrame();
1895 if (o.isInstanceAlive) {
1898 if (o.isInstanceAlive) {
1899 if (o.whipTimer > 0) --o.whipTimer;
1901 auto obj = MapObject(o);
1902 if (!o.canLiveOutsideOfLevel && (!obj || !obj.heldBy) && o.isOutsideOfLevel()) {
1903 // oops, fallen out of level...
1911 // return `true` if thinker should be removed
1912 final void thinkOne (MapEntity o, optional bool doHeldObject, optional bool dontAddHeldObject) {
1914 if (o == thinkerHeld && !doHeldObject) return; // skip it
1916 if (!o.active || !o.isInstanceAlive) return;
1918 auto obj = MapObject(o);
1920 if (obj && obj.heldBy == player) {
1921 // fix held item coords
1922 obj.fixHoldCoords();
1924 doThinkActionsForObject(o);
1926 if (!dontAddHeldObject) {
1928 foreach (MapEntity e; postponedThinkers) if (e == o) { found = true; break; }
1929 if (!found) postponedThinkers[$] = o;
1935 bool doThink = true;
1937 // collision with player weapon
1938 auto hh = PlayerWeapon(player.holdItem);
1939 bool doWeaponAction = false;
1941 if (hh.blockedBySolids && !global.config.killEnemiesThruWalls) {
1942 int xx = player.ix+(player.dir == MapObject::Dir.Left ? -16 : 16);
1943 //doWeaponAction = !isSolidAtPoint(xx, player.iy);
1944 doWeaponAction = !isSolidAtPoint(xx, hh.yCenter);
1946 int dh = max(1, hh.height-2);
1947 doWeaponAction = !checkTilesInRect(player.ix, player.iy);
1950 doWeaponAction = true;
1954 if (obj && doWeaponAction && hh && (o.whipTimer <= 0 || hh.ignoreWhipTimer) && hh.collidesWithObject(obj)) {
1955 //writeln("WEAPONED!");
1956 bool dontChangeWhipTimer = hh.dontChangeWhipTimer;
1957 if (!o.onTouchedByPlayerWeapon(player, hh)) {
1958 if (o.isInstanceAlive) hh.onCollisionWithObject(obj);
1960 if (!dontChangeWhipTimer) o.whipTimer = o.whipTimerValue; //HACK
1961 doThink = o.isInstanceAlive;
1964 if (doThink && o.isInstanceAlive) {
1965 doThinkActionsForObject(o);
1966 doThink = o.isInstanceAlive;
1969 // collision with player
1970 if (doThink && obj && o.collidesWith(player)) {
1971 if (!player.onObjectTouched(obj) && o.isInstanceAlive) {
1972 doThink = !o.onTouchedByPlayer(player);
1979 final void processThinkers (float timeDelta) {
1980 if (timeDelta <= 0) return;
1983 if (onBeforeFrame) onBeforeFrame(false);
1984 if (onAfterFrame) onAfterFrame(false);
1990 accumTime += timeDelta;
1991 bool wasFrame = false;
1993 auto olddel = ImmediateDelete;
1994 ImmediateDelete = false;
1995 while (accumTime >= FrameTime) {
1996 postponedThinkers.clear();
1998 accumTime -= FrameTime;
1999 if (onBeforeFrame) onBeforeFrame(accumTime >= FrameTime);
2001 if (shakeLeft > 0) {
2003 if (!shakeDir.x) shakeDir.x = (global.randOther(0, 1) ? -3 : 3);
2004 if (!shakeDir.y) shakeDir.y = (global.randOther(0, 1) ? -3 : 3);
2005 shakeOfs.x = shakeDir.x;
2006 shakeOfs.y = shakeDir.y;
2007 int sgnc = global.randOther(1, 3);
2008 if (sgnc&0x01) shakeDir.x = -shakeDir.x;
2009 if (sgnc&0x02) shakeDir.y = -shakeDir.y;
2018 // we don't want the time to grow too large
2019 if (time < 0) { time = 0; lastRenderTime = -1; }
2020 // game-global events
2022 // frame thinkers: player
2023 if (player && !disablePlayerThink) {
2025 if (!player.dead && isNormalLevel() &&
2026 (maxPlayingTime < 0 ||
2027 (maxPlayingTime > 0 && maxPlayingTime*30 <= time &&
2028 time%30 == 0 && global.randOther(1, 100) <= 20)))
2030 MakeMapObject(player.ix, player.iy, 'oExplosion');
2032 //HACK: check for stolen items
2033 auto item = MapItem(player.holdItem);
2034 if (item) item.onCheckItemStolen(player);
2035 item = MapItem(player.pickedItem);
2036 if (item) item.onCheckItemStolen(player);
2038 doThinkActionsForObject(player);
2040 // frame thinkers: held object
2041 thinkerHeld = player.holdItem;
2042 if (thinkerHeld && thinkerHeld.isInstanceAlive) {
2043 thinkOne(thinkerHeld, doHeldObject:true);
2044 if (!thinkerHeld.isInstanceAlive) {
2045 if (player.holdItem == thinkerHeld) player.holdItem = none;
2046 thinkerHeld.grid.remove(thinkerHeld.gridId);
2048 thinkerHeld.onDestroy();
2053 // frame thinkers: objects
2054 activeThinkerList.clear();
2055 auto grid = objGrid;
2056 // collect active objects
2057 if (global.config.useFrozenRegion) {
2058 foreach (MapEntity e; grid.inRectPix(viewStart.x/global.scale-64, viewStart.y/global.scale-64, 320+64*2, 240+64*2, precise:false)) {
2059 if (e.active) activeThinkerList[$] = e;
2063 foreach (MapEntity e; grid.allObjects()) {
2064 if (e.active) activeThinkerList[$] = e;
2067 // process active objects
2068 //writeln("thinkers: ", activeThinkerList.length);
2069 foreach (MapEntity o; activeThinkerList) {
2071 thinkOne(o, doHeldObject:false);
2072 if (!o.isInstanceAlive) {
2073 //writeln("dead thinker: '", o.objType, "'");
2074 if (o.grid) o.grid.remove(o.gridId);
2075 auto obj = MapObject(o);
2076 if (obj && obj.heldBy) obj.heldBy.holdItem = none;
2083 // postponed thinkers
2084 foreach (MapEntity o; postponedThinkers) {
2086 thinkOne(o, doHeldObject:true, dontAddHeldObject:true);
2087 if (!o.isInstanceAlive) {
2088 //writeln("dead pp-thinker: '", o.objType, "'");
2095 postponedThinkers.clear();
2097 // clean dead things
2099 // fix held item coords
2100 if (player && player.holdItem) {
2101 if (player.holdItem.isInstanceAlive) {
2102 player.holdItem.fixHoldCoords();
2104 player.holdItem = none;
2108 if (collectCounter == 0) {
2109 xmoney = max(0, xmoney-100);
2115 if (!player.dead) stats.oneMoreFramePlayed();
2116 SoundSystem.ListenerOrigin = vector(player.xCenter, player.yCenter, -1);
2117 //writeln("plrpos=(", player.xCenter, ",", player.yCenter, "); lo=", SoundSystem.ListenerOrigin);
2119 if (onAfterFrame) onAfterFrame(accumTime >= FrameTime);
2120 ++framesProcessedFromLastClear;
2123 if (!player.visible && player.holdItem) player.holdItem.visible = false;
2124 if (winCutsceneSwitchToNext) {
2125 winCutsceneSwitchToNext = false;
2126 switch (++inWinCutscene) {
2127 case 2: startWinCutsceneVolcano(); break;
2128 case 3: default: startWinCutsceneWinFall(); break;
2132 if (playerExited) break;
2134 ImmediateDelete = olddel;
2136 playerExited = false;
2138 centerViewAtPlayer();
2141 // if we were processed at least one frame, collect garbage
2143 CollectGarbage(true); // destroy delayed objects too
2145 if (accumTime > 0 && onInterFrame) onInterFrame(accumTime/FrameTime);
2149 // ////////////////////////////////////////////////////////////////////////// //
2150 final void tileXY2roomXY (int tileX, int tileY, out int roomX, out int roomY) {
2151 roomX = (tileX-1)/RoomGen::Width;
2152 roomY = (tileY-1)/RoomGen::Height;
2156 final bool isInShop (int tileX, int tileY) {
2157 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
2158 auto n = roomType[tileX, tileY];
2159 if (n == 4 || n == 5) return true;
2160 return !!checkTilesInRect(tileX*16, tileY*16, 16, 16, delegate bool (MapTile t) { return t.shopWall; });
2161 //k8: we don't have this
2162 //if (t && t.objType == 'oShop') return true;
2168 // ////////////////////////////////////////////////////////////////////////// //
2169 override void Destroy () {
2171 delete tempSolidTile;
2176 // ////////////////////////////////////////////////////////////////////////// //
2177 // WARNING! delegate should not create/delete objects!
2178 final MapObject findNearestObject (int px, int py, scope bool delegate (MapObject o) dg, optional class!MapObject castClass) {
2179 MapObject res = none;
2180 if (!castClass) castClass = MapObject;
2181 int curdistsq = int.max;
2182 foreach (MapObject o; objGrid.allObjects(MapObject)) {
2183 if (o.spectral) continue;
2184 if (!dg(o)) continue;
2185 int xc = px-o.xCenter, yc = py-o.yCenter;
2186 int distsq = xc*xc+yc*yc;
2187 if (distsq < curdistsq) {
2196 // WARNING! delegate should not create/delete objects!
2197 final MapObject findNearestEnemy (int px, int py, optional scope bool delegate (MapEnemy o) dg, optional class!MapEnemy castClass) {
2198 if (!castClass) castClass = MapEnemy;
2199 if (castClass !isa MapEnemy) return none;
2200 MapObject res = none;
2201 int curdistsq = int.max;
2202 foreach (MapEnemy o; objGrid.allObjects(castClass)) {
2203 //k8: i added `dead` check
2204 if (o.spectral || o.dead) continue;
2206 if (!dg(o)) continue;
2208 int xc = px-o.xCenter, yc = py-o.yCenter;
2209 int distsq = xc*xc+yc*yc;
2210 if (distsq < curdistsq) {
2219 final MonsterShopkeeper findNearestCalmShopkeeper (int px, int py) {
2220 auto obj = MonsterShopkeeper(findNearestEnemy(px, py, delegate bool (MapEnemy o) {
2221 auto sk = MonsterShopkeeper(o);
2222 if (sk && !sk.angered) return true;
2224 }, castClass:MonsterShopkeeper));
2229 final MonsterShopkeeper hasAliveShopkeepers (optional bool skipAngry) {
2230 foreach (MonsterShopkeeper sc; objGrid.allObjects(MonsterShopkeeper)) {
2231 if (sc.spectral || sc.dead) continue;
2232 if (skipAngry && (sc.angered || sc.outlaw)) continue;
2239 // WARNING! delegate should not create/delete objects!
2240 final int calcNearestEnemyDist (int px, int py, optional scope bool delegate (MapEnemy o) dg, optional class!MapEnemy castClass) {
2241 auto e = findNearestEnemy(px, py, dg!optional, castClass!optional);
2242 if (!e) return int.max;
2243 int xc = px-e.xCenter, yc = py-e.yCenter;
2244 return round(sqrt(xc*xc+yc*yc));
2248 // WARNING! delegate should not create/delete objects!
2249 final int calcNearestObjectDist (int px, int py, optional scope bool delegate (MapObject o) dg, optional class!MapObject castClass) {
2250 auto e = findNearestObject(px, py, dg!optional, castClass!optional);
2251 if (!e) return int.max;
2252 int xc = px-e.xCenter, yc = py-e.yCenter;
2253 return round(sqrt(xc*xc+yc*yc));
2257 // WARNING! delegate should not create/delete objects!
2258 final MapTile findNearestMoveableSolid (int px, int py, optional scope bool delegate (MapTile t) dg) {
2260 int curdistsq = int.max;
2261 foreach (MapTile t; objGrid.allObjects(MapTile)) {
2262 if (t.spectral) continue;
2264 if (!dg(t)) continue;
2266 if (!t.solid || !t.moveable) continue;
2268 int xc = px-t.xCenter, yc = py-t.yCenter;
2269 int distsq = xc*xc+yc*yc;
2270 if (distsq < curdistsq) {
2279 // WARNING! delegate should not create/delete objects!
2280 final MapTile findNearestTile (int px, int py, optional scope bool delegate (MapTile t) dg) {
2281 if (!dg) return none;
2283 int curdistsq = int.max;
2285 //FIXME: make this faster!
2286 foreach (MapTile t; objGrid.allObjects(MapTile)) {
2287 if (t.spectral) continue;
2288 int xc = px-t.xCenter, yc = py-t.yCenter;
2289 int distsq = xc*xc+yc*yc;
2290 if (distsq < curdistsq && dg(t)) {
2300 // ////////////////////////////////////////////////////////////////////////// //
2301 final bool cbIsObjectWeb (MapObject o) { return (o isa ItemWeb); }
2302 final bool cbIsObjectBlob (MapObject o) { return (o isa EnemyBlob); }
2303 final bool cbIsObjectArrow (MapObject o) { return (o isa ItemProjectileArrow); }
2304 final bool cbIsObjectEnemy (MapObject o) { return (/*o != self &&*/ o isa MapEnemy); }
2306 final bool cbIsObjectTreasure (MapObject o) { return (o.isTreasure); }
2308 final bool cbIsItemObject (MapObject o) { return (o isa MapItem); }
2310 final bool cbIsItemOrEnemyObject (MapObject o) { return (o isa MapItem || o isa MapEnemy); }
2313 final MapObject isObjectAtTile (int tileX, int tileY, optional scope bool delegate (MapObject o) dg, optional bool precise) {
2314 if (!specified_precise) precise = true;
2317 foreach (MapObject o; objGrid.inRectPix(tileX, tileY, 16, 16, precise:precise, castClass:MapObject)) {
2318 if (o.spectral) continue;
2320 if (dg(o)) return o;
2329 final MapObject isObjectAtTilePix (int x, int y, optional scope bool delegate (MapObject o) dg) {
2330 return isObjectAtTile(x/16, y/16, dg!optional);
2334 final MapObject isObjectAtPoint (int xpos, int ypos, optional scope bool delegate (MapObject o) dg, optional bool precise, optional class!MapObject castClass) {
2335 if (!specified_precise) precise = true;
2336 if (!castClass) castClass = MapObject;
2337 foreach (MapObject o; objGrid.inCellPix(xpos, ypos, precise:precise, castClass:castClass)) {
2338 if (o.spectral) continue;
2340 if (dg(o)) return o;
2342 if (o isa MapEnemy) return o;
2349 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) {
2350 if (w < 1 || h < 1) return none;
2351 if (!castClass) castClass = MapObject;
2352 if (w == 1 && h == 1) return isObjectAtPoint(xpos, ypos, dg!optional, precise!optional, castClass);
2353 if (!specified_precise) precise = true;
2354 foreach (MapObject o; objGrid.inRectPix(xpos, ypos, w, h, precise:precise, castClass:castClass)) {
2355 if (o.spectral) continue;
2357 if (dg(o)) return o;
2359 if (o isa MapEnemy) return o;
2366 final MapObject forEachObject (scope bool delegate (MapObject o) dg, optional bool allowSpectrals, optional class!MapObject castClass) {
2367 if (!dg) return none;
2368 if (!castClass) castClass = MapObject;
2369 foreach (MapObject o; objGrid.allObjectsSafe(castClass)) {
2370 if (!allowSpectrals && o.spectral) continue;
2371 if (dg(o)) return o;
2377 final MapObject forEachObjectAtPoint (int xpos, int ypos, scope bool delegate (MapObject o) dg, optional bool precise) {
2378 if (!dg) return none;
2379 if (!specified_precise) precise = true;
2380 foreach (MapObject o; objGrid.inCellPix(xpos, ypos, precise:precise, castClass:MapObject)) {
2381 if (o.spectral) continue;
2382 if (dg(o)) return o;
2388 final MapObject forEachObjectInRect (int xpos, int ypos, int w, int h, scope bool delegate (MapObject o) dg, optional bool precise) {
2389 if (!dg || w < 1 || h < 1) return none;
2390 if (w == 1 && h == 1) return forEachObjectAtPoint(xpos, ypos, dg!optional, precise!optional);
2391 if (!specified_precise) precise = true;
2392 foreach (MapObject o; objGrid.inRectPix(xpos, ypos, w, h, precise:precise, castClass:MapObject)) {
2393 if (o.spectral) continue;
2394 if (dg(o)) return o;
2400 final MapEntity forEachEntityInRect (int xpos, int ypos, int w, int h, scope bool delegate (MapEntity o) dg, optional bool precise, optional class!MapEntity castClass) {
2401 if (!dg || w < 1 || h < 1) return none;
2402 if (!castClass) castClass = MapEntity;
2403 if (!specified_precise) precise = true;
2404 foreach (MapEntity e; objGrid.inRectPix(xpos, ypos, w, h, precise:precise, castClass:castClass)) {
2405 if (e.spectral) continue;
2406 if (dg(e)) return e;
2412 private final bool cbIsRopeTile (MapTile t) { return (t isa MapTileRope); }
2414 final MapTile isRopeAtPoint (int px, int py) {
2415 return checkTileAtPoint(px, py, &cbIsRopeTile);
2420 final MapTile isWaterSwimAtPoint (int px, int py) {
2421 return isWaterAtPoint(px, py);
2425 // ////////////////////////////////////////////////////////////////////////// //
2426 private array!MapEntity tmpEntityList;
2428 private final bool cbCollectEntitiesWithMask (MapEntity t) {
2429 if (!t.visible || t.spectral) return false;
2430 tmpEntityList[$] = t;
2435 final void touchEntitiesWithMask (int x, int y, SpriteFrame frm, scope bool delegate (MapEntity t) dg, optional class!MapEntity castClass) {
2436 if (!frm || frm.bw < 1 || frm.bh < 1 || !dg) return;
2437 if (frm.isEmptyPixelMask) return;
2438 if (!castClass) castClass = MapEntity;
2440 if (tmpEntityList.length) tmpEntityList.clear();
2441 if (player isa castClass && player.isRectCollisionFrame(frm, x, y)) tmpEntityList[$] = player;
2442 forEachEntityInRect(x+frm.bx, y+frm.by, frm.bw, frm.bh, &cbCollectEntitiesWithMask, castClass:castClass);
2443 foreach (MapEntity e; tmpEntityList) {
2444 if (!e || !e.isInstanceAlive || !e.visible || e.spectral) continue;
2445 if (e.isRectCollisionFrame(frm, x, y)) {
2452 // ////////////////////////////////////////////////////////////////////////// //
2453 final bool cbCollisionAnySolid (MapTile t) { return t.solid; }
2454 final bool cbCollisionAnyMoveableSolid (MapTile t) { return t.solid && t.moveable; }
2455 final bool cbCollisionLiquid (MapTile t) { return (t.water || t.lava); }
2456 final bool cbCollisionAnySolidOrLiquid (MapTile t) { return (t.solid || t.water || t.lava); }
2457 final bool cbCollisionLadder (MapTile t) { return t.ladder; }
2458 final bool cbCollisionLadderTop (MapTile t) { return t.laddertop; }
2459 final bool cbCollisionAnyLadder (MapTile t) { return (t.ladder || t.laddertop); }
2460 final bool cbCollisionAnySolidIce (MapTile t) { return (t.solid && t.ice); }
2461 final bool cbCollisionWater (MapTile t) { return t.water; }
2462 final bool cbCollisionLava (MapTile t) { return t.lava; }
2463 final bool cbCollisionAnyTree (MapTile t) { return t.tree; }
2464 final bool cbCollisionPlatform (MapTile t) { return t.platform; }
2465 final bool cbCollisionLadderOrPlatform (MapTile t) { return (t.ladder || t.platform); }
2466 //final bool cbCollisionAnyHangeable (MapTile t) { return t.hangeable; } // no need to check for solids here, it is done in MapTile initializer
2467 final bool cbCollisionSpringTrap (MapTile t) { return t.springtrap; }
2468 final bool cbCollisionSpikes (MapTile t) { return t.spikes; }
2469 final bool cbCollisionExitTile (MapTile t) { return t.isExitActive(); }
2471 final bool cbCollisionForWhoa (MapTile t) { return (t.solid || t.laddertop); }
2473 //final bool cbCollisionSacAltar (MapTile t) { return (t.objName == 'oSacAltarLeft'); } // only left part, yeah
2474 final bool cbCollisionSacAltar (MapTile t) { return t.sacrificingAltar; }
2477 // ////////////////////////////////////////////////////////////////////////// //
2478 transient MapTileTemp tempSolidTile;
2480 private final MapTileTemp makeWalkeableSolidTile (MapObject o) {
2481 if (!tempSolidTile) {
2482 tempSolidTile = SpawnObject(MapTileTemp);
2483 } else if (!tempSolidTile.isInstanceAlive) {
2484 delete tempSolidTile;
2485 tempSolidTile = SpawnObject(MapTileTemp);
2488 tempSolidTile.level = self;
2489 tempSolidTile.global = global;
2490 tempSolidTile.solid = true;
2491 tempSolidTile.objName = MapTileTemp.default.objName;
2492 tempSolidTile.objType = MapTileTemp.default.objType;
2493 tempSolidTile.e = o;
2494 tempSolidTile.fltx = o.fltx;
2495 tempSolidTile.flty = o.flty;
2496 return tempSolidTile;
2500 final MapTile checkTilesInRect (int x0, int y0, const int w, const int h,
2501 optional scope bool delegate (MapTile dg) dg, optional bool precise,
2502 optional class!MapTile castClass)
2504 if (w < 1 || h < 1) return none;
2505 if (w == 1 && h == 1) return checkTileAtPoint(x0, y0, dg!optional);
2506 int x1 = x0+w-1, y1 = y0+h-1;
2507 if (x0 >= tilesWidth*16 || y0 >= tilesHeight*16 || x1 < 0 || y1 < 0) return none;
2508 if (!specified_precise) precise = true;
2509 if (!castClass) castClass = MapTile;
2510 if (!dg) dg = &cbCollisionAnySolid;
2512 // check walkable solid objects too
2513 foreach (MapEntity e; objGrid.inRectPix(x0, y0, w, h, precise:precise, castClass:castClass)) {
2514 if (e.spectral || !e.visible) continue;
2515 auto t = MapTile(e);
2517 if (dg(t)) return t;
2520 auto o = MapObject(e);
2521 if (o && o.walkableSolid) {
2522 t = makeWalkeableSolidTile(o);
2523 if (dg(t)) return t;
2532 final MapTile checkTileAtPoint (int x0, int y0, optional scope bool delegate (MapTile dg) dg, optional bool precise, optional class!MapTile castClass) {
2533 if (x0 < 0 || y0 < 0 || x0 >= tilesWidth*16 || y0 >= tilesHeight*16) return none;
2534 if (!specified_precise) precise = true;
2535 if (!castClass) castClass = MapTile;
2536 if (!dg) dg = &cbCollisionAnySolid;
2538 // check walkable solid objects
2539 foreach (MapEntity e; objGrid.inCellPix(x0, y0, precise:precise, castClass:castClass)) {
2540 if (e.spectral || !e.visible) continue;
2541 auto t = MapTile(e);
2543 if (dg(t)) return t;
2546 auto o = MapObject(e);
2547 if (o && o.walkableSolid) {
2548 t = makeWalkeableSolidTile(o);
2549 if (dg(t)) return t;
2558 // ////////////////////////////////////////////////////////////////////////// //
2559 final MapTile isSolidAtPoint (int px, int py) { return checkTileAtPoint(px, py); }
2560 final MapTile isSolidInRect (int rx, int ry, int rw, int rh) { return checkTilesInRect(rx, ry, rw, rh); }
2561 final MapTile isLadderAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadder); }
2562 final MapTile isLadderOrPlatformAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadderOrPlatform); }
2563 final MapTile isLadderTopAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadderTop); }
2564 final MapTile isAnyLadderAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyLadder); }
2565 final MapTile isWaterAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionWater); }
2566 final MapTile isLavaAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLava); }
2567 final MapTile isLiquidAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLiquid); }
2568 final MapTile isIceAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnySolidIce); }
2569 final MapTile isTreeAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyTree); }
2570 //final MapTile isHangeableAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyHangeable); }
2573 // ////////////////////////////////////////////////////////////////////////// //
2574 final void setTileTypeAt (int tileX, int tileY, LevelGen::RType rt) {
2575 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) roomType[tileX, tileY] = rt;
2579 //FIXME: make this faster
2580 transient float gtagX, gtagY;
2582 // only non-moveables and non-specials
2583 final MapTile getTileAtGrid (int tileX, int tileY) {
2586 return checkTileAtPoint(tileX*16, tileY*16, delegate bool (MapTile t) {
2587 if (t.spectral || t.moveable || t.toSpecialGrid || !t.visible) return false;
2588 if (t.fltx != gtagX || t.flty != gtagY) return false; // emulate grid
2589 if (t.width != 16 || t.height != 16) return false;
2592 //return (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight ? tiles[tileX, tileY] : none);
2596 final MapTile getTileAtGridAny (int tileX, int tileY) {
2599 return checkTileAtPoint(tileX*16, tileY*16, delegate bool (MapTile t) {
2600 if (t.spectral /*|| t.moveable*/ || !t.visible) return false;
2601 if (t.fltx != gtagX || t.flty != gtagY) return false; // emulate grid
2602 if (t.width != 16 || t.height != 16) return false;
2605 //return (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight ? tiles[tileX, tileY] : none);
2609 final bool isNamedTileAt (int tileX, int tileY, name atypename) {
2610 if (!atypename) return false;
2611 auto t = getTileAtGridAny(tileX, tileY);
2612 return (t && t.objName == atypename);
2616 final void setTileAtGrid (int tileX, int tileY, MapTile tile) {
2617 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
2619 tile.fltx = tileX*16;
2620 tile.flty = tileY*16;
2621 if (!tile.dontReplaceOthers) {
2622 auto osp = tile.spectral;
2623 tile.spectral = true;
2624 auto t = getTileAtGridAny(tileX, tileY);
2625 tile.spectral = osp;
2626 if (t && !t.immuneToReplacement) {
2627 writeln("REPLACING tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
2628 writeln(" NEW tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (tile.toSpecialGrid ? "special " : ""), "grid, pos=(", tile.fltx/16.0, ",", tile.flty/16.0, ")");
2634 auto t = getTileAtGridAny(tileX, tileY);
2635 if (t && !t.immuneToReplacement) {
2636 writeln("REMOVING tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
2644 // ////////////////////////////////////////////////////////////////////////// //
2645 // return `true` from delegate to stop
2646 MapTile forEachSolidTileOnGrid (scope bool delegate (int tileX, int tileY, MapTile t) dg) {
2647 if (!dg) return none;
2648 foreach (MapTile t; objGrid.allObjectsSafe(MapTile)) {
2649 if (t.spectral || !t.solid || !t.visible) continue;
2650 if (t.ix%16 != 0 || t.iy%16 != 0) continue; // emulate grid
2651 if (t.width != 16 || t.height != 16) continue;
2652 if (dg(t.ix/16, t.iy/16, t)) return t;
2658 // ////////////////////////////////////////////////////////////////////////// //
2659 // return `true` from delegate to stop
2660 MapTile forEachTile (scope bool delegate (MapTile t) dg, optional class!MapTile castClass) {
2661 if (!dg) return none;
2662 if (!castClass) castClass = MapTile;
2663 foreach (MapTile t; objGrid.allObjectsSafe(castClass)) {
2664 if (t.spectral || !t.visible) continue;
2665 if (dg(t)) return t;
2671 // ////////////////////////////////////////////////////////////////////////// //
2672 final void fixWallTiles () {
2673 foreach (MapTile t; objGrid.allObjectsSafe(MapTile)) t.beautifyTile();
2677 // ////////////////////////////////////////////////////////////////////////// //
2678 final MapTile isCollisionAtPoint (int px, int py, optional scope bool delegate (MapTile dg) dg) {
2679 if (!dg) dg = &cbCollisionAnySolid;
2680 return checkTilesInRect(px, py, 1, 1, dg);
2684 // ////////////////////////////////////////////////////////////////////////// //
2685 string scrGetKaliGift (MapTile altar, optional name gift) {
2688 // find other side of the altar
2689 int sx = player.ix, sy = player.iy;
2693 auto a2 = isCollisionAtPoint(altar.ix+18, altar.iy+8, &cbCollisionSacAltar);
2694 if (!a2) a2 = isCollisionAtPoint(altar.ix-8, altar.iy+8, &cbCollisionSacAltar);
2695 if (a2) { sx = a2.ix; sy = a2.iy; }
2698 if (global.favor <= -8) res = "SHE SEEMS VERY ANGRY WITH YOU!";
2699 else if (global.favor < 0) res = "SHE SEEMS ANGRY WITH YOU.";
2700 else if (global.favor == 0) res = "SHE HAS FORGIVEN YOU!";
2701 else if (global.favor >= 32) {
2702 if (global.kaliGift >= 3 && global.favor >= 32+(global.kaliGift-2)*16) {
2703 res = "YOU FEEL INVIGORATED!";
2704 global.kaliGift += 1;
2705 global.plife += global.randOther(4, 8);
2706 } else if (global.kaliGift >= 3) {
2707 res = "SHE SEEMS ECSTATIC WITH YOU!";
2708 } else if (global.bombs < 80) {
2709 res = "YOUR SATCHEL FEELS VERY FULL NOW!";
2710 global.kaliGift = 3;
2713 res = "YOU FEEL INVIGORATED!";
2714 global.kaliGift += 1;
2715 global.plife += global.randOther(4, 8);
2717 } else if (global.favor >= 16) {
2718 if (global.kaliGift >= 2) {
2719 res = "SHE SEEMS VERY HAPPY WITH YOU!";
2721 res = "SHE BESTOWS A GIFT UPON YOU!";
2722 global.kaliGift = 2;
2724 auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2727 obj = MakeMapObject(sx, sy-8, 'oPoof');
2732 if (gift) obj = MakeMapObject(sx, sy-8, gift);
2733 if (!obj) obj = MakeMapObject(sx, sy-8, 'oKapala');
2735 } else if (global.favor >= 8) {
2736 if (global.kaliGift >= 1) {
2737 res = "SHE SEEMS HAPPY WITH YOU.";
2739 res = "SHE BESTOWS A GIFT UPON YOU!";
2740 global.kaliGift = 1;
2741 //rAltar = instance_nearest(x, y, oSacAltarRight);
2742 //if (instance_exists(rAltar)) {
2744 auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2747 obj = MakeMapObject(sx, sy-8, 'oPoof');
2751 if (gift) obj = MakeMapObject(sx, sy-8, gift);
2753 auto n = global.randOther(1, 8);
2757 if (n == 1 && !global.hasCape && !global.hasJetpack) aname = 'oCapePickup';
2758 else if (n == 2 && !global.hasGloves) aname = 'oGloves';
2759 else if (n == 3 && !global.hasSpectacles) aname = 'oSpectacles';
2760 else if (n == 4 && !global.hasMitt) aname = 'oMitt';
2761 else if (n == 5 && !global.hasSpringShoes) aname = 'oSpringShoes';
2762 else if (n == 6 && !global.hasSpikeShoes) aname = 'oSpikeShoes';
2763 else if (n == 7 && !global.hasStickyBombs) aname = 'oPaste';
2764 else if (n == 8 && !global.hasCompass) aname = 'oCompass';
2766 obj = MakeMapObject(sx, sy-8, aname);
2772 obj = MakeMapObject(sx, sy-8, (global.hasJetpack ? 'oBombBox' : 'oJetpack'));
2778 } else if (global.favor > 0) {
2779 res = "SHE SEEMS PLEASED WITH YOU.";
2784 global.message = "";
2785 res = "KALI DEVOURS YOU!"; // sacrifice is player
2793 void performSacrifice (MapObject what, MapTile where) {
2794 if (!what || !what.isInstanceAlive) return;
2795 MakeMapObject(what.ix+8, what.iy+8, 'oFlame');
2796 if (where) where.playSound('sndSmallExplode'); else what.playSound('sndSmallExplode');
2797 if (!what.bloodless) what.scrCreateBlood(what.ix+8, what.iy+8, 3);
2799 string msg = "KALI ACCEPTS THE SACRIFICE!";
2801 auto idol = ItemGoldIdol(what);
2803 ++stats.totalSacrifices;
2804 if (global.favor <= -8) msg = "SHE SEEMS VERY ANGRY WITH YOU!";
2805 else if (global.favor < 0) msg = "SHE SEEMS ANGRY WITH YOU.";
2806 else if (global.favor >= 0) {
2807 // find other side of the altar
2808 int sx = player.ix, sy = player.iy;
2813 auto a2 = isCollisionAtPoint(altar.ix+18, altar.iy+8, &cbCollisionSacAltar);
2814 if (!a2) a2 = isCollisionAtPoint(altar.ix-8, altar.iy+8, &cbCollisionSacAltar);
2815 if (a2) { sx = a2.ix; sy = a2.iy; }
2818 auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2821 obj = MakeMapObject(sx, sy-8, 'oPoof');
2825 obj = MakeMapObject(sx, sy-16-8, 'oMonkeyGold');
2827 osdMessage(msg, 6.66);
2829 idol.instanceRemove();
2833 if (global.favor <= -8) {
2834 msg = "KALI DEVOURS THE SACRIFICE!";
2835 } else if (global.favor < 0) {
2836 global.favor += (what.status == MapObject::STUNNED ? what.favor : round(what.favor/2.0)); //k8: added `round()`
2837 if (what.favor > 0) what.favor = 0;
2839 global.favor += (what.status == MapObject::STUNNED ? what.favor : round(what.favor/2.0)); //k8: added `round()`
2843 if (myAltar.gift1 != "" and global.kaliGift == 0) scrGetKaliGift(myAltar.gift1);
2844 else if (myAltar.gift2 != "" and global.kaliGift == 1) scrGetKaliGift(myAltar.gift2);
2845 else scrGetKaliGift("");
2848 // sacrifice is player?
2849 if (what isa PlayerPawn) {
2850 ++stats.totalSelfSacrifices;
2851 msg = "KALI DEVOURS YOU!";
2852 player.visible = false;
2853 player.removeBallAndChain(temp:true);
2855 player.status = MapObject::DEAD;
2857 ++stats.totalSacrifices;
2858 auto msg2 = scrGetKaliGift(where);
2859 what.instanceRemove();
2860 if (msg2) msg = va("%s\n%s", msg, msg2);
2863 osdMessage(msg, 6.66);
2865 //!if (isRealLevel()) global.totalSacrifices += 1;
2867 //!global.messageTimer = 200;
2868 //!global.shake = 10;
2872 instance_create(x, y, oFlame);
2873 playSound(global.sndSmallExplode);
2874 scrCreateBlood(x, y, 3);
2875 global.message = "KALI ACCEPTS YOUR SACRIFICE!";
2876 if (global.favor <= -8) {
2877 global.message = "KALI DEVOURS YOUR SACRIFICE!";
2878 } else if (global.favor < 0) {
2879 if (status == 98) global.favor += favor*1.5; else global.favor += favor;
2880 if (favor > 0) favor = 0;
2882 if (status == 98) global.favor += favor*1.5; else global.favor += favor;
2885 if (myAltar.gift1 != "" and global.kaliGift == 0) scrGetFavorMsg(myAltar.gift1);
2886 else if (myAltar.gift2 != "" and global.kaliGift == 1) scrGetFavorMsg(myAltar.gift2);
2887 else scrGetFavorMsg("");
2889 global.messageTimer = 200;
2896 // ////////////////////////////////////////////////////////////////////////// //
2897 final void addBackgroundGfxDetails () {
2898 // add background details
2899 //if (global.customLevel) return;
2901 // bg = instance_create(16*randRoom(1, 42), 16*randRoom(1, 33), oCaveBG);
2902 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);
2903 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);
2904 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);
2905 else MakeMapBackTile('bgExtras', 32*global.randRoom(0, 1), 0, 32, 32, 16*global.randRoom(1, 42), 16*global.randRoom(1, 33), 10002);
2910 // ////////////////////////////////////////////////////////////////////////// //
2911 private final void fixRealViewStart () {
2912 int scale = global.scale;
2913 realViewStart.x = clamp(realViewStart.x, viewMin.x*scale, viewMax.x*scale-viewWidth);
2914 realViewStart.y = clamp(realViewStart.y, viewMin.y*scale, viewMax.y*scale-viewHeight);
2918 final int cameraCurrX () { return realViewStart.x/global.scale; }
2919 final int cameraCurrY () { return realViewStart.y/global.scale; }
2922 private final void fixViewStart () {
2923 int scale = global.scale;
2924 viewStart.x = clamp(viewStart.x, viewMin.x*scale, viewMax.x*scale-viewWidth);
2925 viewStart.y = clamp(viewStart.y, viewMin.y*scale, viewMax.y*scale-viewHeight);
2929 final void centerViewAtPlayer () {
2930 if (viewWidth < 1 || viewHeight < 1 || !player) return;
2931 centerViewAt(player.xCenter, player.yCenter);
2935 final void centerViewAt (int x, int y) {
2936 if (viewWidth < 1 || viewHeight < 1) return;
2938 cameraSlideToSpeed.x = 0;
2939 cameraSlideToSpeed.y = 0;
2940 cameraSlideToPlayer = 0;
2942 int scale = global.scale;
2945 realViewStart.x = clamp(x-viewWidth/2, 0, tilesWidth*16*scale-viewWidth);
2946 realViewStart.y = clamp(y-viewHeight/2, 0, tilesHeight*16*scale-viewHeight);
2949 viewStart.x = realViewStart.x;
2950 viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
2953 if (onCameraTeleported) onCameraTeleported();
2957 const int ViewPortToleranceX = 16*1+8;
2958 const int ViewPortToleranceY = 16*1+8;
2960 final void fixCamera () {
2961 if (!player) return;
2962 if (viewWidth < 1 || viewHeight < 1) return;
2963 int scale = global.scale;
2964 auto alwaysCenterX = global.config.alwaysCenterPlayer;
2965 auto alwaysCenterY = alwaysCenterX;
2966 // calculate offset from viewport center (in game units), and fix viewport
2968 int camDestX = player.ix+8;
2969 int camDestY = player.iy+8;
2970 if (!cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y) != 0) {
2971 // slide camera to point
2972 if (cameraSlideToSpeed.x) camDestX = cameraSlideToCurr.x;
2973 if (cameraSlideToSpeed.y) camDestY = cameraSlideToCurr.y;
2974 int dx = cameraSlideToDest.x-camDestX;
2975 int dy = cameraSlideToDest.y-camDestY;
2976 //writeln("speed:(", cameraSlideToSpeed.x, ",", cameraSlideToSpeed.y, "); cur:(", camDestX, ",", camDestY, "); dest:(", cameraSlideToDest.x, ",", cameraSlideToDest.y, "); delta=(", dx, ",", dy, ")");
2977 if (dx && cameraSlideToSpeed.x != 0) {
2978 alwaysCenterX = true;
2979 if (abs(dx) <= abs(cameraSlideToSpeed.x)) {
2980 camDestX = cameraSlideToDest.x;
2982 camDestX += sign(dx)*abs(cameraSlideToSpeed.x);
2985 if (dy && abs(cameraSlideToSpeed.y) != 0) {
2986 alwaysCenterY = true;
2987 if (abs(dy) <= cameraSlideToSpeed.y) {
2988 camDestY = cameraSlideToDest.y;
2990 camDestY += sign(dy)*abs(cameraSlideToSpeed.y);
2993 //writeln(" new:(", camDestX, ",", camDestY, ")");
2994 if (cameraSlideToSpeed.x) cameraSlideToCurr.x = camDestX;
2995 if (cameraSlideToSpeed.y) cameraSlideToCurr.y = camDestY;
2999 if (cameraSlideToSpeed.x && !cameraSlideToPlayer) {
3000 realViewStart.x = clamp(camDestX*scale, 0, tilesWidth*16*scale-viewWidth);
3001 } else if (!player.cameraBlockX) {
3002 int x = camDestX*scale;
3003 int cx = realViewStart.x;
3004 if (alwaysCenterX) {
3007 int xofs = x-(cx+viewWidth/2);
3008 if (xofs < -ViewPortToleranceX*scale) cx += xofs+ViewPortToleranceX*scale;
3009 else if (xofs > ViewPortToleranceX*scale) cx += xofs-ViewPortToleranceX*scale;
3011 // slide back to player?
3012 if (cameraSlideToPlayer && cameraSlideToSpeed.x) {
3013 int prevx = cameraSlideToCurr.x*scale;
3014 int dx = (cx-prevx)/scale;
3015 if (abs(dx) <= cameraSlideToSpeed.x) {
3016 writeln("BACKSLIDE X COMPLETE!");
3017 cameraSlideToSpeed.x = 0;
3019 cx = prevx+(sign(dx)*cameraSlideToSpeed.x)*scale;
3020 cameraSlideToCurr.x += sign(dx)*cameraSlideToSpeed.x;
3021 if ((dx < 0 && cx <= 0) || (dx > 0 && cx >= tilesWidth*16*scale-viewWidth)) {
3022 writeln("BACKSLIDE X COMPLETE!");
3023 cameraSlideToSpeed.x = 0;
3027 realViewStart.x = clamp(cx, 0, tilesWidth*16*scale-viewWidth);
3031 if (cameraSlideToSpeed.y && !cameraSlideToPlayer) {
3032 realViewStart.y = clamp(camDestY*scale, 0, tilesHeight*16*scale-viewHeight);
3033 } else if (!player.cameraBlockY) {
3034 int y = camDestY*scale;
3035 int cy = realViewStart.y;
3036 if (alwaysCenterY) {
3037 cy = y-viewHeight/2;
3039 int yofs = y-(cy+viewHeight/2);
3040 if (yofs < -ViewPortToleranceY*scale) cy += yofs+ViewPortToleranceY*scale;
3041 else if (yofs > ViewPortToleranceY*scale) cy += yofs-ViewPortToleranceY*scale;
3043 // slide back to player?
3044 if (cameraSlideToPlayer && cameraSlideToSpeed.y) {
3045 int prevy = cameraSlideToCurr.y*scale;
3046 int dy = (cy-prevy)/scale;
3047 if (abs(dy) <= cameraSlideToSpeed.y) {
3048 writeln("BACKSLIDE Y COMPLETE!");
3049 cameraSlideToSpeed.y = 0;
3051 cy = prevy+(sign(dy)*cameraSlideToSpeed.y)*scale;
3052 cameraSlideToCurr.y += sign(dy)*cameraSlideToSpeed.y;
3053 if ((dy < 0 && cy <= 0) || (dy > 0 && cy >= tilesHeight*16*scale-viewHeight)) {
3054 writeln("BACKSLIDE Y COMPLETE!");
3055 cameraSlideToSpeed.y = 0;
3059 realViewStart.y = clamp(cy, 0, tilesHeight*16*scale-viewHeight);
3062 if (cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y) == 0) cameraSlideToPlayer = 0;
3065 //writeln(" new2:(", cameraCurrX, ",", cameraCurrY, ")");
3067 viewStart.x = realViewStart.x;
3068 viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
3073 // ////////////////////////////////////////////////////////////////////////// //
3074 // x0 and y0 are non-scaled (and will be scaled)
3075 final void drawSpriteAt (name sprName, float frnumf, int x0, int y0, optional bool small) {
3076 if (!sprName) return;
3077 auto spr = sprStore[sprName];
3078 if (!spr || !spr.frames.length) return;
3079 int scale = global.scale;
3082 int frnum = max(0, trunc(frnumf))%spr.frames.length;
3083 auto sfr = spr.frames[frnum];
3084 int sx0 = x0-sfr.xofs*scale;
3085 int sy0 = y0-sfr.yofs*scale;
3086 if (small && scale > 1) {
3087 sfr.tex.blitExt(sx0, sy0, round(sx0+sfr.tex.width*(scale/2.0)), round(sy0+sfr.tex.height*(scale/2.0)), 0, 0);
3089 sfr.tex.blitAt(sx0, sy0, scale);
3094 final void drawSpriteAtS3 (name sprName, float frnumf, int x0, int y0) {
3095 if (!sprName) return;
3096 auto spr = sprStore[sprName];
3097 if (!spr || !spr.frames.length) return;
3100 int frnum = max(0, trunc(frnumf))%spr.frames.length;
3101 auto sfr = spr.frames[frnum];
3102 int sx0 = x0-sfr.xofs*3;
3103 int sy0 = y0-sfr.yofs*3;
3104 sfr.tex.blitAt(sx0, sy0, 3);
3108 // x0 and y0 are non-scaled (and will be scaled)
3109 final void drawTextAt (int x0, int y0, string text, optional int scale, optional int hiColor1, optional int hiColor2) {
3111 if (!specified_scale) scale = global.scale;
3114 sprStore.renderTextWithHighlight(x0, y0, text, scale, hiColor1!optional, hiColor2!optional);
3118 void renderCompass (float currFrameDelta) {
3119 if (!global.hasCompass) return;
3122 if (isRoom("rOlmec")) {
3125 } else if (isRoom("rOlmec2")) {
3131 bool hasMessage = osdHasMessage();
3132 foreach (MapTile et; allExits) {
3134 int exitX = et.ix, exitY = et.iy;
3135 int vx0 = viewStart.x/global.scale, vy0 = viewStart.y/global.scale;
3136 int vx1 = (viewStart.x+viewWidth)/global.scale;
3137 int vy1 = (viewStart.y+viewHeight)/global.scale;
3138 if (exitY > vy1-16) {
3140 drawSpriteAt(hasMessage ? 'sCompassSmallLL' : 'sCompassLL', 0, 0, 224);
3141 } else if (exitX > vx1-16) {
3142 drawSpriteAt(hasMessage ? 'sCompassSmallLR' : 'sCompassLR', 0, 304, 224);
3144 drawSpriteAt(hasMessage ? 'sCompassSmallDown' : 'sCompassDown', 0, exitX-vx0, 224);
3146 } else if (exitX < vx0) {
3147 drawSpriteAt(hasMessage ? 'sCompassSmallLeft' : 'sCompassLeft', 0, 0, exitY-vy0);
3148 } else if (exitX > vx1-16) {
3149 drawSpriteAt(hasMessage ? 'sCompassSmallRight' : 'sCompassRight', 0, 304, exitY-vy0);
3151 break; // only the first exit
3156 final bool totalsNameCmpCB (ref GameStats::TotalItem a, ref GameStats::TotalItem b) {
3157 auto sa = string(a.objName);
3158 auto sb = string(b.objName);
3162 void renderTransitionInfo (float currFrameDelta) {
3165 GameStats.sortTotalsList(stats.kills, &totalsNameCmpCB);
3168 foreach (int idx, ref auto k; stats.kills) {
3169 string s = string(k);
3170 maxLen = max(maxLen, s.length);
3174 sprStore.loadFont('sFontSmall');
3175 Video.color = 0xff_ff_00;
3176 foreach (int idx, ref auto k; stats.kills) {
3178 foreach (int xidx, ref auto d; stats.totalKills) {
3179 if (d.objName == k) { deaths = d.count; break; }
3181 //drawTextAt(16, 4+idx*8, va("%s: %d (%d)", string(k.objName).toUpperCase, k.count, deaths));
3182 drawTextAt(16, 4+idx*8, string(k).toUpperCase);
3183 drawTextAt(16+maxLen, 4+idx*8, va(": %d (%d)", k.count, deaths));
3189 void renderGhostTimer (float currFrameDelta) {
3190 if (ghostTimeLeft <= 0) return;
3191 //ghostTimeLeft /= 30; // frames -> seconds
3193 int hgt = Video.screenHeight-64;
3194 if (hgt < 1) return;
3195 int rhgt = round(float(hgt)*(float(ghostTimeLeft)/30.0)/1000.0);
3196 //writeln("hgt=", hgt, "; rhgt=", rhgt, "; gtl=", ghostTimeLeft/30);
3198 auto oclr = Video.color;
3199 Video.color = 0xcf_ff_7f_00;
3200 Video.fillRect(Video.screenWidth-20, 32, 16, hgt-rhgt);
3201 Video.color = 0x7f_ff_7f_00;
3202 Video.fillRect(Video.screenWidth-20, 32+(hgt-rhgt), 16, rhgt);
3208 void renderStarsHUD (float currFrameDelta) {
3209 bool scumSmallHud = global.config.scumSmallHud;
3211 //auto life = max(0, global.plife);
3212 int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3213 int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3214 //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3219 sprStore.loadFont('sFontSmall');
3222 sprStore.loadFont('sFont');
3226 Video.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3227 //draw_sprite(sHeart, -1, view_xview[0]+8, view_yview[0]+8);
3228 //draw_text(view_xview[0]+24, view_yview[0]+8, life);
3230 if (global.plife == 1) {
3231 drawSpriteAt('sHeartSmallBlink', global.heartBlink, 10, hhup-4);
3232 global.heartBlink += 0.1;
3233 if (global.heartBlink > 3) global.heartBlink = 0;
3235 drawSpriteAt('sHeartSmall', -1, 10, hhup-4);
3236 global.heartBlink = 0;
3239 if (global.plife == 1) {
3240 drawSpriteAt('sHeartBlink', global.heartBlink, 8, hhup);
3241 global.heartBlink += 0.1;
3242 if (global.heartBlink > 3) global.heartBlink = 0;
3244 drawSpriteAt('sHeart', -1, 8, hhup);
3245 global.heartBlink = 0;
3248 int life = clamp(global.plife, 0, 99);
3249 drawTextAt(16+8, hhup, va("%d", life));
3251 //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3252 drawSpriteAt('sShopkeeperIcon', -1, 64, hhup, scumSmallHud);
3253 drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", starsKills));
3255 if (starsRoomTimer1 > 0) {
3256 sprStore.loadFont('sFontSmall');
3257 Video.color = 0xff_ff_00;
3258 int scale = global.scale;
3259 sprStore.renderMultilineTextCentered(Video.screenWidth/2, 216*scale, va("SHOTGUN CHALLENGE BEGINS IN ~%d~", (starsRoomTimer1/30)+1), scale, 0xff_00_00);
3264 void renderSunHUD (float currFrameDelta) {
3265 bool scumSmallHud = global.config.scumSmallHud;
3267 //auto life = max(0, global.plife);
3268 int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3269 int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3270 //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3275 sprStore.loadFont('sFontSmall');
3278 sprStore.loadFont('sFont');
3282 Video.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3283 //draw_sprite(sHeart, -1, view_xview[0]+8, view_yview[0]+8);
3284 //draw_text(view_xview[0]+24, view_yview[0]+8, life);
3286 if (global.plife == 1) {
3287 drawSpriteAt('sHeartSmallBlink', global.heartBlink, 10, hhup-4);
3288 global.heartBlink += 0.1;
3289 if (global.heartBlink > 3) global.heartBlink = 0;
3291 drawSpriteAt('sHeartSmall', -1, 10, hhup-4);
3292 global.heartBlink = 0;
3295 if (global.plife == 1) {
3296 drawSpriteAt('sHeartBlink', global.heartBlink, 8, hhup);
3297 global.heartBlink += 0.1;
3298 if (global.heartBlink > 3) global.heartBlink = 0;
3300 drawSpriteAt('sHeart', -1, 8, hhup);
3301 global.heartBlink = 0;
3304 int life = clamp(global.plife, 0, 99);
3305 drawTextAt(16+8, hhup, va("%d", life));
3307 //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3308 drawSpriteAt('sDamselIcon', -1, 64, hhup, scumSmallHud);
3309 drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", sunScore));
3311 if (sunRoomTimer1 > 0) {
3312 sprStore.loadFont('sFontSmall');
3313 Video.color = 0xff_ff_00;
3314 int scale = global.scale;
3315 sprStore.renderMultilineTextCentered(Video.screenWidth/2, 216*scale, va("DAMSEL CHALLENGE BEGINS IN ~%d~", (sunRoomTimer1/30)+1), scale, 0xff_00_00);
3320 void renderMoonHUD (float currFrameDelta) {
3321 bool scumSmallHud = global.config.scumSmallHud;
3323 //auto life = max(0, global.plife);
3324 int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3325 int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3326 //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3331 sprStore.loadFont('sFontSmall');
3334 sprStore.loadFont('sFont');
3338 Video.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3340 //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3341 drawSpriteAt('sHoopsIcon', -1, 8, hhup, scumSmallHud);
3342 drawTextAt(8+16-(scumSmallHud ? 6 : 0), hhup, va("%d", moonScore));
3343 drawSpriteAt('sTimerIcon', -1, 64, hhup, scumSmallHud);
3344 drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", max(0, moonTimer)));
3346 if (moonRoomTimer1 > 0) {
3347 sprStore.loadFont('sFontSmall');
3348 Video.color = 0xff_ff_00;
3349 int scale = global.scale;
3350 sprStore.renderMultilineTextCentered(Video.screenWidth/2, 216*scale, va("ARCHERY CHALLENGE BEGINS IN ~%d~", (moonRoomTimer1/30)+1), scale, 0xff_00_00);
3355 void renderHUD (float currFrameDelta) {
3356 if (levelKind == LevelKind.Stars) { renderStarsHUD(currFrameDelta); return; }
3357 if (levelKind == LevelKind.Sun) { renderSunHUD(currFrameDelta); return; }
3358 if (levelKind == LevelKind.Moon) { renderMoonHUD(currFrameDelta); return; }
3360 if (!isHUDEnabled()) return;
3362 if (levelKind == LevelKind.Transition) { renderTransitionInfo(currFrameDelta); return; }
3370 bool scumSmallHud = global.config.scumSmallHud;
3371 if (!global.config.optSGAmmo) moneyX = ammoX;
3374 sprStore.loadFont('sFontSmall');
3377 sprStore.loadFont('sFont');
3380 //int alpha = 0x6f_00_00_00;
3381 int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3382 int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3384 //Video.color = 0xff_ff_ff;
3385 Video.color = 0xff_ff_ff|talpha;
3389 if (global.plife == 1) {
3390 drawSpriteAt('sHeartSmallBlink', global.heartBlink, lifeX+2, 4-hhup);
3391 global.heartBlink += 0.1;
3392 if (global.heartBlink > 3) global.heartBlink = 0;
3394 drawSpriteAt('sHeartSmall', -1, lifeX+2, 4-hhup);
3395 global.heartBlink = 0;
3398 if (global.plife == 1) {
3399 drawSpriteAt('sHeartBlink', global.heartBlink, lifeX, 8-hhup);
3400 global.heartBlink += 0.1;
3401 if (global.heartBlink > 3) global.heartBlink = 0;
3403 drawSpriteAt('sHeart', -1, lifeX, 8-hhup);
3404 global.heartBlink = 0;
3408 int life = clamp(global.plife, 0, 99);
3409 //if (!scumHud && life > 99) life = 99;
3410 drawTextAt(lifeX+16, 8-hhup, va("%d", life));
3413 if (global.hasStickyBombs && global.stickyBombsActive) {
3414 if (scumSmallHud) drawSpriteAt('sStickyBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sStickyBombIcon', -1, bombX, 8-hhup);
3416 if (scumSmallHud) drawSpriteAt('sBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sBombIcon', -1, bombX, 8-hhup);
3418 int n = global.bombs;
3419 if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3420 drawTextAt(bombX+16, 8-hhup, va("%d", n));
3423 if (scumSmallHud) drawSpriteAt('sRopeIconSmall', -1, ropeX+2, 4-hhup); else drawSpriteAt('sRopeIcon', -1, ropeX, 8-hhup);
3425 if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3426 drawTextAt(ropeX+16, 8-hhup, va("%d", n));
3429 if (global.config.optSGAmmo && (!player || player.holdItem !isa ItemWeaponBow)) {
3430 if (scumSmallHud) drawSpriteAt('sShellsIcon', -1, ammoX+6, 8-hhup); else drawSpriteAt('sShellsIcon', -1, ammoX+7, 12-hhup);
3432 if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3433 drawTextAt(ammoX+16, 8-hhup, va("%d", n));
3434 } else if (player && player.holdItem isa ItemWeaponBow) {
3435 if (scumSmallHud) drawSpriteAt('sArrowRight', -1, ammoX+6, 8-hhup); else drawSpriteAt('sArrowRight', -1, ammoX+7, 12-hhup);
3437 if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3438 drawTextAt(ammoX+16, 8-hhup, va("%d", n));
3442 if (scumSmallHud) drawSpriteAt('sDollarSignSmall', -1, moneyX+2, 3-hhup); else drawSpriteAt('sDollarSign', -1, moneyX, 8-hhup);
3443 drawTextAt(moneyX+16, 8-hhup, va("%d", stats.money-xmoney));
3446 Video.color = 0xff_ff_ff|ialpha;
3448 int ity = (scumSmallHud ? 18-hhup : 24-hhup);
3451 if (global.hasUdjatEye) {
3452 if (global.udjatBlink) drawSpriteAt('sUdjatEyeIcon2', -1, n, ity); else drawSpriteAt('sUdjatEyeIcon', -1, n, ity);
3455 if (global.hasAnkh) { drawSpriteAt('sAnkhIcon', -1, n, ity); n += 20; }
3456 if (global.hasCrown) { drawSpriteAt('sCrownIcon', -1, n, ity); n += 20; }
3457 if (global.hasKapala) {
3458 if (global.bloodLevel == 0) drawSpriteAt('sKapalaIcon', 0, n, ity);
3459 else if (global.bloodLevel <= 2) drawSpriteAt('sKapalaIcon', 1, n, ity);
3460 else if (global.bloodLevel <= 4) drawSpriteAt('sKapalaIcon', 2, n, ity);
3461 else if (global.bloodLevel <= 6) drawSpriteAt('sKapalaIcon', 3, n, ity);
3462 else if (global.bloodLevel <= 8) drawSpriteAt('sKapalaIcon', 4, n, ity);
3465 if (global.hasSpectacles) { drawSpriteAt('sSpectaclesIcon', -1, n, ity); n += 20; }
3466 if (global.hasGloves) { drawSpriteAt('sGlovesIcon', -1, n, ity); n += 20; }
3467 if (global.hasMitt) { drawSpriteAt('sMittIcon', -1, n, ity); n += 20; }
3468 if (global.hasSpringShoes) { drawSpriteAt('sSpringShoesIcon', -1, n, ity); n += 20; }
3469 if (global.hasJordans) { drawSpriteAt('sJordansIcon', -1, n, ity); n += 20; }
3470 if (global.hasSpikeShoes) { drawSpriteAt('sSpikeShoesIcon', -1, n, ity); n += 20; }
3471 if (global.hasCape) { drawSpriteAt('sCapeIcon', -1, n, ity); n += 20; }
3472 if (global.hasJetpack) { drawSpriteAt('sJetpackIcon', -1, n, ity); n += 20; }
3473 if (global.hasStickyBombs) { drawSpriteAt('sPasteIcon', -1, n, ity); n += 20; }
3474 if (global.hasCompass) { drawSpriteAt('sCompassIcon', -1, n, ity); n += 20; }
3475 if (global.hasParachute) { drawSpriteAt('sParachuteIcon', -1, n, ity); n += 20; }
3477 if (player.holdItem && player.holdItem.objName == 'Bow' && global.arrows > 0) {
3480 while (m <= global.arrows && m <= 20 && malpha > 0) {
3481 Video.color = trunc(malpha*255)<<24|0xff_ff_ff;
3482 drawSpriteAt('sArrowIcon', -1, n, ity);
3484 if (m > 10) malpha -= 0.1; //and global.arrows > 20 malpha -= 0.1
3490 sprStore.loadFont('sFontSmall');
3491 Video.color = 0xff_ff_00|talpha;
3492 if (scumSmallHud) drawTextAt(moneyX+8, 8+8-hhup, va("+%d", xmoney));
3493 else drawTextAt(moneyX, 8+16-hhup, va("+%d", xmoney));
3496 Video.color = 0xff_ff_ff;
3497 if (global.config.ghostShowTime) renderGhostTimer(currFrameDelta);
3501 // ////////////////////////////////////////////////////////////////////////// //
3502 // x0 and y0 are non-scaled (and will be scaled)
3503 final void drawTextAtS3 (int x0, int y0, string text, optional int hiColor1, optional int hiColor2) {
3507 sprStore.renderTextWithHighlight(x0, y0, text, 3, hiColor1!optional, hiColor2!optional);
3511 final void drawTextAtS3Centered (int y0, string text, optional int hiColor1, optional int hiColor2) {
3513 int x0 = (Video.screenWidth-sprStore.getTextWidth(text, 3, specified_hiColor1, specified_hiColor2))/2;
3514 sprStore.renderTextWithHighlight(x0, y0*3, text, 3, hiColor1!optional, hiColor2!optional);
3518 void renderHelpOverlay () {
3520 Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
3523 int txoff = 0; // text x pos offset (for multi-color lines)
3525 if (gameHelpScreen) {
3526 sprStore.loadFont('sFontSmall');
3527 Video.color = 0xff_ff_ff;
3528 drawTextAtS3Centered(ty, va("HELP (PAGE ~%d~ OF ~%d~)", gameHelpScreen, MaxGameHelpScreen), 0xff_ff_00);
3532 if (gameHelpScreen == 1) {
3533 sprStore.loadFont('sFontSmall');
3534 Video.color = 0xff_ff_00; drawTextAtS3(tx, ty, "INVENTORY BASICS"); ty += 16;
3535 Video.color = 0xff_ff_ff;
3536 drawTextAtS3(tx, ty, global.expandString("Press $SWITCH to cycle through items."), 0x00_ff_00);
3539 Video.color = 0xff_ff_ff;
3540 drawSpriteAtS3('sHelpSprite1', -1, 64, 96);
3541 } else if (gameHelpScreen == 2) {
3542 sprStore.loadFont('sFontSmall');
3543 Video.color = 0xff_ff_00;
3544 drawTextAtS3(tx, ty, "SELLING TO SHOPKEEPERS"); ty += 16;
3545 Video.color = 0xff_ff_ff;
3546 drawTextAtS3(tx, ty, global.expandString("Press $PAY to offer your currently"), 0x00_ff_00); ty += 8;
3547 drawTextAtS3(tx, ty, "held item to the shopkeeper."); ty += 16;
3548 drawTextAtS3(tx, ty, "If the shopkeeper is interested, "); ty += 8;
3549 //drawTextAtS3(tx, ty, global.expandString("press $PAY again to complete the sale."), 0x00_ff_00); ty += 72;
3550 drawTextAtS3(tx, ty, global.expandString("press $PAY again to complete"), 0x00_ff_00);
3551 drawTextAtS3(tx, ty+8, "the sale.");
3553 drawSpriteAtS3('sHelpSell', -1, 112, 100);
3554 drawTextAtS3(tx, ty, "Purchasing goods from the shopkeeper"); ty += 8;
3555 drawTextAtS3(tx, ty, "will increase the funds he has"); ty += 8;
3556 drawTextAtS3(tx, ty, "available to buy your unwanted stuff."); ty += 8;
3559 sprStore.loadFont('sFont');
3560 Video.color = 0xff_ff_ff;
3561 drawTextAtS3(136, 8, "MAP");
3563 Video.color = 0xff_ff_00;
3564 drawTextAtS3Centered(24, lg.mapTitle);
3567 auto spf = sprStore[lg.mapSprite].frames[0];
3568 int mapX = 160-spf.width/2;
3569 int mapY = 120-spf.height/2;
3570 //mapTitleX = 160-string_length(global.mapTitle)*8/2;
3572 Video.color = 0xff_ff_ff;
3573 drawSpriteAtS3(lg.mapSprite, -1, mapX, mapY);
3575 if (lg.mapSprite != 'sMapDefault') {
3576 int mx = -1, my = -1;
3578 // set position of player icon
3579 switch (global.currLevel) {
3580 case 1: mx = 81; my = 22; break;
3581 case 2: mx = 113; my = 63; break;
3582 case 3: mx = 197; my = 86; break;
3583 case 4: mx = 133; my = 109; break;
3584 case 5: mx = 181; my = 22; break;
3585 case 6: mx = 126; my = 64; break;
3586 case 7: mx = 158; my = 112; break;
3587 case 8: mx = 66; my = 80; break;
3588 case 9: mx = 30; my = 26; break;
3589 case 10: mx = 88; my = 54; break;
3590 case 11: mx = 148; my = 81; break;
3591 case 12: mx = 210; my = 205; break;
3592 case 13: mx = 66; my = 17; break;
3593 case 14: mx = 146; my = 17; break;
3594 case 15: mx = 82; my = 77; break;
3595 case 16: mx = 178; my = 81; break;
3599 int plrx = mx+player.ix/16;
3600 int plry = my+player.iy/16;
3601 name plrspr = 'sMapSpelunker';
3602 if (global.isDamsel) plrspr = 'sMapDamsel';
3603 else if (global.isTunnelMan) plrspr = 'sMapTunnel';
3604 auto ss = sprStore[plrspr];
3605 drawSpriteAtS3(plrspr, (pausedTime/2)%ss.frames.length, mapX+plrx, mapY+plry);
3607 if (global.hasCompass && allExits.length) {
3608 drawSpriteAtS3('sMapRedDot', -1, mapX+mx+allExits[0].ix/16, mapY+my+allExits[0].iy/16);
3615 sprStore.loadFont('sFontSmall');
3616 Video.color = 0xff_ff_00;
3617 drawTextAtS3Centered(232, "PRESS ~SPACE~/~LEFT~/~RIGHT~ TO CHANGE PAGE", 0x00_ff_00);
3619 Video.color = 0xff_ff_ff;
3623 void renderPauseOverlay () {
3624 //drawTextAt(256, 432, "PAUSED", scale);
3626 if (gameShowHelp) { renderHelpOverlay(); return; }
3628 Video.color = 0xff_ff_00;
3629 //int hiColor = 0x00_ff_00;
3632 if (isTutorialRoom()) {
3633 sprStore.loadFont('sFont');
3634 drawTextAtS3(40, n-24, "TUTORIAL CAVE");
3635 } else if (isNormalLevel()) {
3636 sprStore.loadFont('sFont');
3638 drawTextAtS3Centered(n-32, va("LEVEL ~%d~", global.currLevel), 0x00_ff_00);
3640 sprStore.loadFont('sFontSmall');
3642 int depth = round((174.8*(global.currLevel-1)+(player.iy+8)*0.34)*(global.config.scumMetric ? 0.3048 : 1.0)*10);
3643 string depthStr = va("DEPTH: ~%d.%d~ %s", depth/10, depth%10, (global.config.scumMetric ? "METRES" : "FEET"));
3644 drawTextAtS3Centered(n-16, depthStr, 0x00_ff_00);
3647 drawTextAtS3Centered(n, va("MONEY: ~%d~", stats.money), 0x00_ff_00);
3648 drawTextAtS3Centered(n+16, va("KILLS: ~%d~", stats.countKills), 0x00_ff_00);
3649 drawTextAtS3Centered(n+32, va("SAVES: ~%d~", stats.damselsSaved), 0x00_ff_00);
3650 drawTextAtS3Centered(n+48, va("TIME: ~%s~", time2str(time/30)), 0x00_ff_00);
3651 drawTextAtS3Centered(n+64, va("LEVEL TIME: ~%s~", time2str((time-levelStartTime)/30)), 0x00_ff_00);
3654 sprStore.loadFont('sFontSmall');
3655 Video.color = 0xff_ff_ff;
3656 drawTextAtS3Centered(240-2-8, "~ESC~-RETURN ~F10~-QUIT ~CTRL+DEL~-SUICIDE", 0xff_7f_00);
3657 drawTextAtS3Centered(2, "~O~PTIONS REDEFINE ~K~EYS ~S~TATISTICS", 0xff_7f_00);
3661 // ////////////////////////////////////////////////////////////////////////// //
3662 transient int drawLoot;
3663 transient int drawPosX, drawPosY;
3665 void resetTransitionOverlay () {
3672 // current game, uncollapsed
3673 struct LevelStatInfo {
3675 // for transition screen
3682 void thinkFrameTransition () {
3683 if (drawLoot == 0) {
3684 if (drawPosX > 272) {
3687 if (drawPosY > 83+4) drawPosY = 83;
3689 } else if (drawPosX > 232) {
3692 if (drawPosY > 91+4) drawPosY = 91;
3697 void renderTransitionOverlay () {
3698 sprStore.loadFont('sFontSmall');
3699 Video.color = 0xff_ff_00;
3700 //else if (global.currLevel-1 < 1) draw_text(32, 48, "TUTORIAL CAVE COMPLETED!");
3701 //else draw_text(32, 48, "LEVEL "+string(global.currLevel-1)+" COMPLETED!");
3702 drawTextAt(32, 48, va("LEVEL ~%d~ COMPLETED!", global.currLevel), hiColor1:0x00_ff_ff);
3703 Video.color = 0xff_ff_ff;
3704 drawTextAt(32, 64, va("TIME = ~%s~", time2str((levelEndTime-levelStartTime)/30)), hiColor1:0xff_ff_00);
3706 if (/*stats.collected.length == 0*/stats.money <= levelMoneyStart) {
3707 drawTextAt(32, 80, "LOOT = ~NONE~", hiColor1:0xff_00_00);
3709 drawTextAt(32, 80, va("LOOT = ~%d~", stats.money-levelMoneyStart), hiColor1:0xff_ff_00);
3712 if (stats.kills.length == 0) {
3713 drawTextAt(32, 96, "KILLS = ~NONE~", hiColor1:0x00_ff_00);
3715 drawTextAt(32, 96, va("KILLS = ~%d~", stats.kills.length), hiColor1:0xff_ff_00);
3718 drawTextAt(32, 112, va("MONEY = ~%d~", stats.money), hiColor1:0xff_ff_00);
3722 // ////////////////////////////////////////////////////////////////////////// //
3723 private transient array!MapEntity renderVisibleCids;
3724 private transient array!MapEntity renderVisibleLights;
3725 private transient array!MapTile renderFrontTiles; // normal, with fg
3727 final bool renderSortByDepth (MapEntity oa, MapEntity ob) {
3728 auto da = oa.depth, db = ob.depth;
3729 if (da == db) return (oa.objId < ob.objId);
3734 const int RenderEdgePixNormal = 64;
3735 const int RenderEdgePixLight = 256;
3737 #ifndef EXPERIMENTAL_RENDER_CACHE
3738 enum skipListCreation = false;
3741 void renderWithOfs (int xofs, int yofs, float currFrameDelta) {
3742 int scale = global.scale;
3744 // don't touch framebuffer alpha
3745 Video.colorMask = Video::CMask.Colors;
3746 Video.color = 0xff_ff_ff;
3748 bool isDarkLevel = global.darkLevel;
3751 switch (global.config.scumPlayerLit) {
3752 case 0: player.lightRadius = 0; break; // never
3753 case 1: // only in "scumDarkness"
3754 player.lightRadius = (global.config.scumDarkness >= 2 ? 96 : 32);
3757 player.lightRadius = 96;
3762 // render cave background
3765 int bgw = levBGImg.tex.width*scale;
3766 int bgh = levBGImg.tex.height*scale;
3767 int bgTW = (tilesWidth*tsz+bgw-1)/bgw;
3768 int bgTH = (tilesHeight*tsz+bgh-1)/bgh;
3769 int bgX0 = max(0, xofs/bgw);
3770 int bgY0 = max(0, yofs/bgh);
3771 int bgX1 = min(bgTW, (xofs+viewWidth+bgw-1)/bgw);
3772 int bgY1 = min(bgTH, (yofs+viewHeight+bgh-1)/bgh);
3773 foreach (int ty; bgY0..bgY1) {
3774 foreach (int tx; bgX0..bgX1) {
3775 int x0 = tx*bgw-xofs;
3776 int y0 = ty*bgh-yofs;
3777 levBGImg.tex.blitAt(x0, y0, scale);
3782 int RenderEdgePix = (global.darkLevel ? RenderEdgePixLight : RenderEdgePixNormal);
3784 // render background tiles
3785 for (MapBackTile bt = backtiles; bt; bt = bt.next) {
3786 bt.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3789 // collect visible special tiles
3790 #ifdef EXPERIMENTAL_RENDER_CACHE
3791 bool skipListCreation = (lastRenderTime == time && renderVisibleCids.length); //FIXME
3794 if (!skipListCreation) {
3795 renderVisibleCids.clear();
3796 renderVisibleLights.clear();
3797 renderFrontTiles.clear();
3799 int endVX = xofs+viewWidth;
3800 int endVY = yofs+viewHeight;
3804 if (player && player.visible) renderVisibleCids[$] = player; // include player in render list
3806 //FIXME: drop lit objects which cannot affect visible area
3808 // collect visible objects
3809 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)) {
3810 if (!o.visible) continue;
3811 auto tile = MapTile(o);
3813 if (isDarkLevel && (o.lightRadius > 4 || tile.litWholeTile)) renderVisibleLights[$] = o;
3814 if (tile.invisible) continue;
3815 if (tile.bgfront) renderFrontTiles[$] = tile;
3816 if (tile.bgback) tile.drawWithOfsBack(xofs, yofs, scale, currFrameDelta);
3818 if (isDarkLevel && o.lightRadius > 4) renderVisibleLights[$] = o;
3820 // check if the object is really visible -- this will speed up later sorting
3821 int fx0, fy0, fx1, fy1;
3822 auto spf = o.getSpriteFrame(default, out fx0, out fy0, out fx1, out fy1);
3823 if (!spf) continue; // no sprite -- nothing to draw (no, really)
3824 int ix = o.ix, iy = o.iy;
3825 int x0 = (ix+fx0)*scale, y0 = (iy+fy0)*scale;
3826 int x1 = (ix+fx1)*scale, y1 = (iy+fy1)*scale;
3827 if (x1 <= xofs || y1 <= yofs || x0 >= endVX || y0 >= endVY) {
3831 renderVisibleCids[$] = o;
3834 foreach (MapEntity o; objGrid.allObjects()) {
3835 if (!o.visible) continue;
3836 auto tile = MapTile(o);
3838 if (isDarkLevel && (o.lightRadius > 4 || tile.litWholeTile)) renderVisibleLights[$] = o;
3839 if (tile.invisible) continue;
3840 if (tile.bgfront) renderFrontTiles[$] = tile;
3841 if (tile.bgback) tile.drawWithOfsBack(xofs, yofs, scale, currFrameDelta);
3843 if (isDarkLevel && o.lightRadius > 4) renderVisibleLights[$] = o;
3845 renderVisibleCids[$] = o;
3848 //writeln("::: ", cnt, " invisible objects dropped");
3850 renderVisibleCids.sort(&renderSortByDepth);
3851 lastRenderTime = time;
3854 auto depth4Start = 0;
3855 foreach (auto xidx, MapEntity o; renderVisibleCids) {
3862 bool playerPowerupRendered = false;
3864 // render objects (part one: depth > 3)
3865 foreach (auto idx; depth4Start..renderVisibleCids.length; reverse) {
3866 MapEntity o = renderVisibleCids[idx];
3867 // 1000 is an ordinary tile
3868 if (!playerPowerupRendered && o.depth <= 1200) {
3869 playerPowerupRendered = true;
3870 // so ducking player will have it's cape correctly rendered
3871 if (player.visible) player.drawPrePrePowerupWithOfs(xofs, yofs, scale, currFrameDelta);
3873 //if (idx && renderSortByDepth(o, renderVisibleCids[idx-1])) writeln("VIOLATION!");
3874 o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3877 // render object (part two: front tile parts, depth 3.5)
3878 foreach (MapTile tile; renderFrontTiles) {
3879 tile.drawWithOfsFront(xofs, yofs, scale, currFrameDelta);
3882 // render objects (part three: depth <= 3)
3883 foreach (auto idx; 0..depth4Start; reverse) {
3884 MapEntity o = renderVisibleCids[idx];
3885 o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3886 //done above;if (isDarkLevel && (o.lightRadius > 4 || (o isa MapTile && MapTile(o).litWholeTile))) renderVisibleLights[$] = o;
3889 // render player post-effects, so cape or jetpack will be correctly rendered on level exit, for example
3890 player.lastDrawWithOfs(xofs, yofs, scale, currFrameDelta);
3894 auto ltex = bgtileStore.lightTexture('ltx512', 512);
3896 // set screen alpha to min
3897 Video.colorMask = Video::CMask.Alpha;
3898 Video.blendMode = Video::BlendMode.None;
3899 Video.color = 0xff_ff_ff_ff;
3900 Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
3901 //Video.colorMask = Video::CMask.All;
3904 // also, stencil 'em, so we can filter dark areas
3905 Video.textureFiltering = true;
3906 Video.stencil = true;
3907 Video.stencilFunc(Video::StencilFunc.Always, 1);
3908 Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Replace);
3909 Video.alphaTestFunc = Video::AlphaFunc.Greater;
3910 Video.alphaTestVal = 0.03+0.011*global.config.darknessDarkness;
3911 Video.color = 0xff_ff_ff;
3912 Video.blendFunc = Video::BlendFunc.Max;
3913 Video.blendMode = Video::BlendMode.Blend; // anything except `Normal`
3914 Video.colorMask = Video::CMask.Alpha;
3916 foreach (MapEntity e; renderVisibleLights) {
3918 e.getInterpCoords(currFrameDelta, scale, out xi, out yi);
3919 auto tile = MapTile(e);
3920 if (tile && tile.litWholeTile) {
3921 //Video.color = 0xff_ff_ff;
3922 Video.fillRect(xi-xofs, yi-yofs, e.width*scale, e.height*scale);
3924 int lrad = e.lightRadius;
3925 if (lrad < 4) continue; // just in case
3927 float lightscale = float(lrad*scale)/float(ltex.tex.width);
3928 #ifdef OLD_LIGHT_OFFSETS
3929 int fx0, fy0, fx1, fy1;
3931 auto spf = e.getSpriteFrame(out doMirror, out fx0, out fy0, out fx1, out fy1);
3933 xi += (fx1-fx0)*scale/2;
3934 yi += (fy1-fy0)*scale/2;
3938 e.getLightOffset(out lxofs, out lyofs);
3943 lrad = lrad*scale/2;
3946 ltex.tex.blitAt(xi, yi, lightscale);
3948 Video.textureFiltering = false;
3950 // modify only lit parts
3951 Video.stencilFunc(Video::StencilFunc.Equal, 1);
3952 Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Keep);
3953 // multiply framebuffer colors by framebuffer alpha
3954 Video.color = 0xff_ff_ff; // it doesn't matter
3955 Video.blendFunc = Video::BlendFunc.Add;
3956 Video.blendMode = Video::BlendMode.DstMulDstAlpha;
3957 Video.colorMask = Video::CMask.Colors;
3958 Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
3960 // filter unlit parts
3961 Video.stencilFunc(Video::StencilFunc.NotEqual, 1);
3962 Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Keep);
3963 Video.blendFunc = Video::BlendFunc.Add;
3964 Video.blendMode = Video::BlendMode.Filter;
3965 Video.colorMask = Video::CMask.Colors;
3966 Video.color = 0x00_00_18+0x00_00_10*global.config.darknessDarkness;
3967 //Video.color = 0x00_00_18;
3968 //Video.color = 0x00_00_38;
3969 Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
3972 Video.blendFunc = Video::BlendFunc.Add;
3973 Video.blendMode = Video::BlendMode.Normal;
3974 Video.colorMask = Video::CMask.All;
3975 Video.alphaTestFunc = Video::AlphaFunc.Always;
3976 Video.stencil = false;
3979 // clear visible objects list (nope)
3980 //renderVisibleCids.clear();
3981 //renderVisibleLights.clear();
3984 if (global.config.drawHUD) renderHUD(currFrameDelta);
3985 renderCompass(currFrameDelta);
3987 float osdTimeLeft, osdTimeStart;
3988 string msg = osdGetMessage(out osdTimeLeft, out osdTimeStart);
3990 auto ct = GetTickCount();
3992 sprStore.loadFont('sFontSmall');
3993 auto msgHeight = sprStore.getMultilineTextHeight(msg, msgScale);
3994 int x = Video.screenWidth/2;
3995 int y = Video.screenHeight-64-msgHeight;
3996 auto oldColor = Video.color;
3997 Video.color = 0xff_ff_00;
3998 if (osdTimeLeft < 0.5) {
3999 int alpha = 255-clamp(int(510*osdTimeLeft), 0, 255);
4000 Video.color = Video.color|(alpha<<24);
4001 } else if (ct-osdTimeStart < 0.5) {
4002 osdTimeStart = ct-osdTimeStart;
4003 int alpha = 255-clamp(int(510*osdTimeStart), 0, 255);
4004 Video.color = Video.color|(alpha<<24);
4006 sprStore.renderMultilineTextCentered(x, y, msg, msgScale, 0x00_ff_00, 0xff_ff_ff);
4007 Video.color = oldColor;
4010 if (inWinCutscene) renderWinCutsceneOverlay();
4011 if (isTransitionRoom()) renderTransitionOverlay();
4012 Video.color = 0xff_ff_ff;
4016 // ////////////////////////////////////////////////////////////////////////// //
4017 final class!MapObject findGameObjectClassByName (name aname) {
4018 if (!aname) return none; // just in case
4019 auto co = FindClassByGameObjName(aname);
4021 writeln("***UNKNOWN GAME OBJECT: '", aname, "'");
4024 co = GetClassReplacement(co);
4025 if (!co) FatalError("findGameObjectClassByName: WTF?!");
4026 if (!class!MapObject(co)) FatalError("findGameTileClassByName: object '%n' is not a map object, it is a `%n`", aname, GetClassName(co));
4027 return class!MapObject(co);
4031 final class!MapTile findGameTileClassByName (name aname) {
4032 if (!aname) return none; // just in case
4033 auto co = FindClassByGameObjName(aname);
4034 if (!co) return MapTile; // unknown names will be routed directly to tile object
4035 co = GetClassReplacement(co);
4036 if (!co) FatalError("findGameTileClassByName: WTF?!");
4037 if (!class!MapTile(co)) FatalError("findGameTileClassByName: object '%n' is not a tile, it is a `%n`", aname, GetClassName(co));
4038 return class!MapTile(co);
4042 final MapObject findAnyObjectOfType (name aname) {
4043 if (!aname) return none;
4044 auto cls = FindClassByGameObjName(aname);
4045 if (!cls) return none;
4046 foreach (MapObject obj; objGrid.allObjects(MapObject)) {
4047 if (obj.spectral) continue;
4048 if (obj isa cls) return obj;
4054 // ////////////////////////////////////////////////////////////////////////// //
4055 final bool isRopePlacedAt (int x, int y) {
4057 foreach (ref auto v; covered) v = false;
4058 foreach (MapTile t; objGrid.inRectPix(x, y-8, 1, 17, precise:false, castClass:MapTileRope)) {
4059 //if (!cbIsRopeTile(t)) continue;
4060 if (t.ix != x) continue;
4061 if (t.iy == y) return true;
4062 foreach (int ty; t.iy..t.iy+8) {
4064 if (d >= 0 && d < covered.length) covered[d] = true;
4067 // check if the whole rope height is completely covered with ropes
4068 foreach (auto v; covered) if (!v) return false;
4073 final MapTile CreateMapTile (int xpos, int ypos, name aname) {
4074 if (!aname) FatalError("cannot create typeless tile");
4075 auto tclass = findGameTileClassByName(aname);
4076 if (!tclass) return none;
4077 MapTile tile = SpawnObject(tclass);
4078 tile.global = global;
4080 tile.objName = aname;
4081 tile.objType = aname; // just in case
4084 tile.objId = ++lastUsedObjectId;
4085 if (!tile.initialize()) FatalError("cannot create tile '%n'", aname);
4090 final bool PutSpawnedMapTile (int x, int y, MapTile tile, optional bool putToGrid) {
4091 if (!tile || !tile.isInstanceAlive) return false;
4093 if (!putToGrid) putToGrid = (tile.moveable || tile.toSpecialGrid || tile.width != 16 || tile.height != 16 || x%16 != 0 || y%16 != 0);
4095 //writeln("putting tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (putToGrid ? "special " : ""), "grid, pos=(", x/16.0, ",", y/16.0, ")");
4098 int mapx = x/16, mapy = y/16;
4099 if (mapx < 0 || mapx >= tilesWidth || mapy < 0 || mapy >= tilesHeight) return false;
4102 // if we already have rope tile there, there is no reason to add another one
4103 if (tile isa MapTileRope) {
4104 if (isRopePlacedAt(x, y)) return false;
4107 // activate special or animated tile
4108 tile.active = tile.active || putToGrid || tile.moveable || tile.toSpecialGrid || tile.lava /*|| tile.water*/; // will be done in MakeMapTile
4109 // animated tiles must be active
4111 auto spr = tile.getSprite();
4112 if (spr && spr.frames.length > 1) {
4113 writeln("activated animated tile '", tile.objName, "'");
4121 //if (tile isa TitleTileCopy) writeln("*** PUTTING COPYRIGHT TILE");
4122 tile.toSpecialGrid = true;
4123 if (!tile.dontReplaceOthers && x&16 == 0 && y%16 == 0) {
4124 auto t = getTileAtGridAny(x/16, y/16);
4125 if (t && !t.immuneToReplacement) {
4126 writeln("REPLACING tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
4127 writeln(" NEW tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (tile.toSpecialGrid ? "special " : ""), "grid, pos=(", tile.fltx/16.0, ",", tile.flty/16.0, ")");
4131 objGrid.insert(tile);
4133 //writeln("SIZE: ", tilesWidth, "x", tilesHeight);
4134 setTileAtGrid(x/16, y/16, tile);
4135 auto t = getTileAtGridAny(x/16, y/16);
4138 writeln("putting tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (putToGrid ? "special " : ""), "grid, pos=(", x/16.0, ",", y/16.0, ")");
4139 checkTilesInRect(x/16, y/16, 16, 16, delegate bool (MapTile tile) {
4140 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, ")");
4143 FatalError("FUUUUUU");
4148 if (tile.enter) registerEnter(tile);
4149 if (tile.exit) registerExit(tile);
4155 // won't call `onDestroy()`
4156 final void RemoveMapTileFromGrid (int tileX, int tileY, optional string reason) {
4157 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
4158 auto t = getTileAtGridAny(tileX, tileY);
4160 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, ")");
4168 final MapTile MakeMapTile (int mapx, int mapy, name aname, optional bool putToGrid) {
4169 //if (mapy >= tilesHeight) { writeln("OOM: ", aname, "; ty=", mapy, "; hgt=", tilesHeight); }
4170 if (mapx < 0 || mapx >= tilesWidth || mapy < 0 || mapy >= tilesHeight) return none;
4172 // if we already have rope tile there, there is no reason to add another one
4173 if (aname == 'oRope') {
4174 if (isRopePlacedAt(mapx*16, mapy*16)) return none;
4177 auto tile = CreateMapTile(mapx*16, mapy*16, aname);
4178 if (!tile) return none;
4179 if (!PutSpawnedMapTile(mapx*16, mapy*16, tile, putToGrid!optional)) {
4188 final MapTile MakeMapTileAtPix (int xpix, int ypix, name aname, optional bool putToGrid) {
4189 // if we already have rope tile there, there is no reason to add another one
4190 if (aname == 'oRope') {
4191 if (isRopePlacedAt(xpix, ypix)) return none;
4194 auto tile = CreateMapTile(xpix, ypix, aname);
4195 if (!tile) return none;
4196 if (!PutSpawnedMapTile(xpix, ypix, tile, putToGrid!optional)) {
4205 final MapTile MakeMapRopeTileAt (int x0, int y0) {
4206 // if we already have rope tile there, there is no reason to add another one
4207 if (isRopePlacedAt(x0, y0)) return none;
4209 auto tile = CreateMapTile(x0, y0, 'oRope');
4210 if (!PutSpawnedMapTile(x0, y0, tile, putToGrid:true)) {
4219 // ////////////////////////////////////////////////////////////////////////// //
4220 final MapBackTile CreateBgTile (name sprName, optional int atx0, optional int aty0, optional int aw, optional int ah) {
4221 BackTileImage img = bgtileStore[sprName];
4222 auto res = SpawnObject(MapBackTile);
4223 res.global = global;
4226 res.bgtName = sprName;
4227 if (specified_atx0) res.tx0 = atx0;
4228 if (specified_aty0) res.ty0 = aty0;
4229 if (specified_aw) res.w = aw;
4230 if (specified_ah) res.h = ah;
4231 if (!res.initialize()) FatalError("cannot create background tile with image '%n'", sprName);
4236 // ////////////////////////////////////////////////////////////////////////// //
4238 background The background asset from which the new tile will be extracted.
4239 left The x coordinate of the left of the new tile, relative to the background asset's top left corner.
4240 top The y coordinate of the top of the new tile, relative to the background assets top left corner.
4241 width The width of the tile.
4242 height The height of the tile.
4243 x The x position in the room to place the tile.
4244 y The y position in the room to place the tile.
4245 depth The depth at which to place the tile.
4247 final void MakeMapBackTile (name bgname, int left, int top, int width, int height, int x, int y, int depth) {
4248 if (width < 1 || height < 1 || !bgname) return;
4249 auto bgt = bgtileStore[bgname];
4250 if (!bgt) FatalError("cannot load background '%n'", bgname);
4251 MapBackTile bt = SpawnObject(MapBackTile);
4254 bt.objName = bgname;
4256 bt.bgtName = bgname;
4264 // find a place for it
4269 // back tiles with the highest depth should come first
4270 MapBackTile ct = backtiles, cprev = none;
4271 while (ct && depth <= ct.depth) { cprev = ct; ct = ct.next; }
4274 bt.next = cprev.next;
4277 bt.next = backtiles;
4283 // ////////////////////////////////////////////////////////////////////////// //
4284 final MapObject SpawnMapObjectWithClass (class!MapObject oclass) {
4285 if (!oclass) return none;
4287 MapObject obj = SpawnObject(oclass);
4288 if (!obj) FatalError("cannot create map object '%n'", GetClassName(oclass));
4290 //if (aname == 'oShells' || aname == 'oShellSingle' || aname == 'oRopePile') writeln("*** CREATED '", aname, "'");
4292 obj.global = global;
4294 obj.objId = ++lastUsedObjectId;
4300 final MapObject SpawnMapObject (name aname) {
4301 if (!aname) return none;
4302 auto res = SpawnMapObjectWithClass(findGameObjectClassByName(aname));
4303 if (res && !res.objType) res.objType = aname; // just in case
4308 final MapObject PutSpawnedMapObject (int x, int y, MapObject obj) {
4309 if (!obj /*|| obj.global || obj.level*/) return none; // oops
4313 if (!obj.initialize()) { delete obj; return none; } // not fatal
4321 final MapObject MakeMapObject (int x, int y, name aname) {
4322 MapObject obj = SpawnMapObject(aname);
4323 obj = PutSpawnedMapObject(x, y, obj);
4328 // ////////////////////////////////////////////////////////////////////////// //
4329 int winCutSceneTimer = -1;
4330 int winVolcanoTimer = -1;
4331 int winCutScenePhase = 0;
4332 int winSceneDrawStatus = 0;
4333 int winMoneyCount = 0;
4335 bool winFadeOut = false;
4336 int winFadeLevel = 0;
4337 int winCutsceneSkip = 0; // 1: waiting for pay release; 2: pay released, do skip
4338 bool winCutsceneSwitchToNext = false;
4341 void startWinCutscene () {
4342 global.hasParachute = false;
4344 winCutsceneSwitchToNext = false;
4345 winCutsceneSkip = 0;
4346 isKeyPressed(GameConfig::Key.Pay);
4347 isKeyReleased(GameConfig::Key.Pay);
4349 auto olddel = ImmediateDelete;
4350 ImmediateDelete = false;
4355 addBackgroundGfxDetails();
4357 levBGImgName = 'bgCave';
4358 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
4360 blockWaterChecking = true;
4364 ImmediateDelete = olddel;
4365 CollectGarbage(true); // destroy delayed objects too
4367 if (dumpGridStats) objGrid.dumpStats();
4369 playerExited = false; // just in case
4370 playerExitDoor = none;
4378 winCutSceneTimer = -1;
4379 winCutScenePhase = 0;
4382 if (global.config.gameMode != GameConfig::GameMode.Vanilla) {
4383 if (global.config.bizarre) {
4384 global.yasmScore = 1;
4385 global.config.bizarrePlusTitle = true;
4388 array!MapTile toReplace;
4389 forEachTile(delegate bool (MapTile t) {
4390 if (t.objType == 'oGTemple' ||
4391 t.objType == 'oIce' ||
4392 t.objType == 'oDark' ||
4393 t.objType == 'oBrick' ||
4394 t.objType == 'oLush')
4401 foreach (MapTile t; miscTileGrid.allObjects()) {
4402 if (t.objType == 'oGTemple' ||
4403 t.objType == 'oIce' ||
4404 t.objType == 'oDark' ||
4405 t.objType == 'oBrick' ||
4406 t.objType == 'oLush')
4412 foreach (MapTile t; toReplace) {
4414 t.cleanDeath = true;
4415 if (rand(1,120) == 1) instance_change(oGTemple, false);
4416 else if (rand(1,100) == 1) instance_change(oIce, false);
4417 else if (rand(1,90) == 1) instance_change(oDark, false);
4418 else if (rand(1,80) == 1) instance_change(oBrick, false);
4419 else if (rand(1,70) == 1) instance_change(oLush, false);
4427 if (rand(1,5) == 1) instance_change(oLush, false);
4432 //!instance_create(0, 0, oBricks);
4434 //shakeToggle = false;
4435 //oPDummy.status = 2;
4440 if (global.kaliPunish >= 2) {
4441 instance_create(oPDummy.x, oPDummy.y+2, oBall2);
4442 obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
4444 obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
4446 obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
4448 obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
4455 void startWinCutsceneVolcano () {
4456 global.hasParachute = false;
4458 writeln("VOLCANO HOLD 0: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4459 writeln("VOLCANO PICK 0: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4463 winCutsceneSwitchToNext = false;
4464 auto olddel = ImmediateDelete;
4465 ImmediateDelete = false;
4469 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
4471 blockWaterChecking = true;
4473 ImmediateDelete = olddel;
4474 CollectGarbage(true); // destroy delayed objects too
4476 spawnPlayerAt(2*16+8, 11*16+8);
4477 player.dir = MapEntity::Dir.Right;
4479 playerExited = false; // just in case
4480 playerExitDoor = none;
4488 winCutSceneTimer = -1;
4489 winCutScenePhase = 0;
4491 MakeMapTile(0, 0, 'oEnd2BG');
4492 realViewStart.x = 0;
4493 realViewStart.y = 0;
4502 player.dead = false;
4503 player.active = true;
4504 player.visible = false;
4505 player.removeBallAndChain(temp:true);
4506 player.stunned = false;
4507 player.status = MapObject::FALLING;
4508 if (player.holdItem) player.holdItem.visible = false;
4509 player.fltx = 320/2;
4513 writeln("VOLCANO HOLD 1: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4514 writeln("VOLCANO PICK 1: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4519 void startWinCutsceneWinFall () {
4520 global.hasParachute = false;
4522 writeln("FALL HOLD 0: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4523 writeln("FALL PICK 0: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4527 winCutsceneSwitchToNext = false;
4529 auto olddel = ImmediateDelete;
4530 ImmediateDelete = false;
4534 setMenuTilesVisible(false);
4536 //addBackgroundGfxDetails();
4539 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
4541 blockWaterChecking = true;
4545 ImmediateDelete = olddel;
4546 CollectGarbage(true); // destroy delayed objects too
4548 if (dumpGridStats) objGrid.dumpStats();
4550 playerExited = false; // just in case
4551 playerExitDoor = none;
4559 winCutSceneTimer = -1;
4560 winCutScenePhase = 0;
4562 player.dead = false;
4563 player.active = true;
4564 player.visible = false;
4565 player.removeBallAndChain(temp:true);
4566 player.stunned = false;
4567 player.status = MapObject::FALLING;
4568 if (player.holdItem) player.holdItem.visible = false;
4569 player.fltx = 320/2;
4572 winSceneDrawStatus = 0;
4579 writeln("FALL HOLD 1: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4580 writeln("FALL PICK 1: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4585 void setGameOver () {
4586 if (inWinCutscene) {
4587 player.visible = false;
4588 player.removeBallAndChain(temp:true);
4589 if (player.holdItem) player.holdItem.visible = false;
4592 if (inWinCutscene > 0) {
4595 winSceneDrawStatus = 8;
4600 MapTile findEndPlatTile () {
4601 return forEachTile(delegate bool (MapTile t) { return (t isa MapTileEndPlat); }, castClass:MapTileEndPlat);
4605 MapObject findBigTreasure () {
4606 return forEachObject(delegate bool (MapObject o) { return (o isa MapObjectBigTreasure); }, castClass:MapObjectBigTreasure);
4610 void setMenuTilesVisible (bool vis) {
4612 forEachTile(delegate bool (MapTile t) {
4613 if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4614 t.invisible = false;
4619 forEachTile(delegate bool (MapTile t) {
4620 if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4629 void setMenuTilesOnTop () {
4630 forEachTile(delegate bool (MapTile t) {
4631 if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4639 void winCutscenePlayerControl (PlayerPawn plr) {
4640 auto payPress = isKeyPressed(GameConfig::Key.Pay);
4641 auto payRelease = isKeyReleased(GameConfig::Key.Pay);
4643 switch (winCutsceneSkip) {
4644 case 0: // nothing was pressed
4645 if (payPress) winCutsceneSkip = 1;
4647 case 1: // waiting for pay release
4648 if (payRelease) winCutsceneSkip = 2;
4650 case 2: // pay released, do skip
4655 // first winning room
4656 if (inWinCutscene == 1) {
4657 if (plr.ix < 448+8) {
4662 // waiting for chest to open
4663 if (winCutScenePhase == 0) {
4664 winCutSceneTimer = 120/2;
4665 winCutScenePhase = 1;
4670 if (winCutScenePhase == 1) {
4671 if (--winCutSceneTimer == 0) {
4672 winCutScenePhase = 2;
4673 winCutSceneTimer = 20;
4674 forEachObject(delegate bool (MapObject o) {
4675 if (o isa MapObjectBigChest) {
4676 o.setSprite(global.config.gameMode == GameConfig::GameMode.Vanilla ? 'sBigChestOpen' : 'sBigChestOpen2');
4677 auto treasure = MakeMapObject(o.ix, o.iy, 'oBigTreasure');
4681 o.playSound('sndClick');
4682 //!!!if (global.config.gameMode != GameConfig::GameMode.Vanilla) scrSprayGems(oBigChest.x+24, oBigChest.y+24);
4692 if (winCutScenePhase == 2) {
4693 if (--winCutSceneTimer == 0) {
4694 winCutScenePhase = 3;
4695 winCutSceneTimer = 50;
4701 if (winCutScenePhase == 3) {
4702 auto ep = findEndPlatTile();
4703 if (ep) MakeMapObject(ep.ix+global.randOther(0, 80), /*ep.iy*/192+32, 'oBurn');
4704 if (--winCutSceneTimer == 0) {
4705 winCutScenePhase = 4;
4706 winCutSceneTimer = 10;
4707 if (ep) MakeMapObject(ep.ix, ep.iy+30, 'oLavaSpray');
4713 // lava pump first accel
4714 if (winCutScenePhase == 4) {
4715 if (--winCutSceneTimer == 0) {
4716 forEachObject(delegate bool (MapObject o) {
4717 if (o isa MapObjectLavaSpray) o.yAcc = -0.1;
4723 // lava pump complete
4724 if (winCutScenePhase == 5) {
4725 if (--winCutSceneTimer == 0) {
4726 //if (oLavaSpray) oLavaSpray.yAcc = -0.1;
4727 startWinCutsceneVolcano();
4736 if (inWinCutscene == 2) {
4740 if (winCutScenePhase == 0) {
4741 winCutSceneTimer = 50;
4742 winCutScenePhase = 1;
4743 winVolcanoTimer = 10;
4747 if (winVolcanoTimer > 0) {
4748 if (--winVolcanoTimer == 0) {
4749 MakeMapObject(224+global.randOther(0,48), 144+global.randOther(0,8), 'oVolcanoFlame');
4750 winVolcanoTimer = global.randOther(10, 20);
4755 if (winCutScenePhase == 1) {
4756 if (--winCutSceneTimer == 0) {
4757 winCutSceneTimer = 30;
4758 winCutScenePhase = 2;
4759 auto sil = MakeMapObject(240, 132, 'oPlayerSil');
4767 if (winCutScenePhase == 2) {
4768 if (--winCutSceneTimer == 0) {
4769 winCutScenePhase = 3;
4770 auto sil = MakeMapObject(240, 132, 'oTreasureSil');
4780 // winning camel room
4781 if (inWinCutscene == 3) {
4782 //if (!player.holdItem) writeln("SCENE 3: LOST ITEM!");
4784 if (!plr.visible) plr.flty = -32;
4787 if (winCutScenePhase == 0) {
4788 winCutSceneTimer = 50;
4789 winCutScenePhase = 1;
4794 if (winCutScenePhase == 1) {
4795 if (--winCutSceneTimer == 0) {
4796 winCutSceneTimer = 50;
4797 winCutScenePhase = 2;
4798 plr.playSound('sndPFall');
4801 writeln("MUST BE CHAINED: ", plr.mustBeChained);
4802 if (plr.mustBeChained) {
4803 plr.removeBallAndChain(temp:true);
4804 plr.spawnBallAndChain();
4807 writeln("HOLD: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4808 writeln("PICK: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4810 if (!player.holdItem && player.pickedItem) player.scrSwitchToPocketItem(forceIfEmpty:false);
4811 if (player.holdItem) {
4812 player.holdItem.visible = true;
4813 player.holdItem.canLiveOutsideOfLevel = true;
4814 writeln("HOLD ITEM: '", GetClassName(player.holdItem.Class), "'");
4816 plr.status == MapObject::FALLING;
4817 global.plife += 99; // just in case
4822 if (winCutScenePhase == 2) {
4823 auto ball = plr.getMyBall();
4824 if (ball && plr.holdItem != ball) {
4825 ball.teleportTo(plr.fltx, plr.flty+8);
4829 if (plr.status == MapObject::STUNNED || plr.stunned) {
4833 auto treasure = MakeMapObject(144+16+8, -32, 'oBigTreasure');
4834 if (treasure) treasure.depth = 1;
4835 winCutScenePhase = 3;
4837 plr.playSound('sndTFall');
4842 if (winCutScenePhase == 3) {
4843 if (plr.status != MapObject::STUNNED && !plr.stunned) {
4844 auto bt = findBigTreasure();
4848 //plr.status = MapObject::JUMPING;
4850 plr.kJumpPressed = true;
4851 winCutScenePhase = 4;
4852 winCutSceneTimer = 50;
4859 if (winCutScenePhase == 4) {
4860 if (--winCutSceneTimer == 0) {
4861 setMenuTilesVisible(true);
4862 winCutScenePhase = 5;
4863 winSceneDrawStatus = 1;
4864 global.playMusic('musVictory', loop:false);
4865 winCutSceneTimer = 50;
4870 if (winCutScenePhase == 5) {
4871 if (winSceneDrawStatus == 3) {
4872 int money = stats.money;
4873 if (winMoneyCount < money) {
4874 if (money-winMoneyCount > 1000) {
4875 winMoneyCount += 1000;
4876 } else if (money-winMoneyCount > 100) {
4877 winMoneyCount += 100;
4878 } else if (money-winMoneyCount > 10) {
4879 winMoneyCount += 10;
4884 if (winMoneyCount >= money) {
4885 winMoneyCount = money;
4886 ++winSceneDrawStatus;
4891 if (winSceneDrawStatus == 7) {
4894 if (winFadeLevel >= 255) {
4895 ++winSceneDrawStatus;
4896 winCutSceneTimer = 30*30;
4901 if (winSceneDrawStatus == 8) {
4902 if (--winCutSceneTimer == 0) {
4908 if (--winCutSceneTimer == 0) {
4909 ++winSceneDrawStatus;
4910 winCutSceneTimer = 50;
4919 // ////////////////////////////////////////////////////////////////////////// //
4920 void renderWinCutsceneOverlay () {
4921 if (inWinCutscene == 3) {
4922 if (winSceneDrawStatus > 0) {
4923 Video.color = 0xff_ff_ff;
4924 sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
4925 //draw_set_color(txtCol);
4926 drawTextAt(64, 32, "YOU MADE IT!");
4928 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4929 if (global.config.gameMode == GameConfig::GameMode.Vanilla) {
4930 Video.color = 0xff_ff_00; //draw_set_color(c_yellow);
4931 drawTextAt(64, 48, "Classic Mode done!");
4933 Video.color = 0x00_80_80; //draw_set_color(c_teal);
4934 if (global.config.bizarrePlus) drawTextAt(64, 48, "Bizarre Mode Plus done!");
4935 else drawTextAt(64, 48, "Bizarre Mode done!");
4936 //draw_set_color(c_white);
4938 if (!global.usedShortcut) {
4939 Video.color = 0xc0_c0_c0; //draw_set_color(c_silver);
4940 drawTextAt(64, 56, "No shortcuts used!");
4941 //draw_set_color(c_yellow);
4945 if (winSceneDrawStatus > 1) {
4946 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4947 //draw_set_color(txtCol);
4948 Video.color = 0xff_ff_ff;
4949 drawTextAt(64, 64, "FINAL SCORE:");
4952 if (winSceneDrawStatus > 2) {
4953 sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
4954 Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4955 drawTextAt(64, 72, va("$%d", winMoneyCount));
4958 if (winSceneDrawStatus > 4) {
4959 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4960 Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4961 drawTextAt(64, 96, va("Time: %s", time2str(winTime/30)));
4963 draw_set_color(c_white);
4964 if (s < 10) draw_text(96+24, 96, string(m) + ":0" + string(s));
4965 else draw_text(96+24, 96, string(m) + ":" + string(s));
4969 if (winSceneDrawStatus > 5) {
4970 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4971 Video.color = 0xff_ff_ff; //draw_set_color(txtCol);
4972 drawTextAt(64, 96+8, "Kills: ");
4973 Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4974 drawTextAt(96+24, 96+8, va("%s", stats.countKills()));
4977 if (winSceneDrawStatus > 6) {
4978 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4979 Video.color = 0xff_ff_ff; //draw_set_color(txtCol);
4980 drawTextAt(64, 96+16, "Saves: ");
4981 Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4982 drawTextAt(96+24, 96+16, va("%s", stats.damselsSaved));
4986 Video.color = (255-clamp(winFadeLevel, 0, 255))<<24;
4987 Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
4990 if (winSceneDrawStatus == 8) {
4991 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4992 Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4994 if (global.config.gameMode == GameConfig::GameMode.Vanilla) {
4995 Video.color = 0xff_ff_00; //draw_set_color(c_yellow);
4996 lastString = "YOU SHALL BE REMEMBERED AS A HERO.";
4998 Video.color = 0x00_ff_ff;
4999 if (global.config.bizarrePlus) lastString = "ANOTHER LEGENDARY ADVENTURE!";
5000 else lastString = "YOUR DISCOVERIES WILL BE CELEBRATED!";
5002 auto strLen = lastString.length*8;
5004 n = trunc(ceil(n/2.0));
5005 drawTextAt(n, 116, lastString);
5011 // ////////////////////////////////////////////////////////////////////////// //
5012 #include "roomTitle.vc"
5013 #include "roomTrans1.vc"
5014 #include "roomTrans2.vc"
5015 #include "roomTrans3.vc"
5016 #include "roomTrans4.vc"
5017 #include "roomOlmec.vc"
5018 #include "roomEnd.vc"
5019 #include "roomTutorial.vc"
5020 #include "roomScores.vc"
5021 #include "roomStars.vc"
5022 #include "roomSun.vc"
5023 #include "roomMoon.vc"
5026 // ////////////////////////////////////////////////////////////////////////// //
5027 #include "packages/Generator/loadRoomGens.vc"
5028 #include "packages/Generator/loadEntityGens.vc"