hanging code cleanup; slightly easier hang
[k8vacspelynky.git] / spelunky_main.vc
blob3c9cb5d55d1e27149681165b098d57e7473681d7
1 /**********************************************************************************
2  * Copyright (c) 2008, 2009 Derek Yu and Mossmouth, LLC
3  * Copyright (c) 2018, Ketmar Dark
4  *
5  * This file is part of Spelunky.
6  *
7  * You can redistribute and/or modify Spelunky, including its source code, under
8  * the terms of the Spelunky User License.
9  *
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.
12  *
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/>
16  *
17  **********************************************************************************/
18 import 'Video';
19 import 'SoundSys';
20 import 'Geom';
21 import 'Game';
22 import 'Generator';
23 import 'Sprites';
25 //#define QUIT_DOUBLE_ESC
27 //#define DARK_TEST
29 //#define MASK_TEST
31 //#define BIGGER_REPLAY_DATA
33 // ////////////////////////////////////////////////////////////////////////// //
34 #include "mapent/0all.vc"
35 #include "PlayerPawn.vc"
36 #include "PlayerPowerup.vc"
37 #include "GameLevel.vc"
40 // ////////////////////////////////////////////////////////////////////////// //
41 #include "uisimple.vc"
44 // ////////////////////////////////////////////////////////////////////////// //
45 class DebugSessionMovement : Object;
47 #ifdef BIGGER_REPLAY_DATA
48 array!(GameLevel::SavedKeyState) keypresses;
49 #else
50 array!ubyte keypresses; // on each frame
51 #endif
52 GameConfig playconfig;
54 transient int keypos;
55 transient int otherSeed, roomSeed;
58 override void Destroy () {
59   delete playconfig;
60   keypresses.length = 0;
61   ::Destroy();
65 final void resetReplay () {
66   keypos = 0;
70 #ifndef BIGGER_REPLAY_DATA
71 final void addKey (int kbidx, bool down) {
72   if (kbidx < 0 || kbidx >= 127) FatalError("DebugSessionMovement: invalid kbidx (%d)", kbidx);
73   keypresses[$] = kbidx|(down ? 0x80 : 0);
77 final void addEndOfFrame () {
78   keypresses[$] = 0xff;
82 enum {
83   NORMAL,
84   END_OF_FRAME,
85   END_OF_RECORD,
88 final int getKey (out int kbidx, out bool down) {
89   if (keypos < 0) FatalError("DebugSessionMovement: invalid keypos");
90   if (keypos >= keypresses.length) return END_OF_RECORD;
91   ubyte b = keypresses[keypos++];
92   if (b == 0xff) return END_OF_FRAME;
93   kbidx = b&0x7f;
94   down = (b >= 0x80);
95   return NORMAL;
97 #endif
100 // ////////////////////////////////////////////////////////////////////////// //
101 class TempOptionsKeys : Object;
103 int[16*GameConfig::MaxActionBinds] keybinds;
106 // ////////////////////////////////////////////////////////////////////////// //
107 class Main : Object;
109 transient string dbgSessionStateFileName = "debug_game_session_state";
110 transient string dbgSessionMovementFileName = "debug_game_session_movement";
112 GLTexture texTigerEye;
114 GameConfig config;
115 GameGlobal global;
116 SpriteStore sprStore;
117 BackTileStore bgtileStore;
118 GameLevel level;
120 enum StartMode {
121   Dead,
122   Alive,
123   Title,
124   Stars,
125   Sun,
126   Moon,
129 StartMode startMode = StartMode.Title;
130 bool freeRide = false;
131 bool switchInterpolator;
132 bool pauseRequested;
134 bool replayFastForward = false;
135 int replayFastForwardSpeed = 2;
136 bool saveGameSession = false;
137 bool replayGameSession = false;
138 enum Replay {
139   None,
140   Saving,
141   Replaying,
143 Replay doGameSavingPlaying = Replay.None;
144 float saveMovementLastTime = 0;
145 DebugSessionMovement debugMovement;
146 GameStats origStats; // for replaying
147 GameConfig origConfig; // for replaying
148 int origRoomSeed, origOtherSeed;
150 int showHelp;
151 int escCount;
153 #ifdef MASK_TEST
154 transient int maskSX, maskSY;
155 transient SpriteImage smask;
156 transient int maskFrame;
157 #endif
160 // ////////////////////////////////////////////////////////////////////////// //
161 final void saveKeyboardBindings () {
162   auto tok = SpawnObject(TempOptionsKeys);
163   foreach (auto idx, auto v; global.config.keybinds) tok.keybinds[idx] = v;
164   appSaveOptions(tok, "keybindings");
165   delete tok;
169 final void loadKeyboardBindings () {
170   auto tok = appLoadOptions(TempOptionsKeys, "keybindings");
171   if (tok) {
172     foreach (auto idx, ref auto v; global.config.keybinds) v = tok.keybinds[idx];
173     delete tok;
174   }
178 // ////////////////////////////////////////////////////////////////////////// //
179 void saveGameOptions () {
180   appSaveOptions(global.config, "config");
184 void loadGameOptions () {
185   auto cfg = appLoadOptions(GameConfig, "config");
186   if (cfg) {
187     auto oldHero = config.heroType;
188     auto tok = SpawnObject(TempOptionsKeys);
189     foreach (auto idx, auto v; global.config.keybinds) tok.keybinds[idx] = v;
190     delete global.config;
191     global.config = cfg;
192     config = cfg;
193     foreach (auto idx, ref auto v; global.config.keybinds) v = tok.keybinds[idx];
194     delete tok;
195     writeln("config loaded");
196     global.restartMusic();
197     global.fixVolumes();
198     //config.heroType = GameConfig::Hero.Spelunker;
199     config.heroType = oldHero;
200   }
201   // fix my bug
202   if (global.config.ghostExtraTime > 300) global.config.ghostExtraTime = 30;
206 // ////////////////////////////////////////////////////////////////////////// //
207 void saveGameStats () {
208   if (level.stats) appSaveOptions(level.stats, "stats");
212 void loadGameStats () {
213   auto stats = appLoadOptions(GameStats, "stats");
214   if (stats) {
215     delete level.stats;
216     level.stats = stats;
217   }
218   if (!level.stats) level.stats = SpawnObject(GameStats);
219   level.stats.global = global;
223 // ////////////////////////////////////////////////////////////////////////// //
224 struct UIPaneSaveInfo {
225   name id;
226   UIPane::SaveInfo nfo;
229 transient UIPane optionsPane; // either options, or binding editor
231 transient GameLevel::IVec2D optionsPaneOfs;
232 transient void delegate () saveOptionsDG;
234 transient array!UIPaneSaveInfo optionsPaneState;
237 final void saveCurrentPane () {
238   if (!optionsPane || !optionsPane.id) return;
240   // summon ghost
241   if (optionsPane.id == 'CheatFlags') {
242     if (instantGhost && level.ghostTimeLeft > 0) {
243       level.ghostTimeLeft = 1;
244     }
245   }
247   foreach (ref auto psv; optionsPaneState) {
248     if (psv.id == optionsPane.id) {
249       optionsPane.saveState(psv.nfo);
250       return;
251     }
252   }
253   // append new
254   optionsPaneState.length += 1;
255   optionsPaneState[$-1].id = optionsPane.id;
256   optionsPane.saveState(optionsPaneState[$-1].nfo);
260 final void restoreCurrentPane () {
261   if (optionsPane) optionsPane.setupHotkeys(); // why not?
262   if (!optionsPane || !optionsPane.id) return;
263   foreach (ref auto psv; optionsPaneState) {
264     if (psv.id == optionsPane.id) {
265       optionsPane.restoreState(psv.nfo);
266       return;
267     }
268   }
272 // ////////////////////////////////////////////////////////////////////////// //
273 final void onCheatObjectSpawnSelectedCB (UIMenuItem it) {
274   if (!it.tagClass) return;
275   if (class!MapObject(it.tagClass)) {
276     level.debugSpawnObjectWithClass(class!MapObject(it.tagClass), playerDir:true);
277     it.owner.closeMe = true;
278   }
282 // ////////////////////////////////////////////////////////////////////////// //
283 transient array!(class!MapObject) cheatItemsList;
286 final void fillCheatItemsList () {
287   cheatItemsList.length = 0;
288   cheatItemsList[$] = ItemProjectileArrow;
289   cheatItemsList[$] = ItemWeaponShotgun;
290   cheatItemsList[$] = ItemWeaponAshShotgun;
291   cheatItemsList[$] = ItemWeaponPistol;
292   cheatItemsList[$] = ItemWeaponMattock;
293   cheatItemsList[$] = ItemWeaponMachete;
294   cheatItemsList[$] = ItemWeaponWebCannon;
295   cheatItemsList[$] = ItemWeaponSceptre;
296   cheatItemsList[$] = ItemWeaponBow;
297   cheatItemsList[$] = ItemBones;
298   cheatItemsList[$] = ItemFakeBones;
299   cheatItemsList[$] = ItemFishBone;
300   cheatItemsList[$] = ItemRock;
301   cheatItemsList[$] = ItemJar;
302   cheatItemsList[$] = ItemSkull;
303   cheatItemsList[$] = ItemGoldenKey;
304   cheatItemsList[$] = ItemGoldIdol;
305   cheatItemsList[$] = ItemCrystalSkull;
306   cheatItemsList[$] = ItemShellSingle;
307   cheatItemsList[$] = ItemChest;
308   cheatItemsList[$] = ItemCrate;
309   cheatItemsList[$] = ItemLockedChest;
310   cheatItemsList[$] = ItemDice;
314 final UIPane createCheatItemsPane () {
315   if (!level.player) return none;
317   UIPane pane = SpawnObject(UIPane);
318   pane.id = 'Items';
319   pane.sprStore = sprStore;
321   pane.width = 320*3-64;
322   pane.height = 240*3-64;
324   foreach (auto ipk; cheatItemsList) {
325     auto it = UIMenuItem.Create(pane, ipk.default.desc.toUpperCase(), ipk.default.desc2.toUpperCase(), &onCheatObjectSpawnSelectedCB);
326     it.tagClass = ipk;
327   }
329   //optionsPaneOfs.x = 100;
330   //optionsPaneOfs.y = 50;
332   return pane;
336 // ////////////////////////////////////////////////////////////////////////// //
337 transient array!(class!MapObject) cheatEnemiesList;
340 final void fillCheatEnemiesList () {
341   cheatEnemiesList.length = 0;
342   cheatEnemiesList[$] = MonsterDamsel; // not an enemy, but meh..
343   cheatEnemiesList[$] = EnemyBat;
344   cheatEnemiesList[$] = EnemySpiderHang;
345   cheatEnemiesList[$] = EnemySpider;
346   cheatEnemiesList[$] = EnemySnake;
347   cheatEnemiesList[$] = EnemyCaveman;
348   cheatEnemiesList[$] = EnemySkeleton;
349   cheatEnemiesList[$] = MonsterShopkeeper;
350   cheatEnemiesList[$] = EnemyZombie;
351   cheatEnemiesList[$] = EnemyVampire;
352   cheatEnemiesList[$] = EnemyFrog;
353   cheatEnemiesList[$] = EnemyGreenFrog;
354   cheatEnemiesList[$] = EnemyFireFrog;
355   cheatEnemiesList[$] = EnemyMantrap;
356   cheatEnemiesList[$] = EnemyScarab;
357   cheatEnemiesList[$] = EnemyFloater;
358   cheatEnemiesList[$] = EnemyBlob;
359   cheatEnemiesList[$] = EnemyMonkey;
360   cheatEnemiesList[$] = EnemyGoldMonkey;
361   cheatEnemiesList[$] = EnemyAlien;
362   cheatEnemiesList[$] = EnemyYeti;
363   cheatEnemiesList[$] = EnemyHawkman;
364   cheatEnemiesList[$] = EnemyUFO;
365   cheatEnemiesList[$] = EnemyYetiKing;
369 final UIPane createCheatEnemiesPane () {
370   if (!level.player) return none;
372   UIPane pane = SpawnObject(UIPane);
373   pane.id = 'Enemies';
374   pane.sprStore = sprStore;
376   pane.width = 320*3-64;
377   pane.height = 240*3-64;
379   foreach (auto ipk; cheatEnemiesList) {
380     auto it = UIMenuItem.Create(pane, ipk.default.desc.toUpperCase(), ipk.default.desc2.toUpperCase(), &onCheatObjectSpawnSelectedCB);
381     it.tagClass = ipk;
382   }
384   //optionsPaneOfs.x = 100;
385   //optionsPaneOfs.y = 50;
387   return pane;
391 // ////////////////////////////////////////////////////////////////////////// //
392 transient array!(class!/*ItemPickup*/MapItem) cheatPickupList;
395 final void fillCheatPickupList () {
396   cheatPickupList.length = 0;
397   cheatPickupList[$] = ItemPickupBombBag;
398   cheatPickupList[$] = ItemPickupBombBox;
399   cheatPickupList[$] = ItemPickupPaste;
400   cheatPickupList[$] = ItemPickupRopePile;
401   cheatPickupList[$] = ItemPickupShellBox;
402   cheatPickupList[$] = ItemPickupAnkh;
403   cheatPickupList[$] = ItemPickupCape;
404   cheatPickupList[$] = ItemPickupJetpack;
405   cheatPickupList[$] = ItemPickupUdjatEye;
406   cheatPickupList[$] = ItemPickupCrown;
407   cheatPickupList[$] = ItemPickupKapala;
408   cheatPickupList[$] = ItemPickupParachute;
409   cheatPickupList[$] = ItemPickupCompass;
410   cheatPickupList[$] = ItemPickupSpectacles;
411   cheatPickupList[$] = ItemPickupGloves;
412   cheatPickupList[$] = ItemPickupMitt;
413   cheatPickupList[$] = ItemPickupJordans;
414   cheatPickupList[$] = ItemPickupSpringShoes;
415   cheatPickupList[$] = ItemPickupSpikeShoes;
416   cheatPickupList[$] = ItemPickupTeleporter;
420 final UIPane createCheatPickupsPane () {
421   if (!level.player) return none;
423   UIPane pane = SpawnObject(UIPane);
424   pane.id = 'Pickups';
425   pane.sprStore = sprStore;
427   pane.width = 320*3-64;
428   pane.height = 240*3-64;
430   foreach (auto ipk; cheatPickupList) {
431     auto it = UIMenuItem.Create(pane, ipk.default.desc.toUpperCase(), ipk.default.desc2.toUpperCase(), &onCheatObjectSpawnSelectedCB);
432     it.tagClass = ipk;
433   }
435   //optionsPaneOfs.x = 100;
436   //optionsPaneOfs.y = 50;
438   return pane;
442 // ////////////////////////////////////////////////////////////////////////// //
443 transient int instantGhost;
445 final UIPane createCheatFlagsPane () {
446   UIPane pane = SpawnObject(UIPane);
447   pane.id = 'CheatFlags';
448   pane.sprStore = sprStore;
450   pane.width = 320*3-64;
451   pane.height = 240*3-64;
453   instantGhost = 0;
455   UICheckBox.Create(pane, &global.hasUdjatEye, "UDJAT EYE", "UDJAT EYE");
456   UICheckBox.Create(pane, &global.hasAnkh, "ANKH", "ANKH");
457   UICheckBox.Create(pane, &global.hasCrown, "CROWN", "CROWN");
458   UICheckBox.Create(pane, &global.hasKapala, "KAPALA", "COLLECT BLOOD TO GET MORE LIVES!");
459   UICheckBox.Create(pane, &global.hasStickyBombs, "STICKY BOMBS", "YOUR BOMBS CAN STICK!");
460   //UICheckBox.Create(pane, &global.stickyBombsActive, "stickyBombsActive", "stickyBombsActive");
461   UICheckBox.Create(pane, &global.hasSpectacles, "SPECTACLES", "YOU CAN SEE WHAT WAS HIDDEN!");
462   UICheckBox.Create(pane, &global.hasCompass, "COMPASS", "COMPASS");
463   UICheckBox.Create(pane, &global.hasParachute, "PARACHUTE", "YOU WILL DEPLOY PARACHUTE ON LONG FALLS.");
464   UICheckBox.Create(pane, &global.hasSpringShoes, "SPRING SHOES", "YOU CAN JUMP HIGHER!");
465   UICheckBox.Create(pane, &global.hasSpikeShoes, "SPIKE SHOES", "YOUR HEAD-JUMPS DOES MORE DAMAGE!");
466   UICheckBox.Create(pane, &global.hasJordans, "JORDANS", "YOU CAN JUMP TO THE MOON!");
467   //UICheckBox.Create(pane, &global.hasNinjaSuit, "hasNinjaSuit", "hasNinjaSuit");
468   UICheckBox.Create(pane, &global.hasCape, "CAPE", "YOU CAN CONTROL YOUR FALLS!");
469   UICheckBox.Create(pane, &global.hasJetpack, "JETPACK", "FLY TO THE SKY!");
470   UICheckBox.Create(pane, &global.hasGloves, "GLOVES", "OH, THOSE GLOVES ARE STICKY!");
471   UICheckBox.Create(pane, &global.hasMitt, "MITT", "YAY, YOU'RE THE BEST CATCHER IN THE WORLD NOW!");
472   UICheckBox.Create(pane, &instantGhost, "INSTANT GHOST", "SUMMON GHOST");
474   optionsPaneOfs.x = 100;
475   optionsPaneOfs.y = 50;
477   return pane;
481 final UIPane createOptionsPane () {
482   UIPane pane = SpawnObject(UIPane);
483   pane.id = 'Options';
484   pane.sprStore = sprStore;
486   pane.width = 320*3-64;
487   pane.height = 240*3-64;
489   UICheckBox.Create(pane, &config.interpolateMovement, "INTERPOLATE MOVEMENT", "IF TURNED OFF, THE MOVEMENT WILL BE JERKY AND ANNOYING.");
490   //!UICheckBox.Create(pane, &config.optSeedComputer, "SHOW SEED COMPUTER", "SHOWS SEED COMPUTER IN TITLE ROOM. IT SHOULD PRODUCE REPEATEBLE ROOMS, BUT ACTUALLY IT IS OLD AND BROKEN, SO IT DOESN'T WORK AS EXPECTED.");
491   UICheckBox.Create(pane, &config.downToRun, "PRESS 'DOWN' TO RUN", "PLAYER CAN PRESS 'DOWN' KEY TO RUN.");
492   UICheckBox.Create(pane, &config.alwaysCenterPlayer, "ALWAYS KEEP PLAYER IN CENTER", "ALWAYS KEEP PLAYER IN THE CENTER OF THE SCREEN. IF THIS OPTION IS UNSET, PLAYER WILL BE ALLOWED TO MOVE SLIGHTLY BEFORE THE VIEWPORT STARTS FOLLOWING HIM (THIS IS HOW IT WAS DONE IN THE ORIGINAL GAME).");
493   UICheckBox.Create(pane, &config.skipIntro, "SKIP INTRO", "AUTOMATICALLY SKIPS THE INTRO SEQUENCE AND STARTS THE GAME AT THE TITLE SCREEN.");
495   auto mm = UIIntEnum.Create(pane, &config.transitionMusicMode, 0, 2, "TRANSITION MUSIC  : ", "THIS IS WHAT GAME SHOULD DO WITH MUSIC ON TRANSITION LEVELS.");
496   mm.names[$] = "SILENCE";
497   mm.names[$] = "RESTART";
498   mm.names[$] = "DON'T TOUCH";
500   mm = UIIntEnum.Create(pane, &config.nextLevelMusicMode, 1, 2, "NORMAL LEVEL MUSIC: ", "THIS IS WHAT GAME SHOULD DO WITH MUSIC ON NORMAL LEVELS.");
501   //mm.names[$] = "SILENCE";
502   mm.names[$] = "RESTART";
503   mm.names[$] = "DON'T TOUCH";
505   auto herotype = UIIntEnum.Create(pane, &config.heroType, 0, 2, "PLAY AS: ", "CHOOSE YOUR HERO!");
506   herotype.names[$] = "SPELUNKY GUY";
507   herotype.names[$] = "DAMSEL";
508   herotype.names[$] = "TUNNEL MAN";
510   //UICheckBox.Create(pane, &config.optImmTransition, "FASTER TRANSITIONS", "PRESSING ACTION SECOND TIME WILL IMMEDIATELY SKIP TRANSITION LEVEL.");
511   UICheckBox.Create(pane, &config.useDoorWithButton, "BUTTON TO USE DOOR", "WITH THIS OPTION ENABLED YOU WILL NEED TO PRESS THE 'PURCHASE' BUTTON INSTEAD OF 'UP' TO USE DOORS. RECOMMENDED FOR GAMEPAD USERS.");
512   // i won't implement this
513   UICheckBox.Create(pane, &config.scumSmallHud, "SMALLER HUD", "THE INFORMATION AT THE TOP OF THE SCREEN SHOWING YOUR HEARTS, BOMBS, ROPES AND MONEY WILL BE REDUCED IN SIZE.");
515   auto halpha = UIIntEnum.Create(pane, &config.hudTextAlpha, 0, 250, "HUD TEXT ALPHA :", "THE BIGGER THIS NUMBER, THE MORE TRANSPARENT YOUR MAIN HUD WILL BE.");
516   halpha.step = 10;
518   auto ialpha = UIIntEnum.Create(pane, &config.hudItemsAlpha, 0, 250, "HUD ITEMS ALPHA:", "THE BIGGER THIS NUMBER, THE MORE TRANSPARENT YOUR ITEMS HUD WILL BE.");
519   ialpha.step = 10;
521   UICheckBox.Create(pane, &config.scumMetric, "METRIC UNITS", "DEPTH WILL BE MEASURED IN METRES INSTEAD OF FEET.");
522   // this is buggy
523   //!UICheckBox.Create(pane, &config.useFrozenRegion, "FROZEN REGION", "OFF-SCREEN ENTITIES ARE PAUSED TO IMPROVE PERFORMANCE. LEAVE THIS ENABLED IF YOU DON'T KNOW WHAT IT IS. DO A WEB SEARCH FOR 'SPELUNKY FROZEN REGION' FOR A FULL EXPLANATION. THE YASM README FILE ALSO HAS INFO.");
524   UICheckBox.Create(pane, &config.scumUnlocked, "UNLOCK SHORTCUTS", "OPENS ALL DOORS IN THE SHORTCUT HOUSE AND HI-SCORES ROOM. DOES NOT AFFECT YOUR SCORES OR UNLOCK PROGRESS. DISABLE THIS AGAIN TO REVEAL WHAT YOU HAVE LEGITIMATELY UNLOCKED.");
525   UIIntEnum.Create(pane, &config.scumClimbSpeed, 1, 3, "CLIMB SPEED:", "ADJUST THE SPEED THAT YOU CLIMB LADDERS, ROPES AND VINES. 1 IS DEFAULT SPEED, 2 IS FAST, AND 3 IS FASTER.");
527   UICheckBox.Create(pane, &config.scumFlipHold, "HOLD ITEM ON FLIP", "ALLOWS YOU TO FLIP DOWN TO HANG FROM A LEDGE WITHOUT BEING FORCED TO DROP ITEMS THAT COULD BE HELD WITH ONE HAND. HEAVY ITEMS WILL ALWAYS BE DROPPED.");
528   UICheckBox.Create(pane, &config.weaponsOpenContainers, "MELEE CONTAINERS", "ALLOWS YOU TO OPEN CRATES AND CHESTS BY HITTING THEM WITH THE WHIP, MACHETE OR MATTOCK.");
529   UICheckBox.Create(pane, &config.nudge, "MELEE ITEMS", "ALLOWS HITTING LOOSE ITEMS WITH MELEE WEAPONS TO MOVE THEM SLIGHTLY. WITH THE RIGHT TIMING YOU CAN HIT FLYING ARROWS TO DEFEND YOURSELF!");
530   UICheckBox.Create(pane, &config.scumSpringShoesReduceFallDamage, "SPRING SHOES EFFECT", "WITH THIS OPTION ENABLED, THE SPRING SHOES WILL ALLOW YOU TO FALL FARTHER THAN NORMAL BEFORE YOU TAKE DAMAGE.");
532   auto whiptype = UIIntEnum.Create(pane, &config.scumWhipUpgrade, 0, 1, "WHIP TYPE:", "YOU CAN HAVE A NORMAL WHIP, OR A LONGER ONE.");
533   whiptype.names[$] = "NORMAL";
534   whiptype.names[$] = "LONG";
535   UICheckBox.Create(pane, &global.config.unarmed, "UNARMED", "WITH THIS OPTION ENABLED, YOU WILL HAVE NO WHIP.");
537   UICheckBox.Create(pane, &config.optSGAmmo, "SHOTGUN NEEDS AMMO", "SHOTGUNS WILL REQUIRE SHELLS TO SHOOT. NEW SHOTGUN HAS 7 SHELLS. YOU CAN ALSO FOUND SHELLS IN JARS, CRATES AND CHESTS.");
538   UICheckBox.Create(pane, &config.optThrowEmptyShotgun, "THROW EMPTY SHOTGUN", "PRESSING ACTION WHEN SHOTGUN IS EMPTY WILL THROW IT.");
539   UICheckBox.Create(pane, &config.enemyBreakWeb, "ENEMIES BREAK WEBS", "ALLOWS MOST ENEMIES TO BREAK FREE FROM SPIDER WEBS AFTER A PERIOD OF TIME. SNAKES AND BATS ARE TOO WEAK TO ESCAPE.");
540   UICheckBox.Create(pane, &config.toggleRunAnywhere, "EASY WALK/RUN SWITCH", "ALLOWS PLAYER TO CONTROL SPEED IN MID-AIR WITH THE RUN KEY LIKE SPELUNKY HD, INSTEAD OF KEEPING THE SAME AIR SPEED UNTIL TOUCHING THE GROUND AGAIN.");
541   UICheckBox.Create(pane, &config.naturalSwim, "IMPROVED SWIMMING", "HOLD DOWN TO SINK FASTER, HOLD UP TO SINK SLOWER."); // Spelunky Natural swim mechanics
543   auto plrlit = UIIntEnum.Create(pane, &config.scumPlayerLit, 0, 2, "PLAYER LIT:", "LIT PLAYER IN DARKNESS WHEN...");
544   plrlit.names[$] = "NEVER";
545   plrlit.names[$] = "FORCED DARKNESS";
546   plrlit.names[$] = "ALWAYS";
548   auto rdark = UIIntEnum.Create(pane, &config.scumDarkness, 0, 2, "DARK :", "THE CHANCE OF GETTING A DARK LEVEL. THE BLACK MARKET AND FINAL BOSS LEVELS WILL BE LIT EVEN IF THIS OPTION IS SET TO 'ALWAYS'.");
549   rdark.names[$] = "NEVER";
550   rdark.names[$] = "DEFAULT";
551   rdark.names[$] = "ALWAYS";
553   auto rghost = UIIntEnum.Create(pane, &config.scumGhost, -30, 960, "GHOST:", "HOW LONG UNTIL THE 'A CHILL RUNS DOWN YOUR SPINE!' WARNING APPEARS. 30 SECONDS AFTER THAT, THE GHOST APPEARS. DEFAULT TIME IS 2 MINUTES. 'INSTANT' WILL SUMMON THE GHOST AT LEVEL START WITHOUT THE 30 SECOND DELAY.");
554   rghost.step = 30;
555   rghost.getNameCB = delegate string (int val) {
556     if (val < 0) return "INSTANT";
557     if (val == 0) return "NEVER";
558     if (val < 120) return va("%d SEC", val);
559     if (val%60 == 0) return va("%d MIN", val/60);
560     if (val%60 == 30) return va("%d.5 MIN", val/60);
561     return va("%d MIN, %d SEC", val/60, val%60);
562   };
564   UICheckBox.Create(pane, &config.ghostRandom, "RANDOM GHOST DELAY", "THIS OPTION WILL RANDOMIZE THE DELAY UNTIL THE GHOST APPEARS AFTER THE TIME LIMIT ABOVE IS REACHED INSTEAD OF USING THE DEFAULT 30 SECONDS. CHANGES EACH LEVEL AND VARIES WITH THE TIME LIMIT YOU SET.");
565   UICheckBox.Create(pane, &config.ghostShowTime, "SHOW GHOST TIME", "TURN THIS OPTION ON TO SEE HOW MUCH TIME IS LEFT UNTIL THE GHOST WILL APPEAR.");
566   UIIntEnum.Create(pane, &config.enemyMult, 1, 10, "ENEMIES:", "MULTIPLIES THE AMOUNT OF ENEMIES THAT SPAWN IN LEVELS. 1 IS NORMAL. THE SAME SETTING WILL AFFECT NORMAL AND BIZARRE MODES DIFFERENTLY.");
567   UIIntEnum.Create(pane, &config.trapMult, 1, 10,  "TRAPS  :", "MULTIPLIES THE AMOUNT OF TRAPS THAT SPAWN IN LEVELS. 1 IS NORMAL. THE SAME SETTING WILL AFFECT NORMAL AND BIZARRE MODES DIFFERENTLY.");
568   UICheckBox.Create(pane, &config.woodSpikes, "WOOD SPIKES", "REPLACES METAL SPIKES WITH WOODEN ONES THAT ALLOW YOU TO SAFELY DROP FROM ONE TILE ABOVE, AS IN AN EARLY VERSION OF THE GAME. DOES NOT AFFECT CUSTOM LEVELS.");
569   UICheckBox.Create(pane, &config.optSpikeVariations, "RANDOM SPIKES", "GENERATE SPIKES OF RANDOM TYPE (DEFAULT TYPE HAS GREATER PROBABILITY, THOUGH).");
570   UICheckBox.Create(pane, &config.boulderChaos, "BOULDER CHAOS", "BOULDERS WILL ROLL FASTER, BOUNCE A BIT HIGHER, AND KEEP THEIR MOMENTUM LONGER.");
571   UIIntEnum.Create(pane, &config.scumFallDamage, 1, 10, "FALL DAMAGE: ", "ADJUST THE MULTIPLIER FOR THE AMOUNT OF DAMAGE YOU TAKE FROM LONG FALLS. 1 IS DEFAULT, 2 IS DOUBLE DAMAGE, ETC.");
572   UICheckBox.Create(pane, &config.scumBallAndChain, "BALL AND CHAIN", "PLAYER WILL ALWAYS BE WEARING THE BALL AND CHAIN. YOU CAN GAIN OR LOSE FAVOR WITH KALI AS NORMAL, BUT THE BALL AND CHAIN WILL REMAIN. FOR THOSE THAT WANT AN EXTRA CHALLENGE.");
573   UICheckBox.Create(pane, &config.startWithKapala, "START WITH KAPALA", "PLAYER WILL ALWAYS START WITH KAPALA. THIS IS USEFUL TO PERFORM 'KAPALA CHALLENGES'.");
574   UICheckBox.Create(pane, &config.optIdolForEachLevelType, "IDOL IN EACH LEVEL TYPE", "GENERATE IDOL IN EACH LEVEL TYPE.");
575   UICheckBox.Create(pane, &config.optEnemyVariations, "ENEMY VARIATIONS", "ADD SOME ENEMY VARIATIONS IN MINES AND JUNGLE WHEN YOU DIED ENOUGH TIMES.");
577   auto rstl = UIIntEnum.Create(pane, &config.optRoomStyle, -1, 1, "ROOM STYLE:", "WHAT KIND OF ROOMS LEVEL GENERATOR SHOULD USE.");
578   rstl.names[$] = "RANDOM";
579   rstl.names[$] = "NORMAL";
580   rstl.names[$] = "BIZARRE";
582   UIIntEnum.Create(pane, &config.scumStartLife,  1, 42, "STARTING LIVES:", "STARTING NUMBER OF LIVES FOR SPELUNKER.");
583   UIIntEnum.Create(pane, &config.scumStartBombs, 1, 42, "STARTING BOMBS:", "STARTING NUMBER OF BOMBS FOR SPELUNKER.");
584   UIIntEnum.Create(pane, &config.scumStartRope,  1, 42, "STARTING ROPES:", "STARTING NUMBER OF ROPES FOR SPELUNKER.");
586   UICheckBox.Create(pane, &config.optDoubleKiss, "UNHURT DAMSEL KISSES TWICE", "IF YOU WILL BRING UNHURT DAMSEL TO THE EXIT WITHOUT DROPPING HER, SHE WILL KISS YOU TWICE.");
587   UICheckBox.Create(pane, &config.optShopkeeperIdiots, "SHOPKEEPERS ARE IDIOTS", "DO YOU WANT SHOPKEEPERS TO BE A BUNCH OF MORONS, IGNORANT AND UNABLE TO NOTICE ARMED BOMBS?");
589   //auto swstereo = UICheckBox.Create(pane, &config.swapStereo, "SWAP STEREO", "SWAP STEREO CHANNELS.");
590   /*
591   swstereo.onValueChanged = delegate void (int newval) {
592     SoundSystem.SwapStereo = newval;
593   };
594   */
596   auto rmusonoff = UICheckBox.Create(pane, &config.musicEnabled, "MUSIC", "PLAY OR DON'T PLAY MUSIC.");
597   rmusonoff.onValueChanged = delegate void (int newval) {
598     global.restartMusic();
599   };
601   UICheckBox.Create(pane, &config.soundEnabled, "SOUND", "PLAY OR DON'T PLAY SOUND.");
603   auto rvol = UIIntEnum.Create(pane, &config.musicVol, 0, GameConfig::MaxVolume, "MUSIC VOLUME:", "SET MUSIC VOLUME.");
604   rvol.onValueChanged = delegate void (int newval) { global.fixVolumes(); };
606   rvol = UIIntEnum.Create(pane, &config.soundVol, 0, GameConfig::MaxVolume, "SOUND VOLUME:", "SET SOUND VOLUME.");
607   rvol.onValueChanged = delegate void (int newval) { global.fixVolumes(); };
609   saveOptionsDG = delegate void () {
610     writeln("saving options");
611     saveGameOptions();
612   };
613   optionsPaneOfs.x = 42;
614   optionsPaneOfs.y = 0;
616   return pane;
620 final void createBindingsControl (UIPane pane, int keyidx) {
621   string kname, khelp;
622   switch (keyidx) {
623     case GameConfig::Key.Left: kname = "LEFT"; khelp = "MOVE SPELUNKER TO THE LEFT"; break;
624     case GameConfig::Key.Right: kname = "RIGHT"; khelp = "MOVE SPELUNKER TO THE RIGHT"; break;
625     case GameConfig::Key.Up: kname = "UP"; khelp = "MOVE SPELUNKER UP, OR LOOK UP"; break;
626     case GameConfig::Key.Down: kname = "DOWN"; khelp = "MOVE SPELUNKER DOWN, OR LOOK DOWN"; break;
627     case GameConfig::Key.Jump: kname = "JUMP"; khelp = "MAKE SPELUNKER JUMP"; break;
628     case GameConfig::Key.Run: kname = "RUN"; khelp = "MAKE SPELUNKER RUN"; break;
629     case GameConfig::Key.Attack: kname = "ATTACK"; khelp = "USE CURRENT ITEM, OR PERFORM AN ATTACK WITH THE CURRENT WEAPON"; break;
630     case GameConfig::Key.Switch: kname = "SWITCH"; khelp = "SWITCH BETWEEN ROPE/BOMB/ITEM"; break;
631     case GameConfig::Key.Pay: kname = "PAY"; khelp = "PAY SHOPKEEPER"; break;
632     case GameConfig::Key.Bomb: kname = "BOMB"; khelp = "DROP AN ARMED BOMB"; break;
633     case GameConfig::Key.Rope: kname = "ROPE"; khelp = "THROW A ROPE"; break;
634     default: return;
635   }
636   int arridx = GameConfig.getKeyIndex(keyidx);
637   UIKeyBinding.Create(pane, &global.config.keybinds[arridx+0], &global.config.keybinds[arridx+1], kname, khelp);
641 final UIPane createBindingsPane () {
642   UIPane pane = SpawnObject(UIPane);
643   pane.id = 'KeyBindings';
644   pane.sprStore = sprStore;
646   pane.width = 320*3-64;
647   pane.height = 240*3-64;
649   createBindingsControl(pane, GameConfig::Key.Left);
650   createBindingsControl(pane, GameConfig::Key.Right);
651   createBindingsControl(pane, GameConfig::Key.Up);
652   createBindingsControl(pane, GameConfig::Key.Down);
653   createBindingsControl(pane, GameConfig::Key.Jump);
654   createBindingsControl(pane, GameConfig::Key.Run);
655   createBindingsControl(pane, GameConfig::Key.Attack);
656   createBindingsControl(pane, GameConfig::Key.Switch);
657   createBindingsControl(pane, GameConfig::Key.Pay);
658   createBindingsControl(pane, GameConfig::Key.Bomb);
659   createBindingsControl(pane, GameConfig::Key.Rope);
661   saveOptionsDG = delegate void () {
662     writeln("saving keys");
663     saveKeyboardBindings();
664   };
665   optionsPaneOfs.x = 120;
666   optionsPaneOfs.y = 140;
668   return pane;
672 // ////////////////////////////////////////////////////////////////////////// //
673 void clearGameMovement () {
674   debugMovement = SpawnObject(DebugSessionMovement);
675   debugMovement.playconfig = SpawnObject(GameConfig);
676   debugMovement.playconfig.copyGameplayConfigFrom(config);
677   debugMovement.resetReplay();
681 void saveGameMovement (string fname) {
682   if (debugMovement) appSaveOptions(debugMovement, fname);
683   saveMovementLastTime = GetTickCount();
687 void loadGameMovement (string fname) {
688   delete debugMovement;
689   debugMovement = appLoadOptions(DebugSessionMovement, fname);
690   debugMovement.resetReplay();
691   if (debugMovement) {
692     delete origStats;
693     origStats = level.stats;
694     origStats.global = none;
695     level.stats = SpawnObject(GameStats);
696     level.stats.global = global;
697     delete origConfig;
698     origConfig = config;
699     config = debugMovement.playconfig;
700     global.config = config;
701     origRoomSeed = global.globalRoomSeed;
702     origOtherSeed = global.globalOtherSeed;
703     writeln(va("saving seeds: (0x%x, 0x%x)", origRoomSeed, origOtherSeed));
704   }
708 void stopReplaying () {
709   if (debugMovement) {
710     writeln(va("restoring seeds: (0x%x, 0x%x)", origRoomSeed, origOtherSeed));
711     global.globalRoomSeed = origRoomSeed;
712     global.globalOtherSeed = origOtherSeed;
713   }
714   delete debugMovement;
715   saveGameSession = false;
716   replayGameSession = false;
717   doGameSavingPlaying = Replay.None;
718   if (origStats) {
719     delete level.stats;
720     origStats.global = global;
721     level.stats = origStats;
722     origStats = none;
723   }
724   if (origConfig) {
725     delete config;
726     config = origConfig;
727     global.config = origConfig;
728     origConfig = none;
729   }
733 // ////////////////////////////////////////////////////////////////////////// //
734 final bool saveGame (string gmname) {
735   return appSaveOptions(level, gmname);
739 final bool loadGame (string gmname) {
740   auto olddel = ImmediateDelete;
741   ImmediateDelete = false;
742   bool res = false;
743   auto stats = level.stats;
744   level.stats = none;
746   auto lvl = appLoadOptions(GameLevel, gmname);
747   if (lvl) {
748     //lvl.global.config = config;
749     delete level;
750     delete global;
752     level = lvl;
753     global = level.global;
754     global.config = config;
756     level.sprStore = sprStore;
757     level.bgtileStore = bgtileStore;
760     level.onBeforeFrame = &beforeNewFrame;
761     level.onAfterFrame = &afterNewFrame;
762     level.onInterFrame = &interFrame;
764     level.viewWidth = Video.screenWidth;
765     level.viewHeight = Video.screenHeight;
767     level.onLoaded();
768     level.centerViewAtPlayer();
769     teleportCameraAt(level.viewStart);
771     recalcCameraCoords(0);
773     res = true;
774   }
775   level.stats = stats;
776   level.stats.global = level.global;
778   ImmediateDelete = olddel;
779   CollectGarbage(true); // destroy delayed objects too
780   return res;
784 // ////////////////////////////////////////////////////////////////////////// //
785 float lastThinkerTime;
786 int replaySkipFrame = 0;
789 final void onTimePasses () {
790   float curTime = GetTickCount();
791   if (lastThinkerTime > 0) {
792     if (curTime < lastThinkerTime) {
793       writeln("something is VERY wrong with timers! %f %f", curTime, lastThinkerTime);
794       lastThinkerTime = curTime;
795       return;
796     }
797     if (replayFastForward && replaySkipFrame) {
798       level.accumTime = 0;
799       lastThinkerTime = curTime-GameLevel::FrameTime*replayFastForwardSpeed;
800       replaySkipFrame = 0;
801     }
802     level.processThinkers(curTime-lastThinkerTime);
803   }
804   lastThinkerTime = curTime;
808 // ////////////////////////////////////////////////////////////////////////// //
809 private float currFrameDelta; // so level renderer can properly interpolate the player
810 private GameLevel::IVec2D camPrev, camCurr;
811 private GameLevel::IVec2D camShake;
812 private GameLevel::IVec2D viewCameraPos;
815 final void teleportCameraAt (const ref GameLevel::IVec2D pos) {
816   camPrev.x = pos.x;
817   camPrev.y = pos.y;
818   camCurr.x = pos.x;
819   camCurr.y = pos.y;
820   viewCameraPos.x = pos.x;
821   viewCameraPos.y = pos.y;
822   camShake.x = 0;
823   camShake.y = 0;
827 // call `recalcCameraCoords()` to get real camera coords after this
828 final void setNewCameraPos (const ref GameLevel::IVec2D pos) {
829   // check if camera is moved too far, and teleport it
830   if (abs(camCurr.x-pos.x)/global.scale >= 16*4 ||
831       abs(camCurr.y-pos.y)/global.scale >= 16*4)
832   {
833     teleportCameraAt(pos);
834   } else {
835     camPrev.x = camCurr.x;
836     camPrev.y = camCurr.y;
837     camCurr.x = pos.x;
838     camCurr.y = pos.y;
839   }
840   camShake.x = level.shakeDir.x*global.scale;
841   camShake.y = level.shakeDir.y*global.scale;
845 final void recalcCameraCoords (float frameDelta) {
846   currFrameDelta = frameDelta;
847   viewCameraPos.x = round(camPrev.x+(camCurr.x-camPrev.x)*frameDelta);
848   viewCameraPos.y = round(camPrev.y+(camCurr.y-camPrev.y)*frameDelta);
850   // update sound listener position (it is either at player position, or in viewport center)
851   TVec lv;
852   if (freeRide) {
853     lv = vector(
854       (viewCameraPos.x+level.viewWidth/2.0)/global.scale,
855       (viewCameraPos.y+level.viewHeight/2.0)/global.scale
856     );
857   } else {
858     viewCameraPos.x += camShake.x;
859     viewCameraPos.y += camShake.y;
860     lv = vector(float(level.player.xCenter), float(level.player.yCenter));
861   }
862   SoundSystem.ListenerOrigin = lv;
863   SoundSystem.UpdateSounds();
867 GameLevel::SavedKeyState savedKeyState;
869 final void pauseGame () {
870   if (!level.gamePaused) {
871     if (doGameSavingPlaying != Replay.None) level.keysSaveState(savedKeyState);
872     level.gamePaused = true;
873   }
877 final void unpauseGame () {
878   if (level.gamePaused) {
879     if (doGameSavingPlaying != Replay.None) level.keysRestoreState(savedKeyState);
880     level.gamePaused = false;
881     showHelp = false;
882     //lastThinkerTime = 0;
883   }
887 final void beforeNewFrame (bool frameSkip) {
888   if (freeRide) {
889     level.disablePlayerThink = true;
891     int delta = 2;
892     if (level.isKeyDown(GameConfig::Key.Attack)) delta *= 2;
893     if (level.isKeyDown(GameConfig::Key.Jump)) delta *= 4;
894     if (level.isKeyDown(GameConfig::Key.Run)) delta /= 2;
896     if (level.isKeyDown(GameConfig::Key.Left)) level.viewStart.x -= delta;
897     if (level.isKeyDown(GameConfig::Key.Right)) level.viewStart.x += delta;
898     if (level.isKeyDown(GameConfig::Key.Up)) level.viewStart.y -= delta;
899     if (level.isKeyDown(GameConfig::Key.Down)) level.viewStart.y += delta;
900   } else {
901     level.disablePlayerThink = false;
902   }
904   /*
905   if (level.isKeyDown(PlayerPawn::KeyLeft)) level.player.fltx -= delta;
906   if (level.isKeyDown(PlayerPawn::KeyRight)) level.player.fltx += delta;
907   if (level.isKeyDown(PlayerPawn::KeyUp)) level.player.flty -= delta;
908   if (level.isKeyDown(PlayerPawn::KeyDown)) level.player.flty += delta;
909   */
911   if (!level.gamePaused) {
912     // save seeds for afterframe processing
913     /*
914     if (doGameSavingPlaying == Replay.Saving && debugMovement) {
915       debugMovement.otherSeed = global.globalOtherSeed;
916       debugMovement.roomSeed = global.globalRoomSeed;
917     }
918     */
920     if (doGameSavingPlaying == Replay.Replaying && !debugMovement) stopReplaying();
922 #ifdef BIGGER_REPLAY_DATA
923     if (doGameSavingPlaying == Replay.Saving && debugMovement) {
924       debugMovement.keypresses.length += 1;
925       level.keysSaveState(debugMovement.keypresses[$-1]);
926       debugMovement.keypresses[$-1].otherSeed = global.globalOtherSeed;
927       debugMovement.keypresses[$-1].roomSeed = global.globalRoomSeed;
928     }
929 #endif
931     if (doGameSavingPlaying == Replay.Replaying && debugMovement) {
932 #ifdef BIGGER_REPLAY_DATA
933       if (debugMovement.keypos < debugMovement.keypresses.length) {
934         level.keysRestoreState(debugMovement.keypresses[debugMovement.keypos]);
935         global.globalOtherSeed = debugMovement.keypresses[debugMovement.keypos].otherSeed;
936         global.globalRoomSeed = debugMovement.keypresses[debugMovement.keypos].roomSeed;
937         ++debugMovement.keypos;
938       }
939 #else
940       for (;;) {
941         int kbidx;
942         bool down;
943         auto code = debugMovement.getKey(out kbidx, out down);
944         if (code == DebugSessionMovement::END_OF_RECORD) {
945           // do this in main loop, so we can view totals
946           //stopReplaying();
947           break;
948         }
949         if (code == DebugSessionMovement::END_OF_FRAME) {
950           break;
951         }
952         if (code != DebugSessionMovement::NORMAL) FatalError("UNKNOWN REPLAY CODE");
953         level.onKey(1<<(kbidx/GameConfig::MaxActionBinds), down);
954       }
955 #endif
956     }
957   }
961 final void afterNewFrame (bool frameSkip) {
962   if (!replayFastForward) replaySkipFrame = 0;
964   if (level.gamePaused) return;
966   if (!level.gamePaused) {
967     if (doGameSavingPlaying != Replay.None) {
968       if (doGameSavingPlaying == Replay.Saving) {
969         replayFastForward = false; // just in case
970 #ifndef BIGGER_REPLAY_DATA
971         debugMovement.addEndOfFrame();
972 #endif
973         auto stt = GetTickCount();
974         if (stt-saveMovementLastTime >= 20) saveGameMovement(dbgSessionMovementFileName);
975       } else if (doGameSavingPlaying == Replay.Replaying) {
976         if (!frameSkip && replayFastForward && replaySkipFrame == 0) {
977           replaySkipFrame = 1;
978         }
979       }
980     }
981   }
983   //SoundSystem.ListenerOrigin = vector(level.player.fltx, level.player.flty);
984   //SoundSystem.UpdateSounds();
986   if (!freeRide) level.fixCamera();
987   setNewCameraPos(level.viewStart);
988   /*
989   prevCameraX = currCameraX;
990   prevCameraY = currCameraY;
991   currCameraX = level.cameraX;
992   currCameraY = level.cameraY;
993   // disable camera interpolation if the screen is shaking
994   if (level.shakeX|level.shakeY) {
995     prevCameraX = currCameraX;
996     prevCameraY = currCameraY;
997     return;
998   }
999   // disable camera interpolation if it moves too far away
1000   if (fabs(prevCameraX-currCameraX) > 64) prevCameraX = currCameraX;
1001   if (fabs(prevCameraY-currCameraY) > 64) prevCameraY = currCameraY;
1002   */
1003   if (switchInterpolator) {
1004     switchInterpolator = false;
1005     config.interpolateMovement = !config.interpolateMovement;
1006   }
1007   recalcCameraCoords(config.interpolateMovement ? 0.0 : 1.0); // recalc camera coords
1009   if (pauseRequested) {
1010     pauseRequested = false;
1011     pauseGame();
1012     if (!showHelp) showHelp = true;
1013     return;
1014   }
1018 final void interFrame (float frameDelta) {
1019   if (!config.interpolateMovement) return;
1020   recalcCameraCoords(frameDelta);
1024 // ////////////////////////////////////////////////////////////////////////// //
1025 #ifdef MASK_TEST
1026 final void setColorByIdx (bool isset, int col) {
1027   if (col == -666) {
1028     // missed collision: red
1029     Video.color = (isset ? 0x3f_ff_00_00 : 0xcf_ff_00_00);
1030   } else if (col == -999) {
1031     // superfluous collision: blue
1032     Video.color = (isset ? 0x3f_00_00_ff : 0xcf_00_00_ff);
1033   } else if (col <= 0) {
1034     // no collision: yellow
1035     Video.color = (isset ? 0x3f_ff_ff_00 : 0xcf_ff_ff_00);
1036   } else if (col > 0) {
1037     // collision: green
1038     Video.color = (isset ? 0x3f_00_ff_00 : 0xcf_00_ff_00);
1039   }
1043 final void drawMaskSimple (SpriteFrame frm, int xofs, int yofs) {
1044   if (!frm) return;
1045   CollisionMask cm = CollisionMask.Create(frm, false);
1046   if (!cm) return;
1047   int scale = global.config.scale;
1048   int bx0, by0, bx1, by1;
1049   frm.getBBox(out bx0, out by0, out bx1, out by1, false);
1050   Video.color = 0x7f_00_00_ff;
1051   Video.fillRect(xofs+bx0*scale, yofs+by0*scale, (bx1-bx0+1)*scale, (by1-by0+1)*scale);
1052   if (!cm.isEmptyMask) {
1053     //writeln(cm.mask.length, "; ", cm.width, "x", cm.height, "; (", cm.x0, ",", cm.y0, ")-(", cm.x1, ",", cm.y1, ")");
1054     foreach (int iy; 0..cm.height) {
1055       foreach (int ix; 0..cm.width) {
1056         int v = cm.mask[ix, iy];
1057         foreach (int dx; 0..32) {
1058           int xx = ix*32+dx;
1059           if (v < 0) {
1060             Video.color = 0x3f_00_ff_00;
1061             Video.fillRect(xofs+xx*scale, yofs+iy*scale, scale, scale);
1062           }
1063           v <<= 1;
1064         }
1065       }
1066     }
1067   } else {
1068     // bounding box
1069     /+
1070     foreach (int iy; 0..frm.tex.height) {
1071       foreach (int ix; 0..(frm.tex.width+31)/31) {
1072         foreach (int dx; 0..32) {
1073           int xx = ix*32+dx;
1074           //if (xx >= frm.bx && xx < frm.bx+frm.bw && iy >= frm.by && iy < frm.by+frm.bh) {
1075           if (xx >= x0 && xx <= x1 && iy >= y0 && iy <= y1) {
1076             setColorByIdx(true, col);
1077             if (col <= 0) Video.color = 0xaf_ff_ff_00;
1078           } else {
1079             Video.color = 0xaf_00_ff_00;
1080           }
1081           Video.fillRect(sx+xx*scale, sy+iy*scale, scale, scale);
1082         }
1083       }
1084     }
1085     +/
1086     /*
1087     if (frm.bw > 0 && frm.bh > 0) {
1088       setColorByIdx(true, col);
1089       Video.fillRect(x0+frm.bx*scale, y0+frm.by*scale, frm.bw*scale, frm.bh*scale);
1090       Video.color = 0xff_00_00;
1091       Video.drawRect(x0+frm.bx*scale, y0+frm.by*scale, frm.bw*scale, frm.bh*scale);
1092     }
1093     */
1094   }
1095   delete cm;
1097 #endif
1100 // ////////////////////////////////////////////////////////////////////////// //
1101 transient int drawStats;
1102 transient array!int statsTopItem;
1105 final bool totalsNameCmpCB (ref GameStats::TotalItem a, ref GameStats::TotalItem b) {
1106   auto sa = string(a.objName);
1107   auto sb = string(b.objName);
1108   return (sa < sb);
1112 final int getStatsTopItem () {
1113   return max(0, (drawStats >= 0 && drawStats < statsTopItem.length ? statsTopItem[drawStats] : 0));
1117 final void setStatsTopItem (int val) {
1118   if (drawStats <= statsTopItem.length) statsTopItem.length = drawStats+1;
1119   statsTopItem[drawStats] = val;
1123 final void resetStatsTopItem () {
1124   setStatsTopItem(0);
1128 void statsDrawGetStartPosLoadFont (out int currX, out int currY) {
1129   sprStore.loadFont('sFontSmall');
1130   currX = 64;
1131   currY = 32;
1135 final int calcStatsVisItems () {
1136   int scale = 3;
1137   int currX, currY;
1138   statsDrawGetStartPosLoadFont(currX, currY);
1139   int endY = Video.screenHeight-(currY*2);
1140   return max(1, endY/sprStore.getFontHeight(scale));
1144 int getStatsItemCount () {
1145   switch (drawStats) {
1146     case 2: return level.stats.totalKills.length;
1147     case 3: return level.stats.totalDeaths.length;
1148     case 4: return level.stats.totalCollected.length;
1149   }
1150   return -1;
1154 final void statsMoveUp () {
1155   int count = getStatsItemCount();
1156   if (count < 0) return;
1157   int visItems = calcStatsVisItems();
1158   if (count <= visItems) { resetStatsTopItem(); return; }
1159   int top = getStatsTopItem();
1160   if (!top) return;
1161   setStatsTopItem(top-1);
1165 final void statsMoveDown () {
1166   int count = getStatsItemCount();
1167   if (count < 0) return;
1168   int visItems = calcStatsVisItems();
1169   if (count <= visItems) { resetStatsTopItem(); return; }
1170   int top = getStatsTopItem();
1171   //writeln("top=", top, "; count=", count, "; visItems=", visItems, "; maxtop=", count-visItems+1);
1172   top = clamp(top+1, 0, count-visItems);
1173   setStatsTopItem(top);
1177 void drawTotalsList (string pfx, ref array!(GameStats::TotalItem) arr) {
1178   GameStats.sortTotalsList(arr, &totalsNameCmpCB);
1179   int scale = 3;
1181   int currX, currY;
1182   statsDrawGetStartPosLoadFont(currX, currY);
1184   int endY = Video.screenHeight-(currY*2);
1185   int visItems = calcStatsVisItems();
1187   if (arr.length <= visItems) resetStatsTopItem();
1189   int topItem = getStatsTopItem();
1191   // "upscroll" mark
1192   if (topItem > 0) {
1193     Video.color = 0x3f_ff_ff_00;
1194     auto spr = sprStore['sPageUp'];
1195     spr.frames[0].tex.blitAt(currX-24, currY, scale);
1196   }
1198   // "downscroll" mark
1199   if (topItem+visItems < arr.length) {
1200     Video.color = 0x3f_ff_ff_00;
1201     auto spr = sprStore['sPageDown'];
1202     spr.frames[0].tex.blitAt(currX-24, endY/*-sprStore.getFontHeight(scale)*/, scale);
1203   }
1205   Video.color = 0xff_ff_00;
1206   int hiColor = 0x00_ff_00;
1207   int hiColor1 = 0xf_ff_ff;
1209   int it = topItem;
1210   while (it < arr.length && visItems-- > 0) {
1211     sprStore.renderTextWithHighlight(currX, currY, va("%s |%s| ~%d~ TIME%s", pfx, string(arr[it].objName).toUpperCase, arr[it].count, (arr[it].count != 1 ? "S" : "")), scale, hiColor, hiColor1);
1212     currY += sprStore.getFontHeight(scale);
1213     ++it;
1214   }
1218 void drawStatsScreen () {
1219   int deathCount, killCount, collectCount;
1221   switch (drawStats) {
1222     case 2: drawTotalsList("KILLED", level.stats.totalKills); return;
1223     case 3: drawTotalsList("DIED FROM", level.stats.totalDeaths); return;
1224     case 4: drawTotalsList("COLLECTED", level.stats.totalCollected); return;
1225   }
1226   if (drawStats > 1) {
1227     // turn off
1228     foreach (ref auto i; statsTopItem) i = 0;
1229     drawStats = 0;
1230     return;
1231   }
1233   foreach (ref auto ti; level.stats.totalDeaths) deathCount += ti.count;
1234   foreach (ref auto ti; level.stats.totalKills) killCount += ti.count;
1235   foreach (ref auto ti; level.stats.totalCollected) collectCount += ti.count;
1237   Video.color = 0xff_ff_00;
1238   int hiColor = 0x00_ff_00;
1239   sprStore.loadFont('sFontSmall');
1241   int currX = 64;
1242   int currY = 96;
1243   int scale = 3;
1245   sprStore.renderTextWithHighlight(currX, currY, va("MAXIMUM MONEY YOU GOT IS ~%d~", level.stats.maxMoney), scale, hiColor);
1246   currY += sprStore.getFontHeight(scale);
1248   int gw = level.stats.gamesWon;
1249   sprStore.renderTextWithHighlight(currX, currY, va("YOU WON ~%d~ GAME%s", gw, (gw != 1 ? "S" : "")), scale, hiColor);
1250   currY += sprStore.getFontHeight(scale);
1252   sprStore.renderTextWithHighlight(currX, currY, va("YOU DIED ~%d~ TIMES", deathCount), scale, hiColor);
1253   currY += sprStore.getFontHeight(scale);
1255   sprStore.renderTextWithHighlight(currX, currY, va("YOU KILLED ~%d~ CREATURES", killCount), scale, hiColor);
1256   currY += sprStore.getFontHeight(scale);
1258   sprStore.renderTextWithHighlight(currX, currY, va("YOU COLLECTED ~%d~ TREASURE ITEMS", collectCount), scale, hiColor);
1259   currY += sprStore.getFontHeight(scale);
1261   sprStore.renderTextWithHighlight(currX, currY, va("YOU SAVED ~%d~ DAMSELS", level.stats.totalDamselsSaved), scale, hiColor);
1262   currY += sprStore.getFontHeight(scale);
1264   sprStore.renderTextWithHighlight(currX, currY, va("YOU STOLE ~%d~ IDOLS", level.stats.totalIdolsStolen), scale, hiColor);
1265   currY += sprStore.getFontHeight(scale);
1267   sprStore.renderTextWithHighlight(currX, currY, va("YOU SOLD ~%d~ IDOLS", level.stats.totalIdolsConverted), scale, hiColor);
1268   currY += sprStore.getFontHeight(scale);
1270   sprStore.renderTextWithHighlight(currX, currY, va("YOU STOLE ~%d~ CRYSTAL SKULLS", level.stats.totalCrystalIdolsStolen), scale, hiColor);
1271   currY += sprStore.getFontHeight(scale);
1273   sprStore.renderTextWithHighlight(currX, currY, va("YOU SOLD ~%d~ CRYSTAL SKULLS", level.stats.totalCrystalIdolsConverted), scale, hiColor);
1274   currY += sprStore.getFontHeight(scale);
1276   int gs = level.stats.totalGhostSummoned;
1277   sprStore.renderTextWithHighlight(currX, currY, va("YOU SUMMONED ~%d~ GHOST%s", gs, (gs != 1 ? "S" : "")), scale, hiColor);
1278   currY += sprStore.getFontHeight(scale);
1280   currY += sprStore.getFontHeight(scale);
1281   sprStore.renderTextWithHighlight(currX, currY, va("TOTAL PLAYING TIME: ~%s~", GameLevel.time2str(level.stats.playingTime)), scale, hiColor);
1282   currY += sprStore.getFontHeight(scale);
1286 void onDraw () {
1287   if (Video.frameTime == 0) {
1288     onTimePasses();
1289     Video.requestRefresh();
1290   }
1292   if (!level) return;
1293   Video.stencil = true; // you NEED this to be set! (stencil buffer is used for lighting)
1294   Video.clearScreen();
1295   Video.stencil = false;
1296   Video.color = 0xff_ff_ff;
1297   Video.textureFiltering = false;
1298   // don't touch framebuffer alpha
1299   Video.colorMask = Video::CMask.Colors;
1301   if (level.gamePaused) {
1302     //level.renderWithOfs(trunc(currCameraX), trunc(currCameraY), 1.0);
1303     level.renderWithOfs(viewCameraPos.x, viewCameraPos.y, currFrameDelta);
1304   } else {
1305 #ifndef DARK_TEST
1306     level.renderWithOfs(viewCameraPos.x, viewCameraPos.y, currFrameDelta);
1307 #else
1308     auto ltex = bgtileStore.lightTexture('ltx256', 128);
1309     auto ctex = bgtileStore.circleTexture('ctx256', 128);
1311     auto oblend = Video.blendMode;
1312     Video.blendMode = Video::BlendMode.Normal;
1313     //Video.blendMode = Video::Blend.Blend;
1314     //Video.blendMode = Video::Blend.Particle;
1315     Video.color = 0x00_ff_ff_ff;
1318     // stenciling (it works)
1319     Video.stencil = true;
1320     Video.stencilFunc(Video::StencilFunc.Always, 1);
1321     Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Replace);
1322     Video.alphaTestFunc = Video::AlphaFunc.Equal;
1323     Video.alphaTestVal = 1;
1324     ctex.tex.blitAt(10, 10);
1325     Video.alphaTestFunc = Video::AlphaFunc.Always;
1327     /*
1328     Video.stencil = false;
1329     Video.clearScreen();
1330     Video.stencil = true;
1331     */
1333     Video.stencilFunc(Video::StencilFunc.NotEqual, 1);
1334     Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Keep);
1336     level.renderWithOfs(viewCameraPos.x, viewCameraPos.y, currFrameDelta);
1338     /+
1339     Video.blendMode = Video::BlendMode.Normal;
1340     level.renderWithOfs(viewCameraPos.x, viewCameraPos.y, currFrameDelta);
1342     Video.blendMode = Video::BlendMode.Particle;
1343     //Video.blendMode = Video::Blend.Highlight;
1344     Video.alphaTestFunc = Video::AlphaFunc.Less;
1345     Video.alphaTestVal = 0.8;
1347     ltex.tex.blitAt(100, 100);
1348     ltex.tex.blitAt(150, 150);
1351     Video.stencil = false;
1352     Video.alphaTestFunc = Video::AlphaFunc.Always;
1353     Video.alphaTestVal = 1;
1354     +/
1356 //native final static void stencilOp (StencilOp sfail, StencilOp dpfail, optional StencilOp dppass);
1357 //native final static void stencilFunc (StencilFunc func, int refval, optional int mask);
1359     //!!ctex.tex.blitAt(10, 10);
1360     //ltex.tex.blitAt(60, 60);
1361     //ltex.tex.blitAt(260, 260);
1362     Video.blendMode = oblend;
1363     //level.renderWithOfs(cameraX, cameraY, currFrameDelta);
1364     /+ dark level
1365     auto oblend = Video.blendMode;
1366     Video.blendMode = Video::Blend.Filter;
1367     Video.color = 0x00_00_3f;
1368     Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
1369     Video.blendMode = oblend;
1370     +/
1371 #endif
1372   }
1374   /+
1375   {
1376     auto ltex = bgtileStore.lightTexture('ltx512', 512);
1378     // set screen alpha to min
1379     Video.colorMask = Video::CMask.Alpha;
1380     Video.blendMode = Video::BlendMode.None;
1381     Video.color = 0x7f_ff_ff_ff;
1382     Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
1383     //Video.colorMask = Video::CMask.All;
1385     // blend lights
1386     // also, stencil 'em, so we can filter dark areas
1387     Video.textureFiltering = true;
1388     Video.stencil = true;
1389     Video.stencilFunc(Video::StencilFunc.Always, 1);
1390     Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Replace);
1391     Video.alphaTestFunc = Video::AlphaFunc.Greater;
1392     Video.alphaTestVal = 0.05;
1393     Video.color = 0xff_ff_ff;
1394     Video.blendFunc = Video::BlendFunc.Max;
1395     Video.blendMode = Video::BlendMode.Blend; // anything except `Normal`
1396     Video.colorMask = Video::CMask.Alpha;
1397     ltex.tex.blitAt(Video.screenWidth/2-256, Video.screenHeight/2-256, 0.5);
1398     //ltex.tex.blitAt(120, 120, 0.5);
1399     Video.textureFiltering = false;
1401     // modify only lit parts
1402     Video.stencilFunc(Video::StencilFunc.Equal, 1);
1403     Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Keep);
1404     // multiply framebuffer colors by framebuffer alpha
1405     Video.color = 0xff_ff_ff; // it doesn't matter
1406     Video.blendFunc = Video::BlendFunc.Add;
1407     Video.blendMode = Video::BlendMode.DstMulDstAlpha;
1408     Video.colorMask = Video::CMask.Colors;
1409     Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
1411     // filter unlit parts
1412     Video.stencilFunc(Video::StencilFunc.NotEqual, 1);
1413     Video.stencilOp(Video::StencilOp.Keep, Video::StencilOp.Keep);
1414     Video.blendFunc = Video::BlendFunc.Add;
1415     Video.blendMode = Video::BlendMode.Filter;
1416     Video.colorMask = Video::CMask.Colors;
1417     Video.color = 0x00_00_18;
1418     Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
1420     // restore defaults
1421     Video.blendFunc = Video::BlendFunc.Add;
1422     Video.blendMode = Video::BlendMode.Normal;
1423     Video.colorMask = Video::CMask.All;
1424     Video.alphaTestFunc = Video::AlphaFunc.Always;
1425     Video.stencil = false;
1426   }
1427   +/
1429   switch (doGameSavingPlaying) {
1430     case Replay.Saving:
1431       Video.color = 0x7f_00_ff_00;
1432       sprStore.loadFont('sFont');
1433       sprStore.renderText(Video.screenWidth-sprStore.getTextWidth("S", 2)-2, 2, "S", 2);
1434       break;
1435     case Replay.Replaying:
1436       if (level.player && !level.player.dead) {
1437         Video.color = 0x7f_ff_00_00;
1438         sprStore.loadFont('sFont');
1439         sprStore.renderText(Video.screenWidth-sprStore.getTextWidth("R", 2)-2, 2, "R", 2);
1440         int th = sprStore.getFontHeight(2);
1441         if (replayFastForward) {
1442           sprStore.loadFont('sFontSmall');
1443           string sstr = va("x%d", replayFastForwardSpeed+1);
1444           sprStore.renderText(Video.screenWidth-sprStore.getTextWidth(sstr, 2)-2, 2+th, sstr, 2);
1445         }
1446       }
1447       break;
1448     default:
1449       if (saveGameSession) {
1450         Video.color = 0x7f_ff_7f_00;
1451         sprStore.loadFont('sFont');
1452         sprStore.renderText(Video.screenWidth-sprStore.getTextWidth("S", 2)-2, 2, "S", 2);
1453       }
1454       break;
1455   }
1458   if (level.player && level.player.dead && !showHelp) {
1459     // darken
1460     Video.color = 0x8f_00_00_00;
1461     Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
1462     // draw text
1463     if (drawStats) {
1464       drawStatsScreen();
1465     } else {
1466       if (true /*level.inWinCutscene == 0*/) {
1467         Video.color = 0xff_ff_ff;
1468         sprStore.loadFont('sFontSmall');
1469         string kmsg = va((level.stats.newMoneyRecord ? "NEW HIGH SCORE: |%d|\n" : "SCORE: |%d|\n")~
1470                          "\n"~
1471                          "PRESS $PAY TO RESTART GAME\n"~
1472                          "\n"~
1473                          "PRESS ~ESCAPE~ TO EXIT TO TITLE\n"~
1474                          "\n"~
1475                          "TOTAL PLAYING TIME: |%s|"~
1476                          "",
1477                          (level.levelKind == GameLevel::LevelKind.Stars ? level.starsKills :
1478                           level.levelKind == GameLevel::LevelKind.Sun ? level.sunScore :
1479                           level.levelKind == GameLevel::LevelKind.Moon ? level.moonScore :
1480                           level.stats.money),
1481                          GameLevel.time2str(level.stats.playingTime)
1482                         );
1483         kmsg = global.expandString(kmsg);
1484         sprStore.renderMultilineTextCentered(Video.screenWidth/2, int.min, kmsg, 3, 0x00_ff_00, 0x00_ff_ff);
1485       }
1486     }
1487   }
1489 #ifdef MASK_TEST
1490   {
1491     Video.color = 0xff_7f_00;
1492     sprStore.loadFont('sFontSmall');
1493     sprStore.renderText(8, Video.screenHeight-20, va("%s; FRAME:%d", (smask.precise ? "PRECISE" : "HITBOX"), maskFrame), 2);
1494     auto spf = smask.frames[maskFrame];
1495     sprStore.renderText(8, Video.screenHeight-20-16, va("OFS=(%d,%d); BB=(%d,%d)x(%d,%d); EMPTY:%s; PRECISE:%s",
1496       spf.xofs, spf.yofs,
1497       spf.bx, spf.by, spf.bw, spf.bh,
1498       (spf.maskEmpty ? "TAN" : "ONA"),
1499       (spf.precise ? "TAN" : "ONA")),
1500       2
1501     );
1502     //spf.tex.blitAt(maskSX*global.config.scale-viewCameraPos.x, maskSY*global.config.scale-viewCameraPos.y, global.config.scale);
1503     //writeln("pos=(", maskSX, ",", maskSY, ")");
1504     int scale = global.config.scale;
1505     int xofs = viewCameraPos.x, yofs = viewCameraPos.y;
1506     int mapX = xofs/scale+maskSX;
1507     int mapY = yofs/scale+maskSY;
1508     mapX -= spf.xofs;
1509     mapY -= spf.yofs;
1510     writeln("==== tiles ====");
1511     /*
1512     level.touchTilesWithMask(mapX, mapY, spf, delegate bool (MapTile t) {
1513       if (t.spectral || !t.isInstanceAlive) return false;
1514       Video.color = 0x7f_ff_00_00;
1515       Video.fillRect(t.x0*global.config.scale-viewCameraPos.x, t.y0*global.config.scale-viewCameraPos.y, t.width*global.config.scale, t.height*global.config.scale);
1516       auto tsf = t.getSpriteFrame();
1518       auto spf = smask.frames[maskFrame];
1519       int xofs = viewCameraPos.x, yofs = viewCameraPos.y;
1520       int mapX = xofs/global.config.scale+maskSX;
1521       int mapY = yofs/global.config.scale+maskSY;
1522       mapX -= spf.xofs;
1523       mapY -= spf.yofs;
1524       //bool hit = spf.pixelCheck(tsf, t.ix-mapX, t.iy-mapY);
1525       bool hit = tsf.pixelCheck(spf, mapX-t.ix, mapY-t.iy);
1526       writeln("  tile '", t.objName, "': precise=", tsf.precise, "; hit=", hit);
1527       return false;
1528     });
1529     */
1530     level.touchObjectsWithMask(mapX, mapY, spf, delegate bool (MapObject t) {
1531       Video.color = 0x7f_ff_00_00;
1532       Video.fillRect(t.x0*global.config.scale-viewCameraPos.x, t.y0*global.config.scale-viewCameraPos.y, t.width*global.config.scale, t.height*global.config.scale);
1533       return false;
1534     });
1535     //
1536     drawMaskSimple(spf, mapX*scale-xofs, mapY*scale-yofs);
1537     // mask
1538     Video.color = 0xaf_ff_ff_ff;
1539     spf.tex.blitAt(mapX*scale-xofs, mapY*scale-yofs, scale);
1540     Video.color = 0xff_ff_00;
1541     Video.drawRect((mapX+spf.bx)*scale-xofs, (mapY+spf.by)*scale-yofs, spf.bw*scale, spf.bh*scale);
1542     // player colbox
1543     {
1544       bool doMirrorSelf;
1545       int fx0, fy0, fx1, fy1;
1546       auto pfm = level.player.getSpriteFrame(out doMirrorSelf, out fx0, out fy0, out fx1, out fy1);
1547       Video.color = 0x7f_00_00_ff;
1548       Video.fillRect((level.player.ix+fx0)*scale-xofs, (level.player.iy+fy0)*scale-yofs, (fx1-fx0)*scale, (fy1-fy0)*scale);
1549     }
1550   }
1551 #endif
1553   if (showHelp) {
1554     Video.color = 0x8f_00_00_00;
1555     Video.fillRect(0, 0, Video.screenWidth, Video.screenHeight);
1556     if (optionsPane) {
1557       optionsPane.drawWithOfs(optionsPaneOfs.x+32, optionsPaneOfs.y+32);
1558     } else {
1559       if (drawStats) {
1560         drawStatsScreen();
1561       } else {
1562         Video.color = 0xff_ff_00;
1563         //if (showHelp > 1) Video.color = 0xaf_ff_ff_00;
1564         if (showHelp == 1) {
1565           sprStore.loadFont('sFontSmall');
1566           sprStore.renderTextWrapped(16, 16, (320-16)*2,
1567             "F1: show this help\n"~
1568             "O : options\n"~
1569             "K : redefine keys\n"~
1570             "I : toggle interpolaion\n"~
1571             "N : create some blood\n"~
1572             "R : generate a new level\n"~
1573             "F : toggle \"Frozen Area\"\n"~
1574             "X : resurrect player\n"~
1575             "Q : teleport to exit\n"~
1576             "D : teleport to damel\n"~
1577             "--------------\n"~
1578             "C : cheat flags menu\n"~
1579             "P : cheat pickup menu\n"~
1580             "E : cheat enemy menu\n"~
1581             "Enter: cheat items menu\n"~
1582             "\n"~
1583             "TAB: toggle 'freeroam' mode\n"~
1584             "",
1585             2);
1586         }
1587       }
1588     }
1589     //SoundSystem.UpdateSounds();
1590   }
1591   //sprStore.renderText(16, 16, "SPELUNKY!", 2);
1593   if (TigerEye) {
1594     Video.color = 0xaf_ff_ff_ff;
1595     texTigerEye.blitAt(Video.screenWidth-texTigerEye.width-2, Video.screenHeight-texTigerEye.height-2);
1596   }
1600 // ////////////////////////////////////////////////////////////////////////// //
1601 transient bool gameJustOver;
1602 transient bool waitingForPayRestart;
1605 final void onEvent (ref event_t evt) {
1606   if (evt.type == ev_closequery) { Video.requestQuit(); return; }
1608   if (evt.type == ev_winfocus) {
1609     if (level && !evt.focused) {
1610       escCount = 0;
1611       level.clearKeys();
1612     }
1613     return;
1614   }
1616   if (evt.type == ev_keyup && evt.keycode != K_ESCAPE) escCount = 0;
1618   if (evt.type == ev_keydown && evt.keycode == "1") { global.config.scale = 1; return; }
1619   if (evt.type == ev_keydown && evt.keycode == "2") { global.config.scale = 2; return; }
1620   if (evt.type == ev_keydown && evt.keycode == "3") { global.config.scale = 3; return; }
1621   if (evt.type == ev_keydown && evt.keycode == "4") { global.config.scale = 4; return; }
1623 #ifdef MASK_TEST
1624   if (evt.type == ev_mouse) {
1625     maskSX = evt.x/global.config.scale;
1626     maskSY = evt.y/global.config.scale;
1627     return;
1628   }
1629   if (evt.type == ev_keydown && evt.keycode == K_PADMINUS) {
1630     maskFrame = max(0, maskFrame-1);
1631     return;
1632   }
1633   if (evt.type == ev_keydown && evt.keycode == K_PADPLUS) {
1634     maskFrame = clamp(maskFrame+1, 0, smask.frames.length-1);
1635     return;
1636   }
1637 #endif
1639   if (showHelp) {
1640     escCount = 0;
1642     if (optionsPane) {
1643       if (optionsPane.closeMe || (evt.type == ev_keyup && evt.keycode == K_ESCAPE)) {
1644         saveCurrentPane();
1645         if (saveOptionsDG) saveOptionsDG();
1646         saveOptionsDG = none;
1647         delete optionsPane;
1648         SoundSystem.UpdateSounds(); // just in case
1649         if (global.hasSpectacles) level.pickedSpectacles();
1650         return;
1651       }
1652       optionsPane.onEvent(evt);
1653       return;
1654     }
1656     if (evt.type == ev_keyup && evt.keycode == K_ESCAPE) { unpauseGame(); return; }
1657     if (evt.type == ev_keydown) {
1658       switch (evt.keycode) {
1659         case K_F1: if (showHelp > 1) showHelp = 1; else unpauseGame(); return;
1660         case K_F10: Video.requestQuit(); return;
1661         case K_F12: showHelp = 3-showHelp; return;
1663         case K_UPARROW: case K_PAD8:
1664           if (drawStats) statsMoveUp();
1665           return;
1666         case K_DOWNARROW: case K_PAD2:
1667           if (drawStats) statsMoveDown();
1668           return;
1670         case K_F6: {
1671           // save level
1672           saveGame("level");
1673           unpauseGame();
1674           return;
1675         }
1677         case K_F9: {
1678           // load level
1679           loadGame("level");
1680           unpauseGame();
1681           return;
1682         }
1684         case K_F5:
1685           global.plife = 99;
1686           unpauseGame();
1687           return;
1689         case K_s:
1690           ++drawStats;
1691           return;
1693         case K_o: optionsPane = createOptionsPane(); restoreCurrentPane(); return;
1694         case K_k: optionsPane = createBindingsPane(); restoreCurrentPane(); return;
1695         case K_c: optionsPane = createCheatFlagsPane(); restoreCurrentPane(); return;
1696         case K_p: optionsPane = createCheatPickupsPane(); restoreCurrentPane(); return;
1697         case K_ENTER: optionsPane = createCheatItemsPane(); restoreCurrentPane(); return;
1698         case K_e: optionsPane = createCheatEnemiesPane(); restoreCurrentPane(); return;
1699         case K_TAB: freeRide = !freeRide; return;
1700         //case K_s: global.hasSpringShoes = !global.hasSpringShoes; return;
1701         //case K_j: global.hasJordans = !global.hasJordans; return;
1702         case K_i: switchInterpolator = true; unpauseGame(); return;
1703         case K_x:
1704           {
1705             /*
1706             auto bomb = ItemBomb(level.MakeMapObject(level.player.ix, level.player.iy, 'oBomb'));
1707             if (bomb) bomb.armIt();
1708             */
1709             level.resurrectPlayer();
1710             unpauseGame();
1711             return;
1712           }
1713         case K_r:
1714           //writeln("*** ROOM  SEED: ", global.globalRoomSeed);
1715           //writeln("*** OTHER SEED: ", global.globalOtherSeed);
1716           if (evt.bAlt && level.player && level.player.dead) {
1717             saveGameSession = false;
1718             replayGameSession = true;
1719             unpauseGame();
1720             return;
1721           }
1722           if (evt.bCtrl) global.idol = false;
1723           level.generateLevel();
1724           lastThinkerTime = 0;
1725           level.centerViewAtPlayer();
1726           teleportCameraAt(level.viewStart);
1727           return;
1728         case K_m:
1729           global.toggleMusic();
1730           return;
1731         case K_v:
1732           level.pickedSpectacles();
1733           return;
1734         case K_f:
1735           global.config.useFrozenRegion = !global.config.useFrozenRegion;
1736           unpauseGame();
1737           return;
1738         case K_q:
1739           if (level.allExits.length) {
1740             level.teleportPlayerTo(level.allExits[0].ix+8, level.allExits[0].iy+8);
1741             unpauseGame();
1742           }
1743           return;
1744         case K_d:
1745           if (level.player) {
1746             auto damsel = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa MonsterDamsel); });
1747             if (damsel) {
1748               level.teleportPlayerTo(damsel.ix, damsel.iy);
1749               unpauseGame();
1750             }
1751           }
1752           return;
1753         case K_h:
1754           if (level.player) {
1755             auto obj = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa ItemGoldenKey); });
1756             if (obj) {
1757               level.teleportPlayerTo(obj.ix, obj.iy-4);
1758               unpauseGame();
1759             }
1760           }
1761           return;
1762         case K_j:
1763           if (level.player) {
1764             auto obj = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa ItemLockedChest); });
1765             if (obj) {
1766               level.teleportPlayerTo(obj.ix, obj.iy);
1767               unpauseGame();
1768             }
1769           }
1770           return;
1771         case K_b:
1772           {
1773             auto obj = ObjBoulder(level.MakeMapTile((level.player.ix+32)/16, (level.player.iy-16)/16, 'oBoulder'));
1774             //if (obj) obj.monkey = monkey;
1775             if (obj) {
1776               //playSound('sndThump');
1777               unpauseGame();
1778             }
1779           }
1780           return;
1782         case K_DELETE: // suicide
1783           if (doGameSavingPlaying == Replay.None) {
1784             if (level.player && !level.player.dead && evt.bCtrl) {
1785               global.hasAnkh = false;
1786               level.global.plife = 1;
1787               level.player.invincible = 0;
1788               level.MakeMapObject(level.player.ix, level.player.iy, 'oExplosion');
1789               unpauseGame();
1790             }
1791           }
1792           return;
1794         case K_INSERT:
1795           if (level.player && !level.player.dead && evt.bAlt) {
1796             if (doGameSavingPlaying != Replay.None) {
1797               if (doGameSavingPlaying == Replay.Replaying) {
1798                 stopReplaying();
1799               } else if (doGameSavingPlaying == Replay.Saving) {
1800                 saveGameMovement(dbgSessionMovementFileName);
1801               }
1802               doGameSavingPlaying = Replay.None;
1803               stopReplaying();
1804               saveGameSession = false;
1805               replayGameSession = false;
1806               unpauseGame();
1807             }
1808           }
1809           return;
1811         case K_SPACE:
1812           level.stats.setMoneyCheat();
1813           level.stats.addMoney(10000);
1814           return;
1815       }
1816     }
1817   } else {
1818     if (evt.type == ev_keyup && evt.keycode == K_ESCAPE) {
1819       if (level.player && level.player.dead) {
1820         //Video.requestQuit();
1821         escCount = 0;
1822         if (gameJustOver) { gameJustOver = false; level.restartTitle(); }
1823       } else {
1824 #ifdef QUIT_DOUBLE_ESC
1825         if (++escCount == 2) Video.requestQuit();
1826 #else
1827         showHelp = 2;
1828         pauseRequested = true;
1829 #endif
1830       }
1831       return;
1832     }
1833     if (evt.type == ev_keydown && evt.keycode == K_F1) { pauseRequested = true; return; }
1834   }
1836   if (evt.type == ev_keydown && evt.keycode == K_n) { level.player.scrCreateBlood(level.player.ix, level.player.iy, 3); return; }
1838   if (level) {
1839     if (!level.player || !level.player.dead) {
1840       gameJustOver = false;
1841     } else if (level.player && level.player.dead) {
1842       if (!gameJustOver) {
1843         drawStats = 0;
1844         gameJustOver = true;
1845         waitingForPayRestart = true;
1846         level.clearKeysPressRelease();
1847         if (doGameSavingPlaying == Replay.None) {
1848           stopReplaying(); // just in case
1849           saveGameStats();
1850         }
1851       }
1852       replayFastForward = false;
1853       if (doGameSavingPlaying == Replay.Saving) {
1854         if (debugMovement) saveGameMovement(dbgSessionMovementFileName);
1855         doGameSavingPlaying = Replay.None;
1856         //clearGameMovement();
1857         saveGameSession = false;
1858         replayGameSession = false;
1859       }
1860     }
1861     if (evt.type == ev_keydown || evt.type == ev_keyup) {
1862       bool down = (evt.type == ev_keydown);
1863       if (doGameSavingPlaying == Replay.Replaying && level.player && !level.player.dead) {
1864         if (down && evt.keycode == K_f) {
1865           if (evt.bCtrl) {
1866             if (replayFastForwardSpeed != 4) {
1867               replayFastForwardSpeed = 4;
1868               replayFastForward = true;
1869             } else {
1870               replayFastForward = !replayFastForward;
1871             }
1872           } else {
1873             replayFastForwardSpeed = 2;
1874             replayFastForward = !replayFastForward;
1875           }
1876         }
1877       }
1878       if (doGameSavingPlaying != Replay.Replaying || !level.player || level.player.dead) {
1879         foreach (int kbidx, int kval; global.config.keybinds) {
1880           if (kval && kval == evt.keycode) {
1881 #ifndef BIGGER_REPLAY_DATA
1882             if (doGameSavingPlaying == Replay.Saving) debugMovement.addKey(kbidx, down);
1883 #endif
1884             level.onKey(1<<(kbidx/GameConfig::MaxActionBinds), down);
1885           }
1886         }
1887       }
1888       if (level.player && level.player.dead) {
1889         if (down && evt.keycode == K_r && evt.bAlt) {
1890           saveGameSession = false;
1891           replayGameSession = true;
1892           unpauseGame();
1893         }
1894         if (down && evt.keycode == K_s && evt.bAlt) {
1895           bool wasSaveReq = saveGameSession;
1896           stopReplaying(); // just in case
1897           saveGameSession = !wasSaveReq;
1898           replayGameSession = false;
1899           //unpauseGame();
1900         }
1901         if (replayGameSession) {
1902           stopReplaying(); // just in case
1903           saveGameSession = false;
1904           replayGameSession = false;
1905           loadGameMovement(dbgSessionMovementFileName);
1906           loadGame(dbgSessionStateFileName);
1907           doGameSavingPlaying = Replay.Replaying;
1908         } else {
1909           if (down && evt.keycode == K_s && !evt.bAlt) ++drawStats;
1910           if (waitingForPayRestart) {
1911             level.isKeyReleased(GameConfig::Key.Pay);
1912             if (level.isKeyPressed(GameConfig::Key.Pay)) waitingForPayRestart = false;
1913           } else {
1914             level.isKeyPressed(GameConfig::Key.Pay);
1915             if (level.isKeyReleased(GameConfig::Key.Pay)) {
1916               auto doSave = saveGameSession;
1917               stopReplaying(); // just in case
1918               level.clearKeysPressRelease();
1919               level.restartGame();
1920               level.generateNormalLevel();
1921               if (doSave) {
1922                 saveGameSession = false;
1923                 replayGameSession = false;
1924                 writeln("DBG: saving game session...");
1925                 clearGameMovement();
1926                 doGameSavingPlaying = Replay.Saving;
1927                 saveGame(dbgSessionStateFileName);
1928                 saveGameMovement(dbgSessionMovementFileName);
1929               }
1930             }
1931           }
1932         }
1933       }
1934     }
1935   }
1939 void levelExited () {
1940   // just in case
1941   saveGameStats();
1945 final void runGameLoop () {
1946   Video.frameTime = 0; // unlimited FPS
1947   lastThinkerTime = 0;
1949   sprStore = SpawnObject(SpriteStore);
1950   sprStore.bDumpLoaded = false;
1952   bgtileStore = SpawnObject(BackTileStore);
1953   bgtileStore.bDumpLoaded = false;
1955   level = SpawnObject(GameLevel);
1956   level.setup(global, sprStore, bgtileStore);
1958   level.global = global;
1959   level.sprStore = sprStore;
1960   level.bgtileStore = bgtileStore;
1962   loadGameStats();
1964   level.onBeforeFrame = &beforeNewFrame;
1965   level.onAfterFrame = &afterNewFrame;
1966   level.onInterFrame = &interFrame;
1967   level.onLevelExitedCB = &levelExited;
1969 #ifdef MASK_TEST
1970   maskSX = -0x0ff_fff;
1971   maskSY = maskSX;
1972   smask = sprStore['sExplosionMask'];
1973   maskFrame = 3;
1974 #endif
1976   sprStore.loadFont('sFontSmall');
1978   Video.swapInterval = (global.config.optVSync ? 1 : 0);
1979   Video.openScreen("Spelunky/VaVoom C", 320*3, 240*3);
1981   if (Video.realStencilBits < 8) {
1982     Video.closeScreen();
1983     FatalError("FATAL: no stencil buffer!");
1984   }
1985   if (!Video.framebufferHasAlpha) {
1986     Video.closeScreen();
1987     FatalError("FATAL: no alpha channel in framebuffer!");
1988   }
1990   //SoundSystem.SwapStereo = config.swapStereo;
1991   SoundSystem.DopplerFactor = 1.0f;
1992   SoundSystem.DopplerVelocity = 10000.0f;
1993   SoundSystem.RolloffFactor = 1.0f;
1994   //SoundSystem.ReferenceDistance = 32.0f; // The distance under which the volume for the source would normally drop by half (before being influenced by rolloff factor or AL_MAX_DISTANCE)
1995   SoundSystem.ReferenceDistance = 32.0f*5; // The distance under which the volume for the source would normally drop by half (before being influenced by rolloff factor or AL_MAX_DISTANCE)
1996   SoundSystem.MaxDistance = 800.0f*2; // Used with the Inverse Clamped Distance Model to set the distance where there will no longer be any attenuation of the source
1997   SoundSystem.NumChannels = 64;
1998   SoundSystem.Sound2DPos = vector(0, 0, -1);
2000   SoundSystem.Initialize();
2001   global.fixVolumes();
2003   level.viewWidth = Video.screenWidth;
2004   level.viewHeight = Video.screenHeight;
2006   level.restartGame(); // this will NOT generate a new level
2007   setupCheats();
2008   setupSeeds();
2009   performTimeCheck();
2011   texTigerEye = GLTexture.Load("sprites/teye0.png");
2013   if (global.cheatEndGameSequence) {
2014     level.winTime = 12*60+42;
2015     level.stats.money = 6666;
2016     switch (global.cheatEndGameSequence) {
2017       case 1: default: level.startWinCutscene(); break;
2018       case 2: level.startWinCutsceneVolcano(); break;
2019       case 3: level.startWinCutsceneWinFall(); break;
2020     }
2021   } else {
2022     switch (startMode) {
2023       case StartMode.Title: level.restartTitle(); break;
2024       case StartMode.Stars: level.restartStarsRoom(); break;
2025       case StartMode.Sun: level.restartSunRoom(); break;
2026       case StartMode.Moon: level.restartMoonRoom(); break;
2027       default:
2028         level.generateNormalLevel();
2029         if (startMode == StartMode.Dead) {
2030           level.player.dead = true;
2031           level.player.visible = false;
2032         }
2033         break;
2034     }
2035   }
2037   //global.rope = 666;
2038   //global.bombs = 666;
2040   //global.globalRoomSeed = 871520037;
2041   //global.globalOtherSeed = 1047036290;
2043   //level.createTitleRoom();
2044   //level.createTrans4Room();
2045   //level.createOlmecRoom();
2046   //level.generateLevel();
2048   //level.centerViewAtPlayer();
2049   teleportCameraAt(level.viewStart);
2050   //writeln(Video.swapInterval);
2052   Video.runEventLoop();
2053   Video.closeScreen();
2055   if (doGameSavingPlaying == Replay.Saving) saveGameMovement(dbgSessionMovementFileName);
2056   stopReplaying();
2057   saveGameStats();
2059   level.clearTiles();
2060   level.clearObjects();
2064 // ////////////////////////////////////////////////////////////////////////// //
2065 // duplicates are not allowed!
2066 final void checkGameObjNames () {
2067   array!(class!Object) known;
2068   class!Object cc;
2069   int classCount = 0, namedCount = 0;
2070   foreach AllClasses(Object, out cc) {
2071     auto gn = GetClassGameObjName(cc);
2072     if (gn) {
2073       //writeln("'", gn, "' is `", GetClassName(cc), "`");
2074       auto nid = NameToInt(gn);
2075       if (nid < known.length && known[nid]) FatalError("duplicate game object name '%n' (defined for class is '%n', redefined in class '%n')", gn, GetClassName(known[nid]), GetClassName(cc));
2076       known[nid] = cc;
2077       ++namedCount;
2078     }
2079     ++classCount;
2080   }
2081   writeln(classCount, " classes, ", namedCount, " game object classes.");
2085 // ////////////////////////////////////////////////////////////////////////// //
2086 #include "timelimit.vc"
2087 //const int TimeLimitDate = 2018232;
2090 void performTimeCheck () {
2091 #ifdef DISABLE_TIME_CHECK
2092 #else
2093   if (TigerEye) return;
2095   TTimeVal tv;
2096   if (!GetTimeOfDay(out tv)) FatalError("cannot get time of day");
2098   TDateTime tm;
2099   if (!DecodeTimeVal(out tm, ref tv)) FatalError("cannot decode time of day");
2101   int tldate = tm.year*1000+tm.yday;
2103   if (tldate > TimeLimitDate) {
2104     level.maxPlayingTime = 24;
2105   } else {
2106     //writeln("*** days left: ", TimeLimitDate-tldate);
2107   }
2108 #endif
2112 void setupCheats () {
2113   return;
2114   //startMode = StartMode.Dead;
2115   //startMode = StartMode.Title;
2116   //startMode = StartMode.Stars;
2117   //startMode = StartMode.Sun;
2118   startMode = StartMode.Moon;
2119   return;
2120   //global.scumGenSacrificePit = true;
2121   //global.scumAlwaysSacrificeAltar = true;
2123   // first lush jungle level
2124   //global.levelType = 1;
2125   /*
2126   global.scumGenCemetary = true;
2127   */
2128   //global.idol = false;
2129   //global.currLevel = 5;
2131   //global.isTunnelMan = true;
2132   //return;
2134   //global.currLevel = 5;
2135   //global.scumGenLake = true;
2137   //global.currLevel = 5;
2138   //global.currLevel = 9;
2139   //global.currLevel = 13;
2140   //global.currLevel = 14;
2141   //global.cheatEndGameSequence = 1;
2142   //return;
2144   //global.currLevel = 6;
2145   global.scumGenAlienCraft = true;
2146   global.currLevel = 9;
2147   //global.scumGenYetiLair = true;
2148   //global.genBlackMarket = true;
2149   //startDead = false;
2150   startMode = StartMode.Alive;
2151   return;
2153   global.cheatCanSkipOlmec = true;
2154   global.currLevel = 15;
2155   startMode = StartMode.Alive;
2156   return;
2158   global.scumGenShop = true;
2159   //global.scumGenShopType = GameGlobal::ShopType.Weapon;
2160   global.scumGenShopType = GameGlobal::ShopType.Craps;
2161   //global.scumGenShopType = 6; // craps
2162   //global.scumGenShopType = 7; // kissing
2164   //global.scumAlwaysSacrificeAltar = true;
2168 void setupSeeds () {
2172 // ////////////////////////////////////////////////////////////////////////// //
2173 void main () {
2174   checkGameObjNames();
2176   appSetName("k8spelunky");
2177   config = SpawnObject(GameConfig);
2178   global = SpawnObject(GameGlobal);
2179   global.config = config;
2180   config.heroType = GameConfig::Hero.Spelunker;
2182   global.randomizeSeedAll();
2184   fillCheatPickupList();
2185   fillCheatItemsList();
2186   fillCheatEnemiesList();
2188   loadGameOptions();
2189   loadKeyboardBindings();
2190   runGameLoop();