1 /**********************************************************************************
2 * Copyright (c) 2008, 2009 Derek Yu and Mossmouth, LLC
3 * Copyright (c) 2018, Ketmar Dark
5 * This file is part of Spelunky.
7 * You can redistribute and/or modify Spelunky, including its source code, under
8 * the terms of the Spelunky User License.
10 * Spelunky is distributed in the hope that it will be entertaining and useful,
11 * but WITHOUT WARRANTY. Please see the Spelunky User License for more details.
13 * The Spelunky User License should be available in "Game .Information", which
14 * can be found in the Resource Explorer, or as an external file called COPYING.
15 * If not, please obtain a new copy of Spelunky from <http://spelunkyworld.com/>
17 **********************************************************************************/
18 // this is the level we're playing in, with all objects and tiles
19 class GameLevel : Object;
21 //#define EXPERIMENTAL_RENDER_CACHE
23 const float FrameTime = 1.0f/30.0f;
25 const int dumpGridStats = true;
32 //enum NormalTilesWidth = LevelGen::LevelWidth*RoomGen::Width+2;
33 //enum NormalTilesHeight = LevelGen::LevelHeight*RoomGen::Height+2;
35 enum MaxTilesWidth = 64;
36 enum MaxTilesHeight = 64;
39 transient GameStats stats;
40 transient SpriteStore sprStore;
41 transient BackTileStore bgtileStore;
42 transient BackTileImage levBGImg;
45 transient name lastMusicName;
46 //RoomGen[LevelGen::LevelWidth, LevelGen::LevelHeight] rooms; // moved to levelgen
48 transient float accumTime;
49 transient bool gamePaused = false;
50 transient bool gameShowHelp = false;
51 transient int gameHelpScreen = 0;
52 const int MaxGameHelpScreen = 2;
53 transient bool checkWater;
54 transient int liquidTileCount; // cached
55 /*transient*/ int damselSaved;
59 transient int collectCounter;
60 /*transient*/ int levelMoneyStart;
62 // all movable (thinkable) map objects
63 EntityGrid objGrid; // monsters, items and tiles
65 MapBackTile backtiles;
66 bool blockWaterChecking;
70 LevelGen::RType[MaxTilesWidth, MaxTilesHeight] roomType;
83 LevelKind levelKind = LevelKind.Normal;
85 array!MapTile allEnters;
86 array!MapTile allExits;
89 int startRoomX, startRoomY;
90 int endRoomX, endRoomY;
93 transient bool playerExited;
94 transient MapEntity playerExitDoor;
95 transient bool disablePlayerThink = false;
96 transient int maxPlayingTime; // in seconds
102 bool ghostSpawned; // to speed up some checks
103 bool resetBMCOG = false;
107 // FPS, i.e. incremented by 30 in one second
108 int time; // in frames
109 int lastUsedObjectId;
110 transient int lastRenderTime = -1;
111 transient int pausedTime;
113 MapEntity deadItemsHead;
115 // screen shake variables
120 // set this before calling `fixCamera()`
121 // dimensions should be real, not scaled up/down
122 transient int viewWidth, viewHeight;
123 // room bounds, not scaled
124 IVec2D viewMin, viewMax;
126 // for Olmec level cinematics
127 IVec2D cameraSlideToDest;
128 IVec2D cameraSlideToCurr;
129 IVec2D cameraSlideToSpeed; // !0: slide
130 int cameraSlideToPlayer;
131 // `fixCamera()` will set the following
132 // coordinates will be real too (with scale applied)
133 // shake is not applied
134 transient IVec2D viewStart; // with `player.viewOffset`
135 private transient IVec2D realViewStart; // without `player.viewOffset`
137 transient int framesProcessedFromLastClear;
139 transient int BuildYear;
140 transient int BuildMonth;
141 transient int BuildDay;
142 transient int BuildHour;
143 transient int BuildMin;
144 transient string BuildDateString;
147 final string getBuildDateString () {
148 if (!BuildYear) return BuildDateString;
149 if (BuildDateString) return BuildDateString;
150 BuildDateString = va("%d-%02d-%02d %02d:%02d", BuildYear, BuildMonth, BuildDay, BuildHour, BuildMin);
151 return BuildDateString;
155 final void cameraSlideToPoint (int dx, int dy, int speedx, int speedy) {
156 cameraSlideToPlayer = 0;
157 cameraSlideToDest.x = dx;
158 cameraSlideToDest.y = dy;
159 cameraSlideToSpeed.x = abs(speedx);
160 cameraSlideToSpeed.y = abs(speedy);
161 cameraSlideToCurr.x = cameraCurrX;
162 cameraSlideToCurr.y = cameraCurrY;
166 final void cameraReturnToPlayer () {
167 if (!cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y)) {
168 cameraSlideToCurr.x = cameraCurrX;
169 cameraSlideToCurr.y = cameraCurrY;
170 if (cameraSlideToSpeed.x && abs(cameraSlideToSpeed.x) < 8) cameraSlideToSpeed.x = 8;
171 if (cameraSlideToSpeed.y && abs(cameraSlideToSpeed.y) < 8) cameraSlideToSpeed.y = 8;
172 cameraSlideToPlayer = 1;
177 // if `frameSkip` is `true`, there are more frames waiting
178 // (i.e. you may skip rendering and such)
179 transient void delegate (bool frameSkip) onBeforeFrame;
180 transient void delegate (bool frameSkip) onAfterFrame;
182 transient void delegate () onCameraTeleported;
184 transient void delegate () onLevelExitedCB;
186 // this will be called in-between frames, and
187 // `frameTime` is [0..1)
188 transient void delegate (float frameTime) onInterFrame;
190 final int bizRoomStyle { get { return (lg ? lg.bizRoomStyle : 0); } }
193 final bool isNormalLevel () { return (levelKind == LevelKind.Normal); }
194 final bool isTitleRoom () { return (levelKind == LevelKind.Title); }
195 final bool isTutorialRoom () { return (levelKind == LevelKind.Tutorial); }
197 bool isHUDEnabled () {
198 if (inWinCutscene) return false;
199 if (lg.finalBossLevel) return true;
200 if (isNormalLevel()) return true;
201 // allow HUD in challenge chambers
206 // ////////////////////////////////////////////////////////////////////////// //
208 void addDeath (name aname) { if (isNormalLevel()) stats.addDeath(aname); }
215 void addKill (name aname, optional bool telefrag) {
216 if (isNormalLevel()) stats.addKill(aname, telefrag!optional);
217 else if (aname == 'Shopkeeper' && levelKind == LevelKind.Stars) { ++stats.starsKills; ++starsKills; }
220 void addCollect (name aname, optional int amount) { if (isNormalLevel()) stats.addCollect(aname, amount!optional); }
222 void addDamselSaved () { if (isNormalLevel()) stats.addDamselSaved(); }
223 void addIdolStolen () { if (isNormalLevel()) stats.addIdolStolen(); }
224 void addIdolConverted () { if (isNormalLevel()) stats.addIdolConverted(); }
225 void addCrystalIdolStolen () { if (isNormalLevel()) stats.addCrystalIdolStolen(); }
226 void addCrystalIdolConverted () { if (isNormalLevel()) stats.addCrystalIdolConverted(); }
227 void addGhostSummoned () { if (isNormalLevel()) stats.addGhostSummoned(); }
230 // ////////////////////////////////////////////////////////////////////////// //
231 static final string time2str (int time) {
232 int secs = time%60; time /= 60;
233 int mins = time%60; time /= 60;
234 int hours = time%24; time /= 24;
236 if (days) return va("%d DAYS, %d:%02d:%02d", days, hours, mins, secs);
237 if (hours) return va("%d:%02d:%02d", hours, mins, secs);
238 return va("%02d:%02d", mins, secs);
242 // ////////////////////////////////////////////////////////////////////////// //
243 final int tilesWidth () { return lg.levelRoomWidth*RoomGen::Width+2; }
244 final int tilesHeight () { return (lg.finalBossLevel ? 55 : lg.levelRoomHeight*RoomGen::Height+2); }
247 // ////////////////////////////////////////////////////////////////////////// //
248 protected void resetGameInternal () {
249 if (player) player.removeBallAndChain();
263 player.removeBallAndChain();
264 auto hi = player.holdItem;
265 player.holdItem = none;
266 if (hi) hi.instanceRemove();
267 hi = player.pickedItem;
268 player.pickedItem = none;
269 if (hi) hi.instanceRemove();
276 stats.clearGameTotals();
280 // this won't generate a level yet
281 void restartGame () {
283 if (global.startMoney > 0) stats.setMoneyCheat();
284 stats.setMoney(global.startMoney);
285 levelKind = LevelKind.Normal;
289 // complement function to `restart game`
290 void generateNormalLevel () {
292 centerViewAtPlayer();
296 void restartTitle () {
299 createSpecialLevel(LevelKind.Title, &createTitleRoom, 'musTitle');
308 void restartTutorial () {
311 createSpecialLevel(LevelKind.Tutorial, &createTutorialRoom, 'musCave');
320 void restartScores () {
323 createSpecialLevel(LevelKind.Scores, &createScoresRoom, 'musTitle');
332 void restartStarsRoom () {
335 createSpecialLevel(LevelKind.Stars, &createStarsRoom, '');
344 void restartSunRoom () {
347 createSpecialLevel(LevelKind.Sun, &createSunRoom, '');
356 void restartMoonRoom () {
359 createSpecialLevel(LevelKind.Moon, &createMoonRoom, '');
368 // ////////////////////////////////////////////////////////////////////////// //
369 // generate angry shopkeeper at exit if murderer or thief
370 void generateAngryShopkeepers () {
371 if (global.murderer || global.thiefLevel > 0) {
372 foreach (MapTile e; allExits) {
373 auto obj = MonsterShopkeeper(MakeMapObject(e.ix, e.iy, 'oShopkeeper'));
375 obj.style = 'Bounty Hunter';
376 obj.status = MapObject::PATROL;
383 // ////////////////////////////////////////////////////////////////////////// //
384 final void resetRoomBounds () {
387 viewMax.x = tilesWidth*16;
388 viewMax.y = tilesHeight*16;
389 // Great Lake is bottomless (nope)
390 //if (global.lake == 1) viewMax.y -= 16;
391 //writeln("ROOM BOUNDS: (", viewMax.x, "x", viewMax.y, ")");
395 final void setRoomBounds (int x0, int y0, int x1, int y1) {
403 // ////////////////////////////////////////////////////////////////////////// //
406 float timeout; // seconds
407 float starttime; // for active
408 bool active; // true: timeout is `GetTickCount()` dismissing time
411 array!OSDMessage msglist; // [0]: current one
414 private final void osdCheckTimeouts () {
415 auto stt = GetTickCount();
416 while (msglist.length) {
417 if (!msglist[0].active) {
418 msglist[0].active = true;
419 msglist[0].starttime = stt;
421 if (msglist[0].starttime+msglist[0].timeout >= stt) break;
427 final bool osdHasMessage () {
429 return (msglist.length > 0);
433 final string osdGetMessage (out float timeLeft, out float timeStart) {
435 if (msglist.length == 0) { timeLeft = 0; return ""; }
436 auto stt = GetTickCount();
437 timeStart = msglist[0].starttime;
438 timeLeft = fmax(0, msglist[0].starttime+msglist[0].timeout-stt);
439 return msglist[0].msg;
443 final void osdClear () {
448 final void osdMessage (string msg, optional float timeout, optional bool urgent) {
450 msg = global.expandString(msg);
451 if (!specified_timeout) timeout = 3.33;
452 // special message for shops
453 if (timeout == -666) {
455 if (msglist.length && msglist[0].msg == msg) return;
456 if (msglist.length == 0 || msglist[0].msg != msg) {
459 msglist[0].msg = msg;
461 msglist[0].active = false;
462 msglist[0].timeout = 3.33;
466 if (timeout < 0.1) return;
467 timeout = fmax(1.0, timeout);
468 //writeln("OSD: ", msg);
469 // find existing one, and bring it to the top
471 for (; oldidx < msglist.length; ++oldidx) {
472 if (msglist[oldidx].msg == msg) break; // i found her!
475 if (oldidx < msglist.length) {
476 // yeah, move duplicate to the top
477 msglist[oldidx].timeout = fmax(timeout, msglist[oldidx].timeout);
478 msglist[oldidx].active = false;
479 if (urgent && oldidx != 0) {
480 timeout = msglist[oldidx].timeout;
481 msglist.remove(oldidx);
483 msglist[0].msg = msg;
484 msglist[0].timeout = timeout;
485 msglist[0].active = false;
489 msglist[0].msg = msg;
490 msglist[0].timeout = timeout;
491 msglist[0].active = false;
495 msglist[$-1].msg = msg;
496 msglist[$-1].timeout = timeout;
497 msglist[$-1].active = false;
503 // ////////////////////////////////////////////////////////////////////////// //
504 void setup (GameGlobal aGlobal, SpriteStore aSprStore, BackTileStore aBGTileStore) {
506 sprStore = aSprStore;
507 bgtileStore = aBGTileStore;
509 lg = SpawnObject(LevelGen);
513 objGrid = SpawnObject(EntityGrid);
514 objGrid.setup(0, 0, MaxTilesWidth*16, MaxTilesHeight*16);
518 // stores should be set
522 levBGImg = bgtileStore[levBGImgName];
523 foreach (MapEntity o; objGrid.allObjects()) {
526 if (t && (t.lava || t.water)) ++liquidTileCount;
528 for (MapBackTile bt = backtiles; bt; bt = bt.next) bt.onLoaded();
529 if (player) player.onLoaded();
531 if (msglist.length) {
532 msglist[0].active = false;
533 msglist[0].timeout = 0.200;
536 if (lg && lg.musicName) global.playMusic(lg.musicName); else global.stopMusic();
540 // ////////////////////////////////////////////////////////////////////////// //
541 void pickedSpectacles () {
542 foreach (MapTile t; objGrid.allObjectsSafe(MapTile)) t.onGotSpectacles();
546 // ////////////////////////////////////////////////////////////////////////// //
547 #include "rgentile.vc"
548 #include "rgenobj.vc"
551 void onLevelExited () {
552 if (playerExitDoor isa TitleTileXTitle) {
553 playerExitDoor = none;
558 if (isTitleRoom() || levelKind == LevelKind.Scores) {
559 if (playerExitDoor) processTitleExit(playerExitDoor);
560 playerExitDoor = none;
563 if (isTutorialRoom()) {
564 playerExitDoor = none;
566 global.currLevel = 1;
567 generateNormalLevel();
571 if (levelKind == LevelKind.Stars || levelKind == LevelKind.Sun || levelKind == LevelKind.Moon) {
572 playerExitDoor = none;
574 if (onLevelExitedCB) onLevelExitedCB();
579 if (isNormalLevel()) {
580 stats.maxLevelComplete = max(stats.maxLevelComplete, global.currLevel);
582 if (playerExitDoor) {
583 if (playerExitDoor.objType == 'oXGold') {
584 writeln("exiting to City Of Gold");
585 global.cityOfGold = true;
586 //!global.currLevel += 1;
587 } else if (playerExitDoor.objType == 'oXMarket') {
588 writeln("exiting to Black Market");
589 global.genBlackMarket = true;
590 //!global.currLevel += 1;
594 if (onLevelExitedCB) onLevelExitedCB();
596 playerExitDoor = none;
597 if (levelKind == LevelKind.Transition) {
598 if (global.thiefLevel > 0) global.thiefLevel -= 1;
599 if (global.alienCraft) ++global.alienCraft;
600 if (global.yetiLair) ++global.yetiLair;
601 if (global.lake) ++global.lake;
602 if (global.cityOfGold) ++global.cityOfGold;
603 //orig dbg:if (global.currLevel == 1) global.currLevel += firstLevelSkip; else global.currLevel += levelSkip;
605 if (!global.blackMarket && !global.cityOfGold /*&& !global.yetiLair*/) {
606 global.currLevel += 1;
612 // < 20 seconds per level: looks like a speedrun
613 global.noDarkLevel = (levelEndTime > levelStartTime && levelEndTime-levelStartTime < 20*30);
614 if (lg.finalBossLevel) {
617 // add money for big idol
618 player.addScore(50000);
622 generateTransitionLevel();
625 //centerViewAtPlayer();
629 void onOlmecDead (MapObject o) {
630 writeln("*** OLMEC IS DEAD!");
631 foreach (MapTile t; allExits) {
634 auto st = checkTileAtPoint(t.ix+8, t.iy+16);
636 st = MakeMapTile(t.ix/16, t.iy/16+1, 'oTemple');
639 st.invincible = true;
645 void generateLevelMessages () {
646 writeln("LEVEL NUMBER: ", global.currLevel);
647 if (global.darkLevel) {
648 if (global.hasCrown) {
649 osdMessage("THE HEDJET SHINES BRIGHTLY.");
650 global.darkLevel = false;
651 } else if (global.config.scumDarkness < 2) {
652 osdMessage("I CAN'T SEE A THING!\nI'D BETTER USE THESE FLARES!");
656 if (global.blackMarket) osdMessage("WELCOME TO THE BLACK MARKET!");
658 if (global.cemetary) osdMessage("THE DEAD ARE RESTLESS!");
659 if (global.lake == 1) osdMessage("I CAN HEAR RUSHING WATER...");
661 if (global.snakePit) osdMessage("I HEAR SNAKES... I HATE SNAKES!");
662 if (global.yetiLair == 1) osdMessage("IT SMELLS LIKE WET FUR IN HERE!");
663 if (global.alienCraft == 1) osdMessage("THERE'S A PSYCHIC PRESENCE HERE!");
664 if (global.cityOfGold == 1) {
665 if (true /*hasOlmec()*/) osdMessage("IT'S THE LEGENDARY CITY OF GOLD!");
668 if (global.sacrificePit) osdMessage("I CAN HEAR PRAYERS TO KALI!");
672 final MapObject debugSpawnObjectWithClass (class!MapObject oclass, optional bool playerDir) {
673 if (!oclass) return none;
675 bool canLeft = !isSolidAtPoint(player.ix-8, player.iy);
676 bool canRight = !isSolidAtPoint(player.ix+16, player.iy);
677 if (!canLeft && !canRight) return none;
678 if (canLeft && canRight) {
680 dx = (player.dir == MapObject::Dir.Left ? -16 : 16);
685 dx = (canLeft ? -16 : 16);
687 auto obj = SpawnMapObjectWithClass(oclass);
688 if (obj isa MapEnemy) { dx -= 8; dy -= 8; }
689 if (obj) PutSpawnedMapObject(player.ix+dx, player.iy+dy, obj);
694 final MapObject debugSpawnObject (name aname) {
695 if (!aname) return none;
696 return debugSpawnObjectWithClass(findGameObjectClassByName(aname));
700 void createSpecialLevel (LevelKind kind, scope void delegate () creator, name amusic) {
701 global.darkLevel = false;
705 global.resetStartingItems();
707 global.setMusicPitch(1.0);
710 auto olddel = ImmediateDelete;
711 ImmediateDelete = false;
719 addBackgroundGfxDetails();
720 //levBGImgName = 'bgCave';
721 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
723 blockWaterChecking = true;
727 ImmediateDelete = olddel;
728 CollectGarbage(true); // destroy delayed objects too
730 if (dumpGridStats) objGrid.dumpStats();
732 playerExited = false; // just in case
733 playerExitDoor = none;
738 lg.musicName = amusic;
739 if (amusic) global.playMusic(lg.musicName); else global.stopMusic();
743 void createTitleLevel () {
744 createSpecialLevel(LevelKind.Title, &createTitleRoom, 'musTitle');
748 void createTutorialLevel () {
749 createSpecialLevel(LevelKind.Tutorial, &createTutorialRoom, 'musCave');
758 // `global.currLevel` is the new level
759 void generateTransitionLevel () {
760 global.darkLevel = false;
765 global.setMusicPitch(1.0);
766 switch (global.config.transitionMusicMode) {
767 case GameConfig::MusicMode.Silent: global.stopMusic(); break;
768 case GameConfig::MusicMode.Restart: global.restartMusic(); break;
769 case GameConfig::MusicMode.DontTouch: break;
772 levelKind = LevelKind.Transition;
774 auto olddel = ImmediateDelete;
775 ImmediateDelete = false;
778 if (global.currLevel < 4) createTrans1Room();
779 else if (global.currLevel == 4) createTrans1xRoom();
780 else if (global.currLevel < 8) createTrans2Room();
781 else if (global.currLevel == 8) createTrans2xRoom();
782 else if (global.currLevel < 12) createTrans3Room();
783 else if (global.currLevel == 12) createTrans3xRoom();
784 else if (global.currLevel < 16) createTrans4Room();
785 else if (global.currLevel == 16) createTrans4Room();
786 else createTrans1Room(); //???
791 addBackgroundGfxDetails();
792 //levBGImgName = 'bgCave';
793 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
795 blockWaterChecking = true;
799 if (damselSaved > 0) {
800 // this is special "damsel ready to kiss you" object, not a heart
801 MakeMapObject(176+8, 176+8, 'oDamselKiss');
802 global.plife += damselSaved; // if player skipped transition cutscene
806 ImmediateDelete = olddel;
807 CollectGarbage(true); // destroy delayed objects too
809 if (dumpGridStats) objGrid.dumpStats();
811 playerExited = false; // just in case
812 playerExitDoor = none;
817 //global.playMusic(lg.musicName);
821 void generateLevel () {
822 levelStartTime = time;
828 global.genBlackMarket = false;
831 global.setMusicPitch(1.0);
832 stats.clearLevelTotals();
834 levelKind = LevelKind.Normal;
841 //writeln("tw:", tilesWidth, "; th:", tilesHeight);
843 auto olddel = ImmediateDelete;
844 ImmediateDelete = false;
847 if (lg.finalBossLevel) {
848 blockWaterChecking = true;
852 // if transition cutscene was skipped...
853 global.plife += max(0, damselSaved); // if player skipped transition cutscene
857 startRoomX = lg.startRoomX;
858 startRoomY = lg.startRoomY;
859 endRoomX = lg.endRoomX;
860 endRoomY = lg.endRoomY;
861 addBackgroundGfxDetails();
862 foreach (int y; 0..tilesHeight) {
863 foreach (int x; 0..tilesWidth) {
869 levBGImgName = lg.bgImgName;
870 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
872 if (global.allowAngryShopkeepers) generateAngryShopkeepers();
874 lg.generateEntities();
876 // add box of flares to dark level
877 if (global.darkLevel && allEnters.length) {
878 auto enter = allEnters[0];
879 int x = enter.ix, y = enter.iy;
880 if (!isSolidAtPoint(x-16, y)) MakeMapObject(x-16+8, y+8, 'oFlareCrate');
881 else if (!isSolidAtPoint(x+16, y)) MakeMapObject(x+16+8, y+8, 'oFlareCrate');
882 else MakeMapObject(x+8, y+8, 'oFlareCrate');
885 //scrGenerateEntities();
886 //foreach (; 0..2) scrGenerateEntities();
888 writeln(objGrid.countObjects, " alive objects inserted");
889 writeln(countBackTiles, " background tiles inserted");
891 if (!player) FatalError("player pawn is not spawned");
893 if (lg.finalBossLevel) {
894 blockWaterChecking = true;
896 blockWaterChecking = false;
901 ImmediateDelete = olddel;
902 CollectGarbage(true); // destroy delayed objects too
904 if (dumpGridStats) objGrid.dumpStats();
906 playerExited = false; // just in case
907 playerExitDoor = none;
909 levelMoneyStart = stats.money;
912 generateLevelMessages();
917 if (lastMusicName != lg.musicName) {
918 global.playMusic(lg.musicName);
919 //writeln("!<", lastMusicName, ">:<", lg.musicName, ">");
921 //writeln("MM: ", global.config.nextLevelMusicMode);
922 switch (global.config.nextLevelMusicMode) {
923 case GameConfig::MusicMode.Silent: global.stopMusic(); break; // the thing that should not be
924 case GameConfig::MusicMode.Restart: global.restartMusic(); break;
925 case GameConfig::MusicMode.DontTouch:
926 if (global.config.transitionMusicMode == GameConfig::MusicMode.Silent) {
927 global.playMusic(lg.musicName);
932 lastMusicName = lg.musicName;
933 //global.playMusic(lg.musicName);
936 if (global.cityOfGold == 1 || global.genBlackMarket) resetBMCOG = true;
938 if (global.cityOfGold == 1) {
939 lg.mapSprite = 'sMapTemple';
940 lg.mapTitle = "City of Gold";
941 } else if (global.blackMarket) {
942 lg.mapSprite = 'sMapJungle';
943 lg.mapTitle = "Black Market";
948 // ////////////////////////////////////////////////////////////////////////// //
949 int currKeys, nextKeys;
950 int pressedKeysQ, releasedKeysQ;
951 int keysPressed, keysReleased = -1;
954 struct SavedKeyState {
955 int currKeys, nextKeys;
956 int pressedKeysQ, releasedKeysQ;
957 int keysPressed, keysReleased;
959 int roomSeed, otherSeed;
963 // for saving/replaying
964 final void keysSaveState (out SavedKeyState ks) {
965 ks.currKeys = currKeys;
966 ks.nextKeys = nextKeys;
967 ks.pressedKeysQ = pressedKeysQ;
968 ks.releasedKeysQ = releasedKeysQ;
969 ks.keysPressed = keysPressed;
970 ks.keysReleased = keysReleased;
973 // for saving/replaying
974 final void keysRestoreState (const ref SavedKeyState ks) {
975 currKeys = ks.currKeys;
976 nextKeys = ks.nextKeys;
977 pressedKeysQ = ks.pressedKeysQ;
978 releasedKeysQ = ks.releasedKeysQ;
979 keysPressed = ks.keysPressed;
980 keysReleased = ks.keysReleased;
984 final void keysNextFrame () {
989 final void clearKeys () {
999 final void onKey (int code, bool down) {
1004 if (keysReleased&code) {
1005 keysPressed |= code;
1006 keysReleased &= ~code;
1007 pressedKeysQ |= code;
1011 if (keysPressed&code) {
1012 keysReleased |= code;
1013 keysPressed &= ~code;
1014 releasedKeysQ |= code;
1019 final bool isKeyDown (int code) {
1020 return !!(currKeys&code);
1023 final bool isKeyPressed (int code) {
1024 bool res = !!(pressedKeysQ&code);
1025 pressedKeysQ &= ~code;
1029 final bool isKeyReleased (int code) {
1030 bool res = !!(releasedKeysQ&code);
1031 releasedKeysQ &= ~code;
1036 final void clearKeysPressRelease () {
1037 keysPressed = default.keysPressed;
1038 keysReleased = default.keysReleased;
1039 pressedKeysQ = default.pressedKeysQ;
1040 releasedKeysQ = default.releasedKeysQ;
1046 // ////////////////////////////////////////////////////////////////////////// //
1047 final void registerEnter (MapTile t) {
1054 final void registerExit (MapTile t) {
1061 final bool isYAtEntranceRow (int py) {
1063 foreach (MapTile t; allEnters) if (t.iy == py) return true;
1068 final int calcNearestEnterDist (int px, int py) {
1069 if (allEnters.length == 0) return int.max;
1070 int curdistsq = int.max;
1071 foreach (MapTile t; allEnters) {
1072 int xc = px-t.xCenter, yc = py-t.yCenter;
1073 int distsq = xc*xc+yc*yc;
1074 if (distsq < curdistsq) curdistsq = distsq;
1076 return round(sqrt(curdistsq));
1080 final int calcNearestExitDist (int px, int py) {
1081 if (allExits.length == 0) return int.max;
1082 int curdistsq = int.max;
1083 foreach (MapTile t; allExits) {
1084 int xc = px-t.xCenter, yc = py-t.yCenter;
1085 int distsq = xc*xc+yc*yc;
1086 if (distsq < curdistsq) curdistsq = distsq;
1088 return round(sqrt(curdistsq));
1092 // ////////////////////////////////////////////////////////////////////////// //
1093 final void clearForTransition () {
1094 auto olddel = ImmediateDelete;
1095 ImmediateDelete = false;
1097 ImmediateDelete = olddel;
1098 CollectGarbage(true); // destroy delayed objects too
1099 global.darkLevel = false;
1103 // ////////////////////////////////////////////////////////////////////////// //
1104 final int countBackTiles () {
1106 for (MapBackTile bt = backtiles; bt; bt = bt.next) ++res;
1111 final void clearWholeLevel () {
1115 // don't kill objects the player is holding
1117 if (player.pickedItem isa ItemBall) {
1118 player.pickedItem.instanceRemove();
1119 player.pickedItem = none;
1121 if (player.pickedItem && player.pickedItem.grid) {
1122 player.pickedItem.grid.remove(player.pickedItem.gridId);
1123 writeln("secured pocket item '", GetClassName(player.pickedItem.Class), "'");
1125 if (player.holdItem isa ItemBall) {
1126 player.removeBallAndChain(temp:true);
1127 if (player.holdItem) player.holdItem.instanceRemove();
1128 player.holdItem = none;
1130 if (player.holdItem && player.holdItem.grid) {
1131 player.holdItem.grid.remove(player.holdItem.gridId);
1132 writeln("secured held item '", GetClassName(player.holdItem.Class), "'");
1134 writeln("secured ball; mustBeChained=", player.mustBeChained, "; wasHoldingBall=", player.wasHoldingBall);
1137 int count = objGrid.countObjects();
1138 if (dumpGridStats) { if (objGrid.getFirstObjectCID()) objGrid.dumpStats(); }
1139 objGrid.removeAllObjects(true); // and destroy
1140 if (count > 0) writeln(count, " objects destroyed");
1142 lastUsedObjectId = 0;
1145 lastRenderTime = -1;
1146 liquidTileCount = 0;
1150 MapBackTile t = backtiles;
1156 framesProcessedFromLastClear = 0;
1160 final void insertObject (MapEntity o) {
1162 if (o.grid) FatalError("cannot put object into level twice");
1167 final void spawnPlayerAt (int x, int y) {
1168 // if we have no player, spawn new one
1169 // otherwise this just a level transition, so simply reposition him
1171 // don't add player to object list, as it has very separate processing anyway
1172 player = SpawnObject(PlayerPawn);
1173 player.global = global;
1174 player.level = self;
1175 if (!player.initialize()) {
1177 FatalError("something is wrong with player initialization");
1183 player.saveInterpData();
1185 if (player.mustBeChained || global.config.scumBallAndChain) {
1186 writeln("*** spawning ball and chain");
1187 player.spawnBallAndChain(levelStart:true);
1189 playerExited = false;
1190 playerExitDoor = none;
1191 if (global.config.startWithKapala) global.hasKapala = true;
1192 centerViewAtPlayer();
1193 // reinsert player items into grid
1194 if (player.pickedItem) objGrid.insert(player.pickedItem);
1195 if (player.holdItem) objGrid.insert(player.holdItem);
1196 //writeln("player spawned; active=", player.active);
1197 player.scrSwitchToPocketItem(forceIfEmpty:false);
1201 final void teleportPlayerTo (int x, int y) {
1205 player.saveInterpData();
1210 final void resurrectPlayer () {
1211 if (player) player.resurrect();
1212 playerExited = false;
1213 playerExitDoor = none;
1217 // ////////////////////////////////////////////////////////////////////////// //
1218 final void scrShake (int duration) {
1219 if (shakeLeft == 0) {
1225 shakeLeft = max(shakeLeft, duration);
1230 // ////////////////////////////////////////////////////////////////////////// //
1233 ItemStolen, // including damsel, lol
1240 // make the nearest shopkeeper angry. RAWR!
1241 void scrShopkeeperAnger (SCAnger reason, optional int maxdist, optional MapEntity offender) {
1242 if (!offender) offender = player;
1243 auto shp = MonsterShopkeeper(findNearestEnemy(offender.ix, offender.iy, delegate bool (MapEnemy o) {
1244 auto sc = MonsterShopkeeper(o);
1245 if (!sc) return false;
1246 if (sc.dead || sc.angered) return false;
1248 }, castClass:MonsterShopkeeper));
1251 if (specified_maxdist && offender.directionToEntityCenter(shp) > maxdist) return;
1252 if (!shp.dead && !shp.angered) {
1253 shp.status = MapObject::ATTACK;
1255 if (global.murderer) msg = "YOU'LL PAY FOR YOUR CRIMES!";
1256 else if (reason == SCAnger.ItemStolen) msg = "COME BACK HERE, THIEF!";
1257 else if (reason == SCAnger.TileDestroyed) msg = "DIE, YOU VANDAL!";
1258 else if (reason == SCAnger.BombDropped) msg = "TERRORIST!";
1259 else if (reason == SCAnger.DamselWhipped) msg = "HEY, ONLY I CAN DO THAT!";
1260 else if (reason == SCAnger.CrapsCheated) msg = "DIE, CHEATER!";
1261 else msg = "NOW I'M REALLY STEAMED!";
1262 if (msg) osdMessage(msg, -666);
1263 global.thiefLevel += (global.thiefLevel > 0 ? 3 : 2);
1269 final MapObject findCrapsPrize () {
1270 foreach (MapObject o; objGrid.allObjects(MapObject)) {
1271 if (!o.spectral && o.inDiceHouse) return o;
1277 // ////////////////////////////////////////////////////////////////////////// //
1278 // 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.
1279 // note: idols moved by monkeys will have false `stolenIdol`
1280 void scrTriggerIdolAltar (bool stolenIdol) {
1281 ObjTikiCurse res = none;
1282 int curdistsq = int.max;
1283 int px = player.xCenter, py = player.yCenter;
1284 foreach (MapObject o; objGrid.allObjects(MapObject)) {
1285 auto tcr = ObjTikiCurse(o);
1287 if (tcr.activated) continue;
1288 int xc = px-tcr.xCenter, yc = py-tcr.yCenter;
1289 int distsq = xc*xc+yc*yc;
1290 if (distsq < curdistsq) {
1295 if (res) res.activate(stolenIdol);
1299 // ////////////////////////////////////////////////////////////////////////// //
1300 void setupGhostTime () {
1301 musicFadeTimer = -1;
1302 ghostSpawned = false;
1304 // there is no ghost on the first level
1305 if (inWinCutscene || !isNormalLevel() || lg.finalBossLevel ||
1306 (!global.config.ghostAtFirstLevel && global.currLevel == 1))
1309 global.setMusicPitch(1.0);
1313 if (global.config.scumGhost < 0) {
1316 osdMessage("A GHASTLY VOICE CALLS YOUR NAME!\nLET'S GET OUT OF HERE!", 6.66, urgent:true);
1320 if (global.config.scumGhost == 0) {
1326 // randomizes time until ghost appears once time limit is reached
1327 // global.ghostRandom (true/false) if extra time is randomized - loaded from yasm.cfg. if false, use 30 second default
1328 // ghostTimeLeft (time in seconds * 1000) for currently generated level
1330 if (global.config.ghostRandom) {
1331 auto tMin = global.randOther(10, 30); // min(global.plife*10, 30);
1332 auto tMax = max(tMin+global.randOther(10, 30), min(global.config.scumGhost, 300)); // 5 minutes max
1333 auto tTime = global.randOther(tMin, tMax);
1334 if (tTime <= 0) tTime = round(tMax/2.0);
1335 ghostTimeLeft = tTime;
1337 ghostTimeLeft = global.config.scumGhost; // 5 minutes max
1340 ghostTimeLeft += max(0, global.config.ghostExtraTime);
1342 ghostTimeLeft *= 30; // seconds -> frames
1343 //global.ghostShowTime
1347 void spawnGhost () {
1349 ghostSpawned = true;
1352 int vwdt = (viewMax.x-viewMin.x);
1353 int vhgt = (viewMax.y-viewMin.y);
1357 if (player.ix < viewMin.x+vwdt/2) {
1358 // player is in the left side
1359 gx = viewMin.x+vwdt/2+vwdt/4;
1361 // player is in the right side
1362 gx = viewMin.x+vwdt/4;
1365 if (player.iy < viewMin.y+vhgt/2) {
1366 // player is in the left side
1367 gy = viewMin.y+vhgt/2+vhgt/4;
1369 // player is in the right side
1370 gy = viewMin.y+vhgt/4;
1373 writeln("spawning ghost at tile (", gx/16, ",", gy/16, "); player is at tile (", player.ix/16, ",", player.iy/16, ")");
1375 MakeMapObject(gx, gy, 'oGhost');
1378 if (oPlayer1.x > room_width/2) instance_create(view_xview[0]+view_wview[0]+8, view_yview[0]+floor(view_hview[0] / 2), oGhost);
1379 else instance_create(view_xview[0]-32, view_yview[0]+floor(view_hview[0]/2), oGhost);
1380 global.ghostExists = true;
1385 void thinkFrameGameGhost () {
1386 if (player.dead) return;
1387 if (!isNormalLevel()) return; // just in case
1389 if (ghostTimeLeft < 0) {
1391 if (musicFadeTimer > 0) {
1392 musicFadeTimer = -1;
1393 global.setMusicPitch(1.0);
1398 if (musicFadeTimer >= 0) {
1400 if (musicFadeTimer > 0 && musicFadeTimer <= 100*3) {
1401 float pitch = 1.0-(0.00226/3.0)*musicFadeTimer;
1402 //writeln("MFT: ", musicFadeTimer, "; pitch=", pitch);
1403 global.setMusicPitch(pitch);
1407 if (ghostTimeLeft == 0) {
1408 // she is already here!
1412 // no ghost if we have a crown
1413 if (global.hasCrown) {
1418 // if she was already spawned, don't do it again
1424 if (--ghostTimeLeft != 0) {
1426 if (global.config.ghostExtraTime > 0) {
1427 if (ghostTimeLeft == global.config.ghostExtraTime*30) {
1428 osdMessage("A CHILL RUNS UP YOUR SPINE...\nLET'S GET OUT OF HERE!", 6.66, urgent:true);
1430 if (musicFadeTimer < 0 && ghostTimeLeft <= global.config.ghostExtraTime*30) {
1438 if (player.isExitingSprite) {
1439 // no reason to spawn her, we're leaving
1448 void thinkFrameGame () {
1449 thinkFrameGameGhost();
1450 // udjat eye blinking
1451 if (global.hasUdjatEye && player) {
1452 foreach (MapTile t; allExits) {
1453 if (t isa MapTileBlackMarketDoor) {
1454 auto dm = int(player.distanceToEntity(t));
1456 if (udjatAlarm < 1 || dm < udjatAlarm) udjatAlarm = dm;
1460 global.udjatBlink = false;
1463 if (udjatAlarm > 0) {
1464 if (--udjatAlarm == 0) {
1465 global.udjatBlink = !global.udjatBlink;
1466 if (global.hasUdjatEye && player) {
1467 player.playSound(global.udjatBlink ? 'sndBlink1' : 'sndBlink2');
1471 switch (levelKind) {
1472 case LevelKind.Stars: thinkFrameGameStars(); break;
1473 case LevelKind.Sun: thinkFrameGameSun(); break;
1474 case LevelKind.Moon: thinkFrameGameMoon(); break;
1479 // ////////////////////////////////////////////////////////////////////////// //
1480 private final bool isWaterTileCB (MapTile t) {
1481 return (t && t.visible && t.water);
1485 private final bool isLavaTileCB (MapTile t) {
1486 return (t && t.visible && t.lava);
1490 // ////////////////////////////////////////////////////////////////////////// //
1491 const int GreatLakeStartTileY = 28;
1494 final void fillGreatLake () {
1495 if (global.lake == 1) {
1496 foreach (int y; GreatLakeStartTileY..tilesHeight) {
1497 foreach (int x; 0..tilesWidth) {
1498 auto t = checkTileAtPoint(x*16, y*16, delegate bool (MapTile t) {
1499 if (t.spectral || !t.visible || t.invisible || t.moveable) return false;
1503 t = MakeMapTile(x, y, 'oWaterSwim');
1507 t.setSprite(y == GreatLakeStartTileY ? 'sWaterTop' : 'sWater');
1508 } else if (t.lava) {
1509 t.setSprite(y == GreatLakeStartTileY ? 'sLavaTop' : 'sLava');
1517 // called once after level generation
1518 final void fixLiquidTop () {
1519 if (global.lake == 1) fillGreatLake();
1521 liquidTileCount = 0;
1522 foreach (MapTile t; objGrid.allObjects(MapTile)) {
1523 if (!t.water && !t.lava) continue;
1526 //writeln("fixing water tile(", GetClassName(t.Class), "):'", t.objName, "' (water=", t.water, "; lava=", t.lava, "); lqc=", liquidTileCount);
1528 //if (global.lake == 1) continue; // it is done in `fillGreatLake()`
1530 if (!checkTileAtPoint(t.ix+8, t.iy-8, (t.lava ? &isLavaTileCB : &isWaterTileCB))) {
1531 t.setSprite(t.lava ? 'sLavaTop' : 'sWaterTop');
1533 // don't do this, it will destroy seaweed
1534 //t.setSprite(t.lava ? 'sLava' : 'sWater');
1535 auto spr = t.getSprite();
1536 if (!spr) t.setSprite(t.lava ? 'sLava' : 'sWater');
1537 else if (spr.Name == 'sLavaTop') t.setSprite('sLava');
1538 else if (spr.Name == 'sWaterTop') t.setSprite('sWater');
1541 //writeln("liquid tiles count: ", liquidTileCount);
1545 // ////////////////////////////////////////////////////////////////////////// //
1546 transient MapTile curWaterTile;
1547 transient bool curWaterTileCheckHitsLava;
1548 transient bool curWaterTileCheckHitsSolidOrWater; // only for `checkWaterOrSolidTilePartialCB`
1549 transient int curWaterTileLastHDir;
1550 transient ubyte[16, 16] curWaterOccupied;
1551 transient int curWaterOccupiedCount;
1552 transient int curWaterTileCheckX0, curWaterTileCheckY0;
1555 private final void clearCurWaterCheckState () {
1556 curWaterTileCheckHitsLava = false;
1557 curWaterOccupiedCount = 0;
1558 foreach (auto idx; 0..16*16) curWaterOccupied[idx] = 0;
1562 private final bool checkWaterOrSolidTileCB (MapTile t) {
1563 if (t == curWaterTile) return false;
1564 if (t.lava && curWaterTile.water) {
1565 curWaterTileCheckHitsLava = true;
1568 if (t.ix%16 != 0 || t.iy%16 != 0) {
1569 if (t.water || t.solid) {
1570 // fill occupied array
1571 //FIXME: optimize this
1572 if (curWaterOccupiedCount < 16*16) {
1573 foreach (auto dy; t.y0..t.y1+1) {
1574 foreach (auto dx; t.x0..t.x1+1) {
1575 int sx = dx-curWaterTileCheckX0;
1576 int sy = dy-curWaterTileCheckY0;
1577 if (sx >= 0 && sx <= 16 && sy >= 0 && sy <= 15 && !curWaterOccupied[sx, sy]) {
1578 curWaterOccupied[sx, sy] = 1;
1579 ++curWaterOccupiedCount;
1585 return false; // need to check for lava
1587 if (t.water || t.solid || t.lava) {
1588 curWaterOccupiedCount = 16*16;
1589 if (t.water && curWaterTile.lava) t.instanceRemove();
1591 return false; // need to check for lava
1595 private final bool checkWaterOrSolidTilePartialCB (MapTile t) {
1596 if (t == curWaterTile) return false;
1597 if (t.lava && curWaterTile.water) {
1598 //writeln("!!!!!!!!");
1599 curWaterTileCheckHitsLava = true;
1602 if (t.water || t.solid || t.lava) {
1603 //writeln("*********");
1604 curWaterTileCheckHitsSolidOrWater = true;
1605 if (t.water && curWaterTile.lava) t.instanceRemove();
1607 return false; // need to check for lava
1611 private final bool isFullyOccupiedAtTilePos (int tileX, int tileY) {
1612 clearCurWaterCheckState();
1613 curWaterTileCheckX0 = tileX*16;
1614 curWaterTileCheckY0 = tileY*16;
1615 checkTilesInRect(tileX*16, tileY*16, 16, 16, &checkWaterOrSolidTileCB);
1616 return (curWaterTileCheckHitsLava || curWaterOccupiedCount == 16*16);
1620 private final bool isAtLeastPartiallyOccupiedAtTilePos (int tileX, int tileY) {
1621 curWaterTileCheckHitsLava = false;
1622 curWaterTileCheckHitsSolidOrWater = false;
1623 checkTilesInRect(tileX*16, tileY*16, 16, 16, &checkWaterOrSolidTilePartialCB);
1624 return (curWaterTileCheckHitsSolidOrWater || curWaterTileCheckHitsLava);
1628 private final bool waterCanReachGroundHoleInDir (MapTile wtile, int dx) {
1629 if (dx == 0) return false; // just in case
1631 int x = wtile.ix/16, y = wtile.iy/16;
1633 while (x >= 0 && x < tilesWidth) {
1634 if (!isAtLeastPartiallyOccupiedAtTilePos(x, y+1)) return true;
1635 if (isAtLeastPartiallyOccupiedAtTilePos(x, y)) return false;
1642 // returns `true` if this tile must be removed
1643 private final bool checkWaterFlow (MapTile wtile) {
1644 if (global.lake == 1) {
1645 if (wtile.iy >= GreatLakeStartTileY*16) return false; // lake tile, don't touch
1646 if (wtile.iy >= GreatLakeStartTileY*16-16) return true; // remove it, so it won't stack on a lake
1649 if (wtile.ix%16 != 0 || wtile.iy%16 != 0) return true; // sanity check
1651 curWaterTile = wtile;
1652 curWaterTileLastHDir = 0; // never moved to the side
1654 bool wasMoved = false;
1657 int tileX = wtile.ix/16, tileY = wtile.iy/16;
1660 if (tileY >= tilesHeight) return true;
1662 // check if we can fall down
1663 auto canFall = !isAtLeastPartiallyOccupiedAtTilePos(tileX, tileY+1);
1664 // disappear if can fall in lava
1665 if (wtile.water && curWaterTileCheckHitsLava) {
1666 //!writeln(wtile.objId, ": LAVA HIT DOWN");
1670 // fake, so caller will not start removing tiles
1671 if (canFall) wtile.waterMovedDown = true;
1677 //!writeln(wtile.objId, ": GOING DOWN");
1678 curWaterTileLastHDir = 0;
1679 wtile.iy = wtile.iy+16;
1681 wtile.waterMovedDown = true;
1685 bool canMoveLeft = (curWaterTileLastHDir > 0 ? false : !isAtLeastPartiallyOccupiedAtTilePos(tileX-1, tileY));
1686 // disappear if near lava
1687 if (wtile.water && curWaterTileCheckHitsLava) {
1688 //!writeln(wtile.objId, ": LAVA HIT LEFT");
1692 bool canMoveRight = (curWaterTileLastHDir < 0 ? false : !isAtLeastPartiallyOccupiedAtTilePos(tileX+1, tileY));
1693 // disappear if near lava
1694 if (wtile.water && curWaterTileCheckHitsLava) {
1695 //!writeln(wtile.objId, ": LAVA HIT RIGHT");
1699 if (!canMoveLeft && !canMoveRight) {
1701 //!if (wasMove) writeln(wtile.objId, ": NO MORE MOVES");
1705 if (canMoveLeft && canMoveRight) {
1706 // choose random direction
1707 //!writeln(wtile.objId, ": CHOOSING RANDOM HDIR");
1708 // actually, choose direction that leads to hole in a ground
1709 if (waterCanReachGroundHoleInDir(wtile, -1)) {
1710 // can reach hole at the left side
1711 if (waterCanReachGroundHoleInDir(wtile, 1)) {
1712 // can reach hole at the right side, choose at random
1713 if (global.randOther(0, 1)) canMoveRight = false; else canMoveLeft = false;
1716 canMoveRight = false;
1719 // can't reach hole at the left side
1720 if (waterCanReachGroundHoleInDir(wtile, 1)) {
1721 // can reach hole at the right side, choose at random
1722 canMoveLeft = false;
1724 // no holes at any side, choose at random
1725 if (global.randOther(0, 1)) canMoveRight = false; else canMoveLeft = false;
1732 if (canMoveRight) FatalError("WATERCHECK: WTF RIGHT");
1733 //!writeln(wtile.objId, ": MOVING LEFT (", curWaterTileLastHDir, ")");
1734 curWaterTileLastHDir = -1;
1735 wtile.ix = wtile.ix-16;
1736 } else if (canMoveRight) {
1737 if (canMoveLeft) FatalError("WATERCHECK: WTF LEFT");
1738 //!writeln(wtile.objId, ": MOVING RIGHT (", curWaterTileLastHDir, ")");
1739 curWaterTileLastHDir = 1;
1740 wtile.ix = wtile.ix+16;
1748 wtile.setSprite(wtile.lava ? 'sLava' : 'sWater');
1749 wtile.waterMoved = true;
1750 // if this tile was not moved down, check if it can move down on any next step
1751 if (!wtile.waterMovedDown) {
1752 if (waterCanReachGroundHoleInDir(wtile, -1)) wtile.waterMovedDown = true;
1753 else if (waterCanReachGroundHoleInDir(wtile, 1)) wtile.waterMovedDown = true;
1757 return false; // don't remove
1759 //if (!isWetTileAtPix(tileX*16+8, tileY*16-8)) wtile.setSprite(wtile.lava ? 'sLavaTop' : 'sWaterTop');
1763 transient array!MapTile waterTilesList;
1765 final bool sortWaterTilesByCoordsLess (MapTile a, MapTile b) {
1767 if (dy) return (dy < 0);
1768 return (a.ix < b.ix);
1771 transient int waterFlowPause = 0;
1772 transient bool debugWaterFlowPause = false;
1774 final void cleanDeadObjects () {
1775 // remove dead objects
1776 if (deadItemsHead) {
1777 auto olddel = ImmediateDelete;
1778 ImmediateDelete = false;
1780 auto it = deadItemsHead;
1781 deadItemsHead = it.deadItemsNext;
1782 if (it.grid) it.grid.remove(it.gridId);
1785 } while (deadItemsHead);
1786 ImmediateDelete = olddel;
1787 if (olddel) CollectGarbage(true); // destroy delayed objects too
1791 final void cleanDeadTiles () {
1792 if (checkWater && /*global.lake == 1 ||*/ (!blockWaterChecking && liquidTileCount)) {
1793 if (global.lake == 1) fillGreatLake();
1794 if (waterFlowPause > 1) {
1799 if (debugWaterFlowPause) waterFlowPause = 4;
1800 //writeln("checking water");
1801 waterTilesList.clear();
1802 foreach (MapTile wtile; objGrid.allObjectsSafe(MapTile)) {
1803 if (wtile.water || wtile.lava) {
1805 if (wtile.ix%16 == 0 && wtile.iy%16 == 0) {
1806 wtile.waterMoved = false;
1807 wtile.waterMovedDown = false;
1808 wtile.waterSlideOldX = wtile.ix;
1809 wtile.waterSlideOldY = wtile.iy;
1810 waterTilesList[$] = wtile;
1815 liquidTileCount = 0;
1816 waterTilesList.sort(&sortWaterTilesByCoordsLess);
1818 bool wasAnyMove = false;
1819 bool wasAnyMoveDown = false;
1820 foreach (MapTile wtile; waterTilesList) {
1821 if (!wtile || !wtile.isInstanceAlive) continue;
1822 auto killIt = checkWaterFlow(wtile);
1826 wtile.instanceRemove(); // just in case
1828 wtile.saveInterpData();
1830 wasAnyMove = wasAnyMove || wtile.waterMoved;
1831 wasAnyMoveDown = wasAnyMoveDown || wtile.waterMovedDown;
1832 if (wtile.waterMoved && debugWaterFlowPause) wtile.waterSlideCounter = 4;
1836 liquidTileCount = 0;
1837 foreach (MapTile wtile; waterTilesList) {
1838 if (!wtile || !wtile.isInstanceAlive) continue;
1839 if (wasAnyMoveDown) {
1843 //checkWater = checkWater || wtile.waterMoved;
1844 curWaterTile = wtile;
1845 int tileX = wtile.ix/16, tileY = wtile.iy/16;
1846 // check if we are have no way to leak
1847 bool killIt = false;
1848 if (!isFullyOccupiedAtTilePos(tileX-1, tileY) || (wtile.water && curWaterTileCheckHitsLava)) {
1849 //writeln(" LEFT DEATH (", curWaterOccupiedCount, " : ", curWaterTileCheckHitsLava, ")");
1852 if (!killIt && (!isFullyOccupiedAtTilePos(tileX+1, tileY) || (wtile.water && curWaterTileCheckHitsLava))) {
1853 //writeln(" RIGHT DEATH (", curWaterOccupiedCount, " : ", curWaterTileCheckHitsLava, ")");
1856 if (!killIt && (!isFullyOccupiedAtTilePos(tileX, tileY+1) || (wtile.water && curWaterTileCheckHitsLava))) {
1857 //writeln(" DOWN DEATH (", curWaterOccupiedCount, " : ", curWaterTileCheckHitsLava, ")");
1864 wtile.instanceRemove(); // just in case
1869 if (wasAnyMove) checkWater = true;
1870 //writeln("water check: liquidTileCount=", liquidTileCount, "; checkWater=", checkWater, "; wasAnyMove=", wasAnyMove, "; wasAnyMoveDown=", wasAnyMoveDown);
1872 // fill empty spaces in lake with water
1880 // ////////////////////////////////////////////////////////////////////////// //
1881 private transient array!MapEntity postponedThinkers;
1882 private transient MapEntity thinkerHeld;
1883 private transient array!MapEntity activeThinkerList;
1886 final void doThinkActionsForObject (MapEntity o) {
1887 if (o.justSpawned) o.justSpawned = false;
1888 else if (o.imageSpeed > 0) o.nextAnimFrame();
1891 if (o.isInstanceAlive) {
1894 if (o.isInstanceAlive) {
1895 if (o.whipTimer > 0) --o.whipTimer;
1897 auto obj = MapObject(o);
1898 if (!o.canLiveOutsideOfLevel && (!obj || !obj.heldBy) && o.isOutsideOfLevel()) {
1899 // oops, fallen out of level...
1907 // return `true` if thinker should be removed
1908 final void thinkOne (MapEntity o, optional bool doHeldObject, optional bool dontAddHeldObject) {
1910 if (o == thinkerHeld && !doHeldObject) return; // skip it
1912 if (!o.active || !o.isInstanceAlive) return;
1914 auto obj = MapObject(o);
1916 if (obj && obj.heldBy == player) {
1917 // fix held item coords
1918 obj.fixHoldCoords();
1920 doThinkActionsForObject(o);
1922 if (!dontAddHeldObject) {
1924 foreach (MapEntity e; postponedThinkers) if (e == o) { found = true; break; }
1925 if (!found) postponedThinkers[$] = o;
1931 bool doThink = true;
1933 // collision with player weapon
1934 auto hh = PlayerWeapon(player.holdItem);
1935 bool doWeaponAction = false;
1937 if (hh.blockedBySolids && !global.config.killEnemiesThruWalls) {
1938 int xx = player.ix+(player.dir == MapObject::Dir.Left ? -16 : 16);
1939 //doWeaponAction = !isSolidAtPoint(xx, player.iy);
1940 doWeaponAction = !isSolidAtPoint(xx, hh.yCenter);
1942 int dh = max(1, hh.height-2);
1943 doWeaponAction = !checkTilesInRect(player.ix, player.iy);
1946 doWeaponAction = true;
1950 if (obj && doWeaponAction && hh && (o.whipTimer <= 0 || hh.ignoreWhipTimer) && hh.collidesWithObject(obj)) {
1951 //writeln("WEAPONED!");
1952 bool dontChangeWhipTimer = hh.dontChangeWhipTimer;
1953 if (!o.onTouchedByPlayerWeapon(player, hh)) {
1954 if (o.isInstanceAlive) hh.onCollisionWithObject(obj);
1956 if (!dontChangeWhipTimer) o.whipTimer = o.whipTimerValue; //HACK
1957 doThink = o.isInstanceAlive;
1960 if (doThink && o.isInstanceAlive) {
1961 doThinkActionsForObject(o);
1962 doThink = o.isInstanceAlive;
1965 // collision with player
1966 if (doThink && obj && o.collidesWith(player)) {
1967 if (!player.onObjectTouched(obj) && o.isInstanceAlive) {
1968 doThink = !o.onTouchedByPlayer(player);
1975 final void processThinkers (float timeDelta) {
1976 if (timeDelta <= 0) return;
1979 if (onBeforeFrame) onBeforeFrame(false);
1980 if (onAfterFrame) onAfterFrame(false);
1986 accumTime += timeDelta;
1987 bool wasFrame = false;
1989 auto olddel = ImmediateDelete;
1990 ImmediateDelete = false;
1991 while (accumTime >= FrameTime) {
1992 postponedThinkers.clear();
1994 accumTime -= FrameTime;
1995 if (onBeforeFrame) onBeforeFrame(accumTime >= FrameTime);
1997 if (shakeLeft > 0) {
1999 if (!shakeDir.x) shakeDir.x = (global.randOther(0, 1) ? -3 : 3);
2000 if (!shakeDir.y) shakeDir.y = (global.randOther(0, 1) ? -3 : 3);
2001 shakeOfs.x = shakeDir.x;
2002 shakeOfs.y = shakeDir.y;
2003 int sgnc = global.randOther(1, 3);
2004 if (sgnc&0x01) shakeDir.x = -shakeDir.x;
2005 if (sgnc&0x02) shakeDir.y = -shakeDir.y;
2014 // we don't want the time to grow too large
2015 if (time < 0) { time = 0; lastRenderTime = -1; }
2016 // game-global events
2018 // frame thinkers: player
2019 if (player && !disablePlayerThink) {
2021 if (!player.dead && isNormalLevel() &&
2022 (maxPlayingTime < 0 ||
2023 (maxPlayingTime > 0 && maxPlayingTime*30 <= time &&
2024 time%30 == 0 && global.randOther(1, 100) <= 20)))
2026 MakeMapObject(player.ix, player.iy, 'oExplosion');
2028 //HACK: check for stolen items
2029 auto item = MapItem(player.holdItem);
2030 if (item) item.onCheckItemStolen(player);
2031 item = MapItem(player.pickedItem);
2032 if (item) item.onCheckItemStolen(player);
2034 doThinkActionsForObject(player);
2036 // frame thinkers: held object
2037 thinkerHeld = player.holdItem;
2038 if (thinkerHeld && thinkerHeld.isInstanceAlive) {
2039 thinkOne(thinkerHeld, doHeldObject:true);
2040 if (!thinkerHeld.isInstanceAlive) {
2041 if (player.holdItem == thinkerHeld) player.holdItem = none;
2042 thinkerHeld.grid.remove(thinkerHeld.gridId);
2044 thinkerHeld.onDestroy();
2049 // frame thinkers: objects
2050 activeThinkerList.clear();
2051 auto grid = objGrid;
2052 // collect active objects
2053 if (global.config.useFrozenRegion) {
2054 foreach (MapEntity e; grid.inRectPix(viewStart.x/global.scale-64, viewStart.y/global.scale-64, 320+64*2, 240+64*2, precise:false)) {
2055 if (e.active) activeThinkerList[$] = e;
2059 foreach (MapEntity e; grid.allObjects()) {
2060 if (e.active) activeThinkerList[$] = e;
2063 // process active objects
2064 //writeln("thinkers: ", activeThinkerList.length);
2065 foreach (MapEntity o; activeThinkerList) {
2067 thinkOne(o, doHeldObject:false);
2068 if (!o.isInstanceAlive) {
2069 //writeln("dead thinker: '", o.objType, "'");
2070 if (o.grid) o.grid.remove(o.gridId);
2071 auto obj = MapObject(o);
2072 if (obj && obj.heldBy) obj.heldBy.holdItem = none;
2079 // postponed thinkers
2080 foreach (MapEntity o; postponedThinkers) {
2082 thinkOne(o, doHeldObject:true, dontAddHeldObject:true);
2083 if (!o.isInstanceAlive) {
2084 //writeln("dead pp-thinker: '", o.objType, "'");
2091 postponedThinkers.clear();
2093 // clean dead things
2095 // fix held item coords
2096 if (player && player.holdItem) {
2097 if (player.holdItem.isInstanceAlive) {
2098 player.holdItem.fixHoldCoords();
2100 player.holdItem = none;
2104 if (collectCounter == 0) {
2105 xmoney = max(0, xmoney-100);
2111 if (!player.dead) stats.oneMoreFramePlayed();
2112 SoundSystem.ListenerOrigin = vector(player.xCenter, player.yCenter, -1);
2113 //writeln("plrpos=(", player.xCenter, ",", player.yCenter, "); lo=", SoundSystem.ListenerOrigin);
2115 if (onAfterFrame) onAfterFrame(accumTime >= FrameTime);
2116 ++framesProcessedFromLastClear;
2119 if (!player.visible && player.holdItem) player.holdItem.visible = false;
2120 if (winCutsceneSwitchToNext) {
2121 winCutsceneSwitchToNext = false;
2122 switch (++inWinCutscene) {
2123 case 2: startWinCutsceneVolcano(); break;
2124 case 3: default: startWinCutsceneWinFall(); break;
2128 if (playerExited) break;
2130 ImmediateDelete = olddel;
2132 playerExited = false;
2134 centerViewAtPlayer();
2137 // if we were processed at least one frame, collect garbage
2139 CollectGarbage(true); // destroy delayed objects too
2141 if (accumTime > 0 && onInterFrame) onInterFrame(accumTime/FrameTime);
2145 // ////////////////////////////////////////////////////////////////////////// //
2146 final void tileXY2roomXY (int tileX, int tileY, out int roomX, out int roomY) {
2147 roomX = (tileX-1)/RoomGen::Width;
2148 roomY = (tileY-1)/RoomGen::Height;
2152 final bool isInShop (int tileX, int tileY) {
2153 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
2154 auto n = roomType[tileX, tileY];
2155 if (n == 4 || n == 5) return true;
2156 return !!checkTilesInRect(tileX*16, tileY*16, 16, 16, delegate bool (MapTile t) { return t.shopWall; });
2157 //k8: we don't have this
2158 //if (t && t.objType == 'oShop') return true;
2164 // ////////////////////////////////////////////////////////////////////////// //
2165 override void Destroy () {
2167 delete tempSolidTile;
2172 // ////////////////////////////////////////////////////////////////////////// //
2173 // WARNING! delegate should not create/delete objects!
2174 final MapObject findNearestObject (int px, int py, scope bool delegate (MapObject o) dg, optional class!MapObject castClass) {
2175 MapObject res = none;
2176 if (!castClass) castClass = MapObject;
2177 int curdistsq = int.max;
2178 foreach (MapObject o; objGrid.allObjects(MapObject)) {
2179 if (o.spectral) continue;
2180 if (!dg(o)) continue;
2181 int xc = px-o.xCenter, yc = py-o.yCenter;
2182 int distsq = xc*xc+yc*yc;
2183 if (distsq < curdistsq) {
2192 // WARNING! delegate should not create/delete objects!
2193 final MapObject findNearestEnemy (int px, int py, optional scope bool delegate (MapEnemy o) dg, optional class!MapEnemy castClass) {
2194 if (!castClass) castClass = MapEnemy;
2195 if (castClass !isa MapEnemy) return none;
2196 MapObject res = none;
2197 int curdistsq = int.max;
2198 foreach (MapEnemy o; objGrid.allObjects(castClass)) {
2199 //k8: i added `dead` check
2200 if (o.spectral || o.dead) continue;
2202 if (!dg(o)) continue;
2204 int xc = px-o.xCenter, yc = py-o.yCenter;
2205 int distsq = xc*xc+yc*yc;
2206 if (distsq < curdistsq) {
2215 final MonsterShopkeeper findNearestCalmShopkeeper (int px, int py) {
2216 auto obj = MonsterShopkeeper(findNearestEnemy(px, py, delegate bool (MapEnemy o) {
2217 auto sk = MonsterShopkeeper(o);
2218 if (sk && !sk.angered) return true;
2220 }, castClass:MonsterShopkeeper));
2225 final MonsterShopkeeper hasAliveShopkeepers (optional bool skipAngry) {
2226 foreach (MonsterShopkeeper sc; objGrid.allObjects(MonsterShopkeeper)) {
2227 if (sc.spectral || sc.dead) continue;
2228 if (skipAngry && (sc.angered || sc.outlaw)) continue;
2235 // WARNING! delegate should not create/delete objects!
2236 final int calcNearestEnemyDist (int px, int py, optional scope bool delegate (MapEnemy o) dg, optional class!MapEnemy castClass) {
2237 auto e = findNearestEnemy(px, py, dg!optional, castClass!optional);
2238 if (!e) return int.max;
2239 int xc = px-e.xCenter, yc = py-e.yCenter;
2240 return round(sqrt(xc*xc+yc*yc));
2244 // WARNING! delegate should not create/delete objects!
2245 final int calcNearestObjectDist (int px, int py, optional scope bool delegate (MapObject o) dg, optional class!MapObject castClass) {
2246 auto e = findNearestObject(px, py, dg!optional, castClass!optional);
2247 if (!e) return int.max;
2248 int xc = px-e.xCenter, yc = py-e.yCenter;
2249 return round(sqrt(xc*xc+yc*yc));
2253 // WARNING! delegate should not create/delete objects!
2254 final MapTile findNearestMoveableSolid (int px, int py, optional scope bool delegate (MapTile t) dg) {
2256 int curdistsq = int.max;
2257 foreach (MapTile t; objGrid.allObjects(MapTile)) {
2258 if (t.spectral) continue;
2260 if (!dg(t)) continue;
2262 if (!t.solid || !t.moveable) continue;
2264 int xc = px-t.xCenter, yc = py-t.yCenter;
2265 int distsq = xc*xc+yc*yc;
2266 if (distsq < curdistsq) {
2275 // WARNING! delegate should not create/delete objects!
2276 final MapTile findNearestTile (int px, int py, optional scope bool delegate (MapTile t) dg) {
2277 if (!dg) return none;
2279 int curdistsq = int.max;
2281 //FIXME: make this faster!
2282 foreach (MapTile t; objGrid.allObjects(MapTile)) {
2283 if (t.spectral) continue;
2284 int xc = px-t.xCenter, yc = py-t.yCenter;
2285 int distsq = xc*xc+yc*yc;
2286 if (distsq < curdistsq && dg(t)) {
2296 // ////////////////////////////////////////////////////////////////////////// //
2297 final bool cbIsObjectWeb (MapObject o) { return (o isa ItemWeb); }
2298 final bool cbIsObjectBlob (MapObject o) { return (o isa EnemyBlob); }
2299 final bool cbIsObjectArrow (MapObject o) { return (o isa ItemProjectileArrow); }
2300 final bool cbIsObjectEnemy (MapObject o) { return (/*o != self &&*/ o isa MapEnemy); }
2302 final bool cbIsObjectTreasure (MapObject o) { return (o.isTreasure); }
2304 final bool cbIsItemObject (MapObject o) { return (o isa MapItem); }
2306 final bool cbIsItemOrEnemyObject (MapObject o) { return (o isa MapItem || o isa MapEnemy); }
2309 final MapObject isObjectAtTile (int tileX, int tileY, optional scope bool delegate (MapObject o) dg, optional bool precise) {
2310 if (!specified_precise) precise = true;
2313 foreach (MapObject o; objGrid.inRectPix(tileX, tileY, 16, 16, precise:precise, castClass:MapObject)) {
2314 if (o.spectral) continue;
2316 if (dg(o)) return o;
2325 final MapObject isObjectAtTilePix (int x, int y, optional scope bool delegate (MapObject o) dg) {
2326 return isObjectAtTile(x/16, y/16, dg!optional);
2330 final MapObject isObjectAtPoint (int xpos, int ypos, optional scope bool delegate (MapObject o) dg, optional bool precise, optional class!MapObject castClass) {
2331 if (!specified_precise) precise = true;
2332 if (!castClass) castClass = MapObject;
2333 foreach (MapObject o; objGrid.inCellPix(xpos, ypos, precise:precise, castClass:castClass)) {
2334 if (o.spectral) continue;
2336 if (dg(o)) return o;
2338 if (o isa MapEnemy) return o;
2345 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) {
2346 if (w < 1 || h < 1) return none;
2347 if (!castClass) castClass = MapObject;
2348 if (w == 1 && h == 1) return isObjectAtPoint(xpos, ypos, dg!optional, precise!optional, castClass);
2349 if (!specified_precise) precise = true;
2350 foreach (MapObject o; objGrid.inRectPix(xpos, ypos, w, h, precise:precise, castClass:castClass)) {
2351 if (o.spectral) continue;
2353 if (dg(o)) return o;
2355 if (o isa MapEnemy) return o;
2362 final MapObject forEachObject (scope bool delegate (MapObject o) dg, optional bool allowSpectrals, optional class!MapObject castClass) {
2363 if (!dg) return none;
2364 if (!castClass) castClass = MapObject;
2365 foreach (MapObject o; objGrid.allObjectsSafe(castClass)) {
2366 if (!allowSpectrals && o.spectral) continue;
2367 if (dg(o)) return o;
2373 final MapObject forEachObjectAtPoint (int xpos, int ypos, scope bool delegate (MapObject o) dg, optional bool precise) {
2374 if (!dg) return none;
2375 if (!specified_precise) precise = true;
2376 foreach (MapObject o; objGrid.inCellPix(xpos, ypos, precise:precise, castClass:MapObject)) {
2377 if (o.spectral) continue;
2378 if (dg(o)) return o;
2384 final MapObject forEachObjectInRect (int xpos, int ypos, int w, int h, scope bool delegate (MapObject o) dg, optional bool precise) {
2385 if (!dg || w < 1 || h < 1) return none;
2386 if (w == 1 && h == 1) return forEachObjectAtPoint(xpos, ypos, dg!optional, precise!optional);
2387 if (!specified_precise) precise = true;
2388 foreach (MapObject o; objGrid.inRectPix(xpos, ypos, w, h, precise:precise, castClass:MapObject)) {
2389 if (o.spectral) continue;
2390 if (dg(o)) return o;
2396 final MapEntity forEachEntityInRect (int xpos, int ypos, int w, int h, scope bool delegate (MapEntity o) dg, optional bool precise, optional class!MapEntity castClass) {
2397 if (!dg || w < 1 || h < 1) return none;
2398 if (!castClass) castClass = MapEntity;
2399 if (!specified_precise) precise = true;
2400 foreach (MapEntity e; objGrid.inRectPix(xpos, ypos, w, h, precise:precise, castClass:castClass)) {
2401 if (e.spectral) continue;
2402 if (dg(e)) return e;
2408 private final bool cbIsRopeTile (MapTile t) { return (t isa MapTileRope); }
2410 final MapTile isRopeAtPoint (int px, int py) {
2411 return checkTileAtPoint(px, py, &cbIsRopeTile);
2416 final MapTile isWaterSwimAtPoint (int px, int py) {
2417 return isWaterAtPoint(px, py);
2421 // ////////////////////////////////////////////////////////////////////////// //
2422 private array!MapEntity tmpEntityList;
2424 private final bool cbCollectEntitiesWithMask (MapEntity t) {
2425 if (!t.visible || t.spectral) return false;
2426 tmpEntityList[$] = t;
2431 final void touchEntitiesWithMask (int x, int y, SpriteFrame frm, scope bool delegate (MapEntity t) dg, optional class!MapEntity castClass) {
2432 if (!frm || frm.bw < 1 || frm.bh < 1 || !dg) return;
2433 if (frm.isEmptyPixelMask) return;
2434 if (!castClass) castClass = MapEntity;
2436 if (tmpEntityList.length) tmpEntityList.clear();
2437 if (player isa castClass && player.isRectCollisionFrame(frm, x, y)) tmpEntityList[$] = player;
2438 forEachEntityInRect(x+frm.bx, y+frm.by, frm.bw, frm.bh, &cbCollectEntitiesWithMask, castClass:castClass);
2439 foreach (MapEntity e; tmpEntityList) {
2440 if (!e || !e.isInstanceAlive || !e.visible || e.spectral) continue;
2441 if (e.isRectCollisionFrame(frm, x, y)) {
2448 // ////////////////////////////////////////////////////////////////////////// //
2449 final bool cbCollisionAnySolid (MapTile t) { return t.solid; }
2450 final bool cbCollisionAnyMoveableSolid (MapTile t) { return t.solid && t.moveable; }
2451 final bool cbCollisionLiquid (MapTile t) { return (t.water || t.lava); }
2452 final bool cbCollisionAnySolidOrLiquid (MapTile t) { return (t.solid || t.water || t.lava); }
2453 final bool cbCollisionLadder (MapTile t) { return t.ladder; }
2454 final bool cbCollisionLadderTop (MapTile t) { return t.laddertop; }
2455 final bool cbCollisionAnyLadder (MapTile t) { return (t.ladder || t.laddertop); }
2456 final bool cbCollisionAnySolidIce (MapTile t) { return (t.solid && t.ice); }
2457 final bool cbCollisionWater (MapTile t) { return t.water; }
2458 final bool cbCollisionLava (MapTile t) { return t.lava; }
2459 final bool cbCollisionAnyTree (MapTile t) { return t.tree; }
2460 final bool cbCollisionPlatform (MapTile t) { return t.platform; }
2461 final bool cbCollisionLadderOrPlatform (MapTile t) { return (t.ladder || t.platform); }
2462 //final bool cbCollisionAnyHangeable (MapTile t) { return t.hangeable; } // no need to check for solids here, it is done in MapTile initializer
2463 final bool cbCollisionSpringTrap (MapTile t) { return t.springtrap; }
2464 final bool cbCollisionSpikes (MapTile t) { return t.spikes; }
2465 final bool cbCollisionExitTile (MapTile t) { return t.isExitActive(); }
2467 final bool cbCollisionForWhoa (MapTile t) { return (t.solid || t.laddertop); }
2469 //final bool cbCollisionSacAltar (MapTile t) { return (t.objName == 'oSacAltarLeft'); } // only left part, yeah
2470 final bool cbCollisionSacAltar (MapTile t) { return t.sacrificingAltar; }
2473 // ////////////////////////////////////////////////////////////////////////// //
2474 transient MapTileTemp tempSolidTile;
2476 private final MapTileTemp makeWalkeableSolidTile (MapObject o) {
2477 if (!tempSolidTile) {
2478 tempSolidTile = SpawnObject(MapTileTemp);
2479 } else if (!tempSolidTile.isInstanceAlive) {
2480 delete tempSolidTile;
2481 tempSolidTile = SpawnObject(MapTileTemp);
2484 tempSolidTile.level = self;
2485 tempSolidTile.global = global;
2486 tempSolidTile.solid = true;
2487 tempSolidTile.objName = MapTileTemp.default.objName;
2488 tempSolidTile.objType = MapTileTemp.default.objType;
2489 tempSolidTile.e = o;
2490 tempSolidTile.fltx = o.fltx;
2491 tempSolidTile.flty = o.flty;
2492 return tempSolidTile;
2496 final MapTile checkTilesInRect (int x0, int y0, const int w, const int h,
2497 optional scope bool delegate (MapTile dg) dg, optional bool precise,
2498 optional class!MapTile castClass)
2500 if (w < 1 || h < 1) return none;
2501 if (w == 1 && h == 1) return checkTileAtPoint(x0, y0, dg!optional);
2502 int x1 = x0+w-1, y1 = y0+h-1;
2503 if (x0 >= tilesWidth*16 || y0 >= tilesHeight*16 || x1 < 0 || y1 < 0) return none;
2504 if (!specified_precise) precise = true;
2505 if (!castClass) castClass = MapTile;
2506 if (!dg) dg = &cbCollisionAnySolid;
2508 // check walkable solid objects too
2509 foreach (MapEntity e; objGrid.inRectPix(x0, y0, w, h, precise:precise, castClass:castClass)) {
2510 if (e.spectral || !e.visible) continue;
2511 auto t = MapTile(e);
2513 if (dg(t)) return t;
2516 auto o = MapObject(e);
2517 if (o && o.walkableSolid) {
2518 t = makeWalkeableSolidTile(o);
2519 if (dg(t)) return t;
2528 final MapTile checkTileAtPoint (int x0, int y0, optional scope bool delegate (MapTile dg) dg, optional bool precise, optional class!MapTile castClass) {
2529 if (x0 < 0 || y0 < 0 || x0 >= tilesWidth*16 || y0 >= tilesHeight*16) return none;
2530 if (!specified_precise) precise = true;
2531 if (!castClass) castClass = MapTile;
2532 if (!dg) dg = &cbCollisionAnySolid;
2534 // check walkable solid objects
2535 foreach (MapEntity e; objGrid.inCellPix(x0, y0, precise:precise, castClass:castClass)) {
2536 if (e.spectral || !e.visible) continue;
2537 auto t = MapTile(e);
2539 if (dg(t)) return t;
2542 auto o = MapObject(e);
2543 if (o && o.walkableSolid) {
2544 t = makeWalkeableSolidTile(o);
2545 if (dg(t)) return t;
2554 // ////////////////////////////////////////////////////////////////////////// //
2555 final MapTile isSolidAtPoint (int px, int py) { return checkTileAtPoint(px, py); }
2556 final MapTile isSolidInRect (int rx, int ry, int rw, int rh) { return checkTilesInRect(rx, ry, rw, rh); }
2557 final MapTile isLadderAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadder); }
2558 final MapTile isLadderOrPlatformAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadderOrPlatform); }
2559 final MapTile isLadderTopAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadderTop); }
2560 final MapTile isAnyLadderAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyLadder); }
2561 final MapTile isWaterAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionWater); }
2562 final MapTile isLavaAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLava); }
2563 final MapTile isLiquidAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLiquid); }
2564 final MapTile isIceAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnySolidIce); }
2565 final MapTile isTreeAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyTree); }
2566 //final MapTile isHangeableAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyHangeable); }
2569 // ////////////////////////////////////////////////////////////////////////// //
2570 final void setTileTypeAt (int tileX, int tileY, LevelGen::RType rt) {
2571 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) roomType[tileX, tileY] = rt;
2575 //FIXME: make this faster
2576 transient float gtagX, gtagY;
2578 // only non-moveables and non-specials
2579 final MapTile getTileAtGrid (int tileX, int tileY) {
2582 return checkTileAtPoint(tileX*16, tileY*16, delegate bool (MapTile t) {
2583 if (t.spectral || t.moveable || t.toSpecialGrid || !t.visible) return false;
2584 if (t.fltx != gtagX || t.flty != gtagY) return false; // emulate grid
2585 if (t.width != 16 || t.height != 16) return false;
2588 //return (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight ? tiles[tileX, tileY] : none);
2592 final MapTile getTileAtGridAny (int tileX, int tileY) {
2595 return checkTileAtPoint(tileX*16, tileY*16, delegate bool (MapTile t) {
2596 if (t.spectral /*|| t.moveable*/ || !t.visible) return false;
2597 if (t.fltx != gtagX || t.flty != gtagY) return false; // emulate grid
2598 if (t.width != 16 || t.height != 16) return false;
2601 //return (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight ? tiles[tileX, tileY] : none);
2605 final bool isNamedTileAt (int tileX, int tileY, name atypename) {
2606 if (!atypename) return false;
2607 auto t = getTileAtGridAny(tileX, tileY);
2608 return (t && t.objName == atypename);
2612 final void setTileAtGrid (int tileX, int tileY, MapTile tile) {
2613 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
2615 tile.fltx = tileX*16;
2616 tile.flty = tileY*16;
2617 if (!tile.dontReplaceOthers) {
2618 auto osp = tile.spectral;
2619 tile.spectral = true;
2620 auto t = getTileAtGridAny(tileX, tileY);
2621 tile.spectral = osp;
2622 if (t && !t.immuneToReplacement) {
2623 writeln("REPLACING tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
2624 writeln(" NEW tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (tile.toSpecialGrid ? "special " : ""), "grid, pos=(", tile.fltx/16.0, ",", tile.flty/16.0, ")");
2630 auto t = getTileAtGridAny(tileX, tileY);
2631 if (t && !t.immuneToReplacement) {
2632 writeln("REMOVING tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
2640 // ////////////////////////////////////////////////////////////////////////// //
2641 // return `true` from delegate to stop
2642 MapTile forEachSolidTileOnGrid (scope bool delegate (int tileX, int tileY, MapTile t) dg) {
2643 if (!dg) return none;
2644 foreach (MapTile t; objGrid.allObjectsSafe(MapTile)) {
2645 if (t.spectral || !t.solid || !t.visible) continue;
2646 if (t.ix%16 != 0 || t.iy%16 != 0) continue; // emulate grid
2647 if (t.width != 16 || t.height != 16) continue;
2648 if (dg(t.ix/16, t.iy/16, t)) return t;
2654 // ////////////////////////////////////////////////////////////////////////// //
2655 // return `true` from delegate to stop
2656 MapTile forEachTile (scope bool delegate (MapTile t) dg, optional class!MapTile castClass) {
2657 if (!dg) return none;
2658 if (!castClass) castClass = MapTile;
2659 foreach (MapTile t; objGrid.allObjectsSafe(castClass)) {
2660 if (t.spectral || !t.visible) continue;
2661 if (dg(t)) return t;
2667 // ////////////////////////////////////////////////////////////////////////// //
2668 final void fixWallTiles () {
2669 foreach (MapTile t; objGrid.allObjectsSafe(MapTile)) t.beautifyTile();
2673 // ////////////////////////////////////////////////////////////////////////// //
2674 final MapTile isCollisionAtPoint (int px, int py, optional scope bool delegate (MapTile dg) dg) {
2675 if (!dg) dg = &cbCollisionAnySolid;
2676 return checkTilesInRect(px, py, 1, 1, dg);
2680 // ////////////////////////////////////////////////////////////////////////// //
2681 string scrGetKaliGift (MapTile altar, optional name gift) {
2684 // find other side of the altar
2685 int sx = player.ix, sy = player.iy;
2689 auto a2 = isCollisionAtPoint(altar.ix+18, altar.iy+8, &cbCollisionSacAltar);
2690 if (!a2) a2 = isCollisionAtPoint(altar.ix-8, altar.iy+8, &cbCollisionSacAltar);
2691 if (a2) { sx = a2.ix; sy = a2.iy; }
2694 if (global.favor <= -8) res = "SHE SEEMS VERY ANGRY WITH YOU!";
2695 else if (global.favor < 0) res = "SHE SEEMS ANGRY WITH YOU.";
2696 else if (global.favor == 0) res = "SHE HAS FORGIVEN YOU!";
2697 else if (global.favor >= 32) {
2698 if (global.kaliGift >= 3 && global.favor >= 32+(global.kaliGift-2)*16) {
2699 res = "YOU FEEL INVIGORATED!";
2700 global.kaliGift += 1;
2701 global.plife += global.randOther(4, 8);
2702 } else if (global.kaliGift >= 3) {
2703 res = "SHE SEEMS ECSTATIC WITH YOU!";
2704 } else if (global.bombs < 80) {
2705 res = "YOUR SATCHEL FEELS VERY FULL NOW!";
2706 global.kaliGift = 3;
2709 res = "YOU FEEL INVIGORATED!";
2710 global.kaliGift += 1;
2711 global.plife += global.randOther(4, 8);
2713 } else if (global.favor >= 16) {
2714 if (global.kaliGift >= 2) {
2715 res = "SHE SEEMS VERY HAPPY WITH YOU!";
2717 res = "SHE BESTOWS A GIFT UPON YOU!";
2718 global.kaliGift = 2;
2720 auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2723 obj = MakeMapObject(sx, sy-8, 'oPoof');
2728 if (gift) obj = MakeMapObject(sx, sy-8, gift);
2729 if (!obj) obj = MakeMapObject(sx, sy-8, 'oKapala');
2731 } else if (global.favor >= 8) {
2732 if (global.kaliGift >= 1) {
2733 res = "SHE SEEMS HAPPY WITH YOU.";
2735 res = "SHE BESTOWS A GIFT UPON YOU!";
2736 global.kaliGift = 1;
2737 //rAltar = instance_nearest(x, y, oSacAltarRight);
2738 //if (instance_exists(rAltar)) {
2740 auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2743 obj = MakeMapObject(sx, sy-8, 'oPoof');
2747 if (gift) obj = MakeMapObject(sx, sy-8, gift);
2749 auto n = global.randOther(1, 8);
2753 if (n == 1 && !global.hasCape && !global.hasJetpack) aname = 'oCapePickup';
2754 else if (n == 2 && !global.hasGloves) aname = 'oGloves';
2755 else if (n == 3 && !global.hasSpectacles) aname = 'oSpectacles';
2756 else if (n == 4 && !global.hasMitt) aname = 'oMitt';
2757 else if (n == 5 && !global.hasSpringShoes) aname = 'oSpringShoes';
2758 else if (n == 6 && !global.hasSpikeShoes) aname = 'oSpikeShoes';
2759 else if (n == 7 && !global.hasStickyBombs) aname = 'oPaste';
2760 else if (n == 8 && !global.hasCompass) aname = 'oCompass';
2762 obj = MakeMapObject(sx, sy-8, aname);
2768 obj = MakeMapObject(sx, sy-8, (global.hasJetpack ? 'oBombBox' : 'oJetpack'));
2774 } else if (global.favor > 0) {
2775 res = "SHE SEEMS PLEASED WITH YOU.";
2780 global.message = "";
2781 res = "KALI DEVOURS YOU!"; // sacrifice is player
2789 void performSacrifice (MapObject what, MapTile where) {
2790 if (!what || !what.isInstanceAlive) return;
2791 MakeMapObject(what.ix+8, what.iy+8, 'oFlame');
2792 if (where) where.playSound('sndSmallExplode'); else what.playSound('sndSmallExplode');
2793 if (!what.bloodless) what.scrCreateBlood(what.ix+8, what.iy+8, 3);
2795 string msg = "KALI ACCEPTS THE SACRIFICE!";
2797 auto idol = ItemGoldIdol(what);
2799 ++stats.totalSacrifices;
2800 if (global.favor <= -8) msg = "SHE SEEMS VERY ANGRY WITH YOU!";
2801 else if (global.favor < 0) msg = "SHE SEEMS ANGRY WITH YOU.";
2802 else if (global.favor >= 0) {
2803 // find other side of the altar
2804 int sx = player.ix, sy = player.iy;
2809 auto a2 = isCollisionAtPoint(altar.ix+18, altar.iy+8, &cbCollisionSacAltar);
2810 if (!a2) a2 = isCollisionAtPoint(altar.ix-8, altar.iy+8, &cbCollisionSacAltar);
2811 if (a2) { sx = a2.ix; sy = a2.iy; }
2814 auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2817 obj = MakeMapObject(sx, sy-8, 'oPoof');
2821 obj = MakeMapObject(sx, sy-16-8, 'oMonkeyGold');
2823 osdMessage(msg, 6.66);
2825 idol.instanceRemove();
2829 if (global.favor <= -8) {
2830 msg = "KALI DEVOURS THE SACRIFICE!";
2831 } else if (global.favor < 0) {
2832 global.favor += (what.status == MapObject::STUNNED ? what.favor : round(what.favor/2.0)); //k8: added `round()`
2833 if (what.favor > 0) what.favor = 0;
2835 global.favor += (what.status == MapObject::STUNNED ? what.favor : round(what.favor/2.0)); //k8: added `round()`
2839 if (myAltar.gift1 != "" and global.kaliGift == 0) scrGetKaliGift(myAltar.gift1);
2840 else if (myAltar.gift2 != "" and global.kaliGift == 1) scrGetKaliGift(myAltar.gift2);
2841 else scrGetKaliGift("");
2844 // sacrifice is player?
2845 if (what isa PlayerPawn) {
2846 ++stats.totalSelfSacrifices;
2847 msg = "KALI DEVOURS YOU!";
2848 player.visible = false;
2849 player.removeBallAndChain(temp:true);
2851 player.status = MapObject::DEAD;
2853 ++stats.totalSacrifices;
2854 auto msg2 = scrGetKaliGift(where);
2855 what.instanceRemove();
2856 if (msg2) msg = va("%s\n%s", msg, msg2);
2859 osdMessage(msg, 6.66);
2861 //!if (isRealLevel()) global.totalSacrifices += 1;
2863 //!global.messageTimer = 200;
2864 //!global.shake = 10;
2868 instance_create(x, y, oFlame);
2869 playSound(global.sndSmallExplode);
2870 scrCreateBlood(x, y, 3);
2871 global.message = "KALI ACCEPTS YOUR SACRIFICE!";
2872 if (global.favor <= -8) {
2873 global.message = "KALI DEVOURS YOUR SACRIFICE!";
2874 } else if (global.favor < 0) {
2875 if (status == 98) global.favor += favor*1.5; else global.favor += favor;
2876 if (favor > 0) favor = 0;
2878 if (status == 98) global.favor += favor*1.5; else global.favor += favor;
2881 if (myAltar.gift1 != "" and global.kaliGift == 0) scrGetFavorMsg(myAltar.gift1);
2882 else if (myAltar.gift2 != "" and global.kaliGift == 1) scrGetFavorMsg(myAltar.gift2);
2883 else scrGetFavorMsg("");
2885 global.messageTimer = 200;
2892 // ////////////////////////////////////////////////////////////////////////// //
2893 final void addBackgroundGfxDetails () {
2894 // add background details
2895 //if (global.customLevel) return;
2897 // bg = instance_create(16*randRoom(1, 42), 16*randRoom(1, 33), oCaveBG);
2898 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);
2899 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);
2900 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);
2901 else MakeMapBackTile('bgExtras', 32*global.randRoom(0, 1), 0, 32, 32, 16*global.randRoom(1, 42), 16*global.randRoom(1, 33), 10002);
2906 // ////////////////////////////////////////////////////////////////////////// //
2907 private final void fixRealViewStart () {
2908 int scale = global.scale;
2909 realViewStart.x = clamp(realViewStart.x, viewMin.x*scale, viewMax.x*scale-viewWidth);
2910 realViewStart.y = clamp(realViewStart.y, viewMin.y*scale, viewMax.y*scale-viewHeight);
2914 final int cameraCurrX () { return realViewStart.x/global.scale; }
2915 final int cameraCurrY () { return realViewStart.y/global.scale; }
2918 private final void fixViewStart () {
2919 int scale = global.scale;
2920 viewStart.x = clamp(viewStart.x, viewMin.x*scale, viewMax.x*scale-viewWidth);
2921 viewStart.y = clamp(viewStart.y, viewMin.y*scale, viewMax.y*scale-viewHeight);
2925 final void centerViewAtPlayer () {
2926 if (viewWidth < 1 || viewHeight < 1 || !player) return;
2927 centerViewAt(player.xCenter, player.yCenter);
2931 final void centerViewAt (int x, int y) {
2932 if (viewWidth < 1 || viewHeight < 1) return;
2934 cameraSlideToSpeed.x = 0;
2935 cameraSlideToSpeed.y = 0;
2936 cameraSlideToPlayer = 0;
2938 int scale = global.scale;
2941 realViewStart.x = clamp(x-viewWidth/2, 0, tilesWidth*16*scale-viewWidth);
2942 realViewStart.y = clamp(y-viewHeight/2, 0, tilesHeight*16*scale-viewHeight);
2945 viewStart.x = realViewStart.x;
2946 viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
2949 if (onCameraTeleported) onCameraTeleported();
2953 const int ViewPortToleranceX = 16*1+8;
2954 const int ViewPortToleranceY = 16*1+8;
2956 final void fixCamera () {
2957 if (!player) return;
2958 if (viewWidth < 1 || viewHeight < 1) return;
2959 int scale = global.scale;
2960 auto alwaysCenterX = global.config.alwaysCenterPlayer;
2961 auto alwaysCenterY = alwaysCenterX;
2962 // calculate offset from viewport center (in game units), and fix viewport
2964 int camDestX = player.ix+8;
2965 int camDestY = player.iy+8;
2966 if (!cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y) != 0) {
2967 // slide camera to point
2968 if (cameraSlideToSpeed.x) camDestX = cameraSlideToCurr.x;
2969 if (cameraSlideToSpeed.y) camDestY = cameraSlideToCurr.y;
2970 int dx = cameraSlideToDest.x-camDestX;
2971 int dy = cameraSlideToDest.y-camDestY;
2972 //writeln("speed:(", cameraSlideToSpeed.x, ",", cameraSlideToSpeed.y, "); cur:(", camDestX, ",", camDestY, "); dest:(", cameraSlideToDest.x, ",", cameraSlideToDest.y, "); delta=(", dx, ",", dy, ")");
2973 if (dx && cameraSlideToSpeed.x != 0) {
2974 alwaysCenterX = true;
2975 if (abs(dx) <= abs(cameraSlideToSpeed.x)) {
2976 camDestX = cameraSlideToDest.x;
2978 camDestX += sign(dx)*abs(cameraSlideToSpeed.x);
2981 if (dy && abs(cameraSlideToSpeed.y) != 0) {
2982 alwaysCenterY = true;
2983 if (abs(dy) <= cameraSlideToSpeed.y) {
2984 camDestY = cameraSlideToDest.y;
2986 camDestY += sign(dy)*abs(cameraSlideToSpeed.y);
2989 //writeln(" new:(", camDestX, ",", camDestY, ")");
2990 if (cameraSlideToSpeed.x) cameraSlideToCurr.x = camDestX;
2991 if (cameraSlideToSpeed.y) cameraSlideToCurr.y = camDestY;
2995 if (cameraSlideToSpeed.x && !cameraSlideToPlayer) {
2996 realViewStart.x = clamp(camDestX*scale, 0, tilesWidth*16*scale-viewWidth);
2997 } else if (!player.cameraBlockX) {
2998 int x = camDestX*scale;
2999 int cx = realViewStart.x;
3000 if (alwaysCenterX) {
3003 int xofs = x-(cx+viewWidth/2);
3004 if (xofs < -ViewPortToleranceX*scale) cx += xofs+ViewPortToleranceX*scale;
3005 else if (xofs > ViewPortToleranceX*scale) cx += xofs-ViewPortToleranceX*scale;
3007 // slide back to player?
3008 if (cameraSlideToPlayer && cameraSlideToSpeed.x) {
3009 int prevx = cameraSlideToCurr.x*scale;
3010 int dx = (cx-prevx)/scale;
3011 if (abs(dx) <= cameraSlideToSpeed.x) {
3012 writeln("BACKSLIDE X COMPLETE!");
3013 cameraSlideToSpeed.x = 0;
3015 cx = prevx+(sign(dx)*cameraSlideToSpeed.x)*scale;
3016 cameraSlideToCurr.x += sign(dx)*cameraSlideToSpeed.x;
3017 if ((dx < 0 && cx <= 0) || (dx > 0 && cx >= tilesWidth*16*scale-viewWidth)) {
3018 writeln("BACKSLIDE X COMPLETE!");
3019 cameraSlideToSpeed.x = 0;
3023 realViewStart.x = clamp(cx, 0, tilesWidth*16*scale-viewWidth);
3027 if (cameraSlideToSpeed.y && !cameraSlideToPlayer) {
3028 realViewStart.y = clamp(camDestY*scale, 0, tilesHeight*16*scale-viewHeight);
3029 } else if (!player.cameraBlockY) {
3030 int y = camDestY*scale;
3031 int cy = realViewStart.y;
3032 if (alwaysCenterY) {
3033 cy = y-viewHeight/2;
3035 int yofs = y-(cy+viewHeight/2);
3036 if (yofs < -ViewPortToleranceY*scale) cy += yofs+ViewPortToleranceY*scale;
3037 else if (yofs > ViewPortToleranceY*scale) cy += yofs-ViewPortToleranceY*scale;
3039 // slide back to player?
3040 if (cameraSlideToPlayer && cameraSlideToSpeed.y) {
3041 int prevy = cameraSlideToCurr.y*scale;
3042 int dy = (cy-prevy)/scale;
3043 if (abs(dy) <= cameraSlideToSpeed.y) {
3044 writeln("BACKSLIDE Y COMPLETE!");
3045 cameraSlideToSpeed.y = 0;
3047 cy = prevy+(sign(dy)*cameraSlideToSpeed.y)*scale;
3048 cameraSlideToCurr.y += sign(dy)*cameraSlideToSpeed.y;
3049 if ((dy < 0 && cy <= 0) || (dy > 0 && cy >= tilesHeight*16*scale-viewHeight)) {
3050 writeln("BACKSLIDE Y COMPLETE!");
3051 cameraSlideToSpeed.y = 0;
3055 realViewStart.y = clamp(cy, 0, tilesHeight*16*scale-viewHeight);
3058 if (cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y) == 0) cameraSlideToPlayer = 0;
3061 //writeln(" new2:(", cameraCurrX, ",", cameraCurrY, ")");
3063 viewStart.x = realViewStart.x;
3064 viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
3069 // ////////////////////////////////////////////////////////////////////////// //
3070 // x0 and y0 are non-scaled (and will be scaled)
3071 final void drawSpriteAt (name sprName, float frnumf, int x0, int y0, optional bool small) {
3072 if (!sprName) return;
3073 auto spr = sprStore[sprName];
3074 if (!spr || !spr.frames.length) return;
3075 int scale = global.scale;
3078 int frnum = max(0, trunc(frnumf))%spr.frames.length;
3079 auto sfr = spr.frames[frnum];
3080 int sx0 = x0-sfr.xofs*scale;
3081 int sy0 = y0-sfr.yofs*scale;
3082 if (small && scale > 1) {
3083 sfr.tex.blitExt(sx0, sy0, round(sx0+sfr.tex.width*(scale/2.0)), round(sy0+sfr.tex.height*(scale/2.0)), 0, 0);
3085 sfr.tex.blitAt(sx0, sy0, scale);
3090 final void drawSpriteAtS3 (name sprName, float frnumf, int x0, int y0) {
3091 if (!sprName) return;
3092 auto spr = sprStore[sprName];
3093 if (!spr || !spr.frames.length) return;
3096 int frnum = max(0, trunc(frnumf))%spr.frames.length;
3097 auto sfr = spr.frames[frnum];
3098 int sx0 = x0-sfr.xofs*3;
3099 int sy0 = y0-sfr.yofs*3;
3100 sfr.tex.blitAt(sx0, sy0, 3);
3104 // x0 and y0 are non-scaled (and will be scaled)
3105 final void drawTextAt (int x0, int y0, string text, optional int scale) {
3107 if (!specified_scale) scale = global.scale;
3110 sprStore.renderText(x0, y0, text, scale);
3114 void renderCompass (float currFrameDelta) {
3115 if (!global.hasCompass) return;
3118 if (isRoom("rOlmec")) {
3121 } else if (isRoom("rOlmec2")) {
3127 bool hasMessage = osdHasMessage();
3128 foreach (MapTile et; allExits) {
3130 int exitX = et.ix, exitY = et.iy;
3131 int vx0 = viewStart.x/global.scale, vy0 = viewStart.y/global.scale;
3132 int vx1 = (viewStart.x+viewWidth)/global.scale;
3133 int vy1 = (viewStart.y+viewHeight)/global.scale;
3134 if (exitY > vy1-16) {
3136 drawSpriteAt(hasMessage ? 'sCompassSmallLL' : 'sCompassLL', 0, 0, 224);
3137 } else if (exitX > vx1-16) {
3138 drawSpriteAt(hasMessage ? 'sCompassSmallLR' : 'sCompassLR', 0, 304, 224);
3140 drawSpriteAt(hasMessage ? 'sCompassSmallDown' : 'sCompassDown', 0, exitX-vx0, 224);
3142 } else if (exitX < vx0) {
3143 drawSpriteAt(hasMessage ? 'sCompassSmallLeft' : 'sCompassLeft', 0, 0, exitY-vy0);
3144 } else if (exitX > vx1-16) {
3145 drawSpriteAt(hasMessage ? 'sCompassSmallRight' : 'sCompassRight', 0, 304, exitY-vy0);
3147 break; // only the first exit
3152 final bool totalsNameCmpCB (ref GameStats::TotalItem a, ref GameStats::TotalItem b) {
3153 auto sa = string(a.objName);
3154 auto sb = string(b.objName);
3158 void renderTransitionInfo (float currFrameDelta) {
3161 GameStats.sortTotalsList(stats.kills, &totalsNameCmpCB);
3164 foreach (int idx, ref auto k; stats.kills) {
3165 string s = string(k);
3166 maxLen = max(maxLen, s.length);
3170 sprStore.loadFont('sFontSmall');
3171 Video.color = 0xff_ff_00;
3172 foreach (int idx, ref auto k; stats.kills) {
3174 foreach (int xidx, ref auto d; stats.totalKills) {
3175 if (d.objName == k) { deaths = d.count; break; }
3177 //drawTextAt(16, 4+idx*8, va("%s: %d (%d)", string(k.objName).toUpperCase, k.count, deaths));
3178 drawTextAt(16, 4+idx*8, string(k).toUpperCase);
3179 drawTextAt(16+maxLen, 4+idx*8, va(": %d (%d)", k.count, deaths));
3185 void renderGhostTimer (float currFrameDelta) {
3186 if (ghostTimeLeft <= 0) return;
3187 //ghostTimeLeft /= 30; // frames -> seconds
3189 int hgt = Video.screenHeight-64;
3190 if (hgt < 1) return;
3191 int rhgt = round(float(hgt)*(float(ghostTimeLeft)/30.0)/1000.0);
3192 //writeln("hgt=", hgt, "; rhgt=", rhgt, "; gtl=", ghostTimeLeft/30);
3194 auto oclr = Video.color;
3195 Video.color = 0xcf_ff_7f_00;
3196 Video.fillRect(Video.screenWidth-20, 32, 16, hgt-rhgt);
3197 Video.color = 0x7f_ff_7f_00;
3198 Video.fillRect(Video.screenWidth-20, 32+(hgt-rhgt), 16, rhgt);
3204 void renderStarsHUD (float currFrameDelta) {
3205 bool scumSmallHud = global.config.scumSmallHud;
3207 //auto life = max(0, global.plife);
3208 int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3209 int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3210 //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3215 sprStore.loadFont('sFontSmall');
3218 sprStore.loadFont('sFont');
3222 Video.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3223 //draw_sprite(sHeart, -1, view_xview[0]+8, view_yview[0]+8);
3224 //draw_text(view_xview[0]+24, view_yview[0]+8, life);
3226 if (global.plife == 1) {
3227 drawSpriteAt('sHeartSmallBlink', global.heartBlink, 10, hhup-4);
3228 global.heartBlink += 0.1;
3229 if (global.heartBlink > 3) global.heartBlink = 0;
3231 drawSpriteAt('sHeartSmall', -1, 10, hhup-4);
3232 global.heartBlink = 0;
3235 if (global.plife == 1) {
3236 drawSpriteAt('sHeartBlink', global.heartBlink, 8, hhup);
3237 global.heartBlink += 0.1;
3238 if (global.heartBlink > 3) global.heartBlink = 0;
3240 drawSpriteAt('sHeart', -1, 8, hhup);
3241 global.heartBlink = 0;
3244 int life = clamp(global.plife, 0, 99);
3245 drawTextAt(16+8, hhup, va("%d", life));
3247 //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3248 drawSpriteAt('sShopkeeperIcon', -1, 64, hhup, scumSmallHud);
3249 drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", starsKills));
3251 if (starsRoomTimer1 > 0) {
3252 sprStore.loadFont('sFontSmall');
3253 Video.color = 0xff_ff_00;
3254 int scale = global.scale;
3255 sprStore.renderMultilineTextCentered(Video.screenWidth/2, 216*scale, va("SHOTGUN CHALLENGE BEGINS IN ~%d~", (starsRoomTimer1/30)+1), scale, 0xff_00_00);
3260 void renderSunHUD (float currFrameDelta) {
3261 bool scumSmallHud = global.config.scumSmallHud;
3263 //auto life = max(0, global.plife);
3264 int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3265 int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3266 //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3271 sprStore.loadFont('sFontSmall');
3274 sprStore.loadFont('sFont');
3278 Video.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3279 //draw_sprite(sHeart, -1, view_xview[0]+8, view_yview[0]+8);
3280 //draw_text(view_xview[0]+24, view_yview[0]+8, life);
3282 if (global.plife == 1) {
3283 drawSpriteAt('sHeartSmallBlink', global.heartBlink, 10, hhup-4);
3284 global.heartBlink += 0.1;
3285 if (global.heartBlink > 3) global.heartBlink = 0;
3287 drawSpriteAt('sHeartSmall', -1, 10, hhup-4);
3288 global.heartBlink = 0;
3291 if (global.plife == 1) {
3292 drawSpriteAt('sHeartBlink', global.heartBlink, 8, hhup);
3293 global.heartBlink += 0.1;
3294 if (global.heartBlink > 3) global.heartBlink = 0;
3296 drawSpriteAt('sHeart', -1, 8, hhup);
3297 global.heartBlink = 0;
3300 int life = clamp(global.plife, 0, 99);
3301 drawTextAt(16+8, hhup, va("%d", life));
3303 //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3304 drawSpriteAt('sDamselIcon', -1, 64, hhup, scumSmallHud);
3305 drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", sunScore));
3307 if (sunRoomTimer1 > 0) {
3308 sprStore.loadFont('sFontSmall');
3309 Video.color = 0xff_ff_00;
3310 int scale = global.scale;
3311 sprStore.renderMultilineTextCentered(Video.screenWidth/2, 216*scale, va("DAMSEL CHALLENGE BEGINS IN ~%d~", (sunRoomTimer1/30)+1), scale, 0xff_00_00);
3316 void renderMoonHUD (float currFrameDelta) {
3317 bool scumSmallHud = global.config.scumSmallHud;
3319 //auto life = max(0, global.plife);
3320 int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3321 int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3322 //sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3327 sprStore.loadFont('sFontSmall');
3330 sprStore.loadFont('sFont');
3334 Video.color = 0xff_ff_ff|talpha; //draw_set_color(c_white);
3336 //draw_sprite(sShopkeeperIcon, -1, view_xview[0]+64, view_yview[0]+8);
3337 drawSpriteAt('sHoopsIcon', -1, 8, hhup, scumSmallHud);
3338 drawTextAt(8+16-(scumSmallHud ? 6 : 0), hhup, va("%d", moonScore));
3339 drawSpriteAt('sTimerIcon', -1, 64, hhup, scumSmallHud);
3340 drawTextAt(64+16-(scumSmallHud ? 6 : 0), hhup, va("%d", max(0, moonTimer)));
3342 if (moonRoomTimer1 > 0) {
3343 sprStore.loadFont('sFontSmall');
3344 Video.color = 0xff_ff_00;
3345 int scale = global.scale;
3346 sprStore.renderMultilineTextCentered(Video.screenWidth/2, 216*scale, va("ARCHERY CHALLENGE BEGINS IN ~%d~", (moonRoomTimer1/30)+1), scale, 0xff_00_00);
3351 void renderHUD (float currFrameDelta) {
3352 if (levelKind == LevelKind.Stars) { renderStarsHUD(currFrameDelta); return; }
3353 if (levelKind == LevelKind.Sun) { renderSunHUD(currFrameDelta); return; }
3354 if (levelKind == LevelKind.Moon) { renderMoonHUD(currFrameDelta); return; }
3356 if (!isHUDEnabled()) return;
3358 if (levelKind == LevelKind.Transition) { renderTransitionInfo(currFrameDelta); return; }
3366 bool scumSmallHud = global.config.scumSmallHud;
3367 if (!global.config.optSGAmmo) moneyX = ammoX;
3370 sprStore.loadFont('sFontSmall');
3373 sprStore.loadFont('sFont');
3376 //int alpha = 0x6f_00_00_00;
3377 int talpha = clamp(global.config.hudTextAlpha, 0, 255)<<24;
3378 int ialpha = clamp(global.config.hudItemsAlpha, 0, 255)<<24;
3380 //Video.color = 0xff_ff_ff;
3381 Video.color = 0xff_ff_ff|talpha;
3385 if (global.plife == 1) {
3386 drawSpriteAt('sHeartSmallBlink', global.heartBlink, lifeX+2, 4-hhup);
3387 global.heartBlink += 0.1;
3388 if (global.heartBlink > 3) global.heartBlink = 0;
3390 drawSpriteAt('sHeartSmall', -1, lifeX+2, 4-hhup);
3391 global.heartBlink = 0;
3394 if (global.plife == 1) {
3395 drawSpriteAt('sHeartBlink', global.heartBlink, lifeX, 8-hhup);
3396 global.heartBlink += 0.1;
3397 if (global.heartBlink > 3) global.heartBlink = 0;
3399 drawSpriteAt('sHeart', -1, lifeX, 8-hhup);
3400 global.heartBlink = 0;
3404 int life = clamp(global.plife, 0, 99);
3405 //if (!scumHud && life > 99) life = 99;
3406 drawTextAt(lifeX+16, 8-hhup, va("%d", life));
3409 if (global.hasStickyBombs && global.stickyBombsActive) {
3410 if (scumSmallHud) drawSpriteAt('sStickyBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sStickyBombIcon', -1, bombX, 8-hhup);
3412 if (scumSmallHud) drawSpriteAt('sBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sBombIcon', -1, bombX, 8-hhup);
3414 int n = global.bombs;
3415 if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3416 drawTextAt(bombX+16, 8-hhup, va("%d", n));
3419 if (scumSmallHud) drawSpriteAt('sRopeIconSmall', -1, ropeX+2, 4-hhup); else drawSpriteAt('sRopeIcon', -1, ropeX, 8-hhup);
3421 if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3422 drawTextAt(ropeX+16, 8-hhup, va("%d", n));
3425 if (global.config.optSGAmmo && (!player || player.holdItem !isa ItemWeaponBow)) {
3426 if (scumSmallHud) drawSpriteAt('sShellsIcon', -1, ammoX+6, 8-hhup); else drawSpriteAt('sShellsIcon', -1, ammoX+7, 12-hhup);
3428 if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3429 drawTextAt(ammoX+16, 8-hhup, va("%d", n));
3430 } else if (player && player.holdItem isa ItemWeaponBow) {
3431 if (scumSmallHud) drawSpriteAt('sArrowRight', -1, ammoX+6, 8-hhup); else drawSpriteAt('sArrowRight', -1, ammoX+7, 12-hhup);
3433 if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
3434 drawTextAt(ammoX+16, 8-hhup, va("%d", n));
3438 if (scumSmallHud) drawSpriteAt('sDollarSignSmall', -1, moneyX+2, 3-hhup); else drawSpriteAt('sDollarSign', -1, moneyX, 8-hhup);
3439 drawTextAt(moneyX+16, 8-hhup, va("%d", stats.money-xmoney));
3442 Video.color = 0xff_ff_ff|ialpha;
3444 int ity = (scumSmallHud ? 18-hhup : 24-hhup);
3447 if (global.hasUdjatEye) {
3448 if (global.udjatBlink) drawSpriteAt('sUdjatEyeIcon2', -1, n, ity); else drawSpriteAt('sUdjatEyeIcon', -1, n, ity);
3451 if (global.hasAnkh) { drawSpriteAt('sAnkhIcon', -1, n, ity); n += 20; }
3452 if (global.hasCrown) { drawSpriteAt('sCrownIcon', -1, n, ity); n += 20; }
3453 if (global.hasKapala) {
3454 if (global.bloodLevel == 0) drawSpriteAt('sKapalaIcon', 0, n, ity);
3455 else if (global.bloodLevel <= 2) drawSpriteAt('sKapalaIcon', 1, n, ity);
3456 else if (global.bloodLevel <= 4) drawSpriteAt('sKapalaIcon', 2, n, ity);
3457 else if (global.bloodLevel <= 6) drawSpriteAt('sKapalaIcon', 3, n, ity);
3458 else if (global.bloodLevel <= 8) drawSpriteAt('sKapalaIcon', 4, n, ity);
3461 if (global.hasSpectacles) { drawSpriteAt('sSpectaclesIcon', -1, n, ity); n += 20; }
3462 if (global.hasGloves) { drawSpriteAt('sGlovesIcon', -1, n, ity); n += 20; }
3463 if (global.hasMitt) { drawSpriteAt('sMittIcon', -1, n, ity); n += 20; }
3464 if (global.hasSpringShoes) { drawSpriteAt('sSpringShoesIcon', -1, n, ity); n += 20; }
3465 if (global.hasJordans) { drawSpriteAt('sJordansIcon', -1, n, ity); n += 20; }
3466 if (global.hasSpikeShoes) { drawSpriteAt('sSpikeShoesIcon', -1, n, ity); n += 20; }
3467 if (global.hasCape) { drawSpriteAt('sCapeIcon', -1, n, ity); n += 20; }
3468 if (global.hasJetpack) { drawSpriteAt('sJetpackIcon', -1, n, ity); n += 20; }
3469 if (global.hasStickyBombs) { drawSpriteAt('sPasteIcon', -1, n, ity); n += 20; }
3470 if (global.hasCompass) { drawSpriteAt('sCompassIcon', -1, n, ity); n += 20; }
3471 if (global.hasParachute) { drawSpriteAt('sParachuteIcon', -1, n, ity); n += 20; }
3473 if (player.holdItem && player.holdItem.objName == 'Bow' && global.arrows > 0) {
3476 while (m <= global.arrows && m <= 20 && malpha > 0) {
3477 Video.color = trunc(malpha*255)<<24|0xff_ff_ff;
3478 drawSpriteAt('sArrowIcon', -1, n, ity);
3480 if (m > 10) malpha -= 0.1; //and global.arrows > 20 malpha -= 0.1
3486 sprStore.loadFont('sFontSmall');
3487 Video.color = 0xff_ff_00|talpha;
3488 if (scumSmallHud) drawTextAt(moneyX+8, 8+8-hhup, va("+%d", xmoney));
3489 else drawTextAt(moneyX, 8+16-hhup, va("+%d", xmoney));
3492 Video.color = 0xff_ff_ff;
3493 if (global.config.ghostShowTime) renderGhostTimer(currFrameDelta);
3497 // ////////////////////////////////////////////////////////////////////////// //
3498 // x0 and y0 are non-scaled (and will be scaled)
3499 final void drawTextAtS3 (int x0, int y0, string text, optional int hiColor1, optional int hiColor2) {
3503 sprStore.renderTextWithHighlight(x0, y0, text, 3, hiColor1!optional, hiColor2!optional);
3507 final void drawTextAtS3Centered (int y0, string text, optional int hiColor1, optional int hiColor2) {
3509 int x0 = (Video.screenWidth-sprStore.getTextWidth(text, 3, specified_hiColor1, specified_hiColor2))/2;
3510 sprStore.renderTextWithHighlight(x0, y0*3, text, 3, hiColor1!optional, hiColor2!optional);
3514 void renderHelpOverlay () {
3516 Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
3519 int txoff = 0; // text x pos offset (for multi-color lines)
3521 if (gameHelpScreen) {
3522 sprStore.loadFont('sFontSmall');
3523 Video.color = 0xff_ff_ff;
3524 drawTextAtS3Centered(ty, va("HELP (PAGE ~%d~ OF ~%d~)", gameHelpScreen, MaxGameHelpScreen), 0xff_ff_00);
3528 if (gameHelpScreen == 1) {
3529 sprStore.loadFont('sFontSmall');
3530 Video.color = 0xff_ff_00; drawTextAtS3(tx, ty, "INVENTORY BASICS"); ty += 16;
3531 Video.color = 0xff_ff_ff;
3532 drawTextAtS3(tx, ty, global.expandString("Press $SWITCH to cycle through items."), 0x00_ff_00);
3535 Video.color = 0xff_ff_ff;
3536 drawSpriteAtS3('sHelpSprite1', -1, 64, 96);
3537 } else if (gameHelpScreen == 2) {
3538 sprStore.loadFont('sFontSmall');
3539 Video.color = 0xff_ff_00;
3540 drawTextAtS3(tx, ty, "SELLING TO SHOPKEEPERS"); ty += 16;
3541 Video.color = 0xff_ff_ff;
3542 drawTextAtS3(tx, ty, global.expandString("Press $PAY to offer your currently"), 0x00_ff_00); ty += 8;
3543 drawTextAtS3(tx, ty, "held item to the shopkeeper."); ty += 16;
3544 drawTextAtS3(tx, ty, "If the shopkeeper is interested, "); ty += 8;
3545 //drawTextAtS3(tx, ty, global.expandString("press $PAY again to complete the sale."), 0x00_ff_00); ty += 72;
3546 drawTextAtS3(tx, ty, global.expandString("press $PAY again to complete"), 0x00_ff_00);
3547 drawTextAtS3(tx, ty+8, "the sale.");
3549 drawSpriteAtS3('sHelpSell', -1, 112, 100);
3550 drawTextAtS3(tx, ty, "Purchasing goods from the shopkeeper"); ty += 8;
3551 drawTextAtS3(tx, ty, "will increase the funds he has"); ty += 8;
3552 drawTextAtS3(tx, ty, "available to buy your unwanted stuff."); ty += 8;
3555 sprStore.loadFont('sFont');
3556 Video.color = 0xff_ff_ff;
3557 drawTextAtS3(136, 8, "MAP");
3559 Video.color = 0xff_ff_00;
3560 drawTextAtS3Centered(24, lg.mapTitle);
3563 auto spf = sprStore[lg.mapSprite].frames[0];
3564 int mapX = 160-spf.width/2;
3565 int mapY = 120-spf.height/2;
3566 //mapTitleX = 160-string_length(global.mapTitle)*8/2;
3568 Video.color = 0xff_ff_ff;
3569 drawSpriteAtS3(lg.mapSprite, -1, mapX, mapY);
3571 if (lg.mapSprite != 'sMapDefault') {
3572 int mx = -1, my = -1;
3574 // set position of player icon
3575 switch (global.currLevel) {
3576 case 1: mx = 81; my = 22; break;
3577 case 2: mx = 113; my = 63; break;
3578 case 3: mx = 197; my = 86; break;
3579 case 4: mx = 133; my = 109; break;
3580 case 5: mx = 181; my = 22; break;
3581 case 6: mx = 126; my = 64; break;
3582 case 7: mx = 158; my = 112; break;
3583 case 8: mx = 66; my = 80; break;
3584 case 9: mx = 30; my = 26; break;
3585 case 10: mx = 88; my = 54; break;
3586 case 11: mx = 148; my = 81; break;
3587 case 12: mx = 210; my = 205; break;
3588 case 13: mx = 66; my = 17; break;
3589 case 14: mx = 146; my = 17; break;
3590 case 15: mx = 82; my = 77; break;
3591 case 16: mx = 178; my = 81; break;
3595 int plrx = mx+player.ix/16;
3596 int plry = my+player.iy/16;
3597 name plrspr = 'sMapSpelunker';
3598 if (global.isDamsel) plrspr = 'sMapDamsel';
3599 else if (global.isTunnelMan) plrspr = 'sMapTunnel';
3600 auto ss = sprStore[plrspr];
3601 drawSpriteAtS3(plrspr, (pausedTime/2)%ss.frames.length, mapX+plrx, mapY+plry);
3603 if (global.hasCompass && allExits.length) {
3604 drawSpriteAtS3('sMapRedDot', -1, mapX+mx+allExits[0].ix/16, mapY+my+allExits[0].iy/16);
3611 sprStore.loadFont('sFontSmall');
3612 Video.color = 0xff_ff_00;
3613 drawTextAtS3Centered(232, "PRESS ~SPACE~/~LEFT~/~RIGHT~ TO CHANGE PAGE", 0x00_ff_00);
3615 Video.color = 0xff_ff_ff;
3619 void renderPauseOverlay () {
3620 //drawTextAt(256, 432, "PAUSED", scale);
3622 if (gameShowHelp) { renderHelpOverlay(); return; }
3624 Video.color = 0xff_ff_00;
3625 //int hiColor = 0x00_ff_00;
3628 if (isTutorialRoom()) {
3629 sprStore.loadFont('sFont');
3630 drawTextAtS3(40, n-24, "TUTORIAL CAVE");
3631 } else if (isNormalLevel()) {
3632 sprStore.loadFont('sFont');
3634 drawTextAtS3Centered(n-32, va("LEVEL ~%d~", global.currLevel), 0x00_ff_00);
3636 sprStore.loadFont('sFontSmall');
3638 int depth = round((174.8*(global.currLevel-1)+(player.iy+8)*0.34)*(global.config.scumMetric ? 0.3048 : 1.0)*10);
3639 string depthStr = va("DEPTH: ~%d.%d~ %s", depth/10, depth%10, (global.config.scumMetric ? "METRES" : "FEET"));
3640 drawTextAtS3Centered(n-16, depthStr, 0x00_ff_00);
3643 drawTextAtS3Centered(n, va("MONEY: ~%d~", stats.money), 0x00_ff_00);
3644 drawTextAtS3Centered(n+16, va("KILLS: ~%d~", stats.countKills), 0x00_ff_00);
3645 drawTextAtS3Centered(n+32, va("SAVES: ~%d~", stats.damselsSaved), 0x00_ff_00);
3646 drawTextAtS3Centered(n+48, va("TIME: ~%s~", time2str(time/30)), 0x00_ff_00);
3647 drawTextAtS3Centered(n+64, va("LEVEL TIME: ~%s~", time2str((time-levelStartTime)/30)), 0x00_ff_00);
3650 sprStore.loadFont('sFontSmall');
3651 Video.color = 0xff_ff_ff;
3652 drawTextAtS3Centered(240-2-8, "~ESC~-RETURN ~F10~-QUIT ~CTRL+DEL~-SUICIDE", 0xff_7f_00);
3653 drawTextAtS3Centered(2, "~O~PTIONS REDEFINE ~K~EYS ~S~TATISTICS", 0xff_7f_00);
3657 // ////////////////////////////////////////////////////////////////////////// //
3658 private transient array!MapEntity renderVisibleCids;
3659 private transient array!MapEntity renderVisibleLights;
3660 private transient array!MapTile renderFrontTiles; // normal, with fg
3662 final bool renderSortByDepth (MapEntity oa, MapEntity ob) {
3663 auto da = oa.depth, db = ob.depth;
3664 if (da == db) return (oa.objId < ob.objId);
3669 const int RenderEdgePixNormal = 64;
3670 const int RenderEdgePixLight = 256;
3672 #ifndef EXPERIMENTAL_RENDER_CACHE
3673 enum skipListCreation = false;
3676 void renderWithOfs (int xofs, int yofs, float currFrameDelta) {
3677 int scale = global.scale;
3679 // don't touch framebuffer alpha
3680 Video.colorMask = Video::CMask.Colors;
3681 Video.color = 0xff_ff_ff;
3683 bool isDarkLevel = global.darkLevel;
3686 switch (global.config.scumPlayerLit) {
3687 case 0: player.lightRadius = 0; break; // never
3688 case 1: // only in "scumDarkness"
3689 player.lightRadius = (global.config.scumDarkness >= 2 ? 96 : 32);
3692 player.lightRadius = 96;
3697 // render cave background
3700 int bgw = levBGImg.tex.width*scale;
3701 int bgh = levBGImg.tex.height*scale;
3702 int bgTW = (tilesWidth*tsz+bgw-1)/bgw;
3703 int bgTH = (tilesHeight*tsz+bgh-1)/bgh;
3704 int bgX0 = max(0, xofs/bgw);
3705 int bgY0 = max(0, yofs/bgh);
3706 int bgX1 = min(bgTW, (xofs+viewWidth+bgw-1)/bgw);
3707 int bgY1 = min(bgTH, (yofs+viewHeight+bgh-1)/bgh);
3708 foreach (int ty; bgY0..bgY1) {
3709 foreach (int tx; bgX0..bgX1) {
3710 int x0 = tx*bgw-xofs;
3711 int y0 = ty*bgh-yofs;
3712 levBGImg.tex.blitAt(x0, y0, scale);
3717 int RenderEdgePix = (global.darkLevel ? RenderEdgePixLight : RenderEdgePixNormal);
3719 // render background tiles
3720 for (MapBackTile bt = backtiles; bt; bt = bt.next) {
3721 bt.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3724 // collect visible special tiles
3725 #ifdef EXPERIMENTAL_RENDER_CACHE
3726 bool skipListCreation = (lastRenderTime == time && renderVisibleCids.length); //FIXME
3729 if (!skipListCreation) {
3730 renderVisibleCids.clear();
3731 renderVisibleLights.clear();
3732 renderFrontTiles.clear();
3734 int endVX = xofs+viewWidth;
3735 int endVY = yofs+viewHeight;
3739 if (player && player.visible) renderVisibleCids[$] = player; // include player in render list
3741 //FIXME: drop lit objects which cannot affect visible area
3743 // collect visible objects
3744 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)) {
3745 if (!o.visible) continue;
3746 auto tile = MapTile(o);
3748 if (isDarkLevel && (o.lightRadius > 4 || tile.litWholeTile)) renderVisibleLights[$] = o;
3749 if (tile.invisible) continue;
3750 if (tile.bgfront) renderFrontTiles[$] = tile;
3751 if (tile.bgback) tile.drawWithOfsBack(xofs, yofs, scale, currFrameDelta);
3753 if (isDarkLevel && o.lightRadius > 4) renderVisibleLights[$] = o;
3755 // check if the object is really visible -- this will speed up later sorting
3756 int fx0, fy0, fx1, fy1;
3757 auto spf = o.getSpriteFrame(default, out fx0, out fy0, out fx1, out fy1);
3758 if (!spf) continue; // no sprite -- nothing to draw (no, really)
3759 int ix = o.ix, iy = o.iy;
3760 int x0 = (ix+fx0)*scale, y0 = (iy+fy0)*scale;
3761 int x1 = (ix+fx1)*scale, y1 = (iy+fy1)*scale;
3762 if (x1 <= xofs || y1 <= yofs || x0 >= endVX || y0 >= endVY) {
3766 renderVisibleCids[$] = o;
3769 foreach (MapEntity o; objGrid.allObjects()) {
3770 if (!o.visible) continue;
3771 auto tile = MapTile(o);
3773 if (isDarkLevel && (o.lightRadius > 4 || tile.litWholeTile)) renderVisibleLights[$] = o;
3774 if (tile.invisible) continue;
3775 if (tile.bgfront) renderFrontTiles[$] = tile;
3776 if (tile.bgback) tile.drawWithOfsBack(xofs, yofs, scale, currFrameDelta);
3778 if (isDarkLevel && o.lightRadius > 4) renderVisibleLights[$] = o;
3780 renderVisibleCids[$] = o;
3783 //writeln("::: ", cnt, " invisible objects dropped");
3785 renderVisibleCids.sort(&renderSortByDepth);
3786 lastRenderTime = time;
3789 auto depth4Start = 0;
3790 foreach (auto xidx, MapEntity o; renderVisibleCids) {
3797 bool playerPowerupRendered = false;
3799 // render objects (part one: depth > 3)
3800 foreach (auto idx; depth4Start..renderVisibleCids.length; reverse) {
3801 MapEntity o = renderVisibleCids[idx];
3802 // 1000 is an ordinary tile
3803 if (!playerPowerupRendered && o.depth <= 1200) {
3804 playerPowerupRendered = true;
3805 // so ducking player will have it's cape correctly rendered
3806 if (player.visible) player.drawPrePrePowerupWithOfs(xofs, yofs, scale, currFrameDelta);
3808 //if (idx && renderSortByDepth(o, renderVisibleCids[idx-1])) writeln("VIOLATION!");
3809 o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3812 // render object (part two: front tile parts, depth 3.5)
3813 foreach (MapTile tile; renderFrontTiles) {
3814 tile.drawWithOfsFront(xofs, yofs, scale, currFrameDelta);
3817 // render objects (part three: depth <= 3)
3818 foreach (auto idx; 0..depth4Start; reverse) {
3819 MapEntity o = renderVisibleCids[idx];
3820 o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3821 //done above;if (isDarkLevel && (o.lightRadius > 4 || (o isa MapTile && MapTile(o).litWholeTile))) renderVisibleLights[$] = o;
3824 // render player post-effects, so cape or jetpack will be correctly rendered on level exit, for example
3825 player.lastDrawWithOfs(xofs, yofs, scale, currFrameDelta);
3829 auto ltex = bgtileStore.lightTexture('ltx512', 512);
3831 // set screen alpha to min
3832 Video.colorMask = Video::CMask.Alpha;
3833 Video.blendMode = Video::BlendMode.None;
3834 Video.color = 0xff_ff_ff_ff;
3835 Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
3836 //Video.colorMask = Video::CMask.All;
3839 // also, stencil 'em, so we can filter dark areas
3840 Video.textureFiltering = true;
3841 Video.stencil = true;
3842 Video.stencilFunc(Video::StencilFunc.Always, 1);
3843 Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Replace);
3844 Video.alphaTestFunc = Video::AlphaFunc.Greater;
3845 Video.alphaTestVal = 0.03+0.011*global.config.darknessDarkness;
3846 Video.color = 0xff_ff_ff;
3847 Video.blendFunc = Video::BlendFunc.Max;
3848 Video.blendMode = Video::BlendMode.Blend; // anything except `Normal`
3849 Video.colorMask = Video::CMask.Alpha;
3851 foreach (MapEntity e; renderVisibleLights) {
3853 e.getInterpCoords(currFrameDelta, scale, out xi, out yi);
3854 auto tile = MapTile(e);
3855 if (tile && tile.litWholeTile) {
3856 //Video.color = 0xff_ff_ff;
3857 Video.fillRect(xi-xofs, yi-yofs, e.width*scale, e.height*scale);
3859 int lrad = e.lightRadius;
3860 if (lrad < 4) continue; // just in case
3862 float lightscale = float(lrad*scale)/float(ltex.tex.width);
3863 #ifdef OLD_LIGHT_OFFSETS
3864 int fx0, fy0, fx1, fy1;
3866 auto spf = e.getSpriteFrame(out doMirror, out fx0, out fy0, out fx1, out fy1);
3868 xi += (fx1-fx0)*scale/2;
3869 yi += (fy1-fy0)*scale/2;
3873 e.getLightOffset(out lxofs, out lyofs);
3878 lrad = lrad*scale/2;
3881 ltex.tex.blitAt(xi, yi, lightscale);
3883 Video.textureFiltering = false;
3885 // modify only lit parts
3886 Video.stencilFunc(Video::StencilFunc.Equal, 1);
3887 Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Keep);
3888 // multiply framebuffer colors by framebuffer alpha
3889 Video.color = 0xff_ff_ff; // it doesn't matter
3890 Video.blendFunc = Video::BlendFunc.Add;
3891 Video.blendMode = Video::BlendMode.DstMulDstAlpha;
3892 Video.colorMask = Video::CMask.Colors;
3893 Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
3895 // filter unlit parts
3896 Video.stencilFunc(Video::StencilFunc.NotEqual, 1);
3897 Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Keep);
3898 Video.blendFunc = Video::BlendFunc.Add;
3899 Video.blendMode = Video::BlendMode.Filter;
3900 Video.colorMask = Video::CMask.Colors;
3901 Video.color = 0x00_00_18+0x00_00_10*global.config.darknessDarkness;
3902 //Video.color = 0x00_00_18;
3903 //Video.color = 0x00_00_38;
3904 Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
3907 Video.blendFunc = Video::BlendFunc.Add;
3908 Video.blendMode = Video::BlendMode.Normal;
3909 Video.colorMask = Video::CMask.All;
3910 Video.alphaTestFunc = Video::AlphaFunc.Always;
3911 Video.stencil = false;
3914 // clear visible objects list (nope)
3915 //renderVisibleCids.clear();
3916 //renderVisibleLights.clear();
3919 if (global.config.drawHUD) renderHUD(currFrameDelta);
3920 renderCompass(currFrameDelta);
3922 float osdTimeLeft, osdTimeStart;
3923 string msg = osdGetMessage(out osdTimeLeft, out osdTimeStart);
3925 auto ct = GetTickCount();
3927 sprStore.loadFont('sFontSmall');
3928 auto msgHeight = sprStore.getMultilineTextHeight(msg, msgScale);
3929 int x = Video.screenWidth/2;
3930 int y = Video.screenHeight-64-msgHeight;
3931 auto oldColor = Video.color;
3932 Video.color = 0xff_ff_00;
3933 if (osdTimeLeft < 0.5) {
3934 int alpha = 255-clamp(int(510*osdTimeLeft), 0, 255);
3935 Video.color = Video.color|(alpha<<24);
3936 } else if (ct-osdTimeStart < 0.5) {
3937 osdTimeStart = ct-osdTimeStart;
3938 int alpha = 255-clamp(int(510*osdTimeStart), 0, 255);
3939 Video.color = Video.color|(alpha<<24);
3941 sprStore.renderMultilineTextCentered(x, y, msg, msgScale, 0x00_ff_00, 0xff_ff_ff);
3942 Video.color = oldColor;
3945 if (inWinCutscene) renderWinCutsceneOverlay();
3946 Video.color = 0xff_ff_ff;
3950 // ////////////////////////////////////////////////////////////////////////// //
3951 final class!MapObject findGameObjectClassByName (name aname) {
3952 if (!aname) return none; // just in case
3953 auto co = FindClassByGameObjName(aname);
3955 writeln("***UNKNOWN GAME OBJECT: '", aname, "'");
3958 co = GetClassReplacement(co);
3959 if (!co) FatalError("findGameObjectClassByName: WTF?!");
3960 if (!class!MapObject(co)) FatalError("findGameTileClassByName: object '%n' is not a map object, it is a `%n`", aname, GetClassName(co));
3961 return class!MapObject(co);
3965 final class!MapTile findGameTileClassByName (name aname) {
3966 if (!aname) return none; // just in case
3967 auto co = FindClassByGameObjName(aname);
3968 if (!co) return MapTile; // unknown names will be routed directly to tile object
3969 co = GetClassReplacement(co);
3970 if (!co) FatalError("findGameTileClassByName: WTF?!");
3971 if (!class!MapTile(co)) FatalError("findGameTileClassByName: object '%n' is not a tile, it is a `%n`", aname, GetClassName(co));
3972 return class!MapTile(co);
3976 final MapObject findAnyObjectOfType (name aname) {
3977 if (!aname) return none;
3978 auto cls = FindClassByGameObjName(aname);
3979 if (!cls) return none;
3980 foreach (MapObject obj; objGrid.allObjects(MapObject)) {
3981 if (obj.spectral) continue;
3982 if (obj isa cls) return obj;
3988 // ////////////////////////////////////////////////////////////////////////// //
3989 final bool isRopePlacedAt (int x, int y) {
3991 foreach (ref auto v; covered) v = false;
3992 foreach (MapTile t; objGrid.inRectPix(x, y-8, 1, 17, precise:false, castClass:MapTileRope)) {
3993 //if (!cbIsRopeTile(t)) continue;
3994 if (t.ix != x) continue;
3995 if (t.iy == y) return true;
3996 foreach (int ty; t.iy..t.iy+8) {
3998 if (d >= 0 && d < covered.length) covered[d] = true;
4001 // check if the whole rope height is completely covered with ropes
4002 foreach (auto v; covered) if (!v) return false;
4007 final MapTile CreateMapTile (int xpos, int ypos, name aname) {
4008 if (!aname) FatalError("cannot create typeless tile");
4009 auto tclass = findGameTileClassByName(aname);
4010 if (!tclass) return none;
4011 MapTile tile = SpawnObject(tclass);
4012 tile.global = global;
4014 tile.objName = aname;
4015 tile.objType = aname; // just in case
4018 tile.objId = ++lastUsedObjectId;
4019 if (!tile.initialize()) FatalError("cannot create tile '%n'", aname);
4024 final bool PutSpawnedMapTile (int x, int y, MapTile tile, optional bool putToGrid) {
4025 if (!tile || !tile.isInstanceAlive) return false;
4027 if (!putToGrid) putToGrid = (tile.moveable || tile.toSpecialGrid || tile.width != 16 || tile.height != 16 || x%16 != 0 || y%16 != 0);
4029 //writeln("putting tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (putToGrid ? "special " : ""), "grid, pos=(", x/16.0, ",", y/16.0, ")");
4032 int mapx = x/16, mapy = y/16;
4033 if (mapx < 0 || mapx >= tilesWidth || mapy < 0 || mapy >= tilesHeight) return false;
4036 // if we already have rope tile there, there is no reason to add another one
4037 if (tile isa MapTileRope) {
4038 if (isRopePlacedAt(x, y)) return false;
4041 // activate special or animated tile
4042 tile.active = tile.active || putToGrid || tile.moveable || tile.toSpecialGrid || tile.lava /*|| tile.water*/; // will be done in MakeMapTile
4043 // animated tiles must be active
4045 auto spr = tile.getSprite();
4046 if (spr && spr.frames.length > 1) {
4047 writeln("activated animated tile '", tile.objName, "'");
4055 //if (tile isa TitleTileCopy) writeln("*** PUTTING COPYRIGHT TILE");
4056 tile.toSpecialGrid = true;
4057 if (!tile.dontReplaceOthers && x&16 == 0 && y%16 == 0) {
4058 auto t = getTileAtGridAny(x/16, y/16);
4059 if (t && !t.immuneToReplacement) {
4060 writeln("REPLACING tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "' at ", (t.toSpecialGrid ? "special " : ""), "grid, pos=(", t.fltx/16.0, ",", t.flty/16.0, ")");
4061 writeln(" NEW tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (tile.toSpecialGrid ? "special " : ""), "grid, pos=(", tile.fltx/16.0, ",", tile.flty/16.0, ")");
4065 objGrid.insert(tile);
4067 //writeln("SIZE: ", tilesWidth, "x", tilesHeight);
4068 setTileAtGrid(x/16, y/16, tile);
4069 auto t = getTileAtGridAny(x/16, y/16);
4072 writeln("putting tile(", GetClassName(tile.Class), "): '", tile.objType, ":", tile.objName, "' at ", (putToGrid ? "special " : ""), "grid, pos=(", x/16.0, ",", y/16.0, ")");
4073 checkTilesInRect(x/16, y/16, 16, 16, delegate bool (MapTile tile) {
4074 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, ")");
4077 FatalError("FUUUUUU");
4082 if (tile.enter) registerEnter(tile);
4083 if (tile.exit) registerExit(tile);
4089 // won't call `onDestroy()`
4090 final void RemoveMapTileFromGrid (int tileX, int tileY, optional string reason) {
4091 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
4092 auto t = getTileAtGridAny(tileX, tileY);
4094 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, ")");
4102 final MapTile MakeMapTile (int mapx, int mapy, name aname, optional bool putToGrid) {
4103 //if (mapy >= tilesHeight) { writeln("OOM: ", aname, "; ty=", mapy, "; hgt=", tilesHeight); }
4104 if (mapx < 0 || mapx >= tilesWidth || mapy < 0 || mapy >= tilesHeight) return none;
4106 // if we already have rope tile there, there is no reason to add another one
4107 if (aname == 'oRope') {
4108 if (isRopePlacedAt(mapx*16, mapy*16)) return none;
4111 auto tile = CreateMapTile(mapx*16, mapy*16, aname);
4112 if (!tile) return none;
4113 if (!PutSpawnedMapTile(mapx*16, mapy*16, tile, putToGrid!optional)) {
4122 final MapTile MakeMapTileAtPix (int xpix, int ypix, name aname, optional bool putToGrid) {
4123 // if we already have rope tile there, there is no reason to add another one
4124 if (aname == 'oRope') {
4125 if (isRopePlacedAt(xpix, ypix)) return none;
4128 auto tile = CreateMapTile(xpix, ypix, aname);
4129 if (!tile) return none;
4130 if (!PutSpawnedMapTile(xpix, ypix, tile, putToGrid!optional)) {
4139 final MapTile MakeMapRopeTileAt (int x0, int y0) {
4140 // if we already have rope tile there, there is no reason to add another one
4141 if (isRopePlacedAt(x0, y0)) return none;
4143 auto tile = CreateMapTile(x0, y0, 'oRope');
4144 if (!PutSpawnedMapTile(x0, y0, tile, putToGrid:true)) {
4153 // ////////////////////////////////////////////////////////////////////////// //
4154 final MapBackTile CreateBgTile (name sprName, optional int atx0, optional int aty0, optional int aw, optional int ah) {
4155 BackTileImage img = bgtileStore[sprName];
4156 auto res = SpawnObject(MapBackTile);
4157 res.global = global;
4160 res.bgtName = sprName;
4161 if (specified_atx0) res.tx0 = atx0;
4162 if (specified_aty0) res.ty0 = aty0;
4163 if (specified_aw) res.w = aw;
4164 if (specified_ah) res.h = ah;
4165 if (!res.initialize()) FatalError("cannot create background tile with image '%n'", sprName);
4170 // ////////////////////////////////////////////////////////////////////////// //
4172 background The background asset from which the new tile will be extracted.
4173 left The x coordinate of the left of the new tile, relative to the background asset's top left corner.
4174 top The y coordinate of the top of the new tile, relative to the background assets top left corner.
4175 width The width of the tile.
4176 height The height of the tile.
4177 x The x position in the room to place the tile.
4178 y The y position in the room to place the tile.
4179 depth The depth at which to place the tile.
4181 final void MakeMapBackTile (name bgname, int left, int top, int width, int height, int x, int y, int depth) {
4182 if (width < 1 || height < 1 || !bgname) return;
4183 auto bgt = bgtileStore[bgname];
4184 if (!bgt) FatalError("cannot load background '%n'", bgname);
4185 MapBackTile bt = SpawnObject(MapBackTile);
4188 bt.objName = bgname;
4190 bt.bgtName = bgname;
4198 // find a place for it
4203 // back tiles with the highest depth should come first
4204 MapBackTile ct = backtiles, cprev = none;
4205 while (ct && depth <= ct.depth) { cprev = ct; ct = ct.next; }
4208 bt.next = cprev.next;
4211 bt.next = backtiles;
4217 // ////////////////////////////////////////////////////////////////////////// //
4218 final MapObject SpawnMapObjectWithClass (class!MapObject oclass) {
4219 if (!oclass) return none;
4221 MapObject obj = SpawnObject(oclass);
4222 if (!obj) FatalError("cannot create map object '%n'", GetClassName(oclass));
4224 //if (aname == 'oShells' || aname == 'oShellSingle' || aname == 'oRopePile') writeln("*** CREATED '", aname, "'");
4226 obj.global = global;
4228 obj.objId = ++lastUsedObjectId;
4234 final MapObject SpawnMapObject (name aname) {
4235 if (!aname) return none;
4236 auto res = SpawnMapObjectWithClass(findGameObjectClassByName(aname));
4237 if (res && !res.objType) res.objType = aname; // just in case
4242 final MapObject PutSpawnedMapObject (int x, int y, MapObject obj) {
4243 if (!obj /*|| obj.global || obj.level*/) return none; // oops
4247 if (!obj.initialize()) { delete obj; return none; } // not fatal
4255 final MapObject MakeMapObject (int x, int y, name aname) {
4256 MapObject obj = SpawnMapObject(aname);
4257 obj = PutSpawnedMapObject(x, y, obj);
4262 // ////////////////////////////////////////////////////////////////////////// //
4263 int winCutSceneTimer = -1;
4264 int winVolcanoTimer = -1;
4265 int winCutScenePhase = 0;
4266 int winSceneDrawStatus = 0;
4267 int winMoneyCount = 0;
4269 bool winFadeOut = false;
4270 int winFadeLevel = 0;
4271 int winCutsceneSkip = 0; // 1: waiting for pay release; 2: pay released, do skip
4272 bool winCutsceneSwitchToNext = false;
4275 void startWinCutscene () {
4276 global.hasParachute = false;
4278 winCutsceneSwitchToNext = false;
4279 winCutsceneSkip = 0;
4280 isKeyPressed(GameConfig::Key.Pay);
4281 isKeyReleased(GameConfig::Key.Pay);
4283 auto olddel = ImmediateDelete;
4284 ImmediateDelete = false;
4289 addBackgroundGfxDetails();
4291 levBGImgName = 'bgCave';
4292 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
4294 blockWaterChecking = true;
4298 ImmediateDelete = olddel;
4299 CollectGarbage(true); // destroy delayed objects too
4301 if (dumpGridStats) objGrid.dumpStats();
4303 playerExited = false; // just in case
4304 playerExitDoor = none;
4312 winCutSceneTimer = -1;
4313 winCutScenePhase = 0;
4316 if (global.config.gameMode != GameConfig::GameMode.Vanilla) {
4317 if (global.config.bizarre) {
4318 global.yasmScore = 1;
4319 global.config.bizarrePlusTitle = true;
4322 array!MapTile toReplace;
4323 forEachTile(delegate bool (MapTile t) {
4324 if (t.objType == 'oGTemple' ||
4325 t.objType == 'oIce' ||
4326 t.objType == 'oDark' ||
4327 t.objType == 'oBrick' ||
4328 t.objType == 'oLush')
4335 foreach (MapTile t; miscTileGrid.allObjects()) {
4336 if (t.objType == 'oGTemple' ||
4337 t.objType == 'oIce' ||
4338 t.objType == 'oDark' ||
4339 t.objType == 'oBrick' ||
4340 t.objType == 'oLush')
4346 foreach (MapTile t; toReplace) {
4348 t.cleanDeath = true;
4349 if (rand(1,120) == 1) instance_change(oGTemple, false);
4350 else if (rand(1,100) == 1) instance_change(oIce, false);
4351 else if (rand(1,90) == 1) instance_change(oDark, false);
4352 else if (rand(1,80) == 1) instance_change(oBrick, false);
4353 else if (rand(1,70) == 1) instance_change(oLush, false);
4361 if (rand(1,5) == 1) instance_change(oLush, false);
4366 //!instance_create(0, 0, oBricks);
4368 //shakeToggle = false;
4369 //oPDummy.status = 2;
4374 if (global.kaliPunish >= 2) {
4375 instance_create(oPDummy.x, oPDummy.y+2, oBall2);
4376 obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
4378 obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
4380 obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
4382 obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
4389 void startWinCutsceneVolcano () {
4390 global.hasParachute = false;
4392 writeln("VOLCANO HOLD 0: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4393 writeln("VOLCANO PICK 0: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4397 winCutsceneSwitchToNext = false;
4398 auto olddel = ImmediateDelete;
4399 ImmediateDelete = false;
4403 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
4405 blockWaterChecking = true;
4407 ImmediateDelete = olddel;
4408 CollectGarbage(true); // destroy delayed objects too
4410 spawnPlayerAt(2*16+8, 11*16+8);
4411 player.dir = MapEntity::Dir.Right;
4413 playerExited = false; // just in case
4414 playerExitDoor = none;
4422 winCutSceneTimer = -1;
4423 winCutScenePhase = 0;
4425 MakeMapTile(0, 0, 'oEnd2BG');
4426 realViewStart.x = 0;
4427 realViewStart.y = 0;
4436 player.dead = false;
4437 player.active = true;
4438 player.visible = false;
4439 player.removeBallAndChain(temp:true);
4440 player.stunned = false;
4441 player.status = MapObject::FALLING;
4442 if (player.holdItem) player.holdItem.visible = false;
4443 player.fltx = 320/2;
4447 writeln("VOLCANO HOLD 1: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4448 writeln("VOLCANO PICK 1: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4453 void startWinCutsceneWinFall () {
4454 global.hasParachute = false;
4456 writeln("FALL HOLD 0: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4457 writeln("FALL PICK 0: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4461 winCutsceneSwitchToNext = false;
4463 auto olddel = ImmediateDelete;
4464 ImmediateDelete = false;
4468 setMenuTilesVisible(false);
4470 //addBackgroundGfxDetails();
4473 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
4475 blockWaterChecking = true;
4479 ImmediateDelete = olddel;
4480 CollectGarbage(true); // destroy delayed objects too
4482 if (dumpGridStats) objGrid.dumpStats();
4484 playerExited = false; // just in case
4485 playerExitDoor = none;
4493 winCutSceneTimer = -1;
4494 winCutScenePhase = 0;
4496 player.dead = false;
4497 player.active = true;
4498 player.visible = false;
4499 player.removeBallAndChain(temp:true);
4500 player.stunned = false;
4501 player.status = MapObject::FALLING;
4502 if (player.holdItem) player.holdItem.visible = false;
4503 player.fltx = 320/2;
4506 winSceneDrawStatus = 0;
4513 writeln("FALL HOLD 1: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4514 writeln("FALL PICK 1: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4519 void setGameOver () {
4520 if (inWinCutscene) {
4521 player.visible = false;
4522 player.removeBallAndChain(temp:true);
4523 if (player.holdItem) player.holdItem.visible = false;
4526 if (inWinCutscene > 0) {
4529 winSceneDrawStatus = 8;
4534 MapTile findEndPlatTile () {
4535 return forEachTile(delegate bool (MapTile t) { return (t isa MapTileEndPlat); }, castClass:MapTileEndPlat);
4539 MapObject findBigTreasure () {
4540 return forEachObject(delegate bool (MapObject o) { return (o isa MapObjectBigTreasure); }, castClass:MapObjectBigTreasure);
4544 void setMenuTilesVisible (bool vis) {
4546 forEachTile(delegate bool (MapTile t) {
4547 if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4548 t.invisible = false;
4553 forEachTile(delegate bool (MapTile t) {
4554 if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4563 void setMenuTilesOnTop () {
4564 forEachTile(delegate bool (MapTile t) {
4565 if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
4573 void winCutscenePlayerControl (PlayerPawn plr) {
4574 auto payPress = isKeyPressed(GameConfig::Key.Pay);
4575 auto payRelease = isKeyReleased(GameConfig::Key.Pay);
4577 switch (winCutsceneSkip) {
4578 case 0: // nothing was pressed
4579 if (payPress) winCutsceneSkip = 1;
4581 case 1: // waiting for pay release
4582 if (payRelease) winCutsceneSkip = 2;
4584 case 2: // pay released, do skip
4589 // first winning room
4590 if (inWinCutscene == 1) {
4591 if (plr.ix < 448+8) {
4596 // waiting for chest to open
4597 if (winCutScenePhase == 0) {
4598 winCutSceneTimer = 120/2;
4599 winCutScenePhase = 1;
4604 if (winCutScenePhase == 1) {
4605 if (--winCutSceneTimer == 0) {
4606 winCutScenePhase = 2;
4607 winCutSceneTimer = 20;
4608 forEachObject(delegate bool (MapObject o) {
4609 if (o isa MapObjectBigChest) {
4610 o.setSprite(global.config.gameMode == GameConfig::GameMode.Vanilla ? 'sBigChestOpen' : 'sBigChestOpen2');
4611 auto treasure = MakeMapObject(o.ix, o.iy, 'oBigTreasure');
4615 o.playSound('sndClick');
4616 //!!!if (global.config.gameMode != GameConfig::GameMode.Vanilla) scrSprayGems(oBigChest.x+24, oBigChest.y+24);
4626 if (winCutScenePhase == 2) {
4627 if (--winCutSceneTimer == 0) {
4628 winCutScenePhase = 3;
4629 winCutSceneTimer = 50;
4635 if (winCutScenePhase == 3) {
4636 auto ep = findEndPlatTile();
4637 if (ep) MakeMapObject(ep.ix+global.randOther(0, 80), /*ep.iy*/192+32, 'oBurn');
4638 if (--winCutSceneTimer == 0) {
4639 winCutScenePhase = 4;
4640 winCutSceneTimer = 10;
4641 if (ep) MakeMapObject(ep.ix, ep.iy+30, 'oLavaSpray');
4647 // lava pump first accel
4648 if (winCutScenePhase == 4) {
4649 if (--winCutSceneTimer == 0) {
4650 forEachObject(delegate bool (MapObject o) {
4651 if (o isa MapObjectLavaSpray) o.yAcc = -0.1;
4657 // lava pump complete
4658 if (winCutScenePhase == 5) {
4659 if (--winCutSceneTimer == 0) {
4660 //if (oLavaSpray) oLavaSpray.yAcc = -0.1;
4661 startWinCutsceneVolcano();
4670 if (inWinCutscene == 2) {
4674 if (winCutScenePhase == 0) {
4675 winCutSceneTimer = 50;
4676 winCutScenePhase = 1;
4677 winVolcanoTimer = 10;
4681 if (winVolcanoTimer > 0) {
4682 if (--winVolcanoTimer == 0) {
4683 MakeMapObject(224+global.randOther(0,48), 144+global.randOther(0,8), 'oVolcanoFlame');
4684 winVolcanoTimer = global.randOther(10, 20);
4689 if (winCutScenePhase == 1) {
4690 if (--winCutSceneTimer == 0) {
4691 winCutSceneTimer = 30;
4692 winCutScenePhase = 2;
4693 auto sil = MakeMapObject(240, 132, 'oPlayerSil');
4701 if (winCutScenePhase == 2) {
4702 if (--winCutSceneTimer == 0) {
4703 winCutScenePhase = 3;
4704 auto sil = MakeMapObject(240, 132, 'oTreasureSil');
4714 // winning camel room
4715 if (inWinCutscene == 3) {
4716 //if (!player.holdItem) writeln("SCENE 3: LOST ITEM!");
4718 if (!plr.visible) plr.flty = -32;
4721 if (winCutScenePhase == 0) {
4722 winCutSceneTimer = 50;
4723 winCutScenePhase = 1;
4728 if (winCutScenePhase == 1) {
4729 if (--winCutSceneTimer == 0) {
4730 winCutSceneTimer = 50;
4731 winCutScenePhase = 2;
4732 plr.playSound('sndPFall');
4735 writeln("MUST BE CHAINED: ", plr.mustBeChained);
4736 if (plr.mustBeChained) {
4737 plr.removeBallAndChain(temp:true);
4738 plr.spawnBallAndChain();
4741 writeln("HOLD: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
4742 writeln("PICK: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
4744 if (!player.holdItem && player.pickedItem) player.scrSwitchToPocketItem(forceIfEmpty:false);
4745 if (player.holdItem) {
4746 player.holdItem.visible = true;
4747 player.holdItem.canLiveOutsideOfLevel = true;
4748 writeln("HOLD ITEM: '", GetClassName(player.holdItem.Class), "'");
4750 plr.status == MapObject::FALLING;
4751 global.plife += 99; // just in case
4756 if (winCutScenePhase == 2) {
4757 auto ball = plr.getMyBall();
4758 if (ball && plr.holdItem != ball) {
4759 ball.teleportTo(plr.fltx, plr.flty+8);
4763 if (plr.status == MapObject::STUNNED || plr.stunned) {
4767 auto treasure = MakeMapObject(144+16+8, -32, 'oBigTreasure');
4768 if (treasure) treasure.depth = 1;
4769 winCutScenePhase = 3;
4771 plr.playSound('sndTFall');
4776 if (winCutScenePhase == 3) {
4777 if (plr.status != MapObject::STUNNED && !plr.stunned) {
4778 auto bt = findBigTreasure();
4782 //plr.status = MapObject::JUMPING;
4784 plr.kJumpPressed = true;
4785 winCutScenePhase = 4;
4786 winCutSceneTimer = 50;
4793 if (winCutScenePhase == 4) {
4794 if (--winCutSceneTimer == 0) {
4795 setMenuTilesVisible(true);
4796 winCutScenePhase = 5;
4797 winSceneDrawStatus = 1;
4798 global.playMusic('musVictory', loop:false);
4799 winCutSceneTimer = 50;
4804 if (winCutScenePhase == 5) {
4805 if (winSceneDrawStatus == 3) {
4806 int money = stats.money;
4807 if (winMoneyCount < money) {
4808 if (money-winMoneyCount > 1000) {
4809 winMoneyCount += 1000;
4810 } else if (money-winMoneyCount > 100) {
4811 winMoneyCount += 100;
4812 } else if (money-winMoneyCount > 10) {
4813 winMoneyCount += 10;
4818 if (winMoneyCount >= money) {
4819 winMoneyCount = money;
4820 ++winSceneDrawStatus;
4825 if (winSceneDrawStatus == 7) {
4828 if (winFadeLevel >= 255) {
4829 ++winSceneDrawStatus;
4830 winCutSceneTimer = 30*30;
4835 if (winSceneDrawStatus == 8) {
4836 if (--winCutSceneTimer == 0) {
4842 if (--winCutSceneTimer == 0) {
4843 ++winSceneDrawStatus;
4844 winCutSceneTimer = 50;
4853 // ////////////////////////////////////////////////////////////////////////// //
4854 void renderWinCutsceneOverlay () {
4855 if (inWinCutscene == 3) {
4856 if (winSceneDrawStatus > 0) {
4857 Video.color = 0xff_ff_ff;
4858 sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
4859 //draw_set_color(txtCol);
4860 drawTextAt(64, 32, "YOU MADE IT!");
4862 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4863 if (global.config.gameMode == GameConfig::GameMode.Vanilla) {
4864 Video.color = 0xff_ff_00; //draw_set_color(c_yellow);
4865 drawTextAt(64, 48, "Classic Mode done!");
4867 Video.color = 0x00_80_80; //draw_set_color(c_teal);
4868 if (global.config.bizarrePlus) drawTextAt(64, 48, "Bizarre Mode Plus done!");
4869 else drawTextAt(64, 48, "Bizarre Mode done!");
4870 //draw_set_color(c_white);
4872 if (!global.usedShortcut) {
4873 Video.color = 0xc0_c0_c0; //draw_set_color(c_silver);
4874 drawTextAt(64, 56, "No shortcuts used!");
4875 //draw_set_color(c_yellow);
4879 if (winSceneDrawStatus > 1) {
4880 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4881 //draw_set_color(txtCol);
4882 Video.color = 0xff_ff_ff;
4883 drawTextAt(64, 64, "FINAL SCORE:");
4886 if (winSceneDrawStatus > 2) {
4887 sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
4888 Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4889 drawTextAt(64, 72, va("$%d", winMoneyCount));
4892 if (winSceneDrawStatus > 4) {
4893 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4894 Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4895 drawTextAt(64, 96, va("Time: %s", time2str(winTime/30)));
4897 draw_set_color(c_white);
4898 if (s < 10) draw_text(96+24, 96, string(m) + ":0" + string(s));
4899 else draw_text(96+24, 96, string(m) + ":" + string(s));
4903 if (winSceneDrawStatus > 5) {
4904 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4905 Video.color = 0xff_ff_ff; //draw_set_color(txtCol);
4906 drawTextAt(64, 96+8, "Kills: ");
4907 Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4908 drawTextAt(96+24, 96+8, va("%s", stats.countKills()));
4911 if (winSceneDrawStatus > 6) {
4912 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4913 Video.color = 0xff_ff_ff; //draw_set_color(txtCol);
4914 drawTextAt(64, 96+16, "Saves: ");
4915 Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4916 drawTextAt(96+24, 96+16, va("%s", stats.damselsSaved));
4920 Video.color = (255-clamp(winFadeLevel, 0, 255))<<24;
4921 Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
4924 if (winSceneDrawStatus == 8) {
4925 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4926 Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4928 if (global.config.gameMode == GameConfig::GameMode.Vanilla) {
4929 Video.color = 0xff_ff_00; //draw_set_color(c_yellow);
4930 lastString = "YOU SHALL BE REMEMBERED AS A HERO.";
4932 Video.color = 0x00_ff_ff;
4933 if (global.config.bizarrePlus) lastString = "ANOTHER LEGENDARY ADVENTURE!";
4934 else lastString = "YOUR DISCOVERIES WILL BE CELEBRATED!";
4936 auto strLen = lastString.length*8;
4938 n = trunc(ceil(n/2.0));
4939 drawTextAt(n, 116, lastString);
4945 // ////////////////////////////////////////////////////////////////////////// //
4946 #include "roomTitle.vc"
4947 #include "roomTrans1.vc"
4948 #include "roomTrans2.vc"
4949 #include "roomTrans3.vc"
4950 #include "roomTrans4.vc"
4951 #include "roomOlmec.vc"
4952 #include "roomEnd.vc"
4953 #include "roomTutorial.vc"
4954 #include "roomScores.vc"
4955 #include "roomStars.vc"
4956 #include "roomSun.vc"
4957 #include "roomMoon.vc"
4960 // ////////////////////////////////////////////////////////////////////////// //
4961 #include "packages/Generator/loadRoomGens.vc"
4962 #include "packages/Generator/loadEntityGens.vc"