1 /**********************************************************************************
2 * Copyright (c) 2008, 2009 Derek Yu and Mossmouth, LLC
3 * Copyright (c) 2010, Moloch
4 * Copyright (c) 2018, Ketmar Dark
6 * This file is part of Spelunky.
8 * You can redistribute and/or modify Spelunky, including its source code, under
9 * the terms of the Spelunky User License.
11 * Spelunky is distributed in the hope that it will be entertaining and useful,
12 * but WITHOUT WARRANTY. Please see the Spelunky User License for more details.
14 * The Spelunky User License should be available in "Game .Information", which
15 * can be found in the Resource Explorer, or as an external file called COPYING.
16 * If not, please obtain a new copy of Spelunky from <http://spelunkyworld.com/>
18 **********************************************************************************/
19 // this is the level we're playing in, with all objects and tiles
20 class GameLevel : Object;
22 //#define EXPERIMENTAL_RENDER_CACHE
24 const float FrameTime = 1.0f/30.0f;
26 const int dumpGridStats = true;
33 //enum NormalTilesWidth = LevelGen::LevelWidth*RoomGen::Width+2;
34 //enum NormalTilesHeight = LevelGen::LevelHeight*RoomGen::Height+2;
36 enum MaxTilesWidth = 64;
37 enum MaxTilesHeight = 64;
40 transient GameStats stats;
41 transient SpriteStore sprStore;
42 transient BackTileStore bgtileStore;
43 transient BackTileImage levBGImg;
46 transient name lastMusicName;
47 //RoomGen[LevelGen::LevelWidth, LevelGen::LevelHeight] rooms; // moved to levelgen
49 transient float accumTime;
50 transient bool gamePaused = false;
51 transient bool gameShowHelp = false;
52 transient int gameHelpScreen = 0;
53 const int MaxGameHelpScreen = 2;
54 transient bool checkWater;
55 transient int liquidTileCount; // cached
56 /*transient*/ int damselSaved;
60 transient int collectCounter;
61 /*transient*/ int levelMoneyStart;
63 // all movable (thinkable) map objects
64 EntityGrid objGrid; // monsters, items and tiles
66 MapBackTile backtiles;
67 bool blockWaterChecking;
71 bool cameFromIntroRoom; // for title screen
72 bool allowFinalCutsceneSkip;
74 LevelGen::RType[MaxTilesWidth, MaxTilesHeight] roomType;
88 LevelKind levelKind = LevelKind.Normal;
90 array!MapTile allEnters;
91 array!MapTile allExits;
94 int startRoomX, startRoomY;
95 int endRoomX, endRoomY;
99 MapEntity playerExitDoor;
100 transient bool disablePlayerThink = false;
101 int maxPlayingTime; // in seconds
107 bool ghostSpawned; // to speed up some checks
108 bool resetBMCOG = false;
112 // FPS, i.e. incremented by 30 in one second
113 int time; // in frames
114 int lastUsedObjectId;
115 transient int lastRenderTime = -1;
116 transient int pausedTime;
118 MapEntity deadItemsHead;
119 transient /*bool*/int hasSolidObjects = true;
121 // screen shake variables
126 // set this before calling `fixCamera()`
127 // dimensions should be real, not scaled up/down
128 transient int viewWidth, viewHeight;
129 //transient int viewOffsetX, viewOffsetY;
131 // room bounds, not scaled
132 IVec2D viewMin, viewMax;
134 // for Olmec level cinematics
135 IVec2D cameraSlideToDest;
136 IVec2D cameraSlideToCurr;
137 IVec2D cameraSlideToSpeed; // !0: slide
138 int cameraSlideToPlayer;
139 // `fixCamera()` will set the following
140 // coordinates will be real too (with scale applied)
141 // shake is not applied
142 transient IVec2D viewStart; // with `player.viewOffset`
143 private transient IVec2D realViewStart; // without `player.viewOffset`
145 transient int framesProcessedFromLastClear;
147 transient int BuildYear;
148 transient int BuildMonth;
149 transient int BuildDay;
150 transient int BuildHour;
151 transient int BuildMin;
152 transient string BuildDateString;
155 final string getBuildDateString () {
156 if (!BuildYear) return BuildDateString;
157 if (BuildDateString) return BuildDateString;
158 BuildDateString = va("%d-%02d-%02d %02d:%02d", BuildYear, BuildMonth, BuildDay, BuildHour, BuildMin);
159 return BuildDateString;
163 final void cameraSlideToPoint (int dx, int dy, int speedx, int speedy) {
164 cameraSlideToPlayer = 0;
165 cameraSlideToDest.x = dx;
166 cameraSlideToDest.y = dy;
167 cameraSlideToSpeed.x = abs(speedx);
168 cameraSlideToSpeed.y = abs(speedy);
169 cameraSlideToCurr.x = cameraCurrX;
170 cameraSlideToCurr.y = cameraCurrY;
174 final void cameraReturnToPlayer () {
175 if (!cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y)) {
176 cameraSlideToCurr.x = cameraCurrX;
177 cameraSlideToCurr.y = cameraCurrY;
178 if (cameraSlideToSpeed.x && abs(cameraSlideToSpeed.x) < 8) cameraSlideToSpeed.x = 8;
179 if (cameraSlideToSpeed.y && abs(cameraSlideToSpeed.y) < 8) cameraSlideToSpeed.y = 8;
180 cameraSlideToPlayer = 1;
185 // if `frameSkip` is `true`, there are more frames waiting
186 // (i.e. you may skip rendering and such)
187 transient void delegate (bool frameSkip) onBeforeFrame;
188 transient void delegate (bool frameSkip) onAfterFrame;
190 transient void delegate () onCameraTeleported;
192 transient void delegate () onLevelExitedCB;
194 // this will be called in-between frames, and
195 // `frameTime` is [0..1)
196 transient void delegate (float frameTime) onInterFrame;
198 final int bizRoomStyle { get { return (lg ? lg.bizRoomStyle : 0); } }
201 final bool isNormalLevel () { return (levelKind == LevelKind.Normal); }
202 final bool isTitleRoom () { return (levelKind == LevelKind.Title); }
203 final bool isTutorialRoom () { return (levelKind == LevelKind.Tutorial); }
204 final bool isTransitionRoom () { return (levelKind == LevelKind.Transition); }
205 final bool isIntroRoom () { return (levelKind == LevelKind.Transition); }
208 bool isHUDEnabled () {
209 if (inWinCutscene) return false;
210 if (inIntroCutscene) return false;
211 if (lg.finalBossLevel) return true;
212 if (isNormalLevel()) return true;
217 // ////////////////////////////////////////////////////////////////////////// //
219 void addDeath (name aname) { if (isNormalLevel()) stats.addDeath(aname); }
226 void addKill (name aname, optional bool telefrag) {
227 if (isNormalLevel()) stats.addKill(aname, telefrag!optional);
228 else if (aname == 'Shopkeeper' && levelKind == LevelKind.Stars) { ++stats.starsKills; ++starsKills; }
231 void addCollect (name aname, optional int amount) { if (isNormalLevel()) stats.addCollect(aname, amount!optional); }
233 void addDamselSaved () { if (isNormalLevel()) stats.addDamselSaved(); }
234 void addIdolStolen () { if (isNormalLevel()) stats.addIdolStolen(); }
235 void addIdolConverted () { if (isNormalLevel()) stats.addIdolConverted(); }
236 void addCrystalIdolStolen () { if (isNormalLevel()) stats.addCrystalIdolStolen(); }
237 void addCrystalIdolConverted () { if (isNormalLevel()) stats.addCrystalIdolConverted(); }
238 void addGhostSummoned () { if (isNormalLevel()) stats.addGhostSummoned(); }
241 // ////////////////////////////////////////////////////////////////////////// //
242 static final string time2str (int time) {
243 int secs = time%60; time /= 60;
244 int mins = time%60; time /= 60;
245 int hours = time%24; time /= 24;
247 if (days) return va("%d DAYS, %d:%02d:%02d", days, hours, mins, secs);
248 if (hours) return va("%d:%02d:%02d", hours, mins, secs);
249 return va("%02d:%02d", mins, secs);
253 // ////////////////////////////////////////////////////////////////////////// //
254 final int tilesWidth () { return lg.levelRoomWidth*RoomGen::Width+2; }
255 final int tilesHeight () { return (lg.finalBossLevel ? 55 : lg.levelRoomHeight*RoomGen::Height+2); }
258 // ////////////////////////////////////////////////////////////////////////// //
259 protected void resetGameInternal () {
260 if (player) player.removeBallAndChain();
263 allowFinalCutsceneSkip = true;
264 //inIntroCutscene = 0;
276 player.removeBallAndChain();
277 auto hi = player.holdItem;
278 player.holdItem = none;
279 if (hi) hi.instanceRemove();
280 hi = player.pickedItem;
281 player.pickedItem = none;
282 if (hi) hi.instanceRemove();
289 stats.clearGameTotals();
293 // this won't generate a level yet
294 void restartGame () {
296 if (global.startMoney > 0) stats.setMoneyCheat();
297 stats.setMoney(global.startMoney);
298 levelKind = LevelKind.Normal;
302 // complement function to `restart game`
303 void generateNormalLevel () {
305 centerViewAtPlayer();
309 void restartTitle () {
312 createSpecialLevel(LevelKind.Title, &createTitleRoom, 'musTitle');
321 void restartIntro () {
324 createSpecialLevel(LevelKind.Intro, &createIntroRoom, '');
333 void restartTutorial () {
336 createSpecialLevel(LevelKind.Tutorial, &createTutorialRoom, 'musCave');
345 void restartScores () {
348 createSpecialLevel(LevelKind.Scores, &createScoresRoom, 'musTitle');
357 void restartStarsRoom () {
360 createSpecialLevel(LevelKind.Stars, &createStarsRoom, '');
369 void restartSunRoom () {
372 createSpecialLevel(LevelKind.Sun, &createSunRoom, '');
381 void restartMoonRoom () {
384 createSpecialLevel(LevelKind.Moon, &createMoonRoom, '');
393 // ////////////////////////////////////////////////////////////////////////// //
394 // generate angry shopkeeper at exit if murderer or thief
395 void generateAngryShopkeepers () {
396 if (global.murderer || global.thiefLevel > 0) {
397 foreach (MapTile e; allExits) {
398 auto obj = MonsterShopkeeper(MakeMapObject(e.ix, e.iy, 'oShopkeeper'));
400 obj.style = 'Bounty Hunter';
401 obj.status = MapObject::PATROL;
408 // ////////////////////////////////////////////////////////////////////////// //
409 final void resetRoomBounds () {
412 viewMax.x = tilesWidth*16;
413 viewMax.y = tilesHeight*16;
414 // Great Lake is bottomless (nope)
415 //if (global.lake == 1) viewMax.y -= 16;
416 //writeln("ROOM BOUNDS: (", viewMax.x, "x", viewMax.y, ")");
420 final void setRoomBounds (int x0, int y0, int x1, int y1) {
428 // ////////////////////////////////////////////////////////////////////////// //
431 float timeout; // seconds
432 float starttime; // for active
433 bool active; // true: timeout is `GetTickCount()` dismissing time
436 array!OSDMessage msglist; // [0]: current one
438 struct OSDMessageTalk {
440 float timeout; // seconds;
441 float starttime; // for active
442 bool active; // true: timeout is `GetTickCount()` dismissing time
443 bool shopOnly; // true: timeout when player exited the shop
444 int hiColor1; // -1: default
445 int hiColor2; // -1: default
448 array!OSDMessageTalk msgtalklist; // [0]: current one
451 private final void osdCheckTimeouts () {
452 auto stt = GetTickCount();
453 while (msglist.length) {
454 if (!msglist[0].msg) { msglist.remove(0); continue; }
455 if (!msglist[0].active) {
456 msglist[0].active = true;
457 msglist[0].starttime = stt;
459 if (msglist[0].starttime+msglist[0].timeout >= stt) break;
462 if (msgtalklist.length) {
463 bool inshop = isInShop(player.ix/16, player.iy/16);
464 while (msgtalklist.length) {
465 if (!msgtalklist[0].msg) { msgtalklist.remove(0); continue; }
466 if (msgtalklist[0].shopOnly) {
467 if (inshop == msgtalklist[0].active) {
468 msgtalklist[0].active = !inshop;
469 if (!inshop) msgtalklist[0].starttime = stt;
472 if (!msgtalklist[0].active) {
473 msgtalklist[0].active = true;
474 msgtalklist[0].starttime = stt;
477 if (!msgtalklist[0].active) break;
478 //writeln("timedelta: ", msgtalklist[0].starttime+msgtalklist[0].timeout-stt);
479 if (msgtalklist[0].starttime+msgtalklist[0].timeout >= stt) break;
480 msgtalklist.remove(0);
486 final bool osdHasMessage () {
488 return (msglist.length > 0);
492 final string osdGetMessage (out float timeLeft, out float timeStart) {
494 if (msglist.length == 0) { timeLeft = 0; return ""; }
495 auto stt = GetTickCount();
496 timeStart = msglist[0].starttime;
497 timeLeft = fmax(0, msglist[0].starttime+msglist[0].timeout-stt);
498 return msglist[0].msg;
502 final string osdGetTalkMessage (optional out int hiColor1, optional out int hiColor2) {
504 if (msgtalklist.length == 0) return "";
505 hiColor1 = msgtalklist[0].hiColor1;
506 hiColor2 = msgtalklist[0].hiColor2;
507 return msgtalklist[0].msg;
511 final void osdClear (optional bool clearTalk) {
513 if (clearTalk) msgtalklist.clear();
517 final void osdMessage (string msg, optional float timeout, optional bool urgent) {
519 msg = global.expandString(msg);
520 if (!specified_timeout) timeout = 3.33;
521 // special message for shops
522 if (timeout == -666) {
524 if (msglist.length && msglist[0].msg == msg) return;
525 if (msglist.length == 0 || msglist[0].msg != msg) {
526 osdClear(clearTalk:false);
528 msglist[0].msg = msg;
530 msglist[0].active = false;
531 msglist[0].timeout = 3.33;
535 if (timeout < 0.1) return;
536 timeout = fmax(1.0, timeout);
537 //writeln("OSD: ", msg);
538 // find existing one, and bring it to the top
540 for (; oldidx < msglist.length; ++oldidx) {
541 if (msglist[oldidx].msg == msg) break; // i found her!
544 if (oldidx < msglist.length) {
545 // yeah, move duplicate to the top
546 msglist[oldidx].timeout = fmax(timeout, msglist[oldidx].timeout);
547 msglist[oldidx].active = false;
548 if (urgent && oldidx != 0) {
549 timeout = msglist[oldidx].timeout;
550 msglist.remove(oldidx);
552 msglist[0].msg = msg;
553 msglist[0].timeout = timeout;
554 msglist[0].active = false;
558 msglist[0].msg = msg;
559 msglist[0].timeout = timeout;
560 msglist[0].active = false;
564 msglist[$-1].msg = msg;
565 msglist[$-1].timeout = timeout;
566 msglist[$-1].active = false;
572 void osdMessageTalk (string msg, optional bool replace, optional float timeout, optional bool inShopOnly,
573 optional int hiColor1, optional int hiColor2)
576 //writeln("talk msg: replace=", replace, "; timeout=", timeout, "; inshop=", inShopOnly, "; msg=", msg);
577 if (!specified_timeout) timeout = 3.33;
578 if (!specified_inShopOnly) inShopOnly = true;
579 if (!specified_hiColor1) hiColor1 = -1;
580 if (!specified_hiColor2) hiColor2 = -1;
581 msg = global.expandString(msg);
583 if (!msg) { msgtalklist.clear(); return; }
584 if (msgtalklist.length && msgtalklist[0].msg == msg) {
585 while (msgtalklist.length > 1) msgtalklist.remove(1);
586 msgtalklist[$-1].timeout = timeout;
587 msgtalklist[$-1].shopOnly = inShopOnly;
589 if (msgtalklist.length) msgtalklist.clear();
590 msgtalklist.length += 1;
591 msgtalklist[$-1].msg = msg;
592 msgtalklist[$-1].timeout = timeout;
593 msgtalklist[$-1].active = false;
594 msgtalklist[$-1].shopOnly = inShopOnly;
595 msgtalklist[$-1].hiColor1 = hiColor1;
596 msgtalklist[$-1].hiColor2 = hiColor2;
601 foreach (auto midx, ref auto mnfo; msgtalklist) {
602 if (mnfo.msg == msg) {
603 mnfo.timeout = timeout;
604 mnfo.shopOnly = inShopOnly;
609 msgtalklist.length += 1;
610 msgtalklist[$-1].msg = msg;
611 msgtalklist[$-1].timeout = timeout;
612 msgtalklist[$-1].active = false;
613 msgtalklist[$-1].shopOnly = inShopOnly;
614 msgtalklist[$-1].hiColor1 = hiColor1;
615 msgtalklist[$-1].hiColor2 = hiColor2;
622 // ////////////////////////////////////////////////////////////////////////// //
623 void setup (GameGlobal aGlobal, SpriteStore aSprStore, BackTileStore aBGTileStore) {
625 sprStore = aSprStore;
626 bgtileStore = aBGTileStore;
628 lg = SpawnObject(LevelGen);
632 objGrid = SpawnObject(EntityGrid);
633 objGrid.setup(0, 0, MaxTilesWidth*16, MaxTilesHeight*16);
637 // stores should be set
641 levBGImg = bgtileStore[levBGImgName];
642 foreach (MapEntity o; objGrid.allObjects()) {
645 if (t && (t.lava || t.water)) ++liquidTileCount;
647 for (MapBackTile bt = backtiles; bt; bt = bt.next) bt.onLoaded();
648 if (player) player.onLoaded();
650 if (msglist.length) {
651 msglist[0].active = false;
652 msglist[0].timeout = 0.200;
655 lastMusicName = (lg ? lg.musicName : '');
656 global.setMusicPitch(1.0);
657 if (lg && lg.musicName) global.playMusic(lg.musicName); else global.stopMusic();
661 // ////////////////////////////////////////////////////////////////////////// //
662 void pickedSpectacles () {
663 foreach (MapTile t; objGrid.allObjectsSafe(MapTile)) t.onGotSpectacles();
667 // ////////////////////////////////////////////////////////////////////////// //
668 #include "rgentile.vc"
669 #include "rgenobj.vc"
672 void onLevelExited () {
673 if (playerExitDoor isa TitleTileXTitle) {
674 playerExitDoor = none;
679 if (isTitleRoom() || levelKind == LevelKind.Scores) {
680 if (playerExitDoor) processTitleExit(playerExitDoor);
681 playerExitDoor = none;
684 if (isTutorialRoom()) {
685 playerExitDoor = none;
687 //global.currLevel = 1;
688 //generateNormalLevel();
689 global.currLevel = 0;
690 generateTransitionLevel();
694 if (levelKind == LevelKind.Stars || levelKind == LevelKind.Sun || levelKind == LevelKind.Moon) {
695 playerExitDoor = none;
697 if (onLevelExitedCB) onLevelExitedCB();
702 if (isNormalLevel()) {
703 stats.maxLevelComplete = max(stats.maxLevelComplete, global.currLevel);
705 if (playerExitDoor) {
706 if (playerExitDoor.objType == 'oXGold') {
707 writeln("exiting to City Of Gold");
708 global.cityOfGold = -1;
709 //!global.currLevel += 1;
710 } else if (playerExitDoor.objType == 'oXMarket') {
711 writeln("exiting to Black Market");
712 global.genBlackMarket = true;
713 //!global.currLevel += 1;
715 writeln("exit door(", GetClassName(playerExitDoor.Class), "): '", playerExitDoor.objType, "'");
718 writeln("WTF?! NO EXIT DOOR!");
721 if (onLevelExitedCB) onLevelExitedCB();
723 playerExitDoor = none;
724 if (levelKind == LevelKind.Transition) {
725 if (global.thiefLevel > 0) global.thiefLevel -= 1;
726 if (global.alienCraft) ++global.alienCraft;
727 if (global.yetiLair) ++global.yetiLair;
728 if (global.lake) ++global.lake;
729 if (global.cityOfGold) { if (++global.cityOfGold == 0) global.cityOfGold = 1; }
730 //orig dbg:if (global.currLevel == 1) global.currLevel += firstLevelSkip; else global.currLevel += levelSkip;
732 if (!global.blackMarket && !global.cityOfGold /*&& !global.yetiLair*/) {
733 global.currLevel += 1;
739 // < 20 seconds per level: looks like a speedrun
740 global.noDarkLevel = (levelEndTime > levelStartTime && levelEndTime-levelStartTime < 20*30);
741 if (lg.finalBossLevel) {
743 allowFinalCutsceneSkip = (stats.gamesWon != 0);
745 // add money for big idol
746 player.addScore(50000);
750 generateTransitionLevel();
753 //centerViewAtPlayer();
757 void onOlmecDead (MapObject o) {
758 writeln("*** OLMEC IS DEAD!");
759 foreach (MapTile t; allExits) {
762 auto st = checkTileAtPoint(t.ix+8, t.iy+16);
764 st = MakeMapTile(t.ix/16, t.iy/16+1, 'oTemple');
767 st.invincible = true;
773 void generateLevelMessages () {
774 writeln("LEVEL NUMBER: ", global.currLevel);
775 if (global.darkLevel) {
776 if (global.hasCrown) {
777 osdMessage("THE HEDJET SHINES BRIGHTLY.");
778 global.darkLevel = false;
779 } else if (global.config.scumDarkness < 2) {
780 osdMessage("I CAN'T SEE A THING!\nI'D BETTER USE THESE FLARES!");
784 if (global.blackMarket) osdMessage("WELCOME TO THE BLACK MARKET!");
786 if (global.cemetary) osdMessage("THE DEAD ARE RESTLESS!");
787 if (global.lake == 1) osdMessage("I CAN HEAR RUSHING WATER...");
789 if (global.snakePit) osdMessage("I HEAR SNAKES... I HATE SNAKES!");
790 if (global.yetiLair == 1) osdMessage("IT SMELLS LIKE WET FUR IN HERE!");
791 if (global.alienCraft == 1) osdMessage("THERE'S A PSYCHIC PRESENCE HERE!");
792 if (global.cityOfGold == 1) {
793 if (true /*hasOlmec()*/) osdMessage("IT'S THE LEGENDARY CITY OF GOLD!");
796 if (global.sacrificePit) osdMessage("I CAN HEAR PRAYERS TO KALI!");
800 final MapObject debugSpawnObjectWithClass (class!MapObject oclass, optional bool playerDir) {
801 if (!oclass) return none;
803 bool canLeft = !isSolidAtPoint(player.ix-8, player.iy);
804 bool canRight = !isSolidAtPoint(player.ix+16, player.iy);
805 if (!canLeft && !canRight) return none;
806 if (canLeft && canRight) {
808 dx = (player.dir == MapObject::Dir.Left ? -16 : 16);
813 dx = (canLeft ? -16 : 16);
815 auto obj = SpawnMapObjectWithClass(oclass);
816 if (obj isa MapEnemy) {
818 dy -= (obj isa MonsterDamsel ? 2 : 8);
820 if (obj) PutSpawnedMapObject(player.ix+dx, player.iy+dy, obj);
825 final MapObject debugSpawnObject (name aname) {
826 if (!aname) return none;
827 return debugSpawnObjectWithClass(findGameObjectClassByName(aname));
831 void createSpecialLevel (LevelKind kind, scope void delegate () creator, name amusic) {
832 global.darkLevel = false;
836 global.resetStartingItems();
838 global.setMusicPitch(1.0);
841 auto olddel = ImmediateDelete;
842 ImmediateDelete = false;
850 addBackgroundGfxDetails();
851 //levBGImgName = 'bgCave';
852 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
854 blockWaterChecking = true;
858 ImmediateDelete = olddel;
859 CollectGarbage(true); // destroy delayed objects too
861 if (dumpGridStats) objGrid.dumpStats();
863 playerExited = false; // just in case
864 playerExitDoor = none;
866 osdClear(clearTalk:true);
869 lg.musicName = amusic;
870 lastMusicName = amusic;
871 global.setMusicPitch(1.0);
872 if (amusic) global.playMusic(lg.musicName); else global.stopMusic();
876 void createTitleLevel () {
877 createSpecialLevel(LevelKind.Title, &createTitleRoom, 'musTitle');
881 void createTutorialLevel () {
882 createSpecialLevel(LevelKind.Tutorial, &createTutorialRoom, 'musCave');
891 // `global.currLevel` is the new level
892 void generateTransitionLevel () {
893 global.darkLevel = false;
898 resetTransitionOverlay();
900 global.setMusicPitch(1.0);
901 switch (global.config.transitionMusicMode) {
902 case GameConfig::MusicMode.Silent: global.stopMusic(); break;
903 case GameConfig::MusicMode.Restart: global.restartMusic(); break;
904 case GameConfig::MusicMode.DontTouch: break;
907 levelKind = LevelKind.Transition;
909 auto olddel = ImmediateDelete;
910 ImmediateDelete = false;
913 if (global.currLevel < 4) createTrans1Room();
914 else if (global.currLevel == 4) createTrans1xRoom();
915 else if (global.currLevel < 8) createTrans2Room();
916 else if (global.currLevel == 8) createTrans2xRoom();
917 else if (global.currLevel < 12) createTrans3Room();
918 else if (global.currLevel == 12) createTrans3xRoom();
919 else if (global.currLevel < 16) createTrans4Room();
920 else if (global.currLevel == 16) createTrans4Room();
921 else createTrans1Room(); //???
926 addBackgroundGfxDetails();
927 //levBGImgName = 'bgCave';
928 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
930 blockWaterChecking = true;
934 if (damselSaved > 0) {
935 // this is special "damsel ready to kiss you" object, not a heart
936 MakeMapObject(176+8, 176+8, 'oDamselKiss');
937 global.plife += damselSaved; // if player skipped transition cutscene
941 ImmediateDelete = olddel;
942 CollectGarbage(true); // destroy delayed objects too
944 if (dumpGridStats) objGrid.dumpStats();
946 playerExited = false; // just in case
947 playerExitDoor = none;
949 osdClear(clearTalk:true);
952 //global.playMusic(lg.musicName);
956 void generateLevel () {
957 levelStartTime = time;
963 global.genBlackMarket = false;
966 global.setMusicPitch(1.0);
967 stats.clearLevelTotals();
969 levelKind = LevelKind.Normal;
976 //writeln("tw:", tilesWidth, "; th:", tilesHeight);
978 auto olddel = ImmediateDelete;
979 ImmediateDelete = false;
982 if (lg.finalBossLevel) {
983 blockWaterChecking = true;
987 // if transition cutscene was skipped...
988 global.plife += max(0, damselSaved); // if player skipped transition cutscene
992 startRoomX = lg.startRoomX;
993 startRoomY = lg.startRoomY;
994 endRoomX = lg.endRoomX;
995 endRoomY = lg.endRoomY;
996 addBackgroundGfxDetails();
997 foreach (int y; 0..tilesHeight) {
998 foreach (int x; 0..tilesWidth) {
1004 levBGImgName = lg.bgImgName;
1005 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
1007 if (global.allowAngryShopkeepers) generateAngryShopkeepers();
1009 lg.generateEntities();
1011 // add box of flares to dark level
1012 if (global.darkLevel && allEnters.length) {
1013 auto enter = allEnters[0];
1014 int x = enter.ix, y = enter.iy;
1015 if (!isSolidAtPoint(x-16, y)) MakeMapObject(x-16+8, y+8, 'oFlareCrate');
1016 else if (!isSolidAtPoint(x+16, y)) MakeMapObject(x+16+8, y+8, 'oFlareCrate');
1017 else MakeMapObject(x+8, y+8, 'oFlareCrate');
1020 //scrGenerateEntities();
1021 //foreach (; 0..2) scrGenerateEntities();
1023 writeln(objGrid.countObjects, " alive objects inserted");
1024 writeln(countBackTiles, " background tiles inserted");
1026 if (!player) FatalError("player pawn is not spawned");
1028 if (lg.finalBossLevel) {
1029 blockWaterChecking = true;
1031 blockWaterChecking = false;
1036 ImmediateDelete = olddel;
1037 CollectGarbage(true); // destroy delayed objects too
1039 if (dumpGridStats) objGrid.dumpStats();
1041 playerExited = false; // just in case
1042 playerExitDoor = none;
1044 levelMoneyStart = stats.money;
1046 osdClear(clearTalk:true);
1047 generateLevelMessages();
1052 //writeln("!<", lastMusicName, ">:<", lg.musicName, ">");
1053 global.setMusicPitch(1.0);
1054 if (lastMusicName != lg.musicName) {
1055 global.playMusic(lg.musicName);
1057 //writeln("MM: ", global.config.nextLevelMusicMode);
1058 switch (global.config.nextLevelMusicMode) {
1059 case GameConfig::MusicMode.Silent: global.stopMusic(); break; // the thing that should not be
1060 case GameConfig::MusicMode.Restart: global.restartMusic(); break;
1061 case GameConfig::MusicMode.DontTouch:
1062 if (global.config.transitionMusicMode == GameConfig::MusicMode.Silent) {
1063 global.playMusic(lg.musicName);
1068 lastMusicName = lg.musicName;
1069 //global.playMusic(lg.musicName);
1072 if (global.cityOfGold == 1 || global.genBlackMarket) resetBMCOG = true;
1074 if (global.cityOfGold == 1) {
1075 lg.mapSprite = 'sMapTemple';
1076 lg.mapTitle = "City of Gold";
1077 } else if (global.blackMarket) {
1078 lg.mapSprite = 'sMapJungle';
1079 lg.mapTitle = "Black Market";
1084 // ////////////////////////////////////////////////////////////////////////// //
1085 int currKeys, nextKeys;
1086 int pressedKeysQ, releasedKeysQ;
1087 int keysPressed, keysReleased = -1;
1090 struct SavedKeyState {
1091 int currKeys, nextKeys;
1092 int pressedKeysQ, releasedKeysQ;
1093 int keysPressed, keysReleased;
1095 int roomSeed, otherSeed;
1099 // for saving/replaying
1100 final void keysSaveState (out SavedKeyState ks) {
1101 ks.currKeys = currKeys;
1102 ks.nextKeys = nextKeys;
1103 ks.pressedKeysQ = pressedKeysQ;
1104 ks.releasedKeysQ = releasedKeysQ;
1105 ks.keysPressed = keysPressed;
1106 ks.keysReleased = keysReleased;
1109 // for saving/replaying
1110 final void keysRestoreState (const ref SavedKeyState ks) {
1111 currKeys = ks.currKeys;
1112 nextKeys = ks.nextKeys;
1113 pressedKeysQ = ks.pressedKeysQ;
1114 releasedKeysQ = ks.releasedKeysQ;
1115 keysPressed = ks.keysPressed;
1116 keysReleased = ks.keysReleased;
1120 final void keysNextFrame () {
1121 currKeys = nextKeys;
1125 final void clearKeys () {
1135 final void onKey (int code, bool down) {
1140 if (keysReleased&code) {
1141 keysPressed |= code;
1142 keysReleased &= ~code;
1143 pressedKeysQ |= code;
1147 if (keysPressed&code) {
1148 keysReleased |= code;
1149 keysPressed &= ~code;
1150 releasedKeysQ |= code;
1155 final bool isKeyDown (int code) {
1156 return !!(currKeys&code);
1159 final bool isKeyPressed (int code) {
1160 bool res = !!(pressedKeysQ&code);
1161 pressedKeysQ &= ~code;
1165 final bool isKeyReleased (int code) {
1166 bool res = !!(releasedKeysQ&code);
1167 releasedKeysQ &= ~code;
1172 final void clearKeysPressRelease () {
1173 keysPressed = default.keysPressed;
1174 keysReleased = default.keysReleased;
1175 pressedKeysQ = default.pressedKeysQ;
1176 releasedKeysQ = default.releasedKeysQ;
1182 // ////////////////////////////////////////////////////////////////////////// //
1183 final void registerEnter (MapTile t) {
1190 final void registerExit (MapTile t) {
1197 final bool isYAtEntranceRow (int py) {
1199 foreach (MapTile t; allEnters) if (t.iy == py) return true;
1204 final int calcNearestEnterDist (int px, int py) {
1205 if (allEnters.length == 0) return int.max;
1206 int curdistsq = int.max;
1207 foreach (MapTile t; allEnters) {
1208 int xc = px-t.xCenter, yc = py-t.yCenter;
1209 int distsq = xc*xc+yc*yc;
1210 if (distsq < curdistsq) curdistsq = distsq;
1212 return round(sqrt(curdistsq));
1216 final int calcNearestExitDist (int px, int py) {
1217 if (allExits.length == 0) return int.max;
1218 int curdistsq = int.max;
1219 foreach (MapTile t; allExits) {
1220 int xc = px-t.xCenter, yc = py-t.yCenter;
1221 int distsq = xc*xc+yc*yc;
1222 if (distsq < curdistsq) curdistsq = distsq;
1224 return round(sqrt(curdistsq));
1228 // ////////////////////////////////////////////////////////////////////////// //
1229 final void clearForTransition () {
1230 auto olddel = ImmediateDelete;
1231 ImmediateDelete = false;
1233 ImmediateDelete = olddel;
1234 CollectGarbage(true); // destroy delayed objects too
1235 global.darkLevel = false;
1239 // ////////////////////////////////////////////////////////////////////////// //
1240 final int countBackTiles () {
1242 for (MapBackTile bt = backtiles; bt; bt = bt.next) ++res;
1247 final void clearWholeLevel () {
1251 // don't kill objects the player is holding
1253 if (player.pickedItem isa ItemBall) {
1254 player.pickedItem.instanceRemove();
1255 player.pickedItem = none;
1257 if (player.pickedItem && player.pickedItem.grid) {
1258 player.pickedItem.grid.remove(player.pickedItem.gridId);
1259 writeln("secured pocket item '", GetClassName(player.pickedItem.Class), "'");
1261 if (player.holdItem isa ItemBall) {
1262 player.removeBallAndChain(temp:true);
1263 if (player.holdItem) player.holdItem.instanceRemove();
1264 player.holdItem = none;
1266 if (player.holdItem && player.holdItem.grid) {
1267 player.holdItem.grid.remove(player.holdItem.gridId);
1268 writeln("secured held item '", GetClassName(player.holdItem.Class), "'");
1270 writeln("secured ball; mustBeChained=", player.mustBeChained, "; wasHoldingBall=", player.wasHoldingBall);
1273 int count = objGrid.countObjects();
1274 if (dumpGridStats) { if (objGrid.getFirstObjectCID()) objGrid.dumpStats(); }
1275 objGrid.removeAllObjects(true); // and destroy
1276 if (count > 0) writeln(count, " objects destroyed");
1278 lastUsedObjectId = 0;
1281 lastRenderTime = -1;
1282 liquidTileCount = 0;
1286 MapBackTile t = backtiles;
1292 framesProcessedFromLastClear = 0;
1296 final void insertObject (MapEntity o) {
1298 if (o.grid) FatalError("cannot put object into level twice");
1303 final void spawnPlayerAt (int x, int y) {
1304 // if we have no player, spawn new one
1305 // otherwise this just a level transition, so simply reposition him
1307 // don't add player to object list, as it has very separate processing anyway
1308 player = SpawnObject(PlayerPawn);
1309 player.global = global;
1310 player.level = self;
1311 if (!player.initialize()) {
1313 FatalError("something is wrong with player initialization");
1319 player.saveInterpData();
1321 if (player.mustBeChained || global.config.scumBallAndChain) {
1322 writeln("*** spawning ball and chain");
1323 player.spawnBallAndChain(levelStart:true);
1325 playerExited = false;
1326 playerExitDoor = none;
1327 if (global.config.startWithKapala) global.hasKapala = true;
1328 centerViewAtPlayer();
1329 // reinsert player items into grid
1330 if (player.pickedItem) objGrid.insert(player.pickedItem);
1331 if (player.holdItem) objGrid.insert(player.holdItem);
1332 //writeln("player spawned; active=", player.active);
1333 player.scrSwitchToPocketItem(forceIfEmpty:false);
1337 final void teleportPlayerTo (int x, int y) {
1341 player.saveInterpData();
1346 final void resurrectPlayer () {
1347 if (player) player.resurrect();
1348 playerExited = false;
1349 playerExitDoor = none;
1353 // ////////////////////////////////////////////////////////////////////////// //
1354 final void scrShake (int duration) {
1355 if (shakeLeft == 0) {
1361 shakeLeft = max(shakeLeft, duration);
1366 // ////////////////////////////////////////////////////////////////////////// //
1369 ItemStolen, // including damsel, lol
1375 // checks for dead, agnered, distance, etc. should be already done
1376 protected void doAngerShopkeeper (MonsterShopkeeper shp, SCAnger reason, ref bool messaged,
1377 int maxdist, MapEntity offender)
1379 if (!shp || shp.dead || shp.angered) return;
1380 if (offender.distanceToEntityCenter(shp) > maxdist) return;
1382 shp.status = MapObject::ATTACK;
1384 if (global.murderer) {
1385 msg = "~YOU'LL PAY FOR YOUR CRIMES!~";
1388 case SCAnger.TileDestroyed: msg = "~DIE, YOU VANDAL!~"; break;
1389 case SCAnger.ItemStolen: msg = "~COME BACK HERE, THIEF!~"; break;
1390 case SCAnger.CrapsCheated: msg = "~DIE, CHEATER!~"; break;
1391 case SCAnger.BombDropped: msg = "~TERRORIST!~"; break;
1392 case SCAnger.DamselWhipped: msg = "~HEY, ONLY I CAN DO THAT!~"; break;
1393 default: "~NOW I'M REALLY STEAMED!~"; break;
1397 writeln("shopkeeper angered; reason=", reason, "; maxdist=", maxdist, "; msg=\"", msg, "\"");
1400 if (msg) osdMessageTalk(msg, replace:true, inShopOnly:false, hiColor1:0xff_00_00);
1401 global.thiefLevel += (global.thiefLevel > 0 ? 3 : 2);
1406 // make the nearest shopkeeper angry. RAWR!
1407 void scrShopkeeperAnger (SCAnger reason, optional int maxdist, optional MapEntity offender) {
1408 bool messaged = false;
1409 maxdist = clamp(maxdist, 96, 100000);
1410 if (!offender) offender = player;
1411 if (maxdist == 100000) {
1412 foreach (MonsterShopkeeper shp; objGrid.allObjects(MonsterShopkeeper)) {
1413 doAngerShopkeeper(shp, reason, messaged, maxdist, offender);
1416 foreach (MonsterShopkeeper shp; objGrid.inRectPix(offender.xCenter-maxdist-128, offender.yCenter-maxdist-128, (maxdist+128)*2, (maxdist+128)*2, precise:false, castClass:MonsterShopkeeper)) {
1417 doAngerShopkeeper(shp, reason, messaged, maxdist, offender);
1423 final MapObject findCrapsPrize () {
1424 foreach (MapObject o; objGrid.allObjects(MapObject)) {
1425 if (!o.spectral && o.inDiceHouse) return o;
1431 // ////////////////////////////////////////////////////////////////////////// //
1432 // 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.
1433 // note: idols moved by monkeys will have false `stolenIdol`
1434 void scrTriggerIdolAltar (bool stolenIdol) {
1435 ObjTikiCurse res = none;
1436 int curdistsq = int.max;
1437 int px = player.xCenter, py = player.yCenter;
1438 foreach (MapObject o; objGrid.allObjects(MapObject)) {
1439 auto tcr = ObjTikiCurse(o);
1441 if (tcr.activated) continue;
1442 int xc = px-tcr.xCenter, yc = py-tcr.yCenter;
1443 int distsq = xc*xc+yc*yc;
1444 if (distsq < curdistsq) {
1449 if (res) res.activate(stolenIdol);
1453 // ////////////////////////////////////////////////////////////////////////// //
1454 void setupGhostTime () {
1455 musicFadeTimer = -1;
1456 ghostSpawned = false;
1458 // there is no ghost on the first level
1459 if (inWinCutscene || inIntroCutscene || !isNormalLevel() || lg.finalBossLevel ||
1460 (!global.config.ghostAtFirstLevel && global.currLevel == 1))
1463 global.setMusicPitch(1.0);
1467 if (global.config.scumGhost < 0) {
1470 osdMessage("A GHASTLY VOICE CALLS YOUR NAME!\nLET'S GET OUT OF HERE!", 6.66, urgent:true);
1474 if (global.config.scumGhost == 0) {
1480 // randomizes time until ghost appears once time limit is reached
1481 // global.ghostRandom (true/false) if extra time is randomized - loaded from yasm.cfg. if false, use 30 second default
1482 // ghostTimeLeft (time in seconds * 1000) for currently generated level
1484 if (global.config.ghostRandom) {
1485 auto tMin = global.randOther(10, 30); // min(global.plife*10, 30);
1486 auto tMax = max(tMin+global.randOther(10, 30), min(global.config.scumGhost, 300)); // 5 minutes max
1487 auto tTime = global.randOther(tMin, tMax);
1488 if (tTime <= 0) tTime = round(tMax/2.0);
1489 ghostTimeLeft = tTime;
1491 ghostTimeLeft = global.config.scumGhost; // 5 minutes max
1494 ghostTimeLeft += max(0, global.config.ghostExtraTime);
1496 ghostTimeLeft *= 30; // seconds -> frames
1497 //global.ghostShowTime
1501 void spawnGhost () {
1503 ghostSpawned = true;
1506 int vwdt = (viewMax.x-viewMin.x);
1507 int vhgt = (viewMax.y-viewMin.y);
1511 if (player.ix < viewMin.x+vwdt/2) {
1512 // player is in the left side
1513 gx = viewMin.x+vwdt/2+vwdt/4;
1515 // player is in the right side
1516 gx = viewMin.x+vwdt/4;
1519 if (player.iy < viewMin.y+vhgt/2) {
1520 // player is in the left side
1521 gy = viewMin.y+vhgt/2+vhgt/4;
1523 // player is in the right side
1524 gy = viewMin.y+vhgt/4;
1527 writeln("spawning ghost at tile (", gx/16, ",", gy/16, "); player is at tile (", player.ix/16, ",", player.iy/16, ")");
1529 MakeMapObject(gx, gy, 'oGhost');
1532 if (oPlayer1.x > room_width/2) instance_create(view_xview[0]+view_wview[0]+8, view_yview[0]+floor(view_hview[0] / 2), oGhost);
1533 else instance_create(view_xview[0]-32, view_yview[0]+floor(view_hview[0]/2), oGhost);
1534 global.ghostExists = true;
1539 void thinkFrameGameGhost () {
1540 if (player.dead) return;
1541 if (!isNormalLevel()) return; // just in case
1543 if (ghostTimeLeft < 0) {
1545 if (musicFadeTimer > 0) {
1546 musicFadeTimer = -1;
1547 global.setMusicPitch(1.0);
1552 if (musicFadeTimer >= 0) {
1554 if (musicFadeTimer > 0 && musicFadeTimer <= 100*3) {
1555 float pitch = 1.0-(0.00226/3.0)*musicFadeTimer;
1556 //writeln("MFT: ", musicFadeTimer, "; pitch=", pitch);
1557 global.setMusicPitch(pitch);
1561 if (ghostTimeLeft == 0) {
1562 // she is already here!
1566 // no ghost if we have a crown
1567 if (global.hasCrown) {
1572 // if she was already spawned, don't do it again
1578 if (--ghostTimeLeft != 0) {
1580 if (global.config.ghostExtraTime > 0) {
1581 if (ghostTimeLeft == global.config.ghostExtraTime*30) {
1582 osdMessage("A CHILL RUNS UP YOUR SPINE...\nLET'S GET OUT OF HERE!", 6.66, urgent:true);
1584 if (musicFadeTimer < 0 && ghostTimeLeft <= global.config.ghostExtraTime*30) {
1592 if (player.isExitingSprite) {
1593 // no reason to spawn her, we're leaving
1602 void thinkFrameGame () {
1603 thinkFrameGameGhost();
1604 // udjat eye blinking
1605 if (global.hasUdjatEye && player) {
1606 foreach (MapTile t; allExits) {
1607 if (t isa MapTileBlackMarketDoor) {
1608 auto dm = int(player.distanceToEntity(t));
1610 if (udjatAlarm < 1 || dm < udjatAlarm) udjatAlarm = dm;
1614 global.udjatBlink = false;
1617 if (udjatAlarm > 0) {
1618 if (--udjatAlarm == 0) {
1619 global.udjatBlink = !global.udjatBlink;
1620 if (global.hasUdjatEye && player) {
1621 player.playSound(global.udjatBlink ? 'sndBlink1' : 'sndBlink2');
1625 switch (levelKind) {
1626 case LevelKind.Stars: thinkFrameGameStars(); break;
1627 case LevelKind.Sun: thinkFrameGameSun(); break;
1628 case LevelKind.Moon: thinkFrameGameMoon(); break;
1629 case LevelKind.Transition: thinkFrameTransition(); break;
1630 case LevelKind.Intro: thinkFrameIntro(); break;
1635 // ////////////////////////////////////////////////////////////////////////// //
1636 private final bool isWaterTileCB (MapTile t) {
1637 return (t && t.visible && t.water);
1641 private final bool isLavaTileCB (MapTile t) {
1642 return (t && t.visible && t.lava);
1646 // ////////////////////////////////////////////////////////////////////////// //
1647 const int GreatLakeStartTileY = 28;
1650 final void fillGreatLake () {
1651 if (global.lake == 1) {
1652 foreach (int y; GreatLakeStartTileY..tilesHeight) {
1653 foreach (int x; 0..tilesWidth) {
1654 auto t = checkTileAtPoint(x*16, y*16, delegate bool (MapTile t) {
1655 if (t.spectral || !t.visible || t.invisible || t.moveable) return false;
1659 t = MakeMapTile(x, y, 'oWaterSwim');
1663 t.setSprite(y == GreatLakeStartTileY ? 'sWaterTop' : 'sWater');
1664 } else if (t.lava) {
1665 t.setSprite(y == GreatLakeStartTileY ? 'sLavaTop' : 'sLava');
1673 // called once after level generation
1674 final void fixLiquidTop () {
1675 if (global.lake == 1) fillGreatLake();
1677 liquidTileCount = 0;
1678 foreach (MapTile t; objGrid.allObjects(MapTile)) {
1679 if (!t.water && !t.lava) continue;
1682 //writeln("fixing water tile(", GetClassName(t.Class), "):'", t.objName, "' (water=", t.water, "; lava=", t.lava, "); lqc=", liquidTileCount);
1684 //if (global.lake == 1) continue; // it is done in `fillGreatLake()`
1686 if (!checkTileAtPoint(t.ix+8, t.iy-8, (t.lava ? &isLavaTileCB : &isWaterTileCB))) {
1687 t.setSprite(t.lava ? 'sLavaTop' : 'sWaterTop');
1689 // don't do this, it will destroy seaweed
1690 //t.setSprite(t.lava ? 'sLava' : 'sWater');
1691 auto spr = t.getSprite();
1692 if (!spr) t.setSprite(t.lava ? 'sLava' : 'sWater');
1693 else if (spr.Name == 'sLavaTop') t.setSprite('sLava');
1694 else if (spr.Name == 'sWaterTop') t.setSprite('sWater');
1697 //writeln("liquid tiles count: ", liquidTileCount);
1701 // ////////////////////////////////////////////////////////////////////////// //
1702 transient MapTile curWaterTile;
1703 transient bool curWaterTileCheckHitsLava;
1704 transient bool curWaterTileCheckHitsSolidOrWater; // only for `checkWaterOrSolidTilePartialCB`
1705 transient int curWaterTileLastHDir;
1706 transient ubyte[16, 16] curWaterOccupied;
1707 transient int curWaterOccupiedCount;
1708 transient int curWaterTileCheckX0, curWaterTileCheckY0;
1711 private final void clearCurWaterCheckState () {
1712 curWaterTileCheckHitsLava = false;
1713 curWaterOccupiedCount = 0;
1714 foreach (auto idx; 0..16*16) curWaterOccupied[idx] = 0;
1718 private final bool checkWaterOrSolidTileCB (MapTile t) {
1719 if (t == curWaterTile) return false;
1720 if (t.lava && curWaterTile.water) {
1721 curWaterTileCheckHitsLava = true;
1724 if (t.ix%16 != 0 || t.iy%16 != 0) {
1725 if (t.water || t.solid) {
1726 // fill occupied array
1727 //FIXME: optimize this
1728 if (curWaterOccupiedCount < 16*16) {
1729 foreach (auto dy; t.y0..t.y1+1) {
1730 foreach (auto dx; t.x0..t.x1+1) {
1731 int sx = dx-curWaterTileCheckX0;
1732 int sy = dy-curWaterTileCheckY0;
1733 if (sx >= 0 && sx <= 16 && sy >= 0 && sy <= 15 && !curWaterOccupied[sx, sy]) {
1734 curWaterOccupied[sx, sy] = 1;
1735 ++curWaterOccupiedCount;
1741 return false; // need to check for lava
1743 if (t.water || t.solid || t.lava) {
1744 curWaterOccupiedCount = 16*16;
1745 if (t.water && curWaterTile.lava) t.instanceRemove();
1747 return false; // need to check for lava
1751 private final bool checkWaterOrSolidTilePartialCB (MapTile t) {
1752 if (t == curWaterTile) return false;
1753 if (t.lava && curWaterTile.water) {
1754 //writeln("!!!!!!!!");
1755 curWaterTileCheckHitsLava = true;
1758 if (t.water || t.solid || t.lava) {
1759 //writeln("*********");
1760 curWaterTileCheckHitsSolidOrWater = true;
1761 if (t.water && curWaterTile.lava) t.instanceRemove();
1763 return false; // need to check for lava
1767 private final bool isFullyOccupiedAtTilePos (int tileX, int tileY) {
1768 clearCurWaterCheckState();
1769 curWaterTileCheckX0 = tileX*16;
1770 curWaterTileCheckY0 = tileY*16;
1771 checkTilesInRect(tileX*16, tileY*16, 16, 16, &checkWaterOrSolidTileCB);
1772 return (curWaterTileCheckHitsLava || curWaterOccupiedCount == 16*16);
1776 private final bool isAtLeastPartiallyOccupiedAtTilePos (int tileX, int tileY) {
1777 curWaterTileCheckHitsLava = false;
1778 curWaterTileCheckHitsSolidOrWater = false;
1779 checkTilesInRect(tileX*16, tileY*16, 16, 16, &checkWaterOrSolidTilePartialCB);
1780 return (curWaterTileCheckHitsSolidOrWater || curWaterTileCheckHitsLava);
1784 private final bool waterCanReachGroundHoleInDir (MapTile wtile, int dx) {
1785 if (dx == 0) return false; // just in case
1787 int x = wtile.ix/16, y = wtile.iy/16;
1789 while (x >= 0 && x < tilesWidth) {
1790 if (!isAtLeastPartiallyOccupiedAtTilePos(x, y+1)) return true;
1791 if (isAtLeastPartiallyOccupiedAtTilePos(x, y)) return false;
1798 // returns `true` if this tile must be removed
1799 private final bool checkWaterFlow (MapTile wtile) {
1800 if (global.lake == 1) {
1801 if (wtile.iy >= GreatLakeStartTileY*16) return false; // lake tile, don't touch
1802 if (wtile.iy >= GreatLakeStartTileY*16-16) return true; // remove it, so it won't stack on a lake
1805 if (wtile.ix%16 != 0 || wtile.iy%16 != 0) return true; // sanity check
1807 curWaterTile = wtile;
1808 curWaterTileLastHDir = 0; // never moved to the side
1810 bool wasMoved = false;
1813 int tileX = wtile.ix/16, tileY = wtile.iy/16;
1816 if (tileY >= tilesHeight) return true;
1818 // check if we can fall down
1819 auto canFall = !isAtLeastPartiallyOccupiedAtTilePos(tileX, tileY+1);
1820 // disappear if can fall in lava
1821 if (wtile.water && curWaterTileCheckHitsLava) {
1822 //!writeln(wtile.objId, ": LAVA HIT DOWN");
1826 // fake, so caller will not start removing tiles
1827 if (canFall) wtile.waterMovedDown = true;
1833 //!writeln(wtile.objId, ": GOING DOWN");
1834 curWaterTileLastHDir = 0;
1835 wtile.iy = wtile.iy+16;
1837 wtile.waterMovedDown = true;
1841 bool canMoveLeft = (curWaterTileLastHDir > 0 ? false : !isAtLeastPartiallyOccupiedAtTilePos(tileX-1, tileY));
1842 // disappear if near lava
1843 if (wtile.water && curWaterTileCheckHitsLava) {
1844 //!writeln(wtile.objId, ": LAVA HIT LEFT");
1848 bool canMoveRight = (curWaterTileLastHDir < 0 ? false : !isAtLeastPartiallyOccupiedAtTilePos(tileX+1, tileY));
1849 // disappear if near lava
1850 if (wtile.water && curWaterTileCheckHitsLava) {
1851 //!writeln(wtile.objId, ": LAVA HIT RIGHT");
1855 if (!canMoveLeft && !canMoveRight) {
1857 //!if (wasMove) writeln(wtile.objId, ": NO MORE MOVES");
1861 if (canMoveLeft && canMoveRight) {
1862 // choose random direction
1863 //!writeln(wtile.objId, ": CHOOSING RANDOM HDIR");
1864 // actually, choose direction that leads to hole in a ground
1865 if (waterCanReachGroundHoleInDir(wtile, -1)) {
1866 // can reach hole at the left side
1867 if (waterCanReachGroundHoleInDir(wtile, 1)) {
1868 // can reach hole at the right side, choose at random
1869 if (global.randOther(0, 1)) canMoveRight = false; else canMoveLeft = false;
1872 canMoveRight = false;
1875 // can't reach hole at the left side
1876 if (waterCanReachGroundHoleInDir(wtile, 1)) {
1877 // can reach hole at the right side, choose at random
1878 canMoveLeft = false;
1880 // no holes at any side, choose at random
1881 if (global.randOther(0, 1)) canMoveRight = false; else canMoveLeft = false;
1888 if (canMoveRight) FatalError("WATERCHECK: WTF RIGHT");
1889 //!writeln(wtile.objId, ": MOVING LEFT (", curWaterTileLastHDir, ")");
1890 curWaterTileLastHDir = -1;
1891 wtile.ix = wtile.ix-16;
1892 } else if (canMoveRight) {
1893 if (canMoveLeft) FatalError("WATERCHECK: WTF LEFT");
1894 //!writeln(wtile.objId, ": MOVING RIGHT (", curWaterTileLastHDir, ")");
1895 curWaterTileLastHDir = 1;
1896 wtile.ix = wtile.ix+16;
1904 wtile.setSprite(wtile.lava ? 'sLava' : 'sWater');
1905 wtile.waterMoved = true;
1906 // if this tile was not moved down, check if it can move down on any next step
1907 if (!wtile.waterMovedDown) {
1908 if (waterCanReachGroundHoleInDir(wtile, -1)) wtile.waterMovedDown = true;
1909 else if (waterCanReachGroundHoleInDir(wtile, 1)) wtile.waterMovedDown = true;
1913 return false; // don't remove
1915 //if (!isWetTileAtPix(tileX*16+8, tileY*16-8)) wtile.setSprite(wtile.lava ? 'sLavaTop' : 'sWaterTop');
1919 transient array!MapTile waterTilesList;
1921 final bool sortWaterTilesByCoordsLess (MapTile a, MapTile b) {
1923 if (dy) return (dy < 0);
1924 return (a.ix < b.ix);
1927 transient int waterFlowPause = 0;
1928 transient bool debugWaterFlowPause = false;
1930 final void cleanDeadObjects () {
1931 // remove dead objects
1932 if (deadItemsHead) {
1933 auto olddel = ImmediateDelete;
1934 ImmediateDelete = false;
1936 auto it = deadItemsHead;
1937 deadItemsHead = it.deadItemsNext;
1938 if (it.grid) it.grid.remove(it.gridId);
1941 } while (deadItemsHead);
1942 ImmediateDelete = olddel;
1943 if (olddel) CollectGarbage(true); // destroy delayed objects too
1947 final void cleanDeadTiles () {
1948 if (checkWater && /*global.lake == 1 ||*/ (!blockWaterChecking && liquidTileCount)) {
1949 if (global.lake == 1) fillGreatLake();
1950 if (waterFlowPause > 1) {
1955 if (debugWaterFlowPause) waterFlowPause = 4;
1956 //writeln("checking water");
1957 waterTilesList.clear();
1958 foreach (MapTile wtile; objGrid.allObjectsSafe(MapTile)) {
1959 if (wtile.water || wtile.lava) {
1961 if (wtile.ix%16 == 0 && wtile.iy%16 == 0) {
1962 wtile.waterMoved = false;
1963 wtile.waterMovedDown = false;
1964 wtile.waterSlideOldX = wtile.ix;
1965 wtile.waterSlideOldY = wtile.iy;
1966 waterTilesList[$] = wtile;
1971 liquidTileCount = 0;
1972 waterTilesList.sort(&sortWaterTilesByCoordsLess);
1974 bool wasAnyMove = false;
1975 bool wasAnyMoveDown = false;
1976 foreach (MapTile wtile; waterTilesList) {
1977 if (!wtile || !wtile.isInstanceAlive) continue;
1978 auto killIt = checkWaterFlow(wtile);
1982 wtile.instanceRemove(); // just in case
1984 wtile.saveInterpData();
1986 wasAnyMove = wasAnyMove || wtile.waterMoved;
1987 wasAnyMoveDown = wasAnyMoveDown || wtile.waterMovedDown;
1988 if (wtile.waterMoved && debugWaterFlowPause) wtile.waterSlideCounter = 4;
1992 liquidTileCount = 0;
1993 foreach (MapTile wtile; waterTilesList) {
1994 if (!wtile || !wtile.isInstanceAlive) continue;
1995 if (wasAnyMoveDown) {
1999 //checkWater = checkWater || wtile.waterMoved;
2000 curWaterTile = wtile;
2001 int tileX = wtile.ix/16, tileY = wtile.iy/16;
2002 // check if we are have no way to leak
2003 bool killIt = false;
2004 if (!isFullyOccupiedAtTilePos(tileX-1, tileY) || (wtile.water && curWaterTileCheckHitsLava)) {
2005 //writeln(" LEFT DEATH (", curWaterOccupiedCount, " : ", curWaterTileCheckHitsLava, ")");
2008 if (!killIt && (!isFullyOccupiedAtTilePos(tileX+1, tileY) || (wtile.water && curWaterTileCheckHitsLava))) {
2009 //writeln(" RIGHT DEATH (", curWaterOccupiedCount, " : ", curWaterTileCheckHitsLava, ")");
2012 if (!killIt && (!isFullyOccupiedAtTilePos(tileX, tileY+1) || (wtile.water && curWaterTileCheckHitsLava))) {
2013 //writeln(" DOWN DEATH (", curWaterOccupiedCount, " : ", curWaterTileCheckHitsLava, ")");
2020 wtile.instanceRemove(); // just in case
2025 if (wasAnyMove) checkWater = true;
2026 //writeln("water check: liquidTileCount=", liquidTileCount, "; checkWater=", checkWater, "; wasAnyMove=", wasAnyMove, "; wasAnyMoveDown=", wasAnyMoveDown);
2028 // fill empty spaces in lake with water
2036 // ////////////////////////////////////////////////////////////////////////// //
2037 private transient array!MapEntity postponedThinkers;
2038 private transient MapEntity thinkerHeld;
2039 private transient array!MapEntity activeThinkerList;
2042 final void doThinkActionsForObject (MapEntity o) {
2043 if (o.justSpawned) o.justSpawned = false;
2044 else if (o.imageSpeed > 0) o.nextAnimFrame();
2047 if (o.isInstanceAlive) {
2050 if (o.isInstanceAlive) {
2051 if (o.whipTimer > 0) --o.whipTimer;
2053 auto obj = MapObject(o);
2054 if (!o.canLiveOutsideOfLevel && (!obj || !obj.heldBy) && o.isOutsideOfLevel()) {
2055 // oops, fallen out of level...
2063 // return `true` if thinker should be removed
2064 final void thinkOne (MapEntity o, optional bool doHeldObject, optional bool dontAddHeldObject) {
2066 if (o == thinkerHeld && !doHeldObject) return; // skip it
2068 if (!o.isInstanceAlive) return;
2070 if (!o.active) return;
2072 auto obj = MapObject(o);
2074 if (obj && obj.heldBy == player) {
2075 // fix held item coords
2076 obj.fixHoldCoords();
2078 doThinkActionsForObject(o);
2080 if (!dontAddHeldObject) {
2082 foreach (MapEntity e; postponedThinkers) if (e == o) { found = true; break; }
2083 if (!found) postponedThinkers[$] = o;
2089 bool doThink = true;
2091 // collision with player weapon
2092 auto hh = PlayerWeapon(player.holdItem);
2093 bool doWeaponAction = false;
2095 if (hh.blockedBySolids && !global.config.killEnemiesThruWalls) {
2096 int xx = player.ix+(player.dir == MapObject::Dir.Left ? -16 : 16);
2097 //doWeaponAction = !isSolidAtPoint(xx, player.iy);
2098 doWeaponAction = !isSolidAtPoint(xx, hh.yCenter);
2100 int dh = max(1, hh.height-2);
2101 doWeaponAction = !checkTilesInRect(player.ix, player.iy);
2104 doWeaponAction = true;
2108 if (obj && doWeaponAction && hh && (o.whipTimer <= 0 || hh.ignoreWhipTimer) && hh.collidesWithObject(obj)) {
2109 //writeln("WEAPONED!");
2110 //writeln("weapon collides with '", GetClassName(o.Class), "' (", o.objType, "'");
2111 bool dontChangeWhipTimer = hh.dontChangeWhipTimer;
2112 if (!o.onTouchedByPlayerWeapon(player, hh)) {
2113 if (o.isInstanceAlive) hh.onCollisionWithObject(obj);
2115 if (!dontChangeWhipTimer) o.whipTimer = o.whipTimerValue; //HACK
2116 doThink = o.isInstanceAlive;
2119 if (doThink && o.isInstanceAlive) {
2120 doThinkActionsForObject(o);
2121 doThink = o.isInstanceAlive;
2124 // collision with player
2125 if (doThink && obj && o.collidesWith(player)) {
2126 if (!player.onObjectTouched(obj) && o.isInstanceAlive) {
2127 doThink = !o.onTouchedByPlayer(player);
2134 final void processThinkers (float timeDelta) {
2135 if (timeDelta <= 0) return;
2138 if (onBeforeFrame) onBeforeFrame(false);
2139 if (onAfterFrame) onAfterFrame(false);
2145 accumTime += timeDelta;
2146 bool wasFrame = false;
2148 auto olddel = ImmediateDelete;
2149 ImmediateDelete = false;
2150 while (accumTime >= FrameTime) {
2151 bool solidObjectSeen = false;
2152 postponedThinkers.clear();
2154 accumTime -= FrameTime;
2155 if (onBeforeFrame) onBeforeFrame(accumTime >= FrameTime);
2157 if (shakeLeft > 0) {
2159 if (!shakeDir.x) shakeDir.x = (global.randOther(0, 1) ? -3 : 3);
2160 if (!shakeDir.y) shakeDir.y = (global.randOther(0, 1) ? -3 : 3);
2161 shakeOfs.x = shakeDir.x;
2162 shakeOfs.y = shakeDir.y;
2163 int sgnc = global.randOther(1, 3);
2164 if (sgnc&0x01) shakeDir.x = -shakeDir.x;
2165 if (sgnc&0x02) shakeDir.y = -shakeDir.y;
2174 // we don't want the time to grow too large
2175 if (time < 0) { time = 0; lastRenderTime = -1; }
2176 // game-global events
2178 // frame thinkers: player
2179 if (player && !disablePlayerThink) {
2181 if (!player.dead && isNormalLevel() &&
2182 (maxPlayingTime < 0 ||
2183 (maxPlayingTime > 0 && maxPlayingTime*30 <= time &&
2184 time%30 == 0 && global.randOther(1, 100) <= 20)))
2186 global.hasAnkh = false;
2188 player.invincible = 0;
2189 auto xplo = MapObjExplosion(MakeMapObject(player.ix, player.iy, 'oExplosion'));
2190 if (xplo) xplo.suicide = true;
2192 //HACK: check for stolen items
2193 auto item = MapItem(player.holdItem);
2194 if (item) item.onCheckItemStolen(player);
2195 item = MapItem(player.pickedItem);
2196 if (item) item.onCheckItemStolen(player);
2198 doThinkActionsForObject(player);
2200 // frame thinkers: held object
2201 thinkerHeld = player.holdItem;
2202 if (thinkerHeld && thinkerHeld.isInstanceAlive) {
2203 if (thinkerHeld.active) {
2204 thinkOne(thinkerHeld, doHeldObject:true);
2205 if (!thinkerHeld.isInstanceAlive) {
2206 if (player.holdItem == thinkerHeld) player.holdItem = none;
2207 thinkerHeld.grid.remove(thinkerHeld.gridId);
2211 auto item = MapItem(thinkerHeld);
2213 if (item.forSale || item.sellOfferDone) {
2214 if (++item.forSaleFrame < 0) item.forSaleFrame = 0;
2219 // frame thinkers: objects
2220 activeThinkerList.clear();
2221 auto grid = objGrid;
2222 // collect active objects
2223 if (global.config.useFrozenRegion) {
2224 foreach (MapEntity e; grid.inRectPix(viewStart.x/global.scale-64, viewStart.y/global.scale-64, 320+64*2, 240+64*2, precise:false)) {
2225 if (e.active) activeThinkerList[$] = e;
2230 foreach (MapEntity e; grid.allObjects()) {
2231 if (e.active) activeThinkerList[$] = e;
2234 grid.collectAllActiveObjects(activeThinkerList);
2236 // process active objects
2237 //writeln("thinkers: ", activeThinkerList.length);
2238 foreach (MapEntity o; activeThinkerList) {
2241 thinkOne(o, doHeldObject:false);
2242 if (!o.isInstanceAlive) {
2243 if (o.grid) o.grid.remove(o.gridId);
2244 auto obj = MapObject(o);
2245 if (obj && obj.heldBy) obj.heldBy.holdItem = none;
2246 } else if (!solidObjectSeen && o.walkableSolid) {
2247 solidObjectSeen = true;
2248 hasSolidObjects = true;
2252 // postponed thinkers
2253 foreach (MapEntity o; postponedThinkers) {
2255 if (o.active) thinkOne(o, doHeldObject:true, dontAddHeldObject:true);
2256 if (!o.isInstanceAlive) {
2257 if (o.grid) o.grid.remove(o.gridId);
2258 auto obj = MapObject(o);
2259 if (obj && obj.heldBy) obj.heldBy.holdItem = none;
2260 } else if (!solidObjectSeen && o.walkableSolid) {
2261 solidObjectSeen = true;
2262 hasSolidObjects = true;
2265 postponedThinkers.clear();
2267 // clean dead things
2269 hasSolidObjects = !!solidObjectSeen;
2270 // fix held item coords
2271 if (player && player.holdItem) {
2272 if (player.holdItem.isInstanceAlive) {
2273 player.holdItem.fixHoldCoords();
2275 player.holdItem = none;
2279 if (collectCounter == 0) {
2280 xmoney = max(0, xmoney-100);
2286 if (!player.dead) stats.oneMoreFramePlayed();
2287 SoundSystem.ListenerOrigin = vector(player.xCenter, player.yCenter, -1);
2288 //writeln("plrpos=(", player.xCenter, ",", player.yCenter, "); lo=", SoundSystem.ListenerOrigin);
2290 if (onAfterFrame) onAfterFrame(accumTime >= FrameTime);
2291 ++framesProcessedFromLastClear;
2294 if (!player.visible && player.holdItem) player.holdItem.visible = false;
2295 if (winCutsceneSwitchToNext) {
2296 winCutsceneSwitchToNext = false;
2297 switch (++inWinCutscene) {
2298 case 2: startWinCutsceneVolcano(); break;
2299 case 3: default: startWinCutsceneWinFall(); break;
2303 if (playerExited) break;
2305 ImmediateDelete = olddel;
2307 playerExited = false;
2309 centerViewAtPlayer();
2312 // if we were processed at least one frame, collect garbage
2314 CollectGarbage(true); // destroy delayed objects too
2316 if (accumTime > 0 && onInterFrame) onInterFrame(accumTime/FrameTime);
2320 // ////////////////////////////////////////////////////////////////////////// //
2321 final void tileXY2roomXY (int tileX, int tileY, out int roomX, out int roomY) {
2322 roomX = (tileX-1)/RoomGen::Width;
2323 roomY = (tileY-1)/RoomGen::Height;
2327 final bool isInShop (int tileX, int tileY) {
2328 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
2329 auto n = roomType[tileX, tileY];
2330 if (n == 4 || n == 5) return true;
2331 return !!checkTilesInRect(tileX*16, tileY*16, 16, 16, delegate bool (MapTile t) { return t.shopWall; });
2332 //k8: we don't have this
2333 //if (t && t.objType == 'oShop') return true;
2339 // ////////////////////////////////////////////////////////////////////////// //
2340 override void Destroy () {
2342 delete tempSolidTile;
2347 // ////////////////////////////////////////////////////////////////////////// //
2348 // WARNING! delegate should not create/delete objects!
2349 final MapObject findNearestObject (int px, int py, scope bool delegate (MapObject o) dg, optional class!MapObject castClass) {
2350 MapObject res = none;
2351 if (!castClass) castClass = MapObject;
2352 int curdistsq = int.max;
2353 foreach (MapObject o; objGrid.allObjects(MapObject)) {
2354 if (o.spectral) continue;
2355 if (!dg(o)) continue;
2356 int xc = px-o.xCenter, yc = py-o.yCenter;
2357 int distsq = xc*xc+yc*yc;
2358 if (distsq < curdistsq) {
2367 // WARNING! delegate should not create/delete objects!
2368 final MapObject findNearestEnemy (int px, int py, optional scope bool delegate (MapEnemy o) dg, optional class!MapEnemy castClass) {
2369 if (!castClass) castClass = MapEnemy;
2370 if (castClass !isa MapEnemy) return none;
2371 MapObject res = none;
2372 int curdistsq = int.max;
2373 foreach (MapEnemy o; objGrid.allObjects(castClass)) {
2374 //k8: i added `dead` check
2375 if (o.spectral || o.dead) continue;
2377 if (!dg(o)) continue;
2379 int xc = px-o.xCenter, yc = py-o.yCenter;
2380 int distsq = xc*xc+yc*yc;
2381 if (distsq < curdistsq) {
2390 final MonsterShopkeeper findNearestCalmShopkeeper (int px, int py) {
2391 auto obj = MonsterShopkeeper(findNearestEnemy(px, py, delegate bool (MapEnemy o) {
2392 auto sk = MonsterShopkeeper(o);
2393 if (sk && !sk.angered) return true;
2395 }, castClass:MonsterShopkeeper));
2400 final MonsterShopkeeper hasAliveShopkeepers (optional bool skipAngry) {
2401 foreach (MonsterShopkeeper sc; objGrid.allObjects(MonsterShopkeeper)) {
2402 if (sc.spectral || sc.dead) continue;
2403 if (skipAngry && (sc.angered || sc.outlaw)) continue;
2410 // WARNING! delegate should not create/delete objects!
2411 final int calcNearestEnemyDist (int px, int py, optional scope bool delegate (MapEnemy o) dg, optional class!MapEnemy castClass) {
2412 auto e = findNearestEnemy(px, py, dg!optional, castClass!optional);
2413 if (!e) return int.max;
2414 int xc = px-e.xCenter, yc = py-e.yCenter;
2415 return round(sqrt(xc*xc+yc*yc));
2419 // WARNING! delegate should not create/delete objects!
2420 final int calcNearestObjectDist (int px, int py, optional scope bool delegate (MapObject o) dg, optional class!MapObject castClass) {
2421 auto e = findNearestObject(px, py, dg!optional, castClass!optional);
2422 if (!e) return int.max;
2423 int xc = px-e.xCenter, yc = py-e.yCenter;
2424 return round(sqrt(xc*xc+yc*yc));
2428 // WARNING! delegate should not create/delete objects!
2429 final MapTile findNearestMoveableSolid (int px, int py, optional scope bool delegate (MapTile t) dg) {
2431 int curdistsq = int.max;
2432 foreach (MapTile t; objGrid.allObjects(MapTile)) {
2433 if (t.spectral) continue;
2435 if (!dg(t)) continue;
2437 if (!t.solid || !t.moveable) continue;
2439 int xc = px-t.xCenter, yc = py-t.yCenter;
2440 int distsq = xc*xc+yc*yc;
2441 if (distsq < curdistsq) {
2450 // WARNING! delegate should not create/delete objects!
2451 final MapTile findNearestTile (int px, int py, optional scope bool delegate (MapTile t) dg) {
2452 if (!dg) return none;
2454 int curdistsq = int.max;
2456 //FIXME: make this faster!
2457 foreach (MapTile t; objGrid.allObjects(MapTile)) {
2458 if (t.spectral) continue;
2459 int xc = px-t.xCenter, yc = py-t.yCenter;
2460 int distsq = xc*xc+yc*yc;
2461 if (distsq < curdistsq && dg(t)) {
2471 // ////////////////////////////////////////////////////////////////////////// //
2472 final bool cbIsObjectWeb (MapObject o) { return (o isa ItemWeb); }
2473 final bool cbIsObjectBlob (MapObject o) { return (o isa EnemyBlob); }
2474 final bool cbIsObjectArrow (MapObject o) { return (o isa ItemProjectileArrow); }
2475 final bool cbIsObjectEnemy (MapObject o) { return (/*o != self &&*/ o isa MapEnemy); }
2477 final bool cbIsObjectTreasure (MapObject o) { return (o.isTreasure); }
2479 final bool cbIsItemObject (MapObject o) { return (o isa MapItem); }
2481 final bool cbIsItemOrEnemyObject (MapObject o) { return (o isa MapItem || o isa MapEnemy); }
2484 final MapObject isObjectAtTile (int tileX, int tileY, optional scope bool delegate (MapObject o) dg, optional bool precise) {
2485 if (!specified_precise) precise = true;
2488 foreach (MapObject o; objGrid.inRectPix(tileX, tileY, 16, 16, precise:precise, castClass:MapObject)) {
2489 if (o.spectral) continue;
2491 if (dg(o)) return o;
2500 final MapObject isObjectAtTilePix (int x, int y, optional scope bool delegate (MapObject o) dg) {
2501 return isObjectAtTile(x/16, y/16, dg!optional);
2505 final MapObject isObjectAtPoint (int xpos, int ypos, optional scope bool delegate (MapObject o) dg, optional bool precise, optional class!MapObject castClass) {
2506 if (!specified_precise) precise = true;
2507 if (!castClass) castClass = MapObject;
2508 foreach (MapObject o; objGrid.inCellPix(xpos, ypos, precise:precise, castClass:castClass)) {
2509 if (o.spectral) continue;
2511 if (dg(o)) return o;
2513 if (o isa MapEnemy) return o;
2520 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) {
2521 if (w < 1 || h < 1) return none;
2522 if (!castClass) castClass = MapObject;
2523 if (w == 1 && h == 1) return isObjectAtPoint(xpos, ypos, dg!optional, precise!optional, castClass);
2524 if (!specified_precise) precise = true;
2525 foreach (MapObject o; objGrid.inRectPix(xpos, ypos, w, h, precise:precise, castClass:castClass)) {
2526 if (o.spectral) continue;
2528 if (dg(o)) return o;
2530 if (o isa MapEnemy) return o;
2537 final MapObject forEachObject (scope bool delegate (MapObject o) dg, optional bool allowSpectrals, optional class!MapObject castClass) {
2538 if (!dg) return none;
2539 if (!castClass) castClass = MapObject;
2540 foreach (MapObject o; objGrid.allObjectsSafe(castClass)) {
2541 if (!allowSpectrals && o.spectral) continue;
2542 if (dg(o)) return o;
2548 final MapObject forEachObjectAtPoint (int xpos, int ypos, scope bool delegate (MapObject o) dg, optional bool precise) {
2549 if (!dg) return none;
2550 if (!specified_precise) precise = true;
2551 foreach (MapObject o; objGrid.inCellPix(xpos, ypos, precise:precise, castClass:MapObject)) {
2552 if (o.spectral) continue;
2553 if (dg(o)) return o;
2559 final MapObject forEachObjectInRect (int xpos, int ypos, int w, int h, scope bool delegate (MapObject o) dg, optional bool precise) {
2560 if (!dg || w < 1 || h < 1) return none;
2561 if (w == 1 && h == 1) return forEachObjectAtPoint(xpos, ypos, dg!optional, precise!optional);
2562 if (!specified_precise) precise = true;
2563 foreach (MapObject o; objGrid.inRectPix(xpos, ypos, w, h, precise:precise, castClass:MapObject)) {
2564 if (o.spectral) continue;
2565 if (dg(o)) return o;
2571 final MapEntity forEachEntityInRect (int xpos, int ypos, int w, int h, scope bool delegate (MapEntity o) dg, optional bool precise, optional class!MapEntity castClass) {
2572 if (!dg || w < 1 || h < 1) return none;
2573 if (!castClass) castClass = MapEntity;
2574 if (!specified_precise) precise = true;
2575 foreach (MapEntity e; objGrid.inRectPix(xpos, ypos, w, h, precise:precise, castClass:castClass)) {
2576 if (e.spectral) continue;
2577 if (dg(e)) return e;
2583 private final bool cbIsRopeTile (MapTile t) { return (t isa MapTileRope); }
2585 final MapTile isRopeAtPoint (int px, int py) {
2586 return checkTileAtPoint(px, py, &cbIsRopeTile);
2591 final MapTile isWaterSwimAtPoint (int px, int py) {
2592 return isWaterAtPoint(px, py);
2596 // ////////////////////////////////////////////////////////////////////////// //
2597 private array!MapEntity tmpEntityList;
2599 private final bool cbCollectEntitiesWithMask (MapEntity t) {
2600 if (!t.visible || t.spectral) return false;
2601 tmpEntityList[$] = t;
2606 final void touchEntitiesWithMask (int x, int y, SpriteFrame frm, scope bool delegate (MapEntity t) dg, optional class!MapEntity castClass) {
2607 if (!frm || frm.bw < 1 || frm.bh < 1 || !dg) return;
2608 if (frm.isEmptyPixelMask) return;
2609 if (!castClass) castClass = MapEntity;
2611 if (tmpEntityList.length) tmpEntityList.clear();
2612 if (player isa castClass && player.isRectCollisionFrame(frm, x, y)) tmpEntityList[$] = player;
2613 forEachEntityInRect(x+frm.bx, y+frm.by, frm.bw, frm.bh, &cbCollectEntitiesWithMask, castClass:castClass);
2614 foreach (MapEntity e; tmpEntityList) {
2615 if (!e || !e.isInstanceAlive || !e.visible || e.spectral) continue;
2616 if (e.isRectCollisionFrame(frm, x, y)) {
2623 // ////////////////////////////////////////////////////////////////////////// //
2624 final bool cbCollisionAnySolid (MapTile t) { return t.solid; }
2625 final bool cbCollisionAnyMoveableSolid (MapTile t) { return t.solid && t.moveable; }
2626 final bool cbCollisionLiquid (MapTile t) { return (t.water || t.lava); }
2627 final bool cbCollisionAnySolidOrLiquid (MapTile t) { return (t.solid || t.water || t.lava); }
2628 final bool cbCollisionLadder (MapTile t) { return t.ladder; }
2629 final bool cbCollisionLadderTop (MapTile t) { return t.laddertop; }
2630 final bool cbCollisionAnyLadder (MapTile t) { return (t.ladder || t.laddertop); }
2631 final bool cbCollisionAnySolidIce (MapTile t) { return (t.solid && t.ice); }
2632 final bool cbCollisionWater (MapTile t) { return t.water; }
2633 final bool cbCollisionLava (MapTile t) { return t.lava; }
2634 final bool cbCollisionAnyTree (MapTile t) { return t.tree; }
2635 final bool cbCollisionPlatform (MapTile t) { return t.platform; }
2636 final bool cbCollisionLadderOrPlatform (MapTile t) { return (t.ladder || t.platform); }
2637 //final bool cbCollisionAnyHangeable (MapTile t) { return t.hangeable; } // no need to check for solids here, it is done in MapTile initializer
2638 final bool cbCollisionSpringTrap (MapTile t) { return t.springtrap; }
2639 final bool cbCollisionSpikes (MapTile t) { return t.spikes; }
2640 final bool cbCollisionExitTile (MapTile t) { return t.isExitActive(); }
2642 final bool cbCollisionForWhoa (MapTile t) { return (t.solid || t.laddertop); }
2644 //final bool cbCollisionSacAltar (MapTile t) { return (t.objName == 'oSacAltarLeft'); } // only left part, yeah
2645 final bool cbCollisionSacAltar (MapTile t) { return t.sacrificingAltar; }
2648 // ////////////////////////////////////////////////////////////////////////// //
2649 transient MapTileTemp tempSolidTile;
2651 private final MapTileTemp makeWalkeableSolidTile (MapObject o) {
2652 if (!tempSolidTile) {
2653 tempSolidTile = SpawnObject(MapTileTemp);
2654 } else if (!tempSolidTile.isInstanceAlive) {
2655 delete tempSolidTile;
2656 tempSolidTile = SpawnObject(MapTileTemp);
2659 tempSolidTile.level = self;
2660 tempSolidTile.global = global;
2661 tempSolidTile.solid = true;
2662 tempSolidTile.objName = MapTileTemp.default.objName;
2663 tempSolidTile.objType = MapTileTemp.default.objType;
2664 tempSolidTile.e = o;
2665 tempSolidTile.fltx = o.fltx;
2666 tempSolidTile.flty = o.flty;
2667 return tempSolidTile;
2671 final MapTile checkTilesInRect (int x0, int y0, const int w, const int h,
2672 optional scope bool delegate (MapTile dg) dg, optional bool precise,
2673 optional class!MapTile castClass)
2675 if (w < 1 || h < 1) return none;
2676 if (w == 1 && h == 1) return checkTileAtPoint(x0, y0, dg!optional);
2677 int x1 = x0+w-1, y1 = y0+h-1;
2678 if (x0 >= tilesWidth*16 || y0 >= tilesHeight*16 || x1 < 0 || y1 < 0) return none;
2679 if (!specified_precise) precise = true;
2680 if (!castClass) castClass = MapTile;
2681 if (castClass !isa MapTile) return none;
2682 if (!dg) dg = &cbCollisionAnySolid;
2684 if (hasSolidObjects) {
2685 // check walkable solid objects too
2686 foreach (MapEntity e; objGrid.inRectPix(x0, y0, w, h, precise:precise/*, castClass:castClass*/)) {
2687 if (e.spectral || !e.visible) continue;
2688 auto t = MapTile(e);
2690 if (t isa castClass && dg(t)) return t;
2693 auto o = MapObject(e);
2694 if (o && o.walkableSolid) {
2695 t = makeWalkeableSolidTile(o);
2696 if (t isa castClass && dg(t)) return t;
2701 // no walkeable solid MapObjects, speed it up
2702 foreach (MapTile t; objGrid.inRectPix(x0, y0, w, h, precise:precise, castClass:castClass)) {
2703 if (t.spectral || !t.visible) continue;
2704 if (dg(t)) return t;
2712 final MapTile checkTileAtPoint (int x0, int y0, optional scope bool delegate (MapTile dg) dg, optional bool precise, optional class!MapTile castClass) {
2713 if (x0 < 0 || y0 < 0 || x0 >= tilesWidth*16 || y0 >= tilesHeight*16) return none;
2714 if (!specified_precise) precise = true;
2715 if (!castClass) castClass = MapTile;
2716 if (castClass !isa MapTile) return none;
2717 if (!dg) dg = &cbCollisionAnySolid;
2719 if (hasSolidObjects) {
2720 // check walkable solid objects
2721 foreach (MapEntity e; objGrid.inCellPix(x0, y0, precise:precise/*, castClass:castClass*/)) {
2722 if (e.spectral || !e.visible) continue;
2723 auto t = MapTile(e);
2725 if (t isa castClass && dg(t)) return t;
2728 auto o = MapObject(e);
2729 if (o && o.walkableSolid) {
2730 t = makeWalkeableSolidTile(o);
2731 if (t isa castClass && dg(t)) return t;
2737 // no walkeable solid MapObjects, speed it up
2738 foreach (MapTile t; objGrid.inCellPix(x0, y0, precise:precise, castClass:castClass)) {
2739 if (t.spectral || !t.visible) continue;
2740 if (dg(t)) return t;
2748 // ////////////////////////////////////////////////////////////////////////// //
2749 final MapTile isSolidAtPoint (int px, int py) { return checkTileAtPoint(px, py); }
2750 final MapTile isSolidInRect (int rx, int ry, int rw, int rh) { return checkTilesInRect(rx, ry, rw, rh); }
2751 final MapTile isLadderAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadder); }
2752 final MapTile isLadderOrPlatformAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadderOrPlatform); }
2753 final MapTile isLadderTopAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadderTop); }
2754 final MapTile isAnyLadderAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyLadder); }
2755 final MapTile isWaterAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionWater); }
2756 final MapTile isLavaAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLava); }
2757 final MapTile isLiquidAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLiquid); }
2758 final MapTile isIceAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnySolidIce); }
2759 final MapTile isTreeAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyTree); }
2760 //final MapTile isHangeableAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyHangeable); }
2763 // ////////////////////////////////////////////////////////////////////////// //
2764 final void setTileTypeAt (int tileX, int tileY, LevelGen::RType rt) {
2765 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) roomType[tileX, tileY] = rt;
2769 //FIXME: make this faster
2770 transient float gtagX, gtagY;
2772 // only non-moveables and non-specials
2773 final MapTile getTileAtGrid (int tileX, int tileY) {
2776 return checkTileAtPoint(tileX*16, tileY*16, delegate bool (MapTile t) {
2777 if (t.spectral || t.moveable || t.toSpecialGrid || !t.visible) return false;
2778 if (t.fltx != gtagX || t.flty != gtagY) return false; // emulate grid
2779 if (t.width != 16 || t.height != 16) return false;
2782 //return (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight ? tiles[tileX, tileY] : none);
2786 final MapTile getTileAtGridAny (int tileX, int tileY) {
2789 return checkTileAtPoint(tileX*16, tileY*16, delegate bool (MapTile t) {
2790 if (t.spectral /*|| t.moveable*/ || !t.visible) return false;
2791 if (t.fltx != gtagX || t.flty != gtagY) return false; // emulate grid
2792 if (t.width != 16 || t.height != 16) return false;
2795 //return (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight ? tiles[tileX, tileY] : none);
2799 final bool isNamedTileAt (int tileX, int tileY, name atypename) {
2800 if (!atypename) return false;
2801 auto t = getTileAtGridAny(tileX, tileY);
2802 return (t && t.objName == atypename);
2806 final void setTileAtGrid (int tileX, int tileY, MapTile tile) {
2807 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
2809 tile.fltx = tileX*16;
2810 tile.flty = tileY*16;
2811 if (!tile.dontReplaceOthers) {
2812 auto osp = tile.spectral;
2813 tile.spectral = true;
2814 auto t = getTileAtGridAny(tileX, tileY);
2815 tile.spectral = osp;
2816 if (t && !t.immuneToReplacement) {
2817 writeln("REPLACING tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
2818 writeln(" NEW tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (tile.toSpecialGrid ? "special " : ""), "grid, pos=(", tile.fltx/16.0, ",", tile.flty/16.0, ")");
2824 auto t = getTileAtGridAny(tileX, tileY);
2825 if (t && !t.immuneToReplacement) {
2826 writeln("REMOVING tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
2834 // ////////////////////////////////////////////////////////////////////////// //
2835 // return `true` from delegate to stop
2836 MapTile forEachSolidTileOnGrid (scope bool delegate (int tileX, int tileY, MapTile t) dg) {
2837 if (!dg) return none;
2838 foreach (MapTile t; objGrid.allObjectsSafe(MapTile)) {
2839 if (t.spectral || !t.solid || !t.visible) continue;
2840 if (t.ix%16 != 0 || t.iy%16 != 0) continue; // emulate grid
2841 if (t.width != 16 || t.height != 16) continue;
2842 if (dg(t.ix/16, t.iy/16, t)) return t;
2848 // ////////////////////////////////////////////////////////////////////////// //
2849 // return `true` from delegate to stop
2850 MapTile forEachTile (scope bool delegate (MapTile t) dg, optional class!MapTile castClass) {
2851 if (!dg) return none;
2852 if (!castClass) castClass = MapTile;
2853 foreach (MapTile t; objGrid.allObjectsSafe(castClass)) {
2854 if (t.spectral || !t.visible) continue;
2855 if (dg(t)) return t;
2861 // ////////////////////////////////////////////////////////////////////////// //
2862 final void fixWallTiles () {
2863 foreach (MapTile t; objGrid.allObjectsSafe(MapTile)) {
2864 //writeln("beautify: '", GetClassName(t.Class), "' (", t.objType, "' (name:", t.objName, ")");
2870 // ////////////////////////////////////////////////////////////////////////// //
2871 final MapTile isCollisionAtPoint (int px, int py, optional scope bool delegate (MapTile dg) dg) {
2872 if (!dg) dg = &cbCollisionAnySolid;
2873 return checkTilesInRect(px, py, 1, 1, dg);
2877 // ////////////////////////////////////////////////////////////////////////// //
2878 string scrGetKaliGift (MapTile altar, optional name gift) {
2881 // find other side of the altar
2882 int sx = player.ix, sy = player.iy;
2886 auto a2 = isCollisionAtPoint(altar.ix+18, altar.iy+8, &cbCollisionSacAltar);
2887 if (!a2) a2 = isCollisionAtPoint(altar.ix-8, altar.iy+8, &cbCollisionSacAltar);
2888 if (a2) { sx = a2.ix; sy = a2.iy; }
2891 if (global.favor <= -8) res = "SHE SEEMS VERY ANGRY WITH YOU!";
2892 else if (global.favor < 0) res = "SHE SEEMS ANGRY WITH YOU.";
2893 else if (global.favor == 0) res = "SHE HAS FORGIVEN YOU!";
2894 else if (global.favor >= 32) {
2895 if (global.kaliGift >= 3 && global.favor >= 32+(global.kaliGift-2)*16) {
2896 res = "YOU FEEL INVIGORATED!";
2897 global.kaliGift += 1;
2898 global.plife += global.randOther(4, 8);
2899 } else if (global.kaliGift >= 3) {
2900 res = "SHE SEEMS ECSTATIC WITH YOU!";
2901 } else if (global.bombs < 80) {
2902 res = "YOUR SATCHEL FEELS VERY FULL NOW!";
2903 global.kaliGift = 3;
2906 res = "YOU FEEL INVIGORATED!";
2907 global.kaliGift += 1;
2908 global.plife += global.randOther(4, 8);
2910 } else if (global.favor >= 16) {
2911 if (global.kaliGift >= 2) {
2912 res = "SHE SEEMS VERY HAPPY WITH YOU!";
2914 res = "SHE BESTOWS A GIFT UPON YOU!";
2915 global.kaliGift = 2;
2917 auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2920 obj = MakeMapObject(sx, sy-8, 'oPoof');
2925 if (gift) obj = MakeMapObject(sx, sy-8, gift);
2926 if (!obj) obj = MakeMapObject(sx, sy-8, 'oKapala');
2928 } else if (global.favor >= 8) {
2929 if (global.kaliGift >= 1) {
2930 res = "SHE SEEMS HAPPY WITH YOU.";
2932 res = "SHE BESTOWS A GIFT UPON YOU!";
2933 global.kaliGift = 1;
2934 //rAltar = instance_nearest(x, y, oSacAltarRight);
2935 //if (instance_exists(rAltar)) {
2937 auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2940 obj = MakeMapObject(sx, sy-8, 'oPoof');
2944 if (gift) obj = MakeMapObject(sx, sy-8, gift);
2946 auto n = global.randOther(1, 8);
2950 if (n == 1 && !global.hasCape && !global.hasJetpack) aname = 'oCapePickup';
2951 else if (n == 2 && !global.hasGloves) aname = 'oGloves';
2952 else if (n == 3 && !global.hasSpectacles) aname = 'oSpectacles';
2953 else if (n == 4 && !global.hasMitt) aname = 'oMitt';
2954 else if (n == 5 && !global.hasSpringShoes) aname = 'oSpringShoes';
2955 else if (n == 6 && !global.hasSpikeShoes) aname = 'oSpikeShoes';
2956 else if (n == 7 && !global.hasStickyBombs) aname = 'oPaste';
2957 else if (n == 8 && !global.hasCompass) aname = 'oCompass';
2959 obj = MakeMapObject(sx, sy-8, aname);
2965 obj = MakeMapObject(sx, sy-8, (global.hasJetpack ? 'oBombBox' : 'oJetpack'));
2971 } else if (global.favor > 0) {
2972 res = "SHE SEEMS PLEASED WITH YOU.";
2977 global.message = "";
2978 res = "KALI DEVOURS YOU!"; // sacrifice is player
2986 void performSacrifice (MapObject what, MapTile where) {
2987 if (!what || !what.isInstanceAlive) return;
2988 MakeMapObject(what.ix+8, what.iy+8, 'oFlame');
2989 if (where) where.playSound('sndSmallExplode'); else what.playSound('sndSmallExplode');
2990 what.spillBlood(amount:3, forced:true);
2992 string msg = "KALI ACCEPTS THE SACRIFICE!";
2994 auto idol = ItemGoldIdol(what);
2996 ++stats.totalSacrifices;
2997 if (global.favor <= -8) msg = "SHE SEEMS VERY ANGRY WITH YOU!";
2998 else if (global.favor < 0) msg = "SHE SEEMS ANGRY WITH YOU.";
2999 else if (global.favor >= 0) {
3000 // find other side of the altar
3001 int sx = player.ix, sy = player.iy;
3006 auto a2 = isCollisionAtPoint(altar.ix+18, altar.iy+8, &cbCollisionSacAltar);
3007 if (!a2) a2 = isCollisionAtPoint(altar.ix-8, altar.iy+8, &cbCollisionSacAltar);
3008 if (a2) { sx = a2.ix; sy = a2.iy; }
3011 auto obj = MakeMapObject(sx, sy-8, 'oPoof');
3014 obj = MakeMapObject(sx, sy-8, 'oPoof');
3018 obj = MakeMapObject(sx, sy-16-8, 'oMonkeyGold');
3020 osdMessage(msg, 6.66);
3022 idol.instanceRemove();
3026 if (global.favor <= -8) {
3027 msg = "KALI DEVOURS THE SACRIFICE!";
3028 } else if (global.favor < 0) {
3029 global.favor += (what.status == MapObject::STUNNED ? what.favor : round(what.favor/2.0)); //k8: added `round()`
3030 if (what.favor > 0) what.favor = 0;
3032 global.favor += (what.status == MapObject::STUNNED ? what.favor : round(what.favor/2.0)); //k8: added `round()`
3036 if (myAltar.gift1 != "" and global.kaliGift == 0) scrGetKaliGift(myAltar.gift1);
3037 else if (myAltar.gift2 != "" and global.kaliGift == 1) scrGetKaliGift(myAltar.gift2);
3038 else scrGetKaliGift("");
3041 // sacrifice is player?
3042 if (what isa PlayerPawn) {
3043 ++stats.totalSelfSacrifices;
3044 msg = "KALI DEVOURS YOU!";
3045 player.visible = false;
3046 player.removeBallAndChain(temp:true);
3048 player.status = MapObject::DEAD;
3050 ++stats.totalSacrifices;
3051 auto msg2 = scrGetKaliGift(where);
3052 what.instanceRemove();
3053 if (msg2) msg = va("%s\n%s", msg, msg2);
3056 osdMessage(msg, 6.66);
3062 // ////////////////////////////////////////////////////////////////////////// //
3063 final void addBackgroundGfxDetails () {
3064 // add background details
3065 //if (global.customLevel) return;
3067 // bg = instance_create(16*randRoom(1, 42), 16*randRoom(1, 33), oCaveBG);
3068 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);
3069 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);
3070 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);
3071 else MakeMapBackTile('bgExtras', 32*global.randRoom(0, 1), 0, 32, 32, 16*global.randRoom(1, 42), 16*global.randRoom(1, 33), 10002);
3076 // ////////////////////////////////////////////////////////////////////////// //
3077 private final void fixRealViewStart () {
3078 int scale = global.scale;
3079 realViewStart.x = clamp(realViewStart.x, viewMin.x*scale, viewMax.x*scale-viewWidth);
3080 realViewStart.y = clamp(realViewStart.y, viewMin.y*scale, viewMax.y*scale-viewHeight);
3084 final int cameraCurrX () { return realViewStart.x/global.scale; }
3085 final int cameraCurrY () { return realViewStart.y/global.scale; }
3088 private final void fixViewStart () {
3089 int scale = global.scale;
3090 viewStart.x = clamp(viewStart.x, viewMin.x*scale, viewMax.x*scale-viewWidth);
3091 viewStart.y = clamp(viewStart.y, viewMin.y*scale, viewMax.y*scale-viewHeight);
3095 final void centerViewAtPlayer () {
3096 if (viewWidth < 1 || viewHeight < 1 || !player) return;
3097 centerViewAt(player.xCenter, player.yCenter);
3101 final void centerViewAt (int x, int y) {
3102 if (viewWidth < 1 || viewHeight < 1) return;
3104 cameraSlideToSpeed.x = 0;
3105 cameraSlideToSpeed.y = 0;
3106 cameraSlideToPlayer = 0;
3108 int scale = global.scale;
3111 realViewStart.x = clamp(x-viewWidth/2, 0, tilesWidth*16*scale-viewWidth);
3112 realViewStart.y = clamp(y-viewHeight/2, 0, tilesHeight*16*scale-viewHeight);
3115 viewStart.x = realViewStart.x;
3116 viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
3119 if (onCameraTeleported) onCameraTeleported();
3123 const int ViewPortToleranceX = 16*1+8;
3124 const int ViewPortToleranceY = 16*1+8;
3126 final void fixCamera () {
3127 if (!player) return;
3128 if (viewWidth < 1 || viewHeight < 1) return;
3129 int scale = global.scale;
3130 auto alwaysCenterX = global.config.alwaysCenterPlayer;
3131 auto alwaysCenterY = alwaysCenterX;
3132 // calculate offset from viewport center (in game units), and fix viewport
3134 int camDestX = player.ix+8;
3135 int camDestY = player.iy+8;
3136 if (!cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y) != 0) {
3137 // slide camera to point
3138 if (cameraSlideToSpeed.x) camDestX = cameraSlideToCurr.x;
3139 if (cameraSlideToSpeed.y) camDestY = cameraSlideToCurr.y;
3140 int dx = cameraSlideToDest.x-camDestX;
3141 int dy = cameraSlideToDest.y-camDestY;
3142 //writeln("speed:(", cameraSlideToSpeed.x, ",", cameraSlideToSpeed.y, "); cur:(", camDestX, ",", camDestY, "); dest:(", cameraSlideToDest.x, ",", cameraSlideToDest.y, "); delta=(", dx, ",", dy, ")");
3143 if (dx && cameraSlideToSpeed.x != 0) {
3144 alwaysCenterX = true;
3145 if (abs(dx) <= abs(cameraSlideToSpeed.x)) {
3146 camDestX = cameraSlideToDest.x;
3148 camDestX += sign(dx)*abs(cameraSlideToSpeed.x);
3151 if (dy && abs(cameraSlideToSpeed.y) != 0) {
3152 alwaysCenterY = true;
3153 if (abs(dy) <= cameraSlideToSpeed.y) {
3154 camDestY = cameraSlideToDest.y;
3156 camDestY += sign(dy)*abs(cameraSlideToSpeed.y);
3159 //writeln(" new:(", camDestX, ",", camDestY, ")");
3160 if (cameraSlideToSpeed.x) cameraSlideToCurr.x = camDestX;
3161 if (cameraSlideToSpeed.y) cameraSlideToCurr.y = camDestY;
3165 if (cameraSlideToSpeed.x && !cameraSlideToPlayer) {
3166 realViewStart.x = clamp(camDestX*scale, 0, tilesWidth*16*scale-viewWidth);
3167 } else if (!player.cameraBlockX) {
3168 int x = camDestX*scale;
3169 int cx = realViewStart.x;
3170 if (alwaysCenterX) {
3173 int xofs = x-(cx+viewWidth/2);
3174 if (xofs < -ViewPortToleranceX*scale) cx += xofs+ViewPortToleranceX*scale;
3175 else if (xofs > ViewPortToleranceX*scale) cx += xofs-ViewPortToleranceX*scale;
3177 // slide back to player?
3178 if (cameraSlideToPlayer && cameraSlideToSpeed.x) {
3179 int prevx = cameraSlideToCurr.x*scale;
3180 int dx = (cx-prevx)/scale;
3181 if (abs(dx) <= cameraSlideToSpeed.x) {
3182 writeln("BACKSLIDE X COMPLETE!");
3183 cameraSlideToSpeed.x = 0;
3185 cx = prevx+(sign(dx)*cameraSlideToSpeed.x)*scale;
3186 cameraSlideToCurr.x += sign(dx)*cameraSlideToSpeed.x;
3187 if ((dx < 0 && cx <= 0) || (dx > 0 && cx >= tilesWidth*16*scale-viewWidth)) {
3188 writeln("BACKSLIDE X COMPLETE!");
3189 cameraSlideToSpeed.x = 0;
3193 realViewStart.x = clamp(cx, 0, tilesWidth*16*scale-viewWidth);
3197 if (cameraSlideToSpeed.y && !cameraSlideToPlayer) {
3198 realViewStart.y = clamp(camDestY*scale, 0, tilesHeight*16*scale-viewHeight);
3199 } else if (!player.cameraBlockY) {
3200 int y = camDestY*scale;
3201 int cy = realViewStart.y;
3202 if (alwaysCenterY) {
3203 cy = y-viewHeight/2;
3205 int yofs = y-(cy+viewHeight/2);
3206 if (yofs < -ViewPortToleranceY*scale) cy += yofs+ViewPortToleranceY*scale;
3207 else if (yofs > ViewPortToleranceY*scale) cy += yofs-ViewPortToleranceY*scale;
3209 // slide back to player?
3210 if (cameraSlideToPlayer && cameraSlideToSpeed.y) {
3211 int prevy = cameraSlideToCurr.y*scale;
3212 int dy = (cy-prevy)/scale;
3213 if (abs(dy) <= cameraSlideToSpeed.y) {
3214 writeln("BACKSLIDE Y COMPLETE!");
3215 cameraSlideToSpeed.y = 0;
3217 cy = prevy+(sign(dy)*cameraSlideToSpeed.y)*scale;
3218 cameraSlideToCurr.y += sign(dy)*cameraSlideToSpeed.y;
3219 if ((dy < 0 && cy <= 0) || (dy > 0 && cy >= tilesHeight*16*scale-viewHeight)) {
3220 writeln("BACKSLIDE Y COMPLETE!");
3221 cameraSlideToSpeed.y = 0;
3225 realViewStart.y = clamp(cy, 0, tilesHeight*16*scale-viewHeight);
3228 if (cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y) == 0) cameraSlideToPlayer = 0;
3231 //writeln(" new2:(", cameraCurrX, ",", cameraCurrY, ")");
3233 viewStart.x = realViewStart.x;
3234 viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
3239 // ////////////////////////////////////////////////////////////////////////// //
3240 // x0 and y0 are non-scaled (and will be scaled)
3241 final void drawSpriteAt (name sprName, float frnumf, int x0, int y0, optional bool small) {
3242 if (!sprName) return;
3243 auto spr = sprStore[sprName];
3244 if (!spr || !spr.frames.length) return;
3245 int scale = global.scale;
3248 int frnum = max(0, trunc(frnumf))%spr.frames.length;
3249 auto sfr = spr.frames[frnum];
3250 int sx0 = x0-sfr.xofs*scale;
3251 int sy0 = y0-sfr.yofs*scale;
3252 if (small && scale > 1) {
3253 sfr.tex.blitExt(sx0, sy0, round(sx0+sfr.tex.width*(scale/2.0)), round(sy0+sfr.tex.height*(scale/2.0)), 0, 0);
3255 sfr.tex.blitAt(sx0, sy0, scale);
3260 final void drawSpriteAtS3 (name sprName, float frnumf, int x0, int y0) {
3261 if (!sprName) return;
3262 auto spr = sprStore[sprName];
3263 if (!spr || !spr.frames.length) return;
3266 int frnum = max(0, trunc(frnumf))%spr.frames.length;
3267 auto sfr = spr.frames[frnum];
3268 int sx0 = x0-sfr.xofs*3;
3269 int sy0 = y0-sfr.yofs*3;
3270 sfr.tex.blitAt(sx0, sy0, 3);
3274 // x0 and y0 are non-scaled (and will be scaled)
3275 final void drawTextAt (int x0, int y0, string text, optional int scale, optional int hiColor1, optional int hiColor2) {
3277 if (!specified_scale) scale = global.scale;
3280 sprStore.renderTextWithHighlight(x0, y0, text, scale, hiColor1!optional, hiColor2!optional);
3284 void renderCompass (float currFrameDelta) {
3285 if (!global.hasCompass) return;
3288 if (isRoom("rOlmec")) {
3291 } else if (isRoom("rOlmec2")) {
3297 bool hasMessage = osdHasMessage();
3298 foreach (MapTile et; allExits) {
3300 int exitX = et.ix, exitY = et.iy;
3301 int vx0 = viewStart.x/global.scale, vy0 = viewStart.y/global.scale;
3302 int vx1 = (viewStart.x+viewWidth)/global.scale;
3303 int vy1 = (viewStart.y+viewHeight)/global.scale;
3304 if (exitY > vy1-16) {
3306 drawSpriteAt(hasMessage ? 'sCompassSmallLL' : 'sCompassLL', 0, 0, 224);
3307 } else if (exitX > vx1-16) {
3308 drawSpriteAt(hasMessage ? 'sCompassSmallLR' : 'sCompassLR', 0, 304, 224);
3310 drawSpriteAt(hasMessage ? 'sCompassSmallDown' : 'sCompassDown', 0, exitX-vx0, 224);
3312 } else if (exitX < vx0) {
3313 drawSpriteAt(hasMessage ? 'sCompassSmallLeft' : 'sCompassLeft', 0, 0, exitY-vy0);
3314 } else if (exitX > vx1-16) {
3315 drawSpriteAt(hasMessage ? 'sCompassSmallRight' : 'sCompassRight', 0, 304, exitY-vy0);
3317 break; // only the first exit
3322 final bool totalsNameCmpCB (ref GameStats::TotalItem a, ref GameStats::TotalItem b) {
3323 auto sa = string(a.objName);
3324 auto sb = string(b.objName);
3328 void renderTransitionInfo (float currFrameDelta) {
3331 GameStats.sortTotalsList(stats.kills, &totalsNameCmpCB);
3334 foreach (int idx, ref auto k; stats.kills) {
3335 string s = string(k);
3336 maxLen = max(maxLen, s.length);
3340 sprStore.loadFont('sFontSmall');
3341 Video.color = 0xff_ff_00;
3342 foreach (int idx, ref auto k; stats.kills) {
3344 foreach (int xidx, ref auto d; stats.totalKills) {
3345 if (d.objName == k) { deaths = d.count; break; }
3347 //drawTextAt(16, 4+idx*8, va("%s: %d (%d)", string(k.objName).toUpperCase, k.count, deaths));
3348 drawTextAt(16, 4+idx*8, string(k).toUpperCase);
3349 drawTextAt(16+maxLen, 4+idx*8, va(": %d (%d)", k.count, deaths));
3355 void renderGhostTimer (float currFrameDelta) {
3356 if (ghostTimeLeft <= 0) return;
3357 //ghostTimeLeft /= 30; // frames -> seconds
3359 int hgt = viewHeight-64;
3360 if (hgt < 1) return;
3361 int rhgt = round(float(hgt)*(float(ghostTimeLeft)/30.0)/1000.0);
3362 //writeln("hgt=", hgt, "; rhgt=", rhgt, "; gtl=", ghostTimeLeft/30);
3364 auto oclr = Video.color;
3365 Video.color = 0xcf_ff_7f_00;
3366 Video.fillRect(viewWidth-20, 32, 16, hgt-rhgt);
3367 Video.color = 0x7f_ff_7f_00;
3368 Video.fillRect(viewWidth-20, 32+(hgt-rhgt), 16, rhgt);
3374 void renderStarsHUD (float currFrameDelta) {
3375 bool scumSmallHud = global.config.scumSmallHud;
3377 //auto life = max(0, global.plife);
3378 int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3379 int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3380 //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3385 sprStore.loadFont('sFontSmall');
3388 sprStore.loadFont('sFont');
3392 Video.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3393 //draw_sprite(sHeart, -1, view_xview[0]+8, view_yview[0]+8);
3394 //draw_text(view_xview[0]+24, view_yview[0]+8, life);
3396 if (global.plife == 1) {
3397 drawSpriteAt('sHeartSmallBlink', global.heartBlink, 10, hhup-4);
3398 global.heartBlink += 0.1;
3399 if (global.heartBlink > 3) global.heartBlink = 0;
3401 drawSpriteAt('sHeartSmall', -1, 10, hhup-4);
3402 global.heartBlink = 0;
3405 if (global.plife == 1) {
3406 drawSpriteAt('sHeartBlink', global.heartBlink, 8, hhup);
3407 global.heartBlink += 0.1;
3408 if (global.heartBlink > 3) global.heartBlink = 0;
3410 drawSpriteAt('sHeart', -1, 8, hhup);
3411 global.heartBlink = 0;
3414 int life = clamp(global.plife, 0, 99);
3415 drawTextAt(16+8, hhup, va("%d", life));
3417 //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3418 drawSpriteAt('sShopkeeperIcon', -1, 64, hhup, scumSmallHud);
3419 drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", starsKills));
3421 if (starsRoomTimer1 > 0) {
3422 sprStore.loadFont('sFontSmall');
3423 Video.color = 0xff_ff_00;
3424 int scale = global.scale;
3425 sprStore.renderMultilineTextCentered(viewWidth/2, 216*scale, va("SHOTGUN CHALLENGE BEGINS IN ~%d~", (starsRoomTimer1/30)+1), scale, 0xff_00_00);
3430 void renderSunHUD (float currFrameDelta) {
3431 bool scumSmallHud = global.config.scumSmallHud;
3433 //auto life = max(0, global.plife);
3434 int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3435 int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3436 //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3441 sprStore.loadFont('sFontSmall');
3444 sprStore.loadFont('sFont');
3448 Video.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3449 //draw_sprite(sHeart, -1, view_xview[0]+8, view_yview[0]+8);
3450 //draw_text(view_xview[0]+24, view_yview[0]+8, life);
3452 if (global.plife == 1) {
3453 drawSpriteAt('sHeartSmallBlink', global.heartBlink, 10, hhup-4);
3454 global.heartBlink += 0.1;
3455 if (global.heartBlink > 3) global.heartBlink = 0;
3457 drawSpriteAt('sHeartSmall', -1, 10, hhup-4);
3458 global.heartBlink = 0;
3461 if (global.plife == 1) {
3462 drawSpriteAt('sHeartBlink', global.heartBlink, 8, hhup);
3463 global.heartBlink += 0.1;
3464 if (global.heartBlink > 3) global.heartBlink = 0;
3466 drawSpriteAt('sHeart', -1, 8, hhup);
3467 global.heartBlink = 0;
3470 int life = clamp(global.plife, 0, 99);
3471 drawTextAt(16+8, hhup, va("%d", life));
3473 //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3474 drawSpriteAt('sDamselIcon', -1, 64, hhup, scumSmallHud);
3475 drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", sunScore));
3477 if (sunRoomTimer1 > 0) {
3478 sprStore.loadFont('sFontSmall');
3479 Video.color = 0xff_ff_00;
3480 int scale = global.scale;
3481 sprStore.renderMultilineTextCentered(viewWidth/2, 216*scale, va("DAMSEL CHALLENGE BEGINS IN ~%d~", (sunRoomTimer1/30)+1), scale, 0xff_00_00);
3486 void renderMoonHUD (float currFrameDelta) {
3487 bool scumSmallHud = global.config.scumSmallHud;
3489 //auto life = max(0, global.plife);
3490 int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3491 int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3492 //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3497 sprStore.loadFont('sFontSmall');
3500 sprStore.loadFont('sFont');
3504 Video.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3506 //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3507 drawSpriteAt('sHoopsIcon', -1, 8, hhup, scumSmallHud);
3508 drawTextAt(8+16-(scumSmallHud ? 6 : 0), hhup, va("%d", moonScore));
3509 drawSpriteAt('sTimerIcon', -1, 64, hhup, scumSmallHud);
3510 drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", max(0, moonTimer)));
3512 if (moonRoomTimer1 > 0) {
3513 sprStore.loadFont('sFontSmall');
3514 Video.color = 0xff_ff_00;
3515 int scale = global.scale;
3516 sprStore.renderMultilineTextCentered(viewWidth/2, 216*scale, va("ARCHERY CHALLENGE BEGINS IN ~%d~", (moonRoomTimer1/30)+1), scale, 0xff_00_00);
3521 void renderHUD (float currFrameDelta) {
3522 if (levelKind == LevelKind.Stars) { renderStarsHUD(currFrameDelta); return; }
3523 if (levelKind == LevelKind.Sun) { renderSunHUD(currFrameDelta); return; }
3524 if (levelKind == LevelKind.Moon) { renderMoonHUD(currFrameDelta); return; }
3526 if (!isHUDEnabled()) return;
3528 if (levelKind == LevelKind.Transition) { renderTransitionInfo(currFrameDelta); return; }
3536 bool scumSmallHud = global.config.scumSmallHud;
3537 if (!global.config.optSGAmmo) moneyX = ammoX;
3540 sprStore.loadFont('sFontSmall');
3543 sprStore.loadFont('sFont');
3546 //int alpha = 0x6f_00_00_00;
3547 int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3548 int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3550 //Video.color = 0xff_ff_ff;
3551 Video.color = 0xff_ff_ff|talpha;
3555 if (global.plife == 1) {
3556 drawSpriteAt('sHeartSmallBlink', global.heartBlink, lifeX+2, 4-hhup);
3557 global.heartBlink += 0.1;
3558 if (global.heartBlink > 3) global.heartBlink = 0;
3560 drawSpriteAt('sHeartSmall', -1, lifeX+2, 4-hhup);
3561 global.heartBlink = 0;
3564 if (global.plife == 1) {
3565 drawSpriteAt('sHeartBlink', global.heartBlink, lifeX, 8-hhup);
3566 global.heartBlink += 0.1;
3567 if (global.heartBlink > 3) global.heartBlink = 0;
3569 drawSpriteAt('sHeart', -1, lifeX, 8-hhup);
3570 global.heartBlink = 0;
3574 int life = clamp(global.plife, 0, 99);
3575 //if (!scumHud && life > 99) life = 99;
3576 drawTextAt(lifeX+16, 8-hhup, va("%d", life));
3579 if (global.hasStickyBombs && global.stickyBombsActive) {
3580 if (scumSmallHud) drawSpriteAt('sStickyBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sStickyBombIcon', -1, bombX, 8-hhup);
3582 if (scumSmallHud) drawSpriteAt('sBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sBombIcon', -1, bombX, 8-hhup);
3584 int n = global.bombs;
3585 if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3586 drawTextAt(bombX+16, 8-hhup, va("%d", n));
3589 if (scumSmallHud) drawSpriteAt('sRopeIconSmall', -1, ropeX+2, 4-hhup); else drawSpriteAt('sRopeIcon', -1, ropeX, 8-hhup);
3591 if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3592 drawTextAt(ropeX+16, 8-hhup, va("%d", n));
3595 if (global.config.optSGAmmo && (!player || player.holdItem !isa ItemWeaponBow)) {
3596 if (scumSmallHud) drawSpriteAt('sShellsIcon', -1, ammoX+6, 8-hhup); else drawSpriteAt('sShellsIcon', -1, ammoX+7, 12-hhup);
3598 if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3599 drawTextAt(ammoX+16, 8-hhup, va("%d", n));
3600 } else if (player && player.holdItem isa ItemWeaponBow) {
3601 if (scumSmallHud) drawSpriteAt('sArrowRight', -1, ammoX+6, 8-hhup); else drawSpriteAt('sArrowRight', -1, ammoX+7, 12-hhup);
3603 if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3604 drawTextAt(ammoX+16, 8-hhup, va("%d", n));
3608 if (scumSmallHud) drawSpriteAt('sDollarSignSmall', -1, moneyX+2, 3-hhup); else drawSpriteAt('sDollarSign', -1, moneyX, 8-hhup);
3609 drawTextAt(moneyX+16, 8-hhup, va("%d", stats.money-xmoney));
3612 Video.color = 0xff_ff_ff|ialpha;
3614 int ity = (scumSmallHud ? 18-hhup : 24-hhup);
3617 if (global.hasUdjatEye) {
3618 if (global.udjatBlink) drawSpriteAt('sUdjatEyeIcon2', -1, n, ity); else drawSpriteAt('sUdjatEyeIcon', -1, n, ity);
3621 if (global.hasAnkh) { drawSpriteAt('sAnkhIcon', -1, n, ity); n += 20; }
3622 if (global.hasCrown) { drawSpriteAt('sCrownIcon', -1, n, ity); n += 20; }
3623 if (global.hasKapala) {
3624 if (global.bloodLevel == 0) drawSpriteAt('sKapalaIcon', 0, n, ity);
3625 else if (global.bloodLevel <= 2) drawSpriteAt('sKapalaIcon', 1, n, ity);
3626 else if (global.bloodLevel <= 4) drawSpriteAt('sKapalaIcon', 2, n, ity);
3627 else if (global.bloodLevel <= 6) drawSpriteAt('sKapalaIcon', 3, n, ity);
3628 else if (global.bloodLevel <= 8) drawSpriteAt('sKapalaIcon', 4, n, ity);
3631 if (global.hasSpectacles) { drawSpriteAt('sSpectaclesIcon', -1, n, ity); n += 20; }
3632 if (global.hasGloves) { drawSpriteAt('sGlovesIcon', -1, n, ity); n += 20; }
3633 if (global.hasMitt) { drawSpriteAt('sMittIcon', -1, n, ity); n += 20; }
3634 if (global.hasSpringShoes) { drawSpriteAt('sSpringShoesIcon', -1, n, ity); n += 20; }
3635 if (global.hasJordans) { drawSpriteAt('sJordansIcon', -1, n, ity); n += 20; }
3636 if (global.hasSpikeShoes) { drawSpriteAt('sSpikeShoesIcon', -1, n, ity); n += 20; }
3637 if (global.hasCape) { drawSpriteAt('sCapeIcon', -1, n, ity); n += 20; }
3638 if (global.hasJetpack) { drawSpriteAt('sJetpackIcon', -1, n, ity); n += 20; }
3639 if (global.hasStickyBombs) { drawSpriteAt('sPasteIcon', -1, n, ity); n += 20; }
3640 if (global.hasCompass) { drawSpriteAt('sCompassIcon', -1, n, ity); n += 20; }
3641 if (global.hasParachute) { drawSpriteAt('sParachuteIcon', -1, n, ity); n += 20; }
3643 if (player.holdItem && player.holdItem.objName == 'Bow' && global.arrows > 0) {
3646 while (m <= global.arrows && m <= 20 && malpha > 0) {
3647 Video.color = trunc(malpha*255)<<24|0xff_ff_ff;
3648 drawSpriteAt('sArrowIcon', -1, n, ity);
3650 if (m > 10) malpha -= 0.1; //and global.arrows > 20 malpha -= 0.1
3656 sprStore.loadFont('sFontSmall');
3657 Video.color = 0xff_ff_00|talpha;
3658 if (scumSmallHud) drawTextAt(moneyX+8, 8+8-hhup, va("+%d", xmoney));
3659 else drawTextAt(moneyX, 8+16-hhup, va("+%d", xmoney));
3662 Video.color = 0xff_ff_ff;
3663 if (global.config.ghostShowTime) renderGhostTimer(currFrameDelta);
3667 // ////////////////////////////////////////////////////////////////////////// //
3668 // x0 and y0 are non-scaled (and will be scaled)
3669 final void drawTextAtS3 (int x0, int y0, string text, optional int hiColor1, optional int hiColor2) {
3673 sprStore.renderTextWithHighlight(x0, y0, text, 3, hiColor1!optional, hiColor2!optional);
3677 final void drawTextAtS3Centered (int y0, string text, optional int hiColor1, optional int hiColor2) {
3679 int x0 = (viewWidth-sprStore.getTextWidth(text, 3, specified_hiColor1, specified_hiColor2))/2;
3680 sprStore.renderTextWithHighlight(x0, y0*3, text, 3, hiColor1!optional, hiColor2!optional);
3684 void renderHelpOverlay () {
3686 Video.fillRect(0, 0, viewWidth, viewHeight);
3689 int txoff = 0; // text x pos offset (for multi-color lines)
3691 if (gameHelpScreen) {
3692 sprStore.loadFont('sFontSmall');
3693 Video.color = 0xff_ff_ff;
3694 drawTextAtS3Centered(ty, va("HELP (PAGE ~%d~ OF ~%d~)", gameHelpScreen, MaxGameHelpScreen), 0xff_ff_00);
3698 if (gameHelpScreen == 1) {
3699 sprStore.loadFont('sFontSmall');
3700 Video.color = 0xff_ff_00; drawTextAtS3(tx, ty, "INVENTORY BASICS"); ty += 16;
3701 Video.color = 0xff_ff_ff;
3702 drawTextAtS3(tx, ty, global.expandString("Press $SWITCH to cycle through items."), 0x00_ff_00);
3705 Video.color = 0xff_ff_ff;
3706 drawSpriteAtS3('sHelpSprite1', -1, 64, 96);
3707 } else if (gameHelpScreen == 2) {
3708 sprStore.loadFont('sFontSmall');
3709 Video.color = 0xff_ff_00;
3710 drawTextAtS3(tx, ty, "SELLING TO SHOPKEEPERS"); ty += 16;
3711 Video.color = 0xff_ff_ff;
3712 drawTextAtS3(tx, ty, global.expandString("Press $PAY to offer your currently"), 0x00_ff_00); ty += 8;
3713 drawTextAtS3(tx, ty, "held item to the shopkeeper."); ty += 16;
3714 drawTextAtS3(tx, ty, "If the shopkeeper is interested, "); ty += 8;
3715 //drawTextAtS3(tx, ty, global.expandString("press $PAY again to complete the sale."), 0x00_ff_00); ty += 72;
3716 drawTextAtS3(tx, ty, global.expandString("press $PAY again to complete"), 0x00_ff_00);
3717 drawTextAtS3(tx, ty+8, "the sale.");
3719 drawSpriteAtS3('sHelpSell', -1, 112, 100);
3720 drawTextAtS3(tx, ty, "Purchasing goods from the shopkeeper"); ty += 8;
3721 drawTextAtS3(tx, ty, "will increase the funds he has"); ty += 8;
3722 drawTextAtS3(tx, ty, "available to buy your unwanted stuff."); ty += 8;
3725 sprStore.loadFont('sFont');
3726 Video.color = 0xff_ff_ff;
3727 drawTextAtS3(136, 8, "MAP");
3729 if (lg.mapSprite && (isNormalLevel() || isTransitionRoom())) {
3730 Video.color = 0xff_ff_00;
3731 drawTextAtS3Centered(24, lg.mapTitle);
3733 auto spf = sprStore[lg.mapSprite].frames[0];
3734 int mapX = 160-spf.width/2;
3735 int mapY = 120-spf.height/2;
3736 //mapTitleX = 160-string_length(global.mapTitle)*8/2;
3738 Video.color = 0xff_ff_ff;
3739 drawSpriteAtS3(lg.mapSprite, -1, mapX, mapY);
3741 if (lg.mapSprite != 'sMapDefault') {
3742 int mx = -1, my = -1;
3744 // set position of player icon
3745 switch (global.currLevel) {
3746 case 1: mx = 81; my = 22; break;
3747 case 2: mx = 113; my = 63; break;
3748 case 3: mx = 197; my = 86; break;
3749 case 4: mx = 133; my = 109; break;
3750 case 5: mx = 181; my = 22; break;
3751 case 6: mx = 126; my = 64; break;
3752 case 7: mx = 158; my = 112; break;
3753 case 8: mx = 66; my = 80; break;
3754 case 9: mx = 30; my = 26; break;
3755 case 10: mx = 88; my = 54; break;
3756 case 11: mx = 148; my = 81; break;
3757 case 12: mx = 210; my = 205; break;
3758 case 13: mx = 66; my = 17; break;
3759 case 14: mx = 146; my = 17; break;
3760 case 15: mx = 82; my = 77; break;
3761 case 16: mx = 178; my = 81; break;
3765 int plrx = mx+player.ix/16;
3766 int plry = my+player.iy/16;
3767 if (isTransitionRoom()) { plrx = mx+20; plry = my+16; }
3768 name plrspr = 'sMapSpelunker';
3769 if (global.isDamsel) plrspr = 'sMapDamsel';
3770 else if (global.isTunnelMan) plrspr = 'sMapTunnel';
3771 auto ss = sprStore[plrspr];
3772 drawSpriteAtS3(plrspr, (pausedTime/2)%ss.frames.length, mapX+plrx, mapY+plry);
3774 if (global.hasCompass && allExits.length) {
3775 drawSpriteAtS3('sMapRedDot', -1, mapX+mx+allExits[0].ix/16, mapY+my+allExits[0].iy/16);
3782 sprStore.loadFont('sFontSmall');
3783 Video.color = 0xff_ff_00;
3784 drawTextAtS3Centered(232, "PRESS ~SPACE~/~LEFT~/~RIGHT~ TO CHANGE PAGE", 0x00_ff_00);
3786 Video.color = 0xff_ff_ff;
3790 void renderPauseOverlay () {
3791 //drawTextAt(256, 432, "PAUSED", scale);
3793 if (gameShowHelp) { renderHelpOverlay(); return; }
3795 Video.color = 0xff_ff_00;
3796 //int hiColor = 0x00_ff_00;
3799 if (isTutorialRoom()) {
3800 sprStore.loadFont('sFont');
3801 drawTextAtS3Centered(n-24, "TUTORIAL CAVE");
3802 } else if (isNormalLevel()) {
3803 sprStore.loadFont('sFont');
3805 drawTextAtS3Centered(n-32, va("LEVEL ~%d~", global.currLevel), 0x00_ff_00);
3807 sprStore.loadFont('sFontSmall');
3809 int depth = round((174.8*(global.currLevel-1)+(player.iy+8)*0.34)*(global.config.scumMetric ? 0.3048 : 1.0)*10);
3810 string depthStr = va("DEPTH: ~%d.%d~ %s", depth/10, depth%10, (global.config.scumMetric ? "METRES" : "FEET"));
3811 drawTextAtS3Centered(n-16, depthStr, 0x00_ff_00);
3814 drawTextAtS3Centered(n, va("MONEY: ~%d~", stats.money), 0x00_ff_00);
3815 drawTextAtS3Centered(n+16, va("KILLS: ~%d~", stats.countKills), 0x00_ff_00);
3816 drawTextAtS3Centered(n+32, va("SAVES: ~%d~", stats.damselsSaved), 0x00_ff_00);
3817 drawTextAtS3Centered(n+48, va("TIME: ~%s~", time2str(time/30)), 0x00_ff_00);
3818 drawTextAtS3Centered(n+64, va("LEVEL TIME: ~%s~", time2str((time-levelStartTime)/30)), 0x00_ff_00);
3821 sprStore.loadFont('sFontSmall');
3822 Video.color = 0xff_ff_ff;
3823 drawTextAtS3Centered(240-2-8, "~ESC~-RETURN ~F10~-QUIT ~CTRL+DEL~-SUICIDE", 0xff_7f_00);
3824 drawTextAtS3Centered(2, "~O~PTIONS REDEFINE ~K~EYS ~S~TATISTICS", 0xff_7f_00);
3828 // ////////////////////////////////////////////////////////////////////////// //
3829 transient int drawLoot;
3830 transient int drawPosX, drawPosY;
3832 void resetTransitionOverlay () {
3839 // current game, uncollapsed
3840 struct LevelStatInfo {
3842 // for transition screen
3849 void thinkFrameTransition () {
3850 if (drawLoot == 0) {
3851 if (drawPosX > 272) {
3854 if (drawPosY > 83+4) drawPosY = 83;
3856 } else if (drawPosX > 232) {
3859 if (drawPosY > 91+4) drawPosY = 91;
3864 void renderTransitionOverlay () {
3865 sprStore.loadFont('sFontSmall');
3866 Video.color = 0xff_ff_00;
3867 //else if (global.currLevel-1 < 1) draw_text(32, 48, "TUTORIAL CAVE COMPLETED!");
3868 //else draw_text(32, 48, "LEVEL "+string(global.currLevel-1)+" COMPLETED!");
3869 if (global.currLevel == 0) {
3870 drawTextAt(32, 48, "TUTORIAL CAVE COMPLETED!");
3872 drawTextAt(32, 48, va("LEVEL ~%d~ COMPLETED!", global.currLevel), hiColor1:0x00_ff_ff);
3874 Video.color = 0xff_ff_ff;
3875 drawTextAt(32, 64, va("TIME = ~%s~", time2str((levelEndTime-levelStartTime)/30)), hiColor1:0xff_ff_00);
3877 if (/*stats.collected.length == 0*/stats.money <= levelMoneyStart) {
3878 drawTextAt(32, 80, "LOOT = ~NONE~", hiColor1:0xff_00_00);
3880 drawTextAt(32, 80, va("LOOT = ~%d~", stats.money-levelMoneyStart), hiColor1:0xff_ff_00);
3883 if (stats.kills.length == 0) {
3884 drawTextAt(32, 96, "KILLS = ~NONE~", hiColor1:0x00_ff_00);
3886 drawTextAt(32, 96, va("KILLS = ~%d~", stats.kills.length), hiColor1:0xff_ff_00);
3889 drawTextAt(32, 112, va("MONEY = ~%d~", stats.money), hiColor1:0xff_ff_00);
3893 // ////////////////////////////////////////////////////////////////////////// //
3894 private transient array!MapEntity renderVisibleCids;
3895 private transient array!MapEntity renderVisibleLights;
3896 private transient array!MapTile renderFrontTiles; // normal, with fg
3898 final bool renderSortByDepth (MapEntity oa, MapEntity ob) {
3899 auto da = oa.depth, db = ob.depth;
3900 if (da == db) return (oa.objId < ob.objId);
3905 const int RenderEdgePixNormal = 64;
3906 const int RenderEdgePixLight = 256;
3908 #ifndef EXPERIMENTAL_RENDER_CACHE
3909 enum skipListCreation = false;
3912 void renderWithOfs (int xofs, int yofs, float currFrameDelta) {
3913 int scale = global.scale;
3915 // don't touch framebuffer alpha
3916 Video.colorMask = Video::CMask.Colors;
3917 Video.color = 0xff_ff_ff;
3920 Video::ScissorRect scsave;
3921 bool doRestoreGL = false;
3923 if (viewOffsetX > 0 || viewOffsetY > 0) {
3925 Video.getScissor(scsave);
3926 Video.scissorCombine(viewOffsetX, viewOffsetY, viewWidth, viewHeight);
3927 Video.glPushMatrix();
3928 Video.glTranslate(viewOffsetX, viewOffsetY);
3929 //Video.glTranslate(-550, 0);
3930 //Video.glScale(1, 1);
3935 bool isDarkLevel = global.darkLevel;
3938 switch (global.config.scumPlayerLit) {
3939 case 0: player.lightRadius = 0; break; // never
3940 case 1: // only in "scumDarkness"
3941 player.lightRadius = (global.config.scumDarkness >= 2 ? 96 : 32);
3944 player.lightRadius = 96;
3949 // render cave background
3952 int bgw = levBGImg.tex.width*scale;
3953 int bgh = levBGImg.tex.height*scale;
3954 int bgTW = (tilesWidth*tsz+bgw-1)/bgw;
3955 int bgTH = (tilesHeight*tsz+bgh-1)/bgh;
3956 int bgX0 = max(0, xofs/bgw);
3957 int bgY0 = max(0, yofs/bgh);
3958 int bgX1 = min(bgTW, (xofs+viewWidth+bgw-1)/bgw);
3959 int bgY1 = min(bgTH, (yofs+viewHeight+bgh-1)/bgh);
3960 foreach (int ty; bgY0..bgY1) {
3961 foreach (int tx; bgX0..bgX1) {
3962 int x0 = tx*bgw-xofs;
3963 int y0 = ty*bgh-yofs;
3964 levBGImg.tex.blitAt(x0, y0, scale);
3969 int RenderEdgePix = (global.darkLevel ? RenderEdgePixLight : RenderEdgePixNormal);
3971 // render background tiles
3972 for (MapBackTile bt = backtiles; bt; bt = bt.next) {
3973 bt.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3976 // collect visible special tiles
3977 #ifdef EXPERIMENTAL_RENDER_CACHE
3978 bool skipListCreation = (lastRenderTime == time && renderVisibleCids.length); //FIXME
3981 if (!skipListCreation) {
3982 renderVisibleCids.clear();
3983 renderVisibleLights.clear();
3984 renderFrontTiles.clear();
3986 int endVX = xofs+viewWidth;
3987 int endVY = yofs+viewHeight;
3991 if (player && player.visible) renderVisibleCids[$] = player; // include player in render list
3993 //FIXME: drop lit objects which cannot affect visible area
3995 // collect visible objects
3996 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)) {
3997 if (!o.visible) continue;
3998 auto tile = MapTile(o);
4000 if (isDarkLevel && (o.lightRadius > 4 || tile.litWholeTile)) renderVisibleLights[$] = o;
4001 if (tile.invisible) continue;
4002 if (tile.bgfront /*|| tile.spriteLeftDeco || tile.spriteRightDeco*/) renderFrontTiles[$] = tile;
4003 if (tile.bgback) tile.drawWithOfsBack(xofs, yofs, scale, currFrameDelta);
4005 if (isDarkLevel && o.lightRadius > 4) renderVisibleLights[$] = o;
4007 // check if the object is really visible -- this will speed up later sorting
4008 int fx0, fy0, fx1, fy1;
4009 auto spf = o.getSpriteFrame(default, out fx0, out fy0, out fx1, out fy1);
4010 if (!spf) continue; // no sprite -- nothing to draw (no, really)
4011 int ix = o.ix, iy = o.iy;
4012 int x0 = (ix+fx0)*scale, y0 = (iy+fy0)*scale;
4013 int x1 = (ix+fx1)*scale, y1 = (iy+fy1)*scale;
4014 if (x1 <= xofs || y1 <= yofs || x0 >= endVX || y0 >= endVY) {
4018 renderVisibleCids[$] = o;
4021 foreach (MapEntity o; objGrid.allObjects()) {
4022 if (!o.visible) continue;
4023 auto tile = MapTile(o);
4025 if (isDarkLevel && (o.lightRadius > 4 || tile.litWholeTile)) renderVisibleLights[$] = o;
4026 if (tile.invisible) continue;
4027 if (tile.bgfront /*|| tile.spriteLeftDeco || tile.spriteRightDeco*/) renderFrontTiles[$] = tile;
4028 if (tile.bgback) tile.drawWithOfsBack(xofs, yofs, scale, currFrameDelta);
4030 if (isDarkLevel && o.lightRadius > 4) renderVisibleLights[$] = o;
4032 renderVisibleCids[$] = o;
4035 //writeln("::: ", cnt, " invisible objects dropped");
4037 renderVisibleCids.sort(&renderSortByDepth);
4038 lastRenderTime = time;
4041 auto depth4Start = 0;
4042 foreach (auto xidx, MapEntity o; renderVisibleCids) {
4049 bool playerPowerupRendered = false;
4051 // render objects (part one: depth > 3)
4052 foreach (auto idx; depth4Start..renderVisibleCids.length; reverse) {
4053 MapEntity o = renderVisibleCids[idx];
4054 // 1000 is an ordinary tile
4055 if (!playerPowerupRendered && o.depth <= 1200) {
4056 playerPowerupRendered = true;
4057 // so ducking player will have it's cape correctly rendered
4058 if (player.visible) player.drawPrePrePowerupWithOfs(xofs, yofs, scale, currFrameDelta);
4060 //if (idx && renderSortByDepth(o, renderVisibleCids[idx-1])) writeln("VIOLATION!");
4061 o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
4064 // render object (part two: front tile parts, depth 3.5)
4065 foreach (MapTile tile; renderFrontTiles) {
4066 tile.drawWithOfsFront(xofs, yofs, scale, currFrameDelta);
4069 // render objects (part three: depth <= 3)
4070 foreach (auto idx; 0..depth4Start; reverse) {
4071 MapEntity o = renderVisibleCids[idx];
4072 o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
4073 //done above;if (isDarkLevel && (o.lightRadius > 4 || (o isa MapTile && MapTile(o).litWholeTile))) renderVisibleLights[$] = o;
4076 // render player post-effects, so cape or jetpack will be correctly rendered on level exit, for example
4077 player.lastDrawWithOfs(xofs, yofs, scale, currFrameDelta);
4081 auto ltex = bgtileStore.lightTexture('ltx512', 512);
4083 // set screen alpha to min
4084 Video.colorMask = Video::CMask.Alpha;
4085 Video.blendMode = Video::BlendMode.None;
4086 Video.color = 0xff_ff_ff_ff;
4087 Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
4088 //Video.colorMask = Video::CMask.All;
4091 // also, stencil 'em, so we can filter dark areas
4092 Video.textureFiltering = true;
4093 Video.stencil = true;
4094 Video.stencilFunc(Video::StencilFunc.Always, 1);
4095 Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Replace);
4096 Video.alphaTestFunc = Video::AlphaFunc.Greater;
4097 Video.alphaTestVal = 0.03+0.011*global.config.darknessDarkness;
4098 Video.color = 0xff_ff_ff;
4099 Video.blendFunc = Video::BlendFunc.Max;
4100 Video.blendMode = Video::BlendMode.Blend; // anything except `Normal`
4101 Video.colorMask = Video::CMask.Alpha;
4103 foreach (MapEntity e; renderVisibleLights) {
4105 e.getInterpCoords(currFrameDelta, scale, out xi, out yi);
4106 auto tile = MapTile(e);
4107 if (tile && tile.litWholeTile) {
4108 //Video.color = 0xff_ff_ff;
4109 Video.fillRect(xi-xofs, yi-yofs, e.width*scale, e.height*scale);
4111 int lrad = e.lightRadius;
4112 if (lrad < 4) continue; // just in case
4114 float lightscale = float(lrad*scale)/float(ltex.tex.width);
4115 #ifdef OLD_LIGHT_OFFSETS
4116 int fx0, fy0, fx1, fy1;
4118 auto spf = e.getSpriteFrame(out doMirror, out fx0, out fy0, out fx1, out fy1);
4120 xi += (fx1-fx0)*scale/2;
4121 yi += (fy1-fy0)*scale/2;
4125 e.getLightOffset(out lxofs, out lyofs);
4130 lrad = lrad*scale/2;
4133 ltex.tex.blitAt(xi, yi, lightscale);
4135 Video.textureFiltering = false;
4137 // modify only lit parts
4138 Video.stencilFunc(Video::StencilFunc.Equal, 1);
4139 Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Keep);
4140 // multiply framebuffer colors by framebuffer alpha
4141 Video.color = 0xff_ff_ff; // it doesn't matter
4142 Video.blendFunc = Video::BlendFunc.Add;
4143 Video.blendMode = Video::BlendMode.DstMulDstAlpha;
4144 Video.colorMask = Video::CMask.Colors;
4145 Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
4147 // filter unlit parts
4148 Video.stencilFunc(Video::StencilFunc.NotEqual, 1);
4149 Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Keep);
4150 Video.blendFunc = Video::BlendFunc.Add;
4151 Video.blendMode = Video::BlendMode.Filter;
4152 Video.colorMask = Video::CMask.Colors;
4153 Video.color = 0x00_00_18+0x00_00_10*global.config.darknessDarkness;
4154 //Video.color = 0x00_00_18;
4155 //Video.color = 0x00_00_38;
4156 Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
4159 Video.blendFunc = Video::BlendFunc.Add;
4160 Video.blendMode = Video::BlendMode.Normal;
4161 Video.colorMask = Video::CMask.All;
4162 Video.alphaTestFunc = Video::AlphaFunc.Always;
4163 Video.stencil = false;
4166 // clear visible objects list (nope)
4167 //renderVisibleCids.clear();
4168 //renderVisibleLights.clear();
4171 if (global.config.drawHUD) renderHUD(currFrameDelta);
4172 renderCompass(currFrameDelta);
4174 float osdTimeLeft, osdTimeStart;
4175 string msg = osdGetMessage(out osdTimeLeft, out osdTimeStart);
4177 auto ct = GetTickCount();
4179 sprStore.loadFont('sFontSmall');
4180 auto msgHeight = sprStore.getMultilineTextHeight(msg, msgScale);
4181 int x = viewWidth/2;
4182 int y = viewHeight-64-msgHeight;
4183 auto oldColor = Video.color;
4184 Video.color = 0xff_ff_00;
4185 if (osdTimeLeft < 0.5) {
4186 int alpha = 255-clamp(int(510*osdTimeLeft), 0, 255);
4187 Video.color = Video.color|(alpha<<24);
4188 } else if (ct-osdTimeStart < 0.5) {
4189 osdTimeStart = ct-osdTimeStart;
4190 int alpha = 255-clamp(int(510*osdTimeStart), 0, 255);
4191 Video.color = Video.color|(alpha<<24);
4193 sprStore.renderMultilineTextCentered(x, y, msg, msgScale, 0x00_ff_00, 0xff_ff_ff);
4194 Video.color = oldColor;
4197 int hiColor1, hiColor2;
4198 msg = osdGetTalkMessage(out hiColor1, out hiColor2);
4201 sprStore.loadFont('sFontSmall');
4202 auto msgWidth = sprStore.getMultilineTextWidth(msg, processHighlights1:true, processHighlights2:true);
4203 auto msgHeight = sprStore.getMultilineTextHeight(msg);
4204 auto msgWidthOrig = msgWidth*msgScale;
4205 auto msgHeightOrig = msgHeight*msgScale;
4206 if (msgWidth%16 != 0) msgWidth = (msgWidth|0x0f)+1;
4207 if (msgHeight%16 != 0) msgHeight = (msgHeight|0x0f)+1;
4208 msgWidth *= msgScale;
4209 msgHeight *= msgScale;
4210 int x = (viewWidth-msgWidth)/2;
4211 int y = 32*msgScale;
4212 auto oldColor = Video.color;
4213 // draw text frame and text background
4215 Video.fillRect(x, y, msgWidth, msgHeight);
4216 Video.color = 0xff_ff_ff;
4217 for (int fdx = 0; fdx < msgWidth; fdx += 16*msgScale) {
4218 auto spf = sprStore['sMenuTop'].frames[0];
4219 spf.tex.blitAt(x+fdx, y-16*msgScale, msgScale);
4220 spf = sprStore['sMenuBottom'].frames[0];
4221 spf.tex.blitAt(x+fdx, y+msgHeight, msgScale);
4223 for (int fdy = 0; fdy < msgHeight; fdy += 16*msgScale) {
4224 auto spf = sprStore['sMenuLeft'].frames[0];
4225 spf.tex.blitAt(x-16*msgScale, y+fdy, msgScale);
4226 spf = sprStore['sMenuRight'].frames[0];
4227 spf.tex.blitAt(x+msgWidth, y+fdy, msgScale);
4230 auto spf = sprStore['sMenuUL'].frames[0];
4231 spf.tex.blitAt(x-16*msgScale, y-16*msgScale, msgScale);
4232 spf = sprStore['sMenuUR'].frames[0];
4233 spf.tex.blitAt(x+msgWidth, y-16*msgScale, msgScale);
4234 spf = sprStore['sMenuLL'].frames[0];
4235 spf.tex.blitAt(x-16*msgScale, y+msgHeight, msgScale);
4236 spf = sprStore['sMenuLR'].frames[0];
4237 spf.tex.blitAt(x+msgWidth, y+msgHeight, msgScale);
4239 Video.color = 0xff_ff_00;
4240 sprStore.renderMultilineText(x+(msgWidth-msgWidthOrig)/2, y+(msgHeight-msgHeightOrig)/2-3*msgScale, msg, msgScale, (hiColor1 == -1 ? 0x00_ff_00 : hiColor1), (hiColor2 == -1 ? 0xff_ff_ff : hiColor2));
4241 Video.color = oldColor;
4244 if (inWinCutscene) renderWinCutsceneOverlay();
4245 if (inIntroCutscene) renderTitleCutsceneOverlay();
4246 if (isTransitionRoom()) renderTransitionOverlay();
4250 Video.setScissor(scsave);
4251 Video.glPopMatrix();
4255 Video.color = 0xff_ff_ff;
4259 // ////////////////////////////////////////////////////////////////////////// //
4260 final class!MapObject findGameObjectClassByName (name aname) {
4261 if (!aname) return none; // just in case
4262 auto co = FindClassByGameObjName(aname);
4264 writeln("***UNKNOWN GAME OBJECT: '", aname, "'");
4267 co = GetClassReplacement(co);
4268 if (!co) FatalError("findGameObjectClassByName: WTF?!");
4269 if (!class!MapObject(co)) FatalError("findGameTileClassByName: object '%n' is not a map object, it is a `%n`", aname, GetClassName(co));
4270 return class!MapObject(co);
4274 final class!MapTile findGameTileClassByName (name aname) {
4275 if (!aname) return none; // just in case
4276 auto co = FindClassByGameObjName(aname);
4277 if (!co) return MapTile; // unknown names will be routed directly to tile object
4278 co = GetClassReplacement(co);
4279 if (!co) FatalError("findGameTileClassByName: WTF?!");
4280 if (!class!MapTile(co)) FatalError("findGameTileClassByName: object '%n' is not a tile, it is a `%n`", aname, GetClassName(co));
4281 return class!MapTile(co);
4285 final MapObject findAnyObjectOfType (name aname) {
4286 if (!aname) return none;
4287 auto cls = FindClassByGameObjName(aname);
4288 if (!cls) return none;
4289 foreach (MapObject obj; objGrid.allObjects(MapObject)) {
4290 if (obj.spectral) continue;
4291 if (obj isa cls) return obj;
4297 // ////////////////////////////////////////////////////////////////////////// //
4298 final bool isRopePlacedAt (int x, int y) {
4300 foreach (ref auto v; covered) v = false;
4301 foreach (MapTile t; objGrid.inRectPix(x, y-8, 1, 17, precise:false, castClass:MapTileRope)) {
4302 //if (!cbIsRopeTile(t)) continue;
4303 if (t.ix != x) continue;
4304 if (t.iy == y) return true;
4305 foreach (int ty; t.iy..t.iy+8) {
4307 if (d >= 0 && d < covered.length) covered[d] = true;
4310 // check if the whole rope height is completely covered with ropes
4311 foreach (auto v; covered) if (!v) return false;
4316 final MapTile CreateMapTile (int xpos, int ypos, name aname) {
4317 if (!aname) FatalError("cannot create typeless tile");
4318 auto tclass = findGameTileClassByName(aname);
4319 if (!tclass) return none;
4320 MapTile tile = SpawnObject(tclass);
4321 tile.global = global;
4323 tile.objName = aname;
4324 tile.objType = aname; // just in case
4327 tile.objId = ++lastUsedObjectId;
4328 if (!tile.initialize()) FatalError("cannot create tile '%n'", aname);
4333 final bool PutSpawnedMapTile (int x, int y, MapTile tile) {
4334 if (!tile || !tile.isInstanceAlive) return false;
4336 //if (putToGrid) tile.active = true;
4337 bool putToGrid = (tile.moveable || tile.toSpecialGrid || tile.width != 16 || tile.height != 16 || x%16 != 0 || y%16 != 0);
4339 //writeln("putting tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (putToGrid ? "special " : ""), "grid, pos=(", x/16.0, ",", y/16.0, ")");
4342 int mapx = x/16, mapy = y/16;
4343 if (mapx < 0 || mapx >= tilesWidth || mapy < 0 || mapy >= tilesHeight) return false;
4346 // if we already have rope tile there, there is no reason to add another one
4347 if (tile isa MapTileRope) {
4348 if (isRopePlacedAt(x, y)) return false;
4351 // activate special or animated tile
4352 tile.active = tile.active || tile.moveable || tile.toSpecialGrid;
4353 // animated tiles must be active
4355 auto spr = tile.getSprite();
4356 if (spr && spr.frames.length > 1) {
4357 writeln("activated animated tile '", tile.objName, "'");
4365 //if (tile isa TitleTileCopy) writeln("*** PUTTING COPYRIGHT TILE");
4366 //tile.toSpecialGrid = true;
4367 if (!tile.dontReplaceOthers && x&16 == 0 && y%16 == 0) {
4368 auto t = getTileAtGridAny(x/16, y/16);
4369 if (t && !t.immuneToReplacement) {
4370 writeln("REPLACING tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
4371 writeln(" NEW tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (tile.toSpecialGrid ? "special " : ""), "grid, pos=(", tile.fltx/16.0, ",", tile.flty/16.0, ")");
4375 objGrid.insert(tile);
4377 //writeln("SIZE: ", tilesWidth, "x", tilesHeight);
4378 setTileAtGrid(x/16, y/16, tile);
4380 auto t = getTileAtGridAny(x/16, y/16);
4382 writeln("putting tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (putToGrid ? "special " : ""), "grid, pos=(", x/16.0, ",", y/16.0, ")");
4383 checkTilesInRect(x/16, y/16, 16, 16, delegate bool (MapTile tile) {
4384 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, ")");
4387 FatalError("FUUUUUU");
4392 if (tile.enter) registerEnter(tile);
4393 if (tile.exit) registerExit(tile);
4395 // make tile under exit invulnerable
4396 if (checkTilesInRect(tile.ix, tile.iy-16, 16, 16, delegate bool (MapTile t) { return t.exit; })) {
4397 tile.invincible = true;
4404 // won't call `onDestroy()`
4405 final void RemoveMapTileFromGrid (int tileX, int tileY, optional string reason) {
4406 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
4407 auto t = getTileAtGridAny(tileX, tileY);
4409 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, ")");
4417 final MapTile MakeMapTile (int mapx, int mapy, name aname) {
4418 //writeln("tile at (", mapx, ",", mapy, "): ", aname);
4419 //if (aname == 'oLush') { MapObject fail; fail.initialize(); }
4420 //if (mapy >= tilesHeight) { writeln("OOM: ", aname, "; ty=", mapy, "; hgt=", tilesHeight); }
4421 if (mapx < 0 || mapx >= tilesWidth || mapy < 0 || mapy >= tilesHeight) return none;
4423 // if we already have rope tile there, there is no reason to add another one
4424 if (aname == 'oRope') {
4425 if (isRopePlacedAt(mapx*16, mapy*16)) return none;
4428 auto tile = CreateMapTile(mapx*16, mapy*16, aname);
4429 if (!tile) return none;
4430 if (!PutSpawnedMapTile(mapx*16, mapy*16, tile)) {
4439 final MapTile MakeMapTileAtPix (int xpix, int ypix, name aname) {
4440 // if we already have rope tile there, there is no reason to add another one
4441 if (aname == 'oRope') {
4442 if (isRopePlacedAt(xpix, ypix)) return none;
4445 auto tile = CreateMapTile(xpix, ypix, aname);
4446 if (!tile) return none;
4447 if (!PutSpawnedMapTile(xpix, ypix, tile)) {
4456 final MapTile MakeMapRopeTileAt (int x0, int y0) {
4457 // if we already have rope tile there, there is no reason to add another one
4458 if (isRopePlacedAt(x0, y0)) return none;
4460 auto tile = CreateMapTile(x0, y0, 'oRope');
4461 if (!PutSpawnedMapTile(x0, y0, tile)) {
4470 // ////////////////////////////////////////////////////////////////////////// //
4471 final MapBackTile CreateBgTile (name sprName, optional int atx0, optional int aty0, optional int aw, optional int ah) {
4472 BackTileImage img = bgtileStore[sprName];
4473 auto res = SpawnObject(MapBackTile);
4474 res.global = global;
4477 res.bgtName = sprName;
4478 if (specified_atx0) res.tx0 = atx0;
4479 if (specified_aty0) res.ty0 = aty0;
4480 if (specified_aw) res.w = aw;
4481 if (specified_ah) res.h = ah;
4482 if (!res.initialize()) FatalError("cannot create background tile with image '%n'", sprName);
4487 // ////////////////////////////////////////////////////////////////////////// //
4489 background The background asset from which the new tile will be extracted.
4490 left The x coordinate of the left of the new tile, relative to the background asset's top left corner.
4491 top The y coordinate of the top of the new tile, relative to the background assets top left corner.
4492 width The width of the tile.
4493 height The height of the tile.
4494 x The x position in the room to place the tile.
4495 y The y position in the room to place the tile.
4496 depth The depth at which to place the tile.
4498 final void MakeMapBackTile (name bgname, int left, int top, int width, int height, int x, int y, int depth) {
4499 if (width < 1 || height < 1 || !bgname) return;
4500 auto bgt = bgtileStore[bgname];
4501 if (!bgt) FatalError("cannot load background '%n'", bgname);
4502 MapBackTile bt = SpawnObject(MapBackTile);
4505 bt.objName = bgname;
4507 bt.bgtName = bgname;
4515 // find a place for it
4520 // back tiles with the highest depth should come first
4521 MapBackTile ct = backtiles, cprev = none;
4522 while (ct && depth <= ct.depth) { cprev = ct; ct = ct.next; }
4525 bt.next = cprev.next;
4528 bt.next = backtiles;
4534 // ////////////////////////////////////////////////////////////////////////// //
4535 final MapObject SpawnMapObjectWithClass (class!MapObject oclass) {
4536 if (!oclass) return none;
4538 MapObject obj = SpawnObject(oclass);
4539 if (!obj) FatalError("cannot create map object '%n'", GetClassName(oclass));
4541 //if (aname == 'oShells' || aname == 'oShellSingle' || aname == 'oRopePile') writeln("*** CREATED '", aname, "'");
4543 obj.global = global;
4545 obj.objId = ++lastUsedObjectId;
4551 final MapObject SpawnMapObject (name aname) {
4552 if (!aname) return none;
4553 auto res = SpawnMapObjectWithClass(findGameObjectClassByName(aname));
4554 if (res && !res.objType) res.objType = aname; // just in case
4559 final MapObject PutSpawnedMapObject (int x, int y, MapObject obj) {
4560 if (!obj /*|| obj.global || obj.level*/) return none; // oops
4564 if (!obj.initialize()) { delete obj; return none; } // not fatal
4567 if (obj.walkableSolid) hasSolidObjects = true;
4573 final MapObject MakeMapObject (int x, int y, name aname) {
4574 MapObject obj = SpawnMapObject(aname);
4575 obj = PutSpawnedMapObject(x, y, obj);
4580 // ////////////////////////////////////////////////////////////////////////// //
4581 void setMenuTilesVisible (bool vis) {
4583 forEachTile(delegate bool (MapTile t) {
4584 if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4585 t.invisible = false;
4590 forEachTile(delegate bool (MapTile t) {
4591 if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4600 void setMenuTilesOnTop () {
4601 forEachTile(delegate bool (MapTile t) {
4602 if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4610 // ////////////////////////////////////////////////////////////////////////// //
4611 #include "roomTitle.vc"
4612 #include "roomTrans1.vc"
4613 #include "roomTrans2.vc"
4614 #include "roomTrans3.vc"
4615 #include "roomTrans4.vc"
4616 #include "roomOlmec.vc"
4617 #include "roomEnd.vc"
4618 #include "roomIntro.vc"
4619 #include "roomTutorial.vc"
4620 #include "roomScores.vc"
4621 #include "roomStars.vc"
4622 #include "roomSun.vc"
4623 #include "roomMoon.vc"
4626 // ////////////////////////////////////////////////////////////////////////// //
4627 #include "packages/Generator/loadRoomGens.vc"
4628 #include "packages/Generator/loadEntityGens.vc"