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 //RoomGen[LevelGen::LevelWidth, LevelGen::LevelHeight] rooms; // moved to levelgen
45 transient float accumTime;
46 transient bool gamePaused = false;
47 transient bool checkWater;
48 transient int damselSaved;
52 transient int collectCounter;
53 transient int levelMoneyStart;
55 // all movable (thinkable) map objects
56 EntityGrid objGrid; // monsters and items
58 MapTile[MaxTilesWidth, MaxTilesHeight] tiles;
59 MapBackTile backtiles;
60 EntityGrid miscTileGrid; // moveables and ropes
61 bool blockWaterChecking;
63 array!MapObject ballObjects; // list of all ball objects, for speed
65 LevelGen::RType[MaxTilesWidth, MaxTilesHeight] roomType;
73 LevelKind levelKind = LevelKind.Normal;
75 array!MapTile allEnters;
76 array!MapTile allExits;
79 int startRoomX, startRoomY;
80 int endRoomX, endRoomY;
83 transient bool playerExited;
84 transient bool disablePlayerThink = false;
85 transient int maxPlayingTime; // in seconds
89 bool ghostSpawned; // to speed up some checks
92 // FPS, i.e. incremented by 30 in one second
93 int time; // in frames
96 // screen shake variables
101 // set this before calling `fixCamera()`
102 // dimensions should be real, not scaled up/down
103 transient int viewWidth, viewHeight;
104 // room bounds, not scaled
105 IVec2D viewMin, viewMax;
107 // `fixCamera()` will set the following
108 // coordinates will be real too (with scale applied)
109 // shake is not applied
110 transient IVec2D viewStart; // with `player.viewOffset`
111 private transient IVec2D realViewStart; // without `player.viewOffset`
113 // if `frameSkip` is `true`, there are more frames waiting
114 // (i.e. you may skip rendering and such)
115 transient void delegate (bool frameSkip) onBeforeFrame;
116 transient void delegate (bool frameSkip) onAfterFrame;
118 transient void delegate () onLevelExitedCB;
120 // this will be called in-between frames, and
121 // `frameTime` is [0..1)
122 transient void delegate (float frameTime) onInterFrame;
124 final int bizRoomStyle { get { return (lg ? lg.bizRoomStyle : 0); } }
127 final bool isNormalLevel () { return (levelKind == LevelKind.Normal); }
130 // ////////////////////////////////////////////////////////////////////////// //
132 void addDeath (name aname) { stats.addDeath(aname); }
133 void addKill (name aname) { stats.addKill(aname); }
134 void addCollect (name aname, optional int amount) { stats.addCollect(aname, amount!optional); }
136 void addDamselSaved () { stats.addDamselSaved(); }
137 void addIdolStolen () { stats.addIdolStolen(); }
138 void addIdolConverted () { stats.addIdolConverted(); }
139 void addCrystalIdolStolen () { stats.addCrystalIdolStolen(); }
140 void addCrystalIdolConverted () { stats.addCrystalIdolConverted(); }
141 void addGhostSummoned () { stats.addGhostSummoned(); }
144 // ////////////////////////////////////////////////////////////////////////// //
145 final int tilesWidth () { return lg.levelRoomWidth*RoomGen::Width+2; }
146 final int tilesHeight () { return lg.levelRoomHeight*RoomGen::Height+2; }
149 // ////////////////////////////////////////////////////////////////////////// //
150 // this won't generate a level yet
151 void restartGame () {
153 auto hi = player.holdItem;
154 player.holdItem = none;
155 if (hi) hi.instanceRemove();
156 hi = player.pickedItem;
157 player.pickedItem = none;
158 if (hi) hi.instanceRemove();
161 stats.clearGameTotals();
162 if (global.startMoney > 0) stats.setMoneyCheat();
163 stats.setMoney(global.startMoney);
164 //writeln("level=", global.currLevel, "; lt=", global.levelType);
168 // complement function to `restart game`
169 void generateNormalLevel () {
171 centerViewAtPlayer();
175 // ////////////////////////////////////////////////////////////////////////// //
176 // generate angry shopkeeper at exit if murderer or thief
177 void generateAngryShopkeepers () {
178 if (global.murderer || global.thiefLevel > 0) {
179 foreach (MapTile e; allExits) {
180 auto obj = MonsterShopkeeper(MakeMapObject(e.ix, e.iy, 'oShopkeeper'));
182 obj.style = 'Bounty Hunter';
183 obj.status = MapObject::PATROL;
190 // ////////////////////////////////////////////////////////////////////////// //
191 final void resetRoomBounds () {
194 viewMax.x = tilesWidth*16;
195 viewMax.y = tilesHeight*16;
196 // Great Lake is bottomless (nope)
197 //if (global.lake) viewMax.y -= 16;
198 //writeln("ROOM BOUNDS: (", viewMax.x, "x", viewMax.y, ")");
202 final void setRoomBounds (int x0, int y0, int x1, int y1) {
210 // ////////////////////////////////////////////////////////////////////////// //
213 float timeout; // seconds
214 float starttime; // for active
215 bool active; // true: timeout is `GetTickCount()` dismissing time
218 array!OSDMessage msglist; // [0]: current one
221 private final void osdCheckTimeouts () {
222 auto stt = GetTickCount();
223 while (msglist.length) {
224 if (!msglist[0].active) {
225 msglist[0].active = true;
226 msglist[0].starttime = stt;
228 if (msglist[0].starttime+msglist[0].timeout >= stt) break;
234 final bool osdHasMessage () {
236 return (msglist.length > 0);
240 final string osdGetMessage (out float timeLeft, out float timeStart) {
242 if (msglist.length == 0) { timeLeft = 0; return ""; }
243 auto stt = GetTickCount();
244 timeStart = msglist[0].starttime;
245 timeLeft = fmax(0, msglist[0].starttime+msglist[0].timeout-stt);
246 return msglist[0].msg;
250 final void osdClear () {
251 msglist.length -= msglist.length;
255 final void osdMessage (string msg, optional float timeout, optional bool urgent) {
257 if (!specified_timeout) timeout = 3.33;
258 // special message for shops
259 if (timeout == -666) {
261 if (msglist.length && msglist[0].msg == msg) return;
262 if (msglist.length == 0 || msglist[0].msg != msg) {
265 msglist[0].msg = msg;
267 msglist[0].active = false;
268 msglist[0].timeout = 3.33;
272 if (timeout < 0.1) return;
273 timeout = fmax(1.0, timeout);
274 //writeln("OSD: ", msg);
275 // find existing one, and bring it to the top
277 for (; oldidx < msglist.length; ++oldidx) {
278 if (msglist[oldidx].msg == msg) break; // i found her!
281 if (oldidx < msglist.length) {
282 // yeah, move duplicate to the top
283 msglist[oldidx].timeout = fmax(timeout, msglist[oldidx].timeout);
284 msglist[oldidx].active = false;
285 if (urgent && oldidx != 0) {
286 timeout = msglist[oldidx].timeout;
287 msglist.remove(oldidx);
289 msglist[0].msg = msg;
290 msglist[0].timeout = timeout;
291 msglist[0].active = false;
295 msglist[0].msg = msg;
296 msglist[0].timeout = timeout;
297 msglist[0].active = false;
301 msglist[$-1].msg = msg;
302 msglist[$-1].timeout = timeout;
303 msglist[$-1].active = false;
309 // ////////////////////////////////////////////////////////////////////////// //
310 void setup (GameGlobal aGlobal, SpriteStore aSprStore, BackTileStore aBGTileStore) {
312 sprStore = aSprStore;
313 bgtileStore = aBGTileStore;
315 lg = SpawnObject(LevelGen);
319 miscTileGrid = SpawnObject(EntityGrid);
320 miscTileGrid.setup(0, 0, MaxTilesWidth*16, MaxTilesHeight*16, MapTile);
321 //miscTileGrid.ownObjects = true;
323 objGrid = SpawnObject(EntityGrid);
324 objGrid.setup(0, 0, MaxTilesWidth*16, MaxTilesHeight*16, MapObject);
328 // stores should be set
331 levBGImg = bgtileStore[levBGImgName];
332 foreach (int y; 0..MaxTilesHeight) {
333 foreach (int x; 0..MaxTilesWidth) {
334 if (tiles[x, y]) tiles[x, y].onLoaded();
337 foreach (MapEntity o; miscTileGrid.allObjects()) o.onLoaded();
338 foreach (MapEntity o; objGrid.allObjects()) o.onLoaded();
339 for (MapBackTile bt = backtiles; bt; bt = bt.next) bt.onLoaded();
340 if (player) player.onLoaded();
342 if (msglist.length) {
343 msglist[0].active = false;
344 msglist[0].timeout = 0.200;
350 // ////////////////////////////////////////////////////////////////////////// //
351 void pickedSpectacles () {
352 foreach (int y; 0..tilesHeight) {
353 foreach (int x; 0..tilesWidth) {
354 MapTile t = tiles[x, y];
355 if (t && t.isInstanceAlive) t.onGotSpectacles();
358 foreach (MapTile t; miscTileGrid.allObjects()) {
359 if (t.isInstanceAlive) t.onGotSpectacles();
364 // ////////////////////////////////////////////////////////////////////////// //
365 #include "rgentile.vc"
366 #include "rgenobj.vc"
369 void onLevelExited () {
370 if (isNormalLevel()) stats.maxLevelComplete = max(stats.maxLevelComplete, global.currLevel);
371 if (onLevelExitedCB) onLevelExitedCB();
372 if (levelKind == LevelKind.Transition) {
373 if (global.thiefLevel > 0) global.thiefLevel -= 1;
374 //orig dbg:if (global.currLevel == 1) global.currLevel += firstLevelSkip; else global.currLevel += levelSkip;
375 global.currLevel += 1;
378 generateTransitionLevel();
380 centerViewAtPlayer();
384 void generateLevelMessages () {
385 if (global.darkLevel) {
386 if (global.hasCrown) osdMessage("THE HEDJET SHINES BRIGHTLY.");
387 else if (global.config.scumDarkness < 2) osdMessage("I CAN'T SEE A THING!");
389 else global.message = "";
390 if (global.hasCrown) global.message2 = "";
391 else if (global.scumDarkness < 2) global.message2 = "I'D BETTER USE THESE FLARES!";
392 else global.message2 = "";
393 global.messageTimer = 200;
398 if (global.blackMarket) osdMessage("WELCOME TO THE BLACK MARKET!");
400 if (global.cemetary) osdMessage("THE DEAD ARE RESTLESS!");
401 if (global.lake) osdMessage("I CAN HEAR RUSHING WATER...");
403 if (global.snakePit) osdMessage("I HEAR SNAKES... I HATE SNAKES!");
404 if (global.yetiLair) osdMessage("IT SMELLS LIKE WET FUR IN HERE!");
405 if (global.alienCraft) osdMessage("THERE'S A PSYCHIC PRESENCE HERE!");
406 if (global.cityOfGold) {
407 if (true /*hasOlmec()*/) osdMessage("IT'S THE LEGENDARY CITY OF GOLD!");
410 if (global.sacrificePit) osdMessage("I CAN HEAR PRAYERS TO KALI!");
414 final MapObject debugSpawnObjectWithClass (class!MapObject oclass, optional bool playerDir) {
415 if (!oclass) return none;
417 bool canLeft = !isSolidAtPoint(player.ix-8, player.iy);
418 bool canRight = !isSolidAtPoint(player.ix+16, player.iy);
419 if (!canLeft && !canRight) return none;
420 if (canLeft && canRight) {
422 dx = (player.dir == MapObject::Dir.Left ? -16 : 16);
427 dx = (canLeft ? -16 : 16);
429 auto obj = SpawnMapObjectWithClass(oclass);
430 if (obj isa MapEnemy) { dx -= 8; dy -= 8; }
431 if (obj) PutSpawnedMapObject(player.ix+dx, player.iy+dy, obj);
436 final MapObject debugSpawnObject (name aname) {
437 if (!aname) return none;
438 return debugSpawnObjectWithClass(findGameObjectClassByName(aname));
442 // `global.currLevel` is the new level
443 void generateTransitionLevel () {
447 global.setMusicPitch(1.0);
450 levelKind = LevelKind.Transition;
452 auto olddel = ImmediateDelete;
453 ImmediateDelete = false;
457 if (global.currLevel < 4) createTrans1Room();
458 else if (global.currLevel == 4) createTrans1xRoom();
459 else if (global.currLevel < 8) createTrans2Room();
460 else if (global.currLevel == 8) createTrans2xRoom();
461 else if (global.currLevel < 12) createTrans3Room();
462 else if (global.currLevel == 12) createTrans3xRoom();
463 else if (global.currLevel < 16) createTrans4Room();
464 else if (global.currLevel == 16) createTrans4Room();
465 else createTrans1Room(); //???
468 addBackgroundGfxDetails();
469 levBGImgName = 'bgCave';
470 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
472 blockWaterChecking = true;
476 if (damselSaved > 0) {
477 MakeMapObject(176+8, 176+8, 'oDamselKiss');
478 global.plife += damselSaved; // if player skipped transition cutscene
482 ImmediateDelete = olddel;
483 CollectGarbage(true); // destroy delayed objects too
486 miscTileGrid.dumpStats();
490 playerExited = false; // just in case
495 //global.playMusic(lg.musicName);
499 void generateLevel () {
500 global.setMusicPitch(1.0);
501 stats.clearLevelTotals();
503 levelKind = LevelKind.Normal;
511 auto olddel = ImmediateDelete;
512 ImmediateDelete = false;
516 // if transition cutscene was skipped...
517 if (damselSaved > 0) global.plife += damselSaved; // if player skipped transition cutscene
521 startRoomX = lg.startRoomX;
522 startRoomY = lg.startRoomY;
523 endRoomX = lg.endRoomX;
524 endRoomY = lg.endRoomY;
525 addBackgroundGfxDetails();
526 foreach (int y; 0..tilesHeight) {
527 foreach (int x; 0..tilesWidth) {
533 levBGImgName = lg.bgImgName;
534 levBGImg = (levBGImgName ? bgtileStore[levBGImgName] : none);
536 if (global.allowAngryShopkeepers) generateAngryShopkeepers();
538 lg.generateEntities();
540 // add box of flares to dark level
541 if (global.darkLevel && allEnters.length) {
542 auto enter = allEnters[0];
543 int x = enter.ix, y = enter.iy;
544 if (!isSolidAtPoint(x-16, y)) MakeMapObject(x-16+8, y+8, 'oFlareCrate');
545 else if (!isSolidAtPoint(x+16, y)) MakeMapObject(x+16+8, y+8, 'oFlareCrate');
546 else MakeMapObject(x+8, y+8, 'oFlareCrate');
549 //scrGenerateEntities();
550 //foreach (; 0..2) scrGenerateEntities();
552 writeln(countObjects, " alive objects inserted");
553 writeln(countBackTiles, " background tiles inserted");
555 if (!player) FatalError("player pawn is not spawned");
557 blockWaterChecking = false;
561 ImmediateDelete = olddel;
562 CollectGarbage(true); // destroy delayed objects too
565 miscTileGrid.dumpStats();
569 playerExited = false; // just in case
571 levelMoneyStart = stats.money;
574 generateLevelMessages();
579 global.playMusic(lg.musicName);
585 // ////////////////////////////////////////////////////////////////////////// //
586 int currKeys, nextKeys;
587 int pressedKeysQ, releasedKeysQ;
588 int keysPressed, keysReleased = -1;
591 struct SavedKeyState {
592 int currKeys, nextKeys;
593 int pressedKeysQ, releasedKeysQ;
594 int keysPressed, keysReleased;
596 int roomSeed, otherSeed;
600 // for saving/replaying
601 final void keysSaveState (out SavedKeyState ks) {
602 ks.currKeys = currKeys;
603 ks.nextKeys = nextKeys;
604 ks.pressedKeysQ = pressedKeysQ;
605 ks.releasedKeysQ = releasedKeysQ;
606 ks.keysPressed = keysPressed;
607 ks.keysReleased = keysReleased;
610 // for saving/replaying
611 final void keysRestoreState (const ref SavedKeyState ks) {
612 currKeys = ks.currKeys;
613 nextKeys = ks.nextKeys;
614 pressedKeysQ = ks.pressedKeysQ;
615 releasedKeysQ = ks.releasedKeysQ;
616 keysPressed = ks.keysPressed;
617 keysReleased = ks.keysReleased;
621 final void keysNextFrame () {
626 final void clearKeys () {
636 final void onKey (int code, bool down) {
641 if (keysReleased&code) {
643 keysReleased &= ~code;
644 pressedKeysQ |= code;
648 if (keysPressed&code) {
649 keysReleased |= code;
650 keysPressed &= ~code;
651 releasedKeysQ |= code;
656 final bool isKeyDown (int code) {
657 return !!(currKeys&code);
660 final bool isKeyPressed (int code) {
661 bool res = !!(pressedKeysQ&code);
662 pressedKeysQ &= ~code;
666 final bool isKeyReleased (int code) {
667 bool res = !!(releasedKeysQ&code);
668 releasedKeysQ &= ~code;
673 final void clearKeysPressRelease () {
674 keysPressed = default.keysPressed;
675 keysReleased = default.keysReleased;
676 pressedKeysQ = default.pressedKeysQ;
677 releasedKeysQ = default.releasedKeysQ;
683 // ////////////////////////////////////////////////////////////////////////// //
684 final void registerEnter (MapTile t) {
691 final void registerExit (MapTile t) {
698 final bool isYAtEntranceRow (int py) {
700 foreach (MapTile t; allEnters) if (t.iy == py) return true;
705 final int calcNearestEnterDist (int px, int py) {
706 if (allEnters.length == 0) return int.max;
707 int curdistsq = int.max;
708 foreach (MapTile t; allEnters) {
709 int xc = px-t.xCenter, yc = py-t.yCenter;
710 int distsq = xc*xc+yc*yc;
711 if (distsq < curdistsq) curdistsq = distsq;
713 return round(sqrt(curdistsq));
717 final int calcNearestExitDist (int px, int py) {
718 if (allExits.length == 0) return int.max;
719 int curdistsq = int.max;
720 foreach (MapTile t; allExits) {
721 int xc = px-t.xCenter, yc = py-t.yCenter;
722 int distsq = xc*xc+yc*yc;
723 if (distsq < curdistsq) curdistsq = distsq;
725 return round(sqrt(curdistsq));
729 // ////////////////////////////////////////////////////////////////////////// //
730 final void clearForTransition () {
731 auto olddel = ImmediateDelete;
732 ImmediateDelete = false;
735 ImmediateDelete = olddel;
736 CollectGarbage(true); // destroy delayed objects too
740 final void clearTiles () {
743 allEnters.length -= allEnters.length; // don't deallocate
744 allExits.length -= allExits.length; // don't deallocate
745 foreach (ref auto tile; tiles) delete tile;
746 if (dumpGridStats) { if (miscTileGrid.getFirstObject()) miscTileGrid.dumpStats(); }
747 miscTileGrid.removeAllObjects(true); // and destroy
749 MapBackTile t = backtiles;
757 // ////////////////////////////////////////////////////////////////////////// //
758 final int countObjects () {
759 return objGrid.countObjects();
762 final int countBackTiles () {
764 for (MapBackTile bt = backtiles; bt; bt = bt.next) ++res;
768 final void clearObjects () {
769 // don't kill objects player is holding
771 if (player.pickedItem && player.pickedItem.grid) {
772 player.pickedItem.grid.remove(player.pickedItem.gridId);
773 writeln("secured pocket item '", GetClassName(player.pickedItem.Class), "'");
774 //player.pickedItem.grid = none;
776 if (player.holdItem && player.holdItem.grid) {
777 player.holdItem.grid.remove(player.holdItem.gridId);
778 writeln("secured held item '", GetClassName(player.holdItem.Class), "'");
779 //player.holdItem.grid = none;
783 int count = objGrid.countObjects();
784 if (dumpGridStats) { if (objGrid.getFirstObject()) objGrid.dumpStats(); }
785 objGrid.removeAllObjects(true); // and destroy
786 if (count > 0) writeln(count, " objects destroyed");
787 ballObjects.length = 0;
788 lastUsedObjectId = 0;
792 final void insertObject (MapObject o) {
794 if (o.grid) FatalError("cannot put object into level twice");
795 o.objId = ++lastUsedObjectId;
797 // ball from ball-and-chain
798 if (o.objType == 'oBall') {
800 foreach (MapObject bo; ballObjects) if (bo == o) { found = true; break; }
801 if (!found) ballObjects[$] = o;
808 final void spawnPlayerAt (int x, int y) {
809 // if we have no player, spawn new one
810 // otherwise this just a level transition, so simply reposition him
812 // don't add player to object list, as it has very separate processing anyway
813 player = SpawnObject(PlayerPawn);
814 player.global = global;
816 if (!player.initialize()) {
818 FatalError("something is wrong with player initialization");
824 player.saveInterpData();
826 playerExited = false;
827 if (global.config.startWithKapala) global.hasKapala = true;
828 centerViewAtPlayer();
829 // reinsert player items into grid
830 if (player.pickedItem) objGrid.insert(player.pickedItem);
831 if (player.holdItem) objGrid.insert(player.holdItem);
832 //writeln("player spawned; active=", player.active);
833 player.scrSwitchToPocketItem(forceIfEmpty:false);
837 final void teleportPlayerTo (int x, int y) {
841 player.saveInterpData();
846 final void resurrectPlayer () {
847 if (player) player.resurrect();
848 playerExited = false;
852 // ////////////////////////////////////////////////////////////////////////// //
853 final void scrShake (int duration) {
854 if (shakeLeft == 0) {
860 shakeLeft = max(shakeLeft, duration);
865 // ////////////////////////////////////////////////////////////////////////// //
868 ItemStolen, // including damsel, lol
875 // make the nearest shopkeeper angry. RAWR!
876 void scrShopkeeperAnger (SCAnger reason, optional int maxdist, optional MapEntity offender) {
877 if (!offender) offender = player;
878 auto shp = MonsterShopkeeper(findNearestEnemy(offender.ix, offender.iy, delegate bool (MapEnemy o) {
879 auto sc = MonsterShopkeeper(o);
880 if (!sc) return false;
881 if (sc.dead || sc.angered) return false;
886 if (specified_maxdist && offender.directionToEntityCenter(shp) > maxdist) return;
887 if (!shp.dead && !shp.angered) {
888 shp.status = MapObject::ATTACK;
890 if (global.murderer) msg = "YOU'LL PAY FOR YOUR CRIMES!";
891 else if (reason == SCAnger.ItemStolen) msg = "COME BACK HERE, THIEF!";
892 else if (reason == SCAnger.TileDestroyed) msg = "DIE, YOU VANDAL!";
893 else if (reason == SCAnger.BombDropped) msg = "TERRORIST!";
894 else if (reason == SCAnger.DamselWhipped) msg = "HEY, ONLY I CAN DO THAT!";
895 else if (reason == SCAnger.CrapsCheated) msg = "DIE, CHEATER!";
896 else msg = "NOW I'M REALLY STEAMED!";
897 if (msg) osdMessage(msg, -666);
898 global.thiefLevel += (global.thiefLevel > 0 ? 3 : 2);
904 final MapObject findCrapsPrize () {
905 foreach (MapObject o; objGrid.allObjects()) {
906 if (o.spectral || !o.isInstanceAlive) continue;
907 if (o.inDiceHouse) return o;
913 // ////////////////////////////////////////////////////////////////////////// //
914 // 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.
915 // note: idols moved by monkeys will have false `stolenIdol`
916 void scrTriggerIdolAltar (bool stolenIdol) {
917 ObjTikiCurse res = none;
918 int curdistsq = int.max;
919 int px = player.xCenter, py = player.yCenter;
920 foreach (MapObject o; objGrid.allObjects()) {
921 auto tcr = ObjTikiCurse(o);
922 if (!tcr || !tcr.isInstanceAlive) continue;
923 if (tcr.activated) continue;
924 int xc = px-tcr.xCenter, yc = py-tcr.yCenter;
925 int distsq = xc*xc+yc*yc;
926 if (distsq < curdistsq) {
931 if (res) res.activate(stolenIdol);
935 // ////////////////////////////////////////////////////////////////////////// //
936 void setupGhostTime () {
938 ghostSpawned = false;
940 if (!isNormalLevel()) {
942 global.setMusicPitch(1.0);
946 if (global.config.scumGhost < 0) {
949 osdMessage("A GHASTLY VOICE CALLS YOUR NAME!\nLET'S GET OUT OF HERE!", 6.66, urgent:true);
953 if (global.config.scumGhost == 0) {
959 // randomizes time until ghost appears once time limit is reached
960 // global.ghostRandom (true/false) if extra time is randomized - loaded from yasm.cfg. if false, use 30 second default
961 // ghostTimeLeft (time in seconds * 1000) for currently generated level
963 if (global.config.ghostRandom) {
964 auto tMin = global.randOther(10, 30); // min(global.plife*10, 30);
965 auto tMax = max(tMin+global.randOther(10, 30), min(global.config.scumGhost, 300)); // 5 minutes max
966 auto tTime = global.randOther(tMin, tMax);
967 if (tTime <= 0) tTime = round(tMax/2.0);
968 ghostTimeLeft = tTime;
970 ghostTimeLeft = global.config.scumGhost; // 5 minutes max
973 ghostTimeLeft += max(0, global.config.ghostExtraTime);
975 ghostTimeLeft *= 30; // seconds -> frames
976 //global.ghostShowTime
984 int vwdt = (viewMax.x-viewMin.x);
985 int vhgt = (viewMax.y-viewMin.y);
989 if (player.ix < viewMin.x+vwdt/2) {
990 // player is in the left side
991 gx = viewMin.x+vwdt/2+vwdt/4;
993 // player is in the right side
994 gx = viewMin.x+vwdt/4;
997 if (player.iy < viewMin.y+vhgt/2) {
998 // player is in the left side
999 gy = viewMin.y+vhgt/2+vhgt/4;
1001 // player is in the right side
1002 gy = viewMin.y+vhgt/4;
1005 writeln("spawning ghost at tile (", gx/16, ",", gy/16, "); player is at tile (", player.ix/16, ",", player.iy/16, ")");
1007 MakeMapObject(gx, gy, 'oGhost');
1010 if (oPlayer1.x > room_width/2) instance_create(view_xview[0]+view_wview[0]+8, view_yview[0]+floor(view_hview[0] / 2), oGhost);
1011 else instance_create(view_xview[0]-32, view_yview[0]+floor(view_hview[0]/2), oGhost);
1012 global.ghostExists = true;
1017 void thinkFrameGameGhost () {
1018 if (player.dead) return;
1019 if (!isNormalLevel()) return; // just in case
1021 if (ghostTimeLeft < 0) {
1023 if (musicFadeTimer > 0) {
1024 musicFadeTimer = -1;
1025 global.setMusicPitch(1.0);
1030 if (musicFadeTimer >= 0) {
1032 if (musicFadeTimer > 0 && musicFadeTimer <= 100*3) {
1033 float pitch = 1.0-(0.00226/3.0)*musicFadeTimer;
1034 //writeln("MFT: ", musicFadeTimer, "; pitch=", pitch);
1035 global.setMusicPitch(pitch);
1039 if (ghostTimeLeft == 0) {
1040 // she is already here!
1044 // no ghost if we have a crown
1045 if (global.hasCrown) {
1050 // if she was already spawned, don't do it again
1056 if (--ghostTimeLeft != 0) {
1058 if (global.config.ghostExtraTime > 0) {
1059 if (ghostTimeLeft == global.config.ghostExtraTime*30) {
1060 osdMessage("A CHILL RUNS UP YOUR SPINE...\nLET'S GET OUT OF HERE!", 6.66, urgent:true);
1062 if (musicFadeTimer < 0 && ghostTimeLeft <= global.config.ghostExtraTime*30) {
1070 if (player.isExitingSprite) {
1071 // no reason to spawn her, we're leaving
1080 void thinkFrameGame () {
1081 thinkFrameGameGhost();
1085 // ////////////////////////////////////////////////////////////////////////// //
1086 private transient array!MapObject activeThinkerList;
1089 private final bool isWetTile (MapTile t) {
1090 return (t && t.visible && (t.water || t.lava || t.wet));
1094 private final bool isWetOrSolidTile (MapTile t) {
1095 return (t && t.visible && (t.water || t.lava || t.wet || t.solid) && t.isInstanceAlive);
1099 final bool isWetOrSolidTileAtPoint (int px, int py) {
1100 return !!checkTileAtPoint(px, py, &isWetOrSolidTile);
1104 final bool isWetOrSolidTileAtTile (int tx, int ty) {
1105 return !!checkTileAtPoint(tx*16, ty*16, &isWetOrSolidTile);
1109 final bool isWetTileAtTile (int tx, int ty) {
1110 return !!checkTileAtPoint(tx*16, ty*16, &isWetTile);
1114 const int GreatLakeStartTileY = 28;
1116 // called once after level generation
1117 final void fixLiquidTop () {
1118 foreach (int tileY; 0..tilesHeight) {
1119 foreach (int tileX; 0..tilesWidth) {
1120 auto t = tiles[tileX, tileY];
1122 if (t && !t.isInstanceAlive) {
1123 delete tiles[tileX, tileY];
1128 //if (!t.water && !t.lava) continue;
1131 if (global.lake && tileY >= GreatLakeStartTileY) {
1132 //if (tileX >= NormalTilesWidth) continue;
1133 // fill level with water for lake
1134 MakeMapTile(tileX, tileY, 'oWaterSwim');
1135 t = tiles[tileX, tileY];
1141 if (!t.water && !t.lava) {
1142 // mark as wet for lake
1143 if (global.lake && tileY >= GreatLakeStartTileY) {
1146 delete tiles[tileX, tileY];
1147 MakeMapTile(tileX, tileY, 'oWaterSwim');
1148 t = tiles[tileX, tileY];
1158 if (!isWetTileAtTile(tileX, tileY-1)) {
1159 t.setSprite(t.lava ? 'sLavaTop' : 'sWaterTop');
1163 /* k8: don't distribute, the code above will take care of this
1164 if (t.spriteName != 'sWaterTop' && t.spriteName != 'sLavaTop') {
1165 MapTile obj = getTileAt(tileX-1, tileY);
1166 if (obj && (obj.water || obj.lava)) {
1167 if (obj.spriteName == 'sWaterTop' || obj.spriteName == 'sLavaTop') {
1168 //writeln("FIXLTOP (01)!");
1169 t.setSprite(t.lava ? 'sLavaTop' : 'sWaterTop');
1174 if (t.spriteName != 'sWaterTop' && t.spriteName != 'sLavaTop') {
1175 MapTile obj = getTileAt(tileX+1, tileY);
1176 if (obj && (obj.water || obj.lava)) {
1177 if (obj.spriteName == 'sWaterTop' || obj.spriteName == 'sLavaTop') {
1178 //writeln("FIXLTOP (02)!");
1179 t.setSprite(t.lava ? 'sLavaTop' : 'sWaterTop');
1189 private final void checkWaterFlow (MapTile wtile) {
1190 //if (!wtile || (!wtile.water && !wtile.lava)) return;
1191 //instance_activate_region(x-16, y-16, 48, 48, true);
1193 //int x = wtile.ix, y = wtile.iy;
1194 int tileX = wtile.ix/16, tileY = wtile.iy/16;
1196 if (global.lake && tileY >= GreatLakeStartTileY) return;
1199 if ((not collision_point(x-16, y, oSolid, 0, 0) and not collision_point(x-16, y, oWater, 0, 0)) or
1200 (not collision_point(x+16, y, oSolid, 0, 0) and not collision_point(x+16, y, oWater, 0, 0)) or
1201 (not collision_point(x, y+16, oSolid, 0, 0) and not collision_point(x, y+16, oWater, 0, 0)))
1203 if (!isWetOrSolidTileAtTile(tileX-1, tileY) ||
1204 !isWetOrSolidTileAtTile(tileX+1, tileY) ||
1205 !isWetOrSolidTileAtTile(tileX, tileY+1))
1208 wtile.instanceRemove();
1211 tiles[tileX, tileY] = none;
1215 //if (!isSolidAtPoint(x, y-16) && !isLiquidAtPoint(x, y-16)) {
1216 if (!isWetTileAtTile(tileX, tileY-1)) {
1217 wtile.setSprite(wtile.lava ? 'sLavaTop' : 'sWaterTop');
1220 /* k8: don't distribute, the code above will take care of this
1221 if (wtile.spriteName != 'sWaterTop' && wtile.spriteName != 'sLavaTop') {
1222 MapTile obj = getTileAt(tileX-1, tileY);
1223 if (obj && (obj.water || obj.lava)) {
1224 if (obj.spriteName == 'sWaterTop' || obj.spriteName == 'sLavaTop') {
1225 wtile.setSprite(wtile.lava ? 'sLavaTop' : 'sWaterTop');
1230 if (wtile.spriteName != 'sWaterTop' && wtile.spriteName != 'sLavaTop') {
1231 MapTile obj = getTileAt(tileX+1, tileY);
1232 if (obj && (obj.water || obj.lava)) {
1233 if (obj.spriteName == 'sWaterTop' || obj.spriteName == 'sLavaTop') {
1234 wtile.setSprite(wtile.lava ? 'sLavaTop' : 'sWaterTop');
1242 transient private array!MapTile waterTilesToCheck;
1244 final void cleanDeadTiles () {
1245 bool hasWater = false;
1246 waterTilesToCheck.length -= waterTilesToCheck.length;
1247 foreach (int y; 0..tilesHeight) {
1248 foreach (int x; 0..tilesWidth) {
1249 auto t = tiles[x, y];
1251 if (t.isInstanceAlive) {
1252 if (t.water || t.lava) waterTilesToCheck[$] = t;
1261 if (waterTilesToCheck.length && checkWater && !blockWaterChecking) {
1262 //writeln("checking water");
1263 checkWater = false; // `checkWaterFlow()` can set it again
1264 foreach (MapTile t; waterTilesToCheck) {
1265 if (t && t.isInstanceAlive && (t.water || t.lava)) checkWaterFlow(t);
1267 // fill empty spaces in lake with water
1269 foreach (int y; GreatLakeStartTileY..tilesHeight) {
1270 foreach (int x; 0..tilesWidth) {
1271 auto t = tiles[x, y];
1273 if (t && !t.isInstanceAlive) {
1279 if (!t.water || !t.lava) { t.wet = true; continue; }
1281 MakeMapTile(x, y, 'oWaterSwim');
1285 t.setSprite(y == GreatLakeStartTileY ? 'sWaterTop' : 'sWater');
1286 } else if (t.lava) {
1287 t.setSprite(y == GreatLakeStartTileY ? 'sLavaTop' : 'sLava');
1296 // return `true` if thinker should be removed
1297 final bool thinkOne (MapObject o) {
1298 if (!o) return true;
1299 if (o.active && o.isInstanceAlive) {
1300 bool doThink = true;
1302 // collision with player weapon
1303 auto hh = PlayerWeapon(player.holdItem);
1304 bool doWeaponAction;
1306 if (hh.blockedBySolids) {
1307 int xx = player.ix+(player.dir == MapObject::Dir.Left ? -16 : 16);
1308 doWeaponAction = !isSolidAtPoint(xx, player.iy);
1310 doWeaponAction = true;
1313 doWeaponAction = false;
1316 if (doWeaponAction && o.whipTimer <= 0 && hh && hh.collidesWithObject(o)) {
1317 //writeln("WEAPONED!");
1318 if (!o.onTouchedByPlayerWeapon(player, hh)) {
1319 if (o.isInstanceAlive) hh.onCollisionWithObject(o);
1321 o.whipTimer = o.whipTimerValue; //HACK
1322 doThink = o.isInstanceAlive;
1325 // collision with player
1326 if (doThink && o.collidesWith(player)) {
1327 if (!player.onObjectTouched(o) && o.isInstanceAlive) {
1328 doThink = !o.onTouchedByPlayer(player);
1329 if (o.grid && o.gridId) o.grid.update(o.gridId, markAsDead:true);
1333 if (doThink && o.isInstanceAlive) {
1336 if (o.isInstanceAlive) {
1337 if (o.grid && o.gridId) o.grid.update(o.gridId, markAsDead:true);
1339 if (o.isInstanceAlive) {
1341 if (o.grid && o.gridId) o.grid.update(o.gridId, markAsDead:true);
1346 if (o.isInstanceAlive) {
1347 if (!o.canLiveOutsideOfLevel && o.isOutsideOfLevel()) {
1361 final void processThinkers (float timeDelta) {
1362 if (timeDelta <= 0) return;
1364 if (onBeforeFrame) onBeforeFrame(false);
1365 if (onAfterFrame) onAfterFrame(false);
1369 accumTime += timeDelta;
1370 bool wasFrame = false;
1372 auto olddel = ImmediateDelete;
1373 ImmediateDelete = false;
1374 while (accumTime >= FrameTime) {
1375 accumTime -= FrameTime;
1376 if (onBeforeFrame) onBeforeFrame(accumTime >= FrameTime);
1378 if (shakeLeft > 0) {
1380 if (!shakeDir.x) shakeDir.x = (global.randOther(0, 1) ? -3 : 3);
1381 if (!shakeDir.y) shakeDir.y = (global.randOther(0, 1) ? -3 : 3);
1382 shakeOfs.x = shakeDir.x;
1383 shakeOfs.y = shakeDir.y;
1384 int sgnc = global.randOther(1, 3);
1385 if (sgnc&0x01) shakeDir.x = -shakeDir.x;
1386 if (sgnc&0x02) shakeDir.y = -shakeDir.y;
1393 // game-global events
1395 // frame thinkers: player
1396 if (player && !disablePlayerThink) {
1398 if (!player.dead && isNormalLevel() &&
1399 (maxPlayingTime < 0 ||
1400 (maxPlayingTime > 0 && maxPlayingTime*30 <= time &&
1401 time%30 == 0 && global.randOther(1, 100) <= 20)))
1403 MakeMapObject(player.ix, player.iy, 'oExplosion');
1404 player.scrCreateFlame(player.ix, player.iy, 3);
1406 //HACK: check for stolen items
1407 auto item = MapItem(player.holdItem);
1408 if (item) item.onCheckItemStolen(player);
1409 item = MapItem(player.pickedItem);
1410 if (item) item.onCheckItemStolen(player);
1412 player.saveInterpData();
1413 player.processAlarms();
1414 if (player.isInstanceAlive) {
1415 player.thinkFrame();
1416 if (player.isInstanceAlive) player.nextAnimFrame();
1419 // frame thinkers: moveable solids
1421 // frame thinkers: objects
1422 auto grid = objGrid;
1423 // collect active objects
1424 if (global.config.useFrozenRegion) {
1425 activeThinkerList.length -= activeThinkerList.length;
1426 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)) {
1427 activeThinkerList[$] = o;
1429 //writeln("thinkers: ", activeThinkerList.length);
1430 foreach (MapObject o; activeThinkerList) {
1432 grid.remove(o.gridId);
1439 bool killThisOne = false;
1440 for (int cid = grid.getFirstObject(); cid; cid = grid.getNextObject(cid, killThisOne)) {
1441 killThisOne = false;
1442 MapObject o = grid.getObject(MapObject, cid);
1443 if (!o) { killThisOne = true; continue; }
1444 // remove this object if it is dead
1454 // done with thinkers
1457 if (collectCounter == 0) {
1458 xmoney = max(0, xmoney-100);
1463 if (player && !player.dead) stats.oneMoreFramePlayed();
1464 if (onAfterFrame) onAfterFrame(accumTime >= FrameTime);
1467 if (playerExited) break;
1469 ImmediateDelete = olddel;
1471 playerExited = false;
1475 // if we were processed at least one frame, collect garbage
1477 CollectGarbage(true); // destroy delayed objects too
1479 if (player.holdItem) player.holdItem.fixHoldCoords();
1480 if (accumTime > 0 && onInterFrame) onInterFrame(accumTime/FrameTime);
1484 // ////////////////////////////////////////////////////////////////////////// //
1485 final void tileXY2roomXY (int tileX, int tileY, out int roomX, out int roomY) {
1486 roomX = (tileX-1)/RoomGen::Width;
1487 roomY = (tileY-1)/RoomGen::Height;
1491 final bool isInShop (int tileX, int tileY) {
1492 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
1493 auto n = roomType[tileX, tileY];
1494 if (n == 4 || n == 5) return true;
1495 auto t = getTileAt(tileX, tileY);
1496 if (t && t.shopWall) return true;
1497 //k8: we don't have this
1498 //if (t && t.objType == 'oShop') return true;
1504 // ////////////////////////////////////////////////////////////////////////// //
1505 override void Destroy () {
1512 // ////////////////////////////////////////////////////////////////////////// //
1513 final MapObject findNearestBall (int px, int py) {
1514 MapObject res = none;
1515 int curdistsq = int.max;
1516 foreach (MapObject o; ballObjects) {
1517 if (!o || o.spectral || !o.isInstanceAlive) continue;
1518 int xc = px-o.xCenter, yc = py-o.yCenter;
1519 int distsq = xc*xc+yc*yc;
1520 if (distsq < curdistsq) {
1529 final int calcNearestBallDist (int px, int py) {
1530 auto e = findNearestBall(px, py);
1531 if (!e) return int.max;
1532 int xc = px-e.xCenter, yc = py-e.yCenter;
1533 return round(sqrt(xc*xc+yc*yc));
1537 final MapObject findNearestObject (int px, int py, bool delegate (MapObject o) dg) {
1538 MapObject res = none;
1539 int curdistsq = int.max;
1540 foreach (MapObject o; objGrid.allObjects()) {
1541 if (o.spectral || !o.isInstanceAlive) continue;
1542 if (!dg(o)) continue;
1543 int xc = px-o.xCenter, yc = py-o.yCenter;
1544 int distsq = xc*xc+yc*yc;
1545 if (distsq < curdistsq) {
1554 final MapObject findNearestEnemy (int px, int py, optional bool delegate (MapEnemy o) dg) {
1555 MapObject res = none;
1556 int curdistsq = int.max;
1557 foreach (MapObject o; objGrid.allObjects()) {
1558 //k8: i added `dead` check
1559 if (o.spectral || o !isa MapEnemy || o.dead || !o.isInstanceAlive) continue;
1561 if (!dg(MapEnemy(o))) continue;
1563 int xc = px-o.xCenter, yc = py-o.yCenter;
1564 int distsq = xc*xc+yc*yc;
1565 if (distsq < curdistsq) {
1574 final MonsterShopkeeper hasAliveShopkeepers (optional bool skipAngry) {
1575 foreach (MapObject o; objGrid.allObjects()) {
1576 auto sc = MonsterShopkeeper(o);
1577 if (!sc || o.spectral || !o.isInstanceAlive) continue;
1578 if (sc.dead) continue;
1579 if (skipAngry && sc.angered) continue;
1586 final int calcNearestEnemyDist (int px, int py, optional bool delegate (MapEnemy o) dg) {
1587 auto e = findNearestEnemy(px, py, dg!optional);
1588 if (!e) return int.max;
1589 int xc = px-e.xCenter, yc = py-e.yCenter;
1590 return round(sqrt(xc*xc+yc*yc));
1594 final int calcNearestObjectDist (int px, int py, optional bool delegate (MapObject o) dg) {
1595 auto e = findNearestObject(px, py, dg!optional);
1596 if (!e) return int.max;
1597 int xc = px-e.xCenter, yc = py-e.yCenter;
1598 return round(sqrt(xc*xc+yc*yc));
1602 final MapTile findNearestMoveableSolid (int px, int py, optional bool delegate (MapTile t) dg) {
1604 int curdistsq = int.max;
1605 foreach (MapTile t; miscTileGrid.allObjects()) {
1606 if (t.spectral || !t.isInstanceAlive) continue;
1608 if (!dg(t)) continue;
1610 if (!t.solid || !t.moveable) continue;
1612 int xc = px-t.xCenter, yc = py-t.yCenter;
1613 int distsq = xc*xc+yc*yc;
1614 if (distsq < curdistsq) {
1623 final MapTile findNearestTile (int px, int py, optional bool delegate (MapTile t) dg) {
1624 if (!dg) return none;
1626 int curdistsq = int.max;
1628 //FIXME: make this faster!
1629 foreach (MapTile t; tiles) {
1630 if (!t || t.spectral || !t.isInstanceAlive) continue;
1631 int xc = px-t.xCenter, yc = py-t.yCenter;
1632 int distsq = xc*xc+yc*yc;
1633 if (distsq < curdistsq && dg(t)) {
1639 foreach (MapTile t; miscTileGrid.allObjects()) {
1640 if (!t || t.spectral || !t.isInstanceAlive) continue;
1641 int xc = px-t.xCenter, yc = py-t.yCenter;
1642 int distsq = xc*xc+yc*yc;
1643 if (distsq < curdistsq && dg(t)) {
1653 // ////////////////////////////////////////////////////////////////////////// //
1654 final bool cbIsObjectWeb (MapObject o) { return (o isa ItemWeb); }
1655 final bool cbIsObjectBlob (MapObject o) { return (o.objName == 'oBlob'); }
1656 final bool cbIsObjectArrow (MapObject o) { return (o isa ItemProjectileArrow); }
1657 final bool cbIsObjectEnemy (MapObject o) { return (/*o != self &&*/ o isa MapEnemy); }
1659 final bool cbIsObjectTreasure (MapObject o) { return (o.isTreasure); }
1661 final bool cbIsItemObject (MapObject o) { return (o isa MapItem); }
1663 final bool cbIsItemOrEnemyObject (MapObject o) { return (o isa MapItem || o isa MapEnemy); }
1666 final MapObject isObjectAtTile (int tileX, int tileY, optional bool delegate (MapObject o) dg) {
1669 foreach (MapObject o; objGrid.inRectPix(tileX, tileY, 16, 16, objGrid.nextTag(), precise: true)) {
1670 if (o.spectral || !o.isInstanceAlive) continue;
1672 if (dg(o)) return o;
1681 final MapObject isObjectAtTilePix (int x, int y, optional bool delegate (MapObject o) dg) {
1682 return isObjectAtTile(x/16, y/16, dg!optional);
1686 final MapObject isObjectAtPoint (int xpos, int ypos, optional bool delegate (MapObject o) dg, optional bool precise) {
1687 if (!specified_precise) precise = true;
1688 foreach (MapObject o; objGrid.inCellPix(xpos, ypos, objGrid.nextTag(), precise: precise)) {
1689 if (o.spectral || !o.isInstanceAlive) continue;
1691 if (dg(o)) return o;
1693 if (o isa MapEnemy) return o;
1700 final MapObject isObjectInRect (int xpos, int ypos, int w, int h, optional bool delegate (MapObject o) dg, optional bool precise) {
1701 if (w < 1 || h < 1) return none;
1702 if (w == 1 && h == 1) return isObjectAtPoint(xpos, ypos, dg!optional, precise!optional);
1703 if (!specified_precise) precise = true;
1704 foreach (MapObject o; objGrid.inRectPix(xpos, ypos, w, h, objGrid.nextTag(), precise: precise)) {
1705 if (o.spectral || !o.isInstanceAlive) continue;
1707 if (dg(o)) return o;
1709 if (o isa MapEnemy) return o;
1716 final MapObject forEachObject (bool delegate (MapObject o) dg) {
1717 if (!dg) return none;
1718 foreach (MapObject o; objGrid.allObjects()) {
1719 if (o.spectral || !o.isInstanceAlive) continue;
1720 if (dg(o)) return o;
1726 final MapObject forEachObjectAtPoint (int xpos, int ypos, bool delegate (MapObject o) dg, optional bool precise) {
1727 if (!dg) return none;
1728 if (!specified_precise) precise = true;
1729 foreach (MapObject o; objGrid.inCellPix(xpos, ypos, objGrid.nextTag(), precise: precise)) {
1730 if (o.spectral || !o.isInstanceAlive) continue;
1731 if (dg(o)) return o;
1737 final MapObject forEachObjectInRect (int xpos, int ypos, int w, int h, bool delegate (MapObject o) dg, optional bool precise) {
1738 if (!dg || w < 1 || h < 1) return none;
1739 if (w == 1 && h == 1) return forEachObjectAtPoint(xpos, ypos, dg!optional, precise!optional);
1740 if (!specified_precise) precise = true;
1741 foreach (MapObject o; objGrid.inRectPix(xpos, ypos, w, h, objGrid.nextTag(), precise: precise)) {
1742 if (o.spectral || !o.isInstanceAlive) continue;
1743 if (dg(o)) return o;
1749 private final bool cbIsRopeTile (MapTile t) { return t.rope; }
1751 final MapTile isRopeAtPoint (int px, int py) {
1752 return checkTileAtPoint(px, py, &cbIsRopeTile);
1757 final MapTile isWaterSwimAtPoint (int px, int py) {
1758 return isWaterAtPoint(px, py);
1762 // ////////////////////////////////////////////////////////////////////////// //
1763 private array!MapObject tmpObjectList;
1765 private final bool cbCollectObjectsWithMask (MapObject t) {
1766 if (!t || !t.visible || t.spectral /*|| t.invincible*/ || !t.isInstanceAlive) return false;
1767 //auto spf = getSpriteFrame();
1768 //if (!t.sprite || t.sprite.frames.length < 1) return false;
1769 tmpObjectList[$] = t;
1774 final void touchObjectsWithMask (int x, int y, SpriteFrame frm, bool delegate (MapObject t) dg) {
1775 if (!frm || frm.bw < 1 || frm.bh < 1 || !dg) return;
1776 if (frm.isEmptyPixelMask) return;
1778 if (tmpObjectList.length) tmpObjectList.length -= tmpObjectList.length; // don't realloc
1779 if (player.isRectCollisionFrame(frm, x, y)) {
1780 //writeln("player hit");
1781 tmpObjectList[$] = player;
1784 writeln("no player hit: plr=(", player.ix, ",", player.iy, ")-(", player.ix+player.width-1, ",", player.iy+player.height-1, "); ",
1785 "frm=(", x+frm.bx, ",", y+frm.by, ")-(", x+frm.bx+frm.bw-1, ",", y+frm.by+frm.bh-1, ")");
1788 forEachObjectInRect(x+frm.bx, y+frm.by, frm.bw, frm.bh, &cbCollectObjectsWithMask);
1789 foreach (MapObject t; tmpObjectList) {
1790 if (!t || !t.isInstanceAlive || !t.visible || t.spectral) continue;
1792 auto tf = t.getSpriteFrame();
1794 //writeln("no sprite frame for ", GetClassName(t.Class));
1799 if (frm.pixelCheck(tf, t.ix-tf.xofs-x, t.iy-tf.yofs-y)) {
1800 //writeln("pixel hit for ", GetClassName(t.Class));
1804 if (t.isRectCollisionFrame(frm, x, y)) {
1811 // ////////////////////////////////////////////////////////////////////////// //
1812 final void destroyTileAt (int x, int y) {
1813 if (x < 0 || y < 0 || x >= tilesWidth*16 || y >= tilesHeight*16) return;
1816 MapTile t = tiles[x, y];
1817 if (!t || !t.visible || t.spectral || t.invincible || !t.isInstanceAlive) return;
1825 private array!MapTile tmpTileList;
1827 private final bool cbCollectTilesWithMask (MapTile t) {
1828 if (!t || !t.visible || t.spectral /*|| t.invincible*/ || !t.isInstanceAlive) return false;
1829 if (!t.sprite || t.sprite.frames.length < 1) return false;
1834 final void touchTilesWithMask (int x, int y, SpriteFrame frm, bool delegate (MapTile t) dg) {
1835 if (!frm || frm.bw < 1 || frm.bh < 1 || !dg) return;
1836 if (frm.isEmptyPixelMask) return;
1838 if (tmpTileList.length) tmpTileList.length -= tmpTileList.length; // don't realloc
1839 checkTilesInRect(x+frm.bx, y+frm.by, frm.bw, frm.bh, &cbCollectTilesWithMask);
1840 foreach (MapTile t; tmpTileList) {
1841 if (!t || !t.isInstanceAlive || !t.visible || t.spectral) continue;
1843 auto tf = t.sprite.frames[0];
1844 if (frm.pixelCheck(tf, t.ix-x, t.iy-y)) {
1846 //doCleanup = doCleanup || !t.isInstanceAlive;
1847 //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, ")");
1850 if (t.isRectCollisionFrame(frm, x, y)) {
1857 // ////////////////////////////////////////////////////////////////////////// //
1858 final bool cbCollisionAnySolid (MapTile t) { return t.solid; }
1859 final bool cbCollisionAnyMoveableSolid (MapTile t) { return t.solid && t.moveable; }
1860 final bool cbCollisionLiquid (MapTile t) { return (t.water || t.lava); }
1861 final bool cbCollisionAnySolidOrLiquid (MapTile t) { return (t.solid || t.water || t.lava); }
1862 final bool cbCollisionLadder (MapTile t) { return t.ladder; }
1863 final bool cbCollisionLadderTop (MapTile t) { return t.laddertop; }
1864 final bool cbCollisionAnyLadder (MapTile t) { return (t.ladder || t.laddertop); }
1865 final bool cbCollisionAnySolidIce (MapTile t) { return (t.solid && t.ice); }
1866 final bool cbCollisionWater (MapTile t) { return t.water; }
1867 final bool cbCollisionLava (MapTile t) { return t.lava; }
1868 final bool cbCollisionAnyTree (MapTile t) { return t.tree; }
1869 final bool cbCollisionPlatform (MapTile t) { return t.platform; }
1870 final bool cbCollisionLadderOrPlatform (MapTile t) { return (t.ladder || t.platform); }
1871 //final bool cbCollisionAnyHangeable (MapTile t) { return t.hangeable; } // no need to check for solids here, it is done in MapTile initializer
1872 final bool cbCollisionSpringTrap (MapTile t) { return t.springtrap; }
1873 final bool cbCollisionSpikes (MapTile t) { return t.spikes; }
1874 final bool cbCollisionExitTile (MapTile t) { return t.exit; }
1876 final bool cbCollisionForWhoa (MapTile t) { return (t.solid || t.laddertop); }
1878 //final bool cbCollisionSacAltar (MapTile t) { return (t.objName == 'oSacAltarLeft'); } // only left part, yeah
1879 final bool cbCollisionSacAltar (MapTile t) { return t.sacrificingAltar; }
1882 // ////////////////////////////////////////////////////////////////////////// //
1883 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*/) {
1884 //!if (dbgdump) writeln("checkTilesInRect: (", x0, ",", y0, ")-(", x0+w-1, ",", y0+h-1, ") ; w=", w, "; h=", h);
1885 if (w < 1 || h < 1) return none;
1886 if (w == 1 && h == 1) return checkTileAtPoint(x0, y0, dg!optional);
1887 int x1 = x0+w-1, y1 = y0+h-1;
1888 if (x0 >= tilesWidth*16 || y0 >= tilesHeight*16 || x1 < 0 || y1 < 0) return none;
1890 //!if (dbgdump) writeln("default checker set");
1891 dg = &cbCollisionAnySolid;
1893 //!if (dbgdump) writeln("delegate: ", dg);
1894 int origx0 = x0, origy0 = y0;
1895 int tileSX = max(0, x0)/16;
1896 int tileSY = max(0, y0)/16;
1897 int tileEX = min(tilesWidth*16-1, x1)/16;
1898 int tileEY = min(tilesHeight*16-1, y1)/16;
1899 //!if (dbgdump) writeln(" tiles: (", tileSX, ",", tileSY, ")-(", tileEX, ",", tileEY, ")");
1900 auto grid = miscTileGrid;
1901 int tag = grid.nextTag();
1902 for (int ty = tileSY; ty <= tileEY; ++ty) {
1903 for (int tx = tileSX; tx <= tileEX; ++tx) {
1904 MapTile t = tiles[tx, ty];
1905 //!if (dbgdump && t && t.visible && t.isInstanceAlive && t.isRectCollision(origx0, origy0, w, h) ) writeln(" tile: ", GetClassName(t.Class), " : ", t.objName, " : ", t.objType, " : ", dg(t));
1906 if (t && !t.spectral && t.visible && t.isInstanceAlive && t.isRectCollision(origx0, origy0, w, h) && dg(t)) return t;
1907 // moveable tiles are in separate grid
1908 foreach (t; grid.inCellPix(tx*16, ty*16, tag, precise:precise)) {
1909 //if (t.isRectCollision(origx0, origy0, w, h) && dg(t)) return t;
1910 if (t.isInstanceAlive && !t.spectral && t.visible && t.isRectCollision(origx0, origy0, w, h) && dg(t)) return t;
1918 final MapTile checkTileAtPoint (int x0, int y0, optional bool delegate (MapTile dg) dg) {
1919 if (x0 < 0 || y0 < 0 || x0 >= tilesWidth*16 || y0 >= tilesHeight*16) return none;
1920 if (!dg) dg = &cbCollisionAnySolid;
1921 //if (!self) { writeln("WTF?!"); return none; }
1922 MapTile t = tiles[x0/16, y0/16];
1923 if (t && !t.spectral && t.visible && t.isInstanceAlive && t.isPointCollision(x0, y0) && dg(t)) return t;
1924 // moveable tiles are in separate grid
1925 auto grid = miscTileGrid;
1926 foreach (t; grid.inCellPix(x0, y0, grid.nextTag(), precise:true)) {
1927 if (t.isInstanceAlive && !t.spectral && t.visible && dg(t)) return t;
1933 //FIXME: optimize this with clipping first
1934 //TODO: moveable tiles
1936 final MapTile checkTilesAtLine (int ax0, int ay0, int ax1, int ay1, optional bool delegate (MapTile dg) dg) {
1937 // do it faster if we can
1939 // strict vertical check?
1940 if (ax0 == ax1 && ay0 <= ay1) return checkTilesInRect(ax0, ay0, 1, ay1-ay0+1, dg!optional);
1941 // strict horizontal check?
1942 if (ay0 == ay1 && ax0 <= ax1) return checkTilesInRect(ax0, ay0, ax1-ax0+1, 1, dg!optional);
1944 float x0 = float(ax0)/16.0, y0 = float(ay0)/16.0, x1 = float(ax1)/16.0, y1 = float(ay1)/16.0;
1947 if (!dg) dg = &cbCollisionAnySolid;
1949 // get starting and enging tile
1950 int tileSX = trunc(x0), tileSY = trunc(y0);
1951 int tileEX = trunc(x1), tileEY = trunc(y1);
1953 // first hit is always landed
1954 if (tileSX >= 0 && tileSY >= 0 && tileSX < TilesWidth && tileSY < TilesHeight) {
1955 MapTile t = tiles[tileSX, tileSY];
1956 if (t && t.visible && Geom.lineAABBIntersects(ax0, ay0, ax1, ay1, tileSX*16, tileSY*16, 16, 16) && dg(t)) return t;
1959 // if starting and ending tile is the same, we don't need to do anything more
1960 if (tileSX == tileEX && tileSY == tileEY) return none;
1962 // calculate ray direction
1963 TVec dv = (vector(x1, y1)-vector(x0, y0)).normalise2d;
1965 // length of ray from one x or y-side to next x or y-side
1966 float deltaDistX = (fabs(dv.x) > 0.0001 ? fabs(1.0/dv.x) : 0.0);
1967 float deltaDistY = (fabs(dv.y) > 0.0001 ? fabs(1.0/dv.y) : 0.0);
1969 // calculate step and initial sideDists
1971 float sideDistX; // length of ray from current position to next x-side
1972 int stepX; // what direction to step in x (either +1 or -1)
1975 sideDistX = (x0-tileSX)*deltaDistX;
1978 sideDistX = (tileSX+1.0-x0)*deltaDistX;
1981 float sideDistY; // length of ray from current position to next y-side
1982 int stepY; // what direction to step in y (either +1 or -1)
1985 sideDistY = (y0-tileSY)*deltaDistY;
1988 sideDistY = (tileSY+1.0-y0)*deltaDistY;
1992 //int side; // was a NS or a EW wall hit?
1994 // jump to next map square, either in x-direction, or in y-direction
1995 if (sideDistX < sideDistY) {
1996 sideDistX += deltaDistX;
2000 sideDistY += deltaDistY;
2005 if (tileSX >= 0 && tileSY >= 0 && tileSX < TilesWidth && tileSY < TilesHeight) {
2006 MapTile t = tiles[tileSX, tileSY];
2007 if (t && t.visible && Geom.lineAABBIntersects(ax0, ay0, ax1, ay1, tileSX*16, tileSY*16, 16, 16) && dg(t)) return t;
2009 // did we arrived at the destination?
2010 if (tileSX == tileEX && tileSY == tileEY) break;
2018 // ////////////////////////////////////////////////////////////////////////// //
2019 final MapTile isSolidAtPoint (int px, int py) { return checkTileAtPoint(px, py); }
2020 final MapTile isSolidInRect (int rx, int ry, int rw, int rh) { return checkTilesInRect(rx, ry, rw, rh); }
2021 final MapTile isLadderAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadder); }
2022 final MapTile isLadderOrPlatformAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadderOrPlatform); }
2023 final MapTile isLadderTopAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLadderTop); }
2024 final MapTile isAnyLadderAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyLadder); }
2025 final MapTile isWaterAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionWater); }
2026 final MapTile isLavaAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLava); }
2027 final MapTile isLiquidAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionLiquid); }
2028 final MapTile isIceAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnySolidIce); }
2029 final MapTile isTreeAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyTree); }
2030 //final MapTile isHangeableAtPoint (int px, int py) { return checkTileAtPoint(px, py, &cbCollisionAnyHangeable); }
2033 // ////////////////////////////////////////////////////////////////////////// //
2034 // PlayerPawn has it's own movement code, so don't process it here
2035 // but process moveable solids here, yeah
2036 final void physStep () {
2039 // we don't want the time to grow too large
2040 if (time > 100000000) time = 0;
2042 auto grid = miscTileGrid;
2044 // process gravity for moveable solids and burning for ropes
2045 int cid = grid.getFirstObject();
2047 MapTile t = grid.getObject(MapTile, cid);
2049 if (t.isInstanceAlive) {
2052 if (t.isInstanceAlive) {
2053 grid.update(cid, markAsDead:false);
2055 if (t.isInstanceAlive && !t.canLiveOutsideOfLevel && t.isOutsideOfLevel()) t.instanceRemove();
2056 grid.update(cid, markAsDead:false);
2059 if (t.isInstanceAlive) {
2060 cid = grid.getNextObject(cid, removeThis:false);
2062 cid = grid.getNextObject(cid, removeThis:true);
2063 t.instanceRemove(); // just in case
2072 // ////////////////////////////////////////////////////////////////////////// //
2073 final void setTileTypeAt (int tileX, int tileY, LevelGen::RType rt) {
2074 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) roomType[tileX, tileY] = rt;
2077 final MapTile getTileAt (int tileX, int tileY) {
2078 return (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight ? tiles[tileX, tileY] : none);
2081 final bool isNamedTileAt (int tileX, int tileY, name atypename) {
2082 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
2083 auto t = tiles[tileX, tileY];
2084 if (t && t.objName == atypename) return true;
2089 final void setTileAt (int tileX, int tileY, MapTile tile) {
2090 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
2092 if (tiles[tileX, tileY]) checkWater = true;
2093 delete tiles[tileX, tileY];
2094 tiles[tileX, tileY] = tile;
2099 // ////////////////////////////////////////////////////////////////////////// //
2100 // return `true` from delegate to stop
2101 MapTile forEachSolidTile (bool delegate (int x, int y, MapTile t) dg) {
2102 if (!dg) return none;
2103 foreach (int y; 0..tilesHeight) {
2104 foreach (int x; 0..tilesWidth) {
2105 auto t = tiles[x, y];
2106 if (t && t.solid && t.visible && t.isInstanceAlive) {
2107 if (dg(x, y, t)) return t;
2115 // ////////////////////////////////////////////////////////////////////////// //
2116 // return `true` from delegate to stop
2117 MapTile forEachNormalTile (bool delegate (int x, int y, MapTile t) dg) {
2118 if (!dg) return none;
2119 foreach (int y; 0..tilesHeight) {
2120 foreach (int x; 0..tilesWidth) {
2121 auto t = tiles[x, y];
2122 if (t && t.visible && t.isInstanceAlive) {
2123 if (dg(x, y, t)) return t;
2131 // WARNING! don't destroy tiles here! (instanceRemove() is ok, tho)
2132 MapTile forEachTile (bool delegate (MapTile t) dg) {
2133 if (!dg) return none;
2134 foreach (int y; 0..tilesHeight) {
2135 foreach (int x; 0..tilesWidth) {
2136 auto t = tiles[x, y];
2137 if (t && t.visible && !t.spectral && t.isInstanceAlive) {
2138 if (dg(t)) return t;
2142 foreach (MapObject o; miscTileGrid.allObjects()) {
2143 auto mt = MapTile(o);
2145 if (mt.visible && !mt.spectral && mt.isInstanceAlive) {
2146 //writeln("special map tile: '", GetClassName(mt.Class), "'");
2147 if (dg(mt)) return mt;
2154 // ////////////////////////////////////////////////////////////////////////// //
2155 final void fixWallTiles () {
2156 foreach (int y; 0..tilesHeight) {
2157 foreach (int x; 0..tilesWidth) {
2158 auto t = getTileAt(x, y);
2161 if (y == tilesHeight-2) {
2162 writeln("0: tx=", x, "; tile=", t.objName, "; ty=", y, ": (", t.iy/16, ")");
2163 } else if (y == tilesHeight-1) {
2164 writeln("1: tx=", x, "; tile=", t.objName, "; ty=", y, ": (", t.iy/16, ")");
2170 foreach (MapTile t; miscTileGrid.allObjects()) {
2171 if (t.isInstanceAlive) t.beautifyTile();
2176 // ////////////////////////////////////////////////////////////////////////// //
2177 final MapTile isCollisionAtPoint (int px, int py, optional bool delegate (MapTile dg) dg) {
2178 if (!dg) dg = &cbCollisionAnySolid;
2179 return checkTilesInRect(px, py, 1, 1, dg);
2183 // ////////////////////////////////////////////////////////////////////////// //
2184 string scrGetKaliGift (MapTile altar, optional name gift) {
2187 // find other side of the altar
2188 int sx = player.ix, sy = player.iy;
2192 auto a2 = isCollisionAtPoint(altar.ix+18, altar.iy+8, &cbCollisionSacAltar);
2193 if (!a2) a2 = isCollisionAtPoint(altar.ix-8, altar.iy+8, &cbCollisionSacAltar);
2194 if (a2) { sx = a2.ix; sy = a2.iy; }
2197 if (global.favor <= -8) res = "SHE SEEMS VERY ANGRY WITH YOU!";
2198 else if (global.favor < 0) res = "SHE SEEMS ANGRY WITH YOU.";
2199 else if (global.favor == 0) res = "SHE HAS FORGIVEN YOU!";
2200 else if (global.favor >= 32) {
2201 if (global.kaliGift >= 3 && global.favor >= 32+(global.kaliGift-2)*16) {
2202 res = "YOU FEEL INVIGORATED!";
2203 global.kaliGift += 1;
2204 global.plife += global.randOther(4, 8);
2205 } else if (global.kaliGift >= 3) {
2206 res = "SHE SEEMS ECSTATIC WITH YOU!";
2207 } else if (global.bombs < 80) {
2208 res = "YOUR SATCHEL FEELS VERY FULL NOW!";
2209 global.kaliGift = 3;
2212 res = "YOU FEEL INVIGORATED!";
2213 global.kaliGift += 1;
2214 global.plife += global.randOther(4, 8);
2216 } else if (global.favor >= 16) {
2217 if (global.kaliGift >= 2) {
2218 res = "SHE SEEMS VERY HAPPY WITH YOU!";
2220 res = "SHE BESTOWS A GIFT UPON YOU!";
2221 global.kaliGift = 2;
2223 auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2226 obj = MakeMapObject(sx, sy-8, 'oPoof');
2231 if (gift) obj = MakeMapObject(sx, sy-8, gift);
2232 if (!obj) obj = MakeMapObject(sx, sy-8, 'oKapala');
2234 } else if (global.favor >= 8) {
2235 if (global.kaliGift >= 1) {
2236 res = "SHE SEEMS HAPPY WITH YOU.";
2238 res = "SHE BESTOWS A GIFT UPON YOU!";
2239 global.kaliGift = 1;
2240 //rAltar = instance_nearest(x, y, oSacAltarRight);
2241 //if (instance_exists(rAltar)) {
2243 auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2246 obj = MakeMapObject(sx, sy-8, 'oPoof');
2250 if (gift) obj = MakeMapObject(sx, sy-8, gift);
2252 auto n = global.randOther(1, 8);
2256 if (n == 1 && !global.hasCape && !global.hasJetpack) aname = 'oCapePickup';
2257 else if (n == 2 && !global.hasGloves) aname = 'oGloves';
2258 else if (n == 3 && !global.hasSpectacles) aname = 'oSpectacles';
2259 else if (n == 4 && !global.hasMitt) aname = 'oMitt';
2260 else if (n == 5 && !global.hasSpringShoes) aname = 'oSpringShoes';
2261 else if (n == 6 && !global.hasSpikeShoes) aname = 'oSpikeShoes';
2262 else if (n == 7 && !global.hasStickyBombs) aname = 'oPaste';
2263 else if (n == 8 && !global.hasCompass) aname = 'oCompass';
2265 obj = MakeMapObject(sx, sy-8, aname);
2271 obj = MakeMapObject(sx, sy-8, (global.hasJetpack ? 'oBombBox' : 'oJetpack'));
2277 } else if (global.favor > 0) {
2278 res = "SHE SEEMS PLEASED WITH YOU.";
2283 global.message = "";
2284 res = "KALI DEVOURS YOU!"; // sacrifice is player
2292 void performSacrifice (MapObject what, MapTile where) {
2293 if (!what || !what.isInstanceAlive) return;
2294 MakeMapObject(what.ix+8, what.iy+8, 'oFlame');
2295 if (where) where.playSound('sndSmallExplode'); else what.playSound('sndSmallExplode');
2296 if (!what.bloodless) what.scrCreateBlood(what.ix+8, what.iy+8, 3);
2298 string msg = "KALI ACCEPTS THE SACRIFICE!";
2300 auto idol = ItemGoldIdol(what);
2302 ++stats.totalSacrifices;
2303 if (global.favor <= -8) msg = "SHE SEEMS VERY ANGRY WITH YOU!";
2304 else if (global.favor < 0) msg = "SHE SEEMS ANGRY WITH YOU.";
2305 else if (global.favor >= 0) {
2306 // find other side of the altar
2307 int sx = player.ix, sy = player.iy;
2312 auto a2 = isCollisionAtPoint(altar.ix+18, altar.iy+8, &cbCollisionSacAltar);
2313 if (!a2) a2 = isCollisionAtPoint(altar.ix-8, altar.iy+8, &cbCollisionSacAltar);
2314 if (a2) { sx = a2.ix; sy = a2.iy; }
2317 auto obj = MakeMapObject(sx, sy-8, 'oPoof');
2320 obj = MakeMapObject(sx, sy-8, 'oPoof');
2324 obj = MakeMapObject(sx, sy-16-8, 'oMonkeyGold');
2326 osdMessage(msg, 6.66);
2328 idol.instanceRemove();
2332 if (global.favor <= -8) {
2333 msg = "KALI DEVOURS THE SACRIFICE!";
2334 } else if (global.favor < 0) {
2335 global.favor += (what.status == MapObject::STUNNED ? what.favor : round(what.favor/2.0)); //k8: added `round()`
2336 if (what.favor > 0) what.favor = 0;
2338 global.favor += (what.status == MapObject::STUNNED ? what.favor : round(what.favor/2.0)); //k8: added `round()`
2342 if (myAltar.gift1 != "" and global.kaliGift == 0) scrGetKaliGift(myAltar.gift1);
2343 else if (myAltar.gift2 != "" and global.kaliGift == 1) scrGetKaliGift(myAltar.gift2);
2344 else scrGetKaliGift("");
2347 // sacrifice is player?
2348 if (what isa PlayerPawn) {
2349 ++stats.totalSelfSacrifices;
2350 msg = "KALI DEVOURS YOU!";
2351 player.visible = false;
2353 player.status = MapObject::DEAD;
2355 ++stats.totalSacrifices;
2356 auto msg2 = scrGetKaliGift(where);
2357 what.instanceRemove();
2358 if (msg2) msg = va("%s\n%s", msg, msg2);
2361 osdMessage(msg, 6.66);
2363 //!if (isRealLevel()) global.totalSacrifices += 1;
2365 //!global.messageTimer = 200;
2366 //!global.shake = 10;
2370 instance_create(x, y, oFlame);
2371 playSound(global.sndSmallExplode);
2372 scrCreateBlood(x, y, 3);
2373 global.message = "KALI ACCEPTS YOUR SACRIFICE!";
2374 if (global.favor <= -8) {
2375 global.message = "KALI DEVOURS YOUR SACRIFICE!";
2376 } else if (global.favor < 0) {
2377 if (status == 98) global.favor += favor*1.5; else global.favor += favor;
2378 if (favor > 0) favor = 0;
2380 if (status == 98) global.favor += favor*1.5; else global.favor += favor;
2383 if (myAltar.gift1 != "" and global.kaliGift == 0) scrGetFavorMsg(myAltar.gift1);
2384 else if (myAltar.gift2 != "" and global.kaliGift == 1) scrGetFavorMsg(myAltar.gift2);
2385 else scrGetFavorMsg("");
2387 global.messageTimer = 200;
2394 // ////////////////////////////////////////////////////////////////////////// //
2395 final void addBackgroundGfxDetails () {
2396 // add background details
2397 //if (global.customLevel || global.parallax) return;
2399 // bg = instance_create(16*randRoom(1, 42), 16*randRoom(1, 33), oCaveBG);
2400 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);
2401 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);
2402 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);
2403 else MakeMapBackTile('bgExtras', 32*global.randRoom(0, 1), 0, 32, 32, 16*global.randRoom(1, 42), 16*global.randRoom(1, 33), 10002);
2408 // ////////////////////////////////////////////////////////////////////////// //
2409 private final void fixRealViewStart () {
2410 int scale = global.scale;
2411 realViewStart.x = clamp(realViewStart.x, viewMin.x*scale, viewMax.x*scale-viewWidth);
2412 realViewStart.y = clamp(realViewStart.y, viewMin.y*scale, viewMax.y*scale-viewHeight);
2416 private final void fixViewStart () {
2417 int scale = global.scale;
2418 viewStart.x = clamp(viewStart.x, viewMin.x*scale, viewMax.x*scale-viewWidth);
2419 viewStart.y = clamp(viewStart.y, viewMin.y*scale, viewMax.y*scale-viewHeight);
2423 final void centerViewAtPlayer () {
2424 if (viewWidth < 1 || viewHeight < 1 || !player) return;
2425 centerViewAt(player.xCenter, player.yCenter);
2429 final void centerViewAt (int x, int y) {
2430 if (viewWidth < 1 || viewHeight < 1) return;
2431 int scale = global.scale;
2434 realViewStart.x = clamp(x-viewWidth/2, 0, tilesWidth*16*scale-viewWidth);
2435 realViewStart.y = clamp(y-viewHeight/2, 0, tilesHeight*16*scale-viewHeight);
2438 viewStart.x = realViewStart.x;
2439 viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
2444 const int ViewPortToleranceX = 16*1+8;
2445 const int ViewPortToleranceY = 16*1+8;
2447 final void fixCamera () {
2448 if (!player) return;
2449 if (viewWidth < 1 || viewHeight < 1) return;
2450 int scale = global.scale;
2451 auto alwaysCenter = global.config.alwaysCenterPlayer;
2452 // calculate offset from viewport center (in game units), and fix viewport
2454 if (!player.cameraBlockX) {
2455 int x = player.xCenter*scale;
2456 int cx = realViewStart.x;
2460 int xofs = x-(cx+viewWidth/2);
2461 if (xofs < -ViewPortToleranceX*scale) cx += xofs+ViewPortToleranceX*scale;
2462 else if (xofs > ViewPortToleranceX*scale) cx += xofs-ViewPortToleranceX*scale;
2464 realViewStart.x = clamp(cx, 0, tilesWidth*16*scale-viewWidth);
2467 if (!player.cameraBlockY) {
2468 int y = player.yCenter*scale;
2469 int cy = realViewStart.y;
2471 cy = y-viewHeight/2;
2473 int yofs = y-(cy+viewHeight/2);
2474 if (yofs < -ViewPortToleranceY*scale) cy += yofs+ViewPortToleranceY*scale;
2475 else if (yofs > ViewPortToleranceY*scale) cy += yofs-ViewPortToleranceY*scale;
2477 realViewStart.y = clamp(cy, 0, tilesHeight*16*scale-viewHeight);
2481 viewStart.x = realViewStart.x;
2482 viewStart.y = clamp(realViewStart.y+(player ? player.viewOffset*scale : 0), 0, tilesHeight*16*scale-viewHeight);
2487 // ////////////////////////////////////////////////////////////////////////// //
2488 // x0 and y0 are non-scaled (and will be scaled)
2489 final void drawSpriteAt (name sprName, float frnumf, int x0, int y0, optional bool small) {
2490 if (!sprName) return;
2491 auto spr = sprStore[sprName];
2492 if (!spr || !spr.frames.length) return;
2493 int scale = global.scale;
2496 int frnum = max(0, trunc(frnumf))%spr.frames.length;
2497 auto sfr = spr.frames[frnum];
2498 int sx0 = x0-sfr.xofs*scale;
2499 int sy0 = y0-sfr.yofs*scale;
2500 if (small && scale > 1) {
2501 sfr.tex.blitExt(sx0, sy0, round(sx0+sfr.tex.width*(scale/2.0)), round(sy0+sfr.tex.height*(scale/2.0)), 0, 0);
2503 sfr.tex.blitAt(sx0, sy0, scale);
2508 // x0 and y0 are non-scaled (and will be scaled)
2509 final void drawTextAt (int x0, int y0, string text) {
2511 int scale = global.scale;
2514 sprStore.renderText(x0, y0, text, scale);
2518 void renderCompass (float currFrameDelta) {
2519 if (!global.hasCompass) return;
2522 if (isRoom("rOlmec")) {
2525 } else if (isRoom("rOlmec2")) {
2531 bool hasMessage = osdHasMessage();
2532 foreach (MapTile et; allExits) {
2534 int exitX = et.ix, exitY = et.iy;
2535 int vx0 = viewStart.x/global.scale, vy0 = viewStart.y/global.scale;
2536 int vx1 = (viewStart.x+viewWidth)/global.scale;
2537 int vy1 = (viewStart.y+viewHeight)/global.scale;
2538 if (exitY > vy1-16) {
2540 drawSpriteAt(hasMessage ? 'sCompassSmallLL' : 'sCompassLL', 0, 0, 224);
2541 } else if (exitX > vx1-16) {
2542 drawSpriteAt(hasMessage ? 'sCompassSmallLR' : 'sCompassLR', 0, 304, 224);
2544 drawSpriteAt(hasMessage ? 'sCompassSmallDown' : 'sCompassDown', 0, exitX-vx0, 224);
2546 } else if (exitX < vx0) {
2547 drawSpriteAt(hasMessage ? 'sCompassSmallLeft' : 'sCompassLeft', 0, 0, exitY-vy0);
2548 } else if (exitX > vx1-16) {
2549 drawSpriteAt(hasMessage ? 'sCompassSmallRight' : 'sCompassRight', 0, 304, exitY-vy0);
2555 final bool totalsNameCmpCB (ref GameStats::TotalItem a, ref GameStats::TotalItem b) {
2556 auto sa = string(a.objName);
2557 auto sb = string(b.objName);
2561 void renderTransitionInfo (float currFrameDelta) {
2564 GameStats.sortTotalsList(stats.kills, &totalsNameCmpCB);
2567 foreach (int idx, ref auto k; stats.kills) {
2568 string s = string(k);
2569 maxLen = max(maxLen, s.length);
2573 sprStore.loadFont('sFontSmall');
2574 Video.color = 0xff_ff_00;
2575 foreach (int idx, ref auto k; stats.kills) {
2577 foreach (int xidx, ref auto d; stats.totalKills) {
2578 if (d.objName == k) { deaths = d.count; break; }
2580 //drawTextAt(16, 4+idx*8, va("%s: %d (%d)", string(k.objName).toUpperCase, k.count, deaths));
2581 drawTextAt(16, 4+idx*8, string(k).toUpperCase);
2582 drawTextAt(16+maxLen, 4+idx*8, va(": %d (%d)", k.count, deaths));
2588 void renderGhostTimer (float currFrameDelta) {
2589 if (ghostTimeLeft <= 0) return;
2590 //ghostTimeLeft /= 30; // frames -> seconds
2592 int hgt = Video.screenHeight-64;
2593 if (hgt < 1) return;
2594 int rhgt = round(float(hgt)*(float(ghostTimeLeft)/30.0)/1000.0);
2595 //writeln("hgt=", hgt, "; rhgt=", rhgt, "; gtl=", ghostTimeLeft/30);
2597 auto oclr = Video.color;
2598 Video.color = 0xcf_ff_7f_00;
2599 Video.fillRect(Video.screenWidth-20, 32, 16, hgt-rhgt);
2600 Video.color = 0x7f_ff_7f_00;
2601 Video.fillRect(Video.screenWidth-20, 32+(hgt-rhgt), 16, rhgt);
2607 void renderHUD (float currFrameDelta) {
2608 if (levelKind == LevelKind.Transition) { renderTransitionInfo(currFrameDelta); return; }
2616 bool scumSmallHud = global.config.scumSmallHud;
2617 if (!global.config.optSGAmmo) moneyX = ammoX;
2620 sprStore.loadFont('sFontSmall');
2623 sprStore.loadFont('sFont');
2626 Video.color = 0xff_ff_ff;
2630 if (global.plife == 1) {
2631 drawSpriteAt('sHeartSmallBlink', global.heartBlink, lifeX+2, 4-hhup);
2632 global.heartBlink += 0.1;
2633 if (global.heartBlink > 3) global.heartBlink = 0;
2635 drawSpriteAt('sHeartSmall', -1, lifeX+2, 4-hhup);
2636 global.heartBlink = 0;
2639 if (global.plife == 1) {
2640 drawSpriteAt('sHeartBlink', global.heartBlink, lifeX, 8-hhup);
2641 global.heartBlink += 0.1;
2642 if (global.heartBlink > 3) global.heartBlink = 0;
2644 drawSpriteAt('sHeart', -1, lifeX, 8-hhup);
2645 global.heartBlink = 0;
2649 int life = clamp(global.plife, 0, 99);
2650 //if (!scumHud && life > 99) life = 99;
2651 drawTextAt(lifeX+16, 8-hhup, va("%d", life));
2654 if (global.hasStickyBombs && global.stickyBombsActive) {
2655 if (scumSmallHud) drawSpriteAt('sStickyBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sStickyBombIcon', -1, bombX, 8-hhup);
2657 if (scumSmallHud) drawSpriteAt('sBombIconSmall', -1, bombX+2, 4-hhup); else drawSpriteAt('sBombIcon', -1, bombX, 8-hhup);
2659 int n = global.bombs;
2660 if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
2661 drawTextAt(bombX+16, 8-hhup, va("%d", n));
2664 if (scumSmallHud) drawSpriteAt('sRopeIconSmall', -1, ropeX+2, 4-hhup); else drawSpriteAt('sRopeIcon', -1, ropeX, 8-hhup);
2666 if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
2667 drawTextAt(ropeX+16, 8-hhup, va("%d", n));
2670 if (global.config.optSGAmmo) {
2671 if (scumSmallHud) drawSpriteAt('sShellsIcon', -1, ammoX+6, 8-hhup); else drawSpriteAt('sShellsIcon', -1, ammoX+7, 12-hhup);
2673 if (n < 0) n = 0; else if (!scumSmallHud && n > 99) n = 99;
2674 drawTextAt(ammoX+16, 8-hhup, va("%d", n));
2678 if (scumSmallHud) drawSpriteAt('sDollarSignSmall', -1, moneyX+2, 3-hhup); else drawSpriteAt('sDollarSign', -1, moneyX, 8-hhup);
2679 drawTextAt(moneyX+16, 8-hhup, va("%d", stats.money-xmoney));
2681 int ity = (scumSmallHud ? 18-hhup : 24-hhup);
2684 if (global.hasUdjatEye) {
2685 if (global.udjatBlink) drawSpriteAt('sUdjatEyeIcon2', -1, n, ity); else drawSpriteAt('sUdjatEyeIcon', -1, n, ity);
2688 if (global.hasAnkh) { drawSpriteAt('sAnkhIcon', -1, n, ity); n += 20; }
2689 if (global.hasCrown) { drawSpriteAt('sCrownIcon', -1, n, ity); n += 20; }
2690 if (global.hasKapala) {
2691 if (global.bloodLevel == 0) drawSpriteAt('sKapalaIcon', 0, n, ity);
2692 else if (global.bloodLevel <= 2) drawSpriteAt('sKapalaIcon', 1, n, ity);
2693 else if (global.bloodLevel <= 4) drawSpriteAt('sKapalaIcon', 2, n, ity);
2694 else if (global.bloodLevel <= 6) drawSpriteAt('sKapalaIcon', 3, n, ity);
2695 else if (global.bloodLevel <= 8) drawSpriteAt('sKapalaIcon', 4, n, ity);
2698 if (global.hasSpectacles) { drawSpriteAt('sSpectaclesIcon', -1, n, ity); n += 20; }
2699 if (global.hasGloves) { drawSpriteAt('sGlovesIcon', -1, n, ity); n += 20; }
2700 if (global.hasMitt) { drawSpriteAt('sMittIcon', -1, n, ity); n += 20; }
2701 if (global.hasSpringShoes) { drawSpriteAt('sSpringShoesIcon', -1, n, ity); n += 20; }
2702 if (global.hasJordans) { drawSpriteAt('sJordansIcon', -1, n, ity); n += 20; }
2703 if (global.hasSpikeShoes) { drawSpriteAt('sSpikeShoesIcon', -1, n, ity); n += 20; }
2704 if (global.hasCape) { drawSpriteAt('sCapeIcon', -1, n, ity); n += 20; }
2705 if (global.hasJetpack) { drawSpriteAt('sJetpackIcon', -1, n, ity); n += 20; }
2706 if (global.hasStickyBombs) { drawSpriteAt('sPasteIcon', -1, n, ity); n += 20; }
2707 if (global.hasCompass) { drawSpriteAt('sCompassIcon', -1, n, ity); n += 20; }
2708 if (global.hasParachute) { drawSpriteAt('sParachuteIcon', -1, n, ity); n += 20; }
2710 if (player.holdItem && player.holdItem.objName == 'Bow' && global.arrows > 0) {
2713 while (m <= global.arrows && m <= 20 && malpha > 0) {
2714 Video.color = trunc(malpha*255)<<24|0xff_ff_ff;
2715 drawSpriteAt('sArrowIcon', -1, n, ity);
2717 if (m > 10) malpha -= 0.1; //and global.arrows > 20 malpha -= 0.1
2720 Video.color = 0xff_ff_ff;
2724 sprStore.loadFont('sFontSmall');
2725 Video.color = 0xff_ff_00;
2726 if (scumSmallHud) drawTextAt(moneyX+8, 8+8-hhup, va("+%d", xmoney));
2727 else drawTextAt(moneyX, 8+16-hhup, va("+%d", xmoney));
2730 if (global.config.ghostShowTime) renderGhostTimer(currFrameDelta);
2734 // ////////////////////////////////////////////////////////////////////////// //
2735 private transient array!MapEntity renderVisibleCids;
2736 private transient array!MapTile renderMidTiles, renderFrontTiles; // normal, with fg
2738 final bool renderSortByDepth (MapEntity oa, MapEntity ob) {
2739 //MapObject oa = MapObject(a);
2740 //MapObject ob = MapObject(b);
2741 auto da = oa.depth, db = ob.depth;
2742 if (da == db) return (oa.objId < ob.objId);
2747 const int RenderEdgePix = 32;
2749 void renderWithOfs (int xofs, int yofs, float currFrameDelta) {
2750 int scale = global.scale;
2753 Video.color = 0xff_ff_ff;
2755 // render cave background
2757 int bgw = levBGImg.tex.width*scale;
2758 int bgh = levBGImg.tex.height*scale;
2759 int bgTW = (tilesWidth*tsz+bgw-1)/bgw;
2760 int bgTH = (tilesHeight*tsz+bgh-1)/bgh;
2761 int bgX0 = max(0, xofs/bgw);
2762 int bgY0 = max(0, yofs/bgh);
2763 int bgX1 = min(bgTW, (xofs+viewWidth+bgw-1)/bgw);
2764 int bgY1 = min(bgTH, (yofs+viewHeight+bgh-1)/bgh);
2765 foreach (int ty; bgY0..bgY1) {
2766 foreach (int tx; bgX0..bgX1) {
2767 int x0 = tx*bgw-xofs;
2768 int y0 = ty*bgh-yofs;
2769 levBGImg.tex.blitAt(x0, y0, scale);
2774 // render background tiles
2775 for (MapBackTile bt = backtiles; bt; bt = bt.next) {
2776 bt.drawWithOfs(xofs, yofs, scale, currFrameDelta);
2779 // render stationary tiles
2780 int tileX0 = max(0, xofs/tsz);
2781 int tileY0 = max(0, yofs/tsz);
2782 int tileX1 = min(tilesWidth, (xofs+viewWidth+tsz-1)/tsz);
2783 int tileY1 = min(tilesHeight, (yofs+viewHeight+tsz-1)/tsz);
2785 // render backs; collect tile arrays
2786 renderMidTiles.length -= renderMidTiles.length; // don't realloc
2787 renderFrontTiles.length -= renderFrontTiles.length; // don't realloc
2789 foreach (int ty; tileY0..tileY1) {
2790 foreach (int tx; tileX0..tileX1) {
2791 auto tile = getTileAt(tx, ty);
2792 if (tile && tile.visible && tile.isInstanceAlive) {
2793 renderMidTiles[$] = tile;
2794 if (tile.bgfront) renderFrontTiles[$] = tile;
2795 if (tile.bgback) tile.drawWithOfsBack(xofs, yofs, scale, currFrameDelta);
2800 // render "mid" (i.e. normal) tiles
2801 foreach (MapTile tile; renderMidTiles) tile.drawWithOfs(xofs, yofs, scale, currFrameDelta);
2803 // render moveable tiles
2805 foreach (MapTile mt; miscTileGrid.allObjects()) {
2806 if (mt.visible && mt.isInstanceAlive) {
2807 Video.color = (mt.moveable ? 0xff_7f_00 : 0xff_ff_ff);
2808 mt.drawWithOfs(xofs, yofs, scale, currFrameDelta);
2812 renderVisibleCids.length -= renderVisibleCids.length;
2814 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)) {
2815 if (!mt.visible || !mt.isInstanceAlive) continue;
2816 //Video.color = (mt.moveable ? 0xff_7f_00 : 0xff_ff_ff);
2817 //!mt.drawWithOfs(xofs, yofs, scale, currFrameDelta);
2818 renderVisibleCids[$] = mt;
2820 //Video.color = 0xff_ff_ff;
2822 // render objects (and player)
2823 if (player && player.visible) renderVisibleCids[$] = player; // include player in render list
2824 auto ogrid = objGrid;
2825 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)) {
2826 if (o.visible && o.isInstanceAlive) renderVisibleCids[$] = o;
2828 EntityGrid.sortEntList(renderVisibleCids, &renderSortByDepth);
2830 auto depth4Start = 0;
2831 foreach (auto xidx, MapEntity o; renderVisibleCids) {
2838 foreach (auto idx; depth4Start..renderVisibleCids.length; reverse) {
2839 MapEntity o = renderVisibleCids[idx];
2840 //if (idx && renderSortByDepth(o, renderVisibleCids[idx-1])) writeln("VIOLATION!");
2841 o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
2845 //if (player && player.visible) player.drawWithOfs(xofs, yofs, scale, currFrameDelta);
2847 // render front tile parts (depth 3.5)
2848 foreach (MapTile tile; renderFrontTiles) tile.drawWithOfsFront(xofs, yofs, scale, currFrameDelta);
2850 // render items with depth 3 and less
2851 foreach (auto idx; 0..depth4Start; reverse) {
2852 MapEntity o = renderVisibleCids[idx];
2853 o.drawWithOfs(xofs, yofs, scale, currFrameDelta);
2856 renderVisibleCids.length -= renderVisibleCids.length;
2858 // render player post-effects, so cape or jetpack will be correctly rendered on level exit, for example
2859 player.lastDrawWithOfs(xofs, yofs, scale, currFrameDelta);
2861 if (global.config.drawHUD) renderHUD(currFrameDelta);
2862 renderCompass(currFrameDelta);
2864 float osdTimeLeft, osdTimeStart;
2865 string msg = osdGetMessage(out osdTimeLeft, out osdTimeStart);
2867 auto ct = GetTickCount();
2869 sprStore.loadFont('sFontSmall');
2870 auto msgHeight = sprStore.getMultilineTextHeight(msg, msgScale);
2871 int x = Video.screenWidth/2;
2872 int y = Video.screenHeight-64-msgHeight;
2873 auto oldColor = Video.color;
2874 Video.color = 0xff_ff_00;
2875 if (osdTimeLeft < 0.5) {
2876 int alpha = 255-clamp(int(510*osdTimeLeft), 0, 255);
2877 Video.color = Video.color|(alpha<<24);
2878 } else if (ct-osdTimeStart < 0.5) {
2879 osdTimeStart = ct-osdTimeStart;
2880 int alpha = 255-clamp(int(510*osdTimeStart), 0, 255);
2881 Video.color = Video.color|(alpha<<24);
2883 sprStore.renderMultilineTextCentered(x, y, msg, msgScale);
2884 Video.color = oldColor;
2889 // ////////////////////////////////////////////////////////////////////////// //
2890 final class!MapObject findGameObjectClassByName (name aname) {
2891 if (!aname) return none; // just in case
2892 auto co = FindClassByGameObjName(aname);
2894 writeln("***UNKNOWN GAME OBJECT: '", aname, "'");
2897 co = GetClassReplacement(co);
2898 if (!co) FatalError("findGameObjectClassByName: WTF?!");
2899 if (!class!MapObject(co)) FatalError("findGameTileClassByName: object '%n' is not a map object, it is a `%n`", aname, GetClassName(co));
2900 return class!MapObject(co);
2904 final class!MapTile findGameTileClassByName (name aname) {
2905 if (!aname) return none; // just in case
2906 auto co = FindClassByGameObjName(aname);
2907 if (!co) return MapTile; // unknown names will be routed directly to tile object
2908 co = GetClassReplacement(co);
2909 if (!co) FatalError("findGameTileClassByName: WTF?!");
2910 if (!class!MapTile(co)) FatalError("findGameTileClassByName: object '%n' is not a tile, it is a `%n`", aname, GetClassName(co));
2911 return class!MapTile(co);
2915 final MapObject findAnyObjectOfType (name aname) {
2916 if (!aname) return none;
2917 auto cls = FindClassByGameObjName(aname);
2918 if (!cls) return none;
2919 for (auto cid = objGrid.getFirstObject(); cid; cid = objGrid.getNextObject(cid)) {
2920 MapObject obj = objGrid.getObject(MapObject, cid);
2921 if (!obj || obj.spectral || !obj.isInstanceAlive) continue;
2922 if (obj isa cls) return obj;
2928 // ////////////////////////////////////////////////////////////////////////// //
2929 final MapTile CreateMapTile (int xpos, int ypos, name aname) {
2930 if (!aname) FatalError("cannot create typeless tile");
2931 //MapTile tile = SpawnObject(aname == 'oRope' ? MapTileRope : MapTile);
2932 auto tclass = findGameTileClassByName(aname);
2933 if (!tclass) return none;
2934 MapTile tile = SpawnObject(tclass);
2935 tile.global = global;
2937 tile.objName = aname;
2938 tile.objType = aname; // just in case
2941 if (!tile.initialize()) FatalError("cannot create tile '%n'", aname);
2946 final bool isRopePlacedAt (int x, int y) {
2948 foreach (ref auto v; covered) v = false;
2949 foreach (MapTile t; miscTileGrid.inRectPix(x, y-8, 1, 17, precise:false)) {
2950 if (!cbIsRopeTile(t)) continue;
2951 if (t.ix != x) continue;
2952 if (t.iy == y) return true;
2953 foreach (int ty; t.iy..t.iy+8) {
2955 if (d >= 0 && d < covered.length) covered[d] = true;
2958 // check if the whole rope height is completely covered with ropes
2959 foreach (auto v; covered) if (!v) return false;
2964 // won't call `onDestroy()`
2965 final void RemoveMapTile (int tileX, int tileY) {
2966 if (tileX >= 0 & tileY >= 0 && tileX < tilesWidth && tileY < tilesHeight) {
2967 if (tiles[tileX, tileY]) checkWater = true;
2968 delete tiles[tileX, tileY];
2969 tiles[tileX, tileY] = none;
2974 final MapTile MakeMapTile (int mapx, int mapy, name aname/*, optional name sprname*/) {
2975 if (mapx < 0 || mapx >= tilesWidth || mapy < 0 || mapy >= tilesHeight) return none;
2977 // if we already have rope tile there, there is no reason to add another one
2978 if (aname == 'oRope') {
2979 if (isRopePlacedAt(mapx*16, mapy*16)) {
2980 //writeln("dupe rope (0)!");
2985 auto tile = CreateMapTile(mapx*16, mapy*16, aname);
2986 if (tile.moveable || tile.toSpecialGrid) {
2987 // moveable tiles goes to the separate list
2988 miscTileGrid.insert(tile);
2990 setTileAt(mapx, mapy, tile);
2994 case 'oEntrance': registerEnter(tile); break;
2995 case 'oExit': registerExit(tile); break;
3002 final void MarkTileAsWet (int tileX, int tileY) {
3003 auto t = getTileAt(tileX, tileY);
3004 if (t) t.wet = true;
3008 final MapTile MakeMapTileAtPix (int xpix, int ypix, name aname/*, optional name sprname*/) {
3009 if (xpix%16 == 0 && ypix%16 == 0) return MakeMapTile(xpix/16, ypix/16, aname);
3010 //if (mapx < 0 || mapx >= TilesWidth || mapy < 0 || mapy >= TilesHeight) return none;
3012 // if we already have rope tile there, there is no reason to add another one
3013 if (aname == 'oRope') {
3014 if (isRopePlacedAt(xpix, ypix)) {
3015 //writeln("dupe rope (0)!");
3020 auto tile = CreateMapTile(xpix, ypix, aname);
3021 // non-aligned tiles goes to the special grid
3022 miscTileGrid.insert(tile);
3025 case 'oEntrance': registerEnter(tile); break;
3026 case 'oExit': registerExit(tile); break;
3033 final MapTile MakeMapRopeTileAt (int x0, int y0) {
3034 // if we already have rope tile there, there is no reason to add another one
3035 if (isRopePlacedAt(x0, y0)) {
3036 //writeln("dupe rope (1)!");
3040 auto tile = CreateMapTile(x0, y0, 'oRope');
3041 miscTileGrid.insert(tile);
3047 // ////////////////////////////////////////////////////////////////////////// //
3048 final MapBackTile CreateBgTile (name sprName, optional int atx0, optional int aty0, optional int aw, optional int ah) {
3049 BackTileImage img = bgtileStore[sprName];
3050 auto res = SpawnObject(MapBackTile);
3051 res.global = global;
3054 res.bgtName = sprName;
3055 if (specified_atx0) res.tx0 = atx0;
3056 if (specified_aty0) res.ty0 = aty0;
3057 if (specified_aw) res.w = aw;
3058 if (specified_ah) res.h = ah;
3059 if (!res.initialize()) FatalError("cannot create background tile with image '%n'", sprName);
3064 // ////////////////////////////////////////////////////////////////////////// //
3066 background The background asset from which the new tile will be extracted.
3067 left The x coordinate of the left of the new tile, relative to the background asset's top left corner.
3068 top The y coordinate of the top of the new tile, relative to the background assets top left corner.
3069 width The width of the tile.
3070 height The height of the tile.
3071 x The x position in the room to place the tile.
3072 y The y position in the room to place the tile.
3073 depth The depth at which to place the tile.
3075 final void MakeMapBackTile (name bgname, int left, int top, int width, int height, int x, int y, int depth) {
3076 if (width < 1 || height < 1 || !bgname) return;
3077 auto bgt = bgtileStore[bgname];
3078 if (!bgt) FatalError("cannot load background '%n'", bgname);
3079 MapBackTile bt = SpawnObject(MapBackTile);
3082 bt.objName = bgname;
3084 bt.bgtName = bgname;
3092 // find a place for it
3097 // back tiles with the highest depth should come first
3098 MapBackTile ct = backtiles, cprev = none;
3099 while (ct && depth <= ct.depth) { cprev = ct; ct = ct.next; }
3102 bt.next = cprev.next;
3105 bt.next = backtiles;
3111 // ////////////////////////////////////////////////////////////////////////// //
3112 final MapObject SpawnMapObjectWithClass (class!MapObject oclass) {
3113 if (!oclass) return none;
3115 MapObject obj = SpawnObject(oclass);
3116 if (!obj) FatalError("cannot create map object '%n'", GetClassName(oclass));
3118 //if (aname == 'oShells' || aname == 'oShellSingle' || aname == 'oRopePile') writeln("*** CREATED '", aname, "'");
3120 obj.global = global;
3127 final MapObject SpawnMapObject (name aname) {
3128 if (!aname) return none;
3129 return SpawnMapObjectWithClass(findGameObjectClassByName(aname));
3133 final MapObject PutSpawnedMapObject (int x, int y, MapObject obj) {
3134 if (!obj /*|| obj.global || obj.level*/) return none; // oops
3138 if (!obj.initialize()) { delete obj; return none; } // not fatal
3146 final MapObject MakeMapObject (int x, int y, name aname) {
3147 MapObject obj = SpawnMapObject(aname);
3148 obj = PutSpawnedMapObject(x, y, obj);
3153 // ////////////////////////////////////////////////////////////////////////// //
3154 #include "roomTitle.vc"
3155 #include "roomTrans1.vc"
3156 #include "roomTrans2.vc"
3157 #include "roomTrans3.vc"
3158 #include "roomTrans4.vc"
3159 #include "roomOlmec.vc"
3162 // ////////////////////////////////////////////////////////////////////////// //
3163 #include "packages/Generator/loadRoomGens.vc"
3164 #include "packages/Generator/loadEntityGens.vc"