1 /**********************************************************************************
2 * Copyright (c) 2008, 2009 Derek Yu and Mossmouth, LLC
3 * Copyright (c) 2018, Ketmar Dark
5 * This file is part of Spelunky.
7 * You can redistribute and/or modify Spelunky, including its source code, under
8 * the terms of the Spelunky User License.
10 * Spelunky is distributed in the hope that it will be entertaining and useful,
11 * but WITHOUT WARRANTY. Please see the Spelunky User License for more details.
13 * The Spelunky User License should be available in "Game .Information", which
14 * can be found in the Resource Explorer, or as an external file called COPYING.
15 * If not, please obtain a new copy of Spelunky from <http://spelunkyworld.com/>
17 **********************************************************************************/
18 // this is the level we're playing in, with all objects and tiles
19 class GameLevel : Object;
21 //#define EXPERIMENTAL_RENDER_CACHE
23 const float FrameTime = 1.0f/30.0f;
25 const int dumpGridStats = true;
32 //enum NormalTilesWidth = LevelGen::LevelWidth*RoomGen::Width+2;
33 //enum NormalTilesHeight = LevelGen::LevelHeight*RoomGen::Height+2;
35 enum MaxTilesWidth = 64;
36 enum MaxTilesHeight = 64;
39 transient GameStats stats;
40 transient SpriteStore sprStore;
41 transient BackTileStore bgtileStore;
42 transient BackTileImage levBGImg;
45 transient name lastMusicName;
46 //RoomGen[LevelGen::LevelWidth, LevelGen::LevelHeight] rooms; // moved to levelgen
48 transient float accumTime;
49 transient bool gamePaused = false;
50 transient bool gameShowHelp = false;
51 transient int gameHelpScreen = 0;
52 const int MaxGameHelpScreen = 2;
53 transient bool checkWater;
54 transient int liquidTileCount; // cached
55 /*transient*/ int damselSaved;
59 transient int collectCounter;
60 /*transient*/ int levelMoneyStart;
62 // all movable (thinkable) map objects
63 EntityGrid objGrid; // monsters, items and tiles
65 MapBackTile backtiles;
66 bool blockWaterChecking;
70 bool cameFromIntroRoom; // for title screen
72 LevelGen::RType[MaxTilesWidth, MaxTilesHeight] roomType;
86 LevelKind levelKind = LevelKind.Normal;
88 array!MapTile allEnters;
89 array!MapTile allExits;
92 int startRoomX, startRoomY;
93 int endRoomX, endRoomY;
96 transient bool playerExited;
97 transient MapEntity playerExitDoor;
98 transient bool disablePlayerThink = false;
99 transient int maxPlayingTime; // in seconds
105 bool ghostSpawned; // to speed up some checks
106 bool resetBMCOG = false;
110 // FPS, i.e. incremented by 30 in one second
111 int time; // in frames
112 int lastUsedObjectId;
113 transient int lastRenderTime = -1;
114 transient int pausedTime;
116 MapEntity deadItemsHead;
118 // screen shake variables
123 // set this before calling `fixCamera()`
124 // dimensions should be real, not scaled up/down
125 transient int viewWidth, viewHeight;
126 //transient int viewOffsetX, viewOffsetY;
128 // room bounds, not scaled
129 IVec2D viewMin, viewMax;
131 // for Olmec level cinematics
132 IVec2D cameraSlideToDest;
133 IVec2D cameraSlideToCurr;
134 IVec2D cameraSlideToSpeed; // !0: slide
135 int cameraSlideToPlayer;
136 // `fixCamera()` will set the following
137 // coordinates will be real too (with scale applied)
138 // shake is not applied
139 transient IVec2D viewStart; // with `player.viewOffset`
140 private transient IVec2D realViewStart; // without `player.viewOffset`
142 transient int framesProcessedFromLastClear;
144 transient int BuildYear;
145 transient int BuildMonth;
146 transient int BuildDay;
147 transient int BuildHour;
148 transient int BuildMin;
149 transient string BuildDateString;
152 final string getBuildDateString () {
153 if (!BuildYear) return BuildDateString;
154 if (BuildDateString) return BuildDateString;
155 BuildDateString = va("%d-%02d-%02d %02d:%02d", BuildYear, BuildMonth, BuildDay, BuildHour, BuildMin);
156 return BuildDateString;
160 final void cameraSlideToPoint (int dx, int dy, int speedx, int speedy) {
161 cameraSlideToPlayer = 0;
162 cameraSlideToDest.x = dx;
163 cameraSlideToDest.y = dy;
164 cameraSlideToSpeed.x = abs(speedx);
165 cameraSlideToSpeed.y = abs(speedy);
166 cameraSlideToCurr.x = cameraCurrX;
167 cameraSlideToCurr.y = cameraCurrY;
171 final void cameraReturnToPlayer () {
172 if (!cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y)) {
173 cameraSlideToCurr.x = cameraCurrX;
174 cameraSlideToCurr.y = cameraCurrY;
175 if (cameraSlideToSpeed.x && abs(cameraSlideToSpeed.x) < 8) cameraSlideToSpeed.x = 8;
176 if (cameraSlideToSpeed.y && abs(cameraSlideToSpeed.y) < 8) cameraSlideToSpeed.y = 8;
177 cameraSlideToPlayer = 1;
182 // if `frameSkip` is `true`, there are more frames waiting
183 // (i.e. you may skip rendering and such)
184 transient void delegate (bool frameSkip) onBeforeFrame;
185 transient void delegate (bool frameSkip) onAfterFrame;
187 transient void delegate () onCameraTeleported;
189 transient void delegate () onLevelExitedCB;
191 // this will be called in-between frames, and
192 // `frameTime` is [0..1)
193 transient void delegate (float frameTime) onInterFrame;
195 final int bizRoomStyle { get { return (lg ? lg.bizRoomStyle : 0); } }
198 final bool isNormalLevel () { return (levelKind == LevelKind.Normal); }
199 final bool isTitleRoom () { return (levelKind == LevelKind.Title); }
200 final bool isTutorialRoom () { return (levelKind == LevelKind.Tutorial); }
201 final bool isTransitionRoom () { return (levelKind == LevelKind.Transition); }
202 final bool isIntroRoom () { return (levelKind == LevelKind.Transition); }
205 bool isHUDEnabled () {
206 if (inWinCutscene) return false;
207 if (inIntroCutscene) return false;
208 if (lg.finalBossLevel) return true;
209 if (isNormalLevel()) return true;
214 // ////////////////////////////////////////////////////////////////////////// //
216 void addDeath (name aname) { if (isNormalLevel()) stats.addDeath(aname); }
223 void addKill (name aname, optional bool telefrag) {
224 if (isNormalLevel()) stats.addKill(aname, telefrag!optional);
225 else if (aname == 'Shopkeeper' && levelKind == LevelKind.Stars) { ++stats.starsKills; ++starsKills; }
228 void addCollect (name aname, optional int amount) { if (isNormalLevel()) stats.addCollect(aname, amount!optional); }
230 void addDamselSaved () { if (isNormalLevel()) stats.addDamselSaved(); }
231 void addIdolStolen () { if (isNormalLevel()) stats.addIdolStolen(); }
232 void addIdolConverted () { if (isNormalLevel()) stats.addIdolConverted(); }
233 void addCrystalIdolStolen () { if (isNormalLevel()) stats.addCrystalIdolStolen(); }
234 void addCrystalIdolConverted () { if (isNormalLevel()) stats.addCrystalIdolConverted(); }
235 void addGhostSummoned () { if (isNormalLevel()) stats.addGhostSummoned(); }
238 // ////////////////////////////////////////////////////////////////////////// //
239 static final string time2str (int time) {
240 int secs = time%60; time /= 60;
241 int mins = time%60; time /= 60;
242 int hours = time%24; time /= 24;
244 if (days) return va("%d DAYS, %d:%02d:%02d", days, hours, mins, secs);
245 if (hours) return va("%d:%02d:%02d", hours, mins, secs);
246 return va("%02d:%02d", mins, secs);
250 // ////////////////////////////////////////////////////////////////////////// //
251 final int tilesWidth () { return lg.levelRoomWidth*RoomGen::Width+2; }
252 final int tilesHeight () { return (lg.finalBossLevel ? 55 : lg.levelRoomHeight*RoomGen::Height+2); }
255 // ////////////////////////////////////////////////////////////////////////// //
256 protected void resetGameInternal () {
257 if (player) player.removeBallAndChain();
260 //inIntroCutscene = 0;
272 player.removeBallAndChain();
273 auto hi = player.holdItem;
274 player.holdItem = none;
275 if (hi) hi.instanceRemove();
276 hi = player.pickedItem;
277 player.pickedItem = none;
278 if (hi) hi.instanceRemove();
285 stats.clearGameTotals();
289 // this won't generate a level yet
290 void restartGame () {
292 if (global.startMoney > 0) stats.setMoneyCheat();
293 stats.setMoney(global.startMoney);
294 levelKind = LevelKind.Normal;
298 // complement function to `restart game`
299 void generateNormalLevel () {
301 centerViewAtPlayer();
305 void restartTitle () {
308 createSpecialLevel(LevelKind.Title, &createTitleRoom, 'musTitle');
317 void restartIntro () {
320 createSpecialLevel(LevelKind.Intro, &createIntroRoom, '');
329 void restartTutorial () {
332 createSpecialLevel(LevelKind.Tutorial, &createTutorialRoom, 'musCave');
341 void restartScores () {
344 createSpecialLevel(LevelKind.Scores, &createScoresRoom, 'musTitle');
353 void restartStarsRoom () {
356 createSpecialLevel(LevelKind.Stars, &createStarsRoom, '');
365 void restartSunRoom () {
368 createSpecialLevel(LevelKind.Sun, &createSunRoom, '');
377 void restartMoonRoom () {
380 createSpecialLevel(LevelKind.Moon, &createMoonRoom, '');
389 // ////////////////////////////////////////////////////////////////////////// //
390 // generate angry shopkeeper at exit if murderer or thief
391 void generateAngryShopkeepers () {
392 if (global.murderer || global.thiefLevel > 0) {
393 foreach (MapTile e; allExits) {
394 auto obj = MonsterShopkeeper(MakeMapObject(e.ix, e.iy, 'oShopkeeper'));
396 obj.style = 'Bounty Hunter';
397 obj.status = MapObject::PATROL;
404 // ////////////////////////////////////////////////////////////////////////// //
405 final void resetRoomBounds () {
408 viewMax.x = tilesWidth*16;
409 viewMax.y = tilesHeight*16;
410 // Great Lake is bottomless (nope)
411 //if (global.lake == 1) viewMax.y -= 16;
412 //writeln("ROOM BOUNDS: (", viewMax.x, "x", viewMax.y, ")");
416 final void setRoomBounds (int x0, int y0, int x1, int y1) {
424 // ////////////////////////////////////////////////////////////////////////// //
427 float timeout; // seconds
428 float starttime; // for active
429 bool active; // true: timeout is `GetTickCount()` dismissing time
432 array!OSDMessage msglist; // [0]: current one
434 struct OSDMessageTalk {
436 float timeout; // seconds;
437 float starttime; // for active
438 bool active; // true: timeout is `GetTickCount()` dismissing time
439 bool shopOnly; // true: timeout when player exited the shop
440 int hiColor1; // -1: default
441 int hiColor2; // -1: default
444 array!OSDMessageTalk msgtalklist; // [0]: current one
447 private final void osdCheckTimeouts () {
448 auto stt = GetTickCount();
449 while (msglist.length) {
450 if (!msglist[0].msg) { msglist.remove(0); continue; }
451 if (!msglist[0].active) {
452 msglist[0].active = true;
453 msglist[0].starttime = stt;
455 if (msglist[0].starttime+msglist[0].timeout >= stt) break;
458 if (msgtalklist.length) {
459 bool inshop = isInShop(player.ix/16, player.iy/16);
460 while (msgtalklist.length) {
461 if (!msgtalklist[0].msg) { msgtalklist.remove(0); continue; }
462 if (msgtalklist[0].shopOnly) {
463 if (inshop == msgtalklist[0].active) {
464 msgtalklist[0].active = !inshop;
465 if (!inshop) msgtalklist[0].starttime = stt;
468 if (!msgtalklist[0].active) {
469 msgtalklist[0].active = true;
470 msgtalklist[0].starttime = stt;
473 if (!msgtalklist[0].active) break;
474 if (msgtalklist[0].starttime+msgtalklist[0].timeout >= stt) break;
475 msgtalklist.remove(0);
481 final bool osdHasMessage () {
483 return (msglist.length > 0);
487 final string osdGetMessage (out float timeLeft, out float timeStart) {
489 if (msglist.length == 0) { timeLeft = 0; return ""; }
490 auto stt = GetTickCount();
491 timeStart = msglist[0].starttime;
492 timeLeft = fmax(0, msglist[0].starttime+msglist[0].timeout-stt);
493 return msglist[0].msg;
497 final string osdGetTalkMessage (optional out int hiColor1, optional out int hiColor2) {
499 if (msgtalklist.length == 0) return "";
500 hiColor1 = msgtalklist[0].hiColor1;
501 hiColor2 = msgtalklist[0].hiColor2;
502 return msgtalklist[0].msg;
506 final void osdClear () {
512 final void osdMessage (string msg, optional float timeout, optional bool urgent) {
514 msg = global.expandString(msg);
515 if (!specified_timeout) timeout = 3.33;
516 // special message for shops
517 if (timeout == -666) {
519 if (msglist.length && msglist[0].msg == msg) return;
520 if (msglist.length == 0 || msglist[0].msg != msg) {
523 msglist[0].msg = msg;
525 msglist[0].active = false;
526 msglist[0].timeout = 3.33;
530 if (timeout < 0.1) return;
531 timeout = fmax(1.0, timeout);
532 //writeln("OSD: ", msg);
533 // find existing one, and bring it to the top
535 for (; oldidx < msglist.length; ++oldidx) {
536 if (msglist[oldidx].msg == msg) break; // i found her!
539 if (oldidx < msglist.length) {
540 // yeah, move duplicate to the top
541 msglist[oldidx].timeout = fmax(timeout, msglist[oldidx].timeout);
542 msglist[oldidx].active = false;
543 if (urgent && oldidx != 0) {
544 timeout = msglist[oldidx].timeout;
545 msglist.remove(oldidx);
547 msglist[0].msg = msg;
548 msglist[0].timeout = timeout;
549 msglist[0].active = false;
553 msglist[0].msg = msg;
554 msglist[0].timeout = timeout;
555 msglist[0].active = false;
559 msglist[$-1].msg = msg;
560 msglist[$-1].timeout = timeout;
561 msglist[$-1].active = false;
567 void osdMessageTalk (string msg, optional bool replace, optional float timeout, optional bool inShopOnly,
568 optional int hiColor1, optional int hiColor2)
570 if (!specified_timeout) timeout = 3.33;
571 if (!specified_inShopOnly) inShopOnly = true;
572 if (!specified_hiColor1) hiColor1 = -1;
573 if (!specified_hiColor2) hiColor2 = -1;
574 msg = global.expandString(msg);
576 if (!msg) { msgtalklist.clear(); return; }
577 if (msgtalklist.length && msgtalklist[0].msg == msg) {
578 while (msgtalklist.length > 1) msgtalklist.remove(1);
579 msgtalklist[$-1].timeout = timeout;
580 msgtalklist[$-1].shopOnly = inShopOnly;
582 if (msgtalklist.length) msgtalklist.clear();
583 msgtalklist.length += 1;
584 msgtalklist[$-1].msg = msg;
585 msgtalklist[$-1].timeout = timeout;
586 msgtalklist[$-1].active = false;
587 msgtalklist[$-1].shopOnly = inShopOnly;
588 msgtalklist[$-1].hiColor1 = hiColor1;
589 msgtalklist[$-1].hiColor2 = hiColor2;
594 foreach (auto midx, ref auto mnfo; msgtalklist) {
595 if (mnfo.msg == msg) {
596 mnfo.timeout = timeout;
597 mnfo.shopOnly = inShopOnly;
602 msgtalklist.length += 1;
603 msgtalklist[$-1].msg = msg;
604 msgtalklist[$-1].timeout = timeout;
605 msgtalklist[$-1].active = false;
606 msgtalklist[$-1].hiColor1 = hiColor1;
607 msgtalklist[$-1].hiColor2 = hiColor2;
614 // ////////////////////////////////////////////////////////////////////////// //
615 void setup (GameGlobal aGlobal, SpriteStore aSprStore, BackTileStore aBGTileStore) {
617 sprStore = aSprStore;
618 bgtileStore = aBGTileStore;
620 lg = SpawnObject(LevelGen);
624 objGrid = SpawnObject(EntityGrid);
625 objGrid.setup(0, 0, MaxTilesWidth*16, MaxTilesHeight*16);
629 // stores should be set
633 levBGImg = bgtileStore[levBGImgName];
634 foreach (MapEntity o; objGrid.allObjects()) {
637 if (t && (t.lava || t.water)) ++liquidTileCount;
639 for (MapBackTile bt = backtiles; bt; bt = bt.next) bt.onLoaded();
640 if (player) player.onLoaded();
642 if (msglist.length) {
643 msglist[0].active = false;
644 msglist[0].timeout = 0.200;
647 lastMusicName = (lg ? lg.musicName : '');
648 global.setMusicPitch(1.0);
649 if (lg && lg.musicName) global.playMusic(lg.musicName); else global.stopMusic();
653 // ////////////////////////////////////////////////////////////////////////// //
654 void pickedSpectacles () {
655 foreach (MapTile t; objGrid.allObjectsSafe(MapTile)) t.onGotSpectacles();
659 // ////////////////////////////////////////////////////////////////////////// //
660 #include "rgentile.vc"
661 #include "rgenobj.vc"
664 void onLevelExited () {
665 if (playerExitDoor isa TitleTileXTitle) {
666 playerExitDoor = none;
671 if (isTitleRoom() || levelKind == LevelKind.Scores) {
672 if (playerExitDoor) processTitleExit(playerExitDoor);
673 playerExitDoor = none;
676 if (isTutorialRoom()) {
677 playerExitDoor = none;
679 global.currLevel = 1;
680 generateNormalLevel();
684 if (levelKind == LevelKind.Stars || levelKind == LevelKind.Sun || levelKind == LevelKind.Moon) {
685 playerExitDoor = none;
687 if (onLevelExitedCB) onLevelExitedCB();
692 if (isNormalLevel()) {
693 stats.maxLevelComplete = max(stats.maxLevelComplete, global.currLevel);
695 if (playerExitDoor) {
696 if (playerExitDoor.objType == 'oXGold') {
697 writeln("exiting to City Of Gold");
698 global.cityOfGold = true;
699 //!global.currLevel += 1;
700 } else if (playerExitDoor.objType == 'oXMarket') {
701 writeln("exiting to Black Market");
702 global.genBlackMarket = true;
703 //!global.currLevel += 1;
707 if (onLevelExitedCB) onLevelExitedCB();
709 playerExitDoor = none;
710 if (levelKind == LevelKind.Transition) {
711 if (global.thiefLevel > 0) global.thiefLevel -= 1;
712 if (global.alienCraft) ++global.alienCraft;
713 if (global.yetiLair) ++global.yetiLair;
714 if (global.lake) ++global.lake;
715 if (global.cityOfGold) ++global.cityOfGold;
716 //orig dbg:if (global.currLevel == 1) global.currLevel += firstLevelSkip; else global.currLevel += levelSkip;
718 if (!global.blackMarket && !global.cityOfGold /*&& !global.yetiLair*/) {
719 global.currLevel += 1;
725 // < 20 seconds per level: looks like a speedrun
726 global.noDarkLevel = (levelEndTime > levelStartTime && levelEndTime-levelStartTime < 20*30);
727 if (lg.finalBossLevel) {
730 // add money for big idol
731 player.addScore(50000);
735 generateTransitionLevel();
738 //centerViewAtPlayer();
742 void onOlmecDead (MapObject o) {
743 writeln("*** OLMEC IS DEAD!");
744 foreach (MapTile t; allExits) {
747 auto st = checkTileAtPoint(t.ix+8, t.iy+16);
749 st = MakeMapTile(t.ix/16, t.iy/16+1, 'oTemple');
752 st.invincible = true;
758 void generateLevelMessages () {
759 writeln("LEVEL NUMBER: ", global.currLevel);
760 if (global.darkLevel) {
761 if (global.hasCrown) {
762 osdMessage("THE HEDJET SHINES BRIGHTLY.");
763 global.darkLevel = false;
764 } else if (global.config.scumDarkness < 2) {
765 osdMessage("I CAN'T SEE A THING!\nI'D BETTER USE THESE FLARES!");
769 if (global.blackMarket) osdMessage("WELCOME TO THE BLACK MARKET!");
771 if (global.cemetary) osdMessage("THE DEAD ARE RESTLESS!");
772 if (global.lake == 1) osdMessage("I CAN HEAR RUSHING WATER...");
774 if (global.snakePit) osdMessage("I HEAR SNAKES... I HATE SNAKES!");
775 if (global.yetiLair == 1) osdMessage("IT SMELLS LIKE WET FUR IN HERE!");
776 if (global.alienCraft == 1) osdMessage("THERE'S A PSYCHIC PRESENCE HERE!");
777 if (global.cityOfGold == 1) {
778 if (true /*hasOlmec()*/) osdMessage("IT'S THE LEGENDARY CITY OF GOLD!");
781 if (global.sacrificePit) osdMessage("I CAN HEAR PRAYERS TO KALI!");
785 final MapObject debugSpawnObjectWithClass (class!MapObject oclass, optional bool playerDir) {
786 if (!oclass) return none;
788 bool canLeft = !isSolidAtPoint(player.ix-8, player.iy);
789 bool canRight = !isSolidAtPoint(player.ix+16, player.iy);
790 if (!canLeft && !canRight) return none;
791 if (canLeft && canRight) {
793 dx = (player.dir == MapObject::Dir.Left ? -16 : 16);
798 dx = (canLeft ? -16 : 16);
800 auto obj = SpawnMapObjectWithClass(oclass);
801 if (obj isa MapEnemy) {
803 dy -= (obj isa MonsterDamsel ? 2 : 8);
805 if (obj) PutSpawnedMapObject(player.ix+dx, player.iy+dy, obj);
810 final MapObject debugSpawnObject (name aname) {
811 if (!aname) return none;
812 return debugSpawnObjectWithClass(findGameObjectClassByName(aname));
816 void createSpecialLevel (LevelKind kind, scope void delegate () creator, name amusic) {
817 global.darkLevel = false;
821 global.resetStartingItems();
823 global.setMusicPitch(1.0);
826 auto olddel = ImmediateDelete;
827 ImmediateDelete = false;
835 addBackgroundGfxDetails();
836 //levBGImgName = 'bgCave';
837 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
839 blockWaterChecking = true;
843 ImmediateDelete = olddel;
844 CollectGarbage(true); // destroy delayed objects too
846 if (dumpGridStats) objGrid.dumpStats();
848 playerExited = false; // just in case
849 playerExitDoor = none;
854 lg.musicName = amusic;
855 lastMusicName = amusic;
856 global.setMusicPitch(1.0);
857 if (amusic) global.playMusic(lg.musicName); else global.stopMusic();
861 void createTitleLevel () {
862 createSpecialLevel(LevelKind.Title, &createTitleRoom, 'musTitle');
866 void createTutorialLevel () {
867 createSpecialLevel(LevelKind.Tutorial, &createTutorialRoom, 'musCave');
876 // `global.currLevel` is the new level
877 void generateTransitionLevel () {
878 global.darkLevel = false;
883 resetTransitionOverlay();
885 global.setMusicPitch(1.0);
886 switch (global.config.transitionMusicMode) {
887 case GameConfig::MusicMode.Silent: global.stopMusic(); break;
888 case GameConfig::MusicMode.Restart: global.restartMusic(); break;
889 case GameConfig::MusicMode.DontTouch: break;
892 levelKind = LevelKind.Transition;
894 auto olddel = ImmediateDelete;
895 ImmediateDelete = false;
898 if (global.currLevel < 4) createTrans1Room();
899 else if (global.currLevel == 4) createTrans1xRoom();
900 else if (global.currLevel < 8) createTrans2Room();
901 else if (global.currLevel == 8) createTrans2xRoom();
902 else if (global.currLevel < 12) createTrans3Room();
903 else if (global.currLevel == 12) createTrans3xRoom();
904 else if (global.currLevel < 16) createTrans4Room();
905 else if (global.currLevel == 16) createTrans4Room();
906 else createTrans1Room(); //???
911 addBackgroundGfxDetails();
912 //levBGImgName = 'bgCave';
913 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
915 blockWaterChecking = true;
919 if (damselSaved > 0) {
920 // this is special "damsel ready to kiss you" object, not a heart
921 MakeMapObject(176+8, 176+8, 'oDamselKiss');
922 global.plife += damselSaved; // if player skipped transition cutscene
926 ImmediateDelete = olddel;
927 CollectGarbage(true); // destroy delayed objects too
929 if (dumpGridStats) objGrid.dumpStats();
931 playerExited = false; // just in case
932 playerExitDoor = none;
937 //global.playMusic(lg.musicName);
941 void generateLevel () {
942 levelStartTime = time;
948 global.genBlackMarket = false;
951 global.setMusicPitch(1.0);
952 stats.clearLevelTotals();
954 levelKind = LevelKind.Normal;
961 //writeln("tw:", tilesWidth, "; th:", tilesHeight);
963 auto olddel = ImmediateDelete;
964 ImmediateDelete = false;
967 if (lg.finalBossLevel) {
968 blockWaterChecking = true;
972 // if transition cutscene was skipped...
973 global.plife += max(0, damselSaved); // if player skipped transition cutscene
977 startRoomX = lg.startRoomX;
978 startRoomY = lg.startRoomY;
979 endRoomX = lg.endRoomX;
980 endRoomY = lg.endRoomY;
981 addBackgroundGfxDetails();
982 foreach (int y; 0..tilesHeight) {
983 foreach (int x; 0..tilesWidth) {
989 levBGImgName = lg.bgImgName;
990 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
992 if (global.allowAngryShopkeepers) generateAngryShopkeepers();
994 lg.generateEntities();
996 // add box of flares to dark level
997 if (global.darkLevel && allEnters.length) {
998 auto enter = allEnters[0];
999 int x = enter.ix, y = enter.iy;
1000 if (!isSolidAtPoint(x-16, y)) MakeMapObject(x-16+8, y+8, 'oFlareCrate');
1001 else if (!isSolidAtPoint(x+16, y)) MakeMapObject(x+16+8, y+8, 'oFlareCrate');
1002 else MakeMapObject(x+8, y+8, 'oFlareCrate');
1005 //scrGenerateEntities();
1006 //foreach (; 0..2) scrGenerateEntities();
1008 writeln(objGrid.countObjects, " alive objects inserted");
1009 writeln(countBackTiles, " background tiles inserted");
1011 if (!player) FatalError("player pawn is not spawned");
1013 if (lg.finalBossLevel) {
1014 blockWaterChecking = true;
1016 blockWaterChecking = false;
1021 ImmediateDelete = olddel;
1022 CollectGarbage(true); // destroy delayed objects too
1024 if (dumpGridStats) objGrid.dumpStats();
1026 playerExited = false; // just in case
1027 playerExitDoor = none;
1029 levelMoneyStart = stats.money;
1032 generateLevelMessages();
1037 //writeln("!<", lastMusicName, ">:<", lg.musicName, ">");
1038 global.setMusicPitch(1.0);
1039 if (lastMusicName != lg.musicName) {
1040 global.playMusic(lg.musicName);
1042 //writeln("MM: ", global.config.nextLevelMusicMode);
1043 switch (global.config.nextLevelMusicMode) {
1044 case GameConfig::MusicMode.Silent: global.stopMusic(); break; // the thing that should not be
1045 case GameConfig::MusicMode.Restart: global.restartMusic(); break;
1046 case GameConfig::MusicMode.DontTouch:
1047 if (global.config.transitionMusicMode == GameConfig::MusicMode.Silent) {
1048 global.playMusic(lg.musicName);
1053 lastMusicName = lg.musicName;
1054 //global.playMusic(lg.musicName);
1057 if (global.cityOfGold == 1 || global.genBlackMarket) resetBMCOG = true;
1059 if (global.cityOfGold == 1) {
1060 lg.mapSprite = 'sMapTemple';
1061 lg.mapTitle = "City of Gold";
1062 } else if (global.blackMarket) {
1063 lg.mapSprite = 'sMapJungle';
1064 lg.mapTitle = "Black Market";
1069 // ////////////////////////////////////////////////////////////////////////// //
1070 int currKeys, nextKeys;
1071 int pressedKeysQ, releasedKeysQ;
1072 int keysPressed, keysReleased = -1;
1075 struct SavedKeyState {
1076 int currKeys, nextKeys;
1077 int pressedKeysQ, releasedKeysQ;
1078 int keysPressed, keysReleased;
1080 int roomSeed, otherSeed;
1084 // for saving/replaying
1085 final void keysSaveState (out SavedKeyState ks) {
1086 ks.currKeys = currKeys;
1087 ks.nextKeys = nextKeys;
1088 ks.pressedKeysQ = pressedKeysQ;
1089 ks.releasedKeysQ = releasedKeysQ;
1090 ks.keysPressed = keysPressed;
1091 ks.keysReleased = keysReleased;
1094 // for saving/replaying
1095 final void keysRestoreState (const ref SavedKeyState ks) {
1096 currKeys = ks.currKeys;
1097 nextKeys = ks.nextKeys;
1098 pressedKeysQ = ks.pressedKeysQ;
1099 releasedKeysQ = ks.releasedKeysQ;
1100 keysPressed = ks.keysPressed;
1101 keysReleased = ks.keysReleased;
1105 final void keysNextFrame () {
1106 currKeys = nextKeys;
1110 final void clearKeys () {
1120 final void onKey (int code, bool down) {
1125 if (keysReleased&code) {
1126 keysPressed |= code;
1127 keysReleased &= ~code;
1128 pressedKeysQ |= code;
1132 if (keysPressed&code) {
1133 keysReleased |= code;
1134 keysPressed &= ~code;
1135 releasedKeysQ |= code;
1140 final bool isKeyDown (int code) {
1141 return !!(currKeys&code);
1144 final bool isKeyPressed (int code) {
1145 bool res = !!(pressedKeysQ&code);
1146 pressedKeysQ &= ~code;
1150 final bool isKeyReleased (int code) {
1151 bool res = !!(releasedKeysQ&code);
1152 releasedKeysQ &= ~code;
1157 final void clearKeysPressRelease () {
1158 keysPressed = default.keysPressed;
1159 keysReleased = default.keysReleased;
1160 pressedKeysQ = default.pressedKeysQ;
1161 releasedKeysQ = default.releasedKeysQ;
1167 // ////////////////////////////////////////////////////////////////////////// //
1168 final void registerEnter (MapTile t) {
1175 final void registerExit (MapTile t) {
1182 final bool isYAtEntranceRow (int py) {
1184 foreach (MapTile t; allEnters) if (t.iy == py) return true;
1189 final int calcNearestEnterDist (int px, int py) {
1190 if (allEnters.length == 0) return int.max;
1191 int curdistsq = int.max;
1192 foreach (MapTile t; allEnters) {
1193 int xc = px-t.xCenter, yc = py-t.yCenter;
1194 int distsq = xc*xc+yc*yc;
1195 if (distsq < curdistsq) curdistsq = distsq;
1197 return round(sqrt(curdistsq));
1201 final int calcNearestExitDist (int px, int py) {
1202 if (allExits.length == 0) return int.max;
1203 int curdistsq = int.max;
1204 foreach (MapTile t; allExits) {
1205 int xc = px-t.xCenter, yc = py-t.yCenter;
1206 int distsq = xc*xc+yc*yc;
1207 if (distsq < curdistsq) curdistsq = distsq;
1209 return round(sqrt(curdistsq));
1213 // ////////////////////////////////////////////////////////////////////////// //
1214 final void clearForTransition () {
1215 auto olddel = ImmediateDelete;
1216 ImmediateDelete = false;
1218 ImmediateDelete = olddel;
1219 CollectGarbage(true); // destroy delayed objects too
1220 global.darkLevel = false;
1224 // ////////////////////////////////////////////////////////////////////////// //
1225 final int countBackTiles () {
1227 for (MapBackTile bt = backtiles; bt; bt = bt.next) ++res;
1232 final void clearWholeLevel () {
1236 // don't kill objects the player is holding
1238 if (player.pickedItem isa ItemBall) {
1239 player.pickedItem.instanceRemove();
1240 player.pickedItem = none;
1242 if (player.pickedItem && player.pickedItem.grid) {
1243 player.pickedItem.grid.remove(player.pickedItem.gridId);
1244 writeln("secured pocket item '", GetClassName(player.pickedItem.Class), "'");
1246 if (player.holdItem isa ItemBall) {
1247 player.removeBallAndChain(temp:true);
1248 if (player.holdItem) player.holdItem.instanceRemove();
1249 player.holdItem = none;
1251 if (player.holdItem && player.holdItem.grid) {
1252 player.holdItem.grid.remove(player.holdItem.gridId);
1253 writeln("secured held item '", GetClassName(player.holdItem.Class), "'");
1255 writeln("secured ball; mustBeChained=", player.mustBeChained, "; wasHoldingBall=", player.wasHoldingBall);
1258 int count = objGrid.countObjects();
1259 if (dumpGridStats) { if (objGrid.getFirstObjectCID()) objGrid.dumpStats(); }
1260 objGrid.removeAllObjects(true); // and destroy
1261 if (count > 0) writeln(count, " objects destroyed");
1263 lastUsedObjectId = 0;
1266 lastRenderTime = -1;
1267 liquidTileCount = 0;
1271 MapBackTile t = backtiles;
1277 framesProcessedFromLastClear = 0;
1281 final void insertObject (MapEntity o) {
1283 if (o.grid) FatalError("cannot put object into level twice");
1288 final void spawnPlayerAt (int x, int y) {
1289 // if we have no player, spawn new one
1290 // otherwise this just a level transition, so simply reposition him
1292 // don't add player to object list, as it has very separate processing anyway
1293 player = SpawnObject(PlayerPawn);
1294 player.global = global;
1295 player.level = self;
1296 if (!player.initialize()) {
1298 FatalError("something is wrong with player initialization");
1304 player.saveInterpData();
1306 if (player.mustBeChained || global.config.scumBallAndChain) {
1307 writeln("*** spawning ball and chain");
1308 player.spawnBallAndChain(levelStart:true);
1310 playerExited = false;
1311 playerExitDoor = none;
1312 if (global.config.startWithKapala) global.hasKapala = true;
1313 centerViewAtPlayer();
1314 // reinsert player items into grid
1315 if (player.pickedItem) objGrid.insert(player.pickedItem);
1316 if (player.holdItem) objGrid.insert(player.holdItem);
1317 //writeln("player spawned; active=", player.active);
1318 player.scrSwitchToPocketItem(forceIfEmpty:false);
1322 final void teleportPlayerTo (int x, int y) {
1326 player.saveInterpData();
1331 final void resurrectPlayer () {
1332 if (player) player.resurrect();
1333 playerExited = false;
1334 playerExitDoor = none;
1338 // ////////////////////////////////////////////////////////////////////////// //
1339 final void scrShake (int duration) {
1340 if (shakeLeft == 0) {
1346 shakeLeft = max(shakeLeft, duration);
1351 // ////////////////////////////////////////////////////////////////////////// //
1354 ItemStolen, // including damsel, lol
1360 // checks for dead, agnered, distance, etc. should be already done
1361 protected void doAngerShopkeeper (MonsterShopkeeper shp, SCAnger reason, ref bool messaged,
1362 int maxdist, MapEntity offender)
1364 if (!shp || shp.dead || shp.angered) return;
1365 if (offender.distanceToEntityCenter(shp) > maxdist) return;
1367 shp.status = MapObject::ATTACK;
1369 if (global.murderer) {
1370 msg = "~YOU'LL PAY FOR YOUR CRIMES!~";
1373 case SCAnger.TileDestroyed: msg = "~DIE, YOU VANDAL!~"; break;
1374 case SCAnger.ItemStolen: msg = "~COME BACK HERE, THIEF!~"; break;
1375 case SCAnger.CrapsCheated: msg = "~DIE, CHEATER!~"; break;
1376 case SCAnger.BombDropped: msg = "~TERRORIST!~"; break;
1377 case SCAnger.DamselWhipped: msg = "~HEY, ONLY I CAN DO THAT!~"; break;
1378 default: "~NOW I'M REALLY STEAMED!~"; break;
1382 writeln("shopkeeper angered; reason=", reason, "; maxdist=", maxdist, "; msg=\"", msg, "\"");
1385 if (msg) osdMessageTalk(msg, replace:true, inShopOnly:false, hiColor1:0xff_00_00);
1386 global.thiefLevel += (global.thiefLevel > 0 ? 3 : 2);
1391 // make the nearest shopkeeper angry. RAWR!
1392 void scrShopkeeperAnger (SCAnger reason, optional int maxdist, optional MapEntity offender) {
1393 bool messaged = false;
1394 maxdist = clamp(maxdist, 96, 100000);
1395 if (!offender) offender = player;
1396 if (maxdist == 100000) {
1397 foreach (MonsterShopkeeper shp; objGrid.allObjects(MonsterShopkeeper)) {
1398 doAngerShopkeeper(shp, reason, messaged, maxdist, offender);
1401 foreach (MonsterShopkeeper shp; objGrid.inRectPix(offender.xCenter-maxdist-128, offender.yCenter-maxdist-128, (maxdist+128)*2, (maxdist+128)*2, precise:false, castClass:MonsterShopkeeper)) {
1402 doAngerShopkeeper(shp, reason, messaged, maxdist, offender);
1408 final MapObject findCrapsPrize () {
1409 foreach (MapObject o; objGrid.allObjects(MapObject)) {
1410 if (!o.spectral && o.inDiceHouse) return o;
1416 // ////////////////////////////////////////////////////////////////////////// //
1417 // 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.
1418 // note: idols moved by monkeys will have false `stolenIdol`
1419 void scrTriggerIdolAltar (bool stolenIdol) {
1420 ObjTikiCurse res = none;
1421 int curdistsq = int.max;
1422 int px = player.xCenter, py = player.yCenter;
1423 foreach (MapObject o; objGrid.allObjects(MapObject)) {
1424 auto tcr = ObjTikiCurse(o);
1426 if (tcr.activated) continue;
1427 int xc = px-tcr.xCenter, yc = py-tcr.yCenter;
1428 int distsq = xc*xc+yc*yc;
1429 if (distsq < curdistsq) {
1434 if (res) res.activate(stolenIdol);
1438 // ////////////////////////////////////////////////////////////////////////// //
1439 void setupGhostTime () {
1440 musicFadeTimer = -1;
1441 ghostSpawned = false;
1443 // there is no ghost on the first level
1444 if (inWinCutscene || inIntroCutscene || !isNormalLevel() || lg.finalBossLevel ||
1445 (!global.config.ghostAtFirstLevel && global.currLevel == 1))
1448 global.setMusicPitch(1.0);
1452 if (global.config.scumGhost < 0) {
1455 osdMessage("A GHASTLY VOICE CALLS YOUR NAME!\nLET'S GET OUT OF HERE!", 6.66, urgent:true);
1459 if (global.config.scumGhost == 0) {
1465 // randomizes time until ghost appears once time limit is reached
1466 // global.ghostRandom (true/false) if extra time is randomized - loaded from yasm.cfg. if false, use 30 second default
1467 // ghostTimeLeft (time in seconds * 1000) for currently generated level
1469 if (global.config.ghostRandom) {
1470 auto tMin = global.randOther(10, 30); // min(global.plife*10, 30);
1471 auto tMax = max(tMin+global.randOther(10, 30), min(global.config.scumGhost, 300)); // 5 minutes max
1472 auto tTime = global.randOther(tMin, tMax);
1473 if (tTime <= 0) tTime = round(tMax/2.0);
1474 ghostTimeLeft = tTime;
1476 ghostTimeLeft = global.config.scumGhost; // 5 minutes max
1479 ghostTimeLeft += max(0, global.config.ghostExtraTime);
1481 ghostTimeLeft *= 30; // seconds -> frames
1482 //global.ghostShowTime
1486 void spawnGhost () {
1488 ghostSpawned = true;
1491 int vwdt = (viewMax.x-viewMin.x);
1492 int vhgt = (viewMax.y-viewMin.y);
1496 if (player.ix < viewMin.x+vwdt/2) {
1497 // player is in the left side
1498 gx = viewMin.x+vwdt/2+vwdt/4;
1500 // player is in the right side
1501 gx = viewMin.x+vwdt/4;
1504 if (player.iy < viewMin.y+vhgt/2) {
1505 // player is in the left side
1506 gy = viewMin.y+vhgt/2+vhgt/4;
1508 // player is in the right side
1509 gy = viewMin.y+vhgt/4;
1512 writeln("spawning ghost at tile (", gx/16, ",", gy/16, "); player is at tile (", player.ix/16, ",", player.iy/16, ")");
1514 MakeMapObject(gx, gy, 'oGhost');
1517 if (oPlayer1.x > room_width/2) instance_create(view_xview[0]+view_wview[0]+8, view_yview[0]+floor(view_hview[0] / 2), oGhost);
1518 else instance_create(view_xview[0]-32, view_yview[0]+floor(view_hview[0]/2), oGhost);
1519 global.ghostExists = true;
1524 void thinkFrameGameGhost () {
1525 if (player.dead) return;
1526 if (!isNormalLevel()) return; // just in case
1528 if (ghostTimeLeft < 0) {
1530 if (musicFadeTimer > 0) {
1531 musicFadeTimer = -1;
1532 global.setMusicPitch(1.0);
1537 if (musicFadeTimer >= 0) {
1539 if (musicFadeTimer > 0 && musicFadeTimer <= 100*3) {
1540 float pitch = 1.0-(0.00226/3.0)*musicFadeTimer;
1541 //writeln("MFT: ", musicFadeTimer, "; pitch=", pitch);
1542 global.setMusicPitch(pitch);
1546 if (ghostTimeLeft == 0) {
1547 // she is already here!
1551 // no ghost if we have a crown
1552 if (global.hasCrown) {
1557 // if she was already spawned, don't do it again
1563 if (--ghostTimeLeft != 0) {
1565 if (global.config.ghostExtraTime > 0) {
1566 if (ghostTimeLeft == global.config.ghostExtraTime*30) {
1567 osdMessage("A CHILL RUNS UP YOUR SPINE...\nLET'S GET OUT OF HERE!", 6.66, urgent:true);
1569 if (musicFadeTimer < 0 && ghostTimeLeft <= global.config.ghostExtraTime*30) {
1577 if (player.isExitingSprite) {
1578 // no reason to spawn her, we're leaving
1587 void thinkFrameGame () {
1588 thinkFrameGameGhost();
1589 // udjat eye blinking
1590 if (global.hasUdjatEye && player) {
1591 foreach (MapTile t; allExits) {
1592 if (t isa MapTileBlackMarketDoor) {
1593 auto dm = int(player.distanceToEntity(t));
1595 if (udjatAlarm < 1 || dm < udjatAlarm) udjatAlarm = dm;
1599 global.udjatBlink = false;
1602 if (udjatAlarm > 0) {
1603 if (--udjatAlarm == 0) {
1604 global.udjatBlink = !global.udjatBlink;
1605 if (global.hasUdjatEye && player) {
1606 player.playSound(global.udjatBlink ? 'sndBlink1' : 'sndBlink2');
1610 switch (levelKind) {
1611 case LevelKind.Stars: thinkFrameGameStars(); break;
1612 case LevelKind.Sun: thinkFrameGameSun(); break;
1613 case LevelKind.Moon: thinkFrameGameMoon(); break;
1614 case LevelKind.Transition: thinkFrameTransition(); break;
1615 case LevelKind.Intro: thinkFrameIntro(); break;
1620 // ////////////////////////////////////////////////////////////////////////// //
1621 private final bool isWaterTileCB (MapTile t) {
1622 return (t && t.visible && t.water);
1626 private final bool isLavaTileCB (MapTile t) {
1627 return (t && t.visible && t.lava);
1631 // ////////////////////////////////////////////////////////////////////////// //
1632 const int GreatLakeStartTileY = 28;
1635 final void fillGreatLake () {
1636 if (global.lake == 1) {
1637 foreach (int y; GreatLakeStartTileY..tilesHeight) {
1638 foreach (int x; 0..tilesWidth) {
1639 auto t = checkTileAtPoint(x*16, y*16, delegate bool (MapTile t) {
1640 if (t.spectral || !t.visible || t.invisible || t.moveable) return false;
1644 t = MakeMapTile(x, y, 'oWaterSwim');
1648 t.setSprite(y == GreatLakeStartTileY ? 'sWaterTop' : 'sWater');
1649 } else if (t.lava) {
1650 t.setSprite(y == GreatLakeStartTileY ? 'sLavaTop' : 'sLava');
1658 // called once after level generation
1659 final void fixLiquidTop () {
1660 if (global.lake == 1) fillGreatLake();
1662 liquidTileCount = 0;
1663 foreach (MapTile t; objGrid.allObjects(MapTile)) {
1664 if (!t.water && !t.lava) continue;
1667 //writeln("fixing water tile(", GetClassName(t.Class), "):'", t.objName, "' (water=", t.water, "; lava=", t.lava, "); lqc=", liquidTileCount);
1669 //if (global.lake == 1) continue; // it is done in `fillGreatLake()`
1671 if (!checkTileAtPoint(t.ix+8, t.iy-8, (t.lava ? &isLavaTileCB : &isWaterTileCB))) {
1672 t.setSprite(t.lava ? 'sLavaTop' : 'sWaterTop');
1674 // don't do this, it will destroy seaweed
1675 //t.setSprite(t.lava ? 'sLava' : 'sWater');
1676 auto spr = t.getSprite();
1677 if (!spr) t.setSprite(t.lava ? 'sLava' : 'sWater');
1678 else if (spr.Name == 'sLavaTop') t.setSprite('sLava');
1679 else if (spr.Name == 'sWaterTop') t.setSprite('sWater');
1682 //writeln("liquid tiles count: ", liquidTileCount);
1686 // ////////////////////////////////////////////////////////////////////////// //
1687 transient MapTile curWaterTile;
1688 transient bool curWaterTileCheckHitsLava;
1689 transient bool curWaterTileCheckHitsSolidOrWater; // only for `checkWaterOrSolidTilePartialCB`
1690 transient int curWaterTileLastHDir;
1691 transient ubyte[16, 16] curWaterOccupied;
1692 transient int curWaterOccupiedCount;
1693 transient int curWaterTileCheckX0, curWaterTileCheckY0;
1696 private final void clearCurWaterCheckState () {
1697 curWaterTileCheckHitsLava = false;
1698 curWaterOccupiedCount = 0;
1699 foreach (auto idx; 0..16*16) curWaterOccupied[idx] = 0;
1703 private final bool checkWaterOrSolidTileCB (MapTile t) {
1704 if (t == curWaterTile) return false;
1705 if (t.lava && curWaterTile.water) {
1706 curWaterTileCheckHitsLava = true;
1709 if (t.ix%16 != 0 || t.iy%16 != 0) {
1710 if (t.water || t.solid) {
1711 // fill occupied array
1712 //FIXME: optimize this
1713 if (curWaterOccupiedCount < 16*16) {
1714 foreach (auto dy; t.y0..t.y1+1) {
1715 foreach (auto dx; t.x0..t.x1+1) {
1716 int sx = dx-curWaterTileCheckX0;
1717 int sy = dy-curWaterTileCheckY0;
1718 if (sx >= 0 && sx <= 16 && sy >= 0 && sy <= 15 && !curWaterOccupied[sx, sy]) {
1719 curWaterOccupied[sx, sy] = 1;
1720 ++curWaterOccupiedCount;
1726 return false; // need to check for lava
1728 if (t.water || t.solid || t.lava) {
1729 curWaterOccupiedCount = 16*16;
1730 if (t.water && curWaterTile.lava) t.instanceRemove();
1732 return false; // need to check for lava
1736 private final bool checkWaterOrSolidTilePartialCB (MapTile t) {
1737 if (t == curWaterTile) return false;
1738 if (t.lava && curWaterTile.water) {
1739 //writeln("!!!!!!!!");
1740 curWaterTileCheckHitsLava = true;
1743 if (t.water || t.solid || t.lava) {
1744 //writeln("*********");
1745 curWaterTileCheckHitsSolidOrWater = true;
1746 if (t.water && curWaterTile.lava) t.instanceRemove();
1748 return false; // need to check for lava
1752 private final bool isFullyOccupiedAtTilePos (int tileX, int tileY) {
1753 clearCurWaterCheckState();
1754 curWaterTileCheckX0 = tileX*16;
1755 curWaterTileCheckY0 = tileY*16;
1756 checkTilesInRect(tileX*16, tileY*16, 16, 16, &checkWaterOrSolidTileCB);
1757 return (curWaterTileCheckHitsLava || curWaterOccupiedCount == 16*16);
1761 private final bool isAtLeastPartiallyOccupiedAtTilePos (int tileX, int tileY) {
1762 curWaterTileCheckHitsLava = false;
1763 curWaterTileCheckHitsSolidOrWater = false;
1764 checkTilesInRect(tileX*16, tileY*16, 16, 16, &checkWaterOrSolidTilePartialCB);
1765 return (curWaterTileCheckHitsSolidOrWater || curWaterTileCheckHitsLava);
1769 private final bool waterCanReachGroundHoleInDir (MapTile wtile, int dx) {
1770 if (dx == 0) return false; // just in case
1772 int x = wtile.ix/16, y = wtile.iy/16;
1774 while (x >= 0 && x < tilesWidth) {
1775 if (!isAtLeastPartiallyOccupiedAtTilePos(x, y+1)) return true;
1776 if (isAtLeastPartiallyOccupiedAtTilePos(x, y)) return false;
1783 // returns `true` if this tile must be removed
1784 private final bool checkWaterFlow (MapTile wtile) {
1785 if (global.lake == 1) {
1786 if (wtile.iy >= GreatLakeStartTileY*16) return false; // lake tile, don't touch
1787 if (wtile.iy >= GreatLakeStartTileY*16-16) return true; // remove it, so it won't stack on a lake
1790 if (wtile.ix%16 != 0 || wtile.iy%16 != 0) return true; // sanity check
1792 curWaterTile = wtile;
1793 curWaterTileLastHDir = 0; // never moved to the side
1795 bool wasMoved = false;
1798 int tileX = wtile.ix/16, tileY = wtile.iy/16;
1801 if (tileY >= tilesHeight) return true;
1803 // check if we can fall down
1804 auto canFall = !isAtLeastPartiallyOccupiedAtTilePos(tileX, tileY+1);
1805 // disappear if can fall in lava
1806 if (wtile.water && curWaterTileCheckHitsLava) {
1807 //!writeln(wtile.objId, ": LAVA HIT DOWN");
1811 // fake, so caller will not start removing tiles
1812 if (canFall) wtile.waterMovedDown = true;
1818 //!writeln(wtile.objId, ": GOING DOWN");
1819 curWaterTileLastHDir = 0;
1820 wtile.iy = wtile.iy+16;
1822 wtile.waterMovedDown = true;
1826 bool canMoveLeft = (curWaterTileLastHDir > 0 ? false : !isAtLeastPartiallyOccupiedAtTilePos(tileX-1, tileY));
1827 // disappear if near lava
1828 if (wtile.water && curWaterTileCheckHitsLava) {
1829 //!writeln(wtile.objId, ": LAVA HIT LEFT");
1833 bool canMoveRight = (curWaterTileLastHDir < 0 ? false : !isAtLeastPartiallyOccupiedAtTilePos(tileX+1, tileY));
1834 // disappear if near lava
1835 if (wtile.water && curWaterTileCheckHitsLava) {
1836 //!writeln(wtile.objId, ": LAVA HIT RIGHT");
1840 if (!canMoveLeft && !canMoveRight) {
1842 //!if (wasMove) writeln(wtile.objId, ": NO MORE MOVES");
1846 if (canMoveLeft && canMoveRight) {
1847 // choose random direction
1848 //!writeln(wtile.objId, ": CHOOSING RANDOM HDIR");
1849 // actually, choose direction that leads to hole in a ground
1850 if (waterCanReachGroundHoleInDir(wtile, -1)) {
1851 // can reach hole at the left side
1852 if (waterCanReachGroundHoleInDir(wtile, 1)) {
1853 // can reach hole at the right side, choose at random
1854 if (global.randOther(0, 1)) canMoveRight = false; else canMoveLeft = false;
1857 canMoveRight = false;
1860 // can't reach hole at the left side
1861 if (waterCanReachGroundHoleInDir(wtile, 1)) {
1862 // can reach hole at the right side, choose at random
1863 canMoveLeft = false;
1865 // no holes at any side, choose at random
1866 if (global.randOther(0, 1)) canMoveRight = false; else canMoveLeft = false;
1873 if (canMoveRight) FatalError("WATERCHECK: WTF RIGHT");
1874 //!writeln(wtile.objId, ": MOVING LEFT (", curWaterTileLastHDir, ")");
1875 curWaterTileLastHDir = -1;
1876 wtile.ix = wtile.ix-16;
1877 } else if (canMoveRight) {
1878 if (canMoveLeft) FatalError("WATERCHECK: WTF LEFT");
1879 //!writeln(wtile.objId, ": MOVING RIGHT (", curWaterTileLastHDir, ")");
1880 curWaterTileLastHDir = 1;
1881 wtile.ix = wtile.ix+16;
1889 wtile.setSprite(wtile.lava ? 'sLava' : 'sWater');
1890 wtile.waterMoved = true;
1891 // if this tile was not moved down, check if it can move down on any next step
1892 if (!wtile.waterMovedDown) {
1893 if (waterCanReachGroundHoleInDir(wtile, -1)) wtile.waterMovedDown = true;
1894 else if (waterCanReachGroundHoleInDir(wtile, 1)) wtile.waterMovedDown = true;
1898 return false; // don't remove
1900 //if (!isWetTileAtPix(tileX*16+8, tileY*16-8)) wtile.setSprite(wtile.lava ? 'sLavaTop' : 'sWaterTop');
1904 transient array!MapTile waterTilesList;
1906 final bool sortWaterTilesByCoordsLess (MapTile a, MapTile b) {
1908 if (dy) return (dy < 0);
1909 return (a.ix < b.ix);
1912 transient int waterFlowPause = 0;
1913 transient bool debugWaterFlowPause = false;
1915 final void cleanDeadObjects () {
1916 // remove dead objects
1917 if (deadItemsHead) {
1918 auto olddel = ImmediateDelete;
1919 ImmediateDelete = false;
1921 auto it = deadItemsHead;
1922 deadItemsHead = it.deadItemsNext;
1923 if (it.grid) it.grid.remove(it.gridId);
1926 } while (deadItemsHead);
1927 ImmediateDelete = olddel;
1928 if (olddel) CollectGarbage(true); // destroy delayed objects too
1932 final void cleanDeadTiles () {
1933 if (checkWater && /*global.lake == 1 ||*/ (!blockWaterChecking && liquidTileCount)) {
1934 if (global.lake == 1) fillGreatLake();
1935 if (waterFlowPause > 1) {
1940 if (debugWaterFlowPause) waterFlowPause = 4;
1941 //writeln("checking water");
1942 waterTilesList.clear();
1943 foreach (MapTile wtile; objGrid.allObjectsSafe(MapTile)) {
1944 if (wtile.water || wtile.lava) {
1946 if (wtile.ix%16 == 0 && wtile.iy%16 == 0) {
1947 wtile.waterMoved = false;
1948 wtile.waterMovedDown = false;
1949 wtile.waterSlideOldX = wtile.ix;
1950 wtile.waterSlideOldY = wtile.iy;
1951 waterTilesList[$] = wtile;
1956 liquidTileCount = 0;
1957 waterTilesList.sort(&sortWaterTilesByCoordsLess);
1959 bool wasAnyMove = false;
1960 bool wasAnyMoveDown = false;
1961 foreach (MapTile wtile; waterTilesList) {
1962 if (!wtile || !wtile.isInstanceAlive) continue;
1963 auto killIt = checkWaterFlow(wtile);
1967 wtile.instanceRemove(); // just in case
1969 wtile.saveInterpData();
1971 wasAnyMove = wasAnyMove || wtile.waterMoved;
1972 wasAnyMoveDown = wasAnyMoveDown || wtile.waterMovedDown;
1973 if (wtile.waterMoved && debugWaterFlowPause) wtile.waterSlideCounter = 4;
1977 liquidTileCount = 0;
1978 foreach (MapTile wtile; waterTilesList) {
1979 if (!wtile || !wtile.isInstanceAlive) continue;
1980 if (wasAnyMoveDown) {
1984 //checkWater = checkWater || wtile.waterMoved;
1985 curWaterTile = wtile;
1986 int tileX = wtile.ix/16, tileY = wtile.iy/16;
1987 // check if we are have no way to leak
1988 bool killIt = false;
1989 if (!isFullyOccupiedAtTilePos(tileX-1, tileY) || (wtile.water && curWaterTileCheckHitsLava)) {
1990 //writeln(" LEFT DEATH (", curWaterOccupiedCount, " : ", curWaterTileCheckHitsLava, ")");
1993 if (!killIt && (!isFullyOccupiedAtTilePos(tileX+1, tileY) || (wtile.water && curWaterTileCheckHitsLava))) {
1994 //writeln(" RIGHT DEATH (", curWaterOccupiedCount, " : ", curWaterTileCheckHitsLava, ")");
1997 if (!killIt && (!isFullyOccupiedAtTilePos(tileX, tileY+1) || (wtile.water && curWaterTileCheckHitsLava))) {
1998 //writeln(" DOWN DEATH (", curWaterOccupiedCount, " : ", curWaterTileCheckHitsLava, ")");
2005 wtile.instanceRemove(); // just in case
2010 if (wasAnyMove) checkWater = true;
2011 //writeln("water check: liquidTileCount=", liquidTileCount, "; checkWater=", checkWater, "; wasAnyMove=", wasAnyMove, "; wasAnyMoveDown=", wasAnyMoveDown);
2013 // fill empty spaces in lake with water
2021 // ////////////////////////////////////////////////////////////////////////// //
2022 private transient array!MapEntity postponedThinkers;
2023 private transient MapEntity thinkerHeld;
2024 private transient array!MapEntity activeThinkerList;
2027 final void doThinkActionsForObject (MapEntity o) {
2028 if (o.justSpawned) o.justSpawned = false;
2029 else if (o.imageSpeed > 0) o.nextAnimFrame();
2032 if (o.isInstanceAlive) {
2035 if (o.isInstanceAlive) {
2036 if (o.whipTimer > 0) --o.whipTimer;
2038 auto obj = MapObject(o);
2039 if (!o.canLiveOutsideOfLevel && (!obj || !obj.heldBy) && o.isOutsideOfLevel()) {
2040 // oops, fallen out of level...
2048 // return `true` if thinker should be removed
2049 final void thinkOne (MapEntity o, optional bool doHeldObject, optional bool dontAddHeldObject) {
2051 if (o == thinkerHeld && !doHeldObject) return; // skip it
2053 if (!o.isInstanceAlive) return;
2055 auto obj = MapObject(o);
2057 if (obj) obj.prevhp = obj.hp; // so i don't have to do it in `thinkFrame()`
2058 if (!o.active) return;
2060 if (obj && obj.heldBy == player) {
2061 // fix held item coords
2062 obj.fixHoldCoords();
2064 doThinkActionsForObject(o);
2066 if (!dontAddHeldObject) {
2068 foreach (MapEntity e; postponedThinkers) if (e == o) { found = true; break; }
2069 if (!found) postponedThinkers[$] = o;
2075 bool doThink = true;
2077 // collision with player weapon
2078 auto hh = PlayerWeapon(player.holdItem);
2079 bool doWeaponAction = false;
2081 if (hh.blockedBySolids && !global.config.killEnemiesThruWalls) {
2082 int xx = player.ix+(player.dir == MapObject::Dir.Left ? -16 : 16);
2083 //doWeaponAction = !isSolidAtPoint(xx, player.iy);
2084 doWeaponAction = !isSolidAtPoint(xx, hh.yCenter);
2086 int dh = max(1, hh.height-2);
2087 doWeaponAction = !checkTilesInRect(player.ix, player.iy);
2090 doWeaponAction = true;
2094 if (obj && doWeaponAction && hh && (o.whipTimer <= 0 || hh.ignoreWhipTimer) && hh.collidesWithObject(obj)) {
2095 //writeln("WEAPONED!");
2096 //writeln("weapon collides with '", GetClassName(o.Class), "' (", o.objType, "'");
2097 bool dontChangeWhipTimer = hh.dontChangeWhipTimer;
2098 if (!o.onTouchedByPlayerWeapon(player, hh)) {
2099 if (o.isInstanceAlive) hh.onCollisionWithObject(obj);
2101 if (!dontChangeWhipTimer) o.whipTimer = o.whipTimerValue; //HACK
2102 doThink = o.isInstanceAlive;
2105 if (doThink && o.isInstanceAlive) {
2106 doThinkActionsForObject(o);
2107 doThink = o.isInstanceAlive;
2110 // collision with player
2111 if (doThink && obj && o.collidesWith(player)) {
2112 if (!player.onObjectTouched(obj) && o.isInstanceAlive) {
2113 doThink = !o.onTouchedByPlayer(player);
2120 final void processThinkers (float timeDelta) {
2121 if (timeDelta <= 0) return;
2124 if (onBeforeFrame) onBeforeFrame(false);
2125 if (onAfterFrame) onAfterFrame(false);
2131 accumTime += timeDelta;
2132 bool wasFrame = false;
2134 auto olddel = ImmediateDelete;
2135 ImmediateDelete = false;
2136 while (accumTime >= FrameTime) {
2137 postponedThinkers.clear();
2139 accumTime -= FrameTime;
2140 if (onBeforeFrame) onBeforeFrame(accumTime >= FrameTime);
2142 if (shakeLeft > 0) {
2144 if (!shakeDir.x) shakeDir.x = (global.randOther(0, 1) ? -3 : 3);
2145 if (!shakeDir.y) shakeDir.y = (global.randOther(0, 1) ? -3 : 3);
2146 shakeOfs.x = shakeDir.x;
2147 shakeOfs.y = shakeDir.y;
2148 int sgnc = global.randOther(1, 3);
2149 if (sgnc&0x01) shakeDir.x = -shakeDir.x;
2150 if (sgnc&0x02) shakeDir.y = -shakeDir.y;
2159 // we don't want the time to grow too large
2160 if (time < 0) { time = 0; lastRenderTime = -1; }
2161 // game-global events
2163 // frame thinkers: player
2164 if (player && !disablePlayerThink) {
2166 if (!player.dead && isNormalLevel() &&
2167 (maxPlayingTime < 0 ||
2168 (maxPlayingTime > 0 && maxPlayingTime*30 <= time &&
2169 time%30 == 0 && global.randOther(1, 100) <= 20)))
2171 global.hasAnkh = false;
2173 player.invincible = 0;
2174 auto xplo = MapObjExplosion(MakeMapObject(player.ix, player.iy, 'oExplosion'));
2175 if (xplo) xplo.suicide = true;
2177 //HACK: check for stolen items
2178 auto item = MapItem(player.holdItem);
2179 if (item) item.onCheckItemStolen(player);
2180 item = MapItem(player.pickedItem);
2181 if (item) item.onCheckItemStolen(player);
2183 doThinkActionsForObject(player);
2185 // frame thinkers: held object
2186 thinkerHeld = player.holdItem;
2187 if (thinkerHeld && thinkerHeld.isInstanceAlive) {
2188 bool wasAct = thinkerHeld.active;
2189 thinkOne(thinkerHeld, doHeldObject:true);
2190 if (!thinkerHeld.isInstanceAlive) {
2191 if (player.holdItem == thinkerHeld) player.holdItem = none;
2192 thinkerHeld.grid.remove(thinkerHeld.gridId);
2194 thinkerHeld.onDestroy();
2197 } else if (!wasAct) {
2199 auto item = MapItem(thinkerHeld);
2201 if (item.forSale || item.sellOfferDone) {
2202 if (++item.forSaleFrame < 0) item.forSaleFrame = 0;
2207 // frame thinkers: objects
2208 activeThinkerList.clear();
2209 auto grid = objGrid;
2210 // collect active objects
2211 if (global.config.useFrozenRegion) {
2212 foreach (MapEntity e; grid.inRectPix(viewStart.x/global.scale-64, viewStart.y/global.scale-64, 320+64*2, 240+64*2, precise:false)) {
2213 if (e.active) activeThinkerList[$] = e;
2217 foreach (MapEntity e; grid.allObjects()) {
2218 if (e.active) activeThinkerList[$] = e;
2221 // process active objects
2222 //writeln("thinkers: ", activeThinkerList.length);
2223 foreach (MapEntity o; activeThinkerList) {
2225 thinkOne(o, doHeldObject:false);
2226 if (!o.isInstanceAlive) {
2227 //writeln("dead thinker: '", o.objType, "'");
2228 if (o.grid) o.grid.remove(o.gridId);
2229 auto obj = MapObject(o);
2230 if (obj && obj.heldBy) obj.heldBy.holdItem = none;
2237 // postponed thinkers
2238 foreach (MapEntity o; postponedThinkers) {
2240 thinkOne(o, doHeldObject:true, dontAddHeldObject:true);
2241 if (!o.isInstanceAlive) {
2242 //writeln("dead pp-thinker: '", o.objType, "'");
2249 postponedThinkers.clear();
2251 // clean dead things
2253 // fix held item coords
2254 if (player && player.holdItem) {
2255 if (player.holdItem.isInstanceAlive) {
2256 player.holdItem.fixHoldCoords();
2258 player.holdItem = none;
2262 if (collectCounter == 0) {
2263 xmoney = max(0, xmoney-100);
2269 if (!player.dead) stats.oneMoreFramePlayed();
2270 SoundSystem.ListenerOrigin = vector(player.xCenter, player.yCenter, -1);
2271 //writeln("plrpos=(", player.xCenter, ",", player.yCenter, "); lo=", SoundSystem.ListenerOrigin);
2273 if (onAfterFrame) onAfterFrame(accumTime >= FrameTime);
2274 ++framesProcessedFromLastClear;
2277 if (!player.visible && player.holdItem) player.holdItem.visible = false;
2278 if (winCutsceneSwitchToNext) {
2279 winCutsceneSwitchToNext = false;
2280 switch (++inWinCutscene) {
2281 case 2: startWinCutsceneVolcano(); break;
2282 case 3: default: startWinCutsceneWinFall(); break;
2286 if (playerExited) break;
2288 ImmediateDelete = olddel;
2290 playerExited = false;
2292 centerViewAtPlayer();
2295 // if we were processed at least one frame, collect garbage
2297 CollectGarbage(true); // destroy delayed objects too
2299 if (accumTime > 0 && onInterFrame) onInterFrame(accumTime/FrameTime);
2303 // ////////////////////////////////////////////////////////////////////////// //
2304 final void tileXY2roomXY (int tileX, int tileY, out int roomX, out int roomY) {
2305 roomX = (tileX-1)/RoomGen::Width;
2306 roomY = (tileY-1)/RoomGen::Height;
2310 final bool isInShop (int tileX, int tileY) {
2311 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
2312 auto n = roomType[tileX, tileY];
2313 if (n == 4 || n == 5) return true;
2314 return !!checkTilesInRect(tileX*16, tileY*16, 16, 16, delegate bool (MapTile t) { return t.shopWall; });
2315 //k8: we don't have this
2316 //if (t && t.objType == 'oShop') return true;
2322 // ////////////////////////////////////////////////////////////////////////// //
2323 override void Destroy () {
2325 delete tempSolidTile;
2330 // ////////////////////////////////////////////////////////////////////////// //
2331 // WARNING! delegate should not create/delete objects!
2332 final MapObject findNearestObject (int px, int py, scope bool delegate (MapObject o) dg, optional class!MapObject castClass) {
2333 MapObject res = none;
2334 if (!castClass) castClass = MapObject;
2335 int curdistsq = int.max;
2336 foreach (MapObject o; objGrid.allObjects(MapObject)) {
2337 if (o.spectral) continue;
2338 if (!dg(o)) continue;
2339 int xc = px-o.xCenter, yc = py-o.yCenter;
2340 int distsq = xc*xc+yc*yc;
2341 if (distsq < curdistsq) {
2350 // WARNING! delegate should not create/delete objects!
2351 final MapObject findNearestEnemy (int px, int py, optional scope bool delegate (MapEnemy o) dg, optional class!MapEnemy castClass) {
2352 if (!castClass) castClass = MapEnemy;
2353 if (castClass !isa MapEnemy) return none;
2354 MapObject res = none;
2355 int curdistsq = int.max;
2356 foreach (MapEnemy o; objGrid.allObjects(castClass)) {
2357 //k8: i added `dead` check
2358 if (o.spectral || o.dead) continue;
2360 if (!dg(o)) continue;
2362 int xc = px-o.xCenter, yc = py-o.yCenter;
2363 int distsq = xc*xc+yc*yc;
2364 if (distsq < curdistsq) {
2373 final MonsterShopkeeper findNearestCalmShopkeeper (int px, int py) {
2374 auto obj = MonsterShopkeeper(findNearestEnemy(px, py, delegate bool (MapEnemy o) {
2375 auto sk = MonsterShopkeeper(o);
2376 if (sk && !sk.angered) return true;
2378 }, castClass:MonsterShopkeeper));
2383 final MonsterShopkeeper hasAliveShopkeepers (optional bool skipAngry) {
2384 foreach (MonsterShopkeeper sc; objGrid.allObjects(MonsterShopkeeper)) {
2385 if (sc.spectral || sc.dead) continue;
2386 if (skipAngry && (sc.angered || sc.outlaw)) continue;
2393 // WARNING! delegate should not create/delete objects!
2394 final int calcNearestEnemyDist (int px, int py, optional scope bool delegate (MapEnemy o) dg, optional class!MapEnemy castClass) {
2395 auto e = findNearestEnemy(px, py, dg!optional, castClass!optional);
2396 if (!e) return int.max;
2397 int xc = px-e.xCenter, yc = py-e.yCenter;
2398 return round(sqrt(xc*xc+yc*yc));
2402 // WARNING! delegate should not create/delete objects!
2403 final int calcNearestObjectDist (int px, int py, optional scope bool delegate (MapObject o) dg, optional class!MapObject castClass) {
2404 auto e = findNearestObject(px, py, dg!optional, castClass!optional);
2405 if (!e) return int.max;
2406 int xc = px-e.xCenter, yc = py-e.yCenter;
2407 return round(sqrt(xc*xc+yc*yc));
2411 // WARNING! delegate should not create/delete objects!
2412 final MapTile findNearestMoveableSolid (int px, int py, optional scope bool delegate (MapTile t) dg) {
2414 int curdistsq = int.max;
2415 foreach (MapTile t; objGrid.allObjects(MapTile)) {
2416 if (t.spectral) continue;
2418 if (!dg(t)) continue;
2420 if (!t.solid || !t.moveable) continue;
2422 int xc = px-t.xCenter, yc = py-t.yCenter;
2423 int distsq = xc*xc+yc*yc;
2424 if (distsq < curdistsq) {
2433 // WARNING! delegate should not create/delete objects!
2434 final MapTile findNearestTile (int px, int py, optional scope bool delegate (MapTile t) dg) {
2435 if (!dg) return none;
2437 int curdistsq = int.max;
2439 //FIXME: make this faster!
2440 foreach (MapTile t; objGrid.allObjects(MapTile)) {
2441 if (t.spectral) continue;
2442 int xc = px-t.xCenter, yc = py-t.yCenter;
2443 int distsq = xc*xc+yc*yc;
2444 if (distsq < curdistsq && dg(t)) {
2454 // ////////////////////////////////////////////////////////////////////////// //
2455 final bool cbIsObjectWeb (MapObject o) { return (o isa ItemWeb); }
2456 final bool cbIsObjectBlob (MapObject o) { return (o isa EnemyBlob); }
2457 final bool cbIsObjectArrow (MapObject o) { return (o isa ItemProjectileArrow); }
2458 final bool cbIsObjectEnemy (MapObject o) { return (/*o != self &&*/ o isa MapEnemy); }
2460 final bool cbIsObjectTreasure (MapObject o) { return (o.isTreasure); }
2462 final bool cbIsItemObject (MapObject o) { return (o isa MapItem); }
2464 final bool cbIsItemOrEnemyObject (MapObject o) { return (o isa MapItem || o isa MapEnemy); }
2467 final MapObject isObjectAtTile (int tileX, int tileY, optional scope bool delegate (MapObject o) dg, optional bool precise) {
2468 if (!specified_precise) precise = true;
2471 foreach (MapObject o; objGrid.inRectPix(tileX, tileY, 16, 16, precise:precise, castClass:MapObject)) {
2472 if (o.spectral) continue;
2474 if (dg(o)) return o;
2483 final MapObject isObjectAtTilePix (int x, int y, optional scope bool delegate (MapObject o) dg) {
2484 return isObjectAtTile(x/16, y/16, dg!optional);
2488 final MapObject isObjectAtPoint (int xpos, int ypos, optional scope bool delegate (MapObject o) dg, optional bool precise, optional class!MapObject castClass) {
2489 if (!specified_precise) precise = true;
2490 if (!castClass) castClass = MapObject;
2491 foreach (MapObject o; objGrid.inCellPix(xpos, ypos, precise:precise, castClass:castClass)) {
2492 if (o.spectral) continue;
2494 if (dg(o)) return o;
2496 if (o isa MapEnemy) return o;
2503 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) {
2504 if (w < 1 || h < 1) return none;
2505 if (!castClass) castClass = MapObject;
2506 if (w == 1 && h == 1) return isObjectAtPoint(xpos, ypos, dg!optional, precise!optional, castClass);
2507 if (!specified_precise) precise = true;
2508 foreach (MapObject o; objGrid.inRectPix(xpos, ypos, w, h, precise:precise, castClass:castClass)) {
2509 if (o.spectral) continue;
2511 if (dg(o)) return o;
2513 if (o isa MapEnemy) return o;
2520 final MapObject forEachObject (scope bool delegate (MapObject o) dg, optional bool allowSpectrals, optional class!MapObject castClass) {
2521 if (!dg) return none;
2522 if (!castClass) castClass = MapObject;
2523 foreach (MapObject o; objGrid.allObjectsSafe(castClass)) {
2524 if (!allowSpectrals && o.spectral) continue;
2525 if (dg(o)) return o;
2531 final MapObject forEachObjectAtPoint (int xpos, int ypos, scope bool delegate (MapObject o) dg, optional bool precise) {
2532 if (!dg) return none;
2533 if (!specified_precise) precise = true;
2534 foreach (MapObject o; objGrid.inCellPix(xpos, ypos, precise:precise, castClass:MapObject)) {
2535 if (o.spectral) continue;
2536 if (dg(o)) return o;
2542 final MapObject forEachObjectInRect (int xpos, int ypos, int w, int h, scope bool delegate (MapObject o) dg, optional bool precise) {
2543 if (!dg || w < 1 || h < 1) return none;
2544 if (w == 1 && h == 1) return forEachObjectAtPoint(xpos, ypos, dg!optional, precise!optional);
2545 if (!specified_precise) precise = true;
2546 foreach (MapObject o; objGrid.inRectPix(xpos, ypos, w, h, precise:precise, castClass:MapObject)) {
2547 if (o.spectral) continue;
2548 if (dg(o)) return o;
2554 final MapEntity forEachEntityInRect (int xpos, int ypos, int w, int h, scope bool delegate (MapEntity o) dg, optional bool precise, optional class!MapEntity castClass) {
2555 if (!dg || w < 1 || h < 1) return none;
2556 if (!castClass) castClass = MapEntity;
2557 if (!specified_precise) precise = true;
2558 foreach (MapEntity e; objGrid.inRectPix(xpos, ypos, w, h, precise:precise, castClass:castClass)) {
2559 if (e.spectral) continue;
2560 if (dg(e)) return e;
2566 private final bool cbIsRopeTile (MapTile t) { return (t isa MapTileRope); }
2568 final MapTile isRopeAtPoint (int px, int py) {
2569 return checkTileAtPoint(px, py, &cbIsRopeTile);
2574 final MapTile isWaterSwimAtPoint (int px, int py) {
2575 return isWaterAtPoint(px, py);
2579 // ////////////////////////////////////////////////////////////////////////// //
2580 private array!MapEntity tmpEntityList;
2582 private final bool cbCollectEntitiesWithMask (MapEntity t) {
2583 if (!t.visible || t.spectral) return false;
2584 tmpEntityList[$] = t;
2589 final void touchEntitiesWithMask (int x, int y, SpriteFrame frm, scope bool delegate (MapEntity t) dg, optional class!MapEntity castClass) {
2590 if (!frm || frm.bw < 1 || frm.bh < 1 || !dg) return;
2591 if (frm.isEmptyPixelMask) return;
2592 if (!castClass) castClass = MapEntity;
2594 if (tmpEntityList.length) tmpEntityList.clear();
2595 if (player isa castClass && player.isRectCollisionFrame(frm, x, y)) tmpEntityList[$] = player;
2596 forEachEntityInRect(x+frm.bx, y+frm.by, frm.bw, frm.bh, &cbCollectEntitiesWithMask, castClass:castClass);
2597 foreach (MapEntity e; tmpEntityList) {
2598 if (!e || !e.isInstanceAlive || !e.visible || e.spectral) continue;
2599 if (e.isRectCollisionFrame(frm, x, y)) {
2606 // ////////////////////////////////////////////////////////////////////////// //
2607 final bool cbCollisionAnySolid (MapTile t) { return t.solid; }
2608 final bool cbCollisionAnyMoveableSolid (MapTile t) { return t.solid && t.moveable; }
2609 final bool cbCollisionLiquid (MapTile t) { return (t.water || t.lava); }
2610 final bool cbCollisionAnySolidOrLiquid (MapTile t) { return (t.solid || t.water || t.lava); }
2611 final bool cbCollisionLadder (MapTile t) { return t.ladder; }
2612 final bool cbCollisionLadderTop (MapTile t) { return t.laddertop; }
2613 final bool cbCollisionAnyLadder (MapTile t) { return (t.ladder || t.laddertop); }
2614 final bool cbCollisionAnySolidIce (MapTile t) { return (t.solid && t.ice); }
2615 final bool cbCollisionWater (MapTile t) { return t.water; }
2616 final bool cbCollisionLava (MapTile t) { return t.lava; }
2617 final bool cbCollisionAnyTree (MapTile t) { return t.tree; }
2618 final bool cbCollisionPlatform (MapTile t) { return t.platform; }
2619 final bool cbCollisionLadderOrPlatform (MapTile t) { return (t.ladder || t.platform); }
2620 //final bool cbCollisionAnyHangeable (MapTile t) { return t.hangeable; } // no need to check for solids here, it is done in MapTile initializer
2621 final bool cbCollisionSpringTrap (MapTile t) { return t.springtrap; }
2622 final bool cbCollisionSpikes (MapTile t) { return t.spikes; }
2623 final bool cbCollisionExitTile (MapTile t) { return t.isExitActive(); }
2625 final bool cbCollisionForWhoa (MapTile t) { return (t.solid || t.laddertop); }
2627 //final bool cbCollisionSacAltar (MapTile t) { return (t.objName == 'oSacAltarLeft'); } // only left part, yeah
2628 final bool cbCollisionSacAltar (MapTile t) { return t.sacrificingAltar; }
2631 // ////////////////////////////////////////////////////////////////////////// //
2632 transient MapTileTemp tempSolidTile;
2634 private final MapTileTemp makeWalkeableSolidTile (MapObject o) {
2635 if (!tempSolidTile) {
2636 tempSolidTile = SpawnObject(MapTileTemp);
2637 } else if (!tempSolidTile.isInstanceAlive) {
2638 delete tempSolidTile;
2639 tempSolidTile = SpawnObject(MapTileTemp);
2642 tempSolidTile.level = self;
2643 tempSolidTile.global = global;
2644 tempSolidTile.solid = true;
2645 tempSolidTile.objName = MapTileTemp.default.objName;
2646 tempSolidTile.objType = MapTileTemp.default.objType;
2647 tempSolidTile.e = o;
2648 tempSolidTile.fltx = o.fltx;
2649 tempSolidTile.flty = o.flty;
2650 return tempSolidTile;
2654 final MapTile checkTilesInRect (int x0, int y0, const int w, const int h,
2655 optional scope bool delegate (MapTile dg) dg, optional bool precise,
2656 optional class!MapTile castClass)
2658 if (w < 1 || h < 1) return none;
2659 if (w == 1 && h == 1) return checkTileAtPoint(x0, y0, dg!optional);
2660 int x1 = x0+w-1, y1 = y0+h-1;
2661 if (x0 >= tilesWidth*16 || y0 >= tilesHeight*16 || x1 < 0 || y1 < 0) return none;
2662 if (!specified_precise) precise = true;
2663 if (!castClass) castClass = MapTile;
2664 if (!dg) dg = &cbCollisionAnySolid;
2666 // check walkable solid objects too
2667 foreach (MapEntity e; objGrid.inRectPix(x0, y0, w, h, precise:precise, castClass:castClass)) {
2668 if (e.spectral || !e.visible) continue;
2669 auto t = MapTile(e);
2671 if (dg(t)) return t;
2674 auto o = MapObject(e);
2675 if (o && o.walkableSolid) {
2676 t = makeWalkeableSolidTile(o);
2677 if (dg(t)) return t;
2686 final MapTile checkTileAtPoint (int x0, int y0, optional scope bool delegate (MapTile dg) dg, optional bool precise, optional class!MapTile castClass) {
2687 if (x0 < 0 || y0 < 0 || x0 >= tilesWidth*16 || y0 >= tilesHeight*16) return none;
2688 if (!specified_precise) precise = true;
2689 if (!castClass) castClass = MapTile;
2690 if (!dg) dg = &cbCollisionAnySolid;
2692 // check walkable solid objects
2693 foreach (MapEntity e; objGrid.inCellPix(x0, y0, precise:precise, castClass:castClass)) {
2694 if (e.spectral || !e.visible) continue;
2695 auto t = MapTile(e);
2697 if (dg(t)) return t;
2700 auto o = MapObject(e);
2701 if (o && o.walkableSolid) {
2702 t = makeWalkeableSolidTile(o);
2703 if (dg(t)) return t;
2712 // ////////////////////////////////////////////////////////////////////////// //
2713 final MapTile isSolidAtPoint (int px, int py) { return checkTileAtPoint(px, py); }
2714 final MapTile isSolidInRect (int rx, int ry, int rw, int rh) { return checkTilesInRect(rx, ry, rw, rh); }
2715 final MapTile isLadderAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadder); }
2716 final MapTile isLadderOrPlatformAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadderOrPlatform); }
2717 final MapTile isLadderTopAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadderTop); }
2718 final MapTile isAnyLadderAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyLadder); }
2719 final MapTile isWaterAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionWater); }
2720 final MapTile isLavaAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLava); }
2721 final MapTile isLiquidAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLiquid); }
2722 final MapTile isIceAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnySolidIce); }
2723 final MapTile isTreeAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyTree); }
2724 //final MapTile isHangeableAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyHangeable); }
2727 // ////////////////////////////////////////////////////////////////////////// //
2728 final void setTileTypeAt (int tileX, int tileY, LevelGen::RType rt) {
2729 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) roomType[tileX, tileY] = rt;
2733 //FIXME: make this faster
2734 transient float gtagX, gtagY;
2736 // only non-moveables and non-specials
2737 final MapTile getTileAtGrid (int tileX, int tileY) {
2740 return checkTileAtPoint(tileX*16, tileY*16, delegate bool (MapTile t) {
2741 if (t.spectral || t.moveable || t.toSpecialGrid || !t.visible) return false;
2742 if (t.fltx != gtagX || t.flty != gtagY) return false; // emulate grid
2743 if (t.width != 16 || t.height != 16) return false;
2746 //return (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight ? tiles[tileX, tileY] : none);
2750 final MapTile getTileAtGridAny (int tileX, int tileY) {
2753 return checkTileAtPoint(tileX*16, tileY*16, delegate bool (MapTile t) {
2754 if (t.spectral /*|| t.moveable*/ || !t.visible) return false;
2755 if (t.fltx != gtagX || t.flty != gtagY) return false; // emulate grid
2756 if (t.width != 16 || t.height != 16) return false;
2759 //return (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight ? tiles[tileX, tileY] : none);
2763 final bool isNamedTileAt (int tileX, int tileY, name atypename) {
2764 if (!atypename) return false;
2765 auto t = getTileAtGridAny(tileX, tileY);
2766 return (t && t.objName == atypename);
2770 final void setTileAtGrid (int tileX, int tileY, MapTile tile) {
2771 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
2773 tile.fltx = tileX*16;
2774 tile.flty = tileY*16;
2775 if (!tile.dontReplaceOthers) {
2776 auto osp = tile.spectral;
2777 tile.spectral = true;
2778 auto t = getTileAtGridAny(tileX, tileY);
2779 tile.spectral = osp;
2780 if (t && !t.immuneToReplacement) {
2781 writeln("REPLACING tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
2782 writeln(" NEW tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (tile.toSpecialGrid ? "special " : ""), "grid, pos=(", tile.fltx/16.0, ",", tile.flty/16.0, ")");
2788 auto t = getTileAtGridAny(tileX, tileY);
2789 if (t && !t.immuneToReplacement) {
2790 writeln("REMOVING tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
2798 // ////////////////////////////////////////////////////////////////////////// //
2799 // return `true` from delegate to stop
2800 MapTile forEachSolidTileOnGrid (scope bool delegate (int tileX, int tileY, MapTile t) dg) {
2801 if (!dg) return none;
2802 foreach (MapTile t; objGrid.allObjectsSafe(MapTile)) {
2803 if (t.spectral || !t.solid || !t.visible) continue;
2804 if (t.ix%16 != 0 || t.iy%16 != 0) continue; // emulate grid
2805 if (t.width != 16 || t.height != 16) continue;
2806 if (dg(t.ix/16, t.iy/16, t)) return t;
2812 // ////////////////////////////////////////////////////////////////////////// //
2813 // return `true` from delegate to stop
2814 MapTile forEachTile (scope bool delegate (MapTile t) dg, optional class!MapTile castClass) {
2815 if (!dg) return none;
2816 if (!castClass) castClass = MapTile;
2817 foreach (MapTile t; objGrid.allObjectsSafe(castClass)) {
2818 if (t.spectral || !t.visible) continue;
2819 if (dg(t)) return t;
2825 // ////////////////////////////////////////////////////////////////////////// //
2826 final void fixWallTiles () {
2827 foreach (MapTile t; objGrid.allObjectsSafe(MapTile)) t.beautifyTile();
2831 // ////////////////////////////////////////////////////////////////////////// //
2832 final MapTile isCollisionAtPoint (int px, int py, optional scope bool delegate (MapTile dg) dg) {
2833 if (!dg) dg = &cbCollisionAnySolid;
2834 return checkTilesInRect(px, py, 1, 1, dg);
2838 // ////////////////////////////////////////////////////////////////////////// //
2839 string scrGetKaliGift (MapTile altar, optional name gift) {
2842 // find other side of the altar
2843 int sx = player.ix, sy = player.iy;
2847 auto a2 = isCollisionAtPoint(altar.ix+18, altar.iy+8, &cbCollisionSacAltar);
2848 if (!a2) a2 = isCollisionAtPoint(altar.ix-8, altar.iy+8, &cbCollisionSacAltar);
2849 if (a2) { sx = a2.ix; sy = a2.iy; }
2852 if (global.favor <= -8) res = "SHE SEEMS VERY ANGRY WITH YOU!";
2853 else if (global.favor < 0) res = "SHE SEEMS ANGRY WITH YOU.";
2854 else if (global.favor == 0) res = "SHE HAS FORGIVEN YOU!";
2855 else if (global.favor >= 32) {
2856 if (global.kaliGift >= 3 && global.favor >= 32+(global.kaliGift-2)*16) {
2857 res = "YOU FEEL INVIGORATED!";
2858 global.kaliGift += 1;
2859 global.plife += global.randOther(4, 8);
2860 } else if (global.kaliGift >= 3) {
2861 res = "SHE SEEMS ECSTATIC WITH YOU!";
2862 } else if (global.bombs < 80) {
2863 res = "YOUR SATCHEL FEELS VERY FULL NOW!";
2864 global.kaliGift = 3;
2867 res = "YOU FEEL INVIGORATED!";
2868 global.kaliGift += 1;
2869 global.plife += global.randOther(4, 8);
2871 } else if (global.favor >= 16) {
2872 if (global.kaliGift >= 2) {
2873 res = "SHE SEEMS VERY HAPPY WITH YOU!";
2875 res = "SHE BESTOWS A GIFT UPON YOU!";
2876 global.kaliGift = 2;
2878 auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2881 obj = MakeMapObject(sx, sy-8, 'oPoof');
2886 if (gift) obj = MakeMapObject(sx, sy-8, gift);
2887 if (!obj) obj = MakeMapObject(sx, sy-8, 'oKapala');
2889 } else if (global.favor >= 8) {
2890 if (global.kaliGift >= 1) {
2891 res = "SHE SEEMS HAPPY WITH YOU.";
2893 res = "SHE BESTOWS A GIFT UPON YOU!";
2894 global.kaliGift = 1;
2895 //rAltar = instance_nearest(x, y, oSacAltarRight);
2896 //if (instance_exists(rAltar)) {
2898 auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2901 obj = MakeMapObject(sx, sy-8, 'oPoof');
2905 if (gift) obj = MakeMapObject(sx, sy-8, gift);
2907 auto n = global.randOther(1, 8);
2911 if (n == 1 && !global.hasCape && !global.hasJetpack) aname = 'oCapePickup';
2912 else if (n == 2 && !global.hasGloves) aname = 'oGloves';
2913 else if (n == 3 && !global.hasSpectacles) aname = 'oSpectacles';
2914 else if (n == 4 && !global.hasMitt) aname = 'oMitt';
2915 else if (n == 5 && !global.hasSpringShoes) aname = 'oSpringShoes';
2916 else if (n == 6 && !global.hasSpikeShoes) aname = 'oSpikeShoes';
2917 else if (n == 7 && !global.hasStickyBombs) aname = 'oPaste';
2918 else if (n == 8 && !global.hasCompass) aname = 'oCompass';
2920 obj = MakeMapObject(sx, sy-8, aname);
2926 obj = MakeMapObject(sx, sy-8, (global.hasJetpack ? 'oBombBox' : 'oJetpack'));
2932 } else if (global.favor > 0) {
2933 res = "SHE SEEMS PLEASED WITH YOU.";
2938 global.message = "";
2939 res = "KALI DEVOURS YOU!"; // sacrifice is player
2947 void performSacrifice (MapObject what, MapTile where) {
2948 if (!what || !what.isInstanceAlive) return;
2949 MakeMapObject(what.ix+8, what.iy+8, 'oFlame');
2950 if (where) where.playSound('sndSmallExplode'); else what.playSound('sndSmallExplode');
2951 what.spillBlood(amount:3, forced:true);
2953 string msg = "KALI ACCEPTS THE SACRIFICE!";
2955 auto idol = ItemGoldIdol(what);
2957 ++stats.totalSacrifices;
2958 if (global.favor <= -8) msg = "SHE SEEMS VERY ANGRY WITH YOU!";
2959 else if (global.favor < 0) msg = "SHE SEEMS ANGRY WITH YOU.";
2960 else if (global.favor >= 0) {
2961 // find other side of the altar
2962 int sx = player.ix, sy = player.iy;
2967 auto a2 = isCollisionAtPoint(altar.ix+18, altar.iy+8, &cbCollisionSacAltar);
2968 if (!a2) a2 = isCollisionAtPoint(altar.ix-8, altar.iy+8, &cbCollisionSacAltar);
2969 if (a2) { sx = a2.ix; sy = a2.iy; }
2972 auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2975 obj = MakeMapObject(sx, sy-8, 'oPoof');
2979 obj = MakeMapObject(sx, sy-16-8, 'oMonkeyGold');
2981 osdMessage(msg, 6.66);
2983 idol.instanceRemove();
2987 if (global.favor <= -8) {
2988 msg = "KALI DEVOURS THE SACRIFICE!";
2989 } else if (global.favor < 0) {
2990 global.favor += (what.status == MapObject::STUNNED ? what.favor : round(what.favor/2.0)); //k8: added `round()`
2991 if (what.favor > 0) what.favor = 0;
2993 global.favor += (what.status == MapObject::STUNNED ? what.favor : round(what.favor/2.0)); //k8: added `round()`
2997 if (myAltar.gift1 != "" and global.kaliGift == 0) scrGetKaliGift(myAltar.gift1);
2998 else if (myAltar.gift2 != "" and global.kaliGift == 1) scrGetKaliGift(myAltar.gift2);
2999 else scrGetKaliGift("");
3002 // sacrifice is player?
3003 if (what isa PlayerPawn) {
3004 ++stats.totalSelfSacrifices;
3005 msg = "KALI DEVOURS YOU!";
3006 player.visible = false;
3007 player.removeBallAndChain(temp:true);
3009 player.status = MapObject::DEAD;
3011 ++stats.totalSacrifices;
3012 auto msg2 = scrGetKaliGift(where);
3013 what.instanceRemove();
3014 if (msg2) msg = va("%s\n%s", msg, msg2);
3017 osdMessage(msg, 6.66);
3023 // ////////////////////////////////////////////////////////////////////////// //
3024 final void addBackgroundGfxDetails () {
3025 // add background details
3026 //if (global.customLevel) return;
3028 // bg = instance_create(16*randRoom(1, 42), 16*randRoom(1, 33), oCaveBG);
3029 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);
3030 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);
3031 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);
3032 else MakeMapBackTile('bgExtras', 32*global.randRoom(0, 1), 0, 32, 32, 16*global.randRoom(1, 42), 16*global.randRoom(1, 33), 10002);
3037 // ////////////////////////////////////////////////////////////////////////// //
3038 private final void fixRealViewStart () {
3039 int scale = global.scale;
3040 realViewStart.x = clamp(realViewStart.x, viewMin.x*scale, viewMax.x*scale-viewWidth);
3041 realViewStart.y = clamp(realViewStart.y, viewMin.y*scale, viewMax.y*scale-viewHeight);
3045 final int cameraCurrX () { return realViewStart.x/global.scale; }
3046 final int cameraCurrY () { return realViewStart.y/global.scale; }
3049 private final void fixViewStart () {
3050 int scale = global.scale;
3051 viewStart.x = clamp(viewStart.x, viewMin.x*scale, viewMax.x*scale-viewWidth);
3052 viewStart.y = clamp(viewStart.y, viewMin.y*scale, viewMax.y*scale-viewHeight);
3056 final void centerViewAtPlayer () {
3057 if (viewWidth < 1 || viewHeight < 1 || !player) return;
3058 centerViewAt(player.xCenter, player.yCenter);
3062 final void centerViewAt (int x, int y) {
3063 if (viewWidth < 1 || viewHeight < 1) return;
3065 cameraSlideToSpeed.x = 0;
3066 cameraSlideToSpeed.y = 0;
3067 cameraSlideToPlayer = 0;
3069 int scale = global.scale;
3072 realViewStart.x = clamp(x-viewWidth/2, 0, tilesWidth*16*scale-viewWidth);
3073 realViewStart.y = clamp(y-viewHeight/2, 0, tilesHeight*16*scale-viewHeight);
3076 viewStart.x = realViewStart.x;
3077 viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
3080 if (onCameraTeleported) onCameraTeleported();
3084 const int ViewPortToleranceX = 16*1+8;
3085 const int ViewPortToleranceY = 16*1+8;
3087 final void fixCamera () {
3088 if (!player) return;
3089 if (viewWidth < 1 || viewHeight < 1) return;
3090 int scale = global.scale;
3091 auto alwaysCenterX = global.config.alwaysCenterPlayer;
3092 auto alwaysCenterY = alwaysCenterX;
3093 // calculate offset from viewport center (in game units), and fix viewport
3095 int camDestX = player.ix+8;
3096 int camDestY = player.iy+8;
3097 if (!cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y) != 0) {
3098 // slide camera to point
3099 if (cameraSlideToSpeed.x) camDestX = cameraSlideToCurr.x;
3100 if (cameraSlideToSpeed.y) camDestY = cameraSlideToCurr.y;
3101 int dx = cameraSlideToDest.x-camDestX;
3102 int dy = cameraSlideToDest.y-camDestY;
3103 //writeln("speed:(", cameraSlideToSpeed.x, ",", cameraSlideToSpeed.y, "); cur:(", camDestX, ",", camDestY, "); dest:(", cameraSlideToDest.x, ",", cameraSlideToDest.y, "); delta=(", dx, ",", dy, ")");
3104 if (dx && cameraSlideToSpeed.x != 0) {
3105 alwaysCenterX = true;
3106 if (abs(dx) <= abs(cameraSlideToSpeed.x)) {
3107 camDestX = cameraSlideToDest.x;
3109 camDestX += sign(dx)*abs(cameraSlideToSpeed.x);
3112 if (dy && abs(cameraSlideToSpeed.y) != 0) {
3113 alwaysCenterY = true;
3114 if (abs(dy) <= cameraSlideToSpeed.y) {
3115 camDestY = cameraSlideToDest.y;
3117 camDestY += sign(dy)*abs(cameraSlideToSpeed.y);
3120 //writeln(" new:(", camDestX, ",", camDestY, ")");
3121 if (cameraSlideToSpeed.x) cameraSlideToCurr.x = camDestX;
3122 if (cameraSlideToSpeed.y) cameraSlideToCurr.y = camDestY;
3126 if (cameraSlideToSpeed.x && !cameraSlideToPlayer) {
3127 realViewStart.x = clamp(camDestX*scale, 0, tilesWidth*16*scale-viewWidth);
3128 } else if (!player.cameraBlockX) {
3129 int x = camDestX*scale;
3130 int cx = realViewStart.x;
3131 if (alwaysCenterX) {
3134 int xofs = x-(cx+viewWidth/2);
3135 if (xofs < -ViewPortToleranceX*scale) cx += xofs+ViewPortToleranceX*scale;
3136 else if (xofs > ViewPortToleranceX*scale) cx += xofs-ViewPortToleranceX*scale;
3138 // slide back to player?
3139 if (cameraSlideToPlayer && cameraSlideToSpeed.x) {
3140 int prevx = cameraSlideToCurr.x*scale;
3141 int dx = (cx-prevx)/scale;
3142 if (abs(dx) <= cameraSlideToSpeed.x) {
3143 writeln("BACKSLIDE X COMPLETE!");
3144 cameraSlideToSpeed.x = 0;
3146 cx = prevx+(sign(dx)*cameraSlideToSpeed.x)*scale;
3147 cameraSlideToCurr.x += sign(dx)*cameraSlideToSpeed.x;
3148 if ((dx < 0 && cx <= 0) || (dx > 0 && cx >= tilesWidth*16*scale-viewWidth)) {
3149 writeln("BACKSLIDE X COMPLETE!");
3150 cameraSlideToSpeed.x = 0;
3154 realViewStart.x = clamp(cx, 0, tilesWidth*16*scale-viewWidth);
3158 if (cameraSlideToSpeed.y && !cameraSlideToPlayer) {
3159 realViewStart.y = clamp(camDestY*scale, 0, tilesHeight*16*scale-viewHeight);
3160 } else if (!player.cameraBlockY) {
3161 int y = camDestY*scale;
3162 int cy = realViewStart.y;
3163 if (alwaysCenterY) {
3164 cy = y-viewHeight/2;
3166 int yofs = y-(cy+viewHeight/2);
3167 if (yofs < -ViewPortToleranceY*scale) cy += yofs+ViewPortToleranceY*scale;
3168 else if (yofs > ViewPortToleranceY*scale) cy += yofs-ViewPortToleranceY*scale;
3170 // slide back to player?
3171 if (cameraSlideToPlayer && cameraSlideToSpeed.y) {
3172 int prevy = cameraSlideToCurr.y*scale;
3173 int dy = (cy-prevy)/scale;
3174 if (abs(dy) <= cameraSlideToSpeed.y) {
3175 writeln("BACKSLIDE Y COMPLETE!");
3176 cameraSlideToSpeed.y = 0;
3178 cy = prevy+(sign(dy)*cameraSlideToSpeed.y)*scale;
3179 cameraSlideToCurr.y += sign(dy)*cameraSlideToSpeed.y;
3180 if ((dy < 0 && cy <= 0) || (dy > 0 && cy >= tilesHeight*16*scale-viewHeight)) {
3181 writeln("BACKSLIDE Y COMPLETE!");
3182 cameraSlideToSpeed.y = 0;
3186 realViewStart.y = clamp(cy, 0, tilesHeight*16*scale-viewHeight);
3189 if (cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y) == 0) cameraSlideToPlayer = 0;
3192 //writeln(" new2:(", cameraCurrX, ",", cameraCurrY, ")");
3194 viewStart.x = realViewStart.x;
3195 viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
3200 // ////////////////////////////////////////////////////////////////////////// //
3201 // x0 and y0 are non-scaled (and will be scaled)
3202 final void drawSpriteAt (name sprName, float frnumf, int x0, int y0, optional bool small) {
3203 if (!sprName) return;
3204 auto spr = sprStore[sprName];
3205 if (!spr || !spr.frames.length) return;
3206 int scale = global.scale;
3209 int frnum = max(0, trunc(frnumf))%spr.frames.length;
3210 auto sfr = spr.frames[frnum];
3211 int sx0 = x0-sfr.xofs*scale;
3212 int sy0 = y0-sfr.yofs*scale;
3213 if (small && scale > 1) {
3214 sfr.tex.blitExt(sx0, sy0, round(sx0+sfr.tex.width*(scale/2.0)), round(sy0+sfr.tex.height*(scale/2.0)), 0, 0);
3216 sfr.tex.blitAt(sx0, sy0, scale);
3221 final void drawSpriteAtS3 (name sprName, float frnumf, int x0, int y0) {
3222 if (!sprName) return;
3223 auto spr = sprStore[sprName];
3224 if (!spr || !spr.frames.length) return;
3227 int frnum = max(0, trunc(frnumf))%spr.frames.length;
3228 auto sfr = spr.frames[frnum];
3229 int sx0 = x0-sfr.xofs*3;
3230 int sy0 = y0-sfr.yofs*3;
3231 sfr.tex.blitAt(sx0, sy0, 3);
3235 // x0 and y0 are non-scaled (and will be scaled)
3236 final void drawTextAt (int x0, int y0, string text, optional int scale, optional int hiColor1, optional int hiColor2) {
3238 if (!specified_scale) scale = global.scale;
3241 sprStore.renderTextWithHighlight(x0, y0, text, scale, hiColor1!optional, hiColor2!optional);
3245 void renderCompass (float currFrameDelta) {
3246 if (!global.hasCompass) return;
3249 if (isRoom("rOlmec")) {
3252 } else if (isRoom("rOlmec2")) {
3258 bool hasMessage = osdHasMessage();
3259 foreach (MapTile et; allExits) {
3261 int exitX = et.ix, exitY = et.iy;
3262 int vx0 = viewStart.x/global.scale, vy0 = viewStart.y/global.scale;
3263 int vx1 = (viewStart.x+viewWidth)/global.scale;
3264 int vy1 = (viewStart.y+viewHeight)/global.scale;
3265 if (exitY > vy1-16) {
3267 drawSpriteAt(hasMessage ? 'sCompassSmallLL' : 'sCompassLL', 0, 0, 224);
3268 } else if (exitX > vx1-16) {
3269 drawSpriteAt(hasMessage ? 'sCompassSmallLR' : 'sCompassLR', 0, 304, 224);
3271 drawSpriteAt(hasMessage ? 'sCompassSmallDown' : 'sCompassDown', 0, exitX-vx0, 224);
3273 } else if (exitX < vx0) {
3274 drawSpriteAt(hasMessage ? 'sCompassSmallLeft' : 'sCompassLeft', 0, 0, exitY-vy0);
3275 } else if (exitX > vx1-16) {
3276 drawSpriteAt(hasMessage ? 'sCompassSmallRight' : 'sCompassRight', 0, 304, exitY-vy0);
3278 break; // only the first exit
3283 final bool totalsNameCmpCB (ref GameStats::TotalItem a, ref GameStats::TotalItem b) {
3284 auto sa = string(a.objName);
3285 auto sb = string(b.objName);
3289 void renderTransitionInfo (float currFrameDelta) {
3292 GameStats.sortTotalsList(stats.kills, &totalsNameCmpCB);
3295 foreach (int idx, ref auto k; stats.kills) {
3296 string s = string(k);
3297 maxLen = max(maxLen, s.length);
3301 sprStore.loadFont('sFontSmall');
3302 Video.color = 0xff_ff_00;
3303 foreach (int idx, ref auto k; stats.kills) {
3305 foreach (int xidx, ref auto d; stats.totalKills) {
3306 if (d.objName == k) { deaths = d.count; break; }
3308 //drawTextAt(16, 4+idx*8, va("%s: %d (%d)", string(k.objName).toUpperCase, k.count, deaths));
3309 drawTextAt(16, 4+idx*8, string(k).toUpperCase);
3310 drawTextAt(16+maxLen, 4+idx*8, va(": %d (%d)", k.count, deaths));
3316 void renderGhostTimer (float currFrameDelta) {
3317 if (ghostTimeLeft <= 0) return;
3318 //ghostTimeLeft /= 30; // frames -> seconds
3320 int hgt = viewHeight-64;
3321 if (hgt < 1) return;
3322 int rhgt = round(float(hgt)*(float(ghostTimeLeft)/30.0)/1000.0);
3323 //writeln("hgt=", hgt, "; rhgt=", rhgt, "; gtl=", ghostTimeLeft/30);
3325 auto oclr = Video.color;
3326 Video.color = 0xcf_ff_7f_00;
3327 Video.fillRect(viewWidth-20, 32, 16, hgt-rhgt);
3328 Video.color = 0x7f_ff_7f_00;
3329 Video.fillRect(viewWidth-20, 32+(hgt-rhgt), 16, rhgt);
3335 void renderStarsHUD (float currFrameDelta) {
3336 bool scumSmallHud = global.config.scumSmallHud;
3338 //auto life = max(0, global.plife);
3339 int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3340 int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3341 //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3346 sprStore.loadFont('sFontSmall');
3349 sprStore.loadFont('sFont');
3353 Video.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3354 //draw_sprite(sHeart, -1, view_xview[0]+8, view_yview[0]+8);
3355 //draw_text(view_xview[0]+24, view_yview[0]+8, life);
3357 if (global.plife == 1) {
3358 drawSpriteAt('sHeartSmallBlink', global.heartBlink, 10, hhup-4);
3359 global.heartBlink += 0.1;
3360 if (global.heartBlink > 3) global.heartBlink = 0;
3362 drawSpriteAt('sHeartSmall', -1, 10, hhup-4);
3363 global.heartBlink = 0;
3366 if (global.plife == 1) {
3367 drawSpriteAt('sHeartBlink', global.heartBlink, 8, hhup);
3368 global.heartBlink += 0.1;
3369 if (global.heartBlink > 3) global.heartBlink = 0;
3371 drawSpriteAt('sHeart', -1, 8, hhup);
3372 global.heartBlink = 0;
3375 int life = clamp(global.plife, 0, 99);
3376 drawTextAt(16+8, hhup, va("%d", life));
3378 //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3379 drawSpriteAt('sShopkeeperIcon', -1, 64, hhup, scumSmallHud);
3380 drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", starsKills));
3382 if (starsRoomTimer1 > 0) {
3383 sprStore.loadFont('sFontSmall');
3384 Video.color = 0xff_ff_00;
3385 int scale = global.scale;
3386 sprStore.renderMultilineTextCentered(viewWidth/2, 216*scale, va("SHOTGUN CHALLENGE BEGINS IN ~%d~", (starsRoomTimer1/30)+1), scale, 0xff_00_00);
3391 void renderSunHUD (float currFrameDelta) {
3392 bool scumSmallHud = global.config.scumSmallHud;
3394 //auto life = max(0, global.plife);
3395 int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3396 int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3397 //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3402 sprStore.loadFont('sFontSmall');
3405 sprStore.loadFont('sFont');
3409 Video.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3410 //draw_sprite(sHeart, -1, view_xview[0]+8, view_yview[0]+8);
3411 //draw_text(view_xview[0]+24, view_yview[0]+8, life);
3413 if (global.plife == 1) {
3414 drawSpriteAt('sHeartSmallBlink', global.heartBlink, 10, hhup-4);
3415 global.heartBlink += 0.1;
3416 if (global.heartBlink > 3) global.heartBlink = 0;
3418 drawSpriteAt('sHeartSmall', -1, 10, hhup-4);
3419 global.heartBlink = 0;
3422 if (global.plife == 1) {
3423 drawSpriteAt('sHeartBlink', global.heartBlink, 8, hhup);
3424 global.heartBlink += 0.1;
3425 if (global.heartBlink > 3) global.heartBlink = 0;
3427 drawSpriteAt('sHeart', -1, 8, hhup);
3428 global.heartBlink = 0;
3431 int life = clamp(global.plife, 0, 99);
3432 drawTextAt(16+8, hhup, va("%d", life));
3434 //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3435 drawSpriteAt('sDamselIcon', -1, 64, hhup, scumSmallHud);
3436 drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", sunScore));
3438 if (sunRoomTimer1 > 0) {
3439 sprStore.loadFont('sFontSmall');
3440 Video.color = 0xff_ff_00;
3441 int scale = global.scale;
3442 sprStore.renderMultilineTextCentered(viewWidth/2, 216*scale, va("DAMSEL CHALLENGE BEGINS IN ~%d~", (sunRoomTimer1/30)+1), scale, 0xff_00_00);
3447 void renderMoonHUD (float currFrameDelta) {
3448 bool scumSmallHud = global.config.scumSmallHud;
3450 //auto life = max(0, global.plife);
3451 int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3452 int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3453 //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3458 sprStore.loadFont('sFontSmall');
3461 sprStore.loadFont('sFont');
3465 Video.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3467 //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3468 drawSpriteAt('sHoopsIcon', -1, 8, hhup, scumSmallHud);
3469 drawTextAt(8+16-(scumSmallHud ? 6 : 0), hhup, va("%d", moonScore));
3470 drawSpriteAt('sTimerIcon', -1, 64, hhup, scumSmallHud);
3471 drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", max(0, moonTimer)));
3473 if (moonRoomTimer1 > 0) {
3474 sprStore.loadFont('sFontSmall');
3475 Video.color = 0xff_ff_00;
3476 int scale = global.scale;
3477 sprStore.renderMultilineTextCentered(viewWidth/2, 216*scale, va("ARCHERY CHALLENGE BEGINS IN ~%d~", (moonRoomTimer1/30)+1), scale, 0xff_00_00);
3482 void renderHUD (float currFrameDelta) {
3483 if (levelKind == LevelKind.Stars) { renderStarsHUD(currFrameDelta); return; }
3484 if (levelKind == LevelKind.Sun) { renderSunHUD(currFrameDelta); return; }
3485 if (levelKind == LevelKind.Moon) { renderMoonHUD(currFrameDelta); return; }
3487 if (!isHUDEnabled()) return;
3489 if (levelKind == LevelKind.Transition) { renderTransitionInfo(currFrameDelta); return; }
3497 bool scumSmallHud = global.config.scumSmallHud;
3498 if (!global.config.optSGAmmo) moneyX = ammoX;
3501 sprStore.loadFont('sFontSmall');
3504 sprStore.loadFont('sFont');
3507 //int alpha = 0x6f_00_00_00;
3508 int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3509 int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3511 //Video.color = 0xff_ff_ff;
3512 Video.color = 0xff_ff_ff|talpha;
3516 if (global.plife == 1) {
3517 drawSpriteAt('sHeartSmallBlink', global.heartBlink, lifeX+2, 4-hhup);
3518 global.heartBlink += 0.1;
3519 if (global.heartBlink > 3) global.heartBlink = 0;
3521 drawSpriteAt('sHeartSmall', -1, lifeX+2, 4-hhup);
3522 global.heartBlink = 0;
3525 if (global.plife == 1) {
3526 drawSpriteAt('sHeartBlink', global.heartBlink, lifeX, 8-hhup);
3527 global.heartBlink += 0.1;
3528 if (global.heartBlink > 3) global.heartBlink = 0;
3530 drawSpriteAt('sHeart', -1, lifeX, 8-hhup);
3531 global.heartBlink = 0;
3535 int life = clamp(global.plife, 0, 99);
3536 //if (!scumHud && life > 99) life = 99;
3537 drawTextAt(lifeX+16, 8-hhup, va("%d", life));
3540 if (global.hasStickyBombs && global.stickyBombsActive) {
3541 if (scumSmallHud) drawSpriteAt('sStickyBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sStickyBombIcon', -1, bombX, 8-hhup);
3543 if (scumSmallHud) drawSpriteAt('sBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sBombIcon', -1, bombX, 8-hhup);
3545 int n = global.bombs;
3546 if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3547 drawTextAt(bombX+16, 8-hhup, va("%d", n));
3550 if (scumSmallHud) drawSpriteAt('sRopeIconSmall', -1, ropeX+2, 4-hhup); else drawSpriteAt('sRopeIcon', -1, ropeX, 8-hhup);
3552 if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3553 drawTextAt(ropeX+16, 8-hhup, va("%d", n));
3556 if (global.config.optSGAmmo && (!player || player.holdItem !isa ItemWeaponBow)) {
3557 if (scumSmallHud) drawSpriteAt('sShellsIcon', -1, ammoX+6, 8-hhup); else drawSpriteAt('sShellsIcon', -1, ammoX+7, 12-hhup);
3559 if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3560 drawTextAt(ammoX+16, 8-hhup, va("%d", n));
3561 } else if (player && player.holdItem isa ItemWeaponBow) {
3562 if (scumSmallHud) drawSpriteAt('sArrowRight', -1, ammoX+6, 8-hhup); else drawSpriteAt('sArrowRight', -1, ammoX+7, 12-hhup);
3564 if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3565 drawTextAt(ammoX+16, 8-hhup, va("%d", n));
3569 if (scumSmallHud) drawSpriteAt('sDollarSignSmall', -1, moneyX+2, 3-hhup); else drawSpriteAt('sDollarSign', -1, moneyX, 8-hhup);
3570 drawTextAt(moneyX+16, 8-hhup, va("%d", stats.money-xmoney));
3573 Video.color = 0xff_ff_ff|ialpha;
3575 int ity = (scumSmallHud ? 18-hhup : 24-hhup);
3578 if (global.hasUdjatEye) {
3579 if (global.udjatBlink) drawSpriteAt('sUdjatEyeIcon2', -1, n, ity); else drawSpriteAt('sUdjatEyeIcon', -1, n, ity);
3582 if (global.hasAnkh) { drawSpriteAt('sAnkhIcon', -1, n, ity); n += 20; }
3583 if (global.hasCrown) { drawSpriteAt('sCrownIcon', -1, n, ity); n += 20; }
3584 if (global.hasKapala) {
3585 if (global.bloodLevel == 0) drawSpriteAt('sKapalaIcon', 0, n, ity);
3586 else if (global.bloodLevel <= 2) drawSpriteAt('sKapalaIcon', 1, n, ity);
3587 else if (global.bloodLevel <= 4) drawSpriteAt('sKapalaIcon', 2, n, ity);
3588 else if (global.bloodLevel <= 6) drawSpriteAt('sKapalaIcon', 3, n, ity);
3589 else if (global.bloodLevel <= 8) drawSpriteAt('sKapalaIcon', 4, n, ity);
3592 if (global.hasSpectacles) { drawSpriteAt('sSpectaclesIcon', -1, n, ity); n += 20; }
3593 if (global.hasGloves) { drawSpriteAt('sGlovesIcon', -1, n, ity); n += 20; }
3594 if (global.hasMitt) { drawSpriteAt('sMittIcon', -1, n, ity); n += 20; }
3595 if (global.hasSpringShoes) { drawSpriteAt('sSpringShoesIcon', -1, n, ity); n += 20; }
3596 if (global.hasJordans) { drawSpriteAt('sJordansIcon', -1, n, ity); n += 20; }
3597 if (global.hasSpikeShoes) { drawSpriteAt('sSpikeShoesIcon', -1, n, ity); n += 20; }
3598 if (global.hasCape) { drawSpriteAt('sCapeIcon', -1, n, ity); n += 20; }
3599 if (global.hasJetpack) { drawSpriteAt('sJetpackIcon', -1, n, ity); n += 20; }
3600 if (global.hasStickyBombs) { drawSpriteAt('sPasteIcon', -1, n, ity); n += 20; }
3601 if (global.hasCompass) { drawSpriteAt('sCompassIcon', -1, n, ity); n += 20; }
3602 if (global.hasParachute) { drawSpriteAt('sParachuteIcon', -1, n, ity); n += 20; }
3604 if (player.holdItem && player.holdItem.objName == 'Bow' && global.arrows > 0) {
3607 while (m <= global.arrows && m <= 20 && malpha > 0) {
3608 Video.color = trunc(malpha*255)<<24|0xff_ff_ff;
3609 drawSpriteAt('sArrowIcon', -1, n, ity);
3611 if (m > 10) malpha -= 0.1; //and global.arrows > 20 malpha -= 0.1
3617 sprStore.loadFont('sFontSmall');
3618 Video.color = 0xff_ff_00|talpha;
3619 if (scumSmallHud) drawTextAt(moneyX+8, 8+8-hhup, va("+%d", xmoney));
3620 else drawTextAt(moneyX, 8+16-hhup, va("+%d", xmoney));
3623 Video.color = 0xff_ff_ff;
3624 if (global.config.ghostShowTime) renderGhostTimer(currFrameDelta);
3628 // ////////////////////////////////////////////////////////////////////////// //
3629 // x0 and y0 are non-scaled (and will be scaled)
3630 final void drawTextAtS3 (int x0, int y0, string text, optional int hiColor1, optional int hiColor2) {
3634 sprStore.renderTextWithHighlight(x0, y0, text, 3, hiColor1!optional, hiColor2!optional);
3638 final void drawTextAtS3Centered (int y0, string text, optional int hiColor1, optional int hiColor2) {
3640 int x0 = (viewWidth-sprStore.getTextWidth(text, 3, specified_hiColor1, specified_hiColor2))/2;
3641 sprStore.renderTextWithHighlight(x0, y0*3, text, 3, hiColor1!optional, hiColor2!optional);
3645 void renderHelpOverlay () {
3647 Video.fillRect(0, 0, viewWidth, viewHeight);
3650 int txoff = 0; // text x pos offset (for multi-color lines)
3652 if (gameHelpScreen) {
3653 sprStore.loadFont('sFontSmall');
3654 Video.color = 0xff_ff_ff;
3655 drawTextAtS3Centered(ty, va("HELP (PAGE ~%d~ OF ~%d~)", gameHelpScreen, MaxGameHelpScreen), 0xff_ff_00);
3659 if (gameHelpScreen == 1) {
3660 sprStore.loadFont('sFontSmall');
3661 Video.color = 0xff_ff_00; drawTextAtS3(tx, ty, "INVENTORY BASICS"); ty += 16;
3662 Video.color = 0xff_ff_ff;
3663 drawTextAtS3(tx, ty, global.expandString("Press $SWITCH to cycle through items."), 0x00_ff_00);
3666 Video.color = 0xff_ff_ff;
3667 drawSpriteAtS3('sHelpSprite1', -1, 64, 96);
3668 } else if (gameHelpScreen == 2) {
3669 sprStore.loadFont('sFontSmall');
3670 Video.color = 0xff_ff_00;
3671 drawTextAtS3(tx, ty, "SELLING TO SHOPKEEPERS"); ty += 16;
3672 Video.color = 0xff_ff_ff;
3673 drawTextAtS3(tx, ty, global.expandString("Press $PAY to offer your currently"), 0x00_ff_00); ty += 8;
3674 drawTextAtS3(tx, ty, "held item to the shopkeeper."); ty += 16;
3675 drawTextAtS3(tx, ty, "If the shopkeeper is interested, "); ty += 8;
3676 //drawTextAtS3(tx, ty, global.expandString("press $PAY again to complete the sale."), 0x00_ff_00); ty += 72;
3677 drawTextAtS3(tx, ty, global.expandString("press $PAY again to complete"), 0x00_ff_00);
3678 drawTextAtS3(tx, ty+8, "the sale.");
3680 drawSpriteAtS3('sHelpSell', -1, 112, 100);
3681 drawTextAtS3(tx, ty, "Purchasing goods from the shopkeeper"); ty += 8;
3682 drawTextAtS3(tx, ty, "will increase the funds he has"); ty += 8;
3683 drawTextAtS3(tx, ty, "available to buy your unwanted stuff."); ty += 8;
3686 sprStore.loadFont('sFont');
3687 Video.color = 0xff_ff_ff;
3688 drawTextAtS3(136, 8, "MAP");
3690 if (lg.mapSprite && (isNormalLevel() || isTransitionRoom())) {
3691 Video.color = 0xff_ff_00;
3692 drawTextAtS3Centered(24, lg.mapTitle);
3694 auto spf = sprStore[lg.mapSprite].frames[0];
3695 int mapX = 160-spf.width/2;
3696 int mapY = 120-spf.height/2;
3697 //mapTitleX = 160-string_length(global.mapTitle)*8/2;
3699 Video.color = 0xff_ff_ff;
3700 drawSpriteAtS3(lg.mapSprite, -1, mapX, mapY);
3702 if (lg.mapSprite != 'sMapDefault') {
3703 int mx = -1, my = -1;
3705 // set position of player icon
3706 switch (global.currLevel) {
3707 case 1: mx = 81; my = 22; break;
3708 case 2: mx = 113; my = 63; break;
3709 case 3: mx = 197; my = 86; break;
3710 case 4: mx = 133; my = 109; break;
3711 case 5: mx = 181; my = 22; break;
3712 case 6: mx = 126; my = 64; break;
3713 case 7: mx = 158; my = 112; break;
3714 case 8: mx = 66; my = 80; break;
3715 case 9: mx = 30; my = 26; break;
3716 case 10: mx = 88; my = 54; break;
3717 case 11: mx = 148; my = 81; break;
3718 case 12: mx = 210; my = 205; break;
3719 case 13: mx = 66; my = 17; break;
3720 case 14: mx = 146; my = 17; break;
3721 case 15: mx = 82; my = 77; break;
3722 case 16: mx = 178; my = 81; break;
3726 int plrx = mx+player.ix/16;
3727 int plry = my+player.iy/16;
3728 if (isTransitionRoom()) { plrx = mx+20; plry = my+16; }
3729 name plrspr = 'sMapSpelunker';
3730 if (global.isDamsel) plrspr = 'sMapDamsel';
3731 else if (global.isTunnelMan) plrspr = 'sMapTunnel';
3732 auto ss = sprStore[plrspr];
3733 drawSpriteAtS3(plrspr, (pausedTime/2)%ss.frames.length, mapX+plrx, mapY+plry);
3735 if (global.hasCompass && allExits.length) {
3736 drawSpriteAtS3('sMapRedDot', -1, mapX+mx+allExits[0].ix/16, mapY+my+allExits[0].iy/16);
3743 sprStore.loadFont('sFontSmall');
3744 Video.color = 0xff_ff_00;
3745 drawTextAtS3Centered(232, "PRESS ~SPACE~/~LEFT~/~RIGHT~ TO CHANGE PAGE", 0x00_ff_00);
3747 Video.color = 0xff_ff_ff;
3751 void renderPauseOverlay () {
3752 //drawTextAt(256, 432, "PAUSED", scale);
3754 if (gameShowHelp) { renderHelpOverlay(); return; }
3756 Video.color = 0xff_ff_00;
3757 //int hiColor = 0x00_ff_00;
3760 if (isTutorialRoom()) {
3761 sprStore.loadFont('sFont');
3762 drawTextAtS3(40, n-24, "TUTORIAL CAVE");
3763 } else if (isNormalLevel()) {
3764 sprStore.loadFont('sFont');
3766 drawTextAtS3Centered(n-32, va("LEVEL ~%d~", global.currLevel), 0x00_ff_00);
3768 sprStore.loadFont('sFontSmall');
3770 int depth = round((174.8*(global.currLevel-1)+(player.iy+8)*0.34)*(global.config.scumMetric ? 0.3048 : 1.0)*10);
3771 string depthStr = va("DEPTH: ~%d.%d~ %s", depth/10, depth%10, (global.config.scumMetric ? "METRES" : "FEET"));
3772 drawTextAtS3Centered(n-16, depthStr, 0x00_ff_00);
3775 drawTextAtS3Centered(n, va("MONEY: ~%d~", stats.money), 0x00_ff_00);
3776 drawTextAtS3Centered(n+16, va("KILLS: ~%d~", stats.countKills), 0x00_ff_00);
3777 drawTextAtS3Centered(n+32, va("SAVES: ~%d~", stats.damselsSaved), 0x00_ff_00);
3778 drawTextAtS3Centered(n+48, va("TIME: ~%s~", time2str(time/30)), 0x00_ff_00);
3779 drawTextAtS3Centered(n+64, va("LEVEL TIME: ~%s~", time2str((time-levelStartTime)/30)), 0x00_ff_00);
3782 sprStore.loadFont('sFontSmall');
3783 Video.color = 0xff_ff_ff;
3784 drawTextAtS3Centered(240-2-8, "~ESC~-RETURN ~F10~-QUIT ~CTRL+DEL~-SUICIDE", 0xff_7f_00);
3785 drawTextAtS3Centered(2, "~O~PTIONS REDEFINE ~K~EYS ~S~TATISTICS", 0xff_7f_00);
3789 // ////////////////////////////////////////////////////////////////////////// //
3790 transient int drawLoot;
3791 transient int drawPosX, drawPosY;
3793 void resetTransitionOverlay () {
3800 // current game, uncollapsed
3801 struct LevelStatInfo {
3803 // for transition screen
3810 void thinkFrameTransition () {
3811 if (drawLoot == 0) {
3812 if (drawPosX > 272) {
3815 if (drawPosY > 83+4) drawPosY = 83;
3817 } else if (drawPosX > 232) {
3820 if (drawPosY > 91+4) drawPosY = 91;
3825 void renderTransitionOverlay () {
3826 sprStore.loadFont('sFontSmall');
3827 Video.color = 0xff_ff_00;
3828 //else if (global.currLevel-1 < 1) draw_text(32, 48, "TUTORIAL CAVE COMPLETED!");
3829 //else draw_text(32, 48, "LEVEL "+string(global.currLevel-1)+" COMPLETED!");
3830 drawTextAt(32, 48, va("LEVEL ~%d~ COMPLETED!", global.currLevel), hiColor1:0x00_ff_ff);
3831 Video.color = 0xff_ff_ff;
3832 drawTextAt(32, 64, va("TIME = ~%s~", time2str((levelEndTime-levelStartTime)/30)), hiColor1:0xff_ff_00);
3834 if (/*stats.collected.length == 0*/stats.money <= levelMoneyStart) {
3835 drawTextAt(32, 80, "LOOT = ~NONE~", hiColor1:0xff_00_00);
3837 drawTextAt(32, 80, va("LOOT = ~%d~", stats.money-levelMoneyStart), hiColor1:0xff_ff_00);
3840 if (stats.kills.length == 0) {
3841 drawTextAt(32, 96, "KILLS = ~NONE~", hiColor1:0x00_ff_00);
3843 drawTextAt(32, 96, va("KILLS = ~%d~", stats.kills.length), hiColor1:0xff_ff_00);
3846 drawTextAt(32, 112, va("MONEY = ~%d~", stats.money), hiColor1:0xff_ff_00);
3850 // ////////////////////////////////////////////////////////////////////////// //
3851 private transient array!MapEntity renderVisibleCids;
3852 private transient array!MapEntity renderVisibleLights;
3853 private transient array!MapTile renderFrontTiles; // normal, with fg
3855 final bool renderSortByDepth (MapEntity oa, MapEntity ob) {
3856 auto da = oa.depth, db = ob.depth;
3857 if (da == db) return (oa.objId < ob.objId);
3862 const int RenderEdgePixNormal = 64;
3863 const int RenderEdgePixLight = 256;
3865 #ifndef EXPERIMENTAL_RENDER_CACHE
3866 enum skipListCreation = false;
3869 void renderWithOfs (int xofs, int yofs, float currFrameDelta) {
3870 int scale = global.scale;
3872 // don't touch framebuffer alpha
3873 Video.colorMask = Video::CMask.Colors;
3874 Video.color = 0xff_ff_ff;
3877 Video::ScissorRect scsave;
3878 bool doRestoreGL = false;
3880 if (viewOffsetX > 0 || viewOffsetY > 0) {
3882 Video.getScissor(scsave);
3883 Video.scissorCombine(viewOffsetX, viewOffsetY, viewWidth, viewHeight);
3884 Video.glPushMatrix();
3885 Video.glTranslate(viewOffsetX, viewOffsetY);
3886 //Video.glTranslate(-550, 0);
3887 //Video.glScale(1, 1);
3892 bool isDarkLevel = global.darkLevel;
3895 switch (global.config.scumPlayerLit) {
3896 case 0: player.lightRadius = 0; break; // never
3897 case 1: // only in "scumDarkness"
3898 player.lightRadius = (global.config.scumDarkness >= 2 ? 96 : 32);
3901 player.lightRadius = 96;
3906 // render cave background
3909 int bgw = levBGImg.tex.width*scale;
3910 int bgh = levBGImg.tex.height*scale;
3911 int bgTW = (tilesWidth*tsz+bgw-1)/bgw;
3912 int bgTH = (tilesHeight*tsz+bgh-1)/bgh;
3913 int bgX0 = max(0, xofs/bgw);
3914 int bgY0 = max(0, yofs/bgh);
3915 int bgX1 = min(bgTW, (xofs+viewWidth+bgw-1)/bgw);
3916 int bgY1 = min(bgTH, (yofs+viewHeight+bgh-1)/bgh);
3917 foreach (int ty; bgY0..bgY1) {
3918 foreach (int tx; bgX0..bgX1) {
3919 int x0 = tx*bgw-xofs;
3920 int y0 = ty*bgh-yofs;
3921 levBGImg.tex.blitAt(x0, y0, scale);
3926 int RenderEdgePix = (global.darkLevel ? RenderEdgePixLight : RenderEdgePixNormal);
3928 // render background tiles
3929 for (MapBackTile bt = backtiles; bt; bt = bt.next) {
3930 bt.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3933 // collect visible special tiles
3934 #ifdef EXPERIMENTAL_RENDER_CACHE
3935 bool skipListCreation = (lastRenderTime == time && renderVisibleCids.length); //FIXME
3938 if (!skipListCreation) {
3939 renderVisibleCids.clear();
3940 renderVisibleLights.clear();
3941 renderFrontTiles.clear();
3943 int endVX = xofs+viewWidth;
3944 int endVY = yofs+viewHeight;
3948 if (player && player.visible) renderVisibleCids[$] = player; // include player in render list
3950 //FIXME: drop lit objects which cannot affect visible area
3952 // collect visible objects
3953 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)) {
3954 if (!o.visible) continue;
3955 auto tile = MapTile(o);
3957 if (isDarkLevel && (o.lightRadius > 4 || tile.litWholeTile)) renderVisibleLights[$] = o;
3958 if (tile.invisible) continue;
3959 if (tile.bgfront) renderFrontTiles[$] = tile;
3960 if (tile.bgback) tile.drawWithOfsBack(xofs, yofs, scale, currFrameDelta);
3962 if (isDarkLevel && o.lightRadius > 4) renderVisibleLights[$] = o;
3964 // check if the object is really visible -- this will speed up later sorting
3965 int fx0, fy0, fx1, fy1;
3966 auto spf = o.getSpriteFrame(default, out fx0, out fy0, out fx1, out fy1);
3967 if (!spf) continue; // no sprite -- nothing to draw (no, really)
3968 int ix = o.ix, iy = o.iy;
3969 int x0 = (ix+fx0)*scale, y0 = (iy+fy0)*scale;
3970 int x1 = (ix+fx1)*scale, y1 = (iy+fy1)*scale;
3971 if (x1 <= xofs || y1 <= yofs || x0 >= endVX || y0 >= endVY) {
3975 renderVisibleCids[$] = o;
3978 foreach (MapEntity o; objGrid.allObjects()) {
3979 if (!o.visible) continue;
3980 auto tile = MapTile(o);
3982 if (isDarkLevel && (o.lightRadius > 4 || tile.litWholeTile)) renderVisibleLights[$] = o;
3983 if (tile.invisible) continue;
3984 if (tile.bgfront) renderFrontTiles[$] = tile;
3985 if (tile.bgback) tile.drawWithOfsBack(xofs, yofs, scale, currFrameDelta);
3987 if (isDarkLevel && o.lightRadius > 4) renderVisibleLights[$] = o;
3989 renderVisibleCids[$] = o;
3992 //writeln("::: ", cnt, " invisible objects dropped");
3994 renderVisibleCids.sort(&renderSortByDepth);
3995 lastRenderTime = time;
3998 auto depth4Start = 0;
3999 foreach (auto xidx, MapEntity o; renderVisibleCids) {
4006 bool playerPowerupRendered = false;
4008 // render objects (part one: depth > 3)
4009 foreach (auto idx; depth4Start..renderVisibleCids.length; reverse) {
4010 MapEntity o = renderVisibleCids[idx];
4011 // 1000 is an ordinary tile
4012 if (!playerPowerupRendered && o.depth <= 1200) {
4013 playerPowerupRendered = true;
4014 // so ducking player will have it's cape correctly rendered
4015 if (player.visible) player.drawPrePrePowerupWithOfs(xofs, yofs, scale, currFrameDelta);
4017 //if (idx && renderSortByDepth(o, renderVisibleCids[idx-1])) writeln("VIOLATION!");
4018 o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
4021 // render object (part two: front tile parts, depth 3.5)
4022 foreach (MapTile tile; renderFrontTiles) {
4023 tile.drawWithOfsFront(xofs, yofs, scale, currFrameDelta);
4026 // render objects (part three: depth <= 3)
4027 foreach (auto idx; 0..depth4Start; reverse) {
4028 MapEntity o = renderVisibleCids[idx];
4029 o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
4030 //done above;if (isDarkLevel && (o.lightRadius > 4 || (o isa MapTile && MapTile(o).litWholeTile))) renderVisibleLights[$] = o;
4033 // render player post-effects, so cape or jetpack will be correctly rendered on level exit, for example
4034 player.lastDrawWithOfs(xofs, yofs, scale, currFrameDelta);
4038 auto ltex = bgtileStore.lightTexture('ltx512', 512);
4040 // set screen alpha to min
4041 Video.colorMask = Video::CMask.Alpha;
4042 Video.blendMode = Video::BlendMode.None;
4043 Video.color = 0xff_ff_ff_ff;
4044 Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
4045 //Video.colorMask = Video::CMask.All;
4048 // also, stencil 'em, so we can filter dark areas
4049 Video.textureFiltering = true;
4050 Video.stencil = true;
4051 Video.stencilFunc(Video::StencilFunc.Always, 1);
4052 Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Replace);
4053 Video.alphaTestFunc = Video::AlphaFunc.Greater;
4054 Video.alphaTestVal = 0.03+0.011*global.config.darknessDarkness;
4055 Video.color = 0xff_ff_ff;
4056 Video.blendFunc = Video::BlendFunc.Max;
4057 Video.blendMode = Video::BlendMode.Blend; // anything except `Normal`
4058 Video.colorMask = Video::CMask.Alpha;
4060 foreach (MapEntity e; renderVisibleLights) {
4062 e.getInterpCoords(currFrameDelta, scale, out xi, out yi);
4063 auto tile = MapTile(e);
4064 if (tile && tile.litWholeTile) {
4065 //Video.color = 0xff_ff_ff;
4066 Video.fillRect(xi-xofs, yi-yofs, e.width*scale, e.height*scale);
4068 int lrad = e.lightRadius;
4069 if (lrad < 4) continue; // just in case
4071 float lightscale = float(lrad*scale)/float(ltex.tex.width);
4072 #ifdef OLD_LIGHT_OFFSETS
4073 int fx0, fy0, fx1, fy1;
4075 auto spf = e.getSpriteFrame(out doMirror, out fx0, out fy0, out fx1, out fy1);
4077 xi += (fx1-fx0)*scale/2;
4078 yi += (fy1-fy0)*scale/2;
4082 e.getLightOffset(out lxofs, out lyofs);
4087 lrad = lrad*scale/2;
4090 ltex.tex.blitAt(xi, yi, lightscale);
4092 Video.textureFiltering = false;
4094 // modify only lit parts
4095 Video.stencilFunc(Video::StencilFunc.Equal, 1);
4096 Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Keep);
4097 // multiply framebuffer colors by framebuffer alpha
4098 Video.color = 0xff_ff_ff; // it doesn't matter
4099 Video.blendFunc = Video::BlendFunc.Add;
4100 Video.blendMode = Video::BlendMode.DstMulDstAlpha;
4101 Video.colorMask = Video::CMask.Colors;
4102 Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
4104 // filter unlit parts
4105 Video.stencilFunc(Video::StencilFunc.NotEqual, 1);
4106 Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Keep);
4107 Video.blendFunc = Video::BlendFunc.Add;
4108 Video.blendMode = Video::BlendMode.Filter;
4109 Video.colorMask = Video::CMask.Colors;
4110 Video.color = 0x00_00_18+0x00_00_10*global.config.darknessDarkness;
4111 //Video.color = 0x00_00_18;
4112 //Video.color = 0x00_00_38;
4113 Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
4116 Video.blendFunc = Video::BlendFunc.Add;
4117 Video.blendMode = Video::BlendMode.Normal;
4118 Video.colorMask = Video::CMask.All;
4119 Video.alphaTestFunc = Video::AlphaFunc.Always;
4120 Video.stencil = false;
4123 // clear visible objects list (nope)
4124 //renderVisibleCids.clear();
4125 //renderVisibleLights.clear();
4128 if (global.config.drawHUD) renderHUD(currFrameDelta);
4129 renderCompass(currFrameDelta);
4131 float osdTimeLeft, osdTimeStart;
4132 string msg = osdGetMessage(out osdTimeLeft, out osdTimeStart);
4134 auto ct = GetTickCount();
4136 sprStore.loadFont('sFontSmall');
4137 auto msgHeight = sprStore.getMultilineTextHeight(msg, msgScale);
4138 int x = viewWidth/2;
4139 int y = viewHeight-64-msgHeight;
4140 auto oldColor = Video.color;
4141 Video.color = 0xff_ff_00;
4142 if (osdTimeLeft < 0.5) {
4143 int alpha = 255-clamp(int(510*osdTimeLeft), 0, 255);
4144 Video.color = Video.color|(alpha<<24);
4145 } else if (ct-osdTimeStart < 0.5) {
4146 osdTimeStart = ct-osdTimeStart;
4147 int alpha = 255-clamp(int(510*osdTimeStart), 0, 255);
4148 Video.color = Video.color|(alpha<<24);
4150 sprStore.renderMultilineTextCentered(x, y, msg, msgScale, 0x00_ff_00, 0xff_ff_ff);
4151 Video.color = oldColor;
4154 int hiColor1, hiColor2;
4155 msg = osdGetTalkMessage(out hiColor1, out hiColor2);
4158 sprStore.loadFont('sFontSmall');
4159 auto msgWidth = sprStore.getMultilineTextWidth(msg, processHighlights1:true, processHighlights2:true);
4160 auto msgHeight = sprStore.getMultilineTextHeight(msg);
4161 auto msgWidthOrig = msgWidth*msgScale;
4162 auto msgHeightOrig = msgHeight*msgScale;
4163 if (msgWidth%16 != 0) msgWidth = (msgWidth|0x0f)+1;
4164 if (msgHeight%16 != 0) msgHeight = (msgHeight|0x0f)+1;
4165 msgWidth *= msgScale;
4166 msgHeight *= msgScale;
4167 int x = (viewWidth-msgWidth)/2;
4168 int y = 32*msgScale;
4169 auto oldColor = Video.color;
4170 // draw text frame and text background
4172 Video.fillRect(x, y, msgWidth, msgHeight);
4173 Video.color = 0xff_ff_ff;
4174 for (int fdx = 0; fdx < msgWidth; fdx += 16*msgScale) {
4175 auto spf = sprStore['sMenuTop'].frames[0];
4176 spf.tex.blitAt(x+fdx, y-16*msgScale, msgScale);
4177 spf = sprStore['sMenuBottom'].frames[0];
4178 spf.tex.blitAt(x+fdx, y+msgHeight, msgScale);
4180 for (int fdy = 0; fdy < msgHeight; fdy += 16*msgScale) {
4181 auto spf = sprStore['sMenuLeft'].frames[0];
4182 spf.tex.blitAt(x-16*msgScale, y+fdy, msgScale);
4183 spf = sprStore['sMenuRight'].frames[0];
4184 spf.tex.blitAt(x+msgWidth, y+fdy, msgScale);
4187 auto spf = sprStore['sMenuUL'].frames[0];
4188 spf.tex.blitAt(x-16*msgScale, y-16*msgScale, msgScale);
4189 spf = sprStore['sMenuUR'].frames[0];
4190 spf.tex.blitAt(x+msgWidth, y-16*msgScale, msgScale);
4191 spf = sprStore['sMenuLL'].frames[0];
4192 spf.tex.blitAt(x-16*msgScale, y+msgHeight, msgScale);
4193 spf = sprStore['sMenuLR'].frames[0];
4194 spf.tex.blitAt(x+msgWidth, y+msgHeight, msgScale);
4196 Video.color = 0xff_ff_00;
4197 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));
4198 Video.color = oldColor;
4201 if (inWinCutscene) renderWinCutsceneOverlay();
4202 if (inIntroCutscene) renderTitleCutsceneOverlay();
4203 if (isTransitionRoom()) renderTransitionOverlay();
4207 Video.setScissor(scsave);
4208 Video.glPopMatrix();
4212 Video.color = 0xff_ff_ff;
4216 // ////////////////////////////////////////////////////////////////////////// //
4217 final class!MapObject findGameObjectClassByName (name aname) {
4218 if (!aname) return none; // just in case
4219 auto co = FindClassByGameObjName(aname);
4221 writeln("***UNKNOWN GAME OBJECT: '", aname, "'");
4224 co = GetClassReplacement(co);
4225 if (!co) FatalError("findGameObjectClassByName: WTF?!");
4226 if (!class!MapObject(co)) FatalError("findGameTileClassByName: object '%n' is not a map object, it is a `%n`", aname, GetClassName(co));
4227 return class!MapObject(co);
4231 final class!MapTile findGameTileClassByName (name aname) {
4232 if (!aname) return none; // just in case
4233 auto co = FindClassByGameObjName(aname);
4234 if (!co) return MapTile; // unknown names will be routed directly to tile object
4235 co = GetClassReplacement(co);
4236 if (!co) FatalError("findGameTileClassByName: WTF?!");
4237 if (!class!MapTile(co)) FatalError("findGameTileClassByName: object '%n' is not a tile, it is a `%n`", aname, GetClassName(co));
4238 return class!MapTile(co);
4242 final MapObject findAnyObjectOfType (name aname) {
4243 if (!aname) return none;
4244 auto cls = FindClassByGameObjName(aname);
4245 if (!cls) return none;
4246 foreach (MapObject obj; objGrid.allObjects(MapObject)) {
4247 if (obj.spectral) continue;
4248 if (obj isa cls) return obj;
4254 // ////////////////////////////////////////////////////////////////////////// //
4255 final bool isRopePlacedAt (int x, int y) {
4257 foreach (ref auto v; covered) v = false;
4258 foreach (MapTile t; objGrid.inRectPix(x, y-8, 1, 17, precise:false, castClass:MapTileRope)) {
4259 //if (!cbIsRopeTile(t)) continue;
4260 if (t.ix != x) continue;
4261 if (t.iy == y) return true;
4262 foreach (int ty; t.iy..t.iy+8) {
4264 if (d >= 0 && d < covered.length) covered[d] = true;
4267 // check if the whole rope height is completely covered with ropes
4268 foreach (auto v; covered) if (!v) return false;
4273 final MapTile CreateMapTile (int xpos, int ypos, name aname) {
4274 if (!aname) FatalError("cannot create typeless tile");
4275 auto tclass = findGameTileClassByName(aname);
4276 if (!tclass) return none;
4277 MapTile tile = SpawnObject(tclass);
4278 tile.global = global;
4280 tile.objName = aname;
4281 tile.objType = aname; // just in case
4284 tile.objId = ++lastUsedObjectId;
4285 if (!tile.initialize()) FatalError("cannot create tile '%n'", aname);
4290 final bool PutSpawnedMapTile (int x, int y, MapTile tile, optional bool putToGrid) {
4291 if (!tile || !tile.isInstanceAlive) return false;
4293 if (!putToGrid) putToGrid = (tile.moveable || tile.toSpecialGrid || tile.width != 16 || tile.height != 16 || x%16 != 0 || y%16 != 0);
4295 //writeln("putting tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (putToGrid ? "special " : ""), "grid, pos=(", x/16.0, ",", y/16.0, ")");
4298 int mapx = x/16, mapy = y/16;
4299 if (mapx < 0 || mapx >= tilesWidth || mapy < 0 || mapy >= tilesHeight) return false;
4302 // if we already have rope tile there, there is no reason to add another one
4303 if (tile isa MapTileRope) {
4304 if (isRopePlacedAt(x, y)) return false;
4307 // activate special or animated tile
4308 tile.active = tile.active || putToGrid || tile.moveable || tile.toSpecialGrid || tile.lava /*|| tile.water*/; // will be done in MakeMapTile
4309 // animated tiles must be active
4311 auto spr = tile.getSprite();
4312 if (spr && spr.frames.length > 1) {
4313 writeln("activated animated tile '", tile.objName, "'");
4321 //if (tile isa TitleTileCopy) writeln("*** PUTTING COPYRIGHT TILE");
4322 tile.toSpecialGrid = true;
4323 if (!tile.dontReplaceOthers && x&16 == 0 && y%16 == 0) {
4324 auto t = getTileAtGridAny(x/16, y/16);
4325 if (t && !t.immuneToReplacement) {
4326 writeln("REPLACING tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
4327 writeln(" NEW tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (tile.toSpecialGrid ? "special " : ""), "grid, pos=(", tile.fltx/16.0, ",", tile.flty/16.0, ")");
4331 objGrid.insert(tile);
4333 //writeln("SIZE: ", tilesWidth, "x", tilesHeight);
4334 setTileAtGrid(x/16, y/16, tile);
4335 auto t = getTileAtGridAny(x/16, y/16);
4338 writeln("putting tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (putToGrid ? "special " : ""), "grid, pos=(", x/16.0, ",", y/16.0, ")");
4339 checkTilesInRect(x/16, y/16, 16, 16, delegate bool (MapTile tile) {
4340 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, ")");
4343 FatalError("FUUUUUU");
4348 if (tile.enter) registerEnter(tile);
4349 if (tile.exit) registerExit(tile);
4355 // won't call `onDestroy()`
4356 final void RemoveMapTileFromGrid (int tileX, int tileY, optional string reason) {
4357 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
4358 auto t = getTileAtGridAny(tileX, tileY);
4360 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, ")");
4368 final MapTile MakeMapTile (int mapx, int mapy, name aname, optional bool putToGrid) {
4369 //writeln("tile at (", mapx, ",", mapy, "): ", aname);
4370 //if (aname == 'oLush') { MapObject fail; fail.initialize(); }
4371 //if (mapy >= tilesHeight) { writeln("OOM: ", aname, "; ty=", mapy, "; hgt=", tilesHeight); }
4372 if (mapx < 0 || mapx >= tilesWidth || mapy < 0 || mapy >= tilesHeight) return none;
4374 // if we already have rope tile there, there is no reason to add another one
4375 if (aname == 'oRope') {
4376 if (isRopePlacedAt(mapx*16, mapy*16)) return none;
4379 auto tile = CreateMapTile(mapx*16, mapy*16, aname);
4380 if (!tile) return none;
4381 if (!PutSpawnedMapTile(mapx*16, mapy*16, tile, putToGrid!optional)) {
4390 final MapTile MakeMapTileAtPix (int xpix, int ypix, name aname, optional bool putToGrid) {
4391 // if we already have rope tile there, there is no reason to add another one
4392 if (aname == 'oRope') {
4393 if (isRopePlacedAt(xpix, ypix)) return none;
4396 auto tile = CreateMapTile(xpix, ypix, aname);
4397 if (!tile) return none;
4398 if (!PutSpawnedMapTile(xpix, ypix, tile, putToGrid!optional)) {
4407 final MapTile MakeMapRopeTileAt (int x0, int y0) {
4408 // if we already have rope tile there, there is no reason to add another one
4409 if (isRopePlacedAt(x0, y0)) return none;
4411 auto tile = CreateMapTile(x0, y0, 'oRope');
4412 if (!PutSpawnedMapTile(x0, y0, tile, putToGrid:true)) {
4421 // ////////////////////////////////////////////////////////////////////////// //
4422 final MapBackTile CreateBgTile (name sprName, optional int atx0, optional int aty0, optional int aw, optional int ah) {
4423 BackTileImage img = bgtileStore[sprName];
4424 auto res = SpawnObject(MapBackTile);
4425 res.global = global;
4428 res.bgtName = sprName;
4429 if (specified_atx0) res.tx0 = atx0;
4430 if (specified_aty0) res.ty0 = aty0;
4431 if (specified_aw) res.w = aw;
4432 if (specified_ah) res.h = ah;
4433 if (!res.initialize()) FatalError("cannot create background tile with image '%n'", sprName);
4438 // ////////////////////////////////////////////////////////////////////////// //
4440 background The background asset from which the new tile will be extracted.
4441 left The x coordinate of the left of the new tile, relative to the background asset's top left corner.
4442 top The y coordinate of the top of the new tile, relative to the background assets top left corner.
4443 width The width of the tile.
4444 height The height of the tile.
4445 x The x position in the room to place the tile.
4446 y The y position in the room to place the tile.
4447 depth The depth at which to place the tile.
4449 final void MakeMapBackTile (name bgname, int left, int top, int width, int height, int x, int y, int depth) {
4450 if (width < 1 || height < 1 || !bgname) return;
4451 auto bgt = bgtileStore[bgname];
4452 if (!bgt) FatalError("cannot load background '%n'", bgname);
4453 MapBackTile bt = SpawnObject(MapBackTile);
4456 bt.objName = bgname;
4458 bt.bgtName = bgname;
4466 // find a place for it
4471 // back tiles with the highest depth should come first
4472 MapBackTile ct = backtiles, cprev = none;
4473 while (ct && depth <= ct.depth) { cprev = ct; ct = ct.next; }
4476 bt.next = cprev.next;
4479 bt.next = backtiles;
4485 // ////////////////////////////////////////////////////////////////////////// //
4486 final MapObject SpawnMapObjectWithClass (class!MapObject oclass) {
4487 if (!oclass) return none;
4489 MapObject obj = SpawnObject(oclass);
4490 if (!obj) FatalError("cannot create map object '%n'", GetClassName(oclass));
4492 //if (aname == 'oShells' || aname == 'oShellSingle' || aname == 'oRopePile') writeln("*** CREATED '", aname, "'");
4494 obj.global = global;
4496 obj.objId = ++lastUsedObjectId;
4502 final MapObject SpawnMapObject (name aname) {
4503 if (!aname) return none;
4504 auto res = SpawnMapObjectWithClass(findGameObjectClassByName(aname));
4505 if (res && !res.objType) res.objType = aname; // just in case
4510 final MapObject PutSpawnedMapObject (int x, int y, MapObject obj) {
4511 if (!obj /*|| obj.global || obj.level*/) return none; // oops
4515 if (!obj.initialize()) { delete obj; return none; } // not fatal
4523 final MapObject MakeMapObject (int x, int y, name aname) {
4524 MapObject obj = SpawnMapObject(aname);
4525 obj = PutSpawnedMapObject(x, y, obj);
4530 // ////////////////////////////////////////////////////////////////////////// //
4531 int winCutSceneTimer = -1;
4532 int winVolcanoTimer = -1;
4533 int winCutScenePhase = 0;
4534 int winSceneDrawStatus = 0;
4535 int winMoneyCount = 0;
4537 bool winFadeOut = false;
4538 int winFadeLevel = 0;
4539 int winCutsceneSkip = 0; // 1: waiting for pay release; 2: pay released, do skip
4540 bool winCutsceneSwitchToNext = false;
4543 void startWinCutscene () {
4544 global.hasParachute = false;
4546 winCutsceneSwitchToNext = false;
4547 winCutsceneSkip = 0;
4548 isKeyPressed(GameConfig::Key.Pay);
4549 isKeyReleased(GameConfig::Key.Pay);
4551 auto olddel = ImmediateDelete;
4552 ImmediateDelete = false;
4557 addBackgroundGfxDetails();
4559 levBGImgName = 'bgCave';
4560 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
4562 blockWaterChecking = true;
4566 ImmediateDelete = olddel;
4567 CollectGarbage(true); // destroy delayed objects too
4569 if (dumpGridStats) objGrid.dumpStats();
4571 playerExited = false; // just in case
4572 playerExitDoor = none;
4580 winCutSceneTimer = -1;
4581 winCutScenePhase = 0;
4584 if (global.config.gameMode != GameConfig::GameMode.Vanilla) {
4585 if (global.config.bizarre) {
4586 global.yasmScore = 1;
4587 global.config.bizarrePlusTitle = true;
4590 array!MapTile toReplace;
4591 forEachTile(delegate bool (MapTile t) {
4592 if (t.objType == 'oGTemple' ||
4593 t.objType == 'oIce' ||
4594 t.objType == 'oDark' ||
4595 t.objType == 'oBrick' ||
4596 t.objType == 'oLush')
4603 foreach (MapTile t; miscTileGrid.allObjects()) {
4604 if (t.objType == 'oGTemple' ||
4605 t.objType == 'oIce' ||
4606 t.objType == 'oDark' ||
4607 t.objType == 'oBrick' ||
4608 t.objType == 'oLush')
4614 foreach (MapTile t; toReplace) {
4616 t.cleanDeath = true;
4617 if (rand(1,120) == 1) instance_change(oGTemple, false);
4618 else if (rand(1,100) == 1) instance_change(oIce, false);
4619 else if (rand(1,90) == 1) instance_change(oDark, false);
4620 else if (rand(1,80) == 1) instance_change(oBrick, false);
4621 else if (rand(1,70) == 1) instance_change(oLush, false);
4629 if (rand(1,5) == 1) instance_change(oLush, false);
4634 //!instance_create(0, 0, oBricks);
4636 //shakeToggle = false;
4637 //oPDummy.status = 2;
4642 if (global.kaliPunish >= 2) {
4643 instance_create(oPDummy.x, oPDummy.y+2, oBall2);
4644 obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
4646 obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
4648 obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
4650 obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
4657 void startWinCutsceneVolcano () {
4658 global.hasParachute = false;
4660 writeln("VOLCANO HOLD 0: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4661 writeln("VOLCANO PICK 0: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4665 winCutsceneSwitchToNext = false;
4666 auto olddel = ImmediateDelete;
4667 ImmediateDelete = false;
4671 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
4673 blockWaterChecking = true;
4675 ImmediateDelete = olddel;
4676 CollectGarbage(true); // destroy delayed objects too
4678 spawnPlayerAt(2*16+8, 11*16+8);
4679 player.dir = MapEntity::Dir.Right;
4681 playerExited = false; // just in case
4682 playerExitDoor = none;
4690 winCutSceneTimer = -1;
4691 winCutScenePhase = 0;
4693 MakeMapTile(0, 0, 'oEnd2BG');
4694 realViewStart.x = 0;
4695 realViewStart.y = 0;
4704 player.dead = false;
4705 player.active = true;
4706 player.visible = false;
4707 player.removeBallAndChain(temp:true);
4708 player.stunned = false;
4709 player.status = MapObject::FALLING;
4710 if (player.holdItem) player.holdItem.visible = false;
4711 player.fltx = 320/2;
4715 writeln("VOLCANO HOLD 1: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4716 writeln("VOLCANO PICK 1: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4721 void startWinCutsceneWinFall () {
4722 global.hasParachute = false;
4724 writeln("FALL HOLD 0: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4725 writeln("FALL PICK 0: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4729 winCutsceneSwitchToNext = false;
4731 auto olddel = ImmediateDelete;
4732 ImmediateDelete = false;
4736 setMenuTilesVisible(false);
4738 //addBackgroundGfxDetails();
4741 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
4743 blockWaterChecking = true;
4747 ImmediateDelete = olddel;
4748 CollectGarbage(true); // destroy delayed objects too
4750 if (dumpGridStats) objGrid.dumpStats();
4752 playerExited = false; // just in case
4753 playerExitDoor = none;
4761 winCutSceneTimer = -1;
4762 winCutScenePhase = 0;
4764 player.dead = false;
4765 player.active = true;
4766 player.visible = false;
4767 player.removeBallAndChain(temp:true);
4768 player.stunned = false;
4769 player.status = MapObject::FALLING;
4770 if (player.holdItem) player.holdItem.visible = false;
4771 player.fltx = 320/2;
4774 winSceneDrawStatus = 0;
4781 writeln("FALL HOLD 1: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4782 writeln("FALL PICK 1: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4787 void setGameOver () {
4788 if (inWinCutscene) {
4789 player.visible = false;
4790 player.removeBallAndChain(temp:true);
4791 if (player.holdItem) player.holdItem.visible = false;
4794 if (inWinCutscene > 0) {
4797 winSceneDrawStatus = 8;
4802 MapTile findEndPlatTile () {
4803 return forEachTile(delegate bool (MapTile t) { return (t isa MapTileEndPlat); }, castClass:MapTileEndPlat);
4807 MapObject findBigTreasure () {
4808 return forEachObject(delegate bool (MapObject o) { return (o isa MapObjectBigTreasure); }, castClass:MapObjectBigTreasure);
4812 void setMenuTilesVisible (bool vis) {
4814 forEachTile(delegate bool (MapTile t) {
4815 if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4816 t.invisible = false;
4821 forEachTile(delegate bool (MapTile t) {
4822 if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4831 void setMenuTilesOnTop () {
4832 forEachTile(delegate bool (MapTile t) {
4833 if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4841 void winCutscenePlayerControl (PlayerPawn plr) {
4842 auto payPress = isKeyPressed(GameConfig::Key.Pay);
4843 auto payRelease = isKeyReleased(GameConfig::Key.Pay);
4845 switch (winCutsceneSkip) {
4846 case 0: // nothing was pressed
4847 if (payPress) winCutsceneSkip = 1;
4849 case 1: // waiting for pay release
4850 if (payRelease) winCutsceneSkip = 2;
4852 case 2: // pay released, do skip
4857 // first winning room
4858 if (inWinCutscene == 1) {
4859 if (plr.ix < 448+8) {
4864 // waiting for chest to open
4865 if (winCutScenePhase == 0) {
4866 winCutSceneTimer = 120/2;
4867 winCutScenePhase = 1;
4872 if (winCutScenePhase == 1) {
4873 if (--winCutSceneTimer == 0) {
4874 winCutScenePhase = 2;
4875 winCutSceneTimer = 20;
4876 forEachObject(delegate bool (MapObject o) {
4877 if (o isa MapObjectBigChest) {
4878 o.setSprite(global.config.gameMode == GameConfig::GameMode.Vanilla ? 'sBigChestOpen' : 'sBigChestOpen2');
4879 auto treasure = MakeMapObject(o.ix, o.iy, 'oBigTreasure');
4883 o.playSound('sndClick');
4884 //!!!if (global.config.gameMode != GameConfig::GameMode.Vanilla) scrSprayGems(oBigChest.x+24, oBigChest.y+24);
4894 if (winCutScenePhase == 2) {
4895 if (--winCutSceneTimer == 0) {
4896 winCutScenePhase = 3;
4897 winCutSceneTimer = 50;
4903 if (winCutScenePhase == 3) {
4904 auto ep = findEndPlatTile();
4905 if (ep) MakeMapObject(ep.ix+global.randOther(0, 80), /*ep.iy*/192+32, 'oBurn');
4906 if (--winCutSceneTimer == 0) {
4907 winCutScenePhase = 4;
4908 winCutSceneTimer = 10;
4909 if (ep) MakeMapObject(ep.ix, ep.iy+30, 'oLavaSpray');
4915 // lava pump first accel
4916 if (winCutScenePhase == 4) {
4917 if (--winCutSceneTimer == 0) {
4918 forEachObject(delegate bool (MapObject o) {
4919 if (o isa MapObjectLavaSpray) o.yAcc = -0.1;
4925 // lava pump complete
4926 if (winCutScenePhase == 5) {
4927 if (--winCutSceneTimer == 0) {
4928 //if (oLavaSpray) oLavaSpray.yAcc = -0.1;
4929 startWinCutsceneVolcano();
4938 if (inWinCutscene == 2) {
4942 if (winCutScenePhase == 0) {
4943 winCutSceneTimer = 50;
4944 winCutScenePhase = 1;
4945 winVolcanoTimer = 10;
4949 if (winVolcanoTimer > 0) {
4950 if (--winVolcanoTimer == 0) {
4951 MakeMapObject(224+global.randOther(0,48), 144+global.randOther(0,8), 'oVolcanoFlame');
4952 winVolcanoTimer = global.randOther(10, 20);
4957 if (winCutScenePhase == 1) {
4958 if (--winCutSceneTimer == 0) {
4959 winCutSceneTimer = 30;
4960 winCutScenePhase = 2;
4961 auto sil = MakeMapObject(240, 132, 'oPlayerSil');
4969 if (winCutScenePhase == 2) {
4970 if (--winCutSceneTimer == 0) {
4971 winCutScenePhase = 3;
4972 auto sil = MakeMapObject(240, 132, 'oTreasureSil');
4982 // winning camel room
4983 if (inWinCutscene == 3) {
4984 //if (!player.holdItem) writeln("SCENE 3: LOST ITEM!");
4986 if (!plr.visible) plr.flty = -32;
4989 if (winCutScenePhase == 0) {
4990 winCutSceneTimer = 50;
4991 winCutScenePhase = 1;
4996 if (winCutScenePhase == 1) {
4997 if (--winCutSceneTimer == 0) {
4998 winCutSceneTimer = 50;
4999 winCutScenePhase = 2;
5000 plr.playSound('sndPFall');
5003 writeln("MUST BE CHAINED: ", plr.mustBeChained);
5004 if (plr.mustBeChained) {
5005 plr.removeBallAndChain(temp:true);
5006 plr.spawnBallAndChain();
5009 writeln("HOLD: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
5010 writeln("PICK: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
5012 if (!player.holdItem && player.pickedItem) player.scrSwitchToPocketItem(forceIfEmpty:false);
5013 if (player.holdItem) {
5014 player.holdItem.visible = true;
5015 player.holdItem.canLiveOutsideOfLevel = true;
5016 writeln("HOLD ITEM: '", GetClassName(player.holdItem.Class), "'");
5018 plr.status == MapObject::FALLING;
5019 global.plife += 99; // just in case
5024 if (winCutScenePhase == 2) {
5025 auto ball = plr.getMyBall();
5026 if (ball && plr.holdItem != ball) {
5027 ball.teleportTo(plr.fltx, plr.flty+8);
5031 if (plr.status == MapObject::STUNNED || plr.stunned) {
5035 auto treasure = MakeMapObject(144+16+8, -32, 'oBigTreasure');
5036 if (treasure) treasure.depth = 1;
5037 winCutScenePhase = 3;
5039 plr.playSound('sndTFall');
5044 if (winCutScenePhase == 3) {
5045 if (plr.status != MapObject::STUNNED && !plr.stunned) {
5046 auto bt = findBigTreasure();
5050 //plr.status = MapObject::JUMPING;
5052 plr.kJumpPressed = true;
5053 winCutScenePhase = 4;
5054 winCutSceneTimer = 50;
5061 if (winCutScenePhase == 4) {
5062 if (--winCutSceneTimer == 0) {
5063 setMenuTilesVisible(true);
5064 winCutScenePhase = 5;
5065 winSceneDrawStatus = 1;
5067 global.setMusicPitch(1.0);
5068 global.playMusic('musVictory', loop:false);
5069 winCutSceneTimer = 50;
5074 if (winCutScenePhase == 5) {
5075 if (winSceneDrawStatus == 3) {
5076 int money = stats.money;
5077 if (winMoneyCount < money) {
5078 if (money-winMoneyCount > 1000) {
5079 winMoneyCount += 1000;
5080 } else if (money-winMoneyCount > 100) {
5081 winMoneyCount += 100;
5082 } else if (money-winMoneyCount > 10) {
5083 winMoneyCount += 10;
5088 if (winMoneyCount >= money) {
5089 winMoneyCount = money;
5090 ++winSceneDrawStatus;
5095 if (winSceneDrawStatus == 7) {
5098 if (winFadeLevel >= 255) {
5099 ++winSceneDrawStatus;
5100 winCutSceneTimer = 30*30;
5105 if (winSceneDrawStatus == 8) {
5106 if (--winCutSceneTimer == 0) {
5112 if (--winCutSceneTimer == 0) {
5113 ++winSceneDrawStatus;
5114 winCutSceneTimer = 50;
5123 // ////////////////////////////////////////////////////////////////////////// //
5124 void renderWinCutsceneOverlay () {
5125 if (inWinCutscene == 3) {
5126 if (winSceneDrawStatus > 0) {
5127 Video.color = 0xff_ff_ff;
5128 sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
5129 //draw_set_color(txtCol);
5130 drawTextAt(64, 32, "YOU MADE IT!");
5132 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
5133 if (global.config.gameMode == GameConfig::GameMode.Vanilla) {
5134 Video.color = 0xff_ff_00; //draw_set_color(c_yellow);
5135 drawTextAt(64, 48, "Classic Mode done!");
5137 Video.color = 0x00_80_80; //draw_set_color(c_teal);
5138 if (global.config.bizarrePlus) drawTextAt(64, 48, "Bizarre Mode Plus done!");
5139 else drawTextAt(64, 48, "Bizarre Mode done!");
5140 //draw_set_color(c_white);
5142 if (!global.usedShortcut) {
5143 Video.color = 0xc0_c0_c0; //draw_set_color(c_silver);
5144 drawTextAt(64, 56, "No shortcuts used!");
5145 //draw_set_color(c_yellow);
5149 if (winSceneDrawStatus > 1) {
5150 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
5151 //draw_set_color(txtCol);
5152 Video.color = 0xff_ff_ff;
5153 drawTextAt(64, 64, "FINAL SCORE:");
5156 if (winSceneDrawStatus > 2) {
5157 sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
5158 Video.color = 0xff_ff_ff; //draw_set_color(c_white);
5159 drawTextAt(64, 72, va("$%d", winMoneyCount));
5162 if (winSceneDrawStatus > 4) {
5163 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
5164 Video.color = 0xff_ff_ff; //draw_set_color(c_white);
5165 drawTextAt(64, 96, va("Time: %s", time2str(winTime/30)));
5167 draw_set_color(c_white);
5168 if (s < 10) draw_text(96+24, 96, string(m) + ":0" + string(s));
5169 else draw_text(96+24, 96, string(m) + ":" + string(s));
5173 if (winSceneDrawStatus > 5) {
5174 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
5175 Video.color = 0xff_ff_ff; //draw_set_color(txtCol);
5176 drawTextAt(64, 96+8, "Kills: ");
5177 Video.color = 0xff_ff_ff; //draw_set_color(c_white);
5178 drawTextAt(96+24, 96+8, va("%s", stats.countKills()));
5181 if (winSceneDrawStatus > 6) {
5182 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
5183 Video.color = 0xff_ff_ff; //draw_set_color(txtCol);
5184 drawTextAt(64, 96+16, "Saves: ");
5185 Video.color = 0xff_ff_ff; //draw_set_color(c_white);
5186 drawTextAt(96+24, 96+16, va("%s", stats.damselsSaved));
5190 Video.color = (255-clamp(winFadeLevel, 0, 255))<<24;
5191 Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
5194 if (winSceneDrawStatus == 8) {
5195 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
5196 Video.color = 0xff_ff_ff; //draw_set_color(c_white);
5198 if (global.config.gameMode == GameConfig::GameMode.Vanilla) {
5199 Video.color = 0xff_ff_00; //draw_set_color(c_yellow);
5200 lastString = "YOU SHALL BE REMEMBERED AS A HERO.";
5202 Video.color = 0x00_ff_ff;
5203 if (global.config.bizarrePlus) lastString = "ANOTHER LEGENDARY ADVENTURE!";
5204 else lastString = "YOUR DISCOVERIES WILL BE CELEBRATED!";
5206 auto strLen = lastString.length*8;
5208 n = trunc(ceil(n/2.0));
5209 drawTextAt(n, 116, lastString);
5215 // ////////////////////////////////////////////////////////////////////////// //
5216 #include "roomTitle.vc"
5217 #include "roomTrans1.vc"
5218 #include "roomTrans2.vc"
5219 #include "roomTrans3.vc"
5220 #include "roomTrans4.vc"
5221 #include "roomOlmec.vc"
5222 #include "roomEnd.vc"
5223 #include "roomIntro.vc"
5224 #include "roomTutorial.vc"
5225 #include "roomScores.vc"
5226 #include "roomStars.vc"
5227 #include "roomSun.vc"
5228 #include "roomMoon.vc"
5231 // ////////////////////////////////////////////////////////////////////////// //
5232 #include "packages/Generator/loadRoomGens.vc"
5233 #include "packages/Generator/loadEntityGens.vc"