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 const float FrameTime = 1.0f/30.0f;
23 const int dumpGridStats = false;
30 //enum NormalTilesWidth = LevelGen::LevelWidth*RoomGen::Width+2;
31 //enum NormalTilesHeight = LevelGen::LevelHeight*RoomGen::Height+2;
33 enum MaxTilesWidth = 64;
34 enum MaxTilesHeight = 64;
37 transient GameStats stats;
38 transient SpriteStore sprStore;
39 transient BackTileStore bgtileStore;
40 transient BackTileImage levBGImg;
43 transient name lastMusicName;
44 //RoomGen[LevelGen::LevelWidth, LevelGen::LevelHeight] rooms; // moved to levelgen
46 transient float accumTime;
47 transient bool gamePaused = false;
48 transient bool checkWater;
49 transient int damselSaved;
53 transient int collectCounter;
54 transient int levelMoneyStart;
56 // all movable (thinkable) map objects
57 EntityGrid objGrid; // monsters and items
59 MapTile[MaxTilesWidth, MaxTilesHeight] tiles;
60 MapBackTile backtiles;
61 EntityGrid miscTileGrid; // moveables and ropes
62 bool blockWaterChecking;
63 array!MapTile lavatiles; // they need to think/animate, but i don't want to move 'em out of `tiles`
65 array!MapObject ballObjects; // list of all ball objects, for speed
69 LevelGen::RType[MaxTilesWidth, MaxTilesHeight] roomType;
77 LevelKind levelKind = LevelKind.Normal;
79 array!MapTile allEnters;
80 array!MapTile allExits;
83 int startRoomX, startRoomY;
84 int endRoomX, endRoomY;
87 transient bool playerExited;
88 transient bool disablePlayerThink = false;
89 transient int maxPlayingTime; // in seconds
93 bool ghostSpawned; // to speed up some checks
96 // FPS, i.e. incremented by 30 in one second
97 int time; // in frames
100 // screen shake variables
105 // set this before calling `fixCamera()`
106 // dimensions should be real, not scaled up/down
107 transient int viewWidth, viewHeight;
108 // room bounds, not scaled
109 IVec2D viewMin, viewMax;
111 // for Olmec level cinematics
112 IVec2D cameraSlideToDest;
113 IVec2D cameraSlideToCurr;
114 IVec2D cameraSlideToSpeed; // !0: slide
115 int cameraSlideToPlayer;
116 // `fixCamera()` will set the following
117 // coordinates will be real too (with scale applied)
118 // shake is not applied
119 transient IVec2D viewStart; // with `player.viewOffset`
120 private transient IVec2D realViewStart; // without `player.viewOffset`
122 final void cameraSlideToPoint (int dx, int dy, int speedx, int speedy) {
123 cameraSlideToPlayer = 0;
124 cameraSlideToDest.x = dx;
125 cameraSlideToDest.y = dy;
126 cameraSlideToSpeed.x = abs(speedx);
127 cameraSlideToSpeed.y = abs(speedy);
128 cameraSlideToCurr.x = cameraCurrX;
129 cameraSlideToCurr.y = cameraCurrY;
132 final void cameraReturnToPlayer () {
133 if (!cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y)) {
134 cameraSlideToCurr.x = cameraCurrX;
135 cameraSlideToCurr.y = cameraCurrY;
136 if (cameraSlideToSpeed.x && abs(cameraSlideToSpeed.x) < 8) cameraSlideToSpeed.x = 8;
137 if (cameraSlideToSpeed.y && abs(cameraSlideToSpeed.y) < 8) cameraSlideToSpeed.y = 8;
138 cameraSlideToPlayer = 1;
142 // if `frameSkip` is `true`, there are more frames waiting
143 // (i.e. you may skip rendering and such)
144 transient void delegate (bool frameSkip) onBeforeFrame;
145 transient void delegate (bool frameSkip) onAfterFrame;
147 transient void delegate () onLevelExitedCB;
149 // this will be called in-between frames, and
150 // `frameTime` is [0..1)
151 transient void delegate (float frameTime) onInterFrame;
153 final int bizRoomStyle { get { return (lg ? lg.bizRoomStyle : 0); } }
156 final bool isNormalLevel () { return (levelKind == LevelKind.Normal); }
159 // ////////////////////////////////////////////////////////////////////////// //
161 void addDeath (name aname) { stats.addDeath(aname); }
162 void addKill (name aname) { stats.addKill(aname); }
163 void addCollect (name aname, optional int amount) { stats.addCollect(aname, amount!optional); }
165 void addDamselSaved () { stats.addDamselSaved(); }
166 void addIdolStolen () { stats.addIdolStolen(); }
167 void addIdolConverted () { stats.addIdolConverted(); }
168 void addCrystalIdolStolen () { stats.addCrystalIdolStolen(); }
169 void addCrystalIdolConverted () { stats.addCrystalIdolConverted(); }
170 void addGhostSummoned () { stats.addGhostSummoned(); }
173 // ////////////////////////////////////////////////////////////////////////// //
174 static final string val2dig (int n) {
175 return (n < 10 ? va("0%d", n) : va("%d", n));
179 static final string time2str (int time) {
180 int secs = time%60; time /= 60;
181 int mins = time%60; time /= 60;
182 int hours = time%24; time /= 24;
184 if (days) return va("%d DAYS, %d:%s:%s", days, hours, val2dig(mins), val2dig(secs));
185 if (hours) return va("%d:%s:%s", hours, val2dig(mins), val2dig(secs));
186 return va("%s:%s", val2dig(mins), val2dig(secs));
190 // ////////////////////////////////////////////////////////////////////////// //
191 final int tilesWidth () { return lg.levelRoomWidth*RoomGen::Width+2; }
192 final int tilesHeight () { return (lg.finalBossLevel ? 55 : lg.levelRoomHeight*RoomGen::Height+2); }
195 // ////////////////////////////////////////////////////////////////////////// //
196 // this won't generate a level yet
197 void restartGame () {
200 auto hi = player.holdItem;
201 player.holdItem = none;
202 if (hi) hi.instanceRemove();
203 hi = player.pickedItem;
204 player.pickedItem = none;
205 if (hi) hi.instanceRemove();
208 stats.clearGameTotals();
209 if (global.startMoney > 0) stats.setMoneyCheat();
210 stats.setMoney(global.startMoney);
211 //writeln("level=", global.currLevel, "; lt=", global.levelType);
215 // complement function to `restart game`
216 void generateNormalLevel () {
218 centerViewAtPlayer();
222 // ////////////////////////////////////////////////////////////////////////// //
223 // generate angry shopkeeper at exit if murderer or thief
224 void generateAngryShopkeepers () {
225 if (global.murderer || global.thiefLevel > 0) {
226 foreach (MapTile e; allExits) {
227 auto obj = MonsterShopkeeper(MakeMapObject(e.ix, e.iy, 'oShopkeeper'));
229 obj.style = 'Bounty Hunter';
230 obj.status = MapObject::PATROL;
237 // ////////////////////////////////////////////////////////////////////////// //
238 final void resetRoomBounds () {
241 viewMax.x = tilesWidth*16;
242 viewMax.y = tilesHeight*16;
243 // Great Lake is bottomless (nope)
244 //if (global.lake) viewMax.y -= 16;
245 //writeln("ROOM BOUNDS: (", viewMax.x, "x", viewMax.y, ")");
249 final void setRoomBounds (int x0, int y0, int x1, int y1) {
257 // ////////////////////////////////////////////////////////////////////////// //
260 float timeout; // seconds
261 float starttime; // for active
262 bool active; // true: timeout is `GetTickCount()` dismissing time
265 array!OSDMessage msglist; // [0]: current one
268 private final void osdCheckTimeouts () {
269 auto stt = GetTickCount();
270 while (msglist.length) {
271 if (!msglist[0].active) {
272 msglist[0].active = true;
273 msglist[0].starttime = stt;
275 if (msglist[0].starttime+msglist[0].timeout >= stt) break;
281 final bool osdHasMessage () {
283 return (msglist.length > 0);
287 final string osdGetMessage (out float timeLeft, out float timeStart) {
289 if (msglist.length == 0) { timeLeft = 0; return ""; }
290 auto stt = GetTickCount();
291 timeStart = msglist[0].starttime;
292 timeLeft = fmax(0, msglist[0].starttime+msglist[0].timeout-stt);
293 return msglist[0].msg;
297 final void osdClear () {
298 msglist.length -= msglist.length;
302 final void osdMessage (string msg, optional float timeout, optional bool urgent) {
304 if (!specified_timeout) timeout = 3.33;
305 // special message for shops
306 if (timeout == -666) {
308 if (msglist.length && msglist[0].msg == msg) return;
309 if (msglist.length == 0 || msglist[0].msg != msg) {
312 msglist[0].msg = msg;
314 msglist[0].active = false;
315 msglist[0].timeout = 3.33;
319 if (timeout < 0.1) return;
320 timeout = fmax(1.0, timeout);
321 //writeln("OSD: ", msg);
322 // find existing one, and bring it to the top
324 for (; oldidx < msglist.length; ++oldidx) {
325 if (msglist[oldidx].msg == msg) break; // i found her!
328 if (oldidx < msglist.length) {
329 // yeah, move duplicate to the top
330 msglist[oldidx].timeout = fmax(timeout, msglist[oldidx].timeout);
331 msglist[oldidx].active = false;
332 if (urgent && oldidx != 0) {
333 timeout = msglist[oldidx].timeout;
334 msglist.remove(oldidx);
336 msglist[0].msg = msg;
337 msglist[0].timeout = timeout;
338 msglist[0].active = false;
342 msglist[0].msg = msg;
343 msglist[0].timeout = timeout;
344 msglist[0].active = false;
348 msglist[$-1].msg = msg;
349 msglist[$-1].timeout = timeout;
350 msglist[$-1].active = false;
356 // ////////////////////////////////////////////////////////////////////////// //
357 void setup (GameGlobal aGlobal, SpriteStore aSprStore, BackTileStore aBGTileStore) {
359 sprStore = aSprStore;
360 bgtileStore = aBGTileStore;
362 lg = SpawnObject(LevelGen);
366 miscTileGrid = SpawnObject(EntityGrid);
367 miscTileGrid.setup(0, 0, MaxTilesWidth*16, MaxTilesHeight*16, MapTile);
368 //miscTileGrid.ownObjects = true;
370 objGrid = SpawnObject(EntityGrid);
371 objGrid.setup(0, 0, MaxTilesWidth*16, MaxTilesHeight*16, MapObject);
375 // stores should be set
378 levBGImg = bgtileStore[levBGImgName];
379 foreach (int y; 0..MaxTilesHeight) {
380 foreach (int x; 0..MaxTilesWidth) {
381 if (tiles[x, y]) tiles[x, y].onLoaded();
384 foreach (MapEntity o; miscTileGrid.allObjects()) o.onLoaded();
385 foreach (MapEntity o; objGrid.allObjects()) o.onLoaded();
386 for (MapBackTile bt = backtiles; bt; bt = bt.next) bt.onLoaded();
387 if (player) player.onLoaded();
389 if (msglist.length) {
390 msglist[0].active = false;
391 msglist[0].timeout = 0.200;
397 // ////////////////////////////////////////////////////////////////////////// //
398 void pickedSpectacles () {
399 foreach (int y; 0..tilesHeight) {
400 foreach (int x; 0..tilesWidth) {
401 MapTile t = tiles[x, y];
402 if (t && t.isInstanceAlive) t.onGotSpectacles();
405 foreach (MapTile t; miscTileGrid.allObjects()) {
406 if (t.isInstanceAlive) t.onGotSpectacles();
411 // ////////////////////////////////////////////////////////////////////////// //
412 #include "rgentile.vc"
413 #include "rgenobj.vc"
416 void onLevelExited () {
417 if (isNormalLevel()) stats.maxLevelComplete = max(stats.maxLevelComplete, global.currLevel);
418 if (onLevelExitedCB) onLevelExitedCB();
419 if (levelKind == LevelKind.Transition) {
420 if (global.thiefLevel > 0) global.thiefLevel -= 1;
421 //orig dbg:if (global.currLevel == 1) global.currLevel += firstLevelSkip; else global.currLevel += levelSkip;
422 global.currLevel += 1;
425 if (lg.finalBossLevel) {
428 generateTransitionLevel();
431 centerViewAtPlayer();
435 void onOlmecDead (MapObject o) {
436 //class EnemyOlmec['oOlmec'] : MapEnemy;
437 //level.onOlmecDead(self);
438 writeln("*** OLMEC IS DEAD!");
439 foreach (MapTile t; allExits) {
442 auto st = checkTileAtPoint(t.ix+8, t.iy+16);
444 st = MakeMapTile(t.ix/16, t.iy/16+1, 'oTemple');
447 st.invincible = true;
453 void generateLevelMessages () {
454 if (global.darkLevel) {
455 if (global.hasCrown) osdMessage("THE HEDJET SHINES BRIGHTLY.");
456 else if (global.config.scumDarkness < 2) osdMessage("I CAN'T SEE A THING!");
458 else global.message = "";
459 if (global.hasCrown) global.message2 = "";
460 else if (global.scumDarkness < 2) global.message2 = "I'D BETTER USE THESE FLARES!";
461 else global.message2 = "";
462 global.messageTimer = 200;
467 if (global.blackMarket) osdMessage("WELCOME TO THE BLACK MARKET!");
469 if (global.cemetary) osdMessage("THE DEAD ARE RESTLESS!");
470 if (global.lake) osdMessage("I CAN HEAR RUSHING WATER...");
472 if (global.snakePit) osdMessage("I HEAR SNAKES... I HATE SNAKES!");
473 if (global.yetiLair) osdMessage("IT SMELLS LIKE WET FUR IN HERE!");
474 if (global.alienCraft) osdMessage("THERE'S A PSYCHIC PRESENCE HERE!");
475 if (global.cityOfGold) {
476 if (true /*hasOlmec()*/) osdMessage("IT'S THE LEGENDARY CITY OF GOLD!");
479 if (global.sacrificePit) osdMessage("I CAN HEAR PRAYERS TO KALI!");
483 final MapObject debugSpawnObjectWithClass (class!MapObject oclass, optional bool playerDir) {
484 if (!oclass) return none;
486 bool canLeft = !isSolidAtPoint(player.ix-8, player.iy);
487 bool canRight = !isSolidAtPoint(player.ix+16, player.iy);
488 if (!canLeft && !canRight) return none;
489 if (canLeft && canRight) {
491 dx = (player.dir == MapObject::Dir.Left ? -16 : 16);
496 dx = (canLeft ? -16 : 16);
498 auto obj = SpawnMapObjectWithClass(oclass);
499 if (obj isa MapEnemy) { dx -= 8; dy -= 8; }
500 if (obj) PutSpawnedMapObject(player.ix+dx, player.iy+dy, obj);
505 final MapObject debugSpawnObject (name aname) {
506 if (!aname) return none;
507 return debugSpawnObjectWithClass(findGameObjectClassByName(aname));
511 // `global.currLevel` is the new level
512 void generateTransitionLevel () {
516 global.setMusicPitch(1.0);
517 switch (global.config.transitionMusicMode) {
518 case GameConfig::MusicMode.Silent: global.stopMusic(); break;
519 case GameConfig::MusicMode.Restart: global.restartMusic(); break;
520 case GameConfig::MusicMode.DontTouch: break;
523 levelKind = LevelKind.Transition;
525 auto olddel = ImmediateDelete;
526 ImmediateDelete = false;
530 if (global.currLevel < 4) createTrans1Room();
531 else if (global.currLevel == 4) createTrans1xRoom();
532 else if (global.currLevel < 8) createTrans2Room();
533 else if (global.currLevel == 8) createTrans2xRoom();
534 else if (global.currLevel < 12) createTrans3Room();
535 else if (global.currLevel == 12) createTrans3xRoom();
536 else if (global.currLevel < 16) createTrans4Room();
537 else if (global.currLevel == 16) createTrans4Room();
538 else createTrans1Room(); //???
541 addBackgroundGfxDetails();
542 levBGImgName = 'bgCave';
543 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
545 blockWaterChecking = true;
549 if (damselSaved > 0) {
550 MakeMapObject(176+8, 176+8, 'oDamselKiss');
551 global.plife += damselSaved; // if player skipped transition cutscene
557 ImmediateDelete = olddel;
558 CollectGarbage(true); // destroy delayed objects too
561 miscTileGrid.dumpStats();
565 playerExited = false; // just in case
570 //global.playMusic(lg.musicName);
574 void generateLevel () {
575 global.setMusicPitch(1.0);
576 stats.clearLevelTotals();
578 levelKind = LevelKind.Normal;
585 writeln("tw:", tilesWidth, "; th:", tilesHeight);
587 auto olddel = ImmediateDelete;
588 ImmediateDelete = false;
592 if (lg.finalBossLevel) {
593 blockWaterChecking = true;
597 // if transition cutscene was skipped...
598 if (damselSaved > 0) global.plife += damselSaved; // if player skipped transition cutscene
602 startRoomX = lg.startRoomX;
603 startRoomY = lg.startRoomY;
604 endRoomX = lg.endRoomX;
605 endRoomY = lg.endRoomY;
606 addBackgroundGfxDetails();
607 foreach (int y; 0..tilesHeight) {
608 foreach (int x; 0..tilesWidth) {
614 levBGImgName = lg.bgImgName;
615 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
617 if (global.allowAngryShopkeepers) generateAngryShopkeepers();
619 lg.generateEntities();
621 // add box of flares to dark level
622 if (global.darkLevel && allEnters.length) {
623 auto enter = allEnters[0];
624 int x = enter.ix, y = enter.iy;
625 if (!isSolidAtPoint(x-16, y)) MakeMapObject(x-16+8, y+8, 'oFlareCrate');
626 else if (!isSolidAtPoint(x+16, y)) MakeMapObject(x+16+8, y+8, 'oFlareCrate');
627 else MakeMapObject(x+8, y+8, 'oFlareCrate');
630 //scrGenerateEntities();
631 //foreach (; 0..2) scrGenerateEntities();
633 writeln(countObjects, " alive objects inserted");
634 writeln(countBackTiles, " background tiles inserted");
636 if (!player) FatalError("player pawn is not spawned");
638 if (lg.finalBossLevel) {
639 blockWaterChecking = true;
641 blockWaterChecking = false;
648 ImmediateDelete = olddel;
649 CollectGarbage(true); // destroy delayed objects too
652 miscTileGrid.dumpStats();
656 playerExited = false; // just in case
658 levelMoneyStart = stats.money;
661 generateLevelMessages();
666 if (lastMusicName != lg.musicName) {
667 global.playMusic(lg.musicName);
668 //writeln("!<", lastMusicName, ">:<", lg.musicName, ">");
670 //writeln("MM: ", global.config.nextLevelMusicMode);
671 switch (global.config.nextLevelMusicMode) {
672 case GameConfig::MusicMode.Silent: global.stopMusic(); break; // the thing that should not be
673 case GameConfig::MusicMode.Restart: global.restartMusic(); break;
674 case GameConfig::MusicMode.DontTouch:
675 if (global.config.transitionMusicMode == GameConfig::MusicMode.Silent) {
676 global.playMusic(lg.musicName);
681 lastMusicName = lg.musicName;
682 //global.playMusic(lg.musicName);
688 // ////////////////////////////////////////////////////////////////////////// //
689 int currKeys, nextKeys;
690 int pressedKeysQ, releasedKeysQ;
691 int keysPressed, keysReleased = -1;
694 struct SavedKeyState {
695 int currKeys, nextKeys;
696 int pressedKeysQ, releasedKeysQ;
697 int keysPressed, keysReleased;
699 int roomSeed, otherSeed;
703 // for saving/replaying
704 final void keysSaveState (out SavedKeyState ks) {
705 ks.currKeys = currKeys;
706 ks.nextKeys = nextKeys;
707 ks.pressedKeysQ = pressedKeysQ;
708 ks.releasedKeysQ = releasedKeysQ;
709 ks.keysPressed = keysPressed;
710 ks.keysReleased = keysReleased;
713 // for saving/replaying
714 final void keysRestoreState (const ref SavedKeyState ks) {
715 currKeys = ks.currKeys;
716 nextKeys = ks.nextKeys;
717 pressedKeysQ = ks.pressedKeysQ;
718 releasedKeysQ = ks.releasedKeysQ;
719 keysPressed = ks.keysPressed;
720 keysReleased = ks.keysReleased;
724 final void keysNextFrame () {
729 final void clearKeys () {
739 final void onKey (int code, bool down) {
744 if (keysReleased&code) {
746 keysReleased &= ~code;
747 pressedKeysQ |= code;
751 if (keysPressed&code) {
752 keysReleased |= code;
753 keysPressed &= ~code;
754 releasedKeysQ |= code;
759 final bool isKeyDown (int code) {
760 return !!(currKeys&code);
763 final bool isKeyPressed (int code) {
764 bool res = !!(pressedKeysQ&code);
765 pressedKeysQ &= ~code;
769 final bool isKeyReleased (int code) {
770 bool res = !!(releasedKeysQ&code);
771 releasedKeysQ &= ~code;
776 final void clearKeysPressRelease () {
777 keysPressed = default.keysPressed;
778 keysReleased = default.keysReleased;
779 pressedKeysQ = default.pressedKeysQ;
780 releasedKeysQ = default.releasedKeysQ;
786 // ////////////////////////////////////////////////////////////////////////// //
787 final void registerEnter (MapTile t) {
794 final void registerExit (MapTile t) {
801 final bool isYAtEntranceRow (int py) {
803 foreach (MapTile t; allEnters) if (t.iy == py) return true;
808 final int calcNearestEnterDist (int px, int py) {
809 if (allEnters.length == 0) return int.max;
810 int curdistsq = int.max;
811 foreach (MapTile t; allEnters) {
812 int xc = px-t.xCenter, yc = py-t.yCenter;
813 int distsq = xc*xc+yc*yc;
814 if (distsq < curdistsq) curdistsq = distsq;
816 return round(sqrt(curdistsq));
820 final int calcNearestExitDist (int px, int py) {
821 if (allExits.length == 0) return int.max;
822 int curdistsq = int.max;
823 foreach (MapTile t; allExits) {
824 int xc = px-t.xCenter, yc = py-t.yCenter;
825 int distsq = xc*xc+yc*yc;
826 if (distsq < curdistsq) curdistsq = distsq;
828 return round(sqrt(curdistsq));
832 // ////////////////////////////////////////////////////////////////////////// //
833 final void clearForTransition () {
834 auto olddel = ImmediateDelete;
835 ImmediateDelete = false;
838 ImmediateDelete = olddel;
839 CollectGarbage(true); // destroy delayed objects too
843 final void clearTiles () {
846 allEnters.length -= allEnters.length; // don't deallocate
847 allExits.length -= allExits.length; // don't deallocate
848 lavatiles.length -= lavatiles.length;
849 foreach (ref auto tile; tiles) delete tile;
850 if (dumpGridStats) { if (miscTileGrid.getFirstObject()) miscTileGrid.dumpStats(); }
851 miscTileGrid.removeAllObjects(true); // and destroy
853 MapBackTile t = backtiles;
861 // ////////////////////////////////////////////////////////////////////////// //
862 final int countObjects () {
863 return objGrid.countObjects();
866 final int countBackTiles () {
868 for (MapBackTile bt = backtiles; bt; bt = bt.next) ++res;
872 final void clearObjects () {
873 // don't kill objects player is holding
875 if (player.pickedItem && player.pickedItem.grid) {
876 player.pickedItem.grid.remove(player.pickedItem.gridId);
877 writeln("secured pocket item '", GetClassName(player.pickedItem.Class), "'");
878 //player.pickedItem.grid = none;
880 if (player.holdItem && player.holdItem.grid) {
881 player.holdItem.grid.remove(player.holdItem.gridId);
882 writeln("secured held item '", GetClassName(player.holdItem.Class), "'");
883 //player.holdItem.grid = none;
887 int count = objGrid.countObjects();
888 if (dumpGridStats) { if (objGrid.getFirstObject()) objGrid.dumpStats(); }
889 objGrid.removeAllObjects(true); // and destroy
890 if (count > 0) writeln(count, " objects destroyed");
891 ballObjects.length = 0;
892 lastUsedObjectId = 0;
896 final void insertObject (MapObject o) {
898 if (o.grid) FatalError("cannot put object into level twice");
899 o.objId = ++lastUsedObjectId;
901 // ball from ball-and-chain
902 if (o.objType == 'oBall') {
904 foreach (MapObject bo; ballObjects) if (bo == o) { found = true; break; }
905 if (!found) ballObjects[$] = o;
912 final void spawnPlayerAt (int x, int y) {
913 // if we have no player, spawn new one
914 // otherwise this just a level transition, so simply reposition him
916 // don't add player to object list, as it has very separate processing anyway
917 player = SpawnObject(PlayerPawn);
918 player.global = global;
920 if (!player.initialize()) {
922 FatalError("something is wrong with player initialization");
928 player.saveInterpData();
930 playerExited = false;
931 if (global.config.startWithKapala) global.hasKapala = true;
932 centerViewAtPlayer();
933 // reinsert player items into grid
934 if (player.pickedItem) objGrid.insert(player.pickedItem);
935 if (player.holdItem) objGrid.insert(player.holdItem);
936 //writeln("player spawned; active=", player.active);
937 player.scrSwitchToPocketItem(forceIfEmpty:false);
941 final void teleportPlayerTo (int x, int y) {
945 player.saveInterpData();
950 final void resurrectPlayer () {
951 if (player) player.resurrect();
952 playerExited = false;
956 // ////////////////////////////////////////////////////////////////////////// //
957 final void scrShake (int duration) {
958 if (shakeLeft == 0) {
964 shakeLeft = max(shakeLeft, duration);
969 // ////////////////////////////////////////////////////////////////////////// //
972 ItemStolen, // including damsel, lol
979 // make the nearest shopkeeper angry. RAWR!
980 void scrShopkeeperAnger (SCAnger reason, optional int maxdist, optional MapEntity offender) {
981 if (!offender) offender = player;
982 auto shp = MonsterShopkeeper(findNearestEnemy(offender.ix, offender.iy, delegate bool (MapEnemy o) {
983 auto sc = MonsterShopkeeper(o);
984 if (!sc) return false;
985 if (sc.dead || sc.angered) return false;
990 if (specified_maxdist && offender.directionToEntityCenter(shp) > maxdist) return;
991 if (!shp.dead && !shp.angered) {
992 shp.status = MapObject::ATTACK;
994 if (global.murderer) msg = "YOU'LL PAY FOR YOUR CRIMES!";
995 else if (reason == SCAnger.ItemStolen) msg = "COME BACK HERE, THIEF!";
996 else if (reason == SCAnger.TileDestroyed) msg = "DIE, YOU VANDAL!";
997 else if (reason == SCAnger.BombDropped) msg = "TERRORIST!";
998 else if (reason == SCAnger.DamselWhipped) msg = "HEY, ONLY I CAN DO THAT!";
999 else if (reason == SCAnger.CrapsCheated) msg = "DIE, CHEATER!";
1000 else msg = "NOW I'M REALLY STEAMED!";
1001 if (msg) osdMessage(msg, -666);
1002 global.thiefLevel += (global.thiefLevel > 0 ? 3 : 2);
1008 final MapObject findCrapsPrize () {
1009 foreach (MapObject o; objGrid.allObjects()) {
1010 if (o.spectral || !o.isInstanceAlive) continue;
1011 if (o.inDiceHouse) return o;
1017 // ////////////////////////////////////////////////////////////////////////// //
1018 // 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.
1019 // note: idols moved by monkeys will have false `stolenIdol`
1020 void scrTriggerIdolAltar (bool stolenIdol) {
1021 ObjTikiCurse res = none;
1022 int curdistsq = int.max;
1023 int px = player.xCenter, py = player.yCenter;
1024 foreach (MapObject o; objGrid.allObjects()) {
1025 auto tcr = ObjTikiCurse(o);
1026 if (!tcr || !tcr.isInstanceAlive) continue;
1027 if (tcr.activated) continue;
1028 int xc = px-tcr.xCenter, yc = py-tcr.yCenter;
1029 int distsq = xc*xc+yc*yc;
1030 if (distsq < curdistsq) {
1035 if (res) res.activate(stolenIdol);
1039 // ////////////////////////////////////////////////////////////////////////// //
1040 void setupGhostTime () {
1041 musicFadeTimer = -1;
1042 ghostSpawned = false;
1044 if (inWinCutscene || !isNormalLevel() || lg.finalBossLevel) {
1046 global.setMusicPitch(1.0);
1050 if (global.config.scumGhost < 0) {
1053 osdMessage("A GHASTLY VOICE CALLS YOUR NAME!\nLET'S GET OUT OF HERE!", 6.66, urgent:true);
1057 if (global.config.scumGhost == 0) {
1063 // randomizes time until ghost appears once time limit is reached
1064 // global.ghostRandom (true/false) if extra time is randomized - loaded from yasm.cfg. if false, use 30 second default
1065 // ghostTimeLeft (time in seconds * 1000) for currently generated level
1067 if (global.config.ghostRandom) {
1068 auto tMin = global.randOther(10, 30); // min(global.plife*10, 30);
1069 auto tMax = max(tMin+global.randOther(10, 30), min(global.config.scumGhost, 300)); // 5 minutes max
1070 auto tTime = global.randOther(tMin, tMax);
1071 if (tTime <= 0) tTime = round(tMax/2.0);
1072 ghostTimeLeft = tTime;
1074 ghostTimeLeft = global.config.scumGhost; // 5 minutes max
1077 ghostTimeLeft += max(0, global.config.ghostExtraTime);
1079 ghostTimeLeft *= 30; // seconds -> frames
1080 //global.ghostShowTime
1084 void spawnGhost () {
1086 ghostSpawned = true;
1088 int vwdt = (viewMax.x-viewMin.x);
1089 int vhgt = (viewMax.y-viewMin.y);
1093 if (player.ix < viewMin.x+vwdt/2) {
1094 // player is in the left side
1095 gx = viewMin.x+vwdt/2+vwdt/4;
1097 // player is in the right side
1098 gx = viewMin.x+vwdt/4;
1101 if (player.iy < viewMin.y+vhgt/2) {
1102 // player is in the left side
1103 gy = viewMin.y+vhgt/2+vhgt/4;
1105 // player is in the right side
1106 gy = viewMin.y+vhgt/4;
1109 writeln("spawning ghost at tile (", gx/16, ",", gy/16, "); player is at tile (", player.ix/16, ",", player.iy/16, ")");
1111 MakeMapObject(gx, gy, 'oGhost');
1114 if (oPlayer1.x > room_width/2) instance_create(view_xview[0]+view_wview[0]+8, view_yview[0]+floor(view_hview[0] / 2), oGhost);
1115 else instance_create(view_xview[0]-32, view_yview[0]+floor(view_hview[0]/2), oGhost);
1116 global.ghostExists = true;
1121 void thinkFrameGameGhost () {
1122 if (player.dead) return;
1123 if (!isNormalLevel()) return; // just in case
1125 if (ghostTimeLeft < 0) {
1127 if (musicFadeTimer > 0) {
1128 musicFadeTimer = -1;
1129 global.setMusicPitch(1.0);
1134 if (musicFadeTimer >= 0) {
1136 if (musicFadeTimer > 0 && musicFadeTimer <= 100*3) {
1137 float pitch = 1.0-(0.00226/3.0)*musicFadeTimer;
1138 //writeln("MFT: ", musicFadeTimer, "; pitch=", pitch);
1139 global.setMusicPitch(pitch);
1143 if (ghostTimeLeft == 0) {
1144 // she is already here!
1148 // no ghost if we have a crown
1149 if (global.hasCrown) {
1154 // if she was already spawned, don't do it again
1160 if (--ghostTimeLeft != 0) {
1162 if (global.config.ghostExtraTime > 0) {
1163 if (ghostTimeLeft == global.config.ghostExtraTime*30) {
1164 osdMessage("A CHILL RUNS UP YOUR SPINE...\nLET'S GET OUT OF HERE!", 6.66, urgent:true);
1166 if (musicFadeTimer < 0 && ghostTimeLeft <= global.config.ghostExtraTime*30) {
1174 if (player.isExitingSprite) {
1175 // no reason to spawn her, we're leaving
1184 void thinkFrameGame () {
1185 thinkFrameGameGhost();
1189 // ////////////////////////////////////////////////////////////////////////// //
1190 private transient array!MapObject activeThinkerList;
1193 private final bool isWetTile (MapTile t) {
1194 return (t && t.visible && (t.water || t.lava || t.wet));
1198 private final bool isWetOrSolidTile (MapTile t) {
1199 return (t && t.visible && (t.water || t.lava || t.wet || t.solid) && t.isInstanceAlive);
1203 final bool isWetOrSolidTileAtPoint (int px, int py) {
1204 return !!checkTileAtPoint(px, py, &isWetOrSolidTile);
1208 final bool isWetOrSolidTileAtTile (int tx, int ty) {
1209 return !!checkTileAtPoint(tx*16, ty*16, &isWetOrSolidTile);
1213 final bool isWetTileAtTile (int tx, int ty) {
1214 return !!checkTileAtPoint(tx*16, ty*16, &isWetTile);
1218 // ////////////////////////////////////////////////////////////////////////// //
1219 const int GreatLakeStartTileY = 28;
1221 // called once after level generation
1222 final void fixLiquidTop () {
1223 foreach (int tileY; 0..tilesHeight) {
1224 foreach (int tileX; 0..tilesWidth) {
1225 auto t = tiles[tileX, tileY];
1227 if (t && !t.isInstanceAlive) {
1228 delete tiles[tileX, tileY];
1233 if (global.lake && tileY >= GreatLakeStartTileY) {
1234 // fill level with water for lake
1235 MakeMapTile(tileX, tileY, 'oWaterSwim');
1236 t = tiles[tileX, tileY];
1242 if (!t.water && !t.lava) {
1243 // mark as wet for lake
1244 if (global.lake && tileY >= GreatLakeStartTileY) {
1250 if (!isWetTileAtTile(tileX, tileY-1)) {
1251 t.setSprite(t.lava ? 'sLavaTop' : 'sWaterTop');
1253 if (t.spriteName == 'sWaterTop') t.setSprite('sWater');
1254 else if (t.spriteName == 'sLavaTop') t.setSprite('sLava');
1261 private final void checkWaterFlow (MapTile wtile) {
1262 //if (!wtile || (!wtile.water && !wtile.lava)) return;
1263 //instance_activate_region(x-16, y-16, 48, 48, true);
1265 //int x = wtile.ix, y = wtile.iy;
1266 int tileX = wtile.ix/16, tileY = wtile.iy/16;
1268 if (global.lake && tileY >= GreatLakeStartTileY) return;
1271 if ((not collision_point(x-16, y, oSolid, 0, 0) and not collision_point(x-16, y, oWater, 0, 0)) or
1272 (not collision_point(x+16, y, oSolid, 0, 0) and not collision_point(x+16, y, oWater, 0, 0)) or
1273 (not collision_point(x, y+16, oSolid, 0, 0) and not collision_point(x, y+16, oWater, 0, 0)))
1275 if (!isWetOrSolidTileAtTile(tileX-1, tileY) ||
1276 !isWetOrSolidTileAtTile(tileX+1, tileY) ||
1277 !isWetOrSolidTileAtTile(tileX, tileY+1))
1281 wtile.instanceRemove();
1284 tiles[tileX, tileY] = none;
1288 //if (!isSolidAtPoint(x, y-16) && !isLiquidAtPoint(x, y-16)) {
1289 if (!isWetTileAtTile(tileX, tileY-1)) {
1290 wtile.setSprite(wtile.lava ? 'sLavaTop' : 'sWaterTop');
1295 transient private array!MapTile waterTilesToCheck;
1297 final void cleanDeadTiles () {
1298 bool hasWater = false;
1299 waterTilesToCheck.length -= waterTilesToCheck.length;
1300 foreach (int y; 0..tilesHeight) {
1301 foreach (int x; 0..tilesWidth) {
1302 auto t = tiles[x, y];
1304 if (t.isInstanceAlive) {
1305 if (t.water || t.lava) waterTilesToCheck[$] = t;
1314 if (waterTilesToCheck.length && checkWater && !blockWaterChecking) {
1315 //writeln("checking water");
1316 checkWater = false; // `checkWaterFlow()` can set it again
1317 foreach (MapTile t; waterTilesToCheck) {
1318 if (t && t.isInstanceAlive && (t.water || t.lava)) checkWaterFlow(t);
1320 // fill empty spaces in lake with water
1322 foreach (int y; GreatLakeStartTileY..tilesHeight) {
1323 foreach (int x; 0..tilesWidth) {
1324 auto t = tiles[x, y];
1326 if (t && !t.isInstanceAlive) {
1332 if (!t.water || !t.lava) { t.wet = true; continue; }
1334 MakeMapTile(x, y, 'oWaterSwim');
1338 t.setSprite(y == GreatLakeStartTileY ? 'sWaterTop' : 'sWater');
1339 } else if (t.lava) {
1340 t.setSprite(y == GreatLakeStartTileY ? 'sLavaTop' : 'sLava');
1349 // ////////////////////////////////////////////////////////////////////////// //
1350 void collectLavaTiles () {
1351 lavatiles.length -= lavatiles.length;
1352 foreach (MapTile t; tiles) {
1353 if (t && t.lava && t.isInstanceAlive) lavatiles[$] = t;
1358 void processLavaTiles () {
1359 int tn = 0, tlen = lavatiles.length;
1361 MapTile t = lavatiles[tn];
1362 if (t && t.isInstanceAlive) {
1366 lavatiles.remove(tn, 1);
1373 // ////////////////////////////////////////////////////////////////////////// //
1374 // return `true` if thinker should be removed
1375 final bool thinkOne (MapObject o) {
1376 if (!o) return true;
1377 if (o.active && o.isInstanceAlive) {
1378 bool doThink = true;
1380 // collision with player weapon
1381 auto hh = PlayerWeapon(player.holdItem);
1382 bool doWeaponAction;
1384 if (hh.blockedBySolids) {
1385 int xx = player.ix+(player.dir == MapObject::Dir.Left ? -16 : 16);
1386 doWeaponAction = !isSolidAtPoint(xx, player.iy);
1388 doWeaponAction = true;
1391 doWeaponAction = false;
1394 if (doWeaponAction && o.whipTimer <= 0 && hh && hh.collidesWithObject(o)) {
1395 //writeln("WEAPONED!");
1396 if (!o.onTouchedByPlayerWeapon(player, hh)) {
1397 if (o.isInstanceAlive) hh.onCollisionWithObject(o);
1399 o.whipTimer = o.whipTimerValue; //HACK
1400 doThink = o.isInstanceAlive;
1403 // collision with player
1404 if (doThink && o.collidesWith(player)) {
1405 if (!player.onObjectTouched(o) && o.isInstanceAlive) {
1406 doThink = !o.onTouchedByPlayer(player);
1407 if (o.grid && o.gridId) o.grid.update(o.gridId, markAsDead:true);
1411 if (doThink && o.isInstanceAlive) {
1414 if (o.isInstanceAlive) {
1415 if (o.grid && o.gridId) o.grid.update(o.gridId, markAsDead:true);
1417 if (o.isInstanceAlive) {
1419 if (o.grid && o.gridId) o.grid.update(o.gridId, markAsDead:true);
1424 if (o.isInstanceAlive) {
1425 if (!o.canLiveOutsideOfLevel && o.isOutsideOfLevel()) {
1439 final void processThinkers (float timeDelta) {
1440 if (timeDelta <= 0) return;
1442 if (onBeforeFrame) onBeforeFrame(false);
1443 if (onAfterFrame) onAfterFrame(false);
1447 accumTime += timeDelta;
1448 bool wasFrame = false;
1450 auto olddel = ImmediateDelete;
1451 ImmediateDelete = false;
1452 while (accumTime >= FrameTime) {
1453 accumTime -= FrameTime;
1454 if (onBeforeFrame) onBeforeFrame(accumTime >= FrameTime);
1456 if (shakeLeft > 0) {
1458 if (!shakeDir.x) shakeDir.x = (global.randOther(0, 1) ? -3 : 3);
1459 if (!shakeDir.y) shakeDir.y = (global.randOther(0, 1) ? -3 : 3);
1460 shakeOfs.x = shakeDir.x;
1461 shakeOfs.y = shakeDir.y;
1462 int sgnc = global.randOther(1, 3);
1463 if (sgnc&0x01) shakeDir.x = -shakeDir.x;
1464 if (sgnc&0x02) shakeDir.y = -shakeDir.y;
1471 // game-global events
1473 // frame thinkers: lava tiles
1475 // frame thinkers: player
1476 if (player && !disablePlayerThink) {
1478 if (!player.dead && isNormalLevel() &&
1479 (maxPlayingTime < 0 ||
1480 (maxPlayingTime > 0 && maxPlayingTime*30 <= time &&
1481 time%30 == 0 && global.randOther(1, 100) <= 20)))
1483 MakeMapObject(player.ix, player.iy, 'oExplosion');
1484 player.scrCreateFlame(player.ix, player.iy, 3);
1486 //HACK: check for stolen items
1487 auto item = MapItem(player.holdItem);
1488 if (item) item.onCheckItemStolen(player);
1489 item = MapItem(player.pickedItem);
1490 if (item) item.onCheckItemStolen(player);
1492 player.saveInterpData();
1493 player.processAlarms();
1494 if (player.isInstanceAlive) {
1495 player.thinkFrame();
1496 if (player.isInstanceAlive) player.nextAnimFrame();
1499 // frame thinkers: moveable solids
1501 // frame thinkers: objects
1502 auto grid = objGrid;
1503 // collect active objects
1504 if (global.config.useFrozenRegion) {
1505 activeThinkerList.length -= activeThinkerList.length;
1506 foreach (MapObject o; grid.inRectPix(viewStart.x/global.scale-64, viewStart.y/global.scale-64, 320+64*2, 240+64*2, tag:grid.nextTag(), precise:false)) {
1507 activeThinkerList[$] = o;
1509 //writeln("thinkers: ", activeThinkerList.length);
1510 foreach (MapObject o; activeThinkerList) {
1512 grid.remove(o.gridId);
1519 bool killThisOne = false;
1520 for (int cid = grid.getFirstObject(); cid; cid = grid.getNextObject(cid, killThisOne)) {
1521 killThisOne = false;
1522 MapObject o = grid.getObject(MapObject, cid);
1523 if (!o) { killThisOne = true; continue; }
1524 // remove this object if it is dead
1534 if (player && player.holdItem) {
1535 if (player.holdItem.isInstanceAlive) {
1536 player.holdItem.fixHoldCoords();
1538 player.holdItem = none;
1541 // done with thinkers
1544 if (collectCounter == 0) {
1545 xmoney = max(0, xmoney-100);
1550 if (player && !player.dead) stats.oneMoreFramePlayed();
1551 if (onAfterFrame) onAfterFrame(accumTime >= FrameTime);
1554 if (playerExited) break;
1556 ImmediateDelete = olddel;
1558 playerExited = false;
1562 // if we were processed at least one frame, collect garbage
1564 CollectGarbage(true); // destroy delayed objects too
1566 if (accumTime > 0 && onInterFrame) onInterFrame(accumTime/FrameTime);
1570 // ////////////////////////////////////////////////////////////////////////// //
1571 final void tileXY2roomXY (int tileX, int tileY, out int roomX, out int roomY) {
1572 roomX = (tileX-1)/RoomGen::Width;
1573 roomY = (tileY-1)/RoomGen::Height;
1577 final bool isInShop (int tileX, int tileY) {
1578 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
1579 auto n = roomType[tileX, tileY];
1580 if (n == 4 || n == 5) return true;
1581 auto t = getTileAt(tileX, tileY);
1582 if (t && t.shopWall) return true;
1583 //k8: we don't have this
1584 //if (t && t.objType == 'oShop') return true;
1590 // ////////////////////////////////////////////////////////////////////////// //
1591 override void Destroy () {
1594 delete tempSolidTile;
1599 // ////////////////////////////////////////////////////////////////////////// //
1600 final MapObject findNearestBall (int px, int py) {
1601 MapObject res = none;
1602 int curdistsq = int.max;
1603 foreach (MapObject o; ballObjects) {
1604 if (!o || o.spectral || !o.isInstanceAlive) continue;
1605 int xc = px-o.xCenter, yc = py-o.yCenter;
1606 int distsq = xc*xc+yc*yc;
1607 if (distsq < curdistsq) {
1616 final int calcNearestBallDist (int px, int py) {
1617 auto e = findNearestBall(px, py);
1618 if (!e) return int.max;
1619 int xc = px-e.xCenter, yc = py-e.yCenter;
1620 return round(sqrt(xc*xc+yc*yc));
1624 final MapObject findNearestObject (int px, int py, bool delegate (MapObject o) dg) {
1625 MapObject res = none;
1626 int curdistsq = int.max;
1627 foreach (MapObject o; objGrid.allObjects()) {
1628 if (o.spectral || !o.isInstanceAlive) continue;
1629 if (!dg(o)) continue;
1630 int xc = px-o.xCenter, yc = py-o.yCenter;
1631 int distsq = xc*xc+yc*yc;
1632 if (distsq < curdistsq) {
1641 final MapObject findNearestEnemy (int px, int py, optional bool delegate (MapEnemy o) dg) {
1642 MapObject res = none;
1643 int curdistsq = int.max;
1644 foreach (MapObject o; objGrid.allObjects()) {
1645 //k8: i added `dead` check
1646 if (o.spectral || o !isa MapEnemy || o.dead || !o.isInstanceAlive) continue;
1648 if (!dg(MapEnemy(o))) continue;
1650 int xc = px-o.xCenter, yc = py-o.yCenter;
1651 int distsq = xc*xc+yc*yc;
1652 if (distsq < curdistsq) {
1661 final MonsterShopkeeper hasAliveShopkeepers (optional bool skipAngry) {
1662 foreach (MapObject o; objGrid.allObjects()) {
1663 auto sc = MonsterShopkeeper(o);
1664 if (!sc || o.spectral || !o.isInstanceAlive) continue;
1665 if (sc.dead) continue;
1666 if (skipAngry && sc.angered) continue;
1673 final int calcNearestEnemyDist (int px, int py, optional bool delegate (MapEnemy o) dg) {
1674 auto e = findNearestEnemy(px, py, dg!optional);
1675 if (!e) return int.max;
1676 int xc = px-e.xCenter, yc = py-e.yCenter;
1677 return round(sqrt(xc*xc+yc*yc));
1681 final int calcNearestObjectDist (int px, int py, optional bool delegate (MapObject o) dg) {
1682 auto e = findNearestObject(px, py, dg!optional);
1683 if (!e) return int.max;
1684 int xc = px-e.xCenter, yc = py-e.yCenter;
1685 return round(sqrt(xc*xc+yc*yc));
1689 final MapTile findNearestMoveableSolid (int px, int py, optional bool delegate (MapTile t) dg) {
1691 int curdistsq = int.max;
1692 foreach (MapTile t; miscTileGrid.allObjects()) {
1693 if (t.spectral || !t.isInstanceAlive) continue;
1695 if (!dg(t)) continue;
1697 if (!t.solid || !t.moveable) continue;
1699 int xc = px-t.xCenter, yc = py-t.yCenter;
1700 int distsq = xc*xc+yc*yc;
1701 if (distsq < curdistsq) {
1710 final MapTile findNearestTile (int px, int py, optional bool delegate (MapTile t) dg) {
1711 if (!dg) return none;
1713 int curdistsq = int.max;
1715 //FIXME: make this faster!
1716 foreach (MapTile t; tiles) {
1717 if (!t || t.spectral || !t.isInstanceAlive) continue;
1718 int xc = px-t.xCenter, yc = py-t.yCenter;
1719 int distsq = xc*xc+yc*yc;
1720 if (distsq < curdistsq && dg(t)) {
1726 foreach (MapTile t; miscTileGrid.allObjects()) {
1727 if (!t || t.spectral || !t.isInstanceAlive) continue;
1728 int xc = px-t.xCenter, yc = py-t.yCenter;
1729 int distsq = xc*xc+yc*yc;
1730 if (distsq < curdistsq && dg(t)) {
1740 // ////////////////////////////////////////////////////////////////////////// //
1741 final bool cbIsObjectWeb (MapObject o) { return (o isa ItemWeb); }
1742 final bool cbIsObjectBlob (MapObject o) { return (o.objName == 'oBlob'); }
1743 final bool cbIsObjectArrow (MapObject o) { return (o isa ItemProjectileArrow); }
1744 final bool cbIsObjectEnemy (MapObject o) { return (/*o != self &&*/ o isa MapEnemy); }
1746 final bool cbIsObjectTreasure (MapObject o) { return (o.isTreasure); }
1748 final bool cbIsItemObject (MapObject o) { return (o isa MapItem); }
1750 final bool cbIsItemOrEnemyObject (MapObject o) { return (o isa MapItem || o isa MapEnemy); }
1753 final MapObject isObjectAtTile (int tileX, int tileY, optional bool delegate (MapObject o) dg) {
1756 foreach (MapObject o; objGrid.inRectPix(tileX, tileY, 16, 16, objGrid.nextTag(), precise: true)) {
1757 if (o.spectral || !o.isInstanceAlive) continue;
1759 if (dg(o)) return o;
1768 final MapObject isObjectAtTilePix (int x, int y, optional bool delegate (MapObject o) dg) {
1769 return isObjectAtTile(x/16, y/16, dg!optional);
1773 final MapObject isObjectAtPoint (int xpos, int ypos, optional bool delegate (MapObject o) dg, optional bool precise) {
1774 if (!specified_precise) precise = true;
1775 foreach (MapObject o; objGrid.inCellPix(xpos, ypos, objGrid.nextTag(), precise: precise)) {
1776 if (o.spectral || !o.isInstanceAlive) continue;
1778 if (dg(o)) return o;
1780 if (o isa MapEnemy) return o;
1787 final MapObject isObjectInRect (int xpos, int ypos, int w, int h, optional bool delegate (MapObject o) dg, optional bool precise) {
1788 if (w < 1 || h < 1) return none;
1789 if (w == 1 && h == 1) return isObjectAtPoint(xpos, ypos, dg!optional, precise!optional);
1790 if (!specified_precise) precise = true;
1791 foreach (MapObject o; objGrid.inRectPix(xpos, ypos, w, h, objGrid.nextTag(), precise: precise)) {
1792 if (o.spectral || !o.isInstanceAlive) continue;
1794 if (dg(o)) return o;
1796 if (o isa MapEnemy) return o;
1803 final MapObject forEachObject (bool delegate (MapObject o) dg, optional bool allowSpectrals) {
1804 if (!dg) return none;
1806 foreach (MapObject o; objGrid.allObjects()) {
1807 if (o.spectral || !o.isInstanceAlive) continue;
1808 if (dg(o)) return o;
1811 // process gravity for moveable solids and burning for ropes
1812 auto grid = objGrid;
1813 int cid = grid.getFirstObject();
1815 MapObject o = grid.getObject(MapObject, cid);
1816 if (!o || !o.isInstanceAlive) {
1817 cid = grid.getNextObject(cid, removeThis:true);
1820 if (!allowSpectrals && o.spectral) {
1821 cid = grid.getNextObject(cid, removeThis:false);
1824 if (dg(o)) return o;
1825 if (o.isInstanceAlive) {
1826 cid = grid.getNextObject(cid, removeThis:false);
1828 cid = grid.getNextObject(cid, removeThis:true);
1829 o.instanceRemove(); // just in case
1838 final MapObject forEachObjectAtPoint (int xpos, int ypos, bool delegate (MapObject o) dg, optional bool precise) {
1839 if (!dg) return none;
1840 if (!specified_precise) precise = true;
1841 foreach (MapObject o; objGrid.inCellPix(xpos, ypos, objGrid.nextTag(), precise: precise)) {
1842 if (o.spectral || !o.isInstanceAlive) continue;
1843 if (dg(o)) return o;
1849 final MapObject forEachObjectInRect (int xpos, int ypos, int w, int h, bool delegate (MapObject o) dg, optional bool precise) {
1850 if (!dg || w < 1 || h < 1) return none;
1851 if (w == 1 && h == 1) return forEachObjectAtPoint(xpos, ypos, dg!optional, precise!optional);
1852 if (!specified_precise) precise = true;
1853 foreach (MapObject o; objGrid.inRectPix(xpos, ypos, w, h, objGrid.nextTag(), precise: precise)) {
1854 if (o.spectral || !o.isInstanceAlive) continue;
1855 if (dg(o)) return o;
1861 private final bool cbIsRopeTile (MapTile t) { return t.rope; }
1863 final MapTile isRopeAtPoint (int px, int py) {
1864 return checkTileAtPoint(px, py, &cbIsRopeTile);
1869 final MapTile isWaterSwimAtPoint (int px, int py) {
1870 return isWaterAtPoint(px, py);
1874 // ////////////////////////////////////////////////////////////////////////// //
1875 private array!MapObject tmpObjectList;
1877 private final bool cbCollectObjectsWithMask (MapObject t) {
1878 if (!t || !t.visible || t.spectral /*|| t.invincible*/ || !t.isInstanceAlive) return false;
1879 //auto spf = getSpriteFrame();
1880 //if (!t.sprite || t.sprite.frames.length < 1) return false;
1881 tmpObjectList[$] = t;
1886 final void touchObjectsWithMask (int x, int y, SpriteFrame frm, bool delegate (MapObject t) dg) {
1887 if (!frm || frm.bw < 1 || frm.bh < 1 || !dg) return;
1888 if (frm.isEmptyPixelMask) return;
1890 if (tmpObjectList.length) tmpObjectList.length -= tmpObjectList.length; // don't realloc
1891 if (player.isRectCollisionFrame(frm, x, y)) {
1892 //writeln("player hit");
1893 tmpObjectList[$] = player;
1896 writeln("no player hit: plr=(", player.ix, ",", player.iy, ")-(", player.ix+player.width-1, ",", player.iy+player.height-1, "); ",
1897 "frm=(", x+frm.bx, ",", y+frm.by, ")-(", x+frm.bx+frm.bw-1, ",", y+frm.by+frm.bh-1, ")");
1900 forEachObjectInRect(x+frm.bx, y+frm.by, frm.bw, frm.bh, &cbCollectObjectsWithMask);
1901 foreach (MapObject t; tmpObjectList) {
1902 if (!t || !t.isInstanceAlive || !t.visible || t.spectral) continue;
1904 auto tf = t.getSpriteFrame();
1906 //writeln("no sprite frame for ", GetClassName(t.Class));
1911 if (frm.pixelCheck(tf, t.ix-tf.xofs-x, t.iy-tf.yofs-y)) {
1912 //writeln("pixel hit for ", GetClassName(t.Class));
1916 if (t.isRectCollisionFrame(frm, x, y)) {
1923 // ////////////////////////////////////////////////////////////////////////// //
1924 final void destroyTileAt (int x, int y) {
1925 if (x < 0 || y < 0 || x >= tilesWidth*16 || y >= tilesHeight*16) return;
1928 MapTile t = tiles[x, y];
1929 if (!t || !t.visible || t.spectral || t.invincible || !t.isInstanceAlive) return;
1937 private array!MapTile tmpTileList;
1939 private final bool cbCollectTilesWithMask (MapTile t) {
1940 if (!t || !t.visible || t.spectral /*|| t.invincible*/ || !t.isInstanceAlive) return false;
1941 if (!t.sprite || t.sprite.frames.length < 1) return false;
1946 final void touchTilesWithMask (int x, int y, SpriteFrame frm, bool delegate (MapTile t) dg) {
1947 if (!frm || frm.bw < 1 || frm.bh < 1 || !dg) return;
1948 if (frm.isEmptyPixelMask) return;
1950 if (tmpTileList.length) tmpTileList.length -= tmpTileList.length; // don't realloc
1951 checkTilesInRect(x+frm.bx, y+frm.by, frm.bw, frm.bh, &cbCollectTilesWithMask);
1952 foreach (MapTile t; tmpTileList) {
1953 if (!t || !t.isInstanceAlive || !t.visible || t.spectral) continue;
1955 auto tf = t.sprite.frames[0];
1956 if (frm.pixelCheck(tf, t.ix-x, t.iy-y)) {
1958 //doCleanup = doCleanup || !t.isInstanceAlive;
1959 //writeln("dtwm at (", x, ",", y, "): dead at (", t.ix, ",", t.iy, ") : (", x/16, ",", y/16, ") : (", t.ix/16, ",", t.iy/16, ") <", GetClassName(t.Class), "> (name:", t.objName, "; type:", t.objType, ")");
1962 if (t.isRectCollisionFrame(frm, x, y)) {
1969 // ////////////////////////////////////////////////////////////////////////// //
1970 final bool cbCollisionAnySolid (MapTile t) { return t.solid; }
1971 final bool cbCollisionAnyMoveableSolid (MapTile t) { return t.solid && t.moveable; }
1972 final bool cbCollisionLiquid (MapTile t) { return (t.water || t.lava); }
1973 final bool cbCollisionAnySolidOrLiquid (MapTile t) { return (t.solid || t.water || t.lava); }
1974 final bool cbCollisionLadder (MapTile t) { return t.ladder; }
1975 final bool cbCollisionLadderTop (MapTile t) { return t.laddertop; }
1976 final bool cbCollisionAnyLadder (MapTile t) { return (t.ladder || t.laddertop); }
1977 final bool cbCollisionAnySolidIce (MapTile t) { return (t.solid && t.ice); }
1978 final bool cbCollisionWater (MapTile t) { return t.water; }
1979 final bool cbCollisionLava (MapTile t) { return t.lava; }
1980 final bool cbCollisionAnyTree (MapTile t) { return t.tree; }
1981 final bool cbCollisionPlatform (MapTile t) { return t.platform; }
1982 final bool cbCollisionLadderOrPlatform (MapTile t) { return (t.ladder || t.platform); }
1983 //final bool cbCollisionAnyHangeable (MapTile t) { return t.hangeable; } // no need to check for solids here, it is done in MapTile initializer
1984 final bool cbCollisionSpringTrap (MapTile t) { return t.springtrap; }
1985 final bool cbCollisionSpikes (MapTile t) { return t.spikes; }
1986 final bool cbCollisionExitTile (MapTile t) { return t.isExitActive(); }
1988 final bool cbCollisionForWhoa (MapTile t) { return (t.solid || t.laddertop); }
1990 //final bool cbCollisionSacAltar (MapTile t) { return (t.objName == 'oSacAltarLeft'); } // only left part, yeah
1991 final bool cbCollisionSacAltar (MapTile t) { return t.sacrificingAltar; }
1994 // ////////////////////////////////////////////////////////////////////////// //
1995 transient MapTile tempSolidTile;
1997 final MapTile checkTilesInRect (int x0, int y0, const int w, const int h, optional bool delegate (MapTile dg) dg, optional bool precise/*, optional bool dbgdump*/) {
1998 //!if (dbgdump) writeln("checkTilesInRect: (", x0, ",", y0, ")-(", x0+w-1, ",", y0+h-1, ") ; w=", w, "; h=", h);
1999 if (w < 1 || h < 1) return none;
2000 if (w == 1 && h == 1) return checkTileAtPoint(x0, y0, dg!optional);
2001 int x1 = x0+w-1, y1 = y0+h-1;
2002 if (x0 >= tilesWidth*16 || y0 >= tilesHeight*16 || x1 < 0 || y1 < 0) return none;
2004 //!if (dbgdump) writeln("default checker set");
2005 dg = &cbCollisionAnySolid;
2007 //!if (dbgdump) writeln("delegate: ", dg);
2008 int origx0 = x0, origy0 = y0;
2009 int tileSX = max(0, x0)/16;
2010 int tileSY = max(0, y0)/16;
2011 int tileEX = min(tilesWidth*16-1, x1)/16;
2012 int tileEY = min(tilesHeight*16-1, y1)/16;
2013 //!if (dbgdump) writeln(" tiles: (", tileSX, ",", tileSY, ")-(", tileEX, ",", tileEY, ")");
2014 //!!!auto grid = miscTileGrid;
2015 //!!!int tag = grid.nextTag();
2016 for (int ty = tileSY; ty <= tileEY; ++ty) {
2017 for (int tx = tileSX; tx <= tileEX; ++tx) {
2018 MapTile t = tiles[tx, ty];
2019 //!if (dbgdump && t && t.visible && t.isInstanceAlive && t.isRectCollision(origx0, origy0, w, h) ) writeln(" tile: ", GetClassName(t.Class), " : ", t.objName, " : ", t.objType, " : ", dg(t));
2020 if (t && !t.spectral && t.visible && t.isInstanceAlive && t.isRectCollision(origx0, origy0, w, h) && dg(t)) return t;
2021 // moveable tiles are in separate grid
2023 foreach (t; grid.inCellPix(tx*16, ty*16, tag, precise:precise)) {
2024 //if (t.isRectCollision(origx0, origy0, w, h) && dg(t)) return t;
2025 if (t.isInstanceAlive && !t.spectral && t.visible && t.isRectCollision(origx0, origy0, w, h) && dg(t)) return t;
2031 // moveable tiles are in separate grid
2032 foreach (MapTile t; miscTileGrid.inRectPix(x0, y0, w, h, miscTileGrid.nextTag(), precise:precise)) {
2033 //if (t.isRectCollision(origx0, origy0, w, h) && dg(t)) return t;
2034 if (t.isInstanceAlive && !t.spectral && t.visible && t.isRectCollision(origx0, origy0, w, h) && dg(t)) return t;
2037 // check walkable solid objects
2038 foreach (MapObject o; objGrid.inRectPix(x0, y0, w, h, objGrid.nextTag(), precise:precise)) {
2039 if (o && !o.spectral && o.visible && o.walkableSolid && o.isInstanceAlive && o.isRectCollision(origx0, origy0, w, h)) {
2040 if (!tempSolidTile) {
2041 tempSolidTile = SpawnObject(MapTile);
2042 } else if (!tempSolidTile.isInstanceAlive) {
2043 delete tempSolidTile;
2044 tempSolidTile = SpawnObject(MapTile);
2046 tempSolidTile.solid = true;
2047 if (dg(tempSolidTile)) return tempSolidTile;
2055 final MapTile checkTileAtPoint (int x0, int y0, optional bool delegate (MapTile dg) dg) {
2056 if (x0 < 0 || y0 < 0 || x0 >= tilesWidth*16 || y0 >= tilesHeight*16) return none;
2057 if (!dg) dg = &cbCollisionAnySolid;
2058 //if (!self) { writeln("WTF?!"); return none; }
2059 MapTile t = tiles[x0/16, y0/16];
2060 if (t && !t.spectral && t.visible && t.isInstanceAlive && t.isPointCollision(x0, y0) && dg(t)) return t;
2062 // moveable tiles are in separate grid
2063 foreach (t; miscTileGrid.inCellPix(x0, y0, miscTileGrid.nextTag(), precise:true)) {
2064 if (t.isInstanceAlive && !t.spectral && t.visible && dg(t)) return t;
2067 // check walkable solid objects
2068 foreach (MapObject o; objGrid.inCellPix(x0, y0, objGrid.nextTag(), precise:true)) {
2069 if (o && !o.spectral && o.visible && o.walkableSolid && o.isInstanceAlive && o.isRectCollision(x0, y0, 1, 1)) {
2070 if (!tempSolidTile) {
2071 tempSolidTile = SpawnObject(MapTile);
2072 } else if (!tempSolidTile.isInstanceAlive) {
2073 delete tempSolidTile;
2074 tempSolidTile = SpawnObject(MapTile);
2076 tempSolidTile.solid = true;
2077 if (dg(tempSolidTile)) return tempSolidTile;
2085 //FIXME: optimize this with clipping first
2086 //TODO: moveable tiles
2088 final MapTile checkTilesAtLine (int ax0, int ay0, int ax1, int ay1, optional bool delegate (MapTile dg) dg) {
2089 // do it faster if we can
2091 // strict vertical check?
2092 if (ax0 == ax1 && ay0 <= ay1) return checkTilesInRect(ax0, ay0, 1, ay1-ay0+1, dg!optional);
2093 // strict horizontal check?
2094 if (ay0 == ay1 && ax0 <= ax1) return checkTilesInRect(ax0, ay0, ax1-ax0+1, 1, dg!optional);
2096 float x0 = float(ax0)/16.0, y0 = float(ay0)/16.0, x1 = float(ax1)/16.0, y1 = float(ay1)/16.0;
2099 if (!dg) dg = &cbCollisionAnySolid;
2101 // get starting and enging tile
2102 int tileSX = trunc(x0), tileSY = trunc(y0);
2103 int tileEX = trunc(x1), tileEY = trunc(y1);
2105 // first hit is always landed
2106 if (tileSX >= 0 && tileSY >= 0 && tileSX < TilesWidth && tileSY < TilesHeight) {
2107 MapTile t = tiles[tileSX, tileSY];
2108 if (t && t.visible && Geom.lineAABBIntersects(ax0, ay0, ax1, ay1, tileSX*16, tileSY*16, 16, 16) && dg(t)) return t;
2111 // if starting and ending tile is the same, we don't need to do anything more
2112 if (tileSX == tileEX && tileSY == tileEY) return none;
2114 // calculate ray direction
2115 TVec dv = (vector(x1, y1)-vector(x0, y0)).normalise2d;
2117 // length of ray from one x or y-side to next x or y-side
2118 float deltaDistX = (fabs(dv.x) > 0.0001 ? fabs(1.0/dv.x) : 0.0);
2119 float deltaDistY = (fabs(dv.y) > 0.0001 ? fabs(1.0/dv.y) : 0.0);
2121 // calculate step and initial sideDists
2123 float sideDistX; // length of ray from current position to next x-side
2124 int stepX; // what direction to step in x (either +1 or -1)
2127 sideDistX = (x0-tileSX)*deltaDistX;
2130 sideDistX = (tileSX+1.0-x0)*deltaDistX;
2133 float sideDistY; // length of ray from current position to next y-side
2134 int stepY; // what direction to step in y (either +1 or -1)
2137 sideDistY = (y0-tileSY)*deltaDistY;
2140 sideDistY = (tileSY+1.0-y0)*deltaDistY;
2144 //int side; // was a NS or a EW wall hit?
2146 // jump to next map square, either in x-direction, or in y-direction
2147 if (sideDistX < sideDistY) {
2148 sideDistX += deltaDistX;
2152 sideDistY += deltaDistY;
2157 if (tileSX >= 0 && tileSY >= 0 && tileSX < TilesWidth && tileSY < TilesHeight) {
2158 MapTile t = tiles[tileSX, tileSY];
2159 if (t && t.visible && Geom.lineAABBIntersects(ax0, ay0, ax1, ay1, tileSX*16, tileSY*16, 16, 16) && dg(t)) return t;
2161 // did we arrived at the destination?
2162 if (tileSX == tileEX && tileSY == tileEY) break;
2170 // ////////////////////////////////////////////////////////////////////////// //
2171 final MapTile isSolidAtPoint (int px, int py) { return checkTileAtPoint(px, py); }
2172 final MapTile isSolidInRect (int rx, int ry, int rw, int rh) { return checkTilesInRect(rx, ry, rw, rh); }
2173 final MapTile isLadderAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadder); }
2174 final MapTile isLadderOrPlatformAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadderOrPlatform); }
2175 final MapTile isLadderTopAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadderTop); }
2176 final MapTile isAnyLadderAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyLadder); }
2177 final MapTile isWaterAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionWater); }
2178 final MapTile isLavaAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLava); }
2179 final MapTile isLiquidAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLiquid); }
2180 final MapTile isIceAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnySolidIce); }
2181 final MapTile isTreeAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyTree); }
2182 //final MapTile isHangeableAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyHangeable); }
2185 // ////////////////////////////////////////////////////////////////////////// //
2186 // PlayerPawn has it's own movement code, so don't process it here
2187 // but process moveable solids here, yeah
2188 final void physStep () {
2191 // we don't want the time to grow too large
2192 if (time > 100000000) time = 0;
2194 auto grid = miscTileGrid;
2196 // process gravity for moveable solids and burning for ropes
2197 int cid = grid.getFirstObject();
2199 MapTile t = grid.getObject(MapTile, cid);
2201 cid = grid.getNextObject(cid, removeThis:false);
2204 if (t.isInstanceAlive) {
2207 if (t.isInstanceAlive) {
2208 grid.update(cid, markAsDead:false);
2210 if (t.isInstanceAlive && !t.canLiveOutsideOfLevel && t.isOutsideOfLevel()) t.instanceRemove();
2211 grid.update(cid, markAsDead:false);
2214 if (t.isInstanceAlive) {
2215 cid = grid.getNextObject(cid, removeThis:false);
2217 cid = grid.getNextObject(cid, removeThis:true);
2218 t.instanceRemove(); // just in case
2227 // ////////////////////////////////////////////////////////////////////////// //
2228 final void setTileTypeAt (int tileX, int tileY, LevelGen::RType rt) {
2229 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) roomType[tileX, tileY] = rt;
2232 final MapTile getTileAt (int tileX, int tileY) {
2233 return (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight ? tiles[tileX, tileY] : none);
2236 final bool isNamedTileAt (int tileX, int tileY, name atypename) {
2237 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
2238 auto t = tiles[tileX, tileY];
2239 if (t && t.objName == atypename) return true;
2244 final void setTileAt (int tileX, int tileY, MapTile tile) {
2245 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
2247 if (tiles[tileX, tileY]) checkWater = true;
2248 delete tiles[tileX, tileY];
2249 tiles[tileX, tileY] = tile;
2254 // ////////////////////////////////////////////////////////////////////////// //
2255 // return `true` from delegate to stop
2256 MapTile forEachSolidTile (bool delegate (int x, int y, MapTile t) dg) {
2257 if (!dg) return none;
2258 foreach (int y; 0..tilesHeight) {
2259 foreach (int x; 0..tilesWidth) {
2260 auto t = tiles[x, y];
2261 if (t && t.solid && t.visible && t.isInstanceAlive) {
2262 if (dg(x, y, t)) return t;
2270 // ////////////////////////////////////////////////////////////////////////// //
2271 // return `true` from delegate to stop
2272 MapTile forEachNormalTile (bool delegate (int x, int y, MapTile t) dg) {
2273 if (!dg) return none;
2274 foreach (int y; 0..tilesHeight) {
2275 foreach (int x; 0..tilesWidth) {
2276 auto t = tiles[x, y];
2277 if (t && t.visible && t.isInstanceAlive) {
2278 if (dg(x, y, t)) return t;
2286 // WARNING! don't destroy tiles here! (instanceRemove() is ok, tho)
2287 MapTile forEachTile (bool delegate (MapTile t) dg) {
2288 if (!dg) return none;
2289 foreach (int y; 0..tilesHeight) {
2290 foreach (int x; 0..tilesWidth) {
2291 auto t = tiles[x, y];
2292 if (t && t.visible && !t.spectral && t.isInstanceAlive) {
2293 if (dg(t)) return t;
2297 foreach (MapObject o; miscTileGrid.allObjects()) {
2298 auto mt = MapTile(o);
2300 if (mt.visible && !mt.spectral && mt.isInstanceAlive) {
2301 //writeln("special map tile: '", GetClassName(mt.Class), "'");
2302 if (dg(mt)) return mt;
2309 // ////////////////////////////////////////////////////////////////////////// //
2310 final void fixWallTiles () {
2311 foreach (int y; 0..tilesHeight) {
2312 foreach (int x; 0..tilesWidth) {
2313 auto t = getTileAt(x, y);
2316 if (y == tilesHeight-2) {
2317 writeln("0: tx=", x, "; tile=", t.objName, "; ty=", y, ": (", t.iy/16, ")");
2318 } else if (y == tilesHeight-1) {
2319 writeln("1: tx=", x, "; tile=", t.objName, "; ty=", y, ": (", t.iy/16, ")");
2325 foreach (MapTile t; miscTileGrid.allObjects()) {
2326 if (t.isInstanceAlive) t.beautifyTile();
2331 // ////////////////////////////////////////////////////////////////////////// //
2332 final MapTile isCollisionAtPoint (int px, int py, optional bool delegate (MapTile dg) dg) {
2333 if (!dg) dg = &cbCollisionAnySolid;
2334 return checkTilesInRect(px, py, 1, 1, dg);
2338 // ////////////////////////////////////////////////////////////////////////// //
2339 string scrGetKaliGift (MapTile altar, optional name gift) {
2342 // find other side of the altar
2343 int sx = player.ix, sy = player.iy;
2347 auto a2 = isCollisionAtPoint(altar.ix+18, altar.iy+8, &cbCollisionSacAltar);
2348 if (!a2) a2 = isCollisionAtPoint(altar.ix-8, altar.iy+8, &cbCollisionSacAltar);
2349 if (a2) { sx = a2.ix; sy = a2.iy; }
2352 if (global.favor <= -8) res = "SHE SEEMS VERY ANGRY WITH YOU!";
2353 else if (global.favor < 0) res = "SHE SEEMS ANGRY WITH YOU.";
2354 else if (global.favor == 0) res = "SHE HAS FORGIVEN YOU!";
2355 else if (global.favor >= 32) {
2356 if (global.kaliGift >= 3 && global.favor >= 32+(global.kaliGift-2)*16) {
2357 res = "YOU FEEL INVIGORATED!";
2358 global.kaliGift += 1;
2359 global.plife += global.randOther(4, 8);
2360 } else if (global.kaliGift >= 3) {
2361 res = "SHE SEEMS ECSTATIC WITH YOU!";
2362 } else if (global.bombs < 80) {
2363 res = "YOUR SATCHEL FEELS VERY FULL NOW!";
2364 global.kaliGift = 3;
2367 res = "YOU FEEL INVIGORATED!";
2368 global.kaliGift += 1;
2369 global.plife += global.randOther(4, 8);
2371 } else if (global.favor >= 16) {
2372 if (global.kaliGift >= 2) {
2373 res = "SHE SEEMS VERY HAPPY WITH YOU!";
2375 res = "SHE BESTOWS A GIFT UPON YOU!";
2376 global.kaliGift = 2;
2378 auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2381 obj = MakeMapObject(sx, sy-8, 'oPoof');
2386 if (gift) obj = MakeMapObject(sx, sy-8, gift);
2387 if (!obj) obj = MakeMapObject(sx, sy-8, 'oKapala');
2389 } else if (global.favor >= 8) {
2390 if (global.kaliGift >= 1) {
2391 res = "SHE SEEMS HAPPY WITH YOU.";
2393 res = "SHE BESTOWS A GIFT UPON YOU!";
2394 global.kaliGift = 1;
2395 //rAltar = instance_nearest(x, y, oSacAltarRight);
2396 //if (instance_exists(rAltar)) {
2398 auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2401 obj = MakeMapObject(sx, sy-8, 'oPoof');
2405 if (gift) obj = MakeMapObject(sx, sy-8, gift);
2407 auto n = global.randOther(1, 8);
2411 if (n == 1 && !global.hasCape && !global.hasJetpack) aname = 'oCapePickup';
2412 else if (n == 2 && !global.hasGloves) aname = 'oGloves';
2413 else if (n == 3 && !global.hasSpectacles) aname = 'oSpectacles';
2414 else if (n == 4 && !global.hasMitt) aname = 'oMitt';
2415 else if (n == 5 && !global.hasSpringShoes) aname = 'oSpringShoes';
2416 else if (n == 6 && !global.hasSpikeShoes) aname = 'oSpikeShoes';
2417 else if (n == 7 && !global.hasStickyBombs) aname = 'oPaste';
2418 else if (n == 8 && !global.hasCompass) aname = 'oCompass';
2420 obj = MakeMapObject(sx, sy-8, aname);
2426 obj = MakeMapObject(sx, sy-8, (global.hasJetpack ? 'oBombBox' : 'oJetpack'));
2432 } else if (global.favor > 0) {
2433 res = "SHE SEEMS PLEASED WITH YOU.";
2438 global.message = "";
2439 res = "KALI DEVOURS YOU!"; // sacrifice is player
2447 void performSacrifice (MapObject what, MapTile where) {
2448 if (!what || !what.isInstanceAlive) return;
2449 MakeMapObject(what.ix+8, what.iy+8, 'oFlame');
2450 if (where) where.playSound('sndSmallExplode'); else what.playSound('sndSmallExplode');
2451 if (!what.bloodless) what.scrCreateBlood(what.ix+8, what.iy+8, 3);
2453 string msg = "KALI ACCEPTS THE SACRIFICE!";
2455 auto idol = ItemGoldIdol(what);
2457 ++stats.totalSacrifices;
2458 if (global.favor <= -8) msg = "SHE SEEMS VERY ANGRY WITH YOU!";
2459 else if (global.favor < 0) msg = "SHE SEEMS ANGRY WITH YOU.";
2460 else if (global.favor >= 0) {
2461 // find other side of the altar
2462 int sx = player.ix, sy = player.iy;
2467 auto a2 = isCollisionAtPoint(altar.ix+18, altar.iy+8, &cbCollisionSacAltar);
2468 if (!a2) a2 = isCollisionAtPoint(altar.ix-8, altar.iy+8, &cbCollisionSacAltar);
2469 if (a2) { sx = a2.ix; sy = a2.iy; }
2472 auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2475 obj = MakeMapObject(sx, sy-8, 'oPoof');
2479 obj = MakeMapObject(sx, sy-16-8, 'oMonkeyGold');
2481 osdMessage(msg, 6.66);
2483 idol.instanceRemove();
2487 if (global.favor <= -8) {
2488 msg = "KALI DEVOURS THE SACRIFICE!";
2489 } else if (global.favor < 0) {
2490 global.favor += (what.status == MapObject::STUNNED ? what.favor : round(what.favor/2.0)); //k8: added `round()`
2491 if (what.favor > 0) what.favor = 0;
2493 global.favor += (what.status == MapObject::STUNNED ? what.favor : round(what.favor/2.0)); //k8: added `round()`
2497 if (myAltar.gift1 != "" and global.kaliGift == 0) scrGetKaliGift(myAltar.gift1);
2498 else if (myAltar.gift2 != "" and global.kaliGift == 1) scrGetKaliGift(myAltar.gift2);
2499 else scrGetKaliGift("");
2502 // sacrifice is player?
2503 if (what isa PlayerPawn) {
2504 ++stats.totalSelfSacrifices;
2505 msg = "KALI DEVOURS YOU!";
2506 player.visible = false;
2508 player.status = MapObject::DEAD;
2510 ++stats.totalSacrifices;
2511 auto msg2 = scrGetKaliGift(where);
2512 what.instanceRemove();
2513 if (msg2) msg = va("%s\n%s", msg, msg2);
2516 osdMessage(msg, 6.66);
2518 //!if (isRealLevel()) global.totalSacrifices += 1;
2520 //!global.messageTimer = 200;
2521 //!global.shake = 10;
2525 instance_create(x, y, oFlame);
2526 playSound(global.sndSmallExplode);
2527 scrCreateBlood(x, y, 3);
2528 global.message = "KALI ACCEPTS YOUR SACRIFICE!";
2529 if (global.favor <= -8) {
2530 global.message = "KALI DEVOURS YOUR SACRIFICE!";
2531 } else if (global.favor < 0) {
2532 if (status == 98) global.favor += favor*1.5; else global.favor += favor;
2533 if (favor > 0) favor = 0;
2535 if (status == 98) global.favor += favor*1.5; else global.favor += favor;
2538 if (myAltar.gift1 != "" and global.kaliGift == 0) scrGetFavorMsg(myAltar.gift1);
2539 else if (myAltar.gift2 != "" and global.kaliGift == 1) scrGetFavorMsg(myAltar.gift2);
2540 else scrGetFavorMsg("");
2542 global.messageTimer = 200;
2549 // ////////////////////////////////////////////////////////////////////////// //
2550 final void addBackgroundGfxDetails () {
2551 // add background details
2552 //if (global.customLevel || global.parallax) return;
2554 // bg = instance_create(16*randRoom(1, 42), 16*randRoom(1, 33), oCaveBG);
2555 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);
2556 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);
2557 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);
2558 else MakeMapBackTile('bgExtras', 32*global.randRoom(0, 1), 0, 32, 32, 16*global.randRoom(1, 42), 16*global.randRoom(1, 33), 10002);
2563 // ////////////////////////////////////////////////////////////////////////// //
2564 private final void fixRealViewStart () {
2565 int scale = global.scale;
2566 realViewStart.x = clamp(realViewStart.x, viewMin.x*scale, viewMax.x*scale-viewWidth);
2567 realViewStart.y = clamp(realViewStart.y, viewMin.y*scale, viewMax.y*scale-viewHeight);
2571 final int cameraCurrX () { return realViewStart.x/global.scale; }
2572 final int cameraCurrY () { return realViewStart.y/global.scale; }
2575 private final void fixViewStart () {
2576 int scale = global.scale;
2577 viewStart.x = clamp(viewStart.x, viewMin.x*scale, viewMax.x*scale-viewWidth);
2578 viewStart.y = clamp(viewStart.y, viewMin.y*scale, viewMax.y*scale-viewHeight);
2582 final void centerViewAtPlayer () {
2583 if (viewWidth < 1 || viewHeight < 1 || !player) return;
2584 centerViewAt(player.xCenter, player.yCenter);
2588 final void centerViewAt (int x, int y) {
2589 if (viewWidth < 1 || viewHeight < 1) return;
2591 cameraSlideToSpeed.x = 0;
2592 cameraSlideToSpeed.y = 0;
2593 cameraSlideToPlayer = 0;
2595 int scale = global.scale;
2598 realViewStart.x = clamp(x-viewWidth/2, 0, tilesWidth*16*scale-viewWidth);
2599 realViewStart.y = clamp(y-viewHeight/2, 0, tilesHeight*16*scale-viewHeight);
2602 viewStart.x = realViewStart.x;
2603 viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
2608 const int ViewPortToleranceX = 16*1+8;
2609 const int ViewPortToleranceY = 16*1+8;
2611 final void fixCamera () {
2612 if (!player) return;
2613 if (viewWidth < 1 || viewHeight < 1) return;
2614 int scale = global.scale;
2615 auto alwaysCenterX = global.config.alwaysCenterPlayer;
2616 auto alwaysCenterY = alwaysCenterX;
2617 // calculate offset from viewport center (in game units), and fix viewport
2619 int camDestX = player.ix+8;
2620 int camDestY = player.iy+8;
2621 if (!cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y) != 0) {
2622 // slide camera to point
2623 if (cameraSlideToSpeed.x) camDestX = cameraSlideToCurr.x;
2624 if (cameraSlideToSpeed.y) camDestY = cameraSlideToCurr.y;
2625 int dx = cameraSlideToDest.x-camDestX;
2626 int dy = cameraSlideToDest.y-camDestY;
2627 //writeln("speed:(", cameraSlideToSpeed.x, ",", cameraSlideToSpeed.y, "); cur:(", camDestX, ",", camDestY, "); dest:(", cameraSlideToDest.x, ",", cameraSlideToDest.y, "); delta=(", dx, ",", dy, ")");
2628 if (dx && cameraSlideToSpeed.x != 0) {
2629 alwaysCenterX = true;
2630 if (abs(dx) <= abs(cameraSlideToSpeed.x)) {
2631 camDestX = cameraSlideToDest.x;
2633 camDestX += sign(dx)*abs(cameraSlideToSpeed.x);
2636 if (dy && abs(cameraSlideToSpeed.y) != 0) {
2637 alwaysCenterY = true;
2638 if (abs(dy) <= cameraSlideToSpeed.y) {
2639 camDestY = cameraSlideToDest.y;
2641 camDestY += sign(dy)*abs(cameraSlideToSpeed.y);
2644 //writeln(" new:(", camDestX, ",", camDestY, ")");
2645 if (cameraSlideToSpeed.x) cameraSlideToCurr.x = camDestX;
2646 if (cameraSlideToSpeed.y) cameraSlideToCurr.y = camDestY;
2650 if (cameraSlideToSpeed.x && !cameraSlideToPlayer) {
2651 realViewStart.x = clamp(camDestX*scale, 0, tilesWidth*16*scale-viewWidth);
2652 } else if (!player.cameraBlockX) {
2653 int x = camDestX*scale;
2654 int cx = realViewStart.x;
2655 if (alwaysCenterX) {
2658 int xofs = x-(cx+viewWidth/2);
2659 if (xofs < -ViewPortToleranceX*scale) cx += xofs+ViewPortToleranceX*scale;
2660 else if (xofs > ViewPortToleranceX*scale) cx += xofs-ViewPortToleranceX*scale;
2662 // slide back to player?
2663 if (cameraSlideToPlayer && cameraSlideToSpeed.x) {
2664 int prevx = cameraSlideToCurr.x*scale;
2665 int dx = (cx-prevx)/scale;
2666 if (abs(dx) <= cameraSlideToSpeed.x) {
2667 writeln("BACKSLIDE X COMPLETE!");
2668 cameraSlideToSpeed.x = 0;
2670 cx = prevx+(sign(dx)*cameraSlideToSpeed.x)*scale;
2671 cameraSlideToCurr.x += sign(dx)*cameraSlideToSpeed.x;
2672 if ((dx < 0 && cx <= 0) || (dx > 0 && cx >= tilesWidth*16*scale-viewWidth)) {
2673 writeln("BACKSLIDE X COMPLETE!");
2674 cameraSlideToSpeed.x = 0;
2678 realViewStart.x = clamp(cx, 0, tilesWidth*16*scale-viewWidth);
2682 if (cameraSlideToSpeed.y && !cameraSlideToPlayer) {
2683 realViewStart.y = clamp(camDestY*scale, 0, tilesHeight*16*scale-viewHeight);
2684 } else if (!player.cameraBlockY) {
2685 int y = camDestY*scale;
2686 int cy = realViewStart.y;
2687 if (alwaysCenterY) {
2688 cy = y-viewHeight/2;
2690 int yofs = y-(cy+viewHeight/2);
2691 if (yofs < -ViewPortToleranceY*scale) cy += yofs+ViewPortToleranceY*scale;
2692 else if (yofs > ViewPortToleranceY*scale) cy += yofs-ViewPortToleranceY*scale;
2694 // slide back to player?
2695 if (cameraSlideToPlayer && cameraSlideToSpeed.y) {
2696 int prevy = cameraSlideToCurr.y*scale;
2697 int dy = (cy-prevy)/scale;
2698 if (abs(dy) <= cameraSlideToSpeed.y) {
2699 writeln("BACKSLIDE Y COMPLETE!");
2700 cameraSlideToSpeed.y = 0;
2702 cy = prevy+(sign(dy)*cameraSlideToSpeed.y)*scale;
2703 cameraSlideToCurr.y += sign(dy)*cameraSlideToSpeed.y;
2704 if ((dy < 0 && cy <= 0) || (dy > 0 && cy >= tilesHeight*16*scale-viewHeight)) {
2705 writeln("BACKSLIDE Y COMPLETE!");
2706 cameraSlideToSpeed.y = 0;
2710 realViewStart.y = clamp(cy, 0, tilesHeight*16*scale-viewHeight);
2713 if (cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y) == 0) cameraSlideToPlayer = 0;
2716 //writeln(" new2:(", cameraCurrX, ",", cameraCurrY, ")");
2718 viewStart.x = realViewStart.x;
2719 viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
2724 // ////////////////////////////////////////////////////////////////////////// //
2725 // x0 and y0 are non-scaled (and will be scaled)
2726 final void drawSpriteAt (name sprName, float frnumf, int x0, int y0, optional bool small) {
2727 if (!sprName) return;
2728 auto spr = sprStore[sprName];
2729 if (!spr || !spr.frames.length) return;
2730 int scale = global.scale;
2733 int frnum = max(0, trunc(frnumf))%spr.frames.length;
2734 auto sfr = spr.frames[frnum];
2735 int sx0 = x0-sfr.xofs*scale;
2736 int sy0 = y0-sfr.yofs*scale;
2737 if (small && scale > 1) {
2738 sfr.tex.blitExt(sx0, sy0, round(sx0+sfr.tex.width*(scale/2.0)), round(sy0+sfr.tex.height*(scale/2.0)), 0, 0);
2740 sfr.tex.blitAt(sx0, sy0, scale);
2745 // x0 and y0 are non-scaled (and will be scaled)
2746 final void drawTextAt (int x0, int y0, string text) {
2748 int scale = global.scale;
2751 sprStore.renderText(x0, y0, text, scale);
2755 void renderCompass (float currFrameDelta) {
2756 if (!global.hasCompass) return;
2759 if (isRoom("rOlmec")) {
2762 } else if (isRoom("rOlmec2")) {
2768 bool hasMessage = osdHasMessage();
2769 foreach (MapTile et; allExits) {
2771 int exitX = et.ix, exitY = et.iy;
2772 int vx0 = viewStart.x/global.scale, vy0 = viewStart.y/global.scale;
2773 int vx1 = (viewStart.x+viewWidth)/global.scale;
2774 int vy1 = (viewStart.y+viewHeight)/global.scale;
2775 if (exitY > vy1-16) {
2777 drawSpriteAt(hasMessage ? 'sCompassSmallLL' : 'sCompassLL', 0, 0, 224);
2778 } else if (exitX > vx1-16) {
2779 drawSpriteAt(hasMessage ? 'sCompassSmallLR' : 'sCompassLR', 0, 304, 224);
2781 drawSpriteAt(hasMessage ? 'sCompassSmallDown' : 'sCompassDown', 0, exitX-vx0, 224);
2783 } else if (exitX < vx0) {
2784 drawSpriteAt(hasMessage ? 'sCompassSmallLeft' : 'sCompassLeft', 0, 0, exitY-vy0);
2785 } else if (exitX > vx1-16) {
2786 drawSpriteAt(hasMessage ? 'sCompassSmallRight' : 'sCompassRight', 0, 304, exitY-vy0);
2792 final bool totalsNameCmpCB (ref GameStats::TotalItem a, ref GameStats::TotalItem b) {
2793 auto sa = string(a.objName);
2794 auto sb = string(b.objName);
2798 void renderTransitionInfo (float currFrameDelta) {
2801 GameStats.sortTotalsList(stats.kills, &totalsNameCmpCB);
2804 foreach (int idx, ref auto k; stats.kills) {
2805 string s = string(k);
2806 maxLen = max(maxLen, s.length);
2810 sprStore.loadFont('sFontSmall');
2811 Video.color = 0xff_ff_00;
2812 foreach (int idx, ref auto k; stats.kills) {
2814 foreach (int xidx, ref auto d; stats.totalKills) {
2815 if (d.objName == k) { deaths = d.count; break; }
2817 //drawTextAt(16, 4+idx*8, va("%s: %d (%d)", string(k.objName).toUpperCase, k.count, deaths));
2818 drawTextAt(16, 4+idx*8, string(k).toUpperCase);
2819 drawTextAt(16+maxLen, 4+idx*8, va(": %d (%d)", k.count, deaths));
2825 void renderGhostTimer (float currFrameDelta) {
2826 if (ghostTimeLeft <= 0) return;
2827 //ghostTimeLeft /= 30; // frames -> seconds
2829 int hgt = Video.screenHeight-64;
2830 if (hgt < 1) return;
2831 int rhgt = round(float(hgt)*(float(ghostTimeLeft)/30.0)/1000.0);
2832 //writeln("hgt=", hgt, "; rhgt=", rhgt, "; gtl=", ghostTimeLeft/30);
2834 auto oclr = Video.color;
2835 Video.color = 0xcf_ff_7f_00;
2836 Video.fillRect(Video.screenWidth-20, 32, 16, hgt-rhgt);
2837 Video.color = 0x7f_ff_7f_00;
2838 Video.fillRect(Video.screenWidth-20, 32+(hgt-rhgt), 16, rhgt);
2844 void renderHUD (float currFrameDelta) {
2845 if (inWinCutscene) return;
2847 if (levelKind == LevelKind.Transition) { renderTransitionInfo(currFrameDelta); return; }
2855 bool scumSmallHud = global.config.scumSmallHud;
2856 if (!global.config.optSGAmmo) moneyX = ammoX;
2859 sprStore.loadFont('sFontSmall');
2862 sprStore.loadFont('sFont');
2865 Video.color = 0xff_ff_ff;
2869 if (global.plife == 1) {
2870 drawSpriteAt('sHeartSmallBlink', global.heartBlink, lifeX+2, 4-hhup);
2871 global.heartBlink += 0.1;
2872 if (global.heartBlink > 3) global.heartBlink = 0;
2874 drawSpriteAt('sHeartSmall', -1, lifeX+2, 4-hhup);
2875 global.heartBlink = 0;
2878 if (global.plife == 1) {
2879 drawSpriteAt('sHeartBlink', global.heartBlink, lifeX, 8-hhup);
2880 global.heartBlink += 0.1;
2881 if (global.heartBlink > 3) global.heartBlink = 0;
2883 drawSpriteAt('sHeart', -1, lifeX, 8-hhup);
2884 global.heartBlink = 0;
2888 int life = clamp(global.plife, 0, 99);
2889 //if (!scumHud && life > 99) life = 99;
2890 drawTextAt(lifeX+16, 8-hhup, va("%d", life));
2893 if (global.hasStickyBombs && global.stickyBombsActive) {
2894 if (scumSmallHud) drawSpriteAt('sStickyBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sStickyBombIcon', -1, bombX, 8-hhup);
2896 if (scumSmallHud) drawSpriteAt('sBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sBombIcon', -1, bombX, 8-hhup);
2898 int n = global.bombs;
2899 if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
2900 drawTextAt(bombX+16, 8-hhup, va("%d", n));
2903 if (scumSmallHud) drawSpriteAt('sRopeIconSmall', -1, ropeX+2, 4-hhup); else drawSpriteAt('sRopeIcon', -1, ropeX, 8-hhup);
2905 if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
2906 drawTextAt(ropeX+16, 8-hhup, va("%d", n));
2909 if (global.config.optSGAmmo) {
2910 if (scumSmallHud) drawSpriteAt('sShellsIcon', -1, ammoX+6, 8-hhup); else drawSpriteAt('sShellsIcon', -1, ammoX+7, 12-hhup);
2912 if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
2913 drawTextAt(ammoX+16, 8-hhup, va("%d", n));
2917 if (scumSmallHud) drawSpriteAt('sDollarSignSmall', -1, moneyX+2, 3-hhup); else drawSpriteAt('sDollarSign', -1, moneyX, 8-hhup);
2918 drawTextAt(moneyX+16, 8-hhup, va("%d", stats.money-xmoney));
2920 int ity = (scumSmallHud ? 18-hhup : 24-hhup);
2923 if (global.hasUdjatEye) {
2924 if (global.udjatBlink) drawSpriteAt('sUdjatEyeIcon2', -1, n, ity); else drawSpriteAt('sUdjatEyeIcon', -1, n, ity);
2927 if (global.hasAnkh) { drawSpriteAt('sAnkhIcon', -1, n, ity); n += 20; }
2928 if (global.hasCrown) { drawSpriteAt('sCrownIcon', -1, n, ity); n += 20; }
2929 if (global.hasKapala) {
2930 if (global.bloodLevel == 0) drawSpriteAt('sKapalaIcon', 0, n, ity);
2931 else if (global.bloodLevel <= 2) drawSpriteAt('sKapalaIcon', 1, n, ity);
2932 else if (global.bloodLevel <= 4) drawSpriteAt('sKapalaIcon', 2, n, ity);
2933 else if (global.bloodLevel <= 6) drawSpriteAt('sKapalaIcon', 3, n, ity);
2934 else if (global.bloodLevel <= 8) drawSpriteAt('sKapalaIcon', 4, n, ity);
2937 if (global.hasSpectacles) { drawSpriteAt('sSpectaclesIcon', -1, n, ity); n += 20; }
2938 if (global.hasGloves) { drawSpriteAt('sGlovesIcon', -1, n, ity); n += 20; }
2939 if (global.hasMitt) { drawSpriteAt('sMittIcon', -1, n, ity); n += 20; }
2940 if (global.hasSpringShoes) { drawSpriteAt('sSpringShoesIcon', -1, n, ity); n += 20; }
2941 if (global.hasJordans) { drawSpriteAt('sJordansIcon', -1, n, ity); n += 20; }
2942 if (global.hasSpikeShoes) { drawSpriteAt('sSpikeShoesIcon', -1, n, ity); n += 20; }
2943 if (global.hasCape) { drawSpriteAt('sCapeIcon', -1, n, ity); n += 20; }
2944 if (global.hasJetpack) { drawSpriteAt('sJetpackIcon', -1, n, ity); n += 20; }
2945 if (global.hasStickyBombs) { drawSpriteAt('sPasteIcon', -1, n, ity); n += 20; }
2946 if (global.hasCompass) { drawSpriteAt('sCompassIcon', -1, n, ity); n += 20; }
2947 if (global.hasParachute) { drawSpriteAt('sParachuteIcon', -1, n, ity); n += 20; }
2949 if (player.holdItem && player.holdItem.objName == 'Bow' && global.arrows > 0) {
2952 while (m <= global.arrows && m <= 20 && malpha > 0) {
2953 Video.color = trunc(malpha*255)<<24|0xff_ff_ff;
2954 drawSpriteAt('sArrowIcon', -1, n, ity);
2956 if (m > 10) malpha -= 0.1; //and global.arrows > 20 malpha -= 0.1
2959 Video.color = 0xff_ff_ff;
2963 sprStore.loadFont('sFontSmall');
2964 Video.color = 0xff_ff_00;
2965 if (scumSmallHud) drawTextAt(moneyX+8, 8+8-hhup, va("+%d", xmoney));
2966 else drawTextAt(moneyX, 8+16-hhup, va("+%d", xmoney));
2969 if (global.config.ghostShowTime) renderGhostTimer(currFrameDelta);
2973 // ////////////////////////////////////////////////////////////////////////// //
2974 private transient array!MapEntity renderVisibleCids;
2975 private transient array!MapTile renderMidTiles, renderFrontTiles; // normal, with fg
2977 final bool renderSortByDepth (MapEntity oa, MapEntity ob) {
2978 //MapObject oa = MapObject(a);
2979 //MapObject ob = MapObject(b);
2980 auto da = oa.depth, db = ob.depth;
2981 if (da == db) return (oa.objId < ob.objId);
2986 const int RenderEdgePix = 32;
2988 void renderWithOfs (int xofs, int yofs, float currFrameDelta) {
2989 int scale = global.scale;
2992 Video.color = 0xff_ff_ff;
2994 // render cave background
2996 int bgw = levBGImg.tex.width*scale;
2997 int bgh = levBGImg.tex.height*scale;
2998 int bgTW = (tilesWidth*tsz+bgw-1)/bgw;
2999 int bgTH = (tilesHeight*tsz+bgh-1)/bgh;
3000 int bgX0 = max(0, xofs/bgw);
3001 int bgY0 = max(0, yofs/bgh);
3002 int bgX1 = min(bgTW, (xofs+viewWidth+bgw-1)/bgw);
3003 int bgY1 = min(bgTH, (yofs+viewHeight+bgh-1)/bgh);
3004 foreach (int ty; bgY0..bgY1) {
3005 foreach (int tx; bgX0..bgX1) {
3006 int x0 = tx*bgw-xofs;
3007 int y0 = ty*bgh-yofs;
3008 levBGImg.tex.blitAt(x0, y0, scale);
3013 // render background tiles
3014 for (MapBackTile bt = backtiles; bt; bt = bt.next) {
3015 bt.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3018 // collect visible objects
3019 renderVisibleCids.length -= renderVisibleCids.length;
3020 foreach (MapTile mt; miscTileGrid.inRectPix(xofs/scale-RenderEdgePix, yofs/scale-RenderEdgePix, (viewWidth+scale-1)/scale+RenderEdgePix*2, (viewHeight+scale-1)/scale+RenderEdgePix*2, tag:miscTileGrid.nextTag(), precise:false)) {
3021 if (!mt.visible || !mt.isInstanceAlive) continue;
3022 //Video.color = (mt.moveable ? 0xff_7f_00 : 0xff_ff_ff);
3023 //!mt.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3024 renderVisibleCids[$] = mt;
3026 // render objects (and player)
3027 if (player && player.visible) renderVisibleCids[$] = player; // include player in render list
3028 auto ogrid = objGrid;
3029 foreach (MapObject o; ogrid.inRectPix(xofs/scale-RenderEdgePix, yofs/scale-RenderEdgePix, (viewWidth+scale-1)/scale+RenderEdgePix*2, (viewHeight+scale-1)/scale+RenderEdgePix*2, tag:ogrid.nextTag(), precise:false)) {
3030 if (o.visible && o.isInstanceAlive) renderVisibleCids[$] = o;
3033 // collect stationary tiles
3034 int tileX0 = max(0, xofs/tsz);
3035 int tileY0 = max(0, yofs/tsz);
3036 int tileX1 = min(tilesWidth, (xofs+viewWidth+tsz-1)/tsz);
3037 int tileY1 = min(tilesHeight, (yofs+viewHeight+tsz-1)/tsz);
3039 // render backs; collect tile arrays
3040 renderMidTiles.length -= renderMidTiles.length; // don't realloc
3041 renderFrontTiles.length -= renderFrontTiles.length; // don't realloc
3043 foreach (int ty; tileY0..tileY1) {
3044 foreach (int tx; tileX0..tileX1) {
3045 auto tile = getTileAt(tx, ty);
3046 if (tile && tile.visible && tile.isInstanceAlive) {
3047 renderMidTiles[$] = tile;
3048 if (tile.bgfront) renderFrontTiles[$] = tile;
3049 if (tile.bgback) tile.drawWithOfsBack(xofs, yofs, scale, currFrameDelta);
3054 // render "mid" (i.e. normal) tiles
3055 foreach (MapTile tile; renderMidTiles) {
3056 //tile.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3057 renderVisibleCids[$] = tile;
3060 EntityGrid.sortEntList(renderVisibleCids, &renderSortByDepth);
3063 auto depth4Start = 0;
3064 foreach (auto xidx, MapEntity o; renderVisibleCids) {
3071 foreach (auto idx; depth4Start..renderVisibleCids.length; reverse) {
3072 MapEntity o = renderVisibleCids[idx];
3073 //if (idx && renderSortByDepth(o, renderVisibleCids[idx-1])) writeln("VIOLATION!");
3074 o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3077 // render front tile parts (depth 3.5)
3078 foreach (MapTile tile; renderFrontTiles) tile.drawWithOfsFront(xofs, yofs, scale, currFrameDelta);
3080 // render items with depth 3 and less
3081 foreach (auto idx; 0..depth4Start; reverse) {
3082 MapEntity o = renderVisibleCids[idx];
3083 o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3086 renderVisibleCids.length -= renderVisibleCids.length;
3088 // render player post-effects, so cape or jetpack will be correctly rendered on level exit, for example
3089 player.lastDrawWithOfs(xofs, yofs, scale, currFrameDelta);
3091 if (global.config.drawHUD) renderHUD(currFrameDelta);
3092 renderCompass(currFrameDelta);
3094 float osdTimeLeft, osdTimeStart;
3095 string msg = osdGetMessage(out osdTimeLeft, out osdTimeStart);
3097 auto ct = GetTickCount();
3099 sprStore.loadFont('sFontSmall');
3100 auto msgHeight = sprStore.getMultilineTextHeight(msg, msgScale);
3101 int x = Video.screenWidth/2;
3102 int y = Video.screenHeight-64-msgHeight;
3103 auto oldColor = Video.color;
3104 Video.color = 0xff_ff_00;
3105 if (osdTimeLeft < 0.5) {
3106 int alpha = 255-clamp(int(510*osdTimeLeft), 0, 255);
3107 Video.color = Video.color|(alpha<<24);
3108 } else if (ct-osdTimeStart < 0.5) {
3109 osdTimeStart = ct-osdTimeStart;
3110 int alpha = 255-clamp(int(510*osdTimeStart), 0, 255);
3111 Video.color = Video.color|(alpha<<24);
3113 sprStore.renderMultilineTextCentered(x, y, msg, msgScale);
3114 Video.color = oldColor;
3117 if (inWinCutscene) renderWinCutsceneOverlay();
3118 Video.color = 0xff_ff_ff;
3122 // ////////////////////////////////////////////////////////////////////////// //
3123 final class!MapObject findGameObjectClassByName (name aname) {
3124 if (!aname) return none; // just in case
3125 auto co = FindClassByGameObjName(aname);
3127 writeln("***UNKNOWN GAME OBJECT: '", aname, "'");
3130 co = GetClassReplacement(co);
3131 if (!co) FatalError("findGameObjectClassByName: WTF?!");
3132 if (!class!MapObject(co)) FatalError("findGameTileClassByName: object '%n' is not a map object, it is a `%n`", aname, GetClassName(co));
3133 return class!MapObject(co);
3137 final class!MapTile findGameTileClassByName (name aname) {
3138 if (!aname) return none; // just in case
3139 auto co = FindClassByGameObjName(aname);
3140 if (!co) return MapTile; // unknown names will be routed directly to tile object
3141 co = GetClassReplacement(co);
3142 if (!co) FatalError("findGameTileClassByName: WTF?!");
3143 if (!class!MapTile(co)) FatalError("findGameTileClassByName: object '%n' is not a tile, it is a `%n`", aname, GetClassName(co));
3144 return class!MapTile(co);
3148 final MapObject findAnyObjectOfType (name aname) {
3149 if (!aname) return none;
3150 auto cls = FindClassByGameObjName(aname);
3151 if (!cls) return none;
3152 for (auto cid = objGrid.getFirstObject(); cid; cid = objGrid.getNextObject(cid)) {
3153 MapObject obj = objGrid.getObject(MapObject, cid);
3154 if (!obj || obj.spectral || !obj.isInstanceAlive) continue;
3155 if (obj isa cls) return obj;
3161 // ////////////////////////////////////////////////////////////////////////// //
3162 final MapTile CreateMapTile (int xpos, int ypos, name aname) {
3163 if (!aname) FatalError("cannot create typeless tile");
3164 //MapTile tile = SpawnObject(aname == 'oRope' ? MapTileRope : MapTile);
3165 auto tclass = findGameTileClassByName(aname);
3166 if (!tclass) return none;
3167 MapTile tile = SpawnObject(tclass);
3168 tile.global = global;
3170 tile.objName = aname;
3171 tile.objType = aname; // just in case
3174 if (!tile.initialize()) FatalError("cannot create tile '%n'", aname);
3179 final bool isRopePlacedAt (int x, int y) {
3181 foreach (ref auto v; covered) v = false;
3182 foreach (MapTile t; miscTileGrid.inRectPix(x, y-8, 1, 17, precise:false)) {
3183 if (!cbIsRopeTile(t)) continue;
3184 if (t.ix != x) continue;
3185 if (t.iy == y) return true;
3186 foreach (int ty; t.iy..t.iy+8) {
3188 if (d >= 0 && d < covered.length) covered[d] = true;
3191 // check if the whole rope height is completely covered with ropes
3192 foreach (auto v; covered) if (!v) return false;
3197 // won't call `onDestroy()`
3198 final void RemoveMapTile (int tileX, int tileY) {
3199 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
3200 if (tiles[tileX, tileY]) checkWater = true;
3201 delete tiles[tileX, tileY];
3202 tiles[tileX, tileY] = none;
3207 final MapTile MakeMapTile (int mapx, int mapy, name aname/*, optional name sprname*/) {
3208 //if (mapy >= tilesHeight) { writeln("OOM: ", aname, "; ty=", mapy, "; hgt=", tilesHeight); }
3209 if (mapx < 0 || mapx >= tilesWidth || mapy < 0 || mapy >= tilesHeight) return none;
3211 // if we already have rope tile there, there is no reason to add another one
3212 if (aname == 'oRope') {
3213 if (isRopePlacedAt(mapx*16, mapy*16)) {
3214 //writeln("dupe rope (0)!");
3219 auto tile = CreateMapTile(mapx*16, mapy*16, aname);
3220 if (tile.moveable || tile.toSpecialGrid) {
3221 // moveable tiles goes to the separate list
3222 miscTileGrid.insert(tile);
3224 setTileAt(mapx, mapy, tile);
3228 case 'oEntrance': registerEnter(tile); break;
3229 case 'oExit': registerExit(tile); break;
3236 final void MarkTileAsWet (int tileX, int tileY) {
3237 auto t = getTileAt(tileX, tileY);
3238 if (t) t.wet = true;
3242 final MapTile MakeMapTileAtPix (int xpix, int ypix, name aname/*, optional name sprname*/) {
3243 if (xpix%16 == 0 && ypix%16 == 0) return MakeMapTile(xpix/16, ypix/16, aname);
3244 //if (mapx < 0 || mapx >= TilesWidth || mapy < 0 || mapy >= TilesHeight) return none;
3246 // if we already have rope tile there, there is no reason to add another one
3247 if (aname == 'oRope') {
3248 if (isRopePlacedAt(xpix, ypix)) {
3249 //writeln("dupe rope (0)!");
3254 auto tile = CreateMapTile(xpix, ypix, aname);
3255 // non-aligned tiles goes to the special grid
3256 miscTileGrid.insert(tile);
3259 case 'oEntrance': registerEnter(tile); break;
3260 case 'oExit': registerExit(tile); break;
3267 final MapTile MakeMapRopeTileAt (int x0, int y0) {
3268 // if we already have rope tile there, there is no reason to add another one
3269 if (isRopePlacedAt(x0, y0)) {
3270 //writeln("dupe rope (1)!");
3274 auto tile = CreateMapTile(x0, y0, 'oRope');
3275 miscTileGrid.insert(tile);
3281 // ////////////////////////////////////////////////////////////////////////// //
3282 final MapBackTile CreateBgTile (name sprName, optional int atx0, optional int aty0, optional int aw, optional int ah) {
3283 BackTileImage img = bgtileStore[sprName];
3284 auto res = SpawnObject(MapBackTile);
3285 res.global = global;
3288 res.bgtName = sprName;
3289 if (specified_atx0) res.tx0 = atx0;
3290 if (specified_aty0) res.ty0 = aty0;
3291 if (specified_aw) res.w = aw;
3292 if (specified_ah) res.h = ah;
3293 if (!res.initialize()) FatalError("cannot create background tile with image '%n'", sprName);
3298 // ////////////////////////////////////////////////////////////////////////// //
3300 background The background asset from which the new tile will be extracted.
3301 left The x coordinate of the left of the new tile, relative to the background asset's top left corner.
3302 top The y coordinate of the top of the new tile, relative to the background assets top left corner.
3303 width The width of the tile.
3304 height The height of the tile.
3305 x The x position in the room to place the tile.
3306 y The y position in the room to place the tile.
3307 depth The depth at which to place the tile.
3309 final void MakeMapBackTile (name bgname, int left, int top, int width, int height, int x, int y, int depth) {
3310 if (width < 1 || height < 1 || !bgname) return;
3311 auto bgt = bgtileStore[bgname];
3312 if (!bgt) FatalError("cannot load background '%n'", bgname);
3313 MapBackTile bt = SpawnObject(MapBackTile);
3316 bt.objName = bgname;
3318 bt.bgtName = bgname;
3326 // find a place for it
3331 // back tiles with the highest depth should come first
3332 MapBackTile ct = backtiles, cprev = none;
3333 while (ct && depth <= ct.depth) { cprev = ct; ct = ct.next; }
3336 bt.next = cprev.next;
3339 bt.next = backtiles;
3345 // ////////////////////////////////////////////////////////////////////////// //
3346 final MapObject SpawnMapObjectWithClass (class!MapObject oclass) {
3347 if (!oclass) return none;
3349 MapObject obj = SpawnObject(oclass);
3350 if (!obj) FatalError("cannot create map object '%n'", GetClassName(oclass));
3352 //if (aname == 'oShells' || aname == 'oShellSingle' || aname == 'oRopePile') writeln("*** CREATED '", aname, "'");
3354 obj.global = global;
3361 final MapObject SpawnMapObject (name aname) {
3362 if (!aname) return none;
3363 return SpawnMapObjectWithClass(findGameObjectClassByName(aname));
3367 final MapObject PutSpawnedMapObject (int x, int y, MapObject obj) {
3368 if (!obj /*|| obj.global || obj.level*/) return none; // oops
3372 if (!obj.initialize()) { delete obj; return none; } // not fatal
3380 final MapObject MakeMapObject (int x, int y, name aname) {
3381 MapObject obj = SpawnMapObject(aname);
3382 obj = PutSpawnedMapObject(x, y, obj);
3387 // ////////////////////////////////////////////////////////////////////////// //
3388 int winCutSceneTimer = -1;
3389 int winVolcanoTimer = -1;
3390 int winCutScenePhase = 0;
3391 int winSceneDrawStatus = 0;
3392 int winMoneyCount = 0;
3394 bool winFadeOut = false;
3395 int winFadeLevel = 0;
3396 int winCutsceneSkip = 0; // 1: waiting for pay release; 2: pay released, do skip
3399 void startWinCutscene () {
3400 winCutsceneSkip = 0;
3401 isKeyPressed(GameConfig::Key.Pay);
3402 isKeyReleased(GameConfig::Key.Pay);
3406 auto olddel = ImmediateDelete;
3407 ImmediateDelete = false;
3413 addBackgroundGfxDetails();
3415 levBGImgName = 'bgCave';
3416 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
3418 blockWaterChecking = true;
3424 ImmediateDelete = olddel;
3425 CollectGarbage(true); // destroy delayed objects too
3427 if (dumpGridStats) {
3428 miscTileGrid.dumpStats();
3429 objGrid.dumpStats();
3432 playerExited = false; // just in case
3440 winCutSceneTimer = -1;
3441 winCutScenePhase = 0;
3444 if (global.config.gameMode != GameConfig::GameMode.Vanilla) {
3445 if (global.config.bizarre) {
3446 global.yasmScore = 1;
3447 global.config.bizarrePlusTitle = true;
3450 array!MapTile toReplace;
3451 forEachTile(delegate bool (MapTile t) {
3452 if (t.objType == 'oGTemple' ||
3453 t.objType == 'oIce' ||
3454 t.objType == 'oDark' ||
3455 t.objType == 'oBrick' ||
3456 t.objType == 'oLush')
3463 foreach (MapTile t; miscTileGrid.allObjects()) {
3464 if (t.objType == 'oGTemple' ||
3465 t.objType == 'oIce' ||
3466 t.objType == 'oDark' ||
3467 t.objType == 'oBrick' ||
3468 t.objType == 'oLush')
3474 foreach (MapTile t; toReplace) {
3476 t.cleanDeath = true;
3477 if (rand(1,120) == 1) instance_change(oGTemple, false);
3478 else if (rand(1,100) == 1) instance_change(oIce, false);
3479 else if (rand(1,90) == 1) instance_change(oDark, false);
3480 else if (rand(1,80) == 1) instance_change(oBrick, false);
3481 else if (rand(1,70) == 1) instance_change(oLush, false);
3489 if (rand(1,5) == 1) instance_change(oLush, false);
3494 //!instance_create(0, 0, oBricks);
3496 //shakeToggle = false;
3497 //oPDummy.status = 2;
3502 if (global.kaliPunish >= 2) {
3503 instance_create(oPDummy.x, oPDummy.y+2, oBall2);
3504 obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
3506 obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
3508 obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
3510 obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
3518 void startWinCutsceneVolcano () {
3519 auto olddel = ImmediateDelete;
3520 ImmediateDelete = false;
3525 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
3527 blockWaterChecking = true;
3529 ImmediateDelete = olddel;
3530 CollectGarbage(true); // destroy delayed objects too
3532 spawnPlayerAt(2*16+8, 11*16+8);
3533 player.dir = MapEntity::Dir.Right;
3535 playerExited = false; // just in case
3543 winCutSceneTimer = -1;
3544 winCutScenePhase = 0;
3546 MakeMapTile(0, 0, 'oEnd2BG');
3547 realViewStart.x = 0;
3548 realViewStart.y = 0;
3557 player.dead = false;
3558 player.active = true;
3559 player.visible = false;
3560 player.fltx = 320/2;
3565 void startWinCutsceneWinFall () {
3566 auto olddel = ImmediateDelete;
3567 ImmediateDelete = false;
3572 setMenuTilesVisible(false);
3574 //addBackgroundGfxDetails();
3577 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
3579 blockWaterChecking = true;
3585 ImmediateDelete = olddel;
3586 CollectGarbage(true); // destroy delayed objects too
3588 if (dumpGridStats) {
3589 miscTileGrid.dumpStats();
3590 objGrid.dumpStats();
3593 playerExited = false; // just in case
3601 winCutSceneTimer = -1;
3602 winCutScenePhase = 0;
3604 player.dead = false;
3605 player.active = true;
3606 player.visible = false;
3607 player.fltx = 320/2;
3610 winSceneDrawStatus = 0;
3618 void setGameOver () {
3619 if (inWinCutscene) player.visible = false;
3621 if (inWinCutscene > 0) {
3624 winSceneDrawStatus = 8;
3629 MapTile findEndPlatTile () {
3630 return forEachTile(delegate bool (MapTile t) { return (t isa MapTileEndPlat); });
3634 MapObject findBigTreasure () {
3635 return forEachObject(delegate bool (MapObject o) { return (o isa MapObjectBigTreasure); });
3639 void setMenuTilesVisible (bool vis) {
3641 forEachTile(delegate bool (MapTile t) {
3642 if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
3643 t.invisible = false;
3648 forEachTile(delegate bool (MapTile t) {
3649 if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
3658 void winCutscenePlayerControl (PlayerPawn plr) {
3659 auto payPress = isKeyPressed(GameConfig::Key.Pay);
3660 auto payRelease = isKeyReleased(GameConfig::Key.Pay);
3662 switch (winCutsceneSkip) {
3663 case 0: // nothing was pressed
3664 if (payPress) winCutsceneSkip = 1;
3666 case 1: // waiting for pay release
3667 if (payRelease) winCutsceneSkip = 2;
3669 case 2: // pay released, do skip
3675 auto sname = plr.getSprite().Name;
3676 if (sname != 'sStandLeft' && sname != 'sDamselLeft' && sname != 'sTunnelLeft') {
3677 if (x >= 448 + 8) {
3679 if (global.isDamsel) sprite_index = sDamselLeft;
3680 else if (global.isTunnelMan) sprite_index = sTunnelLeft;
3681 else sprite_index = sStandLeft;
3686 } else if (status == LAVA) {
3689 } else if (status == LAVA+1) {
3690 instance_create(oEndPlat.x+rand(0,80), 192+32, oBurn);
3694 // first winning room
3695 if (inWinCutscene == 1) {
3696 if (plr.ix < 448+8) {
3701 // waiting for chest to open
3702 if (winCutScenePhase == 0) {
3703 winCutSceneTimer = 120/2;
3704 winCutScenePhase = 1;
3709 if (winCutScenePhase == 1) {
3710 if (--winCutSceneTimer == 0) {
3711 winCutScenePhase = 2;
3712 winCutSceneTimer = 20;
3713 forEachObject(delegate bool (MapObject o) {
3714 if (o isa MapObjectBigChest) {
3715 o.setSprite(global.config.gameMode == GameConfig::GameMode.Vanilla ? 'sBigChestOpen' : 'sBigChestOpen2');
3716 auto treasure = MakeMapObject(o.ix, o.iy, 'oBigTreasure');
3720 o.playSound('sndClick');
3721 //!!!if (global.config.gameMode != GameConfig::GameMode.Vanilla) scrSprayGems(oBigChest.x+24, oBigChest.y+24);
3731 if (winCutScenePhase == 2) {
3732 if (--winCutSceneTimer == 0) {
3733 winCutScenePhase = 3;
3734 winCutSceneTimer = 50;
3740 if (winCutScenePhase == 3) {
3741 auto ep = findEndPlatTile();
3742 if (ep) MakeMapObject(ep.ix+global.randOther(0, 80), /*ep.iy*/192+32, 'oBurn');
3743 if (--winCutSceneTimer == 0) {
3744 winCutScenePhase = 4;
3745 winCutSceneTimer = 10;
3746 if (ep) MakeMapObject(ep.ix, ep.iy+30, 'oLavaSpray');
3752 // lava pump first accel
3753 if (winCutScenePhase == 4) {
3754 if (--winCutSceneTimer == 0) {
3755 forEachObject(delegate bool (MapObject o) {
3756 if (o isa MapObjectLavaSpray) o.yAcc = -0.1;
3762 // lava pump complete
3763 if (winCutScenePhase == 5) {
3764 if (--winCutSceneTimer == 0) {
3765 //if (oLavaSpray) oLavaSpray.yAcc = -0.1;
3766 startWinCutsceneVolcano();
3775 if (inWinCutscene == 2) {
3779 if (winCutScenePhase == 0) {
3780 winCutSceneTimer = 50;
3781 winCutScenePhase = 1;
3782 winVolcanoTimer = 10;
3786 if (winVolcanoTimer > 0) {
3787 if (--winVolcanoTimer == 0) {
3788 MakeMapObject(224+global.randOther(0,48), 144+global.randOther(0,8), 'oVolcanoFlame');
3789 winVolcanoTimer = global.randOther(10, 20);
3794 if (winCutScenePhase == 1) {
3795 if (--winCutSceneTimer == 0) {
3796 winCutSceneTimer = 30;
3797 winCutScenePhase = 2;
3798 auto sil = MakeMapObject(240, 132, 'oPlayerSil');
3806 if (winCutScenePhase == 2) {
3807 if (--winCutSceneTimer == 0) {
3808 winCutScenePhase = 3;
3809 auto sil = MakeMapObject(240, 132, 'oTreasureSil');
3819 // winning camel room
3820 if (inWinCutscene == 3) {
3821 if (!plr.visible) plr.flty = -32;
3824 if (winCutScenePhase == 0) {
3825 winCutSceneTimer = 50;
3826 winCutScenePhase = 1;
3831 if (winCutScenePhase == 1) {
3832 if (--winCutSceneTimer == 0) {
3833 winCutSceneTimer = 50;
3834 winCutScenePhase = 2;
3835 plr.playSound('sndPFall');
3838 plr.status == MapObject::FALLING;
3839 global.plife += 99; // just in case
3844 if (winCutScenePhase == 2) {
3845 if (plr.status == MapObject::STUNNED || plr.stunned) {
3849 auto treasure = MakeMapObject(144+16+8, -32, 'oBigTreasure');
3850 if (treasure) treasure.depth = 1;
3851 winCutScenePhase = 3;
3853 plr.playSound('sndTFall');
3858 if (winCutScenePhase == 3) {
3859 if (plr.status != MapObject::STUNNED && !plr.stunned) {
3860 auto bt = findBigTreasure();
3864 //plr.status = MapObject::JUMPING;
3866 plr.kJumpPressed = true;
3867 winCutScenePhase = 4;
3868 winCutSceneTimer = 50;
3875 if (winCutScenePhase == 4) {
3876 if (--winCutSceneTimer == 0) {
3877 setMenuTilesVisible(true);
3878 winCutScenePhase = 5;
3879 winSceneDrawStatus = 1;
3880 global.playMusic('musVictory', loop:false);
3881 winCutSceneTimer = 50;
3886 if (winCutScenePhase == 5) {
3887 if (winSceneDrawStatus == 3) {
3888 int money = stats.money;
3889 if (winMoneyCount < money) {
3890 if (money-winMoneyCount > 1000) {
3891 winMoneyCount += 1000;
3892 } else if (money-winMoneyCount > 100) {
3893 winMoneyCount += 100;
3894 } else if (money-winMoneyCount > 10) {
3895 winMoneyCount += 10;
3900 if (winMoneyCount >= money) {
3901 winMoneyCount = money;
3902 ++winSceneDrawStatus;
3907 if (winSceneDrawStatus == 7) {
3910 if (winFadeLevel >= 255) {
3911 ++winSceneDrawStatus;
3912 winCutSceneTimer = 30*30;
3917 if (winSceneDrawStatus == 8) {
3918 if (--winCutSceneTimer == 0) {
3924 if (--winCutSceneTimer == 0) {
3925 ++winSceneDrawStatus;
3926 winCutSceneTimer = 50;
3935 // ////////////////////////////////////////////////////////////////////////// //
3936 void renderWinCutsceneOverlay () {
3937 if (inWinCutscene == 3) {
3938 if (winSceneDrawStatus > 0) {
3939 Video.color = 0xff_ff_ff;
3940 sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3941 //draw_set_color(txtCol);
3942 drawTextAt(64, 32, "YOU MADE IT!");
3944 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
3945 if (global.config.gameMode == GameConfig::GameMode.Vanilla) {
3946 Video.color = 0xff_ff_00; //draw_set_color(c_yellow);
3947 drawTextAt(64, 48, "Classic Mode done!");
3949 Video.color = 0x00_80_80; //draw_set_color(c_teal);
3950 if (global.config.bizarrePlus) drawTextAt(64, 48, "Bizarre Mode Plus done!");
3951 else drawTextAt(64, 48, "Bizarre Mode done!");
3952 //draw_set_color(c_white);
3954 if (!global.usedShortcut) {
3955 Video.color = 0xc0_c0_c0; //draw_set_color(c_silver);
3956 drawTextAt(64, 56, "No shortcuts used!");
3957 //draw_set_color(c_yellow);
3961 if (winSceneDrawStatus > 1) {
3962 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
3963 //draw_set_color(txtCol);
3964 Video.color = 0xff_ff_ff;
3965 drawTextAt(64, 64, "FINAL SCORE:");
3968 if (winSceneDrawStatus > 2) {
3969 sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
3970 Video.color = 0xff_ff_ff; //draw_set_color(c_white);
3971 drawTextAt(64, 72, va("$%d", winMoneyCount));
3974 if (winSceneDrawStatus > 4) {
3975 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
3976 Video.color = 0xff_ff_ff; //draw_set_color(c_white);
3977 drawTextAt(64, 96, va("Time: %s", time2str(winTime)));
3979 draw_set_color(c_white);
3980 if (s < 10) draw_text(96+24, 96, string(m) + ":0" + string(s));
3981 else draw_text(96+24, 96, string(m) + ":" + string(s));
3985 if (winSceneDrawStatus > 5) {
3986 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
3987 Video.color = 0xff_ff_ff; //draw_set_color(txtCol);
3988 drawTextAt(64, 96+8, "Kills: ");
3989 Video.color = 0xff_ff_ff; //draw_set_color(c_white);
3990 drawTextAt(96+24, 96+8, va("%s", stats.countKills()));
3993 if (winSceneDrawStatus > 6) {
3994 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
3995 Video.color = 0xff_ff_ff; //draw_set_color(txtCol);
3996 drawTextAt(64, 96+16, "Saves: ");
3997 Video.color = 0xff_ff_ff; //draw_set_color(c_white);
3998 drawTextAt(96+24, 96+16, va("%s", stats.damselsSaved));
4002 Video.color = (255-clamp(winFadeLevel, 0, 255))<<24;
4003 Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
4006 if (winSceneDrawStatus == 8) {
4007 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4008 Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4010 if (global.config.gameMode == GameConfig::GameMode.Vanilla) {
4011 Video.color = 0xff_ff_00; //draw_set_color(c_yellow);
4012 lastString = "YOU SHALL BE REMEMBERED AS A HERO.";
4014 Video.color = 0x00_ff_ff;
4015 if (global.config.bizarrePlus) lastString = "ANOTHER LEGENDARY ADVENTURE!";
4016 else lastString = "YOUR DISCOVERIES WILL BE CELEBRATED!";
4018 auto strLen = lastString.length*8;
4020 n = trunc(ceil(n/2.0));
4021 drawTextAt(n, 116, lastString);
4027 // ////////////////////////////////////////////////////////////////////////// //
4028 #include "roomTitle.vc"
4029 #include "roomTrans1.vc"
4030 #include "roomTrans2.vc"
4031 #include "roomTrans3.vc"
4032 #include "roomTrans4.vc"
4033 #include "roomOlmec.vc"
4034 #include "roomEnd.vc"
4037 // ////////////////////////////////////////////////////////////////////////// //
4038 #include "packages/Generator/loadRoomGens.vc"
4039 #include "packages/Generator/loadEntityGens.vc"