oops: checking wrong object in "hit by item" handler
[k8vacspelynky.git] / spelunky_main.vc
blobc9afd459d1b8140dc156366c6851419c2983ebbb
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 MASK_TEST
29 //#define BIGGER_REPLAY_DATA
31 // ////////////////////////////////////////////////////////////////////////// //
32 #include "mapent/0all.vc"
33 #include "PlayerPawn.vc"
34 #include "PlayerPowerup.vc"
35 #include "GameLevel.vc"
38 // ////////////////////////////////////////////////////////////////////////// //
39 #include "uisimple.vc"
42 // ////////////////////////////////////////////////////////////////////////// //
43 class DebugSessionMovement : Object;
45 #ifdef BIGGER_REPLAY_DATA
46 array!(GameLevel::SavedKeyState) keypresses;
47 #else
48 array!ubyte keypresses; // on each frame
49 #endif
50 GameConfig playconfig;
52 transient int keypos;
53 transient int otherSeed, roomSeed;
56 override void Destroy () {
57   delete playconfig;
58   keypresses.length = 0;
59   ::Destroy();
63 final void resetReplay () {
64   keypos = 0;
68 #ifndef BIGGER_REPLAY_DATA
69 final void addKey (int kbidx, bool down) {
70   if (kbidx < 0 || kbidx >= 127) FatalError("DebugSessionMovement: invalid kbidx (%d)", kbidx);
71   keypresses[$] = kbidx|(down ? 0x80 : 0);
75 final void addEndOfFrame () {
76   keypresses[$] = 0xff;
80 enum {
81   NORMAL,
82   END_OF_FRAME,
83   END_OF_RECORD,
86 final int getKey (out int kbidx, out bool down) {
87   if (keypos < 0) FatalError("DebugSessionMovement: invalid keypos");
88   if (keypos >= keypresses.length) return END_OF_RECORD;
89   ubyte b = keypresses[keypos++];
90   if (b == 0xff) return END_OF_FRAME;
91   kbidx = b&0x7f;
92   down = (b >= 0x80);
93   return NORMAL;
95 #endif
98 // ////////////////////////////////////////////////////////////////////////// //
99 class TempOptionsKeys : Object;
101 int[16*GameConfig::MaxActionBinds] keybinds;
102 int kbversion = 1;
105 // ////////////////////////////////////////////////////////////////////////// //
106 class Main : Object;
108 transient string dbgSessionStateFileName = "debug_game_session_state";
109 transient string dbgSessionMovementFileName = "debug_game_session_movement";
111 GLTexture texTigerEye;
113 GameConfig config;
114 GameGlobal global;
115 SpriteStore sprStore;
116 BackTileStore bgtileStore;
117 GameLevel level;
119 int mouseX = int.min, mouseY = int.min;
120 int mouseLevelX = int.min, mouseLevelY = int.min;
121 bool renderMouseTile;
122 bool renderMouseRect;
124 enum StartMode {
125   Dead,
126   Alive,
127   Title,
128   Intro,
129   Stars,
130   Sun,
131   Moon,
134 StartMode startMode = StartMode.Intro;
135 bool pauseRequested;
136 bool helpRequested;
138 bool replayFastForward = false;
139 int replayFastForwardSpeed = 2;
140 bool saveGameSession = false;
141 bool replayGameSession = false;
142 enum Replay {
143   None,
144   Saving,
145   Replaying,
147 Replay doGameSavingPlaying = Replay.None;
148 float saveMovementLastTime = 0;
149 DebugSessionMovement debugMovement;
150 GameStats origStats; // for replaying
151 GameConfig origConfig; // for replaying
152 int origRoomSeed, origOtherSeed;
154 int showHelp;
155 int escCount;
157 bool fullscreen;
159 #ifdef MASK_TEST
160 transient int maskSX, maskSY;
161 transient SpriteImage smask;
162 transient int maskFrame;
163 #endif
166 // ////////////////////////////////////////////////////////////////////////// //
167 final void saveKeyboardBindings () {
168   auto tok = SpawnObject(TempOptionsKeys);
169   foreach (auto idx, auto v; global.config.keybinds) tok.keybinds[idx] = v;
170   appSaveOptions(tok, "keybindings");
171   delete tok;
175 final void loadKeyboardBindings () {
176   auto tok = appLoadOptions(TempOptionsKeys, "keybindings");
177   if (tok) {
178     if (tok.kbversion != TempOptionsKeys.default.kbversion) {
179       global.config.resetKeybindings();
180     } else {
181       foreach (auto idx, ref auto v; global.config.keybinds) v = tok.keybinds[idx];
182     }
183     delete tok;
184   }
188 // ////////////////////////////////////////////////////////////////////////// //
189 void saveGameOptions () {
190   appSaveOptions(global.config, "config");
194 void loadGameOptions () {
195   auto cfg = appLoadOptions(GameConfig, "config");
196   if (cfg) {
197     auto oldHero = config.heroType;
198     auto tok = SpawnObject(TempOptionsKeys);
199     foreach (auto idx, auto v; global.config.keybinds) tok.keybinds[idx] = v;
200     delete global.config;
201     global.config = cfg;
202     config = cfg;
203     foreach (auto idx, ref auto v; global.config.keybinds) v = tok.keybinds[idx];
204     delete tok;
205     writeln("config loaded");
206     global.restartMusic();
207     global.fixVolumes();
208     //config.heroType = GameConfig::Hero.Spelunker;
209     config.heroType = oldHero;
210   }
211   // fix my bug
212   if (global.config.ghostExtraTime > 300) global.config.ghostExtraTime = 30;
216 // ////////////////////////////////////////////////////////////////////////// //
217 void saveGameStats () {
218   if (level.stats) appSaveOptions(level.stats, "stats");
222 void loadGameStats () {
223   auto stats = appLoadOptions(GameStats, "stats");
224   if (stats) {
225     delete level.stats;
226     level.stats = stats;
227   }
228   if (!level.stats) level.stats = SpawnObject(GameStats);
229   level.stats.global = global;
233 // ////////////////////////////////////////////////////////////////////////// //
234 struct UIPaneSaveInfo {
235   name id;
236   UIPane::SaveInfo nfo;
239 transient UIPane optionsPane; // either options, or binding editor
241 transient GameLevel::IVec2D optionsPaneOfs;
242 transient void delegate () saveOptionsDG;
244 transient array!UIPaneSaveInfo optionsPaneState;
247 final void saveCurrentPane () {
248   if (!optionsPane || !optionsPane.id) return;
250   // summon ghost
251   if (optionsPane.id == 'CheatFlags') {
252     if (instantGhost && level.ghostTimeLeft > 0) {
253       level.ghostTimeLeft = 1;
254     }
255   }
257   foreach (ref auto psv; optionsPaneState) {
258     if (psv.id == optionsPane.id) {
259       optionsPane.saveState(psv.nfo);
260       return;
261     }
262   }
263   // append new
264   optionsPaneState.length += 1;
265   optionsPaneState[$-1].id = optionsPane.id;
266   optionsPane.saveState(optionsPaneState[$-1].nfo);
270 final void restoreCurrentPane () {
271   if (optionsPane) optionsPane.setupHotkeys(); // why not?
272   if (!optionsPane || !optionsPane.id) return;
273   foreach (ref auto psv; optionsPaneState) {
274     if (psv.id == optionsPane.id) {
275       optionsPane.restoreState(psv.nfo);
276       return;
277     }
278   }
282 // ////////////////////////////////////////////////////////////////////////// //
283 final void onCheatObjectSpawnSelectedCB (UIMenuItem it) {
284   if (!it.tagClass) return;
285   if (class!MapObject(it.tagClass)) {
286     level.debugSpawnObjectWithClass(class!MapObject(it.tagClass), playerDir:true);
287     it.owner.closeMe = true;
288   }
292 // ////////////////////////////////////////////////////////////////////////// //
293 transient array!(class!MapObject) cheatItemsList;
296 final void fillCheatItemsList () {
297   cheatItemsList.length = 0;
298   cheatItemsList[$] = ItemProjectileArrow;
299   cheatItemsList[$] = ItemWeaponShotgun;
300   cheatItemsList[$] = ItemWeaponAshShotgun;
301   cheatItemsList[$] = ItemWeaponPistol;
302   cheatItemsList[$] = ItemWeaponMattock;
303   cheatItemsList[$] = ItemWeaponMachete;
304   cheatItemsList[$] = ItemWeaponWebCannon;
305   cheatItemsList[$] = ItemWeaponSceptre;
306   cheatItemsList[$] = ItemWeaponBow;
307   cheatItemsList[$] = ItemBones;
308   cheatItemsList[$] = ItemFakeBones;
309   cheatItemsList[$] = ItemFishBone;
310   cheatItemsList[$] = ItemRock;
311   cheatItemsList[$] = ItemJar;
312   cheatItemsList[$] = ItemSkull;
313   cheatItemsList[$] = ItemGoldenKey;
314   cheatItemsList[$] = ItemGoldIdol;
315   cheatItemsList[$] = ItemCrystalSkull;
316   cheatItemsList[$] = ItemShellSingle;
317   cheatItemsList[$] = ItemChest;
318   cheatItemsList[$] = ItemCrate;
319   cheatItemsList[$] = ItemLockedChest;
320   cheatItemsList[$] = ItemDice;
321   cheatItemsList[$] = ItemBasketBall;
325 final UIPane createCheatItemsPane () {
326   if (!level.player) return none;
328   UIPane pane = SpawnObject(UIPane);
329   pane.id = 'Items';
330   pane.sprStore = sprStore;
332   pane.width = 320*3-64;
333   pane.height = 240*3-64;
335   foreach (auto ipk; cheatItemsList) {
336     auto it = UIMenuItem.Create(pane, ipk.default.desc.toUpperCase(), ipk.default.desc2.toUpperCase(), &onCheatObjectSpawnSelectedCB);
337     it.tagClass = ipk;
338   }
340   //optionsPaneOfs.x = 100;
341   //optionsPaneOfs.y = 50;
343   return pane;
347 // ////////////////////////////////////////////////////////////////////////// //
348 transient array!(class!MapObject) cheatEnemiesList;
351 final void fillCheatEnemiesList () {
352   cheatEnemiesList.length = 0;
353   cheatEnemiesList[$] = MonsterDamsel; // not an enemy, but meh..
354   cheatEnemiesList[$] = EnemyBat;
355   cheatEnemiesList[$] = EnemySpiderHang;
356   cheatEnemiesList[$] = EnemySpider;
357   cheatEnemiesList[$] = EnemySnake;
358   cheatEnemiesList[$] = EnemyCaveman;
359   cheatEnemiesList[$] = EnemySkeleton;
360   cheatEnemiesList[$] = MonsterShopkeeper;
361   cheatEnemiesList[$] = EnemyZombie;
362   cheatEnemiesList[$] = EnemyVampire;
363   cheatEnemiesList[$] = EnemyFrog;
364   cheatEnemiesList[$] = EnemyGreenFrog;
365   cheatEnemiesList[$] = EnemyFireFrog;
366   cheatEnemiesList[$] = EnemyMantrap;
367   cheatEnemiesList[$] = EnemyScarab;
368   cheatEnemiesList[$] = EnemyFloater;
369   cheatEnemiesList[$] = EnemyBlob;
370   cheatEnemiesList[$] = EnemyMonkey;
371   cheatEnemiesList[$] = EnemyGoldMonkey;
372   cheatEnemiesList[$] = EnemyAlien;
373   cheatEnemiesList[$] = EnemyYeti;
374   cheatEnemiesList[$] = EnemyHawkman;
375   cheatEnemiesList[$] = EnemyUFO;
376   cheatEnemiesList[$] = EnemyYetiKing;
380 final UIPane createCheatEnemiesPane () {
381   if (!level.player) return none;
383   UIPane pane = SpawnObject(UIPane);
384   pane.id = 'Enemies';
385   pane.sprStore = sprStore;
387   pane.width = 320*3-64;
388   pane.height = 240*3-64;
390   foreach (auto ipk; cheatEnemiesList) {
391     auto it = UIMenuItem.Create(pane, ipk.default.desc.toUpperCase(), ipk.default.desc2.toUpperCase(), &onCheatObjectSpawnSelectedCB);
392     it.tagClass = ipk;
393   }
395   //optionsPaneOfs.x = 100;
396   //optionsPaneOfs.y = 50;
398   return pane;
402 // ////////////////////////////////////////////////////////////////////////// //
403 transient array!(class!/*ItemPickup*/MapItem) cheatPickupList;
406 final void fillCheatPickupList () {
407   cheatPickupList.length = 0;
408   cheatPickupList[$] = ItemPickupBombBag;
409   cheatPickupList[$] = ItemPickupBombBox;
410   cheatPickupList[$] = ItemPickupPaste;
411   cheatPickupList[$] = ItemPickupRopePile;
412   cheatPickupList[$] = ItemPickupShellBox;
413   cheatPickupList[$] = ItemPickupAnkh;
414   cheatPickupList[$] = ItemPickupCape;
415   cheatPickupList[$] = ItemPickupJetpack;
416   cheatPickupList[$] = ItemPickupUdjatEye;
417   cheatPickupList[$] = ItemPickupCrown;
418   cheatPickupList[$] = ItemPickupKapala;
419   cheatPickupList[$] = ItemPickupParachute;
420   cheatPickupList[$] = ItemPickupCompass;
421   cheatPickupList[$] = ItemPickupSpectacles;
422   cheatPickupList[$] = ItemPickupGloves;
423   cheatPickupList[$] = ItemPickupMitt;
424   cheatPickupList[$] = ItemPickupJordans;
425   cheatPickupList[$] = ItemPickupSpringShoes;
426   cheatPickupList[$] = ItemPickupSpikeShoes;
427   cheatPickupList[$] = ItemPickupTeleporter;
431 final UIPane createCheatPickupsPane () {
432   if (!level.player) return none;
434   UIPane pane = SpawnObject(UIPane);
435   pane.id = 'Pickups';
436   pane.sprStore = sprStore;
438   pane.width = 320*3-64;
439   pane.height = 240*3-64;
441   foreach (auto ipk; cheatPickupList) {
442     auto it = UIMenuItem.Create(pane, ipk.default.desc.toUpperCase(), ipk.default.desc2.toUpperCase(), &onCheatObjectSpawnSelectedCB);
443     it.tagClass = ipk;
444   }
446   //optionsPaneOfs.x = 100;
447   //optionsPaneOfs.y = 50;
449   return pane;
453 // ////////////////////////////////////////////////////////////////////////// //
454 transient int instantGhost;
456 final UIPane createCheatFlagsPane () {
457   UIPane pane = SpawnObject(UIPane);
458   pane.id = 'CheatFlags';
459   pane.sprStore = sprStore;
461   pane.width = 320*3-64;
462   pane.height = 240*3-64;
464   instantGhost = 0;
466   UICheckBox.Create(pane, &global.hasUdjatEye, "UDJAT EYE", "UDJAT EYE");
467   UICheckBox.Create(pane, &global.hasAnkh, "ANKH", "ANKH");
468   UICheckBox.Create(pane, &global.hasCrown, "CROWN", "CROWN");
469   UICheckBox.Create(pane, &global.hasKapala, "KAPALA", "COLLECT BLOOD TO GET MORE LIVES!");
470   UICheckBox.Create(pane, &global.hasStickyBombs, "STICKY BOMBS", "YOUR BOMBS CAN STICK!");
471   //UICheckBox.Create(pane, &global.stickyBombsActive, "stickyBombsActive", "stickyBombsActive");
472   UICheckBox.Create(pane, &global.hasSpectacles, "SPECTACLES", "YOU CAN SEE WHAT WAS HIDDEN!");
473   UICheckBox.Create(pane, &global.hasCompass, "COMPASS", "COMPASS");
474   UICheckBox.Create(pane, &global.hasParachute, "PARACHUTE", "YOU WILL DEPLOY PARACHUTE ON LONG FALLS.");
475   UICheckBox.Create(pane, &global.hasSpringShoes, "SPRING SHOES", "YOU CAN JUMP HIGHER!");
476   UICheckBox.Create(pane, &global.hasSpikeShoes, "SPIKE SHOES", "YOUR HEAD-JUMPS DOES MORE DAMAGE!");
477   UICheckBox.Create(pane, &global.hasJordans, "JORDANS", "YOU CAN JUMP TO THE MOON!");
478   //UICheckBox.Create(pane, &global.hasNinjaSuit, "hasNinjaSuit", "hasNinjaSuit");
479   UICheckBox.Create(pane, &global.hasCape, "CAPE", "YOU CAN CONTROL YOUR FALLS!");
480   UICheckBox.Create(pane, &global.hasJetpack, "JETPACK", "FLY TO THE SKY!");
481   UICheckBox.Create(pane, &global.hasGloves, "GLOVES", "OH, THOSE GLOVES ARE STICKY!");
482   UICheckBox.Create(pane, &global.hasMitt, "MITT", "YAY, YOU'RE THE BEST CATCHER IN THE WORLD NOW!");
483   UICheckBox.Create(pane, &instantGhost, "INSTANT GHOST", "SUMMON GHOST");
485   optionsPaneOfs.x = 100;
486   optionsPaneOfs.y = 50;
488   return pane;
492 final UIPane createOptionsPane () {
493   UIPane pane = SpawnObject(UIPane);
494   pane.id = 'Options';
495   pane.sprStore = sprStore;
497   pane.width = 320*3-64;
498   pane.height = 240*3-64;
501   // this is buggy
502   //!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.");
505   UILabel.Create(pane, "VISUAL OPTIONS");
506     UICheckBox.Create(pane, &config.skipIntro, "SKIP INTRO", "AUTOMATICALLY SKIPS THE INTRO SEQUENCE AND STARTS THE GAME AT THE TITLE SCREEN.");
507     UICheckBox.Create(pane, &config.interpolateMovement, "INTERPOLATE MOVEMENT", "IF TURNED OFF, THE MOVEMENT WILL BE JERKY AND ANNOYING.");
508     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).");
509     UICheckBox.Create(pane, &config.scumMetric, "METRIC UNITS", "DEPTH WILL BE MEASURED IN METRES INSTEAD OF FEET.");
510     auto startfs = UICheckBox.Create(pane, &config.startFullscreen, "START FULLSCREEN", "START THE GAME IN FULLSCREEN MODE?");
511     startfs.onValueChanged = delegate void (int newval) {
512       Video.closeScreen();
513       fullscreen = newval;
514       initializeVideo();
515     };
516     auto fsmode = UIIntEnum.Create(pane, &config.fsmode, 1, 2, "FULLSCREEN MODE: ", "YOU CAN CHOOSE EITHER REAL FULLSCREEN MODE, OR SCALED. USUALLY, SCALED WORKS BETTER, BUT REAL LOOKS NICER (YET IT MAY NOT WORK ON YOUR GPU).");
517     fsmode.names[$] = "REAL";
518     fsmode.names[$] = "SCALED";
519     fsmode.onValueChanged = delegate void (int newval) {
520       if (fullscreen) {
521         Video.closeScreen();
522         initializeVideo();
523       }
524     };
527   UILabel.Create(pane, "");
528   UILabel.Create(pane, "HUD OPTIONS");
529     UICheckBox.Create(pane, &config.ghostShowTime, "SHOW GHOST TIME", "TURN THIS OPTION ON TO SEE HOW MUCH TIME IS LEFT UNTIL THE GHOST WILL APPEAR.");
530     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.");
531     auto halpha = UIIntEnum.Create(pane, &config.hudTextAlpha, 0, 250, "HUD TEXT ALPHA :", "THE BIGGER THIS NUMBER, THE MORE TRANSPARENT YOUR MAIN HUD WILL BE.");
532     halpha.step = 10;
534     auto ialpha = UIIntEnum.Create(pane, &config.hudItemsAlpha, 0, 250, "HUD ITEMS ALPHA:", "THE BIGGER THIS NUMBER, THE MORE TRANSPARENT YOUR ITEMS HUD WILL BE.");
535     ialpha.step = 10;
538   UILabel.Create(pane, "");
539   UILabel.Create(pane, "COSMETIC GAMEPLAY OPTIONS");
540     //!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.");
541     //UICheckBox.Create(pane, &config.optImmTransition, "FASTER TRANSITIONS", "PRESSING ACTION SECOND TIME WILL IMMEDIATELY SKIP TRANSITION LEVEL.");
542     UICheckBox.Create(pane, &config.downToRun, "PRESS 'DOWN' TO RUN", "PLAYER CAN PRESS 'DOWN' KEY TO RUN.");
543     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.");
544     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.");
545     UICheckBox.Create(pane, &config.naturalSwim, "IMPROVED SWIMMING", "HOLD DOWN TO SINK FASTER, HOLD UP TO SINK SLOWER."); // Spelunky Natural swim mechanics
546     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.");
547     UICheckBox.Create(pane, &config.optSpikeVariations, "RANDOM SPIKES", "GENERATE SPIKES OF RANDOM TYPE (DEFAULT TYPE HAS GREATER PROBABILITY, THOUGH).");
550   UILabel.Create(pane, "");
551   UILabel.Create(pane, "GAMEPLAY OPTIONS");
552     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.");
553     UICheckBox.Create(pane, &config.bomsDontSetArrowTraps, "ARROW TRAPS IGNORE BOMBS", "TURN THIS OPTION ON TO MAKE ARROW TRAP IGNORE FALLING BOMBS AND ROPES.");
554     UICheckBox.Create(pane, &config.weaponsOpenContainers, "MELEE CONTAINERS", "ALLOWS YOU TO OPEN CRATES AND CHESTS BY HITTING THEM WITH THE WHIP, MACHETE OR MATTOCK.");
555     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!");
556     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.");
557     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.");
558     UICheckBox.Create(pane, &config.optThrowEmptyShotgun, "THROW EMPTY SHOTGUN", "PRESSING ACTION WHEN SHOTGUN IS EMPTY WILL THROW IT.");
559     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.");
560     UICheckBox.Create(pane, &config.ghostRandom, "RANDOM GHOST DELAY", "THIS OPTION WILL RANDOMIZE THE DELAY UNTIL THE GHOST APPEARS AFTER THE TIME LIMIT BELOW IS REACHED INSTEAD OF USING THE DEFAULT 30 SECONDS. CHANGES EACH LEVEL AND VARIES WITH THE TIME LIMIT YOU SET.");
561     UICheckBox.Create(pane, &config.ghostAtFirstLevel, "GHOST AT FIRST LEVEL", "TURN THIS OPTION ON IF YOU WANT THE GHOST TO BE SPAWNED ON THE FIRST LEVEL.");
562     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.");
563     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?");
564     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.");
565     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.");
566     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.");
567     UICheckBox.Create(pane, &config.optEnemyVariations, "ENEMY VARIATIONS", "ADD SOME ENEMY VARIATIONS IN MINES AND JUNGLE WHEN YOU DIED ENOUGH TIMES.");
568     UICheckBox.Create(pane, &config.optIdolForEachLevelType, "IDOL IN EACH LEVEL TYPE", "GENERATE IDOL IN EACH LEVEL TYPE.");
569     UICheckBox.Create(pane, &config.boulderChaos, "BOULDER CHAOS", "BOULDERS WILL ROLL FASTER, BOUNCE A BIT HIGHER, AND KEEP THEIR MOMENTUM LONGER.");
570     auto rstl = UIIntEnum.Create(pane, &config.optRoomStyle, -1, 1, "ROOM STYLE:", "WHAT KIND OF ROOMS LEVEL GENERATOR SHOULD USE.");
571     rstl.names[$] = "RANDOM";
572     rstl.names[$] = "NORMAL";
573     rstl.names[$] = "BIZARRE";
576   UILabel.Create(pane, "");
577   UILabel.Create(pane, "WHIP OPTIONS");
578     UICheckBox.Create(pane, &global.config.unarmed, "UNARMED", "WITH THIS OPTION ENABLED, YOU WILL HAVE NO WHIP.");
579     auto whiptype = UIIntEnum.Create(pane, &config.scumWhipUpgrade, 0, 1, "WHIP TYPE:", "YOU CAN HAVE A NORMAL WHIP, OR A LONGER ONE.");
580     whiptype.names[$] = "NORMAL";
581     whiptype.names[$] = "LONG";
582     UICheckBox.Create(pane, &global.config.killEnemiesThruWalls, "PENETRATE WALLS", "WITH THIS OPTION ENABLED, YOU WILL BE ABLE TO WHIP ENEMIES THROUGH THE WALLS SOMETIMES. THIS IS HOW IT WORKED IN CLASSIC.");
585   UILabel.Create(pane, "");
586   UILabel.Create(pane, "PLAYER OPTIONS");
587     auto herotype = UIIntEnum.Create(pane, &config.heroType, 0, 2, "PLAY AS: ", "CHOOSE YOUR HERO!");
588     herotype.names[$] = "SPELUNKY GUY";
589     herotype.names[$] = "DAMSEL";
590     herotype.names[$] = "TUNNEL MAN";
593   UILabel.Create(pane, "");
594   UILabel.Create(pane, "CHEAT OPTIONS");
595     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.");
596     auto plrlit = UIIntEnum.Create(pane, &config.scumPlayerLit, 0, 2, "PLAYER LIT:", "LIT PLAYER IN DARKNESS WHEN...");
597     plrlit.names[$] = "NEVER";
598     plrlit.names[$] = "FORCED DARKNESS";
599     plrlit.names[$] = "ALWAYS";
600     UIIntEnum.Create(pane, &config.darknessDarkness, 0, 8, "DARKNESS LEVEL:", "INCREASE THIS NUMBER TO MAKE DARK AREAS BRIGHTER.");
601     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'.");
602     rdark.names[$] = "NEVER";
603     rdark.names[$] = "DEFAULT";
604     rdark.names[$] = "ALWAYS";
605     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.");
606     rghost.step = 30;
607     rghost.getNameCB = delegate string (int val) {
608       if (val < 0) return "INSTANT";
609       if (val == 0) return "NEVER";
610       if (val < 120) return va("%d SEC", val);
611       if (val%60 == 0) return va("%d MIN", val/60);
612       if (val%60 == 30) return va("%d.5 MIN", val/60);
613       return va("%d MIN, %d SEC", val/60, val%60);
614     };
615     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.");
617   UILabel.Create(pane, "");
618   UILabel.Create(pane, "CHEAT START OPTIONS");
619     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.");
620     UICheckBox.Create(pane, &config.startWithKapala, "START WITH KAPALA", "PLAYER WILL ALWAYS START WITH KAPALA. THIS IS USEFUL TO PERFORM 'KAPALA CHALLENGES'.");
621     UIIntEnum.Create(pane, &config.scumStartLife,  1, 42, "STARTING LIVES:", "STARTING NUMBER OF LIVES FOR SPELUNKER.");
622     UIIntEnum.Create(pane, &config.scumStartBombs, 1, 42, "STARTING BOMBS:", "STARTING NUMBER OF BOMBS FOR SPELUNKER.");
623     UIIntEnum.Create(pane, &config.scumStartRope,  1, 42, "STARTING ROPES:", "STARTING NUMBER OF ROPES FOR SPELUNKER.");
626   UILabel.Create(pane, "");
627   UILabel.Create(pane, "LEVEL MUSIC OPTIONS");
628     auto mm = UIIntEnum.Create(pane, &config.transitionMusicMode, 0, 2, "TRANSITION MUSIC  : ", "THIS IS WHAT GAME SHOULD DO WITH MUSIC ON TRANSITION LEVELS.");
629     mm.names[$] = "SILENCE";
630     mm.names[$] = "RESTART";
631     mm.names[$] = "DON'T TOUCH";
633     mm = UIIntEnum.Create(pane, &config.nextLevelMusicMode, 1, 2, "NORMAL LEVEL MUSIC: ", "THIS IS WHAT GAME SHOULD DO WITH MUSIC ON NORMAL LEVELS.");
634     //mm.names[$] = "SILENCE";
635     mm.names[$] = "RESTART";
636     mm.names[$] = "DON'T TOUCH";
639   //auto swstereo = UICheckBox.Create(pane, &config.swapStereo, "SWAP STEREO", "SWAP STEREO CHANNELS.");
640   /*
641   swstereo.onValueChanged = delegate void (int newval) {
642     SoundSystem.SwapStereo = newval;
643   };
644   */
646   UILabel.Create(pane, "");
647   UILabel.Create(pane, "SOUND CONTROL CENTER");
648     auto rmusonoff = UICheckBox.Create(pane, &config.musicEnabled, "MUSIC", "PLAY OR DON'T PLAY MUSIC.");
649     rmusonoff.onValueChanged = delegate void (int newval) {
650       global.restartMusic();
651     };
653     UICheckBox.Create(pane, &config.soundEnabled, "SOUND", "PLAY OR DON'T PLAY SOUND.");
655     auto rvol = UIIntEnum.Create(pane, &config.musicVol, 0, GameConfig::MaxVolume, "MUSIC VOLUME:", "SET MUSIC VOLUME.");
656     rvol.onValueChanged = delegate void (int newval) { global.fixVolumes(); };
658     rvol = UIIntEnum.Create(pane, &config.soundVol, 0, GameConfig::MaxVolume, "SOUND VOLUME:", "SET SOUND VOLUME.");
659     rvol.onValueChanged = delegate void (int newval) { global.fixVolumes(); };
662   saveOptionsDG = delegate void () {
663     writeln("saving options");
664     saveGameOptions();
665   };
666   optionsPaneOfs.x = 42;
667   optionsPaneOfs.y = 0;
669   return pane;
673 final void createBindingsControl (UIPane pane, int keyidx) {
674   string kname, khelp;
675   switch (keyidx) {
676     case GameConfig::Key.Left: kname = "LEFT"; khelp = "MOVE SPELUNKER TO THE LEFT"; break;
677     case GameConfig::Key.Right: kname = "RIGHT"; khelp = "MOVE SPELUNKER TO THE RIGHT"; break;
678     case GameConfig::Key.Up: kname = "UP"; khelp = "MOVE SPELUNKER UP, OR LOOK UP"; break;
679     case GameConfig::Key.Down: kname = "DOWN"; khelp = "MOVE SPELUNKER DOWN, OR LOOK DOWN"; break;
680     case GameConfig::Key.Jump: kname = "JUMP"; khelp = "MAKE SPELUNKER JUMP"; break;
681     case GameConfig::Key.Run: kname = "RUN"; khelp = "MAKE SPELUNKER RUN"; break;
682     case GameConfig::Key.Attack: kname = "ATTACK"; khelp = "USE CURRENT ITEM, OR PERFORM AN ATTACK WITH THE CURRENT WEAPON"; break;
683     case GameConfig::Key.Switch: kname = "SWITCH"; khelp = "SWITCH BETWEEN ROPE/BOMB/ITEM"; break;
684     case GameConfig::Key.Pay: kname = "PAY"; khelp = "PAY SHOPKEEPER"; break;
685     case GameConfig::Key.Bomb: kname = "BOMB"; khelp = "DROP AN ARMED BOMB"; break;
686     case GameConfig::Key.Rope: kname = "ROPE"; khelp = "THROW A ROPE"; break;
687     default: return;
688   }
689   int arridx = GameConfig.getKeyIndex(keyidx);
690   UIKeyBinding.Create(pane, &global.config.keybinds[arridx+0], &global.config.keybinds[arridx+1], kname, khelp);
694 final UIPane createBindingsPane () {
695   UIPane pane = SpawnObject(UIPane);
696   pane.id = 'KeyBindings';
697   pane.sprStore = sprStore;
699   pane.width = 320*3-64;
700   pane.height = 240*3-64;
702   createBindingsControl(pane, GameConfig::Key.Left);
703   createBindingsControl(pane, GameConfig::Key.Right);
704   createBindingsControl(pane, GameConfig::Key.Up);
705   createBindingsControl(pane, GameConfig::Key.Down);
706   createBindingsControl(pane, GameConfig::Key.Jump);
707   createBindingsControl(pane, GameConfig::Key.Run);
708   createBindingsControl(pane, GameConfig::Key.Attack);
709   createBindingsControl(pane, GameConfig::Key.Switch);
710   createBindingsControl(pane, GameConfig::Key.Pay);
711   createBindingsControl(pane, GameConfig::Key.Bomb);
712   createBindingsControl(pane, GameConfig::Key.Rope);
714   saveOptionsDG = delegate void () {
715     writeln("saving keys");
716     saveKeyboardBindings();
717   };
718   optionsPaneOfs.x = 120;
719   optionsPaneOfs.y = 140;
721   return pane;
725 // ////////////////////////////////////////////////////////////////////////// //
726 void clearGameMovement () {
727   debugMovement = SpawnObject(DebugSessionMovement);
728   debugMovement.playconfig = SpawnObject(GameConfig);
729   debugMovement.playconfig.copyGameplayConfigFrom(config);
730   debugMovement.resetReplay();
734 void saveGameMovement (string fname, optional bool packit) {
735   if (debugMovement) appSaveOptions(debugMovement, fname, packit);
736   saveMovementLastTime = GetTickCount();
740 void loadGameMovement (string fname) {
741   delete debugMovement;
742   debugMovement = appLoadOptions(DebugSessionMovement, fname);
743   debugMovement.resetReplay();
744   if (debugMovement) {
745     delete origStats;
746     origStats = level.stats;
747     origStats.global = none;
748     level.stats = SpawnObject(GameStats);
749     level.stats.global = global;
750     delete origConfig;
751     origConfig = config;
752     config = debugMovement.playconfig;
753     global.config = config;
754     origRoomSeed = global.globalRoomSeed;
755     origOtherSeed = global.globalOtherSeed;
756     writeln(va("saving seeds: (0x%x, 0x%x)", origRoomSeed, origOtherSeed));
757   }
761 void stopReplaying () {
762   if (debugMovement) {
763     writeln(va("restoring seeds: (0x%x, 0x%x)", origRoomSeed, origOtherSeed));
764     global.globalRoomSeed = origRoomSeed;
765     global.globalOtherSeed = origOtherSeed;
766   }
767   delete debugMovement;
768   saveGameSession = false;
769   replayGameSession = false;
770   doGameSavingPlaying = Replay.None;
771   if (origStats) {
772     delete level.stats;
773     origStats.global = global;
774     level.stats = origStats;
775     origStats = none;
776   }
777   if (origConfig) {
778     delete config;
779     config = origConfig;
780     global.config = origConfig;
781     origConfig = none;
782   }
786 // ////////////////////////////////////////////////////////////////////////// //
787 final bool saveGame (string gmname) {
788   return appSaveOptions(level, gmname);
792 final bool loadGame (string gmname) {
793   auto olddel = ImmediateDelete;
794   ImmediateDelete = false;
795   bool res = false;
796   auto stats = level.stats;
797   level.stats = none;
799   auto lvl = appLoadOptions(GameLevel, gmname);
800   if (lvl) {
801     //lvl.global.config = config;
802     delete level;
803     delete global;
805     level = lvl;
806     global = level.global;
807     global.config = config;
809     level.sprStore = sprStore;
810     level.bgtileStore = bgtileStore;
813     level.onBeforeFrame = &beforeNewFrame;
814     level.onAfterFrame = &afterNewFrame;
815     level.onInterFrame = &interFrame;
816     level.onLevelExitedCB = &levelExited;
817     level.onCameraTeleported = &cameraTeleportedCB;
819     //level.viewWidth = Video.screenWidth;
820     //level.viewHeight = Video.screenHeight;
821     level.viewWidth = 320*3;
822     level.viewHeight = 240*3;
824     level.onLoaded();
825     level.centerViewAtPlayer();
826     teleportCameraAt(level.viewStart);
828     recalcCameraCoords(0);
830     res = true;
831   }
832   level.stats = stats;
833   level.stats.global = level.global;
835   ImmediateDelete = olddel;
836   CollectGarbage(true); // destroy delayed objects too
837   return res;
841 // ////////////////////////////////////////////////////////////////////////// //
842 float lastThinkerTime;
843 int replaySkipFrame = 0;
846 final void onTimePasses () {
847   float curTime = GetTickCount();
848   if (lastThinkerTime > 0) {
849     if (curTime < lastThinkerTime) {
850       writeln("something is VERY wrong with timers! %f %f", curTime, lastThinkerTime);
851       lastThinkerTime = curTime;
852       return;
853     }
854     if (replayFastForward && replaySkipFrame) {
855       level.accumTime = 0;
856       lastThinkerTime = curTime-GameLevel::FrameTime*replayFastForwardSpeed;
857       replaySkipFrame = 0;
858     }
859     level.processThinkers(curTime-lastThinkerTime);
860   }
861   lastThinkerTime = curTime;
865 final void resetFramesAndForceOne () {
866   float curTime = GetTickCount();
867   lastThinkerTime = curTime;
868   level.accumTime = 0;
869   auto wasPaused = level.gamePaused;
870   level.gamePaused = false;
871   if (wasPaused && doGameSavingPlaying != Replay.None) level.keysRestoreState(savedKeyState);
872   level.processThinkers(GameLevel::FrameTime);
873   level.gamePaused = wasPaused;
874   //writeln("level.framesProcessedFromLastClear=", level.framesProcessedFromLastClear);
878 // ////////////////////////////////////////////////////////////////////////// //
879 private float currFrameDelta; // so level renderer can properly interpolate the player
880 private GameLevel::IVec2D camPrev, camCurr;
881 private GameLevel::IVec2D camShake;
882 private GameLevel::IVec2D viewCameraPos;
885 final void teleportCameraAt (const ref GameLevel::IVec2D pos) {
886   camPrev.x = pos.x;
887   camPrev.y = pos.y;
888   camCurr.x = pos.x;
889   camCurr.y = pos.y;
890   viewCameraPos.x = pos.x;
891   viewCameraPos.y = pos.y;
892   camShake.x = 0;
893   camShake.y = 0;
897 // call `recalcCameraCoords()` to get real camera coords after this
898 final void setNewCameraPos (const ref GameLevel::IVec2D pos, optional bool doTeleport) {
899   // check if camera is moved too far, and teleport it
900   if (doTeleport ||
901       (abs(camCurr.x-pos.x)/global.scale >= 16*4 ||
902        abs(camCurr.y-pos.y)/global.scale >= 16*4))
903   {
904     teleportCameraAt(pos);
905   } else {
906     camPrev.x = camCurr.x;
907     camPrev.y = camCurr.y;
908     camCurr.x = pos.x;
909     camCurr.y = pos.y;
910   }
911   camShake.x = level.shakeDir.x*global.scale;
912   camShake.y = level.shakeDir.y*global.scale;
916 final void recalcCameraCoords (float frameDelta, optional bool moveSounds) {
917   currFrameDelta = frameDelta;
918   viewCameraPos.x = round(camPrev.x+(camCurr.x-camPrev.x)*frameDelta);
919   viewCameraPos.y = round(camPrev.y+(camCurr.y-camPrev.y)*frameDelta);
921   viewCameraPos.x += camShake.x;
922   viewCameraPos.y += camShake.y;
926 GameLevel::SavedKeyState savedKeyState;
928 final void pauseGame () {
929   if (!level.gamePaused) {
930     if (doGameSavingPlaying != Replay.None) level.keysSaveState(savedKeyState);
931     level.gamePaused = true;
932     global.pauseAllSounds();
933   }
937 final void unpauseGame () {
938   if (level.gamePaused) {
939     if (doGameSavingPlaying != Replay.None) level.keysRestoreState(savedKeyState);
940     level.gamePaused = false;
941     level.gameShowHelp = false;
942     level.gameHelpScreen = 0;
943     //lastThinkerTime = 0;
944     global.resumeAllSounds();
945   }
946   pauseRequested = false;
947   helpRequested = false;
948   showHelp = false;
952 final void beforeNewFrame (bool frameSkip) {
953   /*
954   if (freeRide) {
955     level.disablePlayerThink = true;
957     int delta = 2;
958     if (level.isKeyDown(GameConfig::Key.Attack)) delta *= 2;
959     if (level.isKeyDown(GameConfig::Key.Jump)) delta *= 4;
960     if (level.isKeyDown(GameConfig::Key.Run)) delta /= 2;
962     if (level.isKeyDown(GameConfig::Key.Left)) level.viewStart.x -= delta;
963     if (level.isKeyDown(GameConfig::Key.Right)) level.viewStart.x += delta;
964     if (level.isKeyDown(GameConfig::Key.Up)) level.viewStart.y -= delta;
965     if (level.isKeyDown(GameConfig::Key.Down)) level.viewStart.y += delta;
966   } else {
967     level.disablePlayerThink = false;
968     level.fixCamera();
969   }
970   */
971   level.fixCamera();
973   if (!level.gamePaused) {
974     // save seeds for afterframe processing
975     /*
976     if (doGameSavingPlaying == Replay.Saving && debugMovement) {
977       debugMovement.otherSeed = global.globalOtherSeed;
978       debugMovement.roomSeed = global.globalRoomSeed;
979     }
980     */
982     if (doGameSavingPlaying == Replay.Replaying && !debugMovement) stopReplaying();
984 #ifdef BIGGER_REPLAY_DATA
985     if (doGameSavingPlaying == Replay.Saving && debugMovement) {
986       debugMovement.keypresses.length += 1;
987       level.keysSaveState(debugMovement.keypresses[$-1]);
988       debugMovement.keypresses[$-1].otherSeed = global.globalOtherSeed;
989       debugMovement.keypresses[$-1].roomSeed = global.globalRoomSeed;
990     }
991 #endif
993     if (doGameSavingPlaying == Replay.Replaying && debugMovement) {
994 #ifdef BIGGER_REPLAY_DATA
995       if (debugMovement.keypos < debugMovement.keypresses.length) {
996         level.keysRestoreState(debugMovement.keypresses[debugMovement.keypos]);
997         global.globalOtherSeed = debugMovement.keypresses[debugMovement.keypos].otherSeed;
998         global.globalRoomSeed = debugMovement.keypresses[debugMovement.keypos].roomSeed;
999         ++debugMovement.keypos;
1000       }
1001 #else
1002       for (;;) {
1003         int kbidx;
1004         bool down;
1005         auto code = debugMovement.getKey(out kbidx, out down);
1006         if (code == DebugSessionMovement::END_OF_RECORD) {
1007           // do this in main loop, so we can view totals
1008           //stopReplaying();
1009           break;
1010         }
1011         if (code == DebugSessionMovement::END_OF_FRAME) {
1012           break;
1013         }
1014         if (code != DebugSessionMovement::NORMAL) FatalError("UNKNOWN REPLAY CODE");
1015         level.onKey(1<<(kbidx/GameConfig::MaxActionBinds), down);
1016       }
1017 #endif
1018     }
1019   }
1023 final void afterNewFrame (bool frameSkip) {
1024   if (!replayFastForward) replaySkipFrame = 0;
1026   if (level.gamePaused) return;
1028   if (!level.gamePaused) {
1029     if (doGameSavingPlaying != Replay.None) {
1030       if (doGameSavingPlaying == Replay.Saving) {
1031         replayFastForward = false; // just in case
1032 #ifndef BIGGER_REPLAY_DATA
1033         debugMovement.addEndOfFrame();
1034 #endif
1035         auto stt = GetTickCount();
1036         if (stt-saveMovementLastTime >= 20) saveGameMovement(dbgSessionMovementFileName);
1037       } else if (doGameSavingPlaying == Replay.Replaying) {
1038         if (!frameSkip && replayFastForward && replaySkipFrame == 0) {
1039           replaySkipFrame = 1;
1040         }
1041       }
1042     }
1043   }
1045   //SoundSystem.ListenerOrigin = vector(level.player.fltx, level.player.flty);
1046   //SoundSystem.UpdateSounds();
1048   //if (!freeRide) level.fixCamera();
1049   setNewCameraPos(level.viewStart);
1050   /*
1051   prevCameraX = currCameraX;
1052   prevCameraY = currCameraY;
1053   currCameraX = level.cameraX;
1054   currCameraY = level.cameraY;
1055   // disable camera interpolation if the screen is shaking
1056   if (level.shakeX|level.shakeY) {
1057     prevCameraX = currCameraX;
1058     prevCameraY = currCameraY;
1059     return;
1060   }
1061   // disable camera interpolation if it moves too far away
1062   if (fabs(prevCameraX-currCameraX) > 64) prevCameraX = currCameraX;
1063   if (fabs(prevCameraY-currCameraY) > 64) prevCameraY = currCameraY;
1064   */
1065   recalcCameraCoords(config.interpolateMovement ? 0.0 : 1.0, moveSounds:true); // recalc camera coords
1067   if (pauseRequested && level.framesProcessedFromLastClear > 1) {
1068     pauseRequested = false;
1069     pauseGame();
1070     if (helpRequested) {
1071       helpRequested = false;
1072       level.gameShowHelp = true;
1073       level.gameHelpScreen = 0;
1074       showHelp = 2;
1075     } else {
1076       if (!showHelp) showHelp = true;
1077     }
1078     return;
1079   }
1083 final void interFrame (float frameDelta) {
1084   if (!config.interpolateMovement) return;
1085   recalcCameraCoords(frameDelta);
1089 final void cameraTeleportedCB () {
1090   teleportCameraAt(level.viewStart);
1091   recalcCameraCoords(0);
1095 // ////////////////////////////////////////////////////////////////////////// //
1096 #ifdef MASK_TEST
1097 final void setColorByIdx (bool isset, int col) {
1098   if (col == -666) {
1099     // missed collision: red
1100     Video.color = (isset ? 0x3f_ff_00_00 : 0xcf_ff_00_00);
1101   } else if (col == -999) {
1102     // superfluous collision: blue
1103     Video.color = (isset ? 0x3f_00_00_ff : 0xcf_00_00_ff);
1104   } else if (col <= 0) {
1105     // no collision: yellow
1106     Video.color = (isset ? 0x3f_ff_ff_00 : 0xcf_ff_ff_00);
1107   } else if (col > 0) {
1108     // collision: green
1109     Video.color = (isset ? 0x3f_00_ff_00 : 0xcf_00_ff_00);
1110   }
1114 final void drawMaskSimple (SpriteFrame frm, int xofs, int yofs) {
1115   if (!frm) return;
1116   CollisionMask cm = CollisionMask.Create(frm, false);
1117   if (!cm) return;
1118   int scale = global.config.scale;
1119   int bx0, by0, bx1, by1;
1120   frm.getBBox(out bx0, out by0, out bx1, out by1, false);
1121   Video.color = 0x7f_00_00_ff;
1122   Video.fillRect(xofs+bx0*scale, yofs+by0*scale, (bx1-bx0+1)*scale, (by1-by0+1)*scale);
1123   if (!cm.isEmptyMask) {
1124     //writeln(cm.mask.length, "; ", cm.width, "x", cm.height, "; (", cm.x0, ",", cm.y0, ")-(", cm.x1, ",", cm.y1, ")");
1125     foreach (int iy; 0..cm.height) {
1126       foreach (int ix; 0..cm.width) {
1127         int v = cm.mask[ix, iy];
1128         foreach (int dx; 0..32) {
1129           int xx = ix*32+dx;
1130           if (v < 0) {
1131             Video.color = 0x3f_00_ff_00;
1132             Video.fillRect(xofs+xx*scale, yofs+iy*scale, scale, scale);
1133           }
1134           v <<= 1;
1135         }
1136       }
1137     }
1138   } else {
1139     // bounding box
1140     /+
1141     foreach (int iy; 0..frm.tex.height) {
1142       foreach (int ix; 0..(frm.tex.width+31)/31) {
1143         foreach (int dx; 0..32) {
1144           int xx = ix*32+dx;
1145           //if (xx >= frm.bx && xx < frm.bx+frm.bw && iy >= frm.by && iy < frm.by+frm.bh) {
1146           if (xx >= x0 && xx <= x1 && iy >= y0 && iy <= y1) {
1147             setColorByIdx(true, col);
1148             if (col <= 0) Video.color = 0xaf_ff_ff_00;
1149           } else {
1150             Video.color = 0xaf_00_ff_00;
1151           }
1152           Video.fillRect(sx+xx*scale, sy+iy*scale, scale, scale);
1153         }
1154       }
1155     }
1156     +/
1157     /*
1158     if (frm.bw > 0 && frm.bh > 0) {
1159       setColorByIdx(true, col);
1160       Video.fillRect(x0+frm.bx*scale, y0+frm.by*scale, frm.bw*scale, frm.bh*scale);
1161       Video.color = 0xff_00_00;
1162       Video.drawRect(x0+frm.bx*scale, y0+frm.by*scale, frm.bw*scale, frm.bh*scale);
1163     }
1164     */
1165   }
1166   delete cm;
1168 #endif
1171 // ////////////////////////////////////////////////////////////////////////// //
1172 transient int drawStats;
1173 transient array!int statsTopItem;
1176 final bool totalsNameCmpCB (ref GameStats::TotalItem a, ref GameStats::TotalItem b) {
1177   auto sa = string(a.objName).toUpperCase;
1178   auto sb = string(b.objName).toUpperCase;
1179   return (sa < sb);
1183 final int getStatsTopItem () {
1184   return max(0, (drawStats >= 0 && drawStats < statsTopItem.length ? statsTopItem[drawStats] : 0));
1188 final void setStatsTopItem (int val) {
1189   if (drawStats <= statsTopItem.length) statsTopItem.length = drawStats+1;
1190   statsTopItem[drawStats] = val;
1194 final void resetStatsTopItem () {
1195   setStatsTopItem(0);
1199 void statsDrawGetStartPosLoadFont (out int currX, out int currY) {
1200   sprStore.loadFont('sFontSmall');
1201   currX = 64;
1202   currY = 34;
1206 final int calcStatsVisItems () {
1207   int scale = 3;
1208   int currX, currY;
1209   statsDrawGetStartPosLoadFont(currX, currY);
1210   int endY = level.viewHeight-(currY*2);
1211   return max(1, endY/sprStore.getFontHeight(scale));
1215 int getStatsItemCount () {
1216   switch (drawStats) {
1217     case 2: return level.stats.totalKills.length;
1218     case 3: return level.stats.totalDeaths.length;
1219     case 4: return level.stats.totalCollected.length;
1220   }
1221   return -1;
1225 final void statsMoveUp () {
1226   int count = getStatsItemCount();
1227   if (count < 0) return;
1228   int visItems = calcStatsVisItems();
1229   if (count <= visItems) { resetStatsTopItem(); return; }
1230   int top = getStatsTopItem();
1231   if (!top) return;
1232   setStatsTopItem(top-1);
1236 final void statsMoveDown () {
1237   int count = getStatsItemCount();
1238   if (count < 0) return;
1239   int visItems = calcStatsVisItems();
1240   if (count <= visItems) { resetStatsTopItem(); return; }
1241   int top = getStatsTopItem();
1242   //writeln("top=", top, "; count=", count, "; visItems=", visItems, "; maxtop=", count-visItems+1);
1243   top = clamp(top+1, 0, count-visItems);
1244   setStatsTopItem(top);
1248 void drawTotalsList (string pfx, ref array!(GameStats::TotalItem) arr) {
1249   arr.sort(&totalsNameCmpCB);
1250   int scale = 3;
1252   int currX, currY;
1253   statsDrawGetStartPosLoadFont(currX, currY);
1255   int endY = level.viewHeight-(currY*2);
1256   int visItems = calcStatsVisItems();
1258   if (arr.length <= visItems) resetStatsTopItem();
1260   int topItem = getStatsTopItem();
1262   // "upscroll" mark
1263   if (topItem > 0) {
1264     Video.color = 0x3f_ff_ff_00;
1265     auto spr = sprStore['sPageUp'];
1266     spr.frames[0].tex.blitAt(currX-28, currY, scale);
1267   }
1269   // "downscroll" mark
1270   if (topItem+visItems < arr.length) {
1271     Video.color = 0x3f_ff_ff_00;
1272     auto spr = sprStore['sPageDown'];
1273     spr.frames[0].tex.blitAt(currX-28, endY+3/*-sprStore.getFontHeight(scale)*/, scale);
1274   }
1276   Video.color = 0xff_ff_00;
1277   int hiColor = 0x00_ff_00;
1278   int hiColor1 = 0xf_ff_ff;
1280   int it = topItem;
1281   while (it < arr.length && visItems-- > 0) {
1282     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);
1283     currY += sprStore.getFontHeight(scale);
1284     ++it;
1285   }
1289 void drawStatsScreen () {
1290   int deathCount, killCount, collectCount;
1292   sprStore.loadFont('sFontSmall');
1294   Video.color = 0xff_ff_ff;
1295   level.drawTextAtS3Centered(240-2-8, "ESC-RETURN  F10-QUIT  CTRL+DEL-SUICIDE");
1296   level.drawTextAtS3Centered(2, "~O~PTIONS  REDEFINE ~K~EYS  ~S~TATISTICS", 0xff_7f_00);
1298   Video.color = 0xff_ff_00;
1299   int hiColor = 0x00_ff_00;
1301   switch (drawStats) {
1302     case 2: drawTotalsList("KILLED", level.stats.totalKills); return;
1303     case 3: drawTotalsList("DIED FROM", level.stats.totalDeaths); return;
1304     case 4: drawTotalsList("COLLECTED", level.stats.totalCollected); return;
1305   }
1307   if (drawStats > 1) {
1308     // turn off
1309     foreach (ref auto i; statsTopItem) i = 0;
1310     drawStats = 0;
1311     return;
1312   }
1314   foreach (ref auto ti; level.stats.totalDeaths) deathCount += ti.count;
1315   foreach (ref auto ti; level.stats.totalKills) killCount += ti.count;
1316   foreach (ref auto ti; level.stats.totalCollected) collectCount += ti.count;
1318   int currX = 64;
1319   int currY = 96;
1320   int scale = 3;
1322   sprStore.renderTextWithHighlight(currX, currY, va("MAXIMUM MONEY YOU GOT IS ~%d~", level.stats.maxMoney), scale, hiColor);
1323   currY += sprStore.getFontHeight(scale);
1325   int gw = level.stats.gamesWon;
1326   sprStore.renderTextWithHighlight(currX, currY, va("YOU WON ~%d~ GAME%s", gw, (gw != 1 ? "S" : "")), scale, hiColor);
1327   currY += sprStore.getFontHeight(scale);
1329   sprStore.renderTextWithHighlight(currX, currY, va("YOU DIED ~%d~ TIMES", deathCount), scale, hiColor);
1330   currY += sprStore.getFontHeight(scale);
1332   sprStore.renderTextWithHighlight(currX, currY, va("YOU KILLED ~%d~ CREATURES", killCount), scale, hiColor);
1333   currY += sprStore.getFontHeight(scale);
1335   sprStore.renderTextWithHighlight(currX, currY, va("YOU COLLECTED ~%d~ TREASURE ITEMS", collectCount), scale, hiColor);
1336   currY += sprStore.getFontHeight(scale);
1338   sprStore.renderTextWithHighlight(currX, currY, va("YOU SAVED ~%d~ DAMSELS", level.stats.totalDamselsSaved), scale, hiColor);
1339   currY += sprStore.getFontHeight(scale);
1341   sprStore.renderTextWithHighlight(currX, currY, va("YOU STOLE ~%d~ IDOLS", level.stats.totalIdolsStolen), scale, hiColor);
1342   currY += sprStore.getFontHeight(scale);
1344   sprStore.renderTextWithHighlight(currX, currY, va("YOU SOLD ~%d~ IDOLS", level.stats.totalIdolsConverted), scale, hiColor);
1345   currY += sprStore.getFontHeight(scale);
1347   sprStore.renderTextWithHighlight(currX, currY, va("YOU STOLE ~%d~ CRYSTAL SKULLS", level.stats.totalCrystalIdolsStolen), scale, hiColor);
1348   currY += sprStore.getFontHeight(scale);
1350   sprStore.renderTextWithHighlight(currX, currY, va("YOU SOLD ~%d~ CRYSTAL SKULLS", level.stats.totalCrystalIdolsConverted), scale, hiColor);
1351   currY += sprStore.getFontHeight(scale);
1353   int gs = level.stats.totalGhostSummoned;
1354   sprStore.renderTextWithHighlight(currX, currY, va("YOU SUMMONED ~%d~ GHOST%s", gs, (gs != 1 ? "S" : "")), scale, hiColor);
1355   currY += sprStore.getFontHeight(scale);
1357   currY += sprStore.getFontHeight(scale);
1358   sprStore.renderTextWithHighlight(currX, currY, va("TOTAL PLAYING TIME: ~%s~", GameLevel.time2str(level.stats.playingTime)), scale, hiColor);
1359   currY += sprStore.getFontHeight(scale);
1363 void onDraw () {
1364   if (Video.frameTime == 0) {
1365     onTimePasses();
1366     Video.requestRefresh();
1367   }
1369   if (!level) return;
1371   if (level.framesProcessedFromLastClear < 1) return;
1372   calcMouseMapCoords();
1374   Video.stencil = true; // you NEED this to be set! (stencil buffer is used for lighting)
1375   Video.clearScreen();
1376   Video.stencil = false;
1377   Video.color = 0xff_ff_ff;
1378   Video.textureFiltering = false;
1379   // don't touch framebuffer alpha
1380   Video.colorMask = Video::CMask.Colors;
1382   Video::ScissorRect scsave;
1383   bool doRestoreGL = false;
1385   /*
1386   if (level.viewOffsetX > 0 || level.viewOffsetY > 0) {
1387     doRestoreGL = true;
1388     Video.getScissor(scsave);
1389     Video.scissorCombine(level.viewOffsetX, level.viewOffsetY, level.viewWidth, level.viewHeight);
1390     Video.glPushMatrix();
1391     Video.glTranslate(level.viewOffsetX, level.viewOffsetY);
1392   }
1393   */
1395   if (level.viewWidth != Video.screenWidth || level.viewHeight != Video.screenHeight) {
1396     doRestoreGL = true;
1397     float scx = float(Video.screenWidth)/float(level.viewWidth);
1398     float scy = float(Video.screenHeight)/float(level.viewHeight);
1399     float scale = fmin(scx, scy);
1400     int calcedW = trunc(level.viewWidth*scale);
1401     int calcedH = trunc(level.viewHeight*scale);
1402     Video.getScissor(scsave);
1403     int ofsx = (Video.screenWidth-calcedW)/2;
1404     int ofsy = (Video.screenHeight-calcedH)/2;
1405     Video.scissorCombine(ofsx, ofsy, calcedW, calcedH);
1406     Video.glPushMatrix();
1407     Video.glTranslate(ofsx, ofsy);
1408     Video.glScale(scale, scale);
1409   }
1411   //level.viewOffsetX = (Video.screenWidth-320*3)/2;
1412   //level.viewOffsetY = (Video.screenHeight-240*3)/2;
1414   if (fullscreen) {
1415     /*
1416     level.viewOffsetX = 0;
1417     level.viewOffsetY = 0;
1418     Video.glScale(float(Video.screenWidth)/float(level.viewWidth), float(Video.screenHeight)/float(level.viewHeight));
1419     */
1420     /*
1421     float scx = float(Video.screenWidth)/float(level.viewWidth);
1422     float scy = float(Video.screenHeight)/float(level.viewHeight);
1423     Video.glScale(float(Video.screenWidth)/float(level.viewWidth), 1);
1424     */
1425   }
1428   level.renderWithOfs(viewCameraPos.x, viewCameraPos.y, currFrameDelta);
1430   if (level.gamePaused && showHelp != 2) {
1431     if (mouseLevelX != int.min) {
1432       int scale = level.global.scale;
1433       if (renderMouseRect) {
1434         Video.color = 0xcf_ff_ff_00;
1435         Video.fillRect(mouseLevelX*scale-viewCameraPos.x, mouseLevelY*scale-viewCameraPos.y, 12*scale, 14*scale);
1436       }
1437       if (renderMouseTile) {
1438         Video.color = 0xaf_ff_00_00;
1439         Video.fillRect((mouseLevelX&~15)*scale-viewCameraPos.x, (mouseLevelY&~15)*scale-viewCameraPos.y, 16*scale, 16*scale);
1440       }
1441     }
1442   }
1444   switch (doGameSavingPlaying) {
1445     case Replay.Saving:
1446       Video.color = 0x7f_00_ff_00;
1447       sprStore.loadFont('sFont');
1448       sprStore.renderText(level.viewWidth-sprStore.getTextWidth("S", 2)-2, 2, "S", 2);
1449       break;
1450     case Replay.Replaying:
1451       if (level.player && !level.player.dead) {
1452         Video.color = 0x7f_ff_00_00;
1453         sprStore.loadFont('sFont');
1454         sprStore.renderText(level.viewWidth-sprStore.getTextWidth("R", 2)-2, 2, "R", 2);
1455         int th = sprStore.getFontHeight(2);
1456         if (replayFastForward) {
1457           sprStore.loadFont('sFontSmall');
1458           string sstr = va("x%d", replayFastForwardSpeed+1);
1459           sprStore.renderText(level.viewWidth-sprStore.getTextWidth(sstr, 2)-2, 2+th, sstr, 2);
1460         }
1461       }
1462       break;
1463     default:
1464       if (saveGameSession) {
1465         Video.color = 0x7f_ff_7f_00;
1466         sprStore.loadFont('sFont');
1467         sprStore.renderText(level.viewWidth-sprStore.getTextWidth("S", 2)-2, 2, "S", 2);
1468       }
1469       break;
1470   }
1473   if (level.player && level.player.dead && !showHelp) {
1474     // darken
1475     Video.color = 0x8f_00_00_00;
1476     Video.fillRect(0, 0, level.viewWidth, level.viewHeight);
1477     // draw text
1478     if (drawStats) {
1479       drawStatsScreen();
1480     } else {
1481       if (true /*level.inWinCutscene == 0*/) {
1482         Video.color = 0xff_ff_ff;
1483         sprStore.loadFont('sFontSmall');
1484         string kmsg = va((level.stats.newMoneyRecord ? "NEW HIGH SCORE: |%d|\n" : "SCORE: |%d|\n")~
1485                          "\n"~
1486                          "PRESS $PAY TO RESTART GAME\n"~
1487                          "\n"~
1488                          "PRESS ~ESCAPE~ TO EXIT TO TITLE\n"~
1489                          "\n"~
1490                          "TOTAL PLAYING TIME: |%s|"~
1491                          "",
1492                          (level.levelKind == GameLevel::LevelKind.Stars ? level.starsKills :
1493                           level.levelKind == GameLevel::LevelKind.Sun ? level.sunScore :
1494                           level.levelKind == GameLevel::LevelKind.Moon ? level.moonScore :
1495                           level.stats.money),
1496                          GameLevel.time2str(level.stats.playingTime)
1497                         );
1498         kmsg = global.expandString(kmsg);
1499         sprStore.renderMultilineTextCentered(level.viewWidth/2, -level.viewHeight, kmsg, 3, 0x00_ff_00, 0x00_ff_ff);
1500       }
1501     }
1502   }
1504 #ifdef MASK_TEST
1505   {
1506     Video.color = 0xff_7f_00;
1507     sprStore.loadFont('sFontSmall');
1508     sprStore.renderText(8, level.viewHeight-20, va("%s; FRAME:%d", (smask.precise ? "PRECISE" : "HITBOX"), maskFrame), 2);
1509     auto spf = smask.frames[maskFrame];
1510     sprStore.renderText(8, level.viewHeight-20-16, va("OFS=(%d,%d); BB=(%d,%d)x(%d,%d); EMPTY:%s; PRECISE:%s",
1511       spf.xofs, spf.yofs,
1512       spf.bx, spf.by, spf.bw, spf.bh,
1513       (spf.maskEmpty ? "TAN" : "ONA"),
1514       (spf.precise ? "TAN" : "ONA")),
1515       2
1516     );
1517     //spf.tex.blitAt(maskSX*global.config.scale-viewCameraPos.x, maskSY*global.config.scale-viewCameraPos.y, global.config.scale);
1518     //writeln("pos=(", maskSX, ",", maskSY, ")");
1519     int scale = global.config.scale;
1520     int xofs = viewCameraPos.x, yofs = viewCameraPos.y;
1521     int mapX = xofs/scale+maskSX;
1522     int mapY = yofs/scale+maskSY;
1523     mapX -= spf.xofs;
1524     mapY -= spf.yofs;
1525     writeln("==== tiles ====");
1526     /*
1527     level.touchTilesWithMask(mapX, mapY, spf, delegate bool (MapTile t) {
1528       if (t.spectral || !t.isInstanceAlive) return false;
1529       Video.color = 0x7f_ff_00_00;
1530       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);
1531       auto tsf = t.getSpriteFrame();
1533       auto spf = smask.frames[maskFrame];
1534       int xofs = viewCameraPos.x, yofs = viewCameraPos.y;
1535       int mapX = xofs/global.config.scale+maskSX;
1536       int mapY = yofs/global.config.scale+maskSY;
1537       mapX -= spf.xofs;
1538       mapY -= spf.yofs;
1539       //bool hit = spf.pixelCheck(tsf, t.ix-mapX, t.iy-mapY);
1540       bool hit = tsf.pixelCheck(spf, mapX-t.ix, mapY-t.iy);
1541       writeln("  tile '", t.objName, "': precise=", tsf.precise, "; hit=", hit);
1542       return false;
1543     });
1544     */
1545     level.touchObjectsWithMask(mapX, mapY, spf, delegate bool (MapObject t) {
1546       Video.color = 0x7f_ff_00_00;
1547       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);
1548       return false;
1549     });
1550     //
1551     drawMaskSimple(spf, mapX*scale-xofs, mapY*scale-yofs);
1552     // mask
1553     Video.color = 0xaf_ff_ff_ff;
1554     spf.tex.blitAt(mapX*scale-xofs, mapY*scale-yofs, scale);
1555     Video.color = 0xff_ff_00;
1556     Video.drawRect((mapX+spf.bx)*scale-xofs, (mapY+spf.by)*scale-yofs, spf.bw*scale, spf.bh*scale);
1557     // player colbox
1558     {
1559       bool doMirrorSelf;
1560       int fx0, fy0, fx1, fy1;
1561       auto pfm = level.player.getSpriteFrame(out doMirrorSelf, out fx0, out fy0, out fx1, out fy1);
1562       Video.color = 0x7f_00_00_ff;
1563       Video.fillRect((level.player.ix+fx0)*scale-xofs, (level.player.iy+fy0)*scale-yofs, (fx1-fx0)*scale, (fy1-fy0)*scale);
1564     }
1565   }
1566 #endif
1568   if (showHelp) {
1569     Video.color = 0x8f_00_00_00;
1570     Video.fillRect(0, 0, level.viewWidth, level.viewHeight);
1571     if (optionsPane) {
1572       optionsPane.drawWithOfs(optionsPaneOfs.x+32, optionsPaneOfs.y+32);
1573     } else {
1574       if (drawStats) {
1575         drawStatsScreen();
1576       } else {
1577         Video.color = 0xff_ff_00;
1578         //if (showHelp > 1) Video.color = 0xaf_ff_ff_00;
1579         if (showHelp == 1) {
1580           sprStore.loadFont('sFontSmall');
1581           sprStore.renderTextWrapped(16, 16, (320-16)*2,
1582             "F1: show this help\n"~
1583             "O : options\n"~
1584             "K : redefine keys\n"~
1585             "I : toggle interpolaion\n"~
1586             "N : create some blood\n"~
1587             "R : generate a new level\n"~
1588             "F : toggle \"Frozen Area\"\n"~
1589             "X : resurrect player\n"~
1590             "Q : teleport to exit\n"~
1591             "D : teleport to damel\n"~
1592             "--------------\n"~
1593             "C : cheat flags menu\n"~
1594             "P : cheat pickup menu\n"~
1595             "E : cheat enemy menu\n"~
1596             "Enter: cheat items menu\n"~
1597             "\n"~
1598             "TAB: toggle 'freeroam' mode\n"~
1599             "",
1600             2);
1601         } else {
1602           if (level) level.renderPauseOverlay();
1603         }
1604       }
1605     }
1606     //SoundSystem.UpdateSounds();
1607   }
1608   //sprStore.renderText(16, 16, "SPELUNKY!", 2);
1610   if (doRestoreGL) {
1611     Video.setScissor(scsave);
1612     Video.glPopMatrix();
1613   }
1616   if (TigerEye) {
1617     Video.color = 0xaf_ff_ff_ff;
1618     texTigerEye.blitAt(Video.screenWidth-texTigerEye.width-2, Video.screenHeight-texTigerEye.height-2);
1619   }
1623 // ////////////////////////////////////////////////////////////////////////// //
1624 transient bool gameJustOver;
1625 transient bool waitingForPayRestart;
1628 final void calcMouseMapCoords () {
1629   if (mouseX == int.min || !level || level.framesProcessedFromLastClear < 1) {
1630     mouseLevelX = int.min;
1631     mouseLevelY = int.min;
1632     return;
1633   }
1634   mouseLevelX = (mouseX+viewCameraPos.x)/level.global.scale;
1635   mouseLevelY = (mouseY+viewCameraPos.y)/level.global.scale;
1636   //writeln("mappos: (", mouseLevelX, ",", mouseLevelY, ")");
1640 final void onEvent (ref event_t evt) {
1641   if (evt.type == ev_closequery) { Video.requestQuit(); return; }
1643   if (evt.type == ev_winfocus) {
1644     if (level && !evt.focused) {
1645       escCount = 0;
1646       level.clearKeys();
1647     }
1648     if (evt.focused) {
1649       //writeln("FOCUS!");
1650       Video.getMousePos(out mouseX, out mouseY);
1651     }
1652     return;
1653   }
1655   if (evt.type == ev_mouse) {
1656     mouseX = evt.x;
1657     mouseY = evt.y;
1658     calcMouseMapCoords();
1659   }
1661   if (evt.type == ev_keydown && evt.keycode == K_F12) {
1662     if (level) toggleFullscreen();
1663     return;
1664   }
1666   if (level && level.gamePaused && showHelp != 2 && evt.type == ev_keydown && evt.keycode == K_MOUSE2 && mouseLevelX != int.min) {
1667     writeln("TILE: ", mouseLevelX/16, ",", mouseLevelY/16);
1668     writeln("MAP : ", mouseLevelX, ",", mouseLevelY);
1669   }
1671   if (evt.type == ev_keydown) {
1672     if (evt.keycode == K_LCTRL || evt.keycode == K_RCTRL) evt.bCtrl = true;
1673     if (evt.keycode == K_LALT || evt.keycode == K_RALT) evt.bAlt = true;
1674     renderMouseTile = evt.bCtrl;
1675     renderMouseRect = evt.bAlt;
1676   }
1678   if (evt.type == ev_keyup) {
1679     if (evt.keycode == K_LCTRL || evt.keycode == K_RCTRL) evt.bCtrl = false;
1680     if (evt.keycode == K_LALT || evt.keycode == K_RALT) evt.bAlt = false;
1681     renderMouseTile = evt.bCtrl;
1682     renderMouseRect = evt.bAlt;
1683   }
1685   if (evt.type == ev_keyup && evt.keycode != K_ESCAPE) escCount = 0;
1687   if (evt.type == ev_keydown && evt.bShift && (evt.keycode >= "1" && evt.keycode <= "4")) {
1688     int newScale = evt.keycode-48;
1689     if (global.config.scale != newScale) {
1690       global.config.scale = newScale;
1691       if (level) {
1692         level.fixCamera();
1693         cameraTeleportedCB();
1694       }
1695     }
1696     return;
1697   }
1699 #ifdef MASK_TEST
1700   if (evt.type == ev_mouse) {
1701     maskSX = evt.x/global.config.scale;
1702     maskSY = evt.y/global.config.scale;
1703     return;
1704   }
1705   if (evt.type == ev_keydown && evt.keycode == K_PADMINUS) {
1706     maskFrame = max(0, maskFrame-1);
1707     return;
1708   }
1709   if (evt.type == ev_keydown && evt.keycode == K_PADPLUS) {
1710     maskFrame = clamp(maskFrame+1, 0, smask.frames.length-1);
1711     return;
1712   }
1713 #endif
1715   if (showHelp) {
1716     escCount = 0;
1718     if (optionsPane) {
1719       if (optionsPane.closeMe || (evt.type == ev_keyup && evt.keycode == K_ESCAPE)) {
1720         saveCurrentPane();
1721         if (saveOptionsDG) saveOptionsDG();
1722         saveOptionsDG = none;
1723         delete optionsPane;
1724         //SoundSystem.UpdateSounds(); // just in case
1725         if (global.hasSpectacles) level.pickedSpectacles();
1726         return;
1727       }
1728       optionsPane.onEvent(evt);
1729       return;
1730     }
1732     if (evt.type == ev_keyup && evt.keycode == K_ESCAPE) { unpauseGame(); return; }
1733     if (evt.type == ev_keydown) {
1734       if (evt.keycode == K_SPACE && level && showHelp == 2 && level.gameShowHelp) evt.keycode = K_RIGHTARROW;
1735       switch (evt.keycode) {
1736         case K_F1: if (showHelp == 2 && level) level.gameShowHelp = !level.gameShowHelp; if (level.gameShowHelp) level.gameHelpScreen = 0; return;
1737         case K_F2: if (showHelp != 2) unpauseGame(); return;
1738         case K_F10: Video.requestQuit(); return;
1739         case K_F11: if (showHelp != 2) showHelp = 3-showHelp; return;
1741         case K_UPARROW: case K_PAD8:
1742           if (drawStats) statsMoveUp();
1743           return;
1745         case K_DOWNARROW: case K_PAD2:
1746           if (drawStats) statsMoveDown();
1747           return;
1749         case K_LEFTARROW: case K_PAD4:
1750           if (level && showHelp == 2 && level.gameShowHelp) {
1751             if (level.gameHelpScreen) --level.gameHelpScreen; else level.gameHelpScreen = GameLevel::MaxGameHelpScreen;
1752           }
1753           return;
1755         case K_RIGHTARROW: case K_PAD6:
1756           if (level && showHelp == 2 && level.gameShowHelp) {
1757             level.gameHelpScreen = (level.gameHelpScreen+1)%(GameLevel::MaxGameHelpScreen+1);
1758           }
1759           return;
1761         case K_F6: {
1762           // save level
1763           saveGame("level");
1764           unpauseGame();
1765           return;
1766         }
1768         case K_F9: {
1769           // load level
1770           loadGame("level");
1771           resetFramesAndForceOne();
1772           unpauseGame();
1773           return;
1774         }
1776         case K_F5:
1777           if (/*evt.bCtrl &&*/ showHelp != 2) {
1778             global.plife = 99;
1779             unpauseGame();
1780           }
1781           return;
1783         case K_s:
1784           ++drawStats;
1785           return;
1787         case K_o: optionsPane = createOptionsPane(); restoreCurrentPane(); return;
1788         case K_k: optionsPane = createBindingsPane(); restoreCurrentPane(); return;
1789         case K_c: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatFlagsPane(); restoreCurrentPane(); } return;
1790         case K_p: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatPickupsPane(); restoreCurrentPane(); } return;
1791         case K_ENTER: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatItemsPane(); restoreCurrentPane(); } return;
1792         case K_e: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatEnemiesPane(); restoreCurrentPane(); } return;
1793         //case K_s: global.hasSpringShoes = !global.hasSpringShoes; return;
1794         //case K_j: global.hasJordans = !global.hasJordans; return;
1795         case K_x:
1796           if (/*evt.bCtrl &&*/ showHelp != 2) {
1797             level.resurrectPlayer();
1798             unpauseGame();
1799           }
1800           return;
1801         case K_r:
1802           //writeln("*** ROOM  SEED: ", global.globalRoomSeed);
1803           //writeln("*** OTHER SEED: ", global.globalOtherSeed);
1804           if (evt.bAlt && level.player && level.player.dead) {
1805             saveGameSession = false;
1806             replayGameSession = true;
1807             unpauseGame();
1808             return;
1809           }
1810           if (/*evt.bCtrl &&*/ showHelp != 2) {
1811             if (evt.bShift) global.idol = false;
1812             level.generateLevel();
1813             level.centerViewAtPlayer();
1814             teleportCameraAt(level.viewStart);
1815             resetFramesAndForceOne();
1816           }
1817           return;
1818         case K_m:
1819           global.toggleMusic();
1820           return;
1821         case K_q:
1822           if (/*evt.bCtrl &&*/ showHelp != 2) {
1823             if (level.allExits.length) {
1824               level.teleportPlayerTo(level.allExits[0].ix+8, level.allExits[0].iy+8);
1825               unpauseGame();
1826             }
1827           }
1828           return;
1829         case K_d:
1830           if (/*evt.bCtrl &&*/ level.player && showHelp != 2) {
1831             auto damsel = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa MonsterDamsel); });
1832             if (damsel) {
1833               level.teleportPlayerTo(damsel.ix, damsel.iy);
1834               unpauseGame();
1835             }
1836           }
1837           return;
1838         case K_h:
1839           if (/*evt.bCtrl &&*/ level.player && showHelp != 2) {
1840             MapObject obj;
1841             if (evt.bAlt) {
1842               // locked chest
1843               obj = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa ItemLockedChest); });
1844             } else {
1845               // key
1846               obj = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa ItemGoldenKey); });
1847             }
1848             if (obj) {
1849               level.teleportPlayerTo(obj.ix, obj.iy-4);
1850               unpauseGame();
1851             }
1852           }
1853           return;
1854         case K_b:
1855           if (/*evt.bCtrl &&*/ showHelp != 2) {
1856             if (level && mouseLevelX != int.min) {
1857               int scale = level.global.scale;
1858               int mapX = mouseLevelX;
1859               int mapY = mouseLevelY;
1860               level.MakeMapTile(mapX/16, mapY/16, 'oPushBlock');
1861             }
1862             return;
1863           }
1864           /*
1865           if (evt.bAlt) {
1866             if (level && mouseLevelX != int.min) {
1867               int scale = level.global.scale;
1868               int mapX = mouseLevelX;
1869               int mapY = mouseLevelY;
1870               int wdt = 12;
1871               int hgt = 14;
1872               writeln("=== POS: (", mapX, ",", mapY, ")-(", mapX+wdt-1, ",", mapY+hgt-1, ") ===");
1873               level.checkTilesInRect(mapX, mapY, wdt, hgt, delegate bool (MapTile t) {
1874                 writeln("  tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "': (", t.ix, ",", t.iy, ")");
1875                 return false;
1876               });
1877               writeln(" ---");
1878               foreach (MapTile t; level.objGrid.inRectPix(mapX, mapY, wdt, hgt, precise:false, castClass:MapTile)) {
1879                 writeln("  tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "': (", t.ix, ",", t.iy, "); collision=", t.isRectCollision(mapX, mapY, wdt, hgt));
1880               }
1881             }
1882             return;
1883           }
1884           */
1885           if (/*evt.bAlt &&*/ showHelp != 2) {
1886             auto obj = ObjBoulder(level.MakeMapTile((level.player.ix+32)/16, (level.player.iy-16)/16, 'oBoulder'));
1887             //if (obj) obj.monkey = monkey;
1888             if (obj) {
1889               //playSound('sndThump');
1890               unpauseGame();
1891             }
1892           }
1893           return;
1895         case K_DELETE: // suicide
1896           if (doGameSavingPlaying == Replay.None) {
1897             if (level.player && !level.player.dead && evt.bCtrl) {
1898               global.hasAnkh = false;
1899               level.global.plife = 1;
1900               level.player.invincible = 0;
1901               auto xplo = MapObjExplosion(level.MakeMapObject(level.player.ix, level.player.iy, 'oExplosion'));
1902               if (xplo) xplo.suicide = true;
1903               unpauseGame();
1904             }
1905           }
1906           return;
1908         case K_INSERT:
1909           if (level.player && !level.player.dead && evt.bAlt) {
1910             if (doGameSavingPlaying != Replay.None) {
1911               if (doGameSavingPlaying == Replay.Replaying) {
1912                 stopReplaying();
1913               } else if (doGameSavingPlaying == Replay.Saving) {
1914                 saveGameMovement(dbgSessionMovementFileName, packit:true);
1915               }
1916               doGameSavingPlaying = Replay.None;
1917               stopReplaying();
1918               saveGameSession = false;
1919               replayGameSession = false;
1920               unpauseGame();
1921             }
1922           }
1923           return;
1925         case K_SPACE:
1926           if (/*evt.bCtrl && evt.bShift*/ showHelp != 2) {
1927             level.stats.setMoneyCheat();
1928             level.stats.addMoney(10000);
1929           }
1930           return;
1931       }
1932     }
1933   } else {
1934     if (evt.type == ev_keyup && evt.keycode == K_ESCAPE) {
1935       if (level.player && level.player.dead) {
1936         //Video.requestQuit();
1937         escCount = 0;
1938         if (gameJustOver) { gameJustOver = false; level.restartTitle(); }
1939       } else {
1940 #ifdef QUIT_DOUBLE_ESC
1941         if (++escCount == 2) Video.requestQuit();
1942 #else
1943         showHelp = 2;
1944         pauseRequested = true;
1945 #endif
1946       }
1947       return;
1948     }
1950     if (evt.type == ev_keydown && evt.keycode == K_F1) { pauseRequested = true; helpRequested = true; return; }
1951     if (evt.type == ev_keydown && evt.keycode == K_F2 && (evt.bShift || evt.bAlt)) { pauseRequested = true; return; }
1952     if (evt.type == ev_keydown && evt.keycode == K_BACKQUOTE && (evt.bShift || evt.bAlt)) { pauseRequested = true; return; }
1953   }
1955   //!if (evt.type == ev_keydown && evt.keycode == K_n) { level.player.scrCreateBlood(level.player.ix, level.player.iy, 3); return; }
1957   if (level) {
1958     if (!level.player || !level.player.dead) {
1959       gameJustOver = false;
1960     } else if (level.player && level.player.dead) {
1961       if (!gameJustOver) {
1962         drawStats = 0;
1963         gameJustOver = true;
1964         waitingForPayRestart = true;
1965         level.clearKeysPressRelease();
1966         if (doGameSavingPlaying == Replay.None) {
1967           stopReplaying(); // just in case
1968           saveGameStats();
1969         }
1970       }
1971       replayFastForward = false;
1972       if (doGameSavingPlaying == Replay.Saving) {
1973         if (debugMovement) saveGameMovement(dbgSessionMovementFileName, packit:true);
1974         doGameSavingPlaying = Replay.None;
1975         //clearGameMovement();
1976         saveGameSession = false;
1977         replayGameSession = false;
1978       }
1979     }
1980     if (evt.type == ev_keydown || evt.type == ev_keyup) {
1981       bool down = (evt.type == ev_keydown);
1982       if (doGameSavingPlaying == Replay.Replaying && level.player && !level.player.dead) {
1983         if (down && evt.keycode == K_f) {
1984           if (evt.bCtrl) {
1985             if (replayFastForwardSpeed != 4) {
1986               replayFastForwardSpeed = 4;
1987               replayFastForward = true;
1988             } else {
1989               replayFastForward = !replayFastForward;
1990             }
1991           } else {
1992             replayFastForwardSpeed = 2;
1993             replayFastForward = !replayFastForward;
1994           }
1995         }
1996       }
1997       if (doGameSavingPlaying != Replay.Replaying || !level.player || level.player.dead) {
1998         foreach (int kbidx, int kval; global.config.keybinds) {
1999           if (kval && kval == evt.keycode) {
2000 #ifndef BIGGER_REPLAY_DATA
2001             if (doGameSavingPlaying == Replay.Saving) debugMovement.addKey(kbidx, down);
2002 #endif
2003             level.onKey(1<<(kbidx/GameConfig::MaxActionBinds), down);
2004           }
2005         }
2006       }
2007       if (level.player && level.player.dead) {
2008         if (down && evt.keycode == K_r && evt.bAlt) {
2009           saveGameSession = false;
2010           replayGameSession = true;
2011           unpauseGame();
2012         }
2013         if (down && evt.keycode == K_s && evt.bAlt) {
2014           bool wasSaveReq = saveGameSession;
2015           stopReplaying(); // just in case
2016           saveGameSession = !wasSaveReq;
2017           replayGameSession = false;
2018           //unpauseGame();
2019         }
2020         if (replayGameSession) {
2021           stopReplaying(); // just in case
2022           saveGameSession = false;
2023           replayGameSession = false;
2024           loadGameMovement(dbgSessionMovementFileName);
2025           loadGame(dbgSessionStateFileName);
2026           doGameSavingPlaying = Replay.Replaying;
2027         } else {
2028           if (down && evt.keycode == K_s && !evt.bAlt) ++drawStats;
2029           if (waitingForPayRestart) {
2030             level.isKeyReleased(GameConfig::Key.Pay);
2031             if (level.isKeyPressed(GameConfig::Key.Pay)) waitingForPayRestart = false;
2032           } else {
2033             level.isKeyPressed(GameConfig::Key.Pay);
2034             if (level.isKeyReleased(GameConfig::Key.Pay)) {
2035               auto doSave = saveGameSession;
2036               stopReplaying(); // just in case
2037               level.clearKeysPressRelease();
2038               level.restartGame();
2039               level.generateNormalLevel();
2040               if (doSave) {
2041                 saveGameSession = false;
2042                 replayGameSession = false;
2043                 writeln("DBG: saving game session...");
2044                 clearGameMovement();
2045                 doGameSavingPlaying = Replay.Saving;
2046                 saveGame(dbgSessionStateFileName);
2047                 //saveGameMovement(dbgSessionMovementFileName);
2048               }
2049             }
2050           }
2051         }
2052       }
2053     }
2054   }
2058 void levelExited () {
2059   // just in case
2060   saveGameStats();
2064 void initializeVideo () {
2065   Video.openScreen("Spelunky/VaVoom C", 320*(fullscreen ? 4 : 3), 240*(fullscreen ? 4 : 3), (fullscreen ? global.config.fsmode : 0));
2066   if (Video.realStencilBits < 8) {
2067     Video.closeScreen();
2068     FatalError("=== YOUR GPU SUX! ===\nno stencil buffer!");
2069   }
2070   if (!Video.framebufferHasAlpha) {
2071     Video.closeScreen();
2072     FatalError("=== YOUR GPU SUX! ===\nno alpha channel in framebuffer!");
2073   }
2077 void toggleFullscreen () {
2078   Video.closeScreen();
2079   fullscreen = !fullscreen;
2080   initializeVideo();
2084 final void runGameLoop () {
2085   Video.frameTime = 0; // unlimited FPS
2086   lastThinkerTime = 0;
2088   sprStore = SpawnObject(SpriteStore);
2089   sprStore.bDumpLoaded = false;
2091   bgtileStore = SpawnObject(BackTileStore);
2092   bgtileStore.bDumpLoaded = false;
2094   level = SpawnObject(GameLevel);
2095   level.setup(global, sprStore, bgtileStore);
2097   level.BuildYear = BuildYear;
2098   level.BuildMonth = BuildMonth;
2099   level.BuildDay = BuildDay;
2100   level.BuildHour = BuildHour;
2101   level.BuildMin = BuildMin;
2103   level.global = global;
2104   level.sprStore = sprStore;
2105   level.bgtileStore = bgtileStore;
2107   loadGameStats();
2108   //level.stats.introViewed = 0;
2110   if (level.stats.introViewed == 0) {
2111     startMode = StartMode.Intro;
2112     writeln("FORCED INTRO");
2113   } else {
2114     //writeln("INTRO VIWED: ", level.stats.introViewed);
2115     if (level.global.config.skipIntro) startMode = StartMode.Title;
2116   }
2118   level.onBeforeFrame = &beforeNewFrame;
2119   level.onAfterFrame = &afterNewFrame;
2120   level.onInterFrame = &interFrame;
2121   level.onLevelExitedCB = &levelExited;
2122   level.onCameraTeleported = &cameraTeleportedCB;
2124 #ifdef MASK_TEST
2125   maskSX = -0x0ff_fff;
2126   maskSY = maskSX;
2127   smask = sprStore['sExplosionMask'];
2128   maskFrame = 3;
2129 #endif
2131   sprStore.loadFont('sFontSmall');
2133   level.viewWidth = 320*3;
2134   level.viewHeight = 240*3;
2136   Video.swapInterval = (global.config.optVSync ? 1 : 0);
2137   //Video.openScreen("Spelunky/VaVoom C", 320*(fullscreen ? 4 : 3), 240*(fullscreen ? 4 : 3), fullscreen);
2138   fullscreen = global.config.startFullscreen;
2139   initializeVideo();
2141   //SoundSystem.SwapStereo = config.swapStereo;
2142   SoundSystem.NumChannels = 32;
2143   SoundSystem.MaxHearingDistance = 12000;
2144   //SoundSystem.DopplerFactor = 1.0f;
2145   //SoundSystem.DopplerVelocity = 343.3; //10000.0f;
2146   SoundSystem.RolloffFactor = 1.0f/2; // our levels are small
2147   SoundSystem.ReferenceDistance = 16.0f*4;
2148   SoundSystem.MaxDistance = 16.0f*(5*10);
2150   SoundSystem.Initialize();
2151   if (!SoundSystem.IsInitialized) {
2152     writeln("WARNING: cannot initialize sound system, turning off sound and music");
2153     global.soundDisabled = true;
2154     global.musicDisabled = true;
2155   }
2156   global.fixVolumes();
2158   level.restartGame(); // this will NOT generate a new level
2159   setupCheats();
2160   setupSeeds();
2161   performTimeCheck();
2163   texTigerEye = GLTexture.Load("sprites/teye0.png");
2165   if (global.cheatEndGameSequence) {
2166     level.winTime = 12*60+42;
2167     level.stats.money = 6666;
2168     switch (global.cheatEndGameSequence) {
2169       case 1: default: level.startWinCutscene(); break;
2170       case 2: level.startWinCutsceneVolcano(); break;
2171       case 3: level.startWinCutsceneWinFall(); break;
2172     }
2173   } else {
2174     switch (startMode) {
2175       case StartMode.Title: level.restartTitle(); break;
2176       case StartMode.Intro: level.restartIntro(); break;
2177       case StartMode.Stars: level.restartStarsRoom(); break;
2178       case StartMode.Sun: level.restartSunRoom(); break;
2179       case StartMode.Moon: level.restartMoonRoom(); break;
2180       default:
2181         level.generateNormalLevel();
2182         if (startMode == StartMode.Dead) {
2183           level.player.dead = true;
2184           level.player.visible = false;
2185         }
2186         break;
2187     }
2188   }
2190   //global.rope = 666;
2191   //global.bombs = 666;
2193   //global.globalRoomSeed = 871520037;
2194   //global.globalOtherSeed = 1047036290;
2196   //level.createTitleRoom();
2197   //level.createTrans4Room();
2198   //level.createOlmecRoom();
2199   //level.generateLevel();
2201   //level.centerViewAtPlayer();
2202   teleportCameraAt(level.viewStart);
2203   //writeln(Video.swapInterval);
2205   Video.runEventLoop();
2206   Video.closeScreen();
2207   SoundSystem.Shutdown();
2209   if (doGameSavingPlaying == Replay.Saving) saveGameMovement(dbgSessionMovementFileName, packit:true);
2210   stopReplaying();
2211   saveGameStats();
2213   delete level;
2217 // ////////////////////////////////////////////////////////////////////////// //
2218 // duplicates are not allowed!
2219 final void checkGameObjNames () {
2220   array!(class!Object) known;
2221   class!Object cc;
2222   int classCount = 0, namedCount = 0;
2223   foreach AllClasses(Object, out cc) {
2224     auto gn = GetClassGameObjName(cc);
2225     if (gn) {
2226       //writeln("'", gn, "' is `", GetClassName(cc), "`");
2227       auto nid = NameToInt(gn);
2228       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));
2229       known[nid] = cc;
2230       ++namedCount;
2231     }
2232     ++classCount;
2233   }
2234   writeln(classCount, " classes, ", namedCount, " game object classes.");
2238 // ////////////////////////////////////////////////////////////////////////// //
2239 #include "timelimit.vc"
2240 //const int TimeLimitDate = 2018232;
2243 void performTimeCheck () {
2244 #ifdef DISABLE_TIME_CHECK
2245 #else
2246   if (TigerEye) return;
2248   TTimeVal tv;
2249   if (!GetTimeOfDay(out tv)) FatalError("cannot get time of day");
2251   TDateTime tm;
2252   if (!DecodeTimeVal(out tm, ref tv)) FatalError("cannot decode time of day");
2254   int tldate = tm.year*1000+tm.yday;
2256   if (tldate > TimeLimitDate) {
2257     level.maxPlayingTime = 24;
2258   } else {
2259     //writeln("*** days left: ", TimeLimitDate-tldate);
2260   }
2261 #endif
2265 void setupCheats () {
2266   return;
2268   startMode = StartMode.Alive;
2269   global.currLevel = 2;
2270   global.scumGenShop = true;
2271   //global.scumGenShopType = GameGlobal::ShopType.Weapon;
2272   global.scumGenShopType = GameGlobal::ShopType.Craps;
2273   //global.config.scale = 1;
2274   return;
2276   //startMode = StartMode.Intro;
2277   //return;
2279   global.currLevel = 2;
2280   startMode = StartMode.Alive;
2281   return;
2283   global.currLevel = 5;
2284   startMode = StartMode.Alive;
2285   global.scumGenLake = true;
2286   global.config.scale = 1;
2287   return;
2289   startMode = StartMode.Alive;
2290   global.cheatCanSkipOlmec = true;
2291   global.currLevel = 16;
2292   //global.currLevel = 5;
2293   //global.currLevel = 13;
2294   //global.config.scale = 1;
2295   return;
2296   //startMode = StartMode.Dead;
2297   //startMode = StartMode.Title;
2298   //startMode = StartMode.Stars;
2299   //startMode = StartMode.Sun;
2300   startMode = StartMode.Moon;
2301   return;
2302   //global.scumGenSacrificePit = true;
2303   //global.scumAlwaysSacrificeAltar = true;
2305   // first lush jungle level
2306   //global.levelType = 1;
2307   /*
2308   global.scumGenCemetary = true;
2309   */
2310   //global.idol = false;
2311   //global.currLevel = 5;
2313   //global.isTunnelMan = true;
2314   //return;
2316   //global.currLevel = 5;
2317   //global.scumGenLake = true;
2319   //global.currLevel = 5;
2320   //global.currLevel = 9;
2321   //global.currLevel = 13;
2322   //global.currLevel = 14;
2323   //global.cheatEndGameSequence = 1;
2324   //return;
2326   //global.currLevel = 6;
2327   global.scumGenAlienCraft = true;
2328   global.currLevel = 9;
2329   //global.scumGenYetiLair = true;
2330   //global.genBlackMarket = true;
2331   //startDead = false;
2332   startMode = StartMode.Alive;
2333   return;
2335   global.cheatCanSkipOlmec = true;
2336   global.currLevel = 15;
2337   startMode = StartMode.Alive;
2338   return;
2340   global.scumGenShop = true;
2341   //global.scumGenShopType = GameGlobal::ShopType.Weapon;
2342   global.scumGenShopType = GameGlobal::ShopType.Craps;
2343   //global.scumGenShopType = 6; // craps
2344   //global.scumGenShopType = 7; // kissing
2346   //global.scumAlwaysSacrificeAltar = true;
2350 void setupSeeds () {
2354 // ////////////////////////////////////////////////////////////////////////// //
2355 void main () {
2356   checkGameObjNames();
2358   appSetName("k8spelunky");
2359   config = SpawnObject(GameConfig);
2360   global = SpawnObject(GameGlobal);
2361   global.config = config;
2362   config.heroType = GameConfig::Hero.Spelunker;
2364   global.randomizeSeedAll();
2366   fillCheatPickupList();
2367   fillCheatItemsList();
2368   fillCheatEnemiesList();
2370   loadGameOptions();
2371   loadKeyboardBindings();
2372   runGameLoop();