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 () {
201 player.removeBallAndChain(temp:false);
202 auto hi = player.holdItem;
203 player.holdItem = none;
204 if (hi) hi.instanceRemove();
205 hi = player.pickedItem;
206 player.pickedItem = none;
207 if (hi) hi.instanceRemove();
211 stats.clearGameTotals();
212 if (global.startMoney > 0) stats.setMoneyCheat();
213 stats.setMoney(global.startMoney);
214 //writeln("level=", global.currLevel, "; lt=", global.levelType);
218 // complement function to `restart game`
219 void generateNormalLevel () {
221 centerViewAtPlayer();
225 // ////////////////////////////////////////////////////////////////////////// //
226 // generate angry shopkeeper at exit if murderer or thief
227 void generateAngryShopkeepers () {
228 if (global.murderer || global.thiefLevel > 0) {
229 foreach (MapTile e; allExits) {
230 auto obj = MonsterShopkeeper(MakeMapObject(e.ix, e.iy, 'oShopkeeper'));
232 obj.style = 'Bounty Hunter';
233 obj.status = MapObject::PATROL;
240 // ////////////////////////////////////////////////////////////////////////// //
241 final void resetRoomBounds () {
244 viewMax.x = tilesWidth*16;
245 viewMax.y = tilesHeight*16;
246 // Great Lake is bottomless (nope)
247 //if (global.lake) viewMax.y -= 16;
248 //writeln("ROOM BOUNDS: (", viewMax.x, "x", viewMax.y, ")");
252 final void setRoomBounds (int x0, int y0, int x1, int y1) {
260 // ////////////////////////////////////////////////////////////////////////// //
263 float timeout; // seconds
264 float starttime; // for active
265 bool active; // true: timeout is `GetTickCount()` dismissing time
268 array!OSDMessage msglist; // [0]: current one
271 private final void osdCheckTimeouts () {
272 auto stt = GetTickCount();
273 while (msglist.length) {
274 if (!msglist[0].active) {
275 msglist[0].active = true;
276 msglist[0].starttime = stt;
278 if (msglist[0].starttime+msglist[0].timeout >= stt) break;
284 final bool osdHasMessage () {
286 return (msglist.length > 0);
290 final string osdGetMessage (out float timeLeft, out float timeStart) {
292 if (msglist.length == 0) { timeLeft = 0; return ""; }
293 auto stt = GetTickCount();
294 timeStart = msglist[0].starttime;
295 timeLeft = fmax(0, msglist[0].starttime+msglist[0].timeout-stt);
296 return msglist[0].msg;
300 final void osdClear () {
301 msglist.length -= msglist.length;
305 final void osdMessage (string msg, optional float timeout, optional bool urgent) {
307 if (!specified_timeout) timeout = 3.33;
308 // special message for shops
309 if (timeout == -666) {
311 if (msglist.length && msglist[0].msg == msg) return;
312 if (msglist.length == 0 || msglist[0].msg != msg) {
315 msglist[0].msg = msg;
317 msglist[0].active = false;
318 msglist[0].timeout = 3.33;
322 if (timeout < 0.1) return;
323 timeout = fmax(1.0, timeout);
324 //writeln("OSD: ", msg);
325 // find existing one, and bring it to the top
327 for (; oldidx < msglist.length; ++oldidx) {
328 if (msglist[oldidx].msg == msg) break; // i found her!
331 if (oldidx < msglist.length) {
332 // yeah, move duplicate to the top
333 msglist[oldidx].timeout = fmax(timeout, msglist[oldidx].timeout);
334 msglist[oldidx].active = false;
335 if (urgent && oldidx != 0) {
336 timeout = msglist[oldidx].timeout;
337 msglist.remove(oldidx);
339 msglist[0].msg = msg;
340 msglist[0].timeout = timeout;
341 msglist[0].active = false;
345 msglist[0].msg = msg;
346 msglist[0].timeout = timeout;
347 msglist[0].active = false;
351 msglist[$-1].msg = msg;
352 msglist[$-1].timeout = timeout;
353 msglist[$-1].active = false;
359 // ////////////////////////////////////////////////////////////////////////// //
360 void setup (GameGlobal aGlobal, SpriteStore aSprStore, BackTileStore aBGTileStore) {
362 sprStore = aSprStore;
363 bgtileStore = aBGTileStore;
365 lg = SpawnObject(LevelGen);
369 miscTileGrid = SpawnObject(EntityGrid);
370 miscTileGrid.setup(0, 0, MaxTilesWidth*16, MaxTilesHeight*16, MapTile);
371 //miscTileGrid.ownObjects = true;
373 objGrid = SpawnObject(EntityGrid);
374 objGrid.setup(0, 0, MaxTilesWidth*16, MaxTilesHeight*16, MapObject);
378 // stores should be set
381 levBGImg = bgtileStore[levBGImgName];
382 foreach (int y; 0..MaxTilesHeight) {
383 foreach (int x; 0..MaxTilesWidth) {
384 if (tiles[x, y]) tiles[x, y].onLoaded();
387 foreach (MapEntity o; miscTileGrid.allObjects()) o.onLoaded();
388 foreach (MapEntity o; objGrid.allObjects()) o.onLoaded();
389 for (MapBackTile bt = backtiles; bt; bt = bt.next) bt.onLoaded();
390 if (player) player.onLoaded();
392 if (msglist.length) {
393 msglist[0].active = false;
394 msglist[0].timeout = 0.200;
400 // ////////////////////////////////////////////////////////////////////////// //
401 void pickedSpectacles () {
402 foreach (int y; 0..tilesHeight) {
403 foreach (int x; 0..tilesWidth) {
404 MapTile t = tiles[x, y];
405 if (t && t.isInstanceAlive) t.onGotSpectacles();
408 foreach (MapTile t; miscTileGrid.allObjects()) {
409 if (t.isInstanceAlive) t.onGotSpectacles();
414 // ////////////////////////////////////////////////////////////////////////// //
415 #include "rgentile.vc"
416 #include "rgenobj.vc"
419 void onLevelExited () {
420 if (isNormalLevel()) stats.maxLevelComplete = max(stats.maxLevelComplete, global.currLevel);
421 if (onLevelExitedCB) onLevelExitedCB();
422 if (levelKind == LevelKind.Transition) {
423 if (global.thiefLevel > 0) global.thiefLevel -= 1;
424 //orig dbg:if (global.currLevel == 1) global.currLevel += firstLevelSkip; else global.currLevel += levelSkip;
425 global.currLevel += 1;
428 if (lg.finalBossLevel) {
431 // add money for big idol
432 player.addScore(50000);
435 generateTransitionLevel();
438 centerViewAtPlayer();
442 void onOlmecDead (MapObject o) {
443 writeln("*** OLMEC IS DEAD!");
444 foreach (MapTile t; allExits) {
447 auto st = checkTileAtPoint(t.ix+8, t.iy+16);
449 st = MakeMapTile(t.ix/16, t.iy/16+1, 'oTemple');
452 st.invincible = true;
458 void generateLevelMessages () {
459 if (global.darkLevel) {
460 if (global.hasCrown) osdMessage("THE HEDJET SHINES BRIGHTLY.");
461 else if (global.config.scumDarkness < 2) osdMessage("I CAN'T SEE A THING!");
463 else global.message = "";
464 if (global.hasCrown) global.message2 = "";
465 else if (global.scumDarkness < 2) global.message2 = "I'D BETTER USE THESE FLARES!";
466 else global.message2 = "";
467 global.messageTimer = 200;
472 if (global.blackMarket) osdMessage("WELCOME TO THE BLACK MARKET!");
474 if (global.cemetary) osdMessage("THE DEAD ARE RESTLESS!");
475 if (global.lake) osdMessage("I CAN HEAR RUSHING WATER...");
477 if (global.snakePit) osdMessage("I HEAR SNAKES... I HATE SNAKES!");
478 if (global.yetiLair) osdMessage("IT SMELLS LIKE WET FUR IN HERE!");
479 if (global.alienCraft) osdMessage("THERE'S A PSYCHIC PRESENCE HERE!");
480 if (global.cityOfGold) {
481 if (true /*hasOlmec()*/) osdMessage("IT'S THE LEGENDARY CITY OF GOLD!");
484 if (global.sacrificePit) osdMessage("I CAN HEAR PRAYERS TO KALI!");
488 final MapObject debugSpawnObjectWithClass (class!MapObject oclass, optional bool playerDir) {
489 if (!oclass) return none;
491 bool canLeft = !isSolidAtPoint(player.ix-8, player.iy);
492 bool canRight = !isSolidAtPoint(player.ix+16, player.iy);
493 if (!canLeft && !canRight) return none;
494 if (canLeft && canRight) {
496 dx = (player.dir == MapObject::Dir.Left ? -16 : 16);
501 dx = (canLeft ? -16 : 16);
503 auto obj = SpawnMapObjectWithClass(oclass);
504 if (obj isa MapEnemy) { dx -= 8; dy -= 8; }
505 if (obj) PutSpawnedMapObject(player.ix+dx, player.iy+dy, obj);
510 final MapObject debugSpawnObject (name aname) {
511 if (!aname) return none;
512 return debugSpawnObjectWithClass(findGameObjectClassByName(aname));
516 // `global.currLevel` is the new level
517 void generateTransitionLevel () {
521 global.setMusicPitch(1.0);
522 switch (global.config.transitionMusicMode) {
523 case GameConfig::MusicMode.Silent: global.stopMusic(); break;
524 case GameConfig::MusicMode.Restart: global.restartMusic(); break;
525 case GameConfig::MusicMode.DontTouch: break;
528 levelKind = LevelKind.Transition;
530 auto olddel = ImmediateDelete;
531 ImmediateDelete = false;
535 if (global.currLevel < 4) createTrans1Room();
536 else if (global.currLevel == 4) createTrans1xRoom();
537 else if (global.currLevel < 8) createTrans2Room();
538 else if (global.currLevel == 8) createTrans2xRoom();
539 else if (global.currLevel < 12) createTrans3Room();
540 else if (global.currLevel == 12) createTrans3xRoom();
541 else if (global.currLevel < 16) createTrans4Room();
542 else if (global.currLevel == 16) createTrans4Room();
543 else createTrans1Room(); //???
548 addBackgroundGfxDetails();
549 levBGImgName = 'bgCave';
550 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
552 blockWaterChecking = true;
556 if (damselSaved > 0) {
557 MakeMapObject(176+8, 176+8, 'oDamselKiss');
558 global.plife += damselSaved; // if player skipped transition cutscene
564 ImmediateDelete = olddel;
565 CollectGarbage(true); // destroy delayed objects too
568 miscTileGrid.dumpStats();
572 playerExited = false; // just in case
577 //global.playMusic(lg.musicName);
581 void generateLevel () {
582 global.setMusicPitch(1.0);
583 stats.clearLevelTotals();
585 levelKind = LevelKind.Normal;
592 //writeln("tw:", tilesWidth, "; th:", tilesHeight);
594 auto olddel = ImmediateDelete;
595 ImmediateDelete = false;
599 if (lg.finalBossLevel) {
600 blockWaterChecking = true;
604 // if transition cutscene was skipped...
605 if (damselSaved > 0) global.plife += damselSaved; // if player skipped transition cutscene
609 startRoomX = lg.startRoomX;
610 startRoomY = lg.startRoomY;
611 endRoomX = lg.endRoomX;
612 endRoomY = lg.endRoomY;
613 addBackgroundGfxDetails();
614 foreach (int y; 0..tilesHeight) {
615 foreach (int x; 0..tilesWidth) {
621 levBGImgName = lg.bgImgName;
622 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
624 if (global.allowAngryShopkeepers) generateAngryShopkeepers();
626 lg.generateEntities();
628 // add box of flares to dark level
629 if (global.darkLevel && allEnters.length) {
630 auto enter = allEnters[0];
631 int x = enter.ix, y = enter.iy;
632 if (!isSolidAtPoint(x-16, y)) MakeMapObject(x-16+8, y+8, 'oFlareCrate');
633 else if (!isSolidAtPoint(x+16, y)) MakeMapObject(x+16+8, y+8, 'oFlareCrate');
634 else MakeMapObject(x+8, y+8, 'oFlareCrate');
637 //scrGenerateEntities();
638 //foreach (; 0..2) scrGenerateEntities();
640 writeln(countObjects, " alive objects inserted");
641 writeln(countBackTiles, " background tiles inserted");
643 if (!player) FatalError("player pawn is not spawned");
645 if (lg.finalBossLevel) {
646 blockWaterChecking = true;
648 blockWaterChecking = false;
655 ImmediateDelete = olddel;
656 CollectGarbage(true); // destroy delayed objects too
659 miscTileGrid.dumpStats();
663 playerExited = false; // just in case
665 levelMoneyStart = stats.money;
668 generateLevelMessages();
673 if (lastMusicName != lg.musicName) {
674 global.playMusic(lg.musicName);
675 //writeln("!<", lastMusicName, ">:<", lg.musicName, ">");
677 //writeln("MM: ", global.config.nextLevelMusicMode);
678 switch (global.config.nextLevelMusicMode) {
679 case GameConfig::MusicMode.Silent: global.stopMusic(); break; // the thing that should not be
680 case GameConfig::MusicMode.Restart: global.restartMusic(); break;
681 case GameConfig::MusicMode.DontTouch:
682 if (global.config.transitionMusicMode == GameConfig::MusicMode.Silent) {
683 global.playMusic(lg.musicName);
688 lastMusicName = lg.musicName;
689 //global.playMusic(lg.musicName);
695 // ////////////////////////////////////////////////////////////////////////// //
696 int currKeys, nextKeys;
697 int pressedKeysQ, releasedKeysQ;
698 int keysPressed, keysReleased = -1;
701 struct SavedKeyState {
702 int currKeys, nextKeys;
703 int pressedKeysQ, releasedKeysQ;
704 int keysPressed, keysReleased;
706 int roomSeed, otherSeed;
710 // for saving/replaying
711 final void keysSaveState (out SavedKeyState ks) {
712 ks.currKeys = currKeys;
713 ks.nextKeys = nextKeys;
714 ks.pressedKeysQ = pressedKeysQ;
715 ks.releasedKeysQ = releasedKeysQ;
716 ks.keysPressed = keysPressed;
717 ks.keysReleased = keysReleased;
720 // for saving/replaying
721 final void keysRestoreState (const ref SavedKeyState ks) {
722 currKeys = ks.currKeys;
723 nextKeys = ks.nextKeys;
724 pressedKeysQ = ks.pressedKeysQ;
725 releasedKeysQ = ks.releasedKeysQ;
726 keysPressed = ks.keysPressed;
727 keysReleased = ks.keysReleased;
731 final void keysNextFrame () {
736 final void clearKeys () {
746 final void onKey (int code, bool down) {
751 if (keysReleased&code) {
753 keysReleased &= ~code;
754 pressedKeysQ |= code;
758 if (keysPressed&code) {
759 keysReleased |= code;
760 keysPressed &= ~code;
761 releasedKeysQ |= code;
766 final bool isKeyDown (int code) {
767 return !!(currKeys&code);
770 final bool isKeyPressed (int code) {
771 bool res = !!(pressedKeysQ&code);
772 pressedKeysQ &= ~code;
776 final bool isKeyReleased (int code) {
777 bool res = !!(releasedKeysQ&code);
778 releasedKeysQ &= ~code;
783 final void clearKeysPressRelease () {
784 keysPressed = default.keysPressed;
785 keysReleased = default.keysReleased;
786 pressedKeysQ = default.pressedKeysQ;
787 releasedKeysQ = default.releasedKeysQ;
793 // ////////////////////////////////////////////////////////////////////////// //
794 final void registerEnter (MapTile t) {
801 final void registerExit (MapTile t) {
808 final bool isYAtEntranceRow (int py) {
810 foreach (MapTile t; allEnters) if (t.iy == py) return true;
815 final int calcNearestEnterDist (int px, int py) {
816 if (allEnters.length == 0) return int.max;
817 int curdistsq = int.max;
818 foreach (MapTile t; allEnters) {
819 int xc = px-t.xCenter, yc = py-t.yCenter;
820 int distsq = xc*xc+yc*yc;
821 if (distsq < curdistsq) curdistsq = distsq;
823 return round(sqrt(curdistsq));
827 final int calcNearestExitDist (int px, int py) {
828 if (allExits.length == 0) return int.max;
829 int curdistsq = int.max;
830 foreach (MapTile t; allExits) {
831 int xc = px-t.xCenter, yc = py-t.yCenter;
832 int distsq = xc*xc+yc*yc;
833 if (distsq < curdistsq) curdistsq = distsq;
835 return round(sqrt(curdistsq));
839 // ////////////////////////////////////////////////////////////////////////// //
840 final void clearForTransition () {
841 auto olddel = ImmediateDelete;
842 ImmediateDelete = false;
845 ImmediateDelete = olddel;
846 CollectGarbage(true); // destroy delayed objects too
850 final void clearTiles () {
853 allEnters.length -= allEnters.length; // don't deallocate
854 allExits.length -= allExits.length; // don't deallocate
855 lavatiles.length -= lavatiles.length;
856 foreach (ref auto tile; tiles) delete tile;
857 if (dumpGridStats) { if (miscTileGrid.getFirstObject()) miscTileGrid.dumpStats(); }
858 miscTileGrid.removeAllObjects(true); // and destroy
860 MapBackTile t = backtiles;
868 // ////////////////////////////////////////////////////////////////////////// //
869 final int countObjects () {
870 return objGrid.countObjects();
873 final int countBackTiles () {
875 for (MapBackTile bt = backtiles; bt; bt = bt.next) ++res;
879 final void clearObjects () {
880 // don't kill objects player is holding
882 if (player.pickedItem isa ItemBall) {
883 player.pickedItem.instanceRemove();
884 player.pickedItem = none;
886 if (player.pickedItem && player.pickedItem.grid) {
887 player.pickedItem.grid.remove(player.pickedItem.gridId);
888 writeln("secured pocket item '", GetClassName(player.pickedItem.Class), "'");
889 //player.pickedItem.grid = none;
891 if (player.holdItem isa ItemBall) {
892 player.removeBallAndChain(temp:true);
893 player.holdItem.instanceRemove();
894 player.holdItem = none;
896 if (player.holdItem && player.holdItem.grid) {
897 player.holdItem.grid.remove(player.holdItem.gridId);
898 writeln("secured held item '", GetClassName(player.holdItem.Class), "'");
899 //player.holdItem.grid = none;
903 int count = objGrid.countObjects();
904 if (dumpGridStats) { if (objGrid.getFirstObject()) objGrid.dumpStats(); }
905 objGrid.removeAllObjects(true); // and destroy
906 if (count > 0) writeln(count, " objects destroyed");
907 ballObjects.length = 0;
908 lastUsedObjectId = 0;
912 final void insertObject (MapObject o) {
914 if (o.grid) FatalError("cannot put object into level twice");
915 o.objId = ++lastUsedObjectId;
917 // ball from ball-and-chain
918 if (o isa ItemBall) {
920 foreach (MapObject bo; ballObjects) if (bo == o) { found = true; break; }
921 if (!found) ballObjects[$] = o;
928 final void spawnPlayerAt (int x, int y) {
929 // if we have no player, spawn new one
930 // otherwise this just a level transition, so simply reposition him
932 // don't add player to object list, as it has very separate processing anyway
933 player = SpawnObject(PlayerPawn);
934 player.global = global;
936 if (!player.initialize()) {
938 FatalError("something is wrong with player initialization");
944 player.saveInterpData();
946 if (player.mustBeChained || global.config.scumBallAndChain) player.spawnBallAndChain();
947 playerExited = false;
948 if (global.config.startWithKapala) global.hasKapala = true;
949 centerViewAtPlayer();
950 // reinsert player items into grid
951 if (player.pickedItem) objGrid.insert(player.pickedItem);
952 if (player.holdItem) objGrid.insert(player.holdItem);
953 //writeln("player spawned; active=", player.active);
954 player.scrSwitchToPocketItem(forceIfEmpty:false);
958 final void teleportPlayerTo (int x, int y) {
962 player.saveInterpData();
967 final void resurrectPlayer () {
968 if (player) player.resurrect();
969 playerExited = false;
973 // ////////////////////////////////////////////////////////////////////////// //
974 final void scrShake (int duration) {
975 if (shakeLeft == 0) {
981 shakeLeft = max(shakeLeft, duration);
986 // ////////////////////////////////////////////////////////////////////////// //
989 ItemStolen, // including damsel, lol
996 // make the nearest shopkeeper angry. RAWR!
997 void scrShopkeeperAnger (SCAnger reason, optional int maxdist, optional MapEntity offender) {
998 if (!offender) offender = player;
999 auto shp = MonsterShopkeeper(findNearestEnemy(offender.ix, offender.iy, delegate bool (MapEnemy o) {
1000 auto sc = MonsterShopkeeper(o);
1001 if (!sc) return false;
1002 if (sc.dead || sc.angered) return false;
1007 if (specified_maxdist && offender.directionToEntityCenter(shp) > maxdist) return;
1008 if (!shp.dead && !shp.angered) {
1009 shp.status = MapObject::ATTACK;
1011 if (global.murderer) msg = "YOU'LL PAY FOR YOUR CRIMES!";
1012 else if (reason == SCAnger.ItemStolen) msg = "COME BACK HERE, THIEF!";
1013 else if (reason == SCAnger.TileDestroyed) msg = "DIE, YOU VANDAL!";
1014 else if (reason == SCAnger.BombDropped) msg = "TERRORIST!";
1015 else if (reason == SCAnger.DamselWhipped) msg = "HEY, ONLY I CAN DO THAT!";
1016 else if (reason == SCAnger.CrapsCheated) msg = "DIE, CHEATER!";
1017 else msg = "NOW I'M REALLY STEAMED!";
1018 if (msg) osdMessage(msg, -666);
1019 global.thiefLevel += (global.thiefLevel > 0 ? 3 : 2);
1025 final MapObject findCrapsPrize () {
1026 foreach (MapObject o; objGrid.allObjects()) {
1027 if (o.spectral || !o.isInstanceAlive) continue;
1028 if (o.inDiceHouse) return o;
1034 // ////////////////////////////////////////////////////////////////////////// //
1035 // 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.
1036 // note: idols moved by monkeys will have false `stolenIdol`
1037 void scrTriggerIdolAltar (bool stolenIdol) {
1038 ObjTikiCurse res = none;
1039 int curdistsq = int.max;
1040 int px = player.xCenter, py = player.yCenter;
1041 foreach (MapObject o; objGrid.allObjects()) {
1042 auto tcr = ObjTikiCurse(o);
1043 if (!tcr || !tcr.isInstanceAlive) continue;
1044 if (tcr.activated) continue;
1045 int xc = px-tcr.xCenter, yc = py-tcr.yCenter;
1046 int distsq = xc*xc+yc*yc;
1047 if (distsq < curdistsq) {
1052 if (res) res.activate(stolenIdol);
1056 // ////////////////////////////////////////////////////////////////////////// //
1057 void setupGhostTime () {
1058 musicFadeTimer = -1;
1059 ghostSpawned = false;
1061 if (inWinCutscene || !isNormalLevel() || lg.finalBossLevel) {
1063 global.setMusicPitch(1.0);
1067 if (global.config.scumGhost < 0) {
1070 osdMessage("A GHASTLY VOICE CALLS YOUR NAME!\nLET'S GET OUT OF HERE!", 6.66, urgent:true);
1074 if (global.config.scumGhost == 0) {
1080 // randomizes time until ghost appears once time limit is reached
1081 // global.ghostRandom (true/false) if extra time is randomized - loaded from yasm.cfg. if false, use 30 second default
1082 // ghostTimeLeft (time in seconds * 1000) for currently generated level
1084 if (global.config.ghostRandom) {
1085 auto tMin = global.randOther(10, 30); // min(global.plife*10, 30);
1086 auto tMax = max(tMin+global.randOther(10, 30), min(global.config.scumGhost, 300)); // 5 minutes max
1087 auto tTime = global.randOther(tMin, tMax);
1088 if (tTime <= 0) tTime = round(tMax/2.0);
1089 ghostTimeLeft = tTime;
1091 ghostTimeLeft = global.config.scumGhost; // 5 minutes max
1094 ghostTimeLeft += max(0, global.config.ghostExtraTime);
1096 ghostTimeLeft *= 30; // seconds -> frames
1097 //global.ghostShowTime
1101 void spawnGhost () {
1103 ghostSpawned = true;
1105 int vwdt = (viewMax.x-viewMin.x);
1106 int vhgt = (viewMax.y-viewMin.y);
1110 if (player.ix < viewMin.x+vwdt/2) {
1111 // player is in the left side
1112 gx = viewMin.x+vwdt/2+vwdt/4;
1114 // player is in the right side
1115 gx = viewMin.x+vwdt/4;
1118 if (player.iy < viewMin.y+vhgt/2) {
1119 // player is in the left side
1120 gy = viewMin.y+vhgt/2+vhgt/4;
1122 // player is in the right side
1123 gy = viewMin.y+vhgt/4;
1126 writeln("spawning ghost at tile (", gx/16, ",", gy/16, "); player is at tile (", player.ix/16, ",", player.iy/16, ")");
1128 MakeMapObject(gx, gy, 'oGhost');
1131 if (oPlayer1.x > room_width/2) instance_create(view_xview[0]+view_wview[0]+8, view_yview[0]+floor(view_hview[0] / 2), oGhost);
1132 else instance_create(view_xview[0]-32, view_yview[0]+floor(view_hview[0]/2), oGhost);
1133 global.ghostExists = true;
1138 void thinkFrameGameGhost () {
1139 if (player.dead) return;
1140 if (!isNormalLevel()) return; // just in case
1142 if (ghostTimeLeft < 0) {
1144 if (musicFadeTimer > 0) {
1145 musicFadeTimer = -1;
1146 global.setMusicPitch(1.0);
1151 if (musicFadeTimer >= 0) {
1153 if (musicFadeTimer > 0 && musicFadeTimer <= 100*3) {
1154 float pitch = 1.0-(0.00226/3.0)*musicFadeTimer;
1155 //writeln("MFT: ", musicFadeTimer, "; pitch=", pitch);
1156 global.setMusicPitch(pitch);
1160 if (ghostTimeLeft == 0) {
1161 // she is already here!
1165 // no ghost if we have a crown
1166 if (global.hasCrown) {
1171 // if she was already spawned, don't do it again
1177 if (--ghostTimeLeft != 0) {
1179 if (global.config.ghostExtraTime > 0) {
1180 if (ghostTimeLeft == global.config.ghostExtraTime*30) {
1181 osdMessage("A CHILL RUNS UP YOUR SPINE...\nLET'S GET OUT OF HERE!", 6.66, urgent:true);
1183 if (musicFadeTimer < 0 && ghostTimeLeft <= global.config.ghostExtraTime*30) {
1191 if (player.isExitingSprite) {
1192 // no reason to spawn her, we're leaving
1201 void thinkFrameGame () {
1202 thinkFrameGameGhost();
1206 // ////////////////////////////////////////////////////////////////////////// //
1207 private transient array!MapObject activeThinkerList;
1210 private final bool isWetTile (MapTile t) {
1211 return (t && t.visible && (t.water || t.lava || t.wet));
1215 private final bool isWetOrSolidTile (MapTile t) {
1216 return (t && t.visible && (t.water || t.lava || t.wet || t.solid) && t.isInstanceAlive);
1220 final bool isWetOrSolidTileAtPoint (int px, int py) {
1221 return !!checkTileAtPoint(px, py, &isWetOrSolidTile);
1225 final bool isWetOrSolidTileAtTile (int tx, int ty) {
1226 return !!checkTileAtPoint(tx*16, ty*16, &isWetOrSolidTile);
1230 final bool isWetTileAtTile (int tx, int ty) {
1231 return !!checkTileAtPoint(tx*16, ty*16, &isWetTile);
1235 // ////////////////////////////////////////////////////////////////////////// //
1236 const int GreatLakeStartTileY = 28;
1238 // called once after level generation
1239 final void fixLiquidTop () {
1240 foreach (int tileY; 0..tilesHeight) {
1241 foreach (int tileX; 0..tilesWidth) {
1242 auto t = tiles[tileX, tileY];
1244 if (t && !t.isInstanceAlive) {
1245 delete tiles[tileX, tileY];
1250 if (global.lake && tileY >= GreatLakeStartTileY) {
1251 // fill level with water for lake
1252 MakeMapTile(tileX, tileY, 'oWaterSwim');
1253 t = tiles[tileX, tileY];
1259 if (!t.water && !t.lava) {
1260 // mark as wet for lake
1261 if (global.lake && tileY >= GreatLakeStartTileY) {
1267 if (!isWetTileAtTile(tileX, tileY-1)) {
1268 t.setSprite(t.lava ? 'sLavaTop' : 'sWaterTop');
1270 if (t.spriteName == 'sWaterTop') t.setSprite('sWater');
1271 else if (t.spriteName == 'sLavaTop') t.setSprite('sLava');
1278 private final void checkWaterFlow (MapTile wtile) {
1279 //if (!wtile || (!wtile.water && !wtile.lava)) return;
1280 //instance_activate_region(x-16, y-16, 48, 48, true);
1282 //int x = wtile.ix, y = wtile.iy;
1283 int tileX = wtile.ix/16, tileY = wtile.iy/16;
1285 if (global.lake && tileY >= GreatLakeStartTileY) return;
1288 if ((not collision_point(x-16, y, oSolid, 0, 0) and not collision_point(x-16, y, oWater, 0, 0)) or
1289 (not collision_point(x+16, y, oSolid, 0, 0) and not collision_point(x+16, y, oWater, 0, 0)) or
1290 (not collision_point(x, y+16, oSolid, 0, 0) and not collision_point(x, y+16, oWater, 0, 0)))
1292 if (!isWetOrSolidTileAtTile(tileX-1, tileY) ||
1293 !isWetOrSolidTileAtTile(tileX+1, tileY) ||
1294 !isWetOrSolidTileAtTile(tileX, tileY+1))
1298 wtile.instanceRemove();
1301 tiles[tileX, tileY] = none;
1305 //if (!isSolidAtPoint(x, y-16) && !isLiquidAtPoint(x, y-16)) {
1306 if (!isWetTileAtTile(tileX, tileY-1)) {
1307 wtile.setSprite(wtile.lava ? 'sLavaTop' : 'sWaterTop');
1312 transient private array!MapTile waterTilesToCheck;
1314 final void cleanDeadTiles () {
1315 bool hasWater = false;
1316 waterTilesToCheck.length -= waterTilesToCheck.length;
1317 foreach (int y; 0..tilesHeight) {
1318 foreach (int x; 0..tilesWidth) {
1319 auto t = tiles[x, y];
1321 if (t.isInstanceAlive) {
1322 if (t.water || t.lava) waterTilesToCheck[$] = t;
1331 if (waterTilesToCheck.length && checkWater && !blockWaterChecking) {
1332 //writeln("checking water");
1333 checkWater = false; // `checkWaterFlow()` can set it again
1334 foreach (MapTile t; waterTilesToCheck) {
1335 if (t && t.isInstanceAlive && (t.water || t.lava)) checkWaterFlow(t);
1337 // fill empty spaces in lake with water
1339 foreach (int y; GreatLakeStartTileY..tilesHeight) {
1340 foreach (int x; 0..tilesWidth) {
1341 auto t = tiles[x, y];
1343 if (t && !t.isInstanceAlive) {
1349 if (!t.water || !t.lava) { t.wet = true; continue; }
1351 MakeMapTile(x, y, 'oWaterSwim');
1355 t.setSprite(y == GreatLakeStartTileY ? 'sWaterTop' : 'sWater');
1356 } else if (t.lava) {
1357 t.setSprite(y == GreatLakeStartTileY ? 'sLavaTop' : 'sLava');
1366 // ////////////////////////////////////////////////////////////////////////// //
1367 void collectLavaTiles () {
1368 lavatiles.length -= lavatiles.length;
1369 foreach (MapTile t; tiles) {
1370 if (t && t.lava && t.isInstanceAlive) lavatiles[$] = t;
1375 void processLavaTiles () {
1376 int tn = 0, tlen = lavatiles.length;
1378 MapTile t = lavatiles[tn];
1379 if (t && t.isInstanceAlive) {
1383 lavatiles.remove(tn, 1);
1390 // ////////////////////////////////////////////////////////////////////////// //
1391 // return `true` if thinker should be removed
1392 final bool thinkOne (MapObject o) {
1393 if (!o) return true;
1394 if (o.active && o.isInstanceAlive) {
1395 bool doThink = true;
1397 // collision with player weapon
1398 auto hh = PlayerWeapon(player.holdItem);
1399 bool doWeaponAction;
1401 if (hh.blockedBySolids) {
1402 int xx = player.ix+(player.dir == MapObject::Dir.Left ? -16 : 16);
1403 doWeaponAction = !isSolidAtPoint(xx, player.iy);
1405 doWeaponAction = true;
1408 doWeaponAction = false;
1411 if (doWeaponAction && o.whipTimer <= 0 && hh && hh.collidesWithObject(o)) {
1412 //writeln("WEAPONED!");
1413 if (!o.onTouchedByPlayerWeapon(player, hh)) {
1414 if (o.isInstanceAlive) hh.onCollisionWithObject(o);
1416 o.whipTimer = o.whipTimerValue; //HACK
1417 doThink = o.isInstanceAlive;
1420 // collision with player
1421 if (doThink && o.collidesWith(player)) {
1422 if (!player.onObjectTouched(o) && o.isInstanceAlive) {
1423 doThink = !o.onTouchedByPlayer(player);
1424 if (o.grid && o.gridId) o.grid.update(o.gridId, markAsDead:true);
1428 if (doThink && o.isInstanceAlive) {
1431 if (o.isInstanceAlive) {
1432 if (o.grid && o.gridId) o.grid.update(o.gridId, markAsDead:true);
1434 if (o.isInstanceAlive) {
1436 if (o.grid && o.gridId) o.grid.update(o.gridId, markAsDead:true);
1441 if (o.isInstanceAlive) {
1442 if (!o.canLiveOutsideOfLevel && !o.heldBy && o.isOutsideOfLevel()) {
1456 final void processThinkers (float timeDelta) {
1457 if (timeDelta <= 0) return;
1459 if (onBeforeFrame) onBeforeFrame(false);
1460 if (onAfterFrame) onAfterFrame(false);
1464 accumTime += timeDelta;
1465 bool wasFrame = false;
1467 auto olddel = ImmediateDelete;
1468 ImmediateDelete = false;
1469 while (accumTime >= FrameTime) {
1470 accumTime -= FrameTime;
1471 if (onBeforeFrame) onBeforeFrame(accumTime >= FrameTime);
1473 if (shakeLeft > 0) {
1475 if (!shakeDir.x) shakeDir.x = (global.randOther(0, 1) ? -3 : 3);
1476 if (!shakeDir.y) shakeDir.y = (global.randOther(0, 1) ? -3 : 3);
1477 shakeOfs.x = shakeDir.x;
1478 shakeOfs.y = shakeDir.y;
1479 int sgnc = global.randOther(1, 3);
1480 if (sgnc&0x01) shakeDir.x = -shakeDir.x;
1481 if (sgnc&0x02) shakeDir.y = -shakeDir.y;
1488 // game-global events
1490 // frame thinkers: lava tiles
1492 // frame thinkers: player
1493 if (player && !disablePlayerThink) {
1495 if (!player.dead && isNormalLevel() &&
1496 (maxPlayingTime < 0 ||
1497 (maxPlayingTime > 0 && maxPlayingTime*30 <= time &&
1498 time%30 == 0 && global.randOther(1, 100) <= 20)))
1500 MakeMapObject(player.ix, player.iy, 'oExplosion');
1501 player.scrCreateFlame(player.ix, player.iy, 3);
1503 //HACK: check for stolen items
1504 auto item = MapItem(player.holdItem);
1505 if (item) item.onCheckItemStolen(player);
1506 item = MapItem(player.pickedItem);
1507 if (item) item.onCheckItemStolen(player);
1509 player.saveInterpData();
1510 player.processAlarms();
1511 if (player.isInstanceAlive) {
1512 player.thinkFrame();
1513 if (player.isInstanceAlive) player.nextAnimFrame();
1516 // frame thinkers: moveable solids
1518 // frame thinkers: objects
1519 auto grid = objGrid;
1520 // collect active objects
1521 if (global.config.useFrozenRegion) {
1522 activeThinkerList.length -= activeThinkerList.length;
1523 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)) {
1524 activeThinkerList[$] = o;
1526 //writeln("thinkers: ", activeThinkerList.length);
1527 foreach (MapObject o; activeThinkerList) {
1529 grid.remove(o.gridId);
1536 bool killThisOne = false;
1537 for (int cid = grid.getFirstObject(); cid; cid = grid.getNextObject(cid, killThisOne)) {
1538 killThisOne = false;
1539 MapObject o = grid.getObject(MapObject, cid);
1540 if (!o) { killThisOne = true; continue; }
1541 // remove this object if it is dead
1551 if (player && player.holdItem) {
1552 if (player.holdItem.isInstanceAlive) {
1553 player.holdItem.fixHoldCoords();
1555 player.holdItem = none;
1558 // done with thinkers
1561 if (collectCounter == 0) {
1562 xmoney = max(0, xmoney-100);
1567 if (player && !player.dead) stats.oneMoreFramePlayed();
1568 if (onAfterFrame) onAfterFrame(accumTime >= FrameTime);
1571 if (!player.visible && player.holdItem) player.holdItem.visible = false;
1572 if (winCutsceneSwitchToNext) {
1573 winCutsceneSwitchToNext = false;
1574 switch (++inWinCutscene) {
1575 case 2: startWinCutsceneVolcano(); break;
1576 case 3: default: startWinCutsceneWinFall(); break;
1580 if (playerExited) break;
1582 ImmediateDelete = olddel;
1584 playerExited = false;
1588 // if we were processed at least one frame, collect garbage
1590 CollectGarbage(true); // destroy delayed objects too
1592 if (accumTime > 0 && onInterFrame) onInterFrame(accumTime/FrameTime);
1596 // ////////////////////////////////////////////////////////////////////////// //
1597 final void tileXY2roomXY (int tileX, int tileY, out int roomX, out int roomY) {
1598 roomX = (tileX-1)/RoomGen::Width;
1599 roomY = (tileY-1)/RoomGen::Height;
1603 final bool isInShop (int tileX, int tileY) {
1604 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
1605 auto n = roomType[tileX, tileY];
1606 if (n == 4 || n == 5) return true;
1607 auto t = getTileAt(tileX, tileY);
1608 if (t && t.shopWall) return true;
1609 //k8: we don't have this
1610 //if (t && t.objType == 'oShop') return true;
1616 // ////////////////////////////////////////////////////////////////////////// //
1617 override void Destroy () {
1620 delete tempSolidTile;
1625 // ////////////////////////////////////////////////////////////////////////// //
1626 final MapObject findNearestBall (int px, int py) {
1627 MapObject res = none;
1628 int curdistsq = int.max;
1629 foreach (MapObject o; ballObjects) {
1630 if (!o || o.spectral || !o.isInstanceAlive) continue;
1631 int xc = px-o.xCenter, yc = py-o.yCenter;
1632 int distsq = xc*xc+yc*yc;
1633 if (distsq < curdistsq) {
1642 final int calcNearestBallDist (int px, int py) {
1643 auto e = findNearestBall(px, py);
1644 if (!e) return int.max;
1645 int xc = px-e.xCenter, yc = py-e.yCenter;
1646 return round(sqrt(xc*xc+yc*yc));
1650 final MapObject findNearestObject (int px, int py, bool delegate (MapObject o) dg) {
1651 MapObject res = none;
1652 int curdistsq = int.max;
1653 foreach (MapObject o; objGrid.allObjects()) {
1654 if (o.spectral || !o.isInstanceAlive) continue;
1655 if (!dg(o)) continue;
1656 int xc = px-o.xCenter, yc = py-o.yCenter;
1657 int distsq = xc*xc+yc*yc;
1658 if (distsq < curdistsq) {
1667 final MapObject findNearestEnemy (int px, int py, optional bool delegate (MapEnemy o) dg) {
1668 MapObject res = none;
1669 int curdistsq = int.max;
1670 foreach (MapObject o; objGrid.allObjects()) {
1671 //k8: i added `dead` check
1672 if (o.spectral || o !isa MapEnemy || o.dead || !o.isInstanceAlive) continue;
1674 if (!dg(MapEnemy(o))) continue;
1676 int xc = px-o.xCenter, yc = py-o.yCenter;
1677 int distsq = xc*xc+yc*yc;
1678 if (distsq < curdistsq) {
1687 final MonsterShopkeeper hasAliveShopkeepers (optional bool skipAngry) {
1688 foreach (MapObject o; objGrid.allObjects()) {
1689 auto sc = MonsterShopkeeper(o);
1690 if (!sc || o.spectral || !o.isInstanceAlive) continue;
1691 if (sc.dead) continue;
1692 if (skipAngry && sc.angered) continue;
1699 final int calcNearestEnemyDist (int px, int py, optional bool delegate (MapEnemy o) dg) {
1700 auto e = findNearestEnemy(px, py, dg!optional);
1701 if (!e) return int.max;
1702 int xc = px-e.xCenter, yc = py-e.yCenter;
1703 return round(sqrt(xc*xc+yc*yc));
1707 final int calcNearestObjectDist (int px, int py, optional bool delegate (MapObject o) dg) {
1708 auto e = findNearestObject(px, py, dg!optional);
1709 if (!e) return int.max;
1710 int xc = px-e.xCenter, yc = py-e.yCenter;
1711 return round(sqrt(xc*xc+yc*yc));
1715 final MapTile findNearestMoveableSolid (int px, int py, optional bool delegate (MapTile t) dg) {
1717 int curdistsq = int.max;
1718 foreach (MapTile t; miscTileGrid.allObjects()) {
1719 if (t.spectral || !t.isInstanceAlive) continue;
1721 if (!dg(t)) continue;
1723 if (!t.solid || !t.moveable) continue;
1725 int xc = px-t.xCenter, yc = py-t.yCenter;
1726 int distsq = xc*xc+yc*yc;
1727 if (distsq < curdistsq) {
1736 final MapTile findNearestTile (int px, int py, optional bool delegate (MapTile t) dg) {
1737 if (!dg) return none;
1739 int curdistsq = int.max;
1741 //FIXME: make this faster!
1742 foreach (MapTile t; tiles) {
1743 if (!t || t.spectral || !t.isInstanceAlive) continue;
1744 int xc = px-t.xCenter, yc = py-t.yCenter;
1745 int distsq = xc*xc+yc*yc;
1746 if (distsq < curdistsq && dg(t)) {
1752 foreach (MapTile t; miscTileGrid.allObjects()) {
1753 if (!t || t.spectral || !t.isInstanceAlive) continue;
1754 int xc = px-t.xCenter, yc = py-t.yCenter;
1755 int distsq = xc*xc+yc*yc;
1756 if (distsq < curdistsq && dg(t)) {
1766 // ////////////////////////////////////////////////////////////////////////// //
1767 final bool cbIsObjectWeb (MapObject o) { return (o isa ItemWeb); }
1768 final bool cbIsObjectBlob (MapObject o) { return (o.objName == 'oBlob'); }
1769 final bool cbIsObjectArrow (MapObject o) { return (o isa ItemProjectileArrow); }
1770 final bool cbIsObjectEnemy (MapObject o) { return (/*o != self &&*/ o isa MapEnemy); }
1772 final bool cbIsObjectTreasure (MapObject o) { return (o.isTreasure); }
1774 final bool cbIsItemObject (MapObject o) { return (o isa MapItem); }
1776 final bool cbIsItemOrEnemyObject (MapObject o) { return (o isa MapItem || o isa MapEnemy); }
1779 final MapObject isObjectAtTile (int tileX, int tileY, optional bool delegate (MapObject o) dg) {
1782 foreach (MapObject o; objGrid.inRectPix(tileX, tileY, 16, 16, objGrid.nextTag(), precise: true)) {
1783 if (o.spectral || !o.isInstanceAlive) continue;
1785 if (dg(o)) return o;
1794 final MapObject isObjectAtTilePix (int x, int y, optional bool delegate (MapObject o) dg) {
1795 return isObjectAtTile(x/16, y/16, dg!optional);
1799 final MapObject isObjectAtPoint (int xpos, int ypos, optional bool delegate (MapObject o) dg, optional bool precise) {
1800 if (!specified_precise) precise = true;
1801 foreach (MapObject o; objGrid.inCellPix(xpos, ypos, objGrid.nextTag(), precise: precise)) {
1802 if (o.spectral || !o.isInstanceAlive) continue;
1804 if (dg(o)) return o;
1806 if (o isa MapEnemy) return o;
1813 final MapObject isObjectInRect (int xpos, int ypos, int w, int h, optional bool delegate (MapObject o) dg, optional bool precise) {
1814 if (w < 1 || h < 1) return none;
1815 if (w == 1 && h == 1) return isObjectAtPoint(xpos, ypos, dg!optional, precise!optional);
1816 if (!specified_precise) precise = true;
1817 foreach (MapObject o; objGrid.inRectPix(xpos, ypos, w, h, objGrid.nextTag(), precise: precise)) {
1818 if (o.spectral || !o.isInstanceAlive) continue;
1820 if (dg(o)) return o;
1822 if (o isa MapEnemy) return o;
1829 final MapObject forEachObject (bool delegate (MapObject o) dg, optional bool allowSpectrals) {
1830 if (!dg) return none;
1832 foreach (MapObject o; objGrid.allObjects()) {
1833 if (o.spectral || !o.isInstanceAlive) continue;
1834 if (dg(o)) return o;
1837 // process gravity for moveable solids and burning for ropes
1838 auto grid = objGrid;
1839 int cid = grid.getFirstObject();
1841 MapObject o = grid.getObject(MapObject, cid);
1842 if (!o || !o.isInstanceAlive) {
1843 cid = grid.getNextObject(cid, removeThis:true);
1846 if (!allowSpectrals && o.spectral) {
1847 cid = grid.getNextObject(cid, removeThis:false);
1850 if (dg(o)) return o;
1851 if (o.isInstanceAlive) {
1852 cid = grid.getNextObject(cid, removeThis:false);
1854 cid = grid.getNextObject(cid, removeThis:true);
1855 o.instanceRemove(); // just in case
1864 final MapObject forEachObjectAtPoint (int xpos, int ypos, bool delegate (MapObject o) dg, optional bool precise) {
1865 if (!dg) return none;
1866 if (!specified_precise) precise = true;
1867 foreach (MapObject o; objGrid.inCellPix(xpos, ypos, objGrid.nextTag(), precise: precise)) {
1868 if (o.spectral || !o.isInstanceAlive) continue;
1869 if (dg(o)) return o;
1875 final MapObject forEachObjectInRect (int xpos, int ypos, int w, int h, bool delegate (MapObject o) dg, optional bool precise) {
1876 if (!dg || w < 1 || h < 1) return none;
1877 if (w == 1 && h == 1) return forEachObjectAtPoint(xpos, ypos, dg!optional, precise!optional);
1878 if (!specified_precise) precise = true;
1879 foreach (MapObject o; objGrid.inRectPix(xpos, ypos, w, h, objGrid.nextTag(), precise: precise)) {
1880 if (o.spectral || !o.isInstanceAlive) continue;
1881 if (dg(o)) return o;
1887 private final bool cbIsRopeTile (MapTile t) { return t.rope; }
1889 final MapTile isRopeAtPoint (int px, int py) {
1890 return checkTileAtPoint(px, py, &cbIsRopeTile);
1895 final MapTile isWaterSwimAtPoint (int px, int py) {
1896 return isWaterAtPoint(px, py);
1900 // ////////////////////////////////////////////////////////////////////////// //
1901 private array!MapObject tmpObjectList;
1903 private final bool cbCollectObjectsWithMask (MapObject t) {
1904 if (!t || !t.visible || t.spectral /*|| t.invincible*/ || !t.isInstanceAlive) return false;
1905 //auto spf = getSpriteFrame();
1906 //if (!t.sprite || t.sprite.frames.length < 1) return false;
1907 tmpObjectList[$] = t;
1912 final void touchObjectsWithMask (int x, int y, SpriteFrame frm, bool delegate (MapObject t) dg) {
1913 if (!frm || frm.bw < 1 || frm.bh < 1 || !dg) return;
1914 if (frm.isEmptyPixelMask) return;
1916 if (tmpObjectList.length) tmpObjectList.length -= tmpObjectList.length; // don't realloc
1917 if (player.isRectCollisionFrame(frm, x, y)) {
1918 //writeln("player hit");
1919 tmpObjectList[$] = player;
1922 writeln("no player hit: plr=(", player.ix, ",", player.iy, ")-(", player.ix+player.width-1, ",", player.iy+player.height-1, "); ",
1923 "frm=(", x+frm.bx, ",", y+frm.by, ")-(", x+frm.bx+frm.bw-1, ",", y+frm.by+frm.bh-1, ")");
1926 forEachObjectInRect(x+frm.bx, y+frm.by, frm.bw, frm.bh, &cbCollectObjectsWithMask);
1927 foreach (MapObject t; tmpObjectList) {
1928 if (!t || !t.isInstanceAlive || !t.visible || t.spectral) continue;
1930 auto tf = t.getSpriteFrame();
1932 //writeln("no sprite frame for ", GetClassName(t.Class));
1937 if (frm.pixelCheck(tf, t.ix-tf.xofs-x, t.iy-tf.yofs-y)) {
1938 //writeln("pixel hit for ", GetClassName(t.Class));
1942 if (t.isRectCollisionFrame(frm, x, y)) {
1949 // ////////////////////////////////////////////////////////////////////////// //
1950 final void destroyTileAt (int x, int y) {
1951 if (x < 0 || y < 0 || x >= tilesWidth*16 || y >= tilesHeight*16) return;
1954 MapTile t = tiles[x, y];
1955 if (!t || !t.visible || t.spectral || t.invincible || !t.isInstanceAlive) return;
1963 private array!MapTile tmpTileList;
1965 private final bool cbCollectTilesWithMask (MapTile t) {
1966 if (!t || !t.visible || t.spectral /*|| t.invincible*/ || !t.isInstanceAlive) return false;
1967 if (!t.sprite || t.sprite.frames.length < 1) return false;
1972 final void touchTilesWithMask (int x, int y, SpriteFrame frm, bool delegate (MapTile t) dg) {
1973 if (!frm || frm.bw < 1 || frm.bh < 1 || !dg) return;
1974 if (frm.isEmptyPixelMask) return;
1976 if (tmpTileList.length) tmpTileList.length -= tmpTileList.length; // don't realloc
1977 checkTilesInRect(x+frm.bx, y+frm.by, frm.bw, frm.bh, &cbCollectTilesWithMask);
1978 foreach (MapTile t; tmpTileList) {
1979 if (!t || !t.isInstanceAlive || !t.visible || t.spectral) continue;
1981 auto tf = t.sprite.frames[0];
1982 if (frm.pixelCheck(tf, t.ix-x, t.iy-y)) {
1984 //doCleanup = doCleanup || !t.isInstanceAlive;
1985 //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, ")");
1988 if (t.isRectCollisionFrame(frm, x, y)) {
1995 // ////////////////////////////////////////////////////////////////////////// //
1996 final bool cbCollisionAnySolid (MapTile t) { return t.solid; }
1997 final bool cbCollisionAnyMoveableSolid (MapTile t) { return t.solid && t.moveable; }
1998 final bool cbCollisionLiquid (MapTile t) { return (t.water || t.lava); }
1999 final bool cbCollisionAnySolidOrLiquid (MapTile t) { return (t.solid || t.water || t.lava); }
2000 final bool cbCollisionLadder (MapTile t) { return t.ladder; }
2001 final bool cbCollisionLadderTop (MapTile t) { return t.laddertop; }
2002 final bool cbCollisionAnyLadder (MapTile t) { return (t.ladder || t.laddertop); }
2003 final bool cbCollisionAnySolidIce (MapTile t) { return (t.solid && t.ice); }
2004 final bool cbCollisionWater (MapTile t) { return t.water; }
2005 final bool cbCollisionLava (MapTile t) { return t.lava; }
2006 final bool cbCollisionAnyTree (MapTile t) { return t.tree; }
2007 final bool cbCollisionPlatform (MapTile t) { return t.platform; }
2008 final bool cbCollisionLadderOrPlatform (MapTile t) { return (t.ladder || t.platform); }
2009 //final bool cbCollisionAnyHangeable (MapTile t) { return t.hangeable; } // no need to check for solids here, it is done in MapTile initializer
2010 final bool cbCollisionSpringTrap (MapTile t) { return t.springtrap; }
2011 final bool cbCollisionSpikes (MapTile t) { return t.spikes; }
2012 final bool cbCollisionExitTile (MapTile t) { return t.isExitActive(); }
2014 final bool cbCollisionForWhoa (MapTile t) { return (t.solid || t.laddertop); }
2016 //final bool cbCollisionSacAltar (MapTile t) { return (t.objName == 'oSacAltarLeft'); } // only left part, yeah
2017 final bool cbCollisionSacAltar (MapTile t) { return t.sacrificingAltar; }
2020 // ////////////////////////////////////////////////////////////////////////// //
2021 transient MapTile tempSolidTile;
2023 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*/) {
2024 //!if (dbgdump) writeln("checkTilesInRect: (", x0, ",", y0, ")-(", x0+w-1, ",", y0+h-1, ") ; w=", w, "; h=", h);
2025 if (w < 1 || h < 1) return none;
2026 if (w == 1 && h == 1) return checkTileAtPoint(x0, y0, dg!optional);
2027 int x1 = x0+w-1, y1 = y0+h-1;
2028 if (x0 >= tilesWidth*16 || y0 >= tilesHeight*16 || x1 < 0 || y1 < 0) return none;
2030 //!if (dbgdump) writeln("default checker set");
2031 dg = &cbCollisionAnySolid;
2033 //!if (dbgdump) writeln("delegate: ", dg);
2034 int origx0 = x0, origy0 = y0;
2035 int tileSX = max(0, x0)/16;
2036 int tileSY = max(0, y0)/16;
2037 int tileEX = min(tilesWidth*16-1, x1)/16;
2038 int tileEY = min(tilesHeight*16-1, y1)/16;
2039 //!if (dbgdump) writeln(" tiles: (", tileSX, ",", tileSY, ")-(", tileEX, ",", tileEY, ")");
2040 //!!!auto grid = miscTileGrid;
2041 //!!!int tag = grid.nextTag();
2042 for (int ty = tileSY; ty <= tileEY; ++ty) {
2043 for (int tx = tileSX; tx <= tileEX; ++tx) {
2044 MapTile t = tiles[tx, ty];
2045 //!if (dbgdump && t && t.visible && t.isInstanceAlive && t.isRectCollision(origx0, origy0, w, h) ) writeln(" tile: ", GetClassName(t.Class), " : ", t.objName, " : ", t.objType, " : ", dg(t));
2046 if (t && !t.spectral && t.visible && t.isInstanceAlive && t.isRectCollision(origx0, origy0, w, h) && dg(t)) return t;
2047 // moveable tiles are in separate grid
2049 foreach (t; grid.inCellPix(tx*16, ty*16, tag, precise:precise)) {
2050 //if (t.isRectCollision(origx0, origy0, w, h) && dg(t)) return t;
2051 if (t.isInstanceAlive && !t.spectral && t.visible && t.isRectCollision(origx0, origy0, w, h) && dg(t)) return t;
2057 // moveable tiles are in separate grid
2058 foreach (MapTile t; miscTileGrid.inRectPix(x0, y0, w, h, miscTileGrid.nextTag(), precise:precise)) {
2059 //if (t.isRectCollision(origx0, origy0, w, h) && dg(t)) return t;
2060 if (t.isInstanceAlive && !t.spectral && t.visible && t.isRectCollision(origx0, origy0, w, h) && dg(t)) return t;
2063 // check walkable solid objects
2064 foreach (MapObject o; objGrid.inRectPix(x0, y0, w, h, objGrid.nextTag(), precise:precise)) {
2065 if (o && !o.spectral && o.visible && o.walkableSolid && o.isInstanceAlive && o.isRectCollision(origx0, origy0, w, h)) {
2066 if (!tempSolidTile) {
2067 tempSolidTile = SpawnObject(MapTile);
2068 } else if (!tempSolidTile.isInstanceAlive) {
2069 delete tempSolidTile;
2070 tempSolidTile = SpawnObject(MapTile);
2072 tempSolidTile.solid = true;
2073 if (dg(tempSolidTile)) return tempSolidTile;
2081 final MapTile checkTileAtPoint (int x0, int y0, optional bool delegate (MapTile dg) dg) {
2082 if (x0 < 0 || y0 < 0 || x0 >= tilesWidth*16 || y0 >= tilesHeight*16) return none;
2083 if (!dg) dg = &cbCollisionAnySolid;
2084 //if (!self) { writeln("WTF?!"); return none; }
2085 MapTile t = tiles[x0/16, y0/16];
2086 if (t && !t.spectral && t.visible && t.isInstanceAlive && t.isPointCollision(x0, y0) && dg(t)) return t;
2088 // moveable tiles are in separate grid
2089 foreach (t; miscTileGrid.inCellPix(x0, y0, miscTileGrid.nextTag(), precise:true)) {
2090 if (t.isInstanceAlive && !t.spectral && t.visible && dg(t)) return t;
2093 // check walkable solid objects
2094 foreach (MapObject o; objGrid.inCellPix(x0, y0, objGrid.nextTag(), precise:true)) {
2095 if (o && !o.spectral && o.visible && o.walkableSolid && o.isInstanceAlive && o.isRectCollision(x0, y0, 1, 1)) {
2096 if (!tempSolidTile) {
2097 tempSolidTile = SpawnObject(MapTile);
2098 } else if (!tempSolidTile.isInstanceAlive) {
2099 delete tempSolidTile;
2100 tempSolidTile = SpawnObject(MapTile);
2102 tempSolidTile.solid = true;
2103 if (dg(tempSolidTile)) return tempSolidTile;
2111 //FIXME: optimize this with clipping first
2112 //TODO: moveable tiles
2114 final MapTile checkTilesAtLine (int ax0, int ay0, int ax1, int ay1, optional bool delegate (MapTile dg) dg) {
2115 // do it faster if we can
2117 // strict vertical check?
2118 if (ax0 == ax1 && ay0 <= ay1) return checkTilesInRect(ax0, ay0, 1, ay1-ay0+1, dg!optional);
2119 // strict horizontal check?
2120 if (ay0 == ay1 && ax0 <= ax1) return checkTilesInRect(ax0, ay0, ax1-ax0+1, 1, dg!optional);
2122 float x0 = float(ax0)/16.0, y0 = float(ay0)/16.0, x1 = float(ax1)/16.0, y1 = float(ay1)/16.0;
2125 if (!dg) dg = &cbCollisionAnySolid;
2127 // get starting and enging tile
2128 int tileSX = trunc(x0), tileSY = trunc(y0);
2129 int tileEX = trunc(x1), tileEY = trunc(y1);
2131 // first hit is always landed
2132 if (tileSX >= 0 && tileSY >= 0 && tileSX < TilesWidth && tileSY < TilesHeight) {
2133 MapTile t = tiles[tileSX, tileSY];
2134 if (t && t.visible && Geom.lineAABBIntersects(ax0, ay0, ax1, ay1, tileSX*16, tileSY*16, 16, 16) && dg(t)) return t;
2137 // if starting and ending tile is the same, we don't need to do anything more
2138 if (tileSX == tileEX && tileSY == tileEY) return none;
2140 // calculate ray direction
2141 TVec dv = (vector(x1, y1)-vector(x0, y0)).normalise2d;
2143 // length of ray from one x or y-side to next x or y-side
2144 float deltaDistX = (fabs(dv.x) > 0.0001 ? fabs(1.0/dv.x) : 0.0);
2145 float deltaDistY = (fabs(dv.y) > 0.0001 ? fabs(1.0/dv.y) : 0.0);
2147 // calculate step and initial sideDists
2149 float sideDistX; // length of ray from current position to next x-side
2150 int stepX; // what direction to step in x (either +1 or -1)
2153 sideDistX = (x0-tileSX)*deltaDistX;
2156 sideDistX = (tileSX+1.0-x0)*deltaDistX;
2159 float sideDistY; // length of ray from current position to next y-side
2160 int stepY; // what direction to step in y (either +1 or -1)
2163 sideDistY = (y0-tileSY)*deltaDistY;
2166 sideDistY = (tileSY+1.0-y0)*deltaDistY;
2170 //int side; // was a NS or a EW wall hit?
2172 // jump to next map square, either in x-direction, or in y-direction
2173 if (sideDistX < sideDistY) {
2174 sideDistX += deltaDistX;
2178 sideDistY += deltaDistY;
2183 if (tileSX >= 0 && tileSY >= 0 && tileSX < TilesWidth && tileSY < TilesHeight) {
2184 MapTile t = tiles[tileSX, tileSY];
2185 if (t && t.visible && Geom.lineAABBIntersects(ax0, ay0, ax1, ay1, tileSX*16, tileSY*16, 16, 16) && dg(t)) return t;
2187 // did we arrived at the destination?
2188 if (tileSX == tileEX && tileSY == tileEY) break;
2196 // ////////////////////////////////////////////////////////////////////////// //
2197 final MapTile isSolidAtPoint (int px, int py) { return checkTileAtPoint(px, py); }
2198 final MapTile isSolidInRect (int rx, int ry, int rw, int rh) { return checkTilesInRect(rx, ry, rw, rh); }
2199 final MapTile isLadderAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadder); }
2200 final MapTile isLadderOrPlatformAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadderOrPlatform); }
2201 final MapTile isLadderTopAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadderTop); }
2202 final MapTile isAnyLadderAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyLadder); }
2203 final MapTile isWaterAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionWater); }
2204 final MapTile isLavaAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLava); }
2205 final MapTile isLiquidAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLiquid); }
2206 final MapTile isIceAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnySolidIce); }
2207 final MapTile isTreeAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyTree); }
2208 //final MapTile isHangeableAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyHangeable); }
2211 // ////////////////////////////////////////////////////////////////////////// //
2212 // PlayerPawn has it's own movement code, so don't process it here
2213 // but process moveable solids here, yeah
2214 final void physStep () {
2217 // we don't want the time to grow too large
2218 if (time > 100000000) time = 0;
2220 auto grid = miscTileGrid;
2222 // process gravity for moveable solids and burning for ropes
2223 int cid = grid.getFirstObject();
2225 MapTile t = grid.getObject(MapTile, cid);
2227 cid = grid.getNextObject(cid, removeThis:false);
2230 if (t.isInstanceAlive) {
2233 if (t.isInstanceAlive) {
2234 grid.update(cid, markAsDead:false);
2236 if (t.isInstanceAlive && !t.canLiveOutsideOfLevel && t.isOutsideOfLevel()) t.instanceRemove();
2237 grid.update(cid, markAsDead:false);
2240 if (t.isInstanceAlive) {
2241 cid = grid.getNextObject(cid, removeThis:false);
2243 cid = grid.getNextObject(cid, removeThis:true);
2244 t.instanceRemove(); // just in case
2253 // ////////////////////////////////////////////////////////////////////////// //
2254 final void setTileTypeAt (int tileX, int tileY, LevelGen::RType rt) {
2255 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) roomType[tileX, tileY] = rt;
2258 final MapTile getTileAt (int tileX, int tileY) {
2259 return (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight ? tiles[tileX, tileY] : none);
2262 final bool isNamedTileAt (int tileX, int tileY, name atypename) {
2263 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
2264 auto t = tiles[tileX, tileY];
2265 if (t && t.objName == atypename) return true;
2270 final void setTileAt (int tileX, int tileY, MapTile tile) {
2271 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
2273 if (tiles[tileX, tileY]) checkWater = true;
2274 delete tiles[tileX, tileY];
2275 tiles[tileX, tileY] = tile;
2280 // ////////////////////////////////////////////////////////////////////////// //
2281 // return `true` from delegate to stop
2282 MapTile forEachSolidTile (bool delegate (int x, int y, MapTile t) dg) {
2283 if (!dg) return none;
2284 foreach (int y; 0..tilesHeight) {
2285 foreach (int x; 0..tilesWidth) {
2286 auto t = tiles[x, y];
2287 if (t && t.solid && t.visible && t.isInstanceAlive) {
2288 if (dg(x, y, t)) return t;
2296 // ////////////////////////////////////////////////////////////////////////// //
2297 // return `true` from delegate to stop
2298 MapTile forEachNormalTile (bool delegate (int x, int y, MapTile t) dg) {
2299 if (!dg) return none;
2300 foreach (int y; 0..tilesHeight) {
2301 foreach (int x; 0..tilesWidth) {
2302 auto t = tiles[x, y];
2303 if (t && t.visible && t.isInstanceAlive) {
2304 if (dg(x, y, t)) return t;
2312 // WARNING! don't destroy tiles here! (instanceRemove() is ok, tho)
2313 MapTile forEachTile (bool delegate (MapTile t) dg) {
2314 if (!dg) return none;
2315 foreach (int y; 0..tilesHeight) {
2316 foreach (int x; 0..tilesWidth) {
2317 auto t = tiles[x, y];
2318 if (t && t.visible && !t.spectral && t.isInstanceAlive) {
2319 if (dg(t)) return t;
2323 foreach (MapObject o; miscTileGrid.allObjects()) {
2324 auto mt = MapTile(o);
2326 if (mt.visible && !mt.spectral && mt.isInstanceAlive) {
2327 //writeln("special map tile: '", GetClassName(mt.Class), "'");
2328 if (dg(mt)) return mt;
2335 // ////////////////////////////////////////////////////////////////////////// //
2336 final void fixWallTiles () {
2337 foreach (int y; 0..tilesHeight) {
2338 foreach (int x; 0..tilesWidth) {
2339 auto t = getTileAt(x, y);
2342 if (y == tilesHeight-2) {
2343 writeln("0: tx=", x, "; tile=", t.objName, "; ty=", y, ": (", t.iy/16, ")");
2344 } else if (y == tilesHeight-1) {
2345 writeln("1: tx=", x, "; tile=", t.objName, "; ty=", y, ": (", t.iy/16, ")");
2351 foreach (MapTile t; miscTileGrid.allObjects()) {
2352 if (t.isInstanceAlive) t.beautifyTile();
2357 // ////////////////////////////////////////////////////////////////////////// //
2358 final MapTile isCollisionAtPoint (int px, int py, optional bool delegate (MapTile dg) dg) {
2359 if (!dg) dg = &cbCollisionAnySolid;
2360 return checkTilesInRect(px, py, 1, 1, dg);
2364 // ////////////////////////////////////////////////////////////////////////// //
2365 string scrGetKaliGift (MapTile altar, optional name gift) {
2368 // find other side of the altar
2369 int sx = player.ix, sy = player.iy;
2373 auto a2 = isCollisionAtPoint(altar.ix+18, altar.iy+8, &cbCollisionSacAltar);
2374 if (!a2) a2 = isCollisionAtPoint(altar.ix-8, altar.iy+8, &cbCollisionSacAltar);
2375 if (a2) { sx = a2.ix; sy = a2.iy; }
2378 if (global.favor <= -8) res = "SHE SEEMS VERY ANGRY WITH YOU!";
2379 else if (global.favor < 0) res = "SHE SEEMS ANGRY WITH YOU.";
2380 else if (global.favor == 0) res = "SHE HAS FORGIVEN YOU!";
2381 else if (global.favor >= 32) {
2382 if (global.kaliGift >= 3 && global.favor >= 32+(global.kaliGift-2)*16) {
2383 res = "YOU FEEL INVIGORATED!";
2384 global.kaliGift += 1;
2385 global.plife += global.randOther(4, 8);
2386 } else if (global.kaliGift >= 3) {
2387 res = "SHE SEEMS ECSTATIC WITH YOU!";
2388 } else if (global.bombs < 80) {
2389 res = "YOUR SATCHEL FEELS VERY FULL NOW!";
2390 global.kaliGift = 3;
2393 res = "YOU FEEL INVIGORATED!";
2394 global.kaliGift += 1;
2395 global.plife += global.randOther(4, 8);
2397 } else if (global.favor >= 16) {
2398 if (global.kaliGift >= 2) {
2399 res = "SHE SEEMS VERY HAPPY WITH YOU!";
2401 res = "SHE BESTOWS A GIFT UPON YOU!";
2402 global.kaliGift = 2;
2404 auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2407 obj = MakeMapObject(sx, sy-8, 'oPoof');
2412 if (gift) obj = MakeMapObject(sx, sy-8, gift);
2413 if (!obj) obj = MakeMapObject(sx, sy-8, 'oKapala');
2415 } else if (global.favor >= 8) {
2416 if (global.kaliGift >= 1) {
2417 res = "SHE SEEMS HAPPY WITH YOU.";
2419 res = "SHE BESTOWS A GIFT UPON YOU!";
2420 global.kaliGift = 1;
2421 //rAltar = instance_nearest(x, y, oSacAltarRight);
2422 //if (instance_exists(rAltar)) {
2424 auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2427 obj = MakeMapObject(sx, sy-8, 'oPoof');
2431 if (gift) obj = MakeMapObject(sx, sy-8, gift);
2433 auto n = global.randOther(1, 8);
2437 if (n == 1 && !global.hasCape && !global.hasJetpack) aname = 'oCapePickup';
2438 else if (n == 2 && !global.hasGloves) aname = 'oGloves';
2439 else if (n == 3 && !global.hasSpectacles) aname = 'oSpectacles';
2440 else if (n == 4 && !global.hasMitt) aname = 'oMitt';
2441 else if (n == 5 && !global.hasSpringShoes) aname = 'oSpringShoes';
2442 else if (n == 6 && !global.hasSpikeShoes) aname = 'oSpikeShoes';
2443 else if (n == 7 && !global.hasStickyBombs) aname = 'oPaste';
2444 else if (n == 8 && !global.hasCompass) aname = 'oCompass';
2446 obj = MakeMapObject(sx, sy-8, aname);
2452 obj = MakeMapObject(sx, sy-8, (global.hasJetpack ? 'oBombBox' : 'oJetpack'));
2458 } else if (global.favor > 0) {
2459 res = "SHE SEEMS PLEASED WITH YOU.";
2464 global.message = "";
2465 res = "KALI DEVOURS YOU!"; // sacrifice is player
2473 void performSacrifice (MapObject what, MapTile where) {
2474 if (!what || !what.isInstanceAlive) return;
2475 MakeMapObject(what.ix+8, what.iy+8, 'oFlame');
2476 if (where) where.playSound('sndSmallExplode'); else what.playSound('sndSmallExplode');
2477 if (!what.bloodless) what.scrCreateBlood(what.ix+8, what.iy+8, 3);
2479 string msg = "KALI ACCEPTS THE SACRIFICE!";
2481 auto idol = ItemGoldIdol(what);
2483 ++stats.totalSacrifices;
2484 if (global.favor <= -8) msg = "SHE SEEMS VERY ANGRY WITH YOU!";
2485 else if (global.favor < 0) msg = "SHE SEEMS ANGRY WITH YOU.";
2486 else if (global.favor >= 0) {
2487 // find other side of the altar
2488 int sx = player.ix, sy = player.iy;
2493 auto a2 = isCollisionAtPoint(altar.ix+18, altar.iy+8, &cbCollisionSacAltar);
2494 if (!a2) a2 = isCollisionAtPoint(altar.ix-8, altar.iy+8, &cbCollisionSacAltar);
2495 if (a2) { sx = a2.ix; sy = a2.iy; }
2498 auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2501 obj = MakeMapObject(sx, sy-8, 'oPoof');
2505 obj = MakeMapObject(sx, sy-16-8, 'oMonkeyGold');
2507 osdMessage(msg, 6.66);
2509 idol.instanceRemove();
2513 if (global.favor <= -8) {
2514 msg = "KALI DEVOURS THE SACRIFICE!";
2515 } else if (global.favor < 0) {
2516 global.favor += (what.status == MapObject::STUNNED ? what.favor : round(what.favor/2.0)); //k8: added `round()`
2517 if (what.favor > 0) what.favor = 0;
2519 global.favor += (what.status == MapObject::STUNNED ? what.favor : round(what.favor/2.0)); //k8: added `round()`
2523 if (myAltar.gift1 != "" and global.kaliGift == 0) scrGetKaliGift(myAltar.gift1);
2524 else if (myAltar.gift2 != "" and global.kaliGift == 1) scrGetKaliGift(myAltar.gift2);
2525 else scrGetKaliGift("");
2528 // sacrifice is player?
2529 if (what isa PlayerPawn) {
2530 ++stats.totalSelfSacrifices;
2531 msg = "KALI DEVOURS YOU!";
2532 player.visible = false;
2533 player.removeBallAndChain(temp:true);
2535 player.status = MapObject::DEAD;
2537 ++stats.totalSacrifices;
2538 auto msg2 = scrGetKaliGift(where);
2539 what.instanceRemove();
2540 if (msg2) msg = va("%s\n%s", msg, msg2);
2543 osdMessage(msg, 6.66);
2545 //!if (isRealLevel()) global.totalSacrifices += 1;
2547 //!global.messageTimer = 200;
2548 //!global.shake = 10;
2552 instance_create(x, y, oFlame);
2553 playSound(global.sndSmallExplode);
2554 scrCreateBlood(x, y, 3);
2555 global.message = "KALI ACCEPTS YOUR SACRIFICE!";
2556 if (global.favor <= -8) {
2557 global.message = "KALI DEVOURS YOUR SACRIFICE!";
2558 } else if (global.favor < 0) {
2559 if (status == 98) global.favor += favor*1.5; else global.favor += favor;
2560 if (favor > 0) favor = 0;
2562 if (status == 98) global.favor += favor*1.5; else global.favor += favor;
2565 if (myAltar.gift1 != "" and global.kaliGift == 0) scrGetFavorMsg(myAltar.gift1);
2566 else if (myAltar.gift2 != "" and global.kaliGift == 1) scrGetFavorMsg(myAltar.gift2);
2567 else scrGetFavorMsg("");
2569 global.messageTimer = 200;
2576 // ////////////////////////////////////////////////////////////////////////// //
2577 final void addBackgroundGfxDetails () {
2578 // add background details
2579 //if (global.customLevel || global.parallax) return;
2581 // bg = instance_create(16*randRoom(1, 42), 16*randRoom(1, 33), oCaveBG);
2582 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);
2583 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);
2584 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);
2585 else MakeMapBackTile('bgExtras', 32*global.randRoom(0, 1), 0, 32, 32, 16*global.randRoom(1, 42), 16*global.randRoom(1, 33), 10002);
2590 // ////////////////////////////////////////////////////////////////////////// //
2591 private final void fixRealViewStart () {
2592 int scale = global.scale;
2593 realViewStart.x = clamp(realViewStart.x, viewMin.x*scale, viewMax.x*scale-viewWidth);
2594 realViewStart.y = clamp(realViewStart.y, viewMin.y*scale, viewMax.y*scale-viewHeight);
2598 final int cameraCurrX () { return realViewStart.x/global.scale; }
2599 final int cameraCurrY () { return realViewStart.y/global.scale; }
2602 private final void fixViewStart () {
2603 int scale = global.scale;
2604 viewStart.x = clamp(viewStart.x, viewMin.x*scale, viewMax.x*scale-viewWidth);
2605 viewStart.y = clamp(viewStart.y, viewMin.y*scale, viewMax.y*scale-viewHeight);
2609 final void centerViewAtPlayer () {
2610 if (viewWidth < 1 || viewHeight < 1 || !player) return;
2611 centerViewAt(player.xCenter, player.yCenter);
2615 final void centerViewAt (int x, int y) {
2616 if (viewWidth < 1 || viewHeight < 1) return;
2618 cameraSlideToSpeed.x = 0;
2619 cameraSlideToSpeed.y = 0;
2620 cameraSlideToPlayer = 0;
2622 int scale = global.scale;
2625 realViewStart.x = clamp(x-viewWidth/2, 0, tilesWidth*16*scale-viewWidth);
2626 realViewStart.y = clamp(y-viewHeight/2, 0, tilesHeight*16*scale-viewHeight);
2629 viewStart.x = realViewStart.x;
2630 viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
2635 const int ViewPortToleranceX = 16*1+8;
2636 const int ViewPortToleranceY = 16*1+8;
2638 final void fixCamera () {
2639 if (!player) return;
2640 if (viewWidth < 1 || viewHeight < 1) return;
2641 int scale = global.scale;
2642 auto alwaysCenterX = global.config.alwaysCenterPlayer;
2643 auto alwaysCenterY = alwaysCenterX;
2644 // calculate offset from viewport center (in game units), and fix viewport
2646 int camDestX = player.ix+8;
2647 int camDestY = player.iy+8;
2648 if (!cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y) != 0) {
2649 // slide camera to point
2650 if (cameraSlideToSpeed.x) camDestX = cameraSlideToCurr.x;
2651 if (cameraSlideToSpeed.y) camDestY = cameraSlideToCurr.y;
2652 int dx = cameraSlideToDest.x-camDestX;
2653 int dy = cameraSlideToDest.y-camDestY;
2654 //writeln("speed:(", cameraSlideToSpeed.x, ",", cameraSlideToSpeed.y, "); cur:(", camDestX, ",", camDestY, "); dest:(", cameraSlideToDest.x, ",", cameraSlideToDest.y, "); delta=(", dx, ",", dy, ")");
2655 if (dx && cameraSlideToSpeed.x != 0) {
2656 alwaysCenterX = true;
2657 if (abs(dx) <= abs(cameraSlideToSpeed.x)) {
2658 camDestX = cameraSlideToDest.x;
2660 camDestX += sign(dx)*abs(cameraSlideToSpeed.x);
2663 if (dy && abs(cameraSlideToSpeed.y) != 0) {
2664 alwaysCenterY = true;
2665 if (abs(dy) <= cameraSlideToSpeed.y) {
2666 camDestY = cameraSlideToDest.y;
2668 camDestY += sign(dy)*abs(cameraSlideToSpeed.y);
2671 //writeln(" new:(", camDestX, ",", camDestY, ")");
2672 if (cameraSlideToSpeed.x) cameraSlideToCurr.x = camDestX;
2673 if (cameraSlideToSpeed.y) cameraSlideToCurr.y = camDestY;
2677 if (cameraSlideToSpeed.x && !cameraSlideToPlayer) {
2678 realViewStart.x = clamp(camDestX*scale, 0, tilesWidth*16*scale-viewWidth);
2679 } else if (!player.cameraBlockX) {
2680 int x = camDestX*scale;
2681 int cx = realViewStart.x;
2682 if (alwaysCenterX) {
2685 int xofs = x-(cx+viewWidth/2);
2686 if (xofs < -ViewPortToleranceX*scale) cx += xofs+ViewPortToleranceX*scale;
2687 else if (xofs > ViewPortToleranceX*scale) cx += xofs-ViewPortToleranceX*scale;
2689 // slide back to player?
2690 if (cameraSlideToPlayer && cameraSlideToSpeed.x) {
2691 int prevx = cameraSlideToCurr.x*scale;
2692 int dx = (cx-prevx)/scale;
2693 if (abs(dx) <= cameraSlideToSpeed.x) {
2694 writeln("BACKSLIDE X COMPLETE!");
2695 cameraSlideToSpeed.x = 0;
2697 cx = prevx+(sign(dx)*cameraSlideToSpeed.x)*scale;
2698 cameraSlideToCurr.x += sign(dx)*cameraSlideToSpeed.x;
2699 if ((dx < 0 && cx <= 0) || (dx > 0 && cx >= tilesWidth*16*scale-viewWidth)) {
2700 writeln("BACKSLIDE X COMPLETE!");
2701 cameraSlideToSpeed.x = 0;
2705 realViewStart.x = clamp(cx, 0, tilesWidth*16*scale-viewWidth);
2709 if (cameraSlideToSpeed.y && !cameraSlideToPlayer) {
2710 realViewStart.y = clamp(camDestY*scale, 0, tilesHeight*16*scale-viewHeight);
2711 } else if (!player.cameraBlockY) {
2712 int y = camDestY*scale;
2713 int cy = realViewStart.y;
2714 if (alwaysCenterY) {
2715 cy = y-viewHeight/2;
2717 int yofs = y-(cy+viewHeight/2);
2718 if (yofs < -ViewPortToleranceY*scale) cy += yofs+ViewPortToleranceY*scale;
2719 else if (yofs > ViewPortToleranceY*scale) cy += yofs-ViewPortToleranceY*scale;
2721 // slide back to player?
2722 if (cameraSlideToPlayer && cameraSlideToSpeed.y) {
2723 int prevy = cameraSlideToCurr.y*scale;
2724 int dy = (cy-prevy)/scale;
2725 if (abs(dy) <= cameraSlideToSpeed.y) {
2726 writeln("BACKSLIDE Y COMPLETE!");
2727 cameraSlideToSpeed.y = 0;
2729 cy = prevy+(sign(dy)*cameraSlideToSpeed.y)*scale;
2730 cameraSlideToCurr.y += sign(dy)*cameraSlideToSpeed.y;
2731 if ((dy < 0 && cy <= 0) || (dy > 0 && cy >= tilesHeight*16*scale-viewHeight)) {
2732 writeln("BACKSLIDE Y COMPLETE!");
2733 cameraSlideToSpeed.y = 0;
2737 realViewStart.y = clamp(cy, 0, tilesHeight*16*scale-viewHeight);
2740 if (cameraSlideToPlayer && (cameraSlideToSpeed.x|cameraSlideToSpeed.y) == 0) cameraSlideToPlayer = 0;
2743 //writeln(" new2:(", cameraCurrX, ",", cameraCurrY, ")");
2745 viewStart.x = realViewStart.x;
2746 viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
2751 // ////////////////////////////////////////////////////////////////////////// //
2752 // x0 and y0 are non-scaled (and will be scaled)
2753 final void drawSpriteAt (name sprName, float frnumf, int x0, int y0, optional bool small) {
2754 if (!sprName) return;
2755 auto spr = sprStore[sprName];
2756 if (!spr || !spr.frames.length) return;
2757 int scale = global.scale;
2760 int frnum = max(0, trunc(frnumf))%spr.frames.length;
2761 auto sfr = spr.frames[frnum];
2762 int sx0 = x0-sfr.xofs*scale;
2763 int sy0 = y0-sfr.yofs*scale;
2764 if (small && scale > 1) {
2765 sfr.tex.blitExt(sx0, sy0, round(sx0+sfr.tex.width*(scale/2.0)), round(sy0+sfr.tex.height*(scale/2.0)), 0, 0);
2767 sfr.tex.blitAt(sx0, sy0, scale);
2772 // x0 and y0 are non-scaled (and will be scaled)
2773 final void drawTextAt (int x0, int y0, string text) {
2775 int scale = global.scale;
2778 sprStore.renderText(x0, y0, text, scale);
2782 void renderCompass (float currFrameDelta) {
2783 if (!global.hasCompass) return;
2786 if (isRoom("rOlmec")) {
2789 } else if (isRoom("rOlmec2")) {
2795 bool hasMessage = osdHasMessage();
2796 foreach (MapTile et; allExits) {
2798 int exitX = et.ix, exitY = et.iy;
2799 int vx0 = viewStart.x/global.scale, vy0 = viewStart.y/global.scale;
2800 int vx1 = (viewStart.x+viewWidth)/global.scale;
2801 int vy1 = (viewStart.y+viewHeight)/global.scale;
2802 if (exitY > vy1-16) {
2804 drawSpriteAt(hasMessage ? 'sCompassSmallLL' : 'sCompassLL', 0, 0, 224);
2805 } else if (exitX > vx1-16) {
2806 drawSpriteAt(hasMessage ? 'sCompassSmallLR' : 'sCompassLR', 0, 304, 224);
2808 drawSpriteAt(hasMessage ? 'sCompassSmallDown' : 'sCompassDown', 0, exitX-vx0, 224);
2810 } else if (exitX < vx0) {
2811 drawSpriteAt(hasMessage ? 'sCompassSmallLeft' : 'sCompassLeft', 0, 0, exitY-vy0);
2812 } else if (exitX > vx1-16) {
2813 drawSpriteAt(hasMessage ? 'sCompassSmallRight' : 'sCompassRight', 0, 304, exitY-vy0);
2819 final bool totalsNameCmpCB (ref GameStats::TotalItem a, ref GameStats::TotalItem b) {
2820 auto sa = string(a.objName);
2821 auto sb = string(b.objName);
2825 void renderTransitionInfo (float currFrameDelta) {
2828 GameStats.sortTotalsList(stats.kills, &totalsNameCmpCB);
2831 foreach (int idx, ref auto k; stats.kills) {
2832 string s = string(k);
2833 maxLen = max(maxLen, s.length);
2837 sprStore.loadFont('sFontSmall');
2838 Video.color = 0xff_ff_00;
2839 foreach (int idx, ref auto k; stats.kills) {
2841 foreach (int xidx, ref auto d; stats.totalKills) {
2842 if (d.objName == k) { deaths = d.count; break; }
2844 //drawTextAt(16, 4+idx*8, va("%s: %d (%d)", string(k.objName).toUpperCase, k.count, deaths));
2845 drawTextAt(16, 4+idx*8, string(k).toUpperCase);
2846 drawTextAt(16+maxLen, 4+idx*8, va(": %d (%d)", k.count, deaths));
2852 void renderGhostTimer (float currFrameDelta) {
2853 if (ghostTimeLeft <= 0) return;
2854 //ghostTimeLeft /= 30; // frames -> seconds
2856 int hgt = Video.screenHeight-64;
2857 if (hgt < 1) return;
2858 int rhgt = round(float(hgt)*(float(ghostTimeLeft)/30.0)/1000.0);
2859 //writeln("hgt=", hgt, "; rhgt=", rhgt, "; gtl=", ghostTimeLeft/30);
2861 auto oclr = Video.color;
2862 Video.color = 0xcf_ff_7f_00;
2863 Video.fillRect(Video.screenWidth-20, 32, 16, hgt-rhgt);
2864 Video.color = 0x7f_ff_7f_00;
2865 Video.fillRect(Video.screenWidth-20, 32+(hgt-rhgt), 16, rhgt);
2871 void renderHUD (float currFrameDelta) {
2872 if (inWinCutscene) return;
2874 if (levelKind == LevelKind.Transition) { renderTransitionInfo(currFrameDelta); return; }
2882 bool scumSmallHud = global.config.scumSmallHud;
2883 if (!global.config.optSGAmmo) moneyX = ammoX;
2886 sprStore.loadFont('sFontSmall');
2889 sprStore.loadFont('sFont');
2892 Video.color = 0xff_ff_ff;
2896 if (global.plife == 1) {
2897 drawSpriteAt('sHeartSmallBlink', global.heartBlink, lifeX+2, 4-hhup);
2898 global.heartBlink += 0.1;
2899 if (global.heartBlink > 3) global.heartBlink = 0;
2901 drawSpriteAt('sHeartSmall', -1, lifeX+2, 4-hhup);
2902 global.heartBlink = 0;
2905 if (global.plife == 1) {
2906 drawSpriteAt('sHeartBlink', global.heartBlink, lifeX, 8-hhup);
2907 global.heartBlink += 0.1;
2908 if (global.heartBlink > 3) global.heartBlink = 0;
2910 drawSpriteAt('sHeart', -1, lifeX, 8-hhup);
2911 global.heartBlink = 0;
2915 int life = clamp(global.plife, 0, 99);
2916 //if (!scumHud && life > 99) life = 99;
2917 drawTextAt(lifeX+16, 8-hhup, va("%d", life));
2920 if (global.hasStickyBombs && global.stickyBombsActive) {
2921 if (scumSmallHud) drawSpriteAt('sStickyBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sStickyBombIcon', -1, bombX, 8-hhup);
2923 if (scumSmallHud) drawSpriteAt('sBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sBombIcon', -1, bombX, 8-hhup);
2925 int n = global.bombs;
2926 if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
2927 drawTextAt(bombX+16, 8-hhup, va("%d", n));
2930 if (scumSmallHud) drawSpriteAt('sRopeIconSmall', -1, ropeX+2, 4-hhup); else drawSpriteAt('sRopeIcon', -1, ropeX, 8-hhup);
2932 if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
2933 drawTextAt(ropeX+16, 8-hhup, va("%d", n));
2936 if (global.config.optSGAmmo) {
2937 if (scumSmallHud) drawSpriteAt('sShellsIcon', -1, ammoX+6, 8-hhup); else drawSpriteAt('sShellsIcon', -1, ammoX+7, 12-hhup);
2939 if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
2940 drawTextAt(ammoX+16, 8-hhup, va("%d", n));
2944 if (scumSmallHud) drawSpriteAt('sDollarSignSmall', -1, moneyX+2, 3-hhup); else drawSpriteAt('sDollarSign', -1, moneyX, 8-hhup);
2945 drawTextAt(moneyX+16, 8-hhup, va("%d", stats.money-xmoney));
2947 int ity = (scumSmallHud ? 18-hhup : 24-hhup);
2950 if (global.hasUdjatEye) {
2951 if (global.udjatBlink) drawSpriteAt('sUdjatEyeIcon2', -1, n, ity); else drawSpriteAt('sUdjatEyeIcon', -1, n, ity);
2954 if (global.hasAnkh) { drawSpriteAt('sAnkhIcon', -1, n, ity); n += 20; }
2955 if (global.hasCrown) { drawSpriteAt('sCrownIcon', -1, n, ity); n += 20; }
2956 if (global.hasKapala) {
2957 if (global.bloodLevel == 0) drawSpriteAt('sKapalaIcon', 0, n, ity);
2958 else if (global.bloodLevel <= 2) drawSpriteAt('sKapalaIcon', 1, n, ity);
2959 else if (global.bloodLevel <= 4) drawSpriteAt('sKapalaIcon', 2, n, ity);
2960 else if (global.bloodLevel <= 6) drawSpriteAt('sKapalaIcon', 3, n, ity);
2961 else if (global.bloodLevel <= 8) drawSpriteAt('sKapalaIcon', 4, n, ity);
2964 if (global.hasSpectacles) { drawSpriteAt('sSpectaclesIcon', -1, n, ity); n += 20; }
2965 if (global.hasGloves) { drawSpriteAt('sGlovesIcon', -1, n, ity); n += 20; }
2966 if (global.hasMitt) { drawSpriteAt('sMittIcon', -1, n, ity); n += 20; }
2967 if (global.hasSpringShoes) { drawSpriteAt('sSpringShoesIcon', -1, n, ity); n += 20; }
2968 if (global.hasJordans) { drawSpriteAt('sJordansIcon', -1, n, ity); n += 20; }
2969 if (global.hasSpikeShoes) { drawSpriteAt('sSpikeShoesIcon', -1, n, ity); n += 20; }
2970 if (global.hasCape) { drawSpriteAt('sCapeIcon', -1, n, ity); n += 20; }
2971 if (global.hasJetpack) { drawSpriteAt('sJetpackIcon', -1, n, ity); n += 20; }
2972 if (global.hasStickyBombs) { drawSpriteAt('sPasteIcon', -1, n, ity); n += 20; }
2973 if (global.hasCompass) { drawSpriteAt('sCompassIcon', -1, n, ity); n += 20; }
2974 if (global.hasParachute) { drawSpriteAt('sParachuteIcon', -1, n, ity); n += 20; }
2976 if (player.holdItem && player.holdItem.objName == 'Bow' && global.arrows > 0) {
2979 while (m <= global.arrows && m <= 20 && malpha > 0) {
2980 Video.color = trunc(malpha*255)<<24|0xff_ff_ff;
2981 drawSpriteAt('sArrowIcon', -1, n, ity);
2983 if (m > 10) malpha -= 0.1; //and global.arrows > 20 malpha -= 0.1
2986 Video.color = 0xff_ff_ff;
2990 sprStore.loadFont('sFontSmall');
2991 Video.color = 0xff_ff_00;
2992 if (scumSmallHud) drawTextAt(moneyX+8, 8+8-hhup, va("+%d", xmoney));
2993 else drawTextAt(moneyX, 8+16-hhup, va("+%d", xmoney));
2996 if (global.config.ghostShowTime) renderGhostTimer(currFrameDelta);
3000 // ////////////////////////////////////////////////////////////////////////// //
3001 private transient array!MapEntity renderVisibleCids;
3002 private transient array!MapTile renderMidTiles, renderFrontTiles; // normal, with fg
3004 final bool renderSortByDepth (MapEntity oa, MapEntity ob) {
3005 //MapObject oa = MapObject(a);
3006 //MapObject ob = MapObject(b);
3007 auto da = oa.depth, db = ob.depth;
3008 if (da == db) return (oa.objId < ob.objId);
3013 const int RenderEdgePix = 32;
3015 void renderWithOfs (int xofs, int yofs, float currFrameDelta) {
3016 int scale = global.scale;
3019 Video.color = 0xff_ff_ff;
3021 // render cave background
3023 int bgw = levBGImg.tex.width*scale;
3024 int bgh = levBGImg.tex.height*scale;
3025 int bgTW = (tilesWidth*tsz+bgw-1)/bgw;
3026 int bgTH = (tilesHeight*tsz+bgh-1)/bgh;
3027 int bgX0 = max(0, xofs/bgw);
3028 int bgY0 = max(0, yofs/bgh);
3029 int bgX1 = min(bgTW, (xofs+viewWidth+bgw-1)/bgw);
3030 int bgY1 = min(bgTH, (yofs+viewHeight+bgh-1)/bgh);
3031 foreach (int ty; bgY0..bgY1) {
3032 foreach (int tx; bgX0..bgX1) {
3033 int x0 = tx*bgw-xofs;
3034 int y0 = ty*bgh-yofs;
3035 levBGImg.tex.blitAt(x0, y0, scale);
3040 // render background tiles
3041 for (MapBackTile bt = backtiles; bt; bt = bt.next) {
3042 bt.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3045 // collect visible objects
3046 renderVisibleCids.length -= renderVisibleCids.length;
3047 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)) {
3048 if (!mt.visible || !mt.isInstanceAlive) continue;
3049 //Video.color = (mt.moveable ? 0xff_7f_00 : 0xff_ff_ff);
3050 //!mt.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3051 renderVisibleCids[$] = mt;
3053 // render objects (and player)
3054 if (player && player.visible) renderVisibleCids[$] = player; // include player in render list
3055 auto ogrid = objGrid;
3056 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)) {
3057 if (o.visible && o.isInstanceAlive) renderVisibleCids[$] = o;
3060 // collect stationary tiles
3061 int tileX0 = max(0, xofs/tsz);
3062 int tileY0 = max(0, yofs/tsz);
3063 int tileX1 = min(tilesWidth, (xofs+viewWidth+tsz-1)/tsz);
3064 int tileY1 = min(tilesHeight, (yofs+viewHeight+tsz-1)/tsz);
3066 // render backs; collect tile arrays
3067 renderMidTiles.length -= renderMidTiles.length; // don't realloc
3068 renderFrontTiles.length -= renderFrontTiles.length; // don't realloc
3070 foreach (int ty; tileY0..tileY1) {
3071 foreach (int tx; tileX0..tileX1) {
3072 auto tile = getTileAt(tx, ty);
3073 if (tile && tile.visible && tile.isInstanceAlive) {
3074 renderMidTiles[$] = tile;
3075 if (tile.bgfront) renderFrontTiles[$] = tile;
3076 if (tile.bgback) tile.drawWithOfsBack(xofs, yofs, scale, currFrameDelta);
3081 // render "mid" (i.e. normal) tiles
3082 foreach (MapTile tile; renderMidTiles) {
3083 //tile.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3084 renderVisibleCids[$] = tile;
3087 EntityGrid.sortEntList(renderVisibleCids, &renderSortByDepth);
3090 auto depth4Start = 0;
3091 foreach (auto xidx, MapEntity o; renderVisibleCids) {
3098 foreach (auto idx; depth4Start..renderVisibleCids.length; reverse) {
3099 MapEntity o = renderVisibleCids[idx];
3100 //if (idx && renderSortByDepth(o, renderVisibleCids[idx-1])) writeln("VIOLATION!");
3101 o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3104 // render front tile parts (depth 3.5)
3105 foreach (MapTile tile; renderFrontTiles) tile.drawWithOfsFront(xofs, yofs, scale, currFrameDelta);
3107 // render items with depth 3 and less
3108 foreach (auto idx; 0..depth4Start; reverse) {
3109 MapEntity o = renderVisibleCids[idx];
3110 o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
3113 renderVisibleCids.length -= renderVisibleCids.length;
3115 // render player post-effects, so cape or jetpack will be correctly rendered on level exit, for example
3116 player.lastDrawWithOfs(xofs, yofs, scale, currFrameDelta);
3118 if (global.config.drawHUD) renderHUD(currFrameDelta);
3119 renderCompass(currFrameDelta);
3121 float osdTimeLeft, osdTimeStart;
3122 string msg = osdGetMessage(out osdTimeLeft, out osdTimeStart);
3124 auto ct = GetTickCount();
3126 sprStore.loadFont('sFontSmall');
3127 auto msgHeight = sprStore.getMultilineTextHeight(msg, msgScale);
3128 int x = Video.screenWidth/2;
3129 int y = Video.screenHeight-64-msgHeight;
3130 auto oldColor = Video.color;
3131 Video.color = 0xff_ff_00;
3132 if (osdTimeLeft < 0.5) {
3133 int alpha = 255-clamp(int(510*osdTimeLeft), 0, 255);
3134 Video.color = Video.color|(alpha<<24);
3135 } else if (ct-osdTimeStart < 0.5) {
3136 osdTimeStart = ct-osdTimeStart;
3137 int alpha = 255-clamp(int(510*osdTimeStart), 0, 255);
3138 Video.color = Video.color|(alpha<<24);
3140 sprStore.renderMultilineTextCentered(x, y, msg, msgScale);
3141 Video.color = oldColor;
3144 if (inWinCutscene) renderWinCutsceneOverlay();
3145 Video.color = 0xff_ff_ff;
3149 // ////////////////////////////////////////////////////////////////////////// //
3150 final class!MapObject findGameObjectClassByName (name aname) {
3151 if (!aname) return none; // just in case
3152 auto co = FindClassByGameObjName(aname);
3154 writeln("***UNKNOWN GAME OBJECT: '", aname, "'");
3157 co = GetClassReplacement(co);
3158 if (!co) FatalError("findGameObjectClassByName: WTF?!");
3159 if (!class!MapObject(co)) FatalError("findGameTileClassByName: object '%n' is not a map object, it is a `%n`", aname, GetClassName(co));
3160 return class!MapObject(co);
3164 final class!MapTile findGameTileClassByName (name aname) {
3165 if (!aname) return none; // just in case
3166 auto co = FindClassByGameObjName(aname);
3167 if (!co) return MapTile; // unknown names will be routed directly to tile object
3168 co = GetClassReplacement(co);
3169 if (!co) FatalError("findGameTileClassByName: WTF?!");
3170 if (!class!MapTile(co)) FatalError("findGameTileClassByName: object '%n' is not a tile, it is a `%n`", aname, GetClassName(co));
3171 return class!MapTile(co);
3175 final MapObject findAnyObjectOfType (name aname) {
3176 if (!aname) return none;
3177 auto cls = FindClassByGameObjName(aname);
3178 if (!cls) return none;
3179 for (auto cid = objGrid.getFirstObject(); cid; cid = objGrid.getNextObject(cid)) {
3180 MapObject obj = objGrid.getObject(MapObject, cid);
3181 if (!obj || obj.spectral || !obj.isInstanceAlive) continue;
3182 if (obj isa cls) return obj;
3188 // ////////////////////////////////////////////////////////////////////////// //
3189 final MapTile CreateMapTile (int xpos, int ypos, name aname) {
3190 if (!aname) FatalError("cannot create typeless tile");
3191 //MapTile tile = SpawnObject(aname == 'oRope' ? MapTileRope : MapTile);
3192 auto tclass = findGameTileClassByName(aname);
3193 if (!tclass) return none;
3194 MapTile tile = SpawnObject(tclass);
3195 tile.global = global;
3197 tile.objName = aname;
3198 tile.objType = aname; // just in case
3201 if (!tile.initialize()) FatalError("cannot create tile '%n'", aname);
3206 final bool isRopePlacedAt (int x, int y) {
3208 foreach (ref auto v; covered) v = false;
3209 foreach (MapTile t; miscTileGrid.inRectPix(x, y-8, 1, 17, precise:false)) {
3210 if (!cbIsRopeTile(t)) continue;
3211 if (t.ix != x) continue;
3212 if (t.iy == y) return true;
3213 foreach (int ty; t.iy..t.iy+8) {
3215 if (d >= 0 && d < covered.length) covered[d] = true;
3218 // check if the whole rope height is completely covered with ropes
3219 foreach (auto v; covered) if (!v) return false;
3224 // won't call `onDestroy()`
3225 final void RemoveMapTile (int tileX, int tileY) {
3226 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
3227 if (tiles[tileX, tileY]) checkWater = true;
3228 delete tiles[tileX, tileY];
3229 tiles[tileX, tileY] = none;
3234 final MapTile MakeMapTile (int mapx, int mapy, name aname/*, optional name sprname*/) {
3235 //if (mapy >= tilesHeight) { writeln("OOM: ", aname, "; ty=", mapy, "; hgt=", tilesHeight); }
3236 if (mapx < 0 || mapx >= tilesWidth || mapy < 0 || mapy >= tilesHeight) return none;
3238 // if we already have rope tile there, there is no reason to add another one
3239 if (aname == 'oRope') {
3240 if (isRopePlacedAt(mapx*16, mapy*16)) {
3241 //writeln("dupe rope (0)!");
3246 auto tile = CreateMapTile(mapx*16, mapy*16, aname);
3247 if (tile.moveable || tile.toSpecialGrid) {
3248 // moveable tiles goes to the separate list
3249 miscTileGrid.insert(tile);
3251 setTileAt(mapx, mapy, tile);
3255 case 'oEntrance': registerEnter(tile); break;
3256 case 'oExit': registerExit(tile); break;
3263 final void MarkTileAsWet (int tileX, int tileY) {
3264 auto t = getTileAt(tileX, tileY);
3265 if (t) t.wet = true;
3269 final MapTile MakeMapTileAtPix (int xpix, int ypix, name aname/*, optional name sprname*/) {
3270 if (xpix%16 == 0 && ypix%16 == 0) return MakeMapTile(xpix/16, ypix/16, aname);
3271 //if (mapx < 0 || mapx >= TilesWidth || mapy < 0 || mapy >= TilesHeight) return none;
3273 // if we already have rope tile there, there is no reason to add another one
3274 if (aname == 'oRope') {
3275 if (isRopePlacedAt(xpix, ypix)) {
3276 //writeln("dupe rope (0)!");
3281 auto tile = CreateMapTile(xpix, ypix, aname);
3282 // non-aligned tiles goes to the special grid
3283 miscTileGrid.insert(tile);
3286 case 'oEntrance': registerEnter(tile); break;
3287 case 'oExit': registerExit(tile); break;
3294 final MapTile MakeMapRopeTileAt (int x0, int y0) {
3295 // if we already have rope tile there, there is no reason to add another one
3296 if (isRopePlacedAt(x0, y0)) {
3297 //writeln("dupe rope (1)!");
3301 auto tile = CreateMapTile(x0, y0, 'oRope');
3302 miscTileGrid.insert(tile);
3308 // ////////////////////////////////////////////////////////////////////////// //
3309 final MapBackTile CreateBgTile (name sprName, optional int atx0, optional int aty0, optional int aw, optional int ah) {
3310 BackTileImage img = bgtileStore[sprName];
3311 auto res = SpawnObject(MapBackTile);
3312 res.global = global;
3315 res.bgtName = sprName;
3316 if (specified_atx0) res.tx0 = atx0;
3317 if (specified_aty0) res.ty0 = aty0;
3318 if (specified_aw) res.w = aw;
3319 if (specified_ah) res.h = ah;
3320 if (!res.initialize()) FatalError("cannot create background tile with image '%n'", sprName);
3325 // ////////////////////////////////////////////////////////////////////////// //
3327 background The background asset from which the new tile will be extracted.
3328 left The x coordinate of the left of the new tile, relative to the background asset's top left corner.
3329 top The y coordinate of the top of the new tile, relative to the background assets top left corner.
3330 width The width of the tile.
3331 height The height of the tile.
3332 x The x position in the room to place the tile.
3333 y The y position in the room to place the tile.
3334 depth The depth at which to place the tile.
3336 final void MakeMapBackTile (name bgname, int left, int top, int width, int height, int x, int y, int depth) {
3337 if (width < 1 || height < 1 || !bgname) return;
3338 auto bgt = bgtileStore[bgname];
3339 if (!bgt) FatalError("cannot load background '%n'", bgname);
3340 MapBackTile bt = SpawnObject(MapBackTile);
3343 bt.objName = bgname;
3345 bt.bgtName = bgname;
3353 // find a place for it
3358 // back tiles with the highest depth should come first
3359 MapBackTile ct = backtiles, cprev = none;
3360 while (ct && depth <= ct.depth) { cprev = ct; ct = ct.next; }
3363 bt.next = cprev.next;
3366 bt.next = backtiles;
3372 // ////////////////////////////////////////////////////////////////////////// //
3373 final MapObject SpawnMapObjectWithClass (class!MapObject oclass) {
3374 if (!oclass) return none;
3376 MapObject obj = SpawnObject(oclass);
3377 if (!obj) FatalError("cannot create map object '%n'", GetClassName(oclass));
3379 //if (aname == 'oShells' || aname == 'oShellSingle' || aname == 'oRopePile') writeln("*** CREATED '", aname, "'");
3381 obj.global = global;
3388 final MapObject SpawnMapObject (name aname) {
3389 if (!aname) return none;
3390 return SpawnMapObjectWithClass(findGameObjectClassByName(aname));
3394 final MapObject PutSpawnedMapObject (int x, int y, MapObject obj) {
3395 if (!obj /*|| obj.global || obj.level*/) return none; // oops
3399 if (!obj.initialize()) { delete obj; return none; } // not fatal
3407 final MapObject MakeMapObject (int x, int y, name aname) {
3408 MapObject obj = SpawnMapObject(aname);
3409 obj = PutSpawnedMapObject(x, y, obj);
3414 // ////////////////////////////////////////////////////////////////////////// //
3415 int winCutSceneTimer = -1;
3416 int winVolcanoTimer = -1;
3417 int winCutScenePhase = 0;
3418 int winSceneDrawStatus = 0;
3419 int winMoneyCount = 0;
3421 bool winFadeOut = false;
3422 int winFadeLevel = 0;
3423 int winCutsceneSkip = 0; // 1: waiting for pay release; 2: pay released, do skip
3424 bool winCutsceneSwitchToNext = false;
3427 void startWinCutscene () {
3429 winCutsceneSwitchToNext = false;
3430 winCutsceneSkip = 0;
3431 isKeyPressed(GameConfig::Key.Pay);
3432 isKeyReleased(GameConfig::Key.Pay);
3434 auto olddel = ImmediateDelete;
3435 ImmediateDelete = false;
3441 addBackgroundGfxDetails();
3443 levBGImgName = 'bgCave';
3444 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
3446 blockWaterChecking = true;
3452 ImmediateDelete = olddel;
3453 CollectGarbage(true); // destroy delayed objects too
3455 if (dumpGridStats) {
3456 miscTileGrid.dumpStats();
3457 objGrid.dumpStats();
3460 playerExited = false; // just in case
3468 winCutSceneTimer = -1;
3469 winCutScenePhase = 0;
3472 if (global.config.gameMode != GameConfig::GameMode.Vanilla) {
3473 if (global.config.bizarre) {
3474 global.yasmScore = 1;
3475 global.config.bizarrePlusTitle = true;
3478 array!MapTile toReplace;
3479 forEachTile(delegate bool (MapTile t) {
3480 if (t.objType == 'oGTemple' ||
3481 t.objType == 'oIce' ||
3482 t.objType == 'oDark' ||
3483 t.objType == 'oBrick' ||
3484 t.objType == 'oLush')
3491 foreach (MapTile t; miscTileGrid.allObjects()) {
3492 if (t.objType == 'oGTemple' ||
3493 t.objType == 'oIce' ||
3494 t.objType == 'oDark' ||
3495 t.objType == 'oBrick' ||
3496 t.objType == 'oLush')
3502 foreach (MapTile t; toReplace) {
3504 t.cleanDeath = true;
3505 if (rand(1,120) == 1) instance_change(oGTemple, false);
3506 else if (rand(1,100) == 1) instance_change(oIce, false);
3507 else if (rand(1,90) == 1) instance_change(oDark, false);
3508 else if (rand(1,80) == 1) instance_change(oBrick, false);
3509 else if (rand(1,70) == 1) instance_change(oLush, false);
3517 if (rand(1,5) == 1) instance_change(oLush, false);
3522 //!instance_create(0, 0, oBricks);
3524 //shakeToggle = false;
3525 //oPDummy.status = 2;
3530 if (global.kaliPunish >= 2) {
3531 instance_create(oPDummy.x, oPDummy.y+2, oBall2);
3532 obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
3534 obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
3536 obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
3538 obj = instance_create(oPDummy.x, oPDummy.y, oChain2);
3545 void startWinCutsceneVolcano () {
3547 writeln("VOLCANO HOLD 0: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
3548 writeln("VOLCANO PICK 0: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
3552 winCutsceneSwitchToNext = false;
3553 auto olddel = ImmediateDelete;
3554 ImmediateDelete = false;
3559 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
3561 blockWaterChecking = true;
3563 ImmediateDelete = olddel;
3564 CollectGarbage(true); // destroy delayed objects too
3566 spawnPlayerAt(2*16+8, 11*16+8);
3567 player.dir = MapEntity::Dir.Right;
3569 playerExited = false; // just in case
3577 winCutSceneTimer = -1;
3578 winCutScenePhase = 0;
3580 MakeMapTile(0, 0, 'oEnd2BG');
3581 realViewStart.x = 0;
3582 realViewStart.y = 0;
3591 player.dead = false;
3592 player.active = true;
3593 player.visible = false;
3594 player.removeBallAndChain(temp:true);
3595 player.stunned = false;
3596 player.status = MapObject::FALLING;
3597 if (player.holdItem) player.holdItem.visible = false;
3598 player.fltx = 320/2;
3602 writeln("VOLCANO HOLD 1: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
3603 writeln("VOLCANO PICK 1: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
3608 void startWinCutsceneWinFall () {
3610 writeln("FALL HOLD 0: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
3611 writeln("FALL PICK 0: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
3615 winCutsceneSwitchToNext = false;
3617 auto olddel = ImmediateDelete;
3618 ImmediateDelete = false;
3623 setMenuTilesVisible(false);
3625 //addBackgroundGfxDetails();
3628 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
3630 blockWaterChecking = true;
3636 ImmediateDelete = olddel;
3637 CollectGarbage(true); // destroy delayed objects too
3639 if (dumpGridStats) {
3640 miscTileGrid.dumpStats();
3641 objGrid.dumpStats();
3644 playerExited = false; // just in case
3652 winCutSceneTimer = -1;
3653 winCutScenePhase = 0;
3655 player.dead = false;
3656 player.active = true;
3657 player.visible = false;
3658 player.removeBallAndChain(temp:true);
3659 player.stunned = false;
3660 player.status = MapObject::FALLING;
3661 if (player.holdItem) player.holdItem.visible = false;
3662 player.fltx = 320/2;
3665 winSceneDrawStatus = 0;
3672 writeln("FALL HOLD 1: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
3673 writeln("FALL PICK 1: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
3678 void setGameOver () {
3679 if (inWinCutscene) {
3680 player.visible = false;
3681 player.removeBallAndChain(temp:true);
3682 if (player.holdItem) player.holdItem.visible = false;
3685 if (inWinCutscene > 0) {
3688 winSceneDrawStatus = 8;
3693 MapTile findEndPlatTile () {
3694 return forEachTile(delegate bool (MapTile t) { return (t isa MapTileEndPlat); });
3698 MapObject findBigTreasure () {
3699 return forEachObject(delegate bool (MapObject o) { return (o isa MapObjectBigTreasure); });
3703 void setMenuTilesVisible (bool vis) {
3705 forEachTile(delegate bool (MapTile t) {
3706 if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
3707 t.invisible = false;
3712 forEachTile(delegate bool (MapTile t) {
3713 if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
3722 void setMenuTilesOnTop () {
3723 forEachTile(delegate bool (MapTile t) {
3724 if (t isa TitleTileBlack || t isa TitleTileSpecialNonSolidInvincible) {
3732 void winCutscenePlayerControl (PlayerPawn plr) {
3733 auto payPress = isKeyPressed(GameConfig::Key.Pay);
3734 auto payRelease = isKeyReleased(GameConfig::Key.Pay);
3736 switch (winCutsceneSkip) {
3737 case 0: // nothing was pressed
3738 if (payPress) winCutsceneSkip = 1;
3740 case 1: // waiting for pay release
3741 if (payRelease) winCutsceneSkip = 2;
3743 case 2: // pay released, do skip
3748 // first winning room
3749 if (inWinCutscene == 1) {
3750 if (plr.ix < 448+8) {
3755 // waiting for chest to open
3756 if (winCutScenePhase == 0) {
3757 winCutSceneTimer = 120/2;
3758 winCutScenePhase = 1;
3763 if (winCutScenePhase == 1) {
3764 if (--winCutSceneTimer == 0) {
3765 winCutScenePhase = 2;
3766 winCutSceneTimer = 20;
3767 forEachObject(delegate bool (MapObject o) {
3768 if (o isa MapObjectBigChest) {
3769 o.setSprite(global.config.gameMode == GameConfig::GameMode.Vanilla ? 'sBigChestOpen' : 'sBigChestOpen2');
3770 auto treasure = MakeMapObject(o.ix, o.iy, 'oBigTreasure');
3774 o.playSound('sndClick');
3775 //!!!if (global.config.gameMode != GameConfig::GameMode.Vanilla) scrSprayGems(oBigChest.x+24, oBigChest.y+24);
3785 if (winCutScenePhase == 2) {
3786 if (--winCutSceneTimer == 0) {
3787 winCutScenePhase = 3;
3788 winCutSceneTimer = 50;
3794 if (winCutScenePhase == 3) {
3795 auto ep = findEndPlatTile();
3796 if (ep) MakeMapObject(ep.ix+global.randOther(0, 80), /*ep.iy*/192+32, 'oBurn');
3797 if (--winCutSceneTimer == 0) {
3798 winCutScenePhase = 4;
3799 winCutSceneTimer = 10;
3800 if (ep) MakeMapObject(ep.ix, ep.iy+30, 'oLavaSpray');
3806 // lava pump first accel
3807 if (winCutScenePhase == 4) {
3808 if (--winCutSceneTimer == 0) {
3809 forEachObject(delegate bool (MapObject o) {
3810 if (o isa MapObjectLavaSpray) o.yAcc = -0.1;
3816 // lava pump complete
3817 if (winCutScenePhase == 5) {
3818 if (--winCutSceneTimer == 0) {
3819 //if (oLavaSpray) oLavaSpray.yAcc = -0.1;
3820 startWinCutsceneVolcano();
3829 if (inWinCutscene == 2) {
3833 if (winCutScenePhase == 0) {
3834 winCutSceneTimer = 50;
3835 winCutScenePhase = 1;
3836 winVolcanoTimer = 10;
3840 if (winVolcanoTimer > 0) {
3841 if (--winVolcanoTimer == 0) {
3842 MakeMapObject(224+global.randOther(0,48), 144+global.randOther(0,8), 'oVolcanoFlame');
3843 winVolcanoTimer = global.randOther(10, 20);
3848 if (winCutScenePhase == 1) {
3849 if (--winCutSceneTimer == 0) {
3850 winCutSceneTimer = 30;
3851 winCutScenePhase = 2;
3852 auto sil = MakeMapObject(240, 132, 'oPlayerSil');
3860 if (winCutScenePhase == 2) {
3861 if (--winCutSceneTimer == 0) {
3862 winCutScenePhase = 3;
3863 auto sil = MakeMapObject(240, 132, 'oTreasureSil');
3873 // winning camel room
3874 if (inWinCutscene == 3) {
3875 //if (!player.holdItem) writeln("SCENE 3: LOST ITEM!");
3877 if (!plr.visible) plr.flty = -32;
3880 if (winCutScenePhase == 0) {
3881 winCutSceneTimer = 50;
3882 winCutScenePhase = 1;
3887 if (winCutScenePhase == 1) {
3888 if (--winCutSceneTimer == 0) {
3889 winCutSceneTimer = 50;
3890 winCutScenePhase = 2;
3891 plr.playSound('sndPFall');
3894 writeln("MUST BE CHAINED: ", plr.mustBeChained);
3895 if (plr.mustBeChained) {
3896 plr.removeBallAndChain(temp:true);
3897 plr.spawnBallAndChain();
3900 writeln("HOLD: ", (player.holdItem ? GetClassName(player.holdItem.Class) : ''));
3901 writeln("PICK: ", (player.pickedItem ? GetClassName(player.pickedItem.Class) : ''));
3903 if (!player.holdItem && player.pickedItem) player.scrSwitchToPocketItem(forceIfEmpty:false);
3904 if (player.holdItem) {
3905 player.holdItem.visible = true;
3906 player.holdItem.canLiveOutsideOfLevel = true;
3907 writeln("HOLD ITEM: '", GetClassName(player.holdItem.Class), "'");
3909 plr.status == MapObject::FALLING;
3910 global.plife += 99; // just in case
3915 if (winCutScenePhase == 2) {
3916 auto ball = plr.getMyBall();
3917 if (ball && plr.holdItem != ball) {
3918 ball.teleportTo(plr.fltx, plr.flty+8);
3922 if (plr.status == MapObject::STUNNED || plr.stunned) {
3926 auto treasure = MakeMapObject(144+16+8, -32, 'oBigTreasure');
3927 if (treasure) treasure.depth = 1;
3928 winCutScenePhase = 3;
3930 plr.playSound('sndTFall');
3935 if (winCutScenePhase == 3) {
3936 if (plr.status != MapObject::STUNNED && !plr.stunned) {
3937 auto bt = findBigTreasure();
3941 //plr.status = MapObject::JUMPING;
3943 plr.kJumpPressed = true;
3944 winCutScenePhase = 4;
3945 winCutSceneTimer = 50;
3952 if (winCutScenePhase == 4) {
3953 if (--winCutSceneTimer == 0) {
3954 setMenuTilesVisible(true);
3955 winCutScenePhase = 5;
3956 winSceneDrawStatus = 1;
3957 global.playMusic('musVictory', loop:false);
3958 winCutSceneTimer = 50;
3963 if (winCutScenePhase == 5) {
3964 if (winSceneDrawStatus == 3) {
3965 int money = stats.money;
3966 if (winMoneyCount < money) {
3967 if (money-winMoneyCount > 1000) {
3968 winMoneyCount += 1000;
3969 } else if (money-winMoneyCount > 100) {
3970 winMoneyCount += 100;
3971 } else if (money-winMoneyCount > 10) {
3972 winMoneyCount += 10;
3977 if (winMoneyCount >= money) {
3978 winMoneyCount = money;
3979 ++winSceneDrawStatus;
3984 if (winSceneDrawStatus == 7) {
3987 if (winFadeLevel >= 255) {
3988 ++winSceneDrawStatus;
3989 winCutSceneTimer = 30*30;
3994 if (winSceneDrawStatus == 8) {
3995 if (--winCutSceneTimer == 0) {
4001 if (--winCutSceneTimer == 0) {
4002 ++winSceneDrawStatus;
4003 winCutSceneTimer = 50;
4012 // ////////////////////////////////////////////////////////////////////////// //
4013 void renderWinCutsceneOverlay () {
4014 if (inWinCutscene == 3) {
4015 if (winSceneDrawStatus > 0) {
4016 Video.color = 0xff_ff_ff;
4017 sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
4018 //draw_set_color(txtCol);
4019 drawTextAt(64, 32, "YOU MADE IT!");
4021 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4022 if (global.config.gameMode == GameConfig::GameMode.Vanilla) {
4023 Video.color = 0xff_ff_00; //draw_set_color(c_yellow);
4024 drawTextAt(64, 48, "Classic Mode done!");
4026 Video.color = 0x00_80_80; //draw_set_color(c_teal);
4027 if (global.config.bizarrePlus) drawTextAt(64, 48, "Bizarre Mode Plus done!");
4028 else drawTextAt(64, 48, "Bizarre Mode done!");
4029 //draw_set_color(c_white);
4031 if (!global.usedShortcut) {
4032 Video.color = 0xc0_c0_c0; //draw_set_color(c_silver);
4033 drawTextAt(64, 56, "No shortcuts used!");
4034 //draw_set_color(c_yellow);
4038 if (winSceneDrawStatus > 1) {
4039 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4040 //draw_set_color(txtCol);
4041 Video.color = 0xff_ff_ff;
4042 drawTextAt(64, 64, "FINAL SCORE:");
4045 if (winSceneDrawStatus > 2) {
4046 sprStore.loadFont('sFont'); //draw_set_font(global.myFont);
4047 Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4048 drawTextAt(64, 72, va("$%d", winMoneyCount));
4051 if (winSceneDrawStatus > 4) {
4052 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4053 Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4054 drawTextAt(64, 96, va("Time: %s", time2str(winTime/30)));
4056 draw_set_color(c_white);
4057 if (s < 10) draw_text(96+24, 96, string(m) + ":0" + string(s));
4058 else draw_text(96+24, 96, string(m) + ":" + string(s));
4062 if (winSceneDrawStatus > 5) {
4063 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4064 Video.color = 0xff_ff_ff; //draw_set_color(txtCol);
4065 drawTextAt(64, 96+8, "Kills: ");
4066 Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4067 drawTextAt(96+24, 96+8, va("%s", stats.countKills()));
4070 if (winSceneDrawStatus > 6) {
4071 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4072 Video.color = 0xff_ff_ff; //draw_set_color(txtCol);
4073 drawTextAt(64, 96+16, "Saves: ");
4074 Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4075 drawTextAt(96+24, 96+16, va("%s", stats.damselsSaved));
4079 Video.color = (255-clamp(winFadeLevel, 0, 255))<<24;
4080 Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
4083 if (winSceneDrawStatus == 8) {
4084 sprStore.loadFont('sFontSmall'); //draw_set_font(global.myFontSmall);
4085 Video.color = 0xff_ff_ff; //draw_set_color(c_white);
4087 if (global.config.gameMode == GameConfig::GameMode.Vanilla) {
4088 Video.color = 0xff_ff_00; //draw_set_color(c_yellow);
4089 lastString = "YOU SHALL BE REMEMBERED AS A HERO.";
4091 Video.color = 0x00_ff_ff;
4092 if (global.config.bizarrePlus) lastString = "ANOTHER LEGENDARY ADVENTURE!";
4093 else lastString = "YOUR DISCOVERIES WILL BE CELEBRATED!";
4095 auto strLen = lastString.length*8;
4097 n = trunc(ceil(n/2.0));
4098 drawTextAt(n, 116, lastString);
4104 // ////////////////////////////////////////////////////////////////////////// //
4105 #include "roomTitle.vc"
4106 #include "roomTrans1.vc"
4107 #include "roomTrans2.vc"
4108 #include "roomTrans3.vc"
4109 #include "roomTrans4.vc"
4110 #include "roomOlmec.vc"
4111 #include "roomEnd.vc"
4114 // ////////////////////////////////////////////////////////////////////////// //
4115 #include "packages/Generator/loadRoomGens.vc"
4116 #include "packages/Generator/loadEntityGens.vc"