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 transient int loserGPU;
48 //RoomGen[LevelGen::LevelWidth, LevelGen::LevelHeight] rooms; // moved to levelgen
50 transient float accumTime;
51 transient bool gamePaused = false;
52 transient bool gameShowHelp = false;
53 transient bool checkWater;
54 transient int gameHelpScreen = 0;
55 const int MaxGameHelpScreen = 2;
56 transient int liquidTileCount; // cached
57 /*transient*/ int damselSaved;
61 transient int collectCounter;
62 /*transient*/ int levelMoneyStart;
64 // all movable (thinkable) map objects
65 EntityGrid objGrid; // monsters, items and tiles
67 MapBackTile backtiles;
68 bool blockWaterChecking;
69 bool someTilesRemoved;
73 bool cameFromIntroRoom; // for title screen
74 bool allowFinalCutsceneSkip;
76 LevelGen::RType[MaxTilesWidth, MaxTilesHeight] roomType;
90 LevelKind levelKind = LevelKind.Normal;
91 int transitionLevelIndex; // set in `generateTransitionLevel()`, used by Tunnel Man
93 array!MapTile allEnters;
94 array!MapTile allExits;
97 int startRoomX, startRoomY;
98 int endRoomX, endRoomY;
102 MapEntity playerExitDoor;
103 transient bool disablePlayerThink = false;
104 int maxPlayingTime; // in seconds
110 bool ghostSpawned; // to speed up some checks
111 bool resetBMCOG = false;
115 // FPS, i.e. incremented by 30 in one second
116 int time; // in frames
117 int lastUsedObjectId;
118 transient int lastRenderTime = -1;
119 transient int pausedTime;
121 MapEntity deadItemsHead;
122 transient /*bool*/int hasSolidObjects = true; // to speed up tilechecks
123 // as we have ALOT of inactive tiles on level, we'd better have
124 // a separate list of active items
125 private array!MapEntity activeItemsList;
128 final int activeItemsCount { get { return activeItemsList.length; } }
130 // WARNING! don't add the entity twice!
131 private final void addActiveEntity (MapEntity e) {
133 if (e.activeItemListIndex) FatalError("addActiveEntity: duplicate!");
134 int fh = activeItemsList.length;
135 activeItemsList[fh] = e;
136 e.activeItemListIndex = fh+1;
139 private final void removeActiveEntity (MapEntity e) {
141 int ei = e.activeItemListIndex;
144 if (activeItemsList[ei] != e) FatalError("removeActiveEntity: entity management failed (0)");
145 // swap last item and `e`, so we don't have to fix alot of index backrefs
146 auto alen = activeItemsList.length-1;
148 MapEntity ne = activeItemsList[alen];
149 if (ne.activeItemListIndex-1 != alen) FatalError("removeActiveEntity: entity management failed (1)");
150 ne.activeItemListIndex = ei+1;
151 activeItemsList[ei] = ne;
153 if (ei != 0) FatalError("removeActiveEntity: entity management failed (2)");
155 activeItemsList.length -= 1;
156 e.activeItemListIndex = 0;
159 private final void clearActiveEntities () {
160 foreach (ref auto ai; activeItemsList) if (ai) ai.activeItemListIndex = 0;
161 activeItemsList.clear();
165 // screen shake variables
170 // set this before calling `fixCamera()`
171 // dimensions should be real, not scaled up/down
172 transient int viewWidth, viewHeight;
173 //transient int viewOffsetX, viewOffsetY;
175 // room bounds, not scaled
176 IVec2D viewMin, viewMax;
178 // for Olmec level cinematics
179 IVec2D cameraSlideToDest;
180 IVec2D cameraSlideToCurr;
181 IVec2D cameraSlideToSpeed; // !0: slide
182 int cameraSlideToPlayer;
183 // `fixCamera()` will set the following
184 // coordinates will be real too (with scale applied)
185 // shake is not applied
186 transient IVec2D viewStart; // with `player.viewOffset`
187 private transient IVec2D realViewStart; // without `player.viewOffset`
189 transient int framesProcessedFromLastClear;
191 transient int BuildYear;
192 transient int BuildMonth;
193 transient int BuildDay;
194 transient int BuildHour;
195 transient int BuildMin;
196 transient string BuildDateString;
199 final string getBuildDateString () {
200 if (!BuildYear) return BuildDateString;
201 if (BuildDateString) return BuildDateString;
202 BuildDateString = va("%d-%02d-%02d %02d:%02d", BuildYear, BuildMonth, BuildDay, BuildHour, BuildMin);
203 return BuildDateString;
207 final void cameraSlideToPoint (int dx, int dy, int speedx, int speedy) {
208 cameraSlideToPlayer = 0;
209 cameraSlideToDest.x = dx;
210 cameraSlideToDest.y = dy;
211 cameraSlideToSpeed.x = abs(speedx);
212 cameraSlideToSpeed.y = abs(speedy);
213 cameraSlideToCurr.x = cameraCurrX;
214 cameraSlideToCurr.y = cameraCurrY;
218 final void cameraReturnToPlayer () {
219 if (!cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y)) {
220 cameraSlideToCurr.x = cameraCurrX;
221 cameraSlideToCurr.y = cameraCurrY;
222 if (cameraSlideToSpeed.x && abs(cameraSlideToSpeed.x) < 8) cameraSlideToSpeed.x = 8;
223 if (cameraSlideToSpeed.y && abs(cameraSlideToSpeed.y) < 8) cameraSlideToSpeed.y = 8;
224 cameraSlideToPlayer = 1;
229 // if `frameSkip` is `true`, there are more frames waiting
230 // (i.e. you may skip rendering and such)
231 transient void delegate (bool frameSkip) onBeforeFrame;
232 transient void delegate (bool frameSkip) onAfterFrame;
234 transient void delegate () onCameraTeleported;
236 transient void delegate () onLevelExitedCB;
238 // this will be called in-between frames, and
239 // `frameTime` is [0..1)
240 transient void delegate (float frameTime) onInterFrame;
242 final int bizRoomStyle { get { return (lg ? lg.bizRoomStyle : 0); } }
245 final bool isNormalLevel () { return (levelKind == LevelKind.Normal); }
246 final bool isTitleRoom () { return (levelKind == LevelKind.Title); }
247 final bool isTutorialRoom () { return (levelKind == LevelKind.Tutorial); }
248 final bool isTransitionRoom () { return (levelKind == LevelKind.Transition); }
249 final bool isIntroRoom () { return (levelKind == LevelKind.Transition); }
252 bool isHUDEnabled () {
253 if (inWinCutscene) return false;
254 if (inIntroCutscene) return false;
255 if (lg.finalBossLevel) return true;
256 if (isNormalLevel()) return true;
261 // ////////////////////////////////////////////////////////////////////////// //
263 void addDeath (name aname) { if (isNormalLevel()) stats.addDeath(aname); }
270 void addKill (name aname, optional bool telefrag) {
271 if (isNormalLevel()) stats.addKill(aname, telefrag!optional);
272 else if (aname == 'Shopkeeper' && levelKind == LevelKind.Stars) { ++stats.starsKills; ++starsKills; }
275 void addCollect (name aname, optional int amount) { if (isNormalLevel()) stats.addCollect(aname, amount!optional); }
277 void addDamselSaved () { if (isNormalLevel()) stats.addDamselSaved(); }
278 void addIdolStolen () { if (isNormalLevel()) stats.addIdolStolen(); }
279 void addIdolConverted () { if (isNormalLevel()) stats.addIdolConverted(); }
280 void addCrystalIdolStolen () { if (isNormalLevel()) stats.addCrystalIdolStolen(); }
281 void addCrystalIdolConverted () { if (isNormalLevel()) stats.addCrystalIdolConverted(); }
282 void addGhostSummoned () { if (isNormalLevel()) stats.addGhostSummoned(); }
285 // ////////////////////////////////////////////////////////////////////////// //
286 static final string time2str (int time) {
287 int secs = time%60; time /= 60;
288 int mins = time%60; time /= 60;
289 int hours = time%24; time /= 24;
291 if (days) return va("%d DAYS, %d:%02d:%02d", days, hours, mins, secs);
292 if (hours) return va("%d:%02d:%02d", hours, mins, secs);
293 return va("%02d:%02d", mins, secs);
297 // ////////////////////////////////////////////////////////////////////////// //
298 final int tilesWidth () { return lg.levelRoomWidth*RoomGen::Width+2; }
299 final int tilesHeight () { return (lg.finalBossLevel ? 55 : lg.levelRoomHeight*RoomGen::Height+2); }
302 // ////////////////////////////////////////////////////////////////////////// //
303 protected void resetGameInternal () {
304 if (player) player.removeBallAndChain();
307 allowFinalCutsceneSkip = true;
308 //inIntroCutscene = 0;
320 player.removeBallAndChain();
321 auto hi = player.holdItem;
322 player.holdItem = none;
323 if (hi) hi.instanceRemove();
324 hi = player.pickedItem;
325 player.pickedItem = none;
326 if (hi) hi.instanceRemove();
333 stats.clearGameTotals();
334 someTilesRemoved = false;
338 // this won't generate a level yet
339 void restartGame () {
341 if (global.startMoney > 0) stats.setMoneyCheat();
342 stats.setMoney(global.startMoney);
343 levelKind = LevelKind.Normal;
347 // complement function to `restart game`
348 void generateNormalLevel () {
350 centerViewAtPlayer();
354 void restartTitle () {
357 createSpecialLevel(LevelKind.Title, &createTitleRoom, 'musTitle');
366 void restartIntro () {
369 createSpecialLevel(LevelKind.Intro, &createIntroRoom, '');
378 void restartTutorial () {
381 createSpecialLevel(LevelKind.Tutorial, &createTutorialRoom, 'musCave');
390 void restartScores () {
393 createSpecialLevel(LevelKind.Scores, &createScoresRoom, 'musTitle');
402 void restartStarsRoom () {
405 createSpecialLevel(LevelKind.Stars, &createStarsRoom, '');
414 void restartSunRoom () {
417 createSpecialLevel(LevelKind.Sun, &createSunRoom, '');
426 void restartMoonRoom () {
429 createSpecialLevel(LevelKind.Moon, &createMoonRoom, '');
438 // ////////////////////////////////////////////////////////////////////////// //
439 // generate angry shopkeeper at exit if murderer or thief
440 void generateAngryShopkeepers () {
441 if (global.murderer || global.thiefLevel > 0) {
442 foreach (MapTile e; allExits) {
443 if (e.specialExit || !e.isInstanceAlive) continue;
444 auto obj = MonsterShopkeeper(MakeMapObject(e.ix, e.iy, 'oShopkeeper'));
446 obj.style = 'Bounty Hunter';
447 obj.status = MapObject::PATROL;
454 // ////////////////////////////////////////////////////////////////////////// //
455 final void resetRoomBounds () {
458 viewMax.x = tilesWidth*16;
459 viewMax.y = tilesHeight*16;
460 // Great Lake is bottomless (nope)
461 //if (global.lake == 1) viewMax.y -= 16;
462 //writeln("ROOM BOUNDS: (", viewMax.x, "x", viewMax.y, ")");
466 final void setRoomBounds (int x0, int y0, int x1, int y1) {
474 // ////////////////////////////////////////////////////////////////////////// //
477 float timeout; // seconds
478 float starttime; // for active
479 bool active; // true: timeout is `GetTickCount()` dismissing time
482 array!OSDMessage msglist; // [0]: current one
484 struct OSDMessageTalk {
486 float timeout; // seconds;
487 float starttime; // for active
488 bool active; // true: timeout is `GetTickCount()` dismissing time
489 bool shopOnly; // true: timeout when player exited the shop
490 int hiColor1; // -1: default
491 int hiColor2; // -1: default
494 array!OSDMessageTalk msgtalklist; // [0]: current one
497 private final void osdCheckTimeouts () {
498 auto stt = GetTickCount();
499 while (msglist.length) {
500 if (!msglist[0].msg) { msglist.remove(0); continue; }
501 if (!msglist[0].active) {
502 msglist[0].active = true;
503 msglist[0].starttime = stt;
505 if (msglist[0].starttime+msglist[0].timeout >= stt) break;
508 if (msgtalklist.length) {
509 bool inshop = isInShop(player.ix/16, player.iy/16);
510 while (msgtalklist.length) {
511 if (!msgtalklist[0].msg) { msgtalklist.remove(0); continue; }
512 if (msgtalklist[0].shopOnly) {
513 if (inshop == msgtalklist[0].active) {
514 msgtalklist[0].active = !inshop;
515 if (!inshop) msgtalklist[0].starttime = stt;
518 if (!msgtalklist[0].active) {
519 msgtalklist[0].active = true;
520 msgtalklist[0].starttime = stt;
523 if (!msgtalklist[0].active) break;
524 //writeln("timedelta: ", msgtalklist[0].starttime+msgtalklist[0].timeout-stt);
525 if (msgtalklist[0].starttime+msgtalklist[0].timeout >= stt) break;
526 msgtalklist.remove(0);
532 final bool osdHasMessage () {
534 return (msglist.length > 0);
538 final string osdGetMessage (out float timeLeft, out float timeStart) {
540 if (msglist.length == 0) { timeLeft = 0; return ""; }
541 auto stt = GetTickCount();
542 timeStart = msglist[0].starttime;
543 timeLeft = fmax(0, msglist[0].starttime+msglist[0].timeout-stt);
544 return msglist[0].msg;
548 final string osdGetTalkMessage (optional out int hiColor1, optional out int hiColor2) {
550 if (msgtalklist.length == 0) return "";
551 hiColor1 = msgtalklist[0].hiColor1;
552 hiColor2 = msgtalklist[0].hiColor2;
553 return msgtalklist[0].msg;
557 final void osdClear (optional bool clearTalk) {
559 if (clearTalk) msgtalklist.clear();
563 final void osdMessage (string msg, optional float timeout, optional bool urgent) {
565 msg = global.expandString(msg);
566 if (!specified_timeout) timeout = 3.33;
567 // special message for shops
568 if (timeout == -666) {
570 if (msglist.length && msglist[0].msg == msg) return;
571 if (msglist.length == 0 || msglist[0].msg != msg) {
572 osdClear(clearTalk:false);
574 msglist[0].msg = msg;
576 msglist[0].active = false;
577 msglist[0].timeout = 3.33;
581 if (timeout < 0.1) return;
582 timeout = fmax(1.0, timeout);
583 //writeln("OSD: ", msg);
584 // find existing one, and bring it to the top
586 for (; oldidx < msglist.length; ++oldidx) {
587 if (msglist[oldidx].msg == msg) break; // i found her!
590 if (oldidx < msglist.length) {
591 // yeah, move duplicate to the top
592 msglist[oldidx].timeout = fmax(timeout, msglist[oldidx].timeout);
593 msglist[oldidx].active = false;
594 if (urgent && oldidx != 0) {
595 timeout = msglist[oldidx].timeout;
596 msglist.remove(oldidx);
598 msglist[0].msg = msg;
599 msglist[0].timeout = timeout;
600 msglist[0].active = false;
604 msglist[0].msg = msg;
605 msglist[0].timeout = timeout;
606 msglist[0].active = false;
610 msglist[$-1].msg = msg;
611 msglist[$-1].timeout = timeout;
612 msglist[$-1].active = false;
618 void osdMessageTalk (string msg, optional bool replace, optional float timeout, optional bool inShopOnly,
619 optional int hiColor1, optional int hiColor2)
622 //writeln("talk msg: replace=", replace, "; timeout=", timeout, "; inshop=", inShopOnly, "; msg=", msg);
623 if (!specified_timeout) timeout = 3.33;
624 if (!specified_inShopOnly) inShopOnly = true;
625 if (!specified_hiColor1) hiColor1 = -1;
626 if (!specified_hiColor2) hiColor2 = -1;
627 msg = global.expandString(msg);
629 if (!msg) { msgtalklist.clear(); return; }
630 if (msgtalklist.length && msgtalklist[0].msg == msg) {
631 while (msgtalklist.length > 1) msgtalklist.remove(1);
632 msgtalklist[$-1].timeout = timeout;
633 msgtalklist[$-1].shopOnly = inShopOnly;
635 if (msgtalklist.length) msgtalklist.clear();
636 msgtalklist.length += 1;
637 msgtalklist[$-1].msg = msg;
638 msgtalklist[$-1].timeout = timeout;
639 msgtalklist[$-1].active = false;
640 msgtalklist[$-1].shopOnly = inShopOnly;
641 msgtalklist[$-1].hiColor1 = hiColor1;
642 msgtalklist[$-1].hiColor2 = hiColor2;
647 foreach (auto midx, ref auto mnfo; msgtalklist) {
648 if (mnfo.msg == msg) {
649 mnfo.timeout = timeout;
650 mnfo.shopOnly = inShopOnly;
655 msgtalklist.length += 1;
656 msgtalklist[$-1].msg = msg;
657 msgtalklist[$-1].timeout = timeout;
658 msgtalklist[$-1].active = false;
659 msgtalklist[$-1].shopOnly = inShopOnly;
660 msgtalklist[$-1].hiColor1 = hiColor1;
661 msgtalklist[$-1].hiColor2 = hiColor2;
668 // ////////////////////////////////////////////////////////////////////////// //
669 void setup (GameGlobal aGlobal, SpriteStore aSprStore, BackTileStore aBGTileStore) {
671 sprStore = aSprStore;
672 bgtileStore = aBGTileStore;
674 lg = SpawnObject(LevelGen);
678 objGrid = SpawnObject(EntityGrid);
679 objGrid.setup(0, 0, MaxTilesWidth*16, MaxTilesHeight*16);
683 // stores should be set
687 levBGImg = bgtileStore[levBGImgName];
688 foreach (MapEntity o; objGrid.allObjects()) {
691 if (t && (t.lava || t.water)) ++liquidTileCount;
693 for (MapBackTile bt = backtiles; bt; bt = bt.next) bt.onLoaded();
694 if (player) player.onLoaded();
696 if (msglist.length) {
697 msglist[0].active = false;
698 msglist[0].timeout = 0.200;
701 lastMusicName = (lg ? lg.musicName : '');
702 global.setMusicPitch(1.0);
703 if (lg && lg.musicName) global.playMusic(lg.musicName); else global.stopMusic();
707 // ////////////////////////////////////////////////////////////////////////// //
708 void pickedSpectacles () {
709 foreach (MapTile t; objGrid.allObjectsSafe(MapTile)) t.onGotSpectacles();
713 // ////////////////////////////////////////////////////////////////////////// //
714 #include "rgentile.vc"
715 #include "rgenobj.vc"
718 void onLevelExited () {
719 if (playerExitDoor isa TitleTileXTitle) {
720 playerExitDoor = none;
725 if (isTitleRoom() || levelKind == LevelKind.Scores) {
726 if (playerExitDoor) processTitleExit(playerExitDoor);
727 playerExitDoor = none;
730 if (isTutorialRoom()) {
731 playerExitDoor = none;
733 //global.currLevel = 1;
734 //generateNormalLevel();
735 global.currLevel = 0;
736 generateTransitionLevel();
740 if (levelKind == LevelKind.Stars || levelKind == LevelKind.Sun || levelKind == LevelKind.Moon) {
741 playerExitDoor = none;
743 if (onLevelExitedCB) onLevelExitedCB();
748 if (isNormalLevel()) {
749 stats.maxLevelComplete = max(stats.maxLevelComplete, global.currLevel);
751 global.genBlackMarket = false;
752 if (playerExitDoor) {
753 if (playerExitDoor.objType == 'oXGold') {
754 writeln("exiting to City Of Gold");
755 global.cityOfGold = -1;
756 //!global.currLevel += 1;
757 } else if (playerExitDoor.objType == 'oXMarket') {
758 writeln("exiting to Black Market");
759 global.genBlackMarket = true;
760 //!global.currLevel += 1;
762 writeln("exit door(", GetClassName(playerExitDoor.Class), "): '", playerExitDoor.objType, "'");
765 writeln("WTF?! NO EXIT DOOR!");
768 if (onLevelExitedCB) onLevelExitedCB();
770 playerExitDoor = none;
771 if (levelKind == LevelKind.Transition) {
772 if (global.thiefLevel > 0) global.thiefLevel -= 1;
773 if (global.alienCraft) ++global.alienCraft;
774 if (global.yetiLair) ++global.yetiLair;
775 if (global.lake) ++global.lake;
776 if (global.cityOfGold) { if (++global.cityOfGold == 0) global.cityOfGold = 1; }
777 //orig dbg:if (global.currLevel == 1) global.currLevel += firstLevelSkip; else global.currLevel += levelSkip;
779 if (!global.blackMarket && !global.cityOfGold /*&& !global.yetiLair*/) {
780 global.currLevel += 1;
786 // < 20 seconds per level: looks like a speedrun
787 global.noDarkLevel = (levelEndTime > levelStartTime && levelEndTime-levelStartTime < 20*30);
788 if (lg.finalBossLevel) {
790 allowFinalCutsceneSkip = (stats.gamesWon != 0);
792 // add money for big idol
793 player.addScore(50000);
797 generateTransitionLevel();
800 //centerViewAtPlayer();
804 void onOlmecDead (MapObject o) {
805 writeln("*** OLMEC IS DEAD!");
806 foreach (MapTile t; allExits) {
809 auto st = checkTileAtPoint(t.ix+8, t.iy+16);
811 st = MakeMapTile(t.ix/16, t.iy/16+1, 'oTemple');
814 st.invincible = true;
820 void generateLevelMessages () {
821 writeln("LEVEL NUMBER: ", global.currLevel);
822 if (global.darkLevel) {
823 if (global.hasCrown) {
824 osdMessage("THE HEDJET SHINES BRIGHTLY.");
825 global.darkLevel = false;
826 } else if (global.config.scumDarkness < 2) {
827 osdMessage("I CAN'T SEE A THING!\nI'D BETTER USE THESE FLARES!");
831 if (global.blackMarket) osdMessage("WELCOME TO THE BLACK MARKET!");
833 if (global.cemetary) osdMessage("THE DEAD ARE RESTLESS!");
834 if (global.lake == 1) osdMessage("I CAN HEAR RUSHING WATER...");
836 if (global.snakePit) osdMessage("I HEAR SNAKES... I HATE SNAKES!");
837 if (global.yetiLair == 1) osdMessage("IT SMELLS LIKE WET FUR IN HERE!");
838 if (global.alienCraft == 1) osdMessage("THERE'S A PSYCHIC PRESENCE HERE!");
839 if (global.cityOfGold == 1) {
840 if (true /*hasOlmec()*/) osdMessage("IT'S THE LEGENDARY CITY OF GOLD!");
843 if (global.sacrificePit) osdMessage("I CAN HEAR PRAYERS TO KALI!");
847 final MapObject debugSpawnObjectWithClass (class!MapObject oclass, optional bool playerDir) {
848 if (!oclass) return none;
850 bool canLeft = !isSolidAtPoint(player.x0-12, player.yCenter);
851 bool canRight = !isSolidAtPoint(player.x1+12, player.yCenter);
852 if (!canLeft && !canRight) return none;
853 if (canLeft && canRight) {
855 dx = (player.dir == MapObject::Dir.Left ? -16 : 16);
860 dx = (canLeft ? -16 : 16);
862 auto obj = SpawnMapObjectWithClass(oclass);
863 if (obj isa MapEnemy) {
865 dy -= (obj isa MonsterDamsel ? 2 : 8);
867 if (obj) PutSpawnedMapObject(player.ix+dx, player.iy+dy, obj);
872 final MapObject debugSpawnObject (name aname) {
873 if (!aname) return none;
874 return debugSpawnObjectWithClass(findGameObjectClassByName(aname));
878 void createSpecialLevel (LevelKind kind, scope void delegate () creator, name amusic) {
879 global.darkLevel = false;
883 global.resetStartingItems();
885 transitionLevelIndex = 0;
887 global.setMusicPitch(1.0);
890 auto olddel = ImmediateDelete;
891 ImmediateDelete = false;
899 addBackgroundGfxDetails();
900 //levBGImgName = 'bgCave';
901 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
903 blockWaterChecking = true;
907 ImmediateDelete = olddel;
908 CollectGarbage(true); // destroy delayed objects too
910 if (dumpGridStats) objGrid.dumpStats();
912 playerExited = false; // just in case
913 playerExitDoor = none;
915 osdClear(clearTalk:true);
918 lg.musicName = amusic;
919 lastMusicName = amusic;
920 global.setMusicPitch(1.0);
921 if (amusic) global.playMusic(lg.musicName); else global.stopMusic();
922 someTilesRemoved = false;
926 void createTitleLevel () {
927 createSpecialLevel(LevelKind.Title, &createTitleRoom, 'musTitle');
931 void createTutorialLevel () {
932 createSpecialLevel(LevelKind.Tutorial, &createTutorialRoom, 'musCave');
941 // `global.currLevel` is the new level
942 void generateTransitionLevel () {
943 global.darkLevel = false;
948 transitionLevelIndex = 0;
950 resetTransitionOverlay();
952 global.setMusicPitch(1.0);
953 switch (global.config.transitionMusicMode) {
954 case GameConfig::MusicMode.Silent: global.stopMusic(); break;
955 case GameConfig::MusicMode.Restart: global.stopMusic(); global.playMusic(lastMusicName); break;
956 case GameConfig::MusicMode.DontTouch: break;
959 levelKind = LevelKind.Transition;
961 auto olddel = ImmediateDelete;
962 ImmediateDelete = false;
965 if (global.currLevel < 4) { createTrans1Room(); transitionLevelIndex = 0; }
966 else if (global.currLevel == 4) { createTrans1xRoom(); transitionLevelIndex = 1; }
967 else if (global.currLevel < 8) { createTrans2Room(); transitionLevelIndex = 0; }
968 else if (global.currLevel == 8) { createTrans2xRoom(); transitionLevelIndex = 2; }
969 else if (global.currLevel < 12) { createTrans3Room(); transitionLevelIndex = 0; }
970 else if (global.currLevel == 12) { createTrans3xRoom(); transitionLevelIndex = 3; }
971 else if (global.currLevel < 16) { createTrans4Room(); transitionLevelIndex = 0; }
972 else if (global.currLevel == 16) { createTrans4Room(); transitionLevelIndex = 0; }
973 else { createTrans1Room(); transitionLevelIndex = 0; } //???
975 bool createTunnelMan = true;
976 if (global.config.scumUnlocked || global.isTunnelMan) {
977 createTunnelMan = false;
978 } else if (stats.money > 0) {
979 // WARNING! call `stats.needTunnelMan()` only once!
980 createTunnelMan = stats.needTunnelMan(transitionLevelIndex);
982 createTunnelMan = false;
985 if (!createTunnelMan) {
986 // don't create tunnel man
987 if (/*global.config.bizarre &&*/ global.randOther(1, 3) == 1) {
988 if (global.randOther(1, 3) == 1) MakeMapObject(56+global.randOther(1, 6)*16, 188, 'oRock');
989 else if (global.randOther(1, 3) == 1) MakeMapObject(56+global.randOther(1, 6)*16, 186, 'oJar');
991 MakeMapObject(48+global.randOther(1, 6)*16, 176, 'oBones');
992 MakeMapObject(48+global.randOther(1, 6)*16, 188, 'oSkull');
995 if (global.config.bizarre && global.randOther(1, 5) == 1) MakeMapObject(16+global.randOther(1, 16)*16, 144, 'oWeb');
998 MakeMapObject(96+8, 176+8, 'oTunnelMan');
1002 setMenuTilesOnTop();
1005 addBackgroundGfxDetails();
1006 //levBGImgName = 'bgCave';
1007 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
1009 blockWaterChecking = true;
1013 if (damselSaved > 0) {
1014 // this is special "damsel ready to kiss you" object, not a heart
1015 MakeMapObject(176+8, 176+8, 'oDamselKiss');
1016 global.plife += damselSaved; // if player skipped transition cutscene
1020 ImmediateDelete = olddel;
1021 CollectGarbage(true); // destroy delayed objects too
1023 if (dumpGridStats) objGrid.dumpStats();
1025 playerExited = false; // just in case
1026 playerExitDoor = none;
1028 osdClear(clearTalk:true);
1031 //global.playMusic(lg.musicName);
1032 someTilesRemoved = false;
1036 void generateLevel () {
1037 levelStartTime = time;
1038 levelEndTime = time;
1040 transitionLevelIndex = 0;
1045 global.genBlackMarket = false;
1048 global.setMusicPitch(1.0);
1049 stats.clearLevelTotals();
1051 levelKind = LevelKind.Normal;
1058 //writeln("tw:", tilesWidth, "; th:", tilesHeight);
1060 auto olddel = ImmediateDelete;
1061 ImmediateDelete = false;
1064 if (lg.finalBossLevel) {
1065 blockWaterChecking = true;
1069 // if transition cutscene was skipped...
1070 global.plife += max(0, damselSaved); // if player skipped transition cutscene
1074 startRoomX = lg.startRoomX;
1075 startRoomY = lg.startRoomY;
1076 endRoomX = lg.endRoomX;
1077 endRoomY = lg.endRoomY;
1078 addBackgroundGfxDetails();
1079 foreach (int y; 0..tilesHeight) {
1080 foreach (int x; 0..tilesWidth) {
1086 levBGImgName = lg.bgImgName;
1087 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
1089 if (global.allowAngryShopkeepers) generateAngryShopkeepers();
1091 lg.generateEntities();
1093 // add box of flares to dark level
1094 if (global.darkLevel && allEnters.length) {
1095 auto enter = allEnters[0];
1096 int x = enter.ix, y = enter.iy;
1097 if (!isSolidAtPoint(x-16, y)) MakeMapObject(x-16+8, y+8, 'oFlareCrate');
1098 else if (!isSolidAtPoint(x+16, y)) MakeMapObject(x+16+8, y+8, 'oFlareCrate');
1099 else MakeMapObject(x+8, y+8, 'oFlareCrate');
1102 //scrGenerateEntities();
1103 //foreach (; 0..2) scrGenerateEntities();
1105 writeln(objGrid.countObjects, " alive objects inserted");
1106 writeln(countBackTiles, " background tiles inserted");
1108 if (!player) FatalError("player pawn is not spawned");
1110 if (lg.finalBossLevel) {
1111 blockWaterChecking = true;
1113 blockWaterChecking = false;
1118 ImmediateDelete = olddel;
1119 CollectGarbage(true); // destroy delayed objects too
1121 if (dumpGridStats) objGrid.dumpStats();
1123 playerExited = false; // just in case
1124 playerExitDoor = none;
1126 levelMoneyStart = stats.money;
1128 osdClear(clearTalk:true);
1129 generateLevelMessages();
1134 //writeln("!<", lastMusicName, ">:<", lg.musicName, ">");
1135 global.setMusicPitch(1.0);
1136 if (lastMusicName != lg.musicName) {
1137 global.playMusic(lg.musicName);
1139 writeln("MM: ", global.config.nextLevelMusicMode);
1140 switch (global.config.nextLevelMusicMode) {
1141 case GameConfig::MusicMode.Silent: global.stopMusic(); break; // the thing that should not be
1142 case GameConfig::MusicMode.Restart: global.stopMusic(); global.playMusic(lg.musicName); break;
1143 case GameConfig::MusicMode.DontTouch:
1144 if (global.config.transitionMusicMode == GameConfig::MusicMode.Silent) {
1145 global.playMusic(lg.musicName);
1150 lastMusicName = lg.musicName;
1151 //global.playMusic(lg.musicName);
1154 if (global.cityOfGold == 1 || global.genBlackMarket) resetBMCOG = true;
1156 if (global.cityOfGold == 1) {
1157 lg.mapSprite = 'sMapTemple';
1158 lg.mapTitle = "City of Gold";
1159 } else if (global.blackMarket) {
1160 lg.mapSprite = 'sMapJungle';
1161 lg.mapTitle = "Black Market";
1164 someTilesRemoved = false;
1168 // ////////////////////////////////////////////////////////////////////////// //
1169 int currKeys, nextKeys;
1170 int pressedKeysQ, releasedKeysQ;
1171 int keysPressed, keysReleased = -1;
1174 struct SavedKeyState {
1175 int currKeys, nextKeys;
1176 int pressedKeysQ, releasedKeysQ;
1177 int keysPressed, keysReleased;
1179 int roomSeed, otherSeed;
1183 // for saving/replaying
1184 final void keysSaveState (out SavedKeyState ks) {
1185 ks.currKeys = currKeys;
1186 ks.nextKeys = nextKeys;
1187 ks.pressedKeysQ = pressedKeysQ;
1188 ks.releasedKeysQ = releasedKeysQ;
1189 ks.keysPressed = keysPressed;
1190 ks.keysReleased = keysReleased;
1193 // for saving/replaying
1194 final void keysRestoreState (const ref SavedKeyState ks) {
1195 currKeys = ks.currKeys;
1196 nextKeys = ks.nextKeys;
1197 pressedKeysQ = ks.pressedKeysQ;
1198 releasedKeysQ = ks.releasedKeysQ;
1199 keysPressed = ks.keysPressed;
1200 keysReleased = ks.keysReleased;
1204 final void keysNextFrame () {
1205 currKeys = nextKeys;
1209 final void clearKeys () {
1219 final void onKey (int code, bool down) {
1224 if (keysReleased&code) {
1225 keysPressed |= code;
1226 keysReleased &= ~code;
1227 pressedKeysQ |= code;
1231 if (keysPressed&code) {
1232 keysReleased |= code;
1233 keysPressed &= ~code;
1234 releasedKeysQ |= code;
1239 final bool isKeyDown (int code) {
1240 return !!(currKeys&code);
1243 final bool isKeyPressed (int code) {
1244 bool res = !!(pressedKeysQ&code);
1245 pressedKeysQ &= ~code;
1249 final bool isKeyReleased (int code) {
1250 bool res = !!(releasedKeysQ&code);
1251 releasedKeysQ &= ~code;
1256 final void clearKeysPressRelease () {
1257 keysPressed = default.keysPressed;
1258 keysReleased = default.keysReleased;
1259 pressedKeysQ = default.pressedKeysQ;
1260 releasedKeysQ = default.releasedKeysQ;
1266 // ////////////////////////////////////////////////////////////////////////// //
1267 final void registerEnter (MapTile t) {
1274 final void registerExit (MapTile t) {
1281 final bool isYAtEntranceRow (int py) {
1283 foreach (MapTile t; allEnters) if (t.iy == py) return true;
1288 final int calcNearestEnterDist (int px, int py) {
1289 if (allEnters.length == 0) return int.max;
1290 int curdistsq = int.max;
1291 foreach (MapTile t; allEnters) {
1292 int xc = px-t.xCenter, yc = py-t.yCenter;
1293 int distsq = xc*xc+yc*yc;
1294 if (distsq < curdistsq) curdistsq = distsq;
1296 return round(sqrt(curdistsq));
1300 final int calcNearestExitDist (int px, int py) {
1301 if (allExits.length == 0) return int.max;
1302 int curdistsq = int.max;
1303 foreach (MapTile t; allExits) {
1304 int xc = px-t.xCenter, yc = py-t.yCenter;
1305 int distsq = xc*xc+yc*yc;
1306 if (distsq < curdistsq) curdistsq = distsq;
1308 return round(sqrt(curdistsq));
1312 // ////////////////////////////////////////////////////////////////////////// //
1313 final void clearForTransition () {
1314 auto olddel = ImmediateDelete;
1315 ImmediateDelete = false;
1317 ImmediateDelete = olddel;
1318 CollectGarbage(true); // destroy delayed objects too
1319 global.darkLevel = false;
1323 // ////////////////////////////////////////////////////////////////////////// //
1324 final int countBackTiles () {
1326 for (MapBackTile bt = backtiles; bt; bt = bt.next) ++res;
1331 final void clearWholeLevel () {
1334 clearActiveEntities();
1336 // don't kill objects the player is holding
1338 if (player.pickedItem isa ItemBall) {
1339 player.pickedItem.instanceRemove();
1340 player.pickedItem = none;
1342 if (player.pickedItem && player.pickedItem.grid) {
1343 player.pickedItem.grid.remove(player.pickedItem.gridId);
1344 writeln("secured pocket item '", GetClassName(player.pickedItem.Class), "'");
1346 if (player.holdItem isa ItemBall) {
1347 player.removeBallAndChain(temp:true);
1348 if (player.holdItem) player.holdItem.instanceRemove();
1349 player.holdItem = none;
1351 if (player.holdItem && player.holdItem.grid) {
1352 player.holdItem.grid.remove(player.holdItem.gridId);
1353 writeln("secured held item '", GetClassName(player.holdItem.Class), "'");
1355 writeln("secured ball; mustBeChained=", player.mustBeChained, "; wasHoldingBall=", player.wasHoldingBall);
1358 int count = objGrid.countObjects();
1359 if (dumpGridStats) { if (objGrid.getFirstObjectCID()) objGrid.dumpStats(); }
1360 objGrid.removeAllObjects(true); // and destroy
1361 if (count > 0) writeln(count, " objects destroyed");
1363 lastUsedObjectId = 0;
1366 lastRenderTime = -1;
1367 liquidTileCount = 0;
1371 MapBackTile t = backtiles;
1377 framesProcessedFromLastClear = 0;
1381 final void insertObject (MapEntity o) {
1383 if (o.grid) FatalError("cannot put object into level twice");
1385 if (o.active || o isa MapObject) addActiveEntity(o);
1389 final void reinsertObject (MapEntity o) {
1390 if (!o || !o.isInstanceAlive) return;
1391 if (o.grid) o.grid.remove(o.gridId);
1393 if (o.active || o isa MapObject) addActiveEntity(o);
1397 final void spawnPlayerAt (int x, int y) {
1398 // if we have no player, spawn new one
1399 // otherwise this just a level transition, so simply reposition him
1401 // don't add player to object list, as it has very separate processing anyway
1402 player = SpawnObject(PlayerPawn);
1403 player.global = global;
1404 player.level = self;
1405 if (!player.initialize()) {
1407 FatalError("something is wrong with player initialization");
1413 player.saveInterpData();
1415 if (player.mustBeChained || global.config.scumBallAndChain) {
1416 writeln("*** spawning ball and chain");
1417 player.spawnBallAndChain(levelStart:true);
1419 playerExited = false;
1420 playerExitDoor = none;
1421 if (global.config.startWithKapala) global.hasKapala = true;
1422 centerViewAtPlayer();
1423 // reinsert player items into grid
1424 if (player.pickedItem) reinsertObject(player.pickedItem);
1425 if (player.holdItem) reinsertObject(player.holdItem);
1426 //writeln("player spawned; active=", player.active);
1427 player.scrSwitchToPocketItem(forceIfEmpty:false);
1431 final void teleportPlayerTo (int x, int y) {
1435 player.saveInterpData();
1440 final void resurrectPlayer () {
1441 if (player) player.resurrect();
1442 playerExited = false;
1443 playerExitDoor = none;
1447 // ////////////////////////////////////////////////////////////////////////// //
1448 final void scrShake (int duration) {
1449 if (shakeLeft == 0) {
1455 shakeLeft = max(shakeLeft, duration);
1460 // ////////////////////////////////////////////////////////////////////////// //
1463 ItemStolen, // including damsel, lol
1469 // checks for dead, agnered, distance, etc. should be already done
1470 protected void doAngerShopkeeper (MonsterShopkeeper shp, SCAnger reason, ref bool messaged,
1471 int maxdist, MapEntity offender)
1473 if (!shp || shp.dead || shp.angered) return;
1474 if (offender.distanceToEntityCenter(shp) > maxdist) return;
1476 shp.status = MapObject::ATTACK;
1478 if (global.murderer) {
1479 msg = "~YOU'LL PAY FOR YOUR CRIMES!~";
1482 case SCAnger.TileDestroyed: msg = "~DIE, YOU VANDAL!~"; break;
1483 case SCAnger.ItemStolen: msg = "~COME BACK HERE, THIEF!~"; break;
1484 case SCAnger.CrapsCheated: msg = "~DIE, CHEATER!~"; break;
1485 case SCAnger.BombDropped: msg = "~TERRORIST!~"; break;
1486 case SCAnger.DamselWhipped: msg = "~HEY, ONLY I CAN DO THAT!~"; break;
1487 default: "~NOW I'M REALLY STEAMED!~"; break;
1491 writeln("shopkeeper angered; reason=", reason, "; maxdist=", maxdist, "; msg=\"", msg, "\"");
1494 if (msg) osdMessageTalk(msg, replace:true, inShopOnly:false, hiColor1:0xff_00_00);
1495 global.thiefLevel += (global.thiefLevel > 0 ? 3 : 2);
1500 // make the nearest shopkeeper angry. RAWR!
1501 void scrShopkeeperAnger (SCAnger reason, optional int maxdist, optional MapEntity offender) {
1502 bool messaged = false;
1503 maxdist = clamp(maxdist, 96, 100000);
1504 if (!offender) offender = player;
1505 if (maxdist == 100000) {
1506 foreach (MonsterShopkeeper shp; objGrid.allObjects(MonsterShopkeeper)) {
1507 doAngerShopkeeper(shp, reason, messaged, maxdist, offender);
1510 foreach (MonsterShopkeeper shp; objGrid.inRectPix(offender.xCenter-maxdist-128, offender.yCenter-maxdist-128, (maxdist+128)*2, (maxdist+128)*2, precise:false, castClass:MonsterShopkeeper)) {
1511 doAngerShopkeeper(shp, reason, messaged, maxdist, offender);
1517 final MapObject findCrapsPrize () {
1518 foreach (MapObject o; objGrid.allObjects(MapObject)) {
1519 if (!o.spectral && o.inDiceHouse) return o;
1525 // ////////////////////////////////////////////////////////////////////////// //
1526 // 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.
1527 // note: idols moved by monkeys will have false `stolenIdol`
1528 void scrTriggerIdolAltar (bool stolenIdol) {
1529 ObjTikiCurse res = none;
1530 int curdistsq = int.max;
1531 int px = player.xCenter, py = player.yCenter;
1532 foreach (MapObject o; objGrid.allObjects(MapObject)) {
1533 auto tcr = ObjTikiCurse(o);
1535 if (tcr.activated) continue;
1536 int xc = px-tcr.xCenter, yc = py-tcr.yCenter;
1537 int distsq = xc*xc+yc*yc;
1538 if (distsq < curdistsq) {
1543 if (res) res.activate(stolenIdol);
1547 // ////////////////////////////////////////////////////////////////////////// //
1548 void setupGhostTime () {
1549 musicFadeTimer = -1;
1550 ghostSpawned = false;
1552 // there is no ghost on the first level
1553 if (inWinCutscene || inIntroCutscene || !isNormalLevel() || lg.finalBossLevel ||
1554 (!global.config.ghostAtFirstLevel && global.currLevel == 1))
1557 global.setMusicPitch(1.0);
1561 if (global.config.scumGhost < 0) {
1564 osdMessage("A GHASTLY VOICE CALLS YOUR NAME!\nLET'S GET OUT OF HERE!", 6.66, urgent:true);
1568 if (global.config.scumGhost == 0) {
1574 // randomizes time until ghost appears once time limit is reached
1575 // global.ghostRandom (true/false) if extra time is randomized - loaded from yasm.cfg. if false, use 30 second default
1576 // ghostTimeLeft (time in seconds * 1000) for currently generated level
1578 if (global.config.ghostRandom) {
1579 auto tMin = global.randOther(10, 30); // min(global.plife*10, 30);
1580 auto tMax = max(tMin+global.randOther(10, 30), min(global.config.scumGhost, 300)); // 5 minutes max
1581 auto tTime = global.randOther(tMin, tMax);
1582 if (tTime <= 0) tTime = round(tMax/2.0);
1583 ghostTimeLeft = tTime;
1585 ghostTimeLeft = global.config.scumGhost; // 5 minutes max
1588 ghostTimeLeft += max(0, global.config.ghostExtraTime);
1590 ghostTimeLeft *= 30; // seconds -> frames
1591 //global.ghostShowTime
1595 void spawnGhost () {
1597 ghostSpawned = true;
1600 int vwdt = (viewMax.x-viewMin.x);
1601 int vhgt = (viewMax.y-viewMin.y);
1605 if (player.ix < viewMin.x+vwdt/2) {
1606 // player is in the left side
1607 gx = viewMin.x+vwdt/2+vwdt/4;
1609 // player is in the right side
1610 gx = viewMin.x+vwdt/4;
1613 if (player.iy < viewMin.y+vhgt/2) {
1614 // player is in the left side
1615 gy = viewMin.y+vhgt/2+vhgt/4;
1617 // player is in the right side
1618 gy = viewMin.y+vhgt/4;
1621 writeln("spawning ghost at tile (", gx/16, ",", gy/16, "); player is at tile (", player.ix/16, ",", player.iy/16, ")");
1623 MakeMapObject(gx, gy, 'oGhost');
1626 if (oPlayer1.x > room_width/2) instance_create(view_xview[0]+view_wview[0]+8, view_yview[0]+floor(view_hview[0] / 2), oGhost);
1627 else instance_create(view_xview[0]-32, view_yview[0]+floor(view_hview[0]/2), oGhost);
1628 global.ghostExists = true;
1633 void thinkFrameGameGhost () {
1634 if (player.dead) return;
1635 if (!isNormalLevel()) return; // just in case
1637 if (ghostTimeLeft < 0) {
1639 if (musicFadeTimer > 0) {
1640 musicFadeTimer = -1;
1641 global.setMusicPitch(1.0);
1646 if (musicFadeTimer >= 0) {
1648 if (musicFadeTimer > 0 && musicFadeTimer <= 100*3) {
1649 float pitch = 1.0-(0.00226/3.0)*musicFadeTimer;
1650 //writeln("MFT: ", musicFadeTimer, "; pitch=", pitch);
1651 global.setMusicPitch(pitch);
1655 if (ghostTimeLeft == 0) {
1656 // she is already here!
1660 // no ghost if we have a crown
1661 if (global.hasCrown) {
1666 // if she was already spawned, don't do it again
1672 if (--ghostTimeLeft != 0) {
1674 if (global.config.ghostExtraTime > 0) {
1675 if (ghostTimeLeft == global.config.ghostExtraTime*30) {
1676 osdMessage("A CHILL RUNS UP YOUR SPINE...\nLET'S GET OUT OF HERE!", 6.66, urgent:true);
1678 if (musicFadeTimer < 0 && ghostTimeLeft <= global.config.ghostExtraTime*30) {
1686 if (player.isExitingSprite) {
1687 // no reason to spawn her, we're leaving
1696 void thinkFrameGame () {
1697 thinkFrameGameGhost();
1698 // udjat eye blinking
1699 if (global.hasUdjatEye && player) {
1700 foreach (MapTile t; allExits) {
1701 if (t isa MapTileBlackMarketDoor) {
1702 auto dm = int(player.distanceToEntityCenter(t));
1704 if (udjatAlarm < 1 || dm < udjatAlarm) udjatAlarm = dm;
1708 global.udjatBlink = false;
1711 if (udjatAlarm > 0) {
1712 if (--udjatAlarm == 0) {
1713 global.udjatBlink = !global.udjatBlink;
1714 if (global.hasUdjatEye && player) {
1715 player.playSound(global.udjatBlink ? 'sndBlink1' : 'sndBlink2');
1719 switch (levelKind) {
1720 case LevelKind.Stars: thinkFrameGameStars(); break;
1721 case LevelKind.Sun: thinkFrameGameSun(); break;
1722 case LevelKind.Moon: thinkFrameGameMoon(); break;
1723 case LevelKind.Transition: thinkFrameTransition(); break;
1724 case LevelKind.Intro: thinkFrameIntro(); break;
1729 // ////////////////////////////////////////////////////////////////////////// //
1730 private final bool isWaterTileCB (MapTile t) {
1731 return (t && t.visible && t.water);
1735 private final bool isLavaTileCB (MapTile t) {
1736 return (t && t.visible && t.lava);
1740 // ////////////////////////////////////////////////////////////////////////// //
1741 const int GreatLakeStartTileY = 28;
1744 final void fillGreatLake () {
1745 if (global.lake == 1) {
1746 foreach (int y; GreatLakeStartTileY..tilesHeight) {
1747 foreach (int x; 0..tilesWidth) {
1748 auto t = checkTileAtPoint(x*16, y*16, delegate bool (MapTile t) {
1749 if (t.spectral || !t.visible || t.invisible || t.moveable) return false;
1753 t = MakeMapTile(x, y, 'oWaterSwim');
1757 t.setSprite(y == GreatLakeStartTileY ? 'sWaterTop' : 'sWater');
1758 } else if (t.lava) {
1759 t.setSprite(y == GreatLakeStartTileY ? 'sLavaTop' : 'sLava');
1767 // called once after level generation
1768 final void fixLiquidTop () {
1769 if (global.lake == 1) fillGreatLake();
1771 liquidTileCount = 0;
1772 foreach (MapTile t; objGrid.allObjects(MapTile)) {
1773 if (!t.water && !t.lava) continue;
1776 //writeln("fixing water tile(", GetClassName(t.Class), "):'", t.objName, "' (water=", t.water, "; lava=", t.lava, "); lqc=", liquidTileCount);
1778 //if (global.lake == 1) continue; // it is done in `fillGreatLake()`
1780 if (!checkTileAtPoint(t.ix+8, t.iy-8, (t.lava ? &isLavaTileCB : &isWaterTileCB))) {
1781 t.setSprite(t.lava ? 'sLavaTop' : 'sWaterTop');
1783 // don't do this, it will destroy seaweed
1784 //t.setSprite(t.lava ? 'sLava' : 'sWater');
1785 auto spr = t.getSprite();
1786 if (!spr) t.setSprite(t.lava ? 'sLava' : 'sWater');
1787 else if (spr.Name == 'sLavaTop') t.setSprite('sLava');
1788 else if (spr.Name == 'sWaterTop') t.setSprite('sWater');
1791 //writeln("liquid tiles count: ", liquidTileCount);
1795 // ////////////////////////////////////////////////////////////////////////// //
1796 transient MapTile curWaterTile;
1797 transient bool curWaterTileCheckHitsLava;
1798 transient bool curWaterTileCheckHitsSolidOrWater; // only for `checkWaterOrSolidTilePartialCB`
1799 transient int curWaterTileLastHDir;
1800 transient ubyte[16, 16] curWaterOccupied;
1801 transient int curWaterOccupiedCount;
1802 transient int curWaterTileCheckX0, curWaterTileCheckY0;
1805 private final void clearCurWaterCheckState () {
1806 curWaterTileCheckHitsLava = false;
1807 curWaterOccupiedCount = 0;
1808 foreach (auto idx; 0..16*16) curWaterOccupied[idx] = 0;
1812 private final bool checkWaterOrSolidTileCB (MapTile t) {
1813 if (t == curWaterTile) return false;
1814 if (t.lava && curWaterTile.water) {
1815 curWaterTileCheckHitsLava = true;
1818 if (t.ix%16 != 0 || t.iy%16 != 0) {
1819 if (t.water || t.solid) {
1820 // fill occupied array
1821 //FIXME: optimize this
1822 if (curWaterOccupiedCount < 16*16) {
1823 foreach (auto dy; t.y0..t.y1+1) {
1824 foreach (auto dx; t.x0..t.x1+1) {
1825 int sx = dx-curWaterTileCheckX0;
1826 int sy = dy-curWaterTileCheckY0;
1827 if (sx >= 0 && sx <= 16 && sy >= 0 && sy <= 15 && !curWaterOccupied[sx, sy]) {
1828 curWaterOccupied[sx, sy] = 1;
1829 ++curWaterOccupiedCount;
1835 return false; // need to check for lava
1837 if (t.water || t.solid || t.lava) {
1838 curWaterOccupiedCount = 16*16;
1839 if (t.water && curWaterTile.lava) t.instanceRemove();
1841 return false; // need to check for lava
1845 private final bool checkWaterOrSolidTilePartialCB (MapTile t) {
1846 if (t == curWaterTile) return false;
1847 if (t.lava && curWaterTile.water) {
1848 //writeln("!!!!!!!!");
1849 curWaterTileCheckHitsLava = true;
1852 if (t.water || t.solid || t.lava) {
1853 //writeln("*********");
1854 curWaterTileCheckHitsSolidOrWater = true;
1855 if (t.water && curWaterTile.lava) t.instanceRemove();
1857 return false; // need to check for lava
1861 private final bool isFullyOccupiedAtTilePos (int tileX, int tileY) {
1862 clearCurWaterCheckState();
1863 curWaterTileCheckX0 = tileX*16;
1864 curWaterTileCheckY0 = tileY*16;
1865 checkTilesInRect(tileX*16, tileY*16, 16, 16, &checkWaterOrSolidTileCB);
1866 return (curWaterTileCheckHitsLava || curWaterOccupiedCount == 16*16);
1870 private final bool isAtLeastPartiallyOccupiedAtTilePos (int tileX, int tileY) {
1871 curWaterTileCheckHitsLava = false;
1872 curWaterTileCheckHitsSolidOrWater = false;
1873 checkTilesInRect(tileX*16, tileY*16, 16, 16, &checkWaterOrSolidTilePartialCB);
1874 return (curWaterTileCheckHitsSolidOrWater || curWaterTileCheckHitsLava);
1878 private final bool waterCanReachGroundHoleInDir (MapTile wtile, int dx) {
1879 if (dx == 0) return false; // just in case
1881 int x = wtile.ix/16, y = wtile.iy/16;
1883 while (x >= 0 && x < tilesWidth) {
1884 if (!isAtLeastPartiallyOccupiedAtTilePos(x, y+1)) return true;
1885 if (isAtLeastPartiallyOccupiedAtTilePos(x, y)) return false;
1892 // returns `true` if this tile must be removed
1893 private final bool checkWaterFlow (MapTile wtile) {
1894 if (global.lake == 1) {
1895 if (wtile.iy >= GreatLakeStartTileY*16) return false; // lake tile, don't touch
1896 if (wtile.iy >= GreatLakeStartTileY*16-16) return true; // remove it, so it won't stack on a lake
1899 if (wtile.ix%16 != 0 || wtile.iy%16 != 0) return true; // sanity check
1901 curWaterTile = wtile;
1902 curWaterTileLastHDir = 0; // never moved to the side
1904 bool wasMoved = false;
1907 int tileX = wtile.ix/16, tileY = wtile.iy/16;
1910 if (tileY >= tilesHeight) return true;
1912 // check if we can fall down
1913 auto canFall = !isAtLeastPartiallyOccupiedAtTilePos(tileX, tileY+1);
1914 // disappear if can fall in lava
1915 if (wtile.water && curWaterTileCheckHitsLava) {
1916 //!writeln(wtile.objId, ": LAVA HIT DOWN");
1920 // fake, so caller will not start removing tiles
1921 if (canFall) wtile.waterMovedDown = true;
1927 //!writeln(wtile.objId, ": GOING DOWN");
1928 curWaterTileLastHDir = 0;
1929 wtile.iy = wtile.iy+16;
1931 wtile.waterMovedDown = true;
1935 bool canMoveLeft = (curWaterTileLastHDir > 0 ? false : !isAtLeastPartiallyOccupiedAtTilePos(tileX-1, tileY));
1936 // disappear if near lava
1937 if (wtile.water && curWaterTileCheckHitsLava) {
1938 //!writeln(wtile.objId, ": LAVA HIT LEFT");
1942 bool canMoveRight = (curWaterTileLastHDir < 0 ? false : !isAtLeastPartiallyOccupiedAtTilePos(tileX+1, tileY));
1943 // disappear if near lava
1944 if (wtile.water && curWaterTileCheckHitsLava) {
1945 //!writeln(wtile.objId, ": LAVA HIT RIGHT");
1949 if (!canMoveLeft && !canMoveRight) {
1951 //!if (wasMove) writeln(wtile.objId, ": NO MORE MOVES");
1955 if (canMoveLeft && canMoveRight) {
1956 // choose random direction
1957 //!writeln(wtile.objId, ": CHOOSING RANDOM HDIR");
1958 // actually, choose direction that leads to hole in a ground
1959 if (waterCanReachGroundHoleInDir(wtile, -1)) {
1960 // can reach hole at the left side
1961 if (waterCanReachGroundHoleInDir(wtile, 1)) {
1962 // can reach hole at the right side, choose at random
1963 if (global.randOther(0, 1)) canMoveRight = false; else canMoveLeft = false;
1966 canMoveRight = false;
1969 // can't reach hole at the left side
1970 if (waterCanReachGroundHoleInDir(wtile, 1)) {
1971 // can reach hole at the right side, choose at random
1972 canMoveLeft = false;
1974 // no holes at any side, choose at random
1975 if (global.randOther(0, 1)) canMoveRight = false; else canMoveLeft = false;
1982 if (canMoveRight) FatalError("WATERCHECK: WTF RIGHT");
1983 //!writeln(wtile.objId, ": MOVING LEFT (", curWaterTileLastHDir, ")");
1984 curWaterTileLastHDir = -1;
1985 wtile.ix = wtile.ix-16;
1986 } else if (canMoveRight) {
1987 if (canMoveLeft) FatalError("WATERCHECK: WTF LEFT");
1988 //!writeln(wtile.objId, ": MOVING RIGHT (", curWaterTileLastHDir, ")");
1989 curWaterTileLastHDir = 1;
1990 wtile.ix = wtile.ix+16;
1998 wtile.setSprite(wtile.lava ? 'sLava' : 'sWater');
1999 wtile.waterMoved = true;
2000 // if this tile was not moved down, check if it can move down on any next step
2001 if (!wtile.waterMovedDown) {
2002 if (waterCanReachGroundHoleInDir(wtile, -1)) wtile.waterMovedDown = true;
2003 else if (waterCanReachGroundHoleInDir(wtile, 1)) wtile.waterMovedDown = true;
2007 return false; // don't remove
2009 //if (!isWetTileAtPix(tileX*16+8, tileY*16-8)) wtile.setSprite(wtile.lava ? 'sLavaTop' : 'sWaterTop');
2013 transient array!MapTile waterTilesList;
2015 final int sortWaterTilesByCoordsCmp (MapTile a, MapTile b) {
2021 transient int waterFlowPause = 0;
2022 transient bool debugWaterFlowPause = false;
2024 final void cleanDeadObjects () {
2025 // remove dead objects
2026 if (deadItemsHead) {
2027 auto olddel = ImmediateDelete;
2028 ImmediateDelete = false;
2030 auto it = deadItemsHead;
2031 deadItemsHead = it.deadItemsNext;
2032 if (!someTilesRemoved && it isa MapTile) someTilesRemoved = true;
2033 if (it.grid) it.grid.remove(it.gridId);
2035 removeActiveEntity(it);
2037 } while (deadItemsHead);
2038 ImmediateDelete = olddel;
2039 if (olddel) CollectGarbage(true); // destroy delayed objects too
2043 final void cleanDeadTiles () {
2044 if (checkWater && /*global.lake == 1 ||*/ (!blockWaterChecking && liquidTileCount)) {
2045 if (global.lake == 1) fillGreatLake();
2046 if (waterFlowPause > 1) {
2051 if (debugWaterFlowPause) waterFlowPause = 4;
2052 //writeln("checking water");
2053 waterTilesList.clear();
2054 foreach (MapTile wtile; objGrid.allObjectsSafe(MapTile)) {
2055 if (wtile.water || wtile.lava) {
2057 if (wtile.ix%16 == 0 && wtile.iy%16 == 0) {
2058 wtile.waterMoved = false;
2059 wtile.waterMovedDown = false;
2060 wtile.waterSlideOldX = wtile.ix;
2061 wtile.waterSlideOldY = wtile.iy;
2062 waterTilesList[$] = wtile;
2067 liquidTileCount = 0;
2068 waterTilesList.sort(&sortWaterTilesByCoordsCmp);
2070 bool wasAnyMove = false;
2071 bool wasAnyMoveDown = false;
2072 foreach (MapTile wtile; waterTilesList) {
2073 if (!wtile || !wtile.isInstanceAlive) continue;
2074 auto killIt = checkWaterFlow(wtile);
2078 wtile.instanceRemove(); // just in case
2080 wtile.saveInterpData();
2082 wasAnyMove = wasAnyMove || wtile.waterMoved;
2083 wasAnyMoveDown = wasAnyMoveDown || wtile.waterMovedDown;
2084 if (wtile.waterMoved && debugWaterFlowPause) wtile.waterSlideCounter = 4;
2088 liquidTileCount = 0;
2089 foreach (MapTile wtile; waterTilesList) {
2090 if (!wtile || !wtile.isInstanceAlive) continue;
2091 if (wasAnyMoveDown) {
2095 //checkWater = checkWater || wtile.waterMoved;
2096 curWaterTile = wtile;
2097 int tileX = wtile.ix/16, tileY = wtile.iy/16;
2098 // check if we are have no way to leak
2099 bool killIt = false;
2100 if (!isFullyOccupiedAtTilePos(tileX-1, tileY) || (wtile.water && curWaterTileCheckHitsLava)) {
2101 //writeln(" LEFT DEATH (", curWaterOccupiedCount, " : ", curWaterTileCheckHitsLava, ")");
2104 if (!killIt && (!isFullyOccupiedAtTilePos(tileX+1, tileY) || (wtile.water && curWaterTileCheckHitsLava))) {
2105 //writeln(" RIGHT DEATH (", curWaterOccupiedCount, " : ", curWaterTileCheckHitsLava, ")");
2108 if (!killIt && (!isFullyOccupiedAtTilePos(tileX, tileY+1) || (wtile.water && curWaterTileCheckHitsLava))) {
2109 //writeln(" DOWN DEATH (", curWaterOccupiedCount, " : ", curWaterTileCheckHitsLava, ")");
2116 wtile.instanceRemove(); // just in case
2121 if (wasAnyMove) checkWater = true;
2122 //writeln("water check: liquidTileCount=", liquidTileCount, "; checkWater=", checkWater, "; wasAnyMove=", wasAnyMove, "; wasAnyMoveDown=", wasAnyMoveDown);
2124 // fill empty spaces in lake with water
2132 // ////////////////////////////////////////////////////////////////////////// //
2133 private transient MapEntity thinkerHeld;
2136 private final void doThinkActionsForObject (MapEntity o) {
2137 if (o.justSpawned) o.justSpawned = false;
2138 else if (o.imageSpeed > 0) o.nextAnimFrame();
2141 if (o.isInstanceAlive) {
2144 if (o.isInstanceAlive) {
2145 if (o.whipTimer > 0) --o.whipTimer;
2147 auto obj = MapObject(o);
2148 if (!o.canLiveOutsideOfLevel && (!obj || !obj.heldBy) && o.isOutsideOfLevel()) {
2149 // oops, fallen out of level...
2157 // return `true` if thinker should be removed
2158 private final void thinkOne (MapEntity o) {
2160 //if (!o.isInstanceAlive) return;
2161 //if (!o.active) return;
2163 auto obj = MapObject(o);
2165 if (obj && obj.heldBy == player) {
2166 // fix held item coords
2167 obj.fixHoldCoords();
2168 doThinkActionsForObject(o);
2172 bool doThink = true;
2174 // collision with player weapon
2175 auto hh = PlayerWeapon(player.holdItem);
2176 bool doWeaponAction = false;
2178 if (hh.blockedBySolids && !global.config.killEnemiesThruWalls) {
2179 int xx = player.ix+(player.dir == MapObject::Dir.Left ? -16 : 16);
2180 //doWeaponAction = !isSolidAtPoint(xx, player.iy);
2181 doWeaponAction = !isSolidAtPoint(xx, hh.yCenter);
2183 int dh = max(1, hh.height-2);
2184 doWeaponAction = !checkTilesInRect(player.ix, player.iy);
2187 doWeaponAction = true;
2191 if (obj && doWeaponAction && hh && (o.whipTimer <= 0 || hh.ignoreWhipTimer) && hh.collidesWithObject(obj)) {
2192 //writeln("WEAPONED!");
2193 //writeln("weapon collides with '", GetClassName(o.Class), "' (", o.objType, "'");
2194 bool dontChangeWhipTimer = hh.dontChangeWhipTimer;
2195 if (!o.onTouchedByPlayerWeapon(player, hh)) {
2196 if (o.isInstanceAlive) hh.onCollisionWithObject(obj);
2198 if (!dontChangeWhipTimer) o.whipTimer = o.whipTimerValue; //HACK
2199 doThink = o.isInstanceAlive;
2202 if (doThink && o.isInstanceAlive) {
2203 doThinkActionsForObject(o);
2204 doThink = o.isInstanceAlive;
2207 // collision with player
2208 if (doThink && obj && o.collidesWith(player)) {
2209 if (!player.onObjectTouched(obj) && o.isInstanceAlive) {
2210 doThink = !o.onTouchedByPlayer(player);
2217 final void processThinkers (float timeDelta) {
2218 if (timeDelta <= 0) return;
2221 if (onBeforeFrame) onBeforeFrame(false);
2222 if (onAfterFrame) onAfterFrame(false);
2228 accumTime += timeDelta;
2229 bool wasFrame = false;
2231 auto olddel = ImmediateDelete;
2232 ImmediateDelete = false;
2233 while (accumTime >= FrameTime) {
2234 bool solidObjectSeen = false;
2235 //postponedThinkers.clear();
2237 accumTime -= FrameTime;
2238 if (onBeforeFrame) onBeforeFrame(accumTime >= FrameTime);
2240 if (shakeLeft > 0) {
2242 if (!shakeDir.x) shakeDir.x = (global.randOther(0, 1) ? -3 : 3);
2243 if (!shakeDir.y) shakeDir.y = (global.randOther(0, 1) ? -3 : 3);
2244 shakeOfs.x = shakeDir.x;
2245 shakeOfs.y = shakeDir.y;
2246 int sgnc = global.randOther(1, 3);
2247 if (sgnc&0x01) shakeDir.x = -shakeDir.x;
2248 if (sgnc&0x02) shakeDir.y = -shakeDir.y;
2257 // we don't want the time to grow too large
2258 if (time < 0) { time = 0; lastRenderTime = -1; }
2259 // game-global events
2261 // frame thinkers: player
2262 if (player && !disablePlayerThink) {
2264 if (!player.dead && isNormalLevel() &&
2265 (maxPlayingTime < 0 ||
2266 (maxPlayingTime > 0 && maxPlayingTime*30 <= time &&
2267 time%30 == 0 && global.randOther(1, 100) <= 20)))
2269 global.hasAnkh = false;
2271 player.invincible = 0;
2272 auto xplo = MapObjExplosion(MakeMapObject(player.ix, player.iy, 'oExplosion'));
2273 if (xplo) xplo.suicide = true;
2275 //HACK: check for stolen items
2276 auto item = MapItem(player.holdItem);
2277 if (item) item.onCheckItemStolen(player);
2278 item = MapItem(player.pickedItem);
2279 if (item) item.onCheckItemStolen(player);
2281 doThinkActionsForObject(player);
2283 // frame thinkers: held object
2284 thinkerHeld = player.holdItem;
2285 if (thinkerHeld && thinkerHeld.isInstanceAlive) {
2286 if (thinkerHeld.active) {
2287 thinkOne(thinkerHeld);
2288 if (!thinkerHeld.isInstanceAlive) {
2289 if (player.holdItem == thinkerHeld) player.holdItem = none;
2290 thinkerHeld.grid.remove(thinkerHeld.gridId);
2294 auto item = MapItem(thinkerHeld);
2296 if (item.forSale || item.sellOfferDone) {
2297 if (++item.forSaleFrame < 0) item.forSaleFrame = 0;
2302 // frame thinkers: objects
2303 foreach (MapEntity e; activeItemsList) {
2304 if (!e || e == thinkerHeld) continue;
2305 if (!e.active || !e.isInstanceAlive) continue;
2307 if (!e.isInstanceAlive) {
2308 if (e.grid) e.grid.remove(e.gridId);
2309 auto obj = MapObject(e);
2310 if (obj && obj.heldBy) obj.heldBy.holdItem = none;
2311 } else if (!solidObjectSeen && e.walkableSolid) {
2312 solidObjectSeen = true;
2313 hasSolidObjects = true;
2317 // clean dead things
2318 someTilesRemoved = false;
2320 hasSolidObjects = !!solidObjectSeen;
2321 // fix held item coords
2322 if (player && player.holdItem) {
2323 if (player.holdItem.isInstanceAlive) {
2324 player.holdItem.fixHoldCoords();
2326 player.holdItem = none;
2330 if (collectCounter == 0) {
2331 xmoney = max(0, xmoney-100);
2337 if (!player.dead) stats.oneMoreFramePlayed();
2338 SoundSystem.ListenerOrigin = vector(player.xCenter, player.yCenter, -1);
2339 //writeln("plrpos=(", player.xCenter, ",", player.yCenter, "); lo=", SoundSystem.ListenerOrigin);
2341 if (onAfterFrame) onAfterFrame(accumTime >= FrameTime);
2342 ++framesProcessedFromLastClear;
2345 if (!player.visible && player.holdItem) player.holdItem.visible = false;
2346 if (winCutsceneSwitchToNext) {
2347 winCutsceneSwitchToNext = false;
2348 switch (++inWinCutscene) {
2349 case 2: startWinCutsceneVolcano(); break;
2350 case 3: default: startWinCutsceneWinFall(); break;
2354 if (playerExited) break;
2356 ImmediateDelete = olddel;
2358 playerExited = false;
2360 centerViewAtPlayer();
2363 // if we were processed at least one frame, collect garbage
2365 CollectGarbage(true); // destroy delayed objects too
2367 if (accumTime > 0 && onInterFrame) onInterFrame(accumTime/FrameTime);
2371 // ////////////////////////////////////////////////////////////////////////// //
2372 final void tileXY2roomXY (int tileX, int tileY, out int roomX, out int roomY) {
2373 roomX = (tileX-1)/RoomGen::Width;
2374 roomY = (tileY-1)/RoomGen::Height;
2378 final bool isInShop (int tileX, int tileY) {
2379 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
2380 auto n = roomType[tileX, tileY];
2381 if (n == 4 || n == 5) return true;
2382 return !!checkTilesInRect(tileX*16, tileY*16, 16, 16, delegate bool (MapTile t) { return t.shopWall; });
2383 //k8: we don't have this
2384 //if (t && t.objType == 'oShop') return true;
2390 // ////////////////////////////////////////////////////////////////////////// //
2391 override void Destroy () {
2393 delete tempSolidTile;
2398 // ////////////////////////////////////////////////////////////////////////// //
2399 // WARNING! delegate should not create/delete objects!
2400 final MapObject findNearestObject (int px, int py, scope bool delegate (MapObject o) dg, optional class!MapObject castClass) {
2401 MapObject res = none;
2402 if (!castClass) castClass = MapObject;
2403 int curdistsq = int.max;
2404 foreach (MapObject o; objGrid.allObjects(MapObject)) {
2405 if (o.spectral) continue;
2406 if (!dg(o)) continue;
2407 int xc = px-o.xCenter, yc = py-o.yCenter;
2408 int distsq = xc*xc+yc*yc;
2409 if (distsq < curdistsq) {
2418 // WARNING! delegate should not create/delete objects!
2419 final MapObject findNearestEnemy (int px, int py, optional scope bool delegate (MapEnemy o) dg, optional class!MapEnemy castClass) {
2420 if (!castClass) castClass = MapEnemy;
2421 if (castClass !isa MapEnemy) return none;
2422 MapObject res = none;
2423 int curdistsq = int.max;
2424 foreach (MapEnemy o; objGrid.allObjects(castClass)) {
2425 //k8: i added `dead` check
2426 if (o.spectral || o.dead) continue;
2428 if (!dg(o)) continue;
2430 int xc = px-o.xCenter, yc = py-o.yCenter;
2431 int distsq = xc*xc+yc*yc;
2432 if (distsq < curdistsq) {
2441 final MonsterShopkeeper findNearestCalmShopkeeper (int px, int py) {
2442 auto obj = MonsterShopkeeper(findNearestEnemy(px, py, delegate bool (MapEnemy o) {
2443 auto sk = MonsterShopkeeper(o);
2444 if (sk && !sk.angered) return true;
2446 }, castClass:MonsterShopkeeper));
2451 final MonsterShopkeeper hasAliveShopkeepers (optional bool skipAngry) {
2452 foreach (MonsterShopkeeper sc; objGrid.allObjects(MonsterShopkeeper)) {
2453 if (sc.spectral || sc.dead) continue;
2454 if (skipAngry && (sc.angered || sc.outlaw)) continue;
2461 // WARNING! delegate should not create/delete objects!
2462 final int calcNearestEnemyDist (int px, int py, optional scope bool delegate (MapEnemy o) dg, optional class!MapEnemy castClass) {
2463 auto e = findNearestEnemy(px, py, dg!optional, castClass!optional);
2464 if (!e) return int.max;
2465 int xc = px-e.xCenter, yc = py-e.yCenter;
2466 return round(sqrt(xc*xc+yc*yc));
2470 // WARNING! delegate should not create/delete objects!
2471 final int calcNearestObjectDist (int px, int py, optional scope bool delegate (MapObject o) dg, optional class!MapObject castClass) {
2472 auto e = findNearestObject(px, py, dg!optional, castClass!optional);
2473 if (!e) return int.max;
2474 int xc = px-e.xCenter, yc = py-e.yCenter;
2475 return round(sqrt(xc*xc+yc*yc));
2479 // WARNING! delegate should not create/delete objects!
2480 final MapTile findNearestMoveableSolid (int px, int py, optional scope bool delegate (MapTile t) dg) {
2482 int curdistsq = int.max;
2483 foreach (MapTile t; objGrid.allObjects(MapTile)) {
2484 if (t.spectral) continue;
2486 if (!dg(t)) continue;
2488 if (!t.solid || !t.moveable) continue;
2490 int xc = px-t.xCenter, yc = py-t.yCenter;
2491 int distsq = xc*xc+yc*yc;
2492 if (distsq < curdistsq) {
2501 // WARNING! delegate should not create/delete objects!
2502 final MapTile findNearestTile (int px, int py, optional scope bool delegate (MapTile t) dg) {
2503 if (!dg) return none;
2505 int curdistsq = int.max;
2507 //FIXME: make this faster!
2508 foreach (MapTile t; objGrid.allObjects(MapTile)) {
2509 if (t.spectral) continue;
2510 int xc = px-t.xCenter, yc = py-t.yCenter;
2511 int distsq = xc*xc+yc*yc;
2512 if (distsq < curdistsq && dg(t)) {
2522 // ////////////////////////////////////////////////////////////////////////// //
2523 final bool cbIsObjectWeb (MapObject o) { return (o isa ItemWeb); }
2524 final bool cbIsObjectBlob (MapObject o) { return (o isa EnemyBlob); }
2525 final bool cbIsObjectArrow (MapObject o) { return (o isa ItemProjectileArrow); }
2526 final bool cbIsObjectEnemy (MapObject o) { return (/*o != self &&*/ o isa MapEnemy); }
2528 final bool cbIsObjectTreasure (MapObject o) { return (o.isTreasure); }
2530 final bool cbIsItemObject (MapObject o) { return (o isa MapItem); }
2532 final bool cbIsItemOrEnemyObject (MapObject o) { return (o isa MapItem || o isa MapEnemy); }
2535 final MapObject isObjectAtTile (int tileX, int tileY, optional scope bool delegate (MapObject o) dg, optional bool precise) {
2536 if (!specified_precise) precise = true;
2539 foreach (MapObject o; objGrid.inRectPix(tileX, tileY, 16, 16, precise:precise, castClass:MapObject)) {
2540 if (o.spectral) continue;
2542 if (dg(o)) return o;
2551 final MapObject isObjectAtTilePix (int x, int y, optional scope bool delegate (MapObject o) dg) {
2552 return isObjectAtTile(x/16, y/16, dg!optional);
2556 final MapObject isObjectAtPoint (int xpos, int ypos, optional scope bool delegate (MapObject o) dg, optional bool precise, optional class!MapObject castClass) {
2557 if (!specified_precise) precise = true;
2558 if (!castClass) castClass = MapObject;
2559 foreach (MapObject o; objGrid.inCellPix(xpos, ypos, precise:precise, castClass:castClass)) {
2560 if (o.spectral) continue;
2562 if (dg(o)) return o;
2564 if (o isa MapEnemy) return o;
2571 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) {
2572 if (w < 1 || h < 1) return none;
2573 if (!castClass) castClass = MapObject;
2574 if (w == 1 && h == 1) return isObjectAtPoint(xpos, ypos, dg!optional, precise!optional, castClass);
2575 if (!specified_precise) precise = true;
2576 foreach (MapObject o; objGrid.inRectPix(xpos, ypos, w, h, precise:precise, castClass:castClass)) {
2577 if (o.spectral) continue;
2579 if (dg(o)) return o;
2581 if (o isa MapEnemy) return o;
2588 final MapObject forEachObject (scope bool delegate (MapObject o) dg, optional bool allowSpectrals, optional class!MapObject castClass) {
2589 if (!dg) return none;
2590 if (!castClass) castClass = MapObject;
2591 foreach (MapObject o; objGrid.allObjectsSafe(castClass)) {
2592 if (!allowSpectrals && o.spectral) continue;
2593 if (dg(o)) return o;
2599 final MapObject forEachObjectAtPoint (int xpos, int ypos, scope bool delegate (MapObject o) dg, optional bool precise) {
2600 if (!dg) return none;
2601 if (!specified_precise) precise = true;
2602 foreach (MapObject o; objGrid.inCellPix(xpos, ypos, precise:precise, castClass:MapObject)) {
2603 if (o.spectral) continue;
2604 if (dg(o)) return o;
2610 final MapObject forEachObjectInRect (int xpos, int ypos, int w, int h, scope bool delegate (MapObject o) dg, optional bool precise) {
2611 if (!dg || w < 1 || h < 1) return none;
2612 if (w == 1 && h == 1) return forEachObjectAtPoint(xpos, ypos, dg!optional, precise!optional);
2613 if (!specified_precise) precise = true;
2614 foreach (MapObject o; objGrid.inRectPix(xpos, ypos, w, h, precise:precise, castClass:MapObject)) {
2615 if (o.spectral) continue;
2616 if (dg(o)) return o;
2622 final MapEntity forEachEntityInRect (int xpos, int ypos, int w, int h, scope bool delegate (MapEntity o) dg, optional bool precise, optional class!MapEntity castClass) {
2623 if (!dg || w < 1 || h < 1) return none;
2624 if (!castClass) castClass = MapEntity;
2625 if (!specified_precise) precise = true;
2626 foreach (MapEntity e; objGrid.inRectPix(xpos, ypos, w, h, precise:precise, castClass:castClass)) {
2627 if (e.spectral) continue;
2628 if (dg(e)) return e;
2634 private final bool cbIsRopeTile (MapTile t) { return (t isa MapTileRope); }
2636 final MapTile isRopeAtPoint (int px, int py) {
2637 return checkTileAtPoint(px, py, &cbIsRopeTile);
2642 final MapTile isWaterSwimAtPoint (int px, int py) {
2643 return isWaterAtPoint(px, py);
2647 // ////////////////////////////////////////////////////////////////////////// //
2648 private array!MapEntity tmpEntityList;
2650 private final bool cbCollectEntitiesWithMask (MapEntity t) {
2651 if (!t.visible || t.spectral) return false;
2652 tmpEntityList[$] = t;
2657 final void touchEntitiesWithMask (int x, int y, SpriteFrame frm, scope bool delegate (MapEntity t) dg, optional class!MapEntity castClass) {
2658 if (!frm || frm.bw < 1 || frm.bh < 1 || !dg) return;
2659 if (frm.isEmptyPixelMask) return;
2660 if (!castClass) castClass = MapEntity;
2662 if (tmpEntityList.length) tmpEntityList.clear();
2663 if (player isa castClass && player.isRectCollisionFrame(frm, x, y)) tmpEntityList[$] = player;
2664 forEachEntityInRect(x+frm.bx, y+frm.by, frm.bw, frm.bh, &cbCollectEntitiesWithMask, castClass:castClass);
2665 foreach (MapEntity e; tmpEntityList) {
2666 if (!e || !e.isInstanceAlive || !e.visible || e.spectral) continue;
2667 if (e.isRectCollisionFrame(frm, x, y)) {
2674 // ////////////////////////////////////////////////////////////////////////// //
2675 final bool cbCollisionAnySolid (MapTile t) { return t.solid; }
2676 final bool cbCollisionAnyMoveableSolid (MapTile t) { return t.solid && t.moveable; }
2677 final bool cbCollisionLiquid (MapTile t) { return (t.water || t.lava); }
2678 final bool cbCollisionAnySolidOrLiquid (MapTile t) { return (t.solid || t.water || t.lava); }
2679 final bool cbCollisionLadder (MapTile t) { return t.ladder; }
2680 final bool cbCollisionLadderTop (MapTile t) { return t.laddertop; }
2681 final bool cbCollisionAnyLadder (MapTile t) { return (t.ladder || t.laddertop); }
2682 final bool cbCollisionAnySolidIce (MapTile t) { return (t.solid && t.ice); }
2683 final bool cbCollisionWater (MapTile t) { return t.water; }
2684 final bool cbCollisionLava (MapTile t) { return t.lava; }
2685 final bool cbCollisionAnyTree (MapTile t) { return t.tree; }
2686 final bool cbCollisionPlatform (MapTile t) { return t.platform; }
2687 final bool cbCollisionLadderOrPlatform (MapTile t) { return (t.ladder || t.platform); }
2688 //final bool cbCollisionAnyHangeable (MapTile t) { return t.hangeable; } // no need to check for solids here, it is done in MapTile initializer
2689 final bool cbCollisionSpringTrap (MapTile t) { return t.springtrap; }
2690 final bool cbCollisionSpikes (MapTile t) { return t.spikes; }
2691 final bool cbCollisionExitTile (MapTile t) { return t.isExitActive(); }
2693 final bool cbCollisionForWhoa (MapTile t) { return (t.solid || t.laddertop); }
2695 //final bool cbCollisionSacAltar (MapTile t) { return (t.objName == 'oSacAltarLeft'); } // only left part, yeah
2696 final bool cbCollisionSacAltar (MapTile t) { return t.sacrificingAltar; }
2699 // ////////////////////////////////////////////////////////////////////////// //
2700 transient MapTileTemp tempSolidTile;
2702 private final MapTileTemp makeWalkeableSolidTile (MapObject o) {
2703 if (!tempSolidTile) {
2704 tempSolidTile = SpawnObject(MapTileTemp);
2705 } else if (!tempSolidTile.isInstanceAlive) {
2706 delete tempSolidTile;
2707 tempSolidTile = SpawnObject(MapTileTemp);
2710 tempSolidTile.level = self;
2711 tempSolidTile.global = global;
2712 tempSolidTile.solid = true;
2713 tempSolidTile.objName = MapTileTemp.default.objName;
2714 tempSolidTile.objType = MapTileTemp.default.objType;
2715 tempSolidTile.e = o;
2716 tempSolidTile.fltx = o.fltx;
2717 tempSolidTile.flty = o.flty;
2718 return tempSolidTile;
2722 final MapTile checkTilesInRect (int x0, int y0, const int w, const int h,
2723 optional scope bool delegate (MapTile dg) dg, optional bool precise,
2724 optional class!MapTile castClass)
2726 if (w < 1 || h < 1) return none;
2727 if (w == 1 && h == 1) return checkTileAtPoint(x0, y0, dg!optional);
2728 int x1 = x0+w-1, y1 = y0+h-1;
2729 if (x0 >= tilesWidth*16 || y0 >= tilesHeight*16 || x1 < 0 || y1 < 0) return none;
2730 if (!specified_precise) precise = true;
2731 if (!castClass) castClass = MapTile;
2732 if (castClass !isa MapTile) return none;
2733 if (!dg) dg = &cbCollisionAnySolid;
2735 if (hasSolidObjects) {
2736 // check walkable solid objects too
2737 foreach (MapEntity e; objGrid.inRectPix(x0, y0, w, h, precise:precise/*, castClass:castClass*/)) {
2738 if (e.spectral || !e.visible) continue;
2739 auto t = MapTile(e);
2741 if (t isa castClass && dg(t)) return t;
2744 auto o = MapObject(e);
2745 if (o && o.walkableSolid) {
2746 t = makeWalkeableSolidTile(o);
2747 if (t isa castClass && dg(t)) return t;
2752 // no walkeable solid MapObjects, speed it up
2753 foreach (MapTile t; objGrid.inRectPix(x0, y0, w, h, precise:precise, castClass:castClass)) {
2754 if (t.spectral || !t.visible) continue;
2755 if (dg(t)) return t;
2763 final MapTile checkTileAtPoint (int x0, int y0, optional scope bool delegate (MapTile dg) dg, optional bool precise, optional class!MapTile castClass) {
2764 if (x0 < 0 || y0 < 0 || x0 >= tilesWidth*16 || y0 >= tilesHeight*16) return none;
2765 if (!specified_precise) precise = true;
2766 if (!castClass) castClass = MapTile;
2767 if (castClass !isa MapTile) return none;
2768 if (!dg) dg = &cbCollisionAnySolid;
2770 if (hasSolidObjects) {
2771 // check walkable solid objects
2772 foreach (MapEntity e; objGrid.inCellPix(x0, y0, precise:precise/*, castClass:castClass*/)) {
2773 if (e.spectral || !e.visible) continue;
2774 auto t = MapTile(e);
2776 if (t isa castClass && dg(t)) return t;
2779 auto o = MapObject(e);
2780 if (o && o.walkableSolid) {
2781 t = makeWalkeableSolidTile(o);
2782 if (t isa castClass && dg(t)) return t;
2788 // no walkeable solid MapObjects, speed it up
2789 foreach (MapTile t; objGrid.inCellPix(x0, y0, precise:precise, castClass:castClass)) {
2790 if (t.spectral || !t.visible) continue;
2791 if (dg(t)) return t;
2799 // ////////////////////////////////////////////////////////////////////////// //
2800 final MapTile isSolidAtPoint (int px, int py) { return checkTileAtPoint(px, py); }
2801 final MapTile isSolidInRect (int rx, int ry, int rw, int rh) { return checkTilesInRect(rx, ry, rw, rh); }
2802 final MapTile isLadderAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadder); }
2803 final MapTile isLadderOrPlatformAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadderOrPlatform); }
2804 final MapTile isLadderTopAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadderTop); }
2805 final MapTile isAnyLadderAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyLadder); }
2806 final MapTile isWaterAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionWater); }
2807 final MapTile isLavaAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLava); }
2808 final MapTile isLiquidAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLiquid); }
2809 final MapTile isIceAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnySolidIce); }
2810 final MapTile isTreeAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyTree); }
2811 //final MapTile isHangeableAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyHangeable); }
2814 // ////////////////////////////////////////////////////////////////////////// //
2815 final void setTileTypeAt (int tileX, int tileY, LevelGen::RType rt) {
2816 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) roomType[tileX, tileY] = rt;
2820 //FIXME: make this faster
2821 transient float gtagX, gtagY;
2823 // only non-moveables and non-specials
2824 final MapTile getTileAtGrid (int tileX, int tileY) {
2827 return checkTileAtPoint(tileX*16, tileY*16, delegate bool (MapTile t) {
2828 if (t.spectral || t.moveable || t.toSpecialGrid || !t.visible) return false;
2829 if (t.fltx != gtagX || t.flty != gtagY) return false; // emulate grid
2830 if (t.width != 16 || t.height != 16) return false;
2833 //return (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight ? tiles[tileX, tileY] : none);
2837 final MapTile getTileAtGridAny (int tileX, int tileY) {
2840 return checkTileAtPoint(tileX*16, tileY*16, delegate bool (MapTile t) {
2841 if (t.spectral /*|| t.moveable*/ || !t.visible) return false;
2842 if (t.fltx != gtagX || t.flty != gtagY) return false; // emulate grid
2843 if (t.width != 16 || t.height != 16) return false;
2846 //return (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight ? tiles[tileX, tileY] : none);
2850 final bool isNamedTileAt (int tileX, int tileY, name atypename) {
2851 if (!atypename) return false;
2852 auto t = getTileAtGridAny(tileX, tileY);
2853 return (t && t.objName == atypename);
2857 final void setTileAtGrid (int tileX, int tileY, MapTile tile) {
2858 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
2860 tile.fltx = tileX*16;
2861 tile.flty = tileY*16;
2862 if (!tile.dontReplaceOthers) {
2863 auto osp = tile.spectral;
2864 tile.spectral = true;
2865 auto t = getTileAtGridAny(tileX, tileY);
2866 tile.spectral = osp;
2867 if (t && !t.immuneToReplacement) {
2868 writeln("REPLACING tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
2869 writeln(" NEW tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (tile.toSpecialGrid ? "special " : ""), "grid, pos=(", tile.fltx/16.0, ",", tile.flty/16.0, ")");
2875 auto t = getTileAtGridAny(tileX, tileY);
2876 if (t && !t.immuneToReplacement) {
2877 writeln("REMOVING tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
2885 // ////////////////////////////////////////////////////////////////////////// //
2886 // return `true` from delegate to stop
2887 MapTile forEachSolidTileOnGrid (scope bool delegate (int tileX, int tileY, MapTile t) dg) {
2888 if (!dg) return none;
2889 foreach (MapTile t; objGrid.allObjectsSafe(MapTile)) {
2890 if (t.spectral || !t.solid || !t.visible) continue;
2891 if (t.ix%16 != 0 || t.iy%16 != 0) continue; // emulate grid
2892 if (t.width != 16 || t.height != 16) continue;
2893 if (dg(t.ix/16, t.iy/16, t)) return t;
2899 // ////////////////////////////////////////////////////////////////////////// //
2900 // return `true` from delegate to stop
2901 MapTile forEachTile (scope bool delegate (MapTile t) dg, optional class!MapTile castClass) {
2902 if (!dg) return none;
2903 if (!castClass) castClass = MapTile;
2904 foreach (MapTile t; objGrid.allObjectsSafe(castClass)) {
2905 if (t.spectral || !t.visible) continue;
2906 if (dg(t)) return t;
2912 // ////////////////////////////////////////////////////////////////////////// //
2913 final void fixWallTiles () {
2914 foreach (MapTile t; objGrid.allObjectsSafe(MapTile)) {
2915 //writeln("beautify: '", GetClassName(t.Class), "' (", t.objType, "' (name:", t.objName, ")");
2921 // ////////////////////////////////////////////////////////////////////////// //
2922 final MapTile isCollisionAtPoint (int px, int py, optional scope bool delegate (MapTile dg) dg) {
2923 if (!dg) dg = &cbCollisionAnySolid;
2924 return checkTilesInRect(px, py, 1, 1, dg);
2928 // ////////////////////////////////////////////////////////////////////////// //
2929 string scrGetKaliGift (MapTile altar, optional name gift) {
2932 // find other side of the altar
2933 int sx = player.ix, sy = player.iy;
2937 auto a2 = isCollisionAtPoint(altar.ix+18, altar.iy+8, &cbCollisionSacAltar);
2938 if (!a2) a2 = isCollisionAtPoint(altar.ix-8, altar.iy+8, &cbCollisionSacAltar);
2939 if (a2) { sx = a2.ix; sy = a2.iy; }
2942 if (global.favor <= -8) res = "SHE SEEMS VERY ANGRY WITH YOU!";
2943 else if (global.favor < 0) res = "SHE SEEMS ANGRY WITH YOU.";
2944 else if (global.favor == 0) res = "SHE HAS FORGIVEN YOU!";
2945 else if (global.favor >= 32) {
2946 if (global.kaliGift >= 3 && global.favor >= 32+(global.kaliGift-2)*16) {
2947 res = "YOU FEEL INVIGORATED!";
2948 global.kaliGift += 1;
2949 global.plife += global.randOther(4, 8);
2950 } else if (global.kaliGift >= 3) {
2951 res = "SHE SEEMS ECSTATIC WITH YOU!";
2952 } else if (global.bombs < 80) {
2953 res = "YOUR SATCHEL FEELS VERY FULL NOW!";
2954 global.kaliGift = 3;
2957 res = "YOU FEEL INVIGORATED!";
2958 global.kaliGift += 1;
2959 global.plife += global.randOther(4, 8);
2961 } else if (global.favor >= 16) {
2962 if (global.kaliGift >= 2) {
2963 res = "SHE SEEMS VERY HAPPY WITH YOU!";
2965 res = "SHE BESTOWS A GIFT UPON YOU!";
2966 global.kaliGift = 2;
2968 auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2971 obj = MakeMapObject(sx, sy-8, 'oPoof');
2976 if (gift) obj = MakeMapObject(sx, sy-8, gift);
2977 if (!obj) obj = MakeMapObject(sx, sy-8, 'oKapala');
2979 } else if (global.favor >= 8) {
2980 if (global.kaliGift >= 1) {
2981 res = "SHE SEEMS HAPPY WITH YOU.";
2983 res = "SHE BESTOWS A GIFT UPON YOU!";
2984 global.kaliGift = 1;
2985 //rAltar = instance_nearest(x, y, oSacAltarRight);
2986 //if (instance_exists(rAltar)) {
2988 auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2991 obj = MakeMapObject(sx, sy-8, 'oPoof');
2995 if (gift) obj = MakeMapObject(sx, sy-8, gift);
2997 auto n = global.randOther(1, 8);
3001 if (n == 1 && !global.hasCape && !global.hasJetpack) aname = 'oCapePickup';
3002 else if (n == 2 && !global.hasGloves) aname = 'oGloves';
3003 else if (n == 3 && !global.hasSpectacles) aname = 'oSpectacles';
3004 else if (n == 4 && !global.hasMitt) aname = 'oMitt';
3005 else if (n == 5 && !global.hasSpringShoes) aname = 'oSpringShoes';
3006 else if (n == 6 && !global.hasSpikeShoes) aname = 'oSpikeShoes';
3007 else if (n == 7 && !global.hasStickyBombs) aname = 'oPaste';
3008 else if (n == 8 && !global.hasCompass) aname = 'oCompass';
3010 obj = MakeMapObject(sx, sy-8, aname);
3016 obj = MakeMapObject(sx, sy-8, (global.hasJetpack ? 'oBombBox' : 'oJetpack'));
3022 } else if (global.favor > 0) {
3023 res = "SHE SEEMS PLEASED WITH YOU.";
3028 global.message = "";
3029 res = "KALI DEVOURS YOU!"; // sacrifice is player
3037 void performSacrifice (MapObject what, MapTile where) {
3038 if (!what || !what.isInstanceAlive) return;
3039 MakeMapObject(what.ix+8, what.iy+8, 'oFlame');
3040 if (where) where.playSound('sndSmallExplode'); else what.playSound('sndSmallExplode');
3041 what.spillBlood(amount:3, forced:true);
3043 string msg = "KALI ACCEPTS THE SACRIFICE!";
3045 auto idol = ItemGoldIdol(what);
3047 ++stats.totalSacrifices;
3048 if (global.favor <= -8) msg = "SHE SEEMS VERY ANGRY WITH YOU!";
3049 else if (global.favor < 0) msg = "SHE SEEMS ANGRY WITH YOU.";
3050 else if (global.favor >= 0) {
3051 // find other side of the altar
3052 int sx = player.ix, sy = player.iy;
3057 auto a2 = isCollisionAtPoint(altar.ix+18, altar.iy+8, &cbCollisionSacAltar);
3058 if (!a2) a2 = isCollisionAtPoint(altar.ix-8, altar.iy+8, &cbCollisionSacAltar);
3059 if (a2) { sx = a2.ix; sy = a2.iy; }
3062 auto obj = MakeMapObject(sx, sy-8, 'oPoof');
3065 obj = MakeMapObject(sx, sy-8, 'oPoof');
3069 obj = MakeMapObject(sx, sy-16-8, 'oMonkeyGold');
3071 osdMessage(msg, 6.66);
3073 idol.instanceRemove();
3077 if (global.favor <= -8) {
3078 msg = "KALI DEVOURS THE SACRIFICE!";
3079 } else if (global.favor < 0) {
3080 global.favor += (what.status == MapObject::STUNNED ? what.favor : round(what.favor/2.0)); //k8: added `round()`
3081 if (what.favor > 0) what.favor = 0;
3083 global.favor += (what.status == MapObject::STUNNED ? what.favor : round(what.favor/2.0)); //k8: added `round()`
3087 if (myAltar.gift1 != "" and global.kaliGift == 0) scrGetKaliGift(myAltar.gift1);
3088 else if (myAltar.gift2 != "" and global.kaliGift == 1) scrGetKaliGift(myAltar.gift2);
3089 else scrGetKaliGift("");
3092 // sacrifice is player?
3093 if (what isa PlayerPawn) {
3094 ++stats.totalSelfSacrifices;
3095 msg = "KALI DEVOURS YOU!";
3096 player.visible = false;
3097 player.removeBallAndChain(temp:true);
3099 player.status = MapObject::DEAD;
3101 ++stats.totalSacrifices;
3102 auto msg2 = scrGetKaliGift(where);
3103 what.instanceRemove();
3104 if (msg2) msg = va("%s\n%s", msg, msg2);
3107 osdMessage(msg, 6.66);
3113 // ////////////////////////////////////////////////////////////////////////// //
3114 final void addBackgroundGfxDetails () {
3115 // add background details
3116 //if (global.customLevel) return;
3118 // bg = instance_create(16*randRoom(1, 42), 16*randRoom(1, 33), oCaveBG);
3119 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);
3120 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);
3121 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);
3122 else MakeMapBackTile('bgExtras', 32*global.randRoom(0, 1), 0, 32, 32, 16*global.randRoom(1, 42), 16*global.randRoom(1, 33), 10002);
3127 // ////////////////////////////////////////////////////////////////////////// //
3128 private final void fixRealViewStart () {
3129 int scale = global.scale;
3130 realViewStart.x = clamp(realViewStart.x, viewMin.x*scale, viewMax.x*scale-viewWidth);
3131 realViewStart.y = clamp(realViewStart.y, viewMin.y*scale, viewMax.y*scale-viewHeight);
3135 final int cameraCurrX () { return realViewStart.x/global.scale; }
3136 final int cameraCurrY () { return realViewStart.y/global.scale; }
3139 private final void fixViewStart () {
3140 int scale = global.scale;
3141 viewStart.x = clamp(viewStart.x, viewMin.x*scale, viewMax.x*scale-viewWidth);
3142 viewStart.y = clamp(viewStart.y, viewMin.y*scale, viewMax.y*scale-viewHeight);
3146 final void centerViewAtPlayer () {
3147 if (viewWidth < 1 || viewHeight < 1 || !player) return;
3148 centerViewAt(player.xCenter, player.yCenter);
3152 final void centerViewAt (int x, int y) {
3153 if (viewWidth < 1 || viewHeight < 1) return;
3155 cameraSlideToSpeed.x = 0;
3156 cameraSlideToSpeed.y = 0;
3157 cameraSlideToPlayer = 0;
3159 int scale = global.scale;
3162 realViewStart.x = clamp(x-viewWidth/2, 0, tilesWidth*16*scale-viewWidth);
3163 realViewStart.y = clamp(y-viewHeight/2, 0, tilesHeight*16*scale-viewHeight);
3166 viewStart.x = realViewStart.x;
3167 viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
3170 if (onCameraTeleported) onCameraTeleported();
3174 const int ViewPortToleranceX = 16*1+8;
3175 const int ViewPortToleranceY = 16*1+8;
3177 final void fixCamera () {
3178 if (!player) return;
3179 if (viewWidth < 1 || viewHeight < 1) return;
3180 int scale = global.scale;
3181 auto alwaysCenterX = global.config.alwaysCenterPlayer;
3182 auto alwaysCenterY = alwaysCenterX;
3183 // calculate offset from viewport center (in game units), and fix viewport
3185 int camDestX = player.ix+8;
3186 int camDestY = player.iy+8;
3187 if (!cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y) != 0) {
3188 // slide camera to point
3189 if (cameraSlideToSpeed.x) camDestX = cameraSlideToCurr.x;
3190 if (cameraSlideToSpeed.y) camDestY = cameraSlideToCurr.y;
3191 int dx = cameraSlideToDest.x-camDestX;
3192 int dy = cameraSlideToDest.y-camDestY;
3193 //writeln("speed:(", cameraSlideToSpeed.x, ",", cameraSlideToSpeed.y, "); cur:(", camDestX, ",", camDestY, "); dest:(", cameraSlideToDest.x, ",", cameraSlideToDest.y, "); delta=(", dx, ",", dy, ")");
3194 if (dx && cameraSlideToSpeed.x != 0) {
3195 alwaysCenterX = true;
3196 if (abs(dx) <= abs(cameraSlideToSpeed.x)) {
3197 camDestX = cameraSlideToDest.x;
3199 camDestX += sign(dx)*abs(cameraSlideToSpeed.x);
3202 if (dy && abs(cameraSlideToSpeed.y) != 0) {
3203 alwaysCenterY = true;
3204 if (abs(dy) <= cameraSlideToSpeed.y) {
3205 camDestY = cameraSlideToDest.y;
3207 camDestY += sign(dy)*abs(cameraSlideToSpeed.y);
3210 //writeln(" new:(", camDestX, ",", camDestY, ")");
3211 if (cameraSlideToSpeed.x) cameraSlideToCurr.x = camDestX;
3212 if (cameraSlideToSpeed.y) cameraSlideToCurr.y = camDestY;
3216 if (cameraSlideToSpeed.x && !cameraSlideToPlayer) {
3217 realViewStart.x = clamp(camDestX*scale, 0, tilesWidth*16*scale-viewWidth);
3218 } else if (!player.cameraBlockX) {
3219 int x = camDestX*scale;
3220 int cx = realViewStart.x;
3221 if (alwaysCenterX) {
3224 int xofs = x-(cx+viewWidth/2);
3225 if (xofs < -ViewPortToleranceX*scale) cx += xofs+ViewPortToleranceX*scale;
3226 else if (xofs > ViewPortToleranceX*scale) cx += xofs-ViewPortToleranceX*scale;
3228 // slide back to player?
3229 if (cameraSlideToPlayer && cameraSlideToSpeed.x) {
3230 int prevx = cameraSlideToCurr.x*scale;
3231 int dx = (cx-prevx)/scale;
3232 if (abs(dx) <= cameraSlideToSpeed.x) {
3233 writeln("BACKSLIDE X COMPLETE!");
3234 cameraSlideToSpeed.x = 0;
3236 cx = prevx+(sign(dx)*cameraSlideToSpeed.x)*scale;
3237 cameraSlideToCurr.x += sign(dx)*cameraSlideToSpeed.x;
3238 if ((dx < 0 && cx <= 0) || (dx > 0 && cx >= tilesWidth*16*scale-viewWidth)) {
3239 writeln("BACKSLIDE X COMPLETE!");
3240 cameraSlideToSpeed.x = 0;
3244 realViewStart.x = clamp(cx, 0, tilesWidth*16*scale-viewWidth);
3248 if (cameraSlideToSpeed.y && !cameraSlideToPlayer) {
3249 realViewStart.y = clamp(camDestY*scale, 0, tilesHeight*16*scale-viewHeight);
3250 } else if (!player.cameraBlockY) {
3251 int y = camDestY*scale;
3252 int cy = realViewStart.y;
3253 if (alwaysCenterY) {
3254 cy = y-viewHeight/2;
3256 int yofs = y-(cy+viewHeight/2);
3257 if (yofs < -ViewPortToleranceY*scale) cy += yofs+ViewPortToleranceY*scale;
3258 else if (yofs > ViewPortToleranceY*scale) cy += yofs-ViewPortToleranceY*scale;
3260 // slide back to player?
3261 if (cameraSlideToPlayer && cameraSlideToSpeed.y) {
3262 int prevy = cameraSlideToCurr.y*scale;
3263 int dy = (cy-prevy)/scale;
3264 if (abs(dy) <= cameraSlideToSpeed.y) {
3265 writeln("BACKSLIDE Y COMPLETE!");
3266 cameraSlideToSpeed.y = 0;
3268 cy = prevy+(sign(dy)*cameraSlideToSpeed.y)*scale;
3269 cameraSlideToCurr.y += sign(dy)*cameraSlideToSpeed.y;
3270 if ((dy < 0 && cy <= 0) || (dy > 0 && cy >= tilesHeight*16*scale-viewHeight)) {
3271 writeln("BACKSLIDE Y COMPLETE!");
3272 cameraSlideToSpeed.y = 0;
3276 realViewStart.y = clamp(cy, 0, tilesHeight*16*scale-viewHeight);
3279 if (cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y) == 0) cameraSlideToPlayer = 0;
3282 //writeln(" new2:(", cameraCurrX, ",", cameraCurrY, ")");
3284 viewStart.x = realViewStart.x;
3285 viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
3290 // ////////////////////////////////////////////////////////////////////////// //
3291 // x0 and y0 are non-scaled (and will be scaled)
3292 final void drawSpriteAt (name sprName, float frnumf, int x0, int y0, optional bool small) {
3293 if (!sprName) return;
3294 auto spr = sprStore[sprName];
3295 if (!spr || !spr.frames.length) return;
3296 int scale = global.scale;
3299 int frnum = max(0, trunc(frnumf))%spr.frames.length;
3300 auto sfr = spr.frames[frnum];
3301 int sx0 = x0-sfr.xofs*scale;
3302 int sy0 = y0-sfr.yofs*scale;
3303 if (small && scale > 1) {
3304 sfr.tex.blitExt(sx0, sy0, round(sx0+sfr.width*(scale/2.0)), round(sy0+sfr.height*(scale/2.0)), 0, 0);
3306 sfr.blitAt(sx0, sy0, scale);
3311 final void drawSpriteAtS3 (name sprName, float frnumf, int x0, int y0) {
3312 if (!sprName) return;
3313 auto spr = sprStore[sprName];
3314 if (!spr || !spr.frames.length) return;
3317 int frnum = max(0, trunc(frnumf))%spr.frames.length;
3318 auto sfr = spr.frames[frnum];
3319 int sx0 = x0-sfr.xofs*3;
3320 int sy0 = y0-sfr.yofs*3;
3321 sfr.blitAt(sx0, sy0, 3);
3325 // x0 and y0 are non-scaled (and will be scaled)
3326 final void drawTextAt (int x0, int y0, string text, optional int scale, optional int hiColor1, optional int hiColor2) {
3328 if (!specified_scale) scale = global.scale;
3331 sprStore.renderTextWithHighlight(x0, y0, text, scale, hiColor1!optional, hiColor2!optional);
3335 void renderCompass (float currFrameDelta) {
3336 if (!global.hasCompass) return;
3339 if (isRoom("rOlmec")) {
3342 } else if (isRoom("rOlmec2")) {
3348 bool hasMessage = osdHasMessage();
3349 foreach (MapTile et; allExits) {
3351 int exitX = et.ix, exitY = et.iy;
3352 int vx0 = viewStart.x/global.scale, vy0 = viewStart.y/global.scale;
3353 int vx1 = (viewStart.x+viewWidth)/global.scale;
3354 int vy1 = (viewStart.y+viewHeight)/global.scale;
3355 if (exitY > vy1-16) {
3357 drawSpriteAt(hasMessage ? 'sCompassSmallLL' : 'sCompassLL', 0, 0, 224);
3358 } else if (exitX > vx1-16) {
3359 drawSpriteAt(hasMessage ? 'sCompassSmallLR' : 'sCompassLR', 0, 304, 224);
3361 drawSpriteAt(hasMessage ? 'sCompassSmallDown' : 'sCompassDown', 0, exitX-vx0, 224);
3363 } else if (exitX < vx0) {
3364 drawSpriteAt(hasMessage ? 'sCompassSmallLeft' : 'sCompassLeft', 0, 0, exitY-vy0);
3365 } else if (exitX > vx1-16) {
3366 drawSpriteAt(hasMessage ? 'sCompassSmallRight' : 'sCompassRight', 0, 304, exitY-vy0);
3368 break; // only the first exit
3373 final bool totalsNameCmpCB (ref GameStats::TotalItem a, ref GameStats::TotalItem b) {
3374 auto sa = string(a.objName);
3375 auto sb = string(b.objName);
3379 void renderTransitionInfo (float currFrameDelta) {
3382 GameStats.sortTotalsList(stats.kills, &totalsNameCmpCB);
3385 foreach (int idx, ref auto k; stats.kills) {
3386 string s = string(k);
3387 maxLen = max(maxLen, s.length);
3391 sprStore.loadFont('sFontSmall');
3392 Video.color = 0xff_ff_00;
3393 foreach (int idx, ref auto k; stats.kills) {
3395 foreach (int xidx, ref auto d; stats.totalKills) {
3396 if (d.objName == k) { deaths = d.count; break; }
3398 //drawTextAt(16, 4+idx*8, va("%s: %d (%d)", string(k.objName).toUpperCase, k.count, deaths));
3399 drawTextAt(16, 4+idx*8, string(k).toUpperCase);
3400 drawTextAt(16+maxLen, 4+idx*8, va(": %d (%d)", k.count, deaths));
3406 void renderGhostTimer (float currFrameDelta) {
3407 if (ghostTimeLeft <= 0) return;
3408 //ghostTimeLeft /= 30; // frames -> seconds
3410 int hgt = viewHeight-64;
3411 if (hgt < 1) return;
3412 int rhgt = round(float(hgt)*(float(ghostTimeLeft)/30.0)/1000.0);
3413 //writeln("hgt=", hgt, "; rhgt=", rhgt, "; gtl=", ghostTimeLeft/30);
3415 auto oclr = Video.color;
3416 Video.color = 0xcf_ff_7f_00;
3417 Video.fillRect(viewWidth-20, 32, 16, hgt-rhgt);
3418 Video.color = 0x7f_ff_7f_00;
3419 Video.fillRect(viewWidth-20, 32+(hgt-rhgt), 16, rhgt);
3425 void renderStarsHUD (float currFrameDelta) {
3426 bool scumSmallHud = global.config.scumSmallHud;
3428 //auto life = max(0, global.plife);
3429 int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3430 int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3431 //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3436 sprStore.loadFont('sFontSmall');
3439 sprStore.loadFont('sFont');
3443 Video.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3444 //draw_sprite(sHeart, -1, view_xview[0]+8, view_yview[0]+8);
3445 //draw_text(view_xview[0]+24, view_yview[0]+8, life);
3447 if (global.plife == 1) {
3448 drawSpriteAt('sHeartSmallBlink', global.heartBlink, 10, hhup-4);
3449 global.heartBlink += 0.1;
3450 if (global.heartBlink > 3) global.heartBlink = 0;
3452 drawSpriteAt('sHeartSmall', -1, 10, hhup-4);
3453 global.heartBlink = 0;
3456 if (global.plife == 1) {
3457 drawSpriteAt('sHeartBlink', global.heartBlink, 8, hhup);
3458 global.heartBlink += 0.1;
3459 if (global.heartBlink > 3) global.heartBlink = 0;
3461 drawSpriteAt('sHeart', -1, 8, hhup);
3462 global.heartBlink = 0;
3465 int life = clamp(global.plife, 0, 99);
3466 drawTextAt(16+8, hhup, va("%d", life));
3468 //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3469 drawSpriteAt('sShopkeeperIcon', -1, 64, hhup, scumSmallHud);
3470 drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", starsKills));
3472 if (starsRoomTimer1 > 0) {
3473 sprStore.loadFont('sFontSmall');
3474 Video.color = 0xff_ff_00;
3475 int scale = global.scale;
3476 sprStore.renderMultilineTextCentered(viewWidth/2, 216*scale, va("SHOTGUN CHALLENGE BEGINS IN ~%d~", (starsRoomTimer1/30)+1), scale, 0xff_00_00);
3481 void renderSunHUD (float currFrameDelta) {
3482 bool scumSmallHud = global.config.scumSmallHud;
3484 //auto life = max(0, global.plife);
3485 int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3486 int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3487 //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3492 sprStore.loadFont('sFontSmall');
3495 sprStore.loadFont('sFont');
3499 Video.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3500 //draw_sprite(sHeart, -1, view_xview[0]+8, view_yview[0]+8);
3501 //draw_text(view_xview[0]+24, view_yview[0]+8, life);
3503 if (global.plife == 1) {
3504 drawSpriteAt('sHeartSmallBlink', global.heartBlink, 10, hhup-4);
3505 global.heartBlink += 0.1;
3506 if (global.heartBlink > 3) global.heartBlink = 0;
3508 drawSpriteAt('sHeartSmall', -1, 10, hhup-4);
3509 global.heartBlink = 0;
3512 if (global.plife == 1) {
3513 drawSpriteAt('sHeartBlink', global.heartBlink, 8, hhup);
3514 global.heartBlink += 0.1;
3515 if (global.heartBlink > 3) global.heartBlink = 0;
3517 drawSpriteAt('sHeart', -1, 8, hhup);
3518 global.heartBlink = 0;
3521 int life = clamp(global.plife, 0, 99);
3522 drawTextAt(16+8, hhup, va("%d", life));
3524 //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3525 drawSpriteAt('sDamselIcon', -1, 64, hhup, scumSmallHud);
3526 drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", sunScore));
3528 if (sunRoomTimer1 > 0) {
3529 sprStore.loadFont('sFontSmall');
3530 Video.color = 0xff_ff_00;
3531 int scale = global.scale;
3532 sprStore.renderMultilineTextCentered(viewWidth/2, 216*scale, va("DAMSEL CHALLENGE BEGINS IN ~%d~", (sunRoomTimer1/30)+1), scale, 0xff_00_00);
3537 void renderMoonHUD (float currFrameDelta) {
3538 bool scumSmallHud = global.config.scumSmallHud;
3540 //auto life = max(0, global.plife);
3541 int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3542 int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3543 //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3548 sprStore.loadFont('sFontSmall');
3551 sprStore.loadFont('sFont');
3555 Video.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3557 //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3558 drawSpriteAt('sHoopsIcon', -1, 8, hhup, scumSmallHud);
3559 drawTextAt(8+16-(scumSmallHud ? 6 : 0), hhup, va("%d", moonScore));
3560 drawSpriteAt('sTimerIcon', -1, 64, hhup, scumSmallHud);
3561 drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", max(0, moonTimer)));
3563 if (moonRoomTimer1 > 0) {
3564 sprStore.loadFont('sFontSmall');
3565 Video.color = 0xff_ff_00;
3566 int scale = global.scale;
3567 sprStore.renderMultilineTextCentered(viewWidth/2, 216*scale, va("ARCHERY CHALLENGE BEGINS IN ~%d~", (moonRoomTimer1/30)+1), scale, 0xff_00_00);
3572 void renderHUD (float currFrameDelta) {
3573 if (levelKind == LevelKind.Stars) { renderStarsHUD(currFrameDelta); return; }
3574 if (levelKind == LevelKind.Sun) { renderSunHUD(currFrameDelta); return; }
3575 if (levelKind == LevelKind.Moon) { renderMoonHUD(currFrameDelta); return; }
3577 if (!isHUDEnabled()) return;
3579 if (levelKind == LevelKind.Transition) { renderTransitionInfo(currFrameDelta); return; }
3587 bool scumSmallHud = global.config.scumSmallHud;
3588 if (!global.config.optSGAmmo && (!player || player.holdItem !isa ItemWeaponBow)) moneyX = ammoX;
3591 sprStore.loadFont('sFontSmall');
3594 sprStore.loadFont('sFont');
3597 //int alpha = 0x6f_00_00_00;
3598 int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3599 int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3601 //Video.color = 0xff_ff_ff;
3602 Video.color = 0xff_ff_ff|talpha;
3606 if (global.plife == 1) {
3607 drawSpriteAt('sHeartSmallBlink', global.heartBlink, lifeX+2, 4-hhup);
3608 global.heartBlink += 0.1;
3609 if (global.heartBlink > 3) global.heartBlink = 0;
3611 drawSpriteAt('sHeartSmall', -1, lifeX+2, 4-hhup);
3612 global.heartBlink = 0;
3615 if (global.plife == 1) {
3616 drawSpriteAt('sHeartBlink', global.heartBlink, lifeX, 8-hhup);
3617 global.heartBlink += 0.1;
3618 if (global.heartBlink > 3) global.heartBlink = 0;
3620 drawSpriteAt('sHeart', -1, lifeX, 8-hhup);
3621 global.heartBlink = 0;
3625 int life = clamp(global.plife, 0, 99);
3626 //if (!scumHud && life > 99) life = 99;
3627 drawTextAt(lifeX+16, 8-hhup, va("%d", life));
3630 if (global.hasStickyBombs && global.stickyBombsActive) {
3631 if (scumSmallHud) drawSpriteAt('sStickyBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sStickyBombIcon', -1, bombX, 8-hhup);
3633 if (scumSmallHud) drawSpriteAt('sBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sBombIcon', -1, bombX, 8-hhup);
3635 int n = global.bombs;
3636 if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3637 drawTextAt(bombX+16, 8-hhup, va("%d", n));
3640 if (scumSmallHud) drawSpriteAt('sRopeIconSmall', -1, ropeX+2, 4-hhup); else drawSpriteAt('sRopeIcon', -1, ropeX, 8-hhup);
3642 if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3643 drawTextAt(ropeX+16, 8-hhup, va("%d", n));
3646 if (global.config.optSGAmmo && (!player || player.holdItem !isa ItemWeaponBow)) {
3647 if (scumSmallHud) drawSpriteAt('sShellsIcon', -1, ammoX+6, 8-hhup); else drawSpriteAt('sShellsIcon', -1, ammoX+7, 12-hhup);
3649 if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3650 drawTextAt(ammoX+16, 8-hhup, va("%d", n));
3651 } else if (player && player.holdItem isa ItemWeaponBow) {
3652 if (scumSmallHud) drawSpriteAt('sArrowRight', -1, ammoX+6, 8-hhup); else drawSpriteAt('sArrowRight', -1, ammoX+7, 12-hhup);
3654 if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3655 drawTextAt(ammoX+16, 8-hhup, va("%d", n));
3659 if (scumSmallHud) drawSpriteAt('sDollarSignSmall', -1, moneyX+2, 3-hhup); else drawSpriteAt('sDollarSign', -1, moneyX, 8-hhup);
3660 drawTextAt(moneyX+16, 8-hhup, va("%d", stats.money-xmoney));
3663 Video.color = 0xff_ff_ff|ialpha;
3665 int ity = (scumSmallHud ? 18-hhup : 24-hhup);
3668 if (global.hasUdjatEye) {
3669 if (global.udjatBlink) drawSpriteAt('sUdjatEyeIcon2', -1, n, ity); else drawSpriteAt('sUdjatEyeIcon', -1, n, ity);
3672 if (global.hasAnkh) { drawSpriteAt('sAnkhIcon', -1, n, ity); n += 20; }
3673 if (global.hasCrown) { drawSpriteAt('sCrownIcon', -1, n, ity); n += 20; }
3674 if (global.hasKapala) {
3675 if (global.bloodLevel == 0) drawSpriteAt('sKapalaIcon', 0, n, ity);
3676 else if (global.bloodLevel <= 2) drawSpriteAt('sKapalaIcon', 1, n, ity);
3677 else if (global.bloodLevel <= 4) drawSpriteAt('sKapalaIcon', 2, n, ity);
3678 else if (global.bloodLevel <= 6) drawSpriteAt('sKapalaIcon', 3, n, ity);
3679 else if (global.bloodLevel <= 8) drawSpriteAt('sKapalaIcon', 4, n, ity);
3682 if (global.hasSpectacles) { drawSpriteAt('sSpectaclesIcon', -1, n, ity); n += 20; }
3683 if (global.hasGloves) { drawSpriteAt('sGlovesIcon', -1, n, ity); n += 20; }
3684 if (global.hasMitt) { drawSpriteAt('sMittIcon', -1, n, ity); n += 20; }
3685 if (global.hasSpringShoes) { drawSpriteAt('sSpringShoesIcon', -1, n, ity); n += 20; }
3686 if (global.hasJordans) { drawSpriteAt('sJordansIcon', -1, n, ity); n += 20; }
3687 if (global.hasSpikeShoes) { drawSpriteAt('sSpikeShoesIcon', -1, n, ity); n += 20; }
3688 if (global.hasCape) { drawSpriteAt('sCapeIcon', -1, n, ity); n += 20; }
3689 if (global.hasJetpack) { drawSpriteAt('sJetpackIcon', -1, n, ity); n += 20; }
3690 if (global.hasStickyBombs) { drawSpriteAt('sPasteIcon', -1, n, ity); n += 20; }
3691 if (global.hasCompass) { drawSpriteAt('sCompassIcon', -1, n, ity); n += 20; }
3692 if (global.hasParachute) { drawSpriteAt('sParachuteIcon', -1, n, ity); n += 20; }
3694 if (player.holdItem && player.holdItem.objName == 'Bow' && global.arrows > 0) {
3697 while (m <= global.arrows && m <= 20 && malpha > 0) {
3698 Video.color = trunc(malpha*255)<<24|0xff_ff_ff;
3699 drawSpriteAt('sArrowIcon', -1, n, ity);
3701 if (m > 10) malpha -= 0.1; //and global.arrows > 20 malpha -= 0.1
3707 sprStore.loadFont('sFontSmall');
3708 Video.color = 0xff_ff_00|talpha;
3709 if (scumSmallHud) drawTextAt(moneyX+8, 8+8-hhup, va("+%d", xmoney));
3710 else drawTextAt(moneyX, 8+16-hhup, va("+%d", xmoney));
3713 Video.color = 0xff_ff_ff;
3714 if (global.config.ghostShowTime) renderGhostTimer(currFrameDelta);
3718 // ////////////////////////////////////////////////////////////////////////// //
3719 // x0 and y0 are non-scaled (and will be scaled)
3720 final void drawTextAtS3 (int x0, int y0, string text, optional int hiColor1, optional int hiColor2) {
3724 sprStore.renderTextWithHighlight(x0, y0, text, 3, hiColor1!optional, hiColor2!optional);
3728 final void drawTextAtS3Centered (int y0, string text, optional int hiColor1, optional int hiColor2) {
3730 int x0 = (viewWidth-sprStore.getTextWidth(text, 3, specified_hiColor1, specified_hiColor2))/2;
3731 sprStore.renderTextWithHighlight(x0, y0*3, text, 3, hiColor1!optional, hiColor2!optional);
3735 void renderHelpOverlay () {
3737 Video.fillRect(0, 0, viewWidth, viewHeight);
3740 int txoff = 0; // text x pos offset (for multi-color lines)
3742 if (gameHelpScreen) {
3743 sprStore.loadFont('sFontSmall');
3744 Video.color = 0xff_ff_ff;
3745 drawTextAtS3Centered(ty, va("HELP (PAGE ~%d~ OF ~%d~)", gameHelpScreen, MaxGameHelpScreen), 0xff_ff_00);
3749 if (gameHelpScreen == 1) {
3750 sprStore.loadFont('sFontSmall');
3751 Video.color = 0xff_ff_00; drawTextAtS3(tx, ty, "INVENTORY BASICS"); ty += 16;
3752 Video.color = 0xff_ff_ff;
3753 drawTextAtS3(tx, ty, global.expandString("Press $SWITCH to cycle through items."), 0x00_ff_00);
3756 Video.color = 0xff_ff_ff;
3757 drawSpriteAtS3('sHelpSprite1', -1, 64, 96);
3758 } else if (gameHelpScreen == 2) {
3759 sprStore.loadFont('sFontSmall');
3760 Video.color = 0xff_ff_00;
3761 drawTextAtS3(tx, ty, "SELLING TO SHOPKEEPERS"); ty += 16;
3762 Video.color = 0xff_ff_ff;
3763 drawTextAtS3(tx, ty, global.expandString("Press $PAY to offer your currently"), 0x00_ff_00); ty += 8;
3764 drawTextAtS3(tx, ty, "held item to the shopkeeper."); ty += 16;
3765 drawTextAtS3(tx, ty, "If the shopkeeper is interested, "); ty += 8;
3766 //drawTextAtS3(tx, ty, global.expandString("press $PAY again to complete the sale."), 0x00_ff_00); ty += 72;
3767 drawTextAtS3(tx, ty, global.expandString("press $PAY again to complete"), 0x00_ff_00);
3768 drawTextAtS3(tx, ty+8, "the sale.");
3770 drawSpriteAtS3('sHelpSell', -1, 112, 100);
3771 drawTextAtS3(tx, ty, "Purchasing goods from the shopkeeper"); ty += 8;
3772 drawTextAtS3(tx, ty, "will increase the funds he has"); ty += 8;
3773 drawTextAtS3(tx, ty, "available to buy your unwanted stuff."); ty += 8;
3776 sprStore.loadFont('sFont');
3777 Video.color = 0xff_ff_ff;
3778 drawTextAtS3(136, 8, "MAP");
3780 if (lg.mapSprite && (isNormalLevel() || isTransitionRoom())) {
3781 Video.color = 0xff_ff_00;
3782 drawTextAtS3Centered(24, lg.mapTitle);
3784 auto spf = sprStore[lg.mapSprite].frames[0];
3785 int mapX = 160-spf.width/2;
3786 int mapY = 120-spf.height/2;
3787 //mapTitleX = 160-string_length(global.mapTitle)*8/2;
3789 Video.color = 0xff_ff_ff;
3790 drawSpriteAtS3(lg.mapSprite, -1, mapX, mapY);
3792 if (lg.mapSprite != 'sMapDefault') {
3793 int mx = -1, my = -1;
3795 // set position of player icon
3796 switch (global.currLevel) {
3797 case 1: mx = 81; my = 22; break;
3798 case 2: mx = 113; my = 63; break;
3799 case 3: mx = 197; my = 86; break;
3800 case 4: mx = 133; my = 109; break;
3801 case 5: mx = 181; my = 22; break;
3802 case 6: mx = 126; my = 64; break;
3803 case 7: mx = 158; my = 112; break;
3804 case 8: mx = 66; my = 80; break;
3805 case 9: mx = 30; my = 26; break;
3806 case 10: mx = 88; my = 54; break;
3807 case 11: mx = 148; my = 81; break;
3808 case 12: mx = 210; my = 205; break;
3809 case 13: mx = 66; my = 17; break;
3810 case 14: mx = 146; my = 17; break;
3811 case 15: mx = 82; my = 77; break;
3812 case 16: mx = 178; my = 81; break;
3816 int plrx = mx+player.ix/16;
3817 int plry = my+player.iy/16;
3818 if (isTransitionRoom()) { plrx = mx+20; plry = my+16; }
3819 name plrspr = 'sMapSpelunker';
3820 if (global.isDamsel) plrspr = 'sMapDamsel';
3821 else if (global.isTunnelMan) plrspr = 'sMapTunnel';
3822 auto ss = sprStore[plrspr];
3823 drawSpriteAtS3(plrspr, (pausedTime/2)%ss.frames.length, mapX+plrx, mapY+plry);
3825 if (global.hasCompass && allExits.length) {
3826 drawSpriteAtS3('sMapRedDot', -1, mapX+mx+allExits[0].ix/16, mapY+my+allExits[0].iy/16);
3833 sprStore.loadFont('sFontSmall');
3834 Video.color = 0xff_ff_00;
3835 drawTextAtS3Centered(232, "PRESS ~SPACE~/~LEFT~/~RIGHT~ TO CHANGE PAGE", 0x00_ff_00);
3837 Video.color = 0xff_ff_ff;
3841 void renderPauseOverlay () {
3842 //drawTextAt(256, 432, "PAUSED", scale);
3844 if (gameShowHelp) { renderHelpOverlay(); return; }
3846 Video.color = 0xff_ff_00;
3847 //int hiColor = 0x00_ff_00;
3850 if (isTutorialRoom()) {
3851 sprStore.loadFont('sFont');
3852 drawTextAtS3Centered(n-24, "TUTORIAL CAVE");
3853 } else if (isNormalLevel()) {
3854 sprStore.loadFont('sFont');
3856 drawTextAtS3Centered(n-32, va("LEVEL ~%d~", global.currLevel), 0x00_ff_00);
3858 sprStore.loadFont('sFontSmall');
3860 int depth = round((174.8*(global.currLevel-1)+(player.iy+8)*0.34)*(global.config.scumMetric ? 0.3048 : 1.0)*10);
3861 string depthStr = va("DEPTH: ~%d.%d~ %s", depth/10, depth%10, (global.config.scumMetric ? "METRES" : "FEET"));
3862 drawTextAtS3Centered(n-16, depthStr, 0x00_ff_00);
3865 drawTextAtS3Centered(n, va("MONEY: ~%d~", stats.money), 0x00_ff_00);
3866 drawTextAtS3Centered(n+16, va("KILLS: ~%d~", stats.countKills), 0x00_ff_00);
3867 drawTextAtS3Centered(n+32, va("SAVES: ~%d~", stats.damselsSaved), 0x00_ff_00);
3868 drawTextAtS3Centered(n+48, va("TIME: ~%s~", time2str(time/30)), 0x00_ff_00);
3869 drawTextAtS3Centered(n+64, va("LEVEL TIME: ~%s~", time2str((time-levelStartTime)/30)), 0x00_ff_00);
3872 sprStore.loadFont('sFontSmall');
3873 Video.color = 0xff_ff_ff;
3874 drawTextAtS3Centered(240-2-8, "~ESC~-RETURN ~F10~-QUIT ~CTRL+DEL~-SUICIDE", 0xff_7f_00);
3875 drawTextAtS3Centered(2, "~O~PTIONS REDEFINE ~K~EYS ~S~TATISTICS", 0xff_7f_00);
3879 // ////////////////////////////////////////////////////////////////////////// //
3880 transient int drawLoot;
3881 transient int drawPosX, drawPosY;
3883 void resetTransitionOverlay () {
3890 // current game, uncollapsed
3891 struct LevelStatInfo {
3893 // for transition screen
3900 void thinkFrameTransition () {
3901 if (drawLoot == 0) {
3902 if (drawPosX > 272) {
3905 if (drawPosY > 83+4) drawPosY = 83;
3907 } else if (drawPosX > 232) {
3910 if (drawPosY > 91+4) drawPosY = 91;
3915 void renderTransitionOverlay () {
3916 sprStore.loadFont('sFontSmall');
3917 Video.color = 0xff_ff_00;
3918 //else if (global.currLevel-1 < 1) draw_text(32, 48, "TUTORIAL CAVE COMPLETED!");
3919 //else draw_text(32, 48, "LEVEL "+string(global.currLevel-1)+" COMPLETED!");
3920 if (global.currLevel == 0) {
3921 drawTextAt(32, 48, "TUTORIAL CAVE COMPLETED!");
3923 drawTextAt(32, 48, va("LEVEL ~%d~ COMPLETED!", global.currLevel), hiColor1:0x00_ff_ff);
3925 Video.color = 0xff_ff_ff;
3926 drawTextAt(32, 64, va("TIME = ~%s~", time2str((levelEndTime-levelStartTime)/30)), hiColor1:0xff_ff_00);
3928 if (/*stats.collected.length == 0*/stats.money <= levelMoneyStart) {
3929 drawTextAt(32, 80, "LOOT = ~NONE~", hiColor1:0xff_00_00);
3931 drawTextAt(32, 80, va("LOOT = ~%d~", stats.money-levelMoneyStart), hiColor1:0xff_ff_00);
3934 if (stats.kills.length == 0) {
3935 drawTextAt(32, 96, "KILLS = ~NONE~", hiColor1:0x00_ff_00);
3937 drawTextAt(32, 96, va("KILLS = ~%d~", stats.kills.length), hiColor1:0xff_ff_00);
3940 drawTextAt(32, 112, va("MONEY = ~%d~", stats.money), hiColor1:0xff_ff_00);
3944 // ////////////////////////////////////////////////////////////////////////// //
3945 private transient array!MapEntity renderVisibleCids;
3946 private transient array!MapEntity renderVisibleLights;
3947 private transient array!MapTile renderFrontTiles; // normal, with fg
3949 final int renderSortByDepth (MapEntity oa, MapEntity ob) {
3950 //auto da = oa.depth, db = ob.depth;
3951 //if (da == db) return (oa.objId < ob.objId);
3953 auto d = oa.depth-ob.depth;
3954 return (d ? d : oa.objId-ob.objId);
3958 const int RenderEdgePixNormal = 64;
3959 const int RenderEdgePixLight = 256;
3961 #ifndef EXPERIMENTAL_RENDER_CACHE
3962 enum skipListCreation = false;
3965 void renderWithOfs (int xofs, int yofs, float currFrameDelta) {
3966 int scale = global.scale;
3968 // don't touch framebuffer alpha
3969 Video.colorMask = Video::CMask.Colors;
3970 Video.color = 0xff_ff_ff;
3973 Video::ScissorRect scsave;
3974 bool doRestoreGL = false;
3976 if (viewOffsetX > 0 || viewOffsetY > 0) {
3978 Video.getScissor(scsave);
3979 Video.scissorCombine(viewOffsetX, viewOffsetY, viewWidth, viewHeight);
3980 Video.glPushMatrix();
3981 Video.glTranslate(viewOffsetX, viewOffsetY);
3982 //Video.glTranslate(-550, 0);
3983 //Video.glScale(1, 1);
3988 bool isDarkLevel = global.darkLevel;
3991 switch (global.config.scumPlayerLit) {
3992 case 0: player.lightRadius = 0; break; // never
3993 case 1: // only in "scumDarkness"
3994 player.lightRadius = (global.config.scumDarkness >= 2 ? 96 : 32);
3997 player.lightRadius = 96;
4002 // render cave background
4005 int bgw = levBGImg.tex.width*scale;
4006 int bgh = levBGImg.tex.height*scale;
4007 int bgTW = (tilesWidth*tsz+bgw-1)/bgw;
4008 int bgTH = (tilesHeight*tsz+bgh-1)/bgh;
4009 int bgX0 = max(0, xofs/bgw);
4010 int bgY0 = max(0, yofs/bgh);
4011 int bgX1 = min(bgTW, (xofs+viewWidth+bgw-1)/bgw);
4012 int bgY1 = min(bgTH, (yofs+viewHeight+bgh-1)/bgh);
4013 foreach (int ty; bgY0..bgY1) {
4014 foreach (int tx; bgX0..bgX1) {
4015 int x0 = tx*bgw-xofs;
4016 int y0 = ty*bgh-yofs;
4017 levBGImg.blitAt(x0, y0, scale);
4022 int RenderEdgePix = (global.darkLevel ? RenderEdgePixLight : RenderEdgePixNormal);
4024 // render background tiles
4025 for (MapBackTile bt = backtiles; bt; bt = bt.next) {
4026 bt.drawWithOfs(xofs, yofs, scale, currFrameDelta);
4029 // collect visible special tiles
4030 #ifdef EXPERIMENTAL_RENDER_CACHE
4031 bool skipListCreation = (lastRenderTime == time && renderVisibleCids.length); //FIXME
4034 if (!skipListCreation) {
4035 renderVisibleCids.clear();
4036 renderVisibleLights.clear();
4037 renderFrontTiles.clear();
4039 int endVX = xofs+viewWidth;
4040 int endVY = yofs+viewHeight;
4044 if (player && player.visible) renderVisibleCids[$] = player; // include player in render list
4046 //FIXME: drop lit objects which cannot affect visible area
4048 // collect visible objects
4049 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)) {
4050 if (!o.visible) continue;
4051 auto tile = MapTile(o);
4053 if (isDarkLevel && (o.lightRadius > 4 || tile.litWholeTile)) renderVisibleLights[$] = o;
4054 if (tile.invisible) continue;
4055 if (tile.bgfront /*|| tile.spriteLeftDeco || tile.spriteRightDeco*/) renderFrontTiles[$] = tile;
4056 if (tile.bgback) tile.drawWithOfsBack(xofs, yofs, scale, currFrameDelta);
4058 if (isDarkLevel && o.lightRadius > 4) renderVisibleLights[$] = o;
4060 // check if the object is really visible -- this will speed up later sorting
4061 int fx0, fy0, fx1, fy1;
4062 auto spf = o.getSpriteFrame(default, out fx0, out fy0, out fx1, out fy1);
4063 if (!spf) continue; // no sprite -- nothing to draw (no, really)
4064 int ix = o.ix, iy = o.iy;
4065 int x0 = (ix+fx0)*scale, y0 = (iy+fy0)*scale;
4066 int x1 = (ix+fx1)*scale, y1 = (iy+fy1)*scale;
4067 if (x1 <= xofs || y1 <= yofs || x0 >= endVX || y0 >= endVY) {
4071 renderVisibleCids[$] = o;
4074 foreach (MapEntity o; objGrid.allObjects()) {
4075 if (!o.visible) continue;
4076 auto tile = MapTile(o);
4078 if (isDarkLevel && (o.lightRadius > 4 || tile.litWholeTile)) renderVisibleLights[$] = o;
4079 if (tile.invisible) continue;
4080 if (tile.bgfront /*|| tile.spriteLeftDeco || tile.spriteRightDeco*/) renderFrontTiles[$] = tile;
4081 if (tile.bgback) tile.drawWithOfsBack(xofs, yofs, scale, currFrameDelta);
4083 if (isDarkLevel && o.lightRadius > 4) renderVisibleLights[$] = o;
4085 renderVisibleCids[$] = o;
4088 //writeln("::: ", cnt, " invisible objects dropped");
4090 renderVisibleCids.sort(&renderSortByDepth);
4091 lastRenderTime = time;
4094 auto depth4Start = 0;
4095 foreach (auto xidx, MapEntity o; renderVisibleCids) {
4102 bool playerPowerupRendered = false;
4104 // render objects (part one: depth > 3)
4105 foreach (auto idx; depth4Start..renderVisibleCids.length; reverse) {
4106 MapEntity o = renderVisibleCids[idx];
4107 // 1000 is an ordinary tile
4108 if (!playerPowerupRendered && o.depth <= 1200) {
4109 playerPowerupRendered = true;
4110 // so ducking player will have it's cape correctly rendered
4111 if (player.visible) player.drawPrePrePowerupWithOfs(xofs, yofs, scale, currFrameDelta);
4113 //if (idx && renderSortByDepth(o, renderVisibleCids[idx-1])) writeln("VIOLATION!");
4114 o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
4117 // render object (part two: front tile parts, depth 3.5)
4118 foreach (MapTile tile; renderFrontTiles) {
4119 tile.drawWithOfsFront(xofs, yofs, scale, currFrameDelta);
4122 // render objects (part three: depth <= 3)
4123 foreach (auto idx; 0..depth4Start; reverse) {
4124 MapEntity o = renderVisibleCids[idx];
4125 o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
4126 //done above;if (isDarkLevel && (o.lightRadius > 4 || (o isa MapTile && MapTile(o).litWholeTile))) renderVisibleLights[$] = o;
4129 // render player post-effects, so cape or jetpack will be correctly rendered on level exit, for example
4130 player.lastDrawWithOfs(xofs, yofs, scale, currFrameDelta);
4134 auto ltex = bgtileStore.lightTexture('ltx512', 512);
4136 // set screen alpha to min
4137 Video.colorMask = Video::CMask.Alpha;
4138 Video.blendMode = Video::BlendMode.None;
4139 Video.color = 0xff_ff_ff_ff;
4140 Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
4141 //Video.colorMask = Video::CMask.All;
4144 // also, stencil 'em, so we can filter dark areas
4145 Video.textureFiltering = true;
4146 Video.stencil = true;
4147 Video.stencilFunc(Video::StencilFunc.Always, 1);
4148 Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Replace);
4149 Video.alphaTestFunc = Video::AlphaFunc.Greater;
4150 Video.alphaTestVal = 0.03+0.011*global.config.darknessDarkness;
4151 Video.color = 0xff_ff_ff;
4152 Video.blendFunc = Video::BlendFunc.Max;
4153 Video.blendMode = Video::BlendMode.Blend; // anything except `Normal`
4154 Video.colorMask = Video::CMask.Alpha;
4156 foreach (MapEntity e; renderVisibleLights) {
4158 e.getInterpCoords(currFrameDelta, scale, out xi, out yi);
4159 auto tile = MapTile(e);
4160 if (tile && tile.litWholeTile) {
4161 //Video.color = 0xff_ff_ff;
4162 Video.fillRect(xi-xofs, yi-yofs, e.width*scale, e.height*scale);
4164 int lrad = e.lightRadius;
4165 if (lrad < 4) continue; // just in case
4167 //if (loserGPU && lrad%12 != 0) lrad = (lrad/12)*12;
4168 float lightscale = float(lrad*scale)/float(ltex.tex.width);
4169 #ifdef OLD_LIGHT_OFFSETS
4170 int fx0, fy0, fx1, fy1;
4172 auto spf = e.getSpriteFrame(out doMirror, out fx0, out fy0, out fx1, out fy1);
4174 xi += (fx1-fx0)*scale/2;
4175 yi += (fy1-fy0)*scale/2;
4179 e.getLightOffset(out lxofs, out lyofs);
4184 lrad = lrad*scale/2;
4187 ltex.tex.blitAt(xi, yi, lightscale);
4189 Video.textureFiltering = false;
4192 // modify only lit parts
4193 Video.stencilFunc(Video::StencilFunc.Equal, 1);
4194 Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Keep);
4195 // multiply framebuffer colors by framebuffer alpha
4196 Video.color = 0xff_ff_ff; // it doesn't matter
4197 Video.blendFunc = Video::BlendFunc.Add;
4198 Video.blendMode = Video::BlendMode.DstMulDstAlpha;
4199 Video.colorMask = Video::CMask.Colors;
4200 Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
4203 // filter unlit parts
4204 Video.stencilFunc(Video::StencilFunc.NotEqual, 1);
4205 Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Keep);
4206 Video.blendFunc = Video::BlendFunc.Add;
4207 Video.blendMode = Video::BlendMode.Filter;
4208 Video.colorMask = Video::CMask.Colors;
4209 Video.color = 0x00_00_18+0x00_00_10*global.config.darknessDarkness;
4210 //Video.color = 0x00_00_18;
4211 //Video.color = 0x00_00_38;
4212 Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
4215 Video.blendFunc = Video::BlendFunc.Add;
4216 Video.blendMode = Video::BlendMode.Normal;
4217 Video.colorMask = Video::CMask.All;
4218 Video.alphaTestFunc = Video::AlphaFunc.Always;
4219 Video.stencil = false;
4222 // clear visible objects list (nope)
4223 //renderVisibleCids.clear();
4224 //renderVisibleLights.clear();
4227 if (global.config.drawHUD) renderHUD(currFrameDelta);
4228 renderCompass(currFrameDelta);
4230 float osdTimeLeft, osdTimeStart;
4231 string msg = osdGetMessage(out osdTimeLeft, out osdTimeStart);
4233 auto ct = GetTickCount();
4235 sprStore.loadFont('sFontSmall');
4236 auto msgHeight = sprStore.getMultilineTextHeight(msg, msgScale);
4237 int x = viewWidth/2;
4238 int y = viewHeight-64-msgHeight;
4239 auto oldColor = Video.color;
4240 Video.color = 0xff_ff_00;
4241 if (osdTimeLeft < 0.5) {
4242 int alpha = 255-clamp(int(510*osdTimeLeft), 0, 255);
4243 Video.color = Video.color|(alpha<<24);
4244 } else if (ct-osdTimeStart < 0.5) {
4245 osdTimeStart = ct-osdTimeStart;
4246 int alpha = 255-clamp(int(510*osdTimeStart), 0, 255);
4247 Video.color = Video.color|(alpha<<24);
4249 sprStore.renderMultilineTextCentered(x, y, msg, msgScale, 0x00_ff_00, 0xff_ff_ff);
4250 Video.color = oldColor;
4253 int hiColor1, hiColor2;
4254 msg = osdGetTalkMessage(out hiColor1, out hiColor2);
4257 sprStore.loadFont('sFontSmall');
4258 auto msgWidth = sprStore.getMultilineTextWidth(msg, processHighlights1:true, processHighlights2:true);
4259 auto msgHeight = sprStore.getMultilineTextHeight(msg);
4260 auto msgWidthOrig = msgWidth*msgScale;
4261 auto msgHeightOrig = msgHeight*msgScale;
4262 if (msgWidth%16 != 0) msgWidth = (msgWidth|0x0f)+1;
4263 if (msgHeight%16 != 0) msgHeight = (msgHeight|0x0f)+1;
4264 msgWidth *= msgScale;
4265 msgHeight *= msgScale;
4266 int x = (viewWidth-msgWidth)/2;
4267 int y = 32*msgScale;
4268 auto oldColor = Video.color;
4269 // draw text frame and text background
4271 Video.fillRect(x, y, msgWidth, msgHeight);
4272 Video.color = 0xff_ff_ff;
4273 for (int fdx = 0; fdx < msgWidth; fdx += 16*msgScale) {
4274 auto spf = sprStore['sMenuTop'].frames[0];
4275 spf.blitAt(x+fdx, y-16*msgScale, msgScale);
4276 spf = sprStore['sMenuBottom'].frames[0];
4277 spf.blitAt(x+fdx, y+msgHeight, msgScale);
4279 for (int fdy = 0; fdy < msgHeight; fdy += 16*msgScale) {
4280 auto spf = sprStore['sMenuLeft'].frames[0];
4281 spf.blitAt(x-16*msgScale, y+fdy, msgScale);
4282 spf = sprStore['sMenuRight'].frames[0];
4283 spf.blitAt(x+msgWidth, y+fdy, msgScale);
4286 auto spf = sprStore['sMenuUL'].frames[0];
4287 spf.blitAt(x-16*msgScale, y-16*msgScale, msgScale);
4288 spf = sprStore['sMenuUR'].frames[0];
4289 spf.blitAt(x+msgWidth, y-16*msgScale, msgScale);
4290 spf = sprStore['sMenuLL'].frames[0];
4291 spf.blitAt(x-16*msgScale, y+msgHeight, msgScale);
4292 spf = sprStore['sMenuLR'].frames[0];
4293 spf.blitAt(x+msgWidth, y+msgHeight, msgScale);
4295 Video.color = 0xff_ff_00;
4296 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));
4297 Video.color = oldColor;
4300 if (inWinCutscene) renderWinCutsceneOverlay();
4301 if (inIntroCutscene) renderTitleCutsceneOverlay();
4302 if (isTransitionRoom()) renderTransitionOverlay();
4306 Video.setScissor(scsave);
4307 Video.glPopMatrix();
4311 Video.color = 0xff_ff_ff;
4315 // ////////////////////////////////////////////////////////////////////////// //
4316 final class!MapObject findGameObjectClassByName (name aname) {
4317 if (!aname) return none; // just in case
4318 auto co = FindClassByGameObjName(aname);
4320 writeln("***UNKNOWN GAME OBJECT: '", aname, "'");
4323 co = GetClassReplacement(co);
4324 if (!co) FatalError("findGameObjectClassByName: WTF?!");
4325 if (!class!MapObject(co)) FatalError("findGameTileClassByName: object '%n' is not a map object, it is a `%n`", aname, GetClassName(co));
4326 return class!MapObject(co);
4330 final class!MapTile findGameTileClassByName (name aname) {
4331 if (!aname) return none; // just in case
4332 auto co = FindClassByGameObjName(aname);
4333 if (!co) return MapTile; // unknown names will be routed directly to tile object
4334 co = GetClassReplacement(co);
4335 if (!co) FatalError("findGameTileClassByName: WTF?!");
4336 if (!class!MapTile(co)) FatalError("findGameTileClassByName: object '%n' is not a tile, it is a `%n`", aname, GetClassName(co));
4337 return class!MapTile(co);
4341 final MapObject findAnyObjectOfType (name aname) {
4342 if (!aname) return none;
4343 auto cls = FindClassByGameObjName(aname);
4344 if (!cls) return none;
4345 foreach (MapObject obj; objGrid.allObjects(MapObject)) {
4346 if (obj.spectral) continue;
4347 if (obj isa cls) return obj;
4353 // ////////////////////////////////////////////////////////////////////////// //
4354 final bool isRopePlacedAt (int x, int y) {
4356 foreach (ref auto v; covered) v = false;
4357 foreach (MapTile t; objGrid.inRectPix(x, y-8, 1, 17, precise:false, castClass:MapTileRope)) {
4358 //if (!cbIsRopeTile(t)) continue;
4359 if (t.ix != x) continue;
4360 if (t.iy == y) return true;
4361 foreach (int ty; t.iy..t.iy+8) {
4363 if (d >= 0 && d < covered.length) covered[d] = true;
4366 // check if the whole rope height is completely covered with ropes
4367 foreach (auto v; covered) if (!v) return false;
4372 final MapTile CreateMapTile (int xpos, int ypos, name aname) {
4373 if (!aname) FatalError("cannot create typeless tile");
4374 auto tclass = findGameTileClassByName(aname);
4375 if (!tclass) return none;
4376 MapTile tile = SpawnObject(tclass);
4377 tile.global = global;
4379 tile.objName = aname;
4380 tile.objType = aname; // just in case
4383 tile.objId = ++lastUsedObjectId;
4384 if (!tile.initialize()) FatalError("cannot create tile '%n'", aname);
4389 final bool PutSpawnedMapTile (int x, int y, MapTile tile) {
4390 if (!tile || !tile.isInstanceAlive) return false;
4392 //if (putToGrid) tile.active = true;
4393 bool putToGrid = (tile.moveable || tile.toSpecialGrid || tile.width != 16 || tile.height != 16 || x%16 != 0 || y%16 != 0);
4395 //writeln("putting tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (putToGrid ? "special " : ""), "grid, pos=(", x/16.0, ",", y/16.0, ")");
4398 int mapx = x/16, mapy = y/16;
4399 if (mapx < 0 || mapx >= tilesWidth || mapy < 0 || mapy >= tilesHeight) return false;
4402 // if we already have rope tile there, there is no reason to add another one
4403 if (tile isa MapTileRope) {
4404 if (isRopePlacedAt(x, y)) return false;
4407 // activate special or animated tile
4408 tile.active = tile.active || tile.moveable || tile.toSpecialGrid;
4409 // animated tiles must be active
4411 auto spr = tile.getSprite();
4412 if (spr && spr.frames.length > 1) {
4413 writeln("activated animated tile '", tile.objName, "'");
4421 //if (tile isa TitleTileCopy) writeln("*** PUTTING COPYRIGHT TILE");
4422 //tile.toSpecialGrid = true;
4423 if (!tile.dontReplaceOthers && x&16 == 0 && y%16 == 0) {
4424 auto t = getTileAtGridAny(x/16, y/16);
4425 if (t && !t.immuneToReplacement) {
4426 writeln("REPLACING tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
4427 writeln(" NEW tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (tile.toSpecialGrid ? "special " : ""), "grid, pos=(", tile.fltx/16.0, ",", tile.flty/16.0, ")");
4433 //writeln("SIZE: ", tilesWidth, "x", tilesHeight);
4434 setTileAtGrid(x/16, y/16, tile);
4436 auto t = getTileAtGridAny(x/16, y/16);
4438 writeln("putting tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (putToGrid ? "special " : ""), "grid, pos=(", x/16.0, ",", y/16.0, ")");
4439 checkTilesInRect(x/16, y/16, 16, 16, delegate bool (MapTile tile) {
4440 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, ")");
4443 FatalError("FUUUUUU");
4448 if (tile.enter) registerEnter(tile);
4449 if (tile.exit) registerExit(tile);
4451 // make tile under exit invulnerable
4452 if (checkTilesInRect(tile.ix, tile.iy-16, 16, 16, delegate bool (MapTile t) { return t.exit; })) {
4453 tile.invincible = true;
4460 // won't call `onDestroy()`
4461 final void RemoveMapTileFromGrid (int tileX, int tileY, optional string reason) {
4462 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
4463 auto t = getTileAtGridAny(tileX, tileY);
4465 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, ")");
4473 final MapTile MakeMapTile (int mapx, int mapy, name aname) {
4474 //writeln("tile at (", mapx, ",", mapy, "): ", aname);
4475 //if (aname == 'oLush') { MapObject fail; fail.initialize(); }
4476 //if (mapy >= tilesHeight) { writeln("OOM: ", aname, "; ty=", mapy, "; hgt=", tilesHeight); }
4477 if (mapx < 0 || mapx >= tilesWidth || mapy < 0 || mapy >= tilesHeight) return none;
4479 // if we already have rope tile there, there is no reason to add another one
4480 if (aname == 'oRope') {
4481 if (isRopePlacedAt(mapx*16, mapy*16)) return none;
4484 auto tile = CreateMapTile(mapx*16, mapy*16, aname);
4485 if (!tile) return none;
4486 if (!PutSpawnedMapTile(mapx*16, mapy*16, tile)) {
4495 final MapTile MakeMapTileAtPix (int xpix, int ypix, name aname) {
4496 // if we already have rope tile there, there is no reason to add another one
4497 if (aname == 'oRope') {
4498 if (isRopePlacedAt(xpix, ypix)) return none;
4501 auto tile = CreateMapTile(xpix, ypix, aname);
4502 if (!tile) return none;
4503 if (!PutSpawnedMapTile(xpix, ypix, tile)) {
4512 final MapTile MakeMapRopeTileAt (int x0, int y0) {
4513 // if we already have rope tile there, there is no reason to add another one
4514 if (isRopePlacedAt(x0, y0)) return none;
4516 auto tile = CreateMapTile(x0, y0, 'oRope');
4517 if (!PutSpawnedMapTile(x0, y0, tile)) {
4526 // ////////////////////////////////////////////////////////////////////////// //
4527 final MapBackTile CreateBgTile (name sprName, optional int atx0, optional int aty0, optional int aw, optional int ah) {
4528 BackTileImage img = bgtileStore[sprName];
4529 auto res = SpawnObject(MapBackTile);
4530 res.global = global;
4533 res.bgtName = sprName;
4534 if (specified_atx0) res.tx0 = atx0;
4535 if (specified_aty0) res.ty0 = aty0;
4536 if (specified_aw) res.w = aw;
4537 if (specified_ah) res.h = ah;
4538 if (!res.initialize()) FatalError("cannot create background tile with image '%n'", sprName);
4543 // ////////////////////////////////////////////////////////////////////////// //
4545 background The background asset from which the new tile will be extracted.
4546 left The x coordinate of the left of the new tile, relative to the background asset's top left corner.
4547 top The y coordinate of the top of the new tile, relative to the background assets top left corner.
4548 width The width of the tile.
4549 height The height of the tile.
4550 x The x position in the room to place the tile.
4551 y The y position in the room to place the tile.
4552 depth The depth at which to place the tile.
4554 final void MakeMapBackTile (name bgname, int left, int top, int width, int height, int x, int y, int depth) {
4555 if (width < 1 || height < 1 || !bgname) return;
4556 auto bgt = bgtileStore[bgname];
4557 if (!bgt) FatalError("cannot load background '%n'", bgname);
4558 MapBackTile bt = SpawnObject(MapBackTile);
4561 bt.objName = bgname;
4563 bt.bgtName = bgname;
4571 // find a place for it
4576 // back tiles with the highest depth should come first
4577 MapBackTile ct = backtiles, cprev = none;
4578 while (ct && depth <= ct.depth) { cprev = ct; ct = ct.next; }
4581 bt.next = cprev.next;
4584 bt.next = backtiles;
4590 // ////////////////////////////////////////////////////////////////////////// //
4591 final MapObject SpawnMapObjectWithClass (class!MapObject oclass) {
4592 if (!oclass) return none;
4594 MapObject obj = SpawnObject(oclass);
4595 if (!obj) FatalError("cannot create map object '%n'", GetClassName(oclass));
4597 //if (aname == 'oShells' || aname == 'oShellSingle' || aname == 'oRopePile') writeln("*** CREATED '", aname, "'");
4599 obj.global = global;
4601 obj.objId = ++lastUsedObjectId;
4607 final MapObject SpawnMapObject (name aname) {
4608 if (!aname) return none;
4609 auto res = SpawnMapObjectWithClass(findGameObjectClassByName(aname));
4610 if (res && !res.objType) res.objType = aname; // just in case
4615 final MapObject PutSpawnedMapObject (int x, int y, MapObject obj) {
4616 if (!obj /*|| obj.global || obj.level*/) return none; // oops
4620 if (!obj.initialize()) { delete obj; return none; } // not fatal
4623 if (obj.walkableSolid) hasSolidObjects = true;
4629 final MapObject MakeMapObject (int x, int y, name aname) {
4630 MapObject obj = SpawnMapObject(aname);
4631 obj = PutSpawnedMapObject(x, y, obj);
4636 // ////////////////////////////////////////////////////////////////////////// //
4637 void setMenuTilesVisible (bool vis) {
4639 forEachTile(delegate bool (MapTile t) {
4640 if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4641 t.invisible = false;
4646 forEachTile(delegate bool (MapTile t) {
4647 if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4656 void setMenuTilesOnTop () {
4657 forEachTile(delegate bool (MapTile t) {
4658 if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4666 // ////////////////////////////////////////////////////////////////////////// //
4667 #include "roomTitle.vc"
4668 #include "roomTrans1.vc"
4669 #include "roomTrans2.vc"
4670 #include "roomTrans3.vc"
4671 #include "roomTrans4.vc"
4672 #include "roomOlmec.vc"
4673 #include "roomEnd.vc"
4674 #include "roomIntro.vc"
4675 #include "roomTutorial.vc"
4676 #include "roomScores.vc"
4677 #include "roomStars.vc"
4678 #include "roomSun.vc"
4679 #include "roomMoon.vc"
4682 // ////////////////////////////////////////////////////////////////////////// //
4683 #include "packages/Generator/loadRoomGens.vc"
4684 #include "packages/Generator/loadEntityGens.vc"