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 = GC_ImmediateDelete;
891 GC_ImmediateDelete = false;
899 addBackgroundGfxDetails();
900 //levBGImgName = 'bgCave';
901 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
903 blockWaterChecking = true;
907 GC_ImmediateDelete = olddel;
908 GC_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 = GC_ImmediateDelete;
962 GC_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 GC_ImmediateDelete = olddel;
1021 GC_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 = GC_ImmediateDelete;
1061 GC_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 GC_ImmediateDelete = olddel;
1119 GC_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 roundi(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 roundi(sqrt(curdistsq));
1312 // ////////////////////////////////////////////////////////////////////////// //
1313 final void clearForTransition () {
1314 auto olddel = GC_ImmediateDelete;
1315 GC_ImmediateDelete = false;
1317 GC_ImmediateDelete = olddel;
1318 GC_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 = roundi(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]+ffloor(view_hview[0] / 2), oGhost);
1627 else instance_create(view_xview[0]-32, view_yview[0]+ffloor(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 = GC_ImmediateDelete;
2028 GC_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 GC_ImmediateDelete = olddel;
2039 if (olddel) GC_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 = GC_ImmediateDelete;
2232 GC_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 GC_ImmediateDelete = olddel;
2358 playerExited = false;
2360 centerViewAtPlayer();
2363 // if we were processed at least one frame, collect garbage
2365 GC_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 roundi(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 roundi(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 : roundi(what.favor/2.0)); //k8: added `roundi()`
3081 if (what.favor > 0) what.favor = 0;
3083 global.favor += (what.status == MapObject::STUNNED ? what.favor : roundi(what.favor/2.0)); //k8: added `roundi()`
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);
3143 //print("vy=%s; lo=%s; hi=%s", viewStart.y, viewMin.y*scale, viewMax.y*scale-viewHeight);
3147 final void centerViewAtPlayer () {
3148 if (viewWidth < 1 || viewHeight < 1 || !player) return;
3149 centerViewAt(player.xCenter, player.yCenter);
3153 final void centerViewAt (int x, int y) {
3154 if (viewWidth < 1 || viewHeight < 1) return;
3156 cameraSlideToSpeed.x = 0;
3157 cameraSlideToSpeed.y = 0;
3158 cameraSlideToPlayer = 0;
3160 int scale = global.scale;
3163 realViewStart.x = clamp(x-viewWidth/2, 0, tilesWidth*16*scale-viewWidth);
3164 realViewStart.y = clamp(y-viewHeight/2, 0, tilesHeight*16*scale-viewHeight);
3167 viewStart.x = realViewStart.x;
3168 viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
3171 if (onCameraTeleported) onCameraTeleported();
3175 const int ViewPortToleranceX = 16*1+8;
3176 const int ViewPortToleranceY = 16*1+8;
3178 final void fixCamera () {
3179 if (!player) return;
3180 if (viewWidth < 1 || viewHeight < 1) return;
3181 int scale = global.scale;
3182 auto alwaysCenterX = global.config.alwaysCenterPlayer;
3183 auto alwaysCenterY = alwaysCenterX;
3184 // calculate offset from viewport center (in game units), and fix viewport
3186 int camDestX = player.ix+8;
3187 int camDestY = player.iy+8;
3188 if (!cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y) != 0) {
3189 // slide camera to point
3190 if (cameraSlideToSpeed.x) camDestX = cameraSlideToCurr.x;
3191 if (cameraSlideToSpeed.y) camDestY = cameraSlideToCurr.y;
3192 int dx = cameraSlideToDest.x-camDestX;
3193 int dy = cameraSlideToDest.y-camDestY;
3194 //writeln("speed:(", cameraSlideToSpeed.x, ",", cameraSlideToSpeed.y, "); cur:(", camDestX, ",", camDestY, "); dest:(", cameraSlideToDest.x, ",", cameraSlideToDest.y, "); delta=(", dx, ",", dy, ")");
3195 if (dx && cameraSlideToSpeed.x != 0) {
3196 alwaysCenterX = true;
3197 if (abs(dx) <= abs(cameraSlideToSpeed.x)) {
3198 camDestX = cameraSlideToDest.x;
3200 camDestX += sign(dx)*abs(cameraSlideToSpeed.x);
3203 if (dy && abs(cameraSlideToSpeed.y) != 0) {
3204 alwaysCenterY = true;
3205 if (abs(dy) <= cameraSlideToSpeed.y) {
3206 camDestY = cameraSlideToDest.y;
3208 camDestY += sign(dy)*abs(cameraSlideToSpeed.y);
3211 //writeln(" new:(", camDestX, ",", camDestY, ")");
3212 if (cameraSlideToSpeed.x) cameraSlideToCurr.x = camDestX;
3213 if (cameraSlideToSpeed.y) cameraSlideToCurr.y = camDestY;
3217 if (cameraSlideToSpeed.x && !cameraSlideToPlayer) {
3218 realViewStart.x = clamp(camDestX*scale, 0, tilesWidth*16*scale-viewWidth);
3219 } else if (!player.cameraBlockX) {
3220 int x = camDestX*scale;
3221 int cx = realViewStart.x;
3222 if (alwaysCenterX) {
3225 int xofs = x-(cx+viewWidth/2);
3226 if (xofs < -ViewPortToleranceX*scale) cx += xofs+ViewPortToleranceX*scale;
3227 else if (xofs > ViewPortToleranceX*scale) cx += xofs-ViewPortToleranceX*scale;
3229 // slide back to player?
3230 if (cameraSlideToPlayer && cameraSlideToSpeed.x) {
3231 int prevx = cameraSlideToCurr.x*scale;
3232 int dx = (cx-prevx)/scale;
3233 if (abs(dx) <= cameraSlideToSpeed.x) {
3234 writeln("BACKSLIDE X COMPLETE!");
3235 cameraSlideToSpeed.x = 0;
3237 cx = prevx+(sign(dx)*cameraSlideToSpeed.x)*scale;
3238 cameraSlideToCurr.x += sign(dx)*cameraSlideToSpeed.x;
3239 if ((dx < 0 && cx <= 0) || (dx > 0 && cx >= tilesWidth*16*scale-viewWidth)) {
3240 writeln("BACKSLIDE X COMPLETE!");
3241 cameraSlideToSpeed.x = 0;
3245 realViewStart.x = clamp(cx, 0, tilesWidth*16*scale-viewWidth);
3249 if (cameraSlideToSpeed.y && !cameraSlideToPlayer) {
3250 realViewStart.y = clamp(camDestY*scale, 0, tilesHeight*16*scale-viewHeight);
3251 } else if (!player.cameraBlockY) {
3252 int y = camDestY*scale;
3253 int cy = realViewStart.y;
3254 if (alwaysCenterY) {
3255 cy = y-viewHeight/2;
3257 int yofs = y-(cy+viewHeight/2);
3258 if (yofs < -ViewPortToleranceY*scale) cy += yofs+ViewPortToleranceY*scale;
3259 else if (yofs > ViewPortToleranceY*scale) cy += yofs-ViewPortToleranceY*scale;
3261 // slide back to player?
3262 if (cameraSlideToPlayer && cameraSlideToSpeed.y) {
3263 int prevy = cameraSlideToCurr.y*scale;
3264 int dy = (cy-prevy)/scale;
3265 if (abs(dy) <= cameraSlideToSpeed.y) {
3266 writeln("BACKSLIDE Y COMPLETE!");
3267 cameraSlideToSpeed.y = 0;
3269 cy = prevy+(sign(dy)*cameraSlideToSpeed.y)*scale;
3270 cameraSlideToCurr.y += sign(dy)*cameraSlideToSpeed.y;
3271 if ((dy < 0 && cy <= 0) || (dy > 0 && cy >= tilesHeight*16*scale-viewHeight)) {
3272 writeln("BACKSLIDE Y COMPLETE!");
3273 cameraSlideToSpeed.y = 0;
3277 realViewStart.y = clamp(cy, 0, tilesHeight*16*scale-viewHeight);
3280 if (cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y) == 0) cameraSlideToPlayer = 0;
3283 //writeln(" new2:(", cameraCurrX, ",", cameraCurrY, ")");
3285 viewStart.x = realViewStart.x;
3286 viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
3291 // ////////////////////////////////////////////////////////////////////////// //
3292 // x0 and y0 are non-scaled (and will be scaled)
3293 final void drawSpriteAt (name sprName, float frnumf, int x0, int y0, optional bool small) {
3294 if (!sprName) return;
3295 auto spr = sprStore[sprName];
3296 if (!spr || !spr.frames.length) return;
3297 int scale = global.scale;
3300 int frnum = max(0, trunci(frnumf))%spr.frames.length;
3301 auto sfr = spr.frames[frnum];
3302 int sx0 = x0-sfr.xofs*scale;
3303 int sy0 = y0-sfr.yofs*scale;
3304 if (small && scale > 1) {
3305 sfr.tex.blitExt(sx0, sy0, roundi(sx0+sfr.width*(scale/2.0)), roundi(sy0+sfr.height*(scale/2.0)), 0, 0);
3307 sfr.blitAt(sx0, sy0, scale);
3312 final void drawSpriteAtS3 (name sprName, float frnumf, int x0, int y0) {
3313 if (!sprName) return;
3314 auto spr = sprStore[sprName];
3315 if (!spr || !spr.frames.length) return;
3318 int frnum = max(0, trunci(frnumf))%spr.frames.length;
3319 auto sfr = spr.frames[frnum];
3320 int sx0 = x0-sfr.xofs*3;
3321 int sy0 = y0-sfr.yofs*3;
3322 sfr.blitAt(sx0, sy0, 3);
3326 // x0 and y0 are non-scaled (and will be scaled)
3327 final void drawTextAt (int x0, int y0, string text, optional int scale, optional int hiColor1, optional int hiColor2) {
3329 if (!specified_scale) scale = global.scale;
3332 sprStore.renderTextWithHighlight(x0, y0, text, scale, hiColor1!optional, hiColor2!optional);
3336 void renderCompass (float currFrameDelta) {
3337 if (!global.hasCompass) return;
3340 if (isRoom("rOlmec")) {
3343 } else if (isRoom("rOlmec2")) {
3349 bool hasMessage = osdHasMessage();
3350 foreach (MapTile et; allExits) {
3352 int exitX = et.ix, exitY = et.iy;
3353 int vx0 = viewStart.x/global.scale, vy0 = viewStart.y/global.scale;
3354 int vx1 = (viewStart.x+viewWidth)/global.scale;
3355 int vy1 = (viewStart.y+viewHeight)/global.scale;
3356 if (exitY > vy1-16) {
3358 drawSpriteAt(hasMessage ? 'sCompassSmallLL' : 'sCompassLL', 0, 0, 224);
3359 } else if (exitX > vx1-16) {
3360 drawSpriteAt(hasMessage ? 'sCompassSmallLR' : 'sCompassLR', 0, 304, 224);
3362 drawSpriteAt(hasMessage ? 'sCompassSmallDown' : 'sCompassDown', 0, exitX-vx0, 224);
3364 } else if (exitX < vx0) {
3365 drawSpriteAt(hasMessage ? 'sCompassSmallLeft' : 'sCompassLeft', 0, 0, exitY-vy0);
3366 } else if (exitX > vx1-16) {
3367 drawSpriteAt(hasMessage ? 'sCompassSmallRight' : 'sCompassRight', 0, 304, exitY-vy0);
3369 break; // only the first exit
3374 final bool totalsNameCmpCB (ref GameStats::TotalItem a, ref GameStats::TotalItem b) {
3375 auto sa = string(a.objName);
3376 auto sb = string(b.objName);
3380 void renderTransitionInfo (float currFrameDelta) {
3383 GameStats.sortTotalsList(stats.kills, &totalsNameCmpCB);
3386 foreach (int idx, ref auto k; stats.kills) {
3387 string s = string(k);
3388 maxLen = max(maxLen, s.length);
3392 sprStore.loadFont('sFontSmall');
3393 GLVideo.color = 0xff_ff_00;
3394 foreach (int idx, ref auto k; stats.kills) {
3396 foreach (int xidx, ref auto d; stats.totalKills) {
3397 if (d.objName == k) { deaths = d.count; break; }
3399 //drawTextAt(16, 4+idx*8, va("%s: %d (%d)", string(k.objName).toUpperCase, k.count, deaths));
3400 drawTextAt(16, 4+idx*8, string(k).toUpperCase);
3401 drawTextAt(16+maxLen, 4+idx*8, va(": %d (%d)", k.count, deaths));
3407 void renderGhostTimer (float currFrameDelta) {
3408 if (ghostTimeLeft <= 0) return;
3409 //ghostTimeLeft /= 30; // frames -> seconds
3411 int hgt = viewHeight-64;
3412 if (hgt < 1) return;
3413 int rhgt = roundi(float(hgt)*(float(ghostTimeLeft)/30.0)/1000.0);
3414 //writeln("hgt=", hgt, "; rhgt=", rhgt, "; gtl=", ghostTimeLeft/30);
3416 auto oclr = GLVideo.color;
3417 GLVideo.color = 0xcf_ff_7f_00;
3418 GLVideo.fillRect(viewWidth-20, 32, 16, hgt-rhgt);
3419 GLVideo.color = 0x7f_ff_7f_00;
3420 GLVideo.fillRect(viewWidth-20, 32+(hgt-rhgt), 16, rhgt);
3421 GLVideo.color = oclr;
3426 void renderStarsHUD (float currFrameDelta) {
3427 bool scumSmallHud = global.config.scumSmallHud;
3429 //auto life = max(0, global.plife);
3430 int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3431 //int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3432 //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3437 sprStore.loadFont('sFontSmall');
3440 sprStore.loadFont('sFont');
3444 GLVideo.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3445 //draw_sprite(sHeart, -1, view_xview[0]+8, view_yview[0]+8);
3446 //draw_text(view_xview[0]+24, view_yview[0]+8, life);
3448 if (global.plife == 1) {
3449 drawSpriteAt('sHeartSmallBlink', global.heartBlink, 10, hhup-4);
3450 global.heartBlink += 0.1;
3451 if (global.heartBlink > 3) global.heartBlink = 0;
3453 drawSpriteAt('sHeartSmall', -1, 10, hhup-4);
3454 global.heartBlink = 0;
3457 if (global.plife == 1) {
3458 drawSpriteAt('sHeartBlink', global.heartBlink, 8, hhup);
3459 global.heartBlink += 0.1;
3460 if (global.heartBlink > 3) global.heartBlink = 0;
3462 drawSpriteAt('sHeart', -1, 8, hhup);
3463 global.heartBlink = 0;
3466 int life = clamp(global.plife, 0, 99);
3467 drawTextAt(16+8, hhup, va("%d", life));
3469 //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3470 drawSpriteAt('sShopkeeperIcon', -1, 64, hhup, scumSmallHud);
3471 drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", starsKills));
3473 if (starsRoomTimer1 > 0) {
3474 sprStore.loadFont('sFontSmall');
3475 GLVideo.color = 0xff_ff_00;
3476 int scale = global.scale;
3477 sprStore.renderMultilineTextCentered(viewWidth/2, 216*scale, va("SHOTGUN CHALLENGE BEGINS IN ~%d~", (starsRoomTimer1/30)+1), scale, 0xff_00_00);
3482 void renderSunHUD (float currFrameDelta) {
3483 bool scumSmallHud = global.config.scumSmallHud;
3485 //auto life = max(0, global.plife);
3486 int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3487 //int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3488 //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3493 sprStore.loadFont('sFontSmall');
3496 sprStore.loadFont('sFont');
3500 GLVideo.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3501 //draw_sprite(sHeart, -1, view_xview[0]+8, view_yview[0]+8);
3502 //draw_text(view_xview[0]+24, view_yview[0]+8, life);
3504 if (global.plife == 1) {
3505 drawSpriteAt('sHeartSmallBlink', global.heartBlink, 10, hhup-4);
3506 global.heartBlink += 0.1;
3507 if (global.heartBlink > 3) global.heartBlink = 0;
3509 drawSpriteAt('sHeartSmall', -1, 10, hhup-4);
3510 global.heartBlink = 0;
3513 if (global.plife == 1) {
3514 drawSpriteAt('sHeartBlink', global.heartBlink, 8, hhup);
3515 global.heartBlink += 0.1;
3516 if (global.heartBlink > 3) global.heartBlink = 0;
3518 drawSpriteAt('sHeart', -1, 8, hhup);
3519 global.heartBlink = 0;
3522 int life = clamp(global.plife, 0, 99);
3523 drawTextAt(16+8, hhup, va("%d", life));
3525 //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3526 drawSpriteAt('sDamselIcon', -1, 64, hhup, scumSmallHud);
3527 drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", sunScore));
3529 if (sunRoomTimer1 > 0) {
3530 sprStore.loadFont('sFontSmall');
3531 GLVideo.color = 0xff_ff_00;
3532 int scale = global.scale;
3533 sprStore.renderMultilineTextCentered(viewWidth/2, 216*scale, va("DAMSEL CHALLENGE BEGINS IN ~%d~", (sunRoomTimer1/30)+1), scale, 0xff_00_00);
3538 void renderMoonHUD (float currFrameDelta) {
3539 bool scumSmallHud = global.config.scumSmallHud;
3541 //auto life = max(0, global.plife);
3542 int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3543 //int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3544 //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3549 sprStore.loadFont('sFontSmall');
3552 sprStore.loadFont('sFont');
3556 GLVideo.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3558 //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3559 drawSpriteAt('sHoopsIcon', -1, 8, hhup, scumSmallHud);
3560 drawTextAt(8+16-(scumSmallHud ? 6 : 0), hhup, va("%d", moonScore));
3561 drawSpriteAt('sTimerIcon', -1, 64, hhup, scumSmallHud);
3562 drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", max(0, moonTimer)));
3564 if (moonRoomTimer1 > 0) {
3565 sprStore.loadFont('sFontSmall');
3566 GLVideo.color = 0xff_ff_00;
3567 int scale = global.scale;
3568 sprStore.renderMultilineTextCentered(viewWidth/2, 216*scale, va("ARCHERY CHALLENGE BEGINS IN ~%d~", (moonRoomTimer1/30)+1), scale, 0xff_00_00);
3573 void renderHUD (float currFrameDelta) {
3574 if (levelKind == LevelKind.Stars) { renderStarsHUD(currFrameDelta); return; }
3575 if (levelKind == LevelKind.Sun) { renderSunHUD(currFrameDelta); return; }
3576 if (levelKind == LevelKind.Moon) { renderMoonHUD(currFrameDelta); return; }
3578 if (!isHUDEnabled()) return;
3580 if (levelKind == LevelKind.Transition) { renderTransitionInfo(currFrameDelta); return; }
3588 bool scumSmallHud = global.config.scumSmallHud;
3589 if (!global.config.optSGAmmo && (!player || player.holdItem !isa ItemWeaponBow)) moneyX = ammoX;
3592 sprStore.loadFont('sFontSmall');
3595 sprStore.loadFont('sFont');
3598 //int alpha = 0x6f_00_00_00;
3599 int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3600 int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3602 //GLVideo.color = 0xff_ff_ff;
3603 GLVideo.color = 0xff_ff_ff|talpha;
3607 if (global.plife == 1) {
3608 drawSpriteAt('sHeartSmallBlink', global.heartBlink, lifeX+2, 4-hhup);
3609 global.heartBlink += 0.1;
3610 if (global.heartBlink > 3) global.heartBlink = 0;
3612 drawSpriteAt('sHeartSmall', -1, lifeX+2, 4-hhup);
3613 global.heartBlink = 0;
3616 if (global.plife == 1) {
3617 drawSpriteAt('sHeartBlink', global.heartBlink, lifeX, 8-hhup);
3618 global.heartBlink += 0.1;
3619 if (global.heartBlink > 3) global.heartBlink = 0;
3621 drawSpriteAt('sHeart', -1, lifeX, 8-hhup);
3622 global.heartBlink = 0;
3626 int life = clamp(global.plife, 0, 99);
3627 //if (!scumHud && life > 99) life = 99;
3628 drawTextAt(lifeX+16, 8-hhup, va("%d", life));
3631 if (global.hasStickyBombs && global.stickyBombsActive) {
3632 if (scumSmallHud) drawSpriteAt('sStickyBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sStickyBombIcon', -1, bombX, 8-hhup);
3634 if (scumSmallHud) drawSpriteAt('sBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sBombIcon', -1, bombX, 8-hhup);
3636 int n = global.bombs;
3637 if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3638 drawTextAt(bombX+16, 8-hhup, va("%d", n));
3641 if (scumSmallHud) drawSpriteAt('sRopeIconSmall', -1, ropeX+2, 4-hhup); else drawSpriteAt('sRopeIcon', -1, ropeX, 8-hhup);
3643 if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3644 drawTextAt(ropeX+16, 8-hhup, va("%d", n));
3647 if (global.config.optSGAmmo && (!player || player.holdItem !isa ItemWeaponBow)) {
3648 if (scumSmallHud) drawSpriteAt('sShellsIcon', -1, ammoX+6, 8-hhup); else drawSpriteAt('sShellsIcon', -1, ammoX+7, 12-hhup);
3650 if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3651 drawTextAt(ammoX+16, 8-hhup, va("%d", n));
3652 } else if (player && player.holdItem isa ItemWeaponBow) {
3653 if (scumSmallHud) drawSpriteAt('sArrowRight', -1, ammoX+6, 8-hhup); else drawSpriteAt('sArrowRight', -1, ammoX+7, 12-hhup);
3655 if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3656 drawTextAt(ammoX+16, 8-hhup, va("%d", n));
3660 if (scumSmallHud) drawSpriteAt('sDollarSignSmall', -1, moneyX+2, 3-hhup); else drawSpriteAt('sDollarSign', -1, moneyX, 8-hhup);
3661 drawTextAt(moneyX+16, 8-hhup, va("%d", stats.money-xmoney));
3664 GLVideo.color = 0xff_ff_ff|ialpha;
3666 int ity = (scumSmallHud ? 18-hhup : 24-hhup);
3669 if (global.hasUdjatEye) {
3670 if (global.udjatBlink) drawSpriteAt('sUdjatEyeIcon2', -1, n, ity); else drawSpriteAt('sUdjatEyeIcon', -1, n, ity);
3673 if (global.hasAnkh) { drawSpriteAt('sAnkhIcon', -1, n, ity); n += 20; }
3674 if (global.hasCrown) { drawSpriteAt('sCrownIcon', -1, n, ity); n += 20; }
3675 if (global.hasKapala) {
3676 if (global.bloodLevel == 0) drawSpriteAt('sKapalaIcon', 0, n, ity);
3677 else if (global.bloodLevel <= 2) drawSpriteAt('sKapalaIcon', 1, n, ity);
3678 else if (global.bloodLevel <= 4) drawSpriteAt('sKapalaIcon', 2, n, ity);
3679 else if (global.bloodLevel <= 6) drawSpriteAt('sKapalaIcon', 3, n, ity);
3680 else if (global.bloodLevel <= 8) drawSpriteAt('sKapalaIcon', 4, n, ity);
3683 if (global.hasSpectacles) { drawSpriteAt('sSpectaclesIcon', -1, n, ity); n += 20; }
3684 if (global.hasGloves) { drawSpriteAt('sGlovesIcon', -1, n, ity); n += 20; }
3685 if (global.hasMitt) { drawSpriteAt('sMittIcon', -1, n, ity); n += 20; }
3686 if (global.hasSpringShoes) { drawSpriteAt('sSpringShoesIcon', -1, n, ity); n += 20; }
3687 if (global.hasJordans) { drawSpriteAt('sJordansIcon', -1, n, ity); n += 20; }
3688 if (global.hasSpikeShoes) { drawSpriteAt('sSpikeShoesIcon', -1, n, ity); n += 20; }
3689 if (global.hasCape) { drawSpriteAt('sCapeIcon', -1, n, ity); n += 20; }
3690 if (global.hasJetpack) { drawSpriteAt('sJetpackIcon', -1, n, ity); n += 20; }
3691 if (global.hasStickyBombs) { drawSpriteAt('sPasteIcon', -1, n, ity); n += 20; }
3692 if (global.hasCompass) { drawSpriteAt('sCompassIcon', -1, n, ity); n += 20; }
3693 if (global.hasParachute) { drawSpriteAt('sParachuteIcon', -1, n, ity); n += 20; }
3695 if (player.holdItem && player.holdItem.objName == 'Bow' && global.arrows > 0) {
3698 while (m <= global.arrows && m <= 20 && malpha > 0) {
3699 GLVideo.color = trunci(malpha*255)<<24|0xff_ff_ff;
3700 drawSpriteAt('sArrowIcon', -1, n, ity);
3702 if (m > 10) malpha -= 0.1; //and global.arrows > 20 malpha -= 0.1
3708 sprStore.loadFont('sFontSmall');
3709 GLVideo.color = 0xff_ff_00|talpha;
3710 if (scumSmallHud) drawTextAt(moneyX+8, 8+8-hhup, va("+%d", xmoney));
3711 else drawTextAt(moneyX, 8+16-hhup, va("+%d", xmoney));
3714 GLVideo.color = 0xff_ff_ff;
3715 if (global.config.ghostShowTime) renderGhostTimer(currFrameDelta);
3719 // ////////////////////////////////////////////////////////////////////////// //
3720 // x0 and y0 are non-scaled (and will be scaled)
3721 final void drawTextAtS3 (int x0, int y0, string text, optional int hiColor1, optional int hiColor2) {
3725 sprStore.renderTextWithHighlight(x0, y0, text, 3, hiColor1!optional, hiColor2!optional);
3729 final void drawTextAtS3Centered (int y0, string text, optional int hiColor1, optional int hiColor2) {
3731 int x0 = (viewWidth-sprStore.getTextWidth(text, 3, specified_hiColor1, specified_hiColor2))/2;
3732 sprStore.renderTextWithHighlight(x0, y0*3, text, 3, hiColor1!optional, hiColor2!optional);
3736 void renderHelpOverlay () {
3738 GLVideo.fillRect(0, 0, viewWidth, viewHeight);
3741 //int txoff = 0; // text x pos offset (for multi-color lines)
3743 if (gameHelpScreen) {
3744 sprStore.loadFont('sFontSmall');
3745 GLVideo.color = 0xff_ff_ff;
3746 drawTextAtS3Centered(ty, va("HELP (PAGE ~%d~ OF ~%d~)", gameHelpScreen, MaxGameHelpScreen), 0xff_ff_00);
3750 if (gameHelpScreen == 1) {
3751 sprStore.loadFont('sFontSmall');
3752 GLVideo.color = 0xff_ff_00; drawTextAtS3(tx, ty, "INVENTORY BASICS"); ty += 16;
3753 GLVideo.color = 0xff_ff_ff;
3754 drawTextAtS3(tx, ty, global.expandString("Press $SWITCH to cycle through items."), 0x00_ff_00);
3757 GLVideo.color = 0xff_ff_ff;
3758 drawSpriteAtS3('sHelpSprite1', -1, 64, 96);
3759 } else if (gameHelpScreen == 2) {
3760 sprStore.loadFont('sFontSmall');
3761 GLVideo.color = 0xff_ff_00;
3762 drawTextAtS3(tx, ty, "SELLING TO SHOPKEEPERS"); ty += 16;
3763 GLVideo.color = 0xff_ff_ff;
3764 drawTextAtS3(tx, ty, global.expandString("Press $PAY to offer your currently"), 0x00_ff_00); ty += 8;
3765 drawTextAtS3(tx, ty, "held item to the shopkeeper."); ty += 16;
3766 drawTextAtS3(tx, ty, "If the shopkeeper is interested, "); ty += 8;
3767 //drawTextAtS3(tx, ty, global.expandString("press $PAY again to complete the sale."), 0x00_ff_00); ty += 72;
3768 drawTextAtS3(tx, ty, global.expandString("press $PAY again to complete"), 0x00_ff_00);
3769 drawTextAtS3(tx, ty+8, "the sale.");
3771 drawSpriteAtS3('sHelpSell', -1, 112, 100);
3772 drawTextAtS3(tx, ty, "Purchasing goods from the shopkeeper"); ty += 8;
3773 drawTextAtS3(tx, ty, "will increase the funds he has"); ty += 8;
3774 drawTextAtS3(tx, ty, "available to buy your unwanted stuff."); ty += 8;
3777 sprStore.loadFont('sFont');
3778 GLVideo.color = 0xff_ff_ff;
3779 drawTextAtS3(136, 8, "MAP");
3781 if (lg.mapSprite && (isNormalLevel() || isTransitionRoom())) {
3782 GLVideo.color = 0xff_ff_00;
3783 drawTextAtS3Centered(24, lg.mapTitle);
3785 auto spf = sprStore[lg.mapSprite].frames[0];
3786 int mapX = 160-spf.width/2;
3787 int mapY = 120-spf.height/2;
3788 //mapTitleX = 160-string_length(global.mapTitle)*8/2;
3790 GLVideo.color = 0xff_ff_ff;
3791 drawSpriteAtS3(lg.mapSprite, -1, mapX, mapY);
3793 if (lg.mapSprite != 'sMapDefault') {
3794 int mx = -1, my = -1;
3796 // set position of player icon
3797 switch (global.currLevel) {
3798 case 1: mx = 81; my = 22; break;
3799 case 2: mx = 113; my = 63; break;
3800 case 3: mx = 197; my = 86; break;
3801 case 4: mx = 133; my = 109; break;
3802 case 5: mx = 181; my = 22; break;
3803 case 6: mx = 126; my = 64; break;
3804 case 7: mx = 158; my = 112; break;
3805 case 8: mx = 66; my = 80; break;
3806 case 9: mx = 30; my = 26; break;
3807 case 10: mx = 88; my = 54; break;
3808 case 11: mx = 148; my = 81; break;
3809 case 12: mx = 210; my = 205; break;
3810 case 13: mx = 66; my = 17; break;
3811 case 14: mx = 146; my = 17; break;
3812 case 15: mx = 82; my = 77; break;
3813 case 16: mx = 178; my = 81; break;
3817 int plrx = mx+player.ix/16;
3818 int plry = my+player.iy/16;
3819 if (isTransitionRoom()) { plrx = mx+20; plry = my+16; }
3820 name plrspr = 'sMapSpelunker';
3821 if (global.isDamsel) plrspr = 'sMapDamsel';
3822 else if (global.isTunnelMan) plrspr = 'sMapTunnel';
3823 auto ss = sprStore[plrspr];
3824 drawSpriteAtS3(plrspr, (pausedTime/2)%ss.frames.length, mapX+plrx, mapY+plry);
3826 if (global.hasCompass && allExits.length) {
3827 drawSpriteAtS3('sMapRedDot', -1, mapX+mx+allExits[0].ix/16, mapY+my+allExits[0].iy/16);
3834 sprStore.loadFont('sFontSmall');
3835 GLVideo.color = 0xff_ff_00;
3836 drawTextAtS3Centered(232, "PRESS ~SPACE~/~LEFT~/~RIGHT~ TO CHANGE PAGE", 0x00_ff_00);
3838 GLVideo.color = 0xff_ff_ff;
3842 void renderPauseOverlay () {
3843 //drawTextAt(256, 432, "PAUSED", scale);
3845 if (gameShowHelp) { renderHelpOverlay(); return; }
3847 GLVideo.color = 0xff_ff_00;
3848 //int hiColor = 0x00_ff_00;
3851 if (isTutorialRoom()) {
3852 sprStore.loadFont('sFont');
3853 drawTextAtS3Centered(n-24, "TUTORIAL CAVE");
3854 } else if (isNormalLevel()) {
3855 sprStore.loadFont('sFont');
3857 drawTextAtS3Centered(n-32, va("LEVEL ~%d~", global.currLevel), 0x00_ff_00);
3859 sprStore.loadFont('sFontSmall');
3861 int depth = roundi((174.8*(global.currLevel-1)+(player.iy+8)*0.34)*(global.config.scumMetric ? 0.3048 : 1.0)*10);
3862 string depthStr = va("DEPTH: ~%d.%d~ %s", depth/10, depth%10, (global.config.scumMetric ? "METRES" : "FEET"));
3863 drawTextAtS3Centered(n-16, depthStr, 0x00_ff_00);
3866 drawTextAtS3Centered(n, va("MONEY: ~%d~", stats.money), 0x00_ff_00);
3867 drawTextAtS3Centered(n+16, va("KILLS: ~%d~", stats.countKills), 0x00_ff_00);
3868 drawTextAtS3Centered(n+32, va("SAVES: ~%d~", stats.damselsSaved), 0x00_ff_00);
3869 drawTextAtS3Centered(n+48, va("TIME: ~%s~", time2str(time/30)), 0x00_ff_00);
3870 drawTextAtS3Centered(n+64, va("LEVEL TIME: ~%s~", time2str((time-levelStartTime)/30)), 0x00_ff_00);
3873 sprStore.loadFont('sFontSmall');
3874 GLVideo.color = 0xff_ff_ff;
3875 drawTextAtS3Centered(240-2-8, "~ESC~-RETURN ~F10~-QUIT ~CTRL+DEL~-SUICIDE", 0xff_7f_00);
3876 drawTextAtS3Centered(2, "~O~PTIONS REDEFINE ~K~EYS ~S~TATISTICS", 0xff_7f_00);
3880 // ////////////////////////////////////////////////////////////////////////// //
3881 transient int drawLoot;
3882 transient int drawPosX, drawPosY;
3884 void resetTransitionOverlay () {
3891 // current game, uncollapsed
3892 struct LevelStatInfo {
3894 // for transition screen
3901 void thinkFrameTransition () {
3902 if (drawLoot == 0) {
3903 if (drawPosX > 272) {
3906 if (drawPosY > 83+4) drawPosY = 83;
3908 } else if (drawPosX > 232) {
3911 if (drawPosY > 91+4) drawPosY = 91;
3916 void renderTransitionOverlay () {
3917 sprStore.loadFont('sFontSmall');
3918 GLVideo.color = 0xff_ff_00;
3919 //else if (global.currLevel-1 < 1) draw_text(32, 48, "TUTORIAL CAVE COMPLETED!");
3920 //else draw_text(32, 48, "LEVEL "+string(global.currLevel-1)+" COMPLETED!");
3921 if (global.currLevel == 0) {
3922 drawTextAt(32, 48, "TUTORIAL CAVE COMPLETED!");
3924 drawTextAt(32, 48, va("LEVEL ~%d~ COMPLETED!", global.currLevel), hiColor1:0x00_ff_ff);
3926 GLVideo.color = 0xff_ff_ff;
3927 drawTextAt(32, 64, va("TIME = ~%s~", time2str((levelEndTime-levelStartTime)/30)), hiColor1:0xff_ff_00);
3929 if (/*stats.collected.length == 0*/stats.money <= levelMoneyStart) {
3930 drawTextAt(32, 80, "LOOT = ~NONE~", hiColor1:0xff_00_00);
3932 drawTextAt(32, 80, va("LOOT = ~%d~", stats.money-levelMoneyStart), hiColor1:0xff_ff_00);
3935 if (stats.kills.length == 0) {
3936 drawTextAt(32, 96, "KILLS = ~NONE~", hiColor1:0x00_ff_00);
3938 drawTextAt(32, 96, va("KILLS = ~%d~", stats.kills.length), hiColor1:0xff_ff_00);
3941 drawTextAt(32, 112, va("MONEY = ~%d~", stats.money), hiColor1:0xff_ff_00);
3945 // ////////////////////////////////////////////////////////////////////////// //
3946 private transient array!MapEntity renderVisibleCids;
3947 private transient array!MapEntity renderVisibleLights;
3948 private transient array!MapTile renderFrontTiles; // normal, with fg
3950 final int renderSortByDepth (MapEntity oa, MapEntity ob) {
3951 //auto da = oa.depth, db = ob.depth;
3952 //if (da == db) return (oa.objId < ob.objId);
3954 auto d = oa.depth-ob.depth;
3955 return (d ? d : oa.objId-ob.objId);
3959 const int RenderEdgePixNormal = 64;
3960 const int RenderEdgePixLight = 256;
3962 #ifndef EXPERIMENTAL_RENDER_CACHE
3963 enum skipListCreation = false;
3966 void renderWithOfs (int xofs, int yofs, float currFrameDelta) {
3967 int scale = global.scale;
3969 // don't touch framebuffer alpha
3970 GLVideo.colorMask = GLVideo::CMask.Colors;
3971 GLVideo.color = 0xff_ff_ff;
3974 GLVideo::ScissorRect scsave;
3975 bool doRestoreGL = false;
3977 if (viewOffsetX > 0 || viewOffsetY > 0) {
3979 GLVideo.getScissor(scsave);
3980 GLVideo.scissorCombine(viewOffsetX, viewOffsetY, viewWidth, viewHeight);
3981 GLVideo.glPushMatrix();
3982 GLVideo.glTranslate(viewOffsetX, viewOffsetY);
3983 //GLVideo.glTranslate(-550, 0);
3984 //GLVideo.glScale(1, 1);
3989 bool isDarkLevel = global.darkLevel;
3992 switch (global.config.scumPlayerLit) {
3993 case 0: player.lightRadius = 0; break; // never
3994 case 1: // only in "scumDarkness"
3995 player.lightRadius = (global.config.scumDarkness >= 2 ? 96 : 32);
3998 player.lightRadius = 96;
4003 // render cave background
4006 int bgw = levBGImg.tex.width*scale;
4007 int bgh = levBGImg.tex.height*scale;
4008 int bgTW = (tilesWidth*tsz+bgw-1)/bgw;
4009 int bgTH = (tilesHeight*tsz+bgh-1)/bgh;
4010 int bgX0 = max(0, xofs/bgw);
4011 int bgY0 = max(0, yofs/bgh);
4012 int bgX1 = min(bgTW, (xofs+viewWidth+bgw-1)/bgw);
4013 int bgY1 = min(bgTH, (yofs+viewHeight+bgh-1)/bgh);
4014 foreach (int ty; bgY0..bgY1) {
4015 foreach (int tx; bgX0..bgX1) {
4016 int x0 = tx*bgw-xofs;
4017 int y0 = ty*bgh-yofs;
4018 levBGImg.blitAt(x0, y0, scale);
4023 int RenderEdgePix = (global.darkLevel ? RenderEdgePixLight : RenderEdgePixNormal);
4025 // render background tiles
4026 for (MapBackTile bt = backtiles; bt; bt = bt.next) {
4027 bt.drawWithOfs(xofs, yofs, scale, currFrameDelta);
4030 // collect visible special tiles
4031 #ifdef EXPERIMENTAL_RENDER_CACHE
4032 bool skipListCreation = (lastRenderTime == time && renderVisibleCids.length); //FIXME
4035 if (!skipListCreation) {
4036 renderVisibleCids.clear();
4037 renderVisibleLights.clear();
4038 renderFrontTiles.clear();
4040 int endVX = xofs+viewWidth;
4041 int endVY = yofs+viewHeight;
4045 if (player && player.visible) renderVisibleCids[$] = player; // include player in render list
4047 //FIXME: drop lit objects which cannot affect visible area
4049 // collect visible objects
4050 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)) {
4051 if (!o.visible) continue;
4052 auto tile = MapTile(o);
4054 if (isDarkLevel && (o.lightRadius > 4 || tile.litWholeTile)) renderVisibleLights[$] = o;
4055 if (tile.invisible) continue;
4056 if (tile.bgfront /*|| tile.spriteLeftDeco || tile.spriteRightDeco*/) renderFrontTiles[$] = tile;
4057 if (tile.bgback) tile.drawWithOfsBack(xofs, yofs, scale, currFrameDelta);
4059 if (isDarkLevel && o.lightRadius > 4) renderVisibleLights[$] = o;
4061 // check if the object is really visible -- this will speed up later sorting
4062 int fx0, fy0, fx1, fy1;
4063 /*auto*/SpriteFrame spf = o.getSpriteFrame(default, out fx0, out fy0, out fx1, out fy1);
4064 if (!spf) continue; // no sprite -- nothing to draw (no, really)
4065 int ix = o.ix, iy = o.iy;
4066 int x0 = (ix+fx0)*scale, y0 = (iy+fy0)*scale;
4067 int x1 = (ix+fx1)*scale, y1 = (iy+fy1)*scale;
4068 if (x1 <= xofs || y1 <= yofs || x0 >= endVX || y0 >= endVY) {
4072 renderVisibleCids[$] = o;
4075 foreach (MapEntity o; objGrid.allObjects()) {
4076 if (!o.visible) continue;
4077 auto tile = MapTile(o);
4079 if (isDarkLevel && (o.lightRadius > 4 || tile.litWholeTile)) renderVisibleLights[$] = o;
4080 if (tile.invisible) continue;
4081 if (tile.bgfront /*|| tile.spriteLeftDeco || tile.spriteRightDeco*/) renderFrontTiles[$] = tile;
4082 if (tile.bgback) tile.drawWithOfsBack(xofs, yofs, scale, currFrameDelta);
4084 if (isDarkLevel && o.lightRadius > 4) renderVisibleLights[$] = o;
4086 renderVisibleCids[$] = o;
4089 //writeln("::: ", cnt, " invisible objects dropped");
4091 renderVisibleCids.sort(&renderSortByDepth);
4092 lastRenderTime = time;
4095 auto depth4Start = 0;
4096 foreach (auto xidx, MapEntity o; renderVisibleCids) {
4103 bool playerPowerupRendered = false;
4105 // render objects (part one: depth > 3)
4106 foreach (auto idx; depth4Start..renderVisibleCids.length; reverse) {
4107 MapEntity o = renderVisibleCids[idx];
4108 // 1000 is an ordinary tile
4109 if (!playerPowerupRendered && o.depth <= 1200) {
4110 playerPowerupRendered = true;
4111 // so ducking player will have it's cape correctly rendered
4112 if (player.visible) player.drawPrePrePowerupWithOfs(xofs, yofs, scale, currFrameDelta);
4114 //if (idx && renderSortByDepth(o, renderVisibleCids[idx-1])) writeln("VIOLATION!");
4115 o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
4118 // render object (part two: front tile parts, depth 3.5)
4119 foreach (MapTile tile; renderFrontTiles) {
4120 tile.drawWithOfsFront(xofs, yofs, scale, currFrameDelta);
4123 // render objects (part three: depth <= 3)
4124 foreach (auto idx; 0..depth4Start; reverse) {
4125 MapEntity o = renderVisibleCids[idx];
4126 o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
4127 //done above;if (isDarkLevel && (o.lightRadius > 4 || (o isa MapTile && MapTile(o).litWholeTile))) renderVisibleLights[$] = o;
4130 // render player post-effects, so cape or jetpack will be correctly rendered on level exit, for example
4131 player.lastDrawWithOfs(xofs, yofs, scale, currFrameDelta);
4135 auto ltex = bgtileStore.lightTexture('ltx512', 512);
4137 // set screen alpha to min
4138 GLVideo.colorMask = GLVideo::CMask.Alpha;
4139 GLVideo.blendMode = GLVideo::BlendMode.None;
4140 GLVideo.color = 0xff_ff_ff_ff;
4141 GLVideo.fillRect(0, 0, GLVideo.screenWidth, GLVideo.screenHeight);
4142 //GLVideo.colorMask = GLVideo::CMask.All;
4145 // also, stencil 'em, so we can filter dark areas
4146 GLVideo.textureFiltering = true;
4147 GLVideo.stencil = true;
4148 GLVideo.stencilFunc(GLVideo::StencilFunc.Always, 1);
4149 GLVideo.stencilOp(GLVideo::StencilOp.Keep, GLVideo::StencilOp.Replace);
4150 GLVideo.alphaTestFunc = GLVideo::AlphaFunc.Greater;
4151 GLVideo.alphaTestVal = 0.03+0.011*global.config.darknessDarkness;
4152 GLVideo.color = 0xff_ff_ff;
4153 GLVideo.blendFunc = GLVideo::BlendFunc.Max;
4154 GLVideo.blendMode = GLVideo::BlendMode.Blend; // anything except `Normal`
4155 GLVideo.colorMask = GLVideo::CMask.Alpha;
4157 foreach (MapEntity e; renderVisibleLights) {
4159 e.getInterpCoords(currFrameDelta, scale, out xi, out yi);
4160 auto tile = MapTile(e);
4161 if (tile && tile.litWholeTile) {
4162 //GLVideo.color = 0xff_ff_ff;
4163 GLVideo.fillRect(xi-xofs, yi-yofs, e.width*scale, e.height*scale);
4165 int lrad = e.lightRadius;
4166 if (lrad < 4) continue; // just in case
4168 //if (loserGPU && lrad%12 != 0) lrad = (lrad/12)*12;
4169 float lightscale = float(lrad*scale)/float(ltex.tex.width);
4170 #ifdef OLD_LIGHT_OFFSETS
4171 int fx0, fy0, fx1, fy1;
4173 auto spf = e.getSpriteFrame(out doMirror, out fx0, out fy0, out fx1, out fy1);
4175 xi += (fx1-fx0)*scale/2;
4176 yi += (fy1-fy0)*scale/2;
4180 e.getLightOffset(out lxofs, out lyofs);
4185 lrad = lrad*scale/2;
4188 ltex.tex.blitAt(xi, yi, lightscale);
4190 GLVideo.textureFiltering = false;
4193 // modify only lit parts
4194 GLVideo.stencilFunc(GLVideo::StencilFunc.Equal, 1);
4195 GLVideo.stencilOp(GLVideo::StencilOp.Keep, GLVideo::StencilOp.Keep);
4196 // multiply framebuffer colors by framebuffer alpha
4197 GLVideo.color = 0xff_ff_ff; // it doesn't matter
4198 GLVideo.blendFunc = GLVideo::BlendFunc.Add;
4199 GLVideo.blendMode = GLVideo::BlendMode.DstMulDstAlpha;
4200 GLVideo.colorMask = GLVideo::CMask.Colors;
4201 GLVideo.fillRect(0, 0, GLVideo.screenWidth, GLVideo.screenHeight);
4204 // filter unlit parts
4205 GLVideo.stencilFunc(GLVideo::StencilFunc.NotEqual, 1);
4206 GLVideo.stencilOp(GLVideo::StencilOp.Keep, GLVideo::StencilOp.Keep);
4207 GLVideo.blendFunc = GLVideo::BlendFunc.Add;
4208 GLVideo.blendMode = GLVideo::BlendMode.Filter;
4209 GLVideo.colorMask = GLVideo::CMask.Colors;
4210 GLVideo.color = 0x00_00_18+0x00_00_10*global.config.darknessDarkness;
4211 //GLVideo.color = 0x00_00_18;
4212 //GLVideo.color = 0x00_00_38;
4213 GLVideo.fillRect(0, 0, GLVideo.screenWidth, GLVideo.screenHeight);
4216 GLVideo.blendFunc = GLVideo::BlendFunc.Add;
4217 GLVideo.blendMode = GLVideo::BlendMode.Normal;
4218 GLVideo.colorMask = GLVideo::CMask.All;
4219 GLVideo.alphaTestFunc = GLVideo::AlphaFunc.Always;
4220 GLVideo.stencil = false;
4223 // clear visible objects list (nope)
4224 //renderVisibleCids.clear();
4225 //renderVisibleLights.clear();
4228 if (global.config.drawHUD) renderHUD(currFrameDelta);
4229 renderCompass(currFrameDelta);
4231 float osdTimeLeft, osdTimeStart;
4232 string msg = osdGetMessage(out osdTimeLeft, out osdTimeStart);
4234 auto ct = GetTickCount();
4236 sprStore.loadFont('sFontSmall');
4237 auto msgHeight = sprStore.getMultilineTextHeight(msg, msgScale);
4238 int x = viewWidth/2;
4239 int y = viewHeight-64-msgHeight;
4240 auto oldColor = GLVideo.color;
4241 GLVideo.color = 0xff_ff_00;
4242 if (osdTimeLeft < 0.5) {
4243 int alpha = 255-clamp(int(510*osdTimeLeft), 0, 255);
4244 GLVideo.color = GLVideo.color|(alpha<<24);
4245 } else if (ct-osdTimeStart < 0.5) {
4246 osdTimeStart = ct-osdTimeStart;
4247 int alpha = 255-clamp(int(510*osdTimeStart), 0, 255);
4248 GLVideo.color = GLVideo.color|(alpha<<24);
4250 sprStore.renderMultilineTextCentered(x, y, msg, msgScale, 0x00_ff_00, 0xff_ff_ff);
4251 GLVideo.color = oldColor;
4254 int hiColor1, hiColor2;
4255 msg = osdGetTalkMessage(out hiColor1, out hiColor2);
4258 sprStore.loadFont('sFontSmall');
4259 auto msgWidth = sprStore.getMultilineTextWidth(msg, processHighlights1:true, processHighlights2:true);
4260 auto msgHeight = sprStore.getMultilineTextHeight(msg);
4261 auto msgWidthOrig = msgWidth*msgScale;
4262 auto msgHeightOrig = msgHeight*msgScale;
4263 if (msgWidth%16 != 0) msgWidth = (msgWidth|0x0f)+1;
4264 if (msgHeight%16 != 0) msgHeight = (msgHeight|0x0f)+1;
4265 msgWidth *= msgScale;
4266 msgHeight *= msgScale;
4267 int x = (viewWidth-msgWidth)/2;
4268 int y = 32*msgScale;
4269 auto oldColor = GLVideo.color;
4270 // draw text frame and text background
4272 GLVideo.fillRect(x, y, msgWidth, msgHeight);
4273 GLVideo.color = 0xff_ff_ff;
4274 for (int fdx = 0; fdx < msgWidth; fdx += 16*msgScale) {
4275 auto spf = sprStore['sMenuTop'].frames[0];
4276 spf.blitAt(x+fdx, y-16*msgScale, msgScale);
4277 spf = sprStore['sMenuBottom'].frames[0];
4278 spf.blitAt(x+fdx, y+msgHeight, msgScale);
4280 for (int fdy = 0; fdy < msgHeight; fdy += 16*msgScale) {
4281 auto spf = sprStore['sMenuLeft'].frames[0];
4282 spf.blitAt(x-16*msgScale, y+fdy, msgScale);
4283 spf = sprStore['sMenuRight'].frames[0];
4284 spf.blitAt(x+msgWidth, y+fdy, msgScale);
4287 auto spf = sprStore['sMenuUL'].frames[0];
4288 spf.blitAt(x-16*msgScale, y-16*msgScale, msgScale);
4289 spf = sprStore['sMenuUR'].frames[0];
4290 spf.blitAt(x+msgWidth, y-16*msgScale, msgScale);
4291 spf = sprStore['sMenuLL'].frames[0];
4292 spf.blitAt(x-16*msgScale, y+msgHeight, msgScale);
4293 spf = sprStore['sMenuLR'].frames[0];
4294 spf.blitAt(x+msgWidth, y+msgHeight, msgScale);
4296 GLVideo.color = 0xff_ff_00;
4297 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));
4298 GLVideo.color = oldColor;
4301 if (inWinCutscene) renderWinCutsceneOverlay();
4302 if (inIntroCutscene) renderTitleCutsceneOverlay();
4303 if (isTransitionRoom()) renderTransitionOverlay();
4307 GLVideo.setScissor(scsave);
4308 GLVideo.glPopMatrix();
4312 GLVideo.color = 0xff_ff_ff;
4316 // ////////////////////////////////////////////////////////////////////////// //
4317 final class!MapObject findGameObjectClassByName (name aname) {
4318 if (!aname) return none; // just in case
4319 auto co = FindClassByGameObjName(aname);
4321 writeln("***UNKNOWN GAME OBJECT: '", aname, "'");
4324 co = GetClassReplacement(co);
4325 if (!co) FatalError("findGameObjectClassByName: WTF?!");
4326 if (!class!MapObject(co)) FatalError("findGameTileClassByName: object '%n' is not a map object, it is a `%n`", aname, GetClassName(co));
4327 return class!MapObject(co);
4331 final class!MapTile findGameTileClassByName (name aname) {
4332 if (!aname) return none; // just in case
4333 auto co = FindClassByGameObjName(aname);
4334 if (!co) return MapTile; // unknown names will be routed directly to tile object
4335 co = GetClassReplacement(co);
4336 if (!co) FatalError("findGameTileClassByName: WTF?!");
4337 if (!class!MapTile(co)) FatalError("findGameTileClassByName: object '%n' is not a tile, it is a `%n`", aname, GetClassName(co));
4338 return class!MapTile(co);
4342 final MapObject findAnyObjectOfType (name aname) {
4343 if (!aname) return none;
4344 auto cls = FindClassByGameObjName(aname);
4345 if (!cls) return none;
4346 foreach (MapObject obj; objGrid.allObjects(MapObject)) {
4347 if (obj.spectral) continue;
4348 if (obj isa cls) return obj;
4354 // ////////////////////////////////////////////////////////////////////////// //
4355 final bool isRopePlacedAt (int x, int y) {
4357 foreach (ref auto v; covered) v = false;
4358 foreach (MapTile t; objGrid.inRectPix(x, y-8, 1, 17, precise:false, castClass:MapTileRope)) {
4359 //if (!cbIsRopeTile(t)) continue;
4360 if (t.ix != x) continue;
4361 if (t.iy == y) return true;
4362 foreach (int ty; t.iy..t.iy+8) {
4364 if (d >= 0 && d < covered.length) covered[d] = true;
4367 // check if the whole rope height is completely covered with ropes
4368 foreach (auto v; covered) if (!v) return false;
4373 final MapTile CreateMapTile (int xpos, int ypos, name aname) {
4374 if (!aname) FatalError("cannot create typeless tile");
4375 auto tclass = findGameTileClassByName(aname);
4376 if (!tclass) return none;
4377 MapTile tile = SpawnObject(tclass);
4378 tile.global = global;
4380 tile.objName = aname;
4381 tile.objType = aname; // just in case
4384 tile.objId = ++lastUsedObjectId;
4385 if (!tile.initialize()) FatalError("cannot create tile '%n'", aname);
4390 final bool PutSpawnedMapTile (int x, int y, MapTile tile) {
4391 if (!tile || !tile.isInstanceAlive) return false;
4393 //if (putToGrid) tile.active = true;
4394 bool putToGrid = (tile.moveable || tile.toSpecialGrid || tile.width != 16 || tile.height != 16 || x%16 != 0 || y%16 != 0);
4396 //writeln("putting tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (putToGrid ? "special " : ""), "grid, pos=(", x/16.0, ",", y/16.0, ")");
4399 int mapx = x/16, mapy = y/16;
4400 if (mapx < 0 || mapx >= tilesWidth || mapy < 0 || mapy >= tilesHeight) return false;
4403 // if we already have rope tile there, there is no reason to add another one
4404 if (tile isa MapTileRope) {
4405 if (isRopePlacedAt(x, y)) return false;
4408 // activate special or animated tile
4409 tile.active = tile.active || tile.moveable || tile.toSpecialGrid;
4410 // animated tiles must be active
4412 auto spr = tile.getSprite();
4413 if (spr && spr.frames.length > 1) {
4414 writeln("activated animated tile '", tile.objName, "'");
4422 //if (tile isa TitleTileCopy) writeln("*** PUTTING COPYRIGHT TILE");
4423 //tile.toSpecialGrid = true;
4424 if (!tile.dontReplaceOthers && x%16 == 0 && y%16 == 0) {
4425 auto t = getTileAtGridAny(x/16, y/16);
4426 if (t && !t.immuneToReplacement) {
4427 writeln("REPLACING tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
4428 writeln(" NEW tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (tile.toSpecialGrid ? "special " : ""), "grid, pos=(", tile.fltx/16.0, ",", tile.flty/16.0, ")");
4434 //writeln("SIZE: ", tilesWidth, "x", tilesHeight);
4435 setTileAtGrid(x/16, y/16, tile);
4437 auto t = getTileAtGridAny(x/16, y/16);
4439 writeln("putting tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (putToGrid ? "special " : ""), "grid, pos=(", x/16.0, ",", y/16.0, ")");
4440 checkTilesInRect(x/16, y/16, 16, 16, delegate bool (MapTile tile) {
4441 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, ")");
4444 FatalError("FUUUUUU");
4449 if (tile.enter) registerEnter(tile);
4450 if (tile.exit) registerExit(tile);
4452 // make tile under exit invulnerable
4453 if (checkTilesInRect(tile.ix, tile.iy-16, 16, 16, delegate bool (MapTile t) { return t.exit; })) {
4454 tile.invincible = true;
4461 // won't call `onDestroy()`
4462 final void RemoveMapTileFromGrid (int tileX, int tileY, optional string reason) {
4463 if (tileX >= 0 && tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
4464 auto t = getTileAtGridAny(tileX, tileY);
4466 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, ")");
4474 final MapTile MakeMapTile (int mapx, int mapy, name aname) {
4475 //writeln("tile at (", mapx, ",", mapy, "): ", aname);
4476 //if (aname == 'oLush') { MapObject fail; fail.initialize(); }
4477 //if (mapy >= tilesHeight) { writeln("OOM: ", aname, "; ty=", mapy, "; hgt=", tilesHeight); }
4478 if (mapx < 0 || mapx >= tilesWidth || mapy < 0 || mapy >= tilesHeight) return none;
4480 // if we already have rope tile there, there is no reason to add another one
4481 if (aname == 'oRope') {
4482 if (isRopePlacedAt(mapx*16, mapy*16)) return none;
4485 auto tile = CreateMapTile(mapx*16, mapy*16, aname);
4486 if (!tile) return none;
4487 if (!PutSpawnedMapTile(mapx*16, mapy*16, tile)) {
4496 final MapTile MakeMapTileAtPix (int xpix, int ypix, name aname) {
4497 // if we already have rope tile there, there is no reason to add another one
4498 if (aname == 'oRope') {
4499 if (isRopePlacedAt(xpix, ypix)) return none;
4502 auto tile = CreateMapTile(xpix, ypix, aname);
4503 if (!tile) return none;
4504 if (!PutSpawnedMapTile(xpix, ypix, tile)) {
4513 final MapTile MakeMapRopeTileAt (int x0, int y0) {
4514 // if we already have rope tile there, there is no reason to add another one
4515 if (isRopePlacedAt(x0, y0)) return none;
4517 auto tile = CreateMapTile(x0, y0, 'oRope');
4518 if (!PutSpawnedMapTile(x0, y0, tile)) {
4527 // ////////////////////////////////////////////////////////////////////////// //
4528 final MapBackTile CreateBgTile (name sprName, optional int atx0, optional int aty0, optional int aw, optional int ah) {
4529 BackTileImage img = bgtileStore[sprName];
4530 auto res = SpawnObject(MapBackTile);
4531 res.global = global;
4534 res.bgtName = sprName;
4535 if (specified_atx0) res.tx0 = atx0;
4536 if (specified_aty0) res.ty0 = aty0;
4537 if (specified_aw) res.w = aw;
4538 if (specified_ah) res.h = ah;
4539 if (!res.initialize()) FatalError("cannot create background tile with image '%n'", sprName);
4544 // ////////////////////////////////////////////////////////////////////////// //
4546 background The background asset from which the new tile will be extracted.
4547 left The x coordinate of the left of the new tile, relative to the background asset's top left corner.
4548 top The y coordinate of the top of the new tile, relative to the background assets top left corner.
4549 width The width of the tile.
4550 height The height of the tile.
4551 x The x position in the room to place the tile.
4552 y The y position in the room to place the tile.
4553 depth The depth at which to place the tile.
4555 final void MakeMapBackTile (name bgname, int left, int top, int width, int height, int x, int y, int depth) {
4556 if (width < 1 || height < 1 || !bgname) return;
4557 auto bgt = bgtileStore[bgname];
4558 if (!bgt) FatalError("cannot load background '%n'", bgname);
4559 MapBackTile bt = SpawnObject(MapBackTile);
4562 bt.objName = bgname;
4564 bt.bgtName = bgname;
4572 // find a place for it
4577 // back tiles with the highest depth should come first
4578 MapBackTile ct = backtiles, cprev = none;
4579 while (ct && depth <= ct.depth) { cprev = ct; ct = ct.next; }
4582 bt.next = cprev.next;
4585 bt.next = backtiles;
4591 // ////////////////////////////////////////////////////////////////////////// //
4592 final MapObject SpawnMapObjectWithClass (class!MapObject oclass) {
4593 if (!oclass) return none;
4595 MapObject obj = SpawnObject(oclass);
4596 if (!obj) FatalError("cannot create map object '%n'", GetClassName(oclass));
4598 //if (aname == 'oShells' || aname == 'oShellSingle' || aname == 'oRopePile') writeln("*** CREATED '", aname, "'");
4600 obj.global = global;
4602 obj.objId = ++lastUsedObjectId;
4608 final MapObject SpawnMapObject (name aname) {
4609 if (!aname) return none;
4610 auto res = SpawnMapObjectWithClass(findGameObjectClassByName(aname));
4611 if (res && !res.objType) res.objType = aname; // just in case
4616 final MapObject PutSpawnedMapObject (int x, int y, MapObject obj) {
4617 if (!obj /*|| obj.global || obj.level*/) return none; // oops
4621 if (!obj.initialize()) { delete obj; return none; } // not fatal
4624 if (obj.walkableSolid) hasSolidObjects = true;
4630 final MapObject MakeMapObject (int x, int y, name aname) {
4631 MapObject obj = SpawnMapObject(aname);
4632 obj = PutSpawnedMapObject(x, y, obj);
4637 // ////////////////////////////////////////////////////////////////////////// //
4638 void setMenuTilesVisible (bool vis) {
4640 forEachTile(delegate bool (MapTile t) {
4641 if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4642 t.invisible = false;
4647 forEachTile(delegate bool (MapTile t) {
4648 if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4657 void setMenuTilesOnTop () {
4658 forEachTile(delegate bool (MapTile t) {
4659 if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4667 // ////////////////////////////////////////////////////////////////////////// //
4668 #include "roomTitle.vc"
4669 #include "roomTrans1.vc"
4670 #include "roomTrans2.vc"
4671 #include "roomTrans3.vc"
4672 #include "roomTrans4.vc"
4673 #include "roomOlmec.vc"
4674 #include "roomEnd.vc"
4675 #include "roomIntro.vc"
4676 #include "roomTutorial.vc"
4677 #include "roomScores.vc"
4678 #include "roomStars.vc"
4679 #include "roomSun.vc"
4680 #include "roomMoon.vc"
4683 // ////////////////////////////////////////////////////////////////////////// //
4684 #include "packages/Generator/loadRoomGens.vc"
4685 #include "packages/Generator/loadEntityGens.vc"