giant spider item hit fixes
[k8vacspelynky.git] / spelunky_main.vc
blob62333036f1b8df9841dbbc10f1868bf05d845219
1 /**********************************************************************************
2  * Copyright (c) 2008, 2009 Derek Yu and Mossmouth, LLC
3  * Copyright (c) 2010, Moloch
4  * Copyright (c) 2018, Ketmar Dark
5  *
6  * This file is part of Spelunky.
7  *
8  * You can redistribute and/or modify Spelunky, including its source code, under
9  * the terms of the Spelunky User License.
10  *
11  * Spelunky is distributed in the hope that it will be entertaining and useful,
12  * but WITHOUT WARRANTY.  Please see the Spelunky User License for more details.
13  *
14  * The Spelunky User License should be available in "Game Information", which
15  * can be found in the Resource Explorer, or as an external file called COPYING.
16  * If not, please obtain a new copy of Spelunky from <http://spelunkyworld.com/>
17  *
18  **********************************************************************************/
19 import 'Video';
20 import 'SoundSys';
21 import 'Geom';
22 import 'Game';
23 import 'Generator';
24 import 'Sprites';
26 //#define QUIT_DOUBLE_ESC
28 //#define MASK_TEST
30 //#define BIGGER_REPLAY_DATA
32 // ////////////////////////////////////////////////////////////////////////// //
33 #include "mapent/0all.vc"
34 #include "PlayerPawn.vc"
35 #include "PlayerPowerup.vc"
36 #include "GameLevel.vc"
39 // ////////////////////////////////////////////////////////////////////////// //
40 #include "uisimple.vc"
43 // ////////////////////////////////////////////////////////////////////////// //
44 class DebugSessionMovement : Object;
46 #ifdef BIGGER_REPLAY_DATA
47 array!(GameLevel::SavedKeyState) keypresses;
48 #else
49 array!ubyte keypresses; // on each frame
50 #endif
51 GameConfig playconfig;
53 transient int keypos;
54 transient int otherSeed, roomSeed;
57 override void Destroy () {
58   delete playconfig;
59   keypresses.length = 0;
60   ::Destroy();
64 final void resetReplay () {
65   keypos = 0;
69 #ifndef BIGGER_REPLAY_DATA
70 final void addKey (int kbidx, bool down) {
71   if (kbidx < 0 || kbidx >= 127) FatalError("DebugSessionMovement: invalid kbidx (%d)", kbidx);
72   keypresses[$] = kbidx|(down ? 0x80 : 0);
76 final void addEndOfFrame () {
77   keypresses[$] = 0xff;
81 enum {
82   NORMAL,
83   END_OF_FRAME,
84   END_OF_RECORD,
87 final int getKey (out int kbidx, out bool down) {
88   if (keypos < 0) FatalError("DebugSessionMovement: invalid keypos");
89   if (keypos >= keypresses.length) return END_OF_RECORD;
90   ubyte b = keypresses[keypos++];
91   if (b == 0xff) return END_OF_FRAME;
92   kbidx = b&0x7f;
93   down = (b >= 0x80);
94   return NORMAL;
96 #endif
99 // ////////////////////////////////////////////////////////////////////////// //
100 class TempOptionsKeys : Object;
102 int[16*GameConfig::MaxActionBinds] keybinds;
103 int kbversion = 1;
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 int mouseX = int.min, mouseY = int.min;
121 int mouseLevelX = int.min, mouseLevelY = int.min;
122 bool renderMouseTile;
123 bool renderMouseRect;
125 enum StartMode {
126   Dead,
127   Alive,
128   Title,
129   Intro,
130   Stars,
131   Sun,
132   Moon,
135 StartMode startMode = StartMode.Intro;
136 bool pauseRequested;
137 bool helpRequested;
139 bool replayFastForward = false;
140 int replayFastForwardSpeed = 2;
141 bool saveGameSession = false;
142 bool replayGameSession = false;
143 enum Replay {
144   None,
145   Saving,
146   Replaying,
148 Replay doGameSavingPlaying = Replay.None;
149 float saveMovementLastTime = 0;
150 DebugSessionMovement debugMovement;
151 GameStats origStats; // for replaying
152 GameConfig origConfig; // for replaying
153 int origRoomSeed, origOtherSeed;
155 int showHelp;
156 int escCount;
158 bool fullscreen;
160 #ifdef MASK_TEST
161 transient int maskSX, maskSY;
162 transient SpriteImage smask;
163 transient int maskFrame;
164 #endif
167 // ////////////////////////////////////////////////////////////////////////// //
168 final void saveKeyboardBindings () {
169   auto tok = SpawnObject(TempOptionsKeys);
170   foreach (auto idx, auto v; global.config.keybinds) tok.keybinds[idx] = v;
171   appSaveOptions(tok, "keybindings");
172   delete tok;
176 final void loadKeyboardBindings () {
177   auto tok = appLoadOptions(TempOptionsKeys, "keybindings");
178   if (tok) {
179     if (tok.kbversion != TempOptionsKeys.default.kbversion) {
180       global.config.resetKeybindings();
181     } else {
182       foreach (auto idx, ref auto v; global.config.keybinds) v = tok.keybinds[idx];
183     }
184     delete tok;
185   }
189 // ////////////////////////////////////////////////////////////////////////// //
190 void saveGameOptions () {
191   appSaveOptions(global.config, "config");
195 void loadGameOptions () {
196   auto cfg = appLoadOptions(GameConfig, "config");
197   if (cfg) {
198     auto oldHero = config.heroType;
199     auto tok = SpawnObject(TempOptionsKeys);
200     foreach (auto idx, auto v; global.config.keybinds) tok.keybinds[idx] = v;
201     delete global.config;
202     global.config = cfg;
203     config = cfg;
204     foreach (auto idx, ref auto v; global.config.keybinds) v = tok.keybinds[idx];
205     delete tok;
206     writeln("config loaded");
207     global.restartMusic();
208     global.fixVolumes();
209     //config.heroType = GameConfig::Hero.Spelunker;
210     config.heroType = oldHero;
211   }
212   // fix my bug
213   if (global.config.ghostExtraTime > 300) global.config.ghostExtraTime = 30;
217 // ////////////////////////////////////////////////////////////////////////// //
218 void saveGameStats () {
219   if (level.stats) appSaveOptions(level.stats, "stats");
223 void loadGameStats () {
224   auto stats = appLoadOptions(GameStats, "stats");
225   if (stats) {
226     delete level.stats;
227     level.stats = stats;
228   }
229   if (!level.stats) level.stats = SpawnObject(GameStats);
230   level.stats.global = global;
234 // ////////////////////////////////////////////////////////////////////////// //
235 struct UIPaneSaveInfo {
236   name id;
237   UIPane::SaveInfo nfo;
240 transient UIPane optionsPane; // either options, or binding editor
242 transient GameLevel::IVec2D optionsPaneOfs;
243 transient void delegate () saveOptionsDG;
245 transient array!UIPaneSaveInfo optionsPaneState;
248 final void saveCurrentPane () {
249   if (!optionsPane || !optionsPane.id) return;
251   // summon ghost
252   if (optionsPane.id == 'CheatFlags') {
253     if (instantGhost && level.ghostTimeLeft > 0) {
254       level.ghostTimeLeft = 1;
255     }
256   }
258   foreach (ref auto psv; optionsPaneState) {
259     if (psv.id == optionsPane.id) {
260       optionsPane.saveState(psv.nfo);
261       return;
262     }
263   }
264   // append new
265   optionsPaneState.length += 1;
266   optionsPaneState[$-1].id = optionsPane.id;
267   optionsPane.saveState(optionsPaneState[$-1].nfo);
271 final void restoreCurrentPane () {
272   if (optionsPane) optionsPane.setupHotkeys(); // why not?
273   if (!optionsPane || !optionsPane.id) return;
274   foreach (ref auto psv; optionsPaneState) {
275     if (psv.id == optionsPane.id) {
276       optionsPane.restoreState(psv.nfo);
277       return;
278     }
279   }
283 // ////////////////////////////////////////////////////////////////////////// //
284 final void onCheatObjectSpawnSelectedCB (UIMenuItem it) {
285   if (!it.tagClass) return;
286   if (class!MapObject(it.tagClass)) {
287     level.debugSpawnObjectWithClass(class!MapObject(it.tagClass), playerDir:true);
288     it.owner.closeMe = true;
289   }
293 // ////////////////////////////////////////////////////////////////////////// //
294 transient array!(class!MapObject) cheatItemsList;
297 final void fillCheatItemsList () {
298   cheatItemsList.length = 0;
299   cheatItemsList[$] = ItemProjectileArrow;
300   cheatItemsList[$] = ItemWeaponShotgun;
301   cheatItemsList[$] = ItemWeaponAshShotgun;
302   cheatItemsList[$] = ItemWeaponPistol;
303   cheatItemsList[$] = ItemWeaponMattock;
304   cheatItemsList[$] = ItemWeaponMachete;
305   cheatItemsList[$] = ItemWeaponWebCannon;
306   cheatItemsList[$] = ItemWeaponSceptre;
307   cheatItemsList[$] = ItemWeaponBow;
308   cheatItemsList[$] = ItemBones;
309   cheatItemsList[$] = ItemFakeBones;
310   cheatItemsList[$] = ItemFishBone;
311   cheatItemsList[$] = ItemRock;
312   cheatItemsList[$] = ItemJar;
313   cheatItemsList[$] = ItemSkull;
314   cheatItemsList[$] = ItemGoldenKey;
315   cheatItemsList[$] = ItemGoldIdol;
316   cheatItemsList[$] = ItemCrystalSkull;
317   cheatItemsList[$] = ItemShellSingle;
318   cheatItemsList[$] = ItemChest;
319   cheatItemsList[$] = ItemCrate;
320   cheatItemsList[$] = ItemLockedChest;
321   cheatItemsList[$] = ItemDice;
322   cheatItemsList[$] = ItemBasketBall;
326 final UIPane createCheatItemsPane () {
327   if (!level.player) return none;
329   UIPane pane = SpawnObject(UIPane);
330   pane.id = 'Items';
331   pane.sprStore = sprStore;
333   pane.width = 320*3-64;
334   pane.height = 240*3-64;
336   foreach (auto ipk; cheatItemsList) {
337     auto it = UIMenuItem.Create(pane, ipk.default.desc.toUpperCase(), ipk.default.desc2.toUpperCase(), &onCheatObjectSpawnSelectedCB);
338     it.tagClass = ipk;
339   }
341   //optionsPaneOfs.x = 100;
342   //optionsPaneOfs.y = 50;
344   return pane;
348 // ////////////////////////////////////////////////////////////////////////// //
349 transient array!(class!MapObject) cheatEnemiesList;
352 final void fillCheatEnemiesList () {
353   cheatEnemiesList.length = 0;
354   cheatEnemiesList[$] = MonsterDamsel; // not an enemy, but meh..
355   cheatEnemiesList[$] = EnemyBat;
356   cheatEnemiesList[$] = EnemySpiderHang;
357   cheatEnemiesList[$] = EnemySpider;
358   cheatEnemiesList[$] = EnemySnake;
359   cheatEnemiesList[$] = EnemyCaveman;
360   cheatEnemiesList[$] = EnemySkeleton;
361   cheatEnemiesList[$] = MonsterShopkeeper;
362   cheatEnemiesList[$] = EnemyZombie;
363   cheatEnemiesList[$] = EnemyVampire;
364   cheatEnemiesList[$] = EnemyFrog;
365   cheatEnemiesList[$] = EnemyGreenFrog;
366   cheatEnemiesList[$] = EnemyFireFrog;
367   cheatEnemiesList[$] = EnemyMantrap;
368   cheatEnemiesList[$] = EnemyScarab;
369   cheatEnemiesList[$] = EnemyFloater;
370   cheatEnemiesList[$] = EnemyBlob;
371   cheatEnemiesList[$] = EnemyMonkey;
372   cheatEnemiesList[$] = EnemyGoldMonkey;
373   cheatEnemiesList[$] = EnemyAlien;
374   cheatEnemiesList[$] = EnemyYeti;
375   cheatEnemiesList[$] = EnemyHawkman;
376   cheatEnemiesList[$] = EnemyUFO;
377   cheatEnemiesList[$] = EnemyYetiKing;
381 final UIPane createCheatEnemiesPane () {
382   if (!level.player) return none;
384   UIPane pane = SpawnObject(UIPane);
385   pane.id = 'Enemies';
386   pane.sprStore = sprStore;
388   pane.width = 320*3-64;
389   pane.height = 240*3-64;
391   foreach (auto ipk; cheatEnemiesList) {
392     auto it = UIMenuItem.Create(pane, ipk.default.desc.toUpperCase(), ipk.default.desc2.toUpperCase(), &onCheatObjectSpawnSelectedCB);
393     it.tagClass = ipk;
394   }
396   //optionsPaneOfs.x = 100;
397   //optionsPaneOfs.y = 50;
399   return pane;
403 // ////////////////////////////////////////////////////////////////////////// //
404 transient array!(class!/*ItemPickup*/MapItem) cheatPickupList;
407 final void fillCheatPickupList () {
408   cheatPickupList.length = 0;
409   cheatPickupList[$] = ItemPickupBombBag;
410   cheatPickupList[$] = ItemPickupBombBox;
411   cheatPickupList[$] = ItemPickupPaste;
412   cheatPickupList[$] = ItemPickupRopePile;
413   cheatPickupList[$] = ItemPickupShellBox;
414   cheatPickupList[$] = ItemPickupAnkh;
415   cheatPickupList[$] = ItemPickupCape;
416   cheatPickupList[$] = ItemPickupJetpack;
417   cheatPickupList[$] = ItemPickupUdjatEye;
418   cheatPickupList[$] = ItemPickupCrown;
419   cheatPickupList[$] = ItemPickupKapala;
420   cheatPickupList[$] = ItemPickupParachute;
421   cheatPickupList[$] = ItemPickupCompass;
422   cheatPickupList[$] = ItemPickupSpectacles;
423   cheatPickupList[$] = ItemPickupGloves;
424   cheatPickupList[$] = ItemPickupMitt;
425   cheatPickupList[$] = ItemPickupJordans;
426   cheatPickupList[$] = ItemPickupSpringShoes;
427   cheatPickupList[$] = ItemPickupSpikeShoes;
428   cheatPickupList[$] = ItemPickupTeleporter;
432 final UIPane createCheatPickupsPane () {
433   if (!level.player) return none;
435   UIPane pane = SpawnObject(UIPane);
436   pane.id = 'Pickups';
437   pane.sprStore = sprStore;
439   pane.width = 320*3-64;
440   pane.height = 240*3-64;
442   foreach (auto ipk; cheatPickupList) {
443     auto it = UIMenuItem.Create(pane, ipk.default.desc.toUpperCase(), ipk.default.desc2.toUpperCase(), &onCheatObjectSpawnSelectedCB);
444     it.tagClass = ipk;
445   }
447   //optionsPaneOfs.x = 100;
448   //optionsPaneOfs.y = 50;
450   return pane;
454 // ////////////////////////////////////////////////////////////////////////// //
455 transient int instantGhost;
457 final UIPane createCheatFlagsPane () {
458   UIPane pane = SpawnObject(UIPane);
459   pane.id = 'CheatFlags';
460   pane.sprStore = sprStore;
462   pane.width = 320*3-64;
463   pane.height = 240*3-64;
465   instantGhost = 0;
467   UICheckBox.Create(pane, &global.hasUdjatEye, "UDJAT EYE", "UDJAT EYE");
468   UICheckBox.Create(pane, &global.hasAnkh, "ANKH", "ANKH");
469   UICheckBox.Create(pane, &global.hasCrown, "CROWN", "CROWN");
470   UICheckBox.Create(pane, &global.hasKapala, "KAPALA", "COLLECT BLOOD TO GET MORE LIVES!");
471   UICheckBox.Create(pane, &global.hasStickyBombs, "STICKY BOMBS", "YOUR BOMBS CAN STICK!");
472   //UICheckBox.Create(pane, &global.stickyBombsActive, "stickyBombsActive", "stickyBombsActive");
473   UICheckBox.Create(pane, &global.hasSpectacles, "SPECTACLES", "YOU CAN SEE WHAT WAS HIDDEN!");
474   UICheckBox.Create(pane, &global.hasCompass, "COMPASS", "COMPASS");
475   UICheckBox.Create(pane, &global.hasParachute, "PARACHUTE", "YOU WILL DEPLOY PARACHUTE ON LONG FALLS.");
476   UICheckBox.Create(pane, &global.hasSpringShoes, "SPRING SHOES", "YOU CAN JUMP HIGHER!");
477   UICheckBox.Create(pane, &global.hasSpikeShoes, "SPIKE SHOES", "YOUR HEAD-JUMPS DOES MORE DAMAGE!");
478   UICheckBox.Create(pane, &global.hasJordans, "JORDANS", "YOU CAN JUMP TO THE MOON!");
479   //UICheckBox.Create(pane, &global.hasNinjaSuit, "hasNinjaSuit", "hasNinjaSuit");
480   UICheckBox.Create(pane, &global.hasCape, "CAPE", "YOU CAN CONTROL YOUR FALLS!");
481   UICheckBox.Create(pane, &global.hasJetpack, "JETPACK", "FLY TO THE SKY!");
482   UICheckBox.Create(pane, &global.hasGloves, "GLOVES", "OH, THOSE GLOVES ARE STICKY!");
483   UICheckBox.Create(pane, &global.hasMitt, "MITT", "YAY, YOU'RE THE BEST CATCHER IN THE WORLD NOW!");
484   UICheckBox.Create(pane, &instantGhost, "INSTANT GHOST", "SUMMON GHOST");
486   optionsPaneOfs.x = 100;
487   optionsPaneOfs.y = 50;
489   return pane;
493 final UIPane createOptionsPane () {
494   UIPane pane = SpawnObject(UIPane);
495   pane.id = 'Options';
496   pane.sprStore = sprStore;
498   pane.width = 320*3-64;
499   pane.height = 240*3-64;
502   // this is buggy
503   //!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.");
506   UILabel.Create(pane, "VISUAL OPTIONS");
507     UICheckBox.Create(pane, &config.skipIntro, "SKIP INTRO", "AUTOMATICALLY SKIPS THE INTRO SEQUENCE AND STARTS THE GAME AT THE TITLE SCREEN.");
508     UICheckBox.Create(pane, &config.interpolateMovement, "INTERPOLATE MOVEMENT", "IF TURNED OFF, THE MOVEMENT WILL BE JERKY AND ANNOYING.");
509     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).");
510     UICheckBox.Create(pane, &config.scumMetric, "METRIC UNITS", "DEPTH WILL BE MEASURED IN METRES INSTEAD OF FEET.");
511     auto startfs = UICheckBox.Create(pane, &config.startFullscreen, "START FULLSCREEN", "START THE GAME IN FULLSCREEN MODE?");
512     startfs.onValueChanged = delegate void (int newval) {
513       Video.showMouseCursor();
514       Video.closeScreen();
515       fullscreen = newval;
516       initializeVideo();
517     };
518     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).");
519     fsmode.names[$] = "REAL";
520     fsmode.names[$] = "SCALED";
521     fsmode.onValueChanged = delegate void (int newval) {
522       if (fullscreen) {
523         Video.showMouseCursor();
524         Video.closeScreen();
525         initializeVideo();
526       }
527     };
530   UILabel.Create(pane, "");
531   UILabel.Create(pane, "HUD OPTIONS");
532     UICheckBox.Create(pane, &config.ghostShowTime, "SHOW GHOST TIME", "TURN THIS OPTION ON TO SEE HOW MUCH TIME IS LEFT UNTIL THE GHOST WILL APPEAR.");
533     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.");
534     auto halpha = UIIntEnum.Create(pane, &config.hudTextAlpha, 0, 250, "HUD TEXT ALPHA :", "THE BIGGER THIS NUMBER, THE MORE TRANSPARENT YOUR MAIN HUD WILL BE.");
535     halpha.step = 10;
537     auto ialpha = UIIntEnum.Create(pane, &config.hudItemsAlpha, 0, 250, "HUD ITEMS ALPHA:", "THE BIGGER THIS NUMBER, THE MORE TRANSPARENT YOUR ITEMS HUD WILL BE.");
538     ialpha.step = 10;
541   UILabel.Create(pane, "");
542   UILabel.Create(pane, "COSMETIC GAMEPLAY OPTIONS");
543     //!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.");
544     //UICheckBox.Create(pane, &config.optImmTransition, "FASTER TRANSITIONS", "PRESSING ACTION SECOND TIME WILL IMMEDIATELY SKIP TRANSITION LEVEL.");
545     UICheckBox.Create(pane, &config.downToRun, "PRESS 'DOWN' TO RUN", "PLAYER CAN PRESS 'DOWN' KEY TO RUN.");
546     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.");
547     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.");
548     UICheckBox.Create(pane, &config.naturalSwim, "IMPROVED SWIMMING", "HOLD DOWN TO SINK FASTER, HOLD UP TO SINK SLOWER."); // Spelunky Natural swim mechanics
549     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.");
550     UICheckBox.Create(pane, &config.optSpikeVariations, "RANDOM SPIKES", "GENERATE SPIKES OF RANDOM TYPE (DEFAULT TYPE HAS GREATER PROBABILITY, THOUGH).");
553   UILabel.Create(pane, "");
554   UILabel.Create(pane, "GAMEPLAY OPTIONS");
555     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.");
556     UICheckBox.Create(pane, &config.bomsDontSetArrowTraps, "ARROW TRAPS IGNORE BOMBS", "TURN THIS OPTION ON TO MAKE ARROW TRAP IGNORE FALLING BOMBS AND ROPES.");
557     UICheckBox.Create(pane, &config.weaponsOpenContainers, "MELEE CONTAINERS", "ALLOWS YOU TO OPEN CRATES AND CHESTS BY HITTING THEM WITH THE WHIP, MACHETE OR MATTOCK.");
558     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!");
559     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.");
560     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.");
561     UICheckBox.Create(pane, &config.optThrowEmptyShotgun, "THROW EMPTY SHOTGUN", "PRESSING ACTION WHEN SHOTGUN IS EMPTY WILL THROW IT.");
562     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.");
563     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.");
564     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.");
565     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.");
566     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?");
567     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.");
568     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.");
569     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.");
570     UICheckBox.Create(pane, &config.optEnemyVariations, "ENEMY VARIATIONS", "ADD SOME ENEMY VARIATIONS IN MINES AND JUNGLE WHEN YOU DIED ENOUGH TIMES.");
571     UICheckBox.Create(pane, &config.optIdolForEachLevelType, "IDOL IN EACH LEVEL TYPE", "GENERATE IDOL IN EACH LEVEL TYPE.");
572     UICheckBox.Create(pane, &config.boulderChaos, "BOULDER CHAOS", "BOULDERS WILL ROLL FASTER, BOUNCE A BIT HIGHER, AND KEEP THEIR MOMENTUM LONGER.");
573     auto rstl = UIIntEnum.Create(pane, &config.optRoomStyle, -1, 1, "ROOM STYLE:", "WHAT KIND OF ROOMS LEVEL GENERATOR SHOULD USE.");
574     rstl.names[$] = "RANDOM";
575     rstl.names[$] = "NORMAL";
576     rstl.names[$] = "BIZARRE";
579   UILabel.Create(pane, "");
580   UILabel.Create(pane, "WHIP OPTIONS");
581     UICheckBox.Create(pane, &global.config.unarmed, "UNARMED", "WITH THIS OPTION ENABLED, YOU WILL HAVE NO WHIP.");
582     auto whiptype = UIIntEnum.Create(pane, &config.scumWhipUpgrade, 0, 1, "WHIP TYPE:", "YOU CAN HAVE A NORMAL WHIP, OR A LONGER ONE.");
583     whiptype.names[$] = "NORMAL";
584     whiptype.names[$] = "LONG";
585     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.");
588   UILabel.Create(pane, "");
589   UILabel.Create(pane, "PLAYER OPTIONS");
590     auto herotype = UIIntEnum.Create(pane, &config.heroType, 0, 2, "PLAY AS: ", "CHOOSE YOUR HERO!");
591     herotype.names[$] = "SPELUNKY GUY";
592     herotype.names[$] = "DAMSEL";
593     herotype.names[$] = "TUNNEL MAN";
596   UILabel.Create(pane, "");
597   UILabel.Create(pane, "CHEAT OPTIONS");
598     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.");
599     auto plrlit = UIIntEnum.Create(pane, &config.scumPlayerLit, 0, 2, "PLAYER LIT:", "LIT PLAYER IN DARKNESS WHEN...");
600     plrlit.names[$] = "NEVER";
601     plrlit.names[$] = "FORCED DARKNESS";
602     plrlit.names[$] = "ALWAYS";
603     UIIntEnum.Create(pane, &config.darknessDarkness, 0, 8, "DARKNESS LEVEL:", "INCREASE THIS NUMBER TO MAKE DARK AREAS BRIGHTER.");
604     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'.");
605     rdark.names[$] = "NEVER";
606     rdark.names[$] = "DEFAULT";
607     rdark.names[$] = "ALWAYS";
608     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.");
609     rghost.step = 30;
610     rghost.getNameCB = delegate string (int val) {
611       if (val < 0) return "INSTANT";
612       if (val == 0) return "NEVER";
613       if (val < 120) return va("%d SEC", val);
614       if (val%60 == 0) return va("%d MIN", val/60);
615       if (val%60 == 30) return va("%d.5 MIN", val/60);
616       return va("%d MIN, %d SEC", val/60, val%60);
617     };
618     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.");
620   UILabel.Create(pane, "");
621   UILabel.Create(pane, "CHEAT START OPTIONS");
622     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.");
623     UICheckBox.Create(pane, &config.startWithKapala, "START WITH KAPALA", "PLAYER WILL ALWAYS START WITH KAPALA. THIS IS USEFUL TO PERFORM 'KAPALA CHALLENGES'.");
624     UIIntEnum.Create(pane, &config.scumStartLife,  1, 42, "STARTING LIVES:", "STARTING NUMBER OF LIVES FOR SPELUNKER.");
625     UIIntEnum.Create(pane, &config.scumStartBombs, 1, 42, "STARTING BOMBS:", "STARTING NUMBER OF BOMBS FOR SPELUNKER.");
626     UIIntEnum.Create(pane, &config.scumStartRope,  1, 42, "STARTING ROPES:", "STARTING NUMBER OF ROPES FOR SPELUNKER.");
629   UILabel.Create(pane, "");
630   UILabel.Create(pane, "LEVEL MUSIC OPTIONS");
631     auto mm = UIIntEnum.Create(pane, &config.transitionMusicMode, 0, 2, "TRANSITION MUSIC  : ", "THIS IS WHAT GAME SHOULD DO WITH MUSIC ON TRANSITION LEVELS.");
632     mm.names[$] = "SILENCE";
633     mm.names[$] = "RESTART";
634     mm.names[$] = "DON'T TOUCH";
636     mm = UIIntEnum.Create(pane, &config.nextLevelMusicMode, 1, 2, "NORMAL LEVEL MUSIC: ", "THIS IS WHAT GAME SHOULD DO WITH MUSIC ON NORMAL LEVELS.");
637     //mm.names[$] = "SILENCE";
638     mm.names[$] = "RESTART";
639     mm.names[$] = "DON'T TOUCH";
642   //auto swstereo = UICheckBox.Create(pane, &config.swapStereo, "SWAP STEREO", "SWAP STEREO CHANNELS.");
643   /*
644   swstereo.onValueChanged = delegate void (int newval) {
645     SoundSystem.SwapStereo = newval;
646   };
647   */
649   UILabel.Create(pane, "");
650   UILabel.Create(pane, "SOUND CONTROL CENTER");
651     auto rmusonoff = UICheckBox.Create(pane, &config.musicEnabled, "MUSIC", "PLAY OR DON'T PLAY MUSIC.");
652     rmusonoff.onValueChanged = delegate void (int newval) {
653       global.restartMusic();
654     };
656     UICheckBox.Create(pane, &config.soundEnabled, "SOUND", "PLAY OR DON'T PLAY SOUND.");
658     auto rvol = UIIntEnum.Create(pane, &config.musicVol, 0, GameConfig::MaxVolume, "MUSIC VOLUME:", "SET MUSIC VOLUME.");
659     rvol.onValueChanged = delegate void (int newval) { global.fixVolumes(); };
661     rvol = UIIntEnum.Create(pane, &config.soundVol, 0, GameConfig::MaxVolume, "SOUND VOLUME:", "SET SOUND VOLUME.");
662     rvol.onValueChanged = delegate void (int newval) { global.fixVolumes(); };
665   saveOptionsDG = delegate void () {
666     writeln("saving options");
667     saveGameOptions();
668   };
669   optionsPaneOfs.x = 42;
670   optionsPaneOfs.y = 0;
672   return pane;
676 final void createBindingsControl (UIPane pane, int keyidx) {
677   string kname, khelp;
678   switch (keyidx) {
679     case GameConfig::Key.Left: kname = "LEFT"; khelp = "MOVE SPELUNKER TO THE LEFT"; break;
680     case GameConfig::Key.Right: kname = "RIGHT"; khelp = "MOVE SPELUNKER TO THE RIGHT"; break;
681     case GameConfig::Key.Up: kname = "UP"; khelp = "MOVE SPELUNKER UP, OR LOOK UP"; break;
682     case GameConfig::Key.Down: kname = "DOWN"; khelp = "MOVE SPELUNKER DOWN, OR LOOK DOWN"; break;
683     case GameConfig::Key.Jump: kname = "JUMP"; khelp = "MAKE SPELUNKER JUMP"; break;
684     case GameConfig::Key.Run: kname = "RUN"; khelp = "MAKE SPELUNKER RUN"; break;
685     case GameConfig::Key.Attack: kname = "ATTACK"; khelp = "USE CURRENT ITEM, OR PERFORM AN ATTACK WITH THE CURRENT WEAPON"; break;
686     case GameConfig::Key.Switch: kname = "SWITCH"; khelp = "SWITCH BETWEEN ROPE/BOMB/ITEM"; break;
687     case GameConfig::Key.Pay: kname = "PAY"; khelp = "PAY SHOPKEEPER"; break;
688     case GameConfig::Key.Bomb: kname = "BOMB"; khelp = "DROP AN ARMED BOMB"; break;
689     case GameConfig::Key.Rope: kname = "ROPE"; khelp = "THROW A ROPE"; break;
690     default: return;
691   }
692   int arridx = GameConfig.getKeyIndex(keyidx);
693   UIKeyBinding.Create(pane, &global.config.keybinds[arridx+0], &global.config.keybinds[arridx+1], kname, khelp);
697 final UIPane createBindingsPane () {
698   UIPane pane = SpawnObject(UIPane);
699   pane.id = 'KeyBindings';
700   pane.sprStore = sprStore;
702   pane.width = 320*3-64;
703   pane.height = 240*3-64;
705   createBindingsControl(pane, GameConfig::Key.Left);
706   createBindingsControl(pane, GameConfig::Key.Right);
707   createBindingsControl(pane, GameConfig::Key.Up);
708   createBindingsControl(pane, GameConfig::Key.Down);
709   createBindingsControl(pane, GameConfig::Key.Jump);
710   createBindingsControl(pane, GameConfig::Key.Run);
711   createBindingsControl(pane, GameConfig::Key.Attack);
712   createBindingsControl(pane, GameConfig::Key.Switch);
713   createBindingsControl(pane, GameConfig::Key.Pay);
714   createBindingsControl(pane, GameConfig::Key.Bomb);
715   createBindingsControl(pane, GameConfig::Key.Rope);
717   saveOptionsDG = delegate void () {
718     writeln("saving keys");
719     saveKeyboardBindings();
720   };
721   optionsPaneOfs.x = 120;
722   optionsPaneOfs.y = 140;
724   return pane;
728 // ////////////////////////////////////////////////////////////////////////// //
729 void clearGameMovement () {
730   debugMovement = SpawnObject(DebugSessionMovement);
731   debugMovement.playconfig = SpawnObject(GameConfig);
732   debugMovement.playconfig.copyGameplayConfigFrom(config);
733   debugMovement.resetReplay();
737 void saveGameMovement (string fname, optional bool packit) {
738   if (debugMovement) appSaveOptions(debugMovement, fname, packit);
739   saveMovementLastTime = GetTickCount();
743 void loadGameMovement (string fname) {
744   delete debugMovement;
745   debugMovement = appLoadOptions(DebugSessionMovement, fname);
746   debugMovement.resetReplay();
747   if (debugMovement) {
748     delete origStats;
749     origStats = level.stats;
750     origStats.global = none;
751     level.stats = SpawnObject(GameStats);
752     level.stats.global = global;
753     delete origConfig;
754     origConfig = config;
755     config = debugMovement.playconfig;
756     global.config = config;
757     origRoomSeed = global.globalRoomSeed;
758     origOtherSeed = global.globalOtherSeed;
759     writeln(va("saving seeds: (0x%x, 0x%x)", origRoomSeed, origOtherSeed));
760   }
764 void stopReplaying () {
765   if (debugMovement) {
766     writeln(va("restoring seeds: (0x%x, 0x%x)", origRoomSeed, origOtherSeed));
767     global.globalRoomSeed = origRoomSeed;
768     global.globalOtherSeed = origOtherSeed;
769   }
770   delete debugMovement;
771   saveGameSession = false;
772   replayGameSession = false;
773   doGameSavingPlaying = Replay.None;
774   if (origStats) {
775     delete level.stats;
776     origStats.global = global;
777     level.stats = origStats;
778     origStats = none;
779   }
780   if (origConfig) {
781     delete config;
782     config = origConfig;
783     global.config = origConfig;
784     origConfig = none;
785   }
789 // ////////////////////////////////////////////////////////////////////////// //
790 final bool saveGame (string gmname) {
791   return appSaveOptions(level, gmname);
795 final bool loadGame (string gmname) {
796   auto olddel = ImmediateDelete;
797   ImmediateDelete = false;
798   bool res = false;
799   auto stats = level.stats;
800   level.stats = none;
802   auto lvl = appLoadOptions(GameLevel, gmname);
803   if (lvl) {
804     //lvl.global.config = config;
805     delete level;
806     delete global;
808     level = lvl;
809     global = level.global;
810     global.config = config;
812     level.sprStore = sprStore;
813     level.bgtileStore = bgtileStore;
816     level.onBeforeFrame = &beforeNewFrame;
817     level.onAfterFrame = &afterNewFrame;
818     level.onInterFrame = &interFrame;
819     level.onLevelExitedCB = &levelExited;
820     level.onCameraTeleported = &cameraTeleportedCB;
822     //level.viewWidth = Video.screenWidth;
823     //level.viewHeight = Video.screenHeight;
824     level.viewWidth = 320*3;
825     level.viewHeight = 240*3;
827     level.onLoaded();
828     level.centerViewAtPlayer();
829     teleportCameraAt(level.viewStart);
831     recalcCameraCoords(0);
833     res = true;
834   }
835   level.stats = stats;
836   level.stats.global = level.global;
838   ImmediateDelete = olddel;
839   CollectGarbage(true); // destroy delayed objects too
840   return res;
844 // ////////////////////////////////////////////////////////////////////////// //
845 float lastThinkerTime;
846 int replaySkipFrame = 0;
849 final void onTimePasses () {
850   float curTime = GetTickCount();
851   if (lastThinkerTime > 0) {
852     if (curTime < lastThinkerTime) {
853       writeln("something is VERY wrong with timers! %f %f", curTime, lastThinkerTime);
854       lastThinkerTime = curTime;
855       return;
856     }
857     if (replayFastForward && replaySkipFrame) {
858       level.accumTime = 0;
859       lastThinkerTime = curTime-GameLevel::FrameTime*replayFastForwardSpeed;
860       replaySkipFrame = 0;
861     }
862     level.processThinkers(curTime-lastThinkerTime);
863   }
864   lastThinkerTime = curTime;
868 final void resetFramesAndForceOne () {
869   float curTime = GetTickCount();
870   lastThinkerTime = curTime;
871   level.accumTime = 0;
872   auto wasPaused = level.gamePaused;
873   level.gamePaused = false;
874   if (wasPaused && doGameSavingPlaying != Replay.None) level.keysRestoreState(savedKeyState);
875   level.processThinkers(GameLevel::FrameTime);
876   level.gamePaused = wasPaused;
877   //writeln("level.framesProcessedFromLastClear=", level.framesProcessedFromLastClear);
881 // ////////////////////////////////////////////////////////////////////////// //
882 private float currFrameDelta; // so level renderer can properly interpolate the player
883 private GameLevel::IVec2D camPrev, camCurr;
884 private GameLevel::IVec2D camShake;
885 private GameLevel::IVec2D viewCameraPos;
888 final void teleportCameraAt (const ref GameLevel::IVec2D pos) {
889   camPrev.x = pos.x;
890   camPrev.y = pos.y;
891   camCurr.x = pos.x;
892   camCurr.y = pos.y;
893   viewCameraPos.x = pos.x;
894   viewCameraPos.y = pos.y;
895   camShake.x = 0;
896   camShake.y = 0;
900 // call `recalcCameraCoords()` to get real camera coords after this
901 final void setNewCameraPos (const ref GameLevel::IVec2D pos, optional bool doTeleport) {
902   // check if camera is moved too far, and teleport it
903   if (doTeleport ||
904       (abs(camCurr.x-pos.x)/global.scale >= 16*4 ||
905        abs(camCurr.y-pos.y)/global.scale >= 16*4))
906   {
907     teleportCameraAt(pos);
908   } else {
909     camPrev.x = camCurr.x;
910     camPrev.y = camCurr.y;
911     camCurr.x = pos.x;
912     camCurr.y = pos.y;
913   }
914   camShake.x = level.shakeDir.x*global.scale;
915   camShake.y = level.shakeDir.y*global.scale;
919 final void recalcCameraCoords (float frameDelta, optional bool moveSounds) {
920   currFrameDelta = frameDelta;
921   viewCameraPos.x = round(camPrev.x+(camCurr.x-camPrev.x)*frameDelta);
922   viewCameraPos.y = round(camPrev.y+(camCurr.y-camPrev.y)*frameDelta);
924   viewCameraPos.x += camShake.x;
925   viewCameraPos.y += camShake.y;
929 GameLevel::SavedKeyState savedKeyState;
931 final void pauseGame () {
932   if (!level.gamePaused) {
933     if (doGameSavingPlaying != Replay.None) level.keysSaveState(savedKeyState);
934     level.gamePaused = true;
935     global.pauseAllSounds();
936   }
940 final void unpauseGame () {
941   if (level.gamePaused) {
942     if (doGameSavingPlaying != Replay.None) level.keysRestoreState(savedKeyState);
943     level.gamePaused = false;
944     level.gameShowHelp = false;
945     level.gameHelpScreen = 0;
946     //lastThinkerTime = 0;
947     global.resumeAllSounds();
948   }
949   pauseRequested = false;
950   helpRequested = false;
951   showHelp = false;
955 final void beforeNewFrame (bool frameSkip) {
956   /*
957   if (freeRide) {
958     level.disablePlayerThink = true;
960     int delta = 2;
961     if (level.isKeyDown(GameConfig::Key.Attack)) delta *= 2;
962     if (level.isKeyDown(GameConfig::Key.Jump)) delta *= 4;
963     if (level.isKeyDown(GameConfig::Key.Run)) delta /= 2;
965     if (level.isKeyDown(GameConfig::Key.Left)) level.viewStart.x -= delta;
966     if (level.isKeyDown(GameConfig::Key.Right)) level.viewStart.x += delta;
967     if (level.isKeyDown(GameConfig::Key.Up)) level.viewStart.y -= delta;
968     if (level.isKeyDown(GameConfig::Key.Down)) level.viewStart.y += delta;
969   } else {
970     level.disablePlayerThink = false;
971     level.fixCamera();
972   }
973   */
974   level.fixCamera();
976   if (!level.gamePaused) {
977     // save seeds for afterframe processing
978     /*
979     if (doGameSavingPlaying == Replay.Saving && debugMovement) {
980       debugMovement.otherSeed = global.globalOtherSeed;
981       debugMovement.roomSeed = global.globalRoomSeed;
982     }
983     */
985     if (doGameSavingPlaying == Replay.Replaying && !debugMovement) stopReplaying();
987 #ifdef BIGGER_REPLAY_DATA
988     if (doGameSavingPlaying == Replay.Saving && debugMovement) {
989       debugMovement.keypresses.length += 1;
990       level.keysSaveState(debugMovement.keypresses[$-1]);
991       debugMovement.keypresses[$-1].otherSeed = global.globalOtherSeed;
992       debugMovement.keypresses[$-1].roomSeed = global.globalRoomSeed;
993     }
994 #endif
996     if (doGameSavingPlaying == Replay.Replaying && debugMovement) {
997 #ifdef BIGGER_REPLAY_DATA
998       if (debugMovement.keypos < debugMovement.keypresses.length) {
999         level.keysRestoreState(debugMovement.keypresses[debugMovement.keypos]);
1000         global.globalOtherSeed = debugMovement.keypresses[debugMovement.keypos].otherSeed;
1001         global.globalRoomSeed = debugMovement.keypresses[debugMovement.keypos].roomSeed;
1002         ++debugMovement.keypos;
1003       }
1004 #else
1005       for (;;) {
1006         int kbidx;
1007         bool down;
1008         auto code = debugMovement.getKey(out kbidx, out down);
1009         if (code == DebugSessionMovement::END_OF_RECORD) {
1010           // do this in main loop, so we can view totals
1011           //stopReplaying();
1012           break;
1013         }
1014         if (code == DebugSessionMovement::END_OF_FRAME) {
1015           break;
1016         }
1017         if (code != DebugSessionMovement::NORMAL) FatalError("UNKNOWN REPLAY CODE");
1018         level.onKey(1<<(kbidx/GameConfig::MaxActionBinds), down);
1019       }
1020 #endif
1021     }
1022   }
1026 final void afterNewFrame (bool frameSkip) {
1027   if (!replayFastForward) replaySkipFrame = 0;
1029   if (level.gamePaused) return;
1031   if (!level.gamePaused) {
1032     if (doGameSavingPlaying != Replay.None) {
1033       if (doGameSavingPlaying == Replay.Saving) {
1034         replayFastForward = false; // just in case
1035 #ifndef BIGGER_REPLAY_DATA
1036         debugMovement.addEndOfFrame();
1037 #endif
1038         auto stt = GetTickCount();
1039         if (stt-saveMovementLastTime >= 20) saveGameMovement(dbgSessionMovementFileName);
1040       } else if (doGameSavingPlaying == Replay.Replaying) {
1041         if (!frameSkip && replayFastForward && replaySkipFrame == 0) {
1042           replaySkipFrame = 1;
1043         }
1044       }
1045     }
1046   }
1048   //SoundSystem.ListenerOrigin = vector(level.player.fltx, level.player.flty);
1049   //SoundSystem.UpdateSounds();
1051   //if (!freeRide) level.fixCamera();
1052   setNewCameraPos(level.viewStart);
1053   /*
1054   prevCameraX = currCameraX;
1055   prevCameraY = currCameraY;
1056   currCameraX = level.cameraX;
1057   currCameraY = level.cameraY;
1058   // disable camera interpolation if the screen is shaking
1059   if (level.shakeX|level.shakeY) {
1060     prevCameraX = currCameraX;
1061     prevCameraY = currCameraY;
1062     return;
1063   }
1064   // disable camera interpolation if it moves too far away
1065   if (fabs(prevCameraX-currCameraX) > 64) prevCameraX = currCameraX;
1066   if (fabs(prevCameraY-currCameraY) > 64) prevCameraY = currCameraY;
1067   */
1068   recalcCameraCoords(config.interpolateMovement ? 0.0 : 1.0, moveSounds:true); // recalc camera coords
1070   if (pauseRequested && level.framesProcessedFromLastClear > 1) {
1071     pauseRequested = false;
1072     pauseGame();
1073     if (helpRequested) {
1074       helpRequested = false;
1075       level.gameShowHelp = true;
1076       level.gameHelpScreen = 0;
1077       showHelp = 2;
1078     } else {
1079       if (!showHelp) showHelp = true;
1080     }
1081     return;
1082   }
1086 final void interFrame (float frameDelta) {
1087   if (!config.interpolateMovement) return;
1088   recalcCameraCoords(frameDelta);
1092 final void cameraTeleportedCB () {
1093   teleportCameraAt(level.viewStart);
1094   recalcCameraCoords(0);
1098 // ////////////////////////////////////////////////////////////////////////// //
1099 #ifdef MASK_TEST
1100 final void setColorByIdx (bool isset, int col) {
1101   if (col == -666) {
1102     // missed collision: red
1103     Video.color = (isset ? 0x3f_ff_00_00 : 0xcf_ff_00_00);
1104   } else if (col == -999) {
1105     // superfluous collision: blue
1106     Video.color = (isset ? 0x3f_00_00_ff : 0xcf_00_00_ff);
1107   } else if (col <= 0) {
1108     // no collision: yellow
1109     Video.color = (isset ? 0x3f_ff_ff_00 : 0xcf_ff_ff_00);
1110   } else if (col > 0) {
1111     // collision: green
1112     Video.color = (isset ? 0x3f_00_ff_00 : 0xcf_00_ff_00);
1113   }
1117 final void drawMaskSimple (SpriteFrame frm, int xofs, int yofs) {
1118   if (!frm) return;
1119   CollisionMask cm = CollisionMask.Create(frm, false);
1120   if (!cm) return;
1121   int scale = global.config.scale;
1122   int bx0, by0, bx1, by1;
1123   frm.getBBox(out bx0, out by0, out bx1, out by1, false);
1124   Video.color = 0x7f_00_00_ff;
1125   Video.fillRect(xofs+bx0*scale, yofs+by0*scale, (bx1-bx0+1)*scale, (by1-by0+1)*scale);
1126   if (!cm.isEmptyMask) {
1127     //writeln(cm.mask.length, "; ", cm.width, "x", cm.height, "; (", cm.x0, ",", cm.y0, ")-(", cm.x1, ",", cm.y1, ")");
1128     foreach (int iy; 0..cm.height) {
1129       foreach (int ix; 0..cm.width) {
1130         int v = cm.mask[ix, iy];
1131         foreach (int dx; 0..32) {
1132           int xx = ix*32+dx;
1133           if (v < 0) {
1134             Video.color = 0x3f_00_ff_00;
1135             Video.fillRect(xofs+xx*scale, yofs+iy*scale, scale, scale);
1136           }
1137           v <<= 1;
1138         }
1139       }
1140     }
1141   } else {
1142     // bounding box
1143     /+
1144     foreach (int iy; 0..frm.tex.height) {
1145       foreach (int ix; 0..(frm.tex.width+31)/31) {
1146         foreach (int dx; 0..32) {
1147           int xx = ix*32+dx;
1148           //if (xx >= frm.bx && xx < frm.bx+frm.bw && iy >= frm.by && iy < frm.by+frm.bh) {
1149           if (xx >= x0 && xx <= x1 && iy >= y0 && iy <= y1) {
1150             setColorByIdx(true, col);
1151             if (col <= 0) Video.color = 0xaf_ff_ff_00;
1152           } else {
1153             Video.color = 0xaf_00_ff_00;
1154           }
1155           Video.fillRect(sx+xx*scale, sy+iy*scale, scale, scale);
1156         }
1157       }
1158     }
1159     +/
1160     /*
1161     if (frm.bw > 0 && frm.bh > 0) {
1162       setColorByIdx(true, col);
1163       Video.fillRect(x0+frm.bx*scale, y0+frm.by*scale, frm.bw*scale, frm.bh*scale);
1164       Video.color = 0xff_00_00;
1165       Video.drawRect(x0+frm.bx*scale, y0+frm.by*scale, frm.bw*scale, frm.bh*scale);
1166     }
1167     */
1168   }
1169   delete cm;
1171 #endif
1174 // ////////////////////////////////////////////////////////////////////////// //
1175 transient int drawStats;
1176 transient array!int statsTopItem;
1179 final bool totalsNameCmpCB (ref GameStats::TotalItem a, ref GameStats::TotalItem b) {
1180   auto sa = string(a.objName).toUpperCase;
1181   auto sb = string(b.objName).toUpperCase;
1182   return (sa < sb);
1186 final int getStatsTopItem () {
1187   return max(0, (drawStats >= 0 && drawStats < statsTopItem.length ? statsTopItem[drawStats] : 0));
1191 final void setStatsTopItem (int val) {
1192   if (drawStats <= statsTopItem.length) statsTopItem.length = drawStats+1;
1193   statsTopItem[drawStats] = val;
1197 final void resetStatsTopItem () {
1198   setStatsTopItem(0);
1202 void statsDrawGetStartPosLoadFont (out int currX, out int currY) {
1203   sprStore.loadFont('sFontSmall');
1204   currX = 64;
1205   currY = 34;
1209 final int calcStatsVisItems () {
1210   int scale = 3;
1211   int currX, currY;
1212   statsDrawGetStartPosLoadFont(currX, currY);
1213   int endY = level.viewHeight-(currY*2);
1214   return max(1, endY/sprStore.getFontHeight(scale));
1218 int getStatsItemCount () {
1219   switch (drawStats) {
1220     case 2: return level.stats.totalKills.length;
1221     case 3: return level.stats.totalDeaths.length;
1222     case 4: return level.stats.totalCollected.length;
1223   }
1224   return -1;
1228 final void statsMoveUp () {
1229   int count = getStatsItemCount();
1230   if (count < 0) return;
1231   int visItems = calcStatsVisItems();
1232   if (count <= visItems) { resetStatsTopItem(); return; }
1233   int top = getStatsTopItem();
1234   if (!top) return;
1235   setStatsTopItem(top-1);
1239 final void statsMoveDown () {
1240   int count = getStatsItemCount();
1241   if (count < 0) return;
1242   int visItems = calcStatsVisItems();
1243   if (count <= visItems) { resetStatsTopItem(); return; }
1244   int top = getStatsTopItem();
1245   //writeln("top=", top, "; count=", count, "; visItems=", visItems, "; maxtop=", count-visItems+1);
1246   top = clamp(top+1, 0, count-visItems);
1247   setStatsTopItem(top);
1251 void drawTotalsList (string pfx, ref array!(GameStats::TotalItem) arr) {
1252   arr.sort(&totalsNameCmpCB);
1253   int scale = 3;
1255   int currX, currY;
1256   statsDrawGetStartPosLoadFont(currX, currY);
1258   int endY = level.viewHeight-(currY*2);
1259   int visItems = calcStatsVisItems();
1261   if (arr.length <= visItems) resetStatsTopItem();
1263   int topItem = getStatsTopItem();
1265   // "upscroll" mark
1266   if (topItem > 0) {
1267     Video.color = 0x3f_ff_ff_00;
1268     auto spr = sprStore['sPageUp'];
1269     spr.frames[0].tex.blitAt(currX-28, currY, scale);
1270   }
1272   // "downscroll" mark
1273   if (topItem+visItems < arr.length) {
1274     Video.color = 0x3f_ff_ff_00;
1275     auto spr = sprStore['sPageDown'];
1276     spr.frames[0].tex.blitAt(currX-28, endY+3/*-sprStore.getFontHeight(scale)*/, scale);
1277   }
1279   Video.color = 0xff_ff_00;
1280   int hiColor = 0x00_ff_00;
1281   int hiColor1 = 0xf_ff_ff;
1283   int it = topItem;
1284   while (it < arr.length && visItems-- > 0) {
1285     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);
1286     currY += sprStore.getFontHeight(scale);
1287     ++it;
1288   }
1292 void drawStatsScreen () {
1293   int deathCount, killCount, collectCount;
1295   sprStore.loadFont('sFontSmall');
1297   Video.color = 0xff_ff_ff;
1298   level.drawTextAtS3Centered(240-2-8, "ESC-RETURN  F10-QUIT  CTRL+DEL-SUICIDE");
1299   level.drawTextAtS3Centered(2, "~O~PTIONS  REDEFINE ~K~EYS  ~S~TATISTICS", 0xff_7f_00);
1301   Video.color = 0xff_ff_00;
1302   int hiColor = 0x00_ff_00;
1304   switch (drawStats) {
1305     case 2: drawTotalsList("KILLED", level.stats.totalKills); return;
1306     case 3: drawTotalsList("DIED FROM", level.stats.totalDeaths); return;
1307     case 4: drawTotalsList("COLLECTED", level.stats.totalCollected); return;
1308   }
1310   if (drawStats > 1) {
1311     // turn off
1312     foreach (ref auto i; statsTopItem) i = 0;
1313     drawStats = 0;
1314     return;
1315   }
1317   foreach (ref auto ti; level.stats.totalDeaths) deathCount += ti.count;
1318   foreach (ref auto ti; level.stats.totalKills) killCount += ti.count;
1319   foreach (ref auto ti; level.stats.totalCollected) collectCount += ti.count;
1321   int currX = 64;
1322   int currY = 96;
1323   int scale = 3;
1325   sprStore.renderTextWithHighlight(currX, currY, va("MAXIMUM MONEY YOU GOT IS ~%d~", level.stats.maxMoney), scale, hiColor);
1326   currY += sprStore.getFontHeight(scale);
1328   int gw = level.stats.gamesWon;
1329   sprStore.renderTextWithHighlight(currX, currY, va("YOU WON ~%d~ GAME%s", gw, (gw != 1 ? "S" : "")), scale, hiColor);
1330   currY += sprStore.getFontHeight(scale);
1332   sprStore.renderTextWithHighlight(currX, currY, va("YOU DIED ~%d~ TIMES", deathCount), scale, hiColor);
1333   currY += sprStore.getFontHeight(scale);
1335   sprStore.renderTextWithHighlight(currX, currY, va("YOU KILLED ~%d~ CREATURES", killCount), scale, hiColor);
1336   currY += sprStore.getFontHeight(scale);
1338   sprStore.renderTextWithHighlight(currX, currY, va("YOU COLLECTED ~%d~ TREASURE ITEMS", collectCount), scale, hiColor);
1339   currY += sprStore.getFontHeight(scale);
1341   sprStore.renderTextWithHighlight(currX, currY, va("YOU SAVED ~%d~ DAMSELS", level.stats.totalDamselsSaved), scale, hiColor);
1342   currY += sprStore.getFontHeight(scale);
1344   sprStore.renderTextWithHighlight(currX, currY, va("YOU STOLE ~%d~ IDOLS", level.stats.totalIdolsStolen), scale, hiColor);
1345   currY += sprStore.getFontHeight(scale);
1347   sprStore.renderTextWithHighlight(currX, currY, va("YOU SOLD ~%d~ IDOLS", level.stats.totalIdolsConverted), scale, hiColor);
1348   currY += sprStore.getFontHeight(scale);
1350   sprStore.renderTextWithHighlight(currX, currY, va("YOU STOLE ~%d~ CRYSTAL SKULLS", level.stats.totalCrystalIdolsStolen), scale, hiColor);
1351   currY += sprStore.getFontHeight(scale);
1353   sprStore.renderTextWithHighlight(currX, currY, va("YOU SOLD ~%d~ CRYSTAL SKULLS", level.stats.totalCrystalIdolsConverted), scale, hiColor);
1354   currY += sprStore.getFontHeight(scale);
1356   int gs = level.stats.totalGhostSummoned;
1357   sprStore.renderTextWithHighlight(currX, currY, va("YOU SUMMONED ~%d~ GHOST%s", gs, (gs != 1 ? "S" : "")), scale, hiColor);
1358   currY += sprStore.getFontHeight(scale);
1360   currY += sprStore.getFontHeight(scale);
1361   sprStore.renderTextWithHighlight(currX, currY, va("TOTAL PLAYING TIME: ~%s~", GameLevel.time2str(level.stats.playingTime)), scale, hiColor);
1362   currY += sprStore.getFontHeight(scale);
1366 void onDraw () {
1367   if (Video.frameTime == 0) {
1368     onTimePasses();
1369     Video.requestRefresh();
1370   }
1372   if (!level) return;
1374   if (level.framesProcessedFromLastClear < 1) return;
1375   calcMouseMapCoords();
1377   Video.stencil = true; // you NEED this to be set! (stencil buffer is used for lighting)
1378   Video.clearScreen();
1379   Video.stencil = false;
1380   Video.color = 0xff_ff_ff;
1381   Video.textureFiltering = false;
1382   // don't touch framebuffer alpha
1383   Video.colorMask = Video::CMask.Colors;
1385   Video::ScissorRect scsave;
1386   bool doRestoreGL = false;
1388   /*
1389   if (level.viewOffsetX > 0 || level.viewOffsetY > 0) {
1390     doRestoreGL = true;
1391     Video.getScissor(scsave);
1392     Video.scissorCombine(level.viewOffsetX, level.viewOffsetY, level.viewWidth, level.viewHeight);
1393     Video.glPushMatrix();
1394     Video.glTranslate(level.viewOffsetX, level.viewOffsetY);
1395   }
1396   */
1398   if (level.viewWidth != Video.screenWidth || level.viewHeight != Video.screenHeight) {
1399     doRestoreGL = true;
1400     float scx = float(Video.screenWidth)/float(level.viewWidth);
1401     float scy = float(Video.screenHeight)/float(level.viewHeight);
1402     float scale = fmin(scx, scy);
1403     int calcedW = trunc(level.viewWidth*scale);
1404     int calcedH = trunc(level.viewHeight*scale);
1405     Video.getScissor(scsave);
1406     int ofsx = (Video.screenWidth-calcedW)/2;
1407     int ofsy = (Video.screenHeight-calcedH)/2;
1408     Video.scissorCombine(ofsx, ofsy, calcedW, calcedH);
1409     Video.glPushMatrix();
1410     Video.glTranslate(ofsx, ofsy);
1411     Video.glScale(scale, scale);
1412   }
1414   //level.viewOffsetX = (Video.screenWidth-320*3)/2;
1415   //level.viewOffsetY = (Video.screenHeight-240*3)/2;
1417   if (fullscreen) {
1418     /*
1419     level.viewOffsetX = 0;
1420     level.viewOffsetY = 0;
1421     Video.glScale(float(Video.screenWidth)/float(level.viewWidth), float(Video.screenHeight)/float(level.viewHeight));
1422     */
1423     /*
1424     float scx = float(Video.screenWidth)/float(level.viewWidth);
1425     float scy = float(Video.screenHeight)/float(level.viewHeight);
1426     Video.glScale(float(Video.screenWidth)/float(level.viewWidth), 1);
1427     */
1428   }
1431   level.renderWithOfs(viewCameraPos.x, viewCameraPos.y, currFrameDelta);
1433   if (level.gamePaused && showHelp != 2) {
1434     if (mouseLevelX != int.min) {
1435       int scale = level.global.scale;
1436       if (renderMouseRect) {
1437         Video.color = 0xcf_ff_ff_00;
1438         Video.fillRect(mouseLevelX*scale-viewCameraPos.x, mouseLevelY*scale-viewCameraPos.y, 12*scale, 14*scale);
1439       }
1440       if (renderMouseTile) {
1441         Video.color = 0xaf_ff_00_00;
1442         Video.fillRect((mouseLevelX&~15)*scale-viewCameraPos.x, (mouseLevelY&~15)*scale-viewCameraPos.y, 16*scale, 16*scale);
1443       }
1444     }
1445   }
1447   switch (doGameSavingPlaying) {
1448     case Replay.Saving:
1449       Video.color = 0x7f_00_ff_00;
1450       sprStore.loadFont('sFont');
1451       sprStore.renderText(level.viewWidth-sprStore.getTextWidth("S", 2)-2, 2, "S", 2);
1452       break;
1453     case Replay.Replaying:
1454       if (level.player && !level.player.dead) {
1455         Video.color = 0x7f_ff_00_00;
1456         sprStore.loadFont('sFont');
1457         sprStore.renderText(level.viewWidth-sprStore.getTextWidth("R", 2)-2, 2, "R", 2);
1458         int th = sprStore.getFontHeight(2);
1459         if (replayFastForward) {
1460           sprStore.loadFont('sFontSmall');
1461           string sstr = va("x%d", replayFastForwardSpeed+1);
1462           sprStore.renderText(level.viewWidth-sprStore.getTextWidth(sstr, 2)-2, 2+th, sstr, 2);
1463         }
1464       }
1465       break;
1466     default:
1467       if (saveGameSession) {
1468         Video.color = 0x7f_ff_7f_00;
1469         sprStore.loadFont('sFont');
1470         sprStore.renderText(level.viewWidth-sprStore.getTextWidth("S", 2)-2, 2, "S", 2);
1471       }
1472       break;
1473   }
1476   if (level.player && level.player.dead && !showHelp) {
1477     // darken
1478     Video.color = 0x8f_00_00_00;
1479     Video.fillRect(0, 0, level.viewWidth, level.viewHeight);
1480     // draw text
1481     if (drawStats) {
1482       drawStatsScreen();
1483     } else {
1484       if (true /*level.inWinCutscene == 0*/) {
1485         Video.color = 0xff_ff_ff;
1486         sprStore.loadFont('sFontSmall');
1487         string kmsg = va((level.stats.newMoneyRecord ? "NEW HIGH SCORE: |%d|\n" : "SCORE: |%d|\n")~
1488                          "\n"~
1489                          "PRESS $PAY TO RESTART GAME\n"~
1490                          "\n"~
1491                          "PRESS ~ESCAPE~ TO EXIT TO TITLE\n"~
1492                          "\n"~
1493                          "TOTAL PLAYING TIME: |%s|"~
1494                          "",
1495                          (level.levelKind == GameLevel::LevelKind.Stars ? level.starsKills :
1496                           level.levelKind == GameLevel::LevelKind.Sun ? level.sunScore :
1497                           level.levelKind == GameLevel::LevelKind.Moon ? level.moonScore :
1498                           level.stats.money),
1499                          GameLevel.time2str(level.stats.playingTime)
1500                         );
1501         kmsg = global.expandString(kmsg);
1502         sprStore.renderMultilineTextCentered(level.viewWidth/2, -level.viewHeight, kmsg, 3, 0x00_ff_00, 0x00_ff_ff);
1503       }
1504     }
1505   }
1507 #ifdef MASK_TEST
1508   {
1509     Video.color = 0xff_7f_00;
1510     sprStore.loadFont('sFontSmall');
1511     sprStore.renderText(8, level.viewHeight-20, va("%s; FRAME:%d", (smask.precise ? "PRECISE" : "HITBOX"), maskFrame), 2);
1512     auto spf = smask.frames[maskFrame];
1513     sprStore.renderText(8, level.viewHeight-20-16, va("OFS=(%d,%d); BB=(%d,%d)x(%d,%d); EMPTY:%s; PRECISE:%s",
1514       spf.xofs, spf.yofs,
1515       spf.bx, spf.by, spf.bw, spf.bh,
1516       (spf.maskEmpty ? "TAN" : "ONA"),
1517       (spf.precise ? "TAN" : "ONA")),
1518       2
1519     );
1520     //spf.tex.blitAt(maskSX*global.config.scale-viewCameraPos.x, maskSY*global.config.scale-viewCameraPos.y, global.config.scale);
1521     //writeln("pos=(", maskSX, ",", maskSY, ")");
1522     int scale = global.config.scale;
1523     int xofs = viewCameraPos.x, yofs = viewCameraPos.y;
1524     int mapX = xofs/scale+maskSX;
1525     int mapY = yofs/scale+maskSY;
1526     mapX -= spf.xofs;
1527     mapY -= spf.yofs;
1528     writeln("==== tiles ====");
1529     /*
1530     level.touchTilesWithMask(mapX, mapY, spf, delegate bool (MapTile t) {
1531       if (t.spectral || !t.isInstanceAlive) return false;
1532       Video.color = 0x7f_ff_00_00;
1533       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);
1534       auto tsf = t.getSpriteFrame();
1536       auto spf = smask.frames[maskFrame];
1537       int xofs = viewCameraPos.x, yofs = viewCameraPos.y;
1538       int mapX = xofs/global.config.scale+maskSX;
1539       int mapY = yofs/global.config.scale+maskSY;
1540       mapX -= spf.xofs;
1541       mapY -= spf.yofs;
1542       //bool hit = spf.pixelCheck(tsf, t.ix-mapX, t.iy-mapY);
1543       bool hit = tsf.pixelCheck(spf, mapX-t.ix, mapY-t.iy);
1544       writeln("  tile '", t.objName, "': precise=", tsf.precise, "; hit=", hit);
1545       return false;
1546     });
1547     */
1548     level.touchObjectsWithMask(mapX, mapY, spf, delegate bool (MapObject t) {
1549       Video.color = 0x7f_ff_00_00;
1550       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);
1551       return false;
1552     });
1553     //
1554     drawMaskSimple(spf, mapX*scale-xofs, mapY*scale-yofs);
1555     // mask
1556     Video.color = 0xaf_ff_ff_ff;
1557     spf.tex.blitAt(mapX*scale-xofs, mapY*scale-yofs, scale);
1558     Video.color = 0xff_ff_00;
1559     Video.drawRect((mapX+spf.bx)*scale-xofs, (mapY+spf.by)*scale-yofs, spf.bw*scale, spf.bh*scale);
1560     // player colbox
1561     {
1562       bool doMirrorSelf;
1563       int fx0, fy0, fx1, fy1;
1564       auto pfm = level.player.getSpriteFrame(out doMirrorSelf, out fx0, out fy0, out fx1, out fy1);
1565       Video.color = 0x7f_00_00_ff;
1566       Video.fillRect((level.player.ix+fx0)*scale-xofs, (level.player.iy+fy0)*scale-yofs, (fx1-fx0)*scale, (fy1-fy0)*scale);
1567     }
1568   }
1569 #endif
1571   if (showHelp) {
1572     Video.color = 0x8f_00_00_00;
1573     Video.fillRect(0, 0, level.viewWidth, level.viewHeight);
1574     if (optionsPane) {
1575       optionsPane.drawWithOfs(optionsPaneOfs.x+32, optionsPaneOfs.y+32);
1576     } else {
1577       if (drawStats) {
1578         drawStatsScreen();
1579       } else {
1580         Video.color = 0xff_ff_00;
1581         //if (showHelp > 1) Video.color = 0xaf_ff_ff_00;
1582         if (showHelp == 1) {
1583           int msx, msy, ww, wh;
1584           Video.getMousePos(out msx, out msy);
1585           Video.getRealWindowSize(out ww, out wh);
1586           if (msx >= 0 && msy >= 0 && msx < ww && msy < wh) {
1587             sprStore.loadFont('sFontSmall');
1588             Video.color = 0xff_ff_00;
1589             sprStore.renderTextWrapped(16, 16, (320-16)*2,
1590               "F1: show this help\n"~
1591               "O : options\n"~
1592               "K : redefine keys\n"~
1593               "I : toggle interpolaion\n"~
1594               "N : create some blood\n"~
1595               "R : generate a new level\n"~
1596               "F : toggle \"Frozen Area\"\n"~
1597               "X : resurrect player\n"~
1598               "Q : teleport to exit\n"~
1599               "D : teleport to damel\n"~
1600               "--------------\n"~
1601               "C : cheat flags menu\n"~
1602               "P : cheat pickup menu\n"~
1603               "E : cheat enemy menu\n"~
1604               "Enter: cheat items menu\n"~
1605               "\n"~
1606               "TAB: toggle 'freeroam' mode\n"~
1607               "",
1608               2);
1609           }
1610         } else {
1611           if (level) level.renderPauseOverlay();
1612         }
1613       }
1614     }
1615     //SoundSystem.UpdateSounds();
1616   }
1617   //sprStore.renderText(16, 16, "SPELUNKY!", 2);
1619   if (doRestoreGL) {
1620     Video.setScissor(scsave);
1621     Video.glPopMatrix();
1622   }
1625   if (TigerEye) {
1626     Video.color = 0xaf_ff_ff_ff;
1627     texTigerEye.blitAt(Video.screenWidth-texTigerEye.width-2, Video.screenHeight-texTigerEye.height-2);
1628   }
1632 // ////////////////////////////////////////////////////////////////////////// //
1633 transient bool gameJustOver;
1634 transient bool waitingForPayRestart;
1637 final void calcMouseMapCoords () {
1638   if (mouseX == int.min || !level || level.framesProcessedFromLastClear < 1) {
1639     mouseLevelX = int.min;
1640     mouseLevelY = int.min;
1641     return;
1642   }
1643   mouseLevelX = (mouseX+viewCameraPos.x)/level.global.scale;
1644   mouseLevelY = (mouseY+viewCameraPos.y)/level.global.scale;
1645   //writeln("mappos: (", mouseLevelX, ",", mouseLevelY, ")");
1649 final void onEvent (ref event_t evt) {
1650   if (evt.type == ev_closequery) { Video.requestQuit(); return; }
1652   if (evt.type == ev_winfocus) {
1653     if (level && !evt.focused) {
1654       escCount = 0;
1655       level.clearKeys();
1656     }
1657     if (evt.focused) {
1658       //writeln("FOCUS!");
1659       Video.getMousePos(out mouseX, out mouseY);
1660     }
1661     return;
1662   }
1664   if (evt.type == ev_mouse) {
1665     mouseX = evt.x;
1666     mouseY = evt.y;
1667     calcMouseMapCoords();
1668   }
1670   if (evt.type == ev_keydown && evt.keycode == K_F12) {
1671     if (level) toggleFullscreen();
1672     return;
1673   }
1675   if (level && level.gamePaused && showHelp != 2 && evt.type == ev_keydown && evt.keycode == K_MOUSE2 && mouseLevelX != int.min) {
1676     writeln("TILE: ", mouseLevelX/16, ",", mouseLevelY/16);
1677     writeln("MAP : ", mouseLevelX, ",", mouseLevelY);
1678   }
1680   if (evt.type == ev_keydown) {
1681     if (evt.keycode == K_LCTRL || evt.keycode == K_RCTRL) evt.bCtrl = true;
1682     if (evt.keycode == K_LALT || evt.keycode == K_RALT) evt.bAlt = true;
1683     renderMouseTile = evt.bCtrl;
1684     renderMouseRect = evt.bAlt;
1685   }
1687   if (evt.type == ev_keyup) {
1688     if (evt.keycode == K_LCTRL || evt.keycode == K_RCTRL) evt.bCtrl = false;
1689     if (evt.keycode == K_LALT || evt.keycode == K_RALT) evt.bAlt = false;
1690     renderMouseTile = evt.bCtrl;
1691     renderMouseRect = evt.bAlt;
1692   }
1694   if (evt.type == ev_keyup && evt.keycode != K_ESCAPE) escCount = 0;
1696   if (evt.type == ev_keydown && evt.bShift && (evt.keycode >= "1" && evt.keycode <= "4")) {
1697     int newScale = evt.keycode-48;
1698     if (global.config.scale != newScale) {
1699       global.config.scale = newScale;
1700       if (level) {
1701         level.fixCamera();
1702         cameraTeleportedCB();
1703       }
1704     }
1705     return;
1706   }
1708 #ifdef MASK_TEST
1709   if (evt.type == ev_mouse) {
1710     maskSX = evt.x/global.config.scale;
1711     maskSY = evt.y/global.config.scale;
1712     return;
1713   }
1714   if (evt.type == ev_keydown && evt.keycode == K_PADMINUS) {
1715     maskFrame = max(0, maskFrame-1);
1716     return;
1717   }
1718   if (evt.type == ev_keydown && evt.keycode == K_PADPLUS) {
1719     maskFrame = clamp(maskFrame+1, 0, smask.frames.length-1);
1720     return;
1721   }
1722 #endif
1724   if (showHelp) {
1725     escCount = 0;
1727     if (optionsPane) {
1728       if (optionsPane.closeMe || (evt.type == ev_keyup && evt.keycode == K_ESCAPE)) {
1729         saveCurrentPane();
1730         if (saveOptionsDG) saveOptionsDG();
1731         saveOptionsDG = none;
1732         delete optionsPane;
1733         //SoundSystem.UpdateSounds(); // just in case
1734         if (global.hasSpectacles) level.pickedSpectacles();
1735         return;
1736       }
1737       optionsPane.onEvent(evt);
1738       return;
1739     }
1741     if (evt.type == ev_keyup && evt.keycode == K_ESCAPE) { unpauseGame(); return; }
1742     if (evt.type == ev_keydown) {
1743       if (evt.keycode == K_SPACE && level && showHelp == 2 && level.gameShowHelp) evt.keycode = K_RIGHTARROW;
1744       switch (evt.keycode) {
1745         case K_F1: if (showHelp == 2 && level) level.gameShowHelp = !level.gameShowHelp; if (level.gameShowHelp) level.gameHelpScreen = 0; return;
1746         case K_F2: if (showHelp != 2) unpauseGame(); return;
1747         case K_F10: Video.requestQuit(); return;
1748         case K_F11: if (showHelp != 2) showHelp = 3-showHelp; return;
1750         case K_UPARROW: case K_PAD8:
1751           if (drawStats) statsMoveUp();
1752           return;
1754         case K_DOWNARROW: case K_PAD2:
1755           if (drawStats) statsMoveDown();
1756           return;
1758         case K_LEFTARROW: case K_PAD4:
1759           if (level && showHelp == 2 && level.gameShowHelp) {
1760             if (level.gameHelpScreen) --level.gameHelpScreen; else level.gameHelpScreen = GameLevel::MaxGameHelpScreen;
1761           }
1762           return;
1764         case K_RIGHTARROW: case K_PAD6:
1765           if (level && showHelp == 2 && level.gameShowHelp) {
1766             level.gameHelpScreen = (level.gameHelpScreen+1)%(GameLevel::MaxGameHelpScreen+1);
1767           }
1768           return;
1770         case K_F6: {
1771           // save level
1772           saveGame("level");
1773           unpauseGame();
1774           return;
1775         }
1777         case K_F9: {
1778           // load level
1779           loadGame("level");
1780           resetFramesAndForceOne();
1781           unpauseGame();
1782           return;
1783         }
1785         case K_F5:
1786           if (/*evt.bCtrl &&*/ showHelp != 2) {
1787             global.plife = 99;
1788             unpauseGame();
1789           }
1790           return;
1792         case K_s:
1793           ++drawStats;
1794           return;
1796         case K_o: optionsPane = createOptionsPane(); restoreCurrentPane(); return;
1797         case K_k: optionsPane = createBindingsPane(); restoreCurrentPane(); return;
1798         case K_c: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatFlagsPane(); restoreCurrentPane(); } return;
1799         case K_p: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatPickupsPane(); restoreCurrentPane(); } return;
1800         case K_ENTER: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatItemsPane(); restoreCurrentPane(); } return;
1801         case K_e: if (/*evt.bCtrl &&*/ showHelp != 2) { optionsPane = createCheatEnemiesPane(); restoreCurrentPane(); } return;
1802         //case K_s: global.hasSpringShoes = !global.hasSpringShoes; return;
1803         //case K_j: global.hasJordans = !global.hasJordans; return;
1804         case K_x:
1805           if (/*evt.bCtrl &&*/ showHelp != 2) {
1806             level.resurrectPlayer();
1807             unpauseGame();
1808           }
1809           return;
1810         case K_r:
1811           //writeln("*** ROOM  SEED: ", global.globalRoomSeed);
1812           //writeln("*** OTHER SEED: ", global.globalOtherSeed);
1813           if (evt.bAlt && level.player && level.player.dead) {
1814             saveGameSession = false;
1815             replayGameSession = true;
1816             unpauseGame();
1817             return;
1818           }
1819           if (/*evt.bCtrl &&*/ showHelp != 2) {
1820             if (evt.bShift) global.idol = false;
1821             level.generateLevel();
1822             level.centerViewAtPlayer();
1823             teleportCameraAt(level.viewStart);
1824             resetFramesAndForceOne();
1825           }
1826           return;
1827         case K_m:
1828           global.toggleMusic();
1829           return;
1830         case K_q:
1831           if (/*evt.bCtrl &&*/ showHelp != 2) {
1832             if (level.allExits.length) {
1833               level.teleportPlayerTo(level.allExits[0].ix+8, level.allExits[0].iy+8);
1834               unpauseGame();
1835             }
1836           }
1837           return;
1838         case K_d:
1839           if (/*evt.bCtrl &&*/ level.player && showHelp != 2) {
1840             auto damsel = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa MonsterDamsel); });
1841             if (damsel) {
1842               level.teleportPlayerTo(damsel.ix, damsel.iy);
1843               unpauseGame();
1844             }
1845           }
1846           return;
1847         case K_h:
1848           if (/*evt.bCtrl &&*/ level.player && showHelp != 2) {
1849             MapObject obj;
1850             if (evt.bAlt) {
1851               // locked chest
1852               obj = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa ItemLockedChest); });
1853             } else {
1854               // key
1855               obj = level.findNearestObject(level.player.xCenter, level.player.yCenter, delegate bool (MapObject o) { return (o isa ItemGoldenKey); });
1856             }
1857             if (obj) {
1858               level.teleportPlayerTo(obj.ix, obj.iy-4);
1859               unpauseGame();
1860             }
1861           }
1862           return;
1863         case K_g:
1864           if (/*evt.bCtrl &&*/ showHelp != 2 && evt.bAlt) {
1865             if (level && mouseLevelX != int.min) {
1866               int scale = level.global.scale;
1867               int mapX = mouseLevelX;
1868               int mapY = mouseLevelY;
1869               level.MakeMapTile(mapX/16, mapY/16, 'oGoldDoor');
1870             }
1871             return;
1872           }
1873           break;
1874         case K_w:
1875           if (evt.bCtrl && showHelp != 2) {
1876             if (level && mouseLevelX != int.min) {
1877               int scale = level.global.scale;
1878               int mapX = mouseLevelX;
1879               int mapY = mouseLevelY;
1880               level.MakeMapObject(mapX/16*16, mapY/16*16, 'oWeb');
1881             }
1882             return;
1883           }
1884           break;
1885         case K_b:
1886           if (evt.bCtrl && showHelp != 2) {
1887             if (level && mouseLevelX != int.min) {
1888               int scale = level.global.scale;
1889               int mapX = mouseLevelX;
1890               int mapY = mouseLevelY;
1891               level.MakeMapTile(mapX/16, mapY/16, 'oPushBlock');
1892             }
1893             return;
1894           }
1895           if (evt.bAlt && showHelp != 2) {
1896             if (level && mouseLevelX != int.min) {
1897               int scale = level.global.scale;
1898               int mapX = mouseLevelX;
1899               int mapY = mouseLevelY;
1900               level.MakeMapTile(mapX/16, mapY/16, 'oDarkFall');
1901             }
1902             return;
1903           }
1904           /*
1905           if (evt.bAlt) {
1906             if (level && mouseLevelX != int.min) {
1907               int scale = level.global.scale;
1908               int mapX = mouseLevelX;
1909               int mapY = mouseLevelY;
1910               int wdt = 12;
1911               int hgt = 14;
1912               writeln("=== POS: (", mapX, ",", mapY, ")-(", mapX+wdt-1, ",", mapY+hgt-1, ") ===");
1913               level.checkTilesInRect(mapX, mapY, wdt, hgt, delegate bool (MapTile t) {
1914                 writeln("  tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "': (", t.ix, ",", t.iy, ")");
1915                 return false;
1916               });
1917               writeln(" ---");
1918               foreach (MapTile t; level.objGrid.inRectPix(mapX, mapY, wdt, hgt, precise:false, castClass:MapTile)) {
1919                 writeln("  tile(", GetClassName(t.Class), "): '", t.objType, ":", t.objName, "': (", t.ix, ",", t.iy, "); collision=", t.isRectCollision(mapX, mapY, wdt, hgt));
1920               }
1921             }
1922             return;
1923           }
1924           */
1925           if (/*evt.bAlt &&*/ showHelp != 2) {
1926             auto obj = ObjBoulder(level.MakeMapTile((level.player.ix+32)/16, (level.player.iy-16)/16, 'oBoulder'));
1927             //if (obj) obj.monkey = monkey;
1928             if (obj) {
1929               //playSound('sndThump');
1930               unpauseGame();
1931             }
1932           }
1933           return;
1935         case K_DELETE: // suicide
1936           if (doGameSavingPlaying == Replay.None) {
1937             if (level.player && !level.player.dead && evt.bCtrl) {
1938               global.hasAnkh = false;
1939               level.global.plife = 1;
1940               level.player.invincible = 0;
1941               auto xplo = MapObjExplosion(level.MakeMapObject(level.player.ix, level.player.iy, 'oExplosion'));
1942               if (xplo) xplo.suicide = true;
1943               unpauseGame();
1944             }
1945           }
1946           return;
1948         case K_INSERT:
1949           if (level.player && !level.player.dead && evt.bAlt) {
1950             if (doGameSavingPlaying != Replay.None) {
1951               if (doGameSavingPlaying == Replay.Replaying) {
1952                 stopReplaying();
1953               } else if (doGameSavingPlaying == Replay.Saving) {
1954                 saveGameMovement(dbgSessionMovementFileName, packit:true);
1955               }
1956               doGameSavingPlaying = Replay.None;
1957               stopReplaying();
1958               saveGameSession = false;
1959               replayGameSession = false;
1960               unpauseGame();
1961             }
1962           }
1963           return;
1965         case K_SPACE:
1966           if (/*evt.bCtrl && evt.bShift*/ showHelp != 2) {
1967             level.stats.setMoneyCheat();
1968             level.stats.addMoney(10000);
1969           }
1970           return;
1971       }
1972     }
1973   } else {
1974     if (evt.type == ev_keyup && evt.keycode == K_ESCAPE) {
1975       if (level.player && level.player.dead) {
1976         //Video.requestQuit();
1977         escCount = 0;
1978         if (gameJustOver) { gameJustOver = false; level.restartTitle(); }
1979       } else {
1980 #ifdef QUIT_DOUBLE_ESC
1981         if (++escCount == 2) Video.requestQuit();
1982 #else
1983         showHelp = 2;
1984         pauseRequested = true;
1985 #endif
1986       }
1987       return;
1988     }
1990     if (evt.type == ev_keydown && evt.keycode == K_F1) { pauseRequested = true; helpRequested = true; return; }
1991     if (evt.type == ev_keydown && evt.keycode == K_F2 && (evt.bShift || evt.bAlt)) { pauseRequested = true; return; }
1992     if (evt.type == ev_keydown && evt.keycode == K_BACKQUOTE && (evt.bShift || evt.bAlt)) { pauseRequested = true; return; }
1993   }
1995   //!if (evt.type == ev_keydown && evt.keycode == K_n) { level.player.scrCreateBlood(level.player.ix, level.player.iy, 3); return; }
1997   if (level) {
1998     if (!level.player || !level.player.dead) {
1999       gameJustOver = false;
2000     } else if (level.player && level.player.dead) {
2001       if (!gameJustOver) {
2002         drawStats = 0;
2003         gameJustOver = true;
2004         waitingForPayRestart = true;
2005         level.clearKeysPressRelease();
2006         if (doGameSavingPlaying == Replay.None) {
2007           stopReplaying(); // just in case
2008           saveGameStats();
2009         }
2010       }
2011       replayFastForward = false;
2012       if (doGameSavingPlaying == Replay.Saving) {
2013         if (debugMovement) saveGameMovement(dbgSessionMovementFileName, packit:true);
2014         doGameSavingPlaying = Replay.None;
2015         //clearGameMovement();
2016         saveGameSession = false;
2017         replayGameSession = false;
2018       }
2019     }
2020     if (evt.type == ev_keydown || evt.type == ev_keyup) {
2021       bool down = (evt.type == ev_keydown);
2022       if (doGameSavingPlaying == Replay.Replaying && level.player && !level.player.dead) {
2023         if (down && evt.keycode == K_f) {
2024           if (evt.bCtrl) {
2025             if (replayFastForwardSpeed != 4) {
2026               replayFastForwardSpeed = 4;
2027               replayFastForward = true;
2028             } else {
2029               replayFastForward = !replayFastForward;
2030             }
2031           } else {
2032             replayFastForwardSpeed = 2;
2033             replayFastForward = !replayFastForward;
2034           }
2035         }
2036       }
2037       if (doGameSavingPlaying != Replay.Replaying || !level.player || level.player.dead) {
2038         foreach (int kbidx, int kval; global.config.keybinds) {
2039           if (kval && kval == evt.keycode) {
2040 #ifndef BIGGER_REPLAY_DATA
2041             if (doGameSavingPlaying == Replay.Saving) debugMovement.addKey(kbidx, down);
2042 #endif
2043             level.onKey(1<<(kbidx/GameConfig::MaxActionBinds), down);
2044           }
2045         }
2046       }
2047       if (level.player && level.player.dead) {
2048         if (down && evt.keycode == K_r && evt.bAlt) {
2049           saveGameSession = false;
2050           replayGameSession = true;
2051           unpauseGame();
2052         }
2053         if (down && evt.keycode == K_s && evt.bAlt) {
2054           bool wasSaveReq = saveGameSession;
2055           stopReplaying(); // just in case
2056           saveGameSession = !wasSaveReq;
2057           replayGameSession = false;
2058           //unpauseGame();
2059         }
2060         if (replayGameSession) {
2061           stopReplaying(); // just in case
2062           saveGameSession = false;
2063           replayGameSession = false;
2064           loadGameMovement(dbgSessionMovementFileName);
2065           loadGame(dbgSessionStateFileName);
2066           doGameSavingPlaying = Replay.Replaying;
2067         } else {
2068           if (down && evt.keycode == K_s && !evt.bAlt) ++drawStats;
2069           if (waitingForPayRestart) {
2070             level.isKeyReleased(GameConfig::Key.Pay);
2071             if (level.isKeyPressed(GameConfig::Key.Pay)) waitingForPayRestart = false;
2072           } else {
2073             level.isKeyPressed(GameConfig::Key.Pay);
2074             if (level.isKeyReleased(GameConfig::Key.Pay)) {
2075               auto doSave = saveGameSession;
2076               stopReplaying(); // just in case
2077               level.clearKeysPressRelease();
2078               level.restartGame();
2079               level.generateNormalLevel();
2080               if (doSave) {
2081                 saveGameSession = false;
2082                 replayGameSession = false;
2083                 writeln("DBG: saving game session...");
2084                 clearGameMovement();
2085                 doGameSavingPlaying = Replay.Saving;
2086                 saveGame(dbgSessionStateFileName);
2087                 //saveGameMovement(dbgSessionMovementFileName);
2088               }
2089             }
2090           }
2091         }
2092       }
2093     }
2094   }
2098 void levelExited () {
2099   // just in case
2100   saveGameStats();
2104 void initializeVideo () {
2105   Video.openScreen("Spelunky/VaVoom C", 320*(fullscreen ? 4 : 3), 240*(fullscreen ? 4 : 3), (fullscreen ? global.config.fsmode : 0));
2106   if (Video.realStencilBits < 8) {
2107     Video.closeScreen();
2108     FatalError("=== YOUR GPU SUX! ===\nno stencil buffer!");
2109   }
2110   if (!Video.framebufferHasAlpha) {
2111     Video.closeScreen();
2112     FatalError("=== YOUR GPU SUX! ===\nno alpha channel in framebuffer!");
2113   }
2114   if (fullscreen) Video.hideMouseCursor();
2118 void toggleFullscreen () {
2119   Video.showMouseCursor();
2120   Video.closeScreen();
2121   fullscreen = !fullscreen;
2122   initializeVideo();
2126 final void runGameLoop () {
2127   Video.frameTime = 0; // unlimited FPS
2128   lastThinkerTime = 0;
2130   sprStore = SpawnObject(SpriteStore);
2131   sprStore.bDumpLoaded = false;
2133   bgtileStore = SpawnObject(BackTileStore);
2134   bgtileStore.bDumpLoaded = false;
2136   level = SpawnObject(GameLevel);
2137   level.setup(global, sprStore, bgtileStore);
2139   level.BuildYear = BuildYear;
2140   level.BuildMonth = BuildMonth;
2141   level.BuildDay = BuildDay;
2142   level.BuildHour = BuildHour;
2143   level.BuildMin = BuildMin;
2145   level.global = global;
2146   level.sprStore = sprStore;
2147   level.bgtileStore = bgtileStore;
2149   loadGameStats();
2150   //level.stats.introViewed = 0;
2152   if (level.stats.introViewed == 0) {
2153     startMode = StartMode.Intro;
2154     writeln("FORCED INTRO");
2155   } else {
2156     //writeln("INTRO VIWED: ", level.stats.introViewed);
2157     if (level.global.config.skipIntro) startMode = StartMode.Title;
2158   }
2160   level.onBeforeFrame = &beforeNewFrame;
2161   level.onAfterFrame = &afterNewFrame;
2162   level.onInterFrame = &interFrame;
2163   level.onLevelExitedCB = &levelExited;
2164   level.onCameraTeleported = &cameraTeleportedCB;
2166 #ifdef MASK_TEST
2167   maskSX = -0x0ff_fff;
2168   maskSY = maskSX;
2169   smask = sprStore['sExplosionMask'];
2170   maskFrame = 3;
2171 #endif
2173   sprStore.loadFont('sFontSmall');
2175   level.viewWidth = 320*3;
2176   level.viewHeight = 240*3;
2178   Video.swapInterval = (global.config.optVSync ? 1 : 0);
2179   //Video.openScreen("Spelunky/VaVoom C", 320*(fullscreen ? 4 : 3), 240*(fullscreen ? 4 : 3), fullscreen);
2180   fullscreen = global.config.startFullscreen;
2181   initializeVideo();
2183   //SoundSystem.SwapStereo = config.swapStereo;
2184   SoundSystem.NumChannels = 32;
2185   SoundSystem.MaxHearingDistance = 12000;
2186   //SoundSystem.DopplerFactor = 1.0f;
2187   //SoundSystem.DopplerVelocity = 343.3; //10000.0f;
2188   SoundSystem.RolloffFactor = 1.0f/2; // our levels are small
2189   SoundSystem.ReferenceDistance = 16.0f*4;
2190   SoundSystem.MaxDistance = 16.0f*(5*10);
2192   SoundSystem.Initialize();
2193   if (!SoundSystem.IsInitialized) {
2194     writeln("WARNING: cannot initialize sound system, turning off sound and music");
2195     global.soundDisabled = true;
2196     global.musicDisabled = true;
2197   }
2198   global.fixVolumes();
2200   level.restartGame(); // this will NOT generate a new level
2201   setupCheats();
2202   setupSeeds();
2203   performTimeCheck();
2205   texTigerEye = GLTexture.Load("teye0.png");
2207   if (global.cheatEndGameSequence) {
2208     level.winTime = 12*60+42;
2209     level.stats.money = 6666;
2210     switch (global.cheatEndGameSequence) {
2211       case 1: default: level.startWinCutscene(); break;
2212       case 2: level.startWinCutsceneVolcano(); break;
2213       case 3: level.startWinCutsceneWinFall(); break;
2214     }
2215   } else {
2216     switch (startMode) {
2217       case StartMode.Title: level.restartTitle(); break;
2218       case StartMode.Intro: level.restartIntro(); break;
2219       case StartMode.Stars: level.restartStarsRoom(); break;
2220       case StartMode.Sun: level.restartSunRoom(); break;
2221       case StartMode.Moon: level.restartMoonRoom(); break;
2222       default:
2223         level.generateNormalLevel();
2224         if (startMode == StartMode.Dead) {
2225           level.player.dead = true;
2226           level.player.visible = false;
2227         }
2228         break;
2229     }
2230   }
2232   //global.rope = 666;
2233   //global.bombs = 666;
2235   //global.globalRoomSeed = 871520037;
2236   //global.globalOtherSeed = 1047036290;
2238   //level.createTitleRoom();
2239   //level.createTrans4Room();
2240   //level.createOlmecRoom();
2241   //level.generateLevel();
2243   //level.centerViewAtPlayer();
2244   teleportCameraAt(level.viewStart);
2245   //writeln(Video.swapInterval);
2247   Video.runEventLoop();
2248   Video.showMouseCursor();
2249   Video.closeScreen();
2250   SoundSystem.Shutdown();
2252   if (doGameSavingPlaying == Replay.Saving) saveGameMovement(dbgSessionMovementFileName, packit:true);
2253   stopReplaying();
2254   saveGameStats();
2256   delete level;
2260 // ////////////////////////////////////////////////////////////////////////// //
2261 // duplicates are not allowed!
2262 final void checkGameObjNames () {
2263   array!(class!Object) known;
2264   class!Object cc;
2265   int classCount = 0, namedCount = 0;
2266   foreach AllClasses(Object, out cc) {
2267     auto gn = GetClassGameObjName(cc);
2268     if (gn) {
2269       //writeln("'", gn, "' is `", GetClassName(cc), "`");
2270       auto nid = NameToInt(gn);
2271       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));
2272       known[nid] = cc;
2273       ++namedCount;
2274     }
2275     ++classCount;
2276   }
2277   writeln(classCount, " classes, ", namedCount, " game object classes.");
2281 // ////////////////////////////////////////////////////////////////////////// //
2282 #include "timelimit.vc"
2283 //const int TimeLimitDate = 2018232;
2286 void performTimeCheck () {
2287 #ifdef DISABLE_TIME_CHECK
2288 #else
2289   if (TigerEye) return;
2291   TTimeVal tv;
2292   if (!GetTimeOfDay(out tv)) FatalError("cannot get time of day");
2294   TDateTime tm;
2295   if (!DecodeTimeVal(out tm, ref tv)) FatalError("cannot decode time of day");
2297   int tldate = tm.year*1000+tm.yday;
2299   if (tldate > TimeLimitDate) {
2300     level.maxPlayingTime = 24;
2301   } else {
2302     //writeln("*** days left: ", TimeLimitDate-tldate);
2303   }
2304 #endif
2308 void setupCheats () {
2309   return;
2311   startMode = StartMode.Alive;
2312   global.currLevel = 13;
2313   global.config.scale = 1;
2314   global.cityOfGold = true;
2315   return;
2317   startMode = StartMode.Alive;
2318   global.currLevel = 5;
2319   global.genBlackMarket = true;
2320   return;
2322   startMode = StartMode.Alive;
2323   global.currLevel = 2;
2324   global.scumGenShop = true;
2325   global.scumGenShopType = GameGlobal::ShopType.Weapon;
2326   //global.scumGenShopType = GameGlobal::ShopType.Craps;
2327   //global.config.scale = 1;
2328   return;
2330   //startMode = StartMode.Intro;
2331   //return;
2333   global.currLevel = 2;
2334   startMode = StartMode.Alive;
2335   return;
2337   global.currLevel = 5;
2338   startMode = StartMode.Alive;
2339   global.scumGenLake = true;
2340   global.config.scale = 1;
2341   return;
2343   startMode = StartMode.Alive;
2344   global.cheatCanSkipOlmec = true;
2345   global.currLevel = 16;
2346   //global.currLevel = 5;
2347   //global.currLevel = 13;
2348   //global.config.scale = 1;
2349   return;
2350   //startMode = StartMode.Dead;
2351   //startMode = StartMode.Title;
2352   //startMode = StartMode.Stars;
2353   //startMode = StartMode.Sun;
2354   startMode = StartMode.Moon;
2355   return;
2356   //global.scumGenSacrificePit = true;
2357   //global.scumAlwaysSacrificeAltar = true;
2359   // first lush jungle level
2360   //global.levelType = 1;
2361   /*
2362   global.scumGenCemetary = true;
2363   */
2364   //global.idol = false;
2365   //global.currLevel = 5;
2367   //global.isTunnelMan = true;
2368   //return;
2370   //global.currLevel = 5;
2371   //global.scumGenLake = true;
2373   //global.currLevel = 5;
2374   //global.currLevel = 9;
2375   //global.currLevel = 13;
2376   //global.currLevel = 14;
2377   //global.cheatEndGameSequence = 1;
2378   //return;
2380   //global.currLevel = 6;
2381   global.scumGenAlienCraft = true;
2382   global.currLevel = 9;
2383   //global.scumGenYetiLair = true;
2384   //global.genBlackMarket = true;
2385   //startDead = false;
2386   startMode = StartMode.Alive;
2387   return;
2389   global.cheatCanSkipOlmec = true;
2390   global.currLevel = 15;
2391   startMode = StartMode.Alive;
2392   return;
2394   global.scumGenShop = true;
2395   //global.scumGenShopType = GameGlobal::ShopType.Weapon;
2396   global.scumGenShopType = GameGlobal::ShopType.Craps;
2397   //global.scumGenShopType = 6; // craps
2398   //global.scumGenShopType = 7; // kissing
2400   //global.scumAlwaysSacrificeAltar = true;
2404 void setupSeeds () {
2408 // ////////////////////////////////////////////////////////////////////////// //
2409 void main () {
2410   checkGameObjNames();
2412   appSetName("k8spelunky");
2413   config = SpawnObject(GameConfig);
2414   global = SpawnObject(GameGlobal);
2415   global.config = config;
2416   config.heroType = GameConfig::Hero.Spelunker;
2418   global.randomizeSeedAll();
2420   fillCheatPickupList();
2421   fillCheatItemsList();
2422   fillCheatEnemiesList();
2424   loadGameOptions();
2425   loadKeyboardBindings();
2426   runGameLoop();